Tutorial: Introduction to DSP - How to reinitialize oscillator?

This tutorial has a lot of good stuff I could elaborate on and use for a synthesizer. About the following text ad code from this excellent tutorial;


class CustomOscillator
{
public:
    CustomOscillator() {
        auto& osc = processorChain.template get<oscIndex>();        // [5]
        osc.initialise ([] (Type x) { return std::sin (x); }, OscillatorResolution); // [6]
    }

Please note I have changed the last osc.initialize argument from 128 to my own OscillatorResolution variable which is initially set to 128. All fine so far, but I have now added a slider with a lambda listener that changes my variable, so my question is how do I “call” the osc.initialise from within my lambda listener so to change to resolution of the wavetable lookup?

Format your code by surrounding it with three backticks (```).

If you want to call a method on a class instance from a lambda, the instance must be present in the lambda’s capture list.

This sounds like an X-Y problem. Why do you want to change the size of a wavetable at runtime with a slider? Testing/experimenting? Doing something like this has side effects and you’re going to want to read up on lock-free swapping (it’s a bit tricky).

The way I’d do this is just provide a method called swapOscillator on your processor class and call it from a lambda defined in your editor’s constructor which has a pointer to the processor.

1 Like

Thanks for the answer and tip about code formatting. I see what you are saying about side-effects. I probably need to ensure no sound is currently playing before changing this value. Anyways C++ is the programming language I spent the least time with, and have difficulty implementing your suggestion.

Besides code in first post, this is what I got in the processor class;

{
public:
    //==============================================================================
    DSPTutorialAudioProcessor()
         : AudioProcessor (BusesProperties().withOutput ("Output", AudioChannelSet::stereo(), true))
    {}
    
    //==============================================================================
    void swapOscillator (double Resolution)
    {
		OscillatorResolution = Resolution;

		//How to call CustomOscillator???
    }

And in the editor class within the processor class I got;

    //==============================================================================
    class DSPTutorialAudioProcessorEditor  : public AudioProcessorEditor
    {
    public:
        DSPTutorialAudioProcessorEditor (DSPTutorialAudioProcessor& p)
            : AudioProcessorEditor (&p),
              dspProcessor (p),
              scopeComponent (dspProcessor.getAudioBufferQueue())
        {			
		// Knob objects
		setLookAndFeel(&myLookAndFeel);
			
		int OscResolutionKnobValue;
		OscResolutionKnob.setSliderStyle (Slider::Rotary);
		addAndMakeVisible (OscResolutionKnob);
		OscResolutionKnob.setRange (3, 1024);
		OscResolutionKnob.setValue (128);
		OscResolutionKnob.onValueChange = [this]
			{
				OscillatorResolution = OscResolutionKnob.getValue();
				
				// How to call SwapOscillator???
			};
OscResolutionKnob.onValueChange = [this] // <- this gives you access to members/methods
{
    OscillatorResolution = OscResolutionKnob.getValue();

    // assuming the type of dspProcessor is DSPTutorialAudioProcessor
    dspProcessor.swapOscillator (OscillatorResolution); 

    // Alternatively:
    // the AudioProcessorEditor base class also gives you access to the processor, 
    // but the type is juce::AudioProcessor so we can use a dynamic cast here 
    dynamic_cast<DSPTutorialAudioProcessor&>(processor).swapOscillator(OscillatorResolution);      
};

As for your swap method.

void swapOscillator (double Resolution) // <- why is this a double? Wavetable sizes should be integer types
{
    OscillatorResolution = Resolution;
    oscillator.initialize(Resolution); // or whatever your member is called. 
}

Changing the wavetable size on the fly is a headache, do you really want to be doing that? I can see it could be useful for testing purposes during development, but maybe you don’t really need that working perfectly for the end product…?

Awesome I am getting closer :slight_smile:
For the onValueChange your first suggestion worked like a charm, thanks!

For the swap method not so much luck, or skill (me) rather :frowning:
Now keep in mind that in the CustomOscillator class which is outside the processor, starts with this;

{
public:
    //==============================================================================
    CustomOscillator() {
        auto& osc = processorChain.template get<oscIndex>();        // [5]
        osc.initialise ([] (Type x) { return std::sin (x); }, OscillatorResolution); // [6]
    }

And its that part I would love to call from my SwapOscillator.

Yes I really want to do that. However I understand I do not want to do it while notes are playing, so I must make sure all sound is off, or force all sound off, before I do.

Oops I just edited last reply to you;
“For the swap method not so much luck, or skill rather :(”

And on second view it looked a bit like an insult, which was not what I intended, so I edited to;
“For the swap method not so much luck, or skill (me) rather :(”.

Sorry

And you’re right it should be an int.

Post a minimal example of what you’re talking about because (edit sorry, can’t read) that code isn’t doing anything (the osc variable is destroyed at the end of the constructor…) and I have no idea what classes own what or how they’re exposed.

If the processor doesn’t own the oscillator, how are you going to do anything useful with it?

Here is the code, all except my first variable, is taken directly from the DSP Tutorial;


//==============================================================================
template <typename Type>

class CustomOscillator
{
public:
    //==============================================================================
    CustomOscillator() {
        auto& osc = processorChain.template get<oscIndex>();        // [5]
        osc.initialise ([] (Type x) { return std::sin (x); }, OscillatorResolution); // [6]
    }

    //==============================================================================
    void setFrequency (Type newValue, bool force = false)
    {
        auto& osc = processorChain.template get<oscIndex>();
        osc.setFrequency (newValue, force);                         // [7]
    }

    //==============================================================================
    void setLevel (Type newValue)
    {
        auto& gain = processorChain.template get<gainIndex>();
        gain.setGainLinear (newValue);                              // [8]
    }

    //==============================================================================
    void reset() noexcept {
        processorChain.reset();           // [4]
    }

    //==============================================================================
    template <typename ProcessContext>
    void process (const ProcessContext& context) noexcept
    {
        processorChain.process (context);                           // [9]
    }

    //==============================================================================
    void prepare (const juce::dsp::ProcessSpec& spec)
    {
        processorChain.prepare (spec);     // [3]
    }

private:
    //==============================================================================
    enum
    {
        oscIndex,
        gainIndex
    };                                     // [2]
 
    juce::dsp::ProcessorChain<juce::dsp::Oscillator<Type>, juce::dsp::Gain<Type>> processorChain; // [1]
};

//==============================================================================
class Voice  : public juce::MPESynthesiserVoice
{
public:
    Voice()
    {
        auto& masterGain = processorChain.get<masterGainIndex>();
        masterGain.setGainLinear (0.7f);
    }

    //==============================================================================
    void prepare (const juce::dsp::ProcessSpec& spec)
    {
        tempBlock = juce::dsp::AudioBlock<float> (heapBlock, spec.numChannels, spec.maximumBlockSize);
        processorChain.prepare (spec);
    }

    //==============================================================================
    void noteStarted() override
    {
        auto velocity = getCurrentlyPlayingNote().noteOnVelocity.asUnsignedFloat();
        auto freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz();

        processorChain.get<osc1Index>().setFrequency (freqHz, true);
        processorChain.get<osc1Index>().setLevel (velocity);
    }

    //==============================================================================
    void notePitchbendChanged() override
    {
        auto freqHz = (float) getCurrentlyPlayingNote().getFrequencyInHertz();
        processorChain.get<osc1Index>().setFrequency (freqHz);
    }

    //==============================================================================
    void noteStopped (bool) override
    {
        clearCurrentNote();
    }

    //==============================================================================
    void notePressureChanged() override {}
    void noteTimbreChanged()   override {}
    void noteKeyStateChanged() override {}

    //==============================================================================
    void renderNextBlock (AudioBuffer<float>& outputBuffer, int startSample, int numSamples) override
    {
        auto block = tempBlock.getSubBlock (0, (size_t) numSamples);
        block.clear();
        juce::dsp::ProcessContextReplacing<float> context (block);
        processorChain.process (context);

        juce::dsp::AudioBlock<float> (outputBuffer)
            .getSubBlock ((size_t) startSample, (size_t) numSamples)
            .add (tempBlock);
    }

private:
    //==============================================================================
    juce::HeapBlock<char> heapBlock;
    juce::dsp::AudioBlock<float> tempBlock;

    enum
    {
        osc1Index,
        masterGainIndex
    };

    juce::dsp::ProcessorChain<CustomOscillator<float>, juce::dsp::Gain<float>> processorChain;

    static constexpr size_t lfoUpdateRate = 100;
};

//==============================================================================
class AudioEngine  : public juce::MPESynthesiser
{
public:
    static constexpr auto maxNumVoices = 64;

    //==============================================================================
    AudioEngine()
    {
        for (auto i = 0; i < maxNumVoices; ++i)
            addVoice (new Voice);

        setVoiceStealingEnabled (true);
    }

    //==============================================================================
    void prepare (const juce::dsp::ProcessSpec& spec) noexcept
    {
        setCurrentPlaybackSampleRate (spec.sampleRate);

        for (auto* v : voices)
            dynamic_cast<Voice*> (v)->prepare (spec);
    }

private:
    //==============================================================================
    void renderNextSubBlock (AudioBuffer<float>& outputAudio, int startSample, int numSamples) override
    {
        MPESynthesiser::renderNextSubBlock (outputAudio, startSample, numSamples);
    }
};

//==============================================================================
template <typename SampleType>
class AudioBufferQueue
{
public:
    //==============================================================================
    static constexpr size_t order = 9;
    static constexpr size_t bufferSize = 1U << order;
    static constexpr size_t numBuffers = 5;

    //==============================================================================
    void push (const SampleType* dataToPush, size_t numSamples)
    {
        jassert (numSamples <= bufferSize);

        int start1, size1, start2, size2;
        abstractFifo.prepareToWrite (1, start1, size1, start2, size2);

        jassert (size1 <= 1);
        jassert (size2 == 0);

        if (size1 > 0)
            FloatVectorOperations::copy (buffers[(size_t) start1].data(), dataToPush, (int) jmin (bufferSize, numSamples));

        abstractFifo.finishedWrite (size1);
    }

    //==============================================================================
    void pop (SampleType* outputBuffer)
    {
        int start1, size1, start2, size2;
        abstractFifo.prepareToRead (1, start1, size1, start2, size2);

        jassert (size1 <= 1);
        jassert (size2 == 0);

        if (size1 > 0)
            FloatVectorOperations::copy (outputBuffer, buffers[(size_t) start1].data(), (int) bufferSize);

        abstractFifo.finishedRead (size1);
    }

private:
    //==============================================================================
    AbstractFifo abstractFifo { numBuffers };
    std::array<std::array<SampleType, bufferSize>, numBuffers> buffers;
};

Here is the bottom code part from this example, which is the last of the “class DSPTutorialAudioProcessor : public AudioProcessor”, with a few thing I added, but it seems it is here I need to add something like “CustomOscillator customOscillator;”

        //==============================================================================		
		DSPTutorialAudioProcessor& dspProcessor;
        
		MyLookAndFeel myLookAndFeel;

		TooltipWindow tooltipWindow;

	Slider OscResolutionKnob;
        Label OscResolutionLabel;
        
        MidiKeyboardState midiKeyboardState;
        MidiKeyboardComponent midiKeyboardComponent { midiKeyboardState, MidiKeyboardComponent::horizontalKeyboard };
        ScopeComponent<float> scopeComponent;
        
        int knobTextWidth;
        int knobTextHeight;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DSPTutorialAudioProcessorEditor)
    };

    //==============================================================================
    AudioEngine audioEngine;
    MidiMessageCollector midiMessageCollector;
    AudioBufferQueue<float> audioBufferQueue;
    ScopeDataCollector<float> scopeDataCollector { audioBufferQueue };

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DSPTutorialAudioProcessor)
};

Gotcha. Just forward the method.

template<class Type>
class CustomOscillator {
public: 
    void initialise(const std::function<Type(Type)>& f, size_t n)
    {
       procesorChain.get<gainIndex>().initialise(f, n); 
    }
}; 

Now call initialise when you need to resize the table.

Thank you very much for taking the time to assist. I feel once I get this to work, it will clear a substantial speed bump that will allow me to proceed much faster. I still am not quite sure how to “call” the method you supplied. This what I got in the top of the file;


class CustomOscillator
{
public:
    //==============================================================================
	CustomOscillator() {
        auto& osc = processorChain.template get<oscIndex>();        // [5]
        osc.initialise ([] (Type x) { return std::sin (x); }, OscillatorResolution); // [6]
    }

	void initialise(const std::function<Type(Type)>& f, size_t n)
	{
		procesorChain.get<gainIndex>().initialise(f, n);
	}

Then in the processor I got;

    {
		OscillatorResolution = Resolution;
		initialise();
    }

And off course it won’t let me call initialise like that, and I believe it has something to with I have to add something near the bottom of the file;

        //==============================================================================		
		DSPTutorialAudioProcessor& dspProcessor;
        
		MyLookAndFeel myLookAndFeel;

		TooltipWindow tooltipWindow;

		Slider OscResolutionKnob;
        
        Label OscResolutionLabel;
        
        MidiKeyboardState midiKeyboardState;
        MidiKeyboardComponent midiKeyboardComponent { midiKeyboardState, MidiKeyboardComponent::horizontalKeyboard };
        ScopeComponent<float> scopeComponent;
        
        int knobTextWidth;
        int knobTextHeight;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DSPTutorialAudioProcessorEditor)
    };

    //==============================================================================
    AudioEngine audioEngine;
    MidiMessageCollector midiMessageCollector;
    AudioBufferQueue<float> audioBufferQueue;
    ScopeDataCollector<float> scopeDataCollector { audioBufferQueue };

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DSPTutorialAudioProcessor)
};

but it seems it is here I need to add something like “CustomOscillator customOscillator;”

Yes. Class methods are called on class instances, so you need to declare a CustomOscillator member variable in the processor.

Ok so question is exatly where to add it and how, because when I do this;

        //==============================================================================		
		DSPTutorialAudioProcessor& dspProcessor;
        
		MyLookAndFeel myLookAndFeel;

		CustomOscillator myCustomOscillator;

		TooltipWindow tooltipWindow;

		Slider OscResolutionKnob;
        
        Label OscResolutionLabel;
        
        MidiKeyboardState midiKeyboardState;
        MidiKeyboardComponent midiKeyboardComponent { midiKeyboardState, MidiKeyboardComponent::horizontalKeyboard };
        ScopeComponent<float> scopeComponent;
        
        int knobTextWidth;
        int knobTextHeight;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DSPTutorialAudioProcessorEditor)
    };

    //==============================================================================
    AudioEngine audioEngine;
    MidiMessageCollector midiMessageCollector;
    AudioBufferQueue<float> audioBufferQueue;
    ScopeDataCollector<float> scopeDataCollector { audioBufferQueue };

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DSPTutorialAudioProcessor)
};

Error is “argument list for class template “CustomOscillator” is missing”

Audio processing objects belong in the audio processor, not the editor. Based on the snippet there (not sure if it’s copy/pasted wrong, since the projucer won’t generate code like that for a plugin project), you’re declaring it in the wrong spot. Put it in the area above JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DSPTutorialAudioProcessor) where it would normally say “user declarations go here.”

And template classes require template arguments. If you’re processing float data that would be CustomOscilator<float>, or CustomOscillator<double> depending on what you want to support.

Ok I put this down at the very bottom of the file, and I am inches away from my goal.


	CustomOscillator<float> myCustomOscillator;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DSPTutorialAudioProcessor)
};

So far so good, and now I want to call your initialise method, which was;

	{
		procesorChain.get<gainIndex>().initialise(f, n);
	}

Which somehow, and I do not understand, processes;

        auto& osc = processorChain.template get<oscIndex>();        // [5]
        osc.initialise ([] (Type x) { return std::sin (x); }, OscillatorResolution); // [6]
    }

and creates a new table at my specified resolution. Problem now is when I try to call initialise from DSPTutorialAudioProcessor with the;

    {
		OscillatorResolution = Resolution;
		myCustomOscillator.initialise();
    }

I figure I have to supply some variables, as when I hover over initialise a popup help text is “Void CustomOscillator::initialise(const std::function<Type(Type)>& f, size_t n)”, just not sure which.

Ok I think we’re both getting tripped up here. Let’s break it down:

  1. You have an Oscillator instance and want to call a method on it from the editor.
  2. The oscillator is contained by a ProcessorChain instance.
  3. That’s owned/wrapped by CustomOscillator.
  4. The CustomOscillator is owned by the processor.
  5. The processor is referenced by the editor.

So what we need to do is expose the Oscillator instance to the editor class. The approach I originally mentioned was intended to make it easier to deal with the side effects (that’s why it was called swapOscillator, not initialiseOscillator or something like that), and the implementation was based on what you had written (I didn’t want to confuse you any more).

But let’s ignore that and do this as simple as possible. Just make everything public.

template<class Type>
class CustomOscillator {
public: 
    // ... 
    ProcessorChain</* etc */> processorChain; //< this is public
};
//... 
class DSPTutorialAudioProcessor : public AudioProcessor {
public:
    // ... 
    CustomOscillator<float> customOscillator; // < this is public
};

//... 
class DSPTutorialAudioProcessorEditor {
public: 
    DSPTutorialAudioProcessorEditor(DSPTutorialAudioProcessor& p) 
    : AudioProcessorEditor(p) {
        // ... 
        resolutionSlider.onValueChange = [this] () {
            auto resolution = resolutionSlider.getValue(); 
            dynamic_cast<DSPTutorialAudioProcessor&>(processor) //< cast 
                .customOscillator // < access the oscillator
                .processorChain   // < access the container 
                .get(0)           // < get the oscillator 
                .initialise (/* etc */); // < finally, call the method
        };
     }
};

To move up from the simple way to see why you might change this:

  1. Why is the oscillator/gain wrapped in a CustomOscillatorClass? Why not just put it in the DSPTutorialAudioProcessor?
  2. What happens when the slider is moved while the audio is processing?
  3. How can you make sure the wavetable is updated without artifacts?
  4. Why do you need to do this in realtime?
  5. Are you duplicating code anywhere (e.g. in the CustomOscillator constructor/the slider callback.

You might want to read up a little bit on C++ syntax to understand how member variable/method declaration works and public vs private members.

Thank you very much for your patience trying to solve this problem, and I do feel I learn something a long the way.

I will try again tomorrow, but for let me ask your questions.

  1. I did not wrap anything. All the code, except what you suggested, was taken from the JUCE website DSP Tutorial.
  2. As I have already answered to another poster, yes off course I need to make sure I am not changing the wavetable size while audio is processed. So I must either wait for all notes to be off, send note offs, empty audio buffer, or something along those lines.
  3. I do not intend to change the wavetable size while audio is playing.
  4. I am trying to make my own unique synthesizer. One with unusual, or features not even seen before. Changing the wavetable size, mostly downward (smaller), makes the waveform and therefore the sound different in interesting ways, and in my humble opinion that is that is the most important feature of a synthesizer.
  5. I am not duplicating any code.

Finally I have already planned to read up on C++, as its been many years since I last worked with it.