Using SOX via PHP to cleanup podcasts

Posted: 2010/10/01 in Linux, PHP

Problem: Podcasts are usually too slow to me.  Ironically, they are too often in stereo (how many microphones does it take to record my one mouth?!?) . Even worse, the audio from one podcast to the next can vary drastically in volume as well as dynamic (volume) range!

Solution #1: sox — a great command line ‘agent of’ the libsox library that is used by numerous GUI and CLI audio (and even some video) packages.  I came across the following set of parameters that does the following: converts to 1 channel (mono that is heard in both ears), speeds up the tempo (not pitch) by 1.5x, brings the low sections of the recording up, the loud sections down, and then converts the whole file to all-but clipping loud!  This sample even converts from .wav to .mp3 as it does it’s magic!  Here it is:

sox IncomingFile.wav -c 1 OutgoingFile.mp3 -S tempo 1.5 compand 0.3,1 6:-70,-60,-20 -5 -90 norm

[[ not sure where I got the companding parameters, but they work pretty well for me! ]]

Solution #2: PHP and exec().  I love the command line, but asking others to follow me is, well, a bit of a lost cause!  But when I explain what I’m doing, they really want what sox can do!  So I created these scripts (passwords, tracking, etc. has been removed – some security is left in place) to allow others to have their audio files companded, sped up, mono-ized, and MP3’d.

So here’s a quick screenshot of the page in a browser so you can see what’s coming in, followed by the main script is as follows – logging and password sections replaced with comments.

<?php

/*
 * This code is free software; you can use it, redistribute it, and/or modify as you wish.
 * This code is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY - implied or otherwise.
 * This code is distributed "as is."  All risk and cost are assumed by the user of the code and not the creator thereof.
 * If you want to give attribution to the original creator of the original code, his name is David Malouf and he is, probably, available at EmailTheDavid@gmail.com
 */

/////////////////////////////////////////////////////////////
// Upload an MP3 or WAV file and have it 'fixed'...
//   + output as WAV or MP#
//   + compound & normalize
//   + mono-ize
//   + Speed up 15, 20, 25, 30, 35, 40, 45, or 50%
/////////////////////////////////////////////////////////////
// things that vary
$maxFileSize = 45000000;
// $password = ;

session_start();

//// SECURITY: password required here ////
// this loads the actual upload page if the 'Click here to download' link was clicked
if ($_GET['file'] > '1000') {
  include ('sendTheFile.php');
}

