music-performance 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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