diff --git a/PyATEMMax/ATEMCommandHandlers.py b/PyATEMMax/ATEMCommandHandlers.py index 0bec8b5..c0969d4 100644 --- a/PyATEMMax/ATEMCommandHandlers.py +++ b/PyATEMMax/ATEMCommandHandlers.py @@ -82,14 +82,14 @@ def registerAllHandlers(self): self._sw._registerCmdHandler(funccmd, self._mainHandler) - def _mainHandler(self, cmdStr:str) -> None: + def _mainHandler(self, cmdStr: str) -> None: '''This is the main handler, it redirects calls''' - + if cmdStr not in self._AUTOMANAGED_HANDLERS: self._sw._read2InBuf() self.cmdStr = cmdStr - self._getHandler(cmdStr)() # Call specific handler + self._getHandler(cmdStr)() def _getHandler(self, cmdStr:str) -> Callable[[], None]: @@ -395,10 +395,10 @@ def _handleKeBP(self) -> None: self._d.keyer[mE][keyer].bottom = self._inBuf.getFloat(14, True, 16, 1000) value = self._inBuf.getS16(16) - self._d.keyer[mE][keyer].left = mapValue(value, -16000, 16000, -9.0, 9.0) + self._d.keyer[mE][keyer].left = mapValue(value, -16000, 16000, -16.0, 16.0) value = self._inBuf.getS16(18) - self._d.keyer[mE][keyer].right = mapValue(value, -16000, 16000, -9.0, 9.0) + self._d.keyer[mE][keyer].right = mapValue(value, -16000, 16000, -16.0, 16.0) def _handleKeLm(self) -> None: @@ -456,18 +456,12 @@ def _handleKeDV(self) -> None: self._d.key[mE][keyer].dVE.lightSource.direction = self._inBuf.getFloat(44, False, 16, 10) self._d.key[mE][keyer].dVE.lightSource.altitude = self._inBuf.getU8(46) self._d.key[mE][keyer].dVE.masked = self._inBuf.getU8Flag(47, 0) - self._d.key[mE][keyer].dVE.top = self._inBuf.getFloat(48, True, 16, 1000) - self._d.key[mE][keyer].dVE.bottom = self._inBuf.getFloat(50, True, 16, 1000) - - value = self._inBuf.getS16(52) - self._d.key[mE][keyer].dVE.left = mapValue(value, -16000, 16000, -9.0, 9.0) - - value = self._inBuf.getS16(54) - self._d.key[mE][keyer].dVE.right = mapValue(value, -16000, 16000, -9.0, 9.0) - + self._d.key[mE][keyer].dVE.top = self._inBuf.getFloat(48, False, 16, 1000) + self._d.key[mE][keyer].dVE.bottom = self._inBuf.getFloat(50, False, 16, 1000) + self._d.key[mE][keyer].dVE.left = self._inBuf.getFloat(52, False, 16, 1000) + self._d.key[mE][keyer].dVE.right = self._inBuf.getFloat(54, False, 16, 1000) self._d.key[mE][keyer].dVE.rate = self._inBuf.getU8(56) - def _handleKeFS(self) -> None: mE = self._getBufMixEffect(0) keyer = self._getBufKeyer(1) @@ -477,7 +471,7 @@ def _handleKeFS(self) -> None: self._d.keyer[mE][keyer].fly.isAtKeyFrame.b = self._inBuf.getU8Flag(6, 1) self._d.keyer[mE][keyer].fly.isAtKeyFrame.full = self._inBuf.getU8Flag(6, 2) self._d.keyer[mE][keyer].fly.isAtKeyFrame.runToInfinite = self._inBuf.getU8Flag(6, 3) - self._d.keyer[mE][keyer].fly.runtoInfiniteindex = self._inBuf.getU8(7) + self._d.keyer[mE][keyer].fly.runtoInfiniteindex = self._inBuf.getU8(5) # Changed from 7 to 5 def _handleKKFP(self) -> None: @@ -530,11 +524,11 @@ def _handleDskP(self) -> None: self._d.downstreamKeyer[dsk].top = self._inBuf.getFloat(10, True, 16, 1000) self._d.downstreamKeyer[dsk].bottom = self._inBuf.getFloat(12, True, 16, 1000) - value = self._inBuf.getS16(4) - self._d.downstreamKeyer[dsk].left = mapValue(value, -16000, 16000, -9.0, 9.0) + value = self._inBuf.getS16(14) + self._d.downstreamKeyer[dsk].left = mapValue(value, -16000, 16000, -16.0, 16.0) value = self._inBuf.getS16(16) - self._d.downstreamKeyer[dsk].right = mapValue(value, -16000, 16000, -9.0, 9.0) + self._d.downstreamKeyer[dsk].right = mapValue(value, -16000, 16000, -16.0, 16.0) def _handleDskS(self) -> None: @@ -542,8 +536,7 @@ def _handleDskS(self) -> None: self._d.downstreamKeyer[dsk].onAir = self._inBuf.getU8Flag(1, 0) self._d.downstreamKeyer[dsk].inTransition = self._inBuf.getU8Flag(2, 0) self._d.downstreamKeyer[dsk].isAutoTransitioning = self._inBuf.getU8Flag(3, 0) - self._d.downstreamKeyer[dsk].framesRemaining = self._inBuf.getU8(4) - + self._d.downstreamKeyer[dsk].framesRemaining = self._inBuf.getU8(5) def _handleFtbP(self) -> None: mE = self._getBufMixEffect(0) @@ -557,6 +550,39 @@ def _handleFtbS(self) -> None: self._d.fadeToBlack[mE].state.framesRemaining = self._inBuf.getU8(3) + def _handleTEST(self) -> None: + """Test method to see if new methods work""" + pass + + + def _handleFEna(self) -> None: + """Handle FEna (Fade Enable/Disable) state updates""" + try: + mE = self._getBufMixEffect(0) + + # Get packet bytes + byte0 = self._inBuf.getU8(0) + byte1 = self._inBuf.getU8(1) + + # Try to get byte2 safely + try: + byte2 = self._inBuf.getU8(2) + except: + byte2 = 0 + + # Parse based on your Wireshark analysis + if byte0 == 0 and byte1 == 1: + # Enable packet (00 01) + self._d.fadeToBlack[mE].disabled = False + else: + # Disable packet or other pattern + self._d.fadeToBlack[mE].disabled = True + + except Exception as e: + # Don't crash on parsing errors + pass + + def _handleColV(self) -> None: colorGenerator = self._getBufEnum(0, 8, self._p.colorGenerators) self._d.colorGenerator[colorGenerator].hue = self._inBuf.getFloat(2, False, 16, 10) @@ -909,3 +935,155 @@ def _handleTime(self) -> None: def _handleNOTIMPLEMENTED(self) -> None: pass + + def _handleKACC(self) -> None: + """Handle Key Chroma Sample status response""" + + # Read the data + mE_val = self._inBuf.getU8(0) + keyer_val = self._inBuf.getU8(1) + sample_flag = self._inBuf.getU8(2) != 0 + + # Check byte 3 for preview mode indicator + preview_flag = self._inBuf.getU8(3) == 0x01 + + # Read position data as SIGNED 16-bit integers + x_unsigned = self._inBuf.getU16(4) + y_unsigned = self._inBuf.getU16(6) + size_raw = self._inBuf.getU16(8) + + # Convert unsigned to signed + x_raw = x_unsigned if x_unsigned < 0x8000 else x_unsigned - 0x10000 + y_raw = y_unsigned if y_unsigned < 0x8000 else y_unsigned - 0x10000 + + # Get the enum objects + mE = self._p.mixEffects[mE_val] if mE_val < len(self._p.mixEffects) else self._p.mixEffects[0] + keyer = self._p.keyers[keyer_val] if keyer_val < len(self._p.keyers) else self._p.keyers[0] + + # Use the correct ranges from OpenSwitcher documentation + # X normalization: -16000 to +16000 (symmetric) + x_min = -16000 + x_max = 16000 + x_pos = (x_raw - x_min) / (x_max - x_min) + + # Y normalization: -9000 to +9000 (symmetric) + # -9000 = bottom (0.0), +9000 = top (1.0) + y_min = -9000 + y_max = 9000 + y_pos = (y_raw - y_min) / (y_max - y_min) + + # Size normalization: 620 to 9925 + size_min = 620 + size_max = 9925 + if size_raw <= size_min: + size_pos = 0.0 + elif size_raw >= size_max: + size_pos = 1.0 + else: + size_pos = (size_raw - size_min) / (size_max - size_min) + + # Clamp all values to 0.0-1.0 + x_pos = max(0.0, min(1.0, x_pos)) + y_pos = max(0.0, min(1.0, y_pos)) + size_pos = max(0.0, min(1.0, size_pos)) + + # Update state + self._d.key[mE][keyer].chroma.sample = sample_flag + self._d.key[mE][keyer].chroma.preview = preview_flag + self._d.key[mE][keyer].chroma.samplePosition.x = x_pos + self._d.key[mE][keyer].chroma.samplePosition.y = y_pos + self._d.key[mE][keyer].chroma.sampleSize = size_pos + + if len(self._inBuf) >= 16: + # Read 16-bit YCbCr values + y_raw = self._inBuf.getU16(10) + cb_raw = self._inBuf.getU16(12) + cr_raw = self._inBuf.getU16(14) + + # From OpenSwitcher: Y = (field[7] - 625) / 8544 + # This means Y range is 625 to 9169 (625 + 8544) + # Cb/Cr use 5000 as center with ±5000 range + Y_MIN = 625 + Y_MAX = 9169 + CHROMA_CENTER = 5000 + CHROMA_RANGE = 5000 + + # Normalize values using OpenSwitcher's formulas + y_norm = (y_raw - Y_MIN) / (Y_MAX - Y_MIN) # 0.0 to 1.0 + cb_norm = (cb_raw - CHROMA_CENTER) / CHROMA_RANGE # -1.0 to 1.0 + cr_norm = (cr_raw - CHROMA_CENTER) / CHROMA_RANGE # -1.0 to 1.0 + + # Clamp color values + y_norm = max(0.0, min(1.0, y_norm)) + cb_norm = max(-1.0, min(1.0, cb_norm)) + cr_norm = max(-1.0, min(1.0, cr_norm)) + + # Store the sampled color + self._d.key[mE][keyer].chroma.sampledColor = { + 'y_raw': y_raw, + 'cb_raw': cb_raw, + 'cr_raw': cr_raw, + 'y': y_norm, + 'cb': cb_norm, + 'cr': cr_norm + } + + def _handleKACk(self) -> None: + """Handle Key Adjustment Acknowledgment response""" + + try: + mE_val = self._inBuf.getU8(0) + keyer_val = self._inBuf.getU8(1) + + if mE_val < len(self._p.mixEffects) and keyer_val < len(self._p.keyers): + mE = self._p.mixEffects[mE_val] + keyer = self._p.keyers[keyer_val] + + if len(self._inBuf) >= 24: + + # Key Adjustments (0-100%) + foreground_raw = self._inBuf.getU16(2) + self._d.key[mE][keyer].chroma.foreground = foreground_raw / 1000.0 + + background_raw = self._inBuf.getU16(4) + self._d.key[mE][keyer].chroma.background = background_raw / 1000.0 + + keyEdge_raw = self._inBuf.getU16(6) + self._d.key[mE][keyer].chroma.keyEdge = keyEdge_raw / 1000.0 + + # Chroma Correction (0-100%) + spill_raw = self._inBuf.getU16(8) + self._d.key[mE][keyer].chroma.spill = spill_raw / 1000.0 + + flareSuppression_raw = self._inBuf.getU16(10) + self._d.key[mE][keyer].chroma.flareSuppression = flareSuppression_raw / 1000.0 + + # Color Adjustments + # ALL of these can be negative, so read as signed + + # Brightness: -100% to 100% + brightness_raw = self._inBuf.getS16(12) + self._d.key[mE][keyer].chroma.brightness = brightness_raw / 1000.0 + + # Contrast: -100% to 100% (also signed!) + contrast_raw = self._inBuf.getS16(14) + self._d.key[mE][keyer].chroma.contrast = contrast_raw / 1000.0 + + # Saturation: -100% to 200% (also signed!) + saturation_raw = self._inBuf.getS16(16) + self._d.key[mE][keyer].chroma.saturation = saturation_raw / 1000.0 + + # Red: -100% to 100% + red_raw = self._inBuf.getS16(18) + self._d.key[mE][keyer].chroma.red = red_raw / 1000.0 + + # Green: -100% to 100% + green_raw = self._inBuf.getS16(20) + self._d.key[mE][keyer].chroma.green = green_raw / 1000.0 + + # Blue: -100% to 100% + blue_raw = self._inBuf.getS16(22) + self._d.key[mE][keyer].chroma.blue = blue_raw / 1000.0 + + except Exception as e: + pass \ No newline at end of file diff --git a/PyATEMMax/ATEMConnectionManager.py b/PyATEMMax/ATEMConnectionManager.py index 909d72b..cbfe97d 100644 --- a/PyATEMMax/ATEMConnectionManager.py +++ b/PyATEMMax/ATEMConnectionManager.py @@ -83,7 +83,7 @@ def _registerCmdHandler(self, command: str, callback: Callable[[str], None]) -> self._cmdHandlers[command] = { "callback": callback } - + def __del__(self) -> None: """Things to do when killed :)""" @@ -693,7 +693,7 @@ def _setCommandHeaderWithPckId(self, headerCmdFlags: int, lengthOfData: int, rem def _sendCommand(self, bufferlength: int) -> None: """Skårhøj: void _sendPacketBuffer(uint8_t length)""" - + payload = bytes(self._outBuf[:bufferlength]) self._udp.write(payload) @@ -776,7 +776,7 @@ def _read2InBuf(self, maxBytes: Optional[int] =None) -> bool: def _prepareCommandPacket(self, cmdString: str, cmdBytes: int, indexMatch: Optional[bool]=True) -> None: """Skårhøj: void _prepareCommandPacket(const char *cmdString, uint8_t cmdBytes, bool indexMatch=true)""" - + cmdStrPos = self.atem.headerLen + self._cBBO + self.atem.cmdStrOffset # First, in case of a command bundle, check if indexes are different OR if it's an entirely different command, then increase offset to accommodate new command: @@ -808,9 +808,35 @@ def _prepareCommandPacket(self, cmdString: str, cmdBytes: int, indexMatch: Optio self._outBuf.setUserOffsetCallback(lambda offset : self.atem.headerLen + self._cBBO + self.atem.cmdHeaderLen + offset) + # Add this method to ATEMConnectionManager class: + + def _prepareCACKCommandPacket(self, cmdBytes: int): + """Special preparation for CACK commands with synchronized sequence numbers""" + + # CACK needs special handling - use next sequence after ATEM's last + self._outBuf.reset() + + # Calculate the two sequence numbers we need + seq1 = self.lastRemotePacketID + 1 + seq2 = self.lastRemotePacketID + 2 + + # Store these for the two-packet sequence + self._cackSeq1 = seq1 + self._cackSeq2 = seq2 + + # Prepare first packet with seq1 + self._prepareCommandPacket("CACK", cmdBytes, False) + + # Override the sequence number that was set by _prepareCommandPacket + # It's at position 10-11 in the header + self._outBuf.setU16(10, self._cackSeq1) + + return self._cackSeq1 + + def _finishCommandPacket(self) -> None: """Skårhøj: void _finishCommandPacket()""" - + # Reset control to user: set offset handler for output buffer self._outBuf.setUserOffsetCallback(lambda offset : offset) diff --git a/PyATEMMax/ATEMProtocol.py b/PyATEMMax/ATEMProtocol.py index 98e493a..64e52a8 100644 --- a/PyATEMMax/ATEMProtocol.py +++ b/PyATEMMax/ATEMProtocol.py @@ -135,11 +135,16 @@ class ATEMProtocol: "KeDV": 'Key DVE', "KeFS": 'Keyer Fly', "KKFP": 'Keyer Fly Key Frame', + "KACC": 'Key Chroma Sample Status', + "CACC": 'Chroma Sample Control', + "CACK": 'Chroma Key Adjustment Command', + "KACk": 'Chroma Key Adjustment Status', "DskB": 'Downstream Keyer (B)', "DskP": 'Downstream Keyer (P)', "DskS": 'Downstream Keyer (S)', "FtbP": 'Fade-To-Black', "FtbS": 'Fade-To-Black State', + "FEna": 'Fade-To-Black Enable/Disable', "ColV": 'Color Generator', "AuxS": 'Aux Source', "CCdP": 'Camera Control', @@ -149,6 +154,7 @@ class ATEMProtocol: "MPCS": 'Media Player Clip Source', "MPAS": 'Media Player Audio Source', "MPfe": 'Media Player Still Files', + "CSTL": 'Clear Media Pool Still', "MRPr": 'Macro Run Status', "MPrp": 'Macro Properties', "MRcS": 'Macro Recording Status', diff --git a/PyATEMMax/ATEMProtocolEnums.py b/PyATEMMax/ATEMProtocolEnums.py index ef20707..94507b5 100644 --- a/PyATEMMax/ATEMProtocolEnums.py +++ b/PyATEMMax/ATEMProtocolEnums.py @@ -439,6 +439,21 @@ class ATEMKeyFrames(ATEMConstantList): full = ATEMConstant('full', 3) runToInfinite = ATEMConstant('runToInfinite', 4) +class ATEMInfiniteDirections(ATEMConstantList): + """Infinite Direction list for Run to Infinite operations""" + + # Based on your working frontend direction mapping + center = ATEMConstant('center', 0) # Center buttons (expand/contract) + northWest = ATEMConstant('northWest', 1) # ↖ (top-left) + north = ATEMConstant('north', 2) # ↑ (top) + northEast = ATEMConstant('northEast', 3) # ↗ (top-right) + west = ATEMConstant('west', 4) # ← (left) + # direction 5 appears unused in your grid + east = ATEMConstant('east', 6) # → (right) + southWest = ATEMConstant('southWest', 7) # ↙ (bottom-left) + south = ATEMConstant('south', 8) # ↓ (bottom) + southEast = ATEMConstant('southEast', 9) + # ####################################################################### # diff --git a/PyATEMMax/ATEMSetterMethods.py b/PyATEMMax/ATEMSetterMethods.py index bfff47c..0141a00 100644 --- a/PyATEMMax/ATEMSetterMethods.py +++ b/PyATEMMax/ATEMSetterMethods.py @@ -244,17 +244,28 @@ def setTransitionNextTransition(self, mE: Union[ATEMConstant, str, int], nextTra mE: see ATEMMixEffects nextTransition: see ATEMTransitionStyles """ - + print(f"🔍 setTransitionNextTransition called with mE={mE}, nextTransition={nextTransition}") + mE_val = self.atem.mixEffects[mE].value - nextTransition_val = self.atem.transitionStyles[nextTransition].value + nextTransition_val = nextTransition + + print(f"🔍 mE_val={mE_val}, nextTransition_val={nextTransition_val}") indexMatch:bool = self.switcher._outBuf.getU8(1) == mE_val + print(f"🔍 indexMatch={indexMatch}") self.switcher._prepareCommandPacket("CTTp", 4, indexMatch) self.switcher._outBuf.setU8Flag(0, 1) # Bit 0: Transition Style ON self.switcher._outBuf.setU8(1, mE_val) self.switcher._outBuf.setU8(3, nextTransition_val) + + print(f"🔍 Command packet prepared: CTTp, length=4") + print(f"🔍 Byte 0 (flag): set to 1") + print(f"🔍 Byte 1 (mE): {mE_val}") + print(f"🔍 Byte 3 (nextTransition): {nextTransition_val}") + self.switcher._finishCommandPacket() + print(f"🔍 Command packet sent") def setTransitionPreviewEnabled(self, mE: Union[ATEMConstant, str, int], enabled: bool) -> None: @@ -505,21 +516,13 @@ def setTransitionWipePositionY(self, mE: Union[ATEMConstant, str, int], position def setTransitionWipeReverse(self, mE: Union[ATEMConstant, str, int], reverse: bool) -> None: - """Set Transition Wipe Reverse - - Args: - mE: see ATEMMixEffects - reverse (bool): On/Off - """ - mE_val = self.atem.mixEffects[mE].value - indexMatch:bool = self.switcher._outBuf.getU8(2) == mE_val - + self.switcher._prepareCommandPacket("CTWp", 20, indexMatch) - self.switcher._outBuf.setU8Flag(0, 8) + self.switcher._outBuf.setU8Flag(0, 0) # ✅ Changed from (0, 8) self.switcher._outBuf.setU8(2, mE_val) - self.switcher._outBuf.setU8(18, reverse) + self.switcher._outBuf.setU8(18, int(reverse)) # ✅ Already fixed self.switcher._finishCommandPacket() @@ -536,9 +539,9 @@ def setTransitionWipeFlipFlop(self, mE: Union[ATEMConstant, str, int], flipFlop: indexMatch:bool = self.switcher._outBuf.getU8(2) == mE_val self.switcher._prepareCommandPacket("CTWp", 20, indexMatch) - self.switcher._outBuf.setU8Flag(0, 9) + self.switcher._outBuf.setU8Flag(0, 1) # ✅ Changed from (0, 9) to (0, 1) self.switcher._outBuf.setU8(2, mE_val) - self.switcher._outBuf.setU8(19, flipFlop) + self.switcher._outBuf.setU8(19, int(flipFlop)) # ✅ Added int() conversion self.switcher._finishCommandPacket() @@ -729,7 +732,7 @@ def setTransitionDVEReverse(self, mE: Union[ATEMConstant, str, int], reverse: bo indexMatch:bool = self.switcher._outBuf.getU8(2) == mE_val self.switcher._prepareCommandPacket("CTDv", 20, indexMatch) - self.switcher._outBuf.setU8Flag(0, 10) + self.switcher._outBuf.setU8Flag(0, 2) self.switcher._outBuf.setU8(2, mE_val) self.switcher._outBuf.setU8(17, reverse) self.switcher._finishCommandPacket() @@ -748,7 +751,7 @@ def setTransitionDVEFlipFlop(self, mE: Union[ATEMConstant, str, int], flipFlop: indexMatch:bool = self.switcher._outBuf.getU8(2) == mE_val self.switcher._prepareCommandPacket("CTDv", 20, indexMatch) - self.switcher._outBuf.setU8Flag(0, 11) + self.switcher._outBuf.setU8Flag(0, 3) self.switcher._outBuf.setU8(2, mE_val) self.switcher._outBuf.setU8(18, flipFlop) self.switcher._finishCommandPacket() @@ -1071,7 +1074,7 @@ def setKeyerLeft(self, mE: Union[ATEMConstant, str, int], keyer: Union[ATEMConst Args: mE: see ATEMMixEffects keyer: see ATEMKeyers - left (float): -9.0-9.0 + left (float): -16.0-16.0 """ mE_val = self.atem.mixEffects[mE].value @@ -1085,7 +1088,7 @@ def setKeyerLeft(self, mE: Union[ATEMConstant, str, int], keyer: Union[ATEMConst self.switcher._outBuf.setU8(1, mE_val) self.switcher._outBuf.setU8(2, keyer_val) - value = int(mapValue(left, -9.0, 9.0, -16000, 16000)) + value = int(mapValue(left, -16.0, 16.0, -16000, 16000)) self.switcher._outBuf.setS16(8, value) self.switcher._finishCommandPacket() @@ -1111,7 +1114,7 @@ def setKeyerRight(self, mE: Union[ATEMConstant, str, int], keyer: Union[ATEMCons self.switcher._outBuf.setU8(1, mE_val) self.switcher._outBuf.setU8(2, keyer_val) - value = int(mapValue(right, -9.0, 9.0, -16000, 16000)) + value = int(mapValue(right, -16.0, 16.0, -16000, 16000)) self.switcher._outBuf.setS16(10, value) self.switcher._finishCommandPacket() @@ -2083,54 +2086,66 @@ def setKeyDVEBottom(self, mE: Union[ATEMConstant, str, int], keyer: Union[ATEMCo def setKeyDVELeft(self, mE: Union[ATEMConstant, str, int], keyer: Union[ATEMConstant, str, int], left: float) -> None: - """Set Key DVE Left - + """Set Key DVE Left - Following exact pattern of working Top/Bottom functions + Args: mE: see ATEMMixEffects - keyer: see ATEMKeyers - left (float): -9.0-9.0 + keyer: see ATEMKeyers + left (float): 0.0-52.0 """ - + mE_val = self.atem.mixEffects[mE].value keyer_val = self.atem.keyers[keyer].value - - indexMatch:bool = self.switcher._outBuf.getU8(4) == mE_val and \ - self.switcher._outBuf.getU8(5) == keyer_val - + + indexMatch: bool = self.switcher._outBuf.getU8(4) == mE_val and \ + self.switcher._outBuf.getU8(5) == keyer_val + self.switcher._prepareCommandPacket("CKDV", 64, indexMatch) + + # Flag (1, 7) is working (doesn't turn off mask anymore) self.switcher._outBuf.setU8Flag(1, 7) + self.switcher._outBuf.setU8(4, mE_val) self.switcher._outBuf.setU8(5, keyer_val) - - value = int(mapValue(left, -9.0, 9.0, -16000, 16000)) - self.switcher._outBuf.setS16(56, value) - + + # FOLLOW THE WORKING PATTERN: + # Your working Top uses position 52 + # Your working Bottom uses position 54 + # So Left should use position 56 (next in sequence) + self.switcher._outBuf.setU16(56, int(left * 1000)) + self.switcher._finishCommandPacket() - def setKeyDVERight(self, mE: Union[ATEMConstant, str, int], keyer: Union[ATEMConstant, str, int], right: float) -> None: - """Set Key DVE Right - + """Set Key DVE Right - Following exact pattern of working Top/Bottom functions + Args: mE: see ATEMMixEffects keyer: see ATEMKeyers - right (float): -9.0-9.0 + right (float): 0.0-52.0 """ - + mE_val = self.atem.mixEffects[mE].value keyer_val = self.atem.keyers[keyer].value - - indexMatch:bool = self.switcher._outBuf.getU8(4) == mE_val and \ - self.switcher._outBuf.getU8(5) == keyer_val - + + indexMatch: bool = self.switcher._outBuf.getU8(4) == mE_val and \ + self.switcher._outBuf.getU8(5) == keyer_val + self.switcher._prepareCommandPacket("CKDV", 64, indexMatch) + + # Try flag (0, 0) - need to find unused flag self.switcher._outBuf.setU8Flag(0, 0) + self.switcher._outBuf.setU8(4, mE_val) self.switcher._outBuf.setU8(5, keyer_val) - - value = int(mapValue(right, -9.0, 9.0, -16000, 16000)) - self.switcher._outBuf.setS16(58, value) - + + # FOLLOW THE WORKING PATTERN: + # Your working Top uses position 52 + # Your working Bottom uses position 54 + # Left should use position 56 + # So Right should use position 58 (next in sequence) + self.switcher._outBuf.setU16(58, int(right * 1000)) + self.switcher._finishCommandPacket() @@ -2203,26 +2218,58 @@ def setRunFlyingKeyKeyFrame(self, mE: Union[ATEMConstant, str, int], keyer: Unio self.switcher._finishCommandPacket() - def setRunFlyingKeyRuntoInfiniteindex(self, mE: Union[ATEMConstant, str, int], keyer: Union[ATEMConstant, str, int], runtoInfiniteindex: int) -> None: + def setRunFlyingKeyRuntoInfiniteindex(self, mE: Union[ATEMConstant, str, int], keyer: Union[ATEMConstant, str, int], runtoInfiniteindex: Union[ATEMConstant, int]) -> None: """Set Run Flying Key Run-to-Infinite-index - + + Sets the directional index for infinite key transitions. This must be set before + calling setRunFlyingKeyKeyFrame() with 'runToInfinite' parameter. + Args: mE: see ATEMMixEffects - keyer: see ATEMKeyerser 1-4 - runtoInfiniteindex (int): index + keyer: see ATEMKeyers 1-4 + runtoInfiniteindex: Direction index (0-9) or ATEMInfiniteDirections constant + Valid values: 0=center, 1=northWest, 2=north, 3=northEast, + 4=west, 6=east, 7=southWest, 8=south, 9=southEast + + Example: + # Set infinite direction to north-east, then run to infinite + switcher.setRunFlyingKeyRuntoInfiniteindex(0, 0, ATEMInfiniteDirections.northEast) + time.sleep(0.5) # Allow command processing + switcher.setRunFlyingKeyKeyFrame(0, 0, "runToInfinite") """ - mE_val = self.atem.mixEffects[mE].value keyer_val = self.atem.keyers[keyer].value - - indexMatch:bool = self.switcher._outBuf.getU8(1) == mE_val and \ - self.switcher._outBuf.getU8(2) == keyer_val - + + # Handle both integer and ATEMConstant direction values + if hasattr(runtoInfiniteindex, 'value'): + direction_val = runtoInfiniteindex.value + else: + direction_val = int(runtoInfiniteindex) + + # Validate direction range (direction 5 is unused in ATEM grid) + valid_directions = [0, 1, 2, 3, 4, 6, 7, 8, 9] + if direction_val not in valid_directions: + raise ValueError(f"Invalid infinite direction: {direction_val}. Valid directions: {valid_directions}") + + indexMatch: bool = self.switcher._outBuf.getU8(1) == mE_val and \ + self.switcher._outBuf.getU8(2) == keyer_val + self.switcher._prepareCommandPacket("RFlK", 8, indexMatch) - self.switcher._outBuf.setU8Flag(0, 1) - self.switcher._outBuf.setU8(1, mE_val) - self.switcher._outBuf.setU8(2, keyer_val) - self.switcher._outBuf.setU8(5, runtoInfiniteindex) + + # Clear buffer to ensure clean state + for i in range(8): + self.switcher._outBuf.setU8(i, 0) + + # Packet structure determined through protocol analysis of ATEM Software communication + self.switcher._outBuf.setU8(0, 0x02) # Command flags (bit 1 set) + self.switcher._outBuf.setU8(1, mE_val) # Mix Effect index + self.switcher._outBuf.setU8(2, keyer_val) # Keyer index + self.switcher._outBuf.setU8(3, 0x6d) # Protocol validation byte (109) + self.switcher._outBuf.setU8(4, 0x04) # Protocol validation byte (4) + self.switcher._outBuf.setU8(5, direction_val) # Infinite direction index + self.switcher._outBuf.setU8(6, 0x00) # Reserved + self.switcher._outBuf.setU8(7, 0x00) # Reserved + self.switcher._finishCommandPacket() @@ -2437,7 +2484,7 @@ def setDownstreamKeyerLeft(self, keyer: Union[ATEMConstant, str, int], left: flo Args: keyer: see ATEMKeyers - left (float): -9.0-9.0 + left (float): -16.0-16.0 """ keyer_val = self.atem.keyers[keyer].value @@ -2448,7 +2495,7 @@ def setDownstreamKeyerLeft(self, keyer: Union[ATEMConstant, str, int], left: flo self.switcher._outBuf.setU8Flag(0, 3) self.switcher._outBuf.setU8(1, keyer_val) - value = int(mapValue(left, -9.0, 9.0, -16000, 16000)) + value = int(mapValue(left, -16.0, 16.0, -16000, 16000)) self.switcher._outBuf.setS16(8, value) self.switcher._finishCommandPacket() @@ -2459,7 +2506,7 @@ def setDownstreamKeyerRight(self, keyer: Union[ATEMConstant, str, int], right: f Args: keyer: see ATEMKeyers - right (float): -9.0-9.0 + right (float): -16.0-16.0 """ keyer_val = self.atem.keyers[keyer].value @@ -2470,7 +2517,7 @@ def setDownstreamKeyerRight(self, keyer: Union[ATEMConstant, str, int], right: f self.switcher._outBuf.setU8Flag(0, 4) self.switcher._outBuf.setU8(1, keyer_val) - value = int(mapValue(right, -9.0, 9.0, -16000, 16000)) + value = int(mapValue(right, -16.0, 16.0, -16000, 16000)) self.switcher._outBuf.setS16(10, value) self.switcher._finishCommandPacket() @@ -2512,6 +2559,31 @@ def setFadeToBlackRate(self, mE: Union[ATEMConstant, str, int], rate: int) -> No self.switcher._outBuf.setU8(2, rate) self.switcher._finishCommandPacket() + + # Replace your setFadeToBlackDisabled method with this final version: + + def setFadeToBlackDisabled(self, mE: Union[ATEMConstant, str, int], disabled: bool) -> None: + mE_val = self.atem.mixEffects[mE].value + + print(f"Setting FTB disabled: ME{mE_val} = {disabled}") + + if disabled: + # DISABLE packet + indexMatch: bool = self.switcher._outBuf.getU8(1) == mE_val + self.switcher._prepareCommandPacket("FEna", 4, indexMatch) + self.switcher._outBuf.setU8Flag(0, 0) + self.switcher._outBuf.setU8(1, mE_val) + self.switcher._outBuf.setU8(2, 0) + print(f"Sent disable packet: [00 {mE_val:02x} 00]") + else: + # ENABLE packet + self.switcher._prepareCommandPacket("FEna", 4, False) + self.switcher._outBuf.setU8(0, 0) + self.switcher._outBuf.setU8(1, 1) + print(f"Sent enable packet: [00 01]") + + self.switcher._finishCommandPacket() + def setColorGeneratorHue(self, colorGenerator: Union[ATEMConstant, str, int], hue: float) -> None: """Set Color Generator Hue @@ -3388,6 +3460,21 @@ def setMediaPlayerSourceClipIndex(self, mediaPlayer: Union[ATEMConstant, str, in self.switcher._finishCommandPacket() + def clearMediaPoolStill(self, slot: int) -> None: + """Clear a still from the Media Pool + + Args: + slot (int): 0-based media pool still slot index + """ + + self.switcher._prepareCommandPacket("CSTL", 4) + self.switcher._outBuf.setU8(0, slot) + self.switcher._outBuf.setU8(1, 0x00) + self.switcher._outBuf.setU8(2, 0x00) + self.switcher._outBuf.setU8(3, 0x00) + self.switcher._finishCommandPacket() + + def setMediaPoolStorageClip1MaxLength(self, clip1MaxLength: int) -> None: """Set Media Pool Storage Clip 1 Max Length @@ -4089,3 +4176,573 @@ def setResetAudioMixerPeaksMaster(self, master: bool) -> None: self.switcher._outBuf.setU8Flag(0, 2) self.switcher._outBuf.setU8(4, master) self.switcher._finishCommandPacket() + + def setKeyChromaSample(self, mE, keyer, sample): + """Enable/disable chroma sample cursor""" + mE_val = self.atem.mixEffects[mE].value + keyer_val = self.atem.keyers[keyer].value + + self.switcher._prepareCommandPacket("CACC", 20) + + # Mask byte - bit 0 for cursor enable + mask = 0x01 + + self.switcher._outBuf.setU8(0, mask) + self.switcher._outBuf.setU8(1, mE_val) # M/E index + self.switcher._outBuf.setU8(2, keyer_val) # Keyer index + self.switcher._outBuf.setU8(3, 0x01 if sample else 0x00) # Enable cursor + + # Rest stays at 0 + self.switcher._finishCommandPacket() + + def setKeyChromaSamplePosition(self, mE, keyer, x, y): + """Set chroma sample cursor position + x: 0.0-1.0 (0=left, 1=right) + y: 0.0-1.0 (0=bottom, 1=top) + """ + mE_val = self.atem.mixEffects[mE].value + keyer_val = self.atem.keyers[keyer].value + + # Convert 0.0-1.0 to ATEM coordinates using correct ranges + # X: -16000 to +16000 (symmetric) + x_min = -16000 + x_max = 16000 + x_val = int(x_min + x * (x_max - x_min)) + + # Y: -9000 to +9000 (symmetric) + # 0.0 maps to -9000 (bottom), 1.0 maps to +9000 (top) + y_min = -9000 + y_max = 9000 + y_val = int(y_min + y * (y_max - y_min)) + + self.switcher._prepareCommandPacket("CACC", 20) + + # Mask byte - bits 2 and 3 for X and Y + mask = 0x0C # 0b00001100 + + self.switcher._outBuf.setU8(0, mask) + self.switcher._outBuf.setU8(1, mE_val) # M/E index + self.switcher._outBuf.setU8(2, keyer_val) # Keyer index + self.switcher._outBuf.setU8(3, 0x00) # cursor (not changing) + self.switcher._outBuf.setU8(4, 0x00) # preview (not changing) + self.switcher._outBuf.setU8(5, 0x00) # padding + self.switcher._outBuf.setS16(6, x_val) # Cursor X + self.switcher._outBuf.setS16(8, y_val) # Cursor Y + + self.switcher._finishCommandPacket() + + def setKeyChromaSampleSize(self, mE, keyer, size): + """Set chroma sample cursor size + size: 0.0-1.0 normalized size + """ + mE_val = self.atem.mixEffects[mE].value + keyer_val = self.atem.keyers[keyer].value + + # Convert 0.0-1.0 to ATEM range [620-9925] + size_min = 620 + size_max = 9925 + size_val = int(size_min + (size * (size_max - size_min))) + + self.switcher._prepareCommandPacket("CACC", 20) + + # Mask byte - bit 4 for size + mask = 0x10 # 0b00010000 + + self.switcher._outBuf.setU8(0, mask) + self.switcher._outBuf.setU8(1, mE_val) # M/E index + self.switcher._outBuf.setU8(2, keyer_val) # Keyer index + self.switcher._outBuf.setU8(3, 0x00) # cursor (not changing) + self.switcher._outBuf.setU8(4, 0x00) # preview (not changing) + self.switcher._outBuf.setU8(5, 0x00) # padding + self.switcher._outBuf.setS16(6, 0) # X (not changing) + self.switcher._outBuf.setS16(8, 0) # Y (not changing) + self.switcher._outBuf.setU16(10, size_val) # Size + + self.switcher._finishCommandPacket() + + def setKeyChromaSamplePreview(self, mE, keyer, preview): + """Enable/disable chroma sample preview""" + mE_val = self.atem.mixEffects[mE].value + keyer_val = self.atem.keyers[keyer].value + + self.switcher._prepareCommandPacket("CACC", 20) + + # Mask byte - bit 1 for preview + mask = 0x02 # 0b00000010 + + self.switcher._outBuf.setU8(0, mask) + self.switcher._outBuf.setU8(1, mE_val) # M/E index + self.switcher._outBuf.setU8(2, keyer_val) # Keyer index + self.switcher._outBuf.setU8(3, 0x00) # cursor (not changing) + self.switcher._outBuf.setU8(4, 0x01 if preview else 0x00) # Enable preview + + self.switcher._finishCommandPacket() + + def setKeyChromaForeground(self, mE: Union[ATEMConstant, str, int], + keyer: Union[ATEMConstant, str, int], + foreground: float) -> None: + """Set Key Chroma Foreground""" + + mE_val = self.atem.mixEffects[mE].value + keyer_val = self.atem.keyers[keyer].value + foreground_val = int(foreground * 1000) + + # Get current chroma values for other parameters + try: + chroma = self.key[mE_val][keyer_val].chroma + background_val = int(chroma.background * 1000) + keyEdge_val = int(chroma.keyEdge * 1000) + spill_val = int(chroma.spill * 1000) + flare_val = int(chroma.flareSuppression * 1000) + except Exception: + background_val = 804 + keyEdge_val = 747 + spill_val = 835 + flare_val = 348 + + self.switcher._prepareCommandPacket("CACK", 28) + + self.switcher._outBuf.setU16(0, 1 << 0) # Mask: foreground only + self.switcher._outBuf.setU8(2, mE_val) # M/E index + self.switcher._outBuf.setU8(3, keyer_val) # Keyer index + self.switcher._outBuf.setU16(4, foreground_val) # Foreground + self.switcher._outBuf.setU16(6, background_val) # Background + self.switcher._outBuf.setU16(8, keyEdge_val) # Key edge + self.switcher._outBuf.setU16(10, spill_val) # Spill + self.switcher._outBuf.setU16(12, flare_val) # Flare + self.switcher._outBuf.setU16(14, 0) # Brightness + self.switcher._outBuf.setU16(16, 0) # Contrast + self.switcher._outBuf.setU16(18, 1000) # Saturation + self.switcher._outBuf.setU16(20, 0) # Red + self.switcher._outBuf.setU16(22, 0) # Green + self.switcher._outBuf.setU16(24, 0) # Blue + self.switcher._outBuf.setU16(26, 0) # Padding + + self.switcher._finishCommandPacket() + + def setKeyChromaBackground(self, mE: Union[ATEMConstant, str, int], + keyer: Union[ATEMConstant, str, int], + background: float) -> None: + """Set Key Chroma Background""" + + mE_val = self.atem.mixEffects[mE].value + keyer_val = self.atem.keyers[keyer].value + background_val = int(background * 1000) + + # Get current chroma values for other parameters + try: + chroma = self.key[mE_val][keyer_val].chroma + foreground_val = int(chroma.foreground * 1000) + keyEdge_val = int(chroma.keyEdge * 1000) + spill_val = int(chroma.spill * 1000) + flare_val = int(chroma.flareSuppression * 1000) + except Exception: + foreground_val = 500 + keyEdge_val = 747 + spill_val = 835 + flare_val = 348 + + self.switcher._prepareCommandPacket("CACK", 28) + + self.switcher._outBuf.setU16(0, 1 << 1) # Mask: background only + self.switcher._outBuf.setU8(2, mE_val) # M/E index + self.switcher._outBuf.setU8(3, keyer_val) # Keyer index + self.switcher._outBuf.setU16(4, foreground_val) # Foreground + self.switcher._outBuf.setU16(6, background_val) # Background + self.switcher._outBuf.setU16(8, keyEdge_val) # Key edge + self.switcher._outBuf.setU16(10, spill_val) # Spill + self.switcher._outBuf.setU16(12, flare_val) # Flare + self.switcher._outBuf.setU16(14, 0) # Brightness + self.switcher._outBuf.setU16(16, 0) # Contrast + self.switcher._outBuf.setU16(18, 1000) # Saturation + self.switcher._outBuf.setU16(20, 0) # Red + self.switcher._outBuf.setU16(22, 0) # Green + self.switcher._outBuf.setU16(24, 0) # Blue + self.switcher._outBuf.setU16(26, 0) # Padding + + self.switcher._finishCommandPacket() + + def setKeyChromaKeyEdge(self, mE: Union[ATEMConstant, str, int], + keyer: Union[ATEMConstant, str, int], + keyEdge: float) -> None: + """Set Key Chroma Key Edge""" + + mE_val = self.atem.mixEffects[mE].value + keyer_val = self.atem.keyers[keyer].value + keyEdge_val = int(keyEdge * 1000) + + # Get current chroma values for other parameters + try: + chroma = self.key[mE_val][keyer_val].chroma + foreground_val = int(chroma.foreground * 1000) + background_val = int(chroma.background * 1000) + spill_val = int(chroma.spill * 1000) + flare_val = int(chroma.flareSuppression * 1000) + except Exception: + foreground_val = 500 + background_val = 804 + spill_val = 835 + flare_val = 348 + + self.switcher._prepareCommandPacket("CACK", 28) + + self.switcher._outBuf.setU16(0, 1 << 2) # Mask: key edge only + self.switcher._outBuf.setU8(2, mE_val) # M/E index + self.switcher._outBuf.setU8(3, keyer_val) # Keyer index + self.switcher._outBuf.setU16(4, foreground_val) # Foreground + self.switcher._outBuf.setU16(6, background_val) # Background + self.switcher._outBuf.setU16(8, keyEdge_val) # Key edge + self.switcher._outBuf.setU16(10, spill_val) # Spill + self.switcher._outBuf.setU16(12, flare_val) # Flare + self.switcher._outBuf.setU16(14, 0) # Brightness + self.switcher._outBuf.setU16(16, 0) # Contrast + self.switcher._outBuf.setU16(18, 1000) # Saturation + self.switcher._outBuf.setU16(20, 0) # Red + self.switcher._outBuf.setU16(22, 0) # Green + self.switcher._outBuf.setU16(24, 0) # Blue + self.switcher._outBuf.setU16(26, 0) # Padding + + self.switcher._finishCommandPacket() + + def setKeyChromaSpill(self, mE: Union[ATEMConstant, str, int], + keyer: Union[ATEMConstant, str, int], + spill: float) -> None: + """Set Key Chroma Spill Suppression""" + + mE_val = self.atem.mixEffects[mE].value + keyer_val = self.atem.keyers[keyer].value + spill_val = int(spill * 1000) + + # Get current chroma values for other parameters + try: + chroma = self.key[mE_val][keyer_val].chroma + foreground_val = int(chroma.foreground * 1000) + background_val = int(chroma.background * 1000) + keyEdge_val = int(chroma.keyEdge * 1000) + flare_val = int(chroma.flareSuppression * 1000) + except Exception: + foreground_val = 500 + background_val = 804 + keyEdge_val = 747 + flare_val = 348 + + self.switcher._prepareCommandPacket("CACK", 28) + + self.switcher._outBuf.setU16(0, 1 << 3) # Mask: spill only + self.switcher._outBuf.setU8(2, mE_val) # M/E index + self.switcher._outBuf.setU8(3, keyer_val) # Keyer index + self.switcher._outBuf.setU16(4, foreground_val) # Foreground + self.switcher._outBuf.setU16(6, background_val) # Background + self.switcher._outBuf.setU16(8, keyEdge_val) # Key edge + self.switcher._outBuf.setU16(10, spill_val) # Spill + self.switcher._outBuf.setU16(12, flare_val) # Flare + self.switcher._outBuf.setU16(14, 0) # Brightness + self.switcher._outBuf.setU16(16, 0) # Contrast + self.switcher._outBuf.setU16(18, 1000) # Saturation + self.switcher._outBuf.setU16(20, 0) # Red + self.switcher._outBuf.setU16(22, 0) # Green + self.switcher._outBuf.setU16(24, 0) # Blue + self.switcher._outBuf.setU16(26, 0) # Padding + + self.switcher._finishCommandPacket() + + def setKeyChromaFlareSuppression(self, mE: Union[ATEMConstant, str, int], + keyer: Union[ATEMConstant, str, int], + flareSuppression: float) -> None: + """Set Key Chroma Flare Suppression""" + + mE_val = self.atem.mixEffects[mE].value + keyer_val = self.atem.keyers[keyer].value + flare_val = int(flareSuppression * 1000) + + # Get current chroma values for other parameters + try: + chroma = self.key[mE_val][keyer_val].chroma + foreground_val = int(chroma.foreground * 1000) + background_val = int(chroma.background * 1000) + keyEdge_val = int(chroma.keyEdge * 1000) + spill_val = int(chroma.spill * 1000) + except Exception: + foreground_val = 500 + background_val = 804 + keyEdge_val = 747 + spill_val = 835 + + self.switcher._prepareCommandPacket("CACK", 28) + + self.switcher._outBuf.setU16(0, 1 << 4) # Mask: flare only + self.switcher._outBuf.setU8(2, mE_val) # M/E index + self.switcher._outBuf.setU8(3, keyer_val) # Keyer index + self.switcher._outBuf.setU16(4, foreground_val) # Foreground + self.switcher._outBuf.setU16(6, background_val) # Background + self.switcher._outBuf.setU16(8, keyEdge_val) # Key edge + self.switcher._outBuf.setU16(10, spill_val) # Spill + self.switcher._outBuf.setU16(12, flare_val) # Flare + self.switcher._outBuf.setU16(14, 0) # Brightness + self.switcher._outBuf.setU16(16, 0) # Contrast + self.switcher._outBuf.setU16(18, 1000) # Saturation + self.switcher._outBuf.setU16(20, 0) # Red + self.switcher._outBuf.setU16(22, 0) # Green + self.switcher._outBuf.setU16(24, 0) # Blue + self.switcher._outBuf.setU16(26, 0) # Padding + + self.switcher._finishCommandPacket() + + def setKeyChromaBrightness(self, mE: Union[ATEMConstant, str, int], + keyer: Union[ATEMConstant, str, int], + brightness: float) -> None: + """Set Key Chroma Brightness [-1.0 to 1.0]""" + + mE_val = self.atem.mixEffects[mE].value + keyer_val = self.atem.keyers[keyer].value + brightness_val = int(brightness * 1000) # -1000 to 1000 + + # Get current chroma values for other parameters + try: + chroma = self.key[mE_val][keyer_val].chroma + foreground_val = int(chroma.foreground * 1000) + background_val = int(chroma.background * 1000) + keyEdge_val = int(chroma.keyEdge * 1000) + spill_val = int(chroma.spill * 1000) + flare_val = int(chroma.flareSuppression * 1000) + except Exception: + foreground_val = 500 + background_val = 804 + keyEdge_val = 747 + spill_val = 835 + flare_val = 348 + + self.switcher._prepareCommandPacket("CACK", 28) + + self.switcher._outBuf.setU16(0, 1 << 5) # Mask: brightness only + self.switcher._outBuf.setU8(2, mE_val) # M/E index + self.switcher._outBuf.setU8(3, keyer_val) # Keyer index + self.switcher._outBuf.setU16(4, foreground_val) # Foreground + self.switcher._outBuf.setU16(6, background_val) # Background + self.switcher._outBuf.setU16(8, keyEdge_val) # Key edge + self.switcher._outBuf.setU16(10, spill_val) # Spill + self.switcher._outBuf.setU16(12, flare_val) # Flare + self.switcher._outBuf.setS16(14, brightness_val) # Brightness - SIGNED + self.switcher._outBuf.setU16(16, 0) # Contrast + self.switcher._outBuf.setU16(18, 1000) # Saturation + self.switcher._outBuf.setU16(20, 0) # Red + self.switcher._outBuf.setU16(22, 0) # Green + self.switcher._outBuf.setU16(24, 0) # Blue + self.switcher._outBuf.setU16(26, 0) # Padding + + self.switcher._finishCommandPacket() + + def setKeyChromaContrast(self, mE: Union[ATEMConstant, str, int], + keyer: Union[ATEMConstant, str, int], + contrast: float) -> None: + """Set Key Chroma Contrast [-1.0 to 1.0]""" + + mE_val = self.atem.mixEffects[mE].value + keyer_val = self.atem.keyers[keyer].value + contrast_val = int(contrast * 1000) # -1000 to 1000 + + try: + chroma = self.key[mE_val][keyer_val].chroma + foreground_val = int(chroma.foreground * 1000) + background_val = int(chroma.background * 1000) + keyEdge_val = int(chroma.keyEdge * 1000) + spill_val = int(chroma.spill * 1000) + flare_val = int(chroma.flareSuppression * 1000) + except Exception: + foreground_val = 500 + background_val = 804 + keyEdge_val = 747 + spill_val = 835 + flare_val = 348 + + self.switcher._prepareCommandPacket("CACK", 28) + + self.switcher._outBuf.setU16(0, 1 << 6) # Mask: contrast only + self.switcher._outBuf.setU8(2, mE_val) + self.switcher._outBuf.setU8(3, keyer_val) + self.switcher._outBuf.setU16(4, foreground_val) + self.switcher._outBuf.setU16(6, background_val) + self.switcher._outBuf.setU16(8, keyEdge_val) + self.switcher._outBuf.setU16(10, spill_val) + self.switcher._outBuf.setU16(12, flare_val) + self.switcher._outBuf.setU16(14, 0) # Brightness + self.switcher._outBuf.setS16(16, contrast_val) # Contrast - SIGNED + self.switcher._outBuf.setU16(18, 1000) # Saturation + self.switcher._outBuf.setU16(20, 0) # Red + self.switcher._outBuf.setU16(22, 0) # Green + self.switcher._outBuf.setU16(24, 0) # Blue + self.switcher._outBuf.setU16(26, 0) # Padding + + self.switcher._finishCommandPacket() + + def setKeyChromaSaturation(self, mE: Union[ATEMConstant, str, int], + keyer: Union[ATEMConstant, str, int], + saturation: float) -> None: + """Set Key Chroma Saturation [0.0 to 2.0]""" + + mE_val = self.atem.mixEffects[mE].value + keyer_val = self.atem.keyers[keyer].value + saturation_val = int(saturation * 1000) # 0 to 2000 + + try: + chroma = self.key[mE_val][keyer_val].chroma + foreground_val = int(chroma.foreground * 1000) + background_val = int(chroma.background * 1000) + keyEdge_val = int(chroma.keyEdge * 1000) + spill_val = int(chroma.spill * 1000) + flare_val = int(chroma.flareSuppression * 1000) + except Exception: + foreground_val = 500 + background_val = 804 + keyEdge_val = 747 + spill_val = 835 + flare_val = 348 + + self.switcher._prepareCommandPacket("CACK", 28) + + self.switcher._outBuf.setU16(0, 1 << 7) # Mask: saturation only + self.switcher._outBuf.setU8(2, mE_val) + self.switcher._outBuf.setU8(3, keyer_val) + self.switcher._outBuf.setU16(4, foreground_val) + self.switcher._outBuf.setU16(6, background_val) + self.switcher._outBuf.setU16(8, keyEdge_val) + self.switcher._outBuf.setU16(10, spill_val) + self.switcher._outBuf.setU16(12, flare_val) + self.switcher._outBuf.setU16(14, 0) # Brightness + self.switcher._outBuf.setU16(16, 0) # Contrast + self.switcher._outBuf.setU16(18, saturation_val) # Saturation + self.switcher._outBuf.setU16(20, 0) # Red + self.switcher._outBuf.setU16(22, 0) # Green + self.switcher._outBuf.setU16(24, 0) # Blue + self.switcher._outBuf.setU16(26, 0) # Padding + + self.switcher._finishCommandPacket() + + def setKeyChromaRed(self, mE: Union[ATEMConstant, str, int], + keyer: Union[ATEMConstant, str, int], + red: float) -> None: + """Set Key Chroma Red [-1.0 to 1.0]""" + + mE_val = self.atem.mixEffects[mE].value + keyer_val = self.atem.keyers[keyer].value + red_val = int(red * 1000) # -1000 to 1000 + + try: + chroma = self.key[mE_val][keyer_val].chroma + foreground_val = int(chroma.foreground * 1000) + background_val = int(chroma.background * 1000) + keyEdge_val = int(chroma.keyEdge * 1000) + spill_val = int(chroma.spill * 1000) + flare_val = int(chroma.flareSuppression * 1000) + except Exception: + foreground_val = 500 + background_val = 804 + keyEdge_val = 747 + spill_val = 835 + flare_val = 348 + + self.switcher._prepareCommandPacket("CACK", 28) + + self.switcher._outBuf.setU16(0, 1 << 8) # Mask: red only + self.switcher._outBuf.setU8(2, mE_val) + self.switcher._outBuf.setU8(3, keyer_val) + self.switcher._outBuf.setU16(4, foreground_val) + self.switcher._outBuf.setU16(6, background_val) + self.switcher._outBuf.setU16(8, keyEdge_val) + self.switcher._outBuf.setU16(10, spill_val) + self.switcher._outBuf.setU16(12, flare_val) + self.switcher._outBuf.setU16(14, 0) # Brightness + self.switcher._outBuf.setU16(16, 0) # Contrast + self.switcher._outBuf.setU16(18, 1000) # Saturation + self.switcher._outBuf.setS16(20, red_val) # Red - SIGNED + self.switcher._outBuf.setU16(22, 0) # Green + self.switcher._outBuf.setU16(24, 0) # Blue + self.switcher._outBuf.setU16(26, 0) # Padding + + self.switcher._finishCommandPacket() + + def setKeyChromaGreen(self, mE: Union[ATEMConstant, str, int], + keyer: Union[ATEMConstant, str, int], + green: float) -> None: + """Set Key Chroma Green [-1.0 to 1.0]""" + + mE_val = self.atem.mixEffects[mE].value + keyer_val = self.atem.keyers[keyer].value + green_val = int(green * 1000) # -1000 to 1000 + + try: + chroma = self.key[mE_val][keyer_val].chroma + foreground_val = int(chroma.foreground * 1000) + background_val = int(chroma.background * 1000) + keyEdge_val = int(chroma.keyEdge * 1000) + spill_val = int(chroma.spill * 1000) + flare_val = int(chroma.flareSuppression * 1000) + except Exception: + foreground_val = 500 + background_val = 804 + keyEdge_val = 747 + spill_val = 835 + flare_val = 348 + + self.switcher._prepareCommandPacket("CACK", 28) + + self.switcher._outBuf.setU16(0, 1 << 9) # Mask: green only + self.switcher._outBuf.setU8(2, mE_val) + self.switcher._outBuf.setU8(3, keyer_val) + self.switcher._outBuf.setU16(4, foreground_val) + self.switcher._outBuf.setU16(6, background_val) + self.switcher._outBuf.setU16(8, keyEdge_val) + self.switcher._outBuf.setU16(10, spill_val) + self.switcher._outBuf.setU16(12, flare_val) + self.switcher._outBuf.setU16(14, 0) # Brightness + self.switcher._outBuf.setU16(16, 0) # Contrast + self.switcher._outBuf.setU16(18, 1000) # Saturation + self.switcher._outBuf.setU16(20, 0) # Red + self.switcher._outBuf.setS16(22, green_val) # Green - SIGNED + self.switcher._outBuf.setU16(24, 0) # Blue + self.switcher._outBuf.setU16(26, 0) # Padding + + self.switcher._finishCommandPacket() + + def setKeyChromaBlue(self, mE: Union[ATEMConstant, str, int], + keyer: Union[ATEMConstant, str, int], + blue: float) -> None: + """Set Key Chroma Blue [-1.0 to 1.0]""" + + mE_val = self.atem.mixEffects[mE].value + keyer_val = self.atem.keyers[keyer].value + blue_val = int(blue * 1000) # -1000 to 1000 + + try: + chroma = self.key[mE_val][keyer_val].chroma + foreground_val = int(chroma.foreground * 1000) + background_val = int(chroma.background * 1000) + keyEdge_val = int(chroma.keyEdge * 1000) + spill_val = int(chroma.spill * 1000) + flare_val = int(chroma.flareSuppression * 1000) + except Exception: + foreground_val = 500 + background_val = 804 + keyEdge_val = 747 + spill_val = 835 + flare_val = 348 + + self.switcher._prepareCommandPacket("CACK", 28) + + self.switcher._outBuf.setU16(0, 1 << 10) # Mask: blue only + self.switcher._outBuf.setU8(2, mE_val) + self.switcher._outBuf.setU8(3, keyer_val) + self.switcher._outBuf.setU16(4, foreground_val) + self.switcher._outBuf.setU16(6, background_val) + self.switcher._outBuf.setU16(8, keyEdge_val) + self.switcher._outBuf.setU16(10, spill_val) + self.switcher._outBuf.setU16(12, flare_val) + self.switcher._outBuf.setU16(14, 0) # Brightness + self.switcher._outBuf.setU16(16, 0) # Contrast + self.switcher._outBuf.setU16(18, 1000) # Saturation + self.switcher._outBuf.setU16(20, 0) # Red + self.switcher._outBuf.setU16(22, 0) # Green + self.switcher._outBuf.setS16(24, blue_val) # Blue - SIGNED + self.switcher._outBuf.setU16(26, 0) # Padding + + self.switcher._finishCommandPacket() \ No newline at end of file diff --git a/PyATEMMax/StateData/FadeToBlack.py b/PyATEMMax/StateData/FadeToBlack.py index dfbcf43..3dba854 100644 --- a/PyATEMMax/StateData/FadeToBlack.py +++ b/PyATEMMax/StateData/FadeToBlack.py @@ -1,18 +1,8 @@ -#!/usr/bin/env python3 -# coding: utf-8 -""" -PyATEMMax state data: FadeToBlack -Part of the PyATEMMax library. -""" - -# pylint: disable=missing-class-docstring - from PyATEMMax.ATEMProtocol import ATEMProtocol from PyATEMMax.ATEMValueDict import ATEMValueDict class FadeToBlack(): - class State(): def __init__(self): self.framesRemaining: int = 0 @@ -22,6 +12,7 @@ def __init__(self): def __init__(self): self.rate: int = 0 self.state: FadeToBlack.State = FadeToBlack.State() + self.disabled: bool = False class FadeToBlackList(ATEMValueDict[FadeToBlack]): diff --git a/PyATEMMax/StateData/Key.py b/PyATEMMax/StateData/Key.py index c8c914f..e9e45c2 100644 --- a/PyATEMMax/StateData/Key.py +++ b/PyATEMMax/StateData/Key.py @@ -108,6 +108,40 @@ def __init__(self): # Key.Chroma self.lift: float = 0.0 self.narrow: bool = False self.ySuppress: float = 0.0 + self.sample: bool = False + self.preview: bool = False # Add this line + self.samplePosition: Key.Chroma.Position = Key.Chroma.Position() + self.sampleSize: float = 0.0 + self.sampledColor: dict = { + 'y_raw': 0, # Raw Y value (0-10000) + 'cb_raw': 5000, # Raw Cb value (centered at 5000) + 'cr_raw': 5000, # Raw Cr value (centered at 5000) + 'y': 0.0, # Normalized Y (0.0-1.0) + 'cb': 0.0, # Normalized Cb (-1.0 to 1.0) + 'cr': 0.0 # Normalized Cr (-1.0 to 1.0) + } + + self.foreground: float = 0.0 + self.background: float = 0.0 + self.keyEdge: float = 0.5 # Default 50% + + # Chroma Correction fields + self.spill: float = 0.0 + self.flareSuppression: float = 0.0 + + # Add Color Adjustment fields if needed + self.brightness: float = 1.0 + self.contrast: float = 0.5 + self.saturation: float = 1.0 + self.red: float = 0.5 + self.green: float = 0.5 + self.blue: float = 0.5 + + + class Position(): + def __init__(self): # Key.Chroma.Position + self.x: float = 0.5 # Default to center + self.y: float = 0.5 # Default to center def __init__(self): # Key diff --git a/PyATEMMax/StateData/Transition.py b/PyATEMMax/StateData/Transition.py index ebe4afe..bbd4ec2 100644 --- a/PyATEMMax/StateData/Transition.py +++ b/PyATEMMax/StateData/Transition.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # coding: utf-8 """ -PyATEMMax state data: Transition +PyATEMMax state data: Transition - Enhanced with Stinger totalRate Part of the PyATEMMax library. """ @@ -55,6 +55,44 @@ def __init__(self): # Transition.Stinger self.source: ATEMConstant = ATEMConstant() self.triggerPoint: int = 0 + @property + def totalRate(self) -> int: + """ + Get the total sting rate (mix rate + clip duration) + This matches what ATEM software control displays in the rate field + + Returns: + int: Total transition time in frames (mixRate + clipDuration) + """ + return self.preRoll + self.clipDuration + + @property + def stingRate(self) -> int: + """ + Alias for totalRate - total sting duration including mix and clip + This matches ATEM software behavior where the rate field shows total time + + Returns: + int: Total transition time in frames (mixRate + clipDuration) + """ + return self.totalRate + + @property + def totalRateSeconds(self) -> float: + """ + Get the total sting rate in seconds (assuming 25fps) + + Returns: + float: Total transition time in seconds + """ + return self.totalRate / 25.0 + + def __str__(self) -> str: + """String representation showing all sting parameters""" + return (f"Stinger(mixRate={self.mixRate}, clipDuration={self.clipDuration}, " + f"totalRate={self.totalRate}, source={self.source}, " + f"triggerPoint={self.triggerPoint})") + class Wipe(): class Position():