//// SECURITY: log uses per IP and only allow x songs/day ////
// if file is uploaded
if ($_FILES['theFile']['name']) {

  //// SECURITY: log activity ////
  //
  // Look for errors - if yes, log them and tell user there's a problem
  if ($_FILES['error'] != 0) {
    $ErrorText = "The file did not upload correctly.  Try again if you wish.<br/>\n";
    $ErrorText .= "If you get here more than once, then the site must be freaking out.<br/>\n";
    $ErrorText .= "  Call David and he'll fix it.\n";
    //// SECURITY: log error ////
    include ('theUploadForm.php');
    die();
  }

  if ($_FILES['size'] > $maxFileSize) {
    $ErrorText = "That file is HUGE!  Too much for my little server!<br/>\n";
    $ErrorText .= "  Sorry about that, but I can't do a file that big!\n";
    //// SECURITY: log error ////
    include ('theUploadForm.php');
    die();
  }

  //  get file info
  $fileName = $_FILES['theFile']['name'];
  $tmpName = $_FILES['theFile']['tmp_name'];
  $soxType = $_POST['type'];
  $soxCompand = $_POST['compand'];
  $soxChannels = $_POST['channels'];
  $soxSpeed = $_POST['speed'];
  $scriptFolder = dirname(__FILE__);

  // Sanitize variables
  /// FileName - taken from other script that allows for any/no extension
  $originalExtension = strrchr($fileName, '.'); // from last . to end of filename
  if ($originalExtension) {
    $originalName = substr($fileName, 0, -(strlen($originalExtension))); // get all BEFORE extension
  } else {
    $originalName = $fileName; // no extension present
  }

  // turn spaces into underscores
  $noSpaceName = str_replace(' ', '_', trim($originalName));
  $noSpaceExt = str_replace(' ', '_', trim($originalExtension));

  // Alpha-numeric + underscores only
  $pattern = '/[^a-zA-Z0-9_]/';  // alpha, numeric, underscore
  $safeName = preg_replace($pattern, '', $noSpaceName);
  $safeExt = preg_replace($pattern, '', $noSpaceExt); // this also pulls off '.'
  $fileName = $safeName . '.' . $safeExt;

  /// check that extension is allowable (make extension all lowercase (easier to check/verify/manipulate))
  $safeExtensions = array('wav', 'mp3');
  $lowerCaseExt = strtolower($safeExt);

  if (!in_array($lowerCaseExt, $safeExtensions)) {
    $ErrorText = "Yeah, that's not a WAV nor MP3 file (or at least the extension isn't WAV or MP3)<br/>\n";
    $ErrorText .= "&nbsp; &nbsp; Either only use a WAV or MP3 or rename the file to .mp3 or .wav";
    $ErrorText .= " (whichever is relevant) and try again.\n";
    //// SECURITY: log error ////
    include ('theUploadForm.php');
    die();
  }

  if (!$soxType == 'wav') {
    $soxType = 'mp3';         // default
  }

  /// Channels
  $soxChannels = ($soxChannels == '2') ? '2' : '1';   // if 2, then 2.  If !=2, then 1 (mono)

  /// Speed - if $_POST[speed] is valid, use it.  Otherwise speed = 0
  $validSpeeds = array(0, 15, 20, 25, 30, 35, 35, 40, 45, 50);
  if (!in_array($soxSpeed, $validSpeeds))
    $soxSpeed = 0;

  // Make upload folders if they don't exist
  //  Folder structure:
  //                    .
  //                    ./soxed_files/
  //                    ./soxed_files/fixed/
  //                    ./soxed_files/original/

  $theFolders = array('/soxed_files/', '/soxed_files/original/', '/soxed_files/fixed/');
  foreach ($theFolders as $value) {
    $checkMe = $scriptFolder . $value;
    if (!is_dir($checkMe)) {
      if (!mkdir($checkMe)) {
        // cannot make upload folders, can't use this system
        echo "Hmmmm.... something went really wrong.<br/>\n";
        echo "The system is just plain down.  Please contact David so he can fix it, thanks!\n";
        die();
      }
    }
  }

  //  move webserver-createdd temp-file for SOX manipulation
  $uploadfile = $scriptFolder . '/soxed_files/original/' . $fileName;

  // Set extension per user's request
  $newfile = $scriptFolder . '/soxed_files/fixed/' . $safeName . '.' . $soxType;

  if (move_uploaded_file($tmpName, $uploadfile) === FALSE) {
    echo "Well... something went really, REALLY wrong (not your fault).<br/>\n";
    echo "  Please let David know the site is messed up.  Thanks.\n";
    die();
  }

  //  create SOX command
  $soxCommand = "sox $uploadfile -c $soxChannels $newfile tempo 1.$soxSpeed";
  if ($soxCompand == 'yes') {
    $soxCommand .= ' compand 0.3,1 6:-70,-60,-20 -5 -90 norm';
  }

  //  run SOX command
  exec($soxCommand, $output, $result);

  //  Deal with result
  if ($result != 0) {
    //// SECURITY: log failed activity ////
    // send error to screen
    $ErrorText = "Crud.  Didn't work.  You can try again, but if it doesn't work the second time,";
    $ErrorText .= " just let David know so he can fix it.\n";
    include ('theUploadForm.php');
    die();
  }

  // Store $fileName in the $_SESS under a random key
  $randNumber = rand('11111', '99999');
  $_SESSION['UploadedFile'] = 'Yes';
  $_SESSION["'$randNumber'"] = $fileName; // quotes & ticks used to make keys strings and not index #s w/out using an alpha-salt
  //  create link for upload
  $ErrorText = '<a href="index.php?file=' . $randNumber . '">Click here</a> to download your file.';
  include ('theUploadForm.php');
  die();
}

// form for uploading
include ('theUploadForm.php');
?>

This file above and the support files it calls are available for download: SoxTheFile.php : : :  sendTheFile.php : : :  theUploadForm.php

Security Details: Even though this is for friends and family, I don’t trust anyone!  So the filenames are converted to alpha-numeric+underscore (no spaces).  Right now it’s only MP3 and WAV (it’s a project, not a money maker!) per lines 81-92.  Lines 94-104 are all a very simple way to prevent malicious/injected activity. If someone pushes malicious $_POST data, it is ignored (not in_array() – my favorite white-listing function!) and a default is used. Lines 63-79 sanitize the filename before sox even works on it – and gives a cleaned-up filename for returning.  I use trim(), str_replace() to get rid of extra spaces and convert the rest to underscores.  Lastly, I use a random number to ‘point’ to the modified audio file.  This way, the actual file name is obscured/obfuscated.  The random number is stored in $_SESSION – the number is the key, the actual file name is the value.

What I don’t like:
1) It’s hard to post OOP code on a website like this without loosing the details of the code (back and forth between method calls and the method itself)
2) It can take a bit for sox to finish!  The user might be sitting there for a bit, wondering if this thing is even going!  I’m thinking a JavaScript spinner or something (any ideas?)
3) sox doesn’t always get compiled with MP3 support. rcjhawk has posted a nice article on getting sox to play-nice with MP3s here: http://hawknotes.blogspot.com/2010/05/fixing-sox-again.html

Advertisements
Comments
  1. Hello, I was just wondering, can it process an input file (say mp3,wav,flac) into flac with sampling rate 16khz?
    VF

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s