git: 8e471fbc7dc4 - stable/14 - snd_hdspe(4): One pcm device per physical ADAT port.

From: Christos Margiolis <christos_at_FreeBSD.org>
Date: Tue, 30 Jan 2024 11:08:23 UTC
The branch stable/14 has been updated by christos:

URL: https://cgit.FreeBSD.org/src/commit/?id=8e471fbc7dc4ee0a6c6cc696b28858401ebc8d63

commit 8e471fbc7dc4ee0a6c6cc696b28858401ebc8d63
Author:     Florian Walpen <dev@submerge.ch>
AuthorDate: 2024-01-15 10:21:57 +0000
Commit:     Christos Margiolis <christos@FreeBSD.org>
CommitDate: 2024-01-30 11:07:28 +0000

    snd_hdspe(4): One pcm device per physical ADAT port.
    
    ADAT connections transport 8, 4 or 2 audio channels depending on the
    sample rate. Instead of splitting each physical ADAT port into 4
    (potentially unmapped) stereo pcm devices, create just one pcm
    device of variable channel width for every ADAT port.
    Depending on the sample rate and channel width selected, the pcm
    channels may be only partially mapped to ADAT channels and vice versa.
    
    Added flexibility of the new channel mapping is also prerequisite to
    introduce more pcm device layouts in follow-up commits.
    
    Reviewed by:    br
    Differential Revision:  https://reviews.freebsd.org/D43393
    
    (cherry picked from commit d7fde2c9eccf90b2a889e92800f3bc07376e84f6)
---
 share/man/man4/snd_hdspe.4    |  11 +-
 sys/dev/sound/pci/hdspe-pcm.c | 462 +++++++++++++++++++++++++++++++++++-------
 sys/dev/sound/pci/hdspe.c     |  52 ++---
 sys/dev/sound/pci/hdspe.h     |  36 +++-
 4 files changed, 443 insertions(+), 118 deletions(-)

diff --git a/share/man/man4/snd_hdspe.4 b/share/man/man4/snd_hdspe.4
index 4b925b14aef6..6023cd3d2ccd 100644
--- a/share/man/man4/snd_hdspe.4
+++ b/share/man/man4/snd_hdspe.4
@@ -22,7 +22,7 @@
 .\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 .\" SUCH DAMAGE.
 .\"
-.Dd December 30, 2023
+.Dd January 8, 2024
 .Dt SND_HDSPE 4
 .Os
 .Sh NAME
@@ -59,6 +59,13 @@ RME HDSPe AIO
 .It
 RME HDSPe RayDAT
 .El
+.Pp
+By default, each
+.Xr pcm 4
+device corresponds to a physical port on the sound card.
+For ADAT ports, 8 channel, 4 channel and 2 channel formats are supported.
+Depending on sample rate and channel format selected, not all pcm channels can
+be mapped to ADAT channels and vice versa.
 .Sh SYSCTL TUNABLES
 These settings and informational values can be accessed at runtime with the
 .Xr sysctl 8
@@ -111,3 +118,5 @@ The
 .Nm
 driver was written by
 .An Ruslan Bukin <br@bsdpad.com> .
+.An Florian Walpen <dev@submerge.ch>
+contributed clock source settings and restructured the pcm device mapping.
diff --git a/sys/dev/sound/pci/hdspe-pcm.c b/sys/dev/sound/pci/hdspe-pcm.c
index b3daed4d9599..d9d40c9877ad 100644
--- a/sys/dev/sound/pci/hdspe-pcm.c
+++ b/sys/dev/sound/pci/hdspe-pcm.c
@@ -2,6 +2,7 @@
  * SPDX-License-Identifier: BSD-2-Clause
  *
  * Copyright (c) 2012-2021 Ruslan Bukin <br@bsdpad.com>
+ * Copyright (c) 2023-2024 Florian Walpen <dev@submerge.ch>
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -78,6 +79,152 @@ static struct hdspe_rate rate_map[] = {
 	{ 0, 0 },
 };
 
