music-performance 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.document +3 -0
  3. data/.gitignore +7 -0
  4. data/.rspec +1 -0
  5. data/.ruby-version +1 -0
  6. data/.yardopts +1 -0
  7. data/ChangeLog.rdoc +5 -0
  8. data/Gemfile +3 -0
  9. data/LICENSE.txt +20 -0
  10. data/README.rdoc +28 -0
  11. data/Rakefile +54 -0
  12. data/bin/midify +61 -0
  13. data/lib/music-performance.rb +25 -0
  14. data/lib/music-performance/arrangement/midi/midi_events.rb +9 -0
  15. data/lib/music-performance/arrangement/midi/midi_util.rb +38 -0
  16. data/lib/music-performance/arrangement/midi/part_sequencer.rb +121 -0
  17. data/lib/music-performance/arrangement/midi/score_sequencer.rb +33 -0
  18. data/lib/music-performance/conversion/glissando_converter.rb +36 -0
  19. data/lib/music-performance/conversion/note_sequence_extractor.rb +100 -0
  20. data/lib/music-performance/conversion/note_time_converter.rb +76 -0
  21. data/lib/music-performance/conversion/portamento_converter.rb +26 -0
  22. data/lib/music-performance/conversion/score_collator.rb +121 -0
  23. data/lib/music-performance/conversion/score_time_converter.rb +112 -0
  24. data/lib/music-performance/model/note_attacks.rb +21 -0
  25. data/lib/music-performance/model/note_sequence.rb +113 -0
  26. data/lib/music-performance/util/interpolation.rb +18 -0
  27. data/lib/music-performance/util/note_linker.rb +30 -0
  28. data/lib/music-performance/util/optimization.rb +33 -0
  29. data/lib/music-performance/util/piecewise_function.rb +124 -0
  30. data/lib/music-performance/util/value_computer.rb +172 -0
  31. data/lib/music-performance/version.rb +7 -0
  32. data/music-performance.gemspec +33 -0
  33. data/spec/conversion/glissando_converter_spec.rb +93 -0
  34. data/spec/conversion/note_sequence_extractor_spec.rb +230 -0
  35. data/spec/conversion/note_time_converter_spec.rb +96 -0
  36. data/spec/conversion/portamento_converter_spec.rb +91 -0
  37. data/spec/conversion/score_collator_spec.rb +136 -0
  38. data/spec/conversion/score_time_converter_spec.rb +73 -0
  39. data/spec/model/note_sequence_spec.rb +147 -0
  40. data/spec/music-performance_spec.rb +7 -0
  41. data/spec/spec_helper.rb +8 -0
  42. data/spec/util/note_linker_spec.rb +68 -0
  43. data/spec/util/optimization_spec.rb +73 -0
  44. data/spec/util/value_computer_spec.rb +146 -0
  45. 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