mirror of
https://github.com/zeldaret/oot.git
synced 2025-08-11 17:30:25 +00:00
[Audio 2/?] Extract audio samples to wav (#2020)
* [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>
This commit is contained in:
parent
1021c482af
commit
ef329e633a
24 changed files with 4317 additions and 112 deletions
837
tools/audio/sampleconv/src/container/aiff.c
Normal file
837
tools/audio/sampleconv/src/container/aiff.c
Normal file
|
@ -0,0 +1,837 @@
|
|||
/**
|
||||
* 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 <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "../util.h"
|
||||
#include "../codec/vadpcm.h"
|
||||
#include "aiff.h"
|
||||
|
||||
typedef struct {
|
||||
int16_t numChannels;
|
||||
uint16_t numFramesH;
|
||||
uint16_t numFramesL;
|
||||
int16_t sampleSize;
|
||||
uint8_t sampleRate[10]; // 80-bit float
|
||||
// Followed by compression type + compression name pstring
|
||||
} aiff_COMM;
|
||||
|
||||
typedef struct {
|
||||
uint16_t nMarkers;
|
||||
} aiff_MARK;
|
||||
|
||||
typedef struct {
|
||||
uint16_t MarkerID;
|
||||
uint16_t positionH;
|
||||
uint16_t positionL;
|
||||
} Marker;
|
||||
|
||||
typedef enum {
|
||||
LOOP_PLAYMODE_NONE = 0,
|
||||
LOOP_PLAYMODE_FWD = 1,
|
||||
LOOP_PLAYMODE_FWD_BWD = 2
|
||||
} aiff_loop_playmode;
|
||||
|
||||
typedef struct {
|
||||
int16_t playMode; // aiff_loop_playmode
|
||||
// Marker IDs
|
||||
int16_t beginLoop;
|
||||
int16_t endLoop;
|
||||
} Loop;
|
||||
|
||||
typedef struct {
|
||||
int8_t baseNote;
|
||||
int8_t detune;
|
||||
int8_t lowNote;
|
||||
int8_t highNote;
|
||||
int8_t lowVelocity;
|
||||
int8_t highVelocity;
|
||||
int16_t gain;
|
||||
Loop sustainLoop;
|
||||
Loop releaseLoop;
|
||||
} aiff_INST;
|
||||
|
||||
typedef struct {
|
||||
int32_t offset;
|
||||
int32_t blockSize;
|
||||
} aiff_SSND;
|
||||
|
||||
static void
|
||||
read_pstring(FILE *f, char *out)
|
||||
{
|
||||
unsigned char len;
|
||||
|
||||
// read string length
|
||||
FREAD(f, &len, sizeof(len));
|
||||
|
||||
// read string and null-terminate it
|
||||
FREAD(f, out, len);
|
||||
out[len] = '\0';
|
||||
|
||||
// pad to 2-byte boundary
|
||||
if (!(len & 1))
|
||||
FREAD(f, &len, 1);
|
||||
}
|
||||
|
||||
static char *
|
||||
read_pstring_alloc(FILE *f)
|
||||
{
|
||||
unsigned char len;
|
||||
|
||||
// read string length
|
||||
FREAD(f, &len, sizeof(len));
|
||||
|
||||
// alloc
|
||||
char *out = MALLOC_CHECKED_INFO(len + 1, "len=%u", len);
|
||||
|
||||
// read string and null-terminate it
|
||||
FREAD(f, out, len);
|
||||
out[len] = '\0';
|
||||
|
||||
// pad to 2-byte boundary
|
||||
if (!(len & 1))
|
||||
FREAD(f, &len, 1);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static void
|
||||
write_pstring(FILE *f, const char *in)
|
||||
{
|
||||
unsigned char zr = 0;
|
||||
size_t inlen = strlen(in);
|
||||
|
||||
if (inlen > 255)
|
||||
error("Invalid pstring length.");
|
||||
|
||||
unsigned char len = inlen;
|
||||
|
||||
CHUNK_WRITE(f, &len);
|
||||
CHUNK_WRITE_RAW(f, in, len);
|
||||
if (!(len & 1))
|
||||
CHUNK_WRITE(f, &zr);
|
||||
}
|
||||
|
||||
static_assert(sizeof(double) == sizeof(uint64_t), "Double is assumed to be 64-bit");
|
||||
|
||||
#define F64_GET_SGN(bits) (((bits) >> 63) & 1) // 1-bit
|
||||
#define F64_GET_EXP(bits) ((((bits) >> 52) & 0x7FF) - 0x3FF) // 15-bit
|
||||
#define F64_GET_MANT_H(bits) (((bits) >> 32) & 0xFFFFF) // 20-bit
|
||||
#define F64_GET_MANT_L(bits) ((bits)&0xFFFFFFFF) // 32-bit
|
||||
|
||||
static void
|
||||
f64_to_f80(double f64, uint8_t *f80)
|
||||
{
|
||||
union {
|
||||
uint32_t w[3];
|
||||
uint8_t b[12];
|
||||
} f80tmp;
|
||||
|
||||
// get f64 bits
|
||||
|
||||
uint64_t f64_bits = *(uint64_t *)&f64;
|
||||
|
||||
int f64_sgn = F64_GET_SGN(f64_bits);
|
||||
int f64_exponent = F64_GET_EXP(f64_bits);
|
||||
uint32_t f64_mantissa_hi = F64_GET_MANT_H(f64_bits);
|
||||
uint32_t f64_mantissa_lo = F64_GET_MANT_L(f64_bits);
|
||||
|
||||
// build f80 words
|
||||
|
||||
f80tmp.w[0] = (f64_sgn << 15) | (f64_exponent + 0x3FFF);
|
||||
f80tmp.w[1] = (1 << 31) | (f64_mantissa_hi << 11) | (f64_mantissa_lo >> 21);
|
||||
f80tmp.w[2] = f64_mantissa_lo << 11;
|
||||
|
||||
// byteswap to BE
|
||||
|
||||
f80tmp.w[0] = htobe32(f80tmp.w[0]);
|
||||
f80tmp.w[1] = htobe32(f80tmp.w[1]);
|
||||
f80tmp.w[2] = htobe32(f80tmp.w[2]);
|
||||
|
||||
// write bytes
|
||||
|
||||
for (size_t i = 0; i < 10; i++)
|
||||
f80[i] = f80tmp.b[i + 2];
|
||||
}
|
||||
|
||||
static void
|
||||
f80_to_f64(double *f64, uint8_t *f80)
|
||||
{
|
||||
union {
|
||||
uint32_t w[3];
|
||||
uint8_t b[12];
|
||||
} f80tmp;
|
||||
|
||||
// read bytes
|
||||
|
||||
f80tmp.b[0] = f80tmp.b[1] = 0;
|
||||
for (size_t i = 0; i < 10; i++)
|
||||
f80tmp.b[i + 2] = f80[i];
|
||||
|
||||
// byteswap from BE
|
||||
|
||||
f80tmp.w[0] = be32toh(f80tmp.w[0]);
|
||||
f80tmp.w[1] = be32toh(f80tmp.w[1]);
|
||||
f80tmp.w[2] = be32toh(f80tmp.w[2]);
|
||||
|
||||
// get f64 parts
|
||||
|
||||
int f64_sgn = (f80tmp.w[0] >> 15) & 1;
|
||||
int f64_exponent = (f80tmp.w[0] & 0x7FFF) - 0x3FFF;
|
||||
uint32_t f64_mantissa_hi = (f80tmp.w[1] >> 11) & 0xFFFFF;
|
||||
uint32_t f64_mantissa_lo = ((f80tmp.w[1] & 0x7FF) << 21) | (f80tmp.w[2] >> 11);
|
||||
|
||||
// build bitwise f64
|
||||
|
||||
uint64_t f64_bits = ((uint64_t)f64_sgn << 63) | ((((uint64_t)f64_exponent + 0x3FF) & 0x7FF) << 52) |
|
||||
((uint64_t)f64_mantissa_hi << 32) | ((uint64_t)f64_mantissa_lo);
|
||||
|
||||
// write double
|
||||
|
||||
*f64 = *(double *)&f64_bits;
|
||||
}
|
||||
|
||||
int
|
||||
aiff_aifc_common_read(container_data *out, FILE *in, UNUSED bool matching, uint32_t size)
|
||||
{
|
||||
bool has_comm = false;
|
||||
bool has_inst = false;
|
||||
bool has_ssnd = false;
|
||||
|
||||
size_t num_markers = 0;
|
||||
container_marker *markers = NULL;
|
||||
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
while (true) {
|
||||
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 = be32toh(chunk_size);
|
||||
|
||||
chunk_size++;
|
||||
chunk_size &= ~1;
|
||||
|
||||
switch (CC4(cc4[0], cc4[1], cc4[2], cc4[3])) {
|
||||
case CC4('C', 'O', 'M', 'M'): {
|
||||
aiff_COMM comm;
|
||||
FREAD(in, &comm, sizeof(comm));
|
||||
comm.numChannels = be16toh(comm.numChannels);
|
||||
comm.numFramesH = be16toh(comm.numFramesH);
|
||||
comm.numFramesL = be16toh(comm.numFramesL);
|
||||
comm.sampleSize = be16toh(comm.sampleSize);
|
||||
|
||||
uint32_t num_samples = (comm.numFramesH << 16) | comm.numFramesL;
|
||||
double sample_rate;
|
||||
f80_to_f64(&sample_rate, comm.sampleRate);
|
||||
|
||||
uint32_t comp_type = CC4('N', 'O', 'N', 'E');
|
||||
if (chunk_size > sizeof(aiff_COMM)) {
|
||||
uint32_t compressionType;
|
||||
FREAD(in, &compressionType, sizeof(compressionType));
|
||||
comp_type = be32toh(compressionType);
|
||||
}
|
||||
|
||||
out->num_channels = comm.numChannels;
|
||||
out->sample_rate = sample_rate;
|
||||
out->bit_depth = comm.sampleSize;
|
||||
out->num_samples = num_samples;
|
||||
|
||||
switch (comp_type) {
|
||||
case CC4('N', 'O', 'N', 'E'):
|
||||
out->data_type = SAMPLE_TYPE_PCM16;
|
||||
break;
|
||||
|
||||
case CC4('H', 'P', 'C', 'M'):
|
||||
out->data_type = SAMPLE_TYPE_PCM8;
|
||||
break;
|
||||
|
||||
case CC4('A', 'D', 'P', '9'):
|
||||
out->data_type = SAMPLE_TYPE_VADPCM;
|
||||
break;
|
||||
|
||||
case CC4('A', 'D', 'P', '5'):
|
||||
out->data_type = SAMPLE_TYPE_VADPCM_HALF;
|
||||
break;
|
||||
|
||||
default:
|
||||
error("Unrecognized aiff/aifc compression type");
|
||||
break;
|
||||
}
|
||||
|
||||
if (chunk_size > sizeof(aiff_COMM) + 4) {
|
||||
char compression_name[257];
|
||||
read_pstring(in, compression_name);
|
||||
}
|
||||
has_comm = true;
|
||||
} break;
|
||||
|
||||
case CC4('I', 'N', 'S', 'T'): {
|
||||
aiff_INST inst;
|
||||
FREAD(in, &inst, sizeof(inst));
|
||||
inst.gain = be16toh(inst.gain);
|
||||
inst.sustainLoop.playMode = be16toh(inst.sustainLoop.playMode);
|
||||
inst.sustainLoop.beginLoop = be16toh(inst.sustainLoop.beginLoop);
|
||||
inst.sustainLoop.endLoop = be16toh(inst.sustainLoop.endLoop);
|
||||
inst.releaseLoop.playMode = be16toh(inst.releaseLoop.playMode);
|
||||
inst.releaseLoop.beginLoop = be16toh(inst.releaseLoop.beginLoop);
|
||||
inst.releaseLoop.endLoop = be16toh(inst.releaseLoop.endLoop);
|
||||
|
||||
out->base_note = inst.baseNote;
|
||||
out->fine_tune = inst.detune;
|
||||
out->key_low = inst.lowNote;
|
||||
out->key_hi = inst.highNote;
|
||||
out->vel_low = inst.lowVelocity;
|
||||
out->vel_hi = inst.highVelocity;
|
||||
out->gain = inst.gain;
|
||||
|
||||
size_t nloops = 0;
|
||||
nloops += inst.sustainLoop.playMode != LOOP_PLAYMODE_NONE;
|
||||
nloops += inst.releaseLoop.playMode != LOOP_PLAYMODE_NONE;
|
||||
|
||||
if (nloops != 0) {
|
||||
out->loops = MALLOC_CHECKED_INFO(nloops * sizeof(container_loop), "nloops=%lu", nloops);
|
||||
|
||||
size_t i = 0;
|
||||
|
||||
if (inst.sustainLoop.playMode != LOOP_PLAYMODE_NONE) {
|
||||
out->loops[i].id = 0;
|
||||
|
||||
switch (inst.sustainLoop.playMode) {
|
||||
case LOOP_PLAYMODE_FWD:
|
||||
out->loops[i].type = LOOP_FORWARD;
|
||||
break;
|
||||
|
||||
case LOOP_PLAYMODE_FWD_BWD:
|
||||
out->loops[i].type = LOOP_FORWARD_BACKWARD;
|
||||
break;
|
||||
|
||||
default:
|
||||
error("Unrecognized PlayMode in sustainLoop");
|
||||
break;
|
||||
}
|
||||
out->loops[i].start = inst.sustainLoop.beginLoop;
|
||||
out->loops[i].end = inst.sustainLoop.endLoop;
|
||||
out->loops[i].num = 0xFFFFFFFF;
|
||||
out->loops[i].fraction = 0;
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
if (inst.releaseLoop.playMode != LOOP_PLAYMODE_NONE) {
|
||||
out->loops[i].id = 1;
|
||||
|
||||
switch (inst.sustainLoop.playMode) {
|
||||
case LOOP_PLAYMODE_FWD:
|
||||
out->loops[i].type = LOOP_FORWARD;
|
||||
break;
|
||||
|
||||
case LOOP_PLAYMODE_FWD_BWD:
|
||||
out->loops[i].type = LOOP_FORWARD_BACKWARD;
|
||||
break;
|
||||
|
||||
default:
|
||||
error("Unrecognized PlayMode in releaseLoop");
|
||||
break;
|
||||
}
|
||||
out->loops[i].start = inst.releaseLoop.beginLoop;
|
||||
out->loops[i].end = inst.releaseLoop.endLoop;
|
||||
out->loops[i].num = 0xFFFFFFFF;
|
||||
out->loops[i].fraction = 0;
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
has_inst = true;
|
||||
} break;
|
||||
|
||||
case CC4('M', 'A', 'R', 'K'): {
|
||||
aiff_MARK mark;
|
||||
FREAD(in, &mark, sizeof(mark));
|
||||
mark.nMarkers = be16toh(mark.nMarkers);
|
||||
|
||||
num_markers = mark.nMarkers;
|
||||
markers = MALLOC_CHECKED_INFO(num_markers * sizeof(container_marker), "num_markers=%lu", num_markers);
|
||||
|
||||
for (size_t i = 0; i < num_markers; i++) {
|
||||
Marker marker;
|
||||
char *name;
|
||||
FREAD(in, &marker, sizeof(marker));
|
||||
name = read_pstring_alloc(in);
|
||||
|
||||
markers[i].id = be16toh(marker.MarkerID);
|
||||
markers[i].frame_number = (be16toh(marker.positionH) << 16) | be16toh(marker.positionL);
|
||||
markers[i].name = name;
|
||||
}
|
||||
} break;
|
||||
|
||||
case CC4('A', 'P', 'P', 'L'): {
|
||||
char subcc4[4];
|
||||
|
||||
FREAD(in, subcc4, 4);
|
||||
|
||||
switch (CC4(subcc4[0], subcc4[1], subcc4[2], subcc4[3])) {
|
||||
case CC4('s', 't', 'o', 'c'): {
|
||||
char chunk_name[257];
|
||||
read_pstring(in, chunk_name);
|
||||
|
||||
if (strequ(chunk_name, "VADPCMCODES")) {
|
||||
int16_t version;
|
||||
uint16_t order;
|
||||
uint16_t npredictors;
|
||||
|
||||
FREAD(in, &version, sizeof(version));
|
||||
version = be16toh(version);
|
||||
FREAD(in, &order, sizeof(order));
|
||||
order = be16toh(order);
|
||||
FREAD(in, &npredictors, sizeof(npredictors));
|
||||
npredictors = be16toh(npredictors);
|
||||
|
||||
size_t book_size_bytes = VADPCM_BOOK_SIZE_BYTES(order, npredictors);
|
||||
int16_t *book_state =
|
||||
MALLOC_CHECKED_INFO(book_size_bytes, "order=%u, npredictors=%u", order, npredictors);
|
||||
FREAD(in, book_state, book_size_bytes);
|
||||
|
||||
out->vadpcm.book_version = version;
|
||||
out->vadpcm.book_header.order = order;
|
||||
out->vadpcm.book_header.npredictors = npredictors;
|
||||
out->vadpcm.book_data = book_state;
|
||||
|
||||
for (size_t i = 0; i < VADPCM_BOOK_SIZE(order, npredictors); i++)
|
||||
out->vadpcm.book_data[i] = be16toh(out->vadpcm.book_data[i]);
|
||||
|
||||
out->vadpcm.has_book = true;
|
||||
} else if (strequ(chunk_name, "VADPCMLOOPS")) {
|
||||
int16_t version;
|
||||
int16_t nloops;
|
||||
|
||||
FREAD(in, &version, sizeof(version));
|
||||
version = be16toh(version);
|
||||
FREAD(in, &nloops, sizeof(nloops));
|
||||
nloops = be16toh(nloops);
|
||||
|
||||
out->vadpcm.loop_version = version;
|
||||
out->vadpcm.num_loops = nloops;
|
||||
|
||||
if (out->vadpcm.num_loops)
|
||||
out->vadpcm.loops = MALLOC_CHECKED_INFO(out->vadpcm.num_loops * sizeof(ALADPCMloop),
|
||||
"num_loops=%u", out->vadpcm.num_loops);
|
||||
|
||||
for (size_t i = 0; i < out->vadpcm.num_loops; i++) {
|
||||
FREAD(in, &out->vadpcm.loops[i], sizeof(ALADPCMloop));
|
||||
out->vadpcm.loops[i].start = be32toh(out->vadpcm.loops[i].start);
|
||||
out->vadpcm.loops[i].end = be32toh(out->vadpcm.loops[i].end);
|
||||
out->vadpcm.loops[i].count = be32toh(out->vadpcm.loops[i].count);
|
||||
for (size_t j = 0; j < ARRAY_COUNT(out->vadpcm.loops[i].state); j++)
|
||||
out->vadpcm.loops[i].state[j] = be16toh(out->vadpcm.loops[i].state[j]);
|
||||
}
|
||||
} else {
|
||||
warning("Skipping unknown APPL::stoc subchunk: \"%s\"", chunk_name);
|
||||
}
|
||||
} break;
|
||||
|
||||
default:
|
||||
warning("Skipping unknown APPL subchunk: \"%c%c%c%c\"", subcc4[0], subcc4[1], subcc4[2],
|
||||
subcc4[3]);
|
||||
break;
|
||||
}
|
||||
} break;
|
||||
|
||||
case CC4('S', 'S', 'N', 'D'): {
|
||||
aiff_SSND ssnd;
|
||||
FREAD(in, &ssnd, sizeof(ssnd));
|
||||
ssnd.offset = be32toh(ssnd.offset);
|
||||
ssnd.blockSize = be32toh(ssnd.blockSize);
|
||||
|
||||
size_t data_size = chunk_size - sizeof(ssnd);
|
||||
|
||||
void *data = MALLOC_CHECKED_INFO(data_size, "SSND chunk size = %lu", data_size);
|
||||
FREAD(in, data, data_size);
|
||||
|
||||
if (ssnd.offset != 0 || ssnd.blockSize != 0)
|
||||
error("Bad SSND chunk in aiff/aifc, offset/blockSize != 0");
|
||||
|
||||
out->data = data;
|
||||
out->data_size = data_size;
|
||||
|
||||
has_ssnd = true;
|
||||
} break;
|
||||
|
||||
default:
|
||||
warning("Skipping unknown aiff chunk: \"%c%c%c%c\"", cc4[0], cc4[1], cc4[2], cc4[3]);
|
||||
break;
|
||||
}
|
||||
|
||||
long read_size = ftell(in) - start - 8;
|
||||
|
||||
if (read_size > chunk_size)
|
||||
error("overran chunk: %lu vs %u\n", read_size, chunk_size);
|
||||
else if (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_comm)
|
||||
error("aiff/aifc has no COMM chunk");
|
||||
if (!has_inst)
|
||||
error("aiff/aifc has no INST chunk");
|
||||
if (!has_ssnd)
|
||||
error("aiff/aifc has no SSND chunk");
|
||||
|
||||
if (out->data_type == SAMPLE_TYPE_PCM16) {
|
||||
assert(out->data_size % 2 == 0);
|
||||
assert(out->bit_depth == 16);
|
||||
|
||||
for (size_t i = 0; i < out->data_size / 2; i++)
|
||||
((uint16_t *)(out->data))[i] = be16toh(((uint16_t *)(out->data))[i]);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < out->num_loops; i++) {
|
||||
container_marker *marker;
|
||||
|
||||
marker = NULL;
|
||||
for (size_t j = 0; j < num_markers; j++) {
|
||||
if (markers[j].id == out->loops[i].start) {
|
||||
marker = &markers[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (marker == NULL)
|
||||
error("AIFF/C loop references non-existent marker");
|
||||
|
||||
out->loops[i].start = marker->frame_number;
|
||||
|
||||
marker = NULL;
|
||||
for (size_t j = 0; j < num_markers; j++) {
|
||||
if (markers[j].id == out->loops[i].end) {
|
||||
marker = &markers[j];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (marker == NULL)
|
||||
error("AIFF/C loop references non-existent marker");
|
||||
|
||||
out->loops[i].end = marker->frame_number;
|
||||
}
|
||||
|
||||
fclose(in);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
aifc_read(container_data *out, const char *path, bool matching)
|
||||
{
|
||||
FILE *in = fopen(path, "rb");
|
||||
if (in == NULL)
|
||||
error("Failed to open \"%s\" for reading", path);
|
||||
|
||||
char form[4];
|
||||
uint32_t size;
|
||||
char aifc[4];
|
||||
|
||||
FREAD(in, form, 4);
|
||||
FREAD(in, &size, 4);
|
||||
size = be32toh(size);
|
||||
FREAD(in, aifc, 4);
|
||||
|
||||
if (!CC4_CHECK(form, "FORM") || !CC4_CHECK(aifc, "AIFC"))
|
||||
error("Not an aifc file?");
|
||||
|
||||
return aiff_aifc_common_read(out, in, matching, size);
|
||||
}
|
||||
|
||||
int
|
||||
aiff_read(container_data *out, const char *path, bool matching)
|
||||
{
|
||||
FILE *in = fopen(path, "rb");
|
||||
if (in == NULL)
|
||||
error("Failed to open \"%s\" for reading", path);
|
||||
|
||||
char form[4];
|
||||
uint32_t size;
|
||||
char aiff[4];
|
||||
|
||||
FREAD(in, form, 4);
|
||||
FREAD(in, &size, 4);
|
||||
size = be32toh(size);
|
||||
FREAD(in, aiff, 4);
|
||||
|
||||
if (!CC4_CHECK(form, "FORM") || !CC4_CHECK(aiff, "AIFF"))
|
||||
error("Not an aiff file?");
|
||||
|
||||
return aiff_aifc_common_read(out, in, matching, size);
|
||||
}
|
||||
|
||||
static int
|
||||
aiff_aifc_common_write(container_data *in, const char *path, bool aifc, bool matching)
|
||||
{
|
||||
FILE *out = fopen(path, "wb");
|
||||
if (out == NULL)
|
||||
error("Failed to open %s for writing\n", path);
|
||||
|
||||
const char *aifc_head = "FORM\0\0\0\0AIFC";
|
||||
const char *aiff_head = "FORM\0\0\0\0AIFF";
|
||||
FWRITE(out, (aifc) ? aifc_head : aiff_head, 12);
|
||||
|
||||
long chunk_start;
|
||||
|
||||
uint32_t compression_type;
|
||||
const char *compression_name;
|
||||
|
||||
switch (in->data_type) {
|
||||
case SAMPLE_TYPE_PCM16:
|
||||
compression_type = CC4('N', 'O', 'N', 'E');
|
||||
compression_name = NULL;
|
||||
break;
|
||||
|
||||
case SAMPLE_TYPE_PCM8:
|
||||
compression_type = CC4('H', 'P', 'C', 'M');
|
||||
compression_name = "Half-frame PCM";
|
||||
break;
|
||||
|
||||
case SAMPLE_TYPE_VADPCM:
|
||||
compression_type = CC4('A', 'D', 'P', '9');
|
||||
compression_name = "Nintendo/SGI VADPCM 9-bytes/frame";
|
||||
break;
|
||||
|
||||
case SAMPLE_TYPE_VADPCM_HALF:
|
||||
compression_type = CC4('A', 'D', 'P', '5');
|
||||
compression_name = "Nintendo/SGI VADPCM 5-bytes/frame";
|
||||
break;
|
||||
|
||||
default:
|
||||
error("Unhandled data type for aiff/aifc container");
|
||||
break;
|
||||
}
|
||||
|
||||
if (compression_type != CC4('N', 'O', 'N', 'E') && !aifc) {
|
||||
error("Compressed data types must use aifc output");
|
||||
}
|
||||
|
||||
aiff_COMM comm = {
|
||||
.numChannels = htobe16(in->num_channels),
|
||||
.numFramesH = htobe16(in->num_samples >> 16),
|
||||
.numFramesL = htobe16(in->num_samples),
|
||||
.sampleSize = htobe16(in->bit_depth),
|
||||
.sampleRate = { 0 },
|
||||
};
|
||||
f64_to_f80(in->sample_rate, comm.sampleRate);
|
||||
CHUNK_BEGIN(out, "COMM", &chunk_start);
|
||||
CHUNK_WRITE(out, &comm);
|
||||
if (compression_name != NULL) {
|
||||
uint32_t compressionType = htobe32(compression_type);
|
||||
CHUNK_WRITE(out, &compressionType);
|
||||
write_pstring(out, compression_name);
|
||||
}
|
||||
CHUNK_END(out, chunk_start, htobe32);
|
||||
|
||||
if (in->num_loops > 2)
|
||||
error("Only up to two loops are supported for AIFF/C");
|
||||
|
||||
size_t num_markers = 0;
|
||||
container_marker markers[4];
|
||||
static char *marker_names[4] = {
|
||||
"start",
|
||||
"end",
|
||||
"start",
|
||||
"end",
|
||||
};
|
||||
Loop sustainLoop = { 0 };
|
||||
Loop releaseLoop = { 0 };
|
||||
|
||||
for (size_t i = 0; i < in->num_loops; i++) {
|
||||
Loop *target;
|
||||
|
||||
switch (in->loops[i].id) {
|
||||
case 0:
|
||||
target = &sustainLoop;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
target = &releaseLoop;
|
||||
break;
|
||||
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
|
||||
unsigned type;
|
||||
switch (in->loops[i].type) {
|
||||
case LOOP_FORWARD:
|
||||
type = LOOP_PLAYMODE_FWD;
|
||||
break;
|
||||
|
||||
case LOOP_FORWARD_BACKWARD:
|
||||
type = LOOP_PLAYMODE_FWD_BWD;
|
||||
break;
|
||||
|
||||
default:
|
||||
error("Unsupported loop type in aiff/aifc");
|
||||
break;
|
||||
}
|
||||
|
||||
target->playMode = htobe16(type);
|
||||
|
||||
markers[num_markers].id = num_markers + 1;
|
||||
markers[num_markers].name = marker_names[num_markers];
|
||||
markers[num_markers].frame_number = in->loops[i].start;
|
||||
target->beginLoop = htobe16(1 + num_markers++);
|
||||
|
||||
markers[num_markers].id = num_markers + 1;
|
||||
markers[num_markers].name = marker_names[num_markers];
|
||||
markers[num_markers].frame_number = in->loops[i].end;
|
||||
target->endLoop = htobe16(1 + num_markers++);
|
||||
}
|
||||
|
||||
if (num_markers != 0) {
|
||||
CHUNK_BEGIN(out, "MARK", &chunk_start);
|
||||
|
||||
aiff_MARK mark = {
|
||||
.nMarkers = htobe16(num_markers),
|
||||
};
|
||||
CHUNK_WRITE(out, &mark);
|
||||
|
||||
for (size_t i = 0; i < num_markers; i++) {
|
||||
Marker marker = {
|
||||
.MarkerID = htobe16(markers[i].id),
|
||||
.positionH = htobe16(markers[i].frame_number >> 16),
|
||||
.positionL = htobe16(markers[i].frame_number),
|
||||
};
|
||||
CHUNK_WRITE(out, &marker);
|
||||
write_pstring(out, markers[i].name);
|
||||
}
|
||||
|
||||
CHUNK_END(out, chunk_start, htobe32);
|
||||
}
|
||||
|
||||
aiff_INST inst = {
|
||||
.baseNote = in->base_note,
|
||||
.detune = in->fine_tune,
|
||||
.lowNote = in->key_low,
|
||||
.highNote = in->key_hi,
|
||||
.lowVelocity = in->vel_low,
|
||||
.highVelocity = in->vel_hi,
|
||||
.gain = htobe16(in->gain),
|
||||
.sustainLoop = sustainLoop,
|
||||
.releaseLoop = releaseLoop,
|
||||
};
|
||||
CHUNK_BEGIN(out, "INST", &chunk_start);
|
||||
CHUNK_WRITE(out, &inst);
|
||||
CHUNK_END(out, chunk_start, htobe32);
|
||||
|
||||
if (aifc || matching) {
|
||||
// If we're writing an aifc, or we want to match on round-trip, emit an application-specific chunk for the
|
||||
// vadpcm codebook.
|
||||
if (in->vadpcm.has_book) {
|
||||
// APPL::stoc::VADPCMCODES
|
||||
CHUNK_BEGIN(out, "APPL", &chunk_start);
|
||||
CHUNK_WRITE_RAW(out, "stoc", 4);
|
||||
write_pstring(out, "VADPCMCODES");
|
||||
|
||||
int16_t version = htobe16(in->vadpcm.book_version);
|
||||
int16_t order = htobe16(in->vadpcm.book_header.order);
|
||||
int16_t npredictors = htobe16(in->vadpcm.book_header.npredictors);
|
||||
|
||||
CHUNK_WRITE(out, &version);
|
||||
CHUNK_WRITE(out, &order);
|
||||
CHUNK_WRITE(out, &npredictors);
|
||||
|
||||
size_t book_size = VADPCM_BOOK_SIZE(in->vadpcm.book_header.order, in->vadpcm.book_header.npredictors);
|
||||
|
||||
int16_t book_data_out[book_size];
|
||||
for (size_t i = 0; i < book_size; i++)
|
||||
book_data_out[i] = htobe16(in->vadpcm.book_data[i]);
|
||||
|
||||
CHUNK_WRITE_RAW(out, book_data_out, sizeof(int16_t) * book_size);
|
||||
|
||||
CHUNK_END(out, chunk_start, htobe32);
|
||||
}
|
||||
|
||||
// Only write a vadpcm loop structure for compressed output. Loop states match on round-trip so we need not
|
||||
// save them in uncompressed output.
|
||||
if (aifc && in->vadpcm.num_loops != 0) {
|
||||
// APPL::stoc::VADPCMLOOPS
|
||||
CHUNK_BEGIN(out, "APPL", &chunk_start);
|
||||
CHUNK_WRITE_RAW(out, "stoc", 4);
|
||||
write_pstring(out, "VADPCMLOOPS");
|
||||
|
||||
int16_t version = htobe16(in->vadpcm.loop_version);
|
||||
int16_t nloops = htobe16(in->vadpcm.num_loops);
|
||||
|
||||
CHUNK_WRITE(out, &version);
|
||||
CHUNK_WRITE(out, &nloops);
|
||||
|
||||
for (size_t i = 0; i < in->vadpcm.num_loops; i++) {
|
||||
ALADPCMloop outloop = in->vadpcm.loops[i];
|
||||
outloop.start = htobe32(outloop.start);
|
||||
outloop.end = htobe32(outloop.end);
|
||||
outloop.count = htobe32(outloop.count);
|
||||
for (size_t i = 0; i < ARRAY_COUNT(outloop.state); i++)
|
||||
outloop.state[i] = htobe16(outloop.state[i]);
|
||||
|
||||
CHUNK_WRITE(out, &outloop);
|
||||
}
|
||||
|
||||
CHUNK_END(out, chunk_start, htobe32);
|
||||
}
|
||||
}
|
||||
|
||||
if (in->data_type == SAMPLE_TYPE_PCM16) {
|
||||
assert(in->data_size % 2 == 0);
|
||||
assert(in->bit_depth == 16);
|
||||
|
||||
for (size_t i = 0; i < in->data_size / 2; i++) {
|
||||
((uint16_t *)in->data)[i] = htobe16(((uint16_t *)in->data)[i]);
|
||||
}
|
||||
}
|
||||
|
||||
aiff_SSND ssnd = {
|
||||
.offset = 0,
|
||||
.blockSize = 0,
|
||||
};
|
||||
CHUNK_BEGIN(out, "SSND", &chunk_start);
|
||||
CHUNK_WRITE(out, &ssnd);
|
||||
CHUNK_WRITE_RAW(out, in->data, in->data_size);
|
||||
CHUNK_END(out, chunk_start, htobe32);
|
||||
|
||||
uint32_t size = htobe32(ftell(out) - 8);
|
||||
fseek(out, 4, SEEK_SET);
|
||||
fwrite(&size, 4, 1, out);
|
||||
fclose(out);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
aifc_write(container_data *data, const char *path, bool matching)
|
||||
{
|
||||
return aiff_aifc_common_write(data, path, true, matching);
|
||||
}
|
||||
|
||||
int
|
||||
aiff_write(container_data *data, const char *path, bool matching)
|
||||
{
|
||||
return aiff_aifc_common_write(data, path, false, matching);
|
||||
}
|
28
tools/audio/sampleconv/src/container/aiff.h
Normal file
28
tools/audio/sampleconv/src/container/aiff.h
Normal file
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* 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/.
|
||||
*/
|
||||
#ifndef AIFF_H
|
||||
#define AIFF_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "../codec/vadpcm.h"
|
||||
#include "container.h"
|
||||
|
||||
int
|
||||
aifc_read(container_data *out, const char *path, bool matching);
|
||||
int
|
||||
aiff_read(container_data *out, const char *path, bool matching);
|
||||
|
||||
int
|
||||
aifc_write(container_data *in, const char *path, bool matching);
|
||||
int
|
||||
aiff_write(container_data *in, const char *path, bool matching);
|
||||
|
||||
#endif
|
25
tools/audio/sampleconv/src/container/container.c
Normal file
25
tools/audio/sampleconv/src/container/container.c
Normal file
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* 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 <stdlib.h>
|
||||
|
||||
#include "container.h"
|
||||
|
||||
int
|
||||
container_destroy(container_data *data)
|
||||
{
|
||||
if (data->data != NULL)
|
||||
free(data->data);
|
||||
if (data->loops != NULL)
|
||||
free(data->loops);
|
||||
if (data->vadpcm.book_data != NULL)
|
||||
free(data->vadpcm.book_data);
|
||||
if (data->vadpcm.loops != NULL)
|
||||
free(data->vadpcm.loops);
|
||||
return 0;
|
||||
}
|
99
tools/audio/sampleconv/src/container/container.h
Normal file
99
tools/audio/sampleconv/src/container/container.h
Normal file
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* 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/.
|
||||
*/
|
||||
#ifndef CONTAINER_H
|
||||
#define CONTAINER_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "../codec/vadpcm.h"
|
||||
|
||||
typedef enum {
|
||||
// Uncompressed
|
||||
SAMPLE_TYPE_PCM16,
|
||||
SAMPLE_TYPE_PCM8,
|
||||
|
||||
// Compressed
|
||||
SAMPLE_TYPE_VADPCM,
|
||||
SAMPLE_TYPE_VADPCM_HALF
|
||||
} sample_data_type;
|
||||
|
||||
typedef struct {
|
||||
const char *fext;
|
||||
int (*read)(struct container_data *out, const char *path, bool matching);
|
||||
int (*write)(struct container_data *out, const char *path, bool matching);
|
||||
} container_spec;
|
||||
|
||||
typedef enum {
|
||||
LOOP_NONE = 0,
|
||||
LOOP_FORWARD = 1,
|
||||
LOOP_FORWARD_BACKWARD = 2,
|
||||
LOOP_BACKWARD = 3
|
||||
} loop_type;
|
||||
|
||||
typedef struct {
|
||||
uint16_t id;
|
||||
loop_type type;
|
||||
uint32_t start;
|
||||
uint32_t end;
|
||||
uint32_t fraction;
|
||||
uint32_t num;
|
||||
} container_loop;
|
||||
|
||||
typedef struct {
|
||||
uint16_t id;
|
||||
uint32_t frame_number;
|
||||
char *name;
|
||||
} container_marker;
|
||||
|
||||
typedef struct container_data {
|
||||
// Sample details
|
||||
double sample_rate;
|
||||
unsigned int num_channels;
|
||||
uint32_t byte_rate;
|
||||
uint16_t block_align;
|
||||
uint16_t bit_depth; // also called sample_size
|
||||
uint32_t num_samples; // also apparently called num_frames? but that's wrong
|
||||
|
||||
// Instrument details
|
||||
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;
|
||||
|
||||
// Sample data, possibly compressed
|
||||
sample_data_type data_type;
|
||||
void *data;
|
||||
size_t data_size;
|
||||
|
||||
unsigned num_loops;
|
||||
container_loop *loops;
|
||||
|
||||
// VADPCM-specific data
|
||||
struct {
|
||||
// VADPCM codebook
|
||||
int16_t book_version;
|
||||
ALADPCMbookhead book_header;
|
||||
int16_t *book_data;
|
||||
bool has_book;
|
||||
|
||||
// VADPCM loop data
|
||||
int16_t loop_version;
|
||||
unsigned num_loops;
|
||||
ALADPCMloop *loops;
|
||||
} vadpcm;
|
||||
} container_data;
|
||||
|
||||
int
|
||||
container_destroy(container_data *data);
|
||||
|
||||
#endif
|
552
tools/audio/sampleconv/src/container/wav.c
Normal file
552
tools/audio/sampleconv/src/container/wav.c
Normal file
|
@ -0,0 +1,552 @@
|
|||
/**
|
||||
* 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;
|
||||
}
|
20
tools/audio/sampleconv/src/container/wav.h
Normal file
20
tools/audio/sampleconv/src/container/wav.h
Normal file
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* 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/.
|
||||
*/
|
||||
#ifndef WAV_H
|
||||
#define WAV_H
|
||||
|
||||
#include "../codec/vadpcm.h"
|
||||
#include "container.h"
|
||||
|
||||
int
|
||||
wav_write(container_data *in, const char *path, bool matching);
|
||||
int
|
||||
wav_read(container_data *out, const char *path, bool matching);
|
||||
|
||||
#endif
|
Loading…
Add table
Add a link
Reference in a new issue