phone/src/utils/waiting-sounds.ts
2025-11-17 13:01:24 -08:00

104 lines
2.8 KiB
TypeScript

import { type Player } from "../buzz/index.ts"
import { join } from "path"
import type { Playback, StreamingPlayback } from "../buzz/utils.ts"
import { random } from "./index.ts"
export class WaitingSounds {
typingPlayback?: Playback
speakingPlayback?: Playback
constructor(private player: Player, private streamPlayback: StreamingPlayback) {}
async start() {
if (this.typingPlayback) return // Already playing
this.#startTypingSounds()
this.#startSpeakingSounds()
}
async #startTypingSounds() {
return new Promise<void>(async (resolve) => {
do {
const value = Math.random() * 100
let dir: SoundDir
if (value > 33) {
dir = "typing"
} else {
dir = "clicking"
}
const typingSound = getSound(dir)
this.typingPlayback = await this.player.play(typingSound)
await this.typingPlayback.finished()
} while (this.typingPlayback)
resolve()
})
}
async #startSpeakingSounds() {
const playedSounds = new Set<string>()
let dir: SoundDir | undefined
return new Promise<void>(async (resolve) => {
// Don't start speaking until the stream playback buffer is empty!
while (this.streamPlayback.bufferEmptyFor < 1000) {
await Bun.sleep(100)
}
do {
this.streamPlayback.bufferEmptyFor
const lastSoundDir = dir
const value = Math.random() * 100
if (lastSoundDir === "body-noises") {
dir = "apology"
} else if (value > 99 && !lastSoundDir) {
dir = "body-noises"
} else if (value > 75 && !lastSoundDir) {
dir = "stalling"
} else {
dir = undefined
await Bun.sleep(1000)
}
if (dir) {
const speakingSound = getSound(dir, Array.from(playedSounds))
this.speakingPlayback = await this.player.play(speakingSound)
playedSounds.add(speakingSound)
await this.speakingPlayback.finished()
}
} while (this.typingPlayback)
resolve()
})
}
async stop() {
if (!this.typingPlayback) return
await Promise.all([this.typingPlayback.stop(), this.speakingPlayback?.finished()])
this.typingPlayback = undefined
}
}
type SoundDir = (typeof soundDirs)[number]
const soundDirs = [
"apology",
"background",
"body-noises",
"clicking",
"greeting",
"stalling",
"typing",
] as const
export const soundDir = join(import.meta.dir, "../../", "sounds")
export const getSound = (dir: SoundDir, exclude: string[] = []): string => {
const glob = new Bun.Glob("*.wav")
const soundPaths = Array.from(glob.scanSync({ cwd: join(soundDir, dir), absolute: true }))
const filteredSoundPaths = soundPaths.filter((path) => !exclude.includes(path))
if (filteredSoundPaths.length === 0) {
return random(soundPaths)
}
return random(filteredSoundPaths)
}