Added audio device config ui

This commit is contained in:
Mark Qvist 2025-03-14 14:09:38 +01:00
parent 3d7e894a9d
commit 4d9bba3e4c
7 changed files with 250 additions and 21 deletions

View file

@ -104,3 +104,4 @@ getrns:
cleanrns: cleanrns:
-(rm ./RNS -r) -(rm ./RNS -r)
-(rm ./LXMF -r) -(rm ./LXMF -r)
-(rm ./LXST -r)

View file

@ -1457,6 +1457,8 @@ class SidebandApp(MDApp):
self.close_sub_utilities_action() self.close_sub_utilities_action()
elif self.root.ids.screen_manager.current == "logviewer_screen": elif self.root.ids.screen_manager.current == "logviewer_screen":
self.close_sub_utilities_action() self.close_sub_utilities_action()
elif self.root.ids.screen_manager.current == "voice_settings_screen":
self.close_sub_voice_action()
else: else:
self.open_conversations(direction="right") self.open_conversations(direction="right")
@ -1534,6 +1536,7 @@ class SidebandApp(MDApp):
def announce_now_action(self, sender=None): def announce_now_action(self, sender=None):
self.sideband.lxmf_announce() self.sideband.lxmf_announce()
if self.sideband.telephone: self.sideband.telephone.announce()
yes_button = MDRectangleFlatButton(text="OK",font_size=dp(18)) yes_button = MDRectangleFlatButton(text="OK",font_size=dp(18))

View file

@ -535,6 +535,9 @@ class SidebandCore():
# Voice # Voice
self.config["voice_enabled"] = False 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): if not os.path.isfile(self.db_path):
self.__db_init() self.__db_init()
@ -844,6 +847,12 @@ class SidebandCore():
if not "voice_enabled" in self.config: if not "voice_enabled" in self.config:
self.config["voice_enabled"] = False 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 # Make sure we have a database
if not os.path.isfile(self.db_path): 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) lxmf_destination_hash = RNS.Destination.hash_from_name_and_identity("lxmf.delivery", identity_hash)
existing_voice = self._db_conversation(context_dest) existing_voice = self._db_conversation(context_dest)
existing_lxmf = self._db_conversation(lxmf_destination_hash) 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) if existing_lxmf: return self.peer_display_name(lxmf_destination_hash)
else: return self.peer_display_name(identity_hash) else: return self.peer_display_name(identity_hash)
@ -3458,6 +3462,7 @@ class SidebandCore():
if self.config["start_announce"] == True: if self.config["start_announce"] == True:
time.sleep(12) time.sleep(12)
self.lxmf_announce(attached_interface=self.interface_local) 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() threading.Thread(target=job, daemon=True).start()
if hasattr(self, "interface_rnode") and self.interface_rnode != None: if hasattr(self, "interface_rnode") and self.interface_rnode != None:
@ -3545,6 +3550,7 @@ class SidebandCore():
aif = announce_attached_interface aif = announce_attached_interface
time.sleep(delay) time.sleep(delay)
self.lxmf_announce(attached_interface=aif) self.lxmf_announce(attached_interface=aif)
if self.telephone: self.telephone.announce(attached_interface=aif)
return x return x
threading.Thread(target=gen_announce_job(announce_delay, announce_attached_interface), daemon=True).start() threading.Thread(target=gen_announce_job(announce_delay, announce_attached_interface), daemon=True).start()
@ -3759,6 +3765,7 @@ class SidebandCore():
def da(): def da():
time.sleep(8) time.sleep(8)
self.lxmf_announce() self.lxmf_announce()
if self.telephone: self.telephone.announce()
self.last_if_change_announce = time.time() self.last_if_change_announce = time.time()
threading.Thread(target=da, daemon=True).start() threading.Thread(target=da, daemon=True).start()
@ -5241,7 +5248,7 @@ class SidebandCore():
RNS.log("Starting voice service", RNS.LOG_DEBUG) RNS.log("Starting voice service", RNS.LOG_DEBUG)
self.voice_running = True self.voice_running = True
from .voice import ReticulumTelephone 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") ringtone_path = os.path.join(self.asset_dir, "audio", "notifications", "soft1.opus")
self.telephone.set_ringtone(ringtone_path) self.telephone.set_ringtone(ringtone_path)

View file

@ -22,7 +22,7 @@ class ReticulumTelephone():
WAIT_TIME = 60 WAIT_TIME = 60
PATH_TIME = 10 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.identity = identity
self.service = service self.service = service
self.owner = owner self.owner = owner
@ -37,9 +37,9 @@ class ReticulumTelephone():
self.last_input = None self.last_input = None
self.first_run = False self.first_run = False
self.ringtone_path = None self.ringtone_path = None
self.speaker_device = None self.speaker_device = speaker
self.microphone_device = None self.microphone_device = microphone
self.ringer_device = None self.ringer_device = ringer
self.phonebook = {} self.phonebook = {}
self.aliases = {} self.aliases = {}
self.names = {} self.names = {}
@ -58,6 +58,21 @@ class ReticulumTelephone():
self.ringtone_path = ringtone_path self.ringtone_path = ringtone_path
self.telephone.set_ringtone(self.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 @property
def is_available(self): def is_available(self):
return self.state == self.STATE_AVAILABLE return self.state == self.STATE_AVAILABLE
@ -84,7 +99,6 @@ class ReticulumTelephone():
def start(self): def start(self):
if not self.should_run: if not self.should_run:
self.telephone.announce()
self.should_run = True self.should_run = True
self.run() self.run()

View file

@ -40,7 +40,7 @@ class Utilities():
self.screen.delegate = self self.screen.delegate = self
self.app.root.ids.screen_manager.add_widget(self.screen) 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 = "This section contains various utilities and diagnostics tools, "
info += "that can be helpful while using Sideband and Reticulum." info += "that can be helpful while using Sideband and Reticulum."
@ -220,7 +220,7 @@ MDScreen:
] ]
ScrollView: ScrollView:
id: telemetry_scrollview id: utilities_scrollview
MDBoxLayout: MDBoxLayout:
orientation: "vertical" orientation: "vertical"

View file

@ -10,6 +10,7 @@ from kivymd.uix.recycleview import MDRecycleView
from kivymd.uix.list import OneLineIconListItem from kivymd.uix.list import OneLineIconListItem
from kivymd.uix.pickers import MDColorPicker from kivymd.uix.pickers import MDColorPicker
from kivymd.uix.button import MDRectangleFlatButton from kivymd.uix.button import MDRectangleFlatButton
from kivymd.uix.button import MDRectangleFlatIconButton
from kivymd.uix.dialog import MDDialog from kivymd.uix.dialog import MDDialog
from kivymd.icon_definitions import md_icons from kivymd.icon_definitions import md_icons
from kivymd.toast import toast from kivymd.toast import toast
@ -34,6 +35,11 @@ class Voice():
self.dial_target = None self.dial_target = None
self.ui_updater = None self.ui_updater = None
self.path_requesting = 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"): if not self.app.root.ids.screen_manager.has_screen("voice_screen"):
self.screen = Builder.load_string(layout_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.app.root.ids.screen_manager.add_widget(self.screen)
self.screen.ids.voice_scrollview.effect_cls = ScrollEffect 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): def update_call_status(self, dt=None):
if self.app.root.ids.screen_manager.current == "voice_screen": if self.app.root.ids.screen_manager.current == "voice_screen":
@ -170,6 +169,120 @@ class Voice():
self.app.sideband.telephone.answer() self.app.sideband.telephone.answer()
self.update_call_status() 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 = """ layout_voice_screen = """
MDScreen: MDScreen:
name: "voice_screen" name: "voice_screen"
@ -185,6 +298,7 @@ MDScreen:
[['menu', lambda x: root.app.nav_drawer.set_state("open")]] [['menu', lambda x: root.app.nav_drawer.set_state("open")]]
right_action_items: right_action_items:
[ [
['wrench-cog', lambda x: root.delegate.settings_action(self)],
['close', lambda x: root.app.close_any_action(self)], ['close', lambda x: root.app.close_any_action(self)],
] ]
@ -231,3 +345,93 @@ MDScreen:
on_release: root.delegate.dial_action(self) on_release: root.delegate.dial_action(self)
disabled: True 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)]
"""

View file

@ -123,7 +123,7 @@ setuptools.setup(
"ffpyplayer", "ffpyplayer",
"sh", "sh",
"numpy<=1.26.4", "numpy<=1.26.4",
"lxst>=0.2.4", "lxst>=0.2.7",
"mistune>=3.0.2", "mistune>=3.0.2",
"beautifulsoup4", "beautifulsoup4",
"pycodec2;sys.platform!='Windows' and sys.platform!='win32' and sys.platform!='darwin'", "pycodec2;sys.platform!='Windows' and sys.platform!='win32' and sys.platform!='darwin'",