With a little help from my favorite AI - How to partition an array into two

When trying out jazz and blues licks I prefer a notation with note and duration in one single array, someting like

g_melody = [
  :F4,  1.5,
  :E4,  0.5,
  :D4,  1.0,
  :E4,  1.0,
  :r,   0.5,
  :Fs4, 1.0,
  :D4,  1.0,
  :B3,  0.5,
  :B3,  0.5
]

This way my hands don’t have to type many braces and it’s clear, which duration belongs to which note. Unfortunately, if I want to play it quickly, there is play_pattern_timed. Unfortunately, this method takes two arrays, one with the notes and one with the durations.
I was searching a quick transformation of my melody array to the two arrays needed for play_pattern_timed. After playing around with .each_cons and .map without success, I went back to the good oldfashioned loop:

g_notes = []
g_durations = []

g_melody.each.with_index do |p_item, p_index|
  if p_index.even? then
    g_notes.push(p_item)
  else
    g_durations.push(p_item)
  end
end

print g_notes
print g_durations

This works, but it is rather clumsy. In the end I asked my favourite AI - perplexity - for a more compact and elegant solution. It came up with a ruby function .partition.with_index I’ve never heard of. I couldn’t even find it in the official ruby documentation. With this the solution became a one-liner. Here is the full code:

use_synth :piano
use_bpm 160

g_melody = [
  :F4,  1.5,
  :E4,  0.5,
  :D4,  1.0,
  :E4,  1.0,
  :r,   0.5,
  :Fs4, 1.0,
  :D4,  1.0,
  :B3,  0.5,
  :B3,  0.5
]

# make two arrays, one for the notes and one for the durations
g_notes, g_durations = g_melody.partition.with_index { |_, i| i.even? }
print g_notes
print g_durations

play_pattern_timed g_notes, g_durations

As I was playing around with the code, I realized that it would be even easier to pack the note values and note lengths into a simple string separated by spaces. The only difficulty here is the type conversion to come from a note name like c4 to a symbol like :C4 and from a string like “0.5” to a float number 0.5.

This conversion from a string to play_pattern_timed looks like this:

# Convert melody string with space separated note values and duration
# to arrays for play_pattern_timed
# SR 30.11.2025
# File: melody_string_to_array_for_play_pattern_timed.txt

use_synth :piano
use_bpm 120

g_melody_string = "f4  1.5 e4  0.5 d4 1.0 e4  1.0"
print g_melody_string

# mache einen Array
g_melody = g_melody_string.split(" ")
print g_melody

# mache aus g_melody 2 Arrays fuer Notenwerte als String und Durations
g_note_strings, g_duration_strings = g_melody.partition.with_index { |_, i| i.even? }

# verwandle die String-Notenwerte in Symbols
g_notes = g_note_strings.map {|p_item| p_item.to_sym}
print g_notes

g_durations = g_duration_strings.map {|p_item| p_item.to_f}
print g_durations

play_pattern_timed g_notes, g_durations

As you can see in my example above, this code is robust even if you use more than one space as separation.

An last but not least let’s pack this code into a function:

define :f_play_pattern_timed_from_string do |p_melody_string|
  l_melody = p_melody_string.split(" ")
  l_note_strings, l_duration_strings = l_melody.partition.with_index { |_, i| i.even? }
  l_notes = l_note_strings.map {|p_item| p_item.to_sym}
  l_durations = l_duration_strings.map {|p_item| p_item.to_f}
  play_pattern_timed l_notes, l_durations
end

f_play_pattern_timed_from_string "c4 1 c4 1 d4 1 e4 1 e4 1 d4 1 c4 1 b3 2"

There’s an easier way with each_slice:

arr = [1, 2, 3, 4, 5, 6]

newarr = []

# Iterate over consecutive pairs as array
arr.each_slice(2) do |a,b|
  newarr << [a, b]
end

puts newarr

If you want to generate overlapping pairs, use each_cons:

arr = [1, 2, 3, 4, 5, 6]

newarr = []

# Iterate over consecutive pairs as array
arr.each_cons(2) do |a,b|
  newarr << [a, b]
end

puts newarr

Hi Harry, thanks for your input, but your example does not give the same result as the last code in my first post. For play_pattern_timed you need two arrays, one for the note names and one for the durations. Your code produces one nested array with each note name and duration as a subarray. Your structure makes sense, if you use a loop to play the notes and want to set other options like amp or sustain.

The elegant thing about partition.with_index is that it generates the two arrays for play_pattern_timed with one single line of code:

g_notes, g_durations = g_melody.partition.with_index { |_, i| i.even? }

As I use this code mostly to control the timing of jazz and blues licks that I try to play by ear, I don’t care about amp and all the other stuff. The only thing that interests me, is, whether I got the timing of the lick right.

To have the same with your suggested each_slice, it would be the following:

arr = [
  :F4,  1.5,
  :E4,  0.5,
  :D4,  1.0,
  :E4,  1.0,
  :r,   0.5,
  :Fs4, 1.0,
  :D4,  1.0,
  :B3,  0.5,
  :B3,  0.5
]


g_notes = []
g_durations = []

# Iterate over consecutive pairs as array
arr.each_slice(2) do |a,b|
  g_notes << a
  g_durations << b
end

puts g_notes
puts g_durations

This might be easier to understand, but it is considerably longer

Fair enough. (Though I already have a utility function to turn rows into columns.)

I wasn’t familiar with the partition method. I want to dig into that.

