"""AudioSocket server — Asterisk connects here per agent-handled call (M4). AudioSocket wire protocol (Asterisk `AudioSocket()` app): 1 byte kind | 2 bytes big-endian payload length | payload kind 0x00 = terminate, 0x01 = UUID (16 raw bytes), 0x10 = SLIN audio, 0xff = error. Audio is 16-bit LE mono PCM @ 8 kHz, ~20 ms per message. Run: python -m gogo.voice.server """ from __future__ import annotations import asyncio import logging import uuid from gogo.config import get_settings from gogo.voice.call import CallSession, Transport log = logging.getLogger("gogo.voice.server") KIND_TERMINATE = 0x00 KIND_UUID = 0x01 KIND_AUDIO = 0x10 KIND_ERROR = 0xFF class AudioSocketTransport(Transport): def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter): self.reader = reader self.writer = writer self._closed = False async def read_message(self) -> tuple[int, bytes] | None: try: header = await self.reader.readexactly(3) except (asyncio.IncompleteReadError, ConnectionResetError): return None kind = header[0] length = int.from_bytes(header[1:3], "big") payload = b"" if length: try: payload = await self.reader.readexactly(length) except (asyncio.IncompleteReadError, ConnectionResetError): return None return kind, payload async def read_frame(self) -> bytes | None: """Next audio frame; skips non-audio messages; None on hangup/close.""" while True: msg = await self.read_message() if msg is None: return None kind, payload = msg if kind == KIND_AUDIO: return payload if kind == KIND_TERMINATE: return None if kind == KIND_ERROR: log.warning("audiosocket error frame: %s", payload[:64]) async def send_audio(self, pcm: bytes) -> None: if self._closed: return try: self.writer.write(bytes([KIND_AUDIO]) + len(pcm).to_bytes(2, "big") + pcm) await self.writer.drain() except (ConnectionResetError, BrokenPipeError): self._closed = True async def hangup(self) -> None: if self._closed: return self._closed = True try: self.writer.write(bytes([KIND_TERMINATE, 0, 0])) await self.writer.drain() self.writer.close() except (ConnectionResetError, BrokenPipeError): pass async def handle_connection(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: transport = AudioSocketTransport(reader, writer) msg = await transport.read_message() if msg is None or msg[0] != KIND_UUID or len(msg[1]) != 16: log.warning("connection without UUID handshake — dropping") await transport.hangup() return registration_id = uuid.UUID(bytes=msg[1]) log.info("call connected: %s", registration_id) session = CallSession(transport, registration_id) await session.run() async def serve() -> None: s = get_settings() server = await asyncio.start_server(handle_connection, s.voice_host, s.voice_port) addrs = ", ".join(str(sock.getsockname()) for sock in server.sockets) log.info("AudioSocket server listening on %s", addrs) # health heartbeat file for the admin panel / monitoring (M6) async with server: await server.serve_forever() def main() -> None: logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s") asyncio.run(serve()) if __name__ == "__main__": main()