Skip to content

Instantly share code, notes, and snippets.

@rbnpi
Last active November 9, 2023 09:30
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rbnpi/ded470ae7776ee3cfaa36dc208c68abb to your computer and use it in GitHub Desktop.
Save rbnpi/ded470ae7776ee3cfaa36dc208c68abb to your computer and use it in GitHub Desktop.
Sonic Pi Midifile Player accompanying article at https://rbnrpi.wordpress.com/a-midi-file-player-program-for-sonic-pi/

These files comprise two versions of a midifileplayer for Sonic Pi version 3.0.1 or later. There are two versions.

File SPbasicMidiFilePlayer.rb is the basic version, where the synths used to play each channel in the midifile have to be specified in teh code before running the program in the form of a sonic pi ring. If this contains just a single synth e.g. (ring :saw) then each channel will use this synth. If two synths are specified eg (ring :rri) then midi channel 1 will use :saw, channel 2 :tri and then any further channel will use index into this ring and use in turn :saw, :tri,:saw,:tri This is set in line 38. Also specified at run time in the code is the attenuation level set by the fx :level wrapper in teh variable attenAmp set to 0.5 by default in line 40.

In the second more complicated vbesion, support is provided for an external TouchOSC interface which can be used to specify synth allocation on the fly as the program is running. and also to adjust the attenAmp setting in teh range 0.1 to 1 again as the program is running.

Bot version also requires a template mplayer.touchosc which is created from the file index.xml in this gist post.

Detailed instructions on theeh working of the programs and their installation is included in the associated article which you can read at rbnpi.wordpress.com This also details suitable ways of streaming the midifiles you wish to play to Sonic Pi.

There is a short video of a slightly earlier version of the program at https://twitter.com/rbnman/status/1221201258780020736

