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