"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.downloadAudioFile = downloadAudioFile;
exports.convertAudioToPCMU = convertAudioToPCMU;
exports.readPCMUFromWAV = readPCMUFromWAV;
exports.streamAudioFile = streamAudioFile;
const fluent_ffmpeg_1 = __importDefault(require("fluent-ffmpeg"));
const fs_1 = require("fs");
const http_1 = __importDefault(require("http"));
const https_1 = __importDefault(require("https"));
const path_1 = require("path");
const logger_1 = __importDefault(require("../../logger"));
// Try to set ffmpeg path from installer, fallback to system ffmpeg
try {
    // eslint-disable-next-line @typescript-eslint/no-require-imports
    const ffmpegInstaller = require('@ffmpeg-installer/ffmpeg');
    // @ts-ignore - setFfmpegPath is a method on the default export
    const ffmpegModule = fluent_ffmpeg_1.default;
    if (ffmpegModule.setFfmpegPath) {
        ffmpegModule.setFfmpegPath(ffmpegInstaller.path);
        //logger.info('[Audio Utils] Using ffmpeg from @ffmpeg-installer');
    }
}
catch {
    // Fallback to system ffmpeg (must be in PATH)
    // logger.warn('[Audio Utils] @ffmpeg-installer not found, using system ffmpeg (must be in PATH)');
}
/**
 * Download audio file from URL
 */
async function downloadAudioFile(url) {
    return new Promise((resolve, reject) => {
        const protocol = url.startsWith('https') ? https_1.default : http_1.default;
        protocol
            .get(url, (response) => {
            if (response.statusCode !== 200) {
                reject(new Error(`Failed to download: ${response.statusCode}`));
                return;
            }
            const chunks = [];
            response.on('data', (chunk) => chunks.push(chunk));
            response.on('end', () => {
                const buffer = Buffer.concat(chunks);
                resolve(buffer);
            });
            response.on('error', reject);
        })
            .on('error', reject);
    });
}
/**
 * Check if audio buffer is already in PCMU WAV format
 * This verifies the WAV header structure and PCMU format code
 */
function isPCMUWAV(audioBuffer) {
    // Check for minimum WAV header size
    if (audioBuffer.length < 44) {
        logger_1.default.info('[Audio Conversion] Buffer too small to be a valid WAV file');
        return false;
    }
    // Check for RIFF header
    const header = audioBuffer.toString('ascii', 0, 4);
    if (header !== 'RIFF') {
        logger_1.default.info('[Audio Conversion] Not a RIFF file');
        return false;
    }
    // Check for WAVE format
    const format = audioBuffer.toString('ascii', 8, 12);
    if (format !== 'WAVE') {
        logger_1.default.info('[Audio Conversion] Not a WAVE file');
        return false;
    }
    // Check audio format code (offset 20-21)
    // Format 7 = G.711 mu-law (PCMU)
    const audioFormat = audioBuffer.readUInt16LE(20);
    if (audioFormat !== 7) {
        logger_1.default.info(`[Audio Conversion] Audio format is ${audioFormat}, expected 7 (PCMU)`);
        return false;
    }
    // Check number of channels (offset 22-23, should be 1 for mono)
    const channels = audioBuffer.readUInt16LE(22);
    if (channels !== 1) {
        logger_1.default.info(`[Audio Conversion] Channels is ${channels}, expected 1 (mono)`);
        return false;
    }
    // Check sample rate (offset 24-27, should be 8000 Hz)
    const sampleRate = audioBuffer.readUInt32LE(24);
    if (sampleRate !== 8000) {
        logger_1.default.info(`[Audio Conversion] Sample rate is ${sampleRate}Hz, expected 8000Hz`);
        return false;
    }
    // Check byte rate (offset 28-31, should be 8000 for PCMU mono)
    const byteRate = audioBuffer.readUInt32LE(28);
    if (byteRate !== 8000) {
        logger_1.default.info(`[Audio Conversion] Byte rate is ${byteRate}, expected 8000`);
        return false;
    }
    // Check block align (offset 32-33, should be 1 for PCMU)
    const blockAlign = audioBuffer.readUInt16LE(32);
    if (blockAlign !== 1) {
        logger_1.default.info(`[Audio Conversion] Block align is ${blockAlign}, expected 1`);
        return false;
    }
    // Check bits per sample (offset 34-35, should be 8 for PCMU)
    const bitsPerSample = audioBuffer.readUInt16LE(34);
    if (bitsPerSample !== 8) {
        logger_1.default.info(`[Audio Conversion] Bits per sample is ${bitsPerSample}, expected 8`);
        return false;
    }
    logger_1.default.info('[Audio Conversion] Audio is already in PCMU WAV format (8kHz, mono, G.711 mu-law)');
    return true;
}
/**
 * Convert PCM16 (16-bit signed) to μ-law (8-bit)
 * μ-law encoding algorithm for telephony
 */
function pcm16ToMulaw(pcm16Buffer) {
    const mulawBuffer = Buffer.alloc(pcm16Buffer.length / 2); // μ-law is 8-bit (half the size)
    // μ-law encoding constants
    const BIAS = 0x84;
    const CLIP = 32635;
    for (let i = 0; i < mulawBuffer.length; i++) {
        // Read 16-bit signed PCM sample (little-endian)
        let pcm = pcm16Buffer.readInt16LE(i * 2);
        // Clip to valid range
        if (pcm > CLIP) {
            pcm = CLIP;
        }
        if (pcm < -CLIP) {
            pcm = -CLIP;
        }
        // Get sign bit
        const sign = (pcm >>> 8) & 0x80;
        // Get magnitude
        if (pcm < 0) {
            pcm = -pcm;
        }
        // Add bias
        pcm += BIAS;
        // Find exponent (0-7)
        let exponent = 7;
        for (let exp = 0; exp < 8; exp++) {
            if (pcm <= 0x1f << (exp + 2)) {
                exponent = exp;
                break;
            }
        }
        // Calculate mantissa (4 bits)
        const mantissa = (pcm >> (exponent + 3)) & 0x0f;
        // Combine sign, exponent, and mantissa
        const mulawByte = ~(sign | (exponent << 4) | mantissa);
        mulawBuffer[i] = mulawByte & 0xff;
    }
    return mulawBuffer;
}
/**
 * Convert PCM WAV to PCMU WAV format (without ffmpeg)
 * Handles PCM format 1 (uncompressed) WAV files
 */
