bmdwire 1.3.0__tar.gz → 1.5.1__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.3.0/bmdwire.egg-info → bmdwire-1.5.1}/PKG-INFO +2 -2
  2. {bmdwire-1.3.0 → bmdwire-1.5.1}/README.md +1 -1
  3. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/_transfer.py +9 -0
  4. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/connection.py +13 -4
  5. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/docs/MACRO_FORMAT.md +3 -2
  6. bmdwire-1.5.1/atemwire/docs/MEDIA_LOCK.md +235 -0
  7. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/docs/PROFILE_FORMAT.md +31 -17
  8. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/docs/SURFACE.md +140 -26
  9. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/helpers.py +42 -1
  10. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/_helpers.py +19 -39
  11. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/fairlight.py +9 -6
  12. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/upstream_keyer.py +20 -18
  13. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/pool.py +3 -2
  14. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/profile/_enums.py +36 -2
  15. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/profile/apply.py +12 -9
  16. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/profile/save.py +50 -11
  17. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/protocol.py +256 -63
  18. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/transport.py +190 -13
  19. {bmdwire-1.3.0 → bmdwire-1.5.1/bmdwire.egg-info}/PKG-INFO +2 -2
  20. {bmdwire-1.3.0 → bmdwire-1.5.1}/hyperdeckwire/client.py +81 -3
  21. {bmdwire-1.3.0 → bmdwire-1.5.1}/pyproject.toml +1 -1
  22. bmdwire-1.3.0/atemwire/docs/MEDIA_LOCK.md +0 -85
  23. {bmdwire-1.3.0 → bmdwire-1.5.1}/LICENSE +0 -0
  24. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/LICENSE +0 -0
  25. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/LICENSE-gpl3.txt +0 -0
  26. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/NOTICE +0 -0
  27. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/__init__.py +0 -0
  28. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/_socketqueue.py +0 -0
  29. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/atem.py +0 -0
  30. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/docs/PROFILE_SAVE_RESTORE.md +0 -0
  31. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/imaging.py +0 -0
  32. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/__init__.py +0 -0
  33. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/_io.py +0 -0
  34. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/downstream_keyer.py +0 -0
  35. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/flow.py +0 -0
  36. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/hyperdeck.py +0 -0
  37. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/media.py +0 -0
  38. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/switching.py +0 -0
  39. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/macrotransfer/upstream_keyer.py +0 -0
  40. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/mediaconvertmodule.c +0 -0
  41. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/__init__.py +0 -0
  42. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/_dsl.py +0 -0
  43. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/audio_legacy.py +0 -0
  44. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/camera_control.py +0 -0
  45. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/color_generator.py +0 -0
  46. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/downstream_keyer.py +0 -0
  47. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/fade_to_black.py +0 -0
  48. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/fairlight.py +0 -0
  49. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/file_transfer.py +0 -0
  50. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/hyperdeck.py +0 -0
  51. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/input_video.py +0 -0
  52. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/lock.py +0 -0
  53. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/macros.py +0 -0
  54. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/manual.py +0 -0
  55. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/media.py +0 -0
  56. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/meters.py +0 -0
  57. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/multiviewer.py +0 -0
  58. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/recording.py +0 -0
  59. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/streaming.py +0 -0
  60. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/supersource.py +0 -0
  61. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/switching.py +0 -0
  62. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/system_info.py +0 -0
  63. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/tally.py +0 -0
  64. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/messages/transition.py +0 -0
  65. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/probe.py +0 -0
  66. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/profile/__init__.py +0 -0
  67. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/profile/_common.py +0 -0
  68. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/profile/_xml.py +0 -0
  69. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/profile/options.py +0 -0
  70. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/py.typed +0 -0
  71. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/ready.py +0 -0
  72. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/state.py +0 -0
  73. {bmdwire-1.3.0 → bmdwire-1.5.1}/atemwire/testing.py +0 -0
  74. {bmdwire-1.3.0 → bmdwire-1.5.1}/bmdwire.egg-info/SOURCES.txt +0 -0
  75. {bmdwire-1.3.0 → bmdwire-1.5.1}/bmdwire.egg-info/dependency_links.txt +0 -0
  76. {bmdwire-1.3.0 → bmdwire-1.5.1}/bmdwire.egg-info/requires.txt +0 -0
  77. {bmdwire-1.3.0 → bmdwire-1.5.1}/bmdwire.egg-info/top_level.txt +0 -0
  78. {bmdwire-1.3.0 → bmdwire-1.5.1}/hyperdeckwire/LICENSE +0 -0
  79. {bmdwire-1.3.0 → bmdwire-1.5.1}/hyperdeckwire/__init__.py +0 -0
  80. {bmdwire-1.3.0 → bmdwire-1.5.1}/hyperdeckwire/setup.py +0 -0
  81. {bmdwire-1.3.0 → bmdwire-1.5.1}/hyperdeckwire/upload.py +0 -0
  82. {bmdwire-1.3.0 → bmdwire-1.5.1}/setup.cfg +0 -0
  83. {bmdwire-1.3.0 → bmdwire-1.5.1}/setup.py +0 -0
  84. {bmdwire-1.3.0 → bmdwire-1.5.1}/ultimattewire/LICENSE +0 -0
  85. {bmdwire-1.3.0 → bmdwire-1.5.1}/ultimattewire/__init__.py +0 -0
  86. {bmdwire-1.3.0 → bmdwire-1.5.1}/ultimattewire/_preamble.py +0 -0
  87. {bmdwire-1.3.0 → bmdwire-1.5.1}/ultimattewire/_protocol.py +0 -0
  88. {bmdwire-1.3.0 → bmdwire-1.5.1}/ultimattewire/network.py +0 -0
  89. {bmdwire-1.3.0 → bmdwire-1.5.1}/ultimattewire/profile/__init__.py +0 -0
  90. {bmdwire-1.3.0 → bmdwire-1.5.1}/ultimattewire/profile/_ranges.py +0 -0
  91. {bmdwire-1.3.0 → bmdwire-1.5.1}/ultimattewire/profile/archive.py +0 -0
  92. {bmdwire-1.3.0 → bmdwire-1.5.1}/ultimattewire/profile/restore.py +0 -0
  93. {bmdwire-1.3.0 → bmdwire-1.5.1}/videohubwire/LICENSE +0 -0
  94. {bmdwire-1.3.0 → bmdwire-1.5.1}/videohubwire/__init__.py +0 -0
  95. {bmdwire-1.3.0 → bmdwire-1.5.1}/videohubwire/client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bmdwire
3
- Version: 1.3.0
3
+ Version: 1.5.1
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.3.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.1`) 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.3.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.1`) 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)
@@ -232,9 +232,13 @@ class ATEMConnection:
232
232
  is dequeued so it cannot fire much later against a slot an
233
233
  operator may have since reused.
234
234
  """
235
- 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:
236
239
  raise ConnectionDeadError(
237
- 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)")
238
242
  ip = self.ip_address
239
243
  protocol = self._protocol
240
244
  done = threading.Event()
@@ -299,9 +303,14 @@ class ATEMConnection:
299
303
  the transfer so the connection's transfer lane can't stay wedged
300
304
  for later callers).
301
305
  """
302
- 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:
303
311
  raise ConnectionDeadError(
304
- 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)")
305
314
  # Captured up front: a disconnect racing the timeout wait clears
306
315
  # self.ip_address, and the error would read "on None".
307
316
  ip = self.ip_address
@@ -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,235 @@
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` sent while another session holds the store is **queued** too,
30
+ like a `PLCK` (ATEM Software Control's, 2026-10-15; this package's
31
+ comments used to say it was dropped). Handed the store at the holder's
32
+ release, the requester gets `LKOB` and **nothing is announced**, no
33
+ `LKST` either way.
34
+ * A `PLCK` is granted beside another session's `LOCK`, even half a second
35
+ into its turn; the `FTSU` that follows aborts the other's transfer in
36
+ progress with `FTDE 6` (see "Sharing the store with ATEM Software
37
+ Control").
38
+ * A `LOCK state=0` from a session that does not hold the store does
39
+ nothing visible (this was tested only while a foreign client held it).
40
+
41
+ ## Two sessions at once
42
+
43
+ * Two sessions requesting within ~150 ms can **both** receive `LKOB`. The
44
+ first `FTSU` to be serviced wins; the other transfer is aborted with
45
+ **`FTDE status 6`**. That session **still holds the lock** after the 6;
46
+ the winner's `PLCK` stays queued behind it. The only correct reaction to
47
+ a 6 is: release, keep the task, and re-request when the store frees.
48
+ Re-requesting without releasing deadlocks both sessions until their
49
+ timeouts.
50
+ * With that rule two sessions interleave one frame at a time, exactly like
51
+ two ATEM Software Control instances: four 1080p downloads across two
52
+ sessions complete in 8.2 s, the wire floor.
53
+ * `FTDC` for a transfer is broadcast to **every** session, with the owner's
54
+ transfer id. A session must match the id against its own transfer.
55
+
56
+ ## Session end
57
+
58
+ * A lock still held when the session says goodbye (hello opcode 0x04) is
59
+ released by the switcher within **10 ms**.
60
+ * A lock held by a session whose socket simply closed (no goodbye) is
61
+ released when the switcher reaps the session, about **5 s** later. (The
62
+ "five minutes" that older comments in this package repeated was never
63
+ measured.)
64
+
65
+ ## The state dump, and the bug that looked like a foreign lock
66
+
67
+ * The switcher answers a request sent **during** its initial state dump
68
+ normally; the `LKOB` is the first packet after the dump.
69
+ * `atemwire.transport.receive_packet` used to throw that packet away: it
70
+ pulled a packet from the queue, saw it owed the caller the
71
+ `ConnectionReady` sentinel (the packet after `InCm`), returned the
72
+ sentinel and dropped the packet. The switcher retransmitted the `LKOB`
73
+ every 80 ms, and every copy was deduplicated as already received. The
74
+ session then **held the media lock without knowing it** for the rest of
75
+ its life, and every other client, ATEM Software Control included, was
76
+ refused the store. From the outside that is "locked by another instance"
77
+ with nobody else there. The same shape existed for the
78
+ `TransferQueueFlushed` sentinel mid-upload. Fixed in 1.0.3: a displaced
79
+ packet is kept and handed out on the next call.
80
+ * `atemwire.ready.wait_ready` used to return on the **first** packet of the
81
+ dump (`protocol.connected` flips on any data packet; `video-mode` is in
82
+ that first packet). Since 1.0.3 it waits for the whole dump
83
+ (`protocol.initialized`, set when `InCm` has been processed). A session
84
+ on a LAN reports ready in ~0.5 s with ~107 state keys, where it used to
85
+ report ready in 4 ms with 25.
86
+ * Until 1.0.3 the dump itself was not ACKed until the switcher's first
87
+ no-data packet after it. Any dump slower than the switcher's ~80 ms
88
+ retransmit timer was resent in full, repeatedly. Reliable packets are now
89
+ ACKed as soon as the session is established.
90
+
91
+ ## Timing that is ours, not the switcher's
92
+
93
+ * A 1080p still downloads in ~1.75 s from `FTSU` to `FTDC`. The RLE decode
94
+ that follows is pure Python and takes ~250 ms on the connection's worker
95
+ thread. Since 1.0.3 the lock is released **before** that decode; before,
96
+ every frame held the store a quarter second longer than the transfer
97
+ needed. The decode still blocks the worker for those 250 ms; moving it
98
+ into the C extension is the remaining improvement.
99
+
100
+ ## What ATEM Software Control does with the media pool
101
+
102
+ A Wireshark capture on the ASC machine, 2026-10-14: ATEM Software Control
103
+ connecting to the same model (firmware 9.2.1, 1080p25) with 20 stills in
104
+ the pool, 8 different pictures, then saving a profile.
105
+
106
+ * **On connecting, ASC downloads every used still in full**, one after
107
+ another, starting 0.75 s after its hello (once the dump is in): 20
108
+ transfers of 8.32 MB, about 5,940 `FTDa` chunks of about 1,400 bytes
109
+ each, **1.78 to 1.82 s apiece from `FTSU` to `FTDC`**, with 17 to 39 ms
110
+ (median 24) from one `FTDC` to the next `FTSU`. It downloads repeated
111
+ pictures too: 20 transfers for 8 pictures.
112
+ * It takes the **whole store** for each still, `LOCK store=0 state=1`
113
+ (the last byte varied: `0x64`, `0xff`), and releases it with
114
+ `LOCK store=0 state=0` after each `FTDC`. It never sent a `PLCK`.
115
+ * Every `FTDa` is acknowledged with an `FTUA`; its second field is
116
+ nonzero and varies, where atemwire sends 0. Up to 153 chunks were in
117
+ flight before an acknowledgement.
118
+ * `FTSU`'s last four bytes were `00 ef 67 70` for a still and
119
+ `03 xx xx 00` for a macro (atemwire sends `00 d0 9b 8c` for a still).
120
+ * **Saving the profile, 61 s in, downloaded only the macros**: the six
121
+ used macro slots (three name-only, no ops), in 0.5 s, and **no still**.
122
+ ASC's quick save writes the stills it downloaded on connecting.
123
+
124
+ So a still comes off this switcher at its own pace, about 1.8 s for 1080p,
125
+ for ATEM Software Control as for atemwire (a client of this package,
126
+ bmdctl, measured 1.80 to 1.83 s on the same pool). The wire floor above
127
+ holds for one session too. Nothing in the capture is a faster transfer:
128
+ the whole-store lock, the acknowledgement's second field and `FTSU`'s
129
+ suffix cost the same time. A client that wants a quick save keeps the
130
+ stills it has downloaded, which is what `download_media_pool_images`'s
131
+ `cache_get` and `cache_put` are for (WebATEM 2.1.0 keeps them on disk by
132
+ hash), rather than looking for a faster download.
133
+
134
+ ## What ATEM Software Control does with an upload
135
+
136
+ A Wireshark capture on the ASC machine, 2026-10-15: ATEM Software Control
137
+ restoring a profile with 20 stills (8 pictures) to the same model
138
+ (firmware 9.2.1, 1080p25), into a pool already holding them but for two
139
+ slots changed beforehand.
140
+
141
+ * **It uploads every still the profile names**, changed or not: 20
142
+ uploads. It doesn't skip a slot that already holds its picture.
143
+ * **Twenty photographic 1080p stills in about 12 s.** Each sends 0.31 s of
144
+ data (5,955 to 5,958 chunks, 8.29 MB: RLE barely shortens a
145
+ photograph), takes 0.52 to 0.60 s from `FTSD` to `FTDC`, and the next
146
+ `FTSD` follows 35 to 54 ms after the `FTDC`. A client of this package,
147
+ pacing five chunks per packet from the switcher, took 23 s a still.
148
+ * **The handover.** 1 to 5 ms after each `FTDC`, `LOCK store=0 state=0`;
149
+ the next still's `PLCK` in the same packet (8 of 19 times) or the next
150
+ one, 3 to 21 ms after the `FTDC`. `LKOB` comes 0.3 to 0.7 ms after the
151
+ `PLCK`, every time, before any `LKST`. The release's `LKST` comes at the
152
+ switcher's next frame tick (they sit on a 40 ms grid), after that
153
+ `LKOB` and often after the next `FTSD`: `state=0` with the released slot
154
+ in byte 3, then `state=1`, or two `state=1` when the release and the
155
+ request shared a packet. ASC holds the lock from `LKOB` to its own
156
+ release whatever `LKST` says.
157
+ * Its downloads on connecting hand over the same way with the whole store
158
+ (the 2026-10-14 capture): `LOCK state=0` and `LOCK state=1` in one
159
+ packet, `LKOB` 0.8 ms later.
160
+ * **`FTSD`** follows the `LKOB` by 10 to 35 ms (ASC's own pace), not the
161
+ `LKST`. Its bytes are atemwire's: transfer id, store 0, slot,
162
+ `0x007e9000` (the 8,294,400-byte frame), mode 1.
163
+ * **`FTFD` goes with the first budget**: 0.1 to 1.6 ms after the first
164
+ `FTCD` when that came about 30 ms after the `FTSD`, or among the first
165
+ 150 to 424 chunks when it came within 6 ms. Never after the data. Its
166
+ name and description fields carry leftover memory after the NUL.
167
+ * **Budgets.** `FTCD` grants 768 chunks of 1,396 bytes (a few 601 to 760),
168
+ nine per still. ASC sends each whole, one `FTDa` per packet, back to
169
+ back: 768 chunks in about 35 ms, 24 µs apart (median). The next `FTCD`
170
+ comes about 5 ms after the last chunk. The switcher acknowledged about
171
+ every 16 packets, at most 88 behind.
172
+ * `PLCK`'s bytes 4 to 7 were `ab 01 a9 9b` in every request, where the
173
+ 2026-09-16 notes found `ff 01 00 00`: byte 5 is the constant.
174
+ * Nothing was lost or resent either way. ASC's acknowledgement-only
175
+ packets carry sequence number 0, so nothing tells the switcher that a
176
+ last packet went missing until it is sent again: a client resends what
177
+ isn't acknowledged on a timer, as the switcher does (every ~80 ms).
178
+
179
+ ### The lock request that seemed to be eaten
180
+
181
+ From 2026-07-07 atemwire held back a lock request until the `LKST` echo of
182
+ its own release, believing the switcher silently drops a request that
183
+ arrives while an unlock is in flight. ASC sends exactly that request, every
184
+ time, and the switcher grants it. What happened was the order above: the
185
+ release's broadcast comes after the grant, and atemwire's lock-state
186
+ handler took a release broadcast as the end of its own hold. It forgot the
187
+ new lock, skipped the release after that transfer, and asked again as the
188
+ holder, which the switcher ignores (see "Requests and grants"). Waiting
189
+ for the echo hid it, at a frame per transfer at best and up to 6.6 s
190
+ measured, during a 20-still restore by bmdctl, which mirrors this code.
191
+ Since `bd6ae77` atemwire holds the lock from `LKOB` to its own release, as
192
+ ASC does. That commit also sent the release and the next request in one
193
+ packet, as ASC does; that shut ASC out, and since `5827626` the next
194
+ request waits its turn (below).
195
+
196
+ ## Sharing the store with ATEM Software Control
197
+
198
+ Three tests on 2026-10-15, ATEM Software Control connecting to the same
199
+ switcher while a client of this package downloaded stills back to back,
200
+ two of them captured on the ASC machine.
201
+
202
+ * When it connects, ASC fetches every used still for its thumbnails, one
203
+ at a time, each under a whole-store `LOCK`. A `LOCK` sent while the
204
+ other client held the store was queued and handed to ASC at that
205
+ client's release, with `LKOB` and no `LKST`.
206
+ * With 1.5.0 the other client then asked again at once (its release and
207
+ next request in one packet), or within half a second: its `PLCK` was
208
+ granted beside ASC's `LOCK`, and its `FTSU` aborted ASC's download in
209
+ progress, 230 to 500 ms in, with `FTDE 6`.
210
+ * ASC reacts to the 6 as the notes above say a client should: it
211
+ releases and asks again in one packet, is queued, and is handed the
212
+ store at the next release, to be aborted again. **But it tries each
213
+ still once**: after the 6 it moves on to the next. After 20 stills and
214
+ 20 `FTDE 6` it released the store and asked for nothing more, its
215
+ thumbnails left empty. It fetched them again only when the media pool
216
+ changed (a still cleared from another client).
217
+ * Between its own stills ASC releases and asks again in one packet; the
218
+ switcher announces that as a release and a lock, often in one packet.
219
+ * What lets ASC have its turn (`5827626`): after a release with more
220
+ work queued, ask again only once the store is announced free, and a
221
+ frame later (`LOCK_COURTESY`, 60 ms), so a lock announced right behind
222
+ the release (another client's next transfer) cancels the request;
223
+ never ask while another client is seen holding the store; and with no
224
+ announcement at all (the store handed to a waiting request), wait as
225
+ long as anyone is seen at work, another session's `FTDC` or any `LKST`,
226
+ asking after `LOCK_QUIET` (10 s) of quiet. With it ASC loaded all 20
227
+ thumbnails while the other client downloaded; the download paused 38 s
228
+ for ASC's 20 and went on. Alone, 20 uploads take 14.2 s, against
229
+ 12.7 s with 1.5.0 and about 12 s for ASC.
230
+ * atemwire 1.4.0 waited for its own release's `LKST` echo before asking
231
+ again, so it waited out a client handed the store too, by accident.
232
+ bmdctl, which mirrors this code, saw that as unexplained pauses of 2 to
233
+ 6.6 s between stills in a restore: WebATEM, open on the same switcher,
234
+ was downloading each newly uploaded still for its thumbnail.
235
+
@@ -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.**