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,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