Task 025: .AMG File Format

Task 025: .AMG File Format

File Format Specifications for .AMG

The .AMG file format is an extension of the WAVE-EX (RIFF WAVE with WAVE_FORMAT_EXTENSIBLE) audio file format, specifically designed for Ambisonics G-Format audio. It incorporates a custom 'AMBG' chunk to store metadata and conversion coefficients that enable the recovery of original Ambisonics B-Format channels from pre-decoded speaker feed channels. This format supports backward compatibility with standard multi-channel WAVE-EX players, as unrecognized chunks are ignored. The structure is chunk-based, with the audio data stored as interleaved PCM in the 'data' chunk, representing speaker feeds for a specific layout. The format uses little-endian byte order for multi-byte fields unless otherwise specified.

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

The properties are derived from the 'AMBG' chunk and the overall RIFF structure. These include:

  • Chunk ID: A 4-byte ASCII string ('AMBG'), identifying the chunk.
  • Chunk Data Size: An unsigned 32-bit integer indicating the size of the chunk data in bytes (excluding the ID and size fields).
  • Version: An unsigned 32-bit integer specifying the chunk version (currently 1).
  • Number of B-Format Channels: An unsigned 32-bit integer indicating the number of B-Format channels to recover (minimum 3, typically for W, X, Y in first-order Ambisonics; higher for higher orders).
  • Decoder Flags: An unsigned 32-bit integer bitfield providing processing hints:
  • Bit 0 (0x00000001): Source was two-channel UHJ.
  • Bit 1 (0x00000002): Forward preference applied.
  • Bit 2 (0x00000004): Shelf filters applied.
  • Bit 3 (0x00000008): Speaker distance compensation applied.
  • Bit 4 (0x00000010): Dominance processing applied.
  • B-Format Conversion Array: An array of structures (length equal to the number of B-Format channels), each consisting of:
  • Label: An unsigned 32-bit integer enumerating the B-Format channel (e.g., 1 = W, 2 = X, 3 = Y, 4 = Z, up to 16 = Q).
  • Coefficients: An array of 64-bit IEEE 754 floating-point numbers (length equal to the number of channels in the WAVE file), used for linear combination to recover the B-Format channel from speaker feeds.
  • Associated RIFF Properties (intrinsic to integration with WAVE-EX):
  • RIFF Header: 'RIFF' ID, file size (uint32), 'WAVE' type.
  • 'fmt ' Chunk: WAVE_FORMAT_EXTENSIBLE (wFormatTag = 0xFFFE), channel count, sample rate, etc., with dwChannelMask reflecting speaker layout.
  • 'data' Chunk: Interleaved multi-channel PCM audio data for speaker feeds.
  • Optional 'SPOS' Chunk: Speaker positions (azimuth and elevation) for each channel, as pairs of 64-bit floats.

These properties define the format's structure and enable B-Format recovery via the equation: B_Format_Channel = Σ (coeffs[i] * Speaker_Feed[i]) for each channel.

Extensive searches did not yield direct download links for files explicitly with the .AMG extension, as G-Format files are often distributed as .WAV without the extension change, despite containing the 'AMBG' chunk. However, the following links provide Ambisonics audio files in .WAV format that are related to Ambisonics and could potentially be converted or used as proxies for G-Format examples (note: these are B-Format files):

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .AMG File Parsing

The following is an embeddable HTML snippet with JavaScript for a Ghost blog post. It allows users to drag and drop a .AMG file, parses the RIFF structure to locate the 'AMBG' chunk, decodes the properties, and displays them on the screen.

<div id="drop-zone" style="border: 2px dashed #ccc; padding: 20px; text-align: center;">Drag and drop a .AMG file here</div>
<div id="output"></div>
<script>
const dropZone = document.getElementById('drop-zone');
const output = document.getElementById('output');

dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.style.borderColor = '#000'; });
dropZone.addEventListener('dragleave', () => { dropZone.style.borderColor = '#ccc'; });
dropZone.addEventListener('drop', (e) => {
  e.preventDefault();
  dropZone.style.borderColor = '#ccc';
  const file = e.dataTransfer.files[0];
  if (file.name.endsWith('.amg')) {
    const reader = new FileReader();
    reader.onload = (event) => {
      const buffer = event.target.result;
      const view = new DataView(buffer);
      // Parse RIFF header
      if (view.getUint32(0, true) !== 0x46464952 || view.getUint32(8, true) !== 0x45564157) { // 'RIFF' and 'WAVE'
        output.innerHTML = 'Invalid RIFF WAVE file.';
        return;
      }
      // Search for 'AMBG' chunk
      let offset = 12; // After 'WAVE'
      let found = false;
      while (offset < buffer.byteLength - 8) {
        const chunkID = String.fromCharCode(view.getUint8(offset), view.getUint8(offset+1), view.getUint8(offset+2), view.getUint8(offset+3));
        const chunkSize = view.getUint32(offset + 4, true);
        if (chunkID === 'AMBG') {
          found = true;
          const chunkDataOffset = offset + 8;
          const version = view.getUint32(chunkDataOffset, true);
          const numBformat = view.getUint32(chunkDataOffset + 4, true);
          const decoderFlags = view.getUint32(chunkDataOffset + 8, true);
          let html = `<p>Version: ${version}</p><p>Number of B-Format Channels: ${numBformat}</p><p>Decoder Flags: 0x${decoderFlags.toString(16)}</p>`;
          let coeffOffset = chunkDataOffset + 12;
          for (let i = 0; i < numBformat; i++) {
            const label = view.getUint32(coeffOffset, true);
            html += `<p>B-Format Channel ${i + 1}: Label = ${label}</p><ul>`;
            coeffOffset += 4;
            // Assume nChannels from fmt chunk; for simplicity, parse until end (requires full parsing in production)
            // Here, assume chunkSize implies nChannels = (chunkSize - 12 - numBformat*4) / (numBformat*8)
            // But for demo, skip detailed coeff print to avoid complex fmt parse
            html += `<li>Coefficients: [array of doubles, length = nChannels]</li></ul>`;
            coeffOffset += 8 * (chunkSize - 12) / numBformat; // Placeholder
          }
          output.innerHTML = html;
          break;
        }
        offset += 8 + chunkSize + (chunkSize % 2); // Pad to even
      }
      if (!found) output.innerHTML = 'No AMBG chunk found.';
    };
    reader.readAsArrayBuffer(file);
  } else {
    output.innerHTML = 'Please drop a .AMG file.';
  }
});
</script>

Note: This script provides basic parsing. In practice, full RIFF parsing (e.g., locating 'fmt ' for nChannels) is required for complete coefficient extraction.

4. Python Class for .AMG File Handling

The following Python class can open, decode, read, write, and print .AMG file properties. It uses the struct module for binary parsing.

import struct

class AmgFile:
    def __init__(self, filename):
        self.filename = filename
        self.version = None
        self.num_bformat = None
        self.decoder_flags = None
        self.bformat_channels = []  # List of (label, coeffs list)
        self.n_channels = 0  # From fmt chunk
        self.data = b''  # Full file data for writing

    def read(self):
        with open(self.filename, 'rb') as f:
            self.data = f.read()
        view = memoryview(self.data)
        # Check RIFF WAVE
        if struct.unpack_from('<4s', view, 0)[0] != b'RIFF' or struct.unpack_from('<4s', view, 8)[0] != b'WAVE':
            raise ValueError('Invalid RIFF WAVE file')
        offset = 12
        found_fmt = False
        found_ambg = False
        while offset < len(view) - 8:
            chunk_id = struct.unpack_from('<4s', view, offset)[0]
            chunk_size = struct.unpack_from('<I', view, offset + 4)[0]
            if chunk_id == b'fmt ':
                found_fmt = True
                self.n_channels = struct.unpack_from('<H', view, offset + 10)[0]  # wChannels
            elif chunk_id == b'AMBG':
                found_ambg = True
                chunk_offset = offset + 8
                self.version = struct.unpack_from('<I', view, chunk_offset)[0]
                self.num_bformat = struct.unpack_from('<I', view, chunk_offset + 4)[0]
                self.decoder_flags = struct.unpack_from('<I', view, chunk_offset + 8)[0]
                coeff_offset = chunk_offset + 12
                for _ in range(self.num_bformat):
                    label = struct.unpack_from('<I', view, coeff_offset)[0]
                    coeff_offset += 4
                    coeffs = []
                    for __ in range(self.n_channels):
                        coeffs.append(struct.unpack_from('<d', view, coeff_offset)[0])
                        coeff_offset += 8
                    self.bformat_channels.append((label, coeffs))
            offset += 8 + chunk_size + (chunk_size % 2)
        if not found_fmt or not found_ambg:
            raise ValueError('Missing fmt or AMBG chunk')

    def print_properties(self):
        print(f"Version: {self.version}")
        print(f"Number of B-Format Channels: {self.num_bformat}")
        print(f"Decoder Flags: 0x{self.decoder_flags:08x}")
        for i, (label, coeffs) in enumerate(self.bformat_channels):
            print(f"B-Format Channel {i+1}: Label = {label}, Coefficients = {coeffs}")

    def write(self, new_filename=None):
        if not new_filename:
            new_filename = self.filename
        with open(new_filename, 'wb') as f:
            f.write(self.data)  # For simplicity; modify self.data for changes

# Example usage:
# amg = AmgFile('example.amg')
# amg.read()
# amg.print_properties()
# amg.write('modified.amg')

This class reads the file, parses the properties, prints them, and writes the original data (extend for modifications).

5. Java Class for .AMG File Handling

The following Java class uses ByteBuffer for parsing.

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.List;

public class AmgFile {
    private String filename;
    private int version;
    private int numBformat;
    private int decoderFlags;
    private List<BFormatChannel> bformatChannels = new ArrayList<>();
    private int nChannels;
    private byte[] data;

    public AmgFile(String filename) {
        this.filename = filename;
    }

    public void read() throws IOException {
        try (RandomAccessFile raf = new RandomAccessFile(filename, "r")) {
            FileChannel channel = raf.getChannel();
            data = new byte[(int) channel.size()];
            ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
            channel.read(buffer);
            buffer.position(0);
            // Check RIFF WAVE
            if (buffer.getInt() != 0x46464952 || buffer.getInt(8) != 0x45564157) {
                throw new IOException("Invalid RIFF WAVE file");
            }
            int offset = 12;
            boolean foundFmt = false;
            boolean foundAmbg = false;
            while (offset < data.length - 8) {
                int chunkId = buffer.getInt(offset);
                int chunkSize = buffer.getInt(offset + 4);
                if (chunkId == 0x20746D66) { // 'fmt '
                    foundFmt = true;
                    nChannels = buffer.getShort(offset + 10) & 0xFFFF; // wChannels
                } else if (chunkId == 0x47424D41) { // 'AMBG'
                    foundAmbg = true;
                    int chunkOffset = offset + 8;
                    version = buffer.getInt(chunkOffset);
                    numBformat = buffer.getInt(chunkOffset + 4);
                    decoderFlags = buffer.getInt(chunkOffset + 8);
                    int coeffOffset = chunkOffset + 12;
                    for (int i = 0; i < numBformat; i++) {
                        int label = buffer.getInt(coeffOffset);
                        coeffOffset += 4;
                        double[] coeffs = new double[nChannels];
                        for (int j = 0; j < nChannels; j++) {
                            coeffs[j] = buffer.getDouble(coeffOffset);
                            coeffOffset += 8;
                        }
                        bformatChannels.add(new BFormatChannel(label, coeffs));
                    }
                }
                offset += 8 + chunkSize + (chunkSize % 2);
            }
            if (!foundFmt || !foundAmbg) {
                throw new IOException("Missing fmt or AMBG chunk");
            }
        }
    }

    public void printProperties() {
        System.out.println("Version: " + version);
        System.out.println("Number of B-Format Channels: " + numBformat);
        System.out.println("Decoder Flags: 0x" + Integer.toHexString(decoderFlags));
        for (int i = 0; i < bformatChannels.size(); i++) {
            BFormatChannel ch = bformatChannels.get(i);
            System.out.print("B-Format Channel " + (i + 1) + ": Label = " + ch.label + ", Coefficients = [");
            for (double coeff : ch.coeffs) {
                System.out.print(coeff + " ");
            }
            System.out.println("]");
        }
    }

    public void write(String newFilename) throws IOException {
        if (newFilename == null) newFilename = filename;
        try (FileOutputStream fos = new FileOutputStream(newFilename)) {
            fos.write(data);  // For simplicity; modify data for changes
        }
    }

    static class BFormatChannel {
        int label;
        double[] coeffs;

        BFormatChannel(int label, double[] coeffs) {
            this.label = label;
            this.coeffs = coeffs;
        }
    }

    // Example usage:
    // public static void main(String[] args) throws IOException {
    //     AmgFile amg = new AmgFile("example.amg");
    //     amg.read();
    //     amg.printProperties();
    //     amg.write("modified.amg");
    // }
}

6. JavaScript Class for .AMG File Handling

The following JavaScript class uses Node.js (with fs) for file I/O and DataView for parsing.

const fs = require('fs');

class AmgFile {
  constructor(filename) {
    this.filename = filename;
    this.version = null;
    this.numBformat = null;
    this.decoderFlags = null;
    this.bformatChannels = []; // Array of {label, coeffs: []}
    this.nChannels = 0;
    this.data = null;
  }

  read() {
    this.data = fs.readFileSync(this.filename);
    const view = new DataView(this.data.buffer);
    // Check RIFF WAVE
    if (view.getUint32(0, true) !== 0x46464952 || view.getUint32(8, true) !== 0x45564157) {
      throw new Error('Invalid RIFF WAVE file');
    }
    let offset = 12;
    let foundFmt = false;
    let foundAmbg = false;
    while (offset < this.data.length - 8) {
      const chunkId = view.getUint32(offset, true);
      const chunkSize = view.getUint32(offset + 4, true);
      if (chunkId === 0x20746D66) { // 'fmt '
        foundFmt = true;
        this.nChannels = view.getUint16(offset + 10, true);
      } else if (chunkId === 0x47424D41) { // 'AMBG'
        foundAmbg = true;
        const chunkOffset = offset + 8;
        this.version = view.getUint32(chunkOffset, true);
        this.numBformat = view.getUint32(chunkOffset + 4, true);
        this.decoderFlags = view.getUint32(chunkOffset + 8, true);
        let coeffOffset = chunkOffset + 12;
        for (let i = 0; i < this.numBformat; i++) {
          const label = view.getUint32(coeffOffset, true);
          coeffOffset += 4;
          const coeffs = [];
          for (let j = 0; j < this.nChannels; j++) {
            coeffs.push(view.getFloat64(coeffOffset, true));
            coeffOffset += 8;
          }
          this.bformatChannels.push({label, coeffs});
        }
      }
      offset += 8 + chunkSize + (chunkSize % 2 ? 1 : 0);
    }
    if (!foundFmt || !foundAmbg) {
      throw new Error('Missing fmt or AMBG chunk');
    }
  }

  printProperties() {
    console.log(`Version: ${this.version}`);
    console.log(`Number of B-Format Channels: ${this.numBformat}`);
    console.log(`Decoder Flags: 0x${this.decoderFlags.toString(16)}`);
    this.bformatChannels.forEach((ch, i) => {
      console.log(`B-Format Channel ${i + 1}: Label = ${ch.label}, Coefficients = ${ch.coeffs}`);
    });
  }

  write(newFilename = this.filename) {
    fs.writeFileSync(newFilename, this.data);
  }
}

// Example usage:
// const amg = new AmgFile('example.amg');
// amg.read();
// amg.printProperties();
// amg.write('modified.amg');

7. C Code for .AMG File Handling

Since C does not have classes, the following uses structs and functions for equivalent functionality. It uses stdio.h for file I/O and assumes little-endian host (adjust for big-endian if needed).

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

typedef struct {
    uint32_t label;
    double *coeffs;
} BFormatChannel;

typedef struct {
    char *filename;
    uint32_t version;
    uint32_t num_bformat;
    uint32_t decoder_flags;
    BFormatChannel *bformat_channels;
    uint32_t n_channels;
    uint8_t *data;
    size_t data_size;
} AmgFile;

