Vorbis 注释

• 本文核心参考 Xiph官网Vorbis文档《Vorbis 规格 I》# 注释编码《Vorbis 注释字段 & 标头 规范》
• 本文涉及外网文档,无统一中文翻译。因此使用作者翻译,水平有限,感谢谅解。
• 本文有一定专业性,读者应具备 基础计算机科学知识 或 基本编程能力。

【原作者:神武竹 • 未经允许,禁止转载】


「前言」

      紧随Xiph基金会和Ogg容器,Vorbis格式来了。Vorbis技术上很先进,不要小瞧这2002更新至2020的“老技术”,无论难度、设计上都远超MP3。中网上vorbis是无人详解的,直接给你扔英文的毛用没有;我的中文参考资料只有21世纪初的几篇博士论文(豆丁网),致敬前辈!

      Vorbis的算法比较复杂,看看官网文档有多长?料定Vorbis不是暑假可完成的了(上一篇Ogg的java代码也怕是补不上了)。暂定把Vorbis拆成几个小文章和一篇主文章,慢慢写。作者是学生党,预计8月中旬开学,勿念!

2022.8.9


【原作者:神武竹 • 未经允许,禁止转载】

「正文」

      Vorbis注释【Vorbis Comment】是Vorbis格式用来标记音频信息的数据。根据Ogg容器的限制,Vorbis除初始标头外,还有两个辅助标头:注释标头【Comment Header】& 设置标头【Setup Header】。类似MP3的ID3标签,注释标头标记音乐的作者、演奏者、时长、版权等等信息。Vorbis注释应该只包含简短的信息,不应该储存 无关的元数据 或 过长的段落。

      Vorbis注释最开始设计给Vorbis音频使用,但因为简单实用、拓展性好,被陆续装在了FLAC、Opus、Speex音频和Theroa视频格式里,依然叫Vorbis注释。Vorbis注释在Ogg容器里总是单独成包(通常是第二个),作为辅助标头(见《Ogg格式》);即使没有实际内容(向量数为0),也必须存在。

      Vorbis注释包由一个供应商字符串【vendor string】和若干注释向量【comment vector】组成。“供应商字符串”在前,提供了编解码器的信息,包括 源组织名(公司、集团、基金会)、编解码器名称、时间戳等等。例如官方库 libvorbis 的 VENDOR_STRING 为Xiph.Org libVorbis I 20200704 (Reducing Environment)。供应商字符串由编解码器自行添加,用户不需要控制。

      每个注释向量都是一组键值对,或者叫注释字段【comment field】。很像UNIX环境变量,注释字段结构是 字段名称=值【field name = value】。字段名称只能包括ASCII字符(简单起见,绝非歧视非英语国家)的0x20(空格)~ 0x7D( } ),而且不能有0x4D(‘=’因为等号用来分隔字段名称和值)。参考图:

ASCII参考图

      而且字段名称不区分大小写,也就是 A - Z 等同于 a - z,在libvorbis中,全部作大写处理。等号后是字段值,必须使用UTF-8编码,没有特殊限制。

      字段名称=值构成了一个注释向量(实际上就是一个字符串)。 每个向量总长度不能超过 231-1 (有符号int的最大值)字节。向量总数也不能超过 231-1 个。向量的数量可以为0,但是供应商字符串必须存在(其长度也可以为0)。其实这么大的数压根用不完,Vorbis注释的大小约等于无限制。

      字段名不是唯一互斥的,多个向量可以使用同一字段名。比如一首曲子有三个作曲家,可以使用三个向量,每个向量的字段名都是artists

      理论上,字段名称没有任何限制,你可以随意拼一个来,也就是随意定义注释字段(正如 XML 可以随意定义标签)。但这容易导致注释混乱,例如artistcomposer都是作曲者,用哪一个呢?再例如artist可以是演奏者也可以是作曲家,究竟用哪一个呢?因此,Vorbis定义了一系列标准字段名【standard field names】,作为官方参考标准;非强制性,爱用不用,自定义非标准字段名也可以;但要求上下文清晰,不得滥用,不得污染公共命名空间。谁闲的发慌去自己定义字段名

标准字段名表如下:

标准字段名含义/用途
TITLE音乐作品名称
VERSION音乐版本
ALBUM专辑
TRACKNUMBER曲目编号(专辑里的第几首)
ARTIST艺术家。音乐中指作曲者,比如《命运交响曲》作者是贝多芬;有声读物中指文章的作者
PERFORMER表演者。音乐中指演奏者(个人/乐团);有声读物中是阅读者;如果艺术家和表演者是同一个人,那么省略此字段
COPYRIGHT版权归属
LICENSE许可证。如“保留所有权利”,也可以填许可证的URL地址
ORGANIZATION组织。如作曲者的签约公司等等
DESCRIPTION简短描述
GENRE音乐流派。比如民歌、摇滚、爵士
DATE录制日期。建议使用ISO 8601标准,即 YYYY-MM-DD
LOCATION录制地点。建议使用WGS84标准,即 纬度;经度[;海拔]
CONTACT原作者或组织联系方式。电子邮件地址、网址、电话等
ISRCISRC编号。即国际标准音像制品编码

      实际编码时,因为向量之间没有分隔符,必须编码每个向量的长度。每个向量编码时,先将其长度作为一个有符号int整数写入,再写入向量字符串。有符号int叫做向量长度,按照Little Endian顺序。因为供应商字符串必须存在,单独解码;向量可以放在循环里解码,所以在供应商字符串后有一个有符号int向量总数



