joeylib2/tests/gate/common/gateWav.c

344 lines
11 KiB
C

// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License.
// See LICENSE.md for license information.
// SPDX-License-Identifier: MIT
//
// gateWav.c - WAV files for the gate tools (gateWav.h).
//
// The reading walks a WAV as Python's wave module did, since the graders
// were written against it and a recording that is not quite whole must
// yield the same audio: the RIFF chunk's body is walked chunk by chunk,
// every read capped at the RIFF's declared size and at the file's end, a
// chunk of odd size followed by a pad byte; the fmt chunk before the first
// data chunk gives the format, and the audio is that data chunk's whole
// frames, as far as the caps let them go.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "gateText.h"
#include "gateWav.h"
#include "toolFile.h"
#define RIFF_HEADER 8u
#define CHUNK_ID_BYTES 4u
#define CHUNK_SIZE_BYTES 4u
#define FMT_BYTES 14u
#define FMT_BITS_BYTES 2u
#define FMT_EXT_BYTES 8u
#define FMT_GUID_BYTES 16u
#define FMT_CHANNELS_AT 2u
#define FMT_RATE_AT 4u
#define FORMAT_PCM 0x0001u
#define FORMAT_EXTENSIBLE 0xFFFEu
#define BITS_PER_BYTE 8u
#define SAMPLE_BYTES 2u
#define WAV_FMT_SIZE 16u
#define WAV_RIFF_EXTRA 36u
// The RIFF chunk being read: its declared size, and how much of it has
// been read (its body starts RIFF_HEADER bytes into the file).
typedef struct RiffT {
const uint8_t *file;
size_t fileLen;
uint64_t size;
uint64_t pos;
} RiffT;
// A chunk inside it: its declared size, where its body starts in the RIFF,
// and how much of it has been read.
typedef struct ChunkT {
char id[CHUNK_ID_BYTES];
uint64_t size;
uint64_t offset;
uint64_t pos;
} ChunkT;
static size_t chunkRead(RiffT *riff, ChunkT *chunk, uint64_t n, const uint8_t **at);
static bool chunkSkip(RiffT *riff, ChunkT *chunk);
static bool fmtRead(RiffT *riff, ChunkT *chunk, const char *path, GateWavT *wav, ToolBufT *problem);
static uint32_t le16(const uint8_t *at);
static uint32_t le32(const uint8_t *at);
static size_t riffRead(RiffT *riff, uint64_t n, const uint8_t **at);
// KSDATAFORMAT_SUBTYPE_PCM, the GUID of an extensible WAV's PCM samples.
static const uint8_t kPcmGuid[FMT_GUID_BYTES] = {
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71
};
// Up to n bytes of a chunk's body (then its pad byte, when its size is odd
// and its end is reached): how many, at *at.
static size_t chunkRead(RiffT *riff, ChunkT *chunk, uint64_t n, const uint8_t **at) {
const uint8_t *pad;
size_t got;
*at = NULL;
if (chunk->pos >= chunk->size) {
return 0u;
}
if (n > chunk->size - chunk->pos) {
n = chunk->size - chunk->pos;
}
got = riffRead(riff, n, at);
chunk->pos += got;
if (chunk->pos == chunk->size && (chunk->size & 1u) != 0u) {
chunk->pos += riffRead(riff, 1u, &pad);
}
return got;
}
// Past the rest of a chunk (and its pad byte); false when that would leave
// the RIFF chunk.
static bool chunkSkip(RiffT *riff, ChunkT *chunk) {
int64_t step;
int64_t to;
step = (int64_t)chunk->size - (int64_t)chunk->pos;
if ((chunk->size & 1u) != 0u) {
step++;
}
to = (int64_t)riff->pos + step;
if (to < 0 || (uint64_t)to > riff->size) {
return false;
}
riff->pos = (uint64_t)to;
chunk->pos += (uint64_t)step;
return true;
}
// The fmt chunk: 16-bit PCM (plainly, or as the extensible format's PCM),
// its channels and rate; false, with what is wrong in problem, when it is
// not one this reads.
static bool fmtRead(RiffT *riff, ChunkT *chunk, const char *path, GateWavT *wav, ToolBufT *problem) {
const uint8_t *at;
uint32_t format;
uint32_t bits;
if (chunkRead(riff, chunk, FMT_BYTES, &at) < FMT_BYTES) {
gateTextAdd(problem, "%s: fmt chunk cut short", path);
return false;
}
format = le16(at);
wav->channels = le16(at + FMT_CHANNELS_AT);
wav->rate = le32(at + FMT_RATE_AT);
if (format != FORMAT_PCM && format != FORMAT_EXTENSIBLE) {
gateTextAdd(problem, "%s: format %u, not PCM", path, (unsigned)format);
return false;
}
if (chunkRead(riff, chunk, FMT_BITS_BYTES, &at) < FMT_BITS_BYTES) {
gateTextAdd(problem, "%s: fmt chunk cut short", path);
return false;
}
bits = le16(at);
if (format == FORMAT_EXTENSIBLE) {
if (chunkRead(riff, chunk, FMT_EXT_BYTES, &at) < FMT_EXT_BYTES || chunkRead(riff, chunk, FMT_GUID_BYTES, &at) < FMT_GUID_BYTES) {
gateTextAdd(problem, "%s: fmt chunk cut short", path);
return false;
}
if (memcmp(at, kPcmGuid, FMT_GUID_BYTES) != 0) {
gateTextAdd(problem, "%s: extensible format, not PCM", path);
return false;
}
}
// A sample takes whole bytes: 9 to 16 bits are stored in 16.
if ((bits + BITS_PER_BYTE - 1u) / BITS_PER_BYTE != SAMPLE_BYTES) {
gateTextAdd(problem, "%s: %u-bit samples, not 16-bit", path, (unsigned)bits);
return false;
}
if (wav->channels == 0u) {
gateTextAdd(problem, "%s: no channels", path);
return false;
}
return true;
}
size_t gateWavFrames(const GateWavT *wav) {
return wav->byteCount / (SAMPLE_BYTES * wav->channels);
}
void gateWavFree(GateWavT *wav) {
free(wav->bytes);
memset(wav, 0, sizeof(*wav));
}
double *gateWavMono(const GateWavT *wav) {
double *mono;
size_t frames;
size_t frame;
uint32_t channel;
double total;
frames = gateWavFrames(wav);
mono = (double *)malloc((frames > 0u ? frames : 1u) * sizeof(double));
if (mono == NULL) {
toolFail("out of memory");
}
for (frame = 0u; frame < frames; frame++) {
total = 0.0;
for (channel = 0u; channel < wav->channels; channel++) {
total += gateWavSample(wav, frame, channel);
}
mono[frame] = total / (double)wav->channels;
}
return mono;
}
bool gateWavRead(const char *path, GateWavT *wav, ToolBufT *problem) {
uint8_t *file;
size_t fileLen;
const uint8_t *at;
const char *wrong;
RiffT riff;
ChunkT chunk;
ChunkT data;
bool haveFmt;
bool haveData;
uint64_t frames;
memset(wav, 0, sizeof(*wav));
memset(&data, 0, sizeof(data));
if (!gateTextReadFile(path, &file, &fileLen, problem)) {
return false;
}
riff.file = file;
riff.fileLen = fileLen;
riff.size = fileLen >= RIFF_HEADER ? le32(file + CHUNK_ID_BYTES) : 0u;
riff.pos = 0u;
wrong = NULL;
if (fileLen < RIFF_HEADER || memcmp(file, "RIFF", CHUNK_ID_BYTES) != 0) {
wrong = "not a RIFF file";
} else if (riffRead(&riff, CHUNK_ID_BYTES, &at) < CHUNK_ID_BYTES || memcmp(at, "WAVE", CHUNK_ID_BYTES) != 0) {
wrong = "not a WAVE file";
}
haveFmt = false;
haveData = false;
while (wrong == NULL && !haveData) {
if (riffRead(&riff, CHUNK_ID_BYTES, &at) < CHUNK_ID_BYTES) {
break;
}
memcpy(chunk.id, at, CHUNK_ID_BYTES);
if (riffRead(&riff, CHUNK_SIZE_BYTES, &at) < CHUNK_SIZE_BYTES) {
break;
}
chunk.size = le32(at);
chunk.offset = riff.pos;
chunk.pos = 0u;
if (memcmp(chunk.id, "fmt ", CHUNK_ID_BYTES) == 0) {
if (!fmtRead(&riff, &chunk, path, wav, problem)) {
free(file);
return false;
}
haveFmt = true;
} else if (memcmp(chunk.id, "data", CHUNK_ID_BYTES) == 0) {
data = chunk;
haveData = true;
continue;
}
if (!chunkSkip(&riff, &chunk)) {
wrong = "a chunk runs past the end of the RIFF";
}
}
if (wrong == NULL && !haveFmt) {
wrong = "no fmt chunk before its data";
} else if (wrong == NULL && !haveData) {
wrong = "no data chunk";
} else if (wrong == NULL && data.offset > riff.size) {
wrong = "a chunk runs past the end of the RIFF";
}
if (wrong != NULL) {
gateTextAdd(problem, "%s: %s", path, wrong);
free(file);
return false;
}
// The data's whole frames, as far as the RIFF and the file go.
riff.pos = data.offset;
data.pos = 0u;
frames = data.size / ((uint64_t)wav->channels * SAMPLE_BYTES);
wav->byteCount = chunkRead(&riff, &data, frames * wav->channels * SAMPLE_BYTES, &at);
wav->bytes = (uint8_t *)malloc(wav->byteCount > 0u ? wav->byteCount : 1u);
if (wav->bytes == NULL) {
toolFail("out of memory");
}
if (wav->byteCount > 0u) {
memcpy(wav->bytes, at, wav->byteCount);
}
free(file);
return true;
}
double gateWavSample(const GateWavT *wav, size_t frame, uint32_t channel) {
const uint8_t *at;
at = wav->bytes + (frame * wav->channels + channel) * SAMPLE_BYTES;
return (double)(int16_t)le16(at);
}
bool gateWavWrite(const char *path, uint32_t channels, uint32_t rate, const uint8_t *data, size_t len) {
ToolBufT out;
bool ok;
memset(&out, 0, sizeof(out));
toolBufAdd(&out, "RIFF", CHUNK_ID_BYTES);
toolBufLe32(&out, (uint32_t)(WAV_RIFF_EXTRA + len));
toolBufAdd(&out, "WAVEfmt ", 2u * CHUNK_ID_BYTES);
toolBufLe32(&out, WAV_FMT_SIZE);
toolBufLe16(&out, FORMAT_PCM);
toolBufLe16(&out, channels);
toolBufLe32(&out, rate);
toolBufLe32(&out, channels * rate * SAMPLE_BYTES);
toolBufLe16(&out, channels * SAMPLE_BYTES);
toolBufLe16(&out, SAMPLE_BYTES * BITS_PER_BYTE);
toolBufAdd(&out, "data", CHUNK_ID_BYTES);
toolBufLe32(&out, (uint32_t)len);
toolBufAdd(&out, data, len);
ok = toolWriteFile(path, out.data, out.len);
toolBufFree(&out);
return ok;
}
static uint32_t le16(const uint8_t *at) {
return (uint32_t)at[0] | ((uint32_t)at[1] << BITS_PER_BYTE);
}
static uint32_t le32(const uint8_t *at) {
return le16(at) | (le16(at + SAMPLE_BYTES) << (2u * BITS_PER_BYTE));
}
// Up to n bytes of the RIFF chunk's body, capped at its size and the
// file's end (then its pad byte, when its size is odd and its end is
// reached): how many, at *at.
static size_t riffRead(RiffT *riff, uint64_t n, const uint8_t **at) {
uint64_t offset;
uint64_t avail;
*at = NULL;
if (riff->pos >= riff->size) {
return 0u;
}
if (n > riff->size - riff->pos) {
n = riff->size - riff->pos;
}
offset = RIFF_HEADER + riff->pos;
avail = offset < riff->fileLen ? riff->fileLen - offset : 0u;
if (n > avail) {
n = avail;
}
*at = riff->file + offset;
riff->pos += n;
if (riff->pos == riff->size && (riff->size & 1u) != 0u && offset + n < riff->fileLen) {
riff->pos++;
}
return (size_t)n;
}