Feature: panic skin buttons - #269
Open
kartun83 wants to merge 13 commits into
Open
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Expose panic actions to main skin via button IDs
panicAllNotesOff,panicNoteOffEveryNote,panicRebootDevice) fromSettingsMidiintoProcessor— single authoritative locationSettingsMididelegates to the new methods (no behaviour change for the settings page)VirusEditor::create(forbtPanicAllNotesOff,btPanicNoteOffEveryNote,btPanicReboot— silently skipped when absent, so existing skins are unaffectedSkin usage — no params, no Lua needed:
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: