midicraft 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +332 -0
  5. data/Rakefile +11 -0
  6. data/lib/midicraft/constants/cc.rb +76 -0
  7. data/lib/midicraft/constants/gm.rb +105 -0
  8. data/lib/midicraft/dsl/sequence_builder.rb +24 -0
  9. data/lib/midicraft/dsl/track_builder.rb +103 -0
  10. data/lib/midicraft/duration.rb +85 -0
  11. data/lib/midicraft/errors.rb +11 -0
  12. data/lib/midicraft/events/channel_pressure.rb +24 -0
  13. data/lib/midicraft/events/control_change.rb +50 -0
  14. data/lib/midicraft/events/event.rb +37 -0
  15. data/lib/midicraft/events/key_pressure.rb +25 -0
  16. data/lib/midicraft/events/meta/cue_point.rb +24 -0
  17. data/lib/midicraft/events/meta/end_of_track.rb +21 -0
  18. data/lib/midicraft/events/meta/instrument_name.rb +24 -0
  19. data/lib/midicraft/events/meta/key_signature.rb +27 -0
  20. data/lib/midicraft/events/meta/lyric.rb +24 -0
  21. data/lib/midicraft/events/meta/marker.rb +24 -0
  22. data/lib/midicraft/events/meta/meta_event.rb +25 -0
  23. data/lib/midicraft/events/meta/tempo.rb +38 -0
  24. data/lib/midicraft/events/meta/text.rb +24 -0
  25. data/lib/midicraft/events/meta/time_signature.rb +38 -0
  26. data/lib/midicraft/events/meta/track_name.rb +24 -0
  27. data/lib/midicraft/events/note.rb +86 -0
  28. data/lib/midicraft/events/note_off.rb +50 -0
  29. data/lib/midicraft/events/note_on.rb +50 -0
  30. data/lib/midicraft/events/pitch_bend.rb +40 -0
  31. data/lib/midicraft/events/poly_pressure.rb +17 -0
  32. data/lib/midicraft/events/program_change.rb +49 -0
  33. data/lib/midicraft/events/realtime.rb +58 -0
  34. data/lib/midicraft/events/song_pointer.rb +22 -0
  35. data/lib/midicraft/events/song_select.rb +22 -0
  36. data/lib/midicraft/events/sys_ex.rb +22 -0
  37. data/lib/midicraft/events/tune_request.rb +19 -0
  38. data/lib/midicraft/io/midi_file.rb +233 -0
  39. data/lib/midicraft/io/reader.rb +408 -0
  40. data/lib/midicraft/io/seq_reader.rb +257 -0
  41. data/lib/midicraft/io/seq_writer.rb +51 -0
  42. data/lib/midicraft/io/writer.rb +207 -0
  43. data/lib/midicraft/measure.rb +63 -0
  44. data/lib/midicraft/note_collection.rb +118 -0
  45. data/lib/midicraft/note_util.rb +84 -0
  46. data/lib/midicraft/sequence.rb +459 -0
  47. data/lib/midicraft/track.rb +322 -0
  48. data/lib/midicraft/version.rb +5 -0
  49. data/lib/midicraft.rb +102 -0
  50. metadata +93 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 16ace114f5cd1b7cdd6c19a11306ffc4c0cc07071928723b0dd9f1f6bcc3570c
