Speakeren går klart igennem: normaliseret lyd og ny mixer
CI / test (push) Failing after 31s

Lydfilerne var op til 17 dB højere end speakeren (kategori-lyden -4 LUFS
mod speakerens -21). Alle effekter er nu normaliseret til -20 LUFS og
musikken til -23 LUFS. Speakeren går gennem sin egen kompressor med et
løft, og mens den taler, dykker effekterne og musikken, og musikken får
skåret taleområdet ned. En limiter til sidst forhindrer klipning.

Claude-Session: https://claude.ai/code/session_01JmhJG6JnhXfzNiKv8f2vHu
This commit is contained in:
agw
2026-10-03 17:25:59 +02:00
parent d149aa323e
commit 305a59640e
35 changed files with 50 additions and 8 deletions
+50 -8
View File
@@ -382,14 +382,52 @@
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(vol || 0.18, t + 0.015);
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
o.connect(g); g.connect(actx.destination);
o.connect(g); g.connect(mix.build() ? mix.graph.sfxBus : actx.destination);
o.start(t); o.stop(t + dur + 0.05);
},
play: function (kind) { WPP.sfx(kind); } /* gammelt navn */
});
/* ---------- mixeren ----------
Alt går gennem en master-limiter. Speakeren har sin egen kæde (kompressor + løft), så den står klart;
effekter og musik har hver sin bus, som dukker sig, mens speakeren taler (se mix.duck). */
var mix = (WPP.mix = {
graph: null,
VOICE_GAIN: 1.6, /* ≈ +4 dB: speakerens replikker (ca. −21 LUFS) løftes over resten */
SFX_DUCK: 0.35, /* effekter under tale: ≈ −9 dB */
EQ_DUCK_DB: -9, /* musikkens taleområde (2,6 kHz) skæres ned under tale */
build: function () {
if (mix.graph || !actx) return mix.graph;
var limiter = actx.createDynamicsCompressor();
limiter.threshold.value = -3; limiter.knee.value = 0; limiter.ratio.value = 20;
limiter.attack.value = 0.002; limiter.release.value = 0.2;
limiter.connect(actx.destination);
var sfxBus = actx.createGain(); sfxBus.connect(limiter);
var musicEq = actx.createBiquadFilter();
musicEq.type = 'peaking'; musicEq.frequency.value = 2600; musicEq.Q.value = 0.9; musicEq.gain.value = 0;
musicEq.connect(limiter);
var voiceComp = actx.createDynamicsCompressor();
voiceComp.threshold.value = -28; voiceComp.knee.value = 8; voiceComp.ratio.value = 4;
voiceComp.attack.value = 0.004; voiceComp.release.value = 0.2;
var voiceGain = actx.createGain(); voiceGain.gain.value = mix.VOICE_GAIN;
voiceComp.connect(voiceGain); voiceGain.connect(limiter);
mix.graph = { limiter: limiter, sfxBus: sfxBus, musicEq: musicEq, voiceIn: voiceComp };
return mix.graph;
},
/* speakeren taler: dyk effekter og musik hurtigt, slip langsomt */
duck: function (on) {
var g = mix.build();
if (!g) return;
var t = actx.currentTime, tc = on ? 0.05 : 0.35;
[[g.sfxBus.gain, on ? mix.SFX_DUCK : 1], [g.musicEq.gain, on ? mix.EQ_DUCK_DB : 0]].forEach(function (x) {
x[0].cancelScheduledValues(t); x[0].setValueAtTime(x[0].value, t); x[0].setTargetAtTime(x[1], t, tc);
});
}
});
var wasReady = false;
function onReady() {
if (!sound.ready()) return;
mix.build();
Object.keys(raw).forEach(decode);
music._kick();
if (!wasReady) { wasReady = true; if (sound.onchange) sound.onchange(); }
@@ -453,12 +491,11 @@
}
function playBuf(b, vol, opts) {
var delay = opts.at != null ? Math.max(0, opts.at - actx.currentTime) : (opts.delay || 0);
if (voice.active) vol *= 0.6; /* speakeren skal kunne høres */
var src = actx.createBufferSource(), g = actx.createGain();
src.buffer = b;
if (opts.rate) src.playbackRate.value = opts.rate;
g.gain.value = vol;
src.connect(g); g.connect(actx.destination);
src.connect(g); g.connect(opts.direct || !mix.build() ? actx.destination : mix.graph.sfxBus);
src.start(actx.currentTime + delay);
return delay + b.duration / (opts.rate || 1);
}
@@ -536,7 +573,8 @@
var FADE = 0.8;
var MUSIC_GAIN = { music_reveal: 0.45 }; /* afsløringen: effekterne er i fokus */
var music = (WPP.music = {
name: null, volume: 0.18, duckVolume: 0.05, ducked: false, cur: null, bus: null, _fade: FADE, _gain: 1,
/* filerne er normaliseret til −23 LUFS: 0.5 ≈ −29 LUFS, dukket 0.14 ≈ −40 LUFS (+ EQ-hak i taleområdet) */
name: null, volume: 0.5, duckVolume: 0.14, ducked: false, cur: null, bus: null, _fade: FADE, _gain: 1,
/* WPP.music.set('music_lobby') | WPP.music.set(null, {fade: 0.25}) – fade = udtoning af det gamle nummer (s) */
set: function (name, opts) {
opts = opts || {};
@@ -554,15 +592,15 @@
var g = music.bus.gain, t = actx.currentTime;
g.cancelScheduledValues(t);
g.setValueAtTime(g.value, t);
g.setTargetAtTime(music._target(), t, music.ducked ? 0.12 : 0.35);
g.setTargetAtTime(music._target(), t, music.ducked ? 0.05 : 0.4);
},
/* dukker sig straks, men slipper først efter en kort pause (ingen pumpen mellem to replikker) */
duck: function (on) {
on = !!on;
clearTimeout(music._duckT);
if (on) { if (!music.ducked) { music.ducked = true; music._apply(); } return; }
if (on) { if (!music.ducked) { music.ducked = true; music._apply(); mix.duck(true); } return; }
if (!music.ducked) return;
music._duckT = setTimeout(function () { music.ducked = false; music._apply(); }, 700);
music._duckT = setTimeout(function () { music.ducked = false; music._apply(); mix.duck(false); }, 500);
},
/* skift til music.name, hvis lyden er klar */
_kick: function () {
@@ -570,7 +608,7 @@
if (!music.bus) {
music.bus = actx.createGain();
music.bus.gain.value = music._target();
music.bus.connect(actx.destination);
music.bus.connect(mix.build() ? mix.graph.musicEq : actx.destination);
}
var cur = music.cur, name = music.name, t = actx.currentTime;
if (cur && cur.name === name) return;
@@ -639,6 +677,10 @@
var a = new Audio(x.url);
voice.current = a;
voice._active(true);
if (sound.ready() && mix.build()) {
/* gennem kompressor + løft; ellers (lyd ikke låst op endnu) spilles den direkte */
try { actx.createMediaElementSource(a).connect(mix.graph.voiceIn); } catch (e) { /* */ }
}
a.onended = a.onerror = function () { if (voice.current === a) voice._next(); };
var p = a.play();
if (p && p.catch) p.catch(function (e) {
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.