Skip to content

Instantly share code, notes, and snippets.

@PARC6502
Created January 26, 2021 16:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save PARC6502/277b79c960ed1d0e4334ce40acd124a5 to your computer and use it in GitHub Desktop.
Save PARC6502/277b79c960ed1d0e4334ce40acd124a5 to your computer and use it in GitHub Desktop.
A midi to Sonic Pi script that can handle tempo changes
#! /usr/bin/env ruby
require 'midilib/sequence'
DEFAULT_MIDI_TEST_FILE = 'Tempo_Changes_test.mid'
# Read from MIDI file
seq = MIDI::Sequence.new()
# Either open the file provided by the first argument or the one defined above
File.open(ARGV[0] || DEFAULT_MIDI_TEST_FILE, 'rb') do |file|
# The block we pass in to Sequence.read is called at the end of every
# track read. It is optional, but is useful for progress reports.
seq.read(file) do |track, num_tracks, i|
#puts "read track #{track ? track.name : ''} (#{i} of #{num_tracks})"
end
end
PPQN = seq.ppqn # Pulses per quarter note, the number of ticks in one beat
def to_note (ticks)
# Converts the time in ticks to time in notes
return (ticks*1.0)/PPQN
end
def get_bpm (event)
# Gets a rounded bpm value from a Tempo event
return MIDI::Tempo.mpq_to_bpm(event.tempo).round
end
melodies = {}
sleeps = {}
bpm = seq.bpm.round.to_s
puts "Num tracks: #{seq.tracks.length}"
seq.each_with_index do |track, idx|
# Filter the track so it only has Note On events
puts track.name
track = track.select { |e| e.is_a?(MIDI::Tempo) || e.is_a?(MIDI::NoteOn) }
melody = []
slps = []
track.each_with_index do |e, i|
# Track tempo changes
if e.is_a?(MIDI::Tempo)
melody.push({bpm: get_bpm(e)})
next
end
#Get the first sleep
if i == 0
first_slp = to_note(e.time_from_start)
slps.push(first_slp)
end
# Calculate duration of note by comparing the time between on and off events. Convert from ticks to beats.
dur_delta = e.off.time_from_start - e.time_from_start
duration = to_note(dur_delta)
# Round duration to the closest 1/16
#duration = (duration*16).round / 16.0
note = e.note
melody.push({note: note, duration: duration})
# There is one more note than there is sleep, so we have to break early on the last iteration
break if i == (track.length - 1)
slp_delta = track[i+1].time_from_start - e.time_from_start
slp = to_note(slp_delta)
slps.push(slp)
end
melodies[idx] = melody
sleeps[idx] = slps
end
#= Sonic Pi Output
# This section creates a file that's readable by sonic pi
#== Attack/sutain/release ratios
ATTACK = 0
SUSTAIN = 0.8
RELEASE = 0.2
out = File.new('out.rb', 'w')
out.write("bpm = #{bpm}\n\n")
melodies.each do |key,value|
idx = key.to_s
out.write("melody#{idx} = #{value}\n")
out.write("sleeps#{idx} = #{sleeps[key]}\n\n")
out.write("in_thread do\n")
out.write(" sleep sleeps#{idx}[0]\n")
out.write(" melody#{idx}.each_with_index do |item,i|\n")
out.write(" unless item[:bpm].nil?\n")
out.write(" bpm = item[:bpm]\n")
out.write(" next\n")
out.write(" end \n")
out.write(" with_bpm bpm do\n")
out.write(" play item[:note]")
out.write(", attack: item[:duration]*#{ATTACK}") if ATTACK > 0
out.write(", sustain: item[:duration]*#{SUSTAIN}") if SUSTAIN > 0
out.write(", release: item[:duration]*#{RELEASE}") if RELEASE > 0
out.write("\n")
out.write(" sleep sleeps#{idx}[i+1] if i+1 < sleeps#{idx}.length\n")
out.write(" end\n") #with_bpm
out.write(" end\n") #melody.each
out.write("end\n\n") #thread
end
out.close
# Script output on test file
bpm = 80
melody0 = [{:bpm=>80}, {:note=>72, :duration=>0.9479166666666666}, {:note=>72, :duration=>0.9479166666666666}, {:note=>72, :duration=>0.9479166666666666}, {:note=>72, :duration=>0.9479166666666666}, {:bpm=>40}, {:note=>74, :duration=>0.9479166666666666}, {:note=>74, :duration=>0.9479166666666666}, {:note=>74, :duration=>0.9479166666666666}, {:note=>74, :duration=>0.9479166666666666}, {:bpm=>120}, {:note=>70, :duration=>0.9479166666666666}, {:note=>70, :duration=>0.9479166666666666}, {:note=>70, :duration=>0.9479166666666666}, {:note=>70, :duration=>0.9479166666666666}, {:bpm=>220}, {:note=>69, :duration=>0.9479166666666666}, {:note=>69, :duration=>0.9479166666666666}, {:note=>69, :duration=>0.9479166666666666}, {:note=>69, :duration=>0.9479166666666666}, {:note=>72, :duration=>0.9479166666666666}, {:note=>72, :duration=>0.9479166666666666}, {:note=>72, :duration=>0.9479166666666666}, {:note=>72, :duration=>0.9479166666666666}, {:note=>74, :duration=>0.9479166666666666}, {:note=>74, :duration=>0.9479166666666666}, {:note=>74, :duration=>0.9479166666666666}, {:note=>74, :duration=>0.9479166666666666}, {:note=>70, :duration=>0.9479166666666666}, {:note=>70, :duration=>0.9479166666666666}, {:note=>70, :duration=>0.9479166666666666}, {:note=>70, :duration=>0.9479166666666666}, {:bpm=>180}, {:note=>69, :duration=>0.9479166666666666}, {:bpm=>120}, {:note=>69, :duration=>0.9479166666666666}, {:bpm=>80}, {:note=>69, :duration=>0.9479166666666666}, {:bpm=>40}, {:note=>69, :duration=>0.9479166666666666}]
sleeps0 = [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
in_thread do
sleep sleeps0[0]
melody0.each_with_index do |item,i|
unless item[:bpm].nil?
bpm = item[:bpm]
next
end
with_bpm bpm do
play item[:note], sustain: item[:duration]*0.8, release: item[:duration]*0.2
sleep sleeps0[i+1] if i+1 < sleeps0.length
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment