Task 029: .AMR File Format

Task 029: .AMR File Format

1. Properties of the .AMR File Format

The Adaptive Multi-Rate (AMR) file format, specifically AMR-NB (Narrowband), is an audio compression format optimized for speech coding, widely used in mobile telephony. Based on the provided references and technical specifications, here is a list of intrinsic properties of the .AMR file format relevant to its file structure:

  • File Extension: .amr or .3ga
  • MIME Type: audio/amr, audio/3gpp, audio/3gpp2
  • File Type: Lossy audio compression format
  • Header: A 6-byte header with the ASCII string #!AMR\n (hex: 0x23 0x21 0x41 0x4D 0x52 0x0A)
  • Frame Structure: Consists of packed audio frames, each representing 20 milliseconds of audio and containing 160 samples
  • Bit Rates: Variable, ranging from 4.75 to 12.2 kbit/s, with eight modes (AMR_4.75, AMR_5.15, AMR_5.90, AMR_6.70, AMR_7.40, AMR_7.95, AMR_10.20, AMR_12.20) and a Silence Indicator (SID) mode at 1.8 kbit/s
  • Sample Rate: 8 kHz
  • Channels: Mono
  • Frequency Range: Narrowband (200–3400 Hz)
  • Compression Algorithm: Algebraic Code Excited Linear Prediction (ACELP)
  • Additional Techniques: Supports Discontinuous Transmission (DTX), Voice Activity Detection (VAD), and Comfort Noise Generation (CNG)
  • Frame Size: Varies by mode (e.g., 95 bits for 4.75 kbit/s, 244 bits for 12.2 kbit/s)
  • File Structure: Header followed by a sequence of audio frames, each with a 1-byte frame header indicating the codec mode
  • Developed By: 3rd Generation Partnership Project (3GPP)
  • Initial Release: June 23, 1999
  • Usage: Primarily for speech storage and transmission in mobile devices and VoIP applications
  • License: Proprietary

These properties are derived from the file format structure and are critical for reading, writing, and decoding .AMR files.

2. Python Class for .AMR File Handling

import struct
import os

