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
checksums.yaml
ADDED
@@ -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
|
data/.document
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.0.0
|
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup rdoc --title "music-performance Documentation" --protected
|
data/ChangeLog.rdoc
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.rdoc
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/bin/midify
ADDED
@@ -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,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
|