diff --git a/src/common/dsp/WavetableScriptEvaluator.cpp b/src/common/dsp/WavetableScriptEvaluator.cpp index 8c29da2392d..ae44dfebaad 100644 --- a/src/common/dsp/WavetableScriptEvaluator.cpp +++ b/src/common/dsp/WavetableScriptEvaluator.cpp @@ -29,35 +29,191 @@ namespace Surge namespace WavetableScript { -std::vector evaluateScriptAtFrame(SurgeStorage *storage, const std::string &eqn, - int resolution, int frame, int nFrames) +static constexpr const char *statetable{"statetable"}; + +struct LuaWTEvaluator::Details { -#if HAS_LUA - static lua_State *L = nullptr; - if (L == nullptr) + SurgeStorage *storage{nullptr}; + std::string script{}; + size_t resolution{2048}; + size_t frameCount{10}; + + bool needsParse{false}; + + lua_State *L{nullptr}; + + void prepare() + { + if (L == nullptr) + { + L = lua_open(); + luaL_openlibs(L); + + auto wg = Surge::LuaSupport::SGLD("WavetableScript::prelude", L); + + Surge::LuaSupport::loadSurgePrelude(L, Surge::LuaSources::wtse_prelude); + } + } + + void makeEmptyState(bool pushToGlobal) + { + lua_createtable(L, 0, 10); + lua_pushinteger(L, frameCount); + lua_setfield(L, -2, "frame_count"); + lua_pushinteger(L, resolution); + lua_setfield(L, -2, "sample_count"); + + if (pushToGlobal) + lua_setglobal(L, statetable); + } + void callInitFn() + { + prepare(); + auto wg = Surge::LuaSupport::SGLD("WavetableScript::details::callInitFn", L); + + lua_getglobal(L, "init"); + if (!lua_isfunction(L, -1)) + { + makeEmptyState(true); + } + else + { + Surge::LuaSupport::setSurgeFunctionEnvironment(L); + + makeEmptyState(false); + + auto res = lua_pcall(L, 1, 1, 0); + if (res == LUA_OK) + { + if (lua_istable(L, -1)) + { + lua_setglobal(L, statetable); + } + else + { + if (storage) + storage->reportError("Init function returned a non-table", + "Wavetable Script Evaluator"); + makeEmptyState(true); + } + } + else + { + std::string luaerr = lua_tostring(L, -1); + if (storage) + storage->reportError(luaerr, "Wavetable Evaluator Init Error"); + else + std::cerr << luaerr; + lua_pop(L, -1); + + makeEmptyState(true); + } + } + } + bool parseIfNeeded() + { + prepare(); + if (needsParse) + { + { + // Have a separate guard for this just to make sure I match + auto lwg = Surge::LuaSupport::SGLD("WavetableScript::details::clearGlobals", L); + lua_pushnil(L); + lua_setglobal(L, "generate"); + lua_pushnil(L); + lua_setglobal(L, "init"); + lua_pushnil(L); + lua_setglobal(L, statetable); + } + + auto wg = Surge::LuaSupport::SGLD("WavetableScript::details::parseIfNeeded", L); + std::string emsg; + auto res = Surge::LuaSupport::parseStringDefiningMultipleFunctions( + L, script, {"init", "generate"}, emsg); + if (!res && storage) + { + storage->reportError(emsg, "Wavetable Parse Error"); + } + + lua_pop(L, 2); // remove the 2 functions added in the global state + + callInitFn(); + + needsParse = false; + + return res; + } + else + { + return true; + } + } +}; + +LuaWTEvaluator::LuaWTEvaluator() { details = std::make_unique
(); } + +LuaWTEvaluator::~LuaWTEvaluator() = default; + +void LuaWTEvaluator::setStorage(SurgeStorage *s) { details->storage = s; } +void LuaWTEvaluator::setScript(const std::string &e) +{ + if (e != details->script) { - L = lua_open(); - luaL_openlibs(L); + details->script = e; + details->needsParse = true; } +} +void LuaWTEvaluator::setResolution(size_t r) { details->resolution = r; } +void LuaWTEvaluator::setFrameCount(size_t n) { details->frameCount = n; } +std::optional> LuaWTEvaluator::evaluateScriptAtFrame(size_t frame) +{ +#if HAS_LUA + auto storage = details->storage; + auto &eqn = details->script; + auto resolution = details->resolution; + auto nFrames = details->frameCount; + + std::optional> res{std::nullopt}; auto values = std::vector(); - auto wg = Surge::LuaSupport::SGLD("WavetableScript::evaluate", L); + details->prepare(); + auto L = details->L; - // Load the WTSE prelude - Surge::LuaSupport::loadSurgePrelude(L, Surge::LuaSources::wtse_prelude); + auto wg = Surge::LuaSupport::SGLD("WavetableScript::evaluate", L); - std::string emsg; - auto res = Surge::LuaSupport::parseStringDefiningFunction(L, eqn, "generate", emsg); - if (res) + if (details->parseIfNeeded()) { + auto wgp = Surge::LuaSupport::SGLD("WavetableScript::evaluateInner", L); + lua_getglobal(details->L, "generate"); + if (!lua_isfunction(details->L, -1)) + { + if (storage) + storage->reportError("Unable to locate generate function", + "Wavetable Script Evaluator"); + return std::nullopt; + } Surge::LuaSupport::setSurgeFunctionEnvironment(L); - /* - * Alright so we want the stack to be the config table which - * contains the xs, contains n, contains ntables, etc.. so - */ + lua_createtable(L, 0, 10); + lua_getglobal(L, statetable); + + lua_pushnil(L); /* first key */ + assert(lua_istable(L, -2)); + while (lua_next(L, -2) != 0) + { + // stack is now v > k > global > new + lua_pushvalue(L, -2); + // stack is now k > v > k > global > new + lua_insert(L, -2); + // stack is now v > k > k > global > new + lua_settable(L, -5); + // stack is now k > global > new + } + + lua_pop(L, 1); + // xs is an array of the x locations in phase space lua_createtable(L, resolution, 0); @@ -76,13 +232,26 @@ std::vector evaluateScriptAtFrame(SurgeStorage *storage, const std::strin lua_pushinteger(L, nFrames); lua_setfield(L, -2, "nTables"); + lua_pushinteger(L, frame + 1); + lua_setfield(L, -2, "frame"); + + lua_pushinteger(L, nFrames); + lua_setfield(L, -2, "frame_count"); + + lua_pushinteger(L, resolution); + lua_setfield(L, -2, "sample_count"); + + lua_getglobal(L, statetable); + lua_setfield(L, -2, "state"); + // So stack is now the table and the function auto pcr = lua_pcall(L, 1, 1, 0); if (pcr == LUA_OK) { if (lua_istable(L, -1)) { - for (auto i = 0; i < resolution; ++i) + bool gen{true}; + for (auto i = 0; i < resolution && gen; ++i) { lua_pushinteger(L, i + 1); lua_gettable(L, -2); @@ -93,9 +262,12 @@ std::vector evaluateScriptAtFrame(SurgeStorage *storage, const std::strin else { values.push_back(0.f); + gen = false; } lua_pop(L, 1); } + if (gen) + res = values; } } else @@ -109,62 +281,121 @@ std::vector evaluateScriptAtFrame(SurgeStorage *storage, const std::strin } lua_pop(L, 1); // Error string or pcall result } - else - { - if (storage) - storage->reportError(emsg, "Wavetable Evaluator Syntax Error"); - else - std::cerr << emsg; - lua_pop(L, 1); - } - return values; + + return res; + #else return {}; #endif } -bool constructWavetable(SurgeStorage *storage, const std::string &eqn, int resolution, int frames, - wt_header &wh, float **wavdata) +bool LuaWTEvaluator::constructWavetable(wt_header &wh, float **wavdata) { + auto storage = details->storage; + auto &eqn = details->script; + auto resolution = details->resolution; + auto frames = details->frameCount; + auto wd = new float[frames * resolution]; wh.n_samples = resolution; wh.n_tables = frames; wh.flags = 0; *wavdata = wd; + details->prepare(); + details->parseIfNeeded(); + details->callInitFn(); for (int i = 0; i < frames; ++i) { - auto v = evaluateScriptAtFrame(storage, eqn, resolution, i, frames); - memcpy(&(wd[i * resolution]), &(v[0]), resolution * sizeof(float)); + auto v = evaluateScriptAtFrame(i); + if (v.has_value()) + { + memcpy(&(wd[i * resolution]), &((*v)[0]), resolution * sizeof(float)); + } + else + { + return false; + } } return true; } -std::string defaultWavetableScript() + +std::string LuaWTEvaluator::getSuggestedWavetableName() { - return R"FN(function generate(config) + std::string res = "Scripted Wavetable"; + + details->prepare(); + details->parseIfNeeded(); + details->callInitFn(); + + auto L = details->L; + auto wgp = Surge::LuaSupport::SGLD("WavetableScript::evaluateInner", L); + lua_getglobal(L, statetable); + if (lua_istable(L, -1)) + { + lua_getfield(L, -1, "name"); + if (lua_isstring(L, -1)) + { + res = lua_tostring(L, -1); + } + + lua_pop(L, -1); + } + + lua_pop(L, -1); + + return res; +} + +void LuaWTEvaluator::prepare() +{ + details->prepare(); + details->parseIfNeeded(); + details->callInitFn(); +} + +std::string LuaWTEvaluator::defaultWavetableScript() +{ + return R"FN( -- This script serves as the default example for the wavetable script editor. Unlike the formula editor, which executes --- repeatedly every block, the Lua code here runs only upon applying new settings or receiving GUI inputs like the frame +-- repeatedly every block, the Lua code here runs only upon applying new settings or receiving GUI inputs like the frame -- slider. -- -- When the Generate button is pressed, this function is called for each frame, and the results are collected and sent --- to the Wavetable oscillator. The oscillator can sweep through these frames to evolve the sound produced using the +-- to the Wavetable oscillator. The oscillator can sweep through these frames to evolve the sound produced using the -- Morph parameter. -- --- The for loops iterate over an array of sample values (xs) and a frame number (n) and generate the result for the n-th --- frame. This example uses additive synthesis, a technique that adds sine waves to create waveshapes. The initial frame --- starts with a single sine wave, and additional sine waves are added in subsequent frames. This process creates a Fourier +-- The for loops iterate over an array of sample values (phase) and a frame number (n) and generate the result for the n-th +-- frame. This example uses additive synthesis, a technique that adds sine waves to create waveshapes. The initial frame +-- starts with a single sine wave, and additional sine waves are added in subsequent frames. This process creates a Fourier -- series sawtooth wave defined by the formula: sum 2 / pi n * sin n x. See the tutorial patches for more info. +-- +-- The first time the script is loaded, the engine will call the 'init' function and the resulting +-- state it provides will be available in every subsequent call as the variables provided in the +-- wt structure +function init(wt) + -- wt will have frame_count and sample_count defined + wt.name = "Fourier Saw" + wt.phase = math.linspace(0.0, 1.0, wt.sample_count) + return wt +end + +function generate(wt) + + -- wt will have frame_count, sample_count, frame, and any item from init defined local res = {} - for i,x in ipairs(config.xs) do + + for i,x in ipairs(wt.phase) do local lv = 0 - for q = 1,(config.n) do + for q = 1,(wt.frame) do lv = lv + 2 * sin(2 * pi * q * x) / (pi * q) end res[i] = lv end return res -end)FN"; +end +)FN"; } } // namespace WavetableScript } // namespace Surge diff --git a/src/common/dsp/WavetableScriptEvaluator.h b/src/common/dsp/WavetableScriptEvaluator.h index b20a82b3f04..63100677615 100644 --- a/src/common/dsp/WavetableScriptEvaluator.h +++ b/src/common/dsp/WavetableScriptEvaluator.h @@ -31,22 +31,37 @@ namespace Surge { namespace WavetableScript { -/* - * Unlike the LFO modulator this is called at render time of the wavetable - * not at the evaluation or synthesis time. As such I expect you call it from - * one thread at a time and just you know generally be careful. - */ -std::vector evaluateScriptAtFrame(SurgeStorage *storage, const std::string &eqn, - int resolution, int frame, int nFrames); +struct LuaWTEvaluator +{ + struct Details; + std::unique_ptr
details; + LuaWTEvaluator(); + ~LuaWTEvaluator(); -/* - * Generate all the data required to call BuildWT. The wavdata here is data you - * must free with delete[] - */ -bool constructWavetable(SurgeStorage *storage, const std::string &eqn, int resolution, int frames, - wt_header &wh, float **wavdata); + void setStorage(SurgeStorage *); + void setScript(const std::string &); + void setResolution(size_t); + void setFrameCount(size_t); + + void prepare(); + + /* + * Unlike the LFO modulator this is called at render time of the wavetable + * not at the evaluation or synthesis time. As such I expect you call it from + * one thread at a time and just you know generally be careful. + */ + std::optional> evaluateScriptAtFrame(size_t frame); + + /* + * Generate all the data required to call BuildWT. The wavdata here is data you + * must free with delete[] + */ + bool constructWavetable(wt_header &wh, float **wavdata); + + std::string getSuggestedWavetableName(); -std::string defaultWavetableScript(); + static std::string defaultWavetableScript(); +}; } // namespace WavetableScript } // namespace Surge diff --git a/src/surge-xt/gui/overlays/LuaEditors.cpp b/src/surge-xt/gui/overlays/LuaEditors.cpp index a06a6be7676..88f3b2b3eaa 100644 --- a/src/surge-xt/gui/overlays/LuaEditors.cpp +++ b/src/surge-xt/gui/overlays/LuaEditors.cpp @@ -1524,7 +1524,8 @@ WavetableScriptEditor::WavetableScriptEditor(SurgeGUIEditor *ed, SurgeStorage *s if (osc->wavetable_formula == "") { - mainDocument->insertText(0, Surge::WavetableScript::defaultWavetableScript()); + mainDocument->insertText(0, + Surge::WavetableScript::LuaWTEvaluator::defaultWavetableScript()); } else { @@ -1559,6 +1560,9 @@ WavetableScriptEditor::WavetableScriptEditor(SurgeGUIEditor *ed, SurgeStorage *s showPreludeCode(); break; } + + evaluator = std::make_unique(); + evaluator->setStorage(storage); } WavetableScriptEditor::~WavetableScriptEditor() = default; @@ -1577,12 +1581,28 @@ void WavetableScriptEditor::onSkinChanged() rendererComponent->setSkin(skin, associatedBitmapStore); // FIXME } +void WavetableScriptEditor::setupEvaluator() +{ + auto resi = controlArea->resolutionN->getIntValue(); + auto respt = 32; + for (int i = 1; i < resi; ++i) + respt *= 2; + + evaluator->setScript(mainDocument->getAllContent().toStdString()); + evaluator->setResolution(respt); + evaluator->setFrameCount(controlArea->framesN->getIntValue()); + + evaluator->prepare(); +} + void WavetableScriptEditor::applyCode() { osc->wavetable_formula = mainDocument->getAllContent().toStdString(); osc->wavetable_formula_res_base = controlArea->resolutionN->getIntValue(); osc->wavetable_formula_nframes = controlArea->framesN->getIntValue(); + setupEvaluator(); + editor->repaintFrame(); rerenderFromUIState(); setApplyEnabled(false); @@ -1665,8 +1685,16 @@ void WavetableScriptEditor::rerenderFromUIState() for (int i = 1; i < resi; ++i) respt *= 2; - rendererComponent->points = Surge::WavetableScript::evaluateScriptAtFrame( - storage, mainDocument->getAllContent().toStdString(), respt, cfr, nfr); + setupEvaluator(); + auto rs = evaluator->evaluateScriptAtFrame(cfr); + if (rs.has_value()) + { + rendererComponent->points = *rs; + } + else + { + rendererComponent->points.clear(); + } rendererComponent->repaint(); } @@ -1699,11 +1727,11 @@ void WavetableScriptEditor::generateWavetable() wt_header wh; float *wd = nullptr; - Surge::WavetableScript::constructWavetable(storage, mainDocument->getAllContent().toStdString(), - respt, nfr, wh, &wd); + setupEvaluator(); + evaluator->constructWavetable(wh, &wd); storage->waveTableDataMutex.lock(); osc->wt.BuildWT(wd, wh, wh.flags & wtf_is_sample); - osc->wavetable_display_name = "Scripted Wavetable"; + osc->wavetable_display_name = evaluator->getSuggestedWavetableName(); storage->waveTableDataMutex.unlock(); delete[] wd; diff --git a/src/surge-xt/gui/overlays/LuaEditors.h b/src/surge-xt/gui/overlays/LuaEditors.h index 2c9b11d3445..ebd65bde554 100644 --- a/src/surge-xt/gui/overlays/LuaEditors.h +++ b/src/surge-xt/gui/overlays/LuaEditors.h @@ -29,6 +29,8 @@ #include +#include "WavetableScriptEvaluator.h" + #include "SurgeStorage.h" #include "SkinSupport.h" @@ -146,6 +148,9 @@ struct WavetableScriptEditor : public CodeEditorContainerWithApply, public Refre void rerenderFromUIState(); void setCurrentFrame(int value); + void setupEvaluator(); + + std::unique_ptr evaluator; std::unique_ptr preludeDocument; std::unique_ptr preludeDisplay; std::unique_ptr controlArea;