ILIAS  release_5-4 Revision v5.4.26-12-gabc799a52e6
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  } else {
249  $this->setNumChannels(is_null($numChannelsOrFileName) ? 1 : $numChannelsOrFileName)
250  ->setSampleRate(is_null($sampleRateOrReadData) ? 8000 : $sampleRateOrReadData)
251  ->setBitsPerSample(is_null($bitsPerSample) ? 8 : $bitsPerSample);
252  }
253  }
254 
255  public function __destruct()
256  {
257  if (is_resource($this->_fp)) {
258  $this->closeWav();
259  }
260  }
261 
262  public function __clone()
263  {
264  $this->_fp = null;
265  }
266 
272  public function __toString()
273  {
274  return $this->makeHeader() .
275  $this->getDataSubchunk();
276  }
277 
278 
279  /*%******************************************************************************************%*/
280  // Static methods
281 
289  public static function unpackSample($sampleBinary, $bitDepth = null)
290  {
291  if ($bitDepth === null) {
292  $bitDepth = strlen($sampleBinary) * 8;
293  }
294 
295  switch ($bitDepth) {
296  case 8:
297  // unsigned char
298  return ord($sampleBinary);
299 
300  case 16:
301  // signed short, little endian
302  $data = unpack('v', $sampleBinary);
303  $sample = $data[1];
304  if ($sample >= 0x8000) {
305  $sample -= 0x10000;
306  }
307  return $sample;
308 
309  case 24:
310  // 3 byte packed signed integer, little endian
311  $data = unpack('C3', $sampleBinary);
312  $sample = $data[1] | ($data[2] << 8) | ($data[3] << 16);
313  if ($sample >= 0x800000) {
314  $sample -= 0x1000000;
315  }
316  return $sample;
317 
318  case 32:
319  // 32-bit float
320  $data = unpack('f', $sampleBinary);
321  return $data[1];
322 
323  default:
324  return null;
325  }
326  }
327 
335  public static function packSample($sample, $bitDepth)
336  {
337  switch ($bitDepth) {
338  case 8:
339  // unsigned char
340  return chr($sample);
341 
342  case 16:
343  // signed short, little endian
344  if ($sample < 0) {
345  $sample += 0x10000;
346  }
347  return pack('v', $sample);
348 
349  case 24:
350  // 3 byte packed signed integer, little endian
351  if ($sample < 0) {
352  $sample += 0x1000000;
353  }
354  return pack('C3', $sample & 0xff, ($sample >> 8) & 0xff, ($sample >> 16) & 0xff);
355 
356  case 32:
357  // 32-bit float
358  return pack('f', $sample);
359 
360  default:
361  return null;
362  }
363  }
364 
373  public static function unpackSampleBlock($sampleBlock, $bitDepth, $numChannels = null)
374  {
375  $sampleBytes = $bitDepth / 8;
376  if ($numChannels === null) {
377  $numChannels = strlen($sampleBlock) / $sampleBytes;
378  }
379 
380  $samples = array();
381  for ($i = 0; $i < $numChannels; $i++) {
382  $sampleBinary = substr($sampleBlock, $i * $sampleBytes, $sampleBytes);
383  $samples[$i + 1] = self::unpackSample($sampleBinary, $bitDepth);
384  }
385 
386  return $samples;
387  }
388 
396  public static function packSampleBlock($samples, $bitDepth)
397  {
398  $sampleBlock = '';
399  foreach ($samples as $sample) {
400  $sampleBlock .= self::packSample($sample, $bitDepth);
401  }
402 
403  return $sampleBlock;
404  }
405 
424  public static function normalizeSample($sampleFloat, $threshold)
425  {
426  // apply positive gain
427  if ($threshold >= 1) {
428  return $sampleFloat * $threshold;
429  }
430 
431  // apply negative gain
432  if ($threshold <= -1) {
433  return $sampleFloat / -$threshold;
434  }
435 
436  $sign = $sampleFloat < 0 ? -1 : 1;
437  $sampleAbs = abs($sampleFloat);
438 
439  // logarithmic compression
440  if ($threshold >= 0 && $threshold < 1 && $sampleAbs > $threshold) {
441  $loga = self::$LOOKUP_LOGBASE[(int) ($threshold * 20)]; // log base modifier
442  return $sign * ($threshold + (1 - $threshold) * log(1 + $loga * ($sampleAbs - $threshold) / (2 - $threshold)) / log(1 + $loga));
443  }
444 
445  // linear compression
446  $thresholdAbs = abs($threshold);
447  if ($threshold > -1 && $threshold < 0 && $sampleAbs > $thresholdAbs) {
448  return $sign * ($thresholdAbs + (1 - $thresholdAbs) / (2 - $thresholdAbs) * ($sampleAbs - $thresholdAbs));
449  }
450 
451  // else ?
452  return $sampleFloat;
453  }
454 
455 
456  /*%******************************************************************************************%*/
457  // Getter and Setter methods for properties
458 
459  public function getActualSize()
460  {
461  return $this->_actualSize;
462  }
463 
464  protected function setActualSize($actualSize = null)
465  {
466  if (is_null($actualSize)) {
467  $this->_actualSize = 8 + $this->_chunkSize; // + "RIFF" header (ID + size)
468  } else {
469  $this->_actualSize = $actualSize;
470  }
471 
472  return $this;
473  }
474 
475  public function getChunkSize()
476  {
477  return $this->_chunkSize;
478  }
479 
480  protected function setChunkSize($chunkSize = null)
481  {
482  if (is_null($chunkSize)) {
483  $this->_chunkSize = 4 + // "WAVE" chunk
484  8 + $this->_fmtChunkSize + // "fmt " subchunk
485  ($this->_factChunkSize > 0 ? 8 + $this->_factChunkSize : 0) + // "fact" subchunk
486  8 + $this->_dataSize + // "data" subchunk
487  ($this->_dataSize & 1); // padding byte
488  } else {
489  $this->_chunkSize = $chunkSize;
490  }
491 
492  $this->setActualSize();
493 
494  return $this;
495  }
496 
497  public function getFmtChunkSize()
498  {
499  return $this->_fmtChunkSize;
500  }
501 
502  protected function setFmtChunkSize($fmtChunkSize = null)
503  {
504  if (is_null($fmtChunkSize)) {
505  $this->_fmtChunkSize = 16 + $this->_fmtExtendedSize;
506  } else {
507  $this->_fmtChunkSize = $fmtChunkSize;
508  }
509 
510  $this->setChunkSize() // implicit setActualSize()
511  ->setDataOffset();
512 
513  return $this;
514  }
515 
516  public function getFmtExtendedSize()
517  {
519  }
520 
521  protected function setFmtExtendedSize($fmtExtendedSize = null)
522  {
523  if (is_null($fmtExtendedSize)) {
524  if ($this->_audioFormat == self::WAVE_FORMAT_EXTENSIBLE) {
525  $this->_fmtExtendedSize = 2 + 22; // extension size for WAVE_FORMAT_EXTENSIBLE
526  } elseif ($this->_audioFormat != self::WAVE_FORMAT_PCM) {
527  $this->_fmtExtendedSize = 2 + 0; // empty extension
528  } else {
529  $this->_fmtExtendedSize = 0; // no extension, only for WAVE_FORMAT_PCM
530  }
531  } else {
532  $this->_fmtExtendedSize = $fmtExtendedSize;
533  }
534 
535  $this->setFmtChunkSize(); // implicit setSize(), setActualSize(), setDataOffset()
536 
537  return $this;
538  }
539 
540  public function getFactChunkSize()
541  {
542  return $this->_factChunkSize;
543  }
544 
545  protected function setFactChunkSize($factChunkSize = null)
546  {
547  if (is_null($factChunkSize)) {
548  if ($this->_audioFormat != self::WAVE_FORMAT_PCM) {
549  $this->_factChunkSize = 4;
550  } else {
551  $this->_factChunkSize = 0;
552  }
553  } else {
554  $this->_factChunkSize = $factChunkSize;
555  }
556 
557  $this->setChunkSize() // implicit setActualSize()
558  ->setDataOffset();
559 
560  return $this;
561  }
562 
563  public function getDataSize()
564  {
565  return $this->_dataSize;
566  }
567 
568  protected function setDataSize($dataSize = null)
569  {
570  if (is_null($dataSize)) {
571  $this->_dataSize = strlen($this->_samples);
572  } else {
573  $this->_dataSize = $dataSize;
574  }
575 
576  $this->setChunkSize() // implicit setActualSize()
577  ->setNumBlocks();
578  $this->_dataSize_valid = true;
579 
580  return $this;
581  }
582 
583  public function getDataOffset()
584  {
585  return $this->_dataOffset;
586  }
587 
588  protected function setDataOffset($dataOffset = null)
589  {
590  if (is_null($dataOffset)) {
591  $this->_dataOffset = 8 + // "RIFF" header (ID + size)
592  4 + // "WAVE" chunk
593  8 + $this->_fmtChunkSize + // "fmt " subchunk
594  ($this->_factChunkSize > 0 ? 8 + $this->_factChunkSize : 0) + // "fact" subchunk
595  8; // "data" subchunk
596  } else {
597  $this->_dataOffset = $dataOffset;
598  }
599 
600  return $this;
601  }
602 
603  public function getAudioFormat()
604  {
605  return $this->_audioFormat;
606  }
607 
608  protected function setAudioFormat($audioFormat = null)
609  {
610  if (is_null($audioFormat)) {
611  if (($this->_bitsPerSample <= 16 || $this->_bitsPerSample == 32)
612  && $this->_validBitsPerSample == $this->_bitsPerSample
613  && $this->_channelMask == self::SPEAKER_DEFAULT
614  && $this->_numChannels <= 2) {
615  if ($this->_bitsPerSample <= 16) {
616  $this->_audioFormat = self::WAVE_FORMAT_PCM;
617  } else {
618  $this->_audioFormat = self::WAVE_FORMAT_IEEE_FLOAT;
619  }
620  } else {
621  $this->_audioFormat = self::WAVE_FORMAT_EXTENSIBLE;
622  }
623  } else {
624  $this->_audioFormat = $audioFormat;
625  }
626 
627  $this->setAudioSubFormat()
628  ->setFactChunkSize() // implicit setSize(), setActualSize(), setDataOffset()
629  ->setFmtExtendedSize(); // implicit setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
630 
631  return $this;
632  }
633 
634  public function getAudioSubFormat()
635  {
636  return $this->_audioSubFormat;
637  }
638 
639  protected function setAudioSubFormat($audioSubFormat = null)
640  {
641  if (is_null($audioSubFormat)) {
642  if ($this->_bitsPerSample == 32) {
643  $this->_audioSubFormat = self::WAVE_SUBFORMAT_IEEE_FLOAT; // 32 bits are IEEE FLOAT in this class
644  } else {
645  $this->_audioSubFormat = self::WAVE_SUBFORMAT_PCM; // 8, 16 and 24 bits are PCM in this class
646  }
647  } else {
648  $this->_audioSubFormat = $audioSubFormat;
649  }
650 
651  return $this;
652  }
653 
654  public function getNumChannels()
655  {
656  return $this->_numChannels;
657  }
658 
659  public function setNumChannels($numChannels)
660  {
661  if ($numChannels < 1 || $numChannels > self::MAX_CHANNEL) {
662  throw new WavFileException('Unsupported number of channels. Only up to ' . self::MAX_CHANNEL . ' channels are supported.');
663  } elseif ($this->_samples !== '') {
664  trigger_error('Wav already has sample data. Changing the number of channels does not convert and may corrupt the data.', E_USER_NOTICE);
665  }
666 
667  $this->_numChannels = (int) $numChannels;
668 
669  $this->setAudioFormat() // implicit setAudioSubFormat(), setFactChunkSize(), setFmtExtendedSize(), setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
670  ->setByteRate()
671  ->setBlockAlign(); // implicit setNumBlocks()
672 
673  return $this;
674  }
675 
676  public function getChannelMask()
677  {
678  return $this->_channelMask;
679  }
680 
681  public function setChannelMask($channelMask = self::SPEAKER_DEFAULT)
682  {
683  if ($channelMask != 0) {
684  // count number of set bits - Hamming weight
685  $c = (int) $channelMask;
686  $n = 0;
687  while ($c > 0) {
688  $n += $c & 1;
689  $c >>= 1;
690  }
691  if ($n != $this->_numChannels || (((int) $channelMask | self::SPEAKER_ALL) != self::SPEAKER_ALL)) {
692  throw new WavFileException('Invalid channel mask. The number of channels does not match the number of locations in the mask.');
693  }
694  }
695 
696  $this->_channelMask = (int) $channelMask;
697 
698  $this->setAudioFormat(); // implicit setAudioSubFormat(), setFactChunkSize(), setFmtExtendedSize(), setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
699 
700  return $this;
701  }
702 
703  public function getSampleRate()
704  {
705  return $this->_sampleRate;
706  }
707 
708  public function setSampleRate($sampleRate)
709  {
710  if ($sampleRate < 1 || $sampleRate > self::MAX_SAMPLERATE) {
711  throw new WavFileException('Invalid sample rate.');
712  } elseif ($this->_samples !== '') {
713  trigger_error('Wav already has sample data. Changing the sample rate does not convert the data and may yield undesired results.', E_USER_NOTICE);
714  }
715 
716  $this->_sampleRate = (int) $sampleRate;
717 
718  $this->setByteRate();
719 
720  return $this;
721  }
722 
723  public function getBitsPerSample()
724  {
725  return $this->_bitsPerSample;
726  }
727 
728  public function setBitsPerSample($bitsPerSample)
729  {
730  if (!in_array($bitsPerSample, array(8, 16, 24, 32))) {
731  throw new WavFileException('Unsupported bits per sample. Only 8, 16, 24 and 32 bits are supported.');
732  } elseif ($this->_samples !== '') {
733  trigger_error('Wav already has sample data. Changing the bits per sample does not convert and may corrupt the data.', E_USER_NOTICE);
734  }
735 
736  $this->_bitsPerSample = (int) $bitsPerSample;
737 
738  $this->setValidBitsPerSample() // implicit setAudioFormat(), setAudioSubFormat(), setFmtChunkSize(), setFactChunkSize(), setSize(), setActualSize(), setDataOffset()
739  ->setByteRate()
740  ->setBlockAlign(); // implicit setNumBlocks()
741 
742  return $this;
743  }
744 
745  public function getValidBitsPerSample()
746  {
748  }
749 
750  protected function setValidBitsPerSample($validBitsPerSample = null)
751  {
752  if (is_null($validBitsPerSample)) {
753  $this->_validBitsPerSample = $this->_bitsPerSample;
754  } else {
755  if ($validBitsPerSample < 1 || $validBitsPerSample > $this->_bitsPerSample) {
756  throw new WavFileException('ValidBitsPerSample cannot be greater than BitsPerSample.');
757  }
758  $this->_validBitsPerSample = (int) $validBitsPerSample;
759  }
760 
761  $this->setAudioFormat(); // implicit setAudioSubFormat(), setFactChunkSize(), setFmtExtendedSize(), setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
762 
763  return $this;
764  }
765 
766  public function getBlockAlign()
767  {
768  return $this->_blockAlign;
769  }
770 
771  protected function setBlockAlign($blockAlign = null)
772  {
773  if (is_null($blockAlign)) {
774  $this->_blockAlign = $this->_numChannels * $this->_bitsPerSample / 8;
775  } else {
776  $this->_blockAlign = $blockAlign;
777  }
778 
779  $this->setNumBlocks();
780 
781  return $this;
782  }
783 
784  public function getNumBlocks()
785  {
786  return $this->_numBlocks;
787  }
788 
789  protected function setNumBlocks($numBlocks = null)
790  {
791  if (is_null($numBlocks)) {
792  $this->_numBlocks = (int) ($this->_dataSize / $this->_blockAlign); // do not count incomplete sample blocks
793  } else {
794  $this->_numBlocks = $numBlocks;
795  }
796 
797  return $this;
798  }
799 
800  public function getByteRate()
801  {
802  return $this->_byteRate;
803  }
804 
805  protected function setByteRate($byteRate = null)
806  {
807  if (is_null($byteRate)) {
808  $this->_byteRate = $this->_sampleRate * $this->_numChannels * $this->_bitsPerSample / 8;
809  } else {
810  $this->_byteRate = $byteRate;
811  }
812 
813  return $this;
814  }
815 
816  public function getSamples()
817  {
818  return $this->_samples;
819  }
820 
821  public function setSamples(&$samples = '')
822  {
823  if (strlen($samples) % $this->_blockAlign != 0) {
824  throw new WavFileException('Incorrect samples size. Has to be a multiple of BlockAlign.');
825  }
826 
827  $this->_samples = $samples;
828 
829  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
830 
831  return $this;
832  }
833 
834 
835  /*%******************************************************************************************%*/
836  // Getters
837 
838  public function getMinAmplitude()
839  {
840  if ($this->_bitsPerSample == 8) {
841  return 0;
842  } elseif ($this->_bitsPerSample == 32) {
843  return -1.0;
844  } else {
845  return -(1 << ($this->_bitsPerSample - 1));
846  }
847  }
848 
849  public function getZeroAmplitude()
850  {
851  if ($this->_bitsPerSample == 8) {
852  return 0x80;
853  } elseif ($this->_bitsPerSample == 32) {
854  return 0.0;
855  } else {
856  return 0;
857  }
858  }
859 
860  public function getMaxAmplitude()
861  {
862  if ($this->_bitsPerSample == 8) {
863  return 0xFF;
864  } elseif ($this->_bitsPerSample == 32) {
865  return 1.0;
866  } else {
867  return (1 << ($this->_bitsPerSample - 1)) - 1;
868  }
869  }
870 
871 
872  /*%******************************************************************************************%*/
873  // Wave file methods
874 
881  public function makeHeader()
882  {
883  // reset and recalculate
884  $this->setAudioFormat(); // implicit setAudioSubFormat(), setFactChunkSize(), setFmtExtendedSize(), setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
885  $this->setNumBlocks();
886 
887  // RIFF header
888  $header = pack('N', 0x52494646); // ChunkID - "RIFF"
889  $header .= pack('V', $this->getChunkSize()); // ChunkSize
890  $header .= pack('N', 0x57415645); // Format - "WAVE"
891 
892  // "fmt " subchunk
893  $header .= pack('N', 0x666d7420); // SubchunkID - "fmt "
894  $header .= pack('V', $this->getFmtChunkSize()); // SubchunkSize
895  $header .= pack('v', $this->getAudioFormat()); // AudioFormat
896  $header .= pack('v', $this->getNumChannels()); // NumChannels
897  $header .= pack('V', $this->getSampleRate()); // SampleRate
898  $header .= pack('V', $this->getByteRate()); // ByteRate
899  $header .= pack('v', $this->getBlockAlign()); // BlockAlign
900  $header .= pack('v', $this->getBitsPerSample()); // BitsPerSample
901  if ($this->getFmtExtendedSize() == 24) {
902  $header .= pack('v', 22); // extension size = 24 bytes, cbSize: 24 - 2 = 22 bytes
903  $header .= pack('v', $this->getValidBitsPerSample()); // ValidBitsPerSample
904  $header .= pack('V', $this->getChannelMask()); // ChannelMask
905  $header .= pack('H32', $this->getAudioSubFormat()); // SubFormat
906  } elseif ($this->getFmtExtendedSize() == 2) {
907  $header .= pack('v', 0); // extension size = 2 bytes, cbSize: 2 - 2 = 0 bytes
908  }
909 
910  // "fact" subchunk
911  if ($this->getFactChunkSize() == 4) {
912  $header .= pack('N', 0x66616374); // SubchunkID - "fact"
913  $header .= pack('V', 4); // SubchunkSize
914  $header .= pack('V', $this->getNumBlocks()); // SampleLength (per channel)
915  }
916 
917  return $header;
918  }
919 
925  public function getDataSubchunk()
926  {
927  // check preconditions
928  if (!$this->_dataSize_valid) {
929  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
930  }
931 
932 
933  // create subchunk
934  return pack('N', 0x64617461) . // SubchunkID - "data"
935  pack('V', $this->getDataSize()) . // SubchunkSize
936  $this->_samples . // Subchunk data
937  ($this->getDataSize() & 1 ? chr(0) : ''); // padding byte
938  }
939 
946  public function save($filename)
947  {
948  $fp = @fopen($filename, 'w+b');
949  if (!is_resource($fp)) {
950  throw new WavFileException('Failed to open "' . $filename . '" for writing.');
951  }
952 
953  fwrite($fp, $this->makeHeader());
954  fwrite($fp, $this->getDataSubchunk());
955  fclose($fp);
956 
957  return $this;
958  }
959 
968  public function openWav($filename, $readData = true)
969  {
970  // check preconditions
971  if (!file_exists($filename)) {
972  throw new WavFileException('Failed to open "' . $filename . '". File not found.');
973  } elseif (!is_readable($filename)) {
974  throw new WavFileException('Failed to open "' . $filename . '". File is not readable.');
975  } elseif (is_resource($this->_fp)) {
976  $this->closeWav();
977  }
978 
979 
980  // open the file
981  $this->_fp = @fopen($filename, 'rb');
982  if (!is_resource($this->_fp)) {
983  throw new WavFileException('Failed to open "' . $filename . '".');
984  }
985 
986  // read the file
987  return $this->readWav($readData);
988  }
989 
994  public function closeWav()
995  {
996  if (is_resource($this->_fp)) {
997  fclose($this->_fp);
998  }
999 
1000  return $this;
1001  }
1002 
1011  public function setWavData(&$data, $free = true)
1012  {
1013  // check preconditions
1014  if (is_resource($this->_fp)) {
1015  $this->closeWav();
1016  }
1017 
1018 
1019  // open temporary stream in memory
1020  $this->_fp = @fopen('php://memory', 'w+b');
1021  if (!is_resource($this->_fp)) {
1022  throw new WavFileException('Failed to open memory stream to write wav data. Use openWav() instead.');
1023  }
1024 
1025  // prepare stream
1026  fwrite($this->_fp, $data);
1027  rewind($this->_fp);
1028 
1029  // free the passed data
1030  if ($free) {
1031  $data = null;
1032  }
1033 
1034  // read the stream like a file
1035  return $this->readWav(true);
1036  }
1037 
1045  protected function readWav($readData = true)
1046  {
1047  if (!is_resource($this->_fp)) {
1048  throw new WavFileException('No wav file open. Use openWav() first.');
1049  }
1050 
1051  try {
1052  $this->readWavHeader();
1053  } catch (WavFileException $ex) {
1054  $this->closeWav();
1055  throw $ex;
1056  }
1057 
1058  if ($readData) {
1059  return $this->readWavData();
1060  }
1061 
1062  return $this;
1063  }
1064 
1072  protected function readWavHeader()
1073  {
1074  if (!is_resource($this->_fp)) {
1075  throw new WavFileException('No wav file open. Use openWav() first.');
1076  }
1077 
1078  // get actual file size
1079  $stat = fstat($this->_fp);
1080  $actualSize = $stat['size'];
1081 
1082  $this->_actualSize = $actualSize;
1083 
1084 
1085  // read the common header
1086  $header = fread($this->_fp, 36); // minimum size of the wav header
1087  if (strlen($header) < 36) {
1088  throw new WavFormatException('Not wav format. Header too short.', 1);
1089  }
1090 
1091 
1092  // check "RIFF" header
1093  $RIFF = unpack('NChunkID/VChunkSize/NFormat', $header);
1094 
1095  if ($RIFF['ChunkID'] != 0x52494646) { // "RIFF"
1096  throw new WavFormatException('Not wav format. "RIFF" signature missing.', 2);
1097  }
1098 
1099  if ($actualSize - 8 < $RIFF['ChunkSize']) {
1100  trigger_error('"RIFF" chunk size does not match actual file size. Found ' . $RIFF['ChunkSize'] . ', expected ' . ($actualSize - 8) . '.', E_USER_NOTICE);
1101  $RIFF['ChunkSize'] = $actualSize - 8;
1102  //throw new WavFormatException('"RIFF" chunk size does not match actual file size. Found ' . $RIFF['ChunkSize'] . ', expected ' . ($actualSize - 8) . '.', 3);
1103  }
1104 
1105  if ($RIFF['Format'] != 0x57415645) { // "WAVE"
1106  throw new WavFormatException('Not wav format. "RIFF" chunk format is not "WAVE".', 4);
1107  }
1108 
1109  $this->_chunkSize = $RIFF['ChunkSize'];
1110 
1111 
1112  // check common "fmt " subchunk
1113  $fmt = unpack(
1114  'NSubchunkID/VSubchunkSize/vAudioFormat/vNumChannels/'
1115  . 'VSampleRate/VByteRate/vBlockAlign/vBitsPerSample',
1116  substr($header, 12)
1117  );
1118 
1119  if ($fmt['SubchunkID'] != 0x666d7420) { // "fmt "
1120  throw new WavFormatException('Bad wav header. Expected "fmt " subchunk.', 11);
1121  }
1122 
1123  if ($fmt['SubchunkSize'] < 16) {
1124  throw new WavFormatException('Bad "fmt " subchunk size.', 12);
1125  }
1126 
1127  if ($fmt['AudioFormat'] != self::WAVE_FORMAT_PCM
1128  && $fmt['AudioFormat'] != self::WAVE_FORMAT_IEEE_FLOAT
1129  && $fmt['AudioFormat'] != self::WAVE_FORMAT_EXTENSIBLE) {
1130  throw new WavFormatException('Unsupported audio format. Only PCM or IEEE FLOAT (EXTENSIBLE) audio is supported.', 13);
1131  }
1132 
1133  if ($fmt['NumChannels'] < 1 || $fmt['NumChannels'] > self::MAX_CHANNEL) {
1134  throw new WavFormatException('Invalid number of channels in "fmt " subchunk.', 14);
1135  }
1136 
1137  if ($fmt['SampleRate'] < 1 || $fmt['SampleRate'] > self::MAX_SAMPLERATE) {
1138  throw new WavFormatException('Invalid sample rate in "fmt " subchunk.', 15);
1139  }
1140 
1141  if (($fmt['AudioFormat'] == self::WAVE_FORMAT_PCM && !in_array($fmt['BitsPerSample'], array(8, 16, 24)))
1142  || ($fmt['AudioFormat'] == self::WAVE_FORMAT_IEEE_FLOAT && $fmt['BitsPerSample'] != 32)
1143  || ($fmt['AudioFormat'] == self::WAVE_FORMAT_EXTENSIBLE && !in_array($fmt['BitsPerSample'], array(8, 16, 24, 32)))) {
1144  throw new WavFormatException('Only 8, 16 and 24-bit PCM and 32-bit IEEE FLOAT (EXTENSIBLE) audio is supported.', 16);
1145  }
1146 
1147  $blockAlign = $fmt['NumChannels'] * $fmt['BitsPerSample'] / 8;
1148  if ($blockAlign != $fmt['BlockAlign']) {
1149  trigger_error('Invalid block align in "fmt " subchunk. Found ' . $fmt['BlockAlign'] . ', expected ' . $blockAlign . '.', E_USER_NOTICE);
1150  $fmt['BlockAlign'] = $blockAlign;
1151  //throw new WavFormatException('Invalid block align in "fmt " subchunk. Found ' . $fmt['BlockAlign'] . ', expected ' . $blockAlign . '.', 17);
1152  }
1153 
1154  $byteRate = $fmt['SampleRate'] * $blockAlign;
1155  if ($byteRate != $fmt['ByteRate']) {
1156  trigger_error('Invalid average byte rate in "fmt " subchunk. Found ' . $fmt['ByteRate'] . ', expected ' . $byteRate . '.', E_USER_NOTICE);
1157  $fmt['ByteRate'] = $byteRate;
1158  //throw new WavFormatException('Invalid average byte rate in "fmt " subchunk. Found ' . $fmt['ByteRate'] . ', expected ' . $byteRate . '.', 18);
1159  }
1160 
1161  $this->_fmtChunkSize = $fmt['SubchunkSize'];
1162  $this->_audioFormat = $fmt['AudioFormat'];
1163  $this->_numChannels = $fmt['NumChannels'];
1164  $this->_sampleRate = $fmt['SampleRate'];
1165  $this->_byteRate = $fmt['ByteRate'];
1166  $this->_blockAlign = $fmt['BlockAlign'];
1167  $this->_bitsPerSample = $fmt['BitsPerSample'];
1168 
1169 
1170  // read extended "fmt " subchunk data
1171  $extendedFmt = '';
1172  if ($fmt['SubchunkSize'] > 16) {
1173  // possibly handle malformed subchunk without a padding byte
1174  $extendedFmt = fread($this->_fp, $fmt['SubchunkSize'] - 16 + ($fmt['SubchunkSize'] & 1)); // also read padding byte
1175  if (strlen($extendedFmt) < $fmt['SubchunkSize'] - 16) {
1176  throw new WavFormatException('Not wav format. Header too short.', 1);
1177  }
1178  }
1179 
1180 
1181  // check extended "fmt " for EXTENSIBLE Audio Format
1182  if ($fmt['AudioFormat'] == self::WAVE_FORMAT_EXTENSIBLE) {
1183  if (strlen($extendedFmt) < 24) {
1184  throw new WavFormatException('Invalid EXTENSIBLE "fmt " subchunk size. Found ' . $fmt['SubchunkSize'] . ', expected at least 40.', 19);
1185  }
1186 
1187  $extensibleFmt = unpack('vSize/vValidBitsPerSample/VChannelMask/H32SubFormat', substr($extendedFmt, 0, 24));
1188 
1189  if ($extensibleFmt['SubFormat'] != self::WAVE_SUBFORMAT_PCM
1190  && $extensibleFmt['SubFormat'] != self::WAVE_SUBFORMAT_IEEE_FLOAT) {
1191  throw new WavFormatException('Unsupported audio format. Only PCM or IEEE FLOAT (EXTENSIBLE) audio is supported.', 13);
1192  }
1193 
1194  if (($extensibleFmt['SubFormat'] == self::WAVE_SUBFORMAT_PCM && !in_array($fmt['BitsPerSample'], array(8, 16, 24)))
1195  || ($extensibleFmt['SubFormat'] == self::WAVE_SUBFORMAT_IEEE_FLOAT && $fmt['BitsPerSample'] != 32)) {
1196  throw new WavFormatException('Only 8, 16 and 24-bit PCM and 32-bit IEEE FLOAT (EXTENSIBLE) audio is supported.', 16);
1197  }
1198 
1199  if ($extensibleFmt['Size'] != 22) {
1200  trigger_error('Invaid extension size in EXTENSIBLE "fmt " subchunk.', E_USER_NOTICE);
1201  $extensibleFmt['Size'] = 22;
1202  //throw new WavFormatException('Invaid extension size in EXTENSIBLE "fmt " subchunk.', 20);
1203  }
1204 
1205  if ($extensibleFmt['ValidBitsPerSample'] != $fmt['BitsPerSample']) {
1206  trigger_error('Invaid or unsupported valid bits per sample in EXTENSIBLE "fmt " subchunk.', E_USER_NOTICE);
1207  $extensibleFmt['ValidBitsPerSample'] = $fmt['BitsPerSample'];
1208  //throw new WavFormatException('Invaid or unsupported valid bits per sample in EXTENSIBLE "fmt " subchunk.', 21);
1209  }
1210 
1211  if ($extensibleFmt['ChannelMask'] != 0) {
1212  // count number of set bits - Hamming weight
1213  $c = (int) $extensibleFmt['ChannelMask'];
1214  $n = 0;
1215  while ($c > 0) {
1216  $n += $c & 1;
1217  $c >>= 1;
1218  }
1219  if ($n != $fmt['NumChannels'] || (((int) $extensibleFmt['ChannelMask'] | self::SPEAKER_ALL) != self::SPEAKER_ALL)) {
1220  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);
1221  $extensibleFmt['ChannelMask'] = 0;
1222  //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);
1223  }
1224  }
1225 
1226  $this->_fmtExtendedSize = strlen($extendedFmt);
1227  $this->_validBitsPerSample = $extensibleFmt['ValidBitsPerSample'];
1228  $this->_channelMask = $extensibleFmt['ChannelMask'];
1229  $this->_audioSubFormat = $extensibleFmt['SubFormat'];
1230  } else {
1231  $this->_fmtExtendedSize = strlen($extendedFmt);
1232  $this->_validBitsPerSample = $fmt['BitsPerSample'];
1233  $this->_channelMask = 0;
1234  $this->_audioSubFormat = null;
1235  }
1236 
1237 
1238  // read additional subchunks until "data" subchunk is found
1239  $factSubchunk = array();
1240  $dataSubchunk = array();
1241 
1242  while (!feof($this->_fp)) {
1243  $subchunkHeader = fread($this->_fp, 8);
1244  if (strlen($subchunkHeader) < 8) {
1245  throw new WavFormatException('Missing "data" subchunk.', 101);
1246  }
1247 
1248  $subchunk = unpack('NSubchunkID/VSubchunkSize', $subchunkHeader);
1249 
1250  if ($subchunk['SubchunkID'] == 0x66616374) { // "fact"
1251  // possibly handle malformed subchunk without a padding byte
1252  $subchunkData = fread($this->_fp, $subchunk['SubchunkSize'] + ($subchunk['SubchunkSize'] & 1)); // also read padding byte
1253  if (strlen($subchunkData) < 4) {
1254  throw new WavFormatException('Invalid "fact" subchunk.', 102);
1255  }
1256 
1257  $factParams = unpack('VSampleLength', substr($subchunkData, 0, 4));
1258  $factSubchunk = array_merge($subchunk, $factParams);
1259  } elseif ($subchunk['SubchunkID'] == 0x64617461) { // "data"
1260  $dataSubchunk = $subchunk;
1261 
1262  break;
1263  } elseif ($subchunk['SubchunkID'] == 0x7761766C) { // "wavl"
1264  throw new WavFormatException('Wave List Chunk ("wavl" subchunk) is not supported.', 106);
1265  } else {
1266  // skip all other (unknown) subchunks
1267  // possibly handle malformed subchunk without a padding byte
1268  if ($subchunk['SubchunkSize'] < 0
1269  || fseek($this->_fp, $subchunk['SubchunkSize'] + ($subchunk['SubchunkSize'] & 1), SEEK_CUR) !== 0) { // also skip padding byte
1270  throw new WavFormatException('Invalid subchunk (0x' . dechex($subchunk['SubchunkID']) . ') encountered.', 103);
1271  }
1272  }
1273  }
1274 
1275  if (empty($dataSubchunk)) {
1276  throw new WavFormatException('Missing "data" subchunk.', 101);
1277  }
1278 
1279 
1280  // check "data" subchunk
1281  $dataOffset = ftell($this->_fp);
1282  if ($dataSubchunk['SubchunkSize'] < 0 || $actualSize - $dataOffset < $dataSubchunk['SubchunkSize']) {
1283  trigger_error('Invalid "data" subchunk size.', E_USER_NOTICE);
1284  $dataSubchunk['SubchunkSize'] = $actualSize - $dataOffset;
1285  //throw new WavFormatException('Invalid "data" subchunk size.', 104);
1286  }
1287 
1288  $this->_dataOffset = $dataOffset;
1289  $this->_dataSize = $dataSubchunk['SubchunkSize'];
1290  $this->_dataSize_fp = $dataSubchunk['SubchunkSize'];
1291  $this->_dataSize_valid = false;
1292  $this->_samples = '';
1293 
1294 
1295  // check "fact" subchunk
1296  $numBlocks = (int) ($dataSubchunk['SubchunkSize'] / $fmt['BlockAlign']);
1297 
1298  if (empty($factSubchunk)) { // construct fake "fact" subchunk
1299  $factSubchunk = array('SubchunkSize' => 0, 'SampleLength' => $numBlocks);
1300  }
1301 
1302  if ($factSubchunk['SampleLength'] != $numBlocks) {
1303  trigger_error('Invalid sample length in "fact" subchunk.', E_USER_NOTICE);
1304  $factSubchunk['SampleLength'] = $numBlocks;
1305  //throw new WavFormatException('Invalid sample length in "fact" subchunk.', 105);
1306  }
1307 
1308  $this->_factChunkSize = $factSubchunk['SubchunkSize'];
1309  $this->_numBlocks = $factSubchunk['SampleLength'];
1310 
1311 
1312  return $this;
1313  }
1314 
1322  public function readWavData($dataOffset = 0, $dataSize = null)
1323  {
1324  // check preconditions
1325  if (!is_resource($this->_fp)) {
1326  throw new WavFileException('No wav file open. Use openWav() first.');
1327  }
1328 
1329  if ($dataOffset < 0 || $dataOffset % $this->getBlockAlign() > 0) {
1330  throw new WavFileException('Invalid data offset. Has to be a multiple of BlockAlign.');
1331  }
1332 
1333  if (is_null($dataSize)) {
1334  $dataSize = $this->_dataSize_fp - ($this->_dataSize_fp % $this->getBlockAlign()); // only read complete blocks
1335  } elseif ($dataSize < 0 || $dataSize % $this->getBlockAlign() > 0) {
1336  throw new WavFileException('Invalid data size to read. Has to be a multiple of BlockAlign.');
1337  }
1338 
1339 
1340  // skip offset
1341  if ($dataOffset > 0 && fseek($this->_fp, $dataOffset, SEEK_CUR) !== 0) {
1342  throw new WavFileException('Seeking to data offset failed.');
1343  }
1344 
1345  // read data
1346  $this->_samples .= fread($this->_fp, $dataSize); // allow appending
1347  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1348 
1349  // close file or memory stream
1350  return $this->closeWav();
1351  }
1352 
1353 
1354  /*%******************************************************************************************%*/
1355  // Sample manipulation methods
1356 
1363  public function getSampleBlock($blockNum)
1364  {
1365  // check preconditions
1366  if (!$this->_dataSize_valid) {
1367  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1368  }
1369 
1370  $offset = $blockNum * $this->_blockAlign;
1371  if ($offset + $this->_blockAlign > $this->_dataSize || $offset < 0) {
1372  return null;
1373  }
1374 
1375 
1376  // read data
1377  return substr($this->_samples, $offset, $this->_blockAlign);
1378  }
1379 
1388  public function setSampleBlock($sampleBlock, $blockNum)
1389  {
1390  // check preconditions
1391  $blockAlign = $this->_blockAlign;
1392  if (!isset($sampleBlock[$blockAlign - 1]) || isset($sampleBlock[$blockAlign])) { // faster than: if (strlen($sampleBlock) != $blockAlign)
1393  throw new WavFileException('Incorrect sample block size. Got ' . strlen($sampleBlock) . ', expected ' . $blockAlign . '.');
1394  }
1395 
1396  if (!$this->_dataSize_valid) {
1397  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1398  }
1399 
1400  $numBlocks = (int) ($this->_dataSize / $blockAlign);
1401  $offset = $blockNum * $blockAlign;
1402  if ($blockNum > $numBlocks || $blockNum < 0) { // allow appending
1403  throw new WavFileException('Sample block number is out of range.');
1404  }
1405 
1406 
1407  // replace or append data
1408  if ($blockNum == $numBlocks) {
1409  // append
1410  $this->_samples .= $sampleBlock;
1411  $this->_dataSize += $blockAlign;
1412  $this->_chunkSize += $blockAlign;
1413  $this->_actualSize += $blockAlign;
1414  $this->_numBlocks++;
1415  } else {
1416  // replace
1417  for ($i = 0; $i < $blockAlign; ++$i) {
1418  $this->_samples[$offset + $i] = $sampleBlock[$i];
1419  }
1420  }
1421 
1422  return $this;
1423  }
1424 
1433  public function getSampleValue($blockNum, $channelNum)
1434  {
1435  // check preconditions
1436  if ($channelNum < 1 || $channelNum > $this->_numChannels) {
1437  throw new WavFileException('Channel number is out of range.');
1438  }
1439 
1440  if (!$this->_dataSize_valid) {
1441  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1442  }
1443 
1444  $sampleBytes = $this->_bitsPerSample / 8;
1445  $offset = $blockNum * $this->_blockAlign + ($channelNum - 1) * $sampleBytes;
1446  if ($offset + $sampleBytes > $this->_dataSize || $offset < 0) {
1447  return null;
1448  }
1449 
1450  // read binary value
1451  $sampleBinary = substr($this->_samples, $offset, $sampleBytes);
1452 
1453  // convert binary to value
1454  switch ($this->_bitsPerSample) {
1455  case 8:
1456  // unsigned char
1457  return (float) ((ord($sampleBinary) - 0x80) / 0x80);
1458 
1459  case 16:
1460  // signed short, little endian
1461  $data = unpack('v', $sampleBinary);
1462  $sample = $data[1];
1463  if ($sample >= 0x8000) {
1464  $sample -= 0x10000;
1465  }
1466  return (float) ($sample / 0x8000);
1467 
1468  case 24:
1469  // 3 byte packed signed integer, little endian
1470  $data = unpack('C3', $sampleBinary);
1471  $sample = $data[1] | ($data[2] << 8) | ($data[3] << 16);
1472  if ($sample >= 0x800000) {
1473  $sample -= 0x1000000;
1474  }
1475  return (float) ($sample / 0x800000);
1476 
1477  case 32:
1478  // 32-bit float
1479  $data = unpack('f', $sampleBinary);
1480  return (float) $data[1];
1481 
1482  default:
1483  return null;
1484  }
1485  }
1486 
1497  public function setSampleValue($sampleFloat, $blockNum, $channelNum)
1498  {
1499  // check preconditions
1500  if ($channelNum < 1 || $channelNum > $this->_numChannels) {
1501  throw new WavFileException('Channel number is out of range.');
1502  }
1503 
1504  if (!$this->_dataSize_valid) {
1505  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1506  }
1507 
1508  $dataSize = $this->_dataSize;
1509  $bitsPerSample = $this->_bitsPerSample;
1510  $sampleBytes = $bitsPerSample / 8;
1511  $offset = $blockNum * $this->_blockAlign + ($channelNum - 1) * $sampleBytes;
1512  if (($offset + $sampleBytes > $dataSize && $offset != $dataSize) || $offset < 0) { // allow appending
1513  throw new WavFileException('Sample block or channel number is out of range.');
1514  }
1515 
1516 
1517  // convert to value, quantize and clip
1518  if ($bitsPerSample == 32) {
1519  $sample = $sampleFloat < -1.0 ? -1.0 : ($sampleFloat > 1.0 ? 1.0 : $sampleFloat);
1520  } else {
1521  $p = 1 << ($bitsPerSample - 1); // 2 to the power of _bitsPerSample divided by 2
1522 
1523  // project and quantize (round) float to integer values
1524  $sample = $sampleFloat < 0 ? (int) ($sampleFloat * $p - 0.5) : (int) ($sampleFloat * $p + 0.5);
1525 
1526  // clip if necessary to [-$p, $p - 1]
1527  if ($sample < -$p) {
1528  $sample = -$p;
1529  } elseif ($sample > $p - 1) {
1530  $sample = $p - 1;
1531  }
1532  }
1533 
1534  // convert to binary
1535  switch ($bitsPerSample) {
1536  case 8:
1537  // unsigned char
1538  $sampleBinary = chr($sample + 0x80);
1539  break;
1540 
1541  case 16:
1542  // signed short, little endian
1543  if ($sample < 0) {
1544  $sample += 0x10000;
1545  }
1546  $sampleBinary = pack('v', $sample);
1547  break;
1548 
1549  case 24:
1550  // 3 byte packed signed integer, little endian
1551  if ($sample < 0) {
1552  $sample += 0x1000000;
1553  }
1554  $sampleBinary = pack('C3', $sample & 0xff, ($sample >> 8) & 0xff, ($sample >> 16) & 0xff);
1555  break;
1556 
1557  case 32:
1558  // 32-bit float
1559  $sampleBinary = pack('f', $sample);
1560  break;
1561 
1562  default:
1563  $sampleBinary = null;
1564  $sampleBytes = 0;
1565  break;
1566  }
1567 
1568  // replace or append data
1569  if ($offset == $dataSize) {
1570  // append
1571  $this->_samples .= $sampleBinary;
1572  $this->_dataSize += $sampleBytes;
1573  $this->_chunkSize += $sampleBytes;
1574  $this->_actualSize += $sampleBytes;
1575  $this->_numBlocks = (int) ($this->_dataSize / $this->_blockAlign);
1576  } else {
1577  // replace
1578  for ($i = 0; $i < $sampleBytes; ++$i) {
1579  $this->_samples{$offset + $i} = $sampleBinary{$i};
1580  }
1581  }
1582 
1583  return $this;
1584  }
1585 
1586 
1587  /*%******************************************************************************************%*/
1588  // Audio processing methods
1589 
1615  public function filter($filters, $blockOffset = 0, $numBlocks = null)
1616  {
1617  // check preconditions
1618  $totalBlocks = $this->getNumBlocks();
1619  $numChannels = $this->getNumChannels();
1620  if (is_null($numBlocks)) {
1621  $numBlocks = $totalBlocks - $blockOffset;
1622  }
1623 
1624  if (!is_array($filters) || empty($filters) || $blockOffset < 0 || $blockOffset > $totalBlocks || $numBlocks <= 0) {
1625  // nothing to do
1626  return $this;
1627  }
1628 
1629  // check filtes
1630  $filter_mix = false;
1631  if (array_key_exists(self::FILTER_MIX, $filters)) {
1632  if (!is_array($filters[self::FILTER_MIX])) {
1633  // assume the 'wav' parameter
1634  $filters[self::FILTER_MIX] = array('wav' => $filters[self::FILTER_MIX]);
1635  }
1636 
1637  $mix_wav = @$filters[self::FILTER_MIX]['wav'];
1638  if (!($mix_wav instanceof WavFile)) {
1639  throw new WavFileException("WavFile to mix is missing or invalid.");
1640  } elseif ($mix_wav->getSampleRate() != $this->getSampleRate()) {
1641  throw new WavFileException("Sample rate of WavFile to mix does not match.");
1642  } elseif ($mix_wav->getNumChannels() != $this->getNumChannels()) {
1643  throw new WavFileException("Number of channels of WavFile to mix does not match.");
1644  }
1645 
1646  $mix_loop = @$filters[self::FILTER_MIX]['loop'];
1647  if (is_null($mix_loop)) {
1648  $mix_loop = false;
1649  }
1650 
1651  $mix_blockOffset = @$filters[self::FILTER_MIX]['blockOffset'];
1652  if (is_null($mix_blockOffset)) {
1653  $mix_blockOffset = 0;
1654  }
1655 
1656  $mix_totalBlocks = $mix_wav->getNumBlocks();
1657  $mix_numBlocks = @$filters[self::FILTER_MIX]['numBlocks'];
1658  if (is_null($mix_numBlocks)) {
1659  $mix_numBlocks = $mix_loop ? $mix_totalBlocks : $mix_totalBlocks - $mix_blockOffset;
1660  }
1661  $mix_maxBlock = min($mix_blockOffset + $mix_numBlocks, $mix_totalBlocks);
1662 
1663  $filter_mix = true;
1664  }
1665 
1666  $filter_normalize = false;
1667  if (array_key_exists(self::FILTER_NORMALIZE, $filters)) {
1668  $normalize_threshold = @$filters[self::FILTER_NORMALIZE];
1669 
1670  if (!is_null($normalize_threshold) && abs($normalize_threshold) != 1) {
1671  $filter_normalize = true;
1672  }
1673  }
1674 
1675  $filter_degrade = false;
1676  if (array_key_exists(self::FILTER_DEGRADE, $filters)) {
1677  $degrade_quality = @$filters[self::FILTER_DEGRADE];
1678  if (is_null($degrade_quality)) {
1679  $degrade_quality = 1;
1680  }
1681 
1682  if ($degrade_quality >= 0 && $degrade_quality < 1) {
1683  $filter_degrade = true;
1684  }
1685  }
1686 
1687 
1688  // loop through all sample blocks
1689  for ($block = 0; $block < $numBlocks; ++$block) {
1690  // loop through all channels
1691  for ($channel = 1; $channel <= $numChannels; ++$channel) {
1692  // read current sample
1693  $currentBlock = $blockOffset + $block;
1694  $sampleFloat = $this->getSampleValue($currentBlock, $channel);
1695 
1696 
1697  /************* MIX FILTER ***********************/
1698  if ($filter_mix) {
1699  if ($mix_loop) {
1700  $mixBlock = ($mix_blockOffset + ($block % $mix_numBlocks)) % $mix_totalBlocks;
1701  } else {
1702  $mixBlock = $mix_blockOffset + $block;
1703  }
1704 
1705  if ($mixBlock < $mix_maxBlock) {
1706  $sampleFloat += $mix_wav->getSampleValue($mixBlock, $channel);
1707  }
1708  }
1709 
1710  /************* NORMALIZE FILTER *******************/
1711  if ($filter_normalize) {
1712  $sampleFloat = $this->normalizeSample($sampleFloat, $normalize_threshold);
1713  }
1714 
1715  /************* DEGRADE FILTER *******************/
1716  if ($filter_degrade) {
1717  $sampleFloat += rand(1000000 * ($degrade_quality - 1), 1000000 * (1 - $degrade_quality)) / 1000000;
1718  }
1719 
1720 
1721  // write current sample
1722  $this->setSampleValue($sampleFloat, $currentBlock, $channel);
1723  }
1724  }
1725 
1726  return $this;
1727  }
1728 
1736  public function appendWav(WavFile $wav)
1737  {
1738  // basic checks
1739  if ($wav->getSampleRate() != $this->getSampleRate()) {
1740  throw new WavFileException("Sample rate for wav files do not match.");
1741  } elseif ($wav->getBitsPerSample() != $this->getBitsPerSample()) {
1742  throw new WavFileException("Bits per sample for wav files do not match.");
1743  } elseif ($wav->getNumChannels() != $this->getNumChannels()) {
1744  throw new WavFileException("Number of channels for wav files do not match.");
1745  }
1746 
1747  $this->_samples .= $wav->_samples;
1748  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1749 
1750  return $this;
1751  }
1752 
1761  public function mergeWav(WavFile $wav, $normalizeThreshold = null)
1762  {
1763  return $this->filter(array(
1764  WavFile::FILTER_MIX => $wav,
1765  WavFile::FILTER_NORMALIZE => $normalizeThreshold
1766  ));
1767  }
1768 
1774  public function insertSilence($duration = 1.0)
1775  {
1776  $numSamples = (int) ($this->getSampleRate() * abs($duration));
1777  $numChannels = $this->getNumChannels();
1778 
1779  $data = str_repeat(self::packSample($this->getZeroAmplitude(), $this->getBitsPerSample()), $numSamples * $numChannels);
1780  if ($duration >= 0) {
1781  $this->_samples .= $data;
1782  } else {
1783  $this->_samples = $data . $this->_samples;
1784  }
1785 
1786  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1787 
1788  return $this;
1789  }
1790 
1796  public function degrade($quality = 1.0)
1797  {
1798  return $this->filter(self::FILTER_DEGRADE, array(
1799  WavFile::FILTER_DEGRADE => $quality
1800  ));
1801  }
1802 
1809  public function generateNoise($duration = 1.0, $percent = 100)
1810  {
1811  $numChannels = $this->getNumChannels();
1812  $numSamples = $this->getSampleRate() * $duration;
1813  $minAmp = $this->getMinAmplitude();
1814  $maxAmp = $this->getMaxAmplitude();
1815  $bitDepth = $this->getBitsPerSample();
1816 
1817  for ($s = 0; $s < $numSamples; ++$s) {
1818  if ($bitDepth == 32) {
1819  $val = rand(-$percent * 10000, $percent * 10000) / 1000000;
1820  } else {
1821  $val = rand($minAmp, $maxAmp);
1822  $val = (int) ($val * $percent / 100);
1823  }
1824 
1825  $this->_samples .= str_repeat(self::packSample($val, $bitDepth), $numChannels);
1826  }
1827 
1828  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1829 
1830  return $this;
1831  }
1832 
1839  public function convertBitsPerSample($bitsPerSample)
1840  {
1841  if ($this->getBitsPerSample() == $bitsPerSample) {
1842  return $this;
1843  }
1844 
1845  $tempWav = new WavFile($this->getNumChannels(), $this->getSampleRate(), $bitsPerSample);
1846  $tempWav->filter(
1847  array(self::FILTER_MIX => $this),
1848  0,
1849  $this->getNumBlocks()
1850  );
1851 
1852  $this->setSamples() // implicit setDataSize(), setSize(), setActualSize(), setNumBlocks()
1853  ->setBitsPerSample($bitsPerSample); // implicit setValidBitsPerSample(), setAudioFormat(), setAudioSubFormat(), setFmtChunkSize(), setFactChunkSize(), setSize(), setActualSize(), setDataOffset(), setByteRate(), setBlockAlign(), setNumBlocks()
1854  $this->_samples = $tempWav->_samples;
1855  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1856 
1857  return $this;
1858  }
1859 
1860 
1861  /*%******************************************************************************************%*/
1862  // Miscellaneous methods
1863 
1867  public function displayInfo()
1868  {
1869  $s = "File Size: %u\n"
1870  . "Chunk Size: %u\n"
1871  . "fmt Subchunk Size: %u\n"
1872  . "Extended fmt Size: %u\n"
1873  . "fact Subchunk Size: %u\n"
1874  . "Data Offset: %u\n"
1875  . "Data Size: %u\n"
1876  . "Audio Format: %s\n"
1877  . "Audio SubFormat: %s\n"
1878  . "Channels: %u\n"
1879  . "Channel Mask: 0x%s\n"
1880  . "Sample Rate: %u\n"
1881  . "Bits Per Sample: %u\n"
1882  . "Valid Bits Per Sample: %u\n"
1883  . "Sample Block Size: %u\n"
1884  . "Number of Sample Blocks: %u\n"
1885  . "Byte Rate: %uBps\n";
1886 
1887  $s = sprintf(
1888  $s,
1889  $this->getActualSize(),
1890  $this->getChunkSize(),
1891  $this->getFmtChunkSize(),
1892  $this->getFmtExtendedSize(),
1893  $this->getFactChunkSize(),
1894  $this->getDataOffset(),
1895  $this->getDataSize(),
1896  $this->getAudioFormat() == self::WAVE_FORMAT_PCM ? 'PCM' : ($this->getAudioFormat() == self::WAVE_FORMAT_IEEE_FLOAT ? 'IEEE FLOAT' : 'EXTENSIBLE'),
1897  $this->getAudioSubFormat() == self::WAVE_SUBFORMAT_PCM ? 'PCM' : 'IEEE FLOAT',
1898  $this->getNumChannels(),
1899  dechex($this->getChannelMask()),
1900  $this->getSampleRate(),
1901  $this->getBitsPerSample(),
1902  $this->getValidBitsPerSample(),
1903  $this->getBlockAlign(),
1904  $this->getNumBlocks(),
1905  $this->getByteRate()
1906  );
1907 
1908  if (php_sapi_name() == 'cli') {
1909  return $s;
1910  } else {
1911  return nl2br($s);
1912  }
1913  }
1914 }
1915 
1916 
1917 /*%******************************************************************************************%*/
1918 // Exceptions
1919 
1924 {
1925 }
1926 
1931 {
1932 }
setSampleValue($sampleFloat, $blockNum, $channelNum)
Sets a float sample value for a specific sample block number and channel.
Definition: WavFile.php:1497
getSamples()
Definition: WavFile.php:816
const FILTER_MIX
Definition: WavFile.php:75
$_dataSize
Definition: WavFile.php:156
const SPEAKER_FRONT_RIGHT_OF_CENTER
Definition: WavFile.php:98
const SPEAKER_TOP_BACK_RIGHT
Definition: WavFile.php:108
getByteRate()
Definition: WavFile.php:800
closeWav()
Close a with openWav() previously opened wav file or free the buffer of setWavData().
Definition: WavFile.php:994
const SPEAKER_TOP_BACK_LEFT
Definition: WavFile.php:106
static packSampleBlock($samples, $bitDepth)
Packs an array of numeric channel samples to a binary sample block.
Definition: WavFile.php:396
displayInfo()
Output information about the wav object.
Definition: WavFile.php:1867
const WAVE_FORMAT_EXTENSIBLE
Definition: WavFile.php:118
WavFileException indicates an illegal state or argument in this class.
Definition: WavFile.php:1923
getSampleRate()
Definition: WavFile.php:703
getAudioFormat()
Definition: WavFile.php:603
setByteRate($byteRate=null)
Definition: WavFile.php:805
const SPEAKER_TOP_FRONT_LEFT
Definition: WavFile.php:103
getNumBlocks()
Definition: WavFile.php:784
getChunkSize()
Definition: WavFile.php:475
save($filename)
Save the wav data to a file.
Definition: WavFile.php:946
const SPEAKER_BACK_LEFT
Definition: WavFile.php:95
const SPEAKER_FRONT_LEFT
Definition: WavFile.php:91
const SPEAKER_TOP_FRONT_RIGHT
Definition: WavFile.php:105
static packSample($sample, $bitDepth)
Packs a single numeric sample to binary.
Definition: WavFile.php:335
const SPEAKER_FRONT_CENTER
Definition: WavFile.php:93
$_dataOffset
Definition: WavFile.php:165
readWav($readData=true)
Read wav file from a stream.
Definition: WavFile.php:1045
setFmtChunkSize($fmtChunkSize=null)
Definition: WavFile.php:502
WavFormatException indicates a malformed or unsupported wav file header.
Definition: WavFile.php:1930
const SPEAKER_SIDE_LEFT
Definition: WavFile.php:100
const FILTER_NORMALIZE
Definition: WavFile.php:78
const SPEAKER_BACK_RIGHT
Definition: WavFile.php:96
const MAX_SAMPLERATE
Definition: WavFile.php:87
setAudioFormat($audioFormat=null)
Definition: WavFile.php:608
getDataSize()
Definition: WavFile.php:563
$_numBlocks
Definition: WavFile.php:192
getBlockAlign()
Definition: WavFile.php:766
setFmtExtendedSize($fmtExtendedSize=null)
Definition: WavFile.php:521
setActualSize($actualSize=null)
Definition: WavFile.php:464
setWavData(&$data, $free=true)
Set the wav file data and properties from a wav file in a string.
Definition: WavFile.php:1011
const SPEAKER_SIDE_RIGHT
Definition: WavFile.php:101
$s
Definition: pwgen.php:45
getAudioSubFormat()
Definition: WavFile.php:634
getDataOffset()
Definition: WavFile.php:583
setChunkSize($chunkSize=null)
Definition: WavFile.php:480
const SPEAKER_DEFAULT
Channel Locations for ChannelMask.
Definition: WavFile.php:90
getFmtChunkSize()
Definition: WavFile.php:497
setDataOffset($dataOffset=null)
Definition: WavFile.php:588
getSampleValue($blockNum, $channelNum)
Get a float sample value for a specific sample block and channel number.
Definition: WavFile.php:1433
const WAVE_FORMAT_PCM
Definition: WavFile.php:112
appendWav(WavFile $wav)
Append a wav file to the current wav.
Definition: WavFile.php:1736
$_audioSubFormat
Definition: WavFile.php:171
setSampleBlock($sampleBlock, $blockNum)
Set a single sample block.
Definition: WavFile.php:1388
readWavData($dataOffset=0, $dataSize=null)
Read the wav data from the file into the buffer.
Definition: WavFile.php:1322
const FILTER_DEGRADE
Definition: WavFile.php:81
generateNoise($duration=1.0, $percent=100)
Generate noise at the end of the wav for the specified duration and volume.
Definition: WavFile.php:1809
openWav($filename, $readData=true)
Reads a wav header and data from a file.
Definition: WavFile.php:968
const SPEAKER_FRONT_LEFT_OF_CENTER
Definition: WavFile.php:97
setValidBitsPerSample($validBitsPerSample=null)
Definition: WavFile.php:750
const SPEAKER_TOP_BACK_CENTER
Definition: WavFile.php:107
getChannelMask()
Definition: WavFile.php:676
getMaxAmplitude()
Definition: WavFile.php:860
setChannelMask($channelMask=self::SPEAKER_DEFAULT)
Definition: WavFile.php:681
filter($filters, $blockOffset=0, $numBlocks=null)
Run samples through audio processing filters.
Definition: WavFile.php:1615
getValidBitsPerSample()
Definition: WavFile.php:745
static $LOOKUP_LOGBASE
Definition: WavFile.php:133
const WAVE_SUBFORMAT_PCM
Definition: WavFile.php:121
$_factChunkSize
Definition: WavFile.php:153
__toString()
Output the wav file headers and data.
Definition: WavFile.php:272
makeHeader()
Construct a wav header from this object.
Definition: WavFile.php:881
static unpackSampleBlock($sampleBlock, $bitDepth, $numChannels=null)
Unpacks a binary sample block to numeric values.
Definition: WavFile.php:373
getDataSubchunk()
Construct wav DATA chunk.
Definition: WavFile.php:925
setNumChannels($numChannels)
Definition: WavFile.php:659
$_bitsPerSample
Definition: WavFile.php:183
const SPEAKER_LOW_FREQUENCY
Definition: WavFile.php:94
__destruct()
Definition: WavFile.php:255
$_channelMask
Definition: WavFile.php:177
$_dataSize_fp
Definition: WavFile.php:159
$_byteRate
Definition: WavFile.php:195
__construct($numChannelsOrFileName=null, $sampleRateOrReadData=null, $bitsPerSample=null)
WavFile Constructor.
Definition: WavFile.php:221
$n
Definition: RandomTest.php:85
setBitsPerSample($bitsPerSample)
Definition: WavFile.php:728
const SPEAKER_ALL
Definition: WavFile.php:109
setDataSize($dataSize=null)
Definition: WavFile.php:568
$_numChannels
Definition: WavFile.php:174
static normalizeSample($sampleFloat, $threshold)
Normalizes a float audio sample.
Definition: WavFile.php:424
$filename
Definition: buildRTE.php:89
getZeroAmplitude()
Definition: WavFile.php:849
const WAVE_FORMAT_IEEE_FLOAT
Definition: WavFile.php:115
convertBitsPerSample($bitsPerSample)
Convert sample data to different bits per sample.
Definition: WavFile.php:1839
$_fmtExtendedSize
Definition: WavFile.php:150
getMinAmplitude()
Definition: WavFile.php:838
$_blockAlign
Definition: WavFile.php:189
$_validBitsPerSample
Definition: WavFile.php:186
setFactChunkSize($factChunkSize=null)
Definition: WavFile.php:545
getFactChunkSize()
Definition: WavFile.php:540
$_fmtChunkSize
Definition: WavFile.php:147
setAudioSubFormat($audioSubFormat=null)
Definition: WavFile.php:639
const SPEAKER_BACK_CENTER
Definition: WavFile.php:99
getNumChannels()
Definition: WavFile.php:654
$_dataSize_valid
Definition: WavFile.php:162
const MAX_CHANNEL
Definition: WavFile.php:84
setNumBlocks($numBlocks=null)
Definition: WavFile.php:789
const SPEAKER_FRONT_RIGHT
Definition: WavFile.php:92
$_actualSize
Definition: WavFile.php:141
setSampleRate($sampleRate)
Definition: WavFile.php:708
$i
Definition: disco.tpl.php:19
$_sampleRate
Definition: WavFile.php:180
insertSilence($duration=1.0)
Add silence to the wav file.
Definition: WavFile.php:1774
getActualSize()
Definition: WavFile.php:459
readWavHeader()
Parse a wav header.
Definition: WavFile.php:1072
$_chunkSize
Definition: WavFile.php:144
setSamples(&$samples='')
Definition: WavFile.php:821
const WAVE_SUBFORMAT_IEEE_FLOAT
Definition: WavFile.php:124
__clone()
Definition: WavFile.php:262
const SPEAKER_TOP_CENTER
Definition: WavFile.php:102
const SPEAKER_TOP_FRONT_CENTER
Definition: WavFile.php:104
setBlockAlign($blockAlign=null)
Definition: WavFile.php:771
static unpackSample($sampleBinary, $bitDepth=null)
Unpacks a single binary sample to numeric value.
Definition: WavFile.php:289
getBitsPerSample()
Definition: WavFile.php:723
mergeWav(WavFile $wav, $normalizeThreshold=null)
Mix 2 wav files together.
Definition: WavFile.php:1761
degrade($quality=1.0)
Degrade the quality of the wav file by introducing random noise.
Definition: WavFile.php:1796
$_audioFormat
Definition: WavFile.php:168
$data
Definition: bench.php:6
getSampleBlock($blockNum)
Return a single sample block from the file.
Definition: WavFile.php:1363
getFmtExtendedSize()
Definition: WavFile.php:516