Combining Rings Using Boolean Operations

Hey there! After a long break I’m fiddling with Sonic Pi again.

I’m currently looking into defining rhythmic patterns as a series of boolean rings, some handwritten, some generated using spread. I’d like to eventually combine the rings together to output a final rhythm using boolean operators, but from what I can tell there’s no easy way to do this.

I’d expect to be able to do something like

a = (spread 1, 8)
b = (spread 1, 8, rotate: 2)

c = a & b

but this returns a ring with 2 values and I’m not sure how they’re calculated. I could likely initialize a new ring, then loop through it comparing values in the first two to get the final output but this feels hacky.

Is there a simpler way to create a ring using boolean operations on two existing rings?

I’ve created a function that implements what I’m looking for manually, but I feel like there should be a language construct for this?

define :ring_OR do |ring_a, ring_b|
  length = ring_a.length
  working_list = Array.new
  i = 0
  
  length.times do
    working_list[i] = ring_a[i] | ring_b[i]
    i = (inc i)
  end
  
  return working_list.ring
  
end

## Example
a = (spread 1, 8)
b = (spread 1, 8).rotate(4)

c = ring_OR(a, b)

puts a
puts b
puts c

Realize I’m on a 3rd reply here, but here’s a more comprehensive implementation. Still feels clunky.

define :ring_bitwise do |ring_a, ring_b, operator|
  length = ring_a.length
  working_list = Array.new
  i = 0
  
  length.times do
    case operator
    
    when "or"
      working_list[i] = ring_a[i] | ring_b[i]
      
    when "and"
      working_list[i] = ring_a[i] & ring_b[i]
      
    when "xor"
      working_list[i] = ring_a[i] ^ ring_b[i]
      
    end
    
    i = (inc i)
  end
  
  return working_list.ring
  
end

## Example
a = (spread 2, 4)
b = (spread 2, 4).rotate(1)

c = ring_bitwise(a, b, "or")

puts "or"
puts a
puts b
puts c

a = (spread 2, 4)
b = (spread 4, 4)

c = ring_bitwise(a, b, "and")

puts "and"
puts a
puts b
puts c

a = (spread 2, 4)
b = (spread 4, 4)

c = ring_bitwise(a, b, "xor")

puts "xor"
puts a
puts b
puts c

Hi @Skoddie
I think your OR function is the most concise. As a musician, and novice coder, I often find myself looking for ways to ‘merge’, or hybridise, various arrays into one. I have exhausted stackoverflow, and other web resources, and tried zip, flatten, compact etc. without success.

This is a musical and coding challenge for me/us: how to combine several arrays, of length x, into one array, of the same length. As a novice coder I realise this is not a trivial operation, and your first reply looks like a clear and concise solution.

& returns the elements common to both arrays

a = [1,2,3,4]
b = [2,3,4,5]

puts a&b # ==> [2,3,4]

Happy to be corrected though :wink:

PD-Pi

I use “+” between two spread() usually. What does “&” do?


/ Two fuctions that shortens "spread() to s()" and "rotate() to r()"/
define :s do |a,b|
  spread(a,b)
end

class SonicPi::Core::RingVector # r() only works on s() - doesnt work on an array like [].r()
  def r(n)
    self.rotate(n)
  end
end


#Goal
# a = (spread 1, 8)
# b = (spread 1, 8, rotate: 2)
# c = a & b



use_bpm  60
live_loop :a1 do
  tick
  
  #sample :bd_fat             if (s(1,8)+s(0,8).r(2)).look
  #sample :bd_fat, rpitch: 24 if (s(0,8)+s(1,8).r(2)).look
  
  sample :bd_fat if (s(3,5)*3+s(0,7).r(2)).look
  sample :bd_fat, rpitch: 24 if (s(0,5)*3+s(5,7).r(2)).look
  
  sleep 0.25
end

1 Like

& is a bitwise AND comparison. Essentially I’m using rings as bitmasks, and then doing bitwise operations to get a final bitmask that I can use as a rhythm.

Using + will concatenate the rings, and as @brendanmac noted above & will only return common values.

The more I investigate this the more it seems there aren’t builtins. I’m considering reimplementing this without using rings, and then translating into a ring at the end due to performance issues.

In other languages, like C this construct is fairly easy.
0b1000100010001000 | 0b1001001001001000 = 0b1001101011001000

This is also available in Ruby, I just need to represent it as decimal integers rather than binary values. It, unfortunately, feels inelegant, but it doesn’t seem like it’s currently within the scope of the language.

