]> git.alsa-project.org Git - tinycompress.git/commitdiff
Utils: cplay: Fix WAV header parsing for multi-channel (5.1/7.1) formats
authorSeppo Ingalsuo <seppo.ingalsuo@linux.intel.com>
Wed, 11 Mar 2026 10:27:37 +0000 (12:27 +0200)
committerJaroslav Kysela <perex@perex.cz>
Fri, 29 May 2026 11:24:13 +0000 (13:24 +0200)
Multi-channel WAV files (>2 channels) use WAVE_FORMAT_EXTENSIBLE
(type 0xFFFE) with a larger fmt chunk that includes a channel mask
and subformat GUID. The existing parser only handled basic PCM
(type 0x0001), causing several issues with 5.1 (6ch) and 7.1 (8ch)
content:

 - The fixed-size fread of struct wave_header (44 bytes) misaligned
   the data chunk read when the fmt chunk was larger than 16 bytes,
   resulting in audio data being read from the wrong file offset.

 - No chunk scanning was performed, so intermediate chunks (fact,
   LIST, PEAK) between fmt and data caused parse failures.

 - The WAV channel mask (speaker positions FL, FR, FC, LFE, BL, BR,
   SL, SR) was never extracted, so firmware received ch_mode=0 and
   could not determine the correct channel-to-speaker routing.

Add parse_wave_file() that properly scans chunks, handles both basic
PCM and WAVE_FORMAT_EXTENSIBLE formats, extracts the channel mask,
and positions the file pointer at the start of audio data. For basic
PCM files with >2 channels, a standard default channel mask is
generated per the Microsoft WAV specification.

The channel mask is passed to firmware via snd_codec.ch_mode so it
can correctly map channels to speaker positions. Also add 24-bit
sample format support (SNDRV_PCM_FORMAT_S24_LE).

Closes: https://github.com/alsa-project/tinycompress/pull/32
Signed-off-by: Seppo Ingalsuo <seppo.ingalsuo@linux.intel.com>
Signed-off-by: Jaroslav Kysela <perex@perex.cz>
include/tinycompress/tinywave.h
src/utils/cplay.c
src/utils/wave.c

index 619cb8d09f2fb0dbb1d5d483a29047a6704418bb..7a4c08b357d9391608eb26830bb56c1418d85d24 100644 (file)
@@ -8,6 +8,8 @@
 #ifndef __TINYWAVE_H
 #define __TINYWAVE_H
 
+#include <stdio.h>
+
 struct riff_chunk {
        char desc[4];
        uint32_t size;
@@ -34,10 +36,71 @@ struct wave_header {
        } __attribute__((__packed__)) data;
 } __attribute__((__packed__));
 
