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.
- data/README.md +52 -0
- data/Rakefile +31 -0
- data/lib/mtk/chord.rb +47 -0
- data/lib/mtk/constants/dynamics.rb +56 -0
- data/lib/mtk/constants/intervals.rb +76 -0
- data/lib/mtk/constants/pitch_classes.rb +18 -0
- data/lib/mtk/constants/pitches.rb +24 -0
- data/lib/mtk/constants/pseudo_constants.rb +25 -0
- data/lib/mtk/event.rb +61 -0
- data/lib/mtk/midi/file.rb +179 -0
- data/lib/mtk/note.rb +44 -0
- data/lib/mtk/numeric_extensions.rb +61 -0
- data/lib/mtk/pattern/choice.rb +21 -0
- data/lib/mtk/pattern/note_sequence.rb +60 -0
- data/lib/mtk/pattern/pitch_sequence.rb +22 -0
- data/lib/mtk/pattern/sequence.rb +65 -0
- data/lib/mtk/patterns.rb +4 -0
- data/lib/mtk/pitch.rb +112 -0
- data/lib/mtk/pitch_class.rb +113 -0
- data/lib/mtk/pitch_class_set.rb +106 -0
- data/lib/mtk/pitch_set.rb +95 -0
- data/lib/mtk/timeline.rb +160 -0
- data/lib/mtk/util/mappable.rb +14 -0
- data/lib/mtk.rb +36 -0
- data/spec/mtk/chord_spec.rb +74 -0
- data/spec/mtk/constants/dynamics_spec.rb +94 -0
- data/spec/mtk/constants/intervals_spec.rb +140 -0
- data/spec/mtk/constants/pitch_classes_spec.rb +35 -0
- data/spec/mtk/constants/pitches_spec.rb +23 -0
- data/spec/mtk/event_spec.rb +120 -0
- data/spec/mtk/midi/file_spec.rb +208 -0
- data/spec/mtk/note_spec.rb +65 -0
- data/spec/mtk/numeric_extensions_spec.rb +102 -0
- data/spec/mtk/pattern/choice_spec.rb +21 -0
- data/spec/mtk/pattern/note_sequence_spec.rb +121 -0
- data/spec/mtk/pattern/pitch_sequence_spec.rb +47 -0
- data/spec/mtk/pattern/sequence_spec.rb +54 -0
- data/spec/mtk/pitch_class_set_spec.rb +103 -0
- data/spec/mtk/pitch_class_spec.rb +165 -0
- data/spec/mtk/pitch_set_spec.rb +163 -0
- data/spec/mtk/pitch_spec.rb +217 -0
- data/spec/mtk/timeline_spec.rb +234 -0
- data/spec/spec_helper.rb +7 -0
- data/spec/test.mid +0 -0
- 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
|