pythonaudio-recordingpython-sounddevice

Slight delay in beginning of recording, and end gets cut off with low level streams in python-sounddevice


I'm trying to create a low-level Stream that will allow me to output a WAVE file, while simultaneously recording an input on the same audio device. My audio device is setup so that the output WAVE file will be played through the output and this runs through a system that then goes to an input on the device. Using the convenience function playrec() from python-sounddevice gives me a full recording of the what's seen at the input, however with my code using the lower-level Stream() function the recording starts late and the last tiny bit of the audio isn't recorded. The reason I want to use the lower-level Stream() function is to test whether or not I can decrease the overall delay in that system compared to playrec(). I tried changing the blocksize and buffersize to no avail.

def callback(indata, outdata, frames, time, status):
  assert frames == args.blocksize
  qr.put(indata.copy())
  rec_file.write(qr.get())
  if status.output_underflow:
    print('Output underflow: increase blocksize?', file=sys.stderr)
    raise sd.CallbackAbort
  assert not status
  try:
    data = q.get_nowait()
  except queue.Empty:
    print('Buffer is empty: increase buffersize?', file=sys.stderr)
    raise sd.CallbackAbort
  if data.size < outdata.size:
    outdata[:len(data),0] = data
    outdata[len(data):] = 0
    raise sd.CallbackStop
  else:
    outdata[:,0] = data

try:
    with sf.SoundFile(args.filename) as f:
        #queue for recording input
        qr = queue.Queue(maxsize=args.buffersize)
        #queue for output WAVE file
        q = queue.Queue(maxsize=args.buffersize)
        event = threading.Event()
        for _ in range(args.buffersize):
            data = f.read(frames=args.blocksize, dtype='float32')
            if data.size == 0:
                break
            q.put_nowait(data)  # Pre-fill queue
        stream = sd.Stream(   
            samplerate=f.samplerate, blocksize=args.blocksize,
            dtype='float32', callback=callback, finished_callback=event.set,
            latency='low')
        with sf.SoundFile('output'+str(itr)+'.wav', mode='x', samplerate=f.samplerate,
                          channels=1) as rec_file:
            with stream:
                timeout = args.blocksize * args.buffersize / f.samplerate
                while data.size != 0:
                    data = f.read(args.blocksize, dtype='float32')
                    q.put(data, timeout=timeout)
                event.wait()  # Wait until playback is finished

Solution

  • If you don't mind having the whole input and output signals in memory at once, you should feel free to use sd.playrec(). You will not be able to decrease the latency with your own code using sd.Stream. sd.playrec() internally uses sd.Stream and it adds no latency.

    If you want to reduce the latency, you should try to use lower values for the blocksize and/or latency parameters. Note, however, that low values will be more unstable and might lead to glitches in the playback/recording.


    If you don't want to have all the data at once in memory, you cannot use sd.playrec() and you can try it with sd.Stream, like in your example above.

    Note, however, that the queue in these two adjacent lines is useless at best:

    qr.put(indata.copy())
    rec_file.write(qr.get())
    

    You might as well write:

    rec_file.write(indata)
    

    But please don't!

    Writing to a file might block the audio callback for too long, leading to audio drop-outs.

    Therefore, it is a good idea to use a queue (and it is a good idea to use indata.copy() as well).

    But you should only write to your qr in the callback function. The reading should happen at a different point.

    You should do a non-blocking qr.get_nowait() in the while loop before or after q.put(...) and write the data to the file there.

    In the callback function, you shouldn't do a blocking qr.put(indata.copy()), because again, this might block your audio callback leading to drop-outs. Instead, you should use qr.put_nowait(). To avoid a full queue, you should remove the maxsize argument from qr (but keep it on the other queue!).

    Finally, after leaving the with stream context manager, there might still be data left in qr which hasn't yet been written to the file.

    So after the stream is closed, you should make sure to empty the "recording queue" and write the remaining blocks to the file.