Midilib for Sonic Pi and Generating markov chains from midis

I created a fork of minilib that now runs with ruby 3.x and Sonic Pi.

Also included this ‘forgotten’ feature request to my fork which makes it possible to read the midi from the file and create a markov chain from the parsed melody directly in Sonic Pi:

Here is a neverending jazzy version of old Finnish folk tune:

# Path to midilib for Sonic Pi
load "~/midilib/sonicpi.rb"

# Path to example midi in the examples folder
test_midi = ENV['HOME']+'/midilib/examples/pinnin_valssi.mid'

use_bpm 180

# Parse to hash
midi_hash = midi_to_hash test_midi

melody = midi_hash[:tracks][0].map {|n| n[:note] }
chords = midi_hash[:tracks][1].map {|n| n[:note] }

sleep = midi_hash[:sleeps][0]
chord_sleep = midi_hash[:sleeps][1]

notes_and_lengths = melody.zip(sleep)
chords_and_lengths = chords.zip(chord_sleep)

# Create markov chains from the melody and the chords
melody_chain = markov(notes_and_lengths, 2)
chord_chain = markov(chords_and_lengths, 1)

with_fx :wobble, phase: 1.0, res: 0.5 do
  in_thread do
    loop do |n|
      a = melody_chain.next || melody_chain.reset
      synth :chiplead, note: a[0], attack: 0.1, release: 0.1, amp: 1
      sleep a[1]
    end
  end
  
  loop do |n|
    b = chord_chain.next || chord_chain.reset
    synth :chipbass, note: b[0], attack: 0.1, release: 0.1, amp: 1
    sleep b[1]
  end
end

To run this first “clone https://github.com/amiika/sonicmidilib.git” in your user folder and run the example in Sonic Pi (Or clone elsewhere and change the paths).

EDIT: Changed repository and cleaning up the fork to create a pull request to the original.

6 Likes

Thanks for this, looks great! It will take some time to understand all the code behind…

1 Like

My pull request to midilib went trough which means you can now use midilib with Sonic Pi also from the original repository:

jimm/midilib: Pure Ruby MIDI file and event manipulation library (github.com)

3 Likes

Hi @amiika, this looks awesome and I would like to try this out, so I’ve cloned the original midilib repository, and installed midilib as a gem. Then I had to add your ‘pinnin_valssi.mid and sonicpi.rb files, that where not included from your fork. I’ve changed the paths successfully, no problems there, but when evaluating the example I get stuck at the ‘#create markov chains’ part. I get this → undefined method `markov’ for Runtime:SonicPiLang.
Do I have to define the markov function somehow first to run this ? Or something else to get this going ? Cheers!

My original fork should still be working. I added the markov.rb from the Sonic Pi Feature request to lib folder in there.

I also made the pull request to the original, which means it’s working when required from the path. You don’t have to install the gem as those does not work with Sonic Pi.

Meanwhile … made some new changes to the midilib to include sleep and sustain, which would make it easier to use with Sonic Pi. These changes are in my new fork and don’t know yet if those will be accepted:

load "~/midilib/lib/midilib.rb"

use_bpm 180

# Path to midi
TEST_MIDI = ENV['HOME']+'/sonicmidilib/examples/pinnin_valssi.mid'

seq = MIDI::Sequence.new()

File.open(TEST_MIDI, 'rb') do |file|
  seq.read(file)
end

seq.each_with_index do |track,idx|
  track = track.select { |e| e.is_a?(MIDI::NoteOn) }
  in_thread do
    track.each do |e|
      play e.note, sustain: e.sustain*0.8, decay: e.sustain*0.2
      sleep e.sleep
    end
  end
end

Ok, thanks! I’ll try to use your forks instead and see if I get things going. Cheers!

One thing I noticed with your excellent port is that it can’t handle multiple parts if they start playing at different times from the start. EG in a fugue part 2 might come in 4 bars later than part 1. I deal with this with midi files exported from MuscScore by putting in dummy very high or very low notes each of a bars duration instead of each bar rest, then running a conversion script written in procesing to Sonic Pi code, then substituting the dummy notes back with bars rests. A bit tedious but it works.

2 Likes

@amiika Works from your original fork! Going to investigate further now . . .

1 Like

That should not be a problem, at least in my new fork, but I have to test it out. Could you post a link to some example from Musescore? You can get time_from_start value from the Events, so I quess you have to put a condition that sleeps in the beginning if time_from_start>0.

Sent you a message with a link
EDIT
also, some files have tempo changes within them. Will it handle those?

Great piece. I made some changes to my midilib fork to make it work. Renamed sleep to wait and switched the order around. It makes more sense this way.

Playing multiple parts:

load "~/midilib/lib/midilib.rb"

test_bpm = 200

use_bpm test_bpm

# Path to midi
TEST_MIDI = ENV['HOME']+'/midis/Danse_Macabre_Op.40.mid'

seq = MIDI::Sequence.new()

File.open(TEST_MIDI, 'rb') do |file|
  seq.read(file)
end

seq.each_with_index do |track,idx|
  track = track.select { |e| e.is_a?(MIDI::NoteOn) }
  in_thread do
    track.each do |e|
      with_synth :piano do
        sleep e.wait
        play e.note, release: e.sustain*0.8, decay: e.sustain*0.2
      end
    end
  end
end

Also added bpm function to tempo, which does the same thing as @theibbster did earlier with his script. Except im using separate threads for different types of events. Example with tempo changes:

load "~/midilib/lib/midilib.rb"

test_bpm = 80

use_bpm test_bpm

# Path to midi
TEST_MIDI = ENV['HOME']+'/midis/Tempo_Changes_test.mid'

seq = MIDI::Sequence.new()

File.open(TEST_MIDI, 'rb') do |file|
  seq.read(file)
end

seq.each_with_index do |track,idx|
  tempo = track.select { |e| e.is_a?(MIDI::Tempo) }
  track = track.select { |e| e.is_a?(MIDI::NoteOn) }
  in_thread do
    track.each do |e|
      with_bpm test_bpm do
        sleep e.wait
        play e.note, sustain: e.sustain*0.8, decay: e.sustain*0.2
      end
    end
  end
  in_thread do
    tempo.each do |t|
      sleep t.wait
      test_bpm = test_bpm+(t.bpm-test_bpm)
    end
  end
end

Potentially you could also have different events like Pitch bend, Program or Control change Events also.

Danse macabre sounds great! Well done.