Two-Hand Piano Theme Generator

Hi everyone, first post here and a newbie to Sonic Pi but not new to music production, programming, and composition.

Inspiration sometimes eludes me so I built a quick melody generator to get the juices flowing. I know that there is quite a bit of great work already on the boards here regarding melody generation but I figured I’d throw in my hat as well.

# Piano Theme Generator

# Generator drivers
use_random_seed 4137001          # Random seed changes everything
use_bpm 90                       # Set your bpm to taste
use_synth :piano

PATTERN_REPEAT_CHANCE = 0.2     # Chance of a rhythm division repeating within the rhythm
REST_CHANCE = 0.2               # Chance of a rest being selected in a melody
ROOT = :g5
MODE = :locrian


# Creates notes and durations given a scale, beats, units and the largest beat.
# Return array is an array of note, duration pairs
define :melodyPatternMaker do |myScale, beats, units, largestBeat|
  pattern = make_rhythm_pattern(beats, units, largestBeat)
  melody = create_melody(myScale, pattern.length)
  return melody.zip(pattern)
end

# Pattern division constants
# 4     whole note
# 3     dotted half note
# 2     half note
# 1.5   dotted quarter note
# 1     quarter note
# 0.75  dotted eight note
# 0.5   eight note
# 0.375 dotted sixteenth note
# 0.25  sixteenth note
ALL_PATTERN_BEAT_DIVISIONS = [4, 3, 2, 1.5, 1, 0.75, 0.5, 0.375, 0.25]

# Returns a random beat pattern less than or equal to the time division passed in
define :pickRandomPatternLessThanEqualTo do |lessThanTime|

  chosenDuration = ALL_PATTERN_BEAT_DIVISIONS.choose
  if chosenDuration > lessThanTime
    chosenDuration = lessThanTime
  end

  return chosenDuration

end

# Function that makes a rhythm pattern of various durations
# Beats define the number of beats in the pattern
# Unit defines the unit of beat (just like a time signature)
# Low limit defines the lower limit of the beat division to be used - pass 1 for quarter notes and so on...
define :make_rhythm_pattern do |beats, unit, low_limit|

  # Empty result for the
  result = []

  # The total time in beats we need to fill
  total_time = 4 * beats / unit

  # Select the first duration
  #chosenDuration = ALL_PATTERN_BEAT_DIVISIONS.choose
  chosenDuration = pickRandomPatternLessThanEqualTo(low_limit)
  result.push chosenDuration
  total_time -= chosenDuration

  # While we have time left
  while total_time > 0

    # If we have enough remaining time to cover the same chosen duration
    if total_time >= chosenDuration && rand() < PATTERN_REPEAT_CHANCE
      result.push chosenDuration
      total_time -= chosenDuration
    else
      chosenDuration = pickRandomPatternLessThanEqualTo([total_time, low_limit].min)
      result.push chosenDuration
      total_time -= chosenDuration
    end # end chosenDuration if
  end # end while

  # Return the resultant array of durations
  return result

end #end function

# Creates a random melody from a scale of length
define :create_melody do |theScale, length|
  myNotes = []
  for i in 0...length
    if rand() < REST_CHANCE
      myNotes.push :r
    else
      myNotes.push theScale.choose
    end
  end
  return myNotes
end

define :playPattern do |pattern|
  for i in 0...pattern.length
    play pattern[i][0], sustain: pattern[i][1]
    sleep pattern[i][1]
  end
end

# Click for reference
live_loop :click do
  loop do
    sample :perc_snap
    sleep 1
    3.times do
      sample :perc_snap2, amp: 0.25
      sleep 1
    end
  end
end


# High piano does a 4 bar pattern
live_loop :pianoHigh do
  highPiano8 = melodyPatternMaker(scale(ROOT, MODE, num_octaves: 1.5), 16, 4, 4)
  puts "High Piano" + highPiano8.to_s
  with_fx :reverb, room: 0.8 do
    loop do
      playPattern(highPiano8)
    end
  end
end

# Lower piano does a 2 bar pattern
live_loop :pianoLow do
  lowPiano4 = melodyPatternMaker(scale(ROOT - 24, MODE, num_octaves: 1.5), 8, 4, 4)
  puts "Low Piano" + lowPiano4.to_s
  with_fx :reverb do
    loop do
      playPattern(lowPiano4)
    end
  end
end

Feedback, comments, suggestions welcome.

2 Likes

Hey hey @heavylift! welcome. I like the sounds in this :slight_smile:

There are possibly a few optimisations that could be made here and there, but I think the one that stands out the most is that you are nesting loops inside live_loops. This is most likely unnecessary - live_loops are loops after all :grinning_face_with_smiling_eyes:

I had a go at removing the nested loops, and it still plays fine, though the melody is slightly different, because you are then creating a new melody every time around the loop. Depending on your goals, you may choose to change that, so that it still chooses a single melody to repeat each time around the loop for example.

Thanks @ethancrawford. Still working my away around control constructs and techniques for this language. The melody generation could happen anywhere, so yeah makes sense to take out that extra loop and perhaps find a different place to stick the melody generation. Thanks for the suggestion.

If you’re not making changes whilst it’s running then maybe the external part could be an in_thread rather than a live_loop?

in_thread(name: :pianoLow) do
  lowPiano4 = melodyPatternMaker(scale(ROOT - 24, MODE, num_octaves: 1.5), 8, 4, 4)
  puts "Low Piano" + lowPiano4.to_s
  with_fx :reverb do
    loop do
      playPattern(lowPiano4)
    end
  end
end

This is pretty nifty. When you’re using it, do you play around with random seed until you find something you like, or do you tend to change the other variables?

I’m particularly interested in this kind of thing at the minute. Getting the machine to generate more interesting patterns. Less regular than scales and arpeggios, less random than random picks of notes. Inspired by some of those modular modules that do that.

1 Like

Thanks for the recommendation!

I’ve tried both. I’ve tried changing the length of the patterns, number of octaves, and keys. All lead to fairly interesting results. Its also interesting when you use different time signatures over each other.