Skip to content
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

Closed
ewaldc opened this issue Oct 31, 2022 · 8 comments
Closed

PyAudioWPatch does not run with threading #4

ewaldc opened this issue Oct 31, 2022 · 8 comments
Labels
question Further information is requested

Comments

@ewaldc
Copy link

ewaldc commented Oct 31, 2022

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,

from _spinner_helper import Spinner

import pyaudiowpatch as pyaudio
import numpy as np
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(object):
    def __init__(self):
        self.p = pyaudio.PyAudio()
        self.spinner = Spinner()
        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.\nRun this to check available devices.\nExiting...\n")
                self.spinner.stop()
                exit()

    @threaded
    def start_recording(self, duration):
        default_speakers = self.default_speakers        
        self.spinner.print(f"Recording from: ({default_speakers['index']}){default_speakers['name']}")
        
        waveFile = wave.open(filename, 'wb')
        waveFile.setnchannels(default_speakers["maxInputChannels"])
        waveFile.setsampwidth(pyaudio.get_sample_size(pyaudio.paInt24))
        waveFile.setframerate(int(default_speakers["defaultSampleRate"]))
        
        def callback(in_data, frame_count, time_info, status):
            """Write frames and return PA flag"""
            waveFile.writeframes(in_data)
            return (in_data, pyaudio.paContinue)
        
        with self.p.open(format=pyaudio.paInt24,
                channels=self.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
        
        waveFile.close()


    def wait_for_recording_complete(self, thread_handle):
        thread_handle.join()
    
if __name__ == "__main__":
    pa = AudioRecorder()
    thread_handle = pa.start_recording(duration)
    pa.wait_for_recording_complete(thread_handle)
    exit()
@s0d3s
Copy link
Owner

s0d3s commented Oct 31, 2022

Hello🖐 Thanks for the issue.
Actually you don't need to wrap the 'recording functionality' in another thread.

When you call pyaudio.PyAudio().open(..., stream_callback=...) (that is, in streaming mode), PyAudio/PortAudio itself creates a thread that calls stream_callback on every tick. So I'd rather implement it like this(also you can drop all code related to the spinner):

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:

but take a closer look at the example above, it does the same but without 'garbage' threads

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.

@s0d3s s0d3s added the question Further information is requested label Oct 31, 2022
@ewaldc
Copy link
Author

ewaldc commented Nov 1, 2022

Thanks for your quick and very helpful response !
I was aware of the fact that the callback function itself is a threaded operation and my "AudioRecorder" class implementation itself does not need to be threaded. In fact it is almost identical to your alternative #1, but the issue appeared when that (thread-free) class was called from a threaded application. Because I did not want to post the whole code and also to eliminate possible coding mistakes from my side, I tried to create the smallest possible application that produced the error. That ended up being a threaded "AudioRecorder" class, but that was an unfortunate decision because it's actually not representative of my use case. Apologies for this miscommunication from my side...

The following code is a better representation of what is causing the issue in my application:

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):
            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()

class C(threading.Thread) :
    def __init__(self):
        threading.Thread.__init__(self)
        self.ar = AudioRecorder()

    def run(self) :
        self.ar.start_recording(duration)
        time.sleep(duration/2) # Blocking execution
        ar.join()

if __name__ == "__main__":
    c = C()
    c.start()

PS. your alternative #2 code is working fine.

Perhaps there is a better way to create class C without causing the problem...

@ewaldc
Copy link
Author

ewaldc commented Nov 1, 2022

Interesting finding using the code above but just changing the "class C" part ...

class C() :
    def __init__(self):
        print ("C is initialized")
    
    @threaded
    def start(self) :
        AudioRecorder().start_recording(duration)

if __name__ == "__main__":
    c = C()
    art = c.start()
    print ("waiting for recording to complete")
    art.join()

is working, but

class C() :
    def __init__(self):
        self.ar = AudioRecorder()
    
    @threaded
    def start(self) :
        self.ar.start_recording(duration)

if __name__ == "__main__":
    c = C()
    art = c.start()
    print ("waiting for recording to complete")
    art.join()

is failing. Of course, the second example isn't really great code...

class C(threading.Thread) :
    def __init__(self):
        threading.Thread.__init__(self)
        print ("C is initialized")    

    def run(self) :
        AudioRecorder().start_recording(duration)

if __name__ == "__main__":
    c = C()
    c.start()
    print ("waiting for recording to complete")

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 reason for the treading is to be able to listen to/handle keyboard events during the different recording sessions (e.g. control C to end the app or to dynamically refresh the list of sources to record)
So far I have not been able to make this work using the stream "callback" approach. The "AudioRecorder" object itself does not need the threading because with "callback" it is not blocking by itself, but unfortunately not all of the other classes/objects have that non-blocking bahavior...

@ewaldc
Copy link
Author

ewaldc commented Nov 1, 2022

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.

#!/usr/bin/python3
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, start_lock, stop_lock):
        super().__init__()
        start_lock.acquire(blocking = True)
        stop_lock.acquire(blocking = True)
        p = pyaudio.PyAudio()
        
        try: wasapi_info = p.get_host_api_info_by_type(pyaudio.paWASAPI)
        except OSError: print("Looks like WASAPI is not available on the system. Exiting...")
        # Get default WASAPI speakers
        default_speakers = p.get_device_info_by_index(wasapi_info["defaultOutputDevice"])
        
        if not default_speakers["isLoopbackDevice"]:
            for loopback in p.get_loopback_device_info_generator():
                if default_speakers["name"] in loopback["name"]: default_speakers = loopback; break
            else:
                print("Default loopback output device not found.\n\nRun `python -m pyaudiowpatch` to check available devices.\nExiting...\n")
                exit()

        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):
            wave_file.writeframes(in_data)
            return (in_data, pyaudio.paContinue)

        print(f"The next {duration} seconds will be written to {filename}")        
        with 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:
            start_lock.release()
            time.sleep(duration) # Blocking execution while playing
        
        wave_file.close()
        p.terminate()
        stop_lock.release()

class C() :
    def __init__(self):
        self.start_lock = threading.Lock()
        self.stop_lock = threading.Lock()
    
    @threaded
    def start_recording(self):
        AudioRecorder(self.start_lock, self.stop_lock)
    
    @threaded
    def loop(self) :
        self.start_recording()
        print ("waiting for recording to start")
        self.start_lock.acquire(blocking = True)
        print ("recording started, starting media now")
        self.stop_lock.acquire(blocking = True)
        print ("recording finished")

if __name__ == "__main__":
    c = C()
    c.loop()
    print ("free to do whatever")

@s0d3s
Copy link
Owner

s0d3s commented Nov 2, 2022

Your experiments are interesting, but in fact you are reinventing the wheel😉

PyAudio().open does not block execution (and calls a callback from another thread), so all you need to do is work out the data acquisition pipeline.

The easiest way -> write the data received in the callback to the queue. For example, the simplest implementation, with minimal user interface:

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.

@ewaldc
Copy link
Author

ewaldc commented Nov 2, 2022

Thanks once again for your suggestions! I have a correctly working implementation now but will consider your suggested code.
I have not tried the callback approach with start/strop stream (versus the "with" context approach).

Here is a simplified summary of the code architecture:

  1. The code classifies and determines bird songs (multiple bird songs per audio source, somethings intermixed)

  2. The AudioRecorder is part of an abstraction class "Recorders". It lives alongside other types of "recorders" that e.g. extract a "duration" segment from a video/audio source e.g. a file, web site, streaming service (e.g. "YoutubeRecorder"). There is also a "Players" class, which handles the data sources (URI/URL, playlist etc.). The AudioRecorder is only used if the audio segment can not be extracted for it's source. "Recorders" add a command to the queue for the "AudioRecorder" thread when they fail to extract the audio using their knowledge.
    There is also a "InputFilters" class e.g. to filter multiple bird songs from each other. There are several post-processing classes such "Recognizers" that can determine which bird it could be. All are threaded, some use parallel processing.

  3. All "Recorders" and "Players" are instantiated from a "Session" class that is threaded as there can be several sessions each with "while true { pre-process - play - record - post-process - summary}" processing loops (determined by processor core count) instanciated from a single "main" (which is listening to keyboard commands). Even though for HW restriction reasons there is (generally) only one single paAudio based "AudioRecorder" (hence it is/could be a thread-free class), it is instanciated from a threaded (session) instance.
    That is how I discovered the issue initially, and only when using the "callback" approach. From your suggested code and my experiments, I learned that the problem goes away if the pyaudio.PyAudio() instanciation is not done seperately in the init section, but done together with the recording itself. The only challenge then is the synchronization between the actual start of recording and the "player" source. The Python start lock is resolving that issue.

  4. The recording duration must be as precise as possible (duration is in milliseconds), that is : the audio source needs to start as close as possible to recording session and finish as close as possible to the source audio section (e.g. a bird song segment between 22130ms and 33110ms) . It is important that the recording stops as closely as possible to the specified duration . If the overshoot is small, no post-processing is required. So far I only managed this with two approaches:

  • manually calculate the frames_per_buffer based on the format, CPU power (smaller buffer = more accurate) and use start/stop stream
  • the with pa.open(callback) approach followed by time.sleep(). Combined with Python locks, it's very accurate, probably because in the low-level C code, once the lock is released and the subsequent sleep is started, the schedular favors the thread that is doing a blocking wait for the lock. That means I can start the audio source very close to the start-of-recording time. In the sample code I posted, I made the recoding threaded, but that is not really needed (i.e. the stop_lock can be eliminated). The "Player" can be stopped when the "Recorder" ends when the "start of recording" is well synchronized.

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"

@s0d3s
Copy link
Owner

s0d3s commented Nov 3, 2022

So - which of the options(threaded/start-stop) to use depends on the style/approach of your code.

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.

@ewaldc
Copy link
Author

ewaldc commented Nov 4, 2022

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.

@ewaldc ewaldc closed this as completed Nov 4, 2022
Dadangdut33 added a commit to Dadangdut33/Speech-Translate that referenced this issue Sep 16, 2023
- 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants