Juce Synthesiser Problem: popping sound accompanies midi on/off messages


#1

I’m having a problem correctly implementing a Juce Synthesiser plug-in.

I can get my synth to function how I want it to, producing sin waves in response to midi-on messages and allowing the sound to tail off after the note is released. However, I noticed that, when releasing the midi key, there is often a “pop” in the audio output.

I have spent a few days trying to figure this out. To simplify things for troubleshooting, I started a brand new plug-in in the projucer, and made the most basic SynthesiserVoice and SynthesiserSound subclasses possible. The sound automatically plays on all channels and notes. The voice’s startNote and stopNote functions are empty. The voice “renderNextBlock” function returns “0.25” for all samples of all channels.

When I open the plugin in the Juce host, there is an expected pop as the speaker adjusts to the 0.25 position. And when I start pressing midi keys, there is a click for each on and off message. If i adjust the “renderNextBlock” function to return 0.0 instead of 0.25, there is no click when pressing keys.

The “renderNextBlock” function should be filling the buffer with a consistent value. It seems like midi on-off messages are zeroing some of the buffer outside the “renderNextBlock” function. I don’t know how to debug where/how this is happening.

Any help would be much appreciated. I feel like the answer must be incredibly simple, but I just can’t find it.


#2

Are you sure that you’re rendering only from the startSample upto numSamples in the renderNextBlock() callback? A common cause of clicks is to assume startSample is always zero.


#3

Thanks for the quick response!

I do set startSample to zero. However, I’m working on an audio plugin, as opposed to an audio application. The audio processing function “processBlock” in an audio plugin gets passed an AudioSampleBuffer which does not have a “startSample” member. From every Juce plugin example I can find online, processing begins at index 0 of the buffer.

Is there a way to determine the startsample for an AudioSampleBuffer in this context other than zero?


#4

You both are talking of different things:
Yes, the AudioProcessor::processBlock() method expects the whole buffer to pe processed, i.e. either copy samples into or clearing it.

When you call Synthesiser:::renderNextBlock(), I would say, calling from 0 to buffer.getNumSamples() should be correct, because, the midi buffer is responsible to determine the offsets.

Where @martinrobinson’s note applies is the SynthesiserVoice::renderNextBlock(), where you have to make sure, you only render into the window determined by startSample and numSamples.

I hope that makes sense…

Maybe if you don’t mind sharing your AudioProcessor::processBlock() and your SynthesiserVoiice::renderNextBlock() implementation? (maybe simplified, if you are worried about IP details…)


#5

@daniel

That makes sense. And here’s all code that might be relevant. There’s not much IP at this point :grinning: As you can see, I haven’t changed much from what the projucer generates automatically.

SynthSound Subclass:

class SynthSound : public SynthesiserSound
{
public:
	bool appliesToNote(int midiNoteNumber) {
		return true;
	}
	bool appliesToChannel(int midiChannel) {
		return true;
	}
private:
};

And the SynthVoice SubClass:

class SynthVoice : public SynthesiserVoice
{
public:

	bool canPlaySound(SynthesiserSound* sound) {
		return dynamic_cast<SynthSound*>(sound) != nullptr;
	}

	void startNote(int midiNoteNumber, float velocity, SynthesiserSound* sound, int currentPitchWheelPosition) {
	}

	void renderNextBlock(AudioBuffer<float>& outputBuffer, int startSample, int numSamples) {
		float currentSample;
		float* channelData0 = outputBuffer.getWritePointer(0);
		float* channelData1 = outputBuffer.getWritePointer(1);
		for (int sample = startSample; sample < numSamples; ++sample) {
			currentSample = 0.25f;
			channelData0[sample] += currentSample;
			channelData1[sample] += currentSample;
		}
	}

	void stopNote(float velocity, bool allowTailOff) {
		clearCurrentNote();
	}
	
	void pitchWheelMoved(int newPitchWheelValue) {
	}

	void controllerMoved(int controllerNumber, int newControllerValue) {
	}
};

And the parts of PluginProcessor.cpp that I changed from what Juce creates automatically:

SynthTestAudioProcessor::SynthTestAudioProcessor()
#ifndef JucePlugin_PreferredChannelConfigurations
     : AudioProcessor (BusesProperties()
                     #if ! JucePlugin_IsMidiEffect
                      #if ! JucePlugin_IsSynth
                       .withInput  ("Input",  AudioChannelSet::stereo(), true)
                      #endif
                       .withOutput ("Output", AudioChannelSet::stereo(), true)
                     #endif
                       )
#endif
{
	synth.clearVoices();
	for (int voice = 0; voice < 3; voice++) {
		synth.addVoice(new SynthVoice);
	}
	synth.clearSounds();
	synth.addSound(new SynthSound);
}

void SynthTestAudioProcessor::processBlock (AudioSampleBuffer& buffer, MidiBuffer& midiMessages)
{
	buffer.clear();
	synth.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples());
}

