mirror of
https://github.com/markqvist/LXST.git
synced 2025-12-22 10:57:08 +00:00
Sync upstream
This commit is contained in:
@@ -57,7 +57,6 @@ class Codec2(Codec):
|
||||
elif frame.shape[1] > self.channels:
|
||||
frame = frame[:, 1]
|
||||
|
||||
|
||||
input_samples = frame*self.TYPE_MAP_FACTOR
|
||||
input_samples = input_samples.astype(np.int16)
|
||||
|
||||
@@ -88,7 +87,9 @@ class Codec2(Codec):
|
||||
def decode(self, frame_bytes):
|
||||
frame_header = frame_bytes[0]
|
||||
frame_bytes = frame_bytes[1:]
|
||||
frame_mode = self.HEADER_MODES[frame_header]
|
||||
|
||||
if frame_header in self.HEADER_MODES: frame_mode = self.HEADER_MODES[frame_header]
|
||||
else: frame_mode = self.mode
|
||||
if self.mode != frame_mode:
|
||||
self.set_mode(frame_mode)
|
||||
|
||||
|
||||
69
LXST/Filters.c
Normal file
69
LXST/Filters.c
Normal file
@@ -0,0 +1,69 @@
|
||||
#include <math.h>
|
||||
|
||||
void highpass_filter(float* input, float* output, int samples, int channels, float alpha, float* filter_states, float* last_inputs) {
|
||||
int i, ch;
|
||||
for (ch = 0; ch < channels; ch++) { float input_diff = input[ch] - last_inputs[ch]; output[ch] = alpha * (filter_states[ch] + input_diff); }
|
||||
for (i = 1; i < samples; i++) { for (ch = 0; ch < channels; ch++) { int idx = i * channels + ch; float input_diff = input[idx] - input[idx - channels]; output[idx] = alpha * (output[idx - channels] + input_diff); } }
|
||||
for (ch = 0; ch < channels; ch++) { int last_idx = (samples - 1) * channels + ch; filter_states[ch] = output[last_idx]; last_inputs[ch] = input[last_idx]; }
|
||||
}
|
||||
|
||||
void lowpass_filter(float* input, float* output, int samples, int channels, float alpha, float* filter_states) {
|
||||
int i, ch;
|
||||
float one_minus_alpha = 1.0f - alpha;
|
||||
for (ch = 0; ch < channels; ch++) { output[ch] = alpha * input[ch] + one_minus_alpha * filter_states[ch]; }
|
||||
for (i = 1; i < samples; i++) {
|
||||
for (ch = 0; ch < channels; ch++) { int idx = i * channels + ch; output[idx] = alpha * input[idx] + one_minus_alpha * output[idx - channels]; }
|
||||
}
|
||||
for (ch = 0; ch < channels; ch++) { int last_idx = (samples - 1) * channels + ch; filter_states[ch] = output[last_idx]; }
|
||||
}
|
||||
|
||||
void agc_process(float* input, float* output, int samples, int channels, float target_linear, float max_gain_linear, float trigger_level,
|
||||
float attack_coeff, float release_coeff, float hold_samples, float* current_gain_lin, int* hold_counter, int block_target) {
|
||||
|
||||
for (int i = 0; i < samples * channels; i++) { output[i] = input[i]; }
|
||||
int num_blocks = block_target;
|
||||
int block_size = samples / num_blocks;
|
||||
if (block_size < 1) block_size = 1;
|
||||
|
||||
for (int block = 0; block < num_blocks; block++) {
|
||||
int block_start = block*block_size;
|
||||
int block_end = (block + 1)*block_size;
|
||||
if (block == num_blocks - 1) { block_end = samples; }
|
||||
if (block_end > samples) { block_end = samples; }
|
||||
|
||||
int block_samples = block_end - block_start;
|
||||
if (block_samples <= 0) continue;
|
||||
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
float sum_squares = 0.0f;
|
||||
for (int i = block_start; i < block_end; i++) { int idx = i * channels + ch; sum_squares += output[idx] * output[idx]; }
|
||||
float rms = sqrtf(sum_squares / block_samples);
|
||||
|
||||
float target_gain;
|
||||
if (rms > 1e-9f && rms > trigger_level) {
|
||||
target_gain = target_linear / rms;
|
||||
if (target_gain > max_gain_linear) { target_gain = max_gain_linear; }
|
||||
} else { target_gain = current_gain_lin[ch]; }
|
||||
|
||||
if (target_gain < current_gain_lin[ch]) {
|
||||
current_gain_lin[ch] = attack_coeff * target_gain + (1.0f - attack_coeff) * current_gain_lin[ch];
|
||||
*hold_counter = (int)hold_samples;
|
||||
} else {
|
||||
if (*hold_counter > 0) { *hold_counter -= block_samples; }
|
||||
else { current_gain_lin[ch] = release_coeff * target_gain + (1.0f - release_coeff) * current_gain_lin[ch]; }
|
||||
}
|
||||
|
||||
for (int i = block_start; i < block_end; i++) { int idx = i * channels + ch; output[idx] *= current_gain_lin[ch]; }
|
||||
}
|
||||
}
|
||||
|
||||
float peak_limit = 0.75f;
|
||||
for (int ch = 0; ch < channels; ch++) {
|
||||
float peak = 0.0f;
|
||||
for (int i = 0; i < samples; i++) { int idx = i * channels + ch; float abs_val = fabsf(output[idx]);
|
||||
if (abs_val > peak) { peak = abs_val; } }
|
||||
|
||||
if (peak > peak_limit) { float scale = peak_limit / peak;
|
||||
for (int i = 0; i < samples; i++) { int idx = i * channels + ch; output[idx] *= scale; } }
|
||||
}
|
||||
}
|
||||
3
LXST/Filters.h
Normal file
3
LXST/Filters.h
Normal file
@@ -0,0 +1,3 @@
|
||||
void highpass_filter(float* input, float* output, int samples, int channels, float alpha, float* filter_states, float* last_inputs);
|
||||
void lowpass_filter(float* input, float* output, int samples, int channels, float alpha, float* filter_states);
|
||||
void agc_process(float* input, float* output, int samples, int channels, float target_linear, float max_gain_linear, float trigger_level, float attack_coeff, float release_coeff, float hold_samples, float* current_gain_lin, int* hold_counter, int block_target);
|
||||
276
LXST/Filters.py
Normal file
276
LXST/Filters.py
Normal file
@@ -0,0 +1,276 @@
|
||||
from importlib.util import find_spec
|
||||
import numpy as np
|
||||
import time
|
||||
import RNS
|
||||
import os
|
||||
|
||||
USE_NATIVE_FILTERS = False
|
||||
if not find_spec("cffi"):
|
||||
RNS.log(f"Could not load CFFI module for filter acceleration, falling back to Python filters. This will be slow.", RNS.LOG_WARNING)
|
||||
RNS.log(f"Make sure that the CFFI module is installed and available.", RNS.LOG_WARNING)
|
||||
else:
|
||||
try:
|
||||
from cffi import FFI
|
||||
import pathlib
|
||||
c_src_path = pathlib.Path(__file__).parent.resolve()
|
||||
ffi = FFI()
|
||||
|
||||
try:
|
||||
filterlib_spec = find_spec("LXST.filterlib")
|
||||
if not filterlib_spec or filterlib_spec.origin == None: raise ImportError("Could not locate pre-compiled LXST.filterlib module")
|
||||
with open(os.path.join(c_src_path, "Filters.h"), "r") as f: ffi.cdef(f.read())
|
||||
native_functions = ffi.dlopen(filterlib_spec.origin)
|
||||
USE_NATIVE_FILTERS = True
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not load pre-compiled LXST filters library. The contained exception was: {e}", RNS.LOG_WARNING)
|
||||
RNS.log(f"Attempting to compile library from source...", RNS.LOG_WARNING)
|
||||
|
||||
if USE_NATIVE_FILTERS == False:
|
||||
with open(os.path.join(c_src_path, "Filters.h"), "r") as f: ffi.cdef(f.read())
|
||||
with open(os.path.join(c_src_path, "Filters.c"), "r") as f: c_src = f.read()
|
||||
native_functions = ffi.verify(c_src)
|
||||
USE_NATIVE_FILTERS = True
|
||||
RNS.log(f"Successfully compiled and loaded filters library", RNS.LOG_WARNING)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not compile modules for filter acceleration, falling back to Python filters. This will be slow.", RNS.LOG_WARNING)
|
||||
RNS.log(f"The contained exception was: {e}", RNS.LOG_WARNING)
|
||||
USE_NATIVE_FILTERS = False
|
||||
|
||||
class Filter():
|
||||
def handle_frame(self, frame):
|
||||
raise NotImplementedError(f"The handle_frame method was not implemented on {self}")
|
||||
|
||||
class HighPass(Filter):
|
||||
def __init__(self, cut):
|
||||
super().__init__()
|
||||
self.cut = cut
|
||||
self._samplerate = None
|
||||
self._channels = None
|
||||
self._filter_states = None
|
||||
self._last_inputs = None
|
||||
self._alpha = None
|
||||
|
||||
def handle_frame(self, frame, samplerate):
|
||||
if len(frame) == 0: return frame
|
||||
if samplerate != self._samplerate:
|
||||
self._samplerate = samplerate
|
||||
dt = 1.0 / self._samplerate
|
||||
rc = 1.0 / (2 * np.pi * self.cut)
|
||||
self._alpha = rc / (rc + dt)
|
||||
|
||||
if len(frame.shape) == 1: frame_2d = frame.reshape(-1, 1)
|
||||
else: frame_2d = frame
|
||||
|
||||
samples, channels = frame_2d.shape
|
||||
if self._filter_states is None or self._channels != channels:
|
||||
self._channels = channels
|
||||
self._filter_states = np.zeros(self._channels, dtype=np.float32)
|
||||
self._last_inputs = np.zeros(self._channels, dtype=np.float32)
|
||||
|
||||
if USE_NATIVE_FILTERS:
|
||||
frame_2d = np.ascontiguousarray(frame_2d, dtype=np.float32)
|
||||
output = np.empty_like(frame_2d, dtype=np.float32)
|
||||
input_ptr = ffi.cast("float *", frame_2d.ctypes.data)
|
||||
output_ptr = ffi.cast("float *", output.ctypes.data)
|
||||
states_ptr = ffi.cast("float *", self._filter_states.ctypes.data)
|
||||
last_inputs_ptr = ffi.cast("float *", self._last_inputs.ctypes.data)
|
||||
|
||||
native_functions.highpass_filter(input_ptr, output_ptr, samples, channels, float(self._alpha), states_ptr, last_inputs_ptr)
|
||||
|
||||
result = output.reshape(frame.shape)
|
||||
return result
|
||||
|
||||
else:
|
||||
output = np.empty_like(frame_2d)
|
||||
input_diff_first = frame_2d[0] - self._last_inputs
|
||||
output[0] = self._alpha * (self._filter_states + input_diff_first)
|
||||
|
||||
input_diff = np.empty_like(frame_2d)
|
||||
input_diff[0] = input_diff_first
|
||||
input_diff[1:] = frame_2d[1:] - frame_2d[:-1]
|
||||
|
||||
for i in range(1, samples):
|
||||
output[i] = self._alpha * (output[i-1] + input_diff[i])
|
||||
|
||||
output = self._alpha * (output + input_diff)
|
||||
|
||||
self._filter_states = output[-1].copy()
|
||||
self._last_inputs = frame_2d[-1].copy()
|
||||
|
||||
nframe = output.reshape(frame.shape)
|
||||
return nframe
|
||||
|
||||
class LowPass(Filter):
|
||||
def __init__(self, cut):
|
||||
super().__init__()
|
||||
self.cut = cut
|
||||
self._samplerate = None
|
||||
self._channels = None
|
||||
self._filter_states = None
|
||||
self._alpha = None
|
||||
|
||||
def handle_frame(self, frame, samplerate):
|
||||
if len(frame) == 0: return frame
|
||||
if samplerate != self._samplerate:
|
||||
self._samplerate = samplerate
|
||||
dt = 1.0 / self._samplerate
|
||||
rc = 1.0 / (2 * np.pi * self.cut)
|
||||
self._alpha = dt / (rc + dt)
|
||||
|
||||
if len(frame.shape) == 1: frame_2d = frame.reshape(-1, 1)
|
||||
else: frame_2d = frame
|
||||
|
||||
samples, channels = frame_2d.shape
|
||||
|
||||
if self._filter_states is None or self._channels != channels:
|
||||
self._channels = channels
|
||||
self._filter_states = np.zeros(self._channels, dtype=np.float32)
|
||||
|
||||
if USE_NATIVE_FILTERS:
|
||||
frame_2d = np.ascontiguousarray(frame_2d, dtype=np.float32)
|
||||
output = np.empty_like(frame_2d, dtype=np.float32)
|
||||
input_ptr = ffi.cast("float *", frame_2d.ctypes.data)
|
||||
output_ptr = ffi.cast("float *", output.ctypes.data)
|
||||
states_ptr = ffi.cast("float *", self._filter_states.ctypes.data)
|
||||
|
||||
native_functions.lowpass_filter(input_ptr, output_ptr, samples, channels, float(self._alpha), states_ptr)
|
||||
|
||||
return output.reshape(frame.shape)
|
||||
|
||||
else:
|
||||
output = np.empty_like(frame_2d)
|
||||
output[0] = self._alpha * frame_2d[0] + (1.0 - self._alpha) * self._filter_states
|
||||
for i in range(1, samples):
|
||||
output[i] = self._alpha * frame_2d[i] + (1.0 - self._alpha) * output[i-1]
|
||||
|
||||
self._filter_states = output[-1].copy()
|
||||
|
||||
return output.reshape(frame.shape)
|
||||
|
||||
class BandPass(Filter):
|
||||
def __init__(self, low_cut, high_cut):
|
||||
super().__init__()
|
||||
if low_cut >= high_cut: raise ValueError("Low-cut frequency must be less than high-cut frequency")
|
||||
self.low_cut = low_cut
|
||||
self.high_cut = high_cut
|
||||
self._high_pass = HighPass(self.low_cut)
|
||||
self._low_pass = LowPass(self.high_cut)
|
||||
|
||||
def handle_frame(self, frame, samplerate):
|
||||
# TODO: Remove debug
|
||||
# st = time.time()
|
||||
if len(frame) == 0: return frame
|
||||
high_passed = self._high_pass.handle_frame(frame, samplerate)
|
||||
band_passed = self._low_pass.handle_frame(high_passed, samplerate)
|
||||
# RNS.log(f"Filter ran in {RNS.prettyshorttime(time.time()-st)}", RNS.LOG_DEBUG)
|
||||
return band_passed
|
||||
|
||||
class AGC(Filter):
|
||||
def __init__(self, target_level=-12.0, max_gain=12.0, attack_time=0.0001, release_time=0.002, hold_time=0.001):
|
||||
super().__init__()
|
||||
self.trigger_level = 0.003
|
||||
self.target_level = target_level # In dBFS
|
||||
self.max_gain_db = max_gain
|
||||
self.attack_time = attack_time
|
||||
self.release_time = release_time
|
||||
self.hold_time = hold_time
|
||||
self.target_linear = 10 ** (target_level / 10)
|
||||
self.max_gain_linear = 10 ** (max_gain / 10)
|
||||
self._samplerate = None
|
||||
self._channels = None
|
||||
self._current_gain_lin = 1.0
|
||||
self._hold_counter = 0
|
||||
self._block_target_s = 0.01
|
||||
self._attack_coeff = None
|
||||
self._release_coeff = None
|
||||
self._hold_samples = None
|
||||
|
||||
def handle_frame(self, frame, samplerate):
|
||||
# TODO: Remove debug
|
||||
# st = time.time()
|
||||
if len(frame) == 0: return frame
|
||||
if len(frame.shape) == 1: frame_2d = frame.reshape(-1, 1)
|
||||
else: frame_2d = frame
|
||||
|
||||
samples, channels = frame_2d.shape
|
||||
if samplerate != self._samplerate:
|
||||
self._samplerate = samplerate
|
||||
self._block_target = int((samples/self._samplerate)/self._block_target_s)
|
||||
self._calculate_coefficients()
|
||||
|
||||
if self._channels is None or self._channels != channels:
|
||||
self._channels = channels
|
||||
self._current_gain_lin = np.ones(channels, dtype=np.float32)
|
||||
self._hold_counter = 0
|
||||
|
||||
if USE_NATIVE_FILTERS:
|
||||
frame_2d = np.ascontiguousarray(frame_2d, dtype=np.float32)
|
||||
output = np.empty_like(frame_2d, dtype=np.float32)
|
||||
input_ptr = ffi.cast("float *", frame_2d.ctypes.data)
|
||||
output_ptr = ffi.cast("float *", output.ctypes.data)
|
||||
gain_ptr = ffi.cast("float *", self._current_gain_lin.ctypes.data)
|
||||
hold_ptr = ffi.new("int *", self._hold_counter)
|
||||
|
||||
native_functions.agc_process(
|
||||
input_ptr, output_ptr, samples, channels,
|
||||
float(self.target_linear), float(self.max_gain_linear),
|
||||
float(self.trigger_level),
|
||||
float(self._attack_coeff), float(self._release_coeff),
|
||||
float(self._hold_samples),
|
||||
gain_ptr, hold_ptr, int(self._block_target)
|
||||
)
|
||||
|
||||
self._hold_counter = hold_ptr[0]
|
||||
|
||||
result = output.reshape(frame.shape)
|
||||
# TODO: Remove debug
|
||||
# RNS.log(f"AGC ran in {RNS.prettyshorttime(time.time()-st)}", RNS.LOG_DEBUG)
|
||||
return result
|
||||
|
||||
else:
|
||||
output = np.empty_like(frame_2d)
|
||||
block_size = max(1, samples // self._block_target)
|
||||
for i in range(0, samples, block_size):
|
||||
block_end = min(i + block_size, samples)
|
||||
block = frame_2d[i:block_end]
|
||||
block_samples = block_end - i
|
||||
|
||||
rms = np.sqrt(np.mean(block ** 2, axis=0))
|
||||
target_gain = np.where(rms > 1e-9, self.target_linear / np.maximum(rms, 1e-9), self.max_gain_linear)
|
||||
target_gain = np.minimum(target_gain, self.max_gain_linear)
|
||||
smoothed_gain = np.empty_like(target_gain)
|
||||
|
||||
for ch in range(channels):
|
||||
if (rms[0] < self.trigger_level): target_gain = self._current_gain_lin
|
||||
if target_gain[ch] < self._current_gain_lin[ch]:
|
||||
self._current_gain_lin[ch] = self._attack_coeff * target_gain[ch] + (1 - self._attack_coeff) * self._current_gain_lin[ch]
|
||||
self._hold_counter = self._hold_samples # Reset hold counter
|
||||
else:
|
||||
if self._hold_counter > 0: self._hold_counter -= block_samples
|
||||
else: self._current_gain_lin[ch] = self._release_coeff * target_gain[ch] + (1 - self._release_coeff) * self._current_gain_lin[ch]
|
||||
|
||||
smoothed_gain[ch] = self._current_gain_lin[ch]
|
||||
|
||||
output[i:block_end] = block * smoothed_gain[np.newaxis, :]
|
||||
|
||||
peak_limit = 0.75
|
||||
current_peaks = np.max(np.abs(output), axis=0)
|
||||
limit_gain = np.where(current_peaks > peak_limit, peak_limit / np.maximum(current_peaks, 1e-9), 1.0)
|
||||
|
||||
if np.any(limit_gain < 1.0): output *= limit_gain[np.newaxis, :]
|
||||
nframe = output.reshape(frame.shape)
|
||||
# TODO: Remove debug
|
||||
# RNS.log(f"AGC ran in {RNS.prettyshorttime(time.time()-st)}", RNS.LOG_DEBUG)
|
||||
return nframe
|
||||
|
||||
def _calculate_coefficients(self):
|
||||
if self._samplerate:
|
||||
self._attack_coeff = 1.0 - np.exp(-1.0 / (self.attack_time * self._samplerate))
|
||||
self._release_coeff = 1.0 - np.exp(-1.0 / (self.release_time * self._samplerate))
|
||||
self._hold_samples = int(self.hold_time * self._samplerate)
|
||||
else:
|
||||
self._attack_coeff = 0.1
|
||||
self._release_coeff = 0.01
|
||||
self._hold_samples = 1000
|
||||
@@ -15,7 +15,7 @@ class Mixer(LocalSource, LocalSink):
|
||||
MAX_FRAMES = 8
|
||||
TYPE_MAP_FACTOR = np.iinfo("int16").max
|
||||
|
||||
def __init__(self, target_frame_ms=40, samplerate=None, codec=None, sink=None):
|
||||
def __init__(self, target_frame_ms=40, samplerate=None, codec=None, sink=None, gain=0.0):
|
||||
self.incoming_frames = {}
|
||||
self.target_frame_ms = target_frame_ms
|
||||
self.frame_time = self.target_frame_ms/1000
|
||||
@@ -23,6 +23,8 @@ class Mixer(LocalSource, LocalSink):
|
||||
self.mixer_thread = None
|
||||
self.mixer_lock = threading.Lock()
|
||||
self.insert_lock = threading.Lock()
|
||||
self.muted = False
|
||||
self.gain = gain
|
||||
self.bitdepth = 32
|
||||
self.channels = None
|
||||
self.samplerate = None
|
||||
@@ -44,6 +46,18 @@ class Mixer(LocalSource, LocalSink):
|
||||
def stop(self):
|
||||
self.should_run = False
|
||||
|
||||
def set_gain(self, gain=None):
|
||||
if gain == None: self.gain = 0.0
|
||||
else: self.gain = float(gain)
|
||||
|
||||
def mute(self, mute=True):
|
||||
if mute == True or mute == False: self.muted = mute
|
||||
else: self.muted = False
|
||||
|
||||
def unmute(self, unmute=True):
|
||||
if unmute == True or unmute == False: self.muted = unmute
|
||||
else: self.muted = False
|
||||
|
||||
def set_source_max_frames(self, source, max_frames):
|
||||
with self.insert_lock:
|
||||
if not source in self.incoming_frames: self.incoming_frames[source] = deque(maxlen=max_frames)
|
||||
@@ -78,6 +92,12 @@ class Mixer(LocalSource, LocalSink):
|
||||
|
||||
self.incoming_frames[source].append(frame_samples)
|
||||
|
||||
@property
|
||||
def _mixing_gain(self):
|
||||
if self.muted: return 0.0
|
||||
elif self.gain == 0.0: return 1.0
|
||||
else: return 10**(self.gain/10)
|
||||
|
||||
def _mixer_job(self):
|
||||
with self.mixer_lock:
|
||||
while self.should_run:
|
||||
@@ -87,11 +107,16 @@ class Mixer(LocalSource, LocalSink):
|
||||
for source in self.incoming_frames.copy():
|
||||
if len(self.incoming_frames[source]) > 0:
|
||||
next_frame = self.incoming_frames[source].popleft()
|
||||
if source_count == 0: mixed_frame = next_frame
|
||||
else: mixed_frame = mixed_frame + next_frame
|
||||
if source_count == 0: mixed_frame = next_frame*self._mixing_gain
|
||||
else: mixed_frame = mixed_frame + next_frame*self._mixing_gain
|
||||
source_count += 1
|
||||
|
||||
if source_count > 0:
|
||||
mixed_frame = np.clip(mixed_frame, -1.0, 1.0)
|
||||
if RNS.loglevel >= RNS.LOG_DEBUG:
|
||||
if mixed_frame.max() >= 1.0 or mixed_frame.min() <= -1.0:
|
||||
RNS.log(f"Signal clipped on {self}", RNS.LOG_WARNING)
|
||||
|
||||
if self.codec: self.sink.handle_frame(self.codec.encode(mixed_frame), self)
|
||||
else: self.sink.handle_frame(mixed_frame, self)
|
||||
else:
|
||||
|
||||
@@ -128,10 +128,8 @@ class LinkSource(RemoteSource, SignallingReceiver):
|
||||
else:
|
||||
decoded_frame = self.codec.decode(frame[1:])
|
||||
|
||||
if self.pipeline:
|
||||
self.sink.handle_frame(decoded_frame, self)
|
||||
else:
|
||||
self.sink.handle_frame(decoded_frame, self, decoded=True)
|
||||
if self.pipeline: self.sink.handle_frame(decoded_frame, self)
|
||||
else: self.sink.handle_frame(decoded_frame, self, decoded=True)
|
||||
|
||||
if FIELD_SIGNALLING in unpacked:
|
||||
super()._packet(data=None, packet=packet, unpacked=unpacked)
|
||||
|
||||
@@ -11,6 +11,8 @@ from LXST.Sinks import LineSink
|
||||
from LXST.Sources import LineSource, OpusFileSource
|
||||
from LXST.Generators import ToneSource
|
||||
from LXST.Network import SignallingReceiver, Packetizer, LinkSource
|
||||
from LXST.Filters import BandPass, AGC
|
||||
|
||||
|
||||
PRIMITIVE_NAME = "telephony"
|
||||
|
||||
@@ -112,6 +114,7 @@ class Signalling():
|
||||
class Telephone(SignallingReceiver):
|
||||
RING_TIME = 60
|
||||
WAIT_TIME = 70
|
||||
CONNECT_TIME = 5
|
||||
DIAL_TONE_FREQUENCY = 382
|
||||
DIAL_TONE_EASE_MS = 3.14159
|
||||
JOB_INTERVAL = 5
|
||||
@@ -120,7 +123,13 @@ class Telephone(SignallingReceiver):
|
||||
ALLOW_ALL = 0xFF
|
||||
ALLOW_NONE = 0xFE
|
||||
|
||||
def __init__(self, identity, ring_time=RING_TIME, wait_time=WAIT_TIME, auto_answer=None, allowed=ALLOW_ALL):
|
||||
@staticmethod
|
||||
def available_outputs(): return LXST.Sources.Backend().soundcard.all_speakers()
|
||||
|
||||
@staticmethod
|
||||
def available_inputs(): return LXST.Sinks.Backend().soundcard.all_microphones()
|
||||
|
||||
def __init__(self, identity, ring_time=RING_TIME, wait_time=WAIT_TIME, auto_answer=None, allowed=ALLOW_ALL, receive_gain=0.0, transmit_gain=0.0):
|
||||
super().__init__()
|
||||
self.identity = identity
|
||||
self.destination = RNS.Destination(self.identity, RNS.Destination.IN, RNS.Destination.SINGLE, APP_NAME, PRIMITIVE_NAME)
|
||||
@@ -132,16 +141,22 @@ class Telephone(SignallingReceiver):
|
||||
self.call_handler_lock = threading.Lock()
|
||||
self.pipeline_lock = threading.Lock()
|
||||
self.caller_pipeline_open_lock = threading.Lock()
|
||||
self.establishment_timeout = self.CONNECT_TIME
|
||||
self.links = {}
|
||||
self.ring_time = ring_time
|
||||
self.wait_time = wait_time
|
||||
self.auto_answer = auto_answer
|
||||
self.receive_gain = receive_gain
|
||||
self.transmit_gain = transmit_gain
|
||||
self.use_agc = True
|
||||
self.active_call = None
|
||||
self.call_status = Signalling.STATUS_AVAILABLE
|
||||
self._external_busy = False
|
||||
self.__ringing_callback = None
|
||||
self.__established_callback = None
|
||||
self.__ended_callback = None
|
||||
self.__busy_callback = None
|
||||
self.__rejected_callback = None
|
||||
self.target_frame_time_ms = None
|
||||
self.audio_output = None
|
||||
self.audio_input = None
|
||||
@@ -184,6 +199,9 @@ class Telephone(SignallingReceiver):
|
||||
if type(blocked) == list or blocked == None: self.blocked = blocked
|
||||
else: raise TypeError(f"Invalid type for blocked callers: {type(blocked)}")
|
||||
|
||||
def set_connect_timeout(self, timeout):
|
||||
self.establishment_timeout = timeout
|
||||
|
||||
def set_announce_interval(self, announce_interval):
|
||||
if not type(announce_interval) == int: raise TypeError(f"Invalid type for announce interval: {announce_interval}")
|
||||
else:
|
||||
@@ -202,6 +220,14 @@ class Telephone(SignallingReceiver):
|
||||
if not callable(callback): raise TypeError(f"Invalid callback, {callback} is not callable")
|
||||
self.__ended_callback = callback
|
||||
|
||||
def set_busy_callback(self, callback):
|
||||
if not callable(callback): raise TypeError(f"Invalid callback, {callback} is not callable")
|
||||
self.__busy_callback = callback
|
||||
|
||||
def set_rejected_callback(self, callback):
|
||||
if not callable(callback): raise TypeError(f"Invalid callback, {callback} is not callable")
|
||||
self.__rejected_callback = callback
|
||||
|
||||
def set_speaker(self, device):
|
||||
self.speaker_device = device
|
||||
RNS.log(f"{self} speaker device set to {device}", RNS.LOG_DEBUG)
|
||||
@@ -214,11 +240,19 @@ class Telephone(SignallingReceiver):
|
||||
self.ringer_device = device
|
||||
RNS.log(f"{self} ringer device set to {device}", RNS.LOG_DEBUG)
|
||||
|
||||
def set_ringtone(self, ringtone_path, gain=1.0):
|
||||
def set_ringtone(self, ringtone_path, gain=0.0):
|
||||
self.ringtone_path = ringtone_path
|
||||
self.ringtone_gain = gain
|
||||
RNS.log(f"{self} ringtone set to {self.ringtone_path}", RNS.LOG_DEBUG)
|
||||
|
||||
def enable_agc(self, enable=True):
|
||||
if enable == True: self.use_agc = True
|
||||
else: self.use_agc = False
|
||||
|
||||
def disable_agc(self, disable=True):
|
||||
if disable == True: self.use_agc = False
|
||||
else: self.use_agc = True
|
||||
|
||||
def set_low_latency_output(self, enabled):
|
||||
if enabled:
|
||||
self.low_latency_output = True
|
||||
@@ -243,9 +277,7 @@ class Telephone(SignallingReceiver):
|
||||
|
||||
def __timeout_incoming_call_at(self, call, timeout):
|
||||
def job():
|
||||
while time.time()<timeout and self.active_call == call:
|
||||
time.sleep(0.25)
|
||||
|
||||
while time.time()<timeout and self.active_call == call: time.sleep(0.25)
|
||||
if self.active_call == call and self.call_status < Signalling.STATUS_ESTABLISHED:
|
||||
RNS.log(f"Ring timeout on call from {RNS.prettyhexrep(self.active_call.hash)}, hanging up", RNS.LOG_DEBUG)
|
||||
self.active_call.ring_timeout = True
|
||||
@@ -255,21 +287,29 @@ class Telephone(SignallingReceiver):
|
||||
|
||||
def __timeout_outgoing_call_at(self, call, timeout):
|
||||
def job():
|
||||
while time.time()<timeout and self.active_call == call:
|
||||
time.sleep(0.25)
|
||||
|
||||
while time.time()<timeout and self.active_call == call: time.sleep(0.25)
|
||||
if self.active_call == call and self.call_status < Signalling.STATUS_ESTABLISHED:
|
||||
RNS.log(f"Timeout on outgoing call to {RNS.prettyhexrep(self.active_call.hash)}, hanging up", RNS.LOG_DEBUG)
|
||||
self.hangup()
|
||||
|
||||
threading.Thread(target=job, daemon=True).start()
|
||||
|
||||
def __timeout_outgoing_establishment_at(self, call, timeout):
|
||||
def job():
|
||||
while time.time()<timeout and self.active_call == call: time.sleep(0.25)
|
||||
if self.active_call == call and self.call_status < Signalling.STATUS_RINGING:
|
||||
RNS.log(f"Timeout on outgoing connection establishment to {RNS.prettyhexrep(self.active_call.hash)}, hanging up", RNS.LOG_DEBUG)
|
||||
self.hangup()
|
||||
|
||||
threading.Thread(target=job, daemon=True).start()
|
||||
|
||||
def __incoming_link_established(self, link):
|
||||
link.is_incoming = True
|
||||
link.is_outgoing = False
|
||||
link.ring_timeout = False
|
||||
link.answered = False
|
||||
link.profile = None
|
||||
link.is_incoming = True
|
||||
link.is_outgoing = False
|
||||
link.ring_timeout = False
|
||||
link.answered = False
|
||||
link.is_terminating = False
|
||||
link.profile = None
|
||||
with self.call_handler_lock:
|
||||
if self.active_call or self.busy:
|
||||
RNS.log(f"Incoming call, but line is already active, signalling busy", RNS.LOG_DEBUG)
|
||||
@@ -314,7 +354,7 @@ class Telephone(SignallingReceiver):
|
||||
def __link_closed(self, link):
|
||||
if link == self.active_call:
|
||||
RNS.log(f"Remote for {RNS.prettyhexrep(link.get_remote_identity().hash)} hung up", RNS.LOG_DEBUG)
|
||||
self.hangup()
|
||||
if not self.active_call.is_terminating: self.hangup()
|
||||
|
||||
def set_busy(self, busy):
|
||||
self._external_busy = busy
|
||||
@@ -356,7 +396,7 @@ class Telephone(SignallingReceiver):
|
||||
if self.low_latency_output: self.audio_output.enable_low_latency()
|
||||
return True
|
||||
|
||||
def hangup(self):
|
||||
def hangup(self, reason=None):
|
||||
if self.active_call:
|
||||
with self.call_handler_lock:
|
||||
terminating_call = self.active_call; self.active_call = None
|
||||
@@ -375,31 +415,61 @@ class Telephone(SignallingReceiver):
|
||||
self.audio_output = None
|
||||
self.dial_tone = None
|
||||
self.call_status = Signalling.STATUS_AVAILABLE
|
||||
if remote_identity:
|
||||
RNS.log(f"Call with {RNS.prettyhexrep(remote_identity.hash)} terminated", RNS.LOG_DEBUG)
|
||||
else:
|
||||
RNS.log(f"Outgoing call could not be connected, link establishment failed", RNS.LOG_DEBUG)
|
||||
if remote_identity: RNS.log(f"Call with {RNS.prettyhexrep(remote_identity.hash)} terminated", RNS.LOG_DEBUG)
|
||||
else: RNS.log(f"Outgoing call could not be connected, link establishment failed", RNS.LOG_DEBUG)
|
||||
|
||||
if callable(self.__ended_callback): self.__ended_callback(remote_identity)
|
||||
if reason == None:
|
||||
if callable(self.__ended_callback): self.__ended_callback(remote_identity)
|
||||
elif reason == Signalling.STATUS_BUSY:
|
||||
if callable(self.__busy_callback): self.__busy_callback(remote_identity)
|
||||
elif callable(self.__ended_callback): self.__ended_callback(remote_identity)
|
||||
elif reason == Signalling.STATUS_REJECTED:
|
||||
if callable(self.__rejected_callback): self.__rejected_callback(remote_identity)
|
||||
elif callable(self.__ended_callback): self.__ended_callback(remote_identity)
|
||||
|
||||
def mute_receive(self):
|
||||
pass
|
||||
def mute_receive(self, mute=True):
|
||||
if self.receive_mixer: self.receive_mixer.mute(mute)
|
||||
|
||||
def mute_transmit(self):
|
||||
pass
|
||||
def unmute_receive(self, unmute=True):
|
||||
if self.receive_mixer: self.receive_mixer.unmute(mute)
|
||||
|
||||
def select_call_profile(self, profile=None):
|
||||
def mute_transmit(self, mute=True):
|
||||
if self.transmit_mixer: self.transmit_mixer.mute(mute)
|
||||
|
||||
def unmute_transmit(self, unmute=True):
|
||||
if self.transmit_mixer: self.transmit_mixer.unmute(unmute)
|
||||
|
||||
def set_receive_gain(self, gain=0.0):
|
||||
self.receive_gain = float(gain)
|
||||
if self.receive_mixer: self.receive_mixer.set_gain(self.receive_gain)
|
||||
|
||||
def set_transmit_gain(self, gain=0.0):
|
||||
self.transmit_gain = float(gain)
|
||||
if self.transmit_mixer: self.transmit_mixer.set_gain(self.transmit_gain)
|
||||
|
||||
def switch_profile(self, profile=None, from_signalling=False):
|
||||
if self.active_call:
|
||||
if self.active_call.profile == profile: return
|
||||
else:
|
||||
if self.call_status == Signalling.STATUS_ESTABLISHED:
|
||||
self.active_call.profile = profile
|
||||
self.transmit_codec = Profiles.get_codec(self.active_call.profile)
|
||||
self.target_frame_time_ms = Profiles.get_frame_time(self.active_call.profile)
|
||||
if not from_signalling: self.signal(Signalling.PREFERRED_PROFILE+self.active_call.profile, self.active_call)
|
||||
self.__reconfigure_transmit_pipeline()
|
||||
|
||||
def __select_call_profile(self, profile=None):
|
||||
if profile == None: profile = Profiles.DEFAULT_PROFILE
|
||||
self.active_call.profile = profile
|
||||
self.select_call_codecs(self.active_call.profile)
|
||||
self.select_call_frame_time(self.active_call.profile)
|
||||
self.__select_call_codecs(self.active_call.profile)
|
||||
self.__select_call_frame_time(self.active_call.profile)
|
||||
RNS.log(f"Selected call profile 0x{RNS.hexrep(profile, delimit=False)}", RNS.LOG_DEBUG)
|
||||
|
||||
def select_call_codecs(self, profile=None):
|
||||
def __select_call_codecs(self, profile=None):
|
||||
self.receive_codec = Null()
|
||||
self.transmit_codec = Profiles.get_codec(profile)
|
||||
|
||||
def select_call_frame_time(self, profile=None):
|
||||
def __select_call_frame_time(self, profile=None):
|
||||
self.target_frame_time_ms = Profiles.get_frame_time(profile)
|
||||
|
||||
def __reset_dialling_pipelines(self):
|
||||
@@ -415,9 +485,9 @@ class Telephone(SignallingReceiver):
|
||||
self.__prepare_dialling_pipelines()
|
||||
|
||||
def __prepare_dialling_pipelines(self):
|
||||
self.select_call_profile(self.active_call.profile)
|
||||
self.__select_call_profile(self.active_call.profile)
|
||||
if self.audio_output == None: self.audio_output = LineSink(preferred_device=self.speaker_device)
|
||||
if self.receive_mixer == None: self.receive_mixer = Mixer(target_frame_ms=self.target_frame_time_ms)
|
||||
if self.receive_mixer == None: self.receive_mixer = Mixer(target_frame_ms=self.target_frame_time_ms, gain=self.receive_gain)
|
||||
if self.dial_tone == None: self.dial_tone = ToneSource(frequency=self.dial_tone_frequency, gain=0.0, ease_time_ms=self.dial_tone_ease_ms, target_frame_ms=self.target_frame_time_ms, codec=Null(), sink=self.receive_mixer)
|
||||
if self.receive_pipeline == None: self.receive_pipeline = Pipeline(source=self.receive_mixer, codec=Null(), sink=self.audio_output)
|
||||
|
||||
@@ -473,6 +543,20 @@ class Telephone(SignallingReceiver):
|
||||
if self.dial_tone and self.dial_tone.running:
|
||||
self.dial_tone.stop()
|
||||
|
||||
def __reconfigure_transmit_pipeline(self):
|
||||
if self.transmit_pipeline and self.call_status == Signalling.STATUS_ESTABLISHED:
|
||||
self.audio_input.stop()
|
||||
self.transmit_mixer.stop()
|
||||
self.transmit_pipeline.stop()
|
||||
self.transmit_mixer = Mixer(target_frame_ms=self.target_frame_time_ms, gain=self.transmit_gain)
|
||||
self.audio_input = LineSource(preferred_device=self.microphone_device, target_frame_ms=self.target_frame_time_ms, codec=Raw(), sink=self.transmit_mixer, filters=self.active_call.filters)
|
||||
self.transmit_pipeline = Pipeline(source=self.transmit_mixer,
|
||||
codec=self.transmit_codec,
|
||||
sink=self.active_call.packetizer)
|
||||
self.transmit_mixer.start()
|
||||
self.audio_input.start()
|
||||
self.transmit_pipeline.start()
|
||||
|
||||
def __open_pipelines(self, identity):
|
||||
with self.pipeline_lock:
|
||||
if not self.active_call.get_remote_identity() == identity:
|
||||
@@ -485,12 +569,16 @@ class Telephone(SignallingReceiver):
|
||||
RNS.log(f"Opening audio pipelines for call with {RNS.prettyhexrep(identity.hash)}", RNS.LOG_DEBUG)
|
||||
if self.active_call.is_incoming: self.signal(Signalling.STATUS_CONNECTING, self.active_call)
|
||||
|
||||
if self.use_agc: self.active_call.filters = [BandPass(250, 8500), AGC()]
|
||||
else: self.active_call.filters = [BandPass(250, 8500)]
|
||||
|
||||
self.__prepare_dialling_pipelines()
|
||||
self.transmit_mixer = Mixer(target_frame_ms=self.target_frame_time_ms)
|
||||
self.audio_input = LineSource(preferred_device=self.microphone_device, target_frame_ms=self.target_frame_time_ms, codec=Raw(), sink=self.transmit_mixer)
|
||||
self.active_call.packetizer = Packetizer(self.active_call, failure_callback=self.__packetizer_failure)
|
||||
self.transmit_mixer = Mixer(target_frame_ms=self.target_frame_time_ms, gain=self.transmit_gain)
|
||||
self.audio_input = LineSource(preferred_device=self.microphone_device, target_frame_ms=self.target_frame_time_ms, codec=Raw(), sink=self.transmit_mixer, filters=self.active_call.filters)
|
||||
self.transmit_pipeline = Pipeline(source=self.transmit_mixer,
|
||||
codec=self.transmit_codec,
|
||||
sink=Packetizer(self.active_call, failure_callback=self.__packetizer_failure))
|
||||
sink=self.active_call.packetizer)
|
||||
|
||||
self.active_call.audio_source = LinkSource(link=self.active_call, signalling_receiver=self, sink=self.receive_mixer)
|
||||
self.receive_mixer.set_source_max_frames(self.active_call.audio_source, 2)
|
||||
@@ -524,6 +612,7 @@ class Telephone(SignallingReceiver):
|
||||
if not self.active_call:
|
||||
self.call_status = Signalling.STATUS_CALLING
|
||||
outgoing_call_timeout = time.time()+self.wait_time
|
||||
outgoing_establishment_timeout = time.time()+self.establishment_timeout
|
||||
call_destination = RNS.Destination(identity, RNS.Destination.OUT, RNS.Destination.SINGLE, APP_NAME, PRIMITIVE_NAME)
|
||||
if not RNS.Transport.has_path(call_destination.hash):
|
||||
RNS.log(f"No path known for call to {RNS.prettyhexrep(call_destination.hash)}, requesting path...", RNS.LOG_DEBUG)
|
||||
@@ -537,11 +626,13 @@ class Telephone(SignallingReceiver):
|
||||
established_callback=self.__outgoing_link_established,
|
||||
closed_callback=self.__outgoing_link_closed)
|
||||
|
||||
self.active_call.is_incoming = False
|
||||
self.active_call.is_outgoing = True
|
||||
self.active_call.ring_timeout = False
|
||||
self.active_call.profile = profile
|
||||
self.active_call.is_incoming = False
|
||||
self.active_call.is_outgoing = True
|
||||
self.active_call.is_terminating = False
|
||||
self.active_call.ring_timeout = False
|
||||
self.active_call.profile = profile
|
||||
self.__timeout_outgoing_call_at(self.active_call, outgoing_call_timeout)
|
||||
self.__timeout_outgoing_establishment_at(self.active_call, outgoing_establishment_timeout)
|
||||
|
||||
def __outgoing_link_established(self, link):
|
||||
RNS.log(f"Link established for call with {link.get_remote_identity()}", RNS.LOG_DEBUG)
|
||||
@@ -559,14 +650,15 @@ class Telephone(SignallingReceiver):
|
||||
return
|
||||
elif signal == Signalling.STATUS_BUSY:
|
||||
RNS.log("Remote is busy, terminating", RNS.LOG_DEBUG)
|
||||
self.active_call.is_terminating = True
|
||||
self.__play_busy_tone()
|
||||
self.__disable_dial_tone()
|
||||
self.hangup()
|
||||
self.hangup(reason=Signalling.STATUS_BUSY)
|
||||
elif signal == Signalling.STATUS_REJECTED:
|
||||
RNS.log("Remote rejected call, terminating", RNS.LOG_DEBUG)
|
||||
self.__play_busy_tone()
|
||||
self.__disable_dial_tone()
|
||||
self.hangup()
|
||||
self.hangup(reason=Signalling.STATUS_REJECTED)
|
||||
elif signal == Signalling.STATUS_AVAILABLE:
|
||||
RNS.log("Line available, sending identification", RNS.LOG_DEBUG)
|
||||
self.call_status = signal
|
||||
@@ -594,8 +686,9 @@ class Telephone(SignallingReceiver):
|
||||
if callable(self.__established_callback): self.__established_callback(self.active_call.get_remote_identity())
|
||||
if self.low_latency_output: self.audio_output.enable_low_latency()
|
||||
elif signal >= Signalling.PREFERRED_PROFILE:
|
||||
self.active_call.profile = signal - Signalling.PREFERRED_PROFILE
|
||||
self.select_call_profile(self.active_call.profile)
|
||||
profile = signal - Signalling.PREFERRED_PROFILE
|
||||
if self.active_call and self.call_status == Signalling.STATUS_ESTABLISHED: self.switch_profile(profile, from_signalling=True)
|
||||
else: self.__select_call_profile(profile)
|
||||
|
||||
def __str__(self):
|
||||
return f"<lxst.telephony/{RNS.hexrep(self.identity.hash, delimit=False)}>"
|
||||
@@ -152,7 +152,7 @@ class LineSource(LocalSource):
|
||||
MAX_FRAMES = 128
|
||||
DEFAULT_FRAME_MS = 80
|
||||
|
||||
def __init__(self, preferred_device=None, target_frame_ms=DEFAULT_FRAME_MS, codec=None, sink=None):
|
||||
def __init__(self, preferred_device=None, target_frame_ms=DEFAULT_FRAME_MS, codec=None, sink=None, filters=None):
|
||||
self.preferred_device = preferred_device
|
||||
self.frame_deque = deque(maxlen=self.MAX_FRAMES)
|
||||
self.target_frame_ms = target_frame_ms
|
||||
@@ -165,6 +165,11 @@ class LineSource(LocalSource):
|
||||
self._codec = None
|
||||
self.codec = codec
|
||||
self.sink = sink
|
||||
self.filters = None
|
||||
|
||||
if filters != None:
|
||||
if type(filters) == list: self.filters = filters
|
||||
else: self.filters = [filters]
|
||||
|
||||
@property
|
||||
def codec(self): return self._codec
|
||||
@@ -218,6 +223,8 @@ class LineSource(LocalSource):
|
||||
with self.backend.get_recorder(samples_per_frame=backend_samples_per_frame) as recorder:
|
||||
while self.should_run:
|
||||
frame_samples = recorder.record(numframes=self.samples_per_frame)
|
||||
if self.filters != None:
|
||||
for f in self.filters: frame_samples = f.handle_frame(frame_samples, self.samplerate)
|
||||
if self.codec:
|
||||
frame = self.codec.encode(frame_samples)
|
||||
if self.sink and self.sink.can_receive(from_source=self):
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "0.4.1"
|
||||
__version__ = "0.4.2"
|
||||
|
||||
17
Makefile
17
Makefile
@@ -2,8 +2,9 @@ all: release
|
||||
|
||||
clean:
|
||||
@echo Cleaning...
|
||||
-rm -r ./build
|
||||
-rm -r ./dist
|
||||
-sudo rm -rf ./build
|
||||
-rm -rf ./dist
|
||||
-rm -r ./LXST/__pycache__
|
||||
|
||||
remove_symlinks:
|
||||
@echo Removing symlinks for build...
|
||||
@@ -18,7 +19,19 @@ create_symlinks:
|
||||
-ln -s ../LXST/ ./examples/LXST
|
||||
|
||||
build_wheel:
|
||||
cp ./lib/0.4.2/* ./LXST/
|
||||
python3 setup.py sdist bdist_wheel
|
||||
-(rm ./LXST/*.so)
|
||||
-(rm ./LXST/*.dll)
|
||||
-(rm ./LXST/*.dylib)
|
||||
|
||||
native_libs:
|
||||
./march_build.sh
|
||||
|
||||
persist_libs:
|
||||
-cp ./libs/dev/*.so ./libs/static/
|
||||
-cp ./libs/dev/*.dll ./libs/static/
|
||||
-cp ./libs/dev/*.dylib ./libs/static/
|
||||
|
||||
release: remove_symlinks build_wheel create_symlinks
|
||||
|
||||
|
||||
26
build_wheels.sh
Executable file
26
build_wheels.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
# This script is used to run the binary wheel builds
|
||||
# inside docker containers for multi-arch compilation
|
||||
set -e -x
|
||||
|
||||
yum install -y gcc gcc-c++ make codec2-devel codec2
|
||||
|
||||
PYTHON_VERSIONS=(
|
||||
"cp311-cp311"
|
||||
"cp312-cp312"
|
||||
"cp313-cp313"
|
||||
"cp314-cp314"
|
||||
)
|
||||
|
||||
for PY_TAG in "${PYTHON_VERSIONS[@]}"; do
|
||||
PYBIN="/opt/python/${PY_TAG}/bin"
|
||||
|
||||
if [ ! -d "$PYBIN" ]; then
|
||||
echo "Python version not found: $PYBIN"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Building with: $PYBIN"
|
||||
"${PYBIN}/pip" install cffi
|
||||
"${PYBIN}/pip" wheel /io/ -w wheelhouse/
|
||||
done
|
||||
23
examples/filters.py
Normal file
23
examples/filters.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import RNS
|
||||
import LXST
|
||||
import sys
|
||||
import time
|
||||
|
||||
from LXST.Filters import BandPass, AGC
|
||||
|
||||
target_frame_ms = 40
|
||||
raw = LXST.Codecs.Raw()
|
||||
|
||||
filters = [BandPass(200, 8500), AGC()]
|
||||
|
||||
line_sink = LXST.Sinks.LineSink()
|
||||
mixer = LXST.Mixer(target_frame_ms=target_frame_ms, sink=line_sink)
|
||||
line_source = LXST.Sources.LineSource(target_frame_ms=target_frame_ms, codec=raw, sink=mixer, filters=filters)
|
||||
|
||||
mixer.start()
|
||||
line_source.start()
|
||||
print("Hit enter stop"); input()
|
||||
|
||||
line_source.stop()
|
||||
|
||||
time.sleep(0.5)
|
||||
10
fetch_libs.sh
Executable file
10
fetch_libs.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
TARGET_DIR="${1:-./lib/dev}"
|
||||
mkdir -p "$TARGET_DIR"
|
||||
|
||||
echo "Copying native libraries to $TARGET_DIR..."
|
||||
find ./build -name "filterlib*.*" -type f \( -name "*.so" -o -name "*.dll" -o -name "*.dylib" \) -exec cp {} "$TARGET_DIR/" \;
|
||||
|
||||
echo "Done. Copied files:"
|
||||
ls -lh "$TARGET_DIR/"
|
||||
12
march_build.sh
Executable file
12
march_build.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker run --rm -v $(pwd):/io quay.io/pypa/manylinux_2_34_x86_64 /io/build_wheels.sh
|
||||
docker run --rm -v $(pwd):/io quay.io/pypa/musllinux_1_2_x86_64 /io/build_wheels.sh
|
||||
docker run --rm -v $(pwd):/io quay.io/pypa/manylinux_2_31_armv7l /io/build_wheels.sh
|
||||
docker run --rm -v $(pwd):/io quay.io/pypa/manylinux_2_34_aarch64 /io/build_wheels.sh
|
||||
docker run --rm -v $(pwd):/io quay.io/pypa/manylinux_2_39_riscv64 /io/build_wheels.sh
|
||||
docker run --rm -v $(pwd):/io quay.io/pypa/musllinux_1_2_aarch64 /io/build_wheels.sh
|
||||
docker run --rm -v $(pwd):/io quay.io/pypa/musllinux_1_2_armv7l /io/build_wheels.sh
|
||||
docker run --rm -v $(pwd):/io quay.io/pypa/musllinux_1_2_riscv64 /io/build_wheels.sh
|
||||
|
||||
./fetch_libs.sh
|
||||
29
setup.py
29
setup.py
@@ -1,10 +1,17 @@
|
||||
import setuptools
|
||||
from setuptools import setup, Extension
|
||||
from setuptools.command.build_ext import build_ext
|
||||
import os
|
||||
import platform
|
||||
|
||||
with open("README.md", "r") as fh:
|
||||
long_description = fh.read()
|
||||
BUILD_EXTENSIONS = True
|
||||
|
||||
with open("README.md", "r") as fh: long_description = fh.read()
|
||||
exec(open("LXST/_version.py", "r").read())
|
||||
|
||||
if BUILD_EXTENSIONS: extensions = [ Extension("LXST.filterlib", sources=["LXST/Filters.c"], include_dirs=["LXST"], language="c"), ]
|
||||
else: extensions = []
|
||||
|
||||
packages = setuptools.find_packages(exclude=[])
|
||||
packages.append("LXST.Utilities")
|
||||
packages.append("LXST.Primitives.hardware")
|
||||
@@ -16,6 +23,13 @@ package_data = {
|
||||
"Codecs/libs/pyogg/libs/win_amd64/*",
|
||||
"Codecs/libs/pyogg/libs/macos/*",
|
||||
"Sounds/*",
|
||||
],
|
||||
"LXST": [
|
||||
"Filters.h",
|
||||
"Filters.c",
|
||||
"filterlib*.so",
|
||||
"filterlib*.dll",
|
||||
"filterlib*.dylib",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -30,6 +44,8 @@ setuptools.setup(
|
||||
url="https://git.unsigned.io/markqvist/lxst",
|
||||
packages=packages,
|
||||
package_data=package_data,
|
||||
ext_modules=extensions,
|
||||
cmdclass={"build_ext": build_ext},
|
||||
classifiers=[
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: Other/Proprietary License",
|
||||
@@ -40,11 +56,12 @@ setuptools.setup(
|
||||
'rnphone=LXST.Utilities.rnphone:main',
|
||||
]
|
||||
},
|
||||
install_requires=["rns>=1.0.3",
|
||||
"lxmf>=0.9.1",
|
||||
install_requires=["rns>=1.0.4",
|
||||
"lxmf>=0.9.3",
|
||||
"soundcard>=0.4.5",
|
||||
"numpy>=2.3.4",
|
||||
"pycodec2>=4.1.0",
|
||||
"audioop-lts>=0.2.1;python_version>='3.13'"],
|
||||
python_requires=">=3.7",
|
||||
"audioop-lts>=0.2.1;python_version>='3.13'",
|
||||
"cffi>=1.17.1"],
|
||||
python_requires=">=3.11",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user