From e586b5e3dbc746e70e66e8bcc6d88f420be63a4b Mon Sep 17 00:00:00 2001 From: Buzz Burrowes Date: Mon, 24 Aug 2026 18:09:50 -0400 Subject: [PATCH 1/7] Added a new MidiTranspose script. Also cleaned up ScriptNode a bit to make it a little easier to add new built-in scripts. --- scripts/miditranspose.lua | 89 +++++++++++++++++++++++++++++++++ src/nodes/scriptnode.cpp | 100 ++++++++++++++------------------------ src/nodes/scriptnode.hpp | 2 +- 3 files changed, 126 insertions(+), 65 deletions(-) create mode 100644 scripts/miditranspose.lua diff --git a/scripts/miditranspose.lua b/scripts/miditranspose.lua new file mode 100644 index 0000000000..5201ed1b46 --- /dev/null +++ b/scripts/miditranspose.lua @@ -0,0 +1,89 @@ +--- MIDI Transposer. +-- +-- This is a MIDI filter which shifts the note number of all Note On/Off +-- messages by a specified number of semitones. Set the transpose parameter +-- to '0' to bypass the filter. +-- +-- @script transpose +-- @type DSP +-- @license GPL v3 +-- @author Buzz Burrowes + +local io = require ('io') +local midiBuffer = require ('el.MidiBuffer') +local midi = require ('el.midi') +local script = require ('el.script') +local round = require ('el.round') + +local lastSemitones = 0 +local lastMidiChannelSeen = 1 + +-- Buffer to render filtered output +local output = midiBuffer.new() + +local function layout() + return { + audio = { 0, 0 }, + midi = { 1, 1 }, + control = {{ + { + name = "Transpose", + symbol = "transpose", + min = -24, + max = 24, + default = 0 + } + }} + } +end + +-- prepare for rendering +local function prepare() + -- reserve 128 bytes of memory and clear the output buffer + output:reserve (128) + output:clear() +end + +local function process (_, m, p) + -- Get MIDI input buffer from the MidiPipe + local input = m:get (1) + + -- Get the transpose amount from the parameter array, and round to integer + local semitones = round.integer (p[1]) + + output:clear() + + -- Send an allNotesOff message is the transposition has changed + if semitones ~= lastSemitones then + output:insertPacked (midi.controller (lastMidiChannelSeen, 123, 0), 0) + lastSemitones = semitones + end + + -- For each input message, shift the note number if it's a note on/off + for msg, frame in input:messages() do + if semitones ~= 0 and (msg:isNoteOn() or msg:isNoteOff()) then + local note = msg:note(msg) + semitones + -- clamp to valid MIDI note range + if note < 0 then note = 0 end + if note > 127 then note = 127 end + msg:setNote (note) + lastMidiChannelSeen = msg:channel() + end + output:insert (msg, frame) + end + + -- DSP scripts use replace processing, so swap in the rendered output + input:swap (output) +end + +return { + type = 'DSP', + layout = layout, + parameters = parameters, + prepare = prepare, + process = process, + release = release +} + +-- SPDX-FileCopyrightText: Copyright (C) Kushview, LLC. +-- SPDX-License-Identifier: GPL-3.0-or-later diff --git a/src/nodes/scriptnode.cpp b/src/nodes/scriptnode.cpp index e62f53195d..66cd613fd6 100644 --- a/src/nodes/scriptnode.cpp +++ b/src/nodes/scriptnode.cpp @@ -237,86 +237,58 @@ void ScriptNode::setParameter (int index, float value) } //============================================================================== +struct BuiltInScripts +{ + const char* name; + const char* dspScript; + const int dspSize; + const char* uiScript; + const int uiSize; +}; + +const BuiltInScripts builtInScripts[] = +{ + {"Amp", scripts::amp_lua, scripts::amp_luaSize, scripts::ampui_lua, scripts::ampui_luaSize}, + {"Channelizer", scripts::channelize_lua, scripts::channelize_luaSize, "", 0}, + {"Spoton Scale Chooser", scripts::spontonchordchooser_lua, scripts::spontonchordchooser_luaSize, "", 0}, + {"MIDI Timecode (MTC) Generator", scripts::mtc_generator_lua, scripts::mtc_generator_luaSize, "", 0}, + {"Value", scripts::dial_lua, scripts::dial_luaSize, "", 0}, + {"MIDI CC", scripts::midicc_lua, scripts::midicc_luaSize, "", 0}, + {"Tremolo", scripts::tremolo_lua, scripts::tremolo_luaSize, "", 0}, + {"Test Tone", scripts::testtone_lua, scripts::testtone_luaSize, "", 0}, + {"MIDI Transpose", scripts::miditranspose_lua, scripts::miditranspose_luaSize, "", 0} +}; + +int ScriptNode::getNumPrograms() const +{ + return std::size(builtInScripts); +} + const String ScriptNode::getProgramName (int index) const { if (! juce::isPositiveAndBelow (index, getNumPrograms())) return {}; - switch (index) - { - case 0: - return "Amp"; - break; - case 1: - return "Channelizer"; - break; - case 2: - return "Spoton Scale Chooser"; - break; - case 3: - return "MIDI Timecode (MTC) Generator"; - break; - case 4: - return "Value"; - break; - case 5: - return "MIDI CC"; - break; - case 6: - return "Tremolo"; - break; - case 7: - return "Test Tone"; - break; - } - - String name = TRANS ("Program"); - name << " " << int (index + 1); - return name; + return builtInScripts[index].name; } void ScriptNode::setCurrentProgram (int index) { if (! juce::isPositiveAndBelow (index, getNumPrograms())) return; + _program = index; String newDspCode, newUiCode; - switch (index) + newDspCode = String::fromUTF8 (builtInScripts[index].dspScript, builtInScripts[index].dspSize); + if (builtInScripts[index].uiSize > 0) + { + newUiCode = String::fromUTF8 (builtInScripts[index].uiScript, builtInScripts[index].uiSize); + } + else { - case 0: - newDspCode = String::fromUTF8 (scripts::amp_lua, scripts::amp_luaSize); - newUiCode = String::fromUTF8 (scripts::ampui_lua, scripts::ampui_luaSize); - break; - case 1: - newDspCode = String::fromUTF8 (scripts::channelize_lua, scripts::channelize_luaSize); - newUiCode.clear(); - break; - case 2: - newDspCode = String::fromUTF8 (scripts::spontonchordchooser_lua, scripts::spontonchordchooser_luaSize); - newUiCode.clear(); - break; - case 3: - newDspCode = String::fromUTF8 (scripts::mtc_generator_lua, scripts::mtc_generator_luaSize); - newUiCode.clear(); - break; - case 4: - newDspCode = String::fromUTF8 (scripts::dial_lua, scripts::dial_luaSize); - newUiCode.clear(); - break; - case 5: - newDspCode = String::fromUTF8 (scripts::midicc_lua, scripts::midicc_luaSize); - newUiCode.clear(); - break; - case 6: - newDspCode = String::fromUTF8 (scripts::tremolo_lua, scripts::tremolo_luaSize); - newUiCode.clear(); - break; - case 7: - newDspCode = String::fromUTF8 (scripts::testtone_lua, scripts::testtone_luaSize); - newUiCode.clear(); - break; + newUiCode.clear(); } dspCode.replaceAllContent (newDspCode); diff --git a/src/nodes/scriptnode.hpp b/src/nodes/scriptnode.hpp index ef3d3ccbba..b15403ac74 100644 --- a/src/nodes/scriptnode.hpp +++ b/src/nodes/scriptnode.hpp @@ -45,7 +45,7 @@ class ScriptNode : public Processor, void setPlayHead (juce::AudioPlayHead*) override; //========================================================================== - int getNumPrograms() const override { return 8; } + int getNumPrograms() const override; int getCurrentProgram() const override { return _program; } const String getProgramName (int index) const override; void setCurrentProgram (int index) override; From de6fc22b0eed3dc17fb5f94402f58bc7522fce3a Mon Sep 17 00:00:00 2001 From: Buzz Burrowes Date: Wed, 26 Aug 2026 15:29:20 -0400 Subject: [PATCH 2/7] A whole new way of enumerating 'built-in' lua scripts to make it easier to add them. This may be a problem if the order of these scripts in the preset / program list needs to remain the same! I think it is OK since element saves the actual text of each script in each script node in the session file... not just a program number. This means it kind of doesn't matter what the program number of a script was when it was selected for a script node. --- scripts/amp.lua | 3 +- scripts/channelize.lua | 3 +- scripts/dial.lua | 3 +- scripts/midicc.lua | 3 +- scripts/miditranspose.lua | 3 +- scripts/mtc_generator.lua | 3 +- scripts/spontonchordchooser.lua | 1 + scripts/testtone.lua | 3 +- scripts/tremolo.lua | 3 +- src/nodes/scriptnode.cpp | 34 +---- src/scripting/scriptregistry.cpp | 248 +++++++++++++++++++++++++++++++ src/scripting/scriptregistry.hpp | 88 +++++++++++ 12 files changed, 360 insertions(+), 35 deletions(-) create mode 100644 src/scripting/scriptregistry.cpp create mode 100644 src/scripting/scriptregistry.hpp diff --git a/scripts/amp.lua b/scripts/amp.lua index ead1ecb650..e46f1c3f2e 100644 --- a/scripts/amp.lua +++ b/scripts/amp.lua @@ -50,7 +50,8 @@ end return { type = 'DSP', layout = amp_layout, - process = amp_process + process = amp_process, + dspName = 'Amp' } -- SPDX-FileCopyrightText: Copyright (C) Kushview, LLC. diff --git a/scripts/channelize.lua b/scripts/channelize.lua index e53df90745..8937e9083b 100644 --- a/scripts/channelize.lua +++ b/scripts/channelize.lua @@ -64,7 +64,8 @@ return { parameters = parameters, prepare = prepare, process = process, - release = release + release = release, + dspName = 'Channelizer' } -- SPDX-FileCopyrightText: Copyright (C) Kushview, LLC. diff --git a/scripts/dial.lua b/scripts/dial.lua index be16d3b959..6436b4f559 100644 --- a/scripts/dial.lua +++ b/scripts/dial.lua @@ -25,7 +25,8 @@ end return { type = 'DSP', layout = layout, - process = process + process = process, + dspName = 'Value' } -- SPDX-FileCopyrightText: Copyright (C) Kushview, LLC. diff --git a/scripts/midicc.lua b/scripts/midicc.lua index 77776bfe6e..7f815a7437 100644 --- a/scripts/midicc.lua +++ b/scripts/midicc.lua @@ -59,7 +59,8 @@ return { type = 'DSP', layout = layout, prepare = prepare, - process = process + process = process, + dspName = 'MIDI CC' } -- SPDX-FileCopyrightText: Copyright (C) Kushview, LLC. diff --git a/scripts/miditranspose.lua b/scripts/miditranspose.lua index 5201ed1b46..85abe59df4 100644 --- a/scripts/miditranspose.lua +++ b/scripts/miditranspose.lua @@ -82,7 +82,8 @@ return { parameters = parameters, prepare = prepare, process = process, - release = release + release = release, + dspName = 'MIDI Transpose' } -- SPDX-FileCopyrightText: Copyright (C) Kushview, LLC. diff --git a/scripts/mtc_generator.lua b/scripts/mtc_generator.lua index cb489f4530..9dd861bcd1 100644 --- a/scripts/mtc_generator.lua +++ b/scripts/mtc_generator.lua @@ -73,7 +73,8 @@ return { type = 'DSP', layout = layout, prepare = prepare, - process = process + process = process, + dspName = 'MIDI Timecode (MTC) Generator' } -- SPDX-FileCopyrightText: Copyright (C) Kushview, LLC. diff --git a/scripts/spontonchordchooser.lua b/scripts/spontonchordchooser.lua index 667b6f83f5..3fcc5f737b 100644 --- a/scripts/spontonchordchooser.lua +++ b/scripts/spontonchordchooser.lua @@ -203,6 +203,7 @@ return { type = 'DSP', layout = layout, process = process, + dspName = 'Spoton Scale Chooser' } -- SPDX-FileCopyrightText: Copyright (C) Lokki. diff --git a/scripts/testtone.lua b/scripts/testtone.lua index 4a571e7074..a6788cbc5d 100644 --- a/scripts/testtone.lua +++ b/scripts/testtone.lua @@ -65,7 +65,8 @@ return { type = 'DSP', layout = layout, prepare = prepare, - process = process + process = process, + dspName = 'Test Tone' } -- SPDX-FileCopyrightText: Copyright (C) Kushview, LLC. diff --git a/scripts/tremolo.lua b/scripts/tremolo.lua index 6d1d0c417a..f14aeafbe6 100644 --- a/scripts/tremolo.lua +++ b/scripts/tremolo.lua @@ -67,7 +67,8 @@ return { type = 'DSP', layout = layout, prepare = prepare, - process = process + process = process, + dspName = 'Tremolo' } -- SPDX-FileCopyrightText: Copyright (C) Kushview, LLC. diff --git a/src/nodes/scriptnode.cpp b/src/nodes/scriptnode.cpp index 66cd613fd6..48b36e274a 100644 --- a/src/nodes/scriptnode.cpp +++ b/src/nodes/scriptnode.cpp @@ -13,6 +13,7 @@ #include "scripting/bindings.hpp" #include "scripting/dspscript.hpp" #include "scripting/scriptloader.hpp" +#include "scripting/scriptregistry.hpp" #define EL_LUA_DBG(x) // #define EL_LUA_DBG(x) DBG(x) @@ -237,31 +238,9 @@ void ScriptNode::setParameter (int index, float value) } //============================================================================== -struct BuiltInScripts -{ - const char* name; - const char* dspScript; - const int dspSize; - const char* uiScript; - const int uiSize; -}; - -const BuiltInScripts builtInScripts[] = -{ - {"Amp", scripts::amp_lua, scripts::amp_luaSize, scripts::ampui_lua, scripts::ampui_luaSize}, - {"Channelizer", scripts::channelize_lua, scripts::channelize_luaSize, "", 0}, - {"Spoton Scale Chooser", scripts::spontonchordchooser_lua, scripts::spontonchordchooser_luaSize, "", 0}, - {"MIDI Timecode (MTC) Generator", scripts::mtc_generator_lua, scripts::mtc_generator_luaSize, "", 0}, - {"Value", scripts::dial_lua, scripts::dial_luaSize, "", 0}, - {"MIDI CC", scripts::midicc_lua, scripts::midicc_luaSize, "", 0}, - {"Tremolo", scripts::tremolo_lua, scripts::tremolo_luaSize, "", 0}, - {"Test Tone", scripts::testtone_lua, scripts::testtone_luaSize, "", 0}, - {"MIDI Transpose", scripts::miditranspose_lua, scripts::miditranspose_luaSize, "", 0} -}; - int ScriptNode::getNumPrograms() const { - return std::size(builtInScripts); + return (int)ScriptRegistry::instance().getScripts().size(); } const String ScriptNode::getProgramName (int index) const @@ -269,7 +248,7 @@ const String ScriptNode::getProgramName (int index) const if (! juce::isPositiveAndBelow (index, getNumPrograms())) return {}; - return builtInScripts[index].name; + return ScriptRegistry::instance().getScripts()[index].name; } void ScriptNode::setCurrentProgram (int index) @@ -280,11 +259,12 @@ void ScriptNode::setCurrentProgram (int index) _program = index; String newDspCode, newUiCode; + const BuiltInScripts& scriptInfo = ScriptRegistry::instance().getScripts()[index]; - newDspCode = String::fromUTF8 (builtInScripts[index].dspScript, builtInScripts[index].dspSize); - if (builtInScripts[index].uiSize > 0) + newDspCode = String::fromUTF8 (scriptInfo.dspScript, scriptInfo.dspSize); + if (scriptInfo.uiSize > 0) { - newUiCode = String::fromUTF8 (builtInScripts[index].uiScript, builtInScripts[index].uiSize); + newUiCode = String::fromUTF8 (scriptInfo.uiScript, scriptInfo.uiSize); } else { diff --git a/src/scripting/scriptregistry.cpp b/src/scripting/scriptregistry.cpp new file mode 100644 index 0000000000..37db36f79b --- /dev/null +++ b/src/scripting/scriptregistry.cpp @@ -0,0 +1,248 @@ +// SPDX-FileCopyrightText: Copyright (C) Kushview, LLC. +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include "scriptregistry.hpp" +#include "luascripts.hpp" + +namespace element { + +namespace { + +// One raw embedded resource, resolved to its extension-stripped base name. +// `data`/`size` point directly at the static BinaryData buffer -- NOT +// necessarily null-terminated, so always paired with `size`. +struct RawResource +{ + std::string name; // e.g. "amp", "ampui", "channelize"... + const char* data; + int size; +}; + +std::string stripLuaExtension (const std::string& filename) +{ + static const std::string ext = ".lua"; + if (filename.size() > ext.size() + && filename.compare (filename.size() - ext.size(), ext.size(), ext) == 0) + return filename.substr (0, filename.size() - ext.size()); + return filename; +} + +bool endsWith (const std::string& s, const std::string& suffix) +{ + return s.size() >= suffix.size() + && s.compare (s.size() - suffix.size(), suffix.size(), suffix) == 0; +} + +std::string toUpperCopy (std::string s) +{ + for (auto& c : s) + c = static_cast (std::toupper (static_cast (c))); + return s; +} + +// Matches a the LAST line in the script containing ONLY "return {" (ignores +// whitespace). This is what anchors the start of the script's return block +// so the field scan below can't accidentally match an unrelated +// `type`/`dspName` local variable earlier in the file. +// +// NOTE: matched one line at a time via regex_match() rather than using the +// std::regex::multiline flag + ^/$ over the whole file -- MSVC's STL has +// never implemented std::regex::multiline (a long-standing gap versus +// libstdc++/libc++), so ^/$ are used here in their default, per-call meaning +// of "start/end of the string being matched", with that string being a +// single line. +const std::regex kReturnBlockStartLineRegex (R"(^[ \t]*return[ \t]*\{[ \t]*\r?$)"); + +// Matches e.g. type = 'DSP' or type="DSP" (either quote style, flexible whitespace). +const std::regex kTypeRegex (R"(\btype\s*=\s*['"]([^'"]+)['"])"); + +// Matches e.g. dspName = 'Amplifier' +const std::regex kDspNameRegex (R"(\bdspName\s*=\s*['"]([^'"]+)['"])"); + +/** Returns the substring of `source` starting right after the LAST line + containing only "return {" (see kReturnBlockStartLineRegex), or an empty + string if no such line is found. + + Scripts commonly contain earlier "return {" lines too (e.g. inside a + layout() helper function) -- only the final, module-level return block is + the one that actually declares this script's type/dspName, so every + matching line is checked and the last one found wins. +*/ +std::string extractReturnBlockRegion (const std::string& source) +{ + size_t pos = 0; + bool found = false; + size_t regionStart = 0; // valid only when found == true + + while (pos <= source.size()) + { + size_t newlinePos = source.find ('\n', pos); + std::string line = (newlinePos == std::string::npos) + ? source.substr (pos) + : source.substr (pos, newlinePos - pos); + + if (std::regex_match (line, kReturnBlockStartLineRegex)) + { + found = true; + regionStart = (newlinePos == std::string::npos) ? source.size() : newlinePos + 1; + // keep scanning -- do NOT return here, a later match should win + } + + if (newlinePos == std::string::npos) + break; + + pos = newlinePos + 1; + } + + return found ? source.substr (regionStart) : std::string(); +} + +/** Scans raw Lua source text for a `type = 'DSP'` declaration, but only + within the script's trailing return block (the text following a + stand-alone "return {" line) -- not anywhere else in the file. This is + what lets e.g. `local type = something` earlier in a script's body avoid + being mistaken for the return block's `type` field. + + NOTE: this is a lightweight text scan, not a real Lua parse -- it does + not execute the script. Given this codebase's convention of a single + return block at the very end of each script, anchoring on "return {" + is sufficient in practice. If that ever stops holding true, the robust + fix is to actually execute the chunk through sol2/lua_State and inspect + the returned table directly, rather than scanning text. +*/ +bool isDspScript (const std::string& source, std::string& outDspNameOverride) +{ + std::string region = extractReturnBlockRegion (source); + if (region.empty()) + return false; // no "return {" line found at all -- not a node script + + std::smatch typeMatch; + if (! std::regex_search (region, typeMatch, kTypeRegex)) + return false; + + if (toUpperCopy (typeMatch[1].str()) != "DSP") + return false; + + std::smatch nameMatch; + if (std::regex_search (region, nameMatch, kDspNameRegex)) + outDspNameOverride = nameMatch[1].str(); + + return true; +} + +} // namespace + +ScriptRegistry& ScriptRegistry::instance() +{ + // Function-local static: constructed thread-safely on first call, + // avoids static initialization order issues entirely. + static ScriptRegistry registry; + return registry; +} + +ScriptRegistry::ScriptRegistry() +{ + // 1. Pull every embedded resource out of the generated BinaryData table + // and resolve it to a base name (original filename minus ".lua"). + std::vector raw; + raw.reserve (static_cast (scripts::namedResourceListSize)); + + for (int i = 0; i < scripts::namedResourceListSize; ++i) + { + const char* resourceName = scripts::namedResourceList[i]; + + int dataSize = 0; + const char* data = scripts::getNamedResource (resourceName, dataSize); + if (data == nullptr) + continue; // malformed entry, skip rather than crash + + const char* originalFilename = scripts::getNamedResourceOriginalFilename (resourceName); + std::string baseName = stripLuaExtension (originalFilename != nullptr ? originalFilename + : resourceName); + + raw.push_back ({ std::move (baseName), data, dataSize }); + } + + // Index by file-derived name for O(1) lookups while pairing DSP <-> UI + // scripts and resolving UI companions below. This index intentionally + // uses filenames, not dspName overrides, since ui-companion filenames + // (e.g. "ampui") are matched against the DSP script's *file* name. + std::unordered_map indexByName; + indexByName.reserve (raw.size()); + for (size_t i = 0; i < raw.size(); ++i) + indexByName.emplace (raw[i].name, i); + + // 2. Determine which entries are actually "ui" companions of + // another real entry, so they don't also show up as standalone nodes. + std::vector isUiCompanion (raw.size(), false); + + for (size_t i = 0; i < raw.size(); ++i) + { + if (! endsWith (raw[i].name, "ui")) + continue; + + std::string baseName = raw[i].name.substr (0, raw[i].name.size() - 2); + if (baseName.empty()) + continue; + + auto it = indexByName.find (baseName); + if (it != indexByName.end() && it->second != i) + isUiCompanion[i] = true; + } + + // 3. Build the final entry list: everything that (a) isn't someone else's + // UI companion, and (b) declares `type = 'DSP'` in its return block, + // becomes a top-level BuiltInScripts entry, with its UI companion (if + // any) attached and its display name resolved (dspName override, or + // filename-derived name as the fallback). + names.reserve (raw.size()); // upper bound; guarantees c_str() stability below + scripts.reserve (raw.size()); + + for (size_t i = 0; i < raw.size(); ++i) + { + if (isUiCompanion[i]) + continue; + + std::string source (raw[i].data, static_cast (raw[i].size)); + + std::string dspNameOverride; + if (! isDspScript (source, dspNameOverride)) + continue; // not a DSP node script (e.g. a shared support module) -- skip + + names.push_back (dspNameOverride.empty() ? raw[i].name : dspNameOverride); + + const char* uiScript = nullptr; + int uiSize = 0; + + auto it = indexByName.find (raw[i].name + "ui"); + if (it != indexByName.end()) + { + uiScript = raw[it->second].data; + uiSize = raw[it->second].size; + } + + // dspSize/uiSize are const members, so the struct must be built in + // one aggregate-initialization step rather than default-constructed + // and assigned to afterward. + scripts.push_back (BuiltInScripts { names.back().c_str(), + raw[i].data, + raw[i].size, + uiScript, + uiSize }); + } +} + +const BuiltInScripts* ScriptRegistry::findByName (const char* name) const noexcept +{ + for (auto& s : scripts) + if (std::strcmp (s.name, name) == 0) + return &s; + + return nullptr; +} + +} // namespace element \ No newline at end of file diff --git a/src/scripting/scriptregistry.hpp b/src/scripting/scriptregistry.hpp new file mode 100644 index 0000000000..f6e0fcd42f --- /dev/null +++ b/src/scripting/scriptregistry.hpp @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: Copyright (C) Kushview, LLC. +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +namespace element { + +/** A built-in Lua script pair: a DSP script and its optional companion UI script. + + Naming convention: DSP resource is ".lua", UI resource (if present) + is "ui.lua" -- e.g. "amp.lua" pairs with "ampui.lua". + + A script is only exposed here if its source contains a top-level return + block declaring `type = 'DSP'`, e.g.: + + return { + type = 'DSP', + layout = amp_layout, + process = amp_process + } + + That return block may also optionally declare `dspName = '...'`, which + overrides the display name (otherwise the DSP resource's filename, minus + the .lua extension, is used). +*/ +struct BuiltInScripts +{ + const char* name; + const char* dspScript; + const int dspSize; + const char* uiScript; // nullptr if this script has no companion UI + const int uiSize; // 0 if uiScript is nullptr +}; + +/** Singleton registry of all built-in Lua scripts embedded into the binary via + juce_add_binary_data() (see scripts/CMakeLists.txt, NAMESPACE `scripts`) + that declare themselves as DSP script nodes. + + The script list is discovered entirely at runtime: + 1. Every embedded resource is enumerated from scripts::namedResourceList. + 2. Resources are paired up using the "" / "ui" naming + convention (the ui one becomes a companion, not its own entry). + 3. Each remaining candidate's source is scanned for a `type = 'DSP'` + return block; candidates without one, or whose `type` is something + else (e.g. view.lua's `type = 'View'`), are dropped entirely. + 4. If that return block also declares `dspName = '...'`, it is used as + the entry's display name; otherwise the filename-derived name is used. + + Nothing is hardcoded, so new scripts dropped into scripts/ are picked up + automatically without touching this class. + + Populated lazily on first access. Thread-safe by virtue of C++11 + function-local static initialization guarantees. +*/ +class ScriptRegistry +{ +public: + /** Returns the single shared instance, constructing it on first call. */ + static ScriptRegistry& instance(); + + /** All discovered built-in DSP scripts. */ + const std::vector& getScripts() const noexcept { return scripts; } + + /** Looks up a script by its display name (e.g. "amp", or its dspName + override if one was declared). Returns nullptr if not found. + */ + const BuiltInScripts* findByName (const char* name) const noexcept; + + // Non-copyable, non-movable: there is exactly one registry. + ScriptRegistry (const ScriptRegistry&) = delete; + ScriptRegistry& operator= (const ScriptRegistry&) = delete; + +private: + ScriptRegistry(); + ~ScriptRegistry() = default; + + std::vector scripts; + + // Owns the display-name strings that BuiltInScripts::name points into. + // Capacity is reserved up front in the constructor so push_back never + // reallocates and invalidates c_str(). + std::vector names; +}; + +} // namespace element \ No newline at end of file From 119aa66b91e86edd2071ae54094e3bbd1adcad31 Mon Sep 17 00:00:00 2001 From: Buzz Burrowes Date: Fri, 28 Aug 2026 19:48:59 -0400 Subject: [PATCH 3/7] A few more tweaks to script registry --- scripts/CMakeLists.txt | 2 +- src/scripting/scriptregistry.cpp | 106 +++++++++++++++++++------------ src/scripting/scriptregistry.hpp | 32 +++++++--- 3 files changed, 89 insertions(+), 51 deletions(-) diff --git a/scripts/CMakeLists.txt b/scripts/CMakeLists.txt index a6df4b6d6c..03b2049394 100644 --- a/scripts/CMakeLists.txt +++ b/scripts/CMakeLists.txt @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2026 Kushview, LLC # SPDX-License-Identifier: GPL-3.0-or-later -file(GLOB ELEMENT_LUA_SCRIPTS "${CMAKE_CURRENT_SOURCE_DIR}/*.lua") +file(GLOB ELEMENT_LUA_SCRIPTS CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*.lua") # Lua scripts are currently built into the the binaries until the app and plugins # are able to deal with search paths and so forth. if(TRUE) diff --git a/src/scripting/scriptregistry.cpp b/src/scripting/scriptregistry.cpp index 37db36f79b..25ecf6db01 100644 --- a/src/scripting/scriptregistry.cpp +++ b/src/scripting/scriptregistry.cpp @@ -31,12 +31,6 @@ std::string stripLuaExtension (const std::string& filename) return filename; } -bool endsWith (const std::string& s, const std::string& suffix) -{ - return s.size() >= suffix.size() - && s.compare (s.size() - suffix.size(), suffix.size(), suffix) == 0; -} - std::string toUpperCopy (std::string s) { for (auto& c : s) @@ -44,9 +38,9 @@ std::string toUpperCopy (std::string s) return s; } -// Matches a the LAST line in the script containing ONLY "return {" (ignores +// Matches a the LAST line in the script containing ONLY "return {" (ignores // whitespace). This is what anchors the start of the script's return block -// so the field scan below can't accidentally match an unrelated +// so the field scan below can't accidentally match an unrelated // `type`/`dspName` local variable earlier in the file. // // NOTE: matched one line at a time via regex_match() rather than using the @@ -57,7 +51,7 @@ std::string toUpperCopy (std::string s) // single line. const std::regex kReturnBlockStartLineRegex (R"(^[ \t]*return[ \t]*\{[ \t]*\r?$)"); -// Matches e.g. type = 'DSP' or type="DSP" (either quote style, flexible whitespace). +// Matches e.g. type = 'DSP' or type="DSPUI" (either quote style, flexible whitespace). const std::regex kTypeRegex (R"(\btype\s*=\s*['"]([^'"]+)['"])"); // Matches e.g. dspName = 'Amplifier' @@ -101,11 +95,15 @@ std::string extractReturnBlockRegion (const std::string& source) return found ? source.substr (regionStart) : std::string(); } -/** Scans raw Lua source text for a `type = 'DSP'` declaration, but only - within the script's trailing return block (the text following a - stand-alone "return {" line) -- not anywhere else in the file. This is - what lets e.g. `local type = something` earlier in a script's body avoid - being mistaken for the return block's `type` field. +/** Scans raw Lua source text for the `type = '...'` declaration in the + script's trailing return block (the text following a stand-alone + "return {" line) -- not anywhere else in the file. Returns the type + string (uppercased, e.g. "DSP" or "DSPUI") via outType, and any + `dspName = '...'` override via outDspNameOverride, if present. + + Returns false if no return block was found, or the return block has no + `type` field at all -- either way, the resource is not a node script + this registry cares about. NOTE: this is a lightweight text scan, not a real Lua parse -- it does not execute the script. Given this codebase's convention of a single @@ -114,7 +112,7 @@ std::string extractReturnBlockRegion (const std::string& source) fix is to actually execute the chunk through sol2/lua_State and inspect the returned table directly, rather than scanning text. */ -bool isDspScript (const std::string& source, std::string& outDspNameOverride) +bool extractScriptType (const std::string& source, std::string& outType, std::string& outDspNameOverride) { std::string region = extractReturnBlockRegion (source); if (region.empty()) @@ -124,8 +122,7 @@ bool isDspScript (const std::string& source, std::string& outDspNameOverride) if (! std::regex_search (region, typeMatch, kTypeRegex)) return false; - if (toUpperCopy (typeMatch[1].str()) != "DSP") - return false; + outType = toUpperCopy (typeMatch[1].str()); std::smatch nameMatch; if (std::regex_search (region, nameMatch, kDspNameRegex)) @@ -168,58 +165,83 @@ ScriptRegistry::ScriptRegistry() } // Index by file-derived name for O(1) lookups while pairing DSP <-> UI - // scripts and resolving UI companions below. This index intentionally - // uses filenames, not dspName overrides, since ui-companion filenames - // (e.g. "ampui") are matched against the DSP script's *file* name. + // scripts below. std::unordered_map indexByName; indexByName.reserve (raw.size()); for (size_t i = 0; i < raw.size(); ++i) indexByName.emplace (raw[i].name, i); - // 2. Determine which entries are actually "ui" companions of - // another real entry, so they don't also show up as standalone nodes. + // 2. Determine, up front and independent of iteration order, each raw + // resource's declared `type` (if any) and dspName override. This + // must be computed for EVERY resource before any UI-companion + // pairing decision below, since pairing needs to know whether the + // *candidate companion* positively declares itself as `DSPUI` -- + // not merely that it exists, and not merely that it isn't `DSP`. + std::vector isDspValid (raw.size(), false); + std::vector isUiValid (raw.size(), false); + std::vector dspNameOverride (raw.size()); + + for (size_t i = 0; i < raw.size(); ++i) + { + std::string source (raw[i].data, static_cast (raw[i].size)); + std::string type; + if (! extractScriptType (source, type, dspNameOverride[i])) + continue; // no return block / no type field -- not a node script + + isDspValid[i] = (type == "DSP"); + isUiValid[i] = (type == "DSPUI"); + } + + // 3. Decide UI companions. A resource named "ui" is treated as + // the UI companion of "" only if: + // a) "" is itself a valid DSP script (type == 'DSP'), AND + // b) "ui" is itself a valid UI script (type == 'DSPUI'). + // + // Requiring an explicit `type = 'DSPUI'` on the companion (rather + // than just "isn't DSP") means a same-named resource that happens to + // exist for some unrelated reason, or is malformed, or declares some + // other type entirely, is never mistaken for a real UI companion. + // It also means a genuine standalone DSP script that happens to be + // named e.g. "flexui.lua" is never silently swallowed as someone + // else's UI companion -- it surfaces as its own top-level entry, and + // (per this same rule applied to it) its own potential companion is + // looked up as "flexuiui.lua". std::vector isUiCompanion (raw.size(), false); for (size_t i = 0; i < raw.size(); ++i) { - if (! endsWith (raw[i].name, "ui")) + if (! isDspValid[i]) continue; - std::string baseName = raw[i].name.substr (0, raw[i].name.size() - 2); - if (baseName.empty()) + auto it = indexByName.find (raw[i].name + "ui"); + if (it == indexByName.end()) continue; - auto it = indexByName.find (baseName); - if (it != indexByName.end() && it->second != i) - isUiCompanion[i] = true; + size_t j = it->second; + if (isUiValid[j]) + isUiCompanion[j] = true; } - // 3. Build the final entry list: everything that (a) isn't someone else's - // UI companion, and (b) declares `type = 'DSP'` in its return block, - // becomes a top-level BuiltInScripts entry, with its UI companion (if - // any) attached and its display name resolved (dspName override, or - // filename-derived name as the fallback). + // 4. Build the final entry list: every resource that is a valid DSP + // script and is not itself consumed as another entry's UI companion + // becomes a top-level BuiltInScripts entry, with its UI companion + // (if any, per the rules above) attached and its display name + // resolved (dspName override, or filename-derived name as fallback). names.reserve (raw.size()); // upper bound; guarantees c_str() stability below scripts.reserve (raw.size()); for (size_t i = 0; i < raw.size(); ++i) { - if (isUiCompanion[i]) + if (! isDspValid[i] || isUiCompanion[i]) continue; - std::string source (raw[i].data, static_cast (raw[i].size)); - - std::string dspNameOverride; - if (! isDspScript (source, dspNameOverride)) - continue; // not a DSP node script (e.g. a shared support module) -- skip - - names.push_back (dspNameOverride.empty() ? raw[i].name : dspNameOverride); + names.push_back (dspNameOverride[i].empty() ? raw[i].name : dspNameOverride[i]); const char* uiScript = nullptr; int uiSize = 0; auto it = indexByName.find (raw[i].name + "ui"); - if (it != indexByName.end()) + if (it != indexByName.end() && isUiValid[it->second]) { uiScript = raw[it->second].data; uiSize = raw[it->second].size; diff --git a/src/scripting/scriptregistry.hpp b/src/scripting/scriptregistry.hpp index f6e0fcd42f..a8de5bb1ea 100644 --- a/src/scripting/scriptregistry.hpp +++ b/src/scripting/scriptregistry.hpp @@ -25,6 +25,13 @@ namespace element { That return block may also optionally declare `dspName = '...'`, which overrides the display name (otherwise the DSP resource's filename, minus the .lua extension, is used). + + A "ui" resource is only ever treated as 's UI companion if it + itself declares `type = 'DSPUI'` in its own return block. This means a + ui resource that happens to exist for some unrelated reason (or is + itself a standalone DSP script, or declares some other type entirely) is + never mistakenly swallowed as a companion -- see scriptregistry.cpp for + the full pairing rules. */ struct BuiltInScripts { @@ -41,19 +48,24 @@ struct BuiltInScripts The script list is discovered entirely at runtime: 1. Every embedded resource is enumerated from scripts::namedResourceList. - 2. Resources are paired up using the "" / "ui" naming - convention (the ui one becomes a companion, not its own entry). - 3. Each remaining candidate's source is scanned for a `type = 'DSP'` - return block; candidates without one, or whose `type` is something - else (e.g. view.lua's `type = 'View'`), are dropped entirely. - 4. If that return block also declares `dspName = '...'`, it is used as - the entry's display name; otherwise the filename-derived name is used. + 2. Each resource's declared `type` (if any) is determined by scanning + its trailing return block -- 'DSP', 'DSPUI', or anything else + (which is dropped, e.g. view.lua's `type = 'View'`). + 3. Resources named "ui" are paired to "" as a UI companion + only when is a valid DSP script AND "ui" is itself a + valid DSPUI script -- never merely by name existing. + 4. If a DSP script's return block declares `dspName = '...'`, it is + used as the entry's display name; otherwise the filename-derived + name is used. Nothing is hardcoded, so new scripts dropped into scripts/ are picked up automatically without touching this class. Populated lazily on first access. Thread-safe by virtue of C++11 - function-local static initialization guarantees. + function-local static initialization guarantees (construction only -- + the underlying vectors are populated once during construction and never + mutated afterward, so concurrent reads via getScripts()/findByName() + after that first call are safe). */ class ScriptRegistry { @@ -66,6 +78,10 @@ class ScriptRegistry /** Looks up a script by its display name (e.g. "amp", or its dspName override if one was declared). Returns nullptr if not found. + + The returned pointer refers to storage owned by the registry and + remains valid for the lifetime of the application (the registry is + a function-local static that is never destroyed until program exit). */ const BuiltInScripts* findByName (const char* name) const noexcept; From 9aec5d01bd5d317cb90ed5c1b59dc1a35123a93f Mon Sep 17 00:00:00 2001 From: Buzz Burrowes Date: Fri, 28 Aug 2026 19:55:16 -0400 Subject: [PATCH 4/7] Fix copyright notices to comply with instructions for contributing on GitHub --- src/scripting/scriptregistry.cpp | 4 ++-- src/scripting/scriptregistry.hpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/scripting/scriptregistry.cpp b/src/scripting/scriptregistry.cpp index 25ecf6db01..208e008e87 100644 --- a/src/scripting/scriptregistry.cpp +++ b/src/scripting/scriptregistry.cpp @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: Copyright (C) Kushview, LLC. -// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright 2026. Kushview, LLC +// Author: Buzz Burrowes #include #include diff --git a/src/scripting/scriptregistry.hpp b/src/scripting/scriptregistry.hpp index a8de5bb1ea..949c92baf3 100644 --- a/src/scripting/scriptregistry.hpp +++ b/src/scripting/scriptregistry.hpp @@ -1,5 +1,5 @@ -// SPDX-FileCopyrightText: Copyright (C) Kushview, LLC. -// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright 2026. Kushview, LLC +// Author: Buzz Burrowes #pragma once From 47cf0d142f1678f34797b72a63a201a13af3c953 Mon Sep 17 00:00:00 2001 From: Buzz Burrowes Date: Sat, 5 Sep 2026 11:43:08 -0400 Subject: [PATCH 5/7] Fixed issue with copyright notice at the top of the files --- src/scripting/scriptmanager.cpp | 3 ++- src/scripting/scriptregistry.hpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/scripting/scriptmanager.cpp b/src/scripting/scriptmanager.cpp index efb073c2cf..1eb93beb87 100644 --- a/src/scripting/scriptmanager.cpp +++ b/src/scripting/scriptmanager.cpp @@ -1,5 +1,6 @@ -// Copyright 2023 Kushview, LLC +// Copyright 2026 Kushview, LLC // SPDX-License-Identifier: GPL-3.0-or-later +// Author: Buzz Burrowes #include #include diff --git a/src/scripting/scriptregistry.hpp b/src/scripting/scriptregistry.hpp index 949c92baf3..44d9a41603 100644 --- a/src/scripting/scriptregistry.hpp +++ b/src/scripting/scriptregistry.hpp @@ -1,4 +1,5 @@ -// Copyright 2026. Kushview, LLC +// Copyright 2026 Kushview, LLC +// SPDX-License-Identifier: GPL-3.0-or-later // Author: Buzz Burrowes #pragma once From dbbed68b5acb1a203b10e4ba173a18507f97da61 Mon Sep 17 00:00:00 2001 From: Buzz Burrowes Date: Sat, 5 Sep 2026 11:58:47 -0400 Subject: [PATCH 6/7] Revert "Fixed issue with copyright notice at the top of the files" This reverts commit 47cf0d142f1678f34797b72a63a201a13af3c953. --- src/scripting/scriptmanager.cpp | 3 +-- src/scripting/scriptregistry.hpp | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/scripting/scriptmanager.cpp b/src/scripting/scriptmanager.cpp index 1eb93beb87..efb073c2cf 100644 --- a/src/scripting/scriptmanager.cpp +++ b/src/scripting/scriptmanager.cpp @@ -1,6 +1,5 @@ -// Copyright 2026 Kushview, LLC +// Copyright 2023 Kushview, LLC // SPDX-License-Identifier: GPL-3.0-or-later -// Author: Buzz Burrowes #include #include diff --git a/src/scripting/scriptregistry.hpp b/src/scripting/scriptregistry.hpp index 44d9a41603..949c92baf3 100644 --- a/src/scripting/scriptregistry.hpp +++ b/src/scripting/scriptregistry.hpp @@ -1,5 +1,4 @@ -// Copyright 2026 Kushview, LLC -// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright 2026. Kushview, LLC // Author: Buzz Burrowes #pragma once From dfd47136930bed8348361f692d879fbf9cf1c23b Mon Sep 17 00:00:00 2001 From: Buzz Burrowes Date: Sat, 5 Sep 2026 11:59:37 -0400 Subject: [PATCH 7/7] Another attempt to fix copyright notices --- src/scripting/scriptregistry.cpp | 3 ++- src/scripting/scriptregistry.hpp | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/scripting/scriptregistry.cpp b/src/scripting/scriptregistry.cpp index 208e008e87..fe269a65c4 100644 --- a/src/scripting/scriptregistry.cpp +++ b/src/scripting/scriptregistry.cpp @@ -1,4 +1,5 @@ -// Copyright 2026. Kushview, LLC +// Copyright 2026 Kushview, LLC +// SPDX-License-Identifier: GPL-3.0-or-later // Author: Buzz Burrowes #include diff --git a/src/scripting/scriptregistry.hpp b/src/scripting/scriptregistry.hpp index 949c92baf3..44d9a41603 100644 --- a/src/scripting/scriptregistry.hpp +++ b/src/scripting/scriptregistry.hpp @@ -1,4 +1,5 @@ -// Copyright 2026. Kushview, LLC +// Copyright 2026 Kushview, LLC +// SPDX-License-Identifier: GPL-3.0-or-later // Author: Buzz Burrowes #pragma once