music-transcription 0.3.0

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