Fix: Loudnorm-Cache korrekt als Stream lesen + Tee-Caching

Zwei Fehler behoben:
1. Gecachte PCM-Datei wurde als Pfad-String an createAudioResource übergeben
   → discord.js versuchte die headerlose PCM als Container zu proben → Rauschen/Stille
   Fix: fs.createReadStream() + StreamType.Raw

2. Erster Play wartete auf komplette ffmpeg-Verarbeitung bevor Wiedergabe startete
   Fix: Tee-Stream — ffmpeg-Output wird gleichzeitig an Player UND Cache-Datei geschrieben
   → Sofortige Wiedergabe auch beim ersten Mal, Cache wird nebenbei gefüllt

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bot 2026-03-01 21:24:47 +01:00
parent 4b4a61b2bd
commit 83b8f1acac

View file

@ -24,6 +24,7 @@ import sodium from 'libsodium-wrappers';
import nacl from 'tweetnacl';
// Streaming externer Plattformen entfernt nur MP3-URLs werden noch unterstützt
import child_process from 'node:child_process';
import { PassThrough } from 'node:stream';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -311,29 +312,36 @@ async function playFilePath(guildId: string, channelId: string, filePath: string
: (state.currentVolume ?? 1);
let resource: AudioResource;
if (NORMALIZE_ENABLE) {
// Cache-basierte Normalisierung: ffmpeg läuft nur beim 1. Mal
let cachedPath = getNormCachePath(filePath);
if (!cachedPath) {
try {
cachedPath = await normalizeToCache(filePath);
const cachedPath = getNormCachePath(filePath);
if (cachedPath) {
// Cache-Hit: gecachte PCM-Datei als Stream lesen (kein ffmpeg, instant)
const pcmStream = fs.createReadStream(cachedPath);
resource = createAudioResource(pcmStream, { inlineVolume: true, inputType: StreamType.Raw });
} else {
// Cache-Miss: ffmpeg streamen (sofortige Wiedergabe) UND gleichzeitig in Cache schreiben
const cacheFile = path.join(NORM_CACHE_DIR, normCacheKey(filePath));
const ffArgs = ['-hide_banner', '-loglevel', 'error', '-i', filePath,
'-af', `loudnorm=I=${NORMALIZE_I}:LRA=${NORMALIZE_LRA}:TP=${NORMALIZE_TP}`,
'-f', 's16le', '-ar', '48000', '-ac', '2', 'pipe:1'];
const ff = child_process.spawn('ffmpeg', ffArgs);
// Tee: Daten gleichzeitig an Player und Cache-Datei
const playerStream = new PassThrough();
const cacheWrite = fs.createWriteStream(cacheFile);
ff.stdout.on('data', (chunk: Buffer) => {
playerStream.write(chunk);
cacheWrite.write(chunk);
});
ff.stdout.on('end', () => {
playerStream.end();
cacheWrite.end();
console.log(`${new Date().toISOString()} | Loudnorm cached: ${path.basename(filePath)}`);
} catch (e) {
console.warn(`${new Date().toISOString()} | Loudnorm cache failed, fallback direct:`, e);
// Fallback: direkt ohne Normalisierung abspielen
resource = createAudioResource(filePath, { inlineVolume: true });
if (resource.volume) resource.volume.setVolume(useVolume);
state.player.stop();
state.player.play(resource);
state.currentResource = resource;
state.currentVolume = useVolume;
const soundLabel = relativeKey ? path.parse(relativeKey).name : path.parse(filePath).name;
nowPlaying.set(guildId, soundLabel);
sseBroadcast({ type: 'nowplaying', guildId, name: soundLabel });
if (relativeKey) incrementPlaysFor(relativeKey);
return;
}
});
ff.on('error', () => { try { fs.unlinkSync(cacheFile); } catch {} });
ff.on('close', (code) => {
if (code !== 0) { try { fs.unlinkSync(cacheFile); } catch {} }
});
resource = createAudioResource(playerStream, { inlineVolume: true, inputType: StreamType.Raw });
}
resource = createAudioResource(cachedPath, { inlineVolume: true, inputType: StreamType.Raw });
} else {
resource = createAudioResource(filePath, { inlineVolume: true });
}