音频开发实战(二)第一个乐器 正弦合成器 (附源码)
输出波形
在《音频开发技术(三)》中,我们了解到:
1.波形信号在计算机中是一串用数值表示的点。
2.信号是在 processBlock 回调方法中被获取、处理与返回。
—信号输入到插件—> | processBlock | —信号从插件输出—>
具体要怎么做呢?
WHERE: 根据以上的信息,首先确定声音是在processBlock回调方法中产生的。
HOW: 之前的课程我已经讲过,在 processBlock 中,通过 getWritePointer 接口可以获取到写入数组的指针,然后用这个指针对 得到的写入数组的内容直接赋值,就可以实现输出信号的功能。(详见《音频开发技术(三)》)
WHAT: 正弦波信号 即 正弦波形状的点集 , 我们只需要向数组写入时域上是正弦波形状的点集就可以了。
正弦波形状的点集
示例: 输出 1000 Hz 的正弦波
分析:
1.确定用正弦函数 y = sin(x) 绘制正弦波的形状。
2.1000Hz 即 每秒振动1000次。设采样频率为N,则1秒内有有N个采样点,即每个过一个点,就完成(1000/N)次振动。
3.sin(x) 中 2Pi 个单位 为一次振动。
结合以上三点,得出 每经过一个采样点就相当于 在sin(x) 的横轴上移动了 2Pi * (1000/N) 个单位。
Sin(x) 的周期为 2Pi
好了,现在我们可以编写代码了,首先要知道当前的采样率是多少。JUCE 中用 getSampleRate() 接口获取 当前采样频率。
在JUCE中获取采样频率的接口
编写代码
新建名为 MyOsc 的工程,在IDE中打开,点开 PluginProcessor.h 文件,定义要用到的double字段
1 2 3 4 5 6 | ... private: //============================================================================== double currentAngle = 0.0, volume = 0.1, frequency = -1;; // 定义需要的字段 <- JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MyOscAudioProcessor) ... |
然后来到 PluginProcessor.cpp 文件中 ,编写 processBlock 回调
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | void MyOscAudioProcessor::processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages) { ScopedNoDenormals noDenormals; auto samepleRate = getSampleRate(); auto* channelData = buffer.getWritePointer (0); auto* channelDataR = buffer.getWritePointer(1); for (auto sample = 0; sample < buffer.getNumSamples(); ++sample) { auto sample_pos = (float)std::sin(currentAngle); currentAngle += 2 * MathConstants<double>::pi * 1000 / samepleRate; channelData[sample] = sample_pos * volume; channelDataR[sample] = sample_pos * volume; } } |
以上代码实现了分析中内容。
验证
导出 -> 宿主中加载-> 成功听到了一个 1kHz 的纯音。
播放MIDI按键对应音符的频率
好了,现在我们已经完成了从频率到波形的输出,接下来实现音符到频率的转换。
获取Midi信号
processBlock回调的第二个参数,MidiBuffer中包含多个MidiMessage,而每个MidiMessage对象都相当于一个MIDI事件,每个MidiMessage都有它的事件类型(按键按下、抬起、哪一个键,力度多少等)
这里我们先用迭代器获取到MidiMessage对象,然后再通过MidiMessage对象的 getNoteNumber 接口访问到它的 NoteNumber(哪个键)。
然后又通过 MidiMessage::getMidiNoteInHertz() 接口把MIDI音符转化成频率,有了频率就可以用我们之前的代码发声了。
MidiMessage 对象的 isNoteOn() 意味着当前事件是按下按键触发的,isNoteOff() 则是代表是松开按键触发的。这里按键松开时 frequency 被赋值成 -1 以停止播放。
修改后的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | void MyOscAudioProcessor::processBlock(AudioBuffer<float>& buffer, MidiBuffer& midiMessages) { ScopedNoDenormals noDenormals; int pos; MidiMessage midi_event; for (MidiBuffer::Iterator i(midiMessages); i.getNextEvent(midi_event, pos);) { if (midi_event.isNoteOn()) { frequency = MidiMessage::getMidiNoteInHertz(midi_event.getNoteNumber()); } else if (midi_event.isNoteOff()) { frequency = -1; } } if (frequency > 0) { auto totalNumInputChannels = getTotalNumInputChannels(); auto totalNumOutputChannels = getTotalNumOutputChannels(); auto* channelData = buffer.getWritePointer(0); auto* channelDataR = buffer.getWritePointer(1); auto samepleRate = getSampleRate(); for (auto sample = 0; sample < buffer.getNumSamples(); ++sample) { auto sample_pos = (float)std::sin(currentAngle); currentAngle += 2 * MathConstants<double>::pi * frequency / samepleRate; channelData[sample] = sample_pos * volume; channelDataR[sample] = sample_pos * volume; } } } |
验证
成功了,按下按键,听到了对应的声音。
合成音色
合成器自然是指多个振荡器的合成,上方我们只写了一个振荡器,那么多个振荡器要如何合成呢。
很简单,就是每个采样点算数相加即可。
我们加一个高八度的音,高八度即频率翻倍,即 output = sin(x) + sin(2x)
只用改这一句
1 2 3 | ... auto sample_pos = (float)std::sin(currentAngle) + (float)std::sin(2 * currentAngle); ... |
验证
成功了,打开频谱仪,按下按键,看到了两个频率。
注意
插件导出前要设置成乐器才能接受到MIDI键盘的信号。
这样设置之后再导出到IDE,在生成的插件就可以接受MIDI键盘的信号了。
一个正弦波合成器就完成了。
扩展阅读
输出三角波
有这种
这种
还有这种
输出方波
加法合成
加法合成即 通过多个正弦波相加模拟某种波形。
用加法合成的三角波
用加法合成的方波
加法合成的亮点在于,它可以模拟任何一种波形。
市场中的合成器
简单的合成器 3x Osc
复杂的合成器 Massive X