class AMRFile:
    def __init__(self, filepath):
        self.filepath = filepath
        self.header = b'#!AMR\n'
        self.properties = {
            'file_extension': '.amr',
            'mime_type': 'audio/amr',
            'file_type': 'Lossy audio',
            'header': '#!AMR\\n',
            'frame_duration': '20 ms',
            'sample_rate': '8 kHz',
            'channels': 'Mono',
            'frequency_range': '200–3400 Hz',
            'compression': 'ACELP',
            'bit_rates': ['4.75', '5.15', '5.90', '6.70', '7.40', '7.95', '10.20', '12.20', '1.80 (SID)'],
            'developed_by': '3GPP',
            'initial_release': '1999-06-23',
            'license': 'Proprietary'
        }
        self.frame_sizes = {0: 95, 1: 103, 2: 118, 3: 134, 4: 148, 5: 159, 6: 204, 7: 244, 8: 39}  # Bits per frame

    def read_properties(self):
        try:
            with open(self.filepath, 'rb') as f:
                # Verify header
                header = f.read(6)
                if header != self.header:
                    raise ValueError("Invalid AMR file header")
                
                # Read frames to determine used bit rates
                bit_rates = set()
                frame_count = 0
                while True:
                    frame_header = f.read(1)
                    if not frame_header:
                        break
                    mode = (struct.unpack('B', frame_header)[0] >> 3) & 0x0F
                    if mode in self.frame_sizes:
                        bit_rates.add(self.properties['bit_rates'][mode])
                        f.seek((self.frame_sizes[mode] + 7) // 8, 1)  # Skip frame data
                        frame_count += 1
                self.properties['used_bit_rates'] = list(bit_rates)
                self.properties['frame_count'] = frame_count
                return self.properties
        except Exception as e:
            print(f"Error reading AMR file: {e}")
            return None

    def write_properties(self, output_path):
        try:
            with open(self.filepath, 'rb') as f_in, open(output_path, 'wb') as f_out:
                f_out.write(f_in.read())  # Copy the original file
            print(f"AMR file written to {output_path}")
        except Exception as e:
            print(f"Error writing AMR file: {e}")

    def print_properties(self):
        props = self.read_properties()
        if props:
            for key, value in props.items():
                print(f"{key}: {value}")

# Example usage
if __name__ == "__main__":
    amr = AMRFile("sample.amr")
    amr.print_properties()
    amr.write_properties("output.amr")

This Python class reads an .AMR file, verifies its header, extracts properties (including dynamically determined bit rates and frame count), and allows writing a copy of the file. It prints all properties to the console. Note that decoding the actual audio requires an external library like pydub or ffmpeg-python, as Python’s standard library doesn’t support AMR decoding natively.

3. Java Class for .AMR File Handling

import java.io.*;

public class AMRFile {
    private String filepath;
    private final byte[] header = new byte[] { 0x23, 0x21, 0x41, 0x4D, 0x52, 0x0A };
    private final String[] properties = {
        "file_extension: .amr",
        "mime_type: audio/amr",
        "file_type: Lossy audio",
        "header: #!AMR\\n",
        "frame_duration: 20 ms",
        "sample_rate: 8 kHz",
        "channels: Mono",
        "frequency_range: 200–3400 Hz",
        "compression: ACELP",
        "bit_rates: 4.75, 5.15, 5.90, 6.70, 7.40, 7.95, 10.20, 12.20, 1.80 (SID)",
        "developed_by: 3GPP",
        "initial_release: 1999-06-23",
        "license: Proprietary"
    };
    private final int[] frameSizes = { 95, 103, 118, 134, 148, 159, 204, 244, 39 }; // Bits per frame

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

    public String[] readProperties() throws IOException {
        try (FileInputStream fis = new FileInputStream(filepath)) {
            // Verify header
            byte[] fileHeader = new byte[6];
            if (fis.read(fileHeader) != 6 || !java.util.Arrays.equals(fileHeader, header)) {
                throw new IOException("Invalid AMR file header");
            }

            // Read frames
            java.util.Set<String> bitRates = new java.util.HashSet<>();
            int frameCount = 0;
            int mode;
            while ((mode = fis.read()) != -1) {
                mode = (mode >> 3) & 0x0F;
                if (mode < frameSizes.length) {
                    bitRates.add(properties[9].split(": ")[1].split(", ")[mode]);
                    fis.skip((frameSizes[mode] + 7) / 8);
                    frameCount++;
                }
            }
            String[] extendedProps = new String[properties.length + 2];
            System.arraycopy(properties, 0, extendedProps, 0, properties.length);
            extendedProps[properties.length] = "used_bit_rates: " + String.join(", ", bitRates);
            extendedProps[properties.length + 1] = "frame_count: " + frameCount;
            return extendedProps;
        }
    }

    public void writeProperties(String outputPath) throws IOException {
        try (FileInputStream fis = new FileInputStream(filepath);
             FileOutputStream fos = new FileOutputStream(outputPath)) {
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, bytesRead);
            }
            System.out.println("AMR file written to " + outputPath);
        }
    }

    public void printProperties() {
        try {
            String[] props = readProperties();
            for (String prop : props) {
                System.out.println(prop);
            }
        } catch (IOException e) {
            System.err.println("Error reading AMR file: " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        AMRFile amr = new AMRFile("sample.amr");
        amr.printProperties();
        try {
            amr.writeProperties("output.amr");
        } catch (IOException e) {
            System.err.println("Error writing AMR file: " + e.getMessage());
        }
    }
}

This Java class performs similar operations: it verifies the .AMR header, reads frames to extract bit rates and frame count, writes a copy of the file, and prints properties. Java’s standard library also lacks native AMR decoding, so this focuses on file structure properties.

4. JavaScript Class for .AMR File Handling

const fs = require('fs').promises;

class AMRFile {
    constructor(filepath) {
        this.filepath = filepath;
        this.header = Buffer.from('#!AMR\n');
        this.properties = {
            file_extension: '.amr',
            mime_type: 'audio/amr',
            file_type: 'Lossy audio',
            header: '#!AMR\\n',
            frame_duration: '20 ms',
            sample_rate: '8 kHz',
            channels: 'Mono',
            frequency_range: '200–3400 Hz',
            compression: 'ACELP',
            bit_rates: ['4.75', '5.15', '5.90', '6.70', '7.40', '7.95', '10.20', '12.20', '1.80 (SID)'],
            developed_by: '3GPP',
            initial_release: '1999-06-23',
            license: 'Proprietary'
        };
        this.frameSizes = [95, 103, 118, 134, 148, 159, 204, 244, 39]; // Bits per frame
    }

    async readProperties() {
        try {
            const data = await fs.readFile(this.filepath);
            if (!data.slice(0, 6).equals(this.header)) {
                throw new Error('Invalid AMR file header');
            }

            const bitRates = new Set();
            let frameCount = 0;
            let offset = 6;
            while (offset < data.length) {
                const mode = (data[offset] >> 3) & 0x0F;
                if (mode < this.frameSizes.length) {
                    bitRates.add(this.properties.bit_rates[mode]);
                    offset += 1 + Math.ceil(this.frameSizes[mode] / 8);
                    frameCount++;
                } else {
                    break;
                }
            }
            this.properties.used_bit_rates = Array.from(bitRates);
            this.properties.frame_count = frameCount;
            return this.properties;
        } catch (error) {
            console.error(`Error reading AMR file: ${error.message}`);
            return null;
        }
    }

    async writeProperties(outputPath) {
        try {
            const data = await fs.readFile(this.filepath);
            await fs.writeFile(outputPath, data);
            console.log(`AMR file written to ${outputPath}`);
        } catch (error) {
            console.error(`Error writing AMR file: ${error.message}`);
        }
    }

    async printProperties() {
        const props = await this.readProperties();
        if (props) {
            for (const [key, value] of Object.entries(props)) {
                console.log(`${key}: ${value}`);
            }
        }
    }
}

// Example usage
(async () => {
    const amr = new AMRFile('sample.amr');
    await amr.printProperties();
    await amr.writeProperties('output.amr');
})();

This JavaScript class uses Node.js to handle file operations. It verifies the header, extracts bit rates and frame count, writes a copy of the file, and prints properties. JavaScript requires external libraries like node-amr for full audio decoding, but this handles the file structure.

5. C Class for .AMR File Handling

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

typedef struct {
    const char *filepath;
    const char *header;
    const char *properties[13];
    int frame_sizes[9];
} AMRFile;

void amrfile_init(AMRFile *amr, const char *filepath) {
    amr->filepath = filepath;
    amr->header = "#!AMR\n";
    amr->properties[0] = "file_extension: .amr";
    amr->properties[1] = "mime_type: audio/amr";
    amr->properties[2] = "file_type: Lossy audio";
    amr->properties[3] = "header: #!AMR\\n";
    amr->properties[4] = "frame_duration: 20 ms";
    amr->properties[5] = "sample_rate: 8 kHz";
    amr->properties[6] = "channels: Mono";
    amr->properties[7] = "frequency_range: 200–3400 Hz";
    amr->properties[8] = "compression: ACELP";
    amr->properties[9] = "bit_rates: 4.75, 5.15, 5.90, 6.70, 7.40, 7.95, 10.20, 12.20, 1.80 (SID)";
    amr->properties[10] = "developed_by: 3GPP";
    amr->properties[11] = "initial_release: 1999-06-23";
    amr->properties[12] = "license: Proprietary";
    int sizes[] = {95, 103, 118, 134, 148, 159, 204, 244, 39};
    memcpy(amr->frame_sizes, sizes, sizeof(sizes));
}

int read_properties(AMRFile *amr, char *used_bit_rates, int *frame_count) {
    FILE *file = fopen(amr->filepath, "rb");
    if (!file) {
        printf("Error opening AMR file\n");
        return 0;
    }

    char header[6];
    if (fread(header, 1, 6, file) != 6 || strncmp(header, amr->header, 6) != 0) {
        printf("Invalid AMR file header\n");
        fclose(file);
        return 0;
    }

    int bit_rates[9] = {0};
    *frame_count = 0;
    unsigned char frame_header;
    while (fread(&frame_header, 1, 1, file) == 1) {
        int mode = (frame_header >> 3) & 0x0F;
        if (mode < 9) {
            bit_rates[mode] = 1;
            fseek(file, (amr->frame_sizes[mode] + 7) / 8, SEEK_CUR);
            (*frame_count)++;
        } else {
            break;
        }
    }
    fclose(file);

    char rates[100] = "";
    const char *bit_rates_str[] = {"4.75", "5.15", "5.90", "6.70", "7.40", "7.95", "10.20", "12.20", "1.80"};
    for (int i = 0; i < 9; i++) {
        if (bit_rates[i]) {
            if (strlen(rates) > 0) strcat(rates, ", ");
            strcat(rates, bit_rates_str[i]);
        }
    }
    sprintf(used_bit_rates, "used_bit_rates: %s", rates);
    return 1;
}

void write_properties(AMRFile *amr, const char *output_path) {
    FILE *in = fopen(amr->filepath, "rb");
    FILE *out = fopen(output_path, "wb");
    if (!in || !out) {
        printf("Error opening files for writing\n");
        if (in) fclose(in);
        if (out) fclose(out);
        return;
    }

    char buffer[1024];
    size_t bytes;
    while ((bytes = fread(buffer, 1, sizeof(buffer), in)) > 0) {
        fwrite(buffer, 1, bytes, out);
    }
    printf("AMR file written to %s\n", output_path);
    fclose(in);
    fclose(out);
}

void print_properties(AMRFile *amr) {
    char used_bit_rates[100];
    int frame_count;
    if (read_properties(amr, used_bit_rates, &frame_count)) {
        for (int i = 0; i < 13; i++) {
            printf("%s\n", amr->properties[i]);
        }
        printf("%s\n", used_bit_rates);
        printf("frame_count: %d\n", frame_count);
    }
}

int main() {
    AMRFile amr;
    amrfile_init(&amr, "sample.amr");
    print_properties(&amr);
    write_properties(&amr, "output.amr");
    return 0;
}

This C implementation follows the same logic, verifying the header, extracting bit rates and frame count, copying the file, and printing properties. It uses standard C file I/O operations.

Notes

  • Decoding Limitation: None of these implementations decode the actual audio data (e.g., converting to PCM) because this requires proprietary AMR codecs or external libraries (e.g., FFmpeg, OpenCORE-AMR). The classes focus on file structure properties.
  • Writing: The write operation copies the file as-is, preserving its structure, as creating a new AMR file from scratch requires encoding audio, which is beyond standard library capabilities.
  • Testing: Ensure a valid .amr file (sample.amr) exists for testing. You can create one using tools like Audacity with FFmpeg or a mobile device’s voice recorder.
  • References: The implementations are based on AMR file structure details from RFC 4867 and other sources.

If you need actual audio decoding/encoding, I can extend the code to integrate with libraries like FFmpeg or OpenCORE-AMR, but this would require additional setup instructions. Let me know if you want to pursue that!

File Format Specifications for .AMR

The .AMR file format is defined in RFC 4867 (RTP Payload Format and File Storage Format for AMR and AMR-WB Audio Codecs) and related 3GPP specifications (e.g., TS 26.101 for AMR-NB, TS 26.201 for AMR-WB). It is an audio format for storing speech encoded with the Adaptive Multi-Rate (AMR) narrowband or wideband codec. Files consist of a header followed by a sequence of 20 ms frame blocks, each containing one frame per channel. Frames are octet-aligned, with variable-length speech data based on the frame type.

List of all the properties of this file format intrinsic to its file system:

  • Magic number: ASCII string at the start of the file, identifying the codec variant (AMR or AMR-WB) and channel mode (single-channel or multi-channel). Possible values: "#!AMR\n" (AMR single), "#!AMR-WB\n" (AMR-WB single), "#!AMR_MC1.0\n" (AMR multi), "#!AMR-WB_MC1.0\n" (AMR-WB multi).
  • Channel description field: Present only in multi-channel files; a 4-byte big-endian unsigned integer where the least significant 4 bits (bits 28-31) specify the number of channels (1-15, though typically up to 6), and the upper 28 bits are reserved (must be 0).
  • Number of channels: Derived from the magic number (1 for single-channel) or the channel description field (for multi-channel).
  • Frame blocks: Sequence of blocks until EOF, each representing 20 ms of audio, containing one frame per channel in order (channel 1 to N).
  • Frame header: 1 byte per frame; format: bit 7: padding (0), bit 6: quality indicator (Q, 1=good, 0=bad), bits 5-2: frame type (FT, 0-15), bits 1-0: padding (00).
  • Frame type (FT): 4-bit value per frame determining the codec mode, bit rate, and speech data length in bits (e.g., for AMR: FT 0=4.75 kbps/95 bits; for AMR-WB: FT 0=6.60 kbps/132 bits). Special types: SID (comfort noise), SPEECH_LOST (AMR-WB only), NO_DATA (0 bits).
  • Speech data: Variable-length bytes per frame, immediately following the frame header; length = ceil(bits_for_FT / 8), where bits_for_FT is codec-specific (see mappings in code below).
  • Padding bits: In the last byte of each frame's speech data (if bits_for_FT % 8 != 0), the least significant (7 - (bits % 8)) bits are padding (set to 0, ignored on read).

Python class:

import os
import struct

class AMRFile:
    AMR_NB_BITS = {0: 95, 1: 103, 2: 118, 3: 134, 4: 148, 5: 159, 6: 204, 7: 244, 8: 39, 15: 0}
    AMR_WB_BITS = {0: 132, 1: 177, 2: 253, 3: 285, 4: 317, 5: 365, 6: 397, 7: 461, 8: 477, 9: 40, 14: 0, 15: 0}
    MAGIC_AMR_SINGLE = b'#!AMR\n'
    MAGIC_AMR_WB_SINGLE = b'#!AMR-WB\n'
    MAGIC_AMR_MULTI = b'#!AMR_MC1.0\n'
    MAGIC_AMR_WB_MULTI = b'#!AMR-WB_MC1.0\n'

    def __init__(self, filename=None, is_wb=False, channels=1):
        self.is_wb = is_wb
        self.channels = channels
        self.frame_blocks = []  # list of list of {'q': bool, 'ft': int, 'data': bytes}
        self.f = None
        if filename:
            self.load(filename)

    def load(self, filename):
        with open(filename, 'rb') as f:
            data = f.read()
        offset = 0
        if data.startswith(self.MAGIC_AMR_WB_MULTI):
            self.is_wb = True
            self.channels = struct.unpack('>I', data[14:18])[0] & 0xF
            offset = 18
        elif data.startswith(self.MAGIC_AMR_MULTI):
            self.is_wb = False
            self.channels = struct.unpack('>I', data[11:15])[0] & 0xF
            offset = 15
        elif data.startswith(self.MAGIC_AMR_WB_SINGLE):
            self.is_wb = True
            self.channels = 1
            offset = 9
        elif data.startswith(self.MAGIC_AMR_SINGLE):
            self.is_wb = False
            self.channels = 1
            offset = 6
        else:
            raise ValueError("Invalid AMR magic number")
        if self.channels < 1:
            raise ValueError("Invalid channel count")
        bits_map = self.AMR_WB_BITS if self.is_wb else self.AMR_NB_BITS
        while offset < len(data):
            block = []
            for _ in range(self.channels):
                if offset >= len(data):
                    raise EOFError("Unexpected EOF")
                h = data[offset]
                offset += 1
                if (h & 0x83) != 0:
                    raise ValueError("Invalid padding bits in frame header")
                q = bool((h & 0x40) >> 6)
                ft = (h & 0x3C) >> 2
                bits = bits_map.get(ft)
                if bits is None:
                    raise ValueError(f"Invalid FT: {ft}")
                byte_len = (bits + 7) // 8
                if offset + byte_len > len(data):
                    raise EOFError("Unexpected EOF in frame data")
                frame_data = data[offset:offset + byte_len]
                offset += byte_len
                # Verify padding bits are 0 (optional, but for completeness)
                if bits % 8 != 0:
                    pad_bits = 8 - (bits % 8)
                    last_byte = frame_data[-1]
                    if last_byte & ((1 << pad_bits) - 1) != 0:
                        print("Warning: Non-zero padding bits")
                block.append({'q': q, 'ft': ft, 'data': frame_data})
            self.frame_blocks.append(block)

    def save(self, filename):
        with open(filename, 'wb') as f:
            if self.is_wb and self.channels > 1:
                f.write(self.MAGIC_AMR_WB_MULTI)
                chan_desc = (0 << 4) | (self.channels & 0xF)  # Reserved 0, chan in low 4 bits
                f.write(struct.pack('>I', chan_desc))
            elif not self.is_wb and self.channels > 1:
                f.write(self.MAGIC_AMR_MULTI)
                chan_desc = (0 << 4) | (self.channels & 0xF)
                f.write(struct.pack('>I', chan_desc))
            elif self.is_wb:
                f.write(self.MAGIC_AMR_WB_SINGLE)
            else:
                f.write(self.MAGIC_AMR_SINGLE)
            bits_map = self.AMR_WB_BITS if self.is_wb else self.AMR_NB_BITS
            for block in self.frame_blocks:
                if len(block) != self.channels:
                    raise ValueError("Invalid block channels")
                for frame in block:
                    q = 1 if frame['q'] else 0
                    ft = frame['ft']
                    h = (0 << 7) | (q << 6) | (ft << 2) | (0)
                    f.write(bytes([h]))
                    bits = bits_map.get(ft)
                    if bits is None:
                        raise ValueError(f"Invalid FT: {ft}")
                    expected_len = (bits + 7) // 8
                    if len(frame['data']) != expected_len:
                        raise ValueError("Data length mismatch for FT")
                    # Ensure padding 0 in last byte
                    data = frame['data']
                    if bits % 8 != 0:
                        pad_mask = (1 << (8 - (bits % 8))) - 1
                        if data[-1] & pad_mask != 0:
                            raise ValueError("Non-zero padding in data")
                    f.write(data)
  1. Java class:
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;

public class AMRFile {
    private static final Map<Integer, Integer> AMR_NB_BITS = Map.of(0, 95, 1, 103, 2, 118, 3, 134, 4, 148, 5, 159, 6, 204, 7, 244, 8, 39, 15, 0);
    private static final Map<Integer, Integer> AMR_WB_BITS = Map.of(0, 132, 1, 177, 2, 253, 3, 285, 4, 317, 5, 365, 6, 397, 7, 461, 8, 477, 9, 40, 14, 0, 15, 0);
    private static final byte[] MAGIC_AMR_SINGLE = "#!AMR\n".getBytes();
    private static final byte[] MAGIC_AMR_WB_SINGLE = "#!AMR-WB\n".getBytes();
    private static final byte[] MAGIC_AMR_MULTI = "#!AMR_MC1.0\n".getBytes();
    private static final byte[] MAGIC_AMR_WB_MULTI = "#!AMR-WB_MC1.0\n".getBytes();

    private boolean isWb;
    private int channels;
    private List<List<Map<String, Object>>> frameBlocks = new ArrayList<>();  // List of blocks, each block list of maps: "q":Boolean, "ft":Integer, "data":byte[]

    public AMRFile(String filename) throws IOException {
        load(filename);
    }

    public AMRFile(boolean isWb, int channels) {
        this.isWb = isWb;
        this.channels = channels;
    }

    public void load(String filename) throws IOException {
        try (FileInputStream fis = new FileInputStream(filename)) {
            byte[] data = fis.readAllBytes();
            ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
            int offset = 0;
            if (startsWith(bb, MAGIC_AMR_WB_MULTI)) {
                isWb = true;
                offset = MAGIC_AMR_WB_MULTI.length;
                channels = bb.getInt(offset) & 0xF;
                offset += 4;
            } else if (startsWith(bb, MAGIC_AMR_MULTI)) {
                isWb = false;
                offset = MAGIC_AMR_MULTI.length;
                channels = bb.getInt(offset) & 0xF;
                offset += 4;
            } else if (startsWith(bb, MAGIC_AMR_WB_SINGLE)) {
                isWb = true;
                channels = 1;
                offset = MAGIC_AMR_WB_SINGLE.length;
            } else if (startsWith(bb, MAGIC_AMR_SINGLE)) {
                isWb = false;
                channels = 1;
                offset = MAGIC_AMR_SINGLE.length;
            } else {
                throw new IOException("Invalid AMR magic number");
            }
            if (channels < 1) {
                throw new IOException("Invalid channel count");
            }
            Map<Integer, Integer> bitsMap = isWb ? AMR_WB_BITS : AMR_NB_BITS;
            while (offset < data.length) {
                List<Map<String, Object>> block = new ArrayList<>();
                for (int c = 0; c < channels; c++) {
                    if (offset >= data.length) {
                        throw new EOFException("Unexpected EOF");
                    }
                    byte h = bb.get(offset++);
                    if ((h & 0x83) != 0) {
                        throw new IOException("Invalid padding bits in frame header");
                    }
                    boolean q = ((h & 0x40) >> 6) == 1;
                    int ft = (h & 0x3C) >> 2;
                    Integer bits = bitsMap.get(ft);
                    if (bits == null) {
                        throw new IOException("Invalid FT: " + ft);
                    }
                    int byteLen = (bits + 7) / 8;
                    if (offset + byteLen > data.length) {
                        throw new EOFException("Unexpected EOF in frame data");
                    }
                    byte[] frameData = new byte[byteLen];
                    bb.position(offset);
                    bb.get(frameData);
                    offset += byteLen;
                    // Verify padding
                    if (bits % 8 != 0) {
                        int padBits = 8 - (bits % 8);
                        byte lastByte = frameData[byteLen - 1];
                        if ((lastByte & ((1 << padBits) - 1)) != 0) {
                            System.out.println("Warning: Non-zero padding bits");
                        }
                    }
                    Map<String, Object> frame = new HashMap<>();
                    frame.put("q", q);
                    frame.put("ft", ft);
                    frame.put("data", frameData);
                    block.add(frame);
                }
                frameBlocks.add(block);
            }
        }
    }

    public void save(String filename) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(filename)) {
            if (isWb && channels > 1) {
                fos.write(MAGIC_AMR_WB_MULTI);
                int chanDesc = channels & 0xF;  // Reserved 0
                ByteBuffer bb = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(chanDesc);
                fos.write(bb.array());
            } else if (!isWb && channels > 1) {
                fos.write(MAGIC_AMR_MULTI);
                int chanDesc = channels & 0xF;
                ByteBuffer bb = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(chanDesc);
                fos.write(bb.array());
            } else if (isWb) {
                fos.write(MAGIC_AMR_WB_SINGLE);
            } else {
                fos.write(MAGIC_AMR_SINGLE);
            }
            Map<Integer, Integer> bitsMap = isWb ? AMR_WB_BITS : AMR_NB_BITS;
            for (List<Map<String, Object>> block : frameBlocks) {
                if (block.size() != channels) {
                    throw new IOException("Invalid block channels");
                }
                for (Map<String, Object> frame : block) {
                    boolean q = (boolean) frame.get("q");
                    int ft = (int) frame.get("ft");
                    byte h = (byte) ((q ? 1 : 0) << 6 | ft << 2);
                    fos.write(h);
                    Integer bits = bitsMap.get(ft);
                    if (bits == null) {
                        throw new IOException("Invalid FT: " + ft);
                    }
                    int expectedLen = (bits + 7) / 8;
                    byte[] data = (byte[]) frame.get("data");
                    if (data.length != expectedLen) {
                        throw new IOException("Data length mismatch for FT");
                    }
                    // Check padding
                    if (bits % 8 != 0) {
                        int padMask = (1 << (8 - (bits % 8))) - 1;
                        if ((data[expectedLen - 1] & padMask) != 0) {
                            throw new IOException("Non-zero padding in data");
                        }
                    }
                    fos.write(data);
                }
            }
        }
    }

    private boolean startsWith(ByteBuffer bb, byte[] magic) {
        if (bb.limit() < magic.length) return false;
        for (int i = 0; i < magic.length; i++) {
            if (bb.get(i) != magic[i]) return false;
        }
        return true;
    }
}
  1. Javascript class (assuming Node.js for file I/O):
const fs = require('fs');

class AMRFile {
    static AMR_NB_BITS = new Map([[0, 95], [1, 103], [2, 118], [3, 134], [4, 148], [5, 159], [6, 204], [7, 244], [8, 39], [15, 0]]);
    static AMR_WB_BITS = new Map([[0, 132], [1, 177], [2, 253], [3, 285], [4, 317], [5, 365], [6, 397], [7, 461], [8, 477], [9, 40], [14, 0], [15, 0]]);
    static MAGIC_AMR_SINGLE = Buffer.from('#!AMR\n');
    static MAGIC_AMR_WB_SINGLE = Buffer.from('#!AMR-WB\n');
    static MAGIC_AMR_MULTI = Buffer.from('#!AMR_MC1.0\n');
    static MAGIC_AMR_WB_MULTI = Buffer.from('#!AMR-WB_MC1.0\n');

    constructor(filename = null, isWb = false, channels = 1) {
        this.isWb = isWb;
        this.channels = channels;
        this.frameBlocks = [];  // array of array of {q: boolean, ft: number, data: Buffer}
        if (filename) {
            this.load(filename);
        }
    }

    load(filename) {
        const data = fs.readFileSync(filename);
        let offset = 0;
        if (data.slice(0, AMRFile.MAGIC_AMR_WB_MULTI.length).equals(AMRFile.MAGIC_AMR_WB_MULTI)) {
            this.isWb = true;
            offset = AMRFile.MAGIC_AMR_WB_MULTI.length;
            this.channels = data.readUInt32BE(offset) & 0xF;
            offset += 4;
        } else if (data.slice(0, AMRFile.MAGIC_AMR_MULTI.length).equals(AMRFile.MAGIC_AMR_MULTI)) {
            this.isWb = false;
            offset = AMRFile.MAGIC_AMR_MULTI.length;
            this.channels = data.readUInt32BE(offset) & 0xF;
            offset += 4;
        } else if (data.slice(0, AMRFile.MAGIC_AMR_WB_SINGLE.length).equals(AMRFile.MAGIC_AMR_WB_SINGLE)) {
            this.isWb = true;
            this.channels = 1;
            offset = AMRFile.MAGIC_AMR_WB_SINGLE.length;
        } else if (data.slice(0, AMRFile.MAGIC_AMR_SINGLE.length).equals(AMRFile.MAGIC_AMR_SINGLE)) {
            this.isWb = false;
            this.channels = 1;
            offset = AMRFile.MAGIC_AMR_SINGLE.length;
        } else {
            throw new Error('Invalid AMR magic number');
        }
        if (this.channels < 1) {
            throw new Error('Invalid channel count');
        }
        const bitsMap = this.isWb ? AMRFile.AMR_WB_BITS : AMRFile.AMR_NB_BITS;
        while (offset < data.length) {
            const block = [];
            for (let c = 0; c < this.channels; c++) {
                if (offset >= data.length) {
                    throw new Error('Unexpected EOF');
                }
                const h = data[offset++];
                if ((h & 0x83) !== 0) {
                    throw new Error('Invalid padding bits in frame header');
                }
                const q = !!((h & 0x40) >> 6);
                const ft = (h & 0x3C) >> 2;
                const bits = bitsMap.get(ft);
                if (bits === undefined) {
                    throw new Error(`Invalid FT: ${ft}`);
                }
                const byteLen = Math.ceil(bits / 8);
                if (offset + byteLen > data.length) {
                    throw new Error('Unexpected EOF in frame data');
                }
                const frameData = data.slice(offset, offset + byteLen);
                offset += byteLen;
                // Verify padding
                if (bits % 8 !== 0) {
                    const padBits = 8 - (bits % 8);
                    const lastByte = frameData[byteLen - 1];
                    if ((lastByte & ((1 << padBits) - 1)) !== 0) {
                        console.warn('Warning: Non-zero padding bits');
                    }
                }
                block.push({q, ft, data: frameData});
            }
            this.frameBlocks.push(block);
        }
    }

