1

我正在使用Juce框架来构建 VST/AU 音频插件。音频插件接受 MIDI,并将 MIDI 渲染为音频样本——通过发送 MIDI 消息由FluidSynth(一种声音合成器)处理。

这几乎可以工作了。MIDI 消息正确发送到 FluidSynth。事实上,如果音频插件告诉 FluidSynth 将 MIDI 消息直接渲染到它的音频驱动程序——使用正弦波声音字体——我们会得到一个完美的结果:

完美的正弦波,通过将音频直接发送给驱动程序

但我不应该要求 FluidSynth 直接渲染到音频驱动程序。因为这样 VST 主机将不会收到任何音频。

要正确执行此操作:我需要实现一个renderer。VST 主机每秒会询问我 (44100÷512) 次以呈现 512 个音频样本。


我尝试按需渲染音频样本块,并将其输出到 VST 主机的音频缓冲区,但这是我得到的那种波形:

渲染音频块,效果不佳

这是同一个文件,每 512 个样本(即每个音频块)都有标记:

带标记

所以,很明显我做错了什么。我没有得到连续的波形。我处理的每个音频块之间的不连续性非常明显。


这是我的代码中最重要的部分:我对 JUCE 的SynthesiserVoice.

#include "SoundfontSynthVoice.h"
#include "SoundfontSynthSound.h"

SoundfontSynthVoice::SoundfontSynthVoice(const shared_ptr<fluid_synth_t> synth)
: midiNoteNumber(0),
synth(synth)
{}

bool SoundfontSynthVoice::canPlaySound(SynthesiserSound* sound) {
    return dynamic_cast<SoundfontSynthSound*> (sound) != nullptr;
}
void SoundfontSynthVoice::startNote(int midiNoteNumber, float velocity, SynthesiserSound* /*sound*/, int /*currentPitchWheelPosition*/) {
    this->midiNoteNumber = midiNoteNumber;
    fluid_synth_noteon(synth.get(), 0, midiNoteNumber, static_cast<int>(velocity * 127));
}

void SoundfontSynthVoice::stopNote (float /*velocity*/, bool /*allowTailOff*/) {
    clearCurrentNote();
    fluid_synth_noteoff(synth.get(), 0, this->midiNoteNumber);
}

void SoundfontSynthVoice::renderNextBlock (
    AudioBuffer<float>& outputBuffer,
    int startSample,
    int numSamples
    ) {
    fluid_synth_process(
        synth.get(),    // fluid_synth_t *synth //FluidSynth instance
        numSamples,     // int len //Count of audio frames to synthesize
        1,              // int nin //ignored
        nullptr,        // float **in //ignored
        outputBuffer.getNumChannels(), // int nout //Count of arrays in 'out' 
        outputBuffer.getArrayOfWritePointers() // float **out //Array of arrays to store audio to
        );
}

这是要求合成器的每个声音产生 512 个音频样本块的地方。

这里的重要功能是SynthesiserVoice::renderNextBlock(),我要求fluid_synth_process()生成一个音频样本块。


这是告诉每个声音的代码renderNextBlock():我的AudioProcessor.

AudioProcessor::processBlock()是音频插件的主循环。在其中,Synthesiser::renderNextBlock()调用每个声音的SynthesiserVoice::renderNextBlock()

void LazarusAudioProcessor::processBlock (
    AudioBuffer<float>& buffer,
    MidiBuffer& midiMessages
    ) {
    jassert (!isUsingDoublePrecision());
    const int numSamples = buffer.getNumSamples();

    // Now pass any incoming midi messages to our keyboard state object, and let it
    // add messages to the buffer if the user is clicking on the on-screen keys
    keyboardState.processNextMidiBuffer (midiMessages, 0, numSamples, true);

    // and now get our synth to process these midi events and generate its output.
    synth.renderNextBlock (
        buffer,       // AudioBuffer<float> &outputAudio
        midiMessages, // const MidiBuffer &inputMidi
        0,            // int startSample
        numSamples    // int numSamples
        );

    // In case we have more outputs than inputs, we'll clear any output
    // channels that didn't contain input data, (because these aren't
    // guaranteed to be empty - they may contain garbage).
    for (int i = getTotalNumInputChannels(); i < getTotalNumOutputChannels(); ++i)
        buffer.clear (i, 0, numSamples);
}

我在这里有什么误解吗?让 FluidSynth 给我与前一个样本块背靠背的样本是否需要一些时间上的微妙之处?也许我需要传入一个偏移量?

也许 FluidSynth 是有状态的,并且有我需要控制的自己的时钟?

我的波形是一些众所周知的问题的症状吗?

源代码在这里,以防我遗漏了任何重要的东西。提交时发布的问题95605

4

1 回答 1

2

当我写最后一段时,我意识到:

fluid_synth_process()没有提供指定时序信息或样本偏移量的机制。然而我们观察到时间还是会提前(每个块都不一样),所以最简单的解释是:FluidSynth 实例从时间 0 开始,每次fluid_synth_process()调用都会提前 numSamples*sampleRate 秒。

这导致了一个启示:因为fluid_synth_process()对 FluidSynth 实例的计时有副作用:多个声音在同一个合成器实例上运行它是危险的

我试着减少const int numVoices = 8;const int numVoices = 1;. fluid_synth_process()所以每个块只有一个代理会调用。

这解决了问题;它产生了一个完美的波形,并揭示了不连续性的根源。

所以,我现在面临一个更简单的问题,即“在 FluidSynth 中合成多个声音的最佳方法是什么”。这是一个更好的问题。这超出了这个问题的范围,我将单独调查。谢谢你的时间!

编辑:修复了多个声音。我通过做SynthesiserVoice::renderNextBlock()一个无操作来做到这一点,然后将其fluid_synth_process()移入AudioProcessor::processBlock()——因为它应该每个调用一次(而不是每个块每个语音一次)。

于 2017-09-11T22:45:37.560 回答