Task 015: .ADX File Format

Task 015: .ADX File Format

Clarification on .ADX File Format

The .ADX file extension is ambiguous and associated with multiple formats, primarily:

  1. CRI ADX Audio Format: A proprietary, lossy audio compression format developed by CRI Middleware for video games, derived from ADPCM (Adaptive Differential Pulse-Code Modulation). It is commonly used for background music and sound effects in games, notably on platforms like Sega Dreamcast, PlayStation 2, GameCube, and Wii. Its key feature is a looping function for seamless playback in games.
  2. Lotus Approach Index File: A data file used by IBM Lotus Approach for indexing relational databases to speed up record retrieval.
  3. Other Uses: Less common uses include Aodix project files, MSX Sound Performance System files, and others, but these are niche and less relevant for general programming tasks.

Given the context of video game audio (common in programming tasks) and the need for decoding, reading, and writing, I will assume the CRI ADX audio format is the intended target. However, the CRI ADX specification is proprietary, and detailed documentation is not freely available. Reverse-engineered information provides partial insights into its structure. If you meant a different .ADX format (e.g., Lotus Approach), please clarify, and I can adjust the response accordingly.

Task 1: Properties of the CRI ADX File Format

Based on reverse-engineered information, the CRI ADX audio file format has the following intrinsic properties relevant to its file structure:

File Header:

  • Magic Number: Starts with 0x8000 (2 bytes) to identify the file as ADX.
  • Header Size: Specifies the size of the header (typically 32 bytes for standard ADX).
  • Encoding Type: Indicates the compression type (e.g., 0x03 for standard ADX ADPCM).
  • Block Size: Size of each audio block (usually 18 bytes for 32 samples).
  • Sample Bit Depth: Number of bits per sample (typically 4 bits for ADPCM).
  • Channel Count: Number of audio channels (1 for mono, 2 for stereo).
  • Sample Rate: Frequency of the audio (e.g., 44100 Hz).
  • Total Samples: Total number of samples in the audio stream.
  • Highpass Frequency: Used for filtering during decoding.
  • Version: ADX version (1 or 2).
  • Flags: Additional settings (e.g., loop enable flag).
  • Loop Information:
  • Loop Enabled: Boolean indicating if looping is enabled.
  • Loop Start Sample: Sample index where the loop begins.
  • Loop End Sample: Sample index where the loop ends.

Audio Data:

  • ADPCM Compressed Data: Audio samples encoded using a 4-bit ADPCM variant, stored in blocks.
  • Block Structure: Each block contains 32 samples per channel, with 18 bytes per block for stereo (17 bytes of data plus 1 byte for scale factor).
  • Scale Factor: A per-block value used in ADPCM decoding to adjust amplitude.

Metadata:

  • Copyright String: A string (e.g., "(c)CRI") stored after the header, followed by audio data.
  • Optional AFS Container: ADX files may be bundled in an AFS archive, which uses numerical indices instead of filenames.

MIME Type: audio/x-adx.

Looping Function: Designed for seamless looping, critical for video game background music.

Encryption (Optional): Some ADX files may include encryption, especially in newer versions (ADX2).

Sibling Format (AHX): A related format for voice recordings using MPEG-2 audio, often bundled with ADX in AFS archives.

ADX2 Extensions: Version 2 uses HCA/HCA-MX codecs, typically in ACB/AWB container files.

Note: The exact byte-level structure (e.g., offsets for each field) is not fully public, and tools like vgmstream are often used to decode ADX files based on reverse-engineered specs. For this response, I’ll focus on the above properties for reading and writing.

Task 2: Python Class for CRI ADX

Below is a Python class that reads, decodes, and writes CRI ADX audio files, printing the identified properties. Due to the proprietary nature, this implementation assumes a basic reverse-engineered structure and uses vgmstream or similar logic for decoding. Writing is limited to basic header modification and data copying, as full encoding requires proprietary tools.

import struct
import os

class ADXFile:
    def __init__(self, filepath):
        self.filepath = filepath
        self.magic = None
        self.header_size = None
        self.encoding = None
        self.block_size = None
        self.sample_bit_depth = None
        self.channel_count = None
        self.sample_rate = None
        self.total_samples = None
        self.highpass_freq = None
        self.version = None
        self.flags = None
        self.loop_enabled = None
        self.loop_start = None
        self.loop_end = None
        self.copyright = None
        self.audio_data = None

    def read(self):
        """Read and decode ADX file properties."""
        with open(self.filepath, 'rb') as f:
            # Read header (first 32 bytes for standard ADX)
            header = f.read(32)
            if len(header) < 32:
                raise ValueError("File too small to be a valid ADX file")

            # Parse header
            self.magic = struct.unpack('>H', header[0:2])[0]
            if self.magic != 0x8000:
                raise ValueError("Invalid ADX file: incorrect magic number")

            self.header_size = struct.unpack('>H', header[2:4])[0]
            self.encoding = struct.unpack('B', header[4:5])[0]
            self.block_size = struct.unpack('B', header[5:6])[0]
            self.sample_bit_depth = struct.unpack('B', header[6:7])[0]
            self.channel_count = struct.unpack('B', header[7:8])[0]
            self.sample_rate = struct.unpack('>I', header[8:12])[0]
            self.total_samples = struct.unpack('>I', header[12:16])[0]
            self.highpass_freq = struct.unpack('>H', header[16:18])[0]
            self.version = struct.unpack('B', header[18:19])[0]
            self.flags = struct.unpack('B', header[19:20])[0]

            # Loop information (assuming standard layout)
            self.loop_enabled = struct.unpack('>H', header[20:22])[0]
            self.loop_start = struct.unpack('>I', header[22:26])[0]
            self.loop_end = struct.unpack('>I', header[26:30])[0]

            # Read copyright string
            f.seek(self.header_size - 4)
            copyright_length = struct.unpack('>H', f.read(2))[0]
            self.copyright = f.read(copyright_length).decode('ascii')

            # Read audio data
            f.seek(self.header_size)
            self.audio_data = f.read()

    def decode_audio(self):
        """Decode ADPCM audio data (simplified, requires external library like vgmstream)."""
        print("Decoding requires external tools like vgmstream. Raw data size:", len(self.audio_data))
        # Placeholder: Actual decoding needs proprietary or reverse-engineered ADPCM logic
        return self.audio_data

    def write(self, output_path):
        """Write ADX file with current properties."""
        with open(output_path, 'wb') as f:
            # Write header
            f.write(struct.pack('>H', self.magic))
            f.write(struct.pack('>H', self.header_size))
            f.write(struct.pack('B', self.encoding))
            f.write(struct.pack('B', self.block_size))
            f.write(struct.pack('B', self.sample_bit_depth))
            f.write(struct.pack('B', self.channel_count))
            f.write(struct.pack('>I', self.sample_rate))
            f.write(struct.pack('>I', self.total_samples))
            f.write(struct.pack('>H', self.highpass_freq))
            f.write(struct.pack('B', self.version))
            f.write(struct.pack('B', self.flags))
            f.write(struct.pack('>H', self.loop_enabled))
            f.write(struct.pack('>I', self.loop_start))
            f.write(struct.pack('>I', self.loop_end))
            # Padding to header_size - 4
            f.write(b'\x00' * (self.header_size - 32 - 4))
            # Write copyright
            f.write(struct.pack('>H', len(self.copyright)))
            f.write(self.copyright.encode('ascii'))
            # Write audio data
            f.write(self.audio_data)

    def print_properties(self):
        """Print all ADX file properties."""
        print(f"ADX File: {self.filepath}")
        print(f"Magic Number: 0x{self.magic:04X}")
        print(f"Header Size: {self.header_size} bytes")
        print(f"Encoding Type: {self.encoding}")
        print(f"Block Size: {self.block_size} bytes")
        print(f"Sample Bit Depth: {self.sample_bit_depth} bits")
        print(f"Channel Count: {self.channel_count}")
        print(f"Sample Rate: {self.sample_rate} Hz")
        print(f"Total Samples: {self.total_samples}")
        print(f"Highpass Frequency: {self.highpass_freq} Hz")
        print(f"Version: {self.version}")
        print(f"Flags: 0x{self.flags:02X}")
        print(f"Loop Enabled: {bool(self.loop_enabled)}")
        print(f"Loop Start Sample: {self.loop_start}")
        print(f"Loop End Sample: {self.loop_end}")
        print(f"Copyright: {self.copyright}")
        print(f"Audio Data Size: {len(self.audio_data)} bytes")

# Example usage
if __name__ == "__main__":
    try:
        adx = ADXFile("example.adx")
        adx.read()
        adx.print_properties()
        adx.decode_audio()  # Placeholder for decoding
        adx.write("output.adx")
    except Exception as e:
        print(f"Error: {e}")

Notes:

  • The decode_audio method is a placeholder, as full ADPCM decoding requires complex logic or external libraries like vgmstream.
  • Writing is limited to copying existing audio data, as encoding new ADX audio requires proprietary tools from CRI Middleware.
  • The class assumes a standard ADX header layout based on reverse-engineered data.

Task 3: Java Class for CRI ADX

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class ADXFile {
    private String filepath;
    private int magic;
    private int headerSize;
    private byte encoding;
    private byte blockSize;
    private byte sampleBitDepth;
    private byte channelCount;
    private int sampleRate;
    private int totalSamples;
    private short highpassFreq;
    private byte version;
    private byte flags;
    private short loopEnabled;
    private int loopStart;
    private int loopEnd;
    private String copyright;
    private byte[] audioData;

    public ADXFile(String filepath) {
        this.filepath = filepath;
    }

    public void read() throws IOException {
        try (RandomAccessFile file = new RandomAccessFile(filepath, "r")) {
            byte[] header = new byte[32];
            if (file.read(header) < 32) {
                throw new IOException("File too small to be a valid ADX file");
            }

            ByteBuffer buffer = ByteBuffer.wrap(header).order(ByteOrder.BIG_ENDIAN);
            magic = buffer.getShort() & 0xFFFF;
            if (magic != 0x8000) {
                throw new IOException("Invalid ADX file: incorrect magic number");
            }

            headerSize = buffer.getShort() & 0xFFFF;
            encoding = buffer.get();
            blockSize = buffer.get();
            sampleBitDepth = buffer.get();
            channelCount = buffer.get();
            sampleRate = buffer.getInt();
            totalSamples = buffer.getInt();
            highpassFreq = buffer.getShort();
            version = buffer.get();
            flags = buffer.get();
            loopEnabled = buffer.getShort();
            loopStart = buffer.getInt();
            loopEnd = buffer.getInt();

            file.seek(headerSize - 4);
            byte[] copyrightLenBytes = new byte[2];
            file.read(copyrightLenBytes);
            int copyrightLen = ByteBuffer.wrap(copyrightLenBytes).order(ByteOrder.BIG_ENDIAN).getShort();
            byte[] copyrightBytes = new byte[copyrightLen];
            file.read(copyrightBytes);
            copyright = new String(copyrightBytes, "ASCII");

            file.seek(headerSize);
            audioData = new byte[(int)(file.length() - headerSize)];
            file.read(audioData);
        }
    }

    public byte[] decodeAudio() {
        System.out.println("Decoding requires external tools like vgmstream. Raw data size: " + audioData.length);
        return audioData; // Placeholder
    }

    public void write(String outputPath) throws IOException {
        try (RandomAccessFile file = new RandomAccessFile(outputPath, "rw")) {
            ByteBuffer buffer = ByteBuffer.allocate(headerSize).order(ByteOrder.BIG_ENDIAN);
            buffer.putShort((short) magic);
            buffer.putShort((short) headerSize);
            buffer.put(encoding);
            buffer.put(blockSize);
            buffer.put(sampleBitDepth);
            buffer.put(channelCount);
            buffer.putInt(sampleRate);
            buffer.putInt(totalSamples);
            buffer.putShort(highpassFreq);
            buffer.put(version);
            buffer.put(flags);
            buffer.putShort(loopEnabled);
            buffer.putInt(loopStart);
            buffer.putInt(loopEnd);
            buffer.position(32); // Skip to end of fixed header
            buffer.put(new byte[headerSize - 32 - 4]); // Padding
            buffer.putShort((short) copyright.length());
            file.write(buffer.array());
            file.write(copyright.getBytes("ASCII"));
            file.write(audioData);
        }
    }

    public void printProperties() {
        System.out.println("ADX File: " + filepath);
        System.out.println("Magic Number: 0x" + String.format("%04X", magic));
        System.out.println("Header Size: " + headerSize + " bytes");
        System.out.println("Encoding Type: " + encoding);
        System.out.println("Block Size: " + blockSize + " bytes");
        System.out.println("Sample Bit Depth: " + sampleBitDepth + " bits");
        System.out.println("Channel Count: " + channelCount);
        System.out.println("Sample Rate: " + sampleRate + " Hz");
        System.out.println("Total Samples: " + totalSamples);
        System.out.println("Highpass Frequency: " + highpassFreq + " Hz");
        System.out.println("Version: " + version);
        System.out.println("Flags: 0x" + String.format("%02X", flags));
        System.out.println("Loop Enabled: " + (loopEnabled != 0));
        System.out.println("Loop Start Sample: " + loopStart);
        System.out.println("Loop End Sample: " + loopEnd);
        System.out.println("Copyright: " + copyright);
        System.out.println("Audio Data Size: " + audioData.length + " bytes");
    }

    public static void main(String[] args) {
        try {
            ADXFile adx = new ADXFile("example.adx");
            adx.read();
            adx.printProperties();
            adx.decodeAudio();
            adx.write("output.adx");
        } catch (IOException e) {
            System.err.println("Error: " + e.getMessage());
        }
    }
}

Notes:

  • Similar to the Python implementation, decoding is a placeholder due to the complexity of ADPCM.
  • The class handles basic reading and writing based on the assumed header structure.

Task 4: JavaScript Class for CRI ADX

const fs = require('fs');

class ADXFile {
    constructor(filepath) {
        this.filepath = filepath;
        this.magic = null;
        this.headerSize = null;
        this.encoding = null;
        this.blockSize = null;
        this.sampleBitDepth = null;
        this.channelCount = null;
        this.sampleRate = null;
        this.totalSamples = null;
        this.highpassFreq = null;
        this.version = null;
        this.flags = null;
        this.loopEnabled = null;
        this.loopStart = null;
        this.loopEnd = null;
        this.copyright = null;
        this.audioData = null;
    }

    read() {
        const buffer = fs.readFileSync(this.filepath);
        if (buffer.length < 32) {
            throw new Error("File too small to be a valid ADX file");
        }

        const view = new DataView(buffer.buffer);
        this.magic = view.getUint16(0, false);
        if (this.magic !== 0x8000) {
            throw new Error("Invalid ADX file: incorrect magic number");
        }

        this.headerSize = view.getUint16(2, false);
        this.encoding = view.getUint8(4);
        this.blockSize = view.getUint8(5);
        this.sampleBitDepth = view.getUint8(6);
        this.channelCount = view.getUint8(7);
        this.sampleRate = view.getUint32(8, false);
        this.totalSamples = view.getUint32(12, false);
        this.highpassFreq = view.getUint16(16, false);
        this.version = view.getUint8(18);
        this.flags = view.getUint8(19);
        this.loopEnabled = view.getUint16(20, false);
        this.loopStart = view.getUint32(22, false);
        this.loopEnd = view.getUint32(26, false);

        const copyrightLen = view.getUint16(this.headerSize - 4, false);
        this.copyright = buffer.slice(this.headerSize - 2, this.headerSize - 2 + copyrightLen).toString('ascii');
        this.audioData = buffer.slice(this.headerSize);
    }

    decodeAudio() {
        console.log(`Decoding requires external tools like vgmstream. Raw data size: ${this.audioData.length}`);
        return this.audioData; // Placeholder
    }

    write(outputPath) {
        const buffer = Buffer.alloc(this.headerSize + this.copyright.length + 2 + this.audioData.length);
        const view = new DataView(buffer.buffer);

        view.setUint16(0, this.magic, false);
        view.setUint16(2, this.headerSize, false);
        view.setUint8(4, this.encoding);
        view.setUint8(5, this.blockSize);
        view.setUint8(6, this.sampleBitDepth);
        view.setUint8(7, this.channelCount);
        view.setUint32(8, this.sampleRate, false);
        view.setUint32(12, this.totalSamples, false);
        view.setUint16(16, this.highpassFreq, false);
        view.setUint8(18, this.version);
        view.setUint8(19, this.flags);
        view.setUint16(20, this.loopEnabled, false);
        view.setUint32(22, this.loopStart, false);
        view.setUint32(26, this.loopEnd, false);
        // Padding
        buffer.fill(0, 32, this.headerSize - 4);
        view.setUint16(this.headerSize - 4, this.copyright.length, false);
        buffer.write(this.copyright, this.headerSize - 2, 'ascii');
        buffer.write(this.audioData, this.headerSize);

        fs.writeFileSync(outputPath, buffer);
    }

    printProperties() {
        console.log(`ADX File: ${this.filepath}`);
        console.log(`Magic Number: 0x${this.magic.toString(16).padStart(4, '0')}`);
        console.log(`Header Size: ${this.headerSize} bytes`);
        console.log(`Encoding Type: ${this.encoding}`);
        console.log(`Block Size: ${this.blockSize} bytes`);
        console.log(`Sample Bit Depth: ${this.sampleBitDepth} bits`);
        console.log(`Channel Count: ${this.channelCount}`);
        console.log(`Sample Rate: ${this.sampleRate} Hz`);
        console.log(`Total Samples: ${this.totalSamples}`);
        console.log(`Highpass Frequency: ${this.highpassFreq} Hz`);
        console.log(`Version: ${this.version}`);
        console.log(`Flags: 0x${this.flags.toString(16).padStart(2, '0')}`);
        console.log(`Loop Enabled: ${!!this.loopEnabled}`);
        console.log(`Loop Start Sample: ${this.loopStart}`);
        console.log(`Loop End Sample: ${this.loopEnd}`);
        console.log(`Copyright: ${this.copyright}`);
        console.log(`Audio Data Size: ${this.audioData.length} bytes`);
    }
}

// Example usage
try {
    const adx = new ADXFile('example.adx');
    adx.read();
    adx.printProperties();
    adx.decodeAudio();
    adx.write('output.adx');
} catch (e) {
    console.error(`Error: ${e.message}`);
}

Notes:

  • Uses Node.js for file operations.
  • Decoding is a placeholder, as JavaScript lacks native ADPCM decoding libraries.

Task 5: C Class for CRI ADX

C does not have classes, so I’ll use a struct and functions to emulate class-like behavior.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    char* filepath;
    unsigned short magic;
    unsigned short header_size;
    unsigned char encoding;
    unsigned char block_size;
    unsigned char sample_bit_depth;
    unsigned char channel_count;
    unsigned int sample_rate;
    unsigned int total_samples;
    unsigned short highpass_freq;
    unsigned char version;
    unsigned char flags;
    unsigned short loop_enabled;
    unsigned int loop_start;
    unsigned int loop_end;
    char* copyright;
    unsigned char* audio_data;
    long audio_data_size;
} ADXFile;

ADXFile* adx_create(const char* filepath) {
    ADXFile* adx = (ADXFile*)malloc(sizeof(ADXFile));
    adx->filepath = strdup(filepath);
    adx->copyright = NULL;
    adx->audio_data = NULL;
    return adx;
}

void adx_destroy(ADXFile* adx) {
    if (adx) {
        free(adx->filepath);
        free(adx->copyright);
        free(adx->audio_data);
        free(adx);
    }
}

int adx_read(ADXFile* adx) {
    FILE* file = fopen(adx->filepath, "rb");
    if (!file) {
        printf("Error: Cannot open file %s\n", adx->filepath);
        return 1;
    }

    unsigned char header[32];
    if (fread(header, 1, 32, file) < 32) {
        printf("Error: File too small to be a valid ADX file\n");
        fclose(file);
        return 1;
    }

    adx->magic = (header[0] << 8) | header[1];
    if (adx->magic != 0x8000) {
        printf("Error: Invalid ADX file: incorrect magic number\n");
        fclose(file);
        return 1;
    }

    adx->header_size = (header[2] << 8) | header[3];
    adx->encoding = header[4];
    adx->block_size = header[5];
    adx->sample_bit_depth = header[6];
    adx->channel_count = header[7];
    adx->sample_rate = (header[8] << 24) | (header[9] << 16) | (header[10] << 8) | header[11];
    adx->total_samples = (header[12] << 24) | (header[13] << 16) | (header[14] << 8) | header[15];
    adx->highpass_freq = (header[16] << 8) | header[17];
    adx->version = header[18];
    adx->flags = header[19];
    adx->loop_enabled = (header[20] << 8) | header[21];
    adx->loop_start = (header[22] << 24) | (header[23] << 16) | (header[24] << 8) | header[25];
    adx->loop_end = (header[26] << 24) | (header[27] << 16) | (header[28] << 8) | header[29];

    fseek(file, adx->header_size - 4, SEEK_SET);
    unsigned char copyright_len_bytes[2];
    fread(copyright_len_bytes, 1, 2, file);
    unsigned short copyright_len = (copyright_len_bytes[0] << 8) | copyright_len_bytes[1];
    adx->copyright = (char*)malloc(copyright_len + 1);
    fread(adx->copyright, 1, copyright_len, file);
    adx->copyright[copyright_len] = '\0';

    fseek(file, adx->header_size, SEEK_SET);
    fseek(file, 0, SEEK_END);
    adx->audio_data_size = ftell(file) - adx->header_size;
    fseek(file, adx->header_size, SEEK_SET);
    adx->audio_data = (unsigned char*)malloc(adx->audio_data_size);
    fread(adx->audio_data, 1, adx->audio_data_size, file);

    fclose(file);
    return 0;
}

void adx_decode_audio(ADXFile* adx) {
    printf("Decoding requires external tools like vgmstream. Raw data size: %ld\n", adx->audio_data_size);
}

int adx_write(ADXFile* adx, const char* output_path) {
    FILE* file = fopen(output_path, "wb");
    if (!file) {
        printf("Error: Cannot open output file %s\n", output_path);
        return 1;
    }

    unsigned char header[32] = {0};
    header[0] = (adx->magic >> 8) & 0xFF;
    header[1] = adx->magic & 0xFF;
    header[2] = (adx->header_size >> 8) & 0xFF;
    header[3] = adx->header_size & 0xFF;
    header[4] = adx->encoding;
    header[5] = adx->block_size;
    header[6] = adx->sample_bit_depth;
    header[7] = adx->channel_count;
    header[8] = (adx->sample_rate >> 24) & 0xFF;
    header[9] = (adx->sample_rate >> 16) & 0xFF;
    header[10] = (adx->sample_rate >> 8) & 0xFF;
    header[11] = adx->sample_rate & 0xFF;
    header[12] = (adx->total_samples >> 24) & 0xFF;
    header[13] = (adx->total_samples >> 16) & 0xFF;
    header[14] = (adx->total_samples >> 8) & 0xFF;
    header[15] = adx->total_samples & 0xFF;
    header[16] = (adx->highpass_freq >> 8) & 0xFF;
    header[17] = adx->highpass_freq & 0xFF;
    header[18] = adx->version;
    header[19] = adx->flags;
    header[20] = (adx->loop_enabled >> 8) & 0xFF;
    header[21] = adx->loop_enabled & 0xFF;
    header[22] = (adx->loop_start >> 24) & 0xFF;
    header[23] = (adx->loop_start >> 16) & 0xFF;
    header[24] = (adx->loop_start >> 8) & 0xFF;
    header[25] = adx->loop_start & 0xFF;
    header[26] = (adx->loop_end >> 24) & 0xFF;
    header[27] = (adx->loop_end >> 16) & 0xFF;
    header[28] = (adx->loop_end >> 8) & 0xFF;
    header[29] = adx->loop_end & 0xFF;

    fwrite(header, 1, 32, file);
    fseek(file, adx->header_size - 4, SEEK_SET);
    unsigned short copyright_len = strlen(adx->copyright);
    unsigned char copyright_len_bytes[2] = {(copyright_len >> 8) & 0xFF, copyright_len & 0xFF};
    fwrite(copyright_len_bytes, 1, 2, file);
    fwrite(adx->copyright, 1, copyright_len, file);
    fwrite(adx->audio_data, 1, adx->audio_data_size, file);

    fclose(file);
    return 0;
}

void adx_print_properties(ADXFile* adx) {
    printf("ADX File: %s\n", adx->filepath);
    printf("Magic Number: 0x%04X\n", adx->magic);
    printf("Header Size: %u bytes\n", adx->header_size);
    printf("Encoding Type: %u\n", adx->encoding);
    printf("Block Size: %u bytes\n", adx->block_size);
    printf("Sample Bit Depth: %u bits\n", adx->sample_bit_depth);
    printf("Channel Count: %u\n", adx->channel_count);
    printf("Sample Rate: %u Hz\n", adx->sample_rate);
    printf("Total Samples: %u\n", adx->total_samples);
    printf("Highpass Frequency: %u Hz\n", adx->highpass_freq);
    printf("Version: %u\n", adx->version);
    printf("Flags: 0x%02X\n", adx->flags);
    printf("Loop Enabled: %s\n", adx->loop_enabled ? "true" : "false");
    printf("Loop Start Sample: %u\n", adx->loop_start);
    printf("Loop End Sample: %u\n", adx->loop_end);
    printf("Copyright: %s\n", adx->copyright);
    printf("Audio Data Size: %ld bytes\n", adx->audio_data_size);
}

int main() {
    ADXFile* adx = adx_create("example.adx");
    if (adx_read(adx) != 0) {
        adx_destroy(adx);
        return 1;
    }
    adx_print_properties(adx);
    adx_decode_audio(adx);
    adx_write(adx, "output.adx");
    adx_destroy(adx);
    return 0;
}

Notes:

  • The C implementation uses a struct and functions to mimic a class.
  • Memory management is manual, with proper allocation and deallocation.
  • Decoding is a placeholder, as in other implementations.

Limitations and Assumptions

  1. Proprietary Nature: The CRI ADX specification is not publicly available, so the code relies on reverse-engineered header structures.
  2. Decoding: Full ADPCM decoding is complex and typically handled by tools like vgmstream. The provided classes include a placeholder for decoding, as implementing the full algorithm is beyond the scope without proprietary details.
  3. Writing: The write functions copy existing audio data rather than encoding new audio, as encoding requires CRI Middleware’s proprietary tools.
  4. Encryption: Some ADX files may be encrypted, which is not handled here due to lack of public information.
  5. Testing: The code assumes a standard ADX file structure. Testing with real ADX files (e.g., from Sega games) is recommended to verify compatibility.

If you need assistance with a specific ADX file or a different .ADX format (e.g., Lotus Approach), please provide more details, and I can refine the implementation. For pricing or API access to CRI Middleware tools, check https://x.ai/api.

1. List of Properties of the .ADX File Format

The .ADX file format (CRI ADX audio format) is a binary container for ADPCM-compressed audio, primarily used in video games. The properties listed below are derived from its header structure and are intrinsic to the format. These include fixed header fields, version-dependent loop information, and other structural elements. The format uses big-endian byte order for multi-byte integers. Note that the signature "(c)CRI" is a fixed element (not modifiable), and audio data is treated as a binary blob. Properties are:

  • Magic number: 2-byte unsigned integer (uint16), always 0x8000 (fixed validation value).
  • Audio data offset: 2-byte unsigned integer (uint16), byte offset from file start to the beginning of audio data.
  • Encoding type: 1-byte unsigned integer (uint8), typically 3 for standard ADX ADPCM.
  • Block size: 1-byte unsigned integer (uint8), typically 18 bytes per audio block.
  • Sample bitdepth: 1-byte unsigned integer (uint8), typically 4 bits per sample for ADPCM.
  • Channel count: 1-byte unsigned integer (uint8), number of audio channels (usually 1 or 2).
  • Sample rate: 4-byte unsigned integer (uint32), audio sampling rate in Hz.
  • Total samples: 4-byte unsigned integer (uint32), total number of audio samples in the file.
  • Highpass frequency: 2-byte unsigned integer (uint16), high-pass filter cutoff frequency.
  • Version: 1-byte unsigned integer (uint8), loop data style (3, 4, or 5; 5 indicates no loop data).
  • Flags: 1-byte unsigned integer (uint8), includes encryption flag (0 for no encryption; other values may indicate encryption or other features).
  • Unknown (version-specific): For version 3: 4-byte unsigned integer (uint32); for version 4: 16-byte array (bytes); not present for version 5.
  • Loop enabled: 4-byte unsigned integer (uint32), loop flag (typically 1 if looping is enabled, 0 otherwise; present for versions 3 and 4).
  • Loop begin sample: 4-byte unsigned integer (uint32), sample index where loop starts (present for versions 3 and 4).
  • Loop begin byte: 4-byte unsigned integer (uint32), byte offset where loop starts (present for versions 3 and 4).
  • Loop end sample: 4-byte unsigned integer (uint32), sample index where loop ends (present for versions 3 and 4).
  • Loop end byte: 4-byte unsigned integer (uint32), byte offset where loop ends (present for versions 3 and 4).
  • Padding: Variable-length bytes (typically zeros), between end of header fields and signature (may be empty).
  • Audio data: Variable-length bytes, the raw ADPCM audio payload (including end-of-stream dummy frame).

2. Python Class

import struct
import os

class ADXFile:
    def __init__(self, filename=None):
        self.magic = 0x8000
        self.audio_offset = 0
        self.encoding = 0
        self.block_size = 0
        self.sample_bitdepth = 0
        self.channel_count = 0
        self.sample_rate = 0
        self.total_samples = 0
        self.highpass_frequency = 0
        self.version = 0
        self.flags = 0
        self.unknown = None  # uint32 for v3, bytes for v4
        self.loop_enabled = 0
        self.loop_begin_sample = 0
        self.loop_begin_byte = 0
        self.loop_end_sample = 0
        self.loop_end_byte = 0
        self.padding = b''
        self.audio_data = b''
        if filename:
            self.load(filename)

    def load(self, filename):
        with open(filename, 'rb') as f:
            data = f.read()
        if len(data) < 20:
            raise ValueError("Invalid ADX file: too short")
        (self.magic, self.audio_offset, self.encoding, self.block_size, self.sample_bitdepth,
         self.channel_count, self.sample_rate, self.total_samples, self.highpass_frequency,
         self.version, self.flags) = struct.unpack('>HHBBBBIIHB B', data[:20])
        if self.magic != 0x8000:
            raise ValueError("Invalid magic number")
        pos = 20
        if self.version == 3:
            if len(data) < pos + 24:
                raise ValueError("Invalid ADX file: truncated header")
            (unk, le, lbs, lbb, les, leb) = struct.unpack('>IIIIII', data[pos:pos+24])
            self.unknown = unk
            self.loop_enabled = le
            self.loop_begin_sample = lbs
            self.loop_begin_byte = lbb
            self.loop_end_sample = les
            self.loop_end_byte = leb
            pos += 24
        elif self.version == 4:
            if len(data) < pos + 36:
                raise ValueError("Invalid ADX file: truncated header")
            self.unknown = data[pos:pos+16]
            (le, lbs, lbb, les, leb) = struct.unpack('>IIIII', data[pos+16:pos+36])
            self.loop_enabled = le
            self.loop_begin_sample = lbs
            self.loop_begin_byte = lbb
            self.loop_end_sample = les
            self.loop_end_byte = leb
            pos += 36
        elif self.version == 5:
            pass  # No loop data
        else:
            raise ValueError("Unsupported version")
        if self.flags != 0:
            raise ValueError("Encrypted or flagged files not supported")
        self.padding = data[pos:self.audio_offset - 6]
        signature = data[self.audio_offset - 6:self.audio_offset]
        if signature != b'(c)CRI':
            raise ValueError("Invalid signature")
        self.audio_data = data[self.audio_offset:]

    def save(self, filename):
        if self.version == 3:
            loop_data = struct.pack('>IIIIII', self.unknown, self.loop_enabled, self.loop_begin_sample,
                                    self.loop_begin_byte, self.loop_end_sample, self.loop_end_byte)
            header_fields_size = 20 + 24
        elif self.version == 4:
            if len(self.unknown) != 16:
                raise ValueError("Invalid unknown for version 4")
            loop_data = self.unknown + struct.pack('>IIIII', self.loop_enabled, self.loop_begin_sample,
                                                   self.loop_begin_byte, self.loop_end_sample, self.loop_end_byte)
            header_fields_size = 20 + 36
        elif self.version == 5:
            loop_data = b''
            header_fields_size = 20
        else:
            raise ValueError("Unsupported version")
        self.audio_offset = header_fields_size + len(self.padding) + 6
        fixed_header = struct.pack('>HHBBBBIIHB B', self.magic, self.audio_offset, self.encoding, self.block_size,
                                   self.sample_bitdepth, self.channel_count, self.sample_rate, self.total_samples,
                                   self.highpass_frequency, self.version, self.flags)
        with open(filename, 'wb') as f:
            f.write(fixed_header)
            f.write(loop_data)
            f.write(self.padding)
            f.write(b'(c)CRI')
            f.write(self.audio_data)

3. Java Class

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.nio.file.*;

public class ADXFile {
    private int magic = 0x8000;
    private int audioOffset;
    private byte encoding;
    private byte blockSize;
    private byte sampleBitdepth;
    private byte channelCount;
    private int sampleRate;
    private int totalSamples;
    private short highpassFrequency;
    private byte version;
    private byte flags;
    private Object unknown; // Integer for v3, byte[] for v4
    private int loopEnabled;
    private int loopBeginSample;
    private int loopBeginByte;
    private int loopEndSample;
    private int loopEndByte;
    private byte[] padding = new byte[0];
    private byte[] audioData = new byte[0];

    public ADXFile() {}

    public ADXFile(String filename) throws IOException, IllegalArgumentException {
        load(filename);
    }

    public void load(String filename) throws IOException, IllegalArgumentException {
        byte[] data = Files.readAllBytes(Paths.get(filename));
        ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
        magic = bb.getShort() & 0xFFFF;
        if (magic != 0x8000) throw new IllegalArgumentException("Invalid magic number");
        audioOffset = bb.getShort() & 0xFFFF;
        encoding = bb.get();
        blockSize = bb.get();
        sampleBitdepth = bb.get();
        channelCount = bb.get();
        sampleRate = bb.getInt();
        totalSamples = bb.getInt();
        highpassFrequency = bb.getShort();
        version = bb.get();
        flags = bb.get();
        if (flags != 0) throw new IllegalArgumentException("Encrypted or flagged files not supported");
        int pos = 20;
        if (version == 3) {
            unknown = bb.getInt(pos);
            loopEnabled = bb.getInt(pos + 4);
            loopBeginSample = bb.getInt(pos + 8);
            loopBeginByte = bb.getInt(pos + 12);
            loopEndSample = bb.getInt(pos + 16);
            loopEndByte = bb.getInt(pos + 20);
            pos += 24;
        } else if (version == 4) {
            byte[] unk = new byte[16];
            bb.position(pos);
            bb.get(unk);
            unknown = unk;
            loopEnabled = bb.getInt();
            loopBeginSample = bb.getInt();
            loopBeginByte = bb.getInt();
            loopEndSample = bb.getInt();
            loopEndByte = bb.getInt();
            pos += 36;
        } else if (version == 5) {
            // No loop
        } else {
            throw new IllegalArgumentException("Unsupported version");
        }
        int padStart = pos;
        int padEnd = audioOffset - 6;
        padding = new byte[padEnd - padStart];
        System.arraycopy(data, padStart, padding, 0, padding.length);
        byte[] sig = new byte[6];
        System.arraycopy(data, audioOffset - 6, sig, 0, 6);
        if (!new String(sig).equals("(c)CRI")) throw new IllegalArgumentException("Invalid signature");
        audioData = new byte[data.length - audioOffset];
        System.arraycopy(data, audioOffset, audioData, 0, audioData.length);
    }

    public void save(String filename) throws IOException {
        ByteBuffer bb;
        int headerFieldsSize;
        byte[] loopData;
        if (version == 3) {
            bb = ByteBuffer.allocate(24).order(ByteOrder.BIG_ENDIAN);
            bb.putInt((Integer) unknown);
            bb.putInt(loopEnabled);
            bb.putInt(loopBeginSample);
            bb.putInt(loopBeginByte);
            bb.putInt(loopEndSample);
            bb.putInt(loopEndByte);
            loopData = bb.array();
            headerFieldsSize = 20 + 24;
        } else if (version == 4) {
            byte[] unk = (byte[]) unknown;
            if (unk.length != 16) throw new IllegalArgumentException("Invalid unknown for version 4");
            bb = ByteBuffer.allocate(36).order(ByteOrder.BIG_ENDIAN);
            bb.put(unk);
            bb.putInt(loopEnabled);
            bb.putInt(loopBeginSample);
            bb.putInt(loopBeginByte);
            bb.putInt(loopEndSample);
            bb.putInt(loopEndByte);
            loopData = bb.array();
            headerFieldsSize = 20 + 36;
        } else if (version == 5) {
            loopData = new byte[0];
            headerFieldsSize = 20;
        } else {
            throw new IllegalArgumentException("Unsupported version");
        }
        audioOffset = headerFieldsSize + padding.length + 6;
        bb = ByteBuffer.allocate(20).order(ByteOrder.BIG_ENDIAN);
        bb.putShort((short) magic);
        bb.putShort((short) audioOffset);
        bb.put(encoding);
        bb.put(blockSize);
        bb.put(sampleBitdepth);
        bb.put(channelCount);
        bb.putInt(sampleRate);
        bb.putInt(totalSamples);
        bb.putShort(highpassFrequency);
        bb.put(version);
        bb.put(flags);
        byte[] fixedHeader = bb.array();
        try (FileOutputStream fos = new FileOutputStream(filename)) {
            fos.write(fixedHeader);
            fos.write(loopData);
            fos.write(padding);
            fos.write("(c)CRI".getBytes());
            fos.write(audioData);
        }
    }
}

4. Javascript Class

const fs = require('fs');

class ADXFile {
    constructor(filename = null) {
        this.magic = 0x8000;
        this.audioOffset = 0;
        this.encoding = 0;
        this.blockSize = 0;
        this.sampleBitdepth = 0;
        this.channelCount = 0;
        this.sampleRate = 0;
        this.totalSamples = 0;
        this.highpassFrequency = 0;
        this.version = 0;
        this.flags = 0;
        this.unknown = null; // Number for v3, Buffer for v4
        this.loopEnabled = 0;
        this.loopBeginSample = 0;
        this.loopBeginByte = 0;
        this.loopEndSample = 0;
        this.loopEndByte = 0;
        this.padding = Buffer.alloc(0);
        this.audioData = Buffer.alloc(0);
        if (filename) {
            this.load(filename);
        }
    }

    load(filename) {
        const data = fs.readFileSync(filename);
        if (data.length < 20) throw new Error('Invalid ADX file: too short');
        const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
        this.magic = dv.getUint16(0, false); // big-endian
        if (this.magic !== 0x8000) throw new Error('Invalid magic number');
        this.audioOffset = dv.getUint16(2, false);
        this.encoding = data[4];
        this.blockSize = data[5];
        this.sampleBitdepth = data[6];
        this.channelCount = data[7];
        this.sampleRate = dv.getUint32(8, false);
        this.totalSamples = dv.getUint32(12, false);
        this.highpassFrequency = dv.getUint16(16, false);
        this.version = data[18];
        this.flags = data[19];
        if (this.flags !== 0) throw new Error('Encrypted or flagged files not supported');
        let pos = 20;
        if (this.version === 3) {
            this.unknown = dv.getUint32(pos, false);
            this.loopEnabled = dv.getUint32(pos + 4, false);
            this.loopBeginSample = dv.getUint32(pos + 8, false);
            this.loopBeginByte = dv.getUint32(pos + 12, false);
            this.loopEndSample = dv.getUint32(pos + 16, false);
            this.loopEndByte = dv.getUint32(pos + 20, false);
            pos += 24;
        } else if (this.version === 4) {
            this.unknown = data.slice(pos, pos + 16);
            this.loopEnabled = dv.getUint32(pos + 16, false);
            this.loopBeginSample = dv.getUint32(pos + 20, false);
            this.loopBeginByte = dv.getUint32(pos + 24, false);
            this.loopEndSample = dv.getUint32(pos + 28, false);
            this.loopEndByte = dv.getUint32(pos + 32, false);
            pos += 36;
        } else if (this.version === 5) {
            // No loop
        } else {
            throw new Error('Unsupported version');
        }
        const padStart = pos;
        const padEnd = this.audioOffset - 6;
        this.padding = data.slice(padStart, padEnd);
        const sig = data.slice(this.audioOffset - 6, this.audioOffset).toString('ascii');
        if (sig !== '(c)CRI') throw new Error('Invalid signature');
        this.audioData = data.slice(this.audioOffset);
    }

    save(filename) {
        let loopData;
        let headerFieldsSize;
        if (this.version === 3) {
            const buf = Buffer.alloc(24);
            const dv = new DataView(buf.buffer);
            dv.setUint32(0, this.unknown, false);
            dv.setUint32(4, this.loopEnabled, false);
            dv.setUint32(8, this.loopBeginSample, false);
            dv.setUint32(12, this.loopBeginByte, false);
            dv.setUint32(16, this.loopEndSample, false);
            dv.setUint32(20, this.loopEndByte, false);
            loopData = buf;
            headerFieldsSize = 20 + 24;
        } else if (this.version === 4) {
            if (this.unknown.length !== 16) throw new Error('Invalid unknown for version 4');
            const buf = Buffer.alloc(36);
            this.unknown.copy(buf, 0);
            const dv = new DataView(buf.buffer);
            dv.setUint32(16, this.loopEnabled, false);
            dv.setUint32(20, this.loopBeginSample, false);
            dv.setUint32(24, this.loopBeginByte, false);
            dv.setUint32(28, this.loopEndSample, false);
            dv.setUint32(32, this.loopEndByte, false);
            loopData = buf;
            headerFieldsSize = 20 + 36;
        } else if (this.version === 5) {
            loopData = Buffer.alloc(0);
            headerFieldsSize = 20;
        } else {
            throw new Error('Unsupported version');
        }
        this.audioOffset = headerFieldsSize + this.padding.length + 6;
        const fixedHeader = Buffer.alloc(20);
        const dv = new DataView(fixedHeader.buffer);
        dv.setUint16(0, this.magic, false);
        dv.setUint16(2, this.audioOffset, false);
        fixedHeader[4] = this.encoding;
        fixedHeader[5] = this.blockSize;
        fixedHeader[6] = this.sampleBitdepth;
        fixedHeader[7] = this.channelCount;
        dv.setUint32(8, this.sampleRate, false);
        dv.setUint32(12, this.totalSamples, false);
        dv.setUint16(16, this.highpassFrequency, false);
        fixedHeader[18] = this.version;
        fixedHeader[19] = this.flags;
        const fullData = Buffer.concat([fixedHeader, loopData, this.padding, Buffer.from('(c)CRI'), this.audioData]);
        fs.writeFileSync(filename, fullData);
    }
}

5. C Class (Struct and Functions)

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <arpa/inet.h> // For htonl, ntohl (big-endian conversion)

typedef struct {
    uint16_t magic; // 0x8000
    uint16_t audio_offset;
    uint8_t encoding;
    uint8_t block_size;
    uint8_t sample_bitdepth;
    uint8_t channel_count;
    uint32_t sample_rate;
    uint32_t total_samples;
    uint16_t highpass_frequency;
    uint8_t version;
    uint8_t flags;
    void *unknown; // uint32_t* for v3, uint8_t* for v4 (length 16)
    uint32_t loop_enabled;
    uint32_t loop_begin_sample;
    uint32_t loop_begin_byte;
    uint32_t loop_end_sample;
    uint32_t loop_end_byte;
    uint8_t *padding;
    size_t padding_len;
    uint8_t *audio_data;
    size_t audio_data_len;
} ADXFile;

ADXFile* adx_new() {
    ADXFile* adx = (ADXFile*)malloc(sizeof(ADXFile));
    if (!adx) return NULL;
    memset(adx, 0, sizeof(ADXFile));
    adx->magic = 0x8000;
    return adx;
}

void adx_free(ADXFile* adx) {
    if (adx) {
        if (adx->version == 3) free(adx->unknown);
        else if (adx->version == 4) free(adx->unknown);
        free(adx->padding);
        free(adx->audio_data);
        free(adx);
    }
}