    save(filename) {
        let header = Buffer.alloc(0);
        if (this.isWb && this.channels > 1) {
            header = Buffer.concat([AMRFile.MAGIC_AMR_WB_MULTI, Buffer.alloc(4)]);
            header.writeUInt32BE(this.channels & 0xF, AMRFile.MAGIC_AMR_WB_MULTI.length);
        } else if (!this.isWb && this.channels > 1) {
            header = Buffer.concat([AMRFile.MAGIC_AMR_MULTI, Buffer.alloc(4)]);
            header.writeUInt32BE(this.channels & 0xF, AMRFile.MAGIC_AMR_MULTI.length);
        } else if (this.isWb) {
            header = AMRFile.MAGIC_AMR_WB_SINGLE;
        } else {
            header = AMRFile.MAGIC_AMR_SINGLE;
        }
        const bitsMap = this.isWb ? AMRFile.AMR_WB_BITS : AMRFile.AMR_NB_BITS;
        const framesData = [];
        for (const block of this.frameBlocks) {
            if (block.length !== this.channels) {
                throw new Error('Invalid block channels');
            }
            for (const frame of block) {
                const q = frame.q ? 1 : 0;
                const ft = frame.ft;
                const h = (q << 6) | (ft << 2);
                framesData.push(Buffer.from([h]));
                const bits = bitsMap.get(ft);
                if (bits === undefined) {
                    throw new Error(`Invalid FT: ${ft}`);
                }
                const expectedLen = Math.ceil(bits / 8);
                if (frame.data.length !== expectedLen) {
                    throw new Error('Data length mismatch for FT');
                }
                // Check padding
                if (bits % 8 !== 0) {
                    const padMask = (1 << (8 - (bits % 8))) - 1;
                    if ((frame.data[expectedLen - 1] & padMask) !== 0) {
                        throw new Error('Non-zero padding in data');
                    }
                }
                framesData.push(frame.data);
            }
        }
        const fullData = Buffer.concat([header, ...framesData]);
        fs.writeFileSync(filename, fullData);
    }
}
  1. C class (interpreted as C++ class for object-oriented support):
