02
Oct 11Process 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.
- 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)
- 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
- if you want to mix sound channels you should make that the base class so each channel can be bounced later as well
- 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
- Organize all audio task in classes/or plugins that execute the commands
- 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
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;
}
}
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();
}
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");
}
}
}
}