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