Sitelet https://github.com/spatialaudio/python-sounddevice/issues/547
Skip to content

Having Difficulty trying to create an asynchronous playback #547

Description

@DragonwolfAside

I'm currently up to a project that needs an asynchronous audio support, and I need both the player process and the generator process work at the same time. I've been following your code on readthedocs, but I still cannot figure things out. Here's my code written:

import asyncio

import sounddevice as sd
import soundfile as sf
import numpy as np


fil = sf.SoundFile('files/snd.wav', 'r')


class Engine:
    def __init__(self, sr=48000, blocksize=512, buf_size=2048, channels=2, dtype='float32'):
        self.sr = sr
        self.blocksize = blocksize
        self.channels = channels
        self.dtype = dtype

        self.buf_size = buf_size
        self.buffer = np.zeros((self.buf_size, self.channels), dtype=self.dtype)
        self.stop_event = asyncio.Event()

        self.pos = 0  # read ptr
        self.end = 0  # data end ptr
        self.delta = self.buf_size  # spare memory space
        self.remain = 0  # remaining data

    async def reader(self):
        # Pseudo generator, replacing with other code later
        while True:
            if self.end > self.pos:  # case loopback
                self.buffer[self.end:] = (fil.read(self.buf_size - self.end)
                                          .reshape(self.buf_size - self.end, self.channels))
                self.buffer[:self.pos] = fil.read(self.pos).reshape(self.pos, self.channels)
            else:
                self.buffer[self.end:self.pos] = (fil.read(self.pos - self.end)
                                                  .reshape(self.pos - self.end, self.channels))
            await asyncio.sleep(0)  # yield

    async def callback(self, outdata, frame_count, time_info, status):
        print(f"\rCursor: {self.pos} End: {self.end}\nRemain: {self.remain} Delta: {self.delta}", end="")

        if self.end > self.pos:  # case loopback
            self.remain = self.end - self.pos
            self.delta = self.buf_size - self.remain - 1
        elif self.end < self.pos:
            self.delta = self.pos - self.end - 1
            self.remain = self.buf_size - self.delta + 1
        else:
            print(" !! No Data", end="")

        if self.pos + frame_count <= self.remain:  # sufficient data
            if self.end > self.pos:  # case loopback
                rear = self.buf_size - self.pos - 1  # read
                outdata[:rear] = self.buffer[self.pos:]
                outdata[rear:] = self.buffer[:self.end]
                self.pos = frame_count - rear
            else:
                outdata[:self.remain] = self.buffer[self.pos:self.end]  # read
                self.pos += frame_count

        else:  # buffer underflow
            under = frame_count - self.remain
            print(f"\n!!!Buffer Underflow, under={under}")

            if self.end > self.pos:  # case loopback
                rear = self.buf_size - self.pos - 1  # read rest of data
                outdata[:rear] = self.buffer[self.pos:]
                outdata[rear:] = self.buffer[:self.end]
                outdata[-under:] = np.zeros((under, self.channels), dtype=self.dtype)  # return zeros
            else:
                outdata[:self.remain] = self.buffer[self.pos:self.end]  # read rest of data
                outdata[-under:] = np.zeros((under, self.channels), dtype=self.dtype)  # return zeros

            self.pos = self.end

    def start_stream(self, **kwargs):
        print(self.dtype, self.blocksize, self.channels, self.sr)

        stream = sd.OutputStream(
            callback=self.callback, dtype=self.dtype, blocksize=self.blocksize, channels=self.channels,
            samplerate=self.sr, **kwargs
        )

        with stream:
            await self.stop_event.wait()

async def main():
    engine = Engine(channels=1)
    await asyncio.gather(
        engine.reader(),
        engine.start_stream()
    )


if __name__ == '__main__':
    asyncio.run(main())
    input("Program Exited...")

I knew some tricks like sync2async blocking2async, and I tried without callback, but the thread is still blocked by stream.write().

Here's my tricks:

import asyncio


loop = asyncio.new_event_loop()


async def sync2async(func, *args):
    return await loop.run_in_executor(None, func, *args)


async def blocking2async(func, *args):
    coro = loop.run_in_executor(None, func, *args)
    done, pending = await asyncio.wait([coro], return_when=asyncio.FIRST_COMPLETED)
    if coro in done:
        return coro.result()

Here's my modified methods

class Engine:

    ...

    async def callback(self):
        print(f"\rCursor: {self.pos} End: {self.end}\nRemain: {self.remain} Delta: {self.delta}", end="")
        outdata = np.zeros((self.blocksize, self.channels), dtype=self.dtype)
        ...
        return outdata

    async def writer(self):
        while True:
            await blocking2async(self.stream.write, await self.callback())
            await asyncio.sleep(0)  # yield

    def start_stream(self):
        print(self.dtype, self.blocksize, self.channels, self.sr)
        self.stream.start()


async def main():
    engine = Engine(channels=1)
    engine.start_stream()
    await asyncio.gather(
        engine.reader(),
        engine.writer()
    )

When I run this code, I noticed that the program stuck at done, pending = await asyncio.wait([coro], return_when=asyncio.FIRST_COMPLETED) and it keeps awaiting.

Really we can't use asyncio in sounddevice?

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions