Files
archy/neode-ui/src/composables/useLoginSounds.ts
T

204 lines
5.5 KiB
TypeScript
Raw Normal View History

/**
* Login screen audio: intro loop (MP3) + transition sounds.
*/
let audioContext: AudioContext | null = null
let introAudio: HTMLAudioElement | null = null
let introGain: GainNode | null = null
function getContext(): AudioContext | null {
if (audioContext) return audioContext
try {
audioContext = new (window.AudioContext || (window as any).webkitAudioContext)()
return audioContext
} catch {
return null
}
}
const INTRO_AUDIO_URL = '/assets/audio/cosmic-updrift.mp3'
const LOOP_START_URL = '/assets/audio/loop-start.mp3'
/** Play loop-start when transitioning from typing intro to Welcome Noderunner, as the intro music comes in.
* Uses Web Audio API so it plays after context is resumed (user gesture). */
export function playLoopStart() {
const ctx = getContext()
if (!ctx) return
try {
if (ctx.state === 'suspended') ctx.resume()
} catch {
return
}
fetch(LOOP_START_URL)
.then((res) => res.arrayBuffer())
.then((buf) => ctx.decodeAudioData(buf))
.then((decoded) => {
const src = ctx.createBufferSource()
src.buffer = decoded
const gain = ctx.createGain()
gain.gain.value = 0.5
src.connect(gain)
gain.connect(ctx.destination)
src.start(0)
})
.catch(() => {})
}
/** Resume audio context (call on first user interaction to unlock autoplay) */
export function resumeAudioContext() {
const ctx = getContext()
if (ctx?.state === 'suspended') {
ctx.resume().catch(() => {})
}
}
/** Start intro loop - Cosmic Updrift (free royalty) */
export function startSynthwave() {
const ctxOrNull = getContext()
if (!ctxOrNull) return
try {
if (ctxOrNull.state === 'suspended') ctxOrNull.resume()
} catch {
return
}
stopSynthwave()
const audio = new Audio(INTRO_AUDIO_URL)
audio.loop = true
const ctx = ctxOrNull
const source = ctx.createMediaElementSource(audio)
const gainNode = ctx.createGain()
gainNode.gain.value = 0.25
source.connect(gainNode)
gainNode.connect(ctx.destination)
introGain = gainNode
introAudio = audio
audio.play().catch(() => {})
}
/** Stop intro loop (call on login success) */
export function stopSynthwave() {
if (introAudio) {
if (introGain && audioContext) {
const t = audioContext.currentTime
introGain.gain.setValueAtTime(1, t)
introGain.gain.linearRampToValueAtTime(0.001, t + 0.2)
}
setTimeout(() => {
introAudio?.pause()
introAudio = null
introGain = null
}, 220)
}
}
/** Whoosh transition on successful login */
export function playLoginSuccessWhoosh() {
const woosh = new Audio('/assets/audio/woosh.mp3')
woosh.volume = 0.5
woosh.play().catch(() => {})
}
/** Typing sound - plays once when welcome typing starts (typing.mp3) */
export function playTypingSound() {
const audio = new Audio('/assets/audio/typing.mp3')
audio.volume = 0.6
audio.play().catch(() => {})
}
/** Intro typing - ONE sound per sentence: play when sentence starts, stop when it ends (intro-typing.mp3 from archy assets) */
let introTypingAudio: HTMLAudioElement | null = null
const INTRO_TYPING_URL = '/assets/audio/intro-typing.mp3'
export function playIntroTyping() {
stopIntroTyping()
introTypingAudio = new Audio(INTRO_TYPING_URL)
introTypingAudio.volume = 0.5
introTypingAudio.loop = true
introTypingAudio.play().catch(() => {})
}
export function stopIntroTyping() {
if (introTypingAudio) {
introTypingAudio.pause()
introTypingAudio.currentTime = 0
introTypingAudio = null
}
}
const WELCOME_SPEECH_URL = '/assets/audio/welcome-noderunner.mp3'
/** Sci-fi female voice: "Welcome Noderunner" - plays when welcome text types in.
* Requires pre-recorded audio from ElevenLabs. Run:
* ELEVENLABS_API_KEY=your_key node neode-ui/scripts/generate-welcome-speech.js
* Browse sci-fi voices at elevenlabs.io/voice-library and set ELEVENLABS_VOICE_ID for custom voice. */
export function playWelcomeNoderunnerSpeech() {
const audio = new Audio(WELCOME_SPEECH_URL)
audio.volume = 0.9
audio.play().catch(() => {})
}
/** Typing tick - for dashboard welcome typing (typing.mp3) */
let typingTickPool: HTMLAudioElement[] = []
const TYPING_TICK_POOL_SIZE = 5
function getTypingTick(): HTMLAudioElement {
if (typingTickPool.length === 0) {
for (let i = 0; i < TYPING_TICK_POOL_SIZE; i++) {
const a = new Audio('/assets/audio/typing.mp3')
a.volume = 0.4
typingTickPool.push(a)
}
}
const a = typingTickPool.shift()!
typingTickPool.push(a)
return a
}
export function playTypingTick() {
const a = getTypingTick()
a.currentTime = 0
a.play().catch(() => {})
}
/** Gaming-style boot thud - soft impact when dashboard loads */
export function playDashboardLoadOomph() {
const ctx = getContext()
if (!ctx) return
try {
if (ctx.state === 'suspended') ctx.resume()
} catch {
return
}
const t = ctx.currentTime
// Soft layered thud - sine only, smooth attack/decay
const layers = [
{ freq: 55, dur: 0.35, gain: 0.4, attack: 0.02 },
{ freq: 82, dur: 0.28, gain: 0.25, attack: 0.03 },
{ freq: 110, dur: 0.22, gain: 0.15, attack: 0.04 },
{ freq: 165, dur: 0.18, gain: 0.1, attack: 0.05 },
]
for (let i = 0; i < layers.length; i++) {
const L = layers[i]!
const osc = ctx.createOscillator()
const g = ctx.createGain()
osc.type = 'sine'
osc.frequency.value = L.freq
g.gain.setValueAtTime(0, t)
g.gain.linearRampToValueAtTime(L.gain, t + L.attack)
g.gain.exponentialRampToValueAtTime(0.001, t + L.dur)
osc.connect(g)
g.connect(ctx.destination)
osc.start(t + i * 0.01)
osc.stop(t + L.dur)
}
}