72 const FILTER_MIX = 0x01;
75 const FILTER_NORMALIZE = 0x02;
78 const FILTER_DEGRADE = 0x04;
81 const MAX_CHANNEL = 18;
84 const MAX_SAMPLERATE = 192000;
88 const SPEAKER_FRONT_LEFT = 0x000001;
89 const SPEAKER_FRONT_RIGHT = 0x000002;
90 const SPEAKER_FRONT_CENTER = 0x000004;
91 const SPEAKER_LOW_FREQUENCY = 0x000008;
92 const SPEAKER_BACK_LEFT = 0x000010;
93 const SPEAKER_BACK_RIGHT = 0x000020;
94 const SPEAKER_FRONT_LEFT_OF_CENTER = 0x000040;
95 const SPEAKER_FRONT_RIGHT_OF_CENTER = 0x000080;
96 const SPEAKER_BACK_CENTER = 0x000100;
97 const SPEAKER_SIDE_LEFT = 0x000200;
98 const SPEAKER_SIDE_RIGHT = 0x000400;
99 const SPEAKER_TOP_CENTER = 0x000800;
100 const SPEAKER_TOP_FRONT_LEFT = 0x001000;
101 const SPEAKER_TOP_FRONT_CENTER = 0x002000;
102 const SPEAKER_TOP_FRONT_RIGHT = 0x004000;
103 const SPEAKER_TOP_BACK_LEFT = 0x008000;
104 const SPEAKER_TOP_BACK_CENTER = 0x010000;
105 const SPEAKER_TOP_BACK_RIGHT = 0x020000;
106 const SPEAKER_ALL = 0x03FFFF;
109 const WAVE_FORMAT_PCM = 0x0001;
112 const WAVE_FORMAT_IEEE_FLOAT = 0x0003;
115 const WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
118 const WAVE_SUBFORMAT_PCM =
"0100000000001000800000aa00389b71";
121 const WAVE_SUBFORMAT_IEEE_FLOAT =
"0300000000001000800000aa00389b71";
130 protected static $LOOKUP_LOGBASE = array(
131 2.513, 2.667, 2.841, 3.038, 3.262,
132 3.520, 3.819, 4.171, 4.589, 5.093,
133 5.711, 6.487, 7.483, 8.806, 10.634,
134 13.302, 17.510, 24.970, 41.155, 96.088
138 protected $_actualSize;
141 protected $_chunkSize;
144 protected $_fmtChunkSize;
147 protected $_fmtExtendedSize;
150 protected $_factChunkSize;
153 protected $_dataSize;
156 protected $_dataSize_fp;
159 protected $_dataSize_valid;
162 protected $_dataOffset;
165 protected $_audioFormat;
168 protected $_audioSubFormat;
171 protected $_numChannels;
174 protected $_channelMask;
177 protected $_sampleRate;
180 protected $_bitsPerSample;
183 protected $_validBitsPerSample;
186 protected $_blockAlign;
189 protected $_numBlocks;
192 protected $_byteRate;
218 public function __construct($numChannelsOrFileName = null, $sampleRateOrReadData = null, $bitsPerSample = null)
220 $this->_actualSize = 44;
221 $this->_chunkSize = 36;
222 $this->_fmtChunkSize = 16;
223 $this->_fmtExtendedSize = 0;
224 $this->_factChunkSize = 0;
225 $this->_dataSize = 0;
226 $this->_dataSize_fp = 0;
227 $this->_dataSize_valid =
true;
228 $this->_dataOffset = 44;
229 $this->_audioFormat = self::WAVE_FORMAT_PCM;
230 $this->_audioSubFormat = null;
231 $this->_numChannels = 1;
233 $this->_sampleRate = 8000;
234 $this->_bitsPerSample = 8;
235 $this->_validBitsPerSample = 8;
236 $this->_blockAlign = 1;
237 $this->_numBlocks = 0;
238 $this->_byteRate = 8000;
239 $this->_samples =
'';
243 if (is_string($numChannelsOrFileName)) {
244 $this->
openWav($numChannelsOrFileName, is_bool($sampleRateOrReadData) ? $sampleRateOrReadData :
true);
247 $this->setNumChannels(is_null($numChannelsOrFileName) ? 1 : $numChannelsOrFileName)
248 ->setSampleRate(is_null($sampleRateOrReadData) ? 8000 : $sampleRateOrReadData)
249 ->setBitsPerSample(is_null($bitsPerSample) ? 8 : $bitsPerSample);
253 public function __destruct() {
254 if (is_resource($this->_fp)) $this->
closeWav();
257 public function __clone() {
285 if ($bitDepth === null) {
286 $bitDepth = strlen($sampleBinary) * 8;
292 return ord($sampleBinary);
296 $data = unpack(
'v', $sampleBinary);
298 if ($sample >= 0x8000) {
305 $data = unpack(
'C3', $sampleBinary);
306 $sample = $data[1] | ($data[2] << 8) | ($data[3] << 16);
307 if ($sample >= 0x800000) {
308 $sample -= 0x1000000;
314 $data = unpack(
'f', $sampleBinary);
341 return pack(
'v', $sample);
346 $sample += 0x1000000;
348 return pack(
'C3', $sample & 0xff, ($sample >> 8) & 0xff, ($sample >> 16) & 0xff);
352 return pack(
'f', $sample);
368 $sampleBytes = $bitDepth / 8;
369 if ($numChannels === null) {
370 $numChannels = strlen($sampleBlock) / $sampleBytes;
374 for ($i = 0; $i < $numChannels; $i++) {
375 $sampleBinary = substr($sampleBlock, $i * $sampleBytes, $sampleBytes);
391 foreach($samples as $sample) {
418 if ($threshold >= 1) {
419 return $sampleFloat * $threshold;
423 if ($threshold <= -1) {
424 return $sampleFloat / -$threshold;
427 $sign = $sampleFloat < 0 ? -1 : 1;
428 $sampleAbs = abs($sampleFloat);
431 if ($threshold >= 0 && $threshold < 1 && $sampleAbs > $threshold) {
432 $loga = self::$LOOKUP_LOGBASE[(int)($threshold * 20)];
433 return $sign * ($threshold + (1 - $threshold) * log(1 + $loga * ($sampleAbs - $threshold) / (2 - $threshold)) / log(1 + $loga));
437 $thresholdAbs = abs($threshold);
438 if ($threshold > -1 && $threshold < 0 && $sampleAbs > $thresholdAbs) {
439 return $sign * ($thresholdAbs + (1 - $thresholdAbs) / (2 - $thresholdAbs) * ($sampleAbs - $thresholdAbs));
450 public function getActualSize() {
451 return $this->_actualSize;
454 protected function setActualSize($actualSize = null) {
455 if (is_null($actualSize)) {
456 $this->_actualSize = 8 + $this->_chunkSize;
458 $this->_actualSize = $actualSize;
464 public function getChunkSize() {
465 return $this->_chunkSize;
468 protected function setChunkSize($chunkSize = null) {
469 if (is_null($chunkSize)) {
470 $this->_chunkSize = 4 +
471 8 + $this->_fmtChunkSize +
472 ($this->_factChunkSize > 0 ? 8 + $this->_factChunkSize : 0) +
473 8 + $this->_dataSize +
474 ($this->_dataSize & 1);
476 $this->_chunkSize = $chunkSize;
479 $this->setActualSize();
484 public function getFmtChunkSize() {
485 return $this->_fmtChunkSize;
488 protected function setFmtChunkSize($fmtChunkSize = null) {
489 if (is_null($fmtChunkSize)) {
490 $this->_fmtChunkSize = 16 + $this->_fmtExtendedSize;
492 $this->_fmtChunkSize = $fmtChunkSize;
495 $this->setChunkSize()
501 public function getFmtExtendedSize() {
502 return $this->_fmtExtendedSize;
505 protected function setFmtExtendedSize($fmtExtendedSize = null) {
506 if (is_null($fmtExtendedSize)) {
507 if ($this->_audioFormat == self::WAVE_FORMAT_EXTENSIBLE) {
508 $this->_fmtExtendedSize = 2 + 22;
509 } elseif ($this->_audioFormat != self::WAVE_FORMAT_PCM) {
510 $this->_fmtExtendedSize = 2 + 0;
512 $this->_fmtExtendedSize = 0;
515 $this->_fmtExtendedSize = $fmtExtendedSize;
518 $this->setFmtChunkSize();
523 public function getFactChunkSize() {
524 return $this->_factChunkSize;
527 protected function setFactChunkSize($factChunkSize = null) {
528 if (is_null($factChunkSize)) {
529 if ($this->_audioFormat != self::WAVE_FORMAT_PCM) {
530 $this->_factChunkSize = 4;
532 $this->_factChunkSize = 0;
535 $this->_factChunkSize = $factChunkSize;
538 $this->setChunkSize()
544 public function getDataSize() {
545 return $this->_dataSize;
548 protected function setDataSize($dataSize = null) {
549 if (is_null($dataSize)) {
550 $this->_dataSize = strlen($this->_samples);
552 $this->_dataSize = $dataSize;
555 $this->setChunkSize()
557 $this->_dataSize_valid =
true;
562 public function getDataOffset() {
563 return $this->_dataOffset;
566 protected function setDataOffset($dataOffset = null) {
567 if (is_null($dataOffset)) {
568 $this->_dataOffset = 8 +
570 8 + $this->_fmtChunkSize +
571 ($this->_factChunkSize > 0 ? 8 + $this->_factChunkSize : 0) +
574 $this->_dataOffset = $dataOffset;
580 public function getAudioFormat() {
581 return $this->_audioFormat;
584 protected function setAudioFormat($audioFormat = null) {
585 if (is_null($audioFormat)) {
586 if (($this->_bitsPerSample <= 16 || $this->_bitsPerSample == 32)
587 && $this->_validBitsPerSample == $this->_bitsPerSample
588 && $this->_channelMask == self::SPEAKER_DEFAULT
589 && $this->_numChannels <= 2) {
590 if ($this->_bitsPerSample <= 16) {
591 $this->_audioFormat = self::WAVE_FORMAT_PCM;
593 $this->_audioFormat = self::WAVE_FORMAT_IEEE_FLOAT;
596 $this->_audioFormat = self::WAVE_FORMAT_EXTENSIBLE;
599 $this->_audioFormat = $audioFormat;
602 $this->setAudioSubFormat()
604 ->setFmtExtendedSize();
609 public function getAudioSubFormat() {
610 return $this->_audioSubFormat;
613 protected function setAudioSubFormat($audioSubFormat = null) {
614 if (is_null($audioSubFormat)) {
615 if ($this->_bitsPerSample == 32) {
616 $this->_audioSubFormat = self::WAVE_SUBFORMAT_IEEE_FLOAT;
618 $this->_audioSubFormat = self::WAVE_SUBFORMAT_PCM;
621 $this->_audioSubFormat = $audioSubFormat;
627 public function getNumChannels() {
628 return $this->_numChannels;
631 public function setNumChannels($numChannels) {
632 if ($numChannels < 1 || $numChannels > self::MAX_CHANNEL) {
633 throw new WavFileException(
'Unsupported number of channels. Only up to ' . self::MAX_CHANNEL .
' channels are supported.');
634 } elseif ($this->_samples !==
'') {
635 trigger_error(
'Wav already has sample data. Changing the number of channels does not convert and may corrupt the data.', E_USER_NOTICE);
638 $this->_numChannels = (int)$numChannels;
640 $this->setAudioFormat()
647 public function getChannelMask() {
648 return $this->_channelMask;
651 public function setChannelMask($channelMask = self::SPEAKER_DEFAULT) {
652 if ($channelMask != 0) {
654 $c = (int)$channelMask;
660 if ($n != $this->_numChannels || (((
int)$channelMask | self::SPEAKER_ALL) != self::SPEAKER_ALL)) {
661 throw new WavFileException(
'Invalid channel mask. The number of channels does not match the number of locations in the mask.');
665 $this->_channelMask = (int)$channelMask;
667 $this->setAudioFormat();
672 public function getSampleRate() {
673 return $this->_sampleRate;
676 public function setSampleRate($sampleRate) {
677 if ($sampleRate < 1 || $sampleRate > self::MAX_SAMPLERATE) {
679 } elseif ($this->_samples !==
'') {
680 trigger_error(
'Wav already has sample data. Changing the sample rate does not convert the data and may yield undesired results.', E_USER_NOTICE);
683 $this->_sampleRate = (int)$sampleRate;
685 $this->setByteRate();
690 public function getBitsPerSample() {
691 return $this->_bitsPerSample;
694 public function setBitsPerSample($bitsPerSample) {
695 if (!in_array($bitsPerSample, array(8, 16, 24, 32))) {
696 throw new WavFileException(
'Unsupported bits per sample. Only 8, 16, 24 and 32 bits are supported.');
697 } elseif ($this->_samples !==
'') {
698 trigger_error(
'Wav already has sample data. Changing the bits per sample does not convert and may corrupt the data.', E_USER_NOTICE);
701 $this->_bitsPerSample = (int)$bitsPerSample;
703 $this->setValidBitsPerSample()
710 public function getValidBitsPerSample() {
711 return $this->_validBitsPerSample;
714 protected function setValidBitsPerSample($validBitsPerSample = null) {
715 if (is_null($validBitsPerSample)) {
716 $this->_validBitsPerSample = $this->_bitsPerSample;
718 if ($validBitsPerSample < 1 || $validBitsPerSample > $this->_bitsPerSample) {
719 throw new WavFileException(
'ValidBitsPerSample cannot be greater than BitsPerSample.');
721 $this->_validBitsPerSample = (int)$validBitsPerSample;
724 $this->setAudioFormat();
729 public function getBlockAlign() {
730 return $this->_blockAlign;
733 protected function setBlockAlign($blockAlign = null) {
734 if (is_null($blockAlign)) {
735 $this->_blockAlign = $this->_numChannels * $this->_bitsPerSample / 8;
737 $this->_blockAlign = $blockAlign;
740 $this->setNumBlocks();
745 public function getNumBlocks()
747 return $this->_numBlocks;
750 protected function setNumBlocks($numBlocks = null) {
751 if (is_null($numBlocks)) {
752 $this->_numBlocks = (int)($this->_dataSize / $this->_blockAlign);
754 $this->_numBlocks = $numBlocks;
760 public function getByteRate() {
761 return $this->_byteRate;
764 protected function setByteRate($byteRate = null) {
765 if (is_null($byteRate)) {
766 $this->_byteRate = $this->_sampleRate * $this->_numChannels * $this->_bitsPerSample / 8;
768 $this->_byteRate = $byteRate;
774 public function getSamples() {
775 return $this->_samples;
778 public function setSamples(&$samples =
'') {
779 if (strlen($samples) % $this->_blockAlign != 0) {
780 throw new WavFileException(
'Incorrect samples size. Has to be a multiple of BlockAlign.');
783 $this->_samples = $samples;
785 $this->setDataSize();
794 public function getMinAmplitude()
796 if ($this->_bitsPerSample == 8) {
798 } elseif ($this->_bitsPerSample == 32) {
801 return -(1 << ($this->_bitsPerSample - 1));
805 public function getZeroAmplitude()
807 if ($this->_bitsPerSample == 8) {
809 } elseif ($this->_bitsPerSample == 32) {
816 public function getMaxAmplitude()
818 if($this->_bitsPerSample == 8) {
820 } elseif($this->_bitsPerSample == 32) {
823 return (1 << ($this->_bitsPerSample - 1)) - 1;
840 $this->setAudioFormat();
841 $this->setNumBlocks();
844 $header = pack(
'N', 0x52494646);
845 $header .= pack(
'V', $this->getChunkSize());
846 $header .= pack(
'N', 0x57415645);
849 $header .= pack(
'N', 0x666d7420);
850 $header .= pack(
'V', $this->getFmtChunkSize());
851 $header .= pack(
'v', $this->getAudioFormat());
852 $header .= pack(
'v', $this->getNumChannels());
853 $header .= pack(
'V', $this->getSampleRate());
854 $header .= pack(
'V', $this->getByteRate());
855 $header .= pack(
'v', $this->getBlockAlign());
856 $header .= pack(
'v', $this->getBitsPerSample());
857 if($this->getFmtExtendedSize() == 24) {
858 $header .= pack(
'v', 22);
859 $header .= pack(
'v', $this->getValidBitsPerSample());
860 $header .= pack(
'V', $this->getChannelMask());
861 $header .= pack(
'H32', $this->getAudioSubFormat());
862 } elseif ($this->getFmtExtendedSize() == 2) {
863 $header .= pack(
'v', 0);
867 if ($this->getFactChunkSize() == 4) {
868 $header .= pack(
'N', 0x66616374);
869 $header .= pack(
'V', 4);
870 $header .= pack(
'V', $this->getNumBlocks());
884 if (!$this->_dataSize_valid) {
885 $this->setDataSize();
890 return pack(
'N', 0x64617461) .
891 pack(
'V', $this->getDataSize()) .
893 ($this->getDataSize() & 1 ? chr(0) :
'');
902 public function save($filename)
904 $fp = @fopen($filename,
'w+b');
905 if (!is_resource($fp)) {
906 throw new WavFileException(
'Failed to open "' . $filename .
'" for writing.');
924 public function openWav($filename, $readData =
true)
927 if (!file_exists($filename)) {
928 throw new WavFileException(
'Failed to open "' . $filename .
'". File not found.');
929 } elseif (!is_readable($filename)) {
930 throw new WavFileException(
'Failed to open "' . $filename .
'". File is not readable.');
931 } elseif (is_resource($this->_fp)) {
937 $this->_fp = @fopen($filename,
'rb');
938 if (!is_resource($this->_fp)) {
943 return $this->
readWav($readData);
951 if (is_resource($this->_fp)) fclose($this->_fp);
967 if (is_resource($this->_fp)) $this->
closeWav();
971 $this->_fp = @fopen(
'php://memory',
'w+b');
972 if (!is_resource($this->_fp)) {
973 throw new WavFileException(
'Failed to open memory stream to write wav data. Use openWav() instead.');
977 fwrite($this->_fp, $data);
981 if ($free) $data = null;
996 if (!is_resource($this->_fp)) {
1021 if (!is_resource($this->_fp)) {
1026 $stat = fstat($this->_fp);
1027 $actualSize = $stat[
'size'];
1029 $this->_actualSize = $actualSize;
1033 $header = fread($this->_fp, 36);
1034 if (strlen($header) < 36) {
1040 $RIFF = unpack(
'NChunkID/VChunkSize/NFormat', $header);
1042 if ($RIFF[
'ChunkID'] != 0x52494646) {
1046 if ($actualSize - 8 < $RIFF[
'ChunkSize']) {
1047 trigger_error(
'"RIFF" chunk size does not match actual file size. Found ' . $RIFF[
'ChunkSize'] .
', expected ' . ($actualSize - 8) .
'.', E_USER_NOTICE);
1048 $RIFF[
'ChunkSize'] = $actualSize - 8;
1052 if ($RIFF[
'Format'] != 0x57415645) {
1053 throw new WavFormatException(
'Not wav format. "RIFF" chunk format is not "WAVE".', 4);
1056 $this->_chunkSize = $RIFF[
'ChunkSize'];
1060 $fmt = unpack(
'NSubchunkID/VSubchunkSize/vAudioFormat/vNumChannels/'
1061 .
'VSampleRate/VByteRate/vBlockAlign/vBitsPerSample',
1062 substr($header, 12));
1064 if ($fmt[
'SubchunkID'] != 0x666d7420) {
1068 if ($fmt[
'SubchunkSize'] < 16) {
1072 if ( $fmt[
'AudioFormat'] != self::WAVE_FORMAT_PCM
1073 && $fmt[
'AudioFormat'] != self::WAVE_FORMAT_IEEE_FLOAT
1074 && $fmt[
'AudioFormat'] != self::WAVE_FORMAT_EXTENSIBLE)
1076 throw new WavFormatException(
'Unsupported audio format. Only PCM or IEEE FLOAT (EXTENSIBLE) audio is supported.', 13);
1079 if ($fmt[
'NumChannels'] < 1 || $fmt[
'NumChannels'] > self::MAX_CHANNEL) {
1083 if ($fmt[
'SampleRate'] < 1 || $fmt[
'SampleRate'] > self::MAX_SAMPLERATE) {
1087 if ( ($fmt[
'AudioFormat'] == self::WAVE_FORMAT_PCM && !in_array($fmt[
'BitsPerSample'], array(8, 16, 24)))
1088 || ($fmt[
'AudioFormat'] == self::WAVE_FORMAT_IEEE_FLOAT && $fmt[
'BitsPerSample'] != 32)
1089 || ($fmt[
'AudioFormat'] == self::WAVE_FORMAT_EXTENSIBLE && !in_array($fmt[
'BitsPerSample'], array(8, 16, 24, 32))))
1091 throw new WavFormatException(
'Only 8, 16 and 24-bit PCM and 32-bit IEEE FLOAT (EXTENSIBLE) audio is supported.', 16);
1094 $blockAlign = $fmt[
'NumChannels'] * $fmt[
'BitsPerSample'] / 8;
1095 if ($blockAlign != $fmt[
'BlockAlign']) {
1096 trigger_error(
'Invalid block align in "fmt " subchunk. Found ' . $fmt[
'BlockAlign'] .
', expected ' . $blockAlign .
'.', E_USER_NOTICE);
1097 $fmt[
'BlockAlign'] = $blockAlign;
1101 $byteRate = $fmt[
'SampleRate'] * $blockAlign;
1102 if ($byteRate != $fmt[
'ByteRate']) {
1103 trigger_error(
'Invalid average byte rate in "fmt " subchunk. Found ' . $fmt[
'ByteRate'] .
', expected ' . $byteRate .
'.', E_USER_NOTICE);
1104 $fmt[
'ByteRate'] = $byteRate;
1108 $this->_fmtChunkSize = $fmt[
'SubchunkSize'];
1109 $this->_audioFormat = $fmt[
'AudioFormat'];
1110 $this->_numChannels = $fmt[
'NumChannels'];
1111 $this->_sampleRate = $fmt[
'SampleRate'];
1112 $this->_byteRate = $fmt[
'ByteRate'];
1113 $this->_blockAlign = $fmt[
'BlockAlign'];
1114 $this->_bitsPerSample = $fmt[
'BitsPerSample'];
1119 if ($fmt[
'SubchunkSize'] > 16) {
1121 $extendedFmt = fread($this->_fp, $fmt[
'SubchunkSize'] - 16 + ($fmt[
'SubchunkSize'] & 1));
1122 if (strlen($extendedFmt) < $fmt[
'SubchunkSize'] - 16) {
1129 if ($fmt[
'AudioFormat'] == self::WAVE_FORMAT_EXTENSIBLE) {
1130 if (strlen($extendedFmt) < 24) {
1131 throw new WavFormatException(
'Invalid EXTENSIBLE "fmt " subchunk size. Found ' . $fmt[
'SubchunkSize'] .
', expected at least 40.', 19);
1134 $extensibleFmt = unpack(
'vSize/vValidBitsPerSample/VChannelMask/H32SubFormat', substr($extendedFmt, 0, 24));
1136 if ( $extensibleFmt[
'SubFormat'] != self::WAVE_SUBFORMAT_PCM
1137 && $extensibleFmt[
'SubFormat'] != self::WAVE_SUBFORMAT_IEEE_FLOAT)
1139 throw new WavFormatException(
'Unsupported audio format. Only PCM or IEEE FLOAT (EXTENSIBLE) audio is supported.', 13);
1142 if ( ($extensibleFmt[
'SubFormat'] == self::WAVE_SUBFORMAT_PCM && !in_array($fmt[
'BitsPerSample'], array(8, 16, 24)))
1143 || ($extensibleFmt[
'SubFormat'] == self::WAVE_SUBFORMAT_IEEE_FLOAT && $fmt[
'BitsPerSample'] != 32))
1145 throw new WavFormatException(
'Only 8, 16 and 24-bit PCM and 32-bit IEEE FLOAT (EXTENSIBLE) audio is supported.', 16);
1148 if ($extensibleFmt[
'Size'] != 22) {
1149 trigger_error(
'Invaid extension size in EXTENSIBLE "fmt " subchunk.', E_USER_NOTICE);
1150 $extensibleFmt[
'Size'] = 22;
1154 if ($extensibleFmt[
'ValidBitsPerSample'] != $fmt[
'BitsPerSample']) {
1155 trigger_error(
'Invaid or unsupported valid bits per sample in EXTENSIBLE "fmt " subchunk.', E_USER_NOTICE);
1156 $extensibleFmt[
'ValidBitsPerSample'] = $fmt[
'BitsPerSample'];
1160 if ($extensibleFmt[
'ChannelMask'] != 0) {
1162 $c = (int)$extensibleFmt[
'ChannelMask'];
1168 if ($n != $fmt[
'NumChannels'] || (((
int)$extensibleFmt[
'ChannelMask'] | self::SPEAKER_ALL) != self::SPEAKER_ALL)) {
1169 trigger_error(
'Invalid channel mask in EXTENSIBLE "fmt " subchunk. The number of channels does not match the number of locations in the mask.', E_USER_NOTICE);
1170 $extensibleFmt[
'ChannelMask'] = 0;
1175 $this->_fmtExtendedSize = strlen($extendedFmt);
1176 $this->_validBitsPerSample = $extensibleFmt[
'ValidBitsPerSample'];
1177 $this->_channelMask = $extensibleFmt[
'ChannelMask'];
1178 $this->_audioSubFormat = $extensibleFmt[
'SubFormat'];
1181 $this->_fmtExtendedSize = strlen($extendedFmt);
1182 $this->_validBitsPerSample = $fmt[
'BitsPerSample'];
1183 $this->_channelMask = 0;
1184 $this->_audioSubFormat = null;
1189 $factSubchunk = array();
1190 $dataSubchunk = array();
1192 while (!feof($this->_fp)) {
1193 $subchunkHeader = fread($this->_fp, 8);
1194 if (strlen($subchunkHeader) < 8) {
1198 $subchunk = unpack(
'NSubchunkID/VSubchunkSize', $subchunkHeader);
1200 if ($subchunk[
'SubchunkID'] == 0x66616374) {
1202 $subchunkData = fread($this->_fp, $subchunk[
'SubchunkSize'] + ($subchunk[
'SubchunkSize'] & 1));
1203 if (strlen($subchunkData) < 4) {
1207 $factParams = unpack(
'VSampleLength', substr($subchunkData, 0, 4));
1208 $factSubchunk = array_merge($subchunk, $factParams);
1210 } elseif ($subchunk[
'SubchunkID'] == 0x64617461) {
1211 $dataSubchunk = $subchunk;
1215 } elseif ($subchunk[
'SubchunkID'] == 0x7761766C) {
1216 throw new WavFormatException(
'Wave List Chunk ("wavl" subchunk) is not supported.', 106);
1220 if ( $subchunk[
'SubchunkSize'] < 0
1221 || fseek($this->_fp, $subchunk[
'SubchunkSize'] + ($subchunk[
'SubchunkSize'] & 1), SEEK_CUR) !== 0) {
1222 throw new WavFormatException(
'Invalid subchunk (0x' . dechex($subchunk[
'SubchunkID']) .
') encountered.', 103);
1227 if (empty($dataSubchunk)) {
1233 $dataOffset = ftell($this->_fp);
1234 if ($dataSubchunk[
'SubchunkSize'] < 0 || $actualSize - $dataOffset < $dataSubchunk[
'SubchunkSize']) {
1235 trigger_error(
'Invalid "data" subchunk size.', E_USER_NOTICE);
1236 $dataSubchunk[
'SubchunkSize'] = $actualSize - $dataOffset;
1240 $this->_dataOffset = $dataOffset;
1241 $this->_dataSize = $dataSubchunk[
'SubchunkSize'];
1242 $this->_dataSize_fp = $dataSubchunk[
'SubchunkSize'];
1243 $this->_dataSize_valid =
false;
1244 $this->_samples =
'';
1248 $numBlocks = (int)($dataSubchunk[
'SubchunkSize'] / $fmt[
'BlockAlign']);
1250 if (empty($factSubchunk)) {
1251 $factSubchunk = array(
'SubchunkSize' => 0,
'SampleLength' => $numBlocks);
1254 if ($factSubchunk[
'SampleLength'] != $numBlocks) {
1255 trigger_error(
'Invalid sample length in "fact" subchunk.', E_USER_NOTICE);
1256 $factSubchunk[
'SampleLength'] = $numBlocks;
1260 $this->_factChunkSize = $factSubchunk[
'SubchunkSize'];
1261 $this->_numBlocks = $factSubchunk[
'SampleLength'];
1278 if (!is_resource($this->_fp)) {
1282 if ($dataOffset < 0 || $dataOffset % $this->getBlockAlign() > 0) {
1283 throw new WavFileException(
'Invalid data offset. Has to be a multiple of BlockAlign.');
1286 if (is_null($dataSize)) {
1287 $dataSize = $this->_dataSize_fp - ($this->_dataSize_fp % $this->getBlockAlign());
1288 } elseif ($dataSize < 0 || $dataSize % $this->getBlockAlign() > 0) {
1289 throw new WavFileException(
'Invalid data size to read. Has to be a multiple of BlockAlign.');
1294 if ($dataOffset > 0 && fseek($this->_fp, $dataOffset, SEEK_CUR) !== 0) {
1299 $this->_samples .= fread($this->_fp, $dataSize);
1300 $this->setDataSize();
1319 if (!$this->_dataSize_valid) {
1320 $this->setDataSize();
1323 $offset = $blockNum * $this->_blockAlign;
1324 if ($offset + $this->_blockAlign > $this->_dataSize || $offset < 0) {
1330 return substr($this->_samples, $offset, $this->_blockAlign);
1344 $blockAlign = $this->_blockAlign;
1345 if (!isset($sampleBlock[$blockAlign - 1]) || isset($sampleBlock[$blockAlign])) {
1346 throw new WavFileException(
'Incorrect sample block size. Got ' . strlen($sampleBlock) .
', expected ' . $blockAlign .
'.');
1349 if (!$this->_dataSize_valid) {
1350 $this->setDataSize();
1353 $numBlocks = (int)($this->_dataSize / $blockAlign);
1354 $offset = $blockNum * $blockAlign;
1355 if ($blockNum > $numBlocks || $blockNum < 0) {
1361 if ($blockNum == $numBlocks) {
1363 $this->_samples .= $sampleBlock;
1364 $this->_dataSize += $blockAlign;
1365 $this->_chunkSize += $blockAlign;
1366 $this->_actualSize += $blockAlign;
1367 $this->_numBlocks++;
1370 for ($i = 0; $i < $blockAlign; ++$i) {
1371 $this->_samples[$offset + $i] = $sampleBlock[$i];
1389 if ($channelNum < 1 || $channelNum > $this->_numChannels) {
1393 if (!$this->_dataSize_valid) {
1394 $this->setDataSize();
1397 $sampleBytes = $this->_bitsPerSample / 8;
1398 $offset = $blockNum * $this->_blockAlign + ($channelNum - 1) * $sampleBytes;
1399 if ($offset + $sampleBytes > $this->_dataSize || $offset < 0) {
1404 $sampleBinary = substr($this->_samples, $offset, $sampleBytes);
1407 switch ($this->_bitsPerSample) {
1410 return (
float)((ord($sampleBinary) - 0x80) / 0x80);
1414 $data = unpack(
'v', $sampleBinary);
1416 if ($sample >= 0x8000) {
1419 return (
float)($sample / 0x8000);
1423 $data = unpack(
'C3', $sampleBinary);
1424 $sample = $data[1] | ($data[2] << 8) | ($data[3] << 16);
1425 if ($sample >= 0x800000) {
1426 $sample -= 0x1000000;
1428 return (
float)($sample / 0x800000);
1432 $data = unpack(
'f', $sampleBinary);
1433 return (
float)$data[1];
1453 if ($channelNum < 1 || $channelNum > $this->_numChannels) {
1457 if (!$this->_dataSize_valid) {
1458 $this->setDataSize();
1461 $dataSize = $this->_dataSize;
1462 $bitsPerSample = $this->_bitsPerSample;
1463 $sampleBytes = $bitsPerSample / 8;
1464 $offset = $blockNum * $this->_blockAlign + ($channelNum - 1) * $sampleBytes;
1465 if (($offset + $sampleBytes > $dataSize && $offset != $dataSize) || $offset < 0) {
1466 throw new WavFileException(
'Sample block or channel number is out of range.');
1471 if ($bitsPerSample == 32) {
1472 $sample = $sampleFloat < -1.0 ? -1.0 : ($sampleFloat > 1.0 ? 1.0 : $sampleFloat);
1474 $p = 1 << ($bitsPerSample - 1);
1477 $sample = $sampleFloat < 0 ? (int)($sampleFloat * $p - 0.5) : (int)($sampleFloat * $p + 0.5);
1480 if ($sample < -$p) {
1482 } elseif ($sample > $p - 1) {
1488 switch ($bitsPerSample) {
1491 $sampleBinary = chr($sample + 0x80);
1499 $sampleBinary = pack(
'v', $sample);
1505 $sample += 0x1000000;
1507 $sampleBinary = pack(
'C3', $sample & 0xff, ($sample >> 8) & 0xff, ($sample >> 16) & 0xff);
1512 $sampleBinary = pack(
'f', $sample);
1516 $sampleBinary = null;
1522 if ($offset == $dataSize) {
1524 $this->_samples .= $sampleBinary;
1525 $this->_dataSize += $sampleBytes;
1526 $this->_chunkSize += $sampleBytes;
1527 $this->_actualSize += $sampleBytes;
1528 $this->_numBlocks = (int)($this->_dataSize / $this->_blockAlign);
1531 for ($i = 0; $i < $sampleBytes; ++$i) {
1532 $this->_samples{$offset + $i} = $sampleBinary{$i};
1568 public function filter($filters, $blockOffset = 0, $numBlocks = null)
1571 $totalBlocks = $this->getNumBlocks();
1572 $numChannels = $this->getNumChannels();
1573 if (is_null($numBlocks)) $numBlocks = $totalBlocks - $blockOffset;
1575 if (!is_array($filters) || empty($filters) || $blockOffset < 0 || $blockOffset > $totalBlocks || $numBlocks <= 0) {
1581 $filter_mix =
false;
1582 if (array_key_exists(self::FILTER_MIX, $filters)) {
1583 if (!is_array($filters[self::FILTER_MIX])) {
1585 $filters[self::FILTER_MIX] = array(
'wav' => $filters[self::FILTER_MIX]);
1588 $mix_wav = @$filters[self::FILTER_MIX][
'wav'];
1589 if (!($mix_wav instanceof
WavFile)) {
1591 } elseif ($mix_wav->getSampleRate() != $this->getSampleRate()) {
1592 throw new WavFileException(
"Sample rate of WavFile to mix does not match.");
1593 }
else if ($mix_wav->getNumChannels() != $this->getNumChannels()) {
1594 throw new WavFileException(
"Number of channels of WavFile to mix does not match.");
1597 $mix_loop = @$filters[self::FILTER_MIX][
'loop'];
1598 if (is_null($mix_loop)) $mix_loop =
false;
1600 $mix_blockOffset = @$filters[self::FILTER_MIX][
'blockOffset'];
1601 if (is_null($mix_blockOffset)) $mix_blockOffset = 0;
1603 $mix_totalBlocks = $mix_wav->getNumBlocks();
1604 $mix_numBlocks = @$filters[self::FILTER_MIX][
'numBlocks'];
1605 if (is_null($mix_numBlocks)) $mix_numBlocks = $mix_loop ? $mix_totalBlocks : $mix_totalBlocks - $mix_blockOffset;
1606 $mix_maxBlock = min($mix_blockOffset + $mix_numBlocks, $mix_totalBlocks);
1611 $filter_normalize =
false;
1612 if (array_key_exists(self::FILTER_NORMALIZE, $filters)) {
1613 $normalize_threshold = @$filters[self::FILTER_NORMALIZE];
1615 if (!is_null($normalize_threshold) && abs($normalize_threshold) != 1) $filter_normalize =
true;
1618 $filter_degrade =
false;
1619 if (array_key_exists(self::FILTER_DEGRADE, $filters)) {
1620 $degrade_quality = @$filters[self::FILTER_DEGRADE];
1621 if (is_null($degrade_quality)) $degrade_quality = 1;
1623 if ($degrade_quality >= 0 && $degrade_quality < 1) $filter_degrade =
true;
1628 for ($block = 0; $block < $numBlocks; ++$block) {
1630 for ($channel = 1; $channel <= $numChannels; ++$channel) {
1632 $currentBlock = $blockOffset + $block;
1639 $mixBlock = ($mix_blockOffset + ($block % $mix_numBlocks)) % $mix_totalBlocks;
1641 $mixBlock = $mix_blockOffset + $block;
1644 if ($mixBlock < $mix_maxBlock) {
1645 $sampleFloat += $mix_wav->getSampleValue($mixBlock, $channel);
1650 if ($filter_normalize) {
1651 $sampleFloat = $this->
normalizeSample($sampleFloat, $normalize_threshold);
1655 if ($filter_degrade) {
1656 $sampleFloat += rand(1000000 * ($degrade_quality - 1), 1000000 * (1 - $degrade_quality)) / 1000000;
1677 if ($wav->getSampleRate() != $this->getSampleRate()) {
1679 }
else if ($wav->getBitsPerSample() != $this->getBitsPerSample()) {
1681 }
else if ($wav->getNumChannels() != $this->getNumChannels()) {
1682 throw new WavFileException(
"Number of channels for wav files do not match.");
1685 $this->_samples .= $wav->_samples;
1686 $this->setDataSize();
1700 return $this->
filter(array(
1701 WavFile::FILTER_MIX => $wav,
1702 WavFile::FILTER_NORMALIZE => $normalizeThreshold
1713 $numSamples = $this->getSampleRate() * abs($duration);
1714 $numChannels = $this->getNumChannels();
1716 $data = str_repeat(self::packSample($this->getZeroAmplitude(), $this->getBitsPerSample()), $numSamples * $numChannels);
1717 if ($duration >= 0) {
1718 $this->_samples .= $data;
1720 $this->_samples = $data . $this->_samples;
1723 $this->setDataSize();
1735 return $this->
filter(self::FILTER_DEGRADE, array(
1736 WavFile::FILTER_DEGRADE => $quality
1748 $numChannels = $this->getNumChannels();
1749 $numSamples = $this->getSampleRate() * $duration;
1750 $minAmp = $this->getMinAmplitude();
1751 $maxAmp = $this->getMaxAmplitude();
1752 $bitDepth = $this->getBitsPerSample();
1754 for ($s = 0; $s < $numSamples; ++$s) {
1755 if ($bitDepth == 32) {
1756 $val = rand(-$percent * 10000, $percent * 10000) / 1000000;
1758 $val = rand($minAmp, $maxAmp);
1759 $val = (int)($val * $percent / 100);
1762 $this->_samples .= str_repeat(self::packSample($val, $bitDepth), $numChannels);
1765 $this->setDataSize();
1777 if ($this->getBitsPerSample() == $bitsPerSample) {
1781 $tempWav =
new WavFile($this->getNumChannels(), $this->getSampleRate(), $bitsPerSample);
1783 array(self::FILTER_MIX => $this),
1785 $this->getNumBlocks()
1789 ->setBitsPerSample($bitsPerSample);
1790 $this->_samples = $tempWav->_samples;
1791 $this->setDataSize();
1805 $s =
"File Size: %u\n"
1807 .
"fmt Subchunk Size: %u\n"
1808 .
"Extended fmt Size: %u\n"
1809 .
"fact Subchunk Size: %u\n"
1810 .
"Data Offset: %u\n"
1812 .
"Audio Format: %s\n"
1813 .
"Audio SubFormat: %s\n"
1815 .
"Channel Mask: 0x%s\n"
1816 .
"Sample Rate: %u\n"
1817 .
"Bits Per Sample: %u\n"
1818 .
"Valid Bits Per Sample: %u\n"
1819 .
"Sample Block Size: %u\n"
1820 .
"Number of Sample Blocks: %u\n"
1821 .
"Byte Rate: %uBps\n";
1823 $s = sprintf($s, $this->getActualSize(),
1824 $this->getChunkSize(),
1825 $this->getFmtChunkSize(),
1826 $this->getFmtExtendedSize(),
1827 $this->getFactChunkSize(),
1828 $this->getDataOffset(),
1829 $this->getDataSize(),
1830 $this->getAudioFormat() == self::WAVE_FORMAT_PCM ?
'PCM' : ($this->getAudioFormat() == self::WAVE_FORMAT_IEEE_FLOAT ?
'IEEE FLOAT' :
'EXTENSIBLE'),
1831 $this->getAudioSubFormat() == self::WAVE_SUBFORMAT_PCM ?
'PCM' :
'IEEE FLOAT',
1832 $this->getNumChannels(),
1833 dechex($this->getChannelMask()),
1834 $this->getSampleRate(),
1835 $this->getBitsPerSample(),
1836 $this->getValidBitsPerSample(),
1837 $this->getBlockAlign(),
1838 $this->getNumBlocks(),
1839 $this->getByteRate());
1841 if (php_sapi_name() ==
'cli') {