import sys include "config.pxi" cimport cython from .lasp_daqconfig import DeviceInfo from libcpp.string cimport string from libcpp.vector cimport vector from libc.stdlib cimport malloc, free from libc.stdio cimport printf, fprintf, stderr from libc.string cimport memcpy, memset from cpython.ref cimport PyObject,Py_INCREF, Py_DECREF __all__ = ['AvType', 'RtAudio', 'get_numpy_dtype_from_format_string', 'get_sampwidth_from_format_string'] class AvType: """Specificying the type of data, for adding and removing callbacks from the stream.""" audio_input = 1 audio_output = 2 video = 4 cdef extern from "RtAudio.h" nogil: ctypedef unsigned long RtAudioStreamStatus RtAudioStreamStatus RTAUDIO_INPUT_OVERFLOW RtAudioStreamStatus RTAUDIO_OUTPUT_UNDERFLOW cdef cppclass RtAudioError: ctypedef enum Type: WARNING DEBUG_WARNING UNSPECIFIED NO_DEVICES_FOUND INVALID_DEVICE MEMORY_ERROR INVALID_PARAMETER INVALID_USE DRIVER_ERROR SYSTEM_ERROR THREAD_ERROR ctypedef unsigned long RtAudioStreamFlags RtAudioStreamFlags RTAUDIO_NONINTERLEAVED RtAudioStreamFlags RTAUDIO_MINIMIZE_LATENCY RtAudioStreamFlags RTAUDIO_HOG_DEVICE RtAudioStreamFlags RTAUDIO_SCHEDULE_REALTIME RtAudioStreamFlags RTAUDIO_ALSA_USE_DEFAULT RtAudioStreamFlags RTAUDIO_JACK_DONT_CONNECT ctypedef unsigned long RtAudioFormat RtAudioFormat RTAUDIO_SINT8 RtAudioFormat RTAUDIO_SINT16 RtAudioFormat RTAUDIO_SINT24 RtAudioFormat RTAUDIO_SINT32 RtAudioFormat RTAUDIO_FLOAT32 RtAudioFormat RTAUDIO_FLOAT64 ctypedef int (*RtAudioCallback)(void* outputBuffer, void* inputBuffer, unsigned int nFramesPerBlock, double streamTime, RtAudioStreamStatus status, void* userData) ctypedef void (*RtAudioErrorCallback)(RtAudioError.Type _type, const string& errortxt) cdef cppclass cppRtAudio "RtAudio": enum Api: UNSPECIFIED LINUX_ALSA LINUX_PULSE MACOSX_CORE WINDOWS_WASAPI WINDOWS_ASIO WINDOWS_DS cppclass DeviceInfo: bool probed string name unsigned int outputChannels unsigned int inputChannels unsigned int duplexChannels bool isDefaultOutput bool isDefaultInput vector[unsigned int] sampleRates unsigned int preferredSampleRate RtAudioFormat nativeFormats cppclass StreamOptions: StreamOptions() RtAudioStreamFlags flags unsigned int numberOfBuffers string streamName int priority cppclass StreamParameters: StreamParameters() unsigned int deviceId unsigned int nChannels unsigned int firstChannel @staticmethod void getCompiledApi(vector[cppRtAudio.Api]& apis) @staticmethod cppRtAudio.Api getCompiledApiByName(string& name) @staticmethod string getApiDisplayName(cppRtAudio.Api api) @staticmethod string getApiName(cppRtAudio.Api api) # RtAudio() except + cppRtAudio() except + cppRtAudio(cppRtAudio.Api api) except + # ~RtAudio() Destructors should not be listed unsigned int getDeviceCount() DeviceInfo getDeviceInfo(unsigned int device) unsigned int getDefaultOutputDevice() unsigned int getDefaultInputDevice() void openStream(StreamParameters* outputParameters, StreamParameters* intputParameters, RtAudioFormat _format, unsigned int sampleRate, unsigned int* bufferFrames, RtAudioCallback callback, void* userData, void* StreamOptions, RtAudioErrorCallback) except + void closeStream() void startStream() except + void stopStream() except + void abortStream() except + bool isStreamOpen() bool isStreamRunning() double getStreamTime() void setStreamTime(double) except + long getStreamLatency() unsigned int getStreamSampleRate() void showWarnings(bool value) cdef extern from "lasp_cppthread.h" nogil: cdef cppclass CPPThread[T,F]: CPPThread(F threadfunction, T data) void join() void CPPsleep(unsigned int ms) cdef extern from "lasp_cppqueue.h" nogil: cdef cppclass SafeQueue[T]: SafeQueue() void enqueue(T t) T dequeue() size_t size() const bool empty() const cdef extern from "atomic" namespace "std" nogil: cdef cppclass atomic[T]: T load() void store(T) cdef extern from "lasp_pyarray.h": PyObject* data_to_ndarray(void* data, int n_rows,int n_cols, int typenum, bool transfer_ownership, bool F_contiguous) _formats_strkey = { '8-bit integers': (RTAUDIO_SINT8, 1, np.int8), '16-bit integers': (RTAUDIO_SINT16, 2, np.int16), '24-bit integers': (RTAUDIO_SINT24, 3), '32-bit integers': (RTAUDIO_SINT32, 4, np.int32), '32-bit floats': (RTAUDIO_FLOAT32, 4, np.float32), '64-bit floats': (RTAUDIO_FLOAT64, 8, np.float64), } _formats_rtkey = { RTAUDIO_SINT8: ('8-bit integers', 1, cnp.NPY_INT8), RTAUDIO_SINT16: ('16-bit integers', 2, cnp.NPY_INT16), RTAUDIO_SINT24: ('24-bit integers', 3), RTAUDIO_SINT32: ('32-bit integers', 4, cnp.NPY_INT32), RTAUDIO_FLOAT32: ('32-bit floats', 4, cnp.NPY_FLOAT32), RTAUDIO_FLOAT64: ('64-bit floats', 8, cnp.NPY_FLOAT64), } def get_numpy_dtype_from_format_string(format_string): return _formats_strkey[format_string][-1] def get_sampwidth_from_format_string(format_string): return _formats_strkey[format_string][-2] # It took me quite a long time to fully understand Cython's idiosyncrasies # concerning C(++) callbacks, the GIL and passing Python objects as pointers # into C(++) functions. But finally, here it is! # cdef void fromNPYToBuffer(cnp.ndarray arr, # void* buf): # """ # Copy a Python numpy array over to a buffer # No checks, just memcpy! Careful! # """ # memcpy(buf, arr.data, arr.size*arr.itemsize) cdef void copyChannel(void* to, void* from_, unsigned bytesperchan, unsigned toindex, unsigned fromindex) nogil: memcpy( &(( to)[bytesperchan*toindex]), &(( from_)[bytesperchan*fromindex]), bytesperchan) ctypedef struct _Stream: PyObject* pyCallback RtAudioFormat sampleformat # Flag used to pass the stopThread. atomic[bool] stopThread # Number of frames per block unsigned nFramesPerBlock # Number of bytes per channel unsigned int nBytesPerChan # Number of blocks to delay the output before adding to the input unsigned int outputDelayBlocks # The structures as used by RtAudio cppRtAudio.StreamParameters inputParams cppRtAudio.StreamParameters outputParams bool* inputChannelsEnabled bool* outputChannelsEnabled unsigned ninputchannels_forwarded unsigned noutputchannels_forwarded # If these queue pointers are NULL, it means the stream does not have an # input, or output. SafeQueue[void*] *outputDelayQueue SafeQueue[void*] *inputQueue SafeQueue[void*] *outputQueue CPPThread[void*, void (*)(void*)] *thread cdef int audioCallback(void* outputbuffer, void* inputbuffer, unsigned int nFramesPerBlock, double streamTime, RtAudioStreamStatus status, void* userData) nogil: """ Calls the Python callback function and converts data """ cdef: int rval = 0 _Stream* stream void* outputbuffercpy = NULL void* inputbuffercpy = NULL unsigned j, i unsigned bytesperchan unsigned noutputchannels_forwarded, ninputchannels_forwarded bint ch_en stream = <_Stream*>(userData) bytesperchan = stream.nBytesPerChan ninputchannels_forwarded = stream.ninputchannels_forwarded noutputchannels_forwarded = stream.noutputchannels_forwarded # with gil: # print(f'bytesperchan: {bytesperchan}') # print(f'ninputchannels_forwarded:: {ninputchannels_forwarded}') # print(f'noutputchannels_forwarded:: {noutputchannels_forwarded}') # fprintf(stderr, "Stream heartbeat...\n") # Returning 2 means aborting the stream immediately if status == RTAUDIO_INPUT_OVERFLOW: fprintf(stderr, 'Input overflow.\n') stream.stopThread.store(True) return 2 elif status == RTAUDIO_OUTPUT_UNDERFLOW: fprintf(stderr, 'Output underflow.\n') # stream.stopThread.store(True) return 0 if nFramesPerBlock != stream.nFramesPerBlock: printf('Number of frames mismath in callback data!\n') stream.stopThread.store(True) return 2 if inputbuffer: # fprintf(stderr, "enter copying input buffer code\n") # with gil: # assert stream.inputQueue is not NULL inputbuffercpy = malloc(bytesperchan*ninputchannels_forwarded) if not inputbuffercpy: fprintf(stderr, "Error allocating buffer\n") return 2 if stream.outputDelayQueue: if stream.outputDelayQueue.size() > stream.outputDelayBlocks: outputbuffercpy = stream.outputDelayQueue.dequeue() memcpy(inputbuffercpy, outputbuffercpy, bytesperchan*noutputchannels_forwarded) # Cleanup buffer free(outputbuffercpy) outputbuffercpy = NULL else: memset(inputbuffercpy, 0, bytesperchan*noutputchannels_forwarded) # Starting channel for copying input channels according to the channel # map j = stream.noutputchannels_forwarded else: j = 0 i = 0 for i in range(stream.inputParams.nChannels): ch_en = stream.inputChannelsEnabled[i] if ch_en: copyChannel(inputbuffercpy, inputbuffer, bytesperchan, j, i) j+=1 i+=1 stream.inputQueue.enqueue(inputbuffercpy) if outputbuffer: # with gil: # assert stream.outputQueue # fprintf(stderr, "enter copying output buffer code\n") if stream.outputQueue.empty(): fprintf(stderr, 'Stream output buffer underflow, zero-ing buffer...\n') memset(outputbuffer, 0, stream.outputParams.nChannels*bytesperchan) else: outputbuffercpy = stream.outputQueue.dequeue() # fprintf(stderr, 'Copying data to stream output buffer...\n') j = 0 i = 0 for i in range(stream.outputParams.nChannels): ch_en = stream.outputChannelsEnabled[i] if ch_en: copyChannel(outputbuffer, outputbuffercpy, bytesperchan, i, j) j+=1 else: # If channel is not enabled, we set the data to zero memset( &(( outputbuffer)[bytesperchan*i]), 0, bytesperchan) pass i+=1 if stream.outputDelayQueue: # fprintf(stderr, "Adding to delay queue\n") stream.outputDelayQueue.enqueue(outputbuffercpy) else: free(outputbuffercpy) outputbuffercpy = NULL return 0 cdef void audioCallbackPythonThreadFunction(void* voidstream) nogil: cdef: _Stream* stream cnp.NPY_TYPES npy_format void* inputbuffer = NULL void* outputbuffer = NULL unsigned noutputchannels_forwarded unsigned ninputchannels_forwarded unsigned nBytesPerChan unsigned nFramesPerBlock void* _TESTDATA stream = <_Stream*> voidstream ninputchannels_forwarded = stream.ninputchannels_forwarded noutputchannels_forwarded = stream.noutputchannels_forwarded nBytesPerChan = stream.nBytesPerChan nFramesPerBlock = stream.nFramesPerBlock with gil: npy_format = _formats_rtkey[stream.sampleformat][2] callback = stream.pyCallback print(f'noutputchannels_forwarded: {noutputchannels_forwarded}') print(f'ninputchannels_forwarded: {ninputchannels_forwarded}') print(f'nBytesPerChan: {nBytesPerChan}') print(f'nFramesPerBlock: {nFramesPerBlock}') while True: if stream.stopThread.load() == True: printf('Stopping thread...\n') return if stream.inputQueue: # fprintf(stderr, "Waiting on input queue\n") inputbuffer = stream.inputQueue.dequeue() if not inputbuffer: printf('Stopping thread...\n') return if stream.outputQueue: # fprintf(stderr, 'Allocating output buffer...\n') outputbuffer = malloc(nBytesPerChan*noutputchannels_forwarded) # memset(outputbuffer, 0, nBytesPerChan*noutputchannels_forwarded) with gil: # Obtain stream information npy_input = None npy_output = None if stream.inputQueue: # assert(inputbuffer) try: # print(f'========ninputchannels_forwarded: {ninputchannels_forwarded}') # print(f'========nFramesPerBlock: {nFramesPerBlock}') # print(f'========npy_format: {npy_format}') npy_input = data_to_ndarray( inputbuffer, nFramesPerBlock, ninputchannels_forwarded, npy_format, True, # Do transfer ownership True) # F-contiguous is True: data is Fortran-cont. # fprintf(stderr, "Copying array...\n") # fprintf(stderr, "End Copying array...\n") except Exception as e: print('exception in cython callback for audio input: ', str(e)) return if stream.outputQueue: # fprintf(stderr, 'Copying output buffer to Numpy...\n') try: npy_output = data_to_ndarray( outputbuffer, nFramesPerBlock, noutputchannels_forwarded, npy_format, False, # Do not transfer ownership True) # F-contiguous except Exception as e: print('exception in Cython callback for audio output: ', str(e)) return try: # fprintf(stderr, "Python callback...\n") rval = callback(npy_input, npy_output, nFramesPerBlock, ) # fprintf(stderr, "Return from Python callback...\n") except Exception as e: print('Exception in Cython callback: ', str(e)) return if stream.outputQueue: # fprintf(stderr, 'Enqueuing output buffer...\n') stream.outputQueue.enqueue(outputbuffer) if not stream.inputQueue: # fprintf(stderr, 'No input queue!\n') while stream.outputQueue.size() > 10 and not stream.stopThread.load(): # printf('Sleeping...\n') # No input queue to wait on, so we relax a bit here. CPPsleep(1); # Outputbuffer is free'ed by the audiothread, so should not be touched # here. outputbuffer = NULL # Inputbuffer memory is owned by Numpy, so should not be free'ed inputbuffer = NULL cdef void errorCallback(RtAudioError.Type _type,const string& errortxt) nogil: fprintf(stderr, 'RtAudio error callback called: ') fprintf(stderr, errortxt.c_str()) fprintf(stderr, '\n') cdef class RtAudio: cdef: cppRtAudio* _rtaudio _Stream* _stream int api def __cinit__(self, unsigned int iapi): cdef: cppRtAudio.Api api = iapi self._rtaudio = new cppRtAudio(api) self._stream = NULL self._rtaudio.showWarnings(True) self.api = api def __dealloc__(self): if self._stream is not NULL: # fprintf(stderr, 'Force closing stream...') self._rtaudio.closeStream() self.cleanupStream(self._stream) del self._rtaudio @staticmethod def getApi(): cdef: vector[cppRtAudio.Api] apis cppRtAudio.getCompiledApi(apis) apidict = {} for api in apis: apidict[ api] = { 'displayname': cppRtAudio.getApiDisplayName(api).decode('utf-8'), 'name': cppRtAudio.getApiName(api).decode('utf-8') } return apidict cpdef unsigned int getDeviceCount(self): return self._rtaudio.getDeviceCount() cpdef unsigned int getDefaultOutputDevice(self): return self._rtaudio.getDefaultOutputDevice() cpdef unsigned int getDefaultInputDevice(self): return self._rtaudio.getDefaultInputDevice() def getDeviceInfo(self, unsigned int device): """ Return device information of the current device """ sampleformats = [] cdef: cppRtAudio.DeviceInfo devinfo devinfo = self._rtaudio.getDeviceInfo(device) nf = devinfo.nativeFormats for format_ in [ RTAUDIO_SINT8, RTAUDIO_SINT16, RTAUDIO_SINT24, RTAUDIO_SINT32, RTAUDIO_FLOAT32, RTAUDIO_FLOAT64]: if nf & format_: sampleformats.append(_formats_rtkey[format_][0]) return DeviceInfo( api = self.api, index = device, probed = devinfo.probed, name = devinfo.name.decode('utf-8'), outputchannels = devinfo.outputChannels, inputchannels = devinfo.inputChannels, duplexchannels = devinfo.duplexChannels, samplerates = devinfo.sampleRates, sampleformats = sampleformats, prefsamplerate = devinfo.preferredSampleRate) @cython.nonecheck(True) def openStream(self, avstream ): """ Opening a stream with specified parameters Args: avstream: AvStream instance Returns: None """ if self._stream is not NULL: raise RuntimeError('Stream is already opened.') daqconfig = avstream.daqconfig avtype = avstream.avtype device = avstream.device cdef: bint duplex_mode = daqconfig.duplex_mode bint monitorOutput = daqconfig.monitor_gen unsigned int outputDelayBlocks = daqconfig.outputDelayBlocks cppRtAudio.StreamParameters *rtOutputParams_ptr = NULL cppRtAudio.StreamParameters *rtInputParams_ptr = NULL cppRtAudio.StreamOptions streamoptions size_t sw unsigned int nFramesPerBlock = int(daqconfig.nFramesPerBlock) int firstinputchannel, firstoutputchannel int lastinputchannel, lastoutputchannel unsigned int ninputchannels_forwarded=0 unsigned int ninputchannels_rtaudio=0 unsigned int noutputchannels_rtaudio=0 unsigned int noutputchannels_forwarded=0 unsigned int samplerate int i bint input_stream=False, output_stream=False bool* inputChannelsEnabled bool* outputChannelsEnabled if daqconfig.nFramesPerBlock > 8192 or daqconfig.nFramesPerBlock < 512: raise ValueError('Invalid number of nFramesPerBlock') if daqconfig.outputDelayBlocks < 0 or daqconfig.outputDelayBlocks > 10: raise ValueError('Invalid number of outputDelayBlocks') try: # Determine sample rate and sample format, determine whether we are an # input or an output stream, or both if avtype == AvType.audio_input or duplex_mode: # Here, we override the sample format in case of duplex mode. sampleformat = daqconfig.en_input_sample_format samplerate = int(daqconfig.en_input_rate) input_stream = True if duplex_mode: output_stream = True else: sampleformat = daqconfig.en_output_sample_format samplerate = int(daqconfig.en_output_rate) output_stream = True print(f'Is input stream: {input_stream}') print(f'Is output stream: {output_stream}') sw = get_sampwidth_from_format_string(sampleformat) print(f'samplewidth: {sw}') # All set, allocate the stream! self._stream = <_Stream*> malloc(sizeof(_Stream)) if self._stream == NULL: raise MemoryError('Could not allocate stream: memory error.') self._stream.pyCallback = avstream._audioCallback # Increase reference count to the callback Py_INCREF( avstream._audioCallback) self._stream.sampleformat = _formats_strkey[sampleformat][0] self._stream.stopThread.store(False) self._stream.inputQueue = NULL self._stream.outputQueue = NULL self._stream.outputDelayQueue = NULL self._stream.thread = NULL self._stream.outputDelayBlocks = outputDelayBlocks self._stream.ninputchannels_forwarded = 0 self._stream.noutputchannels_forwarded = 0 self._stream.inputChannelsEnabled = NULL self._stream.outputChannelsEnabled = NULL # Create channel maps for input channels, set RtAudio input stream # parameters if input_stream: firstinputchannel = daqconfig.firstEnabledInputChannelNumber() lastinputchannel = daqconfig.lastEnabledInputChannelNumber() ninputchannels_rtaudio = lastinputchannel-firstinputchannel+1 # print(firstinputchannel) # print(lastinputchannel) # print(ninputchannels_rtaudio) if lastinputchannel < 0 or ninputchannels_rtaudio < 1: raise ValueError('Not enough input channels selected') input_ch = daqconfig.input_channel_configs inputChannelsEnabled = malloc(sizeof(bool)*ninputchannels_rtaudio) self._stream.inputChannelsEnabled = inputChannelsEnabled for i in range(firstinputchannel, lastinputchannel+1): ch_en = input_ch[i].channel_enabled if ch_en: ninputchannels_forwarded += 1 inputChannelsEnabled[i] = ch_en rtInputParams_ptr = &self._stream.inputParams rtInputParams_ptr.deviceId = device.index rtInputParams_ptr.nChannels = ninputchannels_rtaudio rtInputParams_ptr.firstChannel = firstinputchannel self._stream.inputQueue = new SafeQueue[void*]() self._stream.ninputchannels_forwarded = ninputchannels_forwarded # Create channel maps for output channels if output_stream: firstoutputchannel = daqconfig.firstEnabledOutputChannelNumber() lastoutputchannel = daqconfig.lastEnabledOutputChannelNumber() noutputchannels_rtaudio = lastoutputchannel-firstoutputchannel+1 # print(firstoutputchannel) # print(lastoutputchannel) # print(noutputchannels_rtaudio) if lastoutputchannel < 0 or noutputchannels_rtaudio < 1: raise ValueError('Not enough output channels selected') output_ch = daqconfig.output_channel_configs outputChannelsEnabled = malloc(sizeof(bool)*noutputchannels_rtaudio) self._stream.outputChannelsEnabled = outputChannelsEnabled for i in range(firstoutputchannel, lastoutputchannel+1): ch_en = output_ch[i].channel_enabled if ch_en: noutputchannels_forwarded += 1 outputChannelsEnabled[i] = ch_en rtOutputParams_ptr = &self._stream.outputParams rtOutputParams_ptr.deviceId = device.index rtOutputParams_ptr.nChannels = noutputchannels_rtaudio rtOutputParams_ptr.firstChannel = firstoutputchannel self._stream.outputQueue = new SafeQueue[void*]() self._stream.noutputchannels_forwarded = noutputchannels_forwarded if monitorOutput and duplex_mode: self._stream.outputDelayQueue = new SafeQueue[void*]() self._stream.ninputchannels_forwarded += noutputchannels_forwarded streamoptions.flags = RTAUDIO_HOG_DEVICE streamoptions.flags |= RTAUDIO_NONINTERLEAVED streamoptions.numberOfBuffers = 4 streamoptions.streamName = "LASP Audio stream".encode('utf-8') streamoptions.priority = 1 self._rtaudio.openStream(rtOutputParams_ptr, rtInputParams_ptr, _formats_strkey[sampleformat][0], samplerate, &nFramesPerBlock, audioCallback, self._stream, &streamoptions, # Stream options errorCallback # Error callback ) self._stream.nBytesPerChan = nFramesPerBlock*sw self._stream.nFramesPerBlock = nFramesPerBlock except Exception as e: print('Exception occured in stream opening: ', e) self.cleanupStream(self._stream) self._stream = NULL raise e with nogil: self._stream.thread = new CPPThread[void*, void (*)(void*)](audioCallbackPythonThreadFunction, self._stream) # Allow it to start CPPsleep(500) pass return nFramesPerBlock cdef cleanupStream(self, _Stream* stream): # printf('Entrance function cleanupStream...\n') cdef: void* ptr if stream == NULL: return with nogil: if stream.thread: stream.stopThread.store(True) if stream.inputQueue: # If waiting in the input queue, hereby we let it run. stream.inputQueue.enqueue(NULL) # printf('Joining thread...\n') # HERE WE SHOULD RELEASE THE GIL, as exiting the thread function # will require the GIL, which is locked by this thread! stream.thread.join() # printf('Thread joined!\n') del stream.thread stream.thread = NULL if stream.inputChannelsEnabled: free(stream.inputChannelsEnabled) if stream.outputChannelsEnabled: free(stream.outputChannelsEnabled) if stream.outputQueue: while not stream.outputQueue.empty(): free(stream.outputQueue.dequeue()) del stream.outputQueue if stream.inputQueue: while not stream.inputQueue.empty(): free(stream.inputQueue.dequeue()) del stream.inputQueue if stream.outputDelayQueue: while not stream.outputDelayQueue.empty(): free(stream.outputDelayQueue.dequeue()) del stream.outputDelayQueue fprintf(stderr, "End cleanup stream queues...\n") if stream.pyCallback: Py_DECREF( stream.pyCallback) stream.pyCallback = NULL # fprintf(stderr, "End cleanup callback...\n") free(stream) def startStream(self): self._rtaudio.startStream() def stopStream(self): if self._stream is NULL: raise RuntimeError('Stream is not opened') try: self._rtaudio.stopStream() except: pass def closeStream(self): # print('closeStream') if self._stream is NULL: raise RuntimeError('Stream is not opened') # Closing stream self._rtaudio.closeStream() self.cleanupStream(self._stream) self._stream = NULL def abortStream(self): if self._stream is NULL: raise RuntimeError('Stream is not opened') self._rtaudio.abortStream() def isStreamOpen(self): return self._rtaudio.isStreamOpen() def isStreamRunning(self): return self._rtaudio.isStreamRunning() def getStreamTime(self): return self._rtaudio.getStreamTime() def setStreamTime(self, double time): return self._rtaudio.setStreamTime(time)