Midi to Sonic Pi script (Experimental proof of concept)


This is a subject that has been tackled before by Robin Newman.

This is my own, initial, take on it. I’ve written a ruby script that takes a midi file, then outputs a ruby file that is usable by sonic pi.

Why I’m doing this:

  1. Just for the fun of it
  2. To get a better understanding of the process by doing it myself
  3. Having a ruby script for this could be cool for the community since our ruby knowledge will allow us to hack it more easily

Here’s part of an Ed Sheeran cover I made using this.

To make this I used the midi file downloaded from here.

This was the output.

Script information

How to use the script:

  1. Make sure you have Ruby 2.7.x installed (maybe it works with 3, I haven’t tried it)
  2. Install the midilib gem from the terminal with gem install midilib
  3. Put the script and the midi file in the same folder
  4. Navigate to the folder from the terminal and run the program. For example: ruby .\midi-sonic.rb .\Ed_Sheeran_Perfect_Piano_Cover.mid
  5. This will make a file called out.rb. It’s likely to big for Sonic Pi to run, so use the run_file command in Sonic Pi to run it.

If you want to make changes, like the synth, you will probably have to edit the file directly (because it’s too big for Sonic Pi). Of course, you could also just copy a portion of the notes or whatever and play with that.

1 Like

This is cool! Makes a good stab at the conversion. I’ve found some conversion problems, particularly in timing parts which start after a pause of several bars, but at least it gets the note streams for you and is a good start for polishing up the final file.

1 Like

Thanks for your feedback, Robin! I would like to work on it a bit more. I will try to look at the problematic parts and find ways to resolve them.

Yesterday I really just wanted to come up with something that somewhat worked :grin:

Do you have any ideas on how I could improve the process? This is what I do right now, for each individual track.

  1. Filter the track so I only have the Note On events
  2. Determine the length of the note by comparing it to its Note Off event. I’m using the API from the midi parsing for this, so I’m not sure yet on its correctness.
    • At this point, I also convert the duration from ticks to beats.
  3. Determine the sleep duration by comparing to the next Note On event.

I’m also thinking I might take a stab at doing the same thing with a MusicXML file. I have a feeling it might be easier but I’m sure it will come with its own challenges.

I’ll need to play further. NExt I was going to try it with a mixture of my process. I usually prepare the piece in musescore then export as individual track files to musicxml, then convert those. But my script has shortcoming too. I was going to try your script by exporting individual tracks as midi from MuseScore, then converting them separately, One difficulty is in dealing with tempo changes. Not checked to see if your script copes with those or not. EDIT my script can only handle these if they occur at the same time for every part.

Well, do let me know if there’s anything I can change to help you out. I’m happy to make some customisations to suit your project.

I don’t think it would handle a tempo change as it is. Right now it checks the “pulse per quarter note”, which the midilib library has an API for. It then converts all midi timings into beats using that value. I am also only checking the BPM once, right now I’m not too sure where else to check for the tempo but I’m sure something could be figured out as I understand the midi format better :smiley:

The other way to attempt this is to look at the midi->csv conversion and work with that. I believe that is what @vinodv is doing, although he hasn’t released code. However the csv format file is useful to see how midi handles tempo in general. I’ve installed midicsv on my Mac via brew, and have been looking at this output.

One easy change to make to make post your script is to the sustain: values when plauying the notes. I find it is better to use sustain: rather than decay: for percussive synths like pluck or piano, and to use a variable ration of sustain: decay: for other synths eg 80% 10%. You could alter the script to give sustain: * s , decay: * (s-1) with s specified at the start. for example. (For percussive just hac sustain" and don’t specify :decay at all.

I’ve not made much progress with improving the script but I have created a debugger of sorts. It’s not very user friendly but it outputs the results of the original script in a human-readable way.


Each of the above blocks is about 4 beats.

The main issue I’ve identified is if you have several parts and say the second part starts after 4 bars rest then this rest is not included. Your script always assumes the part starts with the first note at time 0.
Its easy enough to fix by putting a suitable sleep inside the thread relevant to the delayed part (at the top) before the loop starts.

Oh, I understand now! I’ve updated the script to take this into account. In addition:

  • The script has more comments this will, hopefully, make it easier for others (and future Ibby) to use.
  • The script has options for setting the attack, sustain and release ratios.

I tested this with a version of Brahm’s Lullaby. This file is small enough to run in Sonic Pi, without run_file.

Note that the synths were added after the file was generated.

use_bpm 100

melody0 = [{:note=>64, :duration=>0.47291666666666665}, {:note=>64, :duration=>0.47291666666666665}, {:note=>67, :duration=>1.8979166666666667}, {:note=>64, :duration=>0.47291666666666665}, {:note=>64, :duration=>0.47291666666666665}, {:note=>67, :duration=>1.8979166666666667}, {:note=>64, :duration=>0.47291666666666665}, {:note=>67, :duration=>0.47291666666666665}, {:note=>72, :duration=>0.9479166666666666}, {:note=>71, :duration=>1.4229166666666666}, {:note=>69, :duration=>0.47291666666666665}, {:note=>69, :duration=>0.9479166666666666}, {:note=>67, :duration=>0.9479166666666666}, {:note=>62, :duration=>0.47291666666666665}, {:note=>64, :duration=>0.47291666666666665}, {:note=>65, :duration=>1.8979166666666667}, {:note=>62, :duration=>0.47291666666666665}, {:note=>64, :duration=>0.47291666666666665}, {:note=>65, :duration=>1.8979166666666667}, {:note=>62, :duration=>0.47291666666666665}, {:note=>65, :duration=>0.47291666666666665}, {:note=>71, :duration=>0.47291666666666665}, {:note=>69, :duration=>0.47291666666666665}, {:note=>67, :duration=>0.9479166666666666}, {:note=>71, :duration=>0.9479166666666666}, {:note=>72, :duration=>1.8979166666666667}, {:note=>60, :duration=>0.47291666666666665}, {:note=>60, :duration=>0.47291666666666665}, {:note=>72, :duration=>1.8979166666666667}, {:note=>69, :duration=>0.47291666666666665}, {:note=>65, :duration=>0.47291666666666665}, {:note=>67, :duration=>1.8979166666666667}, {:note=>64, :duration=>0.47291666666666665}, {:note=>60, :duration=>0.47291666666666665}, {:note=>65, :duration=>0.9479166666666666}, {:note=>67, :duration=>0.9479166666666666}, {:note=>69, :duration=>0.9479166666666666}, {:note=>64, :duration=>0.9979166666666667}, {:note=>67, :duration=>0.9479166666666666}, {:note=>60, :duration=>0.47291666666666665}, {:note=>60, :duration=>0.47291666666666665}, {:note=>72, :duration=>1.8979166666666667}, {:note=>69, :duration=>0.47291666666666665}, {:note=>65, :duration=>0.47291666666666665}, {:note=>67, :duration=>1.8979166666666667}, {:note=>64, :duration=>0.47291666666666665}, {:note=>60, :duration=>0.47291666666666665}, {:note=>65, :duration=>0.4979166666666667}, {:note=>67, :duration=>0.24791666666666667}, {:note=>65, :duration=>0.24791666666666667}, {:note=>64, :duration=>0.9479166666666666}, {:note=>62, :duration=>0.9479166666666666}, {:note=>60, :duration=>1.8979166666666667}]
sleeps0 = [0.0, 0.5, 0.5, 2.0, 0.5, 0.5, 2.0, 0.5, 0.5, 1.0, 1.5, 0.5, 1.0, 1.0, 0.5, 0.5, 2.0, 0.5, 0.5, 2.0, 0.5, 0.5, 0.5, 0.5, 1.0, 1.0, 2.0, 0.5, 0.5, 2.0, 0.5, 0.5, 2.0, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0, 0.5, 0.5, 2.0, 0.5, 0.5, 2.0, 0.5, 0.5, 0.5, 0.25, 0.25, 1.0, 1.0]

in_thread do
  use_synth :dpulse
  sleep sleeps0[0]
  melody0.each_with_index do |item,i|
    play item[:note], sustain: item[:duration]*0.8, release: item[:duration]*0.2
    sleep sleeps0[i+1] if i+1 < sleeps0.length

melody1 = [{:note=>48, :duration=>0.9479166666666666}, {:note=>52, :duration=>1.8979166666666667}, {:note=>55, :duration=>1.8979166666666667}, {:note=>48, :duration=>0.9479166666666666}, {:note=>52, :duration=>1.8979166666666667}, {:note=>55, :duration=>1.8979166666666667}, {:note=>48, :duration=>0.9479166666666666}, {:note=>52, :duration=>1.8979166666666667}, {:note=>55, :duration=>1.8979166666666667}, {:note=>47, :duration=>0.9479166666666666}, {:note=>53, :duration=>0.9479166666666666}, {:note=>55, :duration=>0.9479166666666666}, {:note=>47, :duration=>0.9479166666666666}, {:note=>53, :duration=>1.8979166666666667}, {:note=>55, :duration=>1.8979166666666667}, {:note=>47, :duration=>0.9479166666666666}, {:note=>53, :duration=>1.8979166666666667}, {:note=>55, :duration=>1.8979166666666667}, {:note=>47, :duration=>0.9479166666666666}, {:note=>53, :duration=>1.8979166666666667}, {:note=>55, :duration=>1.8979166666666667}, {:note=>48, :duration=>0.9479166666666666}, {:note=>52, :duration=>1.8979166666666667}, {:note=>55, :duration=>1.8979166666666667}, {:note=>48, :duration=>0.9479166666666666}, {:note=>53, :duration=>1.8979166666666667}, {:note=>57, :duration=>1.8979166666666667}, {:note=>48, :duration=>0.9479166666666666}, {:note=>52, :duration=>1.8979166666666667}, {:note=>55, :duration=>1.8979166666666667}, {:note=>50, :duration=>0.9479166666666666}, {:note=>52, :duration=>0.9479166666666666}, {:note=>53, :duration=>0.9479166666666666}, {:note=>48, :duration=>0.9479166666666666}, {:note=>52, :duration=>1.8979166666666667}, {:note=>55, :duration=>1.8979166666666667}, {:note=>48, :duration=>0.9479166666666666}, {:note=>53, :duration=>1.8979166666666667}, {:note=>57, :duration=>1.8979166666666667}, {:note=>48, :duration=>0.9479166666666666}, {:note=>52, :duration=>1.8979166666666667}, {:note=>55, :duration=>1.8979166666666667}, {:note=>57, :duration=>0.9479166666666666}, {:note=>55, :duration=>0.9479166666666666}, {:note=>53, :duration=>0.9479166666666666}, {:note=>48, :duration=>1.8979166666666667}, {:note=>52, :duration=>1.8979166666666667}]
sleeps1 = [1.0, 1.0, 0.0, 2.0, 1.0, 0.0, 2.0, 1.0, 0.0, 2.0, 1.0, 0.0, 2.0, 1.0, 0.0, 2.0, 1.0, 0.0, 2.0, 1.0, 0.0, 2.0, 1.0, 0.0, 2.0, 1.0, 0.0, 2.0, 1.0, 0.0, 2.0, 1.0, 1.0, 1.0, 1.0, 0.0, 2.0, 1.0, 0.0, 2.0, 1.0, 0.0, 2.0, 1.0, 1.0, 1.0, 0.0]

in_thread do
  use_synth :dsaw
  sleep sleeps1[0]
  melody1.each_with_index do |item,i|
    play item[:note], sustain: item[:duration]*0.8, release: item[:duration]*0.2
    sleep sleeps1[i+1] if i+1 < sleeps1.length

I think line 80 of your script should be altered from
out.write(", sustain: item[:duration]*#{RELEASE}") if RELEASE > 0
out.write(", release: item[:duration]*#{RELEASE}") if RELEASE > 0

otherwise sorts the part timing problem OK

1 Like

I am really enjoying using this. It works well with bach organ music and I’ve quickly done several fugues which play well with :tri synth for top two parts and :saw for the pedal part.
It would be nice to add the ability for tempo change…maybe just for block changes for all parts at the same time, but even better if it can handle them individually.
The script I have been using can handle the former. For example in a prelude and fugue there may well be a tempo change between them. You can of course split the file iditng in musescore and treat the two halves as separate files, and amalgamate them after conversion.

I’m happy to see you enjoying the script.

Would it be possible to message me a midi file that has a tempo change? I’m happy to play around and see what I can come up with.

Are you also wanting the ability to run the script targeting specific tracks as opposed to the whole thing? I’m not sure if I’m misunderstanding but that’s something I plan to do. As well as having a bit of the script that translates drum tracks.

Another cool thing I think would be to have the script able to optionally trigger Sonic Pi with an OSC message. Especially if you can have the script only translating parts of the composition, for example, beats 16-32. Could be cool to have a quick listen to how certain parts sound, especially if the script allows the setting of synths.

I’ve posted a midi test-file with one music track but varying tempo. Mostly on bar changes but the last bar with a tempo change each note as you might get in a rallentando.
At present the script just picks up the initial tempo and ignores the others.

Here is a Bach Prelude and Fugue processed by your script and played and recorded on a Pi400 running Sonic Pi 3.3.0-beta6

1 Like

It sounded a bit glitchy at the end there, but maybe this could be a starting point?

Funnily enough I’ve been working on the script too! I think I,ve worked out how to get yours playing the last bit properly. Just need to tidy it up a bit.

I now have a working script that will handle changes in tempo during the piece, with the constraints that
a) each part must change tempo at the same time, and
b) the tempo change must occur at the beginning of a note or rest, and not during either.
(Midi can change the tempo during a note or rest, but this is not possible with Sonic Pi)

I have tested it with Bach Brandenburg Concerto no 3 which has 10 separate voices and several tempo changes and a duration of over 10 minutes! It does the conversion from midi file to Sonic Pi file in a fraction of a second.

Hello everybody, i have this bug:
/.gem/gems/midilib-2.0.5/lib/midilib/sequence.rb:138:in new': tried to create Proc object without a block (ArgumentError) from /home/me/.gem/gems/midilib-2.0.5/lib/midilib/sequence.rb:138:in read’
from zik.rb:13:in block in <main>' from zik.rb:10:in open’
from zik.rb:10:in `’

what can i do ???

You are trying to use it with ruby 3, midilib is only compatible with Ruby 1.8.x 1.9.x, and 2.x.

I just made a fork of midilib that can be runned directly in Sonic Pi. Clone this repository to try it out:

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

I have not tested the midi writer yet, but reading midi works. I also made some helper methods based on your example. Example usage:

load "~/midilib/sonicpi.rb"

test_midi = ENV['HOME']+'/midilib/examples/pinnin_valssi.mid'

use_bpm 120

# Parse to hash
midi_hash = midi_to_hash test_midi

print midi_hash.keys
print midi_hash

2.times do
  with_fx :reverb, room: 0.7 do
    with_fx :wobble, phase: 1.0, smooth: 0.7, amp: 0.8 do
      # Play directly or from preparsed hash
      play_midi test_midi, [:chiplead, :chipbass]
  # Midi tracks are played in separate threads so sleep until tracks are finished
  sleep midi_hash[:track_lengths].max
1 Like