{"id":50816046,"url":"https://github.com/ydah/midicraft","last_synced_at":"2026-06-13T09:33:42.215Z","repository":{"id":364377388,"uuid":"1174461823","full_name":"ydah/midicraft","owner":"ydah","description":"A pure Ruby library for building, reading, and writing Standard MIDI Files (SMF)","archived":false,"fork":false,"pushed_at":"2026-03-07T08:43:12.000Z","size":81,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-12T21:06:41.104Z","etag":null,"topics":["builder","midi","midi-files","reader","ruby","smf"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ydah.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-06T13:21:32.000Z","updated_at":"2026-05-20T13:35:57.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ydah/midicraft","commit_stats":null,"previous_names":["ydah/midicraft"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/ydah/midicraft","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ydah%2Fmidicraft","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ydah%2Fmidicraft/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ydah%2Fmidicraft/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ydah%2Fmidicraft/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ydah","download_url":"https://codeload.github.com/ydah/midicraft/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ydah%2Fmidicraft/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34276697,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-13T02:00:06.617Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["builder","midi","midi-files","reader","ruby","smf"],"created_at":"2026-06-13T09:33:42.145Z","updated_at":"2026-06-13T09:33:42.197Z","avatar_url":"https://github.com/ydah.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Midicraft\n\nMidicraft 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.\n\n## Highlights\n\n- Build new sequences with `Midicraft.build`\n- Read and write SMF format 0 and format 1\n- Use note names like `\"C4\"`, `:c4`, and `\"Db3\"` instead of raw MIDI numbers\n- Resolve durations from symbols, shorthand strings, floats, or raw ticks\n- Work with GM program names and CC names\n- Convert sequences between format 0 and format 1\n- Inspect playback time, measure positions, tempo, and time signature data\n- Swap reader/writer implementations with `reader_class` and `writer_class`\n- Parse and write note, controller, program, pitch bend, SysEx, system common, realtime, and meta events\n\n## Installation\n\nMidicraft requires Ruby 3.1 or newer.\n\n```bash\nbundle add midicraft\n```\n\nOr install it directly:\n\n```bash\ngem install midicraft\n```\n\n## Quick Start\n\n### Build a MIDI file from scratch\n\n```ruby\nrequire \"midicraft\"\n\nseq = Midicraft.build(tempo: 120, time_signature: [4, 4]) do\n  track \"Melody\", instrument: :acoustic_grand_piano, channel: 0 do\n    note \"C4\", velocity: 100, duration: :quarter\n    note \"E4\", velocity: 100, duration: :quarter\n    note \"G4\", velocity: 100, duration: :quarter\n    note \"C5\", velocity: 100, duration: :half\n    rest :quarter\n    chord [\"C4\", \"E4\", \"G4\"], velocity: 80, duration: :whole\n  end\n\n  track \"Bass\", instrument: :acoustic_bass, channel: 1 do\n    note \"C2\", velocity: 90, duration: :half\n    note \"G2\", velocity: 90, duration: :half\n    note \"C2\", velocity: 90, duration: :whole\n  end\nend\n\nseq.write(\"output.mid\")\n```\n\n### Read and inspect an existing MIDI file\n\n`Midicraft.read` exposes imported MIDI as low-level events such as `NoteOn` and `NoteOff`.\n\n```ruby\nrequire \"midicraft\"\n\nseq = Midicraft.read(\"input.mid\")\n\nseq.tracks.each do |track|\n  puts \"Track: #{track.name || \"(unnamed)\"}\"\n\n  track.events.grep(Midicraft::Events::NoteOn).each do |event|\n    duration = event.off ? (event.off.absolute_time - event.absolute_time) : nil\n    duration_label = duration ? duration.to_s : \"open\"\n\n    puts \"  #{Midicraft.note_name(event.pitch)} \" \\\n         \"ch=#{event.channel} vel=#{event.velocity} \" \\\n         \"start=#{event.absolute_time} dur=#{duration_label}\"\n  end\nend\n```\n\n### Transform authored notes non-destructively\n\n`track.notes` works on tracks that already contain `Midicraft::Events::Note` objects, such as tracks created with the DSL.\n\n```ruby\nrequire \"midicraft\"\n\nseq = Midicraft.build do\n  track \"Lead\", instrument: :electric_guitar_clean, channel: 0 do\n    note \"C4\", duration: :quarter\n    note \"E4\", duration: :quarter\n    note \"G4\", duration: :quarter\n  end\nend\n\nlead = seq.tracks.find { |track| track.name == \"Lead\" }\n\nedited = lead.transform do |track|\n  track.notes.transpose!(12)\n  track.notes.quantize!(:sixteenth, ppqn: seq.ppqn)\n  track.notes.velocity_scale!(0.9)\nend\n\nedited.events.each { |event| puts event }\n```\n\n## Data Model\n\nMidicraft uses two related note representations:\n\n| Workflow | Track contents | Best API |\n| --- | --- | --- |\n| Authoring with `Midicraft.build` or manual `Events::Note` objects | `Midicraft::Events::Note` | `track.notes`, `Midicraft::NoteCollection`, DSL helpers |\n| Reading an existing MIDI file with `Midicraft.read` | `Midicraft::Events::NoteOn`, `NoteOff`, and other raw events | `track.events` |\n\nImportant details:\n\n- `track.notes` only returns existing `Midicraft::Events::Note` objects.\n- Imported MIDI is represented as low-level events, not automatically collapsed into `Events::Note`.\n- Parsed note pairs are linked through `NoteOn#off` and `NoteOff#on` when matching note-off events are found.\n- Track-level operations such as `quantize` work on the full event list; note-collection transforms apply only to tracks that already contain `Events::Note`.\n- `Midicraft.build` creates a format 1 sequence with a meta track at index 0 for tempo and time signature events.\n\n## DSL\n\nThe builder DSL is the easiest way to author new material:\n\n```ruby\nMidicraft.build(tempo: 140, ppqn: 480, time_signature: [3, 4]) do\n  track \"Lead\", instrument: :electric_guitar_clean, channel: 0 do\n    note \"C4\", velocity: 100, duration: :quarter\n    chord [\"C4\", \"E4\", \"G4\"], velocity: 80, duration: :half\n    rest :quarter\n    control :volume, 100\n    control :pan, 64\n    pitch_bend 8192\n    program :electric_guitar_clean\n    repeat 4 do\n      note \"C4\", duration: :eighth\n    end\n    at_tick 1920 do\n      note \"G4\", duration: :quarter\n    end\n    at_bar 3, beat: 1 do\n      note \"A4\", duration: :quarter\n    end\n    velocity 60 do\n      note \"B4\", duration: :quarter\n    end\n  end\nend\n```\n\nAvailable builder methods:\n\n- `track(name = nil, instrument: nil, channel: nil)`\n- `note(pitch, velocity: 100, duration: :quarter)`\n- `chord(pitches, velocity: 100, duration: :quarter)`\n- `rest(duration)`\n- `control(number_or_name, value)`\n- `pitch_bend(value)`\n- `program(name_or_number)`\n- `repeat(count) { ... }`\n- `at_tick(tick) { ... }`\n- `at_bar(bar, beat: 1) { ... }`\n- `velocity(value) { ... }`\n\n## Duration Values\n\nDurations accept several input styles:\n\n| Type | Examples |\n| --- | --- |\n| Symbol | `:whole`, `:half`, `:quarter`, `:eighth`, `:sixteenth` |\n| Dotted symbol | `:dotted_quarter`, `:dotted_eighth` |\n| Triplet symbol | `:quarter_triplet`, `:eighth_triplet` |\n| String shorthand | `\"1n\"`, `\"4n\"`, `\"8n\"`, `\"4n.\"`, `\"4nt\"` |\n| Integer | Raw tick value such as `480` |\n| Float | Quarter-note multiplier such as `1.0`, `0.5`, or `4.0` |\n\nYou can also use sequence helpers when you need explicit conversions:\n\n```ruby\nseq.note_to_length(\"dotted quarter triplet\") #=\u003e 1.0\nseq.note_to_delta(\"eighth\")                  #=\u003e 240 when ppqn is 480\nseq.length_to_delta(0.5)                     #=\u003e 240 when ppqn is 480\n```\n\n## Core API\n\n### Top-level helpers\n\n- `Midicraft.read(path_or_io) { |current, total| ... }`\n- `Midicraft.build(tempo: 120, ppqn: 480, time_signature: [4, 4])`\n- `Midicraft.note_number(\"C4\")`\n- `Midicraft.note_name(60)`\n\n### `Midicraft::Sequence`\n\nUseful sequence methods include:\n\n- `tempo`, `tempo=`, `bpm`\n- `time_signature`, `time_signature=`\n- `name`, `name=`\n- `duration_ticks`, `duration_seconds`\n- `pulses_to_seconds`\n- `note_to_length`, `note_to_delta`, `length_to_delta`\n- `get_measures`\n- `to_format0`, `to_format1`\n- `write(path_or_io, running_status: false, note_off_as_note_on_zero: false, midi_format: nil)`\n\nExample:\n\n```ruby\nseq = Midicraft.build(tempo: 128) do\n  track \"Piano\", channel: 0 do\n    note \"C4\", duration: :quarter\n  end\nend\n\nputs seq.duration_seconds\nputs seq.get_measures.to_mbt(seq.tracks.last.notes.first)\n\nseq.write(\"format0.mid\", midi_format: 0)\n```\n\n### `Midicraft::Track`\n\nUseful track methods include:\n\n- `add(event)` / `\u003c\u003c`\n- `remove(event)`\n- `merge(other_track_or_events)`\n- `quantize(length_or_note)` for in-place event quantization\n- `transform { |copy| ... }` for non-destructive track edits\n- `name`, `name=`\n- `instrument`, `instrument=`\n- `instrument_name`, `instrument_name=`\n- `notes`\n\n### `Midicraft::NoteCollection`\n\n`track.notes` returns a `Midicraft::NoteCollection` when the track contains `Events::Note` objects.\n\n- `transpose(n)` / `transpose!(n)`\n- `quantize(grid, ppqn: 480)` / `quantize!(grid, ppqn: 480)`\n- `velocity_scale(factor)` / `velocity_scale!(factor)`\n- `humanize(timing: 10, velocity: 10)`\n- `legato(overlap: 0)`\n- `filter { |note| ... }`\n- `in_range(low, high)`\n- `on_channel(channel)`\n\n## Notes, Frequencies, and Constants\n\nMidicraft includes helpers beyond raw SMF parsing:\n\n```ruby\nMidicraft::NoteUtil.frequency(\"A4\")          #=\u003e 440.0\nMidicraft::NoteUtil.from_frequency(261.63)   #=\u003e 60\n\nMidicraft::Constants::GM.program_number(:violin)  #=\u003e 40\nMidicraft::Constants::GM.program_name(40)         #=\u003e :violin\nMidicraft::Constants::GM.drum_note(:closed_hi_hat)\n\nMidicraft::Constants::CC.number_for(:sustain) #=\u003e 64\nMidicraft::Constants::CC.name_for(64)         #=\u003e :sustain\nMidicraft::Constants::CC::VOLUME              #=\u003e 7\n```\n\n## I/O Classes and Callbacks\n\n`Sequence#read` supports a progress block with the default reader:\n\n```ruby\nseq = Midicraft.read(\"input.mid\") do |current, total|\n  puts \"Read track #{current}/#{total}\"\nend\n```\n\nFor write progress callbacks, use `Midicraft::IO::SeqWriter` as the sequence writer:\n\n```ruby\nseq = Midicraft.build do\n  track \"Lead\", channel: 0 do\n    note \"C4\", duration: :quarter\n  end\nend\n\nseq.writer_class = Midicraft::IO::SeqWriter\n\nseq.write(\n  \"output.mid\",\n  midi_format: 0,\n  running_status: true,\n  note_off_as_note_on_zero: true\n) do |track, total, index|\n  label = track ? (track.name || \"(unnamed)\") : \"start\"\n  puts \"Write #{index}/#{total}: #{label}\"\nend\n```\n\nYou can also replace `reader_class` or `writer_class` with compatible custom classes if you need different parsing or writing behavior.\n\n## Examples\n\nThe repository includes small example scripts:\n\n- `bundle exec ruby examples/from_scratch.rb`\n- `bundle exec ruby examples/dsl_demo.rb`\n- `bundle exec ruby examples/read_and_print.rb input.mid`\n\n## Development\n\nInstall dependencies and run the test suite:\n\n```bash\nbundle install\nbundle exec rake spec\n```\n\nGenerate API docs with YARD:\n\n```bash\nbundle exec rake yard\n```\n\n`bundle exec rake` runs the default task, which is the spec suite.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fydah%2Fmidicraft","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fydah%2Fmidicraft","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fydah%2Fmidicraft/lists"}