# Python AudioSocket Server

A high performance Asterisk AudioSocket server written in asynchronous Python.

## AudioSocket Introduction

[AudioSocket](https://github.com/CyCoreSystems/audiosocket) is a relatively new addition to the [Asterisk IP-PBX project](https://www.asterisk.org/)
that allows external applications to easily access the raw audio (both read/write) of
active calls, as opposed to audio only being available via RTP.

Such external applications act as a TCP server that Asterisk connects to
after encountering an appropriate line of code in the dialplan. After, Asterisk
will begin sending audio to the specified address/port (the encoding of this
audio varies, as discussed in the tips section). This module is an
implementation of such a TCP server, in Python.

## Module Usage

*Note: Formal documentation is provided below and in the module file itself, for
real-world examples, see the the `examples` directory in the repository.*

This Python module provides a clean and high performance interface for dealing
with AudioSocket connections. To take advantage of the conventions established
by the Python standard library, and to offer the best performance, this module
is modeled around Python's concept of [Protocols](https://docs.python.org/3.11/library/asyncio-protocol.html).

Underneath, this means that all connections are handled within a single thread,
and I/O notification primitives provided by the underlying OS are used to
signal when there is activity on any given connection, preventing the need to
have an full OS thread per-connection. As for users of this module, this means
all interaction with it occurs via two callbacks, and requires `asyncio` to
be imported as well.

The entire module is implemented within a single file, `audiosocket.py`. To
start using it, simply place the file somewhere your project can import it
from. After importing it (`import audiosocket`) and `asyncio`, the server can
be setup with two function calls  - just like `asyncio`'s own `start_server` -
by invoking:
```python
server = await audiosocket.start_server(<on_audio_callback>, <on_exception_callback>, <address>, <port>)`
await server.serve_forever()
```

`start_server()` behaves just like `asyncio.start_server()`, meaning the
`host`, `port` and keyword arguments accepted by that function have the same
behavior with this one.

The function callbacks should accept the following arguments:

`on_audio_callback`, called any time audio data from the connected peer is
received. It is expected to be a function that accepts the following
arguments:
    - `uuid`:      The universally unique ID that identifies this specific
                   call's audio, provided as a hexdecimal string.

    - `peer_name`: A tuple consisting of the IP address and port number of the
                   remote host the audio is being sent from.

    - `audio`:     A `bytearray` instance containing the received audio data.
                   An empty `bytearray` instance (`len(audio) == 0`)
                   indicates the call hung up and no more audio will
                   be received. Audio is either encoded in 8KHz, 16-bit
                   mono PCM (when using the standalone dialplan applcation), or
                   whatever audio codec was decided upon during call setup
                   (when using Dial() application). If this argument is empty
                   (has a length of 0), the call has been hung up and will not
                   generate any more audio.

    - Any extra keyword arguments are passed along to
      `asyncio.loop.create_server()`.

To send audio back to Asterisk, a bytes-like object must be returned by
this callback. Audio must be sent back in chunks of 65,536 bytes or less
(the size must be able to fit into a 16-bit unsigned integer). This audio must
always be encoded as 8KHz, 16-bit mono PCM, regardless of the codec in use
for the call.

Returning `audiosocket.HANGUP_CALL_MESSAGE` (or an empty `bytearray` instance)
will request that the call represented by the value of the `uuid` parameter
be hungup.

`on_exception_callback`, called any time an exception relating to the
connected peer is raised. It is expected to be a function that accepts the
following arguments:
    - `uuid`:      The universally unique ID that identifies the specific
                   call which caused the exception.

    - `peer_name`: A tuple consisting of the IP address and port number of the
                   remote host the exception-causing call came from.

    - `error`:     An instance of the exception that occurred.

## Tips

- There are two ways to begin an AudioSocket connection within Asterisk, via a
standalone dialplan application (`AudioSocket(<uuid>,<address:port>)`) or as a
channel driver to `Dial()` (`Dial(AudioSocket/<address:port>/<uuid>)`). Using
the standalone application will cause Asterisk to send the server 8KHz, 16-bit
mono PCM (see Issues section). Using the `Dial()` application will cause
Asterisk to send audio encoded in whatever codec was agreed upon during call
setup (most commonly ULAW).

- Since the programming model with this module is callback-based (which is not
all that common), as opposed to making state that must persist between function
calls global, a recommended approach is to make the callback functions methods
in a class. That way instance variables can be used to reduce the scope of such
variables, while still allowing the callback paradigm to work as intended.

## Issues (as of my testing with Asterisk 18)

- If the standalone dialplan application is used to initiate an AudioSocket
connection, delivering PCM audio to th server, the CPU core/thread the
connection within Asterisk is running on will reach 100% utilization (likely
a runaway loop, which I will try to identify and fix/report to them).


- If there is a wireless medium at any point in the network link between
Asterisk and the AudioSocket server, the Asterisk module encounters strange
read/write errors. This is a problem with the Asterisk module/Asterisk itself.

