An open API service indexing awesome lists of open source software.

https://github.com/ydah/mtmidi-ruby

Ruby FFI bindings for RtMidi with Ruby-idiomatic MIDI input/output, typed messages, and low-level C API access.
https://github.com/ydah/mtmidi-ruby

audio bindings ffi gem linux macos music ruby windows

Last synced: 24 days ago
JSON representation

Ruby FFI bindings for RtMidi with Ruby-idiomatic MIDI input/output, typed messages, and low-level C API access.

Awesome Lists containing this project

README

          

# 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`.

```bash
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:

```ruby
gem "rtmidi-ruby"
```

Then install dependencies:

```bash
bundle install
```

Or install the gem directly:

```bash
gem install rtmidi-ruby
```

## Quick Start

```ruby
require "rtmidi"

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

### List Ports

```ruby
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

```ruby
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

```ruby
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

```ruby
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.

```ruby
require "rtmidi"

message = Rtmidi::Message.parse([0x92, 64, 96])
p message
# => #

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:

```ruby
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:

```ruby
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

```ruby
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

```ruby
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):

- `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:

```bash
bundle exec ruby -Ilib examples/list_ports.rb
```

## Development

```bash
bundle install
bundle exec rspec
bundle exec rake
```

## License

Released under the MIT License. See `LICENSE.txt`.