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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f9413fe250183b04ea15a863a580cd90887ccb5e
4
- data.tar.gz: d2820bd7695d490fb139508d663b202b2d867202
3
+ metadata.gz: 1f460185c537892ef1e12ffce5b1c829095666c7
4
+ data.tar.gz: fc1512fe588da6ee7599aea61ba713d268c50cc3
5
5
  SHA512:
6
- metadata.gz: ffb85df1738b9948c0d23c627be1a42b195ce438b9098575ec9d662b1383f9689b7c9bc57cd432498bd9dd87306d4ddb5f8c4ad481502c3d3d1dfa664fe7bfca
7
- data.tar.gz: f4e9a0f14009a2a50b3cf377504283cca9e5858bce3335649a80bb99021f386ed6203b62f88ea935541976bddfedb81bf766f5ffb8648ca9e1b1fe67f3236eeb
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
+
@@ -6,4 +6,5 @@ module Musicality
6
6
  class NonRationalError < StandardError; end
7
7
  class NonIncreasingError < StandardError; end
8
8
  class NotValidError < StandardError; end
9
+ class DomainError < StandardError; end
9
10
  end
@@ -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
- initial = base_offset - @elapsed
13
- final = initial + @total_duration
14
- [ initial, base_offset, base_offset + @impending, final ]
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 tempo_computer, sample_rate
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
- def notes_per_second_at offset
15
- Tempo::QNPM.to_nps(@tempo_computer.value_at offset)
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 :value, :duration
4
+ attr_reader :end_value
5
5
 
6
- def initialize value, duration
7
- @value = value
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.value == other.value &&
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,0)
17
+ super(value)
20
18
  end
21
19
 
22
20
  def clone
23
- Immediate.new(@value)
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
- attr_reader :elapsed, :impending, :remaining, :total_duration
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
- def initialize value, impending, elapsed=0, remaining=0
31
- if elapsed < 0
32
- raise NegativeError, "elapsed (#{elapsed}) is < 0"
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
- if impending <= 0
36
- raise NonPositiveError, "impending (#{impending}) is <= 0"
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
- if remaining < 0
40
- raise NegativeError, "remaining #{remaining} is < 0"
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
- @total_duration = elapsed + impending + remaining
44
- @elapsed = elapsed
45
- @impending = impending
46
- @remaining = remaining
47
- super(value,impending)
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 ==(other)
51
- super(other) &&
52
- @elapsed == other.elapsed &&
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 clone
57
- Gradual.new(@value, @impending, @elapsed, @remaining)
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.value.between?(0,1) }
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.value <= 0 }
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.value}
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.value.is_a?(Meter) }
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.value.measure_duration
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 pack
6
- pack_common
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 packing
10
- new(packing["value"])
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 pack
16
- packing = pack_common.merge("impending" => @impending)
17
- unless @remaining == 0
18
- packing["remaining"] = @remaining
19
- end
20
- unless @elapsed == 0
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
- def self.unpack packing
27
- elapsed = packing["elapsed"] || 0
28
- remaining = packing["remaining"] || 0
29
- new(packing["value"], packing["impending"], elapsed, remaining)
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 self.unpack packing
34
- type = const_get(packing["type"])
35
- type.unpack(packing)
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.merge("value" => change.value.to_s)]
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
- [off, Change.unpack(p.merge("value" => Meter.parse(p["value"]))) ]
65
+ [off, Change.unpack(p, :with => :to_meter) ]
66
66
  end ]
67
67
 
68
68
  new(unpacked_start_meter, score.start_tempo,