- Pipewire and Pulseaudio integration
- Opt in Core > Integrate - When microphone is muted or 0% set client to show muted - When output is muted or 0% set client to show deafened - Subscribe to client 'voice_settings_update' events to see when mic/output are changed - Cleaner quit on Ctrl-C -Fixes #327
This commit is contained in:
parent
ff69a4ed8f
commit
6d92d0f79f
6 changed files with 197 additions and 3 deletions
130
discover_overlay/audio_assist.py
Normal file
130
discover_overlay/audio_assist.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""A class to assist with reading pulseaudio changes"""
|
||||
import os
|
||||
import logging
|
||||
import signal
|
||||
import pulsectl_asyncio
|
||||
from contextlib import suppress
|
||||
import asyncio
|
||||
from threading import Thread, Event
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class DiscoverAudioAssist:
|
||||
def __init__(self, discover):
|
||||
|
||||
self.thread=None
|
||||
self.enabled=False
|
||||
self.source=None # String containing the name of the PA/PW microphone or other input
|
||||
self.sink=None # String containing the name of the PA/PW output
|
||||
|
||||
self.discover = discover
|
||||
|
||||
# Keep last known state (or None) so that we don't repeatedly send messages for every little PA/PW signal
|
||||
self.last_set_mute = None
|
||||
self.last_set_deaf = None
|
||||
|
||||
def set_enabled(self, enabled):
|
||||
self.enabled = enabled
|
||||
if enabled:
|
||||
self.start()
|
||||
|
||||
def set_devices(self, sink, source):
|
||||
# Changed devices from client
|
||||
self.source = source
|
||||
self.sink = sink
|
||||
|
||||
def start(self):
|
||||
if not self.enabled:
|
||||
return
|
||||
if not self.thread:
|
||||
self.thread=Thread(target=self.thread_loop)
|
||||
self.thread.start()
|
||||
|
||||
def thread_loop(self):
|
||||
# Start an asyncio specific thread. Not the prettiest but I'm not rewriting from ground up for one feature
|
||||
log.info("Staring Audio subsystem assistance")
|
||||
loop = asyncio.new_event_loop()
|
||||
loop.run_until_complete(self.pulse_loop())
|
||||
log.info("Stopped Audio subsystem assistance")
|
||||
|
||||
async def listen(self):
|
||||
# Async to connect to pulse and listen for events
|
||||
async with pulsectl_asyncio.PulseAsync('Discover-Monitor') as pulse:
|
||||
await self.get_device_details(pulse)
|
||||
async for event in pulse.subscribe_events('all'):
|
||||
await self.print_events(pulse, event)
|
||||
|
||||
async def pulse_loop(self):
|
||||
# Prep before connecting to pulse
|
||||
loop = asyncio.get_event_loop()
|
||||
listen_task = asyncio.create_task(self.listen())
|
||||
with suppress(asyncio.CancelledError):
|
||||
await listen_task
|
||||
|
||||
async def get_device_details(self, pulse):
|
||||
# Decant information about our chosen devices
|
||||
# Feed this back to client to change deaf/mute state
|
||||
mute = None
|
||||
deaf = None
|
||||
for sink in await pulse.sink_list():
|
||||
if sink.description == self.sink:
|
||||
if sink.mute == 1 or sink.volume.values[0]==0.0:
|
||||
deaf = True
|
||||
elif sink.mute == 0:
|
||||
deaf = False
|
||||
|
||||
for source in await pulse.source_list():
|
||||
if source.description == self.source:
|
||||
if source.mute == 1 or source.volume.values[0]==0.0:
|
||||
mute = True
|
||||
elif sink.mute == 0:
|
||||
mute = False
|
||||
|
||||
if mute != self.last_set_mute:
|
||||
self.last_set_mute = mute
|
||||
self.discover.set_mute_async(mute)
|
||||
|
||||
if deaf != self.last_set_deaf:
|
||||
self.last_set_deaf = deaf
|
||||
self.discover.set_deaf_async(deaf)
|
||||
|
||||
async def print_events(self,pulse, ev):
|
||||
if not self.enabled:
|
||||
return
|
||||
# Sink and Source events are fired for changes to output and ints
|
||||
# Server is fired when default sink or source changes.
|
||||
match ev.facility:
|
||||
case 'sink':
|
||||
await self.get_device_details(pulse)
|
||||
|
||||
case 'source':
|
||||
await self.get_device_details(pulse)
|
||||
|
||||
case 'server':
|
||||
await self.get_device_details(pulse)
|
||||
|
||||
case 'source_output':
|
||||
pass
|
||||
|
||||
case 'sink_input':
|
||||
pass
|
||||
|
||||
case 'client':
|
||||
pass
|
||||
|
||||
case _:
|
||||
# If we need to find more events, this here will do it
|
||||
#log.info('Pulse event: %s' % ev)
|
||||
pass
|
||||
|
|
@ -347,6 +347,19 @@ class DiscordConnector:
|
|||
self.req_channel_details(j["data"]["id"], 'new')
|
||||
elif j["evt"] == "NOTIFICATION_CREATE":
|
||||
self.discover.notification_overlay.add_notification_message(j)
|
||||
elif j["evt"] == "VOICE_SETTINGS_UPDATE":
|
||||
source = j['data']['input']['device_id']
|
||||
sink = j['data']['output']['device_id']
|
||||
if sink == 'default':
|
||||
for available_sink in j['data']['output']['available_devices']:
|
||||
if available_sink['id']=='default':
|
||||
sink = available_sink['name'][9:]
|
||||
if source == 'default':
|
||||
for available_source in j['data']['input']['available_devices']:
|
||||
if available_source['id']=='default':
|
||||
source = available_source['name'][9:]
|
||||
self.discover.audio_assist.set_devices(sink, source)
|
||||
|
||||
else:
|
||||
log.warning(j)
|
||||
return
|
||||
|
|
@ -589,6 +602,7 @@ class DiscordConnector:
|
|||
or that reports the users current location
|
||||
"""
|
||||
self.sub_raw("VOICE_CHANNEL_SELECT", {}, "VOICE_CHANNEL_SELECT")
|
||||
self.sub_raw("VOICE_SETTINGS_UPDATE", {}, "VOICE_SETTINGS_UPDATE")
|
||||
self.sub_raw("VOICE_CONNECTION_STATUS", {}, "VOICE_CONNECTION_STATUS")
|
||||
self.sub_raw("GUILD_CREATE", {}, "GUILD_CREATE")
|
||||
self.sub_raw("CHANNEL_CREATE", {}, "CHANNEL_CREATE")
|
||||
|
|
@ -662,6 +676,7 @@ class DiscordConnector:
|
|||
}
|
||||
if self.websocket:
|
||||
self.websocket.send(json.dumps(cmd))
|
||||
return False
|
||||
|
||||
def set_deaf(self, deaf):
|
||||
cmd = {
|
||||
|
|
@ -671,6 +686,7 @@ class DiscordConnector:
|
|||
}
|
||||
if self.websocket:
|
||||
self.websocket.send(json.dumps(cmd))
|
||||
return False
|
||||
|
||||
def change_voice_room(self, id):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import traceback
|
|||
import logging
|
||||
import pkg_resources
|
||||
import json
|
||||
import signal
|
||||
import gi
|
||||
from configparser import ConfigParser
|
||||
|
||||
|
|
@ -28,6 +29,7 @@ from .voice_overlay import VoiceOverlayWindow
|
|||
from .text_overlay import TextOverlayWindow
|
||||
from .notification_overlay import NotificationOverlayWindow
|
||||
from .discord_connector import DiscordConnector
|
||||
from .audio_assist import DiscoverAudioAssist
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
# pylint: disable=wrong-import-position,wrong-import-order
|
||||
|
|
@ -78,6 +80,7 @@ class Discover:
|
|||
self.connection = DiscordConnector(self)
|
||||
|
||||
self.connection.connect()
|
||||
self.audio_assist = DiscoverAudioAssist(self)
|
||||
|
||||
rpc_file = Gio.File.new_for_path(rpc_file)
|
||||
monitor = rpc_file.monitor_file(0, None)
|
||||
|
|
@ -407,6 +410,8 @@ class Discover:
|
|||
self.text_overlay.set_hidden(hidden)
|
||||
self.notification_overlay.set_hidden(hidden)
|
||||
|
||||
self.audio_assist.set_enabled(config.getboolean("general", "audio_assist", fallback = False))
|
||||
|
||||
|
||||
def parse_guild_ids(self, guild_ids_str):
|
||||
"""Parse the guild_ids from a str and return them in a list"""
|
||||
|
|
@ -468,6 +473,13 @@ class Discover:
|
|||
if self.notification_overlay:
|
||||
self.notification_overlay.set_task(visible)
|
||||
|
||||
def set_mute_async(self, mute):
|
||||
if mute != None:
|
||||
GLib.idle_add(self.connection.set_mute, mute)
|
||||
|
||||
def set_deaf_async(self, deaf):
|
||||
if deaf != None:
|
||||
GLib.idle_add(self.connection.set_deaf, deaf)
|
||||
|
||||
def entrypoint():
|
||||
"""
|
||||
|
|
@ -482,6 +494,7 @@ def entrypoint():
|
|||
otherwise start overlay
|
||||
"""
|
||||
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
# Find Config directory
|
||||
config_dir = os.path.join(xdg_config_home, "discover_overlay")
|
||||
os.makedirs(config_dir, exist_ok=True)
|
||||
|
|
|
|||
|
|
@ -2620,7 +2620,7 @@
|
|||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<!-- n-columns=2 n-rows=7 -->
|
||||
<!-- n-columns=2 n-rows=8 -->
|
||||
<object class="GtkGrid">
|
||||
<property name="name">core_grid</property>
|
||||
<property name="visible">True</property>
|
||||
|
|
@ -2773,6 +2773,7 @@
|
|||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
<signal name="toggled" handler="core_hide_overlay_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
|
|
@ -2801,6 +2802,33 @@
|
|||
<property name="receives-default">True</property>
|
||||
<signal name="pressed" handler="core_reset_all" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">7</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="name">core_audio_assist_label</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Integrate with Pipewire/Pulseaudio</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton">
|
||||
<property name="name">core_audio_assist</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="draw-indicator">True</property>
|
||||
<signal name="toggled" handler="core_audio_assist_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">6</property>
|
||||
|
|
|
|||
|
|
@ -552,6 +552,7 @@ class MainSettingsWindow():
|
|||
|
||||
self.start_minimized = config.getboolean(
|
||||
"general", "start_min", fallback=False)
|
||||
|
||||
self.widget['core_settings_min'].set_active(self.start_minimized)
|
||||
|
||||
self.widget['core_settings_min'].set_sensitive(self.show_sys_tray_icon)
|
||||
|
|
@ -559,6 +560,8 @@ class MainSettingsWindow():
|
|||
if 'XDG_SESSION_DESKTOP' in os.environ and os.environ['XDG_SESSION_DESKTOP']=='cinnamon':
|
||||
self.widget['voice_anchor_to_edge_button'].set_sensitive(False)
|
||||
|
||||
self.widget['core_audio_assist'].set_active(config.getboolean("general", "audio_assist", fallback=False))
|
||||
|
||||
self.loading_config = False
|
||||
|
||||
def make_colour(self, col):
|
||||
|
|
@ -1179,3 +1182,6 @@ class MainSettingsWindow():
|
|||
def inactive_fade_time_changed(self,button):
|
||||
self.config_set("main", "inactive_fade_time", "%s" %
|
||||
(int(button.get_value())))
|
||||
|
||||
def core_audio_assist_changed(self, button):
|
||||
self.config_set("general", "audio_assist", "%s" % (button.get_active()))
|
||||
|
|
|
|||
3
setup.py
3
setup.py
|
|
@ -32,7 +32,8 @@ setup(
|
|||
'requests',
|
||||
'pillow',
|
||||
'python-xlib',
|
||||
'setuptools'
|
||||
'setuptools',
|
||||
'pulsectl-asyncio'
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue