perf: in-memory PCM cache + skip InlineVolume at vol 1.0

- PCM-Dateien werden beim ersten Abspielen in den RAM geladen (Map<string, Buffer>)
- Nachfolgende Plays lesen aus RAM statt Disk -> eliminiert I/O-Latenz
- InlineVolume Transform wird bei Volume 1.0 uebersprungen (unnoetige Sample-Verarbeitung)
- Fallback createReadStream mit 256KB highWaterMark fuer schnelleres Buffering
- Memory-Cache-Limit konfigurierbar via PCM_CACHE_MAX_MB env (default 512MB)
- Cache-Invalidierung bei Quelldatei-Aenderungen
This commit is contained in:
Claude Code 2026-03-05 15:10:18 +01:00
parent cde29698ca
commit 901f0bf1dd

View file

@ -24,7 +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';
import { PassThrough, Readable } from 'node:stream';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -158,6 +158,35 @@ const NORMALIZE_TP = String(process.env.NORMALIZE_TP ?? '-1.5');
const NORM_CACHE_DIR = path.join(SOUNDS_DIR, '.norm-cache');
fs.mkdirSync(NORM_CACHE_DIR, { recursive: true });
// In-Memory PCM Cache: gecachte PCM-Dateien werden beim ersten Abspielen in den RAM geladen.
// Danach kein Disk-I/O mehr bei Cache-Hits -> nahezu instant playback.
const pcmMemoryCache = new Map<string, Buffer>();
const PCM_MEMORY_CACHE_MAX_MB = Number(process.env.PCM_CACHE_MAX_MB ?? '512');
let pcmMemoryCacheBytes = 0;
function getPcmFromMemory(cachedPath: string): Buffer | null {
const buf = pcmMemoryCache.get(cachedPath);
if (buf) return buf;
// Erste Anfrage: von Disk in RAM laden
try {
const data = fs.readFileSync(cachedPath);
const newTotal = pcmMemoryCacheBytes + data.byteLength;
if (newTotal <= PCM_MEMORY_CACHE_MAX_MB * 1024 * 1024) {
pcmMemoryCache.set(cachedPath, data);
pcmMemoryCacheBytes = newTotal;
}
return data;
} catch { return null; }
}
function invalidatePcmMemory(cachedPath: string): void {
const buf = pcmMemoryCache.get(cachedPath);
if (buf) {
pcmMemoryCacheBytes -= buf.byteLength;
pcmMemoryCache.delete(cachedPath);
}
}
/** Erzeugt einen Cache-Key aus dem relativen Pfad (Slash-normalisiert, Sonderzeichen escaped) */
function normCacheKey(filePath: string): string {
const rel = path.relative(SOUNDS_DIR, filePath).replace(/\\/g, '/');
@ -172,7 +201,7 @@ function getNormCachePath(filePath: string): string | null {
try {
const srcMtime = fs.statSync(filePath).mtimeMs;
const cacheMtime = fs.statSync(cacheFile).mtimeMs;
if (srcMtime > cacheMtime) { try { fs.unlinkSync(cacheFile); } catch {} return null; }
if (srcMtime > cacheMtime) { try { fs.unlinkSync(cacheFile); } catch {} invalidatePcmMemory(cacheFile); return null; }
} catch { return null; }
return cacheFile;
}
@ -381,9 +410,19 @@ async function playFilePath(guildId: string, channelId: string, filePath: string
if (NORMALIZE_ENABLE) {
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 });
// Cache-Hit: PCM aus RAM lesen (kein Disk-I/O, instant)
const pcmBuf = getPcmFromMemory(cachedPath);
if (pcmBuf) {
const useInline = useVolume !== 1;
resource = createAudioResource(Readable.from(pcmBuf), {
inlineVolume: useInline,
inputType: StreamType.Raw
});
} else {
// Fallback: Stream von Disk
const pcmStream = fs.createReadStream(cachedPath, { highWaterMark: 256 * 1024 });
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));
@ -402,6 +441,14 @@ async function playFilePath(guildId: string, channelId: string, filePath: string
playerStream.end();
cacheWrite.end();
console.log(`${new Date().toISOString()} | Loudnorm cached: ${path.basename(filePath)}`);
// In Memory-Cache laden fuer naechsten Aufruf
try {
const buf = fs.readFileSync(cacheFile);
if (pcmMemoryCacheBytes + buf.byteLength <= PCM_MEMORY_CACHE_MAX_MB * 1024 * 1024) {
pcmMemoryCache.set(cacheFile, buf);
pcmMemoryCacheBytes += buf.byteLength;
}
} catch {}
});
ff.on('error', () => { try { fs.unlinkSync(cacheFile); } catch {} });
ff.on('close', (code) => {