waveformjson 0.0.1

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.
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: