Simon Simon - 4 months ago 11
PHP Question

Synthesise variable length multi tonal audio with SoX while avoiding clipping

I'll preface this by saying I am a total novice when it comes to audio processing and synthesis, if I'm making some stupid assumptions or misunderstanding core concepts please correct me.

I am experimenting with SoX to convert arrays of numeric data to a single audio file. So far I have two "working" methods that produce some pretty horrible results and both have critical limitations.

I am using SoX via PHP on a 64bit WIN 8.1 box.

Method 1 Output individual tones then concatenate

$toneLinks=array();
for($i=0;$i<count($sourceData);$i++){
$filename='tones\\'.$dataTitle.'_'.$i.'.au';
$soxCommand=$soxFolder.'sox -n '.$filename.' synth .5 sin '.($sourceData[$i]).' vol 0.5 ';
shell_exec($soxCommand);
$toneLinks[]=$filename;
}
$chunks=array_chunk($toneLinks,100);
$chunkFiles=array();
for($ch=0;$ch<count($chunks);$ch++){
$name='tones\\'.$dataTitle.'_chunk_'.$ch.'.au';
$soxCommand=$soxFolder.'sox ';
for($i=0;$i<count($chunks[$ch]);$i++){
$soxCommand.=' '.$chunks[$ch][$i];
}
$soxCommand.=' '.$name;
$result=shell_exec($soxCommand);
$chunkFiles[]=$name;
}
$soxCommand=$soxFolder.'sox ';
for($i=0;$i<count($chunkFiles);$i++){
$soxCommand.=' '.$chunkFiles[$i];
}
$soxCommand.=' '.$dataTitle.'.au';
shell_exec($soxCommand);


Limitations:


  • Slow, requires many individual executions

  • Mixing appears to be limited, ie trying to join 100 or 200 files will produce a file containing some but not all of the tones. Trying to join 1000 files will fail with no output. One could presumably concatenate a few files then concatenate those concatenated files but this will exacerbate the first limitation. When creating a final mix of multiple <= 100 tone intermediary files it appears the mix is processed before the components have finished rendering, producing an empty final mix.

  • Abandoned "mixing" and was able to successfully concatenate any number of tones using the updated method 1, as this method is showing the most promise I will continue experimenting and update as progress is made regarding the final limitation.

  • Though not critical, there is no "flow" to the final output and it sounds like what it is, lots of separate tones stuck together.



Method 2 Generate a "chord" in a single command

$soxCommand=$soxFolder.'sox -n '.$dataTitle.'.au synth ';
for($i=1;$i<count($sourceData);$i++){
$soxCommand.='.25 sin '.($sourceData[$i]).' ';
}
$soxCommand.='delay ';
for($i=1;$i<count($sourceData);$i++){
$soxCommand.=($i*.2).' ';
}
$soxCommand.='remix - fade 0 '.(count($sourceData)*.2+.5).' .1 norm -1';
shell_exec($soxCommand);


Limitations:


  • Trying to create "chords" with more than 300 tones one encounters a
    similar issue as with the last method, however concatenating smaller
    files with this method sounds odd as there are mixed tones in the
    components with audible breaks at the join. One could overlap the files but that's still not ideal.

  • While the overlap of notes with this method produces "flowing" audio
    it also introduces clipping presumably due to the layering of two
    tones with a volume of 1. I have been unable to work out how to specify volume as per Method 1



The ideal answer will address the following:


  • Synthesising multiple tones and combining them into a single cohesive
    piece of "music"

  • Work with source data-sets of indeterminate length

  • Avoid clipping in the final output


Answer

After a bit more experimentation I have settled on a method that fulfils my requirements. I'm sure one could make use of pipes to make the process more efficient however this method has been producing the required results on data sets of the approximate size required (~2,000) in under a minute.

//Generate individual tones
$tones=array();
for($i=0;$i<count($sourceData)-1;$i++){
    $name='tones\\'.$dataTitle.'_'.$i.'.au';
    $soxCommand=$soxFolder.'sox -n '.$name.' synth 0.2 sin '.($sourceData[$i]).' fade q 0.05 0 ';
    shell_exec($soxCommand);
    $tones[]=$name;
}
//Break into manageable chunks to avoid exec character limit
$chunks=array_chunk($tones,100);
$chunkFiles=array();
for($ch=0;$ch<count($chunks);$ch++){
    $name='tones\\'.$dataTitle.'_chunk_'.$ch.'.au';
    $soxCommand=$soxFolder.'sox ';
    for($i=0;$i<count($chunks[$ch]);$i++){
        $soxCommand.=' '.$chunks[$ch][$i]; 
    }
    $soxCommand.=' '.$name.' splice 0.2';
    shell_exec($soxCommand);
    $chunkFiles[]=$name;
}
//Render chunks into final track
$soxCommand=$soxFolder.'sox ';
for($i=0;$i<count($chunkFiles);$i++){
    $soxCommand.=' '.$chunkFiles[$i]; 
}
$soxCommand.=' '.$dataTitle.'.au splice 20';
shell_exec($soxCommand);
//Clean component files
for($i=0;$i<count($tones);$i++){
    unlink($tones[$i]); 
}
for($i=0;$i<count($chunkFiles);$i++){
    unlink($chunkFiles[$i]); 
}

Disambiguation of SoX commands

Generate tones: "sox -n [outfile] synth 0.2 [frequency] fade q 0.05 0"

This command generates a 0.2 second tone with a quarter sine fade-in of 0.05 seconds and a quarter sine fade-out 0.05 seconds before the natural end of the track.

Combine tones/chunks: "sox [tone1] [tone2] [tone...] [outfile] splice 0.2"

The secret sauce in this is the splice which will automatically attempt to remove the clicks caused by dumb concatenation. The final command simply replaces the tone infiles with the chunk infiles and increases the splice point to 20sec from 0.2sec.