Synthesiser类群详解 3-3

事实上,开发采样器程序时,很少直接操作SynthesiserSound和SynthesiserVoice这两个类,大多数时候,使用Synthesiser合成器类添加它们的对象即可完成任务。

Synthesiser相当于多个SynthesiserVoice和多个SynthesiserSound的容器,即:本类持有两个数组,一个用来保存SynthesiserVoice类的指针(堆对象),一个用来保存SynthesiserSound类的指针(堆对象)。内部使这两个类协同配合,产生本类所需的音频数据和相关信息。

Synthesiser是一个独立的功能类,既无基类,也无子类(但该类有虚函数,析构函数也为虚。因此,意味着可派生其子类,当然也可直接使用)。该类的主要作用是:接收MIDI消息,产生对应的音频数据,这些音频数据以AudioSource的固有方式发送出去,即:AudioSource的数据来源为本类,经过内部处理和转换后,以音频流的形式发送出去,供程序中的其他模块使用。在此期间,Synthesiser类可对所接收的MIDI消息进行各种控制。

通常,在自定义的AudioSource类中组合本类对象,该类的getNextAudioBlock()函数中,将本类对象所持有的音频数据发送到该函数的参数中(执行的结果是:AudioSource依据本类所持有的数据产生连续不断的音频流)。示例如下。

1、自定义AudioSource类,用于产生对应MIDI事件的音频流(MIDI发声):

class SamplerSource : public AudioSource
{
public:      
    SamplerSource ();

    // 实现基类的3个纯虚函数
    void prepareToPlay (int samplesPerBlock, double sampleRate);    
    void getNextAudioBlock (const AudioSourceChannelInfo& buffers);
    void releaseResources() {}  // 空继承

    // public数据成员,用于接收MIDI信息
    MidiMessageCollector midiCollector; 

private:
    Synthesiser synth;	// 栈对象

    // 下面两个对象用于读取音频数据
    AudioFormatManager formatManager;
    ScopedPointer reader;
};

2、构造函数中,Synthesiser对象添加声部(确定合成器的最大复音数)和音频采样:

SamplerSource::SamplerSource ()
{
    // 注册本类所能识别的音频格式
    formatManager.registerBasicFormats();

    // 合成器添加16个声部对象,即:本合成器最大16复音
    for (int i = 0; i < 16; ++i)	
        synth.addVoice (new SamplerVoice()); 
    
    File audioFile ("../soundBank/flute/C3-D3.wav");

    // 音频文件中的数据“转交”给AudioFormatReader
    reader = formatManager.createReaderFor (audioFile);

    if (reader != nullptr)
    {
        // 定义BigInteger对象,该对象的位数代表能处理的MIDI音符数
        BigInteger notes;

        /* 1参开始,之后的2参个位数设置为1,即本函数所添加的录音采样响应该范围内的MIDI音符。如果2参为1,则等于从1参开始,只设置1位。此时,就相当于录音采样仅对应一个MIDI音符 */
        notes.setRange (12, 24, true); 

        // 创建SampleSound对象,该对象代表reader所读取的音频数据,为合成器对象所用
        SynthesiserSound::Ptr sound = new SamplerSound ("sampleSound", 
                                *reader, notes, 72, 0.0, 0.1, 60.0);

        synth.addSound (sound);		// 合成器对象添加一个SampleSound

        /* 可重复File audioFile之后的语句,添加多个录音采样,并设置所添加的每个采样所对应的音符(音符范围)。此时需更改reader所指向的音频文件,可利用File类的有关函数将某个目录下的音频文件用循环的方式全部添加到合成器对象中,每个文件的音频数据可对应1或少数几个MIDI音符. */
    }    
}

3、实现基类的3个纯虚函数(releaseResources()函数是空继承,见类定义):

// 准备产生音频,Synthesiser设置采样率
void SamplerSource::prepareToPlay (int, double sampleRate) 
{
    midiCollector.reset (sampleRate);
    synth.setCurrentPlaybackSampleRate (sampleRate);
}

// 产生音频流。结果保存到参数buffers中。本函数将由调用方不间断的重复调用
void SamplerSource::getNextAudioBlock (const AudioSourceChannelInfo& buffers) 
{
    buffers.clearActiveBufferRegion();	// 清空缓冲
    MidiBuffer incomingMidi;	// 临时定义MIDI缓冲

    // 将采集到的MIDI信息发送到MIDI缓冲中
    midiCollector.removeNextBlockOfMessages (incomingMidi, 
                                             buffers.numSamples);

    /* 合成器对象“渲染”2参MIDI缓冲中的数据所对应的音频,结果发送到1参,从3参第0个采样开始,渲染个数为4参。执行此句时,synth已经持有了音频数据(比如单个MIDI音符的音频采样) */
    synth.renderNextBlock (*buffers.buffer, incomingMidi, 
                           0, buffers.numSamples);
}

仅仅调用了Synthesiser的4个成员函数即完成了一个软件采样器的最核心类,方便简单!

Synthesiser类的主要成员函数及使用技巧(网站发布,略)