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:
@@ -26,7 +26,30 @@
|
||||
// These are test signals, not music. They exist to be measured.
|
||||
|
||||
import { FeatureTrack } from './FeatureTrack.js';
|
||||
import { synthesizeSong } from './synth.js';
|
||||
import { synthesizeSong, MockAudioBuffer } from './synth.js';
|
||||
|
||||
/**
|
||||
* The bank's PCM contract: peak-normalise to -1 dBFS, then round to the
|
||||
* 16-bit grid — bit-exactly what tools/build-song-bank.js writes into
|
||||
* test/songs/*.wav (int16 LE of round(v * 32767), read back as V / 32768).
|
||||
*
|
||||
* Why this exists: the app decodes the wav while every probe page analyses
|
||||
* the synth, and unless both see these exact samples their features differ
|
||||
* (measured: 3x loudness and a whole extra drop section on drone) and no
|
||||
* preview deep-link can reproduce its image. song() applies it so the bank
|
||||
* IS the files; the writer shares it so the files stay the bank.
|
||||
*/
|
||||
export function bankPcm(samples) {
|
||||
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;
|
||||
const out = new Float32Array(samples.length);
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
const v = Math.max(-1, Math.min(1, samples[i] * gain));
|
||||
out[i] = Math.round(v * 32767) / 32768;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} SongSpec
|
||||
@@ -88,20 +111,24 @@ export function song(name, { fps = 60, duration = DURATION } = {}) {
|
||||
const spec = SONGS.find((s) => s.name === name);
|
||||
if (!spec) throw new Error(`no song named "${name}" — have: ${SONGS.map((s) => s.name).join(', ')}`);
|
||||
|
||||
const entry = {
|
||||
...spec,
|
||||
track: FeatureTrack.fromAudioBuffer(synthesizeSong({
|
||||
bpm: spec.bpm,
|
||||
duration,
|
||||
arrangement: spec.arrangement,
|
||||
brightness: spec.brightness,
|
||||
noise: spec.noise,
|
||||
noiseColour: spec.noiseColour ?? null,
|
||||
dynamics: spec.dynamics,
|
||||
transition: spec.transition ?? 0.5,
|
||||
seed: 1 + SONGS.indexOf(spec),
|
||||
}), { fps }),
|
||||
};
|
||||
// Analysed from the bank PCM, not the raw synth: peak gain, 16-bit
|
||||
// grid, mono — the exact samples test/songs/*.wav holds, so this track
|
||||
// is the file the app decodes rather than a near-twin of it. See bankPcm.
|
||||
const synth = synthesizeSong({
|
||||
bpm: spec.bpm,
|
||||
duration,
|
||||
arrangement: spec.arrangement,
|
||||
brightness: spec.brightness,
|
||||
noise: spec.noise,
|
||||
noiseColour: spec.noiseColour ?? null,
|
||||
dynamics: spec.dynamics,
|
||||
transition: spec.transition ?? 0.5,
|
||||
seed: 1 + SONGS.indexOf(spec),
|
||||
});
|
||||
const mono = new MockAudioBuffer(
|
||||
1, synth.length, synth.sampleRate);
|
||||
mono.getChannelData(0).set(bankPcm(synth.getChannelData(0)));
|
||||
const entry = { ...spec, track: FeatureTrack.fromAudioBuffer(mono, { fps }) };
|
||||
cache.set(cacheKey, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user