diff --git a/include/tinycompress/tinywave.h b/include/tinycompress/tinywave.h index 619cb8d..7a4c08b 100644 --- a/include/tinycompress/tinywave.h +++ b/include/tinycompress/tinywave.h @@ -8,6 +8,8 @@ #ifndef __TINYWAVE_H #define __TINYWAVE_H +#include + 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 diff --git a/src/utils/cplay.c b/src/utils/cplay.c index 14f163d..b0d14a3 100644 --- a/src/utils/cplay.c +++ b/src/utils/cplay.c @@ -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 diff --git a/src/utils/wave.c b/src/utils/wave.c index a74149a..9b9bfab 100644 --- a/src/utils/wave.c +++ b/src/utils/wave.c @@ -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; +}