Как отправить блоки аудио для обработки синтезатором без разрывов

Я использую структуру Juce для создания аудиоплагина VST/AU. Аудио-плагин принимает MIDI-данные и обрабатывает их как аудиосэмплы, отправляя MIDI-сообщения для обработки FluidSynth (синтезатор звуковых шрифтов).

Это почти работает. MIDI-сообщения отправляются в FluidSynth правильно. Действительно, если аудио-плагин сообщает FluidSynth отображать MIDI-сообщения непосредственно в аудиодрайвере — используя синусоидальный звуковой шрифт — мы достигаем идеального результата:

Идеальная синусоида за счет отправки звука непосредственно водителю

Но я не должен просить FluidSynth рендерить напрямую в аудиодрайвер. Потому что тогда хост VST не будет получать аудио.

Чтобы сделать это правильно: мне нужно реализовать рендерер. Хост 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.


person Birchlabs    schedule 11.09.2017    source источник


Ответы (1)


Когда я написал последний абзац, я понял:

fluid_synth_process() не предоставляет механизма для указания информации о времени или смещении выборки. Тем не менее, мы наблюдаем, что время тем не менее увеличивается (каждый блок отличается), поэтому самое простое объяснение таково: экземпляр FluidSynth начинается в момент времени 0 и увеличивается на numSamples*sampleRate секунд каждый раз, когда вызывается fluid_synth_process().

Это приводит к разоблачению: поскольку fluid_synth_process() оказывает побочные эффекты на синхронизацию экземпляра FluidSynth: для нескольких голосов опасно запускать это на одном и том же экземпляре синтезатора.

Я попытался уменьшить const int numVoices = 8; до const int numVoices = 1;. Таким образом, только один агент будет вызывать fluid_synth_process() для каждого блока.

Это решило проблему; он произвел идеальную форму волны и выявил источник разрыва.

Итак, у меня осталась гораздо более простая проблема: «Как лучше всего синтезировать множество голосов в FluidSynth». Это гораздо более приятная проблема. Это выходит за рамки этого вопроса, и я рассмотрю его отдельно. Спасибо за ваше время!

EDIT: исправлено несколько голосов. Я сделал это, сделав SynthesiserVoice::renderNextBlock() недействующим и переместив его fluid_synth_process() в AudioProcessor::processBlock(), потому что он должен вызываться один раз для каждого блока (а не один раз для каждого голоса в каждом блоке).

person Birchlabs    schedule 11.09.2017
comment
Хорошая работа. :) На самом деле, немного удивительно, что FluidSynth такой тяжелый апатрид. Выполняет ли он большую часть фильтрации с задержками? - person bipll; 12.09.2017
comment
@bipll спасибо. :) Я не уверен, что понимаю ваш вопрос. Вы спрашиваете, может ли FluidSynth воспроизводить звук достаточно быстро, чтобы заполнить буфер? конечно, он может отображать 512 выборок 44100 раз в секунду; звук звучит отлично. или вы спрашиваете, может ли он применять фильтры достаточно быстро, чтобы заполнить буфер? Я еще не пробовал хорус и реверберацию. или вы спрашиваете, есть ли у него фильтр задержки? Я думаю, что нет. вы спрашиваете, является ли синтез с низкой задержкой? Я думаю, единственная задержка, которая имеет значение, это то, как быстро он может обрабатывать midiNoteOn. Однако я не измерял эту задержку. - person Birchlabs; 12.09.2017
comment
s/stateless/stateful/ ›_‹. Мне просто любопытно, что Fluid_synth_process даже не имеет аргумента startSample и просто возвращает волновые данные последовательно, блок за блоком. Вероятно, это как-то связано с его внутренними фильтрами, использующими задержки. - person bipll; 13.09.2017