Skip to content

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

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions