作者:Akustikrausch (Andreas Wendorf)
一个小型、无依赖的 Yamaha SMAF 铃声播放器 (.mmf),使用 C++ 编写。你只需给它一个那些曾在 2004 年让你的三星、夏普或松下手机发出“滴滴滴滴”声的文件,它就能为你输出清晰的立体声音频。无需手机、无需固件、无需采样 ROM。只有 FM 芯片,在软件中重建。
这是一个几乎没人愿意用开放代码保存的整整十年的声音。数以百万计的这种文件仍在流传,而 从来没人完成过一个开源的播放器:尝试在 2004 年就停滞了,或者绕过了合成器本身。所以,这里有一个 C++ 版本,从格式层面开始编写,带有自己的 FM 合成核心。
🎧 这段代码已实际产品化:FXChainPlayer。 一个原生 Windows 音频播放器,能让你把旧铃声与 FLAC、SID 和 Amiga 模块一起播放。这个引擎每天都在那里运行,处理着数百个真实手机铃声的语料库。如果你只想听,可以下载播放器;如果想了解芯片的工作原理,请继续阅读。
Yamaha MA 系列(YMU762 = MA-3, YMU765 = MA-5)是 2000 年代早期大量手机中使用的音效芯片。它是一个真正的 FM 合成器,与 Adlib 和世嘉 Genesis 时代的 OPL/OPN 芯片是近亲,只是为手机缩小了尺寸。它所播放的文件,SMAF (.mmf),携带了 FM 乐谱以及可选的 ADPCM 采样。和弦铃声就此诞生。
随后智能手机到来,MP3 铃声取代了一切,整个格式悄无声息地消亡了。雅马哈下架了它的 SDK。与 Amiga 或 C64 社区不同,几乎没人编写过模拟器。现有的情况是:
umjammer/vavi-sound (Java) 能解析并播放 SMAF,它是一个极好的行为参考,但它根本不进行 FM 合成:它将音色重新包装为通用 MIDI,让 Java 的合成器近似模拟。而且它是 Java。因此,一个原生的、宽松许可的、无需 ROM 的实际 FM 芯片播放器,之前并不存在。这就是它。解析器的每个字节和合成器的每个算子都是根据公开的 SMAF 格式描述,加上在十六进制编辑器中大量审视真实文件后编写的,从未复制过其他播放器。
引擎由五个小型部分组成,每个部分都可以独立测试:
src/
smaf_file MMMD 容器:大端序 TLV 块(CNTI/OPDA/MTR/ATR),
两个乐谱生成(HandyPhone 用于 MA-1/2,Mobile 用于 MA-3/5)
ma_player 两个事件解码器(+ 用于压缩变体的奥村哈夫曼解码),
一个像芯片一样以毫秒为单位运行的调度器,
以及驱动一组 FM 声部的渲染循环
ma_fm_core FM DSP:2 算子和 4 算子运算器、指数 ADSR 包络、
OPL 风格连接算法,以及自建的通用 MIDI 近似音色库
smaf_voice 将雅马哈声音 SysEx 数据块(MA-3/5 的 VM35,MA-1/2 的 VMA)
解码为 FM 音色,包括 MA-3 的位压缩格式
yamaha_adpcm YM2608 系列 4 位 ADPCM 编解码器,用于采样声部
时序模型是一个令人惊喜的地方:SMAF 没有速度事件,时间纯粹是度量性的(事件时间 = 时值总和乘以一个时间基准,单位为毫秒),因此一个 tick 直接映射为一毫秒,再映射为采样点。FM 核心直接以你的输出采样率运行,因此合成路径上无需重采样。
MA 芯片还带有一个小型 通用 MIDI 采样 ROM,这样铃声可以说“播放程序 5”并得到一个预设的钢琴音色。这个 ROM 属于雅马哈,从未被公开转储,本工程 不包含它、不需要它,也永远不会捆绑它。这是一个硬性界限。
取而代之的是,ma_fm_core 带有一个 自建的 FM 音色库,用于替代 ROM:一个要求“程序 5”的文件会得到一个属于该家族、听起来合理的 FM 声部。它不会与 2003 年的手机完全按位一致,这是一个诚实的折衷。你得到的是真实的 FM 乐谱、真实的音符时序、真实的 ADSR,由它自己的合成器演奏。识别到它,并能播放。
那些带有 自己自定义声部 的文件(很多都是这样)则能完全按创作原文播放,因为这些声部存在于文件内,而不是 ROM 中。
cmake -B build && cmake --build build
./build/smaf-render your_ringtone.mmf out.wav 30
或者将库连接到你的代码中:
#include "smaf_file.h"
#include "ma_player.h"
using namespace fxchain::smaf;
SmafFile file;
file.parse(bytes.data(), bytes.size()); // MMMD 容器
MaPlayer player;
player.init(file, 44100); // 构建声部 + 解码乐谱
float buf[1024 * 2]; // 交错立体声
int frames = player.render(buf, 1024); // 驱动合成器
43 79 06 7F 03);以及 SD-1 特定的更高波形。请参见 ROADMAP.md。.mld)兄弟格式(一个近亲;请参见 ROADMAP.md 中的 m3)。这个解析器设计用于处理不受信任的文件,因此采用了防御性编写:每个块长度都限制在真实缓冲区范围内,哈夫曼膨胀具有节点+深度保护及输出上限,事件解码器带有迭代+总事件上限。遇到问题时会降级为“不是有效的 SMAF 文件”,而不是崩溃。详细信息和报告方式见 SECURITY.md。
src/ 引擎(四个小型翻译单元加一个纯头文件编解码器,无依赖)。example/smaf-render.cpp 不到 100 行的 WAV CLI。CMakeLists.txt 构建静态 smaf 库及示例程序。仅此而已。
Apache License 2.0(见 LICENSE)。解析器和合成器均为原创作品;没有包含第三方代码,也没有包含雅马哈 ROM 或 SDK 材料。格式知识来源于公开的 SMAF 规范和观察。
与雅马哈无关联、未经授权或认可。“SMAF”和 MA 芯片名称属于雅马哈。这是一个互操作性工作:一个独立播放器,用于开放文件格式,构建时未使用任何雅马哈代码或受保护的采样 ROM。
向所有仍然记得自己铃声是什么样子的人致敬。