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

269 lines (268 loc) 9.12 kB
/** * Audio format conversion utilities * Provides conversion between different audio formats (WAV, MP3, OGG) */ import { isNode } from "./environment.js"; import { detectAudioFormat } from "./audio-input.js"; /** * Check if audio conversion is available in the current environment * * Audio format conversion is currently only supported in Node.js environments * due to the need for external tools like ffmpeg or native Node.js modules. * * In browser environments: * - Engines return their native format without conversion * - WebSpeech API engines use browser's native audio capabilities * - Format requests are honored only when engines natively support them */ export function isAudioConversionAvailable() { return isNode; // Currently only supported in Node.js } /** * Get MIME type for audio format */ export function getMimeTypeForFormat(format) { switch (format) { case "mp3": return "audio/mpeg"; case "ogg": return "audio/ogg"; case "wav": return "audio/wav"; default: return "audio/wav"; } } /** * Extract WAV audio parameters from WAV file header */ export function extractWavParameters(wavBytes) { if (wavBytes.length < 44) { return null; } // Check for RIFF header if (wavBytes[0] !== 0x52 || // 'R' wavBytes[1] !== 0x49 || // 'I' wavBytes[2] !== 0x46 || // 'F' wavBytes[3] !== 0x46 // 'F' ) { return null; } // Check for WAVE format if (wavBytes[8] !== 0x57 || // 'W' wavBytes[9] !== 0x41 || // 'A' wavBytes[10] !== 0x56 || // 'V' wavBytes[11] !== 0x45 // 'E' ) { return null; } // Extract parameters from WAV header const sampleRate = new DataView(wavBytes.buffer).getUint32(24, true); const channels = new DataView(wavBytes.buffer).getUint16(22, true); const bitsPerSample = new DataView(wavBytes.buffer).getUint16(34, true); // Find data chunk let dataOffset = 44; let dataSize = 0; // Look for 'data' chunk for (let i = 12; i < wavBytes.length - 8; i += 4) { if (wavBytes[i] === 0x64 && // 'd' wavBytes[i + 1] === 0x61 && // 'a' wavBytes[i + 2] === 0x74 && // 't' wavBytes[i + 3] === 0x61 // 'a' ) { dataSize = new DataView(wavBytes.buffer).getUint32(i + 4, true); dataOffset = i + 8; break; } } return { sampleRate, channels, bitsPerSample, dataOffset, dataSize }; } /** * Convert audio between formats using available Node.js tools */ export async function convertAudioFormat(inputBytes, targetFormat, options = {}) { if (!isNode) { throw new Error("Audio conversion is only available in Node.js environment"); } const inputFormat = detectAudioFormat(inputBytes); const inputFormatName = inputFormat === "audio/mpeg" ? "mp3" : inputFormat === "audio/ogg" ? "ogg" : "wav"; // If already in target format, return as-is if (inputFormatName === targetFormat) { return { audioBytes: inputBytes, format: targetFormat, mimeType: getMimeTypeForFormat(targetFormat) }; } // For now, we'll implement basic conversions // More sophisticated conversion can be added later with external libraries if (inputFormatName === "wav" && targetFormat === "mp3") { return await convertWavToMp3(inputBytes, options); } if (inputFormatName === "wav" && targetFormat === "ogg") { return await convertWavToOgg(inputBytes, options); } // For other conversions, we'll use external tools if available return await convertUsingExternalTools(inputBytes, inputFormatName, targetFormat, options); } /** * Convert WAV to MP3 using Node.js tools */ async function convertWavToMp3(wavBytes, options) { // For now, we'll use a simple approach with external tools // In a production environment, you might want to use libraries like node-lame or ffmpeg try { // Try to use ffmpeg if available return await convertUsingFfmpeg(wavBytes, "wav", "mp3", options); } catch (error) { // Fallback: return WAV with MP3 MIME type and warn console.warn("MP3 conversion not available, returning WAV data. Install ffmpeg for proper MP3 conversion."); return { audioBytes: wavBytes, format: "wav", // Actually WAV, but requested MP3 mimeType: "audio/wav" }; } } /** * Convert WAV to OGG using Node.js tools */ async function convertWavToOgg(wavBytes, options) { try { // Try to use ffmpeg if available return await convertUsingFfmpeg(wavBytes, "wav", "ogg", options); } catch (error) { // Fallback: return WAV with OGG MIME type and warn console.warn("OGG conversion not available, returning WAV data. Install ffmpeg for proper OGG conversion."); return { audioBytes: wavBytes, format: "wav", // Actually WAV, but requested OGG mimeType: "audio/wav" }; } } /** * Convert audio using external tools (ffmpeg, etc.) */ async function convertUsingExternalTools(inputBytes, inputFormat, targetFormat, options) { try { return await convertUsingFfmpeg(inputBytes, inputFormat, targetFormat, options); } catch (error) { // Fallback: return original data with warning console.warn(`Audio conversion from ${inputFormat} to ${targetFormat} not available. Returning original format.`); return { audioBytes: inputBytes, format: inputFormat, mimeType: getMimeTypeForFormat(inputFormat) }; } } /** * Convert audio using ffmpeg (if available) */ async function convertUsingFfmpeg(inputBytes, inputFormat, targetFormat, options) { const { spawn } = await import("node:child_process"); const { tmpdir } = await import("node:os"); const { join } = await import("node:path"); const { writeFileSync, readFileSync, unlinkSync, existsSync } = await import("node:fs"); // Create temporary files const inputFile = join(tmpdir(), `audio-convert-input-${Date.now()}.${inputFormat}`); const outputFile = join(tmpdir(), `audio-convert-output-${Date.now()}.${targetFormat}`); try { // Write input file writeFileSync(inputFile, inputBytes); // Build ffmpeg command const args = [ "-i", inputFile, "-y", // Overwrite output file ]; // Add format-specific options if (targetFormat === "mp3") { args.push("-codec:a", "libmp3lame"); if (options.bitRate) { args.push("-b:a", `${options.bitRate}k`); } } else if (targetFormat === "ogg") { args.push("-codec:a", "libvorbis"); if (options.quality !== undefined) { args.push("-q:a", options.quality.toString()); } } // Add sample rate if specified if (options.sampleRate) { args.push("-ar", options.sampleRate.toString()); } args.push(outputFile); // Execute ffmpeg await new Promise((resolve, reject) => { const ffmpeg = spawn("ffmpeg", args, { stdio: "pipe" }); ffmpeg.on("close", (code) => { if (code === 0) { resolve(); } else { reject(new Error(`ffmpeg exited with code ${code}`)); } }); ffmpeg.on("error", (error) => { reject(new Error(`ffmpeg error: ${error.message}`)); }); }); // Read converted file if (!existsSync(outputFile)) { throw new Error("ffmpeg did not produce output file"); } const convertedBytes = readFileSync(outputFile); return { audioBytes: new Uint8Array(convertedBytes), format: targetFormat, mimeType: getMimeTypeForFormat(targetFormat) }; } finally { // Clean up temporary files try { if (existsSync(inputFile)) unlinkSync(inputFile); if (existsSync(outputFile)) unlinkSync(outputFile); } catch (cleanupError) { console.warn("Could not clean up temporary files:", cleanupError); } } } /** * Check if ffmpeg is available */ export async function isFFmpegAvailable() { if (!isNode) { return false; } try { const { spawn } = await import("node:child_process"); return new Promise((resolve) => { const ffmpeg = spawn("ffmpeg", ["-version"], { stdio: "pipe" }); ffmpeg.on("close", (code) => { resolve(code === 0); }); ffmpeg.on("error", () => { resolve(false); }); }); } catch { return false; } }