music-transcription 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/.document +3 -0
  2. data/.gitignore +7 -0
  3. data/.rspec +1 -0
  4. data/.yardopts +1 -0
  5. data/ChangeLog.rdoc +4 -0
  6. data/Gemfile +3 -0
  7. data/LICENSE.txt +20 -0
  8. data/README.rdoc +28 -0
  9. data/Rakefile +54 -0
  10. data/bin/transcribe +176 -0
  11. data/lib/music-transcription.rb +20 -0
  12. data/lib/music-transcription/arrangement.rb +31 -0
  13. data/lib/music-transcription/instrument_config.rb +38 -0
  14. data/lib/music-transcription/interval.rb +66 -0
  15. data/lib/music-transcription/link.rb +115 -0
  16. data/lib/music-transcription/note.rb +156 -0
  17. data/lib/music-transcription/part.rb +128 -0
  18. data/lib/music-transcription/pitch.rb +297 -0
  19. data/lib/music-transcription/pitch_constants.rb +204 -0
  20. data/lib/music-transcription/profile.rb +105 -0
  21. data/lib/music-transcription/program.rb +136 -0
  22. data/lib/music-transcription/score.rb +122 -0
  23. data/lib/music-transcription/tempo.rb +44 -0
  24. data/lib/music-transcription/transition.rb +71 -0
  25. data/lib/music-transcription/value_change.rb +85 -0
  26. data/lib/music-transcription/version.rb +7 -0
  27. data/music-transcription.gemspec +36 -0
  28. data/samples/arrangements/glissando_test.yml +71 -0
  29. data/samples/arrangements/hip.yml +952 -0
  30. data/samples/arrangements/instrument_test.yml +119 -0
  31. data/samples/arrangements/legato_test.yml +237 -0
  32. data/samples/arrangements/make_glissando_test.rb +27 -0
  33. data/samples/arrangements/make_hip.rb +75 -0
  34. data/samples/arrangements/make_instrument_test.rb +34 -0
  35. data/samples/arrangements/make_legato_test.rb +37 -0
  36. data/samples/arrangements/make_missed_connection.rb +72 -0
  37. data/samples/arrangements/make_portamento_test.rb +27 -0
  38. data/samples/arrangements/make_slur_test.rb +37 -0
  39. data/samples/arrangements/make_song1.rb +84 -0
  40. data/samples/arrangements/make_song2.rb +69 -0
  41. data/samples/arrangements/missed_connection.yml +481 -0
  42. data/samples/arrangements/portamento_test.yml +71 -0
  43. data/samples/arrangements/slur_test.yml +237 -0
  44. data/samples/arrangements/song1.yml +640 -0
  45. data/samples/arrangements/song2.yml +429 -0
  46. data/spec/instrument_config_spec.rb +47 -0
  47. data/spec/interval_spec.rb +38 -0
  48. data/spec/link_spec.rb +22 -0
  49. data/spec/musicality_spec.rb +7 -0
  50. data/spec/note_spec.rb +65 -0
  51. data/spec/part_spec.rb +87 -0
  52. data/spec/pitch_spec.rb +139 -0
  53. data/spec/profile_spec.rb +24 -0
  54. data/spec/program_spec.rb +55 -0
  55. data/spec/score_spec.rb +55 -0
  56. data/spec/spec_helper.rb +23 -0
  57. data/spec/transition_spec.rb +13 -0
  58. data/spec/value_change_spec.rb +19 -0
  59. metadata +239 -0
@@ -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
+ --markup rdoc --title "music-transcription Documentation" --protected
@@ -0,0 +1,4 @@
1
+ === 0.3.0 / 2013-08-07
2
+
3
+ * Tear off music transcription-related code from musicality gem, into a separate gem called music-transcription.
4
+
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-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.
@@ -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,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