UNPKG

@inworld/web-core

Version:
213 lines (212 loc) 9.06 kB
import { decode } from 'base64-arraybuffer'; import { DEFAULT_PLAYBACK_SAMPLE_RATE } from '../../common/constants.js'; import { interpolate } from '../../common/helpers.js'; export class GrpcAudioPlayback { constructor(props) { this.audioQueue = []; this.audioPlaybackConfig = { stop: { duration: 500, // 0.5 second, ticks: 25, }, }; this.isPlaying = false; this.isStopping = false; this.muted = false; this.playQueue = async () => { var _a, _b, _c, _d, _e; if (this.isStopping) return; if (!this.audioQueue.length) { this.getSourceNode().buffer = null; this.isPlaying = false; (_a = this.onStopPlaying) === null || _a === void 0 ? void 0 : _a.call(this); return; } this.currentItem = this.audioQueue.shift(); this.isPlaying = true; let audioBuffer; const { packet } = this.currentItem; if ((packet === null || packet === void 0 ? void 0 : packet.isSilence()) && packet.silence.durationMs) { const durationSec = packet.silence.durationMs / 1000; audioBuffer = this.playbackAudioContext.createBuffer(2, this.playbackAudioContext.sampleRate * durationSec, this.playbackAudioContext.sampleRate); } else { audioBuffer = await this.playbackAudioContext.decodeAudioData( // convertion due to grpc-gateway ws specific. decode(packet.audio.chunk)); } this.clearSourceNode(); if (this.currentItem) { this.getSourceNode().buffer = audioBuffer; if (packet.isAudio()) { if (packet.audio.additionalPhonemeInfo) { (_b = this.onPhoneme) === null || _b === void 0 ? void 0 : _b.call(this, packet.audio.additionalPhonemeInfo); } this.currentItem.packet.audio.durationMs = audioBuffer.duration * 1000; } this.getSourceNode().start(); (_c = this.onBeforePlaying) === null || _c === void 0 ? void 0 : _c.call(this, this.currentItem.packet); (_e = (_d = this.currentItem).onBeforePlaying) === null || _e === void 0 ? void 0 : _e.call(_d, this.currentItem.packet); } }; this.audioPlaybackConfig = Object.assign(Object.assign({ sampleRate: DEFAULT_PLAYBACK_SAMPLE_RATE }, this.audioPlaybackConfig), props === null || props === void 0 ? void 0 : props.audioPlaybackConfig); this.playbackAudioContext = new AudioContext({ sampleRate: this.audioPlaybackConfig.sampleRate, }); this.destinationNode = this.playbackAudioContext.createMediaStreamDestination(); this.gainNode = this.playbackAudioContext.createGain(); this.onAfterPlaying = props === null || props === void 0 ? void 0 : props.onAfterPlaying; this.onBeforePlaying = props === null || props === void 0 ? void 0 : props.onBeforePlaying; this.onStopPlaying = props === null || props === void 0 ? void 0 : props.onStopPlaying; this.onPhoneme = props === null || props === void 0 ? void 0 : props.onPhoneme; this.gainNode.connect(this.playbackAudioContext.destination); } getIsActive() { return this.isPlaying; } addToQueue(item) { this.audioQueue.push(item); if (!this.getIsActive()) { this.playQueue(); } } clearQueue() { this.isPlaying = false; this.audioQueue = []; } excludeCurrentInteractionPackets(exceptInteractionId) { const toExlcude = this.audioQueue.filter((item) => { var _a, _b; return item.packet.packetId.interactionId !== exceptInteractionId && item.packet.packetId.interactionId === ((_b = (_a = this.currentItem) === null || _a === void 0 ? void 0 : _a.packet) === null || _b === void 0 ? void 0 : _b.packetId.interactionId); }); if (toExlcude.length) { this.audioQueue = this.audioQueue.filter((item) => { var _a, _b; return item.packet.packetId.interactionId !== ((_b = (_a = this.currentItem) === null || _a === void 0 ? void 0 : _a.packet) === null || _b === void 0 ? void 0 : _b.packetId.interactionId); }); } const result = [...toExlcude]; if (this.currentItem && this.currentItem.packet.packetId.interactionId !== exceptInteractionId) result.unshift(this.currentItem); return result.map((item) => item.packet); } hasPacketInQueue(props) { const { interactionId, utteranceId } = props; return !!this.audioQueue.find((item) => { let result = true; const packetId = item.packet.packetId; if (interactionId && packetId.interactionId !== interactionId) { result = false; } if (utteranceId && packetId.utteranceId !== utteranceId) { result = false; } return result; }); } isCurrentPacket(props) { var _a; const { interactionId, utteranceId } = props; const { packetId } = ((_a = this.currentItem) === null || _a === void 0 ? void 0 : _a.packet) || {}; let result = true; if (interactionId && (packetId === null || packetId === void 0 ? void 0 : packetId.interactionId) !== interactionId) { result = false; } if (utteranceId && (packetId === null || packetId === void 0 ? void 0 : packetId.utteranceId) !== utteranceId) { result = false; } return result; } getCurrentPacket() { var _a; return (_a = this.currentItem) === null || _a === void 0 ? void 0 : _a.packet; } getPlaybackStream() { return this.destinationNode.stream; } getMute() { return this.muted; } async mute(mute) { this.muted = mute; return this.adjustVolume(mute ? 0 : 1); } getVolume() { return this.gainNode.gain.value; } async stop() { if (this.audioBufferSourceNode) { this.audioBufferSourceNode.onended = null; } this.currentItem = undefined; this.isStopping = true; await this.adjustVolume(0); this.isStopping = false; this.clearSourceNode(); } async stopForInteraction(interactionId) { const packets = this.excludeCurrentInteractionPackets(interactionId); if (this.currentItem && !this.isCurrentPacket({ interactionId })) { await this.stop(); this.playQueue(); } return packets; } async init() { // Way past safari autoplay. Should be called from inside direct user interaction. await this.playbackAudioContext.resume().catch(console.error); } clearSourceNode() { if (this.audioBufferSourceNode) { this.audioBufferSourceNode.disconnect(); } if (!this.getMute()) { this.gainNode.gain.value = 1; } delete this.audioBufferSourceNode; } getSourceNode() { if (!this.audioBufferSourceNode) { this.audioBufferSourceNode = new AudioBufferSourceNode(this.playbackAudioContext); this.audioBufferSourceNode.connect(this.gainNode); this.audioBufferSourceNode.onended = () => { var _a, _b, _c; const { packet } = this.currentItem || {}; if (packet === null || packet === void 0 ? void 0 : packet.isAudio()) { (_a = this.onAfterPlaying) === null || _a === void 0 ? void 0 : _a.call(this, packet); (_c = (_b = this.currentItem).onAfterPlaying) === null || _c === void 0 ? void 0 : _c.call(_b, packet); } this.currentItem = undefined; this.playQueue(); }; } return this.audioBufferSourceNode; } adjustVolume(newVolume) { var _a; const originalVolume = this.gainNode.gain.value; const delta = newVolume - originalVolume; if (!delta) { return Promise.resolve(); } const { duration, ticks } = (_a = this.audioPlaybackConfig) === null || _a === void 0 ? void 0 : _a.stop; let tick = 1; const interval = Math.floor(duration / ticks); return new Promise((resolve) => { const timer = setInterval(() => { this.gainNode.gain.value = originalVolume + interpolate(tick / ticks) * delta; if (++tick === ticks + 1) { clearInterval(timer); resolve(); } }, interval); }); } }