Context
plugins/disksampler.cpp is implemented against the raw C API (Methcla_SynthDef with C function pointers; complex State + host-command lifecycle for streaming reads). plugins/README.md declares the C++ wrapper API "preferred" and currently mis-classifies disksampler as already using the wrapper — this issue makes that classification correct.
Methcla::Plugin::World<Synth> and HostContext (in include/methcla/plugin.hpp) currently expose no way to retrieve the underlying Methcla_World* / Methcla_Host*. DiskSampler needs raw context for State::initBuffer(host) and State::release(world) — both of which are non-trivial to fold into the wrapper itself. Two thin accessors solve it without churning the existing State machinery.
Outcome: disksampler.cpp uses the wrapper API; kSynthDefHasCleanup drives the same async State::release lifecycle. State machinery (ring buffer, refcounted destroy, underrun reporting) is left untouched.
The header is already marked // NOTE: This API is unstable and subject to change! (include/methcla/plugin.hpp:26), so adding accessors is in-scope.
Wrapper API additions (include/methcla/plugin.hpp)
template <class Synth> class World
{
// ... existing members ...
public:
Methcla_World* context() const { return m_context; }
};
class HostContext
{
// ... existing members ...
public:
Methcla_Host* context() const { return m_context; }
};
DiskSampler conversion
Keep untouched: State class and all its static callbacks, process_disk, process_memory, process_disk_interp, process_memory_interp, resample, hermite1, readAll, reportUnderrun.
Convert the DiskSampler struct + six disksampler_* C functions to a wrapper class with kSynthDefHasCleanup.
struct DiskSamplerOptions
{
const char* path;
bool loop;
size_t startFrame;
int32_t frames;
DiskSamplerOptions(OSCPP::Server::ArgStream args)
{
path = args.string();
loop = args.atEnd() ? false : args.int32();
startFrame = args.atEnd() ? 0 : std::max(0, args.int32());
frames = args.atEnd() ? -1 : args.int32();
}
};
class DiskSampler
{
float* m_ports[DiskSamplerPorts::numPorts()];
State* m_state;
friend size_t process_disk(Methcla_World*, DiskSampler*, /*...*/);
friend size_t process_disk_interp(Methcla_World*, DiskSampler*, /*...*/);
friend size_t process_memory(DiskSampler*, /*...*/);
friend size_t process_memory_interp(DiskSampler*, /*...*/);
friend void reportUnderrun(Methcla_World*, size_t, size_t);
public:
DiskSampler(const World<DiskSampler>& world, const Methcla_SynthDef*,
const DiskSamplerOptions& options)
: m_state(nullptr)
{
m_state = static_cast<State*>(world.alloc(sizeof(State)));
if (m_state != nullptr)
{
new (m_state) State(options.path, options.loop, options.startFrame,
options.frames, world.blockSize());
m_state->initBuffer(world.context());
}
}
void cleanup(const World<DiskSampler>& world)
{
if (m_state)
m_state->release(world.context());
}
void connect(DiskSamplerPorts::Port port, void* data)
{
m_ports[port] = static_cast<float*>(data);
}
void process(const World<DiskSampler>& world, size_t numFrames);
};
StaticSynthDef<DiskSampler, DiskSamplerOptions, DiskSamplerPorts,
kSynthDefHasCleanup>
kDiskSamplerDef;
DiskSampler::process is the body of the existing static process(...) (disksampler.cpp:941) with self → this and world.context() passed to helpers that still take raw Methcla_World*.
Lifecycle check. Original disksampler_destroy calls state->release(world), which refcount-decrements and posts destroyCallback → ~State() → freeCallback (frees RT memory). New flow: SynthDef::destroy calls cleanup(World(world)) first (which does exactly m_state->release(world.context())), then ~DiskSampler() (no-op). Identical semantics.
Files
include/methcla/plugin.hpp — add World<Synth>::context() and HostContext::context() accessors
plugins/disksampler.cpp — convert synth boilerplate; kSynthDefHasCleanup; friend the five process helpers
plugins/README.md — update the "Implementation styles" table row for disksampler.cpp (the current row mis-classifies it as wrapper; once this is merged the row will be correct).
CHANGELOG.md — add a ### Changed entry for the migration and a ### Added entry for the two accessors, both under ## [Unreleased], referencing this issue.
Verification
pre-commit run --all-files
cmake --preset debug
cmake --build build/debug
ctest --test-dir build/debug --output-on-failure
Confirm methcla_plugin_disksampler builds. Run any test exercising disksampler load-and-play (check tests/ for what exists).
Behavioural equivalence (no audible / log diff expected):
- Load → fills → streams →
synthDone on end (non-loop).
- Destroy mid-playback frees State asynchronously without leaks.
This plugin requires a Soundfile API Plugin to be registered (see plugins/README.md).
Context
plugins/disksampler.cppis implemented against the raw C API (Methcla_SynthDefwith C function pointers; complex State + host-command lifecycle for streaming reads).plugins/README.mddeclares the C++ wrapper API "preferred" and currently mis-classifies disksampler as already using the wrapper — this issue makes that classification correct.Methcla::Plugin::World<Synth>andHostContext(ininclude/methcla/plugin.hpp) currently expose no way to retrieve the underlyingMethcla_World*/Methcla_Host*. DiskSampler needs raw context forState::initBuffer(host)andState::release(world)— both of which are non-trivial to fold into the wrapper itself. Two thin accessors solve it without churning the existing State machinery.Outcome:
disksampler.cppuses the wrapper API;kSynthDefHasCleanupdrives the same asyncState::releaselifecycle. State machinery (ring buffer, refcounted destroy, underrun reporting) is left untouched.The header is already marked
// NOTE: This API is unstable and subject to change!(include/methcla/plugin.hpp:26), so adding accessors is in-scope.Wrapper API additions (
include/methcla/plugin.hpp)DiskSampler conversion
Keep untouched:
Stateclass and all its static callbacks,process_disk,process_memory,process_disk_interp,process_memory_interp,resample,hermite1,readAll,reportUnderrun.Convert the
DiskSamplerstruct + sixdisksampler_*C functions to a wrapper class withkSynthDefHasCleanup.DiskSampler::processis the body of the existing staticprocess(...)(disksampler.cpp:941) withself→thisandworld.context()passed to helpers that still take rawMethcla_World*.Lifecycle check. Original
disksampler_destroycallsstate->release(world), which refcount-decrements and postsdestroyCallback→~State()→freeCallback(frees RT memory). New flow:SynthDef::destroycallscleanup(World(world))first (which does exactlym_state->release(world.context())), then~DiskSampler()(no-op). Identical semantics.Files
include/methcla/plugin.hpp— addWorld<Synth>::context()andHostContext::context()accessorsplugins/disksampler.cpp— convert synth boilerplate;kSynthDefHasCleanup;friendthe five process helpersplugins/README.md— update the "Implementation styles" table row fordisksampler.cpp(the current row mis-classifies it as wrapper; once this is merged the row will be correct).CHANGELOG.md— add a### Changedentry for the migration and a### Addedentry for the two accessors, both under## [Unreleased], referencing this issue.Verification
Confirm
methcla_plugin_disksamplerbuilds. Run any test exercising disksampler load-and-play (checktests/for what exists).Behavioural equivalence (no audible / log diff expected):
synthDoneon end (non-loop).This plugin requires a Soundfile API Plugin to be registered (see
plugins/README.md).