1
0
Fork 0
polaris-task-force/src/components/frontend/radio/sounds.ts
Z8MB1E 0d58b54977 feat(radio): add radio traffic gameplay
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-03 01:59:49 -04:00

122 lines
4.3 KiB
TypeScript

let audioCtx: AudioContext | null = null;
function getCtx(): AudioContext | null {
if (typeof window === "undefined") return null;
const AudioCtor =
window.AudioContext ??
(window as unknown as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
if (!AudioCtor) return null;
if (!audioCtx) audioCtx = new AudioCtor();
if (audioCtx.state === "suspended") {
void audioCtx.resume();
}
return audioCtx;
}
type ToneOptions = {
readonly frequency: number;
readonly endFrequency?: number;
readonly duration: number;
readonly type?: OscillatorType;
readonly volume?: number;
readonly delay?: number;
};
function playTone(ctx: AudioContext, opts: ToneOptions) {
const { frequency, endFrequency, duration, type = "sine", volume = 0.2, delay = 0 } = opts;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
const t0 = ctx.currentTime + delay;
osc.type = type;
osc.frequency.setValueAtTime(Math.max(frequency, 1), t0);
if (endFrequency && endFrequency !== frequency) {
osc.frequency.exponentialRampToValueAtTime(Math.max(endFrequency, 1), t0 + duration);
}
gain.gain.setValueAtTime(0.0001, t0);
gain.gain.exponentialRampToValueAtTime(volume, t0 + 0.012);
gain.gain.exponentialRampToValueAtTime(0.0001, t0 + duration);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(t0);
osc.stop(t0 + duration + 0.05);
}
function playNoise(
ctx: AudioContext,
opts: { readonly duration: number; readonly volume?: number; readonly delay?: number; readonly lowpass?: number },
) {
const { duration, volume = 0.2, delay = 0, lowpass = 800 } = opts;
const bufferSize = Math.max(1, Math.floor(ctx.sampleRate * duration));
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i += 1) {
data[i] = Math.random() * 2 - 1;
}
const source = ctx.createBufferSource();
source.buffer = buffer;
const filter = ctx.createBiquadFilter();
filter.type = "lowpass";
filter.frequency.setValueAtTime(lowpass, ctx.currentTime + delay);
const gain = ctx.createGain();
const t0 = ctx.currentTime + delay;
gain.gain.setValueAtTime(0.0001, t0);
gain.gain.exponentialRampToValueAtTime(volume, t0 + 0.005);
gain.gain.exponentialRampToValueAtTime(0.0001, t0 + duration);
source.connect(filter);
filter.connect(gain);
gain.connect(ctx.destination);
source.start(t0);
}
export function playKeystroke() {
const ctx = getCtx();
if (!ctx) return;
playTone(ctx, { frequency: 520, duration: 0.025, type: "square", volume: 0.025 });
}
export function playCorrect() {
const ctx = getCtx();
if (!ctx) return;
playTone(ctx, { frequency: 620, endFrequency: 920, duration: 0.08, type: "sine", volume: 0.12 });
}
export function playError() {
const ctx = getCtx();
if (!ctx) return;
playTone(ctx, { frequency: 150, endFrequency: 80, duration: 0.16, type: "square", volume: 0.14 });
playNoise(ctx, { duration: 0.1, volume: 0.08, lowpass: 500 });
}
export function playRoundComplete() {
const ctx = getCtx();
if (!ctx) return;
playTone(ctx, { frequency: 523, duration: 0.1, type: "sine", volume: 0.16 });
playTone(ctx, { frequency: 659, duration: 0.1, type: "sine", volume: 0.16, delay: 0.1 });
playTone(ctx, { frequency: 784, duration: 0.18, type: "sine", volume: 0.18, delay: 0.2 });
}
export function playXp() {
const ctx = getCtx();
if (!ctx) return;
playTone(ctx, { frequency: 660, duration: 0.09, type: "sine", volume: 0.18 });
playTone(ctx, { frequency: 880, duration: 0.16, type: "sine", volume: 0.18, delay: 0.09 });
}
export function playCountdownTick(intensity: "normal" | "fast" | "critical") {
const ctx = getCtx();
if (!ctx) return;
if (intensity === "normal") {
playTone(ctx, { frequency: 880, duration: 0.03, type: "square", volume: 0.05 });
} else if (intensity === "fast") {
playTone(ctx, { frequency: 1046, duration: 0.03, type: "square", volume: 0.07 });
} else {
playTone(ctx, { frequency: 1318, duration: 0.045, type: "square", volume: 0.11 });
}
}
export function playCountdownEnd() {
const ctx = getCtx();
if (!ctx) return;
playTone(ctx, { frequency: 1318, endFrequency: 240, duration: 0.75, type: "sine", volume: 0.16 });
playTone(ctx, { frequency: 659, endFrequency: 120, duration: 0.75, type: "triangle", volume: 0.1 });
}