Skip to content

Commit

Permalink
Continue Issue #, building on Luna's PR.
Browse files Browse the repository at this point in the history
Using Vec!ubyte allows to reuse popLE/popBE and writeLE/writeBE functions, for better and worse. Avoid any problem with who owns what and realloc.
Renamed the callback after identifier shoutout.

Distort now uses futureBinState to act as "showcase" of feature. It just stores its major plugin version.

The finding was that:
 - not all VST2 host call effSetChunk, not sure why. This is unrelated bug.
 - the UI vs client sync for will be as complicated as it is for Parameter... unlike Parameter, it may be a good idea to encourage people to have a duplicated local state in the UI. Now I understand better why JUCE unified all that under "APVTS". This sync problem is just dumped unto users with the callback... it's not clear yet which threads will call loadState and saveState, probably any from the host can.
  • Loading branch information
Guillaume Piolat committed Aug 13, 2023
1 parent 243e914 commit 845b770
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 20 deletions.
52 changes: 44 additions & 8 deletions client/dplug/client/client.d
Original file line number Diff line number Diff line change
Expand Up @@ -899,22 +899,58 @@ nothrow:
version(futureBinState)
{
/**
Get state data to write to file
Write the extra state of plugin in a chunk, so that the host can restore that later.
You would typically serialize arbitrary stuff with `dplug.core.binrange`.
This is called quite frequently.
Memory is owned by the serializer, do not free it!
What should goes here:
* your own chunk format with hopefully your plugin major version.
* user-defined structures, like opened .wav, strings, wavetables...
You can finally make plugins with arbitrary data in presets!
* Typically stuff used to render sound identically.
NOTE: This is not supported in LV2.
Contrarily, this is a disappointing solution for:
* Storing UI size, dark mode, and all kind of editor preferences.
Indeed, when `loadState` is called, the UI might not exist at all.
Note: Using state chunks comes with a BIG challenge of making your own synchronization
with the UI. You can expect any thread to call `saveState` and `loadState`.
A proper design would probably have you represent state in the editor and the
audio client separately, with a clean interchange.
Warning: Just append new content to the `Vec!ubyte`, do not modify its existing content
if any exist.
BUG: This is not currently supported in LV2, and only partially in VST2.
See Issue #352 for the whole story.
See_also: `loadState`.
*/
void getSaveState(ref ubyte[] state) { }
void saveState(ref Vec!ubyte chunk) nothrow @nogc
{
}

/**
Sets the binary state for the plugin
Write the extra state your plugin in a chunk, so that the host can restore that later.
You would typically serialize arbitrary stuff with `dplug.core.binrange`.
This is called on session load.
Memory is owned by the serializer, do not free it!
Note: Using state chunks comes with a BIG challenge of making your own synchronization
with the UI. You can expect any thread to call `saveState` and `loadState`.
A proper design would probably have you represent state in the editor and the
audio client separately, with a clean interchange.
NOTE: This is not supported in LV2.
BUG: This is not currently supported in LV2, and only partially in VST2.
See Issue #352 for the whole story.
Returns: `true` on successful parse, you can return false to indicate a parsing error.
See_also: `loadState`.
*/
void setSaveState(const(ubyte)[] state) { }
bool loadState(const(ubyte)[] chunk) nothrow @nogc
{
return true;
}
}

