ILIAS  Release_5_0_x_branch Revision 61816
 All Data Structures Namespaces Files Functions Variables Groups Pages
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 
69 class WavFile
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;
97  const SPEAKER_FRONT_LEFT_OF_CENTER = 0x000040;
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 
115  const WAVE_FORMAT_IEEE_FLOAT = 0x0003;
116 
118  const WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
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 
150  protected $_fmtExtendedSize;
151 
153  protected $_factChunkSize;
154 
156  protected $_dataSize;
157 
159  protected $_dataSize_fp;
160 
162  protected $_dataSize_valid;
163 
165  protected $_dataOffset;
166 
168  protected $_audioFormat;
169 
171  protected $_audioSubFormat;
172 
174  protected $_numChannels;
175 
177  protected $_channelMask;
178 
180  protected $_sampleRate;
181 
183  protected $_bitsPerSample;
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() {
488  return $this->_fmtChunkSize;
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() {
527  return $this->_factChunkSize;
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() {
613  return $this->_audioSubFormat;
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() {
694  return $this->_bitsPerSample;
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 
1859 class WavFileException extends Exception {}
1860