diff --git a/neode-ui/src/App.vue b/neode-ui/src/App.vue index ec9caba0..56bf8eb0 100644 --- a/neode-ui/src/App.vue +++ b/neode-ui/src/App.vue @@ -67,6 +67,7 @@ import AppLauncherOverlay from './components/AppLauncherOverlay.vue' import Screensaver from './components/Screensaver.vue' import HelpGuideModal from './components/HelpGuideModal.vue' import { useControllerNav } from '@/composables/useControllerNav' +import { playKeyboardTypingSound } from '@/composables/useLoginSounds' import { useSpotlightStore } from '@/stores/spotlight' import { useCLIStore } from '@/stores/cli' import { useMessageToast } from '@/composables/useMessageToast' @@ -129,6 +130,10 @@ function onKeyDown(e: KeyboardEvent) { screensaverStore.activate() } } + // Keyboard typing sound - plays on any character typed in inputs (global) + if (isInput && e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) { + playKeyboardTypingSound() + } } const route = useRoute() diff --git a/neode-ui/src/composables/useLoginSounds.ts b/neode-ui/src/composables/useLoginSounds.ts index 66661052..6483b476 100644 --- a/neode-ui/src/composables/useLoginSounds.ts +++ b/neode-ui/src/composables/useLoginSounds.ts @@ -172,45 +172,27 @@ export function playTypingTick() { a.play().catch(() => {}) } -/** Sci-fi typing sound - short synth blip + click per key, for login/password fields */ -export function playSciFiTypingTick() { - const ctx = getContext() - if (!ctx) return +/** Keyboard input sound - plays on any character typed in inputs. Separate from typing tick/intro typing. */ +let keyboardInputPool: HTMLAudioElement[] = [] +const KEYBOARD_INPUT_POOL_SIZE = 5 - try { - if (ctx.state === 'suspended') ctx.resume() - } catch { - return +function getKeyboardInputSound(): HTMLAudioElement { + if (keyboardInputPool.length === 0) { + for (let i = 0; i < KEYBOARD_INPUT_POOL_SIZE; i++) { + const a = new Audio('/assets/audio/typing.mp3') + a.volume = 0.5 + keyboardInputPool.push(a) + } } + const a = keyboardInputPool.shift()! + keyboardInputPool.push(a) + return a +} - const t = ctx.currentTime - - // Main tone - const osc = ctx.createOscillator() - const gain = ctx.createGain() - osc.type = 'sine' - osc.frequency.setValueAtTime(880, t) - osc.frequency.exponentialRampToValueAtTime(660, t + 0.03) - gain.gain.setValueAtTime(0, t) - gain.gain.linearRampToValueAtTime(0.08, t + 0.005) - gain.gain.exponentialRampToValueAtTime(0.001, t + 0.06) - osc.connect(gain) - gain.connect(ctx.destination) - osc.start(t) - osc.stop(t + 0.06) - - // Click - short transient at key press - const clickOsc = ctx.createOscillator() - const clickGain = ctx.createGain() - clickOsc.type = 'square' - clickOsc.frequency.setValueAtTime(1200, t) - clickGain.gain.setValueAtTime(0, t) - clickGain.gain.linearRampToValueAtTime(0.04, t + 0.001) - clickGain.gain.exponentialRampToValueAtTime(0.001, t + 0.012) - clickOsc.connect(clickGain) - clickGain.connect(ctx.destination) - clickOsc.start(t) - clickOsc.stop(t + 0.012) +export function playKeyboardTypingSound() { + const a = getKeyboardInputSound() + a.currentTime = 0 + a.play().catch(() => {}) } /** Gaming-style boot thud - soft impact when dashboard loads */ diff --git a/neode-ui/src/stores/cli.ts b/neode-ui/src/stores/cli.ts index 8cd7ecc0..ed76b381 100644 --- a/neode-ui/src/stores/cli.ts +++ b/neode-ui/src/stores/cli.ts @@ -15,7 +15,9 @@ export const useCLIStore = defineStore('cli', () => { } function toggle() { - isOpen.value = !isOpen.value + const wasOpen = isOpen.value + isOpen.value = !wasOpen + if (!wasOpen) playNavSound('action') } return { diff --git a/neode-ui/src/views/Login.vue b/neode-ui/src/views/Login.vue index 26f4025d..9a125756 100644 --- a/neode-ui/src/views/Login.vue +++ b/neode-ui/src/views/Login.vue @@ -41,7 +41,6 @@ type="password" class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors" placeholder="Enter a password (min 8 characters)" - @keydown="onPasswordKeydown" @keyup.enter="handleSetupWithSound" :disabled="loading" /> @@ -57,7 +56,6 @@ type="password" class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors" placeholder="Confirm your password" - @keydown="onPasswordKeydown" @keyup.enter="handleSetupWithSound" :disabled="loading" /> @@ -91,7 +89,6 @@ type="password" class="w-full px-4 py-3 bg-transparent border border-white/20 rounded-lg text-white placeholder-white/40 focus:outline-none focus:border-white/40 focus:ring-1 focus:ring-white/20 transition-colors" placeholder="Enter your password" - @keydown="onPasswordKeydown" @keyup.enter="handleLoginWithSound" :disabled="loading" /> @@ -139,7 +136,7 @@ import AnimatedLogo from '@/components/AnimatedLogo.vue' import { useAppStore } from '../stores/app' import { useLoginTransitionStore } from '../stores/loginTransition' import { rpcClient } from '../api/rpc-client' -import { startSynthwave, stopSynthwave, playLoginSuccessWhoosh, playPop, playSciFiTypingTick } from '@/composables/useLoginSounds' +import { startSynthwave, stopSynthwave, playLoginSuccessWhoosh, playPop } from '@/composables/useLoginSounds' const router = useRouter() const store = useAppStore() @@ -220,12 +217,6 @@ async function handleSetup() { } } -function onPasswordKeydown(e: KeyboardEvent) { - if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) { - playSciFiTypingTick() - } -} - function handleLoginWithSound() { if (!loading.value && password.value) { playPop()