MIDI音符与频率转换:从十二平均律到嵌入式音频合成的核心算法
1. 项目概述:从数字音符到物理声波
如果你玩过电子音乐,或者接触过任何数字音频工作站(DAW),那么“MIDI”这个词对你来说一定不陌生。它就像音乐世界的“电报码”,用一串串数字指令告诉合成器或音源:“现在,用多大的力度,按下哪个键,持续多久”。但这里有个有趣的问题:当MIDI说“按下中央C(MIDI编号60)”时,你的设备究竟是如何知道要发出一个频率为261.63 Hz的声音呢?这个从抽象数字编号到具体物理频率的映射关系,就是“MIDI音阶与频率关系”的核心。
我最初深入研究这个关系,是因为在为一个嵌入式音乐项目编写代码时遇到的坑。当时我想用一块微控制器(比如热词里提到的AT32)的PWM输出直接合成简单的旋律。我天真地以为,把MIDI编号直接丢给一个公式就能得到完美的音高,结果出来的声音跑调跑到姥姥家去了。后来才发现,从MIDI编号到频率,中间隔着十二平均律、标准音高(A4=440Hz)以及那看似简单却至关重要的指数运算。搞懂这套关系,不仅是音乐程序员的必修课,对于任何想理解数字音乐底层逻辑、进行音频合成、甚至自己设计简单乐器的硬件爱好者(比如用PWM控制蜂鸣器播放旋律)来说,都是绕不开的基础。
简单来说,这个项目就是彻底拆解MIDI音符编号(Note Number)与对应音高频率(Frequency in Hertz)之间的数学转换关系。它解决了“如何让机器精确地‘知道’该发出什么音高”的问题。无论你是软件开发者想在代码中实现音高计算,还是硬件工程师在用PWM或DAC生成特定频率的波形,亦或是音乐爱好者单纯好奇数字音乐的工作原理,理解这套映射都极具价值。接下来,我们就从最根本的原理开始,一层层把它剥开讲透。
1.1 核心概念解析:MIDI、音阶与频率
在深入公式之前,我们得先统一语言,明确三个核心概念在本文语境下的具体含义。
MIDI音符编号:这是MIDI协议中用于标识一个具体乐音的数字。它的范围是0到127,共128个。编号69被定义为中央A(A4),其标准频率为440 Hz。这是一个线性、离散的整数序列。编号每增加1,代表在钢琴键盘上向右移动一个半音(包括白键和黑键)。
音阶:这里主要指符合十二平均律的音高体系。十二平均律将一个八度(频率翻倍)均匀地分为12个半音。每个半音之间的频率比是一个常数,即2的12次方根(≈1.059463)。这意味着,从物理听觉上,任意相邻两个半音(如C到C#)的“音高距离感”是相同的。
频率:声音的物理属性,单位是赫兹(Hz),表示每秒振动的次数。它决定了我们听到的音高。频率越高,音高越高。频率关系是指数增长的:每升高一个八度,频率翻倍。
理解这三者的关系,关键在于抓住“桥梁”——十二平均律。MIDI编号是台阶的序号,音阶是台阶的均匀分布规则,而频率则是每个台阶所处的实际高度。MIDI协议通过采纳十二平均律和A4=440Hz这个国际标准,将离散的编号与连续的频率世界精确地连接了起来。
2. 核心转换公式的深度推导与解读
知道了“是什么”之后,我们迫切想知道“怎么算”。MIDI编号到频率的转换并非凭空想象,其背后有坚实的音乐声学与数学基础。
2.1 标准转换公式及其由来
最核心、最通用的转换公式如下:
f = 440 * 2^((n - 69) / 12)
其中:
f:所求频率,单位Hz。n:MIDI音符编号(0-127)。440:基准频率,即A4(中央A)的标准频率。69:A4对应的MIDI编号。12:一个八度内的半音数量。
这个公式是怎么来的? 我们可以分步推导:
-
确定基准点:我们规定当
n = 69时,f = 440。代入公式:440 = 440 * 2^((69 - 69) / 12) = 440 * 2^0 = 440。完美成立。 -
理解半音步进:在十二平均律中,每升高一个半音(
n增加1),频率变为原来的2^(1/12)倍(约1.059463倍)。反之,每降低一个半音(n减少1),频率变为原来的2^(-1/12)倍。 -
推广到任意差值:那么,对于任意编号
n,它与基准点A4 (n=69) 相差了(n - 69)个半音。因此,其频率应该是基准频率440Hz乘以2^(1/12)的(n-69)次方。即:f = 440 * (2^(1/12))^(n-69) = 440 * 2^((n-69)/12)。
公式的直观理解:(n - 69) 可以看作以A4为原点的“半音坐标”。公式 2^((n - 69) / 12) 就是这个坐标系的“频率缩放因子”。当坐标为正(音更高),因子大于1;坐标为负(音更低),因子小于1。
2.2 公式的变体与相关计算
掌握了标准公式,我们就能应对各种情况。
已知频率求MIDI编号: 有时我们需要反向计算,例如分析一段音频的主音高。对标准公式两边取以2为底的对数(或用自然对数转换):
由于编程中常用自然对数 log (以e为底) 或 log10,我们可以用换底公式:
n = 69 + 12 * (log(f / 440) / log(2))
计算半音与八度的频率比:
- 一个半音的频率比:
r_semitone = 2^(1/12) ≈ 1.0594630943592953 - 一个八度的频率比:
r_octave = 2^(1) = 2这意味着,MIDI编号每增加12,频率就精确地翻倍。例如,A5 (MIDI 81) 的频率是440 * 2^((81-69)/12) = 440 * 2^1 = 880 Hz。
微调与音分: 在音乐制作中,有时需要进行细微的音高调整,例如模拟老式模拟合成器的“失谐”效果或进行民族音乐的特殊音律微调。这时会用到“音分”。
- 1个半音 = 100音分。
- 因此,1音分对应的频率比是
2^(1/1200) ≈ 1.0005777895。 - 若要以音分
c来微调一个音符,其频率为:f_tuned = f * 2^(c / 1200),其中f是原公式计算出的频率。
注意:在嵌入式或高性能计算场景中,直接计算
pow(2, (n-69)/12)可能开销较大。一个常见的优化是预先计算好所有128个MIDI音符的频率,存入一个常量数组进行查找。或者,利用2^x = e^(x * ln2)进行转换,并利用硬件加速的数学库。
3. 实操应用:在代码与硬件中实现转换
理论必须付诸实践。下面我们看看如何在几种典型场景中应用这个公式。
3.1 软件实现示例(Python / C++)
Python示例:直观且适合原型验证。
C++示例:注重性能和嵌入式环境。
3.2 硬件生成中的关联(PWM、DAC)
在嵌入式系统(如Arduino, STM32, AT32)中,我们常用PWM或DAC来生成模拟电压,驱动扬声器或压电蜂鸣器发出声音。这时,计算出的频率值 f 直接决定了波形生成的定时器参数。
以PWM生成方波为例: 假设我们使用一个定时器来产生占空比为50%的方波来模拟音高。
- 计算周期:频率
f的周期T = 1 / f(秒)。 - 转换为计数:如果定时器的时钟频率为
F_clk(Hz),那么一个周期对应的定时器计数次数为ARR_Val = F_clk / f。其中ARR(Auto-Reload Register) 是定时器的重装载值。 - 设置PWM:将PWM模式设置为在计数达到
ARR_Val/2时翻转输出,即可产生50%占空比的方波。
示例:用STM32的定时器(时钟72MHz)产生中央C (261.63 Hz)。
由于 ARR 是一个整数寄存器,实际频率会有微小误差:f_actual = 72,000,000 / 275,215 ≈ 261.628 Hz,误差极小。对于更精确的控制,可以使用定时器的分频器(PSC)和 ARR 配合来获得更合适的计数值。
实操心得:在资源紧张的MCU上,频繁使用
pow或log函数计算频率是不可取的。最佳实践是:
- 使用查找表:如上文C++示例,预先计算128个频率值,或只计算常用音域的音符。
- 整数运算近似:如果需要动态计算,可以考虑使用定点数运算或近似公式来避免浮点运算。例如,利用
2^(x/12) ≈ 1 + x * 0.0577623(当x较小时的线性近似,精度较差) 或更精确的多项式近似。- 关注定时器精度:PWM频率的精度受限于定时器时钟和
ARR整数值。有时为了获得精确的频率,需要精心选择时钟源和分频系数。
3.3 与音频采样率的关系
在数字音频领域(如WAV文件、DAW工程),还有一个关键参数:采样率(如44.1kHz, 48kHz)。采样率决定了每秒采集或播放多少个音频样本。它和频率的关系体现在生成一个周期波形需要多少个样本点上。
例如,要在44.1kHz采样率下生成一个440Hz的正弦波:
- 每秒需要44100个样本点。
- 440Hz的正弦波,每个周期时长为
1/440 ≈ 0.0022727秒。 - 因此,每个周期包含的样本点数
N = 采样率 / 频率 = 44100 / 440 ≈ 100.227个点。
在编写软件合成器或处理音频缓冲区时,我们通常在一个“相位累加器”,每次增加 频率 / 采样率 * 2π 弧度,来连续生成波形样本。这里,由MIDI编号转换来的 频率 是计算这个相位增量的核心输入。
4. 扩展讨论与常见问题
掌握了基本转换后,我们来看看一些进阶话题和实际中容易踩的坑。
4.1 非标准调音与历史音高
标准公式基于A4=440Hz。但音乐实践中并非总是如此。
- 巴洛克音高:历史上,A4的音高可能低至415Hz或高到466Hz。例如,一些演奏巴洛克音乐的团体使用A4=415Hz,这比标准低约一个半音。
- 乐队调音:有些管弦乐队会将基准音调至442Hz甚至更高,以求获得更“明亮”的音色。
- 自定义调音:电子音乐中,为创造特殊氛围,可能会整体偏移或拉伸音阶。
处理方法:只需修改公式中的基准频率 a4_freq 参数即可。例如,对于A4=415Hz:
f = 415 * 2^((n - 69) / 12)
注意:当使用非标准基准音高时,MIDI编号69对应的物理频率变了,但它在十二平均律体系中的“角色”依然是A4。这可能会在与使用标准调音的乐器或音源合奏时产生音高冲突。
4.2 微音程与MIDI扩展
标准MIDI的128个编号只能表示十二平均律中的音。如何表示微分音(如四分之一音)?
- MIDI音高弯音消息:这是最常用的方法。通过发送弯音轮消息,可以在一个相当大的范围内(通常是±2个半音)连续地改变音高。其本质是在计算频率时,给
n加上一个微调量d(单位可以是半音或音分):f = 440 * 2^((n + d - 69) / 12)。 - MIDI 2.0与高精度:新的MIDI 2.0协议引入了更高的精度,允许直接指定更精细的音高。
- 软件内部处理:在DAW或合成器插件内部,完全可以使用浮点数来表示音高,突破半音的整数限制。
4.3 常见问题与排查技巧
在实际编程和调试中,你可能会遇到以下问题:
1. 声音音高不准,整体偏高或偏低
- 检查基准频率:确认你的代码中A4的基准频率是否是440.0。一个常见的笔误是写成440(整数),在强类型语言中可能导致整数除法问题。确保使用浮点数(440.0)。
- 检查MIDI编号映射:确认你输入的MIDI编号是否正确。中央C是60,A4是69。有时音序器或控制器可能发送了带通道或状态的错误数据。
- 检查硬件时钟:如果是硬件生成声音(如PWM),检查定时器的时钟配置(
F_clk)是否正确。时钟算错了,频率自然不对。
2. 计算出的频率值异常(NaN、无穷大或零)
- 输入有效性检查:在函数入口处,务必检查
note_number是否在0-127范围内,以及frequency是否大于0。对无效输入进行防御性处理。 - 数学运算安全:确保
pow或log函数的输入参数合法。例如,log(0)或log(负数)会导致错误。
3. 性能问题(在嵌入式或实时音频线程中计算太慢)
- 使用查找表:这是最有效的优化方法。对于128个音符,一个
float数组只占512字节,却可以省去所有复杂的指数运算。 - 降低精度:如果不是专业音乐应用,可以考虑使用定点数运算或低精度的近似公式来计算频率。
- 预先计算:在初始化阶段计算好所有需要的频率,避免在实时渲染循环中进行计算。
4. 与其他系统交互时的音高不一致
- 确认调音标准:确保交互的双方使用相同的A4基准频率。特别是在与外部硬件合成器、音源插件或音频文件交互时。
- 注意弯音状态:检查是否有未复位的弯音消息,导致音高被整体偏移。
- 采样率转换问题:如果涉及重采样,不正确的算法可能会引入细微的音高变化(尽管这更多影响音色和时间)。
5. 从理论到创意:应用场景展望
理解MIDI与频率的关系,远不止于实现一个转换函数。它为你打开了一扇门,让你能以程序化的方式理解和创造声音。
1. 软件合成器与音频插件开发:这是最直接的应用。你需要根据MIDI输入实时计算振荡器的频率,来生成正弦波、方波、锯齿波等基础波形。加法合成、频率调制合成等算法也深深依赖精确的频率控制。
2. 音乐可视化与音频分析:你可以将音频信号通过傅里叶变换(FFT)得到频谱,然后利用 freq_to_midi 函数将突出的频率峰值转换为最接近的音符编号,从而实现实时“扒谱”或和弦识别。
3. 嵌入式音乐项目与交互艺术:
- 用Arduino读取传感器数据(如距离、压力),映射到MIDI编号,再转换为频率驱动蜂鸣器,制作一个“空气提琴”。
- 用树莓派或STM32解析MIDI文件,通过PWM或DAC输出,自制一个硬件MIDI音源或迷你电子琴。
- 正如热词中提到的“AT32做MIDI”、“PWM+频率占空比”,这正是许多硬件爱好者正在做的事情。
4. 算法作曲与生成艺术:你可以编写程序,根据数学规则(如分形、L系统)、随机过程或数据分析结果来生成MIDI音符序列。这些序列通过频率转换,最终变成可听的音乐。这模糊了编程、数学与艺术的边界。
5. 音乐教育工具:开发辅助练习音准的App,通过麦克风采集用户演唱或演奏的声音,计算其频率并实时显示对应的音符名及与目标音高的偏差(以音分显示)。
我个人在多个软硬件音乐项目中反复运用这套转换关系。一个深刻的体会是:精度很重要,但音乐性更重要。在追求数学上精确的频率的同时,不要忘记音乐中的“律动”、“情感”和“偶然性”。有时,一点点模拟电路带来的“失谐”或数字精度限制产生的细微偏差,反而会让声音更有生命力。理解规则,是为了在必要时巧妙地打破它或运用它。希望这篇详尽的拆解,能为你未来的音乐科技项目打下坚实的基础。