|
|
在上世纪80年代,雅马哈音源芯片风靡一时,各种YM开头的芯片提供了十分丰富的声音。YM芯片现在虽然已经全面淘汰,但是其所用的FM音乐调制算法,却仍然在音乐制作领域拥有不可替代的地位,DX7、Sytrus、Serum等合成器都集成了FM算法。本文旨在介绍YM芯片的原理,对于音乐制作者、YM音源复刻者很有帮助。
一、为什么是FM?
音乐合成方法有很多。比如电子管风琴使用加法合成,MOOG合成器使用减法合成。为什么雅马哈要使用FM合成呢?简单来说,加法合成与减法合成都需要进行乘法甚至浮点数乘法才能实现,这在80年代缺乏DSP的环境下几乎无法实现。而FM合成器,只需要几次乘法就可以完成一个通道的合成,这是十分划算的。更重要的是FM合成器的音色变化及其丰富,从宁静的正弦波到富有金属质感的噪声,FM都能实现。
二、基本原理
FM合成是使用一个信号改变另一个信号的频率。假设两个信号为y1=sin(w1 * t) y2=sin(w2 * t),则FM合成可以表示为
y=sin(w1 * t + K * ∫y2 dt)
这与FM广播所使用的FM调制没有什么本质不同。信号经过FM调制后,会产生丰富的谐波分量,从载频w1 w2,到二次谐波w1+w2 w1-w2,再到三次谐波2*w1-w2、2
*w2-w1等等,形成极其多变的时域波形,这就是FM音色的来源。
三、实现
如果直接照搬公式y=sin(w1 * t + K * ∫y2 dt)进行实现,那就要实现一个正弦、两次乘法、一次积分,这显然不能承受。但是实际上我们可以进行一些化简,将FM合成算法简化许多。
1、首先要去掉积分,我们知道三角函数的积分还是三角函数,所以干脆直接去掉积分
2、然后处理两个乘法。虽然看着很唬人,但是计算机里时间是离散的,只能取特定值。我们可以定义一个变量x,来表示上一时刻的相位值
x = w1 * t + k * y2(prev)
其中y2(prev)表示上一时刻y2的值。这样,我们FM的式子就变成了这样
y = sin(x + w1 * T + k * (y2 - y2(prev)))
T是系统的采样周期,w1 * T事实上是每个周期中,相位的增量。
3、接下来要处理y1 - y1(prev),根据和差公式,我们可以直接认为
a的值可以由三角函数计算得到。
4、最后,我们把正弦值放到一个数组里,这样就不用算正弦函数了。
y = sinTable[w1 * T + ka * sinTable[w2 * t] ]
至此,我们已经去掉了绝大多数的乘法。当然,这个式子也已经不是标准的FM调制了,不过音乐合成中不是很在乎FM信号是否标准。
四、包络
不同乐器的音量特点是不一样的,对于弹奏乐器,其声音是逐渐衰减的。所以我们需要一个函数来模拟这种变化。一般使用ADSR来模拟乐器音量变化,这里不展开叙述。
五、如何使用FM合成器
无论是YM芯片,还是现代的FM合成器,都有大量的预设参数。但是制作音乐时常常要自己制作音色,此时就必须清楚参数对于音色的影响。
根据FM合成公式y = sinTable[w1 * T + ka * sinTable[w2 * t] ],我们可以调节三个参数,w1 w2与ka。ka决定了音色是宁静还是聒噪,ka为0,输出正弦波,音色非常纯净,ka越大,谐波越大,音色越噪,ka特别大时,音色甚至近似于噪声。w1 w2的比值决定了音色是否和谐,一般来说,w1与w2成整数比或简单小数比,音色比较和谐,成无理数比,则音色比较刺耳。具体效果还请读者自己探索。
六、代码
代码虽然用python写成,但是实际上用的是C语言的书写思维,并未使用python强大的数组运算功能。有经验者可以快速将代码改成C语言,布置在单片机上。
论坛会吞代码缩进,可以下载原始文件。
- import numpy as np
- from scipy.io import wavfile
- import matplotlib.pyplot as plt
- fs = 44100 # 采样率
- T = 5 # 持续时间
- f1 = 440 # 频率
- f2 = 440 * 2.5
- amplitude = 0.3 # 振幅
- #生成正弦波表
- t = np.linspace(0, 2 * np.pi, 4410, endpoint=False)
- sinTable = np.sin(t)
- out = []
- freq1 = 40 #文中w1
- phase1 = 0
- freq2 = 80 #文中w2
- phase2 = 0
- rate = 1280 #文中ka
- statu = 0
- a_vec = 100 #attack速度
- d_vec = 5 #decay速度
- d_amp = 0 #sustain阶段的声音强度
- r_vec = 100 #remain速度
- adrs_out = 0
- trig = 0 #触发声音合成
- for step in range(fs * T) :
- # op1
- osc1 = sinTable[phase1]
- phase1 += freq1
- if phase1 >= 4410 :
- phase1 -= 4410
- # op2
- osc2 = sinTable[phase2]
- phase2 += freq2 + int(osc1 * rate)
- if phase2 >= 4410 :
- phase2 -= 4410
- if phase2 < 0 :
- phase2 += 4410
- # adsr
- if statu == 0 and trig == 1 : #trig
- statu = 1
- if statu == 1 : # attack
- adrs_out += a_vec
- if adrs_out > 65535 :
- statu = 2
- adrs_out = 65535
- if trig == 0 :
- statu = 4
- if statu == 2 : # decay
- adrs_out -= d_vec
- if adrs_out < d_amp :
- statu = 3
- adrs_out = d_amp
- if trig == 0 :
- statu = 4
- if statu == 3 : # sustain
- if trig == 0 :
- statu = 4
- if statu == 4 : # remain
- adrs_out -= r_vec
- if adrs_out < 0 :
- adrs_out = 0
- statu = 1
- # trig
- if step > 10 :
- trig = 1
- if step > 30000 :
- trig = 0
- if step > 40000 :
- trig = 1
- if step > 50000 :
- trig = 0
- out.append(adrs_out / 65535 * osc2)
- plt.plot(out)
- plt.show()
- out = np.array(out) * amplitude * 65536 / 2
- out = out.astype(np.int16)
- output_path = 'sine_wave.wav'
- wavfile.write(output_path, fs, out)
复制代码
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
打赏
-
查看全部打赏
|