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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +47 -0
- data/Rakefile +65 -0
- data/bin/midify +78 -0
- data/examples/hip.rb +32 -0
- data/examples/missed_connection.rb +26 -0
- data/examples/song1.rb +33 -0
- data/examples/song2.rb +32 -0
- data/lib/musicality/errors.rb +9 -0
- data/lib/musicality/notation/conversion/change_conversion.rb +19 -0
- data/lib/musicality/notation/conversion/measure_note_map.rb +40 -0
- data/lib/musicality/notation/conversion/measured_score_conversion.rb +70 -0
- data/lib/musicality/notation/conversion/measured_score_converter.rb +95 -0
- data/lib/musicality/notation/conversion/note_time_converter.rb +68 -0
- data/lib/musicality/notation/conversion/tempo_conversion.rb +25 -0
- data/lib/musicality/notation/conversion/unmeasured_score_conversion.rb +47 -0
- data/lib/musicality/notation/conversion/unmeasured_score_converter.rb +64 -0
- data/lib/musicality/notation/model/articulations.rb +13 -0
- data/lib/musicality/notation/model/change.rb +62 -0
- data/lib/musicality/notation/model/dynamics.rb +12 -0
- data/lib/musicality/notation/model/link.rb +73 -0
- data/lib/musicality/notation/model/meter.rb +54 -0
- data/lib/musicality/notation/model/meters.rb +9 -0
- data/lib/musicality/notation/model/note.rb +120 -0
- data/lib/musicality/notation/model/part.rb +54 -0
- data/lib/musicality/notation/model/pitch.rb +163 -0
- data/lib/musicality/notation/model/pitches.rb +21 -0
- data/lib/musicality/notation/model/program.rb +53 -0
- data/lib/musicality/notation/model/score.rb +132 -0
- data/lib/musicality/notation/packing/change_packing.rb +46 -0
- data/lib/musicality/notation/packing/part_packing.rb +31 -0
- data/lib/musicality/notation/packing/program_packing.rb +16 -0
- data/lib/musicality/notation/packing/score_packing.rb +108 -0
- data/lib/musicality/notation/parsing/articulation_parsing.rb +264 -0
- data/lib/musicality/notation/parsing/articulation_parsing.treetop +59 -0
- data/lib/musicality/notation/parsing/convenience_methods.rb +74 -0
- data/lib/musicality/notation/parsing/duration_nodes.rb +21 -0
- data/lib/musicality/notation/parsing/duration_parsing.rb +205 -0
- data/lib/musicality/notation/parsing/duration_parsing.treetop +25 -0
- data/lib/musicality/notation/parsing/link_nodes.rb +35 -0
- data/lib/musicality/notation/parsing/link_parsing.rb +270 -0
- data/lib/musicality/notation/parsing/link_parsing.treetop +33 -0
- data/lib/musicality/notation/parsing/meter_parsing.rb +190 -0
- data/lib/musicality/notation/parsing/meter_parsing.treetop +29 -0
- data/lib/musicality/notation/parsing/note_node.rb +40 -0
- data/lib/musicality/notation/parsing/note_parsing.rb +229 -0
- data/lib/musicality/notation/parsing/note_parsing.treetop +28 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_float_parsing.rb +289 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_float_parsing.treetop +29 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_integer_parsing.rb +64 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_integer_parsing.treetop +17 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_rational_parsing.rb +86 -0
- data/lib/musicality/notation/parsing/numbers/nonnegative_rational_parsing.treetop +20 -0
- data/lib/musicality/notation/parsing/numbers/positive_float_parsing.rb +503 -0
- data/lib/musicality/notation/parsing/numbers/positive_float_parsing.treetop +33 -0
- data/lib/musicality/notation/parsing/numbers/positive_integer_parsing.rb +95 -0
- data/lib/musicality/notation/parsing/numbers/positive_integer_parsing.treetop +19 -0
- data/lib/musicality/notation/parsing/numbers/positive_rational_parsing.rb +84 -0
- data/lib/musicality/notation/parsing/numbers/positive_rational_parsing.treetop +19 -0
- data/lib/musicality/notation/parsing/parseable.rb +30 -0
- data/lib/musicality/notation/parsing/pitch_node.rb +23 -0
- data/lib/musicality/notation/parsing/pitch_parsing.rb +448 -0
- data/lib/musicality/notation/parsing/pitch_parsing.treetop +52 -0
- data/lib/musicality/notation/parsing/segment_parsing.rb +141 -0
- data/lib/musicality/notation/parsing/segment_parsing.treetop +23 -0
- data/lib/musicality/notation/util/interpolation.rb +16 -0
- data/lib/musicality/notation/util/piecewise_function.rb +122 -0
- data/lib/musicality/notation/util/value_computer.rb +170 -0
- data/lib/musicality/performance/conversion/glissando_converter.rb +34 -0
- data/lib/musicality/performance/conversion/note_sequence_extractor.rb +98 -0
- data/lib/musicality/performance/conversion/portamento_converter.rb +24 -0
- data/lib/musicality/performance/conversion/score_collator.rb +126 -0
- data/lib/musicality/performance/midi/midi_events.rb +34 -0
- data/lib/musicality/performance/midi/midi_util.rb +31 -0
- data/lib/musicality/performance/midi/part_sequencer.rb +123 -0
- data/lib/musicality/performance/midi/score_sequencer.rb +45 -0
- data/lib/musicality/performance/model/note_attacks.rb +19 -0
- data/lib/musicality/performance/model/note_sequence.rb +111 -0
- data/lib/musicality/performance/util/note_linker.rb +28 -0
- data/lib/musicality/performance/util/optimization.rb +31 -0
- data/lib/musicality/validatable.rb +38 -0
- data/lib/musicality/version.rb +3 -0
- data/lib/musicality.rb +81 -0
- data/musicality.gemspec +30 -0
- data/spec/musicality_spec.rb +7 -0
- data/spec/notation/conversion/change_conversion_spec.rb +40 -0
- data/spec/notation/conversion/measure_note_map_spec.rb +73 -0
- data/spec/notation/conversion/measured_score_conversion_spec.rb +141 -0
- data/spec/notation/conversion/measured_score_converter_spec.rb +329 -0
- data/spec/notation/conversion/note_time_converter_spec.rb +81 -0
- data/spec/notation/conversion/tempo_conversion_spec.rb +40 -0
- data/spec/notation/conversion/unmeasured_score_conversion_spec.rb +71 -0
- data/spec/notation/conversion/unmeasured_score_converter_spec.rb +116 -0
- data/spec/notation/model/change_spec.rb +90 -0
- data/spec/notation/model/link_spec.rb +83 -0
- data/spec/notation/model/meter_spec.rb +97 -0
- data/spec/notation/model/note_spec.rb +183 -0
- data/spec/notation/model/part_spec.rb +69 -0
- data/spec/notation/model/pitch_spec.rb +180 -0
- data/spec/notation/model/program_spec.rb +50 -0
- data/spec/notation/model/score_spec.rb +211 -0
- data/spec/notation/packing/change_packing_spec.rb +153 -0
- data/spec/notation/packing/part_packing_spec.rb +66 -0
- data/spec/notation/packing/program_packing_spec.rb +33 -0
- data/spec/notation/packing/score_packing_spec.rb +301 -0
- data/spec/notation/parsing/articulation_parsing_spec.rb +23 -0
- data/spec/notation/parsing/convenience_methods_spec.rb +99 -0
- data/spec/notation/parsing/duration_nodes_spec.rb +83 -0
- data/spec/notation/parsing/duration_parsing_spec.rb +70 -0
- data/spec/notation/parsing/link_nodes_spec.rb +30 -0
- data/spec/notation/parsing/link_parsing_spec.rb +13 -0
- data/spec/notation/parsing/meter_parsing_spec.rb +23 -0
- data/spec/notation/parsing/note_node_spec.rb +87 -0
- data/spec/notation/parsing/note_parsing_spec.rb +46 -0
- data/spec/notation/parsing/numbers/nonnegative_float_spec.rb +28 -0
- data/spec/notation/parsing/numbers/nonnegative_integer_spec.rb +11 -0
- data/spec/notation/parsing/numbers/nonnegative_rational_spec.rb +11 -0
- data/spec/notation/parsing/numbers/positive_float_spec.rb +28 -0
- data/spec/notation/parsing/numbers/positive_integer_spec.rb +28 -0
- data/spec/notation/parsing/numbers/positive_rational_spec.rb +28 -0
- data/spec/notation/parsing/pitch_node_spec.rb +38 -0
- data/spec/notation/parsing/pitch_parsing_spec.rb +14 -0
- data/spec/notation/parsing/segment_parsing_spec.rb +27 -0
- data/spec/notation/util/value_computer_spec.rb +146 -0
- data/spec/performance/conversion/glissando_converter_spec.rb +93 -0
- data/spec/performance/conversion/note_sequence_extractor_spec.rb +230 -0
- data/spec/performance/conversion/portamento_converter_spec.rb +91 -0
- data/spec/performance/conversion/score_collator_spec.rb +183 -0
- data/spec/performance/midi/midi_util_spec.rb +110 -0
- data/spec/performance/midi/part_sequencer_spec.rb +40 -0
- data/spec/performance/midi/score_sequencer_spec.rb +50 -0
- data/spec/performance/model/note_sequence_spec.rb +147 -0
- data/spec/performance/util/note_linker_spec.rb +68 -0
- data/spec/performance/util/optimization_spec.rb +73 -0
- data/spec/spec_helper.rb +43 -0
- 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
|