function convertPCMWAVToPCMU(audioBuffer) {
    if (audioBuffer.length < 44) {
        throw new Error('Invalid WAV file: too small');
    }
    // Read WAV header
    const header = audioBuffer.toString('ascii', 0, 4);
    const format = audioBuffer.toString('ascii', 8, 12);
    if (header !== 'RIFF' || format !== 'WAVE') {
        throw new Error('Invalid WAV file: not a RIFF WAVE file');
    }
    const audioFormat = audioBuffer.readUInt16LE(20);
    if (audioFormat !== 1) {
        throw new Error(`Unsupported audio format: ${audioFormat} (expected 1 for PCM)`);
    }
    const channels = audioBuffer.readUInt16LE(22);
    const sampleRate = audioBuffer.readUInt32LE(24);
    const bitsPerSample = audioBuffer.readUInt16LE(34);
    logger_1.default.info(`[Audio Conversion] Converting PCM WAV: ${channels}ch, ${sampleRate}Hz, ${bitsPerSample}bit`);
    // Find data chunk
    let dataOffset = 44;
    let dataSize = 0;
    let foundDataChunk = false;
    for (let i = 12; i < audioBuffer.length - 8; i++) {
        if (audioBuffer.toString('ascii', i, i + 4) === 'data') {
            dataOffset = i + 8;
            dataSize = audioBuffer.readUInt32LE(i + 4);
            foundDataChunk = true;
            break;
        }
    }
    if (!foundDataChunk) {
        throw new Error('Invalid WAV file: data chunk not found');
    }
    // Extract PCM audio data
    const pcmData = audioBuffer.slice(dataOffset, dataOffset + dataSize);
    // Convert sample rate if needed (simple linear resampling)
    let processedPcmData = pcmData;
    if (sampleRate !== 8000) {
        logger_1.default.info(`[Audio Conversion] Resampling from ${sampleRate}Hz to 8000Hz`);
        const ratio = 8000 / sampleRate;
        const numSamples = Math.floor((pcmData.length / (bitsPerSample / 8) / channels) * ratio);
        const resampledSize = numSamples * (bitsPerSample / 8) * channels;
        const resampledBuffer = Buffer.alloc(resampledSize);
        for (let i = 0; i < numSamples; i++) {
            const srcIndex = Math.floor(i / ratio);
            const srcOffset = srcIndex * (bitsPerSample / 8) * channels;
            const dstOffset = i * (bitsPerSample / 8) * channels;
            if (bitsPerSample === 16) {
                for (let ch = 0; ch < channels; ch++) {
                    const sample = pcmData.readInt16LE(srcOffset + ch * 2);
                    resampledBuffer.writeInt16LE(sample, dstOffset + ch * 2);
                }
            }
            else if (bitsPerSample === 8) {
                for (let ch = 0; ch < channels; ch++) {
                    const sample = pcmData.readInt8(srcOffset + ch);
                    resampledBuffer.writeInt8(sample, dstOffset + ch);
                }
            }
        }
        processedPcmData = resampledBuffer;
    }
    // Convert to mono if needed
    let monoPcmData = processedPcmData;
    if (channels > 1 && bitsPerSample === 16) {
        logger_1.default.info(`[Audio Conversion] Converting ${channels} channels to mono`);
        const numSamples = processedPcmData.length / (bitsPerSample / 8) / channels;
        const monoBuffer = Buffer.alloc(numSamples * 2);
        for (let i = 0; i < numSamples; i++) {
            let sum = 0;
            for (let ch = 0; ch < channels; ch++) {
                sum += processedPcmData.readInt16LE(i * channels * 2 + ch * 2);
            }
            const avg = Math.round(sum / channels);
            monoBuffer.writeInt16LE(avg, i * 2);
        }
        monoPcmData = monoBuffer;
    }
    else if (channels > 1 && bitsPerSample === 8) {
        logger_1.default.info(`[Audio Conversion] Converting ${channels} channels to mono`);
        const numSamples = processedPcmData.length / channels;
        const monoBuffer = Buffer.alloc(numSamples);
        for (let i = 0; i < numSamples; i++) {
            let sum = 0;
            for (let ch = 0; ch < channels; ch++) {
                sum += processedPcmData.readInt8(i * channels + ch);
            }
            const avg = Math.round(sum / channels);
            monoBuffer.writeInt8(avg, i);
        }
        monoPcmData = monoBuffer;
    }
    // Convert to 16-bit if needed
    let pcm16Data = monoPcmData;
    if (bitsPerSample === 8) {
        logger_1.default.info('[Audio Conversion] Converting 8-bit to 16-bit PCM');
        const numSamples = monoPcmData.length;
        const pcm16Buffer = Buffer.alloc(numSamples * 2);
        for (let i = 0; i < numSamples; i++) {
            const sample8 = monoPcmData.readInt8(i);
            const sample16 = (sample8 - 128) * 256; // Convert unsigned 8-bit to signed 16-bit
            pcm16Buffer.writeInt16LE(sample16, i * 2);
        }
        pcm16Data = pcm16Buffer;
    }
    // Convert PCM16 to μ-law
    logger_1.default.info('[Audio Conversion] Converting PCM16 to μ-law (PCMU)');
    const mulawData = pcm16ToMulaw(pcm16Data);
    // Create new WAV file with PCMU format
    const outputBuffer = Buffer.alloc(44 + mulawData.length);
    let offset = 0;
    // RIFF header
    outputBuffer.write('RIFF', offset);
    offset += 4;
    outputBuffer.writeUInt32LE(36 + mulawData.length, offset); // File size - 8
    offset += 4;
    outputBuffer.write('WAVE', offset);
    offset += 4;
    // fmt chunk
    outputBuffer.write('fmt ', offset);
    offset += 4;
    outputBuffer.writeUInt32LE(18, offset); // fmt chunk size
    offset += 4;
    outputBuffer.writeUInt16LE(7, offset); // Audio format: 7 = G.711 mu-law
    offset += 2;
    outputBuffer.writeUInt16LE(1, offset); // Channels: 1 = mono
    offset += 2;
    outputBuffer.writeUInt32LE(8000, offset); // Sample rate: 8000 Hz
    offset += 4;
    outputBuffer.writeUInt32LE(8000, offset); // Byte rate: 8000
    offset += 4;
    outputBuffer.writeUInt16LE(1, offset); // Block align: 1
    offset += 2;
    outputBuffer.writeUInt16LE(8, offset); // Bits per sample: 8
    offset += 2;
    outputBuffer.writeUInt16LE(0, offset); // Extension size (0 for PCMU)
    offset += 2;
    // data chunk
    outputBuffer.write('data', offset);
    offset += 4;
    outputBuffer.writeUInt32LE(mulawData.length, offset); // Data size
    offset += 4;
    mulawData.copy(outputBuffer, offset);
    logger_1.default.info(`[Audio Conversion] Created PCMU WAV: ${mulawData.length} bytes of PCMU data, total file size: ${outputBuffer.length} bytes`);
    return outputBuffer;
}
/**
 * Convert audio buffer to PCMU (G.711 mu-law) format
 */
async function convertAudioToPCMU(audioBuffer, outputPath) {
    // Check if audio is already in PCMU WAV format
    if (isPCMUWAV(audioBuffer)) {
        // Already in correct format, just write it directly
        (0, fs_1.writeFileSync)(outputPath, audioBuffer);
        logger_1.default.info(`[Audio Conversion] Audio already in PCMU format, copied to: ${outputPath}`);
        return Promise.resolve();
    }
    // Check if it's a PCM WAV file that we can convert without ffmpeg
    try {
        const header = audioBuffer.toString('ascii', 0, 4);
        const format = audioBuffer.toString('ascii', 8, 12);
        if (header === 'RIFF' && format === 'WAVE') {
            const audioFormat = audioBuffer.readUInt16LE(20);
            if (audioFormat === 1) {
                // PCM format - try to convert without ffmpeg
                logger_1.default.info('[Audio Conversion] Attempting to convert PCM WAV to PCMU without ffmpeg');
                const pcmuBuffer = convertPCMWAVToPCMU(audioBuffer);
                (0, fs_1.writeFileSync)(outputPath, pcmuBuffer);
                logger_1.default.info(`[Audio Conversion] Successfully converted PCM to PCMU: ${outputPath}`);
                return Promise.resolve();
            }
        }
    }
    catch (conversionError) {
        logger_1.default.warn(`[Audio Conversion] Failed to convert PCM to PCMU without ffmpeg: ${conversionError.message}. Falling back to ffmpeg.`);
        // Fall through to ffmpeg conversion
    }
    return new Promise((resolve, reject) => {
        // Write input buffer to temp file
        const tempInputPath = outputPath + '.input';
        (0, fs_1.writeFileSync)(tempInputPath, audioBuffer);
        logger_1.default.info(`[Audio Conversion] Converting audio file: ${tempInputPath} -> ${outputPath}`);
        // Check if ffmpeg is available
        try {
            // @ts-ignore - ffmpeg types are complex, using any for the command chain
            const command = (0, fluent_ffmpeg_1.default)(tempInputPath);
            command
                .audioCodec('pcm_mulaw') // G.711 mu-law
                .audioFrequency(8000) // 8kHz sample rate (Twilio requirement)
                .audioChannels(1) // Mono
                .format('wav') // WAV container
                .on('start', (commandLine) => {
                logger_1.default.info(`[Audio Conversion] FFmpeg command: ${commandLine}`);
            })
                .on('progress', (progress) => {
                if (progress.percent) {
                    logger_1.default.info(`[Audio Conversion] Progress: ${Math.round(progress.percent)}%`);
                }
            })
                .on('end', () => {
                logger_1.default.info(`[Audio Conversion] Conversion completed: ${outputPath}`);
                // Clean up temp input file
                try {
                    (0, fs_1.unlinkSync)(tempInputPath);
                }
                catch {
                    // Ignore cleanup errors
                }
                resolve();
            })
                .on('error', (err) => {
                logger_1.default.error(`[Audio Conversion] FFmpeg error: ${err.message}`);
                // Clean up temp input file
                try {
                    (0, fs_1.unlinkSync)(tempInputPath);
                }
                catch {
                    // Ignore cleanup errors
                }
                // If ffmpeg is not found, provide helpful error message
                if (err.message.includes('Cannot find ffmpeg') || err.message.includes('ffmpeg')) {
                    reject(new Error('FFmpeg is required for audio conversion but was not found. Please install ffmpeg: https://ffmpeg.org/download.html or install @ffmpeg-installer/ffmpeg package.'));
                }
                else {
                    reject(err);
                }
            })
                .save(outputPath);
        }
        catch (setupError) {
            // Clean up temp input file
            try {
                (0, fs_1.unlinkSync)(tempInputPath);
            }
            catch {
                // Ignore cleanup errors
            }
            reject(new Error(`Failed to initialize ffmpeg: ${setupError.message}. Please ensure ffmpeg is installed.`));
        }
    });
}
/**
 * Read PCMU audio from WAV file and return as base64 chunks
 */
