UNPKG

@aituber-onair/voice

Version:

Voice synthesis library for AITuber OnAir

180 lines (179 loc) 6.59 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.NodeAudioPlayer = void 0; const wavHeader_1 = require("../../utils/wavHeader"); /** * Node.js-based audio player implementation * Uses optional dependencies for audio playback */ class NodeAudioPlayer { constructor() { this.isPlayingAudio = false; this.currentProcess = null; } async play(audioBuffer) { try { this.isPlayingAudio = true; // Try to use available audio playback libraries // First, try node-speaker (requires native compilation) try { const Speaker = await this.tryRequire('speaker'); if (Speaker) { return await this.playWithSpeaker(audioBuffer, Speaker); } } catch (e) { // Speaker not available } // Try play-sound (uses system audio players) try { const player = await this.tryRequire('play-sound'); if (player) { return await this.playWithPlaySound(audioBuffer, player); } } catch (e) { // play-sound not available } // If no audio player is available, just complete immediately console.warn('No audio playback library available in Node.js environment. Audio will not be played.'); this.handlePlaybackEnd(); return Promise.resolve(); } catch (error) { this.isPlayingAudio = false; throw error; } } async playWithSpeaker(audioBuffer, Speaker) { return new Promise((resolve, reject) => { try { // Parse WAV header to get correct audio format const audioFormat = (0, wavHeader_1.getAudioFormat)(audioBuffer); const speaker = new Speaker({ channels: audioFormat.channels, bitDepth: audioFormat.bitsPerSample, sampleRate: audioFormat.sampleRate, }); speaker.on('close', () => { this.handlePlaybackEnd(); resolve(); }); speaker.on('error', (err) => { console.error('Speaker error:', err); this.isPlayingAudio = false; reject(err); }); // Convert ArrayBuffer to Buffer, skip WAV header (44 bytes typically) const wavHeaderSize = this.getWavHeaderSize(audioBuffer); const audioData = audioBuffer.slice(wavHeaderSize); const buffer = Buffer.from(audioData); speaker.write(buffer); speaker.end(); } catch (error) { console.error('playWithSpeaker error:', error); this.isPlayingAudio = false; reject(error); } }); } async playWithPlaySound(audioBuffer, player) { return new Promise((resolve, reject) => { try { const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); // Create temporary file const tempFile = path.join(os.tmpdir(), `aituber-audio-${Date.now()}.wav`); fs.writeFileSync(tempFile, Buffer.from(audioBuffer)); const playerInstance = player(); this.currentProcess = playerInstance.play(tempFile, (err) => { // Clean up temp file try { fs.unlinkSync(tempFile); } catch (e) { // Ignore cleanup errors } if (err) { this.isPlayingAudio = false; reject(err); } else { this.handlePlaybackEnd(); resolve(); } }); } catch (error) { this.isPlayingAudio = false; reject(error); } }); } stop() { if (this.currentProcess && typeof this.currentProcess.kill === 'function') { this.currentProcess.kill(); } this.isPlayingAudio = false; } isPlaying() { return this.isPlayingAudio; } setOnComplete(callback) { this.onCompleteCallback = callback; } dispose() { this.stop(); } handlePlaybackEnd() { this.isPlayingAudio = false; this.currentProcess = null; if (this.onCompleteCallback) { this.onCompleteCallback(); } } async tryRequire(moduleName) { try { return require(moduleName); } catch (e) { return null; } } /** * Calculate WAV header size to skip when sending raw audio data to speaker */ getWavHeaderSize(buffer) { // Check for minimum buffer size if (buffer.byteLength < 12) { console.warn('Buffer too small for WAV header, using default header size: 44'); return 44; // Standard WAV header size } const view = new DataView(buffer); try { // Check for RIFF header const riff = String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3)); if (riff !== 'RIFF') { return 44; // Not a WAV file, use standard header size } // Find data chunk let offset = 12; while (offset < buffer.byteLength - 8) { const chunkId = String.fromCharCode(view.getUint8(offset), view.getUint8(offset + 1), view.getUint8(offset + 2), view.getUint8(offset + 3)); const chunkSize = view.getUint32(offset + 4, true); if (chunkId === 'data') { return offset + 8; // Header ends here, data starts } offset += 8 + chunkSize; } return 44; // Standard WAV header size fallback } catch (error) { console.warn('Error parsing WAV header, using default header size:', error); return 44; // Standard WAV header size } } } exports.NodeAudioPlayer = NodeAudioPlayer;