Playing Audio with Python sounddevice in Raspberry Pi Zero W works Exactly once and then I need to reboot

I was inspired by this project to put a Pi into an old rotary telephone. I’m using python to bring it to life and perform the functions I would like to perform. The first of these is pretty simple: play a dial tone when I pick up the receiver.

However, when I try to play audio using the sounddevice module, it will work the first time, and then stop working. And when I say stop working, I can no longer play audio with aplay or speaker-test or anything else until I reboot. It makes me think that something is not closing out correctly at the end of playback.

Hardware Setup:

Below is the code for my class, DialTone which is meant to be a thread run by my core application so it can start/stop the dial tone whenver necessary. At the bottom is a main meant to test the capability. It works just fine, over and over on Windows, but when I run it on the Pi: just once. Meanwhile I can aplay the same file over and over without issue (provided I haven’t tried to play it with python).

class DialTone(Thread):

    def __init__(self, input_queue:Queue, device:int_or_str ) -> None:
        """Initialize the dialtone class 

        Args:
            input_queue (Queue): A way to communicate with the thread
        """
        super().__init__()
        global _DIAL_TONE_FILE

        self.input_queue = input_queue
        sd.default.device = device

        # Commit the dial-tone data to memory so we can start/end quickly
        try:
            self.data, self.fs = sf.read(_DIAL_TONE_FILE, always_2d=True)
        except Exception as e:
            logging.debug( type(e).__name__ + ":" + str(e) )
    
    def run(self) -> None:
        """Play a dial tone in a continuous loop until we die
        """
        while( True ):
            cmd = self.input_queue.get(True)
            if( cmd == DialToneComms.DT_START ):
                logging.debug("DT: starting dial tone")
                sd.play(self.data, self.fs, loop=True)
                logging.debug(sd.get_status())
            elif( cmd == DialToneComms.DT_END ):
                logging.debug("DT: stopping dial tone")
                try:
                    sd.stop(ignore_errors=False)
                except Exception as e:
                    logging.error( type(e).__name__ + ":" + str(e) )
                logging.debug(sd.get_status())
            elif( cmd == DialToneComms.DT_KILL ):
                logging.debug("DT: killing self")
                try:
                    sd.stop(ignore_errors=False)
                except Exception as e:
                    logging.error( type(e).__name__ + ":" + str(e) )
                logging.debug(sd.get_status())
                break

# Test this capability
if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--device",type=int_or_str,help="Audio device to use", default=1)
    parser.add_argument("--play_time",type=float,help="how long to play the dial tone",default=10.0)
    parser.add_argument("--num_cycles", type=int, help="Number of play, wait cycles", default=2)
    args = parser.parse_args()

    # Setup logging
    logging.basicConfig(level=logging.DEBUG)

    dial_tone_queue = Queue()
    print( "Instantiating DialTone" )
    dial_tone = DialTone(dial_tone_queue, args.device)
    print( "Starting DialTone Thread" )
    dial_tone.start()

    # Play the dial tone a number of iterations
    for _ in range(args.num_cycles):
        print( "Playing dialtone for {} sec".format(args.play_time))
        dial_tone_queue.put(DialToneComms.DT_START)
        time.sleep(args.play_time)
        dial_tone_queue.put(DialToneComms.DT_END)
        
        print("Waiting 2 seconds")
        time.sleep(2)

    print("Kill the thread")
    dial_tone_queue.put(DialToneComms.DT_KILL)
    print("Joining DialTone")
    dial_tone.join(_JOIN_WAIT_TIME)

    if( dial_tone.is_alive() ):
        logging.warning( "Dial tone thread is still alive! What gives?" )
    else:
        logging.warning( "Dial tone thread is nice and dead")

Output of first run on Pi. The error at the bottom is a result of me stopping a stream that’s not playing (the stop call already killed it, and the kill call is just making sure).

Instantiating DialTone
Starting DialTone Thread
Playing dialtone for 3.0 sec
DEBUG:root:DT: starting dial tone
DEBUG:root:
Waiting 2 seconds
DEBUG:root:DT: stopping dial tone
DEBUG:root:
Playing dialtone for 3.0 sec
DEBUG:root:DT: starting dial tone
DEBUG:root:
Waiting 2 seconds
DEBUG:root:DT: stopping dial tone
DEBUG:root:
Playing dialtone for 3.0 sec
DEBUG:root:DT: starting dial tone
DEBUG:root:
Waiting 2 seconds
DEBUG:root:DT: stopping dial tone
DEBUG:root:
Kill the thread
Joining DialTone
DEBUG:root:DT: killing self
ERROR:root:PortAudioError:Error stopping stream: Invalid stream pointer [PaErrorCode -9988]
DEBUG:root:
WARNING:root:Dial tone thread is nice and dead

Output of 2nd Run on Pi. note that things start happening out of order, particularly after the stop, but audio never plays on the second time.

Instantiating DialTone
Starting DialTone Thread
Playing dialtone for 3.0 sec
DEBUG:root:DT: starting dial tone
DEBUG:root:
Waiting 2 seconds
DEBUG:root:DT: stopping dial tone
Playing dialtone for 3.0 sec
Waiting 2 seconds
Playing dialtone for 3.0 sec
Waiting 2 seconds
Kill the thread
Joining DialTone
DEBUG:root:
DEBUG:root:DT: starting dial tone
DEBUG:root:
DEBUG:root:DT: stopping dial tone
WARNING:root:Dial tone thread is still alive! What gives?
DEBUG:root:
DEBUG:root:DT: starting dial tone
DEBUG:root:
DEBUG:root:DT: stopping dial tone
DEBUG:root:
DEBUG:root:DT: killing self
ERROR:root:PortAudioError:Error stopping stream: Invalid stream pointer [PaErrorCode -9988]
DEBUG:root:

Output on Windows:

Instantiating DialTone
Starting DialTone Thread
Playing dialtone for 3.0 sec
DEBUG:root:DT: starting dial tone
DEBUG:root:
Waiting 2 seconds
DEBUG:root:DT: stopping dial tone
DEBUG:root:
Playing dialtone for 3.0 sec
DEBUG:root:DT: starting dial tone
DEBUG:root:
DEBUG:root:DT: stopping dial tone
Waiting 2 seconds
DEBUG:root:
Playing dialtone for 3.0 sec
DEBUG:root:DT: starting dial tone
DEBUG:root:
Waiting 2 seconds
DEBUG:root:DT: stopping dial tone
DEBUG:root:
Kill the thread
DEBUG:root:DT: killing self
Joining DialTone
ERROR:root:PortAudioError:Error stopping stream: Invalid stream pointer [PaErrorCode -9988]
DEBUG:root:
WARNING:root:Dial tone thread is nice and dead

Leave a Comment