musicality 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/ChangeLog.md +30 -0
- data/lib/musicality/errors.rb +1 -0
- data/lib/musicality/notation/conversion/change_conversion.rb +63 -3
- data/lib/musicality/notation/conversion/note_time_converter.rb +23 -5
- data/lib/musicality/notation/conversion/score_conversion.rb +60 -0
- data/lib/musicality/notation/conversion/score_converter.rb +105 -0
- data/lib/musicality/notation/model/change.rb +98 -28
- data/lib/musicality/notation/model/part.rb +1 -1
- data/lib/musicality/notation/model/score.rb +4 -4
- data/lib/musicality/notation/packing/change_packing.rb +35 -25
- data/lib/musicality/notation/packing/score_packing.rb +2 -2
- data/lib/musicality/notation/util/function.rb +99 -0
- data/lib/musicality/notation/util/piecewise_function.rb +79 -99
- data/lib/musicality/notation/util/transition.rb +12 -0
- data/lib/musicality/notation/util/value_computer.rb +12 -152
- data/lib/musicality/performance/conversion/score_collator.rb +35 -20
- data/lib/musicality/performance/midi/part_sequencer.rb +2 -5
- data/lib/musicality/validatable.rb +6 -1
- data/lib/musicality/version.rb +1 -1
- data/lib/musicality.rb +4 -4
- data/musicality.gemspec +1 -0
- data/spec/notation/conversion/change_conversion_spec.rb +216 -9
- data/spec/notation/conversion/measure_note_map_spec.rb +2 -2
- data/spec/notation/conversion/note_time_converter_spec.rb +91 -9
- data/spec/notation/conversion/{measured_score_conversion_spec.rb → score_conversion_spec.rb} +44 -9
- data/spec/notation/conversion/score_converter_spec.rb +246 -0
- data/spec/notation/model/change_spec.rb +139 -36
- data/spec/notation/model/part_spec.rb +3 -3
- data/spec/notation/model/score_spec.rb +4 -4
- data/spec/notation/packing/change_packing_spec.rb +222 -71
- data/spec/notation/packing/part_packing_spec.rb +1 -1
- data/spec/notation/packing/score_packing_spec.rb +3 -2
- data/spec/notation/util/function_spec.rb +43 -0
- data/spec/notation/util/transition_spec.rb +51 -0
- data/spec/notation/util/value_computer_spec.rb +43 -87
- data/spec/performance/conversion/score_collator_spec.rb +46 -7
- data/spec/performance/midi/part_sequencer_spec.rb +2 -1
- metadata +29 -14
- data/lib/musicality/notation/conversion/measured_score_conversion.rb +0 -70
- data/lib/musicality/notation/conversion/measured_score_converter.rb +0 -95
- data/lib/musicality/notation/conversion/unmeasured_score_conversion.rb +0 -47
- data/lib/musicality/notation/conversion/unmeasured_score_converter.rb +0 -64
- data/spec/notation/conversion/measured_score_converter_spec.rb +0 -329
- data/spec/notation/conversion/unmeasured_score_conversion_spec.rb +0 -71
- data/spec/notation/conversion/unmeasured_score_converter_spec.rb +0 -116
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1f460185c537892ef1e12ffce5b1c829095666c7
|
4
|
+
data.tar.gz: fc1512fe588da6ee7599aea61ba713d268c50cc3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ab8468ae5d3f4daf66a6f054480969c0d81e5b409b9a52efc513b3fc8ec6ccf5cdace8be25df378d63a9002ec6c6db61afb9fe6d92506b01a3c53412f8abe587
|
7
|
+
data.tar.gz: 784d01d623fd8436e635853340905ab381915138d9ccf2773e3728084c0ad04b6083e16ee692546a7fd9556930eced67bd70e3703fad91abb3b427443a72695d
|
data/ChangeLog.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
### 0.3.0 / 2014-11-24
|
2
|
+
* Conversion of both tempo-based scores (measured and unmeasured) directly to time-based score
|
3
|
+
* Trimming of gradual changes, useful in collating scores
|
4
|
+
* Refactoring of ValueComputer using Function and Transition utility classes
|
5
|
+
* Add optional start_value for gradual changes, for making them absolute (by default they're relative)
|
6
|
+
* Add 'with' kw arg to change pack/unpack methods, for converting start/end values
|
7
|
+
|
8
|
+
### 0.2.0 / 2014-11-24
|
9
|
+
|
10
|
+
* Add #seconds_long for timed score, #notes_long for tempo-based scores, and #measures_long for measure-based score
|
11
|
+
|
12
|
+
### 0.1.0 / 2014-11-23
|
13
|
+
|
14
|
+
* Pitch class that includes octave, semitone, and cent values
|
15
|
+
* Note class that includes duration, pitches, articulation, accent, and links
|
16
|
+
* Part class that includes notes and dynamics
|
17
|
+
* Program class to define which sections are played, and in what order
|
18
|
+
* Score classes, all of which include parts and a program, but more specifically:
|
19
|
+
* Measured score that also includes tempo and meter changes. It has measure-based program and changes, and note-based notes
|
20
|
+
* Unmeasured score that also includes tempo changes. It has note-based program, changes, notes
|
21
|
+
* Timed score which is entirely time-based
|
22
|
+
* Pass block to help initialize a score or a part
|
23
|
+
* Validation of core model classes (score, part, program, note, etc.), with error list
|
24
|
+
* Based on Ruby 2.0
|
25
|
+
* Stringizing and parsing methods for duration, pitch, note, and meter
|
26
|
+
* Stringized notes include shorthand for note links and articulation
|
27
|
+
* Packing/unpacking to/from hash, using stringizing/parsing to condense, esp. for notes
|
28
|
+
* Convert score to MIDI file via midilib gem
|
29
|
+
* bin/midify command-line utility to run MIDI conversion on score YAML file
|
30
|
+
|
data/lib/musicality/errors.rb
CHANGED
@@ -5,13 +5,73 @@ class Change
|
|
5
5
|
def offsets base_offset
|
6
6
|
[ base_offset ]
|
7
7
|
end
|
8
|
+
|
9
|
+
def remap base_offset, map
|
10
|
+
self.clone
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_transition offset, value
|
14
|
+
Transition::new(Function::Constant.new(@end_value), offset..offset)
|
15
|
+
end
|
8
16
|
end
|
9
17
|
|
10
18
|
class Gradual < Change
|
11
19
|
def offsets base_offset
|
12
|
-
|
13
|
-
|
14
|
-
|
20
|
+
[ base_offset, base_offset + @duration ]
|
21
|
+
end
|
22
|
+
|
23
|
+
def remap base_offset, map
|
24
|
+
newdur = map[base_offset + @duration] - map[base_offset]
|
25
|
+
Gradual.new(@end_value, newdur, @transition)
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_transition offset, value
|
29
|
+
p1 = [ offset, @start_value || value ]
|
30
|
+
p2 = [ offset + @duration, @end_value ]
|
31
|
+
func = case @transition
|
32
|
+
when LINEAR then Function::Linear.new(p1, p2)
|
33
|
+
when SIGMOID then Function::Sigmoid.new(p1, p2)
|
34
|
+
end
|
35
|
+
Transition.new(func, p1[0]..p2[0])
|
36
|
+
end
|
37
|
+
|
38
|
+
class Trimmed < Gradual
|
39
|
+
def offsets base_offset
|
40
|
+
origin = base_offset - @preceding
|
41
|
+
[ origin, base_offset, base_offset + @remaining, origin + @duration ]
|
42
|
+
end
|
43
|
+
|
44
|
+
def remap base_offset, map
|
45
|
+
x0 = base_offset - @preceding
|
46
|
+
y0 = map[x0]
|
47
|
+
new_dur = map[x0 + @duration] - y0
|
48
|
+
x1 = base_offset
|
49
|
+
y1 = map[x1]
|
50
|
+
Trimmed.new(@end_value, new_dur, @transition, preceding: y1 - y0,
|
51
|
+
remaining: map[x1 + @remaining] - y1)
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_transition offset, value
|
55
|
+
x1,x2,x3 = offset - @preceding, offset, offset + @remaining
|
56
|
+
x4 = x1 + @duration
|
57
|
+
func = case @transition
|
58
|
+
when LINEAR
|
59
|
+
Function::Linear.new(@start_value.nil? ? [x2,value] : [x1,@start_value],[x4, @end_value])
|
60
|
+
when SIGMOID
|
61
|
+
y1 = @start_value || Function::Sigmoid.find_y0(x1..x4, [x2, value], @end_value)
|
62
|
+
Function::Sigmoid.new([x1,y1],[x4, @end_value])
|
63
|
+
end
|
64
|
+
Transition.new(func, x2..x3)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def inv_sigm start_domain, x
|
70
|
+
sigm_domain = Function::Sigmoid::SIGM_DOMAIN
|
71
|
+
x_ = Function.transform_domains(start_domain, sigm_domain, x)
|
72
|
+
dy = Function::Sigmoid::sigm(sigm_domain.last) - Function::Sigmoid::sigm(sigm_domain.first)
|
73
|
+
(Function::Sigmoid::sigm(x_) - Function::Sigmoid::sigm(sigm_domain.first)) / dy
|
74
|
+
end
|
15
75
|
end
|
16
76
|
end
|
17
77
|
end
|
@@ -1,18 +1,36 @@
|
|
1
1
|
module Musicality
|
2
|
-
|
2
|
+
|
3
3
|
# Convert offsets in unmeasured note time to just plain time.
|
4
4
|
class NoteTimeConverter
|
5
5
|
# @param [ValueComputer] tempo_computer Given an offset, returns tempo
|
6
6
|
# value in quarter-notes-per-minute
|
7
7
|
# @param [Numeric] sample_rate Rate at which tempo values are sampled
|
8
8
|
# in the conversion (samples/sec).
|
9
|
-
def initialize
|
10
|
-
@tempo_computer = tempo_computer
|
9
|
+
def initialize sample_rate
|
11
10
|
@sample_period = Rational(1,sample_rate)
|
12
11
|
end
|
12
|
+
|
13
|
+
class Unmeasured < NoteTimeConverter
|
14
|
+
def initialize tempo_computer, sample_rate
|
15
|
+
@tempo_computer = tempo_computer
|
16
|
+
super(sample_rate)
|
17
|
+
end
|
18
|
+
|
19
|
+
def notes_per_second_at offset
|
20
|
+
Tempo::QNPM.to_nps(@tempo_computer.at offset)
|
21
|
+
end
|
22
|
+
end
|
13
23
|
|
14
|
-
|
15
|
-
|
24
|
+
class Measured < NoteTimeConverter
|
25
|
+
def initialize tempo_computer, bdur_computer, sample_rate
|
26
|
+
@tempo_computer = tempo_computer
|
27
|
+
@bdur_computer = bdur_computer
|
28
|
+
super(sample_rate)
|
29
|
+
end
|
30
|
+
|
31
|
+
def notes_per_second_at offset
|
32
|
+
Tempo::BPM.to_nps(@tempo_computer.at(offset), @bdur_computer.at(offset))
|
33
|
+
end
|
16
34
|
end
|
17
35
|
|
18
36
|
# Calculate the time elapsed between given start/end note offset. Using the
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Musicality
|
2
|
+
|
3
|
+
class Score
|
4
|
+
class Unmeasured < TempoBased
|
5
|
+
# Convert to timed score by converting measure-based offsets and note-based
|
6
|
+
# durations to time-based. This eliminates the use of tempos.
|
7
|
+
def to_timed tempo_sample_rate
|
8
|
+
ScoreConverter::Unmeasured.new(self, tempo_sample_rate).convert_score
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Measured < TempoBased
|
13
|
+
# Convert to timed score by converting measure-based offsets and note-based
|
14
|
+
# durations to time-based. This eliminates the use of meters and tempos.
|
15
|
+
def to_timed tempo_sample_rate
|
16
|
+
ScoreConverter::Measured.new(self, tempo_sample_rate).convert_score
|
17
|
+
end
|
18
|
+
|
19
|
+
def measure_note_map
|
20
|
+
Conversion::measure_note_map(measure_offsets,measure_durations)
|
21
|
+
end
|
22
|
+
|
23
|
+
def measure_offsets
|
24
|
+
moffs = Set.new([0.to_r])
|
25
|
+
@tempo_changes.each {|moff,change| moffs += change.offsets(moff) }
|
26
|
+
@meter_changes.keys.each {|moff| moffs.add(moff) }
|
27
|
+
@parts.values.each do |part|
|
28
|
+
part.dynamic_changes.each {|moff,change| moffs += change.offsets(moff) }
|
29
|
+
end
|
30
|
+
moffs += @program.segments.map {|seg| [seg.first, seg.last] }.flatten
|
31
|
+
return moffs.sort
|
32
|
+
end
|
33
|
+
|
34
|
+
def beat_durations
|
35
|
+
bdurs = @meter_changes.map do |offset,change|
|
36
|
+
[ offset, change.end_value.beat_duration ]
|
37
|
+
end.sort
|
38
|
+
|
39
|
+
if bdurs.empty? || bdurs[0][0] != 0
|
40
|
+
bdurs.unshift([0.to_r,@start_meter.beat_duration])
|
41
|
+
end
|
42
|
+
|
43
|
+
return Hash[ bdurs ]
|
44
|
+
end
|
45
|
+
|
46
|
+
def measure_durations
|
47
|
+
mdurs = @meter_changes.map do |offset,change|
|
48
|
+
[ offset, change.end_value.measure_duration ]
|
49
|
+
end.sort
|
50
|
+
|
51
|
+
if mdurs.empty? || mdurs[0][0] != 0
|
52
|
+
mdurs.unshift([0.to_r,@start_meter.measure_duration])
|
53
|
+
end
|
54
|
+
|
55
|
+
return Hash[ mdurs ]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Musicality
|
2
|
+
|
3
|
+
class ScoreConverter
|
4
|
+
def self.convert_changes changes, offset_map
|
5
|
+
Hash[ changes.map do |off,change|
|
6
|
+
[ offset_map[off], change.remap(off,offset_map) ]
|
7
|
+
end ]
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.convert_parts parts, offset_map
|
11
|
+
Hash[ parts.map do |name,part|
|
12
|
+
offset = 0.to_r
|
13
|
+
new_notes = part.notes.map do |note|
|
14
|
+
starttime = offset_map[offset]
|
15
|
+
endtime = offset_map[offset + note.duration]
|
16
|
+
offset += note.duration
|
17
|
+
newnote = note.clone
|
18
|
+
newnote.duration = endtime - starttime
|
19
|
+
newnote
|
20
|
+
end
|
21
|
+
new_dcs = convert_changes(part.dynamic_changes, offset_map)
|
22
|
+
[name, Part.new(part.start_dynamic,
|
23
|
+
notes: new_notes, dynamic_changes: new_dcs)]
|
24
|
+
end]
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.convert_program program, offset_map
|
28
|
+
Program.new(program.segments.map do |segment|
|
29
|
+
offset_map[segment.first]...offset_map[segment.last]
|
30
|
+
end)
|
31
|
+
end
|
32
|
+
|
33
|
+
class TempoBased
|
34
|
+
def initialize parts, program, note_time_converter
|
35
|
+
@parts = parts
|
36
|
+
@program = program
|
37
|
+
@note_time_map = note_time_converter.note_time_map(note_offsets)
|
38
|
+
end
|
39
|
+
|
40
|
+
def note_offsets
|
41
|
+
noffs = Set.new([0.to_r])
|
42
|
+
@parts.values.each do |part|
|
43
|
+
noff = 0.to_r
|
44
|
+
part.notes.each {|note| noffs.add(noff += note.duration) }
|
45
|
+
part.dynamic_changes.each {|noff2,change| noffs += change.offsets(noff2) }
|
46
|
+
end
|
47
|
+
noffs += @program.segments.map {|seg| [seg.first, seg.last] }.flatten
|
48
|
+
return noffs.sort
|
49
|
+
end
|
50
|
+
|
51
|
+
def convert_score
|
52
|
+
Score::Timed.new(parts: convert_parts, program: convert_program)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Convert note-based offsets & durations to time-based.
|
56
|
+
def convert_parts
|
57
|
+
ScoreConverter.convert_parts(@parts, @note_time_map)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Convert note-based offsets & durations to time-based.
|
61
|
+
def convert_program
|
62
|
+
ScoreConverter.convert_program(@program, @note_time_map)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Converts unmeasured score to timed score, by converting note-based offsets
|
67
|
+
# and durations to time-based, and eliminating the use of tempo.
|
68
|
+
class Unmeasured < TempoBased
|
69
|
+
def initialize score, tempo_sample_rate
|
70
|
+
if score.invalid?
|
71
|
+
raise NotValidError, "Errors detected given score: #{score.errors}"
|
72
|
+
end
|
73
|
+
tempo_computer = ValueComputer.new(score.start_tempo, score.tempo_changes)
|
74
|
+
ntc = NoteTimeConverter::Unmeasured.new(tempo_computer, tempo_sample_rate)
|
75
|
+
super(score.parts, score.program, ntc)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Converts measured score to timed score, by converting note-based offsets
|
80
|
+
# and durations to time-based, and eliminating the use of tempo and meters.
|
81
|
+
class Measured < TempoBased
|
82
|
+
def initialize score, tempo_sample_rate
|
83
|
+
if score.invalid?
|
84
|
+
raise NotValidError, "Errors detected given score: #{score.errors}"
|
85
|
+
end
|
86
|
+
mn_map = score.measure_note_map
|
87
|
+
new_parts = Hash[ score.parts.map do |name,part|
|
88
|
+
new_dcs = ScoreConverter.convert_changes(part.dynamic_changes, mn_map)
|
89
|
+
new_notes = part.notes.map {|n| n.clone } # note duration is already note-based
|
90
|
+
[name, Part.new(part.start_dynamic, notes: new_notes, dynamic_changes: new_dcs)]
|
91
|
+
end]
|
92
|
+
new_program = ScoreConverter.convert_program(score.program, mn_map)
|
93
|
+
new_tempo_changes = ScoreConverter.convert_changes(score.tempo_changes, mn_map)
|
94
|
+
new_beat_durations = Hash[ score.beat_durations.map do |moff,bdur|
|
95
|
+
[mn_map[moff], Change::Immediate.new(bdur) ]
|
96
|
+
end]
|
97
|
+
tempo_computer = ValueComputer.new(score.start_tempo, new_tempo_changes)
|
98
|
+
bdur_computer = ValueComputer.new(score.start_meter.beat_duration, new_beat_durations)
|
99
|
+
ntc = NoteTimeConverter::Measured.new(tempo_computer, bdur_computer, tempo_sample_rate)
|
100
|
+
super(new_parts, new_program, ntc)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
@@ -1,60 +1,130 @@
|
|
1
1
|
module Musicality
|
2
2
|
|
3
3
|
class Change
|
4
|
-
attr_reader :
|
4
|
+
attr_reader :end_value
|
5
5
|
|
6
|
-
def initialize
|
7
|
-
@
|
8
|
-
@duration = duration
|
6
|
+
def initialize end_value
|
7
|
+
@end_value = end_value
|
9
8
|
end
|
10
9
|
|
11
10
|
def ==(other)
|
12
11
|
self.class == other.class &&
|
13
|
-
self.
|
14
|
-
self.duration == other.duration
|
12
|
+
self.end_value == other.end_value
|
15
13
|
end
|
16
|
-
|
14
|
+
|
17
15
|
class Immediate < Change
|
18
16
|
def initialize value
|
19
|
-
super(value
|
17
|
+
super(value)
|
20
18
|
end
|
21
19
|
|
22
20
|
def clone
|
23
|
-
Immediate.new(@
|
21
|
+
Immediate.new(@end_value)
|
24
22
|
end
|
23
|
+
|
24
|
+
def duration; 0; end
|
25
25
|
end
|
26
26
|
|
27
27
|
class Gradual < Change
|
28
|
-
|
28
|
+
LINEAR = :linear
|
29
|
+
SIGMOID = :sigmoid
|
30
|
+
TRANSITIONS = [LINEAR,SIGMOID]
|
31
|
+
|
32
|
+
def self.linear end_value, duration, start_value: nil
|
33
|
+
Gradual.new(end_value, duration, LINEAR, start_value: start_value)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.sigmoid end_value, duration, start_value: nil
|
37
|
+
Gradual.new(end_value, duration, SIGMOID, start_value: start_value)
|
38
|
+
end
|
29
39
|
|
30
|
-
|
31
|
-
|
32
|
-
|
40
|
+
attr_reader :duration, :transition, :start_value
|
41
|
+
def initialize end_value, duration, transition, start_value: nil
|
42
|
+
if duration <= 0
|
43
|
+
raise NonPositiveError, "duration (#{duration}) is <= 0"
|
44
|
+
end
|
45
|
+
|
46
|
+
unless TRANSITIONS.include?(transition)
|
47
|
+
raise ArgumentError, "transition (#{transition}) is not supported"
|
33
48
|
end
|
34
49
|
|
35
|
-
|
36
|
-
|
50
|
+
@duration = duration
|
51
|
+
@transition = transition
|
52
|
+
@start_value = start_value
|
53
|
+
super(end_value)
|
54
|
+
end
|
55
|
+
|
56
|
+
def ==(other)
|
57
|
+
super(other) && @duration == other.duration &&
|
58
|
+
@transition == other.transition &&
|
59
|
+
@start_value == other.start_value
|
60
|
+
end
|
61
|
+
|
62
|
+
def clone; Gradual.new(@end_value, @duration, @transition); end
|
63
|
+
def relative?; @start_value.nil?; end
|
64
|
+
def absolute?; !@start_value.nil?; end
|
65
|
+
|
66
|
+
class Trimmed < Gradual
|
67
|
+
attr_reader :preceding, :remaining
|
68
|
+
|
69
|
+
def self.linear end_value, duration, start_value: nil, preceding: 0, remaining: 0
|
70
|
+
Trimmed.new(end_value, duration, LINEAR, start_value: start_value,
|
71
|
+
preceding: preceding, remaining: remaining)
|
37
72
|
end
|
38
73
|
|
39
|
-
|
40
|
-
|
74
|
+
def self.sigmoid end_value, duration, start_value: nil, preceding: 0, remaining: 0
|
75
|
+
Trimmed.new(end_value, duration, SIGMOID, start_value: start_value,
|
76
|
+
preceding: preceding, remaining: remaining)
|
41
77
|
end
|
42
78
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
79
|
+
def initialize end_value, duration, transition, start_value: nil, preceding: 0, remaining: 0
|
80
|
+
if preceding < 0
|
81
|
+
raise NegativeError, "preceding (#{preceding}) is < 0"
|
82
|
+
end
|
83
|
+
|
84
|
+
if remaining <= 0
|
85
|
+
raise NonPositiveError, "remaining (#{remaining}) is <= 0"
|
86
|
+
end
|
87
|
+
|
88
|
+
@preceding, @remaining = preceding, remaining
|
89
|
+
super(end_value, duration, transition, start_value: start_value)
|
90
|
+
end
|
91
|
+
|
92
|
+
def trailing
|
93
|
+
@duration - @preceding - @remaining
|
94
|
+
end
|
95
|
+
|
96
|
+
def untrim
|
97
|
+
Gradual.new(@end_value, @duration, @transition, start_value: @start_value)
|
98
|
+
end
|
99
|
+
|
100
|
+
def ==(other)
|
101
|
+
super(other) && @preceding == other.preceding && @remaining == other.remaining
|
102
|
+
end
|
103
|
+
|
104
|
+
def clone
|
105
|
+
Trimmed.new(@end_value, @duration, @transition, start_value: @start_value,
|
106
|
+
preceding: @preceding, remaining: @remaining)
|
107
|
+
end
|
48
108
|
end
|
49
109
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
53
|
-
@remaining == other.remaining
|
110
|
+
def trim_left(amount)
|
111
|
+
Trimmed.new(@end_value, @duration, @transition, start_value: @start_value,
|
112
|
+
preceding: amount, remaining: (@duration - amount))
|
54
113
|
end
|
55
114
|
|
56
|
-
def
|
57
|
-
|
115
|
+
def trim_right(amount)
|
116
|
+
Trimmed.new(@end_value, @duration, @transition, start_value: @start_value,
|
117
|
+
preceding: 0, remaining: (@duration - amount))
|
118
|
+
end
|
119
|
+
|
120
|
+
def trim(ltrim, rtrim)
|
121
|
+
Trimmed.new(@end_value, @duration, @transition, start_value: @start_value,
|
122
|
+
preceding: ltrim, remaining: (@duration - ltrim - rtrim))
|
123
|
+
end
|
124
|
+
|
125
|
+
def to_trimmed(preceding, remaining)
|
126
|
+
Trimmed.new(@end_value, @duration, @transition, start_value: @start_value,
|
127
|
+
preceding: preceding, remaining: remaining)
|
58
128
|
end
|
59
129
|
end
|
60
130
|
end
|
@@ -44,7 +44,7 @@ class Part
|
|
44
44
|
end
|
45
45
|
|
46
46
|
def ensure_dynamic_change_values_range
|
47
|
-
outofrange = @dynamic_changes.values.select {|v| !v.
|
47
|
+
outofrange = @dynamic_changes.values.select {|v| !v.end_value.between?(0,1) }
|
48
48
|
if outofrange.any?
|
49
49
|
raise RangeError, "dynamic change values #{outofrange} are not between 0 and 1"
|
50
50
|
end
|
@@ -67,7 +67,7 @@ class Score
|
|
67
67
|
end
|
68
68
|
|
69
69
|
def check_tempo_changes
|
70
|
-
badvalues = @tempo_changes.select {|k,v| v.
|
70
|
+
badvalues = @tempo_changes.select {|k,v| v.end_value <= 0 }
|
71
71
|
if badvalues.any?
|
72
72
|
raise NonPositiveError, "tempo changes (#{badvalues}) are not positive"
|
73
73
|
end
|
@@ -99,7 +99,7 @@ class Score
|
|
99
99
|
end
|
100
100
|
|
101
101
|
def validatables
|
102
|
-
super() + [ @start_meter ] + @meter_changes.values.map {|v| v.
|
102
|
+
super() + [ @start_meter ] + @meter_changes.values.map {|v| v.end_value}
|
103
103
|
end
|
104
104
|
|
105
105
|
def check_startmeter_type
|
@@ -109,7 +109,7 @@ class Score
|
|
109
109
|
end
|
110
110
|
|
111
111
|
def check_meterchange_types
|
112
|
-
badtypes = @meter_changes.select {|k,v| !v.
|
112
|
+
badtypes = @meter_changes.select {|k,v| !v.end_value.is_a?(Meter) }
|
113
113
|
if badtypes.any?
|
114
114
|
raise TypeError, "meter change values #{nonmeter_values} are not Meter objects"
|
115
115
|
end
|
@@ -140,7 +140,7 @@ class Score
|
|
140
140
|
moff_prev, mdur_prev = 0.to_r, @start_meter.measure_duration
|
141
141
|
|
142
142
|
@meter_changes.sort.each do |moff,change|
|
143
|
-
mdur = change.
|
143
|
+
mdur = change.end_value.measure_duration
|
144
144
|
notes_elapsed = mdur_prev * (moff - moff_prev)
|
145
145
|
noff = noff_prev + notes_elapsed
|
146
146
|
|
@@ -2,45 +2,55 @@ module Musicality
|
|
2
2
|
|
3
3
|
class Change
|
4
4
|
class Immediate < Change
|
5
|
-
def
|
6
|
-
|
5
|
+
def change_type_str; "Immediate"; end
|
6
|
+
def pack with: nil
|
7
|
+
super(:with => with)
|
7
8
|
end
|
8
9
|
|
9
|
-
def self.unpack
|
10
|
-
|
10
|
+
def self.unpack(packing, with: nil)
|
11
|
+
end_val = packing["end_value"]
|
12
|
+
new(with.nil? ? end_val : end_val.send(with))
|
11
13
|
end
|
12
14
|
end
|
13
15
|
|
14
16
|
class Gradual < Change
|
15
|
-
def
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
packing["elapsed"] = @elapsed
|
17
|
+
def change_type_str; "Gradual"; end
|
18
|
+
def pack with: nil
|
19
|
+
packing = super(:with => with)
|
20
|
+
packing.merge!("duration" => @duration, "transition" => @transition)
|
21
|
+
unless @start_value.nil?
|
22
|
+
packing["start_value"] = with.nil? ? @start_value : @start_value.send(with)
|
22
23
|
end
|
23
24
|
return packing
|
24
25
|
end
|
26
|
+
def self.unpack packing, with: nil
|
27
|
+
start_val, end_val = packing["start_value"], packing["end_value"]
|
28
|
+
new(with.nil? ? end_val : end_val.send(with),
|
29
|
+
packing["duration"], packing["transition"],
|
30
|
+
start_value: (start_val.nil? || with.nil?) ? start_val : start_val.send(with))
|
31
|
+
end
|
25
32
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
33
|
+
class Trimmed < Gradual
|
34
|
+
def change_type_str; "Gradual::Trimmed"; end
|
35
|
+
def pack with: nil
|
36
|
+
super(:with => with).merge("preceding" => @preceding, "remaining" => @remaining)
|
37
|
+
end
|
38
|
+
def self.unpack packing, with: nil
|
39
|
+
Gradual.unpack(packing, :with => with).to_trimmed(
|
40
|
+
packing["preceding"], packing["remaining"])
|
41
|
+
end
|
30
42
|
end
|
31
43
|
end
|
32
44
|
|
33
|
-
def
|
34
|
-
|
35
|
-
|
36
|
-
end
|
37
|
-
|
38
|
-
private
|
39
|
-
|
40
|
-
def pack_common
|
41
|
-
{ "type" => self.class.to_s.split("::")[-1],
|
42
|
-
"value" => @value }
|
45
|
+
def pack with: nil
|
46
|
+
{ "end_value" => (with.nil? ? @end_value : @end_value.send(with)),
|
47
|
+
"type" => self.change_type_str }
|
43
48
|
end
|
49
|
+
|
50
|
+
def self.unpack packing, with: nil
|
51
|
+
type = const_get(packing["type"])
|
52
|
+
type.unpack(packing, with: with)
|
53
|
+
end
|
44
54
|
end
|
45
55
|
|
46
56
|
end
|
@@ -53,7 +53,7 @@ class Score
|
|
53
53
|
def pack
|
54
54
|
return super().merge("start_meter" => start_meter.to_s,
|
55
55
|
"meter_changes" => Hash[ meter_changes.map do |off,change|
|
56
|
-
[off,change.pack
|
56
|
+
[off,change.pack(:with => :to_s)]
|
57
57
|
end ]
|
58
58
|
)
|
59
59
|
end
|
@@ -62,7 +62,7 @@ class Score
|
|
62
62
|
score = superclass.unpack(packing)
|
63
63
|
unpacked_start_meter = Meter.parse(packing["start_meter"])
|
64
64
|
unpacked_mcs = Hash[ packing["meter_changes"].map do |off,p|
|
65
|
-
|
65
|
+
[off, Change.unpack(p, :with => :to_meter) ]
|
66
66
|
end ]
|
67
67
|
|
68
68
|
new(unpacked_start_meter, score.start_tempo,
|