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 v5 03/13] ALSA: jockey3: Add PCM playback
Date: Mon, 19 Oct 2026 13:44:35 -0400	[thread overview]
Message-ID: <20261019174445.1931263-4-fvdpol@gmail.com> (raw)
In-Reply-To: <20261019174445.1931263-1-fvdpol@gmail.com>

Add a PCM device with a 4-channel, 24-bit playback stream at 44.1 kHz,
and the encoder that turns S24_3LE frames into the Ploytec wire format.

The wire format is bit-plane interleaved rather than sample-interleaved:
every byte on the wire holds one bit of each channel. The comment at the
top of ploytec_codec.c explains where that layout comes from. This patch
adds the portable implementation, which is written to be read as a
definition of the format. Optimized variants follow once tests exist.

The URBs keep running as before. The PCM callbacks only decide whether
the playback completion handler fills a packet from the ALSA buffer or
with silence:

 - .trigger and .pointer run with the stream lock held and must not
   sleep, so they only take the stream's own spinlock.
 - The completion handler drops that spinlock before it calls
   snd_pcm_period_elapsed(), which takes the stream lock.
 - .sync_stop waits for completion handlers that may still touch the
   substream or its buffer before the core frees either.

The channel map marks the master pair as FL/FR and leaves the headphone
pair unpositioned, because it is a second stereo output and not a pair
of rear speakers.

Assisted-by: LLM
Signed-off-by: Frank van de Pol <fvdpol@gmail.com>
---
 sound/usb/jockey3/Kconfig         |   1 +
 sound/usb/jockey3/Makefile        |   2 +-
 sound/usb/jockey3/jockey3.c       | 455 +++++++++++++++++++++++++++++-
 sound/usb/jockey3/ploytec_codec.c | 101 +++++++
 sound/usb/jockey3/ploytec_codec.h |  26 ++
 5 files changed, 571 insertions(+), 14 deletions(-)
 create mode 100644 sound/usb/jockey3/ploytec_codec.c
 create mode 100644 sound/usb/jockey3/ploytec_codec.h

diff --git a/sound/usb/jockey3/Kconfig b/sound/usb/jockey3/Kconfig
index 5714f46c57de3..83135d65b3ffd 100644
--- a/sound/usb/jockey3/Kconfig
+++ b/sound/usb/jockey3/Kconfig
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: GPL-2.0-only
 config SND_USB_JOCKEY3
 	tristate "Reloop Jockey 3 support"
+	select SND_PCM
 	help
 	  Say Y here to include support for the Reloop Jockey 3 DJ controllers.
 	  These devices utilize a non-standard, proprietary Ploytec USB
diff --git a/sound/usb/jockey3/Makefile b/sound/usb/jockey3/Makefile
index 4a3d02b7176e0..0e81aea50f9ac 100644
--- a/sound/usb/jockey3/Makefile
+++ b/sound/usb/jockey3/Makefile
@@ -1,3 +1,3 @@
 # SPDX-License-Identifier: GPL-2.0
 obj-$(CONFIG_SND_USB_JOCKEY3) += snd-reloop-jockey3.o
-snd-reloop-jockey3-y := jockey3.o ploytec_proto.o
+snd-reloop-jockey3-y := jockey3.o ploytec_proto.o ploytec_codec.o
diff --git a/sound/usb/jockey3/jockey3.c b/sound/usb/jockey3/jockey3.c
index 351612b1fbe33..3863ed12cf566 100644
--- a/sound/usb/jockey3/jockey3.c
+++ b/sound/usb/jockey3/jockey3.c
@@ -11,12 +11,16 @@
 #include <linux/usb.h>
 #include <linux/slab.h>
 #include <linux/delay.h>
+#include <linux/jiffies.h>
 #include <linux/bitops.h>
 #include <linux/mutex.h>
+#include <linux/wait.h>
 #include <linux/cleanup.h>
 #include <sound/core.h>
 #include <sound/initval.h>
+#include <sound/pcm.h>
 #include "ploytec_proto.h"
+#include "ploytec_codec.h"
 
 #define RELOOP_VENDOR_ID         0x200c
 #define RELOOP_JOCKEY3_ME_PID    0x1019
@@ -72,9 +76,14 @@ MODULE_PARM_DESC(enable, "Enable " CARD_NAME " soundcard.");
  * - 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 implementation.
+ *
  * 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.
+ * 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.
  */
 
 /**
@@ -85,9 +94,9 @@ MODULE_PARM_DESC(enable, "Enable " CARD_NAME " soundcard.");
  * 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 holding the card open would
- * stall hotplug for the whole hub. Every other USB sound driver in the tree
- * avoids that the same way.
+ * 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:
  *
@@ -103,7 +112,9 @@ MODULE_PARM_DESC(enable, "Enable " CARD_NAME " soundcard.");
  *   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.
+ * 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 ends the same
  * way, with snd_card_free_when_closed(): a failing snd_card_register() may
@@ -117,6 +128,17 @@ MODULE_PARM_DESC(enable, "Enable " CARD_NAME " soundcard.");
  * There are three locks, all IRQ-safe leaf spinlocks: playback.lock,
  * capture.lock and midi_lock. They are never nested inside one another;
  * anything that needs more than one takes them in sequence, not nested.
+ *
+ * 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.
  */
 
 #define JOCKEY3_N_URBS 8
@@ -126,11 +148,21 @@ MODULE_PARM_DESC(enable, "Enable " CARD_NAME " soundcard.");
 
 /**
  * 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
  * @urbs_in_flight: number of submitted URBs; diagnostic, must reach 0 after a stop
  * @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
+ * @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
@@ -139,11 +171,17 @@ MODULE_PARM_DESC(enable, "Enable " CARD_NAME " soundcard.");
  *	successful completion and by jockey3_start_urbs().
  */
 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;
 	spinlock_t lock;	/* protects this stream's state; IRQ-safe leaf */
+	unsigned int dma_off;
+	unsigned int period_off;
+	bool running;
+	unsigned int callbacks_active;
+	wait_queue_head_t drain_wait;
 	bool stopping;
 	unsigned int consec_errors;
 };
@@ -153,10 +191,11 @@ struct jockey3_pcm_urb_stream {
  * @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 before
- *	jockey3_free_resources() runs, which releases them.
+ *	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
  * @xfer_buf: bounce buffer for EP0 control transfers, USB_XFER_BUF_SIZE bytes.
  * @flags: JOCKEY3_FLAG_* bits, atomic bitops
  * @dev_idx: card slot held in jockey3_devices_used
@@ -173,6 +212,7 @@ struct jockey3_chip {
 	struct usb_device *dev;
 	struct usb_interface *intf0;
 	struct usb_interface *intf1;
+	struct snd_pcm *pcm;
 	unsigned char *xfer_buf;
 	unsigned long flags;
 	unsigned int dev_idx;
@@ -239,6 +279,69 @@ static inline bool jockey3_is_disconnected(const struct jockey3_chip *chip)
 	return test_bit(JOCKEY3_FLAG_DISCONNECTED, &chip->flags);
 }
 
+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 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;
+}
+
 enum jockey3_urb_state {
 	JOCKEY3_URB_OK,		/* completed normally */
 	JOCKEY3_URB_STOPPED,	/* teardown in progress; return without resubmitting */
@@ -292,6 +395,43 @@ static void jockey3_warn_unexpected_stop(struct jockey3_chip *chip, bool stoppin
 			     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
@@ -375,6 +515,8 @@ static bool jockey3_urb_error_give_up(struct jockey3_chip *chip,
 		"%s stopped after %u consecutive URB errors; deferring recovery\n",
 		type, errors);
 
+	jockey3_report_xrun(urb_stream);
+
 	return true;
 }
 
@@ -432,6 +574,19 @@ static void jockey3_capture_callback(struct urb *urb)
 		jockey3_resubmit_failed(chip, ret, "capture");
 }
 
+/*
+ * 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
@@ -449,7 +604,10 @@ 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;
+	bool period_elapsed = false;
 	bool data_valid = true;
+	bool active = false;
 	bool stopping;
 	int i, ret;
 
@@ -473,6 +631,23 @@ static void jockey3_playback_callback(struct urb *urb)
 	if (unlikely(jockey3_is_disconnected(chip)))
 		return;
 
+	/* 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;
+
+			period_elapsed = jockey3_process_out_packet(chip, buf);
+		} else {
+			jockey3_silence_out_packet(buf);
+		}
+	}
+
 	/*
 	 * Every packet reserves one slot for a MIDI OUT byte. Nothing feeds it
 	 * yet, so it carries the idle byte.
@@ -484,10 +659,21 @@ static void jockey3_playback_callback(struct urb *urb)
 	for (i = PLOYTEC_SYNC_BYTE_OFFSET + 1; i < PLOYTEC_PKT_SIZE; i++)
 		buf[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) {
-		if (data_valid)
-			urb_stream->consec_errors = 0;
+		/* 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
@@ -591,7 +777,8 @@ static void jockey3_stop_urbs(struct jockey3_chip *chip)
 	/*
 	 * 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 running once these return.
+	 * 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
@@ -797,6 +984,190 @@ static int jockey3_set_rate(struct jockey3_chip *chip, unsigned int rate, bool c
 	return 0;
 }
 
+/*
+ * 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.
+ */
+#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
+
+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		= 44100,
+	.channels_min		= 4,
+	.channels_max		= 4,
+	.buffer_bytes_max	= 1024 * 1024,
+	/*
+	 * One playback URB carries one packet of 10 frames * 4 channels * 3
+	 * bytes = 120 bytes. A URB reports at most one elapsed period, so the
+	 * minimum must cover at least one packet, 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 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 = jockey3_pcm_hw_playback;
+
+	/* 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;
+
+	/* 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);
+
+	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;
+	}
+
+	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);
+
+	dev_dbg(&chip->intf0->dev, "PCM prepare stream %d\n", substream->stream);
+	if (jockey3_is_disconnected(chip))
+		return -ENODEV;
+
+	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.
+ *
+ * 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:
+			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;
@@ -820,6 +1191,15 @@ static int jockey3_initialize_ploytec(struct jockey3_chip *chip, u32 *fw_version
 	return 0;
 }
 
+static const struct snd_pcm_ops jockey3_pcm_ops = {
+	.open = jockey3_pcm_open,
+	.close = jockey3_pcm_close,
+	.prepare = jockey3_pcm_prepare,
+	.trigger = jockey3_pcm_trigger,
+	.sync_stop = jockey3_pcm_sync_stop,
+	.pointer = jockey3_pcm_pointer,
+};
+
 static int jockey3_initialize(struct jockey3_chip *chip, int model)
 {
 	int ret;
@@ -1051,6 +1431,44 @@ static int jockey3_validate_endpoints(struct usb_interface *intf0, struct usb_in
 	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.
+ *
+ * Physical layout:
+ *   Playback: 1-2 Master Out L/R, 3-4 Headphone L/R
+ */
+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 int jockey3_init_pcm(struct jockey3_chip *chip)
+{
+	int ret = snd_pcm_new(chip->card, CARD_NAME " Audio", 0, 1, 0, &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_managed_buffer_all(chip->pcm, SNDRV_DMA_TYPE_VMALLOC, NULL, 0, 0);
+
+	return snd_pcm_add_chmap_ctls(chip->pcm, SNDRV_PCM_STREAM_PLAYBACK,
+				      jockey3_playback_chmap, 4, 0, NULL);
+}
+
 static void jockey3_setup_card_names(struct jockey3_chip *chip, int driver_info)
 {
 	/*
@@ -1097,7 +1515,7 @@ static int jockey3_probe(struct usb_interface *intf, const struct usb_device_id
 	 * 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
-	 * holding the card open would stall hotplug for the whole hub. The card is
+	 * 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.
@@ -1132,6 +1550,8 @@ static int jockey3_probe(struct usb_interface *intf, const struct usb_device_id
 
 	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);
 
 	/*
 	 * From here on every failure goes to err_free_card, and the card slot is
@@ -1168,6 +1588,10 @@ static int jockey3_probe(struct usb_interface *intf, const struct usb_device_id
 	if (ret < 0)
 		goto err_free_card;
 
+	ret = jockey3_init_pcm(chip);
+	if (ret < 0)
+		goto err_free_card;
+
 	jockey3_setup_card_names(chip, usb_id->driver_info);
 
 	if (card->id[0] == '\0')
@@ -1220,7 +1644,8 @@ static void jockey3_disconnect(struct usb_interface *intf)
 	struct jockey3_chip *chip = usb_get_intfdata(intf);
 
 	/*
-	 * Latch DISCONNECTED first, and for EITHER interface.
+	 * Latch DISCONNECTED first, and for EITHER interface. Every ALSA entry
+	 * point tests it on the way in.
 	 *
 	 * 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
@@ -1260,7 +1685,10 @@ static void jockey3_disconnect(struct usb_interface *intf)
 		 * 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()). @chip lives in card->private_data and may be gone
+		 * 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);
@@ -1302,3 +1730,4 @@ module_usb_driver(jockey3_driver);
 MODULE_AUTHOR("Frank van de Pol <fvdpol@gmail.com>");
 MODULE_DESCRIPTION(CARD_NAME " ALSA Driver");
 MODULE_LICENSE("GPL");
+MODULE_SOFTDEP("pre: snd-pcm");
diff --git a/sound/usb/jockey3/ploytec_codec.c b/sound/usb/jockey3/ploytec_codec.c
new file mode 100644
index 0000000000000..2c431853b8434
--- /dev/null
+++ b/sound/usb/jockey3/ploytec_codec.c
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ *   ALSA driver for Reloop Jockey 3 devices
+ *   Ploytec PCM encoding functions for the S24_3LE format (3 bytes per sample)
+ *
+ *   Copyright (c) 2026 by Frank van de Pol <fvdpol@gmail.com>
+ */
+
+#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.
+ */
+
+/*
+ * This 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.
+ */
+
+/**
+ * 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_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_s24_3le(dest, src);
+		dest += PLOYTEC_PLAYBACK_FRAME_SIZE;
+		src += PLOYTEC_PLAYBACK_PCM_FRAME_SIZE;
+	}
+}
diff --git a/sound/usb/jockey3/ploytec_codec.h b/sound/usb/jockey3/ploytec_codec.h
new file mode 100644
index 0000000000000..32660f1b4350a
--- /dev/null
+++ b/sound/usb/jockey3/ploytec_codec.h
@@ -0,0 +1,26 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ *   ALSA driver for Reloop Jockey 3 devices
+ *   Ploytec PCM encoding 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 of 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
+
+/* batching of the sample processing */
+void ploytec_encode_batch(u8 *dest, const u8 *src, const int n_frames);
+
+#endif /* __SOUND_USB_JOCKEY3_PLOYTEC_CODEC_H */
-- 
2.47.3


  parent reply	other threads:[~2026-10-19 17:46 UTC|newest]

Thread overview: 14+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-19 17:44 [PATCH v5 00/13] ALSA: jockey3: Add support for Reloop Jockey 3 DJ controllers Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 01/13] ALSA: jockey3: Add probe skeleton " Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 02/13] ALSA: jockey3: Start the bulk streams Frank van de Pol
2026-10-19 17:44 ` Frank van de Pol [this message]
2026-10-19 17:44 ` [PATCH v5 04/13] ALSA: jockey3: Add PCM capture Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 05/13] ALSA: jockey3: Add KUnit tests for the Ploytec codec Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 06/13] ALSA: jockey3: Add optimized codec variants Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 07/13] ALSA: jockey3: Coalesce several packets into one URB Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 08/13] ALSA: jockey3: Recover a stalled stream Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 09/13] ALSA: jockey3: Add a watchdog for mid-stream stalls Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 10/13] ALSA: jockey3: Add sample rate switching Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 11/13] ALSA: jockey3: Add MIDI support Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 12/13] ALSA: jockey3: Add power management Frank van de Pol
2026-10-19 17:44 ` [PATCH v5 13/13] ALSA: jockey3: Add driver documentation Frank van de Pol

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=20261019174445.1931263-4-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®