/ proof-of-concept / cymbalsim
Sign in
proof-of-concept / cymbalsim
Keep the loudness warning through the live updates of a full run
The periodic refresh of the peak badge during a run no longer clears the warning or the acceptance; only a new recording, another level mode or a change of the gain does. A browser test presses Play during a run and checks that the warning outlives several live updates and that Play anyway then plays; the earlier test drives the level select and the gain slider as the user would.
Jeremy Magland <jmagland@flatironinstitute.org> committed commit 30fcba1a8303 parent 00e1895 Browse files
2 changed files+52−8
src/main.tsmodified+9−4View file
@@ -353,11 +353,11 @@ class App {
353353 this.timeLabel.textContent = "0.000 s";
354354 const levelSel = document.createElement("select");
355355 levelSel.innerHTML = `<option value="absolute">Absolute level</option><option value="loudness">Normalize loudness</option><option value="peak">Normalize peak</option>`;
356- levelSel.addEventListener("change", () => { this.levelMode = levelSel.value as LevelMode; this.updatePeak(); });
356+ levelSel.addEventListener("change", () => { this.levelMode = levelSel.value as LevelMode; this.resetLoudness(); this.updatePeak(); });
357357 const gain = document.createElement("input");
358358 gain.type = "range"; gain.min = "-60"; gain.max = "40"; gain.step = "1"; gain.value = String(this.gainDb);
359359 const gainLabel = document.createElement("span"); gainLabel.textContent = `${this.gainDb} dB`;
360- gain.addEventListener("input", () => { this.gainDb = Number(gain.value); gainLabel.textContent = `${this.gainDb} dB`; this.updatePeak(); });
360+ gain.addEventListener("input", () => { this.gainDb = Number(gain.value); gainLabel.textContent = `${this.gainDb} dB`; this.resetLoudness(); this.updatePeak(); });
361361 this.peakLabel = document.createElement("span");
362362 this.downloadBtn = this.button("Download .wav", () => this.download());
363363 const lab = (text: string, ...els: HTMLElement[]) => { const l = document.createElement("label"); l.append(text, ...els); return l; };
@@ -704,6 +704,7 @@ class App {
704704 const loss: FullLoss = fullLoss(fit, standRates(pl), decayRate(pl.loss, 5000));
705705 this.sim = new Simulation(this.device, pl, p, loss);
706706 this.rec = { audio: new Float32Array(0), samples: 0, fs: p.fs, frames: [new Float32Array(pl.grid.n)], fps: p.fps, source: "full" };
707+ this.resetLoudness();
707708 this.spec.reset();
708709 this.spec.setOptions({ fs: p.fs });
709710 this.spec.setDuration(p.duration);
@@ -819,6 +820,7 @@ class App {
819820
820821 private installRecording(audio: Float32Array, frames: Float32Array[], p: SimParams, source: RunMode): void {
821822 this.rec = { audio, samples: audio.length, fs: p.fs, frames, fps: p.fps, source };
823+ this.resetLoudness();
822824 this.spec.setOptions({ fs: p.fs });
823825 this.spec.setDuration(p.duration);
824826 this.spec.setSignal(audio);
@@ -856,10 +858,13 @@ class App {
856858 return applyLevel(sig, levelGain(sig, rec.fs, this.levelMode, this.gainDb));
857859 }
858860
859- private updatePeak(): void {
860- // Anything that changes the signal to be played (a new recording, the level mode, the gain) comes through here.
861+ /** Forgets the user's acceptance of the loudness warning: called when the signal to be played changes (a new recording, the level mode, the gain), not when a live recording merely grows. */
862+ private resetLoudness(): void {
861863 this.hideLoudWarning();
862864 this.loudAccepted = false;
865+ }
866+
867+ private updatePeak(): void {
863868 const s = this.playable();
864869 if (!s) { this.peakLabel.textContent = ""; this.downloadBtn.disabled = true; this.playBtn.disabled = true; return; }
865870 const db = peakDbfs(s);
tests/gpu.test.tsmodified+43−4View file
@@ -143,8 +143,12 @@ test("a loud recording warns before playing and plays once accepted", async () =
143143 }, script);
144144 let r = await run("");
145145 assert.equal(r.warn, false, "no warning before Play");
146+ // The level select and the gain slider, driven as the user would.
147+ const level = (mode: string) => page.selectOption("#movieToolbar select", mode);
148+ const gain = (db: number) => page.evaluate((v: number) => { const g = document.querySelector("#movieToolbar input[type=range]:not(.scrub)") as HTMLInputElement; g.value = String(v); g.dispatchEvent(new Event("input")); }, db);
146149 // Normalizing the peak puts it at -1 dBFS: Play warns instead of playing.
147- r = await run(`a.levelMode = "peak"; a.updatePeak(); a.togglePlay();`);
150+ await level("peak");
151+ r = await run("a.togglePlay();");
148152 assert.equal(r.playing, false);
149153 assert.equal(r.warn, true);
150154 assert.match(r.text, /peak -1\.0 dBFS/);
@@ -163,21 +167,56 @@ test("a loud recording warns before playing and plays once accepted", async () =
163167 assert.equal(r.playing, true, "the accepted signal replays without a prompt");
164168 assert.equal(r.warn, false);
165169 // Any change brings the warning back: the level mode (loudness normalization is loud too), then the gain.
166- r = await run(`a.togglePlay(); a.levelMode = "loudness"; a.updatePeak(); a.togglePlay();`);
170+ await run("a.togglePlay();");
171+ await level("loudness");
172+ r = await run("a.togglePlay();");
167173 assert.equal(r.playing, false);
168174 assert.equal(r.warn, true, "loudness normalization warns");
169175 await page.click("#movieToolbar .loud-warn button:has-text('Play anyway')");
170- r = await run(`a.togglePlay(); a.levelMode = "absolute"; a.gainDb = 0; a.updatePeak(); a.togglePlay();`);
176+ await run("a.togglePlay();");
177+ await level("absolute");
178+ await gain(0);
179+ r = await run("a.togglePlay();");
171180 assert.equal(r.playing, false);
172181 assert.equal(r.warn, true, "a gain change warns again");
173182 // A quiet signal never prompts.
174- r = await run(`a.gainDb = -60; a.updatePeak(); a.togglePlay();`);
183+ await gain(-60);
184+ r = await run("a.togglePlay();");
175185 assert.equal(r.playing, true);
176186 assert.equal(r.warn, false);
177187 assert.deepEqual(errors, []);
178188 });
179189 });
180190
191+test("the loudness warning survives the live updates of a full run", async () => {
192+ await withPage("preset=flat-rectangle&mode=full&duration=1.5&spacing=0.012&fps=60", async (page, errors) => {
193+ if (!(await hasGpu(page))) { console.log("no WebGPU in this Chromium; skipping"); return; }
194+ type A = { player: { playing: boolean }; levelMode: string; updatePeak: () => void; togglePlay: () => void; loudWarn: HTMLElement; running: boolean };
195+ const state = () => page.evaluate(() => {
196+ const a = (window as unknown as { cymbalsim: { app: A } }).cymbalsim.app;
197+ return { playing: a.player.playing, warn: getComputedStyle(a.loudWarn).display !== "none", running: a.running };
198+ });
199+ // Start the run without awaiting it, wait for some audio, then press Play with peak normalization.
200+ await page.evaluate(() => { void (window as unknown as { cymbalsim: { runFull: () => Promise<void> } }).cymbalsim.runFull(); });
201+ await page.waitForFunction(() => (window as unknown as { cymbalsim: { status: () => Status } }).cymbalsim.status().samples > 4800, null, { timeout: 60000 });
202+ await page.evaluate(() => { const a = (window as unknown as { cymbalsim: { app: A } }).cymbalsim.app; a.levelMode = "peak"; a.updatePeak(); a.togglePlay(); });
203+ let s = await state();
204+ assert.equal(s.running, true, "still running");
205+ assert.equal(s.warn, true);
206+ assert.equal(s.playing, false);
207+ // Several live updates later the warning is still there.
208+ await page.waitForTimeout(500);
209+ s = await state();
210+ assert.equal(s.warn, true, "the warning outlives the live updates");
211+ await page.click("#movieToolbar .loud-warn button:has-text('Play anyway')");
212+ s = await state();
213+ assert.equal(s.playing, true);
214+ assert.equal(s.warn, false);
215+ await page.waitForFunction(() => !(window as unknown as { cymbalsim: { status: () => Status } }).cymbalsim.status().running, null, { timeout: 120000 });
216+ assert.deepEqual(errors, []);
217+ });
218+});
219+
181220 test("a script with an error is reported", async () => {
182221 await withPage("preset=ellipse", async (page) => {
183222 await page.evaluate(() => {
↑↓ move↵openescclose