waveform 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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