mirror of
https://github.com/onyx-and-iris/duckypad-twitch.git
synced 2026-04-20 01:13:30 +00:00
Compare commits
24 Commits
4fda9ddb4d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f045c00fd | |||
| 36ffdb5c61 | |||
|
|
21775e5066 | ||
|
|
ab9332be34 | ||
|
|
66baab1a7a | ||
|
|
0218579ba8 | ||
|
|
775455e618 | ||
|
|
ebe9af8e56 | ||
|
|
530fa2ff34 | ||
|
|
5992e25c79 | ||
| b2dda092aa | |||
| 04047577c6 | |||
| 2abcaefecc | |||
| e9126f0f59 | |||
|
|
26e68900aa | ||
|
|
bb10786b94 | ||
| f94936777a | |||
|
|
e6d9092562 | ||
|
|
be71c49806 | ||
|
|
4f087a0358 | ||
|
|
e271c2a324 | ||
|
|
789f3e8491 | ||
|
|
bbdd64edb4 | ||
|
|
62297835d9 |
@@ -29,23 +29,22 @@ We use a triple pc streaming setup, one gaming pc for each of us and a third pc
|
|||||||
|
|
||||||
- Both of our microphones, as well as both gaming pc are wired into an [MR18 mixer][mr18] which itself is connected to the streaming pc.
|
- Both of our microphones, as well as both gaming pc are wired into an [MR18 mixer][mr18] which itself is connected to the streaming pc.
|
||||||
- Then we vban our microphones from the workstation off to each of our pcs in order to talk in-game. All audio is routed through [Voicemeeter][voicemeeter].
|
- Then we vban our microphones from the workstation off to each of our pcs in order to talk in-game. All audio is routed through [Voicemeeter][voicemeeter].
|
||||||
- Voicemeeter is connected to Studio ONE daw for background noise removal. Any voice communication software (such as Discord) is therefore installed onto the workstation, separate of our gaming pcs.
|
- Voicemeeter is connected to Studio ONE daw for live processing. Any voice communication software (such as Discord) is therefore installed onto the workstation, separate of our gaming pcs.
|
||||||
|
|
||||||
If you've ever attempted to setup a dual pc streaming setup, you may appreciate the challenges of a triple pc setup.
|
If you've ever attempted to setup a dual pc streaming setup, you may appreciate the challenges of a triple pc setup.
|
||||||
|
|
||||||
## Details about the code
|
## Details about the code
|
||||||
|
|
||||||
This package is for demonstration purposes only. Several of the interfaces on which it depends have been tightly coupled into a duckypad macros program.
|
This package is for demonstration purposes only. Several of the interfaces on which it depends have been merged into a duckypad macros program.
|
||||||
|
|
||||||
- The package entry point can be found at `duckypad_twitch.macros.duckypad`.
|
- The package entry point can be found at `duckypad_twitch.macros.duckypad`.
|
||||||
- A base DuckyPad class in duckypad.py is used to connect the various layers of the driver.
|
- A base DuckyPad class in duckypad.py is used to connect the various layers of the driver.
|
||||||
- Most of the audio routing for the dual stream is handled in the `Audio class` in audio.py with the aid of Voicemeeter's Remote API.
|
- Most of the audio routing for the dual stream is handled in the `Audio class` in audio.py with the aid of Voicemeeter's Remote API.
|
||||||
- Some communication with the Xair mixer and the vban protocol can also be found in this class.
|
- Some communication with the XAir mixer and the vban protocol can also be found in this class.
|
||||||
- Scene switching and some audio routing are handled in the `Scene class` in scene.py.
|
- Scene switching and some audio routing are handled in the `Scene class` in scene.py.
|
||||||
- A `OBSWS` class is used to communicate with OBS websocket.
|
- A `OBSWS` class is used to communicate with OBS websocket.
|
||||||
- Dataclasses are used to hold internal states and states are updated using event callbacks.
|
- Dataclasses are used to hold internal states and states are updated using event callbacks.
|
||||||
- Decorators are used to confirm websocket connections.
|
- Decorators are used to confirm websocket connections.
|
||||||
- A separate OBSWS class is used to handle scenes and mic muting (for a single pc stream).
|
|
||||||
- Logging is included to help with debugging but also to provide stream information in real time.
|
- Logging is included to help with debugging but also to provide stream information in real time.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ label = "Onyx Pc"
|
|||||||
A1 = false
|
A1 = false
|
||||||
A2 = false
|
A2 = false
|
||||||
A3 = false
|
A3 = false
|
||||||
A4 = false
|
A4 = true
|
||||||
A5 = true
|
A5 = false
|
||||||
B1 = false
|
B1 = false
|
||||||
B2 = false
|
B2 = false
|
||||||
B3 = false
|
B3 = false
|
||||||
@@ -57,8 +57,8 @@ label = "Iris Pc"
|
|||||||
A1 = false
|
A1 = false
|
||||||
A2 = false
|
A2 = false
|
||||||
A3 = false
|
A3 = false
|
||||||
A4 = false
|
A4 = true
|
||||||
A5 = true
|
A5 = false
|
||||||
B1 = false
|
B1 = false
|
||||||
B2 = false
|
B2 = false
|
||||||
B3 = false
|
B3 = false
|
||||||
@@ -90,8 +90,8 @@ gate.knob = 0
|
|||||||
|
|
||||||
[strip-5]
|
[strip-5]
|
||||||
label = "System"
|
label = "System"
|
||||||
A1 = false
|
A1 = true
|
||||||
A2 = true
|
A2 = false
|
||||||
A3 = false
|
A3 = false
|
||||||
A4 = false
|
A4 = false
|
||||||
A5 = false
|
A5 = false
|
||||||
@@ -107,8 +107,8 @@ limit = 0
|
|||||||
[strip-6]
|
[strip-6]
|
||||||
label = "Comms"
|
label = "Comms"
|
||||||
A1 = false
|
A1 = false
|
||||||
A2 = false
|
A2 = true
|
||||||
A3 = true
|
A3 = false
|
||||||
A4 = false
|
A4 = false
|
||||||
A5 = false
|
A5 = false
|
||||||
B1 = false
|
B1 = false
|
||||||
@@ -125,8 +125,8 @@ k = 0
|
|||||||
label = "Pretzel"
|
label = "Pretzel"
|
||||||
A1 = false
|
A1 = false
|
||||||
A2 = false
|
A2 = false
|
||||||
A3 = false
|
A3 = true
|
||||||
A4 = true
|
A4 = false
|
||||||
A5 = false
|
A5 = false
|
||||||
B1 = false
|
B1 = false
|
||||||
B2 = false
|
B2 = false
|
||||||
@@ -138,7 +138,7 @@ gain = 0.0
|
|||||||
limit = 0
|
limit = 0
|
||||||
|
|
||||||
[bus-0]
|
[bus-0]
|
||||||
label = "MR18"
|
label = "System"
|
||||||
mono = false
|
mono = false
|
||||||
eq.on = false
|
eq.on = false
|
||||||
mute = false
|
mute = false
|
||||||
@@ -146,7 +146,7 @@ gain = 0.0
|
|||||||
mode = "normal"
|
mode = "normal"
|
||||||
|
|
||||||
[bus-1]
|
[bus-1]
|
||||||
label = "ASIO [1,2]"
|
label = "Comms"
|
||||||
mono = false
|
mono = false
|
||||||
eq.on = false
|
eq.on = false
|
||||||
mute = false
|
mute = false
|
||||||
@@ -154,7 +154,7 @@ gain = 0.0
|
|||||||
mode = "normal"
|
mode = "normal"
|
||||||
|
|
||||||
[bus-2]
|
[bus-2]
|
||||||
label = "ASIO [3,4]"
|
label = "Pretzel"
|
||||||
mono = false
|
mono = false
|
||||||
eq.on = false
|
eq.on = false
|
||||||
mute = false
|
mute = false
|
||||||
@@ -162,7 +162,7 @@ gain = 0.0
|
|||||||
mode = "normal"
|
mode = "normal"
|
||||||
|
|
||||||
[bus-3]
|
[bus-3]
|
||||||
label = "ASIO [5,6]"
|
label = "GAME PCs"
|
||||||
mono = false
|
mono = false
|
||||||
eq.on = false
|
eq.on = false
|
||||||
mute = false
|
mute = false
|
||||||
@@ -170,7 +170,7 @@ gain = 0.0
|
|||||||
mode = "normal"
|
mode = "normal"
|
||||||
|
|
||||||
[bus-4]
|
[bus-4]
|
||||||
label = "ASIO [7,8]"
|
label = ""
|
||||||
mono = false
|
mono = false
|
||||||
eq.on = false
|
eq.on = false
|
||||||
mute = false
|
mute = false
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class Audio(ILayer):
|
|||||||
super().__init__(duckypad)
|
super().__init__(duckypad)
|
||||||
for attr, val in kwargs.items():
|
for attr, val in kwargs.items():
|
||||||
setattr(self, attr, val)
|
setattr(self, attr, val)
|
||||||
|
self.vm.observer.add(self.on_mdirty)
|
||||||
|
|
||||||
self.reset_states()
|
self.reset_states()
|
||||||
|
|
||||||
@@ -39,6 +40,56 @@ class Audio(ILayer):
|
|||||||
for button in Buttons:
|
for button in Buttons:
|
||||||
self.vm.button[button].stateonly = getattr(AudioState, button.name)
|
self.vm.button[button].stateonly = getattr(AudioState, button.name)
|
||||||
|
|
||||||
|
def on_mdirty(self):
|
||||||
|
"""Callback for Voicemeeter mdirty events.
|
||||||
|
|
||||||
|
|
||||||
|
This method keeps the DuckyPad state in sync with changes made from the Stream Deck"""
|
||||||
|
self.logger.debug('Voicemeeter state changed (mdirty event)')
|
||||||
|
for button in Buttons:
|
||||||
|
current_value = self.vm.button[button].stateonly
|
||||||
|
if getattr(self.state, button.name) != current_value:
|
||||||
|
match button.name:
|
||||||
|
case 'mute_mics':
|
||||||
|
if current_value:
|
||||||
|
self.logger.info('Mics Muted')
|
||||||
|
else:
|
||||||
|
self.logger.info('Mics Unmuted')
|
||||||
|
case 'only_discord':
|
||||||
|
if current_value:
|
||||||
|
self.mixer.strip[XAirStrips.comms].send[XAirBuses.stream_mix].level = -90
|
||||||
|
self.logger.info('Only Discord Enabled')
|
||||||
|
else:
|
||||||
|
self.mixer.strip[XAirStrips.comms].send[XAirBuses.stream_mix].level = -24
|
||||||
|
self.logger.info('Only Discord Disabled')
|
||||||
|
case 'only_stream':
|
||||||
|
if current_value:
|
||||||
|
self.logger.info('Only Stream Enabled')
|
||||||
|
else:
|
||||||
|
self.logger.info('Only Stream Disabled')
|
||||||
|
case 'sound_test':
|
||||||
|
if current_value:
|
||||||
|
self.logger.info('Sound Test Enabled')
|
||||||
|
else:
|
||||||
|
self.logger.info('Sound Test Disabled')
|
||||||
|
case 'patch_onyx':
|
||||||
|
if current_value:
|
||||||
|
self.logger.info('Onyx mic has been patched')
|
||||||
|
else:
|
||||||
|
self.logger.info('Onyx mic has been unpatched')
|
||||||
|
case 'patch_iris':
|
||||||
|
if current_value:
|
||||||
|
self.logger.info('Iris mic has been patched')
|
||||||
|
else:
|
||||||
|
self.logger.info('Iris mic has been unpatched')
|
||||||
|
case 'mute_game_pcs':
|
||||||
|
if current_value:
|
||||||
|
self.logger.info('Game PCs Muted')
|
||||||
|
else:
|
||||||
|
self.logger.info('Game PCs Unmuted')
|
||||||
|
|
||||||
|
setattr(self.state, button.name, current_value)
|
||||||
|
|
||||||
def mute_mics(self):
|
def mute_mics(self):
|
||||||
self.state.mute_mics = not self.state.mute_mics
|
self.state.mute_mics = not self.state.mute_mics
|
||||||
if self.state.mute_mics:
|
if self.state.mute_mics:
|
||||||
@@ -70,11 +121,13 @@ class Audio(ILayer):
|
|||||||
self.vm.bus[VMBuses.iris_mic].mute = True
|
self.vm.bus[VMBuses.iris_mic].mute = True
|
||||||
self.vm.strip[VMStrips.onyx_pc].gain = -3
|
self.vm.strip[VMStrips.onyx_pc].gain = -3
|
||||||
self.vm.strip[VMStrips.iris_pc].gain = -3
|
self.vm.strip[VMStrips.iris_pc].gain = -3
|
||||||
|
self.vm.strip[VMStrips.comms].gain = -6
|
||||||
self.vm.strip[VMStrips.pretzel].gain = -3
|
self.vm.strip[VMStrips.pretzel].gain = -3
|
||||||
self.logger.info('Only Stream Enabled')
|
self.logger.info('Only Stream Enabled')
|
||||||
else:
|
else:
|
||||||
self.vm.strip[VMStrips.onyx_pc].gain = 0
|
self.vm.strip[VMStrips.onyx_pc].gain = 0
|
||||||
self.vm.strip[VMStrips.iris_pc].gain = 0
|
self.vm.strip[VMStrips.iris_pc].gain = 0
|
||||||
|
self.vm.strip[VMStrips.comms].gain = 0
|
||||||
self.vm.strip[VMStrips.pretzel].gain = 0
|
self.vm.strip[VMStrips.pretzel].gain = 0
|
||||||
self.vm.bus[VMBuses.onyx_mic].mute = False
|
self.vm.bus[VMBuses.onyx_mic].mute = False
|
||||||
self.vm.bus[VMBuses.iris_mic].mute = False
|
self.vm.bus[VMBuses.iris_mic].mute = False
|
||||||
@@ -97,6 +150,7 @@ class Audio(ILayer):
|
|||||||
ENABLE_SOUNDTEST = {
|
ENABLE_SOUNDTEST = {
|
||||||
'A1': True,
|
'A1': True,
|
||||||
'A2': True,
|
'A2': True,
|
||||||
|
'A4': False,
|
||||||
'B1': False,
|
'B1': False,
|
||||||
'B2': False,
|
'B2': False,
|
||||||
'mono': True,
|
'mono': True,
|
||||||
@@ -104,6 +158,7 @@ class Audio(ILayer):
|
|||||||
DISABLE_SOUNDTEST = {
|
DISABLE_SOUNDTEST = {
|
||||||
'A1': False,
|
'A1': False,
|
||||||
'A2': False,
|
'A2': False,
|
||||||
|
'A4': True,
|
||||||
'B1': True,
|
'B1': True,
|
||||||
'B2': True,
|
'B2': True,
|
||||||
'mono': False,
|
'mono': False,
|
||||||
@@ -111,55 +166,71 @@ class Audio(ILayer):
|
|||||||
|
|
||||||
self.state.sound_test = not self.state.sound_test
|
self.state.sound_test = not self.state.sound_test
|
||||||
if self.state.sound_test:
|
if self.state.sound_test:
|
||||||
self.vm.strip[VMStrips.onyx_mic].apply({'A1': True, 'B1': False, 'B3': False, 'mute': False})
|
self.vm.strip[VMStrips.onyx_mic].apply({'A5': True, 'B1': False, 'B3': False, 'mute': False})
|
||||||
self.vm.strip[VMStrips.iris_mic].apply({'A1': True, 'B2': False, 'B3': False, 'mute': False})
|
self.vm.strip[VMStrips.iris_mic].apply({'A5': True, 'B2': False, 'B3': False, 'mute': False})
|
||||||
self.vm.vban.outstream[VBANChannels.onyx_mic].on = True
|
self.vm.bus[VMBuses.game_pcs].mute = True
|
||||||
self.vm.vban.outstream[VBANChannels.iris_mic].on = True
|
self.vm.vban.outstream[VBANChannels.onyx_mic].apply({'on': True, 'route': 4})
|
||||||
self.vm.vban.outstream[VBANChannels.onyx_mic].route = 0
|
self.vm.vban.outstream[VBANChannels.iris_mic].apply({'on': True, 'route': 4})
|
||||||
self.vm.vban.outstream[VBANChannels.iris_mic].route = 0
|
|
||||||
toggle_soundtest(ENABLE_SOUNDTEST)
|
toggle_soundtest(ENABLE_SOUNDTEST)
|
||||||
self.logger.info('Sound Test Enabled')
|
self.logger.info('Sound Test Enabled')
|
||||||
else:
|
else:
|
||||||
toggle_soundtest(DISABLE_SOUNDTEST)
|
toggle_soundtest(DISABLE_SOUNDTEST)
|
||||||
self.vm.vban.outstream[VBANChannels.onyx_mic].route = 5
|
self.vm.vban.outstream[VBANChannels.onyx_mic].route = 5
|
||||||
self.vm.vban.outstream[VBANChannels.iris_mic].route = 6
|
self.vm.vban.outstream[VBANChannels.iris_mic].route = 6
|
||||||
self.vm.strip[VMStrips.onyx_mic].apply({'A1': False, 'B1': True, 'B3': True, 'mute': True})
|
self.vm.bus[VMBuses.game_pcs].mute = False
|
||||||
self.vm.strip[VMStrips.iris_mic].apply({'A1': False, 'B2': True, 'B3': True, 'mute': True})
|
self.vm.strip[VMStrips.onyx_mic].apply({'A5': False, 'B1': True, 'B3': True, 'mute': True})
|
||||||
|
self.vm.strip[VMStrips.iris_mic].apply({'A5': False, 'B2': True, 'B3': True, 'mute': True})
|
||||||
|
self.vm.button[Buttons.mute_mics].stateonly = True
|
||||||
self.logger.info('Sound Test Disabled')
|
self.logger.info('Sound Test Disabled')
|
||||||
|
self.vm.button[Buttons.sound_test].stateonly = self.state.sound_test
|
||||||
|
|
||||||
@ensure_mixer_fadeout
|
@ensure_mixer_fadeout
|
||||||
def stage_onyx_mic(self):
|
def stage_onyx_mic(self):
|
||||||
"""Gain stage SE Electronics DCM8 with phantom power"""
|
"""Gain stage onyx mic"""
|
||||||
self.mixer.headamp[XAirStrips.onyx_mic].phantom = True
|
config = configuration.mic('onyx')
|
||||||
for i in range(21):
|
|
||||||
|
self.mixer.headamp[XAirStrips.onyx_mic].phantom = config['phantom']
|
||||||
|
for i in range(config['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')
|
||||||
|
|
||||||
@ensure_mixer_fadeout
|
@ensure_mixer_fadeout
|
||||||
def stage_iris_mic(self):
|
def stage_iris_mic(self):
|
||||||
"""Gain stage TLM102 with phantom power"""
|
"""Gain stage iris mic"""
|
||||||
self.mixer.headamp[XAirStrips.iris_mic].phantom = True
|
config = configuration.mic('iris')
|
||||||
for i in range(31):
|
|
||||||
|
self.mixer.headamp[XAirStrips.iris_mic].phantom = config['phantom']
|
||||||
|
for i in range(config['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 onyx mic, if phantom power was enabled, disable it"""
|
||||||
for i in reversed(range(21)):
|
config = configuration.mic('onyx')
|
||||||
|
|
||||||
|
for i in reversed(range(config['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
|
if config['phantom']:
|
||||||
self.logger.info('Onyx Mic Unstaged and Phantom Power Disabled')
|
self.mixer.headamp[XAirStrips.onyx_mic].phantom = False
|
||||||
|
self.logger.info('Onyx Mic Unstaged and Phantom Power Disabled')
|
||||||
|
else:
|
||||||
|
self.logger.info('Onyx Mic Unstaged')
|
||||||
|
|
||||||
def unstage_iris_mic(self):
|
def unstage_iris_mic(self):
|
||||||
"""Unstage TLM102 and disable phantom power"""
|
"""Unstage iris mic, if phantom power was enabled, disable it"""
|
||||||
for i in reversed(range(31)):
|
config = configuration.mic('iris')
|
||||||
|
|
||||||
|
for i in reversed(range(config['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
|
if config['phantom']:
|
||||||
self.logger.info('Iris Mic Unstaged and Phantom Power Disabled')
|
self.mixer.headamp[XAirStrips.iris_mic].phantom = False
|
||||||
|
self.logger.info('Iris Mic Unstaged and Phantom Power Disabled')
|
||||||
|
else:
|
||||||
|
self.logger.info('Iris Mic Unstaged')
|
||||||
|
|
||||||
def patch_onyx(self):
|
def patch_onyx(self):
|
||||||
self.state.patch_onyx = not self.state.patch_onyx
|
self.state.patch_onyx = not self.state.patch_onyx
|
||||||
@@ -169,6 +240,7 @@ class Audio(ILayer):
|
|||||||
else:
|
else:
|
||||||
self.vm.patch.asio[0].set(0)
|
self.vm.patch.asio[0].set(0)
|
||||||
self.logger.info('Onyx mic has been unpatched')
|
self.logger.info('Onyx mic has been unpatched')
|
||||||
|
self.vm.button[Buttons.patch_onyx].stateonly = self.state.patch_onyx
|
||||||
|
|
||||||
def patch_iris(self):
|
def patch_iris(self):
|
||||||
self.state.patch_iris = not self.state.patch_iris
|
self.state.patch_iris = not self.state.patch_iris
|
||||||
@@ -178,17 +250,43 @@ class Audio(ILayer):
|
|||||||
else:
|
else:
|
||||||
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')
|
||||||
|
self.vm.button[Buttons.patch_iris].stateonly = self.state.patch_iris
|
||||||
|
|
||||||
|
def mute_game_pcs(self):
|
||||||
|
self.state.mute_game_pcs = not self.state.mute_game_pcs
|
||||||
|
if self.state.mute_game_pcs:
|
||||||
|
self.vm.bus[VMBuses.game_pcs].mute = True
|
||||||
|
self.logger.info('Game PCs Muted')
|
||||||
|
else:
|
||||||
|
self.vm.bus[VMBuses.game_pcs].mute = False
|
||||||
|
self.logger.info('Game PCs Unmuted')
|
||||||
|
self.vm.button[Buttons.mute_game_pcs].stateonly = self.state.mute_game_pcs
|
||||||
|
|
||||||
### Workstation and TV Audio Routing via VBAN ###
|
### Workstation and TV Audio Routing via VBAN ###
|
||||||
|
|
||||||
def _fade_mixer(self, target_fader, fade_in=True):
|
def __fadein_main(self, target_level: float, duration: float = 5.0):
|
||||||
"""Fade the mixer's fader to the target level."""
|
current_level = self.mixer.lr.mix.fader
|
||||||
current_fader = self.mixer.lr.mix.fader
|
level_difference = abs(target_level - current_level)
|
||||||
step = 1 if fade_in else -1
|
steps = max(10, min(100, int(level_difference)))
|
||||||
while (fade_in and current_fader < target_fader) or (not fade_in and current_fader > target_fader):
|
step_duration = duration / steps
|
||||||
current_fader += step
|
level_step = (target_level - current_level) / steps
|
||||||
self.mixer.lr.mix.fader = current_fader
|
|
||||||
time.sleep(0.05)
|
for _ in range(steps):
|
||||||
|
current_level += level_step
|
||||||
|
self.mixer.lr.mix.fader = current_level
|
||||||
|
time.sleep(step_duration)
|
||||||
|
|
||||||
|
def __fadeout_main(self, target_level: float, duration: float = 5.0):
|
||||||
|
current_level = self.mixer.lr.mix.fader
|
||||||
|
level_difference = abs(current_level - target_level)
|
||||||
|
steps = max(10, min(100, int(level_difference)))
|
||||||
|
step_duration = duration / steps
|
||||||
|
level_step = (current_level - target_level) / steps
|
||||||
|
|
||||||
|
for _ in range(steps):
|
||||||
|
current_level -= level_step
|
||||||
|
self.mixer.lr.mix.fader = current_level
|
||||||
|
time.sleep(step_duration)
|
||||||
|
|
||||||
def _toggle_workstation_routing(self, state_attr, target_name, vban_config_key):
|
def _toggle_workstation_routing(self, state_attr, target_name, vban_config_key):
|
||||||
"""Toggle routing of workstation audio to either Onyx or Iris via VBAN."""
|
"""Toggle routing of workstation audio to either Onyx or Iris via VBAN."""
|
||||||
@@ -201,17 +299,17 @@ class Audio(ILayer):
|
|||||||
|
|
||||||
if new_state:
|
if new_state:
|
||||||
with vban_cmd.api('potato', outbound=True, **target_conn) as vban:
|
with vban_cmd.api('potato', outbound=True, **target_conn) as vban:
|
||||||
vban.vban.instream[2].on = True
|
vban.vban.instream[6].on = True
|
||||||
self.vm.strip[5].gain = -6
|
self.vm.strip[5].gain = -6
|
||||||
self.vm.vban.outstream[3].on = True
|
self.vm.vban.outstream[2].on = True
|
||||||
self._fade_mixer(-90, fade_in=False)
|
self.__fadeout_main(-90)
|
||||||
self.logger.info(f'Workstation audio routed to {target_name}')
|
self.logger.info(f'Workstation audio routed to {target_name}')
|
||||||
else:
|
else:
|
||||||
with vban_cmd.api('potato', outbound=True, **target_conn) as vban:
|
with vban_cmd.api('potato', outbound=True, **target_conn) as vban:
|
||||||
vban.vban.instream[2].on = False
|
vban.vban.instream[6].on = False
|
||||||
self.vm.strip[5].gain = 0
|
self.vm.strip[5].gain = 0
|
||||||
self.vm.vban.outstream[3].on = False
|
self.vm.vban.outstream[2].on = False
|
||||||
self._fade_mixer(-36, fade_in=True)
|
self.__fadein_main(-24)
|
||||||
self.logger.info('Workstation audio routed back to monitor speakers')
|
self.logger.info('Workstation audio routed back to monitor speakers')
|
||||||
|
|
||||||
def toggle_workstation_to_onyx(self):
|
def toggle_workstation_to_onyx(self):
|
||||||
@@ -237,7 +335,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[7].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 +345,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[7].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):
|
||||||
|
|||||||
@@ -18,3 +18,14 @@ with open(configpath, 'rb') as f:
|
|||||||
def get(name):
|
def get(name):
|
||||||
if name in configuration:
|
if name in configuration:
|
||||||
return configuration[name]
|
return configuration[name]
|
||||||
|
|
||||||
|
|
||||||
|
def mic(name):
|
||||||
|
assert 'microphones' in configuration, 'No microphones defined in configuration'
|
||||||
|
|
||||||
|
try:
|
||||||
|
mic_key = configuration['microphones'][name]
|
||||||
|
mic_cfg = configuration['microphone'][mic_key]
|
||||||
|
return mic_cfg
|
||||||
|
except KeyError as e:
|
||||||
|
raise KeyError(f'Microphone configuration for "{name}" not found') from e
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
Buttons = IntEnum('Buttons', 'mute_mics only_discord only_stream', start=0)
|
Buttons = IntEnum(
|
||||||
|
'Buttons', 'mute_mics only_discord only_stream sound_test patch_onyx patch_iris mute_game_pcs', start=0
|
||||||
|
)
|
||||||
|
|
||||||
# Voicemeeter Channels
|
# Voicemeeter Channels
|
||||||
VMStrips = IntEnum('Strips', 'onyx_mic iris_mic onyx_pc iris_pc st_input_5 system comms pretzel', start=0)
|
VMStrips = IntEnum('Strips', 'onyx_mic iris_mic onyx_pc iris_pc st_input_5 system comms pretzel', start=0)
|
||||||
VMBuses = IntEnum('Buses', 'onyx_mic iris_mic both_mics', start=5)
|
VMBuses = IntEnum('Buses', 'system comms pretzel game_pcs output_5 onyx_mic iris_mic both_mics', start=0)
|
||||||
|
|
||||||
# VBAN Channels
|
# VBAN Channels
|
||||||
VBANChannels = IntEnum('VBANChannels', 'onyx_mic iris_mic comms workstation', start=0)
|
VBANChannels = IntEnum('VBANChannels', 'onyx_mic iris_mic comms workstation', start=0)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
from .duckypad import run
|
from .main import run
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -57,7 +58,7 @@ def run():
|
|||||||
xair_config = configuration.get('xair')
|
xair_config = configuration.get('xair')
|
||||||
|
|
||||||
with (
|
with (
|
||||||
voicemeeterlib.api('potato') as vm,
|
voicemeeterlib.api('potato', mdirty=True) as vm,
|
||||||
xair_api.connect('MR18', **xair_config) as mixer,
|
xair_api.connect('MR18', **xair_config) as mixer,
|
||||||
duckypad_twitch.connect(vm=vm, mixer=mixer) as duckypad,
|
duckypad_twitch.connect(vm=vm, mixer=mixer) as duckypad,
|
||||||
):
|
):
|
||||||
@@ -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')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from .enums import VMStrips
|
from .enums import VMBuses, VMStrips
|
||||||
from .layer import ILayer
|
from .layer import ILayer
|
||||||
from .states import SceneState
|
from .states import SceneState
|
||||||
|
|
||||||
@@ -40,8 +40,7 @@ class Scene(ILayer):
|
|||||||
def dual_stream(self):
|
def dual_stream(self):
|
||||||
ENABLE_PC = {
|
ENABLE_PC = {
|
||||||
'mute': False,
|
'mute': False,
|
||||||
'A5': True,
|
f'A{VMBuses.game_pcs + 1}': True, # Voicemeeter A output is 1-indexed
|
||||||
'gain': 0,
|
|
||||||
}
|
}
|
||||||
self.vm.strip[VMStrips.onyx_pc].apply(ENABLE_PC)
|
self.vm.strip[VMStrips.onyx_pc].apply(ENABLE_PC)
|
||||||
self.vm.strip[VMStrips.iris_pc].apply(ENABLE_PC)
|
self.vm.strip[VMStrips.iris_pc].apply(ENABLE_PC)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user