ILIAS  release_5-1 Revision 5.0.0-5477-g43f3e3fab5f
WavFile.php
Go to the documentation of this file.
1<?php
2
3// error_reporting(E_ALL); ini_set('display_errors', 1); // uncomment this line for debugging
4
70{
71 /*%******************************************************************************************%*/
72 // Class constants
73
75 const FILTER_MIX = 0x01;
76
78 const FILTER_NORMALIZE = 0x02;
79
81 const FILTER_DEGRADE = 0x04;
82
84 const MAX_CHANNEL = 18;
85
87 const MAX_SAMPLERATE = 192000;
88
90 const SPEAKER_DEFAULT = 0x000000;
91 const SPEAKER_FRONT_LEFT = 0x000001;
92 const SPEAKER_FRONT_RIGHT = 0x000002;
93 const SPEAKER_FRONT_CENTER = 0x000004;
94 const SPEAKER_LOW_FREQUENCY = 0x000008;
95 const SPEAKER_BACK_LEFT = 0x000010;
96 const SPEAKER_BACK_RIGHT = 0x000020;
99 const SPEAKER_BACK_CENTER = 0x000100;
100 const SPEAKER_SIDE_LEFT = 0x000200;
101 const SPEAKER_SIDE_RIGHT = 0x000400;
102 const SPEAKER_TOP_CENTER = 0x000800;
103 const SPEAKER_TOP_FRONT_LEFT = 0x001000;
104 const SPEAKER_TOP_FRONT_CENTER = 0x002000;
105 const SPEAKER_TOP_FRONT_RIGHT = 0x004000;
106 const SPEAKER_TOP_BACK_LEFT = 0x008000;
107 const SPEAKER_TOP_BACK_CENTER = 0x010000;
108 const SPEAKER_TOP_BACK_RIGHT = 0x020000;
109 const SPEAKER_ALL = 0x03FFFF;
110
112 const WAVE_FORMAT_PCM = 0x0001;
113
116
119
121 const WAVE_SUBFORMAT_PCM = "0100000000001000800000aa00389b71";
122
124 const WAVE_SUBFORMAT_IEEE_FLOAT = "0300000000001000800000aa00389b71";
125
126
127 /*%******************************************************************************************%*/
128 // Properties
129
133 protected static $LOOKUP_LOGBASE = array(
134 2.513, 2.667, 2.841, 3.038, 3.262,
135 3.520, 3.819, 4.171, 4.589, 5.093,
136 5.711, 6.487, 7.483, 8.806, 10.634,
137 13.302, 17.510, 24.970, 41.155, 96.088
138 );
139
141 protected $_actualSize;
142
144 protected $_chunkSize;
145
147 protected $_fmtChunkSize;
148
151
154
156 protected $_dataSize;
157
159 protected $_dataSize_fp;
160
163
165 protected $_dataOffset;
166
168 protected $_audioFormat;
169
172
174 protected $_numChannels;
175
177 protected $_channelMask;
178
180 protected $_sampleRate;
181
184
187
189 protected $_blockAlign;
190
192 protected $_numBlocks;
193
195 protected $_byteRate;
196
198 protected $_samples;
199
201 protected $_fp;
202
203
204 /*%******************************************************************************************%*/
205 // Special methods
206
221 public function __construct($numChannelsOrFileName = null, $sampleRateOrReadData = null, $bitsPerSample = null)
222 {
223 $this->_actualSize = 44;
224 $this->_chunkSize = 36;
225 $this->_fmtChunkSize = 16;
226 $this->_fmtExtendedSize = 0;
227 $this->_factChunkSize = 0;
228 $this->_dataSize = 0;
229 $this->_dataSize_fp = 0;
230 $this->_dataSize_valid = true;
231 $this->_dataOffset = 44;
232 $this->_audioFormat = self::WAVE_FORMAT_PCM;
233 $this->_audioSubFormat = null;
234 $this->_numChannels = 1;
235 $this->_channelMask = self::SPEAKER_DEFAULT;
236 $this->_sampleRate = 8000;
237 $this->_bitsPerSample = 8;
238 $this->_validBitsPerSample = 8;
239 $this->_blockAlign = 1;
240 $this->_numBlocks = 0;
241 $this->_byteRate = 8000;
242 $this->_samples = '';
243 $this->_fp = null;
244
245
246 if (is_string($numChannelsOrFileName)) {
247 $this->openWav($numChannelsOrFileName, is_bool($sampleRateOrReadData) ? $sampleRateOrReadData : true);
248
249 } else {
250 $this->setNumChannels(is_null($numChannelsOrFileName) ? 1 : $numChannelsOrFileName)
251 ->setSampleRate(is_null($sampleRateOrReadData) ? 8000 : $sampleRateOrReadData)
252 ->setBitsPerSample(is_null($bitsPerSample) ? 8 : $bitsPerSample);
253 }
254 }
255
256 public function __destruct() {
257 if (is_resource($this->_fp)) $this->closeWav();
258 }
259
260 public function __clone() {
261 $this->_fp = null;
262 }
263
269 public function __toString()
270 {
271 return $this->makeHeader() .
272 $this->getDataSubchunk();
273 }
274
275
276 /*%******************************************************************************************%*/
277 // Static methods
278
286 public static function unpackSample($sampleBinary, $bitDepth = null)
287 {
288 if ($bitDepth === null) {
289 $bitDepth = strlen($sampleBinary) * 8;
290 }
291
292 switch ($bitDepth) {
293 case 8:
294 // unsigned char
295 return ord($sampleBinary);
296
297 case 16:
298 // signed short, little endian
299 $data = unpack('v', $sampleBinary);
300 $sample = $data[1];
301 if ($sample >= 0x8000) {
302 $sample -= 0x10000;
303 }
304 return $sample;
305
306 case 24:
307 // 3 byte packed signed integer, little endian
308 $data = unpack('C3', $sampleBinary);
309 $sample = $data[1] | ($data[2] << 8) | ($data[3] << 16);
310 if ($sample >= 0x800000) {
311 $sample -= 0x1000000;
312 }
313 return $sample;
314
315 case 32:
316 // 32-bit float
317 $data = unpack('f', $sampleBinary);
318 return $data[1];
319
320 default:
321 return null;
322 }
323 }
324
332 public static function packSample($sample, $bitDepth)
333 {
334 switch ($bitDepth) {
335 case 8:
336 // unsigned char
337 return chr($sample);
338
339 case 16:
340 // signed short, little endian
341 if ($sample < 0) {
342 $sample += 0x10000;
343 }
344 return pack('v', $sample);
345
346 case 24:
347 // 3 byte packed signed integer, little endian
348 if ($sample < 0) {
349 $sample += 0x1000000;
350 }
351 return pack('C3', $sample & 0xff, ($sample >> 8) & 0xff, ($sample >> 16) & 0xff);
352
353 case 32:
354 // 32-bit float
355 return pack('f', $sample);
356
357 default:
358 return null;
359 }
360 }
361
370 public static function unpackSampleBlock($sampleBlock, $bitDepth, $numChannels = null) {
371 $sampleBytes = $bitDepth / 8;
372 if ($numChannels === null) {
373 $numChannels = strlen($sampleBlock) / $sampleBytes;
374 }
375
376 $samples = array();
377 for ($i = 0; $i < $numChannels; $i++) {
378 $sampleBinary = substr($sampleBlock, $i * $sampleBytes, $sampleBytes);
379 $samples[$i + 1] = self::unpackSample($sampleBinary, $bitDepth);
380 }
381
382 return $samples;
383 }
384
392 public static function packSampleBlock($samples, $bitDepth) {
393 $sampleBlock = '';
394 foreach($samples as $sample) {
395 $sampleBlock .= self::packSample($sample, $bitDepth);
396 }
397
398 return $sampleBlock;
399 }
400
419 public static function normalizeSample($sampleFloat, $threshold) {
420 // apply positive gain
421 if ($threshold >= 1) {
422 return $sampleFloat * $threshold;
423 }
424
425 // apply negative gain
426 if ($threshold <= -1) {
427 return $sampleFloat / -$threshold;
428 }
429
430 $sign = $sampleFloat < 0 ? -1 : 1;
431 $sampleAbs = abs($sampleFloat);
432
433 // logarithmic compression
434 if ($threshold >= 0 && $threshold < 1 && $sampleAbs > $threshold) {
435 $loga = self::$LOOKUP_LOGBASE[(int)($threshold * 20)]; // log base modifier
436 return $sign * ($threshold + (1 - $threshold) * log(1 + $loga * ($sampleAbs - $threshold) / (2 - $threshold)) / log(1 + $loga));
437 }
438
439 // linear compression
440 $thresholdAbs = abs($threshold);
441 if ($threshold > -1 && $threshold < 0 && $sampleAbs > $thresholdAbs) {
442 return $sign * ($thresholdAbs + (1 - $thresholdAbs) / (2 - $thresholdAbs) * ($sampleAbs - $thresholdAbs));
443 }
444
445 // else ?
446 return $sampleFloat;
447 }
448
449
450 /*%******************************************************************************************%*/
451 // Getter and Setter methods for properties
452
453 public function getActualSize() {
454 return $this->_actualSize;
455 }
456
457 protected function setActualSize($actualSize = null) {
458 if (is_null($actualSize)) {
459 $this->_actualSize = 8 + $this->_chunkSize; // + "RIFF" header (ID + size)
460 } else {
461 $this->_actualSize = $actualSize;
462 }
463
464 return $this;
465 }
466
467 public function getChunkSize() {
468 return $this->_chunkSize;
469 }
470
471 protected function setChunkSize($chunkSize = null) {
472 if (is_null($chunkSize)) {
473 $this->_chunkSize = 4 + // "WAVE" chunk
474 8 + $this->_fmtChunkSize + // "fmt " subchunk
475 ($this->_factChunkSize > 0 ? 8 + $this->_factChunkSize : 0) + // "fact" subchunk
476 8 + $this->_dataSize + // "data" subchunk
477 ($this->_dataSize & 1); // padding byte
478 } else {
479 $this->_chunkSize = $chunkSize;
480 }
481
482 $this->setActualSize();
483
484 return $this;
485 }
486
487 public function getFmtChunkSize() {
489 }
490
491 protected function setFmtChunkSize($fmtChunkSize = null) {
492 if (is_null($fmtChunkSize)) {
493 $this->_fmtChunkSize = 16 + $this->_fmtExtendedSize;
494 } else {
495 $this->_fmtChunkSize = $fmtChunkSize;
496 }
497
498 $this->setChunkSize() // implicit setActualSize()
499 ->setDataOffset();
500
501 return $this;
502 }
503
504 public function getFmtExtendedSize() {
506 }
507
508 protected function setFmtExtendedSize($fmtExtendedSize = null) {
509 if (is_null($fmtExtendedSize)) {
510 if ($this->_audioFormat == self::WAVE_FORMAT_EXTENSIBLE) {
511 $this->_fmtExtendedSize = 2 + 22; // extension size for WAVE_FORMAT_EXTENSIBLE
512 } elseif ($this->_audioFormat != self::WAVE_FORMAT_PCM) {
513 $this->_fmtExtendedSize = 2 + 0; // empty extension
514 } else {
515 $this->_fmtExtendedSize = 0; // no extension, only for WAVE_FORMAT_PCM
516 }
517 } else {
518 $this->_fmtExtendedSize = $fmtExtendedSize;
519 }
520
521 $this->setFmtChunkSize(); // implicit setSize(), setActualSize(), setDataOffset()
522
523 return $this;
524 }
525
526 public function getFactChunkSize() {
528 }
529
530 protected function setFactChunkSize($factChunkSize = null) {
531 if (is_null($factChunkSize)) {
532 if ($this->_audioFormat != self::WAVE_FORMAT_PCM) {
533 $this->_factChunkSize = 4;
534 } else {
535 $this->_factChunkSize = 0;
536 }
537 } else {
538 $this->_factChunkSize = $factChunkSize;
539 }
540
541 $this->setChunkSize() // implicit setActualSize()
542 ->setDataOffset();
543
544 return $this;
545 }
546
547 public function getDataSize() {
548 return $this->_dataSize;
549 }
550
551 protected function setDataSize($dataSize = null) {
552 if (is_null($dataSize)) {
553 $this->_dataSize = strlen($this->_samples);
554 } else {
555 $this->_dataSize = $dataSize;
556 }
557
558 $this->setChunkSize() // implicit setActualSize()
559 ->setNumBlocks();
560 $this->_dataSize_valid = true;
561
562 return $this;
563 }
564
565 public function getDataOffset() {
566 return $this->_dataOffset;
567 }
568
569 protected function setDataOffset($dataOffset = null) {
570 if (is_null($dataOffset)) {
571 $this->_dataOffset = 8 + // "RIFF" header (ID + size)
572 4 + // "WAVE" chunk
573 8 + $this->_fmtChunkSize + // "fmt " subchunk
574 ($this->_factChunkSize > 0 ? 8 + $this->_factChunkSize : 0) + // "fact" subchunk
575 8; // "data" subchunk
576 } else {
577 $this->_dataOffset = $dataOffset;
578 }
579
580 return $this;
581 }
582
583 public function getAudioFormat() {
584 return $this->_audioFormat;
585 }
586
587 protected function setAudioFormat($audioFormat = null) {
588 if (is_null($audioFormat)) {
589 if (($this->_bitsPerSample <= 16 || $this->_bitsPerSample == 32)
590 && $this->_validBitsPerSample == $this->_bitsPerSample
591 && $this->_channelMask == self::SPEAKER_DEFAULT
592 && $this->_numChannels <= 2) {
593 if ($this->_bitsPerSample <= 16) {
594 $this->_audioFormat = self::WAVE_FORMAT_PCM;
595 } else {
596 $this->_audioFormat = self::WAVE_FORMAT_IEEE_FLOAT;
597 }
598 } else {
599 $this->_audioFormat = self::WAVE_FORMAT_EXTENSIBLE;
600 }
601 } else {
602 $this->_audioFormat = $audioFormat;
603 }
604
605 $this->setAudioSubFormat()
606 ->setFactChunkSize() // implicit setSize(), setActualSize(), setDataOffset()
607 ->setFmtExtendedSize(); // implicit setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
608
609 return $this;
610 }
611
612 public function getAudioSubFormat() {
614 }
615
616 protected function setAudioSubFormat($audioSubFormat = null) {
617 if (is_null($audioSubFormat)) {
618 if ($this->_bitsPerSample == 32) {
619 $this->_audioSubFormat = self::WAVE_SUBFORMAT_IEEE_FLOAT; // 32 bits are IEEE FLOAT in this class
620 } else {
621 $this->_audioSubFormat = self::WAVE_SUBFORMAT_PCM; // 8, 16 and 24 bits are PCM in this class
622 }
623 } else {
624 $this->_audioSubFormat = $audioSubFormat;
625 }
626
627 return $this;
628 }
629
630 public function getNumChannels() {
631 return $this->_numChannels;
632 }
633
634 public function setNumChannels($numChannels) {
635 if ($numChannels < 1 || $numChannels > self::MAX_CHANNEL) {
636 throw new WavFileException('Unsupported number of channels. Only up to ' . self::MAX_CHANNEL . ' channels are supported.');
637 } elseif ($this->_samples !== '') {
638 trigger_error('Wav already has sample data. Changing the number of channels does not convert and may corrupt the data.', E_USER_NOTICE);
639 }
640
641 $this->_numChannels = (int)$numChannels;
642
643 $this->setAudioFormat() // implicit setAudioSubFormat(), setFactChunkSize(), setFmtExtendedSize(), setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
644 ->setByteRate()
645 ->setBlockAlign(); // implicit setNumBlocks()
646
647 return $this;
648 }
649
650 public function getChannelMask() {
651 return $this->_channelMask;
652 }
653
654 public function setChannelMask($channelMask = self::SPEAKER_DEFAULT) {
655 if ($channelMask != 0) {
656 // count number of set bits - Hamming weight
657 $c = (int)$channelMask;
658 $n = 0;
659 while ($c > 0) {
660 $n += $c & 1;
661 $c >>= 1;
662 }
663 if ($n != $this->_numChannels || (((int)$channelMask | self::SPEAKER_ALL) != self::SPEAKER_ALL)) {
664 throw new WavFileException('Invalid channel mask. The number of channels does not match the number of locations in the mask.');
665 }
666 }
667
668 $this->_channelMask = (int)$channelMask;
669
670 $this->setAudioFormat(); // implicit setAudioSubFormat(), setFactChunkSize(), setFmtExtendedSize(), setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
671
672 return $this;
673 }
674
675 public function getSampleRate() {
676 return $this->_sampleRate;
677 }
678
679 public function setSampleRate($sampleRate) {
680 if ($sampleRate < 1 || $sampleRate > self::MAX_SAMPLERATE) {
681 throw new WavFileException('Invalid sample rate.');
682 } elseif ($this->_samples !== '') {
683 trigger_error('Wav already has sample data. Changing the sample rate does not convert the data and may yield undesired results.', E_USER_NOTICE);
684 }
685
686 $this->_sampleRate = (int)$sampleRate;
687
688 $this->setByteRate();
689
690 return $this;
691 }
692
693 public function getBitsPerSample() {
695 }
696
697 public function setBitsPerSample($bitsPerSample) {
698 if (!in_array($bitsPerSample, array(8, 16, 24, 32))) {
699 throw new WavFileException('Unsupported bits per sample. Only 8, 16, 24 and 32 bits are supported.');
700 } elseif ($this->_samples !== '') {
701 trigger_error('Wav already has sample data. Changing the bits per sample does not convert and may corrupt the data.', E_USER_NOTICE);
702 }
703
704 $this->_bitsPerSample = (int)$bitsPerSample;
705
706 $this->setValidBitsPerSample() // implicit setAudioFormat(), setAudioSubFormat(), setFmtChunkSize(), setFactChunkSize(), setSize(), setActualSize(), setDataOffset()
707 ->setByteRate()
708 ->setBlockAlign(); // implicit setNumBlocks()
709
710 return $this;
711 }
712
713 public function getValidBitsPerSample() {
715 }
716
717 protected function setValidBitsPerSample($validBitsPerSample = null) {
718 if (is_null($validBitsPerSample)) {
719 $this->_validBitsPerSample = $this->_bitsPerSample;
720 } else {
721 if ($validBitsPerSample < 1 || $validBitsPerSample > $this->_bitsPerSample) {
722 throw new WavFileException('ValidBitsPerSample cannot be greater than BitsPerSample.');
723 }
724 $this->_validBitsPerSample = (int)$validBitsPerSample;
725 }
726
727 $this->setAudioFormat(); // implicit setAudioSubFormat(), setFactChunkSize(), setFmtExtendedSize(), setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
728
729 return $this;
730 }
731
732 public function getBlockAlign() {
733 return $this->_blockAlign;
734 }
735
736 protected function setBlockAlign($blockAlign = null) {
737 if (is_null($blockAlign)) {
738 $this->_blockAlign = $this->_numChannels * $this->_bitsPerSample / 8;
739 } else {
740 $this->_blockAlign = $blockAlign;
741 }
742
743 $this->setNumBlocks();
744
745 return $this;
746 }
747
748 public function getNumBlocks()
749 {
750 return $this->_numBlocks;
751 }
752
753 protected function setNumBlocks($numBlocks = null) {
754 if (is_null($numBlocks)) {
755 $this->_numBlocks = (int)($this->_dataSize / $this->_blockAlign); // do not count incomplete sample blocks
756 } else {
757 $this->_numBlocks = $numBlocks;
758 }
759
760 return $this;
761 }
762
763 public function getByteRate() {
764 return $this->_byteRate;
765 }
766
767 protected function setByteRate($byteRate = null) {
768 if (is_null($byteRate)) {
769 $this->_byteRate = $this->_sampleRate * $this->_numChannels * $this->_bitsPerSample / 8;
770 } else {
771 $this->_byteRate = $byteRate;
772 }
773
774 return $this;
775 }
776
777 public function getSamples() {
778 return $this->_samples;
779 }
780
781 public function setSamples(&$samples = '') {
782 if (strlen($samples) % $this->_blockAlign != 0) {
783 throw new WavFileException('Incorrect samples size. Has to be a multiple of BlockAlign.');
784 }
785
786 $this->_samples = $samples;
787
788 $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
789
790 return $this;
791 }
792
793
794 /*%******************************************************************************************%*/
795 // Getters
796
797 public function getMinAmplitude()
798 {
799 if ($this->_bitsPerSample == 8) {
800 return 0;
801 } elseif ($this->_bitsPerSample == 32) {
802 return -1.0;
803 } else {
804 return -(1 << ($this->_bitsPerSample - 1));
805 }
806 }
807
808 public function getZeroAmplitude()
809 {
810 if ($this->_bitsPerSample == 8) {
811 return 0x80;
812 } elseif ($this->_bitsPerSample == 32) {
813 return 0.0;
814 } else {
815 return 0;
816 }
817 }
818
819 public function getMaxAmplitude()
820 {
821 if($this->_bitsPerSample == 8) {
822 return 0xFF;
823 } elseif($this->_bitsPerSample == 32) {
824 return 1.0;
825 } else {
826 return (1 << ($this->_bitsPerSample - 1)) - 1;
827 }
828 }
829
830
831 /*%******************************************************************************************%*/
832 // Wave file methods
833
840 public function makeHeader()
841 {
842 // reset and recalculate
843 $this->setAudioFormat(); // implicit setAudioSubFormat(), setFactChunkSize(), setFmtExtendedSize(), setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
844 $this->setNumBlocks();
845
846 // RIFF header
847 $header = pack('N', 0x52494646); // ChunkID - "RIFF"
848 $header .= pack('V', $this->getChunkSize()); // ChunkSize
849 $header .= pack('N', 0x57415645); // Format - "WAVE"
850
851 // "fmt " subchunk
852 $header .= pack('N', 0x666d7420); // SubchunkID - "fmt "
853 $header .= pack('V', $this->getFmtChunkSize()); // SubchunkSize
854 $header .= pack('v', $this->getAudioFormat()); // AudioFormat
855 $header .= pack('v', $this->getNumChannels()); // NumChannels
856 $header .= pack('V', $this->getSampleRate()); // SampleRate
857 $header .= pack('V', $this->getByteRate()); // ByteRate
858 $header .= pack('v', $this->getBlockAlign()); // BlockAlign
859 $header .= pack('v', $this->getBitsPerSample()); // BitsPerSample
860 if($this->getFmtExtendedSize() == 24) {
861 $header .= pack('v', 22); // extension size = 24 bytes, cbSize: 24 - 2 = 22 bytes
862 $header .= pack('v', $this->getValidBitsPerSample()); // ValidBitsPerSample
863 $header .= pack('V', $this->getChannelMask()); // ChannelMask
864 $header .= pack('H32', $this->getAudioSubFormat()); // SubFormat
865 } elseif ($this->getFmtExtendedSize() == 2) {
866 $header .= pack('v', 0); // extension size = 2 bytes, cbSize: 2 - 2 = 0 bytes
867 }
868
869 // "fact" subchunk
870 if ($this->getFactChunkSize() == 4) {
871 $header .= pack('N', 0x66616374); // SubchunkID - "fact"
872 $header .= pack('V', 4); // SubchunkSize
873 $header .= pack('V', $this->getNumBlocks()); // SampleLength (per channel)
874 }
875
876 return $header;
877 }
878
884 public function getDataSubchunk()
885 {
886 // check preconditions
887 if (!$this->_dataSize_valid) {
888 $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
889 }
890
891
892 // create subchunk
893 return pack('N', 0x64617461) . // SubchunkID - "data"
894 pack('V', $this->getDataSize()) . // SubchunkSize
895 $this->_samples . // Subchunk data
896 ($this->getDataSize() & 1 ? chr(0) : ''); // padding byte
897 }
898
905 public function save($filename)
906 {
907 $fp = @fopen($filename, 'w+b');
908 if (!is_resource($fp)) {
909 throw new WavFileException('Failed to open "' . $filename . '" for writing.');
910 }
911
912 fwrite($fp, $this->makeHeader());
913 fwrite($fp, $this->getDataSubchunk());
914 fclose($fp);
915
916 return $this;
917 }
918
927 public function openWav($filename, $readData = true)
928 {
929 // check preconditions
930 if (!file_exists($filename)) {
931 throw new WavFileException('Failed to open "' . $filename . '". File not found.');
932 } elseif (!is_readable($filename)) {
933 throw new WavFileException('Failed to open "' . $filename . '". File is not readable.');
934 } elseif (is_resource($this->_fp)) {
935 $this->closeWav();
936 }
937
938
939 // open the file
940 $this->_fp = @fopen($filename, 'rb');
941 if (!is_resource($this->_fp)) {
942 throw new WavFileException('Failed to open "' . $filename . '".');
943 }
944
945 // read the file
946 return $this->readWav($readData);
947 }
948
953 public function closeWav() {
954 if (is_resource($this->_fp)) fclose($this->_fp);
955
956 return $this;
957 }
958
967 public function setWavData(&$data, $free = true)
968 {
969 // check preconditions
970 if (is_resource($this->_fp)) $this->closeWav();
971
972
973 // open temporary stream in memory
974 $this->_fp = @fopen('php://memory', 'w+b');
975 if (!is_resource($this->_fp)) {
976 throw new WavFileException('Failed to open memory stream to write wav data. Use openWav() instead.');
977 }
978
979 // prepare stream
980 fwrite($this->_fp, $data);
981 rewind($this->_fp);
982
983 // free the passed data
984 if ($free) $data = null;
985
986 // read the stream like a file
987 return $this->readWav(true);
988 }
989
997 protected function readWav($readData = true)
998 {
999 if (!is_resource($this->_fp)) {
1000 throw new WavFileException('No wav file open. Use openWav() first.');
1001 }
1002
1003 try {
1004 $this->readWavHeader();
1005 } catch (WavFileException $ex) {
1006 $this->closeWav();
1007 throw $ex;
1008 }
1009
1010 if ($readData) return $this->readWavData();
1011
1012 return $this;
1013 }
1014
1022 protected function readWavHeader()
1023 {
1024 if (!is_resource($this->_fp)) {
1025 throw new WavFileException('No wav file open. Use openWav() first.');
1026 }
1027
1028 // get actual file size
1029 $stat = fstat($this->_fp);
1030 $actualSize = $stat['size'];
1031
1032 $this->_actualSize = $actualSize;
1033
1034
1035 // read the common header
1036 $header = fread($this->_fp, 36); // minimum size of the wav header
1037 if (strlen($header) < 36) {
1038 throw new WavFormatException('Not wav format. Header too short.', 1);
1039 }
1040
1041
1042 // check "RIFF" header
1043 $RIFF = unpack('NChunkID/VChunkSize/NFormat', $header);
1044
1045 if ($RIFF['ChunkID'] != 0x52494646) { // "RIFF"
1046 throw new WavFormatException('Not wav format. "RIFF" signature missing.', 2);
1047 }
1048
1049 if ($actualSize - 8 < $RIFF['ChunkSize']) {
1050 trigger_error('"RIFF" chunk size does not match actual file size. Found ' . $RIFF['ChunkSize'] . ', expected ' . ($actualSize - 8) . '.', E_USER_NOTICE);
1051 $RIFF['ChunkSize'] = $actualSize - 8;
1052 //throw new WavFormatException('"RIFF" chunk size does not match actual file size. Found ' . $RIFF['ChunkSize'] . ', expected ' . ($actualSize - 8) . '.', 3);
1053 }
1054
1055 if ($RIFF['Format'] != 0x57415645) { // "WAVE"
1056 throw new WavFormatException('Not wav format. "RIFF" chunk format is not "WAVE".', 4);
1057 }
1058
1059 $this->_chunkSize = $RIFF['ChunkSize'];
1060
1061
1062 // check common "fmt " subchunk
1063 $fmt = unpack('NSubchunkID/VSubchunkSize/vAudioFormat/vNumChannels/'
1064 .'VSampleRate/VByteRate/vBlockAlign/vBitsPerSample',
1065 substr($header, 12));
1066
1067 if ($fmt['SubchunkID'] != 0x666d7420) { // "fmt "
1068 throw new WavFormatException('Bad wav header. Expected "fmt " subchunk.', 11);
1069 }
1070
1071 if ($fmt['SubchunkSize'] < 16) {
1072 throw new WavFormatException('Bad "fmt " subchunk size.', 12);
1073 }
1074
1075 if ( $fmt['AudioFormat'] != self::WAVE_FORMAT_PCM
1076 && $fmt['AudioFormat'] != self::WAVE_FORMAT_IEEE_FLOAT
1077 && $fmt['AudioFormat'] != self::WAVE_FORMAT_EXTENSIBLE)
1078 {
1079 throw new WavFormatException('Unsupported audio format. Only PCM or IEEE FLOAT (EXTENSIBLE) audio is supported.', 13);
1080 }
1081
1082 if ($fmt['NumChannels'] < 1 || $fmt['NumChannels'] > self::MAX_CHANNEL) {
1083 throw new WavFormatException('Invalid number of channels in "fmt " subchunk.', 14);
1084 }
1085
1086 if ($fmt['SampleRate'] < 1 || $fmt['SampleRate'] > self::MAX_SAMPLERATE) {
1087 throw new WavFormatException('Invalid sample rate in "fmt " subchunk.', 15);
1088 }
1089
1090 if ( ($fmt['AudioFormat'] == self::WAVE_FORMAT_PCM && !in_array($fmt['BitsPerSample'], array(8, 16, 24)))
1091 || ($fmt['AudioFormat'] == self::WAVE_FORMAT_IEEE_FLOAT && $fmt['BitsPerSample'] != 32)
1092 || ($fmt['AudioFormat'] == self::WAVE_FORMAT_EXTENSIBLE && !in_array($fmt['BitsPerSample'], array(8, 16, 24, 32))))
1093 {
1094 throw new WavFormatException('Only 8, 16 and 24-bit PCM and 32-bit IEEE FLOAT (EXTENSIBLE) audio is supported.', 16);
1095 }
1096
1097 $blockAlign = $fmt['NumChannels'] * $fmt['BitsPerSample'] / 8;
1098 if ($blockAlign != $fmt['BlockAlign']) {
1099 trigger_error('Invalid block align in "fmt " subchunk. Found ' . $fmt['BlockAlign'] . ', expected ' . $blockAlign . '.', E_USER_NOTICE);
1100 $fmt['BlockAlign'] = $blockAlign;
1101 //throw new WavFormatException('Invalid block align in "fmt " subchunk. Found ' . $fmt['BlockAlign'] . ', expected ' . $blockAlign . '.', 17);
1102 }
1103
1104 $byteRate = $fmt['SampleRate'] * $blockAlign;
1105 if ($byteRate != $fmt['ByteRate']) {
1106 trigger_error('Invalid average byte rate in "fmt " subchunk. Found ' . $fmt['ByteRate'] . ', expected ' . $byteRate . '.', E_USER_NOTICE);
1107 $fmt['ByteRate'] = $byteRate;
1108 //throw new WavFormatException('Invalid average byte rate in "fmt " subchunk. Found ' . $fmt['ByteRate'] . ', expected ' . $byteRate . '.', 18);
1109 }
1110
1111 $this->_fmtChunkSize = $fmt['SubchunkSize'];
1112 $this->_audioFormat = $fmt['AudioFormat'];
1113 $this->_numChannels = $fmt['NumChannels'];
1114 $this->_sampleRate = $fmt['SampleRate'];
1115 $this->_byteRate = $fmt['ByteRate'];
1116 $this->_blockAlign = $fmt['BlockAlign'];
1117 $this->_bitsPerSample = $fmt['BitsPerSample'];
1118
1119
1120 // read extended "fmt " subchunk data
1121 $extendedFmt = '';
1122 if ($fmt['SubchunkSize'] > 16) {
1123 // possibly handle malformed subchunk without a padding byte
1124 $extendedFmt = fread($this->_fp, $fmt['SubchunkSize'] - 16 + ($fmt['SubchunkSize'] & 1)); // also read padding byte
1125 if (strlen($extendedFmt) < $fmt['SubchunkSize'] - 16) {
1126 throw new WavFormatException('Not wav format. Header too short.', 1);
1127 }
1128 }
1129
1130
1131 // check extended "fmt " for EXTENSIBLE Audio Format
1132 if ($fmt['AudioFormat'] == self::WAVE_FORMAT_EXTENSIBLE) {
1133 if (strlen($extendedFmt) < 24) {
1134 throw new WavFormatException('Invalid EXTENSIBLE "fmt " subchunk size. Found ' . $fmt['SubchunkSize'] . ', expected at least 40.', 19);
1135 }
1136
1137 $extensibleFmt = unpack('vSize/vValidBitsPerSample/VChannelMask/H32SubFormat', substr($extendedFmt, 0, 24));
1138
1139 if ( $extensibleFmt['SubFormat'] != self::WAVE_SUBFORMAT_PCM
1140 && $extensibleFmt['SubFormat'] != self::WAVE_SUBFORMAT_IEEE_FLOAT)
1141 {
1142 throw new WavFormatException('Unsupported audio format. Only PCM or IEEE FLOAT (EXTENSIBLE) audio is supported.', 13);
1143 }
1144
1145 if ( ($extensibleFmt['SubFormat'] == self::WAVE_SUBFORMAT_PCM && !in_array($fmt['BitsPerSample'], array(8, 16, 24)))
1146 || ($extensibleFmt['SubFormat'] == self::WAVE_SUBFORMAT_IEEE_FLOAT && $fmt['BitsPerSample'] != 32))
1147 {
1148 throw new WavFormatException('Only 8, 16 and 24-bit PCM and 32-bit IEEE FLOAT (EXTENSIBLE) audio is supported.', 16);
1149 }
1150
1151 if ($extensibleFmt['Size'] != 22) {
1152 trigger_error('Invaid extension size in EXTENSIBLE "fmt " subchunk.', E_USER_NOTICE);
1153 $extensibleFmt['Size'] = 22;
1154 //throw new WavFormatException('Invaid extension size in EXTENSIBLE "fmt " subchunk.', 20);
1155 }
1156
1157 if ($extensibleFmt['ValidBitsPerSample'] != $fmt['BitsPerSample']) {
1158 trigger_error('Invaid or unsupported valid bits per sample in EXTENSIBLE "fmt " subchunk.', E_USER_NOTICE);
1159 $extensibleFmt['ValidBitsPerSample'] = $fmt['BitsPerSample'];
1160 //throw new WavFormatException('Invaid or unsupported valid bits per sample in EXTENSIBLE "fmt " subchunk.', 21);
1161 }
1162
1163 if ($extensibleFmt['ChannelMask'] != 0) {
1164 // count number of set bits - Hamming weight
1165 $c = (int)$extensibleFmt['ChannelMask'];
1166 $n = 0;
1167 while ($c > 0) {
1168 $n += $c & 1;
1169 $c >>= 1;
1170 }
1171 if ($n != $fmt['NumChannels'] || (((int)$extensibleFmt['ChannelMask'] | self::SPEAKER_ALL) != self::SPEAKER_ALL)) {
1172 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);
1173 $extensibleFmt['ChannelMask'] = 0;
1174 //throw new WavFormatException('Invalid channel mask in EXTENSIBLE "fmt " subchunk. The number of channels does not match the number of locations in the mask.', 22);
1175 }
1176 }
1177
1178 $this->_fmtExtendedSize = strlen($extendedFmt);
1179 $this->_validBitsPerSample = $extensibleFmt['ValidBitsPerSample'];
1180 $this->_channelMask = $extensibleFmt['ChannelMask'];
1181 $this->_audioSubFormat = $extensibleFmt['SubFormat'];
1182
1183 } else {
1184 $this->_fmtExtendedSize = strlen($extendedFmt);
1185 $this->_validBitsPerSample = $fmt['BitsPerSample'];
1186 $this->_channelMask = 0;
1187 $this->_audioSubFormat = null;
1188 }
1189
1190
1191 // read additional subchunks until "data" subchunk is found
1192 $factSubchunk = array();
1193 $dataSubchunk = array();
1194
1195 while (!feof($this->_fp)) {
1196 $subchunkHeader = fread($this->_fp, 8);
1197 if (strlen($subchunkHeader) < 8) {
1198 throw new WavFormatException('Missing "data" subchunk.', 101);
1199 }
1200
1201 $subchunk = unpack('NSubchunkID/VSubchunkSize', $subchunkHeader);
1202
1203 if ($subchunk['SubchunkID'] == 0x66616374) { // "fact"
1204 // possibly handle malformed subchunk without a padding byte
1205 $subchunkData = fread($this->_fp, $subchunk['SubchunkSize'] + ($subchunk['SubchunkSize'] & 1)); // also read padding byte
1206 if (strlen($subchunkData) < 4) {
1207 throw new WavFormatException('Invalid "fact" subchunk.', 102);
1208 }
1209
1210 $factParams = unpack('VSampleLength', substr($subchunkData, 0, 4));
1211 $factSubchunk = array_merge($subchunk, $factParams);
1212
1213 } elseif ($subchunk['SubchunkID'] == 0x64617461) { // "data"
1214 $dataSubchunk = $subchunk;
1215
1216 break;
1217
1218 } elseif ($subchunk['SubchunkID'] == 0x7761766C) { // "wavl"
1219 throw new WavFormatException('Wave List Chunk ("wavl" subchunk) is not supported.', 106);
1220 } else {
1221 // skip all other (unknown) subchunks
1222 // possibly handle malformed subchunk without a padding byte
1223 if ( $subchunk['SubchunkSize'] < 0
1224 || fseek($this->_fp, $subchunk['SubchunkSize'] + ($subchunk['SubchunkSize'] & 1), SEEK_CUR) !== 0) { // also skip padding byte
1225 throw new WavFormatException('Invalid subchunk (0x' . dechex($subchunk['SubchunkID']) . ') encountered.', 103);
1226 }
1227 }
1228 }
1229
1230 if (empty($dataSubchunk)) {
1231 throw new WavFormatException('Missing "data" subchunk.', 101);
1232 }
1233
1234
1235 // check "data" subchunk
1236 $dataOffset = ftell($this->_fp);
1237 if ($dataSubchunk['SubchunkSize'] < 0 || $actualSize - $dataOffset < $dataSubchunk['SubchunkSize']) {
1238 trigger_error('Invalid "data" subchunk size.', E_USER_NOTICE);
1239 $dataSubchunk['SubchunkSize'] = $actualSize - $dataOffset;
1240 //throw new WavFormatException('Invalid "data" subchunk size.', 104);
1241 }
1242
1243 $this->_dataOffset = $dataOffset;
1244 $this->_dataSize = $dataSubchunk['SubchunkSize'];
1245 $this->_dataSize_fp = $dataSubchunk['SubchunkSize'];
1246 $this->_dataSize_valid = false;
1247 $this->_samples = '';
1248
1249
1250 // check "fact" subchunk
1251 $numBlocks = (int)($dataSubchunk['SubchunkSize'] / $fmt['BlockAlign']);
1252
1253 if (empty($factSubchunk)) { // construct fake "fact" subchunk
1254 $factSubchunk = array('SubchunkSize' => 0, 'SampleLength' => $numBlocks);
1255 }
1256
1257 if ($factSubchunk['SampleLength'] != $numBlocks) {
1258 trigger_error('Invalid sample length in "fact" subchunk.', E_USER_NOTICE);
1259 $factSubchunk['SampleLength'] = $numBlocks;
1260 //throw new WavFormatException('Invalid sample length in "fact" subchunk.', 105);
1261 }
1262
1263 $this->_factChunkSize = $factSubchunk['SubchunkSize'];
1264 $this->_numBlocks = $factSubchunk['SampleLength'];
1265
1266
1267 return $this;
1268
1269 }
1270
1278 public function readWavData($dataOffset = 0, $dataSize = null)
1279 {
1280 // check preconditions
1281 if (!is_resource($this->_fp)) {
1282 throw new WavFileException('No wav file open. Use openWav() first.');
1283 }
1284
1285 if ($dataOffset < 0 || $dataOffset % $this->getBlockAlign() > 0) {
1286 throw new WavFileException('Invalid data offset. Has to be a multiple of BlockAlign.');
1287 }
1288
1289 if (is_null($dataSize)) {
1290 $dataSize = $this->_dataSize_fp - ($this->_dataSize_fp % $this->getBlockAlign()); // only read complete blocks
1291 } elseif ($dataSize < 0 || $dataSize % $this->getBlockAlign() > 0) {
1292 throw new WavFileException('Invalid data size to read. Has to be a multiple of BlockAlign.');
1293 }
1294
1295
1296 // skip offset
1297 if ($dataOffset > 0 && fseek($this->_fp, $dataOffset, SEEK_CUR) !== 0) {
1298 throw new WavFileException('Seeking to data offset failed.');
1299 }
1300
1301 // read data
1302 $this->_samples .= fread($this->_fp, $dataSize); // allow appending
1303 $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1304
1305 // close file or memory stream
1306 return $this->closeWav();
1307 }
1308
1309
1310 /*%******************************************************************************************%*/
1311 // Sample manipulation methods
1312
1319 public function getSampleBlock($blockNum)
1320 {
1321 // check preconditions
1322 if (!$this->_dataSize_valid) {
1323 $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1324 }
1325
1326 $offset = $blockNum * $this->_blockAlign;
1327 if ($offset + $this->_blockAlign > $this->_dataSize || $offset < 0) {
1328 return null;
1329 }
1330
1331
1332 // read data
1333 return substr($this->_samples, $offset, $this->_blockAlign);
1334 }
1335
1344 public function setSampleBlock($sampleBlock, $blockNum)
1345 {
1346 // check preconditions
1347 $blockAlign = $this->_blockAlign;
1348 if (!isset($sampleBlock[$blockAlign - 1]) || isset($sampleBlock[$blockAlign])) { // faster than: if (strlen($sampleBlock) != $blockAlign)
1349 throw new WavFileException('Incorrect sample block size. Got ' . strlen($sampleBlock) . ', expected ' . $blockAlign . '.');
1350 }
1351
1352 if (!$this->_dataSize_valid) {
1353 $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1354 }
1355
1356 $numBlocks = (int)($this->_dataSize / $blockAlign);
1357 $offset = $blockNum * $blockAlign;
1358 if ($blockNum > $numBlocks || $blockNum < 0) { // allow appending
1359 throw new WavFileException('Sample block number is out of range.');
1360 }
1361
1362
1363 // replace or append data
1364 if ($blockNum == $numBlocks) {
1365 // append
1366 $this->_samples .= $sampleBlock;
1367 $this->_dataSize += $blockAlign;
1368 $this->_chunkSize += $blockAlign;
1369 $this->_actualSize += $blockAlign;
1370 $this->_numBlocks++;
1371 } else {
1372 // replace
1373 for ($i = 0; $i < $blockAlign; ++$i) {
1374 $this->_samples[$offset + $i] = $sampleBlock[$i];
1375 }
1376 }
1377
1378 return $this;
1379 }
1380
1389 public function getSampleValue($blockNum, $channelNum)
1390 {
1391 // check preconditions
1392 if ($channelNum < 1 || $channelNum > $this->_numChannels) {
1393 throw new WavFileException('Channel number is out of range.');
1394 }
1395
1396 if (!$this->_dataSize_valid) {
1397 $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1398 }
1399
1400 $sampleBytes = $this->_bitsPerSample / 8;
1401 $offset = $blockNum * $this->_blockAlign + ($channelNum - 1) * $sampleBytes;
1402 if ($offset + $sampleBytes > $this->_dataSize || $offset < 0) {
1403 return null;
1404 }
1405
1406 // read binary value
1407 $sampleBinary = substr($this->_samples, $offset, $sampleBytes);
1408
1409 // convert binary to value
1410 switch ($this->_bitsPerSample) {
1411 case 8:
1412 // unsigned char
1413 return (float)((ord($sampleBinary) - 0x80) / 0x80);
1414
1415 case 16:
1416 // signed short, little endian
1417 $data = unpack('v', $sampleBinary);
1418 $sample = $data[1];
1419 if ($sample >= 0x8000) {
1420 $sample -= 0x10000;
1421 }
1422 return (float)($sample / 0x8000);
1423
1424 case 24:
1425 // 3 byte packed signed integer, little endian
1426 $data = unpack('C3', $sampleBinary);
1427 $sample = $data[1] | ($data[2] << 8) | ($data[3] << 16);
1428 if ($sample >= 0x800000) {
1429 $sample -= 0x1000000;
1430 }
1431 return (float)($sample / 0x800000);
1432
1433 case 32:
1434 // 32-bit float
1435 $data = unpack('f', $sampleBinary);
1436 return (float)$data[1];
1437
1438 default:
1439 return null;
1440 }
1441 }
1442
1453 public function setSampleValue($sampleFloat, $blockNum, $channelNum)
1454 {
1455 // check preconditions
1456 if ($channelNum < 1 || $channelNum > $this->_numChannels) {
1457 throw new WavFileException('Channel number is out of range.');
1458 }
1459
1460 if (!$this->_dataSize_valid) {
1461 $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1462 }
1463
1464 $dataSize = $this->_dataSize;
1465 $bitsPerSample = $this->_bitsPerSample;
1466 $sampleBytes = $bitsPerSample / 8;
1467 $offset = $blockNum * $this->_blockAlign + ($channelNum - 1) * $sampleBytes;
1468 if (($offset + $sampleBytes > $dataSize && $offset != $dataSize) || $offset < 0) { // allow appending
1469 throw new WavFileException('Sample block or channel number is out of range.');
1470 }
1471
1472
1473 // convert to value, quantize and clip
1474 if ($bitsPerSample == 32) {
1475 $sample = $sampleFloat < -1.0 ? -1.0 : ($sampleFloat > 1.0 ? 1.0 : $sampleFloat);
1476 } else {
1477 $p = 1 << ($bitsPerSample - 1); // 2 to the power of _bitsPerSample divided by 2
1478
1479 // project and quantize (round) float to integer values
1480 $sample = $sampleFloat < 0 ? (int)($sampleFloat * $p - 0.5) : (int)($sampleFloat * $p + 0.5);
1481
1482 // clip if necessary to [-$p, $p - 1]
1483 if ($sample < -$p) {
1484 $sample = -$p;
1485 } elseif ($sample > $p - 1) {
1486 $sample = $p - 1;
1487 }
1488 }
1489
1490 // convert to binary
1491 switch ($bitsPerSample) {
1492 case 8:
1493 // unsigned char
1494 $sampleBinary = chr($sample + 0x80);
1495 break;
1496
1497 case 16:
1498 // signed short, little endian
1499 if ($sample < 0) {
1500 $sample += 0x10000;
1501 }
1502 $sampleBinary = pack('v', $sample);
1503 break;
1504
1505 case 24:
1506 // 3 byte packed signed integer, little endian
1507 if ($sample < 0) {
1508 $sample += 0x1000000;
1509 }
1510 $sampleBinary = pack('C3', $sample & 0xff, ($sample >> 8) & 0xff, ($sample >> 16) & 0xff);
1511 break;
1512
1513 case 32:
1514 // 32-bit float
1515 $sampleBinary = pack('f', $sample);
1516 break;
1517
1518 default:
1519 $sampleBinary = null;
1520 $sampleBytes = 0;
1521 break;
1522 }
1523
1524 // replace or append data
1525 if ($offset == $dataSize) {
1526 // append
1527 $this->_samples .= $sampleBinary;
1528 $this->_dataSize += $sampleBytes;
1529 $this->_chunkSize += $sampleBytes;
1530 $this->_actualSize += $sampleBytes;
1531 $this->_numBlocks = (int)($this->_dataSize / $this->_blockAlign);
1532 } else {
1533 // replace
1534 for ($i = 0; $i < $sampleBytes; ++$i) {
1535 $this->_samples{$offset + $i} = $sampleBinary{$i};
1536 }
1537 }
1538
1539 return $this;
1540 }
1541
1542
1543 /*%******************************************************************************************%*/
1544 // Audio processing methods
1545
1571 public function filter($filters, $blockOffset = 0, $numBlocks = null)
1572 {
1573 // check preconditions
1574 $totalBlocks = $this->getNumBlocks();
1575 $numChannels = $this->getNumChannels();
1576 if (is_null($numBlocks)) $numBlocks = $totalBlocks - $blockOffset;
1577
1578 if (!is_array($filters) || empty($filters) || $blockOffset < 0 || $blockOffset > $totalBlocks || $numBlocks <= 0) {
1579 // nothing to do
1580 return $this;
1581 }
1582
1583 // check filtes
1584 $filter_mix = false;
1585 if (array_key_exists(self::FILTER_MIX, $filters)) {
1586 if (!is_array($filters[self::FILTER_MIX])) {
1587 // assume the 'wav' parameter
1588 $filters[self::FILTER_MIX] = array('wav' => $filters[self::FILTER_MIX]);
1589 }
1590
1591 $mix_wav = @$filters[self::FILTER_MIX]['wav'];
1592 if (!($mix_wav instanceof WavFile)) {
1593 throw new WavFileException("WavFile to mix is missing or invalid.");
1594 } elseif ($mix_wav->getSampleRate() != $this->getSampleRate()) {
1595 throw new WavFileException("Sample rate of WavFile to mix does not match.");
1596 } else if ($mix_wav->getNumChannels() != $this->getNumChannels()) {
1597 throw new WavFileException("Number of channels of WavFile to mix does not match.");
1598 }
1599
1600 $mix_loop = @$filters[self::FILTER_MIX]['loop'];
1601 if (is_null($mix_loop)) $mix_loop = false;
1602
1603 $mix_blockOffset = @$filters[self::FILTER_MIX]['blockOffset'];
1604 if (is_null($mix_blockOffset)) $mix_blockOffset = 0;
1605
1606 $mix_totalBlocks = $mix_wav->getNumBlocks();
1607 $mix_numBlocks = @$filters[self::FILTER_MIX]['numBlocks'];
1608 if (is_null($mix_numBlocks)) $mix_numBlocks = $mix_loop ? $mix_totalBlocks : $mix_totalBlocks - $mix_blockOffset;
1609 $mix_maxBlock = min($mix_blockOffset + $mix_numBlocks, $mix_totalBlocks);
1610
1611 $filter_mix = true;
1612 }
1613
1614 $filter_normalize = false;
1615 if (array_key_exists(self::FILTER_NORMALIZE, $filters)) {
1616 $normalize_threshold = @$filters[self::FILTER_NORMALIZE];
1617
1618 if (!is_null($normalize_threshold) && abs($normalize_threshold) != 1) $filter_normalize = true;
1619 }
1620
1621 $filter_degrade = false;
1622 if (array_key_exists(self::FILTER_DEGRADE, $filters)) {
1623 $degrade_quality = @$filters[self::FILTER_DEGRADE];
1624 if (is_null($degrade_quality)) $degrade_quality = 1;
1625
1626 if ($degrade_quality >= 0 && $degrade_quality < 1) $filter_degrade = true;
1627 }
1628
1629
1630 // loop through all sample blocks
1631 for ($block = 0; $block < $numBlocks; ++$block) {
1632 // loop through all channels
1633 for ($channel = 1; $channel <= $numChannels; ++$channel) {
1634 // read current sample
1635 $currentBlock = $blockOffset + $block;
1636 $sampleFloat = $this->getSampleValue($currentBlock, $channel);
1637
1638
1639 /************* MIX FILTER ***********************/
1640 if ($filter_mix) {
1641 if ($mix_loop) {
1642 $mixBlock = ($mix_blockOffset + ($block % $mix_numBlocks)) % $mix_totalBlocks;
1643 } else {
1644 $mixBlock = $mix_blockOffset + $block;
1645 }
1646
1647 if ($mixBlock < $mix_maxBlock) {
1648 $sampleFloat += $mix_wav->getSampleValue($mixBlock, $channel);
1649 }
1650 }
1651
1652 /************* NORMALIZE FILTER *******************/
1653 if ($filter_normalize) {
1654 $sampleFloat = $this->normalizeSample($sampleFloat, $normalize_threshold);
1655 }
1656
1657 /************* DEGRADE FILTER *******************/
1658 if ($filter_degrade) {
1659 $sampleFloat += rand(1000000 * ($degrade_quality - 1), 1000000 * (1 - $degrade_quality)) / 1000000;
1660 }
1661
1662
1663 // write current sample
1664 $this->setSampleValue($sampleFloat, $currentBlock, $channel);
1665 }
1666 }
1667
1668 return $this;
1669 }
1670
1678 public function appendWav(WavFile $wav) {
1679 // basic checks
1680 if ($wav->getSampleRate() != $this->getSampleRate()) {
1681 throw new WavFileException("Sample rate for wav files do not match.");
1682 } else if ($wav->getBitsPerSample() != $this->getBitsPerSample()) {
1683 throw new WavFileException("Bits per sample for wav files do not match.");
1684 } else if ($wav->getNumChannels() != $this->getNumChannels()) {
1685 throw new WavFileException("Number of channels for wav files do not match.");
1686 }
1687
1688 $this->_samples .= $wav->_samples;
1689 $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1690
1691 return $this;
1692 }
1693
1702 public function mergeWav(WavFile $wav, $normalizeThreshold = null) {
1703 return $this->filter(array(
1704 WavFile::FILTER_MIX => $wav,
1705 WavFile::FILTER_NORMALIZE => $normalizeThreshold
1706 ));
1707 }
1708
1714 public function insertSilence($duration = 1.0)
1715 {
1716 $numSamples = (int)($this->getSampleRate() * abs($duration));
1717 $numChannels = $this->getNumChannels();
1718
1719 $data = str_repeat(self::packSample($this->getZeroAmplitude(), $this->getBitsPerSample()), $numSamples * $numChannels);
1720 if ($duration >= 0) {
1721 $this->_samples .= $data;
1722 } else {
1723 $this->_samples = $data . $this->_samples;
1724 }
1725
1726 $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1727
1728 return $this;
1729 }
1730
1736 public function degrade($quality = 1.0)
1737 {
1738 return $this->filter(self::FILTER_DEGRADE, array(
1739 WavFile::FILTER_DEGRADE => $quality
1740 ));
1741 }
1742
1749 public function generateNoise($duration = 1.0, $percent = 100)
1750 {
1751 $numChannels = $this->getNumChannels();
1752 $numSamples = $this->getSampleRate() * $duration;
1753 $minAmp = $this->getMinAmplitude();
1754 $maxAmp = $this->getMaxAmplitude();
1755 $bitDepth = $this->getBitsPerSample();
1756
1757 for ($s = 0; $s < $numSamples; ++$s) {
1758 if ($bitDepth == 32) {
1759 $val = rand(-$percent * 10000, $percent * 10000) / 1000000;
1760 } else {
1761 $val = rand($minAmp, $maxAmp);
1762 $val = (int)($val * $percent / 100);
1763 }
1764
1765 $this->_samples .= str_repeat(self::packSample($val, $bitDepth), $numChannels);
1766 }
1767
1768 $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1769
1770 return $this;
1771 }
1772
1779 public function convertBitsPerSample($bitsPerSample) {
1780 if ($this->getBitsPerSample() == $bitsPerSample) {
1781 return $this;
1782 }
1783
1784 $tempWav = new WavFile($this->getNumChannels(), $this->getSampleRate(), $bitsPerSample);
1785 $tempWav->filter(
1786 array(self::FILTER_MIX => $this),
1787 0,
1788 $this->getNumBlocks()
1789 );
1790
1791 $this->setSamples() // implicit setDataSize(), setSize(), setActualSize(), setNumBlocks()
1792 ->setBitsPerSample($bitsPerSample); // implicit setValidBitsPerSample(), setAudioFormat(), setAudioSubFormat(), setFmtChunkSize(), setFactChunkSize(), setSize(), setActualSize(), setDataOffset(), setByteRate(), setBlockAlign(), setNumBlocks()
1793 $this->_samples = $tempWav->_samples;
1794 $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1795
1796 return $this;
1797 }
1798
1799
1800 /*%******************************************************************************************%*/
1801 // Miscellaneous methods
1802
1806 public function displayInfo()
1807 {
1808 $s = "File Size: %u\n"
1809 ."Chunk Size: %u\n"
1810 ."fmt Subchunk Size: %u\n"
1811 ."Extended fmt Size: %u\n"
1812 ."fact Subchunk Size: %u\n"
1813 ."Data Offset: %u\n"
1814 ."Data Size: %u\n"
1815 ."Audio Format: %s\n"
1816 ."Audio SubFormat: %s\n"
1817 ."Channels: %u\n"
1818 ."Channel Mask: 0x%s\n"
1819 ."Sample Rate: %u\n"
1820 ."Bits Per Sample: %u\n"
1821 ."Valid Bits Per Sample: %u\n"
1822 ."Sample Block Size: %u\n"
1823 ."Number of Sample Blocks: %u\n"
1824 ."Byte Rate: %uBps\n";
1825
1826 $s = sprintf($s, $this->getActualSize(),
1827 $this->getChunkSize(),
1828 $this->getFmtChunkSize(),
1829 $this->getFmtExtendedSize(),
1830 $this->getFactChunkSize(),
1831 $this->getDataOffset(),
1832 $this->getDataSize(),
1833 $this->getAudioFormat() == self::WAVE_FORMAT_PCM ? 'PCM' : ($this->getAudioFormat() == self::WAVE_FORMAT_IEEE_FLOAT ? 'IEEE FLOAT' : 'EXTENSIBLE'),
1834 $this->getAudioSubFormat() == self::WAVE_SUBFORMAT_PCM ? 'PCM' : 'IEEE FLOAT',
1835 $this->getNumChannels(),
1836 dechex($this->getChannelMask()),
1837 $this->getSampleRate(),
1838 $this->getBitsPerSample(),
1839 $this->getValidBitsPerSample(),
1840 $this->getBlockAlign(),
1841 $this->getNumBlocks(),
1842 $this->getByteRate());
1843
1844 if (php_sapi_name() == 'cli') {
1845 return $s;
1846 } else {
1847 return nl2br($s);
1848 }
1849 }
1850}
1851
1852
1853/*%******************************************************************************************%*/
1854// Exceptions
1855
1859class WavFileException extends Exception {}
1860
$n
Definition: RandomTest.php:80
$filename
Definition: buildRTE.php:89
WavFileException indicates an illegal state or argument in this class.
Definition: WavFile.php:1859
getMinAmplitude()
Definition: WavFile.php:797
const SPEAKER_TOP_BACK_CENTER
Definition: WavFile.php:107
getChunkSize()
Definition: WavFile.php:467
getSampleBlock($blockNum)
Return a single sample block from the file.
Definition: WavFile.php:1319
static unpackSampleBlock($sampleBlock, $bitDepth, $numChannels=null)
Unpacks a binary sample block to numeric values.
Definition: WavFile.php:370
setAudioSubFormat($audioSubFormat=null)
Definition: WavFile.php:616
openWav($filename, $readData=true)
Reads a wav header and data from a file.
Definition: WavFile.php:927
getChannelMask()
Definition: WavFile.php:650
getDataSize()
Definition: WavFile.php:547
__destruct()
Definition: WavFile.php:256
static unpackSample($sampleBinary, $bitDepth=null)
Unpacks a single binary sample to numeric value.
Definition: WavFile.php:286
const SPEAKER_TOP_FRONT_CENTER
Definition: WavFile.php:104
getAudioSubFormat()
Definition: WavFile.php:612
const MAX_CHANNEL
Definition: WavFile.php:84
const WAVE_SUBFORMAT_PCM
Definition: WavFile.php:121
const FILTER_DEGRADE
Definition: WavFile.php:81
static packSampleBlock($samples, $bitDepth)
Packs an array of numeric channel samples to a binary sample block.
Definition: WavFile.php:392
const WAVE_FORMAT_EXTENSIBLE
Definition: WavFile.php:118
setDataSize($dataSize=null)
Definition: WavFile.php:551
makeHeader()
Construct a wav header from this object.
Definition: WavFile.php:840
setSampleBlock($sampleBlock, $blockNum)
Set a single sample block.
Definition: WavFile.php:1344
setChunkSize($chunkSize=null)
Definition: WavFile.php:471
setFmtChunkSize($fmtChunkSize=null)
Definition: WavFile.php:491
getMaxAmplitude()
Definition: WavFile.php:819
$_fmtChunkSize
Definition: WavFile.php:147
setActualSize($actualSize=null)
Definition: WavFile.php:457
mergeWav(WavFile $wav, $normalizeThreshold=null)
Mix 2 wav files together.
Definition: WavFile.php:1702
const FILTER_MIX
Definition: WavFile.php:75
setNumBlocks($numBlocks=null)
Definition: WavFile.php:753
setChannelMask($channelMask=self::SPEAKER_DEFAULT)
Definition: WavFile.php:654
convertBitsPerSample($bitsPerSample)
Convert sample data to different bits per sample.
Definition: WavFile.php:1779
const SPEAKER_FRONT_LEFT
Definition: WavFile.php:91
degrade($quality=1.0)
Degrade the quality of the wav file by introducing random noise.
Definition: WavFile.php:1736
$_numBlocks
Definition: WavFile.php:192
__clone()
Definition: WavFile.php:260
getFmtExtendedSize()
Definition: WavFile.php:504
save($filename)
Save the wav data to a file.
Definition: WavFile.php:905
$_bitsPerSample
Definition: WavFile.php:183
static normalizeSample($sampleFloat, $threshold)
Normalizes a float audio sample.
Definition: WavFile.php:419
setFactChunkSize($factChunkSize=null)
Definition: WavFile.php:530
$_actualSize
Definition: WavFile.php:141
$_dataOffset
Definition: WavFile.php:165
$_dataSize_fp
Definition: WavFile.php:159
$_blockAlign
Definition: WavFile.php:189
$_validBitsPerSample
Definition: WavFile.php:186
insertSilence($duration=1.0)
Add silence to the wav file.
Definition: WavFile.php:1714
filter($filters, $blockOffset=0, $numBlocks=null)
Run samples through audio processing filters.
Definition: WavFile.php:1571
setFmtExtendedSize($fmtExtendedSize=null)
Definition: WavFile.php:508
setValidBitsPerSample($validBitsPerSample=null)
Definition: WavFile.php:717
const SPEAKER_TOP_CENTER
Definition: WavFile.php:102
getSampleValue($blockNum, $channelNum)
Get a float sample value for a specific sample block and channel number.
Definition: WavFile.php:1389
const WAVE_FORMAT_PCM
Definition: WavFile.php:112
const SPEAKER_DEFAULT
Channel Locations for ChannelMask.
Definition: WavFile.php:90
getSampleRate()
Definition: WavFile.php:675
getDataOffset()
Definition: WavFile.php:565
getAudioFormat()
Definition: WavFile.php:583
$_byteRate
Definition: WavFile.php:195
setSampleValue($sampleFloat, $blockNum, $channelNum)
Sets a float sample value for a specific sample block number and channel.
Definition: WavFile.php:1453
const SPEAKER_TOP_BACK_RIGHT
Definition: WavFile.php:108
displayInfo()
Output information about the wav object.
Definition: WavFile.php:1806
const SPEAKER_TOP_BACK_LEFT
Definition: WavFile.php:106
getDataSubchunk()
Construct wav DATA chunk.
Definition: WavFile.php:884
const SPEAKER_SIDE_LEFT
Definition: WavFile.php:100
const SPEAKER_BACK_RIGHT
Definition: WavFile.php:96
getBitsPerSample()
Definition: WavFile.php:693
const SPEAKER_SIDE_RIGHT
Definition: WavFile.php:101
$_dataSize
Definition: WavFile.php:156
const SPEAKER_BACK_LEFT
Definition: WavFile.php:95
getNumBlocks()
Definition: WavFile.php:748
const SPEAKER_FRONT_LEFT_OF_CENTER
Definition: WavFile.php:97
const SPEAKER_ALL
Definition: WavFile.php:109
const SPEAKER_LOW_FREQUENCY
Definition: WavFile.php:94
$_fmtExtendedSize
Definition: WavFile.php:150
const SPEAKER_TOP_FRONT_LEFT
Definition: WavFile.php:103
setByteRate($byteRate=null)
Definition: WavFile.php:767
setAudioFormat($audioFormat=null)
Definition: WavFile.php:587
$_audioFormat
Definition: WavFile.php:168
readWavData($dataOffset=0, $dataSize=null)
Read the wav data from the file into the buffer.
Definition: WavFile.php:1278
setWavData(&$data, $free=true)
Set the wav file data and properties from a wav file in a string.
Definition: WavFile.php:967
$_chunkSize
Definition: WavFile.php:144
generateNoise($duration=1.0, $percent=100)
Generate noise at the end of the wav for the specified duration and volume.
Definition: WavFile.php:1749
const SPEAKER_BACK_CENTER
Definition: WavFile.php:99
setNumChannels($numChannels)
Definition: WavFile.php:634
const SPEAKER_FRONT_CENTER
Definition: WavFile.php:93
closeWav()
Close a with openWav() previously opened wav file or free the buffer of setWavData().
Definition: WavFile.php:953
getFmtChunkSize()
Definition: WavFile.php:487
setBitsPerSample($bitsPerSample)
Definition: WavFile.php:697
$_channelMask
Definition: WavFile.php:177
static packSample($sample, $bitDepth)
Packs a single numeric sample to binary.
Definition: WavFile.php:332
setBlockAlign($blockAlign=null)
Definition: WavFile.php:736
readWav($readData=true)
Read wav file from a stream.
Definition: WavFile.php:997
getSamples()
Definition: WavFile.php:777
$_audioSubFormat
Definition: WavFile.php:171
getBlockAlign()
Definition: WavFile.php:732
__construct($numChannelsOrFileName=null, $sampleRateOrReadData=null, $bitsPerSample=null)
WavFile Constructor.
Definition: WavFile.php:221
const SPEAKER_FRONT_RIGHT
Definition: WavFile.php:92
getNumChannels()
Definition: WavFile.php:630
$_factChunkSize
Definition: WavFile.php:153
getFactChunkSize()
Definition: WavFile.php:526
setSamples(&$samples='')
Definition: WavFile.php:781
const WAVE_SUBFORMAT_IEEE_FLOAT
Definition: WavFile.php:124
getByteRate()
Definition: WavFile.php:763
getActualSize()
Definition: WavFile.php:453
$_sampleRate
Definition: WavFile.php:180
$_numChannels
Definition: WavFile.php:174
setDataOffset($dataOffset=null)
Definition: WavFile.php:569
setSampleRate($sampleRate)
Definition: WavFile.php:679
readWavHeader()
Parse a wav header.
Definition: WavFile.php:1022
static $LOOKUP_LOGBASE
Definition: WavFile.php:133
getZeroAmplitude()
Definition: WavFile.php:808
$_dataSize_valid
Definition: WavFile.php:162
const SPEAKER_TOP_FRONT_RIGHT
Definition: WavFile.php:105
__toString()
Output the wav file headers and data.
Definition: WavFile.php:269
const FILTER_NORMALIZE
Definition: WavFile.php:78
const SPEAKER_FRONT_RIGHT_OF_CENTER
Definition: WavFile.php:98
const WAVE_FORMAT_IEEE_FLOAT
Definition: WavFile.php:115
getValidBitsPerSample()
Definition: WavFile.php:713
appendWav(WavFile $wav)
Append a wav file to the current wav.
Definition: WavFile.php:1678
const MAX_SAMPLERATE
Definition: WavFile.php:87
WavFormatException indicates a malformed or unsupported wav file header.
Definition: WavFile.php:1864
$header
$data