music-performance 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +3 -0
- data/.gitignore +7 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/.yardopts +1 -0
- data/ChangeLog.rdoc +5 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +28 -0
- data/Rakefile +54 -0
- data/bin/midify +61 -0
- data/lib/music-performance.rb +25 -0
- data/lib/music-performance/arrangement/midi/midi_events.rb +9 -0
- data/lib/music-performance/arrangement/midi/midi_util.rb +38 -0
- data/lib/music-performance/arrangement/midi/part_sequencer.rb +121 -0
- data/lib/music-performance/arrangement/midi/score_sequencer.rb +33 -0
- data/lib/music-performance/conversion/glissando_converter.rb +36 -0
- data/lib/music-performance/conversion/note_sequence_extractor.rb +100 -0
- data/lib/music-performance/conversion/note_time_converter.rb +76 -0
- data/lib/music-performance/conversion/portamento_converter.rb +26 -0
- data/lib/music-performance/conversion/score_collator.rb +121 -0
- data/lib/music-performance/conversion/score_time_converter.rb +112 -0
- data/lib/music-performance/model/note_attacks.rb +21 -0
- data/lib/music-performance/model/note_sequence.rb +113 -0
- data/lib/music-performance/util/interpolation.rb +18 -0
- data/lib/music-performance/util/note_linker.rb +30 -0
- data/lib/music-performance/util/optimization.rb +33 -0
- data/lib/music-performance/util/piecewise_function.rb +124 -0
- data/lib/music-performance/util/value_computer.rb +172 -0
- data/lib/music-performance/version.rb +7 -0
- data/music-performance.gemspec +33 -0
- data/spec/conversion/glissando_converter_spec.rb +93 -0
- data/spec/conversion/note_sequence_extractor_spec.rb +230 -0
- data/spec/conversion/note_time_converter_spec.rb +96 -0
- data/spec/conversion/portamento_converter_spec.rb +91 -0
- data/spec/conversion/score_collator_spec.rb +136 -0
- data/spec/conversion/score_time_converter_spec.rb +73 -0
- data/spec/model/note_sequence_spec.rb +147 -0
- data/spec/music-performance_spec.rb +7 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/util/note_linker_spec.rb +68 -0
- data/spec/util/optimization_spec.rb +73 -0
- data/spec/util/value_computer_spec.rb +146 -0
- metadata +242 -0
@@ -0,0 +1,113 @@
|
|
1
|
+
module Music
|
2
|
+
module Performance
|
3
|
+
|
4
|
+
SlurredElement = Struct.new(:duration, :pitch, :accented) do
|
5
|
+
def slurred?; true; end
|
6
|
+
def articulation; Music::Transcription::Articulations::NORMAL; end
|
7
|
+
def accented?; accented; end
|
8
|
+
end
|
9
|
+
|
10
|
+
LegatoElement = Struct.new(:duration, :pitch, :accented) do
|
11
|
+
def slurred?; false; end
|
12
|
+
def articulation; Music::Transcription::Articulations::NORMAL; end
|
13
|
+
def accented?; accented; end
|
14
|
+
end
|
15
|
+
|
16
|
+
FinalElement = Struct.new(:duration, :pitch, :accented, :articulation) do
|
17
|
+
def slurred?; false; end
|
18
|
+
def accented?; accented; end
|
19
|
+
end
|
20
|
+
|
21
|
+
class NoteSequence
|
22
|
+
def self.adjust_duration duration, articulation
|
23
|
+
x = duration
|
24
|
+
y = Math.log2(x)
|
25
|
+
|
26
|
+
case articulation
|
27
|
+
when Music::Transcription::Articulations::TENUTO
|
28
|
+
x
|
29
|
+
when Music::Transcription::Articulations::PORTATO
|
30
|
+
x / (1 + 2**(y-1))
|
31
|
+
when Music::Transcription::Articulations::STACCATO
|
32
|
+
x / (1 + 2**(y))
|
33
|
+
when Music::Transcription::Articulations::STACCATISSIMO
|
34
|
+
x / (1 + 2**(y+1))
|
35
|
+
else
|
36
|
+
x - (1/16.0)*(1/(1+2**(-y)))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
attr_reader :start, :stop, :pitches, :attacks
|
41
|
+
def initialize start, stop, pitches, attacks
|
42
|
+
if start >= stop
|
43
|
+
raise ArgumentError, "start #{start} is not less than stop #{stop}"
|
44
|
+
end
|
45
|
+
|
46
|
+
if pitches.empty?
|
47
|
+
raise ArgumentError, "no pitches given (at least one pitch is required at start offset)"
|
48
|
+
end
|
49
|
+
|
50
|
+
unless pitches.has_key?(start)
|
51
|
+
raise ArgumentError, "no start pitch given"
|
52
|
+
end
|
53
|
+
|
54
|
+
pitches.keys.each do |offset|
|
55
|
+
unless offset.between?(start,stop)
|
56
|
+
raise ArgumentError, "pitch offset #{offset} is not between start #{start} and stop #{stop}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
if attacks.empty?
|
61
|
+
raise ArgumentError, "no attacks given (at least one is required at start offset)"
|
62
|
+
end
|
63
|
+
|
64
|
+
unless attacks.has_key?(start)
|
65
|
+
raise ArgumentError, "no start attack given"
|
66
|
+
end
|
67
|
+
|
68
|
+
attacks.keys.each do |offset|
|
69
|
+
unless offset.between?(start,stop)
|
70
|
+
raise ArgumentError, "attack offset #{offset} is not between start #{start} and stop #{stop}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
@start, @stop = start, stop
|
75
|
+
@pitches, @attacks = pitches, attacks
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.from_elements offset, elements
|
79
|
+
pitches = {}
|
80
|
+
attacks = {}
|
81
|
+
start = offset
|
82
|
+
|
83
|
+
if elements.empty?
|
84
|
+
raise ArgumentError, "no elements given"
|
85
|
+
end
|
86
|
+
|
87
|
+
last = elements.last
|
88
|
+
skip_attack = false
|
89
|
+
elements.each do |el|
|
90
|
+
if skip_attack
|
91
|
+
unless pitches.max[1] == el.pitch
|
92
|
+
pitches[offset] = el.pitch
|
93
|
+
end
|
94
|
+
else
|
95
|
+
pitches[offset] = el.pitch
|
96
|
+
attacks[offset] = el.accented ? ACCENTED : UNACCENTED
|
97
|
+
end
|
98
|
+
skip_attack = el.slurred?
|
99
|
+
|
100
|
+
unless el.equal?(last)
|
101
|
+
offset += el.duration
|
102
|
+
end
|
103
|
+
end
|
104
|
+
stop = offset + NoteSequence.adjust_duration(last.duration, last.articulation)
|
105
|
+
|
106
|
+
new(start, stop, pitches, attacks)
|
107
|
+
end
|
108
|
+
|
109
|
+
def duration; @stop - @start; end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Music
|
2
|
+
module Performance
|
3
|
+
|
4
|
+
module Interpolation
|
5
|
+
# Linear interpolation
|
6
|
+
# Given 2 sample points, interpolates a value anywhere between the two points.
|
7
|
+
#
|
8
|
+
# @param [Numeric] y0 First (left) y-value
|
9
|
+
# @param [Numeric] y1 Second (right) y-value
|
10
|
+
# @param [Numeric] x Percent distance (along the x-axis) between the two y-values
|
11
|
+
def self.linear y0, y1, x
|
12
|
+
raise ArgumentError, "x is not between 0.0 and 1.0" unless x.between?(0.0,1.0)
|
13
|
+
return y0 + x * (y1 - y0)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Music
|
2
|
+
module Performance
|
3
|
+
|
4
|
+
class NoteLinker
|
5
|
+
def self.find_unlinked_pitches note
|
6
|
+
linked = Set.new(note.pitches) & note.links.keys
|
7
|
+
(Set.new(note.pitches) - linked).to_a
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.find_untargeted_pitches note, next_note
|
11
|
+
linked = Set.new(note.pitches) & note.links.keys
|
12
|
+
targeted = Set.new(linked.map {|p| note.links[p].target_pitch })
|
13
|
+
(Set.new(next_note.pitches) - targeted).to_a
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.figure_links note, next_note
|
17
|
+
unlinked = find_unlinked_pitches(note)
|
18
|
+
untargeted = find_untargeted_pitches(note, next_note)
|
19
|
+
Optimization.linking(unlinked, untargeted)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.fully_link note, next_note, link_class
|
23
|
+
figure_links(note,next_note).each do |pitch,tgt_pitch|
|
24
|
+
note.links[pitch] = link_class.new(tgt_pitch)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Music
|
2
|
+
module Performance
|
3
|
+
|
4
|
+
module Optimization
|
5
|
+
def self.linking unlinked, untargeted
|
6
|
+
n = [unlinked.size, untargeted.size].min
|
7
|
+
|
8
|
+
bestsol = nil
|
9
|
+
bestscore = Float::INFINITY
|
10
|
+
unlinked.combination(n).each do |comb|
|
11
|
+
untargeted.permutation(n).each do |perm|
|
12
|
+
score = 0
|
13
|
+
n.times do |i|
|
14
|
+
score += perm[i].diff(comb[i]).abs
|
15
|
+
end
|
16
|
+
|
17
|
+
if score < bestscore
|
18
|
+
bestsol = [ comb, perm ]
|
19
|
+
bestscore = score
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
solution = {}
|
25
|
+
n.times do |i|
|
26
|
+
solution[ bestsol[0][i] ] = bestsol[1][i]
|
27
|
+
end
|
28
|
+
return solution
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
module Music
|
2
|
+
module Performance
|
3
|
+
|
4
|
+
# Combine functions that are each applicable for a non-overlapping domain.
|
5
|
+
#
|
6
|
+
# @author James Tunnell
|
7
|
+
class PiecewiseFunction
|
8
|
+
attr_reader :pieces
|
9
|
+
|
10
|
+
# Take an array of points (each point is a two-element array pair) and
|
11
|
+
# create a piecewise function to calculate values in-between.
|
12
|
+
def initialize points = []
|
13
|
+
@pieces = { }
|
14
|
+
|
15
|
+
points = points.sort_by {|p| p[0]}
|
16
|
+
|
17
|
+
if points.count > 1
|
18
|
+
if points.is_a?(Hash)
|
19
|
+
points = points.to_a
|
20
|
+
end
|
21
|
+
|
22
|
+
for i in 1...points.count
|
23
|
+
add_points points[i-1], points[i]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def add_points prev_point, point
|
29
|
+
domain = prev_point[0]..point[0]
|
30
|
+
func = lambda do |x|
|
31
|
+
perc = (x - domain.min).to_f / (domain.max - domain.min)
|
32
|
+
y = Interpolation.linear prev_point[1], point[1], perc
|
33
|
+
return y
|
34
|
+
end
|
35
|
+
add_piece(domain, func)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Add a function piece, which covers the given domain (includes domain start
|
39
|
+
# but not the end).
|
40
|
+
# @param [Range] domain The function domain. If this overlaps an existing domain,
|
41
|
+
# the existing domain will be split with the non-
|
42
|
+
# overlapping pieces kept and the overlapping old piece
|
43
|
+
# discarded.
|
44
|
+
def add_piece domain, func
|
45
|
+
|
46
|
+
raise ArgumentError, "domain is not a Range" if !domain.is_a? Range
|
47
|
+
raise ArgumentError, "func is not a Proc" if !func.is_a? Proc
|
48
|
+
|
49
|
+
contains_domain_completely = @pieces.select { |d,f| d.include?(domain.begin) && d.include?(domain.end) }
|
50
|
+
if contains_domain_completely.any?
|
51
|
+
contains_domain_completely.each do |d,f|
|
52
|
+
l = d.begin...domain.begin
|
53
|
+
if d.exclude_end?
|
54
|
+
r = domain.end...d.end
|
55
|
+
else
|
56
|
+
r = domain.end..d.end
|
57
|
+
end
|
58
|
+
|
59
|
+
@pieces.delete d
|
60
|
+
|
61
|
+
if domain.begin != d.begin
|
62
|
+
@pieces[l] = f
|
63
|
+
end
|
64
|
+
if domain.end == d.end
|
65
|
+
@pieces[domain.begin..domain.end] = func
|
66
|
+
else
|
67
|
+
@pieces[domain.begin...domain.end] = func
|
68
|
+
@pieces[r] = f
|
69
|
+
end
|
70
|
+
end
|
71
|
+
else
|
72
|
+
delete_completely = @pieces.select { |d,f| domain.include?(d.begin) && domain.include?(d.end) }
|
73
|
+
delete_completely.each do |d,f|
|
74
|
+
@pieces.delete d
|
75
|
+
end
|
76
|
+
|
77
|
+
# should only be one
|
78
|
+
move_end = @pieces.select { |d,f| domain.include?(d.end) }
|
79
|
+
move_end.each do |d,f|
|
80
|
+
@pieces.delete d
|
81
|
+
@pieces[d.begin...domain.begin] = f
|
82
|
+
end
|
83
|
+
|
84
|
+
# should only be one
|
85
|
+
move_begin = @pieces.select { |d,f| domain.include?(d.begin) }
|
86
|
+
move_begin.each do |d,f|
|
87
|
+
@pieces.delete d
|
88
|
+
if d.exclude_end?
|
89
|
+
@pieces[domain.end...d.end] = f
|
90
|
+
else
|
91
|
+
@pieces[domain.end..d.end] = f
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
if move_begin.any?
|
96
|
+
@pieces[domain.begin...domain.end] = func
|
97
|
+
else
|
98
|
+
@pieces[domain] = func
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Evaluate the piecewise function by finding a function piece whose domain
|
104
|
+
# includes the given independent value.
|
105
|
+
def eval x
|
106
|
+
y = nil
|
107
|
+
|
108
|
+
@pieces.each do |domain, func|
|
109
|
+
if domain.include? x
|
110
|
+
y = func.call x
|
111
|
+
break
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
if y.nil?
|
116
|
+
raise ArgumentError, "The input #{x} is not in the domain."
|
117
|
+
end
|
118
|
+
|
119
|
+
return y
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
module Music
|
2
|
+
module Performance
|
3
|
+
|
4
|
+
# Given a start value, and value changes, compute the value at any offset.
|
5
|
+
class ValueComputer
|
6
|
+
attr_reader :piecewise_function
|
7
|
+
|
8
|
+
def initialize start_value, value_changes = {}
|
9
|
+
@piecewise_function = PiecewiseFunction.new
|
10
|
+
set_default_value start_value
|
11
|
+
|
12
|
+
if value_changes.any?
|
13
|
+
value_changes.sort.each do |offset,change|
|
14
|
+
|
15
|
+
case change
|
16
|
+
when Music::Transcription::Change::Immediate
|
17
|
+
add_immediate_change change, offset
|
18
|
+
when Music::Transcription::Change::Gradual
|
19
|
+
add_linear_change change, offset
|
20
|
+
# add_sigmoid_change change, offset
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Compute the value at the given offset.
|
28
|
+
# @param [Numeric] offset The given offset to compute value at.
|
29
|
+
def value_at offset
|
30
|
+
@piecewise_function.eval offset
|
31
|
+
end
|
32
|
+
|
33
|
+
def sample xmin, xmax, srate
|
34
|
+
sample_period = Rational(1,srate)
|
35
|
+
((xmin.to_r)..(xmax.to_r)).step(sample_period).map do |x|
|
36
|
+
value_at(x)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# finds the minimum domain value
|
41
|
+
def domain_min
|
42
|
+
-Float::INFINITY
|
43
|
+
end
|
44
|
+
|
45
|
+
# finds the maximum domain value
|
46
|
+
def domain_max
|
47
|
+
Float::INFINITY
|
48
|
+
end
|
49
|
+
|
50
|
+
# finds the minimum domain value
|
51
|
+
def self.domain_min
|
52
|
+
-Float::INFINITY
|
53
|
+
end
|
54
|
+
|
55
|
+
# finds the maximum domain value
|
56
|
+
def self.domain_max
|
57
|
+
Float::INFINITY
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def set_default_value value
|
63
|
+
func = lambda {|x| value }
|
64
|
+
@piecewise_function.add_piece( domain_min..domain_max, func )
|
65
|
+
end
|
66
|
+
|
67
|
+
# Add a function piece to the piecewise function, which will to compute value
|
68
|
+
# for a matching note offset. Transition duration will be ignored since the
|
69
|
+
# change is immediate.
|
70
|
+
#
|
71
|
+
# @param [ValueChange] value_change An event with information about the new value.
|
72
|
+
# @param [Numeric] offset
|
73
|
+
def add_immediate_change value_change, offset
|
74
|
+
func = nil
|
75
|
+
value = value_change.value
|
76
|
+
domain = offset..domain_max
|
77
|
+
func = lambda {|x| value }
|
78
|
+
|
79
|
+
@piecewise_function.add_piece domain, func
|
80
|
+
end
|
81
|
+
|
82
|
+
# Add a function piece to the piecewise function, which will to compute value
|
83
|
+
# for a matching note offset. If the dynamic event duration is non-zero, a
|
84
|
+
# linear transition function is created.
|
85
|
+
#
|
86
|
+
# @param [ValueChange] value_change An event with information about the new value.
|
87
|
+
# @param [Numeric] offset
|
88
|
+
def add_linear_change value_change, offset
|
89
|
+
|
90
|
+
func = nil
|
91
|
+
value = value_change.value
|
92
|
+
duration = value_change.duration
|
93
|
+
domain = offset..domain_max
|
94
|
+
|
95
|
+
if duration == 0
|
96
|
+
add_immediate_change(value_change, offset)
|
97
|
+
else
|
98
|
+
b = @piecewise_function.eval domain.first
|
99
|
+
m = (value.to_f - b.to_f) / duration.to_f
|
100
|
+
|
101
|
+
func = lambda do |x|
|
102
|
+
raise RangeError, "#{x} is not in the domain" if !domain.include?(x)
|
103
|
+
|
104
|
+
if x < (domain.first + duration)
|
105
|
+
(m * (x - domain.first)) + b
|
106
|
+
else
|
107
|
+
value
|
108
|
+
end
|
109
|
+
end
|
110
|
+
@piecewise_function.add_piece domain, func
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Add a function piece to the piecewise function, which will to compute value
|
115
|
+
# for a matching note offset. If the dynamic event duration is non-zero, a
|
116
|
+
# linear transition function is created.
|
117
|
+
#
|
118
|
+
# @param [ValueChange] value_change An event with information about the new value.
|
119
|
+
# @param [Numeric] offset
|
120
|
+
def add_sigmoid_change value_change, offset
|
121
|
+
|
122
|
+
func = nil
|
123
|
+
start_value = @piecewise_function.eval offset
|
124
|
+
end_value = value_change.value
|
125
|
+
value_diff = end_value - start_value
|
126
|
+
duration = value_change.duration
|
127
|
+
domain = offset.to_f..domain_max
|
128
|
+
abruptness = 0.7 # value_change.transition.abruptness.to_f
|
129
|
+
|
130
|
+
if duration == 0
|
131
|
+
add_immediate_change(value_change,offset)
|
132
|
+
else
|
133
|
+
raise ArgumentError, "abruptness is not between 0 and 1" unless abruptness.between?(0,1)
|
134
|
+
|
135
|
+
min_magn = 2
|
136
|
+
max_magn = 6
|
137
|
+
tanh_domain_magn = abruptness * (max_magn - min_magn) + min_magn
|
138
|
+
tanh_domain = -tanh_domain_magn..tanh_domain_magn
|
139
|
+
|
140
|
+
tanh_range = Math::tanh(tanh_domain.first)..Math::tanh(tanh_domain.last)
|
141
|
+
tanh_span = tanh_range.last - tanh_range.first
|
142
|
+
|
143
|
+
func = lambda do |x|
|
144
|
+
raise RangeError, "#{x} is not in the domain" if !domain.include?(x)
|
145
|
+
if x < (domain.first + duration)
|
146
|
+
start_domain = domain.first...(domain.first + duration)
|
147
|
+
x2 = transform_domains(start_domain, tanh_domain, x)
|
148
|
+
y = Math::tanh x2
|
149
|
+
z = (y / tanh_span) + 0.5 # ranges from 0 to 1
|
150
|
+
start_value + (z * value_diff)
|
151
|
+
else
|
152
|
+
end_value
|
153
|
+
end
|
154
|
+
end
|
155
|
+
@piecewise_function.add_piece domain, func
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# x should be in the start domain
|
160
|
+
def transform_domains start_domain, end_domain, x
|
161
|
+
perc = (x - start_domain.first) / (start_domain.last - start_domain.first).to_f
|
162
|
+
x2 = perc * (end_domain.last - end_domain.first) + end_domain.first
|
163
|
+
end
|
164
|
+
|
165
|
+
# 0 to 1
|
166
|
+
def logistic x
|
167
|
+
1.0 / (1 + Math::exp(-x))
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
end
|