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,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d523ae35fe6714368362b37241e3ffd10c0e2982
4
+ data.tar.gz: 3604e251beb16c2095e9c9d8e90b889b0bb385a7
5
+ SHA512:
6
+ metadata.gz: bc6d3bf49947281586e433f301048c6a64eff08611aa8a8565c40a4a60f52f1357b8436feb56d5d92cc1a400bdf63e3d99c161ccf24960e81c75ffc9cc18cc33
7
+ data.tar.gz: f4104ce48fbc02e4790df8511d6afadf668466578ffa5ee0e2ee43b0014c840ae988f3eb1bbb36a1d9cae169ce43289cd62c5caa5eaaf055659a2c02a05ab674
@@ -0,0 +1,3 @@
1
+ -
2
+ ChangeLog.rdoc
3
+ LICENSE.txt
@@ -0,0 +1,7 @@
1
+ Gemfile.lock
2
+ doc/
3
+ pkg/
4
+ vendor/cache/*.gem
5
+ .yardoc
6
+ *~
7
+ .project
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
@@ -0,0 +1 @@
1
+ 2.0.0
@@ -0,0 +1 @@
1
+ --markup rdoc --title "music-performance Documentation" --protected
@@ -0,0 +1,5 @@
1
+ === 0.1.0 / 2014-09-16
2
+
3
+ * Tear off music performance-related code from musicality gem, into a separate gem called music-performance.
4
+
5
+
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 James Tunnell
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,28 @@
1
+ = music-performance
2
+
3
+ * {Homepage}[https://github.com/jamestunnell/music-performance]
4
+ * {Issues}[https://github.com/jamestunnell/music-performance/issues]
5
+ * {Documentation}[http://rubydoc.info/gems/music-performance/frames]
6
+ * {Email}[mailto:jamestunnell@gmail.com]
7
+
8
+ == Description
9
+
10
+ Classes for preparing a music score for computer performance (e.g. via MIDI).
11
+
12
+ == Features
13
+
14
+ == Examples
15
+
16
+ require 'music-performance'
17
+
18
+ == Requirements
19
+
20
+ == Install
21
+
22
+ $ gem install music-performance
23
+
24
+ == Copyright
25
+
26
+ Copyright (c) 2012 James Tunnell
27
+
28
+ See LICENSE.txt for details.
@@ -0,0 +1,54 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+
5
+ begin
6
+ require 'bundler'
7
+ rescue LoadError => e
8
+ warn e.message
9
+ warn "Run `gem install bundler` to install Bundler."
10
+ exit -1
11
+ end
12
+
13
+ begin
14
+ Bundler.setup(:development)
15
+ rescue Bundler::BundlerError => e
16
+ warn e.message
17
+ warn "Run `bundle install` to install missing gems."
18
+ exit e.status_code
19
+ end
20
+
21
+ require 'rake'
22
+
23
+ require 'rspec/core/rake_task'
24
+ RSpec::Core::RakeTask.new
25
+
26
+ task :test => :spec
27
+ task :default => :spec
28
+
29
+ require "bundler/gem_tasks"
30
+
31
+ require 'yard'
32
+ YARD::Rake::YardocTask.new
33
+ task :doc => :yard
34
+
35
+ task :make_samples do
36
+ current_dir = Dir.getwd
37
+ samples_dir = File.join(File.dirname(__FILE__), 'samples')
38
+ Dir.chdir samples_dir
39
+
40
+ samples = []
41
+ Dir.glob('**/make*.rb') do |file|
42
+ samples.push File.expand_path(file)
43
+ end
44
+
45
+ samples.each do |sample|
46
+ dirname = File.dirname(sample)
47
+ filename = File.basename(sample)
48
+
49
+ Dir.chdir dirname
50
+ ruby filename
51
+ end
52
+
53
+ Dir.chdir current_dir
54
+ end
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ exe_name = File.basename(__FILE__)
4
+
5
+ doc = <<DOCOPT
6
+ Loads a music-transcription score from YAML file, and converts to MIDI file.
7
+
8
+ Usage:
9
+ #{exe_name} <input>
10
+ #{exe_name} <input> <output>
11
+ #{exe_name} -h | --help
12
+ #{exe_name} --version
13
+
14
+ Options:
15
+ -h --help Show this screen.
16
+ --version Show version.
17
+
18
+ DOCOPT
19
+
20
+ require 'docopt'
21
+ begin
22
+ require "pp"
23
+ args = Docopt::docopt(doc)
24
+ pp args
25
+ rescue Docopt::Exit => e
26
+ puts e.message
27
+ exit
28
+ end
29
+
30
+ require 'yaml'
31
+ require 'music-transcription'
32
+ require 'music-performance'
33
+ include Music
34
+
35
+ fin_name = args["<input>"]
36
+ File.open(fin_name) do |fin|
37
+ print "Reading file '#{fin_name}'..."
38
+ score = YAML.load(fin.read)
39
+ if score.is_a? Hash
40
+ score = Transcription::Score.unpack(score)
41
+ end
42
+ puts "complete"
43
+
44
+ if score.valid?
45
+ print "Making MIDI sequence..."
46
+ seq = Performance::ScoreSequencer.new(score).make_midi_seq
47
+ puts "complete"
48
+
49
+ fout_name = args["<output>"]
50
+ if fout_name.nil?
51
+ fout_name = "#{File.dirname(fin_name)}/#{File.basename(fin_name,File.extname(fin_name))}.mid"
52
+ end
53
+ print "Writing file '#{fout_name}'..."
54
+ File.open(fout_name, 'wb'){ |fout| seq.write(fout) }
55
+ puts "complete"
56
+ else
57
+ puts "Failed to load a valid score."
58
+ puts "Errors:"
59
+ puts score.errors.join("\n")
60
+ end
61
+ end
@@ -0,0 +1,25 @@
1
+ require 'music-transcription'
2
+
3
+ require 'music-performance/version'
4
+
5
+ require 'music-performance/model/note_attacks'
6
+ require 'music-performance/model/note_sequence'
7
+
8
+ require 'music-performance/util/interpolation'
9
+ require 'music-performance/util/piecewise_function'
10
+ require 'music-performance/util/value_computer'
11
+ require 'music-performance/util/optimization'
12
+ require 'music-performance/util/note_linker'
13
+
14
+ require 'music-performance/conversion/note_time_converter'
15
+ require 'music-performance/conversion/score_time_converter'
16
+ require 'music-performance/conversion/score_collator'
17
+ require 'music-performance/conversion/glissando_converter'
18
+ require 'music-performance/conversion/portamento_converter'
19
+ require 'music-performance/conversion/note_sequence_extractor'
20
+
21
+ require 'midilib'
22
+ require 'music-performance/arrangement/midi/midi_util'
23
+ require 'music-performance/arrangement/midi/midi_events'
24
+ require 'music-performance/arrangement/midi/part_sequencer'
25
+ require 'music-performance/arrangement/midi/score_sequencer'
@@ -0,0 +1,9 @@
1
+ module Music
2
+ module Performance
3
+
4
+ NoteOnEvent = Struct.new(:notenum, :accented)
5
+ NoteOffEvent = Struct.new(:notenum)
6
+ VolumeExpressionEvent = Struct.new(:volume)
7
+
8
+ end
9
+ end
@@ -0,0 +1,38 @@
1
+ module Music
2
+ module Performance
3
+
4
+ class MidiUtil
5
+ QUARTER = Rational(1,4)
6
+ def self.delta duration, ppqn
7
+ pulses = (duration / QUARTER) * ppqn
8
+ return pulses.round
9
+ end
10
+
11
+ def self.usec_per_qnote notes_per_sec
12
+ spn = 1.0 / notes_per_sec
13
+ spqn = spn / 4.0
14
+ return (spqn * 1_000_000).to_i
15
+ end
16
+
17
+ p0 = Music::Transcription::Pitch.new(octave:-1,semitone:0)
18
+ MIDI_NOTENUMS = Hash[
19
+ (0..127).map do |note_num|
20
+ [ p0.transpose(note_num), note_num ]
21
+ end
22
+ ]
23
+
24
+ def self.pitch_to_notenum pitch
25
+ MIDI_NOTENUMS[pitch.round]
26
+ end
27
+
28
+ def self.dynamic_to_volume dynamic
29
+ (dynamic * 127).round
30
+ end
31
+
32
+ def self.note_velocity(accented)
33
+ accented ? 112 : 70
34
+ end
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,121 @@
1
+ module Music
2
+ module Performance
3
+
4
+ class PartSequencer
5
+ def initialize part, dynamics_sample_rate: 50, cents_per_step: 10
6
+ extractor = NoteSequenceExtractor.new(part.notes, cents_per_step)
7
+ note_sequences = extractor.extract_sequences
8
+ note_events = gather_note_events(note_sequences)
9
+
10
+ dynamic_events = gather_dynamic_events(part.start_dynamic,
11
+ part.dynamic_changes, dynamics_sample_rate)
12
+
13
+ @events = (note_events + dynamic_events).sort_by {|x| x[0] }
14
+ end
15
+
16
+ def make_midi_track midi_sequence, part_name, channel, ppqn
17
+ track = begin_track(midi_sequence, part_name, channel)
18
+
19
+ prev_offset = 0
20
+ @events.each do |offset, event|
21
+ if offset == prev_offset
22
+ delta = 0
23
+ else
24
+ delta = MidiUtil.delta(offset - prev_offset, ppqn)
25
+ end
26
+
27
+ track.events << case event
28
+ when NoteOnEvent
29
+ vel = MidiUtil.note_velocity(event.accented)
30
+ MIDI::NoteOn.new(channel, event.notenum, vel, delta)
31
+ when NoteOffEvent
32
+ MIDI::NoteOff.new(channel, event.notenum, 127, delta)
33
+ when VolumeExpressionEvent
34
+ MIDI::Controller.new(channel, MIDI::CC_EXPRESSION_CONTROLLER, event.volume, delta)
35
+ end
36
+
37
+ prev_offset = offset
38
+ end
39
+ return track
40
+ end
41
+
42
+ private
43
+
44
+ #def add_event events_hash, offset, event
45
+ # if events_hash.has_key? offset
46
+ # events_hash[offset].push event
47
+ # else
48
+ # events_hash[offset] = [ event ]
49
+ # end
50
+ #end
51
+
52
+ def gather_note_events note_sequences
53
+ note_events = []
54
+ note_sequences.each do |note_seq|
55
+ pitches = note_seq.pitches.sort
56
+ pitches.each_index do |i|
57
+ offset, pitch = pitches[i]
58
+
59
+ accented = false
60
+ if note_seq.attacks.has_key?(offset)
61
+ accented = note_seq.attacks[offset].accented?
62
+ end
63
+
64
+ note_num = MidiUtil.pitch_to_notenum(pitch)
65
+ on_at = offset
66
+ off_at = (i < (pitches.size - 1)) ? pitches[i+1][0] : note_seq.stop
67
+
68
+ note_events.push [on_at, NoteOnEvent.new(note_num, accented)]
69
+ note_events.push [off_at, NoteOffEvent.new(note_num)]
70
+ end
71
+ end
72
+ return note_events
73
+ end
74
+
75
+ def gather_dynamic_events start_dyn, dyn_changes, sample_rate
76
+ dynamic_events = []
77
+
78
+ dyn_comp = ValueComputer.new(start_dyn,dyn_changes)
79
+ finish = 0
80
+ if dyn_changes.any?
81
+ finish, change = dyn_changes.max
82
+ if change.is_a? Music::Transcription::Change::Gradual
83
+ finish += change.duration
84
+ end
85
+ end
86
+ samples = dyn_comp.sample(0, finish, sample_rate)
87
+
88
+ prev = nil
89
+ samples.each_index do |i|
90
+ sample = samples[i]
91
+ unless sample == prev
92
+ offset = Rational(i,sample_rate)
93
+ volume = MidiUtil.dynamic_to_volume(sample)
94
+ dynamic_events.push [offset, VolumeExpressionEvent.new(volume)]
95
+ end
96
+ prev = sample
97
+ end
98
+
99
+ return dynamic_events
100
+ end
101
+
102
+ def begin_track midi_sequence, part_name, channel
103
+ # Track to hold part notes
104
+ track = MIDI::Track.new(midi_sequence)
105
+
106
+ # Name the track and instrument
107
+ track.name = part_name.to_s
108
+ track.instrument = MIDI::GM_PATCH_NAMES[0]
109
+
110
+ # Add a volume controller event (optional).
111
+ track.events << MIDI::Controller.new(channel, MIDI::CC_VOLUME, 127)
112
+
113
+ # Change to particular instrument sound
114
+ track.events << MIDI::ProgramChange.new(channel, 1, 0)
115
+
116
+ return track
117
+ end
118
+ end
119
+
120
+ end
121
+ end
@@ -0,0 +1,33 @@
1
+ module Music
2
+ module Performance
3
+
4
+ class ScoreSequencer
5
+ def initialize score
6
+ start_nps = NoteTimeConverter.notes_per_second(score.start_tempo,
7
+ score.start_meter.beat_duration)
8
+ @start_usec_per_qnote = MidiUtil.usec_per_qnote(start_nps)
9
+ @parts = ScoreCollator.new(score).collate_parts
10
+ end
11
+
12
+ def make_midi_seq
13
+ seq = MIDI::Sequence.new()
14
+
15
+ # first track for the sequence holds time sig and tempo events
16
+ track0 = MIDI::Track.new(seq)
17
+ seq.tracks << track0
18
+ track0.events << MIDI::Tempo.new(@start_usec_per_qnote)
19
+ track0.events << MIDI::MetaEvent.new(MIDI::META_SEQ_NAME, 'Sequence Name')
20
+
21
+ channel = 0
22
+ @parts.each do |part_name,part|
23
+ pseq = PartSequencer.new(part)
24
+ seq.tracks << pseq.make_midi_track(seq, part_name, channel, seq.ppqn)
25
+ channel += 1
26
+ end
27
+
28
+ return seq
29
+ end
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ module Music
2
+ module Performance
3
+
4
+ class GlissandoConverter
5
+ def self.glissando_pitches(start_pitch, target_pitch)
6
+ start, finish = start_pitch.total_semitones, target_pitch.total_semitones
7
+ if finish >= start
8
+ semitones = start.ceil.upto(finish.floor).to_a
9
+ else
10
+ semitones = start.floor.downto(finish.ceil).to_a
11
+ end
12
+
13
+ if semitones.empty? || semitones[0] != start
14
+ semitones.unshift(start)
15
+ end
16
+
17
+ if semitones.size > 1 && semitones[-1] == finish
18
+ semitones.pop
19
+ end
20
+
21
+ semitones.map do |semitone|
22
+ Music::Transcription::Pitch.from_semitones(semitone)
23
+ end
24
+ end
25
+
26
+ def self.glissando_elements(start_pitch, target_pitch, duration, accented)
27
+ pitches = glissando_pitches(start_pitch, target_pitch)
28
+ subdur = Rational(duration, pitches.size)
29
+ pitches.map do |pitch|
30
+ LegatoElement.new(subdur, pitch, accented)
31
+ end
32
+ end
33
+ end
34
+
35
+ end
36
+ end