rtmidi-ruby

rtmidi-ruby is a Ruby FFI binding for the RtMidi C API (rtmidi_c.h). It exposes the low-level C bindings through Rtmidi::Native and a higher-level Ruby API through Rtmidi::MidiIn, Rtmidi::MidiOut, and Rtmidi::Message.

Features

  • Rtmidi.version, Rtmidi.compiled_apis, and API name helpers
  • low-level access to the RtMidi C API via Rtmidi::Native
  • Rtmidi::MidiIn with callback and polling based input
  • Rtmidi::MidiOut helpers for channel, system common, and realtime messages
  • typed MIDI messages through Rtmidi::Message
  • virtual port support

Requirements

  • Ruby 3.1+
  • a system librtmidi that provides the RtMidi C API

Install librtmidi with your package manager:

  • macOS: brew install rtmidi
  • Ubuntu/Debian: sudo apt install librtmidi-dev
  • Fedora: sudo dnf install rtmidi-devel
  • Arch: sudo pacman -S rtmidi
  • Windows: install an RtMidi DLL and make sure it is on PATH

If the library is installed in a non-standard location, set RTMIDI_LIB_PATH.

RTMIDI_LIB_PATH=/path/to/librtmidi.dylib bundle exec ruby your_script.rb

Some librtmidi builds do not expose rtmidi_set_error_callback. In that case, normal MIDI I/O still works, but on_error callbacks are unavailable.

Installation

Add the gem to your Gemfile:

gem "rtmidi-ruby"

Then install dependencies:

bundle install

Or install the gem directly:

gem install rtmidi-ruby

Quick Start

require "rtmidi"

puts "RtMidi version: #{Rtmidi.version}"
puts "Compiled APIs: #{Rtmidi.compiled_apis.inspect}"

List Ports

require "rtmidi"

out = Rtmidi::MidiOut.new
puts "Output ports:"
out.port_names.each_with_index { |name, index| puts "  #{index}: #{name}" }
out.close

input = Rtmidi::MidiIn.new
puts "Input ports:"
input.port_names.each_with_index { |name, index| puts "  #{index}: #{name}" }
input.close

Send Note On/Off

require "rtmidi"

out = Rtmidi::MidiOut.new

if out.port_count.zero?
  warn "No output ports available."
else
  begin
    out.open_port(0)
    out.note_on(0, 60, 100)
    sleep 0.5
    out.note_off(0, 60)
  ensure
    out.close
  end
end

Receive With Callback

require "rtmidi"

midi_in = Rtmidi::MidiIn.new

if midi_in.port_count.zero?
  warn "No input ports available."
  midi_in.close
  exit 0
end

begin
  midi_in.ignore_types(sysex: false, timing: false, active_sensing: false)
  midi_in.open_port(0)

  midi_in.on_message do |message, timestamp|
    puts "#{timestamp}: #{message.map { |byte| format('%02X', byte) }.join(' ')}"
  end

  puts "Listening... (Ctrl-C to quit)"
  sleep
ensure
  midi_in&.close
end

Receive With Polling

require "rtmidi"

midi_in = Rtmidi::MidiIn.new

if midi_in.port_count.zero?
  warn "No input ports available."
  midi_in.close
  exit 0
end

begin
  midi_in.open_port(0)

  loop do
    packet = midi_in.get_message
    next if packet.nil?

    message, timestamp = packet
    puts "#{timestamp}: #{message.inspect}"

    sleep 0.001
  end
ensure
  midi_in&.close
end

Typed Messages

Rtmidi::Message can parse raw bytes into typed structs and MidiOut#send_message accepts either raw byte arrays or typed messages.

require "rtmidi"

message = Rtmidi::Message.parse([0x92, 64, 96])
p message
# => #<struct Rtmidi::Message::NoteOn channel=2, note=64, velocity=96>

out = Rtmidi::MidiOut.new

if out.port_count.zero?
  warn "No output ports available."
else
  begin
    out.open_port(0)
    out.send_message(Rtmidi::Message::ProgramChange.new(channel: 1, program: 10))
  ensure
    out.close
  end
end

For typed input callbacks:

midi_in.on_message(parsed: true) do |message, timestamp|
  p [message.class, message, timestamp]
end

System/Common and Realtime Helpers

Rtmidi::MidiOut includes helpers for common output messages:

  • sysex
  • control_change
  • program_change
  • pitch_bend
  • channel_aftertouch
  • poly_aftertouch
  • time_code_quarter_frame
  • song_position_pointer
  • song_select
  • tune_request
  • timing_clock
  • start
  • continue
  • stop
  • active_sensing
  • system_reset
  • nrpn

Example:

require "rtmidi"

out = Rtmidi::MidiOut.new

if out.port_count.zero?
  warn "No output ports available."
else
  begin
    out.open_port(0)
    out.sysex([0x7D, 0x01])
    out.song_select(3)
    out.start
    out.nrpn(0, 0x1234, 0x0567)
  ensure
    out.close
  end
end

Virtual Ports

require "rtmidi"

midi_in = Rtmidi::MidiIn.new
midi_out = Rtmidi::MidiOut.new

begin
  midi_in.open_virtual_port(name: "Rtmidi Ruby Virtual In")
  midi_out.open_virtual_port(name: "Rtmidi Ruby Virtual Out")

  puts "Virtual ports opened."
  sleep
ensure
  midi_out&.close
  midi_in&.close
end

Low-Level C API

require "rtmidi"

handle = nil

begin
  handle = Rtmidi::Native.rtmidi_out_create_default
  Rtmidi::Native.check_error(handle)

  count = Rtmidi::Native.rtmidi_get_port_count(handle)
  Rtmidi::Native.check_error(handle)

  puts "#{count} output ports found"
ensure
  Rtmidi::Native.rtmidi_out_free(handle) if handle && !handle.null?
end

Error Handling

Synchronous operations raise Rtmidi::Error subclasses where possible, including:

  • Rtmidi::NoDevicesError
  • Rtmidi::InvalidPortError
  • Rtmidi::InvalidUseError
  • Rtmidi::DriverError

Validation failures use ArgumentError.

If a callback raises, the exception is stored and surfaced on the next locked API call.

Callback Notes

  • Keep callback processing lightweight.
  • For heavier work, pass the event to a Queue and handle it in another thread.
  • Do not call close or close_port from inside an input callback.
  • parsed: true yields Rtmidi::Message::* structs instead of raw byte arrays.

Examples

The repository includes runnable examples in examples/:

  • examples/list_ports.rb
  • examples/send_note.rb
  • examples/sysex_send.rb
  • examples/receive_callback.rb
  • examples/receive_polling.rb
  • examples/virtual_port.rb

Run them against the local checkout with:

bundle exec ruby -Ilib examples/list_ports.rb

Development

bundle install
bundle exec rspec
bundle exec rake

License

Released under the MIT License. See LICENSE.txt.