Files
Zauberwald/src/audio/sounds.ts
T
gurix 52596c57b0 feat(04-01): add mute button with CSS, wire audio toggle in main.ts
- Add mute-btn element to index.html outside all screen sections
- Add .mute-btn CSS styles (fixed position, bottom-right, circular)
- Wire mute button click handler to toggle audioEnabled in Settings
- Initialize AudioContext on first user interaction (autoplay policy)
- Fix lint issues: import ordering, non-null assertion
2026-03-29 18:18:31 +02:00

152 lines
3.5 KiB
TypeScript

import { getDB } from "../app";
import { getSettings } from "../storage/db";
let audioCtx: AudioContext | null = null;
/**
* Creates or resumes AudioContext on first user interaction.
* Call from a user-interaction event handler (click, keydown) to comply
* with browser autoplay policy.
*/
export function initAudioOnInteraction(): void {
try {
if (!audioCtx) {
audioCtx = new AudioContext();
}
if (audioCtx.state === "suspended") {
audioCtx.resume();
}
} catch {
// AudioContext not supported — silent fallback
}
}
async function isAudioEnabled(): Promise<boolean> {
try {
const db = getDB();
const settings = await getSettings(db);
return settings?.audioEnabled !== false;
} catch {
return true; // default to enabled if settings unavailable
}
}
function ensureAudioCtx(): AudioContext | null {
if (!audioCtx) {
initAudioOnInteraction();
}
return audioCtx;
}
/**
* Short confirmation beep: 800Hz sine, 0.1s, gain 0.15.
*/
export async function playCorrectSound(): Promise<void> {
try {
if (!(await isAudioEnabled())) return;
const ctx = ensureAudioCtx();
if (!ctx) return;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = "sine";
osc.frequency.value = 800;
gain.gain.value = 0.15;
osc.connect(gain);
gain.connect(ctx.destination);
const now = ctx.currentTime;
// Ramp gain to 0 over last 0.02s for click-free cutoff
gain.gain.setValueAtTime(0.15, now);
gain.gain.linearRampToValueAtTime(0, now + 0.1);
osc.start(now);
osc.stop(now + 0.1);
} catch {
// Never throw to caller
}
}
/**
* Ascending reward tone sequence: C5 E5 G5 C6, ~0.8s total.
*/
export async function playRewardSound(): Promise<void> {
try {
if (!(await isAudioEnabled())) return;
const ctx = ensureAudioCtx();
if (!ctx) return;
const notes = [523, 659, 784, 1047]; // C5, E5, G5, C6
const noteSpacing = 0.2;
const noteDuration = 0.15;
const now = ctx.currentTime;
for (const [i, freq] of notes.entries()) {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = "sine";
osc.frequency.value = freq;
gain.gain.value = 0.12;
osc.connect(gain);
gain.connect(ctx.destination);
const startTime = now + i * noteSpacing;
gain.gain.setValueAtTime(0.12, startTime);
gain.gain.linearRampToValueAtTime(0, startTime + noteDuration);
osc.start(startTime);
osc.stop(startTime + noteDuration);
}
} catch {
// Never throw to caller
}
}
/**
* Forest element sound: filtered white noise, bandpass 3000Hz, 0.5s.
*/
export async function playForestElementSound(): Promise<void> {
try {
if (!(await isAudioEnabled())) return;
const ctx = ensureAudioCtx();
if (!ctx) return;
// Create white noise buffer
const bufferSize = ctx.sampleRate * 0.5; // 0.5 seconds
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
const source = ctx.createBufferSource();
source.buffer = buffer;
const filter = ctx.createBiquadFilter();
filter.type = "bandpass";
filter.frequency.value = 3000;
filter.Q.value = 0.5;
const gain = ctx.createGain();
gain.gain.value = 0.1;
source.connect(filter);
filter.connect(gain);
gain.connect(ctx.destination);
const now = ctx.currentTime;
// Ramp down gain over duration
gain.gain.setValueAtTime(0.1, now);
gain.gain.linearRampToValueAtTime(0, now + 0.5);
source.start(now);
source.stop(now + 0.5);
} catch {
// Never throw to caller
}
}