After finishing my synthesiser, I wanted to add preset saving functionality. I found that I have to implement a ValueTreeState. I realized that it would’ve been easier to use this all along, but changing my code now is unavoidable. I foolishly did not save a copy of my project before this undertaking, although I do have a .vst3 of the working version (without preset saving). The problem is that after attempting to implement a ValueTreeState, I seemingly broke my ADSR implementation. It was working as expected before, but now, the attack goes from 0 to 100, instead of a gradient. The decay and sustain also do not work as expected, although the release seems to work fine. I’m not sure what could be causing this, as all I changed was how the parameters are changed, not the actual DSP, although it is possible that I unknowingly changed something. To test however, I manually set the correct parameters immediately before applying the envelope to the buffer, and I still have this problem. Below is my inherited Synthesiser class, inherited SynthesiserVoice class, plugin editor, and processor. I have spent several hours trying to find the problem to no avail. Thank you in advance.
class SynthVoice : public SynthesiserVoice
{
public:
SynthVoice::SynthVoice(int type)
{
waveType = type;
}
bool canPlaySound(SynthesiserSound* sound) override
{
return dynamic_cast<SynthesiserSound*>(sound) != nullptr;
}
void startNote(int midiNoteNumber, float velocity, SynthesiserSound* sound, int currentPitchWheelPosition)
{
if (waveType == sine)
sinOsc.setFrequency(MidiMessage::getMidiNoteInHertz(midiNoteNumber));
else if (waveType == triangle)
triangleOsc.setFrequency(MidiMessage::getMidiNoteInHertz(midiNoteNumber));
else if (waveType == saw)
sawOsc.setFrequency(MidiMessage::getMidiNoteInHertz(midiNoteNumber));
else if (waveType == square)
squareOsc.setFrequency(MidiMessage::getMidiNoteInHertz(midiNoteNumber));
adsr.noteOn();
}
void stopNote(float velocity, bool allowTailOff)
{
adsr.noteOff();
}
void pitchWheelMoved(int newPitchWheelValue)
{
}
void controllerMoved(int controllerNumber, int newControllerValue)
{
}
void prepareToPlay(double sampleRate, int samplesPerBlock, int outputChannels)
{
synthesisBuffer.setSize(1, samplesPerBlock, true, true, true);
juce::dsp::ProcessSpec spec;
spec.maximumBlockSize = samplesPerBlock;
spec.sampleRate = sampleRate;
spec.numChannels = outputChannels;
sinOsc.prepare(spec);
triangleOsc.prepare(spec);
sawOsc.prepare(spec);
squareOsc.prepare(spec);
filter.prepare(spec);
gain.prepare(spec);
adsr.setSampleRate(sampleRate);
}
void renderNextBlock(AudioBuffer<float>& outputBuffer, int startSample, int numSamples)
{
AudioBuffer<float> synthesisBufferProxy(synthesisBuffer.getArrayOfWritePointers(), 1, 0, numSamples);
dsp::AudioBlock<float> audioBlock{ synthesisBufferProxy };
if (waveType == sine)
sinOsc.process(juce::dsp::ProcessContextReplacing<float>(audioBlock));
else if (waveType == triangle)
triangleOsc.process(juce::dsp::ProcessContextReplacing<float>(audioBlock));
else if (waveType == saw)
sawOsc.process(juce::dsp::ProcessContextReplacing<float>(audioBlock));
else if (waveType == square)
squareOsc.process(juce::dsp::ProcessContextReplacing<float>(audioBlock));
gain.process(juce::dsp::ProcessContextReplacing<float>(audioBlock));
filter.process(dsp::ProcessContextReplacing<float>(audioBlock));
adsr.applyEnvelopeToBuffer(synthesisBufferProxy, 0, numSamples);
for (int chan = 0; chan < outputBuffer.getNumChannels(); ++chan)
outputBuffer.addFrom(chan, startSample, synthesisBufferProxy, 0, 0, numSamples, 1.0f);
}
enum waveTypes
{
sine = 1,
triangle,
saw,
square
};
juce::ADSR::Parameters adsrParams;
dsp::Gain<float> gain;
int waveType = sine;
juce::ADSR adsr;
dsp::ProcessorDuplicator<dsp::StateVariableFilter::Filter<float>, dsp::StateVariableFilter::Parameters<float>> filter;
private:
dsp::Oscillator<float> sinOsc{ [](float x) { return std::sin(x); } };
dsp::Oscillator<float> triangleOsc{ [](float x) { return abs(x / MathConstants<float>::pi); } };
dsp::Oscillator<float> sawOsc{ [](float x) { return x / MathConstants<float>::pi; } };
dsp::Oscillator<float> squareOsc{ [](float x) { return x < 0.0f ? -1.0f : 1.0f; } };
AudioBuffer<float> synthesisBuffer;
};
class ArbSynth : public Synthesiser
{
public:
void ArbSynth::updateGainValue(float newGainVal)
{
for (auto* v : voices)
{
if (auto voice = dynamic_cast<SynthVoice*>(v))
{
voice->gain.setGainLinear(newGainVal / 50000.0f);
}
}
}
void ArbSynth::updateWaveType(int newWaveType)
{
for (auto* v : voices)
{
if (auto voice = dynamic_cast<SynthVoice*>(v))
{
voice->waveType = newWaveType;
}
}
}
void ArbSynth::updateADSR(float atk, float dcy, float sus, float rls)
{
for (auto* v : voices)
{
if (auto voice = dynamic_cast<SynthVoice*>(v))
{
voice->adsrParams.attack = 0.8f;
voice->adsrParams.decay = 5.0f;
voice->adsrParams.sustain = 100.0f;
voice->adsrParams.release = 0.1f;
voice->adsr.setParameters(voice->adsrParams);
}
}
}
void ArbSynth::updateFilter(float frequency, float resonance, dsp::StateVariableFilter::StateVariableFilterType filterType)
{
for (auto* v : voices)
{
if (auto voice = dynamic_cast<SynthVoice*>(v))
{
voice->filter.state->type = filterType;
voice->filter.state->setCutOffFrequency(getSampleRate(), frequency, resonance / 10.0f);
}
}
}
void ArbSynth::prepareToPlay(double sampleRate, int samplesPerBlock, int outputChannels)
{
for (auto* v : voices)
{
if (auto voice = dynamic_cast<SynthVoice*>(v))
{
voice->prepareToPlay(sampleRate, samplesPerBlock, outputChannels);
}
}
}
};
class ArbitraryAudioProcessorEditor : public juce::AudioProcessorEditor, public ComboBox::Listener
{
public:
ArbitraryAudioProcessorEditor (ArbitraryAudioProcessor&, AudioProcessorValueTreeState&);
~ArbitraryAudioProcessorEditor() override;
//==============================================================================
void paint (juce::Graphics&) override;
void resized() override;
void comboBoxChanged(ComboBox* box) override
{
if (box == &waveBox)
{
audioProcessor.currentWaveType = waveBox.getSelectedId();
}
if (box == &filterBox)
{
if (filterBox.getSelectedId() == 1)
audioProcessor.filterOn = false;
else if (filterBox.getSelectedId() == 2)
{
audioProcessor.currentFilterType = dsp::StateVariableFilter::StateVariableFilterType::lowPass;
audioProcessor.filterOn = true;
}
else if (filterBox.getSelectedId() == 3)
{
audioProcessor.currentFilterType = dsp::StateVariableFilter::StateVariableFilterType::highPass;
audioProcessor.filterOn = true;
}
else if (filterBox.getSelectedId() == 4)
{
audioProcessor.currentFilterType = dsp::StateVariableFilter::StateVariableFilterType::bandPass;
audioProcessor.filterOn = true;
}
}
}
juce::AudioProcessorValueTreeState& valueTreeState;
typedef juce::AudioProcessorValueTreeState::SliderAttachment SliderAttachment;
typedef juce::AudioProcessorValueTreeState::ComboBoxAttachment ComboBoxAttachment;
private:
// This reference is provided as a quick way for your editor to
// access the processor object that created it.
ArbitraryAudioProcessor& audioProcessor;
Label masterLabel;
Label oscLabel;
Label filterLabel;
Label envLabel;
Slider gainSlider;
Label gainLabel;
std::unique_ptr<SliderAttachment> gainAttachment;
Slider freqSlider;
Label freqLabel;
std::unique_ptr<SliderAttachment> freqAttachment;
Slider resSlider;
Label resLabel;
std::unique_ptr<SliderAttachment> resAttachment;
Slider atkSlider;
Label atkLabel;
std::unique_ptr<SliderAttachment> atkAttachment;
Slider dcySlider;
Label dcyLabel;
std::unique_ptr<SliderAttachment> dcyAttachment;
Slider susSlider;
Label susLabel;
std::unique_ptr<SliderAttachment> susAttachment;
Slider rlsSlider;
Label rlsLabel;
std::unique_ptr<SliderAttachment> rlsAttachment;
ComboBox waveBox;
Label waveLabel;
ComboBox filterBox;
Label filterTypeLabel;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ArbitraryAudioProcessorEditor)
};
ArbitraryAudioProcessorEditor::ArbitraryAudioProcessorEditor (ArbitraryAudioProcessor& p, AudioProcessorValueTreeState& vts)
: AudioProcessorEditor (&p), audioProcessor (p),
valueTreeState(vts)
{
// Make sure that before the constructor has finished, you've set the
// editor's size to whatever you need it to be.
setSize (800, 300);
addAndMakeVisible(&masterLabel);
masterLabel.setText("Master", dontSendNotification);
addAndMakeVisible(&oscLabel);
oscLabel.setText("Oscillator", dontSendNotification);
addAndMakeVisible(&filterLabel);
filterLabel.setText("Filter", dontSendNotification);
addAndMakeVisible(&envLabel);
envLabel.setText("Envelope", dontSendNotification);
addAndMakeVisible(&gainSlider);
gainSlider.setSliderStyle(Slider::SliderStyle::LinearVertical);
gainSlider.setTextBoxStyle(Slider::TextBoxRight, true, 40, 20);
gainSlider.setTextValueSuffix("%");
gainAttachment.reset(new SliderAttachment(valueTreeState, "gain", gainSlider));
gainSlider.setRange(0.0f, 99.0f, 1.0f);
gainSlider.setValue(50.0f);
addAndMakeVisible(&gainLabel);
gainLabel.setText("Gain", dontSendNotification);
addAndMakeVisible(&atkSlider);
atkSlider.setSliderStyle(Slider::SliderStyle::LinearVertical);
atkSlider.setTextBoxStyle(Slider::TextBoxRight, true, 50, 20);
atkSlider.setTextValueSuffix("s");
atkAttachment.reset(new SliderAttachment(valueTreeState, "atk", atkSlider));
atkSlider.setRange(0.0f, 5.0f, 0.02f);
atkSlider.setValue(0.05f);
addAndMakeVisible(&atkLabel);
atkLabel.setText("Attack", dontSendNotification);
addAndMakeVisible(&dcySlider);
dcySlider.setSliderStyle(Slider::SliderStyle::LinearVertical);
dcySlider.setTextBoxStyle(Slider::TextBoxRight, true, 50, 20);
dcySlider.setTextValueSuffix("s");
dcyAttachment.reset(new SliderAttachment(valueTreeState, "dcy", dcySlider));
dcySlider.setRange(0.0f, 5.0f, 0.02f);
dcySlider.setValue(5.0f);
addAndMakeVisible(&dcyLabel);
dcyLabel.setText("Decay", dontSendNotification);
addAndMakeVisible(&susSlider);
susSlider.setSliderStyle(Slider::SliderStyle::LinearVertical);
susSlider.setTextBoxStyle(Slider::TextBoxRight, true, 45, 20);
susSlider.setTextValueSuffix("%");
susAttachment.reset(new SliderAttachment(valueTreeState, "sus", susSlider));
susSlider.setRange(0.0f, 100.0f, 1.0f);
susSlider.setValue(100.0f);
addAndMakeVisible(&susLabel);
susLabel.setText("Sustain", dontSendNotification);
addAndMakeVisible(&rlsSlider);
rlsSlider.setSliderStyle(Slider::SliderStyle::LinearVertical);
rlsSlider.setTextBoxStyle(Slider::TextBoxRight, true, 50, 20);
rlsSlider.setTextValueSuffix("s");
rlsAttachment.reset(new SliderAttachment(valueTreeState, "rls", rlsSlider));
rlsSlider.setRange(0.0f, 5.0f, 0.02f);
rlsSlider.setValue(0.05f);
addAndMakeVisible(&rlsLabel);
rlsLabel.setText("Release", dontSendNotification);
addAndMakeVisible(&freqSlider);
freqSlider.setSkewFactor(0.34);
freqSlider.setSliderStyle(Slider::SliderStyle::LinearVertical);
freqSlider.setTextBoxStyle(Slider::TextBoxRight, true, 60, 20);
freqSlider.setTextValueSuffix("Hz");
freqAttachment.reset(new SliderAttachment(valueTreeState, "freq", freqSlider));
freqSlider.setRange(40.0f, 20000.0f, 10.0f);
freqSlider.setValue(400.0f);
addAndMakeVisible(&freqLabel);
freqLabel.setText("Cutoff Frequency", dontSendNotification);
addAndMakeVisible(&resSlider);
resSlider.setSliderStyle(Slider::SliderStyle::LinearVertical);
resSlider.setTextBoxStyle(Slider::TextBoxRight, true, 40, 20);
resAttachment.reset(new SliderAttachment(valueTreeState, "res", resSlider));
resSlider.setRange(1.0f, 100.0f, 1.0f);
resSlider.setValue(5.0f);
addAndMakeVisible(&resLabel);
resLabel.setText("Resonance", dontSendNotification);
addAndMakeVisible(&waveBox);
waveBox.addItem("Sine", 1);
waveBox.addItem("Triangle", 2);
waveBox.addItem("Saw", 3);
waveBox.addItem("Square", 4);
waveBox.setSelectedId(1);
waveBox.addListener(this);
addAndMakeVisible(&waveLabel);
waveLabel.setText("Waveform", dontSendNotification);
addAndMakeVisible(&filterBox);
filterBox.addItem("None", 1);
filterBox.addItem("LP", 2);
filterBox.addItem("HP", 3);
filterBox.addItem("BP", 4);
filterBox.setSelectedId(1);
filterBox.addListener(this);
addAndMakeVisible(&filterTypeLabel);
filterTypeLabel.setText("Type", dontSendNotification);
}
ArbitraryAudioProcessorEditor::~ArbitraryAudioProcessorEditor()
{
}
//==============================================================================
void ArbitraryAudioProcessorEditor::paint (juce::Graphics& g)
{
// (Our component is opaque, so we must completely fill the background with a solid colour)
g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));
g.setColour(Colours::white);
g.fillRect(0, 20, 51, 1);
g.fillRect(50, 0, 1, 20);
g.fillRect(107, 0, 1, 300);
g.fillRect(107, 20, 60, 1);
g.fillRect(166, 0, 1, 20);
g.fillRect(219, 0, 1, 300);
g.fillRect(219, 17, 40, 1);
g.fillRect(258, 0, 1, 18);
g.fillRect(405, 0, 1, 300);
g.fillRect(405, 17, 60, 1);
g.fillRect(465, 0, 1, 18);
}
void ArbitraryAudioProcessorEditor::resized()
{
// This is generally where you'll want to lay out the positions of any
// subcomponents in your editor..
masterLabel.setBounds(0, 5, 50, 10);
oscLabel.setBounds(105, 5, 65, 10);
filterLabel.setBounds(220, 3, 40, 10);
envLabel.setBounds(406, 1, 60, 15);
gainSlider.setBounds(32, 19, 70, 62);
gainLabel.setBounds(0, 46, 40, 10);
freqSlider.setBounds(223, 74, 100, 200);
freqLabel.setBounds(248, 140, 120, 15);
resSlider.setBounds(327, 228, 65, 62);
resLabel.setBounds(258, 254, 75, 12);
atkSlider.setBounds(414, 24, 85, 200);
atkLabel.setBounds(448, 96, 48, 12);
dcySlider.setBounds(494, 24, 85, 200);
dcyLabel.setBounds(531, 95, 46, 14);
susSlider.setBounds(574, 24, 80, 200);
susLabel.setBounds(604, 96, 55, 12);
rlsSlider.setBounds(654, 24, 85, 200);
rlsLabel.setBounds(686, 96, 55, 12);
waveBox.setBounds(111, 45, 100, 20);
waveLabel.setBounds(124, 28, 73, 10);
filterBox.setBounds(223, 45, 80, 20);
filterTypeLabel.setBounds(238, 25, 40, 15);
}
class ArbitraryAudioProcessor : public juce::AudioProcessor
{
public:
//==============================================================================
ArbitraryAudioProcessor();
~ArbitraryAudioProcessor() override;
//==============================================================================
void prepareToPlay (double sampleRate, int samplesPerBlock) override;
void releaseResources() override;
#ifndef JucePlugin_PreferredChannelConfigurations
bool isBusesLayoutSupported (const BusesLayout& layouts) const override;
#endif
void processBlock (juce::AudioBuffer<float>&, juce::MidiBuffer&) override;
//==============================================================================
juce::AudioProcessorEditor* createEditor() override;
bool hasEditor() const override;
//==============================================================================
const juce::String getName() const override;
bool acceptsMidi() const override;
bool producesMidi() const override;
bool isMidiEffect() const override;
double getTailLengthSeconds() const override;
//==============================================================================
int getNumPrograms() override;
int getCurrentProgram() override;
void setCurrentProgram (int index) override;
const juce::String getProgramName (int index) override;
void changeProgramName (int index, const juce::String& newName) override;
//==============================================================================
void getStateInformation (juce::MemoryBlock& destData) override;
void setStateInformation (const void* data, int sizeInBytes) override;
void updateParameters();
ArbSynth synth;
std::atomic<float>* gainParameter = nullptr;
std::atomic<float>* freqParameter = nullptr;
std::atomic<float>* resParameter = nullptr;
std::atomic<float>* atkParameter = nullptr;
std::atomic<float>* dcyParameter = nullptr;
std::atomic<float>* susParameter = nullptr;
std::atomic<float>* rlsParameter = nullptr;
dsp::StateVariableFilter::StateVariableFilterType currentFilterType = dsp::StateVariableFilter::StateVariableFilterType::lowPass;
bool filterOn = false;
int currentWaveType = 0;
AudioProcessorValueTreeState parameters;
private:
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ArbitraryAudioProcessor)
};
ArbitraryAudioProcessor::ArbitraryAudioProcessor()
#ifndef JucePlugin_PreferredChannelConfigurations
: AudioProcessor (BusesProperties()
#if ! JucePlugin_IsMidiEffect
#if ! JucePlugin_IsSynth
.withInput ("Input", juce::AudioChannelSet::stereo(), true)
#endif
.withOutput ("Output", juce::AudioChannelSet::stereo(), true)
#endif
),
parameters(*this, nullptr, juce::Identifier("ArbIdentifier"),
{
std::make_unique<juce::AudioParameterFloat>("gain", "Gain", 0, 100, 50),
std::make_unique<juce::AudioParameterFloat>("freq", "Cutoff Frequency", 40, 20000, 400),
std::make_unique<juce::AudioParameterFloat>("res", "Resonance", 0, 100, 1),
std::make_unique<juce::AudioParameterFloat>("atk", "Attack", 0.0f, 5.0f, 0.05f),
std::make_unique<juce::AudioParameterFloat>("dcy", "Decay", 0.0f, 5.0f, 5.0f),
std::make_unique<juce::AudioParameterFloat>("sus", "Sustain", 0, 100, 100),
std::make_unique<juce::AudioParameterFloat>("rls", "Release", 0.0f, 5.0f, 0.05f)
})
#endif
{
gainParameter = parameters.getRawParameterValue("gain");
freqParameter = parameters.getRawParameterValue("freq");
resParameter = parameters.getRawParameterValue("res");
atkParameter = parameters.getRawParameterValue("atk");
dcyParameter = parameters.getRawParameterValue("dcy");
susParameter = parameters.getRawParameterValue("sus");
rlsParameter = parameters.getRawParameterValue("rls");
synth.addSound(new SynthSound());
synth.clearVoices();
for (int i = 0; i < 1; i++)
synth.addVoice(new SynthVoice(currentWaveType));
}
ArbitraryAudioProcessor::~ArbitraryAudioProcessor()
{
}
//==============================================================================
const juce::String ArbitraryAudioProcessor::getName() const
{
return JucePlugin_Name;
}
bool ArbitraryAudioProcessor::acceptsMidi() const
{
#if JucePlugin_WantsMidiInput
return true;
#else
return false;
#endif
}
bool ArbitraryAudioProcessor::producesMidi() const
{
#if JucePlugin_ProducesMidiOutput
return true;
#else
return false;
#endif
}
bool ArbitraryAudioProcessor::isMidiEffect() const
{
#if JucePlugin_IsMidiEffect
return true;
#else
return false;
#endif
}
double ArbitraryAudioProcessor::getTailLengthSeconds() const
{
return 0.0;
}
int ArbitraryAudioProcessor::getNumPrograms()
{
return 1; // NB: some hosts don't cope very well if you tell them there are 0 programs,
// so this should be at least 1, even if you're not really implementing programs.
}
int ArbitraryAudioProcessor::getCurrentProgram()
{
return 0;
}
void ArbitraryAudioProcessor::setCurrentProgram (int index)
{
}
const juce::String ArbitraryAudioProcessor::getProgramName (int index)
{
return {};
}
void ArbitraryAudioProcessor::changeProgramName (int index, const juce::String& newName)
{
}
//==============================================================================
void ArbitraryAudioProcessor::prepareToPlay (double sampleRate, int samplesPerBlock)
{
synth.setCurrentPlaybackSampleRate(sampleRate);
synth.prepareToPlay(sampleRate, samplesPerBlock, getTotalNumOutputChannels());
}
void ArbitraryAudioProcessor::releaseResources()
{
// When playback stops, you can use this as an opportunity to free up any
// spare memory, etc.
}
#ifndef JucePlugin_PreferredChannelConfigurations
bool ArbitraryAudioProcessor::isBusesLayoutSupported (const BusesLayout& layouts) const
{
#if JucePlugin_IsMidiEffect
juce::ignoreUnused (layouts);
return true;
#else
// This is the place where you check if the layout is supported.
// In this template code we only support mono or stereo.
if (layouts.getMainOutputChannelSet() != juce::AudioChannelSet::mono()
&& layouts.getMainOutputChannelSet() != juce::AudioChannelSet::stereo())
return false;
// This checks if the input layout matches the output layout
#if ! JucePlugin_IsSynth
if (layouts.getMainOutputChannelSet() != layouts.getMainInputChannelSet())
return false;
#endif
return true;
#endif
}
#endif
void ArbitraryAudioProcessor::processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages)
{
synth.updateGainValue(*gainParameter);
synth.updateWaveType(currentWaveType);
//synth.updateADSR(*atkParameter, *dcyParameter, *susParameter, *rlsParameter);
if (filterOn)
synth.updateFilter(*freqParameter, *resParameter, currentFilterType);
else
synth.updateFilter(20000.0f, 0.1f, dsp::StateVariableFilter::StateVariableFilterType::lowPass);
synth.renderNextBlock(buffer, midiMessages, 0, buffer.getNumSamples());
}
//==============================================================================
bool ArbitraryAudioProcessor::hasEditor() const
{
return true; // (change this to false if you choose to not supply an editor)
}
juce::AudioProcessorEditor* ArbitraryAudioProcessor::createEditor()
{
return new ArbitraryAudioProcessorEditor (*this, parameters);
}
//==============================================================================
void ArbitraryAudioProcessor::getStateInformation (juce::MemoryBlock& destData)
{
auto state = parameters.copyState();
std::unique_ptr<juce::XmlElement> xml(state.createXml());
copyXmlToBinary(*xml, destData);
}
void ArbitraryAudioProcessor::setStateInformation (const void* data, int sizeInBytes)
{
std::unique_ptr<juce::XmlElement> xmlState(getXmlFromBinary(data, sizeInBytes));
if (xmlState.get() != nullptr)
if (xmlState->hasTagName(parameters.state.getType()))
parameters.replaceState(juce::ValueTree::fromXml(*xmlState));
}
//==============================================================================
// This creates new instances of the plugin..
juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
{
return new ArbitraryAudioProcessor();
}