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