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)
- Host: GitHub
- URL: https://github.com/ydah/midicraft
- Owner: ydah
- License: mit
- Created: 2026-03-06T13:21:32.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-03-07T08:43:12.000Z (4 months ago)
- Last Synced: 2026-06-12T21:06:41.104Z (10 days ago)
- Topics: builder, midi, midi-files, reader, ruby, smf
- Language: Ruby
- Homepage:
- Size: 79.1 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE.txt
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).