Class: Waveform
- Inherits:
-
Object
- Object
- Waveform
- Defined in:
- lib/waveform.rb,
lib/waveform.rb
Defined Under Namespace
Classes: ArgumentError, Log, RuntimeError
Constant Summary collapse
- VERSION =
"0.0.3"
- DefaultOptions =
{ :method => :peak, :width => 1800, :height => 280, :background_color => "#666666", :color => "#00ccff", :force => false }
- TransparencyMask =
"#00ff00"
- TransparencyAlternate =
in case the mask is the background color!
"#ffff00"
Instance Attribute Summary collapse
-
#source ⇒ Object
readonly
Returns the value of attribute source.
Instance Method Summary collapse
-
#frames(width, method = :peak) ⇒ Object
Returns a sampling of frames from the given wave file using the given method the sample size is determined by the given pixel width – we want one sample frame per horizontal pixel.
-
#generate(filename, options = {}) ⇒ Object
Generate a Waveform image at the given filename with the given options.
-
#initialize(source, log = nil) ⇒ Waveform
constructor
Setup a new Waveform for the given audio file.
Constructor Details
#initialize(source, log = nil) ⇒ Waveform
Setup a new Waveform for the given audio file. If given anything besides a WAV file it will attempt to first convert the file to a WAV using ffmpeg.
Optionally takes an IO stream to which it will print log/benchmarking info.
See #generate for how to generate the waveform image from the given audio file.
Available conversions depend on your installation of ffmpeg.
Example:
Waveform.new("mp3s/Kickstart My Heart.mp3")
Waveform.new("mp3s/Kickstart My Heart.mp3", $stdout)
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 |
# File 'lib/waveform.rb', line 47 def initialize(source, log=nil) raise ArgumentError.new("No source audio filename given, must be an existing sound file.") unless source raise RuntimeError.new("Source audio file '#{source}' not found.") unless File.exist?(source) @log = Log.new(log) @log.start! # @source is the path to the given source file, whatever it may be @source = source # @audio is the path to the actual audio file we will process, always wav if File.extname(source) == ".wav" @audio = File.open(source, "rb") else # This happens in initialize so you can generate multiple waveforms from # the same audio without decoding multiple times # # Note that we're leaving it up to the ruby/system GC to clean up these # tempfiles because someone may be generating multiple waveform images # from a single audio source so we can't explicitly unlink the tempfile. @audio = to_wav(source) end 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 end |
Instance Attribute Details
#source ⇒ Object (readonly)
Returns the value of attribute source.
25 26 27 |
# File 'lib/waveform.rb', line 25 def source @source end |
Instance Method Details
#frames(width, method = :peak) ⇒ Object
Returns a sampling of frames from the given wave file using the given method the sample size is determined by the given pixel width – we want one sample frame per horizontal pixel.
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 |
# File 'lib/waveform.rb', line 181 def frames(width, method = :peak) raise ArgumentError.new("Unknown sampling method #{method}") unless [ :peak, :rms ].include?(method) frames = [] RubyAudio::Sound.open(@audio.path) do |snd| frames_read = 0 frames_per_sample = (snd.info.frames.to_f / width.to_f).to_i sample = RubyAudio::Buffer.new("float", frames_per_sample, snd.info.channels) @log.timed("Sampling #{frames_per_sample} frames per sample: ") do while(frames_read = snd.read(sample)) > 0 frames << send(method, sample, snd.info.channels) @log.out(".") end end end frames end |
#generate(filename, options = {}) ⇒ Object
Generate a Waveform image at the given filename with the given options.
Available options are:
:method => The method used to read sample frames, available methods
are peak and rms. peak is probably what you're used to seeing, it uses
the maximum amplitude per sample to generate the waveform, so the
waveform looks more dynamic. RMS gives a more fluid waveform and
probably more accurately reflects what you hear, but isn't as
pronounced (typically).
Can be :rms or :peak
Default is :peak.
:width => The width (in pixels) of the final waveform image.
Default is 1800.
:height => The height (in pixels) of the final waveform image.
Default is 280.
:background_color => Hex code of the background color of the generated
waveform image.
Default is #666666 (gray).
:color => Hex code of the color to draw the waveform, or can pass
:transparent to render the waveform transparent (use w/ a solid
color background to achieve a "cutout" effect).
Default is #00ccff (cyan-ish).
:force => Force generation of waveform, overwriting WAV or PNG file.
Example:
waveform = Waveform.new("mp3s/Kickstart My Heart.mp3")
waveform.generate("waves/Kickstart My Heart.png")
waveform.generate("waves/Kickstart My Heart.png", :method => :rms)
waveform.generate("waves/Kickstart My Heart.png", :color => "#ff00ff")
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/waveform.rb', line 111 def generate(filename, ={}) raise ArgumentError.new("No destination filename given for waveform") unless filename if File.exists?(filename) if [:force] @log.out("Output file #{filename} encountered. Removing.") File.unlink(filename) else raise RuntimeError.new("Destination file #{filename} exists. Use --force if you want to automatically remove it.") end end = DefaultOptions.merge() # Frames gives the amplitudes for each channel, for our waveform we're # saying the "visual" amplitude is the average of the amplitude across all # the channels. This might be a little weird w/ the "peak" method if the # frames are very wide (i.e. the image width is very small) -- I *think* # the larger the frames are, the more "peaky" the waveform should get, # perhaps to the point of inaccurately reflecting the actual sound. samples = frames([:width], [:method]).collect do |frame| frame.inject(0.0) { |sum, peak| sum + peak } / frame.size end @log.timed("\nDrawing...") do background_color = [:background_color] == :transparent ? ChunkyPNG::Color::TRANSPARENT : [:background_color] if [:color] == :transparent color = transparent = ChunkyPNG::Color.from_hex( # Have to do this little bit because it's possible the color we were # intending to use a transparency mask *is* the background color, and # then we'd end up wiping out the whole image. [:background_color].downcase == TransparencyMask ? TransparencyAlternate : TransparencyMask ) else color = ChunkyPNG::Color.from_hex([:color]) end image = ChunkyPNG::Image.new([:width], [:height], background_color) # Calling "zero" the middle of the waveform, like there's positive and # negative amplitude zero = [:height] / 2.0 samples.each_with_index do |sample, x| # Half the amplitude goes above zero, half below amplitude = sample * [:height].to_f / 2.0 # If you give ChunkyPNG floats for pixel positions all sorts of things # go haywire. image.line(x, (zero - amplitude).round, x, (zero + amplitude).round, color) end # Simple transparency masking, it just loops over every pixel and makes # ones which match the transparency mask color completely clear. if transparent (0..image.width - 1).each do |x| (0..image.height - 1).each do |y| image[x, y] = ChunkyPNG::Color.rgba(0, 0, 0, 0) if image[x, y] == transparent end end end image.save(filename) end @log.done!("Generated waveform '#{filename}'") end |