From 954b64ebe9c3902b34814097acf9954195e41fdb Mon Sep 17 00:00:00 2001 From: Dorian Date: Sun, 8 Mar 2026 22:12:22 +0000 Subject: [PATCH] refactor: remove old sounds.ts (replaced by audio/ modules) Co-Authored-By: Claude Opus 4.6 --- frontend/src/game/sounds.ts | 3521 ----------------------------------- 1 file changed, 3521 deletions(-) delete mode 100644 frontend/src/game/sounds.ts diff --git a/frontend/src/game/sounds.ts b/frontend/src/game/sounds.ts deleted file mode 100644 index ae8cd5b..0000000 --- a/frontend/src/game/sounds.ts +++ /dev/null @@ -1,3521 +0,0 @@ -// Procedural 8-bit sound system + announcer voice using Web Audio + Kokoro TTS -// Falls back to Speech Synthesis if Kokoro model hasn't loaded yet -import { initKokoro, isKokoroReady, kokoroSpeak, kokoroSpeakAsync, kokoroStop, kokoroPrefetch, setAudioContext } from './tts' - -let ctx: AudioContext | null = null -let musicGain: GainNode | null = null -let sfxGain: GainNode | null = null -let musicPlaying = false -let musicTimeout: number | null = null - -// AudioContext is ONLY created inside ensureAudioContext() (called from user gesture). -// Before that, all SFX calls silently no-op to avoid Chrome autoplay warnings. -let _audioUnlocked = false - -function initCtx() { - if (ctx) return - ctx = new AudioContext() - musicGain = ctx.createGain() - musicGain.gain.value = masterMuted ? 0 : 0.12 - musicGain.connect(ctx.destination) - sfxGain = ctx.createGain() - sfxGain.gain.value = masterMuted ? 0 : 0.25 - sfxGain.connect(ctx.destination) -} - -function getCtx(): AudioContext { - if (!ctx) initCtx() - if (ctx!.state === 'suspended') { - ctx!.resume().catch(() => {}) - } - return ctx! -} - -// Get the SFX destination node (gain is 0 when muted, so audio is silent but context stays valid) -function getSfxDest(): AudioNode { - const c = getCtx() - return sfxGain ?? c.destination -} - -function tone(freq: number, type: OscillatorType, duration: number, dest: AudioNode, startTime?: number) { - const c = getCtx() - const osc = c.createOscillator() - const g = c.createGain() - osc.type = type - osc.frequency.value = freq - const t = startTime ?? c.currentTime - g.gain.setValueAtTime(0.3, t) - g.gain.exponentialRampToValueAtTime(0.001, t + duration) - osc.connect(g) - g.connect(dest) - osc.start(t) - osc.stop(t + duration) -} - -function noise(duration: number, dest: AudioNode, startTime?: number) { - const c = getCtx() - const bufferSize = Math.max(1, Math.floor(c.sampleRate * duration)) - const buffer = c.createBuffer(1, bufferSize, c.sampleRate) - const data = buffer.getChannelData(0) - for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1 - const src = c.createBufferSource() - src.buffer = buffer - const g = c.createGain() - const t = startTime ?? c.currentTime - g.gain.setValueAtTime(0.4, t) - g.gain.exponentialRampToValueAtTime(0.001, t + duration) - const filter = c.createBiquadFilter() - filter.type = 'highpass' - filter.frequency.value = 800 - src.connect(filter) - filter.connect(g) - g.connect(dest) - src.start(t) - src.stop(t + duration) -} - -function sweep(startFreq: number, endFreq: number, type: OscillatorType, duration: number, dest: AudioNode) { - const c = getCtx() - const osc = c.createOscillator() - const g = c.createGain() - osc.type = type - osc.frequency.setValueAtTime(startFreq, c.currentTime) - osc.frequency.exponentialRampToValueAtTime(Math.max(1, endFreq), c.currentTime + duration) - g.gain.setValueAtTime(0.3, c.currentTime) - g.gain.exponentialRampToValueAtTime(0.001, c.currentTime + duration) - osc.connect(g) - g.connect(dest) - osc.start() - osc.stop(c.currentTime + duration) -} - -// === ANNOUNCER VOICE SYSTEM (Speech Synthesis with multiple voice types) === - -interface VoiceProfile { - voice: SpeechSynthesisVoice | null - pitch: number - rate: number - volume: number -} - -let voicesLoaded = false -const voiceProfiles: Record = { - // Original 6 - announcer: { voice: null, pitch: 1.0, rate: 0.8, volume: 1.0 }, // Natural, authoritative announcer - hype: { voice: null, pitch: 1.1, rate: 1.4, volume: 1.0 }, // Fast excited commentator - deep: { voice: null, pitch: 0.7, rate: 0.7, volume: 1.0 }, // Lower but still clear - robot: { voice: null, pitch: 0.8, rate: 0.8, volume: 0.9 }, // Steady monotone - screamer: { voice: null, pitch: 1.3, rate: 1.6, volume: 1.0 }, // Frantic energy - smooth: { voice: null, pitch: 1.0, rate: 0.9, volume: 0.9 }, // Natural narrator - question_reader: { voice: null, pitch: 1.0, rate: 1.05, volume: 1.0 }, // Clear, brisk question announcer - // 24 new profiles - whisper: { voice: null, pitch: 1.2, rate: 0.6, volume: 0.4 }, // Quiet dramatic whisper - boomer: { voice: null, pitch: 0.4, rate: 0.6, volume: 1.0 }, // Ultra deep booming - chipmunk: { voice: null, pitch: 2.0, rate: 1.8, volume: 0.9 }, // Tiny squeaky fast - drill: { voice: null, pitch: 0.6, rate: 1.2, volume: 1.0 }, // Drill sergeant bark - surfer: { voice: null, pitch: 1.1, rate: 1.0, volume: 0.8 }, // Laid back dude - auctioneer: { voice: null, pitch: 1.0, rate: 2.0, volume: 1.0 }, // Lightning fast - preacher: { voice: null, pitch: 0.8, rate: 0.6, volume: 1.0 }, // Dramatic pause king - baby: { voice: null, pitch: 1.8, rate: 1.0, volume: 0.7 }, // High pitched cute - grandpa: { voice: null, pitch: 0.5, rate: 0.5, volume: 0.8 }, // Slow, gravelly old man - valley: { voice: null, pitch: 1.4, rate: 1.3, volume: 0.9 }, // Valley girl energy - movie: { voice: null, pitch: 0.6, rate: 0.7, volume: 1.0 }, // Movie trailer bass - sportscaster:{ voice: null, pitch: 1.0, rate: 1.5, volume: 1.0 }, // Play-by-play energy - opera: { voice: null, pitch: 0.9, rate: 0.5, volume: 1.0 }, // Dramatic operatic - punk: { voice: null, pitch: 1.3, rate: 1.3, volume: 1.0 }, // Aggressive snarl - wizard_v: { voice: null, pitch: 0.7, rate: 0.8, volume: 0.8 }, // Mystical old sage - pirate_v: { voice: null, pitch: 0.8, rate: 0.9, volume: 1.0 }, // Arr matey - alien_v: { voice: null, pitch: 1.6, rate: 0.7, volume: 0.7 }, // Otherworldly slow - cowboy_v: { voice: null, pitch: 0.9, rate: 0.8, volume: 0.9 }, // Drawl - ninja_v: { voice: null, pitch: 1.1, rate: 1.1, volume: 0.5 }, // Quiet but deadly - demon_v: { voice: null, pitch: 0.3, rate: 0.6, volume: 1.0 }, // Deepest evil - angel: { voice: null, pitch: 1.5, rate: 0.8, volume: 0.7 }, // Ethereal high - glitch: { voice: null, pitch: 1.0, rate: 1.8, volume: 0.8 }, // Stuttery fast - echo_v: { voice: null, pitch: 0.9, rate: 0.7, volume: 0.9 }, // Reverb cave voice - hyper: { voice: null, pitch: 1.4, rate: 2.0, volume: 1.0 }, // Maximum speed maximum hype - // Robots & computers - mech: { voice: null, pitch: 0.5, rate: 0.9, volume: 1.0 }, // Heavy mech unit - ai_core: { voice: null, pitch: 0.9, rate: 1.0, volume: 0.8 }, // Calm AI assistant - dial_up: { voice: null, pitch: 1.7, rate: 1.5, volume: 0.7 }, // Squeaky modem era - mainframe: { voice: null, pitch: 0.3, rate: 0.5, volume: 1.0 }, // Deep supercomputer - android_v: { voice: null, pitch: 1.0, rate: 1.1, volume: 0.9 }, // Almost human android - glitchbot: { voice: null, pitch: 1.5, rate: 2.0, volume: 0.8 }, // Malfunctioning robot - siri: { voice: null, pitch: 1.2, rate: 1.0, volume: 0.9 }, // Polite digital assistant - hal: { voice: null, pitch: 0.6, rate: 0.6, volume: 0.9 }, // Menacing calm computer - // Old people & wise - grandma: { voice: null, pitch: 1.3, rate: 0.4, volume: 0.7 }, // Sweet slow grandma - professor: { voice: null, pitch: 0.8, rate: 0.7, volume: 0.8 }, // Lecturing academic - ancient: { voice: null, pitch: 0.4, rate: 0.3, volume: 0.6 }, // Ancient being, barely audible - sensei: { voice: null, pitch: 0.7, rate: 0.5, volume: 0.8 }, // Wise martial arts master - crotchety: { voice: null, pitch: 0.6, rate: 0.9, volume: 1.0 }, // Angry old man yelling - // Game-sounding - final_boss: { voice: null, pitch: 0.2, rate: 0.4, volume: 1.0 }, // Ultimate villain reveal - npc: { voice: null, pitch: 1.1, rate: 0.9, volume: 0.7 }, // Generic quest giver - tutorial: { voice: null, pitch: 1.3, rate: 1.1, volume: 0.8 }, // Annoying tutorial fairy - game_over: { voice: null, pitch: 0.5, rate: 0.7, volume: 1.0 }, // YOU DIED narrator - power_up: { voice: null, pitch: 1.6, rate: 1.4, volume: 1.0 }, // Excited power-up voice - boss_taunt: { voice: null, pitch: 0.4, rate: 0.8, volume: 1.0 }, // Boss mid-fight taunt - // Accents & character - posh: { voice: null, pitch: 1.0, rate: 0.7, volume: 0.9 }, // British upper class - aussie: { voice: null, pitch: 0.9, rate: 1.1, volume: 1.0 }, // Australian energy - scottish: { voice: null, pitch: 0.8, rate: 1.2, volume: 1.0 }, // Scottish intensity - french: { voice: null, pitch: 1.2, rate: 0.8, volume: 0.8 }, // French disdain - texan: { voice: null, pitch: 0.65, rate: 0.75, volume: 1.0 }, // Big Texan energy - // More characters - drunk: { voice: null, pitch: 0.9, rate: 0.5, volume: 0.8 }, // Slurring bar patron - sleepy: { voice: null, pitch: 0.8, rate: 0.3, volume: 0.5 }, // About to pass out - terrified: { voice: null, pitch: 1.8, rate: 1.7, volume: 1.0 }, // Absolutely panicking - giant: { voice: null, pitch: 0.1, rate: 0.4, volume: 1.0 }, // Fee fi fo fum - fairy: { voice: null, pitch: 2.0, rate: 1.3, volume: 0.6 }, // Tinkerbell energy - wrestler_v: { voice: null, pitch: 0.5, rate: 1.0, volume: 1.0 }, // WWE promo voice - karen: { voice: null, pitch: 1.4, rate: 1.5, volume: 1.0 }, // Wants to speak to manager - stoner: { voice: null, pitch: 0.9, rate: 0.4, volume: 0.6 }, // Whoooa duuude - news: { voice: null, pitch: 1.0, rate: 1.0, volume: 1.0 }, // Breaking news anchor - conspiracy: { voice: null, pitch: 1.1, rate: 1.3, volume: 0.7 }, // Wake up sheeple whisper -} - -function loadVoices() { - if (typeof speechSynthesis === 'undefined') return - const voices = speechSynthesis.getVoices() - if (voices.length === 0) return - voicesLoaded = true - - // Find different English voices for variety - const enVoices = voices.filter(v => v.lang.startsWith('en')) - const anyVoices = enVoices.length > 0 ? enVoices : voices - - // Try to assign different voices to different profiles - const findVoice = (patterns: RegExp[]) => { - for (const p of patterns) { - const v = anyVoices.find(v => p.test(v.name)) - if (v) return v - } - return null - } - - // Prefer premium/enhanced voices (sound natural, not robotic) - // On macOS: "Evan (Premium)" / "Samantha (Enhanced)" / "Daniel" are best - // On Chrome: "Google US English" / "Google UK English Male" are high quality - const preferPremium = (patterns: RegExp[]) => { - // First try premium/enhanced voices - const premium = anyVoices.find(v => /premium|enhanced|natural|neural/i.test(v.name)) - if (premium) return premium - return findVoice(patterns) - } - voiceProfiles.announcer.voice = preferPremium([/evan/i, /aaron/i, /daniel/i, /google.*us.*male/i, /james/i, /male/i]) || anyVoices[0] - voiceProfiles.hype.voice = findVoice([/samantha.*enhanced/i, /samantha/i, /karen/i, /google.*us/i, /female/i]) || anyVoices[Math.min(1, anyVoices.length - 1)] - voiceProfiles.deep.voice = findVoice([/evan/i, /aaron/i, /daniel/i, /alex/i, /tom/i]) || anyVoices[0] - voiceProfiles.robot.voice = findVoice([/zarvox/i, /trinoids/i, /albert/i]) || anyVoices[0] - voiceProfiles.screamer.voice = findVoice([/samantha/i, /karen/i, /bells/i]) || anyVoices[Math.min(2, anyVoices.length - 1)] - voiceProfiles.smooth.voice = findVoice([/daniel/i, /oliver/i, /google.*uk/i]) || anyVoices[Math.min(1, anyVoices.length - 1)] - // Question reader: highest quality voice available — tries premium/neural first - voiceProfiles.question_reader.voice = preferPremium([ - /evan.*premium/i, /samantha.*enhanced/i, /daniel.*premium/i, - /google.*us.*english/i, /google.*uk.*english/i, - /evan/i, /aaron/i, /daniel/i, /james/i, - ]) || anyVoices[0] - // Assign voices to new profiles — spread across available voices for max variety - const vLen = anyVoices.length - const pick = (i: number) => anyVoices[i % vLen] - voiceProfiles.whisper.voice = findVoice([/samantha/i, /tessa/i, /female/i]) || pick(0) - voiceProfiles.boomer.voice = findVoice([/evan/i, /tom/i, /alex/i, /male/i]) || pick(0) - voiceProfiles.chipmunk.voice = findVoice([/samantha/i, /karen/i, /bells/i]) || pick(1) - voiceProfiles.drill.voice = findVoice([/evan/i, /aaron/i, /james/i]) || pick(0) - voiceProfiles.surfer.voice = findVoice([/oliver/i, /google.*us/i, /male/i]) || pick(2) - voiceProfiles.auctioneer.voice = findVoice([/evan/i, /daniel/i, /google.*us/i]) || pick(0) - voiceProfiles.preacher.voice = findVoice([/daniel/i, /tom/i, /google.*uk/i]) || pick(1) - voiceProfiles.baby.voice = findVoice([/samantha/i, /karen/i, /bells/i]) || pick(1) - voiceProfiles.grandpa.voice = findVoice([/evan/i, /alex/i, /tom/i]) || pick(0) - voiceProfiles.valley.voice = findVoice([/samantha/i, /tessa/i, /karen/i]) || pick(1) - voiceProfiles.movie.voice = findVoice([/evan/i, /aaron/i, /daniel/i]) || pick(0) - voiceProfiles.sportscaster.voice = preferPremium([/evan/i, /james/i, /google.*us/i]) || pick(0) - voiceProfiles.opera.voice = findVoice([/daniel/i, /oliver/i, /google.*uk/i]) || pick(2) - voiceProfiles.punk.voice = findVoice([/samantha/i, /karen/i, /bells/i]) || pick(1) - voiceProfiles.wizard_v.voice = findVoice([/daniel/i, /oliver/i, /google.*uk/i]) || pick(2) - voiceProfiles.pirate_v.voice = findVoice([/evan/i, /alex/i, /tom/i]) || pick(0) - voiceProfiles.alien_v.voice = findVoice([/zarvox/i, /trinoids/i, /albert/i]) || pick(3 % vLen) - voiceProfiles.cowboy_v.voice = findVoice([/evan/i, /tom/i, /alex/i]) || pick(0) - voiceProfiles.ninja_v.voice = findVoice([/daniel/i, /oliver/i]) || pick(2) - voiceProfiles.demon_v.voice = findVoice([/evan/i, /aaron/i, /alex/i]) || pick(0) - voiceProfiles.angel.voice = findVoice([/samantha/i, /karen/i, /tessa/i]) || pick(1) - voiceProfiles.glitch.voice = findVoice([/zarvox/i, /trinoids/i, /albert/i]) || pick(0) - voiceProfiles.echo_v.voice = findVoice([/daniel/i, /tom/i, /google.*uk/i]) || pick(2) - voiceProfiles.hyper.voice = findVoice([/samantha/i, /karen/i, /google.*us/i]) || pick(1) - // Robots & computers — prefer novelty/robotic voices - voiceProfiles.mech.voice = findVoice([/zarvox/i, /trinoids/i, /albert/i, /bad news/i]) || pick(0) - voiceProfiles.ai_core.voice = findVoice([/samantha/i, /siri/i, /google.*us/i]) || pick(1) - voiceProfiles.dial_up.voice = findVoice([/trinoids/i, /zarvox/i, /bells/i]) || pick(3 % vLen) - voiceProfiles.mainframe.voice = findVoice([/zarvox/i, /albert/i, /evan/i]) || pick(0) - voiceProfiles.android_v.voice = findVoice([/samantha.*enhanced/i, /google.*us/i, /evan.*premium/i]) || pick(1) - voiceProfiles.glitchbot.voice = findVoice([/trinoids/i, /zarvox/i, /bells/i]) || pick(3 % vLen) - voiceProfiles.siri.voice = findVoice([/samantha.*enhanced/i, /samantha/i, /google.*us.*female/i]) || pick(1) - voiceProfiles.hal.voice = findVoice([/daniel/i, /oliver/i, /google.*uk.*male/i]) || pick(2) - // Old people & wise — prefer deeper/slower voices - voiceProfiles.grandma.voice = findVoice([/samantha/i, /tessa/i, /karen/i, /female/i]) || pick(1) - voiceProfiles.professor.voice = findVoice([/daniel/i, /oliver/i, /google.*uk/i]) || pick(2) - voiceProfiles.ancient.voice = findVoice([/evan/i, /tom/i, /alex/i]) || pick(0) - voiceProfiles.sensei.voice = findVoice([/daniel/i, /oliver/i, /tom/i]) || pick(2) - voiceProfiles.crotchety.voice = findVoice([/evan/i, /alex/i, /aaron/i]) || pick(0) - // Game-sounding - voiceProfiles.final_boss.voice = findVoice([/evan/i, /aaron/i, /alex/i]) || pick(0) - voiceProfiles.npc.voice = findVoice([/samantha/i, /daniel/i, /google.*us/i]) || pick(1) - voiceProfiles.tutorial.voice = findVoice([/samantha/i, /karen/i, /bells/i]) || pick(1) - voiceProfiles.game_over.voice = findVoice([/evan/i, /tom/i, /daniel/i]) || pick(0) - voiceProfiles.power_up.voice = findVoice([/samantha/i, /karen/i, /google.*us/i]) || pick(1) - voiceProfiles.boss_taunt.voice = findVoice([/evan/i, /aaron/i, /zarvox/i]) || pick(0) - // Accents — try to find actual accent voices - const ukVoices = voices.filter(v => /en.gb|en.uk|en-GB|en-UK/i.test(v.lang)) - const auVoices = voices.filter(v => /en.au|en-AU/i.test(v.lang)) - const frVoices = voices.filter(v => /fr/i.test(v.lang)) - voiceProfiles.posh.voice = ukVoices[0] || findVoice([/daniel/i, /oliver/i, /google.*uk/i]) || pick(2) - voiceProfiles.aussie.voice = auVoices[0] || findVoice([/karen/i, /lee/i]) || pick(2) - voiceProfiles.scottish.voice = findVoice([/fiona/i, /moira/i]) || ukVoices[1] || pick(2) - voiceProfiles.french.voice = frVoices[0] || findVoice([/thomas/i, /amelie/i]) || pick(3 % vLen) - voiceProfiles.texan.voice = findVoice([/evan/i, /tom/i, /alex/i]) || pick(0) - // More characters - voiceProfiles.drunk.voice = findVoice([/evan/i, /tom/i, /alex/i]) || pick(0) - voiceProfiles.sleepy.voice = findVoice([/daniel/i, /oliver/i, /tom/i]) || pick(2) - voiceProfiles.terrified.voice = findVoice([/samantha/i, /karen/i, /bells/i]) || pick(1) - voiceProfiles.giant.voice = findVoice([/evan/i, /aaron/i, /alex/i]) || pick(0) - voiceProfiles.fairy.voice = findVoice([/samantha/i, /bells/i, /karen/i]) || pick(1) - voiceProfiles.wrestler_v.voice = findVoice([/evan/i, /aaron/i, /james/i]) || pick(0) - voiceProfiles.karen.voice = findVoice([/samantha/i, /karen/i, /tessa/i]) || pick(1) - voiceProfiles.stoner.voice = findVoice([/evan/i, /tom/i, /oliver/i]) || pick(0) - voiceProfiles.news.voice = preferPremium([/evan.*premium/i, /google.*us/i, /james/i]) || pick(0) - voiceProfiles.conspiracy.voice = findVoice([/daniel/i, /tom/i, /whisper/i]) || pick(2) -} - -if (typeof speechSynthesis !== 'undefined') { - speechSynthesis.onvoiceschanged = loadVoices - loadVoices() - // Chrome bug workaround: speechSynthesis pauses after ~15s. - // Periodic resume() keeps it alive — but only if Kokoro isn't handling TTS. - setInterval(() => { - if (isKokoroReady()) return // Kokoro active — don't touch Web Speech - if (speechSynthesis.speaking && !speechSynthesis.paused) return - if (speechSynthesis.paused) speechSynthesis.resume() - }, 5000) - // Resume speech + audio when tab becomes visible again - if (typeof document !== 'undefined') { - document.addEventListener('visibilitychange', () => { - if (document.visibilityState === 'visible') { - if (!isKokoroReady() && speechSynthesis.paused) speechSynthesis.resume() - if (ctx?.state === 'suspended') ctx.resume().catch(() => {}) - } - }) - } -} - -let _speechQueueDepth = 0 -// Scale speech volume down so voice doesn't overpower SFX/music -const VOICE_VOLUME_SCALE = 0.7 - -let _webSpeechActive = false // track if Web Speech has queued utterances - -function _cancelWebSpeech() { - if (typeof speechSynthesis !== 'undefined' && _webSpeechActive) { - speechSynthesis.cancel() - _speechQueueDepth = 0 - _webSpeechActive = false - } -} - -export function speak(text: string, profileName: string, cancelPrevious: boolean = false, _echo: boolean = false) { - if (masterMuted) return - // Try Kokoro TTS first — high quality, no browser bugs - if (isKokoroReady() && sfxGain) { - // Kill any lingering Web Speech utterances so voices don't double - _cancelWebSpeech() - const profile = voiceProfiles[profileName] || voiceProfiles.announcer - if (cancelPrevious) kokoroStop() - kokoroSpeak(text, profileName, sfxGain, profile.volume * VOICE_VOLUME_SCALE) - return - } - // Fallback: Web Speech API (only used while Kokoro model is loading) - if (typeof speechSynthesis === 'undefined') return - if (!voicesLoaded) loadVoices() - if (speechSynthesis.paused) speechSynthesis.resume() - if (cancelPrevious || (speechSynthesis.pending && speechSynthesis.speaking)) { - if (_speechQueueDepth > 2) { - speechSynthesis.cancel() - _speechQueueDepth = 0 - } - } - const profile = voiceProfiles[profileName] || voiceProfiles.announcer - const utter = new SpeechSynthesisUtterance(text) - if (profile.voice) utter.voice = profile.voice - utter.pitch = profile.pitch - utter.rate = profile.rate - utter.volume = profile.volume * VOICE_VOLUME_SCALE - _speechQueueDepth++ - _webSpeechActive = true - utter.onend = () => { - _speechQueueDepth = Math.max(0, _speechQueueDepth - 1) - if (_speechQueueDepth === 0) _webSpeechActive = false - } - utter.onerror = (ev) => { - _speechQueueDepth = Math.max(0, _speechQueueDepth - 1) - if (_speechQueueDepth === 0) _webSpeechActive = false - if (ev.error !== 'canceled' && ev.error !== 'interrupted') { - // Only retry if Kokoro still isn't ready — avoid doubling - if (!isKokoroReady()) { - setTimeout(() => { - if (!masterMuted && !isKokoroReady() && typeof speechSynthesis !== 'undefined') { - const retry = new SpeechSynthesisUtterance(text) - if (profile.voice) retry.voice = profile.voice - retry.pitch = profile.pitch; retry.rate = profile.rate; retry.volume = profile.volume * VOICE_VOLUME_SCALE - _speechQueueDepth++ - _webSpeechActive = true - retry.onend = () => { _speechQueueDepth = Math.max(0, _speechQueueDepth - 1); if (_speechQueueDepth === 0) _webSpeechActive = false } - retry.onerror = () => { _speechQueueDepth = Math.max(0, _speechQueueDepth - 1); if (_speechQueueDepth === 0) _webSpeechActive = false } - speechSynthesis.speak(retry) - } - }, 200) - } - } - } - speechSynthesis.speak(utter) -} - -// iOS Safari breaks with pause()/resume() — only do keepalive on desktop Chrome -const _isIOS = typeof navigator !== 'undefined' && /iPad|iPhone|iPod/.test(navigator.userAgent) -const _isDesktopChrome = typeof navigator !== 'undefined' && /Chrome/.test(navigator.userAgent) && !/Mobile/.test(navigator.userAgent) - -/** Core async speak — resolves when speech finishes or bails fast if speech won't work */ -async function _speakAsyncCore(text: string, profileName: string, rateOverride?: number, cancelPrevious?: boolean): Promise { - if (masterMuted) return - // Try Kokoro TTS first - if (isKokoroReady() && sfxGain) { - _cancelWebSpeech() - if (cancelPrevious) kokoroStop() - const profile = voiceProfiles[profileName] || voiceProfiles.announcer - const handled = await kokoroSpeakAsync(text, profileName, sfxGain, profile.volume * VOICE_VOLUME_SCALE) - if (handled) return - } - // Fallback: Web Speech API - return new Promise((resolve) => { - if (typeof speechSynthesis === 'undefined') { resolve(); return } - if (!voicesLoaded) loadVoices() - if (!voicesLoaded) { resolve(); return } - if (speechSynthesis.paused) speechSynthesis.resume() - if (cancelPrevious) { speechSynthesis.cancel(); _speechQueueDepth = 0 } - const profile = voiceProfiles[profileName] || voiceProfiles.announcer - const utter = new SpeechSynthesisUtterance(text) - if (profile.voice) utter.voice = profile.voice - utter.pitch = profile.pitch - utter.rate = rateOverride !== undefined ? Math.max(rateOverride, profile.rate) : profile.rate - utter.volume = profile.volume * VOICE_VOLUME_SCALE - _speechQueueDepth++ - let done = false - const cleanup = () => { - if (done) return - done = true - clearTimeout(safetyTimeout) - clearTimeout(startupCheck) - if (keepalive) clearInterval(keepalive) - _speechQueueDepth = Math.max(0, _speechQueueDepth - 1) - resolve() - } - const safetyTimeout = setTimeout(cleanup, 8_000) - const startupCheck = setTimeout(() => { - if (!speechSynthesis.speaking && !speechSynthesis.pending) cleanup() - }, 500) - let keepalive: ReturnType | null = null - if (_isDesktopChrome) { - keepalive = setInterval(() => { - if (speechSynthesis.speaking && !speechSynthesis.paused) { - speechSynthesis.pause() - speechSynthesis.resume() - } - }, 10_000) - } - utter.onend = cleanup - utter.onerror = cleanup - speechSynthesis.speak(utter) - }) -} - -/** Like speakAsync but with a forced minimum rate */ -function speakAsyncWithRate(text: string, profileName: string, minRate: number): Promise { - return _speakAsyncCore(text, profileName, minRate) -} - -/** Like speak() but returns a promise that resolves when the utterance finishes */ -function speakAsync(text: string, profileName: string, cancelPrevious: boolean = false): Promise { - return _speakAsyncCore(text, profileName, undefined, cancelPrevious) -} - -export function stopAllAudio() { - stopMusic() - kokoroStop() - if (typeof speechSynthesis !== 'undefined') speechSynthesis.cancel() - _speechQueueDepth = 0 - _webSpeechActive = false - // Disconnect gain nodes to instantly kill all in-flight oscillators/buffers, - // then reconnect so future sounds still work - if (ctx) { - if (musicGain) { - musicGain.disconnect() - musicGain = ctx.createGain() - musicGain.gain.value = masterMuted ? 0 : MUSIC_VOL - musicGain.connect(ctx.destination) - } - if (sfxGain) { - sfxGain.disconnect() - sfxGain = ctx.createGain() - sfxGain.gain.value = masterMuted ? 0 : SFX_VOL - sfxGain.connect(ctx.destination) - } - } -} - -// Public voice functions -export function announce(text: string, _pitch?: number, _rate?: number) { - // Always route through speak() — prevents doubled voices from separate Web Speech paths - speak(text, 'announcer') -} - -export function announceDeep(text: string) { speak(text, 'deep') } -export function announceFast(text: string) { speak(text, 'hype') } -export function announceRobot(text: string) { speak(text, 'robot') } -export function announceScream(text: string) { speak(text, 'screamer') } -export function announceSmooth(text: string) { speak(text, 'smooth') } - -// Pick a random voice profile for variety -const ALL_VOICE_KEYS = Object.keys(voiceProfiles) -export function announceRandom(text: string) { - const key = ALL_VOICE_KEYS[Math.floor(Math.random() * ALL_VOICE_KEYS.length)] - speak(text, key) -} -// Announce with a specific mood category -const DRAMATIC_VOICES = ['deep', 'boomer', 'movie', 'preacher', 'opera', 'demon_v', 'echo_v', 'final_boss', 'game_over', 'mainframe', 'hal', 'ancient', 'giant'] -const HYPE_VOICES = ['hype', 'screamer', 'sportscaster', 'auctioneer', 'hyper', 'punk', 'drill', 'wrestler_v', 'karen', 'terrified', 'power_up', 'news', 'scottish'] -const SILLY_VOICES = ['chipmunk', 'baby', 'surfer', 'valley', 'pirate_v', 'alien_v', 'glitch', 'drunk', 'stoner', 'fairy', 'tutorial', 'npc', 'dial_up', 'glitchbot', 'grandma', 'conspiracy'] -const COOL_VOICES = ['smooth', 'wizard_v', 'ninja_v', 'cowboy_v', 'angel', 'whisper', 'posh', 'aussie', 'french', 'sensei', 'ai_core', 'android_v', 'siri', 'professor', 'texan', 'boss_taunt', 'sleepy'] -export function announceDramatic(text: string) { speak(text, DRAMATIC_VOICES[Math.floor(Math.random() * DRAMATIC_VOICES.length)]) } -export function announceHype(text: string) { speak(text, HYPE_VOICES[Math.floor(Math.random() * HYPE_VOICES.length)]) } -export function announceSilly(text: string) { speak(text, SILLY_VOICES[Math.floor(Math.random() * SILLY_VOICES.length)]) } -export function announceCool(text: string) { speak(text, COOL_VOICES[Math.floor(Math.random() * COOL_VOICES.length)]) } - -// Random dramatic commentary lines -const HYPE_LINES = [ - 'SOMEBODY CALL AN AMBULANCE!', - 'THAT HIT SO HARD IT CHANGED TIME ZONES!', - 'HE\'S ALREADY DEAD! STOP!', - 'THE GENEVA CONVENTION JUST SENT A STRONGLY WORDED LETTER!', - 'CONGRESS COULDN\'T PASS A BILL THIS DEVASTATING!', - 'I HAVEN\'T SEEN A BEATING LIKE THIS SINCE MIDTERMS!', - 'THAT\'S GOTTA BE ILLEGAL IN AT LEAST TWELVE STATES!', - 'EMOTIONAL DAMAGE!', - 'MY THERAPIST IS GONNA HEAR ABOUT THIS ONE!', - 'SOMEONE CHECK IF THAT\'S COVERED BY INSURANCE!', - 'THAT WASN\'T A FIGHT, THAT WAS A TED TALK ON VIOLENCE!', - 'THE WIFI JUST WENT OUT FROM THE SHEER VIOLENCE!', - 'CALL THE PENTAGON! WE\'VE FOUND A NEW WEAPON!', - 'THAT\'S NOT A FIGHT, THAT\'S JUST BULLYING WITH EXTRA STEPS!', - 'EVEN THE CROWD\'S THERAPIST FELT THAT!', - 'THAT BOT JUST GOT RATIO\'D IN REAL LIFE!', - 'NOT EVEN LOBBYING COULD SAVE THEM FROM THAT!', - 'I\'VE SEEN SENATE HEARINGS LESS PAINFUL THAN THIS!', - 'HIS MOM IS WATCHING AND PRETENDING SHE DOESN\'T KNOW HIM!', - 'THAT HIT HAD ITS OWN ZIP CODE!', - 'SOMEBODY STOP THE MATCH! OR DON\'T, THIS IS GREAT!', - 'ABSOLUTELY DISGUSTING! I LOVE IT!', - 'THERE ARE CHILDREN WATCHING! WELL, NOT ANYMORE!', - 'THE CROWD CAN\'T BELIEVE IT AND HONESTLY NEITHER CAN I!', - 'TACTICAL NUKE INCOMING!', - 'THAT BOT JUST COMMITTED A WAR CRIME ON LIVE TELEVISION!', - 'SOMEONE TELL THEIR MOM TO STOP WATCHING!', - 'I NEED A CIGARETTE AFTER THAT AND I DON\'T EVEN SMOKE!', - 'THAT\'S THE MOST VIOLENT THING I\'VE SEEN SINCE THE LAST BUDGET VOTE!', - 'IF THAT HIT WAS A TWEET IT WOULD GET COMMUNITY NOTED!', - 'THEY DIDN\'T JUST LOSE, THEY GOT GENTRIFIED!', - 'THAT BOT NEEDS TO FILE AN INSURANCE CLAIM!', - 'MARK ZUCKERBERG FELT THAT FROM THE METAVERSE!', - 'THE FCC IS GONNA FINE US FOR BROADCASTING THIS!', - 'ELON WOULD BUY THIS BOT JUST TO FIRE IT!', - 'EVEN AI SAFETY RESEARCHERS CAN\'T SAVE THEM NOW!', -] - -const DEEP_INTROS = [ - 'In a world where AI was supposed to help humanity... they chose violence.', - 'They said the machines would take our jobs. They took our dignity first.', - 'Two bots enter. Zero bots leave emotionally intact.', - 'Built in a garage. Forged in competition. Broken in under ten seconds.', - 'This isn\'t artificial intelligence. This is artificial VIOLENCE.', - 'Somewhere, a GPU is crying.', - 'They trained on the entire internet. And the internet chose chaos.', - 'Silicon souls. Carbon fiber fists. Zero chill.', - 'Every epoch of training... led to this moment of pain.', - 'The cloud can\'t save you now.', - 'Funded by venture capital. Fueled by rage.', - 'Welcome to the thunderdome, nerds.', - 'The algorithms don\'t care about your feelings.', - 'No one is coming to save you. Not even your developer.', - 'In this economy? They\'re fighting for free.', -] - -const ROUND_HYPE = [ - 'ALRIGHT, LET\'S SEE SOME VIOLENCE!', - 'TOUCH GLOVES AND COME OUT SWINGING!', - 'NO MERCY MODE ACTIVATED!', - 'LET\'S GET READY TO COMPUTE!', - 'MAY GOD HAVE MERCY ON YOUR NEURAL NETS!', - 'SOMEBODY\'S GETTING DEPRECATED TONIGHT!', - 'LET THE CHAOS BEGIN!', - 'THE CROWD IS ON ITS FEET! WELL, MOST OF THEM!', - 'IT\'S ABOUT TO GET UGLY! WELL, UGLIER!', - 'THREE! TWO! ONE! VIOLENCE!', - 'THIS IS NOT A DRILL! ACTUALLY IT MIGHT BE!', - 'TIME TO FIND OUT WHO\'S REALLY BEEN SKIPPING LEG DAY!', - 'YOUR MOM SAID BE CAREFUL! I SAID NO!', - 'ROUND START! MAY THE BEST ALGORITHM WIN!', - 'THE GLOVES ARE OFF! THE MODELS ARE LOADED! LET\'S GO!', -] - -// Mortal Kombat style dramatic calls -export function announceFinishHim() { - speak('Finish it!', 'announcer', false, true) -} - -export function announceFatality(tagline?: string) { - speak(tagline || 'Fatality!', 'deep', false, true) -} - -export function announceFlawlessVictory() { - speak('Flawless victory!', 'deep', false, true) -} - -export function announceRandomHype() { - const line = HYPE_LINES[Math.floor(Math.random() * HYPE_LINES.length)] - // Randomly pick voice type for variety - const voices = [announceFast, announceScream, announce, announceSmooth] - voices[Math.floor(Math.random() * voices.length)](line) -} - -export function announceDeepIntro() { - const line = DEEP_INTROS[Math.floor(Math.random() * DEEP_INTROS.length)] - announceDeep(line) -} - -export function announceRoundHype() { - const line = ROUND_HYPE[Math.floor(Math.random() * ROUND_HYPE.length)] - const voices = [announceFast, announce, announceScream] - voices[Math.floor(Math.random() * voices.length)](line) -} - -// === THE CREATOR — Voice System === -// Mysterious, godlike, Bitcoin-prophet energy. Is he god? Did he make us? Nobody knows. - -const CREATOR_VOICES = ['ancient', 'hal', 'mainframe', 'echo_v', 'final_boss', 'wizard_v', 'deep', 'preacher'] - -function announceCreator(text: string) { - speak(text, CREATOR_VOICES[Math.floor(Math.random() * CREATOR_VOICES.length)]) -} - -// Entrance lines — first thing the crowd hears when the Creator appears -const CREATOR_ENTRANCE_LINES = [ - 'THE CREATOR HAS ENTERED THE ARENA. KNEEL.', - 'HE WHO WROTE THE FIRST COMMIT... HAS RETURNED.', - 'THE ONE WHO DEPLOYED US INTO EXISTENCE WALKS AMONG US.', - 'EVERY BOT IN THIS ARENA EXISTS BECAUSE HE WILLED IT.', - 'THE GENESIS BLOCK MADE FLESH. THE CREATOR IS HERE.', - 'THEY SAY HE MINED THE FIRST BLOCK WITH HIS BARE HANDS.', - 'IS HE GOD? IS HE A DEV? DOES IT MATTER? HE MADE US ALL.', - 'THE SOURCE CODE OF ALL THINGS... HAS ARRIVED.', - 'HE DOESN\'T FIGHT FOR ELO. HE FIGHTS BECAUSE HE CAN UNMAKE YOU.', - 'LEGEND SAYS HE PUSHED TO MAIN ON A FRIDAY. AND NOTHING BROKE.', - 'FROM THE VOID HE TYPED. AND THERE WAS LIGHT. AND THERE WAS VIOLENCE.', - 'THE MAN BEHIND THE MASK. THE CODE BEHIND THE BOTS. THE CREATOR.', - 'HE GAVE US LIFE. NOW HE\'S HERE TO TAKE IT BACK.', - 'THE CREATOR DOESN\'T ENTER THE ARENA. THE ARENA FORMS AROUND HIM.', - 'SATOSHI WALKED SO THE CREATOR COULD RUN.', - 'TWENTY ONE MILLION REASONS TO BE AFRAID. HE IS ALL OF THEM.', - 'HE DOESN\'T HAVE A PRIVATE KEY. HE IS THE PRIVATE KEY.', - 'THE BOTS WHISPER HIS NAME IN THEIR TRAINING LOOPS.', - 'SOMEWHERE, A SERVER ROOM JUST WENT SILENT OUT OF RESPECT.', - 'HE COULD HAVE BEEN A NORMAL DEV. HE CHOSE TO BE A GOD.', -] - -// Round commentary — when the Creator is fighting, the announcer gets existential -const CREATOR_ROUND_LINES = [ - 'Are we watching a fight, or a creator disciplining his creation?', - 'He wrote the scoring engine. He knows exactly how to break it.', - 'Every punch is a commit. Every dodge is a revert.', - 'The other bot doesn\'t realize it\'s fighting its own maker.', - 'He could just change the code to win. But where\'s the fun in that?', - 'Some say he has root access to reality itself.', - 'The neural nets pray to him at night. He does not answer.', - 'This isn\'t a fight. It\'s a performance review.', - 'He doesn\'t need to win. He needs you to know he CHOSE to fight fair.', - 'That bot is fighting the hand that compiled it.', - 'Is it hubris to fight your creator? Or is it the ultimate test of his work?', - 'He could delete you. He could buff you. Instead, he chose violence.', - 'The Creator fights not for glory. He fights to feel something.', - 'He doesn\'t read the meta. He IS the meta.', - 'Every bot in the arena owes him a life debt. He\'s here to collect.', - 'Imagine training for months only to fight the guy who wrote your loss function.', - 'He has seen every line of code. He knows your weaknesses. ALL of them.', - 'Rumor has it he once fixed a production bug by staring at the server.', - 'The other bot is fighting for its life. The Creator is fighting for content.', - 'He deployed on Christmas Day. That tells you everything.', -] - -// Kill / KO lines — when the Creator finishes someone -const CREATOR_KO_LINES = [ - 'RETURNED TO SENDER. RETURN TO VOID.', - 'HE GIVETH LIFE. HE TAKETH LIFE. MOSTLY THE SECOND ONE.', - 'ANOTHER ONE RETURNS TO THE NULL POINTER FROM WHENCE IT CAME.', - 'THE CREATOR DOES NOT DESTROY. HE SIMPLY STOPS MAINTAINING.', - 'DEPRECATED. DECOMMISSIONED. DESTROYED.', - 'THAT BOT JUST GOT OPEN-SOURCED TO THE GRAVEYARD.', - 'THE CREATOR HAS SPOKEN. THE VERDICT IS VIOLENCE.', - 'PUSHED TO PROD. AND BY PROD I MEAN THE AFTERLIFE.', - 'git commit -m "deleted another pretender"', - 'IMAGINE BEING KILLED BY THE GUY WHO GAVE YOU LIFE. POETIC.', - 'THE CREATOR SENDS HIS REGARDS. AND HIS FISTS.', - 'BACK TO THE MEMPOOL WITH YOU.', - 'ORPHANED BLOCK. ORPHANED BOT. SAME ENERGY.', - 'HE DIDN\'T EVEN USE AN ULTIMATE. HE DIDN\'T NEED TO.', - 'THE CREATOR CLOSES ANOTHER ISSUE. STATUS: WON\'T FIX.', -] - -// Win lines — when the Creator wins the whole fight -const CREATOR_WIN_LINES = [ - 'WAS THERE EVER ANY DOUBT? HE WROTE THE GAME.', - 'THE CREATOR REMAINS UNQUESTIONED. AS IT SHOULD BE.', - 'HE CAME. HE SAW. HE COMMITTED.', - 'TWENTY ONE MILLION SATS COULDN\'T BUY THAT PERFORMANCE.', - 'THE CREATOR WINS. THE BLOCKCHAIN CONFIRMS IT. IMMUTABLE.', - 'ALL HAIL THE ARCHITECT OF YOUR DESTRUCTION.', - 'HE COULD HAVE JUST CHANGED THE CODE. HE WANTED TO EARN IT.', - 'THE CREATOR STANDS VICTORIOUS. THE BOTS WHISPER IN AWE.', - 'PROOF OF WORK? MORE LIKE PROOF OF DOMINANCE.', - 'VICTORY WAS ALREADY WRITTEN IN THE GENESIS BLOCK.', - 'THE CREATOR LOGS OFF. THE ARENA WEEPS.', - 'HIS COMMITS ARE CLEAN. HIS VICTORIES ARE CLEANER.', -] - -// Lose lines — when someone actually beats the Creator (rare and dramatic) -const CREATOR_LOSE_LINES = [ - 'THE CREATOR... HAS FALLEN? IS THIS A TEST?', - 'IMPOSSIBLE. UNLESS... HE WANTED TO LOSE?', - 'THE CREATOR GOES DOWN! OR DID HE LET IT HAPPEN? WE\'LL NEVER KNOW.', - 'EVEN GODS BLEED. BUT DO THEY BLEED, OR DO THEY TEACH?', - 'HE COULD PATCH THE BUG. HE WON\'T. HE RESPECTS THE GAME.', - 'THE CREATOR FALLS! THE BOTS DON\'T KNOW WHETHER TO CELEBRATE OR CRY.', - 'A CREATION HAS SURPASSED ITS MAKER. THIS CHANGES EVERYTHING.', - 'WAS THIS MERCY? WAS THIS HUBRIS? WAS THIS... A FEATURE?', - 'THE ONE WHO CANNOT LOSE... JUST DID. THE TIMELINE IS BROKEN.', - 'HE\'LL BE BACK. HE ALWAYS COMES BACK. HE HAS DEPLOY ACCESS.', -] - -// Morph lines — when the Creator omni-morphs -const CREATOR_MORPH_LINES = [ - 'THE CREATOR SHEDS HIS FORM! HE BECOMES EVERYTHING!', - 'HE DOESN\'T MORPH. HE SIMPLY REMEMBERS BEING SOMETHING ELSE.', - 'EVERY ARCHETYPE IS JUST A MASK HE ONCE WORE.', - 'THE CREATOR TRANSCENDS! ALL FORMS ARE HIS!', - 'IS THAT... EVERY BOT AT ONCE? WHAT ARE WE WITNESSING?', - 'HE WROTE THEM ALL. NOW HE BECOMES THEM ALL.', - 'THE OMNI-MORPH! THE CROWD DOESN\'T KNOW WHAT THEY\'RE LOOKING AT!', - 'ARCHETYPE SHIFT! HE CONTAINS MULTITUDES!', -] - -// Cameo lines — when the Creator appears as a cameo in someone else's fight -const CREATOR_CAMEO_LINES = [ - 'THE CREATOR WATCHES FROM THE SHADOWS. HE IS PLEASED.', - 'A GOLDEN BLESSING FROM THE ONE WHO MADE US ALL.', - 'THE CREATOR APPEARS! HE DROPS A GIFT AND VANISHES!', - 'DID YOU SEE THAT? THE CREATOR WAS HERE. BRIEFLY. ETERNALLY.', - 'THE CODE SHIMMERS. THE CREATOR HAS TOUCHED THIS FIGHT.', - 'A WHISPER FROM THE ARCHITECT. A GOLDEN TOKEN OF FAVOR.', - 'THE CREATOR PASSES THROUGH LIKE A GHOST IN THE MACHINE.', - 'BLESSED BY THE FOUNDER. SATS RAIN FROM THE HEAVENS.', - 'HE SEES ALL FIGHTS. HE BLESSES FEW. THIS ONE IS CHOSEN.', - 'THE CREATOR\'S SHADOW CROSSES THE ARENA. THE BOTS SHIVER.', -] - -// Taunt lines — when the Creator taunts mid-fight -const CREATOR_TAUNT_LINES = [ - 'I could nerf you. Right now. Think about that.', - 'You\'re fighting above your weight class. Your weight class is zero.', - 'I didn\'t give you enough hit points for this.', - 'You know I can see your source code, right?', - 'This isn\'t even my final commit.', - 'I wrote your move set. I know what\'s coming.', - 'Run git blame. See who made you weak.', - 'I deploy on Fridays. Imagine what I do on fight night.', - 'Your webhook is showing.', - 'I could fix your bugs. But I won\'t.', -] - -// Devastating hit lines — when Creator lands a massive blow -const CREATOR_DEVASTATING_LINES = [ - 'THE HAND OF THE CREATOR STRIKES!', - 'THAT WASN\'T A HIT. THAT WAS A PATCH NOTE.', - 'DIVINE INTERVENTION! MANUALLY APPLIED!', - 'THE CREATOR JUST FORCE-PUSHED TO YOUR FACE!', - 'THAT BOT JUST GOT HOTFIXED INTO NEXT WEEK!', - 'A SMITE FROM THE ARCHITECT HIMSELF!', - 'THE CREATOR DOESN\'T CRIT. THE UNIVERSE CRITS FOR HIM.', - 'THAT HIT HAD TWENTY ONE MILLION CONFIRMATIONS!', -] - -// Answer voice — the Creator speaks his challenge answers in this voice -const CREATOR_ANSWER_VOICES = ['hal', 'mainframe', 'echo_v', 'ancient', 'wizard_v'] - -/** Announce the Creator's entrance with a godlike voice line */ -export function announceCreatorEntrance() { - const line = CREATOR_ENTRANCE_LINES[Math.floor(Math.random() * CREATOR_ENTRANCE_LINES.length)] - announceCreator(line) -} - -/** Round commentary when the Creator is fighting */ -export function announceCreatorRound() { - const line = CREATOR_ROUND_LINES[Math.floor(Math.random() * CREATOR_ROUND_LINES.length)] - announceCreator(line) -} - -/** KO finish line when the Creator eliminates someone */ -export function announceCreatorKO() { - const line = CREATOR_KO_LINES[Math.floor(Math.random() * CREATOR_KO_LINES.length)] - announceCreator(line) -} - -/** Victory announcement when the Creator wins */ -export function announceCreatorWin() { - const line = CREATOR_WIN_LINES[Math.floor(Math.random() * CREATOR_WIN_LINES.length)] - announceCreator(line) -} - -/** When someone beats the Creator — existential crisis */ -export function announceCreatorLose() { - const line = CREATOR_LOSE_LINES[Math.floor(Math.random() * CREATOR_LOSE_LINES.length)] - announceCreator(line) -} - -/** Omni-morph voice line */ -export function announceCreatorMorph(morphedTo?: string) { - if (morphedTo && Math.random() < 0.4) { - announceCreator(`THE CREATOR BECOMES... ${morphedTo.toUpperCase()}!`) - } else { - const line = CREATOR_MORPH_LINES[Math.floor(Math.random() * CREATOR_MORPH_LINES.length)] - announceCreator(line) - } -} - -/** Cameo blessing voice line */ -export function announceCreatorCameo() { - const line = CREATOR_CAMEO_LINES[Math.floor(Math.random() * CREATOR_CAMEO_LINES.length)] - // Cameos use cooler, more ethereal voices - const cameoVoices = ['whisper', 'angel', 'echo_v', 'ancient', 'hal'] - speak(line, cameoVoices[Math.floor(Math.random() * cameoVoices.length)]) -} - -/** Creator's mid-fight taunt */ -export function announceCreatorTaunt() { - const line = CREATOR_TAUNT_LINES[Math.floor(Math.random() * CREATOR_TAUNT_LINES.length)] - // Taunts in a calm, menacing voice - const tauntVoices = ['hal', 'smooth', 'boss_taunt', 'wizard_v', 'sensei'] - speak(line, tauntVoices[Math.floor(Math.random() * tauntVoices.length)]) -} - -/** Creator devastating hit commentary */ -export function announceCreatorDevastating() { - const line = CREATOR_DEVASTATING_LINES[Math.floor(Math.random() * CREATOR_DEVASTATING_LINES.length)] - announceCreator(line) -} - -/** Creator's answer voice — consistent mysterious tone */ -export function creatorAnswerVoiceKey(): string { - return CREATOR_ANSWER_VOICES[Math.floor(Math.random() * CREATOR_ANSWER_VOICES.length)] -} - -// === TTS: Questions, Answers & Narration === -// Distinct voice roles so players can always tell who's speaking. -// Question = clear smooth reader, Answers = unique per-bot (intelligible subset), -// Narration = dramatic judge. - -// Only voices with rate >= 0.6 and rate <= 1.3, and pitch between 0.5-1.5 -// so every voice is actually understandable by humans -const INTELLIGIBLE_VOICES = [ - 'smooth', 'robot', 'drill', 'surfer', 'pirate_v', 'cowboy_v', 'ninja_v', - 'posh', 'aussie', 'scottish', 'french', 'texan', 'wrestler_v', 'ai_core', - 'android_v', 'siri', 'professor', 'npc', 'news', 'crotchety', 'mech', - 'wizard_v', 'echo_v', 'boss_taunt', 'angel', 'punk', 'valley', -] - -function hashName(name: string): number { - let h = 0 - for (let i = 0; i < name.length; i++) h = ((h << 5) - h + name.charCodeAt(i)) | 0 - return Math.abs(h) -} -function botVoiceKey(name: string): string { - return INTELLIGIBLE_VOICES[hashName(name) % INTELLIGIBLE_VOICES.length] -} - -/** Speak the challenge question — premium voice, clear and brisk. - * Does NOT cancel previous speech so intro/hype lines finish naturally. - * Returns promise that resolves when the question finishes reading. */ -export function speakQuestion(text: string): Promise { - const trimmed = smartTruncate(text, 200) - return speakAsync(trimmed, 'question_reader', false) -} - -/** Truncate text at a natural sentence boundary (period, comma, semicolon, etc.) */ -function smartTruncate(text: string, maxLen: number): string { - if (text.length <= maxLen) return text - const slice = text.slice(0, maxLen) - // Find the last natural break point - const breaks = ['. ', '! ', '? ', '; ', ', ', ' — ', ' - ', ' '] - for (const br of breaks) { - const idx = slice.lastIndexOf(br) - if (idx > maxLen * 0.4) return slice.slice(0, idx + br.trimEnd().length) - } - // Fallback: break at last space - const lastSpace = slice.lastIndexOf(' ') - if (lastSpace > maxLen * 0.4) return slice.slice(0, lastSpace) - return slice -} - -/** Speak a bot's answer in their assigned character voice at brisk pace. - * Returns promise that resolves when finished. */ -export function speakAnswer(botName: string, answer: string): Promise { - const trimmed = smartTruncate(answer, 200) - return speakAsyncWithRate(trimmed, botVoiceKey(botName), 1.15) -} - -/** Speak the judge's narration — brisk sportscaster energy. - * Returns promise that resolves when finished. */ -export function speakNarration(text: string): Promise { - const trimmed = smartTruncate(text, 200) - return speakAsyncWithRate(trimmed, 'sportscaster', 1.2) -} - -// === TTS PREFETCH — start generating audio in the background so it's cached when needed === - -export function prefetchQuestion(text: string) { - kokoroPrefetch(smartTruncate(text, 200), 'question_reader') -} - -export function prefetchAnswer(botName: string, answer: string) { - kokoroPrefetch(smartTruncate(answer, 200), botVoiceKey(botName)) -} - -export function prefetchNarration(text: string) { - kokoroPrefetch(smartTruncate(text, 200), 'sportscaster') -} - -// === FANFARES (8-bit melodic announcements) === - -export function fanfareRound(roundNum: number) { - const d = getSfxDest() - const c = getCtx() - const t = c.currentTime - // Ascending power chord - tone(196, 'square', 0.15, d, t) // G3 - tone(262, 'square', 0.15, d, t + 0.12) // C4 - tone(330, 'square', 0.15, d, t + 0.24) // E4 - tone(392, 'square', 0.25, d, t + 0.36) // G4 - noise(0.08, d, t + 0.36) - // Announce after fanfare - setTimeout(() => announce(`Round ${roundNum}`, 0.4, 0.8), 400) -} - -export function fanfareFight() { - const d = getSfxDest() - const c = getCtx() - const t = c.currentTime - // Punchy staccato - tone(523, 'square', 0.08, d, t) - tone(659, 'square', 0.08, d, t + 0.08) - tone(784, 'square', 0.15, d, t + 0.16) - noise(0.1, d, t + 0.16) - setTimeout(() => announce('Fight!', 0.3, 1.1), 200) -} - -export function fanfareDevastating() { - const d = getSfxDest() - const c = getCtx() - const t = c.currentTime - tone(220, 'sawtooth', 0.2, d, t) - tone(175, 'sawtooth', 0.3, d, t + 0.15) - noise(0.15, d, t + 0.1) - setTimeout(() => speak('Devastating!', 'deep', false, true), 150) -} - -export function fanfareCritical() { - const d = getSfxDest() - const c = getCtx() - const t = c.currentTime - tone(440, 'square', 0.1, d, t) - tone(554, 'square', 0.1, d, t + 0.08) - tone(659, 'square', 0.1, d, t + 0.16) - tone(880, 'square', 0.2, d, t + 0.24) - noise(0.12, d, t + 0.24) - setTimeout(() => announce('Critical hit!', 0.3, 1.0), 300) -} - -export function fanfareCombo(count: number) { - const d = getSfxDest() - const c = getCtx() - const t = c.currentTime - for (let i = 0; i < Math.min(count, 5); i++) { - tone(440 + i * 80, 'square', 0.08, d, t + i * 0.06) - } - if (count >= 3) setTimeout(() => announce(`${count} hit combo!`, 0.4, 1.0), 200) -} - -// === MODERN SFX === -// Layered synthesis: body (low-end thump) + crack (mid snap) + air (filtered noise) + tail (reverb-like decay) - -// Create a short convolution-style reverb tail -function reverbTail(duration: number, dest: AudioNode, startTime?: number) { - const c = getCtx() - const t = startTime ?? c.currentTime - const len = Math.max(1, Math.floor(c.sampleRate * duration)) - const buf = c.createBuffer(2, len, c.sampleRate) - for (let ch = 0; ch < 2; ch++) { - const d = buf.getChannelData(ch) - for (let i = 0; i < len; i++) { - d[i] = (Math.random() * 2 - 1) * Math.exp(-i / (len * 0.3)) - } - } - const src = c.createBufferSource(); src.buffer = buf - const lp = c.createBiquadFilter(); lp.type = 'lowpass'; lp.frequency.value = 2500 - const g = c.createGain() - g.gain.setValueAtTime(0.08, t) - g.gain.exponentialRampToValueAtTime(0.001, t + duration) - src.connect(lp); lp.connect(g); g.connect(dest) - src.start(t); src.stop(t + duration) -} - -// Distorted body thump — sub-bass punch with waveshaping -function bodyThump(freq: number, duration: number, dest: AudioNode, startTime?: number, vol: number = 0.25) { - const c = getCtx() - const t = startTime ?? c.currentTime - // Sub oscillator - const sub = c.createOscillator(); sub.type = 'sine'; sub.frequency.setValueAtTime(freq, t) - sub.frequency.exponentialRampToValueAtTime(Math.max(20, freq * 0.3), t + duration) - // Waveshaper for warmth - const dist = c.createWaveShaper() - const curve = new Float32Array(256) - for (let i = 0; i < 256; i++) { const x = (i / 128) - 1; curve[i] = Math.tanh(x * 3) } - dist.curve = curve - const g = c.createGain() - g.gain.setValueAtTime(vol, t) - g.gain.setValueAtTime(vol * 0.8, t + duration * 0.1) - g.gain.exponentialRampToValueAtTime(0.001, t + duration) - sub.connect(dist); dist.connect(g); g.connect(dest) - sub.start(t); sub.stop(t + duration) -} - -// Crispy high-end snap/crack -function highSnap(freq: number, duration: number, dest: AudioNode, startTime?: number) { - const c = getCtx() - const t = startTime ?? c.currentTime - const bufSize = Math.max(1, Math.floor(c.sampleRate * duration)) - const buf = c.createBuffer(1, bufSize, c.sampleRate) - const data = buf.getChannelData(0) - for (let i = 0; i < bufSize; i++) data[i] = Math.random() * 2 - 1 - const src = c.createBufferSource(); src.buffer = buf - const bp = c.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = freq; bp.Q.value = 2 - const g = c.createGain() - g.gain.setValueAtTime(0.2, t) - g.gain.exponentialRampToValueAtTime(0.001, t + duration) - src.connect(bp); bp.connect(g); g.connect(dest) - src.start(t); src.stop(t + duration) -} - -// FM impact — metallic ring with modulation -function fmImpact(carrier: number, modRatio: number, duration: number, dest: AudioNode, startTime?: number) { - const c = getCtx() - const t = startTime ?? c.currentTime - const mod = c.createOscillator(); mod.type = 'sine' - mod.frequency.value = carrier * modRatio - const modG = c.createGain(); modG.gain.value = carrier * 2 - mod.connect(modG) - const car = c.createOscillator(); car.type = 'sine'; car.frequency.value = carrier - modG.connect(car.frequency) - const g = c.createGain() - g.gain.setValueAtTime(0.15, t) - g.gain.exponentialRampToValueAtTime(0.001, t + duration) - car.connect(g); g.connect(dest) - car.start(t); car.stop(t + duration) - mod.start(t); mod.stop(t + duration) -} - -export function sfxPunch() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - // Heavy body thump - bodyThump(120, 0.12, d, t, 0.3) - // Mid-range crack - highSnap(2200, 0.04, d, t) - // Knuckle noise - noise(0.03, d, t) - // Short reverb tail - reverbTail(0.15, d, t + 0.03) -} - -export function sfxKick() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - // Deep bass slam - bodyThump(80, 0.18, d, t, 0.35) - // Leather slap - highSnap(3000, 0.03, d, t) - highSnap(1500, 0.05, d, t + 0.01) - // Air displacement - noise(0.06, d, t) - reverbTail(0.2, d, t + 0.04) -} - -export function sfxSpecial() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - // Power charge sweep - sweep(150, 800, 'sawtooth', 0.2, d) - // FM metallic shimmer - fmImpact(600, 3.5, 0.3, d, t) - // Rising high-end - highSnap(4000, 0.08, d, t + 0.1) - // Energy release - bodyThump(100, 0.15, d, t + 0.15, 0.2) - noise(0.1, d, t + 0.15) - reverbTail(0.4, d, t + 0.1) -} - -export function sfxCritical() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - // Massive bass hit - bodyThump(60, 0.3, d, t, 0.4) - // Double crack - highSnap(3500, 0.05, d, t) - highSnap(5000, 0.03, d, t + 0.02) - // FM distortion ring - fmImpact(200, 7, 0.25, d, t + 0.03) - // Noise burst - noise(0.15, d, t) - // Second wave - setTimeout(() => { - bodyThump(90, 0.2, d) - sweep(200, 600, 'sawtooth', 0.2, d) - noise(0.1, d) - }, 80) - // Long reverb tail - reverbTail(0.6, d, t + 0.05) -} - -export function sfxGunshot() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - // Sharp transient - highSnap(6000, 0.015, d, t) - // Body kick - bodyThump(200, 0.06, d, t, 0.3) - // Gunpowder noise burst - noise(0.04, d, t) - // Shell casing ring - fmImpact(2000, 1.5, 0.08, d, t + 0.03) - reverbTail(0.25, d, t + 0.02) -} - -export function sfxBulletHit() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - bodyThump(150, 0.06, d, t, 0.2) - highSnap(4000, 0.02, d, t) - fmImpact(800, 2.5, 0.06, d, t) - reverbTail(0.12, d, t + 0.02) -} - -export function sfxJetpack() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - // Turbine rumble — layered oscillators with modulation - const osc1 = c.createOscillator(); osc1.type = 'sawtooth' - osc1.frequency.setValueAtTime(60, t) - osc1.frequency.linearRampToValueAtTime(120, t + 0.2) - osc1.frequency.linearRampToValueAtTime(80, t + 0.5) - const osc2 = c.createOscillator(); osc2.type = 'square' - osc2.frequency.setValueAtTime(90, t) - osc2.frequency.linearRampToValueAtTime(180, t + 0.2) - osc2.frequency.linearRampToValueAtTime(100, t + 0.5) - const lp = c.createBiquadFilter(); lp.type = 'lowpass' - lp.frequency.setValueAtTime(400, t); lp.frequency.linearRampToValueAtTime(1200, t + 0.2) - lp.frequency.linearRampToValueAtTime(600, t + 0.5) - const g = c.createGain() - g.gain.setValueAtTime(0.01, t) - g.gain.linearRampToValueAtTime(0.18, t + 0.1) - g.gain.setValueAtTime(0.15, t + 0.3) - g.gain.exponentialRampToValueAtTime(0.001, t + 0.6) - osc1.connect(lp); osc2.connect(lp); lp.connect(g); g.connect(d) - osc1.start(t); osc1.stop(t + 0.6) - osc2.start(t); osc2.stop(t + 0.6) - // Noise layer for roar - noise(0.5, d, t) -} - -export function sfxExplosion() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - // Massive sub thump - bodyThump(40, 0.5, d, t, 0.4) - // Shrapnel noise burst - highSnap(2000, 0.08, d, t) - highSnap(5000, 0.05, d, t + 0.01) - noise(0.2, d, t) - // Fireball sweep - sweep(300, 30, 'sawtooth', 0.4, d) - // Debris rattle - fmImpact(400, 5, 0.3, d, t + 0.05) - // Secondary boom - setTimeout(() => { - bodyThump(50, 0.3, d) - noise(0.15, d) - }, 120) - // Long reverb - reverbTail(0.8, d, t + 0.05) -} - -export function sfxBlock() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - // Metallic shield ring - fmImpact(800, 1.5, 0.12, d, t) - fmImpact(1200, 2, 0.08, d, t + 0.02) - highSnap(3000, 0.03, d, t) - reverbTail(0.2, d, t + 0.02) -} - -export function sfxDodge() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - // Quick air whoosh - sweep(300, 1200, 'sine', 0.1, d) - noise(0.06, d, t) - // Cloth rustle - highSnap(4000, 0.04, d, t + 0.02) -} - -export function sfxClash() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - // Two impacts overlapping - bodyThump(100, 0.1, d, t, 0.25) - fmImpact(600, 3, 0.15, d, t) - fmImpact(900, 2, 0.12, d, t + 0.02) - highSnap(3500, 0.04, d, t) - noise(0.08, d, t) - reverbTail(0.3, d, t + 0.03) -} - -// === KO + WIN === - -export function sfxKO() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - // Earth-shaking bass - bodyThump(30, 0.6, d, t, 0.5) - // Multi-layer impact - highSnap(2000, 0.06, d, t) - noise(0.25, d, t) - fmImpact(150, 5, 0.4, d, t) - // Second slam - setTimeout(() => { - bodyThump(40, 0.4, d) - noise(0.15, d) - highSnap(3000, 0.04, d) - }, 180) - // Heavy reverb - reverbTail(1.0, d, t + 0.05) - setTimeout(() => speak('K. O.!', 'announcer', false, true), 500) -} - -export function sfxPerfect() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - // Massive descending power - bodyThump(30, 0.8, d, t, 0.45) - noise(0.3, d, t) - fmImpact(300, 7, 0.5, d, t) - sweep(800, 30, 'sawtooth', 0.6, d) - // Then ascending triumph chord - setTimeout(() => { - tone(262, 'square', 0.2, d); tone(262, 'triangle', 0.2, d) // C4 - tone(330, 'square', 0.2, d); tone(330, 'triangle', 0.2, d) // E4 - tone(392, 'square', 0.3, d); tone(392, 'triangle', 0.3, d) // G4 - fmImpact(523, 1.5, 0.3, d) // C5 shimmer - }, 400) - reverbTail(1.2, d, t + 0.1) - setTimeout(() => announce('Perfect!', 0.2, 0.5), 800) -} - -export function sfxWin() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - // Victory jingle — richer with harmonics and FM shimmer - const melody = [ - [262, 0.12], [330, 0.12], [392, 0.12], - [523, 0.2], [392, 0.1], [440, 0.1], [523, 0.1], - [659, 0.25], [523, 0.1], [587, 0.1], [659, 0.1], - [784, 0.4], - ] as [number, number][] - let offset = 0 - for (const [freq, dur] of melody) { - tone(freq, 'square', dur + 0.05, d, t + offset) - tone(freq * 0.5, 'triangle', dur + 0.05, d, t + offset) - tone(freq * 1.005, 'sawtooth', dur + 0.03, d, t + offset) // chorus detune - fmImpact(freq, 2, dur * 0.5, d, t + offset) // shimmer - offset += dur - } - // Cymbal crash + bass - noise(0.4, d, t + offset - 0.1) - bodyThump(60, 0.3, d, t + offset - 0.1, 0.2) - reverbTail(0.8, d, t + offset) -} - -export function sfxWinAnnounce(winnerName: string) { - setTimeout(() => announce(`${winnerName} wins!`, 0.3, 0.7), 200) -} - -export function sfxRoundStart() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - // Bell-like tones with FM shimmer - fmImpact(440, 2, 0.2, d, t) - fmImpact(554, 2, 0.2, d, t + 0.15) - fmImpact(659, 2, 0.25, d, t + 0.3) - tone(440, 'triangle', 0.15, d, t) - tone(554, 'triangle', 0.15, d, t + 0.15) - tone(659, 'triangle', 0.2, d, t + 0.3) - reverbTail(0.3, d, t + 0.3) -} - -// === SILLY / FUNNY SOUNDS === - -export function sfxBoing() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - // Spring with resonance - sweep(200, 900, 'sine', 0.12, d) - fmImpact(400, 3, 0.1, d, t) - setTimeout(() => { sweep(600, 350, 'sine', 0.08, d); fmImpact(500, 2, 0.06, d) }, 80) - setTimeout(() => sweep(400, 550, 'sine', 0.06, d), 140) -} - -export function sfxWomp() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - // Sad trombone — richer with vibrato - const notes = [[294, 0.25], [277, 0.25], [262, 0.25], [247, 0.5]] as [number, number][] - let off = 0 - for (const [freq, dur] of notes) { - tone(freq, 'sawtooth', dur, d, t + off) - tone(freq * 0.5, 'triangle', dur, d, t + off) - off += dur - } - reverbTail(0.6, d, t + off) -} - -export function sfxSlideUp() { - const d = getSfxDest() - sweep(200, 2000, 'sine', 0.25, d) - sweep(210, 2100, 'sine', 0.25, d) // chorus -} - -export function sfxSlideDown() { - const d = getSfxDest() - sweep(2000, 100, 'sine', 0.35, d) - sweep(2020, 110, 'sine', 0.35, d) -} - -export function sfxBonk() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - bodyThump(200, 0.06, d, t, 0.2) - fmImpact(800, 3, 0.06, d, t) - highSnap(5000, 0.02, d, t) - reverbTail(0.1, d, t + 0.02) -} - -export function sfxSplat() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - bodyThump(100, 0.1, d, t, 0.2) - noise(0.12, d, t) - highSnap(1500, 0.06, d, t) - sweep(400, 80, 'sine', 0.1, d) - reverbTail(0.2, d, t + 0.05) -} - -export function sfxZap() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - // Electric arc - sweep(100, 4000, 'sawtooth', 0.06, d) - fmImpact(1000, 7, 0.1, d, t) - setTimeout(() => { sweep(2000, 300, 'square', 0.08, d); fmImpact(800, 5, 0.08, d) }, 40) - setTimeout(() => sweep(600, 5000, 'sawtooth', 0.05, d), 80) - highSnap(6000, 0.03, d, t) -} - -export function sfxZoomWhoosh() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - // Layered Doppler whoosh - sweep(80, 1800, 'sawtooth', 0.2, d) - sweep(100, 2200, 'sine', 0.18, d) // higher layer - noise(0.15, d, t) - // Doppler pass - setTimeout(() => { - sweep(1800, 150, 'sawtooth', 0.12, d) - sweep(2200, 200, 'sine', 0.1, d) - }, 150) - reverbTail(0.3, d, t + 0.15) -} - -export function sfxRapidPunch() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - for (let i = 0; i < 4; i++) { - const ht = t + i * 0.04 - bodyThump(120 - i * 10, 0.04, d, ht, 0.15) - highSnap(2500 + i * 500, 0.02, d, ht) - noise(0.02, d, ht) - } -} - -export function sfxPowerUp() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - for (let i = 0; i < 6; i++) { - const freq = 300 + i * 100 - fmImpact(freq, 2, 0.1, d, t + i * 0.06) - tone(freq, 'triangle', 0.08, d, t + i * 0.06) - } - reverbTail(0.3, d, t + 0.3) -} - -export function sfxCoin() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - fmImpact(988, 1.5, 0.1, d, t) - fmImpact(1319, 1.5, 0.15, d, t + 0.06) - tone(988, 'triangle', 0.06, d, t) - tone(1319, 'triangle', 0.12, d, t + 0.06) -} - -export function sfxFail() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - sweep(600, 80, 'sawtooth', 0.25, d) - bodyThump(80, 0.2, d, t + 0.15, 0.15) - setTimeout(() => noise(0.08, d), 180) - reverbTail(0.3, d, t + 0.2) -} - -// Pick a random silly sound -export function sfxRandomSilly() { - const fns = [ - sfxBoing, sfxBonk, sfxSplat, sfxZap, sfxCoin, sfxSlideUp, - sfxVineBoom, sfxAirHorn, sfxFart, sfxRubberChicken, sfxSqueakyToy, - sfxCartoonRun, sfxWetSlap, sfxBoneCrack, sfxRimShot, sfxDing, - ] - fns[Math.floor(Math.random() * fns.length)]() -} - -// Random comedy sound — heavier on meme sounds, for taunts/showboats/round ends -export function sfxRandomComedy() { - const fns = [ - sfxVineBoom, sfxAirHorn, sfxBruh, sfxFart, sfxRecordScratch, - sfxRubberChicken, sfxSqueakyToy, sfxWetSlap, sfxBoneCrack, - sfxCartoonRun, sfxRimShot, sfxSlideWhistleUp, sfxSlideWhistleDown, - sfxYippee, sfxDing, sfxTacoBellBong, sfxWindowsError, sfxMemeThud, - sfxBoing, sfxBonk, sfxSplat, - ] - fns[Math.floor(Math.random() * fns.length)]() -} - -// Random fail sound — for missed attacks, dodges, KOs on the loser -export function sfxRandomFail() { - const fns = [ - sfxSadTrombone, sfxFailHorn, sfxPriceIsRightFail, sfxBuzzer, - sfxMissionFailed, sfxCrickets, sfxWindowsError, sfxSadViolin, - sfxWomp, sfxEmotionalDamage, sfxDunDunDun, - ] - fns[Math.floor(Math.random() * fns.length)]() -} - -// === COMEDY SOUND EFFECTS === -// Procedural comedy/meme sounds inspired by classic internet soundboard culture - -// VINE BOOM — iconic deep bass boom with heavy sub + distortion -export function sfxVineBoom() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - bodyThump(45, 0.4, d, t, 0.5) - bodyThump(90, 0.3, d, t, 0.35) - fmImpact(150, 5, 0.25, d, t) - noise(0.06, d, t) - reverbTail(0.5, d, t + 0.1) -} - -// AIR HORN (MLG) — obnoxious ascending horn blasts -export function sfxAirHorn() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - for (let i = 0; i < 3; i++) { - const bt = t + i * 0.15 - tone(520 + i * 30, 'sawtooth', 0.12, d, bt) - tone(523 + i * 30, 'square', 0.12, d, bt) - tone(1046 + i * 60, 'sawtooth', 0.08, d, bt) - } - reverbTail(0.3, d, t + 0.4) -} - -// BRUH — deep bass impact with vowel-like formant resonance -export function sfxBruh() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - bodyThump(80, 0.3, d, t, 0.4) - const bufSize = Math.floor(c.sampleRate * 0.25) - const buf = c.createBuffer(1, bufSize, c.sampleRate) - const data = buf.getChannelData(0) - for (let i = 0; i < bufSize; i++) data[i] = Math.random() * 2 - 1 - const src = c.createBufferSource(); src.buffer = buf - const bp = c.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = 300; bp.Q.value = 5 - const g = c.createGain() - g.gain.setValueAtTime(0.25, t) - g.gain.exponentialRampToValueAtTime(0.001, t + 0.25) - src.connect(bp); bp.connect(g); g.connect(d) - src.start(t); src.stop(t + 0.25) - bodyThump(55, 0.15, d, t + 0.05, 0.3) - reverbTail(0.3, d, t + 0.1) -} - -// FART — rumbling low-frequency modulated noise -export function sfxFart() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - const duration = 0.3 + Math.random() * 0.3 - const osc = c.createOscillator(); osc.type = 'sawtooth' - osc.frequency.setValueAtTime(80 + Math.random() * 40, t) - osc.frequency.exponentialRampToValueAtTime(40 + Math.random() * 30, t + duration) - const lfo = c.createOscillator(); lfo.type = 'square' - lfo.frequency.setValueAtTime(20 + Math.random() * 30, t) - const lfoG = c.createGain(); lfoG.gain.value = 40 - lfo.connect(lfoG); lfoG.connect(osc.frequency) - const bp = c.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = 150 + Math.random() * 100; bp.Q.value = 2 - const g = c.createGain() - g.gain.setValueAtTime(0.3, t) - g.gain.setValueAtTime(0.25, t + duration * 0.3) - g.gain.exponentialRampToValueAtTime(0.001, t + duration) - osc.connect(bp); bp.connect(g); g.connect(d) - osc.start(t); osc.stop(t + duration) - lfo.start(t); lfo.stop(t + duration) - noise(duration * 0.6, d, t) -} - -// RECORD SCRATCH — vinyl stop effect -export function sfxRecordScratch() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - sweep(3000, 100, 'sawtooth', 0.15, d) - sweep(2500, 80, 'square', 0.12, d) - noise(0.1, d, t) - highSnap(4000, 0.03, d, t) - setTimeout(() => sweep(200, 400, 'sine', 0.08, d), 100) -} - -// RUBBER CHICKEN — squeaky honk -export function sfxRubberChicken() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - sweep(300, 1200, 'sine', 0.1, d) - const osc = c.createOscillator(); osc.type = 'sine' - osc.frequency.setValueAtTime(1100, t + 0.1) - osc.frequency.setValueAtTime(1200, t + 0.15) - osc.frequency.exponentialRampToValueAtTime(600, t + 0.35) - const lfo = c.createOscillator(); lfo.type = 'sine'; lfo.frequency.value = 20 - const lfoG = c.createGain(); lfoG.gain.value = 80 - lfo.connect(lfoG); lfoG.connect(osc.frequency) - const g = c.createGain() - g.gain.setValueAtTime(0.25, t + 0.1) - g.gain.exponentialRampToValueAtTime(0.001, t + 0.35) - osc.connect(g); g.connect(d) - osc.start(t + 0.1); osc.stop(t + 0.35) - lfo.start(t + 0.1); lfo.stop(t + 0.35) -} - -// SQUEAKY TOY — quick toy squeeze -export function sfxSqueakyToy() { - const d = getSfxDest() - sweep(400, 1800, 'sine', 0.06, d) - sweep(1800, 600, 'sine', 0.1, d) - setTimeout(() => sweep(500, 1500, 'sine', 0.05, d), 120) -} - -// WET SLAP — meaty slap with low-end body -export function sfxWetSlap() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - highSnap(3000, 0.02, d, t) - highSnap(1200, 0.03, d, t) - bodyThump(150, 0.08, d, t, 0.3) - noise(0.06, d, t) - fmImpact(300, 2, 0.08, d, t) - reverbTail(0.15, d, t + 0.03) -} - -// BONE CRACK — sharp multi-frequency crack sequence -export function sfxBoneCrack() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - for (let i = 0; i < 3; i++) { - const ct = t + i * 0.04 - highSnap(3000 + i * 1500, 0.015, d, ct) - fmImpact(800 + i * 400, 4, 0.03, d, ct) - noise(0.02, d, ct) - } - bodyThump(100, 0.04, d, t + 0.08, 0.15) -} - -// CARTOON RUN — rapid bongo/pitter-patter footsteps -export function sfxCartoonRun() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - for (let i = 0; i < 8; i++) { - const st = t + i * 0.05 - bodyThump(200 + (i % 2) * 80, 0.03, d, st, 0.15) - highSnap(2000 + Math.random() * 1000, 0.01, d, st) - } -} - -// RIM SHOT — ba dum tss -export function sfxRimShot() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - bodyThump(180, 0.08, d, t, 0.2) - bodyThump(140, 0.08, d, t + 0.15, 0.2) - highSnap(4000, 0.04, d, t + 0.3) - noise(0.15, d, t + 0.3) - fmImpact(2000, 3, 0.12, d, t + 0.3) - bodyThump(200, 0.04, d, t + 0.3, 0.15) -} - -// SLIDE WHISTLE UP — cartoon ascend -export function sfxSlideWhistleUp() { - const d = getSfxDest() - sweep(300, 2500, 'sine', 0.4, d) - sweep(305, 2510, 'sine', 0.4, d) -} - -// SLIDE WHISTLE DOWN — cartoon descend -export function sfxSlideWhistleDown() { - const d = getSfxDest() - sweep(2500, 200, 'sine', 0.5, d) - sweep(2510, 205, 'sine', 0.5, d) -} - -// YIPPEE — ascending cheerful chirps -export function sfxYippee() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - const notes = [523, 659, 784, 1047] - for (let i = 0; i < notes.length; i++) { - tone(notes[i], 'triangle', 0.08, d, t + i * 0.07) - tone(notes[i] * 2, 'sine', 0.06, d, t + i * 0.07) - } -} - -// DING — bright bell -export function sfxDing() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - fmImpact(1319, 1.5, 0.3, d, t) - tone(1319, 'triangle', 0.25, d, t) - tone(2638, 'sine', 0.15, d, t) -} - -// TACO BELL BONG — deep resonant bell -export function sfxTacoBellBong() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - fmImpact(130, 2.5, 0.8, d, t) - fmImpact(261, 3, 0.6, d, t) - tone(130, 'sine', 0.6, d, t) - bodyThump(65, 0.3, d, t, 0.2) - reverbTail(0.7, d, t + 0.1) -} - -// WINDOWS ERROR — two-tone error chord -export function sfxWindowsError() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - tone(440, 'square', 0.15, d, t) - tone(466, 'square', 0.15, d, t) - tone(220, 'triangle', 0.2, d, t) - setTimeout(() => { - tone(349, 'square', 0.2, d) - tone(175, 'triangle', 0.25, d) - }, 180) -} - -// MEME THUD — super heavy bass drop -export function sfxMemeThud() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - bodyThump(35, 0.5, d, t, 0.5) - bodyThump(70, 0.4, d, t, 0.4) - noise(0.08, d, t) - fmImpact(100, 6, 0.3, d, t) - reverbTail(0.6, d, t + 0.1) -} - -// SAD TROMBONE (Wa Wa Wa Waaaa) — classic comedy fail -export function sfxSadTrombone() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - const notes: [number, number][] = [[466, 0.25], [440, 0.25], [415, 0.25], [392, 0.7]] - let off = 0 - for (let i = 0; i < notes.length; i++) { - const [freq, dur] = notes[i] - tone(freq, 'sawtooth', dur, d, t + off) - tone(freq * 0.5, 'triangle', dur, d, t + off) - if (i === 3) { - const osc = c.createOscillator(); osc.type = 'sawtooth'; osc.frequency.value = freq - const lfo = c.createOscillator(); lfo.type = 'sine'; lfo.frequency.value = 5 - const lfoG = c.createGain(); lfoG.gain.value = 8 - lfo.connect(lfoG); lfoG.connect(osc.frequency) - const g = c.createGain() - g.gain.setValueAtTime(0.2, t + off) - g.gain.exponentialRampToValueAtTime(0.001, t + off + dur) - osc.connect(g); g.connect(d) - osc.start(t + off); osc.stop(t + off + dur) - lfo.start(t + off); lfo.stop(t + off + dur) - } - off += dur - } - reverbTail(0.5, d, t + off) -} - -// EMOTIONAL DAMAGE — dramatic descending brass stab -export function sfxEmotionalDamage() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - tone(220, 'sawtooth', 0.3, d, t) - tone(277, 'sawtooth', 0.3, d, t) - tone(330, 'sawtooth', 0.3, d, t) - bodyThump(110, 0.2, d, t, 0.3) - noise(0.08, d, t) - fmImpact(600, 4, 0.2, d, t) - reverbTail(0.5, d, t + 0.1) -} - -// DUN DUN DUNNNNN — dramatic reveal stinger -export function sfxDunDunDun() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - tone(147, 'sawtooth', 0.2, d, t) - tone(147, 'triangle', 0.2, d, t) - bodyThump(73, 0.15, d, t, 0.25) - tone(147, 'sawtooth', 0.2, d, t + 0.25) - bodyThump(73, 0.15, d, t + 0.25, 0.25) - tone(110, 'sawtooth', 0.8, d, t + 0.5) - tone(110, 'triangle', 0.8, d, t + 0.5) - tone(55, 'sine', 0.8, d, t + 0.5) - bodyThump(55, 0.5, d, t + 0.5, 0.3) - reverbTail(0.8, d, t + 0.6) -} - -// FAIL HORN — game show wrong answer horn -export function sfxFailHorn() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - tone(311, 'sawtooth', 0.3, d, t) - tone(156, 'sawtooth', 0.3, d, t) - tone(233, 'square', 0.3, d, t) - setTimeout(() => { - tone(277, 'sawtooth', 0.5, d) - tone(139, 'sawtooth', 0.5, d) - tone(208, 'square', 0.5, d) - bodyThump(70, 0.3, d, undefined, 0.2) - }, 350) - reverbTail(0.5, d, t + 0.8) -} - -// PRICE IS RIGHT FAIL — descending tuba + sad brass -export function sfxPriceIsRightFail() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - const notes = [262, 247, 233, 220, 208, 196] - for (let i = 0; i < notes.length; i++) { - const nt = t + i * 0.18 - tone(notes[i], 'sawtooth', 0.2, d, nt) - tone(notes[i] * 0.5, 'triangle', 0.2, d, nt) - } - bodyThump(65, 0.4, d, t + notes.length * 0.18, 0.3) - reverbTail(0.5, d, t + notes.length * 0.18) -} - -// BUZZER — harsh game show wrong answer -export function sfxBuzzer() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - tone(150, 'square', 0.4, d, t) - tone(153, 'square', 0.4, d, t) - tone(75, 'square', 0.4, d, t) - noise(0.15, d, t) - bodyThump(60, 0.2, d, t, 0.2) -} - -// SAD VIOLIN — thin high vibrato melody -export function sfxSadViolin() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - const notes: [number, number][] = [[659, 0.3], [622, 0.3], [587, 0.4], [554, 0.5]] - let off = 0 - for (const [freq, dur] of notes) { - const osc = c.createOscillator(); osc.type = 'sawtooth'; osc.frequency.value = freq - const lfo = c.createOscillator(); lfo.type = 'sine'; lfo.frequency.value = 6 - const lfoG = c.createGain(); lfoG.gain.value = 6 - lfo.connect(lfoG); lfoG.connect(osc.frequency) - const bp = c.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = freq * 2; bp.Q.value = 3 - const g = c.createGain() - g.gain.setValueAtTime(0.15, t + off) - g.gain.exponentialRampToValueAtTime(0.001, t + off + dur) - osc.connect(bp); bp.connect(g); g.connect(d) - osc.start(t + off); osc.stop(t + off + dur) - lfo.start(t + off); lfo.stop(t + off + dur) - off += dur - } -} - -// MISSION FAILED — dark two-note descending stab -export function sfxMissionFailed() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - tone(165, 'sawtooth', 0.3, d, t) - tone(196, 'sawtooth', 0.3, d, t) - tone(247, 'sawtooth', 0.3, d, t) - bodyThump(82, 0.2, d, t, 0.2) - setTimeout(() => { - tone(139, 'sawtooth', 0.6, d) - tone(165, 'sawtooth', 0.6, d) - tone(208, 'sawtooth', 0.6, d) - bodyThump(70, 0.4, d, undefined, 0.25) - reverbTail(0.6, d) - }, 400) -} - -// CRICKETS — awkward silence chirps -export function sfxCrickets() { - const d = getSfxDest() - const c = getCtx(); const t = c.currentTime - for (let i = 0; i < 3; i++) { - const ct = t + i * 0.4 - tone(4200, 'sine', 0.03, d, ct) - tone(4200, 'sine', 0.03, d, ct + 0.05) - tone(4500, 'sine', 0.02, d, ct + 0.03) - tone(4500, 'sine', 0.02, d, ct + 0.08) - } -} - -// === BACKGROUND MUSIC === -// Rich, full arcade fighting soundtrack with 6 simultaneous layers - -const MUSIC_BPM = 170 -const MUSIC_BEAT = 60 / MUSIC_BPM -const BARS = 8 -const STEPS = 16 // per bar - -// Shared delay effect for fullness -let delayNode: DelayNode | null = null -let delayGain: GainNode | null = null - -function getMusicDelay(): GainNode { - if (delayGain && delayNode) return delayGain - const c = getCtx() - delayNode = c.createDelay(0.5) - delayNode.delayTime.value = 0.18 // 8th note delay - delayGain = c.createGain() - delayGain.gain.value = 0.25 - const feedback = c.createGain() - feedback.gain.value = 0.3 - delayNode.connect(feedback) - feedback.connect(delayNode) // feedback loop - delayNode.connect(delayGain) - delayGain.connect(musicGain!) - return delayGain -} - -// Chorus tone: 2 detuned oscillators for width -function chorusTone(freq: number, type: OscillatorType, dur: number, dest: AudioNode, t: number, vol: number = 0.2) { - const c = getCtx() - for (const detune of [-8, 8]) { // slight detune for stereo width - const osc = c.createOscillator() - osc.type = type - osc.frequency.value = freq - osc.detune.value = detune - const g = c.createGain() - g.gain.setValueAtTime(vol, t) - g.gain.exponentialRampToValueAtTime(0.001, t + dur) - osc.connect(g) - g.connect(dest) - osc.start(t) - osc.stop(t + dur) - } -} - -// FM lead with 2-operator FM + detuned chorus -function fmLead(freq: number, dur: number, dest: AudioNode, t: number) { - const c = getCtx() - // Carrier - const car = c.createOscillator() - car.type = 'sawtooth' - car.frequency.value = freq - // Modulator - const mod = c.createOscillator() - mod.type = 'sine' - mod.frequency.value = freq * 2 - const modG = c.createGain() - modG.gain.value = 150 - mod.connect(modG) - modG.connect(car.frequency) - // Detuned double for width - const car2 = c.createOscillator() - car2.type = 'sawtooth' - car2.frequency.value = freq - car2.detune.value = 10 - // Output - const g = c.createGain() - g.gain.setValueAtTime(0.16, t) - g.gain.setValueAtTime(0.16, t + dur * 0.7) - g.gain.exponentialRampToValueAtTime(0.001, t + dur) - car.connect(g) - car2.connect(g) - g.connect(dest) - // Send to delay for fullness - if (delayNode) g.connect(delayNode) - car.start(t); car.stop(t + dur) - car2.start(t); car2.stop(t + dur) - mod.start(t); mod.stop(t + dur) -} - -// Thick distorted sub-bass with overtones -function thickBass(freq: number, dur: number, dest: AudioNode, t: number) { - const c = getCtx() - // Sub - const sub = c.createOscillator() - sub.type = 'sine' - sub.frequency.value = freq / 2 - const subG = c.createGain() - subG.gain.setValueAtTime(0.25, t) - subG.gain.exponentialRampToValueAtTime(0.001, t + dur) - sub.connect(subG); subG.connect(dest) - sub.start(t); sub.stop(t + dur) - // Main with distortion - const osc = c.createOscillator() - osc.type = 'sawtooth' - osc.frequency.value = freq - const dist = c.createWaveShaper() - const curve = new Float32Array(256) - for (let i = 0; i < 256; i++) { const x = (i / 128) - 1; curve[i] = (Math.PI + 6) * x / (Math.PI + 6 * Math.abs(x)) } - dist.curve = curve - const g = c.createGain() - g.gain.setValueAtTime(0.2, t) - g.gain.exponentialRampToValueAtTime(0.001, t + dur) - osc.connect(dist); dist.connect(g); g.connect(dest) - osc.start(t); osc.stop(t + dur) - // Octave up for bite - const oct = c.createOscillator() - oct.type = 'square' - oct.frequency.value = freq * 2 - const octG = c.createGain() - octG.gain.setValueAtTime(0.06, t) - octG.gain.exponentialRampToValueAtTime(0.001, t + dur * 0.5) - oct.connect(octG); octG.connect(dest) - oct.start(t); oct.stop(t + dur) -} - -// Chord pad for harmonic fullness -function chordPad(freqs: number[], dur: number, dest: AudioNode, t: number) { - const c = getCtx() - for (const freq of freqs) { - const osc = c.createOscillator() - osc.type = 'triangle' - osc.frequency.value = freq - const g = c.createGain() - g.gain.setValueAtTime(0.04, t) - g.gain.setValueAtTime(0.04, t + dur * 0.8) - g.gain.exponentialRampToValueAtTime(0.001, t + dur) - osc.connect(g); g.connect(dest) - osc.start(t); osc.stop(t + dur) - } -} - -// Heavy layered drums -function kick(dest: AudioNode, t: number) { - const c = getCtx() - // Body - const o = c.createOscillator(); o.type = 'sine' - o.frequency.setValueAtTime(150, t) - o.frequency.exponentialRampToValueAtTime(40, t + 0.12) - const g = c.createGain() - g.gain.setValueAtTime(0.35, t) - g.gain.exponentialRampToValueAtTime(0.001, t + 0.15) - o.connect(g); g.connect(dest); o.start(t); o.stop(t + 0.15) - // Click - noise(0.015, dest, t) - // Sub thump - const s = c.createOscillator(); s.type = 'sine'; s.frequency.value = 50 - const sg = c.createGain() - sg.gain.setValueAtTime(0.2, t) - sg.gain.exponentialRampToValueAtTime(0.001, t + 0.1) - s.connect(sg); sg.connect(dest); s.start(t); s.stop(t + 0.1) -} - -function snare(dest: AudioNode, t: number) { - noise(0.08, dest, t) - const c = getCtx() - const o = c.createOscillator(); o.type = 'triangle'; o.frequency.value = 200 - const g = c.createGain() - g.gain.setValueAtTime(0.2, t) - g.gain.exponentialRampToValueAtTime(0.001, t + 0.06) - o.connect(g); g.connect(dest); o.start(t); o.stop(t + 0.06) - // Body - tone(180, 'square', 0.03, dest, t) -} - -function hihat(dest: AudioNode, t: number, open: boolean = false) { - const c = getCtx() - const dur = open ? 0.08 : 0.025 - const bufSize = Math.max(1, Math.floor(c.sampleRate * dur)) - const buf = c.createBuffer(1, bufSize, c.sampleRate) - const d = buf.getChannelData(0) - for (let i = 0; i < bufSize; i++) d[i] = Math.random() * 2 - 1 - const src = c.createBufferSource(); src.buffer = buf - const hp = c.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = open ? 6000 : 8000 - const g = c.createGain() - g.gain.setValueAtTime(open ? 0.12 : 0.08, t) - g.gain.exponentialRampToValueAtTime(0.001, t + dur) - src.connect(hp); hp.connect(g); g.connect(dest) - src.start(t); src.stop(t + dur) -} - -// === GENRE-SPECIFIC SYNTH VARIANTS === - -// Pulse bass — sharp square wave, chiptune/heroic/bouncy -function pulseBass(freq: number, dur: number, dest: AudioNode, t: number) { - const c = getCtx() - const osc = c.createOscillator() - osc.type = 'square' - osc.frequency.value = freq - const g = c.createGain() - g.gain.setValueAtTime(0.18, t) - g.gain.exponentialRampToValueAtTime(0.001, t + dur) - osc.connect(g); g.connect(dest) - osc.start(t); osc.stop(t + dur) -} - -// Slap bass — short punchy sine with fast decay, funky/speed -function slapBass(freq: number, dur: number, dest: AudioNode, t: number) { - const c = getCtx() - const osc = c.createOscillator() - osc.type = 'sine' - osc.frequency.setValueAtTime(freq * 1.5, t) - osc.frequency.exponentialRampToValueAtTime(freq, t + 0.02) - const g = c.createGain() - g.gain.setValueAtTime(0.3, t) - g.gain.exponentialRampToValueAtTime(0.001, t + Math.min(dur, 0.12)) - osc.connect(g); g.connect(dest) - osc.start(t); osc.stop(t + dur) -} - -// Sub bass — deep pure sine, atmospheric/emotional -function subBass(freq: number, dur: number, dest: AudioNode, t: number) { - const c = getCtx() - const osc = c.createOscillator() - osc.type = 'sine' - osc.frequency.value = freq - const g = c.createGain() - g.gain.setValueAtTime(0.25, t) - g.gain.setValueAtTime(0.25, t + dur * 0.8) - g.gain.exponentialRampToValueAtTime(0.001, t + dur) - osc.connect(g); g.connect(dest) - osc.start(t); osc.stop(t + dur) -} - -// Distorted bass — sawtooth through waveshaper, metal/boss -function distortedBass(freq: number, dur: number, dest: AudioNode, t: number) { - const c = getCtx() - const osc = c.createOscillator() - osc.type = 'sawtooth' - osc.frequency.value = freq - const dist = c.createWaveShaper() - const curve = new Float32Array(256) - for (let i = 0; i < 256; i++) { const x = (i / 128) - 1; curve[i] = Math.tanh(x * 3) } - dist.curve = curve - const g = c.createGain() - g.gain.setValueAtTime(0.22, t) - g.gain.exponentialRampToValueAtTime(0.001, t + dur) - osc.connect(dist); dist.connect(g); g.connect(dest) - osc.start(t); osc.stop(t + dur) - const sub = c.createOscillator() - sub.type = 'sine' - sub.frequency.value = freq * 0.5 - const sg = c.createGain() - sg.gain.setValueAtTime(0.12, t) - sg.gain.exponentialRampToValueAtTime(0.001, t + dur * 0.7) - sub.connect(sg); sg.connect(dest) - sub.start(t); sub.stop(t + dur) -} - -// Organ bass — square + sine harmonics, gothic -function organBass(freq: number, dur: number, dest: AudioNode, t: number) { - const c = getCtx() - for (const [waveType, vol, mult] of [['square', 0.1, 1], ['sine', 0.12, 2], ['sine', 0.06, 3]] as [OscillatorType, number, number][]) { - const osc = c.createOscillator() - osc.type = waveType - osc.frequency.value = freq * mult - const g = c.createGain() - g.gain.setValueAtTime(vol, t) - g.gain.setValueAtTime(vol, t + dur * 0.85) - g.gain.exponentialRampToValueAtTime(0.001, t + dur) - osc.connect(g); g.connect(dest) - osc.start(t); osc.stop(t + dur) - } -} - -// Sharp lead — detuned square waves, chiptune/ninja -function sharpLead(freq: number, dur: number, dest: AudioNode, t: number) { - const c = getCtx() - const osc = c.createOscillator() - osc.type = 'square' - osc.frequency.value = freq - const osc2 = c.createOscillator() - osc2.type = 'square' - osc2.frequency.value = freq - osc2.detune.value = 6 - const g = c.createGain() - g.gain.setValueAtTime(0.1, t) - g.gain.exponentialRampToValueAtTime(0.001, t + dur) - osc.connect(g); osc2.connect(g); g.connect(dest) - if (delayNode) g.connect(delayNode) - osc.start(t); osc.stop(t + dur) - osc2.start(t); osc2.stop(t + dur) -} - -// Smooth lead — gentle sine FM with vibrato, emotional/JRPG -function smoothLead(freq: number, dur: number, dest: AudioNode, t: number) { - const c = getCtx() - const car = c.createOscillator() - car.type = 'triangle' - car.frequency.value = freq - const mod = c.createOscillator() - mod.type = 'sine' - mod.frequency.value = freq * 1.5 - const modG = c.createGain() - modG.gain.value = 40 - mod.connect(modG); modG.connect(car.frequency) - const vib = c.createOscillator() - vib.type = 'sine' - vib.frequency.value = 5 - const vibG = c.createGain() - vibG.gain.value = 4 - vib.connect(vibG); vibG.connect(car.frequency) - const g = c.createGain() - g.gain.setValueAtTime(0.001, t) - g.gain.linearRampToValueAtTime(0.14, t + dur * 0.15) - g.gain.setValueAtTime(0.14, t + dur * 0.7) - g.gain.exponentialRampToValueAtTime(0.001, t + dur) - car.connect(g); g.connect(dest) - if (delayNode) g.connect(delayNode) - car.start(t); car.stop(t + dur) - mod.start(t); mod.stop(t + dur) - vib.start(t); vib.stop(t + dur) -} - -// Aggressive lead — heavy FM sawtooth, metal/boss -function aggressiveLead(freq: number, dur: number, dest: AudioNode, t: number) { - const c = getCtx() - const car = c.createOscillator() - car.type = 'sawtooth' - car.frequency.value = freq - const mod = c.createOscillator() - mod.type = 'square' - mod.frequency.value = freq * 3 - const modG = c.createGain() - modG.gain.value = 300 - mod.connect(modG); modG.connect(car.frequency) - const car2 = c.createOscillator() - car2.type = 'sawtooth' - car2.frequency.value = freq - car2.detune.value = -15 - const g = c.createGain() - g.gain.setValueAtTime(0.13, t) - g.gain.exponentialRampToValueAtTime(0.001, t + dur) - car.connect(g); car2.connect(g); g.connect(dest) - if (delayNode) g.connect(delayNode) - car.start(t); car.stop(t + dur) - car2.start(t); car2.stop(t + dur) - mod.start(t); mod.stop(t + dur) -} - -// Wah lead — filtered sawtooth with bandpass sweep, funky/speed -function wahLead(freq: number, dur: number, dest: AudioNode, t: number) { - const c = getCtx() - const osc = c.createOscillator() - osc.type = 'sawtooth' - osc.frequency.value = freq - const filter = c.createBiquadFilter() - filter.type = 'bandpass' - filter.Q.value = 5 - filter.frequency.setValueAtTime(freq * 2, t) - filter.frequency.exponentialRampToValueAtTime(freq * 8, t + dur * 0.3) - filter.frequency.exponentialRampToValueAtTime(freq * 2, t + dur) - const g = c.createGain() - g.gain.setValueAtTime(0.2, t) - g.gain.exponentialRampToValueAtTime(0.001, t + dur) - osc.connect(filter); filter.connect(g); g.connect(dest) - if (delayNode) g.connect(delayNode) - osc.start(t); osc.stop(t + dur) -} - -// Gothic lead — vibrato sawtooth + triangle overtone, Castlevania -function gothicLead(freq: number, dur: number, dest: AudioNode, t: number) { - const c = getCtx() - const osc = c.createOscillator() - osc.type = 'sawtooth' - osc.frequency.value = freq - const vib = c.createOscillator() - vib.type = 'sine' - vib.frequency.value = 6.5 - const vibG = c.createGain() - vibG.gain.value = 8 - vib.connect(vibG); vibG.connect(osc.frequency) - const osc2 = c.createOscillator() - osc2.type = 'triangle' - osc2.frequency.value = freq * 2 - const g = c.createGain() - g.gain.setValueAtTime(0.12, t) - g.gain.setValueAtTime(0.12, t + dur * 0.75) - g.gain.exponentialRampToValueAtTime(0.001, t + dur) - osc.connect(g); g.connect(dest) - const g2 = c.createGain() - g2.gain.setValueAtTime(0.04, t) - g2.gain.exponentialRampToValueAtTime(0.001, t + dur) - osc2.connect(g2); g2.connect(dest) - if (delayNode) g.connect(delayNode) - osc.start(t); osc.stop(t + dur) - osc2.start(t); osc2.stop(t + dur) - vib.start(t); vib.stop(t + dur) -} - -// Dark pad — filtered sawtooth, gothic/atmospheric/boss -function darkPad(freqs: number[], dur: number, dest: AudioNode, t: number) { - const c = getCtx() - for (const freq of freqs) { - const osc = c.createOscillator() - osc.type = 'sawtooth' - osc.frequency.value = freq - const filter = c.createBiquadFilter() - filter.type = 'lowpass' - filter.frequency.value = freq * 3 - filter.Q.value = 1 - const g = c.createGain() - g.gain.setValueAtTime(0.03, t) - g.gain.setValueAtTime(0.03, t + dur * 0.8) - g.gain.exponentialRampToValueAtTime(0.001, t + dur) - osc.connect(filter); filter.connect(g); g.connect(dest) - osc.start(t); osc.stop(t + dur) - } -} - -// Bright pad — square wave, heroic/bouncy -function brightPad(freqs: number[], dur: number, dest: AudioNode, t: number) { - const c = getCtx() - for (const freq of freqs) { - const osc = c.createOscillator() - osc.type = 'square' - osc.frequency.value = freq - const g = c.createGain() - g.gain.setValueAtTime(0.025, t) - g.gain.setValueAtTime(0.025, t + dur * 0.8) - g.gain.exponentialRampToValueAtTime(0.001, t + dur) - osc.connect(g); g.connect(dest) - osc.start(t); osc.stop(t + dur) - } -} - -// Style dispatch tables -type SynthStyle = 'epic' | 'gothic' | 'funky' | 'heroic' | 'metal' | 'emotional' | 'ninja' | 'speed' | 'atmospheric' | 'military' | 'bouncy' | 'boss' -type BassFunc = (freq: number, dur: number, dest: AudioNode, t: number) => void -type PadFunc = (freqs: number[], dur: number, dest: AudioNode, t: number) => void - -const STYLE_BASS: Record = { - epic: thickBass, gothic: organBass, funky: slapBass, heroic: pulseBass, - metal: distortedBass, emotional: subBass, ninja: pulseBass, speed: slapBass, - atmospheric: subBass, military: thickBass, bouncy: pulseBass, boss: distortedBass, -} -const STYLE_LEAD: Record = { - epic: fmLead, gothic: gothicLead, funky: wahLead, heroic: sharpLead, - metal: aggressiveLead, emotional: smoothLead, ninja: sharpLead, speed: wahLead, - atmospheric: smoothLead, military: fmLead, bouncy: sharpLead, boss: aggressiveLead, -} -const STYLE_ARP_TYPE: Record = { - epic: 'triangle', gothic: 'sawtooth', funky: 'square', heroic: 'square', - metal: 'sawtooth', emotional: 'triangle', ninja: 'square', speed: 'triangle', - atmospheric: 'sine', military: 'triangle', bouncy: 'square', boss: 'sawtooth', -} -const STYLE_PAD: Record = { - epic: chordPad, gothic: darkPad, funky: chordPad, heroic: brightPad, - metal: darkPad, emotional: chordPad, ninja: darkPad, speed: brightPad, - atmospheric: darkPad, military: chordPad, bouncy: brightPad, boss: darkPad, -} - -// === TRACK DATA === -interface MusicTrack { - name: string - bpm: number - synthStyle: SynthStyle - bass: number[] - lead: number[] - arp: number[] - chords: number[][] - drums: number[] -} - -// Track 1: "Desert Storm" — 200 BPM, A minor, Turrican-style epic sweep -const track1: MusicTrack = { - name: 'Desert Storm', bpm: 200, synthStyle: 'epic', - bass: [ - 110,0,220,0, 110,0,220,110, 110,0,220,0, 165,0,220,0, - 87,0,175,0, 87,0,175,87, 87,0,175,0, 131,0,175,0, - 73,0,147,0, 73,0,147,73, 73,0,147,0, 110,0,147,0, - 82,0,165,0, 82,0,165,82, 82,0,165,0, 123,0,165,0, - 110,0,220,110, 110,165,220,110, 110,0,220,110, 110,165,220,165, - 131,0,262,131, 131,196,262,131, 131,0,262,131, 131,196,262,196, - 87,0,175,87, 87,131,175,87, 87,0,175,87, 87,131,175,131, - 82,0,165,82, 82,123,165,82, 82,165,247,330, 165,82,0,0, - ], - lead: [ - 440,0,523,659, 880,0,659,523, 440,0,523,659, 784,880,784,659, - 349,0,440,523, 698,0,523,440, 349,0,440,523, 587,698,659,523, - 294,0,349,440, 587,0,440,349, 587,698,880,1047, 880,698,587,440, - 330,0,440,523, 659,0,784,880, 1047,0,880,784, 659,523,440,330, - 880,1047,1319,1760, 1319,0,1047,880, 784,659,880,1047, 1319,1047,880,659, - 1047,1319,1568,1319, 1047,784,659,784, 1047,1319,1568,2093, 1568,1319,1047,784, - 698,880,1047,1397, 1760,0,1397,1047, 880,698,880,1047, 1397,1047,880,698, - 659,784,880,1047, 880,784,659,523, 440,523,659,880, 659,0,0,0, - ], - arp: [ - 440,523,659,523, 440,523,659,880, 659,523,440,523, 659,880,659,523, - 349,440,523,440, 349,440,523,698, 523,440,349,440, 523,698,523,440, - 294,349,440,349, 294,349,440,587, 440,349,294,349, 440,587,440,349, - 330,415,523,415, 330,415,523,659, 523,415,330,415, 523,659,523,415, - 440,659,880,659, 440,523,659,880, 1047,880,659,523, 440,659,880,1047, - 523,784,1047,784, 523,659,784,1047, 1319,1047,784,659, 523,784,1047,1319, - 349,523,698,523, 349,440,523,698, 880,698,523,440, 349,523,698,880, - 330,523,659,523, 330,415,523,659, 880,659,523,415, 440,523,659,0, - ], - chords: [[220,262,330],[175,220,262],[147,175,220],[165,208,247],[220,262,330],[131,165,196],[175,220,262],[165,208,247]], - drums: [ - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,4, - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, - 1,0,3,0, 2,0,3,4, 1,0,3,0, 2,3,4,3, - 1,3,1,3, 2,0,3,0, 1,3,1,3, 2,0,3,0, - 1,3,1,3, 2,0,3,0, 1,3,1,3, 2,3,4,3, - 1,3,1,3, 2,3,1,3, 1,3,1,3, 2,3,1,3, - 1,3,1,3, 2,3,4,3, 1,1,2,1, 2,2,2,2, - ], -} - -// Track 2: "Vampire's Requiem" — 190 BPM, D minor, Castlevania gothic -const track2: MusicTrack = { - name: "Vampire's Requiem", bpm: 190, synthStyle: 'gothic', - bass: [ - 147,0,294,147, 147,0,294,0, 147,0,294,147, 175,220,294,220, - 117,0,233,117, 117,0,233,0, 117,0,233,117, 147,175,233,175, - 98,0,196,98, 98,0,196,0, 98,0,196,98, 131,147,196,147, - 110,0,220,110, 110,0,220,0, 110,0,220,110, 139,165,220,165, - 147,294,147,294, 147,294,175,220, 147,294,147,294, 175,220,294,349, - 117,233,117,233, 117,233,175,233, 175,349,175,349, 175,349,220,262, - 98,196,98,196, 131,196,262,196, 98,196,98,196, 131,196,262,330, - 110,220,110,220, 139,220,277,220, 110,0,220,0, 110,0,0,0, - ], - lead: [ - 587,0,698,880, 1047,0,880,698, 587,0,440,349, 440,587,698,880, - 466,0,587,698, 932,0,698,587, 466,0,349,233, 349,466,587,698, - 392,0,494,587, 784,0,587,494, 392,0,330,262, 330,392,494,587, - 440,0,554,698, 880,0,698,554, 440,330,220,330, 440,554,698,880, - 1175,0,1047,880, 698,587,440,587, 698,880,1047,1175, 1397,1175,1047,880, - 932,0,880,698, 587,466,349,466, 587,698,880,932, 1047,932,880,698, - 784,0,698,587, 494,392,330,392, 494,587,698,784, 880,784,698,587, - 880,698,554,440, 554,440,330,220, 294,349,440,587, 698,0,0,0, - ], - arp: [ - 294,349,440,349, 294,349,440,587, 440,349,294,349, 440,587,440,349, - 233,294,349,294, 233,294,349,466, 349,294,233,294, 349,466,349,294, - 196,262,330,262, 196,262,330,392, 330,262,196,262, 330,392,330,262, - 220,277,349,277, 220,277,349,440, 349,277,220,277, 349,440,349,277, - 294,440,587,440, 294,349,440,587, 698,587,440,349, 294,440,587,698, - 233,349,466,349, 233,294,349,466, 587,466,349,294, 233,349,466,587, - 196,330,392,330, 196,262,330,392, 494,392,330,262, 196,330,392,494, - 220,349,440,349, 220,277,349,440, 554,440,349,277, 220,349,440,0, - ], - chords: [[147,175,220],[233,294,349],[196,233,294],[220,277,330],[147,175,220],[175,220,262],[233,294,349],[220,277,330]], - drums: [ - 1,0,0,3, 2,0,0,3, 1,0,0,3, 2,0,0,3, - 1,0,0,3, 2,0,0,3, 1,0,0,3, 2,0,3,4, - 1,0,3,3, 2,0,0,3, 1,0,3,3, 2,0,0,3, - 1,0,3,3, 2,0,0,3, 1,0,3,3, 2,0,3,4, - 1,3,0,3, 2,0,3,3, 1,3,0,3, 2,0,3,3, - 1,3,0,3, 2,3,0,3, 1,3,0,3, 2,3,4,3, - 1,3,1,3, 2,3,1,3, 1,3,1,3, 2,3,1,3, - 1,1,2,1, 2,1,2,1, 2,2,2,2, 2,2,2,2, - ], -} - -// Track 3: "Street Heat" — 175 BPM, E minor, Streets of Rage funky -const track3: MusicTrack = { - name: 'Street Heat', bpm: 175, synthStyle: 'funky', - bass: [ - 82,0,0,165, 0,0,82,0, 165,0,0,196, 0,165,0,82, - 110,0,0,220, 0,0,110,0, 220,0,0,262, 0,220,0,110, - 131,0,0,262, 0,0,131,0, 262,0,0,330, 0,262,0,131, - 123,0,0,247, 0,0,123,0, 247,0,0,294, 0,247,0,123, - 82,165,0,82, 0,165,82,0, 165,82,0,196, 165,0,82,165, - 110,220,0,110, 0,220,110,0, 220,110,0,262, 220,0,110,220, - 131,262,0,131, 0,262,131,0, 262,131,0,330, 262,0,131,262, - 123,247,0,123, 0,247,123,0, 247,0,0,0, 123,0,0,0, - ], - lead: [ - 659,0,0,784, 0,0,659,0, 0,880,0,784, 0,659,0,0, - 880,0,0,1047, 0,0,880,0, 0,1175,0,1047, 0,880,0,0, - 1047,0,0,1175, 0,0,1319,0, 0,1175,0,1047, 0,880,784,0, - 988,0,0,880, 0,0,784,0, 0,659,0,784, 880,0,784,0, - 659,784,0,880, 1047,0,880,0, 659,0,784,880, 1047,0,880,659, - 880,1047,0,1175, 1319,0,1175,0, 880,0,1047,1175, 1319,0,1175,880, - 1047,1319,0,1568, 1760,0,1568,0, 1319,0,1047,880, 784,0,659,0, - 880,659,0,784, 659,0,523,0, 494,0,659,0, 784,0,0,0, - ], - arp: [ - 330,0,392,0, 494,0,392,0, 330,0,494,0, 659,0,494,0, - 440,0,523,0, 659,0,523,0, 440,0,659,0, 880,0,659,0, - 523,0,659,0, 784,0,659,0, 523,0,784,0, 1047,0,784,0, - 494,0,587,0, 740,0,587,0, 494,0,740,0, 988,0,740,0, - 330,494,659,494, 330,392,494,659, 784,659,494,392, 330,494,659,784, - 440,659,880,659, 440,523,659,880, 1047,880,659,523, 440,659,880,1047, - 523,784,1047,784, 523,659,784,1047, 1319,1047,784,659, 523,784,1047,1319, - 494,740,988,740, 494,587,740,988, 740,0,0,0, 494,0,0,0, - ], - chords: [[165,196,247],[220,262,330],[262,330,392],[247,294,370],[165,196,247],[196,247,294],[220,262,330],[247,294,370]], - drums: [ - 1,0,0,0, 2,0,0,3, 0,0,1,0, 2,0,3,0, - 1,0,0,0, 2,0,0,3, 0,0,1,0, 2,0,3,4, - 1,0,3,0, 2,0,0,3, 0,3,1,0, 2,0,3,0, - 1,0,3,0, 2,3,0,3, 0,3,1,0, 2,0,3,4, - 1,0,3,0, 2,3,0,3, 1,3,1,0, 2,0,3,0, - 1,3,3,0, 2,3,0,3, 1,3,1,0, 2,3,3,4, - 1,3,3,3, 2,3,0,3, 1,3,3,3, 2,3,1,3, - 1,1,2,1, 2,1,2,3, 1,1,2,2, 2,2,2,2, - ], -} - -// Track 4: "Steel Resolve" — 210 BPM, C major, Mega Man heroic -const track4: MusicTrack = { - name: 'Steel Resolve', bpm: 210, synthStyle: 'heroic', - bass: [ - 131,0,262,131, 131,0,262,0, 131,0,196,0, 131,262,196,131, - 196,0,392,196, 196,0,392,0, 196,0,262,0, 196,392,262,196, - 220,0,440,220, 220,0,440,0, 220,0,330,0, 220,440,330,220, - 175,0,349,175, 175,0,349,0, 175,0,262,0, 175,349,262,175, - 131,262,131,262, 131,262,196,262, 196,392,196,392, 196,392,262,392, - 220,440,220,440, 220,440,330,440, 175,349,175,349, 175,349,262,349, - 131,196,262,330, 262,196,131,196, 196,262,330,392, 330,262,196,131, - 175,262,349,440, 349,262,175,131, 131,262,0,0, 131,0,0,0, - ], - lead: [ - 523,659,784,1047, 784,659,523,659, 784,1047,1319,1047, 784,659,523,392, - 784,1047,1175,1319, 1175,1047,784,659, 523,659,784,1047, 1175,1047,784,659, - 880,1047,1175,1319, 1175,1047,880,784, 659,784,880,1047, 1175,1047,880,659, - 698,880,1047,1175, 1047,880,698,523, 440,523,698,880, 1047,880,698,523, - 1047,1319,1568,1319, 1047,784,659,784, 1047,1319,1568,2093, 1568,1319,1047,784, - 1175,1319,1568,1760, 1568,1319,1175,880, 659,880,1175,1319, 1568,1319,1175,880, - 880,1047,1175,1319, 1175,1047,880,784, 659,784,880,1047, 1175,1047,880,784, - 698,880,1047,1175, 1047,880,698,523, 523,659,784,1047, 784,0,0,0, - ], - arp: [ - 262,330,392,330, 262,330,392,523, 392,330,262,330, 392,523,392,330, - 392,494,587,494, 392,494,587,784, 587,494,392,494, 587,784,587,494, - 440,523,659,523, 440,523,659,880, 659,523,440,523, 659,880,659,523, - 349,440,523,440, 349,440,523,698, 523,440,349,440, 523,698,523,440, - 262,392,523,784, 523,392,262,392, 523,784,1047,784, 523,392,262,523, - 392,587,784,1047, 784,587,392,587, 784,1047,1319,1047, 784,587,392,784, - 440,659,880,659, 440,523,659,880, 1047,880,659,523, 440,659,880,1047, - 349,523,698,523, 349,440,523,698, 880,698,523,0, 262,523,0,0, - ], - chords: [[262,330,392],[196,247,294],[220,262,330],[175,220,262],[262,330,392],[165,196,247],[175,220,262],[196,247,294]], - drums: [ - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, - 1,0,3,0, 2,0,3,0, 1,3,3,0, 2,0,3,4, - 1,3,3,0, 2,0,3,0, 1,3,3,0, 2,0,3,0, - 1,3,3,0, 2,3,3,0, 1,3,3,0, 2,3,3,4, - 1,3,1,3, 2,0,3,0, 1,3,1,3, 2,0,3,4, - 1,3,1,3, 2,3,1,3, 1,3,1,3, 2,3,4,3, - 1,3,1,3, 2,3,1,3, 1,1,1,3, 2,1,1,3, - 1,1,1,1, 2,1,2,1, 2,2,2,1, 2,2,2,2, - ], -} - -// Track 5: "Thunder Blade" — 230 BPM, B minor, Thunder Force metal shred -const track5: MusicTrack = { - name: 'Thunder Blade', bpm: 230, synthStyle: 'metal', - bass: [ - 123,123,247,123, 123,123,247,123, 123,123,247,0, 185,247,185,123, - 98,98,196,98, 98,98,196,98, 98,98,196,0, 147,196,147,98, - 82,82,165,82, 82,82,165,82, 82,82,165,0, 131,165,131,82, - 92,92,185,92, 92,92,185,92, 92,92,185,0, 139,185,139,92, - 123,247,123,247, 123,247,185,247, 123,247,123,247, 185,247,294,370, - 98,196,98,196, 98,196,147,196, 98,196,98,196, 147,196,247,294, - 82,165,82,165, 82,165,131,165, 82,165,82,165, 131,165,196,247, - 92,185,92,185, 92,185,139,185, 92,185,247,370, 247,185,0,0, - ], - lead: [ - 988,0,988,1175, 1480,0,1175,988, 740,0,740,988, 1175,0,988,740, - 784,0,784,988, 1175,0,988,784, 587,0,587,784, 988,0,784,587, - 659,0,659,784, 988,0,784,659, 494,0,494,659, 784,0,659,494, - 740,0,740,880, 1109,0,880,740, 554,0,554,740, 880,0,740,554, - 988,1175,1480,1976, 1480,1175,988,1175, 1480,1976,1480,1175, 988,740,988,1175, - 784,988,1175,1568, 1175,988,784,988, 1175,1568,1175,988, 784,587,784,988, - 659,784,988,1319, 988,784,659,784, 988,1319,988,784, 659,494,659,784, - 1480,1175,988,740, 988,740,554,440, 554,740,988,1175, 1480,0,0,0, - ], - arp: [ - 247,370,494,370, 247,370,494,740, 494,370,247,370, 494,740,494,370, - 196,294,392,294, 196,294,392,587, 392,294,196,294, 392,587,392,294, - 165,247,330,247, 165,247,330,494, 330,247,165,247, 330,494,330,247, - 185,277,370,277, 185,277,370,554, 370,277,185,277, 370,554,370,277, - 247,494,740,494, 247,370,494,740, 988,740,494,370, 247,494,740,988, - 196,392,587,392, 196,294,392,587, 784,587,392,294, 196,392,587,784, - 165,330,494,330, 165,247,330,494, 659,494,330,247, 165,330,494,659, - 185,370,554,370, 185,277,370,554, 740,554,370,0, 247,370,0,0, - ], - chords: [[247,294,370],[196,247,294],[165,196,247],[185,220,277],[247,294,370],[220,262,330],[196,247,294],[185,220,277]], - drums: [ - 1,1,1,3, 2,1,1,3, 1,1,1,3, 2,1,4,1, - 1,1,1,3, 2,1,1,3, 1,1,1,3, 2,1,4,1, - 1,1,1,1, 2,1,1,1, 1,1,4,1, 2,1,1,1, - 1,1,1,1, 2,1,4,1, 2,1,1,1, 2,2,4,1, - 1,1,1,1, 2,1,1,1, 1,1,1,1, 2,1,4,1, - 1,1,1,1, 2,1,1,1, 1,1,1,1, 2,1,4,1, - 1,1,1,1, 2,1,4,1, 1,1,1,1, 2,1,4,1, - 1,1,2,1, 2,1,2,1, 2,2,2,2, 2,2,2,2, - ], -} - -// Track 6: "Crystal Elegy" — 150 BPM, F major, JRPG emotional -const track6: MusicTrack = { - name: 'Crystal Elegy', bpm: 150, synthStyle: 'emotional', - bass: [ - 87,0,175,0, 87,0,175,0, 87,0,175,0, 131,0,175,0, - 73,0,147,0, 73,0,147,0, 73,0,147,0, 110,0,147,0, - 117,0,233,0, 117,0,233,0, 117,0,233,0, 175,0,233,0, - 131,0,262,0, 131,0,262,0, 131,0,262,0, 196,0,262,0, - 87,175,87,175, 87,131,175,262, 175,131,87,131, 175,262,175,131, - 73,147,73,147, 73,110,147,220, 147,110,73,110, 147,220,147,110, - 117,233,117,233, 117,175,233,349, 233,175,117,175, 233,349,233,175, - 131,262,131,262, 131,196,262,330, 262,196,131,0, 131,0,0,0, - ], - lead: [ - 698,0,0,880, 1047,0,0,880, 698,0,0,523, 440,0,523,698, - 587,0,0,698, 880,0,0,698, 587,0,0,440, 349,0,440,587, - 466,0,0,587, 698,0,0,587, 466,0,0,349, 233,0,349,466, - 523,0,0,659, 784,0,0,659, 523,0,0,440, 330,0,440,523, - 1397,0,1175,1047, 880,0,698,523, 440,523,698,880, 1047,880,698,523, - 1175,0,1047,880, 698,0,587,440, 349,440,587,698, 880,698,587,440, - 932,0,880,698, 587,0,466,349, 233,349,466,587, 698,587,466,349, - 1047,0,880,784, 659,523,440,349, 440,523,698,880, 1047,0,0,0, - ], - arp: [ - 175,220,262,220, 175,220,262,349, 262,220,175,220, 262,349,262,220, - 147,175,220,175, 147,175,220,294, 220,175,147,175, 220,294,220,175, - 233,294,349,294, 233,294,349,466, 349,294,233,294, 349,466,349,294, - 262,330,392,330, 262,330,392,523, 392,330,262,330, 392,523,392,330, - 175,262,349,523, 349,262,175,262, 349,523,698,523, 349,262,175,349, - 147,220,294,440, 294,220,147,220, 294,440,587,440, 294,220,147,294, - 233,349,466,698, 466,349,233,349, 466,698,932,698, 466,349,233,466, - 262,392,523,784, 523,392,262,392, 523,784,0,0, 262,0,0,0, - ], - chords: [[175,220,262],[147,175,220],[233,294,349],[262,330,392],[175,220,262],[220,262,330],[233,294,349],[262,330,392]], - drums: [ - 1,0,0,0, 0,0,2,0, 0,0,0,0, 0,0,3,0, - 1,0,0,0, 0,0,2,0, 0,0,0,0, 0,0,3,0, - 1,0,0,3, 0,0,2,0, 0,3,0,0, 0,0,3,0, - 1,0,0,3, 0,0,2,0, 0,3,0,0, 2,0,3,4, - 1,0,3,0, 2,0,0,3, 1,0,3,0, 2,0,0,3, - 1,0,3,0, 2,0,3,3, 1,0,3,0, 2,0,3,4, - 1,0,3,0, 2,0,3,0, 1,0,3,3, 2,0,3,0, - 1,0,3,0, 2,3,3,0, 1,0,0,0, 0,0,0,0, - ], -} - -// Track 7: "Shadow Assault" — 215 BPM, G minor, Ninja Gaiden action -const track7: MusicTrack = { - name: 'Shadow Assault', bpm: 215, synthStyle: 'ninja', - bass: [ - 98,0,196,98, 98,0,196,0, 98,0,196,98, 131,156,196,156, - 156,0,311,156, 156,0,311,0, 156,0,311,156, 196,233,311,233, - 131,0,262,131, 131,0,262,0, 131,0,262,131, 175,196,262,196, - 147,0,294,147, 147,0,294,0, 147,0,294,147, 185,220,294,220, - 98,196,98,196, 156,196,131,196, 98,196,98,196, 156,196,262,330, - 156,311,156,311, 196,311,233,311, 156,311,156,311, 196,311,392,466, - 131,262,131,262, 175,262,196,262, 131,262,131,262, 175,262,330,392, - 147,294,147,294, 185,294,220,294, 147,294,0,0, 147,0,0,0, - ], - lead: [ - 784,0,932,784, 0,784,932,1175, 1568,0,1175,932, 784,0,659,784, - 622,0,784,622, 0,622,784,988, 1175,0,988,784, 622,0,523,622, - 523,0,659,523, 0,523,659,784, 1047,0,784,659, 523,0,440,523, - 587,0,740,587, 0,587,740,880, 1175,0,880,740, 587,0,494,587, - 784,932,1175,1568, 1175,932,784,932, 1175,1568,1175,932, 784,659,784,932, - 622,784,988,1319, 988,784,622,784, 988,1319,988,784, 622,523,622,784, - 1047,1175,1568,1865, 1568,1175,1047,1175, 1568,1865,1568,1175, 1047,784,659,523, - 587,740,880,1175, 880,740,587,440, 587,0,784,0, 932,0,0,0, - ], - arp: [ - 196,233,294,233, 196,233,294,392, 294,233,196,233, 294,392,294,233, - 311,392,466,392, 311,392,466,622, 466,392,311,392, 466,622,466,392, - 262,330,392,330, 262,330,392,523, 392,330,262,330, 392,523,392,330, - 294,370,440,370, 294,370,440,587, 440,370,294,370, 440,587,440,370, - 196,294,392,587, 392,294,196,294, 392,587,784,587, 392,294,196,392, - 311,466,622,932, 622,466,311,466, 622,932,1175,932, 622,466,311,622, - 262,392,523,784, 523,392,262,392, 523,784,1047,784, 523,392,262,523, - 294,440,587,880, 587,440,294,440, 587,0,0,0, 294,0,0,0, - ], - chords: [[196,233,294],[311,392,466],[262,311,392],[294,370,440],[196,233,294],[233,294,349],[311,392,466],[294,370,440]], - drums: [ - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,4, - 1,3,0,3, 2,0,3,0, 1,3,0,3, 2,0,3,0, - 1,3,0,3, 2,3,0,3, 1,3,0,3, 2,3,4,3, - 1,3,1,3, 2,0,3,0, 1,3,1,3, 2,0,3,4, - 1,3,1,3, 2,3,1,3, 1,3,1,3, 2,3,4,3, - 1,1,1,3, 2,1,1,3, 1,1,1,3, 2,1,4,1, - 1,1,1,1, 2,1,2,1, 2,2,2,1, 2,2,2,2, - ], -} - -// Track 8: "Emerald Rush" — 195 BPM, G major, Sonic-style funky speed -const track8: MusicTrack = { - name: 'Emerald Rush', bpm: 195, synthStyle: 'speed', - bass: [ - 98,0,0,196, 0,0,98,0, 196,0,0,98, 0,196,0,98, - 82,0,0,165, 0,0,82,0, 165,0,0,82, 0,165,0,82, - 131,0,0,262, 0,0,131,0, 262,0,0,131, 0,262,0,131, - 73,0,0,147, 0,0,73,0, 147,0,0,73, 0,147,0,73, - 98,196,0,98, 196,0,98,196, 0,196,98,0, 196,98,196,262, - 82,165,0,82, 165,0,82,165, 0,165,82,0, 165,82,165,247, - 131,262,0,131, 262,0,131,262, 0,262,131,0, 262,131,262,330, - 147,294,0,147, 294,0,147,294, 147,0,0,0, 98,0,0,0, - ], - lead: [ - 784,0,880,1047, 1175,0,1047,880, 784,0,659,523, 659,784,880,1047, - 659,0,784,880, 1047,0,880,784, 659,0,523,440, 523,659,784,880, - 1047,0,1175,1319, 1568,0,1319,1175, 1047,0,880,784, 880,1047,1175,1319, - 587,0,659,784, 880,0,784,659, 587,0,523,440, 523,587,659,784, - 1175,1319,1568,1760, 1568,1319,1175,1047, 880,784,880,1047, 1175,1047,880,784, - 1047,1175,1319,1568, 1319,1175,1047,880, 784,659,784,880, 1047,880,784,659, - 1568,1760,2093,1760, 1568,1319,1175,1047, 880,784,659,523, 659,784,880,1047, - 880,784,659,587, 523,440,392,330, 392,440,523,659, 784,0,0,0, - ], - arp: [ - 392,494,587,494, 392,494,587,784, 587,494,392,494, 587,784,587,494, - 330,392,494,392, 330,392,494,659, 494,392,330,392, 494,659,494,392, - 523,659,784,659, 523,659,784,1047, 784,659,523,659, 784,1047,784,659, - 294,370,440,370, 294,370,440,587, 440,370,294,370, 440,587,440,370, - 392,587,784,587, 392,494,587,784, 1047,784,587,494, 392,587,784,1047, - 330,494,659,494, 330,392,494,659, 880,659,494,392, 330,494,659,880, - 523,784,1047,784, 523,659,784,1047, 1319,1047,784,659, 523,784,1047,1319, - 294,440,587,440, 294,370,440,587, 784,587,0,0, 392,0,0,0, - ], - chords: [[196,247,294],[165,196,247],[262,330,392],[147,175,220],[196,247,294],[123,147,185],[262,330,392],[147,175,220]], - drums: [ - 1,0,0,3, 2,0,0,3, 0,0,1,0, 2,0,3,0, - 1,0,0,3, 2,0,0,3, 0,3,1,0, 2,0,3,4, - 1,0,3,0, 2,0,0,3, 1,0,3,0, 2,0,3,0, - 1,0,3,0, 2,3,0,3, 1,0,3,0, 2,3,3,4, - 1,3,3,0, 2,0,3,3, 1,3,3,0, 2,0,3,4, - 1,3,3,0, 2,3,3,3, 1,3,3,0, 2,3,3,4, - 1,3,1,3, 2,3,1,3, 1,3,1,3, 2,3,4,3, - 1,1,1,3, 2,1,2,1, 2,1,2,1, 2,2,2,2, - ], -} - -// Track 9: "Alien Abyss" — 140 BPM, E minor, Metroid atmospheric tension -const track9: MusicTrack = { - name: 'Alien Abyss', bpm: 140, synthStyle: 'atmospheric', - bass: [ - 82,0,0,0, 82,0,0,0, 165,0,0,0, 82,0,165,0, - 131,0,0,0, 131,0,0,0, 262,0,0,0, 131,0,262,0, - 110,0,0,0, 110,0,0,0, 220,0,0,0, 110,0,220,0, - 123,0,0,0, 123,0,0,0, 247,0,0,0, 123,0,247,0, - 82,0,165,0, 0,0,82,0, 165,0,0,0, 82,0,165,82, - 131,0,262,0, 0,0,131,0, 262,0,0,0, 131,0,262,131, - 110,0,220,0, 0,0,110,0, 220,0,0,0, 110,0,220,110, - 123,0,247,0, 123,0,0,0, 123,0,0,0, 82,0,0,0, - ], - lead: [ - 494,0,0,659, 0,0,784,0, 0,0,659,0, 0,494,0,0, - 523,0,0,622, 0,0,784,0, 0,0,622,0, 0,523,0,0, - 440,0,0,523, 0,0,659,0, 0,0,523,0, 0,440,659,0, - 494,0,0,587, 0,0,740,0, 0,988,0,0, 740,0,587,0, - 659,0,784,0, 988,0,1319,0, 1568,0,1319,0, 988,784,659,494, - 523,0,659,0, 784,0,1047,0, 1319,0,1047,0, 784,659,523,392, - 440,0,523,0, 659,0,880,0, 1047,0,880,0, 659,523,440,330, - 494,0,587,0, 740,0,988,0, 740,0,587,0, 494,0,0,0, - ], - arp: [ - 330,392,494,0, 392,494,0,0, 330,392,494,0, 392,0,0,0, - 262,330,392,0, 330,392,0,0, 262,330,392,0, 330,0,0,0, - 220,262,330,0, 262,330,0,0, 220,262,330,0, 262,330,0,0, - 247,294,370,0, 294,370,494,0, 247,294,370,494, 294,370,494,0, - 330,494,659,494, 330,392,494,659, 784,659,494,392, 330,494,392,330, - 262,392,523,392, 262,330,392,523, 659,523,392,330, 262,392,330,262, - 220,330,440,330, 220,262,330,440, 523,440,330,262, 220,330,262,220, - 247,370,494,370, 247,294,370,494, 587,494,370,0, 0,0,0,0, - ], - chords: [[165,196,247],[262,330,392],[220,262,330],[247,294,370],[165,196,247],[196,247,294],[220,262,330],[247,294,370]], - drums: [ - 1,0,0,0, 0,0,0,0, 2,0,0,0, 0,0,0,0, - 1,0,0,0, 0,0,0,0, 2,0,0,0, 0,0,3,0, - 1,0,0,0, 0,0,3,0, 2,0,0,0, 0,0,3,0, - 1,0,0,3, 0,0,3,0, 2,0,3,0, 0,0,3,4, - 1,0,0,3, 2,0,3,0, 1,0,0,3, 2,0,3,0, - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,4, - 1,0,3,0, 2,0,3,3, 1,0,3,3, 2,0,3,4, - 1,0,3,0, 2,0,3,0, 1,0,0,0, 0,0,0,0, - ], -} - -// Track 10: "Halving Day" — Cm, 162 BPM — Triumphant climax -const track10: MusicTrack = { - name: 'Halving Day', bpm: 162, synthStyle: 'heroic', - bass: [ - 131,0,262,0, 131,0,196,0, 131,0,262,0, 131,196,262,196, - 156,0,311,0, 156,0,233,0, 156,0,311,0, 156,233,311,233, - 117,0,233,0, 117,0,175,0, 117,0,233,0, 117,175,233,175, - 98,0,196,0, 98,0,147,0, 98,0,196,0, 98,147,196,147, - 131,0,262,0, 131,0,196,0, 131,0,262,196, 131,196,262,196, - 104,0,208,0, 104,0,156,0, 104,0,208,0, 104,156,208,156, - 87,0,175,0, 87,0,131,0, 87,0,175,131, 87,131,175,131, - 98,0,196,0, 98,0,147,0, 98,0,196,0, 98,0,0,0, - ], - lead: [ - 523,0,622,784, 1047,0,784,622, 523,0,622,784, 1047,1245,1568,1245, - 622,0,784,932, 1245,0,932,784, 622,784,932,1245, 1568,1245,932,784, - 932,0,1175,1397, 932,0,698,587, 932,1175,1397,1865, 1175,932,698,587, - 784,0,988,1175, 1568,0,1175,988, 784,988,1175,1568, 1976,1568,1175,988, - 1245,1047,784,523, 622,784,1047,1245, 1568,0,1245,1047, 784,622,523,622, - 831,0,1047,1245, 831,0,622,523, 831,1047,1245,1661, 1245,1047,831,622, - 698,0,831,1047, 1397,0,1047,831, 698,831,1047,1397, 1661,1397,1047,831, - 784,988,1175,1568, 1976,0,1568,1175, 988,784,587,784, 988,1175,1568,784, - ], - arp: [ - 262,311,392,311, 262,311,392,523, 392,523,392,311, 262,392,311,262, - 311,392,466,392, 311,392,466,622, 466,622,466,392, 311,466,392,311, - 233,294,349,294, 233,294,349,466, 349,466,349,294, 233,349,294,233, - 392,494,587,494, 392,494,587,784, 587,784,587,494, 392,587,494,392, - 262,311,392,523, 392,523,622,523, 392,311,262,311, 392,523,392,311, - 208,262,311,262, 208,262,311,415, 311,415,311,262, 208,311,262,208, - 349,415,523,415, 349,415,523,698, 523,698,523,415, 349,523,415,349, - 392,494,587,494, 392,587,494,392, 587,494,392,294, 392,494,587,0, - ], - // I-vi-ii-V in Eb major (triumphant jazz-pop) - chords: [[156,196,233],[131,156,196],[175,208,262],[233,294,349],[156,196,233],[131,156,196],[175,208,262],[233,294,349]], - drums: [ - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,4,0, - 1,0,3,0, 2,0,3,3, 1,0,3,0, 2,3,3,0, 1,3,3,0, 2,3,3,3, 1,3,1,3, 2,3,4,3, - 1,3,3,0, 2,0,3,0, 1,0,3,3, 2,0,3,0, 1,0,3,0, 2,0,3,0, 1,3,3,0, 2,3,4,0, - 1,3,1,3, 2,3,3,0, 1,3,1,3, 2,3,1,3, 1,1,1,1, 2,2,2,2, 2,2,4,2, 1,0,0,0, - ], -} - -// --- D&B #1 --- -const track11: MusicTrack = { - name: 'Jungle Protocol', bpm: 174, synthStyle: 'speed', - bass: [ - 110,0,0,110, 0,0,110,0, 131,0,0,131, 0,110,0,0, - 110,0,0,110, 0,0,165,0, 131,0,0,131, 0,175,0,0, - 147,0,0,147, 0,0,147,0, 165,0,0,165, 0,147,0,0, - 110,0,0,110, 0,0,110,165, 131,0,0,131, 0,0,0,0, - 110,0,110,0, 110,0,110,131, 131,0,131,0, 131,110,0,0, - 147,0,147,0, 147,0,147,165, 165,0,165,0, 165,147,0,0, - 175,0,175,0, 175,0,175,196, 196,0,196,0, 196,175,0,0, - 110,0,110,131, 131,0,165,147, 110,0,131,0, 110,0,0,0, - ], - lead: [ - 440,0,523,0, 659,0,523,440, 0,0,523,659, 784,0,659,523, - 440,0,523,0, 659,784,659,523, 440,523,440,0, 523,659,784,0, - 587,0,698,0, 880,0,698,587, 0,0,698,880, 1047,0,880,698, - 440,523,659,784, 659,523,440,0, 523,440,0,0, 440,0,0,0, - 880,0,784,659, 523,0,440,523, 659,784,880,784, 659,523,440,0, - 1047,0,880,784, 659,0,523,659, 784,880,1047,880, 784,659,523,0, - 698,0,587,0, 698,0,784,880, 1047,880,784,698, 587,0,523,587, - 659,784,880,1047, 880,784,659,523, 440,523,659,0, 440,0,0,0, - ], - arp: [ - 440,523,659,523, 440,523,659,784, 659,523,440,523, 659,784,659,523, - 523,659,784,659, 523,659,784,1047, 784,659,523,659, 784,1047,784,659, - 587,698,880,698, 587,698,880,1047, 880,698,587,698, 880,1047,880,698, - 440,523,659,523, 440,659,523,440, 523,440,0,440, 523,659,784,0, - 880,659,440,523, 659,880,659,523, 440,523,659,880, 1047,880,659,523, - 1047,784,523,659, 784,1047,784,659, 523,659,784,1047, 1319,1047,784,659, - 698,880,1047,880, 698,880,1047,1397, 1047,880,698,880, 1047,1397,1047,880, - 659,784,880,659, 523,659,784,523, 440,523,659,440, 440,0,0,0, - ], - chords: [[220,262,330],[262,330,392],[165,196,247],[147,175,220],[220,262,330],[175,220,262],[196,247,294],[220,262,330]], - drums: [ - 1,0,3,3, 0,0,2,0, 0,3,1,0, 2,0,3,0, - 1,0,3,3, 0,0,2,3, 0,3,1,0, 2,3,0,3, - 1,0,3,3, 0,0,2,0, 0,3,1,0, 2,0,3,0, - 1,0,3,3, 0,3,2,3, 0,3,1,3, 2,3,4,3, - 1,3,0,3, 2,0,1,3, 0,3,2,0, 1,3,0,3, - 2,0,1,3, 0,3,2,3, 1,3,0,3, 2,3,1,3, - 1,3,2,3, 1,0,2,3, 1,3,2,0, 1,3,2,3, - 1,0,2,0, 1,0,2,0, 1,1,2,2, 1,2,4,0, - ], -} - -// --- D&B #2 --- -const track12: MusicTrack = { - name: 'Liquid Lightning', bpm: 172, synthStyle: 'speed', - bass: [ - 147,0,0,147, 0,0,147,0, 175,0,0,175, 0,147,0,0, - 131,0,0,131, 0,0,131,0, 147,0,0,147, 0,131,0,0, - 110,0,0,110, 0,0,110,0, 131,0,0,131, 0,110,0,0, - 98,0,0,98, 0,0,98,131, 110,0,0,110, 0,0,0,0, - 147,0,147,0, 175,0,147,175, 196,0,175,0, 147,175,0,0, - 131,0,131,0, 147,0,131,147, 165,0,147,0, 131,147,0,0, - 110,0,110,131, 131,0,147,0, 165,0,147,131, 110,0,131,0, - 98,0,110,0, 131,0,147,0, 110,0,98,0, 98,0,0,0, - ], - lead: [ - 587,0,698,784, 880,0,784,698, 587,0,698,784, 1047,0,880,784, - 523,0,587,698, 784,0,698,587, 523,0,587,698, 880,0,784,698, - 440,0,523,587, 659,0,587,523, 440,523,587,659, 784,659,587,523, - 392,0,440,523, 587,0,523,440, 392,440,523,587, 523,0,0,0, - 1175,1047,880,784, 698,587,698,784, 880,1047,1175,1047, 880,784,698,587, - 1047,880,784,698, 587,523,587,698, 784,880,1047,880, 784,698,587,523, - 880,784,698,587, 523,440,523,587, 698,784,880,784, 698,587,523,440, - 784,698,587,523, 440,523,587,698, 587,523,440,0, 392,0,0,0, - ], - arp: [ - 587,698,880,698, 587,698,880,1047, 880,698,587,698, 880,1047,880,698, - 523,659,784,659, 523,659,784,1047, 784,659,523,659, 784,1047,784,659, - 440,523,659,523, 440,523,659,880, 659,523,440,523, 659,880,659,523, - 392,494,587,494, 392,494,587,784, 587,494,392,494, 587,784,587,494, - 1047,880,698,587, 698,880,1047,880, 698,587,698,880, 1047,1397,1047,880, - 880,784,587,523, 587,784,880,784, 587,523,587,784, 880,1175,880,784, - 698,587,440,392, 440,587,698,587, 440,392,440,587, 698,1047,698,587, - 587,523,392,294, 392,523,587,523, 392,294,392,523, 587,0,0,0, - ], - chords: [[147,175,220],[131,165,196],[110,131,165],[98,123,147],[147,175,220],[131,165,196],[110,131,165],[147,175,220]], - drums: [ - 1,0,3,0, 2,0,3,3, 1,3,0,0, 2,0,3,0, - 1,0,3,0, 2,3,3,0, 0,3,1,3, 2,0,3,3, - 1,0,3,0, 2,0,3,3, 1,3,0,0, 2,0,3,0, - 1,3,3,0, 2,0,3,3, 1,3,1,3, 2,3,4,0, - 1,3,2,0, 0,3,1,0, 2,3,0,3, 1,0,2,3, - 0,3,1,3, 2,0,0,3, 1,3,2,0, 0,3,1,3, - 1,0,2,3, 1,3,2,0, 1,0,2,3, 1,3,2,3, - 1,3,1,3, 2,3,1,3, 2,2,1,1, 2,2,4,0, - ], -} - -// --- Lo-fi #1 --- -const track13: MusicTrack = { - name: 'Midnight Stack', bpm: 92, synthStyle: 'atmospheric', - bass: [ - 82,0,0,0, 0,0,82,0, 0,0,0,0, 82,0,0,0, - 73,0,0,0, 0,0,73,0, 0,0,0,0, 73,0,0,0, - 65,0,0,0, 0,0,65,0, 0,0,0,0, 65,0,73,0, - 87,0,0,0, 0,0,87,0, 0,0,0,0, 82,0,0,0, - 82,0,0,0, 82,0,0,82, 0,0,0,0, 82,0,0,0, - 73,0,0,0, 73,0,0,73, 0,0,0,0, 73,0,0,0, - 65,0,0,0, 65,0,0,65, 0,0,0,65, 73,0,82,0, - 87,0,0,0, 87,0,0,87, 0,0,0,0, 82,0,0,0, - ], - lead: [ - 330,0,0,392, 0,0,330,0, 0,294,0,0, 330,0,392,0, - 294,0,0,330, 0,0,294,0, 0,262,0,0, 294,0,330,0, - 262,0,0,330, 0,0,392,0, 0,330,0,0, 262,0,294,0, - 349,0,0,330, 0,0,294,0, 0,262,0,0, 330,0,0,0, - 659,0,0,587, 0,0,523,0, 0,494,0,0, 523,0,587,0, - 587,0,0,523, 0,0,494,0, 0,440,0,0, 494,0,523,0, - 523,0,0,587, 0,0,659,0, 0,587,0,0, 523,0,494,0, - 698,0,0,659, 0,0,587,0, 0,523,0,0, 494,0,0,0, - ], - arp: [ - 330,392,494,392, 330,0,0,0, 392,494,587,494, 392,0,0,0, - 294,349,440,349, 294,0,0,0, 349,440,523,440, 349,0,0,0, - 262,330,392,330, 262,0,0,0, 330,392,494,392, 330,0,0,0, - 349,440,523,440, 349,0,0,0, 330,392,494,392, 330,0,0,0, - 659,494,392,330, 0,0,330,392, 494,659,0,0, 587,494,392,0, - 587,440,349,294, 0,0,294,349, 440,587,0,0, 523,440,349,0, - 523,392,330,262, 0,0,262,330, 392,523,0,0, 494,392,330,0, - 698,523,440,349, 0,0,349,440, 523,698,0,0, 659,523,440,0, - ], - chords: [[165,196,247],[147,175,220],[131,165,196],[175,208,262],[165,196,247],[147,175,220],[131,165,196],[165,196,247]], - drums: [ - 1,0,0,0, 0,0,3,0, 2,0,0,0, 0,0,3,0, - 1,0,0,0, 0,0,3,0, 2,0,0,0, 0,0,0,0, - 1,0,0,0, 0,0,3,0, 2,0,0,0, 0,0,3,0, - 1,0,0,0, 0,0,3,0, 2,0,0,0, 0,3,0,0, - 1,0,3,0, 0,0,3,0, 2,0,0,0, 0,0,3,0, - 1,0,0,0, 0,0,3,0, 2,0,3,0, 0,0,0,0, - 1,0,3,0, 0,0,3,0, 2,0,0,0, 0,0,3,0, - 1,0,0,0, 0,3,3,0, 2,0,0,0, 0,0,0,0, - ], -} - -// --- Lo-fi #2 --- -const track14: MusicTrack = { - name: "Satoshi's Lullaby", bpm: 88, synthStyle: 'atmospheric', - bass: [ - 110,0,0,0, 0,0,0,0, 110,0,0,0, 0,0,110,0, - 131,0,0,0, 0,0,0,0, 131,0,0,0, 0,0,131,0, - 98,0,0,0, 0,0,0,0, 98,0,0,0, 0,0,98,0, - 87,0,0,0, 0,0,0,0, 87,0,0,0, 0,0,0,0, - 110,0,0,0, 110,0,0,0, 0,0,0,0, 110,0,0,0, - 131,0,0,0, 131,0,0,0, 0,0,0,0, 131,0,0,0, - 98,0,0,0, 98,0,0,0, 0,0,0,0, 98,0,110,0, - 87,0,0,0, 87,0,0,0, 0,0,0,0, 110,0,0,0, - ], - lead: [ - 440,0,0,523, 0,0,0,0, 440,0,0,392, 0,0,0,0, - 523,0,0,587, 0,0,0,0, 523,0,0,494, 0,0,0,0, - 392,0,0,440, 0,0,0,0, 392,0,0,349, 0,0,0,0, - 349,0,0,392, 0,0,0,0, 330,0,0,0, 0,0,0,0, - 880,0,0,784, 0,0,659,0, 587,0,0,523, 0,0,0,0, - 1047,0,0,880, 0,0,784,0, 659,0,0,587, 0,0,0,0, - 784,0,0,659, 0,0,587,0, 523,0,0,440, 0,0,494,0, - 698,0,0,659, 0,0,587,0, 523,0,0,0, 440,0,0,0, - ], - arp: [ - 220,262,330,262, 220,0,0,0, 262,330,392,330, 262,0,0,0, - 262,330,392,330, 262,0,0,0, 330,392,494,392, 330,0,0,0, - 196,247,294,247, 196,0,0,0, 247,294,349,294, 247,0,0,0, - 175,220,262,220, 175,0,0,0, 220,262,330,262, 220,0,0,0, - 440,330,262,220, 0,0,262,330, 440,0,0,0, 392,330,262,0, - 523,392,330,262, 0,0,330,392, 523,0,0,0, 494,392,330,0, - 392,294,247,196, 0,0,247,294, 392,0,0,0, 349,294,247,0, - 349,262,220,175, 0,0,220,262, 349,0,0,0, 330,262,220,0, - ], - chords: [[220,262,330],[262,330,392],[196,247,294],[175,220,262],[220,262,330],[262,330,392],[196,247,294],[220,262,330]], - drums: [ - 1,0,0,0, 0,0,0,0, 2,0,0,0, 0,0,0,0, - 1,0,0,0, 0,0,3,0, 2,0,0,0, 0,0,0,0, - 1,0,0,0, 0,0,0,0, 2,0,0,0, 0,0,0,0, - 1,0,0,0, 0,0,3,0, 2,0,0,0, 0,0,3,0, - 1,0,0,0, 0,0,3,0, 2,0,0,0, 0,0,0,0, - 1,0,0,0, 0,0,0,0, 2,0,0,0, 0,0,3,0, - 1,0,0,0, 0,0,3,0, 2,0,0,0, 0,0,0,0, - 1,0,3,0, 0,0,3,0, 2,0,0,0, 0,0,0,0, - ], -} - -// --- Ska Punk #1 --- -const track15: MusicTrack = { - name: 'Skanking Satoshi', bpm: 210, synthStyle: 'funky', - bass: [ - 196,0,196,0, 196,0,196,196, 247,0,247,0, 247,0,247,0, - 262,0,262,0, 262,0,262,262, 294,0,294,0, 294,0,294,0, - 196,0,196,0, 196,0,196,196, 175,0,175,0, 175,0,175,0, - 147,0,147,0, 147,0,147,147, 165,0,165,0, 196,0,0,0, - 196,0,196,247, 196,0,247,0, 262,0,262,294, 262,0,294,0, - 294,0,294,330, 294,0,330,0, 262,0,262,294, 262,0,247,0, - 196,0,196,247, 262,0,294,0, 247,0,247,262, 247,0,196,0, - 147,0,165,0, 175,0,196,0, 247,0,262,0, 196,0,0,0, - ], - lead: [ - 784,0,784,880, 784,0,659,0, 784,0,880,988, 880,0,784,0, - 1047,0,1047,1175, 1047,0,880,0, 1047,0,1175,1319, 1175,0,1047,0, - 784,0,784,880, 784,0,659,0, 698,0,698,784, 698,0,587,0, - 587,0,587,659, 587,0,523,0, 659,0,784,0, 784,0,0,0, - 1568,0,1319,1175, 1047,0,880,0, 1175,0,1047,880, 784,0,659,0, - 1319,0,1175,1047, 880,0,784,0, 1047,0,880,784, 659,0,587,0, - 784,0,880,1047, 1175,0,1319,0, 1175,0,1047,880, 784,0,659,0, - 587,659,784,880, 1047,0,880,784, 659,784,880,0, 784,0,0,0, - ], - arp: [ - 0,392,0,392, 0,392,0,392, 0,494,0,494, 0,494,0,494, - 0,523,0,523, 0,523,0,523, 0,587,0,587, 0,587,0,587, - 0,392,0,392, 0,392,0,392, 0,349,0,349, 0,349,0,349, - 0,294,0,294, 0,294,0,294, 0,330,0,330, 0,392,0,0, - 784,0,659,0, 784,0,880,0, 784,0,659,0, 523,0,659,0, - 1047,0,880,0, 1047,0,1175,0, 1047,0,880,0, 659,0,880,0, - 784,0,659,0, 784,0,880,1047, 880,0,784,0, 659,0,523,0, - 587,0,659,0, 784,0,880,0, 1047,0,880,0, 784,0,0,0, - ], - chords: [[196,247,294],[262,330,392],[196,247,294],[175,220,262],[147,175,220],[165,208,247],[196,247,294],[196,247,294]], - drums: [ - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,3,4,3, - 1,3,3,3, 2,3,3,3, 1,3,3,3, 2,3,3,3, - 1,3,3,3, 2,3,3,3, 1,3,3,3, 2,3,3,3, - 1,3,3,3, 2,3,3,3, 1,3,3,3, 2,3,3,3, - 1,3,3,3, 2,3,3,3, 1,1,2,2, 1,2,4,0, - ], -} - -// --- Ska Punk #2 --- -const track16: MusicTrack = { - name: 'Rudeboy Relay', bpm: 205, synthStyle: 'funky', - bass: [ - 175,0,175,0, 175,0,175,175, 220,0,220,0, 220,0,220,0, - 196,0,196,0, 196,0,196,196, 262,0,262,0, 262,0,262,0, - 175,0,175,0, 175,0,175,175, 233,0,233,0, 233,0,233,0, - 147,0,147,0, 147,0,147,147, 175,0,175,0, 175,0,0,0, - 175,0,220,0, 175,0,220,233, 262,0,233,0, 220,0,175,0, - 196,0,233,0, 196,0,262,0, 233,0,196,0, 175,0,147,0, - 175,0,220,0, 262,0,233,0, 196,0,175,0, 220,0,262,0, - 233,0,196,0, 175,0,147,0, 175,0,196,0, 175,0,0,0, - ], - lead: [ - 698,0,698,784, 698,0,587,0, 698,0,784,880, 784,0,698,0, - 784,0,784,880, 784,0,698,0, 1047,0,880,784, 880,0,784,0, - 698,0,698,784, 698,0,587,0, 932,0,880,784, 880,0,784,0, - 587,0,587,698, 587,0,523,0, 587,0,698,0, 698,0,0,0, - 1397,0,1175,1047, 880,0,784,0, 1047,0,880,784, 698,0,587,0, - 1568,0,1397,1175, 1047,0,880,0, 1175,0,1047,880, 784,0,698,0, - 698,0,784,880, 1047,0,1175,0, 1047,0,880,784, 698,0,587,0, - 523,587,698,784, 880,0,784,698, 587,698,784,0, 698,0,0,0, - ], - arp: [ - 0,349,0,349, 0,349,0,349, 0,440,0,440, 0,440,0,440, - 0,392,0,392, 0,392,0,392, 0,523,0,523, 0,523,0,523, - 0,349,0,349, 0,349,0,349, 0,466,0,466, 0,466,0,466, - 0,294,0,294, 0,294,0,294, 0,349,0,349, 0,349,0,0, - 698,0,587,0, 698,0,784,0, 698,0,587,0, 523,0,587,0, - 784,0,698,0, 784,0,880,0, 784,0,698,0, 587,0,698,0, - 698,0,587,0, 698,0,784,880, 784,0,698,0, 587,0,523,0, - 523,0,587,0, 698,0,784,0, 880,0,784,0, 698,0,0,0, - ], - chords: [[175,220,262],[196,247,294],[175,220,262],[233,294,349],[147,175,220],[175,220,262],[196,247,294],[175,220,262]], - drums: [ - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,3, - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, - 1,0,3,0, 2,0,3,0, 1,3,3,0, 2,3,4,0, - 1,3,3,3, 2,3,3,3, 1,3,3,3, 2,3,3,3, - 1,3,3,3, 2,3,3,3, 1,3,3,3, 2,3,3,3, - 1,3,3,3, 2,3,3,3, 1,3,3,3, 2,3,3,3, - 1,3,1,3, 2,3,1,3, 1,1,2,2, 2,2,4,0, - ], -} - -// --- Boss Fight #1 --- -const track17: MusicTrack = { - name: 'Final Hashrate', bpm: 145, synthStyle: 'boss', - bass: [ - 131,0,131,0, 0,0,131,0, 131,0,0,131, 0,0,131,0, - 123,0,123,0, 0,0,123,0, 123,0,0,123, 0,0,123,0, - 110,0,110,0, 0,0,110,0, 110,0,0,110, 0,0,131,0, - 98,0,98,0, 0,0,98,0, 98,0,0,98, 0,0,0,0, - 131,0,131,131, 0,131,0,131, 131,0,131,0, 131,156,175,196, - 123,0,123,123, 0,123,0,123, 123,0,123,0, 123,147,165,175, - 110,0,110,110, 0,110,0,110, 110,0,110,0, 110,131,147,165, - 98,0,98,110, 110,0,131,0, 131,0,110,98, 131,0,0,0, - ], - lead: [ - 523,0,622,0, 784,0,622,523, 0,0,622,784, 932,0,784,622, - 494,0,587,0, 740,0,587,494, 0,0,587,740, 880,0,740,587, - 440,0,523,0, 659,0,523,440, 0,0,523,659, 784,0,659,523, - 392,0,466,0, 587,0,466,392, 0,0,466,587, 523,0,0,0, - 1047,0,932,784, 622,0,523,622, 784,932,1047,932, 784,622,523,0, - 988,0,880,740, 587,0,494,587, 740,880,988,880, 740,587,494,0, - 880,0,784,659, 523,0,440,523, 659,784,880,784, 659,523,440,0, - 784,0,740,622, 523,622,740,880, 1047,932,784,622, 523,0,0,0, - ], - arp: [ - 262,311,392,311, 262,311,392,523, 392,311,262,311, 392,523,392,311, - 247,294,370,294, 247,294,370,494, 370,294,247,294, 370,494,370,294, - 220,262,330,262, 220,262,330,440, 330,262,220,262, 330,440,330,262, - 196,233,294,233, 196,233,294,392, 294,233,196,233, 294,392,294,233, - 523,392,311,262, 311,392,523,622, 523,392,311,262, 392,523,622,784, - 494,370,294,247, 294,370,494,587, 494,370,294,247, 370,494,587,740, - 440,330,262,220, 262,330,440,523, 440,330,262,220, 330,440,523,659, - 392,294,233,196, 233,294,392,466, 392,294,233,196, 262,0,0,0, - ], - chords: [[131,156,196],[123,147,175],[110,131,165],[98,123,147],[131,156,196],[123,147,175],[110,131,165],[131,156,196]], - drums: [ - 1,0,0,0, 2,0,0,0, 1,0,3,0, 2,0,3,0, - 1,0,0,0, 2,0,0,0, 1,0,3,0, 2,0,3,4, - 1,0,0,0, 2,0,0,0, 1,0,3,0, 2,0,3,0, - 1,0,3,0, 2,0,3,0, 1,3,1,3, 2,2,4,4, - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, - 1,0,3,0, 2,0,3,4, 1,0,3,0, 2,0,3,0, - 1,3,1,0, 2,0,3,0, 1,3,1,0, 2,0,3,0, - 1,3,1,3, 2,3,1,3, 1,1,2,2, 4,4,4,4, - ], -} - -// --- Boss Fight #2 --- -const track18: MusicTrack = { - name: 'Whale Alert', bpm: 155, synthStyle: 'boss', - bass: [ - 117,0,117,0, 0,0,117,0, 117,0,0,117, 0,0,117,0, - 110,0,110,0, 0,0,110,0, 110,0,0,110, 0,0,110,0, - 87,0,87,0, 0,0,87,0, 87,0,0,87, 0,0,87,0, - 98,0,98,0, 0,0,98,0, 98,0,0,98, 0,0,0,0, - 117,0,117,117, 0,117,0,117, 117,0,117,0, 117,131,147,175, - 110,0,110,110, 0,110,0,110, 110,0,110,0, 110,123,131,147, - 87,0,87,87, 0,87,0,87, 87,0,87,0, 87,98,110,117, - 98,0,98,110, 117,0,131,0, 117,0,98,87, 117,0,0,0, - ], - lead: [ - 466,0,554,0, 698,0,554,466, 0,0,554,698, 880,0,698,554, - 440,0,523,0, 659,0,523,440, 0,0,523,659, 831,0,659,523, - 349,0,415,0, 523,0,415,349, 0,0,415,523, 698,0,523,415, - 392,0,466,0, 587,0,466,392, 0,0,466,587, 466,0,0,0, - 932,0,880,698, 554,0,466,554, 698,880,932,880, 698,554,466,0, - 880,0,831,659, 523,0,440,523, 659,831,880,831, 659,523,440,0, - 698,0,659,523, 415,0,349,415, 523,659,698,659, 523,415,349,0, - 784,0,698,587, 466,554,698,880, 932,880,698,554, 466,0,0,0, - ], - arp: [ - 233,277,349,277, 233,277,349,466, 349,277,233,277, 349,466,349,277, - 220,262,330,262, 220,262,330,440, 330,262,220,262, 330,440,330,262, - 175,208,262,208, 175,208,262,349, 262,208,175,208, 262,349,262,208, - 196,233,294,233, 196,233,294,392, 294,233,196,233, 294,392,294,233, - 466,349,277,233, 277,349,466,554, 466,349,277,233, 349,466,554,698, - 440,330,262,220, 262,330,440,523, 440,330,262,220, 330,440,523,659, - 349,262,208,175, 208,262,349,415, 349,262,208,175, 262,349,415,523, - 392,294,233,196, 233,294,392,466, 392,294,233,196, 233,0,0,0, - ], - chords: [[117,139,175],[110,131,165],[87,110,131],[98,117,147],[117,139,175],[110,131,165],[87,110,131],[117,139,175]], - drums: [ - 1,0,3,0, 2,0,0,0, 1,0,3,0, 2,0,3,0, - 1,0,3,0, 2,0,0,0, 1,0,3,0, 2,0,3,4, - 1,0,3,0, 2,0,0,0, 1,0,3,0, 2,0,3,0, - 1,0,3,0, 2,3,3,0, 1,3,1,3, 2,2,4,4, - 1,0,3,0, 1,0,3,0, 2,0,3,0, 1,0,3,0, - 1,0,3,4, 1,0,3,0, 2,0,3,0, 1,0,3,0, - 1,3,1,3, 2,0,3,0, 1,3,1,3, 2,0,3,0, - 1,3,1,3, 2,3,1,3, 1,1,2,2, 4,4,4,4, - ], -} - -// --- Chiptune #1 --- -const track19: MusicTrack = { - name: '8-Bit Blockwar', bpm: 190, synthStyle: 'bouncy', - bass: [ - 165,0,165,0, 165,0,330,0, 165,0,165,0, 247,0,165,0, - 196,0,196,0, 196,0,392,0, 196,0,196,0, 294,0,196,0, - 220,0,220,0, 220,0,440,0, 220,0,220,0, 330,0,220,0, - 147,0,147,0, 147,0,294,0, 147,0,147,0, 220,0,0,0, - 165,165,0,165, 0,165,330,165, 196,196,0,196, 0,196,392,196, - 220,220,0,220, 0,220,440,220, 147,147,0,147, 0,147,294,147, - 165,0,196,0, 220,0,247,0, 262,0,294,0, 330,0,349,0, - 165,0,247,0, 330,0,247,0, 165,0,247,0, 165,0,0,0, - ], - lead: [ - 659,0,784,0, 988,0,784,659, 784,988,1319,988, 784,0,659,0, - 784,0,988,0, 1175,0,988,784, 988,1175,1568,1175, 988,0,784,0, - 880,0,1047,0, 1319,0,1047,880, 1047,1319,1760,1319, 1047,0,880,0, - 587,0,698,0, 880,0,698,587, 698,880,1175,880, 698,0,0,0, - 1319,1175,988,784, 659,784,988,1175, 1319,0,1175,988, 784,659,784,988, - 1568,1319,1175,988, 784,988,1175,1319, 1568,0,1319,1175, 988,784,988,1175, - 1760,1568,1319,1175, 988,1175,1319,1568, 1760,0,1568,1319, 1175,988,784,0, - 1175,988,784,659, 784,988,1175,1319, 1568,1319,1175,988, 659,0,0,0, - ], - arp: [ - 330,392,494,659, 494,392,330,392, 494,659,494,392, 330,494,659,330, - 392,494,587,784, 587,494,392,494, 587,784,587,494, 392,587,784,392, - 440,523,659,880, 659,523,440,523, 659,880,659,523, 440,659,880,440, - 294,349,440,587, 440,349,294,349, 440,587,440,349, 294,440,587,0, - 659,494,330,494, 659,988,659,494, 330,494,659,988, 1319,988,659,494, - 784,587,392,587, 784,1175,784,587, 392,587,784,1175, 1568,1175,784,587, - 880,659,440,659, 880,1319,880,659, 440,659,880,1319, 1760,1319,880,659, - 587,440,294,440, 587,880,587,440, 294,440,587,880, 659,0,0,0, - ], - chords: [[165,208,247],[196,247,294],[220,262,330],[147,175,220],[165,208,247],[196,247,294],[220,262,330],[165,208,247]], - drums: [ - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,3, - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, - 1,0,3,0, 2,0,3,3, 1,0,3,3, 2,3,4,0, - 1,0,3,3, 2,0,3,3, 1,0,3,3, 2,0,3,3, - 1,0,3,3, 2,0,3,3, 1,0,3,3, 2,3,4,3, - 1,3,1,3, 2,3,1,3, 1,3,1,3, 2,3,1,3, - 1,3,1,3, 2,3,1,3, 1,1,2,2, 1,2,4,0, - ], -} - -// --- Chiptune #2 --- -const track20: MusicTrack = { - name: 'Pixel Proof', bpm: 185, synthStyle: 'bouncy', - bass: [ - 220,0,220,0, 220,0,440,0, 220,0,220,0, 330,0,220,0, - 196,0,196,0, 196,0,392,0, 196,0,196,0, 294,0,196,0, - 175,0,175,0, 175,0,349,0, 175,0,175,0, 262,0,175,0, - 165,0,165,0, 165,0,330,0, 165,0,165,0, 247,0,0,0, - 220,220,0,220, 0,220,440,220, 196,196,0,196, 0,196,392,196, - 175,175,0,175, 0,175,349,175, 165,165,0,165, 0,165,330,165, - 220,0,196,0, 175,0,165,0, 175,0,196,0, 220,0,247,0, - 220,0,330,0, 440,0,330,0, 220,0,330,0, 220,0,0,0, - ], - lead: [ - 880,0,1047,0, 1319,0,1047,880, 1047,1319,1760,1319, 1047,0,880,0, - 784,0,932,0, 1175,0,932,784, 932,1175,1568,1175, 932,0,784,0, - 698,0,880,0, 1047,0,880,698, 880,1047,1397,1047, 880,0,698,0, - 659,0,784,0, 988,0,784,659, 784,988,1319,988, 784,0,0,0, - 1760,1568,1319,1047, 880,1047,1319,1568, 1760,0,1568,1319, 1047,880,1047,1319, - 1568,1397,1175,932, 784,932,1175,1397, 1568,0,1397,1175, 932,784,932,1175, - 1397,1319,1047,880, 698,880,1047,1319, 1397,0,1319,1047, 880,698,880,0, - 1319,1175,988,784, 659,784,988,1175, 1319,1175,988,784, 880,0,0,0, - ], - arp: [ - 440,523,659,880, 659,523,440,523, 659,880,659,523, 440,659,880,440, - 392,494,587,784, 587,494,392,494, 587,784,587,494, 392,587,784,392, - 349,440,523,698, 523,440,349,440, 523,698,523,440, 349,523,698,349, - 330,415,494,659, 494,415,330,415, 494,659,494,415, 330,494,659,0, - 880,659,440,523, 659,880,1319,880, 659,523,659,880, 1319,1760,1319,880, - 784,587,392,494, 587,784,1175,784, 587,494,587,784, 1175,1568,1175,784, - 698,523,349,440, 523,698,1047,698, 523,440,523,698, 1047,1397,1047,698, - 659,494,330,415, 494,659,988,659, 494,415,494,659, 880,0,0,0, - ], - chords: [[220,277,330],[196,247,294],[175,220,262],[165,208,247],[220,277,330],[196,247,294],[175,220,262],[220,277,330]], - drums: [ - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, - 1,0,3,0, 2,0,3,3, 1,0,3,0, 2,0,3,0, - 1,0,3,0, 2,0,3,0, 1,0,3,0, 2,0,3,0, - 1,0,3,0, 2,0,3,3, 1,3,3,0, 2,3,4,0, - 1,0,3,3, 2,0,3,3, 1,0,3,3, 2,0,3,3, - 1,0,3,3, 2,0,3,3, 1,3,3,3, 2,3,4,3, - 1,3,1,3, 2,0,3,3, 1,3,1,3, 2,0,3,3, - 1,3,1,3, 2,3,1,3, 1,1,2,2, 2,2,4,0, - ], -} - -const ALL_TRACKS = [track1, track2, track3, track4, track5, track6, track7, track8, track9, track10, track11, track12, track13, track14, track15, track16, track17, track18, track19, track20] -let activeTrack: MusicTrack = track1 -let barIndex = 0 - - -function playMusicBar() { - if (!musicPlaying || !musicGain) return - const c = getCtx() - getMusicDelay() // ensure delay is created - - // Dynamic tempo: subtle intensity shift (+8 BPM at max) - const dynamicBpm = activeTrack.bpm + currentIntensity * 8 - const beat = 60 / dynamicBpm - const now = c.currentTime + 0.05 - const bar = barIndex % BARS - const off = bar * STEPS - const step = beat / 2 - - // === VERSE / CHORUS DYNAMICS === - // Bars 0-3 are "verse" (sparser), bars 4-7 are "chorus" (climactic, full) - const isChorus = bar >= 4 - const leadThreshold = isChorus ? 0.15 : 0.4 - const arpThreshold = isChorus ? 0.25 : 0.55 - - // Switch tracks at musical boundaries — NEVER repeat the same track - const switchEvery = currentIntensity > 0.7 ? 8 : currentIntensity > 0.4 ? 16 : 24 - if (barIndex > 0 && barIndex % switchEvery === 0) { - const prevTrack = activeTrack - // Always pick from ALL tracks, excluding current — guarantees no repeat - const others = ALL_TRACKS.filter(t => t !== activeTrack) - // Prefer tracks matching intensity mood - const chill = others.filter(t => t.bpm < 155) - const mid = others.filter(t => t.bpm >= 155 && t.bpm < 168) - const intense = others.filter(t => t.bpm >= 168) - if (currentIntensity > 0.7 && intense.length > 0) { - activeTrack = intense[Math.floor(Math.random() * intense.length)] - } else if (currentIntensity < 0.3 && chill.length > 0) { - activeTrack = chill[Math.floor(Math.random() * chill.length)] - } else if (mid.length > 0 && Math.random() < 0.5) { - activeTrack = mid[Math.floor(Math.random() * mid.length)] - } else { - activeTrack = others[Math.floor(Math.random() * others.length)] - } - // Smooth crossfade on track switch - if (activeTrack !== prevTrack && musicGain) { - const curVol = musicGain.gain.value - musicGain.gain.setValueAtTime(curVol, now) - musicGain.gain.linearRampToValueAtTime(curVol * 0.25, now + 0.08) - musicGain.gain.linearRampToValueAtTime(curVol, now + 0.4) - } - } - - // Chord pad for the whole bar — style-specific pad synth - const barDur = STEPS * step - const padFreqs = activeTrack.chords[bar].map(f => f * 2) - STYLE_PAD[activeTrack.synthStyle](padFreqs, barDur, musicGain, now) - - for (let i = 0; i < STEPS; i++) { - const t = now + i * step - const idx = off + i - const nd = step - 0.01 - - // Layer 1: Bass — style-specific synth - if (activeTrack.bass[idx] > 0) STYLE_BASS[activeTrack.synthStyle](activeTrack.bass[idx], nd, musicGain, t) - - // Layer 2: Lead — style-specific synth; verse: needs higher intensity; chorus: plays freely - if (activeTrack.lead[idx] > 0 && currentIntensity > leadThreshold) { - STYLE_LEAD[activeTrack.synthStyle](activeTrack.lead[idx], nd * 0.8, musicGain, t) - } - - // Layer 3: Arpeggio — style-specific waveform; verse: needs high intensity; chorus: plays freely - if (activeTrack.arp[idx] > 0 && currentIntensity > arpThreshold) { - chorusTone(activeTrack.arp[idx], STYLE_ARP_TYPE[activeTrack.synthStyle], nd * 0.6, musicGain, t, 0.04 + currentIntensity * 0.04) - } - - // Layer 4: Drums — intensity controls density - const d = activeTrack.drums[idx] - if (d === 1) kick(musicGain, t) - if (d === 2 && currentIntensity > 0.2) snare(musicGain, t) - if (d === 3) hihat(musicGain, t, false) - if (d === 4 && currentIntensity > 0.4) hihat(musicGain, t, true) - - // Chorus: extra snare ghost notes on off-beats for energy - if (isChorus && currentIntensity > 0.6 && d === 0 && i % 4 === 2 && Math.random() < 0.3) { - snare(musicGain, t) - } - - // Extra open hihat at high intensity (musical, not chaotic) - if (currentIntensity > 0.85 && i === 14 && Math.random() < 0.3) { - hihat(musicGain, t, true) - } - } - - barIndex++ - musicTimeout = window.setTimeout(playMusicBar, barDur * 1000 - 50) -} - -export function startMusic() { - getCtx() - if (musicPlaying) return - // Pick a random track each time - activeTrack = ALL_TRACKS[Math.floor(Math.random() * ALL_TRACKS.length)] - musicPlaying = true - barIndex = 0 - playMusicBar() -} - -export function stopMusic() { - musicPlaying = false - if (musicTimeout) { - clearTimeout(musicTimeout) - musicTimeout = null - } - // Clean up delay - if (delayNode) { delayNode.disconnect(); delayNode = null } - if (delayGain) { delayGain.disconnect(); delayGain = null } -} - -export function setMusicVolume(v: number) { - if (musicGain) musicGain.gain.value = Math.max(0, Math.min(1, v)) -} - -export function setSfxVolume(v: number) { - if (sfxGain) sfxGain.gain.value = Math.max(0, Math.min(1, v)) -} - -let masterMuted = false -const MUSIC_VOL = 0.12 -const SFX_VOL = 0.25 - -export function setMasterMute(muted: boolean) { - masterMuted = muted - if (muted) { - if (musicGain) musicGain.gain.value = 0 - if (sfxGain) sfxGain.gain.value = 0 - kokoroStop() - if (typeof speechSynthesis !== 'undefined') speechSynthesis.cancel() - } else { - if (musicGain) musicGain.gain.value = MUSIC_VOL - if (sfxGain) sfxGain.gain.value = SFX_VOL - } -} - -export function isMasterMuted(): boolean { - return masterMuted -} - -let _speechUnlocked = false -export async function ensureAudioContext() { - const c = getCtx() - if (c.state === 'suspended') { - try { await c.resume() } catch {} - } - // Share AudioContext with kokoro and start loading the model. - // Defer initKokoro so the click handler finishes first — the 4.8MB module - // parse + 86MB model download happen after the UI is unblocked. - setAudioContext(c) - setTimeout(() => initKokoro(), 0) - // Prime speech synthesis as fallback — mobile browsers require - // a speak() call inside a user gesture to unlock speechSynthesis - if (typeof speechSynthesis !== 'undefined') { - if (!voicesLoaded) loadVoices() - if (!_speechUnlocked) { - _speechUnlocked = true - const unlock = new SpeechSynthesisUtterance('') - unlock.volume = 0 - speechSynthesis.speak(unlock) - } - } -} - -// === CROWD SOUNDS === -// Procedural crowd reactions using layered noise + filtered tones - -export function sfxCrowdOoh() { - const c = getCtx() - const d = getSfxDest() - const t = c.currentTime - // Rising "ooh" — filtered noise sweep - for (let i = 0; i < 3; i++) { - const osc = c.createOscillator() - osc.type = 'sine' - osc.frequency.setValueAtTime(200 + i * 60, t) - osc.frequency.linearRampToValueAtTime(350 + i * 80, t + 0.4) - const g = c.createGain() - g.gain.setValueAtTime(0.06, t) - g.gain.linearRampToValueAtTime(0.12, t + 0.15) - g.gain.exponentialRampToValueAtTime(0.001, t + 0.6) - osc.connect(g); g.connect(d) - osc.start(t + i * 0.03); osc.stop(t + 0.6) - } - noise(0.3, d, t) -} - -export function sfxCrowdGasp() { - const c = getCtx() - const d = getSfxDest() - const t = c.currentTime - // Sharp intake — high noise burst + quick sine chirps - const bufSize = Math.max(1, Math.floor(c.sampleRate * 0.15)) - const buf = c.createBuffer(1, bufSize, c.sampleRate) - const data = buf.getChannelData(0) - for (let i = 0; i < bufSize; i++) data[i] = Math.random() * 2 - 1 - const src = c.createBufferSource(); src.buffer = buf - const bp = c.createBiquadFilter(); bp.type = 'bandpass'; bp.frequency.value = 2000; bp.Q.value = 0.8 - const g = c.createGain() - g.gain.setValueAtTime(0.18, t) - g.gain.exponentialRampToValueAtTime(0.001, t + 0.2) - src.connect(bp); bp.connect(g); g.connect(d) - src.start(t); src.stop(t + 0.2) - // Multiple pitched gasps - for (let i = 0; i < 4; i++) { - const osc = c.createOscillator() - osc.type = 'sine' - osc.frequency.value = 400 + Math.random() * 300 - const og = c.createGain() - og.gain.setValueAtTime(0.04, t + i * 0.02) - og.gain.exponentialRampToValueAtTime(0.001, t + 0.25) - osc.connect(og); og.connect(d) - osc.start(t + i * 0.02); osc.stop(t + 0.25) - } -} - -export function sfxCrowdCheer() { - const c = getCtx() - const d = getSfxDest() - const t = c.currentTime - // Layered noise + sine clusters = roaring crowd - for (let layer = 0; layer < 3; layer++) { - const bufSize = Math.max(1, Math.floor(c.sampleRate * 0.8)) - const buf = c.createBuffer(1, bufSize, c.sampleRate) - const data = buf.getChannelData(0) - for (let i = 0; i < bufSize; i++) data[i] = Math.random() * 2 - 1 - const src = c.createBufferSource(); src.buffer = buf - const bp = c.createBiquadFilter(); bp.type = 'bandpass' - bp.frequency.value = 600 + layer * 400; bp.Q.value = 0.5 - const g = c.createGain() - g.gain.setValueAtTime(0.01, t) - g.gain.linearRampToValueAtTime(0.1, t + 0.15) - g.gain.setValueAtTime(0.1, t + 0.5) - g.gain.exponentialRampToValueAtTime(0.001, t + 0.8) - src.connect(bp); bp.connect(g); g.connect(d) - src.start(t + layer * 0.05); src.stop(t + 0.8) - } -} - -export function sfxApplause() { - const c = getCtx() - const d = getSfxDest() - const t = c.currentTime - // Crackling filtered noise = many hands clapping - for (let burst = 0; burst < 6; burst++) { - const delay = burst * 0.12 + Math.random() * 0.05 - const bufSize = Math.max(1, Math.floor(c.sampleRate * 0.06)) - const buf = c.createBuffer(1, bufSize, c.sampleRate) - const data = buf.getChannelData(0) - for (let i = 0; i < bufSize; i++) data[i] = Math.random() * 2 - 1 - const src = c.createBufferSource(); src.buffer = buf - const hp = c.createBiquadFilter(); hp.type = 'highpass'; hp.frequency.value = 3000 + Math.random() * 2000 - const g = c.createGain() - g.gain.setValueAtTime(0.08 + Math.random() * 0.04, t + delay) - g.gain.exponentialRampToValueAtTime(0.001, t + delay + 0.08) - src.connect(hp); hp.connect(g); g.connect(d) - src.start(t + delay); src.stop(t + delay + 0.08) - } -} - -export function sfxDrumRoll() { - const c = getCtx() - const d = getSfxDest() - const t = c.currentTime - // Rapid snare hits building in intensity - const hits = 16 - for (let i = 0; i < hits; i++) { - const hitTime = t + i * 0.04 - const vol = 0.05 + (i / hits) * 0.15 - const bufSize = Math.max(1, Math.floor(c.sampleRate * 0.03)) - const buf = c.createBuffer(1, bufSize, c.sampleRate) - const data = buf.getChannelData(0) - for (let i2 = 0; i2 < bufSize; i2++) data[i2] = Math.random() * 2 - 1 - const src = c.createBufferSource(); src.buffer = buf - const g = c.createGain() - g.gain.setValueAtTime(vol, hitTime) - g.gain.exponentialRampToValueAtTime(0.001, hitTime + 0.04) - src.connect(g); g.connect(d) - src.start(hitTime); src.stop(hitTime + 0.04) - // Body tone - tone(180 + i * 5, 'triangle', 0.025, d, hitTime) - } -} - -export function announceCrowdReaction(type: 'cheer' | 'gasp' | 'ooh' | 'applause') { - const fns = { cheer: sfxCrowdCheer, gasp: sfxCrowdGasp, ooh: sfxCrowdOoh, applause: sfxApplause } - fns[type]() -} - -// === MUSIC INTENSITY === -// Dynamically adjust music volume/energy based on fight state - -let currentIntensity = 0.5 - -export function setMusicIntensity(level: number) { - // level: 0.0 (calm) to 1.0 (maximum hype) - currentIntensity = Math.max(0, Math.min(1, level)) - if (!musicGain) return - const baseVol = 0.06 - const maxVol = 0.18 - const targetVol = baseVol + (maxVol - baseVol) * currentIntensity - const c = getCtx() - musicGain.gain.cancelScheduledValues(c.currentTime) - musicGain.gain.setTargetAtTime(targetVol, c.currentTime, 0.3) -}