Skip to content

Instantly share code, notes, and snippets.

@davidcornu
Created August 25, 2011 17:23
Show Gist options
  • Save davidcornu/1171205 to your computer and use it in GitHub Desktop.
Save davidcornu/1171205 to your computer and use it in GitHub Desktop.
Convert MP3 Files to Waveform Images in Ruby
# --------------------------------------------------------------------
# Create SoundCloud-style waveforms from mp3 files
# Author: David Cornu
# Credit for the idea goes to http://cl.ly/292t2B09022I3S3X3N2O
#
# Requirements:
# mpg123 - [ % brew install mpg123 ]
# imagemagick - [ % brew install imagemagick ]
# rmagick - [ % brew install rmagick ]
#
# RUBY VERSION:
# Runs on 1.9.2 (remove File.size(filename) from #49 for 1.8.x)
# --------------------------------------------------------------------
class SoundParser
# Convenience function for command line usage
def run(filename)
imageFile = filename.gsub(".","_") + ".png"
rawFile = self.decode(filename)
graphData = self.parse(rawFile)
self.draw(graphData,imageFile)
cleanup(rawFile)
return 'You have a handsome waveform.'
end
# Use mpg123 to decode the file into raw PCM data
# Flags used:
# - Output raw data to a file: "-O"
# - Convert to mono: "-m"
# - Downsample at 1:4 ratio: "-4"
# See manpage for details
def decode(filename)
rawFilename = filename.gsub(".","_") + ".raw"
`mpg123 -4 -m -O #{rawFilename} #{filename}`
puts "Decode --------------------- OK"
return rawFilename
end
# Generate array of amplitude readings
def parse(filename)
plotData = []
# Read file as binary and populate initial array of readings
File.open(filename, File.size(filename)) do |file|
while !file.eof?
point = file.read(2).unpack('s')
plotData << point[0]
end
end
puts "Processing ----------------- OK"
# Work out the amount of readings that constiture a sample
# Each sample corresponds to one pixel of width
sampleSize = (plotData.length.to_f/2000).to_int
result = []
sampleBin = []
# Seperate readings into samples
# Work out averages of all negative and positive numbers
plotData.each do |point|
sampleBin << point
if sampleBin.length == sampleSize
negatives = sampleBin.select{|p| p > 0}
positives = sampleBin.select{|p| p < 0 }
negAvg = (negatives.inject(0){|sum,item| sum + item}.to_f/negatives.length).to_i
posAvg = (positives.inject(0){|sum,item| sum + item}.to_f/positives.length).to_i
result << [negAvg,posAvg]
sampleBin = []
end
end
puts "Averaging ------------------ OK"
# Work out maximum and minimum values
minSampleValue = result.flatten.min
maxSampleValue = result.flatten.max
# Work out how much to add to make everything positive
# Must maintain center by using the biggest number
if minSampleValue.abs > maxSampleValue.abs
shiftValue = minSampleValue.abs
else
shiftValue = maxSampleValue.abs
end
# Use the highest positive or negative point as maximum range
# Also used to maintain the waveform's centered position
sampleRange = shiftValue*2
rangedResult = []
# Reduce and round everything down to a value between 0 and 100
result.each do |coords|
rangedCoords = []
coords.each do |point|
point += shiftValue
rangedCoords << ((point.to_f/sampleRange)*100).to_int
end
rangedResult << rangedCoords
end
puts "Rounding ------------------- OK"
return rangedResult
end
def draw(coords, targetFile)
require 'rubygems'
require 'RMagick'
# Creates a blank canvas to draw on
canvas = Magick::ImageList.new
canvas.new_image(2000, 200){ self.background_color = "#E5E5E5" }
# Used to work out line positions
xPosition = 0
# Loop through coordinates and plot them on the canvas
# Points must be modified to use the full height
# See http://www.imagemagick.org/RMagick/doc/draw.html for details
coords.each do |points|
bottom = ((points[0].to_f/100)*200).to_int
top = ((points[1].to_f/100)*200).to_int
draw = Magick::Draw.new
draw.stroke('#6E6E6E')
draw.fill_opacity(0)
draw.stroke_opacity(1)
draw.stroke_width(1)
draw.line(xPosition,top, xPosition,bottom)
draw.draw(canvas)
xPosition += 1
end
puts "Plotting ------------------- OK"
# Write resulting image to a file
canvas.write(targetFile)
puts "Writing File --------------- OK"
end
# Convenience function for cleanup raw PCM data file
def cleanup(filename)
`rm #{filename}`
puts "Cleanup -------------------- OK"
end
end
# Allows for basic command line usage
# % ruby SoundParser.rb filename.mp3
if ARGV[0]
sp = SoundParser.new
sp.run(ARGV[0])
end
@johnloringpollard
Copy link

Have you noticed that this has a rounding error somewhere? I used this to generate a long waveform image and it cuts off sound waves.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment