如何发送合成器处理的音频块 - 没有不连续性

时间:2017-09-11 22:28:45

标签: vst juce synthesizer sound-synthesis fluidsynth

我正在使用Juce框架来构建VST / AU音频插件。音频插件接受MIDI,并将MIDI作为音频样本呈现 - 通过发送由FluidSynth(声音合成器)处理的MIDI信息。

这几乎正常。 MIDI信息正确发送到FluidSynth。实际上,如果音频插件告诉FluidSynth将MIDI信息直接渲染到其音频驱动程序 - 使用正弦波声音 - 我们可以获得完美的结果:

Perfect sine wave, by sending audio direct to driver

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

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

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

Rendering blocks of audio, poorly

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

with markers

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

这是我的代码中最重要的部分:我对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是有状态的,并且有自己的时钟,我需要控制它?

我的波形是否有一些众所周知的问题?

源代码是here,以防我遗漏任何重要内容。提交时发布的问题95605

1 个答案:

答案 0 :(得分:1)

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

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

这导致了一个启示:因为const int numVoices = 8;对FluidSynth实例的时间有副作用: 多个声音在同一个合成实例上运行< / em>的

我尝试将const int numVoices = 1;缩减为fluid_synth_process()。因此,每个块只有一个代理会调用SynthesiserVoice::renderNextBlock()

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

所以,我现在离开了一个更容易的问题“什么是在FluidSynth中合成多个声音的最佳方法”。这是一个更好的问题。这超出了这个问题的范围,我将分别进行调查。谢谢你的时间!

编辑:修复了多个声音。我这样做是为了让fluid_synth_process()成为无操作,并将其AudioProcessor::processBlock()移动到{{1}} - 因为它应该每调用一次(不是每次一次)每块语音)。