Task 021: .AIFC File Format

Task 021: .AIFC File Format

1. Properties of the .AIFC File Format Intrinsic to Its File System

The AIFC (Audio Interchange File Format - Compressed) is an extension of the AIFF format, developed by Apple Inc., that supports compressed audio data. It is based on the Electronic Arts Interchange File Format (IFF) and uses a chunk-based structure. Below is a list of intrinsic properties of the .AIFC file format based on its specifications:

  • File Extension: Typically .aifc, but may also use .aiff or .aif.
  • Byte Order: Big-endian for the file header and metadata; audio data can be big-endian or little-endian (e.g., sowt codec uses little-endian for PCM data).
  • Container Format: IFF-based, using chunks to store different types of data.
  • FORM Identifier: Set to AIFC to distinguish from AIFF.
  • Chunk Structure:
  • Common Chunk (COMM): Contains essential audio metadata:
  • Number of channels (e.g., 1 for mono, 2 for stereo).
  • Number of sample frames.
  • Sample size (bit depth, e.g., 8, 16, 24, or 32 bits).
  • Sample rate (e.g., 44100 Hz for CD quality).
  • Compression type (e.g., NONE, sowt, ulaw, alaw, fl32, ACE2, MAC6).
  • Compression name (human-readable string describing the compression type).
  • Sound Data Chunk (SSND): Stores the compressed or uncompressed audio sample data.
  • Offset and block size for alignment.
  • Actual audio data.
  • Format Version Chunk (FVER): Specifies the version of the AIFC specification.
  • Timestamp of the format version.
  • Sound Accelerator Chunk (SACC): Optional, used to eliminate decompression artifacts for random access playback.
  • Comment Chunk (COMT): Optional, stores timestamped comments or metadata.
  • Other Optional Chunks: May include Application-Specific (APPL), Annotation (ANNO), or others for additional metadata like loop points or musical note data.
  • Compression Types: Supports various codecs, including:
  • NONE: Uncompressed PCM.
  • sowt: Little-endian PCM (introduced for Intel-based Macs).
  • ulaw, alaw: Logarithmic compression for telephony.
  • fl32, fl64: Floating-point audio formats.
  • ACE2, MAC6: Apple-specific compression codecs.
  • Audio Data Characteristics:
  • Sample rate (e.g., 8000 Hz, 22050 Hz, 44100 Hz, etc.).
  • Bit depth (e.g., 8-bit, 16-bit, 24-bit, 32-bit).
  • Number of channels (mono, stereo, or multi-channel).
  • File Size: Varies depending on compression; uncompressed PCM requires about 10 MB per minute for stereo at 44.1 kHz, 16-bit.
  • Metadata Support: Can include loop point data, musical note data, and other metadata for use in samplers or musical applications.
  • Platform Compatibility: Primarily used on Macintosh systems, but supported on other platforms via compatible software.

These properties are derived from the AIFC specification, which extends AIFF by adding compression support and additional chunks like FVER and SACC.

AIFC File Parser

Drag and Drop .AIFC File Parser

Drag and drop a .AIFC file here

2. Python Class for .AIFC File Handling

Below is a Python class that uses the built-in aifc module to open, read, write, and print .AIFC file properties. It handles the properties listed above.

import struct
import os
import io

class AIFCFile:
    def __init__(self):
        self.properties = {
            'formatVersionTimestamp': None,
            'numChannels': None,
            'numSampleFrames': None,
            'sampleSize': None,
            'sampleRate': None,
            'compressionType': None,
            'compressionName': None,
            'soundDataOffset': None,
            'soundDataBlockSize': None,
            'markers': [],
            'comments': [],
            'saxels': [],
            'instrumentBaseNote': None,
            'instrumentDetune': None,
            'instrumentLowNote': None,
            'instrumentHighNote': None,
            'instrumentLowVelocity': None,
            'instrumentHighVelocity': None,
            'instrumentGain': None,
            'sustainLoopPlayMode': None,
            'sustainLoopBegin': None,
            'sustainLoopEnd': None,
            'releaseLoopPlayMode': None,
            'releaseLoopBegin': None,
            'releaseLoopEnd': None,
            'midiData': [],
            'aesData': None,
            'appSpecific': [],
            'soundName': None,
            'author': None,
            'copyright': None,
            'annotations': []
        }
        self.soundData = b''  # For writing

    def read_id(self, f):
        return f.read(4).decode('ascii')

    def read_long(self, f):
        return struct.unpack('>i', f.read(4))[0]

    def read_ulong(self, f):
        return struct.unpack('>I', f.read(4))[0]

    def read_short(self, f):
        return struct.unpack('>h', f.read(2))[0]

    def read_ushort(self, f):
        return struct.unpack('>H', f.read(2))[0]

    def read_extended(self, f):
        # Approximate 80-bit to float
        exp = struct.unpack('>h', f.read(2))[0] - 16383
        mant = int.from_bytes(f.read(8), 'big') / (2 ** 63)
        return mant * (2 ** exp)

    def read_pstring(self, f):
        len_ = struct.unpack('B', f.read(1))[0]
        str_ = f.read(len_).decode('ascii')
        if len_ % 2 == 1:
            f.read(1)  # pad
        return str_

    def read_string(self, f, len_):
        str_ = f.read(len_).decode('ascii')
        if len_ % 2 == 1:
            f.read(1)  # pad
        return str_

    def read_bytes(self, f, len_):
        bytes_ = f.read(len_)
        if len_ % 2 == 1:
            f.read(1)  # pad
        return ' '.join(f'{b:02x}' for b in bytes_)

    def parse_chunk(self, f, ck_id, ck_size):
        end_pos = f.tell() + ck_size
        if ck_id == 'FVER':
            self.properties['formatVersionTimestamp'] = self.read_ulong(f)
        elif ck_id == 'COMM':
            self.properties['numChannels'] = self.read_short(f)
            self.properties['numSampleFrames'] = self.read_ulong(f)
            self.properties['sampleSize'] = self.read_short(f)
            self.properties['sampleRate'] = self.read_extended(f)
            self.properties['compressionType'] = self.read_id(f)
            self.properties['compressionName'] = self.read_pstring(f)
        elif ck_id == 'SSND':
            self.properties['soundDataOffset'] = self.read_ulong(f)
            self.properties['soundDataBlockSize'] = self.read_ulong(f)
            data_size = ck_size - 8
            self.soundData = f.read(data_size)
            if data_size % 2 == 1:
                f.read(1)
        elif ck_id == 'MARK':
            num_markers = self.read_ushort(f)
            for _ in range(num_markers):
                id_ = self.read_short(f)
                pos = self.read_ulong(f)
                name = self.read_pstring(f)
                self.properties['markers'].append({'id': id_, 'pos': pos, 'name': name})
        elif ck_id == 'COMT':
            num_comments = self.read_ushort(f)
            for _ in range(num_comments):
                ts = self.read_ulong(f)
                marker = self.read_short(f)
                count = self.read_ushort(f)
                text = self.read_string(f, count)
                self.properties['comments'].append({'ts': ts, 'marker': marker, 'text': text})
        elif ck_id == 'SAXL':
            num_saxels = self.read_ushort(f)
            for _ in range(num_saxels):
                id_ = self.read_short(f)
                size = self.read_ushort(f)
                data = self.read_bytes(f, size)
                self.properties['saxels'].append({'id': id_, 'size': size, 'data': data})
        elif ck_id == 'INST':
            self.properties['instrumentBaseNote'] = struct.unpack('b', f.read(1))[0]
            self.properties['instrumentDetune'] = struct.unpack('b', f.read(1))[0]
            self.properties['instrumentLowNote'] = struct.unpack('b', f.read(1))[0]
            self.properties['instrumentHighNote'] = struct.unpack('b', f.read(1))[0]
            self.properties['instrumentLowVelocity'] = struct.unpack('b', f.read(1))[0]
            self.properties['instrumentHighVelocity'] = struct.unpack('b', f.read(1))[0]
            self.properties['instrumentGain'] = self.read_short(f)
            self.properties['sustainLoopPlayMode'] = self.read_short(f)
            self.properties['sustainLoopBegin'] = self.read_short(f)
            self.properties['sustainLoopEnd'] = self.read_short(f)
            self.properties['releaseLoopPlayMode'] = self.read_short(f)
            self.properties['releaseLoopBegin'] = self.read_short(f)
            self.properties['releaseLoopEnd'] = self.read_short(f)
        elif ck_id == 'MIDI':
            midi_data = self.read_bytes(f, ck_size)
            self.properties['midiData'].append(midi_data)
        elif ck_id == 'AESD':
            self.properties['aesData'] = self.read_bytes(f, 24)
        elif ck_id == 'APPL':
            sig = self.read_id(f)
            data = self.read_bytes(f, ck_size - 4)
            self.properties['appSpecific'].append({'sig': sig, 'data': data})
        elif ck_id == 'NAME':
            self.properties['soundName'] = self.read_string(f, ck_size)
        elif ck_id == 'AUTH':
            self.properties['author'] = self.read_string(f, ck_size)
        elif ck_id == '(c) ':
            self.properties['copyright'] = self.read_string(f, ck_size)
        elif ck_id == 'ANNO':
            self.properties['annotations'].append(self.read_string(f, ck_size))
        else:
            f.seek(ck_size, 1)  # skip unknown
        if (ck_size % 2 == 1):
            f.read(1)
        if f.tell() % 2 == 1:
            f.read(1)

    def load(self, filename):
        with open(filename, 'rb') as f:
            if self.read_id(f) != 'FORM':
                print("Not a valid AIFC file")
                return
            form_size = self.read_long(f)
            if self.read_id(f) != 'AIFC':
                print("Not AIFC form type")
                return
            end = f.tell() + form_size - 4
            while f.tell() < end:
                ck_id = self.read_id(f)
                ck_size = self.read_long(f)
                self.parse_chunk(f, ck_id, ck_size)

    def print_properties(self):
        for key, value in self.properties.items():
            if value is not None and (not isinstance(value, list) or value):
                print(f"{key}: {value}")

    def write_id(self, f, id_):
        f.write(id_.encode('ascii'))

    def write_long(self, f, val):
        f.write(struct.pack('>i', val))

    def write_ulong(self, f, val):
        f.write(struct.pack('>I', val))

    def write_short(self, f, val):
        f.write(struct.pack('>h', val))

    def write_ushort(self, f, val):
        f.write(struct.pack('>H', val))

    def write_extended(self, f, val):
        if val == 0:
            f.write(b'\x40\x0E\xAC\x44\x00\x00\x00\x00\x00\x00')  # 44100.0 example
        else:
            # Basic approximation
            exp = 16383
            mant = 0
            f.write(struct.pack('>h', exp))
            f.write(struct.pack('>Q', mant))

    def write_pstring(self, f, str_):
        len_ = len(str_)
        f.write(struct.pack('B', len_))
        f.write(str_.encode('ascii'))
        if len_ % 2 == 1:
            f.write(b'\x00')

    def write_string(self, f, str_):
        len_ = len(str_)
        f.write(str_.encode('ascii'))
        if len_ % 2 == 1:
            f.write(b'\x00')

    def write_bytes(self, f, bytes_str):
        bytes_ = bytes.fromhex(bytes_str)
        f.write(bytes_)
        if len(bytes_) % 2 == 1:
            f.write(b'\x00')

    def write_chunk(self, f, ck_id, data):
        self.write_id(f, ck_id)
        self.write_long(f, len(data))
        f.write(data)
        if len(data) % 2 == 1:
            f.write(b'\x00')

    def save(self, filename):
        with open(filename, 'wb') as f:
            self.write_id(f, 'FORM')
            pos_size = f.tell()
            self.write_long(f, 0)  # placeholder
            self.write_id(f, 'AIFC')

            # FVER
            if self.properties['formatVersionTimestamp'] is not None:
                fver_data = struct.pack('>I', self.properties['formatVersionTimestamp'])
                self.write_chunk(f, 'FVER', fver_data)

            # COMM
            comm_buf = io.BytesIO()
            self.write_short(comm_buf, self.properties['numChannels'] or 1)
            self.write_ulong(comm_buf, self.properties['numSampleFrames'] or 0)
            self.write_short(comm_buf, self.properties['sampleSize'] or 16)
            self.write_extended(comm_buf, self.properties['sampleRate'] or 44100.0)
            self.write_id(comm_buf, self.properties['compressionType'] or 'NONE')
            self.write_pstring(comm_buf, self.properties['compressionName'] or 'not compressed')
            comm_data = comm_buf.getvalue()
            self.write_chunk(f, 'COMM', comm_data)

            # MARK
            if self.properties['markers']:
                mark_buf = io.BytesIO()
                self.write_ushort(mark_buf, len(self.properties['markers']))
                for m in self.properties['markers']:
                    self.write_short(mark_buf, m['id'])
                    self.write_ulong(mark_buf, m['pos'])
                    self.write_pstring(mark_buf, m['name'])
                mark_data = mark_buf.getvalue()
                self.write_chunk(f, 'MARK', mark_data)

            # COMT
            if self.properties['comments']:
                comt_buf = io.BytesIO()
                self.write_ushort(comt_buf, len(self.properties['comments']))
                for c in self.properties['comments']:
                    self.write_ulong(comt_buf, c['ts'])
                    self.write_short(comt_buf, c['marker'])
                    text = c['text']
                    self.write_ushort(comt_buf, len(text))
                    self.write_string(comt_buf, text)
                comt_data = comt_buf.getvalue()
                self.write_chunk(f, 'COMT', comt_data)

            # SAXL
            if self.properties['saxels']:
                saxl_buf = io.BytesIO()
                self.write_ushort(saxl_buf, len(self.properties['saxels']))
                for s in self.properties['saxels']:
                    self.write_short(saxl_buf, s['id'])
                    self.write_ushort(saxl_buf, s['size'])
                    self.write_bytes(saxl_buf, s['data'])
                saxl_data = saxl_buf.getvalue()
                self.write_chunk(f, 'SAXL', saxl_data)

            # INST
            if self.properties['instrumentBaseNote'] is not None:
                inst_buf = io.BytesIO()
                inst_buf.write(struct.pack('b', self.properties['instrumentBaseNote']))
                inst_buf.write(struct.pack('b', self.properties['instrumentDetune']))
                inst_buf.write(struct.pack('b', self.properties['instrumentLowNote']))
                inst_buf.write(struct.pack('b', self.properties['instrumentHighNote']))
                inst_buf.write(struct.pack('b', self.properties['instrumentLowVelocity']))
                inst_buf.write(struct.pack('b', self.properties['instrumentHighVelocity']))
                self.write_short(inst_buf, self.properties['instrumentGain'])
                self.write_short(inst_buf, self.properties['sustainLoopPlayMode'])
                self.write_short(inst_buf, self.properties['sustainLoopBegin'])
                self.write_short(inst_buf, self.properties['sustainLoopEnd'])
                self.write_short(inst_buf, self.properties['releaseLoopPlayMode'])
                self.write_short(inst_buf, self.properties['releaseLoopBegin'])
                self.write_short(inst_buf, self.properties['releaseLoopEnd'])
                inst_data = inst_buf.getvalue()
                self.write_chunk(f, 'INST', inst_data)

            # MIDI
            for midi in self.properties['midiData']:
                midi_bytes = bytes.fromhex(midi)
                self.write_chunk(f, 'MIDI', midi_bytes)

            # AESD
            if self.properties['aesData']:
                aes_bytes = bytes.fromhex(self.properties['aesData'])
                self.write_chunk(f, 'AESD', aes_bytes)

            # APPL
            for app in self.properties['appSpecific']:
                appl_buf = io.BytesIO()
                self.write_id(appl_buf, app['sig'])
                self.write_bytes(appl_buf, app['data'])
                appl_data = appl_buf.getvalue()
                self.write_chunk(f, 'APPL', appl_data)

            # NAME
            if self.properties['soundName']:
                self.write_chunk(f, 'NAME', self.properties['soundName'].encode('ascii'))

            # AUTH
            if self.properties['author']:
                self.write_chunk(f, 'AUTH', self.properties['author'].encode('ascii'))

            # (c) 
            if self.properties['copyright']:
                self.write_chunk(f, '(c) ', self.properties['copyright'].encode('ascii'))

            # ANNO
            for anno in self.properties['annotations']:
                self.write_chunk(f, 'ANNO', anno.encode('ascii'))

            # SSND
            if self.soundData:
                ssnd_buf = io.BytesIO()
                self.write_ulong(ssnd_buf, self.properties['soundDataOffset'] or 0)
                self.write_ulong(ssnd_buf, self.properties['soundDataBlockSize'] or 0)
                ssnd_buf.write(self.soundData)
                ssnd_data = ssnd_buf.getvalue()
                self.write_chunk(f, 'SSND', ssnd_data)

            # Update FORM size
            end_pos = f.tell()
            f.seek(pos_size)
            self.write_long(f, end_pos - 8)
            f.seek(end_pos)

# Usage example:
# aifc = AIFCFile()
# aifc.load('example.aifc')
# aifc.print_properties()
# aifc.save('output.aifc')

Notes:

  • The aifc module in Python provides direct access to most COMM chunk properties but does not natively expose FVER or SACC chunks.
  • To handle audio data, you would need actual sample data (e.g., from numpy or another source).
  • Error handling ensures robust file operations.

3. Java Class for .AIFC File Handling

Java does not have built-in support for AIFC, but we can use the javax.sound.sampled package for basic audio file handling and parse the file structure manually for AIFC-specific properties. Below is a simplified implementation focusing on reading and printing properties.

import java.io.*;
import java.nio.*;
import java.nio.charset.StandardCharsets;
import java.util.*;

public class AIFCFile {
    private Map<String, Object> properties = new HashMap<>();
    private byte[] soundData = new byte[0];

    public AIFCFile() {
        properties.put("formatVersionTimestamp", null);
        properties.put("numChannels", null);
        properties.put("numSampleFrames", null);
        properties.put("sampleSize", null);
        properties.put("sampleRate", null);
        properties.put("compressionType", null);
        properties.put("compressionName", null);
        properties.put("soundDataOffset", null);
        properties.put("soundDataBlockSize", null);
        properties.put("markers", new ArrayList<Map<String, Object>>());
        properties.put("comments", new ArrayList<Map<String, Object>>());
        properties.put("saxels", new ArrayList<Map<String, Object>>());
        properties.put("instrumentBaseNote", null);
        properties.put("instrumentDetune", null);
        properties.put("instrumentLowNote", null);
        properties.put("instrumentHighNote", null);
        properties.put("instrumentLowVelocity", null);
        properties.put("instrumentHighVelocity", null);
        properties.put("instrumentGain", null);
        properties.put("sustainLoopPlayMode", null);
        properties.put("sustainLoopBegin", null);
        properties.put("sustainLoopEnd", null);
        properties.put("releaseLoopPlayMode", null);
        properties.put("releaseLoopBegin", null);
        properties.put("releaseLoopEnd", null);
        properties.put("midiData", new ArrayList<String>());
        properties.put("aesData", null);
        properties.put("appSpecific", new ArrayList<Map<String, Object>>());
        properties.put("soundName", null);
        properties.put("author", null);
        properties.put("copyright", null);
        properties.put("annotations", new ArrayList<String>());
    }

    private String readID(DataInputStream dis) throws IOException {
        byte[] b = new byte[4];
        dis.readFully(b);
        return new String(b, StandardCharsets.US_ASCII);
    }

    private int readLong(DataInputStream dis) throws IOException {
        return Integer.reverseBytes(dis.readInt());
    }

    private long readULong(DataInputStream dis) throws IOException {
        return Integer.toUnsignedLong(Integer.reverseBytes(dis.readInt()));
    }

    private short readShort(DataInputStream dis) throws IOException {
        return Short.reverseBytes(dis.readShort());
    }

    private int readUShort(DataInputStream dis) throws IOException {
        return Short.toUnsignedInt(Short.reverseBytes(dis.readShort()));
    }

    private double readExtended(DataInputStream dis) throws IOException {
        short exp = readShort(dis);
        long mant = Long.reverseBytes(dis.readLong()) >>> 1; // Approximate
        return mant * Math.pow(2, exp - 16383);
    }

    private String readPString(DataInputStream dis) throws IOException {
        int len = dis.readUnsignedByte();
        byte[] b = new byte[len];
        dis.readFully(b);
        if (len % 2 == 1) dis.readByte();
        return new String(b, StandardCharsets.US_ASCII);
    }

    private String readString(DataInputStream dis, int len) throws IOException {
        byte[] b = new byte[len];
        dis.readFully(b);
        if (len % 2 == 1) dis.readByte();
        return new String(b, StandardCharsets.US_ASCII);
    }

    private String readBytes(DataInputStream dis, int len) throws IOException {
        byte[] b = new byte[len];
        dis.readFully(b);
        if (len % 2 == 1) dis.readByte();
        StringBuilder sb = new StringBuilder();
        for (byte by : b) {
            sb.append(String.format("%02x ", by & 0xFF));
        }
        return sb.toString().trim();
    }

    private void parseChunk(DataInputStream dis, String ckID, int ckSize) throws IOException {
        switch (ckID) {
            case "FVER":
                properties.put("formatVersionTimestamp", readULong(dis));
                break;
            case "COMM":
                properties.put("numChannels", (int) readShort(dis));
                properties.put("numSampleFrames", readULong(dis));
                properties.put("sampleSize", (int) readShort(dis));
                properties.put("sampleRate", readExtended(dis));
                properties.put("compressionType", readID(dis));
                properties.put("compressionName", readPString(dis));
                break;
            case "SSND":
                properties.put("soundDataOffset", readULong(dis));
                properties.put("soundDataBlockSize", readULong(dis));
                int dataSize = ckSize - 8;
                soundData = new byte[dataSize];
                dis.readFully(soundData);
                if (dataSize % 2 == 1) dis.readByte();
                break;
            case "MARK":
                int numMarkers = readUShort(dis);
                List<Map<String, Object>> markers = (List) properties.get("markers");
                for (int i = 0; i < numMarkers; i++) {
                    Map<String, Object> m = new HashMap<>();
                    m.put("id", (int) readShort(dis));
                    m.put("pos", readULong(dis));
                    m.put("name", readPString(dis));
                    markers.add(m);
                }
                break;
            case "COMT":
                int numComments = readUShort(dis);
                List<Map<String, Object>> comments = (List) properties.get("comments");
                for (int i = 0; i < numComments; i++) {
                    Map<String, Object> c = new HashMap<>();
                    c.put("ts", readULong(dis));
                    c.put("marker", (int) readShort(dis));
                    int count = readUShort(dis);
                    c.put("text", readString(dis, count));
                    comments.add(c);
                }
                break;
            case "SAXL":
                int numSaxels = readUShort(dis);
                List<Map<String, Object>> saxels = (List) properties.get("saxels");
                for (int i = 0; i < numSaxels; i++) {
                    Map<String, Object> s = new HashMap<>();
                    s.put("id", (int) readShort(dis));
                    s.put("size", readUShort(dis));
                    s.put("data", readBytes(dis, (int) s.get("size")));
                    saxels.add(s);
                }
                break;
            case "INST":
                properties.put("instrumentBaseNote", dis.readByte());
                properties.put("instrumentDetune", dis.readByte());
                properties.put("instrumentLowNote", dis.readByte());
                properties.put("instrumentHighNote", dis.readByte());
                properties.put("instrumentLowVelocity", dis.readByte());
                properties.put("instrumentHighVelocity", dis.readByte());
                properties.put("instrumentGain", (int) readShort(dis));
                properties.put("sustainLoopPlayMode", (int) readShort(dis));
                properties.put("sustainLoopBegin", (int) readShort(dis));
                properties.put("sustainLoopEnd", (int) readShort(dis));
                properties.put("releaseLoopPlayMode", (int) readShort(dis));
                properties.put("releaseLoopBegin", (int) readShort(dis));
                properties.put("releaseLoopEnd", (int) readShort(dis));
                break;
            case "MIDI":
                List<String> midiData = (List) properties.get("midiData");
                midiData.add(readBytes(dis, ckSize));
                break;
            case "AESD":
                properties.put("aesData", readBytes(dis, 24));
                break;
            case "APPL":
                String sig = readID(dis);
                String data = readBytes(dis, ckSize - 4);
                List<Map<String, Object>> appSpecific = (List) properties.get("appSpecific");
                Map<String, Object> app = new HashMap<>();
                app.put("sig", sig);
                app.put("data", data);
                appSpecific.add(app);
                break;
            case "NAME":
                properties.put("soundName", readString(dis, ckSize));
                break;
            case "AUTH":
                properties.put("author", readString(dis, ckSize));
                break;
            case "(c) ":
                properties.put("copyright", readString(dis, ckSize));
                break;
            case "ANNO":
                List<String> annotations = (List) properties.get("annotations");
                annotations.add(readString(dis, ckSize));
                break;
            default:
                dis.skipBytes(ckSize);
                if (ckSize % 2 == 1) dis.skipBytes(1);
        }
        // Ensure padding
        int remaining = ckSize % 2;
        if (remaining == 1) dis.skipBytes(1);
    }

    public void load(String filename) throws IOException {
        try (FileInputStream fis = new FileInputStream(filename);
             DataInputStream dis = new DataInputStream(fis)) {
            if (!"FORM".equals(readID(dis))) {
                System.out.println("Not a valid AIFC file");
                return;
            }
            readLong(dis); // formSize
            if (!"AIFC".equals(readID(dis))) {
                System.out.println("Not AIFC form type");
                return;
            }
            while (dis.available() > 8) {
                String ckID = readID(dis);
                int ckSize = readLong(dis);
                parseChunk(dis, ckID, ckSize);
            }
        }
    }

    public void printProperties() {
        for (Map.Entry<String, Object> entry : properties.entrySet()) {
            if (entry.getValue() != null && !(entry.getValue() instanceof List && ((List) entry.getValue()).isEmpty())) {
                System.out.println(entry.getKey() + ": " + entry.getValue());
            }
        }
    }

    private void writeID(DataOutputStream dos, String id) throws IOException {
        dos.writeBytes(id);
    }

    private void writeLong(DataOutputStream dos, int val) throws IOException {
        dos.writeInt(Integer.reverseBytes(val));
    }

    private void writeULong(DataOutputStream dos, long val) throws IOException {
        dos.writeInt(Integer.reverseBytes((int) val));
    }

    private void writeShort(DataOutputStream dos, short val) throws IOException {
        dos.writeShort(Short.reverseBytes(val));
    }

    private void writeUShort(DataOutputStream dos, int val) throws IOException {
        dos.writeShort(Short.reverseBytes((short) val));
    }

    private void writeExtended(DataOutputStream dos, double val) throws IOException {
        // Placeholder for 44100.0
        dos.writeShort(Short.reverseBytes((short) 0x400E));
        dos.writeLong(Long.reverseBytes(0xAC44000000000000L));
    }

    private void writePString(DataOutputStream dos, String str) throws IOException {
        int len = str.length();
        dos.writeByte(len);
        dos.writeBytes(str);
        if (len % 2 == 1) dos.writeByte(0);
    }

    private void writeString(DataOutputStream dos, String str) throws IOException {
        int len = str.length();
        dos.writeBytes(str);
        if (len % 2 == 1) dos.writeByte(0);
    }

    private void writeBytes(DataOutputStream dos, String hexStr) throws IOException {
        String[] hexes = hexStr.split(" ");
        for (String hex : hexes) {
            dos.writeByte(Integer.parseInt(hex, 16));
        }
        if (hexes.length % 2 == 1) dos.writeByte(0);
    }

    private void writeChunk(DataOutputStream dos, String ckID, byte[] data) throws IOException {
        writeID(dos, ckID);
        writeLong(dos, data.length);
        dos.write(data);
        if (data.length % 2 == 1) dos.writeByte(0);
    }

    public void save(String filename) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(filename);
             DataOutputStream dos = new DataOutputStream(fos)) {
            writeID(dos, "FORM");
            long posSize = fos.getChannel().position();
            writeLong(dos, 0); // placeholder
            writeID(dos, "AIFC");

            // FVER
            if (properties.get("formatVersionTimestamp") != null) {
                byte[] fverData = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt((int) (long) properties.get("formatVersionTimestamp")).array();
                writeChunk(dos, "FVER", fverData);
            }

            // COMM
            ByteArrayOutputStream commBaos = new ByteArrayOutputStream();
            DataOutputStream commDos = new DataOutputStream(commBaos);
            writeShort(commDos, (short) (int) (properties.get("numChannels") != null ? properties.get("numChannels") : 1));
            writeULong(commDos, (long) (properties.get("numSampleFrames") != null ? properties.get("numSampleFrames") : 0));
            writeShort(commDos, (short) (int) (properties.get("sampleSize") != null ? properties.get("sampleSize") : 16));
            writeExtended(commDos, (double) (properties.get("sampleRate") != null ? properties.get("sampleRate") : 44100.0));
            writeID(commDos, (String) (properties.get("compressionType") != null ? properties.get("compressionType") : "NONE"));
            writePString(commDos, (String) (properties.get("compressionName") != null ? properties.get("compressionName") : "not compressed"));
            writeChunk(dos, "COMM", commBaos.toByteArray());

            // Similar implementation for other chunks
            // MARK
            if (!((List) properties.get("markers")).isEmpty()) {
                ByteArrayOutputStream markBaos = new ByteArrayOutputStream();
                DataOutputStream markDos = new DataOutputStream(markBaos);
                List<Map<String, Object>> markers = (List) properties.get("markers");
                writeUShort(markDos, markers.size());
                for (Map<String, Object> m : markers) {
                    writeShort(markDos, (short) (int) m.get("id"));
                    writeULong(markDos, (long) m.get("pos"));
                    writePString(markDos, (String) m.get("name"));
                }
                writeChunk(dos, "MARK", markBaos.toByteArray());
            }

            // COMT
            if (!((List) properties.get("comments")).isEmpty()) {
                ByteArrayOutputStream comtBaos = new ByteArrayOutputStream();
                DataOutputStream comtDos = new DataOutputStream(comtBaos);
                List<Map<String, Object>> comments = (List) properties.get("comments");
                writeUShort(comtDos, comments.size());
                for (Map<String, Object> c : comments) {
                    writeULong(comtDos, (long) c.get("ts"));
                    writeShort(comtDos, (short) (int) c.get("marker"));
                    String text = (String) c.get("text");
                    writeUShort(comtDos, text.length());
                    writeString(comtDos, text);
                }
                writeChunk(dos, "COMT", comtBaos.toByteArray());
            }

            // SAXL
            if (!((List) properties.get("saxels")).isEmpty()) {
                ByteArrayOutputStream saxlBaos = new ByteArrayOutputStream();
                DataOutputStream saxlDos = new DataOutputStream(saxlBaos);
                List<Map<String, Object>> saxels = (List) properties.get("saxels");
                writeUShort(saxlDos, saxels.size());
                for (Map<String, Object> s : saxels) {
                    writeShort(saxlDos, (short) (int) s.get("id"));
                    writeUShort(saxlDos, (int) s.get("size"));
                    writeBytes(saxlDos, (String) s.get("data"));
                }
                writeChunk(dos, "SAXL", saxlBaos.toByteArray());
            }

            // INST
            if (properties.get("instrumentBaseNote") != null) {
                ByteArrayOutputStream instBaos = new ByteArrayOutputStream();
                DataOutputStream instDos = new DataOutputStream(instBaos);
                instDos.writeByte((byte) properties.get("instrumentBaseNote"));
                instDos.writeByte((byte) properties.get("instrumentDetune"));
                instDos.writeByte((byte) properties.get("instrumentLowNote"));
                instDos.writeByte((byte) properties.get("instrumentHighNote"));
                instDos.writeByte((byte) properties.get("instrumentLowVelocity"));
                instDos.writeByte((byte) properties.get("instrumentHighVelocity"));
                writeShort(instDos, (short) (int) properties.get("instrumentGain"));
                writeShort(instDos, (short) (int) properties.get("sustainLoopPlayMode"));
                writeShort(instDos, (short) (int) properties.get("sustainLoopBegin"));
                writeShort(instDos, (short) (int) properties.get("sustainLoopEnd"));
                writeShort(instDos, (short) (int) properties.get("releaseLoopPlayMode"));
                writeShort(instDos, (short) (int) properties.get("releaseLoopBegin"));
                writeShort(instDos, (short) (int) properties.get("releaseLoopEnd"));
                writeChunk(dos, "INST", instBaos.toByteArray());
            }

            // MIDI
            List<String> midiData = (List) properties.get("midiData");
            for (String midi : midiData) {
                String[] hexes = midi.split(" ");
                byte[] bytes = new byte[hexes.length];
                for (int i = 0; i < hexes.length; i++) {
                    bytes[i] = (byte) Integer.parseInt(hexes[i], 16);
                }
                writeChunk(dos, "MIDI", bytes);
            }

            // AESD
            if (properties.get("aesData") != null) {
                String aesStr = (String) properties.get("aesData");
                String[] hexes = aesStr.split(" ");
                byte[] aesBytes = new byte[hexes.length];
                for (int i = 0; i < hexes.length; i++) {
                    aesBytes[i] = (byte) Integer.parseInt(hexes[i], 16);
                }
                writeChunk(dos, "AESD", aesBytes);
            }

            // APPL
            List<Map<String, Object>> appSpecific = (List) properties.get("appSpecific");
            for (Map<String, Object> app : appSpecific) {
                ByteArrayOutputStream applBaos = new ByteArrayOutputStream();
                DataOutputStream applDos = new DataOutputStream(applBaos);
                writeID(applDos, (String) app.get("sig"));
                writeBytes(applDos, (String) app.get("data"));
                writeChunk(dos, "APPL", applBaos.toByteArray());
            }

            // NAME
            if (properties.get("soundName") != null) {
                writeChunk(dos, "NAME", ((String) properties.get("soundName")).getBytes(StandardCharsets.US_ASCII));
            }

            // AUTH
            if (properties.get("author") != null) {
                writeChunk(dos, "AUTH", ((String) properties.get("author")).getBytes(StandardCharsets.US_ASCII));
            }

            // (c) 
            if (properties.get("copyright") != null) {
                writeChunk(dos, "(c) ", ((String) properties.get("copyright")).getBytes(StandardCharsets.US_ASCII));
            }

            // ANNO
            List<String> annotations = (List) properties.get("annotations");
            for (String anno : annotations) {
                writeChunk(dos, "ANNO", anno.getBytes(StandardCharsets.US_ASCII));
            }

            // SSND
            ByteArrayOutputStream ssndBaos = new ByteArrayOutputStream();
            DataOutputStream ssndDos = new DataOutputStream(ssndBaos);
            writeULong(ssndDos, (long) (properties.get("soundDataOffset") != null ? properties.get("soundDataOffset") : 0));
            writeULong(ssndDos, (long) (properties.get("soundDataBlockSize") != null ? properties.get("soundDataBlockSize") : 0));
            ssndDos.write(soundData);
            writeChunk(dos, "SSND", ssndBaos.toByteArray());

            // Update FORM size
            long endPos = fos.getChannel().position();
            fos.getChannel().position(posSize);
            writeLong(dos, (int) (endPos - 8));
            fos.getChannel().position(endPos);
        }
    }

    // Main for testing
    public static void main(String[] args) throws IOException {
        AIFCFile aifc = new AIFCFile();
        aifc.load("example.aifc");
        aifc.printProperties();
        aifc.save("output.aifc");
    }
}

Notes:

  • Java’s javax.sound.sampled supports AIFF but not all AIFC compression types. Manual chunk parsing is needed for FVER, SACC, and specific compression types.
  • Writing compressed AIFC files requires external libraries (e.g., Tritonus) or custom implementation, which is complex and omitted here for simplicity.
  • The example reads basic COMM chunk data and prints properties.

4. JavaScript Class for .AIFC File Handling

JavaScript in a browser environment has limited direct file access, but Node.js with the fs module can handle file operations. Below is a Node.js-based class to read and print AIFC properties. Writing AIFC files is complex due to the lack of native libraries, so a basic placeholder is provided.

const fs = require('fs');

class AIFCFile {
    constructor() {
        this.properties = {
            formatVersionTimestamp: null,
            numChannels: null,
            numSampleFrames: null,
            sampleSize: null,
            sampleRate: null,
            compressionType: null,
            compressionName: null,
            soundDataOffset: null,
            soundDataBlockSize: null,
            markers: [],
            comments: [],
            saxels: [],
            instrumentBaseNote: null,
            instrumentDetune: null,
            instrumentLowNote: null,
            instrumentHighNote: null,
            instrumentLowVelocity: null,
            instrumentHighVelocity: null,
            instrumentGain: null,
            sustainLoopPlayMode: null,
            sustainLoopBegin: null,
            sustainLoopEnd: null,
            releaseLoopPlayMode: null,
            releaseLoopBegin: null,
            releaseLoopEnd: null,
            midiData: [],
            aesData: null,
            appSpecific: [],
            soundName: null,
            author: null,
            copyright: null,
            annotations: []
        };
        this.soundData = Buffer.alloc(0);
        this.offset = 0;
        this.buffer = null;
    }

    readID() {
        const id = this.buffer.toString('ascii', this.offset, this.offset + 4);
        this.offset += 4;
        return id;
    }

    readLong() {
        const val = this.buffer.readInt32BE(this.offset);
        this.offset += 4;
        return val;
    }

    readULong() {
        const val = this.buffer.readUInt32BE(this.offset);
        this.offset += 4;
        return val;
    }

    readShort() {
        const val = this.buffer.readInt16BE(this.offset);
        this.offset += 2;
        return val;
    }

    readUShort() {
        const val = this.buffer.readUInt16BE(this.offset);
        this.offset += 2;
        return val;
    }

    readExtended() {
        const exp = this.buffer.readInt16BE(this.offset) - 16383;
        this.offset += 2;
        // Approximate mantissa
        let mant = 0;
        for (let i = 0; i < 8; i++) {
            mant = (mant << 8) + this.buffer.readUInt8(this.offset++);
        }
        mant /= 2 ** 63;
        return mant * (2 ** exp);
    }

    readPString() {
        const len = this.buffer.readUInt8(this.offset++);
        const str = this.buffer.toString('ascii', this.offset, this.offset + len);
        this.offset += len;
        if (len % 2 === 1) this.offset++;
        return str;
    }

    readString(len) {
        const str = this.buffer.toString('ascii', this.offset, this.offset + len);
        this.offset += len;
        if (len % 2 === 1) this.offset++;
        return str;
    }

    readBytes(len) {
        const bytes = [];
        for (let i = 0; i < len; i++) {
            bytes.push(this.buffer.readUInt8(this.offset++).toString(16).padStart(2, '0'));
        }
        if (len % 2 === 1) this.offset++;
        return bytes.join(' ');
    }

    parseChunk() {
        const ckID = this.readID();
        const ckSize = this.readLong();
        const end = this.offset + ckSize + (ckSize % 2 ? 1 : 0);
        switch (ckID) {
            case 'FVER':
                this.properties.formatVersionTimestamp = this.readULong();
                break;
            case 'COMM':
                this.properties.numChannels = this.readShort();
                this.properties.numSampleFrames = this.readULong();
                this.properties.sampleSize = this.readShort();
                this.properties.sampleRate = this.readExtended();
                this.properties.compressionType = this.readID();
                this.properties.compressionName = this.readPString();
                break;
            case 'SSND':
                this.properties.soundDataOffset = this.readULong();
                this.properties.soundDataBlockSize = this.readULong();
                const dataSize = ckSize - 8;
                this.soundData = this.buffer.slice(this.offset, this.offset + dataSize);
                this.offset += dataSize;
                if (dataSize % 2 === 1) this.offset++;
                break;
            case 'MARK':
                const numMarkers = this.readUShort();
                for (let i = 0; i < numMarkers; i++) {
                    const id = this.readShort();
                    const pos = this.readULong();
                    const name = this.readPString();
                    this.properties.markers.push({ id, pos, name });
                }
                break;
            case 'COMT':
                const numComments = this.readUShort();
                for (let i = 0; i < numComments; i++) {
                    const ts = this.readULong();
                    const marker = this.readShort();
                    const count = this.readUShort();
                    const text = this.readString(count);
                    this.properties.comments.push({ ts, marker, text });
                }
                break;
            case 'SAXL':
                const numSaxels = this.readUShort();
                for (let i = 0; i < numSaxels; i++) {
                    const id = this.readShort();
                    const size = this.readUShort();
                    const data = this.readBytes(size);
                    this.properties.saxels.push({ id, size, data });
                }
                break;
            case 'INST':
                this.properties.instrumentBaseNote = this.buffer.readInt8(this.offset++);
                this.properties.instrumentDetune = this.buffer.readInt8(this.offset++);
                this.properties.instrumentLowNote = this.buffer.readInt8(this.offset++);
                this.properties.instrumentHighNote = this.buffer.readInt8(this.offset++);
                this.properties.instrumentLowVelocity = this.buffer.readInt8(this.offset++);
                this.properties.instrumentHighVelocity = this.buffer.readInt8(this.offset++);
                this.properties.instrumentGain = this.readShort();
                this.properties.sustainLoopPlayMode = this.readShort();
                this.properties.sustainLoopBegin = this.readShort();
                this.properties.sustainLoopEnd = this.readShort();
                this.properties.releaseLoopPlayMode = this.readShort();
                this.properties.releaseLoopBegin = this.readShort();
                this.properties.releaseLoopEnd = this.readShort();
                break;
            case 'MIDI':
                const midiData = this.readBytes(ckSize);
                this.properties.midiData.push(midiData);
                break;
            case 'AESD':
                this.properties.aesData = this.readBytes(24);
                break;
            case 'APPL':
                const sig = this.readID();
                const data = this.readBytes(ckSize - 4);
                this.properties.appSpecific.push({ sig, data });
                break;
            case 'NAME':
                this.properties.soundName = this.readString(ckSize);
                break;
            case 'AUTH':
                this.properties.author = this.readString(ckSize);
                break;
            case '(c) ':
                this.properties.copyright = this.readString(ckSize);
                break;
            case 'ANNO':
                this.properties.annotations.push(this.readString(ckSize));
                break;
            default:
                this.offset += ckSize;
                if (ckSize % 2 === 1) this.offset++;
        }
        this.offset = end;
    }

    load(filename) {
        this.buffer = fs.readFileSync(filename);
        if (this.readID() !== 'FORM') {
            console.log('Not a valid AIFC file');
            return;
        }
        this.readLong(); // form size
        if (this.readID() !== 'AIFC') {
            console.log('Not AIFC form type');
            return;
        }
        while (this.offset < this.buffer.length) {
            this.parseChunk();
        }
    }

    printProperties() {
        for (const [key, value] of Object.entries(this.properties)) {
            if (value !== null && !(Array.isArray(value) && value.length === 0)) {
                console.log(`${key}: ${JSON.stringify(value)}`);
            }
        }
    }

    writeID(buffer, id, pos) {
        buffer.write(id, pos, 4, 'ascii');
        return pos + 4;
    }

    writeLong(buffer, val, pos) {
        buffer.writeInt32BE(val, pos);
        return pos + 4;
    }

    writeULong(buffer, val, pos) {
        buffer.writeUInt32BE(val, pos);
        return pos + 4;
    }

    writeShort(buffer, val, pos) {
        buffer.writeInt16BE(val, pos);
        return pos + 2;
    }

    writeUShort(buffer, val, pos) {
        buffer.writeUInt16BE(val, pos);
        return pos + 2;
    }

    writeExtended(buffer, val, pos) {
        // Placeholder
        buffer.writeInt16BE(0x400E, pos);
        pos += 2;
        buffer.writeUInt32BE(0xAC44, pos);
        pos += 4;
        buffer.writeUInt32BE(0, pos);
        pos += 4;
        return pos;
    }

    writePString(buffer, str, pos) {
        const len = str.length;
        buffer.writeUInt8(len, pos++);
        buffer.write(str, pos, len, 'ascii');
        pos += len;
        if (len % 2 === 1) buffer.writeUInt8(0, pos++);
        return pos;
    }

    writeString(buffer, str, pos) {
        const len = str.length;
        buffer.write(str, pos, len, 'ascii');
        pos += len;
        if (len % 2 === 1) buffer.writeUInt8(0, pos++);
        return pos;
    }

    writeBytes(buffer, hexStr, pos) {
        const hexes = hexStr.split(' ');
        hexes.forEach(hex => {
            buffer.writeUInt8(parseInt(hex, 16), pos++);
        });
        if (hexes.length % 2 === 1) buffer.writeUInt8(0, pos++);
        return pos;
    }

    calculateChunkSize(dataLength) {
        return dataLength + (dataLength % 2 === 1 ? 1 : 0);
    }

    save(filename) {
        let totalSize = 12; // FORM + size + AIFC
        const chunks = [];

        // FVER
        if (this.properties.formatVersionTimestamp !== null) {
            const data = Buffer.alloc(4);
            data.writeUInt32BE(this.properties.formatVersionTimestamp, 0);
            chunks.push({ id: 'FVER', data });
            totalSize += 8 + this.calculateChunkSize(4);
        }

        // COMM
        const commBuffer = Buffer.alloc(100);
        let commPos = 0;
        commPos = this.writeShort(commBuffer, this.properties.numChannels || 1, commPos);
        commPos = this.writeULong(commBuffer, this.properties.numSampleFrames || 0, commPos);
        commPos = this.writeShort(commBuffer, this.properties.sampleSize || 16, commPos);
        commPos = this.writeExtended(commBuffer, this.properties.sampleRate || 44100, commPos);
        commPos = this.writeID(commBuffer, this.properties.compressionType || 'NONE', commPos);
        commPos = this.writePString(commBuffer, this.properties.compressionName || 'not compressed', commPos);
        const commData = commBuffer.slice(0, commPos);
        chunks.push({ id: 'COMM', data: commData });
        totalSize += 8 + this.calculateChunkSize(commPos);

        // MARK
        if (this.properties.markers.length > 0) {
            const markBuffer = Buffer.alloc(1024);
            let markPos = 0;
            markPos = this.writeUShort(markBuffer, this.properties.markers.length, markPos);
            this.properties.markers.forEach(m => {
                markPos = this.writeShort(markBuffer, m.id, markPos);
                markPos = this.writeULong(markBuffer, m.pos, markPos);
                markPos = this.writePString(markBuffer, m.name, markPos);
            });
            const markData = markBuffer.slice(0, markPos);
            chunks.push({ id: 'MARK', data: markData });
            totalSize += 8 + this.calculateChunkSize(markPos);
        }

        // Add similar for other chunks...

        // SSND
        const ssndBuffer = Buffer.alloc(8 + this.soundData.length);
        let ssndPos = 0;
        ssndPos = this.writeULong(ssndBuffer, this.properties.soundDataOffset || 0, ssndPos);
        ssndPos = this.writeULong(ssndBuffer, this.properties.soundDataBlockSize || 0, ssndPos);
        this.soundData.copy(ssndBuffer, ssndPos);
        ssndPos += this.soundData.length;
        const ssndData = ssndBuffer.slice(0, ssndPos);
        chunks.push({ id: 'SSND', data: ssndData });
        totalSize += 8 + this.calculateChunkSize(ssndPos);

        // Write to file
        const outBuffer = Buffer.alloc(totalSize);
        let pos = 0;
        pos = this.writeID(outBuffer, 'FORM', pos);
        pos = this.writeLong(outBuffer, totalSize - 8, pos);
        pos = this.writeID(outBuffer, 'AIFC', pos);
        chunks.forEach(chunk => {
            pos = this.writeID(outBuffer, chunk.id, pos);
            pos = this.writeLong(outBuffer, chunk.data.length, pos);
            chunk.data.copy(outBuffer, pos);
            pos += chunk.data.length;
            if (chunk.data.length % 2 === 1) {
                outBuffer.writeUInt8(0, pos++);
            }
        });
        fs.writeFileSync(filename, outBuffer.slice(0, pos));
    }
}

// Usage:
// const aifc = new AIFCFile();
// aifc.load('example.aifc');
// aifc.printProperties();
// aifc.save('output.aifc');

Notes:

  • Node.js is used for file access. Browser-based JavaScript would require a file input element and FileReader.
  • Parsing the COMM chunk is shown; FVER and SACC require similar binary parsing but are omitted for brevity.
  • Writing AIFC files in JavaScript is non-trivial without libraries like audio-file-encoder.

5. C++ Class for .AIFC File Handling

C does not have a standard library for AIFC, so we implement a basic class-like structure to parse the file manually. This example focuses on reading and printing properties.

#include <iostream>
#include <fstream>
#include <vector>
#include <map>
#include <string>
#include <iomanip>
#include <cstring>
#include <sstream>

class AIFCFile {
private:
    std::map<std::string, std::string> properties; // Use string for simplicity in print
    std::vector<char> soundData;

    std::string toHex(const std::vector<char>& data) {
        std::ostringstream oss;
        for (char c : data) {
            oss << std::hex << std::setw(2) << std::setfill('0') << (int)(unsigned char)c << " ";
        }
        std::string s = oss.str();
        if (!s.empty()) s.pop_back();
        return s;
    }

    int32_t readLong(std::ifstream& f) {
        int32_t val;
        f.read(reinterpret_cast<char*>(&val), 4);
        return __builtin_bswap32(val);
    }

    uint32_t readULong(std::ifstream& f) {
        uint32_t val;
        f.read(reinterpret_cast<char*>(&val), 4);
        return __builtin_bswap32(val);
    }

    int16_t readShort(std::ifstream& f) {
        int16_t val;
        f.read(reinterpret_cast<char*>(&val), 2);
        return __builtin_bswap16(val);
    }

    uint16_t readUShort(std::ifstream& f) {
        uint16_t val;
        f.read(reinterpret_cast<char*>(&val), 2);
        return __builtin_bswap16(val);
    }

    double readExtended(std::ifstream& f) {
        int16_t exp;
        f.read(reinterpret_cast<char*>(&exp), 2);
        exp = __builtin_bswap16(exp) - 16383;
        uint64_t mant = 0;
        f.read(reinterpret_cast<char*>(&mant), 8);
        mant = __builtin_bswap64(mant);
        return (mant / static_cast<double>(1LL << 63)) * std::pow(2, exp);
    }

    std::string readID(std::ifstream& f) {
        char id[5] = {0};
        f.read(id, 4);
        return std::string(id);
    }

    std::string readPString(std::ifstream& f) {
        uint8_t len;
        f.read(reinterpret_cast<char*>(&len), 1);
        std::string str(len, 0);
        f.read(&str[0], len);
        if (len % 2 == 1) {
            char pad;
            f.read(&pad, 1);
        }
        return str;
    }

    std::string readString(std::ifstream& f, int len) {
        std::string str(len, 0);
        f.read(&str[0], len);
        if (len % 2 == 1) {
            char pad;
            f.read(&pad, 1);
        }
        return str;
    }

    std::string readBytes(std::ifstream& f, int len) {
        std::vector<char> bytes(len);
        f.read(bytes.data(), len);
        if (len % 2 == 1) {
            char pad;
            f.read(&pad, 1);
        }
        return toHex(bytes);
    }

    void parseChunk(std::ifstream& f, const std::string& ckID, int ckSize) {
        if (ckID == "FVER") {
            properties["formatVersionTimestamp"] = std::to_string(readULong(f));
        } else if (ckID == "COMM") {
            properties["numChannels"] = std::to_string(readShort(f));
            properties["numSampleFrames"] = std::to_string(readULong(f));
            properties["sampleSize"] = std::to_string(readShort(f));
            properties["sampleRate"] = std::to_string(readExtended(f));
            properties["compressionType"] = readID(f);
            properties["compressionName"] = readPString(f);
        } else if (ckID == "SSND") {
            properties["soundDataOffset"] = std::to_string(readULong(f));
            properties["soundDataBlockSize"] = std::to_string(readULong(f));
            int dataSize = ckSize - 8;
            soundData.resize(dataSize);
            f.read(soundData.data(), dataSize);
            if (dataSize % 2 == 1) {
                char pad;
                f.read(&pad, 1);
            }
        } else if (ckID == "MARK") {
            uint16_t numMarkers = readUShort(f);
            std::ostringstream oss;
            oss << "[";
            for (int i = 0; i < numMarkers; ++i) {
                int16_t id = readShort(f);
                uint32_t pos = readULong(f);
                std::string name = readPString(f);
                oss << "{id:" << id << ", pos:" << pos << ", name:\"" << name << "\"}";
                if (i < numMarkers - 1) oss << ", ";
            }
            oss << "]";
            properties["markers"] = oss.str();
        } else if (ckID == "COMT") {
            uint16_t numComments = readUShort(f);
            std::ostringstream oss;
            oss << "[";
            for (int i = 0; i < numComments; ++i) {
                uint32_t ts = readULong(f);
                int16_t marker = readShort(f);
                uint16_t count = readUShort(f);
                std::string text = readString(f, count);
                oss << "{ts:" << ts << ", marker:" << marker << ", text:\"" << text << "\"}";
                if (i < numComments - 1) oss << ", ";
            }
            oss << "]";
            properties["comments"] = oss.str();
        } else if (ckID == "SAXL") {
            uint16_t numSaxels = readUShort(f);
            std::ostringstream oss;
            oss << "[";
            for (int i = 0; i < numSaxels; ++i) {
                int16_t id = readShort(f);
                uint16_t size = readUShort(f);
                std::string data = readBytes(f, size);
                oss << "{id:" << id << ", size:" << size << ", data:\"" << data << "\"}";
                if (i < numSaxels - 1) oss << ", ";
            }
            oss << "]";
            properties["saxels"] = oss.str();
        } else if (ckID == "INST") {
            char baseNote, detune, lowNote, highNote, lowVel, highVel;
            f.read(&baseNote, 1);
            f.read(&detune, 1);
            f.read(&lowNote, 1);
            f.read(&highNote, 1);
            f.read(&lowVel, 1);
            f.read(&highVel, 1);
            properties["instrumentBaseNote"] = std::to_string(static_cast<int>(baseNote));
            properties["instrumentDetune"] = std::to_string(static_cast<int>(detune));
            properties["instrumentLowNote"] = std::to_string(static_cast<int>(lowNote));
            properties["instrumentHighNote"] = std::to_string(static_cast<int>(highNote));
            properties["instrumentLowVelocity"] = std::to_string(static_cast<int>(lowVel));
            properties["instrumentHighVelocity"] = std::to_string(static_cast<int>(highVel));
            properties["instrumentGain"] = std::to_string(readShort(f));
            properties["sustainLoopPlayMode"] = std::to_string(readShort(f));
            properties["sustainLoopBegin"] = std::to_string(readShort(f));
            properties["sustainLoopEnd"] = std::to_string(readShort(f));
            properties["releaseLoopPlayMode"] = std::to_string(readShort(f));
            properties["releaseLoopBegin"] = std::to_string(readShort(f));
            properties["releaseLoopEnd"] = std::to_string(readShort(f));
        } else if (ckID == "MIDI") {
            std::string midi = readBytes(f, ckSize);
            std::string& curr = properties["midiData"];
            if (!curr.empty()) curr += ", ";
            curr += "\"" + midi + "\"";
            if (curr.front() != '[') curr = "[" + curr + "]";
        } else if (ckID == "AESD") {
            properties["aesData"] = readBytes(f, 24);
        } else if (ckID == "APPL") {
            std::string sig = readID(f);
            std::string data = readBytes(f, ckSize - 4);
            std::string& curr = properties["appSpecific"];
            if (!curr.empty()) curr += ", ";
            curr += "{sig:\"" + sig + "\", data:\"" + data + "\"}";
            if (curr.front() != '[') curr = "[" + curr + "]";
        } else if (ckID == "NAME") {
            properties["soundName"] = readString(f, ckSize);
        } else if (ckID == "AUTH") {
            properties["author"] = readString(f, ckSize);
        } else if (ckID == "(c) ") {
            properties["copyright"] = readString(f, ckSize);
        } else if (ckID == "ANNO") {
            std::string anno = readString(f, ckSize);
            std::string& curr = properties["annotations"];
            if (!curr.empty()) curr += ", ";
            curr += "\"" + anno + "\"";
            if (curr.front() != '[') curr = "[" + curr + "]";
        } else {
            f.seekg(ckSize, std::ios::cur);
            if (ckSize % 2 == 1) {
                char pad;
                f.read(&pad, 1);
            }
        }
    }

public:
    AIFCFile() {
        properties["markers"] = "";
        properties["comments"] = "";
        properties["saxels"] = "";
        properties["midiData"] = "";
        properties["appSpecific"] = "";
        properties["annotations"] = "";
    }

    void load(const std::string& filename) {
        std::ifstream f(filename, std::ios::binary);
        if (!f) {
            std::cout << "Cannot open file" << std::endl;
            return;
        }
        if (readID(f) != "FORM") {
            std::cout << "Not a valid AIFC file" << std::endl;
            return;
        }
        readLong(f); // form size
        if (readID(f) != "AIFC") {
            std::cout << "Not AIFC form type" << std::endl;
            return;
        }
        while (f) {
            std::string ckID = readID(f);
            if (f.eof()) break;
            int ckSize = readLong(f);
            parseChunk(f, ckID, ckSize);
        }
    }

    void printProperties() {
        for (const auto& p : properties) {
            if (!p.second.empty()) {
                std::cout << p.first << ": " << p.second << std::endl;
            }
        }
    }

    void save(const std::string& filename) {
        std::ofstream f(filename, std::ios::binary);
        if (!f) return;

        // Placeholder write, similar logic as above but for output
        // Implement writing each chunk based on properties
        // For brevity, stubbed as full implementation is lengthy
    }
};

int main() {
    AIFCFile aifc;
    aifc.load("example.aifc");
    aifc.printProperties();
    aifc.save("output.aifc");
    return 0;
}

Notes:

  • The C implementation manually parses the COMM chunk to extract properties.
  • Writing AIFC files requires constructing the chunk structure manually, which is complex and not fully implemented here.
  • The sample rate parsing is approximated due to the complexity of the 80-bit extended float format.

General Notes

  • Reading: All implementations parse the COMM chunk to extract core audio properties. FVER, SACC, and COMT chunks are not fully parsed due to complexity and limited library support.
  • Writing: Python’s aifc module supports writing basic AIFC files, but Java, JavaScript, and C implementations are limited due to the lack of native AIFC compression support.
  • Testing: These classes assume the presence of an example.aifc file. You may need to provide a valid AIFC file or generate one for testing.
  • Limitations: Compression-specific handling (e.g., ulaw, alaw) requires additional libraries or manual implementation, which is beyond the scope of this basic example.
  • Sources: The properties and structure are based on the AIFC specification from Apple and related documentation.

Let me know if you need further clarification or additional features!

1. List of Properties Intrinsic to the .AIFC File Format

Based on the AIFC file format specifications (an extension of AIFF for compressed audio, following the EA IFF 85 standard), the format is chunk-based with a top-level FORM chunk of type 'AIFC'. The intrinsic properties are the metadata fields stored in the required and optional chunks, excluding the raw sound data itself (which is variable and not a "property" but the payload). These properties define the file's structure, audio parameters, and additional metadata. All data is big-endian unless noted.

Required Properties (from mandatory chunks: FVER, COMM, SSND):

  • Format Version Timestamp: Unsigned long (32-bit), representing the version timestamp (e.g., 0xA2805140 for version 1, seconds since Jan 1, 1904).
  • Number of Audio Channels: Short (16-bit signed), e.g., 1 for mono, 2 for stereo.
  • Number of Sample Frames: Unsigned long (32-bit), total frames in the sound data.
  • Sample Size: Short (16-bit signed), bits per sample (1-32; for compressed, this is the original bit depth).
  • Sample Rate: Extended (80-bit IEEE 754 floating-point), samples per second.
  • Compression Type: ID (32-bit, 4 ASCII chars), e.g., 'NONE' for uncompressed, 'ACE2' for 2:1 ACE, 'MAC3' for MACE 3:1.
  • Compression Name: Pstring (Pascal-style: 1-byte unsigned count + chars; padded to even length if needed), human-readable name like "not compressed".
  • Sound Data Offset: Unsigned long (32-bit), byte offset to first sample frame (usually 0).
  • Sound Data Block Size: Unsigned long (32-bit), alignment block size (usually 0 for no block alignment).

Optional Properties (from optional chunks; can be absent or multiple where noted):

  • Markers: List of markers, each with:
  • Marker ID: Short (16-bit signed), unique identifier.
  • Position: Unsigned long (32-bit), sample frame offset.
  • Marker Name: Pstring (as above).
  • Comments: List of comments, each with:
  • Timestamp: Unsigned long (32-bit), seconds since Jan 1, 1904.
  • Marker ID: Short (16-bit signed), linked marker or 0 if none.
  • Text: String (length from 16-bit unsigned count + chars).
  • Instrument Data:
  • Base Note: Unsigned char (8-bit), MIDI note for middle C (60).
  • Detune: Signed char (8-bit), cents (-50 to +50).
  • Low Note: Unsigned char (8-bit), lowest MIDI note.
  • High Note: Unsigned char (8-bit), highest MIDI note.
  • Low Velocity: Unsigned char (8-bit), lowest MIDI velocity.
  • High Velocity: Unsigned char (8-bit), highest MIDI velocity.
  • Gain: Short (16-bit signed), dB gain.
  • Sustain Loop:
  • Play Mode: Short (16-bit signed; 0=no loop, 1=forward, 2=forward/backward).
  • Begin Loop Marker ID: Short (16-bit signed).
  • End Loop Marker ID: Short (16-bit signed).
  • Release Loop: Same structure as Sustain Loop.
  • Name: String (chars, padded to even length if needed).
  • Author: String (as above).
  • Copyright: String (as above).
  • Annotations: List of strings (multiple ANNO chunks allowed; each as above).
  • AES Channel Status Data: 24-byte binary array (Audio Engineering Society channel status).
  • MIDI Data: Binary array (variable length MIDI data).
  • Application Specific Data: List of entries, each with:
  • Application Signature: OSType (32-bit, 4 chars).
  • Data: Binary array (variable).

Unknown chunks are ignored during parsing. The raw sound data in SSND is not listed as a property but must be handled for full file I/O.

2. Python Class

import struct
import os

class AIFCFile:
    def __init__(self):
        self.format_version_timestamp = 0
        self.num_channels = 0
        self.num_sample_frames = 0
        self.sample_size = 0
        self.sample_rate = 0.0  # Approximate as float; extended is 80-bit
        self.compression_type = b''
        self.compression_name = ''
        self.sound_data_offset = 0
        self.sound_data_block_size = 0
        self.markers = []  # List of (id, position, name)
        self.comments = []  # List of (timestamp, marker_id, text)
        self.instrument = {
            'base_note': 0, 'detune': 0, 'low_note': 0, 'high_note': 0,
            'low_velocity': 0, 'high_velocity': 0, 'gain': 0,
            'sustain_loop': {'play_mode': 0, 'begin': 0, 'end': 0},
            'release_loop': {'play_mode': 0, 'begin': 0, 'end': 0}
        }
        self.name = ''
        self.author = ''
        self.copyright = ''
        self.annotations = []  # List of strings
        self.aes_data = b''
        self.midi_data = b''
        self.app_specific = []  # List of (signature, data)
        self.sound_data = b''  # Raw sound data (for full read/write)

    def _read_extended(self, data):
        # Unpack 80-bit extended to float (approximate)
        exp = struct.unpack('>h', data[0:2])[0]
        hi = struct.unpack('>I', data[2:6])[0]
        lo = struct.unpack('>I', data[6:10])[0]
        mant = (hi << 32 | lo) / (1 << 63)
        return mant * (2 ** (exp - 16383))

    def _write_extended(self, value):
        # Approximate float to 80-bit extended
        if value == 0:
            return b'\x00' * 10
        import math
        sign = 0 if value > 0 else 0x8000
        value = abs(value)
        exp = math.floor(math.log2(value)) + 16383
        mant = value / (2 ** (exp - 16383))
        hi = int(mant * (1 << 31))
        lo = int((mant * (1 << 31) - hi) * (1 << 32))
        return struct.pack('>hII', sign | (exp & 0x7FFF), hi, lo)

    def _read_pstring(self, f):
        count = struct.unpack('B', f.read(1))[0]
        s = f.read(count).decode('ascii')
        if count % 2 == 0:
            f.read(1)  # Pad
        return s

    def _write_pstring(self, s):
        count = len(s)
        pad = b'\x00' if count % 2 == 0 else b''
        return struct.pack('B', count) + s.encode('ascii') + pad

    def _read_string(self, f, size):
        s = f.read(size).decode('ascii').rstrip('\x00')
        if size % 2 == 1:
            f.read(1)  # Pad
        return s

    def _write_string(self, s, pad_to_even=True):
        data = s.encode('ascii')
        if pad_to_even and len(data) % 2 == 1:
            data += b'\x00'
        return data

    def read(self, file_path):
        with open(file_path, 'rb') as f:
            ck_id = f.read(4)
            if ck_id != b'FORM':
                raise ValueError("Not a valid AIFC file")
            ck_size = struct.unpack('>I', f.read(4))[0]
            form_type = f.read(4)
            if form_type != b'AIFC':
                raise ValueError("Not AIFC format")
            end_pos = f.tell() + ck_size - 4
            while f.tell() < end_pos:
                ck_id = f.read(4)
                if len(ck_id) == 0:
                    break
                ck_size = struct.unpack('>I', f.read(4))[0]
                start_pos = f.tell()
                if ck_id == b'FVER':
                    self.format_version_timestamp = struct.unpack('>I', f.read(4))[0]
                elif ck_id == b'COMM':
                    self.num_channels = struct.unpack('>h', f.read(2))[0]
                    self.num_sample_frames = struct.unpack('>I', f.read(4))[0]
                    self.sample_size = struct.unpack('>h', f.read(2))[0]
                    self.sample_rate = self._read_extended(f.read(10))
                    self.compression_type = f.read(4)
                    self.compression_name = self._read_pstring(f)
                elif ck_id == b'SSND':
                    self.sound_data_offset = struct.unpack('>I', f.read(4))[0]
                    self.sound_data_block_size = struct.unpack('>I', f.read(4))[0]
                    data_size = ck_size - 8
                    f.seek(self.sound_data_offset, 1)
                    self.sound_data = f.read(data_size - self.sound_data_offset)
                elif ck_id == b'MARK':
                    num_markers = struct.unpack('>H', f.read(2))[0]
                    for _ in range(num_markers):
                        mid = struct.unpack('>h', f.read(2))[0]
                        pos = struct.unpack('>I', f.read(4))[0]
                        name = self._read_pstring(f)
                        self.markers.append((mid, pos, name))
                elif ck_id == b'COMT':
                    num_comm = struct.unpack('>H', f.read(2))[0]
                    for _ in range(num_comm):
                        ts = struct.unpack('>I', f.read(4))[0]
                        mid = struct.unpack('>h', f.read(2))[0]
                        count = struct.unpack('>H', f.read(2))[0]
                        text = self._read_string(f, count)
                        self.comments.append((ts, mid, text))
                elif ck_id == b'INST':
                    self.instrument['base_note'] = struct.unpack('B', f.read(1))[0]
                    self.instrument['detune'] = struct.unpack('b', f.read(1))[0]
                    self.instrument['low_note'] = struct.unpack('B', f.read(1))[0]
                    self.instrument['high_note'] = struct.unpack('B', f.read(1))[0]
                    self.instrument['low_velocity'] = struct.unpack('B', f.read(1))[0]
                    self.instrument['high_velocity'] = struct.unpack('B', f.read(1))[0]
                    self.instrument['gain'] = struct.unpack('>h', f.read(2))[0]
                    self.instrument['sustain_loop']['play_mode'] = struct.unpack('>h', f.read(2))[0]
                    self.instrument['sustain_loop']['begin'] = struct.unpack('>h', f.read(2))[0]
                    self.instrument['sustain_loop']['end'] = struct.unpack('>h', f.read(2))[0]
                    self.instrument['release_loop']['play_mode'] = struct.unpack('>h', f.read(2))[0]
                    self.instrument['release_loop']['begin'] = struct.unpack('>h', f.read(2))[0]
                    self.instrument['release_loop']['end'] = struct.unpack('>h', f.read(2))[0]
                elif ck_id == b'NAME':
                    self.name = self._read_string(f, ck_size)
                elif ck_id == b'AUTH':
                    self.author = self._read_string(f, ck_size)
                elif ck_id == b'(c) ':
                    self.copyright = self._read_string(f, ck_size)
                elif ck_id == b'ANNO':
                    self.annotations.append(self._read_string(f, ck_size))
                elif ck_id == b'AESD':
                    self.aes_data = f.read(24)
                elif ck_id == b'MIDI':
                    self.midi_data = f.read(ck_size)
                elif ck_id == b'APPL':
                    sig = f.read(4)
                    data = f.read(ck_size - 4)
                    self.app_specific.append((sig, data))
                else:
                    f.seek(ck_size, 1)  # Skip unknown
                if (f.tell() - start_pos) % 2 == 1:
                    f.read(1)  # Pad to even

    def write(self, file_path):
        chunks = []
        # FVER
        chunks.append(b'FVER' + struct.pack('>I', 4) + struct.pack('>I', self.format_version_timestamp))
        # COMM
        comm_data = struct.pack('>hIh', self.num_channels, self.num_sample_frames, self.sample_size) + \
                    self._write_extended(self.sample_rate) + self.compression_type + \
                    self._write_pstring(self.compression_name)
        chunks.append(b'COMM' + struct.pack('>I', len(comm_data)) + comm_data)
        # SSND
        ssnd_data = struct.pack('>II', self.sound_data_offset, self.sound_data_block_size) + self.sound_data
        if len(ssnd_data) % 2 == 1:
            ssnd_data += b'\x00'
        chunks.append(b'SSND' + struct.pack('>I', len(ssnd_data)) + ssnd_data)
        # MARK
        if self.markers:
            mark_data = struct.pack('>H', len(self.markers))
            for mid, pos, name in self.markers:
                mark_data += struct.pack('>hI', mid, pos) + self._write_pstring(name)
            chunks.append(b'MARK' + struct.pack('>I', len(mark_data)) + mark_data)
        # COMT
        if self.comments:
            comt_data = struct.pack('>H', len(self.comments))
            for ts, mid, text in self.comments:
                tdata = text.encode('ascii')
                comt_data += struct.pack('>IhH', ts, mid, len(tdata)) + tdata
                if len(tdata) % 2 == 1:
                    comt_data += b'\x00'
            chunks.append(b'COMT' + struct.pack('>I', len(comt_data)) + comt_data)
        # INST
        if any(self.instrument.values()):
            inst_data = struct.pack('B b B B B B >h >h h h >h h h', 
                                    self.instrument['base_note'], self.instrument['detune'],
                                    self.instrument['low_note'], self.instrument['high_note'],
                                    self.instrument['low_velocity'], self.instrument['high_velocity'],
                                    self.instrument['gain'],
                                    self.instrument['sustain_loop']['play_mode'], self.instrument['sustain_loop']['begin'], self.instrument['sustain_loop']['end'],
                                    self.instrument['release_loop']['play_mode'], self.instrument['release_loop']['begin'], self.instrument['release_loop']['end'])
            chunks.append(b'INST' + struct.pack('>I', len(inst_data)) + inst_data)
        # NAME
        if self.name:
            name_data = self._write_string(self.name)
            chunks.append(b'NAME' + struct.pack('>I', len(name_data)) + name_data)
        # AUTH
        if self.author:
            auth_data = self._write_string(self.author)
            chunks.append(b'AUTH' + struct.pack('>I', len(auth_data)) + auth_data)
        # (c) 
        if self.copyright:
            copy_data = self._write_string(self.copyright)
            chunks.append(b'(c) ' + struct.pack('>I', len(copy_data)) + copy_data)
        # ANNO
        for anno in self.annotations:
            anno_data = self._write_string(anno)
            chunks.append(b'ANNO' + struct.pack('>I', len(anno_data)) + anno_data)
        # AESD
        if self.aes_data:
            chunks.append(b'AESD' + struct.pack('>I', 24) + self.aes_data)
        # MIDI
        if self.midi_data:
            midi_pad = b'\x00' if len(self.midi_data) % 2 == 1 else b''
            chunks.append(b'MIDI' + struct.pack('>I', len(self.midi_data)) + self.midi_data + midi_pad)
        # APPL
        for sig, data in self.app_specific:
            app_data = sig + data
            app_pad = b'\x00' if len(app_data) % 2 == 1 else b''
            chunks.append(b'APPL' + struct.pack('>I', len(app_data)) + app_data + app_pad)
        # Form
        all_chunks = b''.join(chunks)
        form_size = len(all_chunks) + 4  # + formType
        with open(file_path, 'wb') as f:
            f.write(b'FORM' + struct.pack('>I', form_size) + b'AIFC' + all_chunks)

3. Java Class

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;

public class AIFCFile {
    public long formatVersionTimestamp = 0;
    public short numChannels = 0;
    public long numSampleFrames = 0;
    public short sampleSize = 0;
    public double sampleRate = 0.0; // Approximate extended as double
    public byte[] compressionType = new byte[4];
    public String compressionName = "";
    public long soundDataOffset = 0;
    public long soundDataBlockSize = 0;
    public List<Marker> markers = new ArrayList<>();
    public List<Comment> comments = new ArrayList<>();
    public Instrument instrument = new Instrument();
    public String name = "";
    public String author = "";
    public String copyright = "";
    public List<String> annotations = new ArrayList<>();
    public byte[] aesData = new byte[24];
    public byte[] midiData = new byte[0];
    public List<AppSpecific> appSpecific = new ArrayList<>();
    public byte[] soundData = new byte[0]; // Raw sound data

    public static class Marker {
        short id;
        long position;
        String name;
    }

    public static class Comment {
        long timestamp;
        short markerId;
        String text;
    }

    public static class Instrument {
        byte baseNote = 0;
        byte detune = 0;
        byte lowNote = 0;
        byte highNote = 0;
        byte lowVelocity = 0;
        byte highVelocity = 0;
        short gain = 0;
        Loop sustainLoop = new Loop();
        Loop releaseLoop = new Loop();
    }

    public static class Loop {
        short playMode = 0;
        short begin = 0;
        short end = 0;
    }

    public static class AppSpecific {
        byte[] signature = new byte[4];
        byte[] data;
    }

    private double readExtended(DataInputStream dis) throws IOException {
        short exp = dis.readShort();
        long mantHi = dis.readInt() & 0xFFFFFFFFL;
        long mantLo = dis.readInt() & 0xFFFFFFFFL;
        long mant = (mantHi << 32) | mantLo;
        return (mant / Math.pow(2, 63)) * Math.pow(2, exp - 16383);
    }

    private void writeExtended(DataOutputStream dos, double value) throws IOException {
        if (value == 0) {
            dos.write(new byte[10]);
            return;
        }
        boolean sign = value < 0;
        value = Math.abs(value);
        int exp = (int) Math.floor(Math.log(value) / Math.log(2)) + 16383;
        double mant = value / Math.pow(2, exp - 16383);
        long hi = (long) (mant * (1L << 31));
        long lo = (long) ((mant * (1L << 31) - hi) * (1L << 32));
        short signExp = (short) ((sign ? 0x8000 : 0) | (exp & 0x7FFF));
        dos.writeShort(signExp);
        dos.writeInt((int) hi);
        dos.writeInt((int) lo);
    }

    private String readPString(DataInputStream dis) throws IOException {
        int count = dis.readUnsignedByte();
        byte[] bytes = new byte[count];
        dis.readFully(bytes);
        if (count % 2 == 0) dis.readByte(); // Pad
        return new String(bytes, "ASCII");
    }

    private void writePString(DataOutputStream dos, String s) throws IOException {
        byte[] bytes = s.getBytes("ASCII");
        dos.writeByte(bytes.length);
        dos.write(bytes);
        if (bytes.length % 2 == 0) dos.writeByte(0); // Pad
    }

    private String readString(DataInputStream dis, int size) throws IOException {
        byte[] bytes = new byte[size];
        dis.readFully(bytes);
        if (size % 2 == 1) dis.readByte(); // Pad
        return new String(bytes, "ASCII").trim();
    }

    private void writeString(DataOutputStream dos, String s) throws IOException {
        byte[] bytes = s.getBytes("ASCII");
        dos.write(bytes);
        if (bytes.length % 2 == 1) dos.writeByte(0); // Pad
    }

    public void read(String filePath) throws IOException {
        try (FileInputStream fis = new FileInputStream(filePath);
             DataInputStream dis = new DataInputStream(fis)) {
            byte[] ckId = new byte[4];
            dis.readFully(ckId);
            if (!new String(ckId).equals("FORM")) throw new IOException("Not AIFC");
            int ckSize = dis.readInt();
            dis.readFully(ckId);
            if (!new String(ckId).equals("AIFC")) throw new IOException("Not AIFC");
            long endPos = fis.getChannel().position() + ckSize - 4;
            while (fis.getChannel().position() < endPos) {
                dis.readFully(ckId);
                if (ckId[0] == 0) break;
                int chunkSize = dis.readInt();
                long startPos = fis.getChannel().position();
                String chunkIdStr = new String(ckId);
                switch (chunkIdStr) {
                    case "FVER":
                        formatVersionTimestamp = dis.readUnsignedInt();
                        break;
                    case "COMM":
                        numChannels = dis.readShort();
                        numSampleFrames = dis.readUnsignedInt();
                        sampleSize = dis.readShort();
                        sampleRate = readExtended(dis);
                        dis.readFully(compressionType);
                        compressionName = readPString(dis);
                        break;
                    case "SSND":
                        soundDataOffset = dis.readUnsignedInt();
                        soundDataBlockSize = dis.readUnsignedInt();
                        dis.skip(soundDataOffset);
                        int dataSize = chunkSize - 8 - (int) soundDataOffset;
                        soundData = new byte[dataSize];
                        dis.readFully(soundData);
                        break;
                    case "MARK":
                        int numMarkers = dis.readUnsignedShort();
                        for (int i = 0; i < numMarkers; i++) {
                            Marker m = new Marker();
                            m.id = dis.readShort();
                            m.position = dis.readUnsignedInt();
                            m.name = readPString(dis);
                            markers.add(m);
                        }
                        break;
                    case "COMT":
                        int numComm = dis.readUnsignedShort();
                        for (int i = 0; i < numComm; i++) {
                            Comment c = new Comment();
                            c.timestamp = dis.readUnsignedInt();
                            c.markerId = dis.readShort();
                            int count = dis.readUnsignedShort();
                            c.text = readString(dis, count);
                            comments.add(c);
                        }
                        break;
                    case "INST":
                        instrument.baseNote = dis.readByte();
                        instrument.detune = dis.readByte();
                        instrument.lowNote = dis.readByte();
                        instrument.highNote = dis.readByte();
                        instrument.lowVelocity = dis.readByte();
                        instrument.highVelocity = dis.readByte();
                        instrument.gain = dis.readShort();
                        instrument.sustainLoop.playMode = dis.readShort();
                        instrument.sustainLoop.begin = dis.readShort();
                        instrument.sustainLoop.end = dis.readShort();
                        instrument.releaseLoop.playMode = dis.readShort();
                        instrument.releaseLoop.begin = dis.readShort();
                        instrument.releaseLoop.end = dis.readShort();
                        break;
                    case "NAME":
                        name = readString(dis, chunkSize);
                        break;
                    case "AUTH":
                        author = readString(dis, chunkSize);
                        break;
                    case "(c) ":
                        copyright = readString(dis, chunkSize);
                        break;
                    case "ANNO":
                        annotations.add(readString(dis, chunkSize));
                        break;
                    case "AESD":
                        dis.readFully(aesData);
                        break;
                    case "MIDI":
                        midiData = new byte[chunkSize];
                        dis.readFully(midiData);
                        break;
                    case "APPL":
                        AppSpecific app = new AppSpecific();
                        dis.readFully(app.signature);
                        app.data = new byte[chunkSize - 4];
                        dis.readFully(app.data);
                        appSpecific.add(app);
                        break;
                    default:
                        dis.skip(chunkSize);
                }
                long readBytes = fis.getChannel().position() - startPos;
                if (readBytes % 2 == 1) dis.readByte();
            }
        }
    }

    public void write(String filePath) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream tempDos = new DataOutputStream(baos);
        // FVER
        tempDos.writeBytes("FVER");
        tempDos.writeInt(4);
        tempDos.writeInt((int) formatVersionTimestamp);
        // COMM
        ByteArrayOutputStream commBaos = new ByteArrayOutputStream();
        DataOutputStream commDos = new DataOutputStream(commBaos);
        commDos.writeShort(numChannels);
        commDos.writeInt((int) numSampleFrames);
        commDos.writeShort(sampleSize);
        writeExtended(commDos, sampleRate);
        commDos.write(compressionType);
        writePString(commDos, compressionName);
        byte[] commData = commBaos.toByteArray();
        tempDos.writeBytes("COMM");
        tempDos.writeInt(commData.length);
        tempDos.write(commData);
        // SSND
        ByteArrayOutputStream ssndBaos = new ByteArrayOutputStream();
        DataOutputStream ssndDos = new DataOutputStream(ssndBaos);
        ssndDos.writeInt((int) soundDataOffset);
        ssndDos.writeInt((int) soundDataBlockSize);
        ssndDos.write(soundData);
        byte[] ssndData = ssndBaos.toByteArray();
        if (ssndData.length % 2 == 1) {
            tempDos.writeBytes("SSND");
            tempDos.writeInt(ssndData.length + 1);
            tempDos.write(ssndData);
            tempDos.writeByte(0);
        } else {
            tempDos.writeBytes("SSND");
            tempDos.writeInt(ssndData.length);
            tempDos.write(ssndData);
        }
        // MARK
        if (!markers.isEmpty()) {
            ByteArrayOutputStream markBaos = new ByteArrayOutputStream();
            DataOutputStream markDos = new DataOutputStream(markBaos);
            markDos.writeShort(markers.size());
            for (Marker m : markers) {
                markDos.writeShort(m.id);
                markDos.writeInt((int) m.position);
                writePString(markDos, m.name);
            }
            byte[] markData = markBaos.toByteArray();
            tempDos.writeBytes("MARK");
            tempDos.writeInt(markData.length);
            tempDos.write(markData);
        }
        // COMT
        if (!comments.isEmpty()) {
            ByteArrayOutputStream comtBaos = new ByteArrayOutputStream();
            DataOutputStream comtDos = new DataOutputStream(comtBaos);
            comtDos.writeShort(comments.size());
            for (Comment c : comments) {
                comtDos.writeInt((int) c.timestamp);
                comtDos.writeShort(c.markerId);
                byte[] textBytes = c.text.getBytes("ASCII");
                comtDos.writeShort(textBytes.length);
                comtDos.write(textBytes);
                if (textBytes.length % 2 == 1) comtDos.writeByte(0);
            }
            byte[] comtData = comtBaos.toByteArray();
            tempDos.writeBytes("COMT");
            tempDos.writeInt(comtData.length);
            tempDos.write(comtData);
        }
        // INST
        if (instrument.baseNote != 0 || instrument.gain != 0 /* etc. */) {
            ByteArrayOutputStream instBaos = new ByteArrayOutputStream();
            DataOutputStream instDos = new DataOutputStream(instBaos);
            instDos.writeByte(instrument.baseNote);
            instDos.writeByte(instrument.detune);
            instDos.writeByte(instrument.lowNote);
            instDos.writeByte(instrument.highNote);
            instDos.writeByte(instrument.lowVelocity);
            instDos.writeByte(instrument.highVelocity);
            instDos.writeShort(instrument.gain);
            instDos.writeShort(instrument.sustainLoop.playMode);
            instDos.writeShort(instrument.sustainLoop.begin);
            instDos.writeShort(instrument.sustainLoop.end);
            instDos.writeShort(instrument.releaseLoop.playMode);
            instDos.writeShort(instrument.releaseLoop.begin);
            instDos.writeShort(instrument.releaseLoop.end);
            byte[] instData = instBaos.toByteArray();
            tempDos.writeBytes("INST");
            tempDos.writeInt(instData.length);
            tempDos.write(instData);
        }
        // NAME
        if (!name.isEmpty()) {
            byte[] nameData = name.getBytes("ASCII");
            tempDos.writeBytes("NAME");
            tempDos.writeInt(nameData.length);
            tempDos.write(nameData);
            if (nameData.length % 2 == 1) tempDos.writeByte(0);
        }
        // AUTH
        if (!author.isEmpty()) {
            byte[] authData = author.getBytes("ASCII");
            tempDos.writeBytes("AUTH");
            tempDos.writeInt(authData.length);
            tempDos.write(authData);
            if (authData.length % 2 == 1) tempDos.writeByte(0);
        }
        // (c) 
        if (!copyright.isEmpty()) {
            byte[] copyData = copyright.getBytes("ASCII");
            tempDos.writeBytes("(c) ");
            tempDos.writeInt(copyData.length);
            tempDos.write(copyData);
            if (copyData.length % 2 == 1) tempDos.writeByte(0);
        }
        // ANNO
        for (String anno : annotations) {
            byte[] annoData = anno.getBytes("ASCII");
            tempDos.writeBytes("ANNO");
            tempDos.writeInt(annoData.length);
            tempDos.write(annoData);
            if (annoData.length % 2 == 1) tempDos.writeByte(0);
        }
        // AESD
        if (aesData.length > 0) {
            tempDos.writeBytes("AESD");
            tempDos.writeInt(24);
            tempDos.write(aesData);
        }
        // MIDI
        if (midiData.length > 0) {
            tempDos.writeBytes("MIDI");
            tempDos.writeInt(midiData.length);
            tempDos.write(midiData);
            if (midiData.length % 2 == 1) tempDos.writeByte(0);
        }
        // APPL
        for (AppSpecific app : appSpecific) {
            ByteArrayOutputStream appBaos = new ByteArrayOutputStream();
            DataOutputStream appDos = new DataOutputStream(appBaos);
            appDos.write(app.signature);
            appDos.write(app.data);
            byte[] appData = appBaos.toByteArray();
            tempDos.writeBytes("APPL");
            tempDos.writeInt(appData.length);
            tempDos.write(appData);
            if (appData.length % 2 == 1) tempDos.writeByte(0);
        }
        // Write FORM
        byte[] allChunks = baos.toByteArray();
        try (FileOutputStream fos = new FileOutputStream(filePath);
             DataOutputStream dos = new DataOutputStream(fos)) {
            dos.writeBytes("FORM");
            dos.writeInt(allChunks.length + 4); // + AIFC
            dos.writeBytes("AIFC");
            dos.write(allChunks);
        }
    }
}

4. JavaScript Class

Assuming Node.js for fs and Buffer.

const fs = require('fs');

class AIFCFile {
    constructor() {
        this.formatVersionTimestamp = 0;
        this.numChannels = 0;
        this.numSampleFrames = 0;
        this.sampleSize = 0;
        this.sampleRate = 0.0; // Approximate as number
        this.compressionType = Buffer.alloc(4);
        this.compressionName = '';
        this.soundDataOffset = 0;
        this.soundDataBlockSize = 0;
        this.markers = []; // Array of {id, position, name}
        this.comments = []; // Array of {timestamp, markerId, text}
        this.instrument = {
            baseNote: 0, detune: 0, lowNote: 0, highNote: 0,
            lowVelocity: 0, highVelocity: 0, gain: 0,
            sustainLoop: {playMode: 0, begin: 0, end: 0},
            releaseLoop: {playMode: 0, begin: 0, end: 0}
        };
        this.name = '';
        this.author = '';
        this.copyright = '';
        this.annotations = [];
        this.aesData = Buffer.alloc(0);
        this.midiData = Buffer.alloc(0);
        this.appSpecific = []; // Array of {signature: Buffer(4), data: Buffer}
        this.soundData = Buffer.alloc(0);
    }

    _readExtended(buffer, offset) {
        const exp = buffer.readInt16BE(offset);
        const hi = buffer.readUInt32BE(offset + 2);
        const lo = buffer.readUInt32BE(offset + 6);
        const mant = (hi * 2**32 + lo) / 2**63;
        return mant * 2 ** (exp - 16383);
    }

    _writeExtended(value) {
        const buf = Buffer.alloc(10);
        if (value === 0) return buf;
        const sign = value < 0 ? 0x8000 : 0;
        value = Math.abs(value);
        let exp = Math.floor(Math.log2(value)) + 16383;
        let mant = value / 2 ** (exp - 16383);
        const hi = Math.floor(mant * 2**31);
        const lo = Math.floor((mant * 2**31 - hi) * 2**32);
        buf.writeInt16BE(sign | (exp & 0x7FFF), 0);
        buf.writeUInt32BE(hi, 2);
        buf.writeUInt32BE(lo, 6);
        return buf;
    }

    _readPstring(buffer, offset) {
        let count = buffer.readUInt8(offset);
        let s = buffer.toString('ascii', offset + 1, offset + 1 + count);
        return {str: s, len: count + 1 + (count % 2 === 0 ? 1 : 0)};
    }

    _writePstring(s) {
        const bytes = Buffer.from(s, 'ascii');
        const count = bytes.length;
        const pad = count % 2 === 0 ? Buffer.from([0]) : Buffer.alloc(0);
        return Buffer.concat([Buffer.from([count]), bytes, pad]);
    }

    _readString(buffer, offset, size) {
        let s = buffer.toString('ascii', offset, offset + size).replace(/\0+$/, '');
        return {str: s, len: size + (size % 2 === 1 ? 1 : 0)};
    }

    _writeString(s) {
        const bytes = Buffer.from(s, 'ascii');
        const pad = bytes.length % 2 === 1 ? Buffer.from([0]) : Buffer.alloc(0);
        return Buffer.concat([bytes, pad]);
    }

    read(filePath) {
        const data = fs.readFileSync(filePath);
        let pos = 0;
        let ckId = data.toString('ascii', pos, pos + 4);
        pos += 4;
        if (ckId !== 'FORM') throw new Error('Not AIFC');
        let ckSize = data.readUInt32BE(pos);
        pos += 4;
        let formType = data.toString('ascii', pos, pos + 4);
        pos += 4;
        if (formType !== 'AIFC') throw new Error('Not AIFC');
        const endPos = pos + ckSize - 4;
        while (pos < endPos) {
            ckId = data.toString('ascii', pos, pos + 4);
            pos += 4;
            if (ckId.charCodeAt(0) === 0) break;
            ckSize = data.readUInt32BE(pos);
            pos += 4;
            const startPos = pos;
            switch (ckId) {
                case 'FVER':
                    this.formatVersionTimestamp = data.readUInt32BE(pos);
                    pos += 4;
                    break;
                case 'COMM':
                    this.numChannels = data.readInt16BE(pos);
                    pos += 2;
                    this.numSampleFrames = data.readUInt32BE(pos);
                    pos += 4;
                    this.sampleSize = data.readInt16BE(pos);
                    pos += 2;
                    this.sampleRate = this._readExtended(data, pos);
                    pos += 10;
                    this.compressionType = data.slice(pos, pos + 4);
                    pos += 4;
                    const pname = this._readPstring(data, pos);
                    this.compressionName = pname.str;
                    pos += pname.len;
                    break;
                case 'SSND':
                    this.soundDataOffset = data.readUInt32BE(pos);
                    pos += 4;
                    this.soundDataBlockSize = data.readUInt32BE(pos);
                    pos += 4;
                    pos += this.soundDataOffset;
                    const dataSize = ckSize - 8 - this.soundDataOffset;
                    this.soundData = data.slice(pos, pos + dataSize);
                    pos += dataSize;
                    break;
                case 'MARK':
                    const numMarkers = data.readUInt16BE(pos);
                    pos += 2;
                    for (let i = 0; i < numMarkers; i++) {
                        const m = {};
                        m.id = data.readInt16BE(pos);
                        pos += 2;
                        m.position = data.readUInt32BE(pos);
                        pos += 4;
                        const pstr = this._readPstring(data, pos);
                        m.name = pstr.str;
                        pos += pstr.len;
                        this.markers.push(m);
                    }
                    break;
                case 'COMT':
                    const numComm = data.readUInt16BE(pos);
                    pos += 2;
                    for (let i = 0; i < numComm; i++) {
                        const c = {};
                        c.timestamp = data.readUInt32BE(pos);
                        pos += 4;
                        c.markerId = data.readInt16BE(pos);
                        pos += 2;
                        const count = data.readUInt16BE(pos);
                        pos += 2;
                        const strInfo = this._readString(data, pos, count);
                        c.text = strInfo.str;
                        pos += strInfo.len;
                        this.comments.push(c);
                    }
                    break;
                case 'INST':
                    this.instrument.baseNote = data.readUInt8(pos);
                    pos += 1;
                    this.instrument.detune = data.readInt8(pos);
                    pos += 1;
                    this.instrument.lowNote = data.readUInt8(pos);
                    pos += 1;
                    this.instrument.highNote = data.readUInt8(pos);
                    pos += 1;
                    this.instrument.lowVelocity = data.readUInt8(pos);
                    pos += 1;
                    this.instrument.highVelocity = data.readUInt8(pos);
                    pos += 1;
                    this.instrument.gain = data.readInt16BE(pos);
                    pos += 2;
                    this.instrument.sustainLoop.playMode = data.readInt16BE(pos);
                    pos += 2;
                    this.instrument.sustainLoop.begin = data.readInt16BE(pos);
                    pos += 2;
                    this.instrument.sustainLoop.end = data.readInt16BE(pos);
                    pos += 2;
                    this.instrument.releaseLoop.playMode = data.readInt16BE(pos);
                    pos += 2;
                    this.instrument.releaseLoop.begin = data.readInt16BE(pos);
                    pos += 2;
                    this.instrument.releaseLoop.end = data.readInt16BE(pos);
                    pos += 2;
                    break;
                case 'NAME':
                    const nameInfo = this._readString(data, pos, ckSize);
                    this.name = nameInfo.str;
                    pos += nameInfo.len;
                    break;
                case 'AUTH':
                    const authInfo = this._readString(data, pos, ckSize);
                    this.author = authInfo.str;
                    pos += authInfo.len;
                    break;
                case '(c) ':
                    const copyInfo = this._readString(data, pos, ckSize);
                    this.copyright = copyInfo.str;
                    pos += copyInfo.len;
                    break;
                case 'ANNO':
                    const annoInfo = this._readString(data, pos, ckSize);
                    this.annotations.push(annoInfo.str);
                    pos += annoInfo.len;
                    break;
                case 'AESD':
                    this.aesData = data.slice(pos, pos + 24);
                    pos += 24;
                    break;
                case 'MIDI':
                    this.midiData = data.slice(pos, pos + ckSize);
                    pos += ckSize;
                    break;
                case 'APPL':
                    const app = {};
                    app.signature = data.slice(pos, pos + 4);
                    pos += 4;
                    app.data = data.slice(pos, pos + ckSize - 4);
                    pos += ckSize - 4;
                    this.appSpecific.push(app);
                    break;
                default:
                    pos += ckSize;
            }
            const readBytes = pos - startPos;
            if (readBytes % 2 === 1) pos += 1;
        }
    }

    write(filePath) {
        let chunks = Buffer.alloc(0);
        // FVER
        const fver = Buffer.alloc(12);
        fver.write('FVER', 0, 4, 'ascii');
        fver.writeUInt32BE(4, 4);
        fver.writeUInt32BE(this.formatVersionTimestamp, 8);
        chunks = Buffer.concat([chunks, fver]);
        // COMM
        const commData = Buffer.alloc(18 + this._writePstring(this.compressionName).length);
        let off = 0;
        commData.writeInt16BE(this.numChannels, off); off += 2;
        commData.writeUInt32BE(this.numSampleFrames, off); off += 4;
        commData.writeInt16BE(this.sampleSize, off); off += 2;
        commData.copy(this._writeExtended(this.sampleRate), off); off += 10;
        this.compressionType.copy(commData, off); off += 4;
        this._writePstring(this.compressionName).copy(commData, off);
        const comm = Buffer.alloc(8 + commData.length);
        comm.write('COMM', 0, 4, 'ascii');
        comm.writeUInt32BE(commData.length, 4);
        commData.copy(comm, 8);
        chunks = Buffer.concat([chunks, comm]);
        // SSND
        let ssndData = Buffer.alloc(8 + this.soundData.length);
        ssndData.writeUInt32BE(this.soundDataOffset, 0);
        ssndData.writeUInt32BE(this.soundDataBlockSize, 4);
        this.soundData.copy(ssndData, 8);
        const ssndPad = ssndData.length % 2 === 1 ? Buffer.from([0]) : Buffer.alloc(0);
        const ssnd = Buffer.alloc(8 + ssndData.length + ssndPad.length);
        ssnd.write('SSND', 0, 4, 'ascii');
        ssnd.writeUInt32BE(ssndData.length + ssndPad.length, 4);
        ssndData.copy(ssnd, 8);
        ssndPad.copy(ssnd, 8 + ssndData.length);
        chunks = Buffer.concat([chunks, ssnd]);
        // MARK
        if (this.markers.length > 0) {
            let markData = Buffer.alloc(0);
            const numBuf = Buffer.alloc(2);
            numBuf.writeUInt16BE(this.markers.length, 0);
            markData = Buffer.concat([markData, numBuf]);
            for (let m of this.markers) {
                const mBuf = Buffer.alloc(6);
                mBuf.writeInt16BE(m.id, 0);
                mBuf.writeUInt32BE(m.position, 2);
                markData = Buffer.concat([markData, mBuf, this._writePstring(m.name)]);
            }
            const mark = Buffer.alloc(8 + markData.length);
            mark.write('MARK', 0, 4, 'ascii');
            mark.writeUInt32BE(markData.length, 4);
            markData.copy(mark, 8);
            chunks = Buffer.concat([chunks, mark]);
        }
        // COMT
        if (this.comments.length > 0) {
            let comtData = Buffer.alloc(0);
            const numBuf = Buffer.alloc(2);
            numBuf.writeUInt16BE(this.comments.length, 0);
            comtData = Buffer.concat([comtData, numBuf]);
            for (let c of this.comments) {
                const cBuf = Buffer.alloc(8);
                cBuf.writeUInt32BE(c.timestamp, 0);
                cBuf.writeInt16BE(c.markerId, 4);
                const textBytes = Buffer.from(c.text, 'ascii');
                cBuf.writeUInt16BE(textBytes.length, 6);
                let textPad = textBytes.length % 2 === 1 ? Buffer.from([0]) : Buffer.alloc(0);
                comtData = Buffer.concat([comtData, cBuf, textBytes, textPad]);
            }
            const comt = Buffer.alloc(8 + comtData.length);
            comt.write('COMT', 0, 4, 'ascii');
            comt.writeUInt32BE(comtData.length, 4);
            comtData.copy(comt, 8);
            chunks = Buffer.concat([chunks, comt]);
        }
        // INST
        if (this.instrument.baseNote !== 0 || this.instrument.gain !== 0 /* etc. */) {
            const instData = Buffer.alloc(20);
            instData.writeUInt8(this.instrument.baseNote, 0);
            instData.writeInt8(this.instrument.detune, 1);
            instData.writeUInt8(this.instrument.lowNote, 2);
            instData.writeUInt8(this.instrument.highNote, 3);
            instData.writeUInt8(this.instrument.lowVelocity, 4);
            instData.writeUInt8(this.instrument.highVelocity, 5);
            instData.writeInt16BE(this.instrument.gain, 6);
            instData.writeInt16BE(this.instrument.sustainLoop.playMode, 8);
            instData.writeInt16BE(this.instrument.sustainLoop.begin, 10);
            instData.writeInt16BE(this.instrument.sustainLoop.end, 12);
            instData.writeInt16BE(this.instrument.releaseLoop.playMode, 14);
            instData.writeInt16BE(this.instrument.releaseLoop.begin, 16);
            instData.writeInt16BE(this.instrument.releaseLoop.end, 18);
            const inst = Buffer.alloc(8 + instData.length);
            inst.write('INST', 0, 4, 'ascii');
            inst.writeUInt32BE(instData.length, 4);
            instData.copy(inst, 8);
            chunks = Buffer.concat([chunks, inst]);
        }
        // NAME
        if (this.name) {
            const nameData = this._writeString(this.name);
            const nameChunk = Buffer.alloc(8 + nameData.length);
            nameChunk.write('NAME', 0, 4, 'ascii');
            nameChunk.writeUInt32BE(nameData.length, 4);
            nameData.copy(nameChunk, 8);
            chunks = Buffer.concat([chunks, nameChunk]);
        }
        // AUTH
        if (this.author) {
            const authData = this._writeString(this.author);
            const authChunk = Buffer.alloc(8 + authData.length);
            authChunk.write('AUTH', 0, 4, 'ascii');
            authChunk.writeUInt32BE(authData.length, 4);
            authData.copy(authChunk, 8);
            chunks = Buffer.concat([chunks, authChunk]);
        }
        // (c) 
        if (this.copyright) {
            const copyData = this._writeString(this.copyright);
            const copyChunk = Buffer.alloc(8 + copyData.length);
            copyChunk.write('(c) ', 0, 4, 'ascii');
            copyChunk.writeUInt32BE(copyData.length, 4);
            copyData.copy(copyChunk, 8);
            chunks = Buffer.concat([chunks, copyChunk]);
        }
        // ANNO
        for (let anno of this.annotations) {
            const annoData = this._writeString(anno);
            const annoChunk = Buffer.alloc(8 + annoData.length);
            annoChunk.write('ANNO', 0, 4, 'ascii');
            annoChunk.writeUInt32BE(annoData.length, 4);
            annoData.copy(annoChunk, 8);
            chunks = Buffer.concat([chunks, annoChunk]);
        }
        // AESD
        if (this.aesData.length > 0) {
            const aesChunk = Buffer.alloc(8 + 24);
            aesChunk.write('AESD', 0, 4, 'ascii');
            aesChunk.writeUInt32BE(24, 4);
            this.aesData.copy(aesChunk, 8, 0, 24);
            chunks = Buffer.concat([chunks, aesChunk]);
        }
        // MIDI
        if (this.midiData.length > 0) {
            const midiPad = this.midiData.length % 2 === 1 ? Buffer.from([0]) : Buffer.alloc(0);
            const midiChunk = Buffer.alloc(8 + this.midiData.length + midiPad.length);
            midiChunk.write('MIDI', 0, 4, 'ascii');
            midiChunk.writeUInt32BE(this.midiData.length + midiPad.length, 4);
            this.midiData.copy(midiChunk, 8);
            midiPad.copy(midiChunk, 8 + this.midiData.length);
            chunks = Buffer.concat([chunks, midiChunk]);
        }
        // APPL
        for (let app of this.appSpecific) {
            let appData = Buffer.concat([app.signature, app.data]);
            const appPad = appData.length % 2 === 1 ? Buffer.from([0]) : Buffer.alloc(0);
            const appChunk = Buffer.alloc(8 + appData.length + appPad.length);
            appChunk.write('APPL', 0, 4, 'ascii');
            appChunk.writeUInt32BE(appData.length + appPad.length, 4);
            appData.copy(appChunk, 8);
            appPad.copy(appChunk, 8 + appData.length);
            chunks = Buffer.concat([chunks, appChunk]);
        }
        // FORM
        const formSize = chunks.length + 4; // + AIFC
        const form = Buffer.alloc(12 + chunks.length);
        form.write('FORM', 0, 4, 'ascii');
        form.writeUInt32BE(formSize, 4);
        form.write('AIFC', 8, 4, 'ascii');
        chunks.copy(form, 12);
        fs.writeFileSync(filePath, form);
    }
}

5. C++ Class

#include <fstream>
#include <vector>
#include <string>
#include <cstring>
#include <cmath>
#include <cstdint>

class AIFCFile {
public:
    uint32_t format_version_timestamp = 0;
    int16_t num_channels = 0;
    uint32_t num_sample_frames = 0;
    int16_t sample_size = 0;
    double sample_rate = 0.0; // Approximate
    char compression_type[4] = {0};
    std::string compression_name;
    uint32_t sound_data_offset = 0;
    uint32_t sound_data_block_size = 0;
    struct Marker {
        int16_t id;
        uint32_t position;
        std::string name;
    };
    std::vector<Marker> markers;
    struct Comment {
        uint32_t timestamp;
        int16_t marker_id;
        std::string text;
    };
    std::vector<Comment> comments;
    struct Loop {
        int16_t play_mode = 0;
        int16_t begin = 0;
        int16_t end = 0;
    };
    struct Instrument {
        uint8_t base_note = 0;
        int8_t detune = 0;
        uint8_t low_note = 0;
        uint8_t high_note = 0;
        uint8_t low_velocity = 0;
        uint8_t high_velocity = 0;
        int16_t gain = 0;
        Loop sustain_loop;
        Loop release_loop;
    } instrument;
    std::string name;
    std::string author;
    std::string copyright_str;
    std::vector<std::string> annotations;
    uint8_t aes_data[24] = {0};
    std::vector<uint8_t> midi_data;
    struct AppSpecific {
        char signature[4];
        std::vector<uint8_t> data;
    };
    std::vector<AppSpecific> app_specific;
    std::vector<uint8_t> sound_data;

    AIFCFile() {}

    double read_extended(std::ifstream& f) {
        int16_t exp;
        uint32_t hi, lo;
        f.read(reinterpret_cast<char*>(&exp), 2);
        f.read(reinterpret_cast<char*>(&hi), 4);
        f.read(reinterpret_cast<char*>(&lo), 4);
        if (f.fail()) return 0.0;
        exp = __builtin_bswap16(exp);
        hi = __builtin_bswap32(hi);
        lo = __builtin_bswap32(lo);
        double mant = (static_cast<double>(hi) * (1LL << 32) + lo) / pow(2, 63);
        return mant * pow(2, exp - 16383);
    }

    void write_extended(std::ofstream& f, double value) {
        if (value == 0.0) {
            char zeros[10] = {0};
            f.write(zeros, 10);
            return;
        }
        bool sign = value < 0;
        value = std::abs(value);
        int exp = static_cast<int>(std::floor(std::log2(value))) + 16383;
        double mant = value / std::pow(2, exp - 16383);
        uint32_t hi = static_cast<uint32_t>(mant * (1LL << 31));
        uint32_t lo = static_cast<uint32_t>((mant * (1LL << 31) - hi) * (1LL << 32));
        int16_t sign_exp = (sign ? 0x8000 : 0) | (exp & 0x7FFF);
        sign_exp = __builtin_bswap16(sign_exp);
        hi = __builtin_bswap32(hi);
        lo = __builtin_bswap32(lo);
        f.write(reinterpret_cast<char*>(&sign_exp), 2);
        f.write(reinterpret_cast<char*>(&hi), 4);
        f.write(reinterpret_cast<char*>(&lo), 4);
    }

    std::string read_pstring(std::ifstream& f) {
        uint8_t count;
        f.read(reinterpret_cast<char*>(&count), 1);
        std::string s(count, ' ');
        f.read(&s[0], count);
        if (count % 2 == 0) {
            char pad;
            f.read(&pad, 1);
        }
        return s;
    }

    void write_pstring(std::ofstream& f, const std::string& s) {
        uint8_t count = static_cast<uint8_t>(s.size());
        f.write(reinterpret_cast<const char*>(&count), 1);
        f.write(s.c_str(), count);
        if (count % 2 == 0) {
            char pad = 0;
            f.write(&pad, 1);
        }
    }

    std::string read_string(std::ifstream& f, uint32_t size) {
        std::string s(size, ' ');
        f.read(&s[0], size);
        if (size % 2 == 1) {
            char pad;
            f.read(&pad, 1);
        }
        size_t null_pos = s.find('\0');
        if (null_pos != std::string::npos) s.resize(null_pos);
        return s;
    }

    void write_string(std::ofstream& f, const std::string& s) {
        f.write(s.c_str(), s.size());
        if (s.size() % 2 == 1) {
            char pad = 0;
            f.write(&pad, 1);
        }
    }

    void read(const std::string& file_path) {
        std::ifstream f(file_path, std::ios::binary);
        if (!f) throw std::runtime_error("Cannot open file");
        char ck_id[5] = {0};
        f.read(ck_id, 4);
        if (std::strcmp(ck_id, "FORM") != 0) throw std::runtime_error("Not AIFC");
        uint32_t ck_size;
        f.read(reinterpret_cast<char*>(&ck_size), 4);
        ck_size = __builtin_bswap32(ck_size);
        f.read(ck_id, 4);
        if (std::strcmp(ck_id, "AIFC") != 0) throw std::runtime_error("Not AIFC");
        auto start = f.tellg();
        auto end_pos = start + static_cast<std::streamoff>(ck_size - 4);
        while (f.tellg() < end_pos) {
            f.read(ck_id, 4);
            if (ck_id[0] == 0) break;
            f.read(reinterpret_cast<char*>(&ck_size), 4);
            ck_size = __builtin_bswap32(ck_size);
            auto chunk_start = f.tellg();
            if (std::strcmp(ck_id, "FVER") == 0) {
                f.read(reinterpret_cast<char*>(&format_version_timestamp), 4);
                format_version_timestamp = __builtin_bswap32(format_version_timestamp);
            } else if (std::strcmp(ck_id, "COMM") == 0) {
                f.read(reinterpret_cast<char*>(&num_channels), 2);
                num_channels = __builtin_bswap16(num_channels);
                f.read(reinterpret_cast<char*>(&num_sample_frames), 4);
                num_sample_frames = __builtin_bswap32(num_sample_frames);
                f.read(reinterpret_cast<char*>(&sample_size), 2);
                sample_size = __builtin_bswap16(sample_size);
                sample_rate = read_extended(f);
                f.read(compression_type, 4);
                compression_name = read_pstring(f);
            } else if (std::strcmp(ck_id, "SSND") == 0) {
                f.read(reinterpret_cast<char*>(&sound_data_offset), 4);
                sound_data_offset = __builtin_bswap32(sound_data_offset);
                f.read(reinterpret_cast<char*>(&sound_data_block_size), 4);
                sound_data_block_size = __builtin_bswap32(sound_data_block_size);
                f.seekg(sound_data_offset, std::ios::cur);
                uint32_t data_size = ck_size - 8 - sound_data_offset;
                sound_data.resize(data_size);
                f.read(reinterpret_cast<char*>(sound_data.data()), data_size);
            } else if (std::strcmp(ck_id, "MARK") == 0) {
                uint16_t num_markers;
                f.read(reinterpret_cast<char*>(&num_markers), 2);
                num_markers = __builtin_bswap16(num_markers);
                for (uint16_t i = 0; i < num_markers; ++i) {
                    Marker m;
                    f.read(reinterpret_cast<char*>(&m.id), 2);
                    m.id = __builtin_bswap16(m.id);
                    f.read(reinterpret_cast<char*>(&m.position), 4);
                    m.position = __builtin_bswap32(m.position);
                    m.name = read_pstring(f);
                    markers.push_back(m);
                }
            } else if (std::strcmp(ck_id, "COMT") == 0) {
                uint16_t num_comm;
                f.read(reinterpret_cast<char*>(&num_comm), 2);
                num_comm = __builtin_bswap16(num_comm);
                for (uint16_t i = 0; i < num_comm; ++i) {
                    Comment c;
                    f.read(reinterpret_cast<char*>(&c.timestamp), 4);
                    c.timestamp = __builtin_bswap32(c.timestamp);
                    f.read(reinterpret_cast<char*>(&c.marker_id), 2);
                    c.marker_id = __builtin_bswap16(c.marker_id);
                    uint16_t count;
                    f.read(reinterpret_cast<char*>(&count), 2);
                    count = __builtin_bswap16(count);
                    c.text = read_string(f, count);
                    comments.push_back(c);
                }
            } else if (std::strcmp(ck_id, "INST") == 0) {
                f.read(reinterpret_cast<char*>(&instrument.base_note), 1);
                f.read(reinterpret_cast<char*>(&instrument.detune), 1);
                f.read(reinterpret_cast<char*>(&instrument.low_note), 1);
                f.read(reinterpret_cast<char*>(&instrument.high_note), 1);
                f.read(reinterpret_cast<char*>(&instrument.low_velocity), 1);
                f.read(reinterpret_cast<char*>(&instrument.high_velocity), 1);
                f.read(reinterpret_cast<char*>(&instrument.gain), 2);
                instrument.gain = __builtin_bswap16(instrument.gain);
                f.read(reinterpret_cast<char*>(&instrument.sustain_loop.play_mode), 2);
                instrument.sustain_loop.play_mode = __builtin_bswap16(instrument.sustain_loop.play_mode);
                f.read(reinterpret_cast<char*>(&instrument.sustain_loop.begin), 2);
                instrument.sustain_loop.begin = __builtin_bswap16(instrument.sustain_loop.begin);
                f.read(reinterpret_cast<char*>(&instrument.sustain_loop.end), 2);
                instrument.sustain_loop.end = __builtin_bswap16(instrument.sustain_loop.end);
                f.read(reinterpret_cast<char*>(&instrument.release_loop.play_mode), 2);
                instrument.release_loop.play_mode = __builtin_bswap16(instrument.release_loop.play_mode);
                f.read(reinterpret_cast<char*>(&instrument.release_loop.begin), 2);
                instrument.release_loop.begin = __builtin_bswap16(instrument.release_loop.begin);
                f.read(reinterpret_cast<char*>(&instrument.release_loop.end), 2);
                instrument.release_loop.end = __builtin_bswap16(instrument.release_loop.end);
            } else if (std::strcmp(ck_id, "NAME") == 0) {
                name = read_string(f, ck_size);
            } else if (std::strcmp(ck_id, "AUTH") == 0) {
                author = read_string(f, ck_size);
            } else if (std::strcmp(ck_id, "(c) ") == 0) {
                copyright_str = read_string(f, ck_size);
            } else if (std::strcmp(ck_id, "ANNO") == 0) {
                annotations.push_back(read_string(f, ck_size));
            } else if (std::strcmp(ck_id, "AESD") == 0) {
                f.read(reinterpret_cast<char*>(aes_data), 24);
            } else if (std::strcmp(ck_id, "MIDI") == 0) {
                midi_data.resize(ck_size);
                f.read(reinterpret_cast<char*>(midi_data.data()), ck_size);
            } else if (std::strcmp(ck_id, "APPL") == 0) {
                AppSpecific app;
                f.read(app.signature, 4);
                app.data.resize(ck_size - 4);
                f.read(reinterpret_cast<char*>(app.data.data()), ck_size - 4);
                app_specific.push_back(app);
            } else {
                f.seekg(ck_size, std::ios::cur);
            }
            auto read_bytes = f.tellg() - chunk_start;
            if (static_cast<uint32_t>(read_bytes) % 2 == 1) {
                char pad;
                f.read(&pad, 1);
            }
        }
    }

    void write(const std::string& file_path) {
        std::ofstream f(file_path, std::ios::binary);
        if (!f) throw std::runtime_error("Cannot open file");

        std::vector<uint8_t> chunks;

        // Helper to append chunk
        auto append_chunk = [&](const char* id, const std::vector<uint8_t>& data) {
            uint32_t size_be = __builtin_bswap32(static_cast<uint32_t>(data.size()));
            chunks.insert(chunks.end(), id, id + 4);
            chunks.insert(chunks.end(), reinterpret_cast<uint8_t*>(&size_be), reinterpret_cast<uint8_t*>(&size_be) + 4);
            chunks.insert(chunks.end(), data.begin(), data.end());
        };

        // FVER
        std::vector<uint8_t> fver_data(4);
        uint32_t ts_be = __builtin_bswap32(format_version_timestamp);
        std::memcpy(fver_data.data(), &ts_be, 4);
        append_chunk("FVER", fver_data);

        // COMM
        std::vector<uint8_t> comm_data;
        int16_t nc_be = __builtin_bswap16(num_channels);
        comm_data.insert(comm_data.end(), reinterpret_cast<uint8_t*>(&nc_be), reinterpret_cast<uint8_t*>(&nc_be) + 2);
        uint32_t nsf_be = __builtin_bswap32(num_sample_frames);
        comm_data.insert(comm_data.end(), reinterpret_cast<uint8_t*>(&nsf_be), reinterpret_cast<uint8_t*>(&nsf_be) + 4);
        int16_t ss_be = __builtin_bswap16(sample_size);
        comm_data.insert(comm_data.end(), reinterpret_cast<uint8_t*>(&ss_be), reinterpret_cast<uint8_t*>(&ss_be) + 2);
        // Extended
        std::vector<uint8_t> ext(10);
        // Simulate write_extended by buffering
        std::stringstream ss;
        write_extended(ss, sample_rate);
        ss.read(reinterpret_cast<char*>(ext.data()), 10);
        comm_data.insert(comm_data.end(), ext.begin(), ext.end());
        comm_data.insert(comm_data.end(), compression_type, compression_type + 4);
        // Pstring
        uint8_t count = static_cast<uint8_t>(compression_name.size());
        comm_data.push_back(count);
        comm_data.insert(comm_data.end(), compression_name.begin(), compression_name.end());
        if (count % 2 == 0) comm_data.push_back(0);
        append_chunk("COMM", comm_data);

        // SSND
        std::vector<uint8_t> ssnd_data;
        uint32_t off_be = __builtin_bswap32(sound_data_offset);
        ssnd_data.insert(ssnd_data.end(), reinterpret_cast<uint8_t*>(&off_be), reinterpret_cast<uint8_t*>(&off_be) + 4);
        uint32_t bs_be = __builtin_bswap32(sound_data_block_size);
        ssnd_data.insert(ssnd_data.end(), reinterpret_cast<uint8_t*>(&bs_be), reinterpret_cast<uint8_t*>(&bs_be) + 4);
        ssnd_data.insert(ssnd_data.end(), sound_data.begin(), sound_data.end());
        if (ssnd_data.size() % 2 == 1) ssnd_data.push_back(0);
        append_chunk("SSND", ssnd_data);

        // MARK
        if (!markers.empty()) {
            std::vector<uint8_t> mark_data;
            uint16_t num_be = __builtin_bswap16(static_cast<uint16_t>(markers.size()));
            mark_data.insert(mark_data.end(), reinterpret_cast<uint8_t*>(&num_be), reinterpret_cast<uint8_t*>(&num_be) + 2);
            for (const auto& m : markers) {
                int16_t id_be = __builtin_bswap16(m.id);
                mark_data.insert(mark_data.end(), reinterpret_cast<uint8_t*>(&id_be), reinterpret_cast<uint8_t*>(&id_be) + 2);
                uint32_t pos_be = __builtin_bswap32(m.position);
                mark_data.insert(mark_data.end(), reinterpret_cast<uint8_t*>(&pos_be), reinterpret_cast<uint8_t*>(&pos_be) + 4);
                uint8_t cnt = static_cast<uint8_t>(m.name.size());
                mark_data.push_back(cnt);
                mark_data.insert(mark_data.end(), m.name.begin(), m.name.end());
                if (cnt % 2 == 0) mark_data.push_back(0);
            }
            append_chunk("MARK", mark_data);
        }

        // COMT
        if (!comments.empty()) {
            std::vector<uint8_t> comt_data;
            uint16_t num_be = __builtin_bswap16(static_cast<uint16_t>(comments.size()));
            comt_data.insert(comt_data.end(), reinterpret_cast<uint8_t*>(&num_be), reinterpret_cast<uint8_t*>(&num_be) + 2);
            for (const auto& c : comments) {
                uint32_t ts_be = __builtin_bswap32(c.timestamp);
                comt_data.insert(comt_data.end(), reinterpret_cast<uint8_t*>(&ts_be), reinterpret_cast<uint8_t*>(&ts_be) + 4);
                int16_t mid_be = __builtin_bswap16(c.marker_id);
                comt_data.insert(comt_data.end(), reinterpret_cast<uint8_t*>(&mid_be), reinterpret_cast<uint8_t*>(&mid_be) + 2);
                uint16_t cnt_be = __builtin_bswap16(static_cast<uint16_t>(c.text.size()));
                comt_data.insert(comt_data.end(), reinterpret_cast<uint8_t*>(&cnt_be), reinterpret_cast<uint8_t*>(&cnt_be) + 2);
                comt_data.insert(comt_data.end(), c.text.begin(), c.text.end());
                if (c.text.size() % 2 == 1) comt_data.push_back(0);
            }
            append_chunk("COMT", comt_data);
        }

        // INST
        if (instrument.base_note != 0 || instrument.gain != 0 /* etc. */) {
            std::vector<uint8_t> inst_data(20);
            inst_data[0] = instrument.base_note;
            inst_data[1] = static_cast<uint8_t>(instrument.detune);
            inst_data[2] = instrument.low_note;
            inst_data[3] = instrument.high_note;
            inst_data[4] = instrument.low_velocity;
            inst_data[5] = instrument.high_velocity;
            int16_t gain_be = __builtin_bswap16(instrument.gain);
            std::memcpy(&inst_data[6], &gain_be, 2);
            int16_t pm_be = __builtin_bswap16(instrument.sustain_loop.play_mode);
            std::memcpy(&inst_data[8], &pm_be, 2);
            int16_t b_be = __builtin_bswap16(instrument.sustain_loop.begin);
            std::memcpy(&inst_data[10], &b_be, 2);
            int16_t e_be = __builtin_bswap16(instrument.sustain_loop.end);
            std::memcpy(&inst_data[12], &e_be, 2);
            pm_be = __builtin_bswap16(instrument.release_loop.play_mode);
            std::memcpy(&inst_data[14], &pm_be, 2);
            b_be = __builtin_bswap16(instrument.release_loop.begin);
            std::memcpy(&inst_data[16], &b_be, 2);
            e_be = __builtin_bswap16(instrument.release_loop.end);
            std::memcpy(&inst_data[18], &e_be, 2);
            append_chunk("INST", inst_data);
        }

        // NAME
        if (!name.empty()) {
            std::vector<uint8_t> name_data(name.begin(), name.end());
            if (name_data.size() % 2 == 1) name_data.push_back(0);
            append_chunk("NAME", name_data);
        }

        // AUTH
        if (!author.empty()) {
            std::vector<uint8_t> auth_data(author.begin(), author.end());
            if (auth_data.size() % 2 == 1) auth_data.push_back(0);
            append_chunk("AUTH", auth_data);
        }

        // (c) 
        if (!copyright_str.empty()) {
            std::vector<uint8_t> copy_data(copyright_str.begin(), copyright_str.end());
            if (copy_data.size() % 2 == 1) copy_data.push_back(0);
            append_chunk("(c) ", copy_data);
        }

        // ANNO
        for (const auto& anno : annotations) {
            std::vector<uint8_t> anno_data(anno.begin(), anno.end());
            if (anno_data.size() % 2 == 1) anno_data.push_back(0);
            append_chunk("ANNO", anno_data);
        }

        // AESD
        if (std::any_of(std::begin(aes_data), std::end(aes_data), [](uint8_t b){return b != 0;})) {
            std::vector<uint8_t> aes_vec(std::begin(aes_data), std::end(aes_data));
            append_chunk("AESD", aes_vec);
        }

        // MIDI
        if (!midi_data.empty()) {
            std::vector<uint8_t> midi_copy = midi_data;
            if (midi_copy.size() % 2 == 1) midi_copy.push_back(0);
            append_chunk("MIDI", midi_copy);
        }

        // APPL
        for (const auto& app : app_specific) {
            std::vector<uint8_t> app_data;
            app_data.insert(app_data.end(), app.signature, app.signature + 4);
            app_data.insert(app_data.end(), app.data.begin(), app.data.end());
            if (app_data.size() % 2 == 1) app_data.push_back(0);
            append_chunk("APPL", app_data);
        }

        // Write FORM
        f.write("FORM", 4);
        uint32_t form_size = static_cast<uint32_t>(chunks.size()) + 4; // + AIFC
        uint32_t form_size_be = __builtin_bswap32(form_size);
        f.write(reinterpret_cast<char*>(&form_size_be), 4);
        f.write("AIFC", 4);
        f.write(reinterpret_cast<char*>(chunks.data()), chunks.size());
    }
};