Trying to get a "GroupComponent" around a slider and linked label - What am I doing wrong?

I’m taking the basic synthesizer tutorial and just trying to create a GroupComponent around the “frequencySlider” and its attached label.

I am having some problems though.

  1. It’s not drawing the slider at all within the GroupComponent.
  2. I don’t know what to do with some of the code around the frequencySlider, as it doesn’t work when moved to within the GroupComponent declaration. And if I move it elsewhere it doesn’t work either.

I feel like in general this approach makes little sense. I don’t think it makes that much sense to have to declare so many parameters for “frequencySlider” within the class declaration for “labeledFrequencySlider”, and then I’ll still have to declare other parameters for “frequencySlider” outside of it.

What are the fixes? Or what is the proper way to do this?

Thanks


class labeledFrequencySlider : GroupComponent
{
public:
labeledFrequencySlider()
{
	addAndMakeVisible(frequencySlider);
	addAndMakeVisible(frequencyLabel);

	 

 frequencySlider.setSliderStyle(Slider::RotaryHorizontalVerticalDrag);
    frequencySlider.setTextBoxStyle(Slider::TextBoxBelow, false, 100, 20);
    frequencySlider.setRange(50.0, 5000.0);
		frequencySlider.setSkewFactorFromMidPoint(500.0);
		frequencySlider.setNumDecimalPlacesToDisplay(1);
//		frequencySlider.setValue(currentFrequency, dontSendNotification);
//		frequencySlider.onValueChange = [this] { targetFrequency = frequencySlider.getValue(); };

		frequencyLabel.setText("Frequency", dontSendNotification);
		frequencyLabel.attachToComponent(&frequencySlider, false);
	frequencyLabel.setJustificationType(Justification::horizontallyCentred);
    }

	void resized() override
	{
		frequencySlider.setBounds(0, 0, getWidth(), getHeight());
	}

private:
	Slider frequencySlider;
	Label frequencyLabel;

};

class MainContentComponent : public AudioAppComponent
{
public:
    MainContentComponent()
    {

		addAndMakeVisible(labeledFrequencySlider);
		
        addAndMakeVisible (levelSlider);
		levelSlider.setSliderStyle(Slider::Rotary);
		levelSlider.setRange (0.0, 0.125);
        levelSlider.setValue ((double) currentLevel, dontSendNotification);
        levelSlider.onValueChange = [this] { targetLevel = (float) levelSlider.getValue(); };

        setSize (600, 600);
        setAudioChannels (0, 2); // no inputs, two outputs
    }

    ~MainContentComponent()
    {
        shutdownAudio();
    }

    void resized() override
    {
		labeledFrequencySlider.setBounds(0, 10, 100, 100);
		levelSlider    .setBounds (0, 100, rotaryDiam, rotaryDiam);
    }

    inline void updateAngleDelta()
    {
        auto cyclesPerSample = currentFrequency / currentSampleRate;
        angleDelta = cyclesPerSample * 2.0 * MathConstants<double>::pi;
    }

    void prepareToPlay (int, double sampleRate) override
    {
        currentSampleRate = sampleRate;
        updateAngleDelta();
    }

    void releaseResources() override {}

    void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
    {
        auto* leftBuffer  = bufferToFill.buffer->getWritePointer (0, bufferToFill.startSample);
        auto* rightBuffer = bufferToFill.buffer->getWritePointer (1, bufferToFill.startSample);

        auto localTargetFrequency = targetFrequency;

        if (targetFrequency != currentFrequency)
        {
            auto frequencyIncrement = (targetFrequency - currentFrequency) / bufferToFill.numSamples;

            for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
            {
                auto currentSample = (float) std::sin (currentAngle);
                currentFrequency += frequencyIncrement;
                updateAngleDelta();
                currentAngle += angleDelta;
                leftBuffer[sample]  = currentSample;
                rightBuffer[sample] = currentSample;
            }

            currentFrequency = localTargetFrequency;
        }
        else
        {
            for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
            {
                auto currentSample = (float) std::sin (currentAngle);
                currentAngle += angleDelta;
                leftBuffer[sample]  = currentSample;
                rightBuffer[sample] = currentSample;
            }
        }

        auto localTargetLevel = targetLevel;
        bufferToFill.buffer->applyGainRamp (bufferToFill.startSample, bufferToFill.numSamples, currentLevel, localTargetLevel);
        currentLevel = localTargetLevel;
    }

private:
    GroupComponent labeledFrequencySlider;
	Slider levelSlider;
    
    double currentSampleRate = 0.0, currentAngle = 0.0, angleDelta = 0.0;
    double currentFrequency = 500.0, targetFrequency = 500.0;
    float currentLevel = 0.1f, targetLevel = 0.1f;
	int rotaryDiam = 100;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};

First the technical problems: If you inherit from GroupComponent, you have to specify public, so the inherited symbols are available from outside. Otherwise you can only call methods from GroupComponent from inside.

You are right, it doesn’t. That’s why I left the slider as a public member, so you can use it outside of the class. Your class would look like this (btw. I capitalised the class name, which is a commonly adapted convention to distinguish class names from instances):

class LabeledSlider : public GroupComponent
{
public:
    LabeledSlider (const String& name)
    {
        setText (name);
        setTextLabelPosition (Justification::centredTop);
        addAndMakeVisible (slider);
    }
    
    void resized() override
    {
        slider.setBounds (getLocalBounds().reduced (10));
    }
    
    Slider slider { Slider::RotaryHorizontalVerticalDrag, Slider::TextBoxBelow };
};

Now as members you can create a frequency and a level like this:

private:
    LabeledSlider frequency {"Frequency"};
    LabeledSlider level {"Level"};
    // ...

And you set it up in your constructor:

MainComponent::MainComponent()
{
    addAndMakeVisible (frequency);
    addAndMakeVisible (level);

    frequency.slider.setRange (20.0, 20000.0);
    frequency.slider.setSkewFactorFromMidPoint (500.0);
    frequency.slider.setNumDecimalPlacesToDisplay (1);

    level.slider.setRange (0.0, 1.0);
    level.slider.onValueChange = [this] { targetLevel = (float) level.slider.getValue(); };
    // ...

finally set it somewhere in the resized:

void MainComponent::resized()
{
    frequency.setBounds (10, 10, 100, 100);
    level.setBounds (110, 10, 100, 100);
}

…or have it placed automatically occupying all space:

void MainComponent::resized()
{
    auto bounds = getLocalBounds();
    frequency.setBounds (bounds.removeFromLeft (getWidth() / 2));
    level.setBounds (bounds);
}

It looks like that here:

Hope that gets you one step further…

Thanks daniel. That’s very, very helpful.

So the main points I’m getting from that if i’m understanding:

  • You used the new GroupComponent “LabeledSlider” to directly label the knobs, as this class has the labeling function built in, rather than defining a label separately and attaching it to the sliders.
  • You defined the new GroupComponent “LabeledSlider” to automatically generate a generically named “slider” with “addAndMakeVisible (slider);” each time it is used with the generic properties “Slider::RotaryHorizontalVerticalDrag, Slider::TextBoxBelow”.
  • You named the GroupComponents holding these generically named sliders “Frequency” and “Level”.
  • Properties for these sliders can then be accessed and set in MainContentComponent via “frequency.slider.parameter” and “level.slider.parameter”

I think this makes sense if I’m getting it right. This allows us to define general knob characteristics under the LabeledSlider class definition, and specific knob characteristics under the MainComponentClass. It also saves us from manually defining a label each time.

But I am getting an error when I try to implement what you provided:

I tried to implement it exactly as you suggested. What am I doing wrong?

Thanks again.

class LabeledSlider : public GroupComponent

{
    public:
    LabeledSlider (const String& name)
    {
        setText (name);
        setTextLabelPosition (Justification::centredTop);
        addAndMakeVisible (slider);
    }
    
    void resized() override
    {
        slider.setBounds (getLocalBounds().reduced (10));
    }
    
    Slider slider 
    { 
        Slider::RotaryHorizontalVerticalDrag, Slider::TextBoxBelow 
    };
    
    private:
    LabeledSlider frequency {"Frequency"};
    LabeledSlider level {"Level"};

};


class MainContentComponent : public AudioAppComponent
{
public:
    MainContentComponent()
    {

    addAndMakeVisible (frequency);
    addAndMakeVisible (level);

    frequency.slider.setRange (20.0, 20000.0);
    frequency.slider.setSkewFactorFromMidPoint (500.0);
    frequency.slider.setNumDecimalPlacesToDisplay (1);
    frequency.slider.setValue(currentFrequency, dontSendNotification);
    frequency.slider.onValueChange = [this] { targetFrequency = frequency.slider.getValue(); };
	frequency.slider.setTextBoxStyle(Slider::TextBoxBelow, false, 100, 20);
	frequency.slider.setRange(50.0, 5000.0);
	frequency.slider.setSkewFactorFromMidPoint(500.0);
	frequency.slider.setNumDecimalPlacesToDisplay(1);

    level.slider.setRange (0.0, 1.0);
    level.slider.onValueChange = [this] { targetLevel = (float) level.slider.getValue(); };

    setSize (600, 600);
    setAudioChannels (0, 2); // no inputs, two outputs
    }

    ~MainContentComponent()
    {
        shutdownAudio();
    }

    void resized() override
    {
    auto bounds = getLocalBounds();
    frequency.setBounds (bounds.removeFromLeft (getWidth() / 2));
    level.setBounds (bounds);
    
    }

    inline void updateAngleDelta()
    {
        auto cyclesPerSample = currentFrequency / currentSampleRate;
        angleDelta = cyclesPerSample * 2.0 * MathConstants<double>::pi;
    }

    void prepareToPlay (int, double sampleRate) override
    {
        currentSampleRate = sampleRate;
        updateAngleDelta();
    }

    void releaseResources() override {}

    void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
    {
        auto* leftBuffer  = bufferToFill.buffer->getWritePointer (0, bufferToFill.startSample);
        auto* rightBuffer = bufferToFill.buffer->getWritePointer (1, bufferToFill.startSample);

        auto localTargetFrequency = targetFrequency;

        if (targetFrequency != currentFrequency)
        {
            auto frequencyIncrement = (targetFrequency - currentFrequency) / bufferToFill.numSamples;

            for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
            {
                auto currentSample = (float) std::sin (currentAngle);
                currentFrequency += frequencyIncrement;
                updateAngleDelta();
                currentAngle += angleDelta;
                leftBuffer[sample]  = currentSample;
                rightBuffer[sample] = currentSample;
            }

            currentFrequency = localTargetFrequency;
        }
        else
        {
            for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
            {
                auto currentSample = (float) std::sin (currentAngle);
                currentAngle += angleDelta;
                leftBuffer[sample]  = currentSample;
                rightBuffer[sample] = currentSample;
            }
        }

        auto localTargetLevel = targetLevel;
        bufferToFill.buffer->applyGainRamp (bufferToFill.startSample, bufferToFill.numSamples, currentLevel, localTargetLevel);
        currentLevel = localTargetLevel;
    }

private:    
    double currentSampleRate = 0.0, currentAngle = 0.0, angleDelta = 0.0;
    double currentFrequency = 500.0, targetFrequency = 500.0;
    float currentLevel = 0.1f, targetLevel = 0.1f;
	int rotaryDiam = 100;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};

Yes, you got that right.

But the LabeledSlider needs to be defined before the MainComponent, so best to put it into the header before the MainComponent.
Or in a separate header, that you include before the MainComponent.h

Or I don’t know which C++ standard you are using, maybe it doesn’t like the initialiser. In that case you have to initialise it the old way in your MainComponent constructor:

MainContentComponent() : 
frequency ("Frequency"), level ("Level")
{
    // ...

and in the header without the part in angle brackets:

private:
    LabeledSlider frequency, level;

Thanks Daniel! I figured out what I did wrong - I tried to declare the LabeledSlider objects within the class declaration for the LabeledSlider class. I was supposed to declare those objects inside MainContentComponent.

Of course, you can’t declare objects of a given class within the same class definition because that creates an infinite loop.

Here is the working code now:

class LabeledSlider : public GroupComponent

{
    public:
    LabeledSlider (const String& name)
    {
        setText (name);
        setTextLabelPosition (Justification::centredTop);
        addAndMakeVisible (slider);
    }
    
    void resized() override
    {
        slider.setBounds (getLocalBounds().reduced (10));
    }
    
    Slider slider 
    { 
        Slider::RotaryHorizontalVerticalDrag, Slider::TextBoxBelow 
    };
    

};



class MainContentComponent : public AudioAppComponent
{
public:
    MainContentComponent()
	
    {
    addAndMakeVisible (frequency);
    addAndMakeVisible (level);

    frequency.slider.setRange (20.0, 20000.0);
    frequency.slider.setSkewFactorFromMidPoint (500.0);
    frequency.slider.setNumDecimalPlacesToDisplay (1);
    frequency.slider.setValue(currentFrequency, dontSendNotification);
    frequency.slider.onValueChange = [this] { targetFrequency = frequency.slider.getValue(); };
	frequency.slider.setTextBoxStyle(Slider::TextBoxBelow, false, 100, 20);
	frequency.slider.setRange(50.0, 5000.0);
	frequency.slider.setSkewFactorFromMidPoint(500.0);
	frequency.slider.setNumDecimalPlacesToDisplay(1);

    level.slider.setRange (0.0, 1.0);
    level.slider.onValueChange = [this] { targetLevel = (float) level.slider.getValue(); };

    setSize (600, 600);
    setAudioChannels (0, 2); // no inputs, two outputs
    }

    ~MainContentComponent()
    {
        shutdownAudio();
    }

    void resized() override
    {
    auto bounds = getLocalBounds();
    frequency.setBounds (bounds.removeFromLeft (getWidth() / 2));
    level.setBounds (bounds);
    
    }

    inline void updateAngleDelta()
    {
        auto cyclesPerSample = currentFrequency / currentSampleRate;
        angleDelta = cyclesPerSample * 2.0 * MathConstants<double>::pi;
    }

    void prepareToPlay (int, double sampleRate) override
    {
        currentSampleRate = sampleRate;
        updateAngleDelta();
    }

    void releaseResources() override {}

    void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
    {
        auto* leftBuffer  = bufferToFill.buffer->getWritePointer (0, bufferToFill.startSample);
        auto* rightBuffer = bufferToFill.buffer->getWritePointer (1, bufferToFill.startSample);

        auto localTargetFrequency = targetFrequency;

        if (targetFrequency != currentFrequency)
        {
            auto frequencyIncrement = (targetFrequency - currentFrequency) / bufferToFill.numSamples;

            for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
            {
                auto currentSample = (float) std::sin (currentAngle);
                currentFrequency += frequencyIncrement;
                updateAngleDelta();
                currentAngle += angleDelta;
                leftBuffer[sample]  = currentSample;
                rightBuffer[sample] = currentSample;
            }

            currentFrequency = localTargetFrequency;
        }
        else
        {
            for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
            {
                auto currentSample = (float) std::sin (currentAngle);
                currentAngle += angleDelta;
                leftBuffer[sample]  = currentSample;
                rightBuffer[sample] = currentSample;
            }
        }

        auto localTargetLevel = targetLevel;
        bufferToFill.buffer->applyGainRamp (bufferToFill.startSample, bufferToFill.numSamples, currentLevel, localTargetLevel);
        currentLevel = localTargetLevel;
	}

private:
	double currentSampleRate = 0.0, currentAngle = 0.0, angleDelta = 0.0;
	double currentFrequency = 500.0, targetFrequency = 500.0;
	float currentLevel = 0.1f, targetLevel = 0.1f;
	int rotaryDiam = 100;

	LabeledSlider frequency{ "Frequency" };
	LabeledSlider level{ "Level" };


    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};

Thanks again for the help so far. I find learning by doing with some guidance is the most helpful. All the reading in the world does not teach nearly as much as practical implementation.

Well I got a simple FlexBox implementation going now thanks to you daniel and this thread.

Here is the code, which is again the header code from the SineSynth Tutorial modified, and stealing some bits from the FlexBox tutorial:

class LabeledSlider : public GroupComponent

{
    public:
    LabeledSlider (const String& name)
    {
        setText (name);
        setTextLabelPosition (Justification::centredTop);
        addAndMakeVisible (slider);
    }
    
    void resized() override
    {
        slider.setBounds (getLocalBounds().reduced (10));
    }
    
    Slider slider 
    { 
        Slider::RotaryHorizontalVerticalDrag, Slider::TextBoxBelow 
    };
    

};



class MainContentComponent : public AudioAppComponent
{
public:
    MainContentComponent()
	
    {
    addAndMakeVisible(frequency);
    addAndMakeVisible(level);
	addAndMakeVisible(dummy1);
	addAndMakeVisible(dummy2);
	addAndMakeVisible(dummy3);
	addAndMakeVisible(dummy4);
	addAndMakeVisible(dummy5);
	addAndMakeVisible(dummy6);
	addAndMakeVisible(dummy7);
	addAndMakeVisible(dummy8);
	addAndMakeVisible(dummy9);

    frequency.slider.setRange (20.0, 20000.0);
    frequency.slider.setSkewFactorFromMidPoint (500.0);
    frequency.slider.setNumDecimalPlacesToDisplay (1);
    frequency.slider.setValue(currentFrequency, dontSendNotification);
    frequency.slider.onValueChange = [this] { targetFrequency = frequency.slider.getValue(); };
	frequency.slider.setTextBoxStyle(Slider::TextBoxBelow, false, 100, 20);
	frequency.slider.setRange(50.0, 5000.0);
	frequency.slider.setSkewFactorFromMidPoint(500.0);
	frequency.slider.setNumDecimalPlacesToDisplay(1);

    level.slider.setRange (0.0, 1.0);
    level.slider.onValueChange = [this] { targetLevel = (float) level.slider.getValue(); };

    setSize (600, 600);
    setAudioChannels (0, 2); // no inputs, two outputs
    }

    ~MainContentComponent()
    {
        shutdownAudio();
    }

    void resized() override
    {
			
		//==============================================================================
		FlexBox knobBox;
		knobBox.flexWrap = FlexBox::Wrap::wrap;
		knobBox.justifyContent = FlexBox::JustifyContent::flexStart;
		knobBox.alignContent = FlexBox::AlignContent::flexStart;

		Array<LabeledSlider*> knobs;
		knobs.add(&frequency, &level, &dummy1, &dummy2, &dummy3, &dummy4, &dummy5, &dummy6, &dummy7, &dummy8, &dummy9);

		for (auto* k : knobs)
			knobBox.items.add(FlexItem(*k).withMinHeight(80.0f).withMinWidth(80.0f).withFlex(1));

		//==============================================================================
		FlexBox fb;
		fb.flexDirection = FlexBox::Direction::column;
		fb.items.add(FlexItem(knobBox).withFlex(2.5));
		fb.performLayout(getLocalBounds().toFloat());
	

    }

    inline void updateAngleDelta()
    {
        auto cyclesPerSample = currentFrequency / currentSampleRate;
        angleDelta = cyclesPerSample * 2.0 * MathConstants<double>::pi;
    }

    void prepareToPlay (int, double sampleRate) override
    {
        currentSampleRate = sampleRate;
        updateAngleDelta();
    }

    void releaseResources() override {}

    void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
    {
        auto* leftBuffer  = bufferToFill.buffer->getWritePointer (0, bufferToFill.startSample);
        auto* rightBuffer = bufferToFill.buffer->getWritePointer (1, bufferToFill.startSample);

        auto localTargetFrequency = targetFrequency;

        if (targetFrequency != currentFrequency)
        {
            auto frequencyIncrement = (targetFrequency - currentFrequency) / bufferToFill.numSamples;

            for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
            {
                auto currentSample = (float) std::sin (currentAngle);
                currentFrequency += frequencyIncrement;
                updateAngleDelta();
                currentAngle += angleDelta;
                leftBuffer[sample]  = currentSample;
                rightBuffer[sample] = currentSample;
            }

            currentFrequency = localTargetFrequency;
        }
        else
        {
            for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
            {
                auto currentSample = (float) std::sin (currentAngle);
                currentAngle += angleDelta;
                leftBuffer[sample]  = currentSample;
                rightBuffer[sample] = currentSample;
            }
        }

        auto localTargetLevel = targetLevel;
        bufferToFill.buffer->applyGainRamp (bufferToFill.startSample, bufferToFill.numSamples, currentLevel, localTargetLevel);
        currentLevel = localTargetLevel;
	}

private:
	double currentSampleRate = 0.0, currentAngle = 0.0, angleDelta = 0.0;
	double currentFrequency = 500.0, targetFrequency = 500.0;
	float currentLevel = 0.1f, targetLevel = 0.1f;
	int rotaryDiam = 100;

	LabeledSlider frequency{ "Frequency" };
	LabeledSlider level{ "Level" };
	LabeledSlider dummy1{ "Dummy 1" };
	LabeledSlider dummy2{ "Dummy 2" };
	LabeledSlider dummy3{ "Dummy 3" };
	LabeledSlider dummy4{ "Dummy 4" };
	LabeledSlider dummy5{ "Dummy 5" };
	LabeledSlider dummy6{ "Dummy 6" };
	LabeledSlider dummy7{ "Dummy 7" };
	LabeledSlider dummy8{ "Dummy 8" };
	LabeledSlider dummy9{ "Dummy 9" };


    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};

This creates this result:

Now I have two follow up questions:

1) Automating the Array?
Currently to add a knob, I must enter it in three spaces of MainContentComponent: (i) Create it as an object under Private, (ii) addAndMakeVisible under Public, and (iii) Add it to the Array under “void resized()” in Public.

This is acceptable, but is there any more efficient way to do it? Is there any way to automate step (iii) and somehow have all “labeledSlider” objects automatically added to the array in the order they’re declared?

This would help with efficiency and reduce potential error on projects with many knobs as I have planned.

2) Left Justifying the FlexBox?
In the screencap of my FlexBox test, the 3rd row which only has three knobs (vs. four knobs for the other rows) is stretched horizontally, so the three LabeledSlider objects on this row are different width from the rest.

