UNPKG

js-tts-wrapper

Version:

A JavaScript/TypeScript library that provides a unified API for working with multiple cloud-based Text-to-Speech (TTS) services

501 lines (500 loc) 18.4 kB
"use strict"; /** * Utility functions for playing audio in Node.js */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.isNodeAudioAvailable = isNodeAudioAvailable; exports.playAudioInNode = playAudioInNode; exports.pauseAudioPlayback = pauseAudioPlayback; exports.resumeAudioPlayback = resumeAudioPlayback; exports.stopAudioPlayback = stopAudioPlayback; const environment_1 = require("./environment"); const audioState = { isPlaying: false, isPaused: false, currentProcess: null, tempFile: null, childProcess: null, fs: null, }; // This function is no longer used, but we keep it for reference // eslint-disable-next-line @typescript-eslint/no-unused-vars // @ts-ignore async function _unusedDynamicRequire(moduleName) { // In Node.js, we can use a dynamic import if (environment_1.isNode) { try { // For Node.js built-in modules if (moduleName.startsWith("node:")) { return await Promise.resolve(`${moduleName}`).then(s => __importStar(require(s))); } // For third-party modules return await Promise.resolve(`${moduleName}`).then(s => __importStar(require(s))); } catch (_importError) { // Fallback to global require if available try { // @ts-ignore return require(moduleName); } catch (_requireError) { // Ignore errors } } } throw new Error(`Failed to load module: ${moduleName}`); } /** * Check if Node.js audio playback is available * @returns True if Node.js audio playback is available */ async function isNodeAudioAvailable() { if (!environment_1.isNode) return false; try { // Try to load required modules const childProcess = await Promise.resolve().then(() => __importStar(require("node:child_process"))); const fs = await Promise.resolve().then(() => __importStar(require("node:fs"))); // Store for later use audioState.childProcess = childProcess; audioState.fs = fs; // Check if we have a suitable audio player const platform = process.platform; if (platform === "darwin") { // Check if afplay is available on macOS try { childProcess.execSync("which afplay", { stdio: "ignore" }); return true; } catch (_error) { return false; } } else if (platform === "win32") { // Windows should have PowerShell available return true; } else { // Check if aplay is available on Linux try { childProcess.execSync("which aplay", { stdio: "ignore" }); return true; } catch (_error) { return false; } } } catch (_error) { return false; } } // These functions are no longer used, but we keep them for reference // in case we need to handle WAV files in the future /** * Parse a WAV file header to extract audio format information * @param audioBytes Audio data as Uint8Array * @returns Object containing audio format information */ // eslint-disable-next-line @typescript-eslint/no-unused-vars // @ts-ignore function _unusedParseWavHeader(audioBytes) { const result = { sampleRate: 0, numChannels: 0, bitsPerSample: 0, dataOffset: 0, dataSize: 0, isValidWav: false, }; // Check if it's a valid WAV file (has RIFF header) const hasRiffHeader = audioBytes.length >= 4 && audioBytes[0] === 0x52 && // 'R' audioBytes[1] === 0x49 && // 'I' audioBytes[2] === 0x46 && // 'F' audioBytes[3] === 0x46; // 'F' if (!hasRiffHeader || audioBytes.length < 44) { return result; } result.isValidWav = true; result.numChannels = audioBytes[22] | (audioBytes[23] << 8); result.sampleRate = audioBytes[24] | (audioBytes[25] << 8) | (audioBytes[26] << 16) | (audioBytes[27] << 24); result.bitsPerSample = audioBytes[34] | (audioBytes[35] << 8); // Find the data chunk let offset = 36; while (offset < audioBytes.length - 8) { if (audioBytes[offset] === 0x64 && // 'd' audioBytes[offset + 1] === 0x61 && // 'a' audioBytes[offset + 2] === 0x74 && // 't' audioBytes[offset + 3] === 0x61 // 'a' ) { // Found the data chunk const dataSize = audioBytes[offset + 4] | (audioBytes[offset + 5] << 8) | (audioBytes[offset + 6] << 16) | (audioBytes[offset + 7] << 24); result.dataOffset = offset + 8; result.dataSize = dataSize; break; } offset += 4; // Skip the chunk size const chunkSize = audioBytes[offset] | (audioBytes[offset + 1] << 8) | (audioBytes[offset + 2] << 16) | (audioBytes[offset + 3] << 24); offset += 4 + chunkSize; } return result; } /** * Create a WAV file with the given audio data * @param audioBytes Audio data as Uint8Array * @param sampleRate Sample rate in Hz * @param numChannels Number of channels * @param bitsPerSample Bits per sample * @returns Uint8Array containing the WAV file */ // eslint-disable-next-line @typescript-eslint/no-unused-vars // @ts-ignore function _unusedCreateWavFile(audioBytes, sampleRate, numChannels, bitsPerSample) { // Calculate sizes const dataSize = audioBytes.length; const blockAlign = numChannels * (bitsPerSample / 8); const byteRate = sampleRate * blockAlign; // Create WAV header const headerSize = 44; // Standard WAV header size const wavFile = new Uint8Array(headerSize + dataSize); // RIFF header wavFile.set([0x52, 0x49, 0x46, 0x46]); // "RIFF" // Chunk size (file size - 8) const chunkSize = headerSize + dataSize - 8; wavFile[4] = chunkSize & 0xff; wavFile[5] = (chunkSize >> 8) & 0xff; wavFile[6] = (chunkSize >> 16) & 0xff; wavFile[7] = (chunkSize >> 24) & 0xff; wavFile.set([0x57, 0x41, 0x56, 0x45], 8); // "WAVE" // Format chunk wavFile.set([0x66, 0x6d, 0x74, 0x20], 12); // "fmt " wavFile[16] = 16; // Chunk size (16 for PCM) wavFile[17] = 0; wavFile[18] = 0; wavFile[19] = 0; wavFile[20] = 1; // Audio format (1 for PCM) wavFile[21] = 0; wavFile[22] = numChannels; // Number of channels wavFile[23] = 0; // Sample rate wavFile[24] = sampleRate & 0xff; wavFile[25] = (sampleRate >> 8) & 0xff; wavFile[26] = (sampleRate >> 16) & 0xff; wavFile[27] = (sampleRate >> 24) & 0xff; // Byte rate wavFile[28] = byteRate & 0xff; wavFile[29] = (byteRate >> 8) & 0xff; wavFile[30] = (byteRate >> 16) & 0xff; wavFile[31] = (byteRate >> 24) & 0xff; // Block align wavFile[32] = blockAlign & 0xff; wavFile[33] = (blockAlign >> 8) & 0xff; // Bits per sample wavFile[34] = bitsPerSample & 0xff; wavFile[35] = (bitsPerSample >> 8) & 0xff; // Data chunk wavFile.set([0x64, 0x61, 0x74, 0x61], 36); // "data" // Data size wavFile[40] = dataSize & 0xff; wavFile[41] = (dataSize >> 8) & 0xff; wavFile[42] = (dataSize >> 16) & 0xff; wavFile[43] = (dataSize >> 24) & 0xff; // Add audio data wavFile.set(audioBytes, 44); return wavFile; } /** * Create a WAV header for raw PCM audio data * @param audioData Raw PCM audio data * @param sampleRate Sample rate in Hz * @param numChannels Number of audio channels (default: 1) * @param bitsPerSample Bits per sample (default: 16) * @returns Uint8Array containing the complete WAV file */ function createWavFile(audioData, sampleRate, numChannels = 1, bitsPerSample = 16) { // Calculate sizes const dataSize = audioData.length; const blockAlign = numChannels * (bitsPerSample / 8); const byteRate = sampleRate * blockAlign; // Create WAV header (44 bytes) const headerSize = 44; const wavFile = new Uint8Array(headerSize + dataSize); // RIFF header wavFile.set([0x52, 0x49, 0x46, 0x46]); // "RIFF" // Chunk size (file size - 8) const chunkSize = headerSize + dataSize - 8; wavFile[4] = chunkSize & 0xff; wavFile[5] = (chunkSize >> 8) & 0xff; wavFile[6] = (chunkSize >> 16) & 0xff; wavFile[7] = (chunkSize >> 24) & 0xff; wavFile.set([0x57, 0x41, 0x56, 0x45], 8); // "WAVE" // Format chunk wavFile.set([0x66, 0x6d, 0x74, 0x20], 12); // "fmt " wavFile[16] = 16; // Chunk size (16 for PCM) wavFile[17] = 0; wavFile[18] = 0; wavFile[19] = 0; wavFile[20] = 1; // Audio format (1 for PCM) wavFile[21] = 0; wavFile[22] = numChannels; // Number of channels wavFile[23] = 0; // Sample rate wavFile[24] = sampleRate & 0xff; wavFile[25] = (sampleRate >> 8) & 0xff; wavFile[26] = (sampleRate >> 16) & 0xff; wavFile[27] = (sampleRate >> 24) & 0xff; // Byte rate wavFile[28] = byteRate & 0xff; wavFile[29] = (byteRate >> 8) & 0xff; wavFile[30] = (byteRate >> 16) & 0xff; wavFile[31] = (byteRate >> 24) & 0xff; // Block align wavFile[32] = blockAlign & 0xff; wavFile[33] = (blockAlign >> 8) & 0xff; // Bits per sample wavFile[34] = bitsPerSample & 0xff; wavFile[35] = (bitsPerSample >> 8) & 0xff; // Data chunk wavFile.set([0x64, 0x61, 0x74, 0x61], 36); // "data" // Data size wavFile[40] = dataSize & 0xff; wavFile[41] = (dataSize >> 8) & 0xff; wavFile[42] = (dataSize >> 16) & 0xff; wavFile[43] = (dataSize >> 24) & 0xff; // Add audio data wavFile.set(audioData, 44); return wavFile; } /** * Check if audio data is raw PCM (no WAV header) * @param audioBytes Audio data to check * @returns True if the data appears to be raw PCM */ function isRawPCM(audioBytes) { // Check if it's NOT a WAV file (doesn't start with RIFF header) const hasRiffHeader = audioBytes.length >= 4 && audioBytes[0] === 0x52 && // 'R' audioBytes[1] === 0x49 && // 'I' audioBytes[2] === 0x46 && // 'F' audioBytes[3] === 0x46; // 'F' return !hasRiffHeader; } /** * Play audio in Node.js * @param audioBytes Audio data as Uint8Array * @param sampleRate Sample rate in Hz (default: 24000 for WitAI, 16000 for others) * @param engineName Name of the TTS engine (used to determine if raw PCM conversion is needed) * @returns Promise that resolves when audio playback is complete */ async function playAudioInNode(audioBytes, sampleRate, engineName) { if (!environment_1.isNode) { throw new Error("This function can only be used in Node.js"); } // Stop any currently playing audio stopAudioPlayback(); try { // Load required modules if not already loaded if (!audioState.childProcess || !audioState.fs) { audioState.childProcess = await Promise.resolve().then(() => __importStar(require("node:child_process"))); audioState.fs = await Promise.resolve().then(() => __importStar(require("node:fs"))); } const os = await Promise.resolve().then(() => __importStar(require("node:os"))); const path = await Promise.resolve().then(() => __importStar(require("node:path"))); // Create a temporary file to play const tempDir = os.tmpdir(); const tempFile = path.join(tempDir, `tts-audio-${Date.now()}.wav`); audioState.tempFile = tempFile; // Determine if we need to add a WAV header let finalAudioBytes = audioBytes; // Check if this is raw PCM data that needs a WAV header if (isRawPCM(audioBytes)) { // Determine sample rate based on engine let actualSampleRate = sampleRate || 24000; // Default to WitAI's sample rate if (engineName === "witai") { actualSampleRate = 24000; } else if (engineName === "polly") { actualSampleRate = 16000; } finalAudioBytes = createWavFile(audioBytes, actualSampleRate); } // Write the audio data to the temp file audioState.fs.writeFileSync(tempFile, Buffer.from(finalAudioBytes)); console.log(`Audio saved to temporary file: ${tempFile}`); // Determine which player to use based on platform let command; let args; const platform = process.platform; if (platform === "darwin") { // macOS command = "afplay"; args = [tempFile]; } else if (platform === "win32") { // Windows command = "powershell"; args = ["-c", `(New-Object System.Media.SoundPlayer "${tempFile}").PlaySync()`]; } else { // Linux and others - try to use aplay command = "aplay"; args = ["-q", tempFile]; } console.log(`Playing audio with ${command}...`); return new Promise((resolve, reject) => { try { // Spawn the process const process = audioState.childProcess.spawn(command, args); audioState.currentProcess = process; audioState.isPlaying = true; audioState.isPaused = false; // Handle process events process.on("close", (code) => { console.log(`Audio playback process exited with code ${code}`); cleanupTempFile(); audioState.currentProcess = null; audioState.isPlaying = false; audioState.isPaused = false; resolve(); }); process.on("error", (err) => { console.error("Audio playback process error:", err); cleanupTempFile(); audioState.currentProcess = null; audioState.isPlaying = false; audioState.isPaused = false; reject(err); }); } catch (error) { console.error("Error starting audio playback:", error); cleanupTempFile(); audioState.currentProcess = null; audioState.isPlaying = false; audioState.isPaused = false; reject(error); } }); } catch (error) { cleanupTempFile(); throw error; } } /** * Clean up temporary audio file */ function cleanupTempFile() { if (audioState.tempFile && audioState.fs) { try { if (audioState.fs.existsSync(audioState.tempFile)) { audioState.fs.unlinkSync(audioState.tempFile); } } catch (error) { console.error("Error cleaning up temp file:", error); } audioState.tempFile = null; } } /** * Pause audio playback by stopping the current process * @returns True if playback was paused, false otherwise */ function pauseAudioPlayback() { if (audioState.currentProcess && audioState.isPlaying && !audioState.isPaused) { try { // We'll implement pause by killing the process // This is a simple approach that works across platforms console.log("Pausing audio playback..."); if (audioState.currentProcess.kill) { audioState.currentProcess.kill(); audioState.isPaused = true; return true; } } catch (error) { console.error("Error pausing audio playback:", error); } } return false; } /** * Resume audio playback is not supported in this implementation * @returns Always false as resume is not supported */ function resumeAudioPlayback() { console.log("Resume not supported in the current implementation"); return false; } /** * Stop audio playback * @returns True if playback was stopped, false otherwise */ function stopAudioPlayback() { if (audioState.currentProcess && (audioState.isPlaying || audioState.isPaused)) { try { // Kill the audio playback process if (audioState.currentProcess.kill) { audioState.currentProcess.kill(); } console.log("Stopped audio playback"); // Clean up cleanupTempFile(); audioState.currentProcess = null; audioState.isPlaying = false; audioState.isPaused = false; return true; } catch (error) { console.error("Error stopping audio playback:", error); // Reset state even if there was an error audioState.currentProcess = null; audioState.isPlaying = false; audioState.isPaused = false; } } return false; }