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