4
+ data.tar.gz: 8a37bab27004fcc2e4626affa8b95c7eaa9d046ad7c0bcb90bd857b94a815b68
5
+ SHA512:
6
+ metadata.gz: c4f8ba38d6a306ac82a68aa81e4fd99d3347dfec6e41571f69264dbdd7e86585aa596763b018577a643223b6cacf81fb664da8846abc8d7003b4f46f0910c23d
7
+ data.tar.gz: 8df895ffb4e1de6422684bac09b0d4bc36007b1310149eef5b6e8a903a0dbc78f0f0367178e08148484057941c2d353bdf2fd2ef352cbbebe60ed286ab9c5f34
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-03-07
4
+
5
+ - Initial release.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yudai Takada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,332 @@
1
+ # Midicraft
2
+
3
+ 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.
4
+
5
+ ## Highlights
6
+
7
+ - Build new sequences with `Midicraft.build`
8
+ - Read and write SMF format 0 and format 1
9
+ - Use note names like `"C4"`, `:c4`, and `"Db3"` instead of raw MIDI numbers
10
+ - Resolve durations from symbols, shorthand strings, floats, or raw ticks
11
+ - Work with GM program names and CC names
12
+ - Convert sequences between format 0 and format 1
13
+ - Inspect playback time, measure positions, tempo, and time signature data
14
+ - Swap reader/writer implementations with `reader_class` and `writer_class`
15
+ - Parse and write note, controller, program, pitch bend, SysEx, system common, realtime, and meta events
16
+
17
+ ## Installation
18
+
19
+ Midicraft requires Ruby 3.1 or newer.
20
+
21
+ ```bash
22
+ bundle add midicraft
23
+ ```
24
+
25
+ Or install it directly:
26
+
27
+ ```bash
28
+ gem install midicraft
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ### Build a MIDI file from scratch
34
+
35
+ ```ruby
36
+ require "midicraft"
37
+
38
+ seq = Midicraft.build(tempo: 120, time_signature: [4, 4]) do
39
+ track "Melody", instrument: :acoustic_grand_piano, channel: 0 do
40
+ note "C4", velocity: 100, duration: :quarter
41
+ note "E4", velocity: 100, duration: :quarter
42
+ note "G4", velocity: 100, duration: :quarter
43
+ note "C5", velocity: 100, duration: :half
44
+ rest :quarter
45
+ chord ["C4", "E4", "G4"], velocity: 80, duration: :whole
46
+ end
47
+
48
+ track "Bass", instrument: :acoustic_bass, channel: 1 do
49
+ note "C2", velocity: 90, duration: :half
50
+ note "G2", velocity: 90, duration: :half
51
+ note "C2", velocity: 90, duration: :whole
52
+ end
53
+ end
54
+
55
+ seq.write("output.mid")
56
+ ```
57
+
58
+ ### Read and inspect an existing MIDI file
59
+
60
+ `Midicraft.read` exposes imported MIDI as low-level events such as `NoteOn` and `NoteOff`.
61
+
62
+ ```ruby
63
+ require "midicraft"
64
+
65
+ seq = Midicraft.read("input.mid")
66
+
67
+ seq.tracks.each do |track|
68
+ puts "Track: #{track.name || "(unnamed)"}"
69
+
70
+ track.events.grep(Midicraft::Events::NoteOn).each do |event|
71
+ duration = event.off ? (event.off.absolute_time - event.absolute_time) : nil
72
+ duration_label = duration ? duration.to_s : "open"
73
+
74
+ puts " #{Midicraft.note_name(event.pitch)} " \
75
+ "ch=#{event.channel} vel=#{event.velocity} " \
76
+ "start=#{event.absolute_time} dur=#{duration_label}"
77
+ end
78
+ end
79
+ ```
80
+
81
+ ### Transform authored notes non-destructively
82
+
83
+ `track.notes` works on tracks that already contain `Midicraft::Events::Note` objects, such as tracks created with the DSL.
84
+
85
+ ```ruby
86
+ require "midicraft"
87
+
88
+ seq = Midicraft.build do
89
+ track "Lead", instrument: :electric_guitar_clean, channel: 0 do
90
+ note "C4", duration: :quarter
91
+ note "E4", duration: :quarter
92
+ note "G4", duration: :quarter
93
+ end
94
+ end
95
+
96
+ lead = seq.tracks.find { |track| track.name == "Lead" }
97
+
98
+ edited = lead.transform do |track|
99
+ track.notes.transpose!(12)
100
+ track.notes.quantize!(:sixteenth, ppqn: seq.ppqn)
101
+ track.notes.velocity_scale!(0.9)
102
+ end
103
+
104
+ edited.events.each { |event| puts event }
105
+ ```
106
+
107
+ ## Data Model
108
+
109
+ Midicraft uses two related note representations:
110
+
111
+ | Workflow | Track contents | Best API |
112
+ | --- | --- | --- |
113
+ | Authoring with `Midicraft.build` or manual `Events::Note` objects | `Midicraft::Events::Note` | `track.notes`, `Midicraft::NoteCollection`, DSL helpers |
114
+ | Reading an existing MIDI file with `Midicraft.read` | `Midicraft::Events::NoteOn`, `NoteOff`, and other raw events | `track.events` |
115
+
116
+ Important details:
117
+
118
+ - `track.notes` only returns existing `Midicraft::Events::Note` objects.
119
+ - Imported MIDI is represented as low-level events, not automatically collapsed into `Events::Note`.
120
+ - Parsed note pairs are linked through `NoteOn#off` and `NoteOff#on` when matching note-off events are found.
121
+ - Track-level operations such as `quantize` work on the full event list; note-collection transforms apply only to tracks that already contain `Events::Note`.
122
+ - `Midicraft.build` creates a format 1 sequence with a meta track at index 0 for tempo and time signature events.
123
+
124
+ ## DSL
125
+
126
+ The builder DSL is the easiest way to author new material:
127
+
128
+ ```ruby
129
+ Midicraft.build(tempo: 140, ppqn: 480, time_signature: [3, 4]) do
130
+ track "Lead", instrument: :electric_guitar_clean, channel: 0 do
131
+ note "C4", velocity: 100, duration: :quarter
132
+ chord ["C4", "E4", "G4"], velocity: 80, duration: :half
133
+ rest :quarter
134
+ control :volume, 100
135
+ control :pan, 64
136
+ pitch_bend 8192
137
+ program :electric_guitar_clean
138
+ repeat 4 do
139
+ note "C4", duration: :eighth
140
+ end
141
+ at_tick 1920 do
142
+ note "G4", duration: :quarter
143
+ end
144
+ at_bar 3, beat: 1 do
145
+ note "A4", duration: :quarter
146
+ end
147
+ velocity 60 do
148
+ note "B4", duration: :quarter
149
+ end
150
+ end
151
+ end
152
+ ```
153
+
154
+ Available builder methods:
155
+
156
+ - `track(name = nil, instrument: nil, channel: nil)`
157
+ - `note(pitch, velocity: 100, duration: :quarter)`
158
+ - `chord(pitches, velocity: 100, duration: :quarter)`
159
+ - `rest(duration)`
160
+ - `control(number_or_name, value)`
161
+ - `pitch_bend(value)`
162
+ - `program(name_or_number)`
163
+ - `repeat(count) { ... }`
164
+ - `at_tick(tick) { ... }`
165
+ - `at_bar(bar, beat: 1) { ... }`
166
+ - `velocity(value) { ... }`
167
+
168
+ ## Duration Values
169
+
170
+ Durations accept several input styles:
171
+
172
+ | Type | Examples |
173
+ | --- | --- |
174
+ | Symbol | `:whole`, `:half`, `:quarter`, `:eighth`, `:sixteenth` |
175
+ | Dotted symbol | `:dotted_quarter`, `:dotted_eighth` |
176
+ | Triplet symbol | `:quarter_triplet`, `:eighth_triplet` |
177
+ | String shorthand | `"1n"`, `"4n"`, `"8n"`, `"4n."`, `"4nt"` |
178
+ | Integer | Raw tick value such as `480` |
179
+ | Float | Quarter-note multiplier such as `1.0`, `0.5`, or `4.0` |
180
+
181
+ You can also use sequence helpers when you need explicit conversions:
182
+
183
+ ```ruby
184
+ seq.note_to_length("dotted quarter triplet") #=> 1.0
185
+ seq.note_to_delta("eighth") #=> 240 when ppqn is 480
186
+ seq.length_to_delta(0.5) #=> 240 when ppqn is 480
187
+ ```
188
+
189
+ ## Core API
190
+
191
+ ### Top-level helpers
192
+
193
+ - `Midicraft.read(path_or_io) { |current, total| ... }`
194
+ - `Midicraft.build(tempo: 120, ppqn: 480, time_signature: [4, 4])`
195
+ - `Midicraft.note_number("C4")`
196
+ - `Midicraft.note_name(60)`
197
+
198
+ ### `Midicraft::Sequence`
199
+
200
+ Useful sequence methods include:
201
+
202
+ - `tempo`, `tempo=`, `bpm`
203
+ - `time_signature`, `time_signature=`
204
+ - `name`, `name=`
205
+ - `duration_ticks`, `duration_seconds`
206
+ - `pulses_to_seconds`
207
+ - `note_to_length`, `note_to_delta`, `length_to_delta`
208
+ - `get_measures`
209
+ - `to_format0`, `to_format1`
210
+ - `write(path_or_io, running_status: false, note_off_as_note_on_zero: false, midi_format: nil)`
211
+
212
+ Example:
213
+
214
+ ```ruby
215
+ seq = Midicraft.build(tempo: 128) do
216
+ track "Piano", channel: 0 do
217
+ note "C4", duration: :quarter
218
+ end
219
+ end
220
+
221
+ puts seq.duration_seconds
222
+ puts seq.get_measures.to_mbt(seq.tracks.last.notes.first)
223
+
224
+ seq.write("format0.mid", midi_format: 0)
225
+ ```
226
+
227
+ ### `Midicraft::Track`
228
+
229
+ Useful track methods include:
230
+
231
+ - `add(event)` / `<<`
232
+ - `remove(event)`
233
+ - `merge(other_track_or_events)`
234
+ - `quantize(length_or_note)` for in-place event quantization
235
+ - `transform { |copy| ... }` for non-destructive track edits
236
+ - `name`, `name=`
237
+ - `instrument`, `instrument=`
238
+ - `instrument_name`, `instrument_name=`
239
+ - `notes`
240
+
241
+ ### `Midicraft::NoteCollection`
242
+
243
+ `track.notes` returns a `Midicraft::NoteCollection` when the track contains `Events::Note` objects.
244
+
245
+ - `transpose(n)` / `transpose!(n)`
246
+ - `quantize(grid, ppqn: 480)` / `quantize!(grid, ppqn: 480)`
247
+ - `velocity_scale(factor)` / `velocity_scale!(factor)`
248
+ - `humanize(timing: 10, velocity: 10)`
249
+ - `legato(overlap: 0)`
250
+ - `filter { |note| ... }`
251
+ - `in_range(low, high)`
252
+ - `on_channel(channel)`
253
+
254
+ ## Notes, Frequencies, and Constants
255
+
256
+ Midicraft includes helpers beyond raw SMF parsing:
257
+
258
+ ```ruby
259
+ Midicraft::NoteUtil.frequency("A4") #=> 440.0
260
+ Midicraft::NoteUtil.from_frequency(261.63) #=> 60
261
+
262
+ Midicraft::Constants::GM.program_number(:violin) #=> 40
263
+ Midicraft::Constants::GM.program_name(40) #=> :violin
264
+ Midicraft::Constants::GM.drum_note(:closed_hi_hat)
265
+
266
+ Midicraft::Constants::CC.number_for(:sustain) #=> 64
267
+ Midicraft::Constants::CC.name_for(64) #=> :sustain
268
+ Midicraft::Constants::CC::VOLUME #=> 7
269
+ ```
270
+
271
+ ## I/O Classes and Callbacks
272
+
273
+ `Sequence#read` supports a progress block with the default reader:
274
+
275
+ ```ruby
276
+ seq = Midicraft.read("input.mid") do |current, total|
277
+ puts "Read track #{current}/#{total}"
278
+ end
279
+ ```
280
+
281
+ For write progress callbacks, use `Midicraft::IO::SeqWriter` as the sequence writer:
282
+
283
+ ```ruby
284
+ seq = Midicraft.build do
285
+ track "Lead", channel: 0 do
286
+ note "C4", duration: :quarter
287
+ end
288
+ end
289
+
290
+ seq.writer_class = Midicraft::IO::SeqWriter
291
+
292
+ seq.write(
293
+ "output.mid",
294
+ midi_format: 0,
295
+ running_status: true,
296
+ note_off_as_note_on_zero: true
297
+ ) do |track, total, index|
298
+ label = track ? (track.name || "(unnamed)") : "start"
299
+ puts "Write #{index}/#{total}: #{label}"
300
+ end
301
+ ```
302
+
303
+ You can also replace `reader_class` or `writer_class` with compatible custom classes if you need different parsing or writing behavior.
304
+
305
+ ## Examples
306
+
307
+ The repository includes small example scripts:
308
+
309
+ - `bundle exec ruby examples/from_scratch.rb`
310
+ - `bundle exec ruby examples/dsl_demo.rb`
311
+ - `bundle exec ruby examples/read_and_print.rb input.mid`
312
+
313
+ ## Development
314
+
315
+ Install dependencies and run the test suite:
316
+
317
+ ```bash
318
+ bundle install
319
+ bundle exec rake spec
320
+ ```
321
+
322
+ Generate API docs with YARD:
323
+
324
+ ```bash
325
+ bundle exec rake yard
326
+ ```
327
+
328
+ `bundle exec rake` runs the default task, which is the spec suite.
329
+
330
+ ## License
331
+
332
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "yard"
6
+ require "yard/rake/yardoc_task"
7
+
8
+ RSpec::Core::RakeTask.new(:spec)
9
+ YARD::Rake::YardocTask.new(:yard)
10
+
11
+ task default: :spec
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Midicraft
4
+ module Constants
5
+ module CC
6
+ NUMBERS = {
7
+ bank_select: 0,
8
+ modulation: 1,
9
+ breath_controller: 2,
10
+ foot_controller: 4,
11
+ portamento_time: 5,
12
+ data_entry: 6,
13
+ volume: 7,
14
+ balance: 8,
15
+ pan: 10,
16
+ expression: 11,
17
+ effect_control_1: 12,
18
+ effect_control_2: 13,
19
+ general_purpose_1: 16,
20
+ general_purpose_2: 17,
21
+ general_purpose_3: 18,
22
+ general_purpose_4: 19,
23
+ sustain: 64,
24
+ portamento: 65,
25
+ sostenuto: 66,
26
+ soft_pedal: 67,
27
+ legato_footswitch: 68,
28
+ hold_2: 69,
29
+ sound_variation: 70,
30
+ timbre: 71,
31
+ release_time: 72,
32
+ attack_time: 73,
33
+ brightness: 74,
34
+ decay_time: 75,
35
+ vibrato_rate: 76,
36
+ vibrato_depth: 77,
37
+ vibrato_delay: 78,
38
+ reverb: 91,
39
+ tremolo: 92,
40
+ chorus: 93,
41
+ detune: 94,
42
+ phaser: 95,
43
+ data_increment: 96,
44
+ data_decrement: 97,
45
+ nrpn_lsb: 98,
46
+ nrpn_msb: 99,
47
+ rpn_lsb: 100,
48
+ rpn_msb: 101,
49
+ all_sound_off: 120,
50
+ reset_all_controllers: 121,
51
+ local_control: 122,
52
+ all_notes_off: 123,
53
+ omni_off: 124,
54
+ omni_on: 125,
55
+ mono_on: 126,
56
+ poly_on: 127
57
+ }.freeze
58
+
59
+ NUMBERS.each do |name, number|
60
+ const_set(name.to_s.upcase, number)
61
+ end
62
+
63
+ CC_NAMES = NUMBERS.invert.freeze
64
+
65
+ module_function
66
+
67
+ def number_for(name)
68
+ NUMBERS.fetch(name) { raise ArgumentError, "Unknown CC name: #{name.inspect}" }
69
+ end
70
+
71
+ def name_for(number)
72
+ CC_NAMES[number]
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Midicraft
4
+ module Constants
5
+ module GM
6
+ PATCHES = {
7
+ # Piano
8
+ acoustic_grand_piano: 0, bright_acoustic_piano: 1, electric_grand_piano: 2,
9
+ honky_tonk_piano: 3, electric_piano_1: 4, electric_piano_2: 5,
10
+ harpsichord: 6, clavinet: 7,
11
+ # Chromatic Percussion
12
+ celesta: 8, glockenspiel: 9, music_box: 10, vibraphone: 11,
13
+ marimba: 12, xylophone: 13, tubular_bells: 14, dulcimer: 15,
14
+ # Organ
15
+ drawbar_organ: 16, percussive_organ: 17, rock_organ: 18, church_organ: 19,
16
+ reed_organ: 20, accordion: 21, harmonica: 22, tango_accordion: 23,
17
+ # Guitar
18
+ acoustic_guitar_nylon: 24, acoustic_guitar_steel: 25,
19
+ electric_guitar_jazz: 26, electric_guitar_clean: 27,
20
+ electric_guitar_muted: 28, overdriven_guitar: 29,
21
+ distortion_guitar: 30, guitar_harmonics: 31,
22
+ # Bass
23
+ acoustic_bass: 32, electric_bass_finger: 33, electric_bass_pick: 34,
24
+ fretless_bass: 35, slap_bass_1: 36, slap_bass_2: 37,
25
+ synth_bass_1: 38, synth_bass_2: 39,
26
+ # Strings
27
+ violin: 40, viola: 41, cello: 42, contrabass: 43,
28
+ tremolo_strings: 44, pizzicato_strings: 45,
29
+ orchestral_harp: 46, timpani: 47,
30
+ # Ensemble
31
+ string_ensemble_1: 48, string_ensemble_2: 49,
32
+ synth_strings_1: 50, synth_strings_2: 51,
33
+ choir_aahs: 52, voice_oohs: 53, synth_choir: 54, orchestra_hit: 55,
34
+ # Brass
35
+ trumpet: 56, trombone: 57, tuba: 58, muted_trumpet: 59,
36
+ french_horn: 60, brass_section: 61, synth_brass_1: 62, synth_brass_2: 63,
37
+ # Reed
38
+ soprano_sax: 64, alto_sax: 65, tenor_sax: 66, baritone_sax: 67,
39
+ oboe: 68, english_horn: 69, bassoon: 70, clarinet: 71,
40
+ # Pipe
41
+ piccolo: 72, flute: 73, recorder: 74, pan_flute: 75,
42
+ blown_bottle: 76, shakuhachi: 77, whistle: 78, ocarina: 79,
43
+ # Synth Lead
44
+ lead_1_square: 80, lead_2_sawtooth: 81, lead_3_calliope: 82,
45
+ lead_4_chiff: 83, lead_5_charang: 84, lead_6_voice: 85,
46
+ lead_7_fifths: 86, lead_8_bass_lead: 87,
47
+ # Synth Pad
48
+ pad_1_new_age: 88, pad_2_warm: 89, pad_3_polysynth: 90,
49
+ pad_4_choir: 91, pad_5_bowed: 92, pad_6_metallic: 93,
50
+ pad_7_halo: 94, pad_8_sweep: 95,
51
+ # Synth Effects
52
+ fx_1_rain: 96, fx_2_soundtrack: 97, fx_3_crystal: 98,
53
+ fx_4_atmosphere: 99, fx_5_brightness: 100, fx_6_goblins: 101,
54
+ fx_7_echoes: 102, fx_8_sci_fi: 103,
55
+ # Ethnic
56
+ sitar: 104, banjo: 105, shamisen: 106, koto: 107,
57
+ kalimba: 108, bag_pipe: 109, fiddle: 110, shanai: 111,
58
+ # Percussive
59
+ tinkle_bell: 112, agogo: 113, steel_drums: 114, woodblock: 115,
60
+ taiko_drum: 116, melodic_tom: 117, synth_drum: 118, reverse_cymbal: 119,
61
+ # Sound Effects
62
+ guitar_fret_noise: 120, breath_noise: 121, seashore: 122,
63
+ bird_tweet: 123, telephone_ring: 124, helicopter: 125,
64
+ applause: 126, gunshot: 127
65
+ }.freeze
66
+
67
+ PATCH_NAMES = PATCHES.invert.freeze
68
+
69
+ DRUM_MAP = {
70
+ bass_drum_2: 35, bass_drum_1: 36, side_stick: 37, snare_drum_1: 38,
71
+ hand_clap: 39, snare_drum_2: 40, low_floor_tom: 41, closed_hi_hat: 42,
72
+ high_floor_tom: 43, pedal_hi_hat: 44, low_tom: 45, open_hi_hat: 46,
73
+ low_mid_tom: 47, hi_mid_tom: 48, crash_cymbal_1: 49, high_tom: 50,
74
+ ride_cymbal_1: 51, chinese_cymbal: 52, ride_bell: 53, tambourine: 54,
75
+ splash_cymbal: 55, cowbell: 56, crash_cymbal_2: 57, vibraslap: 58,
76
+ ride_cymbal_2: 59, hi_bongo: 60, low_bongo: 61, mute_hi_conga: 62,
77
+ open_hi_conga: 63, low_conga: 64, high_timbale: 65, low_timbale: 66,
78
+ high_agogo: 67, low_agogo: 68, cabasa: 69, maracas: 70,
79
+ short_whistle: 71, long_whistle: 72, short_guiro: 73, long_guiro: 74,
80
+ claves: 75, hi_wood_block: 76, low_wood_block: 77,
81
+ mute_cuica: 78, open_cuica: 79, mute_triangle: 80, open_triangle: 81
82
+ }.freeze
83
+
84
+ DRUM_NAMES = DRUM_MAP.invert.freeze
85
+
86
+ module_function
87
+
88
+ def program_number(name)
89
+ PATCHES.fetch(name) { raise ArgumentError, "Unknown GM patch: #{name.inspect}" }
90
+ end
91
+
92
+ def program_name(number)
93
+ PATCH_NAMES.fetch(number) { raise ArgumentError, "Unknown GM program number: #{number}" }
94
+ end
95
+
96
+ def drum_note(name)
97
+ DRUM_MAP.fetch(name) { raise ArgumentError, "Unknown drum: #{name.inspect}" }
98
+ end
99
+
100
+ def drum_name(number)
101
+ DRUM_NAMES.fetch(number) { raise ArgumentError, "Unknown drum note number: #{number}" }
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Midicraft
4
+ module DSL
5
+ class SequenceBuilder
6
+ def initialize(sequence)
7
+ @sequence = sequence
8
+ end
9
+
10
+ def track(name = nil, instrument: nil, channel: nil, &block)
11
+ track = @sequence.add_track(name: name)
12
+ track.channel = channel
13
+ track.instrument = instrument if instrument
14
+
15
+ if block
16
+ builder = TrackBuilder.new(track, ppqn: @sequence.ppqn)
17
+ builder.instance_eval(&block)
18
+ end
19
+
20
+ track
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Midicraft
4
+ module DSL
5
+ class TrackBuilder
6
+ def initialize(track, ppqn:)
7
+ @track = track
8
+ @ppqn = ppqn
9
+ @cursor = 0
10
+ @default_velocity = 100
11
+ end
12
+
13
+ def note(pitch, velocity: nil, duration: :quarter)
14
+ vel = velocity || @default_velocity
15
+ dur = Duration.resolve(duration, @ppqn)
16
+ n = Events::Note.new(pitch, velocity: vel, channel: @track.channel || 0,
17
+ duration: dur, start_time: @cursor)
18
+ @track.add(n)
19
+ @cursor += dur
20
+ end
21
+
22
+ def chord(pitches, velocity: nil, duration: :quarter)
23
+ vel = velocity || @default_velocity
24
+ dur = Duration.resolve(duration, @ppqn)
25
+ pitches.each do |pitch|
26
+ n = Events::Note.new(pitch, velocity: vel, channel: @track.channel || 0,
27
+ duration: dur, start_time: @cursor)
28
+ @track.add(n)
29
+ end
30
+ @cursor += dur
31
+ end
32
+
33
+ def rest(duration)
34
+ @cursor += Duration.resolve(duration, @ppqn)
35
+ end
36
+
37
+ def control(name, value)
38
+ cc = Events::ControlChange.new(
39
+ channel: @track.channel || 0,
40
+ number: name,
41
+ value: value,
42
+ absolute_time: @cursor
43
+ )
44
+ @track.add(cc)
45
+ end
46
+
47
+ def pitch_bend(value)
48
+ pb = Events::PitchBend.new(
49
+ channel: @track.channel || 0,
50
+ value: value,
51
+ absolute_time: @cursor
52
+ )
53
+ @track.add(pb)
54
+ end
55
+
56
+ def program(name_or_number)
57
+ pc = Events::ProgramChange.new(
58
+ channel: @track.channel || 0,
59
+ program: name_or_number,
60
+ absolute_time: @cursor
61
+ )
62
+ @track.add(pc)
63
+ end
64
+
65
+ def repeat(times, &block)
66
+ times.times do
67
+ instance_eval(&block)
68
+ end
69
+ end
70
+
71
+ def at_tick(tick, &block)
72
+ saved = @cursor
73
+ @cursor = tick
74
+ instance_eval(&block)
75
+ @cursor = saved
76
+ end
77
+
78
+ def at_bar(bar, beat: 1, &block)
79
+ ts = find_time_signature
80
+ numerator = ts ? ts[0] : 4
81
+ denominator = ts ? ts[1] : 4
82
+ beats_per_bar = numerator * (4.0 / denominator)
83
+ ticks_per_beat = @ppqn * (4.0 / denominator)
84
+ tick = ((bar - 1) * beats_per_bar * @ppqn * (4.0 / denominator) / (4.0 / denominator)).to_i
85
+ tick = ((bar - 1) * numerator + (beat - 1)) * ticks_per_beat
86
+ at_tick(tick.to_i, &block)
87
+ end
88
+
89
+ def velocity(v, &block)
90
+ saved = @default_velocity
91
+ @default_velocity = v
92
+ instance_eval(&block)
93
+ @default_velocity = saved
94
+ end
95
+
96
+ private
97
+
98
+ def find_time_signature
99
+ @track.sequence&.time_signature
100
+ end
101
+ end
102
+ end
103
+ end