Gentics Portal.Node PHP API
 All Classes Namespaces Functions Variables Pages
WavFile.php
1 <?php
2 
3 // error_reporting(E_ALL); ini_set('display_errors', 1); // uncomment this line for debugging
4 
5 /**
6 * Project: PHPWavUtils: Classes for creating, reading, and manipulating WAV files in PHP<br />
7 * File: WavFile.php<br />
8 *
9 * Copyright (c) 2012, Drew Phillips
10 * All rights reserved.
11 *
12 * Redistribution and use in source and binary forms, with or without modification,
13 * are permitted provided that the following conditions are met:
14 *
15 * - Redistributions of source code must retain the above copyright notice,
16 * this list of conditions and the following disclaimer.
17 * - Redistributions in binary form must reproduce the above copyright notice,
18 * this list of conditions and the following disclaimer in the documentation
19 * and/or other materials provided with the distribution.
20 *
21 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
25 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 * POSSIBILITY OF SUCH DAMAGE.
32 *
33 * Any modifications to the library should be indicated clearly in the source code
34 * to inform users that the changes are not a part of the original software.<br /><br />
35 *
36 * @copyright 2012 Drew Phillips
37 * @author Drew Phillips <drew@drew-phillips.com>
38 * @author Paul Voegler <http://www.voegler.eu/>
39 * @version 1.0RC1 (April 2012)
40 * @package PHPWavUtils
41 * @license BSD License
42 *
43 * Changelog:
44 *
45 * 1.0 RC1 (4/20/2012)
46 * - Initial release candidate
47 * - Supports 8, 16, 24, 32 bit PCM, 32-bit IEEE FLOAT, Extensible Format
48 * - Support for 18 channels of audio
49 * - Ability to read an offset from a file to reduce memory footprint with large files
50 * - Single-pass audio filter processing
51 * - Highly accurate and efficient mix and normalization filters (http://www.voegler.eu/pub/audio/)
52 * - Utility filters for degrading audio, and inserting silence
53 *
54 * 0.6 (4/12/2012)
55 * - Support 8, 16, 24, 32 bit and PCM float (Paul Voegler)
56 * - Add normalize filter, misc improvements and fixes (Paul Voegler)
57 * - Normalize parameters to filter() to use filter constants as array indices
58 * - Add option to mix filter to loop the target file if the source is longer
59 *
60 * 0.5 (4/3/2012)
61 * - Fix binary pack routine (Paul Voegler)
62 * - Add improved mixing function (Paul Voegler)
63 *
64 */
65 
66 class WavFile
67 {
68  /*%******************************************************************************************%*/
69  // Class constants
70 
71  /** @var int Filter flag for mixing two files */
72  const FILTER_MIX = 0x01;
73 
74  /** @var int Filter flag for normalizing audio data */
75  const FILTER_NORMALIZE = 0x02;
76 
77  /** @var int Filter flag for degrading audio data */
78  const FILTER_DEGRADE = 0x04;
79 
80  /** @var int Maximum number of channels */
81  const MAX_CHANNEL = 18;
82 
83  /** @var int Maximum sample rate */
84  const MAX_SAMPLERATE = 192000;
85 
86  /** Channel Locations for ChannelMask */
87  const SPEAKER_DEFAULT = 0x000000;
88  const SPEAKER_FRONT_LEFT = 0x000001;
89  const SPEAKER_FRONT_RIGHT = 0x000002;
90  const SPEAKER_FRONT_CENTER = 0x000004;
91  const SPEAKER_LOW_FREQUENCY = 0x000008;
92  const SPEAKER_BACK_LEFT = 0x000010;
93  const SPEAKER_BACK_RIGHT = 0x000020;
94  const SPEAKER_FRONT_LEFT_OF_CENTER = 0x000040;
95  const SPEAKER_FRONT_RIGHT_OF_CENTER = 0x000080;
96  const SPEAKER_BACK_CENTER = 0x000100;
97  const SPEAKER_SIDE_LEFT = 0x000200;
98  const SPEAKER_SIDE_RIGHT = 0x000400;
99  const SPEAKER_TOP_CENTER = 0x000800;
100  const SPEAKER_TOP_FRONT_LEFT = 0x001000;
101  const SPEAKER_TOP_FRONT_CENTER = 0x002000;
102  const SPEAKER_TOP_FRONT_RIGHT = 0x004000;
103  const SPEAKER_TOP_BACK_LEFT = 0x008000;
104  const SPEAKER_TOP_BACK_CENTER = 0x010000;
105  const SPEAKER_TOP_BACK_RIGHT = 0x020000;
106  const SPEAKER_ALL = 0x03FFFF;
107 
108  /** @var int PCM Audio Format */
109  const WAVE_FORMAT_PCM = 0x0001;
110 
111  /** @var int IEEE FLOAT Audio Format */
112  const WAVE_FORMAT_IEEE_FLOAT = 0x0003;
113 
114  /** @var int EXTENSIBLE Audio Format - actual audio format defined by SubFormat */
115  const WAVE_FORMAT_EXTENSIBLE = 0xFFFE;
116 
117  /** @var string PCM Audio Format SubType - LE hex representation of GUID {00000001-0000-0010-8000-00AA00389B71} */
118  const WAVE_SUBFORMAT_PCM = "0100000000001000800000aa00389b71";
119 
120  /** @var string IEEE FLOAT Audio Format SubType - LE hex representation of GUID {00000003-0000-0010-8000-00AA00389B71} */
121  const WAVE_SUBFORMAT_IEEE_FLOAT = "0300000000001000800000aa00389b71";
122 
123 
124  /*%******************************************************************************************%*/
125  // Properties
126 
127  /** @var array Log base modifier lookup table for a given threshold (in 0.05 steps) used by normalizeSample.
128  * Adjusts the slope (1st derivative) of the log function at the threshold to 1 for a smooth transition
129  * from linear to logarithmic amplitude output. */
130  protected static $LOOKUP_LOGBASE = array(
131  2.513, 2.667, 2.841, 3.038, 3.262,
132  3.520, 3.819, 4.171, 4.589, 5.093,
133  5.711, 6.487, 7.483, 8.806, 10.634,
134  13.302, 17.510, 24.970, 41.155, 96.088
135  );
136 
137  /** @var int The actual physical file size */
138  protected $_actualSize;
139 
140  /** @var int The size of the file in RIFF header */
141  protected $_chunkSize;
142 
143  /** @var int The size of the "fmt " chunk */
144  protected $_fmtChunkSize;
145 
146  /** @var int The size of the extended "fmt " data */
147  protected $_fmtExtendedSize;
148 
149  /** @var int The size of the "fact" chunk */
150  protected $_factChunkSize;
151 
152  /** @var int Size of the data chunk */
153  protected $_dataSize;
154 
155  /** @var int Size of the data chunk in the opened wav file */
156  protected $_dataSize_fp;
157 
158  /** @var int Does _dataSize really reflect strlen($_samples)? Case when a wav file is read with readData = false */
159  protected $_dataSize_valid;
160 
161  /** @var int Starting offset of data chunk */
162  protected $_dataOffset;
163 
164  /** @var int The audio format - WavFile::WAVE_FORMAT_* */
165  protected $_audioFormat;
166 
167  /** @var int The audio subformat - WavFile::WAVE_SUBFORMAT_* */
168  protected $_audioSubFormat;
169 
170  /** @var int Number of channels in the audio file */
171  protected $_numChannels;
172 
173  /** @var int The channel mask */
174  protected $_channelMask;
175 
176  /** @var int Samples per second */
177  protected $_sampleRate;
178 
179  /** @var int Number of bits per sample */
180  protected $_bitsPerSample;
181 
182  /** @var int Number of valid bits per sample */
183  protected $_validBitsPerSample;
184 
185  /** @var int NumChannels * BitsPerSample/8 */
186  protected $_blockAlign;
187 
188  /** @var int Number of sample blocks */
189  protected $_numBlocks;
190 
191  /** @var int Bytes per second */
192  protected $_byteRate;
193 
194  /** @var string Binary string of samples */
195  protected $_samples;
196 
197  /** @var resource The file pointer used for reading wavs from file or memory */
198  protected $_fp;
199 
200 
201  /*%******************************************************************************************%*/
202  // Special methods
203 
204  /**
205  * WavFile Constructor.
206  *
207  * <code>
208  * $wav1 = new WavFile(2, 44100, 16); // new wav with 2 channels, at 44100 samples/sec and 16 bits per sample
209  * $wav2 = new WavFile('./audio/sound.wav'); // open and read wav file
210  * </code>
211  *
212  * @param string|int $numChannelsOrFileName (Optional) If string, the filename of the wav file to open. The number of channels otherwise. Defaults to 1.
213  * @param int|bool $sampleRateOrReadData (Optional) If opening a file and boolean, decides whether to read the data chunk or not. Defaults to true. The sample rate in samples per second otherwise. 8000 = standard telephone, 16000 = wideband telephone, 32000 = FM radio and 44100 = CD quality. Defaults to 8000.
214  * @param int $bitsPerSample (Optional) The number of bits per sample. Has to be 8, 16 or 24 for PCM audio or 32 for IEEE FLOAT audio. 8 = telephone, 16 = CD and 24 or 32 = studio quality. Defaults to 8.
215  * @throws WavFormatException
216  * @throws WavFileException
217  */
218  public function __construct($numChannelsOrFileName = null, $sampleRateOrReadData = null, $bitsPerSample = null)
219  {
220  $this->_actualSize = 44;
221  $this->_chunkSize = 36;
222  $this->_fmtChunkSize = 16;
223  $this->_fmtExtendedSize = 0;
224  $this->_factChunkSize = 0;
225  $this->_dataSize = 0;
226  $this->_dataSize_fp = 0;
227  $this->_dataSize_valid = true;
228  $this->_dataOffset = 44;
229  $this->_audioFormat = self::WAVE_FORMAT_PCM;
230  $this->_audioSubFormat = null;
231  $this->_numChannels = 1;
232  $this->_channelMask = self::SPEAKER_DEFAULT;
233  $this->_sampleRate = 8000;
234  $this->_bitsPerSample = 8;
235  $this->_validBitsPerSample = 8;
236  $this->_blockAlign = 1;
237  $this->_numBlocks = 0;
238  $this->_byteRate = 8000;
239  $this->_samples = '';
240  $this->_fp = null;
241 
242 
243  if (is_string($numChannelsOrFileName)) {
244  $this->openWav($numChannelsOrFileName, is_bool($sampleRateOrReadData) ? $sampleRateOrReadData : true);
245 
246  } else {
247  $this->setNumChannels(is_null($numChannelsOrFileName) ? 1 : $numChannelsOrFileName)
248  ->setSampleRate(is_null($sampleRateOrReadData) ? 8000 : $sampleRateOrReadData)
249  ->setBitsPerSample(is_null($bitsPerSample) ? 8 : $bitsPerSample);
250  }
251  }
252 
253  public function __destruct() {
254  if (is_resource($this->_fp)) $this->closeWav();
255  }
256 
257  public function __clone() {
258  $this->_fp = null;
259  }
260 
261  /**
262  * Output the wav file headers and data.
263  *
264  * @return string The encoded file.
265  */
266  public function __toString()
267  {
268  return $this->makeHeader() .
269  $this->getDataSubchunk();
270  }
271 
272 
273  /*%******************************************************************************************%*/
274  // Static methods
275 
276  /**
277  * Unpacks a single binary sample to numeric value.
278  *
279  * @param string $sampleBinary (Required) The sample to decode.
280  * @param int $bitDepth (Optional) The bits per sample to decode. If omitted, derives it from the length of $sampleBinary.
281  * @return int|float The numeric sample value. Float for 32-bit samples. Returns null for unsupported bit depths.
282  */
283  public static function unpackSample($sampleBinary, $bitDepth = null)
284  {
285  if ($bitDepth === null) {
286  $bitDepth = strlen($sampleBinary) * 8;
287  }
288 
289  switch ($bitDepth) {
290  case 8:
291  // unsigned char
292  return ord($sampleBinary);
293 
294  case 16:
295  // signed short, little endian
296  $data = unpack('v', $sampleBinary);
297  $sample = $data[1];
298  if ($sample >= 0x8000) {
299  $sample -= 0x10000;
300  }
301  return $sample;
302 
303  case 24:
304  // 3 byte packed signed integer, little endian
305  $data = unpack('C3', $sampleBinary);
306  $sample = $data[1] | ($data[2] << 8) | ($data[3] << 16);
307  if ($sample >= 0x800000) {
308  $sample -= 0x1000000;
309  }
310  return $sample;
311 
312  case 32:
313  // 32-bit float
314  $data = unpack('f', $sampleBinary);
315  return $data[1];
316 
317  default:
318  return null;
319  }
320  }
321 
322  /**
323  * Packs a single numeric sample to binary.
324  *
325  * @param int|float $sample (Required) The sample to encode. Has to be within valid range for $bitDepth. Float values only for 32 bits.
326  * @param int $bitDepth (Required) The bits per sample to encode with.
327  * @return string The encoded binary sample. Returns null for unsupported bit depths.
328  */
329  public static function packSample($sample, $bitDepth)
330  {
331  switch ($bitDepth) {
332  case 8:
333  // unsigned char
334  return chr($sample);
335 
336  case 16:
337  // signed short, little endian
338  if ($sample < 0) {
339  $sample += 0x10000;
340  }
341  return pack('v', $sample);
342 
343  case 24:
344  // 3 byte packed signed integer, little endian
345  if ($sample < 0) {
346  $sample += 0x1000000;
347  }
348  return pack('C3', $sample & 0xff, ($sample >> 8) & 0xff, ($sample >> 16) & 0xff);
349 
350  case 32:
351  // 32-bit float
352  return pack('f', $sample);
353 
354  default:
355  return null;
356  }
357  }
358 
359  /**
360  * Unpacks a binary sample block to numeric values.
361  *
362  * @param string $sampleBlock (Required) The binary sample block (all channels).
363  * @param int $bitDepth (Required) The bits per sample to decode.
364  * @param int $numChannels (Optional) The number of channels to decode. If omitted, derives it from the length of $sampleBlock and $bitDepth.
365  * @return array The sample values as an array of integers of floats for 32 bits. First channel is array index 1.
366  */
367  public static function unpackSampleBlock($sampleBlock, $bitDepth, $numChannels = null) {
368  $sampleBytes = $bitDepth / 8;
369  if ($numChannels === null) {
370  $numChannels = strlen($sampleBlock) / $sampleBytes;
371  }
372 
373  $samples = array();
374  for ($i = 0; $i < $numChannels; $i++) {
375  $sampleBinary = substr($sampleBlock, $i * $sampleBytes, $sampleBytes);
376  $samples[$i + 1] = self::unpackSample($sampleBinary, $bitDepth);
377  }
378 
379  return $samples;
380  }
381 
382  /**
383  * Packs an array of numeric channel samples to a binary sample block.
384  *
385  * @param array $samples (Required) The array of channel sample values. Expects float values for 32 bits and integer otherwise.
386  * @param int $bitDepth (Required) The bits per sample to encode with.
387  * @return string The encoded binary sample block.
388  */
389  public static function packSampleBlock($samples, $bitDepth) {
390  $sampleBlock = '';
391  foreach($samples as $sample) {
392  $sampleBlock .= self::packSample($sample, $bitDepth);
393  }
394 
395  return $sampleBlock;
396  }
397 
398  /**
399  * Normalizes a float audio sample. Maximum input range assumed for compression is [-2, 2].
400  * See http://www.voegler.eu/pub/audio/ for more information.
401  *
402  * @param float $sampleFloat (Required) The float sample to normalize.
403  * @param float $threshold (Required) The threshold or gain factor for normalizing the amplitude. <ul>
404  * <li> >= 1 - Normalize by multiplying by the threshold (boost - positive gain). <br />
405  * A value of 1 in effect means no normalization (and results in clipping). </li>
406  * <li> <= -1 - Normalize by dividing by the the absolute value of threshold (attenuate - negative gain). <br />
407  * A factor of 2 (-2) is about 6dB reduction in volume.</li>
408  * <li> [0, 1) - (open inverval - not including 1) - The threshold
409  * above which amplitudes are comressed logarithmically. <br />
410  * e.g. 0.6 to leave amplitudes up to 60% "as is" and compress above. </li>
411  * <li> (-1, 0) - (open inverval - not including -1 and 0) - The threshold
412  * above which amplitudes are comressed linearly. <br />
413  * e.g. -0.6 to leave amplitudes up to 60% "as is" and compress above. </li></ul>
414  * @return float The normalized sample.
415  **/
416  public static function normalizeSample($sampleFloat, $threshold) {
417  // apply positive gain
418  if ($threshold >= 1) {
419  return $sampleFloat * $threshold;
420  }
421 
422  // apply negative gain
423  if ($threshold <= -1) {
424  return $sampleFloat / -$threshold;
425  }
426 
427  $sign = $sampleFloat < 0 ? -1 : 1;
428  $sampleAbs = abs($sampleFloat);
429 
430  // logarithmic compression
431  if ($threshold >= 0 && $threshold < 1 && $sampleAbs > $threshold) {
432  $loga = self::$LOOKUP_LOGBASE[(int)($threshold * 20)]; // log base modifier
433  return $sign * ($threshold + (1 - $threshold) * log(1 + $loga * ($sampleAbs - $threshold) / (2 - $threshold)) / log(1 + $loga));
434  }
435 
436  // linear compression
437  $thresholdAbs = abs($threshold);
438  if ($threshold > -1 && $threshold < 0 && $sampleAbs > $thresholdAbs) {
439  return $sign * ($thresholdAbs + (1 - $thresholdAbs) / (2 - $thresholdAbs) * ($sampleAbs - $thresholdAbs));
440  }
441 
442  // else ?
443  return $sampleFloat;
444  }
445 
446 
447  /*%******************************************************************************************%*/
448  // Getter and Setter methods for properties
449 
450  public function getActualSize() {
451  return $this->_actualSize;
452  }
453 
454  protected function setActualSize($actualSize = null) {
455  if (is_null($actualSize)) {
456  $this->_actualSize = 8 + $this->_chunkSize; // + "RIFF" header (ID + size)
457  } else {
458  $this->_actualSize = $actualSize;
459  }
460 
461  return $this;
462  }
463 
464  public function getChunkSize() {
465  return $this->_chunkSize;
466  }
467 
468  protected function setChunkSize($chunkSize = null) {
469  if (is_null($chunkSize)) {
470  $this->_chunkSize = 4 + // "WAVE" chunk
471  8 + $this->_fmtChunkSize + // "fmt " subchunk
472  ($this->_factChunkSize > 0 ? 8 + $this->_factChunkSize : 0) + // "fact" subchunk
473  8 + $this->_dataSize + // "data" subchunk
474  ($this->_dataSize & 1); // padding byte
475  } else {
476  $this->_chunkSize = $chunkSize;
477  }
478 
479  $this->setActualSize();
480 
481  return $this;
482  }
483 
484  public function getFmtChunkSize() {
485  return $this->_fmtChunkSize;
486  }
487 
488  protected function setFmtChunkSize($fmtChunkSize = null) {
489  if (is_null($fmtChunkSize)) {
490  $this->_fmtChunkSize = 16 + $this->_fmtExtendedSize;
491  } else {
492  $this->_fmtChunkSize = $fmtChunkSize;
493  }
494 
495  $this->setChunkSize() // implicit setActualSize()
496  ->setDataOffset();
497 
498  return $this;
499  }
500 
501  public function getFmtExtendedSize() {
502  return $this->_fmtExtendedSize;
503  }
504 
505  protected function setFmtExtendedSize($fmtExtendedSize = null) {
506  if (is_null($fmtExtendedSize)) {
507  if ($this->_audioFormat == self::WAVE_FORMAT_EXTENSIBLE) {
508  $this->_fmtExtendedSize = 2 + 22; // extension size for WAVE_FORMAT_EXTENSIBLE
509  } elseif ($this->_audioFormat != self::WAVE_FORMAT_PCM) {
510  $this->_fmtExtendedSize = 2 + 0; // empty extension
511  } else {
512  $this->_fmtExtendedSize = 0; // no extension, only for WAVE_FORMAT_PCM
513  }
514  } else {
515  $this->_fmtExtendedSize = $fmtExtendedSize;
516  }
517 
518  $this->setFmtChunkSize(); // implicit setSize(), setActualSize(), setDataOffset()
519 
520  return $this;
521  }
522 
523  public function getFactChunkSize() {
524  return $this->_factChunkSize;
525  }
526 
527  protected function setFactChunkSize($factChunkSize = null) {
528  if (is_null($factChunkSize)) {
529  if ($this->_audioFormat != self::WAVE_FORMAT_PCM) {
530  $this->_factChunkSize = 4;
531  } else {
532  $this->_factChunkSize = 0;
533  }
534  } else {
535  $this->_factChunkSize = $factChunkSize;
536  }
537 
538  $this->setChunkSize() // implicit setActualSize()
539  ->setDataOffset();
540 
541  return $this;
542  }
543 
544  public function getDataSize() {
545  return $this->_dataSize;
546  }
547 
548  protected function setDataSize($dataSize = null) {
549  if (is_null($dataSize)) {
550  $this->_dataSize = strlen($this->_samples);
551  } else {
552  $this->_dataSize = $dataSize;
553  }
554 
555  $this->setChunkSize() // implicit setActualSize()
556  ->setNumBlocks();
557  $this->_dataSize_valid = true;
558 
559  return $this;
560  }
561 
562  public function getDataOffset() {
563  return $this->_dataOffset;
564  }
565 
566  protected function setDataOffset($dataOffset = null) {
567  if (is_null($dataOffset)) {
568  $this->_dataOffset = 8 + // "RIFF" header (ID + size)
569  4 + // "WAVE" chunk
570  8 + $this->_fmtChunkSize + // "fmt " subchunk
571  ($this->_factChunkSize > 0 ? 8 + $this->_factChunkSize : 0) + // "fact" subchunk
572  8; // "data" subchunk
573  } else {
574  $this->_dataOffset = $dataOffset;
575  }
576 
577  return $this;
578  }
579 
580  public function getAudioFormat() {
581  return $this->_audioFormat;
582  }
583 
584  protected function setAudioFormat($audioFormat = null) {
585  if (is_null($audioFormat)) {
586  if (($this->_bitsPerSample <= 16 || $this->_bitsPerSample == 32)
587  && $this->_validBitsPerSample == $this->_bitsPerSample
588  && $this->_channelMask == self::SPEAKER_DEFAULT
589  && $this->_numChannels <= 2) {
590  if ($this->_bitsPerSample <= 16) {
591  $this->_audioFormat = self::WAVE_FORMAT_PCM;
592  } else {
593  $this->_audioFormat = self::WAVE_FORMAT_IEEE_FLOAT;
594  }
595  } else {
596  $this->_audioFormat = self::WAVE_FORMAT_EXTENSIBLE;
597  }
598  } else {
599  $this->_audioFormat = $audioFormat;
600  }
601 
602  $this->setAudioSubFormat()
603  ->setFactChunkSize() // implicit setSize(), setActualSize(), setDataOffset()
604  ->setFmtExtendedSize(); // implicit setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
605 
606  return $this;
607  }
608 
609  public function getAudioSubFormat() {
610  return $this->_audioSubFormat;
611  }
612 
613  protected function setAudioSubFormat($audioSubFormat = null) {
614  if (is_null($audioSubFormat)) {
615  if ($this->_bitsPerSample == 32) {
616  $this->_audioSubFormat = self::WAVE_SUBFORMAT_IEEE_FLOAT; // 32 bits are IEEE FLOAT in this class
617  } else {
618  $this->_audioSubFormat = self::WAVE_SUBFORMAT_PCM; // 8, 16 and 24 bits are PCM in this class
619  }
620  } else {
621  $this->_audioSubFormat = $audioSubFormat;
622  }
623 
624  return $this;
625  }
626 
627  public function getNumChannels() {
628  return $this->_numChannels;
629  }
630 
631  public function setNumChannels($numChannels) {
632  if ($numChannels < 1 || $numChannels > self::MAX_CHANNEL) {
633  throw new WavFileException('Unsupported number of channels. Only up to ' . self::MAX_CHANNEL . ' channels are supported.');
634  } elseif ($this->_samples !== '') {
635  trigger_error('Wav already has sample data. Changing the number of channels does not convert and may corrupt the data.', E_USER_NOTICE);
636  }
637 
638  $this->_numChannels = (int)$numChannels;
639 
640  $this->setAudioFormat() // implicit setAudioSubFormat(), setFactChunkSize(), setFmtExtendedSize(), setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
641  ->setByteRate()
642  ->setBlockAlign(); // implicit setNumBlocks()
643 
644  return $this;
645  }
646 
647  public function getChannelMask() {
648  return $this->_channelMask;
649  }
650 
651  public function setChannelMask($channelMask = self::SPEAKER_DEFAULT) {
652  if ($channelMask != 0) {
653  // count number of set bits - Hamming weight
654  $c = (int)$channelMask;
655  $n = 0;
656  while ($c > 0) {
657  $n += $c & 1;
658  $c >>= 1;
659  }
660  if ($n != $this->_numChannels || (((int)$channelMask | self::SPEAKER_ALL) != self::SPEAKER_ALL)) {
661  throw new WavFileException('Invalid channel mask. The number of channels does not match the number of locations in the mask.');
662  }
663  }
664 
665  $this->_channelMask = (int)$channelMask;
666 
667  $this->setAudioFormat(); // implicit setAudioSubFormat(), setFactChunkSize(), setFmtExtendedSize(), setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
668 
669  return $this;
670  }
671 
672  public function getSampleRate() {
673  return $this->_sampleRate;
674  }
675 
676  public function setSampleRate($sampleRate) {
677  if ($sampleRate < 1 || $sampleRate > self::MAX_SAMPLERATE) {
678  throw new WavFileException('Invalid sample rate.');
679  } elseif ($this->_samples !== '') {
680  trigger_error('Wav already has sample data. Changing the sample rate does not convert the data and may yield undesired results.', E_USER_NOTICE);
681  }
682 
683  $this->_sampleRate = (int)$sampleRate;
684 
685  $this->setByteRate();
686 
687  return $this;
688  }
689 
690  public function getBitsPerSample() {
691  return $this->_bitsPerSample;
692  }
693 
694  public function setBitsPerSample($bitsPerSample) {
695  if (!in_array($bitsPerSample, array(8, 16, 24, 32))) {
696  throw new WavFileException('Unsupported bits per sample. Only 8, 16, 24 and 32 bits are supported.');
697  } elseif ($this->_samples !== '') {
698  trigger_error('Wav already has sample data. Changing the bits per sample does not convert and may corrupt the data.', E_USER_NOTICE);
699  }
700 
701  $this->_bitsPerSample = (int)$bitsPerSample;
702 
703  $this->setValidBitsPerSample() // implicit setAudioFormat(), setAudioSubFormat(), setFmtChunkSize(), setFactChunkSize(), setSize(), setActualSize(), setDataOffset()
704  ->setByteRate()
705  ->setBlockAlign(); // implicit setNumBlocks()
706 
707  return $this;
708  }
709 
710  public function getValidBitsPerSample() {
711  return $this->_validBitsPerSample;
712  }
713 
714  protected function setValidBitsPerSample($validBitsPerSample = null) {
715  if (is_null($validBitsPerSample)) {
716  $this->_validBitsPerSample = $this->_bitsPerSample;
717  } else {
718  if ($validBitsPerSample < 1 || $validBitsPerSample > $this->_bitsPerSample) {
719  throw new WavFileException('ValidBitsPerSample cannot be greater than BitsPerSample.');
720  }
721  $this->_validBitsPerSample = (int)$validBitsPerSample;
722  }
723 
724  $this->setAudioFormat(); // implicit setAudioSubFormat(), setFactChunkSize(), setFmtExtendedSize(), setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
725 
726  return $this;
727  }
728 
729  public function getBlockAlign() {
730  return $this->_blockAlign;
731  }
732 
733  protected function setBlockAlign($blockAlign = null) {
734  if (is_null($blockAlign)) {
735  $this->_blockAlign = $this->_numChannels * $this->_bitsPerSample / 8;
736  } else {
737  $this->_blockAlign = $blockAlign;
738  }
739 
740  $this->setNumBlocks();
741 
742  return $this;
743  }
744 
745  public function getNumBlocks()
746  {
747  return $this->_numBlocks;
748  }
749 
750  protected function setNumBlocks($numBlocks = null) {
751  if (is_null($numBlocks)) {
752  $this->_numBlocks = (int)($this->_dataSize / $this->_blockAlign); // do not count incomplete sample blocks
753  } else {
754  $this->_numBlocks = $numBlocks;
755  }
756 
757  return $this;
758  }
759 
760  public function getByteRate() {
761  return $this->_byteRate;
762  }
763 
764  protected function setByteRate($byteRate = null) {
765  if (is_null($byteRate)) {
766  $this->_byteRate = $this->_sampleRate * $this->_numChannels * $this->_bitsPerSample / 8;
767  } else {
768  $this->_byteRate = $byteRate;
769  }
770 
771  return $this;
772  }
773 
774  public function getSamples() {
775  return $this->_samples;
776  }
777 
778  public function setSamples(&$samples = '') {
779  if (strlen($samples) % $this->_blockAlign != 0) {
780  throw new WavFileException('Incorrect samples size. Has to be a multiple of BlockAlign.');
781  }
782 
783  $this->_samples = $samples;
784 
785  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
786 
787  return $this;
788  }
789 
790 
791  /*%******************************************************************************************%*/
792  // Getters
793 
794  public function getMinAmplitude()
795  {
796  if ($this->_bitsPerSample == 8) {
797  return 0;
798  } elseif ($this->_bitsPerSample == 32) {
799  return -1.0;
800  } else {
801  return -(1 << ($this->_bitsPerSample - 1));
802  }
803  }
804 
805  public function getZeroAmplitude()
806  {
807  if ($this->_bitsPerSample == 8) {
808  return 0x80;
809  } elseif ($this->_bitsPerSample == 32) {
810  return 0.0;
811  } else {
812  return 0;
813  }
814  }
815 
816  public function getMaxAmplitude()
817  {
818  if($this->_bitsPerSample == 8) {
819  return 0xFF;
820  } elseif($this->_bitsPerSample == 32) {
821  return 1.0;
822  } else {
823  return (1 << ($this->_bitsPerSample - 1)) - 1;
824  }
825  }
826 
827 
828  /*%******************************************************************************************%*/
829  // Wave file methods
830 
831  /**
832  * Construct a wav header from this object. Includes "fact" chunk in necessary.
833  * http://www-mmsp.ece.mcgill.ca/documents/audioformats/wave/wave.html
834  *
835  * @return string The RIFF header data.
836  */
837  public function makeHeader()
838  {
839  // reset and recalculate
840  $this->setAudioFormat(); // implicit setAudioSubFormat(), setFactChunkSize(), setFmtExtendedSize(), setFmtChunkSize(), setSize(), setActualSize(), setDataOffset()
841  $this->setNumBlocks();
842 
843  // RIFF header
844  $header = pack('N', 0x52494646); // ChunkID - "RIFF"
845  $header .= pack('V', $this->getChunkSize()); // ChunkSize
846  $header .= pack('N', 0x57415645); // Format - "WAVE"
847 
848  // "fmt " subchunk
849  $header .= pack('N', 0x666d7420); // SubchunkID - "fmt "
850  $header .= pack('V', $this->getFmtChunkSize()); // SubchunkSize
851  $header .= pack('v', $this->getAudioFormat()); // AudioFormat
852  $header .= pack('v', $this->getNumChannels()); // NumChannels
853  $header .= pack('V', $this->getSampleRate()); // SampleRate
854  $header .= pack('V', $this->getByteRate()); // ByteRate
855  $header .= pack('v', $this->getBlockAlign()); // BlockAlign
856  $header .= pack('v', $this->getBitsPerSample()); // BitsPerSample
857  if($this->getFmtExtendedSize() == 24) {
858  $header .= pack('v', 22); // extension size = 24 bytes, cbSize: 24 - 2 = 22 bytes
859  $header .= pack('v', $this->getValidBitsPerSample()); // ValidBitsPerSample
860  $header .= pack('V', $this->getChannelMask()); // ChannelMask
861  $header .= pack('H32', $this->getAudioSubFormat()); // SubFormat
862  } elseif ($this->getFmtExtendedSize() == 2) {
863  $header .= pack('v', 0); // extension size = 2 bytes, cbSize: 2 - 2 = 0 bytes
864  }
865 
866  // "fact" subchunk
867  if ($this->getFactChunkSize() == 4) {
868  $header .= pack('N', 0x66616374); // SubchunkID - "fact"
869  $header .= pack('V', 4); // SubchunkSize
870  $header .= pack('V', $this->getNumBlocks()); // SampleLength (per channel)
871  }
872 
873  return $header;
874  }
875 
876  /**
877  * Construct wav DATA chunk.
878  *
879  * @return string The DATA header and chunk.
880  */
881  public function getDataSubchunk()
882  {
883  // check preconditions
884  if (!$this->_dataSize_valid) {
885  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
886  }
887 
888 
889  // create subchunk
890  return pack('N', 0x64617461) . // SubchunkID - "data"
891  pack('V', $this->getDataSize()) . // SubchunkSize
892  $this->_samples . // Subchunk data
893  ($this->getDataSize() & 1 ? chr(0) : ''); // padding byte
894  }
895 
896  /**
897  * Save the wav data to a file.
898  *
899  * @param string $filename (Required) The file path to save the wav to.
900  * @throws WavFileException
901  */
902  public function save($filename)
903  {
904  $fp = @fopen($filename, 'w+b');
905  if (!is_resource($fp)) {
906  throw new WavFileException('Failed to open "' . $filename . '" for writing.');
907  }
908 
909  fwrite($fp, $this->makeHeader());
910  fwrite($fp, $this->getDataSubchunk());
911  fclose($fp);
912 
913  return $this;
914  }
915 
916  /**
917  * Reads a wav header and data from a file.
918  *
919  * @param string $filename (Required) The path to the wav file to read.
920  * @param bool $readData (Optional) If true, also read the data chunk.
921  * @throws WavFormatException
922  * @throws WavFileException
923  */
924  public function openWav($filename, $readData = true)
925  {
926  // check preconditions
927  if (!file_exists($filename)) {
928  throw new WavFileException('Failed to open "' . $filename . '". File not found.');
929  } elseif (!is_readable($filename)) {
930  throw new WavFileException('Failed to open "' . $filename . '". File is not readable.');
931  } elseif (is_resource($this->_fp)) {
932  $this->closeWav();
933  }
934 
935 
936  // open the file
937  $this->_fp = @fopen($filename, 'rb');
938  if (!is_resource($this->_fp)) {
939  throw new WavFileException('Failed to open "' . $filename . '".');
940  }
941 
942  // read the file
943  return $this->readWav($readData);
944  }
945 
946  /**
947  * Close a with openWav() previously opened wav file or free the buffer of setWavData().
948  * Not necessary if the data has been read (readData = true) already.
949  */
950  public function closeWav() {
951  if (is_resource($this->_fp)) fclose($this->_fp);
952 
953  return $this;
954  }
955 
956  /**
957  * Set the wav file data and properties from a wav file in a string.
958  *
959  * @param string $data (Required) The wav file data. Passed by reference.
960  * @param bool $free (Optional) True to free the passed $data after copying.
961  * @throws WavFormatException
962  * @throws WavFileException
963  */
964  public function setWavData(&$data, $free = true)
965  {
966  // check preconditions
967  if (is_resource($this->_fp)) $this->closeWav();
968 
969 
970  // open temporary stream in memory
971  $this->_fp = @fopen('php://memory', 'w+b');
972  if (!is_resource($this->_fp)) {
973  throw new WavFileException('Failed to open memory stream to write wav data. Use openWav() instead.');
974  }
975 
976  // prepare stream
977  fwrite($this->_fp, $data);
978  rewind($this->_fp);
979 
980  // free the passed data
981  if ($free) $data = null;
982 
983  // read the stream like a file
984  return $this->readWav(true);
985  }
986 
987  /**
988  * Read wav file from a stream.
989  *
990  * @param $readData (Optional) If true, also read the data chunk.
991  * @throws WavFormatException
992  * @throws WavFileException
993  */
994  protected function readWav($readData = true)
995  {
996  if (!is_resource($this->_fp)) {
997  throw new WavFileException('No wav file open. Use openWav() first.');
998  }
999 
1000  try {
1001  $this->readWavHeader();
1002  } catch (WavFileException $ex) {
1003  $this->closeWav();
1004  throw $ex;
1005  }
1006 
1007  if ($readData) return $this->readWavData();
1008 
1009  return $this;
1010  }
1011 
1012  /**
1013  * Parse a wav header.
1014  * http://www-mmsp.ece.mcgill.ca/documents/audioformats/wave/wave.html
1015  *
1016  * @throws WavFormatException
1017  * @throws WavFileException
1018  */
1019  protected function readWavHeader()
1020  {
1021  if (!is_resource($this->_fp)) {
1022  throw new WavFileException('No wav file open. Use openWav() first.');
1023  }
1024 
1025  // get actual file size
1026  $stat = fstat($this->_fp);
1027  $actualSize = $stat['size'];
1028 
1029  $this->_actualSize = $actualSize;
1030 
1031 
1032  // read the common header
1033  $header = fread($this->_fp, 36); // minimum size of the wav header
1034  if (strlen($header) < 36) {
1035  throw new WavFormatException('Not wav format. Header too short.', 1);
1036  }
1037 
1038 
1039  // check "RIFF" header
1040  $RIFF = unpack('NChunkID/VChunkSize/NFormat', $header);
1041 
1042  if ($RIFF['ChunkID'] != 0x52494646) { // "RIFF"
1043  throw new WavFormatException('Not wav format. "RIFF" signature missing.', 2);
1044  }
1045 
1046  if ($actualSize - 8 < $RIFF['ChunkSize']) {
1047  trigger_error('"RIFF" chunk size does not match actual file size. Found ' . $RIFF['ChunkSize'] . ', expected ' . ($actualSize - 8) . '.', E_USER_NOTICE);
1048  $RIFF['ChunkSize'] = $actualSize - 8;
1049  //throw new WavFormatException('"RIFF" chunk size does not match actual file size. Found ' . $RIFF['ChunkSize'] . ', expected ' . ($actualSize - 8) . '.', 3);
1050  }
1051 
1052  if ($RIFF['Format'] != 0x57415645) { // "WAVE"
1053  throw new WavFormatException('Not wav format. "RIFF" chunk format is not "WAVE".', 4);
1054  }
1055 
1056  $this->_chunkSize = $RIFF['ChunkSize'];
1057 
1058 
1059  // check common "fmt " subchunk
1060  $fmt = unpack('NSubchunkID/VSubchunkSize/vAudioFormat/vNumChannels/'
1061  .'VSampleRate/VByteRate/vBlockAlign/vBitsPerSample',
1062  substr($header, 12));
1063 
1064  if ($fmt['SubchunkID'] != 0x666d7420) { // "fmt "
1065  throw new WavFormatException('Bad wav header. Expected "fmt " subchunk.', 11);
1066  }
1067 
1068  if ($fmt['SubchunkSize'] < 16) {
1069  throw new WavFormatException('Bad "fmt " subchunk size.', 12);
1070  }
1071 
1072  if ( $fmt['AudioFormat'] != self::WAVE_FORMAT_PCM
1073  && $fmt['AudioFormat'] != self::WAVE_FORMAT_IEEE_FLOAT
1074  && $fmt['AudioFormat'] != self::WAVE_FORMAT_EXTENSIBLE)
1075  {
1076  throw new WavFormatException('Unsupported audio format. Only PCM or IEEE FLOAT (EXTENSIBLE) audio is supported.', 13);
1077  }
1078 
1079  if ($fmt['NumChannels'] < 1 || $fmt['NumChannels'] > self::MAX_CHANNEL) {
1080  throw new WavFormatException('Invalid number of channels in "fmt " subchunk.', 14);
1081  }
1082 
1083  if ($fmt['SampleRate'] < 1 || $fmt['SampleRate'] > self::MAX_SAMPLERATE) {
1084  throw new WavFormatException('Invalid sample rate in "fmt " subchunk.', 15);
1085  }
1086 
1087  if ( ($fmt['AudioFormat'] == self::WAVE_FORMAT_PCM && !in_array($fmt['BitsPerSample'], array(8, 16, 24)))
1088  || ($fmt['AudioFormat'] == self::WAVE_FORMAT_IEEE_FLOAT && $fmt['BitsPerSample'] != 32)
1089  || ($fmt['AudioFormat'] == self::WAVE_FORMAT_EXTENSIBLE && !in_array($fmt['BitsPerSample'], array(8, 16, 24, 32))))
1090  {
1091  throw new WavFormatException('Only 8, 16 and 24-bit PCM and 32-bit IEEE FLOAT (EXTENSIBLE) audio is supported.', 16);
1092  }
1093 
1094  $blockAlign = $fmt['NumChannels'] * $fmt['BitsPerSample'] / 8;
1095  if ($blockAlign != $fmt['BlockAlign']) {
1096  trigger_error('Invalid block align in "fmt " subchunk. Found ' . $fmt['BlockAlign'] . ', expected ' . $blockAlign . '.', E_USER_NOTICE);
1097  $fmt['BlockAlign'] = $blockAlign;
1098  //throw new WavFormatException('Invalid block align in "fmt " subchunk. Found ' . $fmt['BlockAlign'] . ', expected ' . $blockAlign . '.', 17);
1099  }
1100 
1101  $byteRate = $fmt['SampleRate'] * $blockAlign;
1102  if ($byteRate != $fmt['ByteRate']) {
1103  trigger_error('Invalid average byte rate in "fmt " subchunk. Found ' . $fmt['ByteRate'] . ', expected ' . $byteRate . '.', E_USER_NOTICE);
1104  $fmt['ByteRate'] = $byteRate;
1105  //throw new WavFormatException('Invalid average byte rate in "fmt " subchunk. Found ' . $fmt['ByteRate'] . ', expected ' . $byteRate . '.', 18);
1106  }
1107 
1108  $this->_fmtChunkSize = $fmt['SubchunkSize'];
1109  $this->_audioFormat = $fmt['AudioFormat'];
1110  $this->_numChannels = $fmt['NumChannels'];
1111  $this->_sampleRate = $fmt['SampleRate'];
1112  $this->_byteRate = $fmt['ByteRate'];
1113  $this->_blockAlign = $fmt['BlockAlign'];
1114  $this->_bitsPerSample = $fmt['BitsPerSample'];
1115 
1116 
1117  // read extended "fmt " subchunk data
1118  $extendedFmt = '';
1119  if ($fmt['SubchunkSize'] > 16) {
1120  // possibly handle malformed subchunk without a padding byte
1121  $extendedFmt = fread($this->_fp, $fmt['SubchunkSize'] - 16 + ($fmt['SubchunkSize'] & 1)); // also read padding byte
1122  if (strlen($extendedFmt) < $fmt['SubchunkSize'] - 16) {
1123  throw new WavFormatException('Not wav format. Header too short.', 1);
1124  }
1125  }
1126 
1127 
1128  // check extended "fmt " for EXTENSIBLE Audio Format
1129  if ($fmt['AudioFormat'] == self::WAVE_FORMAT_EXTENSIBLE) {
1130  if (strlen($extendedFmt) < 24) {
1131  throw new WavFormatException('Invalid EXTENSIBLE "fmt " subchunk size. Found ' . $fmt['SubchunkSize'] . ', expected at least 40.', 19);
1132  }
1133 
1134  $extensibleFmt = unpack('vSize/vValidBitsPerSample/VChannelMask/H32SubFormat', substr($extendedFmt, 0, 24));
1135 
1136  if ( $extensibleFmt['SubFormat'] != self::WAVE_SUBFORMAT_PCM
1137  && $extensibleFmt['SubFormat'] != self::WAVE_SUBFORMAT_IEEE_FLOAT)
1138  {
1139  throw new WavFormatException('Unsupported audio format. Only PCM or IEEE FLOAT (EXTENSIBLE) audio is supported.', 13);
1140  }
1141 
1142  if ( ($extensibleFmt['SubFormat'] == self::WAVE_SUBFORMAT_PCM && !in_array($fmt['BitsPerSample'], array(8, 16, 24)))
1143  || ($extensibleFmt['SubFormat'] == self::WAVE_SUBFORMAT_IEEE_FLOAT && $fmt['BitsPerSample'] != 32))
1144  {
1145  throw new WavFormatException('Only 8, 16 and 24-bit PCM and 32-bit IEEE FLOAT (EXTENSIBLE) audio is supported.', 16);
1146  }
1147 
1148  if ($extensibleFmt['Size'] != 22) {
1149  trigger_error('Invaid extension size in EXTENSIBLE "fmt " subchunk.', E_USER_NOTICE);
1150  $extensibleFmt['Size'] = 22;
1151  //throw new WavFormatException('Invaid extension size in EXTENSIBLE "fmt " subchunk.', 20);
1152  }
1153 
1154  if ($extensibleFmt['ValidBitsPerSample'] != $fmt['BitsPerSample']) {
1155  trigger_error('Invaid or unsupported valid bits per sample in EXTENSIBLE "fmt " subchunk.', E_USER_NOTICE);
1156  $extensibleFmt['ValidBitsPerSample'] = $fmt['BitsPerSample'];
1157  //throw new WavFormatException('Invaid or unsupported valid bits per sample in EXTENSIBLE "fmt " subchunk.', 21);
1158  }
1159 
1160  if ($extensibleFmt['ChannelMask'] != 0) {
1161  // count number of set bits - Hamming weight
1162  $c = (int)$extensibleFmt['ChannelMask'];
1163  $n = 0;
1164  while ($c > 0) {
1165  $n += $c & 1;
1166  $c >>= 1;
1167  }
1168  if ($n != $fmt['NumChannels'] || (((int)$extensibleFmt['ChannelMask'] | self::SPEAKER_ALL) != self::SPEAKER_ALL)) {
1169  trigger_error('Invalid channel mask in EXTENSIBLE "fmt " subchunk. The number of channels does not match the number of locations in the mask.', E_USER_NOTICE);
1170  $extensibleFmt['ChannelMask'] = 0;
1171  //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);
1172  }
1173  }
1174 
1175  $this->_fmtExtendedSize = strlen($extendedFmt);
1176  $this->_validBitsPerSample = $extensibleFmt['ValidBitsPerSample'];
1177  $this->_channelMask = $extensibleFmt['ChannelMask'];
1178  $this->_audioSubFormat = $extensibleFmt['SubFormat'];
1179 
1180  } else {
1181  $this->_fmtExtendedSize = strlen($extendedFmt);
1182  $this->_validBitsPerSample = $fmt['BitsPerSample'];
1183  $this->_channelMask = 0;
1184  $this->_audioSubFormat = null;
1185  }
1186 
1187 
1188  // read additional subchunks until "data" subchunk is found
1189  $factSubchunk = array();
1190  $dataSubchunk = array();
1191 
1192  while (!feof($this->_fp)) {
1193  $subchunkHeader = fread($this->_fp, 8);
1194  if (strlen($subchunkHeader) < 8) {
1195  throw new WavFormatException('Missing "data" subchunk.', 101);
1196  }
1197 
1198  $subchunk = unpack('NSubchunkID/VSubchunkSize', $subchunkHeader);
1199 
1200  if ($subchunk['SubchunkID'] == 0x66616374) { // "fact"
1201  // possibly handle malformed subchunk without a padding byte
1202  $subchunkData = fread($this->_fp, $subchunk['SubchunkSize'] + ($subchunk['SubchunkSize'] & 1)); // also read padding byte
1203  if (strlen($subchunkData) < 4) {
1204  throw new WavFormatException('Invalid "fact" subchunk.', 102);
1205  }
1206 
1207  $factParams = unpack('VSampleLength', substr($subchunkData, 0, 4));
1208  $factSubchunk = array_merge($subchunk, $factParams);
1209 
1210  } elseif ($subchunk['SubchunkID'] == 0x64617461) { // "data"
1211  $dataSubchunk = $subchunk;
1212 
1213  break;
1214 
1215  } elseif ($subchunk['SubchunkID'] == 0x7761766C) { // "wavl"
1216  throw new WavFormatException('Wave List Chunk ("wavl" subchunk) is not supported.', 106);
1217  } else {
1218  // skip all other (unknown) subchunks
1219  // possibly handle malformed subchunk without a padding byte
1220  if ( $subchunk['SubchunkSize'] < 0
1221  || fseek($this->_fp, $subchunk['SubchunkSize'] + ($subchunk['SubchunkSize'] & 1), SEEK_CUR) !== 0) { // also skip padding byte
1222  throw new WavFormatException('Invalid subchunk (0x' . dechex($subchunk['SubchunkID']) . ') encountered.', 103);
1223  }
1224  }
1225  }
1226 
1227  if (empty($dataSubchunk)) {
1228  throw new WavFormatException('Missing "data" subchunk.', 101);
1229  }
1230 
1231 
1232  // check "data" subchunk
1233  $dataOffset = ftell($this->_fp);
1234  if ($dataSubchunk['SubchunkSize'] < 0 || $actualSize - $dataOffset < $dataSubchunk['SubchunkSize']) {
1235  trigger_error('Invalid "data" subchunk size.', E_USER_NOTICE);
1236  $dataSubchunk['SubchunkSize'] = $actualSize - $dataOffset;
1237  //throw new WavFormatException('Invalid "data" subchunk size.', 104);
1238  }
1239 
1240  $this->_dataOffset = $dataOffset;
1241  $this->_dataSize = $dataSubchunk['SubchunkSize'];
1242  $this->_dataSize_fp = $dataSubchunk['SubchunkSize'];
1243  $this->_dataSize_valid = false;
1244  $this->_samples = '';
1245 
1246 
1247  // check "fact" subchunk
1248  $numBlocks = (int)($dataSubchunk['SubchunkSize'] / $fmt['BlockAlign']);
1249 
1250  if (empty($factSubchunk)) { // construct fake "fact" subchunk
1251  $factSubchunk = array('SubchunkSize' => 0, 'SampleLength' => $numBlocks);
1252  }
1253 
1254  if ($factSubchunk['SampleLength'] != $numBlocks) {
1255  trigger_error('Invalid sample length in "fact" subchunk.', E_USER_NOTICE);
1256  $factSubchunk['SampleLength'] = $numBlocks;
1257  //throw new WavFormatException('Invalid sample length in "fact" subchunk.', 105);
1258  }
1259 
1260  $this->_factChunkSize = $factSubchunk['SubchunkSize'];
1261  $this->_numBlocks = $factSubchunk['SampleLength'];
1262 
1263 
1264  return $this;
1265 
1266  }
1267 
1268  /**
1269  * Read the wav data from the file into the buffer.
1270  *
1271  * @param $dataOffset (Optional) The byte offset to skip before starting to read. Must be a multiple of BlockAlign.
1272  * @param $dataSize (Optional) The size of the data to read in bytes. Must be a multiple of BlockAlign. Defaults to all data.
1273  * @throws WavFileException
1274  */
1275  public function readWavData($dataOffset = 0, $dataSize = null)
1276  {
1277  // check preconditions
1278  if (!is_resource($this->_fp)) {
1279  throw new WavFileException('No wav file open. Use openWav() first.');
1280  }
1281 
1282  if ($dataOffset < 0 || $dataOffset % $this->getBlockAlign() > 0) {
1283  throw new WavFileException('Invalid data offset. Has to be a multiple of BlockAlign.');
1284  }
1285 
1286  if (is_null($dataSize)) {
1287  $dataSize = $this->_dataSize_fp - ($this->_dataSize_fp % $this->getBlockAlign()); // only read complete blocks
1288  } elseif ($dataSize < 0 || $dataSize % $this->getBlockAlign() > 0) {
1289  throw new WavFileException('Invalid data size to read. Has to be a multiple of BlockAlign.');
1290  }
1291 
1292 
1293  // skip offset
1294  if ($dataOffset > 0 && fseek($this->_fp, $dataOffset, SEEK_CUR) !== 0) {
1295  throw new WavFileException('Seeking to data offset failed.');
1296  }
1297 
1298  // read data
1299  $this->_samples .= fread($this->_fp, $dataSize); // allow appending
1300  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1301 
1302  // close file or memory stream
1303  return $this->closeWav();
1304  }
1305 
1306 
1307  /*%******************************************************************************************%*/
1308  // Sample manipulation methods
1309 
1310  /**
1311  * Return a single sample block from the file.
1312  *
1313  * @param int $blockNum (Required) The sample block number. Zero based.
1314  * @return string The binary sample block (all channels). Returns null if the sample block number was out of range.
1315  */
1316  public function getSampleBlock($blockNum)
1317  {
1318  // check preconditions
1319  if (!$this->_dataSize_valid) {
1320  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1321  }
1322 
1323  $offset = $blockNum * $this->_blockAlign;
1324  if ($offset + $this->_blockAlign > $this->_dataSize || $offset < 0) {
1325  return null;
1326  }
1327 
1328 
1329  // read data
1330  return substr($this->_samples, $offset, $this->_blockAlign);
1331  }
1332 
1333  /**
1334  * Set a single sample block. <br />
1335  * Allows to append a sample block.
1336  *
1337  * @param string $sampleBlock (Required) The binary sample block (all channels).
1338  * @param int $blockNum (Required) The sample block number. Zero based.
1339  * @throws WavFileException
1340  */
1341  public function setSampleBlock($sampleBlock, $blockNum)
1342  {
1343  // check preconditions
1344  $blockAlign = $this->_blockAlign;
1345  if (!isset($sampleBlock[$blockAlign - 1]) || isset($sampleBlock[$blockAlign])) { // faster than: if (strlen($sampleBlock) != $blockAlign)
1346  throw new WavFileException('Incorrect sample block size. Got ' . strlen($sampleBlock) . ', expected ' . $blockAlign . '.');
1347  }
1348 
1349  if (!$this->_dataSize_valid) {
1350  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1351  }
1352 
1353  $numBlocks = (int)($this->_dataSize / $blockAlign);
1354  $offset = $blockNum * $blockAlign;
1355  if ($blockNum > $numBlocks || $blockNum < 0) { // allow appending
1356  throw new WavFileException('Sample block number is out of range.');
1357  }
1358 
1359 
1360  // replace or append data
1361  if ($blockNum == $numBlocks) {
1362  // append
1363  $this->_samples .= $sampleBlock;
1364  $this->_dataSize += $blockAlign;
1365  $this->_chunkSize += $blockAlign;
1366  $this->_actualSize += $blockAlign;
1367  $this->_numBlocks++;
1368  } else {
1369  // replace
1370  for ($i = 0; $i < $blockAlign; ++$i) {
1371  $this->_samples[$offset + $i] = $sampleBlock[$i];
1372  }
1373  }
1374 
1375  return $this;
1376  }
1377 
1378  /**
1379  * Get a float sample value for a specific sample block and channel number.
1380  *
1381  * @param int $blockNum (Required) The sample block number to fetch. Zero based.
1382  * @param int $channelNum (Required) The channel number within the sample block to fetch. First channel is 1.
1383  * @return float The float sample value. Returns null if the sample block number was out of range.
1384  * @throws WavFileException
1385  */
1386  public function getSampleValue($blockNum, $channelNum)
1387  {
1388  // check preconditions
1389  if ($channelNum < 1 || $channelNum > $this->_numChannels) {
1390  throw new WavFileException('Channel number is out of range.');
1391  }
1392 
1393  if (!$this->_dataSize_valid) {
1394  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1395  }
1396 
1397  $sampleBytes = $this->_bitsPerSample / 8;
1398  $offset = $blockNum * $this->_blockAlign + ($channelNum - 1) * $sampleBytes;
1399  if ($offset + $sampleBytes > $this->_dataSize || $offset < 0) {
1400  return null;
1401  }
1402 
1403  // read binary value
1404  $sampleBinary = substr($this->_samples, $offset, $sampleBytes);
1405 
1406  // convert binary to value
1407  switch ($this->_bitsPerSample) {
1408  case 8:
1409  // unsigned char
1410  return (float)((ord($sampleBinary) - 0x80) / 0x80);
1411 
1412  case 16:
1413  // signed short, little endian
1414  $data = unpack('v', $sampleBinary);
1415  $sample = $data[1];
1416  if ($sample >= 0x8000) {
1417  $sample -= 0x10000;
1418  }
1419  return (float)($sample / 0x8000);
1420 
1421  case 24:
1422  // 3 byte packed signed integer, little endian
1423  $data = unpack('C3', $sampleBinary);
1424  $sample = $data[1] | ($data[2] << 8) | ($data[3] << 16);
1425  if ($sample >= 0x800000) {
1426  $sample -= 0x1000000;
1427  }
1428  return (float)($sample / 0x800000);
1429 
1430  case 32:
1431  // 32-bit float
1432  $data = unpack('f', $sampleBinary);
1433  return (float)$data[1];
1434 
1435  default:
1436  return null;
1437  }
1438  }
1439 
1440  /**
1441  * Sets a float sample value for a specific sample block number and channel. <br />
1442  * Converts float values to appropriate integer values and clips properly. <br />
1443  * Allows to append samples (in order).
1444  *
1445  * @param float $sampleFloat (Required) The float sample value to set. Converts float values and clips if necessary.
1446  * @param int $blockNum (Required) The sample block number to set or append. Zero based.
1447  * @param int $channelNum (Required) The channel number within the sample block to set or append. First channel is 1.
1448  * @throws WavFileException
1449  */
1450  public function setSampleValue($sampleFloat, $blockNum, $channelNum)
1451  {
1452  // check preconditions
1453  if ($channelNum < 1 || $channelNum > $this->_numChannels) {
1454  throw new WavFileException('Channel number is out of range.');
1455  }
1456 
1457  if (!$this->_dataSize_valid) {
1458  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1459  }
1460 
1461  $dataSize = $this->_dataSize;
1462  $bitsPerSample = $this->_bitsPerSample;
1463  $sampleBytes = $bitsPerSample / 8;
1464  $offset = $blockNum * $this->_blockAlign + ($channelNum - 1) * $sampleBytes;
1465  if (($offset + $sampleBytes > $dataSize && $offset != $dataSize) || $offset < 0) { // allow appending
1466  throw new WavFileException('Sample block or channel number is out of range.');
1467  }
1468 
1469 
1470  // convert to value, quantize and clip
1471  if ($bitsPerSample == 32) {
1472  $sample = $sampleFloat < -1.0 ? -1.0 : ($sampleFloat > 1.0 ? 1.0 : $sampleFloat);
1473  } else {
1474  $p = 1 << ($bitsPerSample - 1); // 2 to the power of _bitsPerSample divided by 2
1475 
1476  // project and quantize (round) float to integer values
1477  $sample = $sampleFloat < 0 ? (int)($sampleFloat * $p - 0.5) : (int)($sampleFloat * $p + 0.5);
1478 
1479  // clip if necessary to [-$p, $p - 1]
1480  if ($sample < -$p) {
1481  $sample = -$p;
1482  } elseif ($sample > $p - 1) {
1483  $sample = $p - 1;
1484  }
1485  }
1486 
1487  // convert to binary
1488  switch ($bitsPerSample) {
1489  case 8:
1490  // unsigned char
1491  $sampleBinary = chr($sample + 0x80);
1492  break;
1493 
1494  case 16:
1495  // signed short, little endian
1496  if ($sample < 0) {
1497  $sample += 0x10000;
1498  }
1499  $sampleBinary = pack('v', $sample);
1500  break;
1501 
1502  case 24:
1503  // 3 byte packed signed integer, little endian
1504  if ($sample < 0) {
1505  $sample += 0x1000000;
1506  }
1507  $sampleBinary = pack('C3', $sample & 0xff, ($sample >> 8) & 0xff, ($sample >> 16) & 0xff);
1508  break;
1509 
1510  case 32:
1511  // 32-bit float
1512  $sampleBinary = pack('f', $sample);
1513  break;
1514 
1515  default:
1516  $sampleBinary = null;
1517  $sampleBytes = 0;
1518  break;
1519  }
1520 
1521  // replace or append data
1522  if ($offset == $dataSize) {
1523  // append
1524  $this->_samples .= $sampleBinary;
1525  $this->_dataSize += $sampleBytes;
1526  $this->_chunkSize += $sampleBytes;
1527  $this->_actualSize += $sampleBytes;
1528  $this->_numBlocks = (int)($this->_dataSize / $this->_blockAlign);
1529  } else {
1530  // replace
1531  for ($i = 0; $i < $sampleBytes; ++$i) {
1532  $this->_samples{$offset + $i} = $sampleBinary{$i};
1533  }
1534  }
1535 
1536  return $this;
1537  }
1538 
1539 
1540  /*%******************************************************************************************%*/
1541  // Audio processing methods
1542 
1543  /**
1544  * Run samples through audio processing filters.
1545  *
1546  * <code>
1547  * $wav->filter(
1548  * array(
1549  * WavFile::FILTER_MIX => array( // Filter for mixing 2 WavFile instances.
1550  * 'wav' => $wav2, // (Required) The WavFile to mix into this WhavFile. If no optional arguments are given, can be passed without the array.
1551  * 'loop' => true, // (Optional) Loop the selected portion (with warping to the beginning at the end).
1552  * 'blockOffset' => 0, // (Optional) Block number to start mixing from.
1553  * 'numBlocks' => null // (Optional) Number of blocks to mix in or to select for looping. Defaults to the end or all data for looping.
1554  * ),
1555  * WavFile::FILTER_NORMALIZE => 0.6, // (Required) Normalization of (mixed) audio samples - see threshold parameter for normalizeSample().
1556  * WavFile::FILTER_DEGRADE => 0.9 // (Required) Introduce random noise. The quality relative to the amplitude. 1 = no noise, 0 = max. noise.
1557  * ),
1558  * 0, // (Optional) The block number of this WavFile to start with.
1559  * null // (Optional) The number of blocks to process.
1560  * );
1561  * </code>
1562  *
1563  * @param array $filters (Required) An array of 1 or more audio processing filters.
1564  * @param int $blockOffset (Optional) The block number to start precessing from.
1565  * @param int $numBlocks (Optional) The maximum number of blocks to process.
1566  * @throws WavFileException
1567  */
1568  public function filter($filters, $blockOffset = 0, $numBlocks = null)
1569  {
1570  // check preconditions
1571  $totalBlocks = $this->getNumBlocks();
1572  $numChannels = $this->getNumChannels();
1573  if (is_null($numBlocks)) $numBlocks = $totalBlocks - $blockOffset;
1574 
1575  if (!is_array($filters) || empty($filters) || $blockOffset < 0 || $blockOffset > $totalBlocks || $numBlocks <= 0) {
1576  // nothing to do
1577  return $this;
1578  }
1579 
1580  // check filtes
1581  $filter_mix = false;
1582  if (array_key_exists(self::FILTER_MIX, $filters)) {
1583  if (!is_array($filters[self::FILTER_MIX])) {
1584  // assume the 'wav' parameter
1585  $filters[self::FILTER_MIX] = array('wav' => $filters[self::FILTER_MIX]);
1586  }
1587 
1588  $mix_wav = @$filters[self::FILTER_MIX]['wav'];
1589  if (!($mix_wav instanceof WavFile)) {
1590  throw new WavFileException("WavFile to mix is missing or invalid.");
1591  } elseif ($mix_wav->getSampleRate() != $this->getSampleRate()) {
1592  throw new WavFileException("Sample rate of WavFile to mix does not match.");
1593  } else if ($mix_wav->getNumChannels() != $this->getNumChannels()) {
1594  throw new WavFileException("Number of channels of WavFile to mix does not match.");
1595  }
1596 
1597  $mix_loop = @$filters[self::FILTER_MIX]['loop'];
1598  if (is_null($mix_loop)) $mix_loop = false;
1599 
1600  $mix_blockOffset = @$filters[self::FILTER_MIX]['blockOffset'];
1601  if (is_null($mix_blockOffset)) $mix_blockOffset = 0;
1602 
1603  $mix_totalBlocks = $mix_wav->getNumBlocks();
1604  $mix_numBlocks = @$filters[self::FILTER_MIX]['numBlocks'];
1605  if (is_null($mix_numBlocks)) $mix_numBlocks = $mix_loop ? $mix_totalBlocks : $mix_totalBlocks - $mix_blockOffset;
1606  $mix_maxBlock = min($mix_blockOffset + $mix_numBlocks, $mix_totalBlocks);
1607 
1608  $filter_mix = true;
1609  }
1610 
1611  $filter_normalize = false;
1612  if (array_key_exists(self::FILTER_NORMALIZE, $filters)) {
1613  $normalize_threshold = @$filters[self::FILTER_NORMALIZE];
1614 
1615  if (!is_null($normalize_threshold) && abs($normalize_threshold) != 1) $filter_normalize = true;
1616  }
1617 
1618  $filter_degrade = false;
1619  if (array_key_exists(self::FILTER_DEGRADE, $filters)) {
1620  $degrade_quality = @$filters[self::FILTER_DEGRADE];
1621  if (is_null($degrade_quality)) $degrade_quality = 1;
1622 
1623  if ($degrade_quality >= 0 && $degrade_quality < 1) $filter_degrade = true;
1624  }
1625 
1626 
1627  // loop through all sample blocks
1628  for ($block = 0; $block < $numBlocks; ++$block) {
1629  // loop through all channels
1630  for ($channel = 1; $channel <= $numChannels; ++$channel) {
1631  // read current sample
1632  $currentBlock = $blockOffset + $block;
1633  $sampleFloat = $this->getSampleValue($currentBlock, $channel);
1634 
1635 
1636  /************* MIX FILTER ***********************/
1637  if ($filter_mix) {
1638  if ($mix_loop) {
1639  $mixBlock = ($mix_blockOffset + ($block % $mix_numBlocks)) % $mix_totalBlocks;
1640  } else {
1641  $mixBlock = $mix_blockOffset + $block;
1642  }
1643 
1644  if ($mixBlock < $mix_maxBlock) {
1645  $sampleFloat += $mix_wav->getSampleValue($mixBlock, $channel);
1646  }
1647  }
1648 
1649  /************* NORMALIZE FILTER *******************/
1650  if ($filter_normalize) {
1651  $sampleFloat = $this->normalizeSample($sampleFloat, $normalize_threshold);
1652  }
1653 
1654  /************* DEGRADE FILTER *******************/
1655  if ($filter_degrade) {
1656  $sampleFloat += rand(1000000 * ($degrade_quality - 1), 1000000 * (1 - $degrade_quality)) / 1000000;
1657  }
1658 
1659 
1660  // write current sample
1661  $this->setSampleValue($sampleFloat, $currentBlock, $channel);
1662  }
1663  }
1664 
1665  return $this;
1666  }
1667 
1668  /**
1669  * Append a wav file to the current wav. <br />
1670  * The wav files must have the same sample rate, number of bits per sample, and number of channels.
1671  *
1672  * @param WavFile $wav (Required) The wav file to append.
1673  * @throws WavFileException
1674  */
1675  public function appendWav(WavFile $wav) {
1676  // basic checks
1677  if ($wav->getSampleRate() != $this->getSampleRate()) {
1678  throw new WavFileException("Sample rate for wav files do not match.");
1679  } else if ($wav->getBitsPerSample() != $this->getBitsPerSample()) {
1680  throw new WavFileException("Bits per sample for wav files do not match.");
1681  } else if ($wav->getNumChannels() != $this->getNumChannels()) {
1682  throw new WavFileException("Number of channels for wav files do not match.");
1683  }
1684 
1685  $this->_samples .= $wav->_samples;
1686  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1687 
1688  return $this;
1689  }
1690 
1691  /**
1692  * Mix 2 wav files together. <br />
1693  * Both wavs must have the same sample rate and same number of channels.
1694  *
1695  * @param WavFile $wav (Required) The WavFile to mix.
1696  * @param float $normalizeThreshold (Optional) See normalizeSample for an explanation.
1697  * @throws WavFileException
1698  */
1699  public function mergeWav(WavFile $wav, $normalizeThreshold = null) {
1700  return $this->filter(array(
1701  WavFile::FILTER_MIX => $wav,
1702  WavFile::FILTER_NORMALIZE => $normalizeThreshold
1703  ));
1704  }
1705 
1706  /**
1707  * Add silence to the wav file.
1708  *
1709  * @param float $duration (Optional) How many seconds of silence. If negative, add to the beginning of the file. Defaults to 1s.
1710  */
1711  public function insertSilence($duration = 1.0)
1712  {
1713  $numSamples = $this->getSampleRate() * abs($duration);
1714  $numChannels = $this->getNumChannels();
1715 
1716  $data = str_repeat(self::packSample($this->getZeroAmplitude(), $this->getBitsPerSample()), $numSamples * $numChannels);
1717  if ($duration >= 0) {
1718  $this->_samples .= $data;
1719  } else {
1720  $this->_samples = $data . $this->_samples;
1721  }
1722 
1723  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1724 
1725  return $this;
1726  }
1727 
1728  /**
1729  * Degrade the quality of the wav file by introducing random noise.
1730  *
1731  * @param float quality (Optional) The quality relative to the amplitude. 1 = no noise, 0 = max. noise.
1732  */
1733  public function degrade($quality = 1.0)
1734  {
1735  return $this->filter(self::FILTER_DEGRADE, array(
1736  WavFile::FILTER_DEGRADE => $quality
1737  ));
1738  }
1739 
1740  /**
1741  * Generate noise at the end of the wav for the specified duration and volume.
1742  *
1743  * @param float $duration (Optional) Number of seconds of noise to generate.
1744  * @param float $percent (Optional) The percentage of the maximum amplitude to use. 100 = full amplitude.
1745  */
1746  public function generateNoise($duration = 1.0, $percent = 100)
1747  {
1748  $numChannels = $this->getNumChannels();
1749  $numSamples = $this->getSampleRate() * $duration;
1750  $minAmp = $this->getMinAmplitude();
1751  $maxAmp = $this->getMaxAmplitude();
1752  $bitDepth = $this->getBitsPerSample();
1753 
1754  for ($s = 0; $s < $numSamples; ++$s) {
1755  if ($bitDepth == 32) {
1756  $val = rand(-$percent * 10000, $percent * 10000) / 1000000;
1757  } else {
1758  $val = rand($minAmp, $maxAmp);
1759  $val = (int)($val * $percent / 100);
1760  }
1761 
1762  $this->_samples .= str_repeat(self::packSample($val, $bitDepth), $numChannels);
1763  }
1764 
1765  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1766 
1767  return $this;
1768  }
1769 
1770  /**
1771  * Convert sample data to different bits per sample.
1772  *
1773  * @param int $bitsPerSample (Required) The new number of bits per sample;
1774  * @throws WavFileException
1775  */
1776  public function convertBitsPerSample($bitsPerSample) {
1777  if ($this->getBitsPerSample() == $bitsPerSample) {
1778  return $this;
1779  }
1780 
1781  $tempWav = new WavFile($this->getNumChannels(), $this->getSampleRate(), $bitsPerSample);
1782  $tempWav->filter(
1783  array(self::FILTER_MIX => $this),
1784  0,
1785  $this->getNumBlocks()
1786  );
1787 
1788  $this->setSamples() // implicit setDataSize(), setSize(), setActualSize(), setNumBlocks()
1789  ->setBitsPerSample($bitsPerSample); // implicit setValidBitsPerSample(), setAudioFormat(), setAudioSubFormat(), setFmtChunkSize(), setFactChunkSize(), setSize(), setActualSize(), setDataOffset(), setByteRate(), setBlockAlign(), setNumBlocks()
1790  $this->_samples = $tempWav->_samples;
1791  $this->setDataSize(); // implicit setSize(), setActualSize(), setNumBlocks()
1792 
1793  return $this;
1794  }
1795 
1796 
1797  /*%******************************************************************************************%*/
1798  // Miscellaneous methods
1799 
1800  /**
1801  * Output information about the wav object.
1802  */
1803  public function displayInfo()
1804  {
1805  $s = "File Size: %u\n"
1806  ."Chunk Size: %u\n"
1807  ."fmt Subchunk Size: %u\n"
1808  ."Extended fmt Size: %u\n"
1809  ."fact Subchunk Size: %u\n"
1810  ."Data Offset: %u\n"
1811  ."Data Size: %u\n"
1812  ."Audio Format: %s\n"
1813  ."Audio SubFormat: %s\n"
1814  ."Channels: %u\n"
1815  ."Channel Mask: 0x%s\n"
1816  ."Sample Rate: %u\n"
1817  ."Bits Per Sample: %u\n"
1818  ."Valid Bits Per Sample: %u\n"
1819  ."Sample Block Size: %u\n"
1820  ."Number of Sample Blocks: %u\n"
1821  ."Byte Rate: %uBps\n";
1822 
1823  $s = sprintf($s, $this->getActualSize(),
1824  $this->getChunkSize(),
1825  $this->getFmtChunkSize(),
1826  $this->getFmtExtendedSize(),
1827  $this->getFactChunkSize(),
1828  $this->getDataOffset(),
1829  $this->getDataSize(),
1830  $this->getAudioFormat() == self::WAVE_FORMAT_PCM ? 'PCM' : ($this->getAudioFormat() == self::WAVE_FORMAT_IEEE_FLOAT ? 'IEEE FLOAT' : 'EXTENSIBLE'),
1831  $this->getAudioSubFormat() == self::WAVE_SUBFORMAT_PCM ? 'PCM' : 'IEEE FLOAT',
1832  $this->getNumChannels(),
1833  dechex($this->getChannelMask()),
1834  $this->getSampleRate(),
1835  $this->getBitsPerSample(),
1836  $this->getValidBitsPerSample(),
1837  $this->getBlockAlign(),
1838  $this->getNumBlocks(),
1839  $this->getByteRate());
1840 
1841  if (php_sapi_name() == 'cli') {
1842  return $s;
1843  } else {
1844  return nl2br($s);
1845  }
1846  }
1847 }
1848 
1849 
1850 /*%******************************************************************************************%*/
1851 // Exceptions
1852 
1853 /**
1854  * WavFileException indicates an illegal state or argument in this class.
1855  */
1856 class WavFileException extends Exception {}
1857 
1858 /**
1859  * WavFormatException indicates a malformed or unsupported wav file header.
1860  */