Stream and recording seems to work. Also signal generator seems to work. Error handling is not working properly yet.
This commit is contained in:
parent
4657063467
commit
466a6f5cc1
@ -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:
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
"""
|
||||||
|
def __init__(self, siggendata, dataq, pipe):
|
||||||
|
|
||||||
|
"""
|
||||||
Args:
|
Args:
|
||||||
siggendata: The signal generator data to start with.
|
siggendata: The signal generator data to start with.
|
||||||
dataq: The queue to put generated signal on
|
dataq: The queue to put generated signal on
|
||||||
pipe: Control and status messaging pipe
|
pipe: Control and status messaging pipe
|
||||||
"""
|
"""
|
||||||
fs = siggendata.fs
|
|
||||||
|
self.dataq = dataq
|
||||||
|
self.siggendata = siggendata
|
||||||
|
self.pipe = pipe
|
||||||
|
self.eq = None
|
||||||
|
self.siggen = None
|
||||||
|
|
||||||
|
fs = self.siggendata.fs
|
||||||
nframes_per_block = siggendata.nframes_per_block
|
nframes_per_block = siggendata.nframes_per_block
|
||||||
level_dB = siggendata.level_dB
|
self.nblocks_buffer = max(
|
||||||
dtype = siggendata.dtype
|
|
||||||
|
|
||||||
signaltype = siggendata.signaltype
|
|
||||||
signaltypedata = siggendata.signaltypedata
|
|
||||||
|
|
||||||
nblocks_buffer = max(
|
|
||||||
1, int(QUEUE_BUFFER_TIME * fs/ nframes_per_block)
|
1, int(QUEUE_BUFFER_TIME * fs/ nframes_per_block)
|
||||||
)
|
)
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
def generate(siggen, eq):
|
def newSiggen(self, siggendata):
|
||||||
"""
|
|
||||||
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:
|
|
||||||
eqdata: dictionary containing equalizer data. TODO: document the
|
|
||||||
requiring fields.
|
|
||||||
"""
|
|
||||||
if eqdata is None:
|
|
||||||
return None
|
|
||||||
eq_type = eqdata['type']
|
|
||||||
eq_levels = eqdata['levels']
|
|
||||||
|
|
||||||
if eq_type == 'three':
|
|
||||||
fb = SosThirdOctaveFilterBank(fs)
|
|
||||||
elif eq_type == 'one':
|
|
||||||
fb = SosOctaveFilterBank(fs)
|
|
||||||
|
|
||||||
eq = Equalizer(fb._fb)
|
|
||||||
if eq_levels is not None:
|
|
||||||
eq.setLevels(eq_levels)
|
|
||||||
return eq
|
|
||||||
|
|
||||||
eq = createEqualizer(siggendata.eqdata)
|
|
||||||
|
|
||||||
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,46 +152,81 @@ 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
|
||||||
|
|
||||||
|
def generate(self):
|
||||||
|
"""
|
||||||
|
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))
|
||||||
|
|
||||||
|
def newEqualizer(self, eqdata):
|
||||||
|
"""
|
||||||
|
Create an equalizer object from equalizer data
|
||||||
|
|
||||||
|
Args:
|
||||||
|
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
|
||||||
|
|
||||||
|
if eq_type == 'three':
|
||||||
|
fb = SosThirdOctaveFilterBank(fs)
|
||||||
|
elif eq_type == 'one':
|
||||||
|
fb = SosOctaveFilterBank(fs)
|
||||||
|
|
||||||
|
eq = Equalizer(fb._fb)
|
||||||
|
if eq_levels is not None:
|
||||||
|
eq.setLevels(eq_levels)
|
||||||
|
return eq
|
||||||
|
|
||||||
|
def run(self):
|
||||||
# Initialization
|
# Initialization
|
||||||
try:
|
try:
|
||||||
siggen = newSiggen()
|
self.siggen = self.newSiggen(self.siggendata)
|
||||||
|
self.eq = self.newEqualizer(self.siggendata.eqdata)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pipe.send((SiggenMessage.error, str(e)))
|
self.pipe.send((SiggenMessage.error, str(e)))
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
finally:
|
# Pre-generate blocks of signal data
|
||||||
pipe.send((SiggenMessage.done, None))
|
while self.dataq.qsize() < self.nblocks_buffer:
|
||||||
|
self.generate()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
if pipe.poll(timeout=QUEUE_BUFFER_TIME / 2):
|
if self.pipe.poll(timeout=QUEUE_BUFFER_TIME / 2):
|
||||||
msg, data = pipe.recv()
|
msg, data = self.pipe.recv()
|
||||||
if msg == SiggenMessage.stop:
|
if msg == SiggenMessage.stop:
|
||||||
logging.debug("Signal generator caught 'stop' message. Exiting.")
|
logging.debug("Signal generator caught 'stop' message. Exiting.")
|
||||||
return 0
|
return 0
|
||||||
elif msg == SiggenMessage.adjustVolume:
|
elif msg == SiggenMessage.adjustVolume:
|
||||||
logging.debug(f"Signal generator caught 'adjustVolume' message. New volume = {level_dB:.1f} dB FS")
|
logging.debug(f"Signal generator caught 'adjustVolume' message. New volume = {level_dB:.1f} dB FS")
|
||||||
level_dB = data
|
level_dB = data
|
||||||
siggen.setLevel(level_dB)
|
self.siggen.setLevel(level_dB)
|
||||||
elif msg == SiggenMessage.newEqSettings:
|
elif msg == SiggenMessage.newEqSettings:
|
||||||
eqdata = data
|
eqdata = data
|
||||||
eq = createEqualizer(eqdata)
|
eq = self.newEqualizer(eqdata)
|
||||||
else:
|
else:
|
||||||
pipe.send(
|
self.pipe.send(
|
||||||
SiggenMessage.error, "BUG: Generator caught unknown message. Quiting"
|
SiggenMessage.error, "BUG: Generator caught unknown message. Quiting"
|
||||||
)
|
)
|
||||||
elif dataq.qsize() < nblocks_buffer:
|
while self.dataq.qsize() < self.nblocks_buffer:
|
||||||
# Generate new data and put it in the queue!
|
# Generate new data and put it in the queue!
|
||||||
try:
|
try:
|
||||||
generate(siggen, eq)
|
self.generate()
|
||||||
except SiggenWorkerDone:
|
except SiggenWorkerDone:
|
||||||
pipe.send(SiggenMessage.done)
|
self.pipe.send(SiggenMessage.done)
|
||||||
return 0
|
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))
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
import multiprocessing
|
||||||
|
from lasp.lasp_multiprocessingpatch import apply_patch
|
||||||
|
from lasp.lasp_avstream import AvStream, AvType
|
||||||
|
from lasp.lasp_siggen import Siggen, SignalType, SiggenData
|
||||||
|
from lasp.device import DaqConfigurations
|
||||||
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
if __name__ == '__main__':
|
||||||
|
multiprocessing.set_start_method('forkserver', force=True)
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
description='Play a sine wave'
|
description='Play a sine wave'
|
||||||
)
|
)
|
||||||
device_help = 'DAQ Device to play to'
|
device_help = 'DAQ Device to play to'
|
||||||
parser.add_argument('--device', '-d', help=device_help, type=str,
|
parser.add_argument('--device', '-d', help=device_help, type=str,
|
||||||
default='Default')
|
default='Default')
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
from lasp.lasp_avstream import AvStream, AvType
|
|
||||||
from lasp.device import DAQConfiguration, RtAudio
|
|
||||||
|
|
||||||
config = DAQConfiguration.loadConfigs()[args.device]
|
configs = DaqConfigurations.loadConfigs()
|
||||||
|
|
||||||
rtaudio = RtAudio()
|
config_keys = [key for key in configs.keys()]
|
||||||
devices = rtaudio.getDeviceInfo()
|
for i, key in enumerate(config_keys):
|
||||||
|
print(f'{i:2} : {key}')
|
||||||
|
|
||||||
output_devices = {}
|
choosen_index = input('Number of configuration to use: ')
|
||||||
for device in devices:
|
try:
|
||||||
if device.outputchannels >= 0:
|
daqindex = int(choosen_index)
|
||||||
output_devices[device.name] = device
|
except:
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
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)
|
choosen_key = config_keys[daqindex]
|
||||||
stream = AvStream(output_device,
|
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,
|
AvType.audio_output,
|
||||||
config)
|
config)
|
||||||
|
|
||||||
# freq = 440.
|
outq = stream.getOutputQueue()
|
||||||
freq = 1000.
|
stream.activateSiggen()
|
||||||
omg = 2*np.pi*freq
|
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
|
||||||
|
|
||||||
|
|
||||||
def mycallback(indata, outdata, blockctr):
|
|
||||||
frames = outdata.shape[0]
|
|
||||||
nchannels = outdata.shape[1]
|
|
||||||
# nchannels = 1
|
|
||||||
streamtime = blockctr*frames/samplerate
|
|
||||||
t = np.linspace(streamtime, streamtime + frames/samplerate,
|
|
||||||
frames)[np.newaxis, :]
|
|
||||||
outp = 0.01*np.sin(omg*t)
|
|
||||||
for i in range(nchannels):
|
|
||||||
outdata[:,i] = ((2**16-1)*outp).astype(np.int16)
|
|
||||||
|
|
||||||
stream.addCallback(mycallback, AvType.audio_output)
|
|
||||||
stream.start()
|
|
||||||
|
|
||||||
input()
|
|
||||||
|
|
||||||
print('Stopping stream...')
|
|
||||||
stream.stop()
|
|
||||||
|
|
||||||
print('Stream stopped')
|
|
||||||
print('Closing stream...')
|
|
||||||
stream.close()
|
|
||||||
print('Stream closed')
|
|
||||||
|
Loading…
Reference in New Issue
Block a user