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,343 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MTK::Core::PitchClass do
|
4
|
+
|
5
|
+
let(:enharmonic_spellings_grouped_by_value) {
|
6
|
+
# not sure how to test this without re-coding these names here
|
7
|
+
[
|
8
|
+
%w( B# C Dbb ),
|
9
|
+
%w( B## C# Db ),
|
10
|
+
%w( C## D Ebb ),
|
11
|
+
%w( D# Eb Fbb ),
|
12
|
+
%w( D## E Fb ),
|
13
|
+
%w( E# F Gbb ),
|
14
|
+
%w( E## F# Gb ),
|
15
|
+
%w( F## G Abb ),
|
16
|
+
%w( G# Ab ),
|
17
|
+
%w( G## A Bbb ),
|
18
|
+
%w( A# Bb Cbb ),
|
19
|
+
%w( A## B Cb )
|
20
|
+
]
|
21
|
+
}
|
22
|
+
let(:enharmonic_spellings) { enharmonic_spellings_grouped_by_value.flatten }
|
23
|
+
|
24
|
+
|
25
|
+
describe 'NAMES' do
|
26
|
+
it "is the 12 note names in western chromatic scale" do
|
27
|
+
PitchClass::NAMES.should =~ %w( C Db D Eb E F Gb G Ab A Bb B )
|
28
|
+
end
|
29
|
+
|
30
|
+
it "is immutable" do
|
31
|
+
lambda{ PitchClass::NAMES << 'H' }.should raise_error
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
describe 'VALID_NAMES_BY_VALUE' do
|
37
|
+
it 'contains enharmonic spellings of NAMES' do
|
38
|
+
PitchClass::VALID_NAMES_BY_VALUE.should =~ enharmonic_spellings_grouped_by_value
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'organized such that the index of each equivalent spelling matches the index in NAMES' do
|
42
|
+
PitchClass::NAMES.each.with_index do |name,index|
|
43
|
+
PitchClass::VALID_NAMES_BY_VALUE[index].should include(name)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'is immutable' do
|
48
|
+
lambda{ PitchClass::VALID_NAMES_BY_VALUE << 'H' }.should raise_error
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
describe 'VALID_NAMES' do
|
54
|
+
it 'is all enharmonic spellings of NAMES including sharps, flats, double-sharps, and double-flats' do
|
55
|
+
PitchClass::VALID_NAMES.should =~ enharmonic_spellings.flatten
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'is immutable' do
|
59
|
+
lambda{ PitchClass::VALID_NAMES << 'H' }.should raise_error
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
describe 'PITCH_CLASSES' do
|
65
|
+
it 'is the 12 pitch classes in the chromatic scale, indexed by value' do
|
66
|
+
for pc in PitchClass::PITCH_CLASSES
|
67
|
+
pc.should == PitchClass::PITCH_CLASSES[pc.value]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'is immutable' do
|
72
|
+
lambda{ PitchClass::PITCH_CLASSES << C }.should raise_error
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
describe '.new' do
|
78
|
+
it "is private" do
|
79
|
+
lambda{ PitchClass.new('C',0) }.should raise_error
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
describe '.[]' do
|
85
|
+
context "the argument is a valid name" do
|
86
|
+
it "returns a PitchClass" do
|
87
|
+
enharmonic_spellings.each { |name| PitchClass[name].should be_a PitchClass }
|
88
|
+
end
|
89
|
+
it "returns an object with that name" do
|
90
|
+
enharmonic_spellings.each { |name| PitchClass[name].name.should == name }
|
91
|
+
end
|
92
|
+
it "ignores case" do
|
93
|
+
for name in enharmonic_spellings
|
94
|
+
PitchClass[name.upcase].name.should == name
|
95
|
+
PitchClass[name.downcase].name.should == name
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
context "the argument is not a valid name" do
|
100
|
+
it "raises a NameError if the name doesn't exist" do
|
101
|
+
lambda{ PitchClass['z'] }.should raise_error ArgumentError
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
describe '.from_s' do
|
108
|
+
it "returns a PitchClass for any valid name" do
|
109
|
+
for name in enharmonic_spellings
|
110
|
+
pc = PitchClass.from_s(name)
|
111
|
+
pc.should be_a PitchClass
|
112
|
+
pc.name.should == name
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
it "does case-insensitive matching" do
|
117
|
+
for name in enharmonic_spellings
|
118
|
+
pc = PitchClass.from_s(name.downcase)
|
119
|
+
pc.should be_a PitchClass
|
120
|
+
pc.name.should == name
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
it "raises a ArgumentError for invalid arguments" do
|
125
|
+
lambda{ PitchClass.from_s('H') }.should raise_error ArgumentError
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
describe '.from_name' do
|
131
|
+
it "acts like .from_s" do
|
132
|
+
for name in enharmonic_spellings
|
133
|
+
PitchClass.from_name(name).should == PitchClass.from_s(name)
|
134
|
+
end
|
135
|
+
lambda{ PitchClass.from_name('H') }.should raise_error ArgumentError
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
describe '.from_i' do
|
141
|
+
it "returns the PitchClass with that value" do
|
142
|
+
PitchClass.from_i(2).should == D
|
143
|
+
end
|
144
|
+
|
145
|
+
it "returns the PitchClass with that value mod 12" do
|
146
|
+
PitchClass.from_i(14).should == D
|
147
|
+
PitchClass.from_i(-8).should == E
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
describe '.from_value' do
|
153
|
+
it 'acts like .from_i' do
|
154
|
+
(0..11).each{|v| PitchClass.from_value(v).should == PitchClass.from_i(v) }
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
describe '#name' do
|
159
|
+
it "is the name of the pitch class" do
|
160
|
+
C.name.should == 'C'
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
describe '#value' do
|
166
|
+
it "is the value of the pitch class" do
|
167
|
+
PitchClass::NAMES.each.with_index do |name,value|
|
168
|
+
PitchClass[name].value.should == value
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
|
174
|
+
describe '#to_i' do
|
175
|
+
it "is the integer value of the pitch class" do
|
176
|
+
PitchClass::NAMES.each.with_index do |name,value|
|
177
|
+
PitchClass[name].to_i.should == value
|
178
|
+
PitchClass[name].to_i.should be_an Integer
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
describe '#to_f' do
|
185
|
+
it "is the floating point value of the pitch class" do
|
186
|
+
PitchClass::NAMES.each.with_index do |name,value|
|
187
|
+
PitchClass[name].to_f.should == value
|
188
|
+
PitchClass[name].to_f.should be_a Float
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
describe '#to_s' do
|
195
|
+
it "returns the name" do
|
196
|
+
C.to_s.should == C.name
|
197
|
+
for name in enharmonic_spellings
|
198
|
+
PitchClass.from_s(name).to_s.should == name
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
|
204
|
+
describe '#==' do
|
205
|
+
it "checks for equality" do
|
206
|
+
C.should == C
|
207
|
+
C.should_not == D
|
208
|
+
end
|
209
|
+
it "treats enharmonic names as equal" do
|
210
|
+
C.should == PitchClass('B#')
|
211
|
+
C.should == PitchClass('Dbb')
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
|
216
|
+
describe "#<=>" do
|
217
|
+
it "compares the underlying int value" do
|
218
|
+
(C <=> D).should < 0
|
219
|
+
(B <=> C).should > 0
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
|
224
|
+
describe '#+' do
|
225
|
+
it "adds an integer argument to the pitch class's value" do
|
226
|
+
(C + 4).should == E
|
227
|
+
end
|
228
|
+
|
229
|
+
it "'wraps around' the range 0-11" do
|
230
|
+
(D + 10).should == C
|
231
|
+
end
|
232
|
+
|
233
|
+
it "adds a floating point argument to the pitch class's value, and rounds to the nearest pitch class" do
|
234
|
+
(C + 0.49).should == C
|
235
|
+
(C + 0.50).should == Db
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
|
240
|
+
describe "#transpose" do
|
241
|
+
it "behaves like #+" do
|
242
|
+
C.transpose(2).should == C + 2
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
|
247
|
+
describe '#-' do
|
248
|
+
it "subtracts an integer argument from the pitch class's value" do
|
249
|
+
(E - 2).should == D
|
250
|
+
end
|
251
|
+
|
252
|
+
it "'wraps around' the range 0-11" do
|
253
|
+
(C - 8).should == E
|
254
|
+
end
|
255
|
+
|
256
|
+
it "subtracts a floating point argument from the pitch class's value, and rounds to the nearest pitch class'" do
|
257
|
+
(E - 0.50).should == E
|
258
|
+
(E - 0.51).should == Eb
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
|
263
|
+
describe "#invert" do
|
264
|
+
it 'inverts the pitch class around the given center pitch class' do
|
265
|
+
E.invert(D).should == C
|
266
|
+
end
|
267
|
+
|
268
|
+
it 'wraps around octaves as needed (always returns a valid pitch class)' do
|
269
|
+
E.invert(B).should == Gb
|
270
|
+
end
|
271
|
+
|
272
|
+
it "returns the pitch class when the argument is the same pitch class" do
|
273
|
+
E.invert(E).should == E
|
274
|
+
end
|
275
|
+
|
276
|
+
it 'can invert pitch classes around the halfway point between 2 pitch classes' do
|
277
|
+
C.invert(0.5).should == Db
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
|
282
|
+
describe "#distance_to" do
|
283
|
+
it "is the distance in semitones between 2 PitchClass objects" do
|
284
|
+
C.distance_to(D).should == 2
|
285
|
+
end
|
286
|
+
|
287
|
+
it "is the shortest distance (accounts from octave 'wrap around')" do
|
288
|
+
B.distance_to(C).should == 1
|
289
|
+
end
|
290
|
+
|
291
|
+
it "is a negative distance in semitones when the cloest given PitchClass is at a higher Pitch" do
|
292
|
+
D.distance_to(C).should == -2
|
293
|
+
C.distance_to(B).should == -1
|
294
|
+
end
|
295
|
+
|
296
|
+
it "is (positive) 6 for tritone distances, when this PitchClass is C-F" do
|
297
|
+
for pc in [C,Db,D,Eb,E,F]
|
298
|
+
pc.distance_to(pc+TT).should == 6
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
it "is -6 for tritone distances, when this PitchClass is Gb-B" do
|
303
|
+
for pc in [Gb,G,Ab,A,Bb,B]
|
304
|
+
pc.distance_to(pc+TT).should == -6
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
end
|
310
|
+
|
311
|
+
describe MTK do
|
312
|
+
|
313
|
+
describe '#PitchClass' do
|
314
|
+
it "acts like from_s if the argument is a String" do
|
315
|
+
PitchClass('D').should == PitchClass.from_s('D')
|
316
|
+
end
|
317
|
+
|
318
|
+
it "acts like from_s if the argument is a Symbol" do
|
319
|
+
PitchClass(:D).should == PitchClass.from_s('D')
|
320
|
+
end
|
321
|
+
|
322
|
+
it "acts like from_i if the argument is a Numeric" do
|
323
|
+
PitchClass(3).should == PitchClass.from_i(3)
|
324
|
+
end
|
325
|
+
|
326
|
+
it "returns the argument if it's already a PitchClass" do
|
327
|
+
PitchClass(C).should be_equal C
|
328
|
+
end
|
329
|
+
|
330
|
+
it "raises an error for Strings it doesn't understand" do
|
331
|
+
lambda{ PitchClass('H') }.should raise_error ArgumentError
|
332
|
+
end
|
333
|
+
|
334
|
+
it "raises an error for Symbols it doesn't understand" do
|
335
|
+
lambda{ PitchClass(:H) }.should raise_error ArgumentError
|
336
|
+
end
|
337
|
+
|
338
|
+
it "raises an error for types it doesn't understand" do
|
339
|
+
lambda{ PitchClass({:not => :compatible}) }.should raise_error ArgumentError
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
end
|
@@ -0,0 +1,297 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MTK::Core::Pitch do
|
4
|
+
|
5
|
+
let(:middle_c) { Pitch.new(C, 4) }
|
6
|
+
let(:lowest) { Pitch.new(C, -1) }
|
7
|
+
let(:highest) { Pitch.new(G, 9) }
|
8
|
+
let(:middle_c_and_50_cents) { Pitch.new(C, 4, 0.5) }
|
9
|
+
|
10
|
+
describe '.[]' do
|
11
|
+
it "constructs and caches a pitch with the given pitch_class and octave" do
|
12
|
+
Pitch[C,4].should be_equal Pitch[C,4]
|
13
|
+
end
|
14
|
+
|
15
|
+
it "retains the new() method's ability to construct uncached objects" do
|
16
|
+
Pitch.new(C,4).should_not be_equal Pitch[C,4]
|
17
|
+
end
|
18
|
+
|
19
|
+
it "can handle any type for the first argument that's supported by MTK::Core::PitchClass()" do
|
20
|
+
Pitch['C',4].should == Pitch[0, 4]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#pitch_class' do
|
25
|
+
it "is the pitch class of the pitch" do
|
26
|
+
middle_c.pitch_class.should == C
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#octave' do
|
31
|
+
it "is the octave of the pitch" do
|
32
|
+
middle_c.octave.should == 4
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#offset' do
|
37
|
+
it 'is the third argument of the constructor' do
|
38
|
+
Pitch.new(C, 4, 0.6).offset.should == 0.6
|
39
|
+
end
|
40
|
+
it 'defaults to 0' do
|
41
|
+
Pitch.new(C, 4).offset.should == 0
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe '#offset_in_cents' do
|
46
|
+
it 'is #offset * 100' do
|
47
|
+
middle_c_and_50_cents.offset_in_cents.should == middle_c_and_50_cents.offset * 100
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe '.from_i' do
|
52
|
+
it("converts 60 to middle C") { Pitch.from_i(60).should == middle_c }
|
53
|
+
it("converts 0 to C at octave -1") { Pitch.from_i(0).should == lowest }
|
54
|
+
it("converts 127 to G at octave 9") { Pitch.from_i(127).should == highest }
|
55
|
+
end
|
56
|
+
|
57
|
+
describe '.from_f' do
|
58
|
+
it "converts 60.5 to middle C with a 0.5 offset" do
|
59
|
+
p = Pitch.from_f(60.5)
|
60
|
+
p.pitch_class.should == C
|
61
|
+
p.octave.should == 4
|
62
|
+
p.offset.should == 0.5
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '.from_s' do
|
67
|
+
it("converts 'C4' to middle c") { Pitch.from_s('C4').should == middle_c }
|
68
|
+
it("converts 'c4' to middle c") { Pitch.from_s('c4').should == middle_c }
|
69
|
+
it("converts 'B#4' to middle c") { Pitch.from_s('B#4').should == middle_c }
|
70
|
+
it("converts 'C-1' to a low c, 5 octaves below middle C") { Pitch.from_s('C-1').should == middle_c - 60 }
|
71
|
+
it("converts 'C4+50.0cents' to middle C and 50 cents") { Pitch.from_s('C4+50.0cents').should == middle_c_and_50_cents }
|
72
|
+
|
73
|
+
it "raises an ArgumentError for invalid arguments" do
|
74
|
+
lambda{ Pitch.from_s('H4') }.should raise_error ArgumentError
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe '.from_name' do
|
79
|
+
it "acts like .from_s" do
|
80
|
+
for name in ['C4', 'c4', 'B#4', 'C-1', 'C4+50.0cents']
|
81
|
+
Pitch.from_name(name).should == Pitch.from_s(name)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe ".from_h" do
|
87
|
+
it "constructs a Pitch from a hash of pitch attributes" do
|
88
|
+
Pitch.from_h({:pitch_class => C, :octave => 4, :offset => 0.5}).should == middle_c_and_50_cents
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe '#to_f' do
|
93
|
+
it "is 60.5 for middle C with a 0.5 offset" do
|
94
|
+
middle_c_and_50_cents.to_f.should == 60.5
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
describe '#to_i' do
|
99
|
+
it("is 60 for middle C") { middle_c.to_i.should == 60 }
|
100
|
+
it("is 0 for the C at octave -1") { lowest.to_i.should == 0 }
|
101
|
+
it("is 127 for the G at octave 9") { highest.to_i.should == 127 }
|
102
|
+
it "rounds to the nearest integer (the nearest semitone value) when there is an offset" do
|
103
|
+
Pitch.new(C, 4, 0.4).to_i.should == 60
|
104
|
+
Pitch.new(C, 4, 0.5).to_i.should == 61
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
describe "#to_h" do
|
109
|
+
it "converts to a Hash" do
|
110
|
+
middle_c_and_50_cents.to_h.should == {:pitch_class => C, :octave => 4, :offset => 0.5}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe '#==' do
|
115
|
+
it "compares the pitch_class and octave for equality" do
|
116
|
+
middle_c.should == Pitch.from_s('C4')
|
117
|
+
middle_c.should_not == Pitch.from_s('C3')
|
118
|
+
middle_c.should_not == Pitch.from_s('G4')
|
119
|
+
middle_c.should_not == Pitch.from_s('G3')
|
120
|
+
highest.should == Pitch.from_s('G9')
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
describe "#<=>" do
|
125
|
+
it "orders pitches based on their underlying float value" do
|
126
|
+
( Pitch.from_f(60) <=> Pitch.from_f(60.5) ).should < 0
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe '#to_s' do
|
131
|
+
it "should be the pitch class name and the octave" do
|
132
|
+
for pitch in [middle_c, lowest, highest]
|
133
|
+
pitch.to_s.should == pitch.pitch_class.name + pitch.octave.to_s
|
134
|
+
end
|
135
|
+
end
|
136
|
+
it "should include the offset_in_cents when the offset is not 0" do
|
137
|
+
middle_c_and_50_cents.to_s.should == "C4+50cents"
|
138
|
+
end
|
139
|
+
it "rounds to the nearest cent" do
|
140
|
+
Pitch.from_f(60.556).to_s.should == "C4+56cents"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
describe '#inspect' do
|
145
|
+
it 'is "#<MTK::Core::Pitch:{object_id} @value={value}>"' do
|
146
|
+
for value in [0, 60, 60.5, 127]
|
147
|
+
pitch = Pitch.from_f(value)
|
148
|
+
pitch.inspect.should == "#<MTK::Core::Pitch:#{pitch.object_id} @value=#{value}>"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe '#+' do
|
154
|
+
it 'adds the integer value of the argument and #to_i' do
|
155
|
+
(middle_c + 2).should == Pitch.from_i(62)
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'handles offsets' do
|
159
|
+
(middle_c + Pitch.from_f(0.5)).should == Pitch.from_f(60.5)
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'returns a new pitch (Pitch is immutabile)' do
|
163
|
+
original = Pitch.from_i(60)
|
164
|
+
modified = original + 2
|
165
|
+
original.should_not == modified
|
166
|
+
original.should == Pitch.from_i(60)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
describe "#transpose" do
|
171
|
+
it "behaves like #+" do
|
172
|
+
middle_c.transpose(2).should == middle_c + 2
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
describe '#-' do
|
177
|
+
it 'subtracts the integer value of the argument from #to_i' do
|
178
|
+
(middle_c - 2).should == Pitch.from_i(58)
|
179
|
+
end
|
180
|
+
|
181
|
+
it 'handles offsets' do
|
182
|
+
(middle_c - Pitch.from_f(0.5)).should == Pitch.from_f(59.5)
|
183
|
+
end
|
184
|
+
|
185
|
+
it 'returns a new pitch (Pitch is immutabile)' do
|
186
|
+
original = Pitch.from_i(60)
|
187
|
+
modified = original - 2
|
188
|
+
original.should_not == modified
|
189
|
+
original.should == Pitch.from_i(60)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
describe "#invert" do
|
194
|
+
context 'higher center pitch' do
|
195
|
+
it 'inverts the pitch around the given center pitch' do
|
196
|
+
middle_c.invert(Pitch.from_i 66).should == Pitch.from_i(72)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
context 'lower center pitch' do
|
201
|
+
it 'inverts the pitch around the given center pitch' do
|
202
|
+
middle_c.invert(Pitch.from_i 54).should == Pitch.from_i(48)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
it "returns an equal pitch when given itself as an argument" do
|
207
|
+
middle_c.invert(middle_c).should == middle_c
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
describe "#nearest" do
|
212
|
+
it "is the Pitch with the nearest given PitchClass" do
|
213
|
+
middle_c.nearest(F).should == F4
|
214
|
+
middle_c.nearest(G).should == G3
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
describe '#coerce' do
|
219
|
+
it 'allows a Pitch to be added to a Numeric' do
|
220
|
+
(2 + middle_c).should == Pitch.from_i(62)
|
221
|
+
end
|
222
|
+
|
223
|
+
it 'allows a Pitch to be subtracted from a Numeric' do
|
224
|
+
(62 - middle_c).should == Pitch.from_i(2)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
describe "#clone_with" do
|
229
|
+
it "clones the Pitch when given an empty hash" do
|
230
|
+
middle_c.clone_with({}).should == middle_c
|
231
|
+
end
|
232
|
+
|
233
|
+
it "create a Pitch with the given :pitch_class, and the current Pitch's octave and offset if not provided" do
|
234
|
+
pitch2 = middle_c_and_50_cents.clone_with({:pitch_class => middle_c_and_50_cents.pitch_class+1})
|
235
|
+
pitch2.pitch_class.should == middle_c_and_50_cents.pitch_class + 1
|
236
|
+
pitch2.octave.should == middle_c_and_50_cents.octave
|
237
|
+
pitch2.offset.should == middle_c_and_50_cents.offset
|
238
|
+
end
|
239
|
+
|
240
|
+
it "create a Pitch with the given :octave, and the current Pitch's pitch_class and offset if not provided" do
|
241
|
+
pitch2 = middle_c_and_50_cents.clone_with({:octave => middle_c_and_50_cents.octave+1})
|
242
|
+
pitch2.pitch_class.should == middle_c_and_50_cents.pitch_class
|
243
|
+
pitch2.octave.should == middle_c_and_50_cents.octave + 1
|
244
|
+
pitch2.offset.should == middle_c_and_50_cents.offset
|
245
|
+
end
|
246
|
+
|
247
|
+
it "create a Pitch with the given :offset, and the current Pitch's pitch_class and octave if not provided" do
|
248
|
+
pitch2 = middle_c_and_50_cents.clone_with({:offset => middle_c_and_50_cents.offset+1})
|
249
|
+
pitch2.pitch_class.should == middle_c_and_50_cents.pitch_class
|
250
|
+
pitch2.octave.should == middle_c_and_50_cents.octave
|
251
|
+
pitch2.offset.should == middle_c_and_50_cents.offset + 1
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
end
|
256
|
+
|
257
|
+
describe MTK do
|
258
|
+
|
259
|
+
describe '#Pitch' do
|
260
|
+
it "acts like from_s if the argument is a String" do
|
261
|
+
Pitch('D4').should == Pitch.from_s('D4')
|
262
|
+
end
|
263
|
+
|
264
|
+
it "acts like from_s if the argument is a Symbol" do
|
265
|
+
Pitch(:D4).should == Pitch.from_s(:D4)
|
266
|
+
end
|
267
|
+
|
268
|
+
it "acts like from_f if the argument is a Numberic" do
|
269
|
+
Pitch(3).should == Pitch.from_f(3)
|
270
|
+
end
|
271
|
+
|
272
|
+
it "returns the argument if it's already a PitchClass" do
|
273
|
+
Pitch(C4).should be_equal C4
|
274
|
+
end
|
275
|
+
|
276
|
+
it "acts like Pitch[] for a 2-element Array" do
|
277
|
+
Pitch(C,4).should == Pitch[C,4]
|
278
|
+
end
|
279
|
+
|
280
|
+
it "acts like Pitch.new() for a 3-element Array" do
|
281
|
+
Pitch(C, 4, 0.5).should == Pitch.new(C, 4, 0.5)
|
282
|
+
end
|
283
|
+
|
284
|
+
it "raises an error for Strings it doesn't understand" do
|
285
|
+
lambda{ Pitch('H4') }.should raise_error
|
286
|
+
end
|
287
|
+
|
288
|
+
it "raises an error for Symbols it doesn't understand" do
|
289
|
+
lambda{ Pitch(:H4) }.should raise_error
|
290
|
+
end
|
291
|
+
|
292
|
+
it "raises an error for types it doesn't understand" do
|
293
|
+
lambda{ Pitch({:not => :compatible}) }.should raise_error
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
end
|