All of lore.kernel.org
 help / color / mirror / Atom feed
From: "Piotr Gliźniewicz" <p.glizniewicz@gmail.com>
To: Takashi Iwai <tiwai@suse.com>, Jaroslav Kysela <perex@perex.cz>
Cc: "Torsten Schenk" <torsten.schenk@zoho.com>,
	linux-sound@vger.kernel.org,
	"Piotr Gliźniewicz" <p.glizniewicz@gmail.com>
Subject: [PATCH 3/3] ALSA: 6fire: add S/PDIF output
Date: Fri, 16 Oct 2026 09:31:32 +0200	[thread overview]
Message-ID: <20261016073132.109288-4-p.glizniewicz@gmail.com> (raw)
In-Reply-To: <20261016073132.109288-1-p.glizniewicz@gmail.com>

The DMX 6Fire USB has a coaxial S/PDIF output, which the driver keeps
disabled. Enable it as playback channels 7 and 8 of the existing PCM
device, the layout chosen in the RFC thread linked below. The
protocol was taken from a usbmon capture of the vendor's Windows
driver and checked against a disassembly of that driver.

The packets of altsetting 3 (176.4 and 192 kHz) have room for the six
analog channels only, so the output is available up to 96 kHz.
hw_params rules allow more than 6 channels only up to 96 kHz, and
prepare rejects more than 6 channels while the other stream runs at
176.4 or 192 kHz. A client that takes channels_max therefore gets 8
channels at up to 96 kHz instead of 6 channels at up to 192 kHz (the
Pro Audio profile of PipeWire 1.6.9 opened the device with 8
channels); a client that asks for 6 channels still gets 192 kHz. A
UCM profile can describe the channels and offer a 6-channel
configuration with 192 kHz. The channel map marks channels 7 and 8
UNKNOWN; a program that ignores the map and assumes the 7.1 order
sends the side pair to the S/PDIF output.

The card has one rate and one frame layout for both directions, set by
the first prepare and kept until the last close, and the register bit
that enables the transmitter is the bit that widens the playback frame
from 6 to 8 slots. The driver therefore sets the bit at every rate up to
96 kHz, whichever direction starts first, so that a playback opened
later can use 8 channels; the transmitter sends what is in
slots 6 and 7, zeros while no playback runs. With "IEC958 Playback
Switch" off (it is on by default), the driver sends zeros in the pair
and omits the block start flag, and the receiver used for testing loses
lock. Two versions in which the switch also cleared the bit were tried
with PipeWire and a UCM profile and dropped. With the bit tied to the
switch, the channel count depended on the switch and a change had to be
refused while a stream ran, which broke profile changes. With the bit
set by the switch or by an 8-channel playback, the frame width depended
on which stream started first, and an 8-channel playback was refused
while a capture ran with the switch off. With the bit tied to the rate,
as in the vendor driver, the coaxial output carries a signal whenever
the card streams at up to 96 kHz; a receiver that selects its input when
it detects a signal was not tested. "Digital Thru Playback Route" has
priority: with it on, the output carries the coaxial input.

The channel status is consumer, 24 bit, with the sample rate and the
channel numbers. "IEC958 Playback Default" sets the non-audio,
copyright and emphasis bits, the category code and the generation bit;
"IEC958 Playback Con Mask" lists these bits. With the non-audio bit
set (AC3, DTS), the validity bit of every subframe is set as well, as
the vendor driver does. A change of "IEC958 Playback Default" or of
the switch takes effect at the next 192-frame block.

Tested on a DMX 6Fire USB at 44.1, 48 and 96 kHz with a second
interface locked to the output: a tagged ramp on channels 7/8 arrived
without a missing, repeated or changed sample for 4.7 s at each rate,
and at 48 kHz a -20 dBFS tone arrived at -20.0 dBFS while a tone on
channel 3 played on Line Out 3. With the switch off from the start,
the second interface did not lock during an 8-channel, a 6-channel or
a capture-only stream.

Link: https://lore.kernel.org/linux-sound/30dcedc7-3b56-4396-af3d-6c2701ef1cb7@perex.cz/
Assisted-by: LLM
Signed-off-by: Piotr Gliźniewicz <p.glizniewicz@gmail.com>
---
 sound/usb/6fire/control.c |  25 +++-
 sound/usb/6fire/pcm.c     | 308 +++++++++++++++++++++++++++++++++++---
 sound/usb/6fire/pcm.h     |   7 +-
 3 files changed, 316 insertions(+), 24 deletions(-)

diff --git a/sound/usb/6fire/control.c b/sound/usb/6fire/control.c
index 020e9676d..da5a2d7ac 100644
--- a/sound/usb/6fire/control.c
+++ b/sound/usb/6fire/control.c
@@ -149,9 +149,14 @@ static int usb6fire_control_set_channels(
 	if (ret < 0)
 		return ret;
 
-	/* disable digital inputs and outputs */
-	/* TODO: use spdif_x to enable/disable digital channels */
-	ret = comm_rt->write16(comm_rt, 0x02, 0x03, 0x00, 0x00);
+	/*
+	 * digital (S/PDIF) input and output: bit 0 adds four slots (4-7) to
+	 * the capture frame, which carry the subframes of the receiver; up to
+	 * two receiver frames arrive per card frame; bit 1 enables the
+	 * transmitter, which encodes the playback pair in slots 6/7 (pcm.c)
+	 */
+	ret = comm_rt->write16(comm_rt, 0x02, 0x03, 0x00,
+			(spdif_in ? 0x01 : 0x00) | (spdif_out ? 0x02 : 0x00));
 	if (ret < 0)
 		return ret;
 
@@ -161,14 +166,26 @@ static int usb6fire_control_set_channels(
 static int usb6fire_control_streaming_update(struct control_runtime *rt)
 {
 	struct comm_runtime *comm_rt = rt->chip->comm;
+	int err = 0;
+	int ret;
 
 	if (comm_rt) {
+		/*
+		 * the digital pairs are enabled only while a stream runs; the
+		 * streaming register is written even if this write fails.
+		 * usb6fire_control_digital_thru_put() holds stream_mutex, so
+		 * this write cannot run after set_channels() and before
+		 * streaming starts in usb6fire_pcm_set_rate()
+		 */
+		if (!rt->usb_streaming)
+			err = comm_rt->write16(comm_rt, 0x02, 0x03, 0x00, 0x00);
 		if (!rt->usb_streaming && rt->digital_thru_switch)
 			usb6fire_control_set_rate(rt,
 				DIGITAL_THRU_ONLY_SAMPLERATE);
-		return comm_rt->write16(comm_rt, 0x02, 0x00, 0x00,
+		ret = comm_rt->write16(comm_rt, 0x02, 0x00, 0x00,
 			(rt->usb_streaming ? 0x01 : 0x00) |
 			(rt->digital_thru_switch ? 0x08 : 0x00));
+		return err < 0 ? err : ret;
 	}
 	return -EINVAL;
 }
diff --git a/sound/usb/6fire/pcm.c b/sound/usb/6fire/pcm.c
index d99395229..f5dfb8c30 100644
--- a/sound/usb/6fire/pcm.c
+++ b/sound/usb/6fire/pcm.c
@@ -9,13 +9,34 @@
  * Copyright:	(C) Torsten Schenk
  */
 
+#include <linux/bitops.h>
+#include <sound/asoundef.h>
+#include <sound/control.h>
+
 #include "pcm.h"
 #include "chip.h"
 #include "comm.h"
 #include "control.h"
 
+/*
+ * Every USB frame is a 4 byte header followed by 32-bit slots, one per
+ * channel: 3 bytes of sample and a marker byte. Analog slots are marked
+ * 0x40. Up to 96 kHz (altsettings 1 and 2) the playback frame carries two
+ * more slots, 6/7, which carry the data for the S/PDIF transmitter in
+ * the FPGA. Their marker byte tells the transmitter how to frame the
+ * sample: 0x10 marks the left subframe and 0x00 the right one; 0x20 marks
+ * the start of the channel status block (left subframe, every 192
+ * frames); 0x04 is the channel status bit of the subframe; 0x01 is the
+ * validity bit, set for non-audio data; 0x08 is the parity bit, which
+ * makes the number of ones in the sample, the validity and channel status
+ * bits and itself even.
+ * At altsetting 3 (176.4/192 kHz) the packets hold the analog slots only.
+ */
 enum {
-	OUT_N_CHANNELS = 6, IN_N_CHANNELS = 4
+	OUT_N_ANALOG = 6, N_SPDIF = 2,
+	OUT_N_CHANNELS = OUT_N_ANALOG + N_SPDIF, IN_N_CHANNELS = 4,
+	SPDIF_OUT_SLOT = OUT_N_ANALOG,
+	SPDIF_BLOCK_FRAMES = 192
 };
 
 /* keep next two synced with
@@ -24,6 +45,15 @@ enum {
 static const int rates_in_packet_size[] = { 228, 228, 420, 420, 404, 404 };
 static const int rates_out_packet_size[] = { 228, 228, 420, 420, 604, 604 };
 static const int rates[] = { 44100, 48000, 88200, 96000, 176400, 192000 };
+static const u8 rates_iec958_fs[] = {
+	IEC958_AES3_CON_FS_44100, IEC958_AES3_CON_FS_48000,
+	IEC958_AES3_CON_FS_88200, IEC958_AES3_CON_FS_96000,
+	IEC958_AES3_CON_FS_176400, IEC958_AES3_CON_FS_192000 };
+/* channel status bits that "IEC958 Playback Default" sets, bytes 0 and 1 */
+static const u8 spdif_ctl_mask[] = {
+	IEC958_AES0_NONAUDIO | IEC958_AES0_CON_NOT_COPYRIGHT |
+		IEC958_AES0_CON_EMPHASIS,
+	IEC958_AES1_CON_CATEGORY | IEC958_AES1_CON_ORIGINAL };
 static const int rates_alsaid[] = {
 	SNDRV_PCM_RATE_44100, SNDRV_PCM_RATE_48000,
 	SNDRV_PCM_RATE_88200, SNDRV_PCM_RATE_96000,
@@ -71,6 +101,7 @@ static int usb6fire_pcm_set_rate(struct pcm_runtime *rt)
 {
 	int ret;
 	struct control_runtime *ctrl_rt = rt->chip->control;
+	bool spdif;
 
 	ctrl_rt->usb_streaming = false;
 	ret = ctrl_rt->update_streaming(ctrl_rt);
@@ -89,8 +120,9 @@ static int usb6fire_pcm_set_rate(struct pcm_runtime *rt)
 		return ret;
 	}
 
-	ret = ctrl_rt->set_channels(ctrl_rt, OUT_N_CHANNELS, IN_N_CHANNELS,
-			false, false);
+	spdif = rt->rate <= CONTROL_RATE_96KHZ;
+	ret = ctrl_rt->set_channels(ctrl_rt, OUT_N_ANALOG, IN_N_CHANNELS,
+			spdif, false);
 	if (ret < 0) {
 		dev_err(&rt->chip->dev->dev,
 			"error initializing channels while setting samplerate %d.\n",
@@ -104,16 +136,108 @@ static int usb6fire_pcm_set_rate(struct pcm_runtime *rt)
 		dev_err(&rt->chip->dev->dev,
 			"error starting streaming while setting samplerate %d.\n",
 			rates[rt->rate]);
+		/* stream_stop() will not run; clear 0x02/0x03 here */
+		ctrl_rt->usb_streaming = false;
+		ctrl_rt->update_streaming(ctrl_rt);
 		return ret;
 	}
 
 	rt->in_n_analog = IN_N_CHANNELS;
-	rt->out_n_analog = OUT_N_CHANNELS;
+	rt->out_n_slots = spdif ? OUT_N_CHANNELS : OUT_N_ANALOG;
 	rt->in_packet_size = rates_in_packet_size[rt->rate];
 	rt->out_packet_size = rates_out_packet_size[rt->rate];
+
+	/*
+	 * consumer channel status, 24 bit samples; bytes 0 and 1 are taken
+	 * from rt->spdif_bits at the start of every block
+	 */
+	memset(rt->spdif_status, 0, sizeof(rt->spdif_status));
+	rt->spdif_status[3] = rates_iec958_fs[rt->rate];
+	rt->spdif_status[4] = IEC958_AES4_CON_MAX_WORDLEN_24 |
+			IEC958_AES4_CON_WORDLEN_24_20;
+	rt->spdif_frame = 0;
 	return 0;
 }
 
+/*
+ * Return the marker byte of one playback slot whose 3 sample bytes are
+ * already in place. The marker is 0x40 for an analog slot and the S/PDIF
+ * framing for a digital slot. With "IEC958 Playback Switch" off, the
+ * function also clears the sample bytes of a digital slot. After the right
+ * subframe, the function advances the channel status position.
+ */
+static u8 usb6fire_pcm_marker(struct pcm_runtime *rt, int slot,
+		u8 *sample)
+{
+	bool right = slot == SPDIF_OUT_SLOT + 1;
+	unsigned int n = rt->spdif_frame;
+	u8 marker = right ? 0x00 : 0x10;
+	u8 status;
+
+	if (slot < SPDIF_OUT_SLOT)
+		return 0x40;
+	if (!right && n == 0) {
+		/* a new block takes the bits of "IEC958 Playback Default" */
+		u32 bits = READ_ONCE(rt->spdif_bits);
+
+		rt->spdif_status[0] = bits & 0xff;
+		rt->spdif_status[1] = bits >> 8;
+		/*
+		 * "IEC958 Playback Switch" off omits the block start flag and
+		 * clears the sample bytes of the digital slots
+		 */
+		rt->spdif_on = READ_ONCE(rt->spdif_switch);
+		if (rt->spdif_on)
+			marker |= 0x20;
+	}
+	if (!rt->spdif_on)
+		memset(sample, 0, 3);
+	status = rt->spdif_status[n >> 3];
+	/* byte 2 holds the channel number: 1 = A (left), 2 = B (right) */
+	if ((n >> 3) == 2)
+		status |= right ? (2 << 4) : (1 << 4);
+	if (status & (1 << (n & 7)))
+		marker |= 0x04;
+	/* V = 1 for non-audio data (AC3, DTS), as the vendor driver does */
+	if (rt->spdif_status[0] & IEC958_AES0_NONAUDIO)
+		marker |= 0x01;
+	/* P makes the number of ones in bits 0-27 (sample, V, U, C, P) even */
+	if (parity8(sample[0] ^ sample[1] ^ sample[2] ^ (marker & 0x07)))
+		marker |= 0x08;
+	if (right)
+		rt->spdif_frame = (n + 1) % SPDIF_BLOCK_FRAMES;
+	return marker;
+}
+
+/* more than OUT_N_ANALOG playback channels need a rate up to 96 kHz */
+static int usb6fire_pcm_rule_channels(struct snd_pcm_hw_params *params,
+		struct snd_pcm_hw_rule *rule)
+{
+	const struct snd_interval *r =
+		hw_param_interval_c(params, SNDRV_PCM_HW_PARAM_RATE);
+	struct snd_interval t = { .min = 1, .max = OUT_N_ANALOG, .integer = 1 };
+
+	if (r->min <= rates[CONTROL_RATE_96KHZ])
+		return 0;
+	return snd_interval_refine(hw_param_interval(params,
+			SNDRV_PCM_HW_PARAM_CHANNELS), &t);
+}
+
+static int usb6fire_pcm_rule_rate(struct snd_pcm_hw_params *params,
+		struct snd_pcm_hw_rule *rule)
+{
+	const struct snd_interval *c =
+		hw_param_interval_c(params, SNDRV_PCM_HW_PARAM_CHANNELS);
+	struct snd_interval t = {
+		.min = 0, .max = rates[CONTROL_RATE_96KHZ], .integer = 1
+	};
+
+	if (c->min <= OUT_N_ANALOG)
+		return 0;
+	return snd_interval_refine(hw_param_interval(params,
+			SNDRV_PCM_HW_PARAM_RATE), &t);
+}
+
 static struct pcm_substream *usb6fire_pcm_get_substream(
 		struct snd_pcm_substream *alsa_sub)
 {
@@ -264,19 +388,21 @@ static void usb6fire_pcm_playback(struct pcm_substream *sub,
 	}
 
 	for (i = 0; i < PCM_N_PACKETS_PER_URB; i++) {
-		/* at least 4 header bytes for valid packet.
-		 * after that: 32 bits per sample for analog channels */
+		/*
+		 * at least 4 header bytes for valid packet.
+		 * after that: 32 bits per slot
+		 */
 		isoc = &urb->instance->iso_frame_desc[i];
 		if (isoc->length > 4)
 			frame_count = (isoc->length - 4)
-					/ (rt->out_n_analog << 2);
+					/ (rt->out_n_slots << 2);
 		else
 			frame_count = 0;
 		dest++; /* skip leading 4 bytes of every frame */
 		for (frame = 0; frame < frame_count; frame++) {
 			memcpy(dest, src, bytes_per_frame);
 			src += alsa_rt->channels;
-			dest += rt->out_n_analog;
+			dest += rt->out_n_slots;
 			sub->dma_off++;
 			sub->period_off++;
 			if (src == src_end) {
@@ -301,6 +427,7 @@ static void usb6fire_pcm_in_urb_handler(struct urb *usb_urb)
 	int channel;
 	int i;
 	u8 *dest;
+	unsigned int spdif_frame;
 
 	if (usb_urb->status || rt->panic || rt->stream_state == STREAM_STOPPING)
 		return;
@@ -344,10 +471,10 @@ static void usb6fire_pcm_in_urb_handler(struct urb *usb_urb)
 					/ (rt->in_n_analog << 2);
 		frames = min_t(unsigned int, frames,
 				       (rt->out_packet_size - 4)
-				       / (rt->out_n_analog << 2));
+				       / (rt->out_n_slots << 2));
 
 		isoc_out->offset = total_length;
-		isoc_out->length = frames * (rt->out_n_analog << 2) + 4;
+		isoc_out->length = frames * (rt->out_n_slots << 2) + 4;
 		isoc_out->status = 0;
 		total_length += isoc_out->length;
 	}
@@ -368,28 +495,38 @@ static void usb6fire_pcm_in_urb_handler(struct urb *usb_urb)
 	if (period_elapsed)
 		snd_pcm_period_elapsed(sub->instance);
 
-	/* setup the 4th byte of each sample (0x40 for analog channels) */
+	/*
+	 * set the 4th byte of each sample (0x40 for analog channels,
+	 * S/PDIF framing for the digital pair)
+	 */
+	spdif_frame = rt->spdif_frame;
 	dest = out_urb->buffer;
 	for (i = 0; i < PCM_N_PACKETS_PER_URB; i++) {
 		isoc_out = &out_urb->instance->iso_frame_desc[i];
 		if (isoc_out->length >= 4) {
 			frame_count = (isoc_out->length - 4)
-					/ (rt->out_n_analog << 2);
+					/ (rt->out_n_slots << 2);
 			*(dest++) = 0xaa;
 			*(dest++) = 0xaa;
 			*(dest++) = frame_count;
 			*(dest++) = 0x00;
 			for (frame = 0; frame < frame_count; frame++)
 				for (channel = 0;
-						channel < rt->out_n_analog;
+						channel < rt->out_n_slots;
 						channel++) {
-					dest += 3; /* skip sample data */
-					*(dest++) = 0x40;
+					dest[3] = usb6fire_pcm_marker(rt,
+							channel, dest);
+					dest += 4;
 				}
 		}
 	}
 
-	usb_submit_urb(out_urb->instance, GFP_ATOMIC);
+	/*
+	 * the frames of an out urb that was not submitted never reach the
+	 * transmitter; the next urb continues the block at the same position
+	 */
+	if (usb_submit_urb(out_urb->instance, GFP_ATOMIC) < 0)
+		rt->spdif_frame = spdif_frame;
 	usb_submit_urb(in_urb->instance, GFP_ATOMIC);
 }
 
@@ -409,6 +546,7 @@ static int usb6fire_pcm_open(struct snd_pcm_substream *alsa_sub)
 	struct pcm_runtime *rt = snd_pcm_substream_chip(alsa_sub);
 	struct pcm_substream *sub = NULL;
 	struct snd_pcm_runtime *alsa_rt = alsa_sub->runtime;
+	int ret;
 
 	if (rt->panic)
 		return -EPIPE;
@@ -420,6 +558,17 @@ static int usb6fire_pcm_open(struct snd_pcm_substream *alsa_sub)
 		if (rt->rate < ARRAY_SIZE(rates))
 			alsa_rt->hw.rates = rates_alsaid[rt->rate];
 		alsa_rt->hw.channels_max = OUT_N_CHANNELS;
+		ret = snd_pcm_hw_rule_add(alsa_rt, 0,
+				SNDRV_PCM_HW_PARAM_CHANNELS,
+				usb6fire_pcm_rule_channels, NULL,
+				SNDRV_PCM_HW_PARAM_RATE, -1);
+		if (ret < 0)
+			return ret;
+		ret = snd_pcm_hw_rule_add(alsa_rt, 0, SNDRV_PCM_HW_PARAM_RATE,
+				usb6fire_pcm_rule_rate, NULL,
+				SNDRV_PCM_HW_PARAM_CHANNELS, -1);
+		if (ret < 0)
+			return ret;
 		sub = &rt->playback;
 	} else if (alsa_sub->stream == SNDRV_PCM_STREAM_CAPTURE) {
 		if (rt->rate < ARRAY_SIZE(rates))
@@ -489,7 +638,17 @@ static int usb6fire_pcm_prepare(struct snd_pcm_substream *alsa_sub)
 				alsa_rt->rate);
 			return -EINVAL;
 		}
-
+	}
+	/* the digital pair is not available at 176.4 and 192 kHz */
+	if (rt->rate > CONTROL_RATE_96KHZ &&
+			alsa_sub->stream == SNDRV_PCM_STREAM_PLAYBACK &&
+			alsa_rt->channels > OUT_N_ANALOG) {
+		dev_err(&rt->chip->dev->dev,
+			"%d channels not available at %d Hz.\n",
+			alsa_rt->channels, rates[rt->rate]);
+		return -EINVAL;
+	}
+	if (rt->stream_state == STREAM_DISABLED) {
 		ret = usb6fire_pcm_set_rate(rt);
 		if (ret)
 			return ret;
@@ -612,11 +771,79 @@ static void usb6fire_pcm_free(struct pcm_runtime *rt)
 	kfree(rt);
 }
 
+static int usb6fire_pcm_spdif_info(struct snd_kcontrol *kcontrol,
+		struct snd_ctl_elem_info *uinfo)
+{
+	uinfo->type = SNDRV_CTL_ELEM_TYPE_IEC958;
+	uinfo->count = 1;
+	return 0;
+}
+
+static int usb6fire_pcm_spdif_mask_get(struct snd_kcontrol *kcontrol,
+		struct snd_ctl_elem_value *ucontrol)
+{
+	memset(ucontrol->value.iec958.status, 0,
+	       sizeof(ucontrol->value.iec958.status));
+	ucontrol->value.iec958.status[0] = spdif_ctl_mask[0];
+	ucontrol->value.iec958.status[1] = spdif_ctl_mask[1];
+	return 0;
+}
+
+static int usb6fire_pcm_spdif_default_get(struct snd_kcontrol *kcontrol,
+		struct snd_ctl_elem_value *ucontrol)
+{
+	struct pcm_runtime *rt = snd_kcontrol_chip(kcontrol);
+	u32 bits = READ_ONCE(rt->spdif_bits);
+
+	memset(ucontrol->value.iec958.status, 0,
+	       sizeof(ucontrol->value.iec958.status));
+	ucontrol->value.iec958.status[0] = bits & 0xff;
+	ucontrol->value.iec958.status[1] = bits >> 8;
+	return 0;
+}
+
+static int usb6fire_pcm_spdif_default_put(struct snd_kcontrol *kcontrol,
+		struct snd_ctl_elem_value *ucontrol)
+{
+	struct pcm_runtime *rt = snd_kcontrol_chip(kcontrol);
+	u32 bits = (ucontrol->value.iec958.status[0] & spdif_ctl_mask[0]) |
+		(ucontrol->value.iec958.status[1] & spdif_ctl_mask[1]) << 8;
+
+	/* the control core serializes puts; the urb handler uses READ_ONCE */
+	if (rt->spdif_bits == bits)
+		return 0;
+	WRITE_ONCE(rt->spdif_bits, bits);
+	return 1;
+}
+
+static int usb6fire_pcm_spdif_switch_get(struct snd_kcontrol *kcontrol,
+		struct snd_ctl_elem_value *ucontrol)
+{
+	struct pcm_runtime *rt = snd_kcontrol_chip(kcontrol);
+
+	ucontrol->value.integer.value[0] = READ_ONCE(rt->spdif_switch);
+	return 0;
+}
+
+static int usb6fire_pcm_spdif_switch_put(struct snd_kcontrol *kcontrol,
+		struct snd_ctl_elem_value *ucontrol)
+{
+	struct pcm_runtime *rt = snd_kcontrol_chip(kcontrol);
+	bool on = ucontrol->value.integer.value[0];
+
+	/* the control core serializes puts; the urb handler uses READ_ONCE */
+	if (rt->spdif_switch == on)
+		return 0;
+	WRITE_ONCE(rt->spdif_switch, on);
+	return 1;
+}
+
 /*
  * playback channel map: channel n goes to analog output n, which the
  * manual of the card assigns to FL FR FC LFE RL RR for n = 1..6; a
  * 1-channel stream is MONO, as in snd_pcm_std_chmaps, and plays on
- * analog output 1 only
+ * analog output 1 only; channels 7 and 8 are the S/PDIF pair and have
+ * no position
  */
 static const struct snd_pcm_chmap_elem usb6fire_pcm_chmaps[] = {
 	{ .channels = 1, .map = { SNDRV_CHMAP_MONO } },
@@ -631,9 +858,41 @@ static const struct snd_pcm_chmap_elem usb6fire_pcm_chmaps[] = {
 	{ .channels = 6, .map = { SNDRV_CHMAP_FL, SNDRV_CHMAP_FR,
 				  SNDRV_CHMAP_FC, SNDRV_CHMAP_LFE,
 				  SNDRV_CHMAP_RL, SNDRV_CHMAP_RR } },
+	{ .channels = 7, .map = { SNDRV_CHMAP_FL, SNDRV_CHMAP_FR,
+				  SNDRV_CHMAP_FC, SNDRV_CHMAP_LFE,
+				  SNDRV_CHMAP_RL, SNDRV_CHMAP_RR,
+				  SNDRV_CHMAP_UNKNOWN } },
+	{ .channels = 8, .map = { SNDRV_CHMAP_FL, SNDRV_CHMAP_FR,
+				  SNDRV_CHMAP_FC, SNDRV_CHMAP_LFE,
+				  SNDRV_CHMAP_RL, SNDRV_CHMAP_RR,
+				  SNDRV_CHMAP_UNKNOWN, SNDRV_CHMAP_UNKNOWN } },
 	{ }
 };
 
+static const struct snd_kcontrol_new usb6fire_pcm_spdif_controls[] = {
+	{
+		.iface = SNDRV_CTL_ELEM_IFACE_PCM,
+		.name = SNDRV_CTL_NAME_IEC958("", PLAYBACK, DEFAULT),
+		.info = usb6fire_pcm_spdif_info,
+		.get = usb6fire_pcm_spdif_default_get,
+		.put = usb6fire_pcm_spdif_default_put,
+	},
+	{
+		.iface = SNDRV_CTL_ELEM_IFACE_PCM,
+		.name = SNDRV_CTL_NAME_IEC958("", PLAYBACK, CON_MASK),
+		.access = SNDRV_CTL_ELEM_ACCESS_READ,
+		.info = usb6fire_pcm_spdif_info,
+		.get = usb6fire_pcm_spdif_mask_get,
+	},
+	{
+		.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
+		.name = SNDRV_CTL_NAME_IEC958("", PLAYBACK, SWITCH),
+		.info = snd_ctl_boolean_mono_info,
+		.get = usb6fire_pcm_spdif_switch_get,
+		.put = usb6fire_pcm_spdif_switch_put,
+	},
+};
+
 int usb6fire_pcm_init(struct sfire_chip *chip)
 {
 	int i;
@@ -652,6 +911,9 @@ int usb6fire_pcm_init(struct sfire_chip *chip)
 	rt->chip = chip;
 	rt->stream_state = STREAM_DISABLED;
 	rt->rate = ARRAY_SIZE(rates);
+	rt->spdif_switch = true;
+	rt->spdif_bits = IEC958_AES0_CON_NOT_COPYRIGHT |
+			IEC958_AES1_CON_GENERAL << 8;
 	init_waitqueue_head(&rt->stream_wait_queue);
 	mutex_init(&rt->stream_mutex);
 
@@ -688,7 +950,15 @@ int usb6fire_pcm_init(struct sfire_chip *chip)
 
 	chip->pcm = rt;
 
-	/* on error, rt is freed with the card */
+	/* on error, rt and the controls added so far are freed with the card */
+	for (i = 0; i < ARRAY_SIZE(usb6fire_pcm_spdif_controls); i++) {
+		ret = snd_ctl_add(chip->card,
+				snd_ctl_new1(&usb6fire_pcm_spdif_controls[i], rt));
+		if (ret < 0) {
+			dev_err(&chip->dev->dev, "cannot add S/PDIF control.\n");
+			return ret;
+		}
+	}
 	ret = snd_pcm_add_chmap_ctls(pcm, SNDRV_PCM_STREAM_PLAYBACK,
 			usb6fire_pcm_chmaps, OUT_N_CHANNELS, 0, NULL);
 	if (ret < 0) {
diff --git a/sound/usb/6fire/pcm.h b/sound/usb/6fire/pcm.h
index b586fe220..f1b867b9c 100644
--- a/sound/usb/6fire/pcm.h
+++ b/sound/usb/6fire/pcm.h
@@ -53,7 +53,12 @@ struct pcm_runtime {
 	int in_packet_size;
 	int out_packet_size;
 	int in_n_analog; /* number of analog channels soundcard sends */
-	int out_n_analog; /* number of analog channels soundcard receives */
+	int out_n_slots; /* 32-bit slots per frame the soundcard receives */
+	bool spdif_switch; /* "IEC958 Playback Switch" */
+	bool spdif_on; /* copy of spdif_switch, read at the start of each block */
+	unsigned int spdif_frame; /* position in the channel status block */
+	u8 spdif_status[24]; /* IEC 60958 channel status of the S/PDIF output */
+	u32 spdif_bits; /* status bytes 0 and 1, "IEC958 Playback Default" */
 
 	struct mutex stream_mutex;
 	u8 stream_state; /* one of STREAM_XXX (pcm.c) */
-- 
2.56.0


  parent reply	other threads:[~2026-10-16  7:31 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-16  7:31 [PATCH 0/3] ALSA: 6fire: S/PDIF output Piotr Gliźniewicz
2026-10-16  7:31 ` [PATCH 1/3] ALSA: 6fire: take stream_mutex in the Digital Thru control Piotr Gliźniewicz
2026-10-16  7:31 ` [PATCH 2/3] ALSA: 6fire: add a playback channel map Piotr Gliźniewicz
2026-10-16  7:31 ` Piotr Gliźniewicz [this message]
2026-10-16 16:46 ` [PATCH 0/3] ALSA: 6fire: S/PDIF output Takashi Iwai

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=20261016073132.109288-4-p.glizniewicz@gmail.com \
    --to=p.glizniewicz@gmail.com \
    --cc=linux-sound@vger.kernel.org \
    --cc=perex@perex.cz \
    --cc=tiwai@suse.com \
    --cc=torsten.schenk@zoho.com \
    /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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.