+ * $wav1 = new WavFile(2, 44100, 16); // new wav with 2 channels, at 44100 samples/sec and 16 bits per sample
+ * $wav2 = new WavFile('./audio/sound.wav'); // open and read wav file
+ *
+ *
+ * @param string|int $numChannelsOrFileName (Optional) If string, the filename of the wav file to open. The number of channels otherwise. Defaults to 1.
+ * @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.
+ * @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.
+ * @throws WavFormatException
+ * @throws WavFileException
+ */
+ public function __construct($numChannelsOrFileName = null, $sampleRateOrReadData = null, $bitsPerSample = null)
+ {
+ $this->_actualSize = 44;
+ $this->_chunkSize = 36;
+ $this->_fmtChunkSize = 16;
+ $this->_fmtExtendedSize = 0;
+ $this->_factChunkSize = 0;
+ $this->_dataSize = 0;
+ $this->_dataSize_fp = 0;
+ $this->_dataSize_valid = true;
+ $this->_dataOffset = 44;
+ $this->_audioFormat = self::WAVE_FORMAT_PCM;
+ $this->_audioSubFormat = null;
+ $this->_numChannels = 1;
+ $this->_channelMask = self::SPEAKER_DEFAULT;
+ $this->_sampleRate = 8000;
+ $this->_bitsPerSample = 8;
+ $this->_validBitsPerSample = 8;
+ $this->_blockAlign = 1;
+ $this->_numBlocks = 0;
+ $this->_byteRate = 8000;
+ $this->_samples = '';
+ $this->_fp = null;
+
+
+ if (is_string($numChannelsOrFileName)) {
+ $this->openWav($numChannelsOrFileName, is_bool($sampleRateOrReadData) ? $sampleRateOrReadData : true);
+
+ } else {
+ $this->setNumChannels(is_null($numChannelsOrFileName) ? 1 : $numChannelsOrFileName)
+ ->setSampleRate(is_null($sampleRateOrReadData) ? 8000 : $sampleRateOrReadData)
+ ->setBitsPerSample(is_null($bitsPerSample) ? 8 : $bitsPerSample);
+ }
+ }
+
+ public function __destruct() {
+ if (is_resource($this->_fp)) $this->closeWav();
+ }
+
+ public function __clone() {
+ $this->_fp = null;
+ }
+
+ /**
+ * Output the wav file headers and data.
+ *
+ * @return string The encoded file.
+ */
+ public function __toString()
+ {
+ return $this->makeHeader() .
+ $this->getDataSubchunk();
+ }
+
+
+ /*%******************************************************************************************%*/
+ // Static methods
+
+ /**
+ * Unpacks a single binary sample to numeric value.
+ *
+ * @param string $sampleBinary (Required) The sample to decode.
+ * @param int $bitDepth (Optional) The bits per sample to decode. If omitted, derives it from the length of $sampleBinary.
+ * @return int|float The numeric sample value. Float for 32-bit samples. Returns null for unsupported bit depths.
+ */
+ public static function unpackSample($sampleBinary, $bitDepth = null)
+ {
+ if ($bitDepth === null) {
+ $bitDepth = strlen($sampleBinary) * 8;
+ }
+
+ switch ($bitDepth) {
+ case 8:
+ // unsigned char
+ return ord($sampleBinary);
+
+ case 16:
+ // signed short, little endian
+ $data = unpack('v', $sampleBinary);
+ $sample = $data[1];
+ if ($sample >= 0x8000) {
+ $sample -= 0x10000;
+ }
+ return $sample;
+
+ case 24:
+ // 3 byte packed signed integer, little endian
+ $data = unpack('C3', $sampleBinary);
+ $sample = $data[1] | ($data[2] << 8) | ($data[3] << 16);
+ if ($sample >= 0x800000) {
+ $sample -= 0x1000000;
+ }
+ return $sample;
+
+ case 32:
+ // 32-bit float
+ $data = unpack('f', $sampleBinary);
+ return $data[1];
+
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Packs a single numeric sample to binary.
+ *
+ * @param int|float $sample (Required) The sample to encode. Has to be within valid range for $bitDepth. Float values only for 32 bits.
+ * @param int $bitDepth (Required) The bits per sample to encode with.
+ * @return string The encoded binary sample. Returns null for unsupported bit depths.
+ */
+ public static function packSample($sample, $bitDepth)
+ {
+ switch ($bitDepth) {
+ case 8:
+ // unsigned char
+ return chr($sample);
+
+ case 16:
+ // signed short, little endian
+ if ($sample < 0) {
+ $sample += 0x10000;
+ }
+ return pack('v', $sample);
+
+ case 24:
+ // 3 byte packed signed integer, little endian
+ if ($sample < 0) {
+ $sample += 0x1000000;
+ }
+ return pack('C3', $sample & 0xff, ($sample >> 8) & 0xff, ($sample >> 16) & 0xff);
+
+ case 32:
+ // 32-bit float
+ return pack('f', $sample);
+
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Unpacks a binary sample block to numeric values.
+ *
+ * @param string $sampleBlock (Required) The binary sample block (all channels).
+ * @param int $bitDepth (Required) The bits per sample to decode.
+ * @param int $numChannels (Optional) The number of channels to decode. If omitted, derives it from the length of $sampleBlock and $bitDepth.
+ * @return array The sample values as an array of integers of floats for 32 bits. First channel is array index 1.
+ */
+ public static function unpackSampleBlock($sampleBlock, $bitDepth, $numChannels = null) {
+ $sampleBytes = $bitDepth / 8;
+ if ($numChannels === null) {
+ $numChannels = strlen($sampleBlock) / $sampleBytes;
+ }
+
+ $samples = array();
+ for ($i = 0; $i < $numChannels; $i++) {
+ $sampleBinary = substr($sampleBlock, $i * $sampleBytes, $sampleBytes);
+ $samples[$i + 1] = self::unpackSample($sampleBinary, $bitDepth);
+ }
+
+ return $samples;
+ }
+
+ /**
+ * Packs an array of numeric channel samples to a binary sample block.
+ *
+ * @param array $samples (Required) The array of channel sample values. Expects float values for 32 bits and integer otherwise.
+ * @param int $bitDepth (Required) The bits per sample to encode with.
+ * @return string The encoded binary sample block.
+ */
+ public static function packSampleBlock($samples, $bitDepth) {
+ $sampleBlock = '';
+ foreach($samples as $sample) {
+ $sampleBlock .= self::packSample($sample, $bitDepth);
+ }
+
+ return $sampleBlock;
+ }
+
+ /**
+ * Normalizes a float audio sample. Maximum input range assumed for compression is [-2, 2].
+ * See http://www.voegler.eu/pub/audio/ for more information.
+ *
+ * @param float $sampleFloat (Required) The float sample to normalize.
+ * @param float $threshold (Required) The threshold or gain factor for normalizing the amplitude.
+ * $wav->filter(
+ * array(
+ * WavFile::FILTER_MIX => array( // Filter for mixing 2 WavFile instances.
+ * 'wav' => $wav2, // (Required) The WavFile to mix into this WhavFile. If no optional arguments are given, can be passed without the array.
+ * 'loop' => true, // (Optional) Loop the selected portion (with warping to the beginning at the end).
+ * 'blockOffset' => 0, // (Optional) Block number to start mixing from.
+ * 'numBlocks' => null // (Optional) Number of blocks to mix in or to select for looping. Defaults to the end or all data for looping.
+ * ),
+ * WavFile::FILTER_NORMALIZE => 0.6, // (Required) Normalization of (mixed) audio samples - see threshold parameter for normalizeSample().
+ * WavFile::FILTER_DEGRADE => 0.9 // (Required) Introduce random noise. The quality relative to the amplitude. 1 = no noise, 0 = max. noise.
+ * ),
+ * 0, // (Optional) The block number of this WavFile to start with.
+ * null // (Optional) The number of blocks to process.
+ * );
+ *
+ *
+ * @param array $filters (Required) An array of 1 or more audio processing filters.
+ * @param int $blockOffset (Optional) The block number to start precessing from.
+ * @param int $numBlocks (Optional) The maximum number of blocks to process.
+ * @throws WavFileException
+ */
+ public function filter($filters, $blockOffset = 0, $numBlocks = null)
+ {
+ // check preconditions
+ $totalBlocks = $this->getNumBlocks();
+ $numChannels = $this->getNumChannels();
+ if (is_null($numBlocks)) $numBlocks = $totalBlocks - $blockOffset;
+
+ if (!is_array($filters) || empty($filters) || $blockOffset < 0 || $blockOffset > $totalBlocks || $numBlocks <= 0) {
+ // nothing to do
+ return $this;
+ }
+
+ // check filtes
+ $filter_mix = false;
+ if (array_key_exists(self::FILTER_MIX, $filters)) {
+ if (!is_array($filters[self::FILTER_MIX])) {
+ // assume the 'wav' parameter
+ $filters[self::FILTER_MIX] = array('wav' => $filters[self::FILTER_MIX]);
+ }
+
+ $mix_wav = @$filters[self::FILTER_MIX]['wav'];
+ if (!($mix_wav instanceof WavFile)) {
+ throw new WavFileException("WavFile to mix is missing or invalid.");
+ } elseif ($mix_wav->getSampleRate() != $this->getSampleRate()) {
+ throw new WavFileException("Sample rate of WavFile to mix does not match.");
+ } else if ($mix_wav->getNumChannels() != $this->getNumChannels()) {
+ throw new WavFileException("Number of channels of WavFile to mix does not match.");
+ }
+
+ $mix_loop = @$filters[self::FILTER_MIX]['loop'];
+ if (is_null($mix_loop)) $mix_loop = false;
+
+ $mix_blockOffset = @$filters[self::FILTER_MIX]['blockOffset'];
+ if (is_null($mix_blockOffset)) $mix_blockOffset = 0;
+
+ $mix_totalBlocks = $mix_wav->getNumBlocks();
+ $mix_numBlocks = @$filters[self::FILTER_MIX]['numBlocks'];
+ if (is_null($mix_numBlocks)) $mix_numBlocks = $mix_loop ? $mix_totalBlocks : $mix_totalBlocks - $mix_blockOffset;
+ $mix_maxBlock = min($mix_blockOffset + $mix_numBlocks, $mix_totalBlocks);
+
+ $filter_mix = true;
+ }
+
+ $filter_normalize = false;
+ if (array_key_exists(self::FILTER_NORMALIZE, $filters)) {
+ $normalize_threshold = @$filters[self::FILTER_NORMALIZE];
+
+ if (!is_null($normalize_threshold) && abs($normalize_threshold) != 1) $filter_normalize = true;
+ }
+
+ $filter_degrade = false;
+ if (array_key_exists(self::FILTER_DEGRADE, $filters)) {
+ $degrade_quality = @$filters[self::FILTER_DEGRADE];
+ if (is_null($degrade_quality)) $degrade_quality = 1;
+
+ if ($degrade_quality >= 0 && $degrade_quality < 1) $filter_degrade = true;
+ }
+
+
+ // loop through all sample blocks
+ for ($block = 0; $block < $numBlocks; ++$block) {
+ // loop through all channels
+ for ($channel = 1; $channel <= $numChannels; ++$channel) {
+ // read current sample
+ $currentBlock = $blockOffset + $block;
+ $sampleFloat = $this->getSampleValue($currentBlock, $channel);
+
+
+ /************* MIX FILTER ***********************/
+ if ($filter_mix) {
+ if ($mix_loop) {
+ $mixBlock = ($mix_blockOffset + ($block % $mix_numBlocks)) % $mix_totalBlocks;
+ } else {
+ $mixBlock = $mix_blockOffset + $block;
+ }
+
+ if ($mixBlock < $mix_maxBlock) {
+ $sampleFloat += $mix_wav->getSampleValue($mixBlock, $channel);
+ }
+ }
+
+ /************* NORMALIZE FILTER *******************/
+ if ($filter_normalize) {
+ $sampleFloat = $this->normalizeSample($sampleFloat, $normalize_threshold);
+ }
+
+ /************* DEGRADE FILTER *******************/
+ if ($filter_degrade) {
+ $sampleFloat += rand(1000000 * ($degrade_quality - 1), 1000000 * (1 - $degrade_quality)) / 1000000;
+ }
+
+
+ // write current sample
+ $this->setSampleValue($sampleFloat, $currentBlock, $channel);
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * Append a wav file to the current wav. $message" + . "
$message" + . "
+ * namespace = 'contact_form';
+ *
+ * // in form validator
+ * $img->namespace = 'contact_form';
+ * if ($img->check($code) == true) {
+ * echo "Valid!";
+ * }
+ *
+ */
+ public $namespace;
+
+ /**
+ * The font file to use to draw the captcha code, leave blank for default font AHGBold.ttf
+ * @var string
+ */
+ public $ttf_file;
+ /**
+ * The path to the wordlist file to use, leave blank for default words/words.txt
+ * @var string
+ */
+ public $wordlist_file;
+ /**
+ * The directory to scan for background images, if set a random background will be chosen from this folder
+ * @var string
+ */
+ public $background_directory;
+ /**
+ * The path to the SQLite database file to use, if $use_sqlite_database = true, should be chmod 666
+ * @deprecated 3.2RC4
+ * @var string
+ */
+ public $sqlite_database;
+ /**
+ * The path to the securimage audio directory, can be set in securimage_play.php
+ * @var string
+ *
+ * $img->audio_path = '/home/yoursite/public_html/securimage/audio/en/';
+ *
+ */
+ public $audio_path;
+ /**
+ * The path to the directory containing audio files that will be selected
+ * randomly and mixed with the captcha audio.
+ *
+ * @var string
+ */
+ public $audio_noise_path;
+ /**
+ * Whether or not to mix background noise files into captcha audio (true = mix, false = no)
+ * Mixing random background audio with noise can help improve security of audio captcha.
+ * Default: securimage/audio/noise
+ *
+ * @since 3.0.3
+ * @see Securimage::$audio_noise_path
+ * @var bool
+ */
+ public $audio_use_noise;
+ /**
+ * The method and threshold (or gain factor) used to normalize the mixing with background noise.
+ * See http://www.voegler.eu/pub/audio/ for more information.
+ *
+ * Valid:
+ * $options = array(
+ * 'text_color' => new Securimage_Color('#013020'),
+ * 'code_length' => 5,
+ * 'num_lines' => 5,
+ * 'noise_level' => 3,
+ * 'font_file' => Securimage::getPath() . '/custom.ttf'
+ * );
+ *
+ * $img = new Securimage($options);
+ *
+ */
+ public function __construct($options = array())
+ {
+ $this->securimage_path = dirname(__FILE__);
+
+ if (is_array($options) && sizeof($options) > 0) {
+ foreach($options as $prop => $val) {
+ if ($prop == 'captchaId') {
+ Securimage::$_captchaId = $val;
+ $this->use_database = true;
+ } else if ($prop == 'use_sqlite_db') {
+ trigger_error("The use_sqlite_db option is deprecated, use 'use_database' instead", E_USER_NOTICE);
+ } else {
+ $this->$prop = $val;
+ }
+ }
+ }
+
+ $this->image_bg_color = $this->initColor($this->image_bg_color, '#ffffff');
+ $this->text_color = $this->initColor($this->text_color, '#616161');
+ $this->line_color = $this->initColor($this->line_color, '#616161');
+ $this->noise_color = $this->initColor($this->noise_color, '#616161');
+ $this->signature_color = $this->initColor($this->signature_color, '#616161');
+
+ if (is_null($this->ttf_file)) {
+ $this->ttf_file = $this->securimage_path . '/AHGBold.ttf';
+ }
+
+ $this->signature_font = $this->ttf_file;
+
+ if (is_null($this->wordlist_file)) {
+ $this->wordlist_file = $this->securimage_path . '/words/words.txt';
+ }
+
+ if (is_null($this->database_file)) {
+ $this->database_file = $this->securimage_path . '/database/securimage.sq3';
+ }
+
+ if (is_null($this->audio_path)) {
+ $this->audio_path = $this->securimage_path . '/audio/en/';
+ }
+
+ if (is_null($this->audio_noise_path)) {
+ $this->audio_noise_path = $this->securimage_path . '/audio/noise/';
+ }
+
+ if (is_null($this->audio_use_noise)) {
+ $this->audio_use_noise = true;
+ }
+
+ if (is_null($this->degrade_audio)) {
+ $this->degrade_audio = true;
+ }
+
+ if (is_null($this->code_length) || (int)$this->code_length < 1) {
+ $this->code_length = 6;
+ }
+
+ if (is_null($this->perturbation) || !is_numeric($this->perturbation)) {
+ $this->perturbation = 0.75;
+ }
+
+ if (is_null($this->namespace) || !is_string($this->namespace)) {
+ $this->namespace = 'default';
+ }
+
+ if (is_null($this->no_exit)) {
+ $this->no_exit = false;
+ }
+
+ if (is_null($this->no_session)) {
+ $this->no_session = false;
+ }
+
+ if (is_null($this->send_headers)) {
+ $this->send_headers = true;
+ }
+
+ if ($this->no_session != true) {
+ // Initialize session or attach to existing
+ if ( session_id() == '' ) { // no session has been started yet, which is needed for validation
+ if (!is_null($this->session_name) && trim($this->session_name) != '') {
+ session_name(trim($this->session_name)); // set session name if provided
+ }
+ session_start();
+ }
+ }
+ }
+
+ /**
+ * Return the absolute path to the Securimage directory
+ * @return string The path to the securimage base directory
+ */
+ public static function getPath()
+ {
+ return dirname(__FILE__);
+ }
+
+ /**
+ * Generate a new captcha ID or retrieve the current ID
+ *
+ * @param $new bool If true, generates a new challenge and returns and ID
+ * @param $options array Additional options to be passed to Securimage.
+ * Must include database options if not set directly in securimage.php
+ *
+ * @return null|string Returns null if no captcha id set and new was false, or string captcha ID
+ */
+ public static function getCaptchaId($new = true, array $options = array())
+ {
+ if (is_null($new) || (bool)$new == true) {
+ $id = sha1(uniqid($_SERVER['REMOTE_ADDR'], true));
+ $opts = array('no_session' => true,
+ 'use_database' => true);
+ if (sizeof($options) > 0) $opts = array_merge($options, $opts);
+ $si = new self($opts);
+ Securimage::$_captchaId = $id;
+ $si->createCode();
+
+ return $id;
+ } else {
+ return Securimage::$_captchaId;
+ }
+ }
+
+ /**
+ * Validate a captcha code input against a captcha ID
+ *
+ * @param string $id The captcha ID to check
+ * @param string $value The captcha value supplied by the user
+ * @param array $options Array of options to construct Securimage with.
+ * Options must include database options if they are not set in securimage.php
+ *
+ * @see Securimage::$database_driver
+ * @return bool true if the code was valid for the given captcha ID, false if not or if database failed to open
+ */
+ public static function checkByCaptchaId($id, $value, array $options = array())
+ {
+ $opts = array('captchaId' => $id,
+ 'no_session' => true,
+ 'use_database' => true);
+
+ if (sizeof($options) > 0) $opts = array_merge($options, $opts);
+
+ $si = new self($opts);
+
+ if ($si->openDatabase()) {
+ $code = $si->getCodeFromDatabase();
+
+ if (is_array($code)) {
+ $si->code = $code['code'];
+ $si->code_display = $code['code_disp'];
+ }
+
+ if ($si->check($value)) {
+ $si->clearCodeFromDatabase();
+
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+
+ /**
+ * Used to serve a captcha image to the browser
+ * @param string $background_image The path to the background image to use
+ *
+ * $img = new Securimage();
+ * $img->code_length = 6;
+ * $img->num_lines = 5;
+ * $img->noise_level = 5;
+ *
+ * $img->show(); // sends the image to browser
+ * exit;
+ *
+ */
+ public function show($background_image = '')
+ {
+ set_error_handler(array(&$this, 'errorHandler'));
+
+ if($background_image != '' && is_readable($background_image)) {
+ $this->bgimg = $background_image;
+ }
+
+ $this->doImage();
+ }
+
+ /**
+ * Check a submitted code against the stored value
+ * @param string $code The captcha code to check
+ *
+ * $code = $_POST['code'];
+ * $img = new Securimage();
+ * if ($img->check($code) == true) {
+ * $captcha_valid = true;
+ * } else {
+ * $captcha_valid = false;
+ * }
+ *
+ */
+ public function check($code)
+ {
+ $this->code_entered = $code;
+ $this->validate();
+ return $this->correct_code;
+ }
+
+ /**
+ * Output a wav file of the captcha code to the browser
+ *
+ *
+ * $img = new Securimage();
+ * $img->outputAudioFile(); // outputs a wav file to the browser
+ * exit;
+ *
+ */
+ public function outputAudioFile()
+ {
+ set_error_handler(array(&$this, 'errorHandler'));
+
+ require_once dirname(__FILE__) . '/WavFile.php';
+
+ try {
+ $audio = $this->getAudibleCode();
+ } catch (Exception $ex) {
+ if (($fp = @fopen(dirname(__FILE__) . '/si.error_log', 'a+')) !== false) {
+ fwrite($fp, date('Y-m-d H:i:s') . ': Securimage audio error "' . $ex->getMessage() . '"' . "\n");
+ fclose($fp);
+ }
+
+ $audio = $this->audioError();
+ }
+
+ if ($this->canSendHeaders() || $this->send_headers == false) {
+ if ($this->send_headers) {
+ $uniq = md5(uniqid(microtime()));
+ header("Content-Disposition: attachment; filename=\"securimage_audio-{$uniq}.wav\"");
+ header('Cache-Control: no-store, no-cache, must-revalidate');
+ header('Expires: Sun, 1 Jan 2000 12:00:00 GMT');
+ header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . 'GMT');
+ header('Content-type: audio/x-wav');
+
+ if (extension_loaded('zlib')) {
+ ini_set('zlib.output_compression', true); // compress output if supported by browser
+ } else {
+ header('Content-Length: ' . strlen($audio));
+ }
+ }
+
+ echo $audio;
+ } else {
+ echo '