music-transcription 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +3 -0
- data/.gitignore +7 -0
- data/.rspec +1 -0
- data/.yardopts +1 -0
- data/ChangeLog.rdoc +4 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +28 -0
- data/Rakefile +54 -0
- data/bin/transcribe +176 -0
- data/lib/music-transcription.rb +20 -0
- data/lib/music-transcription/arrangement.rb +31 -0
- data/lib/music-transcription/instrument_config.rb +38 -0
- data/lib/music-transcription/interval.rb +66 -0
- data/lib/music-transcription/link.rb +115 -0
- data/lib/music-transcription/note.rb +156 -0
- data/lib/music-transcription/part.rb +128 -0
- data/lib/music-transcription/pitch.rb +297 -0
- data/lib/music-transcription/pitch_constants.rb +204 -0
- data/lib/music-transcription/profile.rb +105 -0
- data/lib/music-transcription/program.rb +136 -0
- data/lib/music-transcription/score.rb +122 -0
- data/lib/music-transcription/tempo.rb +44 -0
- data/lib/music-transcription/transition.rb +71 -0
- data/lib/music-transcription/value_change.rb +85 -0
- data/lib/music-transcription/version.rb +7 -0
- data/music-transcription.gemspec +36 -0
- data/samples/arrangements/glissando_test.yml +71 -0
- data/samples/arrangements/hip.yml +952 -0
- data/samples/arrangements/instrument_test.yml +119 -0
- data/samples/arrangements/legato_test.yml +237 -0
- data/samples/arrangements/make_glissando_test.rb +27 -0
- data/samples/arrangements/make_hip.rb +75 -0
- data/samples/arrangements/make_instrument_test.rb +34 -0
- data/samples/arrangements/make_legato_test.rb +37 -0
- data/samples/arrangements/make_missed_connection.rb +72 -0
- data/samples/arrangements/make_portamento_test.rb +27 -0
- data/samples/arrangements/make_slur_test.rb +37 -0
- data/samples/arrangements/make_song1.rb +84 -0
- data/samples/arrangements/make_song2.rb +69 -0
- data/samples/arrangements/missed_connection.yml +481 -0
- data/samples/arrangements/portamento_test.yml +71 -0
- data/samples/arrangements/slur_test.yml +237 -0
- data/samples/arrangements/song1.yml +640 -0
- data/samples/arrangements/song2.yml +429 -0
- data/spec/instrument_config_spec.rb +47 -0
- data/spec/interval_spec.rb +38 -0
- data/spec/link_spec.rb +22 -0
- data/spec/musicality_spec.rb +7 -0
- data/spec/note_spec.rb +65 -0
- data/spec/part_spec.rb +87 -0
- data/spec/pitch_spec.rb +139 -0
- data/spec/profile_spec.rb +24 -0
- data/spec/program_spec.rb +55 -0
- data/spec/score_spec.rb +55 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/transition_spec.rb +13 -0
- data/spec/value_change_spec.rb +19 -0
- metadata +239 -0
data/.document
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup rdoc --title "music-transcription 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-transcription
|
2
|
+
|
3
|
+
* {Homepage}[https://github.com/jamestunnell/music-transcription]
|
4
|
+
* {Issues}[https://github.com/jamestunnell/music-transcription/issues]
|
5
|
+
* {Documentation}[http://rubydoc.info/gems/music-transcription/frames]
|
6
|
+
* {Email}[mailto:jamestunnell@lavabit.com]
|
7
|
+
|
8
|
+
== Description
|
9
|
+
|
10
|
+
Classes for representing music features, like pitch, note, dynamic, tempo, etc.
|
11
|
+
|
12
|
+
== Features
|
13
|
+
|
14
|
+
== Examples
|
15
|
+
|
16
|
+
require 'music-transcription'
|
17
|
+
|
18
|
+
== Requirements
|
19
|
+
|
20
|
+
== Install
|
21
|
+
|
22
|
+
$ gem install music-transcription
|
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/transcribe
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'micro-optparse'
|
4
|
+
require 'pry'
|
5
|
+
require 'spcore'
|
6
|
+
require 'wavefile'
|
7
|
+
require 'music-transcription'
|
8
|
+
require 'yaml'
|
9
|
+
|
10
|
+
include Music::Transcription
|
11
|
+
|
12
|
+
options = Parser.new do |p|
|
13
|
+
p.banner = <<-END
|
14
|
+
Transcribe notes from an audio file.
|
15
|
+
|
16
|
+
Usage:
|
17
|
+
transcribe [options] <audio_file>
|
18
|
+
|
19
|
+
Notes:
|
20
|
+
Output directory must already exist.
|
21
|
+
FFT actual_size must be a power of two.
|
22
|
+
Delta (sec) must be > 0.
|
23
|
+
Threshold must be >= 0.
|
24
|
+
|
25
|
+
Options:
|
26
|
+
END
|
27
|
+
p.version = "0.1"
|
28
|
+
p.option :outdir, "set output directory", :default => "./", :value_satisfies => lambda { |path| Dir.exist? path }
|
29
|
+
p.option :freq_res, "set min frequency (Hz) resolution in Hz", :default => 1.0, :value_satisfies => lambda { |mfr| mfr > 0 }
|
30
|
+
p.option :time_res, "set time (sec) between FFT samples", :default => 0.01, :value_satisfies => lambda { |dt| dt > 0 }
|
31
|
+
p.option :threshold, "set theshold (minimum signal energy to run FFT)", :default => 0.5, :value_satisfies => lambda { |t| t >= 0 }
|
32
|
+
p.option :min_freq, "set minimum frequency to consider during harmonic series analysis.", :default => PITCHES.first.freq, :value_satisfies => lambda { |f| f > 0 }
|
33
|
+
p.option :verbose, "is verbose?", :default => false
|
34
|
+
end.process!
|
35
|
+
|
36
|
+
outdir = File.expand_path(options[:outdir])
|
37
|
+
freq_res = options[:freq_res]
|
38
|
+
time_res = options[:time_res]
|
39
|
+
threshold = options[:threshold]
|
40
|
+
verbose = options[:verbose]
|
41
|
+
min_freq = options[:min_freq]
|
42
|
+
|
43
|
+
TIME_RESOLUTION = time_res
|
44
|
+
THRESHOLD = threshold
|
45
|
+
FREQ_RESOLUTION = freq_res
|
46
|
+
|
47
|
+
def force_power_of_two size
|
48
|
+
power_of_two = Math::log2(size)
|
49
|
+
if power_of_two.floor != power_of_two # input size is not an even power of two
|
50
|
+
size = 2**(power_of_two.to_i() + 1)
|
51
|
+
end
|
52
|
+
return size
|
53
|
+
end
|
54
|
+
|
55
|
+
ARGV.each do |filename|
|
56
|
+
|
57
|
+
if !File.exist?(filename)
|
58
|
+
puts "Could not find file #{filename}, skipping."
|
59
|
+
next
|
60
|
+
end
|
61
|
+
|
62
|
+
if verbose
|
63
|
+
puts
|
64
|
+
end
|
65
|
+
|
66
|
+
outfile = File.basename(filename, ".*") + ".yml"
|
67
|
+
outpath = File.join(outdir, outfile)
|
68
|
+
|
69
|
+
notes = []
|
70
|
+
|
71
|
+
WaveFile::Reader.new(filename) do |reader|
|
72
|
+
print "Transcribing #{File.basename(filename)} -> #{outfile} 0.0%"
|
73
|
+
|
74
|
+
if reader.format.channels == 1
|
75
|
+
samples = reader.read(reader.total_sample_frames).samples
|
76
|
+
else
|
77
|
+
puts "Multi-channel audo files not supported yet. Skipping."
|
78
|
+
# TODO handl multi-channel audio
|
79
|
+
break
|
80
|
+
end
|
81
|
+
signal = SPCore::Signal.new(:data => samples, :sample_rate => reader.format.sample_rate)
|
82
|
+
|
83
|
+
ideal_size = (TIME_RESOLUTION * signal.sample_rate).to_i
|
84
|
+
fft_size = reader.format.sample_rate / FREQ_RESOLUTION
|
85
|
+
fft_size = force_power_of_two fft_size
|
86
|
+
|
87
|
+
# more samples are in the chunk than needed by the FFT. Make the FFT size
|
88
|
+
# greater or equal to chunk size.
|
89
|
+
if ideal_size > fft_size
|
90
|
+
fft_size = force_power_of_two ideal_size
|
91
|
+
end
|
92
|
+
|
93
|
+
i = 0
|
94
|
+
while(i < signal.size)
|
95
|
+
print "\b" * 6
|
96
|
+
print "%5.1f%%" % (100 * i / signal.size)
|
97
|
+
|
98
|
+
actual_size = ideal_size
|
99
|
+
if (actual_size + i) > signal.size
|
100
|
+
actual_size = signal.size - i
|
101
|
+
end
|
102
|
+
t = i / signal.sample_rate.to_f
|
103
|
+
puts t if verbose
|
104
|
+
|
105
|
+
chunk = signal.subset i...(i + actual_size)
|
106
|
+
duration_sec = actual_size / signal.sample_rate.to_f
|
107
|
+
|
108
|
+
if chunk.energy > THRESHOLD
|
109
|
+
|
110
|
+
|
111
|
+
window = SPCore::BlackmanWindow.new(chunk.size)
|
112
|
+
chunk.multiply! window.data
|
113
|
+
|
114
|
+
remaining_before = (fft_size - chunk.size) / 2
|
115
|
+
remaining_after = fft_size - chunk.size - remaining_before
|
116
|
+
|
117
|
+
if remaining_before > 0
|
118
|
+
chunk.prepend! Array.new(remaining_before, 0)
|
119
|
+
end
|
120
|
+
|
121
|
+
if remaining_after > 0
|
122
|
+
chunk.append! Array.new(remaining_after, 0)
|
123
|
+
end
|
124
|
+
|
125
|
+
series = chunk.harmonic_series(:min_freq => min_freq)
|
126
|
+
intervals = []
|
127
|
+
if series.nil?
|
128
|
+
if series.any?
|
129
|
+
fundamental = series.min
|
130
|
+
pitch = Pitch.make_from_freq fundamental
|
131
|
+
intervals.push Interval.new(:pitch => pitch)
|
132
|
+
end
|
133
|
+
notes.push Note.new(:duration => duration_sec, :intervals => intervals)
|
134
|
+
else
|
135
|
+
notes.push Note.new(:duration => duration_sec, :intervals => [])
|
136
|
+
end
|
137
|
+
|
138
|
+
i += ideal_size
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
new_notes = []
|
143
|
+
|
144
|
+
# simplify the part
|
145
|
+
notes.each_index do |i|
|
146
|
+
note = notes[i]
|
147
|
+
|
148
|
+
if new_notes.any?
|
149
|
+
prev_note = new_notes.last
|
150
|
+
|
151
|
+
if prev_note.intervals.any? and note.intervals.any?
|
152
|
+
if prev_note.intervals == note.intervals
|
153
|
+
prev_note.duration += note.duration
|
154
|
+
else
|
155
|
+
prev_note.intervals.first.link = slur(note.intervals.first.pitch)
|
156
|
+
new_notes.push note
|
157
|
+
end
|
158
|
+
elsif prev_note.intervals.empty? and note.intervals.empty?
|
159
|
+
prev_note.duration += note.duration
|
160
|
+
else
|
161
|
+
new_notes.push note
|
162
|
+
end
|
163
|
+
else
|
164
|
+
new_notes.push note
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
part = Part.new(:notes => new_notes)
|
169
|
+
yaml = part.make_hash.to_yaml
|
170
|
+
|
171
|
+
File.open(outpath, 'w') do |file|
|
172
|
+
file.write yaml
|
173
|
+
end
|
174
|
+
|
175
|
+
puts
|
176
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'hashmake'
|
2
|
+
|
3
|
+
# basic core classes
|
4
|
+
require 'music-transcription/version'
|
5
|
+
|
6
|
+
# code for transcribing (representing) music
|
7
|
+
require 'music-transcription/pitch'
|
8
|
+
require 'music-transcription/pitch_constants'
|
9
|
+
require 'music-transcription/link'
|
10
|
+
require 'music-transcription/interval'
|
11
|
+
require 'music-transcription/note'
|
12
|
+
require 'music-transcription/transition'
|
13
|
+
require 'music-transcription/value_change'
|
14
|
+
require 'music-transcription/profile'
|
15
|
+
require 'music-transcription/part'
|
16
|
+
require 'music-transcription/program'
|
17
|
+
require 'music-transcription/tempo'
|
18
|
+
require 'music-transcription/score'
|
19
|
+
require 'music-transcription/instrument_config'
|
20
|
+
require 'music-transcription/arrangement'
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Music
|
2
|
+
module Transcription
|
3
|
+
|
4
|
+
# Contains a Score object, and also instrument configurations to be assigned to
|
5
|
+
# score parts.
|
6
|
+
class Arrangement
|
7
|
+
include Hashmake::HashMakeable
|
8
|
+
|
9
|
+
# specifies which hashed args can be used for initialize.
|
10
|
+
ARG_SPECS = {
|
11
|
+
:score => arg_spec(:reqd => true, :type => Score),
|
12
|
+
:instrument_configs => arg_spec_hash(:reqd => false, :type => InstrumentConfig),
|
13
|
+
}
|
14
|
+
|
15
|
+
attr_reader :score, :instrument_configs
|
16
|
+
|
17
|
+
def initialize args
|
18
|
+
hash_make args, Arrangement::ARG_SPECS
|
19
|
+
end
|
20
|
+
|
21
|
+
# Assign a new score.
|
22
|
+
# @param [Score] score The new score.
|
23
|
+
# @raise [ArgumentError] if score is not a Score.
|
24
|
+
def score= score
|
25
|
+
Arrangement::ARG_SPECS[:score].validate_value score
|
26
|
+
@score = score
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Music
|
2
|
+
module Transcription
|
3
|
+
|
4
|
+
# Contains all the information needed to create the instrument plugin, configure
|
5
|
+
# initial settings, and any settings changes.
|
6
|
+
#
|
7
|
+
# @author James Tunnell
|
8
|
+
class InstrumentConfig
|
9
|
+
include Hashmake::HashMakeable
|
10
|
+
|
11
|
+
attr_reader :plugin_name, :initial_settings, :setting_changes
|
12
|
+
|
13
|
+
# hashed-arg specs (for hash-makeable idiom)
|
14
|
+
ARG_SPECS = {
|
15
|
+
:plugin_name => arg_spec(:reqd => true, :type => String),
|
16
|
+
:initial_settings => arg_spec(:reqd => false, :default => ->(){ {} }, :validator => ->(a){ a.is_a?(String) || a.is_a?(Array) || a.is_a?(Hash)} ),
|
17
|
+
:setting_changes => arg_spec_hash(:reqd => false, :type => Hash)
|
18
|
+
}
|
19
|
+
|
20
|
+
# A new instance of InstrumentConfig.
|
21
|
+
# @param [Hash] args Hashed arguments. Required key is :plugin_name (String).
|
22
|
+
# Optional key is :initial_settings and setting_changes.
|
23
|
+
def initialize args={}
|
24
|
+
hash_make args, InstrumentConfig::ARG_SPECS
|
25
|
+
|
26
|
+
@setting_changes.each do |offset, hash|
|
27
|
+
hash.each do |key, val|
|
28
|
+
# replace plain values with immediate value changes
|
29
|
+
unless val.is_a?(ValueChange)
|
30
|
+
hash[keys] = immediate_change(val)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Music
|
2
|
+
module Transcription
|
3
|
+
|
4
|
+
# Describes a pitch (for one note) that can be linked to a pitch in another note.
|
5
|
+
#
|
6
|
+
# @author James Tunnell
|
7
|
+
#
|
8
|
+
# @!attribute [rw] pitch
|
9
|
+
# @return [Pitch] The pitch of the note.
|
10
|
+
#
|
11
|
+
# @!attribute [rw] link
|
12
|
+
# @return [Link] Shows how the current note is related to a following note.
|
13
|
+
#
|
14
|
+
class Interval
|
15
|
+
include Hashmake::HashMakeable
|
16
|
+
attr_reader :pitch, :link
|
17
|
+
|
18
|
+
# hashed-arg specs (for hash-makeable idiom)
|
19
|
+
ARG_SPECS = {
|
20
|
+
:pitch => arg_spec(:type => Pitch, :reqd => true),
|
21
|
+
:link => arg_spec(:type => Link, :reqd => false, :default => ->(){ Link.new } ),
|
22
|
+
}
|
23
|
+
|
24
|
+
def initialize args
|
25
|
+
hash_make args
|
26
|
+
end
|
27
|
+
|
28
|
+
# Return true if the @link relationship is not NONE.
|
29
|
+
def linked?
|
30
|
+
@link.relationship != Link::RELATIONSHIP_NONE
|
31
|
+
end
|
32
|
+
|
33
|
+
# Compare the equality of another Interval object.
|
34
|
+
def == other
|
35
|
+
return (@pitch == other.pitch) && (@link == other.link)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Set the note pitch.
|
39
|
+
# @param [Pitch] pitch The pitch to use.
|
40
|
+
# @raise [ArgumentError] if pitch is not a Pitch.
|
41
|
+
def pitch= pitch
|
42
|
+
ARG_SPECS[:pitch].validate_value pitch
|
43
|
+
@pitch = pitch
|
44
|
+
end
|
45
|
+
|
46
|
+
# Setup the relationship to a following note.
|
47
|
+
# @param [Link] link The Link object to assign.
|
48
|
+
def link= link
|
49
|
+
ARG_SPECS[:link].validate_value link
|
50
|
+
@link = link
|
51
|
+
end
|
52
|
+
|
53
|
+
# Produce an identical Note object.
|
54
|
+
def clone
|
55
|
+
Interval.new(:pitch => @pitch, :link => @link.clone)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
module_function
|
60
|
+
|
61
|
+
def interval pitch, link = Link.new
|
62
|
+
Interval.new(:pitch => pitch, :link => link)
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|