Skip to content

Commit

Permalink
wav64: implement VADPCM support (on CPU for now)
Browse files Browse the repository at this point in the history
  • Loading branch information
rasky committed Aug 4, 2023
1 parent b16dfe0 commit 0174271
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 51 deletions.
2 changes: 1 addition & 1 deletion examples/mixertest/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ all: mixertest.z64
filesystem/%.wav64: assets/%.wav
@mkdir -p $(dir $@)
@echo " [AUDIO] $@"
@$(N64_AUDIOCONV) -o filesystem $<
@$(N64_AUDIOCONV) --wav-compress 1 -v -o filesystem $<

$(BUILD_DIR)/mixertest.dfs: $(assets_conv)
$(BUILD_DIR)/mixertest.elf: $(OBJS)
Expand Down
10 changes: 10 additions & 0 deletions include/wav64.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ typedef struct {

/** @brief Absolute ROM address of WAV64 */
uint32_t rom_addr;

void *ext; ///< Pointer to extended data (internal use)
} wav64_t;

/** @brief Open a WAV64 file for playback.
Expand Down Expand Up @@ -61,6 +63,14 @@ void wav64_set_loop(wav64_t *wav, bool loop);
*/
void wav64_play(wav64_t *wav, int ch);


/**
* @brief Close a WAV64 file.
*
* @param wav Pointer to wav64_t structure
*/
void wav64_close(wav64_t *wav);

#ifdef __cplusplus
}
#endif
Expand Down
17 changes: 17 additions & 0 deletions include/wav64internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#define WAV64_ID "WV64"
#define WAV64_FILE_VERSION 2
#define WAV64_FORMAT_RAW 0
#define WAV64_FORMAT_VADPCM 1

