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,100 @@
|
|
1
|
+
module Music
|
2
|
+
module Performance
|
3
|
+
|
4
|
+
class NoteSequenceExtractor
|
5
|
+
# For all link types:
|
6
|
+
# - Remove link where source pitch is not in current note
|
7
|
+
# For tie:
|
8
|
+
# - Remove any bad tie (where the tying pitch does not exist in the next note).
|
9
|
+
# - Replace any good tie with a slur.
|
10
|
+
# For slur/legato:
|
11
|
+
# - Remove any bad link (where the target pitch does not exist in the next note).
|
12
|
+
# TODO: what to do about multiple links that target the same pitch?
|
13
|
+
def self.fixup_links note, next_note
|
14
|
+
note.links.each do |pitch, link|
|
15
|
+
if !note.pitches.include?(pitch)
|
16
|
+
note.links.delete pitch
|
17
|
+
elsif link.is_a? Music::Transcription::Link::Tie
|
18
|
+
if next_note.pitches.include? pitch
|
19
|
+
note.links[pitch] = Music::Transcription::Link::Slur.new(pitch)
|
20
|
+
else
|
21
|
+
note.links.delete pitch
|
22
|
+
end
|
23
|
+
elsif (link.is_a?(Music::Transcription::Link::Slur) ||
|
24
|
+
link.is_a?(Music::Transcription::Link::Legato))
|
25
|
+
unless next_note.pitches.include? link.target_pitch
|
26
|
+
note.links.delete pitch
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.replace_articulation note, next_note
|
33
|
+
case note.articulation
|
34
|
+
when Music::Transcription::Articulations::SLUR
|
35
|
+
NoteLinker.fully_link(note, next_note, Music::Transcription::Link::Slur)
|
36
|
+
note.articulation = Music::Transcription::Articulations::NORMAL
|
37
|
+
when Music::Transcription::Articulations::LEGATO
|
38
|
+
NoteLinker.fully_link(note, next_note, Music::Transcription::Link::Legato)
|
39
|
+
note.articulation = Music::Transcription::Articulations::NORMAL
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_reader :notes
|
44
|
+
def initialize notes, cents_per_step = 10
|
45
|
+
@cents_per_step = cents_per_step
|
46
|
+
@notes = notes.map {|n| n.clone }
|
47
|
+
|
48
|
+
@notes.push Music::Transcription::Note.quarter
|
49
|
+
(@notes.size-1).times do |i|
|
50
|
+
NoteSequenceExtractor.fixup_links(@notes[i], @notes[i+1])
|
51
|
+
NoteSequenceExtractor.replace_articulation(@notes[i], @notes[i+1])
|
52
|
+
end
|
53
|
+
@notes.pop
|
54
|
+
end
|
55
|
+
|
56
|
+
def extract_sequences
|
57
|
+
sequences = []
|
58
|
+
offset = 0
|
59
|
+
|
60
|
+
@notes.each_index do |i|
|
61
|
+
while @notes[i].pitches.any?
|
62
|
+
elements = []
|
63
|
+
|
64
|
+
j = i
|
65
|
+
loop do
|
66
|
+
note = @notes[j]
|
67
|
+
pitch = note.pitches.pop
|
68
|
+
dur = note.duration
|
69
|
+
accented = note.accented
|
70
|
+
link = note.links[pitch]
|
71
|
+
|
72
|
+
case link
|
73
|
+
when Music::Transcription::Link::Slur
|
74
|
+
elements.push(SlurredElement.new(dur, pitch, accented))
|
75
|
+
when Music::Transcription::Link::Legato
|
76
|
+
elements.push(LegatoElement.new(dur, pitch, accented))
|
77
|
+
when Music::Transcription::Link::Glissando
|
78
|
+
elements += GlissandoConverter.glissando_elements(pitch, link.target_pitch, dur, accented)
|
79
|
+
when Music::Transcription::Link::Portamento
|
80
|
+
elements += PortamentoConverter.portamento_elements(pitch, link.target_pitch, @cents_per_step, dur, accented)
|
81
|
+
else
|
82
|
+
elements.push(FinalElement.new(dur, pitch, accented, note.articulation))
|
83
|
+
break
|
84
|
+
end
|
85
|
+
|
86
|
+
j += 1
|
87
|
+
break if j >= @notes.size || !@notes[j].pitches.include?(link.target_pitch)
|
88
|
+
end
|
89
|
+
|
90
|
+
sequences.push(NoteSequence.from_elements(offset,elements))
|
91
|
+
end
|
92
|
+
offset += @notes[i].duration
|
93
|
+
end
|
94
|
+
|
95
|
+
return sequences
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Music
|
2
|
+
module Performance
|
3
|
+
|
4
|
+
# Convert note duration to time duration.
|
5
|
+
class NoteTimeConverter
|
6
|
+
|
7
|
+
def initialize tempo_computer, beat_duration_computer, sample_rate
|
8
|
+
@tempo_computer = tempo_computer
|
9
|
+
@beat_duration_computer = beat_duration_computer
|
10
|
+
@sample_period = Rational(1,sample_rate)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.notes_per_second tempo, beat_duration
|
14
|
+
(tempo * beat_duration) * Rational(1,60)
|
15
|
+
end
|
16
|
+
|
17
|
+
def notes_per_second_at offset
|
18
|
+
tempo = @tempo_computer.value_at offset
|
19
|
+
beat_duration = @beat_duration_computer.value_at offset
|
20
|
+
return NoteTimeConverter.notes_per_second(tempo,beat_duration)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Convert the given note duration to a time duration. Using the tempo computer
|
24
|
+
# and beat duration computer, figure the current notes-per-second relationship
|
25
|
+
# depending on the current note offset. Using this, note duration for each
|
26
|
+
# sample is known and accumulated as samples are taken. When accumulated note
|
27
|
+
# duration passes the given desired duration (note_end - note_begin), the
|
28
|
+
# number of samples take will indicated the corresponding time duration. There
|
29
|
+
# is adjustment for last sample taken, which likely goes past the desired note
|
30
|
+
# duration.
|
31
|
+
#
|
32
|
+
# @param [Numeric] note_begin the starting note offset.
|
33
|
+
# @param [Numeric] note_end the ending note offset.
|
34
|
+
# @raise [ArgumentError] if note end is less than note begin.
|
35
|
+
def time_elapsed note_begin, note_end
|
36
|
+
raise ArgumentError "note end is less than note begin" if note_end < note_begin
|
37
|
+
|
38
|
+
time = 0.to_r
|
39
|
+
note = note_begin
|
40
|
+
|
41
|
+
while note < note_end
|
42
|
+
notes_per_sec = notes_per_second_at note
|
43
|
+
notes_per_sample = notes_per_sec * @sample_period
|
44
|
+
|
45
|
+
if (note + notes_per_sample) > note_end
|
46
|
+
#interpolate between note and note_end
|
47
|
+
perc = (note_end - note) / notes_per_sample
|
48
|
+
time += @sample_period * perc
|
49
|
+
note = note_end
|
50
|
+
else
|
51
|
+
time += @sample_period
|
52
|
+
note += notes_per_sample
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
return time
|
57
|
+
end
|
58
|
+
|
59
|
+
#map absolute note offsets to relative time offsets
|
60
|
+
def map_note_offsets_to_time_offsets note_offsets
|
61
|
+
time_counter = 0.0
|
62
|
+
sorted_offsets = note_offsets.sort
|
63
|
+
note_time_map = { sorted_offsets.first => time_counter }
|
64
|
+
|
65
|
+
for i in 1...sorted_offsets.count do
|
66
|
+
time_counter += time_elapsed(sorted_offsets[i-1], sorted_offsets[i])
|
67
|
+
note_time_map[sorted_offsets[i]] = time_counter
|
68
|
+
end
|
69
|
+
|
70
|
+
return note_time_map
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Music
|
2
|
+
module Performance
|
3
|
+
|
4
|
+
class PortamentoConverter
|
5
|
+
def self.portamento_pitches(start_pitch, target_pitch, cents_per_step)
|
6
|
+
start, finish = start_pitch.total_cents, target_pitch.total_cents
|
7
|
+
step_size = finish >= start ? cents_per_step : -cents_per_step
|
8
|
+
nsteps = ((finish - 1 - start) / step_size.to_f).ceil
|
9
|
+
cents = Array.new(nsteps+1){|i| start + i * step_size }
|
10
|
+
|
11
|
+
cents.map do |cent|
|
12
|
+
Music::Transcription::Pitch.new(cent: cent)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.portamento_elements(start_pitch, target_pitch, cents_per_step, duration, accented)
|
17
|
+
pitches = portamento_pitches(start_pitch, target_pitch, cents_per_step)
|
18
|
+
subdur = Rational(duration, pitches.size)
|
19
|
+
pitches.map do |pitch|
|
20
|
+
SlurredElement.new(subdur, pitch, accented)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module Music
|
2
|
+
module Performance
|
3
|
+
|
4
|
+
class ScoreNotValidError < StandardError; end
|
5
|
+
|
6
|
+
# Combine multiple program segments to one, using tempo/note/dynamic
|
7
|
+
# replication and truncation where necessary.
|
8
|
+
class ScoreCollator
|
9
|
+
def initialize score
|
10
|
+
unless score.valid?
|
11
|
+
raise ScoreNotValidError, "errors found in score: #{score.errors}"
|
12
|
+
end
|
13
|
+
@score = score
|
14
|
+
end
|
15
|
+
|
16
|
+
def collate_parts
|
17
|
+
segments = @score.program.segments
|
18
|
+
|
19
|
+
Hash[
|
20
|
+
@score.parts.map do |name, part|
|
21
|
+
new_dcs = collate_changes(part.start_dynamic,
|
22
|
+
part.dynamic_changes, segments)
|
23
|
+
new_notes = collate_notes(part.notes, segments)
|
24
|
+
new_part = Music::Transcription::Part.new(part.start_dynamic,
|
25
|
+
dynamic_changes: new_dcs, notes: new_notes)
|
26
|
+
[ name, new_part ]
|
27
|
+
end
|
28
|
+
]
|
29
|
+
end
|
30
|
+
|
31
|
+
def collate_tempo_changes
|
32
|
+
collate_changes(@score.start_tempo,
|
33
|
+
@score.tempo_changes, @score.program.segments)
|
34
|
+
end
|
35
|
+
|
36
|
+
def collate_meter_changes
|
37
|
+
collate_changes(@score.start_meter,
|
38
|
+
@score.meter_changes, @score.program.segments)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def collate_changes start_value, changes, program_segments
|
44
|
+
new_changes = {}
|
45
|
+
comp = ValueComputer.new(start_value,changes)
|
46
|
+
segment_start_offset = 0.to_r
|
47
|
+
|
48
|
+
program_segments.each do |seg|
|
49
|
+
included = changes.select {|offset,change| offset > seg.first && offset < seg.last }
|
50
|
+
included.each do |offset, change|
|
51
|
+
if(offset + change.duration) > seg.last
|
52
|
+
change.duration = seg.last - offset
|
53
|
+
change.value = comp.value_at seg.last
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# find & add segment start value first
|
58
|
+
value = comp.value_at seg.first
|
59
|
+
offset = segment_start_offset
|
60
|
+
new_changes[offset] = Music::Transcription::Change::Immediate.new(value)
|
61
|
+
|
62
|
+
# add changes to part, adjusting for segment start offset
|
63
|
+
included.each do |offset2, change|
|
64
|
+
offset3 = (offset2 - seg.first) + segment_start_offset
|
65
|
+
new_changes[offset3] = change
|
66
|
+
end
|
67
|
+
|
68
|
+
segment_start_offset += (seg.last - seg.first)
|
69
|
+
end
|
70
|
+
|
71
|
+
return new_changes
|
72
|
+
end
|
73
|
+
|
74
|
+
def collate_notes notes, program_segments
|
75
|
+
new_notes = []
|
76
|
+
program_segments.each do |seg|
|
77
|
+
cur_offset = 0
|
78
|
+
cur_notes = []
|
79
|
+
|
80
|
+
l = 0
|
81
|
+
while cur_offset < seg.first && l < notes.size
|
82
|
+
cur_offset += notes[l].duration
|
83
|
+
l += 1
|
84
|
+
end
|
85
|
+
|
86
|
+
pre_remainder = cur_offset - seg.first
|
87
|
+
if pre_remainder > 0
|
88
|
+
cur_notes << Music::Transcription::Note.new(pre_remainder)
|
89
|
+
end
|
90
|
+
|
91
|
+
# found some notes to add...
|
92
|
+
if l < notes.size
|
93
|
+
r = l
|
94
|
+
while cur_offset < seg.last && r < notes.size
|
95
|
+
cur_offset += notes[r].duration
|
96
|
+
r += 1
|
97
|
+
end
|
98
|
+
|
99
|
+
cur_notes += Marshal.load(Marshal.dump(notes[l...r]))
|
100
|
+
overshoot = cur_offset - seg.last
|
101
|
+
if overshoot > 0
|
102
|
+
cur_notes[-1].duration -= overshoot
|
103
|
+
cur_offset = seg.last
|
104
|
+
end
|
105
|
+
|
106
|
+
cur_notes[-1].links.clear
|
107
|
+
end
|
108
|
+
|
109
|
+
post_remainder = seg.last - cur_offset
|
110
|
+
if post_remainder > 0
|
111
|
+
cur_notes << Music::Transcription::Note.new(post_remainder)
|
112
|
+
end
|
113
|
+
|
114
|
+
new_notes.concat cur_notes
|
115
|
+
end
|
116
|
+
return new_notes
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Music
|
4
|
+
module Performance
|
5
|
+
|
6
|
+
# Utility class to convert a score from note-based to time-based offsets
|
7
|
+
class ScoreTimeConverter
|
8
|
+
|
9
|
+
def initialize score, sample_rate
|
10
|
+
tempo_computer = ValueComputer.new(
|
11
|
+
score.start_tempo, score.tempo_changes)
|
12
|
+
|
13
|
+
bdcs = Hash[ score.meter_changes.map do |offset,change|
|
14
|
+
newchange = change.clone
|
15
|
+
newchange.value = change.value.beat_duration
|
16
|
+
[offset, newchange]
|
17
|
+
end ]
|
18
|
+
beat_duration_computer = ValueComputer.new(
|
19
|
+
score.start_meter.beat_duration, bdcs)
|
20
|
+
|
21
|
+
@note_time_converter = NoteTimeConverter.new(
|
22
|
+
tempo_computer, beat_duration_computer, sample_rate)
|
23
|
+
@score = score
|
24
|
+
@note_time_map = make_note_time_map(gather_all_offsets)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Convert note-based offsets & durations to time-based.
|
28
|
+
def convert_parts
|
29
|
+
newparts = {}
|
30
|
+
@score.parts.each do |name,part|
|
31
|
+
offset = 0
|
32
|
+
|
33
|
+
newnotes = part.notes.map do |note|
|
34
|
+
starttime = @note_time_map[offset]
|
35
|
+
endtime = @note_time_map[offset + note.duration]
|
36
|
+
offset += note.duration
|
37
|
+
newnote = note.clone
|
38
|
+
newnote.duration = endtime - starttime
|
39
|
+
newnote
|
40
|
+
end
|
41
|
+
|
42
|
+
new_dcs = Hash[
|
43
|
+
part.dynamic_changes.map do |offset, change|
|
44
|
+
timeoffset = @note_time_map[offset]
|
45
|
+
newchange = change.clone
|
46
|
+
newchange.duration = @note_time_map[offset + change.duration]
|
47
|
+
|
48
|
+
[timeoffset,newchange]
|
49
|
+
end
|
50
|
+
]
|
51
|
+
|
52
|
+
newparts[name] = Music::Transcription::Part.new(
|
53
|
+
part.start_dynamic,
|
54
|
+
notes: newnotes,
|
55
|
+
dynamic_changes: new_dcs
|
56
|
+
)
|
57
|
+
end
|
58
|
+
return newparts
|
59
|
+
end
|
60
|
+
|
61
|
+
# Convert note-based offsets & durations to time-based.
|
62
|
+
def convert_program
|
63
|
+
newsegments = @score.program.segments.map do |segment|
|
64
|
+
first = @note_time_map[segment.first]
|
65
|
+
last = @note_time_map[segment.last]
|
66
|
+
first...last
|
67
|
+
end
|
68
|
+
Program.new(newsegments)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def gather_all_offsets
|
74
|
+
note_offsets = Set.new [0]
|
75
|
+
|
76
|
+
@score.parts.each do |name, part|
|
77
|
+
offset = 0.to_r
|
78
|
+
part.notes.each do |note|
|
79
|
+
offset += note.duration
|
80
|
+
note_offsets << offset
|
81
|
+
end
|
82
|
+
|
83
|
+
part.dynamic_changes.each do |change_offset, change|
|
84
|
+
note_offsets << change_offset
|
85
|
+
note_offsets << (change_offset + change.duration)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
@score.program.segments.each do |segment|
|
90
|
+
note_offsets << segment.first
|
91
|
+
note_offsets << segment.last
|
92
|
+
end
|
93
|
+
|
94
|
+
return note_offsets
|
95
|
+
end
|
96
|
+
|
97
|
+
def make_note_time_map note_offsets
|
98
|
+
return @note_time_converter.map_note_offsets_to_time_offsets note_offsets
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
module Transcription
|
105
|
+
class Score
|
106
|
+
def to_time_score sample_rate
|
107
|
+
ScoreTimeConverter.new(self,sample_rate).make_time_score
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Music
|
2
|
+
module Performance
|
3
|
+
|
4
|
+
require 'singleton'
|
5
|
+
class AccentedAttack
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
def accented?; return true; end
|
9
|
+
end
|
10
|
+
|
11
|
+
class UnaccentedAttack
|
12
|
+
include Singleton
|
13
|
+
|
14
|
+
def accented?; return false; end
|
15
|
+
end
|
16
|
+
|
17
|
+
ACCENTED = AccentedAttack.instance
|
18
|
+
UNACCENTED = UnaccentedAttack.instance
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|