#include <fstream>
#include <vector>
#include <map>
#include <string>
#include <stdexcept>
#include <cstring>
#include <iostream>

class AMRFile {
private:
    static const std::map<int, int> AMR_NB_BITS;
    static const std::map<int, int> AMR_WB_BITS;
    static const char MAGIC_AMR_SINGLE[];
    static const char MAGIC_AMR_WB_SINGLE[];
    static const char MAGIC_AMR_MULTI[];
    static const char MAGIC_AMR_WB_MULTI[];

    bool is_wb;
    int channels;
    struct Frame {
        bool q;
        int ft;
        std::vector<char> data;
    };
    std::vector<std::vector<Frame>> frame_blocks;

public:
    AMRFile(const std::string& filename = "", bool isWb = false, int chans = 1) : is_wb(isWb), channels(chans) {
        if (!filename.empty()) {
            load(filename);
        }
    }

    void load(const std::string& filename) {
        std::ifstream file(filename, std::ios::binary | std::ios::ate);
        if (!file) {
            throw std::runtime_error("Cannot open file");
        }
        std::streamsize size = file.tellg();
        file.seekg(0, std::ios::beg);
        std::vector<char> data(size);
        if (!file.read(data.data(), size)) {
            throw std::runtime_error("Error reading file");
        }
        size_t offset = 0;
        size_t magic_len;
        if (memcmp(data.data(), MAGIC_AMR_WB_MULTI, strlen(MAGIC_AMR_WB_MULTI)) == 0) {
            is_wb = true;
            magic_len = strlen(MAGIC_AMR_WB_MULTI);
            uint32_t chan_desc;
            memcpy(&chan_desc, data.data() + magic_len, 4);
            chan_desc = __builtin_bswap32(chan_desc);  // Assume big-endian
            channels = chan_desc & 0xF;
            offset = magic_len + 4;
        } else if (memcmp(data.data(), MAGIC_AMR_MULTI, strlen(MAGIC_AMR_MULTI)) == 0) {
            is_wb = false;
            magic_len = strlen(MAGIC_AMR_MULTI);
            uint32_t chan_desc;
            memcpy(&chan_desc, data.data() + magic_len, 4);
            chan_desc = __builtin_bswap32(chan_desc);
            channels = chan_desc & 0xF;
            offset = magic_len + 4;
        } else if (memcmp(data.data(), MAGIC_AMR_WB_SINGLE, strlen(MAGIC_AMR_WB_SINGLE)) == 0) {
            is_wb = true;
            channels = 1;
            offset = strlen(MAGIC_AMR_WB_SINGLE);
        } else if (memcmp(data.data(), MAGIC_AMR_SINGLE, strlen(MAGIC_AMR_SINGLE)) == 0) {
            is_wb = false;
            channels = 1;
            offset = strlen(MAGIC_AMR_SINGLE);
        } else {
            throw std::runtime_error("Invalid AMR magic number");
        }
        if (channels < 1) {
            throw std::runtime_error("Invalid channel count");
        }
        const auto& bits_map = is_wb ? AMR_WB_BITS : AMR_NB_BITS;
        frame_blocks.clear();
        while (offset < static_cast<size_t>(size)) {
            std::vector<Frame> block;
            for (int c = 0; c < channels; ++c) {
                if (offset >= static_cast<size_t>(size)) {
                    throw std::runtime_error("Unexpected EOF");
                }
                unsigned char h = static_cast<unsigned char>(data[offset++]);
                if ((h & 0x83) != 0) {
                    throw std::runtime_error("Invalid padding bits in frame header");
                }
                bool q = ((h & 0x40) >> 6) != 0;
                int ft = (h & 0x3C) >> 2;
                auto it = bits_map.find(ft);
                if (it == bits_map.end()) {
                    throw std::runtime_error("Invalid FT: " + std::to_string(ft));
                }
                int bits = it->second;
                size_t byte_len = (bits + 7) / 8;
                if (offset + byte_len > static_cast<size_t>(size)) {
                    throw std::runtime_error("Unexpected EOF in frame data");
                }
                std::vector<char> frame_data(data.begin() + offset, data.begin() + offset + byte_len);
                offset += byte_len;
                // Verify padding
                if (bits % 8 != 0) {
                    int pad_bits = 8 - (bits % 8);
                    unsigned char last_byte = static_cast<unsigned char>(frame_data.back());
                    if ((last_byte & ((1u << pad_bits) - 1)) != 0) {
                        std::cerr << "Warning: Non-zero padding bits" << std::endl;
                    }
                }
                block.push_back({q, ft, frame_data});
            }
            frame_blocks.push_back(block);
        }
    }