1 Like

How would I do bitwise OR in Ruby/SonicPi? For example:

a = [1,0,0,0]
b = [0,0,1,0]
c = a | b

also removes duplicates, but I would like [1,0,1,0] for c.

Apologies for hijacking your post, but I’ve been chasing a solution for months.

PD-Pi

You can do it natively in Ruby with ints by converting from binary to an int.

To rewrite my example from above:

34952 | 37448 = 36924

Then you could populate a ring by using % 2 to pull out the LSB, put it into a ring, right shift and do it again until you run out of bits.

Just seeing this thread—I’ve had the same general thought but never got around to implementing it. The bitwise approach is clever!

My first thought is to use Ruby’s map method, which is much more concise than the explicit iteration in most of the code posted above:

define :ringand do | c, d |
  l = c.length() - 1
  return (0..l).map { |x| c[x] & d[x] }.ring
end

define :ringor do | c, d |
  l = c.length() - 1
  return (0..l).map { |x| c[x] | d[x] }.ring
end

a = (spread 2, 16)
b = (spread 1, 3)

puts a
puts b
puts ringand(a, b)
puts ringor(a, b)

output:

 ├─ (ring true, false, false, false, false, false, false, false, true, false, false, false, false, false, false, false)
 ├─ (ring true, false, false)
 ├─ (ring true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false)
 └─ (ring true, false, false, true, false, false, true, false, true, true, false, false, true, false, false, true)

This isn’t as elegant as it could be. I’d prefer to write the function once and pass the Boolean operator as a symbol (similarly to how you’ve done it above, but without having to translate from an intermediate string like "and"), but Sonic Pi doesn’t seem to like any of my attempts to make that happen. Maybe someone better at Ruby can make it work. And ideally there would be an infix syntax, but that seems like WAY more trouble than it’s worth.

Note that this implementation works fine with rings of different sizes! And perhaps a little more intuitively than the equivalent using bitwise arithmetic. It’s not commutative in those cases, but otherwise I expect it’s mathematically well behaved.

I had a little play around and managed to monkey-patch the ring class to add the logical operators to it:

# Monkey-patch the base class of ring-like classes:
class SonicPi::Core::SPVector
  def |(other)
    self.class.new(@vec.zip(other.vec).map {|a,b| a | b})
  end
  def &(other)
    self.class.new(@vec.zip(other.vec).map {|a,b| a & b})
  end
  def ^(other)
    self.class.new(@vec.zip(other.vec).map {|a,b| a ^ b})
  end
  def !()
    self.class.new(@vec.map {|a| !a})
  end
end

# Create a couple of rings:
r1 = spread(3, 8)
puts "R1 ", r1
r2 = spread(5, 8)
puts "R2 ", r2

# Combine them:
puts "NOT", !r2
puts "AND", r1 & r2
puts "OR ", r1 | r2
puts "XOR", r1 ^ r2

Output:

{run: 1, time: 0.0}
 ├─ "R1 " (ring true, false, false, true, false, false, true, false)
 ├─ "R2 " (ring true, false, true, true, false, true, true, false)
 ├─ "NOT" (ring false, true, false, false, true, false, false, true)
 ├─ "AND" (ring true, false, false, true, false, false, true, false)
 ├─ "OR " (ring true, false, true, true, false, true, true, false)
 └─ "XOR" (ring false, false, true, false, false, true, false, false)

2 Likes

:boom: Wow, @emlyn, that is really clean! And much easier than I expected. Thanks for sharing that. Would this “monkey-patching” technique work to add new “data” elements to things like scale or chord too, or only methods? (I imagine it’s been illustrated here before, so I will also just go back and search at next opportunity.)

I notice that if the first ring is longer than the second, the .zip method will supply nil for the request of the sequence. So

puts spread(1, 8) | spread(1, 3)

gives

 └─ (ring true, false, false, false, false, false, false, false)

which is different from what my method did above, but no less intuitive.

1 Like

Good point about the different length rings.

You can add some data with a couple of methods to set and get it like this (but be aware all this is getting into highly unsupported territory, so could break in any new versions):

class SonicPi::Core::SPVector
  def data()
    @data
  end
  def set_data(d)
    @data = d
  end
end

a = (spread 5, 8)
puts "before", a.data
a.set_data 42
puts "after", a.data

{run: 2, time: 0.0}
 ├─ "before" nil
 └─ "after" 42