+static uint32_t
+hdspe_channel_play_ports(struct hdspe_channel *hc)
+{
+	return (hc->ports & (HDSPE_CHAN_AIO_ALL | HDSPE_CHAN_RAY_ALL));
+}
+
+static uint32_t
+hdspe_channel_rec_ports(struct hdspe_channel *hc)
+{
+	return (hc->ports & (HDSPE_CHAN_AIO_ALL_REC | HDSPE_CHAN_RAY_ALL));
+}
+
+static unsigned int
+hdspe_adat_width(uint32_t speed)
+{
+	if (speed > 96000)
+		return (2);
+	if (speed > 48000)
+		return (4);
+	return (8);
+}
+
+static uint32_t
+hdspe_port_first(uint32_t ports)
+{
+	return (ports & (~(ports - 1)));	/* Extract first bit set. */
+}
+
+static uint32_t
+hdspe_port_first_row(uint32_t ports)
+{
+	uint32_t ends;
+
+	/* Restrict ports to one set with contiguous slots. */
+	if (ports & HDSPE_CHAN_AIO_LINE)
+		ports = HDSPE_CHAN_AIO_LINE;	/* Gap in the AIO slots here. */
+	else if (ports & HDSPE_CHAN_AIO_ALL)
+		ports &= HDSPE_CHAN_AIO_ALL;	/* Rest of the AIO slots. */
+	else if (ports & HDSPE_CHAN_RAY_ALL)
+		ports &= HDSPE_CHAN_RAY_ALL;	/* All RayDAT slots. */
+
+	/* Ends of port rows are followed by a port which is not in the set. */
+	ends = ports & (~(ports >> 1));
+	/* First row of contiguous ports ends in the first row end. */
+	return (ports & (ends ^ (ends - 1)));
+}
+
+static unsigned int
+hdspe_channel_count(uint32_t ports, uint32_t adat_width)
+{
+	unsigned int count = 0;
+
+	if (ports & HDSPE_CHAN_AIO_ALL) {
+		/* AIO ports. */
+		if (ports & HDSPE_CHAN_AIO_LINE)
+			count += 2;
+		if (ports & HDSPE_CHAN_AIO_PHONE)
+			count += 2;
+		if (ports & HDSPE_CHAN_AIO_AES)
+			count += 2;
+		if (ports & HDSPE_CHAN_AIO_SPDIF)
+			count += 2;
+		if (ports & HDSPE_CHAN_AIO_ADAT)
+			count += adat_width;
+	} else if (ports & HDSPE_CHAN_RAY_ALL) {
+		/* RayDAT ports. */
+		if (ports & HDSPE_CHAN_RAY_AES)
+			count += 2;
+		if (ports & HDSPE_CHAN_RAY_SPDIF)
+			count += 2;
+		if (ports & HDSPE_CHAN_RAY_ADAT1)
+			count += adat_width;
+		if (ports & HDSPE_CHAN_RAY_ADAT2)
+			count += adat_width;
+		if (ports & HDSPE_CHAN_RAY_ADAT3)
+			count += adat_width;
+		if (ports & HDSPE_CHAN_RAY_ADAT4)
+			count += adat_width;
+	}
+
+	return (count);
+}
+
+static unsigned int
+hdspe_channel_offset(uint32_t subset, uint32_t ports, unsigned int adat_width)
+{
+	uint32_t preceding;
+
+	/* Make sure we have a subset of ports. */
+	subset &= ports;
+	/* Include all ports preceding the first one of the subset. */
+	preceding = ports & (~subset & (subset - 1));
+
+	if (preceding & HDSPE_CHAN_AIO_ALL)
+		preceding &= HDSPE_CHAN_AIO_ALL;	/* Contiguous AIO slots. */
+	else if (preceding & HDSPE_CHAN_RAY_ALL)
+		preceding &= HDSPE_CHAN_RAY_ALL;	/* Contiguous RayDAT slots. */
+
+	return (hdspe_channel_count(preceding, adat_width));
+}
+
+static unsigned int
+hdspe_port_slot_offset(uint32_t port, unsigned int adat_width)
+{
+	/* Exctract the first port (lowest bit) if set of ports. */
+	switch (hdspe_port_first(port)) {
+	/* AIO ports */
+	case HDSPE_CHAN_AIO_LINE:
+		return (0);
+	case HDSPE_CHAN_AIO_PHONE:
+		return (6);
+	case HDSPE_CHAN_AIO_AES:
+		return (8);
+	case HDSPE_CHAN_AIO_SPDIF:
+		return (10);
+	case HDSPE_CHAN_AIO_ADAT:
+		return (12);
+
+	/* RayDAT ports */
+	case HDSPE_CHAN_RAY_AES:
+		return (0);
+	case HDSPE_CHAN_RAY_SPDIF:
+		return (2);
+	case HDSPE_CHAN_RAY_ADAT1:
+		return (4);
+	case HDSPE_CHAN_RAY_ADAT2:
+		return (4 + adat_width);
+	case HDSPE_CHAN_RAY_ADAT3:
+		return (4 + 2 * adat_width);
+	case HDSPE_CHAN_RAY_ADAT4:
+		return (4 + 3 * adat_width);
+	default:
+		return (0);
+	}
+}
+
+static unsigned int
+hdspe_port_slot_width(uint32_t ports, unsigned int adat_width)
+{
+	uint32_t row;
+
+	/* Count number of contiguous slots from the first physical port. */
+	row = hdspe_port_first_row(ports);
+	return (hdspe_channel_count(row, adat_width));
+}
+
 static int
 hdspe_hw_mixer(struct sc_chinfo *ch, unsigned int dst,
     unsigned int src, unsigned short data)
