musicality 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +47 -0
- data/Rakefile +65 -0
- data/bin/midify +78 -0
- data/examples/hip.rb +32 -0
- data/examples/missed_connection.rb +26 -0
- data/examples/song1.rb +33 -0
- data/examples/song2.rb +32 -0
- data/lib/musicality/errors.rb +9 -0
- data/lib/musicality/notation/conversion/change_conversion.rb +19 -0
- data/lib/musicality/notation/conversion/measure_note_map.rb +40 -0
- data/lib/musicality/notation/conversion/measured_score_conversion.rb +70 -0
- data/lib/musicality/notation/conversion/measured_score_converter.rb +95 -0
- data/lib/musicality/notation/conversion/note_time_converter.rb +68 -0
- data/lib/musicality/notation/conversion/tempo_conversion.rb +25 -0
- data/lib/musicality/notation/conversion/unmeasured_score_conversion.rb +47 -0
- data/lib/musicality/notation/conversion/unmeasured_score_converter.rb +64 -0
- data/lib/musicality/notation/model/articulations.rb +13 -0
- data/lib/musicality/notation/model/change.rb +62 -0
- data/lib/musicality/notation/model/dynamics.rb +12 -0
- data/lib/musicality/notation/model/link.rb +73 -0
- data/lib/musicality/notation/model/meter.rb +54 -0
- data/lib/musicality/notation/model/meters.rb +9 -0
- data/lib/musicality/notation/model/note.rb +120 -0
- data/lib/musicality/notation/model/part.rb +54 -0
- data/lib/musicality/notation/model/pitch.rb +163 -0
- data/lib/musicality/notation/model/pitches.rb +21 -0
- data/lib/musicality/notation/model/program.rb +53 -0
- data/lib/musicality/notation/model/score.rb +132 -0
- data/lib/musicality/notation/packing/change_packing.rb +46 -0
- data/lib/musicality/notation/packing/part_packing.rb +31 -0
- data/lib/musicality/notation/packing/program_packing.rb +16 -0
- data/lib/musicality/notation/packing/score_packing.rb +108 -0
- data/lib/musicality/notation/parsing/articulation_parsing.rb +264 -0
- data/lib/musicality/notation/parsing/articulation_parsing.treetop +59 -0
- data/lib/musicality/notation/parsing/convenience_methods.rb +74 -0
- data/lib/musicality/notation/parsing/duration_nodes.rb +21 -0
- data/lib/musicality/notation/parsing/duration_parsing.rb +205 -0
- data/lib/musicality/notation/parsing/duration_parsing.treetop +25 -0
- data/lib/musicality/notation/parsing/link_nodes.rb +35 -0
- data/lib/musicality/notation/parsing/link_parsing.rb +270 -0
- data/lib/musicality/notation/parsing/link_parsing.treetop +33 -0
- data/lib/musicality/notation/parsing/meter_parsing.rb +190 -0
- data/lib/musicality/notation/parsing/meter_parsing.treetop +29 -0
- data/lib/musicality/notation/parsing/note_node.rb +40 -0
- data/lib/musicality/notation/parsing/note_parsing.rb +229 -0
- data/lib/musicality/notation/parsing/note_parsing.treetop +28 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_float_parsing.rb +289 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_float_parsing.treetop +29 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_integer_parsing.rb +64 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_integer_parsing.treetop +17 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_rational_parsing.rb +86 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_rational_parsing.treetop +20 -0
- data/lib/musicality/notation/parsing/numbers/positive_float_parsing.rb +503 -0
- data/lib/musicality/notation/parsing/numbers/positive_float_parsing.treetop +33 -0
- data/lib/musicality/notation/parsing/numbers/positive_integer_parsing.rb +95 -0
- data/lib/musicality/notation/parsing/numbers/positive_integer_parsing.treetop +19 -0
- data/lib/musicality/notation/parsing/numbers/positive_rational_parsing.rb +84 -0
- data/lib/musicality/notation/parsing/numbers/positive_rational_parsing.treetop +19 -0
- data/lib/musicality/notation/parsing/parseable.rb +30 -0
- data/lib/musicality/notation/parsing/pitch_node.rb +23 -0
- data/lib/musicality/notation/parsing/pitch_parsing.rb +448 -0
- data/lib/musicality/notation/parsing/pitch_parsing.treetop +52 -0
- data/lib/musicality/notation/parsing/segment_parsing.rb +141 -0
- data/lib/musicality/notation/parsing/segment_parsing.treetop +23 -0
- data/lib/musicality/notation/util/interpolation.rb +16 -0
- data/lib/musicality/notation/util/piecewise_function.rb +122 -0
- data/lib/musicality/notation/util/value_computer.rb +170 -0
- data/lib/musicality/performance/conversion/glissando_converter.rb +34 -0
- data/lib/musicality/performance/conversion/note_sequence_extractor.rb +98 -0
- data/lib/musicality/performance/conversion/portamento_converter.rb +24 -0
- data/lib/musicality/performance/conversion/score_collator.rb +126 -0
- data/lib/musicality/performance/midi/midi_events.rb +34 -0
- data/lib/musicality/performance/midi/midi_util.rb +31 -0
- data/lib/musicality/performance/midi/part_sequencer.rb +123 -0
- data/lib/musicality/performance/midi/score_sequencer.rb +45 -0
- data/lib/musicality/performance/model/note_attacks.rb +19 -0
- data/lib/musicality/performance/model/note_sequence.rb +111 -0
- data/lib/musicality/performance/util/note_linker.rb +28 -0
- data/lib/musicality/performance/util/optimization.rb +31 -0
- data/lib/musicality/validatable.rb +38 -0
- data/lib/musicality/version.rb +3 -0
- data/lib/musicality.rb +81 -0
- data/musicality.gemspec +30 -0
- data/spec/musicality_spec.rb +7 -0
- data/spec/notation/conversion/change_conversion_spec.rb +40 -0
- data/spec/notation/conversion/measure_note_map_spec.rb +73 -0
- data/spec/notation/conversion/measured_score_conversion_spec.rb +141 -0
- data/spec/notation/conversion/measured_score_converter_spec.rb +329 -0
- data/spec/notation/conversion/note_time_converter_spec.rb +81 -0
- data/spec/notation/conversion/tempo_conversion_spec.rb +40 -0
- data/spec/notation/conversion/unmeasured_score_conversion_spec.rb +71 -0
- data/spec/notation/conversion/unmeasured_score_converter_spec.rb +116 -0
- data/spec/notation/model/change_spec.rb +90 -0
- data/spec/notation/model/link_spec.rb +83 -0
- data/spec/notation/model/meter_spec.rb +97 -0
- data/spec/notation/model/note_spec.rb +183 -0
- data/spec/notation/model/part_spec.rb +69 -0
- data/spec/notation/model/pitch_spec.rb +180 -0
- data/spec/notation/model/program_spec.rb +50 -0
- data/spec/notation/model/score_spec.rb +211 -0
- data/spec/notation/packing/change_packing_spec.rb +153 -0
- data/spec/notation/packing/part_packing_spec.rb +66 -0
- data/spec/notation/packing/program_packing_spec.rb +33 -0
- data/spec/notation/packing/score_packing_spec.rb +301 -0
- data/spec/notation/parsing/articulation_parsing_spec.rb +23 -0
- data/spec/notation/parsing/convenience_methods_spec.rb +99 -0
- data/spec/notation/parsing/duration_nodes_spec.rb +83 -0
- data/spec/notation/parsing/duration_parsing_spec.rb +70 -0
- data/spec/notation/parsing/link_nodes_spec.rb +30 -0
- data/spec/notation/parsing/link_parsing_spec.rb +13 -0
- data/spec/notation/parsing/meter_parsing_spec.rb +23 -0
- data/spec/notation/parsing/note_node_spec.rb +87 -0
- data/spec/notation/parsing/note_parsing_spec.rb +46 -0
- data/spec/notation/parsing/numbers/nonnegative_float_spec.rb +28 -0
- data/spec/notation/parsing/numbers/nonnegative_integer_spec.rb +11 -0
- data/spec/notation/parsing/numbers/nonnegative_rational_spec.rb +11 -0
- data/spec/notation/parsing/numbers/positive_float_spec.rb +28 -0
- data/spec/notation/parsing/numbers/positive_integer_spec.rb +28 -0
- data/spec/notation/parsing/numbers/positive_rational_spec.rb +28 -0
- data/spec/notation/parsing/pitch_node_spec.rb +38 -0
- data/spec/notation/parsing/pitch_parsing_spec.rb +14 -0
- data/spec/notation/parsing/segment_parsing_spec.rb +27 -0
- data/spec/notation/util/value_computer_spec.rb +146 -0
- data/spec/performance/conversion/glissando_converter_spec.rb +93 -0
- data/spec/performance/conversion/note_sequence_extractor_spec.rb +230 -0
- data/spec/performance/conversion/portamento_converter_spec.rb +91 -0
- data/spec/performance/conversion/score_collator_spec.rb +183 -0
- data/spec/performance/midi/midi_util_spec.rb +110 -0
- data/spec/performance/midi/part_sequencer_spec.rb +40 -0
- data/spec/performance/midi/score_sequencer_spec.rb +50 -0
- data/spec/performance/model/note_sequence_spec.rb +147 -0
- data/spec/performance/util/note_linker_spec.rb +68 -0
- data/spec/performance/util/optimization_spec.rb +73 -0
- data/spec/spec_helper.rb +43 -0
- metadata +323 -0
@@ -0,0 +1,230 @@
|
|
1
|
+
|
2
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
3
|
+
|
4
|
+
describe NoteSequenceExtractor do
|
5
|
+
describe '#initialize' do
|
6
|
+
it 'should clone original notes' do
|
7
|
+
notes = [ Note.quarter([C2]), Note.half, Note.half ]
|
8
|
+
extr = NoteSequenceExtractor.new(notes)
|
9
|
+
extr.notes[0].should eq(notes[0])
|
10
|
+
notes[0].transpose!(1)
|
11
|
+
extr.notes[0].should_not eq(notes[0])
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should maintain the same number of notes' do
|
15
|
+
extr = NoteSequenceExtractor.new(
|
16
|
+
[ Note.quarter, Note.half, Note.half ])
|
17
|
+
extr.notes.size.should eq 3
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should remove any bad ties (tying pitch does not exist in next note' do
|
21
|
+
extr = NoteSequenceExtractor.new(
|
22
|
+
[ Note.quarter([C4,E4], links: {C4 => Link::Tie.new}),
|
23
|
+
Note.quarter([E4]) ]
|
24
|
+
)
|
25
|
+
extr.notes[0].links.should_not have_key(C4)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should replace any good ties with slurs' do
|
29
|
+
extr = NoteSequenceExtractor.new(
|
30
|
+
[ Note.quarter([C4,E4], links: {C4 => Link::Tie.new, E4 => Link::Tie.new}),
|
31
|
+
Note.quarter([C4,E4]) ]
|
32
|
+
)
|
33
|
+
extr.notes[0].links[C4].should be_a Link::Slur
|
34
|
+
extr.notes[0].links[E4].should be_a Link::Slur
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should remove dead slur/legato (where target pitch is non-existent)' do
|
38
|
+
extr = NoteSequenceExtractor.new(
|
39
|
+
[ Note.quarter([C4,E4], links: { C4 => Link::Slur.new(D4), E4 => Link::Legato.new(F4) }),
|
40
|
+
Note.quarter([C4]) ]
|
41
|
+
)
|
42
|
+
extr.notes[0].links.should be_empty
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should remove any link where the source pitch is missing' do
|
46
|
+
extr = NoteSequenceExtractor.new(
|
47
|
+
[ Note.quarter([C4,D4,E4,F4,G4], links: {
|
48
|
+
Bb4 => Link::Tie.new, Db4 => Link::Slur.new(C4),
|
49
|
+
Eb4 => Link::Legato.new(D4), Gb4 => Link::Glissando.new(E4),
|
50
|
+
Ab5 => Link::Portamento.new(F4)
|
51
|
+
}),
|
52
|
+
Note.quarter([C4,D4,E4,F4,G4])
|
53
|
+
])
|
54
|
+
extr.notes[0].links.should be_empty
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'should not remove portamento and glissando with non-existent target pitches' do
|
58
|
+
extr = NoteSequenceExtractor.new(
|
59
|
+
[ Note.quarter([C4,D4]),
|
60
|
+
Note.quarter([C4,D4,E4,F4,G4], links: {
|
61
|
+
C4 => Link::Tie.new, D4 => Link::Slur.new(Eb4),
|
62
|
+
E4 => Link::Legato.new(Gb4), F4 => Link::Glissando.new(A5),
|
63
|
+
G4 => Link::Portamento.new(Bb5)}) ]
|
64
|
+
)
|
65
|
+
extr.notes[-1].links.size.should eq 2
|
66
|
+
extr.notes[-1].links.should have_key(F4)
|
67
|
+
extr.notes[-1].links.should have_key(G4)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
describe '#extract_sequences' do
|
72
|
+
context 'empty note array' do
|
73
|
+
it 'should return empty' do
|
74
|
+
seqs = NoteSequenceExtractor.new([]).extract_sequences
|
75
|
+
seqs.should be_empty
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
context 'array of only rest notes' do
|
80
|
+
it 'should return empty' do
|
81
|
+
notes = [ Note::quarter, Note::quarter ]
|
82
|
+
seqs = NoteSequenceExtractor.new(notes).extract_sequences
|
83
|
+
seqs.should be_empty
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context 'array with only one note, single pitch' do
|
88
|
+
before :all do
|
89
|
+
@note = Note::quarter([C5])
|
90
|
+
@seqs = NoteSequenceExtractor.new([@note]).extract_sequences
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'should return array with one sequence' do
|
94
|
+
@seqs.size.should eq 1
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'should start offset 0' do
|
98
|
+
@seqs[0].start.should eq 0
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'should stop offset <= note duration' do
|
102
|
+
@seqs[0].stop.should be <= @note.duration
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'array with two slurred notes, single pitch' do
|
107
|
+
before :all do
|
108
|
+
@notes = [ Note.quarter([C5], articulation: SLUR), Note.quarter([D5]) ]
|
109
|
+
@seqs = NoteSequenceExtractor.new(@notes).extract_sequences
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'should return array with one sequence' do
|
113
|
+
@seqs.size.should eq 1
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'should start offset 0' do
|
117
|
+
@seqs[0].start.should eq 0
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'should stop offset <= combined duration of the two notes' do
|
121
|
+
@seqs[0].stop.should be <= (@notes[0].duration + @notes[1].duration)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context 'array with one note, multiple pitches' do
|
126
|
+
before :all do
|
127
|
+
@note = Note.quarter([C5,D5,E5])
|
128
|
+
@seqs = NoteSequenceExtractor.new([@note]).extract_sequences
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'should return array with as many sequences as pitches' do
|
132
|
+
@seqs.size.should eq @note.pitches.size
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'should start the sequences at 0' do
|
136
|
+
@seqs.each {|s| s.start.should eq(0) }
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'should end each sequence at or before note duration' do
|
140
|
+
@seqs.each {|s| s.stop.should be <= @note.duration }
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'should put one pitch in each seq' do
|
144
|
+
@seqs.each {|s| s.pitches.size.should eq(1) }
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'should assign a different pitch to each' do
|
148
|
+
@seqs.map {|seq| seq.pitches[0] }.sort.should eq @note.pitches.sort
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
context 'array with multiple notes and links' do
|
153
|
+
before :all do
|
154
|
+
@notes = [ Note.quarter([C3,E3], links: {
|
155
|
+
C3 => Link::Slur.new(D3), E3 => Link::Legato.new(F3)}),
|
156
|
+
Note.eighth([D3,F3]) ]
|
157
|
+
@seqs = NoteSequenceExtractor.new(@notes).extract_sequences
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'should create a sequence for linked notes' do
|
161
|
+
@seqs.size.should eq(2)
|
162
|
+
end
|
163
|
+
|
164
|
+
it 'should add pitch at 0 from first note' do
|
165
|
+
@seqs[0].pitches.should have_key(0)
|
166
|
+
@notes[0].pitches.should include(@seqs[0].pitches[0])
|
167
|
+
@seqs[1].pitches.should have_key(0)
|
168
|
+
@notes[0].pitches.should include(@seqs[1].pitches[0])
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
context 'single note with single pitch, and glissando up' do
|
173
|
+
before :all do
|
174
|
+
@note = Note.whole([D3], links: { D3 => Link::Glissando.new(G3) })
|
175
|
+
@seqs = NoteSequenceExtractor.new([@note]).extract_sequences
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'should produce one sequence' do
|
179
|
+
@seqs.size.should eq(1)
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'should include pitches up to (not including) target pitch' do
|
183
|
+
@seqs[0].pitches.values.should include(D3,Eb3,E3,F3,Gb3)
|
184
|
+
end
|
185
|
+
|
186
|
+
it 'should produce sequence with duration <= note duration' do
|
187
|
+
@seqs[0].duration.should be <= @note.duration
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
context 'single note with single pitch, and glissando down' do
|
192
|
+
before :all do
|
193
|
+
@note = Note.whole([D3], links: { D3 => Link::Glissando.new(A2) })
|
194
|
+
@seqs = NoteSequenceExtractor.new([@note]).extract_sequences
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'should produce one sequence' do
|
198
|
+
@seqs.size.should eq(1)
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'should include pitches down to (not including) target pitch' do
|
202
|
+
@seqs[0].pitches.values.should include(D3,Db3,C3,B2,Bb2)
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'should produce sequence with duration <= note duration' do
|
206
|
+
@seqs[0].duration.should be <= @note.duration
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
context 'two notes with single pitch, glissando up to pitch in second note' do
|
211
|
+
before :all do
|
212
|
+
@notes = [Note.whole([D3], links: { D3 => Link::Glissando.new(G3) }),
|
213
|
+
Note.quarter([G3]) ]
|
214
|
+
@seqs = NoteSequenceExtractor.new(@notes).extract_sequences
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'should produce a single sequence' do
|
218
|
+
@seqs.size.should eq(1)
|
219
|
+
end
|
220
|
+
|
221
|
+
it 'should includes pitches up through target pitch' do
|
222
|
+
@seqs[0].pitches.values.should include(D3,Eb3,E3,F3,Gb3,G3)
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'should produce sequence with duration <= note1dur + note2dur' do
|
226
|
+
@seqs[0].duration.should be <= (@notes[0].duration + @notes[1].duration)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
describe PortamentoConverter do
|
4
|
+
describe '.portamento_pitches' do
|
5
|
+
context 'start pitch <= target_pitch' do
|
6
|
+
[
|
7
|
+
[C4,E4,10],
|
8
|
+
[D2,G3,11],
|
9
|
+
[C4.transpose(0.5), A4.transpose(-0.3), 12],
|
10
|
+
[D5,D5,7],
|
11
|
+
[D5,D5.transpose(0.06),7],
|
12
|
+
[D5,D5.transpose(0.07),7],
|
13
|
+
[D5,D5.transpose(0.08),7],
|
14
|
+
].each do |start,finish,step_size|
|
15
|
+
context "start at #{start.to_s}, end at #{finish.to_s}" do
|
16
|
+
pitches = PortamentoConverter.portamento_pitches(start,finish,step_size)
|
17
|
+
|
18
|
+
it 'should begin at start pitch' do
|
19
|
+
pitches.first.should eq(start)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should space pitches using given cent step size' do
|
23
|
+
(1...pitches.size).each do |i|
|
24
|
+
diff = pitches[i].total_cents - pitches[i-1].total_cents
|
25
|
+
diff.should eq(step_size)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should end where one more step would be >= target pitch' do
|
30
|
+
diff = finish.total_cents - pitches.last.total_cents
|
31
|
+
diff.should be <= step_size
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'start pitch > target_pitch' do
|
38
|
+
[
|
39
|
+
[E4,C4,10],
|
40
|
+
[G3,D2,11],
|
41
|
+
[A4,Bb3,12],
|
42
|
+
[A4.transpose(-0.33),Bb3.transpose(0.11),13],
|
43
|
+
[Bb3.transpose(0.64),Bb3.transpose(0.54),14],
|
44
|
+
].each do |start,finish,step_size|
|
45
|
+
context "start at #{start.to_s}, end at #{finish.to_s}" do
|
46
|
+
pitches = PortamentoConverter.portamento_pitches(start,finish,step_size)
|
47
|
+
|
48
|
+
it 'should begin at start pitch' do
|
49
|
+
pitches.first.should eq(start)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should space pitches using negative of given cent step size' do
|
53
|
+
(1...pitches.size).each do |i|
|
54
|
+
diff = pitches[i-1].total_cents - pitches[i].total_cents
|
55
|
+
diff.should eq(step_size)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should end where one more step would be <= target pitch' do
|
60
|
+
diff = pitches.last.total_cents - finish.total_cents
|
61
|
+
diff.should be <= step_size
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe '.portamento_elements' do
|
69
|
+
before :all do
|
70
|
+
@dur = Rational(3,2)
|
71
|
+
@acc = false
|
72
|
+
@els = PortamentoConverter.portamento_elements(C4,F4,25,@dur,@acc)
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'should return an array of SlurredElement objects' do
|
76
|
+
@els.each {|el| el.should be_a SlurredElement }
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'should split up duration among elements' do
|
80
|
+
sum = @els.map {|el| el.duration }.inject(0,:+)
|
81
|
+
sum.should eq(@dur)
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'should set accented as given' do
|
85
|
+
els = PortamentoConverter.portamento_elements(C4,D4,10,1,false)
|
86
|
+
els.each {|el| el.accented.should eq(false) }
|
87
|
+
els = PortamentoConverter.portamento_elements(C4,D4,10,1,true)
|
88
|
+
els.each {|el| el.accented.should eq(true) }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
describe ScoreCollator do
|
4
|
+
describe '#collate_parts' do
|
5
|
+
before :all do
|
6
|
+
@part = Part.new(Dynamics::FF,
|
7
|
+
notes: [ Note.quarter([C2]),
|
8
|
+
Note.half([D2]),
|
9
|
+
Note.half([E2])
|
10
|
+
])
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'first note starts before the segment start' do
|
14
|
+
context 'first note ends right at segment start' do
|
15
|
+
it 'should not be included in the part' do
|
16
|
+
score = Score::Measured.new(FOUR_FOUR, 120,
|
17
|
+
parts: {1 => @part},
|
18
|
+
program: Program.new(["1/4".to_r..."5/4".to_r]))
|
19
|
+
collator = ScoreCollator.new(score)
|
20
|
+
parts = collator.collate_parts
|
21
|
+
notes = parts[1].notes
|
22
|
+
notes.size.should eq(@part.notes.size - 1)
|
23
|
+
notes[0].pitches[0].should eq D2
|
24
|
+
notes[1].pitches[0].should eq E2
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'first note ends after segment start' do
|
29
|
+
it 'should not be included in the part, and a rest is inserted' do
|
30
|
+
score = Score::Measured.new(FOUR_FOUR, 120,
|
31
|
+
parts: {1 => @part},
|
32
|
+
program: Program.new(["1/8".to_r..."5/4".to_r]))
|
33
|
+
collator = ScoreCollator.new(score)
|
34
|
+
parts = collator.collate_parts
|
35
|
+
notes = parts[1].notes
|
36
|
+
notes.size.should eq(@part.notes.size)
|
37
|
+
notes[0].pitches.should be_empty
|
38
|
+
notes[0].duration.should eq "1/8".to_r
|
39
|
+
notes[1].pitches[0].should eq D2
|
40
|
+
notes[2].pitches[0].should eq E2
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'first note starts at segment start' do
|
46
|
+
context 'last note starts at program end' do
|
47
|
+
it 'should not be included in the part' do
|
48
|
+
score = Score::Measured.new(FOUR_FOUR, 120,
|
49
|
+
parts: {1 => @part},
|
50
|
+
program: Program.new([0.to_r..."3/4".to_r]))
|
51
|
+
collator = ScoreCollator.new(score)
|
52
|
+
parts = collator.collate_parts
|
53
|
+
notes = parts[1].notes
|
54
|
+
notes.size.should eq(@part.notes.size - 1)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'last note start before program end, but lasts until after' do
|
59
|
+
it 'should be included in the part, but truncated' do
|
60
|
+
score = Score::Measured.new(FOUR_FOUR, 120,
|
61
|
+
parts: {1 => @part},
|
62
|
+
program: Program.new([0.to_r...1.to_r]))
|
63
|
+
collator = ScoreCollator.new(score)
|
64
|
+
parts = collator.collate_parts
|
65
|
+
notes = parts[1].notes
|
66
|
+
notes.size.should eq(@part.notes.size)
|
67
|
+
notes[-1].duration.should eq("1/4".to_r)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context 'last note ends before program segment end' do
|
72
|
+
it 'should insert a rest between last note end and segment end' do
|
73
|
+
score = Score::Measured.new(FOUR_FOUR, 120,
|
74
|
+
parts: {1 => @part},
|
75
|
+
program: Program.new([0.to_r..."6/4".to_r]))
|
76
|
+
collator = ScoreCollator.new(score)
|
77
|
+
parts = collator.collate_parts
|
78
|
+
notes = parts[1].notes
|
79
|
+
notes.size.should eq(@part.notes.size + 1)
|
80
|
+
notes[-1].pitches.should be_empty
|
81
|
+
notes[-1].duration.should eq("1/4".to_r)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'should preserve links' do
|
87
|
+
notes = Note.split_parse("1Db4~Bb4")
|
88
|
+
score = Score::Measured.new(
|
89
|
+
FOUR_FOUR,120,
|
90
|
+
parts: { "lead" => Part.new(Dynamics::MP, notes: notes) },
|
91
|
+
program: Program.new([0..1,0..1]),
|
92
|
+
)
|
93
|
+
collator = ScoreCollator.new(score)
|
94
|
+
parts = collator.collate_parts
|
95
|
+
|
96
|
+
notes = parts["lead"].notes
|
97
|
+
notes.size.should eq 2
|
98
|
+
notes.each do |note|
|
99
|
+
note.links.should have_key(Db4)
|
100
|
+
note.links[Db4].should be_a Link::Glissando
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe '#collate_tempo_changes' do
|
106
|
+
before :all do
|
107
|
+
@change0 = Change::Immediate.new(120)
|
108
|
+
@change1 = Change::Immediate.new(200)
|
109
|
+
@change2 = Change::Gradual.new(100,1)
|
110
|
+
end
|
111
|
+
|
112
|
+
context 'tempo change starts at end of program segment' do
|
113
|
+
it 'should not be included in the tempo changes' do
|
114
|
+
score = Score::Measured.new(FOUR_FOUR, 120, tempo_changes: {
|
115
|
+
1 => @change1, 2 => @change2 }, program: Program.new([0..2]))
|
116
|
+
collator = ScoreCollator.new(score)
|
117
|
+
tcs = collator.collate_tempo_changes
|
118
|
+
tcs.size.should eq 2
|
119
|
+
tcs[0.to_r].should eq @change0
|
120
|
+
tcs[1.to_r].should eq @change1
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
context 'tempo change starts and ends before segment' do
|
125
|
+
before :all do
|
126
|
+
score = Score::Measured.new(FOUR_FOUR, 120, tempo_changes: {
|
127
|
+
2 => @change2 }, program: Program.new([4..5]))
|
128
|
+
collator = ScoreCollator.new(score)
|
129
|
+
@tcs = collator.collate_tempo_changes
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'should not be included in the tempo changes' do
|
133
|
+
@tcs.size.should eq 1
|
134
|
+
@tcs[0.to_r].should be_a Change::Immediate
|
135
|
+
end
|
136
|
+
|
137
|
+
it 'should be used as starting tempo change value' do
|
138
|
+
@tcs[0.to_r].value.should eq @change2.value
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
context 'tempo change starts before segment, but ends during segment' do
|
143
|
+
it 'should e included in the tempo changes, but truncated' do
|
144
|
+
score = Score::Measured.new(FOUR_FOUR, 120, tempo_changes: {
|
145
|
+
1.5.to_r => @change2 }, program: Program.new([2..4]))
|
146
|
+
collator = ScoreCollator.new(score)
|
147
|
+
tcs = collator.collate_tempo_changes
|
148
|
+
tcs.size.should eq 1
|
149
|
+
tcs[0.to_r].value.should eq @change2.value
|
150
|
+
tcs[0.to_r].duration.should eq(0.5)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context 'tempo change starts during segment, lasts until after' do
|
155
|
+
it 'should be included in the tempo changes, but truncated' do
|
156
|
+
score = Score::Measured.new(FOUR_FOUR, 120, tempo_changes: {
|
157
|
+
1 => @change1, 2 => @change2 }, program: Program.new([0..2.5]))
|
158
|
+
collator = ScoreCollator.new(score)
|
159
|
+
tcs = collator.collate_tempo_changes
|
160
|
+
tcs.size.should eq 3
|
161
|
+
tcs[0.to_r].should eq @change0
|
162
|
+
tcs[1.to_r].should eq @change1
|
163
|
+
tcs[2.to_r].value.should eq @change2.value
|
164
|
+
tcs[2.to_r].duration.should eq(0.5)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
describe '#collate_meter_changes' do
|
170
|
+
it 'should behave just as #collate_tempo_changes' do
|
171
|
+
change0 = Change::Immediate.new(FOUR_FOUR)
|
172
|
+
change1 = Change::Immediate.new(THREE_FOUR)
|
173
|
+
change2 = Change::Immediate.new(SIX_EIGHT)
|
174
|
+
score = Score::Measured.new(FOUR_FOUR, 120, meter_changes: {
|
175
|
+
1 => change1, 2 => change2 }, program: Program.new([0...2]))
|
176
|
+
collator = ScoreCollator.new(score)
|
177
|
+
tcs = collator.collate_meter_changes
|
178
|
+
tcs.size.should eq 2
|
179
|
+
tcs[0.to_r].should eq change0
|
180
|
+
tcs[1.to_r].should eq change1
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
describe MidiUtil do
|
4
|
+
describe '.pitch_to_notenum' do
|
5
|
+
context 'given C4' do
|
6
|
+
it 'should return 60' do
|
7
|
+
MidiUtil.pitch_to_notenum(C4).should eq(60)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'given A4' do
|
12
|
+
it 'should return 69' do
|
13
|
+
MidiUtil.pitch_to_notenum(A4).should eq(69)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'given octave below C0' do
|
18
|
+
it 'should return 0' do
|
19
|
+
MidiUtil.pitch_to_notenum(Pitch.new(octave:-1)).should eq(0)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'given G9' do
|
24
|
+
it 'should return 127' do
|
25
|
+
MidiUtil.pitch_to_notenum(G9).should eq(127)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'given pitch between C-1 to G9 range' do
|
30
|
+
it 'should pitch diff from C4 should equal notenum diff' do
|
31
|
+
c4_nn = MidiUtil.pitch_to_notenum(C4)
|
32
|
+
[C2,D2,Eb2,F2,A2,Bb3,C3,G3,Gb4,F5,A5,Bb5,C6].each do |pitch|
|
33
|
+
nn = MidiUtil.pitch_to_notenum(pitch)
|
34
|
+
C4.diff(pitch).should eq(c4_nn - nn)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'given pitch outside C-1 to G9 range' do
|
40
|
+
it 'should raise error' do
|
41
|
+
expect { MidiUtil.pitch_to_notenum(Pitch.new(octave:-2)) }.to raise_error
|
42
|
+
expect { MidiUtil.pitch_to_notenum(Ab9) }.to raise_error
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe '.dynamic_to_volume' do
|
48
|
+
context 'given 0' do
|
49
|
+
it 'should return 0' do
|
50
|
+
MidiUtil.dynamic_to_volume(0).should eq(0)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context 'given 1' do
|
55
|
+
it 'should return 0' do
|
56
|
+
MidiUtil.dynamic_to_volume(1).should eq(127)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'given 0.5' do
|
61
|
+
it 'should return 64' do
|
62
|
+
MidiUtil.dynamic_to_volume(0.5).should eq(64)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe 'note_velocity' do
|
68
|
+
context 'given true' do
|
69
|
+
it 'should return a higher value than when given false' do
|
70
|
+
MidiUtil.note_velocity(true).should be > MidiUtil.note_velocity(false)
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'should return a value between 0 and 127' do
|
74
|
+
MidiUtil.note_velocity(true).should be_between(0,127)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'given false' do
|
79
|
+
it 'should return a value between 0 and 127' do
|
80
|
+
MidiUtil.note_velocity(false).should be_between(0,127)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe '.delta' do
|
86
|
+
context 'given 1/4' do
|
87
|
+
it 'should return the given ppqn' do
|
88
|
+
MidiUtil.delta(Rational(1,4),20).should eq(20)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
context 'given 1/2' do
|
93
|
+
it 'should return twice the given ppqn' do
|
94
|
+
MidiUtil.delta(Rational(1,2),20).should eq(40)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'given 1/8' do
|
99
|
+
it 'should return half the given ppqn' do
|
100
|
+
MidiUtil.delta(Rational(1,8),20).should eq(10)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
it 'should return an integer' do
|
105
|
+
(0.1...1.1).step(0.1).each do |dur|
|
106
|
+
MidiUtil.delta(dur,100).should be_an(Integer)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
describe PartSequencer do
|
4
|
+
describe '#make_midi_track' do
|
5
|
+
before :all do
|
6
|
+
part = Part.new(Dynamics::PP, notes: "/4C4 /4D4 /8 /8D4 /8E4 /2C4".to_notes * 2)
|
7
|
+
@midi_seq = MIDI::Sequence.new
|
8
|
+
@part_name = "mypart"
|
9
|
+
@channel = 2
|
10
|
+
@ppqn = 200
|
11
|
+
@program_num = 22
|
12
|
+
@track = PartSequencer.new(part).make_midi_track(@midi_seq,@part_name,@channel,@ppqn,@program_num)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should return MIDI::Track' do
|
16
|
+
@track.should be_a MIDI::Track
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should assign part name to track name' do
|
20
|
+
@track.name.should eq(@part_name)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should assign program number via ProgramChange event' do
|
24
|
+
event = @track.events.select { |x| x.is_a? MIDI::ProgramChange }.first
|
25
|
+
event.program.should eq(@program_num)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should assign the given channel number to all channel events' do
|
29
|
+
@track.events.each do |event|
|
30
|
+
if event.is_a? MIDI::ChannelEvent
|
31
|
+
event.channel.should eq(@channel)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should not add the track to the given midi seq' do
|
37
|
+
@midi_seq.tracks.should_not include(@track)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
describe ScoreSequencer do
|
4
|
+
describe '#initialize' do
|
5
|
+
end
|
6
|
+
|
7
|
+
describe '#make_midi_seq' do
|
8
|
+
before :all do
|
9
|
+
@part1_name = "abc"
|
10
|
+
@part2_name = "def"
|
11
|
+
@part1 = Part.new(Dynamics::PP, notes: "/4C4 /4D4 /8 /8D4 /8E4 3/8C4".to_notes * 2)
|
12
|
+
@part2 = Part.new(Dynamics::FF, notes: "/4E4 3/4F4 /4E4".to_notes * 2)
|
13
|
+
@score = Score::Timed.new(program: Program.new([0..2.5]),
|
14
|
+
parts: {@part1_name => @part2, @part2_name => @part2})
|
15
|
+
@instr_map = {@part1_name => 33, @part2_name => 25}
|
16
|
+
@midi_seq = ScoreSequencer.new(@score).make_midi_seq(@instr_map)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should return MIDI::Sequence' do
|
20
|
+
@midi_seq.should be_a MIDI::Sequence
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should have 1 track for each part, plus one for score meta events' do
|
24
|
+
@midi_seq.tracks.size.should eq(@score.parts.size + 1)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'should add a midi track for each part' do
|
28
|
+
@midi_seq.tracks[1].name.should eq(@part1_name)
|
29
|
+
@midi_seq.tracks[2].name.should eq(@part2_name)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'should assign program number from given instrument map hash' do
|
33
|
+
[ @midi_seq.tracks[1], @midi_seq.tracks[2] ].each do |track|
|
34
|
+
prog_event = track.events.select {|x| x.is_a? MIDI::ProgramChange }.first
|
35
|
+
prog_event.program.should eq(@instr_map[track.name])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'should assign different channel to each part track' do
|
40
|
+
chs_so_far = []
|
41
|
+
[ @midi_seq.tracks[1], @midi_seq.tracks[2] ].each do |track|
|
42
|
+
channel_events = track.events.select {|x| x.is_a? MIDI::ChannelEvent }
|
43
|
+
chs = channel_events.map {|event| event.channel }.uniq
|
44
|
+
chs.size.should eq 1
|
45
|
+
chs_so_far.should_not include(chs[0])
|
46
|
+
chs_so_far.push chs[0]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|