WavFile.php 72 KB

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