Skip to content

Commit 68c2cf4

Browse files
committed
Sync upstream
1 parent 7d5f390 commit 68c2cf4

File tree

10 files changed

+405
-96
lines changed

10 files changed

+405
-96
lines changed

LXST/Codecs/Opus.py

Lines changed: 64 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -40,85 +40,73 @@ def __init__(self, profile=PROFILE_VOICE_LOW):
4040
self.output_bitrate = 0
4141
self.set_profile(profile)
4242

43+
@staticmethod
44+
def profile_channels(profile):
45+
if profile == Opus.PROFILE_VOICE_LOW: return 1
46+
elif profile == Opus.PROFILE_VOICE_MEDIUM: return 1
47+
elif profile == Opus.PROFILE_VOICE_HIGH: return 1
48+
elif profile == Opus.PROFILE_VOICE_MAX: return 2
49+
elif profile == Opus.PROFILE_AUDIO_MIN: return 1
50+
elif profile == Opus.PROFILE_AUDIO_LOW: return 1
51+
elif profile == Opus.PROFILE_AUDIO_MEDIUM: return 2
52+
elif profile == Opus.PROFILE_AUDIO_HIGH: return 2
53+
elif profile == Opus.PROFILE_AUDIO_MAX: return 2
54+
else: raise CodecError(f"Unsupported profile")
55+
56+
@staticmethod
57+
def profile_samplerate(profile):
58+
if profile == Opus.PROFILE_VOICE_LOW: return 8000
59+
elif profile == Opus.PROFILE_VOICE_MEDIUM: return 24000
60+
elif profile == Opus.PROFILE_VOICE_HIGH: return 48000
61+
elif profile == Opus.PROFILE_VOICE_MAX: return 48000
62+
elif profile == Opus.PROFILE_AUDIO_MIN: return 8000
63+
elif profile == Opus.PROFILE_AUDIO_LOW: return 12000
64+
elif profile == Opus.PROFILE_AUDIO_MEDIUM: return 24000
65+
elif profile == Opus.PROFILE_AUDIO_HIGH: return 48000
66+
elif profile == Opus.PROFILE_AUDIO_MAX: return 48000
67+
else: raise CodecError(f"Unsupported profile")
68+
69+
@staticmethod
70+
def profile_application(profile):
71+
if profile == Opus.PROFILE_VOICE_LOW: return "voip"
72+
elif profile == Opus.PROFILE_VOICE_MEDIUM: return "voip"
73+
elif profile == Opus.PROFILE_VOICE_HIGH: return "voip"
74+
elif profile == Opus.PROFILE_VOICE_MAX: return "voip"
75+
elif profile == Opus.PROFILE_AUDIO_MIN: return "audio"
76+
elif profile == Opus.PROFILE_AUDIO_LOW: return "audio"
77+
elif profile == Opus.PROFILE_AUDIO_MEDIUM: return "audio"
78+
elif profile == Opus.PROFILE_AUDIO_HIGH: return "audio"
79+
elif profile == Opus.PROFILE_AUDIO_MAX: return "audio"
80+
else: raise CodecError(f"Unsupported profile")
81+
82+
@staticmethod
83+
def profile_bitrate_ceiling(profile):
84+
if profile == Opus.PROFILE_VOICE_LOW: return 6000
85+
elif profile == Opus.PROFILE_VOICE_MEDIUM: return 8000
86+
elif profile == Opus.PROFILE_VOICE_HIGH: return 16000
87+
elif profile == Opus.PROFILE_VOICE_MAX: return 32000
88+
elif profile == Opus.PROFILE_AUDIO_MIN: return 8000
89+
elif profile == Opus.PROFILE_AUDIO_LOW: return 14000
90+
elif profile == Opus.PROFILE_AUDIO_MEDIUM: return 28000
91+
elif profile == Opus.PROFILE_AUDIO_HIGH: return 56000
92+
elif profile == Opus.PROFILE_AUDIO_MAX: return 128000
93+
else: raise CodecError(f"Unsupported profile")
94+
95+
@staticmethod
96+
def max_bytes_per_frame(bitrate_ceiling, frame_duration_ms):
97+
return math.ceil((bitrate_ceiling/8)*(frame_duration_ms/1000))
98+
4399
def set_profile(self, profile):
44-
if profile == self.PROFILE_VOICE_LOW:
45-
self.profile = profile
46-
self.channels = 1
47-
self.input_channels = self.channels
48-
self.output_samplerate = 8000
49-
self.opus_encoder.set_application("voip")
50-
elif profile == self.PROFILE_VOICE_MEDIUM:
51-
self.profile = profile
52-
self.channels = 1
53-
self.input_channels = self.channels
54-
self.output_samplerate = 24000
55-
self.opus_encoder.set_application("voip")
56-
elif profile == self.PROFILE_VOICE_HIGH:
57-
self.profile = profile
58-
self.channels = 1
59-
self.input_channels = self.channels
60-
self.output_samplerate = 48000
61-
self.opus_encoder.set_application("voip")
62-
elif profile == self.PROFILE_VOICE_MAX:
63-
self.profile = profile
64-
self.channels = 2
65-
self.input_channels = self.channels
66-
self.output_samplerate = 48000
67-
self.opus_encoder.set_application("voip")
68-
elif profile == self.PROFILE_AUDIO_MIN:
69-
self.profile = profile
70-
self.channels = 1
71-
self.input_channels = self.channels
72-
self.output_samplerate = 8000
73-
self.opus_encoder.set_application("audio")
74-
elif profile == self.PROFILE_AUDIO_LOW:
75-
self.profile = profile
76-
self.channels = 1
77-
self.input_channels = self.channels
78-
self.output_samplerate = 12000
79-
self.opus_encoder.set_application("audio")
80-
elif profile == self.PROFILE_AUDIO_MEDIUM:
81-
self.profile = profile
82-
self.channels = 2
83-
self.input_channels = self.channels
84-
self.output_samplerate = 24000
85-
self.opus_encoder.set_application("audio")
86-
elif profile == self.PROFILE_AUDIO_HIGH:
87-
self.profile = profile
88-
self.channels = 2
89-
self.input_channels = self.channels
90-
self.output_samplerate = 48000
91-
self.opus_encoder.set_application("audio")
92-
elif profile == self.PROFILE_AUDIO_MAX:
93-
self.profile = profile
94-
self.channels = 2
95-
self.input_channels = self.channels
96-
self.output_samplerate = 48000
97-
self.opus_encoder.set_application("audio")
98-
else:
99-
raise CodecError(f"Unsupported profile configured for {self}")
100+
self.channels = self.profile_channels(profile)
101+
self.input_channels = self.channels
102+
self.output_samplerate = self.profile_samplerate(profile)
103+
self.opus_encoder.set_application(self.profile_application(profile))
104+
self.profile = profile
100105

101106
def update_bitrate(self, frame_duration_ms):
102-
if self.profile == self.PROFILE_VOICE_LOW:
103-
self.bitrate_ceiling = 6000
104-
elif self.profile == self.PROFILE_VOICE_MEDIUM:
105-
self.bitrate_ceiling = 8000
106-
elif self.profile == self.PROFILE_VOICE_HIGH:
107-
self.bitrate_ceiling = 16000
108-
elif self.profile == self.PROFILE_VOICE_MAX:
109-
self.bitrate_ceiling = 32000
110-
elif self.profile == self.PROFILE_AUDIO_MIN:
111-
self.bitrate_ceiling = 8000
112-
elif self.profile == self.PROFILE_AUDIO_LOW:
113-
self.bitrate_ceiling = 14000
114-
elif self.profile == self.PROFILE_AUDIO_MEDIUM:
115-
self.bitrate_ceiling = 28000
116-
elif self.profile == self.PROFILE_AUDIO_HIGH:
117-
self.bitrate_ceiling = 56000
118-
elif self.profile == self.PROFILE_AUDIO_MAX:
119-
self.bitrate_ceiling = 128000
120-
121-
max_bytes_per_frame = math.ceil((self.bitrate_ceiling/8)*(frame_duration_ms/1000))
107+
self.bitrate_ceiling = self.profile_bitrate_ceiling(self.profile)
108+
max_bytes_per_frame = self.max_bytes_per_frame(self.bitrate_ceiling, frame_duration_ms)
109+
122110
configured_bitrate = (max_bytes_per_frame*8)/(frame_duration_ms/1000)
123111
self.opus_encoder.set_max_bytes_per_frame(max_bytes_per_frame)
124112

LXST/Pipeline.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,10 @@ def __init__(self, source, codec, sink, processor = None):
1818
self.source.sink = sink
1919
self.codec = codec
2020

21-
if isinstance(sink, Loopback):
22-
sink.samplerate = source.samplerate
23-
if isinstance(source, Loopback):
24-
source._sink = sink
25-
if isinstance(sink, Packetizer):
26-
sink.source = source
21+
if isinstance(sink, Loopback): sink.samplerate = source.samplerate
22+
if isinstance(source, Loopback): source._sink = sink
23+
if isinstance(sink, Packetizer): sink.source = source
24+
if isinstance(sink, OpusFileSink): sink.source = source
2725

2826
@property
2927
def codec(self):

LXST/Primitives/Players.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import LXST
2+
import time
3+
import threading
4+
import os
5+
6+
from LXST.Sinks import LineSink
7+
from LXST.Sources import OpusFileSource
8+
9+
class FilePlayer():
10+
def __init__(self, path=None, device=None, loop=False):
11+
self._file_path = path
12+
self._playback_device = None
13+
self.__finished_callback = None
14+
self.__loop = loop
15+
self.__source = None
16+
self.__sink = LineSink(self._playback_device)
17+
self.__raw = LXST.Codecs.Raw()
18+
self.__loopback = LXST.Sources.Loopback()
19+
self.__output_pipeline = LXST.Pipeline(source=self.__loopback, codec=self.__raw, sink=self.__sink)
20+
self.__input_pipeline = None
21+
if path: self.set_source(self._file_path)
22+
23+
@property
24+
def running(self):
25+
if not self.__source: return False
26+
else: return self.__source.should_run
27+
28+
@property
29+
def playing(self): return self.running
30+
31+
@property
32+
def finished_callback(self): return self.__finished_callback
33+
34+
@finished_callback.setter
35+
def finished_callback(self, callback):
36+
if callback == None: self.__finished_callback = None
37+
elif not callable(callback): raise TypeError("Provided callback is not callable")
38+
else: self.__finished_callback = callback
39+
40+
def __callback_job(self):
41+
if self.__finished_callback:
42+
time.sleep(0.2)
43+
while self.running: time.sleep(0.1)
44+
self.__finished_callback(self)
45+
46+
def set_source(self, path=None):
47+
if not path: return
48+
else:
49+
if not os.path.isfile(path): raise OSError(f"File not found: {path}")
50+
else:
51+
self.__source = OpusFileSource(path, loop=self.__loop)
52+
self.__input_pipeline = LXST.Pipeline(source=self.__source, codec=self.__raw, sink=self.__loopback)
53+
54+
def loop(self, loop=True):
55+
if loop == True: self.__loop = True
56+
else: self.__loop = False
57+
if self.__source: self.__source.loop = self.__loop
58+
59+
def start(self):
60+
if not self.running and self.__source:
61+
self.__input_pipeline.start()
62+
self.__output_pipeline.start()
63+
if self.__finished_callback:
64+
threading.Thread(target=self.__callback_job, daemon=True).start()
65+
66+
def stop(self):
67+
if self.running and self.__source:
68+
self.__input_pipeline.stop()
69+
self.__output_pipeline.stop()
70+
71+
def play(self): self.start()

LXST/Primitives/Recorders.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import RNS
2+
import LXST
3+
import time
4+
import os
5+
from LXST.Sources import LineSource
6+
from LXST.Sinks import OpusFileSink
7+
from LXST.Filters import BandPass, AGC
8+
9+
class FileRecorder():
10+
def __init__(self, path=None, device=None, profile=LXST.Codecs.Opus.PROFILE_AUDIO_MAX,
11+
gain=0.0, ease_in=0.125, skip=0.075, filters=[BandPass(25, 24000)]):
12+
self._file_path = path
13+
self._record_device = device
14+
self.__profile = profile
15+
self.__source = None
16+
self.__sink = OpusFileSink(path=self._file_path, profile=profile)
17+
self.__null = LXST.Codecs.Null()
18+
self.__filters = filters
19+
self.__ease_in = ease_in
20+
self.__skip = skip
21+
self.__gain = gain
22+
self.set_source(device)
23+
24+
@property
25+
def running(self):
26+
if not self.__source: return False
27+
else: return self.__source.should_run
28+
29+
@property
30+
def recording(self): return self.running
31+
32+
def set_source(self, device=None):
33+
self._record_device = device
34+
self.__source = LineSource(preferred_device=self._record_device, target_frame_ms=20, codec=self.__null, sink=self.__sink,
35+
gain=self.__gain, ease_in=self.__ease_in, skip=self.__skip, filters=self.__filters)
36+
self.__sink.source = self.__source
37+
38+
def set_output_path(self, path):
39+
self._file_path = path
40+
self.__sink.__output_path = path
41+
42+
def start(self):
43+
if self.__source:
44+
self.__source.start()
45+
46+
def stop(self):
47+
if self.__source:
48+
self.__source.stop()
49+
while self.__sink.frames_waiting: time.sleep(0.1)
50+
self.__sink.stop()
51+
52+
def record(self):
53+
self.start()

LXST/Primitives/Telephony.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ def __open_pipelines(self, identity):
604604
RNS.log(f"Opening audio pipelines for call with {RNS.prettyhexrep(identity.hash)}", RNS.LOG_DEBUG)
605605
if self.active_call.is_incoming: self.signal(Signalling.STATUS_CONNECTING, self.active_call)
606606

607-
if self.use_agc: self.active_call.filters = [BandPass(250, 8500), AGC()]
607+
if self.use_agc: self.active_call.filters = [BandPass(250, 8500), AGC(target_level=-15.0)]
608608
else: self.active_call.filters = [BandPass(250, 8500)]
609609

610610
self.__prepare_dialling_pipelines()

0 commit comments

Comments
 (0)