/** * 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 "aifc.h" #include "util.h" #define FREAD(file, data, size) \ do { \ if (fread((data), (size), 1, (file)) != 1) { \ error("[%s:%d] Could not read %lu bytes from file", __FILE__, __LINE__, (size_t)(size)); \ } \ } while (0) #define VADPCM_VER ((int16_t)1) #if 0 #define DEBUGF(fmt, ...) printf(fmt, ##__VA_ARGS__) #else #define DEBUGF(fmt, ...) (void)0 #endif 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_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 UNUSED 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; } 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(len + 1); // 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; } void aifc_read(aifc_data *af, const char *path, uint8_t *match_buf, size_t *match_buf_pos) { FILE *in; bool has_comm = false; bool has_ssnd = false; memset(af, 0, sizeof(aifc_data)); DEBUGF("[aifc] path [%s]\n", path); if (path == NULL) return; 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); DEBUGF("total size = 0x%X\n", size); if (!CC4_CHECK(form, "FORM") || !CC4_CHECK(aifc, "AIFC")) error("Not an aifc file?"); af->path = path; while (true) { char cc4[4]; uint32_t chunk_size; long start = ftell(in); if (start > 8 + size) { error("Overran file"); } if (start == 8 + size) { break; } FREAD(in, cc4, 4); FREAD(in, &chunk_size, 4); chunk_size = be32toh(chunk_size); chunk_size++; chunk_size &= ~1; DEBUGF("%c%c%c%c\n", cc4[0], cc4[1], cc4[2], cc4[3]); 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); assert(comm.numChannels == 1); // mono assert(comm.sampleSize == 16); // 16-bit samples af->num_channels = comm.numChannels; af->sample_size = comm.sampleSize; af->num_frames = (comm.numFramesH << 16) | comm.numFramesL; f80_to_f64(&af->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); } af->compression_type = comp_type; af->compression_name = NULL; if (chunk_size > sizeof(aiff_COMM) + 4) { af->compression_name = read_pstring_alloc(in); } DEBUGF(" numChannels %d\n" " numFrames %u\n" " sampleSize %d\n" " sampleRate %f\n" " compressionType %c%c%c%c (%s)\n", af->num_channels, af->num_frames, af->sample_size, af->sample_rate, af->compression_type >> 24, af->compression_type >> 16, af->compression_type >> 8, af->compression_type, af->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); // basenote DEBUGF(" baseNote = %d (%d)\n" " detune = %d\n" " lowNote = %d\n" " highNote = %d\n" " lowVelocity = %d\n" " highVelocity= %d\n" " gain = %d\n" " sustainLoop = %d [%d:%d]\n" " releaseLoop = %d [%d:%d]\n", inst.baseNote, NOTE_MIDI_TO_Z64(inst.baseNote), inst.detune, inst.lowNote, inst.highNote, inst.lowVelocity, inst.highVelocity, inst.gain, inst.sustainLoop.playMode, inst.sustainLoop.beginLoop, inst.sustainLoop.endLoop, inst.releaseLoop.playMode, inst.releaseLoop.beginLoop, inst.releaseLoop.endLoop); af->basenote = inst.baseNote; af->detune = inst.detune; af->has_inst = true; } break; case CC4('M', 'A', 'R', 'K'): { aiff_MARK mark; FREAD(in, &mark, sizeof(mark)); mark.nMarkers = be16toh(mark.nMarkers); af->num_markers = mark.nMarkers; af->markers = malloc(mark.nMarkers * sizeof(aifc_marker)); for (size_t i = 0; i < mark.nMarkers; i++) { Marker marker; FREAD(in, &marker, sizeof(marker)); marker.MarkerID = be16toh(marker.MarkerID); marker.positionH = be16toh(marker.positionH); marker.positionL = be16toh(marker.positionL); (*af->markers)[i].id = marker.MarkerID; (*af->markers)[i].pos = (marker.positionH << 16) | marker.positionL; (*af->markers)[i].label = read_pstring_alloc(in); DEBUGF(" MARKER: %d @ %u [%s]\n", (*af->markers)[i].id, (*af->markers)[i].pos, (*af->markers)[i].label); } } break; case CC4('A', 'P', 'P', 'L'): { char subcc4[4]; FREAD(in, subcc4, 4); DEBUGF(" %c%c%c%c\n", subcc4[0], subcc4[1], subcc4[2], subcc4[3]); 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); DEBUGF(" %s\n", 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); if (version != VADPCM_VER) error("Non-identical codebook chunk versions"); size_t book_size = 8 * order * npredictors; af->book.order = order; af->book.npredictors = npredictors; af->book_state = malloc(book_size * sizeof(int16_t)); FREAD(in, af->book_state, book_size * sizeof(int16_t)); for (size_t i = 0; i < book_size; i++) (*af->book_state)[i] = be16toh((*af->book_state)[i]); af->has_book = true; // DEBUG DEBUGF(" order = %d\n" " npredictors = %d\n", af->book.order, af->book.npredictors); for (size_t i = 0; i < book_size; i++) { if (i % 8 == 0) DEBUGF("\n "); DEBUGF("%04X ", (uint16_t)(*af->book_state)[i]); } DEBUGF("\n"); } 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); if (version != VADPCM_VER) error("Non-identical loop chunk versions"); if (nloops != 1) error("Only one loop is supported, got %d", nloops); FREAD(in, &af->loop, sizeof(ALADPCMloop)); af->loop.start = be32toh(af->loop.start); af->loop.end = be32toh(af->loop.end); af->loop.count = be32toh(af->loop.count); for (size_t i = 0; i < ARRAY_COUNT(af->loop.state); i++) af->loop.state[i] = be16toh(af->loop.state[i]); af->has_loop = true; // DEBUG DEBUGF(" start = %d\n" " end = %d\n" " count = %d\n", af->loop.start, af->loop.end, af->loop.count); for (size_t i = 0; i < ARRAY_COUNT(af->loop.state); i++) { if (i % 8 == 0) DEBUGF("\n "); DEBUGF("%04X ", (uint16_t)af->loop.state[i]); } DEBUGF("\n"); } 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); assert(ssnd.offset == 0); assert(ssnd.blockSize == 0); af->ssnd_offset = ftell(in); // TODO use numFrames instead? af->ssnd_size = chunk_size - sizeof(ssnd); // Skip reading the rest of the chunk fseek(in, af->ssnd_size, SEEK_CUR); DEBUGF(" offset = 0x%lX size = 0x%lX\n", af->ssnd_offset, af->ssnd_size); has_ssnd = true; } break; default: // skip it 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_ssnd) error("aiff/aifc has no SSND chunk"); // replicate buffer bug in original tool if (match_buf != NULL && match_buf_pos != NULL) { size_t buf_pos = ALIGN16(*match_buf_pos) % BUG_BUF_SIZE; size_t rem = af->ssnd_size; long seek_offset = 0; if (rem > BUG_BUF_SIZE) { // The sample is so large that it will cover the buffer more than once, let's only read as much as we // need to. // Advance to the buffer position to read only the final data into buf_pos = (buf_pos + rem - BUG_BUF_SIZE) % BUG_BUF_SIZE; // We need to seek to the actual data in the file that would be read at this point seek_offset = rem - BUG_BUF_SIZE; // The remaining data to read is just 1 buffer's worth of data rem = BUG_BUF_SIZE; } fseek(in, af->ssnd_offset + seek_offset, SEEK_SET); if (rem > BUG_BUF_SIZE - buf_pos) { // rem will circle around in the buffer // Fill up to the end of the buffer FREAD(in, &match_buf[buf_pos], BUG_BUF_SIZE - buf_pos); rem -= BUG_BUF_SIZE - buf_pos; // Return to the start of the buffer buf_pos = 0; } // rem fits in the buffer without circling back, fill buffer FREAD(in, &match_buf[buf_pos], rem); *match_buf_pos = (buf_pos + rem) % BUG_BUF_SIZE; } fclose(in); } void aifc_dispose(aifc_data *af) { free(af->book_state); af->has_book = false; af->has_loop = false; free(af->compression_name); for (size_t i = 0; i < af->num_markers; i++) free((*af->markers)[i].label); free(af->markers); }