How do I wrap a Sampler/Synthesiser inside of an AudioSource?

Hi JUCErs,

I need to pass the rendering of my sampler to a ResamplingAudioSource (or so I think) and for that I need to wrap it in an AudioSource.

The problem is - the interface of AudioSource doesn’t provide a way to call sampler::renderNextBlock() and pass the MIDI data.

Can you suggest a better solution than this:

void PluginAudioProcessor::processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages)
{
    buffer.clear ();

    // render inside a buffer, internal to the SamplerAudioSource
    samplerAudioSource.renderSampler (
        buffer.getNumSamples () * resamplingAudioSource.getResamplingRatio (),
        midiMessages
    );

    // render the whole chain with resampling
    AudioSourceChannelInfo channelInfo (buffer);
    resamplingAudioSource.getNextAudioBlock (channelInfo);
}

Update:
Aaand it doesn’t work… I can’t anticipate the precise number of samples I need to render with the sampler - somehow, the block size is translated to more than the above bufferSize * ratio.

I am sure there is a less hackier way to do this… This can’t be the way to use AudioSource within a plugin, right?

@jules, @timur can you guys help me out?

Solved, but I believe it’s still messy - a ResamplingAudioProcessor would be nice.

PluginProcessor.cpp:

void PluginProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
    resamplingAudioSource->setResamplingRatio (
        samplerAudioSource.getOriginalSampleRate () / sampleRate
    );

    resamplingAudioSource->prepareToPlay (samplesPerBlock, sampleRate);
}

...

void PluginProcessor::processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages)
{
    buffer.clear ();

    samplerAudioSource.setBlockMidiMessages (midiMessages);

    AudioSourceChannelInfo channelInfo (buffer);
    resamplingAudioSource->getNextAudioBlock (channelInfo);
}

And in the SamplerAudioSource:

class SamplerAudioSource:    public AudioSource
{
public:
    SamplerAudioSource ()
        : audioChannelCount (DEFAULT_CHANNEL_COUNT),
          midiMessages (nullptr)
    {
        sampler.setCurrentPlaybackSampleRate (DEFAULT_SAMPLE_RATE);
    }

    ...

    void getNextAudioBlock (const AudioSourceChannelInfo& channelInfo) override
    {
        AudioSampleBuffer& outBuffer = *channelInfo.buffer;

        int samplesCount = channelInfo.numSamples,
            outBufferStartIndex = channelInfo.startSample;

        renderingBuffer.clear ();
        sampler.renderNextBlock (renderingBuffer, *midiMessages, 0, samplesCount);

        // not part of JUCE - this simply copies the renderingBuffer onto outBuffer
        AudioBufferUtils::copySamples (
            renderingBuffer,
            0,
            outBuffer,
            outBufferStartIndex,
            audioChannelCount,
            samplesCount
        );
    }

    //==============================================================================

    int getOriginalSampleRate ()
    {
        return sampler.getSampleRate ();
    }

    void setBlockMidiMessages (MidiBuffer& messages)
    {
        midiMessages = &messages;
    }

private:
    Sampler sampler;

    ...

    int audioChannelCount;
    AudioSampleBuffer renderingBuffer;
    MidiBuffer* midiMessages;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SamplerAudioSource);
};
1 Like

Your approach looks good to me and I don’t think it’s messy at all.

1 Like

@fabian, turns out this doesn’t really work as I expected it to - ResamplingAudioSource may call multiple times its input->getNextAudioBlock() even when ratio=1 and on the SamplerAudioSource side I would still pass the same midiMessageBuffer, which causes the messages to be handled multiple times.

Also, these messages should be aligned according to the DAW sampling rate, right? But I am directly passing them to the sampler without any modification. Doesn’t this mean that, if DAW sampleRate=88kHz (just for ease of calculation) and my sampler’s sampleRate=44kHz, if I get a message which has a samplePosition=10, I should translate that to a message with samplePosition=5 and THEN pass it to the sampler, right?

The first issue I could solve by clearing messages, which have already been handled (with MidiBuffer::clear (int start, int numSamples)), or maybe there is a better way?

Is there some JUCE magic I’m unaware of for problem #2?

This is a much bigger problem than I first expected…

I have no idea how many times would the ResamplingAudioSource call my SamplerAudioSource’s getNextAudioBlock (). This means that I don’t know how to divide the midiBuffer…
I can clearly see that sometimes a block is requested along with a buffer with midi messages timestamped after the block’s end, and these aren’t passed on the next call. If I knew that a particular call from ResamplingAudioSource to input->getNextAudioBlock () would be the last one with this buffer, I could pass the entire remaining sequence of messages, but there is no such way with the current ResamplingAudioSource.

That’s what I meant by “messy” @fabian - I am passing these midi messages on my own to the input AudioSource and now it turns out I need to resample the midi buffer (event positions should be invalid in the new samplingRate right?), slice that buffer in pieces AND predict how many times would the resampler call me before the next DAW processBlock call happens, so I can make sure the midiBuffer was drained completely before it’s replaced with the next block’s… What do you think I should do?

Don’t you think there needs to be a ResamplingAudioProcessor calling processBlock(AudioSampleBuffer& buffer, MidiBuffer& midiMessages) on it’s input?