musicality 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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