我的 MP3 编码器之旅:从 LAME 到 Shine 的 Go 实现

编者按:本文作者以轻松幽默的口吻,生动地向我们讲述了自己实现MP3编码的曲折历程。

起初,作者满怀信心地想利用LAME这个业界公认的最佳MP3编码器。然而种种问题让初心萌动的他不得不放弃。随后,作者又尝试自己从零开始搞一个编码器,却在复杂的编码算法面前退缩。在绝望中,作者终于发现了Shine这个"朴实无华"的编码器,于是"一见钟情",决定移植一个Go语言版本。

接下来,作者描绘了将Shine从C语言移植到Go语言的曲折探索。好不容易编译成功,输出的文件又无法播放。作者没有放弃,终于找到cxgo这个自动转换工具。然而,转换后的代码仍需进行少量调整。

在阅读本文时,你几乎能够感同身受作者在实现目标时遇到的种种困难与挫折。但更可贵的是,作者从不放弃,保持积极乐观的心态,最终实现了自己的目标。无论目标大小,这种坚持不懈的精神都值得我们学习。相信阅读此文,你也会像编者一样,对MP3编码有了更深的认识。

本文由丘山子翻译,原文链接:braheezy.github.io/posts/what-... ,原文作者:Michael Braha。

一、我心目中最优秀的 MP3 音频编码器:LAME

说到 MP3 这种音频编码,有一款开源的编码器:LAME。因其提供极其丰富的功能,多年来一直以来受到软件和音频工程师的热烈欢迎。如果我们想将音频数据编码成具有专业性能和质量的 MP3,要么使用 LAME,要么使用基于它构建的工具

作为一名程序员,想要将音频数据编码为 MP3,我们可能会去寻找所在语言生态中的开源库包。让我们看看主流语言生态中的 MP3 编码器都有哪些:

一路看下来,似乎都离不开 LAME。

在之前我尝试Quite OK Audio (QOA) 格式的文件转换为各种音频格式时,我感到非常有趣,这是一种任何工具或库都不支持的格式。这个过程包括将 .qoa 文件转换为 .mp3。

在一个黑云密布、风雨交加的夜晚,我第一次遇见了 LAME。go-lame 项目为 LAME 提供了 Go 语言的bindings。对于我编写的所有 Go 项目,我都希望它们能够支持 Linux、Mac 和 Windows 平台,但 go-lame 没有明确说明它将支持 Windows 平台。这就需要我来做了!也许 ChatGPT 也可以帮忙。

无论我尝试使用 C 语言跨平台编译器、原生 Windows 环境,还是那些声称捆绑了所有工具的、能够构建一切的容器,我都无法将 go-lame 构建为 Windows 版本。

恼羞成怒之下,我放弃了继续开发 QOA 程序,以让其支持 Windows 上的 MP3格式。但这给我留下了不好的印象......如果我有一个不依赖 LAME 的 Go 库就好了。也许我应该写一个?

二、自己手搓一个MP3编码器?

于是我充满自信地开始深入研究 MP3 音频格式的原理。我刚开始觉得这有什么难的?

随着对其的深入了解,我得出一个结论:非常难。

没有类似 MPEG-1 的标准规定编码器应该如何工作。只有关于文件格式和 MP3 比特流编码结构的官方规范。这使得解码器的行为都是一样的,但编码器如何创建比特流则取决于作者。这就导致了 90 年代到 00 年代的编码质量参差不齐。

描述实现编码器所需的细节的相关文档需要付费获取。这种隐藏和封锁知识的做法令人反感,而且会扼杀大家对其的兴趣和进一步创新。 在整个 90 年代和 00 年代,MP3 的基础编码/解码技术一直处于专利保护之下。直到 2017 年,它才在美国失去专利。像我这样的开源开发者和业余开发者会避免任何涉及专利的东西。 MP3 使用的算法非常复杂。看看我从 PDF 文件中找到的这个很棒的图表:

请大家注意一个模块:psycho-acoustic model。除了这是一个非常酷的词之外,还意味着编码算法将分析音频波的特征,并丢弃人类无法听到的部分。举个简单的例子,人类只能听到 20 Hz 和 20 kHz 之间的频率,因此要去掉高于和低于这些频率的部分。

如果你想了解更多关于 MP3 编码算法的细节,这个 PDF 是我找到的最好的学习文档

考虑到我不会从头开始实现一个新的 MP3 编码器,我接下来考虑将 C LAME 库移植到 Go。我打开了一个 shell 来检查这样做是否合理,并运行了 cloc 命令来计算项目中的代码行数:

shell 复制代码
$ cloc .
      62 text files.
      62 unique files.
      11 files ignored.

github.com/AlDanial/cloc v 1.90  T=0.04 s (1381.9 files/s, 735490.1 lines/s)
------------------------------------------------------------------------------
Language                    files          blank        comment           code
------------------------------------------------------------------------------
C                              21           3130           3427          16610
C/C++ Header                   24            483            726           1670
Bourne Shell                    1             64            224            503
make                            3             47             14            164
Windows Resource File           1              3              1             46
IDL                             1              1              0             31
------------------------------------------------------------------------------
SUM:                           51           3728           4392          19024
------------------------------------------------------------------------------

16,000 行晦涩难懂的 C 语言代码,充斥着指针"魔法和神秘的内存操作?不用了,谢谢!

三、其他 MP3 编码器能否大放异彩?

我必须要明白,在这里我只是为了解决这个 QOA 程序中的一个小错误。支持 MP3 并不是特别重要。我需要一条通往成功的捷径。我并不需要什么高级的 MP3 编码器。

LAME 很友好地提到了其他编码器,并尽可能地吹嘘了他们的优秀表现(我也认为他们应该这样做!)。编码器列表中的一项引起了我的注意:

Shine 是由 LAME 的 Gabriel Bouvigne 制作的一个没有什么特色但干净易读的 MP3 编码器。作为一款入门或学习的工具,它非常不错。也可能也是唯一一款开源的、使用定点数学运算的MP3 编码器。(译者注:在计算机中,浮点数运算比定点数运算更精确,但是定点数运算更快,因为它们不需要使用浮点处理器。因此,对于某些应用程序,如嵌入式系统或低功耗设备,使用定点数学运算可能更为合适。 )

这正是我要寻找的:

  • 开源
  • 没有特色,即不过于复杂
  • 可读性强
  • 对新手十分友好

通过快速的互联网搜索,我找到了 Shine 的现代版本。Shine 是一个相对较小的项目,代码量比 LAME 少得多,因此它更容易理解和移植。

四、尝试:将 C 代码转换为 Go 代码

我进行了两次认真的尝试。虽然很遗憾,但这确实让我对整个程序有了更好的理解。

在第一次尝试时,我坐下来,一个文件一个文件地把每个 C 语言函数转换为对应的 Go 语言代码。我得承认,很多东西我都是直接扔给 ChatGPT 让它转换的。下面是一个转换示例:

最后,我终于编译出了一些东西......但是它生成的 MP3 文件比我对比的 MP3 参考文件大了 5 倍。音频播放器拒绝播放它们,甚至告诉我文件格式不正确。为此,我花了几天时间去排除故障,但根据我的输出文件,我感觉我偏离了正确的方向。绝望之余,我在网上搜索了如何进行 C to Go ...

然后找到了 cxgo!它声称可以将 C 转换为 Go。尽管这款工具有实验性警告,但它还是给了我一线希望,于是我很快下载了它。我在一个小的 C 语言文件上进行了测试,结果看起来不错。我把它扔给整个 Shine 库,出乎意料的是,没有出现任何错误!我在几秒钟内得到了一个完整的 Go 程序(这就是第一次尝试失败的痛苦所在)。

以下是一个示例。这段 C 代码如下:

C 复制代码
/*
 * shine_putbits:
 * --------
 * write N bits into the bit stream.
 * bs = bit stream structure
 * val = value to write into the buffer
 * N = number of bits of val
 */
void shine_putbits(bitstream_t *bs, unsigned int val, unsigned int N) {
#ifdef DEBUG
  if (N > 32)
    printf("Cannot write more than 32 bits at a time.\n");
  if (N < 32 && (val >> N) != 0)
    printf("Upper bits (higher than %d) are not all zeros.\n", N);
#endif

  if (bs->cache_bits > N) {
    bs->cache_bits -= N;
    bs->cache |= val << bs->cache_bits;
  } else {
    if (bs->data_position + sizeof(unsigned int) >= bs->data_size) {
      bs->data = (unsigned char *)realloc(bs->data,
                                          bs->data_size + (bs->data_size / 2));
      bs->data_size += (bs->data_size / 2);
    }

    N -= bs->cache_bits;
    unsigned int shift = val >> N;
    bs->cache |= shift;
#ifdef SHINE_BIG_ENDIAN
    *(unsigned int *)(bs->data + bs->data_position) = bs->cache;
#else
    *(unsigned int *)(bs->data + bs->data_position) = SWAB32(bs->cache);
#endif
    bs->data_position += sizeof(unsigned int);
    bs->cache_bits = 32 - N;
    if (N != 0)
      bs->cache = val << bs->cache_bits;
    else
      bs->cache = 0;
  }
}

转换为Go语言代码后:

go 复制代码
import (
	"github.com/gotranspile/cxgo/runtime/libc"
	"unsafe"
)

func shine_putbits(bs *bitstream_t, val uint64, N uint64) {
	if uint64(bs.Cache_bits) > N {
		bs.Cache_bits -= int64(N)
		bs.Cache |= val << uint64(bs.Cache_bits)
	} else {
		if bs.Data_position+int64(unsafe.Sizeof(uint64(0))) >= bs.Data_size {
			bs.Data = (*uint8)(libc.Realloc(unsafe.Pointer(bs.Data), int(bs.Data_size+bs.Data_size/2)))
			bs.Data_size += bs.Data_size / 2
		}
		N -= uint64(bs.Cache_bits)
		bs.Cache |= val >> N
		*(*uint64)(unsafe.Pointer((*uint8)(unsafe.Add(unsafe.Pointer(bs.Data), bs.Data_position)))) = (bs.Cache >> 24) | ((bs.Cache >> 8) & 0xFF00) | (bs.Cache&0xFF00)<<8 | bs.Cache<<24
		bs.Data_position += int64(unsafe.Sizeof(uint64(0)))
		bs.Cache_bits = int64(32 - N)
		if N != 0 {
			bs.Cache = val << uint64(bs.Cache_bits)
		} else {
			bs.Cache = 0
		}
	}
}

五、过渡到纯 Go 库

最后一项任务是全局搜索代码,删除所有使用 cxgo/runtime/libc 的地方。我还会尝试删除unsafe包,因为安全总比不安全好,不是吗?

删除 libc 的工作很简单,只需将数学函数替换为相应的数学函数即可。而让库变得安全则更具挑战性。截至本文撰写时,库中还有一个开放的issue,即删除最后一次使用的 unsafe 包。只有一个issue了,你能解决吗?

没有 C 语言代码啦!任务完成!现在,Windows 用户可以用纯 Go 来编码 MP3 文件了。耶?

相关推荐
monkey_meng25 分钟前
【遵守孤儿规则的External trait pattern】
开发语言·后端·rust
Estar.Lee40 分钟前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
新知图书1 小时前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
盛夏绽放2 小时前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js
Ares-Wang2 小时前
Asp.net Core Hosted Service(托管服务) Timer (定时任务)
后端·asp.net
一只爱撸猫的程序猿3 小时前
简单实现一个系统升级过程中的数据平滑迁移的场景实例
数据库·spring boot·程序员
Rverdoser3 小时前
RabbitMQ的基本概念和入门
开发语言·后端·ruby
Tech Synapse4 小时前
Java根据前端返回的字段名进行查询数据的方法
java·开发语言·后端
.生产的驴4 小时前
SpringCloud OpenFeign用户转发在请求头中添加用户信息 微服务内部调用
spring boot·后端·spring·spring cloud·微服务·架构
微信-since811924 小时前
[ruby on rails] 安装docker
后端·docker·ruby on rails