setcooki
ALLROUND WEB DEVELOPER

02
Oct 11

Process audio with PHP and Sox

  
  
  

You want to mix/merge/edit audio file only with php on-the-fly? export your mix to mp3 … thinks its not possible? Well it is!

This will only work on Linux as you need the sOx Soundexchange Library http://sox.sourceforge.net/ and Lame http://lame.sourceforge.net/. Make sure you have the right versions for your environment installed as the wrong setup will not be stable. For installation – see installation guides on the project websites. Once you have everything up and running you should specify your needs and create a runtime routine for rendering audio on-the-fly.

If you want to mix/merg single audio snippets into a single exportable .mp3 file you should first consider that every step will a) consume ram/cpu time b) needs validation since a failure will effect the whole rendering process. The only performant way of processing multiple render task is to work with a background daemon as running process to manage the cue of rendering task.

You may want to use the following PEAR class http://kevin.vanzonneveld.net/techblog/article/create_daemons_in_php/ which has turned out to be quite stable handling a constant load in production environment. The following approach worked well on a web application handling thousands of jobs a day.

  1. Prepare and organize your source audio files as .wav in 16-bit 44 Khz (Make sure each file is normalized and starts exactly at 0 sample length)
  2. Create a PHP class structure for representation of the audio files, its properties, eventually breaking down songs, into tracks, segments and parts, represented each by a class so a song track contain segments and a segment can contain parts
  3. if you want to mix sound channels you should make that the base class so each channel can be bounced later as well
  4. Create a job class which can recieve the necessary commands for all audio task, which can be serialized and stored in a database for the deamon to pickup a new job once available
  5. Organize all audio task in classes/or plugins that execute the commands
  6. The deamon itsself should trigger the rendering process and execute all commands in a loop until the last step exporting the final audio file to mp3
The way sOx works is in always defining an in and out file for each command. Knowing that you must save every outfile in a temporary folder so the next command can take the outfile as infile. The mix of 2 files will only work if the 2 audio files have the exact same sample length! What if you want to mix one audio part of length x with a part that has more samples? You need to know the exact starting point on where to merge the first part to the second.

You can basically calculate the sample length of a wave file by extracting its header (e.g. http://www.phpclasses.org/browse/file/1582.html) or you know the BPM, signature and bars of a audio part (they must be exact). If you want to make a step sequencer with loops you should know these parameters and the can be stored in a database together with file part, name and so on. Calculate the sample length like:

protected function getSampleLength($bpm = null, $bars = null, $clock = 4, $rate = 44100)
    {
        $timelength = null;
        $samplelength = null;
        if($bpm !== null && $bars !== null)
        {
            $timelength     = (float)((int)$bars * (60 * (int)$clock)) / (int)$bpm;
            $samplelength   = ceil(($timelength * (int)$rate) / 1);
            return $samplelength;
        }
    }
Once you have the sample length you can calculate on each bar the sample start point. You can pass these parameters to the sOx command and you are ready to go. The rest is all functional logic to process each step in rendering step by step, bouncing the outfiles and reusing them in the next rendering step and so on. The more generic you design your application – the more flexible you can handle each audio effect. The best approach is really simple to create a job class which will handle all commands. It could looks something like this:
    System_Daemon::start();

    try
    {
        $db = DBFactory::create(DBFactory::DRIVER_MYSQLI, PSOX::credentials());
        $query = QueryFactory::create(QueryFactory::DRIVER_MYSQL, &$db);

        $ok = 0;

        while(true)
        {
            try
            {
                $ok = 0;

                if(!$db->isConnected())
                {
                    $db->reconnect();
                }
                if(($res = $query->getNextJob()) !== 0)
                {
                    $query->write($res['job_id'], null, PSOX::FLAG_MIXING);
                    $sox = PSOX::deserialize($res['job_data']);
                    PSOX::log(PSOX::LOG_DEBUG, "job is unserialized for mixing");  

                    sleep(1);
                    if($sox->hasJob(PSOX::JOB_INIT))
                    {
                        if($sox->runJob(PSOX::JOB_INIT) === true) $ok++;
                    }
                    sleep(1);
                    if($sox->hasJob(PSOX::JOB_MIX))
                    {
                        if($sox->runJob(PSOX::JOB_MIX) === true) $ok++;
                    }
                    sleep(1);
                    if($sox->hasJob(PSOX::JOB_MERGE))
                    {
                        if($sox->runJob(PSOX::JOB_MERGE) === true) $ok++;
                    }
                    sleep(1);
                    if($sox->hasJob(PSOX::JOB_MP3))
                    {
                        if($sox->runJob(PSOX::JOB_MP3) === true) $ok++;
                    }
                    sleep(1);
                    if($sox->hasJob(PSOX::JOB_COPY))
                    {
                        if($sox->runJob(PSOX::JOB_COPY) === true) $ok++;
                    }
                    sleep(1);
                    if($sox->hasJob(PSOX::JOB_CLEAR))
                    {
                        if($err = $sox->runJob(PSOX::JOB_CLEAR) === true) $ok++;
                    }
                    sleep(1);
                    if($sox->hasJob(PSOX::JOB_MAIL))
                    {
                        if($sox->runJob(PSOX::JOB_MAIL) === true) $ok++;
                    }
                    if($ok === $sox->countJobs())
                    {
                        $query->write($res['job_id'], null, PSOX::FLAG_DONE, null, $sox->runtime($sox->getOption(PSOX::OPTION_RUNTIME_START), microtime()));
                        $sox->publish($res['job_cid']);
                        PSOX::log(PSOX::LOG_DEBUG, "job is done");
                    }else{
                        throw new PSOXException("unable to process job: ".$res['job_id'], PSOX::DAEMON_PROCEED);
                    }
                }
            }
            catch(PSOXException $e)
            {
                if((int)$e->getCode() === PSOX::DAEMON_ABORT)
                {
                    PSOX::log(PSOX::LOG_ERROR, $e->getMessage());
                    echo $e->getMessage();
                    break;
                    System_Daemon::stop();
                }else{
                    $query->bounceJob($res['job_id']);
                    PSOX::log(PSOX::LOG_ERROR, $e->getMessage());
                    PSOX::log(PSOX::LOG_DEBUG, "job is bounced");
                }
            }
            sleep(10);
        }
    }
    catch(PSOXException $e)
    {
        PSOX::log(PSOX::LOG_ERROR, $e->getMessage());
        echo $e->getMessage();
        System_Daemon::stop();
    }
How do you communicate with sOx and lame though? A simple PHPs exec(); will not do since error/pipe/output handling is too limited. Here is the method i used to have maximum control using PHPs proc extension:
class PROC extends PSOX
{
    /**
    * @desc method that does a proc_open command call with the command set
    * in first parameter $cmd. if the second parameter $output is set to false
    * the command will produce no output thus only returning true if command has
    * terminated
    * @param    string $cmd expects the command to call
    * @param    boolean $output expects whether to look for output
    * @param    array $env optionals env options
    * @param    string $cwd optional working dir
    * @access   public
    * @returns  mixed when $output = true returns string, else either empty string or true
    */
    public static function exec($cmd = null, $output = true, $env = null, $cwd = null)
    {
        $process = null;
        $pipes = array();
        $out = '';
        $status = null;

        if($cmd !== null)
        {
            $process = proc_open(   escapeshellcmd($cmd),
                                    array(
                                        0 => array('pipe', 'r'),
                                        1 => array('pipe', 'w'),
                                        2 => array('pipe', 'w')
                                    ),
                                    $pipes,
                                    $cwd,
                                    $env
                                );

            if(is_resource($process))
            {
                usleep(1000);
                stream_set_write_buffer($pipes[1], 0);
                stream_set_write_buffer($pipes[2], 0);
                stream_set_timeout($pipes[1], 600);
                stream_set_timeout($pipes[2], 600);
                stream_set_blocking($pipes[0], false);
                stream_set_blocking($pipes[1], false);
                stream_set_blocking($pipes[2], false);

                if((bool)$output)
                {
                    while(!feof($pipes[2]))
                    {
                        $status = proc_get_status($process);
                        $out .= htmlspecialchars(fgets($pipes[2]), ENT_COMPAT, 'UTF-8');
                    }
                }else{
                    while(true)
                    {
                        $status = proc_get_status($process);
                        if((bool)$status['running'])
                        {
                            $out = true;
                            break;
                        }
                    }
                }

                fclose($pipes[0]);
                fclose($pipes[1]);
                fclose($pipes[2]);

                @proc_close($process);
                @exec('kill '.$status['pid'].' 2>/dev/null >&- >/dev/null');
                @clearstatcache();

                return $out;

            }else{
                 throw new PSOXException("invalid ressource - unable to open process for: $cmd");
            }
        }
    }

}
Keeping all this in mind you can create a whole audio suite just with php. Imagine a step sequencer with a flash frontend which has a mp3 export function having you send an e-mail with your composition. Yes, … it works!

Copyright © 2012 setcooki
Proudly powered by WordPress, Free WordPress Themes