diff --git a/src/song-274/Composition.tsx b/src/song-274/Composition.tsx index 373149c..4321ac7 100644 --- a/src/song-274/Composition.tsx +++ b/src/song-274/Composition.tsx @@ -4,7 +4,7 @@ import { loadTransectData } from "./data"; import { darkPalette } from "./palette"; import { Transect, type TransectProps } from "./Transect"; -const FPS = 30; +const FPS = 60; /** Length of song-274/audio/274_release2.wav (see BOM.md). */ const AUDIO_DURATION_S = 619.270083; diff --git a/src/song-274/camera.ts b/src/song-274/camera.ts index f56b9e3..d3bba09 100644 --- a/src/song-274/camera.ts +++ b/src/song-274/camera.ts @@ -27,6 +27,11 @@ export const ZOOM_IN_S = 3; * This is the standard deviation, in seconds; the window spans ±3 of these. */ export const FOLLOW_SMOOTH_S = 0.6; +/** + * How often that window is sampled, in seconds. Independent of fps: finer + * sampling costs more per frame and changes the average only slightly. + */ +export const FOLLOW_SAMPLE_S = 1 / 30; /** * An overview frames the covered road plus some road ahead of the head, so the * head does not sit against the frame edge: this fraction of the distance @@ -95,7 +100,7 @@ export const overviewWeight = (parts: Span[], t: number): number => { /** The head's position averaged over a Gaussian window around `t`. */ export const smoothedFocus = (focusAt: (t: number) => Point, t: number): Point => { - const step = 1 / 30; + const step = FOLLOW_SAMPLE_S; const reach = 3 * FOLLOW_SMOOTH_S; let sumX = 0; let sumY = 0; diff --git a/src/song-274/spectrum.ts b/src/song-274/spectrum.ts index bee856a..d8fa918 100644 --- a/src/song-274/spectrum.ts +++ b/src/song-274/spectrum.ts @@ -1,5 +1,6 @@ import { visualizeAudio, type MediaUtilsAudioData } from "@remotion/media-utils"; import { random } from "remotion"; +import { secondsToFrames } from "./time"; /** * The outro spectrum: from SPECTRUM_START_S the nodes stop following the lyric @@ -43,11 +44,16 @@ export const TREBLE_TILT = 0.5; export const CURVE = 0.6; /** - * Release: a node's level is the loudest of its last RELEASE_FRAMES frames, each - * frame back multiplied by RELEASE_DECAY once more. Hits jump up at once and - * fall back smoothly, without any state carried between frames. + * Release: a node's level is the loudest reading over the RELEASE_WINDOW_S + * before it, sampled every RELEASE_STEP_S and multiplied by RELEASE_DECAY once + * more per step back. Hits jump up at once and fall back smoothly, without any + * state carried between frames. + * + * Sampling by time rather than by frame keeps the tail the same length, and the + * work per frame the same, whatever the fps. */ -export const RELEASE_FRAMES = 6; +export const RELEASE_WINDOW_S = 0.2; +export const RELEASE_STEP_S = 1 / 30; export const RELEASE_DECAY = 0.75; /** FFT size for visualizeAudio: bins are (sample rate / 2) / FFT_SAMPLES wide (23.4 Hz). */ @@ -69,7 +75,7 @@ export const spectrumWeight = (t: number): number => * otherwise the spectrum's start, so nothing is fetched for the rest of the song. */ export const audioWindowFrame = (frame: number, fps: number): number => - Math.max(frame, Math.floor((SPECTRUM_START_S - SPECTRUM_FADE_S) * fps)); + Math.max(frame, secondsToFrames(SPECTRUM_START_S - SPECTRUM_FADE_S, fps)); /** `count` log-spaced band centres from FREQ_LOW_HZ to FREQ_HIGH_HZ. */ export const bandFrequencies = (count: number): number[] => @@ -131,7 +137,12 @@ export const bandLevels = (args: { frequencies.length > 1 ? Math.pow(FREQ_HIGH_HZ / FREQ_LOW_HZ, 1 / (frequencies.length - 1)) : 2; const levels = frequencies.map(() => 0); - for (let back = 0; back < RELEASE_FRAMES && frame - back >= 0; back++) { + const steps = Math.max(1, Math.round(RELEASE_WINDOW_S / RELEASE_STEP_S)); + for (let step = 0; step < steps; step++) { + const back = secondsToFrames(step * RELEASE_STEP_S, fps); + if (frame - back < 0) { + break; + } const spectrum = visualizeAudio({ audioData, frame: frame - back, @@ -140,7 +151,7 @@ export const bandLevels = (args: { dataOffsetInSeconds, optimizeFor: "accuracy", }); - const decay = Math.pow(RELEASE_DECAY, back); + const decay = Math.pow(RELEASE_DECAY, step); frequencies.forEach((hz, i) => { const raw = bandValue(spectrum, hz, ratio, binHz); const level = diff --git a/src/song-274/time.ts b/src/song-274/time.ts new file mode 100644 index 0000000..815e514 --- /dev/null +++ b/src/song-274/time.ts @@ -0,0 +1,14 @@ +/** + * Frame-rate conversions. + * + * Every timing constant in this composition is in seconds, so the animation is + * identical at any fps. Only Remotion's own API counts frames, so convert here + * rather than writing frame counts into the constants. + */ + +/** Seconds to a whole number of frames, rounded to the nearest. */ +export const secondsToFrames = (seconds: number, fps: number): number => + Math.round(seconds * fps); + +/** A frame number back to its time in seconds. */ +export const framesToSeconds = (frames: number, fps: number): number => frames / fps;