function readPCMUFromWAV(wavPath) {
    const wavBuffer = (0, fs_1.readFileSync)(wavPath);
    logger_1.default.info(`[Audio Read] WAV file size: ${wavBuffer.length} bytes`);
    // WAV header structure for G.711 mu-law can vary
    // Find the 'data' chunk marker
    let dataOffset = 44; // Default for standard WAV
    let foundDataChunk = false;
    for (let i = 0; i < wavBuffer.length - 4; i++) {
        if (wavBuffer.toString('ascii', i, i + 4) === 'data') {
            dataOffset = i + 8; // Skip 'data' (4 bytes) + chunk size (4 bytes)
            foundDataChunk = true;
            logger_1.default.info(`[Audio Read] Found 'data' chunk at offset: ${i}, data starts at: ${dataOffset}`);
            break;
        }
    }
    if (!foundDataChunk) {
        logger_1.default.warn(`[Audio Read] 'data' chunk not found, using default offset: ${dataOffset}`);
    }
    // Extract PCMU audio data (skip WAV header)
    const pcmuData = wavBuffer.slice(dataOffset);
    logger_1.default.info(`[Audio Read] PCMU data size: ${pcmuData.length} bytes (after skipping ${dataOffset} byte header)`);
    if (pcmuData.length === 0) {
        logger_1.default.error('[Audio Read] No PCMU data found in WAV file!');
        return [];
    }
    // Split into 160-byte chunks (20ms at 8kHz for PCMU)
    const chunkSize = 160;
    const chunks = [];
    for (let i = 0; i < pcmuData.length; i += chunkSize) {
        const chunk = pcmuData.slice(i, i + chunkSize);
        chunks.push(chunk.toString('base64'));
    }
    logger_1.default.info(`[Audio Read] Created ${chunks.length} chunks from ${pcmuData.length} bytes of PCMU data`);
    if (chunks.length > 0) {
        logger_1.default.info(`[Audio Read] First chunk size: ${chunks[0].length} chars (base64), raw size: ${Buffer.from(chunks[0], 'base64').length} bytes`);
    }
    return chunks;
}
/**
 * Stream audio file through media stream WebSocket
 */
async function streamAudioFile(connection, // WebSocket connection
streamSid, audioUrl, latestMediaTimestampRef, isConnectionClosed, tempDir) {
    try {
        logger_1.default.info(`[Audio Stream] Starting to download and convert audio: ${audioUrl}`);
        // Download audio file
        const audioBuffer = await downloadAudioFile(audioUrl);
        logger_1.default.info(`[Audio Stream] Downloaded audio file: ${audioBuffer.length} bytes`);
        // Convert to PCMU format (or use directly if already in correct format)
        const tempWavPath = (0, path_1.join)(tempDir, `audio_${Date.now()}.wav`);
        try {
            (0, fs_1.mkdirSync)(tempDir, { recursive: true });
        }
        catch {
            // Directory might already exist
        }
        try {
            await convertAudioToPCMU(audioBuffer, tempWavPath);
            logger_1.default.info('[Audio Stream] Audio processed (converted or already in PCMU format)');
        }
        catch (conversionError) {
            // If conversion fails due to missing ffmpeg, verify if audio is already in PCMU format
            if (conversionError.message.includes('ffmpeg') ||
                conversionError.message.includes('FFmpeg')) {
                // Check if audio is already in PCMU WAV format
                if (isPCMUWAV(audioBuffer)) {
                    logger_1.default.info('[Audio Stream] FFmpeg not available, but audio is already in PCMU format - using directly');
                    // Write the PCMU WAV file directly
                    (0, fs_1.writeFileSync)(tempWavPath, audioBuffer);
                }
                else {
                    // Audio is not in PCMU format and ffmpeg is not available
                    throw new Error('FFmpeg is required to convert audio to PCMU format. The audio file is not already in PCMU WAV format (8kHz, mono, G.711 mu-law). Please install ffmpeg or ensure audio files are in the correct format.');
                }
            }
            else {
                throw conversionError;
            }
        }
        // Read PCMU chunks
        const pcmuChunks = readPCMUFromWAV(tempWavPath);
        logger_1.default.info(`[Audio Stream] Read ${pcmuChunks.length} PCMU chunks (total size: ${pcmuChunks.length * 160} bytes)`);
        if (pcmuChunks.length === 0) {
            throw new Error('No PCMU chunks extracted from audio file');
        }
        // Clean up temp file
        try {
            (0, fs_1.unlinkSync)(tempWavPath);
        }
        catch {
            // Ignore cleanup errors
        }
        // Stream chunks through media stream
        // Use the latest media timestamp from Twilio, or start from 0
        let timestamp = Number(latestMediaTimestampRef.current) || 0;
        // Ensure timestamp is a valid number
        if (!Number.isFinite(timestamp) || timestamp < 0) {
            timestamp = 0;
            logger_1.default.warn('[Audio Stream] Warning: Invalid starting timestamp, using 0. Audio may appear at the beginning');
        }
        const chunkInterval = 20; // 20ms per chunk (160 bytes at 8kHz)
        logger_1.default.info(`[Audio Stream] Starting to stream ${pcmuChunks.length} chunks, starting timestamp: ${timestamp}ms`);
        logger_1.default.info(`[Audio Stream] latestMediaTimestampRef.current: ${latestMediaTimestampRef.current}ms`);
        // Send a few silent chunks first to "prime" the stream and ensure Twilio is ready
        // PCMU silence is typically 0xFF (mu-law encoded silence)
        const silentChunk = Buffer.alloc(160, 0xff).toString('base64'); // 20ms of silence at 8kHz
        const primingChunks = 5; // 100ms of silence to prime the stream
        logger_1.default.info(`[Audio Stream] Sending ${primingChunks} silent chunks to prime the stream`);
        for (let i = 0; i < primingChunks; i++) {
            if (isConnectionClosed()) {
                logger_1.default.info('[Audio Stream] Connection closed during priming, stopping');
                return;
            }
            const primingEvent = {
                event: 'media',
                streamSid,
                media: {
                    payload: silentChunk,
                },
            };
            try {
                if (connection.readyState !== 1) {
                    logger_1.default.warn(`[Audio Stream] Connection not open during priming (state: ${connection.readyState}), stopping`);
                    return;
                }
                connection.send(JSON.stringify(primingEvent));
                await new Promise((resolve) => setTimeout(resolve, 20)); // 20ms delay between chunks
            }
            catch (sendError) {
                logger_1.default.error(`[Audio Stream] Error sending priming chunk ${i}: ${sendError.message}`);
                return;
            }
        }
        logger_1.default.info('[Audio Stream] Priming complete, starting actual audio stream');
        // Send chunks with optimized timing for smooth playback
        // Strategy: Send initial chunks quickly to build buffer, then maintain steady stream
        const initialBurstSize = 20; // Send first 20 chunks (400ms) quickly to build buffer
        const sendDelay = 15; // Slightly faster than real-time (20ms) to maintain buffer
        const burstDelay = 5; // Very fast for initial burst
        let chunksSent = 0;
        const startTime = Date.now();
        // Send initial burst quickly to build buffer
        logger_1.default.info(`[Audio Stream] Sending initial burst of ${initialBurstSize} chunks to build buffer`);
        for (let i = 0; i < Math.min(initialBurstSize, pcmuChunks.length); i++) {
            const chunk = pcmuChunks[i];
            if (isConnectionClosed()) {
                logger_1.default.info('[Audio Stream] Connection closed during initial burst, stopping');
                return;
            }
            const mediaEvent = {
                event: 'media',
                streamSid,
                media: {
                    payload: chunk,
                },
            };
            try {
                if (connection.readyState !== 1) {
                    logger_1.default.warn(`[Audio Stream] Connection not open during burst, stopping`);
                    return;
                }
                connection.send(JSON.stringify(mediaEvent));
                chunksSent++;
                // Update timestamp
                const currentTimestamp = typeof timestamp === 'number' ? timestamp : Number(timestamp) || 0;
                timestamp = currentTimestamp + chunkInterval;
                if (Number.isFinite(timestamp) && timestamp >= 0) {
                    latestMediaTimestampRef.current = timestamp;
                }
                // Small delay for burst (very fast)
                if (i < Math.min(initialBurstSize, pcmuChunks.length) - 1) {
                    await new Promise((resolve) => setTimeout(resolve, burstDelay));
                }
            }
            catch (sendError) {
                logger_1.default.error(`[Audio Stream] Error sending burst chunk ${i}: ${sendError.message}`);
                return;
            }
        }
        logger_1.default.info(`[Audio Stream] Initial burst complete, continuing with steady stream`);
        // Continue with steady streaming for remaining chunks
        for (let i = initialBurstSize; i < pcmuChunks.length; i++) {
            const chunk = pcmuChunks[i];
            // Check if connection is closed before sending
            if (isConnectionClosed()) {
                logger_1.default.info('[Audio Stream] Connection closed during streaming, stopping');
                break;
            }
            // Send media event to Twilio
            // Note: Timestamp is not needed when sending TO Twilio, only when receiving FROM Twilio
            const mediaEvent = {
                event: 'media',
                streamSid,
                media: {
                    payload: chunk,
                },
            };
            try {
                // Verify connection is still open before sending
                if (connection.readyState !== 1) {
                    // 1 = WebSocket.OPEN
                    logger_1.default.warn(`[Audio Stream] Connection not open (state: ${connection.readyState}), stopping`);
                    break;
                }
                // Send chunk immediately (non-blocking)
                // Note: connection.send() may throw synchronously, but write errors
                // can also occur asynchronously via the 'error' event
                try {
                    if (connection.readyState !== 1) {
                        // Connection closed, stop streaming
                        logger_1.default.info('[Audio Stream] Connection closed during stream, stopping');
                        break;
                    }
                    connection.send(JSON.stringify(mediaEvent));
                    chunksSent++;
                }
                catch (sendErr) {
                    // Handle synchronous errors
                    const errMsg = sendErr.message || String(sendErr);
                    const errCode = sendErr.code;
                    if (errMsg.includes('write EOF') ||
                        errMsg.includes('EOF') ||
                        errCode === 'EPIPE' ||
                        errCode === 'ECONNRESET' ||
                        errCode === 'WSAECONNRESET') {
                        logger_1.default.info('[Audio Stream] Connection closed during send, stopping stream');
                        break;
                    }
                    // Log other errors but don't crash
                    logger_1.default.warn(`[Audio Stream] Error sending chunk ${i}: ${errMsg}`);
                    break;
                }
                // Log first chunk and then progress every 50 chunks
                if (chunksSent === 1) {
                    logger_1.default.info(`[Audio Stream] First chunk sent successfully (chunk size: ${chunk.length} chars, base64)`);
                }
                if (chunksSent % 50 === 0) {
                    logger_1.default.info(`[Audio Stream] Sent ${chunksSent}/${pcmuChunks.length} chunks`);
                }
            }
            catch (sendError) {
                logger_1.default.error(`[Audio Stream] Error sending chunk ${i}: ${sendError.message}`);
                logger_1.default.error(`[Audio Stream] Connection state: ${connection.readyState}`);
                break;
            }
            // Increment timestamp for next chunk (20ms per chunk)
            const currentTimestamp = typeof timestamp === 'number' ? timestamp : Number(timestamp) || 0;
            timestamp = currentTimestamp + chunkInterval;
            // Safety check: prevent timestamp from becoming too large (reset if over 24 hours)
            const maxTimestamp = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
            if (timestamp > maxTimestamp) {
                logger_1.default.warn(`[Audio Stream] Timestamp exceeded max (${timestamp}ms > ${maxTimestamp}ms), resetting to 0`);
                timestamp = 0;
            }
            // Update latest media timestamp after incrementing (only if it's a valid number)
            if (Number.isFinite(timestamp) && timestamp >= 0) {
                latestMediaTimestampRef.current = timestamp;
            }
            // Small delay between chunks (but not after the last chunk)
            // Using a slightly faster rate to build buffer and prevent gaps
            if (i < pcmuChunks.length - 1) {
                await new Promise((resolve) => setTimeout(resolve, sendDelay));
            }
        }
        const totalTime = Date.now() - startTime;
        const expectedTime = pcmuChunks.length * 20; // 20ms per chunk
        logger_1.default.info(`[Audio Stream] Streaming completed in ${totalTime}ms (expected: ${expectedTime}ms, ratio: ${((totalTime / expectedTime) * 100).toFixed(1)}%)`);
        logger_1.default.info(`[Audio Stream] Finished streaming ${chunksSent}/${pcmuChunks.length} chunks`);
    }
    catch (error) {
        logger_1.default.error(`[Audio Stream] Error streaming audio file: ${error.message}`);
        throw error;
    }
}
//# sourceMappingURL=audio-utils-phone.js.map