sound effects
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useStore } from '../store'
|
||||
import { playKeyClick, playDelete, playComplete, playHint, playWordComplete } from '../sounds'
|
||||
|
||||
export function useSounds() {
|
||||
const prevEntries = useRef<string[][] | null>(null)
|
||||
const prevSolved = useRef(false)
|
||||
const prevHints = useRef(0)
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = useStore.subscribe((state) => {
|
||||
if (!state.puzzle) return
|
||||
|
||||
// letter entry or deletion
|
||||
if (prevEntries.current && state.entries !== prevEntries.current) {
|
||||
for (let r = 0; r < state.entries.length; r++) {
|
||||
for (let c = 0; c < state.entries[r].length; c++) {
|
||||
const cur = state.entries[r][c]
|
||||
const old = prevEntries.current[r]?.[c] || ''
|
||||
if (cur && !old) {
|
||||
playKeyClick()
|
||||
// word completion check
|
||||
const span = state.wordSpans.find(s =>
|
||||
s.cells.some(([sr, sc]) => sr === r && sc === c)
|
||||
)
|
||||
if (span) {
|
||||
const allFilled = span.cells.every(([wr, wc]) => state.entries[wr]?.[wc])
|
||||
if (allFilled) {
|
||||
const wasFilled = span.cells.every(([wr, wc]) => prevEntries.current?.[wr]?.[wc])
|
||||
if (!wasFilled) setTimeout(() => playWordComplete(), 50)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
if (!cur && old) {
|
||||
playDelete()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
prevEntries.current = state.entries
|
||||
|
||||
// solved
|
||||
if (state.solved && !prevSolved.current) {
|
||||
playComplete()
|
||||
}
|
||||
prevSolved.current = state.solved
|
||||
|
||||
// hints
|
||||
if (state.hintsUsed > prevHints.current) {
|
||||
playHint()
|
||||
}
|
||||
prevHints.current = state.hintsUsed
|
||||
})
|
||||
|
||||
return unsub
|
||||
}, [])
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useStore } from './store'
|
||||
|
||||
let ctx: AudioContext | null = null
|
||||
|
||||
function getCtx(): AudioContext | null {
|
||||
if (ctx && ctx.state !== 'closed') return ctx
|
||||
try { ctx = new AudioContext() } catch { return null }
|
||||
return ctx
|
||||
}
|
||||
|
||||
function isSoundEnabled(): boolean {
|
||||
return useStore.getState().narratorSettings.sounds
|
||||
}
|
||||
|
||||
function tone(freq: number, duration: number, type: OscillatorType = 'sine', gain = 0.15) {
|
||||
if (!isSoundEnabled()) return
|
||||
const ac = getCtx()
|
||||
if (!ac) return
|
||||
if (ac.state === 'suspended') ac.resume()
|
||||
const osc = ac.createOscillator()
|
||||
const g = ac.createGain()
|
||||
osc.type = type
|
||||
osc.frequency.value = freq
|
||||
g.gain.value = gain
|
||||
g.gain.exponentialRampToValueAtTime(0.001, ac.currentTime + duration / 1000)
|
||||
osc.connect(g).connect(ac.destination)
|
||||
osc.start()
|
||||
osc.stop(ac.currentTime + duration / 1000)
|
||||
}
|
||||
|
||||
export function playKeyClick() {
|
||||
tone(1200, 50, 'sine', 0.08)
|
||||
}
|
||||
|
||||
export function playDelete() {
|
||||
tone(400, 50, 'sine', 0.08)
|
||||
}
|
||||
|
||||
export function playComplete() {
|
||||
if (!isSoundEnabled()) return
|
||||
const ac = getCtx()
|
||||
if (!ac) return
|
||||
if (ac.state === 'suspended') ac.resume()
|
||||
const notes = [523, 659, 784]
|
||||
notes.forEach((freq, i) => {
|
||||
const osc = ac.createOscillator()
|
||||
const g = ac.createGain()
|
||||
osc.type = 'sine'
|
||||
osc.frequency.value = freq
|
||||
g.gain.value = 0.15
|
||||
const start = ac.currentTime + i * 0.1
|
||||
g.gain.exponentialRampToValueAtTime(0.001, start + 0.15)
|
||||
osc.connect(g).connect(ac.destination)
|
||||
osc.start(start)
|
||||
osc.stop(start + 0.15)
|
||||
})
|
||||
}
|
||||
|
||||
export function playHint() {
|
||||
tone(660, 100, 'sine', 0.1)
|
||||
}
|
||||
|
||||
export function playWordComplete() {
|
||||
if (!isSoundEnabled()) return
|
||||
const ac = getCtx()
|
||||
if (!ac) return
|
||||
if (ac.state === 'suspended') ac.resume()
|
||||
const notes = [587, 784]
|
||||
notes.forEach((freq, i) => {
|
||||
const osc = ac.createOscillator()
|
||||
const g = ac.createGain()
|
||||
osc.type = 'sine'
|
||||
osc.frequency.value = freq
|
||||
g.gain.value = 0.12
|
||||
const start = ac.currentTime + i * 0.1
|
||||
g.gain.exponentialRampToValueAtTime(0.001, start + 0.12)
|
||||
osc.connect(g).connect(ac.destination)
|
||||
osc.start(start)
|
||||
osc.stop(start + 0.12)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user