Stream and recording seems to work. Also signal generator seems to work. Error handling is not working properly yet.

This commit is contained in:
Anne de Jong 2021-05-05 19:48:04 +02:00
parent 4657063467
commit 466a6f5cc1
5 changed files with 175 additions and 168 deletions

View File

@ -82,7 +82,7 @@ cdef void audioCallbackPythonThreadFunction(void* voidsd) nogil:
callback = <object> sd.pyCallback callback = <object> sd.pyCallback
# print(f'Number of input channels: {ninchannels}') # print(f'Number of input channels: {ninchannels}')
# print(f'Number of out channels: {noutchannels}') # print(f'Number of out channels: {noutchannels}')
fprintf(stderr, 'Sleep time: %d us', sleeptime_us) fprintf(stderr, 'Sleep time: %d us\n', sleeptime_us)
while not sd.stopThread.load(): while not sd.stopThread.load():
with gil: with gil:

View File

@ -169,9 +169,11 @@ class AvStreamProcess(mp.Process):
def streamCallback(self, indata, outdata, nframes): def streamCallback(self, indata, outdata, nframes):
"""This is called (from a separate thread) for each audio block.""" """This is called (from a separate thread) for each audio block."""
# logging.debug('streamCallback()')
self.aframectr += nframes self.aframectr += nframes
if self.siggen_activated: if self.siggen_activated:
# logging.debug('siggen_activated')
if self.outq.empty(): if self.outq.empty():
outdata[:, :] = 0 outdata[:, :] = 0
msgtxt = 'Output signal buffer underflow' msgtxt = 'Output signal buffer underflow'
@ -182,6 +184,7 @@ class AvStreamProcess(mp.Process):
if newdata.shape[0] != outdata.shape[0] or newdata.ndim != 1: if newdata.shape[0] != outdata.shape[0] or newdata.ndim != 1:
self.pipe.send(StreamMsg.streamFatalError, 'Invalid output data obtained from queue') self.pipe.send(StreamMsg.streamFatalError, 'Invalid output data obtained from queue')
return 1 return 1
outdata[:, :] = newdata[:, np.newaxis]
if indata is not None: if indata is not None:
self.putAllInQueues(StreamMsg.streamData, indata) self.putAllInQueues(StreamMsg.streamData, indata)
@ -283,6 +286,15 @@ class AvStream:
""" """
return self.outq return self.outq
def activateSiggen(self):
logging.debug('activateSiggen()')
self.pipe.send((StreamMsg.activateSiggen, None))
def deactivateSiggen(self):
logging.debug('activateSiggen()')
self.pipe.send((StreamMsg.deactivateSiggen, None))
def addListener(self): def addListener(self):
""" """
Add a listener queue to the list of queues, and return the queue. Add a listener queue to the list of queues, and return the queue.

View File

@ -62,14 +62,11 @@ class SiggenMessage(Enum):
Different messages that can be send to the signal generator over the pipe Different messages that can be send to the signal generator over the pipe
connection. connection.
""" """
stop = auto() # Stop and quit the signal generator stop = auto() # Stop and quit the signal generator
generate = auto()
adjustVolume = auto() # Adjust the volume adjustVolume = auto() # Adjust the volume
newEqSettings = auto() # Forward new equalizer settings newEqSettings = auto() # Forward new equalizer settings
# These messages are send back to the main thread over the pipe # These messages are send back to the main thread over the pipe
ready = auto()
error = auto() error = auto()
done = auto() done = auto()
@ -95,71 +92,45 @@ class SiggenData:
signaltypedata: Tuple = None signaltypedata: Tuple = None
def siggenFcn(siggendata: SiggenData, dataq: mp.Queue, pipe): class SiggenProcess(mp.Process):
""" """
Main function running in a different process, is responsible for generating Main function running in a different process, is responsible for generating
new signal data. Uses the signal queue to push new generated signal data new signal data. Uses the signal queue to push new generated signal data
on. on.
Args:
siggendata: The signal generator data to start with.
dataq: The queue to put generated signal on
pipe: Control and status messaging pipe
""" """
fs = siggendata.fs def __init__(self, siggendata, dataq, pipe):
nframes_per_block = siggendata.nframes_per_block
level_dB = siggendata.level_dB
dtype = siggendata.dtype
signaltype = siggendata.signaltype
signaltypedata = siggendata.signaltypedata
nblocks_buffer = max(
1, int(QUEUE_BUFFER_TIME * fs/ nframes_per_block)
)
def generate(siggen, eq):
""" """
Generate a single block of data
"""
signal = siggen.genSignal(nframes_per_block)
if eq is not None:
signal = eq.equalize(signal)
if np.issubdtype((dtype := siggendata.dtype), np.integer):
bitdepth_fixed = dtype.itemsize * 8
signal *= 2 ** (bitdepth_fixed - 1) - 1
dataq.put(signal.astype(dtype))
def createEqualizer(eqdata):
"""
Create an equalizer object from equalizer data
Args: Args:
eqdata: dictionary containing equalizer data. TODO: document the siggendata: The signal generator data to start with.
requiring fields. dataq: The queue to put generated signal on
pipe: Control and status messaging pipe
""" """
if eqdata is None:
return None
eq_type = eqdata['type']
eq_levels = eqdata['levels']
if eq_type == 'three': self.dataq = dataq
fb = SosThirdOctaveFilterBank(fs) self.siggendata = siggendata
elif eq_type == 'one': self.pipe = pipe
fb = SosOctaveFilterBank(fs) self.eq = None
self.siggen = None
eq = Equalizer(fb._fb) fs = self.siggendata.fs
if eq_levels is not None: nframes_per_block = siggendata.nframes_per_block
eq.setLevels(eq_levels) self.nblocks_buffer = max(
return eq 1, int(QUEUE_BUFFER_TIME * fs/ nframes_per_block)
)
super().__init__()
eq = createEqualizer(siggendata.eqdata) def newSiggen(self, siggendata):
def newSiggen():
""" """
Create a signal generator based on parameters specified in global Create a signal generator based on parameters specified in global
function data. function data.
""" """
fs = siggendata.fs
nframes_per_block = siggendata.nframes_per_block
level_dB = siggendata.level_dB
signaltype = siggendata.signaltype
signaltypedata = siggendata.signaltypedata
if signaltype == SignalType.Periodic: if signaltype == SignalType.Periodic:
freq, = signaltypedata freq, = signaltypedata
siggen = pyxSiggen.sineWave(fs, freq, level_dB) siggen = pyxSiggen.sineWave(fs, freq, level_dB)
@ -181,47 +152,82 @@ def siggenFcn(siggendata: SiggenData, dataq: mp.Queue, pipe):
else: else:
raise ValueError(f"Not implemented signal type: {signaltype}") raise ValueError(f"Not implemented signal type: {signaltype}")
# Pre-generate blocks of signal data
while dataq.qsize() < nblocks_buffer:
generate(siggen, eq)
return siggen return siggen
# Initialization def generate(self):
try: """
siggen = newSiggen() Generate a single block of data and put it on the data queue
"""
signal = self.siggen.genSignal(self.siggendata.nframes_per_block)
dtype = self.siggendata.dtype
if self.eq is not None:
signal = self.eq.equalize(signal)
if np.issubdtype(dtype, np.integer):
bitdepth_fixed = dtype.itemsize * 8
signal *= 2 ** (bitdepth_fixed - 1) - 1
self.dataq.put(signal.astype(dtype))
except Exception as e: def newEqualizer(self, eqdata):
pipe.send((SiggenMessage.error, str(e))) """
return 1 Create an equalizer object from equalizer data
finally: Args:
pipe.send((SiggenMessage.done, None)) eqdata: dictionary containing equalizer data. TODO: document the
requiring fields.
"""
if eqdata is None:
return None
eq_type = eqdata['type']
eq_levels = eqdata['levels']
fs = self.siggendata.fs
while True: if eq_type == 'three':
if pipe.poll(timeout=QUEUE_BUFFER_TIME / 2): fb = SosThirdOctaveFilterBank(fs)
msg, data = pipe.recv() elif eq_type == 'one':
if msg == SiggenMessage.stop: fb = SosOctaveFilterBank(fs)
logging.debug("Signal generator caught 'stop' message. Exiting.")
return 0 eq = Equalizer(fb._fb)
elif msg == SiggenMessage.adjustVolume: if eq_levels is not None:
logging.debug(f"Signal generator caught 'adjustVolume' message. New volume = {level_dB:.1f} dB FS") eq.setLevels(eq_levels)
level_dB = data return eq
siggen.setLevel(level_dB)
elif msg == SiggenMessage.newEqSettings: def run(self):
eqdata = data # Initialization
eq = createEqualizer(eqdata) try:
else: self.siggen = self.newSiggen(self.siggendata)
pipe.send( self.eq = self.newEqualizer(self.siggendata.eqdata)
SiggenMessage.error, "BUG: Generator caught unknown message. Quiting" except Exception as e:
) self.pipe.send((SiggenMessage.error, str(e)))
elif dataq.qsize() < nblocks_buffer: return 1
# Generate new data and put it in the queue!
try: # Pre-generate blocks of signal data
generate(siggen, eq) while self.dataq.qsize() < self.nblocks_buffer:
except SiggenWorkerDone: self.generate()
pipe.send(SiggenMessage.done)
return 0 while True:
if self.pipe.poll(timeout=QUEUE_BUFFER_TIME / 2):
msg, data = self.pipe.recv()
if msg == SiggenMessage.stop:
logging.debug("Signal generator caught 'stop' message. Exiting.")
return 0
elif msg == SiggenMessage.adjustVolume:
logging.debug(f"Signal generator caught 'adjustVolume' message. New volume = {level_dB:.1f} dB FS")
level_dB = data
self.siggen.setLevel(level_dB)
elif msg == SiggenMessage.newEqSettings:
eqdata = data
eq = self.newEqualizer(eqdata)
else:
self.pipe.send(
SiggenMessage.error, "BUG: Generator caught unknown message. Quiting"
)
while self.dataq.qsize() < self.nblocks_buffer:
# Generate new data and put it in the queue!
try:
self.generate()
except SiggenWorkerDone:
self.pipe.send(SiggenMessage.done)
return 0
return 1 return 1
@ -237,10 +243,7 @@ class Siggen:
self.pipe, client_end = mp.Pipe(duplex=True) self.pipe, client_end = mp.Pipe(duplex=True)
self.process = mp.Process( self.process = SiggenProcess(siggendata, dataq, client_end)
target=siggenFcn,
args=(siggendata, dataq, client_end),
)
self.process.start() self.process.start()
self.handle_msgs() self.handle_msgs()
@ -285,9 +288,6 @@ class Siggen:
logging.debug('Siggen::stop()') logging.debug('Siggen::stop()')
if self.stopped: if self.stopped:
raise RuntimeError('BUG: Siggen::stop() is called twice!') raise RuntimeError('BUG: Siggen::stop() is called twice!')
self.stream.removeCallback(self.streamCallback, AvType.audio_output)
while not self.dataq.empty():
self.dataq.get()
self.pipe.send((SiggenMessage.stop, None)) self.pipe.send((SiggenMessage.stop, None))
self.pipe.close() self.pipe.close()
@ -300,18 +300,3 @@ class Siggen:
logging.debug('End Siggen::stop()') logging.debug('End Siggen::stop()')
self.stopped = True self.stopped = True
def streamCallback(self, indata, outdata, blockctr):
"""Callback from AvStream.
Copy generated signal from queue
"""
# logging.debug('Siggen::streamCallback()')
assert outdata is not None
if not self.dataq.empty():
outdata[:, :] = self.dataq.get()[:, np.newaxis]
else:
logging.warning("Signal generator queue empty!")
outdata[:, :] = 0
if self.dataq.qsize() < self.nblocks_buffer:
self.pipe.send((SiggenMessage.generate, None))

View File

@ -4,8 +4,7 @@ logging.basicConfig(level=logging.DEBUG)
import multiprocessing import multiprocessing
from lasp.lasp_multiprocessingpatch import apply_patch from lasp.lasp_multiprocessingpatch import apply_patch
from lasp.device import DaqConfigurations
from lasp.device import Daq, DaqChannel, DaqConfigurations
from lasp.lasp_avstream import AvStream, AvType from lasp.lasp_avstream import AvStream, AvType
from lasp.lasp_record import Recording from lasp.lasp_record import Recording

View File

@ -1,65 +1,76 @@
#!/usr/bin/python3 #!/usr/bin/python3
import argparse import argparse
import numpy as np import numpy as np
import sys, logging, os, argparse
logging.basicConfig(level=logging.DEBUG)
parser = argparse.ArgumentParser( import multiprocessing
description='Play a sine wave' from lasp.lasp_multiprocessingpatch import apply_patch
)
device_help = 'DAQ Device to play to'
parser.add_argument('--device', '-d', help=device_help, type=str,
default='Default')
args = parser.parse_args()
from lasp.lasp_avstream import AvStream, AvType from lasp.lasp_avstream import AvStream, AvType
from lasp.device import DAQConfiguration, RtAudio from lasp.lasp_siggen import Siggen, SignalType, SiggenData
from lasp.device import DaqConfigurations
config = DAQConfiguration.loadConfigs()[args.device]
rtaudio = RtAudio()
devices = rtaudio.getDeviceInfo()
output_devices = {}
for device in devices:
if device.outputchannels >= 0:
output_devices[device.name] = device
try:
output_device = output_devices[config.output_device_name]
except KeyError:
raise RuntimeError(f'output device {config.output_device_name} not available')
samplerate = int(config.en_output_rate)
stream = AvStream(output_device,
AvType.audio_output,
config)
# freq = 440.
freq = 1000.
omg = 2*np.pi*freq
def mycallback(indata, outdata, blockctr): if __name__ == '__main__':
frames = outdata.shape[0] multiprocessing.set_start_method('forkserver', force=True)
nchannels = outdata.shape[1] parser = argparse.ArgumentParser(
# nchannels = 1 description='Play a sine wave'
streamtime = blockctr*frames/samplerate )
t = np.linspace(streamtime, streamtime + frames/samplerate, device_help = 'DAQ Device to play to'
frames)[np.newaxis, :] parser.add_argument('--device', '-d', help=device_help, type=str,
outp = 0.01*np.sin(omg*t) default='Default')
for i in range(nchannels):
outdata[:,i] = ((2**16-1)*outp).astype(np.int16)
stream.addCallback(mycallback, AvType.audio_output) args = parser.parse_args()
stream.start()
input()
print('Stopping stream...') configs = DaqConfigurations.loadConfigs()
stream.stop()
config_keys = [key for key in configs.keys()]
for i, key in enumerate(config_keys):
print(f'{i:2} : {key}')
choosen_index = input('Number of configuration to use: ')
try:
daqindex = int(choosen_index)
except:
sys.exit(0)
choosen_key = config_keys[daqindex]
config = configs[choosen_key].output_config
print(f'Choosen configuration: {choosen_key}')
try:
siggendata = SiggenData(
fs=48e3,
nframes_per_block=1024,
dtype=np.dtype(np.int16),
eqdata=None,
level_dB=-20,
signaltype=SignalType.Periodic,
signaltypedata=(1000.,)
)
stream = AvStream(
AvType.audio_output,
config)
outq = stream.getOutputQueue()
stream.activateSiggen()
siggen = Siggen(outq, siggendata)
stream.start()
input('Press any key to stop...')
stream.stop()
siggen.stop()
finally:
try:
stream.cleanup()
del stream
except NameError:
pass
print('Stream stopped')
print('Closing stream...')
stream.close()
print('Stream closed')