Search guides and the APICtrl K
  1. Docs
  2. Guides

The native editor

The interface the SDK draws itself: no browser process, about two megabytes, the same on every machine.

On this page

The one you get for free

A plugin that supplies no web page (no SONORE_UI_HTML) opens the generic native editor: a row per parameter, folded into sections by each parameter’s group, a keyboard along the bottom for an instrument, and the preset bar along the top where the host gives the plugin its presets. Nothing to write.

PluginDescriptor::editor overrides the choice: sonore::EditorKind::Native skips a page the plugin has, sonore::EditorKind::Web asks for a webview without one. PluginDescriptor::editorLimits sets the smallest and largest size (minWidth, minHeight, maxWidth, maxHeight) and whether either direction resizes.

On Linux the native editor needs the X11 development headers at build time (libx11-dev or your distribution’s equivalent); a build without them uses the webview instead.

One of your own

Declare createEditor in SonoreDsp and return a sonore::gfx::AudioProcessorEditor. It is called on the main thread each time the host opens the editor; the window that holds it runs a 33 ms clock that calls editorTick() before each repaint, and destroys the editor when the host closes the window. The DSP outlives every editor it makes, so an editor may keep a reference to it; anything handed to the audio thread still crosses by atomics.

The widgets work in 0..1 and know nothing about parameters. An attachment binds one to a parameter through the sonore::gfx::EditorHost the wrapper passes in: sonore::gfx::SliderAttachment, sonore::gfx::ButtonAttachment (a two-state parameter) and sonore::gfx::ComboAttachment (a stepped one). A drag is one gesture – one automation edit and one undo step in the host – and sync() pulls the host’s value back, so the knob follows automation and preset loads.

C++Compiled and run in CI
#define SONORE_NUM_PARAMS 2
#include <sonore/dsp.h>
#include <sonore/gfx/lookandfeel_versions.h>
#include <sonore/gfx/plugin_editor.h>
#include <sonore/gfx/preset_bar.h>

#include <memory>
#include <string>

namespace gfx = sonore::gfx;

struct SonoreDsp {
  sonore::Smooth gainSm, panSm;

  void prepare(const sonore::ProcessSpec& spec) {
    gainSm.setup((float) spec.sampleRate, 20.0f);
    panSm.setup((float) spec.sampleRate, 20.0f);
    gainSm.snap(1.0f);
    panSm.snap(0.0f);
  }

  void process(sonore::AudioBlock<float>& io, const float* p) {
    if (io.getNumChannels() < 2) return;
    float* L = io.getChannelPointer(0);
    float* R = io.getChannelPointer(1);
    const float gain = sonore::dbToGain(p[0]);
    for (size_t i = 0; i < io.getNumSamples(); ++i) {
      const float g = gainSm.next(gain), pan = panSm.next(p[1]);
      L[i] *= g * (pan > 0.0f ? 1.0f - pan : 1.0f);
      R[i] *= g * (pan < 0.0f ? 1.0f + pan : 1.0f);
    }
  }

  // [main thread] The editor; defined below, once the parameter table exists.
  std::unique_ptr<gfx::AudioProcessorEditor> createEditor(const gfx::EditorHost& host);
};

#include <sonore/plugin.h>

static const sonore::ParamInfo kParamTable[SONORE_NUM_PARAMS] = {
    {"gain", "Gain", "dB", -24.0f, 12.0f, 0.0f},
    {"pan", "Pan", "", -1.0f, 1.0f, 0.0f},
};

class TrimEditor : public gfx::AudioProcessorEditor {
public:
  explicit TrimEditor(const gfx::EditorHost& host) : presets_(host.presets) {
    // Set once here, inherited by every child: a scheme change is one call.
    look_.setColourScheme(gfx::LookAndFeel_V4::getMidnightColourScheme());
    setLookAndFeel(&look_);

    for (int i = 0; i < SONORE_NUM_PARAMS; ++i) {
      Knob& k = knobs_[i];
      k.name.setText(kParamTable[i].label);
      k.name.setJustify(gfx::Justify::Centred);
      k.value.setJustify(gfx::Justify::Centred);
      addChild(&k.name);
      addChild(&k.slider);
      addChild(&k.value);
      k.binding.reset(new gfx::SliderAttachment(k.slider, kParamTable[i], i, host));
      // The text under the knob is the host's own text for the value.
      gfx::Label* value = &k.value;
      k.binding->onDisplayChanged = [value](const std::string& text) { value->setText(text); };
      k.binding->sync();
    }
    // Null where the format offers no presets: then there is no bar.
    if (presets_.manager()) addChild(&presets_);
    setSize(360.0f, 240.0f); // the wrapper then gives it the host's size
  }

  void editorTick(double) override {
    for (Knob& k : knobs_) k.binding->sync(); // automation, a preset the host loaded
    presets_.tick();
  }

  void paint(gfx::Graphics& g) override { g.fillAll(lookAndFeel().background()); }

  void resized() override {
    const gfx::Rect r = localBounds();
    float top = 0.0f;
    if (presets_.manager()) {
      presets_.setBounds({0.0f, 0.0f, r.w, gfx::PresetBar::kHeight});
      top = gfx::PresetBar::kHeight;
    }
    const float w = r.w / SONORE_NUM_PARAMS;
    for (int i = 0; i < SONORE_NUM_PARAMS; ++i) {
      const float x = w * (float) i;
      knobs_[i].name.setBounds({x, top + 8.0f, w, 20.0f});
      knobs_[i].slider.setBounds({x + 16.0f, top + 32.0f, w - 32.0f, r.h - top - 72.0f});
      knobs_[i].value.setBounds({x, r.h - 36.0f, w, 20.0f});
    }
  }

private:
  struct Knob {
    gfx::Label name, value;
    gfx::Slider slider{gfx::Slider::Style::Rotary};
    std::unique_ptr<gfx::SliderAttachment> binding;
  };
  // Declared first, destroyed last: the children draw with it.
  gfx::LookAndFeel_V4 look_;
  gfx::PresetBar presets_;
  Knob knobs_[SONORE_NUM_PARAMS];
};

inline std::unique_ptr<gfx::AudioProcessorEditor> SonoreDsp::createEditor(const gfx::EditorHost& host) {
  return std::unique_ptr<gfx::AudioProcessorEditor>(new TrimEditor(host));
}

static const sonore::PluginDescriptor kDesc = {
    "com.mycompany.trimeditor", "Trim Editor", "My Company", "1.0.0",
    "A gain and pan with an editor of its own.", "",
    false, kParamTable, SONORE_NUM_PARAMS,
};

#include <sonore/formats.h>

Widgets

gfx/widgets.h has sonore::gfx::Slider (rotary, linear and bar styles), sonore::gfx::Button, sonore::gfx::Label and sonore::gfx::ComboBox; the other headers under gfx/ add text fields, tables, tree views, menus and a menu bar, toolbars, a colour selector, a code editor, an on-screen keyboard, images (PNG, JPEG, GIF, SVG through sonore::gfx::Drawable) and layout helpers (sonore::gfx::FlexBox, sonore::gfx::Grid). Each header’s comment says what it is for; the API reference lists them under Native GUI.

A ComboBox opens its list through the editor: set box.onOpenMenu = [this](gfx::ComboBox& b) { openComboMenu(b); };, and the window drops it down as a real menu.

LookAndFeel

Appearance lives in a sonore::gfx::LookAndFeel, separately from behaviour. setLookAndFeel() on a widget applies to it and everything inside it. sonore::gfx::LookAndFeel_V4 draws from nine colours with four built-in schemes (getDarkColourScheme(), getMidnightColourScheme(), getGreyColourScheme(), getLightColourScheme()); for a look of your own, derive from LookAndFeel and override the draw... methods you want – drawRotarySlider, drawButton, drawLinearSlider and the rest.

The preset bar

sonore::gfx::PresetBar is the strip the generic editor shows: the preset’s name with a mark when the sound has changed since it was loaded, previous and next, Save, and a menu with Save As, Rename, Delete, Import, Export and Show Folder. Construct it with host.presets and call tick() each frame, as above; an editor with an undo history can make each load one undo step through wrapLoad. Presets covers what it manages.

Where it appears

CLAP, VST3 and AU open the editor inside the host’s window. The standalone application and an LV2 interface show the webview instead. The showcase example (sdk/examples/showcase/) is a larger editor: menus, a code editor, an SVG logo, a colour selector and the APVTS attachments; sdk/tests/showcase_test.cpp drives it with no window at all, because an editor is a widget and renders into a bitmap.

↑↓ to move↵ to openEvery public name in the headers, and every guide