-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PyAudioWPatch does not run with threading #4
Comments
Hello🖐 Thanks for the issue. When you call from _spinner_helper import Spinner
import pyaudiowpatch as pyaudio
import time
import wave
import threading
duration = 5.0
filename = "0loopback_record_class.wav"
class AudioRecorder:
def __init__(self, p_audio: pyaudio.PyAudio):
super().__init__()
self.p = p_audio
self.spinner = Spinner()
self.spinner.start()
try: # Get default WASAPI info
wasapi_info = self.p.get_host_api_info_by_type(pyaudio.paWASAPI)
except OSError:
self.spinner.print("Looks like WASAPI is not available on the system. Exiting...")
self.spinner.stop()
# Get default WASAPI speakers
sys_default_speakers = self.p.get_device_info_by_index(wasapi_info["defaultOutputDevice"])
self.default_speakers = None
if not sys_default_speakers["isLoopbackDevice"]:
for loopback in self.p.get_loopback_device_info_generator():
if sys_default_speakers["name"] in loopback["name"]:
self.default_speakers = loopback
break
else:
self.spinner.print("Default loopback output device not found.\n\nRun `python -m pyaudiowpatch` to check available devices.\nExiting...\n")
self.spinner.stop()
exit()
self.wave_file = wave.open(filename, 'wb')
self.wave_file.setnchannels(self.default_speakers["maxInputChannels"])
self.wave_file.setsampwidth(pyaudio.get_sample_size(pyaudio.paInt24))
self.wave_file.setframerate(int(self.default_speakers["defaultSampleRate"]))
def callback(self, in_data, frame_count, time_info, status):
"""Write frames and return PA flag"""
self.wave_file.writeframes(in_data)
return (in_data, pyaudio.paContinue)
def start_recording(self, duration):
self.spinner.print(f"Recording from: ({self.default_speakers['index']}){self.default_speakers['name']}")
self.stream = self.p.open(format=pyaudio.paInt24,
channels=self.default_speakers["maxInputChannels"],
rate=int(self.default_speakers["defaultSampleRate"]),
frames_per_buffer=pyaudio.get_sample_size(pyaudio.paInt24),
input=True,
input_device_index=self.default_speakers["index"],
stream_callback=self.callback
)
self.spinner.print("Recording...")
def close(self):
self.stream.stop_stream()
self.stream.close()
self.wave_file.close()
self.spinner.stop()
if __name__ == "__main__":
ar = AudioRecorder(pyaudio.PyAudio())
ar.start_recording(duration)
#~~~ Some code that will work in parallel with audio recording
time.sleep(duration) # Blocking execution
#~~~
ar.close() You didn't describe your specific task. But if you fundamentally need to wrap this code in another thread, then you can do it as follows:
from _spinner_helper import Spinner
import pyaudiowpatch as pyaudio
import time
import wave
import threading
duration = 5.0
filename = "loopback_record_class.wav"
def threaded(fn):
def wrapper(*args, **kwargs):
thread = threading.Thread(target=fn, args=args, kwargs=kwargs)
thread.start()
return thread
return wrapper
class AudioRecorder:
def __init__(self):
super().__init__()
self.p = pyaudio.PyAudio()
self.spinner = Spinner()
self.spinner.start()
try: # Get default WASAPI info
wasapi_info = self.p.get_host_api_info_by_type(pyaudio.paWASAPI)
except OSError:
self.spinner.print("Looks like WASAPI is not available on the system. Exiting...")
self.spinner.stop()
# Get default WASAPI speakers
default_speakers = self.p.get_device_info_by_index(wasapi_info["defaultOutputDevice"])
if not default_speakers["isLoopbackDevice"]:
for loopback in self.p.get_loopback_device_info_generator():
if default_speakers["name"] in loopback["name"]: self.default_speakers = loopback; break
else:
self.spinner.print("Default loopback output device not found.\n\nRun `python -m pyaudiowpatch` to check available devices.\nExiting...\n")
self.spinner.stop()
exit()
def start_recording(self, duration):
default_speakers = self.default_speakers
self.spinner.print(f"Recording from: ({default_speakers['index']}){default_speakers['name']}")
wave_file = wave.open(filename, 'wb')
wave_file.setnchannels(default_speakers["maxInputChannels"])
wave_file.setsampwidth(pyaudio.get_sample_size(pyaudio.paInt24))
wave_file.setframerate(int(default_speakers["defaultSampleRate"]))
def callback(in_data, frame_count, time_info, status):
"""Write frames and return PA flag"""
wave_file.writeframes(in_data)
return (in_data, pyaudio.paContinue)
with self.p.open(format=pyaudio.paInt24,
channels=default_speakers["maxInputChannels"],
rate=int(default_speakers["defaultSampleRate"]),
frames_per_buffer=pyaudio.get_sample_size(pyaudio.paInt24),
input=True,
input_device_index=default_speakers["index"],
stream_callback=callback
) as stream:
self.spinner.print(f"The next {duration} seconds will be written to {filename}")
time.sleep(duration) # Blocking execution while playing
wave_file.close()
self.spinner.stop()
# /
# /
# < Wrap all initialization code, not a specific method.
# \ Inheriting from Thread won't help either.
# \
@threaded
def start():
AudioRecorder().start_recording(duration)
if __name__ == "__main__":
ar = start()
#~~~ Some code that will work in parallel with audio recording
time.sleep(duration/2) # Blocking execution
#~~~
ar.join() P.S. Run the code in the thread as you tried - will not work. This 'bug' came here from the original PyAudio (during thread initialization for PyAudio, the OS throws an error when requesting another thread to be allocated) P.P.S. Such a way as you tried is not optimal (creates unnecessary/garbage threads). Use the first option if possible. |
Thanks for your quick and very helpful response ! The following code is a better representation of what is causing the issue in my application:
PS. your alternative #2 code is working fine. Perhaps there is a better way to create class C without causing the problem... |
Interesting finding using the code above but just changing the "class C" part ...
is working, but
is failing. Of course, the second example isn't really great code...
is also working, but will cause the whole AudioRecorder class to be created at each recording session. As-is that would be too slow on older systems, but maybe I can use a synchronization object (e.g. mutex lock) to understand when the AudioRecording object is at the point of actually starting the stream and start the audio source at that moment. |
The spinlock based solution seems to work (more testing needed of course). It needs the extra "@threaded" for the AudioRecording to force a thread yield when it starts sleeping. Without it, everything runs in a single thread and the 5 second sleep is not obeyed. Class C does not need to be a threaded class now, just it's main loop.
|
Your experiments are interesting, but in fact you are reinventing the wheel😉
The easiest way -> write the data received in the callback to the from queue import Queue
import pyaudiowpatch as pyaudio
import wave
filename = "0loopback_record_class.wav"
data_format = pyaudio.paInt24
class ARException(Exception):
"""Base class for AudioRecorder`s exceptions"""
class WASAPINotFound(ARException):
...
class InvalidDevice(ARException):
...
class AudioRecorder:
def __init__(self, p_audio: pyaudio.PyAudio, output_queue: Queue):
self.p = p_audio
self.output_queue = output_queue
self.stream = None
@staticmethod
def get_default_wasapi_device(p_audio: pyaudio.PyAudio):
try: # Get default WASAPI info
wasapi_info = p_audio.get_host_api_info_by_type(pyaudio.paWASAPI)
except OSError:
raise WASAPINotFound("Looks like WASAPI is not available on the system")
# Get default WASAPI speakers
sys_default_speakers = p_audio.get_device_info_by_index(wasapi_info["defaultOutputDevice"])
if not sys_default_speakers["isLoopbackDevice"]:
for loopback in p_audio.get_loopback_device_info_generator():
if sys_default_speakers["name"] in loopback["name"]:
return loopback
break
else:
raise InvalidDevice("Default loopback output device not found.\n\nRun `python -m pyaudiowpatch` to check available devices")
def callback(self, in_data, frame_count, time_info, status):
"""Write frames and return PA flag"""
self.output_queue.put(in_data)
return (in_data, pyaudio.paContinue)
def start_recording(self, target_device: dict):
self.close_stream()
self.stream = self.p.open(format=data_format,
channels=target_device["maxInputChannels"],
rate=int(target_device["defaultSampleRate"]),
frames_per_buffer=pyaudio.get_sample_size(pyaudio.paInt24),
input=True,
input_device_index=target_device["index"],
stream_callback=self.callback
)
def stop_stream(self):
self.stream.stop_stream()
def start_stream(self):
self.stream.start_stream()
def close_stream(self):
if self.stream is not None:
self.stream.stop_stream()
self.stream.close()
self.stream = None
@property
def stream_status(self):
return "closed" if self.stream is None else "stopped" if self.stream.is_stopped() else "running"
if __name__ == "__main__":
p = pyaudio.PyAudio()
audio_queue = Queue()
ar = AudioRecorder(p, audio_queue)
help_msg = 30*"-"+"\n\n\nStatus:\nRunning=%s | Device=%s | output=%s\n\nCommands:\nlist\nrecord {device_index\\default}\npause\ncontinue\nstop\n"
target_device = None
try:
while True:
print(help_msg % (ar.stream_status, target_device["index"] if target_device is not None else "None", filename))
com = input("Enter command: ").split()
if com[0] == "list":
p.print_detailed_system_info()
elif com[0] == "record":
if len(com)>1 and com[1].isdigit():
target_device = p.get_device_info_by_index(int(com[1]))
else:
try:
target_device = ar.get_default_wasapi_device(p)
except ARException as E:
print(f"Something went wrong... {type(E)} = {str(E)[:30]}...\n")
continue
ar.start_recording(target_device)
elif com[0] == "pause":
ar.stop_stream()
elif com[0] == "continue":
ar.start_stream()
elif com[0] == "stop":
ar.close_stream()
wave_file = wave.open(filename, 'wb')
wave_file.setnchannels(target_device["maxInputChannels"])
wave_file.setsampwidth(pyaudio.get_sample_size(data_format))
wave_file.setframerate(int(target_device["defaultSampleRate"]))
while not audio_queue.empty():
wave_file.writeframes(audio_queue.get())
wave_file.close()
print(f"The audio is written to a [{filename}]. Exit...")
break
else:
print(f"[{com[0]}] is unknown command")
except KeyboardInterrupt:
print("\n\nExit without saving...")
finally:
ar.close_stream()
p.terminate() There are no unnecessary threads and unnecessary code. The same logic can be applied when implementing a full-fledged GUI. |
Thanks once again for your suggestions! I have a correctly working implementation now but will consider your suggested code. Here is a simplified summary of the code architecture:
The bottomline though is that the "AudioRecorder" class (for my use case and IMHO) has to be called from a threaded class and so far I have not found a way the do this with the "callback" approach using a seperated instanciation of the pyaudio.PyAudio() class and it's "start_recording" invocation. Will give it another try with your latest suggestion. PS. I am still bit confused about what you mean by "re-inventing the wheel" |
So - which of the options( If you want I could possibly help you specifically with your program (PR). But this is already beyond the scope of this library, so I propose to close this issue. |
My problem has been solved and I now have several working alternatives. Thanks for your time and great help! I hope our code fragments can help others e.g. those with similar unresolved questions on stackoverflow.com and python-forum.io. |
- fixes bug where after recording using speaker the program will crash (found out that its due to threading with stream callback s0d3s/PyAudioWPatch#4) - updated pyaudowpatch to from 0.2.12.5 to 0.2.12.6 - rename file typo (constant) - added black formatter option to .vscode - threshold setting now shows the db indicator with audiometer - partly implement the new threshold setting in record - separate some device function from record into its own file in device.py
Version: pyaudiowpatch (0.2.12.5) - Python 3.10.5
On both of my systems (Windows 10/11 64-bit), the pawp_record_wasapi_loopback.py example works flawlessly (with loopback as well as Stereo Mix)
It also works fine when I transform that example to a class object (AudioRecorder).
However when adding threading, it crashes with [Errno -9999] Unanticipated host error, but works OK using the traditional start/stop stream and writing the stream data to a file.
It also does not work when the AudioRecorder class itself is thread-free, but the main program is running a thread.
So the issue is somewhere with threading and callbacks.
I'm guessing the issue is somewhere with portaudio and not in the (nice) enhancements your code is providing, but because of the new "with p.open as stream" addition, I am not totally sure.
Thanks in advance,
The text was updated successfully, but these errors were encountered: