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.
Files changed (110) hide show
  1. data/.yardopts +10 -0
  2. data/DEVELOPMENT_NOTES.md +115 -0
  3. data/INTRO.md +129 -0
  4. data/LICENSE.txt +27 -0
  5. data/README.md +50 -0
  6. data/Rakefile +102 -0
  7. data/bin/jmtk +250 -0
  8. data/bin/mtk +250 -0
  9. data/examples/crescendo.rb +20 -0
  10. data/examples/drum_pattern.rb +23 -0
  11. data/examples/dynamic_pattern.rb +36 -0
  12. data/examples/gets_and_play.rb +27 -0
  13. data/examples/notation.rb +22 -0
  14. data/examples/play_midi.rb +17 -0
  15. data/examples/print_midi.rb +13 -0
  16. data/examples/random_tone_row.rb +18 -0
  17. data/examples/syntax_to_midi.rb +28 -0
  18. data/examples/test_output.rb +7 -0
  19. data/examples/tone_row_melody.rb +23 -0
  20. data/lib/mtk.rb +76 -0
  21. data/lib/mtk/core/duration.rb +213 -0
  22. data/lib/mtk/core/intensity.rb +158 -0
  23. data/lib/mtk/core/interval.rb +157 -0
  24. data/lib/mtk/core/pitch.rb +154 -0
  25. data/lib/mtk/core/pitch_class.rb +194 -0
  26. data/lib/mtk/events/event.rb +119 -0
  27. data/lib/mtk/events/note.rb +112 -0
  28. data/lib/mtk/events/parameter.rb +54 -0
  29. data/lib/mtk/events/timeline.rb +232 -0
  30. data/lib/mtk/groups/chord.rb +56 -0
  31. data/lib/mtk/groups/collection.rb +196 -0
  32. data/lib/mtk/groups/melody.rb +96 -0
  33. data/lib/mtk/groups/pitch_class_set.rb +163 -0
  34. data/lib/mtk/groups/pitch_collection.rb +23 -0
  35. data/lib/mtk/io/dls_synth_device.rb +146 -0
  36. data/lib/mtk/io/dls_synth_output.rb +62 -0
  37. data/lib/mtk/io/jsound_input.rb +87 -0
  38. data/lib/mtk/io/jsound_output.rb +82 -0
  39. data/lib/mtk/io/midi_file.rb +209 -0
  40. data/lib/mtk/io/midi_input.rb +97 -0
  41. data/lib/mtk/io/midi_output.rb +195 -0
  42. data/lib/mtk/io/notation.rb +162 -0
  43. data/lib/mtk/io/unimidi_input.rb +117 -0
  44. data/lib/mtk/io/unimidi_output.rb +140 -0
  45. data/lib/mtk/lang/durations.rb +57 -0
  46. data/lib/mtk/lang/intensities.rb +61 -0
  47. data/lib/mtk/lang/intervals.rb +73 -0
  48. data/lib/mtk/lang/mtk_grammar.citrus +237 -0
  49. data/lib/mtk/lang/parser.rb +29 -0
  50. data/lib/mtk/lang/pitch_classes.rb +29 -0
  51. data/lib/mtk/lang/pitches.rb +52 -0
  52. data/lib/mtk/lang/pseudo_constants.rb +26 -0
  53. data/lib/mtk/lang/variable.rb +32 -0
  54. data/lib/mtk/numeric_extensions.rb +66 -0
  55. data/lib/mtk/patterns/chain.rb +49 -0
  56. data/lib/mtk/patterns/choice.rb +43 -0
  57. data/lib/mtk/patterns/cycle.rb +18 -0
  58. data/lib/mtk/patterns/for_each.rb +71 -0
  59. data/lib/mtk/patterns/function.rb +39 -0
  60. data/lib/mtk/patterns/lines.rb +54 -0
  61. data/lib/mtk/patterns/palindrome.rb +45 -0
  62. data/lib/mtk/patterns/pattern.rb +171 -0
  63. data/lib/mtk/patterns/sequence.rb +20 -0
  64. data/lib/mtk/sequencers/event_builder.rb +132 -0
  65. data/lib/mtk/sequencers/legato_sequencer.rb +24 -0
  66. data/lib/mtk/sequencers/rhythmic_sequencer.rb +28 -0
  67. data/lib/mtk/sequencers/sequencer.rb +111 -0
  68. data/lib/mtk/sequencers/step_sequencer.rb +26 -0
  69. data/spec/mtk/core/duration_spec.rb +372 -0
  70. data/spec/mtk/core/intensity_spec.rb +289 -0
  71. data/spec/mtk/core/interval_spec.rb +265 -0
  72. data/spec/mtk/core/pitch_class_spec.rb +343 -0
  73. data/spec/mtk/core/pitch_spec.rb +297 -0
  74. data/spec/mtk/events/event_spec.rb +234 -0
  75. data/spec/mtk/events/note_spec.rb +174 -0
  76. data/spec/mtk/events/parameter_spec.rb +220 -0
  77. data/spec/mtk/events/timeline_spec.rb +430 -0
  78. data/spec/mtk/groups/chord_spec.rb +85 -0
  79. data/spec/mtk/groups/collection_spec.rb +374 -0
  80. data/spec/mtk/groups/melody_spec.rb +225 -0
  81. data/spec/mtk/groups/pitch_class_set_spec.rb +340 -0
  82. data/spec/mtk/io/midi_file_spec.rb +243 -0
  83. data/spec/mtk/io/midi_output_spec.rb +102 -0
  84. data/spec/mtk/lang/durations_spec.rb +89 -0
  85. data/spec/mtk/lang/intensities_spec.rb +101 -0
  86. data/spec/mtk/lang/intervals_spec.rb +143 -0
  87. data/spec/mtk/lang/parser_spec.rb +603 -0
  88. data/spec/mtk/lang/pitch_classes_spec.rb +62 -0
  89. data/spec/mtk/lang/pitches_spec.rb +56 -0
  90. data/spec/mtk/lang/pseudo_constants_spec.rb +20 -0
  91. data/spec/mtk/lang/variable_spec.rb +52 -0
  92. data/spec/mtk/numeric_extensions_spec.rb +83 -0
  93. data/spec/mtk/patterns/chain_spec.rb +110 -0
  94. data/spec/mtk/patterns/choice_spec.rb +97 -0
  95. data/spec/mtk/patterns/cycle_spec.rb +123 -0
  96. data/spec/mtk/patterns/for_each_spec.rb +136 -0
  97. data/spec/mtk/patterns/function_spec.rb +120 -0
  98. data/spec/mtk/patterns/lines_spec.rb +77 -0
  99. data/spec/mtk/patterns/palindrome_spec.rb +108 -0
  100. data/spec/mtk/patterns/pattern_spec.rb +132 -0
  101. data/spec/mtk/patterns/sequence_spec.rb +203 -0
  102. data/spec/mtk/sequencers/event_builder_spec.rb +245 -0
  103. data/spec/mtk/sequencers/legato_sequencer_spec.rb +45 -0
  104. data/spec/mtk/sequencers/rhythmic_sequencer_spec.rb +84 -0
  105. data/spec/mtk/sequencers/sequencer_spec.rb +215 -0
  106. data/spec/mtk/sequencers/step_sequencer_spec.rb +93 -0
  107. data/spec/spec_coverage.rb +2 -0
  108. data/spec/spec_helper.rb +12 -0
  109. data/spec/test.mid +0 -0
  110. 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