Thank you so much!


#6

Your problem is there!

Let’s say you have a non-zero startSample. For this example, make it 200.
Let’s then also say numSamples is 1000.

Your for loop will then only run from 200 to 1000, or 800 samples. This leaves out another 200 end samples.

This actually threw me off the first time I wrote a synth in JUCE, since it’s different from how the audio effect callback works.


#7

@trickyflemming
I see what you mean, but I don’t think that could be the problem here. When I call SynthesiserVoice::renderNextBlock from AudioProcessor::processBlock, I pass a hard-coded “0” in as the startSample value. So startSample can never be anything other than 0.


#8

Why would you NOT expect to hear a click if you slam the level between 0.25 and 0?

There’s already a minimal sine-wave synth example in the demo - why not start with that and learn from there?


#9

This is probably my own lack of knowledge or experience but im having a similar problem with my project. Everything works fine in one DAW (FL) but in another like Live or reaper i get these glitches that look like an interruption in processblock when midi note on and off messages are received. Also moving mod wheel or pitch wheel is causing lots of glitches in these daws (same asio driver in all hosts).
For example when i even remove everything and set a constant value like 0.5 for the buffer (inside processblock called by renderNextBlock). the output is constant 0.5 but with two short moments that the output gets 0.0 and it happens at the moment midi on/off messages are recieved.
I’ve already studied the examples and doing a similar thing. I didn’t notice any problems until i tried another host. Can you guys point me to the direction that might cause something like this?


#10

Can you show us your code so it’ll make it easier for us to see what you mean?


#11

As Joshua says post your code. But to me that sounds like a classic case of not respecting the startSample value and always rendering from the first sample in the block (as per my post above). MIDI messages arrive at offsets within a block. But post your code!


#12

This is basically what i have:

 void SynthVoice::processBlock(AudioBuffer<FloatType>& outputBuffer, int startSample, int numSamples)
    {	
    	if (Envelope.State > 0)
    	{		
    		for (int SN = startSample; SN < numSamples; SN++)
    		{
    			...
    			float EnvVelo = Envelope.GetValue(); //release done -> state = 0
    			outputBuffer.setSample(0, SN, (OscillatorOutputL*EnvVelo));
    			outputBuffer.setSample(1, SN, (OscillatorOutputR*EnvVelo));
    		}
    	}
    	if (Envelope.State == 0)
    	{
    		clearCurrentNote();
    	}
    }

    void SynthVoice::startNote(int midiNoteNumber, float velocity, SynthesiserSound* /*sound*/, int /*currentPitchWheelPosition*/)
    {
    	Freq = ...;
    	Envelope.setGate(1); //state = 1 & attack
    }
    void SynthVoice::stopNote(float /*velocity*/, bool allowTailOff)
    {
    	if (allowTailOff)
    	{
    		Envelope.SetGate(0); //Release
    	}
    	else
    	{
    		ResetStuff();
    		clearCurrentNote();
    	}
    }

of course i have added some sound and voice to the Synth in the
audioProcessor and in its processBlock i just pass it the buffer:
Synth.renderNextBlock(buffer, midiMessages, 0, numSamples);


#13


here buffer is filled with 0.6, but as you can see whenever a midi event is happening, in this case midi on and off, process block takes a break and starts again exactly where it has left off (tried it with sine wave).
And again this is not happening in FL for some reason, but so far in live and reaper its always happening. Constant glitches like that when i move mod or pitch wheels. Looks like midi messages are not being handled properly. What do you think im doing wrong or forgetting to do?


#14

That’s probably part of it. So a similar bug to the one I mentioned but slightly different. That doesn’t count the correct number of samples. If you’re near the end of the buffer then startSample will be large and numSamples will be small so that loop won’t run at all


#15

Something like

	for (int SN = startSample; SN < (startSample + numSamples); SN++)
	{
		...
		float EnvVelo = Envelope.GetValue(); //release done -> state = 0
		outputBuffer.setSample(0, SN, (OscillatorOutputL*EnvVelo));
		outputBuffer.setSample(1, SN, (OscillatorOutputR*EnvVelo));
	}

#16

Dude! :grinning: That worked (i have to test it further but it seems glitches are gone for good). Thanks for the help.
Im not sure what exactly happening here. We’re always giving it a startSample of 0
Synth.renderNextBlock(buffer, midiMessages, 0, numSamples);.
So it can start from any sample it wants and loop back?
Shouldn’t i be worried about (startSample + numSamples) exceeding the buffer size or something like that?
edit: to be safe i changed to a while loop and there is no issues.


#17

Yes, but internally the Synthesiser will subdivide the block based on the timestamps of the MIDI messages. Then your voices process these sub-blocks based on the sub-block offset (startSample) and the number of samples to the start of the next sub-block (numSamples).