feat(audio): sweep a pass-by through the stereo field on each exit
This commit is contained in:
@@ -8,9 +8,13 @@ const CRUISE_ENGINE_GAIN = 0.055;
|
|||||||
const MAX_ENGINE_GAIN = 0.12;
|
const MAX_ENGINE_GAIN = 0.12;
|
||||||
const CRUISE_ROAD_GAIN = 0.006;
|
const CRUISE_ROAD_GAIN = 0.006;
|
||||||
const MAX_ROAD_GAIN = 0.024;
|
const MAX_ROAD_GAIN = 0.024;
|
||||||
|
const PASS_BY_GAIN = 0.05;
|
||||||
|
const PASS_BY_SECONDS = 1.6;
|
||||||
|
|
||||||
export interface DriveTexture {
|
export interface DriveTexture {
|
||||||
setEnergy(sample: DriveEnergySample): void;
|
setEnergy(sample: DriveEnergySample): void;
|
||||||
|
/** The exit gates going by: a filtered air sweep across the stereo field. */
|
||||||
|
passBy?(): void;
|
||||||
dispose(): void;
|
dispose(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,11 +99,29 @@ export const createDriveTexture: DriveTextureFactory = (context, destination) =>
|
|||||||
roadLowpass.connect(roadGain);
|
roadLowpass.connect(roadGain);
|
||||||
roadGain.connect(destination);
|
roadGain.connect(destination);
|
||||||
|
|
||||||
|
// Pass-by: preallocated and silent until an exit, so a chapter change
|
||||||
|
// schedules automation only and never creates nodes.
|
||||||
|
const passSource = context.createBufferSource();
|
||||||
|
passSource.buffer = roadSource.buffer;
|
||||||
|
passSource.loop = true;
|
||||||
|
const passBand = context.createBiquadFilter();
|
||||||
|
passBand.type = 'bandpass';
|
||||||
|
passBand.frequency.value = 500;
|
||||||
|
passBand.Q.value = 0.9;
|
||||||
|
const passPan = context.createStereoPanner();
|
||||||
|
const passGain = context.createGain();
|
||||||
|
passGain.gain.value = 0;
|
||||||
|
passSource.connect(passBand);
|
||||||
|
passBand.connect(passPan);
|
||||||
|
passPan.connect(passGain);
|
||||||
|
passGain.connect(destination);
|
||||||
|
|
||||||
const sources: readonly AudioScheduledSourceNode[] = [
|
const sources: readonly AudioScheduledSourceNode[] = [
|
||||||
fundamental.source,
|
fundamental.source,
|
||||||
harmonic.source,
|
harmonic.source,
|
||||||
ignition.source,
|
ignition.source,
|
||||||
roadSource,
|
roadSource,
|
||||||
|
passSource,
|
||||||
];
|
];
|
||||||
const ownedNodes: readonly AudioNode[] = [
|
const ownedNodes: readonly AudioNode[] = [
|
||||||
...sources,
|
...sources,
|
||||||
@@ -111,6 +133,9 @@ export const createDriveTexture: DriveTextureFactory = (context, destination) =>
|
|||||||
roadHighpass,
|
roadHighpass,
|
||||||
roadLowpass,
|
roadLowpass,
|
||||||
roadGain,
|
roadGain,
|
||||||
|
passBand,
|
||||||
|
passPan,
|
||||||
|
passGain,
|
||||||
];
|
];
|
||||||
for (const source of sources) source.start();
|
for (const source of sources) source.start();
|
||||||
|
|
||||||
@@ -135,6 +160,19 @@ export const createDriveTexture: DriveTextureFactory = (context, destination) =>
|
|||||||
automate(roadGain.gain, CRUISE_ROAD_GAIN + next * (MAX_ROAD_GAIN - CRUISE_ROAD_GAIN), duration);
|
automate(roadGain.gain, CRUISE_ROAD_GAIN + next * (MAX_ROAD_GAIN - CRUISE_ROAD_GAIN), duration);
|
||||||
currentEnergy = next;
|
currentEnergy = next;
|
||||||
},
|
},
|
||||||
|
passBy() {
|
||||||
|
if (disposed) return;
|
||||||
|
const now = context.currentTime;
|
||||||
|
for (const parameter of [passGain.gain, passBand.frequency, passPan.pan]) {
|
||||||
|
holdParameter(parameter, now);
|
||||||
|
}
|
||||||
|
passGain.gain.linearRampToValueAtTime(PASS_BY_GAIN, now + 0.5);
|
||||||
|
passGain.gain.linearRampToValueAtTime(0, now + PASS_BY_SECONDS);
|
||||||
|
passBand.frequency.linearRampToValueAtTime(2_600, now + 0.5);
|
||||||
|
passBand.frequency.linearRampToValueAtTime(380, now + PASS_BY_SECONDS);
|
||||||
|
passPan.pan.linearRampToValueAtTime(-0.75, now + 0.05);
|
||||||
|
passPan.pan.linearRampToValueAtTime(0.75, now + PASS_BY_SECONDS);
|
||||||
|
},
|
||||||
dispose() {
|
dispose() {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
disposed = true;
|
disposed = true;
|
||||||
|
|||||||
@@ -554,6 +554,9 @@ export function createSoundtrackEngine(
|
|||||||
},
|
},
|
||||||
setChapterIntent(intent) {
|
setChapterIntent(intent) {
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
|
// The scene runs its exit gates on every chapter change; the drive
|
||||||
|
// texture answers with the air of the gates going by.
|
||||||
|
if (intent.sectionId !== latestChapter.sectionId) driveTexture?.passBy?.();
|
||||||
latestChapter = intent;
|
latestChapter = intent;
|
||||||
mixer.setChapterIntent(intent);
|
mixer.setChapterIntent(intent);
|
||||||
if (!mainCommitted && introStarted) {
|
if (!mainCommitted && introStarted) {
|
||||||
|
|||||||
Reference in New Issue
Block a user