int adx_load(ADXFile* adx, const char* filename) {
    FILE* f = fopen(filename, "rb");
    if (!f) return -1;
    fseek(f, 0, SEEK_END);
    size_t file_size = ftell(f);
    fseek(f, 0, SEEK_SET);
    uint8_t* data = (uint8_t*)malloc(file_size);
    if (!data) { fclose(f); return -1; }
    fread(data, 1, file_size, f);
    fclose(f);

    if (file_size < 20) { free(data); return -1; }
    uint16_t magic_be = *(uint16_t*)(data + 0);
    adx->magic = ntohs(magic_be);
    if (adx->magic != 0x8000) { free(data); return -1; }
    uint16_t ao_be = *(uint16_t*)(data + 2);
    adx->audio_offset = ntohs(ao_be);
    adx->encoding = data[4];
    adx->block_size = data[5];
    adx->sample_bitdepth = data[6];
    adx->channel_count = data[7];
    uint32_t sr_be = *(uint32_t*)(data + 8);
    adx->sample_rate = ntohl(sr_be);
    uint32_t ts_be = *(uint32_t*)(data + 12);
    adx->total_samples = ntohl(ts_be);
    uint16_t hf_be = *(uint16_t*)(data + 16);
    adx->highpass_frequency = ntohs(hf_be);
    adx->version = data[18];
    adx->flags = data[19];
    if (adx->flags != 0) { free(data); return -1; }
    size_t pos = 20;
    if (adx->version == 3) {
        if (file_size < pos + 24) { free(data); return -1; }
        uint32_t* unk = (uint32_t*)malloc(sizeof(uint32_t));
        uint32_t unk_be = *(uint32_t*)(data + pos);
        *unk = ntohl(unk_be);
        adx->unknown = unk;
        uint32_t le_be = *(uint32_t*)(data + pos + 4);
        adx->loop_enabled = ntohl(le_be);
        uint32_t lbs_be = *(uint32_t*)(data + pos + 8);
        adx->loop_begin_sample = ntohl(lbs_be);
        uint32_t lbb_be = *(uint32_t*)(data + pos + 12);
        adx->loop_begin_byte = ntohl(lbb_be);
        uint32_t les_be = *(uint32_t*)(data + pos + 16);
        adx->loop_end_sample = ntohl(les_be);
        uint32_t leb_be = *(uint32_t*)(data + pos + 20);
        adx->loop_end_byte = ntohl(leb_be);
        pos += 24;
    } else if (adx->version == 4) {
        if (file_size < pos + 36) { free(data); return -1; }
        uint8_t* unk = (uint8_t*)malloc(16);
        memcpy(unk, data + pos, 16);
        adx->unknown = unk;
        uint32_t le_be = *(uint32_t*)(data + pos + 16);
        adx->loop_enabled = ntohl(le_be);
        uint32_t lbs_be = *(uint32_t*)(data + pos + 20);
        adx->loop_begin_sample = ntohl(lbs_be);
        uint32_t lbb_be = *(uint32_t*)(data + pos + 24);
        adx->loop_begin_byte = ntohl(lbb_be);
        uint32_t les_be = *(uint32_t*)(data + pos + 28);
        adx->loop_end_sample = ntohl(les_be);
        uint32_t leb_be = *(uint32_t*)(data + pos + 32);
        adx->loop_end_byte = ntohl(leb_be);
        pos += 36;
    } else if (adx->version == 5) {
        // No loop
    } else {
        free(data); return -1;
    }
    size_t pad_start = pos;
    size_t pad_end = adx->audio_offset - 6;
    adx->padding_len = pad_end - pad_start;
    adx->padding = (uint8_t*)malloc(adx->padding_len);
    memcpy(adx->padding, data + pad_start, adx->padding_len);
    char sig[7];
    memcpy(sig, data + adx->audio_offset - 6, 6);
    sig[6] = '\0';
    if (strcmp(sig, "(c)CRI") != 0) { free(data); return -1; }
    adx->audio_data_len = file_size - adx->audio_offset;
    adx->audio_data = (uint8_t*)malloc(adx->audio_data_len);
    memcpy(adx->audio_data, data + adx->audio_offset, adx->audio_data_len);
    free(data);
    return 0;
}

int adx_save(ADXFile* adx, const char* filename) {
    size_t header_fields_size;
    uint8_t* loop_data = NULL;
    size_t loop_data_len = 0;
    if (adx->version == 3) {
        loop_data_len = 24;
        loop_data = (uint8_t*)malloc(loop_data_len);
        uint32_t* unk = (uint32_t*)adx->unknown;
        *(uint32_t*)(loop_data + 0) = htonl(*unk);
        *(uint32_t*)(loop_data + 4) = htonl(adx->loop_enabled);
        *(uint32_t*)(loop_data + 8) = htonl(adx->loop_begin_sample);
        *(uint32_t*)(loop_data + 12) = htonl(adx->loop_begin_byte);
        *(uint32_t*)(loop_data + 16) = htonl(adx->loop_end_sample);
        *(uint32_t*)(loop_data + 20) = htonl(adx->loop_end_byte);
        header_fields_size = 20 + 24;
    } else if (adx->version == 4) {
        loop_data_len = 36;
        loop_data = (uint8_t*)malloc(loop_data_len);
        memcpy(loop_data, adx->unknown, 16);
        *(uint32_t*)(loop_data + 16) = htonl(adx->loop_enabled);
        *(uint32_t*)(loop_data + 20) = htonl(adx->loop_begin_sample);
        *(uint32_t*)(loop_data + 24) = htonl(adx->loop_begin_byte);
        *(uint32_t*)(loop_data + 28) = htonl(adx->loop_end_sample);
        *(uint32_t*)(loop_data + 32) = htonl(adx->loop_end_byte);
        header_fields_size = 20 + 36;
    } else if (adx->version == 5) {
        loop_data_len = 0;
        loop_data = NULL;
        header_fields_size = 20;
    } else {
        return -1;
    }
    adx->audio_offset = header_fields_size + adx->padding_len + 6;
    uint8_t fixed_header[20];
    *(uint16_t*)(fixed_header + 0) = htons(adx->magic);
    *(uint16_t*)(fixed_header + 2) = htons(adx->audio_offset);
    fixed_header[4] = adx->encoding;
    fixed_header[5] = adx->block_size;
    fixed_header[6] = adx->sample_bitdepth;
    fixed_header[7] = adx->channel_count;
    *(uint32_t*)(fixed_header + 8) = htonl(adx->sample_rate);
    *(uint32_t*)(fixed_header + 12) = htonl(adx->total_samples);
    *(uint16_t*)(fixed_header + 16) = htons(adx->highpass_frequency);
    fixed_header[18] = adx->version;
    fixed_header[19] = adx->flags;

    FILE* f = fopen(filename, "wb");
    if (!f) { free(loop_data); return -1; }
    fwrite(fixed_header, 1, 20, f);
    if (loop_data) fwrite(loop_data, 1, loop_data_len, f);
    fwrite(adx->padding, 1, adx->padding_len, f);
    fwrite("(c)CRI", 1, 6, f);
    fwrite(adx->audio_data, 1, adx->audio_data_len, f);
    fclose(f);
    free(loop_data);
    return 0;
}