【科普】音频开发与音乐制作【第七篇-数字合成器】

本篇将会讲解数字合成器的基本原理,并基于JUCE开发一个简单的数字合成器。

本篇的重点是合成器的原理而不是代码具体如何编写,所以一部分代码细节将会省略。

JUCE框架

编程领域有句话说得好,做项目的时候不要重复造轮子,尽量把精力放到有用的地方。例如对于音频处理,我们希望关注的是具体处理音频的算法,而不是那一大堆和声卡、宿主软件交互的api。

JUCE(Jules’ Utility Class Extensions)框架提供了一系列在音频处理上非常有用的库,可以很方便地编写音频相关应用、插件并打包发布到不同平台。JUCE可以在其官方网站下载。IDE推荐使用visual studio,框架使用可以查询其官方教程或者这个很不错的中文教程

安装JUCE并创建一个音频插件项目(需要在设置-Plugin format中勾选vst3选项),点击上方按钮使用Visual Studio 2019打开。

计算机处理音频时是逐块处理的。虚拟声卡设置中可以更改缓冲区大小。缓冲区越小音频的延迟就越低,但相应的对性能要求更高。

C++中,每个类分为.h文件(类的定义)和.cpp文件(类的实现)。在PluginProcessor.h我们可以定义一些有用的类成员属性,以供后面使用:

class SynthYiAudioProcessor  : public juce::AudioProcessor
{
public:
    //...
    //波形生成相关参数
    int positionStamp = 0;
    double sampleRate;
    //音符列表
    NoteEvent notelist[129];
    //...
}

在Visual Studio打开的项目中找到PluginProcessor.cpp的processBlock函数。在插件运行时,程序将会不断调用此函数来逐块处理音频。我们将在这里编写音频处理代码。

//SynthYi为项目名
void SynthYiAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
    //在这里编写音频处理代码
    sampleRate = getSampleRate();//采样率
}

MIDI

MIDI(Musical Instrument Digital Interface),或乐器数字接口,是一种能被计算机理解的乐谱格式。Midi格式由一系列消息组成,记录了诸如按键按下、按键松开、踏板踩下、移调轮转动、力度改变等事件。在编曲宿主软件中的写的音符也将以MIDI的形式传输给虚拟乐器插件。

MIDI用128个id对应了钢琴上的128个键。JUCE提供了相关的工具类库来将MIDI id转换为对应的音高频率。在processBlock函数中MIDI事件列表作为函数参数传入,可以使用for循环来迭代当前Block中发生的midi事件:

int pos;
juce::MidiMessage midi_event;
for (juce::MidiBuffer::Iterator i(midiMessages); i.getNextEvent(midi_event, pos);) {
    if (midi_event.isNoteOn()) {
        //有音符被按下了。
    }
}

振荡器(Oscillator)

振荡器是合成器的核心。振荡器负责产生特定频率的波形信号,一般是方波、正弦波、三角波等基本波形。模拟振荡器一般由RC震荡电路、LC震荡电路、石英震荡器等电路组成。数字振荡器比较灵活,一般使用一个函数来计算每个采样点的采样值。

例如下面的公式可以生成一个频率为$\omega$正弦波:

$Y_n=\sin (2\pi \omega x)$

锯齿波:

$\arctan\left(\tan\pi \omega x\right)$

或者干脆用傅里叶级数来表示。这是傅里叶级数形式的锯齿波:

$f\left(x\right)=\sum_{n=1}^{\infty}\frac{\sin nx}{n}$

傅里叶级数形式的方波:

$f\left(x\right)=\sum_{n=1}^{\infty}\frac{\sin\left(\left(2n-1\right)x\right)}{\left(2n-1\right)}$

这里的$\infty$是理想情况,合成的时候可以根据需要选择适当的项数。多数时候50级正弦波相加听起来就已经不错了。这里有一些傅里叶级数模拟出的波形图像:

但这两种合成方法都有很大缺陷。第一种方法生成波形时需要针对每个波形都编写一个函数,灵活性很低,而且并不是任意波形都可以用简单的数学函数来表示;第二种方法比第一种更灵活,因为几乎所有周期波形都可以被分解为傅里叶级数。缺陷在于合成器只能计算有限个波形,常常造成高频泛音的缺失。并且波形多了以后一次性需要计算上百个正弦波,对性能要求太高了。

商业合成器软件,如Serum血清合成器,一般多采用波表(wavetable)合成,即预先存储波形单个周期的采样,在合成时根据频率将采样拉伸并不断重复。Serum的波形预设库里有很多wav波形文件,在软件里打开其中一个:

可以看到每个波形都被记录了2048个采样点(注意右下角以采样为单位的时间显示)。

再换一个:

这样的波形是很难用函数表达的,但在波表合成器中,只需要存储波形采样即可。

群奏(Unison)

单个波听起来有时有些单薄。在数字合成器中,常会有群奏效果,即让几个频率相近的振荡器同时发声。

下面为一段群奏的试听。乐曲前半部分为单个锯齿波,后半部分为六个锯齿波齐奏。

波封(Envelope)

如果按下就发出声音,松开声音立刻就停止,这样直来直去的音乐听起来多平淡啊!

波封(Envelope)赋予合成器声音以音量上的动态。最为人熟知的波封形式是ADSR,即启动(Attack),衰减(Decay),延留(Sustain)和释放(Release),分别对应从按下按键到合成器到达最大音量的时间,合成器到达最大音量后音量衰减的时间,到达最大音量后音量衰减多少,以及松开按键后合成器声音消失的时间。一部分合成器如Serum还有保持(Hold)时间的选项,即合成器在最大音量停留多长时间。

听感上来说,启动时间越短,声音听起来越有冲击感,越有力。释放时间越长,音符的余音就越长。

波封作用于波形的振幅。这是一个波形的响度-时间图:

波封函数$f_{Env}(t)$描述了波形相对于时间的变换。对生成的波形应用波封:

$Y_t=X_t*f_{Env}(t)$

波封函数的一种代码实现,仅供参考:

double NoteEvent::EnvAHDSR(int currentStamp)//获取当前的相对振幅
{
	int step = currentStamp - noteStart;
	if (step <= 0) {//before
		releaseFrom = 0.0;
		return 0.0;
	}
	else if (currentStamp > noteReleaseTime && currentStamp < noteReleaseTime + release && noteReleaseTime != -2) {
		//release
		int releaseStep = noteReleaseTime + release - currentStamp;
		return releaseFrom*(powCurve((double)releaseStep / (double)release));
	}
	else if (step < attack && step > 0) {//attack
		releaseFrom= powCurve((double)step / (double)attack);
		return releaseFrom;
	}
	else if (step <= attack + hold) {//hold
		releaseFrom = 1.0;
		return 1.0;
	}
	else if (step <= attack + hold + decay) {//decay
		double nowPercentage = powCurve(1 - ((double)(step - attack - hold) / (double)decay));
		releaseFrom= (1.0 - sustain) * nowPercentage + sustain;
		return releaseFrom;
	}
	else if (noteReleaseTime == -2 || currentStamp < noteReleaseTime) {//sustain
		releaseFrom = sustain;
		return sustain;
	}
	
	return 0.0;//after
}
double NoteEvent::powCurve(double x)//响度一般是指数变化的
{
	return std::pow(21.0, x) / 20.0 - 0.05;
}

滤波器(Filter)

合成器使用的滤波器多是动态滤波器。滤波器的生效幅度和截止频率随时间动态变化,可以创造出很有趣的听感。例如,将滤波器的截止频率与波封挂钩时,可以创造出弹跳感很强的Pluck音效。下面有一段试听,使用了截止频率动态的巴特沃夫滤波器:

低频振荡器(LFO)

低频振荡器(Low-Frequency Oscillation)输出一个频率较低的周期性控制信号。该控制信号可以控制合成器的输出音量,滤波器截止频率等很多东西。甚至,在数字滤波器上,还可以控制合成所用采样的序号,即LFO输出值不同时,合成的波形也不一样。

下面有一段试听,其中合成器合成的波形受LFO输出信号控制:

低频振荡器的波形产生原理和产生声音波形的振荡器完全一致。

效果器(Fx)

在上一篇中,我们实现了很多种效果器。它们也可以放在合成器上,作用于合成的音频。

下面这段试听音频中的旋律由加了失真效果器的正弦波演奏。正弦波被削波失真后,产生了一些类似方波的泛音,音色变得更明亮。

插件的导出和使用

在这里,我使用代码实现了一个简单的基于傅里叶级数的加法合成器。如果你要自己开发合成器的话尽量用波表的方式,因为我这个合成器运行时CPU占用能够到达50%以上……

关于JUCE的UI开发,可以参照JUCE的官方教程和z新豪的教程。因为内容实在是太琐碎这里真的放不下了。

在Visual Studio中编写完代码后,选择生成-生成解决方案,在在输出文件夹中找到.vst后缀的vst插件,拷贝进C:\Program Files\Common Files\VST3目录中:

打开DAW软件,加载插件即可使用。注意,要想让插件被识别为虚拟乐器,需要在JUCE中勾选Plugin is a Synth选项并开启MIDI输入。

下面是一段简单的试听(旋律来自《SandStorm》,启用波封,五个锯齿波群奏,未使用滤波器):

暂无评论

发送评论 编辑评论


|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
洛天依九周年
颜文字
Emoji
小恐龙
花!
上一篇
下一篇