mtk 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. data/.yardopts +9 -0
  2. data/INTRO.md +73 -0
  3. data/LICENSE.txt +27 -0
  4. data/README.md +93 -18
  5. data/Rakefile +13 -1
  6. data/examples/crescendo.rb +20 -0
  7. data/examples/dynamic_pattern.rb +39 -0
  8. data/examples/play_midi.rb +19 -0
  9. data/examples/print_midi.rb +13 -0
  10. data/examples/random_tone_row.rb +18 -0
  11. data/examples/tone_row_melody.rb +21 -0
  12. data/lib/mtk/_constants/durations.rb +80 -0
  13. data/lib/mtk/_constants/intensities.rb +81 -0
  14. data/lib/mtk/{constants → _constants}/intervals.rb +10 -1
  15. data/lib/mtk/_constants/pitch_classes.rb +35 -0
  16. data/lib/mtk/_constants/pitches.rb +49 -0
  17. data/lib/mtk/{numeric_extensions.rb → _numeric_extensions.rb} +0 -0
  18. data/lib/mtk/event.rb +14 -5
  19. data/lib/mtk/helper/collection.rb +114 -0
  20. data/lib/mtk/helper/event_builder.rb +85 -0
  21. data/lib/mtk/{constants → helper}/pseudo_constants.rb +7 -6
  22. data/lib/mtk/lang/grammar.rb +17 -0
  23. data/lib/mtk/lang/mtk_grammar.citrus +60 -0
  24. data/lib/mtk/midi/file.rb +10 -15
  25. data/lib/mtk/midi/jsound_input.rb +68 -0
  26. data/lib/mtk/midi/jsound_output.rb +80 -0
  27. data/lib/mtk/note.rb +22 -3
  28. data/lib/mtk/pattern/abstract_pattern.rb +132 -0
  29. data/lib/mtk/pattern/choice.rb +25 -9
  30. data/lib/mtk/pattern/cycle.rb +51 -0
  31. data/lib/mtk/pattern/enumerator.rb +26 -0
  32. data/lib/mtk/pattern/function.rb +46 -0
  33. data/lib/mtk/pattern/lines.rb +60 -0
  34. data/lib/mtk/pattern/palindrome.rb +42 -0
  35. data/lib/mtk/pattern/sequence.rb +15 -50
  36. data/lib/mtk/pitch.rb +45 -6
  37. data/lib/mtk/pitch_class.rb +36 -35
  38. data/lib/mtk/pitch_class_set.rb +46 -14
  39. data/lib/mtk/pitch_set.rb +20 -31
  40. data/lib/mtk/sequencer/abstract_sequencer.rb +85 -0
  41. data/lib/mtk/sequencer/rhythmic_sequencer.rb +29 -0
  42. data/lib/mtk/sequencer/step_sequencer.rb +26 -0
  43. data/lib/mtk/timeline.rb +75 -22
  44. data/lib/mtk/transform/invertible.rb +15 -0
  45. data/lib/mtk/{util → transform}/mappable.rb +6 -2
  46. data/lib/mtk/transform/set_theory_operations.rb +34 -0
  47. data/lib/mtk/transform/transposable.rb +14 -0
  48. data/lib/mtk.rb +56 -22
  49. data/spec/mtk/_constants/durations_spec.rb +118 -0
  50. data/spec/mtk/{constants/dynamics_spec.rb → _constants/intensities_spec.rb} +48 -17
  51. data/spec/mtk/{constants → _constants}/intervals_spec.rb +21 -0
  52. data/spec/mtk/_constants/pitch_classes_spec.rb +58 -0
  53. data/spec/mtk/_constants/pitches_spec.rb +52 -0
  54. data/spec/mtk/{numeric_extensions_spec.rb → _numeric_extensions_spec.rb} +0 -0
  55. data/spec/mtk/event_spec.rb +19 -0
  56. data/spec/mtk/helper/collection_spec.rb +291 -0
  57. data/spec/mtk/helper/event_builder_spec.rb +92 -0
  58. data/spec/mtk/helper/pseudo_constants_spec.rb +20 -0
  59. data/spec/mtk/lang/grammar_spec.rb +100 -0
  60. data/spec/mtk/midi/file_spec.rb +41 -6
  61. data/spec/mtk/note_spec.rb +53 -3
  62. data/spec/mtk/pattern/abstract_pattern_spec.rb +45 -0
  63. data/spec/mtk/pattern/choice_spec.rb +89 -3
  64. data/spec/mtk/pattern/cycle_spec.rb +133 -0
  65. data/spec/mtk/pattern/function_spec.rb +133 -0
  66. data/spec/mtk/pattern/lines_spec.rb +93 -0
  67. data/spec/mtk/pattern/note_cycle_spec.rb.bak +116 -0
  68. data/spec/mtk/pattern/palindrome_spec.rb +124 -0
  69. data/spec/mtk/pattern/pitch_cycle_spec.rb.bak +47 -0
  70. data/spec/mtk/pattern/pitch_sequence_spec.rb.bak +37 -0
  71. data/spec/mtk/pattern/sequence_spec.rb +128 -31
  72. data/spec/mtk/pitch_class_set_spec.rb +240 -7
  73. data/spec/mtk/pitch_class_spec.rb +84 -18
  74. data/spec/mtk/pitch_set_spec.rb +45 -10
  75. data/spec/mtk/pitch_spec.rb +59 -0
  76. data/spec/mtk/sequencer/abstract_sequencer_spec.rb +159 -0
  77. data/spec/mtk/sequencer/rhythmic_sequencer_spec.rb +49 -0
  78. data/spec/mtk/sequencer/step_sequencer_spec.rb +71 -0
  79. data/spec/mtk/timeline_spec.rb +118 -15
  80. data/spec/spec_helper.rb +4 -3
  81. metadata +59 -22
  82. data/lib/mtk/chord.rb +0 -47
  83. data/lib/mtk/constants/dynamics.rb +0 -56
  84. data/lib/mtk/constants/pitch_classes.rb +0 -18
  85. data/lib/mtk/constants/pitches.rb +0 -24
  86. data/lib/mtk/pattern/note_sequence.rb +0 -60
  87. data/lib/mtk/pattern/pitch_sequence.rb +0 -22
  88. data/lib/mtk/patterns.rb +0 -4
  89. data/spec/mtk/chord_spec.rb +0 -74
  90. data/spec/mtk/constants/pitch_classes_spec.rb +0 -35
  91. data/spec/mtk/constants/pitches_spec.rb +0 -23
  92. data/spec/mtk/pattern/note_sequence_spec.rb +0 -121
  93. 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
@@ -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 Chords in a Timeline to a MIDI file' do
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 => Chord.new([C4, E4], 0.5, 1),
88
- 2 => Chord.new([G4, B4, D5], 1, 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),
@@ -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 "to_hash" do
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
+