// ogg_vorbis_comment.mbt
//
// Ogg 容器(Vorbis / Opus)中 Vorbis Comment packet 的读写。
//
// Ogg Vorbis 的前三个 packet:
// packet 0: identification header(\x01vorbis)
// packet 1: comment header(\x03vorbis + 小端 Vorbis Comment 载荷)
// packet 2: setup header
// Ogg Opus 则是:
// packet 0: OpusHead
// packet 1: OpusTags(OpusTags 魔数 + 同样的 Vorbis Comment 载荷)
//
// 写入时只重建 identification 与 comment 两页;comment 变长时后续页
// 整体后移但内容、页序号、granule 不变——Ogg 播放器按页序号连续
// 容忍空隙,音频数据与时间戳零改动。
///|
/// Ogg 流中 comment 的定位结果。
pub struct OggCommentLoc {
/// comment 载荷(去魔数后的 Vorbis Comment 字节)。
payload : Bytes
/// comment 之后第一个页的偏移(音频数据起点)。
end_offset : Int32
/// 流序列号。
serial : Int32
/// 是否为 Opus 流(否则为 Vorbis)。
is_opus : Bool
}
///|
/// 从 Bytes 里拷贝 [start, end) 段(不 raise,边界由调用方保证)。
fn copy_range(data : Bytes, start : Int32, end : Int32) -> Bytes {
let out = @buffer.Buffer()
let mut i = start
while i < end {
out.write_byte(data.at(i))
i += 1
}
out.to_bytes()
}
///|
/// 从 Ogg 流定位 comment packet;不是 Ogg 或无 comment 返回 `None`。
///
/// `end_offset` 是 comment packet **最后一个字节之后**的偏移(精确到
/// packet 边界,不是页边界),供写入时把 setup/音频原样拼接。
pub fn read_ogg_comment(data : Bytes) -> OggCommentLoc? {
let mut offset = 0
let pages : Array[OggPage] = []
// 逐段扫描:每页的段重组为 packet,直到找到第 2 个完整 packet,
// 并记录其**字节级**结束偏移。
let mut packet_count = 0
let mut comment_end = -1
while offset < data.length() {
let page = parse_page(data, offset) catch { _ => return None }
pages.push(page)
// 直接计算:packet 边界 = 段载荷累计;每遇到 <255 的段,
// 记下该段结束的字节偏移(= 下一个 packet 的开始)。
let mut cursor = page.body_offset
for i in 0..= 2 {
break
}
if pages.length() > 4 {
return None
}
}
if packet_count < 2 || comment_end < 0 {
return None
}
let packets = assemble_packets(data, pages)
if packets.length() < 2 {
return None
}
let ident = packets[0]
let comment = packets[1]
// Vorbis:ident 以 \x01vorbis 开头,comment 以 \x03vorbis 开头
if ident.length() >= 7 &&
ident.at(0) == b'\x01' &&
ident.at(1) == b'v' &&
ident.at(2) == b'o' &&
ident.at(3) == b'r' &&
ident.at(4) == b'b' &&
ident.at(5) == b'i' &&
ident.at(6) == b's' {
if comment.length() > 7 && comment.at(0) == b'\x03' {
return Some({
payload: copy_range(comment, 7, comment.length()),
end_offset: comment_end,
serial: pages[0].serial,
is_opus: false,
})
}
return None
}
// Opus:ident 以 OpusHead 开头,comment 以 OpusTags 开头
if ident.length() >= 8 &&
ident.at(0) == b'O' &&
ident.at(1) == b'p' &&
ident.at(2) == b'u' &&
ident.at(3) == b's' &&
ident.at(4) == b'H' &&
ident.at(5) == b'e' &&
ident.at(6) == b'a' &&
ident.at(7) == b'd' {
if comment.length() > 8 &&
comment.at(0) == b'O' &&
comment.at(1) == b'p' &&
comment.at(2) == b'u' &&
comment.at(3) == b's' &&
comment.at(4) == b'T' &&
comment.at(5) == b'a' &&
comment.at(6) == b'g' &&
comment.at(7) == b's' {
return Some({
payload: copy_range(comment, 8, comment.length()),
end_offset: comment_end,
serial: pages[0].serial,
is_opus: true,
})
}
return None
}
None
}
///|
/// 解析 comment 载荷为 VorbisComment。
pub fn parse_ogg_comment_payload(
loc : OggCommentLoc,
) -> VorbisComment raise TagError {
parse_vorbis_comment(loc.payload.exact_view())
}
///|
/// 用新 comment 重建 Ogg 流前缀。
///
/// Vorbis/Opus 的 setup packet 与 comment 在**同一页**(页 1 的 lacing
/// 表是 `comment_len, 255, ..., setup_tail`)。直接把 comment 换掉再
/// "原样拼接"会丢掉 setup 的 lacing 信息。这里改为:
/// 1. 重组原页 0 + 页 1,取出 ident / 旧 comment / setup 三个 packet;
/// 2. 重建页 0 = ident(原样),页 1 = 新 comment,页 2 = setup(原样);
/// 3. 从原页 2 起,逐页重编号、重算 CRC、保留 granule 与 header_type。
pub fn set_ogg_comment(
data : Bytes,
vc : VorbisComment,
) -> Bytes raise TagError {
let loc = match read_ogg_comment(data) {
Some(l) => l
None => raise NotTagged(format="ogg")
}
let payload = serialize_vorbis_comment(vc)
let comment_packet = if loc.is_opus {
@utf8.encode("OpusTags").add(payload)
} else {
Bytes::from_array([0x03, 0x76, 0x6F, 0x72, 0x62, 0x69, 0x73]).add(payload)
}
// 重组页 0 与页 1(comment 与 setup 同页)
let page0 = parse_page(data, 0) catch { _ => raise NotTagged(format="ogg") }
let page1 = parse_page(data, page0.total_len) catch {
_ => raise NotTagged(format="ogg")
}
let head_packets = assemble_packets(data, [page0, page1])
if head_packets.length() < 2 {
raise BadHeader(reason="Ogg 头不足两个 packet(ident/comment)")
}
let ident_payload = head_packets[0]
// Vorbis 有三个 header packet;Opus 只有两个(无 setup)
let has_setup = head_packets.length() >= 3
let setup_payload = if has_setup { head_packets[2] } else { Bytes::new(0) }
// 重建页 0/1:ident 原样;comment 与 setup 作为**两个独立 packet**
// 合入同一页(Vorbis 的 ffmpeg 解析器要求 comment 后紧邻 setup 或音频)
let out = @buffer.Buffer()
out.write_bytes(make_page(loc.serial, 0, 0x02, 0L, ident_payload))
let page1_packets = if has_setup {
[comment_packet, setup_payload]
} else {
[comment_packet]
}
out.write_bytes(make_page_multi(loc.serial, 1, 0x00, 0L, page1_packets))
// 音频页从原页 2 起**原样**拼接:
// - 页内字节不变 → CRC 仍然有效,无需重算;
// - 页序号(seq=2,3,...)保持连续,与重建后的页 0/1 不冲突;
// - 绝对偏移变化对 Ogg 无影响(页序与 granule 才是语义)。
let mut i = page0.total_len + page1.total_len
while i < data.length() {
out.write_byte(data.at(i))
i += 1
}
out.to_bytes()
}