2017-09-11 6 views
1

J'utilise le framework Juce pour créer un plug-in audio VST/AU. Le plugin audio accepte le MIDI, et rend ce MIDI comme échantillons audio - en envoyant les messages MIDI à traiter par FluidSynth (un synthétiseur soundfont).Comment envoyer des blocs d'audio à traiter par synthétiseur - sans discontinuités

Cela fonctionne presque. Les messages MIDI sont envoyés à FluidSynth correctement. En effet, si le plug-in audio indique FluidSynth pour rendre les messages MIDI directement à son pilote audio - en utilisant une soundfont onde sinusoïdale - nous obtenons un résultat parfait:

Perfect sine wave, by sending audio direct to driver

Mais je ne devrais pas demander FluidSynth rendre directement à le pilote audio. Car alors l'hôte VST ne recevra aucun son.

Pour ce faire correctement: J'ai besoin de mettre en œuvre un renderer. L'hôte VST me demandera (44100 ÷ 512) fois par seconde de rendre 512 échantillons d'audio.


J'ai essayé de rendu des blocs d'échantillons audio à la demande, et la sortie ceux du tampon audio hôte VST, mais c'est le genre de forme d'onde que je suis:

Rendering blocks of audio, poorly

Voici la même fichier, avec des marqueurs tous les 512 échantillons (soit tous les blocs de données audio):

with markers

Donc, clairement, je fais quelque chose de mal. Je ne reçois pas une forme d'onde continue. Les discontinuités sont très visibles entre chaque bloc d'audio que je traite.


est ici la partie la plus importante de mon code: ma mise en œuvre de JUCE de 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 
     ); 
} 

C'est ici que l'on demande à chaque voix du synthétiseur de produire le bloc de 512 échantillons audio.

La fonction importante ici est SynthesiserVoice::renderNextBlock(), dans laquelle je demande fluid_synth_process() pour produire un bloc d'échantillons audio.


Et voici le code qui indique toutes les voix à renderNextBlock(): ma mise en œuvre de AudioProcessor.

AudioProcessor::processBlock() est la boucle principale du plug-in audio. En son sein, Synthesiser::renderNextBlock() invoque SynthesiserVoice::renderNextBlock() de toutes les voix:

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); 
} 

Y at-il quelque chose que je suis malentendu ici? Y a-t-il une certaine subtilité de synchronisation nécessaire pour que FluidSynth me donne des échantillons qui sont dos à dos avec le bloc d'échantillons précédent? Peut-être un décalage que je dois passer?

Peut-être que FluidSynth est à état, et a sa propre horloge dont j'ai besoin de prendre le contrôle de?

Ma forme d'onde est-elle symptomatique d'un problème bien connu?

Le code source est here, dans le cas où j'ai omis quelque chose d'important. Question publiée au moment de la validation 95605.

Répondre

1

Comme je l'ai écrit le dernier paragraphe, je réalise:

fluid_synth_process() fournit pas de mécanisme permettant de spécifier des informations de synchronisation ou de l'échantillon de décalage. Pourtant, nous observons que le temps avance néanmoins (chaque bloc est différent), donc l'explication la plus simple est: l'instance FluidSynth commence à l'instant 0, et avance par numSamples * sampleRate secondes chaque fois que fluid_synth_process() est invoqué.

Cela conduit à la révélation: depuis fluid_synth_process() a des effets secondaires sur le calendrier de l'instance FluidSynth: il est dangereux pour plusieurs voix pour exécuter ce sur la même instance de synthé. J'ai essayé de réduire const int numVoices = 8; à const int numVoices = 1;. Donc, un seul agent invoquerait fluid_synth_process() par bloc.

Cela a résolu le problème; il a produit une forme d'onde parfaite, et a révélé la source de la discontinuité. Donc, il me reste maintenant un problème beaucoup plus facile de "quelle est la meilleure façon de synthétiser une pluralité de voix dans FluidSynth". C'est un problème beaucoup plus agréable à avoir. C'est en dehors de la portée de cette question, et je vais l'étudier séparément. Merci pour votre temps!

EDIT: corrigé les voix multiples. Je l'ai fait en faisant SynthesiserVoice::renderNextBlock() un no-op, et en déplaçant son fluid_synth_process() dans AudioProcessor::processBlock() à la place - parce qu'il devrait être invoqué une fois par bloquer (pas une fois par voix par bloc).

+0

Beau travail. :) En fait, c'est un peu surprenant que FluidSynth soit ce lourd apatride. Est-ce qu'il effectue beaucoup de filtrage avec des retards? – bipll

+0

@bipll merci. :) Je ne suis pas sûr de comprendre votre question. Demandez-vous si fluidsynth peut rendre l'audio assez rapidement pour remplir le tampon? certainement, il peut rendre 512 échantillons 44100 fois par seconde; l'audio sonne bien. ou demandez-vous s'il peut appliquer des filtres assez rapidement pour remplir le tampon? Je n'ai pas encore essayé le chorus et la réverbération. ou demandez-vous s'il a un filtre "retard"? Je pense que non. demandez-vous si la synthèse est à faible latence? Je suppose que la seule latence qui compte est "à quelle vitesse peut-il traiter midiNoteOn". Cependant, je n'ai pas mesuré cette latence. – Birchlabs

+0

's/stateless/stateful /'> _ <. Je suis simplement curieux que fluid_synth_process n'ait même pas d'argument startSample et renvoie simplement les wavedata séquentiellement, block by block. Cela a probablement quelque chose à voir avec ses filtres internes qui utilisent des retards. – bipll