musicality 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (141) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +47 -0
  8. data/Rakefile +65 -0
  9. data/bin/midify +78 -0
  10. data/examples/hip.rb +32 -0
  11. data/examples/missed_connection.rb +26 -0
  12. data/examples/song1.rb +33 -0
  13. data/examples/song2.rb +32 -0
  14. data/lib/musicality/errors.rb +9 -0
  15. data/lib/musicality/notation/conversion/change_conversion.rb +19 -0
  16. data/lib/musicality/notation/conversion/measure_note_map.rb +40 -0
  17. data/lib/musicality/notation/conversion/measured_score_conversion.rb +70 -0
  18. data/lib/musicality/notation/conversion/measured_score_converter.rb +95 -0
  19. data/lib/musicality/notation/conversion/note_time_converter.rb +68 -0
  20. data/lib/musicality/notation/conversion/tempo_conversion.rb +25 -0
  21. data/lib/musicality/notation/conversion/unmeasured_score_conversion.rb +47 -0
  22. data/lib/musicality/notation/conversion/unmeasured_score_converter.rb +64 -0
  23. data/lib/musicality/notation/model/articulations.rb +13 -0
  24. data/lib/musicality/notation/model/change.rb +62 -0
  25. data/lib/musicality/notation/model/dynamics.rb +12 -0
  26. data/lib/musicality/notation/model/link.rb +73 -0
  27. data/lib/musicality/notation/model/meter.rb +54 -0
  28. data/lib/musicality/notation/model/meters.rb +9 -0
  29. data/lib/musicality/notation/model/note.rb +120 -0
  30. data/lib/musicality/notation/model/part.rb +54 -0
  31. data/lib/musicality/notation/model/pitch.rb +163 -0
  32. data/lib/musicality/notation/model/pitches.rb +21 -0
  33. data/lib/musicality/notation/model/program.rb +53 -0
  34. data/lib/musicality/notation/model/score.rb +132 -0
  35. data/lib/musicality/notation/packing/change_packing.rb +46 -0
  36. data/lib/musicality/notation/packing/part_packing.rb +31 -0
  37. data/lib/musicality/notation/packing/program_packing.rb +16 -0
  38. data/lib/musicality/notation/packing/score_packing.rb +108 -0
  39. data/lib/musicality/notation/parsing/articulation_parsing.rb +264 -0
  40. data/lib/musicality/notation/parsing/articulation_parsing.treetop +59 -0
  41. data/lib/musicality/notation/parsing/convenience_methods.rb +74 -0
  42. data/lib/musicality/notation/parsing/duration_nodes.rb +21 -0
  43. data/lib/musicality/notation/parsing/duration_parsing.rb +205 -0
  44. data/lib/musicality/notation/parsing/duration_parsing.treetop +25 -0
  45. data/lib/musicality/notation/parsing/link_nodes.rb +35 -0
  46. data/lib/musicality/notation/parsing/link_parsing.rb +270 -0
  47. data/lib/musicality/notation/parsing/link_parsing.treetop +33 -0
  48. data/lib/musicality/notation/parsing/meter_parsing.rb +190 -0
  49. data/lib/musicality/notation/parsing/meter_parsing.treetop +29 -0
  50. data/lib/musicality/notation/parsing/note_node.rb +40 -0
  51. data/lib/musicality/notation/parsing/note_parsing.rb +229 -0
  52. data/lib/musicality/notation/parsing/note_parsing.treetop +28 -0
  53. data/lib/musicality/notation/parsing/numbers/nonnegative_float_parsing.rb +289 -0
  54. data/lib/musicality/notation/parsing/numbers/nonnegative_float_parsing.treetop +29 -0
  55. data/lib/musicality/notation/parsing/numbers/nonnegative_integer_parsing.rb +64 -0
  56. data/lib/musicality/notation/parsing/numbers/nonnegative_integer_parsing.treetop +17 -0
  57. data/lib/musicality/notation/parsing/numbers/nonnegative_rational_parsing.rb +86 -0
  58. data/lib/musicality/notation/parsing/numbers/nonnegative_rational_parsing.treetop +20 -0
  59. data/lib/musicality/notation/parsing/numbers/positive_float_parsing.rb +503 -0
  60. data/lib/musicality/notation/parsing/numbers/positive_float_parsing.treetop +33 -0
  61. data/lib/musicality/notation/parsing/numbers/positive_integer_parsing.rb +95 -0
  62. data/lib/musicality/notation/parsing/numbers/positive_integer_parsing.treetop +19 -0
  63. data/lib/musicality/notation/parsing/numbers/positive_rational_parsing.rb +84 -0
  64. data/lib/musicality/notation/parsing/numbers/positive_rational_parsing.treetop +19 -0
  65. data/lib/musicality/notation/parsing/parseable.rb +30 -0
  66. data/lib/musicality/notation/parsing/pitch_node.rb +23 -0
  67. data/lib/musicality/notation/parsing/pitch_parsing.rb +448 -0
  68. data/lib/musicality/notation/parsing/pitch_parsing.treetop +52 -0
  69. data/lib/musicality/notation/parsing/segment_parsing.rb +141 -0
  70. data/lib/musicality/notation/parsing/segment_parsing.treetop +23 -0
  71. data/lib/musicality/notation/util/interpolation.rb +16 -0
  72. data/lib/musicality/notation/util/piecewise_function.rb +122 -0
  73. data/lib/musicality/notation/util/value_computer.rb +170 -0
  74. data/lib/musicality/performance/conversion/glissando_converter.rb +34 -0
  75. data/lib/musicality/performance/conversion/note_sequence_extractor.rb +98 -0
  76. data/lib/musicality/performance/conversion/portamento_converter.rb +24 -0
  77. data/lib/musicality/performance/conversion/score_collator.rb +126 -0
  78. data/lib/musicality/performance/midi/midi_events.rb +34 -0
  79. data/lib/musicality/performance/midi/midi_util.rb +31 -0
  80. data/lib/musicality/performance/midi/part_sequencer.rb +123 -0
  81. data/lib/musicality/performance/midi/score_sequencer.rb +45 -0
  82. data/lib/musicality/performance/model/note_attacks.rb +19 -0
  83. data/lib/musicality/performance/model/note_sequence.rb +111 -0
  84. data/lib/musicality/performance/util/note_linker.rb +28 -0
  85. data/lib/musicality/performance/util/optimization.rb +31 -0
  86. data/lib/musicality/validatable.rb +38 -0
  87. data/lib/musicality/version.rb +3 -0
  88. data/lib/musicality.rb +81 -0
  89. data/musicality.gemspec +30 -0
  90. data/spec/musicality_spec.rb +7 -0
  91. data/spec/notation/conversion/change_conversion_spec.rb +40 -0
  92. data/spec/notation/conversion/measure_note_map_spec.rb +73 -0
  93. data/spec/notation/conversion/measured_score_conversion_spec.rb +141 -0
  94. data/spec/notation/conversion/measured_score_converter_spec.rb +329 -0
  95. data/spec/notation/conversion/note_time_converter_spec.rb +81 -0
  96. data/spec/notation/conversion/tempo_conversion_spec.rb +40 -0
  97. data/spec/notation/conversion/unmeasured_score_conversion_spec.rb +71 -0
  98. data/spec/notation/conversion/unmeasured_score_converter_spec.rb +116 -0
  99. data/spec/notation/model/change_spec.rb +90 -0
  100. data/spec/notation/model/link_spec.rb +83 -0
  101. data/spec/notation/model/meter_spec.rb +97 -0
  102. data/spec/notation/model/note_spec.rb +183 -0
  103. data/spec/notation/model/part_spec.rb +69 -0
  104. data/spec/notation/model/pitch_spec.rb +180 -0
  105. data/spec/notation/model/program_spec.rb +50 -0
  106. data/spec/notation/model/score_spec.rb +211 -0
  107. data/spec/notation/packing/change_packing_spec.rb +153 -0
  108. data/spec/notation/packing/part_packing_spec.rb +66 -0
  109. data/spec/notation/packing/program_packing_spec.rb +33 -0
  110. data/spec/notation/packing/score_packing_spec.rb +301 -0
  111. data/spec/notation/parsing/articulation_parsing_spec.rb +23 -0
  112. data/spec/notation/parsing/convenience_methods_spec.rb +99 -0
  113. data/spec/notation/parsing/duration_nodes_spec.rb +83 -0
  114. data/spec/notation/parsing/duration_parsing_spec.rb +70 -0
  115. data/spec/notation/parsing/link_nodes_spec.rb +30 -0
  116. data/spec/notation/parsing/link_parsing_spec.rb +13 -0
  117. data/spec/notation/parsing/meter_parsing_spec.rb +23 -0
  118. data/spec/notation/parsing/note_node_spec.rb +87 -0
  119. data/spec/notation/parsing/note_parsing_spec.rb +46 -0
  120. data/spec/notation/parsing/numbers/nonnegative_float_spec.rb +28 -0
  121. data/spec/notation/parsing/numbers/nonnegative_integer_spec.rb +11 -0
  122. data/spec/notation/parsing/numbers/nonnegative_rational_spec.rb +11 -0
  123. data/spec/notation/parsing/numbers/positive_float_spec.rb +28 -0
  124. data/spec/notation/parsing/numbers/positive_integer_spec.rb +28 -0
  125. data/spec/notation/parsing/numbers/positive_rational_spec.rb +28 -0
  126. data/spec/notation/parsing/pitch_node_spec.rb +38 -0
  127. data/spec/notation/parsing/pitch_parsing_spec.rb +14 -0
  128. data/spec/notation/parsing/segment_parsing_spec.rb +27 -0
  129. data/spec/notation/util/value_computer_spec.rb +146 -0
  130. data/spec/performance/conversion/glissando_converter_spec.rb +93 -0
  131. data/spec/performance/conversion/note_sequence_extractor_spec.rb +230 -0
  132. data/spec/performance/conversion/portamento_converter_spec.rb +91 -0
  133. data/spec/performance/conversion/score_collator_spec.rb +183 -0
  134. data/spec/performance/midi/midi_util_spec.rb +110 -0
  135. data/spec/performance/midi/part_sequencer_spec.rb +40 -0
  136. data/spec/performance/midi/score_sequencer_spec.rb +50 -0
  137. data/spec/performance/model/note_sequence_spec.rb +147 -0
  138. data/spec/performance/util/note_linker_spec.rb +68 -0
  139. data/spec/performance/util/optimization_spec.rb +73 -0
  140. data/spec/spec_helper.rb +43 -0
  141. 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