Java代码实操


package com.mcsw.media.vorbis;

import java.nio.charset.StandardCharsets;
import java.util.*;
import java.io.*;
import javax.sound.sampled.*;
import com.mcsw.media.ogg.*;
import com.mcsw.io.Bits;

/**
 * @author MCSW
 * @version 1.0
 */

public final class VorbisCodec {
    /* 省略若干内容 */

    /**
     * 此方法从字节数组中读取单个向量并返回一个String数组。
     * @param src Vorbis注释包源数据的字节数组
     * @param offset 源数组的偏移量
     * @return 一个String数组,长度为2,0的位置是键,1的位置是值。
     * @exception IndexOutOfBoundsException 读取向量时超出数组长度
     * @exception CommentException 注释向量格式问题
     */
    private static String[] readVector(byte[] src, int offset) {
        // Bits.getInt(byte[] src, int offset)是我写的方法,将数组从偏移量开始的四个连续字节按Little Endian组成int值。
        //读取四个字节并组成int,然后读取int个字节,按照UTF-8转为字符串,再在第一个‘=’处分割字符串
        String[] field = new String(src, offset + 4, Bits.getInt(src, offset), StandardCharsets.UTF_8).split("=", 2);

        // 没有 ‘=’ 抛出自定义的异常
        if (field.length != 2)
            // CommentException是自定义的
            throw new CommentException("char = not found");

        // 检查字段名称有无非法字符
        for (; src[offset] != '='; offset ++) {
            if (src[offset] < 0x20 || src[offset] > 0x7D)
                throw new CommentException("Invaild Character in Vector");
        }
        // 字段名称大写,返回
        field[0] = field[0].toUpperCase(Locale.US);
        return field;
    }

    /**
     * 此方法从字节数组中读取所有向量并返回不可变的Map对象。
     *
     * @param src Vorbis注释包源数据的字节数组
     * @return 一个不可变的Map键值对,键类型为String,值类型为Object,实际上只能为String或String[];必定含有“vendor”键。
     * @exception IndexOutOfBoundsException 读取向量时超出数组长度
     * @exception CommentException 注释向量格式问题
     */
    public static Map<String, Object> readVectors(byte[] src) {
        // 键值对数组,源数组偏移量
        String[] keys;
        String[] values;
        int offset = 4;

        //读取供应商字符串 存储String用的Object
        String str = new String(src, offset, Bits.getInt(src), StandardCharsets.UTF_8);

        // Bits.getInt(byte[]) = Bits.getInt(byte[], 0)
        offset += str.length();

        // 循环、辅助专用工具数——i!在这里是向量总数。
        int i = Bits.getInt(src, offset);
        keys = new String[i];
        values = new String[i];

        // 节点数组
        String[] node;

        // 循环读取向量
        for (i = 0; i < keys.length; i ++) {
            node = readVector(src, offset);
            // readVector确认Key字符串一定是ASCII,所以直接使用String.length();Value字符串可能是UTF-8,所以只能先转换再加长度(效率较低)
            offset += 4 + node[1].getBytes(StandardCharsets.UTF_8).length + node[0].length();
            keys[i] = node[0];
            values[i] = node[1];
        }

        // 存储String用的Object 和 节点集合
        Object value = str;
        List<Map.Entry<String, Object>> buf = new ArrayList<>(1);

        // 添加供应商字符串
        buf.add(Map.entry("vendor", value));

        // 查重 值集合;node转空数组
        List<String> list = new ArrayList<>();
        node = new String[0];

        // 查重
        for (i --; i > 0; i --) {
            if (keys[i] == null)
                continue;
            str = keys[i];
            // 查找所有键相同的节点
            for (offset = 0; offset < i; offset ++) {
                if (str.equals(keys[offset])) {
                    // 清除相同的键,留下值添加到集合
                    list.add(values[offset]);
                    keys[offset] = null;
                }
            }
            //是否有重合
            if (!list.isEmpty()) {
                // 有,value是集合转化为的数组,清空集合
                list.add(values[i]);
                value = list.toArray(node);
                list.clear();
            } else
                value = values[i];
            buf.add(Map.entry(str, value));
        }
        
        return Map.ofEntries(buf.toArray(new Map.Entry[0]));
    }

}

Q:为什么Map的值用Object?不仅不好写,而且代码安全性差,难道就是为了同时放String和String[]?
A:javax.sound.sampled是Java官方标准数字音频包,每个音频文件(或者流、URL)都有一个AudioFileFormat对象;AudioFileFormat有一个成员变量就是一个不可变的Map<String, Object>,用来存储音频信息。此处为了兼容java官方库考虑。

Q:使用“vendor”作为供应商的键,不怕冲突?
A:根据上文,字段名称无视大小写,在官方libvorbis库中统一大写处理;在上面readVector方法中将注释名称全部大写再返回;而“vendor”是小写的,不可能冲突。

【原作者:神武竹 • 未经允许,禁止转载】

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值