Monday, June 27, 2011

High-level realtime MIDI IO with Ruby

Update (9/8/2011): I've created another library that wraps all of the concepts of this post in a Ruby DSL and adds shorthand notation and some other fun things. Read about MicroMIDI here


Understandably, a few people have asked me for advice on how to input and output MIDI in a human friendly way with unimidi so I've decided to put together a quick tutorial. I'll be focusing on two gem libraries that I wrote: midi-message, which deals soley with MIDI message objects, and midi-eye, a library for reacting to MIDI input. Of course, it should be mentioned that there's no one way to do this with unimidi.  You can use whatever MIDI objects you like or create your own classes-- unimidi just deals in raw low-level bytes.  There are other libraries such as midilib that provide an intriguing alternative and could work pretty easily with unimidi.  Or one could get creative and go off with a totally unconventional approach as well. For the examples that follow, I'm using a MIDI input and output that I specify with unimidi.
require 'unimidi'

@input = UniMIDI::Input.use(:first)
@output = UniMIDI::Output.use(:first)

If you copy and paste these, they will just open the first MIDI devices available on your computer. You should determine which MIDI devices you want to use and edit these statements to suit your setup. (here's a blog post that goes into more detail on this)

Dealing with MIDI input using midi-eye

My preferred way of dealing with MIDI input is to react to arriving messages with an event listener.  Midi-eye makes this easy and its constructor accepts a unimidi input to attach to.  Here is an example that will react to all incoming messages in the same way by printing them to the screen

require 'midi-eye'

listener = MIDIEye::Listener.new(@input)
listener.on_message do |event|
  puts event[:timestamp]
  puts event[:message]
end

listener.start
Chances are if you're working with MIDI input that you will want to cherry-pick certain messages, or at least react in a different way depending on what type of message you've received eg. a note message, control change, etc.  To accomplish this, arguments can be passed to the Listener#listen_for method which will match against properties of the incoming messages
listener.listen_for(:class => [MIDIMessage::NoteOn, MIDIMessage::NoteOff]) do |event|

  # raise the note value by an octave
  event[:message].note += 12

  # send the altered note message to the output you chose earlier
  @output.puts(event[:message])

end

listener.start
In this example, I take all note messages (identified by their class), transpose them up one octave and send them to my MIDI output. You can add as many of these callbacks as you like, just keep calling Listener#listen_for. While that type of matching will be useful in a lot of cases, it is limited by the fact that it only matches positively against the properties and values you pass in. If you need more complex matching, I recommend putting a conditional statement within the callback.
listener.listen do |event|

  # is this a note above C3?
  if event[:message].respond_to?(:note) && event[:message].note > 48

    # if so, lower the note value by a fifth
    event[:message].note -= 7

  end

  # and send the message to the unimidi output
  @output.puts(event[:message])

end

listener.start

(listen and listen_for are actually the same method, I just think it looks cleaner to call listen when there is no matching happening)

Threading

Pass :background => true to listener.start to have the listener work only in a background thread. This will allow you to run other listeners or foreground threads while that particular listener is running in the background.

Output MIDI using midi-message

In those examples, I sent messages to an output-- but I didn't create those messages myself. The midi-message library allows you to create messages like that yourself in a flexible way.

require 'midi-message'
include MIDIMessage
Here are three different MIDI note-on messages created using three different methods.
messages = []

messages << NoteOn.new(0, 48, 64) # C3

messages << NoteOn["E3"].new(0, 100)

with(:channel => 0, :velocity => 100) do
  messages << note_on("G3")
end
With those message objects in hand, I pass each to UniMIDI::Output#puts the same way you saw earlier.
messages.each { |message| @output.puts(message) }
That's it... and it works the same for all types of MIDI messages including sysex. You can find some info on creating sysex messages here.

http://github.com/arirusso/midi-eye
http://github.com/arirusso/midi-message
http://github.com/arirusso/unimidi

2 comments:

  1. It's pretty good but I have been develop MIDI using JAVA, I took a time to benchmark both Java and your library to compare which timestamp are accurate, it seem whether chord keys (more than one keys was press at the same time) was pressed, your libraries couldn't get the same milliseconds timing while Java could.

    Any chance to fix inaccurate timing?

    ReplyDelete
  2. I will certainly look at that -- these libraries are still relatively new. When you get a chance post or email me some a more in-depth description of the tests you were doing as well as what OS and Ruby environment you're using. Thanks!

    ReplyDelete