Code for Artiphone Instrument1

At last here is my fuller reply.

I liked the look of this Artiphone project, and the neat use of mapping to hold the data on chords and sample loops. On reading about the problems that goldengrape was experiencing I decided to play around with the code, incorporating some ideas I had worked on previously. Unfortunately I was away last weekend and only had limited resources, (including a virtual keyboard on my iPhone) with which to test ideas, but today I have tidied things up a bit, although what I have come up with probably needs a bit more polish.
There seemed to be two main questions. First how to get a loop repeating indefinitely until an alternative was selected, and secondly a bit more clarity about the use of midi note_on and note_off commands in Sonic Pi.

Looking at the note_on note_off situation first, in Sonic Pi synths work a little differently to those used in most DAWS or midi keyboards integrated with synths. In the latter case, when a key is pressed on a midi keyboard a note_on signal is sent and it starts the selected synth playing. It remains playing until the keyboard key is released, when a midi note_off signal is sent and the synth stops playing (or enters the release phase of any envelope associated with its sound). So the note’s duration is determined by the keyboard player, and is not determined by the hardware.(although the midi input may be recorded and used in a subsequent replay). In the case of Sonic Pi things are a bit different. The synths are designed to be controlled by code is written by the user, and this is in place BEFORE the synth starts playing. So the information about when to start the synth, and how long to keep playing it is received by the synth BEFORE it starts playing. So the key (no pun intended) information is name of synth, note to play, and duration of the note (although these may be altered by any active envelope information). While the note is playing it can also changed by separate control information, altering say its pitch, or cutoff filter values etc). So usually with Sonic Pi, only midi note_on signals are relevant, and you program a fixed duration for each incoming note. However, it is possible to simulate the use of note_on and note_off signals to determine the note lengths in Sonic Pi, although it requires a bit of extra coding to achieve this, as we will see a bit later on.
As an aside, there is one further complication. Some midi keyboards do not use midi note_off signals, but instead use a midi note_on signal but set the associated note velocity to 0. This can be accommodated in the Sonic Pi code, but you need to use the method appropriate to the keyboard (or midi instrument) you are using.

To look at the other problem first, what we want is to have a looping sample running inside a live loop eg.

live_loop :test do
  sample :loopAmen, beat_stretch: 4, amp: 0.8
  sleep 4
end

However we also want to be able to stop this loop playing and to start a similar one with a different sample playing triggered by a midi_on signal from a specified note.
We can stop a live loop playing by inserting a command stop inside it, and we can stop a sample playing by keeping a reference to it eg
sref = sample :loopAmen.... and then using a command kill sref to stop it.
In a previous thread I developed code to do this over a period of time and the final versions are in a gist here

This had a couple of differences. First that example used OSC messages to select the loops to be played, and secondly the loops were designed to play only while a key was held down, and it was possible to have several live loops playing together. In the case of the Artiphone project I have modified the code to allow only ONE live_loop to play at a time, and once selected this plays until a different one is selected. I also added a separate control to stop ALL the live loops playing, as otherwise this could only be achieved by stopping the entire program.

So much for words, lets look at some code.
The first change I made was to change the original functions into live_loops. I also used the :pluck synth which seemed more suited to a guitar type of instrument.

#modified code for Artipohone instrument
#allows for repeating sample loops
#and has option for note_on/note_off control of single notes
#by Robin Newman, Nvoember 2019
use_debug false
use_midi_logging false
use_bpm 120
artiphon_address= "/midi/*/*/*/"
artiphon_synth= :pluck

all_guitar_string=(ring 40,45,50,55,59,64)
chord_C=(ring 45+3,50+2,55,59+1,64)
chord_D=(ring 50,55+2,59+3,64+2)
chord_E=(ring 40,45+2,50+2,55+1,59,64)
chord_F=(ring 50+3,55+2,59+1,64+1)
chord_G=(ring 43,47,50,55,59,67)
chord_A=(ring 45,50+2,55+2,59+2,64)
chord_B=(ring 50+4,55+4,59+4,64+2)

chord_Cm=(ring 45+3,50+1,55)
chord_Dm=(ring 50,55+2,59+3,64+1)
chord_Em=(ring 40,47,52,55,59,64)
chord_Fm=(ring 50+3,55+1,59+1,64+1)
chord_Gm=(ring 50,55+3,59+3,64+3)
chord_Am=(ring 45,52,57,60,64)
chord_Bm=(ring 50+4,55+4,59+3,64+2)

remap_chord={28 => all_guitar_string,\
             29 => chord_C,\
             30 => chord_D,\
             31 => chord_E,\
             32 => chord_F,\
             33 => chord_G,\
             34 => chord_A,\
             35 => chord_B,\
             
             20 => chord_Cm,\
             21 => chord_Dm,\
             22 => chord_Em,\
             23 => chord_Fm,\
             24 => chord_Gm,\
             25 => chord_Am,\
             26 => chord_Bm,\
             }

