mirror of
https://github.com/zeldaret/oot.git
synced 2025-08-12 18:01:16 +00:00
* [Audio 2/?] Extract audio samples to wav Co-authored-by: zelda2774 <69368340+zelda2774@users.noreply.github.com> * How * Hopefully fix warning I don't get locally * Pad default sample filenames, comment on the vadpcm frame encoder functions, other suggested changes * Small tweaks to above * Remove some obsolete code --------- Co-authored-by: zelda2774 <69368340+zelda2774@users.noreply.github.com>
552 lines
17 KiB
C
552 lines
17 KiB
C
/**
|
|
* SPDX-FileCopyrightText: Copyright (C) 2024 ZeldaRET
|
|
* SPDX-License-Identifier: MPL-2.0
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
#include <assert.h>
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "../util.h"
|
|
#include "../codec/vadpcm.h"
|
|
#include "wav.h"
|
|
|
|
typedef enum {
|
|
WAVE_TYPE_PCM = 1,
|
|
WAVE_TYPE_FLOAT = 3,
|
|
WAVE_TYPE_ALAW = 6,
|
|
WAVE_TYPE_MULAW = 7,
|
|
WAVE_TYPE_EXTENSIBLE = 0xFFFE,
|
|
} wav_type;
|
|
|
|
typedef struct {
|
|
uint16_t type;
|
|
uint16_t num_channels;
|
|
uint32_t sample_rate;
|
|
uint32_t byte_rate;
|
|
uint16_t block_align;
|
|
uint16_t bit_depth;
|
|
} wav_fmt;
|
|
|
|
typedef struct {
|
|
uint32_t num_samples;
|
|
} wav_fact;
|
|
|
|
typedef struct {
|
|
int8_t base_note;
|
|
int8_t fine_tune;
|
|
int8_t gain;
|
|
int8_t key_low;
|
|
int8_t key_hi;
|
|
int8_t vel_low;
|
|
int8_t vel_hi;
|
|
char _pad[1];
|
|
} wav_inst;
|
|
|
|
typedef struct {
|
|
uint32_t manufacturer;
|
|
uint32_t product;
|
|
uint32_t sample_period;
|
|
uint32_t unity_note;
|
|
char _pad[3];
|
|
uint8_t fine_tune;
|
|
uint32_t format;
|
|
uint32_t offset;
|
|
uint32_t num_sample_loops;
|
|
uint32_t sampler_data;
|
|
} wav_smpl;
|
|
|
|
typedef struct {
|
|
uint32_t cue_point_index;
|
|
uint32_t type;
|
|
uint32_t start;
|
|
uint32_t end;
|
|
uint32_t fraction;
|
|
uint32_t num;
|
|
} wav_loop;
|
|
|
|
static const char *
|
|
wav_type_name(int type)
|
|
{
|
|
switch (type) {
|
|
case WAVE_TYPE_PCM:
|
|
return "PCM";
|
|
case WAVE_TYPE_FLOAT:
|
|
return "Float";
|
|
case WAVE_TYPE_ALAW:
|
|
return "ALAW";
|
|
case WAVE_TYPE_MULAW:
|
|
return "MULAW";
|
|
case WAVE_TYPE_EXTENSIBLE:
|
|
return "Extensible";
|
|
default:
|
|
return "Unknown (should never be here)";
|
|
}
|
|
}
|
|
|
|
int
|
|
wav_read(container_data *out, const char *path, UNUSED bool matching)
|
|
{
|
|
bool has_fmt = false;
|
|
bool has_fact = false;
|
|
bool has_data = false;
|
|
bool has_inst = false;
|
|
bool has_smpl = false;
|
|
memset(out, 0, sizeof(*out));
|
|
|
|
FILE *in = fopen(path, "rb");
|
|
if (in == NULL)
|
|
error("Failed to open \"%s\" for reading", path);
|
|
|
|
char riff[4];
|
|
uint32_t size;
|
|
char wave[4];
|
|
|
|
FREAD(in, riff, 4);
|
|
FREAD(in, &size, 4);
|
|
size = le32toh(size);
|
|
FREAD(in, wave, 4);
|
|
|
|
if (!CC4_CHECK(riff, "RIFF") || !CC4_CHECK(wave, "WAVE"))
|
|
error("Not a wav file?");
|
|
|
|
while (true) {
|
|
bool skipped = false;
|
|
|
|
long start = ftell(in);
|
|
if (start > 8 + size) {
|
|
error("Overran file");
|
|
}
|
|
if (start == 8 + size) {
|
|
break;
|
|
}
|
|
|
|
char cc4[4];
|
|
uint32_t chunk_size;
|
|
|
|
FREAD(in, cc4, 4);
|
|
FREAD(in, &chunk_size, 4);
|
|
chunk_size = le32toh(chunk_size);
|
|
|
|
switch (CC4(cc4[0], cc4[1], cc4[2], cc4[3])) {
|
|
case CC4('f', 'm', 't', ' '): {
|
|
wav_fmt fmt;
|
|
FREAD(in, &fmt, sizeof(fmt));
|
|
fmt.type = le16toh(fmt.type);
|
|
fmt.num_channels = le16toh(fmt.num_channels);
|
|
fmt.sample_rate = le32toh(fmt.sample_rate);
|
|
fmt.byte_rate = le32toh(fmt.byte_rate);
|
|
fmt.block_align = le16toh(fmt.block_align);
|
|
fmt.bit_depth = le16toh(fmt.bit_depth);
|
|
|
|
if (fmt.bit_depth != 16)
|
|
error("Wav input format should be 16-bit PCM, was %u-bit", fmt.bit_depth);
|
|
|
|
switch (fmt.type) {
|
|
case WAVE_TYPE_PCM:
|
|
out->data_type = SAMPLE_TYPE_PCM16;
|
|
break;
|
|
|
|
case WAVE_TYPE_FLOAT:
|
|
case WAVE_TYPE_MULAW:
|
|
case WAVE_TYPE_ALAW:
|
|
case WAVE_TYPE_EXTENSIBLE:
|
|
error("Unhandled sample type: %s, should be PCM", wav_type_name(fmt.type));
|
|
break;
|
|
|
|
default:
|
|
error("Unrecognized sample type: %d, should be PCM", fmt.type);
|
|
break;
|
|
}
|
|
|
|
out->num_channels = fmt.num_channels;
|
|
out->sample_rate = fmt.sample_rate;
|
|
out->byte_rate = fmt.byte_rate;
|
|
out->block_align = fmt.block_align;
|
|
out->bit_depth = fmt.bit_depth;
|
|
|
|
has_fmt = true;
|
|
} break;
|
|
|
|
case CC4('f', 'a', 'c', 't'): {
|
|
wav_fact fact;
|
|
FREAD(in, &fact, sizeof(fact));
|
|
fact.num_samples = le32toh(fact.num_samples);
|
|
|
|
out->num_samples = fact.num_samples;
|
|
|
|
has_fact = true;
|
|
} break;
|
|
|
|
case CC4('d', 'a', 't', 'a'): {
|
|
void *data = MALLOC_CHECKED_INFO(chunk_size, "data size = %u", chunk_size);
|
|
FREAD(in, data, chunk_size);
|
|
|
|
out->data = data;
|
|
out->data_size = chunk_size;
|
|
|
|
has_data = true;
|
|
} break;
|
|
|
|
case CC4('i', 'n', 's', 't'): {
|
|
wav_inst inst;
|
|
FREAD(in, &inst, sizeof(inst));
|
|
|
|
out->base_note = inst.base_note;
|
|
out->fine_tune = inst.fine_tune;
|
|
out->gain = inst.gain;
|
|
out->key_low = inst.key_low;
|
|
out->key_hi = inst.key_hi;
|
|
out->vel_low = inst.vel_low;
|
|
out->vel_hi = inst.vel_hi;
|
|
|
|
has_inst = true;
|
|
} break;
|
|
|
|
case CC4('s', 'm', 'p', 'l'): {
|
|
wav_smpl smpl;
|
|
FREAD(in, &smpl, sizeof(smpl));
|
|
smpl.manufacturer = le32toh(smpl.manufacturer);
|
|
smpl.product = le32toh(smpl.product);
|
|
smpl.sample_period = le32toh(smpl.sample_period);
|
|
smpl.unity_note = le32toh(smpl.unity_note);
|
|
smpl.format = le32toh(smpl.format);
|
|
smpl.offset = le32toh(smpl.offset);
|
|
smpl.num_sample_loops = le32toh(smpl.num_sample_loops);
|
|
smpl.sampler_data = le32toh(smpl.sampler_data);
|
|
|
|
out->num_loops = smpl.num_sample_loops;
|
|
if (out->num_loops != 0) {
|
|
out->loops =
|
|
MALLOC_CHECKED_INFO(out->num_loops * sizeof(container_loop), "num_loops=%u", out->num_loops);
|
|
|
|
for (size_t i = 0; i < out->num_loops; i++) {
|
|
wav_loop loop;
|
|
FREAD(in, &loop, sizeof(loop));
|
|
loop.cue_point_index = le32toh(loop.cue_point_index);
|
|
loop.type = le32toh(loop.type);
|
|
loop.start = le32toh(loop.start);
|
|
loop.end = le32toh(loop.end);
|
|
loop.fraction = le32toh(loop.fraction);
|
|
loop.num = le32toh(loop.num);
|
|
|
|
loop_type type;
|
|
switch (loop.type) {
|
|
case 0:
|
|
type = LOOP_FORWARD;
|
|
break;
|
|
|
|
case 1:
|
|
type = LOOP_FORWARD_BACKWARD;
|
|
break;
|
|
|
|
case 2:
|
|
type = LOOP_BACKWARD;
|
|
break;
|
|
|
|
default:
|
|
error("Unrecognized loop type in wav");
|
|
}
|
|
|
|
out->loops[i].id = i;
|
|
out->loops[i].type = type;
|
|
out->loops[i].start = loop.start;
|
|
out->loops[i].end = loop.end;
|
|
out->loops[i].fraction = loop.fraction;
|
|
out->loops[i].num = loop.num;
|
|
}
|
|
}
|
|
has_smpl = true;
|
|
} break;
|
|
|
|
case CC4('z', 'z', 'b', 'k'): {
|
|
char vadpcmcodes[12];
|
|
uint16_t version;
|
|
uint16_t order;
|
|
uint16_t npredictors;
|
|
|
|
FREAD(in, vadpcmcodes, sizeof(vadpcmcodes));
|
|
FREAD(in, &version, sizeof(version));
|
|
version = le16toh(version);
|
|
FREAD(in, &order, sizeof(order));
|
|
order = le16toh(order);
|
|
FREAD(in, &npredictors, sizeof(npredictors));
|
|
npredictors = le16toh(npredictors);
|
|
|
|
size_t book_size = VADPCM_BOOK_SIZE(order, npredictors);
|
|
size_t book_data_size = sizeof(int16_t) * book_size;
|
|
int16_t *book_data =
|
|
MALLOC_CHECKED_INFO(book_data_size, "order=%u, npredictors=%u", order, npredictors);
|
|
FREAD(in, book_data, book_data_size);
|
|
|
|
out->vadpcm.book_header.order = order;
|
|
out->vadpcm.book_header.npredictors = npredictors;
|
|
out->vadpcm.book_data = book_data;
|
|
|
|
for (size_t i = 0; i < book_size; i++) {
|
|
out->vadpcm.book_data[i] = le16toh(out->vadpcm.book_data[i]);
|
|
}
|
|
|
|
out->vadpcm.has_book = true;
|
|
} break;
|
|
|
|
case CC4('z', 'z', 'l', 'p'): {
|
|
uint16_t version;
|
|
uint16_t nloops;
|
|
|
|
FREAD(in, &version, sizeof(version));
|
|
version = le16toh(version);
|
|
FREAD(in, &nloops, sizeof(nloops));
|
|
nloops = le16toh(nloops);
|
|
|
|
if (nloops != 0)
|
|
out->vadpcm.loops = MALLOC_CHECKED_INFO(nloops * sizeof(ALADPCMloop), "nloops=%u", nloops);
|
|
|
|
for (size_t i = 0; i < nloops; i++) {
|
|
uint32_t loop_start;
|
|
uint32_t loop_end;
|
|
uint32_t loop_num;
|
|
|
|
FREAD(in, &loop_start, sizeof(loop_start));
|
|
loop_start = le32toh(loop_start);
|
|
FREAD(in, &loop_end, sizeof(loop_end));
|
|
loop_end = le32toh(loop_end);
|
|
FREAD(in, &loop_num, sizeof(loop_num));
|
|
loop_num = le32toh(loop_num);
|
|
|
|
out->vadpcm.loops[i].start = loop_start;
|
|
out->vadpcm.loops[i].end = loop_end;
|
|
out->vadpcm.loops[i].count = loop_num;
|
|
|
|
if (out->vadpcm.loops[i].count != 0) {
|
|
FREAD(in, out->vadpcm.loops[i].state, sizeof(out->vadpcm.loops[i].state));
|
|
|
|
for (size_t j = 0; j < ARRAY_COUNT(out->vadpcm.loops[i].state); j++) {
|
|
out->vadpcm.loops[i].state[j] = le16toh(out->vadpcm.loops[i].state[j]);
|
|
}
|
|
}
|
|
}
|
|
} break;
|
|
|
|
default:
|
|
warning("Skipping unknown wav chunk: \"%c%c%c%c\"", cc4[0], cc4[1], cc4[2], cc4[3]);
|
|
skipped = true;
|
|
break;
|
|
}
|
|
|
|
long read_size = ftell(in) - start - 8;
|
|
|
|
if (read_size > chunk_size)
|
|
error("overran chunk");
|
|
else if (!skipped && read_size < chunk_size)
|
|
warning("did not read entire %*s chunk: %lu vs %u", 4, cc4, read_size, chunk_size);
|
|
|
|
fseek(in, start + 8 + chunk_size, SEEK_SET);
|
|
}
|
|
|
|
if (!has_fmt)
|
|
error("wav has no fmt chunk");
|
|
if (!has_data)
|
|
error("wav has no data chunk");
|
|
|
|
if (!has_fact) {
|
|
out->num_samples = out->data_size / (out->bit_depth / 8);
|
|
}
|
|
|
|
if (!has_inst) {
|
|
out->base_note = 60; // C4
|
|
out->fine_tune = 0;
|
|
out->gain = 0;
|
|
out->key_low = 0;
|
|
out->key_hi = 0;
|
|
out->vel_low = 0;
|
|
out->vel_hi = 0;
|
|
}
|
|
|
|
if (!has_smpl) {
|
|
out->num_loops = 0;
|
|
out->loops = NULL;
|
|
}
|
|
|
|
if (out->data_type == SAMPLE_TYPE_PCM16) {
|
|
if (out->data_size % 2 != 0)
|
|
error("wav data size is not a multiple of 2 despite being pcm16-formatted?");
|
|
|
|
for (size_t i = 0; i < out->data_size / 2; i++)
|
|
((uint16_t *)(out->data))[i] = le16toh(((uint16_t *)(out->data))[i]);
|
|
}
|
|
|
|
fclose(in);
|
|
return 0;
|
|
}
|
|
|
|
int
|
|
wav_write(container_data *in, const char *path, bool matching)
|
|
{
|
|
long chunk_start;
|
|
|
|
FILE *out = fopen(path, "wb");
|
|
FWRITE(out, "RIFF\0\0\0\0WAVE", 12);
|
|
|
|
uint16_t fmt_type;
|
|
switch (in->data_type) {
|
|
case SAMPLE_TYPE_PCM16:
|
|
fmt_type = WAVE_TYPE_PCM;
|
|
break;
|
|
|
|
default:
|
|
error("Unrecognized sample type for wav output");
|
|
break;
|
|
}
|
|
|
|
wav_fmt fmt = {
|
|
.type = htole16(fmt_type),
|
|
.num_channels = htole16(in->num_channels),
|
|
.sample_rate = htole32(in->sample_rate),
|
|
.byte_rate = htole32(in->sample_rate * (in->bit_depth / 8)),
|
|
.block_align = htole16(in->num_channels * (in->bit_depth / 8)),
|
|
.bit_depth = htole16(in->bit_depth),
|
|
};
|
|
CHUNK_BEGIN(out, "fmt ", &chunk_start);
|
|
CHUNK_WRITE(out, &fmt);
|
|
CHUNK_END(out, chunk_start, htole32);
|
|
|
|
wav_fact fact = {
|
|
.num_samples = htole32(in->num_samples),
|
|
};
|
|
CHUNK_BEGIN(out, "fact", &chunk_start);
|
|
CHUNK_WRITE(out, &fact);
|
|
CHUNK_END(out, chunk_start, htole32);
|
|
|
|
if (in->data_type == SAMPLE_TYPE_PCM16) {
|
|
assert(in->bit_depth == 16);
|
|
assert(in->data_size % 2 == 0);
|
|
|
|
for (size_t i = 0; i < in->data_size / 2; i++) {
|
|
((uint16_t *)in->data)[i] = htole16(((uint16_t *)in->data)[i]);
|
|
}
|
|
}
|
|
|
|
CHUNK_BEGIN(out, "data", &chunk_start);
|
|
CHUNK_WRITE_RAW(out, in->data, in->data_size);
|
|
CHUNK_END(out, chunk_start, htole32);
|
|
|
|
wav_inst inst = {
|
|
.base_note = in->base_note,
|
|
.fine_tune = in->fine_tune,
|
|
.gain = in->gain,
|
|
.key_low = in->key_low,
|
|
.key_hi = in->key_hi,
|
|
.vel_low = in->vel_low,
|
|
.vel_hi = in->vel_hi,
|
|
._pad = { 0 },
|
|
};
|
|
CHUNK_BEGIN(out, "inst", &chunk_start);
|
|
CHUNK_WRITE(out, &inst);
|
|
CHUNK_END(out, chunk_start, htole32);
|
|
|
|
wav_smpl smpl = {
|
|
.manufacturer = 0,
|
|
.product = 0,
|
|
.sample_period = 0,
|
|
.unity_note = 0,
|
|
._pad = {0, 0, 0},
|
|
.fine_tune = 0,
|
|
.format = 0,
|
|
.offset = 0,
|
|
.num_sample_loops = htole32(in->num_loops),
|
|
.sampler_data = 0,
|
|
};
|
|
CHUNK_BEGIN(out, "smpl", &chunk_start);
|
|
CHUNK_WRITE(out, &smpl);
|
|
for (size_t i = 0; i < in->num_loops; i++) {
|
|
uint32_t wav_loop_type;
|
|
switch (in->loops[i].type) {
|
|
case LOOP_FORWARD:
|
|
wav_loop_type = 0;
|
|
break;
|
|
|
|
case LOOP_FORWARD_BACKWARD:
|
|
wav_loop_type = 1;
|
|
break;
|
|
|
|
case LOOP_BACKWARD:
|
|
wav_loop_type = 2;
|
|
break;
|
|
|
|
default:
|
|
error("Unrecognized loop type for wav output");
|
|
}
|
|
|
|
wav_loop loop = {
|
|
.cue_point_index = 0,
|
|
.type = htole32(wav_loop_type),
|
|
.start = htole32(in->loops[i].start),
|
|
.end = htole32(in->loops[i].end),
|
|
.fraction = htole32(in->loops[i].fraction),
|
|
.num = htole32(in->loops[i].num),
|
|
};
|
|
CHUNK_WRITE(out, &loop);
|
|
}
|
|
CHUNK_END(out, chunk_start, htole32);
|
|
|
|
// Books generally don't match on round-trip, if matching we should write a vadpcm book chunk to the uncompressed
|
|
// output so it may be read back when re-encoding.
|
|
if (in->vadpcm.has_book && matching) {
|
|
uint32_t book_size = VADPCM_BOOK_SIZE(in->vadpcm.book_header.order, in->vadpcm.book_header.npredictors);
|
|
|
|
uint16_t version = htole16(in->vadpcm.book_version);
|
|
uint16_t order = htole16(in->vadpcm.book_header.order);
|
|
uint16_t npredictors = htole16(in->vadpcm.book_header.npredictors);
|
|
|
|
int16_t book_state[book_size];
|
|
for (size_t i = 0; i < book_size; i++) {
|
|
book_state[i] = htole16(in->vadpcm.book_data[i]);
|
|
}
|
|
|
|
CHUNK_BEGIN(out, "zzbk", &chunk_start);
|
|
CHUNK_WRITE_RAW(out, "VADPCMCODES", sizeof("VADPCMCODES"));
|
|
CHUNK_WRITE(out, &version);
|
|
CHUNK_WRITE(out, &order);
|
|
CHUNK_WRITE(out, &npredictors);
|
|
CHUNK_WRITE_RAW(out, book_state, sizeof(int16_t) * book_size);
|
|
CHUNK_END(out, chunk_start, htole32);
|
|
}
|
|
|
|
// Loop states match on round-trip while books don't, so don't write a vadpcm loop chunk if the output format
|
|
// is uncompressed.
|
|
if (in->vadpcm.num_loops != 0 && in->data_type != SAMPLE_TYPE_PCM16) {
|
|
CHUNK_BEGIN(out, "zzlp", &chunk_start);
|
|
|
|
for (size_t i = 0; i < in->vadpcm.num_loops; i++) {
|
|
uint32_t loop_start = htole32(in->vadpcm.loops[i].start);
|
|
uint32_t loop_end = htole32(in->vadpcm.loops[i].end);
|
|
uint32_t loop_count = htole32(in->vadpcm.loops[i].count);
|
|
|
|
int16_t loop_state[16];
|
|
for (size_t j = 0; j < ARRAY_COUNT(in->vadpcm.loops[i].state); j++) {
|
|
loop_state[j] = htole16(in->vadpcm.loops[i].state[j]);
|
|
}
|
|
|
|
CHUNK_WRITE(out, &loop_start);
|
|
CHUNK_WRITE(out, &loop_end);
|
|
CHUNK_WRITE(out, &loop_count);
|
|
CHUNK_WRITE_RAW(out, loop_state, sizeof(loop_state));
|
|
}
|
|
|
|
CHUNK_END(out, chunk_start, htole32);
|
|
}
|
|
|
|
uint32_t size = htole32(ftell(out) - 8);
|
|
fseek(out, 4, SEEK_SET);
|
|
fwrite(&size, 4, 1, out);
|
|
fclose(out);
|
|
|
|
return 0;
|
|
}
|