From 83b8f1acace8c23d03fdf5dab5fb63b5b7f56c44 Mon Sep 17 00:00:00 2001 From: Bot Date: Sun, 1 Mar 2026 21:24:47 +0100 Subject: [PATCH] Fix: Loudnorm-Cache korrekt als Stream lesen + Tee-Caching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- server/src/index.ts | 50 ++++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index ee7138f..35e701e 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -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 }); }