Simple sampler example

When you implement your SynthesiserVoice, you override renderNextBlock(), which is where you would apply the gain. You have a pointer to the currently playing SynthesiserSound, where you would store the gain setting.
You might have to use dynamic_cast to down cast the sound to your actual sound in order to access the additional gain setting.

I’ve implemented my SynthesiserVoice with renderNextBlock(). Now I can change volume, panning, etc for each note.
But still can’t understand how can I change volume of individual sound. I understand, that it should be something like this
auto sound1 = dynamic_cast<const SynthesiserSound*> (sound);
but where should I use it and how can I change gain of this sound?

Just to remind. I have 4 different samples for each note. At this moment I use 18 notes. 4 samples for each (72 samples).

Thank you.

Each voice has a renderNextBlock(), that you override to mix the generated sound to the output. Since it is mixing here, you need the information about the desired gain here, after mixing you have no longer access to the individual voice’s signal.

The sound the voice is playing bears the information of your desired gain (which is a method you added, hence the cast you quoted). It could work along these lines:

void SynthesiserVoice::renderNextBlock (AudioBuffer<float>& outputBuffer,
                                        int startSample, int numSamples)
{
    auto gain = 1.0f;
    if (auto* sound = dynamic_cast<const MySynthesiserSound*> (getCurrentlyPlayingSound().get()))
        gain = sound->getGain();

    // generate your signal and use gain as multiplier
    for (int c = 0; c < outputBuffer.getNumChannels(); ++c)
        outputBuffer.addFromWithRamp (..., lastGain, gain);

    lastGain = gain;  // lastGain should be a member to have smooth gain changes
}

class MySynthesiserSound : public SynthesiserSound
{
public:
    // ...
    float getGain() const  { return gain.load(); }
    void setGain (float g) { gain.store (g); }
private:
    // ...
    std::atomic<float> gain = 1.0f;
}

Hope that helps

EDIT: you might need to make gain atomic, I added that just in case it is set from a non-audio thread

EDIT 2: you might want to set lastGain in noteOn, otherwise you get unexpected gain ramps from the previous played sound

1 Like

Thank you! Will try.

What is a const Type* source in this method?

AudioBuffer is a template, and Type is the type of the samples, i.e. either float or double.

AudioSampleBuffer is actually AudioBuffer<float>. I.e. in your case Type* is float*, and you get it via buffer.getReadPointer (channel).

1 Like

If I use my own MySynthesiserSound class, what is a solution to set samples?
Previously I made something like this:

synth.addSound(new SamplerSound(“name1”, *reader1, notes, 30, 0, 10, 10.0));
synth.addSound(new SamplerSound(“name2”, *reader2, notes, 30, 0, 10, 10.0));
synth.addSound(new SamplerSound(“name3”, *reader3, notes, 30, 0, 10, 10.0));
synth.addSound(new SamplerSound(“name4”, *reader4, notes, 30, 0, 10, 10.0));

But it’s not an option now, right?

Assuming the constructor for your sound class is the same as SamplerSound has :

Of course you have to implement all the stuff Juce’s SamplerSound does in your own sound class besides your custom things. If I recall right, it’s not necessarily going to be useful to inherit from SamplerSound, you may need to inherit SynthesiserSound and implement all the functionality from scratch. If the inheritance from SamplerSound doesn’t work out, you could copy paste the code from Juce’s SamplerSound, I guess…

1 Like

Looks like the Juce SamplerSound can be inherited from and you can pretty easily add the desired per-sound gain property :

class MySamplerSound : public SamplerSound
{
public:
	MySamplerSound(const String& name,
		AudioFormatReader& source,
		const BigInteger& midiNotes,
		int midiNoteForNormalPitch,
		double attackTimeSecs,
		double releaseTimeSecs,
		double maxSampleLengthSeconds, float initialGain) :
		SamplerSound(name, source, midiNotes, midiNoteForNormalPitch, attackTimeSecs, releaseTimeSecs, maxSampleLengthSeconds),
		soundGain(initialGain)
	{
	}
	float getGain() const { return soundGain; }
	void setGain(float g) { soundGain = g; }
private:
	float soundGain = 1.0f;
};

The soundGain member variable isn’t atomic or protected with a mutex here. It probably should be, though, if it is going to be changed during audio playback.

Yes. It works like this. I’ve already done it. Thank you.

Maybe you can give me an advice or example what is the good way to implement renderNextBlock
now it’s like an endless loop with the code:

void renderNextBlock(AudioBuffer<float>& outputBuffer, int startSample, int numSamples) override
{
	if (auto* playingSound = static_cast<SynthSound*> (getCurrentlyPlayingSound().get()))
	{

		auto& data = *playingSound->data;

		for (int c = 0; c < data.getNumChannels(); ++c) {
			outputBuffer.addFromWithRamp(c, startSample, data.getReadPointer(c), numSamples, lastGain, gain);
		}

}

I believe it should be something with numSamples?

You need to advance the read position into your source sample audio buffer at each renderNextBlock call. data.getReadPointer(c) just gets a pointer to the beginning of the source sample data at each render call.

Add a position variable as a member of your synth voice class and increment it in renderNextBlock as needed. (In your particular code, I guess by numSamples). You need to be very careful you don’t attempt reading past the end of the source sample buffer, it will lead to a crash or worse. (Note that addFromWithRamp doesn’t take care of that for you.)

1 Like

Thank you. Now it works.

But have another question:)

Each note has a mix of the four samples, but it could be different velocity with another four samples.
What is the best way to load all samples for all velocities and then select it during the playback?

My idea is to create SynthesiserSounds for each velocity range and then select required in renderNextBlock. Is it a good option?

Would you be willing to share your source? Am working on a similar problem.

2 Likes