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_classandwriter_class - Parse and write note, controller, program, pitch bend, SysEx, system common, realtime, and meta events
Installation
Midicraft requires Ruby 3.1 or newer.
bundle add midicraftOr install it directly:
gem install midicraftQuick Start
Build a MIDI file from scratch
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.
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
endTransform authored notes non-destructively
track.notes works on tracks that already contain Midicraft::Events::Note objects, such as tracks created with the DSL.
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.notesonly returns existingMidicraft::Events::Noteobjects. - Imported MIDI is represented as low-level events, not automatically collapsed into
Events::Note. - Parsed note pairs are linked through
NoteOn#offandNoteOff#onwhen matching note-off events are found. - Track-level operations such as
quantizework on the full event list; note-collection transforms apply only to tracks that already containEvents::Note. -
Midicraft.buildcreates 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:
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
endAvailable 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:
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 480Core 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:
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:
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 #=> 7I/O Classes and Callbacks
Sequence#read supports a progress block with the default reader:
seq = Midicraft.read("input.mid") do |current, total|
puts "Read track #{current}/#{total}"
endFor write progress callbacks, use Midicraft::IO::SeqWriter as the sequence writer:
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}"
endYou 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.rbbundle exec ruby examples/dsl_demo.rbbundle exec ruby examples/read_and_print.rb input.mid
Development
Install dependencies and run the test suite:
bundle install
bundle exec rake specGenerate API docs with YARD:
bundle exec rake yardbundle 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.