Skip to content

Feature: panic skin buttons - #269

Open
kartun83 wants to merge 13 commits into
dsp56300:mainfrom
kartun83:feature/panic-skin-buttons
Open

Feature: panic skin buttons#269
kartun83 wants to merge 13 commits into
dsp56300:mainfrom
kartun83:feature/panic-skin-buttons

Conversation

@kartun83

@kartun83 kartun83 commented Jun 2, 2026

Copy link
Copy Markdown

Expose panic actions to main skin via button IDs

  • Moves panic logic (panicAllNotesOff, panicNoteOffEveryNote, panicRebootDevice) from SettingsMidi into Processor — single authoritative location
  • SettingsMidi delegates to the new methods (no behaviour change for the settings page)
  • Registers click handlers in VirusEditor::create( for btPanicAllNotesOff, btPanicNoteOffEveryNote, btPanicReboot — silently skipped when absent, so existing skins are unaffected

Skin usage — no params, no Lua needed:

<button id="btPanicAllNotesOff"      class="jucePos juceButton" isToggle="0" style="...">!</button>
<button id="btPanicNoteOffEveryNote" class="jucePos juceButton" isToggle="0" style="...">!</button>
<button id="btPanicReboot"           class="jucePos juceButton" isToggle="0" style="...">!</button>

Why this matters

Stuck notes and runaway reverb/delay tails are a real problem during live performance and studio sessions. On the original Virus TI hardware, Access provided a dedicated panic shortcut (MONO+SYNC held simultaneously) for exactly this reason.

In the emulator the situation was worse than it might appear:

  • CC123 (All Notes Off) from a hardware controller already works — it passes straight through to the firmware. But it does nothing to reverb/delay tails, which live in DSP memory and keep running regardless of voice state. Also, users without hardware controller have to dig their way through the settings for pulling killswitch.
  • The settings page panic buttons were the only way to truly stop sound — requiring two or three clicks through a menu, far too slow when something goes wrong live.
  • Reboot Device is the only option that actually silences everything — it recreates the DSP from scratch, equivalent to what the hardware MONO+SYNC shortcut likely does internally.
  • This PR makes all three panic actions available as first-class skin buttons, so a skin developer can place an immediately accessible panic button directly on the instrument surface — the same way hardware synths expose it as a front-panel control. No new plugin parameters, no automation lanes, no MIDI learn slots consumed. Skins that don't use the buttons pay no cost.

kartun83 added 13 commits May 31, 2026 23:10
build_mac.sh targeted the wrong CMake output directory (temp/cmake
instead of temp/cmake_macos) and called cpack as a bare command which
is not on PATH when CMake is installed as CMake.app on macOS. Fix both.

The SSE flag pairs in juceRmlUi were broken for macOS universal builds:
CMake was deduplicating the second -Xarch_x86_64, leaving a bare
-msse4.1 that clang rejects when compiling the arm64 slice. Wrap each
pair with SHELL: so CMake preserves them as single arguments.
Adds LunaSVG 3.5.0 as a submodule (source/3rdparty/lunasvg, including
its plutovg dependency) and enables the RmlUi SVG plugin via
RMLUI_SVG_PLUGIN=ON in source/3rdparty/CMakeLists.txt.

The fork's RmlUi (dsp56300/RmlUi, rmlui_multiinstance branch) had not
been updated for the multi-instance CoreInstance API introduced in
EMU-70, causing build failures when the SVG plugin is enabled. The
submodule is updated to a commit that brings the SVG plugin in line:

- ElementSVG constructor accepts CoreInstance& and forwards it to Element
- LoadSource() uses GetSystemInterface(GetCoreInstance()) and
  GetFileInterface(GetCoreInstance()), which are no longer static
- SVGPlugin holds a CoreInstance& reference and registers through
  core_instance.factory rather than the removed static Factory API
- SVG::Initialise() accepts and forwards CoreInstance&; Core.cpp updated

The submodule commit also adds two capabilities not present upstream:

Inline SVG: when src is absent, LoadSource() reconstructs the full SVG
document from the element's attributes and GetInnerRML(), so SVG markup
can be embedded directly in RML without an external file.

Dynamic updates via inner_rml: OnChildAdd/OnChildRemove set source_dirty
without calling DirtyLayout (which caused hangs during document teardown).
A subsequent attribute write on the element, e.g. a data-ts cache-buster,
triggers DirtyLayout via OnAttributeChange, scheduling a re-rasterize.
This lets Lua scripts update SVG content at runtime.
Two bugs in MidiClock::start() and process() caused the arpeggiator
to run out of phase with the host tempo (issues dsp56300#205, dsp56300#241).

Bug 1: wrong initial clock phase in start()

The formula:
  quarterPos = (ppqPos - floor(ppqPos + 1.0))
  m_clockTickPos = quarterPos * 24

produces values in [-24, 0). The tick-generation loop only fires when
m_clockTickPos crosses 0, so starting at -24 suppresses all ticks for
24 full tick periods — one entire beat. This is why the ARP was heard
as "wrong at the beginning" and appeared to catch up after one or two
bars.

The fix computes the fractional position within the current clock tick
period and keeps m_clockTickPos in [-1, 0):

  absoluteTicks  = ppqPos * 24
  fracTick       = absoluteTicks - floor(absoluteTicks)
  m_clockTickPos = fracTick - 1.0

Examples:
  ppqPos = 0.0  → fracTick = 0.0 → m_clockTickPos = -1.0 (fires after 1 tick period)
  ppqPos = 0.5  → fracTick = 0.0 → m_clockTickPos = -1.0 (at a tick boundary)
  ppqPos = 0.52 → fracTick = 0.48 → m_clockTickPos = -0.52 (fires after 0.52 tick period)

Bug 2: no loop or backward-seek detection

process() only called start() on the false→true transition of
_isPlaying. When the DAW looped back to an earlier position, ppqPos
jumped backward while m_isPlaying stayed true, so start() was never
called and the clock continued at the wrong phase indefinitely.

The fix tracks m_lastPpqPos and restarts the clock whenever ppqPos
drops by more than 0.5 beats during playback. 0.5 beats is well above
any normal per-buffer advance (e.g. 300 BPM / 4096 samples ≈ 0.28
beats max) while reliably catching all loop-back distances.
getTailLengthSeconds() was hardcoded to 0.0, causing DAWs to cut audio
immediately on stop/bounce — truncating reverb and delay tails that the
hardware produces after the last note-off.

The implementation reads the live patch parameters to compute a tail
length that reflects the current patch state, aligning with the actual
hardware behaviour:

Reverb tail: Reverb Time (page 110, index 4, range 0-127) is mapped
linearly to 0-16 seconds, matching the Virus TI hardware's documented
maximum reverb decay time. Contributes only when Reverb Send > 0,
consistent with the hardware — no signal routed to the reverb engine
means no tail.

Delay tail: computed as delayTime / (1 - feedback), capped at 30 s to
avoid the singularity at full feedback. Delay Time 0-127 maps to
0-1000 ms (free-running mode per the delayTime converter). Contributes
only when Delay Send > 0 for the same reason as reverb.

Supporting changes:
- getControllerConst() added to pluginLib::Processor as a protected
  const accessor that returns the controller pointer without triggering
  lazy creation, enabling const-correct reads from getTailLengthSeconds()
- Reverb/delay parameter name constants added to ParameterNames.h
Implements 10 pages of remote controls covering the complete Virus TI
signal flow: Oscillators, Filter, Filter Envelope, Amplifier, LFO 1,
LFO 2, Reverb, Delay, Arpeggiator, and Performance.

Each page exposes up to 8 parameters using the exact names from
parameterDescriptions_TI.json. Slots with no meaningful parameter are
left as nullptr, which CLAP permits.

OsTIrusProcessor inherits clap_juce_audio_processor_capabilities and
lives in osTIrusJucePlugin, which already has the CLAP headers on its
include path. The shared virusJucePlugin library therefore needs no
dependency on clap-juce-extensions, keeping it format-agnostic.

getControllerConst() (added previously to pluginLib::Processor) is used
in remoteControlsPageFill() to look up live AudioProcessorParameter
pointers by name without triggering lazy controller creation.
The clap-juce-extensions wrapper gates remote controls on
supportsRemoteControls() which defaults to false. Without overriding
it, implementRemoteControls() stays false and the host never learns
the extension is available, so pages never appear in the DAW.
Splits the mixed Oscillator and Filter pages into dedicated per-unit
pages, adds LFO 3, Chorus, Phaser, Filter Bank, Distortion, and EQ
pages to cover the full Virus TI signal path.

Page layout:
  Oscillator 1 / Oscillator 2 — each oscillator on its own page
  Filter 1 / Filter 2 — each filter on its own page with independent
    cutoff, resonance, env amount, keyfollow, mode
  Filter Envelope / Amplifier / LFO 1 / LFO 2 / LFO 3
  Chorus / Phaser / Reverb / Delay
  Filter Bank — type, mix, frequency, resonance + context-dependent slots
  Distortion — curve, intensity, mix, treble booster, high cut, tone,
    bass intensity, punch (all from the FX1 Patch Distortion section)
  EQ — low/mid/high gain and frequency, mid Q (7 params, 1 empty)
  Arpeggiator / Performance

Page table moved to VirusTIRemoteControls.h so OsTIrusProcessor.cpp
stays free of data; the header contains only constexpr definitions in
the virusTI namespace with no runtime cost.
…ture

When the user touches any control in the plugin UI, the host is told
which remote controls page that parameter belongs to, so hardware
controllers automatically switch to the relevant page.

Implementation:
- OsTIrusProcessor inherits juce::AudioProcessorListener and registers
  itself via addListener()/removeListener()
- A name→page reverse map is built at startup from the static page
  table in VirusTIRemoteControls.h (zero runtime overhead after init)
- audioProcessorParameterChangeGestureBegin() looks up the touched
  parameter's name in the map and calls suggestRemoteControlsPage()
  with the encoded (pageIndex << 8 | part) ID

The gesture signal covers mouse/touch interaction with the plugin UI.
Hardware controller gestures are not routed back by the current
clap-juce-extensions wrapper (CLAP_EVENT_PARAM_GESTURE_BEGIN has no
case in process_clap_event), so controller-only users can still
navigate pages manually on their hardware. This is a wrapper-level
limitation, not addressed here.
The "suggest remote controls page on gesture" feature can now be
enabled or disabled from the GUI Settings page under a dedicated
"CLAP Controller Integration" section.

The preference is stored in the per-installation config file
(juce::PropertiesFile via getConfig()), so it persists across
sessions and is independent of project state. It defaults to true.

Implementation:
- VirusProcessor::g_clapSuggestPageKey — shared constexpr config key,
  guarded by #ifdef HAS_CLAP_JUCE_EXTENSIONS so it only compiles in
  CLAP-capable builds
- OsTIrusProcessor::audioProcessorParameterChangeGestureBegin now
  reads the config before calling suggestRemoteControlsPage(); no-op
  when the preference is disabled
- SettingsGuiOsTIrus wires the new "btClapSuggestPage" checkbox to
  the config key; the CLAP section is hidden at compile time in builds
  without HAS_CLAP_JUCE_EXTENSIONS
- tus_settings_gui_OsTIrus.rml adds the CLAP section with the checkbox

Headless builds: the config key is still read and honoured; no UI is
shown since there is no editor in headless mode.
The animation guard was hiding the entire settings root when the
current skin has no logo element (e.g. TrancyTI), which meant the
CLAP Controller Integration section was never shown.

Wrap the Animation block in its own <div id="animationSection"> so
only that section is hidden when supportsLogoAnimation() is false.
The CLAP section is now always reachable regardless of skin.
The settings UI lives in virusJucePlugin (shared static lib) which
never gets HAS_CLAP_JUCE_EXTENSIONS defined — only the final CLAP
plugin target gets it. So the #ifdef always took the #else branch,
hiding the CLAP section for every build.

Fix: the UI wiring only needs a config string key, not any CLAP API
types, so it needs no #ifdef at all. Remove the guard entirely from
SettingsGuiOsTIrus and g_clapSuggestPageKey in VirusProcessor.h.
The actual CLAP API call (suggestRemoteControlsPage) remains guarded
by HAS_CLAP_JUCE_EXTENSIONS in OsTIrusProcessor where it belongs.
Moves the three panic implementations (All Notes Off, Note Off per note,
Reboot Device) from SettingsMidi into Processor so they can be called
from both the settings page and the main skin.

Registers click handlers in VirusEditor::create() for btPanicAllNotesOff,
btPanicNoteOffEveryNote and btPanicReboot — silently skipped when absent,
so skins without them are unaffected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant