fix(tts): reuse audio object in the same paragraph for better performance, closes #1777 (#1853)

* Remove stopInternal which causes audio delaying

* Change audio obj to not be recreated every sentence

* refactor abort signal handler

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
This commit is contained in:
shaqtiktok
2025-08-22 16:22:34 +01:00
committed by GitHub
parent fc4c033cf3
commit 75dc04d598
@@ -97,18 +97,17 @@ export class EdgeTTSClient implements TTSClient {
} as TTSMessageEvent;
return;
} else {
await this.stopInternal();
}
await this.stopInternal();
// Reuse the same Audio element inside the ssml session
this.#audioElement = new Audio();
const audio = this.#audioElement;
audio.setAttribute('x-webkit-airplay', 'deny');
audio.preload = 'auto';
for (const mark of marks) {
if (signal.aborted) {
yield {
code: 'error',
message: 'Aborted',
} as TTSMessageEvent;
break;
}
let abortHandler: null | (() => void) = null;
try {
const { language: voiceLang } = mark;
const voiceId = await this.getVoiceIdFromLang(voiceLang);
@@ -116,11 +115,7 @@ export class EdgeTTSClient implements TTSClient {
const blob = await this.#edgeTTS.createAudio(
this.getPayload(voiceLang, mark.text, voiceId),
);
const url = URL.createObjectURL(blob);
this.#audioElement = new Audio(url);
const audio = this.#audioElement;
audio.setAttribute('x-webkit-airplay', 'deny');
audio.preload = 'auto';
audio.src = URL.createObjectURL(blob);
this.controller?.dispatchSpeakMark(mark);
@@ -135,27 +130,30 @@ export class EdgeTTSClient implements TTSClient {
audio.onended = null;
audio.onerror = null;
audio.pause();
audio.src = '';
URL.revokeObjectURL(url);
if (audio.src) {
URL.revokeObjectURL(audio.src);
audio.removeAttribute('src');
}
};
abortHandler = () => {
cleanUp();
resolve({ code: 'error', message: 'Aborted' });
};
if (signal.aborted) {
abortHandler();
return;
} else {
signal.addEventListener('abort', abortHandler);
}
audio.onended = () => {
cleanUp();
if (signal.aborted) {
resolve({ code: 'error', message: 'Aborted' });
} else {
resolve({ code: 'end', message: `Chunk finished: ${mark.name}` });
}
resolve({ code: 'end', message: `Chunk finished: ${mark.name}` });
};
audio.onerror = (e) => {
cleanUp();
console.warn('Audio playback error:', e);
resolve({ code: 'error', message: 'Audio playback error' });
};
if (signal.aborted) {
cleanUp();
resolve({ code: 'error', message: 'Aborted' });
return;
}
this.#isPlaying = true;
audio.play().catch((err) => {
cleanUp();
@@ -167,22 +165,20 @@ export class EdgeTTSClient implements TTSClient {
} catch (error) {
if (error instanceof Error && error.message === 'No audio data received.') {
console.warn('No audio data received for:', mark.text);
yield {
code: 'end',
message: `Chunk finished: ${mark.name}`,
} as TTSMessageEvent;
yield { code: 'end', message: `Chunk finished: ${mark.name}` } as TTSMessageEvent;
continue;
}
console.log('Error:', error);
yield {
code: 'error',
message: error instanceof Error ? error.message : String(error),
} as TTSMessageEvent;
const message = error instanceof Error ? error.message : String(error);
console.warn('TTS error for mark:', mark.text, message);
yield { code: 'error', message } as TTSMessageEvent;
break;
} finally {
if (abortHandler) {
signal.removeEventListener('abort', abortHandler);
}
}
await this.stopInternal();
}
await this.stopInternal();
}
async pause() {