Skip to content

Migrate disksampler plugin to the C++ wrapper API #144

@kaoskorobase

Description

@kaoskorobase

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 selfthis 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).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions