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,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