python-sounddevice icon indicating copy to clipboard operation
python-sounddevice copied to clipboard

Saving InputStream through soundfile vs. sd.rec()

Open dbs176 opened this issue 4 years ago • 3 comments

I have been trying to apply the arbitrary recording length example, but it appears as if the sounddevice.InputStream and soundfile.SoundFile are not fully cooperating with each other, or at least they produce a different type of output than sounddevice.rec does on its own with scipy.io.wavfile.write. It produces a wav file that will play on the system player, but won't concatenate or play nicely through python.

Is there something I am missing in order to get this to work?

import soundfile as sf
import sounddevice as sd
import time, queue, multiprocessing, os
from scipy.io.wavfile import write

from pydub import AudioSegment
import simpleaudio as sa

soundfile_wav = '/tmp/rec.wav'
sounddevice_wav = '/tmp/rec_simple.wav'
test_wav = '/tmp/test.wav'

q = queue.Queue()
recorder = False

# for reference
sf_subtypes = ['ALAW', 'DOUBLE', 'FLOAT', 'G721_32', 'GSM610', 'IMA_ADPCM',
               'MS_ADPCM', 'PCM_16', 'PCM_24', 'PCM_32', 'PCM_U8', 'ULAW']
sd_dtypes = ['float32', 'int32', 'int16']  
sd_rare_dtypes = ['int8', 'uint8', 'int24','float64']

subtype = 'PCM_16'
dtype = 'int16' 



# recorder, simple
def simple_record():
    sd.default.samplerate = 44100
    sd.default.channels = 1
    myrecording = sd.rec(int(1 * 44100),dtype=dtype)
    sd.wait() 
    write(sounddevice_wav, 44100, myrecording)


# recorder, based on arbitrary duration example
def complicated_record():
    with sf.SoundFile(soundfile_wav, mode='w', samplerate=44100, 
                      subtype=subtype, channels=1) as file:
        with sd.InputStream(samplerate=44100.0, dtype=dtype, 
                            channels=1, callback=complicated_save):
            while True:
                file.write(q.get())

def complicated_save(indata, frames, time, status):
    q.put(indata.copy())




def start():
    global recorder
    recorder = multiprocessing.Process(target=complicated_record)
    recorder.start()

def stop():
    global recorder
    recorder.terminate()
    recorder.join()




def test(f):
    # play on system 
    os.system('omxplayer '+f)
    
    # import into pydub
    wavfile=AudioSegment.from_wav(f)
    print("Length:",len(wavfile))

    # play with simpleaudio
    audio=sa.WaveObject.from_wave_file(f)
    audio.play()
    time.sleep(1) #let it play





print('start simple recording')
simple_record()
print('stop simple recording')

time.sleep(1)

print('start complicated recording')
start()
time.sleep(1)
stop()
print('stop complicated recording')

# all tests work for simple recorder
test(sounddevice_wav)

# only system can play wav file from complicated recorder
test(soundfile_wav)

I freely admit that I rarely work with audio and I am very much learning as I'm going, and thank you for any help in advance.

dbs176 avatar Feb 16 '21 06:02 dbs176

Thanks for the report!

I cannot reproduce the problem because I get an error (on Linux):

sounddevice.PortAudioError: Error opening InputStream: Illegal combination of I/O devices [PaErrorCode -9993]

I think this may have to do with problems related to multiprocessing, see also #309, #120, #147, #245, etc.

Can you reproduce your original problem without using multiprocessing?

mgeier avatar Feb 16 '21 09:02 mgeier

Hi Matthias,

Thank you so much for the response!

It looks like changing the sounddevice.InputStream recorder from a multiprocessing.Process to a threading.Thread worked, at least in my simple case. My goal is to use a keyboard keypress to start and stop the recording. The updated code is below.

I was confused because the resulting wav file would play in many other players, though they may have more error checking than some of the python solutions.

I see in #309 you mentioned talking with a queue, do you have a pattern you can suggest or some key words/topics I can search for this? I still consider myself an "advanced beginner."

# based on arbitrary duration example
import queue, threading, sys, time
import sounddevice as sd
import soundfile as sf
import numpy  # Make sure NumPy is loaded before it is used in the callback
assert numpy  # avoid "imported but unused" message (W0611)

f = "/tmp/rec_threading.wav"

subtype = 'PCM_16'
dtype = 'int16' 

q = queue.Queue()
recorder = False

def rec():
    with sf.SoundFile(f, mode='w', samplerate=44100, 
                      subtype=subtype, channels=1) as file:
        with sd.InputStream(samplerate=44100.0, dtype=dtype, 
                            channels=1, callback=save):
            while getattr(recorder, "record", True):
                file.write(q.get())

def save(indata, frames, time, status):
    q.put(indata.copy())

def start():
    global recorder
    recorder = threading.Thread(target=rec)
    recorder.record = True
    recorder.start()

def stop():
    global recorder
    recorder.record = False
    recorder.join()
    recorder = False

# main
print('start recording')
start()
time.sleep(1)
print('...still recording...')
time.sleep(1)
stop()
print('stop recording')


#test the recording
import os
from pydub import AudioSegment
import simpleaudio as sa

# play on system 
os.system('omxplayer '+f)

# import into pydub
wavfile=AudioSegment.from_wav(f)
print("Length:",len(wavfile))

# play with simpleaudio
audio=sa.WaveObject.from_wave_file(f)
audio.play()
time.sleep(2) #let it play

dbs176 avatar Feb 16 '21 16:02 dbs176

It looks like changing the sounddevice.InputStream recorder from a multiprocessing.Process to a threading.Thread worked, at least in my simple case.

In many cases you don't even need to create a new threading.Thread, since the PortAudio callback is automatically called in a separate thread anyway.

So you might be able to start a stream, do some other stuff while it is running, and then stop the stream and take care of the recorded data, all without a separate threading.Thread.

Sometimes, however, it might indeed be necessary to create a new threading.Thread. For an example have a look at the rec_gui.py example.

My goal is to use a keyboard keypress to start and stop the recording.

Keypress should be fine, recording while holding down a key might be more complicated. For an example see https://github.com/spatialaudio/python-rtmixer/blob/master/examples/sampler.py.

I was confused because the resulting wav file would play in many other players, though they may have more error checking than some of the python solutions.

What exactly is the problem?

The files written by the soundfile module should normally be playable in all players (unless you use some exotic settings).

I have tried your example code (replacing omxplayer with another player that I have installed on my system) and it seems to work fine.

If the recorded file is broken, can you upload it somewhere for me to have a look?

I see in #309 you mentioned talking with a queue, do you have a pattern you can suggest or some key words/topics I can search for this? I still consider myself an "advanced beginner."

Note that there are different kinds of queues in the standard library (but you have used the right one in your example code):

  • for multi-threading: https://docs.python.org/3/library/queue.html#queue.Queue
  • for multiprocessing: https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Queue
  • for asyncio: https://docs.python.org/3/library/asyncio-queue.html#asyncio.Queue

Some of the sounddevice examples use the first one. There is even an example that uses two types of queues: asyncio_generators.py. I consider this an exotic example, though.

If you want to go more low-level, you can use https://github.com/spatialaudio/python-rtmixer which uses yet another type of queue: https://github.com/spatialaudio/python-pa-ringbuffer. It's probably better to familiarize yourself with the standard library queues first, though.

Other than that, I don't really know any concrete resources.

But feel free to ask questions in this issue tracker!

mgeier avatar Feb 17 '21 15:02 mgeier