mtk 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +9 -0
- data/INTRO.md +73 -0
- data/LICENSE.txt +27 -0
- data/README.md +93 -18
- data/Rakefile +13 -1
- data/examples/crescendo.rb +20 -0
- data/examples/dynamic_pattern.rb +39 -0
- data/examples/play_midi.rb +19 -0
- data/examples/print_midi.rb +13 -0
- data/examples/random_tone_row.rb +18 -0
- data/examples/tone_row_melody.rb +21 -0
- data/lib/mtk/_constants/durations.rb +80 -0
- data/lib/mtk/_constants/intensities.rb +81 -0
- data/lib/mtk/{constants → _constants}/intervals.rb +10 -1
- data/lib/mtk/_constants/pitch_classes.rb +35 -0
- data/lib/mtk/_constants/pitches.rb +49 -0
- data/lib/mtk/{numeric_extensions.rb → _numeric_extensions.rb} +0 -0
- data/lib/mtk/event.rb +14 -5
- data/lib/mtk/helper/collection.rb +114 -0
- data/lib/mtk/helper/event_builder.rb +85 -0
- data/lib/mtk/{constants → helper}/pseudo_constants.rb +7 -6
- data/lib/mtk/lang/grammar.rb +17 -0
- data/lib/mtk/lang/mtk_grammar.citrus +60 -0
- data/lib/mtk/midi/file.rb +10 -15
- data/lib/mtk/midi/jsound_input.rb +68 -0
- data/lib/mtk/midi/jsound_output.rb +80 -0
- data/lib/mtk/note.rb +22 -3
- data/lib/mtk/pattern/abstract_pattern.rb +132 -0
- data/lib/mtk/pattern/choice.rb +25 -9
- data/lib/mtk/pattern/cycle.rb +51 -0
- data/lib/mtk/pattern/enumerator.rb +26 -0
- data/lib/mtk/pattern/function.rb +46 -0
- data/lib/mtk/pattern/lines.rb +60 -0
- data/lib/mtk/pattern/palindrome.rb +42 -0
- data/lib/mtk/pattern/sequence.rb +15 -50
- data/lib/mtk/pitch.rb +45 -6
- data/lib/mtk/pitch_class.rb +36 -35
- data/lib/mtk/pitch_class_set.rb +46 -14
- data/lib/mtk/pitch_set.rb +20 -31
- data/lib/mtk/sequencer/abstract_sequencer.rb +85 -0
- data/lib/mtk/sequencer/rhythmic_sequencer.rb +29 -0
- data/lib/mtk/sequencer/step_sequencer.rb +26 -0
- data/lib/mtk/timeline.rb +75 -22
- data/lib/mtk/transform/invertible.rb +15 -0
- data/lib/mtk/{util → transform}/mappable.rb +6 -2
- data/lib/mtk/transform/set_theory_operations.rb +34 -0
- data/lib/mtk/transform/transposable.rb +14 -0
- data/lib/mtk.rb +56 -22
- data/spec/mtk/_constants/durations_spec.rb +118 -0
- data/spec/mtk/{constants/dynamics_spec.rb → _constants/intensities_spec.rb} +48 -17
- data/spec/mtk/{constants → _constants}/intervals_spec.rb +21 -0
- data/spec/mtk/_constants/pitch_classes_spec.rb +58 -0
- data/spec/mtk/_constants/pitches_spec.rb +52 -0
- data/spec/mtk/{numeric_extensions_spec.rb → _numeric_extensions_spec.rb} +0 -0
- data/spec/mtk/event_spec.rb +19 -0
- data/spec/mtk/helper/collection_spec.rb +291 -0
- data/spec/mtk/helper/event_builder_spec.rb +92 -0
- data/spec/mtk/helper/pseudo_constants_spec.rb +20 -0
- data/spec/mtk/lang/grammar_spec.rb +100 -0
- data/spec/mtk/midi/file_spec.rb +41 -6
- data/spec/mtk/note_spec.rb +53 -3
- data/spec/mtk/pattern/abstract_pattern_spec.rb +45 -0
- data/spec/mtk/pattern/choice_spec.rb +89 -3
- data/spec/mtk/pattern/cycle_spec.rb +133 -0
- data/spec/mtk/pattern/function_spec.rb +133 -0
- data/spec/mtk/pattern/lines_spec.rb +93 -0
- data/spec/mtk/pattern/note_cycle_spec.rb.bak +116 -0
- data/spec/mtk/pattern/palindrome_spec.rb +124 -0
- data/spec/mtk/pattern/pitch_cycle_spec.rb.bak +47 -0
- data/spec/mtk/pattern/pitch_sequence_spec.rb.bak +37 -0
- data/spec/mtk/pattern/sequence_spec.rb +128 -31
- data/spec/mtk/pitch_class_set_spec.rb +240 -7
- data/spec/mtk/pitch_class_spec.rb +84 -18
- data/spec/mtk/pitch_set_spec.rb +45 -10
- data/spec/mtk/pitch_spec.rb +59 -0
- data/spec/mtk/sequencer/abstract_sequencer_spec.rb +159 -0
- data/spec/mtk/sequencer/rhythmic_sequencer_spec.rb +49 -0
- data/spec/mtk/sequencer/step_sequencer_spec.rb +71 -0
- data/spec/mtk/timeline_spec.rb +118 -15
- data/spec/spec_helper.rb +4 -3
- metadata +59 -22
- data/lib/mtk/chord.rb +0 -47
- data/lib/mtk/constants/dynamics.rb +0 -56
- data/lib/mtk/constants/pitch_classes.rb +0 -18
- data/lib/mtk/constants/pitches.rb +0 -24
- data/lib/mtk/pattern/note_sequence.rb +0 -60
- data/lib/mtk/pattern/pitch_sequence.rb +0 -22
- data/lib/mtk/patterns.rb +0 -4
- data/spec/mtk/chord_spec.rb +0 -74
- data/spec/mtk/constants/pitch_classes_spec.rb +0 -35
- data/spec/mtk/constants/pitches_spec.rb +0 -23
- data/spec/mtk/pattern/note_sequence_spec.rb +0 -121
- data/spec/mtk/pattern/pitch_sequence_spec.rb +0 -47
@@ -0,0 +1,291 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MTK::Helper::Collection do
|
4
|
+
|
5
|
+
class MockCollection
|
6
|
+
include MTK::Helper::Collection
|
7
|
+
attr_reader :elements
|
8
|
+
def initialize(elements); @elements = elements end
|
9
|
+
def self.from_a(elements); new(elements) end
|
10
|
+
end
|
11
|
+
|
12
|
+
class MockCollectionWithOptions
|
13
|
+
include MTK::Helper::Collection
|
14
|
+
attr_reader :elements, :options
|
15
|
+
def initialize(elements, options={})
|
16
|
+
@elements = elements
|
17
|
+
@options = options
|
18
|
+
end
|
19
|
+
def self.from_a(elements, options={})
|
20
|
+
new(elements, options)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:elements) { [1,2,3,4,5] }
|
25
|
+
let(:collection) { MockCollection.new elements }
|
26
|
+
|
27
|
+
let(:options) { {:opt1 => :val1, :opt2 => :val2} }
|
28
|
+
let(:collection_with_options) { MockCollectionWithOptions.new(elements, options) }
|
29
|
+
|
30
|
+
it "is Enumerable" do
|
31
|
+
collection.should be_a Enumerable
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "#to_a" do
|
35
|
+
it "is the the Array of elements in the collection" do
|
36
|
+
collection.to_a.should == elements
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#size" do
|
41
|
+
it "is the length of the elements" do
|
42
|
+
collection.size.should == elements.length
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#length" do
|
47
|
+
it "acts like #size" do
|
48
|
+
collection.length.should == collection.size
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#empty?" do
|
53
|
+
it "is true when elements is nil" do
|
54
|
+
MockCollection.new(nil).empty?.should be_true
|
55
|
+
end
|
56
|
+
|
57
|
+
it "is true when elements is empty" do
|
58
|
+
MockCollection.new([]).empty?.should be_true
|
59
|
+
end
|
60
|
+
|
61
|
+
it "is false when elements is not empty" do
|
62
|
+
MockCollection.new([1]).empty?.should be_false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "#each" do
|
67
|
+
it "yields each element" do
|
68
|
+
yielded = []
|
69
|
+
collection.each do |element|
|
70
|
+
yielded << element
|
71
|
+
end
|
72
|
+
yielded.should == elements
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "#first" do
|
77
|
+
it "is #[0] when no argument is given" do
|
78
|
+
collection.first.should == collection[0]
|
79
|
+
end
|
80
|
+
|
81
|
+
it "is the first n elements, for an argument n" do
|
82
|
+
collection.first(3).should == elements[0..2]
|
83
|
+
end
|
84
|
+
|
85
|
+
it "is the elements when n is bigger than elements.length, for an argument n" do
|
86
|
+
collection.first(elements.length * 2).should == elements
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe "#last when no argument is given" do
|
91
|
+
it "is #[-1]" do
|
92
|
+
collection.last.should == collection[-1]
|
93
|
+
end
|
94
|
+
|
95
|
+
it "is the last n elements, for an argument n" do
|
96
|
+
collection.last(3).should == elements[-3..-1]
|
97
|
+
end
|
98
|
+
|
99
|
+
it "is the elements when n is bigger than elements.length, for an argument n" do
|
100
|
+
collection.last(elements.length * 2).should == elements
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe "#[]" do
|
105
|
+
it "accesses an element by index" do
|
106
|
+
collection[3].should == elements[3]
|
107
|
+
end
|
108
|
+
|
109
|
+
it "supports negative indexes" do
|
110
|
+
collection[-1].should == elements[-1]
|
111
|
+
end
|
112
|
+
|
113
|
+
it "accesses ranges of elements" do
|
114
|
+
collection[0..3].should == elements[0..3]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe "#repeat" do
|
119
|
+
it "repeats the elements the number of times given by the argument" do
|
120
|
+
collection.repeat(3).should == [1,2,3,4,5,1,2,3,4,5,1,2,3,4,5]
|
121
|
+
end
|
122
|
+
|
123
|
+
it "repeats the elements twice if no argument is given" do
|
124
|
+
collection.repeat.should == [1,2,3,4,5,1,2,3,4,5]
|
125
|
+
end
|
126
|
+
|
127
|
+
it "handles fractional repetitions" do
|
128
|
+
collection.repeat(1.6).should == [1,2,3,4,5,1,2,3]
|
129
|
+
end
|
130
|
+
|
131
|
+
it "returns an instance of the same type" do
|
132
|
+
collection.repeat.should be_a collection.class
|
133
|
+
end
|
134
|
+
|
135
|
+
it "does not modify the original collection" do
|
136
|
+
collection.repeat.should_not equal collection
|
137
|
+
end
|
138
|
+
|
139
|
+
it "maintains the options from the original collection" do
|
140
|
+
collection_with_options.repeat.options.should == options
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe "#permute" do
|
145
|
+
it "randomly rearranges the order of elements" do
|
146
|
+
elements = (0..1000).to_a
|
147
|
+
permuted = MockCollection.new(elements).permute
|
148
|
+
permuted.should_not == elements
|
149
|
+
permuted.sort.should == elements
|
150
|
+
end
|
151
|
+
|
152
|
+
it "returns an instance of the same type" do
|
153
|
+
collection.permute.should be_a collection.class
|
154
|
+
end
|
155
|
+
|
156
|
+
it "does not modify the original collection" do
|
157
|
+
collection.permute.should_not equal collection
|
158
|
+
end
|
159
|
+
|
160
|
+
it "maintains the options from the original collection" do
|
161
|
+
collection_with_options.permute.options.should == options
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
describe "#shuffle" do
|
166
|
+
it "behaves like #permute" do
|
167
|
+
elements = (0..1000).to_a
|
168
|
+
shuffled = MockCollection.new(elements).shuffle
|
169
|
+
shuffled.should_not == elements
|
170
|
+
shuffled.sort.should == elements
|
171
|
+
end
|
172
|
+
|
173
|
+
it "returns an instance of the same type" do
|
174
|
+
collection.shuffle.should be_a collection.class
|
175
|
+
end
|
176
|
+
|
177
|
+
it "does not modify the original collection" do
|
178
|
+
collection.shuffle.should_not equal collection
|
179
|
+
end
|
180
|
+
|
181
|
+
it "maintains the options from the original collection" do
|
182
|
+
collection_with_options.shuffle.options.should == options
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
describe "#rotate" do
|
187
|
+
it "produces a Collection that is rotated right by the given positive offset" do
|
188
|
+
collection.rotate(2).should == [3,4,5,1,2]
|
189
|
+
end
|
190
|
+
|
191
|
+
it "produces a Collection that is rotated left by the given negative offset" do
|
192
|
+
collection.rotate(-2).should == [4,5,1,2,3]
|
193
|
+
end
|
194
|
+
|
195
|
+
it "rotates by 1 if no argument is given" do
|
196
|
+
collection.rotate.should == [2,3,4,5,1]
|
197
|
+
end
|
198
|
+
|
199
|
+
it "returns an instance of the same type" do
|
200
|
+
collection.rotate.should be_a collection.class
|
201
|
+
end
|
202
|
+
|
203
|
+
it "does not modify the original collection" do
|
204
|
+
collection.rotate.should_not equal collection
|
205
|
+
end
|
206
|
+
|
207
|
+
it "maintains the options from the original collection" do
|
208
|
+
collection_with_options.rotate.options.should == options
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
describe "#concat" do
|
213
|
+
it "appends the argument to the elements" do
|
214
|
+
collection.concat([6,7]).should == [1,2,3,4,5,6,7]
|
215
|
+
end
|
216
|
+
|
217
|
+
it "returns an instance of the same type" do
|
218
|
+
collection.concat([6,7]).should be_a collection.class
|
219
|
+
end
|
220
|
+
|
221
|
+
it "does not modify the original collection" do
|
222
|
+
collection.concat([6,7]).should_not equal collection
|
223
|
+
end
|
224
|
+
|
225
|
+
it "maintains the options from the original (receiver) collection" do
|
226
|
+
collection_with_options.concat([6,7]).options.should == options
|
227
|
+
end
|
228
|
+
|
229
|
+
it "ignored any options from the argument collection" do
|
230
|
+
# I considered merging the options, but it seems potentially too confusing, so
|
231
|
+
# we'll go with this simpler behavior until a use-case appears where this is inappropriate.
|
232
|
+
arg = MockCollectionWithOptions.new(elements, :opt1 => :another_val, :opt3 => :val3)
|
233
|
+
collection_with_options.concat(arg).options.should == options
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
describe "#reverse" do
|
238
|
+
it "reverses the elements" do
|
239
|
+
collection.reverse.should == elements.reverse
|
240
|
+
end
|
241
|
+
|
242
|
+
it "returns an instance of the same type" do
|
243
|
+
collection.reverse.should be_a collection.class
|
244
|
+
end
|
245
|
+
|
246
|
+
it "does not modify the original collection" do
|
247
|
+
collection.reverse.should_not equal collection
|
248
|
+
end
|
249
|
+
|
250
|
+
it "maintains the options from the original collection" do
|
251
|
+
collection_with_options.reverse.options.should == options
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
describe "#==" do
|
256
|
+
it "is true when the elements in 2 Collections are equal" do
|
257
|
+
collection.should == MockCollection.new(elements)
|
258
|
+
end
|
259
|
+
|
260
|
+
it "is true when the elements equal the argument" do
|
261
|
+
collection.should == elements
|
262
|
+
end
|
263
|
+
|
264
|
+
it "is false when the elements in 2 Collections are not equal" do
|
265
|
+
collection.should_not == MockCollection.new(elements + [1,2])
|
266
|
+
end
|
267
|
+
|
268
|
+
it "is false when the elements do not equal the argument" do
|
269
|
+
collection.should_not == (elements + [1,2])
|
270
|
+
end
|
271
|
+
|
272
|
+
it "is false when the options are not equal" do
|
273
|
+
collection.should_not == collection_with_options
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
describe "#clone" do
|
278
|
+
it "creates an equal collection" do
|
279
|
+
collection.clone.should == collection
|
280
|
+
end
|
281
|
+
|
282
|
+
it "creates a new collection" do
|
283
|
+
collection.clone.should_not equal collection
|
284
|
+
end
|
285
|
+
|
286
|
+
it "maintains the options from the original collection" do
|
287
|
+
collection_with_options.clone.options.should == options
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MTK::Helper::EventBuilder do
|
4
|
+
|
5
|
+
EVENT_BUILDER = MTK::Helper::EventBuilder
|
6
|
+
|
7
|
+
let(:intensity) { EVENT_BUILDER::DEFAULT_INTENSITY }
|
8
|
+
let(:duration) { EVENT_BUILDER::DEFAULT_DURATION }
|
9
|
+
|
10
|
+
def notes(*pitches)
|
11
|
+
pitches.map{|pitch| Note(pitch, intensity, duration) }
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
describe "#next" do
|
16
|
+
it "builds a single-note list from a single-pitch list argument" do
|
17
|
+
event_builder = EVENT_BUILDER.new [Pattern.Cycle(C4)]
|
18
|
+
event_builder.next.should == notes(C4)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "builds a list of notes from any pitches in the argument" do
|
22
|
+
event_builder = EVENT_BUILDER.new [Pattern.Cycle(C4), Pattern.Cycle(D4)]
|
23
|
+
event_builder.next.should == notes(C4, D4)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "builds a list of notes from pitch sets" do
|
27
|
+
event_builder = EVENT_BUILDER.new [ Pattern.Cycle( PitchSet(C4,D4) ) ]
|
28
|
+
event_builder.next.should == notes(C4, D4)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "builds notes from pitch classes and a default_pitch, selecting the nearest pitch class to the previous pitch" do
|
32
|
+
event_builder = EVENT_BUILDER.new [Pattern.Sequence(C,G,B,Eb,D,C)], :default_pitch => D3
|
33
|
+
notes = []
|
34
|
+
loop do
|
35
|
+
notes << event_builder.next
|
36
|
+
end
|
37
|
+
notes.flatten.should == notes(C3,G2,B2,Eb3,D3,C3)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "defaults to a starting point of C4 (middle C)" do
|
41
|
+
event_builder = EVENT_BUILDER.new [Pattern.Sequence(C4)]
|
42
|
+
event_builder.next.should == notes(C4)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "builds notes from pitch class sets, selecting the neartest pitch classes to the previous/default pitch" do
|
46
|
+
pitch_class_sequence = Pattern::Sequence.new([PitchClassSet(C,G),PitchClassSet(B,Eb),PitchClassSet(D,C)])
|
47
|
+
event_builder = EVENT_BUILDER.new [pitch_class_sequence], :default_pitch => D3
|
48
|
+
event_builder.next.should == notes(C3,G3)
|
49
|
+
event_builder.next.should == notes(B3,Eb3)
|
50
|
+
event_builder.next.should == notes(D3,C3)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "builds notes from by adding Numeric intervals in :pitch type Patterns to the previous Pitch" do
|
54
|
+
event_builder = EVENT_BUILDER.new [ Pattern.PitchSequence( C4, M3, m3, -P5) ]
|
55
|
+
nexts = []
|
56
|
+
loop { nexts << event_builder.next }
|
57
|
+
nexts.should == [notes(C4), notes(E4), notes(G4), notes(C4)]
|
58
|
+
end
|
59
|
+
|
60
|
+
it "builds notes from by adding Numeric intervals in :pitch type Patterns to all pitches in the previous PitchSet" do
|
61
|
+
event_builder = EVENT_BUILDER.new [ Pattern.PitchSequence( PitchSet(C4,Eb4), M3, m3, -P5) ]
|
62
|
+
nexts = []
|
63
|
+
loop { nexts << event_builder.next }
|
64
|
+
nexts.should == [notes(C4,Eb4), notes(E4,G4), notes(G4,Bb4), notes(C4,Eb4)]
|
65
|
+
end
|
66
|
+
|
67
|
+
it "builds notes from intensities" do
|
68
|
+
event_builder = EVENT_BUILDER.new [ Pattern.PitchCycle(C4), Pattern.IntensitySequence(mf, p, fff) ]
|
69
|
+
nexts = []
|
70
|
+
loop { nexts += event_builder.next }
|
71
|
+
nexts.should == [Note(C4, mf, duration), Note(C4, p, duration), Note(C4, fff, duration)]
|
72
|
+
end
|
73
|
+
|
74
|
+
it "builds notes from durations" do
|
75
|
+
event_builder = EVENT_BUILDER.new [ Pattern.PitchCycle(C4), Pattern.DurationSequence(1,2,3) ]
|
76
|
+
nexts = []
|
77
|
+
loop { nexts += event_builder.next }
|
78
|
+
nexts.should == [Note(C4, intensity, 1), Note(C4, intensity, 2), Note(C4, intensity, 3)]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
describe "#rewind" do
|
83
|
+
it "resets the state of the EventBuilder" do
|
84
|
+
event_builder = EVENT_BUILDER.new [ Pattern.PitchSequence(C,P8) ]
|
85
|
+
event_builder.next.should == [Note(C4,intensity,duration)]
|
86
|
+
event_builder.next.should == [Note(C5,intensity,duration)]
|
87
|
+
event_builder.rewind
|
88
|
+
event_builder.next.should == [Note(C4,intensity,duration)]
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MTK::Helper::PseudoConstants do
|
4
|
+
|
5
|
+
module MockConstants
|
6
|
+
extend MTK::Helper::PseudoConstants
|
7
|
+
define_constant 'constant', :value
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "define_constant" do
|
11
|
+
it "defines a method method in the module" do
|
12
|
+
MockConstants::constant.should == :value
|
13
|
+
end
|
14
|
+
|
15
|
+
it "defines a 'module_function'" do
|
16
|
+
MockConstants.constant.should == :value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'mtk/lang/grammar'
|
3
|
+
|
4
|
+
describe MTK::Lang::Grammar do
|
5
|
+
|
6
|
+
def parse syntax, root
|
7
|
+
MTK::Lang::Grammar.parse(syntax, root)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe ".parse" do
|
11
|
+
|
12
|
+
it "should parse pitch sequences" do
|
13
|
+
parse("C4 D4 E4", :pitch_sequence).should == Pattern.PitchSequence(C4, D4, E4)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should parse pitches" do
|
17
|
+
for pitch_class_name in PitchClass::VALID_NAMES
|
18
|
+
for octave in -1..9
|
19
|
+
parse("#{pitch_class_name}#{octave}", :pitch).should == Pitch[PitchClass[pitch_class_name],octave]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should parse pitch classes" do
|
25
|
+
for pitch_class_name in PitchClass::VALID_NAMES
|
26
|
+
parse(pitch_class_name, :pitch_class).should == PitchClass[pitch_class_name]
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should parse intervals" do
|
31
|
+
for interval_name in Intervals::INTERVAL_NAMES
|
32
|
+
parse(interval_name, :interval).should == Intervals[interval_name]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should parse intensities" do
|
37
|
+
for intensity_name in Intensities::INTENSITY_NAMES
|
38
|
+
parse(intensity_name, :intensity).should == Intensities[intensity_name]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should parse intensities with + and - modifiers" do
|
43
|
+
for intensity_name in Intensities::INTENSITY_NAMES
|
44
|
+
name = "#{intensity_name}+"
|
45
|
+
parse(name, :intensity).should == Intensities[name]
|
46
|
+
name = "#{intensity_name}-"
|
47
|
+
parse(name, :intensity).should == Intensities[name]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should parse durations" do
|
52
|
+
for duration in Durations::DURATION_NAMES
|
53
|
+
parse(duration, :duration).should == Durations[duration]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should parse durations with . and t modifiers" do
|
58
|
+
for duration in Durations::DURATION_NAMES
|
59
|
+
name = "#{duration}."
|
60
|
+
parse(name, :duration).should == Durations[name]
|
61
|
+
name = "#{duration}t"
|
62
|
+
parse(name, :duration).should == Durations[name]
|
63
|
+
name = "#{duration}..t.t"
|
64
|
+
parse(name, :duration).should == Durations[name]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should parse ints as numbers" do
|
69
|
+
parse("123", :number).should == 123
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should parse floats as numbers" do
|
73
|
+
parse("1.23", :number).should == 1.23
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should parse floats" do
|
77
|
+
parse("1.23", :float).should == 1.23
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should parse negative floats" do
|
81
|
+
parse("-1.23", :float).should == -1.23
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should parse ints" do
|
85
|
+
parse("123", :int).should == 123
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should parse negative ints" do
|
89
|
+
parse("-123", :int).should == -123
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should parse negative ints" do
|
93
|
+
parse("-123", :int).should == -123
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should give nil as the value for whitespace" do
|
97
|
+
parse(" \t\n", :space).should == nil
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
data/spec/mtk/midi/file_spec.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'spec_helper'
|
1
2
|
require 'mtk/midi/file'
|
2
3
|
require 'tempfile'
|
3
4
|
|
@@ -41,11 +42,11 @@ describe MTK::MIDI::File do
|
|
41
42
|
end
|
42
43
|
|
43
44
|
describe "#write_timeline" do
|
44
|
-
it 'writes Notes in a Timeline to a MIDI file' do
|
45
|
+
it 'writes monophonic Notes in a Timeline to a MIDI file' do
|
45
46
|
MIDI_File(tempfile).write_timeline(
|
46
47
|
Timeline.from_hash({
|
47
48
|
0 => Note.new(C4, 0.7, 1),
|
48
|
-
1 => Note.new(G4, 0.8, 1),
|
49
|
+
1.0 => Note.new(G4, 0.8, 1),
|
49
50
|
2 => Note.new(C5, 0.9, 1)
|
50
51
|
})
|
51
52
|
)
|
@@ -81,11 +82,11 @@ describe MTK::MIDI::File do
|
|
81
82
|
end
|
82
83
|
end
|
83
84
|
|
84
|
-
it 'writes
|
85
|
+
it 'writes polyphonic (simultaneous) Notes in a Timeline to a MIDI file' do
|
85
86
|
MIDI_File(tempfile).write_timeline(
|
86
87
|
Timeline.from_hash({
|
87
|
-
0 =>
|
88
|
-
2 =>
|
88
|
+
0 => [Note(C4,0.5,1), Note(E4,0.5,1)],
|
89
|
+
2.0 => [Note(G4,1,2), Note(B4,1,2), Note(D5,1,2)]
|
89
90
|
})
|
90
91
|
)
|
91
92
|
|
@@ -131,6 +132,40 @@ describe MTK::MIDI::File do
|
|
131
132
|
note_offs[4].time_from_start.should == 1920
|
132
133
|
end
|
133
134
|
end
|
135
|
+
|
136
|
+
it 'ignores rests (events with negative duration)' do
|
137
|
+
MIDI_File(tempfile).write_timeline(
|
138
|
+
Timeline.from_hash({
|
139
|
+
0 => Note.new(C4, 0.7, 1),
|
140
|
+
1 => Note.new(G4, 0.8, -1), # this is a rest because it has a negative duration
|
141
|
+
2 => Note.new(C5, 0.9, 1)
|
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
|
134
169
|
end
|
135
170
|
|
136
171
|
describe "#write_timelines" do
|
@@ -138,7 +173,7 @@ describe MTK::MIDI::File do
|
|
138
173
|
MIDI_File(tempfile).write_timelines([
|
139
174
|
Timeline.from_hash({
|
140
175
|
0 => Note.new(C4, 0.7, 1),
|
141
|
-
1 => Note.new(G4, 0.8, 1),
|
176
|
+
1.0 => Note.new(G4, 0.8, 1),
|
142
177
|
}),
|
143
178
|
Timeline.from_hash({
|
144
179
|
1 => Note.new(C5, 0.9, 2),
|
data/spec/mtk/note_spec.rb
CHANGED
@@ -17,19 +17,25 @@ describe MTK::Note do
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
-
describe "from_hash" do
|
20
|
+
describe ".from_hash" do
|
21
21
|
it "constructs a Note using a hash" do
|
22
22
|
Note.from_hash({ :pitch => C4, :intensity => intensity, :duration => duration }).should == note
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
-
describe 'from_midi' do
|
26
|
+
describe '.from_midi' do
|
27
27
|
it "constructs a Note using a MIDI pitch and velocity" do
|
28
28
|
Note.from_midi(C4.to_i, mf*127, 2.5).should == note
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
describe "
|
32
|
+
describe "#to_midi" do
|
33
|
+
it "converts the Note to an Array of MIDI values: [pitch, velocity, duration]" do
|
34
|
+
note.to_midi.should == [60, (mf*127).round, duration]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "#to_hash" do
|
33
39
|
it "is a hash containing all the attributes of the Note" do
|
34
40
|
note.to_hash.should == { :pitch => pitch, :intensity => intensity, :duration => duration }
|
35
41
|
end
|
@@ -44,6 +50,24 @@ describe MTK::Note do
|
|
44
50
|
end
|
45
51
|
end
|
46
52
|
|
53
|
+
describe "#invert" do
|
54
|
+
context 'higher center pitch' do
|
55
|
+
it 'inverts the pitch around the given center pitch' do
|
56
|
+
note.invert(Pitch 66).should == Note.new(Pitch(72), intensity, duration)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'lower center pitch' do
|
61
|
+
it 'inverts the pitch around the given center pitch' do
|
62
|
+
note.invert(Pitch 54).should == Note.new(Pitch(48), intensity, duration)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
it "returns the an equal note when given it's pitch as an argument" do
|
67
|
+
note.invert(note.pitch).should == note
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
47
71
|
describe "#==" do
|
48
72
|
it "is true when the pitches, intensities, and durations are equal" do
|
49
73
|
note.should == Note.new(pitch, intensity, duration)
|
@@ -63,3 +87,29 @@ describe MTK::Note do
|
|
63
87
|
end
|
64
88
|
|
65
89
|
end
|
90
|
+
|
91
|
+
describe MTK do
|
92
|
+
|
93
|
+
describe '#Note' do
|
94
|
+
|
95
|
+
it "acts like new for multiple arguments" do
|
96
|
+
Note(C4,mf,1).should == Note.new(C4,mf,1)
|
97
|
+
end
|
98
|
+
|
99
|
+
it "acts like new for an Array of arguments by unpacking (splatting) them" do
|
100
|
+
Note([C4,mf,1]).should == Note.new(C4,mf,1)
|
101
|
+
end
|
102
|
+
|
103
|
+
it "returns the argument if it's already a Note" do
|
104
|
+
note = Note.new(C4,mf,1)
|
105
|
+
Note(note).should be_equal note
|
106
|
+
end
|
107
|
+
|
108
|
+
it "raises an error for types it doesn't understand" do
|
109
|
+
lambda{ Note({:not => :compatible}) }.should raise_error
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|