MUFF WIGGLER Forum Index
 FAQ & Terms of UseFAQ & Terms Of Use   Wiggler RadioMW Radio   Muff Wiggler TwitterTwitter   Support the site @ PatreonPatreon 
 SearchSearch   RegisterSign up   Log inLog in 
WIGGLING 'LITE' IN GUEST MODE

raspberrypi based midi router/filter
MUFF WIGGLER Forum Index -> Music Tech DIY  
Author raspberrypi based midi router/filter
imcmahon
This is a simple project that I came up with to solve a live performance need.

I play live in a classic rock/blues band with a Nord Electro 5D primarily, and I have started to add in my Nord Lead A1 for a bit more complexity for certain tunes. The 5D is a great instrument for live performance, and one of the things that's great about it is the beautiful OLED display and patch organization features. My workflow is to use the "set lists" feature to group patches for a tune (usually I only use 1, but the synth supports 4 in a performance patch), and name each performance patch after the tune. The "organize" feature makes it super simple to reorder performance patches within a setlist, and I can scroll through them in order as the show progresses, no paper setlist required, and no patch reference sheet or fumbling for patches.

Unfortunately, the A1 isn't quite as refined in this manner; it has the same general bank/patch scheme as the electro, where a performance patch is a layered collection of up to four timbre patches, and has the same four banks of 50 scheme of performance patches that the 5D has. My idea for keeping patches synchronized between the synths with minimal room for error in a live setting was to always select the same bank/patch on both synths, and do all my sound design on the lead in the appropriate patch. In order to minimize human error, I wanted a mechanism to synchronize program changes between them.

Unfortunately, nord's midi configuration doesn't allow you to send bank/program changes on a separate channel from note data. It's possible to make both synths respond to programming changes from either synth, but in that case they both respond to notes from either synth as well, which made that useless for performance.

There are commercial midi router/filter solutions on the market, Midi Solutions makes a couple. I'm terribly impatient however, and figured rather than spending $150 and waiting a week, I'd try to fabricobble something up. I had some rpi3 boards on hand, and it turned out to be a very workable solution.

Because it's implemented in code, there's not a terribly elegant way to make it "user-serviceable", so I haven't been thinking of it as a commercializable product. The Midi Solutions devices handle this by programming rules via sysex messages, but there's a limited syntax for describing these rules, which limits how crazy you can get with it.

I started with the program change forwarding rules, but I quickly branched out to do more complex stuff. I wrote a CC mangling filter for a friend; his issue was one piece of gear outputting expression data on one CC, but the gear he wanted it to control expects it on another. I wanted the ability to have notes on one synth play on the other, but on a per-program basis. I wrote a filter which keeps state of bank and program change events, and program-specific patches that route or mangle only if a particular program has been selected. It should be possible to implement stuff like midi effects (delay should be trivially easy, and I may implement it soon to demonstrate), complex arpeggiators, even triggering internal sequences from midi events.

It's quite easy to set up; I started with a stock raspbian installation, and the only significant dependency is libportmidi, which installed happily with apt. I then wrote a trivial amount of Go code to handle the routing and filtering. Here's the repository for the code I'm running currently; I haven't coalesced this into a proper project that other people can dive into unassisted, I figured I'd kick it off here and see how much interest there is, and improve the rough edges.

https://github.com/ianmcmahon/midiroute

For my needs, I'm able to use entirely USB midi devices, as both synths and most other things I might want to incorporate in the future support USB midi. It's not hard to use serial midi devices with it; the simplest solution is to get a USB midi device like a midisport 2x2 and plug it into the rpi, but it's also possible to wire DIN jacks to a serial port on the rpi.

The rpi acts as a host, and both synths are cabled via USB to the rpi. The portmidi library saw the USB midi devices straight away. The code allows me to specify a list of partial string matches to filter the number of midi devices it will consider down to only the devices I care about. There are several loopback and system interfaces that show up, and I don't want to waste processing time considering them:

Code:

   devices := devicesMatching("Nord")

   for n, dev := range devices {
      fmt.Printf("Opening device: %s\n", n)
      var err error
      dev.inputStream, err = portmidi.NewInputStream(dev.inputDeviceID, 1024, 0)
      if err != nil {
         closer.Fatalln("[ERR] cannot init input stream: ", err)
      }
      closer.Bind(func() {
         dev.inputStream.Close()
      })
      dev.outputStream, err = portmidi.NewOutputStream(dev.outputDeviceID, 1024, 0, 0)
      if err != nil {
         closer.Fatalln("[ERR] cannot init output stream: ", err)
      }
      closer.Bind(func() {
         dev.outputStream.Close()
      })
   }


I then set up a list of filters, which I'll go over shortly. Then I construct an NxN routing matrix, where every event that's received is evaluated by each filter, for each output device, to determine whether it should be forwarded to that device (and optionally mangled).

