mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Frank van de Pol <fvdpol@gmail.com>
To: tiwai@suse.de, perex@perex.cz
Cc: corbet@lwn.net, khan@linuxfoundation.org, rdunlap@infradead.org,
	fvdpol@gmail.com, linux-sound@vger.kernel.org,
	linux-kernel@vger.kernel.org
Subject: [PATCH v4 1/1] ALSA: usb: Add support for Reloop Jockey 3 DJ controllers
Date: Thu, 24 Sep 2026 22:19:14 -0400	[thread overview]
Message-ID: <20260925021915.78909-2-fvdpol@gmail.com> (raw)
In-Reply-To: <20260925021915.78909-1-fvdpol@gmail.com>

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


      reply	other threads:[~2026-09-25  2:19 UTC|newest]

Thread overview: 2+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-25  2:19 [PATCH v4 0/1] " Frank van de Pol
2026-09-25  2:19 ` Frank van de Pol [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260925021915.78909-2-fvdpol@gmail.com \
    --to=fvdpol@gmail.com \
    --cc=corbet@lwn.net \
    --cc=khan@linuxfoundation.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-sound@vger.kernel.org \
    --cc=perex@perex.cz \
    --cc=rdunlap@infradead.org \
    --cc=tiwai@suse.de \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
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®