// </IClient>
Expand Down
54 changes: 43 additions & 11 deletions client/dplug/client/preset.d
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,19 @@ final class Preset
{
public:

this(string name, const(float)[] normalizedParams, ubyte[] stateData=null) nothrow @nogc
this(string name, const(float)[] normalizedParams, const(ubyte)[] stateData = null) nothrow @nogc
{
_name = name.mallocDup;
_normalizedParams = normalizedParams.mallocDup;
if (stateData) _stateData = stateData.mallocDup;
if (stateData)
_stateData = stateData.mallocDup;
}

~this() nothrow @nogc
{
clearName();
free(_normalizedParams.ptr);
if (_stateData) free(_stateData.ptr);
clearData();
}

void setNormalized(int paramIndex, float value) nothrow @nogc
Expand All @@ -86,6 +87,7 @@ public:

version(futureBinState) void setStateData(ubyte[] data) nothrow @nogc
{
clearData();
_stateData = data.mallocDup;
}

Expand All @@ -96,9 +98,17 @@ public:
{
_normalizedParams[i] = param.getNormalized();
}
version(futureBinState) client.getSaveState(_stateData);

version(futureBinState)
{{
clearData(); // Forget possible existing _stateData
Vec!ubyte chunk;
client.saveState(chunk);
_stateData = chunk.releaseData;
}}
}

// TODO: should this return an error if any encountered?
void loadFromHost(Client client) nothrow @nogc
{
auto params = client.params();
Expand All @@ -112,7 +122,12 @@ public:
param.setFromHost(param.getNormalizedDefault());
}
}
version(futureBinState) client.setSaveState(_stateData);

version(futureBinState)
{
// loadFromHost seems to best effort, no error reported on parse failure
client.loadState(_stateData);
}
}

static bool isValidNormalizedParam(float f) nothrow @nogc
Expand Down Expand Up @@ -143,6 +158,15 @@ private:
_name = null;
}
}

void clearData() nothrow @nogc
{
if (_stateData !is null)
{
free(_stateData.ptr);
_stateData = null;
}
}
}

/// A preset bank is a collection of presets
Expand Down Expand Up @@ -323,7 +347,9 @@ private:
}

int dataLength = chunk.popLE!int();
_client.setSaveState(chunk[0..dataLength]);
bool parseSuccess = _client.loadState(chunk[0..dataLength]);
if (!parseSuccess)
throw mallocNew!Exception("Invalid user-defined state chunk");
}
}

Expand All @@ -335,17 +361,23 @@ private:
writeChunkHeader(chunk, 1);

auto params = _client.params();
ubyte[] stateData;

chunk.writeLE!int(_current);

chunk.writeLE!int(cast(int)params.length);
foreach(param; params)
chunk.writeLE!float(param.getNormalized());

_client.getSaveState(stateData);
chunk.writeLE!int(cast(int)stateData.length);
chunk.put(stateData);

size_t chunkLenIndex = chunk.length;
chunk.writeLE!int(0); // temporary chunk length value

size_t before = chunk.length;
_client.saveState(chunk); // append state to chunk
size_t after = chunk.length;

// Write actual value for chunk length.
ubyte[] indexPos = chunk.ptr[chunkLenIndex.. chunkLenIndex+4];
indexPos.writeLE!int(cast(int)(after - before));
}
} else {

Expand Down
2 changes: 1 addition & 1 deletion examples/distort/dub.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"dplug:wren-support": { "path": "../.." }
},

"versions": ["futureMouseDrag"],
"versions": ["futureMouseDrag", "futureBinState"],

"configurations": [
{
Expand Down
23 changes: 23 additions & 0 deletions examples/distort/main.d
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,29 @@ nothrow:
return mallocNew!DistortGUI(this);
}

override void saveState(ref Vec!ubyte chunk)
{
// dplug.core.binrange allows to write arbitrary chunk bytes here.
// You are responsible for versioning, correct UI interaction, etc.
// See `saveState` definition in client.d for highly-recommended information.
writeLE!uint(chunk, getPublicVersion().major);
}

override bool loadState(const(ubyte)[] chunk)
{
try
{
int major = popLE!uint(chunk);
assert(major == getPublicVersion().major);
return true; // no issue parsing the chunk
}
catch(Exception e)
{
destroyFree(e);
return false;
}
}

private:
LevelComputation _levelInput;
LevelComputation _levelOutput;
Expand Down

0 comments on commit 845b770

Please sign in to comment.