How do I apply MIDI note off to a note that's being played?

Apologies if this is a dumb question, but I’m not finding an answer (or not recognizing one) after searching the tutorial, the Lang tab and of course Google.

As I mentioned in my introductory post one of the things I want to do with SPi is use it as a synth I can play my Linnstrument through. The tutorial is clear about how to capture note_on events and pass them to a synth, i.e, the following is working in the sense that I can press a pad and hear a note played.

live_loop :midi_piano_on do
  note, velocity = sync "/midi/ls-mapped-mode/*/*/note_on"
  synth :piano, note: note, amp: velocity / 127.0

The problem is that I want the note to stop when I release the pad, not before or after. Instead the note plays for whatever duration is the default for the synth (I’ve tried :piano, :tb303 and :tri)

I can see the note_off messages in the cue log, so they’re definitely coming through. So far I’ve tried creating a second live_loop to match “note_off”. I tried 3 different methods. See the comments for the results of each one.

live_loop :note_on_event do
  note, velocity = sync "/midi/ls-mapped-mode/*/*/note_on"
  synth :tri, note: note, amp: velocity / 127.0
live_loop :note_off_event do
  note, velocity = sync "/midi/ls-mapped-mode/*/*/note_off"
  #Re-attacks the note on key up.
  #synth :tri, note: note, amp: velocity / 127.0
  #No effect. Same result as not having this loop.
  #synth :tri, note: note, amp: 0, on: nil
  #No effect. Same result as not having this loop.
  #midi_note_off note: note

What’s right way to handle the note_off event?


Have a look at this

The article above uses note_on with velocity 0 as the note_off midi signal, which one of my keyboards uses. However I have a second keyboard (on a matrixbrute synth) which uses separate note_on and note_off midi signals. I produced a modified version of the polysynth program which works with this, and I have appended that to the gist for the above article which you can see here.
(the modified version is simplepolysynth2.rb

1 Like

I made a monophonic synth a while ago.
It uses both vel=0 and note_off.

I think the main issue with this kind of project is organising the synth pointers.

Hey Robin, this is excellent. Thanks! Your article (and video!) thoroughly document the code so this is going to be a huge help.

I’m going to need to adapt it to extract the channel number from incoming note events to properly support the Linnstrument’s capabilities.

  • The Linnstrument (much like a stringed instrument) has the same pitch in different rows on its 8x24 keypad array.
  • While a note is sounding, the corresponding pad is sensitive to 3 axes of finger motion:
    • Left-right (X) is typically mapped to pitchbend
    • Back-front (Y) can be used to alter timbre
    • Pressure (Z) is mapped to after-touch.
  • To handle all of that unambiguously, the Linnstrument has what they call “channel per note” mode, meaning that it dynamically assigns a MIDI channel at each note_on event from a pool of the 16 possible midi channels (not sure if it omits channel 10 since that’s typically reserved for drums, so maybe only 15 note polyphony is possible. In either case that’s more than 10 fingers and way, way more than my brain can control in 3 dimensions.)

Do you know if Sonic Pi provides a way to extract the channel number from midi messages?

Oops, just spotted :parse_sync_address in your code. Pasted it into my work and called it after a sync and got
["midi", "ls-mapped-mode", "1", "5", "note_on"]
Nice! Looks like a very general way to parse midi events for a dispatcher.

How do you learn about these undocumented features of Sonic Pi?

@samaaron Why is get_event undocumented? It seems like a vital service for anyone who needs make real-time decisions about incoming midi events.

Thanks @Davids-Fiddle , I agree. Having clear, simple data structures to keep track of what’s currently sounding is vital.

Sam told me when developing the event code that it might be wirth my while looking at the get_event function. At that stage I was helping in debugging some of the OSC event code handling.
I think it is undocumented, partly because it may change (there are already proposed some breaking changes in syntax for handling midi / osc events in the latest post 3.1 beta code:see commits on March 23rd), but it is not yet fixed. get_event is not very user friendly to handle, being really an internal function, and requires quite a bit of fiddling as you can see from my parse function to use it, and it doesn’t pass Sam’s test of only including things easily understood by an 8 or 9 year old.
There is nothing to stop you from looking through the Sonic Pi source code on github to find useful functions!

You can of course also deal with events on separate channels by using a separate live loop to detect each one. by matching an event such as “/midi///channel/note_on” where channel has a fixed numerical value equal to the channel you are testing. If you use one routine, you will have to have something like a case statement to deal with different channel triggers.
although more code may be required using separate live_loops for each channel, it can give a better performance, as less processing is required in each loop.

@robin.newman Here’s what I’ve got working so far. Seems pretty solid. It passes what I think you called the “arms” test which in my case means placing both hands on the Linnstrument trying to cover as many as possible of the 128 keypads wiggling and slapping like mad. I’m not even sure the watchdog loop is needed but there’s no harm in having it.

What I’ve got so far should work with any keyboard, not just the Linnstrument.

Next steps are to begin handling CC reports from pad X,Y,Z motions and adding support for a sustain pedal.

Thanks very much for all your help thus far!

# Simple polysynth that supports note per channel
# Author: Mike Ellis

# Change the following to match your midi device
# and preferred synth
midi_device_name = "ls-mapped-mode"
default_synth = :tri

midi_event_pattern = "/midi/" + midi_device_name + "/*/*/*"

define :parse_sync_address do |address|
  # Courtesy of Robin Newman
  evt = get_event(address).to_s.split(",")
  v= evt[6]
  if v != nil
    return v[3..-2].split("/")
    return ["error"]

define :cheap_hashkey do | channel, note |
  # Multiply the channel by 1000 and add the note
  # to get an integer hashkey for storing
  # note events e.g. note 67 on channel 13 is
  # 13067 .  You can decompose this to get the
  # channel and note back with .divmod(1000).
  return 1000 * channel + note

set "current_synth", default_synth
live_loop :handle_midi_events do
  note, velocity = sync midi_event_pattern
  event = parse_sync_address midi_event_pattern
  eventkind = event[-1]
  eventchannel = event[-2].to_i
  #puts eventkind, eventchannel
  if eventkind == "note_on"
    ref = play note: note, amp: velocity / 127.0, sustain: 50
    set cheap_hashkey(eventchannel, note), ref
  elsif eventkind == "note_off"
    kill get(cheap_hashkey(eventchannel, note))
    # More dispatchers go here
  # Pet the watchdog
  set ":petted", true

live_loop :watchdog do
  # If no note events 4 seconds,
  # assume silence is desired and
  # kill any notes left sounding.
  sleep 4
  petted_recently = get ":petted"
  if petted_recently == false
    puts "Woof! I've been abandoned."
    # Try to kill all possible
    # notes on all possible channels.
    # This is doing it the hard way. I suspect
    # Sonic Pi provides a simpler method.
    ch = 1
    note = 1
    16.times do
      128.times do
        candidate = get(cheap_hashkey(ch, note))
        note += 1
        if candidate == nil
          kill candidate
      ch += 1
    set ":petted", false
1 Like

Nice code. Very succinct. Like your cheap_hashkey and your petted watchdog!

To kill all notes on all possible channels try:


By default Sonic Pi will send this message to all ports on all channels :slight_smile:

1 Like

That’s much nicer. Thanks!