mtk 0.0.1

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 (45) hide show
  1. data/README.md +52 -0
  2. data/Rakefile +31 -0
  3. data/lib/mtk/chord.rb +47 -0
  4. data/lib/mtk/constants/dynamics.rb +56 -0
  5. data/lib/mtk/constants/intervals.rb +76 -0
  6. data/lib/mtk/constants/pitch_classes.rb +18 -0
  7. data/lib/mtk/constants/pitches.rb +24 -0
  8. data/lib/mtk/constants/pseudo_constants.rb +25 -0
  9. data/lib/mtk/event.rb +61 -0
  10. data/lib/mtk/midi/file.rb +179 -0
  11. data/lib/mtk/note.rb +44 -0
  12. data/lib/mtk/numeric_extensions.rb +61 -0
  13. data/lib/mtk/pattern/choice.rb +21 -0
  14. data/lib/mtk/pattern/note_sequence.rb +60 -0
  15. data/lib/mtk/pattern/pitch_sequence.rb +22 -0
  16. data/lib/mtk/pattern/sequence.rb +65 -0
  17. data/lib/mtk/patterns.rb +4 -0
  18. data/lib/mtk/pitch.rb +112 -0
  19. data/lib/mtk/pitch_class.rb +113 -0
  20. data/lib/mtk/pitch_class_set.rb +106 -0
  21. data/lib/mtk/pitch_set.rb +95 -0
  22. data/lib/mtk/timeline.rb +160 -0
  23. data/lib/mtk/util/mappable.rb +14 -0
  24. data/lib/mtk.rb +36 -0
  25. data/spec/mtk/chord_spec.rb +74 -0
  26. data/spec/mtk/constants/dynamics_spec.rb +94 -0
  27. data/spec/mtk/constants/intervals_spec.rb +140 -0
  28. data/spec/mtk/constants/pitch_classes_spec.rb +35 -0
  29. data/spec/mtk/constants/pitches_spec.rb +23 -0
  30. data/spec/mtk/event_spec.rb +120 -0
  31. data/spec/mtk/midi/file_spec.rb +208 -0
  32. data/spec/mtk/note_spec.rb +65 -0
  33. data/spec/mtk/numeric_extensions_spec.rb +102 -0
  34. data/spec/mtk/pattern/choice_spec.rb +21 -0
  35. data/spec/mtk/pattern/note_sequence_spec.rb +121 -0
  36. data/spec/mtk/pattern/pitch_sequence_spec.rb +47 -0
  37. data/spec/mtk/pattern/sequence_spec.rb +54 -0
  38. data/spec/mtk/pitch_class_set_spec.rb +103 -0
  39. data/spec/mtk/pitch_class_spec.rb +165 -0
  40. data/spec/mtk/pitch_set_spec.rb +163 -0
  41. data/spec/mtk/pitch_spec.rb +217 -0
  42. data/spec/mtk/timeline_spec.rb +234 -0
  43. data/spec/spec_helper.rb +7 -0
  44. data/spec/test.mid +0 -0
  45. metadata +97 -0
@@ -0,0 +1,208 @@
1
+ require 'mtk/midi/file'
2
+ require 'tempfile'
3
+
4
+ describe MTK::MIDI::File do
5
+
6
+ let(:test_mid) { File.join(File.dirname(__FILE__), '..', '..', 'test.mid') }
7
+
8
+ def tempfile
9
+ @tempfile ||= Tempfile.new 'MTK-midi_file_writer_spec'
10
+ end
11
+
12
+ after do
13
+ if @tempfile
14
+ @tempfile.close
15
+ @tempfile.unlink
16
+ end
17
+ end
18
+
19
+ def note_ons_and_offs(track)
20
+ note_ons, note_offs = [], []
21
+ for event in track.events
22
+ note_ons << event if event.is_a? MIDI::NoteOn
23
+ note_offs << event if event.is_a? MIDI::NoteOff
24
+ end
25
+ return note_ons, note_offs
26
+ end
27
+
28
+ describe "#to_timelines" do
29
+ it "converts a single-track MIDI file to an Array containing one Timeline" do
30
+ MIDI_File(test_mid).to_timelines.length.should == 1 # one track
31
+ end
32
+
33
+ it "converts note on/off messages to Note events" do
34
+ MIDI_File(test_mid).to_timelines.first.should == {
35
+ 0.0 => [Note.new(C4, 126/127.0, 0.25)],
36
+ 1.0 => [Note.new(Db4, 99/127.0, 0.5)],
37
+ 2.0 => [Note.new(D4, 72/127.0, 0.75)],
38
+ 3.0 => [Note.new(Eb4, 46/127.0, 1.0), Note.new(E4, 46/127.0, 1.0)]
39
+ }
40
+ end
41
+ end
42
+
43
+ describe "#write_timeline" do
44
+ it 'writes Notes in a Timeline to a MIDI file' do
45
+ MIDI_File(tempfile).write_timeline(
46
+ Timeline.from_hash({
47
+ 0 => Note.new(C4, 0.7, 1),
48
+ 1 => Note.new(G4, 0.8, 1),
49
+ 2 => Note.new(C5, 0.9, 1)
50
+ })
51
+ )
52
+
53
+ # Now let's parse the file and check some expectations
54
+ File.open(tempfile.path, 'rb') do |file|
55
+ seq = MIDI::Sequence.new
56
+ seq.read(file)
57
+ seq.tracks.size.should == 1
58
+
59
+ track = seq.tracks[0]
60
+ note_ons, note_offs = note_ons_and_offs(track)
61
+ note_ons.length.should == 3
62
+ note_offs.length.should == 3
63
+
64
+ note_ons[0].note.should == C4.to_i
65
+ note_ons[0].velocity.should be_within(0.5).of(127*0.7)
66
+ note_offs[0].note.should == C4.to_i
67
+ note_ons[0].time_from_start.should == 0
68
+ note_offs[0].time_from_start.should == 480
69
+
70
+ note_ons[1].note.should == G4.to_i
71
+ note_ons[1].velocity.should be_within(0.5).of(127*0.8)
72
+ note_offs[1].note.should == G4.to_i
73
+ note_ons[1].time_from_start.should == 480
74
+ note_offs[1].time_from_start.should == 960
75
+
76
+ note_ons[2].note.should == C5.to_i
77
+ note_ons[2].velocity.should be_within(0.5).of(127*0.9)
78
+ note_offs[2].note.should == C5.to_i
79
+ note_ons[2].time_from_start.should == 960
80
+ note_offs[2].time_from_start.should == 1440
81
+ end
82
+ end
83
+
84
+ it 'writes Chords in a Timeline to a MIDI file' do
85
+ MIDI_File(tempfile).write_timeline(
86
+ Timeline.from_hash({
87
+ 0 => Chord.new([C4, E4], 0.5, 1),
88
+ 2 => Chord.new([G4, B4, D5], 1, 2)
89
+ })
90
+ )
91
+
92
+ # Now let's parse the file and check some expectations
93
+ File.open(tempfile.path, 'rb') do |file|
94
+ seq = MIDI::Sequence.new
95
+ seq.read(file)
96
+ seq.tracks.size.should == 1
97
+
98
+ track = seq.tracks[0]
99
+ note_ons, note_offs = note_ons_and_offs(track)
100
+ note_ons.length.should == 5
101
+ note_offs.length.should == 5
102
+
103
+ note_ons[0].note.should == C4.to_i
104
+ note_offs[0].note.should == C4.to_i
105
+ note_ons[0].velocity.should == 64
106
+ note_ons[0].time_from_start.should == 0
107
+ note_offs[0].time_from_start.should == 480
108
+
109
+ note_ons[1].note.should == E4.to_i
110
+ note_offs[1].note.should == E4.to_i
111
+ note_ons[1].velocity.should == 64
112
+ note_ons[1].time_from_start.should == 0
113
+ note_offs[1].time_from_start.should == 480
114
+
115
+ note_ons[2].note.should == G4.to_i
116
+ note_offs[2].note.should == G4.to_i
117
+ note_ons[2].velocity.should == 127
118
+ note_ons[2].time_from_start.should == 960
119
+ note_offs[2].time_from_start.should == 1920
120
+
121
+ note_ons[3].note.should == B4.to_i
122
+ note_offs[3].note.should == B4.to_i
123
+ note_ons[3].velocity.should == 127
124
+ note_ons[3].time_from_start.should == 960
125
+ note_offs[3].time_from_start.should == 1920
126
+
127
+ note_ons[4].note.should == D5.to_i
128
+ note_offs[4].note.should == D5.to_i
129
+ note_ons[4].velocity.should == 127
130
+ note_ons[4].time_from_start.should == 960
131
+ note_offs[4].time_from_start.should == 1920
132
+ end
133
+ end
134
+ end
135
+
136
+ describe "#write_timelines" do
137
+ it "writes a multitrack MIDI file" do
138
+ MIDI_File(tempfile).write_timelines([
139
+ Timeline.from_hash({
140
+ 0 => Note.new(C4, 0.7, 1),
141
+ 1 => Note.new(G4, 0.8, 1),
142
+ }),
143
+ Timeline.from_hash({
144
+ 1 => Note.new(C5, 0.9, 2),
145
+ 2 => Note.new(D5, 1, 2),
146
+ }),
147
+ ])
148
+
149
+ # Now let's parse the file and check some expectations
150
+ File.open(tempfile.path, 'rb') do |file|
151
+ seq = MIDI::Sequence.new
152
+ seq.read(file)
153
+ seq.tracks.size.should == 2
154
+
155
+ track = seq.tracks[0]
156
+ note_ons, note_offs = note_ons_and_offs(track)
157
+ note_ons.length.should == 2
158
+ note_offs.length.should == 2
159
+
160
+ note_ons[0].note.should == C4.to_i
161
+ note_ons[0].velocity.should be_within(0.5).of(127*0.7)
162
+ note_offs[0].note.should == C4.to_i
163
+ note_ons[0].time_from_start.should == 0
164
+ note_offs[0].time_from_start.should == 480
165
+
166
+ note_ons[1].note.should == G4.to_i
167
+ note_ons[1].velocity.should be_within(0.5).of(127*0.8)
168
+ note_offs[1].note.should == G4.to_i
169
+ note_ons[1].time_from_start.should == 480
170
+ note_offs[1].time_from_start.should == 960
171
+
172
+ track = seq.tracks[1]
173
+ note_ons, note_offs = note_ons_and_offs(track)
174
+ note_ons.length.should == 2
175
+ note_offs.length.should == 2
176
+
177
+ note_ons[0].note.should == C5.to_i
178
+ note_ons[0].velocity.should be_within(0.5).of(127*0.9)
179
+ note_offs[0].note.should == C5.to_i
180
+ note_ons[0].time_from_start.should == 480
181
+ note_offs[0].time_from_start.should == 1440
182
+
183
+ note_ons[1].note.should == D5.to_i
184
+ note_ons[1].velocity.should == 127
185
+ note_offs[1].note.should == D5.to_i
186
+ note_ons[1].time_from_start.should == 960
187
+ note_offs[1].time_from_start.should == 1920
188
+ end
189
+ end
190
+ end
191
+
192
+ describe "#write" do
193
+ it "calls write_timeline when given a Timeline" do
194
+ midi_file = MIDI_File(nil)
195
+ timeline = Timeline.new
196
+ midi_file.should_receive(:write_timeline).with(timeline)
197
+ midi_file.write(timeline)
198
+ end
199
+
200
+ it "calls write_timelines when given an Array" do
201
+ midi_file = MIDI_File(nil)
202
+ timelines = [Timeline.new]
203
+ midi_file.should_receive(:write_timelines).with(timelines)
204
+ midi_file.write(timelines)
205
+ end
206
+ end
207
+
208
+ end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ describe MTK::Note do
4
+
5
+ let(:pitch) { C4 }
6
+ let(:intensity) { mf }
7
+ let(:duration) { 2.5 }
8
+ let(:note) { Note.new(pitch, intensity, duration) }
9
+
10
+ describe "#pitch" do
11
+ it "is the pitch used to create the Note" do
12
+ note.pitch.should == pitch
13
+ end
14
+
15
+ it "is a read-only attribute" do
16
+ lambda{ note.pitch = D4 }.should raise_error
17
+ end
18
+ end
19
+
20
+ describe "from_hash" do
21
+ it "constructs a Note using a hash" do
22
+ Note.from_hash({ :pitch => C4, :intensity => intensity, :duration => duration }).should == note
23
+ end
24
+ end
25
+
26
+ describe 'from_midi' do
27
+ it "constructs a Note using a MIDI pitch and velocity" do
28
+ Note.from_midi(C4.to_i, mf*127, 2.5).should == note
29
+ end
30
+ end
31
+
32
+ describe "to_hash" do
33
+ it "is a hash containing all the attributes of the Note" do
34
+ note.to_hash.should == { :pitch => pitch, :intensity => intensity, :duration => duration }
35
+ end
36
+ end
37
+
38
+ describe '#transpose' do
39
+ it 'adds the given interval to the @pitch' do
40
+ (note.transpose 2.semitones).should == Note.new(D4, intensity, duration)
41
+ end
42
+ it 'does not affect the immutability of the Note' do
43
+ (note.transpose 2.semitones).should_not == note
44
+ end
45
+ end
46
+
47
+ describe "#==" do
48
+ it "is true when the pitches, intensities, and durations are equal" do
49
+ note.should == Note.new(pitch, intensity, duration)
50
+ end
51
+
52
+ it "is false when the pitches are not equal" do
53
+ note.should_not == Note.new(pitch + 1, intensity, duration)
54
+ end
55
+
56
+ it "is false when the intensities are not equal" do
57
+ note.should_not == Note.new(pitch, intensity * 0.5, duration)
58
+ end
59
+
60
+ it "is false when the durations are not equal" do
61
+ note.should_not == Note.new(pitch, intensity, duration * 2)
62
+ end
63
+ end
64
+
65
+ end
@@ -0,0 +1,102 @@
1
+ require 'spec_helper'
2
+
3
+ describe Numeric do
4
+
5
+ describe '#semitones' do
6
+ it "is the Numeric value" do
7
+ 100.semitones.should == 100
8
+ end
9
+ end
10
+
11
+ describe "#cents" do
12
+ it "is the Numeric / 100.0" do
13
+ 100.cents.should == 1
14
+ end
15
+ end
16
+
17
+ describe "#minor_seconds" do
18
+ it "is the Numeric value" do
19
+ 2.minor_seconds.should == 2
20
+ end
21
+ end
22
+
23
+ describe "#major_seconds" do
24
+ it "is the Numeric * 2" do
25
+ 2.major_seconds.should == 4
26
+ end
27
+ end
28
+
29
+ describe "#minor_thirds" do
30
+ it "is the Numeric * 3" do
31
+ 2.minor_thirds.should == 6
32
+ end
33
+ end
34
+
35
+ describe "#major_thirds" do
36
+ it "is the Numeric * 4" do
37
+ 2.major_thirds.should == 8
38
+ end
39
+ end
40
+
41
+ describe "#perfect_fourths" do
42
+ it "is the Numeric * 5" do
43
+ 2.perfect_fourths.should == 10
44
+ end
45
+ end
46
+
47
+ describe "#tritones" do
48
+ it "is the Numeric * 6" do
49
+ 2.tritones.should == 12
50
+ end
51
+ end
52
+
53
+ describe "#augmented_fourths" do
54
+ it "is the Numeric * 6" do
55
+ 2.augmented_fourths.should == 12
56
+ end
57
+ end
58
+
59
+ describe "#diminshed_fifths" do
60
+ it "is the Numeric * 6" do
61
+ 2.diminshed_fifths.should == 12
62
+ end
63
+ end
64
+
65
+ describe "#perfect_fifths" do
66
+ it "is the Numeric * 7" do
67
+ 2.perfect_fifths.should == 14
68
+ end
69
+ end
70
+
71
+ describe "#minor_sixths" do
72
+ it "is the Numeric * 8" do
73
+ 2.minor_sixths.should == 16
74
+ end
75
+ end
76
+
77
+ describe "#major_sixths" do
78
+ it "is the Numeric * 9" do
79
+ 2.major_sixths.should == 18
80
+ end
81
+ end
82
+
83
+ describe "#minor_sevenths" do
84
+ it "is the Numeric * 10" do
85
+ 2.minor_sevenths.should == 20
86
+ end
87
+ end
88
+
89
+ describe "#major_sevenths" do
90
+ it "is the Numeric * 11" do
91
+ 2.major_sevenths.should == 22
92
+ end
93
+ end
94
+
95
+ describe "#octaves" do
96
+ it "is the Numeric * 12" do
97
+ 2.octaves.should == 24
98
+ end
99
+ end
100
+
101
+ end
102
+
@@ -0,0 +1,21 @@
1
+ require 'spec_helper'
2
+ require 'set'
3
+
4
+ describe MTK::Pattern::Choice do
5
+
6
+ let(:elements) { [1,2,3] }
7
+ let(:choice) { Pattern::Choice.new elements }
8
+
9
+ describe "#next" do
10
+ it "randomly chooses one of the elements" do
11
+ choosen = Set.new
12
+ 100.times do
13
+ element = choice.next
14
+ choosen << element
15
+ elements.should include(element)
16
+ end
17
+ choosen.to_a.sort.should == elements
18
+ end
19
+ end
20
+
21
+ end
@@ -0,0 +1,121 @@
1
+ require 'spec_helper'
2
+
3
+ describe MTK::Pattern::NoteSequence do
4
+
5
+ def note(pitch, intensity=mf, duration=1)
6
+ Note.new pitch,intensity,duration
7
+ end
8
+
9
+ def chord(pitches, intensity=mf, duration=1)
10
+ Chord.new pitches,intensity,duration
11
+ end
12
+
13
+ describe "#new" do
14
+ it "allows default pitch to be specified"
15
+ it "allows default intensity to be specified"
16
+ it "allows default duration to be specified"
17
+ end
18
+
19
+ describe "#next" do
20
+ it "iterates through the pitch, intensity, and duration list in parallel to emit Notes" do
21
+ sequence = Pattern::NoteSequence.new [C4, D4, E4], [p, f], [1,2,3,4]
22
+ sequence.next.should == Note.new(C4, p, 1)
23
+ sequence.next.should == Note.new(D4, f, 2)
24
+ sequence.next.should == Note.new(E4, p, 3)
25
+ sequence.next.should == Note.new(C4, f, 4)
26
+ sequence.next.should == Note.new(D4, p, 1)
27
+ sequence.next.should == Note.new(E4, f, 2)
28
+ end
29
+
30
+ it "defaults to Pitch 'C4' when no pitches are given" do
31
+ sequence = Pattern::NoteSequence.new [], [p,f], [1,2,3]
32
+ sequence.next.should == Note.new(C4, p, 1)
33
+ sequence.next.should == Note.new(C4, f, 2)
34
+ sequence.next.should == Note.new(C4, p, 3)
35
+ end
36
+
37
+ it "defaults to intensity 'mf' when no intensities are given" do
38
+ sequence = Pattern::NoteSequence.new [C4, D4, E4], nil, [2]
39
+ sequence.next.should == Note.new(C4, mf, 2)
40
+ sequence.next.should == Note.new(D4, mf, 2)
41
+ sequence.next.should == Note.new(E4, mf, 2)
42
+ end
43
+
44
+ it "defaults to duration 1 when no durations are given" do
45
+ sequence = Pattern::NoteSequence.new [C4, D4, E4], [p, f]
46
+ sequence.next.should == Note.new(C4, p, 1)
47
+ sequence.next.should == Note.new(D4, f, 1)
48
+ sequence.next.should == Note.new(E4, p, 1)
49
+ end
50
+
51
+ it "uses the previous pitch/intensity/duration when it encounters a nil value" do
52
+ sequence = Pattern::NoteSequence.new [C4, D4, E4, F4, nil], [mp, mf, f, nil], [1, 2, nil]
53
+ sequence.next.should == Note.new(C4, mp, 1)
54
+ sequence.next.should == Note.new(D4, mf, 2)
55
+ sequence.next.should == Note.new(E4, f, 2)
56
+ sequence.next.should == Note.new(F4, f, 1)
57
+ sequence.next.should == Note.new(F4, mp, 2)
58
+ sequence.next.should == Note.new(C4, mf, 2)
59
+ end
60
+
61
+ it "adds Numeric intervals in the pitch list to the previous pitch" do
62
+ sequence = Pattern::NoteSequence.new [C4, 1, 2, 3]
63
+ sequence.next.should == note(C4)
64
+ sequence.next.should == note(C4+1)
65
+ sequence.next.should == note(C4+1+2)
66
+ sequence.next.should == note(C4+1+2+3)
67
+ sequence.next.should == note(C4)
68
+ end
69
+
70
+ it "goes to the nearest Pitch for any PitchClasses in the pitch list" do
71
+ sequence = Pattern::NoteSequence.new [C4, F, C, G, C]
72
+ sequence.next.should == note(C4)
73
+ sequence.next.should == note(F4)
74
+ sequence.next.should == note(C4)
75
+ sequence.next.should == note(G3)
76
+ sequence.next.should == note(C4)
77
+ end
78
+
79
+ it "does not endlessly ascend or descend when alternating between two pitch classes a tritone apart" do
80
+ sequence = Pattern::NoteSequence.new [C4, Gb, C, Gb, C]
81
+ sequence.next.should == note(C4)
82
+ sequence.next.should == note(Gb4)
83
+ sequence.next.should == note(C4)
84
+ sequence.next.should == note(Gb4)
85
+ sequence.next.should == note(C4)
86
+ end
87
+
88
+ it "sequences Chords for pitch list items that are PitchSets" do
89
+ sequence = Pattern::NoteSequence.new [PitchSet.new([C4, E4, G4]), C4, PitchSet.new([D4, F4, A4])]
90
+ sequence.next.should == chord([C4, E4, G4])
91
+ sequence.next.should == note(C4)
92
+ sequence.next.should == chord([D4, F4, A4])
93
+ end
94
+
95
+ it "adds numeric intervals to PitchSets" do
96
+ sequence = Pattern::NoteSequence.new [PitchSet.new([C4, E4, G4]), 2]
97
+ sequence.next.should == chord([C4, E4, G4])
98
+ sequence.next.should == chord([D4, Gb4, A4])
99
+ end
100
+
101
+ it "goes to the nearest Pitch relative to the lowest note in the PitchSet for any PitchClasses in the pitch list" do
102
+ sequence = Pattern::NoteSequence.new [PitchSet.new([C4, E4, G4]), F, D, Bb]
103
+ sequence.next.should == chord([C4, E4, G4])
104
+ sequence.next.should == chord([F4, A4, C5])
105
+ sequence.next.should == chord([D4, Gb4, A4])
106
+ sequence.next.should == chord([Bb3, D4, F4])
107
+ end
108
+ end
109
+
110
+ describe "#reset" do
111
+ it "resets the sequence to the beginning" do
112
+ sequence = Pattern::NoteSequence.new [C4, D4, E4], [p, f], [1,2,3,4]
113
+ sequence.next.should == Note.new(C4, p, 1)
114
+ sequence.next.should == Note.new(D4, f, 2)
115
+ sequence.reset
116
+ sequence.next.should == Note.new(C4, p, 1)
117
+ sequence.next.should == Note.new(D4, f, 2)
118
+ end
119
+ end
120
+
121
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ describe MTK::Pattern::PitchSequence do
4
+
5
+ describe "#next" do
6
+ it "enumerates Pitches" do
7
+ sequence = Pattern::PitchSequence.new [C4, D4, E4]
8
+ sequence.next.should == C4
9
+ sequence.next.should == D4
10
+ sequence.next.should == E4
11
+ end
12
+
13
+ it "adds Numeric elements (intervals) to the previous pitch" do
14
+ sequence = Pattern::PitchSequence.new [C4, 1, 2, 3]
15
+ sequence.next.should == C4
16
+ sequence.next.should == C4+1
17
+ sequence.next.should == C4+1+2
18
+ sequence.next.should == C4+1+2+3
19
+ end
20
+
21
+ it "returns a Pitch when encountering a Pitch after another type" do
22
+ sequence = Pattern::PitchSequence.new [C4, 1, C4]
23
+ sequence.next
24
+ sequence.next
25
+ sequence.next.should == C4
26
+ end
27
+
28
+ it "goes to the nearest Pitch for any PitchClasses in the pitch list" do
29
+ sequence = Pattern::PitchSequence.new [C4, F, C, G, C]
30
+ sequence.next.should == C4
31
+ sequence.next.should == F4
32
+ sequence.next.should == C4
33
+ sequence.next.should == G3
34
+ sequence.next.should == C4
35
+ end
36
+
37
+ it "does not endlessly ascend or descend when alternating between two pitch classes a tritone apart" do
38
+ sequencer = Pattern::PitchSequence.new [C4, Gb, C, Gb, C]
39
+ sequencer.next.should == C4
40
+ sequencer.next.should == Gb4
41
+ sequencer.next.should == C4
42
+ sequencer.next.should == Gb4
43
+ sequencer.next.should == C4
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,54 @@
1
+ require 'spec_helper'
2
+
3
+ describe MTK::Pattern::Sequence do
4
+
5
+ let(:elements) { [1, 2, 3] }
6
+ let(:sequence) { Pattern::Sequence.new elements }
7
+
8
+ describe "#next" do
9
+ it "iterates through the list of elements and emits them one at a time" do
10
+ sequence.next.should == elements[0]
11
+ sequence.next.should == elements[1]
12
+ sequence.next.should == elements[2]
13
+ end
14
+
15
+ it "starts at the beginning of the list of elements after the end of the list is reached" do
16
+ elements.length.times do
17
+ sequence.next
18
+ end
19
+ sequence.next.should == elements.first
20
+ end
21
+
22
+ it "evaluates any lambdas in the elements list, passing in the previous return value" do
23
+ sequence = Pattern::Sequence.new [1, lambda { |prev_val| prev_val*10 }]
24
+ sequence.next
25
+ sequence.next.should == 10
26
+ end
27
+
28
+ it "does not require the lambda to have any parameters" do
29
+ sequence = Pattern::Sequence.new [lambda { 42 }]
30
+ sequence.next.should == 42
31
+ end
32
+
33
+ it "passed the previous item (which is not necessarily == the previous return value) as the second arg to lambdas that take 2 parameters" do
34
+ arg1, arg2 = nil, nil
35
+ return_5 = lambda { 5 }
36
+ sequence = Pattern::Sequence.new [return_5, lambda { |a1, a2| arg1, arg2=a1, a2 }]
37
+ sequence.next
38
+ sequence.next
39
+ arg1.should == 5
40
+ arg2.should == return_5
41
+ end
42
+ end
43
+
44
+ describe "#reset" do
45
+ it "restarts the sequence" do
46
+ (elements.length - 1).times do
47
+ sequence.next
48
+ end
49
+ sequence.reset
50
+ sequence.next.should == elements.first
51
+ end
52
+ end
53
+
54
+ end