# -------------------------------------------------------------------- # 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