musicality 0.1.0
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.
- 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
|