That’s what I love about ruby. There’s a bunch of ways to solve the same problem.

BTW, here is the rowstocolumns method, and a couple of related methods:

define :rowstocolumns do |*thesearrays|
  # debugprint "top of rowstocolumns"
  # debugprint "thesearrays: ", thesearrays
  results = []
  if thesearrays.length == 1  && thesearrays[0].length > 1  
    # debugprint "stripping outer array"
    thesearrays = thesearrays[0] #get rid of outer wrapping array
  end #if wrapped in an outer array
  thesearrays.each_with_index do |thisarray, j|
    thisarray.each_with_index do |thisitem, i|
      (results[i] ||= [])[j] = thisitem 
    end #each thisitem
  end #each thisarray
  # debugprint "results: ", results
  results #return value
end #define rowstocolumns

# paddedrowstocolumns
# pads all arrays to the same length, repeating values in shorter arrays,
# then passes the arrays to rowstocolumns. 
# *thesearrays: the arrays to pad

define  :paddedrowstocolumns do |*thesearrays|

  # debugprint "top of paddedrowstocolumns"
  if thesearrays.length == 1  
    thesearrays = thesearrays[0] #get rid of outer wrapping array
  end #if wrapped in an outer array

  maxlength = 0
  thesearrays.each do |thisarray|
    maxlength = thisarray.length if thisarray.length > maxlength 
  end #each thisarray
  thesearrays.each do |thisarray|
    if thisarray.length < maxlength
      originallength = thisarray.length  
      (0..maxlength-1).each do |i|
        thisarray[i] = thisarray[i % originallength]
      end #each i
    end #if thisarray.length < maxlength
  end #each thisarray


  rowstocolumns *thesearrays #return value
end #define paddedrowstocolumns


# nilpaddedrowstocolumns
# pads all arrays to the same length, adding trailing nils in shorter arrays,
# then passes the arrays to rowstocolumns. 
# *thesearrays: the arrays to pad

define  :nilpaddedrowstocolumns do |*thesearrays|

  # debugprint "top of nilpaddedrowstocolumns"
  # debugprint "thesearrays: ", thesearrays
  if thesearrays.length == 1  
    thesearrays = thesearrays[0] #get rid of outer wrapping array
  end #if wrapped in an outer array

  # maxlength = 0
  # thesearrays.each do |thisarray|
  #   maxlength = thisarray.length if thisarray.length > maxlength 
  # end #each thisarray
  # thesearrays.each do |thisarray|
  #   if thisarray.length < maxlength
  #     originallength = thisarray.length  
  #     (0..maxlength-1).each do |i|
  #       thisarray[i] = thisarray[i % originallength]
  #     end #each i
  #   end #if thisarray.length < maxlength
  # end #each thisarray

  # debugprint "checking whether we need to pad with trailing nils"
  maxlength = 0
  minlength = 100000000000
  thesearrays.each do |thisarray|
    maxlength = thisarray.length if thisarray.length > maxlength 
    minlength = thisarray.length if thisarray.length < minlength  
  end #each thisarray
  # debugprint "maxlength: ", maxlength
  # debugprint "minlength: ", minlength

  needtrailingnils = (minlength < maxlength)  
  # debugprint "needtrailingnils: ", needtrailingnils 
  if needtrailingnils  
    # debugprint "need trailing nils"
    thesearrays.each do |thisarray|
      # debugprint "thisarray: ", thisarray
      # debugprint "maxlength: ", maxlength
      while thisarray.length < maxlength  
        # debugprint "thisarray.length: ", thisarray.length
        # debugprint "adding trailing nil"
        thisarray << nil 
        # debugprint "thisarray: ", thisarray
      end #while thisarray.length < maxlength
      # debugprint "thisarray: ", thisarray
    end #each thisarray
    # debugprint "thesearrays: ", thesearrays
  else 
    # debugprint "don't need trailing nils"
  end #if needtrailingnils

  rowstocolumns *thesearrays #return value
end #define paddedrowstocolumns


Agreed. Speaking of which, you could optionally use Ruby’s Array#transpose in your rowstocolumns implementation :slight_smile:

D’oh!

Nothing like reinventing the wheel. :slight_smile:
Thanks for pointing this out.

Hi there,

it’s important to point out that you’re making a lot of use of standard Ruby - and whilst that may work, it’s not supported and may not work in future versions. The official line is that only the code in the tutorial is supported. However, if you’re happy with that caveat, feel free to use whatever works.

Nice trick with the partition.with_index—Ruby’s enum chaining is very useful but not well known. If you want to squeeze it even tighter for sonic-pi stuff, use each_slice(2).Without relying on index parity, to_a. transpose gives the same notes and durations split. However, your way is easier to read when looking at melodies.

Thanks Susan for the typ with each_slice. I tried it out with chords:

# Making a nested array from an array by taking regular slices
# 31.12.2025
# Sources:
# - https://in-thread.sonic-pi.net/t/with-a-little-help-from-my-favorite-ai-how-to-partition-an-array-into-two/9895/9
# 
# File: sr_lang_each_slice.txt

g_melody_arr = [
  :fs3, '7-10', 4,
  :b3, :minor, 2,
  :b3, :m6, 2
]

g_chords_arr = g_melody_arr.each_slice(3).map{|p_slice| p_slice}
print g_chords_arr

use_synth :piano
g_chords_arr.each do |p_chord|
  play_chord chord(p_chord[0], p_chord[1]), decay: p_chord[2]
  sleep p_chord[2]
end