mirror of https://github.com/DJ2LS/FreeDATA.git
578 lines
25 KiB
Python
578 lines
25 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Created on Wed Dec 23 07:04:24 2020
|
|
|
|
@author: DJ2LS
|
|
"""
|
|
|
|
import queue
|
|
import threading
|
|
import time
|
|
from freedata_server import codec2
|
|
import numpy as np
|
|
import sounddevice as sd
|
|
import structlog
|
|
from freedata_server import cw
|
|
from freedata_server import audio
|
|
from freedata_server import demodulator
|
|
from freedata_server import modulator
|
|
|
|
|
|
class RF:
|
|
"""Handles FreeDATA modem functionality.
|
|
|
|
This class manages the audio interface, modulation, demodulation, and
|
|
transmission of FreeDATA signals. It interacts with the demodulator,
|
|
modulator, audio devices, and radio manager to handle data transmission
|
|
and reception.
|
|
"""
|
|
|
|
log = structlog.get_logger("RF")
|
|
|
|
def __init__(self, ctx) -> None:
|
|
"""Initializes the RF modem.
|
|
|
|
Args:
|
|
self.ctx.config_manager (dict): self.ctx.config_manageruration dictionary.
|
|
event_manager (EventManager): Event manager instance.
|
|
fft_queue (Queue): Queue for FFT data.
|
|
self.ctx.modem_service (Queue): Queue for freedata_server service commands.
|
|
states (StateManager): State manager instance.
|
|
radio_manager (RadioManager): Radio manager instance.
|
|
"""
|
|
|
|
self.ctx = ctx
|
|
self.sampler_avg = 0
|
|
self.buffer_avg = 0
|
|
|
|
# these are crc ids now
|
|
self.audio_input_device = self.ctx.config_manager.config["AUDIO"]["input_device"]
|
|
self.audio_output_device = self.ctx.config_manager.config["AUDIO"]["output_device"]
|
|
|
|
self.ctx.radio_managercontrol = self.ctx.config_manager.config["RADIO"]["control"]
|
|
self.rigctld_ip = self.ctx.config_manager.config["RIGCTLD"]["ip"]
|
|
self.rigctld_port = self.ctx.config_manager.config["RIGCTLD"]["port"]
|
|
|
|
self.tx_audio_level = self.ctx.config_manager.config["AUDIO"]["tx_audio_level"]
|
|
self.rx_audio_level = self.ctx.config_manager.config["AUDIO"]["rx_audio_level"]
|
|
|
|
self.ptt_state = False
|
|
self.enqueuing_audio = False # set to True, while we are processing audio
|
|
|
|
self.AUDIO_SAMPLE_RATE = 48000
|
|
self.modem_sample_rate = codec2.api.FREEDV_FS_8000
|
|
|
|
# 8192 Let's do some tests with very small chunks for TX
|
|
# 8 * (self.AUDIO_SAMPLE_RATE/self.modem_sample_rate) == 48
|
|
self.AUDIO_CHANNELS = 1
|
|
self.MODE = 0
|
|
self.rms_counter = 0
|
|
|
|
self.AUDIO_STREAMING_CHUNK_SIZE = 2400
|
|
self.audio_out_queue = queue.Queue()
|
|
|
|
# Size of the block the RX DSP chain runs on, in 48 kHz samples. The input
|
|
# stream is opened with blocksize=0 (PortAudio picks the capture size per
|
|
# device), so the DSP must not rely on the captured block size:
|
|
# rx_audio_processing_worker re-blocks whatever the callback is handed
|
|
# to exactly this size, so a device or setting that delivers some other
|
|
# size cannot reach the DSP. 4800 (100 ms, 800 samples at 8 kHz) is the
|
|
# size the rest of the chain is built around:
|
|
# * codec2.resampler.resample48_to_8 asserts len % FDMDV_OS_48 (6) == 0
|
|
# and raises AssertionError otherwise,
|
|
# * audio.calculate_fft pads/truncates to 800 samples at 8 kHz, so a
|
|
# shorter block is mostly zero padding and the spectrum, channel busy
|
|
# detection and audio_dbfs all degrade,
|
|
# * enqueue_streaming_audio_chunks zero-pads every block up to
|
|
# AUDIO_STREAMING_CHUNK_SIZE, so a shorter block streams mostly
|
|
# silence and emits one chunk per block regardless of size,
|
|
# * audio.normalize_audio (rx_auto_audio_level, on by default)
|
|
# normalizes per block, so a shorter block means a faster, jumpier AGC.
|
|
# Lowering this lowers RX latency, but needs those four addressed first.
|
|
self.RX_DSP_BLOCK_48K = 4800
|
|
|
|
# Make sure our resampler will work
|
|
assert (self.AUDIO_SAMPLE_RATE / self.modem_sample_rate) == codec2.api.FDMDV_OS_48 # type: ignore
|
|
|
|
self.data_queue_received = queue.Queue()
|
|
|
|
# RX audio captured by the real-time sounddevice callback is handed to
|
|
# rx_audio_processing_worker through this queue, keeping the callback
|
|
# minimal (copy + enqueue). Running the DSP in the callback under the GIL
|
|
# is what makes it miss its deadline and overflow on slower CPUs.
|
|
self.rx_audio_in_queue = queue.Queue(maxsize=100)
|
|
self.rx_audio_worker_running = False
|
|
self.rx_audio_worker_thread = None
|
|
self.rx_audio_dropped_blocks = 0
|
|
# 48 kHz samples that have been captured but do not yet fill a whole
|
|
# RX_DSP_BLOCK_48K; they are carried over to the next captured block so the
|
|
# sample stream handed to the resampler stays gapless (its filter memory
|
|
# depends on that) and always has a valid length.
|
|
self.rx_audio_carry_48k = np.empty(0, dtype=np.int16)
|
|
|
|
self.demodulator = demodulator.Demodulator(self.ctx)
|
|
self.modulator = modulator.Modulator(self.ctx)
|
|
|
|
def start_modem(self):
|
|
"""Starts the modem.
|
|
|
|
This method initializes the audio devices and starts the demodulator.
|
|
In test mode, it bypasses audio initialization. It raises a
|
|
RuntimeError if audio initialization fails.
|
|
|
|
Returns:
|
|
bool: True if the modem started successfully.
|
|
|
|
Raises:
|
|
RuntimeError: If audio device initialization fails.
|
|
"""
|
|
if self.ctx.TESTMODE:
|
|
self.log.warning("RUNNING IN TEST MODE")
|
|
self.resampler = codec2.resampler() # we need a resampler in test mode
|
|
self.demodulator.start(None)
|
|
return True
|
|
else:
|
|
if not self.init_audio():
|
|
raise RuntimeError("Unable to init audio devices")
|
|
self.demodulator.start(self.sd_input_stream)
|
|
|
|
return True
|
|
|
|
def stop_modem(self):
|
|
"""Stops the modem.
|
|
|
|
This method stops the FreeDATA freedata_server service, closes audio input and
|
|
output streams, and handles any exceptions during the process.
|
|
"""
|
|
try:
|
|
# let's stop the freedata_server service
|
|
self.ctx.modem_service.put("stop")
|
|
# simulate audio class active state for reducing cli output
|
|
# self.stream = lambda: None
|
|
# self.stream.active = False
|
|
# self.stream.stop
|
|
# stop the RX audio processing worker before closing the streams
|
|
self.rx_audio_worker_running = False
|
|
try:
|
|
self.rx_audio_in_queue.put_nowait(None)
|
|
except queue.Full:
|
|
pass
|
|
self.sd_input_stream.close()
|
|
self.sd_output_stream.close()
|
|
except Exception as e:
|
|
self.log.error("[MDM] Error stopping freedata_server", e=e)
|
|
|
|
def init_audio(self):
|
|
"""Initializes the audio input and output streams.
|
|
|
|
This method retrieves the audio device indices based on their CRC
|
|
checksums from the self.ctx.config_manageruration, sets up the default audio
|
|
parameters, initializes the Codec2 resampler, and starts the
|
|
SoundDevice input and output streams with appropriate callbacks and
|
|
buffer sizes. It logs information about the selected audio devices
|
|
and handles potential exceptions during initialization.
|
|
|
|
Returns:
|
|
bool: True if audio initialization was successful, False otherwise.
|
|
"""
|
|
self.log.info(
|
|
"[MDM] init: get audio devices",
|
|
input_device=self.audio_input_device,
|
|
output_device=self.audio_output_device,
|
|
)
|
|
try:
|
|
result = audio.get_device_index_from_crc(self.audio_input_device, True)
|
|
if result is None:
|
|
raise ValueError("Invalid input device")
|
|
else:
|
|
in_dev_index, in_dev_name = result
|
|
|
|
result = audio.get_device_index_from_crc(self.audio_output_device, False)
|
|
if result is None:
|
|
raise ValueError("Invalid output device")
|
|
else:
|
|
out_dev_index, out_dev_name = result
|
|
|
|
self.log.info(f"[MDM] init: receiving audio from '{in_dev_name}'")
|
|
self.log.info(f"[MDM] init: transmiting audio on '{out_dev_name}'")
|
|
self.log.debug("[MDM] init: starting pyaudio callback and decoding threads")
|
|
|
|
sd.default.samplerate = self.AUDIO_SAMPLE_RATE
|
|
sd.default.device = (in_dev_index, out_dev_index)
|
|
|
|
# init codec2 resampler
|
|
self.resampler = codec2.resampler()
|
|
|
|
# SoundDevice audio input stream
|
|
# blocksize=0 lets PortAudio pick the capture block size per device.
|
|
# This is deliberate and load-bearing: a fixed blocksize=4800 makes
|
|
# some virtual devices (measured on snd-aloop) starve/overflow --
|
|
# a two period ring at 100 ms periods drops audio continuously and
|
|
# the modem is deaf. latency=0.2 sets the ring depth explicitly so
|
|
# the ring comes out deep (many small periods) on every device
|
|
# measured (CM108 hardware and snd-aloop alike).
|
|
# The capture block size is decoupled from the DSP block size:
|
|
# PortAudio may deliver any block length here (512/1024-class blocks
|
|
# are common on real hardware, and are NOT a multiple of codec2's
|
|
# FDMDV_OS_48 == 6, which resample48_to_8 asserts on).
|
|
# rx_audio_processing_worker re-blocks whatever arrives into exact
|
|
# RX_DSP_BLOCK_48K blocks, so no capture size can reach the DSP
|
|
# chain short or misaligned; see the note on that constant.
|
|
self.sd_input_stream = sd.InputStream(
|
|
channels=1,
|
|
dtype="int16",
|
|
callback=self.sd_input_audio_callback,
|
|
device=in_dev_index,
|
|
samplerate=self.AUDIO_SAMPLE_RATE,
|
|
blocksize=0,
|
|
latency=0.2,
|
|
)
|
|
self.sd_input_stream.start()
|
|
|
|
# process RX audio off the real-time callback thread
|
|
self.rx_audio_carry_48k = np.empty(0, dtype=np.int16) # no stale audio across restarts
|
|
self.rx_audio_worker_running = True
|
|
self.rx_audio_worker_thread = threading.Thread(
|
|
target=self.rx_audio_processing_worker,
|
|
name="rx_audio_processing_worker",
|
|
daemon=True,
|
|
)
|
|
self.rx_audio_worker_thread.start()
|
|
|
|
self.sd_output_stream = sd.OutputStream(
|
|
channels=1,
|
|
dtype="int16",
|
|
callback=self.sd_output_audio_callback,
|
|
device=out_dev_index,
|
|
samplerate=self.AUDIO_SAMPLE_RATE,
|
|
blocksize=2400,
|
|
)
|
|
self.sd_output_stream.start()
|
|
|
|
return True
|
|
|
|
except Exception as audioerr:
|
|
self.log.error("[MDM] init: starting pyaudio callback failed", e=audioerr)
|
|
self.stop_modem()
|
|
return False
|
|
|
|
def transmit_sine(self):
|
|
"""Transmit a sine wave for audio tuning"""
|
|
self.ctx.state_manager.setTransmitting(True)
|
|
self.log.info("[MDM] TRANSMIT", mode="SINE")
|
|
start_of_transmission = time.time()
|
|
|
|
f0 = 1500 # Frequency of sine wave in Hz
|
|
fs = 48000 # Sample rate in Hz
|
|
max_duration = 30 # Maximum duration in seconds
|
|
|
|
# Create sine wave signal
|
|
t = np.linspace(0, max_duration, int(fs * max_duration), endpoint=False)
|
|
s = 0.5 * np.sin(2 * np.pi * f0 * t)
|
|
signal = np.int16(s * 32767) # Convert to 16-bit integer PCM format
|
|
|
|
signal = audio.normalize_audio(signal)
|
|
|
|
# Set audio volume and prepare buffer for transmission
|
|
txbuffer_out = audio.set_audio_volume(signal, self.tx_audio_level)
|
|
|
|
# Transmit audio
|
|
self.enqueue_audio_out(txbuffer_out)
|
|
|
|
end_of_transmission = time.time()
|
|
transmission_time = end_of_transmission - start_of_transmission
|
|
self.ctx.state_manager.setTransmitting(False)
|
|
|
|
self.log.debug("[MDM] ON AIR TIME", time=transmission_time)
|
|
|
|
def stop_sine(self):
|
|
"""Stop transmitting sine wave"""
|
|
# clear audio out queue
|
|
self.audio_out_queue.queue.clear()
|
|
self.ctx.state_manager.setTransmitting(False)
|
|
self.log.debug("[MDM] Stopped transmitting sine")
|
|
|
|
def transmit_morse(self, repeats, repeat_delay, frames):
|
|
"""Transmits Morse code.
|
|
|
|
This method transmits the station's callsign as Morse code. It waits
|
|
for any ongoing transmissions to complete, sets the transmitting
|
|
state, generates the Morse code audio signal, normalizes it, and
|
|
enqueues it for output. It logs the transmission mode and on-air
|
|
time. The repeats, repeat_delay, and frames arguments are not
|
|
currently used in this method.
|
|
|
|
Args:
|
|
repeats: Currently unused.
|
|
repeat_delay: Currently unused.
|
|
frames: Currently unused.
|
|
"""
|
|
self.ctx.state_manager.waitForTransmission()
|
|
self.ctx.state_manager.setTransmitting(True)
|
|
# if we're transmitting FreeDATA signals, reset channel busy state
|
|
self.log.debug("[MDM] TRANSMIT", mode="MORSE")
|
|
start_of_transmission = time.time()
|
|
txbuffer_out = cw.MorseCodePlayer().text_to_signal(self.ctx.config_manager.config["STATION"].get("mycall"))
|
|
txbuffer_out = audio.normalize_audio(txbuffer_out)
|
|
# transmit audio
|
|
self.enqueue_audio_out(txbuffer_out)
|
|
|
|
end_of_transmission = time.time()
|
|
transmission_time = end_of_transmission - start_of_transmission
|
|
self.log.debug("[MDM] ON AIR TIME", time=transmission_time)
|
|
|
|
def transmit(self, mode, repeats: int, repeat_delay: int, frames: bytearray) -> None:
|
|
"""Transmits data using the specified mode and parameters.
|
|
|
|
This method transmits data using the given FreeDV mode, number of
|
|
repeats, repeat delay, and frames. It handles synchronization with
|
|
other transmissions, creates the modulated burst, resamples the
|
|
audio, sets the transmit audio level, enqueues the audio for
|
|
output, and logs transmission details.
|
|
|
|
Args:
|
|
mode: The FreeDV mode to use for transmission.
|
|
repeats (int): The number of times to repeat the frames.
|
|
repeat_delay (int): The delay between repetitions in milliseconds.
|
|
frames (bytearray): The data frames to transmit.
|
|
"""
|
|
|
|
if self.ctx.TESTMODE:
|
|
self.ctx.TESTMODE_TRANSMIT_QUEUE.put([mode, frames])
|
|
return
|
|
|
|
self.demodulator.reset_data_sync()
|
|
# Wait for some other thread that might be transmitting
|
|
self.ctx.state_manager.waitForTransmission()
|
|
self.ctx.state_manager.setTransmitting(True)
|
|
# self.ctx.state_manager.channel_busy_event.wait()
|
|
|
|
start_of_transmission = time.time()
|
|
txbuffer = self.modulator.create_burst(mode, repeats, repeat_delay, frames)
|
|
# Re-sample back up to 48k (resampler works on np.int16)
|
|
x = np.frombuffer(txbuffer, dtype=np.int16)
|
|
|
|
if self.ctx.config_manager.config["AUDIO"].get("tx_auto_audio_level"):
|
|
x = audio.normalize_audio(x)
|
|
x = audio.set_audio_volume(x, self.tx_audio_level)
|
|
txbuffer_out = self.resampler.resample8_to_48(x)
|
|
# transmit audio
|
|
self.enqueue_audio_out(txbuffer_out)
|
|
|
|
end_of_transmission = time.time()
|
|
transmission_time = end_of_transmission - start_of_transmission
|
|
self.log.debug("[MDM] ON AIR TIME", time=transmission_time)
|
|
|
|
def enqueue_audio_out(self, audio_48k) -> None:
|
|
"""Enqueues audio data for output.
|
|
|
|
This method enqueues the provided 48kHz audio data for output. It
|
|
handles PTT activation, event signaling, slicing the audio into
|
|
blocks, and adding the blocks to the output queue. It also manages
|
|
the transmitting state and waits for the transmission to complete
|
|
before deactivating PTT.
|
|
|
|
Args:
|
|
audio_48k (numpy.ndarray): The 48kHz audio data to enqueue.
|
|
"""
|
|
self.enqueuing_audio = True
|
|
if not self.ctx.state_manager.isTransmitting():
|
|
self.ctx.state_manager.setTransmitting(True)
|
|
if self.ctx.radio_manager:
|
|
self.ctx.radio_manager.set_ptt(True)
|
|
else:
|
|
self.log.warning("Radio manager not yet initialized...should happen soon, some errors might occur") #
|
|
|
|
self.ctx.event_manager.send_ptt_change(True)
|
|
|
|
# slice audio data to needed blocklength
|
|
if self.ctx.TESTMODE:
|
|
block_size = 2400
|
|
else:
|
|
block_size = self.sd_output_stream.blocksize
|
|
|
|
pad_length = -len(audio_48k) % block_size
|
|
padded_data = np.pad(audio_48k, (0, pad_length), mode="constant")
|
|
sliced_audio_data = padded_data.reshape(-1, block_size)
|
|
# add each block to audio out queue
|
|
for block in sliced_audio_data:
|
|
self.audio_out_queue.put(block)
|
|
|
|
self.enqueuing_audio = False
|
|
self.ctx.state_manager.transmitting_event.wait()
|
|
|
|
if self.ctx.radio_manager:
|
|
self.ctx.radio_manager.set_ptt(False)
|
|
else:
|
|
self.log.warning("Radio manager not yet initialized...should happen soon, some errors might occur") #
|
|
|
|
self.ctx.event_manager.send_ptt_change(False)
|
|
|
|
return
|
|
|
|
def enqueue_streaming_audio_chunks(self, audio_block, queue):
|
|
# total_samples = len(audio_block)
|
|
# for start in range(0, total_samples, self.AUDIO_STREAMING_CHUNK_SIZE):
|
|
# end = start + self.AUDIO_STREAMING_CHUNK_SIZE
|
|
# chunk = audio_block[start:end]
|
|
# queue.put(chunk.tobytes())
|
|
|
|
block_size = self.AUDIO_STREAMING_CHUNK_SIZE
|
|
|
|
pad_length = -len(audio_block) % block_size
|
|
padded_data = np.pad(audio_block, (0, pad_length), mode="constant")
|
|
sliced_audio_data = padded_data.reshape(-1, block_size)
|
|
# add each block to audio out queue
|
|
for block in sliced_audio_data:
|
|
queue.put(block)
|
|
|
|
def sd_output_audio_callback(self, outdata: np.ndarray, frames: int, time, status) -> None:
|
|
"""Callback function for the audio output stream.
|
|
|
|
This method is called by the SoundDevice output stream to provide
|
|
audio data for playback. It retrieves audio chunks from the output
|
|
queue, resamples them to 8kHz, calculates the FFT, and sends the
|
|
data to the output stream. It also manages the transmitting state
|
|
and handles exceptions during audio processing.
|
|
|
|
Args:
|
|
outdata (np.ndarray): The output audio buffer.
|
|
frames (int): The number of frames to output.
|
|
time: The current time.
|
|
status: The status of the output stream.
|
|
"""
|
|
|
|
try:
|
|
if not self.audio_out_queue.empty() and not self.enqueuing_audio:
|
|
chunk = self.audio_out_queue.get_nowait()
|
|
audio_8k = self.resampler.resample48_to_8(chunk)
|
|
audio.calculate_fft(audio_8k, self.ctx.modem_fft, self.ctx.state_manager)
|
|
outdata[:] = chunk.reshape(outdata.shape)
|
|
|
|
else:
|
|
# reset transmitting state only, if we are not actively processing audio
|
|
# for avoiding a ptt toggle state bug
|
|
if self.audio_out_queue.empty() and not self.enqueuing_audio:
|
|
self.ctx.state_manager.setTransmitting(False)
|
|
# Fill with zeros if the queue is empty
|
|
outdata.fill(0)
|
|
except Exception as e:
|
|
self.log.warning("[AUDIO STATUS]", status=status, time=time, frames=frames, e=e)
|
|
outdata.fill(0)
|
|
|
|
def sd_input_audio_callback(self, indata: np.ndarray, frames: int, time, status) -> None:
|
|
"""Callback function for the audio input stream.
|
|
|
|
This method is called by the SoundDevice input stream when audio
|
|
data is available. It resamples the incoming 48kHz audio to 8kHz,
|
|
adjusts the audio level, calculates FFT data if not transmitting,
|
|
and pushes the audio data to the appropriate demodulator buffers.
|
|
It handles buffer overflows and logs audio exceptions.
|
|
|
|
Args:
|
|
indata (np.ndarray): Input audio data buffer.
|
|
frames (int): Number of frames received.
|
|
time: Current time.
|
|
status: Input stream status.
|
|
"""
|
|
if status:
|
|
self.log.warning("[AUDIO STATUS]", status=status, time=time, frames=frames)
|
|
# FIXME on windows input overflows crashing the rx audio stream. Lets restart the server then
|
|
# if status.input_overflow:
|
|
# self.self.ctx.modem_service.put("restart")
|
|
return
|
|
# Keep this real-time callback minimal: copy the captured block and hand
|
|
# it to rx_audio_processing_worker. The DSP (resample, FFT, demod-buffer
|
|
# push) runs there so a long GIL hold by another thread cannot stall this
|
|
# callback and cause a sounddevice input overflow on slower hardware.
|
|
try:
|
|
self.rx_audio_in_queue.put_nowait(indata.copy())
|
|
except queue.Full:
|
|
# worker is not draining fast enough; drop this block (counted)
|
|
self.rx_audio_dropped_blocks += 1
|
|
|
|
def rx_audio_processing_worker(self) -> None:
|
|
"""Performs all RX audio DSP off the real-time input callback.
|
|
|
|
Drains rx_audio_in_queue (raw 48 kHz int16 blocks copied by
|
|
sd_input_audio_callback) and runs the resample to 8 kHz, optional
|
|
level/FFT processing and the demodulator-buffer push on a normal
|
|
worker thread, so the audio callback is never blocked by these
|
|
operations (or by the GIL while another thread holds it).
|
|
"""
|
|
while self.rx_audio_worker_running:
|
|
try:
|
|
indata = self.rx_audio_in_queue.get(timeout=0.5)
|
|
except queue.Empty:
|
|
continue
|
|
if indata is None: # shutdown sentinel
|
|
break
|
|
try:
|
|
self.process_rx_audio_block(indata)
|
|
except Exception as e:
|
|
self.log.warning("[AUDIO EXCEPTION]", e=e)
|
|
|
|
def process_rx_audio_block(self, indata) -> None:
|
|
"""Re-blocks one captured audio block and runs the DSP chain on it.
|
|
|
|
The input stream is free to hand the callback any block size, so the
|
|
captured samples are appended to rx_audio_carry_48k and the DSP chain is
|
|
run once per whole RX_DSP_BLOCK_48K available. Anything left over is
|
|
carried into the next captured block rather than being processed short:
|
|
a short block would fail codec2's "multiple of 6" resampler assertion and
|
|
silently degrade the FFT, streaming and AGC paths (see RX_DSP_BLOCK_48K).
|
|
|
|
Args:
|
|
indata (np.ndarray): One captured 48 kHz int16 block, any length.
|
|
"""
|
|
captured_48k = np.frombuffer(indata, dtype=np.int16)
|
|
self.rx_audio_carry_48k = np.concatenate((self.rx_audio_carry_48k, captured_48k))
|
|
|
|
block = self.RX_DSP_BLOCK_48K
|
|
processed = 0
|
|
while len(self.rx_audio_carry_48k) - processed >= block:
|
|
self.run_rx_audio_dsp(self.rx_audio_carry_48k[processed : processed + block])
|
|
processed += block
|
|
|
|
if processed:
|
|
# copy so the carry does not keep the whole concatenated block alive
|
|
self.rx_audio_carry_48k = self.rx_audio_carry_48k[processed:].copy()
|
|
|
|
def run_rx_audio_dsp(self, audio_48k: np.ndarray) -> None:
|
|
"""Runs the RX DSP chain on exactly one RX_DSP_BLOCK_48K of audio.
|
|
|
|
Resamples to 8 kHz, feeds the audio streaming queue, applies the optional
|
|
auto level and the configured RX gain, updates the FFT, and pushes the
|
|
result into each decoding demodulator buffer that has room for it.
|
|
|
|
Args:
|
|
audio_48k (np.ndarray): RX_DSP_BLOCK_48K 48 kHz int16 samples.
|
|
"""
|
|
audio_8k = self.resampler.resample48_to_8(audio_48k)
|
|
|
|
self.enqueue_streaming_audio_chunks(audio_8k, self.ctx.audio_rx_queue)
|
|
|
|
if self.ctx.config_manager.config["AUDIO"].get("rx_auto_audio_level"):
|
|
audio_8k = audio.normalize_audio(audio_8k)
|
|
|
|
audio_8k_level_adjusted = audio.set_audio_volume(audio_8k, self.rx_audio_level)
|
|
|
|
if not self.ctx.state_manager.isTransmitting():
|
|
audio.calculate_fft(audio_8k_level_adjusted, self.ctx.modem_fft, self.ctx.state_manager)
|
|
|
|
length_audio_8k_level_adjusted = len(audio_8k_level_adjusted)
|
|
# Avoid buffer overflow by filling only if buffer for
|
|
# selected datachannel mode is not full
|
|
index = 0
|
|
for mode in self.demodulator.MODE_DICT:
|
|
mode_data = self.demodulator.MODE_DICT[mode]
|
|
audiobuffer = mode_data["audio_buffer"]
|
|
decode = mode_data["decode"]
|
|
index += 1
|
|
if audiobuffer:
|
|
if (audiobuffer.nbuffer + length_audio_8k_level_adjusted) > audiobuffer.size:
|
|
self.demodulator.buffer_overflow_counter[index] += 1
|
|
self.ctx.event_manager.send_buffer_overflow(self.demodulator.buffer_overflow_counter)
|
|
elif decode:
|
|
audiobuffer.push(audio_8k_level_adjusted)
|