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