kopia lustrzana https://github.com/projecthorus/horus-gui
move to poetry package management, repoint to horusdemodlib
rodzic
adb693fb35
commit
476bf69dbb
18
README.md
18
README.md
|
@ -30,23 +30,23 @@ Written by Mark Jessop <vk5qi@rfhead.net>
|
|||
|
||||
### Build HorusLib
|
||||
|
||||
```
|
||||
$ git clone https://github.com/projecthorus/horuslib.git
|
||||
$ cd horuslib && mkdir build_linux && cd build_linux
|
||||
```console
|
||||
$ git clone https://github.com/projecthorus/horusdemodlib.git
|
||||
$ cd horusdemodlib && mkdir build && cd build
|
||||
$ cmake ..
|
||||
$ make
|
||||
$ cp src/libhorus.so ../../
|
||||
$ make install
|
||||
```
|
||||
|
||||
(Replace .so with .dylib under OSX)
|
||||
|
||||
TODO: Make a separate horuslib python package, which handles building of the library.
|
||||
TODO: Make a separate horusdemodlib python package, which handles building of the library.
|
||||
|
||||
### Create a Virtual Environment
|
||||
|
||||
Create a virtual environment and install dependencies.
|
||||
|
||||
```console
|
||||
$ git clone https://github.com/projecthorus/horus-gui.git
|
||||
$ cd horus-gui
|
||||
$ python3 -m venv venv
|
||||
$ source venv/bin/activate
|
||||
(venv) $ pip install pip -U (Optional - this updates pip)
|
||||
|
@ -64,4 +64,6 @@ entry points so it can be used like a normal install.
|
|||
```
|
||||
|
||||
### Run
|
||||
`$ python -m horusgui.gui`
|
||||
```console
|
||||
$ python -m horusgui.gui
|
||||
```
|
|
@ -0,0 +1 @@
|
|||
__version__ = "0.1.0"
|
|
@ -29,7 +29,9 @@ from .fft import *
|
|||
from .modem import *
|
||||
from .config import *
|
||||
from .habitat import *
|
||||
from .horuslib import HorusLib, Mode
|
||||
from horusdemodlib.demod import HorusLib, Mode
|
||||
from horusdemodlib.decoder import decode_packet
|
||||
from horusdemodlib.payloads import *
|
||||
from . import __version__
|
||||
|
||||
# Setup Logging
|
||||
|
@ -51,6 +53,8 @@ fft_process = None
|
|||
horus_modem = None
|
||||
habitat_uploader = None
|
||||
|
||||
decoder_init = False
|
||||
|
||||
# Global running indicator
|
||||
running = False
|
||||
|
||||
|
@ -269,11 +273,16 @@ d2_snr.addWidget(w3_snr)
|
|||
|
||||
# Telemetry Data
|
||||
w4 = pg.LayoutWidget()
|
||||
widgets["latestSentenceLabel"] = QtGui.QLabel("<b>Latest Sentence:</b>")
|
||||
widgets["latestSentenceData"] = QtGui.QLabel("NO DATA")
|
||||
widgets["latestSentenceData"].setFont(QtGui.QFont("Courier New", 18, QtGui.QFont.Bold))
|
||||
w4.addWidget(widgets["latestSentenceLabel"], 0, 0, 1, 1)
|
||||
w4.addWidget(widgets["latestSentenceData"], 0, 1, 1, 6)
|
||||
widgets["latestRawSentenceLabel"] = QtGui.QLabel("<b>Latest Packet (Raw):</b>")
|
||||
widgets["latestRawSentenceData"] = QtGui.QLabel("NO DATA")
|
||||
widgets["latestRawSentenceData"].setFont(QtGui.QFont("Courier New", 18, QtGui.QFont.Bold))
|
||||
widgets["latestDecodedSentenceLabel"] = QtGui.QLabel("<b>Latest Packet (Decoded):</b>")
|
||||
widgets["latestDecodedSentenceData"] = QtGui.QLabel("NO DATA")
|
||||
widgets["latestDecodedSentenceData"].setFont(QtGui.QFont("Courier New", 18, QtGui.QFont.Bold))
|
||||
w4.addWidget(widgets["latestRawSentenceLabel"], 0, 0, 1, 1)
|
||||
w4.addWidget(widgets["latestRawSentenceData"], 0, 1, 1, 6)
|
||||
w4.addWidget(widgets["latestDecodedSentenceLabel"], 1, 0, 1, 1)
|
||||
w4.addWidget(widgets["latestDecodedSentenceData"], 1, 1, 1, 6)
|
||||
d3.addWidget(w4)
|
||||
|
||||
w5 = pg.LayoutWidget()
|
||||
|
@ -436,15 +445,22 @@ def handle_new_packet(frame):
|
|||
else:
|
||||
_packet = frame.data
|
||||
|
||||
widgets["latestSentenceData"].setText(f"{_packet}")
|
||||
widgets["latestRawSentenceData"].setText(f"{_packet}")
|
||||
|
||||
# Immediately upload RTTY packets.
|
||||
# Why are we getting packets from the decoder twice?
|
||||
if _packet.startswith('$$$$$'):
|
||||
# TODO: Check CRC!!!
|
||||
habitat_uploader.add(_packet[3:]+'\n')
|
||||
else:
|
||||
# TODO: Handle binary packets.
|
||||
pass
|
||||
|
||||
try:
|
||||
_decoded = decode_packet(frame.data)
|
||||
widgets["latestDecodedSentenceData"].setText(_decoded['ukhas_str'])
|
||||
habitat_uploader.add(_decoded['ukhas_str']+'\n')
|
||||
except Exception as e:
|
||||
widgets["latestDecodedSentenceData"].setText("DECODE FAILED")
|
||||
logging.error(f"Decode Failed: {str(e)}")
|
||||
|
||||
|
||||
def start_decoding():
|
||||
|
@ -529,7 +545,7 @@ widgets["startDecodeButton"].clicked.connect(start_decoding)
|
|||
# GUI Update Loop
|
||||
def processQueues():
|
||||
""" Read in data from the queues, this decouples the GUI and async inputs somewhat. """
|
||||
global fft_update_queue, status_update_queue
|
||||
global fft_update_queue, status_update_queue, decoder_init
|
||||
|
||||
while fft_update_queue.qsize() > 0:
|
||||
_data = fft_update_queue.get()
|
||||
|
@ -541,6 +557,11 @@ def processQueues():
|
|||
|
||||
handle_status_update(_status)
|
||||
|
||||
if not decoder_init:
|
||||
init_payload_id_list()
|
||||
init_custom_field_list()
|
||||
decoder_init = True
|
||||
|
||||
|
||||
gui_update_timer = QtCore.QTimer()
|
||||
gui_update_timer.timeout.connect(processQueues)
|
|
@ -1,6 +1,6 @@
|
|||
# Modem Interfacing
|
||||
import logging
|
||||
from .horuslib import Mode
|
||||
from horusdemodlib.demod import Mode
|
||||
|
||||
|
||||
# Modem paramers and defaults
|
|
@ -0,0 +1,21 @@
|
|||
[tool.poetry]
|
||||
name = "horus-gui"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["Mark Jessop <vk5qi@rfhead.net>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.6"
|
||||
requests = "^2.24.0"
|
||||
crcmod = "^1.7"
|
||||
PyQt5 = "^5.13.0"
|
||||
pyqtgraph = "^0.11.0"
|
||||
pyaudio = "^0.2.11"
|
||||
ruamel.yaml = "^0.16.10"
|
||||
horusdemodlib = "^0.1.1"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry>=0.12"]
|
||||
build-backend = "poetry.masonry.api"
|
|
@ -1,8 +1,8 @@
|
|||
numpy
|
||||
pyaudio
|
||||
requests
|
||||
crcmod
|
||||
PyQt5
|
||||
pyqtgraph
|
||||
ruamel.yaml
|
||||
requests
|
||||
requests
|
||||
horusdemodlib
|
8
setup.py
8
setup.py
|
@ -4,7 +4,7 @@ from setuptools import setup, find_packages
|
|||
|
||||
regexp = re.compile(r".*__version__ = [\'\"](.*?)[\'\"]", re.S)
|
||||
|
||||
init_file = os.path.join(os.path.dirname(__file__), "src", "horusgui", "__init__.py")
|
||||
init_file = os.path.join(os.path.dirname(__file__), "horusgui", "__init__.py")
|
||||
with open(init_file, "r") as f:
|
||||
module_content = f.read()
|
||||
match = regexp.match(module_content)
|
||||
|
@ -29,13 +29,13 @@ with open("requirements.txt", "r") as f:
|
|||
if __name__ == "__main__":
|
||||
setup(
|
||||
name="horusgui",
|
||||
description="Project Horus Telemetry Decoer",
|
||||
description="Project Horus GUI Telemetry Decdoer",
|
||||
long_description=readme,
|
||||
version=version,
|
||||
install_requires=requirements,
|
||||
keywords=["horus telemetry radio"],
|
||||
package_dir={"": "src"},
|
||||
packages=find_packages("src"),
|
||||
package_dir={"": "."},
|
||||
packages=find_packages("."),
|
||||
classifiers=[
|
||||
"Intended Audience :: Developers",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
__version__ = "20.6.21"
|
|
@ -1,377 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import audioop
|
||||
import ctypes
|
||||
from ctypes import *
|
||||
import logging
|
||||
import sys
|
||||
from enum import Enum
|
||||
import os
|
||||
import logging
|
||||
|
||||
MODEM_STATS_NR_MAX = 8
|
||||
MODEM_STATS_NC_MAX = 50
|
||||
MODEM_STATS_ET_MAX = 8
|
||||
MODEM_STATS_EYE_IND_MAX = 160
|
||||
MODEM_STATS_NSPEC = 512
|
||||
MODEM_STATS_MAX_F_EST = 4
|
||||
|
||||
|
||||
class COMP(Structure):
|
||||
"""
|
||||
Used in MODEM_STATS for representing IQ.
|
||||
"""
|
||||
_fields_ = [
|
||||
("real", c_float),
|
||||
("imag", c_float)
|
||||
]
|
||||
|
||||
|
||||
class MODEM_STATS(Structure): # modem_stats.h
|
||||
"""
|
||||
Extended modem stats structure
|
||||
"""
|
||||
_fields_ = [
|
||||
("Nc", c_int),
|
||||
("snr_est", c_float),
|
||||
# rx_symbols[MODEM_STATS_NR_MAX][MODEM_STATS_NC_MAX+1];
|
||||
("rx_symbols", (COMP * MODEM_STATS_NR_MAX)*(MODEM_STATS_NC_MAX+1)),
|
||||
("nr", c_int),
|
||||
("sync", c_int),
|
||||
("foff", c_float),
|
||||
("rx_timing", c_float),
|
||||
("clock_offset", c_float),
|
||||
("sync_metric", c_float),
|
||||
# float rx_eye[MODEM_STATS_ET_MAX][MODEM_STATS_EYE_IND_MAX];
|
||||
("rx_eye", (c_float * MODEM_STATS_ET_MAX)*MODEM_STATS_EYE_IND_MAX),
|
||||
("neyetr", c_int),
|
||||
("neyesamp", c_int),
|
||||
("f_est", c_float*MODEM_STATS_MAX_F_EST),
|
||||
("fft_buf", c_float * 2*MODEM_STATS_NSPEC),
|
||||
("fft_cfg", POINTER(c_ubyte))
|
||||
]
|
||||
|
||||
|
||||
class Mode(Enum):
|
||||
"""
|
||||
Modes (and aliases for modes) for the HorusLib modem
|
||||
"""
|
||||
BINARY = 0
|
||||
BINARY_V1 = 0
|
||||
RTTY_7N2 = 90
|
||||
RTTY = 90
|
||||
BINARY_V2_256BIT = 1
|
||||
BINARY_V2_128BIT = 2
|
||||
|
||||
|
||||
class Frame():
|
||||
"""
|
||||
Frame class used for demodulation attempts.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
|
||||
data : bytes
|
||||
Demodulated data output. Empty if demodulation didn't succeed
|
||||
sync : bool
|
||||
Modem sync status
|
||||
snr : float
|
||||
Estimated SNR
|
||||
crc_pass : bool
|
||||
CRC check status
|
||||
extended_stats
|
||||
Extended modem stats. These are provided as c_types so will need to be cast prior to use. See MODEM_STATS for structure details
|
||||
"""
|
||||
|
||||
def __init__(self, data: bytes, sync: bool, crc_pass: bool, snr: float, extended_stats: MODEM_STATS):
|
||||
self.data = data
|
||||
self.sync = sync
|
||||
self.snr = snr
|
||||
self.crc_pass = crc_pass
|
||||
self.extended_stats = extended_stats
|
||||
|
||||
|
||||
class HorusLib():
|
||||
"""
|
||||
HorusLib provides a binding to horuslib to demoulate frames.
|
||||
|
||||
Example usage:
|
||||
|
||||
from horuslib import HorusLib, Mode
|
||||
with HorusLib(, mode=Mode.BINARY, verbose=False) as horus:
|
||||
with open("test.wav", "rb") as f:
|
||||
while True:
|
||||
data = f.read(horus.nin*2)
|
||||
if horus.nin != 0 and data == b'': #detect end of file
|
||||
break
|
||||
output = horus.demodulate(data)
|
||||
if output.crc_pass and output.data:
|
||||
print(f'{output.data.hex()} SNR: {output.snr}')
|
||||
for x in range(horus.mfsk):
|
||||
print(f'F{str(x)}: {float(output.extended_stats.f_est[x])}')
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
libpath=f"",
|
||||
mode=Mode.BINARY,
|
||||
rate=-1,
|
||||
tone_spacing=-1,
|
||||
stereo_iq=False,
|
||||
verbose=False,
|
||||
callback=None,
|
||||
sample_rate=48000
|
||||
):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
libpath : str
|
||||
Path to libhorus
|
||||
mode : Mode
|
||||
horuslib.Mode.BINARY, horuslib.Mode.BINARY_V2_256BIT, horuslib.Mode.BINARY_V2_128BIT, horuslib.Mode.RTTY, RTTY_7N2 = 99
|
||||
rate : int
|
||||
Changes the modem rate for supported modems. -1 for default
|
||||
tone_spacing : int
|
||||
Spacing between tones (hz) -1 for default
|
||||
stereo_iq : bool
|
||||
use stereo (IQ) input (quadrature)
|
||||
verbose : bool
|
||||
Enabled horus_set_verbose
|
||||
callback : function
|
||||
When set you can use add_samples to add any number of audio frames and callback will be called when a demodulated frame is avaliable.
|
||||
sample_rate : int
|
||||
The input sample rate of the audio input
|
||||
"""
|
||||
|
||||
if sys.platform == "darwin":
|
||||
libpath = os.path.join(libpath, "libhorus.dylib")
|
||||
elif sys.platform == "win32":
|
||||
libpath = os.path.join(libpath, "libhorus.dll")
|
||||
else:
|
||||
libpath = os.path.join(libpath, "libhorus.so")
|
||||
|
||||
# future improvement would be to try a few places / names
|
||||
self.c_lib = ctypes.cdll.LoadLibrary(libpath)
|
||||
|
||||
# horus_open_advanced
|
||||
self.c_lib.horus_open_advanced.restype = POINTER(c_ubyte)
|
||||
|
||||
# horus_nin
|
||||
self.c_lib.horus_nin.restype = c_uint32
|
||||
|
||||
# horus_get_Fs
|
||||
self.c_lib.horus_get_Fs.restype = c_int
|
||||
|
||||
# horus_set_freq_est_limits - (struct horus *hstates, float fsk_lower, float fsk_upper)
|
||||
self.c_lib.horus_set_freq_est_limits.argtype = [
|
||||
POINTER(c_ubyte),
|
||||
c_float,
|
||||
c_float,
|
||||
]
|
||||
|
||||
# horus_get_max_demod_in
|
||||
self.c_lib.horus_get_max_demod_in.restype = c_int
|
||||
|
||||
# horus_get_max_ascii_out_len
|
||||
self.c_lib.horus_get_max_ascii_out_len.restype = c_int
|
||||
|
||||
# horus_crc_ok
|
||||
self.c_lib.horus_crc_ok.restype = c_int
|
||||
|
||||
# horus_get_modem_extended_stats - (struct horus *hstates, struct MODEM_STATS *stats)
|
||||
self.c_lib.horus_get_modem_extended_stats.argtype = [
|
||||
POINTER(MODEM_STATS),
|
||||
POINTER(c_ubyte),
|
||||
]
|
||||
|
||||
# horus_get_mFSK
|
||||
self.c_lib.horus_get_mFSK.restype = c_int
|
||||
|
||||
# horus_rx
|
||||
self.c_lib.horus_rx.restype = c_int
|
||||
|
||||
if type(mode) != type(Mode(0)):
|
||||
raise ValueError("Must be of type horuslib.Mode")
|
||||
else:
|
||||
self.mode = mode
|
||||
|
||||
self.stereo_iq = stereo_iq
|
||||
|
||||
self.callback = callback
|
||||
|
||||
self.input_buffer = bytearray(b"")
|
||||
|
||||
# intial nin
|
||||
self.nin = 0
|
||||
|
||||
# try to open the modem and set the verbosity
|
||||
self.hstates = self.c_lib.horus_open_advanced(
|
||||
self.mode.value, rate, tone_spacing
|
||||
)
|
||||
self.c_lib.horus_set_verbose(self.hstates, int(verbose))
|
||||
|
||||
# check that the modem was actually opened and we don't just have a null pointer
|
||||
if bool(self.hstates):
|
||||
logging.debug("Opened Horus API")
|
||||
else:
|
||||
logging.error("Couldn't open Horus API for some reason")
|
||||
raise EnvironmentError("Couldn't open Horus API")
|
||||
|
||||
# build some class types to fit the data for demodulation using ctypes
|
||||
max_demod_in = int(self.c_lib.horus_get_max_demod_in(self.hstates))
|
||||
max_ascii_out = int(self.c_lib.horus_get_max_ascii_out_len(self.hstates))
|
||||
self.DemodIn = c_short * (max_demod_in * (1 + int(self.stereo_iq)))
|
||||
self.DataOut = c_char * max_ascii_out
|
||||
self.c_lib.horus_rx.argtype = [
|
||||
POINTER(c_ubyte),
|
||||
c_char * max_ascii_out,
|
||||
c_short * max_demod_in,
|
||||
c_int,
|
||||
]
|
||||
|
||||
self.mfsk = int(self.c_lib.horus_get_mFSK(self.hstates))
|
||||
|
||||
self.resampler_state = None
|
||||
self.audio_sample_rate = sample_rate
|
||||
self.modem_sample_rate = 48000
|
||||
|
||||
# in case someone wanted to use `with` style. I'm not sure if closing the modem does a lot.
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *a):
|
||||
self.close()
|
||||
|
||||
def close(self) -> None:
|
||||
"""
|
||||
Closes Horus modem.
|
||||
"""
|
||||
self.c_lib.horus_close(self.hstates)
|
||||
logging.debug("Shutdown horus modem")
|
||||
|
||||
def _update_nin(self) -> None:
|
||||
"""
|
||||
Updates nin. Called every time RF is demodulated and doesn't need to be run manually
|
||||
"""
|
||||
new_nin = int(self.c_lib.horus_nin(self.hstates))
|
||||
if self.nin != new_nin:
|
||||
logging.debug(f"Updated nin {new_nin}")
|
||||
self.nin = new_nin
|
||||
|
||||
def demodulate(self, demod_in: bytes) -> Frame:
|
||||
"""
|
||||
Demodulates audio in, into bytes output.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
demod_in : bytes
|
||||
16bit, signed for audio in. You'll need .nin frames in to work correctly.
|
||||
"""
|
||||
# resample to 48khz
|
||||
(demod_in, self.resampler_state) = audioop.ratecv(demod_in, 2, 1+int(self.stereo_iq), self.audio_sample_rate, self.modem_sample_rate, self.resampler_state)
|
||||
|
||||
# from_buffer_copy requires exact size so we pad it out.
|
||||
buffer = bytearray(
|
||||
len(self.DemodIn()) * sizeof(c_short)
|
||||
) # create empty byte array
|
||||
buffer[: len(demod_in)] = demod_in # copy across what we have
|
||||
|
||||
modulation = self.DemodIn # get an empty modulation array
|
||||
modulation = modulation.from_buffer_copy(
|
||||
buffer
|
||||
) # copy buffer across and get a pointer to it.
|
||||
|
||||
data_out = self.DataOut() # initilize a pointer to where bytes will be outputed
|
||||
|
||||
self.c_lib.horus_rx(self.hstates, data_out, modulation, int(self.stereo_iq))
|
||||
|
||||
stats = MODEM_STATS()
|
||||
self.c_lib.horus_get_modem_extended_stats(self.hstates, byref(stats))
|
||||
|
||||
crc = bool(self.c_lib.horus_crc_ok(self.hstates))
|
||||
|
||||
data_out = bytes(data_out)
|
||||
self._update_nin()
|
||||
|
||||
# strip the null terminator out
|
||||
data_out = data_out[:-1]
|
||||
|
||||
if data_out == bytes(len(data_out)):
|
||||
data_out = (
|
||||
b"" # check if bytes is just null and return an empty bytes instead
|
||||
)
|
||||
elif self.mode != Mode.RTTY_7N2:
|
||||
try:
|
||||
data_out = bytes.fromhex(data_out.decode("ascii"))
|
||||
except ValueError:
|
||||
logging.debug(data_out)
|
||||
logging.error("Couldn't decode the hex from the modem")
|
||||
return bytes()
|
||||
else:
|
||||
# Ascii
|
||||
data_out = data_out.decode("ascii")
|
||||
# Strip of all null characters.
|
||||
data_out = data_out.rstrip('\x00')
|
||||
|
||||
frame = Frame(
|
||||
data=data_out,
|
||||
snr=float(stats.snr_est),
|
||||
sync=bool(stats.sync),
|
||||
crc_pass=crc,
|
||||
extended_stats=stats,
|
||||
)
|
||||
return frame
|
||||
|
||||
def add_samples(self, samples: bytes):
|
||||
""" Add samples to a input buffer, to pass on to demodulate when we have nin samples """
|
||||
|
||||
# Add samples to input buffer
|
||||
self.input_buffer.extend(samples)
|
||||
|
||||
_processing = True
|
||||
_frame = None
|
||||
while _processing:
|
||||
# Process data until we have less than _nin samples.
|
||||
_nin = int(self.nin*(self.audio_sample_rate/self.modem_sample_rate))
|
||||
if len(self.input_buffer) > (_nin * 2):
|
||||
# Demodulate
|
||||
_frame = self.demodulate(self.input_buffer[:(_nin*2)])
|
||||
|
||||
# Advance sample buffer.
|
||||
self.input_buffer = self.input_buffer[(_nin*2):]
|
||||
|
||||
# If we have decoded a packet, send it on to the callback
|
||||
if len(_frame.data) > 0:
|
||||
if self.callback:
|
||||
self.callback(_frame)
|
||||
else:
|
||||
_processing = False
|
||||
|
||||
return _frame
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
if len(sys.argv) != 2:
|
||||
raise ArgumentError("Usage python3 -m horuslib filename")
|
||||
filename = sys.argv[1]
|
||||
|
||||
def frame_callback(frame):
|
||||
print(f"Callback: {frame.data.hex()} SNR: {frame.snr}")
|
||||
|
||||
# Setup Logging
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s %(levelname)s: %(message)s", level=logging.DEBUG
|
||||
)
|
||||
|
||||
with HorusLib(libpath=".", mode=Mode.BINARY, verbose=False, callback=frame_callback, sample_rate=8000) as horus:
|
||||
with open(filename, "rb") as f:
|
||||
while True:
|
||||
# Fixed read size - 2000 samples
|
||||
data = f.read(2000 * 2)
|
||||
if horus.nin != 0 and data == b"": # detect end of file
|
||||
break
|
||||
output = horus.add_samples(data)
|
||||
if output:
|
||||
print(f"Sync: {output.sync} SNR: {output.snr}")
|
Ładowanie…
Reference in New Issue