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.
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
+