Something went wrong. Try again.
Plays short audio files such as a terminal bell through PipeWire on Linux and WASAPI on Windows.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Opus in Ogg, as RFC 7845 maps it: the identification header, then the//! comment header, then audio packets, always decoded at 48 kHz.
const std = @import("std");const Allocator = std.mem.Allocator;const ogg = @import("ogg");const opus = @import("opus");
const decode_mod = @import("../decode.zig");const Builder = decode_mod.Builder;const Error = decode_mod.Error;
const rate = 48000;/// The longest an Opus packet can be: 120 ms (RFC 6716, Section 3.2.5).const max_packet_frames = rate * 120 / 1000;
/// The identification header (RFC 7845, Section 5.1).const Head = struct { channels: u8, pre_skip: u16, /// Q7.8 dB, to be applied to everything decoded. output_gain: i16, streams: u8, coupled_streams: u8, mapping: [255]u8,
fn parse(packet: []const u8) Error!Head { if (packet.len < 19 or !std.mem.startsWith(u8, packet, "OpusHead")) return error.InvalidData; // Only the major version, the top four bits, is one a decoder must // understand. if (packet[8] >> 4 != 0) return error.UnsupportedFormat; var head: Head = .{ .channels = packet[9], .pre_skip = std.mem.readInt(u16, packet[10..12], .little), .output_gain = std.mem.readInt(i16, packet[16..18], .little), .streams = 1, .coupled_streams = 0, .mapping = @splat(0), }; if (head.channels == 0) return error.InvalidData; switch (packet[18]) { // Mono or stereo, one stream, no table. 0 => { if (head.channels > 2) return error.InvalidData; head.coupled_streams = head.channels - 1; head.mapping[1] = 1; }, // The Vorbis channel orders, up to eight channels. 1 => { if (head.channels > 8) return error.InvalidData; if (packet.len < 21 + @as(usize, head.channels)) return error.InvalidData; head.streams = packet[19]; head.coupled_streams = packet[20]; @memcpy(head.mapping[0..head.channels], packet[21..][0..head.channels]); }, // Ambisonics, or channels with no layout, neither of which a // speaker can be chosen for. else => return error.UnsupportedFormat, } return head; }
fn gain(head: Head) f32 { const db = @as(f32, @floatFromInt(head.output_gain)) / 256.0; return std.math.pow(f32, 10.0, db / 20.0); }};
/// Where each WAVE channel is found in the Vorbis order that mapping family 1/// uses (Vorbis I, Section 4.3.9), indexed by channel count.const vorbis_to_wave = [9][]const u8{ &.{}, &.{0}, &.{ 0, 1 }, &.{ 0, 2, 1 }, &.{ 0, 1, 2, 3 }, &.{ 0, 2, 1, 3, 4 }, &.{ 0, 2, 1, 5, 3, 4 }, &.{ 0, 2, 1, 6, 5, 3, 4 }, &.{ 0, 2, 1, 7, 5, 6, 3, 4 },};
pub fn decode(gpa: Allocator, bytes: []const u8, out: *Builder) Error!void { var input: std.Io.Reader = .fixed(bytes); const page_buffer = try gpa.create([ogg.Page.max_size]u8); defer gpa.destroy(page_buffer); var demuxer: ogg.Demuxer = .init(gpa, &input, page_buffer, .{}); defer demuxer.deinit();
// The first logical bitstream that is Opus; anything else in the file, // such as a Skeleton stream, is passed over. const head, const serial = while (try nextPacket(&demuxer)) |packet| { if (packet.first and ogg.codec.identify(packet.data) == .opus) break .{ try Head.parse(packet.data), packet.serial_number }; } else return error.InvalidData;
var decoder = opus.MultistreamDecoder.init(gpa, rate, head.channels, head.streams, head.coupled_streams, head.mapping[0..head.channels]) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.BadArg => return error.InvalidData, }; defer decoder.deinit(gpa);
const pcm = try gpa.alloc(f32, max_packet_frames * @as(usize, head.channels)); defer gpa.free(pcm);
try out.begin(rate, head.channels); const order = vorbis_to_wave[head.channels]; const gain = head.gain(); // Frames still to be thrown away from the front: the decoder's warm-up. var skip: usize = head.pre_skip; var last_granule: ?i64 = null;
while (try nextPacket(&demuxer)) |packet| { if (packet.serial_number != serial) continue; // The comment header. if (packet.index == 1) continue; if (packet.granule_position) |g| last_granule = g;
const n = decoder.decode(packet.data, pcm) catch return error.InvalidData; const dropped = @min(skip, n); skip -= dropped; const decoded = pcm[dropped * head.channels .. n * head.channels]; const samples = try out.addFrames(decoded.len / head.channels); for (0..decoded.len / head.channels) |i| { const from = decoded[i * head.channels ..][0..head.channels]; for (samples[i * head.channels ..][0..head.channels], order) |*s, o| s.* = from[o] * gain; } if (packet.last) break; }
// The last granule position says how long the stream really is; what the // encoder padded the last packet out with goes (RFC 7845, Section 4.4). if (last_granule) |g| { const total = std.math.cast(usize, g - head.pre_skip) orelse 0; if (total < out.frames()) out.samples.shrinkRetainingCapacity(total * head.channels); }}
fn nextPacket(demuxer: *ogg.Demuxer) Error!?ogg.Packet { return demuxer.next() catch |err| switch (err) { error.OutOfMemory => error.OutOfMemory, else => error.InvalidData, };}
test "Ogg Opus, mono and stereo" { try decode_mod.expectTone(@embedFile("../tests/data/tone-mono.opus"), 48000, 1, 4800); try decode_mod.expectTone(@embedFile("../tests/data/tone-stereo.opus"), 48000, 2, 4800);}
test "the identification header" { try std.testing.expectError(error.InvalidData, Head.parse("OpusHead")); var head: [19]u8 = "OpusHead\x01\x02\x38\x01\x80\xbb\x00\x00\x00\x00\x00".*; const parsed = try Head.parse(&head); try std.testing.expectEqual(@as(u16, 312), parsed.pre_skip); try std.testing.expectEqual(@as(u8, 1), parsed.coupled_streams); head[18] = 2; try std.testing.expectError(error.UnsupportedFormat, Head.parse(&head)); head[18] = 0; head[8] = 0x10; try std.testing.expectError(error.UnsupportedFormat, Head.parse(&head));}