@inworld/web-core
Version:
213 lines (212 loc) • 9.06 kB
JavaScript
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);
});
}
}