musicality 0.2.0 → 0.3.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/ChangeLog.md +30 -0
  3. data/lib/musicality/errors.rb +1 -0
  4. data/lib/musicality/notation/conversion/change_conversion.rb +63 -3
  5. data/lib/musicality/notation/conversion/note_time_converter.rb +23 -5
  6. data/lib/musicality/notation/conversion/score_conversion.rb +60 -0
  7. data/lib/musicality/notation/conversion/score_converter.rb +105 -0
  8. data/lib/musicality/notation/model/change.rb +98 -28
  9. data/lib/musicality/notation/model/part.rb +1 -1
  10. data/lib/musicality/notation/model/score.rb +4 -4
  11. data/lib/musicality/notation/packing/change_packing.rb +35 -25
  12. data/lib/musicality/notation/packing/score_packing.rb +2 -2
  13. data/lib/musicality/notation/util/function.rb +99 -0
  14. data/lib/musicality/notation/util/piecewise_function.rb +79 -99
  15. data/lib/musicality/notation/util/transition.rb +12 -0
  16. data/lib/musicality/notation/util/value_computer.rb +12 -152
  17. data/lib/musicality/performance/conversion/score_collator.rb +35 -20
  18. data/lib/musicality/performance/midi/part_sequencer.rb +2 -5
  19. data/lib/musicality/validatable.rb +6 -1
  20. data/lib/musicality/version.rb +1 -1
  21. data/lib/musicality.rb +4 -4
  22. data/musicality.gemspec +1 -0
  23. data/spec/notation/conversion/change_conversion_spec.rb +216 -9
  24. data/spec/notation/conversion/measure_note_map_spec.rb +2 -2
  25. data/spec/notation/conversion/note_time_converter_spec.rb +91 -9
  26. data/spec/notation/conversion/{measured_score_conversion_spec.rb → score_conversion_spec.rb} +44 -9
  27. data/spec/notation/conversion/score_converter_spec.rb +246 -0
  28. data/spec/notation/model/change_spec.rb +139 -36
  29. data/spec/notation/model/part_spec.rb +3 -3
  30. data/spec/notation/model/score_spec.rb +4 -4
  31. data/spec/notation/packing/change_packing_spec.rb +222 -71
  32. data/spec/notation/packing/part_packing_spec.rb +1 -1
  33. data/spec/notation/packing/score_packing_spec.rb +3 -2
  34. data/spec/notation/util/function_spec.rb +43 -0
  35. data/spec/notation/util/transition_spec.rb +51 -0
  36. data/spec/notation/util/value_computer_spec.rb +43 -87
  37. data/spec/performance/conversion/score_collator_spec.rb +46 -7
  38. data/spec/performance/midi/part_sequencer_spec.rb +2 -1
  39. metadata +29 -14
  40. data/lib/musicality/notation/conversion/measured_score_conversion.rb +0 -70
  41. data/lib/musicality/notation/conversion/measured_score_converter.rb +0 -95
  42. data/lib/musicality/notation/conversion/unmeasured_score_conversion.rb +0 -47
  43. data/lib/musicality/notation/conversion/unmeasured_score_converter.rb +0 -64
  44. data/spec/notation/conversion/measured_score_converter_spec.rb +0 -329
  45. data/spec/notation/conversion/unmeasured_score_conversion_spec.rb +0 -71
  46. data/spec/notation/conversion/unmeasured_score_converter_spec.rb +0 -116
@@ -7,17 +7,156 @@ describe Change::Immediate do
7
7
  c.offsets(44).should eq([44])
8
8
  end
9
9
  end
10
+
11
+ describe '#remap' do
12
+ it 'should return a clone of the change' do
13
+ c = Change::Immediate.new(12)
14
+ c2 = c.remap(1,{})
15
+ c2.should eq(c)
16
+ c2.should_not be c
17
+ end
18
+ end
19
+
20
+ describe '#to_transition' do
21
+ before :all do
22
+ @ch = Change::Immediate.new(20)
23
+ @off = 0
24
+ @f = @ch.to_transition(@off,0)
25
+ end
26
+
27
+ it 'should return a peicewise function' do
28
+ @f.should be_a Function::Piecewise
29
+ end
30
+
31
+ it 'should return a function defined from base offset to DOMAIN_MAX' do
32
+ @f.domain_include?(@off).should be true
33
+ @f.domain_include?(Function::DOMAIN_MAX).should be true
34
+ end
35
+
36
+ it 'should return change value from base offset onward' do
37
+ @f.at(@off).should eq(@ch.end_value)
38
+ @f.at(@off+1).should eq(@ch.end_value)
39
+ @f.at(@off+1000).should eq(@ch.end_value)
40
+ end
41
+ end
10
42
  end
11
43
 
12
44
  describe Change::Gradual do
45
+ before :all do
46
+ @change = Change::Gradual.linear(100,1.5)
47
+ @base = 25.5
48
+ end
49
+
13
50
  describe '#offsets' do
14
51
  before :all do
15
- @change = Change::Gradual.new(100,1.5,0.5,0.25)
16
- @base = 25.5
17
52
  @offsets = @change.offsets(@base)
18
53
  end
19
54
 
20
- it 'should return array with 4 elements' do
55
+ it 'should return array with 2 elements' do
56
+ @offsets.size.should eq(2)
57
+ end
58
+
59
+ it 'should include the given base offset' do
60
+ @offsets.should include(@base)
61
+ end
62
+
63
+ it 'should include the base offset + duration' do
64
+ @offsets.should include(@base + @change.duration)
65
+ end
66
+ end
67
+
68
+ describe '#remap' do
69
+ before :all do
70
+ @c2 = @change.remap(@base, @base => 3, (@base + @change.duration) => 5)
71
+ end
72
+
73
+ it 'should return a new Gradual' do
74
+ @c2.should be_a(Change::Gradual)
75
+ @c2.should_not be(@change)
76
+ end
77
+
78
+ it 'should keep end value, and change duration based on given offset map' do
79
+ @c2.end_value.should eq(@change.end_value)
80
+ @c2.duration.should eq(2)
81
+ end
82
+ end
83
+
84
+ describe '#to_transition' do
85
+ { 'linear transition' => Change::Gradual.linear(130,20),
86
+ 'sigmoid transition' => Change::Gradual.sigmoid(130,20)
87
+ }.each do |descr, change|
88
+ context descr do
89
+ before :all do
90
+ @offset = 3
91
+ @start_value = 50
92
+ @func = @change.to_transition(@offset, @start_value)
93
+ end
94
+
95
+ it 'should return a piecewise function' do
96
+ @func.should be_a Function::Piecewise
97
+ end
98
+
99
+ it 'should return a function that is undefined before base offset' do
100
+ @func.domain_include?(@offset-1e-5).should be false
101
+ @func.domain_include?(@offset-1e5).should be false
102
+ end
103
+
104
+ it 'should return a function defined from base offset to DOMAIN_MAX' do
105
+ @func.domain_include?(@offset).should be true
106
+ @func.domain_include?(@offset + @change.duration/2.0).should be true
107
+ @func.domain_include?(@offset + @change.duration).should be true
108
+ @func.domain_include?(@offset + @change.duration + 1).should be true
109
+ @func.domain_include?(Function::DOMAIN_MAX).should be true
110
+ end
111
+
112
+ it 'should make function that evaluates to start_value at start offset' do
113
+ @func.at(@offset).should eq(@start_value)
114
+ end
115
+
116
+ it 'should make function that evaluates to end_value at start offset + duration' do
117
+ @func.at(@offset + @change.duration).should eq(@change.end_value)
118
+ end
119
+
120
+ it 'should make function that evaluates to 1/2 between start/end value at 1/2 between start/end offset' do
121
+ tgt = (@change.end_value + @start_value) / 2.0
122
+ @func.at(@offset + @change.duration/2.0).should be_within(1e-5).of(tgt)
123
+ end
124
+
125
+ it 'should make function that evaluates to end value after change has elapsed' do
126
+ @func.at(@offset + @change.duration + 1).should eq(@change.end_value)
127
+ end
128
+ end
129
+ end
130
+
131
+ context 'start value already defined in change' do
132
+ { 'linear transition' => Change::Gradual.linear(130,20,start_value:80),
133
+ 'sigmoid transition' => Change::Gradual.sigmoid(130,20,start_value:80)
134
+ }.each do |descr, change|
135
+ context descr do
136
+ it 'should produce a function that begins at already-defined start value' do
137
+ offset = 3
138
+ start_value = 50
139
+ func = change.to_transition(offset, start_value)
140
+ func.at(offset).should eq(change.start_value)
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ describe Change::Gradual::Trimmed do
149
+ before :all do
150
+ @change = Change::Gradual.linear(100,1.5.to_r).to_trimmed(0.5.to_r,0.5.to_r)
151
+ @base = 25.5.to_r
152
+ end
153
+
154
+ describe '#offsets' do
155
+ before :all do
156
+ @offsets = @change.offsets(@base)
157
+ end
158
+
159
+ it 'should return array with 2 elements' do
21
160
  @offsets.size.should eq(4)
22
161
  end
23
162
 
@@ -25,16 +164,84 @@ describe Change::Gradual do
25
164
  @offsets.should include(@base)
26
165
  end
27
166
 
28
- it 'should include the base offset - elapsed' do
29
- @offsets.should include(@base - @change.elapsed)
167
+ it 'should include the base offset - preceding' do
168
+ @offsets.should include(@base - @change.preceding)
169
+ end
170
+
171
+ it 'should include the base offset + remaining' do
172
+ @offsets.should include(@base + @change.remaining)
173
+ end
174
+
175
+ it 'should include the base offset - preceding + duration' do
176
+ @offsets.should include(@base - @change.preceding + @change.duration)
177
+ end
178
+ end
179
+
180
+ describe '#remap' do
181
+ before :all do
182
+ @offsets = { @base => 3, (@base - @change.preceding) => 0,
183
+ (@base + @change.remaining) => 5, (@base - @change.preceding + @change.duration) => 7 }
184
+ @c2 = @change.remap(@base, @offsets)
30
185
  end
31
186
 
32
- it 'should include the base offset + impending' do
33
- @offsets.should include(@base + @change.impending)
187
+ it 'should return a new Gradual::Trimmed' do
188
+ @c2.should be_a(Change::Gradual::Trimmed)
189
+ @c2.should_not be(@change)
34
190
  end
35
191
 
36
- it 'should include the base offset + impending + remaining' do
37
- @offsets.should include(@base + @change.impending + @change.remaining)
192
+ it 'should keep end value, and change duration based on given offset map' do
193
+ @c2.end_value.should eq(@change.end_value)
194
+ @c2.duration.should eq(7)
195
+ @c2.preceding.should eq(3)
196
+ @c2.remaining.should eq(2)
197
+ end
198
+ end
199
+
200
+ describe '#to_transition' do
201
+ Change::Gradual::TRANSITIONS.each do |transition|
202
+ context "#{transition} transition" do
203
+ [nil,35].each do |start_val|
204
+ context "change start_value = #{start_val}" do
205
+ before :all do
206
+ untrimmed = Change::Gradual.new(130,20, transition, start_value: start_val)
207
+ untrimmed_offset = 0
208
+ untrimmed_start_val = 50
209
+ @untrimmed_trans = untrimmed.to_transition(untrimmed_offset, untrimmed_start_val)
210
+
211
+ trimmed = untrimmed.trim(5,3)
212
+ trimmed_offset = untrimmed_offset + trimmed.preceding
213
+ trimmed_start_val = @untrimmed_trans.at(trimmed_offset)
214
+ @trimmed_trans = trimmed.to_transition(trimmed_offset, trimmed_start_val)
215
+
216
+ @xrange = trimmed_offset..(trimmed_offset + trimmed.remaining)
217
+ end
218
+
219
+ it 'should produce function that is undefined before trimmed domain' do
220
+ @trimmed_trans.domain_include?(@xrange.first-1).should be false
221
+ end
222
+
223
+ it 'should produce function that is defined for trimmed domain' do
224
+ @trimmed_trans.domain_include?(@xrange.first).should be true
225
+ @trimmed_trans.domain_include?((@xrange.first + @xrange.last)/2.0).should be true
226
+ @trimmed_trans.domain_include?(@xrange.last).should be true
227
+ end
228
+
229
+ it 'should produce function that is defined after trimmed domain' do
230
+ @trimmed_trans.domain_include?(@xrange.last+1).should be true
231
+ @trimmed_trans.domain_include?(Function::DOMAIN_MAX).should be true
232
+ end
233
+
234
+ it 'should produce function that stays at end value after transition' do
235
+ @trimmed_trans.at(@xrange.last + 1).should eq(@trimmed_trans.at(@xrange.last))
236
+ end
237
+
238
+ it 'should produce function that samples same as equivalent untrimmed' do
239
+ srate = 50
240
+ @trimmed_trans.sample(@xrange, srate).should eq(@untrimmed_trans.sample(@xrange, srate))
241
+ end
242
+ end
243
+ end
244
+ end
38
245
  end
