Task 033: .APE File Format
Task 033: .APE File Format
.APE File Format Specifications
The .APE file format, commonly associated with Monkey's Audio, is a lossless audio compression format. Below is an analysis of the intrinsic properties of the .APE file format based on available information up to March 2024, as well as implementations in Python, Java, JavaScript, and C to handle .APE files.
1. Properties of the .APE File Format Intrinsic to Its File System
The .APE file format (Monkey's Audio) is designed for lossless audio compression. Its intrinsic properties include:
- File Header: Contains metadata about the file, including:
- Signature: A magic number identifying the file as an APE file (e.g., "MAC ").
- Version: The version of the Monkey's Audio format (e.g., 3.97, 3.99, etc.).
- Compression Level: Indicates the compression mode (e.g., Fast, Normal, High, Extra High, Insane).
- Format Flags: Specifies audio properties like stereo/mono, bit depth (e.g., 8-bit, 16-bit, 24-bit).
- Sample Rate: The audio sampling rate (e.g., 44100 Hz, 48000 Hz).
- Channels: Number of audio channels (e.g., 1 for mono, 2 for stereo).
- Total Frames: Number of audio frames in the file.
- Final Frame Blocks: Number of samples in the final frame.
- Total Samples: Total number of audio samples in the file.
- Bits Per Sample: Bit depth of the audio (e.g., 16-bit, 24-bit).
- Block Alignment: Number of bytes per audio block.
- Audio Data: Compressed audio data stored in frames, each containing a header and compressed samples.
- Seek Table: An optional table for fast seeking, storing offsets to frame boundaries.
- Checksums: CRC or MD5 checksums for data integrity verification.
- Tag Support: Optional metadata tags (e.g., APEv2 tags) for storing information like artist, album, title, etc.
- File Size: Total size of the .APE file in bytes.
- Duration: Calculated from total samples and sample rate (duration = total samples / sample rate).
These properties are intrinsic to the .APE file structure and are typically stored in the file header or associated metadata sections.
Implementation Notes
Since the .APE file format is proprietary and complex, fully decoding the compressed audio data requires the Monkey's Audio SDK or libraries like libav
/ffmpeg
. However, for the purposes of this task, the classes below focus on reading and writing the file header properties and printing them to the console. Writing new .APE files is limited to modifying or copying existing headers, as full encoding requires proprietary compression algorithms not publicly documented.
The implementations assume the ability to read the header structure based on typical Monkey's Audio file format specifications. For simplicity, we focus on parsing the header and metadata, not the compressed audio data itself. If full decoding/encoding is needed, you would typically use an external library like ffmpeg
or the Monkey's Audio SDK.
2. Python Class for .APE File Handling
import struct
import os
class ApeFile:
def __init__(self, filepath):
self.filepath = filepath
self.properties = {}
def read_ape_header(self):
try:
with open(self.filepath, 'rb') as f:
# Read the header (simplified, assumes APEv2 or similar structure)
data = f.read(32) # Read first 32 bytes for header
if data[:4] != b'MAC ':
raise ValueError("Not a valid .APE file")
# Parse header fields (example structure)
self.properties['signature'] = data[:4].decode('ascii')
self.properties['version'] = struct.unpack('<H', data[4:6])[0] / 1000 # e.g., 3990 -> 3.99
self.properties['descriptor_size'] = struct.unpack('<H', data[6:8])[0]
# Read descriptor for more properties
f.seek(0)
descriptor = f.read(52) # Typical descriptor size
self.properties['compression_level'] = struct.unpack('<I', descriptor[8:12])[0]
self.properties['format_flags'] = struct.unpack('<I', descriptor[12:16])[0]
self.properties['channels'] = struct.unpack('<I', descriptor[16:20])[0]
self.properties['sample_rate'] = struct.unpack('<I', descriptor[20:24])[0]
self.properties['bits_per_sample'] = struct.unpack('<I', descriptor[24:28])[0]
self.properties['total_frames'] = struct.unpack('<I', descriptor[28:32])[0]
self.properties['final_frame_blocks'] = struct.unpack('<I', descriptor[32:36])[0]
self.properties['total_samples'] = struct.unpack('<I', descriptor[36:40])[0]
self.properties['block_alignment'] = struct.unpack('<I', descriptor[40:44])[0]
self.properties['file_size'] = os.path.getsize(self.filepath)
self.properties['duration'] = self.properties['total_samples'] / self.properties['sample_rate']
# Read APEv2 tags (simplified)
f.seek(-32, os.SEEK_END)
tag_data = f.read(32)
if tag_data[:8] == b'APETAGEX':
self.properties['has_apev2_tags'] = True
else:
self.properties['has_apev2_tags'] = False
except Exception as e:
print(f"Error reading .APE file: {e}")
def write_ape_header(self, output_path):
try:
with open(self.filepath, 'rb') as f_in, open(output_path, 'wb') as f_out:
# Copy the entire file (header + data)
f_out.write(f_in.read())
# Note: Modifying compressed data requires Monkey's Audio SDK
print(f"Written .APE file to {output_path}")
except Exception as e:
print(f"Error writing .APE file: {e}")
def print_properties(self):
for key, value in self.properties.items():
print(f"{key}: {value}")
# Example usage
if __name__ == "__main__":
ape = ApeFile("sample.ape")
ape.read_ape_header()
ape.print_properties()
ape.write_ape_header("output.ape")
Notes:
- The Python class uses the
struct
module to parse the binary header. - Reading assumes a simplified header structure based on Monkey's Audio documentation.
- Writing copies the file as-is, as encoding new .APE files requires proprietary compression.
- Error handling is included for invalid files or I/O issues.
3. Java Class for .APE File Handling
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class ApeFile {
private String filepath;
private String signature;
private double version;
private int descriptorSize;
private int compressionLevel;
private int formatFlags;
private int channels;
private int sampleRate;
private int bitsPerSample;
private int totalFrames;
private int finalFrameBlocks;
private int totalSamples;
private int blockAlignment;
private long fileSize;
private double duration;
private boolean hasApev2Tags;
public ApeFile(String filepath) {
this.filepath = filepath;
}
public void readApeHeader() throws IOException {
try (RandomAccessFile file = new RandomAccessFile(filepath, "r")) {
byte[] header = new byte[32];
file.readFully(header);
// Check signature
signature = new String(header, 0, 4);
if (!signature.equals("MAC ")) {
throw new IOException("Not a valid .APE file");
}
// Parse header
ByteBuffer buffer = ByteBuffer.wrap(header).order(ByteOrder.LITTLE_ENDIAN);
version = buffer.getShort(4) / 1000.0;
descriptorSize = buffer.getShort(6);
// Read descriptor
byte[] descriptor = new byte[52];
file.seek(0);
file.readFully(descriptor);
buffer = ByteBuffer.wrap(descriptor).order(ByteOrder.LITTLE_ENDIAN);
compressionLevel = buffer.getInt(8);
formatFlags = buffer.getInt(12);
channels = buffer.getInt(16);
sampleRate = buffer.getInt(20);
bitsPerSample = buffer.getInt(24);
totalFrames = buffer.getInt(28);
finalFrameBlocks = buffer.getInt(32);
totalSamples = buffer.getInt(36);
blockAlignment = buffer.getInt(40);
fileSize = new File(filepath).length();
duration = (double) totalSamples / sampleRate;
// Check for APEv2 tags
file.seek(file.length() - 32);
byte[] tagData = new byte[32];
file.readFully(tagData);
hasApev2Tags = new String(tagData, 0, 8).equals("APETAGEX");
}
}
public void writeApeHeader(String outputPath) throws IOException {
try (FileInputStream in = new FileInputStream(filepath);
FileOutputStream out = new FileOutputStream(outputPath)) {
// Copy file
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
System.out.println("Written .APE file to " + outputPath);
}
}
public void printProperties() {
System.out.println("signature: " + signature);
System.out.println("version: " + version);
System.out.println("descriptor_size: " + descriptorSize);
System.out.println("compression_level: " + compressionLevel);
System.out.println("format_flags: " + formatFlags);
System.out.println("channels: " + channels);
System.out.println("sample_rate: " + sampleRate);
System.out.println("bits_per_sample: " + bitsPerSample);
System.out.println("total_frames: " + totalFrames);
System.out.println("final_frame_blocks: " + finalFrameBlocks);
System.out.println("total_samples: " + totalSamples);
System.out.println("block_alignment: " + blockAlignment);
System.out.println("file_size: " + fileSize);
System.out.println("duration: " + duration);
System.out.println("has_apev2_tags: " + hasApev2Tags);
}
public static void main(String[] args) {
try {
ApeFile ape = new ApeFile("sample.ape");
ape.readApeHeader();
ape.printProperties();
ape.writeApeHeader("output.ape");
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Notes:
- Uses
RandomAccessFile
for binary reading andByteBuffer
for parsing. - Writing copies the file as-is, similar to Python.
- Error handling ensures robust file operations.
4. JavaScript Class for .APE File Handling
const fs = require('fs').promises;
class ApeFile {
constructor(filepath) {
this.filepath = filepath;
this.properties = {};
}
async readApeHeader() {
try {
const file = await fs.open(this.filepath, 'r');
const header = Buffer.alloc(32);
await file.read(header, 0, 32, 0);
// Check signature
this.properties.signature = header.toString('ascii', 0, 4);
if (this.properties.signature !== 'MAC ') {
throw new Error('Not a valid .APE file');
}
// Parse header
this.properties.version = header.readUInt16LE(4) / 1000;
this.properties.descriptor_size = header.readUInt16LE(6);
// Read descriptor
const descriptor = Buffer.alloc(52);
await file.read(descriptor, 0, 52, 0);
this.properties.compression_level = descriptor.readUInt32LE(8);
this.properties.format_flags = descriptor.readUInt32LE(12);
this.properties.channels = descriptor.readUInt32LE(16);
this.properties.sample_rate = descriptor.readUInt32LE(20);
this.properties.bits_per_sample = descriptor.readUInt32LE(24);
this.properties.total_frames = descriptor.readUInt32LE(28);
this.properties.final_frame_blocks = descriptor.readUInt32LE(32);
this.properties.total_samples = descriptor.readUInt32LE(36);
this.properties.block_alignment = descriptor.readUInt32LE(40);
this.properties.file_size = (await fs.stat(this.filepath)).size;
this.properties.duration = this.properties.total_samples / this.properties.sample_rate;
// Check for APEv2 tags
const tagData = Buffer.alloc(32);
await file.read(tagData, 0, 32, this.properties.file_size - 32);
this.properties.has_apev2_tags = tagData.toString('ascii', 0, 8) === 'APETAGEX';
await file.close();
} catch (error) {
console.error(`Error reading .APE file: ${error.message}`);
}
}
async writeApeHeader(outputPath) {
try {
const data = await fs.readFile(this.filepath);
await fs.writeFile(outputPath, data);
console.log(`Written .APE file to ${outputPath}`);
} catch (error) {
console.error(`Error writing .APE file: ${error.message}`);
}
}
printProperties() {
for (const [key, value] of Object.entries(this.properties)) {
console.log(`${key}: ${value}`);
}
}
}
// Example usage
(async () => {
const ape = new ApeFile('sample.ape');
await ape.readApeHeader();
ape.printProperties();
await ape.writeApeHeader('output.ape');
})();
Notes:
- Uses Node.js
fs.promises
for asynchronous file operations. - Parses binary data using
Buffer
with little-endian encoding. - Writing copies the file, as encoding new .APE files is not feasible without proprietary tools.
5. C Class for .APE File Handling
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char signature[5];
float version;
unsigned short descriptor_size;
unsigned int compression_level;
unsigned int format_flags;
unsigned int channels;
unsigned int sample_rate;
unsigned int bits_per_sample;
unsigned int total_frames;
unsigned int final_frame_blocks;
unsigned int total_samples;
unsigned int block_alignment;
long file_size;
double duration;
int has_apev2_tags;
} ApeFile;
void init_ape_file(ApeFile* ape, const char* filepath) {
strncpy(ape->signature, "", 5);
ape->version = 0.0;
ape->descriptor_size = 0;
ape->compression_level = 0;
ape->format_flags = 0;
ape->channels = 0;
ape->sample_rate = 0;
ape->bits_per_sample = 0;
ape->total_frames = 0;
ape->final_frame_blocks = 0;
ape->total_samples = 0;
ape->block_alignment = 0;
ape->file_size = 0;
ape->duration = 0.0;
ape->has_apev2_tags = 0;
}
int read_ape_header(ApeFile* ape, const char* filepath) {
FILE* file = fopen(filepath, "rb");
if (!file) {
printf("Error: Cannot open file %s\n", filepath);
return 1;
}
// Read header
unsigned char header[32];
if (fread(header, 1, 32, file) != 32) {
printf("Error: Cannot read header\n");
fclose(file);
return 1;
}
// Check signature
strncpy(ape->signature, (char*)header, 4);
ape->signature[4] = '\0';
if (strcmp(ape->signature, "MAC ") != 0) {
printf("Error: Not a valid .APE file\n");
fclose(file);
return 1;
}
// Parse header
ape->version = (*(unsigned short*)(header + 4)) / 1000.0;
ape->descriptor_size = *(unsigned short*)(header + 6);
// Read descriptor
fseek(file, 0, SEEK_SET);
unsigned char descriptor[52];
if (fread(descriptor, 1, 52, file) != 52) {
printf("Error: Cannot read descriptor\n");
fclose(file);
return 1;
}
ape->compression_level = *(unsigned int*)(descriptor + 8);
ape->format_flags = *(unsigned int*)(descriptor + 12);
ape->channels = *(unsigned int*)(descriptor + 16);
ape->sample_rate = *(unsigned int*)(descriptor + 20);
ape->bits_per_sample = *(unsigned int*)(descriptor + 24);
ape->total_frames = *(unsigned int*)(descriptor + 28);
ape->final_frame_blocks = *(unsigned int*)(descriptor + 32);
ape->total_samples = *(unsigned int*)(descriptor + 36);
ape->block_alignment = *(unsigned int*)(descriptor + 40);
// Get file size
fseek(file, 0, SEEK_END);
ape->file_size = ftell(file);
ape->duration = (double)ape->total_samples / ape->sample_rate;
// Check for APEv2 tags
fseek(file, ape->file_size - 32, SEEK_SET);
unsigned char tag_data[32];
if (fread(tag_data, 1, 32, file) == 32) {
ape->has_apev2_tags = strncmp((char*)tag_data, "APETAGEX", 8) == 0;
}
fclose(file);
return 0;
}
int write_ape_header(ApeFile* ape, const char* output_path) {
FILE* in = fopen(ape->signature, "rb");
FILE* out = fopen(output_path, "wb");
if (!in || !out) {
printf("Error: Cannot open input/output file\n");
if (in) fclose(in);
if (out) fclose(out);
return 1;
}
// Copy file
unsigned char buffer[1024];
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, 1024, in)) > 0) {
fwrite(buffer, 1, bytes_read, out);
}
fclose(in);
fclose(out);
printf("Written .APE file to %s\n", output_path);
return 0;
}
void print_properties(ApeFile* ape) {
printf("signature: %s\n", ape->signature);
printf("version: %.3f\n", ape->version);
printf("descriptor_size: %u\n", ape->descriptor_size);
printf("compression_level: %u\n", ape->compression_level);
printf("format_flags: %u\n", ape->format_flags);
printf("channels: %u\n", ape->channels);
printf("sample_rate: %u\n", ape->sample_rate);
printf("bits_per_sample: %u\n", ape->bits_per_sample);
printf("total_frames: %u\n", ape->total_frames);
printf("final_frame_blocks: %u\n", ape->final_frame_blocks);
printf("total_samples: %u\n", ape->total_samples);
printf("block_alignment: %u\n", ape->block_alignment);
printf("file_size: %ld\n", ape->file_size);
printf("duration: %.2f\n", ape->duration);
printf("has_apev2_tags: %d\n", ape->has_apev2_tags);
}
int main() {
ApeFile ape;
init_ape_file(&ape, "sample.ape");
if (read_ape_header(&ape, "sample.ape") == 0) {
print_properties(&ape);
write_ape_header(&ape, "output.ape");
}
return 0;
}
Notes:
- Uses standard C file I/O (
fopen
,fread
,fwrite
) for binary operations. - Writing copies the file, as encoding requires proprietary tools.
- Error handling ensures robustness.
Limitations and Assumptions
- Proprietary Compression: The .APE format uses proprietary compression, so the classes focus on reading and printing header properties and copying files for writing. Full encoding/decoding of audio data requires the Monkey's Audio SDK or libraries like
ffmpeg
. - Header Structure: The implementations assume a simplified header structure based on typical Monkey's Audio files (e.g., version 3.99). Actual files may vary slightly, especially older versions.
- Dependencies: The Python and JavaScript implementations assume access to standard libraries (
struct
,fs.promises
). The Java and C implementations are self-contained. - Testing: The code assumes the presence of a valid .APE file (
sample.ape
). You may need to adjust file paths or use a library likeffmpeg
for real-world applications. - APEv2 Tags: The implementations check for APEv2 tags but do not parse their contents for simplicity.
If you need full audio decoding or encoding, consider integrating with libav
/ffmpeg
or the Monkey's Audio SDK, which provide comprehensive support for .APE files. Let me know if you need guidance on integrating these libraries or further details!
File Format Specifications for .APE
The .APE file format is used by Monkey's Audio, a lossless audio compression algorithm. It supports high-quality audio storage without data loss, with built-in support for metadata tags (like APE or ID3), seek tables for fast navigation, and optional embedded WAV headers. The format is versioned, with significant changes at versions 3.95 and 3.98. The file may start with optional leading junk (e.g., ID3v2 tags), followed by the APE header, optional WAV header, seek table, compressed audio frames, optional terminating data, and trailing tags.
- List of all the properties of this file format intrinsic to its file system:
- Signature: 4-byte string ('MAC ')
- Version: 2-byte unsigned integer (version * 1000, e.g., 3980 for v3.98)
- Padding: 2-byte unsigned integer (alignment padding, present if version >= 3980)
- Descriptor Length: 4-byte unsigned integer (size of descriptor block, >= 3980)
- Header Length: 4-byte unsigned integer (size of header block, >= 3980)
- Seek Table Length: 4-byte unsigned integer (byte size of seek table, >= 3980)
- WAV Header Length: 4-byte unsigned integer (size of embedded WAV header data, >= 3980)
- Audio Data Length Low: 4-byte unsigned integer (lower 32 bits of compressed audio data size, >= 3980)
- Audio Data Length High: 4-byte unsigned integer (upper 32 bits of compressed audio data size for >4GB files, >= 3980)
- WAV Tail Length: 4-byte unsigned integer (size of terminating WAV data, >= 3980)
- MD5 Hash: 16-byte array (MD5 checksum of the file content excluding certain parts, >= 3980)
- Compression Type: 2-byte unsigned integer (compression level: 1000=fast, 2000=normal, 3000=high, 4000=extra high, 5000=insane)
- Format Flags: 2-byte unsigned integer (bit flags: e.g., bit 0=8-bit samples, bit 1=uses CRC32, bit 2=has peak level, bit 3=24-bit samples, bit 4=has seek elements, bit 5=creates WAV header on decode)
- Blocks Per Frame: 4-byte unsigned integer (audio blocks per frame, version-dependent defaults if not set)
- Final Frame Blocks: 4-byte unsigned integer (blocks in the last frame)
- Total Frames: 4-byte unsigned integer (total number of frames)
- Bits Per Sample: 2-byte unsigned integer (sample bit depth, typically 8/16/24)
- Channels: 2-byte unsigned integer (1=mono, 2=stereo)
- Sample Rate: 4-byte unsigned integer (sampling frequency in Hz, e.g., 44100)
Note: For versions <3980, the descriptor block is absent, and some fields (e.g., blocks per frame) are calculated or read differently; peak level (4 bytes) may be present if flagged. The implementation below focuses on version >=3980 for simplicity but can raise errors for older versions.
- Python class:
import struct
import os
import shutil
class APEFile:
def __init__(self):
self.properties = {}
self.header_start = 0 # Position after leading junk
self.original_file = None
def open(self, filename):
with open(filename, 'rb') as f:
data = f.read()
pos = 0
# Handle leading ID3v2 tag
if data[:3] == b'ID3':
# Synchsafe integer for size
size = data[6:10]
size = ((size[0] & 0x7F) << 21) | ((size[1] & 0x7F) << 14) | ((size[2] & 0x7F) << 7) | (size[3] & 0x7F)
pos = 10 + size
self.header_start = pos
if data[pos:pos+4] != b'MAC ':
raise ValueError("Not a valid APE file")
pos += 4
version = struct.unpack('<H', data[pos:pos+2])[0]
self.properties['version'] = version
pos += 2
if version < 3980:
raise ValueError("Versions < 3980 not supported in this implementation")
padding = struct.unpack('<H', data[pos:pos+2])[0]
self.properties['padding'] = padding
pos += 2
desc_len = struct.unpack('<I', data[pos:pos+4])[0]
self.properties['descriptor_length'] = desc_len
pos += 4
header_len = struct.unpack('<I', data[pos:pos+4])[0]
self.properties['header_length'] = header_len
pos += 4
seek_len = struct.unpack('<I', data[pos:pos+4])[0]
self.properties['seek_table_length'] = seek_len
pos += 4
wav_header_len = struct.unpack('<I', data[pos:pos+4])[0]
self.properties['wav_header_length'] = wav_header_len
pos += 4
audio_len = struct.unpack('<I', data[pos:pos+4])[0]
self.properties['audio_data_length_low'] = audio_len
pos += 4
audio_len_high = struct.unpack('<I', data[pos:pos+4])[0]
self.properties['audio_data_length_high'] = audio_len_high
pos += 4
wav_tail_len = struct.unpack('<I', data[pos:pos+4])[0]
self.properties['wav_tail_length'] = wav_tail_len
pos += 4
md5 = data[pos:pos+16]
self.properties['md5'] = md5
pos += 16
extra = desc_len - 52
if extra > 0:
pos += extra
compression = struct.unpack('<H', data[pos:pos+2])[0]
self.properties['compression_type'] = compression
pos += 2
flags = struct.unpack('<H', data[pos:pos+2])[0]
self.properties['format_flags'] = flags
pos += 2
blocks_per = struct.unpack('<I', data[pos:pos+4])[0]
self.properties['blocks_per_frame'] = blocks_per
pos += 4
final_blocks = struct.unpack('<I', data[pos:pos+4])[0]
self.properties['final_frame_blocks'] = final_blocks
pos += 4
total_frames = struct.unpack('<I', data[pos:pos+4])[0]
self.properties['total_frames'] = total_frames
pos += 4
bps = struct.unpack('<H', data[pos:pos+2])[0]
self.properties['bits_per_sample'] = bps
pos += 2
channels = struct.unpack('<H', data[pos:pos+2])[0]
self.properties['channels'] = channels
pos += 2
sample_rate = struct.unpack('<I', data[pos:pos+4])[0]
self.properties['sample_rate'] = sample_rate
# Store original for writing
self.original_file = filename
def get_property(self, key):
return self.properties.get(key)
def set_property(self, key, value):
if key in self.properties:
self.properties[key] = value
else:
raise KeyError("Invalid property")
def save(self, filename=None):
if not self.original_file:
raise ValueError("No file loaded")
if filename is None:
filename = self.original_file
else:
shutil.copy(self.original_file, filename)
with open(filename, 'r+b') as f:
f.seek(self.header_start)
f.write(b'MAC ')
f.write(struct.pack('<H', self.properties['version']))
f.write(struct.pack('<H', self.properties['padding']))
f.write(struct.pack('<I', self.properties['descriptor_length']))
f.write(struct.pack('<I', self.properties['header_length']))
f.write(struct.pack('<I', self.properties['seek_table_length']))
f.write(struct.pack('<I', self.properties['wav_header_length']))
f.write(struct.pack('<I', self.properties['audio_data_length_low']))
f.write(struct.pack('<I', self.properties['audio_data_length_high']))
f.write(struct.pack('<I', self.properties['wav_tail_length']))
f.write(self.properties['md5'])
# Assume no extra bytes for simplicity
f.write(struct.pack('<H', self.properties['compression_type']))
f.write(struct.pack('<H', self.properties['format_flags']))
f.write(struct.pack('<I', self.properties['blocks_per_frame']))
f.write(struct.pack('<I', self.properties['final_frame_blocks']))
f.write(struct.pack('<I', self.properties['total_frames']))
f.write(struct.pack('<H', self.properties['bits_per_sample']))
f.write(struct.pack('<H', self.properties['channels']))
f.write(struct.pack('<I', self.properties['sample_rate']))
- Java class:
import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
public class APEFile {
private ByteBuffer buffer;
private long headerStart = 0;
private String originalFile;
private int version;
private int padding;
private int descriptorLength;
private int headerLength;
private int seekTableLength;
private int wavHeaderLength;
private int audioDataLengthLow;
private int audioDataLengthHigh;
private int wavTailLength;
private byte[] md5 = new byte[16];
private int compressionType;
private int formatFlags;
private int blocksPerFrame;
private int finalFrameBlocks;
private int totalFrames;
private int bitsPerSample;
private int channels;
private int sampleRate;
public void open(String filename) throws IOException {
originalFile = filename;
RandomAccessFile raf = new RandomAccessFile(filename, "r");
FileChannel channel = raf.getChannel();
buffer = ByteBuffer.allocate((int) raf.length());
buffer.order(ByteOrder.LITTLE_ENDIAN);
channel.read(buffer);
buffer.flip();
int pos = 0;
byte[] id3 = new byte[3];
buffer.position(pos);
buffer.get(id3);
if (new String(id3).equals("ID3")) {
buffer.position(6);
int size = buffer.getInt();
size = ((size & 0x7F000000) >> 3) | ((size & 0x7F0000) >> 2) | ((size & 0x7F00) >> 1) | (size & 0x7F);
pos = 10 + size;
}
headerStart = pos;
buffer.position(pos);
byte[] sig = new byte[4];
buffer.get(sig);
if (!new String(sig).equals("MAC ")) {
throw new IOException("Not a valid APE file");
}
pos += 4;
version = buffer.getShort(pos) & 0xFFFF;
pos += 2;
if (version < 3980) {
throw new IOException("Versions < 3980 not supported");
}
padding = buffer.getShort(pos) & 0xFFFF;
pos += 2;
descriptorLength = buffer.getInt(pos);
pos += 4;
headerLength = buffer.getInt(pos);
pos += 4;
seekTableLength = buffer.getInt(pos);
pos += 4;
wavHeaderLength = buffer.getInt(pos);
pos += 4;
audioDataLengthLow = buffer.getInt(pos);
pos += 4;
audioDataLengthHigh = buffer.getInt(pos);
pos += 4;
wavTailLength = buffer.getInt(pos);
pos += 4;
buffer.position(pos);
buffer.get(md5);
pos += 16;
int extra = descriptorLength - 52;
if (extra > 0) {
pos += extra;
}
compressionType = buffer.getShort(pos) & 0xFFFF;
pos += 2;
formatFlags = buffer.getShort(pos) & 0xFFFF;
pos += 2;
blocksPerFrame = buffer.getInt(pos);
pos += 4;
finalFrameBlocks = buffer.getInt(pos);
pos += 4;
totalFrames = buffer.getInt(pos);
pos += 4;
bitsPerSample = buffer.getShort(pos) & 0xFFFF;
pos += 2;
channels = buffer.getShort(pos) & 0xFFFF;
pos += 2;
sampleRate = buffer.getInt(pos);
raf.close();
}
// Getters and setters for each property
public int getVersion() { return version; }
public void setVersion(int val) { version = val; }
public int getPadding() { return padding; }
public void setPadding(int val) { padding = val; }
public int getDescriptorLength() { return descriptorLength; }
public void setDescriptorLength(int val) { descriptorLength = val; }
public int getHeaderLength() { return headerLength; }
public void setHeaderLength(int val) { headerLength = val; }
public int getSeekTableLength() { return seekTableLength; }
public void setSeekTableLength(int val) { seekTableLength = val; }
public int getWavHeaderLength() { return wavHeaderLength; }
public void setWavHeaderLength(int val) { wavHeaderLength = val; }
public int getAudioDataLengthLow() { return audioDataLengthLow; }
public void setAudioDataLengthLow(int val) { audioDataLengthLow = val; }
public int getAudioDataLengthHigh() { return audioDataLengthHigh; }
public void setAudioDataLengthHigh(int val) { audioDataLengthHigh = val; }
public int getWavTailLength() { return wavTailLength; }
public void setWavTailLength(int val) { wavTailLength = val; }
public byte[] getMd5() { return md5.clone(); }
public void setMd5(byte[] val) { if (val.length == 16) md5 = val.clone(); }
public int getCompressionType() { return compressionType; }
public void setCompressionType(int val) { compressionType = val; }
public int getFormatFlags() { return formatFlags; }
public void setFormatFlags(int val) { formatFlags = val; }
public int getBlocksPerFrame() { return blocksPerFrame; }
public void setBlocksPerFrame(int val) { blocksPerFrame = val; }
public int getFinalFrameBlocks() { return finalFrameBlocks; }
public void setFinalFrameBlocks(int val) { finalFrameBlocks = val; }
public int getTotalFrames() { return totalFrames; }
public void setTotalFrames(int val) { totalFrames = val; }
public int getBitsPerSample() { return bitsPerSample; }
public void setBitsPerSample(int val) { bitsPerSample = val; }
public int getChannels() { return channels; }
public void setChannels(int val) { channels = val; }
public int getSampleRate() { return sampleRate; }
public void setSampleRate(int val) { sampleRate = val; }
public void save(String filename) throws IOException {
if (originalFile == null) {
throw new IOException("No file loaded");
}
if (filename == null) {
filename = originalFile;
} else {
File src = new File(originalFile);
File dest = new File(filename);
try (FileChannel srcChannel = new FileInputStream(src).getChannel();
FileChannel destChannel = new FileOutputStream(dest).getChannel()) {
srcChannel.transferTo(0, srcChannel.size(), destChannel);
}
}
RandomAccessFile raf = new RandomAccessFile(filename, "rw");
raf.seek(headerStart);
raf.write(new byte[]{'M', 'A', 'C', ' '});
raf.writeShort(version);
raf.writeShort(padding);
raf.writeInt(descriptorLength);
raf.writeInt(headerLength);
raf.writeInt(seekTableLength);
raf.writeInt(wavHeaderLength);
raf.writeInt(audioDataLengthLow);
raf.writeInt(audioDataLengthHigh);
raf.writeInt(wavTailLength);
raf.write(md5);
// Assume no extra
raf.writeShort(compressionType);
raf.writeShort(formatFlags);
raf.writeInt(blocksPerFrame);
raf.writeInt(finalFrameBlocks);
raf.writeInt(totalFrames);
raf.writeShort(bitsPerSample);
raf.writeShort(channels);
raf.writeInt(sampleRate);
raf.close();
}
}
- JavaScript class (for Node.js):
const fs = require('fs');
class APEFile {
constructor() {
this.properties = {};
this.headerStart = 0;
this.originalFile = null;
}
open(filename) {
const data = fs.readFileSync(filename);
let pos = 0;
if (data.slice(0, 3).toString() === 'ID3') {
let size = data.readUInt32BE(6);
size = ((size & 0x7F000000) >> 3) | ((size & 0x7F0000) >> 2) | ((size & 0x7F00) >> 1) | (size & 0x7F);
pos = 10 + size;
}
this.headerStart = pos;
if (data.slice(pos, pos + 4).toString() !== 'MAC ') {
throw new Error('Not a valid APE file');
}
pos += 4;
const version = data.readUInt16LE(pos);
this.properties.version = version;
pos += 2;
if (version < 3980) {
throw new Error('Versions < 3980 not supported');
}
const padding = data.readUInt16LE(pos);
this.properties.padding = padding;
pos += 2;
const descLen = data.readUInt32LE(pos);
this.properties.descriptor_length = descLen;
pos += 4;
const headerLen = data.readUInt32LE(pos);
this.properties.header_length = headerLen;
pos += 4;
const seekLen = data.readUInt32LE(pos);
this.properties.seek_table_length = seekLen;
pos += 4;
const wavHeaderLen = data.readUInt32LE(pos);
this.properties.wav_header_length = wavHeaderLen;
pos += 4;
const audioLen = data.readUInt32LE(pos);
this.properties.audio_data_length_low = audioLen;
pos += 4;
const audioLenHigh = data.readUInt32LE(pos);
this.properties.audio_data_length_high = audioLenHigh;
pos += 4;
const wavTailLen = data.readUInt32LE(pos);
this.properties.wav_tail_length = wavTailLen;
pos += 4;
const md5 = data.slice(pos, pos + 16);
this.properties.md5 = md5;
pos += 16;
let extra = descLen - 52;
if (extra > 0) {
pos += extra;
}
const compression = data.readUInt16LE(pos);
this.properties.compression_type = compression;
pos += 2;
const flags = data.readUInt16LE(pos);
this.properties.format_flags = flags;
pos += 2;
const blocksPer = data.readUInt32LE(pos);
this.properties.blocks_per_frame = blocksPer;
pos += 4;
const finalBlocks = data.readUInt32LE(pos);
this.properties.final_frame_blocks = finalBlocks;
pos += 4;
const totalFrames = data.readUInt32LE(pos);
this.properties.total_frames = totalFrames;
pos += 4;
const bps = data.readUInt16LE(pos);
this.properties.bits_per_sample = bps;
pos += 2;
const channels = data.readUInt16LE(pos);
this.properties.channels = channels;
pos += 2;
const sampleRate = data.readUInt32LE(pos);
this.properties.sample_rate = sampleRate;
this.originalFile = filename;
}
getProperty(key) {
return this.properties[key];
}
setProperty(key, value) {
if (key in this.properties) {
this.properties[key] = value;
} else {
throw new Error('Invalid property');
}
}
save(filename = null) {
if (!this.originalFile) {
throw new Error('No file loaded');
}
if (filename === null) {
filename = this.originalFile;
} else {
fs.copyFileSync(this.originalFile, filename);
}
const data = fs.readFileSync(filename);
const buf = Buffer.alloc(76); // Descriptor 52 + header 24 (assume no extra)
let pos = 0;
buf.write('MAC ', pos, 4);
pos += 4;
buf.writeUInt16LE(this.properties.version, pos);
pos += 2;
buf.writeUInt16LE(this.properties.padding, pos);
pos += 2;
buf.writeUInt32LE(this.properties.descriptor_length, pos);
pos += 4;
buf.writeUInt32LE(this.properties.header_length, pos);
pos += 4;
buf.writeUInt32LE(this.properties.seek_table_length, pos);
pos += 4;
buf.writeUInt32LE(this.properties.wav_header_length, pos);
pos += 4;
buf.writeUInt32LE(this.properties.audio_data_length_low, pos);
pos += 4;
buf.writeUInt32LE(this.properties.audio_data_length_high, pos);
pos += 4;
buf.writeUInt32LE(this.properties.wav_tail_length, pos);
pos += 4;
this.properties.md5.copy(buf, pos);
pos += 16;
buf.writeUInt16LE(this.properties.compression_type, pos);
pos += 2;
buf.writeUInt16LE(this.properties.format_flags, pos);
pos += 2;
buf.writeUInt32LE(this.properties.blocks_per_frame, pos);
pos += 4;
buf.writeUInt32LE(this.properties.final_frame_blocks, pos);
pos += 4;
buf.writeUInt32LE(this.properties.total_frames, pos);
pos += 4;
buf.writeUInt16LE(this.properties.bits_per_sample, pos);
pos += 2;
buf.writeUInt16LE(this.properties.channels, pos);
pos += 2;
buf.writeUInt32LE(this.properties.sample_rate, pos);
data.write(buf.toString('binary'), this.headerStart, buf.length, 'binary');
fs.writeFileSync(filename, data);
}
}
- C class (implemented as C++ class for object-oriented features):
#include <fstream>
#include <iostream>
#include <cstring>
#include <vector>
class APEFile {
private:
std::string originalFile;
size_t headerStart = 0;
unsigned short version;
unsigned short padding;
unsigned int descriptorLength;
unsigned int headerLength;
unsigned int seekTableLength;
unsigned int wavHeaderLength;
unsigned int audioDataLengthLow;
unsigned int audioDataLengthHigh;
unsigned int wavTailLength;
unsigned char md5[16];
unsigned short compressionType;
unsigned short formatFlags;
unsigned int blocksPerFrame;
unsigned int finalFrameBlocks;
unsigned int totalFrames;
unsigned short bitsPerSample;
unsigned short channels;
unsigned int sampleRate;
public:
APEFile() {}
~APEFile() {}
void open(const std::string& filename) {
originalFile = filename;
std::ifstream file(filename, std::ios::binary | std::ios::ate);
size_t size = file.tellg();
file.seekg(0);
std::vector<char> data(size);
file.read(data.data(), size);
size_t pos = 0;
if (std::strncmp(&data[0], "ID3", 3) == 0) {
unsigned int id3size = 0;
id3size |= (static_cast<unsigned char>(data[6]) & 0x7F) << 21;
id3size |= (static_cast<unsigned char>(data[7]) & 0x7F) << 14;
id3size |= (static_cast<unsigned char>(data[8]) & 0x7F) << 7;
id3size |= (static_cast<unsigned char>(data[9]) & 0x7F);
pos = 10 + id3size;
}
headerStart = pos;
if (std::strncmp(&data[pos], "MAC ", 4) != 0) {
throw std::runtime_error("Not a valid APE file");
}
pos += 4;
memcpy(&version, &data[pos], 2);
pos += 2;
if (version < 3980) {
throw std::runtime_error("Versions < 3980 not supported");
}
memcpy(&padding, &data[pos], 2);
pos += 2;
memcpy(&descriptorLength, &data[pos], 4);
pos += 4;
memcpy(&headerLength, &data[pos], 4);
pos += 4;
memcpy(&seekTableLength, &data[pos], 4);
pos += 4;
memcpy(&wavHeaderLength, &data[pos], 4);
pos += 4;
memcpy(&audioDataLengthLow, &data[pos], 4);
pos += 4;
memcpy(&audioDataLengthHigh, &data[pos], 4);
pos += 4;
memcpy(&wavTailLength, &data[pos], 4);
pos += 4;
memcpy(md5, &data[pos], 16);
pos += 16;
size_t extra = descriptorLength - 52;
if (extra > 0) {
pos += extra;
}
memcpy(&compressionType, &data[pos], 2);
pos += 2;
memcpy(&formatFlags, &data[pos], 2);
pos += 2;
memcpy(&blocksPerFrame, &data[pos], 4);
pos += 4;
memcpy(&finalFrameBlocks, &data[pos], 4);
pos += 4;
memcpy(&totalFrames, &data[pos], 4);
pos += 4;
memcpy(&bitsPerSample, &data[pos], 2);
pos += 2;
memcpy(&channels, &data[pos], 2);
pos += 2;
memcpy(&sampleRate, &data[pos], 4);
}
// Getters and setters
unsigned short getVersion() { return version; }
void setVersion(unsigned short val) { version = val; }
unsigned short getPadding() { return padding; }
void setPadding(unsigned short val) { padding = val; }
unsigned int getDescriptorLength() { return descriptorLength; }
void setDescriptorLength(unsigned int val) { descriptorLength = val; }
unsigned int getHeaderLength() { return headerLength; }
void setHeaderLength(unsigned int val) { headerLength = val; }
unsigned int getSeekTableLength() { return seekTableLength; }
void setSeekTableLength(unsigned int val) { seekTableLength = val; }
unsigned int getWavHeaderLength() { return wavHeaderLength; }
void setWavHeaderLength(unsigned int val) { wavHeaderLength = val; }
unsigned int getAudioDataLengthLow() { return audioDataLengthLow; }
void setAudioDataLengthLow(unsigned int val) { audioDataLengthLow = val; }
unsigned int getAudioDataLengthHigh() { return audioDataLengthHigh; }
void setAudioDataLengthHigh(unsigned int val) { audioDataLengthHigh = val; }
unsigned int getWavTailLength() { return wavTailLength; }
void setWavTailLength(unsigned int val) { wavTailLength = val; }
void getMd5(unsigned char* out) { memcpy(out, md5, 16); }
void setMd5(const unsigned char* val) { memcpy(md5, val, 16); }
unsigned short getCompressionType() { return compressionType; }
void setCompressionType(unsigned short val) { compressionType = val; }
unsigned short getFormatFlags() { return formatFlags; }
void setFormatFlags(unsigned short val) { formatFlags = val; }
unsigned int getBlocksPerFrame() { return blocksPerFrame; }
void setBlocksPerFrame(unsigned int val) { blocksPerFrame = val; }
unsigned int getFinalFrameBlocks() { return finalFrameBlocks; }
void setFinalFrameBlocks(unsigned int val) { finalFrameBlocks = val; }
unsigned int getTotalFrames() { return totalFrames; }
void setTotalFrames(unsigned int val) { totalFrames = val; }
unsigned short getBitsPerSample() { return bitsPerSample; }
void setBitsPerSample(unsigned short val) { bitsPerSample = val; }
unsigned short getChannels() { return channels; }
void setChannels(unsigned short val) { channels = val; }
unsigned int getSampleRate() { return sampleRate; }
void setSampleRate(unsigned int val) { sampleRate = val; }
void save(const std::string& filename = "") {
std::string target = filename.empty() ? originalFile : filename;
if (!filename.empty()) {
std::ifstream src(originalFile, std::ios::binary);
std::ofstream dst(target, std::ios::binary);
dst << src.rdbuf();
}
std::fstream file(target, std::ios::binary | std::ios::in | std::ios::out);
file.seekp(headerStart);
file.write("MAC ", 4);
file.write(reinterpret_cast<const char*>(&version), 2);
file.write(reinterpret_cast<const char*>(&padding), 2);
file.write(reinterpret_cast<const char*>(&descriptorLength), 4);
file.write(reinterpret_cast<const char*>(&headerLength), 4);
file.write(reinterpret_cast<const char*>(&seekTableLength), 4);
file.write(reinterpret_cast<const char*>(&wavHeaderLength), 4);
file.write(reinterpret_cast<const char*>(&audioDataLengthLow), 4);
file.write(reinterpret_cast<const char*>(&audioDataLengthHigh), 4);
file.write(reinterpret_cast<const char*>(&wavTailLength), 4);
file.write(reinterpret_cast<const char*>(md5), 16);
// Assume no extra
file.write(reinterpret_cast<const char*>(&compressionType), 2);
file.write(reinterpret_cast<const char*>(&formatFlags), 2);
file.write(reinterpret_cast<const char*>(&blocksPerFrame), 4);
file.write(reinterpret_cast<const char*>(&finalFrameBlocks), 4);
file.write(reinterpret_cast<const char*>(&totalFrames), 4);
file.write(reinterpret_cast<const char*>(&bitsPerSample), 2);
file.write(reinterpret_cast<const char*>(&channels), 2);
file.write(reinterpret_cast<const char*>(&sampleRate), 4);
}
};