jmtk 0.0.3.3-java
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.
- data/.yardopts +10 -0
- data/DEVELOPMENT_NOTES.md +115 -0
- data/INTRO.md +129 -0
- data/LICENSE.txt +27 -0
- data/README.md +50 -0
- data/Rakefile +102 -0
- data/bin/jmtk +250 -0
- data/bin/mtk +250 -0
- data/examples/crescendo.rb +20 -0
- data/examples/drum_pattern.rb +23 -0
- data/examples/dynamic_pattern.rb +36 -0
- data/examples/gets_and_play.rb +27 -0
- data/examples/notation.rb +22 -0
- data/examples/play_midi.rb +17 -0
- data/examples/print_midi.rb +13 -0
- data/examples/random_tone_row.rb +18 -0
- data/examples/syntax_to_midi.rb +28 -0
- data/examples/test_output.rb +7 -0
- data/examples/tone_row_melody.rb +23 -0
- data/lib/mtk.rb +76 -0
- data/lib/mtk/core/duration.rb +213 -0
- data/lib/mtk/core/intensity.rb +158 -0
- data/lib/mtk/core/interval.rb +157 -0
- data/lib/mtk/core/pitch.rb +154 -0
- data/lib/mtk/core/pitch_class.rb +194 -0
- data/lib/mtk/events/event.rb +119 -0
- data/lib/mtk/events/note.rb +112 -0
- data/lib/mtk/events/parameter.rb +54 -0
- data/lib/mtk/events/timeline.rb +232 -0
- data/lib/mtk/groups/chord.rb +56 -0
- data/lib/mtk/groups/collection.rb +196 -0
- data/lib/mtk/groups/melody.rb +96 -0
- data/lib/mtk/groups/pitch_class_set.rb +163 -0
- data/lib/mtk/groups/pitch_collection.rb +23 -0
- data/lib/mtk/io/dls_synth_device.rb +146 -0
- data/lib/mtk/io/dls_synth_output.rb +62 -0
- data/lib/mtk/io/jsound_input.rb +87 -0
- data/lib/mtk/io/jsound_output.rb +82 -0
- data/lib/mtk/io/midi_file.rb +209 -0
- data/lib/mtk/io/midi_input.rb +97 -0
- data/lib/mtk/io/midi_output.rb +195 -0
- data/lib/mtk/io/notation.rb +162 -0
- data/lib/mtk/io/unimidi_input.rb +117 -0
- data/lib/mtk/io/unimidi_output.rb +140 -0
- data/lib/mtk/lang/durations.rb +57 -0
- data/lib/mtk/lang/intensities.rb +61 -0
- data/lib/mtk/lang/intervals.rb +73 -0
- data/lib/mtk/lang/mtk_grammar.citrus +237 -0
- data/lib/mtk/lang/parser.rb +29 -0
- data/lib/mtk/lang/pitch_classes.rb +29 -0
- data/lib/mtk/lang/pitches.rb +52 -0
- data/lib/mtk/lang/pseudo_constants.rb +26 -0
- data/lib/mtk/lang/variable.rb +32 -0
- data/lib/mtk/numeric_extensions.rb +66 -0
- data/lib/mtk/patterns/chain.rb +49 -0
- data/lib/mtk/patterns/choice.rb +43 -0
- data/lib/mtk/patterns/cycle.rb +18 -0
- data/lib/mtk/patterns/for_each.rb +71 -0
- data/lib/mtk/patterns/function.rb +39 -0
- data/lib/mtk/patterns/lines.rb +54 -0
- data/lib/mtk/patterns/palindrome.rb +45 -0
- data/lib/mtk/patterns/pattern.rb +171 -0
- data/lib/mtk/patterns/sequence.rb +20 -0
- data/lib/mtk/sequencers/event_builder.rb +132 -0
- data/lib/mtk/sequencers/legato_sequencer.rb +24 -0
- data/lib/mtk/sequencers/rhythmic_sequencer.rb +28 -0
- data/lib/mtk/sequencers/sequencer.rb +111 -0
- data/lib/mtk/sequencers/step_sequencer.rb +26 -0
- data/spec/mtk/core/duration_spec.rb +372 -0
- data/spec/mtk/core/intensity_spec.rb +289 -0
- data/spec/mtk/core/interval_spec.rb +265 -0
- data/spec/mtk/core/pitch_class_spec.rb +343 -0
- data/spec/mtk/core/pitch_spec.rb +297 -0
- data/spec/mtk/events/event_spec.rb +234 -0
- data/spec/mtk/events/note_spec.rb +174 -0
- data/spec/mtk/events/parameter_spec.rb +220 -0
- data/spec/mtk/events/timeline_spec.rb +430 -0
- data/spec/mtk/groups/chord_spec.rb +85 -0
- data/spec/mtk/groups/collection_spec.rb +374 -0
- data/spec/mtk/groups/melody_spec.rb +225 -0
- data/spec/mtk/groups/pitch_class_set_spec.rb +340 -0
- data/spec/mtk/io/midi_file_spec.rb +243 -0
- data/spec/mtk/io/midi_output_spec.rb +102 -0
- data/spec/mtk/lang/durations_spec.rb +89 -0
- data/spec/mtk/lang/intensities_spec.rb +101 -0
- data/spec/mtk/lang/intervals_spec.rb +143 -0
- data/spec/mtk/lang/parser_spec.rb +603 -0
- data/spec/mtk/lang/pitch_classes_spec.rb +62 -0
- data/spec/mtk/lang/pitches_spec.rb +56 -0
- data/spec/mtk/lang/pseudo_constants_spec.rb +20 -0
- data/spec/mtk/lang/variable_spec.rb +52 -0
- data/spec/mtk/numeric_extensions_spec.rb +83 -0
- data/spec/mtk/patterns/chain_spec.rb +110 -0
- data/spec/mtk/patterns/choice_spec.rb +97 -0
- data/spec/mtk/patterns/cycle_spec.rb +123 -0
- data/spec/mtk/patterns/for_each_spec.rb +136 -0
- data/spec/mtk/patterns/function_spec.rb +120 -0
- data/spec/mtk/patterns/lines_spec.rb +77 -0
- data/spec/mtk/patterns/palindrome_spec.rb +108 -0
- data/spec/mtk/patterns/pattern_spec.rb +132 -0
- data/spec/mtk/patterns/sequence_spec.rb +203 -0
- data/spec/mtk/sequencers/event_builder_spec.rb +245 -0
- data/spec/mtk/sequencers/legato_sequencer_spec.rb +45 -0
- data/spec/mtk/sequencers/rhythmic_sequencer_spec.rb +84 -0
- data/spec/mtk/sequencers/sequencer_spec.rb +215 -0
- data/spec/mtk/sequencers/step_sequencer_spec.rb +93 -0
- data/spec/spec_coverage.rb +2 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/test.mid +0 -0
- metadata +226 -0
@@ -0,0 +1,340 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MTK::Groups::PitchClassSet do
|
4
|
+
|
5
|
+
PITCH_CLASS_SET = MTK::Groups::PitchClassSet
|
6
|
+
|
7
|
+
let(:pitch_classes) { [C,E,G] }
|
8
|
+
let(:pitch_class_set) { MTK::Groups::PitchClassSet.new(pitch_classes) }
|
9
|
+
|
10
|
+
it "is Enumerable" do
|
11
|
+
pitch_class_set.should be_a Enumerable
|
12
|
+
end
|
13
|
+
|
14
|
+
describe ".random_row" do
|
15
|
+
it "generates a 12-tone row" do
|
16
|
+
PITCH_CLASS_SET.random_row.should =~ PitchClasses::PITCH_CLASSES
|
17
|
+
end
|
18
|
+
|
19
|
+
it "generates a random 12-tone row (NOTE: very slight expected chance of test failure, if this fails run it again!)" do
|
20
|
+
# there's a 1/479_001_600 chance this will fail... whaddyagonnado??
|
21
|
+
PITCH_CLASS_SET.random_row.should_not == PITCH_CLASS_SET.random_row
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe ".all" do
|
26
|
+
it "is the set of all 12 pitch classes" do
|
27
|
+
PITCH_CLASS_SET.all.should == MTK.PitchClassSet(PitchClasses::PITCH_CLASSES)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe ".new" do
|
32
|
+
it "maintains the pitch class collection exactly (preserves order and keeps duplicates)" do
|
33
|
+
PITCH_CLASS_SET.new([C, E, G, E, B, C]).pitch_classes.should == [C, E, G, E, B, C]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#pitch_classes" do
|
38
|
+
it "is the list of pitch_classes contained in this set" do
|
39
|
+
pitch_class_set.pitch_classes.should == pitch_classes
|
40
|
+
end
|
41
|
+
|
42
|
+
it "is immutable" do
|
43
|
+
lambda { pitch_class_set.pitch_classes << D }.should raise_error
|
44
|
+
end
|
45
|
+
|
46
|
+
it "does not affect the immutabilty of the pitch class list used to construct it" do
|
47
|
+
pitch_classes << D
|
48
|
+
pitch_classes.length.should == 4
|
49
|
+
end
|
50
|
+
|
51
|
+
it "is not affected by changes to the pitch class list used to construct it" do
|
52
|
+
pitch_class_set # force construction before we modify the pitch_classes array
|
53
|
+
pitch_classes << D
|
54
|
+
pitch_class_set.pitch_classes.length.should == 3
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "#to_a" do
|
59
|
+
it "is equal to #pitch_classes" do
|
60
|
+
pitch_class_set.to_a.should == pitch_class_set.pitch_classes
|
61
|
+
end
|
62
|
+
|
63
|
+
it "is mutable" do
|
64
|
+
(pitch_class_set.to_a << Bb).should == [C, E, G, Bb]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#size" do
|
69
|
+
it "returns the number of pitch classes in the set" do
|
70
|
+
pitch_class_set.size.should == 3
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe "#length" do
|
75
|
+
it "behaves like #size" do
|
76
|
+
pitch_class_set.length.should == pitch_class_set.size
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "#[]" do
|
81
|
+
it "accesses the individual pitch classes (like an Array)" do
|
82
|
+
pitch_class_set[0].should == C
|
83
|
+
pitch_class_set[1].should == E
|
84
|
+
pitch_class_set[2].should == G
|
85
|
+
end
|
86
|
+
|
87
|
+
it "returns nil for invalid indexes" do
|
88
|
+
pitch_class_set[pitch_class_set.size].should == nil
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "#first" do
|
93
|
+
it "is #[0]" do
|
94
|
+
pitch_class_set.first.should == pitch_class_set[0]
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "#last" do
|
99
|
+
it "is #[-1]" do
|
100
|
+
pitch_class_set.last.should == pitch_class_set[-1]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe "#each" do
|
105
|
+
it "yields each pitch_class" do
|
106
|
+
pcs = []
|
107
|
+
pitch_class_set.each{|pc| pcs << pc }
|
108
|
+
pcs.should == pitch_classes
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe "#map" do
|
113
|
+
it "returns a PITCH_CLASS_SET with each PitchClass replaced with the results of the block" do
|
114
|
+
pitch_class_set.map{|pc| pc + 2}.should == [D, Gb, A]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe '#transpose' do
|
119
|
+
it 'transposes by the given semitones' do
|
120
|
+
(pitch_class_set.transpose 4).should == MTK.PitchClassSet(E, Ab, B)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "#invert" do
|
125
|
+
it 'inverts all pitch_classes around the given center pitch' do
|
126
|
+
pitch_class_set.invert(G).should == MTK.PitchClassSet(D,Bb,G)
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'inverts all pitches around the first pitch, when no center pitch is given' do
|
130
|
+
pitch_class_set.invert.should == MTK.PitchClassSet(C,Ab,F)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe "#reverse" do
|
135
|
+
it "produces a PITCH_CLASS_SET with pitch classes in reverse order" do
|
136
|
+
pitch_class_set.reverse.should == MTK.PitchClassSet(G,E,C)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
describe "#retrograde" do
|
141
|
+
it "acts like reverse" do
|
142
|
+
pitch_class_set.retrograde.should == pitch_class_set.reverse
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
describe "#intersection" do
|
147
|
+
it "produces a PITCH_CLASS_SET containing the common pitch classes from self and the argument" do
|
148
|
+
pitch_class_set.intersection(MTK.PitchClassSet(E,G,B)).should == MTK.PitchClassSet(E,G)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
describe "#union" do
|
153
|
+
it "produces a PITCH_CLASS_SET containing the all pitch classes from either self or the argument" do
|
154
|
+
pitch_class_set.union(MTK.PitchClassSet(E,G,B)).should == MTK.PitchClassSet(C,E,G,B)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
describe "#difference" do
|
159
|
+
it "produces a PITCH_CLASS_SET with the pitch classes from the argument removed" do
|
160
|
+
pitch_class_set.difference(MTK.PitchClassSet(E)).should == MTK.PitchClassSet(C,G)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
describe "#symmetric_difference" do
|
165
|
+
it "produces a PITCH_CLASS_SET containing the pitch classes only in self or only in the argument" do
|
166
|
+
pitch_class_set.symmetric_difference(MTK.PitchClassSet(E,G,B)).should == MTK.PitchClassSet(C,B)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
describe "#complement" do
|
171
|
+
it "produces the set of all PitchClasses not in the current set" do
|
172
|
+
pitch_class_set.complement.should =~ MTK.PitchClassSet(Db,D,Eb,F,Gb,Ab,A,Bb,B)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
describe "#rotate" do
|
177
|
+
it "produces a PITCH_CLASS_SET that is rotated by the given offset" do
|
178
|
+
pitch_class_set.rotate(2).should == MTK.PitchClassSet(G,C,E)
|
179
|
+
pitch_class_set.rotate(-2).should == MTK.PitchClassSet(E,G,C)
|
180
|
+
end
|
181
|
+
|
182
|
+
it "rotates by 1 if no argument is given" do
|
183
|
+
pitch_class_set.rotate.should == pitch_class_set.rotate(1)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe "#permute" do
|
188
|
+
it "randomly rearranges the PITCH_CLASS_SET order (NOTE: very slight expected chance of test failure, if this fails run it again!)" do
|
189
|
+
all_pcs = MTK.PitchClassSet(PitchClasses::PITCH_CLASSES)
|
190
|
+
permuted = all_pcs.permute
|
191
|
+
permuted.should =~ all_pcs
|
192
|
+
permuted.should_not == all_pcs # there's a 1/479_001_600 chance this will fail...
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
describe "#shuffle" do
|
197
|
+
it "behaves like permute (NOTE: very slight expected chance of test failure, if this fails run it again!)" do
|
198
|
+
all_pcs = MTK.PitchClassSet(PitchClasses::PITCH_CLASSES)
|
199
|
+
shuffled = all_pcs.shuffle
|
200
|
+
shuffled.should =~ all_pcs
|
201
|
+
shuffled.should_not == all_pcs # there's a 1/479_001_600 chance this will fail...
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
describe "#concat" do
|
206
|
+
it "appends the pitch classes from the other set" do
|
207
|
+
pitch_class_set.concat(MTK.PitchClassSet(D,E,F)).should == MTK.PitchClassSet(C,E,G,D,E,F)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
describe "#normal_order" do
|
212
|
+
it "permutes the set so that the first and last pitch classes are as close together as possible" do
|
213
|
+
PITCH_CLASS_SET.new([E,A,C]).normal_order.should == [A,C,E]
|
214
|
+
end
|
215
|
+
|
216
|
+
it "breaks ties by minimizing the distance between the first and second-to-last pitch class" do
|
217
|
+
# 0,4,8,9,11
|
218
|
+
PITCH_CLASS_SET.new([C,E,Ab,A,B]).normal_order.should == [Ab,A,B,C,E]
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|
222
|
+
|
223
|
+
describe "#normal_form" do
|
224
|
+
it "is transposes the #normal_order so that the first pitch class set is 0 (C)" do
|
225
|
+
PITCH_CLASS_SET.new([E,A,C]).normal_form.should == [0,3,7]
|
226
|
+
end
|
227
|
+
|
228
|
+
it "is invariant across reorderings of the pitch classes" do
|
229
|
+
PITCH_CLASS_SET.new([C,E,G]).normal_form.should == [0,4,7]
|
230
|
+
PITCH_CLASS_SET.new([E,C,G]).normal_form.should == [0,4,7]
|
231
|
+
PITCH_CLASS_SET.new([G,E,C]).normal_form.should == [0,4,7]
|
232
|
+
end
|
233
|
+
|
234
|
+
it "is invariant across transpositions" do
|
235
|
+
PITCH_CLASS_SET.new([C,Eb,G]).normal_form.should == [0,3,7]
|
236
|
+
PITCH_CLASS_SET.new([Db,E,Ab]).normal_form.should == [0,3,7]
|
237
|
+
PITCH_CLASS_SET.new([Bb,F,Db]).normal_form.should == [0,3,7]
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
describe "#==" do
|
242
|
+
it "is true if two pitch class sets contain the same set in the same order" do
|
243
|
+
pitch_class_set.should == MTK.PitchClassSet(C,E,G)
|
244
|
+
end
|
245
|
+
|
246
|
+
it "is false if two pitch class sets are not in the same order" do
|
247
|
+
pitch_class_set.should_not == MTK.PitchClassSet(C,G,E)
|
248
|
+
end
|
249
|
+
|
250
|
+
it "is false when if otherwise equal pitch class sets don't contain the same number of duplicates" do
|
251
|
+
PITCH_CLASS_SET.new([C, E, G]).should_not == PITCH_CLASS_SET.new([C, C, E, G])
|
252
|
+
end
|
253
|
+
|
254
|
+
it "is false if two pitch class sets do not contain the same pitch classes" do
|
255
|
+
pitch_class_set.should_not == MTK.PitchClassSet(C,E)
|
256
|
+
end
|
257
|
+
|
258
|
+
it "allows for direct comparison with Arrays" do
|
259
|
+
pitch_class_set.should == [C,E,G]
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
describe "#=~" do
|
264
|
+
it "is true if two pitch class sets contain the same set in the same order" do
|
265
|
+
pitch_class_set.should =~ MTK.PitchClassSet(C,E,G)
|
266
|
+
end
|
267
|
+
|
268
|
+
it "is true when all the pitch classes are equal, even with different numbers of duplicates" do
|
269
|
+
MTK::Groups::Melody.new([C, E, G]).should =~ MTK::Groups::Melody.new([C, C, E, G])
|
270
|
+
end
|
271
|
+
|
272
|
+
it "is true if two pitch class sets are not in the same order" do
|
273
|
+
pitch_class_set.should =~ MTK.PitchClassSet(C,G,E)
|
274
|
+
end
|
275
|
+
|
276
|
+
it "is false if two pitch class sets do not contain the same pitch classes" do
|
277
|
+
pitch_class_set.should_not =~ MTK.PitchClassSet(C,E)
|
278
|
+
end
|
279
|
+
|
280
|
+
it "allows for direct comparison with Arrays" do
|
281
|
+
pitch_class_set.should =~ [C,G,E]
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
describe ".span_between" do
|
286
|
+
it "is the distance in semitones between 2 pitch classes" do
|
287
|
+
PITCH_CLASS_SET.span_between(F, Bb).should == 5
|
288
|
+
end
|
289
|
+
|
290
|
+
it "assumes an ascending interval between the arguments (order of arguments matters)" do
|
291
|
+
PITCH_CLASS_SET.span_between(Bb, F).should == 7
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
end
|
296
|
+
|
297
|
+
describe MTK do
|
298
|
+
|
299
|
+
describe '#PitchClassSet' do
|
300
|
+
|
301
|
+
it "constructs a PitchClassSet" do
|
302
|
+
PitchClassSet(C,D).should be_a PITCH_CLASS_SET
|
303
|
+
end
|
304
|
+
|
305
|
+
it "acts like new for a single Array argument" do
|
306
|
+
PitchClassSet([C,D]).should == PITCH_CLASS_SET.new([C,D])
|
307
|
+
end
|
308
|
+
|
309
|
+
it "acts like new for multiple arguments, by treating them like an Array (splat)" do
|
310
|
+
PitchClassSet(C,D).should == PITCH_CLASS_SET.new([C,D])
|
311
|
+
end
|
312
|
+
|
313
|
+
it "handles an Array with elements that can be converted to Pitches" do
|
314
|
+
PitchClassSet(['C','D']).should == PITCH_CLASS_SET.new([C,D])
|
315
|
+
end
|
316
|
+
|
317
|
+
it "handles multiple arguments that can be converted to a Pitch" do
|
318
|
+
PitchClassSet(:C,:D).should == PITCH_CLASS_SET.new([C,D])
|
319
|
+
end
|
320
|
+
|
321
|
+
it "handles a single Pitch" do
|
322
|
+
PitchClassSet(C).should == PITCH_CLASS_SET.new([C])
|
323
|
+
end
|
324
|
+
|
325
|
+
it "handles single elements that can be converted to a Pitch" do
|
326
|
+
PitchClassSet('C').should == PITCH_CLASS_SET.new([C])
|
327
|
+
end
|
328
|
+
|
329
|
+
it "handles a a PitchClassSet" do
|
330
|
+
pitch_set = PITCH_CLASS_SET.new([C,D])
|
331
|
+
PitchClassSet(pitch_set).should == PitchClassSet([C,D])
|
332
|
+
end
|
333
|
+
|
334
|
+
it "raises an error for types it doesn't understand" do
|
335
|
+
lambda{ PitchClassSet({:not => :compatible}) }.should raise_error
|
336
|
+
end
|
337
|
+
|
338
|
+
end
|
339
|
+
|
340
|
+
end
|
@@ -0,0 +1,243 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'mtk/io/midi_file'
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
describe MTK::IO::MIDIFile do
|
6
|
+
|
7
|
+
let(:test_mid) { File.join(File.dirname(__FILE__), '..', '..', 'test.mid') }
|
8
|
+
|
9
|
+
def tempfile
|
10
|
+
@tempfile ||= Tempfile.new 'MTK-midi_file_writer_spec'
|
11
|
+
end
|
12
|
+
|
13
|
+
after do
|
14
|
+
if @tempfile
|
15
|
+
@tempfile.close
|
16
|
+
@tempfile.unlink
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def note_ons_and_offs(track)
|
21
|
+
note_ons, note_offs = [], []
|
22
|
+
for event in track.events
|
23
|
+
note_ons << event if event.is_a? MIDI::NoteOn
|
24
|
+
note_offs << event if event.is_a? MIDI::NoteOff
|
25
|
+
end
|
26
|
+
return note_ons, note_offs
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "#to_timelines" do
|
30
|
+
it "converts a single-track MIDI file to an Array containing one Timeline" do
|
31
|
+
MIDIFile(test_mid).to_timelines.length.should == 1 # one track
|
32
|
+
end
|
33
|
+
|
34
|
+
it "converts note on/off messages to Note events" do
|
35
|
+
MIDIFile(test_mid).to_timelines.first.should == {
|
36
|
+
0.0 => [Note(C4, 0.25, 126/127.0)],
|
37
|
+
1.0 => [Note(Db4, 0.5, 99/127.0)],
|
38
|
+
2.0 => [Note(D4, 0.75, 72/127.0)],
|
39
|
+
3.0 => [Note(Eb4, 1.0, 46/127.0), Note(E4, 1.0, 46/127.0)]
|
40
|
+
}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "#write_timeline" do
|
45
|
+
it 'writes monophonic Notes in a Timeline to a MIDI file' do
|
46
|
+
MIDIFile(tempfile).write_timeline(
|
47
|
+
MTK::Events::Timeline.from_h({
|
48
|
+
0 => Note(C4, q, 0.7),
|
49
|
+
1 => Note(G4, q, 0.8),
|
50
|
+
2 => Note(C5, q, 0.9)
|
51
|
+
})
|
52
|
+
)
|
53
|
+
|
54
|
+
# Now let's parse the file and check some expectations
|
55
|
+
File.open(tempfile.path, 'rb') do |file|
|
56
|
+
seq = MIDI::Sequence.new
|
57
|
+
seq.read(file)
|
58
|
+
seq.tracks.size.should == 1
|
59
|
+
|
60
|
+
track = seq.tracks[0]
|
61
|
+
note_ons, note_offs = note_ons_and_offs(track)
|
62
|
+
note_ons.length.should == 3
|
63
|
+
note_offs.length.should == 3
|
64
|
+
|
65
|
+
note_ons[0].note.should == C4.to_i
|
66
|
+
note_ons[0].velocity.should be_within(0.5).of(127*0.7)
|
67
|
+
note_offs[0].note.should == C4.to_i
|
68
|
+
note_ons[0].time_from_start.should == 0
|
69
|
+
note_offs[0].time_from_start.should == 480
|
70
|
+
|
71
|
+
note_ons[1].note.should == G4.to_i
|
72
|
+
note_ons[1].velocity.should be_within(0.5).of(127*0.8)
|
73
|
+
note_offs[1].note.should == G4.to_i
|
74
|
+
note_ons[1].time_from_start.should == 480
|
75
|
+
note_offs[1].time_from_start.should == 960
|
76
|
+
|
77
|
+
note_ons[2].note.should == C5.to_i
|
78
|
+
note_ons[2].velocity.should be_within(0.5).of(127*0.9)
|
79
|
+
note_offs[2].note.should == C5.to_i
|
80
|
+
note_ons[2].time_from_start.should == 960
|
81
|
+
note_offs[2].time_from_start.should == 1440
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'writes polyphonic (simultaneous) Notes in a Timeline to a MIDI file' do
|
86
|
+
MIDIFile(tempfile).write_timeline(
|
87
|
+
MTK::Events::Timeline.from_h({
|
88
|
+
0 => [Note(C4,q,0.5), Note(E4,q,0.5)],
|
89
|
+
2.0 => [Note(G4,h,1), Note(B4,h,1), Note(D5,h,1)]
|
90
|
+
})
|
91
|
+
)
|
92
|
+
|
93
|
+
# Now let's parse the file and check some expectations
|
94
|
+
File.open(tempfile.path, 'rb') do |file|
|
95
|
+
seq = MIDI::Sequence.new
|
96
|
+
seq.read(file)
|
97
|
+
seq.tracks.size.should == 1
|
98
|
+
|
99
|
+
track = seq.tracks[0]
|
100
|
+
note_ons, note_offs = note_ons_and_offs(track)
|
101
|
+
note_ons.length.should == 5
|
102
|
+
note_offs.length.should == 5
|
103
|
+
|
104
|
+
note_ons[0].note.should == C4.to_i
|
105
|
+
note_offs[0].note.should == C4.to_i
|
106
|
+
note_ons[0].velocity.should == 64
|
107
|
+
note_ons[0].time_from_start.should == 0
|
108
|
+
note_offs[0].time_from_start.should == 480
|
109
|
+
|
110
|
+
note_ons[1].note.should == E4.to_i
|
111
|
+
note_offs[1].note.should == E4.to_i
|
112
|
+
note_ons[1].velocity.should == 64
|
113
|
+
note_ons[1].time_from_start.should == 0
|
114
|
+
note_offs[1].time_from_start.should == 480
|
115
|
+
|
116
|
+
note_ons[2].note.should == G4.to_i
|
117
|
+
note_offs[2].note.should == G4.to_i
|
118
|
+
note_ons[2].velocity.should == 127
|
119
|
+
note_ons[2].time_from_start.should == 960
|
120
|
+
note_offs[2].time_from_start.should == 1920
|
121
|
+
|
122
|
+
note_ons[3].note.should == B4.to_i
|
123
|
+
note_offs[3].note.should == B4.to_i
|
124
|
+
note_ons[3].velocity.should == 127
|
125
|
+
note_ons[3].time_from_start.should == 960
|
126
|
+
note_offs[3].time_from_start.should == 1920
|
127
|
+
|
128
|
+
note_ons[4].note.should == D5.to_i
|
129
|
+
note_offs[4].note.should == D5.to_i
|
130
|
+
note_ons[4].velocity.should == 127
|
131
|
+
note_ons[4].time_from_start.should == 960
|
132
|
+
note_offs[4].time_from_start.should == 1920
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'ignores rests (events with negative duration)' do
|
137
|
+
MIDIFile(tempfile).write_timeline(
|
138
|
+
MTK::Events::Timeline.from_h({
|
139
|
+
0 => Note(C4, q, 0.7),
|
140
|
+
1 => Note(G4, -q, 0.8), # this is a rest because it has a negative duration
|
141
|
+
2 => Note(C5, q, 0.9)
|
142
|
+
})
|
143
|
+
)
|
144
|
+
|
145
|
+
# Now let's parse the file and check some expectations
|
146
|
+
File.open(tempfile.path, 'rb') do |file|
|
147
|
+
seq = MIDI::Sequence.new
|
148
|
+
seq.read(file)
|
149
|
+
seq.tracks.size.should == 1
|
150
|
+
|
151
|
+
track = seq.tracks[0]
|
152
|
+
note_ons, note_offs = note_ons_and_offs(track)
|
153
|
+
note_ons.length.should == 2
|
154
|
+
note_offs.length.should == 2
|
155
|
+
|
156
|
+
note_ons[0].note.should == C4.to_i
|
157
|
+
note_ons[0].velocity.should be_within(0.5).of(127*0.7)
|
158
|
+
note_offs[0].note.should == C4.to_i
|
159
|
+
note_ons[0].time_from_start.should == 0
|
160
|
+
note_offs[0].time_from_start.should == 480
|
161
|
+
|
162
|
+
note_ons[1].note.should == C5.to_i
|
163
|
+
note_ons[1].velocity.should be_within(0.5).of(127*0.9)
|
164
|
+
note_offs[1].note.should == C5.to_i
|
165
|
+
note_ons[1].time_from_start.should == 960
|
166
|
+
note_offs[1].time_from_start.should == 1440
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
describe "#write_timelines" do
|
172
|
+
it "writes a multitrack MIDI file" do
|
173
|
+
MIDIFile(tempfile).write_timelines([
|
174
|
+
MTK::Events::Timeline.from_h({
|
175
|
+
0 => Note(C4, q, 0.7),
|
176
|
+
1.0 => Note(G4, q, 0.8),
|
177
|
+
}),
|
178
|
+
MTK::Events::Timeline.from_h({
|
179
|
+
1 => Note(C5, h, 0.9),
|
180
|
+
2 => Note(D5, h, 1),
|
181
|
+
}),
|
182
|
+
])
|
183
|
+
|
184
|
+
# Now let's parse the file and check some expectations
|
185
|
+
File.open(tempfile.path, 'rb') do |file|
|
186
|
+
seq = MIDI::Sequence.new
|
187
|
+
seq.read(file)
|
188
|
+
seq.tracks.size.should == 2
|
189
|
+
|
190
|
+
track = seq.tracks[0]
|
191
|
+
note_ons, note_offs = note_ons_and_offs(track)
|
192
|
+
note_ons.length.should == 2
|
193
|
+
note_offs.length.should == 2
|
194
|
+
|
195
|
+
note_ons[0].note.should == C4.to_i
|
196
|
+
note_ons[0].velocity.should be_within(0.5).of(127*0.7)
|
197
|
+
note_offs[0].note.should == C4.to_i
|
198
|
+
note_ons[0].time_from_start.should == 0
|
199
|
+
note_offs[0].time_from_start.should == 480
|
200
|
+
|
201
|
+
note_ons[1].note.should == G4.to_i
|
202
|
+
note_ons[1].velocity.should be_within(0.5).of(127*0.8)
|
203
|
+
note_offs[1].note.should == G4.to_i
|
204
|
+
note_ons[1].time_from_start.should == 480
|
205
|
+
note_offs[1].time_from_start.should == 960
|
206
|
+
|
207
|
+
track = seq.tracks[1]
|
208
|
+
note_ons, note_offs = note_ons_and_offs(track)
|
209
|
+
note_ons.length.should == 2
|
210
|
+
note_offs.length.should == 2
|
211
|
+
|
212
|
+
note_ons[0].note.should == C5.to_i
|
213
|
+
note_ons[0].velocity.should be_within(0.5).of(127*0.9)
|
214
|
+
note_offs[0].note.should == C5.to_i
|
215
|
+
note_ons[0].time_from_start.should == 480
|
216
|
+
note_offs[0].time_from_start.should == 1440
|
217
|
+
|
218
|
+
note_ons[1].note.should == D5.to_i
|
219
|
+
note_ons[1].velocity.should == 127
|
220
|
+
note_offs[1].note.should == D5.to_i
|
221
|
+
note_ons[1].time_from_start.should == 960
|
222
|
+
note_offs[1].time_from_start.should == 1920
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
describe "#write" do
|
228
|
+
it "calls write_timeline when given a Timeline" do
|
229
|
+
midi_file = MIDIFile(nil)
|
230
|
+
timeline = MTK::Events::Timeline.new
|
231
|
+
midi_file.should_receive(:write_timeline).with(timeline)
|
232
|
+
midi_file.write(timeline)
|
233
|
+
end
|
234
|
+
|
235
|
+
it "calls write_timelines when given an Array" do
|
236
|
+
midi_file = MIDIFile(nil)
|
237
|
+
timelines = [MTK::Events::Timeline.new]
|
238
|
+
midi_file.should_receive(:write_timelines).with(timelines)
|
239
|
+
midi_file.write(timelines)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|