function Get_iOS_ChunkedSound()
{
    // detect the first request after a new sound icon click, and clear any stored data
    // to avoid reusing the same sound endlessly within a session
    // javascript player adds a timestamp querystring param ("&d=..."), so it can be detected by it
    $isJavaScriptPlayerRequest = array_key_exists('d', $_GET) && LBD_StringHelper::HasValue($_GET['d']);
    if ($isJavaScriptPlayerRequest) {
        // when javascript is enabled, we can detect the first request because the timestamp changed
        $soundClickId = LBD_StringHelper::Normalize($_GET['d']);
        $prevSoundClickId = LBD_Persistence_Load('prevSoundClickId');
        if (0 != strcasecmp($soundClickId, $prevSoundClickId)) {
            Clear_iOS_SoundData();
            LBD_Persistence_Save('prevSoundClickId', $soundClickId);
            // on first request, save for future checks
        }
    }
    // sound byte subset
    $range = GetSoundByteRange();
    $rangeStart = $range['start'];
    $rangeEnd = $range['end'];
    $rangeSize = $rangeEnd - $rangeStart;
    // full sound bytes
    $soundBytes = Get_iOS_SoundData();
    if (is_null($soundBytes)) {
        return;
    }
    $totalSize = strlen($soundBytes) - 1;
    // initial iOS 6.0.1 testing; leaving as fallback since we can't be sure it won't happen again:
    // we depend on observed behavior of invalid range requests to detect
    // end of sound playback, cleanup and tell AppleCoreMedia to stop requesting
    // invalid "bytes=rangeEnd-rangeEnd" ranges in an infinite(?) loop
    if ($rangeStart == $rangeEnd || $rangeEnd > $totalSize) {
        Clear_iOS_SoundData();
        LBD_HttpHelper::BadRequest('invalid byte range');
    }
    while (ob_get_length()) {
        ob_end_clean();
    }
    ob_start();
    try {
        // partial content response with the requested byte range
        header('HTTP/1.1 206 Partial Content');
        $mimeType = $captcha->SoundMimeType;
        header("Content-Type: {$mimeType}");
        header('Content-Transfer-Encoding: binary');
        header('X-Robots-Tag: noindex, nofollow, noarchive, nosnippet');
        header('Accept-Ranges: bytes');
        header("Content-Length: {$rangeSize}");
        header("Content-Range: bytes {$rangeStart}-{$rangeEnd}/{$totalSize}");
        if (!array_key_exists('d', $_GET)) {
            // javascript player not used, we send the file directly as a download
            $downloadId = LBD_CryptoHelper::GenerateGuid();
            header("Content-Disposition: attachment; filename=captcha_{$downloadId}.wav");
        }
        LBD_HttpHelper::SmartDisallowCache();
        $rangeBytes = substr($soundBytes, $rangeStart, $rangeSize);
        echo $rangeBytes;
    } catch (Exception $e) {
        header('Content-Type: text/plain');
        echo $e->getMessage();
    }
    ob_end_flush();
    exit;
}
function GetSound()
{
    $captcha = GetCaptchaObject();
    if (is_null($captcha)) {
        LBD_HttpHelper::BadRequest('Captcha doesn\'t exist');
    }
    if (!$captcha->SoundEnabled) {
        // sound requests can be disabled with this config switch / instance property
        LBD_HttpHelper::BadRequest('Sound disabled');
    }
    $instanceId = GetInstanceId();
    if (is_null($instanceId)) {
        LBD_HttpHelper::BadRequest('Instance doesn\'t exist');
    }
    $soundBytes = GetSoundData($captcha, $instanceId);
    session_write_close();
    if (is_null($soundBytes)) {
        LBD_HttpHelper::BadRequest('Please reload the form page before requesting another Captcha sound');
        exit;
    }
    $totalSize = strlen($soundBytes);
    // response headers
    LBD_HttpHelper::SmartDisallowCache();
    $mimeType = $captcha->SoundMimeType;
    header("Content-Type: {$mimeType}");
    header('Content-Transfer-Encoding: binary');
    if (!array_key_exists('d', $_GET)) {
        // javascript player not used, we send the file directly as a download
        $downloadId = LBD_CryptoHelper::GenerateGuid();
        header("Content-Disposition: attachment; filename=captcha_{$downloadId}.wav");
    }
    if (DetectIosRangeRequest()) {
        // iPhone/iPad sound issues workaround: chunked response for iOS clients
        // sound byte subset
        $range = GetSoundByteRange();
        $rangeStart = $range['start'];
        $rangeEnd = $range['end'];
        $rangeSize = $rangeEnd - $rangeStart + 1;
        // initial iOS 6.0.1 testing; leaving as fallback since we can't be sure it won't happen again:
        // we depend on observed behavior of invalid range requests to detect
        // end of sound playback, cleanup and tell AppleCoreMedia to stop requesting
        // invalid "bytes=rangeEnd-rangeEnd" ranges in an infinite(?) loop
        if ($rangeStart == $rangeEnd || $rangeEnd > $totalSize) {
            LBD_HttpHelper::BadRequest('invalid byte range');
        }
        $rangeBytes = substr($soundBytes, $rangeStart, $rangeSize);
        // partial content response with the requested byte range
        header('HTTP/1.1 206 Partial Content');
        header('Accept-Ranges: bytes');
        header("Content-Length: {$rangeSize}");
        header("Content-Range: bytes {$rangeStart}-{$rangeEnd}/{$totalSize}");
        echo $rangeBytes;
        // chrome needs this kind of response to be able to replay Html5 audio
    } else {
        if (DetectFakeRangeRequest()) {
            header('Accept-Ranges: bytes');
            header("Content-Length: {$totalSize}");
            $end = $totalSize - 1;
            header("Content-Range: bytes 0-{$end}/{$totalSize}");
            echo $soundBytes;
        } else {
            // regular sound request
            header('Accept-Ranges: none');
            header("Content-Length: {$totalSize}");
            echo $soundBytes;
        }
    }
}