mirror of
https://github.com/markqvist/LXST.git
synced 2026-04-27 14:20:39 +00:00
Sync upstream
This commit is contained in:
parent
7d5f390a90
commit
68c2cf489c
10 changed files with 405 additions and 96 deletions
|
|
@ -40,85 +40,73 @@ class Opus(Codec):
|
|||
self.output_bitrate = 0
|
||||
self.set_profile(profile)
|
||||
|
||||
@staticmethod
|
||||
def profile_channels(profile):
|
||||
if profile == Opus.PROFILE_VOICE_LOW: return 1
|
||||
elif profile == Opus.PROFILE_VOICE_MEDIUM: return 1
|
||||
elif profile == Opus.PROFILE_VOICE_HIGH: return 1
|
||||
elif profile == Opus.PROFILE_VOICE_MAX: return 2
|
||||
elif profile == Opus.PROFILE_AUDIO_MIN: return 1
|
||||
elif profile == Opus.PROFILE_AUDIO_LOW: return 1
|
||||
elif profile == Opus.PROFILE_AUDIO_MEDIUM: return 2
|
||||
elif profile == Opus.PROFILE_AUDIO_HIGH: return 2
|
||||
elif profile == Opus.PROFILE_AUDIO_MAX: return 2
|
||||
else: raise CodecError(f"Unsupported profile")
|
||||
|
||||
@staticmethod
|
||||
def profile_samplerate(profile):
|
||||
if profile == Opus.PROFILE_VOICE_LOW: return 8000
|
||||
elif profile == Opus.PROFILE_VOICE_MEDIUM: return 24000
|
||||
elif profile == Opus.PROFILE_VOICE_HIGH: return 48000
|
||||
elif profile == Opus.PROFILE_VOICE_MAX: return 48000
|
||||
elif profile == Opus.PROFILE_AUDIO_MIN: return 8000
|
||||
elif profile == Opus.PROFILE_AUDIO_LOW: return 12000
|
||||
elif profile == Opus.PROFILE_AUDIO_MEDIUM: return 24000
|
||||
elif profile == Opus.PROFILE_AUDIO_HIGH: return 48000
|
||||
elif profile == Opus.PROFILE_AUDIO_MAX: return 48000
|
||||
else: raise CodecError(f"Unsupported profile")
|
||||
|
||||
@staticmethod
|
||||
def profile_application(profile):
|
||||
if profile == Opus.PROFILE_VOICE_LOW: return "voip"
|
||||
elif profile == Opus.PROFILE_VOICE_MEDIUM: return "voip"
|
||||
elif profile == Opus.PROFILE_VOICE_HIGH: return "voip"
|
||||
elif profile == Opus.PROFILE_VOICE_MAX: return "voip"
|
||||
elif profile == Opus.PROFILE_AUDIO_MIN: return "audio"
|
||||
elif profile == Opus.PROFILE_AUDIO_LOW: return "audio"
|
||||
elif profile == Opus.PROFILE_AUDIO_MEDIUM: return "audio"
|
||||
elif profile == Opus.PROFILE_AUDIO_HIGH: return "audio"
|
||||
elif profile == Opus.PROFILE_AUDIO_MAX: return "audio"
|
||||
else: raise CodecError(f"Unsupported profile")
|
||||
|
||||
@staticmethod
|
||||
def profile_bitrate_ceiling(profile):
|
||||
if profile == Opus.PROFILE_VOICE_LOW: return 6000
|
||||
elif profile == Opus.PROFILE_VOICE_MEDIUM: return 8000
|
||||
elif profile == Opus.PROFILE_VOICE_HIGH: return 16000
|
||||
elif profile == Opus.PROFILE_VOICE_MAX: return 32000
|
||||
elif profile == Opus.PROFILE_AUDIO_MIN: return 8000
|
||||
elif profile == Opus.PROFILE_AUDIO_LOW: return 14000
|
||||
elif profile == Opus.PROFILE_AUDIO_MEDIUM: return 28000
|
||||
elif profile == Opus.PROFILE_AUDIO_HIGH: return 56000
|
||||
elif profile == Opus.PROFILE_AUDIO_MAX: return 128000
|
||||
else: raise CodecError(f"Unsupported profile")
|
||||
|
||||
@staticmethod
|
||||
def max_bytes_per_frame(bitrate_ceiling, frame_duration_ms):
|
||||
return math.ceil((bitrate_ceiling/8)*(frame_duration_ms/1000))
|
||||
|
||||
def set_profile(self, profile):
|
||||
if profile == self.PROFILE_VOICE_LOW:
|
||||
self.profile = profile
|
||||
self.channels = 1
|
||||
self.input_channels = self.channels
|
||||
self.output_samplerate = 8000
|
||||
self.opus_encoder.set_application("voip")
|
||||
elif profile == self.PROFILE_VOICE_MEDIUM:
|
||||
self.profile = profile
|
||||
self.channels = 1
|
||||
self.input_channels = self.channels
|
||||
self.output_samplerate = 24000
|
||||
self.opus_encoder.set_application("voip")
|
||||
elif profile == self.PROFILE_VOICE_HIGH:
|
||||
self.profile = profile
|
||||
self.channels = 1
|
||||
self.input_channels = self.channels
|
||||
self.output_samplerate = 48000
|
||||
self.opus_encoder.set_application("voip")
|
||||
elif profile == self.PROFILE_VOICE_MAX:
|
||||
self.profile = profile
|
||||
self.channels = 2
|
||||
self.input_channels = self.channels
|
||||
self.output_samplerate = 48000
|
||||
self.opus_encoder.set_application("voip")
|
||||
elif profile == self.PROFILE_AUDIO_MIN:
|
||||
self.profile = profile
|
||||
self.channels = 1
|
||||
self.input_channels = self.channels
|
||||
self.output_samplerate = 8000
|
||||
self.opus_encoder.set_application("audio")
|
||||
elif profile == self.PROFILE_AUDIO_LOW:
|
||||
self.profile = profile
|
||||
self.channels = 1
|
||||
self.input_channels = self.channels
|
||||
self.output_samplerate = 12000
|
||||
self.opus_encoder.set_application("audio")
|
||||
elif profile == self.PROFILE_AUDIO_MEDIUM:
|
||||
self.profile = profile
|
||||
self.channels = 2
|
||||
self.input_channels = self.channels
|
||||
self.output_samplerate = 24000
|
||||
self.opus_encoder.set_application("audio")
|
||||
elif profile == self.PROFILE_AUDIO_HIGH:
|
||||
self.profile = profile
|
||||
self.channels = 2
|
||||
self.input_channels = self.channels
|
||||
self.output_samplerate = 48000
|
||||
self.opus_encoder.set_application("audio")
|
||||
elif profile == self.PROFILE_AUDIO_MAX:
|
||||
self.profile = profile
|
||||
self.channels = 2
|
||||
self.input_channels = self.channels
|
||||
self.output_samplerate = 48000
|
||||
self.opus_encoder.set_application("audio")
|
||||
else:
|
||||
raise CodecError(f"Unsupported profile configured for {self}")
|
||||
self.channels = self.profile_channels(profile)
|
||||
self.input_channels = self.channels
|
||||
self.output_samplerate = self.profile_samplerate(profile)
|
||||
self.opus_encoder.set_application(self.profile_application(profile))
|
||||
self.profile = profile
|
||||
|
||||
def update_bitrate(self, frame_duration_ms):
|
||||
if self.profile == self.PROFILE_VOICE_LOW:
|
||||
self.bitrate_ceiling = 6000
|
||||
elif self.profile == self.PROFILE_VOICE_MEDIUM:
|
||||
self.bitrate_ceiling = 8000
|
||||
elif self.profile == self.PROFILE_VOICE_HIGH:
|
||||
self.bitrate_ceiling = 16000
|
||||
elif self.profile == self.PROFILE_VOICE_MAX:
|
||||
self.bitrate_ceiling = 32000
|
||||
elif self.profile == self.PROFILE_AUDIO_MIN:
|
||||
self.bitrate_ceiling = 8000
|
||||
elif self.profile == self.PROFILE_AUDIO_LOW:
|
||||
self.bitrate_ceiling = 14000
|
||||
elif self.profile == self.PROFILE_AUDIO_MEDIUM:
|
||||
self.bitrate_ceiling = 28000
|
||||
elif self.profile == self.PROFILE_AUDIO_HIGH:
|
||||
self.bitrate_ceiling = 56000
|
||||
elif self.profile == self.PROFILE_AUDIO_MAX:
|
||||
self.bitrate_ceiling = 128000
|
||||
self.bitrate_ceiling = self.profile_bitrate_ceiling(self.profile)
|
||||
max_bytes_per_frame = self.max_bytes_per_frame(self.bitrate_ceiling, frame_duration_ms)
|
||||
|
||||
max_bytes_per_frame = math.ceil((self.bitrate_ceiling/8)*(frame_duration_ms/1000))
|
||||
configured_bitrate = (max_bytes_per_frame*8)/(frame_duration_ms/1000)
|
||||
self.opus_encoder.set_max_bytes_per_frame(max_bytes_per_frame)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,12 +18,10 @@ class Pipeline():
|
|||
self.source.sink = sink
|
||||
self.codec = codec
|
||||
|
||||
if isinstance(sink, Loopback):
|
||||
sink.samplerate = source.samplerate
|
||||
if isinstance(source, Loopback):
|
||||
source._sink = sink
|
||||
if isinstance(sink, Packetizer):
|
||||
sink.source = source
|
||||
if isinstance(sink, Loopback): sink.samplerate = source.samplerate
|
||||
if isinstance(source, Loopback): source._sink = sink
|
||||
if isinstance(sink, Packetizer): sink.source = source
|
||||
if isinstance(sink, OpusFileSink): sink.source = source
|
||||
|
||||
@property
|
||||
def codec(self):
|
||||
|
|
|
|||
71
LXST/Primitives/Players.py
Normal file
71
LXST/Primitives/Players.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import LXST
|
||||
import time
|
||||
import threading
|
||||
import os
|
||||
|
||||
from LXST.Sinks import LineSink
|
||||
from LXST.Sources import OpusFileSource
|
||||
|
||||
class FilePlayer():
|
||||
def __init__(self, path=None, device=None, loop=False):
|
||||
self._file_path = path
|
||||
self._playback_device = None
|
||||
self.__finished_callback = None
|
||||
self.__loop = loop
|
||||
self.__source = None
|
||||
self.__sink = LineSink(self._playback_device)
|
||||
self.__raw = LXST.Codecs.Raw()
|
||||
self.__loopback = LXST.Sources.Loopback()
|
||||
self.__output_pipeline = LXST.Pipeline(source=self.__loopback, codec=self.__raw, sink=self.__sink)
|
||||
self.__input_pipeline = None
|
||||
if path: self.set_source(self._file_path)
|
||||
|
||||
@property
|
||||
def running(self):
|
||||
if not self.__source: return False
|
||||
else: return self.__source.should_run
|
||||
|
||||
@property
|
||||
def playing(self): return self.running
|
||||
|
||||
@property
|
||||
def finished_callback(self): return self.__finished_callback
|
||||
|
||||
@finished_callback.setter
|
||||
def finished_callback(self, callback):
|
||||
if callback == None: self.__finished_callback = None
|
||||
elif not callable(callback): raise TypeError("Provided callback is not callable")
|
||||
else: self.__finished_callback = callback
|
||||
|
||||
def __callback_job(self):
|
||||
if self.__finished_callback:
|
||||
time.sleep(0.2)
|
||||
while self.running: time.sleep(0.1)
|
||||
self.__finished_callback(self)
|
||||
|
||||
def set_source(self, path=None):
|
||||
if not path: return
|
||||
else:
|
||||
if not os.path.isfile(path): raise OSError(f"File not found: {path}")
|
||||
else:
|
||||
self.__source = OpusFileSource(path, loop=self.__loop)
|
||||
self.__input_pipeline = LXST.Pipeline(source=self.__source, codec=self.__raw, sink=self.__loopback)
|
||||
|
||||
def loop(self, loop=True):
|
||||
if loop == True: self.__loop = True
|
||||
else: self.__loop = False
|
||||
if self.__source: self.__source.loop = self.__loop
|
||||
|
||||
def start(self):
|
||||
if not self.running and self.__source:
|
||||
self.__input_pipeline.start()
|
||||
self.__output_pipeline.start()
|
||||
if self.__finished_callback:
|
||||
threading.Thread(target=self.__callback_job, daemon=True).start()
|
||||
|
||||
def stop(self):
|
||||
if self.running and self.__source:
|
||||
self.__input_pipeline.stop()
|
||||
self.__output_pipeline.stop()
|
||||
|
||||
def play(self): self.start()
|
||||
53
LXST/Primitives/Recorders.py
Normal file
53
LXST/Primitives/Recorders.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import RNS
|
||||
import LXST
|
||||
import time
|
||||
import os
|
||||
from LXST.Sources import LineSource
|
||||
from LXST.Sinks import OpusFileSink
|
||||
from LXST.Filters import BandPass, AGC
|
||||
|
||||
class FileRecorder():
|
||||
def __init__(self, path=None, device=None, profile=LXST.Codecs.Opus.PROFILE_AUDIO_MAX,
|
||||
gain=0.0, ease_in=0.125, skip=0.075, filters=[BandPass(25, 24000)]):
|
||||
self._file_path = path
|
||||
self._record_device = device
|
||||
self.__profile = profile
|
||||
self.__source = None
|
||||
self.__sink = OpusFileSink(path=self._file_path, profile=profile)
|
||||
self.__null = LXST.Codecs.Null()
|
||||
self.__filters = filters
|
||||
self.__ease_in = ease_in
|
||||
self.__skip = skip
|
||||
self.__gain = gain
|
||||
self.set_source(device)
|
||||
|
||||
@property
|
||||
def running(self):
|
||||
if not self.__source: return False
|
||||
else: return self.__source.should_run
|
||||
|
||||
@property
|
||||
def recording(self): return self.running
|
||||
|
||||
def set_source(self, device=None):
|
||||
self._record_device = device
|
||||
self.__source = LineSource(preferred_device=self._record_device, target_frame_ms=20, codec=self.__null, sink=self.__sink,
|
||||
gain=self.__gain, ease_in=self.__ease_in, skip=self.__skip, filters=self.__filters)
|
||||
self.__sink.source = self.__source
|
||||
|
||||
def set_output_path(self, path):
|
||||
self._file_path = path
|
||||
self.__sink.__output_path = path
|
||||
|
||||
def start(self):
|
||||
if self.__source:
|
||||
self.__source.start()
|
||||
|
||||
def stop(self):
|
||||
if self.__source:
|
||||
self.__source.stop()
|
||||
while self.__sink.frames_waiting: time.sleep(0.1)
|
||||
self.__sink.stop()
|
||||
|
||||
def record(self):
|
||||
self.start()
|
||||
|
|
@ -604,7 +604,7 @@ 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()]
|
||||
if self.use_agc: self.active_call.filters = [BandPass(250, 8500), AGC(target_level=-15.0)]
|
||||
else: self.active_call.filters = [BandPass(250, 8500)]
|
||||
|
||||
self.__prepare_dialling_pipelines()
|
||||
|
|
|
|||
133
LXST/Sinks.py
133
LXST/Sinks.py
|
|
@ -2,7 +2,10 @@ import RNS
|
|||
import math
|
||||
import time
|
||||
import threading
|
||||
import numpy as np
|
||||
from collections import deque
|
||||
from LXST.Codecs import Opus
|
||||
from LXST.Codecs.Codec import resample_bytes, resample
|
||||
|
||||
class LinuxBackend():
|
||||
SAMPLERATE = 48000
|
||||
|
|
@ -119,7 +122,6 @@ class LineSink(LocalSink):
|
|||
|
||||
def __init__(self, preferred_device=None, autodigest=True, low_latency=False):
|
||||
self.preferred_device = preferred_device
|
||||
self.frame_deque = deque(maxlen=self.MAX_FRAMES)
|
||||
self.should_run = False
|
||||
self.digest_thread = None
|
||||
self.digest_lock = threading.Lock()
|
||||
|
|
@ -215,4 +217,131 @@ class LineSink(LocalSink):
|
|||
|
||||
self.backend.release_player()
|
||||
|
||||
class PacketSink(RemoteSink): pass
|
||||
class OpusFileSink(LocalSink):
|
||||
MAX_FRAMES = 64
|
||||
AUTOSTART_MIN = 1
|
||||
FINALIZE_TIMEOUT = 2
|
||||
TYPE_MAP_FACTOR = np.iinfo("int16").max
|
||||
|
||||
def __init__(self, path=None, autodigest=True, profile=Opus.PROFILE_AUDIO_MAX):
|
||||
self.should_run = False
|
||||
self.digest_thread = None
|
||||
self.digest_lock = threading.Lock()
|
||||
self.insert_lock = threading.Lock()
|
||||
self.frame_deque = deque(maxlen=self.MAX_FRAMES)
|
||||
self.autodigest = autodigest
|
||||
self.autostart_min = self.AUTOSTART_MIN
|
||||
self.buffer_max_height = self.MAX_FRAMES
|
||||
self.profile = profile
|
||||
self.bitdepth = 32
|
||||
self.samplerate = None
|
||||
self.output_samplerate = Opus.profile_samplerate(self.profile)
|
||||
self.channels = Opus.profile_channels(self.profile)
|
||||
self.application = Opus.profile_application(self.profile)
|
||||
self.bitrate_ceiling = Opus.profile_bitrate_ceiling(self.profile)
|
||||
self.max_bytes_per_frame = None
|
||||
self.samples_per_frame = None
|
||||
self.frame_time = None
|
||||
self.output_latency = 0
|
||||
self.max_latency = 0
|
||||
self.underrun_at = None
|
||||
self.samples_written = 0
|
||||
self.__recording_stopped = False
|
||||
self.__finalized = False
|
||||
self.__opus_writer = None
|
||||
self.__encoder = None
|
||||
self.__output_path = path
|
||||
|
||||
@property
|
||||
def frames_waiting(self): return len(self.frame_deque)
|
||||
|
||||
def can_receive(self, from_source=None):
|
||||
with self.insert_lock:
|
||||
if self.__recording_stopped: return False
|
||||
if len(self.frame_deque) < self.buffer_max_height: return True
|
||||
else: return False
|
||||
|
||||
def handle_frame(self, frame, source=None):
|
||||
with self.insert_lock:
|
||||
self.frame_deque.append(frame)
|
||||
if self.samples_per_frame == None:
|
||||
self.samplerate = source.samplerate
|
||||
self.samples_per_frame = frame.shape[0]
|
||||
self.frame_time = self.samples_per_frame*(1/self.samplerate)
|
||||
if not self.channels: self.channels = frame.shape[1]
|
||||
RNS.log(f"{self} starting at {self.samples_per_frame} samples per frame, {self.channels} channels", RNS.LOG_DEBUG)
|
||||
|
||||
if self.autodigest and not self.should_run:
|
||||
if len(self.frame_deque) >= self.autostart_min: self.start()
|
||||
|
||||
def start(self):
|
||||
if not self.should_run:
|
||||
self.should_run = True
|
||||
self.digest_thread = threading.Thread(target=self.__digest_job, daemon=True)
|
||||
self.digest_thread.start()
|
||||
|
||||
def stop(self):
|
||||
if self.should_run:
|
||||
self.__recording_stopped = True
|
||||
timeout = time.time()+self.FINALIZE_TIMEOUT
|
||||
while len(self.frame_deque) > 0 and time.time() < timeout: time.sleep(0.05)
|
||||
self.should_run = False
|
||||
while self.__finalized == False: time.sleep(0.05)
|
||||
self.__opus_writer.close()
|
||||
self.__opus_writer = None
|
||||
|
||||
def __digest_job(self):
|
||||
with self.digest_lock:
|
||||
from .Codecs.libs.pyogg import OpusBufferedEncoder
|
||||
from .Codecs.libs.pyogg import OggOpusWriter
|
||||
|
||||
if not self.__output_path: raise ValueError("No recording file path configured")
|
||||
self.max_bytes_per_frame = Opus.max_bytes_per_frame(self.bitrate_ceiling, self.frame_time*1000)
|
||||
self.__encoder = OpusBufferedEncoder()
|
||||
self.__encoder.set_application(self.application)
|
||||
self.__encoder.set_sampling_frequency(self.samplerate)
|
||||
self.__encoder.set_channels(self.channels)
|
||||
self.__encoder.set_frame_size(int(self.frame_time*1000))
|
||||
self.__encoder.set_max_bytes_per_frame(self.max_bytes_per_frame)
|
||||
self.__opus_writer = OggOpusWriter(self.__output_path, self.__encoder)
|
||||
|
||||
final_silence_frames = 10
|
||||
while self.should_run or final_silence_frames > 0:
|
||||
frames_ready = len(self.frame_deque) or (self.should_run == False and final_silence_frames)
|
||||
if frames_ready:
|
||||
self.output_latency = len(self.frame_deque)*self.frame_time
|
||||
self.max_latency = self.buffer_max_height*self.frame_time
|
||||
self.underrun_at = None
|
||||
|
||||
if self.should_run:
|
||||
with self.insert_lock: frame = self.frame_deque.popleft()
|
||||
else:
|
||||
final_silence_frames -= 1
|
||||
frame = np.zeros((self.samples_per_frame, self.channels), dtype="float32")
|
||||
|
||||
if frame.shape[1] > self.channels: frame = frame[:, 0:self.channels]
|
||||
elif frame.shape[1] < self.channels:
|
||||
for i in range(self.channels - frame.shape[1]):
|
||||
frame = np.hstack([frame, frame[:, -1:]])
|
||||
|
||||
if frame.shape[0] < self.samples_per_frame:
|
||||
RNS.log("Insufficient frame data, padding with silence", RNS.LOG_DEBUG)
|
||||
silence_frame = np.zeros((self.samples_per_frame-frame.shape[0], frame.shape[1]), dtype=frame.dtype)
|
||||
frame = np.vstack([frame, silence_frame])
|
||||
|
||||
self.samples_written += frame.shape[0]
|
||||
|
||||
if self.samplerate != 48000:
|
||||
frame = resample(frame, self.bitdepth, self.channels, self.samplerate, self.output_samplerate)
|
||||
|
||||
input_samples = frame*self.TYPE_MAP_FACTOR
|
||||
input_samples = input_samples.astype(np.int16)
|
||||
|
||||
self.__opus_writer.write(bytearray(input_samples.tobytes()))
|
||||
|
||||
if len(self.frame_deque) > self.buffer_max_height: RNS.log(f"Buffer lag on {self} (height {len(self.frame_deque)})", RNS.LOG_DEBUG)
|
||||
else:
|
||||
if self.underrun_at == None: self.underrun_at = time.time()
|
||||
else: time.sleep(self.frame_time*0.1)
|
||||
|
||||
self.__finalized = True
|
||||
|
|
|
|||
|
|
@ -160,7 +160,10 @@ 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, filters=None):
|
||||
@staticmethod
|
||||
def linear_gain(gain_db): return 10**(gain_db/10)
|
||||
|
||||
def __init__(self, preferred_device=None, target_frame_ms=DEFAULT_FRAME_MS, codec=None, sink=None, filters=None, gain=0.0, ease_in=0.0, skip=0.0):
|
||||
self.preferred_device = preferred_device
|
||||
self.frame_deque = deque(maxlen=self.MAX_FRAMES)
|
||||
self.target_frame_ms = target_frame_ms
|
||||
|
|
@ -174,11 +177,18 @@ class LineSource(LocalSource):
|
|||
self.codec = codec
|
||||
self.sink = sink
|
||||
self.filters = None
|
||||
self.ease_in = ease_in
|
||||
self.gain = gain
|
||||
self.__skip = skip
|
||||
self.__gain = self.linear_gain(self.gain)
|
||||
self.__target_gain = self.__gain
|
||||
|
||||
if filters != None:
|
||||
if type(filters) == list: self.filters = filters
|
||||
else: self.filters = [filters]
|
||||
|
||||
if self.ease_in != 0.0: self.__gain = 0.0
|
||||
|
||||
@property
|
||||
def codec(self): return self._codec
|
||||
|
||||
|
|
@ -229,18 +239,34 @@ class LineSource(LocalSource):
|
|||
else: backend_samples_per_frame = None
|
||||
|
||||
with self.backend.get_recorder(samples_per_frame=backend_samples_per_frame) as recorder:
|
||||
started = time.time()
|
||||
ease_in_completed = True if self.ease_in <= 0.0 else False
|
||||
skip_completed = True if self.__skip <= 0.0 else False
|
||||
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):
|
||||
self.sink.handle_frame(frame, self)
|
||||
if not skip_completed:
|
||||
if time.time()-started > self.__skip:
|
||||
skip_completed = True
|
||||
started = time.time()
|
||||
else:
|
||||
if self.filters != None:
|
||||
for f in self.filters: frame_samples = f.handle_frame(frame_samples, self.samplerate)
|
||||
if self.__gain != 1.0: frame_samples *= self.__gain
|
||||
if self.codec:
|
||||
frame = self.codec.encode(frame_samples)
|
||||
if self.sink and self.sink.can_receive(from_source=self):
|
||||
self.sink.handle_frame(frame, self)
|
||||
if not ease_in_completed:
|
||||
d = time.time()-started
|
||||
self.__gain = (d/self.ease_in)*self.__target_gain
|
||||
if self.__gain >= self.__target_gain:
|
||||
self.__gain = self.__target_gain
|
||||
ease_in_completed = True
|
||||
|
||||
|
||||
class OpusFileSource(LocalSource):
|
||||
MAX_FRAMES = 128
|
||||
DEFAULT_FRAME_MS = 70
|
||||
DEFAULT_FRAME_MS = 100
|
||||
TYPE_MAP_FACTOR = np.iinfo("int16").max
|
||||
|
||||
def __init__(self, file_path, target_frame_ms=DEFAULT_FRAME_MS, loop=False, codec=None, sink=None, timed=False):
|
||||
|
|
@ -332,5 +358,3 @@ class OpusFileSource(LocalSource):
|
|||
self.sink.handle_frame(frame, self)
|
||||
else:
|
||||
time.sleep(self.frame_time*0.1)
|
||||
|
||||
class PacketSource(RemoteSource): pass
|
||||
|
|
@ -1 +1 @@
|
|||
__version__ = "0.4.3"
|
||||
__version__ = "0.4.4"
|
||||
|
|
|
|||
22
examples/fileplayer.py
Normal file
22
examples/fileplayer.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import sys
|
||||
import time
|
||||
import select
|
||||
from LXST.Primitives.Players import FilePlayer
|
||||
|
||||
loop = False
|
||||
|
||||
if loop:
|
||||
player = FilePlayer("./docs/speech_stereo.opus", loop=True)
|
||||
player.start()
|
||||
|
||||
while player.running:
|
||||
i, o, e = select.select([sys.stdin], [], [], 1.0)
|
||||
if (i): player.stop()
|
||||
|
||||
else:
|
||||
player = FilePlayer("./docs/speech_stereo.opus")
|
||||
player.start()
|
||||
|
||||
while player.running: time.sleep(0.1)
|
||||
|
||||
print("Playback finished")
|
||||
24
examples/filerecorder.py
Normal file
24
examples/filerecorder.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import sys
|
||||
import time
|
||||
import select
|
||||
from LXST.Primitives.Recorders import FileRecorder
|
||||
|
||||
filename = "recording.opus"
|
||||
|
||||
# With default profile (maximum quality)
|
||||
recorder = FileRecorder(filename)
|
||||
|
||||
# Or, with specific profile
|
||||
# from LXST.Codecs import Opus
|
||||
# recorder = FileRecorder(filename, profile=Opus.PROFILE_VOICE_MEDIUM)
|
||||
|
||||
recorder.start()
|
||||
print("Recording started")
|
||||
|
||||
try: input()
|
||||
except KeyboardInterrupt: pass
|
||||
|
||||
recorder.stop()
|
||||
print(f"Recording saved to {filename}")
|
||||
|
||||
time.sleep(0.2)
|
||||
Loading…
Add table
Add a link
Reference in a new issue