[PATCH RFC 1/4] ALSA: pcm: Add common CEA channel allocation helpers
From: Cássio Gabriel
Date: Fri Jun 12 2026 - 00:54:08 EST
HDA HDMI and Intel HDMI LPE carry independent copies of the ordered
CEA-861 channel allocation table, ELD speaker expansion, and allocation
selection loop. The copies originated from the same implementation but
already differ in table coverage and fallback policy.
Move the immutable descriptors and common transformations next to the
other PCM DRM ELD helpers. Build the derived channel count and speaker
mask at compile time, and make table range and fallback policy explicit
arguments so consumers can preserve their existing contracts.
Signed-off-by: Cássio Gabriel <cassiogabrielcontato@xxxxxxxxx>
---
include/sound/pcm_drm_eld.h | 16 ++++
sound/core/pcm_drm_eld.c | 181 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 197 insertions(+)
diff --git a/include/sound/pcm_drm_eld.h b/include/sound/pcm_drm_eld.h
index 5a38413ada91..917228e97375 100644
--- a/include/sound/pcm_drm_eld.h
+++ b/include/sound/pcm_drm_eld.h
@@ -2,6 +2,8 @@
#ifndef __SOUND_PCM_DRM_ELD_H
#define __SOUND_PCM_DRM_ELD_H
+struct snd_info_buffer;
+
enum eld_versions {
ELD_VER_CEA_861D = 2,
ELD_VER_PARTIAL = 31,
@@ -84,6 +86,20 @@ struct snd_parsed_hdmi_eld {
struct snd_cea_sad sad[ELD_MAX_SAD];
};
+struct snd_cea_channel_speaker_allocation {
+ unsigned int ca_index;
+ unsigned int speakers[8];
+ unsigned int channels;
+ unsigned int spk_mask;
+};
+
+extern const struct snd_cea_channel_speaker_allocation
+ snd_cea_channel_allocations[];
+extern const unsigned int snd_cea_channel_allocations_count;
+unsigned int snd_cea_spk_mask_from_alloc(unsigned int spk_alloc);
+int snd_cea_channel_allocation(unsigned int spk_alloc, unsigned int channels,
+ unsigned int max_ca, bool fallback);
+
int snd_pcm_hw_constraint_eld(struct snd_pcm_runtime *runtime, void *eld);
int snd_parse_eld(struct device *dev, struct snd_parsed_hdmi_eld *e,
diff --git a/sound/core/pcm_drm_eld.c b/sound/core/pcm_drm_eld.c
index 1941ee520063..20156a9e3ed9 100644
--- a/sound/core/pcm_drm_eld.c
+++ b/sound/core/pcm_drm_eld.c
@@ -24,6 +24,187 @@
#define SAD1_RATE_176400_MASK BIT(5)
#define SAD1_RATE_192000_MASK BIT(6)
+/*
+ * CEA speaker placement:
+ *
+ * FLH FCH FRH
+ * FLW FL FLC FC FRC FR FRW
+ *
+ * LFE
+ * TC
+ *
+ * RL RLC RC RRC RR
+ *
+ * The Left/Right Surround channel notions LS/RS in SMPTE 320M correspond to
+ * CEA RL/RR; the SMPTE channel assignment C/LFE is swapped to CEA LFE/FC.
+ */
+enum cea_speaker_placement {
+ FL = BIT(0), /* Front Left */
+ FC = BIT(1), /* Front Center */
+ FR = BIT(2), /* Front Right */
+ FLC = BIT(3), /* Front Left Center */
+ FRC = BIT(4), /* Front Right Center */
+ RL = BIT(5), /* Rear Left */
+ RC = BIT(6), /* Rear Center */
+ RR = BIT(7), /* Rear Right */
+ RLC = BIT(8), /* Rear Left Center */
+ RRC = BIT(9), /* Rear Right Center */
+ LFE = BIT(10), /* Low Frequency Effect */
+ FLW = BIT(11), /* Front Left Wide */
+ FRW = BIT(12), /* Front Right Wide */
+ FLH = BIT(13), /* Front Left High */
+ FCH = BIT(14), /* Front Center High */
+ FRH = BIT(15), /* Front Right High */
+ TC = BIT(16), /* Top Center */
+};
+
+#define CEA_CHANNEL_ALLOCATION(_ca, _s7, _s6, _s5, _s4, _s3, _s2, _s1, _s0) \
+ { .ca_index = (_ca), .speakers = { (_s7), (_s6), (_s5), (_s4), \
+ (_s3), (_s2), (_s1), (_s0) }, \
+ .channels = !!(_s7) + !!(_s6) + !!(_s5) + !!(_s4) + \
+ !!(_s3) + !!(_s2) + !!(_s1) + !!(_s0), \
+ .spk_mask = (_s7) | (_s6) | (_s5) | (_s4) | (_s3) | (_s2) | \
+ (_s1) | (_s0) }
+
+/*
+ * This is an ordered list. Earlier entries have a better chance of being
+ * selected by snd_cea_channel_allocation().
+ */
+const struct snd_cea_channel_speaker_allocation snd_cea_channel_allocations[] = {
+ /* channel: 7 6 5 4 3 2 1 0 */
+ CEA_CHANNEL_ALLOCATION(0x00, 0, 0, 0, 0, 0, 0, FR, FL),
+ /* 2.1 */
+ CEA_CHANNEL_ALLOCATION(0x01, 0, 0, 0, 0, 0, LFE, FR, FL),
+ /* Dolby Surround */
+ CEA_CHANNEL_ALLOCATION(0x02, 0, 0, 0, 0, FC, 0, FR, FL),
+ /* surround40 */
+ CEA_CHANNEL_ALLOCATION(0x08, 0, 0, RR, RL, 0, 0, FR, FL),
+ /* surround41 */
+ CEA_CHANNEL_ALLOCATION(0x09, 0, 0, RR, RL, 0, LFE, FR, FL),
+ /* surround50 */
+ CEA_CHANNEL_ALLOCATION(0x0a, 0, 0, RR, RL, FC, 0, FR, FL),
+ /* surround51 */
+ CEA_CHANNEL_ALLOCATION(0x0b, 0, 0, RR, RL, FC, LFE, FR, FL),
+ /* 6.1 */
+ CEA_CHANNEL_ALLOCATION(0x0f, 0, RC, RR, RL, FC, LFE, FR, FL),
+ /* surround71 */
+ CEA_CHANNEL_ALLOCATION(0x13, RRC, RLC, RR, RL, FC, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x03, 0, 0, 0, 0, FC, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x04, 0, 0, 0, RC, 0, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x05, 0, 0, 0, RC, 0, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x06, 0, 0, 0, RC, FC, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x07, 0, 0, 0, RC, FC, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x0c, 0, RC, RR, RL, 0, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x0d, 0, RC, RR, RL, 0, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x0e, 0, RC, RR, RL, FC, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x10, RRC, RLC, RR, RL, 0, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x11, RRC, RLC, RR, RL, 0, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x12, RRC, RLC, RR, RL, FC, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x14, FRC, FLC, 0, 0, 0, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x15, FRC, FLC, 0, 0, 0, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x16, FRC, FLC, 0, 0, FC, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x17, FRC, FLC, 0, 0, FC, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x18, FRC, FLC, 0, RC, 0, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x19, FRC, FLC, 0, RC, 0, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x1a, FRC, FLC, 0, RC, FC, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x1b, FRC, FLC, 0, RC, FC, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x1c, FRC, FLC, RR, RL, 0, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x1d, FRC, FLC, RR, RL, 0, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x1e, FRC, FLC, RR, RL, FC, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x1f, FRC, FLC, RR, RL, FC, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x20, 0, FCH, RR, RL, FC, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x21, 0, FCH, RR, RL, FC, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x22, TC, 0, RR, RL, FC, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x23, TC, 0, RR, RL, FC, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x24, FRH, FLH, RR, RL, 0, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x25, FRH, FLH, RR, RL, 0, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x26, FRW, FLW, RR, RL, 0, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x27, FRW, FLW, RR, RL, 0, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x28, TC, RC, RR, RL, FC, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x29, TC, RC, RR, RL, FC, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x2a, FCH, RC, RR, RL, FC, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x2b, FCH, RC, RR, RL, FC, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x2c, TC, FCH, RR, RL, FC, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x2d, TC, FCH, RR, RL, FC, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x2e, FRH, FLH, RR, RL, FC, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x2f, FRH, FLH, RR, RL, FC, LFE, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x30, FRW, FLW, RR, RL, FC, 0, FR, FL),
+ CEA_CHANNEL_ALLOCATION(0x31, FRW, FLW, RR, RL, FC, LFE, FR, FL),
+};
+EXPORT_SYMBOL_GPL(snd_cea_channel_allocations);
+
+const unsigned int snd_cea_channel_allocations_count =
+ ARRAY_SIZE(snd_cea_channel_allocations);
+EXPORT_SYMBOL_GPL(snd_cea_channel_allocations_count);
+
+/* ELD SA bits in the CEA Speaker Allocation data block. */
+static const unsigned int eld_speaker_allocation_bits[] = {
+ [0] = FL | FR,
+ [1] = LFE,
+ [2] = FC,
+ [3] = RL | RR,
+ [4] = RC,
+ [5] = FLC | FRC,
+ [6] = RLC | RRC,
+ /* Not represented by the baseline ELD speaker allocation byte. */
+ [7] = FLW | FRW,
+ [8] = FLH | FRH,
+ [9] = TC,
+ [10] = FCH,
+};
+
+unsigned int snd_cea_spk_mask_from_alloc(unsigned int spk_alloc)
+{
+ unsigned int spk_mask = 0;
+ int i;
+
+ /* Expand ELD's compact, paired speaker allocation into a full mask. */
+ for (i = 0; i < ARRAY_SIZE(eld_speaker_allocation_bits); i++)
+ if (spk_alloc & BIT(i))
+ spk_mask |= eld_speaker_allocation_bits[i];
+
+ return spk_mask;
+}
+EXPORT_SYMBOL_GPL(snd_cea_spk_mask_from_alloc);
+
+/*
+ * The transformation takes two steps:
+ *
+ * spk_alloc => eld_speaker_allocation_bits[] => spk_mask
+ * spk_mask => snd_cea_channel_allocations[] => CA
+ *
+ * TODO: it could select the wrong CA from multiple candidates.
+ */
+int snd_cea_channel_allocation(unsigned int spk_alloc, unsigned int channels,
+ unsigned int max_ca, bool fallback)
+{
+ unsigned int spk_mask, i;
+
+ /* CA 0 is the basic stereo allocation. */
+ if (channels <= 2)
+ return 0;
+
+ spk_mask = snd_cea_spk_mask_from_alloc(spk_alloc);
+ for (i = 0; i < snd_cea_channel_allocations_count; i++) {
+ const struct snd_cea_channel_speaker_allocation *ca;
+
+ ca = &snd_cea_channel_allocations[i];
+ if (ca->ca_index <= max_ca && ca->channels == channels &&
+ (spk_mask & ca->spk_mask) == ca->spk_mask)
+ return ca->ca_index;
+ }
+
+ /* Fall back to the preferred allocation with the requested channels. */
+ if (fallback)
+ for (i = 0; i < snd_cea_channel_allocations_count; i++)
+ if (snd_cea_channel_allocations[i].ca_index <= max_ca &&
+ snd_cea_channel_allocations[i].channels == channels)
+ return snd_cea_channel_allocations[i].ca_index;
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(snd_cea_channel_allocation);
+
static const unsigned int eld_rates[] = {
32000,
44100,
--
2.54.0