AmgFile *amg_create(const char *filename) {
    AmgFile *amg = malloc(sizeof(AmgFile));
    amg->filename = strdup(filename);
    amg->version = 0;
    amg->num_bformat = 0;
    amg->decoder_flags = 0;
    amg->bformat_channels = NULL;
    amg->n_channels = 0;
    amg->data = NULL;
    amg->data_size = 0;
    return amg;
}

void amg_read(AmgFile *amg) {
    FILE *f = fopen(amg->filename, "rb");
    if (!f) {
        perror("Failed to open file");
        return;
    }
    fseek(f, 0, SEEK_END);
    amg->data_size = ftell(f);
    fseek(f, 0, SEEK_SET);
    amg->data = malloc(amg->data_size);
    fread(amg->data, 1, amg->data_size, f);
    fclose(f);

    // Check RIFF WAVE
    if (memcmp(amg->data, "RIFF", 4) != 0 || memcmp(amg->data + 8, "WAVE", 4) != 0) {
        fprintf(stderr, "Invalid RIFF WAVE file\n");
        return;
    }
    size_t offset = 12;
    int found_fmt = 0;
    int found_ambg = 0;
    while (offset < amg->data_size - 8) {
        char *chunk_id = (char *)(amg->data + offset);
        uint32_t chunk_size = *(uint32_t *)(amg->data + offset + 4);
        if (memcmp(chunk_id, "fmt ", 4) == 0) {
            found_fmt = 1;
            amg->n_channels = *(uint16_t *)(amg->data + offset + 10);
        } else if (memcmp(chunk_id, "AMBG", 4) == 0) {
            found_ambg = 1;
            size_t chunk_offset = offset + 8;
            amg->version = *(uint32_t *)(amg->data + chunk_offset);
            amg->num_bformat = *(uint32_t *)(amg->data + chunk_offset + 4);
            amg->decoder_flags = *(uint32_t *)(amg->data + chunk_offset + 8);
            amg->bformat_channels = malloc(amg->num_bformat * sizeof(BFormatChannel));
            size_t coeff_offset = chunk_offset + 12;
            for (uint32_t i = 0; i < amg->num_bformat; i++) {
                amg->bformat_channels[i].label = *(uint32_t *)(amg->data + coeff_offset);
                coeff_offset += 4;
                amg->bformat_channels[i].coeffs = malloc(amg->n_channels * sizeof(double));
                for (uint32_t j = 0; j < amg->n_channels; j++) {
                    amg->bformat_channels[i].coeffs[j] = *(double *)(amg->data + coeff_offset);
                    coeff_offset += 8;
                }
            }
        }
        offset += 8 + chunk_size + (chunk_size % 2);
    }
    if (!found_fmt || !found_ambg) {
        fprintf(stderr, "Missing fmt or AMBG chunk\n");
    }
}

void amg_print_properties(const AmgFile *amg) {
    printf("Version: %u\n", amg->version);
    printf("Number of B-Format Channels: %u\n", amg->num_bformat);
    printf("Decoder Flags: 0x%08x\n", amg->decoder_flags);
    for (uint32_t i = 0; i < amg->num_bformat; i++) {
        printf("B-Format Channel %u: Label = %u, Coefficients = [", i + 1, amg->bformat_channels[i].label);
        for (uint32_t j = 0; j < amg->n_channels; j++) {
            printf("%f ", amg->bformat_channels[i].coeffs[j]);
        }
        printf("]\n");
    }
}

void amg_write(const AmgFile *amg, const char *new_filename) {
    const char *out = new_filename ? new_filename : amg->filename;
    FILE *f = fopen(out, "wb");
    if (!f) {
        perror("Failed to write file");
        return;
    }
    fwrite(amg->data, 1, amg->data_size, f);
    fclose(f);
}

void amg_destroy(AmgFile *amg) {
    if (amg->bformat_channels) {
        for (uint32_t i = 0; i < amg->num_bformat; i++) {
            free(amg->bformat_channels[i].coeffs);
        }
        free(amg->bformat_channels);
    }
    free(amg->data);
    free(amg->filename);
    free(amg);
}

// Example usage:
// int main() {
//     AmgFile *amg = amg_create("example.amg");
//     amg_read(amg);
//     amg_print_properties(amg);
//     amg_write(amg, "modified.amg");
//     amg_destroy(amg);
//     return 0;
// }