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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +332 -0
- data/Rakefile +11 -0
- data/lib/midicraft/constants/cc.rb +76 -0
- data/lib/midicraft/constants/gm.rb +105 -0
- data/lib/midicraft/dsl/sequence_builder.rb +24 -0
- data/lib/midicraft/dsl/track_builder.rb +103 -0
- data/lib/midicraft/duration.rb +85 -0
- data/lib/midicraft/errors.rb +11 -0
- data/lib/midicraft/events/channel_pressure.rb +24 -0
- data/lib/midicraft/events/control_change.rb +50 -0
- data/lib/midicraft/events/event.rb +37 -0
- data/lib/midicraft/events/key_pressure.rb +25 -0
- data/lib/midicraft/events/meta/cue_point.rb +24 -0
- data/lib/midicraft/events/meta/end_of_track.rb +21 -0
- data/lib/midicraft/events/meta/instrument_name.rb +24 -0
- data/lib/midicraft/events/meta/key_signature.rb +27 -0
- data/lib/midicraft/events/meta/lyric.rb +24 -0
- data/lib/midicraft/events/meta/marker.rb +24 -0
- data/lib/midicraft/events/meta/meta_event.rb +25 -0
- data/lib/midicraft/events/meta/tempo.rb +38 -0
- data/lib/midicraft/events/meta/text.rb +24 -0
- data/lib/midicraft/events/meta/time_signature.rb +38 -0
- data/lib/midicraft/events/meta/track_name.rb +24 -0
- data/lib/midicraft/events/note.rb +86 -0
- data/lib/midicraft/events/note_off.rb +50 -0
- data/lib/midicraft/events/note_on.rb +50 -0
- data/lib/midicraft/events/pitch_bend.rb +40 -0
- data/lib/midicraft/events/poly_pressure.rb +17 -0
- data/lib/midicraft/events/program_change.rb +49 -0
- data/lib/midicraft/events/realtime.rb +58 -0
- data/lib/midicraft/events/song_pointer.rb +22 -0
- data/lib/midicraft/events/song_select.rb +22 -0
- data/lib/midicraft/events/sys_ex.rb +22 -0
- data/lib/midicraft/events/tune_request.rb +19 -0
- data/lib/midicraft/io/midi_file.rb +233 -0
- data/lib/midicraft/io/reader.rb +408 -0
- data/lib/midicraft/io/seq_reader.rb +257 -0
- data/lib/midicraft/io/seq_writer.rb +51 -0
- data/lib/midicraft/io/writer.rb +207 -0
- data/lib/midicraft/measure.rb +63 -0
- data/lib/midicraft/note_collection.rb +118 -0
- data/lib/midicraft/note_util.rb +84 -0
- data/lib/midicraft/sequence.rb +459 -0
- data/lib/midicraft/track.rb +322 -0
- data/lib/midicraft/version.rb +5 -0
- data/lib/midicraft.rb +102 -0
- 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
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,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
|