Simplify Windows TTS playback path
This commit is contained in:
@@ -511,6 +511,11 @@ export class LocalVoiceSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async playToWindowsDefaultSink(playback: PreparedSpeechAudio, signal: AbortSignal): Promise<void> {
|
private async playToWindowsDefaultSink(playback: PreparedSpeechAudio, signal: AbortSignal): Promise<void> {
|
||||||
|
if (playback.sourceFilePath) {
|
||||||
|
await this.playWindowsWaveFile(playback.sourceFilePath, signal);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const chunks: Buffer[] = [];
|
const chunks: Buffer[] = [];
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
@@ -584,6 +589,47 @@ export class LocalVoiceSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async playWindowsWaveFile(filePath: string, signal: AbortSignal): Promise<void> {
|
||||||
|
const psScript = [
|
||||||
|
"Add-Type -AssemblyName System;",
|
||||||
|
`$player = New-Object System.Media.SoundPlayer('${filePath.replace(/'/g, "''")}');`,
|
||||||
|
"$player.PlaySync();",
|
||||||
|
].join(" ");
|
||||||
|
|
||||||
|
const player = spawn("powershell", ["-NoProfile", "-Command", psScript], {
|
||||||
|
stdio: ["ignore", "ignore", "pipe"],
|
||||||
|
});
|
||||||
|
this.currentPlayer = player;
|
||||||
|
|
||||||
|
player.stderr.on("data", (chunk: Buffer) => {
|
||||||
|
const text = chunk.toString().trim();
|
||||||
|
if (text.length > 0) {
|
||||||
|
this.options.logger.debug("[powershell-player]", text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
signal.addEventListener(
|
||||||
|
"abort",
|
||||||
|
() => {
|
||||||
|
if (!player.killed) {
|
||||||
|
player.kill("SIGKILL");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const [code, playSignal] = (await once(player, "exit")) as [number | null, NodeJS.Signals | null];
|
||||||
|
this.currentPlayer = null;
|
||||||
|
|
||||||
|
if (signal.aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code !== 0) {
|
||||||
|
throw new Error(`powershell playback exited with code=${code ?? "null"} signal=${playSignal ?? "null"}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private getFfmpegPath(): string {
|
private getFfmpegPath(): string {
|
||||||
return requireFfmpegPath();
|
return requireFfmpegPath();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Readable } from "node:stream";
|
|||||||
|
|
||||||
export interface PreparedSpeechAudio {
|
export interface PreparedSpeechAudio {
|
||||||
stream: Readable;
|
stream: Readable;
|
||||||
|
sourceFilePath?: string;
|
||||||
dispose: () => void;
|
dispose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { unlink } from "node:fs/promises";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import prism from "prism-media";
|
|
||||||
|
|
||||||
import { resolveFfmpegPath } from "../audio/ffmpeg-path.js";
|
import { resolveFfmpegPath } from "../audio/ffmpeg-path.js";
|
||||||
import type { PreparedSpeechAudio, TtsService } from "./tts.js";
|
import type { PreparedSpeechAudio, TtsService } from "./tts.js";
|
||||||
|
|
||||||
@@ -90,45 +88,19 @@ export class WindowsSystemTtsService implements TtsService {
|
|||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
|
|
||||||
const input = createReadStream(tempPath);
|
|
||||||
const ffmpeg = new prism.FFmpeg({
|
|
||||||
args: [
|
|
||||||
"-analyzeduration",
|
|
||||||
"0",
|
|
||||||
"-loglevel",
|
|
||||||
"0",
|
|
||||||
"-i",
|
|
||||||
tempPath,
|
|
||||||
"-f",
|
|
||||||
"s16le",
|
|
||||||
"-ar",
|
|
||||||
"48000",
|
|
||||||
"-ac",
|
|
||||||
"2",
|
|
||||||
"pipe:1",
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
signal?.addEventListener(
|
|
||||||
"abort",
|
|
||||||
() => {
|
|
||||||
input.destroy();
|
|
||||||
ffmpeg.destroy();
|
|
||||||
void unlink(tempPath).catch(() => null);
|
|
||||||
},
|
|
||||||
{ once: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stream: ffmpeg,
|
stream: createReadStream(tempPath),
|
||||||
|
sourceFilePath: tempPath,
|
||||||
dispose: () => {
|
dispose: () => {
|
||||||
input.destroy();
|
this.cleanupTempWave(tempPath);
|
||||||
ffmpeg.destroy();
|
|
||||||
void unlink(tempPath).catch(() => null);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private cleanupTempWave(filePath: string): void {
|
||||||
|
void unlink(filePath).catch(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
async destroy(): Promise<void> {
|
async destroy(): Promise<void> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user