mirror of
https://github.com/xssfox/freedvtnc2
synced 2026-08-14 19:31:47 -04:00
initial
This commit is contained in:
commit
bfa7f3f6d9
16 changed files with 1478 additions and 0 deletions
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
venv
|
||||
__pycache__
|
||||
*.so
|
||||
*.o
|
||||
*.c
|
||||
.vscode
|
||||
TODO
|
||||
*.raw
|
||||
*.s16
|
||||
*.data
|
||||
.DS_Store
|
||||
10
README.md
Normal file
10
README.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
Work in progress
|
||||
|
||||
Untested instructions:
|
||||
1. Update `include_dirs` and `library_dirs` in `freedvtnc2/freedv_build.py` to point to codec2
|
||||
2. Create venv / activate
|
||||
3. cd into freedvtnc2
|
||||
3. `pip install -r requirements.txt`
|
||||
3. run `python freedv_build.py`
|
||||
4. cd back to the main project dir
|
||||
5. run `python -m freedvtnc2 --help`
|
||||
BIN
c01.raw
Normal file
BIN
c01.raw
Normal file
Binary file not shown.
0
freedvtnc2/__init__.py
Normal file
0
freedvtnc2/__init__.py
Normal file
132
freedvtnc2/__main__.py
Normal file
132
freedvtnc2/__main__.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
from .modem import FreeDVRX, FreeDVTX, Modems, Packet
|
||||
from . import audio
|
||||
from .shell import FreeDVShell
|
||||
import logging
|
||||
import configargparse
|
||||
from . import tnc
|
||||
import time
|
||||
from . import rigctl
|
||||
import readline
|
||||
import sys,struct,fcntl,termios
|
||||
|
||||
logging.basicConfig()
|
||||
|
||||
|
||||
def blank_current_readline():
|
||||
# Next line said to be reasonably portable for various Unixes
|
||||
(rows,cols) = struct.unpack('hh', fcntl.ioctl(sys.stdout, termios.TIOCGWINSZ,'1234'))
|
||||
|
||||
text_len = len(readline.get_line_buffer())+2
|
||||
|
||||
# ANSI escape sequences (All VT100 except ESC[0G)
|
||||
sys.stdout.write('\x1b[2K') # Clear current line
|
||||
sys.stdout.write('\x1b[1A\x1b[2K'*(text_len//cols)) # Move cursor up and clear line
|
||||
sys.stdout.write('\x1b[0G') # Move to start of line
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
p = configargparse.ArgParser(default_config_files=['/etc/freedvtnc2.conf', '~/.freedvtnc2.conf'])
|
||||
p.add('-c', '-config', required=False, is_config_file=True, help='config file path')
|
||||
|
||||
p.add('--no-cli', action='store_true', env_var="FREEDVTNC2_CLI")
|
||||
p.add('--list-audio-devices', action='store_true', default=False)
|
||||
|
||||
p.add('--log-level', type=str, default="INFO", env_var="FREEDVTNC2_LOG_LEVEL", choices=logging._nameToLevel.keys())
|
||||
|
||||
p.add('--input-device', type=str, default=None, env_var="FREEDVTNC2_INPUT_DEVICE")
|
||||
p.add('--output-device', type=str, default=None, env_var="FREEDVTNC2_OUTPUT_DEVICE")
|
||||
|
||||
p.add('--mode', type=str, choices=[x.name for x in Modems], default=Modems.DATAC1.name, help="The TX mode for the modem. The modem will receive all modes at once")
|
||||
|
||||
p.add('--pts', default=False, action='store_true', env_var="FREEDVTNC2_PTS", help="Disables TCP and instead creates a PTS 'fake serial' interface")
|
||||
p.add('--kiss-tcp-port', default=8001, type=int, env_var="FREEDVTNC2_KISS_TCP_PORT")
|
||||
p.add('--kiss-tcp-address', default="127.0.0.1", type=str, env_var="FREEDVTNC2_KISS_TCP_ADDRESS")
|
||||
|
||||
p.add('--rigctld-port', type=int, default=4532, env_var="FREEDVTNC2_RIGTCTLD_PORT", help="TCP port for rigctld - set to 0 to disable rigctld support")
|
||||
p.add('--rigctld-host', type=str, default="localhost", env_var="FREEDVTNC2_RIGTCTLD_HOST", help="Host for rigctld")
|
||||
|
||||
p.add('--callsign', type=str, env_var="FREEDVTNC2_CALLSIGN", help="Currently only used for chat")
|
||||
|
||||
options = p.parse_args()
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(level=options.log_level)
|
||||
logging.debug("Starting")
|
||||
|
||||
if options.list_audio_devices:
|
||||
print(
|
||||
audio.devices
|
||||
)
|
||||
else:
|
||||
modem_tx = FreeDVTX(modem={x.name:x for x in Modems}[options.mode])
|
||||
|
||||
def tx(data):
|
||||
logging.debug(f"Sending {str(data)}")
|
||||
output_device.write(modem_tx.write(data))
|
||||
|
||||
def rx(data: Packet):
|
||||
logging.debug(f"Received {str(data.header)} - {str(data.data)}")
|
||||
if data.header == 255:
|
||||
tnc_interface.tx(data.data)
|
||||
elif data.header == 254: # Chat interface
|
||||
call, message = data.data.split(b"\xff")
|
||||
# this is all hack to make the input line when receiving a message not clobber the input
|
||||
# ignoring debug messages - this is the only place where we have this issues - if we add more threaded output
|
||||
# we should move this into a dedicated function
|
||||
if not options.no_cli:
|
||||
blank_current_readline()
|
||||
print(f"<{call.decode()}> {message.decode()}")
|
||||
if readline.get_line_buffer()[-1:] == "\n": # the readline buffer doesn't get cleared on libedit - I haven't tested this on gnureadline
|
||||
sys.stdout.write(shell.prompt)
|
||||
else:
|
||||
sys.stdout.write(shell.prompt + readline.get_line_buffer())
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
print(f"\n<{call.decode()}> {message.decode()}")
|
||||
|
||||
if options.pts:
|
||||
tnc_interface = tnc.KissInterface(tx)
|
||||
else:
|
||||
tnc_interface = tnc.KissTCPInterface(tx, port=options.kiss_tcp_port, address=options.kiss_tcp_address)
|
||||
|
||||
modem_rx = FreeDVRX(callback=rx)
|
||||
|
||||
input_device_name_or_id = options.input_device
|
||||
output_device_name_or_id = options.output_device
|
||||
|
||||
try:
|
||||
input_device_name_or_id = int(input_device_name_or_id)
|
||||
output_device_name_or_id = int(output_device_name_or_id)
|
||||
except:
|
||||
pass
|
||||
|
||||
if options.rigctld_port != 0:
|
||||
rig = rigctl.Rigctld(hostname=options.rigctld_host, port=options.rigctld_port)
|
||||
ptt_trigger = rig.ptt_enable
|
||||
ptt_release = rig.ptt_disable
|
||||
else:
|
||||
ptt_trigger = None
|
||||
ptt_release = None
|
||||
|
||||
input_device = audio.InputDevice(modem_rx.write, modem_rx.sample_rate, name_or_id=input_device_name_or_id)
|
||||
output_device = audio.OutputDevice(modem_rx.sample_rate, name_or_id=output_device_name_or_id, ptt_release=ptt_release, ptt_trigger=ptt_trigger)
|
||||
|
||||
try:
|
||||
if not options.no_cli:
|
||||
if 'libedit' in readline.__doc__: # macos hack
|
||||
readline.parse_and_bind ("bind ^I rl_complete")
|
||||
|
||||
shell = FreeDVShell()
|
||||
shell.modem_rx = modem_rx
|
||||
shell.modem_tx = modem_tx
|
||||
shell.input_device = input_device
|
||||
shell.output_device = output_device
|
||||
if options.callsign:
|
||||
shell.callsign = options.callsign
|
||||
shell.cmdloop()
|
||||
else:
|
||||
while 1:
|
||||
time.sleep(0.1)
|
||||
except KeyboardInterrupt:
|
||||
input_device.close()
|
||||
output_device.close()
|
||||
250
freedvtnc2/audio.py
Normal file
250
freedvtnc2/audio.py
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
import pyaudio
|
||||
from dataclasses import dataclass
|
||||
from tabulate import tabulate
|
||||
import logging
|
||||
import audioop as pyaudioop
|
||||
import time
|
||||
from threading import Lock
|
||||
#from pydub import pyaudioop
|
||||
|
||||
p = pyaudio.PyAudio()
|
||||
|
||||
FORMAT = pyaudio.paInt16
|
||||
|
||||
|
||||
class AudioDevices:
|
||||
"""
|
||||
Gets info of all audio devices
|
||||
"""
|
||||
devices = []
|
||||
def __init__(self):
|
||||
for x in range(p.get_device_count()):
|
||||
device_info = p.get_device_info_by_index(x)
|
||||
self.devices.append(AudioDevice(
|
||||
input_channels = device_info['maxInputChannels'],
|
||||
output_channels = device_info['maxOutputChannels'],
|
||||
sample_rate = int(device_info['defaultSampleRate']),
|
||||
name = device_info['name'],
|
||||
id = x
|
||||
))
|
||||
def __str__(self):
|
||||
rows = [
|
||||
["Id","Name","In", "Out", "SampleRate"]
|
||||
]
|
||||
for device in self.devices:
|
||||
rows.append(
|
||||
[
|
||||
device.id,
|
||||
device.name,
|
||||
device.input_channels,
|
||||
device.output_channels,
|
||||
device.sample_rate
|
||||
]
|
||||
)
|
||||
return tabulate(rows, tablefmt="plain", headers="firstrow")
|
||||
|
||||
@dataclass
|
||||
class AudioDevice:
|
||||
"""
|
||||
Information about an audio device
|
||||
"""
|
||||
input_channels: int
|
||||
output_channels: int
|
||||
sample_rate: int
|
||||
name: str
|
||||
id: int
|
||||
|
||||
devices = AudioDevices()
|
||||
|
||||
default_input_device = devices.devices[p.get_default_input_device_info()['index']]
|
||||
default_output_device = devices.devices[p.get_default_output_device_info()['index']]
|
||||
|
||||
|
||||
class InputDevice():
|
||||
"""
|
||||
Handles receiving audio from an input device
|
||||
|
||||
Sample rate is the expected modem sample rate
|
||||
"""
|
||||
|
||||
rate_state = None # used for sample rate conversions
|
||||
|
||||
def __init__(self, callback, sample_rate, name_or_id=None):
|
||||
self.sample_rate = sample_rate
|
||||
self.callback = callback
|
||||
self.bit_depth = pyaudio.get_sample_size(FORMAT)
|
||||
|
||||
if name_or_id:
|
||||
try:
|
||||
self.device = next(
|
||||
device for device in devices.devices
|
||||
if device.name == name_or_id or
|
||||
device.id == name_or_id
|
||||
)
|
||||
except StopIteration:
|
||||
raise ValueError(f"Could not find audio device {name_or_id}")
|
||||
else:
|
||||
self.device = default_input_device
|
||||
|
||||
|
||||
logging.debug(f"Opening {self.device.name} for input")
|
||||
|
||||
if self.device.input_channels > 2:
|
||||
raise NotImplementedError("Inputs with greater than 2 channels not supported")
|
||||
if self.device.input_channels == 2:
|
||||
logging.warning("Stereo input detected - Only the left channel will be used")
|
||||
|
||||
if self.device.sample_rate < sample_rate:
|
||||
logging.critical(f"Input audio device sample rate {self.device.sample_rate} is less than modems sample rate {sample_rate} - this will cause problems")
|
||||
|
||||
self.stream = p.open(format=FORMAT,
|
||||
channels=self.device.input_channels,
|
||||
rate=self.device.sample_rate,
|
||||
output=False,
|
||||
input=True,
|
||||
stream_callback=self.pa_callback,
|
||||
input_device_index=self.device.id,
|
||||
frames_per_buffer=4096
|
||||
)
|
||||
|
||||
|
||||
def close(self):
|
||||
self.stream.close()
|
||||
|
||||
def __enter__(self):
|
||||
pass
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
|
||||
def pa_callback(self, in_data, frame_count, time_info, status_flag):
|
||||
|
||||
if self.device.input_channels == 2:
|
||||
in_data = pyaudioop.tomono(
|
||||
in_data,
|
||||
pyaudio.get_sample_size(FORMAT),
|
||||
1,
|
||||
0
|
||||
)
|
||||
|
||||
if self.device.sample_rate != self.sample_rate:
|
||||
(in_data, self.rate_state) = pyaudioop.ratecv(
|
||||
in_data,
|
||||
pyaudio.get_sample_size(FORMAT),
|
||||
1,
|
||||
self.device.sample_rate,
|
||||
self.sample_rate,
|
||||
self.rate_state
|
||||
)
|
||||
|
||||
self.callback(in_data)
|
||||
|
||||
return (None, pyaudio.paContinue)
|
||||
|
||||
class OutputDevice():
|
||||
"""
|
||||
Handles sending audio from an output device
|
||||
|
||||
Sample rate is the expected modem sample rate
|
||||
"""
|
||||
|
||||
rate_state = None # used for sample rate conversions
|
||||
|
||||
buffer = bytearray()
|
||||
|
||||
output_buffer_lock = Lock()
|
||||
|
||||
def __init__(self, sample_rate, name_or_id=None, ptt_trigger=None, ptt_release=None):
|
||||
self.sample_rate = sample_rate
|
||||
self.bit_depth = pyaudio.get_sample_size(FORMAT)
|
||||
|
||||
if name_or_id:
|
||||
try:
|
||||
self.device = next(
|
||||
device for device in devices.devices
|
||||
if device.name == name_or_id or
|
||||
device.id == name_or_id
|
||||
)
|
||||
except StopIteration:
|
||||
raise ValueError(f"Could not find audio device {name_or_id}")
|
||||
else:
|
||||
self.device = default_output_device
|
||||
|
||||
|
||||
logging.debug(f"Opening {self.device.name} for output")
|
||||
|
||||
if self.device.input_channels > 2:
|
||||
raise NotImplementedError("Inputs with greater than 2 channels not supported")
|
||||
|
||||
if self.device.sample_rate < sample_rate:
|
||||
logging.critical(f"Output audio device sample rate {self.device.sample_rate} is less than modems sample rate {sample_rate} - this will cause problems")
|
||||
|
||||
self.stream = p.open(format=FORMAT,
|
||||
channels=self.device.output_channels,
|
||||
rate=self.device.sample_rate,
|
||||
output=True,
|
||||
input=False,
|
||||
output_device_index=self.device.id,
|
||||
stream_callback=self.pa_callback,
|
||||
frames_per_buffer=4096
|
||||
)
|
||||
logging.debug("started output debug")
|
||||
|
||||
self.ptt_trigger = ptt_trigger
|
||||
self.ptt_release = ptt_release
|
||||
self.ptt = False
|
||||
|
||||
|
||||
def write(self, data: bytes):
|
||||
if self.device.sample_rate != self.sample_rate:
|
||||
(data, self.rate_state) = pyaudioop.ratecv(
|
||||
data,
|
||||
pyaudio.get_sample_size(FORMAT),
|
||||
1,
|
||||
self.sample_rate,
|
||||
self.device.sample_rate,
|
||||
self.rate_state,
|
||||
)
|
||||
|
||||
if self.device.output_channels == 2:
|
||||
data = pyaudioop.tostereo(
|
||||
data,
|
||||
pyaudio.get_sample_size(FORMAT),
|
||||
1,
|
||||
1
|
||||
)
|
||||
with self.output_buffer_lock:
|
||||
self.buffer += data
|
||||
logging.debug("wrote to output buffer")
|
||||
|
||||
def pa_callback(self, in_data, frame_count, time_info, status):
|
||||
buffer_size = frame_count * pyaudio.get_sample_size(FORMAT) * self.device.output_channels
|
||||
|
||||
output = bytearray(buffer_size)
|
||||
|
||||
ptt = False
|
||||
|
||||
with self.output_buffer_lock:
|
||||
chunk_size = min(len(self.buffer), buffer_size)
|
||||
output[:chunk_size] = self.buffer[:chunk_size]
|
||||
if self.buffer:
|
||||
ptt = True
|
||||
del self.buffer[:chunk_size]
|
||||
|
||||
if self.ptt != ptt:
|
||||
if ptt and self.ptt_trigger:
|
||||
logging.debug("Triggering PTT")
|
||||
self.ptt_trigger()
|
||||
elif ptt == False and self.ptt_release:
|
||||
logging.debug("Releasing PTT")
|
||||
self.ptt_release()
|
||||
self.ptt = ptt
|
||||
|
||||
return (bytes(output), pyaudio.paContinue)
|
||||
|
||||
def clear(self):
|
||||
with self.output_buffer_lock:
|
||||
self.buffer = bytearray()
|
||||
return
|
||||
def close(self):
|
||||
self.stream.close()
|
||||
266
freedvtnc2/freedv_build.py
Normal file
266
freedvtnc2/freedv_build.py
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
from cffi import FFI
|
||||
ffibuilder = FFI()
|
||||
|
||||
# cdef() expects a single string declaring the C types, functions and
|
||||
# globals needed to use the shared object. It must be in valid C syntax.
|
||||
ffibuilder.cdef("""
|
||||
|
||||
typedef struct {
|
||||
float real;
|
||||
float imag;
|
||||
} COMP;
|
||||
|
||||
|
||||
|
||||
// available speech modes
|
||||
#define FREEDV_MODE_1600 0
|
||||
#define FREEDV_MODE_2400A 3
|
||||
#define FREEDV_MODE_2400B 4
|
||||
#define FREEDV_MODE_800XA 5
|
||||
#define FREEDV_MODE_700C 6
|
||||
#define FREEDV_MODE_700D 7
|
||||
#define FREEDV_MODE_700E 13
|
||||
#define FREEDV_MODE_2020 8
|
||||
#define FREEDV_MODE_2020B 16
|
||||
|
||||
// available data modes
|
||||
#define FREEDV_MODE_FSK_LDPC 9
|
||||
#define FREEDV_MODE_DATAC1 10
|
||||
#define FREEDV_MODE_DATAC3 12
|
||||
#define FREEDV_MODE_DATAC0 14
|
||||
#define FREEDV_MODE_DATAC4 18
|
||||
#define FREEDV_MODE_DATAC13 19
|
||||
|
||||
// Sample rates used
|
||||
#define FREEDV_FS_8000 8000
|
||||
#define FREEDV_FS_16000 16000
|
||||
|
||||
// peak (complex) sample value from Tx modulator
|
||||
#define FREEDV_PEAK 16384
|
||||
|
||||
// Return code flags for freedv_get_rx_status() function
|
||||
#define FREEDV_RX_TRIAL_SYNC 0x1 // demodulator has trial sync
|
||||
#define FREEDV_RX_SYNC 0x2 // demodulator has sync
|
||||
#define FREEDV_RX_BITS 0x4 // data bits have been returned
|
||||
#define FREEDV_RX_BIT_ERRORS \
|
||||
0x8 // FEC may not have corrected all bit errors (not all parity checks OK)
|
||||
|
||||
// optional operator control of OFDM modem state machine
|
||||
#define FREEDV_SYNC_UNSYNC \
|
||||
0 // force sync state machine to lose sync, and search for new sync
|
||||
#define FREEDV_SYNC_AUTO 1 // falls out of sync automatically
|
||||
#define FREEDV_SYNC_MANUAL 2 // fall out of sync only under operator control
|
||||
|
||||
#define FREEDV_VARICODE_MAX_BITS 12 // max bits for each ASCII character
|
||||
|
||||
// These macros allow us to disable unwanted modes at compile tine, for example
|
||||
// to save memory on embedded systems or the remove need to link other
|
||||
// libraries. By default we enable all modes. Disable during compile time e.g
|
||||
// -DFREEDV_MODE_1600_EN=0 will enable all but FreeDV 1600. Or the other way
|
||||
// round -DFREEDV_MODE_EN_DEFAULT=0 -DFREEDV_MODE_1600_EN=1 will enable only
|
||||
// FreeDV 1600
|
||||
|
||||
|
||||
|
||||
// struct that hold state information for one freedv instance
|
||||
struct freedv;
|
||||
|
||||
// Some modes allow extra configuration parameters
|
||||
struct freedv_advanced {
|
||||
int interleave_frames; // now unused but remains to prevent breaking API for
|
||||
// legacy apps
|
||||
|
||||
// parameters for FREEDV_MODE_FSK_LDPC
|
||||
int M; // 2 or 4 FSK
|
||||
int Rs; // Symbol rate Hz
|
||||
int Fs; // Sample rate Hz
|
||||
int first_tone; // Freq of first tone Hz
|
||||
int tone_spacing; // Spacing between tones Hz
|
||||
char *codename; // LDPC codename, from codes listed in ldpc_codes.c
|
||||
};
|
||||
|
||||
// Called when text message char is decoded
|
||||
typedef void (*freedv_callback_rx)(void *, char);
|
||||
// Called when new text message char is needed
|
||||
typedef char (*freedv_callback_tx)(void *);
|
||||
typedef void (*freedv_calback_error_pattern)(void *error_pattern_callback_state,
|
||||
short error_pattern[],
|
||||
int sz_error_pattern);
|
||||
|
||||
// Protocol bits are packed MSB-first
|
||||
// Called when a frame containing protocol data is decoded
|
||||
typedef void (*freedv_callback_protorx)(void *, char *);
|
||||
// Called when a frame containing protocol data is to be sent
|
||||
typedef void (*freedv_callback_prototx)(void *, char *);
|
||||
|
||||
// Data packet callbacks
|
||||
// Called when a packet has been received
|
||||
typedef void (*freedv_callback_datarx)(void *, unsigned char *packet,
|
||||
size_t size);
|
||||
// Called when a new packet can be send
|
||||
typedef void (*freedv_callback_datatx)(void *, unsigned char *packet,
|
||||
size_t *size);
|
||||
|
||||
/*---------------------------------------------------------------------------*\
|
||||
|
||||
FreeDV API functions
|
||||
|
||||
\*---------------------------------------------------------------------------*/
|
||||
|
||||
// open, close ----------------------------------------------------------------
|
||||
|
||||
struct freedv *freedv_open_advanced(int mode, struct freedv_advanced *adv);
|
||||
struct freedv *freedv_open(int mode);
|
||||
void freedv_close(struct freedv *freedv);
|
||||
|
||||
// Transmit -------------------------------------------------------------------
|
||||
|
||||
void freedv_tx(struct freedv *freedv, short mod_out[], short speech_in[]);
|
||||
void freedv_comptx(struct freedv *freedv, COMP mod_out[], short speech_in[]);
|
||||
void freedv_datatx(struct freedv *f, short mod_out[]);
|
||||
int freedv_data_ntxframes(struct freedv *freedv);
|
||||
void freedv_rawdatatx(struct freedv *f, short mod_out[],
|
||||
unsigned char *packed_payload_bits);
|
||||
void freedv_rawdatacomptx(struct freedv *f, COMP mod_out[],
|
||||
unsigned char *packed_payload_bits);
|
||||
int freedv_rawdatapreambletx(struct freedv *f, short mod_out[]);
|
||||
int freedv_rawdatapreamblecomptx(struct freedv *f, COMP mod_out[]);
|
||||
int freedv_rawdatapostambletx(struct freedv *f, short mod_out[]);
|
||||
int freedv_rawdatapostamblecomptx(struct freedv *f, COMP mod_out[]);
|
||||
|
||||
// Receive -------------------------------------------------------------------
|
||||
|
||||
int freedv_nin(struct freedv *freedv);
|
||||
int freedv_rx(struct freedv *freedv, short speech_out[], short demod_in[]);
|
||||
int freedv_shortrx(struct freedv *freedv, short speech_out[], short demod_in[],
|
||||
float gain);
|
||||
int freedv_floatrx(struct freedv *freedv, short speech_out[], float demod_in[]);
|
||||
int freedv_comprx(struct freedv *freedv, short speech_out[], COMP demod_in[]);
|
||||
int freedv_rawdatarx(struct freedv *freedv, unsigned char *packed_payload_bits,
|
||||
short demod_in[]);
|
||||
int freedv_rawdatacomprx(struct freedv *freedv,
|
||||
unsigned char *packed_payload_bits, COMP demod_in[]);
|
||||
|
||||
// Helper functions
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
int freedv_codec_frames_from_rawdata(struct freedv *freedv,
|
||||
unsigned char *codec_frames,
|
||||
unsigned char *rawdata);
|
||||
int freedv_rawdata_from_codec_frames(struct freedv *freedv,
|
||||
unsigned char *rawdata,
|
||||
unsigned char *codec_frames);
|
||||
unsigned short freedv_gen_crc16(unsigned char *bytes, int nbytes);
|
||||
void freedv_pack(unsigned char *bytes, unsigned char *bits, int nbits);
|
||||
void freedv_unpack(unsigned char *bits, unsigned char *bytes, int nbits);
|
||||
unsigned short freedv_crc16_unpacked(unsigned char *bits, int nbits);
|
||||
int freedv_check_crc16_unpacked(unsigned char *unpacked_bits, int nbits);
|
||||
|
||||
// Set parameters ------------------------------------------------------------
|
||||
|
||||
void freedv_set_callback_txt(struct freedv *freedv, freedv_callback_rx rx,
|
||||
freedv_callback_tx tx, void *callback_state);
|
||||
void freedv_set_callback_protocol(struct freedv *freedv,
|
||||
freedv_callback_protorx rx,
|
||||
freedv_callback_prototx tx,
|
||||
void *callback_state);
|
||||
void freedv_set_callback_data(struct freedv *freedv,
|
||||
freedv_callback_datarx datarx,
|
||||
freedv_callback_datatx datatx,
|
||||
void *callback_state);
|
||||
void freedv_set_test_frames(struct freedv *freedv, int test_frames);
|
||||
void freedv_set_test_frames_diversity(struct freedv *freedv,
|
||||
int test_frames_diversity);
|
||||
void freedv_set_squelch_en(struct freedv *freedv, bool squelch_en);
|
||||
void freedv_set_snr_squelch_thresh(struct freedv *freedv,
|
||||
float snr_squelch_thresh);
|
||||
void freedv_set_clip(struct freedv *freedv, bool val);
|
||||
void freedv_set_total_bit_errors(struct freedv *freedv, int val);
|
||||
void freedv_set_total_bits(struct freedv *freedv, int val);
|
||||
void freedv_set_total_bit_errors_coded(struct freedv *freedv, int val);
|
||||
void freedv_set_total_bits_coded(struct freedv *freedv, int val);
|
||||
void freedv_set_total_packets(struct freedv *freedv, int val);
|
||||
void freedv_set_total_packet_errors(struct freedv *freedv, int val);
|
||||
void freedv_set_callback_error_pattern(struct freedv *freedv,
|
||||
freedv_calback_error_pattern cb,
|
||||
void *state);
|
||||
void freedv_set_varicode_code_num(struct freedv *freedv, int val);
|
||||
void freedv_set_data_header(struct freedv *freedv, unsigned char *header);
|
||||
void freedv_set_carrier_ampl(struct freedv *freedv, int c, float ampl);
|
||||
void freedv_set_sync(struct freedv *freedv, int sync_cmd);
|
||||
void freedv_set_verbose(struct freedv *freedv, int verbosity);
|
||||
void freedv_set_tx_bpf(struct freedv *freedv, int val);
|
||||
void freedv_set_tx_amp(struct freedv *freedv, float amp);
|
||||
void freedv_set_ext_vco(struct freedv *f, int val);
|
||||
void freedv_set_phase_est_bandwidth_mode(struct freedv *f, int val);
|
||||
void freedv_set_eq(struct freedv *f, bool val);
|
||||
void freedv_set_frames_per_burst(struct freedv *f, int framesperburst);
|
||||
void freedv_passthrough_gain(struct freedv *f, float g);
|
||||
int freedv_set_tuning_range(struct freedv *freedv, float val_fmin,
|
||||
float val_fmax);
|
||||
|
||||
// Get parameters
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
struct MODEM_STATS;
|
||||
|
||||
int freedv_get_version(void);
|
||||
char *freedv_get_hash(void);
|
||||
int freedv_get_mode(struct freedv *freedv);
|
||||
void freedv_get_modem_stats(struct freedv *freedv, int *sync, float *snr_est);
|
||||
void freedv_get_modem_extended_stats(struct freedv *freedv,
|
||||
struct MODEM_STATS *stats);
|
||||
int freedv_get_test_frames(struct freedv *freedv);
|
||||
|
||||
int freedv_get_speech_sample_rate(struct freedv *freedv);
|
||||
int freedv_get_n_speech_samples(struct freedv *freedv);
|
||||
int freedv_get_n_max_speech_samples(struct freedv *freedv);
|
||||
|
||||
int freedv_get_modem_sample_rate(struct freedv *freedv);
|
||||
int freedv_get_modem_symbol_rate(struct freedv *freedv);
|
||||
int freedv_get_n_max_modem_samples(struct freedv *freedv);
|
||||
int freedv_get_n_nom_modem_samples(struct freedv *freedv);
|
||||
int freedv_get_n_tx_modem_samples(struct freedv *freedv);
|
||||
int freedv_get_n_tx_preamble_modem_samples(struct freedv *freedv);
|
||||
int freedv_get_n_tx_postamble_modem_samples(struct freedv *freedv);
|
||||
|
||||
// bit error rate stats
|
||||
int freedv_get_total_bits(struct freedv *freedv);
|
||||
int freedv_get_total_bit_errors(struct freedv *freedv);
|
||||
int freedv_get_total_bits_coded(struct freedv *freedv);
|
||||
int freedv_get_total_bit_errors_coded(struct freedv *freedv);
|
||||
int freedv_get_total_packets(struct freedv *freedv);
|
||||
int freedv_get_total_packet_errors(struct freedv *freedv);
|
||||
|
||||
int freedv_get_rx_status(struct freedv *freedv);
|
||||
void freedv_get_fsk_S_and_N(struct freedv *freedv, float *S, float *N);
|
||||
|
||||
int freedv_get_sync(struct freedv *freedv);
|
||||
int freedv_get_sync_interleaver(struct freedv *freedv);
|
||||
|
||||
// access to speech codec states
|
||||
struct FSK *freedv_get_fsk(struct freedv *f);
|
||||
struct CODEC2 *freedv_get_codec2(struct freedv *freedv);
|
||||
|
||||
int freedv_get_bits_per_codec_frame(struct freedv *freedv);
|
||||
int freedv_get_bits_per_modem_frame(struct freedv *freedv);
|
||||
int freedv_get_sz_error_pattern(struct freedv *freedv);
|
||||
int freedv_get_protocol_bits(struct freedv *freedv);
|
||||
|
||||
""")
|
||||
|
||||
# set_source() gives the name of the python extension module to
|
||||
# produce, and some C source code as a string. This C code needs
|
||||
# to make the declarated functions, types and globals available,
|
||||
# so it is often just the "#include".
|
||||
ffibuilder.set_source("_freedv_cffi",
|
||||
"""
|
||||
#include "freedv_api.h" // the C header of the library
|
||||
""",
|
||||
libraries=['codec2'],
|
||||
include_dirs = [ "/Users/mwheeler/src/codec2/src/"],
|
||||
library_dirs = ["/Users/mwheeler/src/codec2/build_linux/src/"]
|
||||
) # library name, for the linker
|
||||
|
||||
if __name__ == "__main__":
|
||||
ffibuilder.compile(verbose=True)
|
||||
270
freedvtnc2/modem.py
Normal file
270
freedvtnc2/modem.py
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
from ._freedv_cffi import ffi, lib
|
||||
|
||||
from typing import Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import logging
|
||||
|
||||
class Modems(Enum):
|
||||
"""
|
||||
Supported modems and friendly names for them
|
||||
"""
|
||||
DATAC1 = lib.FREEDV_MODE_DATAC1
|
||||
DATAC3 = lib.FREEDV_MODE_DATAC3
|
||||
DATAC4 = lib.FREEDV_MODE_DATAC4
|
||||
|
||||
@dataclass
|
||||
class FreeDVFrame:
|
||||
"""
|
||||
Receive data from the FreeDV modem with meta data
|
||||
"""
|
||||
data: bytes
|
||||
sync: int
|
||||
snr: float
|
||||
modem: Modems
|
||||
|
||||
|
||||
|
||||
class Modem():
|
||||
def __init__(self, modem: Modems, callback: Callable[[FreeDVFrame],None]|None=None):
|
||||
self.modem = lib.freedv_open(modem.value)
|
||||
self.modem_name = modem.name
|
||||
self.buffer = bytearray()
|
||||
self.callback = callback
|
||||
|
||||
lib.freedv_set_frames_per_burst(self.modem, 1)
|
||||
|
||||
@property
|
||||
def nin(self) -> int:
|
||||
"""
|
||||
Number of bytes that the modem is expecting to process (freedv api is number of shorts - we use bytes to make things easier)
|
||||
"""
|
||||
return lib.freedv_nin(self.modem) * ffi.sizeof("short")
|
||||
|
||||
@property
|
||||
def bytes_per_frame(self) -> int:
|
||||
"""
|
||||
Max number of bytes returned for each frame of audio sent. Used to build buffers.
|
||||
"""
|
||||
return lib.freedv_get_bits_per_modem_frame(self.modem)//8
|
||||
|
||||
@property
|
||||
def snr(self) -> float:
|
||||
"""
|
||||
Receivers SNR reported by the modem
|
||||
"""
|
||||
sync = ffi.new("int *")
|
||||
snr = ffi.new("float *")
|
||||
|
||||
lib.freedv_get_modem_stats(self.modem,sync,snr)
|
||||
|
||||
return snr[0]
|
||||
|
||||
@property
|
||||
def sync(self) -> float:
|
||||
"""
|
||||
Modems sync status.
|
||||
"""
|
||||
sync = ffi.new("int *")
|
||||
snr = ffi.new("float *")
|
||||
|
||||
lib.freedv_get_modem_stats(self.modem,sync,snr)
|
||||
|
||||
return sync[0]
|
||||
|
||||
@property
|
||||
def sample_rate(self) -> int:
|
||||
"""
|
||||
Sample rate the modem is running at
|
||||
"""
|
||||
return lib.freedv_get_modem_sample_rate(self.modem)
|
||||
|
||||
def write(self, data: bytes) -> None:
|
||||
"""
|
||||
Feed in audio bytes.
|
||||
"""
|
||||
# add data to our internal buffer
|
||||
self.buffer += data
|
||||
|
||||
# if we have enough data run the demodulator
|
||||
while (nin := self.nin) <= len(self.buffer):
|
||||
# setup the memory location where audio samples will be read from
|
||||
to_modem = ffi.from_buffer("short[]", self.buffer[:nin] )
|
||||
# remove the loaded samples from the buffer
|
||||
del self.buffer[:nin]
|
||||
# setup a location to put the results
|
||||
from_modem = ffi.new("unsigned char packed_payload_bits[]", bytes(self.bytes_per_frame))
|
||||
|
||||
# run the demodulator
|
||||
bytes_returned = lib.freedv_rawdatarx(self.modem,from_modem,to_modem)
|
||||
|
||||
# check if we get returned bytes
|
||||
if bytes_returned and self.callback:
|
||||
# if we do, create a freedvframe object and return the data
|
||||
self.callback( # we should change this to do depacketization
|
||||
FreeDVFrame(
|
||||
data = bytes(from_modem)[:bytes_returned-2], # Remove the CRC
|
||||
sync = self.sync,
|
||||
snr = self.snr,
|
||||
modem = self.modem_name
|
||||
)
|
||||
)
|
||||
def crc(self, data: bytes) -> bytes:
|
||||
data_in = ffi.from_buffer(f"unsigned char[{self.bytes_per_frame - 2}]", data)
|
||||
return lib.freedv_gen_crc16(data_in, self.bytes_per_frame - 2).to_bytes(2, byteorder="big")
|
||||
|
||||
def modulate(self, data: bytes, header_byte=b'\xff') -> bytes:
|
||||
"""
|
||||
Modulates bytes into audio samples (also bytes)
|
||||
"""
|
||||
|
||||
"""
|
||||
Our packet format is. Packet must be less than 32768
|
||||
|
||||
## TODO build tests for packets exactly the max length
|
||||
|
||||
Short packet
|
||||
0xff [2 byte short for length of data in bytes] [data]
|
||||
|
||||
Long packet
|
||||
[sequence number 0-127] [data]
|
||||
"""
|
||||
|
||||
# Convert to byte array as it will be easier to slice
|
||||
data = bytearray(data)
|
||||
chunks = []
|
||||
|
||||
pop_packet_length = self.bytes_per_frame - 2 - 3 # first iteration we use 3 bytes for the header
|
||||
while data:
|
||||
chunks.append(data[:pop_packet_length])
|
||||
del data[:pop_packet_length]
|
||||
pop_packet_length = self.bytes_per_frame - 2 - 1 # next iterations only use 1 byte for sequence
|
||||
|
||||
|
||||
frames = []
|
||||
|
||||
# first frame includes header
|
||||
frame=bytearray(self.bytes_per_frame)
|
||||
|
||||
# header
|
||||
frame[0:3] = header_byte + sum([len(x) for x in chunks]).to_bytes(2)
|
||||
|
||||
# data
|
||||
frame[3:3+len(chunks[0])] = chunks[0]
|
||||
|
||||
# crc
|
||||
frame[-2:] = self.crc(bytes(frame)[:-2])
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
for seq, next_chunk in enumerate(chunks[1:]):
|
||||
frame=bytearray(self.bytes_per_frame)
|
||||
# header
|
||||
frame[0] = seq
|
||||
|
||||
frame[1:1+len(next_chunk)] = next_chunk
|
||||
|
||||
# crc
|
||||
frame[-2:] = self.crc(bytes(frame)[:-2])
|
||||
|
||||
frames.append(frame)
|
||||
|
||||
output = bytes()
|
||||
for frame in frames:
|
||||
|
||||
from_modem = ffi.new(f"short mod_out[{lib.freedv_get_n_tx_modem_samples(self.modem)}]")
|
||||
|
||||
# preamble
|
||||
samples = lib.freedv_rawdatapreambletx(self.modem, from_modem)
|
||||
output += ffi.buffer(from_modem)[:(samples*ffi.sizeof("short"))]
|
||||
|
||||
to_modem = ffi.from_buffer("unsigned char *", frame)
|
||||
|
||||
# setup a location to put the results
|
||||
lib.freedv_rawdatatx(self.modem, from_modem, to_modem)
|
||||
output += ffi.buffer(from_modem)[:]
|
||||
|
||||
#postamble
|
||||
samples=lib.freedv_rawdatapostambletx(self.modem, from_modem)
|
||||
output += ffi.buffer(from_modem)[:(samples*ffi.sizeof("short"))]
|
||||
|
||||
|
||||
# add an extra bit of silence to clear out buffers
|
||||
output += bytes(lib.freedv_get_n_nom_modem_samples(self.modem)*ffi.sizeof("short")*2)
|
||||
|
||||
return output
|
||||
|
||||
@dataclass
|
||||
class Packet():
|
||||
data: bytes
|
||||
header: int
|
||||
|
||||
class FreeDVRX():
|
||||
def __init__(self, callback: Callable[[bytes],None]):
|
||||
self.callback = callback
|
||||
|
||||
# we RX all the modems at once
|
||||
self.modems = [Modem(x, callback=self.rx) for x in Modems]
|
||||
|
||||
# set sample rate so that the audio processor can perform the required sampling conversion
|
||||
if len(set([x.sample_rate for x in self.modems])) != 1:
|
||||
raise NotImplemented("Not all modems are running the same sample rate - We can't handle this right now")
|
||||
else:
|
||||
self.sample_rate = self.modems[0].sample_rate
|
||||
|
||||
# data for packet rx
|
||||
self.remaining_bytes = None
|
||||
self.next_seq_number = None
|
||||
self.partial_data = None
|
||||
|
||||
def write(self, data: bytes) -> None:
|
||||
"""
|
||||
Accepts bytes of data that will be read by tge modem and demodulated
|
||||
"""
|
||||
for modem in self.modems:
|
||||
modem.write(data)
|
||||
def rx(self, data_frame: FreeDVFrame):
|
||||
logging.debug(f"Received data. snr:{data_frame.snr}")
|
||||
data = bytearray(data_frame.data)
|
||||
header = data.pop(0)
|
||||
if header > 200: # start of packet
|
||||
self.remaining_bytes = int.from_bytes(data[0:2])
|
||||
del data[0:2]
|
||||
logging.debug(f"Found packet start - Expecting {self.remaining_bytes} bytes")
|
||||
self.next_seq_number = 0
|
||||
self.partial_data=b''
|
||||
self.header = header
|
||||
elif self.next_seq_number != None: # should be a seq number
|
||||
if self.next_seq_number != header:
|
||||
logging.debug(f"Missing data - header seq expected {self.next_seq_number}, got {header}")
|
||||
self.next_seq_number = None
|
||||
self.remaining_bytes = None
|
||||
return
|
||||
else:
|
||||
logging.debug(f"Received frame {header}")
|
||||
self.next_seq_number += 1
|
||||
|
||||
else:
|
||||
logging.debug(f"Not expecting data - got {header}")
|
||||
self.next_seq_number = None
|
||||
self.remaining_bytes = None
|
||||
return
|
||||
self.partial_data += data[:self.remaining_bytes]
|
||||
self.remaining_bytes -= len(data[:self.remaining_bytes])
|
||||
|
||||
logging.debug(f"Seq: {header} Remaining data: {self.remaining_bytes}")
|
||||
|
||||
if self.remaining_bytes == 0:
|
||||
self.next_seq_number = None
|
||||
self.remaining_bytes = None
|
||||
self.callback(Packet(header=self.header, data=self.partial_data))
|
||||
def set_mode(self, modem: Modem):
|
||||
self.modem = Modem(modem=modem)
|
||||
|
||||
class FreeDVTX():
|
||||
def __init__(self, modem: Modem = Modems.DATAC1):
|
||||
self.modem = Modem(modem=modem)
|
||||
def set_mode(self, modem: Modem):
|
||||
self.modem = Modem(modem=modem)
|
||||
def write(self, data: bytes, header_byte=b'\xff'):
|
||||
return self.modem.modulate(data, header_byte)
|
||||
172
freedvtnc2/poetry.lock
generated
Normal file
172
freedvtnc2/poetry.lock
generated
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.16.0"
|
||||
description = "Foreign Function Interface for Python calling C code."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"},
|
||||
{file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"},
|
||||
{file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"},
|
||||
{file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"},
|
||||
{file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"},
|
||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"},
|
||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"},
|
||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"},
|
||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"},
|
||||
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"},
|
||||
{file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"},
|
||||
{file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"},
|
||||
{file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"},
|
||||
{file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pycparser = "*"
|
||||
|
||||
[[package]]
|
||||
name = "configargparse"
|
||||
version = "1.7"
|
||||
description = "A drop-in replacement for argparse that allows options to also be set via config files and/or environment variables."
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "ConfigArgParse-1.7-py3-none-any.whl", hash = "sha256:d249da6591465c6c26df64a9f73d2536e743be2f244eb3ebe61114af2f94f86b"},
|
||||
{file = "ConfigArgParse-1.7.tar.gz", hash = "sha256:e7067471884de5478c58a511e529f0f9bd1c66bfef1dea90935438d6c23306d1"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
test = ["PyYAML", "mock", "pytest"]
|
||||
yaml = ["PyYAML"]
|
||||
|
||||
[[package]]
|
||||
name = "kissfix"
|
||||
version = "7.0.11"
|
||||
description = "Python KISS Module."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "kissfix-7.0.11-py3-none-any.whl", hash = "sha256:aa2a90d19549eba1ad08adb83cca7b9d147ec805918c208cfbb85dc834e2eeb8"},
|
||||
{file = "kissfix-7.0.11.tar.gz", hash = "sha256:6d17a27c152a06be0329c701c2f32e1f5bdbc2c1a8361ae1af4c92e4d406baa8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pyserial = ">=3.4"
|
||||
|
||||
[[package]]
|
||||
name = "pyaudio"
|
||||
version = "0.2.14"
|
||||
description = "Cross-platform audio I/O with PortAudio"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "PyAudio-0.2.14-cp310-cp310-win32.whl", hash = "sha256:126065b5e82a1c03ba16e7c0404d8f54e17368836e7d2d92427358ad44fefe61"},
|
||||
{file = "PyAudio-0.2.14-cp310-cp310-win_amd64.whl", hash = "sha256:2a166fc88d435a2779810dd2678354adc33499e9d4d7f937f28b20cc55893e83"},
|
||||
{file = "PyAudio-0.2.14-cp311-cp311-win32.whl", hash = "sha256:506b32a595f8693811682ab4b127602d404df7dfc453b499c91a80d0f7bad289"},
|
||||
{file = "PyAudio-0.2.14-cp311-cp311-win_amd64.whl", hash = "sha256:bbeb01d36a2f472ae5ee5e1451cacc42112986abe622f735bb870a5db77cf903"},
|
||||
{file = "PyAudio-0.2.14-cp312-cp312-win32.whl", hash = "sha256:5fce4bcdd2e0e8c063d835dbe2860dac46437506af509353c7f8114d4bacbd5b"},
|
||||
{file = "PyAudio-0.2.14-cp312-cp312-win_amd64.whl", hash = "sha256:12f2f1ba04e06ff95d80700a78967897a489c05e093e3bffa05a84ed9c0a7fa3"},
|
||||
{file = "PyAudio-0.2.14-cp38-cp38-win32.whl", hash = "sha256:858caf35b05c26d8fc62f1efa2e8f53d5fa1a01164842bd622f70ddc41f55000"},
|
||||
{file = "PyAudio-0.2.14-cp38-cp38-win_amd64.whl", hash = "sha256:2dac0d6d675fe7e181ba88f2de88d321059b69abd52e3f4934a8878e03a7a074"},
|
||||
{file = "PyAudio-0.2.14-cp39-cp39-win32.whl", hash = "sha256:f745109634a7c19fa4d6b8b7d6967c3123d988c9ade0cd35d4295ee1acdb53e9"},
|
||||
{file = "PyAudio-0.2.14-cp39-cp39-win_amd64.whl", hash = "sha256:009f357ee5aa6bc8eb19d69921cd30e98c42cddd34210615d592a71d09c4bd57"},
|
||||
{file = "PyAudio-0.2.14.tar.gz", hash = "sha256:78dfff3879b4994d1f4fc6485646a57755c6ee3c19647a491f790a0895bd2f87"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
test = ["numpy"]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.21"
|
||||
description = "C parser in Python"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
files = [
|
||||
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
|
||||
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydub"
|
||||
version = "0.25.1"
|
||||
description = "Manipulate audio with an simple and easy high level interface"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6"},
|
||||
{file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyserial"
|
||||
version = "3.5"
|
||||
description = "Python Serial Port Extension"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"},
|
||||
{file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
cp2110 = ["hidapi"]
|
||||
|
||||
[[package]]
|
||||
name = "tabulate"
|
||||
version = "0.9.0"
|
||||
description = "Pretty-print tabular data"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"},
|
||||
{file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
widechars = ["wcwidth"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "0c76f5a37399d9d07cc95a4fee91abffff6ea8efbd181335dc1e685678534acb"
|
||||
20
freedvtnc2/pyproject.toml
Normal file
20
freedvtnc2/pyproject.toml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[tool.poetry]
|
||||
name = "freedvtnc2"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["xssfox <xss@sprocketfox.io>"]
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
cffi = "^1.16.0"
|
||||
configargparse = "^1.7"
|
||||
pyaudio = "^0.2.14"
|
||||
tabulate = "^0.9.0"
|
||||
pydub = "^0.25.1"
|
||||
kissfix = "^7.0.11"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
83
freedvtnc2/requirements.txt
Normal file
83
freedvtnc2/requirements.txt
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
cffi==1.16.0 ; python_version >= "3.11" and python_version < "4.0" \
|
||||
--hash=sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc \
|
||||
--hash=sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a \
|
||||
--hash=sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417 \
|
||||
--hash=sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab \
|
||||
--hash=sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520 \
|
||||
--hash=sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36 \
|
||||
--hash=sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743 \
|
||||
--hash=sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8 \
|
||||
--hash=sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed \
|
||||
--hash=sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684 \
|
||||
--hash=sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56 \
|
||||
--hash=sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324 \
|
||||
--hash=sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d \
|
||||
--hash=sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235 \
|
||||
--hash=sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e \
|
||||
--hash=sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088 \
|
||||
--hash=sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000 \
|
||||
--hash=sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7 \
|
||||
--hash=sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e \
|
||||
--hash=sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673 \
|
||||
--hash=sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c \
|
||||
--hash=sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe \
|
||||
--hash=sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2 \
|
||||
--hash=sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098 \
|
||||
--hash=sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8 \
|
||||
--hash=sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a \
|
||||
--hash=sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0 \
|
||||
--hash=sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b \
|
||||
--hash=sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896 \
|
||||
--hash=sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e \
|
||||
--hash=sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9 \
|
||||
--hash=sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2 \
|
||||
--hash=sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b \
|
||||
--hash=sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6 \
|
||||
--hash=sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404 \
|
||||
--hash=sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f \
|
||||
--hash=sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0 \
|
||||
--hash=sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4 \
|
||||
--hash=sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc \
|
||||
--hash=sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936 \
|
||||
--hash=sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba \
|
||||
--hash=sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872 \
|
||||
--hash=sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb \
|
||||
--hash=sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614 \
|
||||
--hash=sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1 \
|
||||
--hash=sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d \
|
||||
--hash=sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969 \
|
||||
--hash=sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b \
|
||||
--hash=sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4 \
|
||||
--hash=sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627 \
|
||||
--hash=sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956 \
|
||||
--hash=sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357
|
||||
configargparse==1.7 ; python_version >= "3.11" and python_version < "4.0" \
|
||||
--hash=sha256:d249da6591465c6c26df64a9f73d2536e743be2f244eb3ebe61114af2f94f86b \
|
||||
--hash=sha256:e7067471884de5478c58a511e529f0f9bd1c66bfef1dea90935438d6c23306d1
|
||||
kissfix==7.0.11 ; python_version >= "3.11" and python_version < "4.0" \
|
||||
--hash=sha256:6d17a27c152a06be0329c701c2f32e1f5bdbc2c1a8361ae1af4c92e4d406baa8 \
|
||||
--hash=sha256:aa2a90d19549eba1ad08adb83cca7b9d147ec805918c208cfbb85dc834e2eeb8
|
||||
pyaudio==0.2.14 ; python_version >= "3.11" and python_version < "4.0" \
|
||||
--hash=sha256:009f357ee5aa6bc8eb19d69921cd30e98c42cddd34210615d592a71d09c4bd57 \
|
||||
--hash=sha256:126065b5e82a1c03ba16e7c0404d8f54e17368836e7d2d92427358ad44fefe61 \
|
||||
--hash=sha256:12f2f1ba04e06ff95d80700a78967897a489c05e093e3bffa05a84ed9c0a7fa3 \
|
||||
--hash=sha256:2a166fc88d435a2779810dd2678354adc33499e9d4d7f937f28b20cc55893e83 \
|
||||
--hash=sha256:2dac0d6d675fe7e181ba88f2de88d321059b69abd52e3f4934a8878e03a7a074 \
|
||||
--hash=sha256:506b32a595f8693811682ab4b127602d404df7dfc453b499c91a80d0f7bad289 \
|
||||
--hash=sha256:5fce4bcdd2e0e8c063d835dbe2860dac46437506af509353c7f8114d4bacbd5b \
|
||||
--hash=sha256:78dfff3879b4994d1f4fc6485646a57755c6ee3c19647a491f790a0895bd2f87 \
|
||||
--hash=sha256:858caf35b05c26d8fc62f1efa2e8f53d5fa1a01164842bd622f70ddc41f55000 \
|
||||
--hash=sha256:bbeb01d36a2f472ae5ee5e1451cacc42112986abe622f735bb870a5db77cf903 \
|
||||
--hash=sha256:f745109634a7c19fa4d6b8b7d6967c3123d988c9ade0cd35d4295ee1acdb53e9
|
||||
pycparser==2.21 ; python_version >= "3.11" and python_version < "4.0" \
|
||||
--hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \
|
||||
--hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206
|
||||
pydub==0.25.1 ; python_version >= "3.11" and python_version < "4.0" \
|
||||
--hash=sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6 \
|
||||
--hash=sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f
|
||||
pyserial==3.5 ; python_version >= "3.11" and python_version < "4.0" \
|
||||
--hash=sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb \
|
||||
--hash=sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0
|
||||
tabulate==0.9.0 ; python_version >= "3.11" and python_version < "4.0" \
|
||||
--hash=sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c \
|
||||
--hash=sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f
|
||||
44
freedvtnc2/rigctl.py
Normal file
44
freedvtnc2/rigctl.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env python3
|
||||
import socket
|
||||
import logging
|
||||
|
||||
# rigctl - https://github.com/darksidelemm/rotctld-web-gui/blob/master/rotatorgui.py#L35
|
||||
|
||||
class Rigctld():
|
||||
""" rigctld (hamlib) communication class """
|
||||
# Note: This is a massive hack.
|
||||
|
||||
def __init__(self, hostname="localhost", port=4532, poll_rate=5, timeout=5):
|
||||
""" Open a connection to rigctld, and test it for validity """
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.settimeout(timeout)
|
||||
|
||||
self.hostname = hostname
|
||||
self.port = port
|
||||
self.connect()
|
||||
logging.debug(f"Rigctl intialized")
|
||||
|
||||
def connect(self):
|
||||
""" Connect to rigctld instance """
|
||||
self.sock.connect((self.hostname,self.port))
|
||||
|
||||
def close(self):
|
||||
self.sock.close()
|
||||
|
||||
def send_command(self, command):
|
||||
""" Send a command to the connected rigctld instance,
|
||||
and return the return value.
|
||||
"""
|
||||
self.sock.sendall(command+b'\n')
|
||||
try:
|
||||
return self.sock.recv(1024)
|
||||
except:
|
||||
return None
|
||||
|
||||
def ptt_enable(self):
|
||||
logging.debug(f"PTT enabled")
|
||||
self.send_command(b"T 1")
|
||||
|
||||
def ptt_disable(self):
|
||||
logging.debug(f"PTT disabled")
|
||||
self.send_command(b"T 0")
|
||||
92
freedvtnc2/shell.py
Normal file
92
freedvtnc2/shell.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import logging
|
||||
import cmd
|
||||
from .modem import Modems, lib, ffi
|
||||
import readline
|
||||
import rlcompleter
|
||||
import code
|
||||
import sys
|
||||
import time
|
||||
from . import audio
|
||||
|
||||
import pydub.generators
|
||||
|
||||
class FreeDVShell(cmd.Cmd):
|
||||
intro = "FreeDVTNC2 Shell - type help or ? to list commands\n"
|
||||
prompt = "(freedvtnc2) "
|
||||
callsign = None
|
||||
|
||||
def do_log_level(self, arg):
|
||||
"Set the log level"
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(level=arg.upper())
|
||||
|
||||
def do_test_ptt(self, arg):
|
||||
"Turns on PTT for 2 seconds"
|
||||
print("Starting TX")
|
||||
sin_wave = pydub.generators.Sine(
|
||||
440,
|
||||
sample_rate=self.modem_tx.modem.sample_rate,
|
||||
bit_depth=16,
|
||||
).to_audio_segment(2000)
|
||||
sin_wave.set_channels(1)
|
||||
self.output_device.write(sin_wave.raw_data)
|
||||
print("Stopping TX")
|
||||
|
||||
def do_mode(self, arg):
|
||||
arg = arg.upper()
|
||||
if arg not in [x.name for x in Modems]:
|
||||
print(f"Mode must be {', '.join([x.name for x in Modems])}")
|
||||
else:
|
||||
modem = {x.name:x for x in Modems}[arg]
|
||||
self.modem_rx.set_mode(modem)
|
||||
self.modem_tx.set_mode(modem)
|
||||
print(f"Set mode {arg}")
|
||||
|
||||
def help_mode(self):
|
||||
print(f"Change TX Mode: mode [{', '.join([x.name for x in Modems])}]")
|
||||
|
||||
def do_clear(self, arg):
|
||||
"Clears TX queues"
|
||||
self.output_device.clear()
|
||||
print("TX buffer cleared")
|
||||
|
||||
def do_list_audio_devices(self, arg):
|
||||
"Lists audio device parameters"
|
||||
print(audio.devices)
|
||||
|
||||
def do_send_string(self, arg):
|
||||
"Sends string over the modem"
|
||||
|
||||
self.output_device.write(self.modem_tx.write(arg.encode()))
|
||||
|
||||
def do_msg(self, arg):
|
||||
"Send a message"
|
||||
|
||||
if not self.callsign:
|
||||
self.callsign = input("Your callsign:")
|
||||
|
||||
data = self.callsign.encode() + b"\xff" + arg.encode()
|
||||
|
||||
self.output_device.write(self.modem_tx.write(data, header_byte=b"\xfe"))
|
||||
|
||||
def do_debug(self, arg):
|
||||
"Open the debug shell"
|
||||
|
||||
def console_exit():
|
||||
raise SystemExit
|
||||
|
||||
variables = globals().copy()
|
||||
variables.update(locals())
|
||||
|
||||
variables['exit'] = console_exit
|
||||
sys.ps1 = "(freedvtnc2)>>> "
|
||||
sys.ps2 = "(freedvtnc2)... "
|
||||
|
||||
readline.set_completer(rlcompleter.Completer(variables).complete)
|
||||
shell = code.InteractiveConsole(variables)
|
||||
try:
|
||||
shell.interact(banner="freedvtnc2 debug console")
|
||||
except SystemExit:
|
||||
pass
|
||||
def emptyline(self):
|
||||
pass
|
||||
15
freedvtnc2/test_audio.py
Normal file
15
freedvtnc2/test_audio.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
import unittest
|
||||
from unittest.mock import Mock, call
|
||||
from . import audio
|
||||
import time
|
||||
|
||||
class TestAudio(unittest.TestCase):
|
||||
def test_audio_list_devices(self):
|
||||
audio.devices # just test that this function doesn't error - we can probably mock out pyaudio for proper tests
|
||||
callback = Mock()
|
||||
with audio.InputDevice(callback, 8000):
|
||||
time.sleep(0.1) # enough time to give an audio sample
|
||||
callback.assert_called()
|
||||
36
freedvtnc2/test_modem.py
Normal file
36
freedvtnc2/test_modem.py
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
import unittest
|
||||
from unittest.mock import Mock, call
|
||||
from . import modem
|
||||
|
||||
class TestModem(unittest.TestCase):
|
||||
def testMultiRX(self):
|
||||
callback = Mock()
|
||||
rx = modem.FreeDVRX(callback)
|
||||
for freedv_modem in rx.modems:
|
||||
freedv_modem.callback = callback
|
||||
with open("c01.raw","rb") as f:
|
||||
while chunk := f.read(100):
|
||||
rx.write(chunk) # spoon feed in data to make sure buffering is working
|
||||
|
||||
self.assertEqual(callback.call_count, 10)
|
||||
for mocked_call in callback.call_args_list:
|
||||
self.assertEqual(len(mocked_call[0]),1)
|
||||
self.assertIsInstance(mocked_call[0][0], modem.FreeDVFrame)
|
||||
self.assertIsInstance(mocked_call[0][0].snr, float)
|
||||
self.assertIsInstance(mocked_call[0][0].sync, int)
|
||||
self.assertEqual(mocked_call[0][0].modem, 'DATAC1')
|
||||
def testTX(self):
|
||||
tx = modem.FreeDVTX()
|
||||
callback = Mock()
|
||||
rx = modem.FreeDVRX(callback)
|
||||
tx_output = tx.write(b'test')
|
||||
tx_output += tx.write(b'test'*200)
|
||||
rx.write(tx_output)
|
||||
self.assertEqual(callback.call_args_list[0][0][0],modem.Packet(data=b'test',header=255))
|
||||
self.assertEqual(callback.call_args_list[1][0][0],modem.Packet(data=b'test'*200,header=255))
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
77
freedvtnc2/tnc.py
Normal file
77
freedvtnc2/tnc.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import kissfix
|
||||
import os, pty, tty, termios
|
||||
import threading
|
||||
import logging
|
||||
import sys, traceback
|
||||
import fcntl
|
||||
|
||||
# This deals with encoding and decoding KISS frames
|
||||
|
||||
class KissInterface():
|
||||
def __init__(self, callback):
|
||||
self.k = kissfix.SerialKISS('/dev/ptmx', 9600)
|
||||
self.k.start()
|
||||
|
||||
# Override the serial interface with our own PTY file descriptor
|
||||
self.control, self.user_port = pty.openpty()
|
||||
self.ttyname = os.ttyname(self.user_port)
|
||||
self.k.interface.fd = self.control # we need to override the the serial port with the fd from pty
|
||||
tty.setraw(self.control, termios.TCSANOW) # this makes the tty act more like a serial port
|
||||
|
||||
# change flags to be non blocking so that buffer full doesn't cause issues
|
||||
flags = fcntl.fcntl(self.control, fcntl.F_GETFL)
|
||||
flags |= os.O_NONBLOCK
|
||||
fcntl.fcntl(self.control, fcntl.F_SETFL, flags)
|
||||
|
||||
self.rx_thread = KissThread(callback, self.k)
|
||||
self.rx_thread.setDaemon(True)
|
||||
self.rx_thread.start()
|
||||
|
||||
def tx(self, bytes_in: bytes):
|
||||
frame = kissfix.FEND + b'\00' + kissfix.escape_special_codes(bytes_in) + kissfix.FEND
|
||||
try:
|
||||
os.write(self.control, frame)
|
||||
except BlockingIOError:
|
||||
logging.error("PTY interface buffer is full. The connected application may have crashed or isn't reading fast enough. Data loss is likely. Alternatively you aren't using the PTY interface and should have used --no-pty. Clearing the buffer now so we can keep going")
|
||||
blocking = os.get_blocking(self.user_port) # remember what the state was before
|
||||
os.set_blocking(self.user_port, False)
|
||||
try:
|
||||
while 1:
|
||||
os.read(self.user_port,32) # read off the buffer until we've cleared it
|
||||
except BlockingIOError:
|
||||
pass
|
||||
os.set_blocking(self.user_port, blocking) # restore the state after
|
||||
|
||||
|
||||
class KissTCPInterface():
|
||||
def __init__(self, callback, port=8001, address="127.0.0.1"):
|
||||
self.k = kissfix.TCPServerKISS(address, port)
|
||||
self.k.start()
|
||||
|
||||
self.rx_thread = KissThread(callback, self.k)
|
||||
self.rx_thread.setDaemon(True)
|
||||
self.rx_thread.start()
|
||||
|
||||
def tx(self, bytes_in: bytes):
|
||||
try:
|
||||
frame = kissfix.FEND + b'\00' + kissfix.escape_special_codes(bytes_in) + kissfix.FEND
|
||||
self.k._write_handler(frame)
|
||||
except:
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
logging.info("Issue send frame to TCP TNC - Client not connected?")
|
||||
pass # so many things can go wrong here
|
||||
|
||||
|
||||
class KissThread(threading.Thread):
|
||||
def __init__(self,callback, interface):
|
||||
threading.Thread.__init__(self)
|
||||
self.callback = callback
|
||||
self._running = True
|
||||
self.interface = interface
|
||||
def run(self):
|
||||
while self._running == True:
|
||||
# check TNC port
|
||||
for frame in self.interface.read(readmode=False):
|
||||
self.callback(bytes(frame[1:])) #we strip the first two byte which is TNC port number.
|
||||
def terminate(self):
|
||||
self._running = False
|
||||
Loading…
Add table
Add a link
Reference in a new issue