Is there any way to prevent this and force the FlexBox process to assign the least (or equal) amount of space needed to all Items? Ie. So they all look the same?

I tried to force left justification instead of stretching with:

FlexBox knobBox;
	knobBox.flexWrap = FlexBox::Wrap::wrap;
	knobBox.justifyContent = FlexBox::JustifyContent::flexStart;
	knobBox.alignContent = FlexBox::AlignContent::flexStart;

But it didn’t work as you can see, as it’s still stretching each row individually to fit the full width. Any fix?

Thanks again. Getting there. :slight_smile:

I’m glad you got that part running.

Yes, there is. Since you create all settings and added connections in the MainComponent constructor, there is no need to have every slider as an individual object. Instead you may use an OwnedArray and stuff all LabeledSlider objects into it:

private:
    OwnedArray<LabeledSlider> knobs;

So that’s one place less to care about.
The constructor is the place, where we set things up, so that one has to be bespoke. But instead having an object as member for each parameter, we create them on the heap, and put them into our array:

MainContentComponent()
{
    LabeledSlider* control = new LabeledSlider ("Frequency");
    control->slider.setRange (20.0, 20000.0);
    control->slider.setSkewFactorFromMidPoint (500.0);
    control->slider.setNumDecimalPlacesToDisplay (1);
    control->slider.setValue(currentFrequency, dontSendNotification);
    control->slider.onValueChange = [this] { targetFrequency = frequency.slider.getValue(); };
    control->slider.setTextBoxStyle(Slider::TextBoxBelow, false, 100, 20);
    control->slider.setRange(50.0, 5000.0);
    control->slider.setSkewFactorFromMidPoint(500.0);
    control->slider.setNumDecimalPlacesToDisplay(1);
    addAndMakeVisible (knobs.add (control));

    control = new LabeledSlider ("Level");
    control->slider.setRange (0.0, 1.0);
    control->slider.onValueChange = [this] { targetLevel = (float) level.slider.getValue(); };
    addAndMakeVisible (knobs.add (control));

    // ...

And the third place, the resized, you copied your individual objects into the Array<LabeledSlider*> knobs. This is now no longer necessary, since you have the sliders in the OwedArray knobs already.
Now the circle is closed…

TBH. I never used FlexBox myself, but I think changing the items to this might help:

for (auto* k : knobs)
    knobBox.items.add(FlexItem(*k).withMinHeight(80.0f).withMinWidth(80.0f).withFlex(1).withAlignSelf (AlignSelf::autoAlign));

EDIT: just checked, and setting that value back didn’t change the behaviour back, so it might have been some other setting, I don’t know…

(Using the FlexBox demo and playing with the values there can be a big help).

Good luck

That suggestion to use dynamic memory and the “owned array” worked well daniel. I got it working like that. The stretching thing is less important of an issue anyway.

For anyone’s reference, here’s the working code:

class LabeledSlider : public GroupComponent

{
    public:
    LabeledSlider (const String& name)
    {
        setText (name);
        setTextLabelPosition (Justification::centredTop);
        addAndMakeVisible (slider);
    }

    void resized() override
    {
        slider.setBounds (getLocalBounds().reduced (10));
    }
    
    Slider slider 
    { 
        Slider::RotaryHorizontalVerticalDrag, Slider::TextBoxBelow 
    };
    

};



class MainContentComponent : public AudioAppComponent
{
public:
    MainContentComponent()
	
    {

		LabeledSlider* control = new LabeledSlider("Frequency");
		control->slider.setRange(20.0, 20000.0);
		control->slider.setSkewFactorFromMidPoint(500.0);
		control->slider.setNumDecimalPlacesToDisplay(1);
		control->slider.setValue(currentFrequency, dontSendNotification);
		control->slider.onValueChange = [this] { targetFrequency = frequency.slider.getValue(); };
		control->slider.setTextBoxStyle(Slider::TextBoxBelow, false, 100, 20);
		control->slider.setRange(50.0, 5000.0);
		control->slider.setSkewFactorFromMidPoint(500.0);
		control->slider.setNumDecimalPlacesToDisplay(1);
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Level");
		control->slider.setRange(0.0, 1.0);
		control->slider.onValueChange = [this] { targetLevel = (float)level.slider.getValue(); };
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy1");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy2");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy3");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy4");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy5");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy6");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy7");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy8");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy9");
		addAndMakeVisible(knobs.add(control));


    setSize (600, 600);
    setAudioChannels (0, 2); // no inputs, two outputs
    }

    ~MainContentComponent()
    {
        shutdownAudio();
    }

    void resized() override
    {
			
		//==============================================================================
		FlexBox knobBox;
		knobBox.flexWrap = FlexBox::Wrap::wrap;
		knobBox.justifyContent = FlexBox::JustifyContent::flexStart;
		knobBox.alignContent = FlexBox::AlignContent::flexStart;


		for (auto* k : knobs)
			knobBox.items.add(FlexItem(*k).withMinHeight(80.0f).withMinWidth(80.0f).withFlex(1));

		//==============================================================================
		FlexBox fb;
		fb.flexDirection = FlexBox::Direction::column;
		fb.items.add(FlexItem(knobBox).withFlex(2.5));
		fb.performLayout(getLocalBounds().toFloat());
	

    }

    inline void updateAngleDelta()
    {
        auto cyclesPerSample = currentFrequency / currentSampleRate;
        angleDelta = cyclesPerSample * 2.0 * MathConstants<double>::pi;
    }

    void prepareToPlay (int, double sampleRate) override
    {
        currentSampleRate = sampleRate;
        updateAngleDelta();
    }

    void releaseResources() override {}

    void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
    {
        auto* leftBuffer  = bufferToFill.buffer->getWritePointer (0, bufferToFill.startSample);
        auto* rightBuffer = bufferToFill.buffer->getWritePointer (1, bufferToFill.startSample);

        auto localTargetFrequency = targetFrequency;

        if (targetFrequency != currentFrequency)
        {
            auto frequencyIncrement = (targetFrequency - currentFrequency) / bufferToFill.numSamples;

            for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
            {
                auto currentSample = (float) std::sin (currentAngle);
                currentFrequency += frequencyIncrement;
                updateAngleDelta();
                currentAngle += angleDelta;
                leftBuffer[sample]  = currentSample;
                rightBuffer[sample] = currentSample;
            }

            currentFrequency = localTargetFrequency;
        }
        else
        {
            for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
            {
                auto currentSample = (float) std::sin (currentAngle);
                currentAngle += angleDelta;
                leftBuffer[sample]  = currentSample;
                rightBuffer[sample] = currentSample;
            }
        }

        auto localTargetLevel = targetLevel;
        bufferToFill.buffer->applyGainRamp (bufferToFill.startSample, bufferToFill.numSamples, currentLevel, localTargetLevel);
        currentLevel = localTargetLevel;
	}

private:
	double currentSampleRate = 0.0, currentAngle = 0.0, angleDelta = 0.0;
	double currentFrequency = 500.0, targetFrequency = 500.0;
	float currentLevel = 0.1f, targetLevel = 0.1f;
	int rotaryDiam = 100;

	LabeledSlider frequency{ "Frequency" };
	LabeledSlider level{ "Level" };
	LabeledSlider dummy1{ "Dummy 1" };
	LabeledSlider dummy2{ "Dummy 2" };
	LabeledSlider dummy3{ "Dummy 3" };
	LabeledSlider dummy4{ "Dummy 4" };
	LabeledSlider dummy5{ "Dummy 5" };
	LabeledSlider dummy6{ "Dummy 6" };
	LabeledSlider dummy7{ "Dummy 7" };
	LabeledSlider dummy8{ "Dummy 8" };
	LabeledSlider dummy9{ "Dummy 9" };

	OwnedArray<LabeledSlider> knobs;
	
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};

I’m now trying to figure out how to nest the classes.

In a standard panel layout you need things organized like this:

Ie. You need labeled knobs (which we have done) inside of labeled groups.

I think the best way to do this is using FlexBox in the manner we have, but extending it to an extra layer so the knobs within each Group are auto-organized, and the Groups are also auto-organized on the panel.

Here is what I’m thinking about how to do this:

  • Define the LabeledSlider class as part of a larger LabeledGroup class.
  • All sliders will then be assigned a label by LabeledSlider (eg. Attack) and also belong to a bigger LabeledGroup (eg. ADSR).
  • A FlexBox design within the LabeledGroup class definition will capture all LabeledSliders from that LabeledGroup into an array and perform Flex arrangement of them.
  • Using the standard method we’ve established, an ownedarray in MainContentComponent can then capture all the LabeledGroups and organize them with FlexBox across the screen.

Does this make sense? I’m fairly certain it should work as intended, no?

The goal again is to have an entire panel of flexible groups of knobs that are sectioned out by groupings like “ADSR”, “LFO”, etc. with no need for specific locations of anything to be set.

I have been reading C++ tutorials about nested Classes for the past few hours. But I can’t quite figure it out.

Here’s the my crude attempt at what I’m thinking, although it doesn’t work in this current form:

class LabeledGroup : public GroupComponent

{

public:

	LabeledGroup(const String& name)
	{
		setText(name);
		setTextLabelPosition(Justification::centredTop);

		
		class LabeledSlider : public GroupComponent
		{
		public:
			LabeledSlider(const String& name)
			{
				setText(name);
				setTextLabelPosition(Justification::centredTop);
				addAndMakeVisible(slider);
			}

			void resized() override
			{
				slider.setBounds(getLocalBounds().reduced(10));
			}

			Slider slider
			{
				Slider::RotaryHorizontalVerticalDrag, Slider::TextBoxBelow
			};

		};

	}

private:
	void resized() override
	{

		OwnedArray<LabeledSlider> knobs;

		//==============================================================================
		FlexBox knobBox;
		knobBox.flexWrap = FlexBox::Wrap::wrap;
		knobBox.justifyContent = FlexBox::JustifyContent::flexStart;
		knobBox.alignContent = FlexBox::AlignContent::flexStart;

		for (auto* k : knobs)
			knobBox.items.add(FlexItem(*k).withMinHeight(80.0f).withMinWidth(80.0f).withFlex(1));

		//==============================================================================
		FlexBox fb;
		fb.flexDirection = FlexBox::Direction::column;
		fb.items.add(FlexItem(knobBox).withFlex(2.5));
		fb.performLayout(getLocalBounds().toFloat());

	}

};




class MainContentComponent : public AudioAppComponent
{
public:
    MainContentComponent()
	
    {
		addAndMakeVisible(group1);

		LabeledSlider* control = new LabeledSlider("Frequency");
		control->slider.setRange(20.0, 20000.0);
		control->slider.setSkewFactorFromMidPoint(500.0);
		control->slider.setNumDecimalPlacesToDisplay(1);
		control->slider.setValue(currentFrequency, dontSendNotification);
		control->slider.onValueChange = [this] { targetFrequency = frequency.slider.getValue(); };
		control->slider.setTextBoxStyle(Slider::TextBoxBelow, false, 100, 20);
		control->slider.setRange(50.0, 5000.0);
		control->slider.setSkewFactorFromMidPoint(500.0);
		control->slider.setNumDecimalPlacesToDisplay(1);
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Level");
		control->slider.setRange(0.0, 1.0);
		control->slider.onValueChange = [this] { targetLevel = (float)level.slider.getValue(); };
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy1");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy2");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy3");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy4");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy5");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy6");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy7");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy8");
		addAndMakeVisible(knobs.add(control));

		control = new LabeledSlider("Dummy9");
		addAndMakeVisible(knobs.add(control));

    setSize (600, 600);
    setAudioChannels (0, 2); // no inputs, two outputs
    }

    ~MainContentComponent()
    {
        shutdownAudio();
    }

    void resized() override
    {
			
		//==============================================================================
		FlexBox knobBox;
		knobBox.flexWrap = FlexBox::Wrap::wrap;
		knobBox.justifyContent = FlexBox::JustifyContent::flexStart;
		knobBox.alignContent = FlexBox::AlignContent::flexStart;

		for (auto* k : knobgroup)
			knobBox.items.add(FlexItem(*k).withMinHeight(80.0f).withMinWidth(80.0f).withFlex(1));

		//==============================================================================
		FlexBox fb;
		fb.flexDirection = FlexBox::Direction::column;
		fb.items.add(FlexItem(knobBox).withFlex(2.5));
		fb.performLayout(getLocalBounds().toFloat());
	

    }

    inline void updateAngleDelta()
    {
        auto cyclesPerSample = currentFrequency / currentSampleRate;
        angleDelta = cyclesPerSample * 2.0 * MathConstants<double>::pi;
    }

    void prepareToPlay (int, double sampleRate) override
    {
        currentSampleRate = sampleRate;
        updateAngleDelta();
    }

    void releaseResources() override {}

    void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override
    {
        auto* leftBuffer  = bufferToFill.buffer->getWritePointer (0, bufferToFill.startSample);
        auto* rightBuffer = bufferToFill.buffer->getWritePointer (1, bufferToFill.startSample);

        auto localTargetFrequency = targetFrequency;

        if (targetFrequency != currentFrequency)
        {
            auto frequencyIncrement = (targetFrequency - currentFrequency) / bufferToFill.numSamples;

            for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
            {
                auto currentSample = (float) std::sin (currentAngle);
                currentFrequency += frequencyIncrement;
                updateAngleDelta();
                currentAngle += angleDelta;
                leftBuffer[sample]  = currentSample;
                rightBuffer[sample] = currentSample;
            }

            currentFrequency = localTargetFrequency;
        }
        else
        {
            for (auto sample = 0; sample < bufferToFill.numSamples; ++sample)
            {
                auto currentSample = (float) std::sin (currentAngle);
                currentAngle += angleDelta;
                leftBuffer[sample]  = currentSample;
                rightBuffer[sample] = currentSample;
            }
        }

        auto localTargetLevel = targetLevel;
        bufferToFill.buffer->applyGainRamp (bufferToFill.startSample, bufferToFill.numSamples, currentLevel, localTargetLevel);
        currentLevel = localTargetLevel;
	}

private:
	double currentSampleRate = 0.0, currentAngle = 0.0, angleDelta = 0.0;
	double currentFrequency = 500.0, targetFrequency = 500.0;
	float currentLevel = 0.1f, targetLevel = 0.1f;
	int rotaryDiam = 100;
		
	LabeledSlider frequency{ "Frequency" };
	LabeledSlider level{ "Level" };
	LabeledSlider dummy1{ "Dummy 1" };
	LabeledSlider dummy2{ "Dummy 2" };
	LabeledSlider dummy3{ "Dummy 3" };
	LabeledSlider dummy4{ "Dummy 4" };
	LabeledSlider dummy5{ "Dummy 5" };
	LabeledSlider dummy6{ "Dummy 6" };
	LabeledSlider dummy7{ "Dummy 7" };
	LabeledSlider dummy8{ "Dummy 8" };
	LabeledSlider dummy9{ "Dummy 9" };

	LabeledGroup group1{ "Group 1" };

	OwnedArray<LabeledGroup> knobgroup;
  JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)

Main Points:

  • I don’t know how to name the ownedarray within the LabeledGroup so that it picks up any LabeledSliders of a given group (it is giving me an “unidentified” error on that at present).
  • I don’t know how to define the knobs as LabeledSliders that belong to a given LabeledGroup.

If you’re still feeling at all generous and I haven’t completely worn you out yet, any help? Is this the right idea or wrong idea for how to do this?

I think it’s a valuable goal to try to achieve and I think it should be possible.

Thanks

1 Like