musicality 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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