Recreating parameter controls from "Sound Stage"

Does anybody have a good reference for a parameter implementation that follows the style used in the “Sound Stage” module? I.e., a text label describes the parameter name, and another describes the selected value. When clicked the parameter scrubs like a knob would, but updates the text label with the value of the parameter. I’m sure it’s relatively simple.

Screenshot 2025-01-01 at 6.03.38 PM copy

You need to use a text display widget to do that, you can use digitalDisplay.hpp as a starting point. It’s a little bit complex honestly, but the code is at least easy to copy/paste. You can find similar code in my Arrange module for the Stage knob (cv funk).

#include “digital_display.hpp”

Then when you initialize your vars:

DigitalDisplay* digitalDisplay = nullptr;

In the main module widget you place it like this:

    // Configure and add the first digital display
    DigitalDisplay* digitalDisplay = new DigitalDisplay();
    digitalDisplay->fontPath = asset::plugin(pluginInstance, "res/fonts/DejaVuSansMono.ttf");
    digitalDisplay->box.pos = Vec(41.5 + 25, 34); // Position on the module
    digitalDisplay->box.size = Vec(100, 18); // Size of the display
    digitalDisplay->text = "Stage : Max"; // Initial text
    digitalDisplay->fgColor = nvgRGB(208, 140, 89); // White color text
    digitalDisplay->textPos = Vec(0, 15); // Text position
    digitalDisplay->setFontSize(16.0f); // Set the font size as desired
    addChild(digitalDisplay);

    if (module) {
        module->digitalDisplay = digitalDisplay; // Link the module to the display
    }

Then in Draw you update it like this:

    // Update Stage progress display
    if (module->digitalDisplay) {
        module->digitalDisplay->text =  std::to_string(module->currentStage + 1) + " / " + std::to_string(module->maxStages);
    }

And you also need to define the widget like this:

DigitalDisplay* createDigitalDisplay(Vec position, std::string initialValue) {
    DigitalDisplay* display = new DigitalDisplay();
    display->box.pos = position;
    display->box.size = Vec(50, 18);
    display->text = initialValue;
    display->fgColor = nvgRGB(208, 140, 89); // Gold color text
    display->fontPath = asset::plugin(pluginInstance, "res/fonts/DejaVuSansMono.ttf");
    display->setFontSize(14.0f);
    return display;
}
1 Like

This was my initial approach, but ultimately I was able to come up with something that integrated with the standard param functionality of VCV:

// A text based knob parameter.
//
// Copyright 2025 Arhythmetic Units
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

#include <string>
#include "rack.hpp"

#ifndef ARHYTHMETIC_UNITS_FOURIER_RACK_EXTENSIONS_TEXT_KNOB_HPP_
#define ARHYTHMETIC_UNITS_FOURIER_RACK_EXTENSIONS_TEXT_KNOB_HPP_

/// @brief A knob that renders the label and value as text on the widget.
struct TextKnob : app::Knob {
    struct {
        /// The text for the label.
        std::string text = "";
        /// The color of the font for the label.
        NVGcolor color = {{{1.f, 1.f, 1.f, 1.f}}};
        /// The size for the font
        float font_size = 10.f;
        /// The line height for the font.
        float line_height = 11.f;
        /// the font for rendering text on the display
        std::shared_ptr<Font> font = APP->window->loadFont(
            asset::plugin(plugin_instance, "res/Font/Arial/Bold.ttf")
        );
    } label, value;  // The label and value text.

    /// @brief Initialize a new text knob.
    TextKnob() {
        // Set the expected size of the widget from Sketch
        setSize(Vec(60, 30));
        // Set the range of the knob (mocks a Rogan knob)
        minAngle = 0.f * M_PI;
        maxAngle = 1.66f * M_PI;
        // Set the default colors for the label and value.
        label.color = {{{0.f / 255.f, 90.f / 255.f, 11.f / 255.f, 1.f}}};
        value.color = {{{0.f / 255.f, 215.f / 255.f, 26.f / 255.f, 1.f}}};
    }

    /// @brief Respond to changes of the parameter.
    void onChange(const ChangeEvent& e) override {
        auto param = getParamQuantity();
        if (param) {
            label.text = param->getLabel();
            for (char &c : label.text)
                c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
            value.text = param->getDisplayValueString() + param->getUnit();
        }
        app::Knob::onChange(e);
    }

    /// @brief Draw the layer on the screen.
    void drawLayer(const DrawArgs& args, int layer) override {
        if (layer == 1) {
            // render the label.
            nvgFontSize(args.vg, label.font_size);
            nvgFontFaceId(args.vg, label.font->handle);
            nvgFillColor(args.vg, label.color);
            nvgTextLineHeight(args.vg, label.line_height);
            nvgTextAlign(args.vg, NVG_ALIGN_TOP | NVG_ALIGN_CENTER);
            nvgText(args.vg, box.size.x / 2.f, 0, label.text.c_str(), NULL);
            // Render the value.
            nvgFontSize(args.vg, value.font_size);
            nvgFontFaceId(args.vg, value.font->handle);
            nvgFillColor(args.vg, value.color);
            nvgTextLineHeight(args.vg, value.line_height);
            nvgTextAlign(args.vg, NVG_ALIGN_TOP | NVG_ALIGN_CENTER);
            nvgText(args.vg, box.size.x / 2.f, 18, value.text.c_str(), NULL);
        }
        app::Knob::drawLayer(args, layer);
    }
};

#endif  // ARHYTHMETIC_UNITS_FOURIER_RACK_EXTENSIONS_TEXT_KNOB_HPP_

This pretty much directly clones the VCV solution, though the static text label for the parameter is not rendered in the module library at the moment since it gets set by configParam. I’m not really sure how to fix that at the moment.

1 Like

You could set a dummy value that is drawn only for show when module is not present.

Oh duh, that worked perfectly!

1 Like