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