39
246
  end
40
247
  end
@@ -38,10 +38,10 @@ describe 'Conversion.measure_note_map' do
38
38
  @score = Score::Measured.new(@start_meter, 120,
39
39
  meter_changes: { @first_mc_off => Change::Immediate.new(@new_meter) },
40
40
  tempo_changes: {
41
- "1/2".to_r => Change::Gradual.new(100,1),
41
+ "1/2".to_r => Change::Gradual.linear(100,1),
42
42
  2 => Change::Immediate.new(120),
43
43
  3 => Change::Immediate.new(100),
44
- 3.1 => Change::Gradual.new(100,1),
44
+ 3.1 => Change::Gradual.linear(100,1),
45
45
  5 => Change::Immediate.new(120),
46
46
  6 => Change::Immediate.new(100),
47
47
  }
@@ -1,13 +1,13 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
2
 
3
- describe NoteTimeConverter do
3
+ describe NoteTimeConverter::Unmeasured do
4
4
  describe '#notes_per_second_at' do
5
5
  it 'should convert tempo using Tempo::QNPM.to_nps' do
6
6
  tc = ValueComputer.new(
7
- 120, 1 => Change::Gradual.new(100,1), 2 => Change::Gradual.new(150,1))
8
- converter = NoteTimeConverter.new(tc,200)
7
+ 120, 1 => Change::Gradual.linear(100,1), 2 => Change::Gradual.linear(150,1))
8
+ converter = NoteTimeConverter::Unmeasured.new(tc,200)
9
9
  (0..2).step(0.2).each do |offset|
10
- nps = Tempo::QNPM.to_nps(tc.value_at(offset))
10
+ nps = Tempo::QNPM.to_nps(tc.at(offset))
11
11
  converter.notes_per_second_at(offset).should eq(nps)
12
12
  end
13
13
  end
@@ -18,7 +18,7 @@ describe NoteTimeConverter do
18
18
  before :each do
19
19
  @tempo_computer = ValueComputer.new 120
20
20
  sample_rate = 48
21
- @converter = NoteTimeConverter.new(@tempo_computer, sample_rate)
21
+ @converter = NoteTimeConverter::Unmeasured.new(@tempo_computer, sample_rate)
22
22
  end
23
23
 
24
24
  it "should return a time of zero when note end is zero." do
@@ -34,9 +34,9 @@ describe NoteTimeConverter do
34
34
  context "linear tempo-change" do
35
35
  before :each do
36
36
  @tempo_computer = ValueComputer.new(
37
- 120, 1 => Change::Gradual.new(60, 1))
37
+ 120, 1 => Change::Gradual.linear(60, 1))
38
38
  sample_rate = 200
39
- @converter = NoteTimeConverter.new(@tempo_computer, sample_rate)
39
+ @converter = NoteTimeConverter::Unmeasured.new(@tempo_computer, sample_rate)
40
40
  end
41
41
 
42
42
  it "should return a time of zero when note end is zero." do
@@ -53,12 +53,12 @@ describe NoteTimeConverter do
53
53
  end
54
54
  end
55
55
 
56
- describe "#map_note_offsets_to_time_offsets" do
56
+ describe "#note_time_map" do
57
57
  context "constant tempo" do
58
58
  before :each do
59
59
  @tempo_computer = ValueComputer.new 120
60
60
  sample_rate = 4800
61
- @converter = NoteTimeConverter.new(
61
+ @converter = NoteTimeConverter::Unmeasured.new(
62
62
  @tempo_computer, sample_rate)
63
63
  end
64
64
 
@@ -79,3 +79,85 @@ describe NoteTimeConverter do
79
79
  end
80
80
  end
81
81
  end
82
+
83
+ describe NoteTimeConverter::Measured do
84
+ describe '#notes_per_second_at' do
85
+ it 'should convert tempo using Tempo::BPM.to_nps' do
86
+ tc = ValueComputer.new(120, 1 => Change::Gradual.linear(100,1),
87
+ 2 => Change::Gradual.linear(150,1))
88
+ bdc = ValueComputer.new(0.25.to_r, 2.5 => Change::Immediate.new(0.375.to_r))
89
+ converter = NoteTimeConverter::Measured.new(tc,bdc,200)
90
+ (0..3.2).step(0.2).each do |offset|
91
+ nps = Tempo::BPM.to_nps(tc.at(offset), bdc.at(offset))
92
+ converter.notes_per_second_at(offset).should eq(nps)
93
+ end
94
+ end
95
+ end
96
+
97
+ describe "#time_elapsed" do
98
+ context "constant tempo, beat duration" do
99
+ before :each do
100
+ @tempo_computer = ValueComputer.new 120
101
+ @bdur_computer = ValueComputer.new Rational(1,4)
102
+ sample_rate = 48
103
+ @converter = NoteTimeConverter::Measured.new(@tempo_computer, @bdur_computer, sample_rate)
104
+ end
105
+
106
+ it "should return a time of zero when note end is zero." do
107
+ @converter.time_elapsed(0, 0).should eq(0)
108
+ end
109
+
110
+ it "should return a time of 1 second when note end is equal to the initial notes-per-second" do
111
+ note_end = @converter.notes_per_second_at(0)
112
+ @converter.time_elapsed(0, note_end).should eq(1)
113
+ end
114
+ end
115
+
116
+ context "linear tempo-change, constant beat duration" do
117
+ before :each do
118
+ @tempo_computer = ValueComputer.new(120, 1 => Change::Gradual.linear(60, 1))
119
+ @bdur_computer = ValueComputer.new Rational(1,4)
120
+ sample_rate = 200
121
+ @converter = NoteTimeConverter::Measured.new(@tempo_computer, @bdur_computer, sample_rate)
122
+ end
123
+
124
+ it "should return a time of zero when note end is zero." do
125
+ @converter.time_elapsed(0.0, 0.0).should eq(0.0)
126
+ end
127
+
128
+ it "should return a time of 3 sec during a 1-note long transition from 120bpm to 60bpm" do
129
+ @converter.notes_per_second_at(1.0).should eq(0.5)
130
+ @converter.notes_per_second_at(2.0).should eq(0.25)
131
+
132
+ @converter.time_elapsed(1.0, 2.0).should be_within(0.05).of(2.77)
133
+ end
134
+
135
+ end
136
+ end
137
+
138
+ describe "#note_time_map" do
139
+ context "constant tempo, beat duration" do
140
+ before :each do
141
+ @tempo_computer = ValueComputer.new 120
142
+ @bdur_computer = ValueComputer.new Rational(1,4)
143
+ sample_rate = 4800
144
+ @converter = NoteTimeConverter::Measured.new(@tempo_computer, @bdur_computer, sample_rate)
145
+ end
146
+
147
+ it "should map offset 0.0 to time 0.0" do
148
+ map = @converter.note_time_map [0.0]
149
+ map[0.0].should eq(0.0)
150
+ end
151
+
152
+ it "should map offset 0.25 to time 0.5" do
153
+ map = @converter.note_time_map [0.0, 0.25]
154
+ map[0.25].should be_within(0.01).of(0.5)
155
+ end
156
+
157
+ it "should map offset 1.0 to time 2.0" do
158
+ map = @converter.note_time_map [0.0, 1.0]
159
+ map[1.0].should be_within(0.01).of(2.0)
160
+ end
161
+ end
162
+ end
163
+ end
@@ -8,7 +8,7 @@ describe Score::Measured do
8
8
  dynamic_changes: {
9
9
  1 => Change::Immediate.new(Dynamics::MF),
10
10
  5 => Change::Immediate.new(Dynamics::FF),
11
- 6 => Change::Gradual.new(Dynamics::MF,2),
11
+ 6 => Change::Gradual.linear(Dynamics::MF,2),
12
12
  14 => Change::Immediate.new(Dynamics::PP),
13
13
  }
14
14
  )
