From 843f3680e1a54543f0a75a837c9cc161f4ea129c Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Tue, 18 Nov 2025 17:43:12 -0800 Subject: [PATCH] wip --- hum1.wav => sounds/stalling/hum1.wav | Bin hum2.wav => sounds/stalling/hum2.wav | Bin src/phone.ts | 662 +++++++++++++++------------ src/pins/input-group.ts | 41 +- test.ts | 72 --- 5 files changed, 407 insertions(+), 368 deletions(-) rename hum1.wav => sounds/stalling/hum1.wav (100%) rename hum2.wav => sounds/stalling/hum2.wav (100%) delete mode 100755 test.ts diff --git a/hum1.wav b/sounds/stalling/hum1.wav similarity index 100% rename from hum1.wav rename to sounds/stalling/hum1.wav diff --git a/hum2.wav b/sounds/stalling/hum2.wav similarity index 100% rename from hum2.wav rename to sounds/stalling/hum2.wav diff --git a/src/phone.ts b/src/phone.ts index c32857a..8bbf597 100644 --- a/src/phone.ts +++ b/src/phone.ts @@ -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]) +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 { + ringer.value = 0 + } + break - 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" }) - } - }) + case "rotaryInUse": + if (event.value === 0) { + digit = 0 + } else { + log.info(`šŸ“ž Dialed digit: ${digit}`) + } + break - baresip.callReceived.connect(({ contact }) => { - log.info(`🐻 incoming call from ${contact}`) - phoneService.send({ type: "incoming_call", from: contact }) - }) + case "rotaryNumber": + if (event.value === 1) { + digit += 1 + } + break - 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) - } - 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") - } + default: + log.error(`šŸ“ž Unknown pin event: ${event.pin}`) + break } } +} - const context = (initial?: Partial): PhoneContext => ({ - numberDialed: 0, - baresip, - startAgent, - ...initial, - }) +const apiKey = process.env.ELEVEN_API_KEY +const agentId = process.env.ELEVEN_AGENT_ID - 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 (!apiKey) { + console.error("āŒ Error: ELEVEN_API_KEY environment variable is required") + process.exit(1) +} + +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 => ({ +// 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 +// } diff --git a/src/pins/input-group.ts b/src/pins/input-group.ts index 8804fb0..f3fb1d8 100644 --- a/src/pins/input-group.ts +++ b/src/pins/input-group.ts @@ -26,8 +26,17 @@ export class InputGroup { } } - get pins(): Record Promise }> { - const result = {} as Record Promise }> + get pins(): Record< + T, + { readonly value: 0 | 1; waitForValue: (targetValue: 0 | 1, timeout?: number) => Promise } + > { + const result = {} as Record< + T, + { + readonly value: 0 | 1 + waitForValue: (targetValue: 0 | 1, timeout?: number) => Promise + } + > for (const [name, config] of this.#pinMap) { const offset = config.offset @@ -43,9 +52,10 @@ export class InputGroup { 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 { if (this.#closed) throw new Error("InputGroup is closed") const eventQueue: InputGroupEvent[] = [] - let resolve: (() => void) | undefined - const listener = (event: InputGroupEvent) => { eventQueue.push(event) - resolve?.() } this.#eventListeners.push(listener) @@ -109,14 +116,14 @@ export class InputGroup { try { while (!this.#closed) { - if (eventQueue.length === 0) { - await new Promise((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 { 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 = { pin: pinInfo.name as T, value, timestamp } for (const listener of this.#eventListeners) { listener(inputEvent) } + + await Bun.sleep(0) } } } finally { diff --git a/test.ts b/test.ts deleted file mode 100755 index 3dffde2..0000000 --- a/test.ts +++ /dev/null @@ -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!")