feat: dashcam photo mode with telemetry frame, lens drift and postcard capture

This commit is contained in:
2026-10-11 07:49:56 +03:00
parent d71b0ecfd4
commit 568b0802b4
29 changed files with 1472 additions and 7 deletions

View File

@@ -9,6 +9,7 @@ let audible = false;
export function attachMusicPulse(node: AnalyserNode | null): void {
analyser = node;
if (node === null) bus = null;
bins = node === null ? null : new Uint8Array(new ArrayBuffer(node.frequencyBinCount));
}
@@ -16,6 +17,13 @@ export function setMusicPulseAudible(value: boolean): void {
audible = value;
}
/**
* DASHCAM: the mix bus (the tap after the mixer's sum, before the output
* gain), so a shutter click obeys mute and volume. Null unless audible.
*/
let bus: AudioNode | null = null;
export const readSwitchboard = (): AudioNode | null => (audible ? bus : null);
/** Bass energy in [0, 1]: the mean of roughly 90-380 Hz. */
export function readMusicPulse(): number {
if (!audible || analyser === null || bins === null) return 0;
@@ -37,5 +45,6 @@ export function createPulseTap(context: BaseAudioContext, destination: AudioNode
} catch {
attachMusicPulse(null);
}
bus = tap;
return tap;
}

View File

@@ -0,0 +1,48 @@
import { readSwitchboard } from '@/audio/music-pulse';
/** Shutter click length, seconds. */
export const SHUTTER_SECONDS = 0.03;
/** The switchboard input's level on the mix bus. */
export const SWITCHBOARD_GAIN = 0.18;
/**
* DASHCAM's shutter: a ~30 ms burst of decaying noise with a fast gain
* envelope, through a switchboard input (gain 0.18) onto the mix bus, so
* mute and volume apply through the output gain. Lives in the lazy dashcam chunk. Plays only while
* the soundtrack is running and audible; never creates an AudioContext and
* never throws.
*/
export function playShutterClick(): boolean {
try {
const input = readSwitchboard();
if (input === null || input.context.state !== 'running') return false;
const context = input.context;
const length = Math.max(1, Math.round(context.sampleRate * SHUTTER_SECONDS));
const buffer = context.createBuffer(1, length, context.sampleRate);
const data = buffer.getChannelData(0);
// Two clicks a few ms apart (curtain open, curtain close) over decaying noise.
for (let i = 0; i < length; i += 1) {
const t = i / length;
const curtain = t > 0.55 && t < 0.62 ? 1.6 : 1;
data[i] = (Math.random() * 2 - 1) * (1 - t) ** 3 * curtain;
}
const source = context.createBufferSource();
source.buffer = buffer;
const envelope = context.createGain();
const start = context.currentTime + 0.005;
envelope.gain.setValueAtTime(0, start);
envelope.gain.linearRampToValueAtTime(SWITCHBOARD_GAIN, start + 0.002);
envelope.gain.exponentialRampToValueAtTime(0.0002, start + SHUTTER_SECONDS);
source.connect(envelope);
envelope.connect(input);
source.onended = () => {
source.disconnect();
envelope.disconnect();
};
source.start(start);
source.stop(start + SHUTTER_SECONDS + 0.01);
return true;
} catch {
return false;
}
}