spatialaudio / python-sounddevice

:sound: Play and Record Sound with Python :snake:
https://python-sounddevice.readthedocs.io/
MIT License
980 stars 145 forks source link

Advice on detecting when Sounddevice has stopped playing audio (and using multiple OutputStreams) #541

Open andrewfr opened 3 weeks ago

andrewfr commented 3 weeks ago

I am writing the audio component for a digital assistant. An important feature is the ability to pause a "thread" producing audio output, (let's call this T_1), start a new one T_2, then when T_2 ends, resume T_1.

Right now, each "thread" has an associated outputStream (and callback). This seems to work but PortAudio literature states that opening multiple OutputStreams is not a good idea. I also do too many things in the callback.

I am looking at "Proposed Enhancements to PortAudio API" 019 []https://www.portaudio.com/docs/proposals/019-NotifyClientWhenAllBuffersHavePlayed.html

I have tried polling with active(). This doesn't work nor do I understand why it should work. As I understand it, a stream is active if it is not stopped. I don't want to open and close the outputStream.

I've reading PortAudio []https://www.portaudio.com/docs/v19-doxydocs/start_stop_abort.html , I've tried , after the callback has finished outputting, putting a thread to sleep for a time based on the estimated duration of the audio .... and a fudge_factor.

audio_context is shared between callback and the producing thread

    def callback(outdata, frames, time, status):

        assert frames == audio_context.block_size
        if status.output_underflow:
            print("Output underflow: increase blocksize?", file=sys.stderr)
            raise sd.CallbackAbort
        assert not status

        try:
            # reading messages from a queue
            message = audio_context.input_queue.get()
            if message is None:
                raise sd.CallbackStop

            data = message.data

        except queue.Empty as e:
            print("Buffer is empty: increase buffersize?", file=sys.stderr)
            raise sd.CallbackAbort from e

        if len(data) < len(outdata):
            outdata[: len(data)] = data
            outdata[len(data) :].fill(0)
            # this should be the last data written by the callback
            audio_context.event.set()
        else:
            outdata[:] = data

in producing thread

# wait until callback is finished
audio_context.event.wait()
# let the audio finish. The a_fudge_factor is < 1.0
time.sleep(duration * a_fudge_factor)

This sort of works. I can't seem to find a more acceptable way of doing this? Also is using multiple outputStreams that bad if one has the resources? Any advice would be appreciated.