diff --git a/PyATEMMax/ATEMCommandHandlers.py b/PyATEMMax/ATEMCommandHandlers.py index 0bec8b5..9a8ca1b 100644 --- a/PyATEMMax/ATEMCommandHandlers.py +++ b/PyATEMMax/ATEMCommandHandlers.py @@ -11,7 +11,7 @@ from typing import Callable -from .ATEMUtils import boolBit, mapValue +from .ATEMUtils import boolBit, mapValue, getComponents from .ATEMProtocolEnums import * from .ATEMException import ATEMException @@ -900,6 +900,151 @@ def _handleTlSr(self) -> None: self._d.tally.bySource.flags[videoSource].preview = self._inBuf.getU8Flag(byteOffset+2, 1) + def _handleFASP(self) -> None: + """ + Fairlight audio mixer state. + Gets 48 bytes. + --- + | Byte # | Description | Value Range | + |--------|-------------------|-------------------------------| + | 0 | Input Index | ATEMAudioSources | + | 8 | Audio Source | fairlightMixerSourceTypes | + | 16 | Source Type | ?? | + | 17 | Max Frames Delay | from 0 to 8 | + | 18 | Frames Delay | from 0 to 8 | + | 20 | Gain | from -10000 to 600 | + | 29 | EQ enabled | True if > 0 | + | 32 | EQ gain | from -2000 to 2000 | + | 36 | Dynamics gain | from 0 to 2000 | + | 40 | Balance | from -10000 to 10000 | + | 44 | Volume (Fader) | from -10000 to 1000 | + | 48 | Valid mix options | from 0b0000 to 0b0111 | + | 49 | Mix option | fairlightMixerInputMixOptions | + --- + """ + audioInput = self._getBufAudioSource(0) + _audioSource = self._getBufEnum(8, 64, self._p.fairlightMixerSourceTypes) + + # Not quite sure what this does and how to use it + self._d.fairlightMixer.input[audioInput].type = self._getBufEnum(16, 8, self._p.fairlightMixerInputTypes) + + self._d.fairlightMixer.input[audioInput].maxFramesDelay = self._inBuf.getU8(17) + self._d.fairlightMixer.input[audioInput].framesDelay = self._inBuf.getU8(18) + + self._d.fairlightMixer.input[audioInput].gain = self._inBuf.getS32(20) / 100 + + self._d.fairlightMixer.input[audioInput].hasStereoSimulation = self._inBuf.getU8(24) > 0 + self._d.fairlightMixer.input[audioInput].stereoSimulation = self._inBuf.getS32(26) + + self._d.fairlightMixer.input[audioInput].equalizer.bandCount = self._inBuf.getU8(28) + self._d.fairlightMixer.input[audioInput].equalizer.enabled = self._inBuf.getS32(29) > 0 + self._d.fairlightMixer.input[audioInput].equalizer.gain = self._inBuf.getS32(32) / 100 + + self._d.fairlightMixer.input[audioInput].dynamics.makeUpGain = self._inBuf.getS32(36) / 100 + + self._d.fairlightMixer.input[audioInput].balance = self._inBuf.getS16(40) / 100 + self._d.fairlightMixer.input[audioInput].volume = self._inBuf.getS32(44) / 100 + + self._d.fairlightMixer.input[audioInput].validMixOptions = getComponents(self._inBuf.getU8(48)) + self._d.fairlightMixer.input[audioInput].mixOption = self._getBufEnum(49, 8, self._p.fairlightMixerInputMixOptions) + + + def _handleAEBP(self) -> None: + """ + Fairlight input EQ band state. + Gets 32 bytes. + --- + | Byte # | Description | Value Range | + |--------|----------------------------|----------------------------| + | 0 | Input Index | ATEMAudioSources | + | 8 | Audio Source | fairlightMixerSourceTypes | + | 16 | EQ band index | from 0 to 5 | + | 17 | EQ band enabled | True if > 0 | + | 18 | Supported Shapes | fairlightEQFilters | + | 19 | Shape | fairlightEQFilters | + | 20 | Supported frequency ranges | fairlightEQFrequencyRanges | + | 21 | Frequency range | fairlightEQFrequencyRanges | + | 24 | Frequency | from 30 to 21700 | + | 28 | Gain | from -2000 to 2000 | + | 32 | Q Factor | from 30 to 1030 | + --- + """ + audioInput = self._getBufAudioSource(0) + band_index = self._inBuf.getU8(16) + eq_band = self._d.fairlightMixer.input[audioInput].equalizer.band[band_index] + + eq_band.enabled = self._inBuf.getU8(17) > 0 + eq_band.supported_filters = getComponents(self._inBuf.getU8(18)) + eq_band.filter = self._getBufEnum(19, 8, self._p.fairlightEQFilters) + eq_band.supported_frequency_ranges = getComponents(self._inBuf.getU8(20)) + eq_band.frequency_range = self._getBufEnum(21, 8, self._p.fairlightEQFrequencyRanges) + + eq_band.frequency = self._inBuf.getS32(24) + eq_band.gain = self._inBuf.getS32(28) / 100 + eq_band.qFactor = self._inBuf.getS16(32) / 100 + + + def _handleFAMP(self) -> None: + """ + Fairlight mixer master state. + Gets 16 bytes. + --- + | Byte # | Description | Value Range | + |--------|-----------------------|---------------------| + | 0 | EQ band count | from 0 to 255 | + | 1 | EQ enabled | True if > 0 | + | 4 | EQ gain | -2000 to 2000 | + | 8 | Dynamics gain | from 0 to 2000 | + | 12 | Master volume (fader) | from -10000 to 1000 | + | 16 | Follow FTB | True if > 0 | + --- + """ + self._d.fairlightMixer.master.equalizer.bandCount = self._inBuf.getU8(0) + self._d.fairlightMixer.master.equalizer.enabled = self._inBuf.getU8(1) > 0 + self._d.fairlightMixer.master.equalizer.gain = self._inBuf.getS32(4) / 100 + self._d.fairlightMixer.master.dynamics.makeUpGain = self._inBuf.getS32(8) / 100 + + self._d.fairlightMixer.master.volume = self._inBuf.getS32(12) / 100 + self._d.fairlightMixer.master.followFadeToBlack = self._inBuf.getU8(16) > 0 + + + def _handleAMBP(self) -> None: + """ + Fairlight audio master EQ band state. + Gets 20 bytes. + --- + | Byte # | Description | Value Range | + |--------|----------------------------|----------------------------| + | 0 | EQ band index | from 0 to 5 | + | 1 | EQ band enabled | True if > 0 | + | 2 | Supported Shapes | fairlightEQFilters | + | 3 | Shape | fairlightEQFilters | + | 4 | Supported frequency ranges | fairlightEQFrequencyRanges | + | 5 | Frequency range | fairlightEQFrequencyRanges | + | 8 | Frequency | from 30 to 21700 | + | 12 | Gain | from -2000 to 2000 | + | 16 | Q Factor | from 30 to 1030 | + --- + """ + band_index = self._inBuf.getU8(0) + eq_band = self._d.fairlightMixer.master.equalizer.band[band_index] + + eq_band.enabled = self._inBuf.getU8(1) > 0 + eq_band.supported_filters = getComponents(self._inBuf.getU8(2)) + eq_band.filter = self._getBufEnum(3, 8, self._p.fairlightEQFilters) + eq_band.supported_frequency_ranges = getComponents(self._inBuf.getU8(4)) + eq_band.frequency_range = self._getBufEnum(5, 8, self._p.fairlightEQFrequencyRanges) + + eq_band.frequency = self._inBuf.getS32(8) + eq_band.gain = self._inBuf.getS32(12) / 100 + eq_band.qFactor = self._inBuf.getS16(16) / 100 + + + def _handleFMTl(self) -> None: + # Fairlight mixer tally commands + pass + + def _handleTime(self) -> None: self._d.lastStateChange.timeCode.hour = self._inBuf.getU8(0) self._d.lastStateChange.timeCode.minute = self._inBuf.getU8(1) diff --git a/PyATEMMax/ATEMProtocol.py b/PyATEMMax/ATEMProtocol.py index 98e493a..d4ea7b0 100644 --- a/PyATEMMax/ATEMProtocol.py +++ b/PyATEMMax/ATEMProtocol.py @@ -77,6 +77,11 @@ class ATEMProtocol: dVETransitionStyles = ATEMDVETransitionStyles() dsks = ATEMDSKs() externalPortTypes = ATEMExternalPortTypes() + fairlightMixerInputMixOptions = ATEMFairlightMixerInputMixOptions() + fairlightMixerInputTypes = ATEMFairlightMixerInputTypes() + fairlightMixerSourceTypes = ATEMFairlightMixerSourceTypes() + fairlightEQFilters = ATEMFairlightEQFilters() + fairlightEQFrequencyRanges = ATEMFairlightEQFrequencyRanges() keyers = ATEMKeyers() keyerTypes = ATEMKeyerTypes() keyFrames = ATEMKeyFrames() @@ -161,6 +166,15 @@ class ATEMProtocol: "AMTl": 'Audio Mixer Tally', "TlIn": 'Tally By Index', "TlSr": 'Tally By Source', + + # Fairlight audio engine commands + 'FASP': 'Fairlight Audio Mixer Input', + 'AEBP': "Fairlight Audio Input EQ Bands", + 'FAMP': 'Fairlight Audio Mixer Master', + 'AMBP': "Fairlight Audio Master EQ Bands", + # 'FMLv': "Fairlight Audio Input Level", + # 'FMTl': 'Fairlight Tally Command', + "Time": 'Last State Change Time Code', 'InCm': 'Initialization Complete', diff --git a/PyATEMMax/ATEMProtocolEnums.py b/PyATEMMax/ATEMProtocolEnums.py index ef20707..9b942fa 100644 --- a/PyATEMMax/ATEMProtocolEnums.py +++ b/PyATEMMax/ATEMProtocolEnums.py @@ -322,6 +322,67 @@ class ATEMAudioMixerInputTypes(ATEMConstantList): externalAudio = ATEMConstant('externalAudio', 2) +class ATEMFairlightMixerInputMixOptions(ATEMConstantList): + """FairlightMixerInputMixOption list""" + + off = ATEMConstant('off', 1) + on = ATEMConstant('on', 2) + afv = ATEMConstant('afv', 4) + + +class ATEMFairlightMixerInputTypes(ATEMConstantList): + """FairlightMixerInputType list""" + + externalVideo = ATEMConstant('externalVideo', 0) + mediaPlayer = ATEMConstant('mediaPlayer', 1) + externalAudio = ATEMConstant('externalAudio', 2) + MADI = ATEMConstant('MADI', 3) + + +class ATEMFairlightMixerSourceTypes(ATEMConstantList): + """ATEMFairlightMixerSourceType list + Converted from signed to unsigned to allow easier parsing + """ + + stereo = ATEMConstant('stereo', 18446744073709486336) + mono1 = ATEMConstant('mono1', 18446744073709551360) + mono2 = ATEMConstant('mono2', 18446744073709551361) + + +class ATEMFairlightEQFilters(ATEMConstantList): + """ATEMFairlightEQFilters list + + https://iconcollective.edu/types-of-eq/ has good explainations + of what the different filters do. + """ + + lowShelf = ATEMConstant('lowShelf', 1 << 0) + highCut = ATEMConstant('highCut', 1 << 1) + bellCurve = ATEMConstant('bellCurve', 1 << 2) + notch = ATEMConstant('notch', 1 << 3) + lowCut = ATEMConstant('lowCut', 1 << 4) + highShelf = ATEMConstant('highShelf', 1 << 5) + + +class ATEMFairlightEQFrequencyRanges(ATEMConstantList): + """ATEMFairlightEQFrequencyRanges list + + --- + | Frequency Range | Min Freq | Max Freq | + |-----------------|----------|----------| + | low | 30 | 395 | + | midlow | 100 | 1480 | + | midhigh | 450 | 7910 | + | high | 1400 | 21700 | + --- + """ + + low = ATEMConstant('low', 1 << 0) + midlow = ATEMConstant('midlow', 1 << 1) + midhigh = ATEMConstant('midhigh', 1 << 2) + high = ATEMConstant('high', 1 << 3) + + class ATEMAudioMixerInputPlugTypes(ATEMConstantList): """AudioMixerInputPlugType list""" diff --git a/PyATEMMax/ATEMSetterMethods.py b/PyATEMMax/ATEMSetterMethods.py index bfff47c..4362854 100644 --- a/PyATEMMax/ATEMSetterMethods.py +++ b/PyATEMMax/ATEMSetterMethods.py @@ -13,6 +13,7 @@ from .ATEMUtils import mapValue from .ATEMProtocolEnums import * +from .StateData import FairlightMixer # -------------------------------------------------- # This is a trick to have type hints from classes @@ -44,6 +45,7 @@ def __init__(self): self.switcher: ATEMConnectionManager self.data: ATEMSwitcherState self.atem: ATEMProtocol + self.fairlightMixer: FairlightMixer # ####################################################################### @@ -4089,3 +4091,492 @@ def setResetAudioMixerPeaksMaster(self, master: bool) -> None: self.switcher._outBuf.setU8Flag(0, 2) self.switcher._outBuf.setU8(4, master) self.switcher._finishCommandPacket() + + + def setFairlightMixerInputMixOption(self, audioSource: Union[ATEMConstant, str, int], mixOption: Union[ATEMConstant, str, int]) -> None: + """Set Fairlight Mixer Input Mix Option + + Args: + audioSource: see ATEMAudioSources + mixOption: see ATEMFairlightMixerInputMixOptions + """ + audioSource_val = self.atem.getAudioSrc(audioSource) + mixOption_val = self.atem.fairlightMixerInputMixOptions[mixOption].value + + self.switcher._prepareCommandPacket("CFSP", 48) + self.switcher._outBuf.setU16Flag(0, 8) + self.switcher._outBuf.setU16(2, audioSource_val) + self.switcher._outBuf.setS64(8, -65280) # Hacky fix to assume stereo + + self.switcher._outBuf.setU8(44, mixOption_val) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerInputVolume(self, audioSource: Union[ATEMConstant, str, int], db: float) -> None: + """Set Fairlight Mixer Input Mix Option + + Args: + audioSource: see ATEMAudioSources + db (float): -100 to +10: volume in dB + """ + audioSource_val = self.atem.getAudioSrc(audioSource) + + self.switcher._prepareCommandPacket("CFSP", 48) + self.switcher._outBuf.setU16Flag(0, 7) + self.switcher._outBuf.setU16(2, audioSource_val) + self.switcher._outBuf.setS64(8, -65280) # Hacky fix to assume stereo + + self.switcher._outBuf.setS32(40, int(db * 100)) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerInputBalance(self, audioSource: Union[ATEMConstant, str, int], balance: float) -> None: + """Set Fairlight Mixer Input Balance + + Args: + audioSource: see ATEMAudioSources + balance (float): -100 to 100: Left/Right Extremes + """ + audioSource_val = self.atem.getAudioSrc(audioSource) + + self.switcher._prepareCommandPacket("CFSP", 48) + self.switcher._outBuf.setU16Flag(0, 6) + self.switcher._outBuf.setU16(2, audioSource_val) + self.switcher._outBuf.setS64(8, -65280) # Hacky fix to assume stereo + + self.switcher._outBuf.setS16(36, int(balance * 100)) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerInputGain(self, audioSource: Union[ATEMConstant, str, int], gain: float) -> None: + """Set Fairlight Mixer Input Gain + + Args: + audioSource: see ATEMAudioSources + gain (float): -100 to +6: gain in db + """ + audioSource_val = self.atem.getAudioSrc(audioSource) + + self.switcher._prepareCommandPacket("CFSP", 48) + self.switcher._outBuf.setU16Flag(0, 1) + self.switcher._outBuf.setU16(2, audioSource_val) + self.switcher._outBuf.setS64(8, -65280) # Hacky fix to assume stereo + + self.switcher._outBuf.setS32(20, int(gain * 100)) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerInputFramesDelay(self, audioSource: Union[ATEMConstant, str, int], delay: int) -> None: + """Set Fairlight Mixer Input Frames Delay + + Args: + audioSource: see ATEMAudioSources + delay (int): 0 to `fairlightMixer.input[].maxFramesDelay`: delay in frames + """ + audioSource_val = self.atem.getAudioSrc(audioSource) + + self.switcher._prepareCommandPacket("CFSP", 48) + self.switcher._outBuf.setU16Flag(0, 0) + self.switcher._outBuf.setU16(2, audioSource_val) + self.switcher._outBuf.setS64(8, -65280) # Hacky fix to assume stereo + + self.switcher._outBuf.setU8(16, delay) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerInputEQEnabled(self, audioSource: Union[ATEMConstant, str, int], enabled: bool) -> None: + """Enable or Disable EQ on an Input + + Args: + audioSource: see ATEMAudioSources + enabled (bool): EQ enabled/disabled + """ + audioSource_val = self.atem.getAudioSrc(audioSource) + + self.switcher._prepareCommandPacket("CFSP", 48) + self.switcher._outBuf.setU16Flag(0, 3) + self.switcher._outBuf.setU16(2, audioSource_val) + self.switcher._outBuf.setS64(8, -65280) # Hacky fix to assume stereo + + self.switcher._outBuf.setU8(26, enabled) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerInputEQGain(self, audioSource: Union[ATEMConstant, str, int], db: float) -> None: + """Set Input Gain for EQ + + Args: + audioSource: see ATEMAudioSources + db (float): -20 to +20: gain in db + """ + audioSource_val = self.atem.getAudioSrc(audioSource) + + self.switcher._prepareCommandPacket("CFSP", 48) + self.switcher._outBuf.setU16Flag(0, 4) + self.switcher._outBuf.setU16(2, audioSource_val) + self.switcher._outBuf.setS64(8, -65280) # Hacky fix to assume stereo + + self.switcher._outBuf.setS32(28, int(db * 100)) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerInputEQBandEnabled(self, audioSource: Union[ATEMConstant, str, int], band: int, enabled: bool) -> None: + """Enable or Disable an Input EQ Band + + Args: + audioSource: see ATEMAudioSources + band (int): Index of EQ band + enabled (bool): Band enabled/disabled + """ + audioSource_val = self.atem.getAudioSrc(audioSource) + + self.switcher._prepareCommandPacket("CEBP", 32) + self.switcher._outBuf.setU8Flag(0, 0) + self.switcher._outBuf.setU16(2, audioSource_val) + self.switcher._outBuf.setS64(8, -65280) # Hacky fix to assume stereo + + self.switcher._outBuf.setU8(16, band) + self.switcher._outBuf.setU8(17, enabled) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerInputEQBandFilter(self, audioSource: Union[ATEMConstant, str, int], band: int, filterShape: Union[ATEMConstant, str, int]) -> None: + """Set Filter Shape of an Input EQ Band + + Args: + audioSource: see ATEMAudioSources + band (int): Index of EQ band + filterShape: See ATEMFairlightEQFilters + """ + audioSource_val = self.atem.getAudioSrc(audioSource) + filter_val = self.atem.fairlightEQFilters[filterShape].value + + self.switcher._prepareCommandPacket("CEBP", 32) + self.switcher._outBuf.setU8Flag(0, 1) + self.switcher._outBuf.setU16(2, audioSource_val) + self.switcher._outBuf.setS64(8, -65280) # Hacky fix to assume stereo + + self.switcher._outBuf.setU8(16, band) + self.switcher._outBuf.setU8(18, filter_val) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerInputEQBandFrequencyRange(self, audioSource: Union[ATEMConstant, str, int], band: int, frequencyRange: Union[ATEMConstant, str, int]) -> None: + """Set Frequency Range of an Input EQ Band + + Args: + audioSource: see ATEMAudioSources + band (int): Index of EQ band + frequencyRange: See ATEMFairlightEQFrequencyRanges + """ + audioSource_val = self.atem.getAudioSrc(audioSource) + freq_range_val = self.atem.fairlightEQFrequencyRanges[frequencyRange].value + + self.switcher._prepareCommandPacket("CEBP", 32) + self.switcher._outBuf.setU8Flag(0, 2) + self.switcher._outBuf.setU16(2, audioSource_val) + self.switcher._outBuf.setS64(8, -65280) # Hacky fix to assume stereo + + self.switcher._outBuf.setU8(16, band) + self.switcher._outBuf.setU8(19, freq_range_val) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerInputEQBandFrequency(self, audioSource: Union[ATEMConstant, str, int], band: int, frequency: int) -> None: + """Set Frequency of an Input EQ Band + + Args: + audioSource: see ATEMAudioSources + band (int): Index of EQ band + frequency (int): +30 to +21700: frequency in Hz + """ + audioSource_val = self.atem.getAudioSrc(audioSource) + + self.switcher._prepareCommandPacket("CEBP", 32) + self.switcher._outBuf.setU8Flag(0, 3) + self.switcher._outBuf.setU16(2, audioSource_val) + self.switcher._outBuf.setS64(8, -65280) # Hacky fix to assume stereo + + self.switcher._outBuf.setU8(16, band) + self.switcher._outBuf.setS32(20, frequency) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerInputEQBandGain(self, audioSource: Union[ATEMConstant, str, int], band: int, db: float) -> None: + """Set Gain of an Input EQ Band + + Args: + audioSource: see ATEMAudioSources + band (int): Index of EQ band + db (float): -20 to +20: gain in db + """ + audioSource_val = self.atem.getAudioSrc(audioSource) + + self.switcher._prepareCommandPacket("CEBP", 32) + self.switcher._outBuf.setU8Flag(0, 4) + self.switcher._outBuf.setU16(2, audioSource_val) + self.switcher._outBuf.setS64(8, -65280) # Hacky fix to assume stereo + + self.switcher._outBuf.setU8(16, band) + self.switcher._outBuf.setS32(24, int(db * 100)) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerInputEQBandQFactor(self, audioSource: Union[ATEMConstant, str, int], band: int, qFactor: float) -> None: + """Set Q Factor of an Input EQ Band + + Args: + audioSource: see ATEMAudioSources + band (int): Index of EQ band + db (float): -0.3 to +10.3: Q Factor + """ + audioSource_val = self.atem.getAudioSrc(audioSource) + + self.switcher._prepareCommandPacket("CEBP", 32) + self.switcher._outBuf.setU8Flag(0, 5) + self.switcher._outBuf.setU16(2, audioSource_val) + self.switcher._outBuf.setS64(8, -65280) # Hacky fix to assume stereo + + self.switcher._outBuf.setU8(16, band) + self.switcher._outBuf.setS16(28, int(qFactor * 100)) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerInputEQReset(self, audioSource: Union[ATEMConstant, str, int]) -> None: + """Reset an Input EQ + Args: + audioSource: see ATEMAudioSources + """ + audioSource_val = self.atem.getAudioSrc(audioSource) + + self.switcher._prepareCommandPacket("RICE", 20) + self.switcher._outBuf.setU8Flag(0, 0) + self.switcher._outBuf.setU16(2, audioSource_val) + self.switcher._outBuf.setS64(8, -65280) # Hacky fix to assume stereo + + self.switcher._outBuf.setU8(16, True) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerInputEQBandReset(self, audioSource: Union[ATEMConstant, str, int], band: int) -> None: + """Reset an Input EQ band + + Args: + audioSource: see ATEMAudioSources + band (int): Index of EQ band + """ + audioSource_val = self.atem.getAudioSrc(audioSource) + + self.switcher._prepareCommandPacket("RICE", 20) + self.switcher._outBuf.setU8Flag(0, 1) + self.switcher._outBuf.setU16(2, audioSource_val) + self.switcher._outBuf.setS64(8, -65280) # Hacky fix to assume stereo + self.switcher._outBuf.setU8(17, band) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerMasterVolume(self, db: float) -> None: + """Set Fairlight Mixer Master Volume + + Args: + db (float): -100 to +10: volume in dB + """ + self.switcher._prepareCommandPacket("CFMP", 20) + self.switcher._outBuf.setU8Flag(0, 3) + + self.switcher._outBuf.setS32(12, int(db * 100)) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerMasterFollowFTB(self, followFTB: bool) -> None: + """Set Fairlight Mixer Master Property 'Follow Fade To Black' + + Args: + followFTB (bool): whether master mutes when FTB is active + """ + self.switcher._prepareCommandPacket("CFMP", 20) + self.switcher._outBuf.setU8Flag(0, 4) + + self.switcher._outBuf.setU8(16, followFTB) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerMasterEQEnabled(self, enabled: bool) -> None: + """Enable or Disable Master EQ + + Args: + enabled (bool): EQ enabled/disabled + """ + self.switcher._prepareCommandPacket("CFMP", 20) + self.switcher._outBuf.setU8Flag(0, 0) + + self.switcher._outBuf.setU8(1, enabled) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerMasterEQGain(self, db: float) -> None: + """Set Master EQ Gain + + Args: + db (float): -20 to +20: gain in db + """ + self.switcher._prepareCommandPacket("CFMP", 20) + self.switcher._outBuf.setU8Flag(0, 1) + + self.switcher._outBuf.setS32(4, int(db * 100)) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerMasterEQBandEnabled(self, band: int, enabled: bool) -> None: + """Enable or Disable a MasterEQ Band + + Args: + band (int): Index of EQ band + enabled (bool): Band enabled/disabled + """ + self.switcher._prepareCommandPacket("CMBP", 20) + self.switcher._outBuf.setU8Flag(0, 0) + self.switcher._outBuf.setU8(1, band) + + self.switcher._outBuf.setU8(2, enabled) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerMasterEQBandFilter(self, band: int, filterShape: Union[ATEMConstant, str, int]) -> None: + """Set Filter Shape of a Master EQ Band + + Args: + band (int): Index of EQ band + filterShape: See ATEMFairlightEQFilters + """ + filter_val = self.atem.fairlightEQFilters[filterShape].value + + self.switcher._prepareCommandPacket("CMBP", 20) + self.switcher._outBuf.setU8Flag(0, 1) + self.switcher._outBuf.setU8(1, band) + + self.switcher._outBuf.setU8(3, filter_val) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerMasterEQBandFrequencyRange(self, band: int, frequencyRange: Union[ATEMConstant, str, int]) -> None: + """Set Frequency Range of a Master EQ Band + + Args: + band (int): Index of EQ band + frequencyRange: See ATEMFairlightEQFrequencyRanges + """ + freq_range_val = self.atem.fairlightEQFrequencyRanges[frequencyRange].value + + self.switcher._prepareCommandPacket("CMBP", 20) + self.switcher._outBuf.setU8Flag(0, 2) + self.switcher._outBuf.setU8(1, band) + + self.switcher._outBuf.setU8(4, freq_range_val) + + self.switcher._finishCommandPacket() + + def setFairlightMixerMasterEQBandFrequency(self, band: int, frequency: int) -> None: + """Set Frequency of a Master EQ Band + + Args: + band (int): Index of EQ band + frequency (int): +30 to +21700: frequency in Hz + """ + self.switcher._prepareCommandPacket("CMBP", 20) + self.switcher._outBuf.setU8Flag(0, 3) + self.switcher._outBuf.setU8(1, band) + + self.switcher._outBuf.setS32(8, frequency) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerMasterEQBandGain(self, band: int, db: float) -> None: + """Set Gain of a Master EQ Band + + Args: + band (int): Index of EQ band + db (float): -20 to +20: gain in db + """ + self.switcher._prepareCommandPacket("CMBP", 20) + self.switcher._outBuf.setU8Flag(0, 4) + self.switcher._outBuf.setU8(1, band) + + self.switcher._outBuf.setS32(12, int(db * 100)) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerMasterEQBandQFactor(self, band: int, qFactor: float) -> None: + """Set Q Factor of a Master EQ Band + + Args: + band (int): Index of EQ band + db (float): -0.3 to +10.3: Q Factor + """ + self.switcher._prepareCommandPacket("CMBP", 20) + self.switcher._outBuf.setU8Flag(0, 5) + self.switcher._outBuf.setU8(1, band) + + self.switcher._outBuf.setS16(16, int(qFactor * 100)) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerMasterEQReset(self) -> None: + """Reset Master EQ""" + self.switcher._prepareCommandPacket("RMOE", 4) + self.switcher._outBuf.setU8Flag(0, 0) + self.switcher._outBuf.setU8(1, True) + + self.switcher._finishCommandPacket() + + + def setFairlightMixerMasterEQBandReset(self, band: int) -> None: + """Reset a Master EQ band + + Args: + band (int): Index of EQ band + """ + self.switcher._prepareCommandPacket("RMOE", 4) + self.switcher._outBuf.setU8Flag(0, 1) + self.switcher._outBuf.setU8(2, band) + + self.switcher._finishCommandPacket() + + + def setFairlightLevelsEnable(self, enableLevels: bool): + """Set if the Fairlight Mixer should send us audio levels. + + Be aware sending audio levels uses a significant amount of bandwidth + when active. + + Args: + enableLevels (bool): whether the fairlight mixer should send audio levels to us + """ + self.switcher._prepareCommandPacket("SFLN", 4) + self.switcher._outBuf.setU8(0, enableLevels) + + self.switcher._finishCommandPacket() diff --git a/PyATEMMax/ATEMSwitcherState.py b/PyATEMMax/ATEMSwitcherState.py index cc318fd..94ddddc 100644 --- a/PyATEMMax/ATEMSwitcherState.py +++ b/PyATEMMax/ATEMSwitcherState.py @@ -27,6 +27,7 @@ def __init__(self): self.downConverter:DownConverter = DownConverter() self.downstreamKeyer: DownStreamKeyerList = DownStreamKeyerList() self.fadeToBlack: FadeToBlackList = FadeToBlackList() + self.fairlightMixer: FairlightMixer = FairlightMixer() self.inputProperties: InputPropertiesList = InputPropertiesList() self.key: KeyList = KeyList() self.keyer: KeyerList = KeyerList() diff --git a/PyATEMMax/ATEMUtils.py b/PyATEMMax/ATEMUtils.py index a0d72ef..1bece5a 100644 --- a/PyATEMMax/ATEMUtils.py +++ b/PyATEMMax/ATEMUtils.py @@ -139,3 +139,17 @@ def mapValue(value:float, minFrom:float, maxFrom:float, minTo:float, maxTo:float rangeTo = maxTo - minTo scaled = (value - minFrom) / rangeFrom return minTo + (scaled * rangeTo) + + +def getComponents(val: int) -> list[int]: + """Binary components of an integer + + e.g. 7 (0b111) would return [0b100, 0b010, 0b001] + """ + components = [] + next_bit = 1 + while next_bit <= val: + if (val & next_bit) > 0: + components.append(next_bit) + next_bit = next_bit << 1 + return components diff --git a/PyATEMMax/StateData/FairlightMixer.py b/PyATEMMax/StateData/FairlightMixer.py new file mode 100644 index 0000000..8b14422 --- /dev/null +++ b/PyATEMMax/StateData/FairlightMixer.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# coding: utf-8 +""" +PyATEMMax state data: Fairlight Mixer +Part of the PyATEMMax library. + +New implementation of the Fairlight Audio Mixer not included +in the skaarhoj library. +""" + +# pylint: disable=missing-class-docstring + +from PyATEMMax.ATEMProtocol import ATEMProtocol +from PyATEMMax.ATEMConstant import ATEMConstant +from PyATEMMax.ATEMValueDict import ATEMValueDict + + +class FairlightMixer(): + """Blackmagic ATEM switcher: fairlightAudioMixer state data""" + + # ####################################################################### + # + # Class members + # + + class Config(): + def __init__(self): # FairlightMixer.Config + self.audioChannels: int = 0 + self.hasMonitor: bool = False + + + class Equalizer(): + def __init__(self): # FairlightMixer.Equalizer + self.enabled: bool = False + self.gain: int = 0 + self.bandCount: int = 0 + self.band: list[FairlightMixer.Equalizer.Band] = [self.Band()] * 6 + + class Band(): # FairlightMixer.Equalizer.Band + def __init__(self) -> None: + self.enabled: bool = False + self.supported_filters: list[ATEMConstant] = [] + self.filter: ATEMConstant = ATEMConstant() + self.supported_frequency_ranges: list[ATEMConstant] = [] + self.frequency_range: ATEMConstant = ATEMConstant() + self.frequency: int = 0 + self.gain: float = 0.0 + self.qFactor: float = 0.0 + + + class Dynamics(): + def __init__(self): # FairlightMixer.Dynamics + self.enabled: bool = False + self.makeUpGain: int = 0 + + + class Input(): + def __init__(self): # FairlightMixer.Input + self.maxFramesDelay: int = 0 + self.hasStereoSimulation:bool = False + self.validMixOptions:list[int] = [] + + self.framesDelay: int = 0 + self.stereoSimulation: int = 0 + self.gain: float = 0 + self.balance: float = 0.0 + self.volume: float = 0 + self.type: ATEMConstant = ATEMConstant() + self.mixOption: ATEMConstant = ATEMConstant() + + self.equalizer: FairlightMixer.Equalizer = FairlightMixer.Equalizer() + self.dynamics: FairlightMixer.Dynamics = FairlightMixer.Dynamics() + + + class InputList(ATEMValueDict[Input]): + def __init__(self): # FairlightMixer.InputList + super().__init__(FairlightMixer.Input, ATEMProtocol.audioSources) + + + class Master(): + def __init__(self): # FairlightMixer.Master + self.volume: float = 0 + self.followFadeToBlack: bool = False + + self.equalizer: FairlightMixer.Equalizer = FairlightMixer.Equalizer() + self.dynamics: FairlightMixer.Dynamics = FairlightMixer.Dynamics() + + + class Monitor(): + # Future implementation + pass + + + class Levels(): + # Future implementation + pass + + + class Tally(): + # Future implementation + pass + + + def __init__(self): # FairlightMixer + self.config = FairlightMixer.Config() + self.input: FairlightMixer.InputList = FairlightMixer.InputList() + self.master: FairlightMixer.Master = FairlightMixer.Master() + + # Future implementation + self.monitor = FairlightMixer.Monitor() + self.tally = FairlightMixer.Tally() + self.levels: FairlightMixer.Levels = FairlightMixer.Levels() diff --git a/PyATEMMax/StateData/__init__.py b/PyATEMMax/StateData/__init__.py index 89ba956..e3eaf77 100644 --- a/PyATEMMax/StateData/__init__.py +++ b/PyATEMMax/StateData/__init__.py @@ -14,6 +14,7 @@ from PyATEMMax.StateData.DownConverter import * from PyATEMMax.StateData.DownStreamKeyer import * from PyATEMMax.StateData.FadeToBlack import * +from PyATEMMax.StateData.FairlightMixer import * from PyATEMMax.StateData.InputProperties import * from PyATEMMax.StateData.Key import * from PyATEMMax.StateData.Keyer import *