/** @brief Header of a WAV64 file. */
typedef struct __attribute__((packed)) {
Expand All @@ -20,6 +21,22 @@ typedef struct __attribute__((packed)) {

_Static_assert(sizeof(wav64_header_t) == 24, "invalid wav64_header size");

/** @brief A vector of audio samples */
typedef struct __attribute__((aligned(8))) {
int16_t v[8]; ///< Samples
} wav64_vadpcm_vector_t;

/** @brief Extended header for a WAV64 file with VADPCM compression. */
typedef struct __attribute__((packed, aligned(8))) {
int8_t npredictors; ///< Number of predictors
int8_t order; ///< Order of the predictors
int16_t padding; ///< Padding
uint32_t current_rom_addr; ///< Current address in ROM
wav64_vadpcm_vector_t loop_state; ///< State at the loop point
wav64_vadpcm_vector_t state; ///< Current decompression state
wav64_vadpcm_vector_t codebook[]; ///< Codebook of the predictors
} wav64_header_vadpcm_t;

typedef struct samplebuffer_s samplebuffer_t;

/**
Expand Down
175 changes: 169 additions & 6 deletions src/audio/wav64.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
#include <stdbool.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>
#include <stdalign.h>

/** ID of a standard WAV file */
#define WAV_RIFF_ID "RIFF"
Expand All @@ -25,6 +28,103 @@
/** @brief Profile of DMA usage by WAV64, used for debugging purposes. */
int64_t __wav64_profile_dma = 0;

/** @brief VADPCM decoding errors */
typedef enum {
// No error (success). Equal to 0.
kVADPCMErrNone,

// Invalid data.
kVADPCMErrInvalidData,

// Predictor order is too large.
kVADPCMErrLargeOrder,

// Predictor count is too large.
kVADPCMErrLargePredictorCount,

// Data uses an unsupported / unknown version of VADPCM.
kVADPCMErrUnknownVersion,

// Invalid encoding parameters.
kVADPCMErrInvalidParams,
} vadpcm_error;

// Extend the sign bit of a 4-bit integer.
static int vadpcm_ext4(int x) {
return x > 7 ? x - 16 : x;
}

// Clamp an integer to a 16-bit range.
static int vadpcm_clamp16(int x) {
if (x < -0x8000 || 0x7fff < x) {
return (x >> (sizeof(int) * CHAR_BIT - 1)) ^ 0x7fff;
}
return x;
}

static vadpcm_error vadpcm_decode(int predictor_count, int order,
const wav64_vadpcm_vector_t *restrict codebook,
wav64_vadpcm_vector_t *restrict state,
size_t frame_count, int16_t *restrict dest,
const void *restrict src) {
const uint8_t *sptr = src;
for (size_t frame = 0; frame < frame_count; frame++) {
const uint8_t *fin = sptr + 9 * frame;

// Control byte: scaling & predictor index.
int control = fin[0];
int scaling = control >> 4;
int predictor_index = control & 15;
if (predictor_index >= predictor_count) {
return kVADPCMErrInvalidData;
}
const wav64_vadpcm_vector_t *predictor =
codebook + order * predictor_index;

// Decode each of the two vectors within the frame.
for (int vector = 0; vector < 2; vector++) {
int32_t accumulator[8];
for (int i = 0; i < 8; i++) {
accumulator[i] = 0;
}

// Accumulate the part of the predictor from the previous block.
for (int k = 0; k < order; k++) {
int sample = state->v[8 - order + k];
for (int i = 0; i < 8; i++) {
accumulator[i] += sample * predictor[k].v[i];
}
}

// Decode the ADPCM residual.
int residuals[8];
for (int i = 0; i < 4; i++) {
int byte = fin[1 + 4 * vector + i];
residuals[2 * i] = vadpcm_ext4(byte >> 4);
residuals[2 * i + 1] = vadpcm_ext4(byte & 15);
}

// Accumulate the residual and predicted values.
const wav64_vadpcm_vector_t *v = &predictor[order - 1];
for (int k = 0; k < 8; k++) {
int residual = residuals[k] << scaling;
accumulator[k] += residual << 11;
for (int i = 0; i < 7 - k; i++) {
accumulator[k + 1 + i] += residual * v->v[i];
}
}

// Discard fractional part and clamp to 16-bit range.
for (int i = 0; i < 8; i++) {
int sample = vadpcm_clamp16(accumulator[i] >> 11);
dest[16 * frame + 8 * vector + i] = sample;
state->v[i] = sample;
}
}
}
return 0;
}

void raw_waveform_read(samplebuffer_t *sbuf, int base_rom_addr, int wpos, int wlen, int bps) {
uint32_t rom_addr = base_rom_addr + (wpos << bps);
uint8_t* ram_addr = (uint8_t*)samplebuffer_append(sbuf, wlen);
Expand All @@ -45,6 +145,37 @@ static void waveform_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, b
raw_waveform_read(sbuf, wav->rom_addr, wpos, wlen, bps);
}

static void waveform_vadpcm_read(void *ctx, samplebuffer_t *sbuf, int wpos, int wlen, bool seeking) {
wav64_t *wav = (wav64_t*)ctx;
wav64_header_vadpcm_t *vhead = (wav64_header_vadpcm_t*)wav->ext;

debugf("vadpcm_read: wpos=%d wlen=%d seeking=%d\n", wpos, wlen, seeking);

if (seeking) {
if (wpos == 0) {
memset(&vhead->state, 0, sizeof(vhead->state));
vhead->current_rom_addr = wav->rom_addr;
} else {
memcpy(&vhead->state, &vhead->loop_state, sizeof(vhead->state));
vhead->current_rom_addr = wav->rom_addr + wav->wave.loop_len / 16 * 9;
}
}

while (wlen > 0) {
uint8_t buf[18] alignas(8);

data_cache_hit_writeback_invalidate(buf, sizeof(buf));
dma_read(buf, vhead->current_rom_addr, sizeof(buf));

int16_t *dest = (int16_t*)samplebuffer_append(sbuf, 32);
vadpcm_error err = vadpcm_decode(vhead->npredictors, vhead->order, vhead->codebook, &vhead->state, 2, dest, buf);
assertf(err == 0, "VADPCM decoding error: %d\n", err);

vhead->current_rom_addr += sizeof(buf);
wlen -= 32;
}
}

void wav64_open(wav64_t *wav, const char *fn) {
memset(wav, 0, sizeof(*wav));

Expand All @@ -59,7 +190,7 @@ void wav64_open(wav64_t *wav, const char *fn) {
int fh = dfs_open(fn);
assertf(fh >= 0, "error opening file %s: %s\n", fn, strerror(errno));

wav64_header_t head;
wav64_header_t head = {0};
dfs_read(&head, 1, sizeof(head), fh);
if (memcmp(head.id, WAV64_ID, 4) != 0) {
assertf(memcmp(head.id, WAV_RIFF_ID, 4) != 0 && memcmp(head.id, WAV_RIFX_ID, 4) != 0,
Expand All @@ -69,8 +200,30 @@ void wav64_open(wav64_t *wav, const char *fn) {
}
assertf(head.version == WAV64_FILE_VERSION, "wav64 %s: invalid version: %02x\n",
fn, head.version);
assertf(head.format == WAV64_FORMAT_RAW, "wav64 %s: invalid format: %02x\n",
fn, head.format);

switch (head.format) {
case WAV64_FORMAT_RAW:
wav->wave.read = waveform_read;
wav->wave.ctx = wav;
break;

case WAV64_FORMAT_VADPCM: {
wav64_header_vadpcm_t vhead = {0};
dfs_read(&vhead, 1, sizeof(vhead), fh);

void *ext = malloc_uncached(sizeof(vhead) + vhead.npredictors * vhead.order * sizeof(wav64_vadpcm_vector_t));
memcpy(ext, &vhead, sizeof(vhead));
dfs_read(ext + sizeof(vhead), 1, vhead.npredictors * vhead.order * sizeof(wav64_vadpcm_vector_t), fh);
wav->ext = ext;
wav->wave.read = waveform_vadpcm_read;
wav->wave.ctx = wav;
assertf(head.loop_len == 0 || head.loop_len % 16 == 0,
"wav64 %s: invalid loop length: %ld\n", fn, head.loop_len);
} break;

default:
assertf(0, "wav64 %s: invalid format: %02x\n", fn, head.format);
}

wav->wave.name = fn;
wav->wave.channels = head.channels;
Expand All @@ -80,13 +233,15 @@ void wav64_open(wav64_t *wav, const char *fn) {
wav->wave.loop_len = head.loop_len;
wav->rom_addr = dfs_rom_addr(fn) + head.start_offset;
dfs_close(fh);

wav->wave.read = waveform_read;
wav->wave.ctx = wav;
debugf("wav64 %s: %d-bit %.1fHz %dch %d samples (loop: %d)\n",
fn, wav->wave.bits, wav->wave.frequency, wav->wave.channels, wav->wave.len, wav->wave.loop_len);
}

void wav64_play(wav64_t *wav, int ch)
{
// Update the context pointer, so that we try to catch cases where the
// wav64_t instance was moved.
wav->wave.ctx = wav;
mixer_ch_play(ch, &wav->wave);
}

Expand All @@ -100,3 +255,11 @@ void wav64_set_loop(wav64_t *wav, bool loop) {
if (wav->wave.bits == 8 && wav->wave.loop_len & 1)
wav->wave.loop_len -= 1;
}

void wav64_close(wav64_t *wav)
{
if (wav->ext) {
free_uncached(wav->ext);
wav->ext = NULL;
}
}
5 changes: 3 additions & 2 deletions tools/audioconv64/Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
INSTALLDIR = $(N64_INST)
CFLAGS = -std=gnu11 -MMD -O2 -Wall -Wno-unused-result -Werror -I../../include
LDFLAGS += -lm
SRC = audioconv64.c

all: audioconv64

audioconv64: audioconv64.c
audioconv64: $(SRC)
@echo " [TOOL] audioconv64"
$(CC) $(CFLAGS) $< $(LDFLAGS) -o $@
$(CC) $(CFLAGS) $(SRC) $(LDFLAGS) -o $@

install: audioconv64
install -m 0755 audioconv64 $(INSTALLDIR)/bin
Expand Down
11 changes: 11 additions & 0 deletions tools/audioconv64/audioconv64.c
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ void usage(void) {
printf(" -v / --verbose Verbose mode\n");
printf("\n");
printf("WAV options:\n");
printf(" --wav-compress <0|1> Enable compression (0:none, 1:vadpcm)\n");
printf(" --wav-loop <true|false> Activate playback loop by default\n");
printf(" --wav-loop-offset <N> Set looping offset (in samples; default: 0)\n");
printf("\n");
Expand Down Expand Up @@ -218,6 +219,16 @@ int main(int argc, char *argv[]) {
return 1;
}
flag_wav_looping = true;
} else if (!strcmp(argv[i], "--wav-compress")) {
if (++i == argc) {
fprintf(stderr, "missing argument for --wav-compress\n");
return 1;
}
flag_wav_compress = atoi(argv[i]);
if (flag_wav_compress < 0 || flag_wav_compress > 1) {
fprintf(stderr, "invalid argument for --wav-compress: %s\n", argv[i]);
return 1;
}
} else if (!strcmp(argv[i], "--ym-compress")) {
if (++i == argc) {
fprintf(stderr, "missing argument for --ym-compress\n");
Expand Down
Loading

0 comments on commit 0174271

Please sign in to comment.