Clickable previews: exact app deep-links everywhere

Every rendered still links to the app frame that produced it (song + seed + frame + venue/viz pins + probe warm-up, new window), via shared src/ui/preview.js. Supporting work, each measured: app honors ?frame/?venueSel/?viz/?warm= with pre-build pins (Show.applyPins) and primes deep-link loads; shared pinViz/pinVenueShell paths for gallery, filmstrip and app; probes prime and carry their warm-up; bank probes analyse the bank PCM and file decode is fixed-rate with bit-exact WAV parsing so app and probes see identical samples; materials built mid-update compile synchronously (needsCompile). Round trip verified bit-identical gallery-versus-app.
This commit is contained in:
Dejvino
2026-09-20 18:42:53 +02:00
parent cc8888077f
commit 4d05250294
18 changed files with 377 additions and 96 deletions
+22 -9
View File
@@ -22,7 +22,7 @@ import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { synthesizeSong } from '../src/audio/synth.js';
import { FeatureTrack } from '../src/audio/FeatureTrack.js';
import { SONGS, AXES } from '../src/audio/songbank.js';
import { SONGS, AXES, bankPcm } from '../src/audio/songbank.js';
const here = dirname(fileURLToPath(import.meta.url));
@@ -41,7 +41,9 @@ const CHECK_ONLY = has('check');
* 16-bit PCM WAV, mono.
*
* Mono halves the size and loses nothing: the analyser sums to mono before it
* does anything, and the synth writes the same signal to both channels anyway.
* does anything, the synth writes the same signal to both channels anyway —
* and the probes analyse the mono bank PCM, so the files must be mono for
* app and probe features to match bit-exactly. See songbank.js bankPcm.
*/
function wav(samples, sampleRate) {
const bytes = samples.length * 2;
@@ -63,13 +65,12 @@ function wav(samples, sampleRate) {
// Peak-normalise to -1 dBFS. Without it the quiet ambient entries clip down
// into 16-bit noise and their measured flatness stops being what the spec
// asked for — the bank would then be testing the encoder, not the generator.
let peak = 0;
for (let i = 0; i < samples.length; i++) peak = Math.max(peak, Math.abs(samples[i]));
const gain = peak > 1e-9 ? 0.89 / peak : 1;
// Shared with songbank.js bankPcm: the files must hold exactly what the
// probes analyse, or app-vs-probe previews can never match.
const pcm = bankPcm(samples);
for (let i = 0; i < samples.length; i++) {
const v = Math.max(-1, Math.min(1, samples[i] * gain));
buffer.writeInt16LE(Math.round(v * 32767), 44 + i * 2);
for (let i = 0; i < pcm.length; i++) {
buffer.writeInt16LE(Math.round(pcm[i] * 32768), 44 + i * 2);
}
return buffer;
}
@@ -92,7 +93,19 @@ for (const spec of SONGS) {
transition: spec.transition ?? 0.5,
seed: 1 + SONGS.indexOf(spec),
});
const track = FeatureTrack.fromAudioBuffer(buffer, { fps: 60 });
// Measured from the bank PCM — the same samples the files hold and the
// probes analyse (see songbank.js bankPcm) — not the raw synth.
const pcm = bankPcm(buffer.getChannelData(0));
const track = FeatureTrack.fromAudioBuffer({
sampleRate: buffer.sampleRate,
length: pcm.length,
duration: buffer.duration,
numberOfChannels: 1,
getChannelData: (i) => {
if (i !== 0) throw new Error('bank is mono');
return pcm;
},
}, { fps: 60 });
if (!CHECK_ONLY) {
writeFileSync(resolve(OUT, `${spec.name}.wav`),