?php
/*
*
* MusyX sample extraction tool by Nisto
* Last revision: May 16, 2014
*
* Minimum required PHP version: 4.1.0
*
*/
if($argc!=3)
{
echo 'Usage: php <script> <sound_dir> <format> ' . PHP_EOL;
echo PHP_EOL;
echo 'script ' . PHP_EOL;
echo ' ' . $argv[0] . PHP_EOL;
echo PHP_EOL;
echo 'sound_dir ' . PHP_EOL;
echo ' path to the sound directory ' . PHP_EOL;
echo PHP_EOL;
echo 'format ' . PHP_EOL;
echo ' 0 - standard MusyX .sdir format ' . PHP_EOL;
echo ' 1 - .sdir within a .arc file (e.g. Resident Evil 0) ' . PHP_EOL;
echo ' 2 - .snd format (e.g. Resident Evil) ' . PHP_EOL;
echo PHP_EOL;
echo PHP_EOL;
echo PHP_EOL;
echo 'The script has only been tested with the following GameCube games:' . PHP_EOL;
echo ' * Biohazard (Resident Evil) ' . PHP_EOL;
echo ' * Biohazard 0 (Resident Evil 0) ' . PHP_EOL;
echo ' * Paper Mario: The Thousand-Year Door ' . PHP_EOL;
echo ' * Star Fox Adventures ' . PHP_EOL;
echo PHP_EOL;
echo 'It may or may not work with other games utilizing MusyX. ' . PHP_EOL;
exit();
}
if(!is_dir($argv[1]))
{
exit('The directory path specified does not appear to be valid.');
}
if($argv[2]!=='0' && $argv[2]!=='1' && $argv[2]!=='2')
{
exit('Invalid format specified.');
}
define('SOUND_DIR', rtrim(realpath($argv[1]), DIRECTORY_SEPARATOR));
define('OUT_DIR', SOUND_DIR . DIRECTORY_SEPARATOR . 'samples');
define('FORMAT', $argv[2]);
define('NULL_64BIT', pack('x8'));
define('HEADER_PAD', pack('x36'));
if(($Dir = opendir(SOUND_DIR)) === FALSE)
{
exit('ERROR: Could not open input directory! Terminating script.');
}
if(!is_dir(OUT_DIR) && mkdir(OUT_DIR) === FALSE)
{
exit('ERROR: Could not create output directory! Terminating script.');
}
$sdirExts = array('sdir','sdi','arc','snd');
$Done = array();
while(($sdirName = readdir($Dir)) !== FALSE)
{
/*
* skip irrelevant and already-done files
*/
$sdirPath = SOUND_DIR . DIRECTORY_SEPARATOR . $sdirName;
if (!is_file($sdirPath))
{
continue;
}
$Ext = strtolower(pathinfo($sdirName, PATHINFO_EXTENSION));
if (!in_array($Ext, $sdirExts))
{
continue;
}
$Basename = strtolower(basename($sdirName, '.'.$Ext));
if (in_array($Basename, $Done))
{
continue;
}
/*
* parse files according to the format specified
* if the extension does not match with the selected format,
* skip the file
*
* because there may be errors before finishing parsing,
* we need to add to $Done[] individually
*/
if (($sdirFile = fopen($sdirPath, 'rb')) === FALSE)
{
err_skip('Could not open sdir %s for reading.', $sdirName);
continue;
}
if(FORMAT=='0' && ($Ext=='sdir'||$Ext=='sdi'))
{
// standard sdir
$Done[] = $Basename;
$sdirOffset = 0;
$sdirSize = filesize($sdirPath);
$sampPath = SOUND_DIR . DIRECTORY_SEPARATOR . $Basename . '.sam';
if(!file_exists($sampPath) && !file_exists($sampPath .= 'p'))
{
err_skip($sdirFile, 'Could not find the sample file for %s.', $sdirName);
continue;
}
$sampName = basename($sampPath);
$sampOffset = 0;
$sampSize = filesize($sampPath);
}
elseif(FORMAT=='1' && $Ext=='arc')
{
// standard sdir in arc
$Done[] = $Basename;
fseek($sdirFile, 0x04);
$arcFileCount = fread($sdirFile, 4);
$arcIndexOffset = fread($sdirFile, 4);
if(strlen($arcFileCount)!=4 || strlen($arcIndexOffset)!=4)
{
err_skip($sdirFile, 'Failed reading arc header values in %s.', $sdirName);
continue;
}
$arcSize = filesize($sdirPath);
$arcFileCount = unpack('N', $arcFileCount)[1];
$arcIndexOffset = unpack('N', $arcIndexOffset)[1];
if ($arcFileCount < 1 || $arcIndexOffset+($arcFileCount*32) > $arcSize)
{
err_skip($sdirFile, '%s does not appear to be a valid .arc file.', $sdirName);
continue;
}
fseek($sdirFile, $arcIndexOffset);
for($i=1; $i<=$arcFileCount; ++$i)
{
$sdirOffset = fread($sdirFile, 4);
$sdirSize = fread($sdirFile, 4);
fseek($sdirFile, 8, SEEK_CUR);
$InnerExt = fread($sdirFile, 4);
fseek($sdirFile, 12, SEEK_CUR);
if (strlen($sdirOffset)!=4 || strlen($sdirSize)!=4 || strlen($InnerExt)!=4)
{
err_skip($sdirFile, 'Failed reading data from arc file %s.', $sdirName);
continue 2;
}
if ($InnerExt=='sdir')
{
break;
}
}
if($InnerExt!='sdir')
{
err_skip($sdirFile, 'Could not locate sdir file within %s.', $sdirName);
continue;
}
$sdirOffset = unpack('N',$sdirOffset)[1];
$sdirSize = unpack('N',$sdirSize)[1];
if($sdirOffset+$sdirSize > $arcSize)
{
err_skip($sdirFile, 'The sdir table in %s is out of range based on header values.', $sdirName);
continue;
}
$sampPath = SOUND_DIR . DIRECTORY_SEPARATOR . $Basename . '.sam';
if(!file_exists($sampPath) && !file_exists($sampPath .= 'p'))
{
err_skip($sdirFile, 'Could not find the sample file for %s.', $sdirName);
continue;
}
$sampName = basename($sampPath);
$sampOffset = 0;
$sampSize = filesize($sampPath);
}
elseif(FORMAT=='2' && $Ext=='snd')
{
// .snd
$Done[] = $Basename;
$sampFile = $sdirFile;
$sampPath = $sdirPath;
$sampName = $sdirName;
fseek($sdirFile, 0x10);
$sdirOffset = fread($sdirFile, 4);
$sdirSize = fread($sdirFile, 4);
fseek($sdirFile, 0x28);
$sampOffset = fread($sdirFile, 4);
$sampSize = fread($sdirFile, 4);
if(strlen($sdirOffset)!=4 || strlen($sdirSize)!=4 || strlen($sampOffset)!=4 || strlen($sampSize)!=4)
{
err_skip($sdirFile, 'Failed to read snd header values in %s.', $sdirName);
continue;
}
$sndSize = filesize($sdirPath);
$sdirOffset = unpack('N',$sdirOffset)[1];
$sdirSize = unpack('N',$sdirSize)[1];
$sampOffset = unpack('N',$sampOffset)[1];
$sampSize = unpack('N',$sampSize)[1];
if($sdirOffset+$sdirSize > $sndSize)
{
err_skip($sdirFile, 'The sdir table in %s is out of range based on header values.', $sdirName);
continue;
}
if($sampOffset+$sampSize > $sndSize)
{
err_skip($sdirFile, 'The sample chunk in %s is out of range based on header values.', $sdirName);
continue;
}
}
else
{
continue;
}
/*
* get offset, sample rate, raw sample count, coefficients
* (and maybe, eventually, some other data)
*
* 4 - 0xFF FF FF FF (end of block)
* 32 - block 1 entry size
* 40 - block 2 entry size (that I've ever seen..)
*/
$SampleArr = array();
$Entries = ($sdirSize-4) / (32+40);
if(($sdirSize-4) % $Entries)
{
err_skip($sdirFile, 'Unexpected entry size in sdir table 2 in %s.', $sdirName);
continue;
}
fseek($sdirFile, $sdirOffset);
for($i=1; $i<=$Entries; ++$i)
{
fseek($sdirFile, 4, SEEK_CUR);
$Offset = fread($sdirFile, 4);
fseek($sdirFile, 6, SEEK_CUR);
$Rate = fread($sdirFile, 2);
$RawSamples = fread($sdirFile, 4);
fseek($sdirFile, 12, SEEK_CUR);
//$LoopStart = fread($sdirFile, 4);
//$LoopEnd = fread($sdirFile, 4);
//$DecDataOffset = fread($sdirFile, 4);
if(strlen($Offset)!=4 || strlen($Rate)!=2 || strlen($RawSamples)!=4)
{
err_skip($sdirFile, 'Failed to read attributes from sdir table in %s.', $sdirName);
continue 2;
}
$SampleArr[$i]['start_offset'] = $sampOffset + unpack('N',$Offset)[1];
$SampleArr[$i]['rate'] = unpack('n',$Rate)[1];
$SampleArr[$i]['raw_samples'] = unpack('N',$RawSamples)[1];
//$SampleArr[$i]['loop_start'] = unpack('N',$LoopStart)[1];
//$SampleArr[$i]['loop_end'] = unpack('N',$LoopEnd)[1];
//$SampleArr[$i]['dec_data_offset'] = unpack('N',$DecDataOffset)[1];
if ($i>1)
{
$SampleArr[$i-1]['end_offset'] = $SampleArr[$i]['start_offset'];
}
if ($i==$Entries)
{
$SampleArr[$i]['end_offset'] = $sampOffset + $sampSize;
}
}
fseek($sdirFile, 4, SEEK_CUR);
for($i=1; $i<=$Entries; ++$i)
{
fseek($sdirFile, 8, SEEK_CUR);
$Coeffs = fread($sdirFile, 32);
if(strlen($Coeffs)!=32)
{
err_skip($sdirFile, 'Failed to read sample %d coefficients from the sdir table in %s.', $n, $Filename);
continue 2;
}
$SampleArr[$i]['coeffs'] = $Coeffs;
}
/*
* get sample data, make header, and output to DSP files
*/
echo 'Extracting samples from ' . $sampName . '... ';
if($sdirPath != $sampPath)
{
fclose($sdirFile);
if(($sampFile = fopen($sampPath,'rb')) === FALSE)
{
err_skip('Could not open file for reading.');
continue;
}
}
$dspDirPath = OUT_DIR . DIRECTORY_SEPARATOR . $Basename;
if(!is_dir($dspDirPath) && mkdir($dspDirPath) === FALSE)
{
err_skip($sampFile, 'Could not create DSP output directory.');
continue;
}
fseek($sampFile, $sampOffset);
foreach($SampleArr as $n => $Sample)
{
if($Sample['start_offset'] >= $sampSize)
{
err_skip($sampFile, 'Sample %d is out of range based on the offsets from the sdir table.', $n);
continue 2;
}
$SampleData = '';
$p = ftell($sampFile);
while ($p < $Sample['end_offset'])
{
$Bytes = fread($sampFile, 16);
$p = ftell($sampFile);
if($p < $sampSize && strlen($Bytes)!=16)
{
err_skip($sampFile, 'Failed to read sample data at 0x%x.', ftell($sampFile));
continue 3;
}
$SampleData .= $Bytes;
}
// fixes some (not all) files that vgmstream won't play
if(substr($SampleData,0,8)!=NULL_64BIT)
{
$SampleData = NULL_64BIT . $SampleData;
}
$SampleDataLen = strlen($SampleData);
$RawSamples = $Sample['raw_samples'];
$Nibbles = $SampleDataLen * 2;
$Rate = $Sample['rate'];
$LoopFlag = 0;//($Sample['loop_start'] || $Sample['loop_end']) ? 1 : 0;
$LoopStart = 2;//$Sample['loop_start'];
$LoopEnd = 0;//$Sample['loop_end'];
$Coeffs = $Sample['coeffs'];
$Header = pack('N',$RawSamples); // 0x00 raw samples
$Header.= pack('N',$Nibbles); // 0x04 nibbles
$Header.= pack('N',$Rate); // 0x08 sample rate
$Header.= pack('n',$LoopFlag); // 0x0C loop flag
$Header.= pack('n',00000000); // 0x0E format (always zero - ADPCM)
$Header.= pack('N',$LoopStart); // 0x10 loop start address (in nibbles)
$Header.= pack('N',$LoopEnd); // 0x14 loop end address (in nibbles)
$Header.= pack('N',00000002); // 0x18 initial offset value (in nibbles)
$Header.= $Coeffs; // 0x1C
$Header.= HEADER_PAD;
//$Header.= pack('n',...); // 0x3C gain
//$Header.= pack('n',...); // 0x3E predictor/scale
//$Header.= pack('n',...); // 0x40 sample history
//$Header.= pack('n',...); // 0x42 sample history
//$Header.= pack('n',...); // 0x44 predictor/scale for loop context
//$Header.= pack('n',...); // 0x46 sample history for loop context
//$Header.= pack('n',...); // 0x48 sample history for loop context
//$Header.= pack('x22'); // 0x4A pad (reserved)
$dspName = str_pad($n,2,'0',STR_PAD_LEFT) . '.dsp';
$dspPath = $dspDirPath . DIRECTORY_SEPARATOR . $dspName;
if(($dspFile = fopen($dspPath, 'wb')) === FALSE)
{
err_skip($sampFile, 'Could not open sample %d output file for writing.', $n);
continue 2;
}
if(fwrite($dspFile, $Header.$SampleData) != 0x60+$SampleDataLen)
{
err_skip($sampFile, $dspFile, 'Failed writing data to %s.', $dspName);
continue 2;
}
fclose($dspFile);
}
fclose($sampFile);
echo 'Done.' . PHP_EOL;
}
echo 'No more files to process.' . PHP_EOL;
closedir($Dir);
function err_skip()
{
$Args = func_get_args();
$Arg1 = array_shift($Args);
while (is_resource($Arg1) && get_resource_type($Arg1)=='stream')
{
fclose($Arg1);
$Arg1 = array_shift($Args);
}
$Err = vsprintf($Arg1, $Args);
echo 'ERROR: ' . $Err . ' Skipping the input file.' . PHP_EOL;
}
?>