/** * 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 #include #include #include #include #include #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", 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", 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); }