+/* WAVE format types */
+#define WAVE_FORMAT_PCM                        0x0001
+#define WAVE_FORMAT_EXTENSIBLE         0xFFFE
+
+/* WAV channel mask - speaker position bit flags (Microsoft standard) */
+#define WAV_SPEAKER_FRONT_LEFT                 0x00000001
+#define WAV_SPEAKER_FRONT_RIGHT                        0x00000002
+#define WAV_SPEAKER_FRONT_CENTER               0x00000004
+#define WAV_SPEAKER_LOW_FREQUENCY              0x00000008
+#define WAV_SPEAKER_BACK_LEFT                  0x00000010
+#define WAV_SPEAKER_BACK_RIGHT                 0x00000020
+#define WAV_SPEAKER_FRONT_LEFT_OF_CENTER       0x00000040
+#define WAV_SPEAKER_FRONT_RIGHT_OF_CENTER      0x00000080
+#define WAV_SPEAKER_BACK_CENTER                        0x00000100
+#define WAV_SPEAKER_SIDE_LEFT                  0x00000200
+#define WAV_SPEAKER_SIDE_RIGHT                 0x00000400
+#define WAV_SPEAKER_TOP_CENTER                 0x00000800
+#define WAV_SPEAKER_TOP_FRONT_LEFT             0x00001000
+#define WAV_SPEAKER_TOP_FRONT_CENTER           0x00002000
+#define WAV_SPEAKER_TOP_FRONT_RIGHT            0x00004000
+#define WAV_SPEAKER_TOP_BACK_LEFT              0x00008000
+#define WAV_SPEAKER_TOP_BACK_CENTER            0x00010000
+#define WAV_SPEAKER_TOP_BACK_RIGHT             0x00020000
+
+/* Standard channel masks for common multi-channel configurations */
+#define WAV_CHANNEL_MASK_MONO          (WAV_SPEAKER_FRONT_CENTER)
+#define WAV_CHANNEL_MASK_STEREO                (WAV_SPEAKER_FRONT_LEFT | \
+                                        WAV_SPEAKER_FRONT_RIGHT)
+#define WAV_CHANNEL_MASK_5_1           (WAV_SPEAKER_FRONT_LEFT | \
+                                        WAV_SPEAKER_FRONT_RIGHT | \
+                                        WAV_SPEAKER_FRONT_CENTER | \
+                                        WAV_SPEAKER_LOW_FREQUENCY | \
+                                        WAV_SPEAKER_BACK_LEFT | \
+                                        WAV_SPEAKER_BACK_RIGHT)
+#define WAV_CHANNEL_MASK_7_1           (WAV_SPEAKER_FRONT_LEFT | \
+                                        WAV_SPEAKER_FRONT_RIGHT | \
+                                        WAV_SPEAKER_FRONT_CENTER | \
+                                        WAV_SPEAKER_LOW_FREQUENCY | \
+                                        WAV_SPEAKER_BACK_LEFT | \
+                                        WAV_SPEAKER_BACK_RIGHT | \
+                                        WAV_SPEAKER_SIDE_LEFT | \
+                                        WAV_SPEAKER_SIDE_RIGHT)
+
 void init_wave_header(struct wave_header *header, uint16_t channels,
                      uint32_t rate, uint16_t samplebits);
 void size_wave_header(struct wave_header *header, uint32_t size);
 
 int parse_wave_header(struct wave_header *header, unsigned int *channels,
                      unsigned int *rate, unsigned int *format);
+
+/**
+ * parse_wave_file() - Parse WAV file with proper chunk scanning
+ * @file:         FILE pointer positioned at beginning of file
+ * @channels:     output - number of channels
+ * @rate:         output - sample rate in Hz
+ * @format:       output - ALSA PCM format (SNDRV_PCM_FORMAT_*)
+ * @channel_mask: output - WAV channel mask (speaker position bitmask),
+ *                0 if not available (basic PCM format infers default mapping)
+ *
+ * Handles both basic PCM (type 0x0001) and WAVE_FORMAT_EXTENSIBLE (type 0xFFFE)
+ * formats. Properly scans chunks to find the "data" chunk, leaving the file
+ * pointer positioned at the start of the audio data.
+ *
+ * Returns 0 on success, -1 on error.
+ */
+int parse_wave_file(FILE *file, unsigned int *channels, unsigned int *rate,
+                   unsigned int *format, unsigned int *channel_mask);
 #endif
index 14f163d108c22b67e7d491366282d97b259c2ce7..b0d14a36056a90e08b46833700e021b8254a2861 100644 (file)
@@ -533,21 +533,14 @@ int main(int argc, char **argv)
 void get_codec_pcm(FILE *file, struct compr_config *config,
                   struct snd_codec *codec)
 {
-       size_t read;
-       struct wave_header header;
-       unsigned int channels, rate, format;
+       unsigned int channels, rate, format, channel_mask;
 
-       read = fread(&header, 1, sizeof(header), file);
-       if (read != sizeof(header)) {
-               fprintf(stderr, "Unable to read header \n");
+       if (parse_wave_file(file, &channels, &rate, &format, &channel_mask) == -1) {
                fclose(file);
                exit(EXIT_FAILURE);
        }
 
-       if (parse_wave_header(&header, &channels, &rate, &format) == -1) {
-               fclose(file);
-               exit(EXIT_FAILURE);
-       }
+       /* File pointer is now at start of audio data */
 
        codec->id = SND_AUDIOCODEC_PCM;
        codec->ch_in = channels;
@@ -562,7 +555,7 @@ void get_codec_pcm(FILE *file, struct compr_config *config,
        codec->rate_control = 0;
        codec->profile = SND_AUDIOCODEC_PCM;
        codec->level = 0;
-       codec->ch_mode = 0;
+       codec->ch_mode = channel_mask;
        codec->format = format;
 }
 #endif
index a74149aec08254c5861842beb5b8f4bd44818341..9b9bfab7560ad97a573f829663d09fa8fd958e49 100644 (file)
@@ -92,3 +92,265 @@ int parse_wave_header(struct wave_header *header, unsigned int *channels,
 
        return 0;
 }
+
+/* WAVE_FORMAT_EXTENSIBLE sub-format GUIDs (first 2 bytes are the format tag) */
+static const uint8_t pcm_subformat_guid[16] = {
+       0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00,
+       0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71
+};
+
+/**
+ * get_default_channel_mask() - Get default WAV channel mask for a given
+ *                              channel count (per Microsoft WAV specification)
+ */
+static unsigned int get_default_channel_mask(unsigned int channels)
+{
+       switch (channels) {
+       case 1:
+               return WAV_CHANNEL_MASK_MONO;
+       case 2:
+               return WAV_CHANNEL_MASK_STEREO;
+       case 3:
+               return WAV_SPEAKER_FRONT_LEFT | WAV_SPEAKER_FRONT_RIGHT |
+                      WAV_SPEAKER_FRONT_CENTER;
+       case 4:
+               return WAV_SPEAKER_FRONT_LEFT | WAV_SPEAKER_FRONT_RIGHT |
+                      WAV_SPEAKER_BACK_LEFT | WAV_SPEAKER_BACK_RIGHT;
+       case 5:
+               return WAV_SPEAKER_FRONT_LEFT | WAV_SPEAKER_FRONT_RIGHT |
+                      WAV_SPEAKER_FRONT_CENTER |
+                      WAV_SPEAKER_BACK_LEFT | WAV_SPEAKER_BACK_RIGHT;
+       case 6:
+               return WAV_CHANNEL_MASK_5_1;
+       case 7:
+               return WAV_SPEAKER_FRONT_LEFT | WAV_SPEAKER_FRONT_RIGHT |
+                      WAV_SPEAKER_FRONT_CENTER | WAV_SPEAKER_LOW_FREQUENCY |
+                      WAV_SPEAKER_BACK_LEFT | WAV_SPEAKER_BACK_RIGHT |
+                      WAV_SPEAKER_BACK_CENTER;
+       case 8:
+               return WAV_CHANNEL_MASK_7_1;
+       default:
+               return 0;
+       }
+}
+
+static const char *speaker_name(unsigned int mask)
+{
+       switch (mask) {
+       case WAV_SPEAKER_FRONT_LEFT:            return "FL";
+       case WAV_SPEAKER_FRONT_RIGHT:           return "FR";
+       case WAV_SPEAKER_FRONT_CENTER:          return "FC";
+       case WAV_SPEAKER_LOW_FREQUENCY:         return "LFE";
+       case WAV_SPEAKER_BACK_LEFT:             return "BL";
+       case WAV_SPEAKER_BACK_RIGHT:            return "BR";
+       case WAV_SPEAKER_FRONT_LEFT_OF_CENTER:  return "FLC";
+       case WAV_SPEAKER_FRONT_RIGHT_OF_CENTER: return "FRC";
+       case WAV_SPEAKER_BACK_CENTER:           return "BC";
+       case WAV_SPEAKER_SIDE_LEFT:             return "SL";
+       case WAV_SPEAKER_SIDE_RIGHT:            return "SR";
+       default:                                return "?";
+       }
+}
+
+static void print_channel_map(unsigned int channels, unsigned int channel_mask)
+{
+       unsigned int i, ch = 0;
+
+       fprintf(stderr, "Channel map (%u ch, mask 0x%04x): ", channels,
+               channel_mask);
+
+       for (i = 0; i < 18 && ch < channels; i++) {
+               unsigned int bit = 1u << i;
+
+               if (channel_mask & bit) {
+                       if (ch > 0)
+                               fprintf(stderr, ", ");
+                       fprintf(stderr, "ch%u=%s", ch, speaker_name(bit));
+                       ch++;
+               }
+       }
+       fprintf(stderr, "\n");
+}
+
+int parse_wave_file(FILE *file, unsigned int *channels, unsigned int *rate,
+                   unsigned int *format, unsigned int *channel_mask)
+{
+       struct riff_chunk chunk;
+       char riff_format[4];
+       uint16_t fmt_type, fmt_channels, fmt_blockalign, fmt_samplebits;
+       uint32_t fmt_rate, fmt_byterate;
+       uint16_t fmt_cb_size;
+       uint16_t fmt_valid_bits;
+       uint32_t fmt_channel_mask;
+       uint8_t fmt_subformat[16];
+       uint32_t fmt_chunk_size;
+       long fmt_end;
+       int found_fmt = 0, found_data = 0;
+       size_t nread;
+
+       /* Seek to beginning */
+       if (fseek(file, 0, SEEK_SET) < 0) {
+               fprintf(stderr, "Failed to seek to start of file\n");
+               return -1;
+       }
+
+       /* Read RIFF header */
+       nread = fread(&chunk, 1, sizeof(chunk), file);
+       if (nread != sizeof(chunk)) {
+               fprintf(stderr, "Failed to read RIFF header\n");
+               return -1;
+       }
+
+       if (strncmp(chunk.desc, "RIFF", 4) != 0) {
+               fprintf(stderr, "RIFF magic not found\n");
+               return -1;
+       }
+
+       nread = fread(riff_format, 1, sizeof(riff_format), file);
+       if (nread != sizeof(riff_format)) {
+               fprintf(stderr, "Failed to read RIFF format\n");
+               return -1;
+       }
+
+       if (strncmp(riff_format, "WAVE", 4) != 0) {
+               fprintf(stderr, "WAVE magic not found\n");
+               return -1;
+       }
+
+       /* Scan chunks until we find both "fmt " and "data" */
+       while (!found_data) {
+               nread = fread(&chunk, 1, sizeof(chunk), file);
+               if (nread != sizeof(chunk)) {
+                       fprintf(stderr, "Unexpected end of file while scanning chunks\n");
+                       return -1;
+               }
+
+               if (strncmp(chunk.desc, "fmt ", 4) == 0) {
+                       fmt_chunk_size = chunk.size;
+                       fmt_end = ftell(file) + fmt_chunk_size;
+
+                       /* Read basic fmt fields (16 bytes) */
+                       if (fmt_chunk_size < 16) {
+                               fprintf(stderr, "fmt chunk too small (%u)\n",
+                                       fmt_chunk_size);
+                               return -1;
+                       }
+
+                       nread = fread(&fmt_type, 1, sizeof(fmt_type), file);
+                       nread += fread(&fmt_channels, 1, sizeof(fmt_channels), file);
+                       nread += fread(&fmt_rate, 1, sizeof(fmt_rate), file);
+                       nread += fread(&fmt_byterate, 1, sizeof(fmt_byterate), file);
+                       nread += fread(&fmt_blockalign, 1, sizeof(fmt_blockalign), file);
+                       nread += fread(&fmt_samplebits, 1, sizeof(fmt_samplebits), file);
+                       if (nread != 16) {
+                               fprintf(stderr, "Failed to read fmt fields\n");
+                               return -1;
+                       }
+
+                       fmt_channel_mask = 0;
+
+                       if (fmt_type == WAVE_FORMAT_EXTENSIBLE) {
+                               /* Need at least 2 (cbSize) + 2 (validBits) +
+                                * 4 (channelMask) + 16 (subformat) = 24 extra bytes
+                                */
+                               if (fmt_chunk_size < 40) {
+                                       fprintf(stderr,
+                                               "WAVE_FORMAT_EXTENSIBLE fmt chunk too small (%u, need 40)\n",
+                                               fmt_chunk_size);
+                                       return -1;
+                               }
+
+                               nread = fread(&fmt_cb_size, 1, sizeof(fmt_cb_size), file);
+                               nread += fread(&fmt_valid_bits, 1, sizeof(fmt_valid_bits), file);
+                               nread += fread(&fmt_channel_mask, 1, sizeof(fmt_channel_mask), file);
+                               nread += fread(fmt_subformat, 1, sizeof(fmt_subformat), file);
+                               if (nread != 24) {
+                                       fprintf(stderr,
+                                               "Failed to read extensible fmt fields\n");
+                                       return -1;
+                               }
+
+                               /* Verify subformat GUID is PCM */
+                               if (memcmp(fmt_subformat, pcm_subformat_guid,
+                                          sizeof(pcm_subformat_guid)) != 0) {
+                                       fprintf(stderr,
+                                               "Unsupported subformat (not PCM)\n");
+                                       return -1;
+                               }
+
+                               /* Use valid bits for format selection */
+                               fmt_samplebits = fmt_valid_bits;
+
+                               fprintf(stderr,
+                                       "WAVE_FORMAT_EXTENSIBLE: %u ch, %u Hz, %u bits, channel mask 0x%04x\n",
+                                       fmt_channels, fmt_rate, fmt_samplebits,
+                                       fmt_channel_mask);
+                       } else if (fmt_type == WAVE_FORMAT_PCM) {
+                               /* Basic PCM - use default channel mask */
+                               fmt_channel_mask = get_default_channel_mask(fmt_channels);
+
+                               if (fmt_channels > 2)
+                                       fprintf(stderr,
+                                               "Basic PCM with %u channels, using default channel mask 0x%04x\n",
+                                               fmt_channels, fmt_channel_mask);
+                       } else {
+                               fprintf(stderr,
+                                       "Unsupported WAVE format type: 0x%04x\n",
+                                       fmt_type);
+                               return -1;
+                       }
+
+                       /* Seek to end of fmt chunk (skip any remaining bytes) */
+                       if (fseek(file, fmt_end, SEEK_SET) < 0) {
+                               fprintf(stderr, "Failed to seek past fmt chunk\n");
+                               return -1;
+                       }
+
+                       found_fmt = 1;
+
+               } else if (strncmp(chunk.desc, "data", 4) == 0) {
+                       if (!found_fmt) {
+                               fprintf(stderr,
+                                       "data chunk found before fmt chunk\n");
+                               return -1;
+                       }
+                       /* File pointer is now at start of audio data */
+                       found_data = 1;
+
+               } else {
+                       /* Unknown chunk - skip it */
+                       if (fseek(file, chunk.size, SEEK_CUR) < 0) {
+                               fprintf(stderr,
+                                       "Failed to skip chunk '%.4s' (%u bytes)\n",
+                                       chunk.desc, chunk.size);
+                               return -1;
+                       }
+               }
+       }
+
+       *channels = fmt_channels;
+       *rate = fmt_rate;
+       *channel_mask = fmt_channel_mask;
+
+       switch (fmt_samplebits) {
+       case 8:
+               *format = SNDRV_PCM_FORMAT_U8;
+               break;
+       case 16:
+               *format = SNDRV_PCM_FORMAT_S16_LE;
+               break;
+       case 24:
+               *format = SNDRV_PCM_FORMAT_S24_LE;
+               break;
+       case 32:
+               *format = SNDRV_PCM_FORMAT_S32_LE;
+               break;
+       default:
+               fprintf(stderr, "Unsupported sample bits %d\n", fmt_samplebits);
+               return -1;
+       }
+
+       print_channel_map(fmt_channels, fmt_channel_mask);
+
+       return 0;
+}