Skip to content

Commit

Permalink
Work on improving the wavetable evaluator
Browse files Browse the repository at this point in the history
1. Step one: Make the evaluator an object with internal details
   state so we can move towards init and so on.
2. Add an init fn which can set shared state across all frames
   and also currently set the name of the object
3. Improve error handling, code clairty, arguments, internal
   state, etc...
4. Change the example to show these new features including
   using the name 'wt' and using a clearer set of names
   (but keep the old names silently present for now)

Addresses #4539
  • Loading branch information
baconpaul committed Nov 30, 2024
1 parent 92eaeb1 commit 71679f0
Show file tree
Hide file tree
Showing 4 changed files with 340 additions and 61 deletions.
313 changes: 272 additions & 41 deletions src/common/dsp/WavetableScriptEvaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,35 +29,191 @@ namespace Surge
namespace WavetableScript
{

std::vector<float> 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<Details>(); }

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<std::vector<float>> 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<std::vector<float>> res{std::nullopt};
auto values = std::vector<float>();

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);

Expand All @@ -76,13 +232,26 @@ std::vector<float> 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);
Expand All @@ -93,9 +262,12 @@ std::vector<float> evaluateScriptAtFrame(SurgeStorage *storage, const std::strin
else
{
values.push_back(0.f);
gen = false;
}
lua_pop(L, 1);
}
if (gen)
res = values;
}
}
else
Expand All @@ -109,62 +281,121 @@ std::vector<float> 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
Loading

0 comments on commit 71679f0

Please sign in to comment.