bmdwire 1.2.0__tar.gz → 1.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. {bmdwire-1.2.0/bmdwire.egg-info → bmdwire-1.5.0}/PKG-INFO +2 -2
  2. {bmdwire-1.2.0 → bmdwire-1.5.0}/README.md +1 -1
  3. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/_transfer.py +9 -0
  4. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/atem.py +4 -1
  5. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/connection.py +49 -5
  6. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/docs/MACRO_FORMAT.md +3 -2
  7. bmdwire-1.5.0/atemwire/docs/MEDIA_LOCK.md +184 -0
  8. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/docs/PROFILE_FORMAT.md +31 -17
  9. bmdwire-1.5.0/atemwire/docs/SURFACE.md +1096 -0
  10. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/helpers.py +42 -1
  11. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/_helpers.py +19 -26
  12. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/fairlight.py +9 -6
  13. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/switching.py +4 -17
  14. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/input_video.py +19 -0
  15. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/upstream_keyer.py +20 -8
  16. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/pool.py +3 -2
  17. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/probe.py +9 -2
  18. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/profile/_enums.py +36 -2
  19. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/profile/apply.py +63 -13
  20. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/profile/save.py +61 -15
  21. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/protocol.py +176 -69
  22. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/transport.py +166 -13
  23. {bmdwire-1.2.0 → bmdwire-1.5.0/bmdwire.egg-info}/PKG-INFO +2 -2
  24. {bmdwire-1.2.0 → bmdwire-1.5.0}/bmdwire.egg-info/SOURCES.txt +1 -0
  25. {bmdwire-1.2.0 → bmdwire-1.5.0}/hyperdeckwire/client.py +81 -3
  26. {bmdwire-1.2.0 → bmdwire-1.5.0}/pyproject.toml +1 -1
  27. bmdwire-1.2.0/atemwire/docs/MEDIA_LOCK.md +0 -85
  28. {bmdwire-1.2.0 → bmdwire-1.5.0}/LICENSE +0 -0
  29. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/LICENSE +0 -0
  30. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/LICENSE-gpl3.txt +0 -0
  31. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/NOTICE +0 -0
  32. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/__init__.py +0 -0
  33. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/_socketqueue.py +0 -0
  34. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/docs/PROFILE_SAVE_RESTORE.md +0 -0
  35. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/imaging.py +0 -0
  36. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/__init__.py +0 -0
  37. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/_io.py +0 -0
  38. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/downstream_keyer.py +0 -0
  39. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/flow.py +0 -0
  40. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/hyperdeck.py +0 -0
  41. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/media.py +0 -0
  42. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/macrotransfer/upstream_keyer.py +0 -0
  43. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/mediaconvertmodule.c +0 -0
  44. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/__init__.py +0 -0
  45. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/_dsl.py +0 -0
  46. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/audio_legacy.py +0 -0
  47. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/camera_control.py +0 -0
  48. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/color_generator.py +0 -0
  49. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/downstream_keyer.py +0 -0
  50. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/fade_to_black.py +0 -0
  51. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/fairlight.py +0 -0
  52. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/file_transfer.py +0 -0
  53. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/hyperdeck.py +0 -0
  54. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/lock.py +0 -0
  55. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/macros.py +0 -0
  56. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/manual.py +0 -0
  57. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/media.py +0 -0
  58. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/meters.py +0 -0
  59. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/multiviewer.py +0 -0
  60. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/recording.py +0 -0
  61. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/streaming.py +0 -0
  62. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/supersource.py +0 -0
  63. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/switching.py +0 -0
  64. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/system_info.py +0 -0
  65. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/tally.py +0 -0
  66. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/messages/transition.py +0 -0
  67. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/profile/__init__.py +0 -0
  68. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/profile/_common.py +0 -0
  69. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/profile/_xml.py +0 -0
  70. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/profile/options.py +0 -0
  71. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/py.typed +0 -0
  72. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/ready.py +0 -0
  73. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/state.py +0 -0
  74. {bmdwire-1.2.0 → bmdwire-1.5.0}/atemwire/testing.py +0 -0
  75. {bmdwire-1.2.0 → bmdwire-1.5.0}/bmdwire.egg-info/dependency_links.txt +0 -0
  76. {bmdwire-1.2.0 → bmdwire-1.5.0}/bmdwire.egg-info/requires.txt +0 -0
  77. {bmdwire-1.2.0 → bmdwire-1.5.0}/bmdwire.egg-info/top_level.txt +0 -0
  78. {bmdwire-1.2.0 → bmdwire-1.5.0}/hyperdeckwire/LICENSE +0 -0
  79. {bmdwire-1.2.0 → bmdwire-1.5.0}/hyperdeckwire/__init__.py +0 -0
  80. {bmdwire-1.2.0 → bmdwire-1.5.0}/hyperdeckwire/setup.py +0 -0
  81. {bmdwire-1.2.0 → bmdwire-1.5.0}/hyperdeckwire/upload.py +0 -0
  82. {bmdwire-1.2.0 → bmdwire-1.5.0}/setup.cfg +0 -0
  83. {bmdwire-1.2.0 → bmdwire-1.5.0}/setup.py +0 -0
  84. {bmdwire-1.2.0 → bmdwire-1.5.0}/ultimattewire/LICENSE +0 -0
  85. {bmdwire-1.2.0 → bmdwire-1.5.0}/ultimattewire/__init__.py +0 -0
  86. {bmdwire-1.2.0 → bmdwire-1.5.0}/ultimattewire/_preamble.py +0 -0
  87. {bmdwire-1.2.0 → bmdwire-1.5.0}/ultimattewire/_protocol.py +0 -0
  88. {bmdwire-1.2.0 → bmdwire-1.5.0}/ultimattewire/network.py +0 -0
  89. {bmdwire-1.2.0 → bmdwire-1.5.0}/ultimattewire/profile/__init__.py +0 -0
  90. {bmdwire-1.2.0 → bmdwire-1.5.0}/ultimattewire/profile/_ranges.py +0 -0
  91. {bmdwire-1.2.0 → bmdwire-1.5.0}/ultimattewire/profile/archive.py +0 -0
  92. {bmdwire-1.2.0 → bmdwire-1.5.0}/ultimattewire/profile/restore.py +0 -0
  93. {bmdwire-1.2.0 → bmdwire-1.5.0}/videohubwire/LICENSE +0 -0
  94. {bmdwire-1.2.0 → bmdwire-1.5.0}/videohubwire/__init__.py +0 -0
  95. {bmdwire-1.2.0 → bmdwire-1.5.0}/videohubwire/client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bmdwire
3
- Version: 1.2.0
3
+ Version: 1.5.0
4
4
  Summary: Python libraries for Blackmagic Design broadcast devices: ATEM switchers, HyperDeck recorders, Ultimatte keyers and Videohub routers
5
5
  Author: Lucas Romanenko, Martijn Braam and the OpenAtem contributors
6
6
  License-Expression: LGPL-3.0-only AND MIT
@@ -49,7 +49,7 @@ pip install bmdwire
49
49
  | **`ultimattewire`** | Ultimatte 12 keyers: archive and restore over the native TCP protocol, Smart Remote 4 compatible zips | MIT | [ultimattewire/README.md](ultimattewire/README.md) |
50
50
  | **`videohubwire`** | Videohub routers: state snapshot, crosspoint routing, labels over TCP 9990 | MIT | [videohubwire/README.md](videohubwire/README.md) |
51
51
 
52
- All four are pure standard library except atemwire, whose small C extension ships prebuilt for Linux, macOS and Windows on CPython 3.10 to 3.14, so installing needs no compiler. Pin the version in a requirements file (`bmdwire==1.2.0`) so a later release cannot change your install under you.
52
+ All four are pure standard library except atemwire, whose small C extension ships prebuilt for Linux, macOS and Windows on CPython 3.10 to 3.14, so installing needs no compiler. Pin the version in a requirements file (`bmdwire==1.5.0`) so a later release cannot change your install under you.
53
53
 
54
54
  ## atemwire stands on pyatem
55
55
 
@@ -15,7 +15,7 @@ pip install bmdwire
15
15
  | **`ultimattewire`** | Ultimatte 12 keyers: archive and restore over the native TCP protocol, Smart Remote 4 compatible zips | MIT | [ultimattewire/README.md](ultimattewire/README.md) |
16
16
  | **`videohubwire`** | Videohub routers: state snapshot, crosspoint routing, labels over TCP 9990 | MIT | [videohubwire/README.md](videohubwire/README.md) |
17
17
 
18
- All four are pure standard library except atemwire, whose small C extension ships prebuilt for Linux, macOS and Windows on CPython 3.10 to 3.14, so installing needs no compiler. Pin the version in a requirements file (`bmdwire==1.2.0`) so a later release cannot change your install under you.
18
+ All four are pure standard library except atemwire, whose small C extension ships prebuilt for Linux, macOS and Windows on CPython 3.10 to 3.14, so installing needs no compiler. Pin the version in a requirements file (`bmdwire==1.5.0`) so a later release cannot change your install under you.
19
19
 
20
20
  ## atemwire stands on pyatem
21
21
 
@@ -29,6 +29,15 @@ class TransferTask:
29
29
 
30
30
  self.name = None
31
31
  self.description = None
32
+ # FTFD (name, description, MD5) goes out with the first FTCD budget
33
+ # of each attempt, before the data, as ATEM Software Control sends
34
+ # it; cleared when the upload is (re)requested.
35
+ self.metadata_sent = False
36
+ # Bytes of ``data`` already cut into chunks in this attempt. Chunks
37
+ # used to be cut by slicing them off the front of ``data``, which
38
+ # copied the rest each time (some 25 GB for the ~6,000 chunks of a
39
+ # 1080p still) and left a retried upload without its start.
40
+ self.offset = 0
32
41
 
33
42
  def calculate_hash(self):
34
43
  hasher = hashlib.md5(self.data)
@@ -221,8 +221,11 @@ class ATEM:
221
221
 
222
222
  @property
223
223
  def connected(self) -> bool:
224
+ """True while the session is up AND its state is current — False
225
+ during a transport re-handshake, when the state is empty (see
226
+ ``ATEMConnection.is_ready``)."""
224
227
  conn = self._conn
225
- return bool(conn and conn.is_connected)
228
+ return bool(conn and conn.is_ready)
226
229
 
227
230
  @property
228
231
  def video_mode(self) -> str:
@@ -131,6 +131,12 @@ class ATEMConnection:
131
131
  # immediately reflects the post-disconnect state.
132
132
  self._handshake_done: bool = False
133
133
 
134
+ # ``_session_ready`` follows the protocol's 'connected' /
135
+ # 'disconnected' events: False from the moment a transport
136
+ # re-handshake empties mixerstate until the new state dump is
137
+ # complete. Read through ``is_ready``.
138
+ self._session_ready: bool = False
139
+
134
140
  self._lifecycle_lock = threading.Lock() # serializes connect/disconnect calls
135
141
  self._transfer_serial_lock = threading.Lock() # one native transfer at a time
136
142
 
@@ -168,6 +174,19 @@ class ATEMConnection:
168
174
  worker = self._worker
169
175
  return worker is not None and worker.is_alive()
170
176
 
177
+ @property
178
+ def is_ready(self) -> bool:
179
+ """True iff ``is_connected`` AND the switcher's state is current.
180
+
181
+ After ~5 s without packets the transport re-handshakes on its own:
182
+ the protocol raises 'disconnected' and empties ``mixerstate``, the
183
+ same worker carries on, and 'connected' fires again once the new
184
+ state dump is complete. ``is_connected`` stays True through that
185
+ gap (the worker is alive and recovering, so the pool must not evict
186
+ it); ``is_ready`` is False for its whole length.
187
+ """
188
+ return self._session_ready and self.is_connected
189
+
171
190
  @property
172
191
  def mixerstate(self) -> dict:
173
192
  """Live state dict from atemwire. Safe to read from any thread — CPython
@@ -213,9 +232,13 @@ class ATEMConnection:
213
232
  is dequeued so it cannot fire much later against a slot an
214
233
  operator may have since reused.
215
234
  """
216
- if not self.is_connected:
235
+ # is_ready, not is_connected: during a transport re-handshake the
236
+ # worker is alive but the session is being replaced, and a clear
237
+ # queued into it waited out its whole timeout (bmdwire#8).
238
+ if not self.is_ready:
217
239
  raise ConnectionDeadError(
218
- f"clear_still: connection to {self.ip_address} is not ready")
240
+ f"clear_still: connection to {self.ip_address} is not ready "
241
+ f"(no session, or the session is being re-established)")
219
242
  ip = self.ip_address
220
243
  protocol = self._protocol
221
244
  done = threading.Event()
@@ -280,9 +303,14 @@ class ATEMConnection:
280
303
  the transfer so the connection's transfer lane can't stay wedged
281
304
  for later callers).
282
305
  """
283
- if not self.is_connected:
306
+ # is_ready, not is_connected: a transfer started during a transport
307
+ # re-handshake queued its lock and transfer requests into the
308
+ # session being replaced and waited out its whole timeout, blaming
309
+ # "another client" for a lock nobody held (bmdwire#8).
310
+ if not self.is_ready:
284
311
  raise ConnectionDeadError(
285
- f"download: connection to {self.ip_address} is not ready")
312
+ f"download: connection to {self.ip_address} is not ready "
313
+ f"(no session, or the session is being re-established)")
286
314
  # Captured up front: a disconnect racing the timeout wait clears
287
315
  # self.ip_address, and the error would read "on None".
288
316
  ip = self.ip_address
@@ -405,6 +433,7 @@ class ATEMConnection:
405
433
  self._ready_event.clear()
406
434
  self._connect_error = None
407
435
  self._handshake_done = False
436
+ self._session_ready = False
408
437
  self._died_signalled = False
409
438
  self.ip_address = ip_address
410
439
 
@@ -478,12 +507,27 @@ class ATEMConnection:
478
507
  # bare (non-indexed) field so the event name is 'change:macro-play-status'.
479
508
  def _on_macro_play(contents):
480
509
  running = bool(getattr(contents, 'running', False))
481
- idx = int(getattr(contents, 'index', 0xFFFF) or 0xFFFF)
510
+ # Slot 0 is a real macro; only 0xFFFF (or no index) is idle.
511
+ idx = getattr(contents, 'index', None)
512
+ idx = 0xFFFF if idx is None else int(idx)
482
513
  if running and idx != 0xFFFF and idx >= 0:
483
514
  self.last_run_macro_index = idx
484
515
 
485
516
  self._protocol.on('change:macro-play-status', _on_macro_play)
486
517
 
518
+ # Session readiness (``is_ready``): 'disconnected' is raised when
519
+ # the transport gives up on the session and mixerstate is
520
+ # emptied; 'connected' when a state dump is complete, at the
521
+ # first connect and again after every re-handshake.
522
+ def _on_connected():
523
+ self._session_ready = True
524
+
525
+ def _on_disconnected():
526
+ self._session_ready = False
527
+
528
+ self._protocol.on('connected', _on_connected)
529
+ self._protocol.on('disconnected', _on_disconnected)
530
+
487
531
  try:
488
532
  self._protocol.connect()
489
533
  except Exception as e:
@@ -612,12 +612,13 @@ corresponding op was recorded).
612
612
  | 0x00DA | `MediaPlayerSourceStillIndex` | `u8 mp, _, u16 LE index` | XML-verified |
613
613
  | 0x00E1 | `MediaPlayerSourceStill` | `u8 mp, _, _, _` | XML-verified |
614
614
  | 0x014A | `FairlightAudioMixerInputSourceMixType` | `u16 LE source, u16 marker(0x4346), i64 LE sourceId, u16 LE state, u16 marker(0x000c)` (state: 1=Off, 2=On, 4=AFV; sourceId is the universal stereo sentinel `0xFFFFFFFFFFFF0100` = i64 -65280) | live-verified |
615
- | 0x014B | `FairlightAudioMixerInputSourceFaderGain` | `u16 LE source, u16 marker(0x4346), i64 LE sourceId, i32 LE gain × 65536` (XML scalar = value / 65536, floats with 6 decimals) | live-verified |
615
+ | 0x014B | `FairlightAudioMixerInputSourceFaderGain` | `u16 LE source, u16 marker(0x4346), i64 LE sourceId, i32 LE gain × 65536` (XML scalar = value / 65536, six significant digits: -21474836 is `-327.68`, silenced) | live-verified |
616
616
  | 0x016B | `FairlightAudioMixerMasterOutFaderGain` | `i32 LE gain × 65536` | live-verified |
617
617
 
618
618
  The strip ops (0x014A/0x014B) carry a sourceId field — Software Control's
619
619
  universal stereo sentinel is `0xFFFFFFFFFFFF0100` (= i64 -65280),
620
- representing "all channels of this source". The 0x4346 u16 at param
620
+ representing "all channels of this source". The encoder writes it for an
621
+ op with no `sourceId` attribute (or an empty one). The 0x4346 u16 at param
621
622
  offset 2 is constant in every captured recording but its meaning isn't
622
623
  known; the decoder ignores it and the encoder emits the constant for
623
624
  byte-identical round-trip.
@@ -0,0 +1,184 @@
1
+ # The media store lock, as the switcher actually behaves
2
+
3
+ Everything below was measured on the wire on 2026-09-16 against an ATEM
4
+ 1 M/E Constellation HD, with a session that logged every `LKOB` / `LKST` /
5
+ `FTDE` / `FTDC` as it was decoded and every `LOCK` / `PLCK` / `FTSU` as it
6
+ was sent. Other models may differ; nothing here is inferred from
7
+ documentation. The OpenSwitcher protocol notes describe the same commands
8
+ but several of their claims (the automatic release of a partial lock, for
9
+ one) did not hold on this hardware.
10
+
11
+ ## Requests and grants
12
+
13
+ * `LOCK store=0 state=1` is answered by `LKOB` in about 1 ms and then an
14
+ `LKST locked` broadcast to every session.
15
+ * `PLCK store=0 slot=n` is answered the same way. It is **not released
16
+ automatically** after one transfer: the explicit `LOCK state=0` is
17
+ required, and its `LKST unlocked` echo arrives about 40 ms later.
18
+ * `LKST` goes out at the switcher's frame tick (every 40 ms at 25p), so a
19
+ request sent with a release, or right after it, is granted (`LKOB`)
20
+ **before** the release is broadcast. See "What ATEM Software Control
21
+ does with an upload".
22
+ * A `PLCK` sent while another session holds the store is **queued by the
23
+ switcher** and granted the moment the holder releases. In that case the
24
+ requester receives `LKOB` directly and there may be **no `LKST unlocked`
25
+ broadcast at all** between the two holders. A client must therefore act
26
+ on `LKOB`, not only on the release broadcast.
27
+ * A `LOCK` (full store) or `PLCK` sent by a session that **already holds**
28
+ the store gets no answer.
29
+ * A `LOCK state=0` from a session that does not hold the store does
30
+ nothing visible (this was tested only while a foreign client held it).
31
+
32
+ ## Two sessions at once
33
+
34
+ * Two sessions requesting within ~150 ms can **both** receive `LKOB`. The
35
+ first `FTSU` to be serviced wins; the other transfer is aborted with
36
+ **`FTDE status 6`**. That session **still holds the lock** after the 6;
37
+ the winner's `PLCK` stays queued behind it. The only correct reaction to
38
+ a 6 is: release, keep the task, and re-request when the store frees.
39
+ Re-requesting without releasing deadlocks both sessions until their
40
+ timeouts.
41
+ * With that rule two sessions interleave one frame at a time, exactly like
42
+ two ATEM Software Control instances: four 1080p downloads across two
43
+ sessions complete in 8.2 s, the wire floor.
44
+ * `FTDC` for a transfer is broadcast to **every** session, with the owner's
45
+ transfer id. A session must match the id against its own transfer.
46
+
47
+ ## Session end
48
+
49
+ * A lock still held when the session says goodbye (hello opcode 0x04) is
50
+ released by the switcher within **10 ms**.
51
+ * A lock held by a session whose socket simply closed (no goodbye) is
52
+ released when the switcher reaps the session, about **5 s** later. (The
53
+ "five minutes" that older comments in this package repeated was never
54
+ measured.)
55
+
56
+ ## The state dump, and the bug that looked like a foreign lock
57
+
58
+ * The switcher answers a request sent **during** its initial state dump
59
+ normally; the `LKOB` is the first packet after the dump.
60
+ * `atemwire.transport.receive_packet` used to throw that packet away: it
61
+ pulled a packet from the queue, saw it owed the caller the
62
+ `ConnectionReady` sentinel (the packet after `InCm`), returned the
63
+ sentinel and dropped the packet. The switcher retransmitted the `LKOB`
64
+ every 80 ms, and every copy was deduplicated as already received. The
65
+ session then **held the media lock without knowing it** for the rest of
66
+ its life, and every other client, ATEM Software Control included, was
67
+ refused the store. From the outside that is "locked by another instance"
68
+ with nobody else there. The same shape existed for the
69
+ `TransferQueueFlushed` sentinel mid-upload. Fixed in 1.0.3: a displaced
70
+ packet is kept and handed out on the next call.
71
+ * `atemwire.ready.wait_ready` used to return on the **first** packet of the
72
+ dump (`protocol.connected` flips on any data packet; `video-mode` is in
73
+ that first packet). Since 1.0.3 it waits for the whole dump
74
+ (`protocol.initialized`, set when `InCm` has been processed). A session
75
+ on a LAN reports ready in ~0.5 s with ~107 state keys, where it used to
76
+ report ready in 4 ms with 25.
77
+ * Until 1.0.3 the dump itself was not ACKed until the switcher's first
78
+ no-data packet after it. Any dump slower than the switcher's ~80 ms
79
+ retransmit timer was resent in full, repeatedly. Reliable packets are now
80
+ ACKed as soon as the session is established.
81
+
82
+ ## Timing that is ours, not the switcher's
83
+
84
+ * A 1080p still downloads in ~1.75 s from `FTSU` to `FTDC`. The RLE decode
85
+ that follows is pure Python and takes ~250 ms on the connection's worker
86
+ thread. Since 1.0.3 the lock is released **before** that decode; before,
87
+ every frame held the store a quarter second longer than the transfer
88
+ needed. The decode still blocks the worker for those 250 ms; moving it
89
+ into the C extension is the remaining improvement.
90
+
91
+ ## What ATEM Software Control does with the media pool
92
+
93
+ A Wireshark capture on the ASC machine, 2026-10-14: ATEM Software Control
94
+ connecting to the same model (firmware 9.2.1, 1080p25) with 20 stills in
95
+ the pool, 8 different pictures, then saving a profile.
96
+
97
+ * **On connecting, ASC downloads every used still in full**, one after
98
+ another, starting 0.75 s after its hello (once the dump is in): 20
99
+ transfers of 8.32 MB, about 5,940 `FTDa` chunks of about 1,400 bytes
100
+ each, **1.78 to 1.82 s apiece from `FTSU` to `FTDC`**, with 17 to 39 ms
101
+ (median 24) from one `FTDC` to the next `FTSU`. It downloads repeated
102
+ pictures too: 20 transfers for 8 pictures.
103
+ * It takes the **whole store** for each still, `LOCK store=0 state=1`
104
+ (the last byte varied: `0x64`, `0xff`), and releases it with
105
+ `LOCK store=0 state=0` after each `FTDC`. It never sent a `PLCK`.
106
+ * Every `FTDa` is acknowledged with an `FTUA`; its second field is
107
+ nonzero and varies, where atemwire sends 0. Up to 153 chunks were in
108
+ flight before an acknowledgement.
109
+ * `FTSU`'s last four bytes were `00 ef 67 70` for a still and
110
+ `03 xx xx 00` for a macro (atemwire sends `00 d0 9b 8c` for a still).
111
+ * **Saving the profile, 61 s in, downloaded only the macros**: the six
112
+ used macro slots (three name-only, no ops), in 0.5 s, and **no still**.
113
+ ASC's quick save writes the stills it downloaded on connecting.
114
+
115
+ So a still comes off this switcher at its own pace, about 1.8 s for 1080p,
116
+ for ATEM Software Control as for atemwire (a client of this package,
117
+ bmdctl, measured 1.80 to 1.83 s on the same pool). The wire floor above
118
+ holds for one session too. Nothing in the capture is a faster transfer:
119
+ the whole-store lock, the acknowledgement's second field and `FTSU`'s
120
+ suffix cost the same time. A client that wants a quick save keeps the
121
+ stills it has downloaded, which is what `download_media_pool_images`'s
122
+ `cache_get` and `cache_put` are for (WebATEM 2.1.0 keeps them on disk by
123
+ hash), rather than looking for a faster download.
124
+
125
+ ## What ATEM Software Control does with an upload
126
+
127
+ A Wireshark capture on the ASC machine, 2026-10-15: ATEM Software Control
128
+ restoring a profile with 20 stills (8 pictures) to the same model
129
+ (firmware 9.2.1, 1080p25), into a pool already holding them but for two
130
+ slots changed beforehand.
131
+
132
+ * **It uploads every still the profile names**, changed or not: 20
133
+ uploads. It doesn't skip a slot that already holds its picture.
134
+ * **Twenty photographic 1080p stills in about 12 s.** Each sends 0.31 s of
135
+ data (5,955 to 5,958 chunks, 8.29 MB: RLE barely shortens a
136
+ photograph), takes 0.52 to 0.60 s from `FTSD` to `FTDC`, and the next
137
+ `FTSD` follows 35 to 54 ms after the `FTDC`. A client of this package,
138
+ pacing five chunks per packet from the switcher, took 23 s a still.
139
+ * **The handover.** 1 to 5 ms after each `FTDC`, `LOCK store=0 state=0`;
140
+ the next still's `PLCK` in the same packet (8 of 19 times) or the next
141
+ one, 3 to 21 ms after the `FTDC`. `LKOB` comes 0.3 to 0.7 ms after the
142
+ `PLCK`, every time, before any `LKST`. The release's `LKST` comes at the
143
+ switcher's next frame tick (they sit on a 40 ms grid), after that
144
+ `LKOB` and often after the next `FTSD`: `state=0` with the released slot
145
+ in byte 3, then `state=1`, or two `state=1` when the release and the
146
+ request shared a packet. ASC holds the lock from `LKOB` to its own
147
+ release whatever `LKST` says.
148
+ * Its downloads on connecting hand over the same way with the whole store
149
+ (the 2026-10-14 capture): `LOCK state=0` and `LOCK state=1` in one
150
+ packet, `LKOB` 0.8 ms later.
151
+ * **`FTSD`** follows the `LKOB` by 10 to 35 ms (ASC's own pace), not the
152
+ `LKST`. Its bytes are atemwire's: transfer id, store 0, slot,
153
+ `0x007e9000` (the 8,294,400-byte frame), mode 1.
154
+ * **`FTFD` goes with the first budget**: 0.1 to 1.6 ms after the first
155
+ `FTCD` when that came about 30 ms after the `FTSD`, or among the first
156
+ 150 to 424 chunks when it came within 6 ms. Never after the data. Its
157
+ name and description fields carry leftover memory after the NUL.
158
+ * **Budgets.** `FTCD` grants 768 chunks of 1,396 bytes (a few 601 to 760),
159
+ nine per still. ASC sends each whole, one `FTDa` per packet, back to
160
+ back: 768 chunks in about 35 ms, 24 µs apart (median). The next `FTCD`
161
+ comes about 5 ms after the last chunk. The switcher acknowledged about
162
+ every 16 packets, at most 88 behind.
163
+ * `PLCK`'s bytes 4 to 7 were `ab 01 a9 9b` in every request, where the
164
+ 2026-09-16 notes found `ff 01 00 00`: byte 5 is the constant.
165
+ * Nothing was lost or resent either way. ASC's acknowledgement-only
166
+ packets carry sequence number 0, so nothing tells the switcher that a
167
+ last packet went missing until it is sent again: a client resends what
168
+ isn't acknowledged on a timer, as the switcher does (every ~80 ms).
169
+
170
+ ### The lock request that seemed to be eaten
171
+
172
+ From 2026-07-07 atemwire held back a lock request until the `LKST` echo of
173
+ its own release, believing the switcher silently drops a request that
174
+ arrives while an unlock is in flight. ASC sends exactly that request, every
175
+ time, and the switcher grants it. What happened was the order above: the
176
+ release's broadcast comes after the grant, and atemwire's lock-state
177
+ handler took a release broadcast as the end of its own hold. It forgot the
178
+ new lock, skipped the release after that transfer, and asked again as the
179
+ holder, which the switcher ignores (see "Requests and grants"). Waiting
180
+ for the echo hid it, at a frame per transfer at best and up to 6.6 s
181
+ measured, during a 20-still restore by bmdctl, which mirrors this code.
182
+ Since `bd6ae77` atemwire holds the lock from `LKOB` to its own release, as
183
+ ASC does, and sends the release and the next request in one packet.
184
+
@@ -531,7 +531,7 @@ not the wait — implement your own settle if you need it.
531
531
  <Limiter enabled="..." threshold="..." attack="..." hold="..." release="..."/>
532
532
  </MasterOutDynamicsProcessor>
533
533
  <AudioInputs>
534
- <AudioInput configuration="Stereo|Mono" id="...">
534
+ <AudioInput configuration="Stereo|Mono|DualMono" id="...">
535
535
  <Analog level="ProLine|Mic"/> <!-- only on inputs that have analog config -->
536
536
  <AudioSource id="-65280" inputGain="..." pan="..." faderGain="..." mixOption="..." delayFrames="...">
537
537
  <Equalizer enabled="..." gain="...">
@@ -614,15 +614,26 @@ configuration (i.e. the TRS jack, source 1401). Levels: `ProLine`
614
614
  (line) or `Mic`. Maps to `mixerstate['fairlight-audio-input'].level`
615
615
  (1=mic, 2=line).
616
616
 
617
- `<AudioSource id="...">` represents one channel within the input. The
618
- ID is from the `mixerstate['fairlight-strip-properties']` structure:
619
- - `-65280` = stereo combined ("0xFF00" — the high byte is 0xFF
620
- meaning "not split", subchannel byte is 0).
621
- - `-256` = mono ("0xFFFFFFFF" — both bytes 0xFF).
622
- - `-768` etc. = split channels.
623
-
624
- For Constellation HD, every AudioSource in the reference uses
625
- `id="-65280"` (full-width stereo source) or `id="-256"` for mono.
617
+ `<AudioSource id="...">` is one strip of the input. Its id is the
618
+ strip's 8-byte wire address (the one `CFSP`, `CEBP` and `FASP` carry:
619
+ six `0xFF` bytes, the split byte, the channel byte) read as a signed
620
+ i64. Captured from ATEM Software Control with one input in Dual Mono
621
+ (bmdwire#9, fixture `tests/profile_fairlight_dual_mono_asc.xml`):
622
+
623
+ | id | address | strip | setter `channel` |
624
+ |---|---|---|---|
625
+ | `-65280` | `ff…ff 01 00` | a stereo input's strip | `-1` |
626
+ | `-256` | `ff…ff ff 00` | a dual-mono input's left half; the mono XLR (1301) | `0` |
627
+ | `-255` | `ff…ff ff 01` | a dual-mono input's right half | `1` |
628
+
629
+ A dual-mono input is `configuration="DualMono"` with two
630
+ `<AudioSource>` elements, left first, each with its own fader, pan, EQ
631
+ and dynamics. Save writes that when the input reports two split strips
632
+ (`FASP.is_split == 0xff`, strip ids `"<source>.0"` and `"<source>.1"`),
633
+ in subchannel order; apply sends each `<AudioSource>` to the channel its
634
+ id names (`_enums.audio_source_channel`). Apply doesn't change an
635
+ input's configuration (atemwire has no command for it), so a dual-mono
636
+ profile restores onto an input that is already in Dual Mono.
626
637
 
627
638
  | AudioSource attr | Source | Op |
628
639
  |------------------|--------|-----|
@@ -799,13 +810,16 @@ Each Op uses a small grammar of attributes:
799
810
  - `clip` / `gain` / `xSize` / `ySize` / etc.: floats matching the
800
811
  units used elsewhere in the file (e.g. `clip="0.300003"` is a 0..1
801
812
  scalar; `gain="-0.0800018"` is dB; `xPosition="0"` is unit-scaled).
802
- - Audio-op `gain` is in dB but **specially encoded**: `-327.68`
803
- represents the wire's `-32768/100`, which is the silenced sentinel
804
- value (≡ `-inf` in the declarative `<AudioSource>` representation).
805
- - Audio-op `sourceId` is a 64-bit unsigned integer that encodes the
806
- same concept as the `<AudioSource id>` attribute, but in a different
807
- representation: `18446744073709486336` = `0xFFFFFFFFFFFF0000` is the
808
- unsigned 64-bit reading of the same -65280 value (full-width stereo).
813
+ - Floats are written to six significant digits (`%g`), as ASC writes
814
+ them: Q16.16 -5243 is `-0.0800018`, 471859 is `7.2` (bmdwire#17).
815
+ - Audio-op `gain` is in dB. `-327.68` is the silenced fader, Q16.16
816
+ -21474836 (bytes `ec 51 b8 fe`), ≡ `-inf` in the declarative
817
+ `<AudioSource>` representation.
818
+ - Audio-op `sourceId` is the `<AudioSource id>` value read unsigned:
819
+ `18446744073709486336` = `0xFFFFFFFFFFFF0100` is -65280 (a stereo
820
+ strip), and a dual-mono input's halves are `18446744073709551360`
821
+ (-256, left) and `18446744073709551361` (-255, right), as ASC
822
+ recorded them (bmdwire#9).
809
823
 
810
824
  **Macro round-trip uses bytecode upload (partial-upload policy) as of
811
825
  2026-04-29.**