This commit is contained in:
Corey Johnson 2025-11-18 17:43:12 -08:00
parent a0cc0c85cf
commit 843f3680e1
5 changed files with 407 additions and 368 deletions

View File

@ -5,6 +5,9 @@ import { sleep } from "bun"
import { processStderr, processStdout } from "./utils/stdio"
import Buzz from "./buzz"
import { join } from "path"
import { GPIO } from "./pins"
import { Agent } from "./agent"
import { searchWeb } from "./agent/tools"
type CancelableTask = () => void
@ -19,327 +22,424 @@ type PhoneContext = {
cancelAgent?: CancelableTask
}
export const pins = {
ringer: 17,
hook: 27,
rotaryInUse: 22,
rotaryNumber: 23,
}
const gpio = new GPIO({ resetOnClose: true })
using ringer = gpio.output(17)
using inputs = gpio.inputGroup({
hook: { pin: 27, debounce: 50 },
rotaryInUse: { pin: 22, debounce: 50 },
rotaryNumber: { pin: 23, debounce: 10 },
})
export const startPhone = async () => {
export const startPhone = async (agentId: string, apiKey: string) => {
await Buzz.setVolume(0.4)
log.info(`📞 Hook ${inputs.pins.hook.value}`)
await handleInputEvents()
}
const baresipConfig = join(import.meta.dir, "..", "baresip")
const baresip = new Baresip(["/usr/bin/baresip", "-v", "-f", baresipConfig])
baresip.registrationSuccess.connect(async () => {
log.info("🐻 server connected")
const result = await gpio.get(pins.hook)
if (result.state === "low") {
phoneService.send({ type: "initialized" })
const handleInputEvents = async () => {
let digit = 0
for await (const event of inputs.events()) {
switch (event.pin) {
case "hook":
const type = event.value == 0 ? "hang_up" : "pick_up"
log.info(`📞 Hook ${event.value} sending ${type}`)
if (type === "hang_up") {
ringer.value = 1
} else {
phoneService.send({ type: "pick_up" })
ringer.value = 0
}
})
break
baresip.callReceived.connect(({ contact }) => {
log.info(`🐻 incoming call from ${contact}`)
phoneService.send({ type: "incoming_call", from: contact })
})
baresip.callEstablished.connect(({ contact }) => {
log.info(`🐻 call established with ${contact}`)
phoneService.send({ type: "answered" })
})
baresip.hungUp.connect(() => {
log.info("🐻 call hung up")
phoneService.send({ type: "remote_hang_up" })
})
baresip.connect().catch((error) => {
log.error("🐻 connection error:", error)
phoneService.send({ type: "error", message: error.message })
})
baresip.error.connect(async ({ message }) => {
log.error("🐻 error:", message)
phoneService.send({ type: "error", message })
for (let i = 0; i < 4; i++) {
await ring(500)
await sleep(250)
case "rotaryInUse":
if (event.value === 0) {
digit = 0
} else {
log.info(`📞 Dialed digit: ${digit}`)
}
break
case "rotaryNumber":
if (event.value === 1) {
digit += 1
}
break
default:
log.error(`📞 Unknown pin event: ${event.pin}`)
break
}
}
}
const apiKey = process.env.ELEVEN_API_KEY
const agentId = process.env.ELEVEN_AGENT_ID
if (!apiKey) {
console.error("❌ Error: ELEVEN_API_KEY environment variable is required")
process.exit(1)
})
}
let agentProcess: Bun.Subprocess
const initializeAgent = () => {
agentProcess = Bun.spawn({
cwd: "/home/corey/code/tone/packages/py-agent",
cmd: ["/home/corey/.local/bin/uv", "run", "main.py"],
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
env: { ...process.env, PYTHONUNBUFFERED: "1" },
})
log.info("☎️ Started agent process", agentProcess.pid)
processStdout(agentProcess, (line) => {
log.info(`🐍 ${line}`)
if (line === "Starting agent session") {
log.info(`💎 HEY! THE AGENT STARTED`)
} else if (line.startsWith("Conversation ended.")) {
phoneService.send({ type: "remote_hang_up" })
}
})
processStderr(agentProcess)
agentProcess.exited.then((code) => {
log.error(`💥 Agent process exited with code ${code}`)
phoneService.send({ type: "remote_hang_up" })
})
}
initializeAgent()
const startAgent = () => {
log.info("☎️ Starting agent conversation")
if (agentProcess?.stdin) {
agentProcess.stdin.write("start\n")
} else {
log.error("☎️ No agent process stdin available")
phoneService.send({ type: "remote_hang_up" })
}
return () => {
log.info("☎️ Stopping agent conversation")
if (agentProcess?.stdin) {
agentProcess.stdin.write("stop\n")
}
}
}
const context = (initial?: Partial<PhoneContext>): PhoneContext => ({
numberDialed: 0,
baresip,
startAgent,
...initial,
})
const phoneMachine = createMachine(
"initializing",
// prettier-ignore
{
initializing: state(
transition("initialized", "idle"),
transition("pick_up", "ready", reduce(playDialTone)),
transition("error", "fault", reduce(handleError))),
idle: state(
transition("incoming_call", "incoming", reduce(incomingCall)),
transition("pick_up", "ready", reduce(playDialTone))),
incoming: state(
transition("remote_hang_up", "idle", reduce(stopRinger)),
transition("pick_up", "connected", reduce(callAnswered))),
connected: state(
transition("remote_hang_up", "ready", reduce(playDialTone)),
transition("hang_up", "idle", reduce(stopCall))),
ready: state(
transition("dial_start", "dialing", reduce(dialStart)),
transition("dial_timeout", "aborted", reduce(stopDialTone)),
transition("hang_up", "idle", reduce(stopDialTone))),
dialing: state(
transition("dial_stop", "outgoing", reduce(makeCall), guard((ctx) => !callAgentGuard(ctx))),
transition("dial_stop", "connectedToAgent", reduce(makeAgentCall), guard((ctx) => callAgentGuard(ctx))),
transition("digit_increment", "dialing", reduce(digitIncrement)),
transition("hang_up", "idle", reduce(stopDialTone))),
outgoing: state(
transition("start_agent", "connectedToAgent"),
transition("answered", "connected"),
transition("hang_up", "idle", reduce(stopCall))),
connectedToAgent: state(
transition("remote_hang_up", "ready", reduce(stopAgent)),
transition("hang_up", "idle", reduce(stopAgent))),
aborted: state(
transition("hang_up", "idle")),
fault: state(),
},
context
if (!agentId) {
console.error(
"❌ Error: ELEVEN_AGENT_ID environELEVEN_AGENT_ID=agent_5601k4taw2cvfjzrz6snxpgeh7x8 ELEVEN_API_KEY=sk_0313740f112c5992cb62ed96c974ab19b5916f1ea172471fment variable is required"
)
const phoneService = interpret(phoneMachine, () => {})
d._onEnter = function (machine, to, state, prevState, event) {
log.info(`📱 ${machine.current} -> ${to} (${JSON.stringify(event)})`)
}
gpio.monitor(pins.hook, { bias: "pull-up" }, (event) => {
const type = event.edge === "falling" ? "hang_up" : "pick_up"
log.info(`📞 Hook ${event.edge} sending ${type}`)
phoneService.send({ type })
})
gpio.monitor(pins.rotaryInUse, { bias: "pull-up", throttleMs: 90 }, (event) => {
const type = event.edge === "falling" ? "dial_start" : "dial_stop"
log.debug(`📞 Rotary in-use ${event.edge} sending ${type}`)
phoneService.send({ type })
})
gpio.monitor(pins.rotaryNumber, { bias: "pull-up", throttleMs: 90 }, (event) => {
if (event.edge !== "rising") return
phoneService.send({ type: "digit_increment" })
})
// Graceful shutdown handling
const cleanup = () => {
log.info("🛑 Shutting down, stopping agent process")
if (agentProcess?.stdin) {
agentProcess.stdin.write("quit\n")
}
}
process.on("SIGINT", cleanup)
process.on("SIGTERM", cleanup)
process.on("exit", cleanup)
console.error(" Create an agent at https://elevenlabs.io/app/conversational-ai")
process.exit(1)
}
const incomingCallRing = (): CancelableTask => {
let abortController = new AbortController()
await startPhone(agentId, apiKey)
const playRingtone = async () => {
while (!abortController.signal.aborted) {
await ring(2000, abortController.signal)
await sleep(4000)
}
}
playRingtone().catch((error) => log.error("Ringer error:", error))
// log.info("📞 GPIO inputs initialized")
return () => abortController.abort()
}
// // const baresipConfig = join(import.meta.dir, "..", "baresip")
// // const baresip = new Baresip(["/usr/bin/baresip", "-v", "-f", baresipConfig])
const handleError = (ctx: PhoneContext, event: { type: "error"; message?: string }) => {
ctx.lastError = event.message
log.error(`Phone error: ${event.message}`)
return ctx
}
// // baresip.registrationSuccess.connect(async () => {
// // log.info("🐻 server connected")
// // const result = await gpio.get(pins.hook)
// // if (result.state === "low") {
// // phoneService.send({ type: "initialized" })
// // } else {
// // phoneService.send({ type: "pick_up" })
// // }
// // })
const incomingCall = (ctx: PhoneContext, event: { type: "incoming_call"; from?: string }) => {
ctx.peer = event.from
ctx.cancelRinger = incomingCallRing()
log.info(`Incoming call from ${event.from}`)
// // baresip.callReceived.connect(({ contact }) => {
// // log.info(`🐻 incoming call from ${contact}`)
// // phoneService.send({ type: "incoming_call", from: contact })
// // })
return ctx
}
// // baresip.callEstablished.connect(({ contact }) => {
// // log.info(`🐻 call established with ${contact}`)
// // phoneService.send({ type: "answered" })
// // })
const stopRinger = (ctx: PhoneContext) => {
ctx.cancelRinger?.()
ctx.cancelRinger = undefined
return ctx
}
// // baresip.hungUp.connect(() => {
// // log.info("🐻 call hung up")
// // phoneService.send({ type: "remote_hang_up" })
// // })
const playDialTone = (ctx: PhoneContext) => {
const tone = new ToneGenerator()
// // baresip.connect().catch((error) => {
// // log.error("🐻 connection error:", error)
// // phoneService.send({ type: "error", message: error.message })
// // })
tone.loopTone([350, 440])
// // baresip.error.connect(async ({ message }) => {
// // log.error("🐻 error:", message)
// // phoneService.send({ type: "error", message })
// // for (let i = 0; i < 4; i++) {
// // await ring(500)
// // await sleep(250)
// // }
// // process.exit(1)
// // })
ctx.cancelDialTone = () => {
tone.stop()
}
// const agent = new Agent({
// agentId,
// apiKey,
// tools: {
// search_web: (args: { query: string }) => searchWeb(args.query),
// },
// })
return ctx
}
// handleAgentEvents(agent)
const playOutgoingTone = () => {
const tone = new ToneGenerator()
let canceled = false
// const startAgent = () => {
// log.info("☎️ Starting agent conversation")
const play = async () => {
while (!canceled) {
await tone.playTone([440, 480], 2000)
await sleep(4000)
}
}
// if (agentProcess?.stdin) {
// agentProcess.stdin.write("start\n")
// } else {
// log.error("☎️ No agent process stdin available")
// phoneService.send({ type: "remote_hang_up" })
// }
play().catch((error) => log.error("Outgoing tone error:", error))
// return () => {
// log.info("☎️ Stopping agent conversation")
// if (agentProcess?.stdin) {
// agentProcess.stdin.write("stop\n")
// }
// }
// }
return () => {
tone.stop()
canceled = true
}
}
// const context = (initial?: Partial<PhoneContext>): PhoneContext => ({
// numberDialed: 0,
// baresip,
// startAgent,
// ...initial,
// })
const dialStart = (ctx: PhoneContext) => {
ctx.numberDialed = 0
ctx = stopDialTone(ctx)
// const phoneMachine = createMachine(
// "initializing",
// // prettier-ignore
// {
// initializing: state(
// transition("initialized", "idle"),
// transition("pick_up", "ready", reduce(playDialTone)),
// transition("error", "fault", reduce(handleError))),
// idle: state(
// transition("incoming_call", "incoming", reduce(incomingCall)),
// transition("pick_up", "ready", reduce(playDialTone))),
// incoming: state(
// transition("remote_hang_up", "idle", reduce(stopRinger)),
// transition("pick_up", "connected", reduce(callAnswered))),
// connected: state(
// transition("remote_hang_up", "ready", reduce(playDialTone)),
// transition("hang_up", "idle", reduce(stopCall))),
// ready: state(
// transition("dial_start", "dialing", reduce(dialStart)),
// transition("dial_timeout", "aborted", reduce(stopDialTone)),
// transition("hang_up", "idle", reduce(stopDialTone))),
// dialing: state(
// transition("dial_stop", "outgoing", reduce(makeCall), guard((ctx) => !callAgentGuard(ctx))),
// transition("dial_stop", "connectedToAgent", reduce(makeAgentCall), guard((ctx) => callAgentGuard(ctx))),
// transition("digit_increment", "dialing", reduce(digitIncrement)),
// transition("hang_up", "idle", reduce(stopDialTone))),
// outgoing: state(
// transition("start_agent", "connectedToAgent"),
// transition("answered", "connected"),
// transition("hang_up", "idle", reduce(stopCall))),
// connectedToAgent: state(
// transition("remote_hang_up", "ready", reduce(stopAgent)),
// transition("hang_up", "idle", reduce(stopAgent))),
// aborted: state(
// transition("hang_up", "idle")),
// fault: state(),
// },
// context
// )
return ctx
}
// const phoneService = interpret(phoneMachine, () => {})
const makeCall = (ctx: PhoneContext) => {
log.info(`Dialing number: ${ctx.numberDialed}`)
if (ctx.numberDialed === 1) {
ctx.baresip.dial("+13476229543")
} else if (ctx.numberDialed === 2) {
ctx.baresip.dial("+18109643563")
} else {
const playTone = async () => {
const tone = new ToneGenerator()
await tone.playTone([900], 200)
await tone.playTone([1350], 200)
await tone.playTone([1750], 200)
}
playTone().catch((error) => log.error("Error playing tone:", error))
}
// d._onEnter = function (machine, to, state, prevState, event) {
// log.info(`📱 ${machine.current} -> ${to} (${JSON.stringify(event)})`)
// }
return ctx
}
// gpio.monitor(pins.hook, { bias: "pull-up" }, (event) => {
// const type = event.edge === "falling" ? "hang_up" : "pick_up"
// log.info(`📞 Hook ${event.edge} sending ${type}`)
// phoneService.send({ type })
// })
const makeAgentCall = (ctx: PhoneContext) => {
log.info(`Calling agent`)
ctx.cancelAgent = ctx.startAgent()
// gpio.monitor(pins.rotaryInUse, { bias: "pull-up", throttleMs: 90 }, (event) => {
// const type = event.edge === "falling" ? "dial_start" : "dial_stop"
// log.debug(`📞 Rotary in-use ${event.edge} sending ${type}`)
// phoneService.send({ type })
// })
return ctx
}
// gpio.monitor(pins.rotaryNumber, { bias: "pull-up", throttleMs: 90 }, (event) => {
// if (event.edge !== "rising") return
// phoneService.send({ type: "digit_increment" })
// })
const callAgentGuard = (ctx: PhoneContext) => {
return ctx.numberDialed === 10
}
// // Graceful shutdown handling
// const cleanup = () => {
// log.info("🛑 Shutting down, stopping agent process")
// if (agentProcess?.stdin) {
// agentProcess.stdin.write("quit\n")
// }
// }
const callAnswered = (ctx: PhoneContext) => {
ctx.baresip.accept()
// process.on("SIGINT", cleanup)
// process.on("SIGTERM", cleanup)
// process.on("exit", cleanup)
// }
ctx.cancelDialTone?.()
ctx.cancelDialTone = undefined
// const handleAgentEvents = (agent: Agent) => {
// agent.events.connect(async (event) => {
// switch (event.type) {
// case "connected":
// console.log("✅ Connected to AI agent\n")
// break
ctx.cancelRinger?.()
ctx.cancelRinger = undefined
// case "user_transcript":
// console.log(`👤 You: ${event.transcript}`)
// break
return ctx
}
// case "agent_response":
// console.log(`🤖 Agent: ${event.response}`)
// break
const stopCall = (ctx: PhoneContext) => {
ctx.baresip.hangUp()
return ctx
}
// case "audio":
// await waitingIndicator.stop()
// const audioBuffer = Buffer.from(event.audioBase64, "base64")
// streamPlayback.write(audioBuffer)
// break
const stopAgent = (ctx: PhoneContext) => {
log.info("🛑 Stopping agent")
ctx.cancelAgent?.()
ctx.cancelAgent = undefined
return ctx
}
// case "interruption":
// console.log("🛑 User interrupted")
// streamPlayback?.stop()
// streamPlayback = player.playStream() // Reset playback stream
// break
const stopDialTone = (ctx: PhoneContext) => {
ctx.cancelDialTone?.()
ctx.cancelDialTone = undefined
// case "tool_call":
// waitingIndicator.start(streamPlayback)
// console.log(`🔧 Tool call: ${event.name}(${JSON.stringify(event.args)})`)
// break
return ctx
}
// case "tool_result":
// console.log(`✅ Tool result: ${JSON.stringify(event.result)}`)
// break
const digitIncrement = (ctx: PhoneContext) => {
ctx.numberDialed += 1
return ctx
}
// case "tool_error":
// console.error(`❌ Tool error: ${event.error}`)
// break
// case "disconnected":
// console.log("\n👋 Conversation ended, returning to dialtone\n")
// streamPlayback?.stop()
// state = "WAITING_FOR_VOICE"
// phoneService.send({ type: "remote_hang_up" })
// break
// case "error":
// console.error("Agent error:", event.error)
// break
// case "ping":
// break
// default:
// console.log(`😵‍💫 ${event.type}`)
// break
// }
// })
// }
// const incomingCallRing = (): CancelableTask => {
// let abortController = new AbortController()
// const playRingtone = async () => {
// while (!abortController.signal.aborted) {
// await ring(2000, abortController.signal)
// await sleep(4000)
// }
// }
// playRingtone().catch((error) => log.error("Ringer error:", error))
// return () => abortController.abort()
// }
// const handleError = (ctx: PhoneContext, event: { type: "error"; message?: string }) => {
// ctx.lastError = event.message
// log.error(`Phone error: ${event.message}`)
// return ctx
// }
// const incomingCall = (ctx: PhoneContext, event: { type: "incoming_call"; from?: string }) => {
// ctx.peer = event.from
// ctx.cancelRinger = incomingCallRing()
// log.info(`Incoming call from ${event.from}`)
// return ctx
// }
// const stopRinger = (ctx: PhoneContext) => {
// ctx.cancelRinger?.()
// ctx.cancelRinger = undefined
// return ctx
// }
// const playDialTone = (ctx: PhoneContext) => {
// const tone = new ToneGenerator()
// tone.loopTone([350, 440])
// ctx.cancelDialTone = () => {
// tone.stop()
// }
// return ctx
// }
// const playOutgoingTone = () => {
// const tone = new ToneGenerator()
// let canceled = false
// const play = async () => {
// while (!canceled) {
// await tone.playTone([440, 480], 2000)
// await sleep(4000)
// }
// }
// play().catch((error) => log.error("Outgoing tone error:", error))
// return () => {
// tone.stop()
// canceled = true
// }
// }
// const dialStart = (ctx: PhoneContext) => {
// ctx.numberDialed = 0
// ctx = stopDialTone(ctx)
// return ctx
// }
// const makeCall = (ctx: PhoneContext) => {
// log.info(`Dialing number: ${ctx.numberDialed}`)
// if (ctx.numberDialed === 1) {
// ctx.baresip.dial("+13476229543")
// } else if (ctx.numberDialed === 2) {
// ctx.baresip.dial("+18109643563")
// } else {
// const playTone = async () => {
// const tone = new ToneGenerator()
// await tone.playTone([900], 200)
// await tone.playTone([1350], 200)
// await tone.playTone([1750], 200)
// }
// playTone().catch((error) => log.error("Error playing tone:", error))
// }
// return ctx
// }
// const makeAgentCall = (ctx: PhoneContext) => {
// log.info(`Calling agent`)
// ctx.cancelAgent = ctx.startAgent()
// return ctx
// }
// const callAgentGuard = (ctx: PhoneContext) => {
// return ctx.numberDialed === 10
// }
// const callAnswered = (ctx: PhoneContext) => {
// ctx.baresip.accept()
// ctx.cancelDialTone?.()
// ctx.cancelDialTone = undefined
// ctx.cancelRinger?.()
// ctx.cancelRinger = undefined
// return ctx
// }
// const stopCall = (ctx: PhoneContext) => {
// ctx.baresip.hangUp()
// return ctx
// }
// const stopAgent = (ctx: PhoneContext) => {
// log.info("🛑 Stopping agent")
// ctx.cancelAgent?.()
// ctx.cancelAgent = undefined
// return ctx
// }
// const stopDialTone = (ctx: PhoneContext) => {
// ctx.cancelDialTone?.()
// ctx.cancelDialTone = undefined
// return ctx
// }
// const digitIncrement = (ctx: PhoneContext) => {
// ctx.numberDialed += 1
// return ctx
// }

View File

@ -26,8 +26,17 @@ export class InputGroup<T extends string = string> {
}
}
get pins(): Record<T, { readonly value: 0 | 1; waitForValue: (targetValue: 0 | 1, timeout?: number) => Promise<void> }> {
const result = {} as Record<T, { readonly value: 0 | 1; waitForValue: (targetValue: 0 | 1, timeout?: number) => Promise<void> }>
get pins(): Record<
T,
{ readonly value: 0 | 1; waitForValue: (targetValue: 0 | 1, timeout?: number) => Promise<void> }
> {
const result = {} as Record<
T,
{
readonly value: 0 | 1
waitForValue: (targetValue: 0 | 1, timeout?: number) => Promise<void>
}
>
for (const [name, config] of this.#pinMap) {
const offset = config.offset
@ -43,9 +52,10 @@ export class InputGroup<T extends string = string> {
if (ret === -1) throw new Error("Failed to get pin value")
return ret as 0 | 1
},
waitForValue: (targetValue: 0 | 1, timeout?: number) => this.#waitForPinValue(pinName as T, targetValue, timeout)
waitForValue: (targetValue: 0 | 1, timeout?: number) =>
this.#waitForPinValue(pinName as T, targetValue, timeout),
}),
enumerable: true
enumerable: true,
})
}
@ -97,11 +107,8 @@ export class InputGroup<T extends string = string> {
if (this.#closed) throw new Error("InputGroup is closed")
const eventQueue: InputGroupEvent<T>[] = []
let resolve: (() => void) | undefined
const listener = (event: InputGroupEvent<T>) => {
eventQueue.push(event)
resolve?.()
}
this.#eventListeners.push(listener)
@ -109,14 +116,14 @@ export class InputGroup<T extends string = string> {
try {
while (!this.#closed) {
if (eventQueue.length === 0) {
await new Promise<void>((r) => {
resolve = r
})
if (eventQueue.length > 0) {
for (const event of eventQueue) {
yield event
}
eventQueue.length = 0
} else {
await Bun.sleep(0)
}
const event = eventQueue.shift()
if (event) yield event
}
} finally {
this.#eventListeners = this.#eventListeners.filter((l) => l !== listener)
@ -159,12 +166,16 @@ export class InputGroup<T extends string = string> {
if (!pinInfo) continue
const pressed = mapLibgpiodEdgeToPressedState(edgeType, pinInfo.pull)
const value = (pressed ? (pinInfo.pull === "up" ? 0 : 1) : (pinInfo.pull === "up" ? 1 : 0)) as 0 | 1
const value = (
pressed ? (pinInfo.pull === "up" ? 0 : 1) : pinInfo.pull === "up" ? 1 : 0
) as 0 | 1
const inputEvent: InputGroupEvent<T> = { pin: pinInfo.name as T, value, timestamp }
for (const listener of this.#eventListeners) {
listener(inputEvent)
}
await Bun.sleep(0)
}
}
} finally {

72
test.ts
View File

@ -1,72 +0,0 @@
#!/usr/bin/env bun
/**
* Basic functionality test for Buzz library
* Tests device listing, player, recorder, and tone generation
*/
import Buzz from "./buzz"
console.log("🎵 Buzz Audio Library - Basic Test\n")
// Test 1: List devices
console.log("📋 Listing devices...")
const devices = await Buzz.listDevices()
console.log(`Found ${devices.length} device(s):`)
devices.forEach((d) => {
console.log(` ${d.type.padEnd(8)} ${d.label} (${d.id})`)
})
console.log("")
// Test 2: Create player
console.log("🔊 Creating default player...")
try {
const player = await Buzz.defaultPlayer()
console.log("✅ Player created\n")
// Test 3: Play sound file
console.log("🔊 Playing greeting sound...")
const playback = await player.play("./sounds/greeting/greet1.wav")
await playback.finished()
console.log("✅ Sound played\n")
// Test 4: Play tone
console.log("🎵 Playing 440Hz tone for 1 second...")
const tone = await player.playTone([440], 1000)
await tone.finished()
console.log("✅ Tone played\n")
} catch (error) {
console.log(`⚠️ Skipping player tests: ${error instanceof Error ? error.message : error}\n`)
}
// Test 5: Create recorder
console.log("🎤 Creating default recorder...")
try {
const recorder = await Buzz.defaultRecorder()
console.log("✅ Recorder created\n")
// Test 6: Stream recording with RMS
console.log("📊 Recording for 2 seconds with RMS monitoring...")
const recording = recorder.start()
let chunkCount = 0
let maxRMS = 0
setTimeout(async () => {
await recording.stop()
}, 2000)
for await (const chunk of recording.stream()) {
chunkCount++
const rms = Buzz.calculateRMS(chunk)
if (rms > maxRMS) maxRMS = rms
if (chunkCount % 20 === 0) {
console.log(` RMS: ${Math.round(rms)}`)
}
}
console.log(`✅ Recorded ${chunkCount} chunks, max RMS: ${Math.round(maxRMS)}\n`)
} catch (error) {
console.log(`⚠️ Skipping recorder tests: ${error instanceof Error ? error.message : error}\n`)
}
console.log("✅ All tests complete!")