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::MidiInwith callback and polling based inputRtmidi::MidiOuthelpers for channel, system common, and realtime messages- typed MIDI messages through
Rtmidi::Message - virtual port support
Requirements
- Ruby 3.1+
- a system
librtmidithat 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. do |, |
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.
next if packet.nil?
, = 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"
= Rtmidi::Message.parse([0x92, 64, 96])
p
# => #<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.(Rtmidi::Message::ProgramChange.new(channel: 1, program: 10))
ensure
out.close
end
end
For typed input callbacks:
midi_in.(parsed: true) do |, |
p [.class, , ]
end
System/Common and Realtime Helpers
Rtmidi::MidiOut includes helpers for common output messages:
sysexcontrol_changeprogram_changepitch_bendchannel_aftertouchpoly_aftertouchtime_code_quarter_framesong_position_pointersong_selecttune_requesttiming_clockstartcontinuestopactive_sensingsystem_resetnrpn
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::NoDevicesErrorRtmidi::InvalidPortErrorRtmidi::InvalidUseErrorRtmidi::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
Queueand handle it in another thread. - Do not call
closeorclose_portfrom inside an input callback. parsed: trueyieldsRtmidi::Message::*structs instead of raw byte arrays.
Examples
The repository includes runnable examples in examples/:
examples/list_ports.rbexamples/send_note.rbexamples/sysex_send.rbexamples/receive_callback.rbexamples/receive_polling.rbexamples/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.