class MultimediaParadise::Waveform
Constants
- DEFAULT_OPTIONS
#¶ ↑
MultimediaParadise::Waveform::DEFAULT_OPTIONS
¶ ↑Specify some default options to use for class
Waveform
.#¶ ↑
- NAMESPACE
#¶ ↑
NAMESPACE
¶ ↑#¶ ↑
- TRANSPARENCY_ALTERNATE
#¶ ↑
MultimediaParadise::Waveform::TRANSPARENCY_ALTERNATE
¶ ↑In case the mask is the background color!
#¶ ↑
- TRANSPARENCY_MASK
#¶ ↑
MultimediaParadise::Waveform::TRANSPARENCY_MASK
¶ ↑#¶ ↑
Public Class Methods
#¶ ↑
Wavelength.draw¶ ↑
Draws the given samples using the given options.
Will return a ChunkyPNG::Image.
#¶ ↑
# File lib/multimedia_paradise/audio/waveform/class.rb, line 173 def self.draw(samples, options) image = ChunkyPNG::Image.new(options[:width], options[:height], options[:background_color] == :transparent ? ChunkyPNG::Color::TRANSPARENT : options[:background_color] ) case options[:color] # ======================================================================= # # === :transparent # ======================================================================= # when :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 would end up wiping out the whole image. # =================================================================== # options[:background_color].downcase == TRANSPARENCY_MASK ? TRANSPARENCY_ALTERNATE : TRANSPARENCY_MASK ) else # Delegate towards class ChunkyPNG next. color = ChunkyPNG::Color.from_hex(options[:color]) end # ======================================================================= # # Calling "zero" the middle of the waveform, like there's positive # and negative amplitude # ======================================================================= # zero = options[:height] / 2.0 samples.each_with_index { |sample, x| # ===================================================================== # # Half the amplitude goes above zero, half below # ===================================================================== # amplitude = sample * options[: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) } # ======================================================================= # # 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 { |x| (0..image.height - 1).each { |y| image[x, y] = ChunkyPNG::Color.rgba(0, 0, 0, 0) if image[x, y] == transparent } } end image end
#¶ ↑
MultimediaParadise::Waveform.generate
(generate tag)¶ ↑
This method will generate a waveform image.
The first argument will be the input-filename of the .wav file in question.
The second argument shall be the output name of the newly generated filename.
The third argument shall be the options.
Available options, all of which are optional, are these here:
: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. :logger => IOStream to log progress to.
Usage examples:
x = MultimediaParadise::Waveform.generate('foo.wav') x = MultimediaParadise::Waveform.generate('foo.wav', 'BOTY.png') x = MultimediaParadise::Waveform.generate('Kickstart My Heart.wav', "Kickstart My Heart.png") x = MultimediaParadise::Waveform.generate('Kickstart My Heart.wav', "Kickstart My Heart.png", :method => :rms) x = MultimediaParadise::Waveform.generate('Kickstart My Heart.wav', "Kickstart My Heart.png", :color => "#ff00ff", :logger => $stdout)
#¶ ↑
# File lib/multimedia_paradise/audio/waveform/class.rb, line 103 def self.generate( source, filename = :guess_from_given_source, options = {} ) options = DEFAULT_OPTIONS.merge(options) case filename when :guess_from_given_source, :default # Replace. filename = source.gsub(/\.wav$/,'')+'.png' end raise ArgumentError.new( 'No source audio filename given, must be an existing sound file.' ) unless source raise ArgumentError.new( 'No destination filename given for waveform.' ) unless filename raise RuntimeError.new( "Source audio file '#{source}' not found." ) unless File.exist?(source) raise RuntimeError.new( "Destination file #{filename} exists. Use --force if you want to automatically remove it." ) if File.exist?(filename) && !options[:force] === true if source.end_with? '.mp3' # User did input a .mp3 file. # ===================================================================== # # === Tap into the toplevel-method MultimediaParadise.mp3_to_wav next # ===================================================================== # source = MultimediaParadise.mp3_to_wav(source) end # ======================================================================= # # === Start the logger classer # ======================================================================= # @log = Log.new(options[:logger]) @log.start! # ======================================================================= # # 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(source, options[:width], options[:method]).map { |frame| frame.inject(0.0) { |sum, peak| sum + peak } / frame.size } @log.timed("\nDrawing...") { # ===================================================================== # # Don't remove the file even if force is true until we're sure # the source was readable. # ===================================================================== # if File.exist?(filename) && options[:force] === true @log.out("Output file #{filename} encountered. Removing.") File.delete(filename) end image = draw(samples, options) image.save(filename) } @log.done!("Generated waveform '#{filename}'") end
Private Class Methods
#¶ ↑
Wavelength.channel_peak¶ ↑
Returns the peak voltage reached on the given channel in the given collection of frames.
TODO:
Could lose some resolution and only sample every other frame, would likely still generate the same waveform as the waveform is so comparitively low resolution to the original input (in most cases), and would increase the analyzation speed (maybe).
#¶ ↑
# File lib/multimedia_paradise/audio/waveform/class.rb, line 314 def channel_peak(frames, channel = 0) peak = 0.0 frames.each { |frame| next if frame.nil? frame = Array(frame) peak = frame[channel].abs if frame[channel].abs > peak } peak end
#¶ ↑
Wavelength.channel_rms¶ ↑
Returns the rms value across the given collection of frames for the given channel.
#¶ ↑
# File lib/multimedia_paradise/audio/waveform/class.rb, line 330 def channel_rms(frames, channel = 0) Math.sqrt(frames.inject(0.0){ |sum, frame| sum += (frame ? Array(frame)[channel] ** 2 : 0) } / frames.size ) end
#¶ ↑
Wavelength.frames¶ ↑
Returns a sampling of frames from the given RubyAudio::Sound using the given method the sample size is determined by the given pixel width – we want one sample frame per horizontal pixel.
#¶ ↑
# File lib/multimedia_paradise/audio/waveform/class.rb, line 245 def frames(source, width, method = :peak) unless %i( peak rms ).include?(method) raise ArgumentError.new("Unknown sampling method #{method}") end frames = [] RubyAudio::Sound.open(source) { |audio| frames_per_sample = (audio.info.frames.to_f / width.to_f).to_i sample = RubyAudio::Buffer.new("float", frames_per_sample, audio.info.channels) @log.timed("Sampling #{frames_per_sample} frames per sample: ") { while(audio.read(sample)) > 0 frames << send(method, sample, audio.info.channels) @log.out('.') end } } frames # Return the found frames here. rescue RubyAudio::Error => e raise e unless e.message == 'File contains data in an unknown format.' raise Waveform::RuntimeError.new( "Source audio file #{source} could not be read by RubyAudio "\ "library -- Hint: non-WAV files are no longer supported, convert "\ "to WAV first using something like ffmpeg (RubyAudio: #{e.message})" ) end
#¶ ↑
Wavelength.peak¶ ↑
Returns an array of the peak of each channel for the given collection of frames – the peak is individual to the channel, and the returned collection of peaks are not (necessarily) from the same frame(s).
#¶ ↑
# File lib/multimedia_paradise/audio/waveform/class.rb, line 279 def peak(frames, channels = 1) peak_frame = [] (0..channels-1).each { |channel| peak_frame << channel_peak(frames, channel) } peak_frame end
#¶ ↑
Wavelength.rms¶ ↑
Returns an array of rms values for the given frameset where each rms value is the rms value for that channel.
#¶ ↑
# File lib/multimedia_paradise/audio/waveform/class.rb, line 293 def rms(frames, channels = 1) rms_frame = [] (0..channels-1).each { |channel| rms_frame << channel_rms(frames, channel) } rms_frame end