Task 021: .AIFC File Format
Task 021: .AIFC File Format
1. Properties of the .AIFC File Format Intrinsic to Its File System
The AIFC (Audio Interchange File Format - Compressed) is an extension of the AIFF format, developed by Apple Inc., that supports compressed audio data. It is based on the Electronic Arts Interchange File Format (IFF) and uses a chunk-based structure. Below is a list of intrinsic properties of the .AIFC file format based on its specifications:
- File Extension: Typically
.aifc
, but may also use.aiff
or.aif
. - Byte Order: Big-endian for the file header and metadata; audio data can be big-endian or little-endian (e.g.,
sowt
codec uses little-endian for PCM data). - Container Format: IFF-based, using chunks to store different types of data.
- FORM Identifier: Set to
AIFC
to distinguish fromAIFF
. - Chunk Structure:
- Common Chunk (COMM): Contains essential audio metadata:
- Number of channels (e.g., 1 for mono, 2 for stereo).
- Number of sample frames.
- Sample size (bit depth, e.g., 8, 16, 24, or 32 bits).
- Sample rate (e.g., 44100 Hz for CD quality).
- Compression type (e.g.,
NONE
,sowt
,ulaw
,alaw
,fl32
,ACE2
,MAC6
). - Compression name (human-readable string describing the compression type).
- Sound Data Chunk (SSND): Stores the compressed or uncompressed audio sample data.
- Offset and block size for alignment.
- Actual audio data.
- Format Version Chunk (FVER): Specifies the version of the AIFC specification.
- Timestamp of the format version.
- Sound Accelerator Chunk (SACC): Optional, used to eliminate decompression artifacts for random access playback.
- Comment Chunk (COMT): Optional, stores timestamped comments or metadata.
- Other Optional Chunks: May include Application-Specific (
APPL
), Annotation (ANNO
), or others for additional metadata like loop points or musical note data. - Compression Types: Supports various codecs, including:
NONE
: Uncompressed PCM.sowt
: Little-endian PCM (introduced for Intel-based Macs).ulaw
,alaw
: Logarithmic compression for telephony.fl32
,fl64
: Floating-point audio formats.ACE2
,MAC6
: Apple-specific compression codecs.- Audio Data Characteristics:
- Sample rate (e.g., 8000 Hz, 22050 Hz, 44100 Hz, etc.).
- Bit depth (e.g., 8-bit, 16-bit, 24-bit, 32-bit).
- Number of channels (mono, stereo, or multi-channel).
- File Size: Varies depending on compression; uncompressed PCM requires about 10 MB per minute for stereo at 44.1 kHz, 16-bit.
- Metadata Support: Can include loop point data, musical note data, and other metadata for use in samplers or musical applications.
- Platform Compatibility: Primarily used on Macintosh systems, but supported on other platforms via compatible software.
These properties are derived from the AIFC specification, which extends AIFF by adding compression support and additional chunks like FVER and SACC.
2. Python Class for .AIFC File Handling
Below is a Python class that uses the built-in aifc
module to open, read, write, and print .AIFC file properties. It handles the properties listed above.
import aifc
import os
class AIFCFile:
def __init__(self, filename):
self.filename = filename
self.mode = None
self.aifc_obj = None
def open_read(self):
"""Open an AIFC file for reading and print its properties."""
try:
self.aifc_obj = aifc.open(self.filename, 'rb')
self.mode = 'r'
self.print_properties()
except aifc.Error as e:
print(f"Error opening {self.filename}: {e}")
def print_properties(self):
"""Print intrinsic properties of the AIFC file."""
if self.aifc_obj is None:
print("No file opened.")
return
print(f"File: {self.filename}")
print(f"File Extension: {os.path.splitext(self.filename)[1]}")
print(f"Number of Channels: {self.aifc_obj.getnchannels()}")
print(f"Sample Frames: {self.aifc_obj.getnframes()}")
print(f"Sample Size (bits): {self.aifc_obj.getsampwidth() * 8}")
print(f"Sample Rate (Hz): {self.aifc_obj.getframerate()}")
print(f"Compression Type: {self.aifc_obj.getcomptype().decode()}")
print(f"Compression Name: {self.aifc_obj.getcompname().decode()}")
# Note: FVER and SACC chunks are not directly accessible via aifc module
print(f"Byte Order: Big-endian (header), {'little-endian' if self.aifc_obj.getcomptype() == b'sowt' else 'big-endian'} (audio data)")
# Estimate file size
print(f"File Size: {os.path.getsize(self.filename)} bytes")
# COMT chunk (comments) may not be present
try:
comments = self.aifc_obj.getmarkers()
print(f"Comments: {comments if comments else 'None'}")
except:
print("Comments: Not accessible")
def write(self, output_filename, audio_data=None, nchannels=2, sampwidth=2, framerate=44100, comptype=b'NONE', compname=b'not compressed'):
"""Write a new AIFC file with specified parameters."""
try:
self.aifc_obj = aifc.open(output_filename, 'wb')
self.aifc_obj.setnchannels(nchannels)
self.aifc_obj.setsampwidth(sampwidth)
self.aifc_obj.setframerate(framerate)
self.aifc_obj.setcomptype(comptype, compname)
if audio_data:
self.aifc_obj.writeframes(audio_data)
self.aifc_obj.close()
print(f"Successfully wrote to {output_filename}")
except aifc.Error as e:
print(f"Error writing {output_filename}: {e}")
def close(self):
"""Close the AIFC file."""
if self.aifc_obj:
self.aifc_obj.close()
self.aifc_obj = None
self.mode = None
# Example usage
if __name__ == "__main__":
# Example: Reading an existing AIFC file
aifc_file = AIFCFile("example.aifc")
aifc_file.open_read()
aifc_file.close()
# Example: Writing a new AIFC file (minimal example, no audio data)
aifc_file.write("output.aifc", nchannels=2, sampwidth=2, framerate=44100, comptype=b'NONE', compname=b'not compressed')
Notes:
- The
aifc
module in Python provides direct access to most COMM chunk properties but does not natively expose FVER or SACC chunks. - To handle audio data, you would need actual sample data (e.g., from
numpy
or another source). - Error handling ensures robust file operations.
3. Java Class for .AIFC File Handling
Java does not have built-in support for AIFC, but we can use the javax.sound.sampled
package for basic audio file handling and parse the file structure manually for AIFC-specific properties. Below is a simplified implementation focusing on reading and printing properties.
import javax.sound.sampled.*;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class AIFCFile {
private String filename;
private AudioInputStream audioInputStream;
public AIFCFile(String filename) {
this.filename = filename;
}
public void openRead() {
try {
File file = new File(filename);
audioInputStream = AudioSystem.getAudioInputStream(file);
AudioFormat format = audioInputStream.getFormat();
printProperties(file, format);
} catch (UnsupportedAudioFileException | IOException e) {
System.err.println("Error opening " + filename + ": " + e.getMessage());
}
}
private void printProperties(File file, AudioFormat format) {
System.out.println("File: " + filename);
System.out.println("File Extension: " + filename.substring(filename.lastIndexOf('.')));
System.out.println("Number of Channels: " + format.getChannels());
System.out.println("Sample Rate (Hz): " + format.getSampleRate());
System.out.println("Sample Size (bits): " + format.getSampleSizeInBits());
System.out.println("Frame Size: " + format.getFrameSize());
System.out.println("File Size: " + file.length() + " bytes");
System.out.println("Byte Order: " + (format.isBigEndian() ? "Big-endian" : "Little-endian"));
// Compression type and other chunks require manual parsing
try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
byte[] header = new byte[12];
raf.read(header);
String formType = new String(header, 8, 4);
System.out.println("FORM Identifier: " + formType);
// Read COMM chunk (simplified)
raf.seek(12); // Skip FORM header
byte[] chunkId = new byte[4];
raf.read(chunkId);
if (new String(chunkId).equals("COMM")) {
byte[] chunkData = new byte[18];
raf.read(chunkData);
ByteBuffer buffer = ByteBuffer.wrap(chunkData).order(ByteOrder.BIG_ENDIAN);
short numChannels = buffer.getShort();
int numSampleFrames = buffer.getInt();
short sampleSize = buffer.getShort();
// Extended float for sample rate (simplified)
System.out.println("COMM Chunk - Channels: " + numChannels);
System.out.println("COMM Chunk - Sample Frames: " + numSampleFrames);
System.out.println("COMM Chunk - Sample Size: " + sampleSize);
}
// FVER and SACC chunks require similar parsing but are omitted for brevity
} catch (IOException e) {
System.err.println("Error reading chunks: " + e.getMessage());
}
}
public void write(String outputFilename, int nchannels, int sampwidth, float framerate, String comptype) {
try {
AudioFormat format = new AudioFormat(framerate, sampwidth * 8, nchannels, true, true);
File file = new File(outputFilename);
// Note: Java's AudioSystem does not natively support writing AIFC with compression
// This is a placeholder for writing uncompressed AIFF
AudioSystem.write(null, AudioFileFormat.Type.AIFF, file);
System.out.println("Successfully wrote to " + outputFilename);
} catch (IOException e) {
System.err.println("Error writing " + outputFilename + ": " + e.getMessage());
}
}
public void close() {
try {
if (audioInputStream != null) {
audioInputStream.close();
}
} catch (IOException e) {
System.err.println("Error closing file: " + e.getMessage());
}
}
public static void main(String[] args) {
AIFCFile aifcFile = new AIFCFile("example.aifc");
aifcFile.openRead();
aifcFile.close();
// Example writing (uncompressed, as Java lacks native AIFC compression support)
aifcFile.write("output.aifc", 2, 2, 44100, "NONE");
}
}
Notes:
- Java’s
javax.sound.sampled
supports AIFF but not all AIFC compression types. Manual chunk parsing is needed for FVER, SACC, and specific compression types. - Writing compressed AIFC files requires external libraries (e.g., Tritonus) or custom implementation, which is complex and omitted here for simplicity.
- The example reads basic COMM chunk data and prints properties.
4. JavaScript Class for .AIFC File Handling
JavaScript in a browser environment has limited direct file access, but Node.js with the fs
module can handle file operations. Below is a Node.js-based class to read and print AIFC properties. Writing AIFC files is complex due to the lack of native libraries, so a basic placeholder is provided.
const fs = require('fs');
class AIFCFile {
constructor(filename) {
this.filename = filename;
this.fileBuffer = null;
}
openRead() {
try {
this.fileBuffer = fs.readFileSync(this.filename);
this.printProperties();
} catch (error) {
console.error(`Error opening ${this.filename}: ${error.message}`);
}
}
printProperties() {
if (!this.fileBuffer) {
console.log('No file opened.');
return;
}
console.log(`File: ${this.filename}`);
console.log(`File Extension: ${this.filename.split('.').pop()}`);
console.log(`File Size: ${this.fileBuffer.length} bytes`);
// Parse FORM and COMM chunks
const formId = this.fileBuffer.toString('ascii', 0, 4);
const formType = this.fileBuffer.toString('ascii', 8, 12);
console.log(`FORM Identifier: ${formType}`);
// COMM chunk parsing (simplified)
let offset = 12;
const chunkId = this.fileBuffer.toString('ascii', offset, offset + 4);
if (chunkId === 'COMM') {
const view = new DataView(this.fileBuffer.buffer, offset + 8);
const numChannels = view.getInt16(0, false); // Big-endian
const numSampleFrames = view.getInt32(2, false);
const sampleSize = view.getInt16(6, false);
const sampleRate = this.readExtendedFloat(view, 8); // Simplified
const compType = this.fileBuffer.toString('ascii', offset + 26, offset + 30);
console.log(`Number of Channels: ${numChannels}`);
console.log(`Sample Frames: ${numSampleFrames}`);
console.log(`Sample Size (bits): ${sampleSize}`);
console.log(`Sample Rate (Hz): ${sampleRate}`);
console.log(`Compression Type: ${compType}`);
console.log(`Byte Order: ${compType === 'sowt' ? 'Little-endian' : 'Big-endian'} (audio data)`);
}
// FVER and SACC chunks require additional parsing
console.log('Comments: Not parsed (COMT chunk)');
}
// Helper to read 80-bit extended float (simplified)
readExtendedFloat(view, offset) {
// AIFC uses 80-bit extended float for sample rate; this is a placeholder
return view.getFloat32(offset, false);
}
write(outputFilename, nchannels = 2, sampwidth = 2, framerate = 44100, comptype = 'NONE') {
// Placeholder: Writing AIFC is complex and requires external libraries
console.log(`Writing ${outputFilename} with channels=${nchannels}, sampwidth=${sampwidth}, framerate=${framerate}, comptype=${comptype}`);
console.log('Writing AIFC files is not fully supported in this example.');
}
close() {
this.fileBuffer = null;
}
}
// Example usage
const aifc = new AIFCFile('example.aifc');
aifc.openRead();
aifc.close();
aifc.write('output.aifc');
Notes:
- Node.js is used for file access. Browser-based JavaScript would require a file input element and
FileReader
. - Parsing the COMM chunk is shown; FVER and SACC require similar binary parsing but are omitted for brevity.
- Writing AIFC files in JavaScript is non-trivial without libraries like
audio-file-encoder
.
5. C Class for .AIFC File Handling
C does not have a standard library for AIFC, so we implement a basic class-like structure to parse the file manually. This example focuses on reading and printing properties.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char* filename;
FILE* file;
} AIFCFile;
AIFCFile* aifcfile_new(const char* filename) {
AIFCFile* aifc = (AIFCFile*)malloc(sizeof(AIFCFile));
aifc->filename = strdup(filename);
aifc->file = NULL;
return aifc;
}
void aifcfile_open_read(AIFCFile* aifc) {
aifc->file = fopen(aifc->filename, "testing_file);
if (!aifc->file) {
printf("Error opening %s\n", aifc->filename);
return;
}
aifcfile_print_properties(aifc);
}
void aifcfile_print_properties(AIFCFile* aifc) {
printf("File: %s\n", aifc->filename);
printf("File Extension: %s\n", strrchr(aifc->filename, '.'));
fseek(aifc->file, 0, SEEK_END);
printf("File Size: %ld bytes\n", ftell(aifc->file));
rewind(aifc->file);
char form_id[5] = {0};
fread(form_id, 1, 4, aifc->file);
char form_type[5] = {0};
fseek(aifc->file, 8, SEEK_SET);
fread(form_type, 1, 4, aifc->file);
printf("FORM Identifier: %s\n", form_type);
fseek(aifc->file, 12, SEEK_SET);
char chunk_id[5] = {0};
fread(chunk_id, 1, 4, aifc->file);
if (strcmp(chunk_id, "COMM") == 0) {
unsigned char chunk_data[18];
fread(chunk_data, 1, 18, aifc->file);
unsigned short num_channels = (chunk_data[0] << 8) | chunk_data[1];
unsigned int num_sample_frames = (chunk_data[2] << 24) | (chunk_data[3] << 16) | (chunk_data[4] << 8) | chunk_data[5];
unsigned short sample_size = (chunk_data[6] << 8) | chunk_data[7];
// Sample rate (80-bit extended float, simplified)
double sample_rate = *(double*)&chunk_data[8]; // Approximation
char comp_type[5] = {0};
fseek(aifc->file, 26, SEEK_CUR);
fread(comp_type, 1, 4, aifc->file);
printf("Number of Channels: %u\n", num_channels);
printf("Sample Frames: %u\n", num_sample_frames);
printf("Sample Size (bits): %u\n", sample_size);
printf("Sample Rate (Hz): %.0f\n", sample_rate);
printf("Compression Type: %s\n", comp_type);
printf("Byte Order: %s (audio data)\n", strcmp(comp_type, "sowt") == 0 ? "Little-endian" : "Big-endian");
}
printf("Comments: Not parsed (COMT chunk)\n");
}
void aifcfile_write(AIFCFile* aifc, const char* output_filename, int nchannels, int sampwidth, int framerate, const char* comptype) {
// Placeholder: Writing AIFC requires complex chunk construction
printf("Writing %s with channels=%d, sampwidth=%d, framerate=%d, comptype=%s\n",
output_filename, nchannels, sampwidth, framerate, comptype);
printf("Writing AIFC files is not fully supported in this example.\n");
}
void aifcfile_close(AIFCFile* aifc) {
if (aifc->file) {
fclose(aifc->file);
aifc->file = NULL;
}
}
void aifcfile_free(AIFCFile* aifc) {
aifcfile_close(aifc);
free(aifc->filename);
free(aifc);
}
int main() {
AIFCFile* aifc = aifcfile_new("example.aifc");
aifcfile_open_read(aifc);
aifcfile_write(aifc, "output.aifc", 2, 2, 44100, "NONE");
aifcfile_free(aifc);
return 0;
}
Notes:
- The C implementation manually parses the COMM chunk to extract properties.
- Writing AIFC files requires constructing the chunk structure manually, which is complex and not fully implemented here.
- The sample rate parsing is approximated due to the complexity of the 80-bit extended float format.
General Notes
- Reading: All implementations parse the COMM chunk to extract core audio properties. FVER, SACC, and COMT chunks are not fully parsed due to complexity and limited library support.
- Writing: Python’s
aifc
module supports writing basic AIFC files, but Java, JavaScript, and C implementations are limited due to the lack of native AIFC compression support. - Testing: These classes assume the presence of an
example.aifc
file. You may need to provide a valid AIFC file or generate one for testing. - Limitations: Compression-specific handling (e.g.,
ulaw
,alaw
) requires additional libraries or manual implementation, which is beyond the scope of this basic example. - Sources: The properties and structure are based on the AIFC specification from Apple and related documentation.
Let me know if you need further clarification or additional features!
1. List of Properties Intrinsic to the .AIFC File Format
Based on the AIFC file format specifications (an extension of AIFF for compressed audio, following the EA IFF 85 standard), the format is chunk-based with a top-level FORM chunk of type 'AIFC'. The intrinsic properties are the metadata fields stored in the required and optional chunks, excluding the raw sound data itself (which is variable and not a "property" but the payload). These properties define the file's structure, audio parameters, and additional metadata. All data is big-endian unless noted.
Required Properties (from mandatory chunks: FVER, COMM, SSND):
- Format Version Timestamp: Unsigned long (32-bit), representing the version timestamp (e.g., 0xA2805140 for version 1, seconds since Jan 1, 1904).
- Number of Audio Channels: Short (16-bit signed), e.g., 1 for mono, 2 for stereo.
- Number of Sample Frames: Unsigned long (32-bit), total frames in the sound data.
- Sample Size: Short (16-bit signed), bits per sample (1-32; for compressed, this is the original bit depth).
- Sample Rate: Extended (80-bit IEEE 754 floating-point), samples per second.
- Compression Type: ID (32-bit, 4 ASCII chars), e.g., 'NONE' for uncompressed, 'ACE2' for 2:1 ACE, 'MAC3' for MACE 3:1.
- Compression Name: Pstring (Pascal-style: 1-byte unsigned count + chars; padded to even length if needed), human-readable name like "not compressed".
- Sound Data Offset: Unsigned long (32-bit), byte offset to first sample frame (usually 0).
- Sound Data Block Size: Unsigned long (32-bit), alignment block size (usually 0 for no block alignment).
Optional Properties (from optional chunks; can be absent or multiple where noted):
- Markers: List of markers, each with:
- Marker ID: Short (16-bit signed), unique identifier.
- Position: Unsigned long (32-bit), sample frame offset.
- Marker Name: Pstring (as above).
- Comments: List of comments, each with:
- Timestamp: Unsigned long (32-bit), seconds since Jan 1, 1904.
- Marker ID: Short (16-bit signed), linked marker or 0 if none.
- Text: String (length from 16-bit unsigned count + chars).
- Instrument Data:
- Base Note: Unsigned char (8-bit), MIDI note for middle C (60).
- Detune: Signed char (8-bit), cents (-50 to +50).
- Low Note: Unsigned char (8-bit), lowest MIDI note.
- High Note: Unsigned char (8-bit), highest MIDI note.
- Low Velocity: Unsigned char (8-bit), lowest MIDI velocity.
- High Velocity: Unsigned char (8-bit), highest MIDI velocity.
- Gain: Short (16-bit signed), dB gain.
- Sustain Loop:
- Play Mode: Short (16-bit signed; 0=no loop, 1=forward, 2=forward/backward).
- Begin Loop Marker ID: Short (16-bit signed).
- End Loop Marker ID: Short (16-bit signed).
- Release Loop: Same structure as Sustain Loop.
- Name: String (chars, padded to even length if needed).
- Author: String (as above).
- Copyright: String (as above).
- Annotations: List of strings (multiple ANNO chunks allowed; each as above).
- AES Channel Status Data: 24-byte binary array (Audio Engineering Society channel status).
- MIDI Data: Binary array (variable length MIDI data).
- Application Specific Data: List of entries, each with:
- Application Signature: OSType (32-bit, 4 chars).
- Data: Binary array (variable).
Unknown chunks are ignored during parsing. The raw sound data in SSND is not listed as a property but must be handled for full file I/O.
2. Python Class
import struct
import os
class AIFCFile:
def __init__(self):
self.format_version_timestamp = 0
self.num_channels = 0
self.num_sample_frames = 0
self.sample_size = 0
self.sample_rate = 0.0 # Approximate as float; extended is 80-bit
self.compression_type = b''
self.compression_name = ''
self.sound_data_offset = 0
self.sound_data_block_size = 0
self.markers = [] # List of (id, position, name)
self.comments = [] # List of (timestamp, marker_id, text)
self.instrument = {
'base_note': 0, 'detune': 0, 'low_note': 0, 'high_note': 0,
'low_velocity': 0, 'high_velocity': 0, 'gain': 0,
'sustain_loop': {'play_mode': 0, 'begin': 0, 'end': 0},
'release_loop': {'play_mode': 0, 'begin': 0, 'end': 0}
}
self.name = ''
self.author = ''
self.copyright = ''
self.annotations = [] # List of strings
self.aes_data = b''
self.midi_data = b''
self.app_specific = [] # List of (signature, data)
self.sound_data = b'' # Raw sound data (for full read/write)
def _read_extended(self, data):
# Unpack 80-bit extended to float (approximate)
exp = struct.unpack('>h', data[0:2])[0]
hi = struct.unpack('>I', data[2:6])[0]
lo = struct.unpack('>I', data[6:10])[0]
mant = (hi << 32 | lo) / (1 << 63)
return mant * (2 ** (exp - 16383))
def _write_extended(self, value):
# Approximate float to 80-bit extended
if value == 0:
return b'\x00' * 10
import math
sign = 0 if value > 0 else 0x8000
value = abs(value)
exp = math.floor(math.log2(value)) + 16383
mant = value / (2 ** (exp - 16383))
hi = int(mant * (1 << 31))
lo = int((mant * (1 << 31) - hi) * (1 << 32))
return struct.pack('>hII', sign | (exp & 0x7FFF), hi, lo)
def _read_pstring(self, f):
count = struct.unpack('B', f.read(1))[0]
s = f.read(count).decode('ascii')
if count % 2 == 0:
f.read(1) # Pad
return s
def _write_pstring(self, s):
count = len(s)
pad = b'\x00' if count % 2 == 0 else b''
return struct.pack('B', count) + s.encode('ascii') + pad
def _read_string(self, f, size):
s = f.read(size).decode('ascii').rstrip('\x00')
if size % 2 == 1:
f.read(1) # Pad
return s
def _write_string(self, s, pad_to_even=True):
data = s.encode('ascii')
if pad_to_even and len(data) % 2 == 1:
data += b'\x00'
return data
def read(self, file_path):
with open(file_path, 'rb') as f:
ck_id = f.read(4)
if ck_id != b'FORM':
raise ValueError("Not a valid AIFC file")
ck_size = struct.unpack('>I', f.read(4))[0]
form_type = f.read(4)
if form_type != b'AIFC':
raise ValueError("Not AIFC format")
end_pos = f.tell() + ck_size - 4
while f.tell() < end_pos:
ck_id = f.read(4)
if len(ck_id) == 0:
break
ck_size = struct.unpack('>I', f.read(4))[0]
start_pos = f.tell()
if ck_id == b'FVER':
self.format_version_timestamp = struct.unpack('>I', f.read(4))[0]
elif ck_id == b'COMM':
self.num_channels = struct.unpack('>h', f.read(2))[0]
self.num_sample_frames = struct.unpack('>I', f.read(4))[0]
self.sample_size = struct.unpack('>h', f.read(2))[0]
self.sample_rate = self._read_extended(f.read(10))
self.compression_type = f.read(4)
self.compression_name = self._read_pstring(f)
elif ck_id == b'SSND':
self.sound_data_offset = struct.unpack('>I', f.read(4))[0]
self.sound_data_block_size = struct.unpack('>I', f.read(4))[0]
data_size = ck_size - 8
f.seek(self.sound_data_offset, 1)
self.sound_data = f.read(data_size - self.sound_data_offset)
elif ck_id == b'MARK':
num_markers = struct.unpack('>H', f.read(2))[0]
for _ in range(num_markers):
mid = struct.unpack('>h', f.read(2))[0]
pos = struct.unpack('>I', f.read(4))[0]
name = self._read_pstring(f)
self.markers.append((mid, pos, name))
elif ck_id == b'COMT':
num_comm = struct.unpack('>H', f.read(2))[0]
for _ in range(num_comm):
ts = struct.unpack('>I', f.read(4))[0]
mid = struct.unpack('>h', f.read(2))[0]
count = struct.unpack('>H', f.read(2))[0]
text = self._read_string(f, count)
self.comments.append((ts, mid, text))
elif ck_id == b'INST':
self.instrument['base_note'] = struct.unpack('B', f.read(1))[0]
self.instrument['detune'] = struct.unpack('b', f.read(1))[0]
self.instrument['low_note'] = struct.unpack('B', f.read(1))[0]
self.instrument['high_note'] = struct.unpack('B', f.read(1))[0]
self.instrument['low_velocity'] = struct.unpack('B', f.read(1))[0]
self.instrument['high_velocity'] = struct.unpack('B', f.read(1))[0]
self.instrument['gain'] = struct.unpack('>h', f.read(2))[0]
self.instrument['sustain_loop']['play_mode'] = struct.unpack('>h', f.read(2))[0]
self.instrument['sustain_loop']['begin'] = struct.unpack('>h', f.read(2))[0]
self.instrument['sustain_loop']['end'] = struct.unpack('>h', f.read(2))[0]
self.instrument['release_loop']['play_mode'] = struct.unpack('>h', f.read(2))[0]
self.instrument['release_loop']['begin'] = struct.unpack('>h', f.read(2))[0]
self.instrument['release_loop']['end'] = struct.unpack('>h', f.read(2))[0]
elif ck_id == b'NAME':
self.name = self._read_string(f, ck_size)
elif ck_id == b'AUTH':
self.author = self._read_string(f, ck_size)
elif ck_id == b'(c) ':
self.copyright = self._read_string(f, ck_size)
elif ck_id == b'ANNO':
self.annotations.append(self._read_string(f, ck_size))
elif ck_id == b'AESD':
self.aes_data = f.read(24)
elif ck_id == b'MIDI':
self.midi_data = f.read(ck_size)
elif ck_id == b'APPL':
sig = f.read(4)
data = f.read(ck_size - 4)
self.app_specific.append((sig, data))
else:
f.seek(ck_size, 1) # Skip unknown
if (f.tell() - start_pos) % 2 == 1:
f.read(1) # Pad to even
def write(self, file_path):
chunks = []
# FVER
chunks.append(b'FVER' + struct.pack('>I', 4) + struct.pack('>I', self.format_version_timestamp))
# COMM
comm_data = struct.pack('>hIh', self.num_channels, self.num_sample_frames, self.sample_size) + \
self._write_extended(self.sample_rate) + self.compression_type + \
self._write_pstring(self.compression_name)
chunks.append(b'COMM' + struct.pack('>I', len(comm_data)) + comm_data)
# SSND
ssnd_data = struct.pack('>II', self.sound_data_offset, self.sound_data_block_size) + self.sound_data
if len(ssnd_data) % 2 == 1:
ssnd_data += b'\x00'
chunks.append(b'SSND' + struct.pack('>I', len(ssnd_data)) + ssnd_data)
# MARK
if self.markers:
mark_data = struct.pack('>H', len(self.markers))
for mid, pos, name in self.markers:
mark_data += struct.pack('>hI', mid, pos) + self._write_pstring(name)
chunks.append(b'MARK' + struct.pack('>I', len(mark_data)) + mark_data)
# COMT
if self.comments:
comt_data = struct.pack('>H', len(self.comments))
for ts, mid, text in self.comments:
tdata = text.encode('ascii')
comt_data += struct.pack('>IhH', ts, mid, len(tdata)) + tdata
if len(tdata) % 2 == 1:
comt_data += b'\x00'
chunks.append(b'COMT' + struct.pack('>I', len(comt_data)) + comt_data)
# INST
if any(self.instrument.values()):
inst_data = struct.pack('B b B B B B >h >h h h >h h h',
self.instrument['base_note'], self.instrument['detune'],
self.instrument['low_note'], self.instrument['high_note'],
self.instrument['low_velocity'], self.instrument['high_velocity'],
self.instrument['gain'],
self.instrument['sustain_loop']['play_mode'], self.instrument['sustain_loop']['begin'], self.instrument['sustain_loop']['end'],
self.instrument['release_loop']['play_mode'], self.instrument['release_loop']['begin'], self.instrument['release_loop']['end'])
chunks.append(b'INST' + struct.pack('>I', len(inst_data)) + inst_data)
# NAME
if self.name:
name_data = self._write_string(self.name)
chunks.append(b'NAME' + struct.pack('>I', len(name_data)) + name_data)
# AUTH
if self.author:
auth_data = self._write_string(self.author)
chunks.append(b'AUTH' + struct.pack('>I', len(auth_data)) + auth_data)
# (c)
if self.copyright:
copy_data = self._write_string(self.copyright)
chunks.append(b'(c) ' + struct.pack('>I', len(copy_data)) + copy_data)
# ANNO
for anno in self.annotations:
anno_data = self._write_string(anno)
chunks.append(b'ANNO' + struct.pack('>I', len(anno_data)) + anno_data)
# AESD
if self.aes_data:
chunks.append(b'AESD' + struct.pack('>I', 24) + self.aes_data)
# MIDI
if self.midi_data:
midi_pad = b'\x00' if len(self.midi_data) % 2 == 1 else b''
chunks.append(b'MIDI' + struct.pack('>I', len(self.midi_data)) + self.midi_data + midi_pad)
# APPL
for sig, data in self.app_specific:
app_data = sig + data
app_pad = b'\x00' if len(app_data) % 2 == 1 else b''
chunks.append(b'APPL' + struct.pack('>I', len(app_data)) + app_data + app_pad)
# Form
all_chunks = b''.join(chunks)
form_size = len(all_chunks) + 4 # + formType
with open(file_path, 'wb') as f:
f.write(b'FORM' + struct.pack('>I', form_size) + b'AIFC' + all_chunks)
3. Java Class
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
public class AIFCFile {
public long formatVersionTimestamp = 0;
public short numChannels = 0;
public long numSampleFrames = 0;
public short sampleSize = 0;
public double sampleRate = 0.0; // Approximate extended as double
public byte[] compressionType = new byte[4];
public String compressionName = "";
public long soundDataOffset = 0;
public long soundDataBlockSize = 0;
public List<Marker> markers = new ArrayList<>();
public List<Comment> comments = new ArrayList<>();
public Instrument instrument = new Instrument();
public String name = "";
public String author = "";
public String copyright = "";
public List<String> annotations = new ArrayList<>();
public byte[] aesData = new byte[24];
public byte[] midiData = new byte[0];
public List<AppSpecific> appSpecific = new ArrayList<>();
public byte[] soundData = new byte[0]; // Raw sound data
public static class Marker {
short id;
long position;
String name;
}
public static class Comment {
long timestamp;
short markerId;
String text;
}
public static class Instrument {
byte baseNote = 0;
byte detune = 0;
byte lowNote = 0;
byte highNote = 0;
byte lowVelocity = 0;
byte highVelocity = 0;
short gain = 0;
Loop sustainLoop = new Loop();
Loop releaseLoop = new Loop();
}
public static class Loop {
short playMode = 0;
short begin = 0;
short end = 0;
}
public static class AppSpecific {
byte[] signature = new byte[4];
byte[] data;
}
private double readExtended(DataInputStream dis) throws IOException {
short exp = dis.readShort();
long mantHi = dis.readInt() & 0xFFFFFFFFL;
long mantLo = dis.readInt() & 0xFFFFFFFFL;
long mant = (mantHi << 32) | mantLo;
return (mant / Math.pow(2, 63)) * Math.pow(2, exp - 16383);
}
private void writeExtended(DataOutputStream dos, double value) throws IOException {
if (value == 0) {
dos.write(new byte[10]);
return;
}
boolean sign = value < 0;
value = Math.abs(value);
int exp = (int) Math.floor(Math.log(value) / Math.log(2)) + 16383;
double mant = value / Math.pow(2, exp - 16383);
long hi = (long) (mant * (1L << 31));
long lo = (long) ((mant * (1L << 31) - hi) * (1L << 32));
short signExp = (short) ((sign ? 0x8000 : 0) | (exp & 0x7FFF));
dos.writeShort(signExp);
dos.writeInt((int) hi);
dos.writeInt((int) lo);
}
private String readPString(DataInputStream dis) throws IOException {
int count = dis.readUnsignedByte();
byte[] bytes = new byte[count];
dis.readFully(bytes);
if (count % 2 == 0) dis.readByte(); // Pad
return new String(bytes, "ASCII");
}
private void writePString(DataOutputStream dos, String s) throws IOException {
byte[] bytes = s.getBytes("ASCII");
dos.writeByte(bytes.length);
dos.write(bytes);
if (bytes.length % 2 == 0) dos.writeByte(0); // Pad
}
private String readString(DataInputStream dis, int size) throws IOException {
byte[] bytes = new byte[size];
dis.readFully(bytes);
if (size % 2 == 1) dis.readByte(); // Pad
return new String(bytes, "ASCII").trim();
}
private void writeString(DataOutputStream dos, String s) throws IOException {
byte[] bytes = s.getBytes("ASCII");
dos.write(bytes);
if (bytes.length % 2 == 1) dos.writeByte(0); // Pad
}
public void read(String filePath) throws IOException {
try (FileInputStream fis = new FileInputStream(filePath);
DataInputStream dis = new DataInputStream(fis)) {
byte[] ckId = new byte[4];
dis.readFully(ckId);
if (!new String(ckId).equals("FORM")) throw new IOException("Not AIFC");
int ckSize = dis.readInt();
dis.readFully(ckId);
if (!new String(ckId).equals("AIFC")) throw new IOException("Not AIFC");
long endPos = fis.getChannel().position() + ckSize - 4;
while (fis.getChannel().position() < endPos) {
dis.readFully(ckId);
if (ckId[0] == 0) break;
int chunkSize = dis.readInt();
long startPos = fis.getChannel().position();
String chunkIdStr = new String(ckId);
switch (chunkIdStr) {
case "FVER":
formatVersionTimestamp = dis.readUnsignedInt();
break;
case "COMM":
numChannels = dis.readShort();
numSampleFrames = dis.readUnsignedInt();
sampleSize = dis.readShort();
sampleRate = readExtended(dis);
dis.readFully(compressionType);
compressionName = readPString(dis);
break;
case "SSND":
soundDataOffset = dis.readUnsignedInt();
soundDataBlockSize = dis.readUnsignedInt();
dis.skip(soundDataOffset);
int dataSize = chunkSize - 8 - (int) soundDataOffset;
soundData = new byte[dataSize];
dis.readFully(soundData);
break;
case "MARK":
int numMarkers = dis.readUnsignedShort();
for (int i = 0; i < numMarkers; i++) {
Marker m = new Marker();
m.id = dis.readShort();
m.position = dis.readUnsignedInt();
m.name = readPString(dis);
markers.add(m);
}
break;
case "COMT":
int numComm = dis.readUnsignedShort();
for (int i = 0; i < numComm; i++) {
Comment c = new Comment();
c.timestamp = dis.readUnsignedInt();
c.markerId = dis.readShort();
int count = dis.readUnsignedShort();
c.text = readString(dis, count);
comments.add(c);
}
break;
case "INST":
instrument.baseNote = dis.readByte();
instrument.detune = dis.readByte();
instrument.lowNote = dis.readByte();
instrument.highNote = dis.readByte();
instrument.lowVelocity = dis.readByte();
instrument.highVelocity = dis.readByte();
instrument.gain = dis.readShort();
instrument.sustainLoop.playMode = dis.readShort();
instrument.sustainLoop.begin = dis.readShort();
instrument.sustainLoop.end = dis.readShort();
instrument.releaseLoop.playMode = dis.readShort();
instrument.releaseLoop.begin = dis.readShort();
instrument.releaseLoop.end = dis.readShort();
break;
case "NAME":
name = readString(dis, chunkSize);
break;
case "AUTH":
author = readString(dis, chunkSize);
break;
case "(c) ":
copyright = readString(dis, chunkSize);
break;
case "ANNO":
annotations.add(readString(dis, chunkSize));
break;
case "AESD":
dis.readFully(aesData);
break;
case "MIDI":
midiData = new byte[chunkSize];
dis.readFully(midiData);
break;
case "APPL":
AppSpecific app = new AppSpecific();
dis.readFully(app.signature);
app.data = new byte[chunkSize - 4];
dis.readFully(app.data);
appSpecific.add(app);
break;
default:
dis.skip(chunkSize);
}
long readBytes = fis.getChannel().position() - startPos;
if (readBytes % 2 == 1) dis.readByte();
}
}
}
public void write(String filePath) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream tempDos = new DataOutputStream(baos);
// FVER
tempDos.writeBytes("FVER");
tempDos.writeInt(4);
tempDos.writeInt((int) formatVersionTimestamp);
// COMM
ByteArrayOutputStream commBaos = new ByteArrayOutputStream();
DataOutputStream commDos = new DataOutputStream(commBaos);
commDos.writeShort(numChannels);
commDos.writeInt((int) numSampleFrames);
commDos.writeShort(sampleSize);
writeExtended(commDos, sampleRate);
commDos.write(compressionType);
writePString(commDos, compressionName);
byte[] commData = commBaos.toByteArray();
tempDos.writeBytes("COMM");
tempDos.writeInt(commData.length);
tempDos.write(commData);
// SSND
ByteArrayOutputStream ssndBaos = new ByteArrayOutputStream();
DataOutputStream ssndDos = new DataOutputStream(ssndBaos);
ssndDos.writeInt((int) soundDataOffset);
ssndDos.writeInt((int) soundDataBlockSize);
ssndDos.write(soundData);
byte[] ssndData = ssndBaos.toByteArray();
if (ssndData.length % 2 == 1) {
tempDos.writeBytes("SSND");
tempDos.writeInt(ssndData.length + 1);
tempDos.write(ssndData);
tempDos.writeByte(0);
} else {
tempDos.writeBytes("SSND");
tempDos.writeInt(ssndData.length);
tempDos.write(ssndData);
}
// MARK
if (!markers.isEmpty()) {
ByteArrayOutputStream markBaos = new ByteArrayOutputStream();
DataOutputStream markDos = new DataOutputStream(markBaos);
markDos.writeShort(markers.size());
for (Marker m : markers) {
markDos.writeShort(m.id);
markDos.writeInt((int) m.position);
writePString(markDos, m.name);
}
byte[] markData = markBaos.toByteArray();
tempDos.writeBytes("MARK");
tempDos.writeInt(markData.length);
tempDos.write(markData);
}
// COMT
if (!comments.isEmpty()) {
ByteArrayOutputStream comtBaos = new ByteArrayOutputStream();
DataOutputStream comtDos = new DataOutputStream(comtBaos);
comtDos.writeShort(comments.size());
for (Comment c : comments) {
comtDos.writeInt((int) c.timestamp);
comtDos.writeShort(c.markerId);
byte[] textBytes = c.text.getBytes("ASCII");
comtDos.writeShort(textBytes.length);
comtDos.write(textBytes);
if (textBytes.length % 2 == 1) comtDos.writeByte(0);
}
byte[] comtData = comtBaos.toByteArray();
tempDos.writeBytes("COMT");
tempDos.writeInt(comtData.length);
tempDos.write(comtData);
}
// INST
if (instrument.baseNote != 0 || instrument.gain != 0 /* etc. */) {
ByteArrayOutputStream instBaos = new ByteArrayOutputStream();
DataOutputStream instDos = new DataOutputStream(instBaos);
instDos.writeByte(instrument.baseNote);
instDos.writeByte(instrument.detune);
instDos.writeByte(instrument.lowNote);
instDos.writeByte(instrument.highNote);
instDos.writeByte(instrument.lowVelocity);
instDos.writeByte(instrument.highVelocity);
instDos.writeShort(instrument.gain);
instDos.writeShort(instrument.sustainLoop.playMode);
instDos.writeShort(instrument.sustainLoop.begin);
instDos.writeShort(instrument.sustainLoop.end);
instDos.writeShort(instrument.releaseLoop.playMode);
instDos.writeShort(instrument.releaseLoop.begin);
instDos.writeShort(instrument.releaseLoop.end);
byte[] instData = instBaos.toByteArray();
tempDos.writeBytes("INST");
tempDos.writeInt(instData.length);
tempDos.write(instData);
}
// NAME
if (!name.isEmpty()) {
byte[] nameData = name.getBytes("ASCII");
tempDos.writeBytes("NAME");
tempDos.writeInt(nameData.length);
tempDos.write(nameData);
if (nameData.length % 2 == 1) tempDos.writeByte(0);
}
// AUTH
if (!author.isEmpty()) {
byte[] authData = author.getBytes("ASCII");
tempDos.writeBytes("AUTH");
tempDos.writeInt(authData.length);
tempDos.write(authData);
if (authData.length % 2 == 1) tempDos.writeByte(0);
}
// (c)
if (!copyright.isEmpty()) {
byte[] copyData = copyright.getBytes("ASCII");
tempDos.writeBytes("(c) ");
tempDos.writeInt(copyData.length);
tempDos.write(copyData);
if (copyData.length % 2 == 1) tempDos.writeByte(0);
}
// ANNO
for (String anno : annotations) {
byte[] annoData = anno.getBytes("ASCII");
tempDos.writeBytes("ANNO");
tempDos.writeInt(annoData.length);
tempDos.write(annoData);
if (annoData.length % 2 == 1) tempDos.writeByte(0);
}
// AESD
if (aesData.length > 0) {
tempDos.writeBytes("AESD");
tempDos.writeInt(24);
tempDos.write(aesData);
}
// MIDI
if (midiData.length > 0) {
tempDos.writeBytes("MIDI");
tempDos.writeInt(midiData.length);
tempDos.write(midiData);
if (midiData.length % 2 == 1) tempDos.writeByte(0);
}
// APPL
for (AppSpecific app : appSpecific) {
ByteArrayOutputStream appBaos = new ByteArrayOutputStream();
DataOutputStream appDos = new DataOutputStream(appBaos);
appDos.write(app.signature);
appDos.write(app.data);
byte[] appData = appBaos.toByteArray();
tempDos.writeBytes("APPL");
tempDos.writeInt(appData.length);
tempDos.write(appData);
if (appData.length % 2 == 1) tempDos.writeByte(0);
}
// Write FORM
byte[] allChunks = baos.toByteArray();
try (FileOutputStream fos = new FileOutputStream(filePath);
DataOutputStream dos = new DataOutputStream(fos)) {
dos.writeBytes("FORM");
dos.writeInt(allChunks.length + 4); // + AIFC
dos.writeBytes("AIFC");
dos.write(allChunks);
}
}
}
4. JavaScript Class
Assuming Node.js for fs and Buffer.
const fs = require('fs');
class AIFCFile {
constructor() {
this.formatVersionTimestamp = 0;
this.numChannels = 0;
this.numSampleFrames = 0;
this.sampleSize = 0;
this.sampleRate = 0.0; // Approximate as number
this.compressionType = Buffer.alloc(4);
this.compressionName = '';
this.soundDataOffset = 0;
this.soundDataBlockSize = 0;
this.markers = []; // Array of {id, position, name}
this.comments = []; // Array of {timestamp, markerId, text}
this.instrument = {
baseNote: 0, detune: 0, lowNote: 0, highNote: 0,
lowVelocity: 0, highVelocity: 0, gain: 0,
sustainLoop: {playMode: 0, begin: 0, end: 0},
releaseLoop: {playMode: 0, begin: 0, end: 0}
};
this.name = '';
this.author = '';
this.copyright = '';
this.annotations = [];
this.aesData = Buffer.alloc(0);
this.midiData = Buffer.alloc(0);
this.appSpecific = []; // Array of {signature: Buffer(4), data: Buffer}
this.soundData = Buffer.alloc(0);
}
_readExtended(buffer, offset) {
const exp = buffer.readInt16BE(offset);
const hi = buffer.readUInt32BE(offset + 2);
const lo = buffer.readUInt32BE(offset + 6);
const mant = (hi * 2**32 + lo) / 2**63;
return mant * 2 ** (exp - 16383);
}
_writeExtended(value) {
const buf = Buffer.alloc(10);
if (value === 0) return buf;
const sign = value < 0 ? 0x8000 : 0;
value = Math.abs(value);
let exp = Math.floor(Math.log2(value)) + 16383;
let mant = value / 2 ** (exp - 16383);
const hi = Math.floor(mant * 2**31);
const lo = Math.floor((mant * 2**31 - hi) * 2**32);
buf.writeInt16BE(sign | (exp & 0x7FFF), 0);
buf.writeUInt32BE(hi, 2);
buf.writeUInt32BE(lo, 6);
return buf;
}
_readPstring(buffer, offset) {
let count = buffer.readUInt8(offset);
let s = buffer.toString('ascii', offset + 1, offset + 1 + count);
return {str: s, len: count + 1 + (count % 2 === 0 ? 1 : 0)};
}
_writePstring(s) {
const bytes = Buffer.from(s, 'ascii');
const count = bytes.length;
const pad = count % 2 === 0 ? Buffer.from([0]) : Buffer.alloc(0);
return Buffer.concat([Buffer.from([count]), bytes, pad]);
}
_readString(buffer, offset, size) {
let s = buffer.toString('ascii', offset, offset + size).replace(/\0+$/, '');
return {str: s, len: size + (size % 2 === 1 ? 1 : 0)};
}
_writeString(s) {
const bytes = Buffer.from(s, 'ascii');
const pad = bytes.length % 2 === 1 ? Buffer.from([0]) : Buffer.alloc(0);
return Buffer.concat([bytes, pad]);
}
read(filePath) {
const data = fs.readFileSync(filePath);
let pos = 0;
let ckId = data.toString('ascii', pos, pos + 4);
pos += 4;
if (ckId !== 'FORM') throw new Error('Not AIFC');
let ckSize = data.readUInt32BE(pos);
pos += 4;
let formType = data.toString('ascii', pos, pos + 4);
pos += 4;
if (formType !== 'AIFC') throw new Error('Not AIFC');
const endPos = pos + ckSize - 4;
while (pos < endPos) {
ckId = data.toString('ascii', pos, pos + 4);
pos += 4;
if (ckId.charCodeAt(0) === 0) break;
ckSize = data.readUInt32BE(pos);
pos += 4;
const startPos = pos;
switch (ckId) {
case 'FVER':
this.formatVersionTimestamp = data.readUInt32BE(pos);
pos += 4;
break;
case 'COMM':
this.numChannels = data.readInt16BE(pos);
pos += 2;
this.numSampleFrames = data.readUInt32BE(pos);
pos += 4;
this.sampleSize = data.readInt16BE(pos);
pos += 2;
this.sampleRate = this._readExtended(data, pos);
pos += 10;
this.compressionType = data.slice(pos, pos + 4);
pos += 4;
const pname = this._readPstring(data, pos);
this.compressionName = pname.str;
pos += pname.len;
break;
case 'SSND':
this.soundDataOffset = data.readUInt32BE(pos);
pos += 4;
this.soundDataBlockSize = data.readUInt32BE(pos);
pos += 4;
pos += this.soundDataOffset;
const dataSize = ckSize - 8 - this.soundDataOffset;
this.soundData = data.slice(pos, pos + dataSize);
pos += dataSize;
break;
case 'MARK':
const numMarkers = data.readUInt16BE(pos);
pos += 2;
for (let i = 0; i < numMarkers; i++) {
const m = {};
m.id = data.readInt16BE(pos);
pos += 2;
m.position = data.readUInt32BE(pos);
pos += 4;
const pstr = this._readPstring(data, pos);
m.name = pstr.str;
pos += pstr.len;
this.markers.push(m);
}
break;
case 'COMT':
const numComm = data.readUInt16BE(pos);
pos += 2;
for (let i = 0; i < numComm; i++) {
const c = {};
c.timestamp = data.readUInt32BE(pos);
pos += 4;
c.markerId = data.readInt16BE(pos);
pos += 2;
const count = data.readUInt16BE(pos);
pos += 2;
const strInfo = this._readString(data, pos, count);
c.text = strInfo.str;
pos += strInfo.len;
this.comments.push(c);
}
break;
case 'INST':
this.instrument.baseNote = data.readUInt8(pos);
pos += 1;
this.instrument.detune = data.readInt8(pos);
pos += 1;
this.instrument.lowNote = data.readUInt8(pos);
pos += 1;
this.instrument.highNote = data.readUInt8(pos);
pos += 1;
this.instrument.lowVelocity = data.readUInt8(pos);
pos += 1;
this.instrument.highVelocity = data.readUInt8(pos);
pos += 1;
this.instrument.gain = data.readInt16BE(pos);
pos += 2;
this.instrument.sustainLoop.playMode = data.readInt16BE(pos);
pos += 2;
this.instrument.sustainLoop.begin = data.readInt16BE(pos);
pos += 2;
this.instrument.sustainLoop.end = data.readInt16BE(pos);
pos += 2;
this.instrument.releaseLoop.playMode = data.readInt16BE(pos);
pos += 2;
this.instrument.releaseLoop.begin = data.readInt16BE(pos);
pos += 2;
this.instrument.releaseLoop.end = data.readInt16BE(pos);
pos += 2;
break;
case 'NAME':
const nameInfo = this._readString(data, pos, ckSize);
this.name = nameInfo.str;
pos += nameInfo.len;
break;
case 'AUTH':
const authInfo = this._readString(data, pos, ckSize);
this.author = authInfo.str;
pos += authInfo.len;
break;
case '(c) ':
const copyInfo = this._readString(data, pos, ckSize);
this.copyright = copyInfo.str;
pos += copyInfo.len;
break;
case 'ANNO':
const annoInfo = this._readString(data, pos, ckSize);
this.annotations.push(annoInfo.str);
pos += annoInfo.len;
break;
case 'AESD':
this.aesData = data.slice(pos, pos + 24);
pos += 24;
break;
case 'MIDI':
this.midiData = data.slice(pos, pos + ckSize);
pos += ckSize;
break;
case 'APPL':
const app = {};
app.signature = data.slice(pos, pos + 4);
pos += 4;
app.data = data.slice(pos, pos + ckSize - 4);
pos += ckSize - 4;
this.appSpecific.push(app);
break;
default:
pos += ckSize;
}
const readBytes = pos - startPos;
if (readBytes % 2 === 1) pos += 1;
}
}
write(filePath) {
let chunks = Buffer.alloc(0);
// FVER
const fver = Buffer.alloc(12);
fver.write('FVER', 0, 4, 'ascii');
fver.writeUInt32BE(4, 4);
fver.writeUInt32BE(this.formatVersionTimestamp, 8);
chunks = Buffer.concat([chunks, fver]);
// COMM
const commData = Buffer.alloc(18 + this._writePstring(this.compressionName).length);
let off = 0;
commData.writeInt16BE(this.numChannels, off); off += 2;
commData.writeUInt32BE(this.numSampleFrames, off); off += 4;
commData.writeInt16BE(this.sampleSize, off); off += 2;
commData.copy(this._writeExtended(this.sampleRate), off); off += 10;
this.compressionType.copy(commData, off); off += 4;
this._writePstring(this.compressionName).copy(commData, off);
const comm = Buffer.alloc(8 + commData.length);
comm.write('COMM', 0, 4, 'ascii');
comm.writeUInt32BE(commData.length, 4);
commData.copy(comm, 8);
chunks = Buffer.concat([chunks, comm]);
// SSND
let ssndData = Buffer.alloc(8 + this.soundData.length);
ssndData.writeUInt32BE(this.soundDataOffset, 0);
ssndData.writeUInt32BE(this.soundDataBlockSize, 4);
this.soundData.copy(ssndData, 8);
const ssndPad = ssndData.length % 2 === 1 ? Buffer.from([0]) : Buffer.alloc(0);
const ssnd = Buffer.alloc(8 + ssndData.length + ssndPad.length);
ssnd.write('SSND', 0, 4, 'ascii');
ssnd.writeUInt32BE(ssndData.length + ssndPad.length, 4);
ssndData.copy(ssnd, 8);
ssndPad.copy(ssnd, 8 + ssndData.length);
chunks = Buffer.concat([chunks, ssnd]);
// MARK
if (this.markers.length > 0) {
let markData = Buffer.alloc(0);
const numBuf = Buffer.alloc(2);
numBuf.writeUInt16BE(this.markers.length, 0);
markData = Buffer.concat([markData, numBuf]);
for (let m of this.markers) {
const mBuf = Buffer.alloc(6);
mBuf.writeInt16BE(m.id, 0);
mBuf.writeUInt32BE(m.position, 2);
markData = Buffer.concat([markData, mBuf, this._writePstring(m.name)]);
}
const mark = Buffer.alloc(8 + markData.length);
mark.write('MARK', 0, 4, 'ascii');
mark.writeUInt32BE(markData.length, 4);
markData.copy(mark, 8);
chunks = Buffer.concat([chunks, mark]);
}
// COMT
if (this.comments.length > 0) {
let comtData = Buffer.alloc(0);
const numBuf = Buffer.alloc(2);
numBuf.writeUInt16BE(this.comments.length, 0);
comtData = Buffer.concat([comtData, numBuf]);
for (let c of this.comments) {
const cBuf = Buffer.alloc(8);
cBuf.writeUInt32BE(c.timestamp, 0);
cBuf.writeInt16BE(c.markerId, 4);
const textBytes = Buffer.from(c.text, 'ascii');
cBuf.writeUInt16BE(textBytes.length, 6);
let textPad = textBytes.length % 2 === 1 ? Buffer.from([0]) : Buffer.alloc(0);
comtData = Buffer.concat([comtData, cBuf, textBytes, textPad]);
}
const comt = Buffer.alloc(8 + comtData.length);
comt.write('COMT', 0, 4, 'ascii');
comt.writeUInt32BE(comtData.length, 4);
comtData.copy(comt, 8);
chunks = Buffer.concat([chunks, comt]);
}
// INST
if (this.instrument.baseNote !== 0 || this.instrument.gain !== 0 /* etc. */) {
const instData = Buffer.alloc(20);
instData.writeUInt8(this.instrument.baseNote, 0);
instData.writeInt8(this.instrument.detune, 1);
instData.writeUInt8(this.instrument.lowNote, 2);
instData.writeUInt8(this.instrument.highNote, 3);
instData.writeUInt8(this.instrument.lowVelocity, 4);
instData.writeUInt8(this.instrument.highVelocity, 5);
instData.writeInt16BE(this.instrument.gain, 6);
instData.writeInt16BE(this.instrument.sustainLoop.playMode, 8);
instData.writeInt16BE(this.instrument.sustainLoop.begin, 10);
instData.writeInt16BE(this.instrument.sustainLoop.end, 12);
instData.writeInt16BE(this.instrument.releaseLoop.playMode, 14);
instData.writeInt16BE(this.instrument.releaseLoop.begin, 16);
instData.writeInt16BE(this.instrument.releaseLoop.end, 18);
const inst = Buffer.alloc(8 + instData.length);
inst.write('INST', 0, 4, 'ascii');
inst.writeUInt32BE(instData.length, 4);
instData.copy(inst, 8);
chunks = Buffer.concat([chunks, inst]);
}
// NAME
if (this.name) {
const nameData = this._writeString(this.name);
const nameChunk = Buffer.alloc(8 + nameData.length);
nameChunk.write('NAME', 0, 4, 'ascii');
nameChunk.writeUInt32BE(nameData.length, 4);
nameData.copy(nameChunk, 8);
chunks = Buffer.concat([chunks, nameChunk]);
}
// AUTH
if (this.author) {
const authData = this._writeString(this.author);
const authChunk = Buffer.alloc(8 + authData.length);
authChunk.write('AUTH', 0, 4, 'ascii');
authChunk.writeUInt32BE(authData.length, 4);
authData.copy(authChunk, 8);
chunks = Buffer.concat([chunks, authChunk]);
}
// (c)
if (this.copyright) {
const copyData = this._writeString(this.copyright);
const copyChunk = Buffer.alloc(8 + copyData.length);
copyChunk.write('(c) ', 0, 4, 'ascii');
copyChunk.writeUInt32BE(copyData.length, 4);
copyData.copy(copyChunk, 8);
chunks = Buffer.concat([chunks, copyChunk]);
}
// ANNO
for (let anno of this.annotations) {
const annoData = this._writeString(anno);
const annoChunk = Buffer.alloc(8 + annoData.length);
annoChunk.write('ANNO', 0, 4, 'ascii');
annoChunk.writeUInt32BE(annoData.length, 4);
annoData.copy(annoChunk, 8);
chunks = Buffer.concat([chunks, annoChunk]);
}
// AESD
if (this.aesData.length > 0) {
const aesChunk = Buffer.alloc(8 + 24);
aesChunk.write('AESD', 0, 4, 'ascii');
aesChunk.writeUInt32BE(24, 4);
this.aesData.copy(aesChunk, 8, 0, 24);
chunks = Buffer.concat([chunks, aesChunk]);
}
// MIDI
if (this.midiData.length > 0) {
const midiPad = this.midiData.length % 2 === 1 ? Buffer.from([0]) : Buffer.alloc(0);
const midiChunk = Buffer.alloc(8 + this.midiData.length + midiPad.length);
midiChunk.write('MIDI', 0, 4, 'ascii');
midiChunk.writeUInt32BE(this.midiData.length + midiPad.length, 4);
this.midiData.copy(midiChunk, 8);
midiPad.copy(midiChunk, 8 + this.midiData.length);
chunks = Buffer.concat([chunks, midiChunk]);
}
// APPL
for (let app of this.appSpecific) {
let appData = Buffer.concat([app.signature, app.data]);
const appPad = appData.length % 2 === 1 ? Buffer.from([0]) : Buffer.alloc(0);
const appChunk = Buffer.alloc(8 + appData.length + appPad.length);
appChunk.write('APPL', 0, 4, 'ascii');
appChunk.writeUInt32BE(appData.length + appPad.length, 4);
appData.copy(appChunk, 8);
appPad.copy(appChunk, 8 + appData.length);
chunks = Buffer.concat([chunks, appChunk]);
}
// FORM
const formSize = chunks.length + 4; // + AIFC
const form = Buffer.alloc(12 + chunks.length);
form.write('FORM', 0, 4, 'ascii');
form.writeUInt32BE(formSize, 4);
form.write('AIFC', 8, 4, 'ascii');
chunks.copy(form, 12);
fs.writeFileSync(filePath, form);
}
}
5. C++ Class
#include <fstream>
#include <vector>
#include <string>
#include <cstring>
#include <cmath>
#include <cstdint>
class AIFCFile {
public:
uint32_t format_version_timestamp = 0;
int16_t num_channels = 0;
uint32_t num_sample_frames = 0;
int16_t sample_size = 0;
double sample_rate = 0.0; // Approximate
char compression_type[4] = {0};
std::string compression_name;
uint32_t sound_data_offset = 0;
uint32_t sound_data_block_size = 0;
struct Marker {
int16_t id;
uint32_t position;
std::string name;
};
std::vector<Marker> markers;
struct Comment {
uint32_t timestamp;
int16_t marker_id;
std::string text;
};
std::vector<Comment> comments;
struct Loop {
int16_t play_mode = 0;
int16_t begin = 0;
int16_t end = 0;
};
struct Instrument {
uint8_t base_note = 0;
int8_t detune = 0;
uint8_t low_note = 0;
uint8_t high_note = 0;
uint8_t low_velocity = 0;
uint8_t high_velocity = 0;
int16_t gain = 0;
Loop sustain_loop;
Loop release_loop;
} instrument;
std::string name;
std::string author;
std::string copyright_str;
std::vector<std::string> annotations;
uint8_t aes_data[24] = {0};
std::vector<uint8_t> midi_data;
struct AppSpecific {
char signature[4];
std::vector<uint8_t> data;
};
std::vector<AppSpecific> app_specific;
std::vector<uint8_t> sound_data;
AIFCFile() {}
double read_extended(std::ifstream& f) {
int16_t exp;
uint32_t hi, lo;
f.read(reinterpret_cast<char*>(&exp), 2);
f.read(reinterpret_cast<char*>(&hi), 4);
f.read(reinterpret_cast<char*>(&lo), 4);
if (f.fail()) return 0.0;
exp = __builtin_bswap16(exp);
hi = __builtin_bswap32(hi);
lo = __builtin_bswap32(lo);
double mant = (static_cast<double>(hi) * (1LL << 32) + lo) / pow(2, 63);
return mant * pow(2, exp - 16383);
}
void write_extended(std::ofstream& f, double value) {
if (value == 0.0) {
char zeros[10] = {0};
f.write(zeros, 10);
return;
}
bool sign = value < 0;
value = std::abs(value);
int exp = static_cast<int>(std::floor(std::log2(value))) + 16383;
double mant = value / std::pow(2, exp - 16383);
uint32_t hi = static_cast<uint32_t>(mant * (1LL << 31));
uint32_t lo = static_cast<uint32_t>((mant * (1LL << 31) - hi) * (1LL << 32));
int16_t sign_exp = (sign ? 0x8000 : 0) | (exp & 0x7FFF);
sign_exp = __builtin_bswap16(sign_exp);
hi = __builtin_bswap32(hi);
lo = __builtin_bswap32(lo);
f.write(reinterpret_cast<char*>(&sign_exp), 2);
f.write(reinterpret_cast<char*>(&hi), 4);
f.write(reinterpret_cast<char*>(&lo), 4);
}
std::string read_pstring(std::ifstream& f) {
uint8_t count;
f.read(reinterpret_cast<char*>(&count), 1);
std::string s(count, ' ');
f.read(&s[0], count);
if (count % 2 == 0) {
char pad;
f.read(&pad, 1);
}
return s;
}
void write_pstring(std::ofstream& f, const std::string& s) {
uint8_t count = static_cast<uint8_t>(s.size());
f.write(reinterpret_cast<const char*>(&count), 1);
f.write(s.c_str(), count);
if (count % 2 == 0) {
char pad = 0;
f.write(&pad, 1);
}
}
std::string read_string(std::ifstream& f, uint32_t size) {
std::string s(size, ' ');
f.read(&s[0], size);
if (size % 2 == 1) {
char pad;
f.read(&pad, 1);
}
size_t null_pos = s.find('\0');
if (null_pos != std::string::npos) s.resize(null_pos);
return s;
}
void write_string(std::ofstream& f, const std::string& s) {
f.write(s.c_str(), s.size());
if (s.size() % 2 == 1) {
char pad = 0;
f.write(&pad, 1);
}
}
void read(const std::string& file_path) {
std::ifstream f(file_path, std::ios::binary);
if (!f) throw std::runtime_error("Cannot open file");
char ck_id[5] = {0};
f.read(ck_id, 4);
if (std::strcmp(ck_id, "FORM") != 0) throw std::runtime_error("Not AIFC");
uint32_t ck_size;
f.read(reinterpret_cast<char*>(&ck_size), 4);
ck_size = __builtin_bswap32(ck_size);
f.read(ck_id, 4);
if (std::strcmp(ck_id, "AIFC") != 0) throw std::runtime_error("Not AIFC");
auto start = f.tellg();
auto end_pos = start + static_cast<std::streamoff>(ck_size - 4);
while (f.tellg() < end_pos) {
f.read(ck_id, 4);
if (ck_id[0] == 0) break;
f.read(reinterpret_cast<char*>(&ck_size), 4);
ck_size = __builtin_bswap32(ck_size);
auto chunk_start = f.tellg();
if (std::strcmp(ck_id, "FVER") == 0) {
f.read(reinterpret_cast<char*>(&format_version_timestamp), 4);
format_version_timestamp = __builtin_bswap32(format_version_timestamp);
} else if (std::strcmp(ck_id, "COMM") == 0) {
f.read(reinterpret_cast<char*>(&num_channels), 2);
num_channels = __builtin_bswap16(num_channels);
f.read(reinterpret_cast<char*>(&num_sample_frames), 4);
num_sample_frames = __builtin_bswap32(num_sample_frames);
f.read(reinterpret_cast<char*>(&sample_size), 2);
sample_size = __builtin_bswap16(sample_size);
sample_rate = read_extended(f);
f.read(compression_type, 4);
compression_name = read_pstring(f);
} else if (std::strcmp(ck_id, "SSND") == 0) {
f.read(reinterpret_cast<char*>(&sound_data_offset), 4);
sound_data_offset = __builtin_bswap32(sound_data_offset);
f.read(reinterpret_cast<char*>(&sound_data_block_size), 4);
sound_data_block_size = __builtin_bswap32(sound_data_block_size);
f.seekg(sound_data_offset, std::ios::cur);
uint32_t data_size = ck_size - 8 - sound_data_offset;
sound_data.resize(data_size);
f.read(reinterpret_cast<char*>(sound_data.data()), data_size);
} else if (std::strcmp(ck_id, "MARK") == 0) {
uint16_t num_markers;
f.read(reinterpret_cast<char*>(&num_markers), 2);
num_markers = __builtin_bswap16(num_markers);
for (uint16_t i = 0; i < num_markers; ++i) {
Marker m;
f.read(reinterpret_cast<char*>(&m.id), 2);
m.id = __builtin_bswap16(m.id);
f.read(reinterpret_cast<char*>(&m.position), 4);
m.position = __builtin_bswap32(m.position);
m.name = read_pstring(f);
markers.push_back(m);
}
} else if (std::strcmp(ck_id, "COMT") == 0) {
uint16_t num_comm;
f.read(reinterpret_cast<char*>(&num_comm), 2);
num_comm = __builtin_bswap16(num_comm);
for (uint16_t i = 0; i < num_comm; ++i) {
Comment c;
f.read(reinterpret_cast<char*>(&c.timestamp), 4);
c.timestamp = __builtin_bswap32(c.timestamp);
f.read(reinterpret_cast<char*>(&c.marker_id), 2);
c.marker_id = __builtin_bswap16(c.marker_id);
uint16_t count;
f.read(reinterpret_cast<char*>(&count), 2);
count = __builtin_bswap16(count);
c.text = read_string(f, count);
comments.push_back(c);
}
} else if (std::strcmp(ck_id, "INST") == 0) {
f.read(reinterpret_cast<char*>(&instrument.base_note), 1);
f.read(reinterpret_cast<char*>(&instrument.detune), 1);
f.read(reinterpret_cast<char*>(&instrument.low_note), 1);
f.read(reinterpret_cast<char*>(&instrument.high_note), 1);
f.read(reinterpret_cast<char*>(&instrument.low_velocity), 1);
f.read(reinterpret_cast<char*>(&instrument.high_velocity), 1);
f.read(reinterpret_cast<char*>(&instrument.gain), 2);
instrument.gain = __builtin_bswap16(instrument.gain);
f.read(reinterpret_cast<char*>(&instrument.sustain_loop.play_mode), 2);
instrument.sustain_loop.play_mode = __builtin_bswap16(instrument.sustain_loop.play_mode);
f.read(reinterpret_cast<char*>(&instrument.sustain_loop.begin), 2);
instrument.sustain_loop.begin = __builtin_bswap16(instrument.sustain_loop.begin);
f.read(reinterpret_cast<char*>(&instrument.sustain_loop.end), 2);
instrument.sustain_loop.end = __builtin_bswap16(instrument.sustain_loop.end);
f.read(reinterpret_cast<char*>(&instrument.release_loop.play_mode), 2);
instrument.release_loop.play_mode = __builtin_bswap16(instrument.release_loop.play_mode);
f.read(reinterpret_cast<char*>(&instrument.release_loop.begin), 2);
instrument.release_loop.begin = __builtin_bswap16(instrument.release_loop.begin);
f.read(reinterpret_cast<char*>(&instrument.release_loop.end), 2);
instrument.release_loop.end = __builtin_bswap16(instrument.release_loop.end);
} else if (std::strcmp(ck_id, "NAME") == 0) {
name = read_string(f, ck_size);
} else if (std::strcmp(ck_id, "AUTH") == 0) {
author = read_string(f, ck_size);
} else if (std::strcmp(ck_id, "(c) ") == 0) {
copyright_str = read_string(f, ck_size);
} else if (std::strcmp(ck_id, "ANNO") == 0) {
annotations.push_back(read_string(f, ck_size));
} else if (std::strcmp(ck_id, "AESD") == 0) {
f.read(reinterpret_cast<char*>(aes_data), 24);
} else if (std::strcmp(ck_id, "MIDI") == 0) {
midi_data.resize(ck_size);
f.read(reinterpret_cast<char*>(midi_data.data()), ck_size);
} else if (std::strcmp(ck_id, "APPL") == 0) {
AppSpecific app;
f.read(app.signature, 4);
app.data.resize(ck_size - 4);
f.read(reinterpret_cast<char*>(app.data.data()), ck_size - 4);
app_specific.push_back(app);
} else {
f.seekg(ck_size, std::ios::cur);
}
auto read_bytes = f.tellg() - chunk_start;
if (static_cast<uint32_t>(read_bytes) % 2 == 1) {
char pad;
f.read(&pad, 1);
}
}
}
void write(const std::string& file_path) {
std::ofstream f(file_path, std::ios::binary);
if (!f) throw std::runtime_error("Cannot open file");
std::vector<uint8_t> chunks;
// Helper to append chunk
auto append_chunk = [&](const char* id, const std::vector<uint8_t>& data) {
uint32_t size_be = __builtin_bswap32(static_cast<uint32_t>(data.size()));
chunks.insert(chunks.end(), id, id + 4);
chunks.insert(chunks.end(), reinterpret_cast<uint8_t*>(&size_be), reinterpret_cast<uint8_t*>(&size_be) + 4);
chunks.insert(chunks.end(), data.begin(), data.end());
};
// FVER
std::vector<uint8_t> fver_data(4);
uint32_t ts_be = __builtin_bswap32(format_version_timestamp);
std::memcpy(fver_data.data(), &ts_be, 4);
append_chunk("FVER", fver_data);
// COMM
std::vector<uint8_t> comm_data;
int16_t nc_be = __builtin_bswap16(num_channels);
comm_data.insert(comm_data.end(), reinterpret_cast<uint8_t*>(&nc_be), reinterpret_cast<uint8_t*>(&nc_be) + 2);
uint32_t nsf_be = __builtin_bswap32(num_sample_frames);
comm_data.insert(comm_data.end(), reinterpret_cast<uint8_t*>(&nsf_be), reinterpret_cast<uint8_t*>(&nsf_be) + 4);
int16_t ss_be = __builtin_bswap16(sample_size);
comm_data.insert(comm_data.end(), reinterpret_cast<uint8_t*>(&ss_be), reinterpret_cast<uint8_t*>(&ss_be) + 2);
// Extended
std::vector<uint8_t> ext(10);
// Simulate write_extended by buffering
std::stringstream ss;
write_extended(ss, sample_rate);
ss.read(reinterpret_cast<char*>(ext.data()), 10);
comm_data.insert(comm_data.end(), ext.begin(), ext.end());
comm_data.insert(comm_data.end(), compression_type, compression_type + 4);
// Pstring
uint8_t count = static_cast<uint8_t>(compression_name.size());
comm_data.push_back(count);
comm_data.insert(comm_data.end(), compression_name.begin(), compression_name.end());
if (count % 2 == 0) comm_data.push_back(0);
append_chunk("COMM", comm_data);
// SSND
std::vector<uint8_t> ssnd_data;
uint32_t off_be = __builtin_bswap32(sound_data_offset);
ssnd_data.insert(ssnd_data.end(), reinterpret_cast<uint8_t*>(&off_be), reinterpret_cast<uint8_t*>(&off_be) + 4);
uint32_t bs_be = __builtin_bswap32(sound_data_block_size);
ssnd_data.insert(ssnd_data.end(), reinterpret_cast<uint8_t*>(&bs_be), reinterpret_cast<uint8_t*>(&bs_be) + 4);
ssnd_data.insert(ssnd_data.end(), sound_data.begin(), sound_data.end());
if (ssnd_data.size() % 2 == 1) ssnd_data.push_back(0);
append_chunk("SSND", ssnd_data);
// MARK
if (!markers.empty()) {
std::vector<uint8_t> mark_data;
uint16_t num_be = __builtin_bswap16(static_cast<uint16_t>(markers.size()));
mark_data.insert(mark_data.end(), reinterpret_cast<uint8_t*>(&num_be), reinterpret_cast<uint8_t*>(&num_be) + 2);
for (const auto& m : markers) {
int16_t id_be = __builtin_bswap16(m.id);
mark_data.insert(mark_data.end(), reinterpret_cast<uint8_t*>(&id_be), reinterpret_cast<uint8_t*>(&id_be) + 2);
uint32_t pos_be = __builtin_bswap32(m.position);
mark_data.insert(mark_data.end(), reinterpret_cast<uint8_t*>(&pos_be), reinterpret_cast<uint8_t*>(&pos_be) + 4);
uint8_t cnt = static_cast<uint8_t>(m.name.size());
mark_data.push_back(cnt);
mark_data.insert(mark_data.end(), m.name.begin(), m.name.end());
if (cnt % 2 == 0) mark_data.push_back(0);
}
append_chunk("MARK", mark_data);
}
// COMT
if (!comments.empty()) {
std::vector<uint8_t> comt_data;
uint16_t num_be = __builtin_bswap16(static_cast<uint16_t>(comments.size()));
comt_data.insert(comt_data.end(), reinterpret_cast<uint8_t*>(&num_be), reinterpret_cast<uint8_t*>(&num_be) + 2);
for (const auto& c : comments) {
uint32_t ts_be = __builtin_bswap32(c.timestamp);
comt_data.insert(comt_data.end(), reinterpret_cast<uint8_t*>(&ts_be), reinterpret_cast<uint8_t*>(&ts_be) + 4);
int16_t mid_be = __builtin_bswap16(c.marker_id);
comt_data.insert(comt_data.end(), reinterpret_cast<uint8_t*>(&mid_be), reinterpret_cast<uint8_t*>(&mid_be) + 2);
uint16_t cnt_be = __builtin_bswap16(static_cast<uint16_t>(c.text.size()));
comt_data.insert(comt_data.end(), reinterpret_cast<uint8_t*>(&cnt_be), reinterpret_cast<uint8_t*>(&cnt_be) + 2);
comt_data.insert(comt_data.end(), c.text.begin(), c.text.end());
if (c.text.size() % 2 == 1) comt_data.push_back(0);
}
append_chunk("COMT", comt_data);
}
// INST
if (instrument.base_note != 0 || instrument.gain != 0 /* etc. */) {
std::vector<uint8_t> inst_data(20);
inst_data[0] = instrument.base_note;
inst_data[1] = static_cast<uint8_t>(instrument.detune);
inst_data[2] = instrument.low_note;
inst_data[3] = instrument.high_note;
inst_data[4] = instrument.low_velocity;
inst_data[5] = instrument.high_velocity;
int16_t gain_be = __builtin_bswap16(instrument.gain);
std::memcpy(&inst_data[6], &gain_be, 2);
int16_t pm_be = __builtin_bswap16(instrument.sustain_loop.play_mode);
std::memcpy(&inst_data[8], &pm_be, 2);
int16_t b_be = __builtin_bswap16(instrument.sustain_loop.begin);
std::memcpy(&inst_data[10], &b_be, 2);
int16_t e_be = __builtin_bswap16(instrument.sustain_loop.end);
std::memcpy(&inst_data[12], &e_be, 2);
pm_be = __builtin_bswap16(instrument.release_loop.play_mode);
std::memcpy(&inst_data[14], &pm_be, 2);
b_be = __builtin_bswap16(instrument.release_loop.begin);
std::memcpy(&inst_data[16], &b_be, 2);
e_be = __builtin_bswap16(instrument.release_loop.end);
std::memcpy(&inst_data[18], &e_be, 2);
append_chunk("INST", inst_data);
}
// NAME
if (!name.empty()) {
std::vector<uint8_t> name_data(name.begin(), name.end());
if (name_data.size() % 2 == 1) name_data.push_back(0);
append_chunk("NAME", name_data);
}
// AUTH
if (!author.empty()) {
std::vector<uint8_t> auth_data(author.begin(), author.end());
if (auth_data.size() % 2 == 1) auth_data.push_back(0);
append_chunk("AUTH", auth_data);
}
// (c)
if (!copyright_str.empty()) {
std::vector<uint8_t> copy_data(copyright_str.begin(), copyright_str.end());
if (copy_data.size() % 2 == 1) copy_data.push_back(0);
append_chunk("(c) ", copy_data);
}
// ANNO
for (const auto& anno : annotations) {
std::vector<uint8_t> anno_data(anno.begin(), anno.end());
if (anno_data.size() % 2 == 1) anno_data.push_back(0);
append_chunk("ANNO", anno_data);
}
// AESD
if (std::any_of(std::begin(aes_data), std::end(aes_data), [](uint8_t b){return b != 0;})) {
std::vector<uint8_t> aes_vec(std::begin(aes_data), std::end(aes_data));
append_chunk("AESD", aes_vec);
}
// MIDI
if (!midi_data.empty()) {
std::vector<uint8_t> midi_copy = midi_data;
if (midi_copy.size() % 2 == 1) midi_copy.push_back(0);
append_chunk("MIDI", midi_copy);
}
// APPL
for (const auto& app : app_specific) {
std::vector<uint8_t> app_data;
app_data.insert(app_data.end(), app.signature, app.signature + 4);
app_data.insert(app_data.end(), app.data.begin(), app.data.end());
if (app_data.size() % 2 == 1) app_data.push_back(0);
append_chunk("APPL", app_data);
}
// Write FORM
f.write("FORM", 4);
uint32_t form_size = static_cast<uint32_t>(chunks.size()) + 4; // + AIFC
uint32_t form_size_be = __builtin_bswap32(form_size);
f.write(reinterpret_cast<char*>(&form_size_be), 4);
f.write("AIFC", 4);
f.write(reinterpret_cast<char*>(chunks.data()), chunks.size());
}
};