mtk 0.0.1 → 0.0.2
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 +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
|
+
|