Task 482: .OPUS File Format
Task 482: .OPUS File Format
File Format Specifications for .OPUS
The .OPUS file format is an audio file that encapsulates the Opus codec within an Ogg container. It is defined in RFC 7845 and related specifications from Xiph.org. The format consists of a logical Ogg stream with at least two mandatory header packets: the identification header ("OpusHead") and the comment header ("OpusTags"). These are followed by audio data packets. The Ogg container provides framing via pages, each starting with the "OggS" magic signature. The headers must appear at the beginning with granule positions of 0. The format supports variable bitrates (6-510 kbps), sample rates (typically resampled to 48 kHz for playback), and 1-255 channels. Audio packets include a TOC (table of contents) byte for configuration (frame duration, channels, etc.). The MIME type is audio/ogg (or audio/ogg; codecs=opus), and the extension is .opus.
- List of all the properties of this file format intrinsic to its file system:
- Version number (1 byte, unsigned): The version of the Opus header (must be 1 for current spec).
- Channel count (1 byte, unsigned): Number of output channels (1-255).
- Pre-skip (2 bytes, unsigned little-endian): Number of samples (at 48 kHz) to discard from the decoder output at the start.
- Input sample rate (4 bytes, unsigned little-endian): Original sample rate of the input audio in Hz (informational; decoder plays at 48 kHz).
- Output gain (2 bytes, signed little-endian Q7.8): Gain in dB to apply to the decoder output.
- Channel mapping family (1 byte, unsigned): Defines channel ordering (0 for RTP mono/stereo, 1 for Vorbis multichannel, 255 for undefined, others reserved).
- Stream count (1 byte, unsigned; present if channel mapping family > 0): Total number of Opus streams in packets.
- Coupled stream count (1 byte, unsigned; present if channel mapping family > 0): Number of stereo streams (coupled pairs).
- Channel mapping (channel count bytes, unsigned; present if channel mapping family > 0): Array mapping output channels to stream indices (0-254) or silence (255).
- Vendor string (variable length UTF-8): String identifying the encoder/vendor.
- User comments (variable list): List of metadata tags in "TAG=value" format (e.g., TITLE, ARTIST, R128_TRACK_GAIN).
- Two direct download links for files of format .OPUS:
- https://filesamples.com/samples/audio/opus/Symphony No.6 (1st movement).opus
- https://filesamples.com/samples/audio/opus/sample4.opus
- Ghost blog embedded HTML JavaScript for drag-and-drop .OPUS file property dump:
Drag and Drop .OPUS File
- Python class for .OPUS file handling:
import struct
class OpusFile:
def __init__(self, filename):
self.filename = filename
self.head = {}
self.tags = {}
self.parse()
def parse(self):
with open(self.filename, 'rb') as f:
filedata = f.read()
offset = 0
current_packet = b''
packets = []
while offset < len(filedata) and len(packets) < 2:
if filedata[offset:offset+4] != b'OggS':
raise ValueError("Invalid Ogg file")
offset += 4
version = filedata[offset]
if version != 0:
raise ValueError("Invalid Ogg version")
offset += 1
header_type = filedata[offset]
offset += 1
granule = struct.unpack_from('<q', filedata, offset)[0]
offset += 8
serial = struct.unpack_from('<i', filedata, offset)[0]
offset += 4
page_seq = struct.unpack_from('<i', filedata, offset)[0]
offset += 4
crc = struct.unpack_from('<i', filedata, offset)[0]
offset += 4
num_segments = filedata[offset]
offset += 1
seg_table = [filedata[offset + i] for i in range(num_segments)]
offset += num_segments
page_data_len = sum(seg_table)
page_data = filedata[offset:offset + page_data_len]
offset += page_data_len
seg_idx = 0
data_pos = 0
while seg_idx < num_segments:
if current_packet:
seg_size = seg_table[seg_idx]
current_packet += page_data[data_pos:data_pos + seg_size]
data_pos += seg_size
seg_idx += 1
if seg_size < 255:
packets.append(current_packet)
current_packet = b''
else:
packet_data = b''
while seg_idx < num_segments and seg_table[seg_idx] == 255:
seg_size = seg_table[seg_idx]
packet_data += page_data[data_pos:data_pos + seg_size]
data_pos += seg_size
seg_idx += 1
if seg_idx < num_segments:
seg_size = seg_table[seg_idx]
packet_data += page_data[data_pos:data_pos + seg_size]
data_pos += seg_size
seg_idx += 1
packets.append(packet_data)
else:
current_packet = packet_data
if len(packets) < 2:
raise ValueError("Incomplete headers")
# Parse OpusHead
head = packets[0]
if head[0:8] != b'OpusHead':
raise ValueError("Invalid OpusHead")
self.head['version'] = head[8]
self.head['channels'] = head[9]
self.head['pre_skip'] = struct.unpack_from('<H', head, 10)[0]
self.head['input_sample_rate'] = struct.unpack_from('<I', head, 12)[0]
self.head['output_gain'] = struct.unpack_from('<h', head, 16)[0]
self.head['channel_mapping_family'] = head[18]
extra_offset = 19
if self.head['channel_mapping_family'] > 0:
self.head['stream_count'] = head[extra_offset]
extra_offset += 1
self.head['coupled_count'] = head[extra_offset]
extra_offset += 1
self.head['channel_mapping'] = list(head[extra_offset:extra_offset + self.head['channels']])
# Parse OpusTags
tags = packets[1]
if tags[0:8] != b'OpusTags':
raise ValueError("Invalid OpusTags")
p_offset = 8
vendor_len = struct.unpack_from('<I', tags, p_offset)[0]
p_offset += 4
self.tags['vendor'] = tags[p_offset:p_offset + vendor_len].decode('utf-8')
p_offset += vendor_len
comment_count = struct.unpack_from('<I', tags, p_offset)[0]
p_offset += 4
self.tags['comments'] = []
for _ in range(comment_count):
len_ = struct.unpack_from('<I', tags, p_offset)[0]
p_offset += 4
comm = tags[p_offset:p_offset + len_].decode('utf-8')
self.tags['comments'].append(comm)
p_offset += len_
def print_properties(self):
print("OpusHead Properties:")
for key, value in self.head.items():
print(f"{key}: {value}")
print("\nOpusTags Properties:")
print(f"Vendor: {self.tags['vendor']}")
print("Comments:")
for comm in self.tags['comments']:
print(comm)
# For write: Simple example to write modified headers back (audio data copied as-is; assumes no spanning changes)
def write(self, new_filename, modified_head=None, modified_tags=None):
if modified_head:
self.head.update(modified_head)
if modified_tags:
self.tags.update(modified_tags)
# Rebuild packets (simplified; real write would require full Ogg re-paging)
# OpusHead rebuild
head_packet = b'OpusHead'
head_packet += struct.pack('<B', self.head['version'])
head_packet += struct.pack('<B', self.head['channels'])
head_packet += struct.pack('<H', self.head['pre_skip'])
head_packet += struct.pack('<I', self.head['input_sample_rate'])
head_packet += struct.pack('<h', self.head['output_gain'])
head_packet += struct.pack('<B', self.head['channel_mapping_family'])
if self.head['channel_mapping_family'] > 0:
head_packet += struct.pack('<B', self.head['stream_count'])
head_packet += struct.pack('<B', self.head['coupled_count'])
head_packet += bytes(self.head['channel_mapping'])
# OpusTags rebuild
tags_packet = b'OpusTags'
vendor_bytes = self.tags['vendor'].encode('utf-8')
tags_packet += struct.pack('<I', len(vendor_bytes))
tags_packet += vendor_bytes
tags_packet += struct.pack('<I', len(self.tags['comments']))
for comm in self.tags['comments']:
comm_bytes = comm.encode('utf-8')
tags_packet += struct.pack('<I', len(comm_bytes))
tags_packet += comm_bytes
# For full write, would need to re-encapsulate in Ogg pages and append original audio data
# Here, placeholder: write headers only (not full file)
with open(new_filename, 'wb') as f:
f.write(head_packet) # Simplified; use opusenc or libogg for real write
f.write(tags_packet)
print("Wrote modified headers (audio data not included in this simple implementation).")
# Example usage:
# opus = OpusFile('example.opus')
# opus.print_properties()
# opus.write('modified.opus', modified_head={'version': 1})
- Java class for .OPUS file handling:
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class OpusFile {
private String filename;
private Map<String, Object> head = new HashMap<>();
private Map<String, Object> tags = new HashMap<>();
public OpusFile(String filename) {
this.filename = filename;
parse();
}
private void parse() {
byte[] filedata;
try {
filedata = Files.readAllBytes(Paths.get(filename));
} catch (IOException e) {
throw new RuntimeException("Error reading file", e);
}
int offset = 0;
byte[] currentPacket = new byte[0];
List<byte[]> packets = new ArrayList<>();
while (offset < filedata.length && packets.size() < 2) {
if (!new String(filedata, offset, 4).equals("OggS")) {
throw new RuntimeException("Invalid Ogg file");
}
offset += 4;
int version = Byte.toUnsignedInt(filedata[offset++]);
if (version != 0) {
throw new RuntimeException("Invalid Ogg version");
}
int headerType = Byte.toUnsignedInt(filedata[offset++]);
offset += 8; // granule
offset += 4; // serial
offset += 4; // page seq
offset += 4; // crc
int numSegments = Byte.toUnsignedInt(filedata[offset++]);
byte[] segTable = new byte[numSegments];
System.arraycopy(filedata, offset, segTable, 0, numSegments);
offset += numSegments;
int pageDataLen = 0;
for (byte b : segTable) pageDataLen += Byte.toUnsignedInt(b);
byte[] pageData = new byte[pageDataLen];
System.arraycopy(filedata, offset, pageData, 0, pageDataLen);
offset += pageDataLen;
int segIdx = 0;
int dataPos = 0;
while (segIdx < numSegments) {
if (currentPacket.length > 0) {
int segSize = Byte.toUnsignedInt(segTable[segIdx]);
byte[] newData = new byte[currentPacket.length + segSize];
System.arraycopy(currentPacket, 0, newData, 0, currentPacket.length);
System.arraycopy(pageData, dataPos, newData, currentPacket.length, segSize);
currentPacket = newData;
dataPos += segSize;
segIdx++;
if (segSize < 255) {
packets.add(currentPacket);
currentPacket = new byte[0];
}
} else {
byte[] packetData = new byte[0];
while (segIdx < numSegments && segTable[segIdx] == (byte)255) {
int segSize = Byte.toUnsignedInt(segTable[segIdx]);
byte[] newData = new byte[packetData.length + segSize];
System.arraycopy(packetData, 0, newData, 0, packetData.length);
System.arraycopy(pageData, dataPos, newData, packetData.length, segSize);
packetData = newData;
dataPos += segSize;
segIdx++;
}
if (segIdx < numSegments) {
int segSize = Byte.toUnsignedInt(segTable[segIdx]);
byte[] newData = new byte[packetData.length + segSize];
System.arraycopy(packetData, 0, newData, 0, packetData.length);
System.arraycopy(pageData, dataPos, newData, packetData.length, segSize);
packetData = newData;
dataPos += segSize;
segIdx++;
packets.add(packetData);
} else {
currentPacket = packetData;
}
}
}
}
if (packets.size() < 2) {
throw new RuntimeException("Incomplete headers");
}
// Parse OpusHead
byte[] headBytes = packets.get(0);
if (!new String(headBytes, 0, 8).equals("OpusHead")) {
throw new RuntimeException("Invalid OpusHead");
}
ByteBuffer bb = ByteBuffer.wrap(headBytes).order(ByteOrder.LITTLE_ENDIAN);
head.put("version", Byte.toUnsignedInt(headBytes[8]));
head.put("channels", Byte.toUnsignedInt(headBytes[9]));
head.put("pre_skip", bb.getShort(10) & 0xFFFF);
head.put("input_sample_rate", bb.getInt(12) & 0xFFFFFFFFL);
head.put("output_gain", bb.getShort(16));
head.put("channel_mapping_family", Byte.toUnsignedInt(headBytes[18]));
int extraOffset = 19;
if ((int) head.get("channel_mapping_family") > 0) {
head.put("stream_count", Byte.toUnsignedInt(headBytes[extraOffset++]));
head.put("coupled_count", Byte.toUnsignedInt(headBytes[extraOffset++]));
int channels = (int) head.get("channels");
List<Integer> mapping = new ArrayList<>();
for (int i = 0; i < channels; i++) {
mapping.add(Byte.toUnsignedInt(headBytes[extraOffset + i]));
}
head.put("channel_mapping", mapping);
}
// Parse OpusTags
byte[] tagsBytes = packets.get(1);
if (!new String(tagsBytes, 0, 8).equals("OpusTags")) {
throw new RuntimeException("Invalid OpusTags");
}
ByteBuffer tbb = ByteBuffer.wrap(tagsBytes).order(ByteOrder.LITTLE_ENDIAN);
int pOffset = 8;
int vendorLen = tbb.getInt(pOffset);
pOffset += 4;
String vendor = new String(tagsBytes, pOffset, vendorLen, "UTF-8");
tags.put("vendor", vendor);
pOffset += vendorLen;
int commentCount = tbb.getInt(pOffset);
pOffset += 4;
List<String> comments = new ArrayList<>();
for (int i = 0; i < commentCount; i++) {
int len = tbb.getInt(pOffset);
pOffset += 4;
String comm = new String(tagsBytes, pOffset, len, "UTF-8");
comments.add(comm);
pOffset += len;
}
tags.put("comments", comments);
}
public void printProperties() {
System.out.println("OpusHead Properties:");
head.forEach((key, value) -> System.out.println(key + ": " + value));
System.out.println("\nOpusTags Properties:");
System.out.println("Vendor: " + tags.get("vendor"));
System.out.println("Comments:");
((List<String>) tags.get("comments")).forEach(System.out::println);
}
// For write: Simple example to write modified headers (audio data not included)
public void write(String newFilename, Map<String, Object> modifiedHead, Map<String, Object> modifiedTags) {
if (modifiedHead != null) head.putAll(modifiedHead);
if (modifiedTags != null) tags.putAll(modifiedTags);
// Rebuild packets (simplified)
ByteBuffer headBb = ByteBuffer.allocate(1024).order(ByteOrder.LITTLE_ENDIAN); // Arbitrary size
headBb.put("OpusHead".getBytes());
headBb.put((byte) (int) head.get("version"));
headBb.put((byte) (int) head.get("channels"));
headBb.putShort((short) (int) head.get("pre_skip"));
headBb.putInt((int) (long) head.get("input_sample_rate"));
headBb.putShort((short) (int) head.get("output_gain"));
headBb.put((byte) (int) head.get("channel_mapping_family"));
if ((int) head.get("channel_mapping_family") > 0) {
headBb.put((byte) (int) head.get("stream_count"));
headBb.put((byte) (int) head.get("coupled_count"));
for (int m : (List<Integer>) head.get("channel_mapping")) {
headBb.put((byte) m);
}
}
byte[] headPacket = new byte[headBb.position()];
headBb.flip();
headBb.get(headPacket);
// Tags
ByteBuffer tagsBb = ByteBuffer.allocate(4096).order(ByteOrder.LITTLE_ENDIAN);
tagsBb.put("OpusTags".getBytes());
byte[] vendorBytes = ((String) tags.get("vendor")).getBytes("UTF-8");
tagsBb.putInt(vendorBytes.length);
tagsBb.put(vendorBytes);
List<String> comments = (List<String>) tags.get("comments");
tagsBb.putInt(comments.size());
for (String comm : comments) {
byte[] commBytes = comm.getBytes("UTF-8");
tagsBb.putInt(commBytes.length);
tagsBb.put(commBytes);
}
byte[] tagsPacket = new byte[tagsBb.position()];
tagsBb.flip();
tagsBb.get(tagsPacket);
// Write (headers only)
try (FileOutputStream fos = new FileOutputStream(newFilename)) {
fos.write(headPacket);
fos.write(tagsPacket);
} catch (IOException e) {
throw new RuntimeException("Error writing file", e);
}
System.out.println("Wrote modified headers (audio data not included in this simple implementation).");
}
// Example usage:
// public static void main(String[] args) {
// OpusFile opus = new OpusFile("example.opus");
// opus.printProperties();
// opus.write("modified.opus", null, null);
// }
}
- JavaScript class for .OPUS file handling (Node.js):
const fs = require('fs');
class OpusFile {
constructor(filename) {
this.filename = filename;
this.head = {};
this.tags = {};
this.parse();
}
parse() {
const filedata = fs.readFileSync(this.filename);
const dv = new DataView(filedata.buffer);
let offset = 0;
let currentPacket = new Uint8Array(0);
let packets = [];
while (offset < filedata.length && packets.length < 2) {
if (String.fromCharCode(...filedata.subarray(offset, offset + 4)) !== 'OggS') {
throw new Error('Invalid Ogg file');
}
offset += 4;
const version = filedata[offset++];
if (version !== 0) {
throw new Error('Invalid Ogg version');
}
const headerType = filedata[offset++];
offset += 8; // granule
offset += 4; // serial
offset += 4; // page seq
offset += 4; // crc
const numSegments = filedata[offset++];
const segTable = filedata.subarray(offset, offset + numSegments);
offset += numSegments;
const pageDataLen = [...segTable].reduce((a, b) => a + b, 0);
const pageData = filedata.subarray(offset, offset + pageDataLen);
offset += pageDataLen;
let segIdx = 0;
let dataPos = 0;
while (segIdx < numSegments) {
if (currentPacket.length > 0) {
const segSize = segTable[segIdx];
const newData = new Uint8Array(currentPacket.length + segSize);
newData.set(currentPacket);
newData.set(pageData.subarray(dataPos, dataPos + segSize), currentPacket.length);
currentPacket = newData;
dataPos += segSize;
segIdx++;
if (segSize < 255) {
packets.push(currentPacket);
currentPacket = new Uint8Array(0);
}
} else {
let packetData = new Uint8Array(0);
while (segIdx < numSegments && segTable[segIdx] === 255) {
const segSize = segTable[segIdx];
const newData = new Uint8Array(packetData.length + segSize);
newData.set(packetData);
newData.set(pageData.subarray(dataPos, dataPos + segSize), packetData.length);
packetData = newData;
dataPos += segSize;
segIdx++;
}
if (segIdx < numSegments) {
const segSize = segTable[segIdx];
const newData = new Uint8Array(packetData.length + segSize);
newData.set(packetData);
newData.set(pageData.subarray(dataPos, dataPos + segSize), packetData.length);
packetData = newData;
dataPos += segSize;
segIdx++;
packets.push(packetData);
} else {
currentPacket = packetData;
}
}
}
}
if (packets.length < 2) {
throw new Error('Incomplete headers');
}
// Parse OpusHead
const head = packets[0];
if (String.fromCharCode(...head.subarray(0, 8)) !== 'OpusHead') {
throw new Error('Invalid OpusHead');
}
const headDv = new DataView(head.buffer, head.byteOffset, head.length);
this.head.version = head[8];
this.head.channels = head[9];
this.head.pre_skip = headDv.getUint16(10, true);
this.head.input_sample_rate = headDv.getUint32(12, true);
this.head.output_gain = headDv.getInt16(16, true);
this.head.channel_mapping_family = head[18];
let extraOffset = 19;
if (this.head.channel_mapping_family > 0) {
this.head.stream_count = head[extraOffset++];
this.head.coupled_count = head[extraOffset++];
this.head.channel_mapping = [...head.subarray(extraOffset, extraOffset + this.head.channels)];
}
// Parse OpusTags
const tags = packets[1];
if (String.fromCharCode(...tags.subarray(0, 8)) !== 'OpusTags') {
throw new Error('Invalid OpusTags');
}
const tagsDv = new DataView(tags.buffer, tags.byteOffset, tags.length);
let pOffset = 8;
const vendorLen = tagsDv.getUint32(pOffset, true);
pOffset += 4;
this.tags.vendor = new TextDecoder().decode(tags.subarray(pOffset, pOffset + vendorLen));
pOffset += vendorLen;
const commentCount = tagsDv.getUint32(pOffset, true);
pOffset += 4;
this.tags.comments = [];
for (let i = 0; i < commentCount; i++) {
const len = tagsDv.getUint32(pOffset, true);
pOffset += 4;
const comm = new TextDecoder().decode(tags.subarray(pOffset, pOffset + len));
this.tags.comments.push(comm);
pOffset += len;
}
}
printProperties() {
console.log('OpusHead Properties:');
console.log(this.head);
console.log('\nOpusTags Properties:');
console.log(`Vendor: ${this.tags.vendor}`);
console.log('Comments:');
this.tags.comments.forEach(comm => console.log(comm));
}
// For write: Simple example to write modified headers (audio data not included)
write(newFilename, modifiedHead = {}, modifiedTags = {}) {
Object.assign(this.head, modifiedHead);
Object.assign(this.tags, modifiedTags);
// Rebuild packets
let headPacket = new TextEncoder().encode('OpusHead');
const headBuf = new Uint8Array(1024); // Arbitrary
const headDv = new DataView(headBuf.buffer);
let pos = 0;
headBuf.set(headPacket, pos);
pos += 8;
headBuf[pos++] = this.head.version;
headBuf[pos++] = this.head.channels;
headDv.setUint16(pos, this.head.pre_skip, true);
pos += 2;
headDv.setUint32(pos, this.head.input_sample_rate, true);
pos += 4;
headDv.setInt16(pos, this.head.output_gain, true);
pos += 2;
headBuf[pos++] = this.head.channel_mapping_family;
if (this.head.channel_mapping_family > 0) {
headBuf[pos++] = this.head.stream_count;
headBuf[pos++] = this.head.coupled_count;
this.head.channel_mapping.forEach(m => headBuf[pos++] = m);
}
const finalHead = headBuf.subarray(0, pos);
// Tags
let tagsPacket = new TextEncoder().encode('OpusTags');
const tagsBuf = new Uint8Array(4096);
const tagsDv = new DataView(tagsBuf.buffer);
pos = 0;
tagsBuf.set(tagsPacket, pos);
pos += 8;
const vendorBytes = new TextEncoder().encode(this.tags.vendor);
tagsDv.setUint32(pos, vendorBytes.length, true);
pos += 4;
tagsBuf.set(vendorBytes, pos);
pos += vendorBytes.length;
tagsDv.setUint32(pos, this.tags.comments.length, true);
pos += 4;
this.tags.comments.forEach(comm => {
const commBytes = new TextEncoder().encode(comm);
tagsDv.setUint32(pos, commBytes.length, true);
pos += 4;
tagsBuf.set(commBytes, pos);
pos += commBytes.length;
});
const finalTags = tagsBuf.subarray(0, pos);
// Write
const output = new Uint8Array(finalHead.length + finalTags.length);
output.set(finalHead);
output.set(finalTags, finalHead.length);
fs.writeFileSync(newFilename, output);
console.log('Wrote modified headers (audio data not included in this simple implementation).');
}
}
// Example usage:
// const opus = new OpusFile('example.opus');
// opus.printProperties();
// opus.write('modified.opus', { version: 1 });
- C class (implemented as C++ class for .OPUS file handling):
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <cstdint>
#include <cstring>
#include <map>
class OpusFile {
private:
std::string filename;
std::map<std::string, std::int64_t> head; // Use int64_t for values
std::string vendor;
std::vector<std::string> comments;
uint16_t le16(const unsigned char* p) {
return (p[1] << 8) | p[0];
}
int16_t sle16(const unsigned char* p) {
return static_cast<int16_t>(le16(p));
}
uint32_t le32(const unsigned char* p) {
return (p[3] << 24) | (p[2] << 16) | (p[1] << 8) | p[0];
}
int32_t sle32(const unsigned char* p) {
return static_cast<int32_t>(le32(p));
}
public:
OpusFile(const std::string& fn) : filename(fn) {
parse();
}
void parse() {
std::ifstream file(filename, std::ios::binary);
if (!file) {
throw std::runtime_error("Error opening file");
}
file.seekg(0, std::ios::end);
size_t size = file.tellg();
file.seekg(0);
std::vector<unsigned char> filedata(size);
file.read(reinterpret_cast<char*>(filedata.data()), size);
size_t offset = 0;
std::vector<unsigned char> current_packet;
std::vector<std::vector<unsigned char>> packets;
while (offset < size && packets.size() < 2) {
if (std::memcmp(&filedata[offset], "OggS", 4) != 0) {
throw std::runtime_error("Invalid Ogg file");
}
offset += 4;
unsigned char version = filedata[offset++];
if (version != 0) {
throw std::runtime_error("Invalid Ogg version");
}
unsigned char header_type = filedata[offset++];
offset += 8; // granule
offset += 4; // serial
offset += 4; // page seq
offset += 4; // crc
unsigned char num_segments = filedata[offset++];
std::vector<unsigned char> seg_table(num_segments);
std::memcpy(seg_table.data(), &filedata[offset], num_segments);
offset += num_segments;
size_t page_data_len = 0;
for (auto s : seg_table) page_data_len += s;
std::vector<unsigned char> page_data(page_data_len);
std::memcpy(page_data.data(), &filedata[offset], page_data_len);
offset += page_data_len;
size_t seg_idx = 0;
size_t data_pos = 0;
while (seg_idx < num_segments) {
if (!current_packet.empty()) {
unsigned char seg_size = seg_table[seg_idx];
current_packet.insert(current_packet.end(), page_data.begin() + data_pos, page_data.begin() + data_pos + seg_size);
data_pos += seg_size;
seg_idx++;
if (seg_size < 255) {
packets.push_back(current_packet);
current_packet.clear();
}
} else {
std::vector<unsigned char> packet_data;
while (seg_idx < num_segments && seg_table[seg_idx] == 255) {
unsigned char seg_size = seg_table[seg_idx];
packet_data.insert(packet_data.end(), page_data.begin() + data_pos, page_data.begin() + data_pos + seg_size);
data_pos += seg_size;
seg_idx++;
}
if (seg_idx < num_segments) {
unsigned char seg_size = seg_table[seg_idx];
packet_data.insert(packet_data.end(), page_data.begin() + data_pos, page_data.begin() + data_pos + seg_size);
data_pos += seg_size;
seg_idx++;
packets.push_back(packet_data);
} else {
current_packet = packet_data;
}
}
}
}
if (packets.size() < 2) {
throw std::runtime_error("Incomplete headers");
}
// Parse OpusHead
auto& head_bytes = packets[0];
if (std::memcmp(head_bytes.data(), "OpusHead", 8) != 0) {
throw std::runtime_error("Invalid OpusHead");
}
head["version"] = head_bytes[8];
head["channels"] = head_bytes[9];
head["pre_skip"] = le16(&head_bytes[10]);
head["input_sample_rate"] = le32(&head_bytes[12]);
head["output_gain"] = sle16(&head_bytes[16]);
head["channel_mapping_family"] = head_bytes[18];
size_t extra_offset = 19;
if (head["channel_mapping_family"] > 0) {
head["stream_count"] = head_bytes[extra_offset++];
head["coupled_count"] = head_bytes[extra_offset++];
// Channel mapping as comma-separated string for simplicity
std::string mapping_str;
for (int i = 0; i < head["channels"]; ++i) {
if (!mapping_str.empty()) mapping_str += ", ";
mapping_str += std::to_string(head_bytes[extra_offset + i]);
}
// Store as string since map is int64
head["channel_mapping"] = 0; // Placeholder; print separately if needed
std::cout << "Channel Mapping: " << mapping_str << std::endl; // Handle list separately
}
// Parse OpusTags
auto& tags_bytes = packets[1];
if (std::memcmp(tags_bytes.data(), "OpusTags", 8) != 0) {
throw std::runtime_error("Invalid OpusTags");
}
size_t p_offset = 8;
uint32_t vendor_len = le32(&tags_bytes[p_offset]);
p_offset += 4;
vendor = std::string(reinterpret_cast<char*>(&tags_bytes[p_offset]), vendor_len);
p_offset += vendor_len;
uint32_t comment_count = le32(&tags_bytes[p_offset]);
p_offset += 4;
for (uint32_t i = 0; i < comment_count; ++i) {
uint32_t len = le32(&tags_bytes[p_offset]);
p_offset += 4;
std::string comm(reinterpret_cast<char*>(&tags_bytes[p_offset]), len);
comments.push_back(comm);
p_offset += len;
}
}
void printProperties() {
std::cout << "OpusHead Properties:" << std::endl;
for (const auto& kv : head) {
std::cout << kv.first << ": " << kv.second << std::endl;
}
std::cout << "\nOpusTags Properties:" << std::endl;
std::cout << "Vendor: " << vendor << std::endl;
std::cout << "Comments:" << std::endl;
for (const auto& comm : comments) {
std::cout << comm << std::endl;
}
}
// For write: Simple example to write modified headers (audio data not included)
void write(const std::string& new_filename) {
// Rebuild packets (simplified)
std::vector<unsigned char> head_packet;
head_packet.insert(head_packet.end(), "OpusHead", "OpusHead" + 8);
head_packet.push_back(static_cast<unsigned char>(head["version"]));
head_packet.push_back(static_cast<unsigned char>(head["channels"]));
uint16_t pre = static_cast<uint16_t>(head["pre_skip"]);
head_packet.push_back(pre & 0xFF);
head_packet.push_back((pre >> 8) & 0xFF);
uint32_t rate = static_cast<uint32_t>(head["input_sample_rate"]);
head_packet.push_back(rate & 0xFF);
head_packet.push_back((rate >> 8) & 0xFF);
head_packet.push_back((rate >> 16) & 0xFF);
head_packet.push_back((rate >> 24) & 0xFF);
int16_t gain = static_cast<int16_t>(head["output_gain"]);
head_packet.push_back(gain & 0xFF);
head_packet.push_back((gain >> 8) & 0xFF);
head_packet.push_back(static_cast<unsigned char>(head["channel_mapping_family"]));
if (head["channel_mapping_family"] > 0) {
head_packet.push_back(static_cast<unsigned char>(head["stream_count"]));
head_packet.push_back(static_cast<unsigned char>(head["coupled_count"]));
// Assume mapping not modified; skip detailed rebuild for simplicity
}
std::vector<unsigned char> tags_packet;
tags_packet.insert(tags_packet.end(), "OpusTags", "OpusTags" + 8);
uint32_t vlen = vendor.size();
tags_packet.push_back(vlen & 0xFF);
tags_packet.push_back((vlen >> 8) & 0xFF);
tags_packet.push_back((vlen >> 16) & 0xFF);
tags_packet.push_back((vlen >> 24) & 0xFF);
tags_packet.insert(tags_packet.end(), vendor.begin(), vendor.end());
uint32_t ccount = comments.size();
tags_packet.push_back(ccount & 0xFF);
tags_packet.push_back((ccount >> 8) & 0xFF);
tags_packet.push_back((ccount >> 16) & 0xFF);
tags_packet.push_back((ccount >> 24) & 0xFF);
for (const auto& comm : comments) {
uint32_t clen = comm.size();
tags_packet.push_back(clen & 0xFF);
tags_packet.push_back((clen >> 8) & 0xFF);
tags_packet.push_back((clen >> 16) & 0xFF);
tags_packet.push_back((clen >> 24) & 0xFF);
tags_packet.insert(tags_packet.end(), comm.begin(), comm.end());
}
std::ofstream out(new_filename, std::ios::binary);
out.write(reinterpret_cast<const char*>(head_packet.data()), head_packet.size());
out.write(reinterpret_cast<const char*>(tags_packet.data()), tags_packet.size());
std::cout << "Wrote modified headers (audio data not included in this simple implementation)." << std::endl;
}
};
// Example usage:
// int main() {
// OpusFile opus("example.opus");
// opus.printProperties();
// opus.write("modified.opus");
// return 0;
// }