    void save(const std::string& filename) {
        std::ofstream file(filename, std::ios::binary);
        if (!file) {
            throw std::runtime_error("Cannot open file for writing");
        }
        if (is_wb && channels > 1) {
            file.write(MAGIC_AMR_WB_MULTI, strlen(MAGIC_AMR_WB_MULTI));
            uint32_t chan_desc = channels & 0xF;
            chan_desc = __builtin_bswap32(chan_desc);  // Big-endian
            file.write(reinterpret_cast<const char*>(&chan_desc), 4);
        } else if (!is_wb && channels > 1) {
            file.write(MAGIC_AMR_MULTI, strlen(MAGIC_AMR_MULTI));
            uint32_t chan_desc = channels & 0xF;
            chan_desc = __builtin_bswap32(chan_desc);
            file.write(reinterpret_cast<const char*>(&chan_desc), 4);
        } else if (is_wb) {
            file.write(MAGIC_AMR_WB_SINGLE, strlen(MAGIC_AMR_WB_SINGLE));
        } else {
            file.write(MAGIC_AMR_SINGLE, strlen(MAGIC_AMR_SINGLE));
        }
        const auto& bits_map = is_wb ? AMR_WB_BITS : AMR_NB_BITS;
        for (const auto& block : frame_blocks) {
            if (static_cast<int>(block.size()) != channels) {
                throw std::runtime_error("Invalid block channels");
            }
            for (const auto& frame : block) {
                unsigned char h = (frame.q ? 1 : 0) << 6 | frame.ft << 2;
                file.write(reinterpret_cast<const char*>(&h), 1);
                auto it = bits_map.find(frame.ft);
                if (it == bits_map.end()) {
                    throw std::runtime_error("Invalid FT: " + std::to_string(frame.ft));
                }
                int bits = it->second;
                size_t expected_len = (bits + 7) / 8;
                if (frame.data.size() != expected_len) {
                    throw std::runtime_error("Data length mismatch for FT");
                }
                // Check padding
                if (bits % 8 != 0) {
                    int pad_mask = (1 << (8 - (bits % 8))) - 1;
                    unsigned char last_byte = static_cast<unsigned char>(frame.data.back());
                    if ((last_byte & pad_mask) != 0) {
                        throw std::runtime_error("Non-zero padding in data");
                    }
                }
                file.write(frame.data.data(), frame.data.size());
            }
        }
    }
};

const std::map<int, int> AMRFile::AMR_NB_BITS = {{0, 95}, {1, 103}, {2, 118}, {3, 134}, {4, 148}, {5, 159}, {6, 204}, {7, 244}, {8, 39}, {15, 0}};
const std::map<int, int> AMRFile::AMR_WB_BITS = {{0, 132}, {1, 177}, {2, 253}, {3, 285}, {4, 317}, {5, 365}, {6, 397}, {7, 461}, {8, 477}, {9, 40}, {14, 0}, {15, 0}};
const char AMRFile::MAGIC_AMR_SINGLE[] = "#!AMR\n";
const char AMRFile::MAGIC_AMR_WB_SINGLE[] = "#!AMR-WB\n";
const char AMRFile::MAGIC_AMR_MULTI[] = "#!AMR_MC1.0\n";
const char AMRFile::MAGIC_AMR_WB_MULTI[] = "#!AMR-WB_MC1.0\n";