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 +33 -38
- data/bin/waveform +5 -5
- data/lib/waveform.rb +153 -191
- data/lib/waveform/version.rb +3 -0
- data/test/waveform_test.rb +137 -143
- data/waveform.gemspec +3 -5
- metadata +23 -16
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
|
-
|
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
|
-
|
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
|
-
|
13
|
+
Then:
|
18
14
|
|
19
|
-
$ sudo gem install
|
20
|
-
|
21
|
-
to make things a bit faster, and:
|
15
|
+
$ sudo gem install waveform
|
22
16
|
|
23
|
-
|
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
|
-
|
19
|
+
$ sudo gem install oily_png
|
28
20
|
|
29
21
|
CLI Usage
|
30
22
|
=========
|
31
23
|
|
32
|
-
$ waveform song.
|
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.
|
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
|
-
|
66
|
+
Converting MP3 to WAV
|
67
|
+
=====================
|
81
68
|
|
82
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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-
|
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
|
data/bin/waveform
CHANGED
@@ -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[:
|
38
|
+
options[:logger] = $stdout
|
38
39
|
o.on("-q", "--quiet", "Don't print anything out when generating waveform") do
|
39
|
-
options[:
|
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.
|
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
|
-
|
data/lib/waveform.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
-
require "
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
#
|
55
|
-
|
56
|
-
|
57
|
-
#
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
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
|
-
|
136
|
-
|
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
|
174
|
+
image
|
173
175
|
end
|
174
|
-
|
175
|
-
@log.done!("Generated waveform '#{filename}'")
|
176
|
-
end
|
177
176
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
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
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
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
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
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
|
data/test/waveform_test.rb
CHANGED
@@ -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
|
-
|
6
|
-
|
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
|
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
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
41
|
-
|
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
|
-
|
44
|
-
|
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
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
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
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
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
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
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
|
-
|
158
|
-
|
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
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
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
|
data/waveform.gemspec
CHANGED
@@ -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
|
7
|
-
s.description = "Generate waveform images from
|
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
|
-
|
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:
|
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
|
-
-
|
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.
|
94
|
+
rubygems_version: 1.8.5
|
88
95
|
signing_key:
|
89
96
|
specification_version: 3
|
90
|
-
summary: Generate waveform images from
|
97
|
+
summary: Generate waveform images from audio files
|
91
98
|
test_files: []
|
92
99
|
|