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

https://github.com/ydah/midicraft

A pure Ruby library for building, reading, and writing Standard MIDI Files (SMF)
https://github.com/ydah/midicraft

builder midi midi-files reader ruby smf

Last synced: 10 days ago
JSON representation

A pure Ruby library for building, reading, and writing Standard MIDI Files (SMF)

Awesome Lists containing this project

README

          

# Midicraft

Midicraft is a pure Ruby library for building, reading, and writing Standard MIDI Files (SMF). It provides a high-level DSL for authoring new sequences, note-name and duration helpers, and low-level event access for parsed MIDI data.

## Highlights

- Build new sequences with `Midicraft.build`
- Read and write SMF format 0 and format 1
- Use note names like `"C4"`, `:c4`, and `"Db3"` instead of raw MIDI numbers
- Resolve durations from symbols, shorthand strings, floats, or raw ticks
- Work with GM program names and CC names
- Convert sequences between format 0 and format 1
- Inspect playback time, measure positions, tempo, and time signature data
- Swap reader/writer implementations with `reader_class` and `writer_class`
- Parse and write note, controller, program, pitch bend, SysEx, system common, realtime, and meta events

## Installation

Midicraft requires Ruby 3.1 or newer.

```bash
bundle add midicraft
```

Or install it directly:

```bash
gem install midicraft
```

## Quick Start

### Build a MIDI file from scratch

```ruby
require "midicraft"

seq = Midicraft.build(tempo: 120, time_signature: [4, 4]) do
track "Melody", instrument: :acoustic_grand_piano, channel: 0 do
note "C4", velocity: 100, duration: :quarter
note "E4", velocity: 100, duration: :quarter
note "G4", velocity: 100, duration: :quarter
note "C5", velocity: 100, duration: :half
rest :quarter
chord ["C4", "E4", "G4"], velocity: 80, duration: :whole
end

track "Bass", instrument: :acoustic_bass, channel: 1 do
note "C2", velocity: 90, duration: :half
note "G2", velocity: 90, duration: :half
note "C2", velocity: 90, duration: :whole
end
end

seq.write("output.mid")
```

### Read and inspect an existing MIDI file

`Midicraft.read` exposes imported MIDI as low-level events such as `NoteOn` and `NoteOff`.

```ruby
require "midicraft"

seq = Midicraft.read("input.mid")

seq.tracks.each do |track|
puts "Track: #{track.name || "(unnamed)"}"

track.events.grep(Midicraft::Events::NoteOn).each do |event|
duration = event.off ? (event.off.absolute_time - event.absolute_time) : nil
duration_label = duration ? duration.to_s : "open"

puts " #{Midicraft.note_name(event.pitch)} " \
"ch=#{event.channel} vel=#{event.velocity} " \
"start=#{event.absolute_time} dur=#{duration_label}"
end
end
```

### Transform authored notes non-destructively

`track.notes` works on tracks that already contain `Midicraft::Events::Note` objects, such as tracks created with the DSL.

```ruby
require "midicraft"

seq = Midicraft.build do
track "Lead", instrument: :electric_guitar_clean, channel: 0 do
note "C4", duration: :quarter
note "E4", duration: :quarter
note "G4", duration: :quarter
end
end

lead = seq.tracks.find { |track| track.name == "Lead" }

edited = lead.transform do |track|
track.notes.transpose!(12)
track.notes.quantize!(:sixteenth, ppqn: seq.ppqn)
track.notes.velocity_scale!(0.9)
end

edited.events.each { |event| puts event }
```

## Data Model

Midicraft uses two related note representations:

| Workflow | Track contents | Best API |
| --- | --- | --- |
| Authoring with `Midicraft.build` or manual `Events::Note` objects | `Midicraft::Events::Note` | `track.notes`, `Midicraft::NoteCollection`, DSL helpers |
| Reading an existing MIDI file with `Midicraft.read` | `Midicraft::Events::NoteOn`, `NoteOff`, and other raw events | `track.events` |

Important details:

- `track.notes` only returns existing `Midicraft::Events::Note` objects.
- Imported MIDI is represented as low-level events, not automatically collapsed into `Events::Note`.
- Parsed note pairs are linked through `NoteOn#off` and `NoteOff#on` when matching note-off events are found.
- Track-level operations such as `quantize` work on the full event list; note-collection transforms apply only to tracks that already contain `Events::Note`.
- `Midicraft.build` creates a format 1 sequence with a meta track at index 0 for tempo and time signature events.

## DSL

The builder DSL is the easiest way to author new material:

```ruby
Midicraft.build(tempo: 140, ppqn: 480, time_signature: [3, 4]) do
track "Lead", instrument: :electric_guitar_clean, channel: 0 do
note "C4", velocity: 100, duration: :quarter
chord ["C4", "E4", "G4"], velocity: 80, duration: :half
rest :quarter
control :volume, 100
control :pan, 64
pitch_bend 8192
program :electric_guitar_clean
repeat 4 do
note "C4", duration: :eighth
end
at_tick 1920 do
note "G4", duration: :quarter
end
at_bar 3, beat: 1 do
note "A4", duration: :quarter
end
velocity 60 do
note "B4", duration: :quarter
end
end
end
```

Available builder methods:

- `track(name = nil, instrument: nil, channel: nil)`
- `note(pitch, velocity: 100, duration: :quarter)`
- `chord(pitches, velocity: 100, duration: :quarter)`
- `rest(duration)`
- `control(number_or_name, value)`
- `pitch_bend(value)`
- `program(name_or_number)`
- `repeat(count) { ... }`
- `at_tick(tick) { ... }`
- `at_bar(bar, beat: 1) { ... }`
- `velocity(value) { ... }`

## Duration Values

Durations accept several input styles:

| Type | Examples |
| --- | --- |
| Symbol | `:whole`, `:half`, `:quarter`, `:eighth`, `:sixteenth` |
| Dotted symbol | `:dotted_quarter`, `:dotted_eighth` |
| Triplet symbol | `:quarter_triplet`, `:eighth_triplet` |
| String shorthand | `"1n"`, `"4n"`, `"8n"`, `"4n."`, `"4nt"` |
| Integer | Raw tick value such as `480` |
| Float | Quarter-note multiplier such as `1.0`, `0.5`, or `4.0` |

You can also use sequence helpers when you need explicit conversions:

```ruby
seq.note_to_length("dotted quarter triplet") #=> 1.0
seq.note_to_delta("eighth") #=> 240 when ppqn is 480
seq.length_to_delta(0.5) #=> 240 when ppqn is 480
```

## Core API

### Top-level helpers

- `Midicraft.read(path_or_io) { |current, total| ... }`
- `Midicraft.build(tempo: 120, ppqn: 480, time_signature: [4, 4])`
- `Midicraft.note_number("C4")`
- `Midicraft.note_name(60)`

### `Midicraft::Sequence`

Useful sequence methods include:

- `tempo`, `tempo=`, `bpm`
- `time_signature`, `time_signature=`
- `name`, `name=`
- `duration_ticks`, `duration_seconds`
- `pulses_to_seconds`
- `note_to_length`, `note_to_delta`, `length_to_delta`
- `get_measures`
- `to_format0`, `to_format1`
- `write(path_or_io, running_status: false, note_off_as_note_on_zero: false, midi_format: nil)`

Example:

```ruby
seq = Midicraft.build(tempo: 128) do
track "Piano", channel: 0 do
note "C4", duration: :quarter
end
end

puts seq.duration_seconds
puts seq.get_measures.to_mbt(seq.tracks.last.notes.first)

seq.write("format0.mid", midi_format: 0)
```

### `Midicraft::Track`

Useful track methods include:

- `add(event)` / `<<`
- `remove(event)`
- `merge(other_track_or_events)`
- `quantize(length_or_note)` for in-place event quantization
- `transform { |copy| ... }` for non-destructive track edits
- `name`, `name=`
- `instrument`, `instrument=`
- `instrument_name`, `instrument_name=`
- `notes`

### `Midicraft::NoteCollection`

`track.notes` returns a `Midicraft::NoteCollection` when the track contains `Events::Note` objects.

- `transpose(n)` / `transpose!(n)`
- `quantize(grid, ppqn: 480)` / `quantize!(grid, ppqn: 480)`
- `velocity_scale(factor)` / `velocity_scale!(factor)`
- `humanize(timing: 10, velocity: 10)`
- `legato(overlap: 0)`
- `filter { |note| ... }`
- `in_range(low, high)`
- `on_channel(channel)`

## Notes, Frequencies, and Constants

Midicraft includes helpers beyond raw SMF parsing:

```ruby
Midicraft::NoteUtil.frequency("A4") #=> 440.0
Midicraft::NoteUtil.from_frequency(261.63) #=> 60

Midicraft::Constants::GM.program_number(:violin) #=> 40
Midicraft::Constants::GM.program_name(40) #=> :violin
Midicraft::Constants::GM.drum_note(:closed_hi_hat)

Midicraft::Constants::CC.number_for(:sustain) #=> 64
Midicraft::Constants::CC.name_for(64) #=> :sustain
Midicraft::Constants::CC::VOLUME #=> 7
```

## I/O Classes and Callbacks

`Sequence#read` supports a progress block with the default reader:

```ruby
seq = Midicraft.read("input.mid") do |current, total|
puts "Read track #{current}/#{total}"
end
```

For write progress callbacks, use `Midicraft::IO::SeqWriter` as the sequence writer:

```ruby
seq = Midicraft.build do
track "Lead", channel: 0 do
note "C4", duration: :quarter
end
end

seq.writer_class = Midicraft::IO::SeqWriter

seq.write(
"output.mid",
midi_format: 0,
running_status: true,
note_off_as_note_on_zero: true
) do |track, total, index|
label = track ? (track.name || "(unnamed)") : "start"
puts "Write #{index}/#{total}: #{label}"
end
```

You can also replace `reader_class` or `writer_class` with compatible custom classes if you need different parsing or writing behavior.

## Examples

The repository includes small example scripts:

- `bundle exec ruby examples/from_scratch.rb`
- `bundle exec ruby examples/dsl_demo.rb`
- `bundle exec ruby examples/read_and_print.rb input.mid`

## Development

Install dependencies and run the test suite:

```bash
bundle install
bundle exec rake spec
```

Generate API docs with YARD:

```bash
bundle exec rake yard
```

`bundle exec rake` runs the default task, which is the spec suite.

## License

The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).