Project

midicraft

0.0
No release in over 3 years
A pure Ruby MIDI library for reading/writing Standard MIDI Files with a modern, expressive DSL interface, note name support, and fluent method chaining.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies
 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.

bundle add midicraft

Or install it directly:

gem install midicraft

Quick 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
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.

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:

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:

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:

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              #=> 7

I/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}"
end

For 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}"
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:

bundle install
bundle exec rake spec

Generate API docs with YARD:

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.