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
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.