2026-02-17 19:19:54 +00:00
|
|
|
/**
|
|
|
|
|
* Epic interface sounds for controller/keyboard navigation.
|
|
|
|
|
* Layered synthesis - cool, impactful, celebratory for actions.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
let audioContext: AudioContext | null = null
|
|
|
|
|
|
|
|
|
|
function getContext(): AudioContext | null {
|
|
|
|
|
if (audioContext) return audioContext
|
|
|
|
|
try {
|
2026-03-04 05:23:42 +00:00
|
|
|
audioContext = new (window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext)()
|
2026-02-17 19:19:54 +00:00
|
|
|
return audioContext
|
|
|
|
|
} catch {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function playTone(
|
|
|
|
|
ctx: AudioContext,
|
|
|
|
|
freq: number,
|
|
|
|
|
duration: number,
|
|
|
|
|
gain: number,
|
|
|
|
|
type: OscillatorType = 'sine',
|
|
|
|
|
startOffset = 0
|
|
|
|
|
) {
|
|
|
|
|
const osc = ctx.createOscillator()
|
|
|
|
|
const g = ctx.createGain()
|
|
|
|
|
osc.connect(g)
|
|
|
|
|
g.connect(ctx.destination)
|
|
|
|
|
g.gain.setValueAtTime(0, ctx.currentTime)
|
|
|
|
|
g.gain.linearRampToValueAtTime(gain, ctx.currentTime + 0.01)
|
|
|
|
|
g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration)
|
|
|
|
|
osc.frequency.value = freq
|
|
|
|
|
osc.type = type
|
|
|
|
|
osc.start(ctx.currentTime + startOffset)
|
|
|
|
|
osc.stop(ctx.currentTime + startOffset + duration)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function playNavSound(type: 'move' | 'select' | 'action' | 'back' = 'move') {
|
|
|
|
|
if (type === 'move') {
|
|
|
|
|
const audio = new Audio('/assets/audio/arrows.mp3')
|
|
|
|
|
audio.volume = 0.5
|
|
|
|
|
audio.play().catch(() => {})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (type === 'select' || type === 'action') {
|
|
|
|
|
const audio = new Audio('/assets/audio/enter.mp3')
|
|
|
|
|
audio.volume = 0.5
|
|
|
|
|
audio.play().catch(() => {})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ctx = getContext()
|
|
|
|
|
if (!ctx) return
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (ctx.state === 'suspended') ctx.resume()
|
|
|
|
|
} catch {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
|
case 'back': {
|
|
|
|
|
playTone(ctx, 440, 0.06, 0.08, 'sine')
|
|
|
|
|
playTone(ctx, 330, 0.08, 0.05, 'sine', 0.03)
|
|
|
|
|
playTone(ctx, 220, 0.1, 0.04, 'triangle', 0.05)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|