<?xml version="1.0" encoding="UTF-8"?><layout version="17" mode="0" orientation="horizontal"><tabpage name="bXBsYXllcg==" scalef="0.0" scalet="1.0" li_t="" li_c="gray" li_s="14" li_o="false" li_b="false" la_t="" la_c="gray" la_s="14" la_o="false" la_b="false" ><control name="dGl0bGU=" x="10" y="9" w="292" h="20" color="yellow" type="labelh" text="TWlkaSBQbGF5ZXI=" size="16" background="true" outline="true" ></control><control name="bGFiZWwy" x="7" y="73" w="75" h="25" color="red" type="labelh" text="UGlhbm8=" size="14" background="true" outline="false" ></control><control name="bGFiZWwxMg==" x="7" y="105" w="75" h="25" color="red" type="labelh" text="VHJp" size="14" background="true" outline="false" ></control><control name="bGFiZWwxMw==" x="7" y="137" w="75" h="25" color="red" type="labelh" text="U2F3" size="14" background="true" outline="false" ></control><control name="bGFiZWwxNA==" x="7" y="170" w="75" h="25" color="red" type="labelh" text="UGx1Y2s=" size="14" background="true" outline="false" ></control><control name="bGFiZWwxNQ==" x="7" y="202" w="75" h="25" color="red" type="labelh" text="VGIzMDM=" size="14" background="true" outline="false" ></control><control name="bGFiZWwxNg==" x="7" y="234" w="75" h="25" color="red" type="labelh" text="QmxhZGU=" size="14" background="true" outline="false" ></control><control name="c2luZ2xlYWRk" x="189" y="68" w="77" h="50" color="orange" scalef="0.0" scalet="1.0" type="multitoggle" number_x="2" number_y="1" ex_mode="true" local_off="false" ></control><control name="bGFiZWwyMA==" x="178" y="38" w="49" h="25" color="orange" type="labelh" text="U2luZ2xl" size="14" background="true" outline="true" ></control><control name="bGFiZWwyMQ==" x="228" y="38" w="40" h="25" color="orange" type="labelh" text="IEFkZCA=" size="14" background="true" outline="true" ></control><control name="bGFiZWwyMg==" x="161" y="148" w="131" h="25" color="blue" type="labelh" text="Vm9sdW1l" size="14" background="true" outline="true" ></control><control name="bGFiZWwyMw==" x="6" y="38" w="134" h="25" color="red" type="labelh" text="U3ludGhz" size="14" background="true" outline="true" ></control><control name="Y3VycmVudFN5bnRocw==" x="9" y="345" w="292" h="20" color="yellow" type="labelh" text="" size="16" background="true" outline="true" ></control><control name="c3ludGhz" x="85" y="68" w="50" h="200" color="red" scalef="0.0" scalet="1.0" type="multipush" number_x="1" number_y="6" local_off="false" ></control><control name="Y3VycmVudFN5bnRoczI=" x="9" y="367" w="292" h="20" color="yellow" type="labelh" text="" size="16" background="true" outline="true" ></control><control name="Y3VycmVudFN5bnRoczM=" x="9" y="389" w="292" h="20" color="yellow" type="labelh" text="" size="16" background="true" outline="true" ></control><control name="Y3VycmVudFN5bnRoczQ=" x="9" y="411" w="292" h="20" color="yellow" type="labelh" text="" size="16" background="true" outline="true" ></control><control name="bGFiZWwyOA==" x="13" y="312" w="283" h="25" color="yellow" type="labelh" text="Q3VycmVudCBTeW50aHMgcmluZyAoNCBwZXIgcm93KQ==" size="14" background="true" outline="false" ></control><control name="cmVtb3Zl" x="135" y="270" w="45" h="45" color="purple" scalef="0.0" scalet="1.0" type="push" local_off="false" sp="true" sr="false" ></control><control name="bGFiZWwyOQ==" x="11" y="280" w="122" h="25" color="purple" type="labelh" text="UmVtb3ZlIExhc3Q=" size="14" background="true" outline="false" ></control><control name="dm9s" x="146" y="185" w="155" h="24" color="blue" scalef="0.0" scalet="1.0" type="faderh" response="absolute" inverted="false" centered="false" ></control></tabpage></layout>
#Multi-channel midi file player for Sonic Pi 3.1 or 3.2
#this program attempts to play midi events for up to 16 channels
#also can handle channel 10 as GM percussion
#handles both note_off and note_on with vel=0 for note stopping
#experimental program by Robin Newman, January 2020
#this version without support for TouchOSC control interface
use_debug false
use_midi_logging false
use_cue_logging false
####******** USER SETTINGS ******
#use_real_time #not recommended
#use_sched_ahead_time 0.75 #possible use for very demanding midi files
percpath="~/Downloads/DrumsetSamplesFlac" #path to GM drumset samples
#drums is a hash table linking note value to sample name
drums={35=>"35 Acoustic Bass Drum",36=>"36 Bass Drum 1",37=>"37 Side Stick",
38=>"38 Acoustic Snare",39=>"39 hand clap",40=>"40 Electric Snare",
41=>"41 Low Floor Tom",42=>"42 Closed Hi-hat",43=>"43 High Floor Tom",
44=>"44 Pedal Hi-hat",45=>"45 Low Tom",46=>"46 Open Hi-hat",
47=>"47 Low-Mid tom",48=>"48 Hi Mid Tom",49=>"49 Crash Cymbal 1",
50=>"50 High Tom",51=>"51 Ride Cymbal 1",52=>"52 Chinese Cymbal",
53=>"53 Ride Bell",54=>"54 Tambourine",55=>"55 Splash Cymbal",
56=>"56 Cowbell",57=>"57 Crash Cymbal 2",58=>"58 Vibraslap",
59=>"59 Ride Cymbal 2",60=>"60 Hi Bongo",61=>"61 Low Bongo",
62=>"62 Mute Hi Conga",63=>"63 Open Hi Conga",64=>"64 Low Conga",
65=>"65 High Timbale",66=>"66 Low Timbale",67=>"67 High Agogo",
68=>"68 Low Agogo",69=>"69 Cabasa",70=>"70 Maracas",
71=>"71 Short Whistle",72=>"72 Long Whistle",73=>"73 Short Guiro",
74=>"74 Long Guiro",75=>"75 Claves",76=>"76 Hi Wood Block",
77=>"77 Low Wood Block",78=>"78 Mute Cuica",79=>"79 Open Cuica",
80=>"80 Mute Triangle",81=>"81 Open Triangle"}
use_transpose 0 #add optional transpose manually
pan=[0,-0.1,0.1,-0.2,0.2,-0.3,0.3,-0.4,0.4,0,
-0.5,0.5,-0.6,0.6,-0.7,0.7] #pan settings for 16 channels
synthRing=(ring :saw) #:pulse,:saw,:tri,:blade select synth(s) to use indexed by channel
# eg synthRing=(ring :piano,:saw,:rb303) #would allocat ch1 piano, ch2 saw ch3 tb303 ch4 piano....
ampAtten=0.5 #manual setting for fx :level amp:
#####******** END USER SETTINGS *****
#np holds state of all notes values for 16 channels whether playing 1 or silent 0
np = Array.new(16){Array.new([0]*128)}
nr= Array.new(16){Array.new(128)}#lists to store reference to notes currently playing
klist = Array.new(16)#list to contain references to playing note to be killed for each channel
sleep 1 #allow arrays time to be built
define:parse_sync_address do |address| #gets info on wild cards used in sync address
v= get_event(address).to_s.split(",")[6]
if v != nil
#return a list of address elements with wild cards substituted by actual values
return v[3..-2].split("/")
else
return ["error"]
end
end
define :notekill do |ch| #fade and kill the note specified by this channel
in_thread do #perform in thread to minimise delays in main live_loop
k=klist[ch] #retreive the note reference for channel ch to be killed
control k,amp: 0,amp_slide: 0.01 if k!=nil#fade note out in 0.01 seconds
sleep 0.01
kill k #kill the note with reference k
#end
end
end
with_fx :reverb, room: 0.8, mix: 0.7 do #add reverb to taste
#set overall volume to minimise distortion when using 16 channel input
with_fx :level,amp: ampAtten do
live_loop :midi_in,auto_cue: false do
note, vel = sync "/midi/*/*/*/note_*" #change to match controller if you wish
r=parse_sync_address("/midi/*/*/*/note_*") #get full parsed address string
ch=r[3].to_i - 1 #extract channel that triggered sync (channel offset from 0)
if r[4]=="note_on" and vel > 0 #check if note_on with +ve velocity
if np[ch][note]==0 #check if new start for the note
np[ch][note]=1 #mark note as started for this pitch
use_synth synthRing[ch] #:pulse,:saw,:tri,:blade select synth to use
#max duration of note set to 5 on next line. Can increase if you wish.
if ch !=9#ignore percussion track
#start a 5 second duration note for the specified pitch
x = play note, sustain: 5,amp: vel/127.0,pan: pan[ch] #+offset
#puts nr[ch][note]
else #otherwise channel 10 so play sample
x = sample percpath,drums[note],amp: vel/127.0
end
nr[ch][note]=x #store reference x to in nr array for given channel
end
else #note_off or vel == 0
if np[ch][note]==1 #check if this pitch is on
klist[ch] = nr[ch][note] #add to list for killing for this channel
np[ch][note]=0 #now set this pitch off
notekill ch #call notekill function
end
end
end
end #fx level
end #fx reverb
#Multi-channel midi file player for Sonic Pi 3.1 or 3.2
#this program attempts to play midi events for up to 16 channels
#also can handle channel 10 as GM percussion
#handles both note_off and note_on with vel=0 for note stopping
#experimental program by Robin Newman, January 2020
#this version supports TouchOSC synth select, and volume selection
use_debug false
use_midi_logging false
use_cue_logging false
use_osc_logging false
####******** USER SETTINGS ******
#use_real_time #not recommended
#use_sched_ahead_time 0.75 #possible use for very demanding midi files
use_osc "192.168.1.240",9000 #address of TouchOSC controller
percpath="~/Downloads/DrumsetSamplesFlac" #path to GM drumset samples
#drums is a hash table linking note value to sample name
drums={35=>"35 Acoustic Bass Drum",36=>"36 Bass Drum 1",37=>"37 Side Stick",
38=>"38 Acoustic Snare",39=>"39 hand clap",40=>"40 Electric Snare",
41=>"41 Low Floor Tom",42=>"42 Closed Hi-hat",43=>"43 High Floor Tom",
44=>"44 Pedal Hi-hat",45=>"45 Low Tom",46=>"46 Open Hi-hat",
47=>"47 Low-Mid tom",48=>"48 Hi Mid Tom",49=>"49 Crash Cymbal 1",
50=>"50 High Tom",51=>"51 Ride Cymbal 1",52=>"52 Chinese Cymbal",
53=>"53 Ride Bell",54=>"54 Tambourine",55=>"55 Splash Cymbal",
56=>"56 Cowbell",57=>"57 Crash Cymbal 2",58=>"58 Vibraslap",
59=>"59 Ride Cymbal 2",60=>"60 Hi Bongo",61=>"61 Low Bongo",
62=>"62 Mute Hi Conga",63=>"63 Open Hi Conga",64=>"64 Low Conga",
65=>"65 High Timbale",66=>"66 Low Timbale",67=>"67 High Agogo",
68=>"68 Low Agogo",69=>"69 Cabasa",70=>"70 Maracas",
71=>"71 Short Whistle",72=>"72 Long Whistle",73=>"73 Short Guiro",
74=>"74 Long Guiro",75=>"75 Claves",76=>"76 Hi Wood Block",
77=>"77 Low Wood Block",78=>"78 Mute Cuica",79=>"79 Open Cuica",
80=>"80 Mute Triangle",81=>"81 Open Triangle"}
use_transpose 0 #add optional transpose manually
pan=[0,-0.1,0.1,-0.2,0.2,-0.3,0.3,-0.4,0.4,0,
-0.5,0.5,-0.6,0.6,-0.7,0.7] #pan settings for 16 channels
synthRing=(ring :piano) #:pulse,:saw,:tri,:blade select synth(s) to use indexed by channel
sList=[:piano,:tri,:saw,:pluck,:tb303,:blade] #selectable synths using TouchOSC
single= true #intialise single mode selection for touchOSC
ampAtten=0.5 #manual setting for fx :level amp:
#####******** END USER SETTINGS *****
#osc calls to intialise TouchOSC display
osc "/mplayer/singleadd/1/1",1.0
osc "/mplayer/vol",0.5
osc "/mplayer/currentSynths","(ring :piano)"
osc "/mplayer/currentSynths2",""
osc "/mplayer/currentSynths3",""
osc "/mplayer/currentSynths4",""
#np holds state of all notes values for 16 channels whether playing 1 or silent 0
np = Array.new(16){Array.new([0]*128)}
nr= Array.new(16){Array.new(128)}#lists to store reference to notes currently playing
klist = Array.new(16)#list to contain references to playing note to be killed for each channel
sleep 1 #allow arrays time to be built
define:parse_sync_address do |address| #gets info on wild cards used in sync address
v= get_event(address).to_s.split(",")[6]
if v != nil
#return a list of address elements with wild cards substituted by actual values
return v[3..-2].split("/")
else
return ["error"]
end
end
live_loop :getSingleAdd do #TouchOSC sets single or additional synth selection
use_real_time
val=sync "/osc*/mplayer/singleadd/*/*"
if val[0]==1.0 #switch is on
#set single true when it is switch 1 that is on
single = parse_sync_address("/osc*/mplayer/singleadd/*/*")[3].to_i == 1
end
end
define :update do #updates TouchOSC synth display
dd=synthRing.to_s
#split dd into list of "words"
#eg "(ring :piano, :saw, :pluck)" becomes ["", "ring", "piano", "saw", "pluck"]
sdd=dd.split(/[^[[:word:]]]+/)
#clear synth labels on display
osc "/mplayer/currentSynths",""
osc "/mplayer/currentSynths2",""
osc "/mplayer/currentSynths3",""
osc "/mplayer/currentSynths4",""
sleep 0.2
#repopulate with current values
osc "/mplayer/currentSynths",sdd[2..5] #first 4
osc "/mplayer/currentSynths2",sdd[6..9] if sdd.length > 6 #5-8
osc "/mplayer/currentSynths3",sdd[10..13] if sdd.length > 10 #9-12
osc "/mplayer/currentSynths4",sdd[14..17] if sdd.length > 14 #13-16
end
live_loop :getSynths do #allows selection of synth ring by TouchOSC
use_real_time
val=sync "/osc*/mplayer/synths/*/*"
if val[0]==1.0
sn = parse_sync_address("/osc*/mplayer/synths/*/*")[4].to_i - 1
if single
synthRing=(ring sList[sn])
elsif synthRing.length<16
synthRing = ((synthRing.to_a).push sList[sn]).ring #add new synth
end
update #the TouchOSC display
end
end
live_loop :removeLast do
use_real_time
val=sync "/osc*/mplayer/remove"
#puts "removing latest synth"
if synthRing.length > 1
#convert to array, drop last entry and set to ring again
synthRing = (synthRing.to_a).reverse.drop(1).reverse.ring
update #update the display
end
end
define :notekill do |ch| #fade and kill the note specified by this channel
in_thread do #perform in thread to minimise delays in main live_loop
k=klist[ch] #retreive the note reference for channel ch to be killed
control k,amp: 0,amp_slide: 0.01 if k!=nil#fade note out in 0.01 seconds
sleep 0.01
kill k #kill the note with reference k
#end
end
end
with_fx :reverb, room: 0.8, mix: 0.7 do #add reverb to taste
#set overall volume to minimise distortion when using 16 channel input
with_fx :level,amp: ampAtten do |vol|
set :vol,vol
live_loop :getVol do
use_real_time
val = sync "/osc*/mplayer/vol"
ampNew=[0.1,val[0],1.0].sort[1] #sets range 0.1=>1
#puts ampNew,get(:vol)
control get(:vol),amp: ampNew
end
live_loop :midi_in,auto_cue: false do
note, vel = sync "/midi/*/*/*/note_*" #change to match controller if you wish
r=parse_sync_address("/midi/*/*/*/note_*") #get full parsed address string
ch=r[3].to_i - 1 #extract channel that triggered sync (channel offset from 0)
if r[4]=="note_on" and vel > 0 #check if note_on with +ve velocity
if np[ch][note]==0 #check if new start for the note
np[ch][note]=1 #mark note as started for this pitch
use_synth synthRing[ch] #:pulse,:saw,:tri,:blade select synth to use
#max duration of note set to 5 on next line. Can increase if you wish.
if ch !=9#ignore percussion track
#start a 5 second duration note for the specified pitch
x = play note, sustain: 5,amp: vel/127.0,pan: pan[ch] #+offset
#puts nr[ch][note]
nr[ch][note]=x #store reference x to in nr array for given channel
else #otherwise channel 10 so play sample
sample percpath,drums[note],amp: 0.5
end
end
else #note_off or vel == 0
if np[ch][note]==1 #check if this pitch is on
klist[ch] = nr[ch][note] #add to list for killing for this channel
np[ch][note]=0 #now set this pitch off
notekill ch #call notekill function
end
end
end
end #fx level
end #fx reverb
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment