feat: dashcam photo mode with telemetry frame, lens drift and postcard capture
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
48
src/audio/switchboard-click.ts
Normal file
48
src/audio/switchboard-click.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user