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

draw(samples, options) click to toggle source
#

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
generate( source, filename = :guess_from_given_source, options = {} ) click to toggle source
#

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

channel_peak(frames, channel = 0) click to toggle source
#

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
channel_rms(frames, channel = 0) click to toggle source
#

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
frames(source, width, method = :peak) click to toggle source
#

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
peak(frames, channels = 1) click to toggle source
#

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
rms(frames, channels = 1) click to toggle source
#

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

Public Instance Methods

source()
Alias for: source?
source?() click to toggle source
#

source?

#
# File lib/multimedia_paradise/audio/waveform/class.rb, line 230
def source?
  @source
end
Also aliased as: source