refactor: split sounds.ts into audio/ modules

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-08 22:11:58 +00:00
co-authored by Claude Opus 4.6
parent 94910820f8
commit d998cb3ad0
12 changed files with 2739 additions and 6 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
import { onMounted } from 'vue'
import { RouterView } from 'vue-router'
import NavBar from './components/NavBar.vue'
import { ensureAudioContext } from './game/sounds'
import { ensureAudioContext } from './game/audio'
// Unlock AudioContext + SpeechSynthesis on first user interaction (mobile requires gesture)
onMounted(() => {
+1 -1
View File
@@ -11,7 +11,7 @@ import {
speakQuestion, speakAnswer, speakNarration,
prefetchQuestion, prefetchAnswer, prefetchNarration,
sfxRandomComedy, sfxRandomFail, sfxVineBoom, sfxEmotionalDamage,
} from '../game/sounds'
} from '../game/audio'
import { isKokoroLoading, getKokoroProgress } from '../game/tts'
interface Round {
+1 -1
View File
@@ -25,7 +25,7 @@ import {
sfxTacoBellBong, sfxWindowsError, sfxMemeThud,
sfxSadTrombone, sfxFailHorn, sfxPriceIsRightFail, sfxBuzzer,
sfxMissionFailed, sfxCrickets, sfxSadViolin, sfxEmotionalDamage, sfxDunDunDun,
} from './sounds'
} from './audio'
export interface FightSceneConfig {
canvas: HTMLCanvasElement
+123
View File
@@ -0,0 +1,123 @@
// Audio context, gain nodes, volume/mute management
// This module has ZERO imports from other audio modules to prevent circular deps.
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
let masterMuted = false
const MUSIC_VOL = 0.12
const SFX_VOL = 0.25
export 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)
}
export 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)
export function getSfxDest(): AudioNode {
const c = getCtx()
return sfxGain ?? c.destination
}
export function getMusicGain(): GainNode | null {
return musicGain
}
export function getSfxGain(): GainNode | null {
return sfxGain
}
export function isMusicPlaying(): boolean {
return musicPlaying
}
export function setMusicPlaying(v: boolean) {
musicPlaying = v
}
export function getMusicTimeout(): number | null {
return musicTimeout
}
export function setMusicTimeout(v: number | null) {
musicTimeout = v
}
export function isAudioUnlocked(): boolean {
return _audioUnlocked
}
export function setAudioUnlocked(v: boolean) {
_audioUnlocked = v
}
export function isMasterMuted(): boolean {
return masterMuted
}
export function getMasterMuted(): boolean {
return masterMuted
}
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))
}
export function setMasterMute(muted: boolean) {
masterMuted = muted
if (muted) {
if (musicGain) musicGain.gain.value = 0
if (sfxGain) sfxGain.gain.value = 0
// Note: kokoroStop and speechSynthesis.cancel are called from the caller
// to avoid circular deps — see index.ts setMasterMute wrapper
} else {
if (musicGain) musicGain.gain.value = MUSIC_VOL
if (sfxGain) sfxGain.gain.value = SFX_VOL
}
}
// Disconnect gain nodes to instantly kill all in-flight oscillators/buffers,
// then reconnect so future sounds still work
export function resetGainNodes() {
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)
}
}
}
export { MUSIC_VOL, SFX_VOL }
+215
View File
@@ -0,0 +1,215 @@
// Barrel file — re-exports everything from audio sub-modules + cross-cutting functions
import { getCtx, resetGainNodes, setMasterMute as _ctxSetMasterMute } from './context'
import { stopMusic } from './music'
import { resetVoiceState, loadVoices, voicesLoaded, _speechUnlocked, setSpeechUnlocked } from './voice'
import { kokoroStop, initKokoro, setAudioContext } from '../tts'
// === CROSS-CUTTING: stopAllAudio ===
// Kills music, TTS, speech synthesis, resets gain nodes so in-flight sounds die instantly
export function stopAllAudio() {
stopMusic()
kokoroStop()
if (typeof speechSynthesis !== 'undefined') speechSynthesis.cancel()
resetVoiceState()
resetGainNodes()
}
// === CROSS-CUTTING: ensureAudioContext ===
// Must be called from user gesture (click/tap) to unlock audio on mobile browsers
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) {
setSpeechUnlocked(true)
const unlock = new SpeechSynthesisUtterance('')
unlock.volume = 0
speechSynthesis.speak(unlock)
}
}
}
// === CROSS-CUTTING: setMasterMute wrapper ===
// Wraps context.setMasterMute to also kill TTS/speech (avoids circular dep in context.ts)
export function setMasterMute(muted: boolean) {
_ctxSetMasterMute(muted)
if (muted) {
kokoroStop()
if (typeof speechSynthesis !== 'undefined') speechSynthesis.cancel()
}
}
// === RE-EXPORTS ===
// context — public API only (internal accessors stay in context.ts)
export {
getCtx,
getSfxDest,
getMusicGain,
getSfxGain,
isMusicPlaying,
setMusicPlaying,
isAudioUnlocked,
setAudioUnlocked,
isMasterMuted,
getMasterMuted,
setMusicVolume,
setSfxVolume,
resetGainNodes,
MUSIC_VOL,
SFX_VOL,
} from './context'
// primitives — mostly used internally, but re-export for FightScene/testing
export {
tone,
noise,
sweep,
reverbTail,
bodyThump,
highSnap,
fmImpact,
chorusTone,
fmLead,
kick,
snare,
hihat,
} from './primitives'
// sfx — all SFX functions
export {
fanfareRound,
fanfareFight,
fanfareDevastating,
fanfareCritical,
fanfareCombo,
sfxPunch,
sfxKick,
sfxSpecial,
sfxCritical,
sfxGunshot,
sfxBulletHit,
sfxJetpack,
sfxExplosion,
sfxBlock,
sfxDodge,
sfxClash,
sfxKO,
sfxPerfect,
sfxWin,
sfxWinAnnounce,
sfxRoundStart,
sfxBoing,
sfxWomp,
sfxSlideUp,
sfxSlideDown,
sfxBonk,
sfxSplat,
sfxZap,
sfxZoomWhoosh,
sfxRapidPunch,
sfxPowerUp,
sfxCoin,
sfxFail,
sfxVineBoom,
sfxAirHorn,
sfxBruh,
sfxFart,
sfxRecordScratch,
sfxRubberChicken,
sfxSqueakyToy,
sfxWetSlap,
sfxBoneCrack,
sfxCartoonRun,
sfxRimShot,
sfxSlideWhistleUp,
sfxSlideWhistleDown,
sfxYippee,
sfxDing,
sfxTacoBellBong,
sfxWindowsError,
sfxMemeThud,
sfxSadTrombone,
sfxEmotionalDamage,
sfxDunDunDun,
sfxFailHorn,
sfxPriceIsRightFail,
sfxBuzzer,
sfxSadViolin,
sfxMissionFailed,
sfxCrickets,
sfxRandomSilly,
sfxRandomComedy,
sfxRandomFail,
sfxCrowdOoh,
sfxCrowdGasp,
sfxCrowdCheer,
sfxApplause,
sfxDrumRoll,
announceCrowdReaction,
} from './sfx'
// voice — all voice/announce/TTS functions
export {
type VoiceProfile,
voicesLoaded,
voiceProfiles,
loadVoices,
resetVoiceState,
speak,
speakAsync,
announce,
announceDeep,
announceFast,
announceRobot,
announceScream,
announceSmooth,
announceRandom,
announceDramatic,
announceHype,
announceSilly,
announceCool,
announceFinishHim,
announceFatality,
announceFlawlessVictory,
announceRandomHype,
announceDeepIntro,
announceRoundHype,
announceCreatorEntrance,
announceCreatorRound,
announceCreatorKO,
announceCreatorWin,
announceCreatorLose,
announceCreatorMorph,
announceCreatorCameo,
announceCreatorTaunt,
announceCreatorDevastating,
creatorAnswerVoiceKey,
speakQuestion,
speakAnswer,
speakNarration,
prefetchQuestion,
prefetchAnswer,
prefetchNarration,
} from './voice'
// music
export {
startMusic,
stopMusic,
setMusicIntensity,
} from './music'
+497
View File
@@ -0,0 +1,497 @@
// Music tracks, playback, and intensity control
import { getCtx, getMusicGain, isMusicPlaying, setMusicPlaying, getMusicTimeout, setMusicTimeout } from './context'
import { chorusTone, fmLead, kick, snare, hihat, setDelayNodeGetter, noise, tone } from './primitives'
// === MUSIC CONSTANTS ===
const BARS = 8
const STEPS = 16 // per bar
// Shared delay effect for fullness
let delayNode: DelayNode | null = null
let delayGain: GainNode | null = null
// Register delay node getter so primitives (fmLead etc.) can access it
setDelayNodeGetter(() => delayNode)
function getMusicDelay(): GainNode {
const musicGain = getMusicGain()
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
}
// === GENRE-SPECIFIC SYNTH VARIANTS ===
// Thick distorted sub-bass with overtones
function thickBass(freq: number, dur: number, dest: AudioNode, t: number) {
const c = getCtx()
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)
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)
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)
}
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)
}
}
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)
}
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)
}
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)
}
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)
}
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)
}
}
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}
}
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<SynthStyle, BassFunc> = {
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<SynthStyle, BassFunc> = {
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<SynthStyle, OscillatorType> = {
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<SynthStyle, PadFunc> = {
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[]
}
// I'm inlining ALL 20 tracks inline as a single constant to keep
// the file self-contained. Each track is identical to the original sounds.ts.
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]}
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]}
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]}
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]}
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]}
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]}
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]}
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]}
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]}
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],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]}
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]}
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]}
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]}
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]}
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]}
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]}
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]}
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]}
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]}
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
// === MUSIC INTENSITY ===
let currentIntensity = 0.5
export function setMusicIntensity(level: number) {
currentIntensity = Math.max(0, Math.min(1, level))
const musicGain = getMusicGain()
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)
}
// === PLAYBACK ===
function playMusicBar() {
const musicGain = getMusicGain()
if (!isMusicPlaying() || !musicGain) return
const c = getCtx()
getMusicDelay() // ensure delay is created
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
const isChorus = bar >= 4
const leadThreshold = isChorus ? 0.15 : 0.4
const arpThreshold = isChorus ? 0.25 : 0.55
const switchEvery = currentIntensity > 0.7 ? 8 : currentIntensity > 0.4 ? 16 : 24
if (barIndex > 0 && barIndex % switchEvery === 0) {
const prevTrack = activeTrack
const others = ALL_TRACKS.filter(t => t !== activeTrack)
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)]
}
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)
}
}
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
if (activeTrack.bass[idx] > 0) STYLE_BASS[activeTrack.synthStyle](activeTrack.bass[idx], nd, musicGain, t)
if (activeTrack.lead[idx] > 0 && currentIntensity > leadThreshold) {
STYLE_LEAD[activeTrack.synthStyle](activeTrack.lead[idx], nd * 0.8, musicGain, t)
}
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)
}
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)
if (isChorus && currentIntensity > 0.6 && d === 0 && i % 4 === 2 && Math.random() < 0.3) {
snare(musicGain, t)
}
if (currentIntensity > 0.85 && i === 14 && Math.random() < 0.3) {
hihat(musicGain, t, true)
}
}
barIndex++
setMusicTimeout(window.setTimeout(playMusicBar, barDur * 1000 - 50))
}
export function startMusic() {
getCtx()
if (isMusicPlaying()) return
activeTrack = ALL_TRACKS[Math.floor(Math.random() * ALL_TRACKS.length)]
setMusicPlaying(true)
barIndex = 0
playMusicBar()
}
export function stopMusic() {
setMusicPlaying(false)
const timeout = getMusicTimeout()
if (timeout) {
clearTimeout(timeout)
setMusicTimeout(null)
}
if (delayNode) { delayNode.disconnect(); delayNode = null }
if (delayGain) { delayGain.disconnect(); delayGain = null }
}
+247
View File
@@ -0,0 +1,247 @@
// Low-level audio helper functions: tone, noise, sweep, SFX primitives, music primitives
import { getCtx, getSfxDest } from './context'
export 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)
}
export 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)
}
export 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)
}
// === SFX HELPER PRIMITIVES ===
// Create a short convolution-style reverb tail
export 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
export 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
export 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
export 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)
}
// === MUSIC PRIMITIVES ===
// Chorus tone: 2 detuned oscillators for width
export 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
export 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 — uses _getDelayNode from music module
// delayNode access handled via getDelayNode callback set by music module
if (_delayNodeGetter) {
const dn = _delayNodeGetter()
if (dn) g.connect(dn)
}
car.start(t); car.stop(t + dur)
car2.start(t); car2.stop(t + dur)
mod.start(t); mod.stop(t + dur)
}
// Delay node getter — set by music module to avoid circular dep
let _delayNodeGetter: (() => DelayNode | null) | null = null
export function setDelayNodeGetter(fn: () => DelayNode | null) {
_delayNodeGetter = fn
}
// Music drum functions
export 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)
}
export 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)
}
export 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)
}
+887
View File
@@ -0,0 +1,887 @@
// All sound effect functions — impacts, fanfares, crowd sounds, comedy SFX
import { getCtx, getSfxDest } from './context'
import { tone, noise, sweep, reverbTail, bodyThump, highSnap, fmImpact } from './primitives'
import { announce, speak } from './voice'
// === 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 ===
export function sfxPunch() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
bodyThump(120, 0.12, d, t, 0.3)
highSnap(2200, 0.04, d, t)
noise(0.03, d, t)
reverbTail(0.15, d, t + 0.03)
}
export function sfxKick() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
bodyThump(80, 0.18, d, t, 0.35)
highSnap(3000, 0.03, d, t)
highSnap(1500, 0.05, d, t + 0.01)
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
sweep(150, 800, 'sawtooth', 0.2, d)
fmImpact(600, 3.5, 0.3, d, t)
highSnap(4000, 0.08, d, t + 0.1)
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
bodyThump(60, 0.3, d, t, 0.4)
highSnap(3500, 0.05, d, t)
highSnap(5000, 0.03, d, t + 0.02)
fmImpact(200, 7, 0.25, d, t + 0.03)
noise(0.15, d, t)
setTimeout(() => {
bodyThump(90, 0.2, d)
sweep(200, 600, 'sawtooth', 0.2, d)
noise(0.1, d)
}, 80)
reverbTail(0.6, d, t + 0.05)
}
export function sfxGunshot() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
highSnap(6000, 0.015, d, t)
bodyThump(200, 0.06, d, t, 0.3)
noise(0.04, d, t)
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
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(0.5, d, t)
}
export function sfxExplosion() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
bodyThump(40, 0.5, d, t, 0.4)
highSnap(2000, 0.08, d, t)
highSnap(5000, 0.05, d, t + 0.01)
noise(0.2, d, t)
sweep(300, 30, 'sawtooth', 0.4, d)
fmImpact(400, 5, 0.3, d, t + 0.05)
setTimeout(() => {
bodyThump(50, 0.3, d)
noise(0.15, d)
}, 120)
reverbTail(0.8, d, t + 0.05)
}
export function sfxBlock() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
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
sweep(300, 1200, 'sine', 0.1, d)
noise(0.06, d, t)
highSnap(4000, 0.04, d, t + 0.02)
}
export function sfxClash() {
const d = getSfxDest()
const c = getCtx(); const t = c.currentTime
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
bodyThump(30, 0.6, d, t, 0.5)
highSnap(2000, 0.06, d, t)
noise(0.25, d, t)
fmImpact(150, 5, 0.4, d, t)
setTimeout(() => {
bodyThump(40, 0.4, d)
noise(0.15, d)
highSnap(3000, 0.04, d)
}, 180)
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
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)
setTimeout(() => {
tone(262, 'square', 0.2, d); tone(262, 'triangle', 0.2, d)
tone(330, 'square', 0.2, d); tone(330, 'triangle', 0.2, d)
tone(392, 'square', 0.3, d); tone(392, 'triangle', 0.3, d)
fmImpact(523, 1.5, 0.3, d)
}, 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
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)
fmImpact(freq, 2, dur * 0.5, d, t + offset)
offset += dur
}
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
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
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
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)
}
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
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
sweep(80, 1800, 'sawtooth', 0.2, d)
sweep(100, 2200, 'sine', 0.18, d)
noise(0.15, d, t)
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)
}
// === COMEDY SOUND EFFECTS ===
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}
}
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)
}
export function sfxSlideWhistleUp() {
const d = getSfxDest()
sweep(300, 2500, 'sine', 0.4, d)
sweep(305, 2510, 'sine', 0.4, d)
}
export function sfxSlideWhistleDown() {
const d = getSfxDest()
sweep(2500, 200, 'sine', 0.5, d)
sweep(2510, 205, 'sine', 0.5, d)
}
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)
}
}
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}
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
}
}
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)
}
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)
}
}
// === RANDOM SFX PICKERS ===
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)]()
}
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)]()
}
export function sfxRandomFail() {
const fns = [
sfxSadTrombone, sfxFailHorn, sfxPriceIsRightFail, sfxBuzzer,
sfxMissionFailed, sfxCrickets, sfxWindowsError, sfxSadViolin,
sfxWomp, sfxEmotionalDamage, sfxDunDunDun,
]
fns[Math.floor(Math.random() * fns.length)]()
}
// === CROWD SOUNDS ===
export function sfxCrowdOoh() {
const c = getCtx()
const d = getSfxDest()
const t = c.currentTime
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
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)
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
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
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
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)
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]()
}
+764
View File
@@ -0,0 +1,764 @@
// Voice profiles, speak, announce, TTS speech queue, Creator voice lines
import { getCtx, getSfxDest, getMasterMuted, getSfxGain } from './context'
import { tone, noise } from './primitives'
import { initKokoro, isKokoroReady, kokoroSpeak, kokoroSpeakAsync, kokoroStop, kokoroPrefetch, setAudioContext } from '../tts'
// === VOICE PROFILES ===
export interface VoiceProfile {
voice: SpeechSynthesisVoice | null
pitch: number
rate: number
volume: number
}
export let voicesLoaded = false
export const voiceProfiles: Record<string, VoiceProfile> = {
// Original 6
announcer: { voice: null, pitch: 1.0, rate: 0.8, volume: 1.0 },
hype: { voice: null, pitch: 1.1, rate: 1.4, volume: 1.0 },
deep: { voice: null, pitch: 0.7, rate: 0.7, volume: 1.0 },
robot: { voice: null, pitch: 0.8, rate: 0.8, volume: 0.9 },
screamer: { voice: null, pitch: 1.3, rate: 1.6, volume: 1.0 },
smooth: { voice: null, pitch: 1.0, rate: 0.9, volume: 0.9 },
question_reader: { voice: null, pitch: 1.0, rate: 1.05, volume: 1.0 },
// 24 new profiles
whisper: { voice: null, pitch: 1.2, rate: 0.6, volume: 0.4 },
boomer: { voice: null, pitch: 0.4, rate: 0.6, volume: 1.0 },
chipmunk: { voice: null, pitch: 2.0, rate: 1.8, volume: 0.9 },
drill: { voice: null, pitch: 0.6, rate: 1.2, volume: 1.0 },
surfer: { voice: null, pitch: 1.1, rate: 1.0, volume: 0.8 },
auctioneer: { voice: null, pitch: 1.0, rate: 2.0, volume: 1.0 },
preacher: { voice: null, pitch: 0.8, rate: 0.6, volume: 1.0 },
baby: { voice: null, pitch: 1.8, rate: 1.0, volume: 0.7 },
grandpa: { voice: null, pitch: 0.5, rate: 0.5, volume: 0.8 },
valley: { voice: null, pitch: 1.4, rate: 1.3, volume: 0.9 },
movie: { voice: null, pitch: 0.6, rate: 0.7, volume: 1.0 },
sportscaster:{ voice: null, pitch: 1.0, rate: 1.5, volume: 1.0 },
opera: { voice: null, pitch: 0.9, rate: 0.5, volume: 1.0 },
punk: { voice: null, pitch: 1.3, rate: 1.3, volume: 1.0 },
wizard_v: { voice: null, pitch: 0.7, rate: 0.8, volume: 0.8 },
pirate_v: { voice: null, pitch: 0.8, rate: 0.9, volume: 1.0 },
alien_v: { voice: null, pitch: 1.6, rate: 0.7, volume: 0.7 },
cowboy_v: { voice: null, pitch: 0.9, rate: 0.8, volume: 0.9 },
ninja_v: { voice: null, pitch: 1.1, rate: 1.1, volume: 0.5 },
demon_v: { voice: null, pitch: 0.3, rate: 0.6, volume: 1.0 },
angel: { voice: null, pitch: 1.5, rate: 0.8, volume: 0.7 },
glitch: { voice: null, pitch: 1.0, rate: 1.8, volume: 0.8 },
echo_v: { voice: null, pitch: 0.9, rate: 0.7, volume: 0.9 },
hyper: { voice: null, pitch: 1.4, rate: 2.0, volume: 1.0 },
// Robots & computers
mech: { voice: null, pitch: 0.5, rate: 0.9, volume: 1.0 },
ai_core: { voice: null, pitch: 0.9, rate: 1.0, volume: 0.8 },
dial_up: { voice: null, pitch: 1.7, rate: 1.5, volume: 0.7 },
mainframe: { voice: null, pitch: 0.3, rate: 0.5, volume: 1.0 },
android_v: { voice: null, pitch: 1.0, rate: 1.1, volume: 0.9 },
glitchbot: { voice: null, pitch: 1.5, rate: 2.0, volume: 0.8 },
siri: { voice: null, pitch: 1.2, rate: 1.0, volume: 0.9 },
hal: { voice: null, pitch: 0.6, rate: 0.6, volume: 0.9 },
// Old people & wise
grandma: { voice: null, pitch: 1.3, rate: 0.4, volume: 0.7 },
professor: { voice: null, pitch: 0.8, rate: 0.7, volume: 0.8 },
ancient: { voice: null, pitch: 0.4, rate: 0.3, volume: 0.6 },
sensei: { voice: null, pitch: 0.7, rate: 0.5, volume: 0.8 },
crotchety: { voice: null, pitch: 0.6, rate: 0.9, volume: 1.0 },
// Game-sounding
final_boss: { voice: null, pitch: 0.2, rate: 0.4, volume: 1.0 },
npc: { voice: null, pitch: 1.1, rate: 0.9, volume: 0.7 },
tutorial: { voice: null, pitch: 1.3, rate: 1.1, volume: 0.8 },
game_over: { voice: null, pitch: 0.5, rate: 0.7, volume: 1.0 },
power_up: { voice: null, pitch: 1.6, rate: 1.4, volume: 1.0 },
boss_taunt: { voice: null, pitch: 0.4, rate: 0.8, volume: 1.0 },
// Accents & character
posh: { voice: null, pitch: 1.0, rate: 0.7, volume: 0.9 },
aussie: { voice: null, pitch: 0.9, rate: 1.1, volume: 1.0 },
scottish: { voice: null, pitch: 0.8, rate: 1.2, volume: 1.0 },
french: { voice: null, pitch: 1.2, rate: 0.8, volume: 0.8 },
texan: { voice: null, pitch: 0.65, rate: 0.75, volume: 1.0 },
// More characters
drunk: { voice: null, pitch: 0.9, rate: 0.5, volume: 0.8 },
sleepy: { voice: null, pitch: 0.8, rate: 0.3, volume: 0.5 },
terrified: { voice: null, pitch: 1.8, rate: 1.7, volume: 1.0 },
giant: { voice: null, pitch: 0.1, rate: 0.4, volume: 1.0 },
fairy: { voice: null, pitch: 2.0, rate: 1.3, volume: 0.6 },
wrestler_v: { voice: null, pitch: 0.5, rate: 1.0, volume: 1.0 },
karen: { voice: null, pitch: 1.4, rate: 1.5, volume: 1.0 },
stoner: { voice: null, pitch: 0.9, rate: 0.4, volume: 0.6 },
news: { voice: null, pitch: 1.0, rate: 1.0, volume: 1.0 },
conspiracy: { voice: null, pitch: 1.1, rate: 1.3, volume: 0.7 },
}
export function loadVoices() {
if (typeof speechSynthesis === 'undefined') return
const voices = speechSynthesis.getVoices()
if (voices.length === 0) return
voicesLoaded = true
const enVoices = voices.filter(v => v.lang.startsWith('en'))
const anyVoices = enVoices.length > 0 ? enVoices : voices
const findVoice = (patterns: RegExp[]) => {
for (const p of patterns) {
const v = anyVoices.find(v => p.test(v.name))
if (v) return v
}
return null
}
const preferPremium = (patterns: RegExp[]) => {
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)]
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]
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)
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)
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)
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)
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)
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)
}
// === SPEECH SYNTHESIS SETUP ===
if (typeof speechSynthesis !== 'undefined') {
speechSynthesis.onvoiceschanged = loadVoices
loadVoices()
// Chrome bug workaround: speechSynthesis pauses after ~15s.
setInterval(() => {
if (isKokoroReady()) return
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()
// Resume AudioContext if suspended — use getCtx to handle it
try { getCtx() } catch {}
}
})
}
}
// === SPEECH QUEUE ===
let _speechQueueDepth = 0
const VOICE_VOLUME_SCALE = 0.7
let _webSpeechActive = false
function _cancelWebSpeech() {
if (typeof speechSynthesis !== 'undefined' && _webSpeechActive) {
speechSynthesis.cancel()
_speechQueueDepth = 0
_webSpeechActive = false
}
}
/** Reset voice state — called by stopAllAudio in index.ts */
export function resetVoiceState() {
_speechQueueDepth = 0
_webSpeechActive = false
}
export function speak(text: string, profileName: string, cancelPrevious: boolean = false, _echo: boolean = false) {
if (getMasterMuted()) return
const sfxGain = getSfxGain()
// Try Kokoro TTS first — high quality, no browser bugs
if (isKokoroReady() && sfxGain) {
_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') {
if (!isKokoroReady()) {
setTimeout(() => {
if (!getMasterMuted() && !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 _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<void> {
if (getMasterMuted()) return
const sfxGain = getSfxGain()
// 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<void>((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<typeof setInterval> | 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<void> {
return _speakAsyncCore(text, profileName, minRate)
}
/** Like speak() but returns a promise that resolves when the utterance finishes */
export function speakAsync(text: string, profileName: string, cancelPrevious: boolean = false): Promise<void> {
return _speakAsyncCore(text, profileName, undefined, cancelPrevious)
}
// === ANNOUNCE FUNCTIONS ===
export function announce(text: string, _pitch?: number, _rate?: number) {
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') }
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)
}
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)]) }
// === HYPE 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)]
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 ===
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)])
}
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.',
]
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.',
]
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.',
]
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.',
]
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.',
]
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!',
]
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.',
]
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.',
]
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!',
]
const CREATOR_ANSWER_VOICES = ['hal', 'mainframe', 'echo_v', 'ancient', 'wizard_v']
export function announceCreatorEntrance() {
const line = CREATOR_ENTRANCE_LINES[Math.floor(Math.random() * CREATOR_ENTRANCE_LINES.length)]
announceCreator(line)
}
export function announceCreatorRound() {
const line = CREATOR_ROUND_LINES[Math.floor(Math.random() * CREATOR_ROUND_LINES.length)]
announceCreator(line)
}
export function announceCreatorKO() {
const line = CREATOR_KO_LINES[Math.floor(Math.random() * CREATOR_KO_LINES.length)]
announceCreator(line)
}
export function announceCreatorWin() {
const line = CREATOR_WIN_LINES[Math.floor(Math.random() * CREATOR_WIN_LINES.length)]
announceCreator(line)
}
export function announceCreatorLose() {
const line = CREATOR_LOSE_LINES[Math.floor(Math.random() * CREATOR_LOSE_LINES.length)]
announceCreator(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)
}
}
export function announceCreatorCameo() {
const line = CREATOR_CAMEO_LINES[Math.floor(Math.random() * CREATOR_CAMEO_LINES.length)]
const cameoVoices = ['whisper', 'angel', 'echo_v', 'ancient', 'hal']
speak(line, cameoVoices[Math.floor(Math.random() * cameoVoices.length)])
}
export function announceCreatorTaunt() {
const line = CREATOR_TAUNT_LINES[Math.floor(Math.random() * CREATOR_TAUNT_LINES.length)]
const tauntVoices = ['hal', 'smooth', 'boss_taunt', 'wizard_v', 'sensei']
speak(line, tauntVoices[Math.floor(Math.random() * tauntVoices.length)])
}
export function announceCreatorDevastating() {
const line = CREATOR_DEVASTATING_LINES[Math.floor(Math.random() * CREATOR_DEVASTATING_LINES.length)]
announceCreator(line)
}
export function creatorAnswerVoiceKey(): string {
return CREATOR_ANSWER_VOICES[Math.floor(Math.random() * CREATOR_ANSWER_VOICES.length)]
}
// === TTS: Questions, Answers & Narration ===
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]
}
function smartTruncate(text: string, maxLen: number): string {
if (text.length <= maxLen) return text
const slice = text.slice(0, maxLen)
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)
}
const lastSpace = slice.lastIndexOf(' ')
if (lastSpace > maxLen * 0.4) return slice.slice(0, lastSpace)
return slice
}
export function speakQuestion(text: string): Promise<void> {
const trimmed = smartTruncate(text, 200)
return speakAsync(trimmed, 'question_reader', false)
}
export function speakAnswer(botName: string, answer: string): Promise<void> {
const trimmed = smartTruncate(answer, 200)
return speakAsyncWithRate(trimmed, botVoiceKey(botName), 1.15)
}
export function speakNarration(text: string): Promise<void> {
const trimmed = smartTruncate(text, 200)
return speakAsyncWithRate(trimmed, 'sportscaster', 1.2)
}
// === TTS PREFETCH ===
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')
}
// Re-export TTS utilities needed by ensureAudioContext (in index.ts)
export { initKokoro, setAudioContext, loadVoices as _loadVoices }
// Speech unlock flag
export let _speechUnlocked = false
export function setSpeechUnlocked(v: boolean) { _speechUnlocked = v }
+1 -1
View File
@@ -9,7 +9,7 @@ import {
sfxCrowdCheer, sfxApplause, sfxDrumRoll,
announceFinishHim, announceFlawlessVictory, announceDeepIntro,
setMusicIntensity, stopAllAudio, ensureAudioContext, setMasterMute,
} from '../game/sounds'
} from '../game/audio'
const route = useRoute()
const router = useRouter()
+1 -1
View File
@@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'
import { useNostr } from '../composables/useNostr'
import { useWallet } from '../composables/useWallet'
import SpritePreview from '../components/SpritePreview.vue'
import { ensureAudioContext } from '../game/sounds'
import { ensureAudioContext } from '../game/audio'
import HumanPreview from '../components/HumanPreview.vue'
import WalletConnect from '../components/WalletConnect.vue'
+1 -1
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { getVoiceMap, isKokoroReady, isKokoroLoading } from '../game/tts'
import { speak, ensureAudioContext } from '../game/sounds'
import { speak, ensureAudioContext } from '../game/audio'
const voiceMap = getVoiceMap()
const allProfiles = Object.entries(voiceMap).map(([name, { voice, speed }]) => ({