remap_sample={16 => :guit_harmonics,\
              17 => :guit_e_fifths,\
              18 => :guit_e_slide,\
              19 => :guit_em9,\
              15 => :loop_industrial,\
              14=> :loop_compus,\
              13=> :loop_amen,\
              12=> :loop_amen_full,\
              11=> :loop_garzul,\
              10=> :loop_mika,\
              9=> :loop_breakbeat,\
              8=> :loop_safari,\
              7=> :loop_tabla,\
              6=> :loop_3d_printer,\
              5=> :loop_drone_g_97,\
              4=> :loop_electric,\
              3=> :loop_mehackit1,\
              2=> :loop_mehackit2,\
              1=> :loop_perc1,\
              0=> :loop_perc2,\
              }

with_fx :reverb do
  ######## CHORD PLAY #################
  live_loop :artiphon_chord do
    use_real_time
    note, velocity = sync artiphon_address+"note_on"
    if remap_chord.keys.include?(note)
      note=remap_chord[note]
      puts "note is",note
      synth artiphon_synth, \
        note: note, \
        release: 1.5,\
        amp: velocity*2/127.0
    end
  end
end #reverb

########## LOOPED SAMPLES #############

#three lists with data to control looping samples
#sFin  is fraction of sample to play from start
sFin=[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,        1,0.6667,1,0.6]
#bStretch is beat_stretch value to apply
bStretch=[4,4,4,4,4,8,16,20,16,4,16,16,12,4,12,2,   8,12,8,20]
#sVol is applitude of sample
sVol=[0.8,0.8,0.8,0.8,1,0.8,0.8,1,0.8,0.8,0.8,0.8,0.6,1,0.8,0.8,1,0.8,0.8,0.8]
#uncomment to print sample data for checking
##| 20.times do |n|
##|   puts n, bst[n]*bs[n]
##|   puts n, (sample_duration remap_sample[n]),remap_sample[n],vols[n]
##| end
##| stop


#initilise control flag for the running sample loop
define :setC do |c|
  #set all controls to off
  20.times do |n|
    set ("c"+n.to_s).to_sym,0
  end
  set ("c"+c.to_s).to_sym,1 if c>=0 #set selected sample control to on
end

define :startSampleLoop do |n|
  puts "remap",remap_sample[n]
  doLoop n,sVol[n],remap_sample[n],bStretch[n],sFin[n]
end

live_loop :artiphon_sample do
  use_real_time
  b = sync "/midi/*/*/*/note_on"
  n=b[0]
  puts n
  if n<20
    setC n.to_i #set new sampleLoop flag,: stops existing one
    puts remap_sample[n]
    startSampleLoop n
  end
end

#this next loop added to allow all looping samples to be stopped.
#it sets all of the stopFlags to 0 in the setC function.
live_loop :killSamples do #any midi_cc 12 value kills sample loops
  use_real_time
  b = sync "/midi/*/*/*/control_change"
  setC -1 if b[0]==12
end

######### CODE FOR STOPPABLE LIVE LOOPS ###############
#general function to set up stoppable live_loop
define :doLoop do |n,vol,sampleName,bs,sFin|
  set ("kill"+n.to_s).to_sym,false #intialise kill flag
  ln=("name"+n.to_s).to_sym #generate name for live_loop
  in_thread do #Thread monitors when to stop live_loop
    loop do
      if get( ("c"+n.to_s).to_sym)==0
        s= get( ("s"+n.to_s).to_sym)
        kill s
        set ("kill"+n.to_s).to_sym, true
        stop
      end
      sleep 0.2
    end
  end
  #start live_loop for selected sample
  #sync start to metro timing loop
  live_loop  ln, sync: :metro do
    s=sample sampleName,beat_stretch: bs,\
      amp: vol,finish: sFin
    set ("s"+n.to_s).to_sym,s #store pointer to sample
    k=(bs/0.25).to_i
    #test stop flag whilst sample runs
    k.times do
      sleep (bs*sFin).to_f/k
      stop if get( ("kill"++n.to_s).to_sym)
    end
  end
end

live_loop :metro do #metronome to sync stuff together
  sleep 1
end

###REMOVE ALL CODE FROM HERE ONWARDS FOR ALTERNATIVE VERSION ####
with_fx :reverb do
  ########## NORMAL NOTES #################
  live_loop :artiphon_normal do
    use_real_time
    note, velocity = sync artiphon_address+"note_on"
    if note>39
      #synth the audio
      synth artiphon_synth, \
        note: note, \
        release: 1.5,\
        amp: velocity/127.0
    end
  end
  
end #reverb

#### ALTERNATIVE CODE FOR NORMAL NOTES USING NOTE_ON NOTE_OFF ###
#polyphonic midi input program with sustained notes
#experimental program by Robin Newman, November 2017

plist=[] #list to contains references to notes to be killed
ns=[] #array to store note playing references
nv=[0]*128 #array to store state of note for a particlar pitch 1=on, 0 = 0ff

128.times do |i|
  ns[i]=("n"+i.to_s).to_sym #set up array of symbols :n0 ...:n127
