First work on new Siggen implementation
This commit is contained in:
parent
99b8553235
commit
11cc623363
279
lasp/lasp_siggen.py
Normal file
279
lasp/lasp_siggen.py
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
#!/usr/bin/env python3.6
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Author: J.A. de Jong - ASCEE
|
||||||
|
|
||||||
|
Description: Signal generator code
|
||||||
|
|
||||||
|
"""
|
||||||
|
import dataclasses
|
||||||
|
import logging
|
||||||
|
import multiprocessing as mp
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from .filter import PinkNoise
|
||||||
|
from .lasp_avstream import AvStream, AvType
|
||||||
|
from .wrappers import Siggen as pyxSiggen
|
||||||
|
|
||||||
|
QUEUE_BUFFER_TIME = 0.3 # The amount of time used in the queues for buffering
|
||||||
|
# of data, larger is more stable, but also enlarges latency
|
||||||
|
|
||||||
|
__all__ = ["SignalType", "NoiseType", "SiggenMessage", "SiggenData", "Siggen"]
|
||||||
|
|
||||||
|
|
||||||
|
class SignalType:
|
||||||
|
Periodic = 0
|
||||||
|
Noise = 1
|
||||||
|
Sweep = 2
|
||||||
|
Meas = 3
|
||||||
|
|
||||||
|
|
||||||
|
class NoiseType:
|
||||||
|
white = (0, "White noise")
|
||||||
|
pink = (1, "Pink noise")
|
||||||
|
types = (white, pink)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fillComboBox(combo):
|
||||||
|
for type_ in NoiseType.types:
|
||||||
|
combo.addItem(type_[1])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def getCurrent(cb):
|
||||||
|
return NoiseType.types[cb.currentIndex()]
|
||||||
|
|
||||||
|
|
||||||
|
class SiggenWorkerDone(Exception):
|
||||||
|
def __str__(self):
|
||||||
|
return "Done generating signal"
|
||||||
|
|
||||||
|
|
||||||
|
class SiggenMessage:
|
||||||
|
"""
|
||||||
|
Different messages that can be send to the signal generator over the pipe
|
||||||
|
connection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
stop = 0 # Stop and quit the signal generator
|
||||||
|
generate = 1
|
||||||
|
adjustVolume = 2 # Adjust the volume
|
||||||
|
newEqSettings = 3 # Forward new equalizer settings
|
||||||
|
|
||||||
|
# These messages are send back to the main thread over the pipe
|
||||||
|
ready = 4
|
||||||
|
error = 5
|
||||||
|
done = 6
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class SiggenData:
|
||||||
|
fs: float # Sample rate [Hz]
|
||||||
|
|
||||||
|
# Number of frames "samples" to send in one block
|
||||||
|
nframes_per_block: int
|
||||||
|
|
||||||
|
# The data type to output
|
||||||
|
dtype: np.dtype
|
||||||
|
|
||||||
|
# Settings for the equalizer etc
|
||||||
|
eqdata: object # Equalizer data
|
||||||
|
|
||||||
|
# Level of output signal [dBFS]el
|
||||||
|
level_dB: float
|
||||||
|
|
||||||
|
# Signal type specific data, i.e.
|
||||||
|
signaltype: SignalType
|
||||||
|
signaltypedata: Tuple = None
|
||||||
|
|
||||||
|
|
||||||
|
def siggenFcn(siggendata: SiggenData, nblocks_buffer: int,
|
||||||
|
queues: list
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Main function running in a different process, is responsible for generating
|
||||||
|
new signal data. Uses the signal queue to push new generated signal data
|
||||||
|
on.
|
||||||
|
"""
|
||||||
|
fs = siggendata.fs
|
||||||
|
nframes_per_block = siggendata.nframes_per_block
|
||||||
|
level_dB = siggendata.level_dB
|
||||||
|
dtype = siggendata.dtype
|
||||||
|
eq = None
|
||||||
|
|
||||||
|
signaltype = siggendata.signaltype
|
||||||
|
signaltypedata = siggendata.signaltypedata
|
||||||
|
|
||||||
|
dataq, msgq, statusq = queues
|
||||||
|
|
||||||
|
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 newSiggen():
|
||||||
|
"""
|
||||||
|
Create a signal generator based on parameters specified in global
|
||||||
|
function data.
|
||||||
|
"""
|
||||||
|
if signaltype == SignalType.Periodic:
|
||||||
|
freq, = signaltypedata
|
||||||
|
siggen = pyxSiggen.sineWave(fs, freq, level_dB)
|
||||||
|
elif signaltype == SignalType.Noise:
|
||||||
|
noisetype, zerodBpoint = signaltypedata
|
||||||
|
if noisetype == NoiseType.white:
|
||||||
|
sos_colorfilter = None
|
||||||
|
elif noisetype == NoiseType.pink:
|
||||||
|
sos_colorfilter = PinkNoise(fs, zerodBpoint).flatten()
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Unknown noise type")
|
||||||
|
|
||||||
|
siggen = pyxSiggen.noise(fs, level_dB, sos_colorfilter)
|
||||||
|
|
||||||
|
elif signaltype == SignalType.Sweep:
|
||||||
|
fl, fu, Ts, Tq, sweep_flags = signaltypedata
|
||||||
|
siggen = pyxSiggen.sweep(fs, fl, fu, Ts, Tq, sweep_flags, level_dB)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Not implemented signal type: {signaltype}")
|
||||||
|
|
||||||
|
# Pre-generate blocks of signal data
|
||||||
|
while dataq.qsize() < nblocks_buffer:
|
||||||
|
generate(siggen, eq)
|
||||||
|
|
||||||
|
return siggen
|
||||||
|
|
||||||
|
# Initialization
|
||||||
|
try:
|
||||||
|
siggen = newSiggen()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
statusq.put((SiggenMessage.error, str(e)))
|
||||||
|
return 1
|
||||||
|
|
||||||
|
finally:
|
||||||
|
statusq.put((SiggenMessage.done, None))
|
||||||
|
|
||||||
|
while True:
|
||||||
|
msg, data = msgq.get()
|
||||||
|
if msg == SiggenMessage.stop:
|
||||||
|
logging.debug("Signal generator caught 'stop' message. Exiting.")
|
||||||
|
return 0
|
||||||
|
elif msg == SiggenMessage.generate:
|
||||||
|
# logging.debug(f"Signal generator caught 'generate' message")
|
||||||
|
try:
|
||||||
|
while dataq.qsize() < nblocks_buffer:
|
||||||
|
# Generate new data and put it in the queue!
|
||||||
|
generate(siggen, eq)
|
||||||
|
except SiggenWorkerDone:
|
||||||
|
statusq.put(SiggenMessage.done)
|
||||||
|
return 0
|
||||||
|
elif msg == SiggenMessage.adjustVolume:
|
||||||
|
logging.debug(f"Signal generator caught 'adjustVolume' message. New volume = {level_dB:.1f} dB FS")
|
||||||
|
level_dB = data
|
||||||
|
siggen = newSiggen()
|
||||||
|
else:
|
||||||
|
statusq.put(
|
||||||
|
SiggenMessage.error, "BUG: Generator caught unknown message. Quiting"
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
class Siggen:
|
||||||
|
"""
|
||||||
|
Signal generator class, generates signal data in a different process to
|
||||||
|
unload the work in the calling thread.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, stream: AvStream, siggendata: SiggenData):
|
||||||
|
""""""
|
||||||
|
self.stream = stream
|
||||||
|
self.nblocks_buffer = max(
|
||||||
|
1, int(QUEUE_BUFFER_TIME * stream.samplerate / stream.blocksize)
|
||||||
|
)
|
||||||
|
|
||||||
|
qs = [mp.Queue() for i in range(3)]
|
||||||
|
self.dataq, self.msgq, self.statusq = qs
|
||||||
|
|
||||||
|
self.process = mp.Process(
|
||||||
|
target=siggenFcn,
|
||||||
|
args=(siggendata, self.nblocks_buffer, qs),
|
||||||
|
)
|
||||||
|
self.process.start()
|
||||||
|
|
||||||
|
self.handle_msgs()
|
||||||
|
if not self.process.is_alive():
|
||||||
|
raise RuntimeError('Unexpected signal generator exception')
|
||||||
|
|
||||||
|
self.stopped = False
|
||||||
|
|
||||||
|
def setLevel(self, new_level):
|
||||||
|
"""
|
||||||
|
Set a new signal level to the generator
|
||||||
|
|
||||||
|
Args:
|
||||||
|
new_level: The new level in [dBFS]
|
||||||
|
"""
|
||||||
|
self.msgq.put((SiggenMessage.adjustVolume, new_level))
|
||||||
|
|
||||||
|
def handle_msgs(self):
|
||||||
|
while not self.statusq.empty():
|
||||||
|
msg, data = self.statusq.get()
|
||||||
|
if msg == SiggenMessage.error:
|
||||||
|
self.stop()
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Error in initialization of signal generator: {data}"
|
||||||
|
)
|
||||||
|
elif msg == SiggenMessage.ready:
|
||||||
|
return
|
||||||
|
|
||||||
|
elif msg == SiggenMessage.done:
|
||||||
|
self.stop()
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.stream.addCallback(self.streamCallback, AvType.audio_output)
|
||||||
|
self.handle_msgs()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
logging.debug('Siggen::stop()')
|
||||||
|
if self.stopped:
|
||||||
|
raise RuntimeError('BUG: Siggen::stop() is called twice!')
|
||||||
|
self.stream.removeCallback(self.streamCallback, AvType.audio_output)
|
||||||
|
while not self.dataq.empty():
|
||||||
|
self.dataq.get()
|
||||||
|
while not self.statusq.empty():
|
||||||
|
self.statusq.get()
|
||||||
|
self.msgq.put((SiggenMessage.stop, None))
|
||||||
|
|
||||||
|
logging.debug('Joining siggen process')
|
||||||
|
self.process.join()
|
||||||
|
logging.debug('Joining siggen process done')
|
||||||
|
self.process.close()
|
||||||
|
|
||||||
|
self.process = None
|
||||||
|
logging.debug('End Siggen::stop()')
|
||||||
|
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.msgq.put((SiggenMessage.generate, None))
|
Loading…
Reference in New Issue
Block a user