waveform 0.0.3 → 0.1.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.
data/README.md CHANGED
@@ -1,35 +1,27 @@
1
1
  Waveform
2
2
  ========
3
3
 
4
- Waveform is a class to generate waveform images from audio files. You can
5
- combine it with jPlayer to make a soundcloud.com style MP3 player. It also
6
- comes with a handy CLI you can use to generate waveform images on the command
7
- line.
4
+ Waveform is a class to generate waveform images from audio files. You can combine it with jPlayer to make a soundcloud.com style MP3 player. It also comes with a handy CLI you can use to generate waveform images on the command line.
8
5
 
9
6
  Installation
10
7
  ============
11
8
 
12
- Build libsndfile (http://www.mega-nerd.com/libsndfile/), or install it via `apt`
13
- (`sudo apt-get install libsndfile1-dev`), or `libsndfile` in macports.
9
+ Waveform depends on `ruby-audio`, which in turn depends on libsndfile.
14
10
 
15
- $ sudo gem install waveform
11
+ Build libsndfile from (http://www.mega-nerd.com/libsndfile/), install it via `apt` (`sudo apt-get install libsndfile1-dev`), `libsndfile` in macports, etc...
16
12
 
17
- You might also want to, but don't have to:
13
+ Then:
18
14
 
19
- $ sudo gem install oily_png
20
-
21
- to make things a bit faster, and:
15
+ $ sudo gem install waveform
22
16
 
23
- $ sudo apt-get install ffmpeg
24
-
25
- if you want Waveform to convert non WAV audio for you.
17
+ Image creation depends on `chunky_png`, which has a faster native library called `oily_png` which will be used if availble.
26
18
 
27
- _See Requirements below for more info_
19
+ $ sudo gem install oily_png
28
20
 
29
21
  CLI Usage
30
22
  =========
31
23
 
32
- $ waveform song.mp3 waveform.png
24
+ $ waveform song.wav waveform.png
33
25
 
34
26
  There are some nifty options you can supply to switch things up:
35
27
 
@@ -53,51 +45,54 @@ Generating a small waveform "cut out" of a white background is pretty useful,
53
45
  then you can overlay it on a web-gradient on the website for your new startup
54
46
  and it will look really cool. To make it you could use:
55
47
 
56
- $ waveform -W900 -H140 -ctransparent -b#ffffff Motley\ Crüe/Kickstart\ my\ Heart.mp3 sweet_waveforms/Kickstart\ my\ Heart.png
48
+ $ waveform -W900 -H140 -ctransparent -b#ffffff Motley\ Crüe/Kickstart\ my\ Heart.wav sweet_waveforms/Kickstart\ my\ Heart.png
57
49
 
58
50
  Usage in code
59
51
  =============
60
52
 
61
- The CLI is really just a thin wrapper around the Waveform class, which you can
62
- also use in your programs for reasons I haven't thought of. The Waveform class
63
- takes pretty much the same options as the CLI when generating waveforms.
53
+ The CLI is really just a thin wrapper around the Waveform 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.
64
54
 
65
55
  Requirements
66
56
  ============
67
57
 
68
58
  `ruby-audio`
69
59
 
70
- The gem version, *not* the old outdated library listed on RAA. `ruby-audio` is
71
- a wrapper for `libsndfile`, on my Ubuntu 10.04LTS VM I installed the necessary
72
- libs to build `ruby-audio` via: `sudo apt-get install libsndfile1-dev`.
60
+ 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`.
73
61
 
74
62
  `chunky_png`
75
63
 
76
- `chunky_png` is a pure ruby (!) PNG manipulation library. Caveat to this
77
- requirement is that if you also install `oily_png` you will get *better
78
- performance* as it uses some C code, and C code is fast.
64
+ `chunky_png` is a pure ruby (!) PNG manipulation library. Caveat to this requirement is that if you also install `oily_png` you will get *better performance* as it uses some C code, and C code is fast.
79
65
 
80
- `ffmpeg` (sorta)
66
+ Converting MP3 to WAV
67
+ =====================
81
68
 
82
- You only need `ffmpeg` if you plan to generate waveforms from files that aren't
83
- already WAVs (like MP3, or M4A). On my same Ubuntu VM I installed it via `sudo
84
- apt-get install ffmpeg` and it was able to convert MP3 and M4A files out of the
85
- box. The formats you can convert depend on which decoders you have installed.
69
+ 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...).
86
70
 
87
- If you don't want to install ffmpeg, you could also use one of the many audio
88
- format converters to convert your files to WAV before generating waveforms.
71
+ If you happen to be using ffmpeg, you can easily convert MP3 to WAV via:
89
72
 
90
- Or you could be all retro and use WAV audio for everything in the first place.
73
+ ffmpeg -i "/path/to/source/file.mp3" -f wav "/path/to/output/file.wav"
91
74
 
92
75
  Tests
93
76
  =====
94
77
 
95
- Tests require `contest` gem & `ffmpeg` (to test conversion), run via:
78
+ $ rake
79
+
80
+ If you get an error about not being able to find ruby-audio gem (and you have ruby-audio gem) you might need to let rake know how to load your gems -- if you're using rubygems:
96
81
 
82
+ $ export RUBYOPT="rubygems"
97
83
  $ rake
98
84
 
99
- Sample sound file is in Public Domain from soundbible.com.
100
- <http://soundbible.com/1598-Electronic-Chime.html>
85
+ Sample sound file used in tests is in the Public Domain from soundbible.com: <http://soundbible.com/1598-Electronic-Chime.html>.
86
+
87
+ Changes
88
+ =======
89
+
90
+ 0.1.0
91
+ -----
92
+ * No more wrapping ffmpeg to automatically convert mp3 to wav
93
+ * Fixed support for mono audio sources (4-channel, surround, etc. should also work)
94
+ * Change to gemspec & added seperate version file so that bundler won't try to load ruby-audio (thanks, amiel)
95
+ * Changed Waveform-class API as there's no longer a need to instantiate a waveform
101
96
 
102
97
  References
103
98
  ==========
@@ -110,7 +105,7 @@ References
110
105
  License
111
106
  =======
112
107
 
113
- Copyright (c) 2010-2011 Ben Alavi
108
+ Copyright (c) 2010-2012 Ben Alavi
114
109
 
115
110
  Permission is hereby granted, free of charge, to any person obtaining a copy of
116
111
  this software and associated documentation files (the "Software"), to deal in
@@ -5,7 +5,8 @@ require "optparse"
5
5
  options = Waveform::DefaultOptions
6
6
  optparse = OptionParser.new do |o|
7
7
  o.banner = "Usage: waveform [options] source_audio [ouput.png]"
8
-
8
+ o.version = Waveform::VERSION
9
+
9
10
  o.on("-W", "--width WIDTH", "Width (in pixels) of generated waveform image -- Default #{Waveform::DefaultOptions[:width]}.") do |width|
10
11
  options[:width] = width.to_i
11
12
  end
@@ -34,9 +35,9 @@ optparse = OptionParser.new do |o|
34
35
  options[:method] = method.to_sym
35
36
  end
36
37
 
37
- options[:quiet] = false
38
+ options[:logger] = $stdout
38
39
  o.on("-q", "--quiet", "Don't print anything out when generating waveform") do
39
- options[:quiet] = true
40
+ options[:logger] = nil
40
41
  end
41
42
 
42
43
  options[:force] = false
@@ -53,11 +54,10 @@ end
53
54
  optparse.parse!
54
55
 
55
56
  begin
56
- Waveform.new(ARGV[0], options[:quiet] ? nil : $stdout).generate(ARGV[1] || "waveform.png", options)
57
+ Waveform.generate(ARGV[0], ARGV[1] || "waveform.png", options)
57
58
  rescue Waveform::ArgumentError => e
58
59
  puts e.message + "\n\n"
59
60
  puts optparse
60
61
  rescue Waveform::RuntimeError => e
61
62
  puts e.message
62
63
  end
63
-
@@ -1,6 +1,6 @@
1
- require "ruby-audio"
2
- require "tempfile"
1
+ require File.join(File.dirname(__FILE__), "waveform/version")
3
2
 
3
+ require "ruby-audio"
4
4
  begin
5
5
  require "oily_png"
6
6
  rescue LoadError
@@ -8,15 +8,14 @@ rescue LoadError
8
8
  end
9
9
 
10
10
  class Waveform
11
- VERSION = "0.0.3"
12
-
13
11
  DefaultOptions = {
14
12
  :method => :peak,
15
13
  :width => 1800,
16
14
  :height => 280,
17
15
  :background_color => "#666666",
18
16
  :color => "#00ccff",
19
- :force => false
17
+ :force => false,
18
+ :logger => nil
20
19
  }
21
20
 
22
21
  TransparencyMask = "#00ff00"
@@ -29,111 +28,115 @@ class Waveform
29
28
  class RuntimeError < ::RuntimeError;end;
30
29
  class ArgumentError < ::ArgumentError;end;
31
30
 
32
- # Setup a new Waveform for the given audio file. If given anything besides a
33
- # WAV file it will attempt to first convert the file to a WAV using ffmpeg.
34
- #
35
- # Optionally takes an IO stream to which it will print log/benchmarking info.
36
- #
37
- # See #generate for how to generate the waveform image from the given audio
38
- # file.
39
- #
40
- # Available conversions depend on your installation of ffmpeg.
41
- #
42
- # Example:
43
- #
44
- # Waveform.new("mp3s/Kickstart My Heart.mp3")
45
- # Waveform.new("mp3s/Kickstart My Heart.mp3", $stdout)
46
- #
47
- def initialize(source, log=nil)
48
- raise ArgumentError.new("No source audio filename given, must be an existing sound file.") unless source
49
- raise RuntimeError.new("Source audio file '#{source}' not found.") unless File.exist?(source)
50
-
51
- @log = Log.new(log)
52
- @log.start!
53
-
54
- # @source is the path to the given source file, whatever it may be
55
- @source = source
56
-
57
- # @audio is the path to the actual audio file we will process, always wav
58
- if File.extname(source) == ".wav"
59
- @audio = File.open(source, "rb")
60
- else
61
- # This happens in initialize so you can generate multiple waveforms from
62
- # the same audio without decoding multiple times
63
- #
64
- # Note that we're leaving it up to the ruby/system GC to clean up these
65
- # tempfiles because someone may be generating multiple waveform images
66
- # from a single audio source so we can't explicitly unlink the tempfile.
67
- @audio = to_wav(source)
68
- end
69
-
70
- raise RuntimeError.new("Unable to decode source \'#{@source}\' to WAV. Do you have ffmpeg installed with an appropriate decoder for your source file?") unless @audio
71
- end
72
-
73
- # Generate a Waveform image at the given filename with the given options.
74
- #
75
- # Available options are:
76
- #
77
- # :method => The method used to read sample frames, available methods
78
- # are peak and rms. peak is probably what you're used to seeing, it uses
79
- # the maximum amplitude per sample to generate the waveform, so the
80
- # waveform looks more dynamic. RMS gives a more fluid waveform and
81
- # probably more accurately reflects what you hear, but isn't as
82
- # pronounced (typically).
83
- #
84
- # Can be :rms or :peak
85
- # Default is :peak.
86
- #
87
- # :width => The width (in pixels) of the final waveform image.
88
- # Default is 1800.
89
- #
90
- # :height => The height (in pixels) of the final waveform image.
91
- # Default is 280.
92
- #
93
- # :background_color => Hex code of the background color of the generated
94
- # waveform image.
95
- # Default is #666666 (gray).
96
- #
97
- # :color => Hex code of the color to draw the waveform, or can pass
98
- # :transparent to render the waveform transparent (use w/ a solid
99
- # color background to achieve a "cutout" effect).
100
- # Default is #00ccff (cyan-ish).
101
- #
102
- # :force => Force generation of waveform, overwriting WAV or PNG file.
103
- #
104
- # Example:
105
- # waveform = Waveform.new("mp3s/Kickstart My Heart.mp3")
106
- #
107
- # waveform.generate("waves/Kickstart My Heart.png")
108
- # waveform.generate("waves/Kickstart My Heart.png", :method => :rms)
109
- # waveform.generate("waves/Kickstart My Heart.png", :color => "#ff00ff")
110
- #
111
- def generate(filename, options={})
112
- raise ArgumentError.new("No destination filename given for waveform") unless filename
31
+ class << self
32
+ # Generate a Waveform image at the given filename with the given options.
33
+ #
34
+ # Available options (all optional) are:
35
+ #
36
+ # :method => The method used to read sample frames, available methods
37
+ # are peak and rms. peak is probably what you're used to seeing, it uses
38
+ # the maximum amplitude per sample to generate the waveform, so the
39
+ # waveform looks more dynamic. RMS gives a more fluid waveform and
40
+ # probably more accurately reflects what you hear, but isn't as
41
+ # pronounced (typically).
42
+ #
43
+ # Can be :rms or :peak
44
+ # Default is :peak.
45
+ #
46
+ # :width => The width (in pixels) of the final waveform image.
47
+ # Default is 1800.
48
+ #
49
+ # :height => The height (in pixels) of the final waveform image.
50
+ # Default is 280.
51
+ #
52
+ # :background_color => Hex code of the background color of the generated
53
+ # waveform image.
54
+ # Default is #666666 (gray).
55
+ #
56
+ # :color => Hex code of the color to draw the waveform, or can pass
57
+ # :transparent to render the waveform transparent (use w/ a solid
58
+ # color background to achieve a "cutout" effect).
59
+ # Default is #00ccff (cyan-ish).
60
+ #
61
+ # :force => Force generation of waveform, overwriting WAV or PNG file.
62
+ #
63
+ # :logger => IOStream to log progress to.
64
+ #
65
+ # Example:
66
+ # Waveform.generate("Kickstart My Heart.wav", "Kickstart My Heart.png")
67
+ # Waveform.generate("Kickstart My Heart.wav", "Kickstart My Heart.png", :method => :rms)
68
+ # Waveform.generate("Kickstart My Heart.wav", "Kickstart My Heart.png", :color => "#ff00ff", :logger => $stdout)
69
+ #
70
+ def generate(source, filename, options={})
71
+ options = DefaultOptions.merge(options)
72
+
73
+ raise ArgumentError.new("No source audio filename given, must be an existing sound file.") unless source
74
+ raise ArgumentError.new("No destination filename given for waveform") unless filename
75
+ raise RuntimeError.new("Source audio file '#{source}' not found.") unless File.exist?(source)
76
+ raise RuntimeError.new("Destination file #{filename} exists. Use --force if you want to automatically remove it.") if File.exists?(filename) && !options[:force] === true
113
77
 
114
- if File.exists?(filename)
115
- if options[:force]
116
- @log.out("Output file #{filename} encountered. Removing.")
117
- File.unlink(filename)
118
- else
119
- raise RuntimeError.new("Destination file #{filename} exists. Use --force if you want to automatically remove it.")
78
+ @log = Log.new(options[:logger])
79
+ @log.start!
80
+
81
+ # Frames gives the amplitudes for each channel, for our waveform we're
82
+ # saying the "visual" amplitude is the average of the amplitude across all
83
+ # the channels. This might be a little weird w/ the "peak" method if the
84
+ # frames are very wide (i.e. the image width is very small) -- I *think*
85
+ # the larger the frames are, the more "peaky" the waveform should get,
86
+ # perhaps to the point of inaccurately reflecting the actual sound.
87
+ samples = frames(source, options[:width], options[:method]).collect do |frame|
88
+ frame.inject(0.0) { |sum, peak| sum + peak } / frame.size
120
89
  end
121
- end
90
+
91
+ @log.timed("\nDrawing...") do
92
+ # Don't remove the file even if force is true until we're sure the
93
+ # source was readable
94
+ if File.exists?(filename) && options[:force] === true
95
+ @log.out("Output file #{filename} encountered. Removing.")
96
+ File.unlink(filename)
97
+ end
98
+
99
+ image = draw samples, options
100
+ image.save filename
101
+ end
102
+
103
+ @log.done!("Generated waveform '#{filename}'")
104
+ end
105
+
106
+ private
107
+
108
+ # Returns a sampling of frames from the given RubyAudio::Sound using the
109
+ # given method the sample size is determined by the given pixel width --
110
+ # we want one sample frame per horizontal pixel.
111
+ def frames(source, width, method = :peak)
112
+ raise ArgumentError.new("Unknown sampling method #{method}") unless [ :peak, :rms ].include?(method)
113
+
114
+ frames = []
122
115
 
123
- options = DefaultOptions.merge(options)
116
+ RubyAudio::Sound.open(source) do |audio|
117
+ frames_read = 0
118
+ frames_per_sample = (audio.info.frames.to_f / width.to_f).to_i
119
+ sample = RubyAudio::Buffer.new("float", frames_per_sample, audio.info.channels)
124
120
 
125
- # Frames gives the amplitudes for each channel, for our waveform we're
126
- # saying the "visual" amplitude is the average of the amplitude across all
127
- # the channels. This might be a little weird w/ the "peak" method if the
128
- # frames are very wide (i.e. the image width is very small) -- I *think*
129
- # the larger the frames are, the more "peaky" the waveform should get,
130
- # perhaps to the point of inaccurately reflecting the actual sound.
131
- samples = frames(options[:width], options[:method]).collect do |frame|
132
- frame.inject(0.0) { |sum, peak| sum + peak } / frame.size
121
+ @log.timed("Sampling #{frames_per_sample} frames per sample: ") do
122
+ while(frames_read = audio.read(sample)) > 0
123
+ frames << send(method, sample, audio.info.channels)
124
+ @log.out(".")
125
+ end
126
+ end
127
+ end
128
+
129
+ frames
130
+ rescue RubyAudio::Error => e
131
+ raise e unless e.message == "File contains data in an unknown format."
132
+ raise Waveform::RuntimeError.new("Source audio file #{source} could not be read by RubyAudio library -- try converting to WAV first (RubyAudio: #{e.message})")
133
133
  end
134
134
 
135
- @log.timed("\nDrawing...") do
136
- background_color = options[:background_color] == :transparent ? ChunkyPNG::Color::TRANSPARENT : options[:background_color]
135
+ # Draws the given samples using the given options, returns a ChunkyPNG::Image.
136
+ def draw(samples, options)
137
+ image = ChunkyPNG::Image.new(options[:width], options[:height],
138
+ options[:background_color] == :transparent ? ChunkyPNG::Color::TRANSPARENT : options[:background_color]
139
+ )
137
140
 
138
141
  if options[:color] == :transparent
139
142
  color = transparent = ChunkyPNG::Color.from_hex(
@@ -146,11 +149,10 @@ class Waveform
146
149
  color = ChunkyPNG::Color.from_hex(options[:color])
147
150
  end
148
151
 
149
- image = ChunkyPNG::Image.new(options[:width], options[:height], background_color)
150
152
  # Calling "zero" the middle of the waveform, like there's positive and
151
153
  # negative amplitude
152
154
  zero = options[:height] / 2.0
153
-
155
+
154
156
  samples.each_with_index do |sample, x|
155
157
  # Half the amplitude goes above zero, half below
156
158
  amplitude = sample * options[:height].to_f / 2.0
@@ -169,96 +171,56 @@ class Waveform
169
171
  end
170
172
  end
171
173
 
172
- image.save(filename)
174
+ image
173
175
  end
174
-
175
- @log.done!("Generated waveform '#{filename}'")
176
- end
177
176
 
178
- # Returns a sampling of frames from the given wave file using the given method
179
- # the sample size is determined by the given pixel width -- we want one sample
180
- # frame per horizontal pixel.
181
- def frames(width, method = :peak)
182
- raise ArgumentError.new("Unknown sampling method #{method}") unless [ :peak, :rms ].include?(method)
183
-
184
- frames = []
185
-
186
- RubyAudio::Sound.open(@audio.path) do |snd|
187
- frames_read = 0
188
- frames_per_sample = (snd.info.frames.to_f / width.to_f).to_i
189
- sample = RubyAudio::Buffer.new("float", frames_per_sample, snd.info.channels)
190
-
191
- @log.timed("Sampling #{frames_per_sample} frames per sample: ") do
192
- while(frames_read = snd.read(sample)) > 0
193
- frames << send(method, sample, snd.info.channels)
194
- @log.out(".")
195
- end
177
+ # Returns an array of the peak of each channel for the given collection of
178
+ # frames -- the peak is individual to the channel, and the returned collection
179
+ # of peaks are not (necessarily) from the same frame(s).
180
+ def peak(frames, channels=1)
181
+ peak_frame = []
182
+ (0..channels-1).each do |channel|
183
+ peak_frame << channel_peak(frames, channel)
196
184
  end
185
+ peak_frame
197
186
  end
198
-
199
- frames
200
- end
201
-
202
- private
203
-
204
- # Decode given src file to a wav Tempfile. Returns the Tempfile if the decode
205
- # succeeded, or false if the decode failed.
206
- def to_wav(src, force=false)
207
- wav = nil
208
-
209
- @log.timed("Decoding source audio '#{src}' to WAV...") do
210
- wav = Tempfile.new(File.basename(src))
211
- system %Q{ffmpeg -y -i "#{src}" -f wav "#{wav.path}" > /dev/null 2>&1}
212
- end
213
-
214
- return wav.size == 0 ? false : wav
215
- end
216
-
217
- # Returns an array of the peak of each channel for the given collection of
218
- # frames -- the peak is individual to the channel, and the returned collection
219
- # of peaks are not (necessarily) from the same frame(s).
220
- def peak(frames, channels=1)
221
- peak_frame = []
222
- (0..channels-1).each do |channel|
223
- peak_frame << channel_peak(frames, channel)
224
- end
225
- peak_frame
226
- end
227
187
 
228
- # Returns an array of rms values for the given frameset where each rms value is
229
- # the rms value for that channel.
230
- def rms(frames, channels=1)
231
- rms_frame = []
232
- (0..channels-1).each do |channel|
233
- rms_frame << channel_rms(frames, channel)
188
+ # Returns an array of rms values for the given frameset where each rms value is
189
+ # the rms value for that channel.
190
+ def rms(frames, channels=1)
191
+ rms_frame = []
192
+ (0..channels-1).each do |channel|
193
+ rms_frame << channel_rms(frames, channel)
194
+ end
195
+ rms_frame
234
196
  end
235
- rms_frame
236
- end
237
197
 
238
- # Returns the peak voltage reached on the given channel in the given collection
239
- # of frames.
240
- #
241
- # TODO: Could lose some resolution and only sample every other frame, would
242
- # likely still generate the same waveform as the waveform is so comparitively
243
- # low resolution to the original input (in most cases), and would increase
244
- # the analyzation speed (maybe).
245
- def channel_peak(frames, channel=0)
246
- peak = 0.0
247
- frames.each do |frame|
248
- next if frame.nil?
249
- peak = frame[channel].abs if frame[channel].abs > peak
198
+ # Returns the peak voltage reached on the given channel in the given collection
199
+ # of frames.
200
+ #
201
+ # TODO: Could lose some resolution and only sample every other frame, would
202
+ # likely still generate the same waveform as the waveform is so comparitively
203
+ # low resolution to the original input (in most cases), and would increase
204
+ # the analyzation speed (maybe).
205
+ def channel_peak(frames, channel=0)
206
+ peak = 0.0
207
+ frames.each do |frame|
208
+ next if frame.nil?
209
+ frame = Array(frame)
210
+ peak = frame[channel].abs if frame[channel].abs > peak
211
+ end
212
+ peak
250
213
  end
251
- peak
252
- end
253
214
 
254
- # Returns the rms value across the given collection of frames for the given
255
- # channel.
256
- #
257
- # FIXME: this RMS calculation might be wrong...
258
- # refactored this from: http://pscode.org/javadoc/src-html/org/pscode/ui/audiotrace/AudioPlotPanel.html#line.996
259
- def channel_rms(frames, channel=0)
260
- avg = frames.inject(0.0){ |sum, frame| sum += frame ? frame[channel] : 0 }/frames.size.to_f
261
- Math.sqrt(frames.inject(0.0){ |sum, frame| sum += frame ? (frame[channel]-avg)**2 : 0 }/frames.size.to_f)
215
+ # Returns the rms value across the given collection of frames for the given
216
+ # channel.
217
+ #
218
+ # FIXME: this RMS calculation might be wrong...
219
+ # refactored this from: http://pscode.org/javadoc/src-html/org/pscode/ui/audiotrace/AudioPlotPanel.html#line.996
220
+ def channel_rms(frames, channel=0)
221
+ avg = frames.inject(0.0){ |sum, frame| sum += frame ? Array(frame)[channel] : 0 }/frames.size.to_f
222
+ Math.sqrt(frames.inject(0.0){ |sum, frame| sum += frame ? (Array(frame)[channel]-avg)**2 : 0 }/frames.size.to_f)
223
+ end
262
224
  end
263
225
  end
264
226
 
@@ -270,11 +232,11 @@ class Waveform
270
232
  # easier than using Google.
271
233
  class Log
272
234
  attr_accessor :io
273
-
235
+
274
236
  def initialize(io=$stdout)
275
237
  @io = io
276
238
  end
277
-
239
+
278
240
  # Prints the given message to the log
279
241
  def out(msg)
280
242
  io.print(msg) if io
@@ -311,7 +273,7 @@ class Waveform
311
273
  def time?(index)
312
274
  Time.now - @benchmarks[index]
313
275
  end
314
-
276
+
315
277
  # Benchmarks the given block, printing out the given message first (if
316
278
  # given).
317
279
  def timed(message=nil, &block)
@@ -321,4 +283,4 @@ class Waveform
321
283
  done!
322
284
  end
323
285
  end
324
- end
286
+ end
@@ -0,0 +1,3 @@
1
+ class Waveform
2
+ VERSION = "0.1.0"
3
+ end
@@ -1,174 +1,168 @@
1
- require "contest"
2
- require "fileutils"
3
1
  require File.expand_path(File.join(File.dirname(__FILE__), "..", "lib", "waveform"))
4
2
 
5
- class WaveformTest < Test::Unit::TestCase
6
- def self.fixture(file)
3
+ require "test/unit"
4
+ require "ruby-debug"
5
+ require "fileutils"
6
+
7
+ module Helpers
8
+ def fixture(file)
7
9
  File.join(File.dirname(__FILE__), "fixtures", file)
8
10
  end
9
- def fixture(file);self.class.fixture(file);end;
10
11
 
11
- def self.output(file)
12
+ def output(file)
12
13
  File.join(File.dirname(__FILE__), "output", file)
13
14
  end
14
- def output(file);self.class.output(file);end;
15
15
 
16
16
  def open_png(file)
17
17
  ChunkyPNG::Image.from_datastream(ChunkyPNG::Datastream.from_file(file))
18
18
  end
19
+ end
20
+
21
+ class WaveformTest < Test::Unit::TestCase
22
+ include Helpers
23
+ extend Helpers
24
+
25
+ def self.cleanup
26
+ puts "Removing existing testing artifacts..."
27
+ Dir[output("*.*")].each{ |f| FileUtils.rm(f) }
28
+ FileUtils.mkdir_p(output(""))
29
+ end
19
30
 
20
- puts "Removing existing testing artifacts..."
21
- FileUtils.rm_rf(output("")) if File.exists?(output(""))
22
- FileUtils.mkdir(output(""))
23
- sample_wav = fixture("sample_mp3.wav")
24
- FileUtils.rm(sample_wav) if File.exists?(sample_wav)
25
-
26
- context "generating waveform" do
27
- setup do
28
- @waveform = Waveform.new(fixture("sample.wav"))
29
- end
30
-
31
- should "generate waveform from audio source" do
32
- @waveform.generate(output("waveform_from_audio_source.png"))
33
- assert File.exists?(output("waveform_from_audio_source.png"))
31
+ def test_generates_waveform
32
+ Waveform.generate(fixture("sample.wav"), output("waveform_from_audio_source.png"))
33
+ assert File.exists?(output("waveform_from_audio_source.png"))
34
34
 
35
- image = open_png(output("waveform_from_audio_source.png"))
36
- assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:color]), image[60, 120]
37
- assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:background_color]), image[0, 0]
38
- end
35
+ image = open_png(output("waveform_from_audio_source.png"))
36
+ assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:color]), image[60, 120]
37
+ assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:background_color]), image[0, 0]
38
+ end
39
39
 
40
- should "convert non-wav audio source before generation" do
41
- Waveform.new(fixture("sample_mp3.mp3")).generate(output("from_mp3.png"))
40
+ def test_generates_waveform_from_mono_audio_source_via_peak
41
+ Waveform.generate(fixture("mono_sample.wav"), output("waveform_from_mono_audio_source_via_peak.png"))
42
+ assert File.exists?(output("waveform_from_mono_audio_source_via_peak.png"))
42
43
 
43
- assert File.exists?(output("from_mp3.png"))
44
- end
44
+ image = open_png(output("waveform_from_mono_audio_source_via_peak.png"))
45
+ assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:color]), image[60, 120]
46
+ assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:background_color]), image[0, 0]
47
+ end
45
48
 
46
- should "log to given io" do
47
- File.open(output("waveform.log"), "w") do |io|
48
- Waveform.new(fixture("sample.wav"), io).generate(output("logged.png"))
49
- end
50
-
51
- assert_match /Generated waveform/, File.read(output("waveform.log"))
52
- end
53
-
54
- should "generate waveform using rms method instead of peak" do
55
- @waveform.generate(output("peak.png"))
56
- @waveform.generate(output("rms.png"), :method => :rms)
57
- rms = open_png(output("rms.png"))
58
- peak = open_png(output("peak.png"))
59
-
60
- assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:color]), peak[44, 43]
61
- assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:background_color]), rms[44, 43]
62
- assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:color]), rms[60, 120]
49
+ def test_generates_waveform_from_mono_audio_source_via_rms
50
+ Waveform.generate(fixture("mono_sample.wav"), output("waveform_from_mono_audio_source_via_rms.png"), :method => :rms)
51
+ assert File.exists?(output("waveform_from_mono_audio_source_via_rms.png"))
52
+
53
+ image = open_png(output("waveform_from_mono_audio_source_via_rms.png"))
54
+ assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:color]), image[60, 120]
55
+ assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:background_color]), image[0, 0]
56
+ end
57
+
58
+ def test_logs_to_given_io
59
+ File.open(output("waveform.log"), "w") do |io|
60
+ Waveform.generate(fixture("sample.wav"), output("logged.png"), :logger => io)
63
61
  end
64
62
 
65
- should "generate waveform 900px wide" do
66
- @waveform.generate(output("width-900.png"), :width => 900)
67
- image = open_png(output("width-900.png"))
68
-
69
- assert_equal 900, image.width
70
- end
63
+ assert_match /Generated waveform/, File.read(output("waveform.log"))
64
+ end
65
+
66
+ def test_uses_rms_instead_of_peak
67
+ Waveform.generate(fixture("sample.wav"), output("peak.png"))
68
+ Waveform.generate(fixture("sample.wav"), output("rms.png"), :method => :rms)
69
+
70
+ rms = open_png(output("rms.png"))
71
+ peak = open_png(output("peak.png"))
71
72
 
72
- should "generate waveform 100px tall" do
73
- @waveform.generate(output("height-100.png"), :height => 100)
74
- image = open_png(output("height-100.png"))
75
-
76
- assert_equal 100, image.height
77
- end
73
+ assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:color]), peak[44, 43]
74
+ assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:background_color]), rms[44, 43]
75
+ assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:color]), rms[60, 120]
76
+ end
77
+
78
+ def test_is_900px_wide
79
+ Waveform.generate(fixture("sample.wav"), output("width-900.png"), :width => 900)
78
80
 
79
- should "generate waveform on red background color" do
80
- @waveform.generate(output("background_color-#ff0000.png"), :background_color => "#ff0000")
81
- image = open_png(output("background_color-#ff0000.png"))
82
-
83
- assert_equal ChunkyPNG::Color.from_hex("#ff0000"), image[0, 0]
84
- end
81
+ image = open_png(output("width-900.png"))
85
82
 
86
- should "generate waveform on transparent background color" do
87
- @waveform.generate(output("background_color-transparent.png"), :background_color => :transparent)
88
- image = open_png(output("background_color-transparent.png"))
89
-
90
- assert_equal ChunkyPNG::Color::TRANSPARENT, image[0, 0]
91
- end
83
+ assert_equal 900, image.width
84
+ end
85
+
86
+ def test_is_100px_tall
87
+ Waveform.generate(fixture("sample.wav"), output("height-100.png"), :height => 100)
92
88
 
93
- should "generate waveform in black foreground color" do
94
- @waveform.generate(output("color-#000000.png"), :color => "#000000")
95
- image = open_png(output("color-#000000.png"))
96
-
97
- assert_equal ChunkyPNG::Color.from_hex("#000000"), image[60, 120]
98
- end
89
+ image = open_png(output("height-100.png"))
99
90
 
100
- should "generate waveform on red background color with transparent foreground cut-out" do
101
- @waveform.generate(output("background_color-#ff0000+color-transparent.png"), :background_color => "#ff0000", :color => :transparent)
102
- image = open_png(output("background_color-#ff0000+color-transparent.png"))
103
-
104
- assert_equal ChunkyPNG::Color.from_hex("#ff0000"), image[0, 0]
105
- assert_equal ChunkyPNG::Color::TRANSPARENT, image[60, 120]
106
- end
91
+ assert_equal 100, image.height
92
+ end
93
+
94
+ def test_has_red_background_color
95
+ Waveform.generate(fixture("sample.wav"), output("background_color-#ff0000.png"), :background_color => "#ff0000")
107
96
 
108
- # Bright green is our transparency mask color, so this test ensures that we
109
- # don't destroy the image if the background also uses the transparency mask
110
- # color
111
- should "generate waveform with transparent foreground on bright green background" do
112
- @waveform.generate(output("background_color-#00ff00+color-transparent.png"), :background_color => "#00ff00", :color => :transparent)
113
- image = open_png(output("background_color-#00ff00+color-transparent.png"))
114
-
115
- assert_equal ChunkyPNG::Color.from_hex("#00ff00"), image[0, 0]
116
- assert_equal ChunkyPNG::Color::TRANSPARENT, image[60, 120]
117
- end
118
-
119
- # Not sure how to best test this as it's totally dependent on the ruby and
120
- # system GC when the tempfiles are removed (as we're not explicitly
121
- # unlinking them).
122
- # should "use a tempfile when generating a temporary wav" do
123
- # tempfiles = Dir[File.join(Dir.tmpdir(), "sample_mp3*")].size
124
- # Waveform.new(fixture("sample_mp3.mp3")).generate(output("cleanup_temporary_wav.png"))
125
- # assert_equal tempfiles + 1, Dir[File.join(Dir.tmpdir(), "sample_mp3*")].size
126
- # end
127
-
128
- should "not delete source wav file if one was given" do
129
- assert File.exists?(fixture("sample.wav"))
130
- Waveform.new(fixture("sample.wav")).generate(output("keep_source_wav.png"))
131
- assert File.exists?(fixture("sample.wav"))
132
- end
97
+ image = open_png(output("background_color-#ff0000.png"))
133
98
 
134
- should "raise an error if unable to decode to wav" do
135
- assert_raise(Waveform::RuntimeError) do
136
- Waveform.new(fixture("sample.txt")).generate(output("shouldnt_exist.png"))
137
- end
138
- end
139
-
140
- context "with existing PNG files" do
141
- setup do
142
- @existing = output("existing.png")
143
- FileUtils.touch @existing
144
- end
145
-
146
- should "generate waveform if :force is true and PNG exists" do
147
- @waveform.generate(@existing, :force => true)
148
- end
149
-
150
- should "raise an exception if PNG exists and :force is false" do
151
- assert_raises Waveform::RuntimeError do
152
- @waveform.generate(@existing, :force => false)
153
- end
154
- end
99
+ assert_equal ChunkyPNG::Color.from_hex("#ff0000"), image[0, 0]
100
+ end
101
+
102
+ def test_has_transparent_background_color
103
+ Waveform.generate(fixture("sample.wav"), output("background_color-transparent.png"), :background_color => :transparent)
104
+
105
+ image = open_png(output("background_color-transparent.png"))
106
+
107
+ assert_equal ChunkyPNG::Color::TRANSPARENT, image[0, 0]
108
+ end
109
+
110
+ def test_has_black_foreground_color
111
+ Waveform.generate(fixture("sample.wav"), output("color-#000000.png"), :color => "#000000")
112
+
113
+ image = open_png(output("color-#000000.png"))
114
+
115
+ assert_equal ChunkyPNG::Color.from_hex("#000000"), image[60, 120]
116
+ end
117
+
118
+ def test_has_red_background_color_with_transparent_foreground_cutout
119
+ Waveform.generate(fixture("sample.wav"), output("background_color-#ff0000+color-transparent.png"), :background_color => "#ff0000", :color => :transparent)
120
+
121
+ image = open_png(output("background_color-#ff0000+color-transparent.png"))
122
+
123
+ assert_equal ChunkyPNG::Color.from_hex("#ff0000"), image[0, 0]
124
+ assert_equal ChunkyPNG::Color::TRANSPARENT, image[60, 120]
125
+ end
126
+
127
+ # Bright green is our transparency mask color, so this test ensures that we
128
+ # don't destroy the image if the background also uses the transparency mask
129
+ # color
130
+ def test_has_transparent_foreground_on_bright_green_background
131
+ Waveform.generate(fixture("sample.wav"), output("background_color-#00ff00+color-transparent.png"), :background_color => "#00ff00", :color => :transparent)
132
+
133
+ image = open_png(output("background_color-#00ff00+color-transparent.png"))
134
+
135
+ assert_equal ChunkyPNG::Color.from_hex("#00ff00"), image[0, 0]
136
+ assert_equal ChunkyPNG::Color::TRANSPARENT, image[60, 120]
137
+ end
138
+
139
+ def test_raises_error_if_not_given_readable_audio_source
140
+ assert_raise(Waveform::RuntimeError) do
141
+ Waveform.generate(fixture("sample.txt"), output("shouldnt_exist.png"))
155
142
  end
143
+ end
144
+
145
+ def test_overwrites_existing_waveform_if_force_is_true_and_file_exists
146
+ FileUtils.touch output("overwritten.png")
156
147
 
157
- context "with existing WAV files" do
158
- setup do
159
- @existing = output("existing.wav")
160
- FileUtils.touch @existing
161
- end
162
-
163
- should "generate waveform if :force is true and WAV exists" do
164
- @waveform.generate(@existing, :force => true)
165
- end
148
+ Waveform.generate(fixture("sample.wav"), output("overwritten.png"), :force => true)
149
+ end
166
150
 
167
- should "raise an exception if WAV exists and :force is false" do
168
- assert_raises Waveform::RuntimeError do
169
- @waveform.generate(@existing, :force => false)
170
- end
171
- end
151
+ def test_raises_exception_if_waveform_exists_and_force_is_false
152
+ FileUtils.touch output("wont_be_overwritten.png")
153
+
154
+ assert_raises Waveform::RuntimeError do
155
+ Waveform.generate(fixture("sample.wav"), output("wont_be_overwritten.png"), :force => false)
172
156
  end
173
157
  end
158
+
159
+ def test_raises_exception_if_waveform_exists
160
+ FileUtils.touch output("wont_be_overwritten_by_default.png")
161
+
162
+ assert_raises Waveform::RuntimeError do
163
+ Waveform.generate(fixture("sample.wav"), output("wont_be_overwritten_by_default.png"))
164
+ end
165
+ end
174
166
  end
167
+
168
+ WaveformTest.cleanup
@@ -1,10 +1,10 @@
1
- require "./lib/waveform"
1
+ require "./lib/waveform/version"
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = "waveform"
5
5
  s.version = Waveform::VERSION
6
- s.summary = "Generate waveform images from WAV, MP3, etc... files"
7
- s.description = "Generate waveform images from WAV, MP3, etc... files - as a gem or via CLI."
6
+ s.summary = "Generate waveform images from audio files"
7
+ s.description = "Generate waveform images 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
8
  s.authors = ["Ben Alavi"]
9
9
  s.email = ["benalavi@gmail.com"]
10
10
  s.homepage = "http://github.com/benalavi/waveform"
@@ -23,6 +23,4 @@ Gem::Specification.new do |s|
23
23
 
24
24
  s.add_dependency "ruby-audio"
25
25
  s.add_dependency "chunky_png"
26
-
27
- s.add_development_dependency "contest"
28
26
  end
metadata CHANGED
@@ -1,8 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: waveform
3
3
  version: !ruby/object:Gem::Version
4
+ hash: 27
4
5
  prerelease:
5
- version: 0.0.3
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
6
11
  platform: ruby
7
12
  authors:
8
13
  - Ben Alavi
@@ -10,7 +15,7 @@ autorequire:
10
15
  bindir: bin
11
16
  cert_chain: []
12
17
 
13
- date: 2011-07-28 00:00:00 Z
18
+ date: 2012-09-27 00:00:00 Z
14
19
  dependencies:
15
20
  - !ruby/object:Gem::Dependency
16
21
  name: ruby-audio
@@ -20,6 +25,9 @@ dependencies:
20
25
  requirements:
21
26
  - - ">="
22
27
  - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
23
31
  version: "0"
24
32
  type: :runtime
25
33
  version_requirements: *id001
@@ -31,21 +39,13 @@ dependencies:
31
39
  requirements:
32
40
  - - ">="
33
41
  - !ruby/object:Gem::Version
42
+ hash: 3
43
+ segments:
44
+ - 0
34
45
  version: "0"
35
46
  type: :runtime
36
47
  version_requirements: *id002
37
- - !ruby/object:Gem::Dependency
38
- name: contest
39
- prerelease: false
40
- requirement: &id003 !ruby/object:Gem::Requirement
41
- none: false
42
- requirements:
43
- - - ">="
44
- - !ruby/object:Gem::Version
45
- version: "0"
46
- type: :development
47
- version_requirements: *id003
48
- description: Generate waveform images from WAV, MP3, etc... files - as a gem or via CLI.
48
+ description: Generate waveform images 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.
49
49
  email:
50
50
  - benalavi@gmail.com
51
51
  executables:
@@ -57,6 +57,7 @@ extra_rdoc_files: []
57
57
  files:
58
58
  - README.md
59
59
  - Rakefile
60
+ - lib/waveform/version.rb
60
61
  - lib/waveform.rb
61
62
  - waveform.gemspec
62
63
  - test/waveform_test.rb
@@ -74,19 +75,25 @@ required_ruby_version: !ruby/object:Gem::Requirement
74
75
  requirements:
75
76
  - - ">="
76
77
  - !ruby/object:Gem::Version
78
+ hash: 3
79
+ segments:
80
+ - 0
77
81
  version: "0"
78
82
  required_rubygems_version: !ruby/object:Gem::Requirement
79
83
  none: false
80
84
  requirements:
81
85
  - - ">="
82
86
  - !ruby/object:Gem::Version
87
+ hash: 3
88
+ segments:
89
+ - 0
83
90
  version: "0"
84
91
  requirements: []
85
92
 
86
93
  rubyforge_project:
87
- rubygems_version: 1.8.6
94
+ rubygems_version: 1.8.5
88
95
  signing_key:
89
96
  specification_version: 3
90
- summary: Generate waveform images from WAV, MP3, etc... files
97
+ summary: Generate waveform images from audio files
91
98
  test_files: []
92
99