mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v4 0/1] ALSA: usb: Add support for Reloop Jockey 3 DJ controllers
@ 2026-09-25  2:19 Frank van de Pol
  2026-09-25  2:19 ` [PATCH v4 1/1] " Frank van de Pol
  0 siblings, 1 reply; 2+ messages in thread
From: Frank van de Pol @ 2026-09-25  2:19 UTC (permalink / raw)
  To: tiwai, perex; +Cc: corbet, khan, rdunlap, fvdpol, linux-sound, linux-kernel

Hi Takashi, Jaroslav, and the ALSA community,

Apologies for the long gap since v3. This is not simply v3 with the review
comments addressed -- I went back to the reverse engineering, and a fair
amount of what came out of it changed the driver structurally.

The patch has grown accordingly: v3 was 8 files and 1613 insertions, this is
17 and 7326.  Most of that is not new logic: of the roughly 5700 added
lines, about 2100 are comments, 1660 are KUnit tests and generated test
vectors for the codec, 249 are the new jockey3.rst documentation, and only
about 1300 are executable code.  The rest is blank lines and Kconfig
plumbing.

The relatively large number of comments comes from the deliberate decision
to include what I learned from the protocol analysis with the relevant
section of code.  The parts that most need explaining (the bit-plane wire
format, device initialization, why the URBs run for the device's whole
lifetime, the locking order etc.) cannot be verified by reading the code
alone.  ploytec_codec.c is over half comment for that reason: the DOC: block
deriving the format from the board's I2S and DMA geometry is effectively the
specification the optimized variants implement.

Previous version:
v3: https://lore.kernel.org/all/20260622011131.1748298-1-fvdpol@gmail.com/

Changes v3 -> v4:

 - Reworked the locking architecture around an explicit hierarchy: a
   process-context rate_mutex outermost, then IRQ-safe leaf spinlocks for
   playback, capture and MIDI that are never nested in one another.
   Documented in jockey3.c.

 - Fixed the capture stall after a sample-rate change, eliminating the USB
   device reset that recovery used to require.  The reset rate per rate
   change went from 19.3% to zero, with a 98k clean streak on arm64 and 61k
   on x86_64.

 - Rewrote the bit-plane codec: a portable reference implementation plus
   32-bit and 64-bit SWAR variants selected at compile time, called through a
   batch API at the driver's real batch sizes. Against the same machine's
   reference build, the optimized path measures 11.0x encode / 7.8x decode on
   x86_64 and 6.9x / 6.1x on arm64. KUnit tests validate all variants against
   an independently derived model of the wire format.

 - Added URB coalescing.  The driver used to submit one 512-byte packet per
   URB, fixing the completion rate at 9923/s at 44.1 kHz and 21600/s at 96
   kHz for as long as the device was plugged in, used or not.  N packets per
   URB (N a power of two, 1 to 8) are now chosen per PCM open from the
   requested period size.

 - Fixed a cold-boot initialization race. Straight after power-on the device
   accepts the entire init sequence, reports success on every transfer, takes
   playback samples, but its audio engine never starts. Bisected over 100
   cold boots to between 144 and 156 ms after enumeration; the driver now
   waits 250 ms before its first control transfer, close to the 297-308 ms
   the Windows driver waits.

 - Added a URB liveness watchdog. Every error path hangs off a URB
   completion, so a device that stops completing URBs produces no error, no
   xrun and no log line. A work item now reports either direction silent for
   20 ms and enters the recovery ladder on a stall onset.

 - Hardened handling of an unresponsive device: an EP0 error-class predicate
   aborts the handshake before usb_set_interface() is reached, since the USB
   core disables an interface's endpoints as its first action and does not
   re-enable them on the failure return.

 - Added Documentation/sound/cards/jockey3.rst

 - Addressed the v3 review comments.


Testing:

Validated with an automated hardware-in-the-loop framework written for the
purpose: probe and unbind endurance, PCM cycling, every rate in both
directions, period and buffer boundary matrices, duplex, xrun injection,
rate-change soaks, suspend/resume, USB disconnects via hub port power
switching, and cold-boot cycling through a network-controlled mains switch. 
Some of the figures above come from runs of tens of thousands of iterations.

Exercised on real hardware on x86_64, i386, arm64 (Raspberry Pi 4) and
armv6/armhf (Raspberry Pi 1B -- functional but tight at 88.2/96 kHz), with
x86_64 and arm64 also run under a KASAN + lockdep debug kernel.  The codec
KUnit suite additionally tested under UML and QEMU on i386, arm, arm64,
riscv and s390 (test big/little endianness).


Two things I would rather state than have found:

 - The mitigation stack around the rate-change stall is now practically
   dormant since the rate-change improvement.  I kept it as cheap insurance;
   happy to remove it.

 - The 250 ms settling delay in jockey3_initialize() runs on the
   USB hub thread.  I can move initialization off the probe path if you
   prefer, though that introduces complexity to avoid races.


I look forward to your review/feedback.

Best regards,
Frank

Frank van de Pol (1):
  ALSA: usb: Add support for Reloop Jockey 3 DJ controllers

 Documentation/sound/cards/index.rst           |    1 +
 Documentation/sound/cards/jockey3.rst         |  249 +
 MAINTAINERS                                   |    8 +
 sound/usb/Kconfig                             |    1 +
 sound/usb/Makefile                            |    1 +
 sound/usb/jockey3/.kunitconfig                |   13 +
 sound/usb/jockey3/Kconfig                     |   60 +
 sound/usb/jockey3/Makefile                    |    7 +
 sound/usb/jockey3/jockey3.c                   | 4060 +++++++++++++++++
 sound/usb/jockey3/ploytec_codec.c             |  656 +++
 sound/usb/jockey3/ploytec_codec.h             |   44 +
 sound/usb/jockey3/ploytec_codec_kunit.c       |  871 ++++
 .../usb/jockey3/ploytec_codec_test_vectors.h  |  793 ++++
 sound/usb/jockey3/ploytec_midi.c              |   83 +
 sound/usb/jockey3/ploytec_midi.h              |   36 +
 sound/usb/jockey3/ploytec_proto.c             |  365 ++
 sound/usb/jockey3/ploytec_proto.h             |   78 +
 17 files changed, 7326 insertions(+)
 create mode 100644 Documentation/sound/cards/jockey3.rst
 create mode 100644 sound/usb/jockey3/.kunitconfig
 create mode 100644 sound/usb/jockey3/Kconfig
 create mode 100644 sound/usb/jockey3/Makefile
 create mode 100644 sound/usb/jockey3/jockey3.c
 create mode 100644 sound/usb/jockey3/ploytec_codec.c
 create mode 100644 sound/usb/jockey3/ploytec_codec.h
 create mode 100644 sound/usb/jockey3/ploytec_codec_kunit.c
 create mode 100644 sound/usb/jockey3/ploytec_codec_test_vectors.h
 create mode 100644 sound/usb/jockey3/ploytec_midi.c
 create mode 100644 sound/usb/jockey3/ploytec_midi.h
 create mode 100644 sound/usb/jockey3/ploytec_proto.c
 create mode 100644 sound/usb/jockey3/ploytec_proto.h

-- 
2.47.3


^ permalink raw reply	[flat|nested] 2+ messages in thread

* [PATCH v4 1/1] ALSA: usb: Add support for Reloop Jockey 3 DJ controllers
  2026-09-25  2:19 [PATCH v4 0/1] ALSA: usb: Add support for Reloop Jockey 3 DJ controllers Frank van de Pol
@ 2026-09-25  2:19 ` Frank van de Pol
  0 siblings, 0 replies; 2+ messages in thread
From: Frank van de Pol @ 2026-09-25  2:19 UTC (permalink / raw)
  To: tiwai, perex; +Cc: corbet, khan, rdunlap, fvdpol, linux-sound, linux-kernel

Add a driver for the Reloop Jockey 3 Master Edition and Reloop Jockey 3
Remix USB DJ controllers.

These devices are not USB Audio Class compliant. They use a proprietary
Ploytec framing protocol, which needs handling beyond what the existing
quirk mechanisms can express, so the driver lives in its own directory
rather than extending usb-audio.

The driver provides:
 - 6-channel capture and 4-channel playback at 24 bits.
 - Sample rates of 44.1, 48, 88.2 and 96 kHz.
 - ALSA RawMIDI input and output for the integrated control surface.

The Ploytec encapsulation and bit-plane streaming format were
reverse-engineered from USB protocol analysis. They are isolated in a
hardware-independent codec layer with 32-bit and 64-bit optimized
variants alongside a portable reference implementation, all three
validated against KUnit tests.

Tested on real hardware on x86_64, i386, arm64 and armhf.

Assisted-by: Claude:claude-sonnet-5
Assisted-by: Claude:claude-opus-5
Assisted-by: Gemini:gemini-3.5-flash
Signed-off-by: Frank van de Pol <fvdpol@gmail.com>
---
 Documentation/sound/cards/index.rst           |    1 +
 Documentation/sound/cards/jockey3.rst         |  249 +
 MAINTAINERS                                   |    8 +
 sound/usb/Kconfig                             |    1 +
 sound/usb/Makefile                            |    1 +
 sound/usb/jockey3/.kunitconfig                |   13 +
 sound/usb/jockey3/Kconfig                     |   60 +
 sound/usb/jockey3/Makefile                    |    7 +
 sound/usb/jockey3/jockey3.c                   | 4060 +++++++++++++++++
 sound/usb/jockey3/ploytec_codec.c             |  656 +++
 sound/usb/jockey3/ploytec_codec.h             |   44 +
 sound/usb/jockey3/ploytec_codec_kunit.c       |  871 ++++
 .../usb/jockey3/ploytec_codec_test_vectors.h  |  793 ++++
 sound/usb/jockey3/ploytec_midi.c              |   83 +
 sound/usb/jockey3/ploytec_midi.h              |   36 +
 sound/usb/jockey3/ploytec_proto.c             |  365 ++
 sound/usb/jockey3/ploytec_proto.h             |   78 +
 17 files changed, 7326 insertions(+)
 create mode 100644 Documentation/sound/cards/jockey3.rst
 create mode 100644 sound/usb/jockey3/.kunitconfig
 create mode 100644 sound/usb/jockey3/Kconfig
 create mode 100644 sound/usb/jockey3/Makefile
 create mode 100644 sound/usb/jockey3/jockey3.c
 create mode 100644 sound/usb/jockey3/ploytec_codec.c
 create mode 100644 sound/usb/jockey3/ploytec_codec.h
 create mode 100644 sound/usb/jockey3/ploytec_codec_kunit.c
 create mode 100644 sound/usb/jockey3/ploytec_codec_test_vectors.h
 create mode 100644 sound/usb/jockey3/ploytec_midi.c
 create mode 100644 sound/usb/jockey3/ploytec_midi.h
 create mode 100644 sound/usb/jockey3/ploytec_proto.c
 create mode 100644 sound/usb/jockey3/ploytec_proto.h

diff --git a/Documentation/sound/cards/index.rst b/Documentation/sound/cards/index.rst
index e68bbb13c384b..7afea6fa88bff 100644
--- a/Documentation/sound/cards/index.rst
+++ b/Documentation/sound/cards/index.rst
@@ -18,4 +18,5 @@ Card-Specific Information
    hdspm
    serial-u16550
    img-spdif-in
+   jockey3
    pcmtest
diff --git a/Documentation/sound/cards/jockey3.rst b/Documentation/sound/cards/jockey3.rst
new file mode 100644
index 0000000000000..8fd65db9316cf
--- /dev/null
+++ b/Documentation/sound/cards/jockey3.rst
@@ -0,0 +1,249 @@
+.. SPDX-License-Identifier: GPL-2.0
+
+=========================================
+Reloop Jockey 3 DJ Controller Driver
+=========================================
+
+The ``snd-reloop-jockey3`` driver supports the Reloop Jockey 3 family of USB DJ
+controllers. These devices do not implement USB Audio Class; they use a
+proprietary protocol from Ploytec GmbH, which this driver implements from
+reverse engineering.
+
+Supported devices
+=================
+
+=================================  ===========  =========
+Device                             VID:PID      Status
+=================================  ===========  =========
+Reloop Jockey 3 Remix              200c:1037    Tested
+Reloop Jockey 3 Master Edition     200c:1019    Tested
+=================================  ===========  =========
+
+The Master Edition is confirmed to be protocol-compatible. The driver author
+does not own one, but user feedback on the Master Edition has been received
+confirming the device is fully functional with this driver.
+
+Audio
+=====
+
+The device provides 4 playback and 6 capture channels, all S24_3LE, at 44.1,
+48, 88.2 or 96 kHz. The sample rate is a device-wide property: if one stream is
+already running, a second stream opening is constrained to the rate in use.
+
+Channel layout
+--------------
+
+Playback:
+
+=======  ==========================
+Channel  Signal
+=======  ==========================
+1-2      Master Out L/R
+3-4      Headphone (cue) L/R
+=======  ==========================
+
+Capture:
+
+=======  ==========================
+Channel  Signal
+=======  ==========================
+1-2      Input 1 L/R
+3-4      Input 2 L/R
+5-6      Microphone
+=======  ==========================
+
+The line inputs are only routed to the ADC when the input selector on the unit
+is in the ``SW`` (software) position.
+
+The microphone is **mono**. Its balanced input stage feeds the same analog
+signal to both converters, so channels 5 and 6 carry identical content -- though
+not bit-identical, since each has its own ADC and picks up independent
+converter noise. Applications may treat the pair as a stereo stream with mono
+content, or simply use one of the two channels.
+
+The channel map advertised to userspace marks only the playback Master pair as
+``FL``/``FR``, so that audio servers can identify the primary output. The
+remaining pairs are reported as ``UNKNOWN``: they are discrete inputs and
+outputs rather than a speaker arrangement, and giving them surround positions
+would invite applications to route, for example, the headphone cue output to
+rear speakers.
+
+Microphone level
+----------------
+
+The microphone level appears to be quieter than when using the vendor
+software. The ADC is a PCM1803A with fixed gain, and the only gain control in
+the signal path is analog, so any additional gain must be applied digitally
+somewhere in the vendor stack; whether that happens in the driver or in the
+application has not been established. This driver applies no digital gain of
+its own, so apply it in userspace if required.
+
+MIDI
+====
+
+The control surface is exposed as a standard ALSA rawmidi device, with input
+and output ports.
+
+MIDI output is multiplexed into the audio playback stream: every outgoing
+packet reserves one byte for MIDI. As a result the playback URBs run
+continuously whether or not any PCM stream is open, and MIDI output throughput
+is bounded by the packet rate. The driver rate-limits MIDI output to roughly
+2500 bytes/sec, as the device otherwise overruns its internal buffers,
+truncates messages, and/or the control surface becomes unresponsive.
+
+The device does not accept MIDI Running Status, so the driver expands the
+outgoing stream to give every message an explicit status byte.
+
+Stream liveness and recovery
+============================
+
+Changing the sample rate requires stopping the URBs, reprogramming the device
+over the control endpoint, and restarting them. During testing and
+validation, cases have been observed where one of the USB streams failed to
+start or stalled afterwards -- the control transfers reported success, but
+the endpoint delivered no data, which surfaces to applications as ``EIO`` on
+the affected direction. The driver guards against this:
+
+* After every rate change, and whenever a stream is prepared, each direction
+  is checked for liveness by watching for URB completion activity.
+
+* A stalled direction is first recovered with a lightweight URB stop/restart.
+  If that does not bring it back, recovery escalates to a full USB device
+  reset, subject to a bounded retry budget: a chip-wide limit on how many
+  resets may be attempted within a rolling time window, so a persistently
+  misbehaving device is reported instead of being reset in a tight loop.
+
+* Playback also carries MIDI output, so a playback stall is always recovered
+  immediately through this path.
+
+* A capture stall is recovered immediately the same way only if a capture
+  stream is currently open. Otherwise it is left alone at that moment, to
+  avoid an audible reset glitch on working playback audio for the sake of a
+  direction nobody is using; the same liveness check and recovery catch it
+  the next time a capture stream is opened instead.
+
+A stall that triggers immediate recovery is logged with ``dev_warn()``; a
+stall on an idle, unused capture endpoint whose recovery is deferred is
+logged at ``dev_dbg()`` instead, since it is an expected, tolerated state
+rather than something acted on right away. Recovery outcomes -- a successful
+URB restart, an escalated reset, an exhausted retry budget, or a stream still
+dead after a reset -- are always logged with ``dev_warn()`` or ``dev_err()``,
+so real-world frequency and severity can be tracked via ``dmesg``.
+
+URB liveness watchdog
+=====================
+
+The checks above run at specific moments: after a rate change, and when a stream
+is prepared. A device that stops completing URBs at any other time is invisible
+to them, and to every other error path in the driver, because all of those hang
+off a URB completion. When completions stop, nothing runs and nothing is logged.
+A playback stream in that state does not even report an underrun, since the
+hardware pointer never advances far enough to overtake the application.
+
+A periodic work item therefore checks both directions for liveness for as long
+as the device is bound. It runs over the device's whole lifetime rather than
+only while a PCM stream is open, because the URBs do too: MIDI output is carried
+in every playback packet, so there is no idle state in which a total absence of
+completions is legitimate.
+
+The watchdog acts as well as reports. A new stall onset enters the same
+recovery ladder described above -- a lightweight URB stop/start, escalating to
+a full device reset if that does not take. This is the only place recovery can
+begin without a PCM ioctl re-entering the driver first, which matters because a
+long-running, uninterrupted stream never re-enters otherwise. Escalation to a
+reset is drawn from a chip-wide bounded budget, so a device that keeps stalling
+is not reset in a tight loop, and only one recovery ladder runs at a time.
+
+Logging is edge-triggered: one line when a direction stops completing URBs, one
+when it starts again, with nothing repeated in between. The message carries the
+measured age of the stall rather than a fixed threshold, since the threshold
+alone would only bound it to the width of one poll interval.
+
+An idle, unused capture endpoint stalling is the one case the driver
+deliberately tolerates without treating it as a fault: recovery for it is
+deferred to the next capture open, as described above, so it can persist
+indefinitely with nothing wrong. Every other persistent stall means recovery
+did not succeed, and the ``dev_err()`` logged for an exhausted retry budget
+or a stream still dead after a reset should already explain why.
+
+Module parameters
+=================
+
+The driver takes the standard ALSA ``index``, ``id`` and ``enable`` parameters.
+
+Kconfig
+=======
+
+``CONFIG_SND_USB_JOCKEY3``
+    Build the driver.
+
+``CONFIG_SND_USB_JOCKEY3_REFERENCE_CODEC``
+    Use the portable reference implementation of the sample codec instead of
+    the architecture-optimized one. The optimized codec is the default and is
+    what should normally be used; the reference implementation is much slower,
+    and exists as a readable definition of the wire format and as a fallback.
+    This option depends on ``CONFIG_EXPERT``.
+
+``CONFIG_SND_USB_JOCKEY3_CODEC_KUNIT_TEST``
+    Build the KUnit tests for the sample codec. Because the codec functions
+    are internal to the driver, the tests are linked into the driver module
+    rather than built as a separate one. See `Testing`_ below.
+
+Testing
+=======
+
+The sample codec has KUnit coverage. It is worth running after any change to
+``ploytec_codec.c``, and on any architecture the optimized codec has not been
+exercised on before::
+
+    tools/testing/kunit/kunit.py run --kunitconfig=sound/usb/jockey3 \
+        --arch=x86_64
+
+Running under UML needs three extra options::
+
+    tools/testing/kunit/kunit.py run --kunitconfig=sound/usb/jockey3 \
+        --kconfig_add CONFIG_VIRTIO=y \
+        --kconfig_add CONFIG_VIRTIO_UML=y \
+        --kconfig_add CONFIG_UML_PCI_OVER_VIRTIO=y
+
+UML disables IOMEM by default, which puts ``CONFIG_USB`` - and with it the
+whole of ``sound/usb`` - out of reach. ``UML_PCI_OVER_VIRTIO`` selects
+``UML_PCI``, which brings in the IOMEM emulation that makes it selectable
+again. These are passed on the command line rather than placed in
+``.kunitconfig`` because they only exist under ``arch/um``, and kunit.py
+treats a requested option it cannot satisfy as an error - so putting them in
+the shared fragment would break configuration on every other architecture.
+
+Other architectures run under QEMU, which is the point of the exercise: the
+codec assumes a little-endian sample format and reaches for 32- and 64-bit
+words through the unaligned accessors, so word size, alignment strictness and
+byte order all matter::
+
+    tools/testing/kunit/kunit.py run --kunitconfig=sound/usb/jockey3 \
+        --arch=arm --cross_compile=arm-linux-gnueabihf-
+
+s390 is worth a run as the only readily available big-endian target, and needs
+``--kconfig_add CONFIG_PCI=y``: on s390 ``HAS_IOMEM`` is ``def_bool PCI``, so
+without it there is no sound subsystem to build against.
+
+The suite has been run on um, i386, arm, arm64, riscv and s390.
+
+Only one of the three codec variants is compiled into any given build, so the
+portable reference deserves a run of its own::
+
+    tools/testing/kunit/kunit.py run --kunitconfig=sound/usb/jockey3 \
+        --kconfig_add CONFIG_EXPERT=y \
+        --kconfig_add CONFIG_SND_USB_JOCKEY3_REFERENCE_CODEC=y
+
+The tests check the compiled-in variant against a declarative description of
+the wire format rather than against a second copy of the same loops. They also
+exploit the fact that the codec is a bit permutation, and therefore linear over
+GF(2): one case enumerates the mapping's action on every input bit, another
+establishes linearity, and a linear map is fully determined by its action on
+the basis vectors.
+
+Further information
+===================
+
+Protocol notes, USB captures and ongoing reverse-engineering work are kept at
+https://github.com/fvdpol/alsa-jockey3.
diff --git a/MAINTAINERS b/MAINTAINERS
index 3a19da74d00c9..075c128bbd95c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -23058,6 +23058,14 @@ F:	Documentation/filesystems/relay.rst
 F:	include/linux/relay.h
 F:	kernel/relay.c
 
+RELOOP JOCKEY 3 DJ CONTROLLER DRIVER
+M:	Frank van de Pol <fvdpol@gmail.com>
+L:	linux-sound@vger.kernel.org
+S:	Maintained
+W:	https://github.com/fvdpol/alsa-jockey3
+F:	Documentation/sound/cards/jockey3.rst
+F:	sound/usb/jockey3/
+
 REGISTER MAP ABSTRACTION
 M:	Mark Brown <broonie@kernel.org>
 L:	linux-kernel@vger.kernel.org
diff --git a/sound/usb/Kconfig b/sound/usb/Kconfig
index b4588915efa11..d7edfbaf099be 100644
--- a/sound/usb/Kconfig
+++ b/sound/usb/Kconfig
@@ -205,6 +205,7 @@ config SND_USB_AUDIO_QMI
 	  will be called snd-usb-audio-qmi.
 
 source "sound/usb/line6/Kconfig"
+source "sound/usb/jockey3/Kconfig"
 
 endif	# SND_USB
 
diff --git a/sound/usb/Makefile b/sound/usb/Makefile
index e62794a87e73a..1f045c00dbc98 100644
--- a/sound/usb/Makefile
+++ b/sound/usb/Makefile
@@ -37,3 +37,4 @@ obj-$(CONFIG_SND_USB_US122L) += snd-usbmidi-lib.o
 
 obj-$(CONFIG_SND) += misc/ usx2y/ caiaq/ 6fire/ hiface/ bcd2000/ qcom/
 obj-$(CONFIG_SND_USB_LINE6)	+= line6/
+obj-$(CONFIG_SND_USB_JOCKEY3) += jockey3/
diff --git a/sound/usb/jockey3/.kunitconfig b/sound/usb/jockey3/.kunitconfig
new file mode 100644
index 0000000000000..a9681c891f096
--- /dev/null
+++ b/sound/usb/jockey3/.kunitconfig
@@ -0,0 +1,13 @@
+CONFIG_KUNIT=y
+
+CONFIG_SOUND=y
+CONFIG_SND=y
+CONFIG_SND_PCM=y
+CONFIG_SND_RAWMIDI=y
+
+CONFIG_USB_SUPPORT=y
+CONFIG_USB=y
+CONFIG_SND_USB=y
+
+CONFIG_SND_USB_JOCKEY3=y
+CONFIG_SND_USB_JOCKEY3_CODEC_KUNIT_TEST=y
diff --git a/sound/usb/jockey3/Kconfig b/sound/usb/jockey3/Kconfig
new file mode 100644
index 0000000000000..4b40a962417dd
--- /dev/null
+++ b/sound/usb/jockey3/Kconfig
@@ -0,0 +1,60 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config SND_USB_JOCKEY3
+	tristate "Reloop Jockey 3 support"
+	select SND_PCM
+	select SND_RAWMIDI
+	help
+	  Say Y here to include support for the Reloop Jockey 3 DJ controllers.
+	  These devices utilize a non-standard, proprietary Ploytec USB
+	  protocol.
+
+	  Supported devices:
+
+	    * Reloop Jockey 3 Master Edition
+	    * Reloop Jockey 3 Remix
+
+	  To compile this driver as a module, choose M here: the module
+	  will be called snd-reloop-jockey3.
+
+config SND_USB_JOCKEY3_REFERENCE_CODEC
+	bool "Use portable reference codec implementation"
+	depends on SND_USB_JOCKEY3
+	depends on EXPERT
+	help
+	  The Jockey 3 driver normally uses an architecture-optimized
+	  bit-packing codec (separate 32-bit and 64-bit variants) to convert
+	  between ALSA's S24_3LE sample format and the Ploytec wire format.
+	  This is the default on all architectures.
+
+	  Say Y to build the driver with the portable reference
+	  implementation instead. It is much slower, and is intended as a
+	  readable definition of the wire format and as a fallback on
+	  architectures where the optimized codec has not been exercised.
+
+	  If unsure, say N.
+
+config SND_USB_JOCKEY3_CODEC_KUNIT_TEST
+	bool "KUnit tests for the Ploytec codec" if !KUNIT_ALL_TESTS
+	depends on SND_USB_JOCKEY3
+	depends on KUNIT
+	depends on KUNIT=y || SND_USB_JOCKEY3=m
+	default KUNIT_ALL_TESTS
+	help
+	  Enable KUnit tests for the Ploytec bit-plane encoder and decoder.
+	  The tests validate whichever codec variant the build selected
+	  against a declarative description of the wire format, and prove
+	  the mapping is a complete bit permutation.
+
+	  Because the codec functions are internal to the driver, the tests
+	  are linked into the snd-reloop-jockey3 module itself rather than
+	  built separately.
+
+	  KUnit tests run during boot and output the results to the debug
+	  log in TAP format (https://testanything.org/). Only useful for
+	  kernel devs running KUnit test harness and are not for inclusion
+	  into a production build.
+
+	  For more information on KUnit and unit tests in general, refer
+	  to the KUnit documentation in Documentation/dev-tools/kunit/.
+
+	  If unsure, say N.
diff --git a/sound/usb/jockey3/Makefile b/sound/usb/jockey3/Makefile
new file mode 100644
index 0000000000000..3f339608f4d23
--- /dev/null
+++ b/sound/usb/jockey3/Makefile
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0
+obj-$(CONFIG_SND_USB_JOCKEY3) += snd-reloop-jockey3.o
+snd-reloop-jockey3-y := jockey3.o ploytec_proto.o ploytec_codec.o ploytec_midi.o
+
+# The codec functions are not exported, so the KUnit tests are
+# linked into the driver module rather than built as a separate test module.
+snd-reloop-jockey3-$(CONFIG_SND_USB_JOCKEY3_CODEC_KUNIT_TEST) += ploytec_codec_kunit.o
\ No newline at end of file
diff --git a/sound/usb/jockey3/jockey3.c b/sound/usb/jockey3/jockey3.c
new file mode 100644
index 0000000000000..c7596428bb282
--- /dev/null
+++ b/sound/usb/jockey3/jockey3.c
@@ -0,0 +1,4060 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *   ALSA driver for Reloop Jockey 3 devices
+ *
+ *   Copyright (c) 2026 by Frank van de Pol <fvdpol@gmail.com>
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/types.h>
+#include <linux/atomic.h>
+#include <linux/module.h>
+#include <linux/usb.h>
+#include <linux/slab.h>
+#include <linux/delay.h>
+#include <linux/jiffies.h>
+#include <linux/bitops.h>
+#include <linux/log2.h>
+#include <linux/timekeeping.h>
+#include <linux/completion.h>
+#include <linux/mutex.h>
+#include <linux/wait.h>
+#include <linux/workqueue.h>
+#include <linux/cleanup.h>
+#include <sound/core.h>
+#include <sound/initval.h>
+#include <sound/rawmidi.h>
+#include <sound/pcm.h>
+#include <sound/pcm_params.h>
+#include "ploytec_proto.h"
+#include "ploytec_codec.h"
+#include "ploytec_midi.h"
+
+#define RELOOP_VENDOR_ID         0x200c
+#define RELOOP_JOCKEY3_ME_PID    0x1019
+#define RELOOP_JOCKEY3_REMIX_PID 0x1037
+
+enum { JOCKEY3_ME, JOCKEY3_REMIX };
+#define CARD_NAME "Reloop Jockey 3"
+
+/* Human-readable edition name for a usb_device_id.driver_info value. */
+static const char *jockey3_model_name(int model)
+{
+	switch (model) {
+	case JOCKEY3_ME:
+		return "Master Edition";
+	case JOCKEY3_REMIX:
+		return "Remix";
+	default:
+		return "Unknown";
+	}
+}
+
+static int index[SNDRV_CARDS] = SNDRV_DEFAULT_IDX;
+static char *id[SNDRV_CARDS] = SNDRV_DEFAULT_STR;
+static bool enable[SNDRV_CARDS] = SNDRV_DEFAULT_ENABLE_PNP;
+
+module_param_array(index, int, NULL, 0444);
+MODULE_PARM_DESC(index, "Index value for " CARD_NAME " soundcard.");
+module_param_array(id, charp, NULL, 0444);
+MODULE_PARM_DESC(id, "ID string for " CARD_NAME " soundcard.");
+module_param_array(enable, bool, NULL, 0444);
+MODULE_PARM_DESC(enable, "Enable " CARD_NAME " soundcard.");
+
+/**
+ * DOC: Reverse-engineering references
+ *
+ * Comments below cite documents by paths beginning "re/". Those are the
+ * reverse-engineering notes this driver was derived from -- USB protocol
+ * analysis, vendor trace comparisons and the experiments behind several of the
+ * timing constants. They are not part of the kernel tree: they live in the
+ * driver's development repository, linked from its MAINTAINERS entry.
+ */
+
+/**
+ * DOC: Device model
+ *
+ * The Reloop Jockey 3 presents two USB interfaces and speaks a proprietary
+ * Ploytec protocol rather than USB Audio Class. Interface 0 is claimed by
+ * probe(); interface 1 is claimed explicitly, as it owns the capture endpoint.
+ * Three bulk endpoints carry everything:
+ *
+ * - EP 0x05 OUT: PCM playback, with the MIDI OUT byte stream multiplexed into
+ *   a reserved slot of every packet (see PLOYTEC_MIDI_OUT_OFFSET)
+ * - EP 0x86 IN:  PCM capture
+ * - EP 0x83 IN:  MIDI input
+ *
+ * Audio is not sample-interleaved but bit-plane interleaved; see
+ * ploytec_codec.c for the wire format and the encode/decode implementations.
+ *
+ * URBs run free for the lifetime of the device rather than being started and
+ * stopped around PCM use: the playback stream must keep flowing because it
+ * carries MIDI OUT, and the device expects a continuous packet stream. The PCM
+ * callbacks therefore only toggle whether a URB's payload is filled from (or
+ * copied to) an ALSA buffer.
+ *
+ * A sample-rate change requires tearing the URBs down, reprogramming the
+ * device over EP0, and starting them again. jockey3_pcm_hw_params() checks
+ * URB liveness on both directions afterward and recovers if either did not
+ * restart; see the comment there for what that covers and why.
+ *
+ * A stall can also be found and recovered mid-stream, with no rate change or
+ * PCM ioctl involved: jockey3_watchdog_work() polls URB liveness for the
+ * device's whole lifetime and calls the same recovery ladder directly on a
+ * new stall onset. See jockey3_watchdog_check() and jockey3_watchdog_arm().
+ */
+
+/**
+ * DOC: Resource lifetime
+ *
+ * The card is created with snd_card_new(), not snd_devm_card_new(), and
+ * released by jockey3_disconnect() with snd_card_free_when_closed(). A managed
+ * card would instead be freed from the devres unwind inside
+ * usb_unbind_interface(), where snd_card_free() blocks until userspace has
+ * closed every file descriptor on the card -- on an unplug that unwind runs on
+ * the USB hub work queue, so a single process sitting on a PCM fd would stall
+ * hotplug for the whole hub. Every other USB sound driver in the tree avoids
+ * that the same way.
+ *
+ * That splits teardown in two, and the split is the thing to keep straight:
+ *
+ * - jockey3_disconnect() does everything that must happen while the device is
+ *   still there: latch JOCKEY3_FLAG_DISCONNECTED (for either interface, since
+ *   the core takes them down one at a time), release anyone waiting on a
+ *   reset, stop the watchdog, kill the URBs, release interface 1, and hand the
+ *   card to snd_card_free_when_closed().
+ *
+ * - jockey3_card_free(), reached through card->private_free once the last file
+ *   descriptor is closed, frees the buffers and URBs, destroys rate_mutex and
+ *   gives the card slot back. It runs arbitrarily long after the disconnect
+ *   and must attempt no USB operation, though the objects themselves stay
+ *   referenced until it returns; see jockey3_free_resources().
+ *
+ * struct jockey3_chip lives in card->private_data, so it is freed with the
+ * card and outlives the USB binding -- which is what makes it safe for an ALSA
+ * callback that is still in flight during an unplug to keep dereferencing it.
+ * Such a callback finds JOCKEY3_FLAG_DISCONNECTED set and returns -ENODEV.
+ *
+ * probe's error path performs the disconnect half by hand and then calls
+ * snd_card_free(); the card is never registered on any path that reaches
+ * there, so the synchronous free cannot block.
+ */
+
+/**
+ * DOC: Locking
+ *
+ * The lock hierarchy is::
+ *
+ *     rate_mutex                     process context only, outermost
+ *       |- playback.lock             IRQ-safe leaf
+ *       |- capture.lock              IRQ-safe leaf
+ *       `- midi_lock                 IRQ-safe leaf
+ *
+ * The three leaf spinlocks are never nested inside one another; anything that
+ * needs more than one takes them in sequence, not nested. rate_mutex is never
+ * taken from atomic context.
+ *
+ * Against the ALSA core the order is::
+ *
+ *     snd_pcm_stream_lock  ->  playback.lock / capture.lock
+ *
+ * which is why the URB completion handlers drop their stream spinlock before
+ * calling snd_pcm_period_elapsed() or snd_pcm_stop_xrun(); both take the
+ * stream lock, and taking it while holding ours would invert the order.
+ *
+ * .trigger and .pointer are called by the core with the stream lock held and
+ * interrupts disabled, so neither may sleep.
+ *
+ * The watchdog work item adds one rule: rate_mutex must never be held across
+ * cancel_delayed_work_sync(&chip->watchdog_work). jockey3_stop_urbs() is called
+ * from inside rate_mutex at four sites, so it disarms with the non-sync
+ * cancel_delayed_work(), which is safe under any lock; a tick that is already
+ * running when the cancel lands re-reads 'stopping' and does nothing. The sync
+ * form appears only in jockey3_disconnect(), in probe's error path and in
+ * jockey3_free_resources(), none of which holds a mutex.
+ *
+ * Because that disarm does not wait, a tick can be parked on rate_mutex while
+ * jockey3_suspend() or jockey3_pre_reset() takes the device down, then wake to
+ * the timestamps they zeroed and read the stream as stalled. So
+ * jockey3_recover_urb_stream() re-tests DISCONNECTED, SUSPENDED and RESETTING
+ * under the mutex and leaves the restart to whichever path is bringing the
+ * device back.
+ *
+ * jockey3_watchdog_work() itself may call jockey3_recover_urb_stream(),
+ * which takes rate_mutex and calls jockey3_stop_urbs() -- i.e. the watchdog's
+ * own tick disarming itself via the non-sync cancel above, which is exactly
+ * the safe case: it never blocks and does not affect the tick already
+ * running. It may also queue a full USB reset; that reset runs on system_wq
+ * (usb_queue_reset_device(), drivers/usb/core/message.c), never
+ * system_long_wq where the watchdog runs, so the two cannot serialize behind
+ * one another, and rate_mutex is dropped before the wait for it, letting
+ * jockey3_pre_reset()/jockey3_post_reset() take the mutex themselves to
+ * complete.
+ *
+ * This also means jockey3_recover_urb_stream() can be entered from two
+ * independent contexts (the watchdog, and a PCM ioctl) for what turns out to
+ * be the same stall. rate_mutex alone does not prevent both from running
+ * their stop/start-or-reset sequence concurrently, since neither holds it for
+ * the whole ladder -- chip->recovery_in_progress (atomic_cmpxchg(), not a
+ * lock, since the second caller must decline rather than block) is what
+ * makes only one such ladder run at a time; see its doc comment.
+ */
+
+#define JOCKEY3_N_URBS 8
+
+/*
+ * Maximum Ploytec packets per URB ("N"), per direction. Several packets are
+ * coalesced into one USB bulk transfer to cut the completion-interrupt rate
+ * (rationale and measurements in re/streaming_overhead.md).
+ *
+ * The live N is not fixed: jockey3_pcm_hw_params() picks it per stream open,
+ * per direction, as the largest power of two that fits the requested period
+ * size, and stores it in struct jockey3_pcm_urb_stream's @n_shift/@n_pkts.
+ * These two values are only the *ceiling* on that choice, and the width the
+ * URB transfer buffers are allocated at (JOCKEY3_*_XFER_SIZE) -- which is why
+ * the ceiling has to be a compile-time constant. Two separate constants, not
+ * one, so the ceiling can be tuned per direction if the firmware ever turns
+ * out to accept coalescing on one direction but not the other.
+ *
+ * Changing the live N never reallocates or tears down the URB ring: only
+ * urb->transfer_buffer_length varies. PLOYTEC_PKT_SIZE (512) is exactly the
+ * USB high-speed bulk max-packet size, so an N x 512 B transfer is
+ * indistinguishable on the wire from N separate 512 B ones; the firmware
+ * cannot tell the two apart. The buffers stay allocated JOCKEY3_*_XFER_SIZE
+ * wide regardless of the N in use.
+ *
+ * A live N of 1 in both directions must be byte-for-byte identical to a
+ * driver without coalescing: every packet loop below is keyed off the live
+ * @n_pkts and degenerates to the original single-packet code path at N=1.
+ */
+#define JOCKEY3_PLAYBACK_N	8
+#define JOCKEY3_CAPTURE_N	8
+
+#define JOCKEY3_PLAYBACK_XFER_SIZE	(JOCKEY3_PLAYBACK_N * PLOYTEC_PKT_SIZE)
+#define JOCKEY3_CAPTURE_XFER_SIZE	(JOCKEY3_CAPTURE_N * PLOYTEC_PKT_SIZE)
+
+/*
+ * jockey3_check_urb_stream_alive()'s liveness window, per direction, scaled
+ * by that direction's own live N (struct jockey3_pcm_urb_stream's @n_shift):
+ * the worst-case URB span scales linearly with N (N packet intervals instead
+ * of one), so scaling the window the same way keeps the same margin against
+ * it at any N instead of eating into a fixed margin as N grows. N is always a
+ * power of two (jockey3_pcm_hw_params() only ever derives one), so this is a
+ * shift rather than a multiply.
+ */
+#define JOCKEY3_LIVENESS_WINDOW_NS(shift)	((u64)NSEC_PER_MSEC << (shift))
+
+/* Consecutive URB transport errors tolerated before a direction is given up on */
+#define JOCKEY3_MAX_URB_ERRORS 8
+
+/*
+ * URB liveness watchdog.
+ *
+ * A healthy stream completes one URB every @n_pkts packet intervals, and a
+ * packet is 226.8 us at 44100 Hz down to 83.3 us at 96000 Hz. So
+ * JOCKEY3_WATCHDOG_STALL_MS of silence is many consecutive missed URBs at any
+ * supported rate and N -- more than scheduling delay or bus contention
+ * produces on a device that is still streaming.
+ *
+ * The threshold is sized against ALSA core's own stall timeout
+ * (wait_for_avail() in sound/core/pcm_lib.c, roughly buffer_size * 1100 / rate
+ * ms), because jockey3_watchdog_check() recovers on this signal and needs
+ * headroom to succeed before the core returns -EIO to userspace itself.
+ * Contrast jockey3_check_urb_stream_alive()'s 1 ms window, which is sampled
+ * repeatedly inside a start grace and need only answer "anything just now?".
+ *
+ * jockey3_watchdog_arm() self-reschedules from the nearer of the two
+ * directions' deadlines, as dev_watchdog() does in net/sched/sch_generic.c.
+ * JOCKEY3_WATCHDOG_POLL_MS is the ceiling, used before either direction has
+ * started; JOCKEY3_WATCHDOG_MIN_POLL_MS the floor, so a confirmed stall is
+ * rechecked tightly rather than waiting out a stale window.
+ */
+#define JOCKEY3_WATCHDOG_POLL_MS	1000
+#define JOCKEY3_WATCHDOG_MIN_POLL_MS	10
+#define JOCKEY3_WATCHDOG_STALL_MS	20
+/*
+ * Bounds and evidence thresholds for the "has this direction reached steady
+ * streaming yet" checks that run over the window from jockey3_start_urbs()
+ * until the device is proven alive -- distinct from JOCKEY3_WATCHDOG_STALL_MS,
+ * which governs silence between two completions on an already-established
+ * stream. The grace duration itself is the runtime-tunable start_grace_ms
+ * (see its comment near the top of the file); these clamp a bad write and
+ * gate what counts as real streaming.
+ *
+ * JOCKEY3_GRACE_MS_MIN is deliberately well above JOCKEY3_WATCHDOG_STALL_MS: a
+ * grace at the stall threshold escalates on scheduling jitter by construction.
+ *
+ * JOCKEY3_HEALTHY_MIN_COMPLETIONS feeds jockey3_stream_streaming_healthy(), the
+ * stricter "did the warm restart actually take" test: a real stream lands
+ * hundreds of completions inside any plausible grace, so requiring a handful,
+ * spread over at least half the time that many URBs need and still arriving,
+ * rejects a trickle of implausibly fast FIFO-drain completions without
+ * rejecting a healthy resume. See that function.
+ */
+#define JOCKEY3_GRACE_MS_MIN		50
+#define JOCKEY3_GRACE_MS_MAX		5000
+#define JOCKEY3_HEALTHY_MIN_COMPLETIONS	4
+
+/*
+ * Bounded-retry budget for jockey3_recover_urb_stream(), chip-wide because the
+ * remedy it escalates to (a full usb_reset_device()) is shared by both
+ * directions -- a per-direction counter would double the effective rate for
+ * no reason. A window re-opens (and the counter resets) the first time the
+ * budget is consulted after JOCKEY3_RECOVERY_WINDOW_MS has passed since the
+ * current one started, so a chip that stops stalling is never left refusing
+ * to recover for the rest of its life.
+ *
+ * The watchdog calls jockey3_recover_urb_stream() directly (see its
+ * report_xrun parameter), and an early xrun report from that call can wake a
+ * concurrent jockey3_pcm_prepare() retry on the same direction before the
+ * watchdog's own call returns. chip->recovery_in_progress (see
+ * jockey3_recover_urb_stream()) makes that concurrent second call decline
+ * outright rather than draw from this budget, so one physical stall still
+ * draws against it at most once.
+ */
+#define JOCKEY3_RECOVERY_MAX_ATTEMPTS	3
+#define JOCKEY3_RECOVERY_WINDOW_MS	60000
+
+/*
+ * Grace period for a PCM direction to reach steady streaming after its URB
+ * ring is (re)started, in milliseconds. Sized well above the worst measured
+ * restart latency rather than tuned close to it: the error cost is
+ * asymmetric, since too long only delays an escalation that was coming
+ * anyway, while too short kills a stream that was merely late.
+ *
+ * Writable at runtime (0644) and read through jockey3_start_grace_ms(),
+ * which clamps to [JOCKEY3_GRACE_MS_MIN, JOCKEY3_GRACE_MS_MAX] so a bad write
+ * cannot drive the grace down to or below the stall threshold.
+ */
+static int start_grace_ms = 200;
+module_param(start_grace_ms, int, 0644);
+MODULE_PARM_DESC(start_grace_ms, "Grace (ms) for PCM to reach steady streaming after its URB ring is (re)started.");
+
+/*
+ * Current start-grace budget in ms, clamped so a runtime write cannot drive
+ * it to or below the stall threshold.
+ */
+static unsigned int jockey3_start_grace_ms(void)
+{
+	int ms = READ_ONCE(start_grace_ms);
+
+	return clamp(ms, JOCKEY3_GRACE_MS_MIN, JOCKEY3_GRACE_MS_MAX);
+}
+
+/**
+ * struct jockey3_pcm_urb_stream - per-direction PCM streaming state
+ * @substream: the open ALSA substream, or NULL; @lock
+ * @anchor: anchor holding the submitted URBs, for stop/kill
+ * @urbs: the URB ring
+ * @bufs: transfer buffer for each URB, allocated at the maximum N; @n_pkts,
+ *	not this width, governs how many bytes an URB actually transfers
+ * @urbs_in_flight: number of submitted URBs; diagnostic, must reach 0 after a stop
+ * @last_callback_time: ktime of the last completion, for stall detection. Zeroed
+ *	by jockey3_stop_urbs() so a stopped stream is not reported as alive.
+ * @first_callback_time: ktime of the first completion since the last
+ *	jockey3_start_urbs(); zeroed there and by jockey3_stop_urbs(). With
+ *	@completions_since_start it lets jockey3_stream_streaming_healthy()
+ *	judge cadence, not just recency: the device can retire a URB
+ *	microseconds after submit, which is a FIFO draining rather than audio.
+ *	Set lock-free from the completion handler.
+ * @completions_since_start: count of completions since the last
+ *	jockey3_start_urbs(); zeroed there and by jockey3_stop_urbs(). atomic_t,
+ *	incremented lock-free from the completion handler.
+ * @urbs_started_time: ktime at which jockey3_start_urbs() submitted this
+ *	direction's ring; zeroed by jockey3_stop_urbs(). The watchdog measures
+ *	from here until the first completion arrives. A separate timestamp is
+ *	needed because @last_callback_time is deliberately 0 between a start and
+ *	the first completion, which jockey3_check_urb_stream_alive() must keep
+ *	reading as "not alive" -- so the watchdog cannot reuse it without either
+ *	reporting a stall at every start or breaking the post-rate-change check.
+ *	It doubles as the watchdog's post-start grace baseline (the grace
+ *	duration is start_grace_ms).
+ * @lock: protects the fields marked "@lock" below; IRQ-safe leaf
+ * @dma_off: byte offset into runtime->dma_area, i.e. the hardware pointer; @lock
+ * @period_off: bytes accumulated towards the current period; @lock
+ * @running: stream is triggered and its payload should be filled; @lock
+ * @rate_committed: this direction has had a rate accepted by
+ *	jockey3_pcm_hw_params() and not yet released by jockey3_pcm_hw_free();
+ *	@lock. See jockey3_rate_committed_streams() for why the rate interlock
+ *	keys off this rather than off @running or off @substream.
+ * @callbacks_active: number of URB completions currently inside the "safe zone"
+ *	where they may still touch @substream or runtime->dma_area; @lock. The
+ *	last one out wakes @drain_wait. A count rather than a flag because there
+ *	are JOCKEY3_N_URBS URBs per direction and their completions can overlap
+ *	on SMP.
+ * @drain_wait: waited on by jockey3_pcm_sync_stop() until @callbacks_active is 0
+ * @stopping: set by jockey3_stop_urbs() before the anchor is killed, cleared by
+ *	jockey3_start_urbs(); @lock. Tested by the completion handler inside the
+ *	same critical section that anchors and resubmits, so a callback can never
+ *	re-anchor a URB after the kill has drained the anchor.
+ * @consec_errors: consecutive URB transport errors; @lock. Reset on any
+ *	successful completion and by jockey3_start_urbs().
+ * @stall_reported: the watchdog has logged the onset of the current stall;
+ *	@lock. Edge flag, so a wedge produces one onset line and one closing
+ *	line rather than one per tick. Cleared either by the stream completing a
+ *	URB again, or by jockey3_watchdog_clear_stall() when a restart ends the
+ *	outage first -- which is the common case, since every recovery path goes
+ *	through jockey3_stop_urbs()/jockey3_start_urbs().
+ * @stall_since: ktime the current stall was measured from; @lock. Only
+ *	meaningful while @stall_reported is set, and used to report how long the
+ *	outage lasted once the stream comes back.
+ * @n_shift: log2 of the Ploytec packets per URB ("N") the next resubmission
+ *	will carry, in [0, 3]; @lock. Set by jockey3_pcm_hw_params() from the
+ *	period size and reset to the default by jockey3_pcm_close(), so an idle
+ *	direction never re-arms at a stale N. Changing it does not tear the ring
+ *	down -- the wire cannot tell one N x 512 B transfer from N separate ones
+ *	-- so each URB picks up the value at its next resubmission.
+ * @n_pkts: 1 << @n_shift, mirrored for loop bounds; @lock
+ */
+struct jockey3_pcm_urb_stream {
+	struct snd_pcm_substream *substream;
+	struct usb_anchor anchor;
+	struct urb *urbs[JOCKEY3_N_URBS];
+	unsigned char *bufs[JOCKEY3_N_URBS];
+	atomic_t urbs_in_flight;
+	atomic64_t last_callback_time;
+	atomic64_t first_callback_time;
+	atomic_t completions_since_start;
+	atomic64_t urbs_started_time;
+	spinlock_t lock;	/* protects this stream's state; IRQ-safe leaf */
+	unsigned int dma_off;
+	unsigned int period_off;
+	bool running;
+	bool rate_committed;
+	unsigned int callbacks_active;
+	wait_queue_head_t drain_wait;
+	bool stopping;
+	unsigned int consec_errors;
+	bool stall_reported;
+	u64 stall_since;
+	u8 n_shift;
+	u8 n_pkts;
+};
+
+/**
+ * struct jockey3_chip - per-device driver state
+ * @card: the ALSA card; read-only after probe
+ * @dev: the USB device; read-only after probe. Referenced for the chip's
+ *	lifetime, as @intf0 and @intf1 are, because the card can outlive the
+ *	unbind and usb_disconnect() would otherwise free them underneath an ALSA
+ *	entry point. Released by jockey3_free_resources().
+ * @intf0: interface 0, which the driver is bound to; read-only after probe
+ * @intf1: interface 1, claimed explicitly because it owns EP 0x86
+ * @pcm: the PCM device; read-only after probe
+ * @rmidi: the rawmidi device; read-only after probe
+ * @xfer_buf: bounce buffer for EP0 control transfers, USB_XFER_BUF_SIZE bytes.
+ *	Serialized by @rate_mutex once the card is live. The one exception is
+ *	jockey3_initialize(), which runs from probe before the card is
+ *	registered or the watchdog armed, so it is the only user in existence.
+ * @rate_mutex: serializes sample-rate changes and the URB stop/start that goes
+ *	with them; process context only, outermost lock
+ * @flags: JOCKEY3_FLAG_* bits, atomic bitops. SUSPENDED is set before
+ *	jockey3_suspend() takes @rate_mutex and cleared by
+ *	jockey3_restore_device() under it, so a tick blocked on that mutex sees
+ *	it on the way out.
+ * @current_rate: sample rate the hardware is programmed to; @rate_mutex
+ * @dev_idx: card slot held in jockey3_devices_used
+ * @reset_done: completed by jockey3_post_reset(), and by jockey3_disconnect()
+ *	so a waiter is released when the USB core skips post_reset() entirely.
+ *	Re-armed by jockey3_queue_reset() for a reset this driver starts, and by
+ *	jockey3_pre_reset() for one started anywhere else.
+ * @watchdog_work: periodic URB liveness check; see jockey3_watchdog_work().
+ *	Armed by jockey3_start_urbs() and disarmed by jockey3_stop_urbs(), so it
+ *	runs exactly when the URBs are supposed to be flowing -- which, for this
+ *	device, is its whole lifetime rather than only while a PCM stream is open.
+ * @recovery_attempts: resets taken from the current window by
+ *	jockey3_recovery_budget_take(); not mutex-protected, since
+ *	jockey3_pcm_hw_params()'s post-rate-change liveness check runs outside
+ *	@rate_mutex by design. The two-atomic race with @recovery_window_start is
+ *	benign: the worst case is a handful of extra resets in one window, not a
+ *	stuck or negative budget.
+ * @recovery_window_start: ktime the current budget window opened; 0 before
+ *	the first attempt. See JOCKEY3_RECOVERY_WINDOW_MS.
+ * @recovery_in_progress: one jockey3_recover_urb_stream() ladder running at a
+ *	time, chip-wide rather than per-direction for the same reason
+ *	@recovery_attempts is chip-wide: jockey3_stop_urbs()/jockey3_start_urbs()
+ *	restart the shared ring for both directions together, so a Playback
+ *	recovery and a concurrent Capture recovery -- e.g. the watchdog and a
+ *	racing jockey3_pcm_prepare() retry -- would step on each other's
+ *	stop/start (or reset) sequence rather than being independent. A second
+ *	caller finding this already set declines immediately rather than racing
+ *	the first; the first caller's restart brings back whichever direction
+ *	the second one wanted too. Test-and-set via atomic_cmpxchg() rather than
+ *	a mutex held across the whole ladder, since jockey3_check_urb_stream_alive()
+ *	callers elsewhere poll rather than block on recovery finishing.
+ * @warm_start: true if the current URB ring was last started by the stall
+ *	watchdog's own lightweight restart, false for every other (re)start
+ *	(first open, rate change, USB reset, resume, probe). Both kinds share
+ *	one start_grace_ms; this only labels which one is in effect, for
+ *	dev_dbg(). Chip-wide, not per-direction: jockey3_start_urbs() restarts
+ *	the shared ring for both directions at once. Plain bool, written by
+ *	jockey3_start_urbs() and read unlocked by the watchdog via
+ *	WRITE_ONCE()/READ_ONCE().
+ * @midi_in_substream: open MIDI IN substream, or NULL; @midi_lock
+ * @midi_out_substream: open MIDI OUT substream, or NULL; @midi_lock
+ * @midi_in_urb: the single MIDI IN URB; not anchored, killed directly
+ * @midi_in_buf: transfer buffer for @midi_in_urb
+ * @midi_lock: protects the MIDI fields; IRQ-safe leaf
+ * @midi_out_acc: accumulator for the MIDI OUT rate limiter; @midi_lock
+ * @midi_rate_divisor: current_rate / PLOYTEC_PLAYBACK_FRAMES; @midi_lock.
+ *	Derived from @current_rate and published here because the rate limiter
+ *	runs in URB completion context and cannot take @rate_mutex.
+ * @midi_state: Running Status expander state; @midi_lock
+ * @midi_stopping: see jockey3_pcm_urb_stream.stopping; @midi_lock
+ * @midi_consec_errors: consecutive MIDI IN URB errors; @midi_lock
+ * @playback: playback streaming state
+ * @capture: capture streaming state
+ */
+struct jockey3_chip {
+	struct snd_card *card;
+	struct usb_device *dev;
+	struct usb_interface *intf0;
+	struct usb_interface *intf1;
+	struct snd_pcm *pcm;
+	struct snd_rawmidi *rmidi;
+	unsigned char *xfer_buf;
+	struct mutex rate_mutex;	/* serializes rate changes; outermost lock */
+	unsigned long flags;
+	unsigned int current_rate;
+	unsigned int dev_idx;
+	struct completion reset_done;
+	struct delayed_work watchdog_work;
+	atomic_t recovery_attempts;
+	atomic64_t recovery_window_start;
+	atomic_t recovery_in_progress;
+	bool warm_start;	/* set by jockey3_start_urbs(): warm vs cold, for dev_dbg() only */
+
+	/* MIDI Path */
+	struct snd_rawmidi_substream *midi_in_substream;
+	struct snd_rawmidi_substream *midi_out_substream;
+	struct urb *midi_in_urb;
+	unsigned char *midi_in_buf;
+	spinlock_t midi_lock;	/* protects the MIDI fields; IRQ-safe leaf */
+	unsigned int midi_out_acc;
+	unsigned int midi_rate_divisor;
+	struct ploytec_midi_running_status midi_state;
+	bool midi_stopping;
+	unsigned int midi_consec_errors;
+
+	/* PCM urb streams */
+	struct jockey3_pcm_urb_stream playback;
+	struct jockey3_pcm_urb_stream capture;
+};
+
+static struct usb_driver jockey3_driver;
+
+/*
+ * Card index allocation. A plain incrementing counter would both race between
+ * concurrent probes and never reuse a slot, so after SNDRV_CARDS successful
+ * probes no further device could attach even if all of them had been unplugged.
+ */
+static DEFINE_MUTEX(jockey3_devices_mutex);
+static DECLARE_BITMAP(jockey3_devices_used, SNDRV_CARDS);
+
+/* Chip flags */
+#define JOCKEY3_FLAG_DISCONNECTED	0
+#define JOCKEY3_FLAG_RESETTING		1
+#define JOCKEY3_FLAG_SUSPENDED		2
+
+static inline bool jockey3_is_disconnected(const struct jockey3_chip *chip)
+{
+	return test_bit(JOCKEY3_FLAG_DISCONNECTED, &chip->flags);
+}
+
+static inline bool jockey3_is_resetting(const struct jockey3_chip *chip)
+{
+	return test_bit(JOCKEY3_FLAG_RESETTING, &chip->flags);
+}
+
+static inline bool jockey3_is_suspended(const struct jockey3_chip *chip)
+{
+	return test_bit(JOCKEY3_FLAG_SUSPENDED, &chip->flags);
+}
+
+/*
+ * Publish a new sample rate.
+ *
+ * chip->current_rate is protected by rate_mutex, but the MIDI OUT rate limiter
+ * runs in URB completion (atomic) context and cannot take it. Rather than have
+ * that path read current_rate unlocked, the value it actually needs is derived
+ * here and published under midi_lock -- which also gets the division off the
+ * per-URB hot path.
+ */
+static void jockey3_set_current_rate(struct jockey3_chip *chip, unsigned int rate)
+{
+	lockdep_assert_held(&chip->rate_mutex);
+
+	chip->current_rate = rate;
+	scoped_guard(spinlock_irqsave, &chip->midi_lock)
+		chip->midi_rate_divisor = rate / PLOYTEC_PLAYBACK_FRAMES;
+}
+
+/*
+ * Rate changes are serialized by chip->rate_mutex alone: jockey3_pcm_hw_params()
+ * performs the whole stop/set-rate/start sequence while holding it, so any other
+ * sleepable callback that takes the mutex is automatically excluded for the
+ * duration. There is deliberately no separate "rate changing" flag to poll --
+ * the only caller that could not take the mutex was .trigger, which runs in
+ * atomic context and must not block at all.
+ */
+
+/*
+ * Bounded, synchronous wait for a device reset queued via
+ * usb_queue_reset_device() to complete. chip->reset_done is completed by
+ * jockey3_post_reset(), and also by jockey3_disconnect() so that a waiter is
+ * released when the USB core skips post_reset() entirely (a failed reset marks
+ * the interface for rebinding and unbinds it instead).
+ *
+ * Deliberately does NOT call usb_reset_device() itself. A failed or aborted
+ * reset marks the interface for rebinding and unbinds it, so the call would
+ * run jockey3_disconnect() -- and the card teardown behind it -- in the very
+ * thread that is still executing an ioctl on one of that card's file
+ * descriptors. Queuing the reset instead lets the disconnect run on the USB
+ * core's own workqueue, where the ALSA core's own refcounting keeps the
+ * substream alive until this ioctl returns and userspace closes it.
+ *
+ * Until the card was switched from snd_devm_card_new() to
+ * snd_card_free_when_closed() this was worse still: the teardown blocked in
+ * snd_card_free() waiting for that same file descriptor to be closed, which
+ * could never happen. That specific self-deadlock is gone, but reaching a
+ * disconnect from inside an ioctl on the disconnecting card remains something
+ * to keep out of this driver's calling threads.
+ */
+static int jockey3_wait_for_reset_completion(struct jockey3_chip *chip)
+{
+	/*
+	 * Empirical testing shows that the reset cycle typically takes around
+	 * 334 ms; a 1000 ms timeout gives sufficient headroom.
+	 */
+	if (!jockey3_is_resetting(chip))
+		return 0;
+
+	dev_dbg(&chip->intf0->dev, "Waiting for reset completion\n");
+
+	if (!wait_for_completion_timeout(&chip->reset_done, msecs_to_jiffies(1000))) {
+		dev_warn(&chip->intf0->dev, "Timeout waiting for reset completion\n");
+		return -EAGAIN;
+	}
+
+	if (jockey3_is_disconnected(chip))
+		return -ENODEV;
+
+	return 0;
+}
+
+/*
+ * Queue a full USB reset and arm chip->reset_done for a waiter. Does not
+ * wait; pair with jockey3_wait_for_reset_completion() for that.
+ */
+static void jockey3_queue_reset(struct jockey3_chip *chip)
+{
+	reinit_completion(&chip->reset_done);
+	set_bit(JOCKEY3_FLAG_RESETTING, &chip->flags);
+	usb_queue_reset_device(chip->intf0);
+}
+
+/*
+ * Chip-wide bounded-retry budget consulted by jockey3_recover_urb_stream()
+ * before it escalates to a full USB reset. Returns true if the caller may go
+ * ahead. A window older than JOCKEY3_RECOVERY_WINDOW_MS (or none opened yet)
+ * is replaced with a fresh one, so a chip that stops stalling is never left
+ * permanently refusing to recover; within a live window, up to
+ * JOCKEY3_RECOVERY_MAX_ATTEMPTS resets are allowed before further attempts
+ * are declined and reported instead.
+ */
+static bool jockey3_recovery_budget_take(struct jockey3_chip *chip)
+{
+	u64 now = ktime_get_mono_fast_ns();
+	u64 window_start = atomic64_read(&chip->recovery_window_start);
+
+	if (!window_start ||
+	    now - window_start > (u64)JOCKEY3_RECOVERY_WINDOW_MS * NSEC_PER_MSEC) {
+		atomic64_set(&chip->recovery_window_start, now);
+		atomic_set(&chip->recovery_attempts, 1);
+		return true;
+	}
+
+	return atomic_inc_return(&chip->recovery_attempts) <= JOCKEY3_RECOVERY_MAX_ATTEMPTS;
+}
+
+static inline struct jockey3_pcm_urb_stream *jockey3_get_pcm_urb_stream(struct jockey3_chip *chip,
+									const int direction)
+{
+	if (direction == SNDRV_PCM_STREAM_PLAYBACK)
+		return &chip->playback;
+	else
+		return &chip->capture;
+}
+
+static void jockey3_set_rate_committed(struct jockey3_chip *chip, const int direction,
+				       bool committed)
+{
+	struct jockey3_pcm_urb_stream *urb_stream = jockey3_get_pcm_urb_stream(chip, direction);
+
+	guard(spinlock_irqsave)(&urb_stream->lock);
+	urb_stream->rate_committed = committed;
+}
+
+/*
+ * How many directions currently hold the hardware rate, i.e. have had a rate
+ * accepted by jockey3_pcm_hw_params() and not yet released by
+ * jockey3_pcm_hw_free(). The device has a single rate for both directions, so
+ * this is what the open-time constraint and the hw_params interlock key off.
+ *
+ * Deliberately not a count of *running* streams: .trigger is what sets
+ * @running, so a stream that is open, has its rate fixed and is merely waiting
+ * to be started would not be counted, and the other direction could reprogram
+ * the hardware underneath it -- it would then play or record at the wrong
+ * speed.
+ *
+ * Equally deliberately not a count of *open* streams: a full-duplex
+ * application opens both directions before calling hw_params on either, and
+ * counting opens would pin the second one to whatever rate the device happens
+ * to be sitting at, refusing an otherwise legal rate change.
+ *
+ * One consequence is worth knowing: the constraint jockey3_pcm_open() applies
+ * is permanent for that substream's runtime, while a commitment is not, so a
+ * stream that opened while the other direction held a rate stays pinned to it
+ * even after that direction closes. That is inherent to a device with a single
+ * rate for both directions, and is the safe side of the trade.
+ */
+static int jockey3_rate_committed_streams(struct jockey3_chip *chip)
+{
+	int committed = 0;
+
+	scoped_guard(spinlock_irqsave, &chip->capture.lock) {
+		if (chip->capture.rate_committed)
+			committed++;
+	}
+
+	scoped_guard(spinlock_irqsave, &chip->playback.lock) {
+		if (chip->playback.rate_committed)
+			committed++;
+	}
+
+	return committed;
+}
+
+static bool jockey3_process_out_packet(struct jockey3_chip *chip, u8 *urb_buf)
+{
+	struct snd_pcm_substream *substream = chip->playback.substream;
+	struct jockey3_pcm_urb_stream *urb_stream = &chip->playback;
+	struct snd_pcm_runtime *runtime;
+	unsigned int pcm_buffer_size;
+	unsigned int alsa_frame_size;
+	unsigned int frames_in_batch;
+	unsigned int bytes_avail;
+	int f = 0;
+
+	if (unlikely(!substream || !substream->runtime))
+		return false;
+
+	runtime = substream->runtime;
+	if (unlikely(!runtime->dma_area))
+		return false;
+
+	pcm_buffer_size = snd_pcm_lib_buffer_bytes(substream);
+	alsa_frame_size = runtime->channels * 3;  // 4 * 3 = 12 bytes
+
+	while (f < PLOYTEC_PLAYBACK_FRAMES) {
+		/* calculate how many samples we can process in one batch */
+		frames_in_batch = PLOYTEC_PLAYBACK_FRAMES - f;
+		bytes_avail = pcm_buffer_size - urb_stream->dma_off;
+
+		/* Respect circular buffer wrap-around */
+		if (bytes_avail < frames_in_batch * alsa_frame_size)
+			frames_in_batch = bytes_avail / alsa_frame_size;
+
+		if (frames_in_batch == 0)
+			break;
+
+		ploytec_encode_batch(urb_buf + f * PLOYTEC_PLAYBACK_FRAME_SIZE,
+				     runtime->dma_area + urb_stream->dma_off,
+				     frames_in_batch);
+
+		urb_stream->dma_off += frames_in_batch * alsa_frame_size;
+		if (urb_stream->dma_off >= pcm_buffer_size)
+			urb_stream->dma_off -= pcm_buffer_size;
+
+		urb_stream->period_off += frames_in_batch * alsa_frame_size;
+
+		f += frames_in_batch;
+	}
+
+	if (urb_stream->period_off >= runtime->period_size * alsa_frame_size) {
+		urb_stream->period_off %= runtime->period_size * alsa_frame_size;
+		return true;
+	}
+
+	return false;
+}
+
+static bool jockey3_process_in_packet(struct jockey3_chip *chip, const u8 *urb_buf)
+{
+	struct snd_pcm_substream *substream = chip->capture.substream;
+	struct jockey3_pcm_urb_stream *urb_stream = &chip->capture;
+	struct snd_pcm_runtime *runtime;
+	unsigned int pcm_buffer_size;
+	unsigned int alsa_frame_size;
+	unsigned int frames_in_batch;
+	unsigned int bytes_left;
+	int f = 0;
+
+	if (unlikely(!substream || !substream->runtime))
+		return false;
+
+	runtime = substream->runtime;
+	if (unlikely(!runtime->dma_area))
+		return false;
+
+	pcm_buffer_size = snd_pcm_lib_buffer_bytes(substream);
+	alsa_frame_size = runtime->channels * 3; // 6 * 3 = 18 bytes
+
+	while (f < PLOYTEC_CAPTURE_FRAMES) {
+		frames_in_batch = PLOYTEC_CAPTURE_FRAMES - f;
+		bytes_left = pcm_buffer_size - urb_stream->dma_off;
+
+		/* Respect circular buffer wrap-around */
+		if (bytes_left < frames_in_batch * alsa_frame_size)
+			frames_in_batch = bytes_left / alsa_frame_size;
+
+		if (frames_in_batch == 0)
+			break;
+
+		ploytec_decode_batch(runtime->dma_area + urb_stream->dma_off,
+				     urb_buf + f * PLOYTEC_CAPTURE_FRAME_SIZE,
+				     frames_in_batch);
+
+		/* Advance pointers */
+		urb_stream->dma_off += frames_in_batch * alsa_frame_size;
+		if (urb_stream->dma_off >= pcm_buffer_size)
+			urb_stream->dma_off -= pcm_buffer_size;
+
+		urb_stream->period_off += frames_in_batch * alsa_frame_size;
+
+		f += frames_in_batch;
+	}
+
+	if (urb_stream->period_off >= runtime->period_size * alsa_frame_size) {
+		urb_stream->period_off %= runtime->period_size * alsa_frame_size;
+		return true;
+	}
+
+	return false;
+}
+
+enum jockey3_urb_state {
+	JOCKEY3_URB_OK,		/* completed normally */
+	JOCKEY3_URB_STOPPED,	/* teardown in progress; return without resubmitting */
+	JOCKEY3_URB_ERROR,	/* transport error, potentially transient */
+};
+
+static inline enum jockey3_urb_state jockey3_urb_check(const struct urb *urb)
+{
+	if (likely(urb->status == 0))
+		return JOCKEY3_URB_OK;
+
+	if (urb->status == -ENOENT || urb->status == -ECONNRESET || urb->status == -ESHUTDOWN)
+		return JOCKEY3_URB_STOPPED;
+
+	return JOCKEY3_URB_ERROR;
+}
+
+/**
+ * jockey3_warn_unexpected_stop() - report a URB that someone else cancelled
+ * @chip: driver state
+ * @stopping: whether this direction's teardown fence was set
+ * @status: the urb->status that retired the URB
+ * @type: direction name, for the log message
+ *
+ * -ENOENT, -ECONNRESET and -ESHUTDOWN normally mean the driver killed the URB
+ * itself, and jockey3_urb_check() maps them to JOCKEY3_URB_STOPPED on that
+ * assumption. Nothing verifies it: the USB core flushes an endpoint's URBs from
+ * usb_disable_endpoint(), so an alt-setting change or an endpoint teardown
+ * started anywhere else retires the whole ring by the same route, and every URB
+ * of the direction would return here without a word.
+ *
+ * A physical unplug arrives here too and is not worth reporting.
+ * usb_disconnect() moves the device to USB_STATE_NOTATTACHED before unbinding
+ * the interfaces, so URBs that complete ahead of jockey3_disconnect() are
+ * recognized by that rather than mistaken for an unexplained teardown.
+ *
+ * This is defensive. No failure observed so far has been traced to this path.
+ */
+static void jockey3_warn_unexpected_stop(struct jockey3_chip *chip, bool stopping,
+					 int status, const char *type)
+{
+	if (stopping || jockey3_is_disconnected(chip) || jockey3_is_resetting(chip))
+		return;
+
+	if (chip->dev->state == USB_STATE_NOTATTACHED)
+		return;
+
+	/* Ratelimited: a whole ring of URBs retires together */
+	dev_warn_ratelimited(&chip->intf0->dev,
+			     "%s URB cancelled without a driver-initiated stop: %d\n",
+			     type, status);
+}
+
+/**
+ * jockey3_report_xrun() - tell userspace this direction lost its data
+ * @urb_stream: the affected direction
+ *
+ * Reports an xrun on the open substream, if there is one. Nothing else in the
+ * driver can do this safely by hand: snd_pcm_stop_xrun() takes the stream lock,
+ * and the documented order is snd_pcm_stream_lock -> urb_stream->lock, so the
+ * call has to be made with our spinlock dropped. Between dropping it and taking
+ * it again the substream could be freed underneath us, which is what the
+ * callbacks_active "safe zone" prevents -- jockey3_pcm_sync_stop() waits for
+ * that count to reach zero before the ALSA core releases the buffer.
+ *
+ * Callers must hold neither @urb_stream->lock nor any driver mutex.
+ */
+static void jockey3_report_xrun(struct jockey3_pcm_urb_stream *urb_stream)
+{
+	struct snd_pcm_substream *substream = NULL;
+
+	scoped_guard(spinlock_irqsave, &urb_stream->lock) {
+		if (urb_stream->running && urb_stream->substream) {
+			/* Join the safe zone so the substream cannot be freed below */
+			urb_stream->callbacks_active++;
+			substream = urb_stream->substream;
+		}
+	}
+
+	if (!substream)
+		return;
+
+	snd_pcm_stop_xrun(substream);
+
+	scoped_guard(spinlock_irqsave, &urb_stream->lock) {
+		if (!--urb_stream->callbacks_active)
+			wake_up(&urb_stream->drain_wait);
+	}
+}
+
+/**
+ * jockey3_err_device_gone() - did the device simply leave?
+ * @err: a transfer's return value
+ *
+ * Keyed on the error code, not on jockey3_is_disconnected(): a failed reset
+ * also leaves the device %USB_STATE_NOTATTACHED and sets that flag on its way
+ * to unbinding, and that one is a fault worth logging.
+ *
+ * Return: true if @err means the device is gone rather than misbehaving.
+ */
+static bool jockey3_err_device_gone(int err)
+{
+	return err == -ENODEV || err == -ESHUTDOWN;
+}
+
+/*
+ * An unplug fails every in-flight URB's resubmit at once, so demote that case
+ * as the submit and rate paths do. Log level only: @consec_errors comes from
+ * jockey3_urb_error_give_up() and the completion status, not from here.
+ */
+static void jockey3_resubmit_failed(struct jockey3_chip *chip, int err, const char *what)
+{
+	if (jockey3_err_device_gone(err)) {
+		dev_dbg(&chip->intf0->dev,
+			"Not resubmitting the %s URB: device is gone (%d)\n", what, err);
+		return;
+	}
+
+	dev_err(&chip->intf0->dev, "Failed to resubmit %s URB: %d\n", what, err);
+}
+
+/**
+ * jockey3_urb_error_give_up() - account for a URB transport error
+ * @chip: driver state
+ * @urb_stream: the affected direction
+ * @status: the urb->status that was seen
+ * @type: direction name, for log messages
+ *
+ * Accounts for a transport error (-EPROTO, -EPIPE, -EOVERFLOW, -ETIME, ...).
+ *
+ * These are frequently transient -- marginal cabling and some host controllers
+ * produce them routinely -- so a single one must not disable the card. Keep
+ * resubmitting while the consecutive count stays below JOCKEY3_MAX_URB_ERRORS;
+ * beyond that, stop feeding this direction and leave recovery to the next
+ * .prepare, which already carries the stall detection and reset path.
+ *
+ * Return: true if the caller should give up and not resubmit.
+ */
+static bool jockey3_urb_error_give_up(struct jockey3_chip *chip,
+				      struct jockey3_pcm_urb_stream *urb_stream,
+				      int status, const char *type)
+{
+	unsigned int errors;
+	bool crossed_limit;
+
+	scoped_guard(spinlock_irqsave, &urb_stream->lock) {
+		errors = ++urb_stream->consec_errors;
+		/*
+		 * The increment and this test share one critical section, so
+		 * exactly one caller observes the transition even though up to
+		 * JOCKEY3_N_URBS completions arrive together.
+		 */
+		crossed_limit = errors == JOCKEY3_MAX_URB_ERRORS;
+	}
+
+	/*
+	 * Already given up. When the device goes away every one of the
+	 * JOCKEY3_N_URBS in-flight URBs completes with an error, so without this
+	 * the limit would be re-reported once per URB (and the stream stopped
+	 * repeatedly). Report the transition only.
+	 */
+	if (errors > JOCKEY3_MAX_URB_ERRORS)
+		return true;
+
+	dev_err_ratelimited(&chip->intf0->dev, "%s URB error: %d (%u consecutive)\n",
+			    type, status, errors);
+
+	if (!crossed_limit)
+		return false;
+
+	dev_err(&chip->intf0->dev,
+		"%s stopped after %u consecutive URB errors; deferring recovery\n",
+		type, errors);
+
+	jockey3_report_xrun(urb_stream);
+
+	return true;
+}
+
+/*
+ * Record one URB completion for the liveness and cadence checks. The device can
+ * retire a URB microseconds after submit -- a hardware-side FIFO draining rather
+ * than audio actually clocked onto or off the wire -- so
+ * jockey3_stream_streaming_healthy() needs the first completion's time and the
+ * running count, not just the last time, to tell real flow from that trickle.
+ * Lock-free; called at the top of each completion handler. If two completions
+ * race, exactly one sees the count go 1->1 and stamps @first_callback_time; a
+ * transient @last_callback_time < @first_callback_time is harmless, the health
+ * check rejects it and is re-polled.
+ */
+static void jockey3_note_completion(struct jockey3_pcm_urb_stream *urb_stream)
+{
+	u64 now = ktime_get_mono_fast_ns();
+
+	atomic64_set(&urb_stream->last_callback_time, now);
+	if (atomic_inc_return(&urb_stream->completions_since_start) == 1)
+		atomic64_set(&urb_stream->first_callback_time, now);
+}
+
+static void jockey3_capture_callback(struct urb *urb)
+{
+	struct jockey3_chip *chip = urb->context;
+	struct jockey3_pcm_urb_stream *urb_stream = &chip->capture;
+	struct snd_pcm_substream *substream = NULL;
+	int n_pkts = 0;
+	bool period_elapsed = false;
+	bool data_valid = true;
+	bool active = false;
+	bool stopping;
+	int sp, ret;
+
+	atomic_dec(&urb_stream->urbs_in_flight);
+	jockey3_note_completion(urb_stream);
+
+	switch (jockey3_urb_check(urb)) {
+	case JOCKEY3_URB_STOPPED:
+		scoped_guard(spinlock_irqsave, &urb_stream->lock)
+			stopping = urb_stream->stopping;
+		jockey3_warn_unexpected_stop(chip, stopping, urb->status, "Capture");
+		return;
+	case JOCKEY3_URB_ERROR:
+		if (jockey3_urb_error_give_up(chip, urb_stream, urb->status, "Capture"))
+			return;
+		/* Transient: resubmit, but this buffer holds no usable data */
+		data_valid = false;
+		break;
+	case JOCKEY3_URB_OK:
+		break;
+	}
+
+	if (unlikely(jockey3_is_disconnected(chip)))
+		return;
+
+	/*
+	 * The firmware fills a multi-packet capture URB completely rather than
+	 * terminating at one 512 B packet, so the expected case is
+	 * actual_length == JOCKEY3_CAPTURE_XFER_SIZE. Derive the count from
+	 * what actually came back rather than assuming N, in case that
+	 * changes under load or on other firmware revisions.
+	 */
+	if (data_valid) {
+		n_pkts = urb->actual_length / PLOYTEC_PKT_SIZE;
+		if (unlikely(n_pkts == 0)) {
+			dev_err(&chip->intf0->dev, "Capture URB too small: %d; required at least %d\n",
+				urb->actual_length, PLOYTEC_PKT_SIZE);
+			data_valid = false;
+		} else if (unlikely(urb->actual_length % PLOYTEC_PKT_SIZE)) {
+			/*
+			 * A short trailing partial packet: use the
+			 * complete ones and drop the rest, but this should
+			 * not happen on firmware behaving as described above
+			 * -- log it once so a change in device behavior is
+			 * visible instead of silently discarded audio.
+			 */
+			dev_warn_once(&chip->intf0->dev,
+				      "Capture URB length %d not a multiple of %d, using %d packet(s)\n",
+				      urb->actual_length, PLOYTEC_PKT_SIZE, n_pkts);
+		}
+	}
+
+	/* Step 1: Safely fetch the pointer and join the safe zone */
+	scoped_guard(spinlock_irqsave, &urb_stream->lock) {
+		if (data_valid)
+			urb_stream->consec_errors = 0;
+
+		if (data_valid && !urb_stream->stopping &&
+		    urb_stream->running && urb_stream->substream) {
+			urb_stream->callbacks_active++;
+			active = true;
+			substream = urb_stream->substream;
+
+			for (sp = 0; sp < n_pkts; sp++)
+				period_elapsed |= jockey3_process_in_packet(chip,
+					urb->transfer_buffer + sp * PLOYTEC_PKT_SIZE);
+		}
+	}
+
+	/*
+	 * Step 2: Safe Zone. ALSA core can't free 'substream' because
+	 * jockey3_pcm_sync_stop() waits for 'callbacks_active' to drain before
+	 * the core releases the buffer. Our lock is released here to avoid an
+	 * ABBA deadlock with ALSA's internal locking: snd_pcm_period_elapsed()
+	 * takes the stream lock, and the order is stream lock -> urb_stream->lock.
+	 */
+	if (period_elapsed && substream)
+		snd_pcm_period_elapsed(substream);
+
+	ret = 0;
+	scoped_guard(spinlock_irqsave, &urb_stream->lock) {
+		/* Leave the safe zone; last one out wakes any waiter */
+		if (active && !--urb_stream->callbacks_active)
+			wake_up(&urb_stream->drain_wait);
+
+		/*
+		 * Keep resubmitting the URB while the interface is alive. The
+		 * 'stopping' test and the anchor+submit below must stay in this
+		 * one critical section: that is what stops a URB being re-added
+		 * to an anchor jockey3_stop_urbs() has already drained.
+		 */
+		if (!urb_stream->stopping && !jockey3_is_disconnected(chip)) {
+			urb->transfer_buffer_length = urb_stream->n_pkts * PLOYTEC_PKT_SIZE;
+			atomic_inc(&urb_stream->urbs_in_flight);
+			usb_anchor_urb(urb, &urb_stream->anchor);
+			ret = usb_submit_urb(urb, GFP_ATOMIC);
+			if (ret < 0) {
+				atomic_dec(&urb_stream->urbs_in_flight);
+				usb_unanchor_urb(urb);
+			}
+		}
+	}
+	if (ret < 0)
+		jockey3_resubmit_failed(chip, ret, "capture");
+}
+
+/**
+ * jockey3_get_next_midi_out_byte() - pick the MIDI byte for one playback packet
+ * @chip: driver state
+ *
+ * Every outgoing playback packet reserves one slot for MIDI. This returns the
+ * byte for it, through a leaky-bucket limiter holding the stream to roughly
+ * 2500 bytes/sec. The hardware carries the full MIDI line rate of 3125
+ * bytes/sec, but sustained traffic at it makes the control surface stop
+ * responding to updates, so the limit sits below that. The idle byte is
+ * returned when there is nothing to send or no budget.
+ *
+ * midi_lock is held across snd_rawmidi_transmit() so
+ * chip->midi_out_substream cannot change under us. The order is safe because
+ * the rawmidi core calls snd_rawmidi_output_trigger() without substream->lock,
+ * and holding a driver lock there is the established idiom (sound/usb/midi.c
+ * does it under ep->buffer_lock).
+ *
+ * Called from the playback URB completion handler, so this runs in atomic
+ * context.
+ *
+ * Return: the byte to place in the packet's MIDI slot.
+ */
+static u8 jockey3_get_next_midi_out_byte(struct jockey3_chip *chip)
+{
+	u8 b;
+
+	guard(spinlock_irqsave)(&chip->midi_lock);
+
+	/*
+	 * Rate limit MIDI to ~2500 bytes/sec -- see the kernel-doc above for why
+	 * this sits below the device's 3125 bytes/sec MIDI line rate.
+	 */
+	chip->midi_out_acc += 2500;
+	if (chip->midi_out_acc < chip->midi_rate_divisor)
+		return PLOYTEC_MIDI_IDLE_BYTE;
+	chip->midi_out_acc -= chip->midi_rate_divisor;
+
+	/* Handle queued byte from Running Status expansion first before consuming from ALSA */
+	if (chip->midi_state.has_queued_byte) {
+		chip->midi_state.has_queued_byte = false;
+		return chip->midi_state.queued_byte;
+	}
+
+	if (!chip->midi_out_substream)
+		return PLOYTEC_MIDI_IDLE_BYTE;
+
+	if (snd_rawmidi_transmit(chip->midi_out_substream, &b, 1) != 1)
+		return PLOYTEC_MIDI_IDLE_BYTE;
+
+	return ploytec_midi_running_status_expand(&chip->midi_state, b, &chip->intf0->dev);
+}
+
+/*
+ * Fill the sample area of a playback packet with silence, for when there is
+ * no PCM data to send.
+ *
+ * Only the sample area is touched: everything from PLOYTEC_MIDI_OUT_OFFSET
+ * onwards -- the MIDI slot, the sync byte and the trailing gap -- is rewritten
+ * unconditionally by jockey3_playback_callback() immediately afterwards.
+ */
+static void jockey3_silence_out_packet(u8 *buf)
+{
+	memset(buf, 0, PLOYTEC_MIDI_OUT_OFFSET);
+}
+
+/*
+ * Prime a freshly allocated playback buffer so the first URB, which is
+ * submitted before any completion handler has run, carries a valid idle
+ * packet. The buffer comes from kzalloc(), so the sample area and the
+ * trailing gap are already silent.
+ */
+static void jockey3_init_out_packet(u8 *buf)
+{
+	int sp;
+
+	for (sp = 0; sp < JOCKEY3_PLAYBACK_N; sp++) {
+		u8 *sub = buf + sp * PLOYTEC_PKT_SIZE;
+
+		sub[PLOYTEC_MIDI_OUT_OFFSET] = PLOYTEC_MIDI_IDLE_BYTE;
+		sub[PLOYTEC_SYNC_BYTE_OFFSET] = PLOYTEC_SYNC_BYTE_VALUE;
+	}
+}
+
+static void jockey3_playback_callback(struct urb *urb)
+{
+	struct jockey3_chip *chip = urb->context;
+	struct jockey3_pcm_urb_stream *urb_stream = &chip->playback;
+	unsigned char *buf = (unsigned char *)urb->transfer_buffer;
+	struct snd_pcm_substream *substream = NULL;
+	unsigned int submit_pkts;
+	bool period_elapsed = false;
+	bool data_valid = true;
+	bool active = false;
+	bool stopping;
+	int i, sp, ret;
+
+	atomic_dec(&urb_stream->urbs_in_flight);
+	jockey3_note_completion(urb_stream);
+
+	switch (jockey3_urb_check(urb)) {
+	case JOCKEY3_URB_STOPPED:
+		scoped_guard(spinlock_irqsave, &urb_stream->lock)
+			stopping = urb_stream->stopping;
+		jockey3_warn_unexpected_stop(chip, stopping, urb->status, "Playback");
+		return;
+	case JOCKEY3_URB_ERROR:
+		if (jockey3_urb_error_give_up(chip, urb_stream, urb->status, "Playback"))
+			return;
+		data_valid = false;
+		break;
+	case JOCKEY3_URB_OK:
+		break;
+	}
+
+	if (unlikely(jockey3_is_disconnected(chip)))
+		return;
+
+	/*
+	 * Step 1: Safely fetch the pointer and join the safe zone.
+	 *
+	 * @n_pkts is captured once here and reused below for the MIDI/sync
+	 * loop and the resubmit's transfer_buffer_length, rather than re-read
+	 * at each site: jockey3_pcm_hw_params() can change it concurrently
+	 * (under the same lock), and this URB's buffer must only ever claim
+	 * to carry as many freshly written packets as it actually filled
+	 * this callback. Reading a larger value at resubmit than was used to
+	 * fill would send stale packets left over from an earlier, larger N
+	 * instead of tearing the ring down.
+	 */
+	scoped_guard(spinlock_irqsave, &urb_stream->lock) {
+		submit_pkts = urb_stream->n_pkts;
+
+		if (data_valid)
+			urb_stream->consec_errors = 0;
+
+		if (data_valid && !urb_stream->stopping &&
+		    urb_stream->running && urb_stream->substream) {
+			urb_stream->callbacks_active++;
+			active = true;
+			substream = urb_stream->substream;
+
+			for (sp = 0; sp < submit_pkts; sp++)
+				period_elapsed |= jockey3_process_out_packet(chip,
+					buf + sp * PLOYTEC_PKT_SIZE);
+		} else {
+			for (sp = 0; sp < submit_pkts; sp++)
+				jockey3_silence_out_packet(buf + sp * PLOYTEC_PKT_SIZE);
+		}
+	}
+
+	/*
+	 * The outgoing MIDI data is encapsulated in the playback stream, one
+	 * real (rate-limited) byte per packet: jockey3_get_next_midi_out_byte()'s
+	 * leaky-bucket limiter is calibrated on @midi_rate_divisor, which
+	 * assumes exactly one call per packet interval. Calling it once
+	 * per URB instead of once per packet would silently divide MIDI
+	 * OUT throughput by the chosen N.
+	 */
+	for (sp = 0; sp < submit_pkts; sp++) {
+		u8 *sub = buf + sp * PLOYTEC_PKT_SIZE;
+
+		sub[PLOYTEC_MIDI_OUT_OFFSET] = jockey3_get_next_midi_out_byte(chip);
+
+		/* Ploytec Sync byte and gap padding */
+		sub[PLOYTEC_SYNC_BYTE_OFFSET] = PLOYTEC_SYNC_BYTE_VALUE;
+		for (i = PLOYTEC_SYNC_BYTE_OFFSET + 1; i < PLOYTEC_PKT_SIZE; i++)
+			sub[i] = 0x00;
+	}
+
+	/*
+	 * Step 2: Safe Zone. ALSA core can't free 'substream' because
+	 * jockey3_pcm_sync_stop() waits for 'callbacks_active' to drain before
+	 * the core releases the buffer. Our lock is released here to avoid an
+	 * ABBA deadlock with ALSA's internal locking: snd_pcm_period_elapsed()
+	 * takes the stream lock, and the order is stream lock -> urb_stream->lock.
+	 */
+	if (period_elapsed && substream)
+		snd_pcm_period_elapsed(substream);
+
+	ret = 0;
+	scoped_guard(spinlock_irqsave, &urb_stream->lock) {
+		/* Leave the safe zone; last one out wakes any waiter */
+		if (active && !--urb_stream->callbacks_active)
+			wake_up(&urb_stream->drain_wait);
+
+		/*
+		 * Keep resubmitting the URB while the interface is alive. The
+		 * 'stopping' test and the anchor+submit below must stay in this
+		 * one critical section: that is what stops a URB being re-added
+		 * to an anchor jockey3_stop_urbs() has already drained.
+		 */
+		if (!urb_stream->stopping && !jockey3_is_disconnected(chip)) {
+			urb->transfer_buffer_length = submit_pkts * PLOYTEC_PKT_SIZE;
+			atomic_inc(&urb_stream->urbs_in_flight);
+			usb_anchor_urb(urb, &urb_stream->anchor);
+			ret = usb_submit_urb(urb, GFP_ATOMIC);
+			if (ret < 0) {
+				atomic_dec(&urb_stream->urbs_in_flight);
+				usb_unanchor_urb(urb);
+			}
+		}
+	}
+	if (ret < 0)
+		jockey3_resubmit_failed(chip, ret, "playback");
+}
+
+static void jockey3_midi_in_callback(struct urb *urb)
+{
+	struct jockey3_chip *chip = urb->context;
+	unsigned char *buf = (unsigned char *)urb->transfer_buffer;
+	unsigned int errors;
+	bool stopping;
+	int i, n = 0, ret;
+
+	switch (jockey3_urb_check(urb)) {
+	case JOCKEY3_URB_STOPPED:
+		scoped_guard(spinlock_irqsave, &chip->midi_lock)
+			stopping = chip->midi_stopping;
+		jockey3_warn_unexpected_stop(chip, stopping, urb->status, "MIDI IN");
+		return;
+	case JOCKEY3_URB_ERROR:
+		scoped_guard(spinlock_irqsave, &chip->midi_lock)
+			errors = ++chip->midi_consec_errors;
+
+		/* Already given up; see jockey3_urb_error_give_up() */
+		if (errors > JOCKEY3_MAX_URB_ERRORS)
+			return;
+
+		dev_err_ratelimited(&chip->intf0->dev,
+				    "MIDI IN URB error: %d (%u consecutive)\n",
+				    urb->status, errors);
+		if (errors == JOCKEY3_MAX_URB_ERRORS) {
+			dev_err(&chip->intf0->dev,
+				"MIDI IN stopped after %u consecutive URB errors\n", errors);
+			return;
+		}
+		/* Transient: resubmit, but there is no usable data in this buffer */
+		urb->actual_length = 0;
+		break;
+	case JOCKEY3_URB_OK:
+		scoped_guard(spinlock_irqsave, &chip->midi_lock)
+			chip->midi_consec_errors = 0;
+		break;
+	}
+
+	if (unlikely(jockey3_is_disconnected(chip)))
+		return;
+
+	/*
+	 * Compact the payload in place, dropping the padding the device emits
+	 * between real MIDI bytes. The transfer buffer is ours, so this needs no
+	 * lock; doing it up front turns the delivery below into a single locked
+	 * call instead of one per byte.
+	 *
+	 * For different devices (firmware revision) different padding bytes have
+	 * been observed: 0xF9, 0xFB, 0xFD, 0xFF. Since the device is not sending
+	 * any MIDI system or real-time messages, we can safely ignore any byte
+	 * 0xF0..0xFF received from the device.
+	 */
+	for (i = 0; i < urb->actual_length; i++)
+		if (buf[i] < 0xF0)
+			buf[n++] = buf[i];
+
+	ret = 0;
+	scoped_guard(spinlock_irqsave, &chip->midi_lock) {
+		/*
+		 * Deliver under midi_lock so the substream cannot be cleared by
+		 * jockey3_midi_in_close() while we are dereferencing it.
+		 */
+		if (n && !chip->midi_stopping && chip->midi_in_substream)
+			snd_rawmidi_receive(chip->midi_in_substream, buf, n);
+
+		if (!chip->midi_stopping && !jockey3_is_disconnected(chip))
+			ret = usb_submit_urb(urb, GFP_ATOMIC);
+	}
+	if (ret < 0)
+		jockey3_resubmit_failed(chip, ret, "MIDI IN");
+}
+
+/*
+ * How long until a direction's watchdog deadline (last activity plus
+ * JOCKEY3_WATCHDOG_STALL_MS, or the current start grace if no completion has
+ * arrived since the last start), in ms clamped to
+ * [JOCKEY3_WATCHDOG_MIN_POLL_MS, JOCKEY3_WATCHDOG_POLL_MS]. Returns
+ * JOCKEY3_WATCHDOG_POLL_MS if the direction has never started (no deadline to
+ * chase yet), and the floor if the deadline has already passed, so a
+ * confirmed stall gets rechecked tightly instead of waiting out a stale
+ * window. Only meaningful while a PCM stream is open somewhere -- see the
+ * caller, jockey3_watchdog_arm().
+ */
+static unsigned long jockey3_watchdog_next_delay_ms(struct jockey3_chip *chip,
+						    const struct jockey3_pcm_urb_stream *urb_stream)
+{
+	u64 last = atomic64_read(&urb_stream->last_callback_time);
+	u64 started = atomic64_read(&urb_stream->urbs_started_time);
+	u64 threshold_ms = JOCKEY3_WATCHDOG_STALL_MS;
+	unsigned int grace_ms = jockey3_start_grace_ms();
+	u64 now, remaining_ns;
+
+	if (!started)
+		return JOCKEY3_WATCHDOG_POLL_MS;
+
+	now = ktime_get_mono_fast_ns();
+
+	/*
+	 * Mirror jockey3_watchdog_check()'s own time-based grace window
+	 * (see its kernel-doc): while still inside it, the deadline to chase
+	 * is urbs_started_time + grace_ms regardless of whether an early
+	 * completion has already advanced last_callback_time, or this would
+	 * schedule a tight re-poll off a completion that does not end grace.
+	 */
+	if (now - started < (u64)grace_ms * NSEC_PER_MSEC) {
+		last = started;
+		threshold_ms = grace_ms;
+	} else if (!last) {
+		last = started;
+	}
+
+	remaining_ns = last + threshold_ms * NSEC_PER_MSEC - now;
+
+	if ((s64)remaining_ns <= 0)
+		return JOCKEY3_WATCHDOG_MIN_POLL_MS;
+
+	return clamp_t(unsigned long, div_u64(remaining_ns, NSEC_PER_MSEC),
+		       JOCKEY3_WATCHDOG_MIN_POLL_MS, JOCKEY3_WATCHDOG_POLL_MS);
+}
+
+/* Forward declaration: defined further down, alongside its other caller jockey3_pcm_hw_params() */
+static bool jockey3_stream_is_open(struct jockey3_chip *chip, const int direction);
+
+/*
+ * Schedule the next watchdog tick.
+ *
+ * State-dependent cadence. With no substream open nothing is waiting on ALSA
+ * core's wait_for_avail() timeout, so there is no reason to chase
+ * JOCKEY3_WATCHDOG_STALL_MS; poll at the JOCKEY3_WATCHDOG_POLL_MS ceiling.
+ * Once a substream is open, self-reschedule from the nearer of the two
+ * directions' deadlines, as dev_watchdog() does in net/sched/sch_generic.c.
+ *
+ * Unlike dev_watchdog() the delay is not passed through
+ * round_jiffies_relative(), which rounds to whole seconds -- right for a
+ * multi-second watchdog_timeo, two orders of magnitude too coarse here.
+ *
+ * system_long_wq rather than system_wq: the tick is cheap, but
+ * jockey3_watchdog_check() may call jockey3_recover_urb_stream(), which blocks
+ * for seconds, and system_wq items are expected to be short.
+ */
+static void jockey3_watchdog_arm(struct jockey3_chip *chip)
+{
+	unsigned long delay_ms;
+
+	if (jockey3_stream_is_open(chip, SNDRV_PCM_STREAM_PLAYBACK) ||
+	    jockey3_stream_is_open(chip, SNDRV_PCM_STREAM_CAPTURE))
+		delay_ms = min(jockey3_watchdog_next_delay_ms(chip, &chip->playback),
+			       jockey3_watchdog_next_delay_ms(chip, &chip->capture));
+	else
+		delay_ms = JOCKEY3_WATCHDOG_POLL_MS;
+
+	queue_delayed_work(system_long_wq, &chip->watchdog_work, msecs_to_jiffies(delay_ms));
+}
+
+/**
+ * jockey3_watchdog_clear_stall() - close out a stall that a restart ended
+ * @chip: driver state
+ * @urb_stream: the affected direction
+ * @type: direction name, for the log message
+ *
+ * Every recovery path in this driver goes through jockey3_stop_urbs() and
+ * jockey3_start_urbs(), so a stalled stream is essentially always brought back
+ * by a restart rather than by starting to complete URBs again on its own. If
+ * the restart simply cleared the flag, the watchdog's onset line would never
+ * be paired with anything and the outage would have no recorded end -- which
+ * makes "stalls that ended" indistinguishable from "stalls still open" for
+ * anything reading the log afterwards.
+ *
+ * So the restart closes the outage explicitly. Together with the recovery line
+ * in jockey3_watchdog_check(), every onset has exactly one counterpart.
+ */
+static void jockey3_watchdog_clear_stall(struct jockey3_chip *chip,
+					 struct jockey3_pcm_urb_stream *urb_stream,
+					 const char *type)
+{
+	u64 outage_ns = 0;
+	bool reported;
+
+	scoped_guard(spinlock_irqsave, &urb_stream->lock) {
+		reported = urb_stream->stall_reported;
+		if (reported)
+			outage_ns = ktime_get_mono_fast_ns() - urb_stream->stall_since;
+		urb_stream->stall_reported = false;
+	}
+
+	if (reported)
+		dev_warn(&chip->intf0->dev,
+			 "%s URB stream restarted after stalling for %llu ms\n",
+			 type, div_u64(outage_ns, NSEC_PER_MSEC));
+}
+
+/*
+ * Stop the watchdog from the URB teardown path.
+ *
+ * Deliberately the non-sync cancel: jockey3_stop_urbs() runs inside rate_mutex
+ * at several sites and the work item takes locks of its own, so waiting for a
+ * running tick here would be a deadlock waiting to happen. Not waiting is safe
+ * because a tick that is already running re-reads 'stopping' under the stream
+ * lock and does nothing. The sync cancel that teardown does need lives in
+ * jockey3_disconnect() and in probe's error path, where no mutex is held.
+ */
+static void jockey3_watchdog_disarm(struct jockey3_chip *chip)
+{
+	cancel_delayed_work(&chip->watchdog_work);
+}
+
+/**
+ * jockey3_stop_urbs() - stop all PCM and MIDI URBs
+ * @chip: driver state
+ *
+ * Fences the completion handlers, then kills every URB. Sleeps, so it must not
+ * be called from atomic context. On return no completion handler is running and
+ * none can resubmit.
+ */
+static void jockey3_stop_urbs(struct jockey3_chip *chip)
+{
+	dev_dbg(&chip->intf0->dev, "Stopping all URBs\n");
+
+	jockey3_watchdog_disarm(chip);
+
+	/*
+	 * Fence the completion handlers before killing anything. Each 'stopping'
+	 * store pairs with the test the matching handler makes while holding the
+	 * same lock it uses to anchor and resubmit, so once these guards are
+	 * released no handler can add a URB back to an anchor we are about to
+	 * drain. The spinlocks provide the required ordering; no explicit barrier
+	 * is needed.
+	 *
+	 * These stores must also stay above the timestamp zeroing below. The
+	 * watchdog is disarmed without waiting for a tick that is already
+	 * running, and such a tick samples the timestamps before it takes the
+	 * stream lock; 'stopping' being set by the time it gets there is the only
+	 * thing that stops it reporting a stall for a stream we stopped on purpose.
+	 */
+	scoped_guard(spinlock_irqsave, &chip->playback.lock)
+		chip->playback.stopping = true;
+	scoped_guard(spinlock_irqsave, &chip->capture.lock)
+		chip->capture.stopping = true;
+	scoped_guard(spinlock_irqsave, &chip->midi_lock)
+		chip->midi_stopping = true;
+
+	/*
+	 * usb_kill_urb()/usb_kill_anchored_urbs() do not return until the
+	 * completion handler of each URB has finished, so no callback can still
+	 * be in its safe zone once these return -- no separate drain is needed
+	 * here (jockey3_pcm_sync_stop() covers the ALSA buffer-teardown path).
+	 *
+	 * Worth spelling out for the anchored form, which keeps getting read as
+	 * a use-after-free: __usb_hcd_giveback_urb() does unanchor before
+	 * urb->complete(), so the kill loop cannot find a running handler on the
+	 * list. anchor->suspend_wakeups covers it -- raised before the unanchor
+	 * and dropped after complete() returns, and usb_kill_anchored_urbs()
+	 * drains until usb_anchor_check_wakeup() sees it zero.
+	 */
+	usb_kill_urb(chip->midi_in_urb);
+	usb_kill_anchored_urbs(&chip->playback.anchor);
+	usb_kill_anchored_urbs(&chip->capture.anchor);
+
+	/*
+	 * Drop the liveness timestamps: a stale value would otherwise make
+	 * jockey3_check_urb_stream_alive() report a stopped stream as alive for
+	 * up to its 1 ms window. The cadence counters go with them so the next
+	 * start's jockey3_stream_streaming_healthy() sees only its own completions.
+	 */
+	atomic64_set(&chip->playback.last_callback_time, 0);
+	atomic64_set(&chip->capture.last_callback_time, 0);
+	atomic64_set(&chip->playback.first_callback_time, 0);
+	atomic64_set(&chip->capture.first_callback_time, 0);
+	atomic_set(&chip->playback.completions_since_start, 0);
+	atomic_set(&chip->capture.completions_since_start, 0);
+	atomic64_set(&chip->playback.urbs_started_time, 0);
+	atomic64_set(&chip->capture.urbs_started_time, 0);
+
+	/* after killing the URBs there will be no in-flight requests anymore since the callback
+	 * function has been called as part of the shutdown. The number of in-flight URBs should
+	 * therefore be zero at this point. Log an inconsistency error if not.
+	 */
+	if (atomic_read(&chip->playback.urbs_in_flight) != 0)
+		dev_err(&chip->intf0->dev, "Inconsistent URB in-flight count: playback=%d != 0\n",
+			atomic_read(&chip->playback.urbs_in_flight));
+	if (atomic_read(&chip->capture.urbs_in_flight) != 0)
+		dev_err(&chip->intf0->dev, "Inconsistent URB in-flight count: capture=%d != 0\n",
+			atomic_read(&chip->capture.urbs_in_flight));
+}
+
+/*
+ * Demote an unplug, as jockey3_start_urbs_failed() already does for the
+ * aggregate result. -%ENOENT stays loud: the endpoints were disabled while the
+ * driver is still bound, which takes a device reset to undo.
+ *
+ * @slot is the ring slot, or -1 for the single MIDI IN URB.
+ */
+static void jockey3_submit_failed(struct jockey3_chip *chip, int err,
+				  const char *what, int slot)
+{
+	if (jockey3_err_device_gone(err)) {
+		if (slot < 0)
+			dev_dbg(&chip->intf0->dev,
+				"Not submitting the %s URB: device is gone (%d)\n", what, err);
+		else
+			dev_dbg(&chip->intf0->dev,
+				"Not submitting %s URB %d: device is gone (%d)\n",
+				what, slot, err);
+		return;
+	}
+
+	if (slot < 0)
+		dev_err(&chip->intf0->dev, "Failed to submit the %s URB: %d\n", what, err);
+	else
+		dev_err(&chip->intf0->dev, "Failed to submit %s URB %d: %d\n",
+			what, slot, err);
+}
+
+/**
+ * jockey3_start_urbs() - submit all PCM and MIDI URBs
+ * @chip: driver state
+ * @warm: true only for the stall watchdog's own lightweight restart of a ring
+ *	that was streaming a moment earlier; false for a cold start (first open,
+ *	rate change, USB reset, resume, probe). Both share start_grace_ms; this
+ *	only labels the restart for dev_dbg() and is recorded chip-wide in
+ *	@chip->warm_start.
+ *
+ * Clears the stop fences and error budgets, then submits the full URB ring for
+ * both directions plus the MIDI IN URB. Uses GFP_KERNEL, so process context
+ * only. A failure to submit one URB does not prevent the others being tried.
+ *
+ * The return value must be checked. Only a completion handler resubmits a URB,
+ * so a ring that comes up short stays short: nothing retries the URBs that
+ * failed here, and the direction runs at reduced depth for as long as the
+ * device stays bound, with no bookkeeping that would ever notice. Callers pass
+ * the result to jockey3_start_urbs_failed(), or return it to their own caller.
+ *
+ * Return: 0 on success, or the first submit error encountered.
+ */
+static int jockey3_start_urbs(struct jockey3_chip *chip, bool warm)
+{
+	int i, ret, first_err = 0;
+	int n_playback = 0, n_capture = 0;
+	u8 playback_n_pkts, capture_n_pkts;
+
+	if (jockey3_is_disconnected(chip))
+		return -ENODEV;
+
+	/*
+	 * Belt and braces; jockey3_recover_urb_stream() is what actually bails.
+	 * jockey3_restore_device() clears the flag before its own restart.
+	 */
+	if (jockey3_is_suspended(chip))
+		return -ESHUTDOWN;
+
+	dev_dbg(&chip->intf0->dev, "Starting all URBs (%s start, grace %u ms)\n",
+		warm ? "warm" : "cold", jockey3_start_grace_ms());
+
+	/*
+	 * Clear the error budget as well: a stream that was given up on must get
+	 * a fresh JOCKEY3_MAX_URB_ERRORS allowance, otherwise the first error
+	 * after a restart would immediately exceed the stale count and give up
+	 * again with no retries.
+	 */
+	scoped_guard(spinlock_irqsave, &chip->playback.lock) {
+		chip->playback.stopping = false;
+		chip->playback.consec_errors = 0;
+	}
+	scoped_guard(spinlock_irqsave, &chip->capture.lock) {
+		chip->capture.stopping = false;
+		chip->capture.consec_errors = 0;
+	}
+	scoped_guard(spinlock_irqsave, &chip->midi_lock) {
+		chip->midi_stopping = false;
+		chip->midi_consec_errors = 0;
+	}
+
+	/* Report and clear any stall this restart is about to end */
+	jockey3_watchdog_clear_stall(chip, &chip->playback, "Playback");
+	jockey3_watchdog_clear_stall(chip, &chip->capture, "Capture");
+
+	/*
+	 * Stamp the start before submitting, not after: this is what the
+	 * watchdog measures from until the first completion arrives, and a URB
+	 * can complete before the loop below has finished. Reset the cadence
+	 * counters in the same breath, and record which grace applies.
+	 */
+	WRITE_ONCE(chip->warm_start, warm);
+	atomic64_set(&chip->playback.first_callback_time, 0);
+	atomic64_set(&chip->capture.first_callback_time, 0);
+	atomic_set(&chip->playback.completions_since_start, 0);
+	atomic_set(&chip->capture.completions_since_start, 0);
+	atomic64_set(&chip->playback.urbs_started_time, ktime_get_mono_fast_ns());
+	atomic64_set(&chip->capture.urbs_started_time, ktime_get_mono_fast_ns());
+
+	/*
+	 * Each direction's ring is (re)armed at its own current N
+	 * (jockey3_pcm_hw_params()), which persists across a restart rather than
+	 * falling back to the JOCKEY3_PLAYBACK_N/JOCKEY3_CAPTURE_N default just
+	 * because the ring turned over. A direction with no open stream is held
+	 * at that default by jockey3_pcm_close(), so it always re-arms at a
+	 * safe N.
+	 *
+	 * Both are read once here rather than per iteration: they are written
+	 * under the stream lock, which is not held here, so a re-load could
+	 * leave URBs in the same ring armed at different lengths.
+	 */
+	playback_n_pkts = READ_ONCE(chip->playback.n_pkts);
+	capture_n_pkts = READ_ONCE(chip->capture.n_pkts);
+
+	for (i = 0; i < JOCKEY3_N_URBS; i++) {
+		chip->playback.urbs[i]->transfer_buffer_length =
+			playback_n_pkts * PLOYTEC_PKT_SIZE;
+		chip->capture.urbs[i]->transfer_buffer_length =
+			capture_n_pkts * PLOYTEC_PKT_SIZE;
+
+		atomic_inc(&chip->playback.urbs_in_flight);
+		usb_anchor_urb(chip->playback.urbs[i], &chip->playback.anchor);
+		ret = usb_submit_urb(chip->playback.urbs[i], GFP_KERNEL);
+		if (ret < 0) {
+			atomic_dec(&chip->playback.urbs_in_flight);
+			usb_unanchor_urb(chip->playback.urbs[i]);
+			jockey3_submit_failed(chip, ret, "playback", i);
+			if (!first_err)
+				first_err = ret;
+		} else {
+			n_playback++;
+		}
+
+		atomic_inc(&chip->capture.urbs_in_flight);
+		usb_anchor_urb(chip->capture.urbs[i], &chip->capture.anchor);
+		ret = usb_submit_urb(chip->capture.urbs[i], GFP_KERNEL);
+		if (ret < 0) {
+			atomic_dec(&chip->capture.urbs_in_flight);
+			usb_unanchor_urb(chip->capture.urbs[i]);
+			jockey3_submit_failed(chip, ret, "capture", i);
+			if (!first_err)
+				first_err = ret;
+		} else {
+			n_capture++;
+		}
+	}
+	ret = usb_submit_urb(chip->midi_in_urb, GFP_KERNEL);
+	if (ret < 0) {
+		jockey3_submit_failed(chip, ret, "MIDI IN", -1);
+		if (!first_err)
+			first_err = ret;
+	}
+
+	if (n_playback < JOCKEY3_N_URBS || n_capture < JOCKEY3_N_URBS) {
+		if (jockey3_err_device_gone(first_err))
+			dev_dbg(&chip->intf0->dev,
+				"Started only %d/%d playback and %d/%d capture URBs; device is gone\n",
+				n_playback, JOCKEY3_N_URBS, n_capture, JOCKEY3_N_URBS);
+		else
+			dev_err(&chip->intf0->dev,
+				"Started only %d/%d playback and %d/%d capture URBs; ring will not refill\n",
+				n_playback, JOCKEY3_N_URBS, n_capture, JOCKEY3_N_URBS);
+	}
+
+	/*
+	 * Arm regardless of first_err. A ring that came up short, or did not come
+	 * up at all, is precisely the state worth watching: when the endpoints
+	 * have been disabled underneath us every submit fails and nothing is left
+	 * to report the resulting silence.
+	 */
+	jockey3_watchdog_arm(chip);
+
+	return first_err;
+}
+
+/**
+ * jockey3_start_urbs_failed() - react to a failed jockey3_start_urbs()
+ * @chip: driver state
+ * @err: the value jockey3_start_urbs() returned; 0 is ignored
+ * @context: what was being attempted, for the log message
+ *
+ * Classifies a submit failure so that a condition needing a device reset is not
+ * mistaken for an ordinary unplug:
+ *
+ * - %-ENOENT means the endpoint is administratively gone while the driver is
+ *   still bound. usb_submit_urb() reports it when usb_pipe_endpoint() finds no
+ *   endpoint, and usb_hcd_link_urb_to_ep() when the endpoint is not enabled --
+ *   which is the state usb_set_interface() leaves interface 0 in when its
+ *   SET_INTERFACE request fails, since it disables the endpoints before sending
+ *   the request and does not re-enable them on that path. Nothing short of
+ *   re-enumerating the device restores it.
+ *
+ * - %-ENODEV means the device is already detached, so a reset would be
+ *   pointless and jockey3_disconnect() is on its way. This is the common case
+ *   on an ordinary unplug and must not be treated like the one above.
+ *
+ * - anything else is reported and left alone; the watchdog picks up whatever
+ *   silence results.
+ *
+ * Note that trying to undo the failure by selecting the working altsetting
+ * again does not work: if the control endpoint is unresponsive, that request
+ * times out as well and the endpoints stay disabled regardless.
+ */
+static void jockey3_start_urbs_failed(struct jockey3_chip *chip, int err, const char *context)
+{
+	if (!err)
+		return;
+
+	if (err == -ENODEV || jockey3_is_disconnected(chip)) {
+		dev_dbg(&chip->intf0->dev, "Could not start URBs after %s: device is gone\n",
+			context);
+		return;
+	}
+
+	if (err == -ESHUTDOWN || jockey3_is_suspended(chip)) {
+		dev_dbg(&chip->intf0->dev,
+			"Could not start URBs after %s: device is suspended; the resume path will start them\n",
+			context);
+		return;
+	}
+
+	if (err == -ENOENT) {
+		dev_err(&chip->intf0->dev,
+			"Endpoints are disabled after %s; the device needs a reset to restore them\n",
+			context);
+		return;
+	}
+
+	dev_err(&chip->intf0->dev, "Failed to start URBs after %s: %d\n", context, err);
+}
+
+/*
+ * Log a failed rate-change step at the severity it deserves, matching what
+ * jockey3_start_urbs_failed() applies on the URB path.
+ */
+static void jockey3_rate_step_failed(struct jockey3_chip *chip, int err, const char *what)
+{
+	if (jockey3_err_device_gone(err)) {
+		dev_dbg(&chip->intf0->dev, "%s: device is gone (%d)\n", what, err);
+		return;
+	}
+
+	dev_err(&chip->intf0->dev, "%s: %d\n", what, err);
+}
+
+static int jockey3_set_rate(struct jockey3_chip *chip, unsigned int rate, bool cold_init)
+{
+	int ret;
+	u32 current_hw_rate;
+
+	if (jockey3_is_disconnected(chip))
+		return -ENODEV;
+
+	dev_dbg(&chip->intf0->dev, "Setting rate to %u Hz\n", rate);
+
+	ret = ploytec_initialize_device(chip->intf0, chip->xfer_buf, !cold_init, NULL);
+	if (ret < 0) {
+		jockey3_rate_step_failed(chip, ret, "Failed to initialize device to change rate");
+		return ret;
+	}
+
+	ret = ploytec_get_rate(chip->intf0, chip->xfer_buf, PLOYTEC_RATE_IDX_DEVICE,
+			       &current_hw_rate);
+	if (ret < 0) {
+		jockey3_rate_step_failed(chip, ret, "Failed to read current hardware rate");
+		return ret;
+	}
+	dev_dbg(&chip->intf0->dev, "Current hardware rate: %u Hz\n", current_hw_rate);
+
+	/*
+	 * Program the rate even when the device already reports it. Skipping the
+	 * write on a match would silently elide it during probe every time,
+	 * since initialization always asks for 44100 Hz and that is also the
+	 * device's power-on default. The write evidently does more than set a
+	 * frequency; no vendor initialization omits it. See
+	 * re/usb/init_timing_comparison.md.
+	 *
+	 * Callers that want to avoid a redundant rate change already check
+	 * against chip->current_rate before getting here.
+	 */
+	dev_dbg(&chip->intf0->dev, "Setting hardware rate: %u Hz\n", rate);
+	ret = ploytec_set_rate(chip->intf0, chip->xfer_buf, rate, cold_init);
+	if (ret < 0) {
+		jockey3_rate_step_failed(chip, ret, "Failed to set rate");
+		return ret;
+	}
+	ret = ploytec_start_streaming(chip->intf0, chip->xfer_buf);
+	if (ret < 0) {
+		jockey3_rate_step_failed(chip, ret, "Failed to start streaming after rate change");
+		return ret;
+	}
+
+	dev_dbg(&chip->intf0->dev, "Rate set OK\n");
+	return 0;
+}
+
+static bool jockey3_stream_is_open(struct jockey3_chip *chip, const int direction)
+{
+	struct jockey3_pcm_urb_stream *urb_stream = jockey3_get_pcm_urb_stream(chip, direction);
+	bool open;
+
+	scoped_guard(spinlock_irqsave, &urb_stream->lock) {
+		open = urb_stream->substream;
+	}
+	return open;
+}
+
+/* Forward declaration: defined further down, alongside its other callers jockey3_pcm_hw_params()
+ * and jockey3_pcm_prepare()
+ */
+static int jockey3_recover_urb_stream(struct jockey3_chip *chip, const int direction,
+				      const char *context, bool report_xrun);
+
+static bool jockey3_check_urb_stream_alive(const struct jockey3_pcm_urb_stream *urb_stream)
+{
+	u64 last_time = atomic64_read(&urb_stream->last_callback_time);
+	u64 window_ns = JOCKEY3_LIVENESS_WINDOW_NS(READ_ONCE(urb_stream->n_shift));
+
+	if (!last_time)
+		return false;
+
+	/*
+	 * Alive if we had activity within the last window_ns. The window
+	 * must exceed one URB span (@n_pkts packet intervals) or a
+	 * perfectly healthy stream could sample as dead between completions
+	 * -- and scaling it with N (see JOCKEY3_LIVENESS_WINDOW_NS()) keeps
+	 * the same margin against that span at any N, rather than a fixed
+	 * window eating into a shrinking margin as N grows.
+	 */
+	return (ktime_get_mono_fast_ns() - last_time <= window_ns);
+}
+
+/*
+ * Shortest wall-clock time one URB's worth of audio can legitimately occupy:
+ * @n_pkts Ploytec packets, PLOYTEC_PLAYBACK_FRAMES (10) or
+ * PLOYTEC_CAPTURE_FRAMES (8) PCM frames each, at @rate. Returns 0 if @rate or
+ * N is not yet known. Completions arriving in a small fraction of this are a
+ * hardware-side FIFO draining or the host controller retiring transfer
+ * descriptors, not the device's audio pipeline running.
+ */
+static u64 jockey3_min_urb_interval_ns(const struct jockey3_pcm_urb_stream *urb_stream,
+				       unsigned int rate, bool is_playback)
+{
+	unsigned int fpp = is_playback ? PLOYTEC_PLAYBACK_FRAMES : PLOYTEC_CAPTURE_FRAMES;
+	unsigned int n = READ_ONCE(urb_stream->n_pkts);
+
+	if (!rate || !n)
+		return 0;
+
+	return div_u64((u64)n * fpp * NSEC_PER_SEC, rate);
+}
+
+/*
+ * jockey3_stream_streaming_healthy() - is this direction actually streaming,
+ * as opposed to having produced a trickle of implausibly fast completions?
+ *
+ * jockey3_check_urb_stream_alive() answers "did anything complete just now",
+ * which a single FIFO-drain completion satisfies. This is the stricter test,
+ * used at the one decision that must not be fooled by trickle: whether the
+ * stall watchdog's warm URB restart worked, or the ladder should escalate to a
+ * full USB reset (a stream-killing event, and on this hardware one that can
+ * leave the capture endpoint down). It requires
+ *
+ *   - at least JOCKEY3_HEALTHY_MIN_COMPLETIONS completions since the last
+ *     start -- more than a FIFO drain supplies, and a real stream lands
+ *     hundreds inside any plausible grace;
+ *   - those spread over at least half the time that many real URBs would take
+ *     (rejects a fast burst);
+ *   - the most recent one still within jockey3_check_urb_stream_alive()'s
+ *     N-scaled liveness window (rejects a burst that then stopped).
+ *
+ * With no programmed rate it cannot form the interval and reports not-healthy
+ * rather than fall back to a weaker test; @current_rate is always set while a
+ * stream is recovering.
+ */
+static bool jockey3_stream_streaming_healthy(struct jockey3_chip *chip,
+					     const struct jockey3_pcm_urb_stream *urb_stream)
+{
+	bool is_playback = urb_stream == &chip->playback;
+	const char *type = is_playback ? "Playback" : "Capture";
+	u64 first = atomic64_read(&urb_stream->first_callback_time);
+	u64 last = atomic64_read(&urb_stream->last_callback_time);
+	int n = atomic_read(&urb_stream->completions_since_start);
+	unsigned int rate = READ_ONCE(chip->current_rate);
+	u64 min_interval, span, floor;
+
+	/* Not enough completions yet to judge -- the common "still warming up"
+	 * state during the poll, kept silent so it does not flood the log.
+	 */
+	if (n < JOCKEY3_HEALTHY_MIN_COMPLETIONS || !first || last <= first)
+		return false;
+
+	min_interval = jockey3_min_urb_interval_ns(urb_stream, rate, is_playback);
+	if (!min_interval) {
+		dev_dbg_ratelimited(&chip->intf0->dev,
+				    "%s health: no programmed rate, cannot judge cadence\n", type);
+		return false;
+	}
+
+	span = last - first;
+	floor = (u64)(n - 1) * (min_interval >> 1);
+	if (span < floor) {
+		dev_dbg_ratelimited(&chip->intf0->dev,
+				    "%s health: %d completions in %llu us spans < %llu us floor -- still trickle\n",
+				    type, n, div_u64(span, NSEC_PER_USEC),
+				    div_u64(floor, NSEC_PER_USEC));
+		return false;
+	}
+
+	if (ktime_get_mono_fast_ns() - last >
+	    JOCKEY3_LIVENESS_WINDOW_NS(READ_ONCE(urb_stream->n_shift))) {
+		dev_dbg_ratelimited(&chip->intf0->dev,
+				    "%s health: cadence plausible but last completion is stale\n",
+				    type);
+		return false;
+	}
+
+	dev_dbg(&chip->intf0->dev,
+		"%s health: streaming -- %d completions, ~%llu us/URB (min %llu us)\n",
+		type, n, div_u64(span, (u64)(n - 1) * NSEC_PER_USEC),
+		div_u64(min_interval, NSEC_PER_USEC));
+	return true;
+}
+
+/**
+ * jockey3_watchdog_check() - one direction's share of a watchdog tick
+ * @chip: driver state
+ * @direction: SNDRV_PCM_STREAM_PLAYBACK or SNDRV_PCM_STREAM_CAPTURE
+ *
+ * Reports a direction that has stopped completing URBs and, on the onset edge,
+ * recovers it -- the only path that catches a stall mid-stream, with no PCM
+ * ioctl to hand it to. Gated as jockey3_pcm_hw_params() gates its own capture
+ * recovery: Playback always recovers because it carries MIDI OUT; Capture only
+ * if a capture stream is open, since the ladder restarts the shared ring and
+ * an idle Capture stall would glitch working Playback for no visible gain. It
+ * is logged and left for the next capture open.
+ *
+ * Logging is edge-triggered, one line per stall start and end. The measured
+ * age is reported rather than the threshold, since the poll interval would
+ * only bound it to one tick.
+ *
+ * Deliberately does not gate on urbs_in_flight: endpoints disabled underneath
+ * the driver leave nothing in flight, which is the case that must not go
+ * unnoticed. The count is reported as evidence -- a full ring means
+ * "submitted, never returned", an empty one "nothing could be submitted".
+ *
+ * The onset tags itself "startup" or "steady-state" after which threshold
+ * caught it: the start grace, or JOCKEY3_WATCHDOG_STALL_MS once that window
+ * has passed. Only "steady-state" is a real mid-stream fault.
+ *
+ * Time-based rather than keyed off the first completion: at low N a restart
+ * can complete an URB within under a millisecond, a hardware FIFO draining
+ * rather than the pipeline catching up, so an early completion must not end
+ * the grace. jockey3_stream_streaming_healthy() applies the same reasoning in
+ * the recovery ladder.
+ */
+static void jockey3_watchdog_check(struct jockey3_chip *chip, const int direction)
+{
+	struct jockey3_pcm_urb_stream *urb_stream = jockey3_get_pcm_urb_stream(chip, direction);
+	const char *type = direction == SNDRV_PCM_STREAM_PLAYBACK ? "Playback" : "Capture";
+	bool log_onset = false, log_recovery = false;
+	u64 now, last, started, age_ns, outage_ns = 0;
+	u64 threshold_ms = JOCKEY3_WATCHDOG_STALL_MS;
+	unsigned int grace_ms = jockey3_start_grace_ms();
+	bool open = false, startup = false;
+
+	scoped_guard(spinlock_irqsave, &urb_stream->lock) {
+		/*
+		 * One flag covers every deliberate stop -- rate change, suspend,
+		 * pre_reset and teardown all reach jockey3_stop_urbs(). Checked
+		 * before now/last/age_ns are sampled, not after: this tick can
+		 * have been sitting on system_long_wq for a while before it got
+		 * to run, and a stop+restart cycle -- which routinely takes on
+		 * the order of 100 ms, far longer than this spinlock is ever
+		 * held -- can complete inside that wait. Sampling first and
+		 * checking stopping second, as this used to, let a tick whose
+		 * data predated the stop act on it once stopping had already
+		 * gone false again, misreporting a rate change's own deliberate
+		 * silence as a stall. See re/rate_change_stall.md for the
+		 * hardware traces this closed.
+		 */
+		if (urb_stream->stopping) {
+			urb_stream->stall_reported = false;
+			return;
+		}
+
+		/*
+		 * Read both candidate baselines before sampling @now, whichever
+		 * one ends up used below: a completion landing on another CPU
+		 * between reading a baseline and reading @now would otherwise let
+		 * "now - last" underflow to a u64 near its max instead of going
+		 * negative. Reading both first guarantees @now is taken no
+		 * earlier than either.
+		 */
+		started = atomic64_read(&urb_stream->urbs_started_time);
+		last = atomic64_read(&urb_stream->last_callback_time);
+		now = ktime_get_mono_fast_ns();
+
+		if (!started)
+			return;		/* never started; nothing to watch yet */
+
+		if (now - started < (u64)grace_ms * NSEC_PER_MSEC) {
+			/*
+			 * Still inside the fixed post-restart grace window --
+			 * measured from urbs_started_time regardless of whether
+			 * last_callback_time has already advanced. See this
+			 * function's kernel-doc for why an early completion is
+			 * not treated as proof the window is over.
+			 */
+			last = started;
+			threshold_ms = grace_ms;
+			startup = true;
+		} else if (!last) {
+			/* Past the grace window and still never completed once */
+			last = started;
+		}
+
+		age_ns = now - last;
+
+		open = urb_stream->substream;
+
+		if (age_ns > threshold_ms * NSEC_PER_MSEC) {
+			if (!urb_stream->stall_reported) {
+				urb_stream->stall_reported = true;
+				urb_stream->stall_since = last;
+				log_onset = true;
+			}
+		} else if (urb_stream->stall_reported) {
+			urb_stream->stall_reported = false;
+			/*
+			 * @last is the completion that ended the outage and
+			 * @stall_since the one before it started, so the
+			 * difference is the true gap rather than a rounding of
+			 * it to the poll interval.
+			 */
+			outage_ns = last - urb_stream->stall_since;
+			log_recovery = true;
+		}
+	}
+
+	if (log_onset) {
+		dev_warn(&chip->intf0->dev,
+			 "%s URB stream stalled: no completion for %llu ms (%d URBs in flight, substream %s, %s)\n",
+			 type, div_u64(age_ns, NSEC_PER_MSEC),
+			 atomic_read(&urb_stream->urbs_in_flight),
+			 open ? "open" : "idle",
+			 startup ? "startup" : "steady-state");
+		if (startup)
+			dev_dbg(&chip->intf0->dev,
+				"%s stall was inside the %s start grace (%u ms)\n", type,
+				READ_ONCE(chip->warm_start) ? "warm" : "cold", grace_ms);
+	} else if (log_recovery) {
+		dev_warn(&chip->intf0->dev, "%s URB stream recovered after %llu ms\n",
+			 type, div_u64(outage_ns, NSEC_PER_MSEC));
+	}
+
+	if (log_onset && (direction == SNDRV_PCM_STREAM_PLAYBACK || open))
+		jockey3_recover_urb_stream(chip, direction, "watchdog", true);
+}
+
+/**
+ * jockey3_watchdog_work() - periodic URB liveness check
+ * @work: the chip's watchdog_work
+ *
+ * Every error path in this driver hangs off a URB completion, so a device that
+ * simply stops completing URBs is invisible to all of them: nothing runs, so
+ * nothing is logged. A playback stream in that state does not even produce an
+ * xrun, because the hardware pointer never advances far enough to overtake the
+ * application. This is the only place that can notice such a silence, which is
+ * why it runs for the device's whole lifetime rather than only while a PCM
+ * stream is open -- the URBs do too, since MIDI OUT rides in every playback
+ * packet and there is no idle state in which "no completions" is legitimate.
+ *
+ * Acts as well as detects: jockey3_watchdog_check() calls
+ * jockey3_recover_urb_stream() directly on a new stall onset. This is the
+ * only place recovery can be triggered without some PCM ioctl (hw_params,
+ * prepare) re-entering the driver first -- necessary because a long-running,
+ * uninterrupted stream never re-enters otherwise, and ALSA core's own
+ * wait_for_avail() (sound/core/pcm_lib.c) times out the open substream with
+ * -EIO on its own schedule regardless of whether this driver ever notices.
+ * jockey3_watchdog_arm()'s cadence tightens while a PCM stream is open, for
+ * exactly this reason.
+ *
+ * Calling into jockey3_recover_urb_stream() from here is safe even though it
+ * calls jockey3_stop_urbs(), which disarms this same work item: the disarm is
+ * the non-sync cancel_delayed_work(), which never blocks and only prevents a
+ * future queueing -- it does not affect the tick that is already running
+ * (this one). The reset jockey3_recover_urb_stream() may queue runs on
+ * system_wq (drivers/usb/core/message.c's usb_queue_reset_device()), not
+ * system_long_wq where this tick runs, so the two cannot serialize behind
+ * each other; and rate_mutex is not held across the wait for that reset
+ * (jockey3_recover_urb_stream() drops it first), which is what lets
+ * jockey3_pre_reset()/jockey3_post_reset() take the mutex themselves and
+ * complete while this tick is blocked waiting.
+ */
+static void jockey3_watchdog_work(struct work_struct *work)
+{
+	struct jockey3_chip *chip = container_of(to_delayed_work(work),
+						 struct jockey3_chip, watchdog_work);
+
+	/*
+	 * Suspend as well as teardown: jockey3_stop_urbs() disarms with the
+	 * non-sync cancel_delayed_work(), which leaves a tick that is already
+	 * running to finish. Without this it would requeue itself and keep
+	 * ticking for the whole suspend, for no purpose -- every check below
+	 * bails out on the suspended flag anyway. Resume re-arms via
+	 * jockey3_start_urbs().
+	 */
+	if (jockey3_is_disconnected(chip) || jockey3_is_suspended(chip))
+		return;
+
+	/*
+	 * A reset stops and restarts the URBs from the USB core's own workqueue.
+	 * Sampling in the middle of that would report a stall that is both
+	 * expected and already being dealt with.
+	 */
+	if (!jockey3_is_resetting(chip)) {
+		jockey3_watchdog_check(chip, SNDRV_PCM_STREAM_PLAYBACK);
+		jockey3_watchdog_check(chip, SNDRV_PCM_STREAM_CAPTURE);
+	}
+
+	jockey3_watchdog_arm(chip);
+}
+
+/*
+ * Poll a stream for up to timeout_ms. With @require_healthy false the bar is
+ * jockey3_check_urb_stream_alive() -- "something completed just now", enough
+ * for the cold-start paths that only ask whether the ring came up. With it
+ * true the bar is jockey3_stream_streaming_healthy(), which a trickle of
+ * FIFO-drain completions cannot clear; used where a false pass would escalate
+ * to a needless USB reset. Always logs a dev_warn on timeout regardless of
+ * whether the caller acts on the result, so stall frequency (Playback and
+ * Capture alike) stays trackable in the field via dmesg.
+ */
+/*
+ * @fresh_start: this wait follows a jockey3_start_urbs() the caller just did, so
+ *	the confirmation latency is reported as the time from that restart to the
+ *	first completion (@urbs_started_time -> @first_callback_time), not from
+ *	when this poll loop happened to begin. The two directions are checked one
+ *	after the other, so measuring from loop entry would credit the whole of
+ *	the first direction's confirmation to the second direction's start time
+ *	and under-report it -- Capture routinely printed "after 0 ms" only
+ *	because Playback had already burned the first several ms. Pass false for a
+ *	bare .prepare() liveness check on an already-running ring, where
+ *	@urbs_started_time is stale and the meaningful number is how long this
+ *	call spent polling.
+ */
+static bool jockey3_wait_urb_stream_started(struct jockey3_chip *chip, const int direction,
+					    const unsigned int timeout_ms, bool require_healthy,
+					    bool fresh_start)
+{
+	struct jockey3_pcm_urb_stream *urb_stream = jockey3_get_pcm_urb_stream(chip, direction);
+	const char *type = direction == SNDRV_PCM_STREAM_PLAYBACK ? "Playback" : "Capture";
+	u64 since_ns = fresh_start ? atomic64_read(&urb_stream->urbs_started_time) : 0;
+	unsigned long start = jiffies;
+	unsigned long deadline = start + msecs_to_jiffies(timeout_ms);
+	u64 first, last;
+
+	dev_dbg(&chip->intf0->dev, "Waiting up to %u ms for %s to %s\n",
+		timeout_ms, type, require_healthy ? "stream steadily" : "show liveness");
+
+	while (time_before(jiffies, deadline)) {
+		/*
+		 * A device that went down mid-wait completes nothing until it is
+		 * restored; spinning out the grace only delays the caller's next
+		 * decision, which in the recovery path is whether to reset.
+		 */
+		if (jockey3_is_disconnected(chip) || jockey3_is_suspended(chip))
+			return false;
+
+		if (require_healthy ? jockey3_stream_streaming_healthy(chip, urb_stream)
+				    : jockey3_check_urb_stream_alive(urb_stream)) {
+			u64 mark;
+			unsigned int elapsed_ms;
+
+			first = atomic64_read(&urb_stream->first_callback_time);
+			if (since_ns && first >= since_ns)
+				mark = first;
+			else if (since_ns)
+				mark = ktime_get_mono_fast_ns();
+			else
+				mark = 0;
+			elapsed_ms = mark ? div_u64(mark - since_ns, NSEC_PER_MSEC)
+					  : jiffies_to_msecs(jiffies - start);
+
+			dev_dbg(&chip->intf0->dev, "%s confirmed %s after %u ms\n", type,
+				require_healthy ? "streaming" : "alive", elapsed_ms);
+			return true;
+		}
+
+		usleep_range(500, 2000);
+	}
+
+	dev_warn(&chip->intf0->dev, "%s URB has stalled.\n", type);
+
+	/* What the grace saw when it gave up: too few completions is silence or
+	 * trickle, many with a tight first->last span is a burst that stopped.
+	 */
+	first = atomic64_read(&urb_stream->first_callback_time);
+	last = atomic64_read(&urb_stream->last_callback_time);
+	dev_dbg(&chip->intf0->dev,
+		"%s stall detail: %d completions since start, first->last %llu us, %d URBs in flight\n",
+		type, atomic_read(&urb_stream->completions_since_start),
+		(first && last > first) ? div_u64(last - first, NSEC_PER_USEC) : 0,
+		atomic_read(&urb_stream->urbs_in_flight));
+	return false;
+}
+
+/**
+ * jockey3_recover_urb_stream() - bring a stalled direction back
+ * @chip: driver state
+ * @direction: SNDRV_PCM_STREAM_PLAYBACK or SNDRV_PCM_STREAM_CAPTURE
+ * @context: short description of what found the stall, for the log
+ * @report_xrun: report an xrun on both directions' open substreams once
+ *	recovery is committed to -- both, since the ring is shared regardless of
+ *	which direction stalled. True only when a running stream just lost
+ *	continuity; false when the caller already expects the discontinuity (a
+ *	rate change) or nothing has flowed yet (.prepare).
+ *
+ * Shared by jockey3_pcm_hw_params()'s post-rate-change check,
+ * jockey3_pcm_prepare()'s liveness check and jockey3_watchdog_check()'s
+ * mid-stream detection. Every call site confirms the stall first, and a
+ * direction found alive at entry returns immediately, so two sequential calls
+ * for one stall are cheap.
+ *
+ * Concurrent calls need more: two can both pass the alive check before either
+ * restarts anything, then run competing stop/start sequences or resets.
+ * @chip->recovery_in_progress closes that -- one ladder at a time, chip-wide.
+ * Without it a watchdog tick and a racing jockey3_pcm_prepare() retry collide
+ * and produce -EPROTO on every endpoint, needing a re-enumeration to clear.
+ *
+ * Ladder: a lightweight URB stop/start, then a full USB reset if that did not
+ * take, queued with usb_queue_reset_device() and awaited by
+ * jockey3_wait_for_reset_completion() rather than calling usb_reset_device()
+ * from ioctl context -- see there for why. The reset step is gated on
+ * jockey3_recovery_budget_take(), so a chip that keeps stalling is not reset
+ * in a tight loop.
+ *
+ * The onset line is ratelimited: jockey3_pcm_prepare() runs on every xrun
+ * recovery, so a client looping on a wedged stream re-enters here several
+ * times a second, and the budget bounds resets, not log lines.
+ *
+ * Return: 0 if the direction is confirmed alive, recovery gave up and logged
+ * why (still non-fatal to the caller), or a concurrent call already had this
+ * in hand; -ENODEV if the device is gone; or -EAGAIN if a reset was queued
+ * but did not complete in time.
+ */
+static int jockey3_recover_urb_stream(struct jockey3_chip *chip, const int direction,
+				      const char *context, bool report_xrun)
+{
+	struct jockey3_pcm_urb_stream *urb_stream = jockey3_get_pcm_urb_stream(chip, direction);
+	const char *type = direction == SNDRV_PCM_STREAM_PLAYBACK ? "Playback" : "Capture";
+	unsigned int grace;
+	int ret = 0;
+
+	if (jockey3_is_disconnected(chip))
+		return -ENODEV;
+
+	if (jockey3_check_urb_stream_alive(urb_stream))
+		return 0;
+
+	if (atomic_cmpxchg(&chip->recovery_in_progress, 0, 1) != 0) {
+		dev_dbg(&chip->intf0->dev,
+			"%s stream stalled (%s); another recovery is already in progress, skipping\n",
+			type, context);
+		return 0;
+	}
+
+	dev_warn_ratelimited(&chip->intf0->dev,
+			     "%s stream stalled (%s); restarting URBs to recover\n",
+			     type, context);
+
+	/*
+	 * Even the light restart below discards in-flight buffer state, so
+	 * sample-accuracy is already broken the moment recovery is committed
+	 * to -- report it now rather than waiting to see how far the ladder
+	 * escalates. Both directions, not just @direction: jockey3_stop_urbs()/
+	 * jockey3_start_urbs() below tear down and resubmit the *shared* ring
+	 * unconditionally, so an open sibling substream loses continuity too,
+	 * even though it was never found stalled itself. Found on the bench: an
+	 * open Capture stream died with its own -EIO from ALSA core's
+	 * wait_for_avail() timeout while a Playback-triggered recovery was still
+	 * in flight, because only Playback had been told. jockey3_report_xrun()
+	 * itself is a no-op if a given substream is not open and running, so
+	 * calling it on both unconditionally is safe.
+	 */
+	if (report_xrun) {
+		jockey3_report_xrun(&chip->playback);
+		jockey3_report_xrun(&chip->capture);
+	}
+
+	scoped_guard(mutex, &chip->rate_mutex) {
+		/*
+		 * Something else may have already fixed this while the call above
+		 * was waiting for rate_mutex -- most plausibly
+		 * jockey3_pcm_hw_params()'s own rate-change restart, which holds
+		 * this same mutex across its whole stop/set-rate/start sequence.
+		 * The alive check above this function's entry is stale by the
+		 * time the lock is held; re-checking here is what makes a call
+		 * that raced a legitimate recovery a no-op instead of a second,
+		 * redundant full URB-ring teardown/rebuild landing right on top
+		 * of one that already succeeded. Without this re-check the
+		 * redundant restart can miss its own liveness budget and escalate
+		 * to a full USB reset that a change which had already recovered on
+		 * its own never needed. See re/rate_change_stall.md.
+		 */
+		if (jockey3_check_urb_stream_alive(urb_stream)) {
+			dev_dbg(&chip->intf0->dev,
+				"%s stream came back under rate_mutex (%s); no restart needed\n",
+				type, context);
+			goto out;
+		}
+
+		/*
+		 * Stale for the same reason the alive check above is repeated: a
+		 * tick that blocked here may have been waiting on
+		 * jockey3_suspend() or jockey3_pre_reset(), and wakes to a
+		 * stream that looks dead only because they zeroed the
+		 * timestamps. Restarting on that would submit URBs to a
+		 * suspended device, then escalate to resetting one.
+		 *
+		 * Bailing loses nothing: both paths restart the ring themselves
+		 * on the way back up.
+		 */
+		if (jockey3_is_disconnected(chip) || jockey3_is_suspended(chip) ||
+		    jockey3_is_resetting(chip)) {
+			dev_dbg(&chip->intf0->dev,
+				"%s stream stalled (%s), but the device is being taken down; leaving the restart to the resume path\n",
+				type, context);
+			goto out;
+		}
+
+		jockey3_stop_urbs(chip);
+		jockey3_start_urbs_failed(chip, jockey3_start_urbs(chip, true), context);
+	}
+
+	/*
+	 * Start grace, and the strict health gate: the light restart above was
+	 * of a ring that was streaming moments earlier, so a real resume shows
+	 * a proper completion cadence quickly, while a trickle of implausibly
+	 * fast FIFO-drain completions must NOT count -- passing on those
+	 * escalates a merely jittery restart to the USB reset below.
+	 */
+	grace = jockey3_start_grace_ms();
+	if (jockey3_wait_urb_stream_started(chip, direction, grace, true, true)) {
+		dev_dbg(&chip->intf0->dev,
+			"%s stream recovered via light URB restart (%s)\n", type, context);
+		goto out;
+	}
+
+	/*
+	 * Re-tested because the restart and grace above take long enough for the
+	 * device to go away underneath them, and escalating then goes badly.
+	 * jockey3_queue_reset() would reinit_completion() over the complete_all()
+	 * that jockey3_disconnect() issues to release waiters, and block the full
+	 * timeout on a reset that will never come. Ahead of the budget, so this
+	 * spends no attempt.
+	 *
+	 * Suspended as well as disconnected, and this is not the same check as
+	 * the one under rate_mutex above. That one covers a tick that arrives
+	 * after jockey3_suspend() has run. A tick which passed it earlier, while
+	 * the device was still live, is legitimately mid-recovery and reaches
+	 * here only after its grace has elapsed -- by which time suspend may have
+	 * happened. Resetting from there was observed to queue a reset the
+	 * suspended device could not complete, leaving the resume path waiting
+	 * out jockey3_wait_for_reset_completion() twice over.
+	 *
+	 * Racy by construction -- either state arriving just after the test still
+	 * hits it. The timeout bounds that; closing it properly would mean
+	 * serializing against jockey3_disconnect(), which must not block on
+	 * driver locks.
+	 */
+	if (jockey3_is_disconnected(chip) || jockey3_is_suspended(chip)) {
+		dev_dbg(&chip->intf0->dev,
+			"%s stream still stalled after URB restart, but the device is down; not resetting (%s)\n",
+			type, context);
+		ret = -ENODEV;
+		goto out;
+	}
+
+	if (!jockey3_recovery_budget_take(chip)) {
+		dev_err(&chip->intf0->dev,
+			"%s stream still stalled after URB restart; recovery budget exhausted, not resetting (%s)\n",
+			type, context);
+		goto out;
+	}
+
+	dev_warn(&chip->intf0->dev,
+		 "%s stream still stalled after URB restart; queuing full USB reset (%s), attempt %d/%d in window\n",
+		 type, context, atomic_read(&chip->recovery_attempts),
+		 JOCKEY3_RECOVERY_MAX_ATTEMPTS);
+	jockey3_queue_reset(chip);
+
+	ret = jockey3_wait_for_reset_completion(chip);
+	if (ret < 0)
+		goto out;
+
+	grace = jockey3_start_grace_ms();
+	if (!jockey3_wait_urb_stream_started(chip, direction, grace, false, true))
+		dev_err(&chip->intf0->dev,
+			"%s stream still stalled after full USB reset; hardware may need power-cycling (%s)\n",
+			type, context);
+	else
+		dev_dbg(&chip->intf0->dev,
+			"%s stream recovered after full USB reset (%s)\n", type, context);
+
+out:
+	atomic_set(&chip->recovery_in_progress, 0);
+	return ret;
+}
+
+/*
+ * SNDRV_PCM_INFO_BATCH: the hardware pointer only advances once per completed
+ * bulk URB (a whole Ploytec frame group), never per sample, so userspace must
+ * not assume a sample-accurate position.
+ *
+ * SNDRV_PCM_INFO_RESUME is deliberately absent: the device loses stream
+ * synchronization across a suspend, so the ALSA core should return -ESTRPIPE
+ * and have userspace re-prepare rather than issue TRIGGER_RESUME.
+ */
+#define JOCKEY3_PCM_INFO	(SNDRV_PCM_INFO_MMAP |		\
+				 SNDRV_PCM_INFO_INTERLEAVED |	\
+				 SNDRV_PCM_INFO_BLOCK_TRANSFER |\
+				 SNDRV_PCM_INFO_BATCH |		\
+				 SNDRV_PCM_INFO_MMAP_VALID)
+
+#define JOCKEY3_PCM_RATES	(SNDRV_PCM_RATE_44100 |	\
+				 SNDRV_PCM_RATE_48000 |	\
+				 SNDRV_PCM_RATE_88200 |	\
+				 SNDRV_PCM_RATE_96000)
+
+static const struct snd_pcm_hardware jockey3_pcm_hw_playback = {
+	.info			= JOCKEY3_PCM_INFO,
+	.formats		= SNDRV_PCM_FMTBIT_S24_3LE,
+	.rates			= JOCKEY3_PCM_RATES,
+	.rate_min		= 44100,
+	.rate_max		= 96000,
+	.channels_min		= 4,
+	.channels_max		= 4,
+	.buffer_bytes_max	= 1024 * 1024,
+	/*
+	 * One playback URB carries a packet count chosen per open by
+	 * jockey3_pcm_hw_params(), of 10 frames * 4 channels * 3 bytes = 120
+	 * bytes each. period_elapsed is OR'd across the packet loop in
+	 * jockey3_playback_callback(), so the minimum must cover at least one
+	 * packet, the smallest a URB can ever carry (N=1), or a period boundary
+	 * inside a URB could be missed entirely.
+	 */
+	.period_bytes_min	= PLOYTEC_PLAYBACK_FRAMES * 4 * 3,
+	.period_bytes_max	= 512 * 1024,
+	.periods_min		= 2,
+	.periods_max		= 1024,
+};
+
+static const struct snd_pcm_hardware jockey3_pcm_hw_capture = {
+	.info			= JOCKEY3_PCM_INFO,
+	.formats		= SNDRV_PCM_FMTBIT_S24_3LE,
+	.rates			= JOCKEY3_PCM_RATES,
+	.rate_min		= 44100,
+	.rate_max		= 96000,
+	.channels_min		= 6,
+	.channels_max		= 6,
+	.buffer_bytes_max	= 1024 * 1024,
+	/*
+	 * One capture URB carries a packet count chosen per open (see
+	 * jockey3_pcm_hw_params()) of up to 8 frames * 6 channels * 3 bytes =
+	 * 144 bytes each; see the playback .period_bytes_min comment above for
+	 * why the minimum must cover at least one packet.
+	 */
+	.period_bytes_min	= PLOYTEC_CAPTURE_FRAMES * 6 * 3,
+	.period_bytes_max	= 512 * 1024,
+	.periods_min		= 2,
+	.periods_max		= 1024,
+};
+
+static int jockey3_pcm_open(struct snd_pcm_substream *substream)
+{
+	struct jockey3_chip *chip = snd_pcm_substream_chip(substream);
+	struct snd_pcm_runtime *runtime = substream->runtime;
+	struct jockey3_pcm_urb_stream *urb_stream =
+		jockey3_get_pcm_urb_stream(chip, substream->stream);
+	int ret;
+
+	dev_dbg(&chip->intf0->dev, "PCM open stream %d\n", substream->stream);
+
+	if (jockey3_is_disconnected(chip))
+		return -ENODEV;
+
+	runtime->hw = substream->stream == SNDRV_PCM_STREAM_PLAYBACK ?
+		      jockey3_pcm_hw_playback : jockey3_pcm_hw_capture;
+
+	/* The period accounting assumes a whole number of periods per buffer */
+	ret = snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS);
+	if (ret < 0)
+		return ret;
+
+	/*
+	 * Rate constraints under rate_mutex, which also excludes a concurrent
+	 * rate change in jockey3_pcm_hw_params(). Re-check for disconnect here:
+	 * the check above raced with anything that happened while we were not
+	 * holding the mutex.
+	 */
+	scoped_guard(mutex, &chip->rate_mutex) {
+		if (jockey3_is_disconnected(chip))
+			return -ENODEV;
+
+		if (jockey3_rate_committed_streams(chip) > 0) {
+			/* Force the new stream to match the existing hardware rate */
+			ret = snd_pcm_hw_constraint_single(runtime,
+							   SNDRV_PCM_HW_PARAM_RATE,
+							   chip->current_rate);
+			if (ret < 0)
+				return ret;
+		}
+	}
+
+	/* Substream registration under spinlock to ensure memory consistency to the ISR*/
+	scoped_guard(spinlock_irqsave, &urb_stream->lock) {
+		urb_stream->substream = substream;
+	}
+
+	return 0;
+}
+
+static int jockey3_pcm_close(struct snd_pcm_substream *substream)
+{
+	struct jockey3_chip *chip = snd_pcm_substream_chip(substream);
+	struct jockey3_pcm_urb_stream *urb_stream =
+		jockey3_get_pcm_urb_stream(chip, substream->stream);
+	unsigned int default_n = substream->stream == SNDRV_PCM_STREAM_PLAYBACK ?
+				 JOCKEY3_PLAYBACK_N : JOCKEY3_CAPTURE_N;
+
+	dev_dbg(&chip->intf0->dev, "PCM close stream %d\n", substream->stream);
+
+	/*
+	 * No drain is needed here: the ALSA core has already run .sync_stop via
+	 * snd_pcm_release_substream() -> snd_pcm_drop() -> do_hw_free(), which is
+	 * also where runtime->dma_area is released.
+	 */
+	scoped_guard(spinlock_irqsave, &urb_stream->lock) {
+		urb_stream->substream = NULL;
+		urb_stream->running = false;
+		urb_stream->rate_committed = false;
+
+		/*
+		 * A closed direction is not covered by jockey3_pcm_hw_params()
+		 * any more, so its N would otherwise sit at whatever the last
+		 * open left it at -- possibly a small, period-tuned value that
+		 * has nothing to do with the next stream to open it, or with
+		 * this direction's own idle ring being re-armed by a rate
+		 * change on the *other* direction (jockey3_start_urbs() re-arms
+		 * both unconditionally). Reset to the default here so an idle
+		 * direction always re-arms at a safe N.
+		 */
+		WRITE_ONCE(urb_stream->n_shift, ilog2(default_n));
+		WRITE_ONCE(urb_stream->n_pkts, default_n);
+	}
+
+	return 0;
+}
+
+/**
+ * jockey3_pcm_sync_stop() - wait for in-flight URB callbacks to drain
+ * @substream: the substream being stopped
+ *
+ * Waits for every URB completion that could still touch this stream's substream
+ * or runtime->dma_area to leave its safe zone.
+ *
+ * The ALSA core calls this from snd_pcm_do_prepare() and from do_hw_free()
+ * *before* snd_pcm_lib_free_pages() releases the buffer, and only when the
+ * stream actually ran (runtime->stop_operating). It runs in process context
+ * with no stream lock held, so sleeping here is safe: a callback in its safe
+ * zone has dropped urb_stream->lock and may freely take the stream lock, so the
+ * stream lock -> urb_stream->lock order is never inverted.
+ *
+ * Return: 0 always; a timeout is logged but cannot usefully be reported.
+ */
+static int jockey3_pcm_sync_stop(struct snd_pcm_substream *substream)
+{
+	struct jockey3_chip *chip = snd_pcm_substream_chip(substream);
+	struct jockey3_pcm_urb_stream *urb_stream =
+		jockey3_get_pcm_urb_stream(chip, substream->stream);
+	long remaining;
+
+	spin_lock_irq(&urb_stream->lock);
+	remaining = wait_event_lock_irq_timeout(urb_stream->drain_wait,
+						urb_stream->callbacks_active == 0,
+						urb_stream->lock,
+						msecs_to_jiffies(1000));
+	spin_unlock_irq(&urb_stream->lock);
+
+	if (!remaining)
+		dev_err(&chip->intf0->dev,
+			"Timeout draining %s URB callbacks\n",
+			substream->stream == SNDRV_PCM_STREAM_PLAYBACK ? "playback" : "capture");
+
+	return 0;
+}
+
+static int jockey3_pcm_prepare(struct snd_pcm_substream *substream)
+{
+	struct jockey3_chip *chip = snd_pcm_substream_chip(substream);
+	struct jockey3_pcm_urb_stream *urb_stream =
+		jockey3_get_pcm_urb_stream(chip, substream->stream);
+	bool stalled = false;
+	int ret = 0;
+
+	dev_dbg(&chip->intf0->dev, "PCM prepare stream %d\n", substream->stream);
+	if (jockey3_is_disconnected(chip))
+		return -ENODEV;
+
+	ret = jockey3_wait_for_reset_completion(chip);
+	if (ret < 0)
+		return ret;
+
+	/*
+	 * Taking rate_mutex here serializes against an in-flight rate change in
+	 * jockey3_pcm_hw_params(), which holds it across the whole stop/set/start
+	 * sequence -- so the liveness below is sampled from a settled state
+	 * rather than from the middle of a URB restart.
+	 */
+	scoped_guard(mutex, &chip->rate_mutex) {
+		if (jockey3_is_disconnected(chip))
+			return -ENODEV;
+
+		/*
+		 * Either direction may have been left stalled by an earlier rate
+		 * change that happened while this stream was not open (see
+		 * jockey3_pcm_hw_params()). Catch it here, before the stream that
+		 * is being prepared starts relying on it.
+		 *
+		 * This single sample is only a hint. jockey3_check_urb_stream_alive()'s
+		 * window is scaled to always span a handful of packet intervals
+		 * regardless of N (JOCKEY3_LIVENESS_WINDOW_NS()), but a handful is
+		 * still not many -- one preemption is enough to make a healthy
+		 * stream read as dead -- and .prepare runs on every xrun recovery,
+		 * where acting on a false positive would disrupt a working stream.
+		 * What it flags is confirmed below before anything is done about it.
+		 */
+		stalled = !jockey3_check_urb_stream_alive(urb_stream);
+	}
+
+	/*
+	 * Confirm, and recover, outside rate_mutex on purpose: polling would
+	 * otherwise hold the mutex for a whole start grace, and recovery may
+	 * escalate to a queued USB reset, whose jockey3_pre_reset() and
+	 * jockey3_post_reset() need to acquire the mutex themselves to complete.
+	 * Start grace and the plain alive check: this is a stream being
+	 * (re)opened, not a warm restart of one that was just running.
+	 */
+	if (stalled)
+		stalled = !jockey3_wait_urb_stream_started(chip, substream->stream,
+							  jockey3_start_grace_ms(), false,
+							  false);
+
+	if (stalled) {
+		const char *context = substream->stream == SNDRV_PCM_STREAM_CAPTURE ?
+			"opening a capture stream" : "preparing a playback stream";
+
+		ret = jockey3_recover_urb_stream(chip, substream->stream, context, false);
+		if (ret < 0)
+			return ret;
+	}
+
+	scoped_guard(spinlock_irqsave, &urb_stream->lock) {
+		urb_stream->dma_off = 0;
+		urb_stream->period_off = 0;
+	}
+	return 0;
+}
+
+/*
+ * Called by the ALSA core with the substream stream lock held and interrupts
+ * disabled, so this callback must never sleep. In particular it must not wait
+ * for a rate change or a device reset: a URB completion can re-enter here via
+ * snd_pcm_period_elapsed() -> snd_pcm_stop_xrun(), and blocking would then
+ * stall the very rate change it is waiting on.
+ *
+ * Serialization against a concurrent rate change is provided by rate_mutex in
+ * the sleepable callbacks instead; the ALSA state machine guarantees .prepare
+ * runs before TRIGGER_START and after every XRUN.
+ *
+ * Lock order here is snd_pcm_stream_lock -> urb_stream->lock, which is why the
+ * URB callbacks drop urb_stream->lock before calling snd_pcm_period_elapsed().
+ */
+static int jockey3_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
+{
+	struct jockey3_chip *chip = snd_pcm_substream_chip(substream);
+	struct jockey3_pcm_urb_stream *urb_stream =
+		jockey3_get_pcm_urb_stream(chip, substream->stream);
+
+	dev_dbg(&chip->intf0->dev, "PCM trigger stream %d, cmd %d\n", substream->stream, cmd);
+
+	if (jockey3_is_disconnected(chip))
+		return -ENODEV;
+
+	scoped_guard(spinlock_irqsave, &urb_stream->lock) {
+		switch (cmd) {
+		case SNDRV_PCM_TRIGGER_START:
+			urb_stream->running = true;
+			break;
+		case SNDRV_PCM_TRIGGER_STOP:
+		case SNDRV_PCM_TRIGGER_SUSPEND:
+			urb_stream->running = false;
+			break;
+		default:
+			return -EINVAL;
+		}
+	}
+	return 0;
+}
+
+static snd_pcm_uframes_t jockey3_pcm_pointer(struct snd_pcm_substream *substream)
+{
+	struct jockey3_chip *chip = snd_pcm_substream_chip(substream);
+	struct jockey3_pcm_urb_stream *urb_stream =
+		jockey3_get_pcm_urb_stream(chip, substream->stream);
+	unsigned int dma_off;
+
+	scoped_guard(spinlock_irqsave, &urb_stream->lock) {
+		dma_off = urb_stream->dma_off;
+	}
+	return bytes_to_frames(substream->runtime, dma_off);
+}
+
+static int jockey3_initialize_ploytec(struct jockey3_chip *chip, u32 *fw_version)
+{
+	int ret;
+
+	if (jockey3_is_disconnected(chip))
+		return -ENODEV;
+
+	ret = ploytec_initialize_device(chip->intf0, chip->xfer_buf, false, fw_version);
+	if (ret < 0) {
+		dev_err(&chip->intf0->dev, "Ploytec failed to initialize: %d\n", ret);
+		return ret;
+	}
+
+	ret = ploytec_start_streaming(chip->intf0, chip->xfer_buf);
+	if (ret < 0) {
+		dev_err(&chip->intf0->dev, "Ploytec failed to start streaming: %d\n", ret);
+		return ret;
+	}
+
+	dev_dbg(&chip->intf0->dev, "Ploytec initialized successfully\n");
+	return 0;
+}
+
+/**
+ * jockey3_pcm_set_n() - choose and arm this direction's packets per URB
+ * @chip: driver state
+ * @substream: the substream hw_params was just called for
+ * @hw_params: the negotiated parameters
+ *
+ * Chooses how many Ploytec packets each USB bulk URB on this direction
+ * carries, called "N" below for brevity: the largest power of two with
+ * N * pkt_bytes <= period_bytes. A big period gets the full coalescing
+ * saving, a tiny one falls back to N=1 automatically via .period_bytes_min.
+ * Takes effect without tearing the URB ring down -- the firmware cannot tell
+ * an N x 512 B bulk transfer apart from N separate 512 B ones, so each URB on
+ * this direction simply picks up the new N at its own next resubmission; the
+ * ring turns over onto it within at most JOCKEY3_N_URBS completions. See
+ * re/streaming_overhead.md for the rationale and measurements.
+ *
+ * Called unconditionally on every hw_params(), including when the rate is
+ * unchanged and the URB ring is never touched: N is a per-open, per-period
+ * property, independent of whether a rate change happens to be in progress.
+ */
+static void jockey3_pcm_set_n(struct jockey3_chip *chip, struct snd_pcm_substream *substream,
+			      struct snd_pcm_hw_params *hw_params)
+{
+	struct jockey3_pcm_urb_stream *urb_stream =
+		jockey3_get_pcm_urb_stream(chip, substream->stream);
+	unsigned int period_bytes = params_period_bytes(hw_params);
+	bool is_playback = substream->stream == SNDRV_PCM_STREAM_PLAYBACK;
+	unsigned int pkt_bytes = is_playback ? PLOYTEC_PLAYBACK_FRAMES * 4 * 3 :
+						  PLOYTEC_CAPTURE_FRAMES * 6 * 3;
+	unsigned int max_n = is_playback ? JOCKEY3_PLAYBACK_N : JOCKEY3_CAPTURE_N;
+	unsigned int n = max(period_bytes / pkt_bytes, 1U);
+	u8 n_shift = clamp_t(u8, ilog2(n), 0, ilog2(max_n));
+
+	scoped_guard(spinlock_irqsave, &urb_stream->lock) {
+		WRITE_ONCE(urb_stream->n_shift, n_shift);
+		WRITE_ONCE(urb_stream->n_pkts, 1 << n_shift);
+	}
+
+	dev_dbg(&chip->intf0->dev, "hw_params: %s using %u packet(s)/URB (period_bytes=%u)\n",
+		is_playback ? "playback" : "capture", 1U << n_shift, period_bytes);
+}
+
+/**
+ * jockey3_pcm_change_rate() - reprogram the hardware rate for hw_params()
+ * @chip: driver state
+ * @substream: the substream asking for the rate
+ * @rate: the requested rate in Hz
+ * @changed: set true if the hardware was actually reprogrammed
+ *
+ * Split out of jockey3_pcm_hw_params() because nesting two scoped_guard()
+ * blocks in one scope shadows the guard's own variable and trips -Wshadow.
+ * Call with the USB device lock held; see the call site for why.
+ *
+ * Return: 0 on success or if the rate already matched (@changed stays false),
+ * %-EBUSY if the other direction holds a different rate, %-ENODEV if the device
+ * is gone, or the error from programming the rate.
+ */
+static int jockey3_pcm_change_rate(struct jockey3_chip *chip,
+				   struct snd_pcm_substream *substream,
+				   unsigned int rate, bool *changed)
+{
+	int ret;
+
+	/*
+	 * rate_mutex is held across the whole stop/set-rate/start sequence, which
+	 * is what excludes a concurrent rate change from another substream: any
+	 * other sleepable path that needs a settled rate takes the same mutex.
+	 */
+	guard(mutex)(&chip->rate_mutex);
+
+	if (jockey3_is_disconnected(chip))
+		return -ENODEV;
+
+	/*
+	 * Drop this direction's own claim on the rate before counting:
+	 * an application is free to hw_free and run hw_params again at
+	 * a different rate, and its previous commitment must not be
+	 * what blocks it. Cleared inside rate_mutex, so the other
+	 * direction cannot observe the gap and reprogram the hardware.
+	 */
+	jockey3_set_rate_committed(chip, substream->stream, false);
+
+	if (chip->current_rate == rate) {
+		dev_dbg(&chip->intf0->dev, "Rate already set to %u, skipping change\n",
+			rate);
+		jockey3_set_rate_committed(chip, substream->stream, true);
+		return 0;
+	}
+
+	*changed = true;
+
+	/*
+	 * The other direction already holds the rate. The ALSA core
+	 * should have enforced the constraint from jockey3_pcm_open(),
+	 * so this is a backstop -- but it is also the only check that
+	 * covers a stream which opened while the device was idle and
+	 * only now asks for a rate the other direction has since fixed.
+	 */
+	if (jockey3_rate_committed_streams(chip) > 0) {
+		dev_err(&chip->intf0->dev,
+			"Cannot change rate to %u while the other stream holds %u\n",
+			rate, chip->current_rate);
+		return -EBUSY;
+	}
+
+	jockey3_stop_urbs(chip);
+
+	ret = jockey3_set_rate(chip, rate, false);
+	if (ret != 0) {
+		if (jockey3_err_device_gone(ret))
+			dev_dbg(&chip->intf0->dev,
+				"Rate change to %u abandoned; device is gone (%d)\n",
+				rate, ret);
+		else
+			dev_err(&chip->intf0->dev,
+				"Rate change to %u failed: %d\n", rate, ret);
+		/*
+		 * The rate change is what left the endpoints disabled if
+		 * they are, so this restart is the one most likely to
+		 * come back -ENOENT. Report it before returning the rate
+		 * error, which would otherwise be the only thing seen.
+		 */
+		jockey3_start_urbs_failed(chip, jockey3_start_urbs(chip, false),
+					  "a failed rate change");
+		return ret;
+	}
+
+	jockey3_set_current_rate(chip, rate);
+	jockey3_set_rate_committed(chip, substream->stream, true);
+
+	jockey3_start_urbs_failed(chip, jockey3_start_urbs(chip, false), "a rate change");
+
+	return 0;
+}
+
+static int jockey3_pcm_hw_params(struct snd_pcm_substream *substream,
+				 struct snd_pcm_hw_params *hw_params)
+{
+	struct jockey3_chip *chip = snd_pcm_substream_chip(substream);
+	unsigned int rate = params_rate(hw_params);
+	bool playback_alive, capture_alive, capture_open;
+	bool rate_changed = false;
+	unsigned int grace;
+	int ret = 0;
+
+	dev_dbg(&chip->intf0->dev, "PCM hw_params rate %u, rate_committed_streams %d\n",
+		rate, jockey3_rate_committed_streams(chip));
+
+	if (jockey3_is_disconnected(chip))
+		return -ENODEV;
+
+	/*
+	 * A previous call may have left a stall-recovery reset in flight (see
+	 * jockey3_recover_urb_stream(), called below). Without this, a rate
+	 * change arriving before that reset completes runs jockey3_set_rate()
+	 * and the URB restart against a device that is mid-reset, which fails
+	 * outright instead of recovering. jockey3_pcm_prepare() waits for the
+	 * same reason.
+	 */
+	ret = jockey3_wait_for_reset_completion(chip);
+	if (ret < 0)
+		return ret;
+
+	/* Independent of rate and of whether it changes; see jockey3_pcm_set_n() */
+	jockey3_pcm_set_n(chip, substream, hw_params);
+
+	/*
+	 * Programming a rate calls usb_set_interface(), which the USB core
+	 * serializes against usb_disconnect() with the device lock and nothing
+	 * else; both reach remove_intf_ep_devs(), whose only guard is the
+	 * unlocked bitfield intf->ep_devs_created.
+	 *
+	 * Three constraints fix where it goes. It cannot move down into
+	 * ploytec_initialize_device(): probe, disconnect and the reset callbacks
+	 * already hold it and it is not recursive. It must be dropped before the
+	 * recovery below, which can queue a reset that needs this same lock. And
+	 * it goes outside rate_mutex, the order pre_reset()/post_reset() use.
+	 *
+	 * The watchdog must never take it: jockey3_disconnect() holds it while
+	 * waiting in cancel_delayed_work_sync().
+	 */
+	scoped_guard(device, &chip->dev->dev)
+		ret = jockey3_pcm_change_rate(chip, substream, rate, &rate_changed);
+	if (ret < 0)
+		return ret;
+	if (!rate_changed)
+		return 0;
+
+	/*
+	 * The firmware does not always resume streaming after a rate change, so
+	 * check liveness on both directions and let
+	 * jockey3_recover_urb_stream() force a re-synchronization.
+	 *
+	 * Playback recovers unconditionally -- it carries MIDI OUT, so a stall
+	 * breaks MIDI control. Capture only if a stream is open: an idle
+	 * Capture stall is logged and deferred to the next open, since
+	 * recovering it would glitch working Playback audio.
+	 *
+	 * Outside rate_mutex, because an escalated reset needs pre_reset() and
+	 * post_reset() to take it. Playback goes first: if both died, its
+	 * restart of the shared ring makes the capture call a no-op.
+	 *
+	 * Start grace -- a rate change reprograms the endpoints over EP0.
+	 */
+	grace = jockey3_start_grace_ms();
+	playback_alive = jockey3_wait_urb_stream_started(chip, SNDRV_PCM_STREAM_PLAYBACK,
+							 grace, false, true);
+	capture_alive = jockey3_wait_urb_stream_started(chip, SNDRV_PCM_STREAM_CAPTURE,
+							grace, false, true);
+	capture_open = jockey3_stream_is_open(chip, SNDRV_PCM_STREAM_CAPTURE);
+
+	if (!playback_alive || (!capture_alive && capture_open)) {
+		dev_warn(&chip->intf0->dev,
+			 "Rate change to %u Hz left a stream stalled (playback_alive=%d, capture_alive=%d, capture_open=%d); attempting recovery\n",
+			 rate, playback_alive, capture_alive, capture_open);
+
+		if (!playback_alive) {
+			ret = jockey3_recover_urb_stream(chip, SNDRV_PCM_STREAM_PLAYBACK,
+							 "rate change", false);
+			if (ret < 0)
+				return ret;
+		}
+
+		if (!capture_alive && capture_open) {
+			ret = jockey3_recover_urb_stream(chip, SNDRV_PCM_STREAM_CAPTURE,
+							 "rate change", false);
+			if (ret < 0)
+				return ret;
+		}
+	} else {
+		if (!capture_alive)
+			dev_dbg(&chip->intf0->dev,
+				"Capture URB stalled after rate change to %u Hz, but no capture stream is open; deferring recovery to next capture open\n",
+				rate);
+		dev_dbg(&chip->intf0->dev, "Rate changed to %u successfully\n", rate);
+	}
+
+	return 0;
+}
+
+/*
+ * Release this direction's claim on the hardware rate. The URB ring is
+ * deliberately left running (see the top-of-file DOC); all this hands back is
+ * the right of the other direction -- or of this one, on a re-run of
+ * hw_params -- to program a different rate.
+ */
+static int jockey3_pcm_hw_free(struct snd_pcm_substream *substream)
+{
+	struct jockey3_chip *chip = snd_pcm_substream_chip(substream);
+
+	jockey3_set_rate_committed(chip, substream->stream, false);
+	return 0;
+}
+
+static const struct snd_pcm_ops jockey3_pcm_ops = {
+	.open = jockey3_pcm_open,
+	.close = jockey3_pcm_close,
+	.hw_params = jockey3_pcm_hw_params,
+	.hw_free = jockey3_pcm_hw_free,
+	.prepare = jockey3_pcm_prepare,
+	.trigger = jockey3_pcm_trigger,
+	.sync_stop = jockey3_pcm_sync_stop,
+	.pointer = jockey3_pcm_pointer,
+};
+
+static int jockey3_midi_in_open(struct snd_rawmidi_substream *substream)
+{
+	struct jockey3_chip *chip = substream->rmidi->private_data;
+
+	if (jockey3_is_disconnected(chip))
+		return -ENODEV;
+	return 0;
+}
+
+/*
+ * Normally trigger(0) has already cleared the pointer, but the rawmidi core can
+ * reach close without it (an interrupted drain, or close_substream() with
+ * cleanup suppressed), so drop it here too rather than leave a dangling
+ * reference for the URB callback.
+ */
+static int jockey3_midi_in_close(struct snd_rawmidi_substream *substream)
+{
+	struct jockey3_chip *chip = substream->rmidi->private_data;
+
+	guard(spinlock_irqsave)(&chip->midi_lock);
+	if (chip->midi_in_substream == substream)
+		chip->midi_in_substream = NULL;
+
+	return 0;
+}
+
+static void jockey3_midi_in_trigger(struct snd_rawmidi_substream *substream, int up)
+{
+	struct jockey3_chip *chip = substream->rmidi->private_data;
+
+	guard(spinlock_irqsave)(&chip->midi_lock);
+	chip->midi_in_substream = up ? substream : NULL;
+}
+
+static int jockey3_midi_out_open(struct snd_rawmidi_substream *substream)
+{
+	struct jockey3_chip *chip = substream->rmidi->private_data;
+
+	if (jockey3_is_disconnected(chip))
+		return -ENODEV;
+	return 0;
+}
+
+/* See jockey3_midi_in_close() for why the pointer is cleared here as well. */
+static int jockey3_midi_out_close(struct snd_rawmidi_substream *substream)
+{
+	struct jockey3_chip *chip = substream->rmidi->private_data;
+
+	guard(spinlock_irqsave)(&chip->midi_lock);
+	if (chip->midi_out_substream == substream)
+		chip->midi_out_substream = NULL;
+
+	return 0;
+}
+
+static void jockey3_midi_out_trigger(struct snd_rawmidi_substream *substream, int up)
+{
+	struct jockey3_chip *chip = substream->rmidi->private_data;
+
+	guard(spinlock_irqsave)(&chip->midi_lock);
+	chip->midi_out_substream = up ? substream : NULL;
+}
+
+static const struct snd_rawmidi_ops jockey3_midi_in_ops = {
+	.open = jockey3_midi_in_open,
+	.close = jockey3_midi_in_close,
+	.trigger = jockey3_midi_in_trigger
+};
+
+static const struct snd_rawmidi_ops jockey3_midi_out_ops = {
+	.open = jockey3_midi_out_open,
+	.close = jockey3_midi_out_close,
+	.trigger = jockey3_midi_out_trigger
+};
+
+static int jockey3_initialize(struct jockey3_chip *chip, int model)
+{
+	int ret;
+	int rate;
+	u32 fw_version;
+
+	/*
+	 * Let the device finish booting before speaking to it.
+	 *
+	 * After a mains power cycle the Jockey 3 enumerates and answers control
+	 * transfers while its audio engine is still coming up. Initialize it in
+	 * that window and the engine never starts: every control transfer
+	 * succeeds, the rate reads back correctly, ALSA accepts playback, and the
+	 * device is silent -- capture returns bit-exact zero and nothing is
+	 * logged. A USB re-enumeration does not provoke it; only a real
+	 * power-off does, because the device is self-powered and a VBUS cut is
+	 * merely a cable unplug to it.
+	 *
+	 * The threshold is sharp: the device needs between 144 and 156 ms after
+	 * enumeration, below which the engine fails to start every time. 250 ms
+	 * rather than the smallest passing value, because the time this driver
+	 * otherwise takes to reach its first transfer is host-dependent, so a
+	 * value that only topped that up would erode on a faster machine.
+	 *
+	 * Placed here rather than in ploytec_initialize_device() because that
+	 * runs twice per probe -- once below, once via jockey3_set_rate() -- and
+	 * again on every rate change. This point runs once, and is the last
+	 * before the driver's first EP0 transfer; everything above it in probe()
+	 * is ALSA and USB core bookkeeping.
+	 *
+	 * See re/usb/init_timing_comparison.md for the measurements.
+	 */
+	msleep(250);
+
+	for (int retry = 10; retry > 0; retry--) {
+		ret = jockey3_initialize_ploytec(chip, &fw_version);
+		if (ret == 0)
+			break;
+		usleep_range(50000, 100000); /* Wait 50-100 ms before retrying */
+	}
+	if (ret < 0) {
+		dev_err(&chip->intf0->dev, "Failed to initialize Ploytec: %d\n", ret);
+		return ret;
+	}
+
+	// see ploytec_get_firmware() for the packing of buf[0..2] into fw_version
+	dev_info(&chip->intf0->dev, CARD_NAME " %s Firmware 0x%02x v%d.%d.%d\n",
+		 jockey3_model_name(model),
+		 (fw_version >> 16) & 0xFF, (fw_version >> 8) & 0xFF,
+		 (fw_version >> 4) & 0x0F, fw_version & 0x0F);
+
+	rate = 44100;	// default sample rate at power-on
+	scoped_guard(mutex, &chip->rate_mutex)
+		jockey3_set_current_rate(chip, rate);
+
+	ret = jockey3_set_rate(chip, rate, true);
+	if (ret < 0)
+		return ret;
+
+	/*
+	 * Fail the probe rather than escalating: a device that cannot accept its
+	 * URBs has no working audio and no MIDI OUT, and queuing a reset against
+	 * one that never started is worse than reporting a clean failure here.
+	 */
+	ret = jockey3_start_urbs(chip, false);
+	if (ret < 0) {
+		dev_err(&chip->intf0->dev, "Failed to start URBs during initialization: %d\n",
+			ret);
+		return ret;
+	}
+
+	dev_dbg(&chip->intf0->dev, "Initialization complete.\n");
+
+	return 0;
+}
+
+/*
+ * Free everything jockey3_probe() allocated into @chip, and give the card slot
+ * back. NULL-safe throughout, so it is correct at any point of a half-built
+ * probe as well as at the end of the device's life.
+ *
+ * Two rules govern what may go in here, both stemming from where it runs:
+ * card->private_free, i.e. after the last file descriptor on the card is
+ * closed, which on an unplug is arbitrarily long after jockey3_disconnect().
+ *
+ *  - No USB *operation* may be attempted. The objects are still allocated,
+ *    since jockey3_probe() references each and this function drops them, but
+ *    they are unbound by now and the hardware may be gone. Reading is safe,
+ *    talking is not. The dev_dbg(&chip->intf0->dev, ...) idiom is avoided too:
+ *    the interface is no longer ours, so the attribution would be wrong.
+ *  - The URBs must already be dead. jockey3_disconnect() kills them, and so
+ *    does probe's error path, both before the card is released.
+ */
+static void jockey3_free_resources(struct jockey3_chip *chip)
+{
+	int i;
+
+	/*
+	 * The last fence on the watchdog. jockey3_disconnect()'s sync cancel is
+	 * not enough on its own: a jockey3_start_urbs() already past its
+	 * DISCONNECTED gate arms the watchdog at its tail, under no mutex that
+	 * would exclude the cancel. Here that cannot happen -- private_free runs
+	 * once the card is closed, so every such caller has returned.
+	 */
+	cancel_delayed_work_sync(&chip->watchdog_work);
+
+	for (i = 0; i < JOCKEY3_N_URBS; i++) {
+		usb_free_urb(chip->playback.urbs[i]);
+		kfree(chip->playback.bufs[i]);
+		usb_free_urb(chip->capture.urbs[i]);
+		kfree(chip->capture.bufs[i]);
+	}
+	usb_free_urb(chip->midi_in_urb);
+	kfree(chip->midi_in_buf);
+	kfree(chip->xfer_buf);
+
+	mutex_destroy(&chip->rate_mutex);
+
+	scoped_guard(mutex, &jockey3_devices_mutex)
+		__clear_bit(chip->dev_idx, jockey3_devices_used);
+
+	/* Last, reversing jockey3_probe(): everything above may read these. */
+	usb_put_intf(chip->intf1);
+	usb_put_intf(chip->intf0);
+	usb_put_dev(chip->dev);
+}
+
+static void jockey3_card_free(struct snd_card *card)
+{
+	jockey3_free_resources(card->private_data);
+}
+
+static int jockey3_init_midi_urb(struct jockey3_chip *chip)
+{
+	struct usb_device *dev = chip->dev;
+
+	memset(&chip->midi_state, 0, sizeof(chip->midi_state));
+	chip->midi_out_acc = 0;
+
+	chip->midi_in_buf = kmalloc(PLOYTEC_PKT_SIZE, GFP_KERNEL);
+	if (!chip->midi_in_buf)
+		return -ENOMEM;
+
+	chip->midi_in_urb = usb_alloc_urb(0, GFP_KERNEL);
+	if (!chip->midi_in_urb)
+		return -ENOMEM;
+
+	usb_fill_bulk_urb(chip->midi_in_urb, dev,
+			  usb_rcvbulkpipe(dev, PLOYTEC_EP_NUM_MIDI_IN),
+			  chip->midi_in_buf, PLOYTEC_PKT_SIZE,
+			  jockey3_midi_in_callback, chip);
+
+	return 0;
+}
+
+static int jockey3_init_playback_urbs(struct jockey3_chip *chip)
+{
+	struct usb_device *dev = chip->dev;
+	int i;
+
+	for (i = 0; i < JOCKEY3_N_URBS; i++) {
+		chip->playback.bufs[i] = kzalloc(JOCKEY3_PLAYBACK_XFER_SIZE, GFP_KERNEL);
+		if (!chip->playback.bufs[i])
+			return -ENOMEM;
+
+		chip->playback.urbs[i] = usb_alloc_urb(0, GFP_KERNEL);
+		if (!chip->playback.urbs[i])
+			return -ENOMEM;
+
+		jockey3_init_out_packet(chip->playback.bufs[i]);
+
+		usb_fill_bulk_urb(chip->playback.urbs[i], dev,
+				  usb_sndbulkpipe(dev, PLOYTEC_EP_NUM_PCM_OUT),
+				  chip->playback.bufs[i], JOCKEY3_PLAYBACK_XFER_SIZE,
+				  jockey3_playback_callback, chip);
+	}
+
+	return 0;
+}
+
+static int jockey3_init_capture_urbs(struct jockey3_chip *chip)
+{
+	struct usb_device *dev = chip->dev;
+	int i;
+
+	for (i = 0; i < JOCKEY3_N_URBS; i++) {
+		chip->capture.bufs[i] = kzalloc(JOCKEY3_CAPTURE_XFER_SIZE, GFP_KERNEL);
+		if (!chip->capture.bufs[i])
+			return -ENOMEM;
+
+		chip->capture.urbs[i] = usb_alloc_urb(0, GFP_KERNEL);
+		if (!chip->capture.urbs[i])
+			return -ENOMEM;
+
+		usb_fill_bulk_urb(chip->capture.urbs[i], dev,
+				  usb_rcvbulkpipe(dev, PLOYTEC_EP_NUM_PCM_IN),
+				  chip->capture.bufs[i], JOCKEY3_CAPTURE_XFER_SIZE,
+				  jockey3_capture_callback, chip);
+	}
+
+	return 0;
+}
+
+static bool jockey3_has_bulk_endpoint(struct usb_interface *intf, u8 ep_num, bool out)
+{
+	int i, j;
+
+	for (i = 0; i < intf->num_altsetting; i++) {
+		struct usb_host_interface *alts = &intf->altsetting[i];
+
+		for (j = 0; j < alts->desc.bNumEndpoints; j++) {
+			struct usb_endpoint_descriptor *epd = &alts->endpoint[j].desc;
+
+			if (out) {
+				if (usb_endpoint_is_bulk_out(epd) &&
+				    usb_endpoint_num(epd) == ep_num)
+					return true;
+			} else {
+				if (usb_endpoint_is_bulk_in(epd) &&
+				    usb_endpoint_num(epd) == ep_num)
+					return true;
+			}
+		}
+	}
+	return false;
+}
+
+static int jockey3_validate_endpoints(struct usb_interface *intf0, struct usb_interface *intf1)
+{
+	if (!jockey3_has_bulk_endpoint(intf0, PLOYTEC_EP_NUM_PCM_OUT, true) ||
+	    !jockey3_has_bulk_endpoint(intf0, PLOYTEC_EP_NUM_MIDI_IN, false)) {
+		dev_err(&intf0->dev, "Required bulk endpoints not found on Interface 0 (OUT: 0x%02x, IN: 0x%02x)\n",
+			PLOYTEC_EP_NUM_PCM_OUT, PLOYTEC_EP_NUM_MIDI_IN);
+		return -ENODEV;
+	}
+
+	if (!jockey3_has_bulk_endpoint(intf1, PLOYTEC_EP_NUM_PCM_IN, false)) {
+		dev_err(&intf0->dev, "Required bulk IN endpoint not found on Interface 1 (IN: 0x%02x)\n",
+			PLOYTEC_EP_NUM_PCM_IN);
+		return -ENODEV;
+	}
+	return 0;
+}
+
+/*
+ * Channel maps.
+ *
+ * This device exposes several independent stereo pairs, which is a different
+ * thing from a multichannel speaker arrangement -- and speaker positions are
+ * all the chmap enum can express. Claiming, say, RL/RR for the headphone pair
+ * would invite an audio server to treat a cue output as rear speakers, so the
+ * discrete pairs are reported as SNDRV_CHMAP_UNKNOWN.
+ *
+ * The one exception is the playback Master pair, which is marked FL/FR so that
+ * userspace (PipeWire and friends) can identify the device's primary output.
+ * There is no equivalent notion for the inputs, so capture is left entirely
+ * unpositioned.
+ *
+ * Physical layout (see Documentation/sound/cards/jockey3.rst):
+ *   Playback: 1-2 Master Out L/R, 3-4 Headphone L/R
+ *   Capture:  1-2 Input 1 L/R, 3-4 Input 2 L/R, 5-6 Microphone
+ *
+ * The microphone is mono: the balanced input stage feeds the same analog
+ * signal to both converters, so channels 5 and 6 carry identical content.
+ */
+static const struct snd_pcm_chmap_elem jockey3_playback_chmap[] = {
+	{ .channels = 4,
+	  .map = { SNDRV_CHMAP_FL, SNDRV_CHMAP_FR,
+		   SNDRV_CHMAP_UNKNOWN, SNDRV_CHMAP_UNKNOWN } },
+	{ }
+};
+
+static const struct snd_pcm_chmap_elem jockey3_capture_chmap[] = {
+	{ .channels = 6,
+	  .map = { SNDRV_CHMAP_UNKNOWN, SNDRV_CHMAP_UNKNOWN,
+		   SNDRV_CHMAP_UNKNOWN, SNDRV_CHMAP_UNKNOWN,
+		   SNDRV_CHMAP_UNKNOWN, SNDRV_CHMAP_UNKNOWN } },
+	{ }
+};
+
+static int jockey3_init_pcm(struct jockey3_chip *chip)
+{
+	int ret = snd_pcm_new(chip->card, CARD_NAME " Audio", 0, 1, 1, &chip->pcm);
+
+	if (ret < 0)
+		return ret;
+
+	strscpy(chip->pcm->name, CARD_NAME " Audio", sizeof(chip->pcm->name));
+	chip->pcm->private_data = chip;
+	snd_pcm_set_ops(chip->pcm, SNDRV_PCM_STREAM_PLAYBACK, &jockey3_pcm_ops);
+	snd_pcm_set_ops(chip->pcm, SNDRV_PCM_STREAM_CAPTURE, &jockey3_pcm_ops);
+	snd_pcm_set_managed_buffer_all(chip->pcm, SNDRV_DMA_TYPE_VMALLOC, NULL, 0, 0);
+
+	ret = snd_pcm_add_chmap_ctls(chip->pcm, SNDRV_PCM_STREAM_PLAYBACK,
+				     jockey3_playback_chmap, 4, 0, NULL);
+	if (ret < 0)
+		return ret;
+
+	return snd_pcm_add_chmap_ctls(chip->pcm, SNDRV_PCM_STREAM_CAPTURE,
+				      jockey3_capture_chmap, 6, 0, NULL);
+}
+
+static int jockey3_init_midi(struct jockey3_chip *chip)
+{
+	int ret = snd_rawmidi_new(chip->card, CARD_NAME " MIDI", 0, 1, 1, &chip->rmidi);
+
+	if (ret < 0)
+		return ret;
+
+	chip->rmidi->private_data = chip;
+	strscpy(chip->rmidi->name, CARD_NAME " MIDI", sizeof(chip->rmidi->name));
+	snd_rawmidi_set_ops(chip->rmidi, SNDRV_RAWMIDI_STREAM_INPUT, &jockey3_midi_in_ops);
+	snd_rawmidi_set_ops(chip->rmidi, SNDRV_RAWMIDI_STREAM_OUTPUT, &jockey3_midi_out_ops);
+	chip->rmidi->info_flags = SNDRV_RAWMIDI_INFO_INPUT |
+				  SNDRV_RAWMIDI_INFO_OUTPUT |
+				  SNDRV_RAWMIDI_INFO_DUPLEX;
+	return 0;
+}
+
+static void jockey3_setup_card_names(struct jockey3_chip *chip, int driver_info)
+{
+	/*
+	 * card->driver is only char[16] and is what shows up as the card ID in
+	 * /proc/asound, so it holds a short model identifier rather than the
+	 * module name (which would be silently truncated).
+	 */
+	strscpy(chip->card->driver, "Jockey3", sizeof(chip->card->driver));
+	strscpy(chip->card->shortname, CARD_NAME, sizeof(chip->card->shortname));
+	strscpy(chip->card->mixername, CARD_NAME, sizeof(chip->card->mixername));
+
+	snprintf(chip->card->longname, sizeof(chip->card->longname),
+		 "%s %s at USB %s", CARD_NAME, jockey3_model_name(driver_info),
+		 dev_name(&chip->dev->dev));
+}
+
+static int jockey3_probe(struct usb_interface *intf, const struct usb_device_id *usb_id)
+{
+	struct usb_device *dev = interface_to_usbdev(intf);
+	struct usb_interface *intf1;
+	struct snd_card *card;
+	struct jockey3_chip *chip;
+	unsigned int dev_idx;
+	int ret;
+
+	if (intf->cur_altsetting->desc.bInterfaceNumber != 0)
+		return -ENODEV;
+
+	intf1 = usb_ifnum_to_if(dev, 1);
+	if (!intf1)
+		return -ENODEV;
+
+	ret = jockey3_validate_endpoints(intf, intf1);
+	if (ret < 0)
+		return ret;
+
+	/* Claim the first enabled, unused card slot */
+	scoped_guard(mutex, &jockey3_devices_mutex) {
+		for (dev_idx = 0; dev_idx < SNDRV_CARDS; dev_idx++)
+			if (enable[dev_idx] && !test_bit(dev_idx, jockey3_devices_used))
+				break;
+
+		if (dev_idx >= SNDRV_CARDS)
+			return -ENODEV;
+
+		__set_bit(dev_idx, jockey3_devices_used);
+	}
+
+	/*
+	 * Deliberately not snd_devm_card_new(): a managed card is freed from the
+	 * devres unwind inside usb_unbind_interface(), where snd_card_free()
+	 * blocks until userspace closes every file descriptor on the card. On an
+	 * unplug that unwind runs on the USB hub work queue, so one process
+	 * sitting on a PCM fd would stall hotplug for the whole hub. The card is
+	 * released from jockey3_disconnect() with snd_card_free_when_closed()
+	 * instead, which is what every other USB sound driver does; the
+	 * resources that outlive it go in card->private_free below.
+	 */
+	ret = snd_card_new(&intf->dev, index[dev_idx], id[dev_idx], THIS_MODULE,
+			   sizeof(struct jockey3_chip), &card);
+	if (ret < 0)
+		goto err_free_idx;
+
+	chip = card->private_data;
+	chip->dev_idx = dev_idx;
+
+	chip->card = card;
+	/*
+	 * The card outlives the unbind, so these pointers must too:
+	 * snd_card_free_when_closed() defers the release until userspace closes
+	 * the card, while usb_disconnect() device_unregister()s the interfaces
+	 * as soon as the device leaves.
+	 *
+	 * This keeps the memory valid, not the device usable -- the interfaces
+	 * are still unbound and every transfer through them fails.
+	 * JOCKEY3_FLAG_DISCONNECTED remains the thing to test.
+	 */
+	chip->dev = usb_get_dev(dev);
+	chip->intf0 = usb_get_intf(intf);
+	chip->intf1 = usb_get_intf(intf1);
+	chip->flags = 0;
+
+	spin_lock_init(&chip->midi_lock);
+	spin_lock_init(&chip->playback.lock);
+	spin_lock_init(&chip->capture.lock);
+	mutex_init(&chip->rate_mutex);
+	init_completion(&chip->reset_done);
+
+	init_usb_anchor(&chip->playback.anchor);
+	init_usb_anchor(&chip->capture.anchor);
+	init_waitqueue_head(&chip->playback.drain_wait);
+	init_waitqueue_head(&chip->capture.drain_wait);
+
+	/*
+	 * Arm the release path only now that everything it touches exists --
+	 * jockey3_free_resources() calls mutex_destroy() on rate_mutex, so it
+	 * must not become reachable before mutex_init() above. From here on
+	 * every failure goes to err_free_card, and the card slot is given back
+	 * by jockey3_free_resources() rather than by err_free_idx. Keep this
+	 * assignment below the initialization block: nothing fallible may be
+	 * inserted between them.
+	 */
+	card->private_free = jockey3_card_free;
+	/*
+	 * card->private_data is zeroed by snd_card_new(), but be explicit:
+	 * these two are load-bearing for the stop/stall bookkeeping.
+	 */
+	atomic_set(&chip->playback.urbs_in_flight, 0);
+	atomic_set(&chip->capture.urbs_in_flight, 0);
+	atomic64_set(&chip->playback.last_callback_time, 0);
+	atomic64_set(&chip->capture.last_callback_time, 0);
+	atomic64_set(&chip->playback.urbs_started_time, 0);
+	atomic64_set(&chip->capture.urbs_started_time, 0);
+	/*
+	 * Default N until the first hw_params() picks a period-tuned value
+	 * (jockey3_pcm_set_n()); matches the width the URB buffers below are
+	 * actually allocated at, and jockey3_init_out_packet() primes.
+	 */
+	chip->playback.n_shift = ilog2(JOCKEY3_PLAYBACK_N);
+	chip->playback.n_pkts = JOCKEY3_PLAYBACK_N;
+	chip->capture.n_shift = ilog2(JOCKEY3_CAPTURE_N);
+	chip->capture.n_pkts = JOCKEY3_CAPTURE_N;
+	INIT_DELAYED_WORK(&chip->watchdog_work, jockey3_watchdog_work);
+
+	chip->xfer_buf = kmalloc(USB_XFER_BUF_SIZE, GFP_KERNEL);
+	if (!chip->xfer_buf) {
+		ret = -ENOMEM;
+		goto err_free_card;
+	}
+
+	ret = jockey3_init_midi_urb(chip);
+	if (ret < 0)
+		goto err_free_card;
+
+	ret = jockey3_init_playback_urbs(chip);
+	if (ret < 0)
+		goto err_free_card;
+
+	ret = jockey3_init_capture_urbs(chip);
+	if (ret < 0)
+		goto err_free_card;
+
+	/* Interface 1 owns the capture endpoint (0x86) */
+	ret = usb_driver_claim_interface(&jockey3_driver, intf1, chip);
+	if (ret < 0)
+		goto err_free_card;
+
+	ret = jockey3_init_pcm(chip);
+	if (ret < 0)
+		goto err_free_card;
+
+	ret = jockey3_init_midi(chip);
+	if (ret < 0)
+		goto err_free_card;
+
+	jockey3_setup_card_names(chip, usb_id->driver_info);
+
+	if (card->id[0] == '\0')
+		snd_card_set_id(card, "RJ3");
+
+	usb_set_intfdata(intf, chip);
+	ret = jockey3_initialize(chip, usb_id->driver_info);
+	if (ret < 0)
+		goto err_free_card;
+
+	ret = snd_card_register(card);
+	if (ret < 0)
+		goto err_free_card;
+
+	return 0;
+
+err_free_card:
+	/*
+	 * Same order as jockey3_disconnect(): latch DISCONNECTED so a watchdog
+	 * tick cannot re-arm itself, wait for any tick already running, kill the
+	 * URBs, and only then let go of interface 1 and the card. All of it is
+	 * safe on a half-built chip -- the locks, anchors and work item are
+	 * initialized above with no failure point between, and everything
+	 * jockey3_free_resources() touches is NULL until it is allocated.
+	 *
+	 * snd_card_free() rather than the disconnect path's
+	 * snd_card_free_when_closed(): the card is not registered yet on any
+	 * path that reaches here, so nothing can have it open and the
+	 * synchronous free cannot block.
+	 */
+	usb_set_intfdata(intf, NULL);
+	set_bit(JOCKEY3_FLAG_DISCONNECTED, &chip->flags);
+	cancel_delayed_work_sync(&chip->watchdog_work);
+	jockey3_stop_urbs(chip);
+	usb_driver_release_interface(&jockey3_driver, intf1);
+	snd_card_free(card);
+	return ret;
+
+err_free_idx:
+	/* Only reached before the card exists */
+	scoped_guard(mutex, &jockey3_devices_mutex)
+		__clear_bit(dev_idx, jockey3_devices_used);
+	return ret;
+}
+
+static void jockey3_disconnect(struct usb_interface *intf)
+{
+	struct jockey3_chip *chip = usb_get_intfdata(intf);
+
+	/*
+	 * Latch DISCONNECTED first, and for EITHER interface. Every ALSA entry
+	 * point tests it on the way in, so setting it before anything else is
+	 * torn down closes the window where e.g. jockey3_pcm_hw_params() had
+	 * already passed its check and would go on to resubmit URBs we just
+	 * killed.
+	 *
+	 * Doing it for interface 1 as well is what covers the unbind order. The
+	 * driver is bound to both interfaces and the USB core takes them down
+	 * one at a time, calling disconnect() and then usb_disable_interface()
+	 * for each, so whichever goes first has its endpoints flushed while the
+	 * other is still bound. Those URBs come back -ESHUTDOWN through the
+	 * ordinary completion path, and without this they are indistinguishable
+	 * from an endpoint torn down behind the driver's back. Losing either
+	 * interface means the card is going away regardless -- interface 1 owns
+	 * the capture endpoint -- so there is nothing to keep running for.
+	 */
+	if (chip)
+		set_bit(JOCKEY3_FLAG_DISCONNECTED, &chip->flags);
+
+	if (chip && intf == chip->intf0) {
+		clear_bit(JOCKEY3_FLAG_RESETTING, &chip->flags);
+		/*
+		 * Release anyone blocked in jockey3_wait_for_reset_completion():
+		 * a failed reset unbinds the interface instead of calling
+		 * jockey3_post_reset(), so this is the only wakeup they get.
+		 */
+		complete_all(&chip->reset_done);
+
+		/*
+		 * Sync here, unlike in jockey3_stop_urbs(): no mutex is held on
+		 * this path, and a tick that is mid-flight must be finished with
+		 * the chip before the card is torn down. DISCONNECTED is already
+		 * set above, so a tick that started just before this will not
+		 * requeue itself.
+		 */
+		cancel_delayed_work_sync(&chip->watchdog_work);
+
+		jockey3_stop_urbs(chip);
+
+		/*
+		 * Release interface 1 here, not from card->private_free: that
+		 * runs when the last file descriptor is closed, by which time
+		 * usb_disconnect() may already have freed the interface. This
+		 * recurses into jockey3_disconnect() for interface 1, which only
+		 * latches DISCONNECTED and clears its intfdata; if the core got
+		 * there first, usb_driver_release_interface() sees a condition
+		 * other than USB_INTERFACE_BOUND and returns.
+		 */
+		usb_driver_release_interface(&jockey3_driver, chip->intf1);
+
+		/*
+		 * snd_card_free_when_closed() disconnects the card now and frees
+		 * it once userspace has closed it -- it never blocks, which is
+		 * the whole point of not using a managed card (see the comment in
+		 * jockey3_probe()). It runs snd_pcm_stop(DISCONNECTED) under the
+		 * stream lock on the way, which drives our .trigger and clears
+		 * 'running' with the proper locking, so there is nothing to clear
+		 * here by hand. @chip lives in card->private_data and may be gone
+		 * the moment this returns, so nothing below may touch it.
+		 */
+		snd_card_free_when_closed(chip->card);
+	}
+	usb_set_intfdata(intf, NULL);
+}
+
+/*
+ * rate_mutex is taken and released within each of pre_reset()/post_reset()
+ * rather than being held across the reset. The USB core does not guarantee
+ * post_reset() runs at all: if the reset fails, the interface is marked for
+ * rebinding and unbound instead, so a lock handed off from pre_reset() would
+ * be leaked permanently and every later PCM ioctl would block on it forever.
+ *
+ * The window between the two callbacks is not left unguarded: the device is
+ * physically in reset, so every EP0 transfer fails and is error-checked, and
+ * JOCKEY3_FLAG_RESETTING gates the ALSA entry points.
+ */
+static int jockey3_pre_reset(struct usb_interface *intf)
+{
+	struct jockey3_chip *chip = usb_get_intfdata(intf);
+
+	if (chip && intf == chip->intf0) {
+		/*
+		 * A reset started outside this driver -- usbfs USBDEVFS_RESET,
+		 * say -- does not pass through jockey3_queue_reset(), so
+		 * chip->reset_done still carries the complete_all() from the
+		 * previous reset and jockey3_wait_for_reset_completion() would
+		 * return immediately, letting an ALSA ioctl talk to a device
+		 * that is mid-reset. Re-arm it for that path. Testing the flag
+		 * unlocked is safe because the USB core serializes resets per
+		 * device, and re-arming before setting the flag leaves no
+		 * window in which a waiter sees "resetting" but a stale
+		 * completion.
+		 */
+		if (!jockey3_is_resetting(chip))
+			reinit_completion(&chip->reset_done);
+		set_bit(JOCKEY3_FLAG_RESETTING, &chip->flags);
+		scoped_guard(mutex, &chip->rate_mutex)
+			jockey3_stop_urbs(chip);
+	}
+	return 0;
+}
+
+static int jockey3_post_reset(struct usb_interface *intf)
+{
+	struct jockey3_chip *chip = usb_get_intfdata(intf);
+	u32 hw_rate = 0;
+
+	if (chip && intf == chip->intf0) {
+		scoped_guard(mutex, &chip->rate_mutex) {
+			jockey3_initialize_ploytec(chip, NULL);
+
+			/*
+			 * Re-apply the rate unconditionally, as the vendor
+			 * sequence does. The read below is a diagnostic only.
+			 */
+			if (ploytec_get_rate(chip->intf0, chip->xfer_buf,
+					     PLOYTEC_RATE_IDX_DEVICE, &hw_rate) == 0 &&
+			    hw_rate != chip->current_rate)
+				dev_dbg(&chip->intf0->dev,
+					"Rate after reset: HW %u, expected %u\n",
+					hw_rate, chip->current_rate);
+
+			jockey3_set_rate(chip, chip->current_rate, true);
+
+			jockey3_start_urbs_failed(chip, jockey3_start_urbs(chip, false),
+						  "a device reset");
+		}
+
+		clear_bit(JOCKEY3_FLAG_RESETTING, &chip->flags);
+		complete_all(&chip->reset_done);
+	}
+	return 0;
+}
+
+static int jockey3_suspend(struct usb_interface *intf, pm_message_t message)
+{
+	struct jockey3_chip *chip = usb_get_intfdata(intf);
+
+	if (chip && intf == chip->intf0) {
+		dev_dbg(&intf->dev, "USB suspend, stopping URBs\n");
+
+		/*
+		 * Before rate_mutex below: a tick blocked on that mutex resumes
+		 * the moment this drops it, and the flag is what stops it
+		 * restarting the ring. See jockey3_recover_urb_stream().
+		 */
+		set_bit(JOCKEY3_FLAG_SUSPENDED, &chip->flags);
+
+		/* Notify ALSA core to transition state and unblock userspace */
+		if (chip->pcm)
+			snd_pcm_suspend_all(chip->pcm);
+
+		/*
+		 * Stop the physical URBs under rate_mutex, matching
+		 * jockey3_restore_device() on the resume side -- otherwise a
+		 * suspend could land in the middle of a rate change.
+		 */
+		scoped_guard(mutex, &chip->rate_mutex)
+			jockey3_stop_urbs(chip);
+	}
+	return 0;
+}
+
+static int jockey3_restore_device(struct jockey3_chip *chip, bool reset)
+{
+	int ret;
+
+	guard(mutex)(&chip->rate_mutex);
+
+	/*
+	 * Cleared under the mutex, before the restart below that
+	 * jockey3_recover_urb_stream() defers to. A tick cannot slip in between:
+	 * it needs this same mutex to restart anything.
+	 */
+	clear_bit(JOCKEY3_FLAG_SUSPENDED, &chip->flags);
+
+	if (reset) {
+		ret = jockey3_initialize_ploytec(chip, NULL);
+		if (ret < 0)
+			return ret;
+	}
+
+	ret = jockey3_set_rate(chip, chip->current_rate, true);
+	if (ret < 0)
+		return ret;
+
+	/*
+	 * Report the failure up: the PM core logs a failed resume, and unlike the
+	 * reset path there is no queued recovery on the way that would pick this
+	 * up on its own.
+	 */
+	ret = jockey3_start_urbs(chip, false);
+	if (ret < 0) {
+		dev_err(&chip->intf0->dev, "Failed to start URBs while restoring device: %d\n",
+			ret);
+		return ret;
+	}
+	return 0;
+}
+
+static int jockey3_resume(struct usb_interface *intf)
+{
+	struct jockey3_chip *chip = usb_get_intfdata(intf);
+
+	if (chip && intf == chip->intf0) {
+		dev_dbg(&intf->dev, "USB resume, restoring device\n");
+		return jockey3_restore_device(chip, false);
+	}
+	return 0;
+}
+
+static int jockey3_reset_resume(struct usb_interface *intf)
+{
+	struct jockey3_chip *chip = usb_get_intfdata(intf);
+
+	if (chip && intf == chip->intf0) {
+		dev_dbg(&intf->dev, "USB reset resume, restoring device\n");
+		return jockey3_restore_device(chip, true);
+	}
+	return 0;
+}
+
+static const struct usb_device_id jockey3_ids[] = {
+	{ USB_DEVICE(RELOOP_VENDOR_ID, RELOOP_JOCKEY3_ME_PID), .driver_info = JOCKEY3_ME },
+	{ USB_DEVICE(RELOOP_VENDOR_ID, RELOOP_JOCKEY3_REMIX_PID), .driver_info = JOCKEY3_REMIX },
+	{}
+};
+MODULE_DEVICE_TABLE(usb, jockey3_ids);
+
+static struct usb_driver jockey3_driver = {
+	.name = "snd-reloop-jockey3",
+	/*
+	 * Tear our own URBs down rather than having the core do it first.
+	 *
+	 * Without this, usb_unbind_interface() calls usb_disable_interface()
+	 * before jockey3_disconnect(), so on an ordinary module unload the whole
+	 * ring retires with -ESHUTDOWN while the driver still believes it is
+	 * running -- indistinguishable, from inside the completion handler, from
+	 * an endpoint torn down behind our back. jockey3_disconnect() already
+	 * kills every URB, which is exactly the contract this flag asks for.
+	 *
+	 * It applies only while the device is still attached; on a physical
+	 * unplug the core kills the URBs first regardless, which is why the
+	 * completion path also tests USB_STATE_NOTATTACHED.
+	 */
+	.soft_unbind = 1,
+	.probe = jockey3_probe,
+	.disconnect = jockey3_disconnect,
+	.pre_reset = jockey3_pre_reset,
+	.post_reset = jockey3_post_reset,
+	.suspend = jockey3_suspend,
+	.resume = jockey3_resume,
+	.reset_resume = jockey3_reset_resume,
+	.id_table = jockey3_ids
+};
+
+/*
+ * The codec's bit-spread lookup tables are module-global and unlocked, so they
+ * are built once per module rather than per device.
+ */
+static int __init jockey3_module_init(void)
+{
+	enum ploytec_codec_variant codec_variant = ploytec_initialize_codec();
+
+	switch (codec_variant) {
+	case PLOYTEC_CODEC_PORTABLE:
+		pr_debug("using portable codec\n");
+		break;
+	case PLOYTEC_CODEC_OPTIMIZED_64BIT:
+		pr_debug("using 64-bit optimized codec\n");
+		break;
+	case PLOYTEC_CODEC_OPTIMIZED_32BIT:
+		pr_debug("using 32-bit optimized codec\n");
+		break;
+	}
+
+	return usb_register(&jockey3_driver);
+}
+module_init(jockey3_module_init);
+
+static void __exit jockey3_module_exit(void)
+{
+	usb_deregister(&jockey3_driver);
+}
+module_exit(jockey3_module_exit);
+
+MODULE_AUTHOR("Frank van de Pol <fvdpol@gmail.com>");
+MODULE_DESCRIPTION(CARD_NAME " ALSA Driver");
+MODULE_LICENSE("GPL");
+MODULE_SOFTDEP("pre: snd-pcm snd-rawmidi");
diff --git a/sound/usb/jockey3/ploytec_codec.c b/sound/usb/jockey3/ploytec_codec.c
new file mode 100644
index 0000000000000..8c660899a32a7
--- /dev/null
+++ b/sound/usb/jockey3/ploytec_codec.c
@@ -0,0 +1,656 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *   ALSA driver for Reloop Jockey 3 devices
+ *   Ploytec PCM encoding/decoding functions for the S24_3LE format (3 bytes per sample)
+ *
+ *   Copyright (c) 2026 by Frank van de Pol <fvdpol@gmail.com>
+ */
+
+#include <linux/unaligned.h>
+#include <linux/string.h>
+#include "ploytec_codec.h"
+
+/**
+ * DOC: Why the wire format looks like this
+ *
+ * The "bit-plane" layout implemented below is not an arbitrary scramble; it is
+ * a direct image of how the hardware moves audio. On the Jockey 3 the USB
+ * device controller (NXP ISP1583) runs in split-bus mode, and the serial
+ * data lines of the capture ADCs are wired straight onto the low bits of its
+ * 16-bit DMA bus - one converter per bus bit. The controller samples the whole
+ * bus once per bit clock and streams the result to the host, so every byte on
+ * the wire is a snapshot of all the channel data lines at one instant.
+ *
+ * Decoding a frame is therefore a transpose:
+ *
+ * - The byte index is one bit-clock tick, most significant bit first: wire
+ *   byte 0 carries bit 23 of the current sample, wire byte 23 carries bit 0.
+ * - The bit position within each byte is one physical converter line, that is
+ *   one channel (bit 0, bit 1, bit 2, ... for successive converters).
+ * - The frame is two 24-byte halves, the left and right sub-frames of the
+ *   underlying I2S frame. On capture an 8-byte gap sits between and after the
+ *   halves: it is the unused tail of each 32-bit I2S slot (64-byte frame).
+ *   Playback is generated on the byte-wide microprocessor bus instead, so its
+ *   slots are packed to 24 bits with no gap (48-byte frame).
+ *
+ * Because the geometry follows from the I2S timing and the DMA bus width, it
+ * is fixed regardless of how many converters are populated. That is why the
+ * codec always works in 8-channel-wide slices and simply ignores the bit
+ * positions that carry no converter (the Jockey 3 populates three capture
+ * lines and two playback lines).
+ *
+ * This is a model inferred from the board schematic and the observed stream
+ * rather than from vendor documentation, but it accounts for every bit of the
+ * format the functions below implement.
+ */
+
+#if IS_ENABLED(CONFIG_SND_USB_JOCKEY3_REFERENCE_CODEC)
+
+/*
+ * The reference implementation of the Ploytec codec performs a bit
+ * gather/scatter operation with minimal optimization. It mainly serves as a
+ * readable definition of the wire format, and as the oracle the optimized
+ * variants below are validated against.
+ *
+ * Selected by CONFIG_SND_USB_JOCKEY3_REFERENCE_CODEC.
+ */
+
+/**
+ * ploytec_encode_s24_3le() - Encode 4-channel S24_3LE to 48-byte Ploytec frame
+ * @dest: 48-byte destination buffer
+ * @src: 12-byte source buffer (4 channels * 3 bytes)
+ *
+ * Ploytec Bit-Plane Interleaving (Playback):
+ * The firmware uses a non-standard "bit-plane" format where bits from different
+ * channels are interleaved into the same byte.
+ * - Each 48-byte frame carries one PCM frame: a single sample for each of
+ *   the 4 channels, spread one bit per wire byte.
+ * - Bytes 0-23: ALSA Channels 1 & 3
+ * - Bytes 24-47: ALSA Channels 2 & 4
+ * - Within each 24-byte block, bits are grouped by significance:
+ *   - [0-7]: Most significant bits
+ *   - [8-15]: Middle bits
+ *   - [16-23]: Least significant bits
+ * - bit 0 of each byte corresponds to the first channel in the pair.
+ * - bit 1 of each byte corresponds to the second channel in the pair.
+ */
+static inline void ploytec_encode_s24_3le(u8 *dest, const u8 *src)
+{
+	int i;
+
+	for (i = 0; i < 8; i++) {
+		/* First 24 bytes: odd channels (ALSA Ch 1 & 3) */
+		dest[i]      = (((src[2] >> (7 - i)) & 1) << 0) | (((src[8] >> (7 - i)) & 1) << 1);
+		dest[8 + i]  = (((src[1] >> (7 - i)) & 1) << 0) | (((src[7] >> (7 - i)) & 1) << 1);
+		dest[16 + i] = (((src[0] >> (7 - i)) & 1) << 0) | (((src[6] >> (7 - i)) & 1) << 1);
+
+		/* Second 24 bytes: even channels (ALSA Ch 2 & 4) */
+		dest[24 + i] = (((src[5] >> (7 - i)) & 1) << 0) | (((src[11] >> (7 - i)) & 1) << 1);
+		dest[32 + i] = (((src[4] >> (7 - i)) & 1) << 0) | (((src[10] >> (7 - i)) & 1) << 1);
+		dest[40 + i] = (((src[3] >> (7 - i)) & 1) << 0) | (((src[9]  >> (7 - i)) & 1) << 1);
+	}
+}
+
+/**
+ * ploytec_decode_s24_3le() - Decode 64-byte Ploytec frame to 6-channel S24_3LE
+ * @dest: 18-byte destination buffer (6 channels * 3 bytes)
+ * @src: 64-byte source buffer
+ *
+ * Ploytec Bit-Plane Interleaving (Capture):
+ * Similar to encoding, the capture path interleaves 3 pairs of channels
+ * into bit-planes (bit 0, 1, and 2 of each byte).
+ * - Bytes 0x00-0x17: Pair 1 (bits 0,1,2)
+ * - Bytes 0x20-0x37: Pair 2 (bits 0,1,2)
+ */
+static inline void ploytec_decode_s24_3le(u8 *dest, const u8 *src)
+{
+	int i;
+
+	memset(dest, 0, 18);
+	for (i = 0; i < 8; i++) {
+		/* Channel 1: odd channel 1 (bit 0 of bytes 0x00-0x17) */
+		dest[0x00] |= ((src[0x10 + i] & 0x01) << (7 - i));	// Ch1 LSB byte
+		dest[0x01] |= ((src[0x08 + i] & 0x01) << (7 - i));
+		dest[0x02] |= ((src[0x00 + i] & 0x01) << (7 - i));	// Ch1 MSB byte
+
+		/* Channel 2: even channel 2 (bit 0 of bytes 0x20-0x37) */
+		dest[0x03] |= ((src[0x30 + i] & 0x01) << (7 - i));
+		dest[0x04] |= ((src[0x28 + i] & 0x01) << (7 - i));
+		dest[0x05] |= ((src[0x20 + i] & 0x01) << (7 - i));
+
+		/* Channel 3: odd channel 3 (bit 1 of bytes 0x00-0x17) */
+		dest[0x06] |= (((src[0x10 + i] & 0x02) >> 1) << (7 - i));
+		dest[0x07] |= (((src[0x08 + i] & 0x02) >> 1) << (7 - i));
+		dest[0x08] |= (((src[0x00 + i] & 0x02) >> 1) << (7 - i));
+
+		/* Channel 4: even channel 4 (bit 1 of bytes 0x20-0x37) */
+		dest[0x09] |= (((src[0x30 + i] & 0x02) >> 1) << (7 - i));
+		dest[0x0A] |= (((src[0x28 + i] & 0x02) >> 1) << (7 - i));
+		dest[0x0B] |= (((src[0x20 + i] & 0x02) >> 1) << (7 - i));
+
+		/* Channel 5: odd channel 5 (bit 2 of bytes 0x00-0x17) */
+		dest[0x0C] |= (((src[0x10 + i] & 0x04) >> 2) << (7 - i));
+		dest[0x0D] |= (((src[0x08 + i] & 0x04) >> 2) << (7 - i));
+		dest[0x0E] |= (((src[0x00 + i] & 0x04) >> 2) << (7 - i));
+
+		/* Channel 6: even channel 6 (bit 2 of bytes 0x20-0x37) */
+		dest[0x0F] |= (((src[0x30 + i] & 0x04) >> 2) << (7 - i));
+		dest[0x10] |= (((src[0x28 + i] & 0x04) >> 2) << (7 - i));
+		dest[0x11] |= (((src[0x20 + i] & 0x04) >> 2) << (7 - i));
+	}
+}
+#elif defined(CONFIG_64BIT)
+/**
+ * DOC: Ploytec bit-plane interleaving optimization
+ *
+ * CORE PERFORMANCE PROBLEM:
+ * The Ploytec firmware uses a non-standard "bit-plane" format where bits
+ * from different channels are interleaved into the same byte. The naive
+ * implementation processes data bit-by-bit using heavily nested loops,
+ * causing massive CPU overhead due to serial bit-shifting operations,
+ * branch mispredictions, and an inability to utilize hardware pipelines.
+ *
+ * THE SOLUTION: SWAR (SIMD Within A Register) & INT-MULTIPLIERS
+ * Instead of bit-by-bit iteration, we treat entire 32-bit or 64-bit blocks
+ * of memory as parallel vector lanes inside standard CPU registers. This
+ * completely eliminates loops and branches, reducing operations to a small,
+ * deterministic chain of native processor instructions.
+ *
+ * -------------------------------------------------------------------------
+ * 1. ENCODING MATH: BIT-SPREAD LOOKUP TABLES (LUT)
+ * -------------------------------------------------------------------------
+ * To encode, we must take a single 8-bit channel byte (e.g., b7 b6 b5 ... b0)
+ * and "spread" its bits across an 8-byte destination block so that each bit
+ * lands on bit 0 of 8 separate bytes.
+ *
+ * 64-Bit Implementation:
+ * We use a precomputed 256-entry 64-bit LUT (ploytec_bit_spread). For any
+ * byte value, the table returns a u64 where the bits are perfectly spaced out:
+ * Input Byte:  0b11000000
+ * u64 Return:  0x0000000000000101 (Bit 7 and 6 mapped to byte 0 and 1)
+ *
+ * By loading the spread values of two channels simultaneously, we shift the
+ * second channel by 1 (<< 1) to align its bits to bit position 1, and bitwise
+ * OR them together. This encodes two full channels into an 8-byte frame using
+ * just one lookup and a single 64-bit store.
+ *
+ * 32-Bit Implementation:
+ * A 32-bit register cannot hold 8 spread bytes at once. Thus, the 32-bit
+ * variant splits the input byte into two 4-bit nibbles using two distinct
+ * 32-bit LUTs (high nibble and low nibble). Each lookup fills 4 bytes of
+ * destination memory natively using 32-bit registers, maintaining low
+ * register pressure and avoiding 64-bit arithmetic emulation penalties on
+ * 32-bit architectures (like ARM32/armhf).
+ *
+ * -------------------------------------------------------------------------
+ * 2. DECODING MATH: PARALLEL BIT GATHER VIA MAGIC MULTIPLIERS
+ * -------------------------------------------------------------------------
+ * To decode, we must perform the inverse: extract a specific bit position
+ * from 8 consecutive bytes in memory and pack them back into a single byte.
+ *
+ * 64-Bit Implementation (`pack_bit_plane64`):
+ * 1. Load 8 bytes into a u64 register.
+ * 2. Shift right by the desired bit index and mask with 0x0101010101010101ULL.
+ * This leaves the target bit isolated as bit 0 of every single byte.
+ * 3. Multiply the masked u64 by the magic constant: 0x8040201008040201ULL.
+ *
+ * This multiplication acts as a parallel shift-and-add engine:
+ * Byte 7 is multiplied by 0x01 (shifted << 0)
+ * Byte 6 is multiplied by 0x02 (shifted << 1)
+ * ...
+ * Byte 0 is multiplied by 0x80 (shifted << 7)
+ *
+ * The mathematical properties of this multiplication collapse all 8 isolated
+ * bits perfectly into the most significant byte (the highest 8 bits) of the
+ * u64 register. A final logical shift right (>> 56) extracts the completed
+ * audio byte in a single operation.
+ *
+ * 32-Bit Implementation (`pack_bit_plane32`):
+ * To avoid emulating 64-bit multiplication on a 32-bit CPU, we read two
+ * separate 4-byte halves into native u32 registers. We apply the 32-bit
+ * magic multiplier (0x08040201U) to both halves independently. This collapses
+ * each half into a 4-bit nibble at the top of each register. We then combine
+ * the two resulting nibbles `(high << 4) | low` to form the final byte.
+ *
+ * -------------------------------------------------------------------------
+ * 3. ENDIANNESS & ALIGNMENT MITIGATION
+ * -------------------------------------------------------------------------
+ * All bitwise shifting (`<<`, `>>`) and multiplier math inside CPU registers
+ * is inherently endian-agnostic. However, reading and writing these multi-byte
+ * integers (u32/u64) directly to physical RAM introduces two system hazards:
+ * - Byte Inversion on Big Endian architectures (e.g., MIPS, PowerPC).
+ * - Kernel Alignment Faults (SIGBUS) on alignment-strict processors.
+ *
+ * To ensure safe, architecture-independent execution, all memory interfaces
+ * utilize the kernel's `get_unaligned_le32/64` and `put_unaligned_le32/64`
+ * macros (or user-space memcpy/bswap equivalents).
+ *
+ * On little-endian architectures with unaligned support (x86_64, arm64, armhf),
+ * the compiler optimizes these macros completely out, compiling down to standard,
+ * zero-overhead native instruction loads and stores. On big-endian or strict-
+ * alignment architectures, the macros safely handle arbitrary memory addresses
+ * and emit hardware byte-swaps (`rev`) automatically.
+ *
+ * -------------------------------------------------------------------------
+ * 4. EMPIRICAL PERFORMANCE TESTING
+ * -------------------------------------------------------------------------
+ * Compared to the reference implementation, testing of these optimized
+ * versions showed a significant improvement, justifying the added complexity
+ * from these optimizations. Table shows the relative speed-up and the time
+ * for encoding (4ch) or decoding (6ch) a sample frame on the validation
+ * hardware, measured through the batch API at the batch sizes the driver
+ * actually uses (10 frames per encode call, 8 per decode):
+ *
+ * Architecture	Bit	Encode	Decode		Encode Time	Decode Time
+ *   i386	32	 6.2x	 5.4x		  8.3 ns	 20.7 ns
+ *   x86_64	64	11.0x	 7.8x		  3.7 ns	 11.9 ns
+ *   armhf	32	 3.3x	 4.8x		358.2 ns	762.7 ns
+ *   arm64	64	 6.9x	 6.1x		 13.6 ns	 35.5 ns
+ *
+ * i386 and x86_64 on a Core i5-6500, arm64 on a Raspberry Pi 4B, armhf on a
+ * Raspberry Pi 1B+ (ARMv6). Speed-up is against this same build's portable
+ * reference codec on the same machine, so it is comparable across rows; the
+ * absolute times are not.
+ *
+ * The 64-bit variants gain more than the 32-bit ones because the transpose
+ * works in register-width chunks: a 64-bit word carries twice the bit-plane
+ * of a 32-bit one, so the same frame needs half the operations.
+ */
+
+static u64 ploytec_bit_spread_64[256];
+
+/**
+ * init_ploytec_bit_spread_lut64() - Build the 64-bit bit-spread lookup table
+ *
+ * Precomputes ploytec_bit_spread_64[], mapping each possible input byte to
+ * the 8-byte bit-plane pattern used by ploytec_encode_s24_3le_lut64(). See
+ * the optimization note above for details of the algorithm.
+ */
+static void init_ploytec_bit_spread_lut64(void)
+{
+	for (int b = 0; b < 256; b++) {
+		u64 spread = 0;
+
+		// Extract bit (7 - i) and place it into bit 0 of byte i
+		for (int i = 0; i < 8; i++)
+			spread |= (u64)((b >> (7 - i)) & 1) << (i * 8);
+
+		ploytec_bit_spread_64[b] = spread;
+	}
+}
+
+/**
+ * ploytec_encode_s24_3le_lut64() - Encode 4-channel S24_3LE using the 64-bit LUT bit-spread method
+ * @dest: 48-byte destination buffer
+ * @src: 12-byte source buffer (4 channels * 3 bytes)
+ *
+ * Optimized, CONFIG_64BIT equivalent of ploytec_encode_s24_3le(); produces
+ * identical output. See the bit-plane layout on ploytec_encode_s24_3le()
+ * and the optimization note above for the LUT technique used here.
+ */
+static inline void ploytec_encode_s24_3le_lut64(u8 *dest, const u8 *src)
+{
+	/*
+	 * put_unaligned_le64 safely writes a 64-bit value in Little Endian
+	 * order, even if the target CPU is Big Endian or alignment-strict.
+	 */
+
+	// First 24 bytes: odd channels (ALSA Ch 1 & 3)
+	put_unaligned_le64(ploytec_bit_spread_64[src[2]] |
+			   (ploytec_bit_spread_64[src[8]] << 1), dest + 0);
+	put_unaligned_le64(ploytec_bit_spread_64[src[1]] |
+			   (ploytec_bit_spread_64[src[7]] << 1), dest + 8);
+	put_unaligned_le64(ploytec_bit_spread_64[src[0]] |
+			   (ploytec_bit_spread_64[src[6]] << 1), dest + 16);
+
+	// Second 24 bytes: even channels (ALSA Ch 2 & 4)
+	put_unaligned_le64(ploytec_bit_spread_64[src[5]] |
+			   (ploytec_bit_spread_64[src[11]] << 1), dest + 24);
+	put_unaligned_le64(ploytec_bit_spread_64[src[4]] |
+			   (ploytec_bit_spread_64[src[10]] << 1), dest + 32);
+	put_unaligned_le64(ploytec_bit_spread_64[src[3]] |
+			   (ploytec_bit_spread_64[src[9]] << 1), dest + 40);
+}
+
+/**
+ * pack_bit_plane64() - Gather bit @bit_index of 8 consecutive bytes into one byte
+ * @val: 8 source bytes loaded as a little-endian u64
+ * @bit_index: bit position (0-7) to extract from each source byte
+ *
+ * Return: the gathered byte, using the magic-multiplier technique described
+ * in the optimization note above.
+ */
+static inline u8 pack_bit_plane64(u64 val, int bit_index)
+{
+	// Shift target bit to bit 0 of each byte, then mask it
+	u64 masked = (val >> bit_index) & 0x0101010101010101ULL;
+
+	/*
+	 * Multiplier acts as a parallel shift-and-add, collecting the bits
+	 * into the highest byte of the u64.
+	 */
+	return (u8)((masked * 0x8040201008040201ULL) >> 56);
+}
+
+/**
+ * ploytec_decode_s24_3le_pack64() - Decode a Ploytec frame using the 64-bit magic-multiplier method
+ * @dest: 18-byte destination buffer (6 channels * 3 bytes)
+ * @src: 64-byte source buffer
+ *
+ * Optimized, CONFIG_64BIT equivalent of ploytec_decode_s24_3le(); produces
+ * identical output. See the bit-plane layout on ploytec_decode_s24_3le()
+ * and the optimization note above for the gather technique used here.
+ */
+static inline void ploytec_decode_s24_3le_pack64(u8 *dest, const u8 *src)
+{
+	u64 blocks_0_17_low  = get_unaligned_le64(src + 0x00);
+	u64 blocks_0_17_mid  = get_unaligned_le64(src + 0x08);
+	u64 blocks_0_17_high = get_unaligned_le64(src + 0x10);
+
+	u64 blocks_20_37_low  = get_unaligned_le64(src + 0x20);
+	u64 blocks_20_37_mid  = get_unaligned_le64(src + 0x28);
+	u64 blocks_20_37_high = get_unaligned_le64(src + 0x30);
+
+	// Channel 1 (Bit 0 of bytes 0x00-0x17)
+	dest[0x00] = pack_bit_plane64(blocks_0_17_high, 0);
+	dest[0x01] = pack_bit_plane64(blocks_0_17_mid,  0);
+	dest[0x02] = pack_bit_plane64(blocks_0_17_low,  0);
+
+	// Channel 2 (Bit 0 of bytes 0x20-0x37)
+	dest[0x03] = pack_bit_plane64(blocks_20_37_high, 0);
+	dest[0x04] = pack_bit_plane64(blocks_20_37_mid,  0);
+	dest[0x05] = pack_bit_plane64(blocks_20_37_low,  0);
+
+	// Channel 3 (Bit 1 of bytes 0x00-0x17)
+	dest[0x06] = pack_bit_plane64(blocks_0_17_high, 1);
+	dest[0x07] = pack_bit_plane64(blocks_0_17_mid,  1);
+	dest[0x08] = pack_bit_plane64(blocks_0_17_low,  1);
+
+	// Channel 4 (Bit 1 of bytes 0x20-0x37)
+	dest[0x09] = pack_bit_plane64(blocks_20_37_high, 1);
+	dest[0x0A] = pack_bit_plane64(blocks_20_37_mid,  1);
+	dest[0x0B] = pack_bit_plane64(blocks_20_37_low,  1);
+
+	// Channel 5 (Bit 2 of bytes 0x00-0x17)
+	dest[0x0C] = pack_bit_plane64(blocks_0_17_high, 2);
+	dest[0x0D] = pack_bit_plane64(blocks_0_17_mid,  2);
+	dest[0x0E] = pack_bit_plane64(blocks_0_17_low,  2);
+
+	// Channel 6 (Bit 2 of bytes 0x20-0x37)
+	dest[0x0F] = pack_bit_plane64(blocks_20_37_high, 2);
+	dest[0x10] = pack_bit_plane64(blocks_20_37_mid,  2);
+	dest[0x11] = pack_bit_plane64(blocks_20_37_low,  2);
+}
+
+#else /* optimized codec, 32-bit build */
+static u32 ploytec_bit_spread_32_high[256];
+static u32 ploytec_bit_spread_32_low[256];
+
+/**
+ * init_ploytec_bit_spread_lut32() - Build the 32-bit bit-spread lookup tables
+ *
+ * Precomputes ploytec_bit_spread_32_high[] and ploytec_bit_spread_32_low[],
+ * the 32-bit nibble-split equivalent of init_ploytec_bit_spread_lut64() for
+ * architectures without efficient native 64-bit arithmetic. See the
+ * optimization note above for details.
+ */
+static void init_ploytec_bit_spread_lut32(void)
+{
+	for (int b = 0; b < 256; b++) {
+		u32 high_spread = 0;
+		u32 low_spread  = 0;
+
+		// Process high nibble (bits 7 down to 4) -> maps to bytes 0 to 3
+		for (int i = 0; i < 4; i++)
+			high_spread |= (u32)((b >> (7 - i)) & 1) << (i * 8);
+
+		// Process low nibble (bits 3 down to 0) -> maps to bytes 0 to 3
+		for (int i = 0; i < 4; i++)
+			low_spread  |= (u32)((b >> (3 - i)) & 1) << (i * 8);
+
+		ploytec_bit_spread_32_high[b] = high_spread;
+		ploytec_bit_spread_32_low[b]  = low_spread;
+	}
+}
+
+/**
+ * ploytec_encode_s24_3le_lut32() - Encode 4-channel S24_3LE using the 32-bit LUT bit-spread method
+ * @dest: 48-byte destination buffer
+ * @src: 12-byte source buffer (4 channels * 3 bytes)
+ *
+ * 32-bit equivalent of ploytec_encode_s24_3le_lut64() for architectures
+ * without efficient native 64-bit arithmetic; produces identical output.
+ */
+static inline void ploytec_encode_s24_3le_lut32(u8 *dest, const u8 *src)
+{
+	/*
+	 * put_unaligned_le32 safely writes a 32-bit value in Little Endian
+	 * order, even if the target CPU is Big Endian or alignment-strict.
+	 */
+
+	// First 24 bytes: Odd channels (ALSA Ch 1 [src 0,1,2] & Ch 3 [src 6,7,8])
+	// Block 1: Most Significant Bytes
+	put_unaligned_le32(ploytec_bit_spread_32_high[src[2]] |
+			   (ploytec_bit_spread_32_high[src[8]] << 1), dest + 0);
+	put_unaligned_le32(ploytec_bit_spread_32_low[src[2]]  |
+			   (ploytec_bit_spread_32_low[src[8]]  << 1), dest + 4);
+
+	// Block 2: Middle Bytes
+	put_unaligned_le32(ploytec_bit_spread_32_high[src[1]] |
+			   (ploytec_bit_spread_32_high[src[7]] << 1), dest + 8);
+	put_unaligned_le32(ploytec_bit_spread_32_low[src[1]]  |
+			   (ploytec_bit_spread_32_low[src[7]]  << 1), dest + 12);
+
+	// Block 3: Least Significant Bytes
+	put_unaligned_le32(ploytec_bit_spread_32_high[src[0]] |
+			   (ploytec_bit_spread_32_high[src[6]] << 1), dest + 16);
+	put_unaligned_le32(ploytec_bit_spread_32_low[src[0]]  |
+			   (ploytec_bit_spread_32_low[src[6]]  << 1), dest + 20);
+
+	// Second 24 bytes: Even channels (ALSA Ch 2 [src 3,4,5] & Ch 4 [src 9,10,11])
+	// Block 1: Most Significant Bytes
+	put_unaligned_le32(ploytec_bit_spread_32_high[src[5]] |
+			   (ploytec_bit_spread_32_high[src[11]] << 1), dest + 24);
+	put_unaligned_le32(ploytec_bit_spread_32_low[src[5]]  |
+			   (ploytec_bit_spread_32_low[src[11]]  << 1), dest + 28);
+
+	// Block 2: Middle Bytes
+	put_unaligned_le32(ploytec_bit_spread_32_high[src[4]] |
+			   (ploytec_bit_spread_32_high[src[10]] << 1), dest + 32);
+	put_unaligned_le32(ploytec_bit_spread_32_low[src[4]]  |
+			   (ploytec_bit_spread_32_low[src[10]]  << 1), dest + 36);
+
+	// Block 3: Least Significant Bytes
+	put_unaligned_le32(ploytec_bit_spread_32_high[src[3]] |
+			   (ploytec_bit_spread_32_high[src[9]] << 1), dest + 40);
+	put_unaligned_le32(ploytec_bit_spread_32_low[src[3]]  |
+			   (ploytec_bit_spread_32_low[src[9]]  << 1), dest + 44);
+}
+
+/**
+ * pack_bit_plane32() - Gather bit @bit_index of 8 consecutive bytes into one byte
+ * @high_4_bytes: first 4 source bytes of the block, holding the high nibble (bits 7-4)
+ * @low_4_bytes: last 4 source bytes of the block, holding the low nibble (bits 3-0)
+ * @bit_index: bit position (0-7) to extract from each source byte
+ *
+ * Return: the gathered byte, using the magic-multiplier technique described
+ * in the optimization note above.
+ */
+static inline u8 pack_bit_plane32(u32 high_4_bytes, u32 low_4_bytes, int bit_index)
+{
+	// Isolate the targeted bit index across all 4 bytes simultaneously
+	u32 high_masked = (high_4_bytes >> bit_index) & 0x01010101U;
+	u32 low_masked  = (low_4_bytes  >> bit_index) & 0x01010101U;
+
+	// Use 32-bit multiplier to gather bits 0,1,2,3 into the highest byte
+	u8 high_bits = (u8)((high_masked * 0x08040201U) >> 24);
+	u8 low_bits  = (u8)((low_masked  * 0x08040201U) >> 24);
+
+	// Combine the upper 4 bits and lower 4 bits into the single output audio byte
+	return (high_bits << 4) | low_bits;
+}
+
+/**
+ * ploytec_decode_s24_3le_pack32() - Decode a Ploytec frame using the 32-bit magic-multiplier method
+ * @dest: 18-byte destination buffer (6 channels * 3 bytes)
+ * @src: 64-byte source buffer
+ *
+ * 32-bit equivalent of ploytec_decode_s24_3le_pack64() for architectures
+ * without efficient native 64-bit arithmetic; produces identical output.
+ */
+static inline void ploytec_decode_s24_3le_pack32(u8 *dest, const u8 *src)
+{
+	// Pre-load all required 4-byte chunks into native 32-bit registers
+	u32 src_00_high = get_unaligned_le32(src + 0x00);
+	u32 src_04_low  = get_unaligned_le32(src + 0x04);
+	u32 src_08_high = get_unaligned_le32(src + 0x08);
+	u32 src_0C_low  = get_unaligned_le32(src + 0x0C);
+	u32 src_10_high = get_unaligned_le32(src + 0x10);
+	u32 src_14_low  = get_unaligned_le32(src + 0x14);
+
+	u32 src_20_high = get_unaligned_le32(src + 0x20);
+	u32 src_24_low  = get_unaligned_le32(src + 0x24);
+	u32 src_28_high = get_unaligned_le32(src + 0x28);
+	u32 src_2C_low  = get_unaligned_le32(src + 0x2C);
+	u32 src_30_high = get_unaligned_le32(src + 0x30);
+	u32 src_34_low  = get_unaligned_le32(src + 0x34);
+
+	// --- Channel 1: Bit 0 of bytes 0x00-0x17 ---
+	dest[0x00] = pack_bit_plane32(src_10_high, src_14_low, 0);
+	dest[0x01] = pack_bit_plane32(src_08_high, src_0C_low, 0);
+	dest[0x02] = pack_bit_plane32(src_00_high, src_04_low, 0);
+
+	// --- Channel 2: Bit 0 of bytes 0x20-0x37 ---
+	dest[0x03] = pack_bit_plane32(src_30_high, src_34_low, 0);
+	dest[0x04] = pack_bit_plane32(src_28_high, src_2C_low, 0);
+	dest[0x05] = pack_bit_plane32(src_20_high, src_24_low, 0);
+
+	// --- Channel 3: Bit 1 of bytes 0x00-0x17 ---
+	dest[0x06] = pack_bit_plane32(src_10_high, src_14_low, 1);
+	dest[0x07] = pack_bit_plane32(src_08_high, src_0C_low, 1);
+	dest[0x08] = pack_bit_plane32(src_00_high, src_04_low, 1);
+
+	// --- Channel 4: Bit 1 of bytes 0x20-0x37 ---
+	dest[0x09] = pack_bit_plane32(src_30_high, src_34_low, 1);
+	dest[0x0A] = pack_bit_plane32(src_28_high, src_2C_low, 1);
+	dest[0x0B] = pack_bit_plane32(src_20_high, src_24_low, 1);
+
+	// --- Channel 5: Bit 2 of bytes 0x00-0x17 ---
+	dest[0x0C] = pack_bit_plane32(src_10_high, src_14_low, 2);
+	dest[0x0D] = pack_bit_plane32(src_08_high, src_0C_low, 2);
+	dest[0x0E] = pack_bit_plane32(src_00_high, src_04_low, 2);
+
+	// --- Channel 6: Bit 2 of bytes 0x20-0x37 ---
+	dest[0x0F] = pack_bit_plane32(src_30_high, src_34_low, 2);
+	dest[0x10] = pack_bit_plane32(src_28_high, src_2C_low, 2);
+	dest[0x11] = pack_bit_plane32(src_20_high, src_24_low, 2);
+}
+#endif
+
+/*
+ * Bind the build's chosen implementation to a single pair of names, so that
+ * everything below is free of conditional compilation.
+ */
+#if IS_ENABLED(CONFIG_SND_USB_JOCKEY3_REFERENCE_CODEC)
+
+static const enum ploytec_codec_variant ploytec_codec_selected =
+	PLOYTEC_CODEC_PORTABLE;
+
+static void ploytec_codec_init_tables(void) { }
+
+static inline void ploytec_encode_frame(u8 *dest, const u8 *src)
+{
+	ploytec_encode_s24_3le(dest, src);
+}
+
+static inline void ploytec_decode_frame(u8 *dest, const u8 *src)
+{
+	ploytec_decode_s24_3le(dest, src);
+}
+
+#elif defined(CONFIG_64BIT)
+
+static const enum ploytec_codec_variant ploytec_codec_selected =
+	PLOYTEC_CODEC_OPTIMIZED_64BIT;
+
+static void ploytec_codec_init_tables(void)
+{
+	init_ploytec_bit_spread_lut64();
+}
+
+static inline void ploytec_encode_frame(u8 *dest, const u8 *src)
+{
+	ploytec_encode_s24_3le_lut64(dest, src);
+}
+
+static inline void ploytec_decode_frame(u8 *dest, const u8 *src)
+{
+	ploytec_decode_s24_3le_pack64(dest, src);
+}
+
+#else
+
+static const enum ploytec_codec_variant ploytec_codec_selected =
+	PLOYTEC_CODEC_OPTIMIZED_32BIT;
+
+static void ploytec_codec_init_tables(void)
+{
+	init_ploytec_bit_spread_lut32();
+}
+
+static inline void ploytec_encode_frame(u8 *dest, const u8 *src)
+{
+	ploytec_encode_s24_3le_lut32(dest, src);
+}
+
+static inline void ploytec_decode_frame(u8 *dest, const u8 *src)
+{
+	ploytec_decode_s24_3le_pack32(dest, src);
+}
+
+#endif
+
+/**
+ * ploytec_initialize_codec() - Initialize the codec's pre-computed lookup tables
+ *
+ * Builds the bit-spread lookup table(s) used by the optimized encoder/decoder.
+ * Must be called once (e.g. at driver probe) before any call to
+ * ploytec_encode_batch() or ploytec_decode_batch().
+ *
+ * Return: which codec variant this build selected, for the caller to log.
+ */
+enum ploytec_codec_variant ploytec_initialize_codec(void)
+{
+	ploytec_codec_init_tables();
+	return ploytec_codec_selected;
+}
+
+/**
+ * ploytec_encode_batch() - Encode a number of 4-channel S24_3LE to 48-byte Ploytec frames
+ * @dest: n_frames * 48-byte destination buffer (Ploytec playback frame)
+ * @src: n_frames * 12-byte source buffer (4 channels * 3 bytes)
+ * @n_frames: number of frames to process in this batch
+ */
+void ploytec_encode_batch(u8 *dest, const u8 *src, const int n_frames)
+{
+	for (int f = 0; f < n_frames; f++) {
+		ploytec_encode_frame(dest, src);
+		dest += PLOYTEC_PLAYBACK_FRAME_SIZE;
+		src += PLOYTEC_PLAYBACK_PCM_FRAME_SIZE;
+	}
+}
+
+/**
+ * ploytec_decode_batch() - Decode a number of 64-byte Ploytec frames to 6-channel S24_3LE
+ * @dest: n_frames * 18-byte destination buffer (6 channels * 3 bytes)
+ * @src: n_frames * 64-byte source buffer
+ * @n_frames: number of frames to process in this batch
+ */
+void ploytec_decode_batch(u8 *dest, const u8 *src, const int n_frames)
+{
+	for (int f = 0; f < n_frames; f++) {
+		ploytec_decode_frame(dest, src);
+		dest += PLOYTEC_CAPTURE_PCM_FRAME_SIZE;
+		src += PLOYTEC_CAPTURE_FRAME_SIZE;
+	}
+}
diff --git a/sound/usb/jockey3/ploytec_codec.h b/sound/usb/jockey3/ploytec_codec.h
new file mode 100644
index 0000000000000..40917f49eae75
--- /dev/null
+++ b/sound/usb/jockey3/ploytec_codec.h
@@ -0,0 +1,44 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ *   ALSA driver for Reloop Jockey 3 devices
+ *   Ploytec PCM encoding/decoding functions for the S24_3LE format (3 bytes per sample)
+ *
+ *   Copyright (c) 2026 by Frank van de Pol <fvdpol@gmail.com>
+ */
+
+#ifndef __SOUND_USB_JOCKEY3_PLOYTEC_CODEC_H
+#define __SOUND_USB_JOCKEY3_PLOYTEC_CODEC_H
+
+#include <linux/types.h>
+
+/*
+ * Playback: 4 channels, capture: 6 channels, both S24_3LE (3 bytes/sample).
+ * The *_FRAME_SIZE values are the Ploytec wire format; the *_PCM_FRAME_SIZE
+ * values are the corresponding ALSA-side frame.
+ */
+#define PLOYTEC_PLAYBACK_FRAMES		10	// number of samples per frame
+#define PLOYTEC_PLAYBACK_FRAME_SIZE	48
+#define PLOYTEC_PLAYBACK_PCM_FRAME_SIZE	12	// 4 channels * 3 bytes
+
+#define PLOYTEC_CAPTURE_FRAMES		8	// number of samples per frame
+#define PLOYTEC_CAPTURE_FRAME_SIZE	64
+#define PLOYTEC_CAPTURE_PCM_FRAME_SIZE	18	// 6 channels * 3 bytes
+
+/**
+ * enum ploytec_codec_variant - which encode/decode implementation got selected
+ * @PLOYTEC_CODEC_PORTABLE: portable reference implementation
+ * @PLOYTEC_CODEC_OPTIMIZED_64BIT: lookup-table/bit-packing codec, 64-bit build
+ * @PLOYTEC_CODEC_OPTIMIZED_32BIT: lookup-table/bit-packing codec, 32-bit build
+ */
+enum ploytec_codec_variant {
+	PLOYTEC_CODEC_PORTABLE,
+	PLOYTEC_CODEC_OPTIMIZED_64BIT,
+	PLOYTEC_CODEC_OPTIMIZED_32BIT,
+};
+
+/* batching of the sample processing */
+enum ploytec_codec_variant ploytec_initialize_codec(void);
+void ploytec_encode_batch(u8 *dest, const u8 *src, const int n_frames);
+void ploytec_decode_batch(u8 *dest, const u8 *src, const int n_frames);
+
+#endif /* __SOUND_USB_JOCKEY3_PLOYTEC_CODEC_H */
diff --git a/sound/usb/jockey3/ploytec_codec_kunit.c b/sound/usb/jockey3/ploytec_codec_kunit.c
new file mode 100644
index 0000000000000..28ca832cfec23
--- /dev/null
+++ b/sound/usb/jockey3/ploytec_codec_kunit.c
@@ -0,0 +1,871 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *   ALSA driver for Reloop Jockey 3 devices
+ *   KUnit tests for the Ploytec bit-plane codec
+ *
+ *   Copyright (c) 2026 by Frank van de Pol <fvdpol@gmail.com>
+ */
+
+/**
+ * DOC: Proving the Ploytec codec correct
+ *
+ * ploytec_codec.c carries three implementations of one bit-scatter/gather
+ * algorithm - a portable reference plus 32-bit and 64-bit SWAR variants that
+ * use magic-multiplier tricks and 256-entry lookup tables. Exactly one is
+ * compiled into any given build, chosen by CONFIG_64BIT and
+ * CONFIG_SND_USB_JOCKEY3_REFERENCE_CODEC. The optimized paths are 4-10x
+ * faster and are what every user actually runs, but they are not obviously
+ * correct by inspection.
+ *
+ * These tests validate whichever variant the build selected, through the
+ * public ploytec_encode_batch() / ploytec_decode_batch() entry points. No
+ * symbols are exported and the codec is not modified to be testable; the
+ * test object is simply linked into the same module.
+ *
+ * The strategy is stronger than sampling random inputs:
+ *
+ * 1. The oracle is a *declarative bit map* (ploytec_encode_map[] and
+ *    ploytec_decode_map[] below), not a second copy of the driver's loops.
+ *    Because it states the format as data rather than as control flow, a
+ *    transcription error cannot propagate symmetrically into both.
+ *
+ * 2. The codec is a pure bit permutation, so as a function over GF(2) it is
+ *    linear. ploytec_test_*_is_linear() establishes that linearity holds, and
+ *    ploytec_test_*_permutation_complete() measures the map's action on every
+ *    single basis vector. A linear map is *fully determined* by its action on
+ *    the basis, so together those two cases pin down the result for all 2^96
+ *    (encode) and 2^512 (decode) possible inputs.
+ *
+ *    The residual risk is a non-linearity that the sampled pairs happen to
+ *    miss; the tests are not a formal proof, but they are far more than spot
+ *    checks.
+ *
+ * 3. Golden vectors generated by an independent Python model of the format
+ *    (tests/codec/ploytec_model.py) guard against the whole family drifting
+ *    away from the wire format together.
+ *
+ * Randomized cases use a small seeded xorshift generator rather than the
+ * kernel RNG, so every architecture exercises identical data and any failure
+ * reproduces exactly.
+ */
+
+#include <kunit/test.h>
+#include <linux/string.h>
+#include "ploytec_codec.h"
+#include "ploytec_codec_test_vectors.h"
+
+/* Largest batch either direction is ever asked for, from jockey3.c */
+#define PLOYTEC_TEST_MAX_ENCODE_FRAMES	PLOYTEC_PLAYBACK_FRAMES
+#define PLOYTEC_TEST_MAX_DECODE_FRAMES	PLOYTEC_CAPTURE_FRAMES
+
+/* How many random frames the sampling-based cases run through */
+#define PLOYTEC_TEST_RANDOM_FRAMES	4096
+#define PLOYTEC_TEST_LINEARITY_PAIRS	2048
+
+/* Guard region used to catch writes outside the destination buffer */
+#define PLOYTEC_TEST_GUARD		64
+#define PLOYTEC_TEST_GUARD_BYTE		0xA5
+
+/* Wire bytes the capture decoder must never read */
+#define PLOYTEC_TEST_UNUSED0_START	0x18
+#define PLOYTEC_TEST_UNUSED0_END	0x20
+#define PLOYTEC_TEST_UNUSED1_START	0x38
+#define PLOYTEC_TEST_UNUSED1_END	0x40
+
+/*
+ * The format, stated as data.
+ *
+ * Playback packs 4 channels into 48 wire bytes as two pairs: (ch0, ch2) in
+ * bytes 0..23 and (ch1, ch3) in bytes 24..47. Each 24-byte block holds three
+ * 8-byte planes ordered most-significant sample byte first. Within a plane,
+ * wire byte i carries bit (7 - i) of the sample byte, and a channel's index
+ * within its pair picks the bit of the wire byte it lands in.
+ */
+struct ploytec_encode_map_entry {
+	u8 src_idx;	// byte of the 12-byte PCM frame
+	u8 dst_base;	// first wire byte of the plane it scatters into
+	u8 dst_bit;	// bit of each wire byte it occupies
+};
+
+static const struct ploytec_encode_map_entry ploytec_encode_map[] = {
+	{  2,  0, 0 }, {  8,  0, 1 },	/* ch0 / ch2, most significant byte */
+	{  1,  8, 0 }, {  7,  8, 1 },	/* ch0 / ch2, middle byte */
+	{  0, 16, 0 }, {  6, 16, 1 },	/* ch0 / ch2, least significant byte */
+	{  5, 24, 0 }, { 11, 24, 1 },	/* ch1 / ch3, most significant byte */
+	{  4, 32, 0 }, { 10, 32, 1 },	/* ch1 / ch3, middle byte */
+	{  3, 40, 0 }, {  9, 40, 1 },	/* ch1 / ch3, least significant byte */
+};
+
+/*
+ * Capture unpacks 6 channels from 48 of the 64 wire bytes, as two groups of
+ * three: group 0 at 0x00..0x17 holds ch0, ch2, ch4 and group 1 at
+ * 0x20..0x37 holds ch1, ch3, ch5. Plane ordering matches playback, and a
+ * channel's index within its group picks the bit of the wire byte it was
+ * packed into. Wire bytes 0x18..0x1f and 0x38..0x3f carry nothing.
+ */
+struct ploytec_decode_map_entry {
+	u8 dst_idx;	// byte of the 18-byte PCM frame
+	u8 src_base;	// first wire byte of the plane it gathers from
+	u8 src_bit;	// bit of each wire byte it was packed into
+};
+
+static const struct ploytec_decode_map_entry ploytec_decode_map[] = {
+	{ 0x00, 0x10, 0 }, { 0x01, 0x08, 0 }, { 0x02, 0x00, 0 },	/* ch0 */
+	{ 0x03, 0x30, 0 }, { 0x04, 0x28, 0 }, { 0x05, 0x20, 0 },	/* ch1 */
+	{ 0x06, 0x10, 1 }, { 0x07, 0x08, 1 }, { 0x08, 0x00, 1 },	/* ch2 */
+	{ 0x09, 0x30, 1 }, { 0x0A, 0x28, 1 }, { 0x0B, 0x20, 1 },	/* ch3 */
+	{ 0x0C, 0x10, 2 }, { 0x0D, 0x08, 2 }, { 0x0E, 0x00, 2 },	/* ch4 */
+	{ 0x0F, 0x30, 2 }, { 0x10, 0x28, 2 }, { 0x11, 0x20, 2 },	/* ch5 */
+};
+
+/**
+ * ploytec_oracle_encode() - Encode one frame straight from the bit map
+ * @dest: 48-byte destination buffer
+ * @src: 12-byte source buffer
+ */
+static void ploytec_oracle_encode(u8 *dest, const u8 *src)
+{
+	memset(dest, 0, PLOYTEC_PLAYBACK_FRAME_SIZE);
+
+	for (size_t e = 0; e < ARRAY_SIZE(ploytec_encode_map); e++) {
+		const struct ploytec_encode_map_entry *m = &ploytec_encode_map[e];
+
+		for (int i = 0; i < 8; i++)
+			dest[m->dst_base + i] |=
+				((src[m->src_idx] >> (7 - i)) & 1) << m->dst_bit;
+	}
+}
+
+/**
+ * ploytec_oracle_decode() - Decode one frame straight from the bit map
+ * @dest: 18-byte destination buffer
+ * @src: 64-byte source buffer
+ */
+static void ploytec_oracle_decode(u8 *dest, const u8 *src)
+{
+	memset(dest, 0, PLOYTEC_CAPTURE_PCM_FRAME_SIZE);
+
+	for (size_t e = 0; e < ARRAY_SIZE(ploytec_decode_map); e++) {
+		const struct ploytec_decode_map_entry *m = &ploytec_decode_map[e];
+
+		for (int i = 0; i < 8; i++)
+			dest[m->dst_idx] |=
+				((src[m->src_base + i] >> m->src_bit) & 1) << (7 - i);
+	}
+}
+
+/* xorshift64*, seeded per test; see the determinism note at the top of this file. */
+struct ploytec_test_rng {
+	u64 state;
+};
+
+static void ploytec_rng_init(struct ploytec_test_rng *rng, u64 seed)
+{
+	rng->state = seed ? seed : 1;
+}
+
+static u64 ploytec_rng_next(struct ploytec_test_rng *rng)
+{
+	u64 x = rng->state;
+
+	x ^= x >> 12;
+	x ^= x << 25;
+	x ^= x >> 27;
+	rng->state = x;
+
+	return x * 0x2545F4914F6CDD1DULL;
+}
+
+static void ploytec_rng_fill(struct ploytec_test_rng *rng, u8 *buf, size_t len)
+{
+	size_t off = 0;
+
+	while (off < len) {
+		u64 v = ploytec_rng_next(rng);
+
+		for (int i = 0; i < 8 && off < len; i++, off++)
+			buf[off] = (v >> (i * 8)) & 0xFF;
+	}
+}
+
+/* Is this wire byte one the capture decoder is allowed to read? */
+static bool ploytec_capture_byte_used(int idx)
+{
+	if (idx >= PLOYTEC_TEST_UNUSED0_START && idx < PLOYTEC_TEST_UNUSED0_END)
+		return false;
+	if (idx >= PLOYTEC_TEST_UNUSED1_START && idx < PLOYTEC_TEST_UNUSED1_END)
+		return false;
+
+	return true;
+}
+
+/* Count set bits in a buffer, recording the position of the last one seen. */
+static int ploytec_count_set_bits(const u8 *buf, size_t len, int *last_pos)
+{
+	int count = 0;
+
+	for (size_t i = 0; i < len; i++) {
+		for (int b = 0; b < 8; b++) {
+			if (buf[i] & (1 << b)) {
+				count++;
+				if (last_pos)
+					*last_pos = i * 8 + b;
+			}
+		}
+	}
+
+	return count;
+}
+
+/* --- Golden vectors from the independent Python model --------------------- */
+
+static void ploytec_encode_vector_desc(const struct ploytec_encode_vector *v,
+				       char *desc)
+{
+	strscpy(desc, v->name, KUNIT_PARAM_DESC_SIZE);
+}
+
+KUNIT_ARRAY_PARAM(ploytec_encode_vector, ploytec_encode_vectors,
+		  ploytec_encode_vector_desc);
+
+static void ploytec_decode_vector_desc(const struct ploytec_decode_vector *v,
+				       char *desc)
+{
+	strscpy(desc, v->name, KUNIT_PARAM_DESC_SIZE);
+}
+
+KUNIT_ARRAY_PARAM(ploytec_decode_vector, ploytec_decode_vectors,
+		  ploytec_decode_vector_desc);
+
+static void ploytec_test_encode_known_vectors(struct kunit *test)
+{
+	const struct ploytec_encode_vector *v = test->param_value;
+	u8 dest[PLOYTEC_PLAYBACK_FRAME_SIZE];
+
+	memset(dest, 0xFF, sizeof(dest));
+	ploytec_encode_batch(dest, v->src, 1);
+
+	KUNIT_EXPECT_MEMEQ_MSG(test, dest, v->expect, sizeof(dest),
+			       "encode of vector '%s' does not match the model",
+			       v->name);
+}
+
+static void ploytec_test_decode_known_vectors(struct kunit *test)
+{
+	const struct ploytec_decode_vector *v = test->param_value;
+	u8 dest[PLOYTEC_CAPTURE_PCM_FRAME_SIZE];
+
+	memset(dest, 0xFF, sizeof(dest));
+	ploytec_decode_batch(dest, v->src, 1);
+
+	KUNIT_EXPECT_MEMEQ_MSG(test, dest, v->expect, sizeof(dest),
+			       "decode of vector '%s' does not match the model",
+			       v->name);
+}
+
+/* --- Agreement with the declarative bit map ------------------------------- */
+
+static void ploytec_test_encode_matches_oracle(struct kunit *test)
+{
+	u8 src[PLOYTEC_PLAYBACK_PCM_FRAME_SIZE];
+	u8 got[PLOYTEC_PLAYBACK_FRAME_SIZE];
+	u8 want[PLOYTEC_PLAYBACK_FRAME_SIZE];
+	struct ploytec_test_rng rng;
+
+	ploytec_rng_init(&rng, 0x0123456789ABCDEFULL);
+
+	for (int t = 0; t < PLOYTEC_TEST_RANDOM_FRAMES; t++) {
+		ploytec_rng_fill(&rng, src, sizeof(src));
+
+		/* Prefill with noise so a short write is visible. */
+		memset(got, 0x5A, sizeof(got));
+		ploytec_encode_batch(got, src, 1);
+		ploytec_oracle_encode(want, src);
+
+		KUNIT_ASSERT_MEMEQ_MSG(test, got, want, sizeof(got),
+				       "encode disagrees with the bit map at iteration %d",
+				       t);
+	}
+}
+
+static void ploytec_test_decode_matches_oracle(struct kunit *test)
+{
+	u8 src[PLOYTEC_CAPTURE_FRAME_SIZE];
+	u8 got[PLOYTEC_CAPTURE_PCM_FRAME_SIZE];
+	u8 want[PLOYTEC_CAPTURE_PCM_FRAME_SIZE];
+	struct ploytec_test_rng rng;
+
+	ploytec_rng_init(&rng, 0x0123456789ABCDEFULL);
+
+	for (int t = 0; t < PLOYTEC_TEST_RANDOM_FRAMES; t++) {
+		ploytec_rng_fill(&rng, src, sizeof(src));
+
+		memset(got, 0x5A, sizeof(got));
+		ploytec_decode_batch(got, src, 1);
+		ploytec_oracle_decode(want, src);
+
+		KUNIT_ASSERT_MEMEQ_MSG(test, got, want, sizeof(got),
+				       "decode disagrees with the bit map at iteration %d",
+				       t);
+	}
+}
+
+/* --- The map is a complete, injective bit permutation --------------------- */
+
+/*
+ * Encode: feed a one-hot input for each of the 96 input bits. Each must land
+ * on exactly one output bit, at the position the map predicts, and no two
+ * inputs may collide. That determines the map's action on every basis vector.
+ */
+static void ploytec_test_encode_permutation_complete(struct kunit *test)
+{
+	u8 src[PLOYTEC_PLAYBACK_PCM_FRAME_SIZE];
+	u8 dest[PLOYTEC_PLAYBACK_FRAME_SIZE];
+	DECLARE_BITMAP(seen, PLOYTEC_PLAYBACK_FRAME_SIZE * 8);
+	int covered = 0;
+
+	bitmap_zero(seen, PLOYTEC_PLAYBACK_FRAME_SIZE * 8);
+
+	for (int byte = 0; byte < PLOYTEC_PLAYBACK_PCM_FRAME_SIZE; byte++) {
+		for (int bit = 0; bit < 8; bit++) {
+			int pos = -1, count, want_pos = -1;
+
+			memset(src, 0, sizeof(src));
+			src[byte] = 1 << bit;
+
+			memset(dest, 0, sizeof(dest));
+			ploytec_encode_batch(dest, src, 1);
+
+			count = ploytec_count_set_bits(dest, sizeof(dest), &pos);
+			KUNIT_ASSERT_EQ_MSG(test, count, 1,
+					    "src[%d] bit %d set %d output bits, expected 1",
+					    byte, bit, count);
+
+			/* Where the bit map says it should have landed. */
+			for (size_t e = 0; e < ARRAY_SIZE(ploytec_encode_map); e++) {
+				const struct ploytec_encode_map_entry *m =
+					&ploytec_encode_map[e];
+
+				if (m->src_idx == byte)
+					want_pos = (m->dst_base + (7 - bit)) * 8 +
+						   m->dst_bit;
+			}
+
+			KUNIT_ASSERT_EQ_MSG(test, pos, want_pos,
+					    "src[%d] bit %d landed at output bit %d, map says %d",
+					    byte, bit, pos, want_pos);
+			KUNIT_ASSERT_FALSE_MSG(test, test_bit(pos, seen),
+					       "output bit %d is written by two different input bits",
+					       pos);
+			__set_bit(pos, seen);
+			covered++;
+		}
+	}
+
+	KUNIT_EXPECT_EQ(test, covered, PLOYTEC_PLAYBACK_PCM_FRAME_SIZE * 8);
+}
+
+/*
+ * Decode: sweep all 512 input bits. The 144 meaningful ones must each land on
+ * a distinct output bit; every other input bit must be ignored entirely, which
+ * checks the unused byte ranges and unused bits in the same pass.
+ */
+static void ploytec_test_decode_permutation_complete(struct kunit *test)
+{
+	u8 src[PLOYTEC_CAPTURE_FRAME_SIZE];
+	u8 dest[PLOYTEC_CAPTURE_PCM_FRAME_SIZE];
+	DECLARE_BITMAP(seen, PLOYTEC_CAPTURE_PCM_FRAME_SIZE * 8);
+	int covered = 0;
+
+	bitmap_zero(seen, PLOYTEC_CAPTURE_PCM_FRAME_SIZE * 8);
+
+	for (int byte = 0; byte < PLOYTEC_CAPTURE_FRAME_SIZE; byte++) {
+		for (int bit = 0; bit < 8; bit++) {
+			bool meaningful = ploytec_capture_byte_used(byte) && bit < 3;
+			int pos = -1, count, want_pos = -1;
+
+			memset(src, 0, sizeof(src));
+			src[byte] = 1 << bit;
+
+			memset(dest, 0, sizeof(dest));
+			ploytec_decode_batch(dest, src, 1);
+
+			count = ploytec_count_set_bits(dest, sizeof(dest), &pos);
+
+			if (!meaningful) {
+				KUNIT_ASSERT_EQ_MSG(test, count, 0,
+						    "wire byte %#04x bit %d is unused but changed the output",
+						    byte, bit);
+				continue;
+			}
+
+			KUNIT_ASSERT_EQ_MSG(test, count, 1,
+					    "wire byte %#04x bit %d set %d output bits, expected 1",
+					    byte, bit, count);
+
+			for (size_t e = 0; e < ARRAY_SIZE(ploytec_decode_map); e++) {
+				const struct ploytec_decode_map_entry *m =
+					&ploytec_decode_map[e];
+
+				if (m->src_bit == bit && byte >= m->src_base &&
+				    byte < m->src_base + 8)
+					want_pos = m->dst_idx * 8 +
+						   (7 - (byte - m->src_base));
+			}
+
+			KUNIT_ASSERT_EQ_MSG(test, pos, want_pos,
+					    "wire byte %#04x bit %d landed at output bit %d, map says %d",
+					    byte, bit, pos, want_pos);
+			KUNIT_ASSERT_FALSE_MSG(test, test_bit(pos, seen),
+					       "output bit %d is written by two different wire bits",
+					       pos);
+			__set_bit(pos, seen);
+			covered++;
+		}
+	}
+
+	KUNIT_EXPECT_EQ(test, covered, PLOYTEC_CAPTURE_PCM_FRAME_SIZE * 8);
+}
+
+/* --- Linearity over GF(2) ------------------------------------------------- */
+
+static void ploytec_test_encode_is_linear(struct kunit *test)
+{
+	u8 a[PLOYTEC_PLAYBACK_PCM_FRAME_SIZE], b[PLOYTEC_PLAYBACK_PCM_FRAME_SIZE];
+	u8 ab[PLOYTEC_PLAYBACK_PCM_FRAME_SIZE];
+	u8 ea[PLOYTEC_PLAYBACK_FRAME_SIZE], eb[PLOYTEC_PLAYBACK_FRAME_SIZE];
+	u8 eab[PLOYTEC_PLAYBACK_FRAME_SIZE], want[PLOYTEC_PLAYBACK_FRAME_SIZE];
+	struct ploytec_test_rng rng;
+
+	ploytec_rng_init(&rng, 0xFEEDFACECAFEBEEFULL);
+
+	for (int t = 0; t < PLOYTEC_TEST_LINEARITY_PAIRS; t++) {
+		ploytec_rng_fill(&rng, a, sizeof(a));
+		ploytec_rng_fill(&rng, b, sizeof(b));
+
+		for (size_t i = 0; i < sizeof(ab); i++)
+			ab[i] = a[i] ^ b[i];
+
+		ploytec_encode_batch(ea, a, 1);
+		ploytec_encode_batch(eb, b, 1);
+		ploytec_encode_batch(eab, ab, 1);
+
+		for (size_t i = 0; i < sizeof(want); i++)
+			want[i] = ea[i] ^ eb[i];
+
+		KUNIT_ASSERT_MEMEQ_MSG(test, eab, want, sizeof(want),
+				       "encode(a^b) != encode(a)^encode(b) at iteration %d",
+				       t);
+	}
+}
+
+static void ploytec_test_decode_is_linear(struct kunit *test)
+{
+	u8 a[PLOYTEC_CAPTURE_FRAME_SIZE], b[PLOYTEC_CAPTURE_FRAME_SIZE];
+	u8 ab[PLOYTEC_CAPTURE_FRAME_SIZE];
+	u8 da[PLOYTEC_CAPTURE_PCM_FRAME_SIZE], db[PLOYTEC_CAPTURE_PCM_FRAME_SIZE];
+	u8 dab[PLOYTEC_CAPTURE_PCM_FRAME_SIZE], want[PLOYTEC_CAPTURE_PCM_FRAME_SIZE];
+	struct ploytec_test_rng rng;
+
+	ploytec_rng_init(&rng, 0xFEEDFACECAFEBEEFULL);
+
+	for (int t = 0; t < PLOYTEC_TEST_LINEARITY_PAIRS; t++) {
+		ploytec_rng_fill(&rng, a, sizeof(a));
+		ploytec_rng_fill(&rng, b, sizeof(b));
+
+		for (size_t i = 0; i < sizeof(ab); i++)
+			ab[i] = a[i] ^ b[i];
+
+		ploytec_decode_batch(da, a, 1);
+		ploytec_decode_batch(db, b, 1);
+		ploytec_decode_batch(dab, ab, 1);
+
+		for (size_t i = 0; i < sizeof(want); i++)
+			want[i] = da[i] ^ db[i];
+
+		KUNIT_ASSERT_MEMEQ_MSG(test, dab, want, sizeof(want),
+				       "decode(a^b) != decode(a)^decode(b) at iteration %d",
+				       t);
+	}
+}
+
+/* --- Batching ------------------------------------------------------------- */
+
+static void ploytec_test_encode_batch_equals_single(struct kunit *test)
+{
+	u8 src[PLOYTEC_TEST_MAX_ENCODE_FRAMES * PLOYTEC_PLAYBACK_PCM_FRAME_SIZE];
+	u8 batched[PLOYTEC_TEST_MAX_ENCODE_FRAMES * PLOYTEC_PLAYBACK_FRAME_SIZE];
+	u8 single[PLOYTEC_TEST_MAX_ENCODE_FRAMES * PLOYTEC_PLAYBACK_FRAME_SIZE];
+	struct ploytec_test_rng rng;
+
+	ploytec_rng_init(&rng, 0xD1CE0FF1CE0FF1CEULL);
+	ploytec_rng_fill(&rng, src, sizeof(src));
+
+	for (int n = 1; n <= PLOYTEC_TEST_MAX_ENCODE_FRAMES; n++) {
+		memset(batched, 0, sizeof(batched));
+		memset(single, 0, sizeof(single));
+
+		ploytec_encode_batch(batched, src, n);
+
+		for (int f = 0; f < n; f++)
+			ploytec_encode_batch(single + f * PLOYTEC_PLAYBACK_FRAME_SIZE,
+					     src + f * PLOYTEC_PLAYBACK_PCM_FRAME_SIZE,
+					     1);
+
+		KUNIT_EXPECT_MEMEQ_MSG(test, batched, single,
+				       n * PLOYTEC_PLAYBACK_FRAME_SIZE,
+				       "batch of %d differs from %d single-frame calls",
+				       n, n);
+	}
+}
+
+static void ploytec_test_decode_batch_equals_single(struct kunit *test)
+{
+	u8 src[PLOYTEC_TEST_MAX_DECODE_FRAMES * PLOYTEC_CAPTURE_FRAME_SIZE];
+	u8 batched[PLOYTEC_TEST_MAX_DECODE_FRAMES * PLOYTEC_CAPTURE_PCM_FRAME_SIZE];
+	u8 single[PLOYTEC_TEST_MAX_DECODE_FRAMES * PLOYTEC_CAPTURE_PCM_FRAME_SIZE];
+	struct ploytec_test_rng rng;
+
+	ploytec_rng_init(&rng, 0xD1CE0FF1CE0FF1CEULL);
+	ploytec_rng_fill(&rng, src, sizeof(src));
+
+	for (int n = 1; n <= PLOYTEC_TEST_MAX_DECODE_FRAMES; n++) {
+		memset(batched, 0, sizeof(batched));
+		memset(single, 0, sizeof(single));
+
+		ploytec_decode_batch(batched, src, n);
+
+		for (int f = 0; f < n; f++)
+			ploytec_decode_batch(single + f * PLOYTEC_CAPTURE_PCM_FRAME_SIZE,
+					     src + f * PLOYTEC_CAPTURE_FRAME_SIZE,
+					     1);
+
+		KUNIT_EXPECT_MEMEQ_MSG(test, batched, single,
+				       n * PLOYTEC_CAPTURE_PCM_FRAME_SIZE,
+				       "batch of %d differs from %d single-frame calls",
+				       n, n);
+	}
+}
+
+/*
+ * jockey3.c computes the batch size from the ALSA ring-buffer wrap and can in
+ * principle arrive at zero, so the codec must treat that as a no-op rather
+ * than walking off the buffer.
+ */
+static void ploytec_test_encode_batch_zero_frames(struct kunit *test)
+{
+	u8 src[PLOYTEC_PLAYBACK_PCM_FRAME_SIZE] = { 0 };
+	u8 dest[PLOYTEC_PLAYBACK_FRAME_SIZE];
+
+	memset(dest, PLOYTEC_TEST_GUARD_BYTE, sizeof(dest));
+	ploytec_encode_batch(dest, src, 0);
+
+	for (size_t i = 0; i < sizeof(dest); i++)
+		KUNIT_EXPECT_EQ_MSG(test, dest[i], PLOYTEC_TEST_GUARD_BYTE,
+				    "encode of 0 frames wrote to dest[%zu]", i);
+}
+
+static void ploytec_test_decode_batch_zero_frames(struct kunit *test)
+{
+	u8 src[PLOYTEC_CAPTURE_FRAME_SIZE] = { 0 };
+	u8 dest[PLOYTEC_CAPTURE_PCM_FRAME_SIZE];
+
+	memset(dest, PLOYTEC_TEST_GUARD_BYTE, sizeof(dest));
+	ploytec_decode_batch(dest, src, 0);
+
+	for (size_t i = 0; i < sizeof(dest); i++)
+		KUNIT_EXPECT_EQ_MSG(test, dest[i], PLOYTEC_TEST_GUARD_BYTE,
+				    "decode of 0 frames wrote to dest[%zu]", i);
+}
+
+static void ploytec_test_encode_batch_no_overrun(struct kunit *test)
+{
+	const size_t payload = PLOYTEC_TEST_MAX_ENCODE_FRAMES *
+			       PLOYTEC_PLAYBACK_FRAME_SIZE;
+	u8 src[PLOYTEC_TEST_MAX_ENCODE_FRAMES * PLOYTEC_PLAYBACK_PCM_FRAME_SIZE];
+	struct ploytec_test_rng rng;
+	u8 *buf;
+
+	buf = kunit_kzalloc(test, payload + 2 * PLOYTEC_TEST_GUARD, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, buf);
+
+	ploytec_rng_init(&rng, 0xBEEFCAFEBEEFCAFEULL);
+	ploytec_rng_fill(&rng, src, sizeof(src));
+
+	memset(buf, PLOYTEC_TEST_GUARD_BYTE, payload + 2 * PLOYTEC_TEST_GUARD);
+	ploytec_encode_batch(buf + PLOYTEC_TEST_GUARD, src,
+			     PLOYTEC_TEST_MAX_ENCODE_FRAMES);
+
+	for (int i = 0; i < PLOYTEC_TEST_GUARD; i++) {
+		KUNIT_EXPECT_EQ_MSG(test, buf[i], PLOYTEC_TEST_GUARD_BYTE,
+				    "encode wrote %d bytes before the destination",
+				    PLOYTEC_TEST_GUARD - i);
+		KUNIT_EXPECT_EQ_MSG(test, buf[PLOYTEC_TEST_GUARD + payload + i],
+				    PLOYTEC_TEST_GUARD_BYTE,
+				    "encode wrote %d bytes past the destination",
+				    i + 1);
+	}
+}
+
+static void ploytec_test_decode_batch_no_overrun(struct kunit *test)
+{
+	const size_t payload = PLOYTEC_TEST_MAX_DECODE_FRAMES *
+			       PLOYTEC_CAPTURE_PCM_FRAME_SIZE;
+	u8 src[PLOYTEC_TEST_MAX_DECODE_FRAMES * PLOYTEC_CAPTURE_FRAME_SIZE];
+	struct ploytec_test_rng rng;
+	u8 *buf;
+
+	buf = kunit_kzalloc(test, payload + 2 * PLOYTEC_TEST_GUARD, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, buf);
+
+	ploytec_rng_init(&rng, 0xBEEFCAFEBEEFCAFEULL);
+	ploytec_rng_fill(&rng, src, sizeof(src));
+
+	memset(buf, PLOYTEC_TEST_GUARD_BYTE, payload + 2 * PLOYTEC_TEST_GUARD);
+	ploytec_decode_batch(buf + PLOYTEC_TEST_GUARD, src,
+			     PLOYTEC_TEST_MAX_DECODE_FRAMES);
+
+	for (int i = 0; i < PLOYTEC_TEST_GUARD; i++) {
+		KUNIT_EXPECT_EQ_MSG(test, buf[i], PLOYTEC_TEST_GUARD_BYTE,
+				    "decode wrote %d bytes before the destination",
+				    PLOYTEC_TEST_GUARD - i);
+		KUNIT_EXPECT_EQ_MSG(test, buf[PLOYTEC_TEST_GUARD + payload + i],
+				    PLOYTEC_TEST_GUARD_BYTE,
+				    "decode wrote %d bytes past the destination",
+				    i + 1);
+	}
+}
+
+/* --- Alignment ------------------------------------------------------------ */
+
+/*
+ * The optimized codecs reach for 32- and 64-bit words through the unaligned
+ * accessors. In the driver the wire buffer happens to be 8-byte aligned, so a
+ * misuse of those accessors would go unnoticed on x86 and only surface as a
+ * fault on a strict-alignment architecture. Run every byte offset explicitly.
+ */
+static void ploytec_test_encode_unaligned_buffers(struct kunit *test)
+{
+	u8 reference[PLOYTEC_PLAYBACK_FRAME_SIZE];
+	u8 src[PLOYTEC_PLAYBACK_PCM_FRAME_SIZE];
+	struct ploytec_test_rng rng;
+	u8 *src_buf, *dst_buf;
+
+	src_buf = kunit_kzalloc(test, PLOYTEC_PLAYBACK_PCM_FRAME_SIZE + 8, GFP_KERNEL);
+	dst_buf = kunit_kzalloc(test, PLOYTEC_PLAYBACK_FRAME_SIZE + 8, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, src_buf);
+	KUNIT_ASSERT_NOT_NULL(test, dst_buf);
+
+	ploytec_rng_init(&rng, 0x1234ABCD5678EF90ULL);
+	ploytec_rng_fill(&rng, src, sizeof(src));
+	ploytec_oracle_encode(reference, src);
+
+	for (int src_off = 0; src_off < 8; src_off++) {
+		for (int dst_off = 0; dst_off < 8; dst_off++) {
+			memcpy(src_buf + src_off, src, sizeof(src));
+			memset(dst_buf, 0, PLOYTEC_PLAYBACK_FRAME_SIZE + 8);
+
+			ploytec_encode_batch(dst_buf + dst_off, src_buf + src_off, 1);
+
+			KUNIT_ASSERT_MEMEQ_MSG(test, dst_buf + dst_off, reference,
+					       PLOYTEC_PLAYBACK_FRAME_SIZE,
+					       "encode differs at src offset %d, dst offset %d",
+					       src_off, dst_off);
+		}
+	}
+}
+
+static void ploytec_test_decode_unaligned_buffers(struct kunit *test)
+{
+	u8 reference[PLOYTEC_CAPTURE_PCM_FRAME_SIZE];
+	u8 src[PLOYTEC_CAPTURE_FRAME_SIZE];
+	struct ploytec_test_rng rng;
+	u8 *src_buf, *dst_buf;
+
+	src_buf = kunit_kzalloc(test, PLOYTEC_CAPTURE_FRAME_SIZE + 8, GFP_KERNEL);
+	dst_buf = kunit_kzalloc(test, PLOYTEC_CAPTURE_PCM_FRAME_SIZE + 8, GFP_KERNEL);
+	KUNIT_ASSERT_NOT_NULL(test, src_buf);
+	KUNIT_ASSERT_NOT_NULL(test, dst_buf);
+
+	ploytec_rng_init(&rng, 0x1234ABCD5678EF90ULL);
+	ploytec_rng_fill(&rng, src, sizeof(src));
+	ploytec_oracle_decode(reference, src);
+
+	for (int src_off = 0; src_off < 8; src_off++) {
+		for (int dst_off = 0; dst_off < 8; dst_off++) {
+			memcpy(src_buf + src_off, src, sizeof(src));
+			memset(dst_buf, 0, PLOYTEC_CAPTURE_PCM_FRAME_SIZE + 8);
+
+			ploytec_decode_batch(dst_buf + dst_off, src_buf + src_off, 1);
+
+			KUNIT_ASSERT_MEMEQ_MSG(test, dst_buf + dst_off, reference,
+					       PLOYTEC_CAPTURE_PCM_FRAME_SIZE,
+					       "decode differs at src offset %d, dst offset %d",
+					       src_off, dst_off);
+		}
+	}
+}
+
+/* --- Format invariants ---------------------------------------------------- */
+
+static void ploytec_test_encode_zero_input(struct kunit *test)
+{
+	u8 src[PLOYTEC_PLAYBACK_PCM_FRAME_SIZE] = { 0 };
+	u8 dest[PLOYTEC_PLAYBACK_FRAME_SIZE];
+	u8 want[PLOYTEC_PLAYBACK_FRAME_SIZE] = { 0 };
+
+	memset(dest, 0xFF, sizeof(dest));
+	ploytec_encode_batch(dest, src, 1);
+
+	KUNIT_EXPECT_MEMEQ_MSG(test, dest, want, sizeof(dest),
+			       "silence did not encode to an all-zero wire frame");
+}
+
+/*
+ * Playback packs only two channels into each wire byte, so bits 2..7 carry
+ * nothing. jockey3.c relies on this: it writes the MIDI and sync bytes into
+ * the same 512-byte packet and would be corrupted by a codec that spilled
+ * into the upper bits.
+ */
+static void ploytec_test_encode_reserved_bits_clear(struct kunit *test)
+{
+	u8 src[PLOYTEC_PLAYBACK_PCM_FRAME_SIZE];
+	u8 dest[PLOYTEC_PLAYBACK_FRAME_SIZE];
+	struct ploytec_test_rng rng;
+
+	ploytec_rng_init(&rng, 0x0F0F0F0FF0F0F0F0ULL);
+
+	/* All ones is the worst case, then sample the space. */
+	memset(src, 0xFF, sizeof(src));
+
+	for (int t = 0; t < 256; t++) {
+		memset(dest, 0, sizeof(dest));
+		ploytec_encode_batch(dest, src, 1);
+
+		for (size_t i = 0; i < sizeof(dest); i++)
+			KUNIT_ASSERT_EQ_MSG(test, dest[i] & 0xFC, 0,
+					    "wire byte %zu has reserved bits set (%#04x)",
+					    i, dest[i]);
+
+		ploytec_rng_fill(&rng, src, sizeof(src));
+	}
+}
+
+static void ploytec_test_decode_ignores_unused_bytes(struct kunit *test)
+{
+	u8 src[PLOYTEC_CAPTURE_FRAME_SIZE];
+	u8 probe[PLOYTEC_CAPTURE_FRAME_SIZE];
+	u8 want[PLOYTEC_CAPTURE_PCM_FRAME_SIZE];
+	u8 got[PLOYTEC_CAPTURE_PCM_FRAME_SIZE];
+	struct ploytec_test_rng rng;
+
+	ploytec_rng_init(&rng, 0xABCDEF0123456789ULL);
+	ploytec_rng_fill(&rng, src, sizeof(src));
+	ploytec_decode_batch(want, src, 1);
+
+	for (int t = 0; t < 256; t++) {
+		memcpy(probe, src, sizeof(probe));
+
+		/* Scribble over everything the decoder should not be reading. */
+		for (size_t i = 0; i < sizeof(probe); i++) {
+			if (!ploytec_capture_byte_used(i))
+				probe[i] = ploytec_rng_next(&rng) & 0xFF;
+		}
+
+		ploytec_decode_batch(got, probe, 1);
+		KUNIT_ASSERT_MEMEQ_MSG(test, got, want, sizeof(want),
+				       "decode result changed when unused wire bytes changed");
+	}
+}
+
+static void ploytec_test_decode_ignores_unused_bits(struct kunit *test)
+{
+	u8 src[PLOYTEC_CAPTURE_FRAME_SIZE];
+	u8 probe[PLOYTEC_CAPTURE_FRAME_SIZE];
+	u8 want[PLOYTEC_CAPTURE_PCM_FRAME_SIZE];
+	u8 got[PLOYTEC_CAPTURE_PCM_FRAME_SIZE];
+	struct ploytec_test_rng rng;
+
+	ploytec_rng_init(&rng, 0x9988776655443322ULL);
+	ploytec_rng_fill(&rng, src, sizeof(src));
+	ploytec_decode_batch(want, src, 1);
+
+	for (int t = 0; t < 256; t++) {
+		/* Only bits 0..2 of each wire byte carry channel data. */
+		for (size_t i = 0; i < sizeof(probe); i++)
+			probe[i] = (src[i] & 0x07) |
+				   (ploytec_rng_next(&rng) & 0xF8);
+
+		ploytec_decode_batch(got, probe, 1);
+		KUNIT_ASSERT_MEMEQ_MSG(test, got, want, sizeof(want),
+				       "decode result changed when unused wire bits changed");
+	}
+}
+
+/* --- Build wiring --------------------------------------------------------- */
+
+/*
+ * The three implementations are chosen by a preprocessor chain, and getting
+ * that chain wrong silently selects the wrong codec. That is not theoretical:
+ * the out-of-tree user-space harness built the 32-bit variant on x86_64 for
+ * months because CONFIG_64BIT was not defined in its build.
+ */
+static void ploytec_test_codec_variant_matches_build(struct kunit *test)
+{
+	enum ploytec_codec_variant variant = ploytec_initialize_codec();
+
+	if (IS_ENABLED(CONFIG_SND_USB_JOCKEY3_REFERENCE_CODEC))
+		KUNIT_EXPECT_EQ_MSG(test, variant, PLOYTEC_CODEC_PORTABLE,
+				    "REFERENCE_CODEC is set but the portable codec was not selected");
+	else if (IS_ENABLED(CONFIG_64BIT))
+		KUNIT_EXPECT_EQ_MSG(test, variant, PLOYTEC_CODEC_OPTIMIZED_64BIT,
+				    "64-bit build did not select the 64-bit codec");
+	else
+		KUNIT_EXPECT_EQ_MSG(test, variant, PLOYTEC_CODEC_OPTIMIZED_32BIT,
+				    "32-bit build did not select the 32-bit codec");
+}
+
+/* --- Suite ---------------------------------------------------------------- */
+
+static int ploytec_codec_suite_init(struct kunit_suite *suite)
+{
+	/* Builds the bit-spread lookup tables the optimized codecs need. */
+	ploytec_initialize_codec();
+
+	return 0;
+}
+
+static struct kunit_case ploytec_codec_test_cases[] = {
+	KUNIT_CASE_PARAM(ploytec_test_encode_known_vectors,
+			 ploytec_encode_vector_gen_params),
+	KUNIT_CASE_PARAM(ploytec_test_decode_known_vectors,
+			 ploytec_decode_vector_gen_params),
+	KUNIT_CASE(ploytec_test_encode_matches_oracle),
+	KUNIT_CASE(ploytec_test_decode_matches_oracle),
+	KUNIT_CASE(ploytec_test_encode_permutation_complete),
+	KUNIT_CASE(ploytec_test_decode_permutation_complete),
+	KUNIT_CASE(ploytec_test_encode_is_linear),
+	KUNIT_CASE(ploytec_test_decode_is_linear),
+	KUNIT_CASE(ploytec_test_encode_batch_equals_single),
+	KUNIT_CASE(ploytec_test_decode_batch_equals_single),
+	KUNIT_CASE(ploytec_test_encode_batch_zero_frames),
+	KUNIT_CASE(ploytec_test_decode_batch_zero_frames),
+	KUNIT_CASE(ploytec_test_encode_batch_no_overrun),
+	KUNIT_CASE(ploytec_test_decode_batch_no_overrun),
+	KUNIT_CASE(ploytec_test_encode_unaligned_buffers),
+	KUNIT_CASE(ploytec_test_decode_unaligned_buffers),
+	KUNIT_CASE(ploytec_test_encode_zero_input),
+	KUNIT_CASE(ploytec_test_encode_reserved_bits_clear),
+	KUNIT_CASE(ploytec_test_decode_ignores_unused_bytes),
+	KUNIT_CASE(ploytec_test_decode_ignores_unused_bits),
+	KUNIT_CASE(ploytec_test_codec_variant_matches_build),
+	{},
+};
+
+static struct kunit_suite ploytec_codec_suite = {
+	.name = "ploytec-codec",
+	.suite_init = ploytec_codec_suite_init,
+	.test_cases = ploytec_codec_test_cases,
+};
+
+kunit_test_suite(ploytec_codec_suite);
diff --git a/sound/usb/jockey3/ploytec_codec_test_vectors.h b/sound/usb/jockey3/ploytec_codec_test_vectors.h
new file mode 100644
index 0000000000000..894d5f830d7f2
--- /dev/null
+++ b/sound/usb/jockey3/ploytec_codec_test_vectors.h
@@ -0,0 +1,793 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Golden test vectors for the Ploytec bit-plane codec.
+ *
+ * GENERATED FILE - DO NOT EDIT.
+ *
+ * Produced by genvectors.py from ploytec_model.py, an independent model
+ * that derives the wire format structurally rather than from this driver's
+ * code, so the two cannot agree on a wrong answer. The same vectors drive a
+ * user-space bench, which is why they live in a header of their own.
+ *
+ * Both generator and model belong to the driver's test tooling and are not
+ * part of the kernel tree. They live under tests/codec at
+ * https://github.com/fvdpol/alsa-jockey3, where ./genvectors.py regenerates
+ * this file.
+ *
+ * ploytec_model.py sha256: 09fdf2831f9de572ee3a0e06b20807ee6235fc67ac79481469d616e965e8f0f3
+ *
+ * Copyright (c) 2026 by Frank van de Pol <fvdpol@gmail.com>
+ */
+
+#ifndef PLOYTEC_CODEC_TEST_VECTORS_H
+#define PLOYTEC_CODEC_TEST_VECTORS_H
+
+struct ploytec_encode_vector {
+	const char *name;
+	u8 src[12];
+	u8 expect[48];
+};
+
+struct ploytec_decode_vector {
+	const char *name;
+	u8 src[64];
+	u8 expect[18];
+};
+
+static const struct ploytec_encode_vector ploytec_encode_vectors[] = {
+	{
+		.name = "zeros",
+		.src = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		},
+		.expect = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		},
+	},
+	{
+		.name = "ones",
+		.src = {
+			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+		},
+		.expect = {
+			0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+			0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+			0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+			0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+		},
+	},
+	{
+		.name = "alternating_55",
+		.src = {
+			0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
+		},
+		.expect = {
+			0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03,
+			0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03,
+			0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03,
+			0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03,
+		},
+	},
+	{
+		.name = "alternating_aa",
+		.src = {
+			0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
+		},
+		.expect = {
+			0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00,
+			0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00,
+			0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00,
+			0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00,
+		},
+	},
+	{
+		.name = "ramp",
+		.src = {
+			0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
+		},
+		.expect = {
+			0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x02, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x02, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x02, 0x01, 0x02, 0x03, 0x00, 0x00, 0x00, 0x00,
+			0x02, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x03,
+		},
+	},
+	{
+		.name = "channel0_full",
+		.src = {
+			0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		},
+		.expect = {
+			0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+			0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		},
+	},
+	{
+		.name = "channel1_full",
+		.src = {
+			0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		},
+		.expect = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+			0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+		},
+	},
+	{
+		.name = "channel2_full",
+		.src = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00,
+		},
+		.expect = {
+			0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+			0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		},
+	},
+	{
+		.name = "channel3_full",
+		.src = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff,
+		},
+		.expect = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+			0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+		},
+	},
+	{
+		.name = "channel0_sign",
+		.src = {
+			0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		},
+		.expect = {
+			0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		},
+	},
+	{
+		.name = "channel1_sign",
+		.src = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		},
+		.expect = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		},
+	},
+	{
+		.name = "channel2_sign",
+		.src = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00,
+		},
+		.expect = {
+			0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		},
+	},
+	{
+		.name = "channel3_sign",
+		.src = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
+		},
+		.expect = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		},
+	},
+	{
+		.name = "distinct",
+		.src = {
+			0x56, 0x34, 0x12, 0xbc, 0x9a, 0x78, 0x12, 0xf0, 0xde, 0x78, 0x56, 0x34,
+		},
+		.expect = {
+			0x02, 0x02, 0x00, 0x03, 0x02, 0x02, 0x03, 0x00, 0x02, 0x02, 0x03, 0x03,
+			0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x01, 0x03, 0x00,
+			0x00, 0x01, 0x03, 0x03, 0x01, 0x02, 0x00, 0x00, 0x01, 0x02, 0x00, 0x03,
+			0x01, 0x02, 0x03, 0x00, 0x01, 0x02, 0x03, 0x03, 0x03, 0x01, 0x00, 0x00,
+		},
+	},
+	{
+		.name = "random00",
+		.src = {
+			0xc5, 0x21, 0x5c, 0x0d, 0x13, 0xea, 0x2d, 0xa6, 0x6b, 0x90, 0x77, 0x77,
+		},
+		.expect = {
+			0x00, 0x03, 0x02, 0x01, 0x03, 0x01, 0x02, 0x02, 0x02, 0x00, 0x03, 0x00,
+			0x00, 0x02, 0x02, 0x01, 0x01, 0x01, 0x02, 0x00, 0x02, 0x03, 0x00, 0x03,
+			0x01, 0x03, 0x03, 0x02, 0x01, 0x02, 0x03, 0x02, 0x00, 0x02, 0x02, 0x03,
+			0x00, 0x02, 0x03, 0x03, 0x02, 0x00, 0x00, 0x02, 0x01, 0x01, 0x00, 0x01,
+		},
+	},
+	{
+		.name = "random01",
+		.src = {
+			0xd2, 0x2b, 0xc8, 0xf8, 0x5d, 0x0f, 0xc4, 0x8b, 0xf0, 0x91, 0xa2, 0x96,
+		},
+		.expect = {
+			0x03, 0x03, 0x02, 0x02, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x00,
+			0x03, 0x00, 0x03, 0x03, 0x03, 0x03, 0x00, 0x01, 0x00, 0x02, 0x01, 0x00,
+			0x02, 0x00, 0x00, 0x02, 0x01, 0x03, 0x03, 0x01, 0x02, 0x01, 0x02, 0x01,
+			0x01, 0x01, 0x02, 0x01, 0x03, 0x01, 0x01, 0x03, 0x01, 0x00, 0x00, 0x02,
+		},
+	},
+	{
+		.name = "random02",
+		.src = {
+			0xc2, 0xc6, 0xd5, 0xf1, 0xf7, 0x91, 0xbb, 0xd9, 0xdb, 0x54, 0x93, 0x29,
+		},
+		.expect = {
+			0x03, 0x03, 0x00, 0x03, 0x02, 0x01, 0x02, 0x03, 0x03, 0x03, 0x00, 0x02,
+			0x02, 0x01, 0x01, 0x02, 0x03, 0x01, 0x02, 0x02, 0x02, 0x00, 0x03, 0x02,
+			0x01, 0x00, 0x02, 0x01, 0x02, 0x00, 0x00, 0x03, 0x03, 0x01, 0x01, 0x03,
+			0x00, 0x01, 0x03, 0x03, 0x01, 0x03, 0x01, 0x03, 0x00, 0x02, 0x00, 0x01,
+		},
+	},
+	{
+		.name = "random03",
+		.src = {
+			0x4f, 0xb4, 0xf2, 0x70, 0x17, 0x6f, 0xe3, 0x64, 0xc0, 0xcd, 0x51, 0xa4,
+		},
+		.expect = {
+			0x03, 0x03, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x03, 0x01,
+			0x00, 0x03, 0x00, 0x00, 0x02, 0x03, 0x02, 0x00, 0x01, 0x01, 0x03, 0x03,
+			0x02, 0x01, 0x03, 0x00, 0x01, 0x03, 0x01, 0x01, 0x00, 0x02, 0x00, 0x03,
+			0x00, 0x01, 0x01, 0x03, 0x02, 0x03, 0x01, 0x01, 0x02, 0x02, 0x00, 0x02,
+		},
+	},
+	{
+		.name = "random04",
+		.src = {
+			0x5d, 0xf8, 0xb4, 0xe8, 0x5a, 0x5b, 0xe5, 0x10, 0x69, 0xd0, 0x02, 0xa9,
+		},
+		.expect = {
+			0x01, 0x02, 0x03, 0x01, 0x02, 0x01, 0x00, 0x02, 0x01, 0x01, 0x01, 0x03,
+			0x01, 0x00, 0x00, 0x00, 0x02, 0x03, 0x02, 0x01, 0x01, 0x03, 0x00, 0x03,
+			0x02, 0x01, 0x02, 0x01, 0x03, 0x00, 0x01, 0x03, 0x00, 0x01, 0x00, 0x01,
+			0x01, 0x00, 0x03, 0x00, 0x03, 0x03, 0x01, 0x02, 0x01, 0x00, 0x00, 0x00,
+		},
+	},
+	{
+		.name = "random05",
+		.src = {
+			0x67, 0x49, 0x0c, 0x85, 0xfd, 0x37, 0xc7, 0xf9, 0xd8, 0xcb, 0x7d, 0x90,
+		},
+		.expect = {
+			0x02, 0x02, 0x00, 0x02, 0x03, 0x01, 0x00, 0x00, 0x02, 0x03, 0x02, 0x02,
+			0x03, 0x00, 0x00, 0x03, 0x02, 0x03, 0x01, 0x00, 0x00, 0x03, 0x03, 0x03,
+			0x02, 0x00, 0x01, 0x03, 0x00, 0x01, 0x01, 0x01, 0x01, 0x03, 0x03, 0x03,
+			0x03, 0x03, 0x00, 0x03, 0x03, 0x02, 0x00, 0x00, 0x02, 0x01, 0x02, 0x03,
+		},
+	},
+	{
+		.name = "random06",
+		.src = {
+			0x49, 0x2c, 0x4f, 0x41, 0x86, 0x56, 0xf0, 0x2f, 0xa1, 0x33, 0x30, 0xb9,
+		},
+		.expect = {
+			0x02, 0x01, 0x02, 0x00, 0x01, 0x01, 0x01, 0x03, 0x00, 0x00, 0x03, 0x00,
+			0x03, 0x03, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02, 0x01, 0x00, 0x00, 0x01,
+			0x02, 0x01, 0x02, 0x03, 0x02, 0x01, 0x01, 0x02, 0x01, 0x00, 0x02, 0x02,
+			0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x02, 0x02, 0x00, 0x00, 0x02, 0x03,
+		},
+	},
+	{
+		.name = "random07",
+		.src = {
+			0x4e, 0x19, 0xe5, 0x1a, 0xcd, 0x69, 0xf2, 0x66, 0x4c, 0xe6, 0x5c, 0x61,
+		},
+		.expect = {
+			0x01, 0x03, 0x01, 0x00, 0x02, 0x03, 0x00, 0x01, 0x00, 0x02, 0x02, 0x01,
+			0x01, 0x02, 0x02, 0x01, 0x02, 0x03, 0x02, 0x02, 0x01, 0x01, 0x03, 0x00,
+			0x00, 0x03, 0x03, 0x00, 0x01, 0x00, 0x00, 0x03, 0x01, 0x03, 0x00, 0x02,
+			0x03, 0x03, 0x00, 0x01, 0x02, 0x02, 0x02, 0x01, 0x01, 0x02, 0x03, 0x00,
+		},
+	},
+	{
+		.name = "random08",
+		.src = {
+			0x22, 0x2a, 0xe7, 0xf8, 0xfb, 0x9c, 0x1e, 0xb4, 0x2a, 0x71, 0xcd, 0x54,
+		},
+		.expect = {
+			0x01, 0x01, 0x03, 0x00, 0x02, 0x01, 0x03, 0x01, 0x02, 0x00, 0x03, 0x02,
+			0x01, 0x02, 0x01, 0x00, 0x00, 0x00, 0x01, 0x02, 0x02, 0x02, 0x03, 0x00,
+			0x01, 0x02, 0x00, 0x03, 0x01, 0x03, 0x00, 0x00, 0x03, 0x03, 0x01, 0x01,
+			0x03, 0x02, 0x01, 0x03, 0x01, 0x03, 0x03, 0x03, 0x01, 0x00, 0x00, 0x02,
+		},
+	},
+	{
+		.name = "random09",
+		.src = {
+			0x1d, 0xf3, 0x3e, 0x4c, 0x48, 0x1a, 0xc5, 0x59, 0x5a, 0xfc, 0xf5, 0x5a,
+		},
+		.expect = {
+			0x00, 0x02, 0x01, 0x03, 0x03, 0x01, 0x03, 0x00, 0x01, 0x03, 0x01, 0x03,
+			0x02, 0x00, 0x01, 0x03, 0x02, 0x02, 0x00, 0x01, 0x01, 0x03, 0x00, 0x03,
+			0x00, 0x02, 0x00, 0x03, 0x03, 0x00, 0x03, 0x00, 0x02, 0x03, 0x02, 0x02,
+			0x01, 0x02, 0x00, 0x02, 0x02, 0x03, 0x02, 0x02, 0x03, 0x03, 0x00, 0x00,
+		},
+	},
+	{
+		.name = "random10",
+		.src = {
+			0x92, 0x19, 0xf2, 0x2e, 0xb9, 0xb6, 0xf6, 0x71, 0xa3, 0x48, 0xd9, 0x10,
+		},
+		.expect = {
+			0x03, 0x01, 0x03, 0x01, 0x00, 0x00, 0x03, 0x02, 0x00, 0x02, 0x02, 0x03,
+			0x01, 0x00, 0x00, 0x03, 0x03, 0x02, 0x02, 0x03, 0x00, 0x02, 0x03, 0x00,
+			0x01, 0x00, 0x01, 0x03, 0x00, 0x01, 0x01, 0x00, 0x03, 0x02, 0x01, 0x03,
+			0x03, 0x00, 0x00, 0x03, 0x00, 0x02, 0x01, 0x00, 0x03, 0x01, 0x01, 0x00,
+		},
+	},
+	{
+		.name = "random11",
+		.src = {
+			0x84, 0xea, 0x64, 0x8c, 0xce, 0x29, 0xff, 0x18, 0xb5, 0xaa, 0xd9, 0xc2,
+		},
+		.expect = {
+			0x02, 0x01, 0x03, 0x02, 0x00, 0x03, 0x00, 0x02, 0x01, 0x01, 0x01, 0x02,
+			0x03, 0x00, 0x01, 0x00, 0x03, 0x02, 0x02, 0x02, 0x02, 0x03, 0x02, 0x02,
+			0x02, 0x02, 0x01, 0x00, 0x01, 0x00, 0x02, 0x01, 0x03, 0x03, 0x00, 0x02,
+			0x03, 0x01, 0x01, 0x02, 0x03, 0x00, 0x02, 0x00, 0x03, 0x01, 0x02, 0x00,
+		},
+	},
+	{
+		.name = "random12",
+		.src = {
+			0xcd, 0x3f, 0x4e, 0x27, 0xbb, 0x31, 0x06, 0x7a, 0xba, 0x97, 0xd3, 0xd4,
+		},
+		.expect = {
+			0x02, 0x01, 0x02, 0x02, 0x03, 0x01, 0x03, 0x00, 0x00, 0x02, 0x03, 0x03,
+			0x03, 0x01, 0x03, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x03, 0x02, 0x01,
+			0x02, 0x02, 0x01, 0x03, 0x00, 0x02, 0x00, 0x01, 0x03, 0x02, 0x01, 0x03,
+			0x01, 0x00, 0x03, 0x03, 0x02, 0x00, 0x01, 0x02, 0x00, 0x03, 0x03, 0x03,
+		},
+	},
+	{
+		.name = "random13",
+		.src = {
+			0x3b, 0x64, 0x49, 0xd4, 0x40, 0x3f, 0xab, 0xf0, 0x0f, 0x87, 0x9a, 0xde,
+		},
+		.expect = {
+			0x00, 0x01, 0x00, 0x00, 0x03, 0x02, 0x02, 0x03, 0x02, 0x03, 0x03, 0x02,
+			0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x03, 0x01, 0x03, 0x00, 0x03, 0x03,
+			0x02, 0x02, 0x01, 0x03, 0x03, 0x03, 0x03, 0x01, 0x02, 0x01, 0x00, 0x02,
+			0x02, 0x00, 0x02, 0x00, 0x03, 0x01, 0x00, 0x01, 0x00, 0x03, 0x02, 0x02,
+		},
+	},
+	{
+		.name = "random14",
+		.src = {
+			0x03, 0x13, 0x49, 0x5c, 0x3b, 0xc2, 0x1f, 0xb2, 0x71, 0x9a, 0x11, 0x17,
+		},
+		.expect = {
+			0x00, 0x03, 0x02, 0x02, 0x01, 0x00, 0x00, 0x03, 0x02, 0x00, 0x02, 0x03,
+			0x00, 0x00, 0x03, 0x01, 0x00, 0x00, 0x00, 0x02, 0x02, 0x02, 0x03, 0x03,
+			0x01, 0x01, 0x00, 0x02, 0x00, 0x02, 0x03, 0x02, 0x00, 0x00, 0x01, 0x03,
+			0x01, 0x00, 0x01, 0x03, 0x02, 0x01, 0x00, 0x03, 0x03, 0x01, 0x02, 0x00,
+		},
+	},
+	{
+		.name = "random15",
+		.src = {
+			0x53, 0x46, 0x16, 0xdb, 0xce, 0x68, 0x55, 0x35, 0x6d, 0x11, 0xe3, 0x4a,
+		},
+		.expect = {
+			0x00, 0x02, 0x02, 0x01, 0x02, 0x03, 0x01, 0x02, 0x00, 0x01, 0x02, 0x02,
+			0x00, 0x03, 0x01, 0x02, 0x00, 0x03, 0x00, 0x03, 0x00, 0x02, 0x01, 0x03,
+			0x00, 0x03, 0x01, 0x00, 0x03, 0x00, 0x02, 0x00, 0x03, 0x03, 0x02, 0x00,
+			0x01, 0x01, 0x03, 0x02, 0x01, 0x01, 0x00, 0x03, 0x01, 0x00, 0x01, 0x03,
+		},
+	},
+};
+
+static const struct ploytec_decode_vector ploytec_decode_vectors[] = {
+	{
+		.name = "zeros",
+		.src = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+		},
+		.expect = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		},
+	},
+	{
+		.name = "ones",
+		.src = {
+			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+			0xff, 0xff, 0xff, 0xff,
+		},
+		.expect = {
+			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+			0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+		},
+	},
+	{
+		.name = "ramp",
+		.src = {
+			0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b,
+			0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+			0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23,
+			0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
+			0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b,
+			0x3c, 0x3d, 0x3e, 0x3f,
+		},
+		.expect = {
+			0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
+			0x0f, 0x0f, 0x0f, 0x0f, 0x0f, 0x0f,
+		},
+	},
+	{
+		.name = "bitplane0_only",
+		.src = {
+			0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+			0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+			0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+			0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+			0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
+			0x01, 0x01, 0x01, 0x01,
+		},
+		.expect = {
+			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		},
+	},
+	{
+		.name = "bitplane1_only",
+		.src = {
+			0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+			0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+			0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+			0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+			0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+			0x02, 0x02, 0x02, 0x02,
+		},
+		.expect = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		},
+	},
+	{
+		.name = "bitplane2_only",
+		.src = {
+			0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+			0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+			0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+			0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+			0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04,
+			0x04, 0x04, 0x04, 0x04,
+		},
+		.expect = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+		},
+	},
+	{
+		.name = "group0_only",
+		.src = {
+			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+		},
+		.expect = {
+			0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00,
+			0xff, 0xff, 0xff, 0x00, 0x00, 0x00,
+		},
+	},
+	{
+		.name = "group1_only",
+		.src = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
+			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00,
+		},
+		.expect = {
+			0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff,
+			0x00, 0x00, 0x00, 0xff, 0xff, 0xff,
+		},
+	},
+	{
+		.name = "unused_bytes_only",
+		.src = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff,
+			0xff, 0xff, 0xff, 0xff,
+		},
+		.expect = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		},
+	},
+	{
+		.name = "unused_bits_only",
+		.src = {
+			0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8,
+			0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8,
+			0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8,
+			0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8,
+			0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8, 0xf8,
+			0xf8, 0xf8, 0xf8, 0xf8,
+		},
+		.expect = {
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+			0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		},
+	},
+	{
+		.name = "random00",
+		.src = {
+			0xc6, 0xd8, 0x5e, 0x8e, 0xac, 0x3a, 0xa9, 0x38, 0x25, 0x20, 0xcb, 0x47,
+			0x26, 0xd0, 0x86, 0xb5, 0xf4, 0xfd, 0x71, 0x3c, 0xb2, 0xed, 0x50, 0x06,
+			0xa5, 0x48, 0x62, 0xc1, 0xa5, 0x7f, 0xc7, 0xc5, 0xcd, 0x91, 0x98, 0xa8,
+			0x8b, 0x35, 0x5a, 0x0c, 0x3e, 0xdc, 0x33, 0xf7, 0x3d, 0x8a, 0x15, 0xf6,
+			0x5e, 0xac, 0x64, 0x96, 0x33, 0x8c, 0x4b, 0xf4, 0x83, 0xa5, 0x41, 0x86,
+			0xfd, 0x51, 0xe5, 0x15,
+		},
+		.expect = {
+			0x64, 0xb1, 0x02, 0x0a, 0x3a, 0xcc, 0x09, 0x3a, 0xb4, 0x9a, 0xb5, 0x0a,
+			0xd5, 0x9b, 0xb8, 0xf5, 0xdb, 0x85,
+		},
+	},
+	{
+		.name = "random01",
+		.src = {
+			0xcd, 0xae, 0xb2, 0x51, 0xf0, 0x30, 0xf7, 0xf1, 0xda, 0x22, 0x94, 0x5d,
+			0xd4, 0x48, 0x69, 0xa6, 0x37, 0x76, 0xd1, 0x4e, 0x58, 0xb0, 0xdf, 0x86,
+			0x19, 0xa5, 0x1d, 0x27, 0x62, 0x3a, 0x41, 0x4a, 0x1d, 0xa6, 0xe1, 0x9a,
+			0x44, 0xe0, 0x6a, 0x40, 0xf5, 0xc2, 0x2f, 0x9b, 0xf4, 0x8e, 0xcb, 0x7c,
+			0x9c, 0xfd, 0x51, 0x89, 0x40, 0x75, 0x99, 0x2d, 0x4b, 0xda, 0x29, 0x14,
+			0xfc, 0xd2, 0x9b, 0x13,
+		},
+		.expect = {
+			0xa2, 0x12, 0x93, 0x77, 0xb2, 0xa0, 0xd3, 0xc1, 0x62, 0x00, 0x76, 0x52,
+			0xd3, 0x39, 0xc2, 0xc5, 0xad, 0xc8,
+		},
+	},
+	{
+		.name = "random02",
+		.src = {
+			0x8e, 0xae, 0x5e, 0x59, 0x19, 0x93, 0xfd, 0x3b, 0x27, 0x0a, 0x8e, 0x06,
+			0xfe, 0x31, 0xb2, 0xab, 0x7e, 0xc0, 0xaa, 0xe8, 0x13, 0x52, 0x24, 0x71,
+			0x4b, 0x2f, 0x0d, 0x71, 0xf6, 0x9c, 0xc5, 0x3f, 0x12, 0x07, 0x45, 0xb1,
+			0xff, 0x89, 0xe3, 0x86, 0x60, 0x41, 0xd4, 0x82, 0xc2, 0xbe, 0x01, 0x48,
+			0x17, 0x7c, 0x8c, 0x42, 0x4f, 0xba, 0xc1, 0xed, 0xb9, 0xf3, 0xb4, 0xfc,
+			0xa8, 0xb9, 0x1e, 0xc0,
+		},
+		.expect = {
+			0x09, 0x85, 0x1f, 0x8b, 0x42, 0x7e, 0xac, 0xfb, 0xe5, 0x9c, 0x1c, 0xcb,
+			0x82, 0xb8, 0xe2, 0xe9, 0x24, 0x69,
+		},
+	},
+	{
+		.name = "random03",
+		.src = {
+			0xb4, 0xbd, 0x6e, 0x34, 0xa0, 0x24, 0x1f, 0x6f, 0x5d, 0x79, 0x0e, 0xa6,
+			0x86, 0x6e, 0xfd, 0x0d, 0x27, 0xa3, 0x08, 0x71, 0x06, 0xc2, 0xd9, 0x68,
+			0xc4, 0xbc, 0x4e, 0xd6, 0x36, 0x0c, 0x5a, 0xaa, 0x7c, 0xf0, 0xbb, 0x88,
+			0xc4, 0x96, 0xb2, 0x49, 0x76, 0x66, 0xee, 0xae, 0x3e, 0x2b, 0xaa, 0x14,
+			0x6d, 0x6f, 0xcd, 0x58, 0x3e, 0x59, 0x50, 0x2b, 0xf7, 0x92, 0x3b, 0xf0,
+			0x80, 0x8d, 0xd4, 0x16,
+		},
+		.expect = {
+			0xd2, 0xc3, 0x43, 0xe5, 0x04, 0x21, 0xcc, 0x3c, 0x23, 0x49, 0xfe, 0x26,
+			0x88, 0xbf, 0xf7, 0xe8, 0xf9, 0x8c,
+		},
+	},
+	{
+		.name = "random04",
+		.src = {
+			0xef, 0x44, 0xa5, 0x78, 0x0f, 0x7d, 0xfe, 0x17, 0xa4, 0xd2, 0x30, 0x98,
+			0xbf, 0xc5, 0xb6, 0xda, 0x4f, 0x6b, 0xde, 0x5f, 0x1d, 0x3c, 0xe7, 0xe6,
+			0x46, 0x7f, 0x70, 0x00, 0xa4, 0x5d, 0xa3, 0xef, 0x7b, 0x9f, 0x64, 0x0e,
+			0xc4, 0x09, 0x25, 0xfa, 0x85, 0x83, 0x13, 0x18, 0x06, 0x26, 0x87, 0x6b,
+			0x94, 0x56, 0x89, 0xc6, 0x12, 0x38, 0xd1, 0xd5, 0xd1, 0x76, 0xf4, 0x00,
+			0x1d, 0xe9, 0xa4, 0xd8,
+		},
+		.expect = {
+			0xda, 0x0c, 0xad, 0x23, 0xe3, 0xc6, 0xf3, 0x4b, 0x8b, 0x58, 0x6f, 0xd1,
+			0xbf, 0x8e, 0xef, 0xd1, 0x8e, 0x7a,
+		},
+	},
+	{
+		.name = "random05",
+		.src = {
+			0x3d, 0x21, 0xf0, 0x7d, 0x1b, 0xd9, 0xf9, 0x41, 0xad, 0x31, 0xe2, 0xea,
+			0x59, 0x26, 0x84, 0xbb, 0x9a, 0xf8, 0x6c, 0xaa, 0x7f, 0x9a, 0x5a, 0x9a,
+			0xb7, 0x58, 0x2d, 0x47, 0xc8, 0xd9, 0x26, 0x94, 0x16, 0xac, 0xc8, 0x30,
+			0x03, 0xd0, 0x65, 0x2b, 0x36, 0xba, 0x6f, 0xf8, 0xdb, 0x56, 0xbf, 0x58,
+			0x2d, 0xb9, 0x98, 0x21, 0xe8, 0x60, 0x2f, 0xe8, 0x08, 0xad, 0xb8, 0x29,
+			0xe0, 0xe1, 0x30, 0xb4,
+		},
+		.expect = {
+			0x08, 0xc9, 0xdf, 0xd2, 0x2a, 0x0b, 0x9f, 0x35, 0x08, 0x02, 0xee, 0x89,
+			0x28, 0x86, 0x90, 0x82, 0xa6, 0xc2,
+		},
+	},
+	{
+		.name = "random06",
+		.src = {
+			0xee, 0x50, 0x9e, 0xc6, 0x2f, 0x39, 0xbe, 0xdc, 0xdf, 0x36, 0x4a, 0xea,
+			0x87, 0x86, 0x56, 0x36, 0xe2, 0xd8, 0x5e, 0x9c, 0x30, 0xb7, 0x21, 0x54,
+			0x3d, 0x11, 0x84, 0x67, 0x00, 0xb9, 0x0c, 0x21, 0x7a, 0x27, 0x27, 0xd5,
+			0x78, 0xc0, 0xf2, 0x12, 0xcb, 0x75, 0xb6, 0x4a, 0x6e, 0x0e, 0xc6, 0x4f,
+			0x19, 0xe5, 0x27, 0xcb, 0xef, 0x20, 0xa9, 0x7e, 0x8f, 0x71, 0xcc, 0x67,
+			0x01, 0x24, 0x65, 0x42,
+		},
+		.expect = {
+			0x06, 0x88, 0x0c, 0xfa, 0xc1, 0x70, 0xa4, 0xff, 0xba, 0x39, 0xbf, 0xe3,
+			0x35, 0xcf, 0xbb, 0x69, 0x6f, 0x70,
+		},
+	},
+	{
+		.name = "random07",
+		.src = {
+			0xe5, 0xe6, 0xd4, 0x3d, 0x9c, 0x02, 0xbe, 0xd4, 0x8e, 0x97, 0x53, 0x62,
+			0x8c, 0xad, 0x44, 0x9f, 0x92, 0x01, 0xf2, 0x87, 0xcc, 0xd6, 0x89, 0xd6,
+			0x2a, 0xa3, 0xe1, 0xa8, 0xc6, 0xde, 0x45, 0x14, 0x35, 0x48, 0xec, 0x8e,
+			0xa3, 0x67, 0x56, 0x9b, 0x7e, 0x39, 0x88, 0xc5, 0x1a, 0x8e, 0xc9, 0x9f,
+			0x15, 0xbe, 0x04, 0x2a, 0xb4, 0xaa, 0x51, 0xa4, 0xe4, 0x64, 0x9c, 0x0f,
+			0x6d, 0xe9, 0x01, 0x87,
+		},
+		.expect = {
+			0x52, 0x65, 0x90, 0x82, 0x53, 0x8d, 0xb5, 0xf1, 0x46, 0x54, 0x8d, 0x1f,
+			0x1d, 0xcf, 0xfb, 0xe9, 0x95, 0xb6,
+		},
+	},
+	{
+		.name = "random08",
+		.src = {
+			0xe6, 0x2a, 0xbf, 0xe4, 0x65, 0x8e, 0xe8, 0x56, 0xcc, 0xf7, 0x9c, 0xce,
+			0xcc, 0x26, 0xbf, 0x43, 0xde, 0xac, 0x2f, 0x15, 0x21, 0xd6, 0x96, 0x2a,
+			0x41, 0xe3, 0x77, 0x00, 0x97, 0xe8, 0xde, 0x17, 0xcc, 0x3f, 0xe4, 0x68,
+			0x81, 0x04, 0xae, 0x8e, 0x5c, 0xec, 0x4b, 0x75, 0x91, 0x3f, 0xe6, 0x4c,
+			0xa4, 0xbb, 0xdd, 0xf0, 0x78, 0xee, 0xf1, 0x61, 0xee, 0x9b, 0xff, 0xed,
+			0xd4, 0x1d, 0x08, 0x4d,
+		},
+		.expect = {
+			0x38, 0x43, 0x28, 0x63, 0x3c, 0x48, 0xa7, 0x57, 0xe5, 0x44, 0x26, 0x43,
+			0xf6, 0xfe, 0xbd, 0xa4, 0xd7, 0xe7,
+		},
+	},
+	{
+		.name = "random09",
+		.src = {
+			0x79, 0xce, 0xe6, 0x42, 0xf3, 0xe2, 0x01, 0xc7, 0x00, 0x7c, 0x86, 0xab,
+			0x74, 0x1c, 0x27, 0x73, 0xe4, 0x11, 0x64, 0x17, 0xe3, 0x04, 0x8c, 0x6d,
+			0xac, 0xc7, 0xb4, 0x76, 0x0f, 0xda, 0x0e, 0x8e, 0x56, 0x3d, 0x2d, 0x21,
+			0x1b, 0xee, 0xf3, 0x57, 0x94, 0xf1, 0xed, 0x35, 0x3b, 0x68, 0x4d, 0x96,
+			0x62, 0x84, 0xaa, 0xe3, 0xb1, 0x68, 0xdf, 0x21, 0x0b, 0xc5, 0xe2, 0x36,
+			0x7d, 0x34, 0xb7, 0xab,
+		},
+		.expect = {
+			0x59, 0x13, 0x8b, 0x1b, 0x7a, 0x7b, 0x18, 0x33, 0x7d, 0xb2, 0x09, 0x8f,
+			0xb7, 0x6e, 0x61, 0x42, 0xb3, 0xe5,
+		},
+	},
+	{
+		.name = "random10",
+		.src = {
+			0x15, 0x04, 0xd5, 0x8f, 0x8a, 0xd6, 0x4a, 0x71, 0xed, 0x8e, 0xdc, 0xb1,
+			0x95, 0x6a, 0xec, 0x57, 0xf2, 0x4e, 0x55, 0xbc, 0x7e, 0x1b, 0xb4, 0x21,
+			0x7d, 0xbe, 0x5f, 0xf7, 0x04, 0x0f, 0x47, 0x7d, 0x02, 0xce, 0x92, 0x6c,
+			0xce, 0xd7, 0x1d, 0x7d, 0x93, 0xb6, 0x79, 0x7a, 0x75, 0x10, 0xb9, 0x96,
+			0x68, 0x14, 0xae, 0x41, 0xe1, 0x3b, 0x3a, 0x6e, 0xa3, 0x47, 0x26, 0x8f,
+			0x26, 0xe7, 0x7c, 0x79,
+		},
+		.expect = {
+			0x25, 0x99, 0xb1, 0x1c, 0xaa, 0x07, 0xcc, 0x45, 0x1e, 0x27, 0xd1, 0xec,
+			0x7a, 0xeb, 0xf4, 0x61, 0x49, 0x5f,
+		},
+	},
+	{
+		.name = "random11",
+		.src = {
+			0x6c, 0x13, 0x40, 0xe8, 0x4e, 0x10, 0x1c, 0x98, 0xee, 0x6d, 0xa3, 0x78,
+			0x32, 0x8b, 0x6a, 0x99, 0x70, 0x92, 0x40, 0xe3, 0x69, 0x54, 0x3b, 0x3b,
+			0x4e, 0xc1, 0x42, 0xdf, 0xb3, 0xe5, 0x65, 0x1b, 0x54, 0x27, 0xf5, 0x3f,
+			0xba, 0xb0, 0x6c, 0x27, 0x0a, 0x05, 0xde, 0x53, 0xb7, 0x7c, 0xf3, 0x50,
+			0xc3, 0xa5, 0xdf, 0xef, 0x83, 0xbf, 0x4f, 0x10, 0xfd, 0xd9, 0x75, 0xea,
+			0x57, 0x43, 0xf8, 0xf0,
+		},
+		.expect = {
+			0x1b, 0x65, 0x40, 0xfe, 0x5a, 0x71, 0x53, 0xae, 0x48, 0xbe, 0xba, 0x59,
+			0x04, 0xc0, 0x8a, 0x76, 0x6c, 0xf3,
+		},
+	},
+	{
+		.name = "random12",
+		.src = {
+			0xd8, 0x7b, 0xd5, 0x75, 0x28, 0xb5, 0x18, 0x63, 0x15, 0xbc, 0xf2, 0xac,
+			0xcd, 0x26, 0x4f, 0xbe, 0xdf, 0x80, 0x99, 0xa0, 0x33, 0x2e, 0x99, 0xe7,
+			0xd0, 0x8a, 0x60, 0x0b, 0x6a, 0xd6, 0x17, 0xed, 0x0e, 0xd3, 0x84, 0xa6,
+			0x12, 0xe5, 0x88, 0xaa, 0x66, 0xac, 0x45, 0x64, 0x7e, 0x9d, 0xf3, 0x9c,
+			0xae, 0xa3, 0x18, 0x10, 0x64, 0x0f, 0xdd, 0xd8, 0x8a, 0xaa, 0xd6, 0xde,
+			0x63, 0x16, 0xc0, 0x12,
+		},
+		.expect = {
+			0xab, 0x8a, 0x75, 0x46, 0x26, 0x44, 0x8d, 0x27, 0x41, 0xc4, 0x8a, 0xd9,
+			0x85, 0xdf, 0x34, 0x8e, 0xfd, 0xb4,
+		},
+	},
+	{
+		.name = "random13",
+		.src = {
+			0x9a, 0x97, 0x1c, 0x40, 0x5b, 0xdf, 0x25, 0xd2, 0xc3, 0x5b, 0xa8, 0xfe,
+			0xf0, 0x61, 0x70, 0x8a, 0xbe, 0x66, 0x51, 0x59, 0xc0, 0x69, 0x4c, 0xc3,
+			0xf7, 0x2f, 0xb5, 0x48, 0xd2, 0x7a, 0xff, 0x5a, 0x15, 0xd2, 0xa6, 0x5e,
+			0xa6, 0x0c, 0xad, 0xdf, 0xce, 0xf6, 0x19, 0x27, 0x28, 0x85, 0x13, 0xd5,
+			0xbd, 0x68, 0xd5, 0x5f, 0x39, 0x8b, 0x34, 0xd0, 0x95, 0x9c, 0x48, 0x29,
+			0xd7, 0x89, 0x14, 0xc0,
+		},
+		.expect = {
+			0x35, 0xc4, 0x4e, 0xbc, 0x37, 0x83, 0xc1, 0xd1, 0xcd, 0x14, 0xd2, 0x79,
+			0xc2, 0x10, 0x66, 0xb2, 0xd5, 0xbf,
+		},
+	},
+	{
+		.name = "random14",
+		.src = {
+			0xbf, 0x78, 0x65, 0x38, 0xf3, 0x58, 0x09, 0x9f, 0x34, 0x5c, 0x40, 0x2d,
+			0xb6, 0xd3, 0x47, 0x04, 0xdb, 0x5f, 0x69, 0x4b, 0x4f, 0xc3, 0xbd, 0xd9,
+			0x5f, 0xf5, 0xcd, 0x94, 0x3b, 0x0c, 0x59, 0xe3, 0x13, 0xc4, 0xd3, 0x34,
+			0x07, 0x47, 0xa5, 0x1a, 0xc3, 0xb8, 0xe3, 0x45, 0xc8, 0x0b, 0x11, 0x9d,
+			0xb3, 0x9a, 0x87, 0x4a, 0x61, 0xb2, 0x99, 0x65, 0xcb, 0xc8, 0x69, 0xb4,
+			0xa3, 0x98, 0x72, 0xd4,
+		},
+		.expect = {
+			0xff, 0x16, 0xab, 0xab, 0xb7, 0xae, 0xdc, 0x0e, 0x89, 0xf4, 0xa4, 0xad,
+			0x4a, 0xdb, 0xa1, 0x21, 0x11, 0x5e,
+		},
+	},
+	{
+		.name = "random15",
+		.src = {
+			0x55, 0xab, 0x93, 0x08, 0xcd, 0x6a, 0x50, 0x3b, 0x0e, 0xc6, 0x2b, 0x52,
+			0x3c, 0x10, 0xc8, 0x4f, 0x37, 0xfc, 0x1c, 0xde, 0x49, 0x6f, 0x08, 0x0a,
+			0x4a, 0x39, 0xa9, 0x99, 0x14, 0xb6, 0x91, 0xf2, 0x4d, 0x09, 0x00, 0x3f,
+			0xf2, 0x8c, 0x48, 0xdf, 0x3f, 0x05, 0xfb, 0xb1, 0xec, 0xb3, 0xee, 0x42,
+			0xb7, 0x8d, 0xce, 0xc9, 0x3e, 0x81, 0xcc, 0x94, 0x0b, 0x63, 0x8d, 0xbf,
+			0xa3, 0xc9, 0xa8, 0x24,
+		},
+		.expect = {
+			0x8c, 0x21, 0xe9, 0xd4, 0xf4, 0xd1, 0x95, 0xf1, 0x65, 0xa8, 0xa7, 0x19,
+			0xf4, 0xc9, 0x88, 0xeb, 0xca, 0x95,
+		},
+	},
+};
+
+#endif /* PLOYTEC_CODEC_TEST_VECTORS_H */
diff --git a/sound/usb/jockey3/ploytec_midi.c b/sound/usb/jockey3/ploytec_midi.c
new file mode 100644
index 0000000000000..3eebc706bb301
--- /dev/null
+++ b/sound/usb/jockey3/ploytec_midi.c
@@ -0,0 +1,83 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *   Generic MIDI 1.0 Running Status expander
+ *
+ *   Copyright (c) 2026 by Frank van de Pol <fvdpol@gmail.com>
+ */
+
+/*
+ * The logic here is generic MIDI 1.0 Running Status handling: it operates
+ * purely on a byte stream, with no dependency on USB or on the rest of this
+ * driver. It carries the ploytec_ prefix and lives in the Ploytec layer
+ * because that firmware is what requires it -- the device does not accept a
+ * Running Status stream and needs every message fully status-prefixed.
+ *
+ * Should a second driver ever need the same behavior, this would be a
+ * reasonable candidate to migrate into a shared kernel library rather than
+ * duplicating it.
+ */
+
+#include "ploytec_midi.h"
+
+/**
+ * ploytec_midi_running_status_expand() - Expand MIDI 1.0 Running Status
+ * @state: expander state, zero-initialized by the caller before first use
+ * @b: the next raw MIDI byte from the source stream, in stream order
+ * @dev: struct device for diagnostic logging (currently unused)
+ *
+ * MIDI 1.0 allows the status byte of a Channel Voice Message to be omitted
+ * when it repeats the previous message's status ("Running Status"). Some
+ * MIDI receivers don't implement Running Status and require every message
+ * to carry its status byte explicitly; this function expands a Running
+ * Status stream back into fully status-prefixed messages.
+ *
+ * Call this once per raw input byte. Most calls simply return @b unchanged.
+ * When a data byte arrives while Running Status is active, the synthesised
+ * status byte is returned immediately for the caller to send first, and @b
+ * itself is queued in @state->queued_byte (with @state->has_queued_byte set)
+ * for the caller to retrieve and send on the next output opportunity, ahead
+ * of consuming any further input.
+ *
+ * Return: the next byte to send to the receiver.
+ */
+u8 ploytec_midi_running_status_expand(struct ploytec_midi_running_status *state,
+				      u8 b, struct device *dev)
+{
+	u8 byte;
+
+	if (b >= 0x80) { // Status byte
+		if (b < 0xf0) { // Channel Voice Message (0x80-0xEF)
+			state->running_status = b;
+			/* Determine expected data bytes based on MIDI opcode */
+			if ((b & 0xf0) == 0xc0 || (b & 0xf0) == 0xd0)
+				state->expected_data = 1; // PC, Channel Pressure
+			else
+				state->expected_data = 2; // Note On/Off, CC, etc.
+		} else if (b < 0xf8) { // System Common Message (0xf0-0xf7)
+			/* System Common messages clear Running Status */
+			state->running_status = 0;
+			state->expected_data = 0;
+		} else { // System Real-Time Message (0xf8-0xff), do not affect Running Status
+			return b;
+		}
+
+		state->data_count = state->expected_data; // initialize expected data byte count
+		return b;
+	}
+
+	/* Data byte */
+	if (state->data_count > 0) {
+		state->data_count--;
+		return b;
+	} else if (state->running_status >= 0x80) {
+		/* Message is complete but we got a data byte -> expand Running Status */
+		byte = state->running_status;
+		state->queued_byte = b;
+		state->has_queued_byte = true;
+		state->data_count = state->expected_data - 1; // already 1 byte queued
+		return byte;
+	}
+
+	/* No running status expansion active, just send the data byte */
+	return b;
+}
diff --git a/sound/usb/jockey3/ploytec_midi.h b/sound/usb/jockey3/ploytec_midi.h
new file mode 100644
index 0000000000000..079bce519e9d9
--- /dev/null
+++ b/sound/usb/jockey3/ploytec_midi.h
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ *   Generic MIDI 1.0 Running Status expander
+ *
+ *   Copyright (c) 2026 by Frank van de Pol <fvdpol@gmail.com>
+ */
+
+#ifndef __SOUND_USB_JOCKEY3_PLOYTEC_MIDI_H
+#define __SOUND_USB_JOCKEY3_PLOYTEC_MIDI_H
+
+#include <linux/types.h>
+
+struct device;
+
+/**
+ * struct ploytec_midi_running_status - MIDI 1.0 Running Status expander state
+ * @expected_data: number of data bytes expected for the current @running_status message
+ * @data_count: number of data bytes still to be consumed for the in-progress message
+ * @running_status: the currently active Running Status (Channel Voice) opcode, or 0 if none
+ * @queued_byte: data byte held back when a Running Status byte had to be synthesised
+ * @has_queued_byte: true when @queued_byte holds a byte still to be delivered
+ *
+ * Zero-initialize before first use.
+ */
+struct ploytec_midi_running_status {
+	int expected_data;
+	int data_count;
+	u8 running_status;
+	u8 queued_byte;
+	bool has_queued_byte;
+};
+
+u8 ploytec_midi_running_status_expand(struct ploytec_midi_running_status *state,
+				      u8 b, struct device *dev);
+
+#endif /* __SOUND_USB_JOCKEY3_PLOYTEC_MIDI_H */
diff --git a/sound/usb/jockey3/ploytec_proto.c b/sound/usb/jockey3/ploytec_proto.c
new file mode 100644
index 0000000000000..a4d5795cbdf4d
--- /dev/null
+++ b/sound/usb/jockey3/ploytec_proto.c
@@ -0,0 +1,365 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *   ALSA driver for Reloop Jockey 3 devices
+ *   Ploytec USB Protocol Handling
+ *
+ *   Copyright (c) 2026 by Frank van de Pol <fvdpol@gmail.com>
+ */
+
+#include <linux/delay.h>
+#include "ploytec_proto.h"
+
+/*
+ * None of the helpers below validate @intf/@xfer_buf for NULL: callers own
+ * the chip's usb_interface and control-transfer buffer for the entire time
+ * the PCM/rawmidi devices can be open, so these arguments are always valid
+ * here.
+ */
+
+/**
+ * ploytec_ctrl_ep_unresponsive - Has the control endpoint stopped answering?
+ * @err: error returned by an EP0 control transfer
+ *
+ * Distinguishes a device that answered -- even to refuse -- from one whose
+ * control endpoint has gone silent. A STALL (-EPIPE) or a short reply
+ * (-EREMOTEIO) both mean live firmware: the transfer completed and the device
+ * drove the bus. A timeout or a transport-level error means EP0 itself is gone,
+ * and every further control transfer will only burn another
+ * PLOYTEC_CTRL_TIMEOUT_MS before failing the same way.
+ *
+ * The list is affirmative rather than an exclusion, so an unfamiliar error
+ * counts as "device still there" and a new class has to be added deliberately.
+ *
+ * Return: true if EP0 has stopped responding.
+ */
+static bool ploytec_ctrl_ep_unresponsive(int err)
+{
+	switch (err) {
+	case -ETIMEDOUT:
+	case -ENODEV:
+	case -ESHUTDOWN:
+	case -ECONNRESET:
+	case -EPROTO:
+	case -EILSEQ:
+	case -ETIME:
+		return true;
+	default:
+		return false;
+	}
+}
+
+/**
+ * ploytec_get_firmware - Read firmware version from the device
+ * @intf: USB interface
+ * @xfer_buf: Temporary transfer buffer (at least 3 bytes)
+ * @fw_version: Optional, set to the packed firmware/hardware version on success
+ *
+ * Performs a request to the device to retrieve the firmware and/or hardware version.
+ * Required as part of the handshake sequence regardless of whether the caller
+ * wants the value; pass @fw_version as NULL to discard it.
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+int ploytec_get_firmware(struct usb_interface *intf, void *xfer_buf, u32 *fw_version)
+{
+	struct usb_device *dev = interface_to_usbdev(intf);
+	u8 *buf = xfer_buf;
+	int ret;
+
+	ret = usb_control_msg_recv(dev, 0, PLOYTEC_REQ_FIRMWARE, PLOYTEC_REQ_FIRMWARE_TYPE, 0, 0,
+				   buf, 3, PLOYTEC_CTRL_TIMEOUT_MS, GFP_KERNEL);
+	if (ret < 0)
+		return ret;
+
+	/*
+	 * Three-byte reply: a suspected hardware revision, then the firmware
+	 * major and minor. See re/protocol_analysis.md.
+	 */
+	if (fw_version)
+		*fw_version = (buf[0] << 16) | (buf[1] << 8) | buf[2];
+	return 0;
+}
+
+/**
+ * ploytec_get_status - Read device status byte
+ * @intf: USB interface
+ * @xfer_buf: Temporary transfer buffer (at least 1 byte)
+ * @status: Pointer to store the status byte
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+int ploytec_get_status(struct usb_interface *intf, void *xfer_buf, u8 *status)
+{
+	struct usb_device *dev = interface_to_usbdev(intf);
+	u8 *buf = xfer_buf;
+	int ret;
+
+	// Read Status (Request 0x49)
+	ret = usb_control_msg_recv(dev, 0, PLOYTEC_REQ_STATUS, PLOYTEC_REQ_STATUS_TYPE, 0, 0,
+				   buf, 1, PLOYTEC_CTRL_TIMEOUT_MS, GFP_KERNEL);
+	if (ret < 0)
+		return ret;
+
+	*status = buf[0];
+	return 0;
+}
+
+/**
+ * ploytec_initialize_device - Perform Ploytec handshake sequence as observed in USB traces.
+ * @intf: USB interface
+ * @xfer_buf: Temporary transfer buffer
+ * @bounce_alt0: drive both interfaces to alt 0 before selecting alt 1, which
+ *	the vendors do only when changing the rate of a running device
+ * @fw_version: Optional, set to the packed firmware/hardware version on success
+ *
+ * Aborts early if EP0 stops responding, rather than continuing into the
+ * alt-setting sequence below; see the comment on the firmware read for why
+ * that distinction matters.
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+int ploytec_initialize_device(struct usb_interface *intf, void *xfer_buf, bool bounce_alt0,
+			      u32 *fw_version)
+{
+	struct usb_device *dev = interface_to_usbdev(intf);
+	const unsigned int halt_pipes[] = {
+		usb_rcvbulkpipe(dev, PLOYTEC_EP_NUM_PCM_IN),
+		usb_sndbulkpipe(dev, PLOYTEC_EP_NUM_PCM_OUT),
+		usb_rcvbulkpipe(dev, PLOYTEC_EP_NUM_MIDI_IN),
+	};
+	u8 status;
+	unsigned int i;
+	int ret;
+
+	/*
+	 * The vendors read the firmware version after power-up and the value
+	 * is unused here, but this is the sequence's first EP0 transfer and so
+	 * the last point at which the usb_set_interface() calls below can
+	 * still be avoided -- which is what makes its return load-bearing.
+	 * usb_set_interface() disables the interface's endpoints before it
+	 * sends SET_INTERFACE and does not re-enable them if that request
+	 * fails, so calling it on a device whose control endpoint has already
+	 * gone silent leaves every endpoint of interface 0 permanently
+	 * disabled and usb_submit_urb() returning -ENOENT until the device is
+	 * reset. A device that merely refuses the request is fine; one that
+	 * has stopped answering is not.
+	 */
+	ret = ploytec_get_firmware(intf, xfer_buf, fw_version);
+	if (ret < 0) {
+		dev_warn(&intf->dev, "Firmware version read failed: %d\n", ret);
+		if (ploytec_ctrl_ep_unresponsive(ret))
+			return ret;
+	}
+
+	/*
+	 * Deactivate the audio interfaces before reactivating them, but only
+	 * when the device is already running. On a device that has just been
+	 * enumerated the interfaces are at alt 0 anyway, and neither vendor
+	 * driver touches alt 0 there. macOS does bounce on a rate change, and
+	 * takes interface 1 down first, which is the order used here.
+	 */
+	if (bounce_alt0) {
+		ret = usb_set_interface(dev, 1, 0);
+		if (ret < 0)
+			return ret;
+		ret = usb_set_interface(dev, 0, 0);
+		if (ret < 0)
+			return ret;
+
+		/* Give the hardware some time to respond, otherwise it might not be ready */
+		usleep_range(3000, 5000);
+	}
+
+	// Select Alt Setting 1 to activate the audio interface
+	ret = usb_set_interface(dev, 0, 1);
+	if (ret < 0)
+		return ret;
+	ret = usb_set_interface(dev, 1, 1);
+	if (ret < 0)
+		return ret;
+
+	/*
+	 * The bulk endpoints just activated are not immediately ready for the
+	 * usb_clear_halt() calls below: without this pause, EP0 stops
+	 * answering anything at all until a full USB re-enumeration clears
+	 * it. Only ever reproduced on a single-core host (Pi 1B, armv6),
+	 * never on a multi-core one; bisected down to this range with real
+	 * device power cycles. See github.com/fvdpol/alsa-jockey3/issues/48.
+	 */
+	usleep_range(25, 35);
+
+	/*
+	 * Clear Feature (ENDPOINT_HALT). A failure here has never been fatal on
+	 * a device that is still answering, so only a silent EP0 aborts -- which
+	 * also avoids burning two more PLOYTEC_CTRL_TIMEOUT_MS on the remaining
+	 * pipes once the first one has timed out.
+	 */
+	for (i = 0; i < ARRAY_SIZE(halt_pipes); i++) {
+		ret = usb_clear_halt(dev, halt_pipes[i]);
+		if (ret < 0) {
+			dev_warn(&intf->dev, "Failed to clear halt on EP 0x%02x: %d\n",
+				 usb_pipeendpoint(halt_pipes[i]) |
+					(usb_pipein(halt_pipes[i]) ? USB_DIR_IN : 0),
+				 ret);
+			if (ploytec_ctrl_ep_unresponsive(ret))
+				return ret;
+		}
+	}
+
+	return ploytec_get_status(intf, xfer_buf, &status);
+}
+
+/**
+ * ploytec_start_streaming - Trigger the device to start streaming
+ * @intf: USB interface
+ * @xfer_buf: Temporary transfer buffer
+ *
+ * Reads the status byte and writes it back with the STREAMING bit set. The
+ * write is unconditional, which is the whole point: the device already
+ * reports STREAMING set after a rate change, so a conditional write would
+ * never be issued there at all. Ending a rate change with a second status
+ * read instead left the capture endpoint failing to restart after roughly one
+ * rate change in six, so the write evidently does more than set a bit -- see
+ * re/rate_change_stall.md.
+ *
+ * The vendors do not read the status back afterwards, so neither do we.
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+int ploytec_start_streaming(struct usb_interface *intf, void *xfer_buf)
+{
+	struct usb_device *dev = interface_to_usbdev(intf);
+	u8 status;
+	int ret;
+
+	ret = ploytec_get_status(intf, xfer_buf, &status);
+	if (ret < 0)
+		return ret;
+	dev_dbg(&intf->dev, "Start Streaming: Status: 0x%02x\n", status);
+
+	return usb_control_msg_send(dev, 0, PLOYTEC_SET_STATUS, PLOYTEC_SET_STATUS_TYPE,
+				    status | PLOYTEC_STATUS_STREAMING,
+				    0, NULL, 0, PLOYTEC_CTRL_TIMEOUT_MS, GFP_KERNEL);
+}
+
+/**
+ * ploytec_get_rate - Read hardware sample rate
+ * @intf: USB interface
+ * @xfer_buf: Temporary transfer buffer
+ * @index: wIndex to read from -- PLOYTEC_RATE_IDX_DEVICE before programming,
+ *	PLOYTEC_RATE_IDX_PCM_IN to verify afterwards
+ * @rate: Pointer to store the rate
+ *
+ * The wIndex is not cosmetic. The device answers both forms: the vendor
+ * drivers read the live rate device-wide and always verify against the
+ * capture endpoint.
+ *
+ * Return: 0 on success, negative errno on failure.
+ */
+int ploytec_get_rate(struct usb_interface *intf, void *xfer_buf, u16 index, u32 *rate)
+{
+	struct usb_device *dev = interface_to_usbdev(intf);
+	u8 *buf = xfer_buf;
+	int ret;
+
+	ret = usb_control_msg_recv(dev, 0, PLOYTEC_REQ_GET_RATE, PLOYTEC_REQ_GET_RATE_TYPE,
+				   0x0100, index,
+				   buf, 3, PLOYTEC_CTRL_TIMEOUT_MS, GFP_KERNEL);
+	if (ret < 0)
+		return ret;
+
+	*rate = (u32)buf[0] | ((u32)buf[1] << 8) | ((u32)buf[2] << 16);
+	return 0;
+}
+
+/**
+ * ploytec_set_rate - Set hardware sample rate
+ * @intf: USB interface
+ * @xfer_buf: Temporary transfer buffer
+ * @rate: Sample rate in Hz
+ * @cold_init: true when this programs the rate as part of bringing the device
+ *	up, false when it changes the rate of a device already running
+ *
+ * The vendor drivers use two shapes here, and the difference is not cosmetic.
+ * A cold init starts programming the rate after a short gap; a rate change
+ * waits longer, writes once, pauses, then repeats the burst. Both end the
+ * burst on the capture endpoint and verify from it, which is the invariant
+ * that holds across every captured vendor sequence on both platforms; the
+ * write count itself is not load-bearing. The sleeps below stand in for
+ * windows in which the vendor host sends nothing at all -- it is waiting, not
+ * polling. See re/usb/init_timing_comparison.md.
+ *
+ * Return: 0 on success (including when the post-write rate verification
+ * detects a mismatch, which is only logged), negative errno if a control
+ * transfer fails.
+ */
+int ploytec_set_rate(struct usb_interface *intf, void *xfer_buf, u32 rate, bool cold_init)
+{
+	static const u16 burst_index[] = {
+		PLOYTEC_RATE_IDX_PCM_IN,
+		PLOYTEC_RATE_IDX_PCM_OUT,
+		PLOYTEC_RATE_IDX_PCM_IN,
+		PLOYTEC_RATE_IDX_PCM_OUT,
+		PLOYTEC_RATE_IDX_PCM_IN,
+	};
+	struct usb_device *dev = interface_to_usbdev(intf);
+	u8 *buf = xfer_buf;
+	u32 current_hw_rate = 0;
+	unsigned int i;
+	int ret;
+
+	dev_dbg(&intf->dev, "Setting rate %u Hz (%s)\n",
+		rate, cold_init ? "cold init" : "rate change");
+
+	buf[0] = rate & 0xFF;
+	buf[1] = (rate >> 8) & 0xFF;
+	buf[2] = (rate >> 16) & 0xFF;
+
+	if (cold_init) {
+		/* A fresh device gets a short gap and no separate first write. */
+		usleep_range(14000, 15000);
+	} else {
+		/* A rate change waits before touching the rate at all. */
+		usleep_range(50000, 51000);
+
+		ret = usb_control_msg_send(dev, 0, PLOYTEC_SET_RATE, PLOYTEC_SET_RATE_TYPE,
+					   0x0100, PLOYTEC_RATE_IDX_PCM_IN,
+					   buf, 3, PLOYTEC_CTRL_TIMEOUT_MS, GFP_KERNEL);
+		if (ret < 0) {
+			dev_err(&intf->dev, "Failed to set rate on EP 0x86: %d\n", ret);
+			return ret;
+		}
+
+		/* Write once, pause, then repeat the burst. */
+		usleep_range(10000, 11000);
+	}
+
+	for (i = 0; i < ARRAY_SIZE(burst_index); i++) {
+		ret = usb_control_msg_send(dev, 0, PLOYTEC_SET_RATE, PLOYTEC_SET_RATE_TYPE,
+					   0x0100, burst_index[i],
+					   buf, 3, PLOYTEC_CTRL_TIMEOUT_MS, GFP_KERNEL);
+		if (ret < 0) {
+			dev_err(&intf->dev, "Failed to set rate on EP 0x%02x: %d\n",
+				burst_index[i], ret);
+			return ret;
+		}
+	}
+
+	if (ploytec_get_rate(intf, xfer_buf, PLOYTEC_RATE_IDX_PCM_IN, &current_hw_rate) == 0) {
+		if (current_hw_rate != rate)
+			dev_warn(&intf->dev, "Rate mismatch! Requested %u Hz, Hardware at %u Hz\n",
+				 rate, current_hw_rate);
+		else
+			dev_dbg(&intf->dev, "Rate verified as %u Hz\n", current_hw_rate);
+	}
+
+	/*
+	 * Every vendor sequence goes quiet between verifying the rate and
+	 * programming the status byte, which is the caller's next step in
+	 * ploytec_start_streaming(). The window does not vary with the rate,
+	 * so there is nothing to scale.
+	 */
+	usleep_range(50000, 51000);
+
+	return 0;
+}
diff --git a/sound/usb/jockey3/ploytec_proto.h b/sound/usb/jockey3/ploytec_proto.h
new file mode 100644
index 0000000000000..5609e4cfd4ec8
--- /dev/null
+++ b/sound/usb/jockey3/ploytec_proto.h
@@ -0,0 +1,78 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ *   ALSA driver for Reloop Jockey 3 devices
+ *   Ploytec USB Protocol Handling
+ *
+ *   Copyright (c) 2026 by Frank van de Pol <fvdpol@gmail.com>
+ *
+ *   Comments here and in ploytec_proto.c cite documents by paths beginning
+ *   "re/". Those are the reverse-engineering notes this protocol layer was
+ *   derived from; they are not part of the kernel tree but live in the driver's
+ *   development repository, linked from its MAINTAINERS entry.
+ */
+
+#ifndef __SOUND_USB_JOCKEY3_PLOYTEC_PROTO_H
+#define __SOUND_USB_JOCKEY3_PLOYTEC_PROTO_H
+
+#include <linux/types.h>
+#include <linux/usb.h>
+
+#define USB_XFER_BUF_SIZE		64	// temporary buffer for USB control transfers
+
+/* Packet format/structure */
+#define PLOYTEC_PKT_SIZE		512
+#define PLOYTEC_MIDI_OUT_OFFSET		480	// location of MIDI data in the playback packet
+#define PLOYTEC_MIDI_IDLE_BYTE		0xFD	// send when no MIDI data is available
+#define PLOYTEC_SYNC_BYTE_OFFSET	481
+#define PLOYTEC_SYNC_BYTE_VALUE		0xFF
+
+/* USB Endpoint numbers*/
+#define PLOYTEC_EP_NUM_PCM_OUT		0x05	// Playback & MIDI Out (EP 0x05)
+#define PLOYTEC_EP_NUM_PCM_IN		0x06	// Capture (EP 0x86)
+#define PLOYTEC_EP_NUM_MIDI_IN		0x03	// MIDI In (EP 0x83)
+
+/* Protocol Commands */
+#define PLOYTEC_SET_RATE		0x01	// bRequest to set sample rate
+#define PLOYTEC_SET_RATE_TYPE		0x22	// bmRequestType to set sample rate
+#define PLOYTEC_SET_STATUS		0x49	// bRequest to set device status
+#define PLOYTEC_SET_STATUS_TYPE		0x40	// bmRequestType to set device status
+#define PLOYTEC_REQ_STATUS		0x49	// bRequest to get device status
+#define PLOYTEC_REQ_STATUS_TYPE		0xC0	// bmRequestType to get device status
+#define PLOYTEC_REQ_FIRMWARE		0x56	// bRequest to get firmware version
+#define PLOYTEC_REQ_FIRMWARE_TYPE	0xC0	// bmRequestType to get firmware version
+#define PLOYTEC_REQ_GET_RATE		0x81	// bRequest to get current sample rate
+#define PLOYTEC_REQ_GET_RATE_TYPE	0xA2	// bmRequestType to get current sample rate
+
+/*
+ * wIndex values for the rate requests: zero addresses the device as a whole,
+ * an endpoint address addresses one stream. Both forms are needed -- see
+ * re/protocol_analysis.md.
+ */
+#define PLOYTEC_RATE_IDX_DEVICE		0x0000	// device-wide, used before programming
+#define PLOYTEC_RATE_IDX_PCM_IN		(PLOYTEC_EP_NUM_PCM_IN | USB_DIR_IN)	// 0x86
+#define PLOYTEC_RATE_IDX_PCM_OUT	(PLOYTEC_EP_NUM_PCM_OUT)		// 0x05
+
+/* Status Bits (bits 0-4 are observed but not understood, and unused) */
+#define PLOYTEC_STATUS_STREAMING	0x20
+
+/* Timeout for the EP0 control transfers above, in milliseconds */
+#define PLOYTEC_CTRL_TIMEOUT_MS		2000
+
+/*
+ * Protocol Helpers
+ *
+ * All of these take the USB interface rather than the USB device so that
+ * dev_dbg()/dev_err()/dev_warn() attribute log messages to our driver
+ * (e.g. "snd-reloop-jockey3 1-13:1.0: ...") instead of to usbcore's generic
+ * per-device node (e.g. "usb 1-13: ..."), which is what struct usb_device's
+ * embedded struct device is bound to.
+ */
+int ploytec_initialize_device(struct usb_interface *intf, void *xfer_buf,
+			      bool bounce_alt0, u32 *fw_version);
+int ploytec_start_streaming(struct usb_interface *intf, void *xfer_buf);
+int ploytec_get_rate(struct usb_interface *intf, void *xfer_buf, u16 index, u32 *rate);
+int ploytec_set_rate(struct usb_interface *intf, void *xfer_buf, u32 rate, bool cold_init);
+int ploytec_get_firmware(struct usb_interface *intf, void *xfer_buf, u32 *fw_version);
+int ploytec_get_status(struct usb_interface *intf, void *xfer_buf, u8 *status);
+
+#endif /* __SOUND_USB_JOCKEY3_PLOYTEC_PROTO_H */
-- 
2.47.3


^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-09-25  2:19 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-25  2:19 [PATCH v4 0/1] ALSA: usb: Add support for Reloop Jockey 3 DJ controllers Frank van de Pol
2026-09-25  2:19 ` [PATCH v4 1/1] " Frank van de Pol

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®