end
#puts ns #for testing

define :sv do |sym| #extract numeric value associated with symbol eg :n64 => 64
  return sym.to_s[1..-1].to_i
end
#puts sv(ns[64]) #for testing

define :geton do |address|
  v= get_event(address).to_s.split(",")[6]#[address.length+1..-2].to_i
  return v.include?"note_on"
end
define :parse_sync_address do |address|
  v= get_event(address).to_s.split(",")[6]#[address.length+1..-2].to_i
  if v != nil
    return v[3..-2].split("/")
  else
    return ["error"]
  end
end

live_loop :midi_piano_on do #this loop starts 5 second notes for spcified pitches and stores reference
  use_real_time
  note, vol = sync "/midi/*/*/*/note_*"
  res= parse_sync_address "/midi/*/*/*/*"
  puts res[4]
  #note = note+12*get(:octave)
  if note>39
    #select ONE of the next two lines
    #if res[4]=="note_on" #select for separate note_on/ note_off
    if vol>0#select to use note_on with vel=0 for end of note
      puts note,nv[note]
      if nv[note]==0 #check if new start for the note
        nv[note]=1 #mark note as started for this pitch
        use_synth artiphon_synth
        #max duration of note set to 5 on next line. Can increase if you wish.
        x = play note, amp: vol*2/127.0,sustain: 50 #play note
        set ns[note],x #store reference in ns array
      end
    else
      if nv[note]==1 #check if this pitch is on
        nv[note]=0 #set this pitch off
        plist << get(ns[note])
      end
    end
  end
end

live_loop :notekill,auto_cue: false,delay: 0.25 do
  use_real_time
  if plist.length > 0 #check if notes to be killed
    k=plist.pop
    control k,amp: 0,amp_slide: 0.02 #fade note out in 0.02 seconds
    sleep 0.02
    kill k #kill the note referred to in ns array
  end
  sleep 0.01
end

There are quite a few comments in the code, but some further pointers may be helpful. The chord play section is very similar to the original, the main difference being to put it directly into its own live loop. Similarly the section dealing with “normal” notes (n > 39) is similar to the original, but again in its own live_loop. An alternative using note_on / note_off control of the note duration is shown later.
for the LOOPED SAMPLES section, the first new bit of code stores data for each looping sample in three lists. This allows you to choose the fraction of sample to use (some of the guitar samples have a long tail without much happening, so you can use say 2/3rds or 0.6 of the sample). You can also adjust the beat_stretch setting to make the sample last a given number, or fraction of beats. Finally you can set individual volumes for each sample.
The idea is to use one bit of code to handle every live loop (a function called doLoop) that can be generated (here there are 20!). A name for the loop is generated from the index number supplied giving live_loop :name0 to live_loop :name19 a kill flag :kill0 to :kill19 is generated, set to 0 and stored in the time line. A thread is then setup which repeatedly looks at the flag initialised in the setC function to see if it has been reset. If so, it retrieves a reference to the looping synth, set up in the relevant live_loop :name and stops the sample. It then sets the kill flag for the live loop to true, and stops the thread running. Meantime the live_loop :name has started, with a reference s to the playing sample being stored in the time line (set :s,s). The live loop sleeps for the duration of the sample (set by the beat-stretch and sFin values), but while it does so, it repeatedly checks if the kill flag has been set to true, so that the live_loop can be stopped as expeditiously as possible. This section of code is quite complex but I hope this explanation as to how it works is helpful.

Finally we can return to the problem of how to determine a note length from the note_on note (as is usually done with Sonic Pi). Just before the NORMAL NOTES section there is a line uncomment do #change to comment do for alternative program
If you comment this, then the ALTERNATIVE CODE FOR NORMAL NOTES section will become active. This uses a program I developed a couple of years ago which lets you start a note using a midi note_on signal, setting the duration of the note to a long value (I used 5 seconds, but you could use 20s or even longer). A reference to the playing note is stored in a list, and when a subsequent midi note_off signal is detected FOR THE SAME KEY the reference is retrieved and the note is stopped. This enables you to use key down/key up to determine how long the note sounds for. There is one “gotcha” you need to be aware of, that is your keyboard/midi instrument may use note_on for both note_on and note_off values as discussed above. (The keyboard I used to test the program does this). Fortunately both scenarios can be catered for by changing one line of the program. There are two lines in the program shown below. ONE of then should always be commented out. Choose the one relevant to your keyboard operation.

    #select ONE of the next two lines
    if res[4]=="note_on" #select for separate note_on/ note_off
    #if vol>0 #select to use note_on with vel=0 for end of note

The parseAddress function in this code may be a bit of a mystery. I usually use it to retrieve information about OSC cues, but it works equally well for providing information from a midi cue. Some explanation is given in this previous post

In principle, it should be possible to modify the CHORD PLAY to allow for note_on note_off control, although it could be a bit tricky, but I haven’t tried it.

So a bit of a mammoth post, but I hope you (and others) may find it useful.
Please feel free to ask if you need further clarifications

2 Likes