@@ -103,11 +250,34 @@ hdspe_hw_mixer(struct sc_chinfo *ch, unsigned int dst,
 static int
 hdspechan_setgain(struct sc_chinfo *ch)
 {
+	struct sc_info *sc;
+	uint32_t port, ports;
+	unsigned int slot, end_slot;
+	unsigned short volume;
+
+	sc = ch->parent->sc;
+
+	/* Iterate through all physical ports of the channel. */
+	ports = ch->ports;
+	port = hdspe_port_first(ports);
+	while (port != 0) {
+		/* Get slot range of the physical port. */
+		slot =
+		    hdspe_port_slot_offset(port, hdspe_adat_width(sc->speed));
+		end_slot = slot +
+		    hdspe_port_slot_width(port, hdspe_adat_width(sc->speed));
+
+		/* Treat first slot as left channel. */
+		volume = ch->lvol * HDSPE_MAX_GAIN / 100;
+		for (; slot < end_slot; slot++) {
+			hdspe_hw_mixer(ch, slot, slot, volume);
+			/* Subsequent slots all get the right channel volume. */
+			volume = ch->rvol * HDSPE_MAX_GAIN / 100;
+		}
 
-	hdspe_hw_mixer(ch, ch->lslot, ch->lslot,
-	    ch->lvol * HDSPE_MAX_GAIN / 100);
-	hdspe_hw_mixer(ch, ch->rslot, ch->rslot,
-	    ch->rvol * HDSPE_MAX_GAIN / 100);
+		ports &= ~port;
+		port = hdspe_port_first(ports);
+	}
 
 	return (0);
 }
@@ -126,10 +296,10 @@ hdspemixer_init(struct snd_mixer *m)
 
 	mask = SOUND_MASK_PCM;
 
-	if (scp->hc->play)
+	if (hdspe_channel_play_ports(scp->hc))
 		mask |= SOUND_MASK_VOLUME;
 
-	if (scp->hc->rec)
+	if (hdspe_channel_rec_ports(scp->hc))
 		mask |= SOUND_MASK_RECLEV;
 
 	snd_mtxlock(sc->lock);
@@ -181,7 +351,9 @@ hdspechan_enable(struct sc_chinfo *ch, int value)
 {
 	struct sc_pcminfo *scp;
 	struct sc_info *sc;
+	uint32_t row, ports;
 	int reg;
+	unsigned int slot, end_slot;
 
 	scp = ch->parent;
 	sc = scp->sc;
@@ -193,9 +365,22 @@ hdspechan_enable(struct sc_chinfo *ch, int value)
 
 	ch->run = value;
 
-	hdspe_write_1(sc, reg + (4 * ch->lslot), value);
-	if (AFMT_CHANNEL(ch->format) == 2)
-		hdspe_write_1(sc, reg + (4 * ch->rslot), value);
+	/* Iterate through rows of ports with contiguous slots. */
+	ports = ch->ports;
+	row = hdspe_port_first_row(ports);
+	while (row != 0) {
+		slot =
+		    hdspe_port_slot_offset(row, hdspe_adat_width(sc->speed));
+		end_slot = slot +
+		    hdspe_port_slot_width(row, hdspe_adat_width(sc->speed));
+
+		for (; slot < end_slot; slot++) {
+			hdspe_write_1(sc, reg + (4 * slot), value);
+		}
+
+		ports &= ~row;
+		row = hdspe_port_first_row(ports);
+	}
 }
 
 static int
@@ -253,56 +438,152 @@ hdspe_stop_audio(struct sc_info *sc)
 	hdspe_write_4(sc, HDSPE_CONTROL_REG, sc->ctrl_register);
 }
 
-/* Multiplex / demultiplex: 2.0 <-> 2 x 1.0. */
+static void
+buffer_mux_write(uint32_t *dma, uint32_t *pcm, unsigned int pos,
+    unsigned int samples, unsigned int slots, unsigned int channels)
+{
+	int slot;
+
+	for (; samples > 0; samples--) {
+		for (slot = 0; slot < slots; slot++) {
+			dma[slot * HDSPE_CHANBUF_SAMPLES + pos] =
+			    pcm[pos * channels + slot];
+		}
+		pos = (pos + 1) % HDSPE_CHANBUF_SAMPLES;
+	}
+}
+
+static void
+buffer_mux_port(uint32_t *dma, uint32_t *pcm, uint32_t subset, uint32_t ports,
+    unsigned int pos, unsigned int samples, unsigned int adat_width,
+    unsigned int pcm_width)
+{
+	unsigned int slot_offset, slots;
+	unsigned int channels, chan_pos;
+
+	/* Translate DMA slot offset to DMA buffer offset. */
+	slot_offset = hdspe_port_slot_offset(subset, adat_width);
+	dma += slot_offset * HDSPE_CHANBUF_SAMPLES;
+
+	/* Channel position of the port subset and total number of channels. */
+	chan_pos = hdspe_channel_offset(subset, ports, pcm_width);
+	pcm += chan_pos;
+	channels = hdspe_channel_count(ports, pcm_width);
+
+	/* Only copy as much as supported by both hardware and pcm channel. */
+	slots = hdspe_port_slot_width(subset, MIN(adat_width, pcm_width));
+
+	/* Let the compiler inline and loop unroll common cases. */
+	if (slots == 2)
+		buffer_mux_write(dma, pcm, pos, samples, 2, channels);
+	else if (slots == 4)
+		buffer_mux_write(dma, pcm, pos, samples, 4, channels);
+	else if (slots == 8)
+		buffer_mux_write(dma, pcm, pos, samples, 8, channels);
+	else
+		buffer_mux_write(dma, pcm, pos, samples, slots, channels);
+}
+
+static void
+buffer_demux_read(uint32_t *dma, uint32_t *pcm, unsigned int pos,
+    unsigned int samples, unsigned int slots, unsigned int channels)
+{
+	int slot;
+
+	for (; samples > 0; samples--) {
+		for (slot = 0; slot < slots; slot++) {
+			pcm[pos * channels + slot] =
+			    dma[slot * HDSPE_CHANBUF_SAMPLES + pos];
+		}
+		pos = (pos + 1) % HDSPE_CHANBUF_SAMPLES;
+	}
+}
+
+static void
+buffer_demux_port(uint32_t *dma, uint32_t *pcm, uint32_t subset, uint32_t ports,
+    unsigned int pos, unsigned int samples, unsigned int adat_width,
+    unsigned int pcm_width)
+{
+	unsigned int slot_offset, slots;
+	unsigned int channels, chan_pos;
+
+	/* Translate port slot offset to DMA buffer offset. */
+	slot_offset = hdspe_port_slot_offset(subset, adat_width);
+	dma += slot_offset * HDSPE_CHANBUF_SAMPLES;
+
+	/* Channel position of the port subset and total number of channels. */
+	chan_pos = hdspe_channel_offset(subset, ports, pcm_width);
+	pcm += chan_pos;
+	channels = hdspe_channel_count(ports, pcm_width);
+
+	/* Only copy as much as supported by both hardware and pcm channel. */
+	slots = hdspe_port_slot_width(subset, MIN(adat_width, pcm_width));
+
+	/* Let the compiler inline and loop unroll common cases. */
+	if (slots == 2)
+		buffer_demux_read(dma, pcm, pos, samples, 2, channels);
+	else if (slots == 4)
+		buffer_demux_read(dma, pcm, pos, samples, 4, channels);
+	else if (slots == 8)
+		buffer_demux_read(dma, pcm, pos, samples, 8, channels);
+	else
+		buffer_demux_read(dma, pcm, pos, samples, slots, channels);
+}
+
+
+/* Copy data between DMA and PCM buffers. */
 static void
 buffer_copy(struct sc_chinfo *ch)
 {
 	struct sc_pcminfo *scp;
 	struct sc_info *sc;
-	int ssize, dsize;
-	int src, dst;
-	int n;
-	int i;
+	uint32_t row, ports;
+	unsigned int pos;
+	unsigned int n;
+	unsigned int adat_width, pcm_width;
 
 	scp = ch->parent;
 	sc = scp->sc;
 
 	n = AFMT_CHANNEL(ch->format); /* n channels */
 
-	if (ch->dir == PCMDIR_PLAY) {
-		src = sndbuf_getreadyptr(ch->buffer);
-	} else {
-		src = sndbuf_getfreeptr(ch->buffer);
-	}
-
-	src /= 4; /* Bytes per sample. */
-	dst = src / n; /* Destination buffer n-times smaller. */
-
-	ssize = ch->size / 4;
-	dsize = ch->size / (4 * n);
+	/* Let pcm formats differ from current hardware ADAT width. */
+	adat_width = hdspe_adat_width(sc->speed);
+	if (n == hdspe_channel_count(ch->ports, 2))
+		pcm_width = 2;
+	else if (n == hdspe_channel_count(ch->ports, 4))
+		pcm_width = 4;
+	else
+		pcm_width = 8;
 
-	/*
-	 * Use two fragment buffer to avoid sound clipping.
-	 */
+	if (ch->dir == PCMDIR_PLAY)
+		pos = sndbuf_getreadyptr(ch->buffer);
+	else
+		pos = sndbuf_getfreeptr(ch->buffer);
 
-	for (i = 0; i < sc->period * 2 /* fragments */; i++) {
-		if (ch->dir == PCMDIR_PLAY) {
-			sc->pbuf[dst + HDSPE_CHANBUF_SAMPLES * ch->lslot] =
-			    ch->data[src];
-			sc->pbuf[dst + HDSPE_CHANBUF_SAMPLES * ch->rslot] =
-			    ch->data[src + 1];
-
-		} else {
-			ch->data[src] =
-			    sc->rbuf[dst + HDSPE_CHANBUF_SAMPLES * ch->lslot];
-			ch->data[src+1] =
-			    sc->rbuf[dst + HDSPE_CHANBUF_SAMPLES * ch->rslot];
-		}
+	pos /= 4; /* Bytes per sample. */
+	pos /= n; /* Destination buffer n-times smaller. */
 
-		dst+=1;
-		dst %= dsize;
-		src += n;
-		src %= ssize;
+	/* Iterate through rows of ports with contiguous slots. */
+	ports = ch->ports;
+	if (pcm_width == adat_width)
+		row = hdspe_port_first_row(ports);
+	else
+		row = hdspe_port_first(ports);
+
+	while (row != 0) {
+		if (ch->dir == PCMDIR_PLAY)
+			buffer_mux_port(sc->pbuf, ch->data, row, ch->ports, pos,
+			    sc->period * 2, adat_width, pcm_width);
+		else
+			buffer_demux_port(sc->rbuf, ch->data, row, ch->ports,
+			    pos, sc->period * 2, adat_width, pcm_width);
+
+		ports &= ~row;
+		if (pcm_width == adat_width)
+			row = hdspe_port_first_row(ports);
+		else
+			row = hdspe_port_first(ports);
 	}
 }
 
@@ -312,17 +593,30 @@ clean(struct sc_chinfo *ch)
 	struct sc_pcminfo *scp;
 	struct sc_info *sc;
 	uint32_t *buf;
+	uint32_t row, ports;
+	unsigned int offset, slots;
 
 	scp = ch->parent;
 	sc = scp->sc;
 	buf = sc->rbuf;
 
-	if (ch->dir == PCMDIR_PLAY) {
+	if (ch->dir == PCMDIR_PLAY)
 		buf = sc->pbuf;
-	}
 
-	bzero(buf + HDSPE_CHANBUF_SAMPLES * ch->lslot, HDSPE_CHANBUF_SIZE);
-	bzero(buf + HDSPE_CHANBUF_SAMPLES * ch->rslot, HDSPE_CHANBUF_SIZE);
+	/* Iterate through rows of ports with contiguous slots. */
+	ports = ch->ports;
+	row = hdspe_port_first_row(ports);
+	while (row != 0) {
+		offset = hdspe_port_slot_offset(row,
+		    hdspe_adat_width(sc->speed));
+		slots = hdspe_port_slot_width(row, hdspe_adat_width(sc->speed));
+
+		bzero(buf + offset * HDSPE_CHANBUF_SAMPLES,
+		    slots * HDSPE_CHANBUF_SIZE);
+
+		ports &= ~row;
+		row = hdspe_port_first_row(ports);
+	}
 
 	return (0);
 }
@@ -344,13 +638,29 @@ hdspechan_init(kobj_t obj, void *devinfo, struct snd_dbuf *b,
 	num = scp->chnum;
 
 	ch = &scp->chan[num];
-	ch->lslot = scp->hc->left;
-	ch->rslot = scp->hc->right;
+
+	if (dir == PCMDIR_PLAY)
+		ch->ports = hdspe_channel_play_ports(scp->hc);
+	else
+		ch->ports = hdspe_channel_rec_ports(scp->hc);
+
 	ch->run = 0;
 	ch->lvol = 0;
 	ch->rvol = 0;
 
-	ch->size = HDSPE_CHANBUF_SIZE * 2; /* max size */
+	/* Support all possible ADAT widths as channel formats. */
+	ch->cap_fmts[0] =
+	    SND_FORMAT(AFMT_S32_LE, hdspe_channel_count(ch->ports, 2), 0);
+	ch->cap_fmts[1] =
+	    SND_FORMAT(AFMT_S32_LE, hdspe_channel_count(ch->ports, 4), 0);
+	ch->cap_fmts[2] =
+	    SND_FORMAT(AFMT_S32_LE, hdspe_channel_count(ch->ports, 8), 0);
+	ch->cap_fmts[3] = 0;
+	ch->caps = malloc(sizeof(struct pcmchan_caps), M_HDSPE, M_NOWAIT);
+	*(ch->caps) = (struct pcmchan_caps) {32000, 192000, ch->cap_fmts, 0};
+
+	/* Allocate maximum buffer size. */
+	ch->size = HDSPE_CHANBUF_SIZE * hdspe_channel_count(ch->ports, 8);
 	ch->data = malloc(ch->size, M_HDSPE, M_NOWAIT);
 
 	ch->buffer = b;
@@ -430,8 +740,7 @@ hdspechan_getptr(kobj_t obj, void *data)
 	snd_mtxunlock(sc->lock);
 
 	pos = ret & HDSPE_BUF_POSITION_MASK;
-	if (AFMT_CHANNEL(ch->format) == 2)
-		pos *= 2; /* Hardbuf twice bigger. */
+	pos *= AFMT_CHANNEL(ch->format); /* Hardbuf with multiple channels. */
 
 	return (pos);
 }
