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