waveformjson 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,97 @@
1
+ Waveformjson
2
+ ========
3
+
4
+ Waveformjson is based on [waveform](https://github.com/benalavi/waveform) and functions the same. The only difference is that it generates json instead of json files.
5
+
6
+ Installation
7
+ ============
8
+
9
+ Waveformjson depends on `ruby-audio`, which in turn depends on libsndfile.
10
+
11
+ Build libsndfile from (http://www.mega-nerd.com/libsndfile/), install it via `apt` (`sudo apt-get install libsndfile1-dev`), `libsndfile` in macports, etc...
12
+
13
+ Then:
14
+
15
+ $ gem install waveformjson
16
+
17
+ or add following to Gemfile:
18
+
19
+ gem 'waveformjson'
20
+
21
+ CLI Usage
22
+ =========
23
+
24
+ $ waveformjson song.wav waveform.json
25
+
26
+ There are some nifty options you can supply to switch things up:
27
+
28
+ -m sets the method used to sample the source audio file, it can either be
29
+ 'peak' or 'rms'. 'peak' is probably what you want because it looks
30
+ cooler, but 'rms' is closer to what you actually hear.
31
+ -W sets the width (in pixels) of the waveform.
32
+
33
+ There are also some less-nifty options:
34
+
35
+ -q will generate your waveform without printing out a bunch of stuff.
36
+ -h will print out a help screen with all this info.
37
+ -F will automatically overwrite destination file.
38
+
39
+ Usage in code
40
+ =============
41
+
42
+ The CLI is really just a thin wrapper around the Waveformjson class, which you can also use in your programs for reasons I haven't thought of. The Waveform class takes pretty much the same options as the CLI when generating waveforms.
43
+
44
+ Requirements
45
+ ============
46
+
47
+ `ruby-audio`
48
+
49
+ The gem version, *not* the old outdated library listed on RAA. `ruby-audio` is a wrapper for `libsndfile`, on my Ubuntu 10.04LTS VM I installed the necessary libs to build `ruby-audio` via: `sudo apt-get install libsndfile1-dev`.
50
+
51
+ Converting MP3 to WAV
52
+ =====================
53
+
54
+ Waveform used to (very thinly) wrap ffmpeg to convert MP3 (and whatever other format) to WAV audio before processing the WAV and generating the waveform image. It seemed a bit presumptious for Waveform to handle that, especially since you might want to use your own conversion options (i.e. downsampling the bitrate to generate waveforms faster, etc...).
55
+
56
+ If you happen to be using ffmpeg, you can easily convert MP3 to WAV via:
57
+
58
+ ffmpeg -i "/path/to/source/file.mp3" -f wav "/path/to/output/file.wav"
59
+
60
+ Tests
61
+ =====
62
+
63
+ $ rake
64
+
65
+
66
+ Sample sound file used in tests is in the Public Domain from soundbible.com: <http://soundbible.com/1598-Electronic-Chime.html>.
67
+
68
+ References
69
+ ==========
70
+
71
+ <http://pscode.org/javadoc/src-html/org/pscode/ui/audiotrace/AudioPlotPanel.html#line.996>
72
+ <http://github.com/pangdudu/rude/blob/master/lib/waveform_narray_testing.rb>
73
+ <http://stackoverflow.com/questions/1931952/asp-net-create-waveform-image-from-mp3>
74
+ <http://codeidol.com/java/swing/Audio/Build-an-Audio-Waveform-Display>
75
+
76
+ License
77
+ =======
78
+
79
+ Copyright (c) 2013 liufengyun
80
+
81
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
82
+ this software and associated documentation files (the "Software"), to deal in
83
+ the Software without restriction, including without limitation the rights to
84
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
85
+ of the Software, and to permit persons to whom the Software is furnished to do
86
+ so, subject to the following conditions:
87
+
88
+ The above copyright notice and this permission notice shall be included in all
89
+ copies or substantial portions of the Software.
90
+
91
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
92
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
93
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
94
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
95
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
96
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
97
+ SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ task :default => :test
2
+
3
+ task :test do
4
+ system "ruby test/*.rb"
5
+ end
data/bin/waveformjson ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env ruby
2
+ require "waveformjson"
3
+ require "optparse"
4
+
5
+ options = Waveformjson::DefaultOptions
6
+ optparse = OptionParser.new do |o|
7
+ o.banner = "Usage: waveformjson [options] source_audio [ouput.json]"
8
+ o.version = Waveformjson::VERSION
9
+
10
+ o.on("-m", "--method METHOD", "Wave analyzation method (can be 'peak' or 'rms') -- Default '#{Waveformjson::DefaultOptions[:method]}'.") do |method|
11
+ options[:method] = method.to_sym
12
+ end
13
+
14
+ o.on("-W", "--width WIDTH", "Width (in pixels) of generated waveform image -- Default #{Waveformjson::DefaultOptions[:width]}.") do |width|
15
+ options[:width] = width.to_i
16
+ end
17
+
18
+ options[:logger] = $stdout
19
+ o.on("-q", "--quiet", "Don't print anything out when generating waveform") do
20
+ options[:logger] = nil
21
+ end
22
+
23
+ options[:force] = false
24
+ o.on("-F", "--force", "Force generationg of waveform if file exists") do
25
+ options[:force] = true
26
+ end
27
+
28
+ o.on("-h", "--help", "Display this screen") do
29
+ puts o
30
+ exit
31
+ end
32
+ end
33
+
34
+ optparse.parse!
35
+
36
+ begin
37
+ output = ARGV[1] || "waveform.json"
38
+
39
+ if File.exists?(output) && !options[:force]
40
+ raise RuntimeError.new("Destination file #{output} exists. Use --force if you want to automatically remove it.")
41
+ end
42
+
43
+ json = Waveformjson.generate(ARGV[0], options)
44
+ File.open(output) do |f|
45
+ f << json.to_s
46
+ end
47
+ rescue ArgumentError => e
48
+ puts e.message + "\n\n"
49
+ puts optparse
50
+ rescue RuntimeError => e
51
+ puts e.message
52
+ end
@@ -0,0 +1,195 @@
1
+ require File.join(File.dirname(__FILE__), "waveformjson/version")
2
+
3
+ require "ruby-audio"
4
+
5
+ class Waveformjson
6
+ DefaultOptions = {
7
+ :method => :peak,
8
+ :width => 1800,
9
+ :logger => nil
10
+ }
11
+
12
+ attr_reader :source
13
+
14
+ class << self
15
+ # Generate a Waveform image at the given filename with the given options.
16
+ #
17
+ # Available options (all optional) are:
18
+ #
19
+ # :method => The method used to read sample frames, available methods
20
+ # are peak and rms. peak is probably what you're used to seeing, it uses
21
+ # the maximum amplitude per sample to generate the waveform, so the
22
+ # waveform looks more dynamic. RMS gives a more fluid waveform and
23
+ # probably more accurately reflects what you hear, but isn't as
24
+ # pronounced (typically).
25
+ #
26
+ # Can be :rms or :peak
27
+ # Default is :peak.
28
+ #
29
+ # :width => The width (in pixels) of the final waveform image.
30
+ # Default is 1800.
31
+ #
32
+ # :logger => IOStream to log progress to.
33
+ #
34
+ # Example:
35
+ # Waveformjson.generate("Kickstart My Heart.wav")
36
+ # Waveformjson.generate("Kickstart My Heart.wav", :method => :rms)
37
+ # Waveformjson.generate("Kickstart My Heart.wav", :logger => $stdout)
38
+ #
39
+ def generate(source, options={})
40
+ options = DefaultOptions.merge(options)
41
+
42
+ raise ArgumentError.new("No source audio filename given, must be an existing sound file.") unless source
43
+ raise RuntimeError.new("Source audio file '#{source}' not found.") unless File.exist?(source)
44
+
45
+ @log = Log.new(options[:logger])
46
+ @log.start!
47
+
48
+ # Frames gives the amplitudes for each channel, for our waveform we're
49
+ # saying the "visual" amplitude is the average of the amplitude across all
50
+ # the channels. This might be a little weird w/ the "peak" method if the
51
+ # frames are very wide (i.e. the image width is very small) -- I *think*
52
+ # the larger the frames are, the more "peaky" the waveform should get,
53
+ # perhaps to the point of inaccurately reflecting the actual sound.
54
+ samples = frames(source, options[:width], options[:method]).collect do |frame|
55
+ frame.inject(0.0) { |sum, peak| sum + peak } / frame.size
56
+ end
57
+
58
+ samples
59
+ end
60
+
61
+ private
62
+
63
+ # Returns a sampling of frames from the given RubyAudio::Sound using the
64
+ # given method the sample size is determined by the given pixel width --
65
+ # we want one sample frame per horizontal pixel.
66
+ def frames(source, width, method = :peak)
67
+ raise ArgumentError.new("Unknown sampling method #{method}") unless [ :peak, :rms ].include?(method)
68
+
69
+ frames = []
70
+
71
+ RubyAudio::Sound.open(source) do |audio|
72
+ frames_read = 0
73
+ frames_per_sample = (audio.info.frames.to_f / width.to_f).floor
74
+ sample = RubyAudio::Buffer.new("float", frames_per_sample, audio.info.channels)
75
+
76
+ @log.timed("Sampling #{frames_per_sample} frames per sample: ") do
77
+ while(frames_read = audio.read(sample)) > 0
78
+ frames << send(method, sample, audio.info.channels)
79
+ @log.out(".")
80
+ end
81
+ end
82
+ end
83
+
84
+ frames
85
+ rescue RubyAudio::Error => e
86
+ raise e unless e.message == "File contains data in an unknown format."
87
+ raise RuntimeError.new("Source audio file #{source} could not be read by RubyAudio library -- Hint: non-WAV files are no longer supported, convert to WAV first using something like ffmpeg (RubyAudio: #{e.message})")
88
+ end
89
+
90
+ # Returns an array of the peak of each channel for the given collection of
91
+ # frames -- the peak is individual to the channel, and the returned collection
92
+ # of peaks are not (necessarily) from the same frame(s).
93
+ def peak(frames, channels=1)
94
+ peak_frame = []
95
+ (0..channels-1).each do |channel|
96
+ peak_frame << channel_peak(frames, channel)
97
+ end
98
+ peak_frame
99
+ end
100
+
101
+ # Returns an array of rms values for the given frameset where each rms value is
102
+ # the rms value for that channel.
103
+ def rms(frames, channels=1)
104
+ rms_frame = []
105
+ (0..channels-1).each do |channel|
106
+ rms_frame << channel_rms(frames, channel)
107
+ end
108
+ rms_frame
109
+ end
110
+
111
+ # Returns the peak voltage reached on the given channel in the given collection
112
+ # of frames.
113
+ #
114
+ # TODO: Could lose some resolution and only sample every other frame, would
115
+ # likely still generate the same waveform as the waveform is so comparitively
116
+ # low resolution to the original input (in most cases), and would increase
117
+ # the analyzation speed (maybe).
118
+ def channel_peak(frames, channel=0)
119
+ peak = 0.0
120
+ frames.each do |frame|
121
+ next if frame.nil?
122
+ frame = Array(frame)
123
+ peak = frame[channel].abs if frame[channel].abs > peak
124
+ end
125
+ peak
126
+ end
127
+
128
+ # Returns the rms value across the given collection of frames for the given
129
+ # channel.
130
+ def channel_rms(frames, channel=0)
131
+ Math.sqrt(frames.inject(0.0){ |sum, frame| sum += (frame ? Array(frame)[channel] ** 2 : 0) } / frames.size)
132
+ end
133
+ end
134
+ end
135
+
136
+ class Waveformjson
137
+ # A simple class for logging + benchmarking, nice to have good feedback on a
138
+ # long batch operation.
139
+ #
140
+ # There's probably 10,000,000 other bechmarking classes, but writing this was
141
+ # easier than using Google.
142
+ class Log
143
+ attr_accessor :io
144
+
145
+ def initialize(io=$stdout)
146
+ @io = io
147
+ end
148
+
149
+ # Prints the given message to the log
150
+ def out(msg)
151
+ io.print(msg) if io
152
+ end
153
+
154
+ # Prints the given message to the log followed by the most recent benchmark
155
+ # (note that it calls .end! which will stop the benchmark)
156
+ def done!(msg="")
157
+ out "#{msg} (#{self.end!}s)\n"
158
+ end
159
+
160
+ # Starts a new benchmark clock and returns the index of the new clock.
161
+ #
162
+ # If .start! is called again before .end! then the time returned will be
163
+ # the elapsed time from the next call to start!, and calling .end! again
164
+ # will return the time from *this* call to start! (that is, the clocks are
165
+ # LIFO)
166
+ def start!
167
+ (@benchmarks ||= []) << Time.now
168
+ @current = @benchmarks.size - 1
169
+ end
170
+
171
+ # Returns the elapsed time from the most recently started benchmark clock
172
+ # and ends the benchmark, so that a subsequent call to .end! will return
173
+ # the elapsed time from the previously started benchmark clock.
174
+ def end!
175
+ elapsed = (Time.now - @benchmarks[@current])
176
+ @current -= 1
177
+ elapsed
178
+ end
179
+
180
+ # Returns the elapsed time from the benchmark clock w/ the given index (as
181
+ # returned from when .start! was called).
182
+ def time?(index)
183
+ Time.now - @benchmarks[index]
184
+ end
185
+
186
+ # Benchmarks the given block, printing out the given message first (if
187
+ # given).
188
+ def timed(message=nil, &block)
189
+ start!
190
+ out(message) if message
191
+ yield
192
+ done!
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,3 @@
1
+ class Waveformjson
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,54 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), "..", "lib", "waveformjson"))
2
+
3
+ require "test/unit"
4
+ require "fileutils"
5
+
6
+ module Helpers
7
+ def fixture(file)
8
+ File.join(File.dirname(__FILE__), "fixtures", file)
9
+ end
10
+ end
11
+
12
+ class WaveformTest < Test::Unit::TestCase
13
+ include Helpers
14
+ extend Helpers
15
+
16
+ def test_generates_waveform
17
+ assert_equal Waveformjson::DefaultOptions[:width], Waveformjson.generate(fixture("sample.wav")).size
18
+ end
19
+
20
+ def test_generates_waveform_from_mono_audio_source_via_peak
21
+ assert_equal Waveformjson::DefaultOptions[:width], Waveformjson.generate(fixture("mono_sample.wav")).size
22
+ end
23
+
24
+ def test_generates_waveform_from_mono_audio_source_via_rms
25
+ assert_equal Waveformjson::DefaultOptions[:width], Waveformjson.generate(fixture("mono_sample.wav"), :method => :rms).size
26
+ end
27
+
28
+ def test_uses_rms_instead_of_peak
29
+ rms = Waveformjson.generate(fixture("sample.wav"))
30
+ peak = Waveformjson.generate(fixture("sample.wav"), :method => :rms)
31
+
32
+ assert peak[44] > rms[43]
33
+ end
34
+
35
+ def test_is_900px_wide
36
+ data = Waveformjson.generate(fixture("sample.wav"), :width => 900)
37
+
38
+ assert_equal 900, data.size
39
+ end
40
+
41
+ def test_raises_error_if_not_given_readable_audio_source
42
+ assert_raise(RuntimeError) do
43
+ Waveformjson.generate(fixture("sample.txt"))
44
+ end
45
+ end
46
+
47
+ def test_raises_deprecation_exception_if_ruby_audio_fails_to_read_source_file
48
+ begin
49
+ Waveformjson.generate(fixture("sample.txt"))
50
+ rescue RuntimeError => e
51
+ assert_match /Hint: non-WAV files are no longer supported, convert to WAV first using something like ffmpeg/, e.message
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,25 @@
1
+ require "./lib/waveformjson/version"
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "waveformjson"
5
+ s.version = Waveformjson::VERSION
6
+ s.summary = "Generate waveform json from audio files"
7
+ s.description = "Generate waveform json from audio files. Includes a Waveform class for generating waveforms in your code as well as a simple command-line program called 'waveform' for generating on the command line."
8
+ s.authors = ["liufengyun"]
9
+ s.email = ["liufengyunchina@gmail.com"]
10
+ s.homepage = "http://github.com/liufengyun/waveformjson"
11
+
12
+ s.files = Dir[
13
+ "LICENSE",
14
+ "README.md",
15
+ "Rakefile",
16
+ "lib/**/*.rb",
17
+ "*.gemspec",
18
+ "test/**/*.rb",
19
+ "bin/*"
20
+ ]
21
+
22
+ s.executables = "waveformjson"
23
+
24
+ s.add_dependency "ruby-audio"
25
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: waveformjson
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - liufengyun
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: ruby-audio
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: Generate waveform json from audio files. Includes a Waveform class for
31
+ generating waveforms in your code as well as a simple command-line program called
32
+ 'waveform' for generating on the command line.
33
+ email:
34
+ - liufengyunchina@gmail.com
35
+ executables:
36
+ - waveformjson
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - README.md
41
+ - Rakefile
42
+ - lib/waveformjson/version.rb
43
+ - lib/waveformjson.rb
44
+ - waveformjson.gemspec
45
+ - test/waveform_test.rb
46
+ - bin/waveformjson
47
+ homepage: http://github.com/liufengyun/waveformjson
48
+ licenses: []
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ! '>='
57
+ - !ruby/object:Gem::Version
58
+ version: '0'
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ! '>='
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubyforge_project:
67
+ rubygems_version: 1.8.24
68
+ signing_key:
69
+ specification_version: 3
70
+ summary: Generate waveform json from audio files
71
+ test_files: []
72
+ has_rdoc: