From 4d9bba3e4cb2ab2a6746c6f2c70eed0788fc324e Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Fri, 14 Mar 2025 14:09:38 +0100 Subject: [PATCH] Added audio device config ui --- sbapp/Makefile | 1 + sbapp/main.py | 3 + sbapp/sideband/core.py | 19 ++-- sbapp/sideband/voice.py | 24 ++++- sbapp/ui/utilities.py | 4 +- sbapp/ui/voice.py | 218 ++++++++++++++++++++++++++++++++++++++-- setup.py | 2 +- 7 files changed, 250 insertions(+), 21 deletions(-) diff --git a/sbapp/Makefile b/sbapp/Makefile index d0f9028..11bdb19 100644 --- a/sbapp/Makefile +++ b/sbapp/Makefile @@ -104,3 +104,4 @@ getrns: cleanrns: -(rm ./RNS -r) -(rm ./LXMF -r) + -(rm ./LXST -r) diff --git a/sbapp/main.py b/sbapp/main.py index c157d92..d9e3390 100644 --- a/sbapp/main.py +++ b/sbapp/main.py @@ -1457,6 +1457,8 @@ class SidebandApp(MDApp): self.close_sub_utilities_action() elif self.root.ids.screen_manager.current == "logviewer_screen": self.close_sub_utilities_action() + elif self.root.ids.screen_manager.current == "voice_settings_screen": + self.close_sub_voice_action() else: self.open_conversations(direction="right") @@ -1534,6 +1536,7 @@ class SidebandApp(MDApp): def announce_now_action(self, sender=None): self.sideband.lxmf_announce() + if self.sideband.telephone: self.sideband.telephone.announce() yes_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) diff --git a/sbapp/sideband/core.py b/sbapp/sideband/core.py index 168cb7c..56f5249 100644 --- a/sbapp/sideband/core.py +++ b/sbapp/sideband/core.py @@ -535,6 +535,9 @@ class SidebandCore(): # Voice self.config["voice_enabled"] = False + self.config["voice_output"] = None + self.config["voice_input"] = None + self.config["voice_ringer"] = None if not os.path.isfile(self.db_path): self.__db_init() @@ -844,6 +847,12 @@ class SidebandCore(): if not "voice_enabled" in self.config: self.config["voice_enabled"] = False + if not "voice_output" in self.config: + self.config["voice_output"] = None + if not "voice_input" in self.config: + self.config["voice_input"] = None + if not "voice_ringer" in self.config: + self.config["voice_ringer"] = None # Make sure we have a database if not os.path.isfile(self.db_path): @@ -1233,11 +1242,6 @@ class SidebandCore(): lxmf_destination_hash = RNS.Destination.hash_from_name_and_identity("lxmf.delivery", identity_hash) existing_voice = self._db_conversation(context_dest) existing_lxmf = self._db_conversation(lxmf_destination_hash) - - print(RNS.prettyhexrep(lxmf_destination_hash)) - print(f"VOICE {existing_voice}") - print(f"LXMF {existing_lxmf}") - if existing_lxmf: return self.peer_display_name(lxmf_destination_hash) else: return self.peer_display_name(identity_hash) @@ -3458,6 +3462,7 @@ class SidebandCore(): if self.config["start_announce"] == True: time.sleep(12) self.lxmf_announce(attached_interface=self.interface_local) + if self.telephone: self.telephone.announce(attached_interface=self.interface_local) threading.Thread(target=job, daemon=True).start() if hasattr(self, "interface_rnode") and self.interface_rnode != None: @@ -3545,6 +3550,7 @@ class SidebandCore(): aif = announce_attached_interface time.sleep(delay) self.lxmf_announce(attached_interface=aif) + if self.telephone: self.telephone.announce(attached_interface=aif) return x threading.Thread(target=gen_announce_job(announce_delay, announce_attached_interface), daemon=True).start() @@ -3759,6 +3765,7 @@ class SidebandCore(): def da(): time.sleep(8) self.lxmf_announce() + if self.telephone: self.telephone.announce() self.last_if_change_announce = time.time() threading.Thread(target=da, daemon=True).start() @@ -5241,7 +5248,7 @@ class SidebandCore(): RNS.log("Starting voice service", RNS.LOG_DEBUG) self.voice_running = True from .voice import ReticulumTelephone - self.telephone = ReticulumTelephone(self.identity, owner=self) + self.telephone = ReticulumTelephone(self.identity, owner=self, speaker=self.config["voice_output"], microphone=self.config["voice_input"], ringer=self.config["voice_ringer"]) ringtone_path = os.path.join(self.asset_dir, "audio", "notifications", "soft1.opus") self.telephone.set_ringtone(ringtone_path) diff --git a/sbapp/sideband/voice.py b/sbapp/sideband/voice.py index 3754fc1..9222c10 100644 --- a/sbapp/sideband/voice.py +++ b/sbapp/sideband/voice.py @@ -22,7 +22,7 @@ class ReticulumTelephone(): WAIT_TIME = 60 PATH_TIME = 10 - def __init__(self, identity, owner = None, service = False): + def __init__(self, identity, owner = None, service = False, speaker=None, microphone=None, ringer=None): self.identity = identity self.service = service self.owner = owner @@ -37,9 +37,9 @@ class ReticulumTelephone(): self.last_input = None self.first_run = False self.ringtone_path = None - self.speaker_device = None - self.microphone_device = None - self.ringer_device = None + self.speaker_device = speaker + self.microphone_device = microphone + self.ringer_device = ringer self.phonebook = {} self.aliases = {} self.names = {} @@ -58,6 +58,21 @@ class ReticulumTelephone(): self.ringtone_path = ringtone_path self.telephone.set_ringtone(self.ringtone_path) + def set_speaker(self, device): + self.speaker_device = device + self.telephone.set_speaker(self.speaker_device) + + def set_microphone(self, device): + self.microphone_device = device + self.telephone.set_microphone(self.microphone_device) + + def set_ringer(self, device): + self.ringer_device = device + self.telephone.set_ringer(self.ringer_device) + + def announce(self, attached_interface=None): + self.telephone.announce(attached_interface=attached_interface) + @property def is_available(self): return self.state == self.STATE_AVAILABLE @@ -84,7 +99,6 @@ class ReticulumTelephone(): def start(self): if not self.should_run: - self.telephone.announce() self.should_run = True self.run() diff --git a/sbapp/ui/utilities.py b/sbapp/ui/utilities.py index bf46914..d3adcc7 100644 --- a/sbapp/ui/utilities.py +++ b/sbapp/ui/utilities.py @@ -40,7 +40,7 @@ class Utilities(): self.screen.delegate = self self.app.root.ids.screen_manager.add_widget(self.screen) - self.screen.ids.telemetry_scrollview.effect_cls = ScrollEffect + self.screen.ids.utilities_scrollview.effect_cls = ScrollEffect info = "This section contains various utilities and diagnostics tools, " info += "that can be helpful while using Sideband and Reticulum." @@ -220,7 +220,7 @@ MDScreen: ] ScrollView: - id: telemetry_scrollview + id: utilities_scrollview MDBoxLayout: orientation: "vertical" diff --git a/sbapp/ui/voice.py b/sbapp/ui/voice.py index fa00ea0..68e13d0 100644 --- a/sbapp/ui/voice.py +++ b/sbapp/ui/voice.py @@ -10,6 +10,7 @@ from kivymd.uix.recycleview import MDRecycleView from kivymd.uix.list import OneLineIconListItem from kivymd.uix.pickers import MDColorPicker from kivymd.uix.button import MDRectangleFlatButton +from kivymd.uix.button import MDRectangleFlatIconButton from kivymd.uix.dialog import MDDialog from kivymd.icon_definitions import md_icons from kivymd.toast import toast @@ -34,6 +35,11 @@ class Voice(): self.dial_target = None self.ui_updater = None self.path_requesting = None + self.output_devices = [] + self.input_devices = [] + self.listed_output_devices = [] + self.listed_input_devices = [] + self.listed_ringer_devices = [] if not self.app.root.ids.screen_manager.has_screen("voice_screen"): self.screen = Builder.load_string(layout_voice_screen) @@ -42,13 +48,6 @@ class Voice(): self.app.root.ids.screen_manager.add_widget(self.screen) self.screen.ids.voice_scrollview.effect_cls = ScrollEffect - # info = "Voice services UI" - # info += "" - - # if self.app.theme_cls.theme_style == "Dark": - # info = "[color=#"+self.app.dark_theme_text_color+"]"+info+"[/color]" - - # self.screen.ids.voice_info.text = info def update_call_status(self, dt=None): if self.app.root.ids.screen_manager.current == "voice_screen": @@ -170,6 +169,120 @@ class Voice(): self.app.sideband.telephone.answer() self.update_call_status() + + ### settings screen + ###################################### + + def settings_action(self, sender=None): + if not self.app.root.ids.screen_manager.has_screen("voice_settings_screen"): + self.voice_settings_screen = Builder.load_string(layout_voice_settings_screen) + self.voice_settings_screen.app = self.app + self.voice_settings_screen.delegate = self + self.app.root.ids.screen_manager.add_widget(self.voice_settings_screen) + + self.app.root.ids.screen_manager.transition.direction = "left" + self.app.root.ids.screen_manager.current = "voice_settings_screen" + self.voice_settings_screen.ids.voice_settings_scrollview.effect_cls = ScrollEffect + self.app.sideband.setstate("app.displaying", self.app.root.ids.screen_manager.current) + + self.update_settings_screen() + + def update_devices(self): + import LXST + self.output_devices = []; self.input_devices = [] + for device in LXST.Sources.Backend().soundcard.all_speakers(): self.output_devices.append(device.name) + for device in LXST.Sinks.Backend().soundcard.all_microphones(): self.input_devices.append(device.name) + if self.app.sideband.config["voice_output"] != None: + if not self.app.sideband.config["voice_output"] in self.output_devices: self.output_devices.append(self.app.sideband.config["voice_output"]) + if self.app.sideband.config["voice_input"] != None: + if not self.app.sideband.config["voice_input"] in self.input_devices: self.input_devices.append(self.app.sideband.config["voice_input"]) + if self.app.sideband.config["voice_ringer"] != None: + if not self.app.sideband.config["voice_ringer"] in self.output_devices: self.output_devices.append(self.app.sideband.config["voice_ringer"]) + + def update_settings_screen(self, sender=None): + bp = 6; ml = 45; fs = 16; ics = 14 + self.update_devices() + + # Output devices + if not "system_default" in self.listed_output_devices: + default_output_button = MDRectangleFlatIconButton(text="System Default", font_size=dp(fs), icon_size=dp(ics), on_release=self.output_device_action) + default_output_button.device = None; default_output_button.size_hint = [1.0, None] + if self.app.sideband.config["voice_output"] == None: default_output_button.icon = "check" + self.voice_settings_screen.ids.output_devices.add_widget(default_output_button) + self.listed_output_devices.append("system_default") + + for device in self.output_devices: + if not device in self.listed_output_devices: + label = device if len(device) < ml else device[:ml-3]+"..." + device_button = MDRectangleFlatIconButton(text=label, font_size=dp(fs), icon_size=dp(ics), on_release=self.output_device_action) + device_button.padding = [dp(bp), dp(bp), dp(bp), dp(bp)]; device_button.size_hint = [1.0, None] + if self.app.sideband.config["voice_output"] == device: device_button.icon = "check" + device_button.device = device + self.voice_settings_screen.ids.output_devices.add_widget(device_button) + self.listed_output_devices.append(device) + + # Input devices + if not "system_default" in self.listed_input_devices: + default_input_button = MDRectangleFlatIconButton(text="System Default", font_size=dp(fs), icon_size=dp(ics), on_release=self.input_device_action) + default_input_button.device = None; default_input_button.size_hint = [1.0, None] + if self.app.sideband.config["voice_output"] == None: default_input_button.icon = "check" + self.voice_settings_screen.ids.input_devices.add_widget(default_input_button) + self.listed_input_devices.append("system_default") + + for device in self.input_devices: + if not device in self.listed_input_devices: + label = device if len(device) < ml else device[:ml-3]+"..." + device_button = MDRectangleFlatIconButton(text=label, font_size=dp(fs), icon_size=dp(ics), on_release=self.input_device_action) + device_button.padding = [dp(bp), dp(bp), dp(bp), dp(bp)]; device_button.size_hint = [1.0, None] + if self.app.sideband.config["voice_input"] == device: device_button.icon = "check" + device_button.device = device + self.voice_settings_screen.ids.input_devices.add_widget(device_button) + self.listed_input_devices.append(device) + + # Ringer devices + if not "system_default" in self.listed_ringer_devices: + default_ringer_button = MDRectangleFlatIconButton(text="System Default", font_size=dp(fs), icon_size=dp(ics), on_release=self.ringer_device_action) + default_ringer_button.device = None; default_ringer_button.size_hint = [1.0, None] + if self.app.sideband.config["voice_ringer"] == None: default_ringer_button.icon = "check" + self.voice_settings_screen.ids.ringer_devices.add_widget(default_ringer_button) + self.listed_ringer_devices.append("system_default") + + for device in self.output_devices: + if not device in self.listed_ringer_devices: + label = device if len(device) < ml else device[:ml-3]+"..." + device_button = MDRectangleFlatIconButton(text=label, font_size=dp(fs), icon_size=dp(ics), on_release=self.ringer_device_action) + device_button.padding = [dp(bp), dp(bp), dp(bp), dp(bp)]; device_button.size_hint = [1.0, None] + if self.app.sideband.config["voice_ringer"] == device: device_button.icon = "check" + device_button.device = device + self.voice_settings_screen.ids.ringer_devices.add_widget(device_button) + self.listed_ringer_devices.append(device) + + + def output_device_action(self, sender=None): + self.app.sideband.config["voice_output"] = sender.device + self.app.sideband.save_configuration() + for w in self.voice_settings_screen.ids.output_devices.children: w.icon = "" + sender.icon = "check" + if self.app.sideband.telephone: + self.app.sideband.telephone.set_speaker(self.app.sideband.config["voice_output"]) + + def input_device_action(self, sender=None): + self.app.sideband.config["voice_input"] = sender.device + self.app.sideband.save_configuration() + for w in self.voice_settings_screen.ids.input_devices.children: w.icon = "" + sender.icon = "check" + if self.app.sideband.telephone: + self.app.sideband.telephone.set_microphone(self.app.sideband.config["voice_input"]) + + def ringer_device_action(self, sender=None): + self.app.sideband.config["voice_ringer"] = sender.device + self.app.sideband.save_configuration() + for w in self.voice_settings_screen.ids.ringer_devices.children: w.icon = "" + sender.icon = "check" + if self.app.sideband.telephone: + self.app.sideband.telephone.set_ringer(self.app.sideband.config["voice_ringer"]) + + layout_voice_screen = """ MDScreen: name: "voice_screen" @@ -185,6 +298,7 @@ MDScreen: [['menu', lambda x: root.app.nav_drawer.set_state("open")]] right_action_items: [ + ['wrench-cog', lambda x: root.delegate.settings_action(self)], ['close', lambda x: root.app.close_any_action(self)], ] @@ -231,3 +345,93 @@ MDScreen: on_release: root.delegate.dial_action(self) disabled: True """ + +layout_voice_settings_screen = """ +MDScreen: + name: "voice_settings_screen" + + BoxLayout: + orientation: "vertical" + + MDTopAppBar: + id: top_bar + title: "Voice Configuration" + anchor_title: "left" + elevation: 0 + left_action_items: + [['menu', lambda x: root.app.nav_drawer.set_state("open")]] + right_action_items: + [ + ['close', lambda x: root.app.close_sub_voice_action(self)], + ] + + MDScrollView: + id: voice_settings_scrollview + size_hint_x: 1 + size_hint_y: None + size: [root.width, root.height-root.ids.top_bar.height] + do_scroll_x: False + do_scroll_y: True + + MDBoxLayout: + orientation: "vertical" + size_hint_y: None + height: self.minimum_height + padding: [dp(28), dp(32), dp(28), dp(16)] + + MDLabel: + id: voice_settings_info + markup: True + text: "You can configure which audio devices Sideband will use for voice calls, by selecting either the system default device, or specific audio devices available." + size_hint_y: None + text_size: self.width, None + height: self.texture_size[1] + padding: [dp(0), dp(0), dp(0), dp(48)] + + MDLabel: + text: "Output Device" + font_style: "H6" + + MDBoxLayout: + id: output_devices + orientation: "vertical" + spacing: "12dp" + size_hint_y: None + height: self.minimum_height + padding: [dp(0), dp(35), dp(0), dp(48)] + + # MDRectangleFlatIconButton: + # id: output_default_button + # text: "System Default" + # padding: [dp(0), dp(14), dp(0), dp(14)] + # icon_size: dp(24) + # font_size: dp(16) + # size_hint: [1.0, None] + # on_release: root.delegate.output_device_action(self) + # disabled: False + + MDLabel: + text: "Input Device" + font_style: "H6" + + MDBoxLayout: + id: input_devices + orientation: "vertical" + spacing: "12dp" + size_hint_y: None + height: self.minimum_height + padding: [dp(0), dp(35), dp(0), dp(48)] + + MDLabel: + text: "Ringer Device" + font_style: "H6" + + MDBoxLayout: + id: ringer_devices + orientation: "vertical" + spacing: "12dp" + size_hint_y: None + height: self.minimum_height + padding: [dp(0), dp(35), dp(0), dp(48)] + +""" \ No newline at end of file diff --git a/setup.py b/setup.py index 83d4476..604c0c1 100644 --- a/setup.py +++ b/setup.py @@ -123,7 +123,7 @@ setuptools.setup( "ffpyplayer", "sh", "numpy<=1.26.4", - "lxst>=0.2.4", + "lxst>=0.2.7", "mistune>=3.0.2", "beautifulsoup4", "pycodec2;sys.platform!='Windows' and sys.platform!='win32' and sys.platform!='darwin'",