@@ -16,7 +16,7 @@ describe Score::Measured do
16
16
  @prog = Program.new([0...3,4...7,1...20,17..."45/2".to_r])
17
17
  tcs = {
18
18
  0 => Change::Immediate.new(120),
19
- 4 => Change::Gradual.new(60,2),
19
+ 4 => Change::Gradual.linear(60,2),
20
20
  11 => Change::Immediate.new(110)
21
21
  }
22
22
  mcs = {
@@ -87,7 +87,7 @@ describe Score::Measured do
87
87
  end
88
88
 
89
89
  it 'should map start meter to offset 0' do
90
- @mdurs[0].should eq(@score.start_meter.measure_duration)
90
+ @mdurs[0.to_r].should eq(@score.start_meter.measure_duration)
91
91
  end
92
92
  end
93
93
 
@@ -107,7 +107,7 @@ describe Score::Measured do
107
107
  end
108
108
 
109
109
  it 'should begin with meter change at offset 0, instead of start meter' do
110
- @mdurs2[0].should eq(@change.value.measure_duration)
110
+ @mdurs2[0].should eq(@change.end_value.measure_duration)
111
111
  end
112
112
  end
113
113
 
@@ -126,16 +126,51 @@ describe Score::Measured do
126
126
  end
127
127
 
128
128
  it 'should begin with start meter' do
129
- @mdurs3[0].should eq(@score3.start_meter.measure_duration)
129
+ @mdurs3[0.to_r].should eq(@score3.start_meter.measure_duration)
130
130
  end
131
131
  end
132
132
  end
133
133
 
134
- describe '#to_unmeasured' do
135
- it 'should use MeasuredScoreConverter#convert_score' do
136
- nscore1 = @score.to_unmeasured
137
- nscore2 = MeasuredScoreConverter.new(@score).convert_score
134
+ describe '#to_timed' do
135
+ it 'should use ScoreConverter::Measured#convert_score' do
136
+ nscore1 = @score.to_timed(200)
137
+ nscore2 = ScoreConverter::Measured.new(@score,200).convert_score
138
138
  nscore1.should eq(nscore2)
139
139
  end
140
140
  end
141
141
  end
142
+
143
+ describe Score::Unmeasured do
144
+ before :all do
145
+ @parts = {
146
+ "piano" => Part.new(Dynamics::MP,
147
+ notes: [Note.quarter(C4), Note.eighth(F3), Note.whole(C4), Note.half(D4)]*12,
148
+ dynamic_changes: {
149
+ 1 => Change::Immediate.new(Dynamics::MF),
150
+ 5 => Change::Immediate.new(Dynamics::FF),
151
+ 6 => Change::Gradual.linear(Dynamics::MF,2),
152
+ 14 => Change::Immediate.new(Dynamics::PP),
153
+ }
154
+ )
155
+ }
156
+ @prog = Program.new([0...3,4...7,1...20,17..."45/2".to_r])
157
+ tcs = {
158
+ 0 => Change::Immediate.new(120),
159
+ 4 => Change::Gradual.linear(60,2),
160
+ 11 => Change::Immediate.new(110)
161
+ }
162
+ @score = Score::Unmeasured.new(120,
163
+ parts: @parts,
164
+ program: @prog,
165
+ tempo_changes: tcs,
166
+ )
167
+ end
168
+
169
+ describe '#to_timed' do
170
+ it 'should use ScoreConverter::Unmeasured#convert_score' do
171
+ nscore1 = @score.to_timed(200)
172
+ nscore2 = ScoreConverter::Unmeasured.new(@score,200).convert_score
173
+ nscore1.should eq(nscore2)
174
+ end
175
+ end
176
+ end