@@ -456,6 +765,10 @@ hdspechan_free(kobj_t obj, void *data)
 		free(ch->data, M_HDSPE);
 		ch->data = NULL;
 	}
+	if (ch->caps != NULL) {
+		free(ch->caps, M_HDSPE);
+		ch->caps = NULL;
+	}
 	snd_mtxunlock(sc->lock);
 
 	return (0);
@@ -580,9 +893,8 @@ hdspechan_setblocksize(kobj_t obj, void *data, uint32_t blocksize)
 
 	/* First look for equal latency. */
 	for (i = 0; latency_map[i].period != 0; i++) {
-		if (latency_map[i].period == blocksize) {
+		if (latency_map[i].period == blocksize)
 			hl = &latency_map[i];
-		}
 	}
 
 	/* If no match, just find nearest. */
@@ -615,20 +927,12 @@ end:
 	return (sndbuf_getblksz(ch->buffer));
 }
 
-static uint32_t hdspe_rfmt[] = {
-	SND_FORMAT(AFMT_S32_LE, 2, 0),
-	0
-};
-
-static struct pcmchan_caps hdspe_rcaps = {32000, 192000, hdspe_rfmt, 0};
-
-static uint32_t hdspe_pfmt[] = {
-	SND_FORMAT(AFMT_S32_LE, 1, 0),
+static uint32_t hdspe_bkp_fmt[] = {
 	SND_FORMAT(AFMT_S32_LE, 2, 0),
 	0
 };
 
-static struct pcmchan_caps hdspe_pcaps = {32000, 192000, hdspe_pfmt, 0};
+static struct pcmchan_caps hdspe_bkp_caps = {32000, 192000, hdspe_bkp_fmt, 0};
 
 static struct pcmchan_caps *
 hdspechan_getcaps(kobj_t obj, void *data)
@@ -642,8 +946,10 @@ hdspechan_getcaps(kobj_t obj, void *data)
 	device_printf(scp->dev, "hdspechan_getcaps()\n");
 #endif
 
-	return ((ch->dir == PCMDIR_PLAY) ?
-	    &hdspe_pcaps : &hdspe_rcaps);
+	if (ch->caps != NULL)
+		return (ch->caps);
+
+	return (&hdspe_bkp_caps);
 }
 
 static kobj_method_t hdspechan_methods[] = {
@@ -695,13 +1001,21 @@ hdspe_pcm_attach(device_t dev)
 	char status[SND_STATUSLEN];
 	struct sc_pcminfo *scp;
 	char desc[64];
-	int i, err;
+	int err;
+	int play, rec;
 
 	scp = device_get_ivars(dev);
 	scp->ih = &hdspe_pcm_intr;
 
 	bzero(desc, sizeof(desc));
-	snprintf(desc, sizeof(desc), "HDSPe AIO [%s]", scp->hc->descr);
+	if (scp->hc->ports & HDSPE_CHAN_AIO_ALL)
+		snprintf(desc, sizeof(desc), "HDSPe AIO [%s]",
+		    scp->hc->descr);
+	else if (scp->hc->ports & HDSPE_CHAN_RAY_ALL)
+		snprintf(desc, sizeof(desc), "HDSPe RayDAT [%s]",
+		    scp->hc->descr);
+	else
+		snprintf(desc, sizeof(desc), "HDSPe ? [%s]", scp->hc->descr);
 	device_set_desc_copy(dev, desc);
 
 	/*
@@ -710,19 +1024,21 @@ hdspe_pcm_attach(device_t dev)
 	 */
 	pcm_setflags(dev, pcm_getflags(dev) | SD_F_MPSAFE);
 
-	err = pcm_register(dev, scp, scp->hc->play, scp->hc->rec);
+	play = (hdspe_channel_play_ports(scp->hc)) ? 1 : 0;
+	rec = (hdspe_channel_rec_ports(scp->hc)) ? 1 : 0;
+	err = pcm_register(dev, scp, play, rec);
 	if (err) {
 		device_printf(dev, "Can't register pcm.\n");
 		return (ENXIO);
 	}
 
 	scp->chnum = 0;
-	for (i = 0; i < scp->hc->play; i++) {
+	if (play) {
 		pcm_addchan(dev, PCMDIR_PLAY, &hdspechan_class, scp);
 		scp->chnum++;
 	}
 
-	for (i = 0; i < scp->hc->rec; i++) {
+	if (rec) {
 		pcm_addchan(dev, PCMDIR_REC, &hdspechan_class, scp);
 		scp->chnum++;
 	}
diff --git a/sys/dev/sound/pci/hdspe.c b/sys/dev/sound/pci/hdspe.c
index 8a7cac87fc1a..e0197d1e981a 100644
--- a/sys/dev/sound/pci/hdspe.c
+++ b/sys/dev/sound/pci/hdspe.c
@@ -2,6 +2,7 @@
  * SPDX-License-Identifier: BSD-2-Clause
  *
  * Copyright (c) 2012-2016 Ruslan Bukin <br@bsdpad.com>
+ * Copyright (c) 2023-2024 Florian Walpen <dev@submerge.ch>
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -69,47 +70,22 @@ static struct hdspe_clock_source hdspe_clock_source_table_aio[] = {
 };
 
 static struct hdspe_channel chan_map_aio[] = {
-	{  0,  1,   "line", 1, 1 },
-	{  6,  7,  "phone", 1, 0 },
-	{  8,  9,    "aes", 1, 1 },
-	{ 10, 11, "s/pdif", 1, 1 },
-	{ 12, 16,   "adat", 1, 1 },
-
-	/* Single or double speed. */
-	{ 14, 18,   "adat", 1, 1 },
-
-	/* Single speed only. */
-	{ 13, 15,   "adat", 1, 1 },
-	{ 17, 19,   "adat", 1, 1 },
-
-	{  0,  0,     NULL, 0, 0 },
+	{ HDSPE_CHAN_AIO_LINE,    "line" },
+	{ HDSPE_CHAN_AIO_PHONE,  "phone" },
+	{ HDSPE_CHAN_AIO_AES,      "aes" },
+	{ HDSPE_CHAN_AIO_SPDIF, "s/pdif" },
+	{ HDSPE_CHAN_AIO_ADAT,    "adat" },
+	{ 0,                        NULL },
 };
 
 static struct hdspe_channel chan_map_rd[] = {
-	{   0, 1,    "aes", 1, 1 },
-	{   2, 3, "s/pdif", 1, 1 },
-	{   4, 5,   "adat", 1, 1 },
-	{   6, 7,   "adat", 1, 1 },
-	{   8, 9,   "adat", 1, 1 },
-	{ 10, 11,   "adat", 1, 1 },
-
-	/* Single or double speed. */
-	{ 12, 13,   "adat", 1, 1 },
-	{ 14, 15,   "adat", 1, 1 },
-	{ 16, 17,   "adat", 1, 1 },
-	{ 18, 19,   "adat", 1, 1 },
-
-	/* Single speed only. */
-	{ 20, 21,   "adat", 1, 1 },
-	{ 22, 23,   "adat", 1, 1 },
-	{ 24, 25,   "adat", 1, 1 },
-	{ 26, 27,   "adat", 1, 1 },
-	{ 28, 29,   "adat", 1, 1 },
-	{ 30, 31,   "adat", 1, 1 },
-	{ 32, 33,   "adat", 1, 1 },
-	{ 34, 35,   "adat", 1, 1 },
-
-	{ 0,  0,      NULL, 0, 0 },
+	{ HDSPE_CHAN_RAY_AES,      "aes" },
+	{ HDSPE_CHAN_RAY_SPDIF, "s/pdif" },
+	{ HDSPE_CHAN_RAY_ADAT1,  "adat1" },
+	{ HDSPE_CHAN_RAY_ADAT2,  "adat2" },
+	{ HDSPE_CHAN_RAY_ADAT3,  "adat3" },
+	{ HDSPE_CHAN_RAY_ADAT4,  "adat4" },
+	{ 0,                        NULL },
 };
 
 static void
diff --git a/sys/dev/sound/pci/hdspe.h b/sys/dev/sound/pci/hdspe.h
index 58362641cb01..d5d8dd46e580 100644
--- a/sys/dev/sound/pci/hdspe.h
+++ b/sys/dev/sound/pci/hdspe.h
@@ -2,6 +2,7 @@
  * SPDX-License-Identifier: BSD-2-Clause
  *
  * Copyright (c) 2012-2016 Ruslan Bukin <br@bsdpad.com>
+ * Copyright (c) 2023-2024 Florian Walpen <dev@submerge.ch>
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -116,12 +117,34 @@
 #define	HDSPE_CHANBUF_SIZE		(4 * HDSPE_CHANBUF_SAMPLES)
 #define	HDSPE_DMASEGSIZE		(HDSPE_CHANBUF_SIZE * HDSPE_MAX_SLOTS)
 
+#define	HDSPE_CHAN_AIO_LINE		(1 << 0)
+#define	HDSPE_CHAN_AIO_PHONE		(1 << 1)
+#define	HDSPE_CHAN_AIO_AES		(1 << 2)
+#define	HDSPE_CHAN_AIO_SPDIF		(1 << 3)
+#define	HDSPE_CHAN_AIO_ADAT		(1 << 4)
+#define	HDSPE_CHAN_AIO_ALL_REC		(HDSPE_CHAN_AIO_LINE | \
+					HDSPE_CHAN_AIO_AES | \
+					HDSPE_CHAN_AIO_SPDIF | \
+					HDSPE_CHAN_AIO_ADAT)
+#define	HDSPE_CHAN_AIO_ALL		(HDSPE_CHAN_AIO_ALL_REC | \
+					HDSPE_CHAN_AIO_PHONE) \
+
+#define	HDSPE_CHAN_RAY_AES		(1 << 5)
+#define	HDSPE_CHAN_RAY_SPDIF		(1 << 6)
+#define	HDSPE_CHAN_RAY_ADAT1		(1 << 7)
+#define	HDSPE_CHAN_RAY_ADAT2		(1 << 8)
+#define	HDSPE_CHAN_RAY_ADAT3		(1 << 9)
+#define	HDSPE_CHAN_RAY_ADAT4		(1 << 10)
+#define	HDSPE_CHAN_RAY_ALL		(HDSPE_CHAN_RAY_AES | \
+					HDSPE_CHAN_RAY_SPDIF | \
+					HDSPE_CHAN_RAY_ADAT1 | \
+					HDSPE_CHAN_RAY_ADAT2 | \
+					HDSPE_CHAN_RAY_ADAT3 | \
+					HDSPE_CHAN_RAY_ADAT4)
+
 struct hdspe_channel {
-	uint32_t	left;
-	uint32_t	right;
+	uint32_t	ports;
 	char		*descr;
-	uint32_t	play;
-	uint32_t	rec;
 };
 
 /* Clock sources */
@@ -150,10 +173,11 @@ struct sc_chinfo {
 	struct sc_pcminfo	*parent;
 
 	/* Channel information */
+	struct pcmchan_caps	*caps;
+	uint32_t	cap_fmts[4];
 	uint32_t	dir;
 	uint32_t	format;
-	uint32_t	lslot;
-	uint32_t	rslot;
+	uint32_t	ports;
 	uint32_t	lvol;
 	uint32_t	rvol;