Compare commits

..

6 Commits

Author SHA1 Message Date
Onyx and Iris
be71c49806 should OBS be manually closed:
clean up the request socket.
the event socket should be handled by obsws-python library
2026-01-02 21:32:41 +00:00
Onyx and Iris
4f087a0358 upd docstrings 2026-01-02 20:51:50 +00:00
Onyx and Iris
e271c2a324 add DCM8 and TLM102 max gain class vars 2026-01-02 20:41:59 +00:00
Onyx and Iris
789f3e8491 bump obsws-python and vm-api dep versions 2026-01-02 20:15:39 +00:00
Onyx and Iris
bbdd64edb4 add {Audio}.mute_game_pcs()
update audio routing binds (ws, tv)
2026-01-02 20:15:23 +00:00
Onyx and Iris
62297835d9 upd tv routing incoming index 2026-01-02 18:15:47 +00:00
6 changed files with 43 additions and 19 deletions

View File

@ -15,6 +15,9 @@ logger = logging.getLogger(__name__)
class Audio(ILayer): class Audio(ILayer):
"""Audio concrete class""" """Audio concrete class"""
DCM8_MAX_GAIN = 20 # SE Electronics DCM8 max gain
TLM102_MAX_GAIN = 30 # Neumann TLM102 max gain
def __init__(self, duckypad, **kwargs): def __init__(self, duckypad, **kwargs):
super().__init__(duckypad) super().__init__(duckypad)
for attr, val in kwargs.items(): for attr, val in kwargs.items():
@ -131,7 +134,7 @@ class Audio(ILayer):
def stage_onyx_mic(self): def stage_onyx_mic(self):
"""Gain stage SE Electronics DCM8 with phantom power""" """Gain stage SE Electronics DCM8 with phantom power"""
self.mixer.headamp[XAirStrips.onyx_mic].phantom = True self.mixer.headamp[XAirStrips.onyx_mic].phantom = True
for i in range(21): for i in range(Audio.DCM8_MAX_GAIN + 1):
self.mixer.headamp[XAirStrips.onyx_mic].gain = i self.mixer.headamp[XAirStrips.onyx_mic].gain = i
time.sleep(0.1) time.sleep(0.1)
self.logger.info('Onyx Mic Staged with Phantom Power') self.logger.info('Onyx Mic Staged with Phantom Power')
@ -140,14 +143,14 @@ class Audio(ILayer):
def stage_iris_mic(self): def stage_iris_mic(self):
"""Gain stage TLM102 with phantom power""" """Gain stage TLM102 with phantom power"""
self.mixer.headamp[XAirStrips.iris_mic].phantom = True self.mixer.headamp[XAirStrips.iris_mic].phantom = True
for i in range(31): for i in range(Audio.TLM102_MAX_GAIN + 1):
self.mixer.headamp[XAirStrips.iris_mic].gain = i self.mixer.headamp[XAirStrips.iris_mic].gain = i
time.sleep(0.1) time.sleep(0.1)
self.logger.info('Iris Mic Staged with Phantom Power') self.logger.info('Iris Mic Staged with Phantom Power')
def unstage_onyx_mic(self): def unstage_onyx_mic(self):
"""Unstage SE Electronics DCM8 and disable phantom power""" """Unstage SE Electronics DCM8 and disable phantom power"""
for i in reversed(range(21)): for i in reversed(range(Audio.DCM8_MAX_GAIN + 1)):
self.mixer.headamp[XAirStrips.onyx_mic].gain = i self.mixer.headamp[XAirStrips.onyx_mic].gain = i
time.sleep(0.1) time.sleep(0.1)
self.mixer.headamp[XAirStrips.onyx_mic].phantom = False self.mixer.headamp[XAirStrips.onyx_mic].phantom = False
@ -155,7 +158,7 @@ class Audio(ILayer):
def unstage_iris_mic(self): def unstage_iris_mic(self):
"""Unstage TLM102 and disable phantom power""" """Unstage TLM102 and disable phantom power"""
for i in reversed(range(31)): for i in reversed(range(Audio.TLM102_MAX_GAIN + 1)):
self.mixer.headamp[XAirStrips.iris_mic].gain = i self.mixer.headamp[XAirStrips.iris_mic].gain = i
time.sleep(0.1) time.sleep(0.1)
self.mixer.headamp[XAirStrips.iris_mic].phantom = False self.mixer.headamp[XAirStrips.iris_mic].phantom = False
@ -179,6 +182,15 @@ class Audio(ILayer):
self.vm.patch.asio[2].set(0) self.vm.patch.asio[2].set(0)
self.logger.info('Iris mic has been unpatched') self.logger.info('Iris mic has been unpatched')
def mute_game_pcs(self):
self.state.mute_game_pcs = not self.state.mute_game_pcs
if self.state.mute_game_pcs:
self.mixer.strip[XAirStrips.game_pcs].send[XAirBuses.stream_mix].level = -90
self.logger.info('Game PCs Muted')
else:
self.mixer.strip[XAirStrips.game_pcs].send[XAirBuses.stream_mix].level = -24
self.logger.info('Game PCs Unmuted')
### Workstation and TV Audio Routing via VBAN ### ### Workstation and TV Audio Routing via VBAN ###
def _fade_mixer(self, target_fader, fade_in=True): def _fade_mixer(self, target_fader, fade_in=True):
@ -237,7 +249,7 @@ class Audio(ILayer):
vban_tv.strip[3].A1 = False vban_tv.strip[3].A1 = False
vban_tv.strip[3].gain = -6 vban_tv.strip[3].gain = -6
vban_tv.vban.outstream[0].on = True vban_tv.vban.outstream[0].on = True
vban_target.vban.instream[2].on = True vban_target.vban.instream[3].on = True
self.logger.info(f'TV audio routed to {target_name}') self.logger.info(f'TV audio routed to {target_name}')
else: else:
with ( with (
@ -247,7 +259,7 @@ class Audio(ILayer):
vban_tv.strip[3].A1 = True vban_tv.strip[3].A1 = True
vban_tv.strip[3].gain = 0 vban_tv.strip[3].gain = 0
vban_tv.vban.outstream[0].on = False vban_tv.vban.outstream[0].on = False
vban_target.vban.instream[2].on = False vban_target.vban.instream[3].on = False
self.logger.info(f'TV audio routing to {target_name} disabled') self.logger.info(f'TV audio routing to {target_name} disabled')
def toggle_tv_audio_to_onyx(self): def toggle_tv_audio_to_onyx(self):

View File

@ -24,10 +24,11 @@ def register_hotkeys(duckypad):
keyboard.add_hotkey('shift+F18', duckypad.audio.unstage_iris_mic) keyboard.add_hotkey('shift+F18', duckypad.audio.unstage_iris_mic)
keyboard.add_hotkey('F19', duckypad.audio.patch_onyx) keyboard.add_hotkey('F19', duckypad.audio.patch_onyx)
keyboard.add_hotkey('F20', duckypad.audio.patch_iris) keyboard.add_hotkey('F20', duckypad.audio.patch_iris)
keyboard.add_hotkey('F21', duckypad.audio.toggle_workstation_to_onyx) keyboard.add_hotkey('F21', duckypad.audio.mute_game_pcs)
keyboard.add_hotkey('F22', duckypad.audio.toggle_workstation_to_iris) keyboard.add_hotkey('alt+F13', duckypad.audio.toggle_workstation_to_onyx)
keyboard.add_hotkey('F23', duckypad.audio.toggle_tv_audio_to_onyx) keyboard.add_hotkey('alt+F14', duckypad.audio.toggle_workstation_to_iris)
keyboard.add_hotkey('F24', duckypad.audio.toggle_tv_audio_to_iris) keyboard.add_hotkey('alt+F15', duckypad.audio.toggle_tv_audio_to_onyx)
keyboard.add_hotkey('alt+F16', duckypad.audio.toggle_tv_audio_to_iris)
def scene_hotkeys(): def scene_hotkeys():
keyboard.add_hotkey('ctrl+F13', duckypad.scene.start) keyboard.add_hotkey('ctrl+F13', duckypad.scene.start)
@ -38,8 +39,8 @@ def register_hotkeys(duckypad):
keyboard.add_hotkey('ctrl+F18', duckypad.scene.iris_solo) keyboard.add_hotkey('ctrl+F18', duckypad.scene.iris_solo)
def obsws_hotkeys(): def obsws_hotkeys():
keyboard.add_hotkey('ctrl+alt+F13', duckypad.obsws.start_stream) keyboard.add_hotkey('ctrl+F22', duckypad.obsws.start_stream)
keyboard.add_hotkey('ctrl+alt+F14', duckypad.obsws.stop_stream) keyboard.add_hotkey('ctrl+F23', duckypad.obsws.stop_stream)
def duckypad_hotkeys(): def duckypad_hotkeys():
keyboard.add_hotkey('ctrl+F24', duckypad.reset) keyboard.add_hotkey('ctrl+F24', duckypad.reset)

View File

@ -54,12 +54,12 @@ class OBSWS(ILayer):
for client in (self.request, self.event): for client in (self.request, self.event):
if client: if client:
client.disconnect() client.disconnect()
self.request = self.event = None
### Event Handlers ### ### Event Handlers ###
def on_stream_state_changed(self, data): def on_stream_state_changed(self, data):
self._duckypad.stream.is_live = data.output_active self._duckypad.stream.is_live = data.output_active
self.logger.info(f'stream is {"live" if self._duckypad.stream.is_live else "offline"}')
def on_current_program_scene_changed(self, data): def on_current_program_scene_changed(self, data):
self._duckypad.stream.current_scene = data.scene_name self._duckypad.stream.current_scene = data.scene_name
@ -77,8 +77,10 @@ class OBSWS(ILayer):
case 'IRIS SOLO': case 'IRIS SOLO':
self.logger.info('Iris Solo Scene enabled, Onyx game pc muted') self.logger.info('Iris Solo Scene enabled, Onyx game pc muted')
def on_exit_started(self, _): def on_exit_started(self, data):
self.event.unsubscribe() self.logger.info('OBS is exiting, disconnecting...')
self.request.disconnect()
self.request = self.event = None
### OBSWS Request Wrappers ### ### OBSWS Request Wrappers ###
@ -98,6 +100,7 @@ class OBSWS(ILayer):
return return
self._call('start_stream') self._call('start_stream')
self.logger.info('stream started')
def stop_stream(self): def stop_stream(self):
resp = self._call('get_stream_status') resp = self._call('get_stream_status')
@ -106,3 +109,4 @@ class OBSWS(ILayer):
return return
self._call('stop_stream') self._call('stop_stream')
self.logger.info('stream stopped')

View File

@ -15,6 +15,7 @@ class AudioState:
sound_test: bool = False sound_test: bool = False
patch_onyx: bool = True patch_onyx: bool = True
patch_iris: bool = True patch_iris: bool = True
mute_game_pcs: bool = False
ws_to_onyx: bool = False ws_to_onyx: bool = False
ws_to_iris: bool = False ws_to_iris: bool = False

View File

@ -1,5 +1,7 @@
def ensure_obsws(func): def ensure_obsws(func):
"""ensure an obs websocket connection has been established""" """ensure an obs websocket connection has been established
Used as a decorator for functions that require an obs websocket connection"""
def wrapper(self, *args): def wrapper(self, *args):
if self.request is None: if self.request is None:
@ -11,8 +13,11 @@ def ensure_obsws(func):
return wrapper return wrapper
def ensure_mixer_fadeout(func): def ensure_mixer_fadeout(func):
"""ensure mixer fadeout is stopped before proceeding""" """ensure mixer is faded out before proceeding (disable monitor speaker)
Used as a decorator for functions that require the mixer to be faded out"""
def wrapper(self, *args): def wrapper(self, *args):
if self.mixer.lr.mix.fader > -90: if self.mixer.lr.mix.fader > -90:
@ -21,6 +26,7 @@ def ensure_mixer_fadeout(func):
return wrapper return wrapper
def to_snakecase(scene_name: str) -> str: def to_snakecase(scene_name: str) -> str:
"""Convert caplitalized words to lowercase snake_case""" """Convert caplitalized words to lowercase snake_case"""
return '_'.join(word.lower() for word in scene_name.split()) return '_'.join(word.lower() for word in scene_name.split())

View File

@ -23,9 +23,9 @@ classifiers = [
dynamic = ["version"] dynamic = ["version"]
dependencies = [ dependencies = [
"keyboard", "keyboard",
"obsws-python>=1.7", "obsws-python>=1.8.0",
"vban-cmd>=2.5.2", "vban-cmd>=2.5.2",
"voicemeeter-api>=2.6.1", "voicemeeter-api>=2.7.1",
"xair-api>=2.4.1", "xair-api>=2.4.1",
] ]