musicality 0.1.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 (141) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +47 -0
  8. data/Rakefile +65 -0
  9. data/bin/midify +78 -0
  10. data/examples/hip.rb +32 -0
  11. data/examples/missed_connection.rb +26 -0
  12. data/examples/song1.rb +33 -0
  13. data/examples/song2.rb +32 -0
  14. data/lib/musicality/errors.rb +9 -0
  15. data/lib/musicality/notation/conversion/change_conversion.rb +19 -0
  16. data/lib/musicality/notation/conversion/measure_note_map.rb +40 -0
  17. data/lib/musicality/notation/conversion/measured_score_conversion.rb +70 -0
  18. data/lib/musicality/notation/conversion/measured_score_converter.rb +95 -0
  19. data/lib/musicality/notation/conversion/note_time_converter.rb +68 -0
  20. data/lib/musicality/notation/conversion/tempo_conversion.rb +25 -0
  21. data/lib/musicality/notation/conversion/unmeasured_score_conversion.rb +47 -0
  22. data/lib/musicality/notation/conversion/unmeasured_score_converter.rb +64 -0
  23. data/lib/musicality/notation/model/articulations.rb +13 -0
  24. data/lib/musicality/notation/model/change.rb +62 -0
  25. data/lib/musicality/notation/model/dynamics.rb +12 -0
  26. data/lib/musicality/notation/model/link.rb +73 -0
  27. data/lib/musicality/notation/model/meter.rb +54 -0
  28. data/lib/musicality/notation/model/meters.rb +9 -0
  29. data/lib/musicality/notation/model/note.rb +120 -0
  30. data/lib/musicality/notation/model/part.rb +54 -0
  31. data/lib/musicality/notation/model/pitch.rb +163 -0
  32. data/lib/musicality/notation/model/pitches.rb +21 -0
  33. data/lib/musicality/notation/model/program.rb +53 -0
  34. data/lib/musicality/notation/model/score.rb +132 -0
  35. data/lib/musicality/notation/packing/change_packing.rb +46 -0
  36. data/lib/musicality/notation/packing/part_packing.rb +31 -0
  37. data/lib/musicality/notation/packing/program_packing.rb +16 -0
  38. data/lib/musicality/notation/packing/score_packing.rb +108 -0
  39. data/lib/musicality/notation/parsing/articulation_parsing.rb +264 -0
  40. data/lib/musicality/notation/parsing/articulation_parsing.treetop +59 -0
  41. data/lib/musicality/notation/parsing/convenience_methods.rb +74 -0
  42. data/lib/musicality/notation/parsing/duration_nodes.rb +21 -0
  43. data/lib/musicality/notation/parsing/duration_parsing.rb +205 -0
  44. data/lib/musicality/notation/parsing/duration_parsing.treetop +25 -0
  45. data/lib/musicality/notation/parsing/link_nodes.rb +35 -0
  46. data/lib/musicality/notation/parsing/link_parsing.rb +270 -0
  47. data/lib/musicality/notation/parsing/link_parsing.treetop +33 -0
  48. data/lib/musicality/notation/parsing/meter_parsing.rb +190 -0
  49. data/lib/musicality/notation/parsing/meter_parsing.treetop +29 -0
  50. data/lib/musicality/notation/parsing/note_node.rb +40 -0
  51. data/lib/musicality/notation/parsing/note_parsing.rb +229 -0
  52. data/lib/musicality/notation/parsing/note_parsing.treetop +28 -0
  53. data/lib/musicality/notation/parsing/numbers/nonnegative_float_parsing.rb +289 -0
  54. data/lib/musicality/notation/parsing/numbers/nonnegative_float_parsing.treetop +29 -0
  55. data/lib/musicality/notation/parsing/numbers/nonnegative_integer_parsing.rb +64 -0
  56. data/lib/musicality/notation/parsing/numbers/nonnegative_integer_parsing.treetop +17 -0
  57. data/lib/musicality/notation/parsing/numbers/nonnegative_rational_parsing.rb +86 -0
  58. data/lib/musicality/notation/parsing/numbers/nonnegative_rational_parsing.treetop +20 -0
  59. data/lib/musicality/notation/parsing/numbers/positive_float_parsing.rb +503 -0
  60. data/lib/musicality/notation/parsing/numbers/positive_float_parsing.treetop +33 -0
  61. data/lib/musicality/notation/parsing/numbers/positive_integer_parsing.rb +95 -0
  62. data/lib/musicality/notation/parsing/numbers/positive_integer_parsing.treetop +19 -0
  63. data/lib/musicality/notation/parsing/numbers/positive_rational_parsing.rb +84 -0
  64. data/lib/musicality/notation/parsing/numbers/positive_rational_parsing.treetop +19 -0
  65. data/lib/musicality/notation/parsing/parseable.rb +30 -0
  66. data/lib/musicality/notation/parsing/pitch_node.rb +23 -0
  67. data/lib/musicality/notation/parsing/pitch_parsing.rb +448 -0
  68. data/lib/musicality/notation/parsing/pitch_parsing.treetop +52 -0
  69. data/lib/musicality/notation/parsing/segment_parsing.rb +141 -0
  70. data/lib/musicality/notation/parsing/segment_parsing.treetop +23 -0
  71. data/lib/musicality/notation/util/interpolation.rb +16 -0
  72. data/lib/musicality/notation/util/piecewise_function.rb +122 -0
  73. data/lib/musicality/notation/util/value_computer.rb +170 -0
  74. data/lib/musicality/performance/conversion/glissando_converter.rb +34 -0
  75. data/lib/musicality/performance/conversion/note_sequence_extractor.rb +98 -0
  76. data/lib/musicality/performance/conversion/portamento_converter.rb +24 -0
  77. data/lib/musicality/performance/conversion/score_collator.rb +126 -0
  78. data/lib/musicality/performance/midi/midi_events.rb +34 -0
  79. data/lib/musicality/performance/midi/midi_util.rb +31 -0
  80. data/lib/musicality/performance/midi/part_sequencer.rb +123 -0
  81. data/lib/musicality/performance/midi/score_sequencer.rb +45 -0
  82. data/lib/musicality/performance/model/note_attacks.rb +19 -0
  83. data/lib/musicality/performance/model/note_sequence.rb +111 -0
  84. data/lib/musicality/performance/util/note_linker.rb +28 -0
  85. data/lib/musicality/performance/util/optimization.rb +31 -0
  86. data/lib/musicality/validatable.rb +38 -0
  87. data/lib/musicality/version.rb +3 -0
  88. data/lib/musicality.rb +81 -0
  89. data/musicality.gemspec +30 -0
  90. data/spec/musicality_spec.rb +7 -0
  91. data/spec/notation/conversion/change_conversion_spec.rb +40 -0
  92. data/spec/notation/conversion/measure_note_map_spec.rb +73 -0
  93. data/spec/notation/conversion/measured_score_conversion_spec.rb +141 -0
  94. data/spec/notation/conversion/measured_score_converter_spec.rb +329 -0
  95. data/spec/notation/conversion/note_time_converter_spec.rb +81 -0
  96. data/spec/notation/conversion/tempo_conversion_spec.rb +40 -0
  97. data/spec/notation/conversion/unmeasured_score_conversion_spec.rb +71 -0
  98. data/spec/notation/conversion/unmeasured_score_converter_spec.rb +116 -0
  99. data/spec/notation/model/change_spec.rb +90 -0
  100. data/spec/notation/model/link_spec.rb +83 -0
  101. data/spec/notation/model/meter_spec.rb +97 -0
  102. data/spec/notation/model/note_spec.rb +183 -0
  103. data/spec/notation/model/part_spec.rb +69 -0
  104. data/spec/notation/model/pitch_spec.rb +180 -0
  105. data/spec/notation/model/program_spec.rb +50 -0
  106. data/spec/notation/model/score_spec.rb +211 -0
  107. data/spec/notation/packing/change_packing_spec.rb +153 -0
  108. data/spec/notation/packing/part_packing_spec.rb +66 -0
  109. data/spec/notation/packing/program_packing_spec.rb +33 -0
  110. data/spec/notation/packing/score_packing_spec.rb +301 -0
  111. data/spec/notation/parsing/articulation_parsing_spec.rb +23 -0
  112. data/spec/notation/parsing/convenience_methods_spec.rb +99 -0
  113. data/spec/notation/parsing/duration_nodes_spec.rb +83 -0
  114. data/spec/notation/parsing/duration_parsing_spec.rb +70 -0
  115. data/spec/notation/parsing/link_nodes_spec.rb +30 -0
  116. data/spec/notation/parsing/link_parsing_spec.rb +13 -0
  117. data/spec/notation/parsing/meter_parsing_spec.rb +23 -0
  118. data/spec/notation/parsing/note_node_spec.rb +87 -0
  119. data/spec/notation/parsing/note_parsing_spec.rb +46 -0
  120. data/spec/notation/parsing/numbers/nonnegative_float_spec.rb +28 -0
  121. data/spec/notation/parsing/numbers/nonnegative_integer_spec.rb +11 -0
  122. data/spec/notation/parsing/numbers/nonnegative_rational_spec.rb +11 -0
  123. data/spec/notation/parsing/numbers/positive_float_spec.rb +28 -0
  124. data/spec/notation/parsing/numbers/positive_integer_spec.rb +28 -0
  125. data/spec/notation/parsing/numbers/positive_rational_spec.rb +28 -0
  126. data/spec/notation/parsing/pitch_node_spec.rb +38 -0
  127. data/spec/notation/parsing/pitch_parsing_spec.rb +14 -0
  128. data/spec/notation/parsing/segment_parsing_spec.rb +27 -0
  129. data/spec/notation/util/value_computer_spec.rb +146 -0
  130. data/spec/performance/conversion/glissando_converter_spec.rb +93 -0
  131. data/spec/performance/conversion/note_sequence_extractor_spec.rb +230 -0
  132. data/spec/performance/conversion/portamento_converter_spec.rb +91 -0
  133. data/spec/performance/conversion/score_collator_spec.rb +183 -0
  134. data/spec/performance/midi/midi_util_spec.rb +110 -0
  135. data/spec/performance/midi/part_sequencer_spec.rb +40 -0
  136. data/spec/performance/midi/score_sequencer_spec.rb +50 -0
  137. data/spec/performance/model/note_sequence_spec.rb +147 -0
  138. data/spec/performance/util/note_linker_spec.rb +68 -0
  139. data/spec/performance/util/optimization_spec.rb +73 -0
  140. data/spec/spec_helper.rb +43 -0
  141. metadata +323 -0
@@ -0,0 +1,23 @@
1
+ module Musicality
2
+ module Parsing
3
+
4
+ grammar Segment
5
+ include NonnegativeInteger
6
+ include NonnegativeFloat
7
+ include NonnegativeRational
8
+
9
+ rule range
10
+ first:nonnegative_number ([.] 2..3) last:nonnegative_number {
11
+ def to_range
12
+ first.to_num...last.to_num
13
+ end
14
+ }
15
+ end
16
+
17
+ rule nonnegative_number
18
+ nonnegative_float / nonnegative_rational / nonnegative_integer
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ module Musicality
2
+
3
+ module Interpolation
4
+ # Linear interpolation
5
+ # Given 2 sample points, interpolates a value anywhere between the two points.
6
+ #
7
+ # @param [Numeric] y0 First (left) y-value
8
+ # @param [Numeric] y1 Second (right) y-value
9
+ # @param [Numeric] x Percent distance (along the x-axis) between the two y-values
10
+ def self.linear y0, y1, x
11
+ raise ArgumentError, "x is not between 0.0 and 1.0" unless x.between?(0.0,1.0)
12
+ return y0 + x * (y1 - y0)
13
+ end
14
+ end
15
+
16
+ end
@@ -0,0 +1,122 @@
1
+ module Musicality
2
+
3
+ # Combine functions that are each applicable for a non-overlapping domain.
4
+ #
5
+ # @author James Tunnell
6
+ class PiecewiseFunction
7
+ attr_reader :pieces
8
+
9
+ # Take an array of points (each point is a two-element array pair) and
10
+ # create a piecewise function to calculate values in-between.
11
+ def initialize points = []
12
+ @pieces = { }
13
+
14
+ points = points.sort_by {|p| p[0]}
15
+
16
+ if points.count > 1
17
+ if points.is_a?(Hash)
18
+ points = points.to_a
19
+ end
20
+
21
+ for i in 1...points.count
22
+ add_points points[i-1], points[i]
23
+ end
24
+ end
25
+ end
26
+
27
+ def add_points prev_point, point
28
+ domain = prev_point[0]..point[0]
29
+ func = lambda do |x|
30
+ perc = (x - domain.min).to_f / (domain.max - domain.min)
31
+ y = Interpolation.linear prev_point[1], point[1], perc
32
+ return y
33
+ end
34
+ add_piece(domain, func)
35
+ end
36
+
37
+ # Add a function piece, which covers the given domain (includes domain start
38
+ # but not the end).
39
+ # @param [Range] domain The function domain. If this overlaps an existing domain,
40
+ # the existing domain will be split with the non-
41
+ # overlapping pieces kept and the overlapping old piece
42
+ # discarded.
43
+ def add_piece domain, func
44
+
45
+ raise ArgumentError, "domain is not a Range" if !domain.is_a? Range
46
+ raise ArgumentError, "func is not a Proc" if !func.is_a? Proc
47
+
48
+ contains_domain_completely = @pieces.select { |d,f| d.include?(domain.begin) && d.include?(domain.end) }
49
+ if contains_domain_completely.any?
50
+ contains_domain_completely.each do |d,f|
51
+ l = d.begin...domain.begin
52
+ if d.exclude_end?
53
+ r = domain.end...d.end
54
+ else
55
+ r = domain.end..d.end
56
+ end
57
+
58
+ @pieces.delete d
59
+
60
+ if domain.begin != d.begin
61
+ @pieces[l] = f
62
+ end
63
+ if domain.end == d.end
64
+ @pieces[domain.begin..domain.end] = func
65
+ else
66
+ @pieces[domain.begin...domain.end] = func
67
+ @pieces[r] = f
68
+ end
69
+ end
70
+ else
71
+ delete_completely = @pieces.select { |d,f| domain.include?(d.begin) && domain.include?(d.end) }
72
+ delete_completely.each do |d,f|
73
+ @pieces.delete d
74
+ end
75
+
76
+ # should only be one
77
+ move_end = @pieces.select { |d,f| domain.include?(d.end) }
78
+ move_end.each do |d,f|
79
+ @pieces.delete d
80
+ @pieces[d.begin...domain.begin] = f
81
+ end
82
+
83
+ # should only be one
84
+ move_begin = @pieces.select { |d,f| domain.include?(d.begin) }
85
+ move_begin.each do |d,f|
86
+ @pieces.delete d
87
+ if d.exclude_end?
88
+ @pieces[domain.end...d.end] = f
89
+ else
90
+ @pieces[domain.end..d.end] = f
91
+ end
92
+ end
93
+
94
+ if move_begin.any?
95
+ @pieces[domain.begin...domain.end] = func
96
+ else
97
+ @pieces[domain] = func
98
+ end
99
+ end
100
+ end
101
+
102
+ # Evaluate the piecewise function by finding a function piece whose domain
103
+ # includes the given independent value.
104
+ def eval x
105
+ y = nil
106
+
107
+ @pieces.each do |domain, func|
108
+ if domain.include? x
109
+ y = func.call x
110
+ break
111
+ end
112
+ end
113
+
114
+ if y.nil?
115
+ raise ArgumentError, "The input #{x} is not in the domain."
116
+ end
117
+
118
+ return y
119
+ end
120
+ end
121
+
122
+ end
@@ -0,0 +1,170 @@
1
+ module Musicality
2
+
3
+ # Given a start value, and value changes, compute the value at any offset.
4
+ class ValueComputer
5
+ attr_reader :piecewise_function
6
+
7
+ def initialize start_value, value_changes = {}
8
+ @piecewise_function = PiecewiseFunction.new
9
+ set_default_value start_value
10
+
11
+ if value_changes.any?
12
+ value_changes.sort.each do |offset,change|
13
+
14
+ case change
15
+ when Change::Immediate
16
+ add_immediate_change change, offset
17
+ when Change::Gradual
18
+ add_linear_change change, offset
19
+ # add_sigmoid_change change, offset
20
+ end
21
+
22
+ end
23
+ end
24
+ end
25
+
26
+ # Compute the value at the given offset.
27
+ # @param [Numeric] offset The given offset to compute value at.
28
+ def value_at offset
29
+ @piecewise_function.eval offset
30
+ end
31
+
32
+ def sample xmin, xmax, srate
33
+ sample_period = Rational(1,srate)
34
+ ((xmin.to_r)..(xmax.to_r)).step(sample_period).map do |x|
35
+ value_at(x)
36
+ end
37
+ end
38
+
39
+ # finds the minimum domain value
40
+ def domain_min
41
+ -Float::INFINITY
42
+ end
43
+
44
+ # finds the maximum domain value
45
+ def domain_max
46
+ Float::INFINITY
47
+ end
48
+
49
+ # finds the minimum domain value
50
+ def self.domain_min
51
+ -Float::INFINITY
52
+ end
53
+
54
+ # finds the maximum domain value
55
+ def self.domain_max
56
+ Float::INFINITY
57
+ end
58
+
59
+ private
60
+
61
+ def set_default_value value
62
+ func = lambda {|x| value }
63
+ @piecewise_function.add_piece( domain_min..domain_max, func )
64
+ end
65
+
66
+ # Add a function piece to the piecewise function, which will to compute value
67
+ # for a matching note offset. Transition duration will be ignored since the
68
+ # change is immediate.
69
+ #
70
+ # @param [ValueChange] value_change An event with information about the new value.
71
+ # @param [Numeric] offset
72
+ def add_immediate_change value_change, offset
73
+ func = nil
74
+ value = value_change.value
75
+ domain = offset..domain_max
76
+ func = lambda {|x| value }
77
+
78
+ @piecewise_function.add_piece domain, func
79
+ end
80
+
81
+ # Add a function piece to the piecewise function, which will to compute value
82
+ # for a matching note offset. If the dynamic event duration is non-zero, a
83
+ # linear transition function is created.
84
+ #
85
+ # @param [ValueChange] value_change An event with information about the new value.
86
+ # @param [Numeric] offset
87
+ def add_linear_change value_change, offset
88
+
89
+ func = nil
90
+ value = value_change.value
91
+ duration = value_change.duration
92
+ domain = offset..domain_max
93
+
94
+ if duration == 0
95
+ add_immediate_change(value_change, offset)
96
+ else
97
+ b = @piecewise_function.eval domain.first
98
+ m = (value.to_f - b.to_f) / duration.to_f
99
+
100
+ func = lambda do |x|
101
+ raise RangeError, "#{x} is not in the domain" if !domain.include?(x)
102
+
103
+ if x < (domain.first + duration)
104
+ (m * (x - domain.first)) + b
105
+ else
106
+ value
107
+ end
108
+ end
109
+ @piecewise_function.add_piece domain, func
110
+ end
111
+ end
112
+
113
+ # Add a function piece to the piecewise function, which will to compute value
114
+ # for a matching note offset. If the dynamic event duration is non-zero, a
115
+ # linear transition function is created.
116
+ #
117
+ # @param [ValueChange] value_change An event with information about the new value.
118
+ # @param [Numeric] offset
119
+ def add_sigmoid_change value_change, offset
120
+
121
+ func = nil
122
+ start_value = @piecewise_function.eval offset
123
+ end_value = value_change.value
124
+ value_diff = end_value - start_value
125
+ duration = value_change.duration
126
+ domain = offset.to_f..domain_max
127
+ abruptness = 0.7 # value_change.transition.abruptness.to_f
128
+
129
+ if duration == 0
130
+ add_immediate_change(value_change,offset)
131
+ else
132
+ raise ArgumentError, "abruptness is not between 0 and 1" unless abruptness.between?(0,1)
133
+
134
+ min_magn = 2
135
+ max_magn = 6
136
+ tanh_domain_magn = abruptness * (max_magn - min_magn) + min_magn
137
+ tanh_domain = -tanh_domain_magn..tanh_domain_magn
138
+
139
+ tanh_range = Math::tanh(tanh_domain.first)..Math::tanh(tanh_domain.last)
140
+ tanh_span = tanh_range.last - tanh_range.first
141
+
142
+ func = lambda do |x|
143
+ raise RangeError, "#{x} is not in the domain" if !domain.include?(x)
144
+ if x < (domain.first + duration)
145
+ start_domain = domain.first...(domain.first + duration)
146
+ x2 = transform_domains(start_domain, tanh_domain, x)
147
+ y = Math::tanh x2
148
+ z = (y / tanh_span) + 0.5 # ranges from 0 to 1
149
+ start_value + (z * value_diff)
150
+ else
151
+ end_value
152
+ end
153
+ end
154
+ @piecewise_function.add_piece domain, func
155
+ end
156
+ end
157
+
158
+ # x should be in the start domain
159
+ def transform_domains start_domain, end_domain, x
160
+ perc = (x - start_domain.first) / (start_domain.last - start_domain.first).to_f
161
+ x2 = perc * (end_domain.last - end_domain.first) + end_domain.first
162
+ end
163
+
164
+ # 0 to 1
165
+ def logistic x
166
+ 1.0 / (1 + Math::exp(-x))
167
+ end
168
+ end
169
+
170
+ end
@@ -0,0 +1,34 @@
1
+ module Musicality
2
+
3
+ class GlissandoConverter
4
+ def self.glissando_pitches(start_pitch, target_pitch)
5
+ start, finish = start_pitch.total_semitones, target_pitch.total_semitones
6
+ if finish >= start
7
+ semitones = start.ceil.upto(finish.floor).to_a
8
+ else
9
+ semitones = start.floor.downto(finish.ceil).to_a
10
+ end
11
+
12
+ if semitones.empty? || semitones[0] != start
13
+ semitones.unshift(start)
14
+ end
15
+
16
+ if semitones.size > 1 && semitones[-1] == finish
17
+ semitones.pop
18
+ end
19
+
20
+ semitones.map do |semitone|
21
+ Pitch.from_semitones(semitone)
22
+ end
23
+ end
24
+
25
+ def self.glissando_elements(start_pitch, target_pitch, duration, accented)
26
+ pitches = glissando_pitches(start_pitch, target_pitch)
27
+ subdur = Rational(duration, pitches.size)
28
+ pitches.map do |pitch|
29
+ LegatoElement.new(subdur, pitch, accented)
30
+ end
31
+ end
32
+ end
33
+
34
+ end
@@ -0,0 +1,98 @@
1
+ module Musicality
2
+
3
+ class NoteSequenceExtractor
4
+ # For all link types:
5
+ # - Remove link where source pitch is not in current note
6
+ # For tie:
7
+ # - Remove any bad tie (where the tying pitch does not exist in the next note).
8
+ # - Replace any good tie with a slur.
9
+ # For slur/legato:
10
+ # - Remove any bad link (where the target pitch does not exist in the next note).
11
+ # TODO: what to do about multiple links that target the same pitch?
12
+ def self.fixup_links note, next_note
13
+ note.links.each do |pitch, link|
14
+ if !note.pitches.include?(pitch)
15
+ note.links.delete pitch
16
+ elsif link.is_a? Link::Tie
17
+ if next_note.pitches.include? pitch
18
+ note.links[pitch] = Link::Slur.new(pitch)
19
+ else
20
+ note.links.delete pitch
21
+ end
22
+ elsif (link.is_a?(Link::Slur) ||
23
+ link.is_a?(Link::Legato))
24
+ unless next_note.pitches.include? link.target_pitch
25
+ note.links.delete pitch
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ def self.replace_articulation note, next_note
32
+ case note.articulation
33
+ when Articulations::SLUR
34
+ NoteLinker.fully_link(note, next_note, Link::Slur)
35
+ note.articulation = Articulations::NORMAL
36
+ when Articulations::LEGATO
37
+ NoteLinker.fully_link(note, next_note, Link::Legato)
38
+ note.articulation = Articulations::NORMAL
39
+ end
40
+ end
41
+
42
+ attr_reader :notes
43
+ def initialize notes, cents_per_step = 10
44
+ @cents_per_step = cents_per_step
45
+ @notes = notes.map {|n| n.clone }
46
+
47
+ @notes.push Note.quarter
48
+ (@notes.size-1).times do |i|
49
+ NoteSequenceExtractor.fixup_links(@notes[i], @notes[i+1])
50
+ NoteSequenceExtractor.replace_articulation(@notes[i], @notes[i+1])
51
+ end
52
+ @notes.pop
53
+ end
54
+
55
+ def extract_sequences
56
+ sequences = []
57
+ offset = 0
58
+
59
+ @notes.each_index do |i|
60
+ while @notes[i].pitches.any?
61
+ elements = []
62
+
63
+ j = i
64
+ loop do
65
+ note = @notes[j]
66
+ pitch = note.pitches.pop
67
+ dur = note.duration
68
+ accented = note.accented
69
+ link = note.links[pitch]
70
+
71
+ case link
72
+ when Link::Slur
73
+ elements.push(SlurredElement.new(dur, pitch, accented))
74
+ when Link::Legato
75
+ elements.push(LegatoElement.new(dur, pitch, accented))
76
+ when Link::Glissando
77
+ elements += GlissandoConverter.glissando_elements(pitch, link.target_pitch, dur, accented)
78
+ when Link::Portamento
79
+ elements += PortamentoConverter.portamento_elements(pitch, link.target_pitch, @cents_per_step, dur, accented)
80
+ else
81
+ elements.push(FinalElement.new(dur, pitch, accented, note.articulation))
82
+ break
83
+ end
84
+
85
+ j += 1
86
+ break if j >= @notes.size || !@notes[j].pitches.include?(link.target_pitch)
87
+ end
88
+
89
+ sequences.push(NoteSequence.from_elements(offset,elements))
90
+ end
91
+ offset += @notes[i].duration
92
+ end
93
+
94
+ return sequences
95
+ end
96
+ end
97
+
98
+ end
@@ -0,0 +1,24 @@
1
+ module Musicality
2
+
3
+ class PortamentoConverter
4
+ def self.portamento_pitches(start_pitch, target_pitch, cents_per_step)
5
+ start, finish = start_pitch.total_cents, target_pitch.total_cents
6
+ step_size = finish >= start ? cents_per_step : -cents_per_step
7
+ nsteps = ((finish - 1 - start) / step_size.to_f).ceil
8
+ cents = Array.new(nsteps+1){|i| start + i * step_size }
9
+
10
+ cents.map do |cent|
11
+ Pitch.new(cent: cent)
12
+ end
13
+ end
14
+
15
+ def self.portamento_elements(start_pitch, target_pitch, cents_per_step, duration, accented)
16
+ pitches = portamento_pitches(start_pitch, target_pitch, cents_per_step)
17
+ subdur = Rational(duration, pitches.size)
18
+ pitches.map do |pitch|
19
+ SlurredElement.new(subdur, pitch, accented)
20
+ end
21
+ end
22
+ end
23
+
24
+ end
@@ -0,0 +1,126 @@
1
+ module Musicality
2
+
3
+ class ScoreNotValidError < StandardError; end
4
+
5
+ # Combine multiple program segments to one, using tempo/note/dynamic
6
+ # replication and truncation where necessary.
7
+ class ScoreCollator
8
+ def initialize score
9
+ unless score.valid?
10
+ raise ScoreNotValidError, "errors found in score: #{score.errors}"
11
+ end
12
+ @score = score
13
+ end
14
+
15
+ def collate_parts
16
+ segments = @score.program.segments
17
+
18
+ Hash[
19
+ @score.parts.map do |name, part|
20
+ new_dcs = collate_changes(part.start_dynamic,
21
+ part.dynamic_changes, segments)
22
+ new_notes = collate_notes(part.notes, segments)
23
+ new_part = Part.new(part.start_dynamic,
24
+ dynamic_changes: new_dcs, notes: new_notes)
25
+ [ name, new_part ]
26
+ end
27
+ ]
28
+ end
29
+
30
+ def collate_tempo_changes
31
+ collate_changes(@score.start_tempo,
32
+ @score.tempo_changes, @score.program.segments)
33
+ end
34
+
35
+ def collate_meter_changes
36
+ collate_changes(@score.start_meter,
37
+ @score.meter_changes, @score.program.segments)
38
+ end
39
+
40
+ private
41
+
42
+ def collate_changes start_value, changes, program_segments
43
+ new_changes = {}
44
+ comp = ValueComputer.new(start_value,changes)
45
+ segment_start_offset = 0.to_r
46
+
47
+ program_segments.each do |seg|
48
+ seg = seg.first...seg.last
49
+
50
+ # add segment start value
51
+ value = comp.value_at seg.first
52
+ new_changes[segment_start_offset] = Change::Immediate.new(value)
53
+
54
+ # add any immediate changes in segment
55
+ changes.select {|o,c| c.is_a?(Change::Immediate) && seg.include?(o) }.each do |off,c|
56
+ new_changes[(off - seg.first) + segment_start_offset] = c.clone
57
+ end
58
+
59
+ # add gradual changes
60
+ changes.select {|o,c| c.is_a?(Change::Gradual)}.each do |off, change|
61
+ adj_start_off = (off - seg.first) + segment_start_offset
62
+ end_off = off + change.duration
63
+ if seg.include?(off) # change that are wholly included in segment
64
+ if end_off <= seg.last
65
+ new_changes[adj_start_off] = change.clone
66
+ else # change that overlap segment end
67
+ over = end_off - seg.last
68
+ new_changes[adj_start_off] = Change::Gradual.new(change.value,
69
+ change.duration - over, change.elapsed, change.remaining + over)
70
+ end
71
+ elsif end_off > seg.first && end_off < seg.last # change that overlap segment start
72
+ under = seg.first - off
73
+ new_changes[segment_start_offset] = Change::Gradual.new(change.value,
74
+ change.duration - under, change.elapsed + under, change.remaining)
75
+ end
76
+ end
77
+ end
78
+
79
+ return new_changes
80
+ end
81
+
82
+ def collate_notes notes, program_segments
83
+ new_notes = []
84
+ program_segments.each do |seg|
85
+ cur_offset = 0
86
+ cur_notes = []
87
+
88
+ l = 0
89
+ while cur_offset < seg.first && l < notes.size
90
+ cur_offset += notes[l].duration
91
+ l += 1
92
+ end
93
+
94
+ pre_remainder = cur_offset - seg.first
95
+ if pre_remainder > 0
96
+ cur_notes << Note.new(pre_remainder)
97
+ end
98
+
99
+ # found some notes to add...
100
+ if l < notes.size
101
+ r = l
102
+ while cur_offset < seg.last && r < notes.size
103
+ cur_offset += notes[r].duration
104
+ r += 1
105
+ end
106
+
107
+ cur_notes += Marshal.load(Marshal.dump(notes[l...r]))
108
+ overshoot = cur_offset - seg.last
109
+ if overshoot > 0
110
+ cur_notes[-1].duration -= overshoot
111
+ cur_offset = seg.last
112
+ end
113
+ end
114
+
115
+ post_remainder = seg.last - cur_offset
116
+ if post_remainder > 0
117
+ cur_notes << Note.new(post_remainder)
118
+ end
119
+
120
+ new_notes.concat cur_notes
121
+ end
122
+ return new_notes
123
+ end
124
+ end
125
+
126
+ end
@@ -0,0 +1,34 @@
1
+ module Musicality
2
+
3
+ class MidiEvent
4
+ def <=> other
5
+ ORDERING[self] <=> ORDERING[other]
6
+ end
7
+
8
+ class NoteOn < MidiEvent
9
+ attr_reader :notenum, :accented
10
+ def initialize notenum, accented
11
+ @notenum, @accented = notenum, accented
12
+ end
13
+ end
14
+
15
+ class NoteOff < MidiEvent
16
+ attr_reader :notenum
17
+ def initialize notenum
18
+ @notenum = notenum
19
+ end
20
+ end
21
+
22
+ class Expression < MidiEvent
23
+ attr_reader :volume
24
+ def initialize volume
25
+ @volume = volume
26
+ end
27
+ end
28
+
29
+ ORDERING = {
30
+ NoteOff => 0, Expression => 1, NoteOn => 2
31
+ }
32
+ end
33
+
34
+ end
@@ -0,0 +1,31 @@
1
+ module Musicality
2
+
3
+ class MidiUtil
4
+ QUARTER = Rational(1,4)
5
+ # Number of pulses equivalent to the given duration
6
+ def self.delta duration, ppqn
7
+ pulses = (duration / QUARTER) * ppqn
8
+ return pulses.round
9
+ end
10
+
11
+ p0 = Pitch.new(octave:-1,semitone:0)
12
+ MIDI_NOTENUMS = Hash[
13
+ (0..127).map do |note_num|
14
+ [ p0.transpose(note_num), note_num ]
15
+ end
16
+ ]
17
+
18
+ def self.pitch_to_notenum pitch
19
+ MIDI_NOTENUMS.fetch(pitch.round)
20
+ end
21
+
22
+ def self.dynamic_to_volume dynamic
23
+ (dynamic * 127).round
24
+ end
25
+
26
+ def self.note_velocity(accented)
27
+ accented ? 112 : 70
28
+ end
29
+ end
30
+
31
+ end