Help with simple demo sine-wave plugin

Hello all,

I’m developing a sequencer / plugin host app, which means that I don’t spend much time writing DSP code. I have a basic understanding of the Synthesizer and Voice classes, but haven’t invested much time into it as it’s not where the main challenges of my program lie. The only part where I need to delve into DSP code is for a small default sine wave plugin, which is basically just there so that the user can get some sound up and running immediately without having to add their own 3rd party plugins.

For this I’ve built something basic using the juce tutorials as a starting point, but it’s not really working as expected and I can’t figure out why. It’s probably a really trivial problem for a plugin developer, so I wonder if someone could help?

The main problem that I’m facing is with polyphony. It’s essential that the plugin can play many notes at one time (say around 16). I created 16 voices and did setNoteStealingEnabled(true), but still the notes seem to max out at 3 or 4 simultaneously playing notes, after which some notes are dropped any everything seems to distort. I’ve played around with it, but I can’t seem to fix this.

The second problem is that it seems to be out of tune! Midi note 60 is not middle C any more, but seems instead to be somewhere around D. This is bound to be something really simple, but I can’t figure it out.

Here is my code:


#pragma once

#include <JuceHeader.h>

class SineWaveSynth : public juce::AudioProcessor
{
public:
    SineWaveSynth()
        : juce::AudioProcessor(BusesProperties().withOutput("Output", juce::AudioChannelSet::stereo()))
    {
        synth.setNoteStealingEnabled(true);

        static constexpr int numVoices = 16;

        // Add some voices...
        for (int i = 0; i < numVoices; ++i)
            synth.addVoice(new SineWaveVoice());

        // ..and give the synth a sound to play

        synth.addSound(new SineWaveSound());
    }

    static String getIdentifier()
    {
        return "Sine Wave Synth";
    }

    //==============================================================================
    void prepareToPlay(double newSampleRate, int) override
    {
        synth.setCurrentPlaybackSampleRate(newSampleRate);
    }

    void releaseResources() override {}

    //==============================================================================
    void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) override
    {
        processBlockTemplate<float>(buffer, midiMessages);
    }


    void processBlock(juce::AudioBuffer<double>& buffer, juce::MidiBuffer& midiMessages) override
    {
        processBlockTemplate<double>(buffer, midiMessages);
    }
    

    const juce::String getName() const override { return getIdentifier(); }
    double getTailLengthSeconds() const override { return 0.0; }
    bool acceptsMidi() const override { return true; }
    bool producesMidi() const override { return true; }
    juce::AudioProcessorEditor* createEditor() override { return nullptr; }
    bool hasEditor() const override { return false; }
    int getNumPrograms() override { return 1; }
    int getCurrentProgram() override { return 0; }
    void setCurrentProgram(int) override {}
    const juce::String getProgramName(int) override { return {}; }
    void changeProgramName(int, const juce::String&) override {}
    void getStateInformation(juce::MemoryBlock&) override {}
    void setStateInformation(const void*, int) override {}
    bool canAddBus(bool b) const override { return true; }
    bool canRemoveBus(bool b) const override { return true; }

private:
    //==============================================================================

    template<typename T>
    void processBlockTemplate(juce::AudioBuffer<T>& buffer, juce::MidiBuffer& midiMessages)
    {
        const int numSamples = buffer.getNumSamples();

        buffer.clear();
        synth.renderNextBlock(buffer, midiMessages, 0, numSamples);
        buffer.applyGain(0.8f);
    }

    struct SineWaveSound : public juce::SynthesiserSound
    {
        SineWaveSound() = default;

        bool appliesToNote(int /*midiNoteNumber*/) override { return true; }
        bool appliesToChannel(int /*midiChannel*/) override { return true; }
    };

    struct SineWaveVoice : public juce::SynthesiserVoice
    {
        SineWaveVoice() = default;

        bool canPlaySound(juce::SynthesiserSound* sound) override
        {
            return dynamic_cast<SineWaveSound*> (sound) != nullptr;
        }

        void startNote(int midiNoteNumber, float velocity,
            juce::SynthesiserSound* /*sound*/,
            int /*currentPitchWheelPosition*/) override
        {
            currentAngle = 0.0;
            level = velocity * 0.15;
            env.noteOn();

            double cyclesPerSecond = MidiMessage::getMidiNoteInHertz(midiNoteNumber);
            double cyclesPerSample = cyclesPerSecond / getSampleRate();

            angleDelta = cyclesPerSample * 2.0 * MathConstants<double>::pi;
        }

        void stopNote(float /*velocity*/, bool allowTailOff) override
        {
            if (allowTailOff)
                env.noteOff();
            else
            {
                clearCurrentNote();
                env.reset();
            }
        }

        void pitchWheelMoved(int /*newValue*/) override
        {
        }

        void controllerMoved(int /*controllerNumber*/, int /*newValue*/) override
        {
        }

        void setCurrentPlaybackSampleRate(double newRate) override
        {
            if (newRate > 0.0)
            {
                env.setSampleRate(newRate);
                env.setParameters(juce::ADSR::Parameters(0.001f, 0.1f, 0.5f, 0.6f));
            }
        }

        void renderNextBlock(juce::AudioBuffer<float>& outputBuffer, int startSample, int numSamples) override
        {
            const int origStart = startSample, origNum = numSamples;

            if (angleDelta != 0.0)
            {
                while (--numSamples >= 0)
                {
                    const float currentSample = (float)(sin(currentAngle) * level * env.getNextSample());

                    for (int i = outputBuffer.getNumChannels(); --i >= 0;)
                        outputBuffer.addSample(i, startSample, currentSample);

                    currentAngle += angleDelta;
                    ++startSample;
                }
                env.applyEnvelopeToBuffer(outputBuffer, origStart, origNum);

                if (!env.isActive())
                {
                    clearCurrentNote();
                    angleDelta = 0.0;
                }
            }
        }

        using juce::SynthesiserVoice::renderNextBlock;

    private:
        double currentAngle = 0, angleDelta = 0, level = 0;
        juce::ADSR env;
    };

    //==============================================================================
    juce::Synthesiser synth;

    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(SineWaveSynth)
};

Instead of digging through the code and trying to fix things, I’d also be happy just to copy code from any other open source project, if someone here is kind enough to share. My only requirements are that it’s a simple sine wave synth which allows for polyphony, and has basically 0 parameters. It seems like a lot of people must have made this before, so why re-invent the wheel?

1 problem i detect from your code and its how you calculate things up.
angleDelta which increase the currentAngle each iteration,
currentAngle reach high values and you lose the amount of bits you have for the decimal part and you lost data because of that, you can use modulu to fix that or recalculate the currentAngle by check if its bigger than pi2 if so than subtract pi2 from the value.

1 Like

Thanks, I’ve fixed this now. Doesn’t seem to have fixed either of my problems though.

Can anyone suggest any working demos that I can take a look at?

The error with the tuning is that the voice is using the default 44100 instead of the new sample rate because it is overriding. add SynthesiserVoice::setCurrentPlaybackSampleRate(newRate);
The other problem is because you are applying the envelope 2 times, remove applyEnvelopeToBuffer

2 Likes

Thanks @Marcusonic, this cleared it all up!

It seems you fixed it, but as another suggestion, you could just copy the default internal SineWaveSynth that is part of the JUCE AudioPluginHost. The source is all there; look in the file InternalPlugins.cpp.

1 Like