# easyaudiostream

Simple playback for intermittent audio byte streams in Python.

I built this library because I was tired of existing audio playback solutions in Python not supporting audio *streams*
particularly well (compared to audio *files*). *easyaudiostream* is intended for simple, high-enough-fidelity playback
of audio byte streams:

- No choppiness between bytestream segments
- Handles non-real-time streams -- faster and slower than real-time
- Handles intermittent streams (i.e., streams that may not yield bytes for a while)
- asyncio-safe -- no blocking computation on main thread

_easyaudiostream_ also includes basic utilities for recording audio from a microphone.

## Installation

Requires Python 3.8 or higher.

By default, easyaudiostream will use the `ffmpeg` (if it is installed) or the bundled PyDub library for playback.

```shell
$ pip install easyaudiostream
```

> [!NOTE]
> You can also install PyAudio to slightly improve playback startup time and enable the microphone input utilities:
>
> ```shell
> $ pip install easyaudiostream[pyaudio]
> ```
>
> This is not installed by default due to compatibility issues across operating systems.

## Usage

There are two main ways of playing an audio stream: using an iterator (if your audio generator yields bytes) or
callbacks (if your audio generator calls a function with generated bytes).

### Iterator Mode

For iterator mode, use the following two functions:

```python
def play_stream(audio_stream: Iterable[bytes]):
    """
    Consume bytes from the audio stream and play them.

    .. note::
        This function will return as soon as the stream is exhausted!
        If your program ends after calling this function, the audio will not play - you might need to ``sleep(...)``.
    """


def play_raw_stream(
    audio_stream: Iterable[bytes], *, sample_width: int = 2, channels: int = 1, frame_rate: int = 24000
):
    """
    Consume PCM audio bytes from the audio stream and play them.

    .. warning::
        If you aren't sure which function to use, use :func:`.stream_audio` instead! Playing raw audio bytes is
        a minor optimization that avoids converting the audio format, but requires a specific input format.

    .. note::
        This function will return as soon as the stream is exhausted!
        If your program ends after calling this function, the audio will not play - you might need to ``sleep(...)``.

    By default, this method accepts raw 16 bit PCM audio at 24kHz, 1 channel, little-endian. You can control the
    expected format of the raw audio using the keyword arguments.
    """

```

### Callback Mode

For callback mode, use the following two functions:

```python
def play_audio(audio_bytes: bytes):
    """
    Play the given audio at the next available opportunity, using a global audio queue.

    .. note::
        This function will return immediately - it just puts the audio on a queue!
        If your program ends after calling this function, the audio will not play - you might need to ``sleep(...)``.
    """


def play_raw_audio(audio_bytes: bytes, *, sample_width: int = 2, channels: int = 1, frame_rate: int = 24000):
    """
    Play the given raw audio at the next available opportunity, using a global audio queue.

    .. warning::
        If you aren't sure which function to use, use :func:`.play_audio` instead! Playing raw audio bytes is
        a minor optimization that avoids converting the audio format, but requires a specific input format.

    .. note::
        This function will return immediately - it just puts the audio on a queue!
        If your program ends after calling this function, the audio will not play - you might need to ``sleep(...)``.

    By default, this method accepts raw 16 bit PCM audio at 24kHz, 1 channel, little-endian. You can control the
    expected format of the raw audio using the keyword arguments.
    """
```

### Record Mic

The following functions are available to record audio from a mic:

```python
def get_mic_stream(mic_id: int | None) -> Iterable[bytes]:
    """Return an audio stream manager that yields audio frames from the given mic."""


def get_mic_stream_async(mic_id: int | None) -> AsyncIterable[bytes]:
    """Return an audio stream manager that yields audio frames from the given mic."""


def list_mics():
    """Print a list of all microphones connected to this device."""
```

You can use `python -m easyaudiostream` to view a list of connected microphones.

## Examples

```python
import time

from easyaudiostream import play_stream

# load dummy audio from file -- normally you would get an audio stream from somewhere else
# in this example, test_audio.wav contains 16bLE mono PCM audio at 24kHz
with open("test_audio.wav", "rb") as f:
    audio = f.read()


# yield it in 1s packets every 0.95s
# but every 5s, add a 3s delay
def choppy_stream():
    for sec in range(0, len(audio), 48000 * 5):
        for i in range(0, 48000 * 5, 48000):
            yield audio[sec + i : sec + i + 48000]
            time.sleep(0.95)

        # oh no, my bytes!
        time.sleep(3)


play_stream(choppy_stream())
time.sleep(10)  # to allow all the audio to play

```
