WAV generator // v3.2 // 29th April 2023 // 'Diminished' define ("CSW2WAV_VERSION", "3.2"); define ("MAX_OP_FILE_LEN", 1000000000); // 1 GB define ("MAX_PULSE_DURATION_SMPS", 10000000); define ("MAX_NON_SILENT_PULSE_DURATION_SMPS", 100); define ("WAV_HEADER_LEN", 44); $argv = $_SERVER['argv']; $argc = $_SERVER['argc']; $square_pulses = FALSE; $pulsed_silence = FALSE; $flip_polarity = FALSE; print "\ncsw2wav.php v".CSW2WAV_VERSION."\n\n"; for ($i=1; $i < $argc; $i++) { $v = $argv[$i]; if ($v[0] != "+") { // not an option break; } $dup = 0; if ($v == "+p") { if ($square_pulses) { $dup = 1; } $square_pulses = TRUE; } else if ($v == "+s") { if ($pulsed_silence) { $dup = 1; } $pulsed_silence = TRUE; } else if ($v == "+f") { if ($flip_polarity) { $dup = 1; } $flip_polarity = TRUE; } else { print "\nE: Unknown option: $v\n\n"; usage($argv[0]); die(); } if ($dup) { print "\nE: Duplicate option: ".$v."\n\n"; exit(101); } } if ($i >= ($argc - 1)) { usage($argv[0]); die(); } //die(); $csw_fn = $argv[$i]; $op_fn = $argv[$i+1]; if (!file_exists($csw_fn)) { print "E: Input file not found: $csw_fn\n"; exit(2); } print "M: Loading $csw_fn ... "; $csw = file_get_contents($csw_fn); $csw_len = strlen($csw); print "$csw_len bytes.\n"; $data = array(); $polarity = FALSE; $rate = 0; $e = parse_csw ($csw, $data, $polarity, $rate); // data, polarity, rate out if (0 != $e) { exit($e); } $e = save ($data, $op_fn, $flip_polarity ? ( ! $polarity ) : $polarity, $rate, ! $square_pulses,// produce sine waves rather than just square pulses? ! $pulsed_silence, // render silent spans as silence, rather than pulses? 0.5); // amplitude exit($e); function usage (string $argv0) { print "Usage:\n\n php -f $argv0 [options] \n\n"; print "where [options] may be:\n"; print " +p produce square wave pulses rather than sine waves\n"; print " +s render silent sections as pulses, rather than as silence\n"; print " +f flip polarity\n\n"; print "Output file will be 16-bit mono.\n\n"; exit(1); } function parse_csw (string $csw, array &$data, bool &$polarity, int &$rate) : int { $rate = 0; $h_len = 0; $zip = false; $polarity = false; $e = parse_header ($csw, $rate, $h_len, $zip, $polarity); // rate, h_len, zip, polarity out if ($e != 0) { return $e; } print "\nM: Body starts at 0x".sprintf("%x", $h_len)."\n"; $e = parse_body (substr($csw, $h_len), $zip, $data); // data out if ($e != 0) { return $e; } print "M: Counted ".count($data)." pulses.\n"; return $e; } function save_header ($fp, int $file_len, int $rate) : int { print "file_len = ".$file_len."\n"; $a = "RIFF".le4($file_len - 8)."WAVEfmt ".le4(16).le2(1).le2(1).le4($rate); $j = (int) (16 * 1) / 8; $i = ($rate * $j); $a .= le4($i).le2($j).le2(16)."data".le4($file_len - WAV_HEADER_LEN); if (FALSE === fwrite($fp, $a)) { print "E: Updating output file header failed.\n"; return 16; } return 0; } function save (array $data, string $opfn, bool $polarity, int $rate, bool $use_sine, bool $silent_silence, float $amplitude) : int { $op = ""; $c=0; if (FALSE === ($fp = fopen($opfn, "wb"))) { print "E: Could not open output file $opfn\n"; return 10; } // temporary fake header if (FALSE === fwrite ($fp, str_repeat("\0", WAV_HEADER_LEN))) { print "E: Error writing to output file $opfn\n"; return 11; } // looking up existing calculated sine values seems to improve performance // by maybe 10-20%, so $sin_lookup = array(); for ($i=0; $i < count($data); $i++) { $p = $data[$i]; // write one pulse if ($p >= MAX_PULSE_DURATION_SMPS) { print "E: Maximum pulse duration exceeded ($p >= ".MAX_PULSE_DURATION_SMPS."), pulse #$i\n"; return 13; } $v = 1.0; $us = $use_sine; $flip = $polarity?1.0:-1.0; if ( $silent_silence && ($p >= MAX_NON_SILENT_PULSE_DURATION_SMPS) ) { // silence, don't render $v = 0.0; $us = false; } else { if (isset($sin_lookup[$p])) { $have_lookup = 1; } else { $have_lookup = 0; $sin_lookup[$p] = array(); } } //print "p = $p\n"; for ($j=0; $j<$p; $j++) { if ( $us ) { if ( ! $have_lookup ) { $v = sin(($j/(float)$p)*M_PI); $sin_lookup[$p][$j] = $v; } else { // re-use lookup table value $v = $sin_lookup[$p][$j]; } } $s = le2((int)round($flip * 32767.0 * $amplitude * $v)); $op .= $s; $c+=2; } if ($c >= MAX_OP_FILE_LEN) { print "E: Maximum output file len exceeded\n"; return 12; } if ((strlen($op) > 1000000) || ($i == (count($data) - 1))) { // flush buffer if (FALSE === fwrite($fp, $op)) { print "E: Error writing to output file $opfn\n"; return 11; } $op=""; } // reverse polarity for next pulse $polarity = ! $polarity; } $flen = 0; if (FALSE === ($flen = ftell($fp))) { print "E: ftell on output file $opfn failed\n"; return 15; } if (FALSE === fseek($fp, 0)) { print "E: fseek back to header failed writing output file $opfn\n"; return 14; } $e = save_header($fp, $flen, $rate); fclose($fp); print "M: Wrote $c bytes.\n"; //print_r($sin_lookup); return $e; } function parse_header (string $csw, int &$rate, int &$h_len, bool &$zipped, bool &$polarity) : int { $len = strlen($csw); if ($len < 0x35) { printf("E: File too short (0x%x bytes, need >= 0x35)\n", $len); return 17; } $magic_s = substr($csw, 0, 23); // v3.2: $version_s = substr($csw, 23, 2); $rate_s = substr($csw, 25, 4); $num_pulses_s = substr($csw, 29, 4); $cmp_type_s = substr($csw, 33, 1); $flags_s = substr($csw, 34, 1); $hdr_extlen_s = substr($csw, 35, 1); $appdesc_s = substr($csw, 36, 16); $hdr_extlen = ord($hdr_extlen_s); $hdr_ext = substr($csw, 52, $hdr_extlen); $rate=0; $num_pulses=0; // v3.2: now parse version properly $vmaj = 0; $vmin = 0; if ($version_s === "\x02\x00") { $vmaj = 2; $vmin = 0; } else if ($version_s === "\x02\x01") { $vmaj = 2; $vmin = 1; } else { print "E: Unknown CSW file version: $vmaj.$vmin\n"; return 18; } if (0 != ($e = parse_le4 ($rate_s, $rate))) { return $e; } if (0 != ($e = parse_le4 ($num_pulses_s, $num_pulses))) { return $e; } $cmp_type = ord($cmp_type_s); $flags = ord($flags_s); if (($cmp_type != 1) && ($cmp_type != 2)) { print "E: Bad compression type: 0x".sprintf("%02x", $cmp_type)."\n"; return 5; } // v3.2: flag legality mask changed from 0xfe, made nonzero bits 0-2 legal if ($vmin == 1) { $flags_legal_mask = 0xf8; printf("W: CSW 2.1: use of flags bits 1 & 2 is unknown; flags are 0x%x\n", $flags); } else if ($vmin == 0) { $flags_legal_mask = 0xfe; } if ($flags & $flags_legal_mask) { //0xfe) { print "E: Bad flags: 0x".sprintf("%02x", $flags)."\n"; return 6; } $polarity = (1 == ($flags & 1)); // seems CSW.exe puts out CSWs with version 2.1 rather than 2.0. // (presumably this is the reason why some of them have bit 2 set // in the flags, too), so we can't just compare \x02\x00 any more // for the version. >:/ if ($magic_s !== "Compressed Square Wave\x1a") { //\x02\x00") { print "E: Bad juju: ".my_hexdump($magic_s)."\n"; return 7; } if ($hdr_extlen != 0) { print "W: Unknown header extension data: ".my_hexdump($hdr_ext)."\n"; } print "\nCSW Version: $vmaj.$vmin\n"; print "App: \"$appdesc_s\"\n"; print "Rate: $rate\n"; print "Pulses: $num_pulses\n"; print "Polarity: ".($polarity?"starts high":"starts low")."\n"; $zipped = ($cmp_type == 2); print "Zipped: ". ($zipped ? "yes" : "no")."\n"; print "Hdr. ext. len.: $hdr_extlen\n"; $h_len = 52 + $hdr_extlen; return 0; } function my_hexdump(string $s) : string { $len = strlen($s); $o = ""; for ($i=0; $i < $len; $i++) { $o.=sprintf("%02x ", ord($s[$i])); } return $o; } function parse_body (string $body, bool $zip, array &$values) : int { $e = 0; if ($zip) { $e = body_unzip($body); // body modified if (0 != $e) { return $e; } } $len = strlen($body); for ($i=0; $i < $len; $i++) { $b = ord($body[$i]); if ($b == 0) { if (($i+4) >= $len) { print "E: long pulse overflows buffer\n"; return 9; } $e = parse_le4 (substr($body, $i+1, 4), $b); print "M: Long pulse, body offset $i (0x".sprintf("%x",$i)."): $b\n"; if (0 != $e) { return $e; } $i+=4; } $values[] = $b; } return $e; } function body_unzip (string &$b) : int { $in = $b; $b = zlib_decode($in); if (false === $b) { print "E: zlib decode failed.\n"; return 8; } print "M: Unzipped body from ".strlen($in)." to ".strlen($b)." bytes.\n"; return 0; } function parse_le4 (string $s, int &$i) : int { $len = strlen($s); if ($len != 4) { print "E: parse_le4: string has len $len, should be 4\n"; return 4; } $b = array(); for ($j=0; $j < 4; $j++) { $b[$j] = ord($s[$j]); } $i=0; $i |= $b[0]; $i |= ($b[1] << 8) & 0xff00; $i |= ($b[2] << 16) & 0xff0000; $i |= ($b[3] << 24) & 0xff000000; return 0; } function le4 (int $i) : string { $a=""; $a[0] = chr($i&0xff); $a[1] = chr(($i>>8)&0xff); $a[2] = chr(($i>>16)&0xff); $a[3] = chr(($i>>24)&0xff); return $a; } function le2 (int $i) : string { $a=""; $a[0] = chr($i&0xff); $a[1] = chr(($i>>8)&0xff); return $a; } ?>