Code:

   for _, s := range devices {
      go func(src *Device) {
         for ev := range src.inputStream.Source() {
            for _, dest := range devices {
               if src == dest {
                  continue
               }
               for _, filter := range filters {
                  if filter(src, dest, &ev) {
                     //fmt.Printf("%s -> %s: %x %x %x\n", src.Name, dest.Name, ev.Message.Status(), ev.Message.Data1(), ev.Message.Data2())
                     dest.outputStream.Sink() <- ev
                  }
               }
            }
         }
      }(s)
   }


Every device gets its own input goroutine, so it's listening for events from every device in parallel. Note that there's code in there that short-circuits processing events for routing back to the source device, but it occurs to me that one may want to mangle events and send them back to the source in certain cases, like the case of a delay effect, I might want to receive events from the Electro, and send them back to the electro with reduced velocity after a delay. Also as I look at it, I should probably run each filter as a goroutine so that they can run non-blocking. This'll be necessary for something like a delay, otherwise event processing will be halted until the delayed event is returned.

Code:


func programChangeFilter() filterFunc {
   return func(s, d *Device, event *portmidi.Event) bool {
      if event.Message.Status() == 0xc0 {
         program &= 0xFFFF0000
         program |= uint32(event.Message.Data1()) << 8
         program |= uint32(event.Message.Data2()) << 16
         return true
      }
      if event.Message.Status() == 0xb0 {
         if event.Message.Data1() == 0x00 {
            program &= 0x00FFFFFF
            program |= uint32(event.Message.Data2()) << 24
            return true
         }
         if event.Message.Data1() == 0x20 {
            program &= 0xFF00FFFF
            program |= uint32(event.Message.Data2()) << 16
            return true
         }
      }
      return false
   }
}


The program change filter is very simple; it looks more complex at first glance because of the code that saves state of the program changes, and the necessary bitwise manipulation to do it. Disregarding that, this code simply forwards all events that are bank or program change messages. It doesn't take into consideration source or dest device, so any program change messages from any devices in the routing matrix will be distributed to every other device. With my setup, this works great, as I can scroll through performance patches on either synth, and both will change in lockstep.

Code:

func nordSlotChangeMessage(s *Device, event *portmidi.Event) (bool, byte) {
   if s.matches("Electro") && event.Message.Status() == STATUS_CC && event.Message.Data1() == CC_SLOT_CHANGE {
      return true, event.Message.Data2()
   }
   return false, 0xFF
}

func nordElectroSlotTracking() filterFunc {
   return func(s, d *Device, event *portmidi.Event) bool {
      if ok, v := nordSlotChangeMessage(s, event); ok {
         slot = v
      }

      return false
   }
}

func setLeadHold(d *Device, state bool, timestamp int32) {
   var val byte = HOLD_OFF
   if state {
      val = HOLD_ON
   }
   d.outputStream.Sink() <- portmidi.Event{timestamp, portmidi.NewMessage(STATUS_CC, CC_HOLD, val), nil}
}


This bit sets up slot tracking. The 5D has four slots in a performance patch, each of which can contain a program. The slot changes emit a midi event, and I was able to write a filter which tracks slot changes similarly to how I track program changes, so that other filters can make decisions on how to route events based on the selected patch. setLeadHold() gives me the ability to set the "hold" feature to on or off as required for a program filter.

Code:

func kashmirMangle() filterFunc {
   return func(s, d *Device, event *portmidi.Event) bool {
      if program != PROG_KASHMIR {
         return false
      }

      if ok, v := nordSlotChangeMessage(s, event); ok {
         switch v {
         case SLOT_A:
            setLeadHold(d, true, event.Timestamp)
         case SLOT_B:
            setLeadHold(d, true, event.Timestamp)
         default:
            setLeadHold(d, false, event.Timestamp)
         }
      }

      switch slot {
      case SLOT_A:
         return s.matches("Lead") && d.matches("Electro") // slot C, lead plays electro
         return false
      case SLOT_B:
         return true
      case SLOT_C:
         return true
      case SLOT_D:
         return s.matches("Lead") && d.matches("Electro") // slot C, lead plays electro
      default:
         return false
      }

      return false
   }
}


This is an example of a more involved filter. It's program specific, so it only functions if the desired program has been selected. Further, I use slot tracking to alter which keyboard controls which voices across both synths, as well as to enable or disable hold on the A1 depending on the slot selected. This gives me the ability to play drones hands-free while playing two hand pieces on the 5D, and quickly reconfigure to stop the drone and bring in voices on the A1 as needed.


I know this isn't quite polished enough to turn loose to laymen, but the code is simple enough that I think wigglers can likely bend to their nefarious purposes. I'm happy to answer any questions!
MUFF WIGGLER Forum Index -> Music Tech DIY  
Page 1 of 1
Powered by phpBB © phpBB Group