Delay in Thread variable synchronisation

I have a function that creates a live_loop dynamically

define :start_note do |note|
  set :enabled,1
  print("add", note)
  live_loop "loop-#{note}" do
    ena = get[:enabled]
    print("Loop", ena)
    synth :bass_foundation , note: note
    sleep 0.1
    ena = get[:enabled]
    print("Loop test", ena)
    if ena==0
      print("Loop stop")
      stop
    end
    print("loop next")
  end
end

I trigger the function via midi in messages. There is also a stop function:

define :stop_note do |note|
  set :enabled,0
  print("remove")
end

Now when I trigger a midi note for a very short duration I still hear the synth playback about 4 times which means the loop is executed 4 times before it detects that :enabled is 0.

I have also tested using a global variable ā€œenabledā€ directly without using set and get. Same behaviour.

How can I get the live_loop (or thread) to react faster?

A nice idea to create a named live_loop then stop to kill your note. However I think this is quite resource intensive. I had a play and prevented the multiple note triggering (which will also add to the resources) by testing to see if the note has already been triggered.
This sort of worked but is not very fast.

define :start_note do |note|
  use_real_time
  currentnote=0
  set :enabled,1
  live_loop "loop-#{note}" do
    use_real_time
    ena = get[:enabled]
    stop if ena==0
    synth :piano , note: note if currentnote !=note
    currentnote=note
    sleep 0.05
    
  end
end

define :stop_note do |note|
  use_real_time
  set :enabled,0
  print("remove")
end

#added net two live_loops to get input from my keyboard which uses note_on with velocity zero for note release
live_loop :mstart do
  use_real_time
  n,v=sync "/midi*/note_on"
  puts n,v
  start_note(n) if v>0
end
live_loop :mstop do
  use_real_time
  n,v=sync "/midi*/note_on"
  stop_note n   if v==0
end

My own approach to get keyboard input playing a synth directly (which is essentially what this is) is shown in the program below which I originally wrote in 2017.
It keeps a record of notes started playing (each in initialised to last 5 beats) and then when the key is released it sends a signal to kill the particular note.
If your keyboard uses note_on and note_off rather than note_on with velocity 0 for note_off then it needs tweaking slightly.

This program will play midi-files OK (no channel 10 in this version) eg just tried it with Scott Joplinā€™s The Entertainer.

#polyphonic midi input program with sustained notes
#experimental program by Robin Newman, November 2017 
#tweaked to accommodate changes to midi cue handling in Sonic Pi from version 3.2.0
#pitchbend can be applied to note BEFORE IT STARTS

set :pb,0 #pitchbend value
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 particular 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

live_loop :pb do #get current pitchbend value adjusted in range -12 to +12 (octave)
  b = sync "/midi*/pitch_bend" #change to match your controller
  set :pb,(b[0]-8192).to_f/8192*12
end


live_loop :midi_piano_on do #this loop starts 5 second notes for specified pitches and stores reference
  use_real_time
  note, on = sync "/midi*/note_on" #change to match your controller
  if on >0
    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 :bass_foundation
      #max duration of note set to 5 on next line. Can increase if you wish.
      x = play note+get(:pb),amp: on/127.0, sustain: 5 #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
      puts"set #{ns[note]} to be killed "
      plist << get(ns[note])#add reference to note to kill list plist
    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

The idea was to see if I can implement something like what Korg did in their WaveState synthesizer.

In essence each note triggers a small program that plays back a certain pattern.

In the example above the dynamically created program just plays back the note. More complex patterns would be easy to implement however.

Unfortunately if a ā€˜note offā€™ needs more than 400ms before it can stop a live loop thread then this approach is not feasible.

Itā€™s likely that the delay youā€™re seeing is due to the default schedule ahead time of 500ms that Sonic Pi uses to ensure timing is accurate regardless of any timing drift in the language runtime. The supported way of circumnavigating this default latency is to use use_real_time in the threads you wish to run in ā€˜realtime modeā€™ - which is typically when you want a thread that responds immediately to external events.

Can you have a look at my other topic, in which I have created a minimal program demonstrating the issue?

1 Like

This is so weird! Just tested on my system (Arch Linux, Pipewire), and I get essentially the same delay (~400ms) with and without the use_real_time statements. I also tried putting a single use_real_time statement at the top of the buffer, on the theory that the threads might somehow sync better when inheriting the scheduling parameter from a common source, but no luck. Also tried set_sched_ahead_time! 0 and 1 at the top of the buffer, and neither of those changed anything.

This is pretty contrary to my intuition about Time State. Can anyone explain why it seems not to be the same between the two threads, or how to get it to behave as a proper global store of ā€œreal-timeā€ parameters?

Or is it somehow an issue with @dwjbosman1 's and my systems, and for other people it works as expected?