Add typewriter clatter during text reveal
A dry synthesized key-clack (filtered noise burst with pitch jitter) plays as each line types out — every other non-space character — for a detective-terminal feel. Skipped in the editor preview and when reduced-motion is set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,17 @@ function audioContext() {
|
||||
return ctx
|
||||
}
|
||||
|
||||
let noiseBuffer: AudioBuffer | null = null
|
||||
function noise(context: AudioContext) {
|
||||
if (!noiseBuffer) {
|
||||
const length = Math.floor(context.sampleRate * 0.05)
|
||||
noiseBuffer = context.createBuffer(1, length, context.sampleRate)
|
||||
const data = noiseBuffer.getChannelData(0)
|
||||
for (let i = 0; i < length; i++) data[i] = Math.random() * 2 - 1
|
||||
}
|
||||
return noiseBuffer
|
||||
}
|
||||
|
||||
const music = typeof Audio !== 'undefined' ? new Audio() : null
|
||||
if (music) music.loop = true
|
||||
let currentUrl: string | null = null
|
||||
@@ -70,6 +81,22 @@ export const audio = {
|
||||
osc.connect(gain).connect(context.destination)
|
||||
osc.start(now); osc.stop(now + 0.18)
|
||||
},
|
||||
// A dry typewriter key-clack, for the text-reveal clatter. Short filtered noise
|
||||
// burst with slight pitch jitter so successive keys differ.
|
||||
type() {
|
||||
if (muted) return
|
||||
const context = audioContext()
|
||||
if (!context) return
|
||||
if (context.state === 'suspended') void context.resume()
|
||||
const src = context.createBufferSource(); src.buffer = noise(context)
|
||||
const filter = context.createBiquadFilter(); filter.type = 'bandpass'; filter.frequency.value = 1500 + Math.random() * 900; filter.Q.value = 0.9
|
||||
const gain = context.createGain()
|
||||
const now = context.currentTime
|
||||
gain.gain.setValueAtTime(0.06, now)
|
||||
gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.028)
|
||||
src.connect(filter).connect(gain).connect(context.destination)
|
||||
src.start(now); src.stop(now + 0.04)
|
||||
},
|
||||
// Resume the context and retry pending music on a user gesture.
|
||||
resume() {
|
||||
void audioContext()?.resume?.()
|
||||
|
||||
@@ -84,6 +84,13 @@ export function DialoguePlayer({ node, onExit, inline, startId }: { node: { utte
|
||||
return () => window.clearInterval(id)
|
||||
}, [currentId, fullText, reduced]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Typewriter clatter as characters are revealed (every other non-space char).
|
||||
useEffect(() => {
|
||||
if (inline || charCount === 0 || charCount > fullText.length) return
|
||||
const ch = fullText[charCount - 1]
|
||||
if (ch && ch !== ' ' && charCount % 2 === 0) audio.type()
|
||||
}, [charCount]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const pick = (choice: RuntimeUtterance) => {
|
||||
if (!inline) audio.sfx('choice')
|
||||
if (choice.childIds.length > 0) setCurrentId(choice.childIds[0])
|
||||
|
||||
Reference in New Issue
Block a user