diff --git a/examples/mixertest/Makefile b/examples/mixertest/Makefile index 22c5346040..b22a17f7a7 100644 --- a/examples/mixertest/Makefile +++ b/examples/mixertest/Makefile @@ -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) diff --git a/include/wav64.h b/include/wav64.h index 415e0845e9..12caf1aa68 100644 --- a/include/wav64.h +++ b/include/wav64.h @@ -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. @@ -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 diff --git a/include/wav64internal.h b/include/wav64internal.h index 60e6d43a01..2d31853630 100644 --- a/include/wav64internal.h +++ b/include/wav64internal.h @@ -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)) { @@ -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; /** diff --git a/src/audio/wav64.c b/src/audio/wav64.c index 3b3d52f450..f5e1400c53 100644 --- a/src/audio/wav64.c +++ b/src/audio/wav64.c @@ -15,7 +15,10 @@ #include #include #include +#include #include +#include +#include /** ID of a standard WAV file */ #define WAV_RIFF_ID "RIFF" @@ -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); @@ -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)); @@ -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, @@ -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; @@ -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); } @@ -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; + } +} diff --git a/tools/audioconv64/Makefile b/tools/audioconv64/Makefile index be058b4b31..abc4dd838b 100644 --- a/tools/audioconv64/Makefile +++ b/tools/audioconv64/Makefile @@ -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 diff --git a/tools/audioconv64/audioconv64.c b/tools/audioconv64/audioconv64.c index 62b102f9eb..5d5f15fd44 100644 --- a/tools/audioconv64/audioconv64.c +++ b/tools/audioconv64/audioconv64.c @@ -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 Activate playback loop by default\n"); printf(" --wav-loop-offset Set looping offset (in samples; default: 0)\n"); printf("\n"); @@ -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"); diff --git a/tools/audioconv64/conv_wav64.c b/tools/audioconv64/conv_wav64.c index 8b076e68d9..0973ad8b8f 100644 --- a/tools/audioconv64/conv_wav64.c +++ b/tools/audioconv64/conv_wav64.c @@ -3,8 +3,15 @@ #define DR_WAV_IMPLEMENTATION #include "dr_wav.h" +#include "vadpcm/vadpcm.h" +#include "vadpcm/encode.c" +#include "vadpcm/error.c" + +#include "../common/binout.h" + bool flag_wav_looping = false; int flag_wav_looping_offset = 0; +int flag_wav_compress = 0; int wav_convert(const char *infn, const char *outfn) { drwav wav; @@ -13,17 +20,29 @@ int wav_convert(const char *infn, const char *outfn) { return 1; } + if (flag_verbose) + fprintf(stderr, "Converting: %s => %s (%d bits, %d Hz, %d channels, %s)\n", infn, outfn, + wav.bitsPerSample, wav.sampleRate, wav.channels, flag_wav_compress ? "vadpcm" : "raw"); + + if (flag_wav_compress == 1 && wav.channels != 1) { + fprintf(stderr, "ERROR: VADPCM compression only support mono files\n"); + return 1; + } + // Decode the samples as 16bit big-endian. This will decode everything including // compressed formats so that we're able to read any kind of WAV file, though // it will end up as an uncompressed file. int16_t* samples = malloc(wav.totalPCMFrameCount * wav.channels * sizeof(int16_t)); - size_t cnt = drwav_read_pcm_frames_s16be(&wav, wav.totalPCMFrameCount, samples); + size_t cnt = drwav_read_pcm_frames_s16le(&wav, wav.totalPCMFrameCount, samples); if (cnt != wav.totalPCMFrameCount) { fprintf(stderr, "WARNING: %s: %llu frames found, but only %zu decoded\n", infn, wav.totalPCMFrameCount, cnt); } // Keep 8 bits file if original is 8 bit, otherwise expand to 16 bit. + // Compressed waveforms always expand to 16 int nbits = wav.bitsPerSample == 8 ? 8 : 16; + if (flag_wav_compress == 1) + nbits = 16; int loop_len = flag_wav_looping ? cnt - flag_wav_looping_offset : 0; if (loop_len < 0) { @@ -38,22 +57,6 @@ int wav_convert(const char *infn, const char *outfn) { loop_len -= 1; } - wav64_header_t head; - memset(&head, 0, sizeof(wav64_header_t)); - - memcpy(head.id, "WV64", 4); - head.version = WAV64_FILE_VERSION; - head.format = 0; - head.channels = wav.channels; - head.nbits = nbits; - head.freq = HOST_TO_BE32(wav.sampleRate); - head.len = HOST_TO_BE32(cnt); - head.loop_len = HOST_TO_BE32(loop_len); - head.start_offset = HOST_TO_BE32(sizeof(wav64_header_t)); - - if (flag_verbose) - fprintf(stderr, "Converting: %s => %s\n", infn, outfn); - FILE *out = fopen(outfn, "wb"); if (!out) { fprintf(stderr, "ERROR: %s: cannot create file\n", outfn); @@ -62,35 +65,90 @@ int wav_convert(const char *infn, const char *outfn) { return 1; } - fwrite(&head, 1, sizeof(wav64_header_t), out); + char id[4] = "WV64"; + fwrite(id, 1, 4, out); + w8(out, WAV64_FILE_VERSION); + w8(out, flag_wav_compress); + w8(out, wav.channels); + w8(out, nbits); + w32(out, wav.sampleRate); + w32(out, cnt); + w32(out, loop_len); + int wstart_offset = w32_placeholder(out); // start_offset (to be filled later) - int16_t *sptr = samples; - for (int i=0;i> 8) | ((v & 0x00FF) << 8); + *sptr = v; + // Write the sample as 16bit or 8bit. Since *sptr is 16-bit big-endian, + // the 8bit representation is just the first byte (MSB). Notice + // that WAV64 8bit is signed anyway. + fwrite(sptr, 1, nbits == 8 ? 1 : 2, out); + sptr++; + } - // Amount of data that can be overread by the player. - const int OVERREAD_BYTES = 64; - if (loop_len == 0) { - for (int i=0;i