scsynth-sys 0.1.0

Raw FFI bindings to a statically-linked SuperCollider scsynth engine.
// Driverless, synchronous (mRealTime=false) pump shim for scsynth on wasm32-unknown-unknown.
//
// Models World_NonRealTimeSynthesis (SC_World.cpp) - the canonical driverless loop - but without
// libsndfile / a command file: OSC packets are pushed in directly and the caller pumps audio in
// fixed blocks, reading world->mAudioBus. No FIFOs, scheduler, audio driver, or threads are used.
//
// Under mRealTime=false:
//   * World_New sets hw->mAudioDriver = nullptr (no SC_NewAudioDriver call), so we must NOT route
//     OSC through ProcessOSCPacket (it requires a driver and queues to a FIFO). Instead we call
//     PerformOSCBundle / PerformOSCMessage inline, exactly as World_NonRealTimeSynthesis does.
//   * CallSequencedCommand dispatches /d_recv etc. via CallEveryStage() - all four stages run
//     synchronously in this call - so /d_recv and /s_new complete before perform() returns.
//
// The trailing "fake implementations" mirror SC_WebAudio.cpp (the emscripten web build): they fill
// the link holes left by excluding SC_Reply.cpp, SC_ComPort.cpp and the device-driver backends.

#include "SC_CoreAudio.h"
#include "SC_HiddenWorld.h"
#include "SC_Prototypes.h"
#include "SC_World.h"
#include "SC_WorldOptions.h"
#include "OSC_Packet.h"
#include "SC_Reply.h"
#include "SC_ReplyImpl.hpp"

#include <cstring>

// Inline OSC entry points defined in SC_CoreAudio.cpp.
void PerformOSCBundle(World* inWorld, OSC_Packet* inPacket);
int PerformOSCMessage(World* inWorld, int inSize, char* inData, ReplyAddress* inReply);

// No-op print function (SC's default scprintf would otherwise call into stdio we do not provide).
static int wasm_print_func(const char*, va_list) { return 0; }

extern "C" {

// Create a driverless, synchronous world from a fully-built `WorldOptions`. The safe `World` wrapper
// (crates/scsynth) builds the options (channels, sample rate, block size, verbosity); we only FORCE
// the two invariants this shim relies on: mRealTime=false (no audio/NRT thread, no driver, no FIFOs)
// and mLoadGraphDefs=false (never touch the filesystem). The caller cannot meaningfully override
// these on wasm.
World* scsynth_wasm_new(WorldOptions* options) {
    if (!options)
        return nullptr;
    SetPrintFunc(wasm_print_func);

    options->mRealTime = false;
    options->mLoadGraphDefs = false;

    World* world = World_New(options);
    if (!world)
        return nullptr;

    // World_New does not call World_SetSampleRate / World_Start for the non-realtime path (the NRT
    // synthesis driver normally does); do it here so the rates and touched-bus state are valid.
    World_SetSampleRate(world, (double)options->mPreferredSampleRate);
    World_Start(world);
    return world;
}

// Feed one OSC packet (message or bundle) synchronously. /d_recv and /s_new complete in full before
// returning, because mRealTime=false routes sequenced commands through CallEveryStage(). Any
// /done/fail/n_end replies are delivered inline (same thread) via reply_func(reply_ctx); the safe
// wrapper passes its reply sink as reply_ctx and reads it back through scsynth_reply_context.
void scsynth_wasm_perform(World* world, const char* data, int size, ReplyFunc reply_func,
                          void* reply_ctx) {
    if (!world || !data || size <= 0)
        return;

    OSC_Packet packet;
    memset(&packet, 0, sizeof(packet));
    // PerformOSC* read from packet.mData; copy so the engine owns a stable, mutable buffer.
    char* buf = (char*)malloc((size_t)size);
    if (!buf)
        return;
    memcpy(buf, data, (size_t)size);
    packet.mData = buf;
    packet.mSize = size;
    packet.mReplyAddr.mReplyFunc = reply_func;
    packet.mReplyAddr.mReplyData = reply_ctx;
    packet.mReplyAddr.mProtocol = kUDP;
    packet.mReplyAddr.mPort = 0;
    packet.mReplyAddr.mSocket = -1;

    // An OSC bundle begins with '#bundle'; a plain message begins with '/'. Run inline at the head
    // of the current block (sample offset 0), exactly as the driverless NRT loop does.
    world->mSampleOffset = 0;
    world->mSubsampleOffset = 0.f;
    if (data[0] == '#') {
        packet.mIsBundle = true;
        PerformOSCBundle(world, &packet);
    } else {
        packet.mIsBundle = false;
        PerformOSCMessage(world, size, buf, &packet.mReplyAddr);
        world->mLocalErrorNotification = 0;
    }
    free(buf);
}

// Render audio in 64-frame blocks. Deinterleaves `in` into the input buses, runs the graph, and
// interleaves the output buses into `out` (zeroing untouched channels). Bus layout matches
// World_NonRealTimeSynthesis: outputs occupy the first mNumOutputs bus slots, inputs the next
// mNumInputs.
void scsynth_wasm_pump(World* world, const float* in, int inCh, float* out, int outCh, int nframes) {
    if (!world)
        return;

    const int bufLength = world->mBufLength;
    const int numOutputs = (int)world->mNumOutputs;
    const int numInputs = (int)world->mNumInputs;

    float* outputBuses = world->mAudioBus;
    float* inputBuses = world->mAudioBus + numOutputs * bufLength;
    int32* outputTouched = world->mAudioBusTouched;
    int32* inputTouched = world->mAudioBusTouched + numOutputs;

    for (int frame = 0; frame < nframes; frame += bufLength) {
        const int32 bufCounter = world->mBufCounter;
        const int block = (nframes - frame < bufLength) ? (nframes - frame) : bufLength;

        // Deinterleave available input channels into the input buses.
        if (in && inCh > 0) {
            for (int ch = 0; ch < numInputs && ch < inCh; ++ch) {
                float* dst = inputBuses + ch * bufLength;
                for (int k = 0; k < block; ++k)
                    dst[k] = in[(frame + k) * inCh + ch];
                for (int k = block; k < bufLength; ++k)
                    dst[k] = 0.f;
                inputTouched[ch] = bufCounter;
            }
        }

        world->mSampleOffset = 0;
        world->mSubsampleOffset = 0.f;
        World_Run(world);

        // Interleave the output buses into the caller's buffer, zeroing untouched channels.
        if (out && outCh > 0) {
            for (int ch = 0; ch < outCh; ++ch) {
                const bool touched = ch < numOutputs && outputTouched[ch] == bufCounter;
                const float* src = outputBuses + ch * bufLength;
                for (int k = 0; k < block; ++k)
                    out[(frame + k) * outCh + ch] = touched ? src[k] : 0.f;
            }
        }

        world->mBufCounter++;
    }
}

} // extern "C"

// ---------------------------------------------------------------------------
// Fake implementations (mirroring SC_WebAudio.cpp lines 277-318) filling the holes left by the
// sources we exclude. None of the thread/network functions are reached at runtime under
// mRealTime=false; the time functions return fixed values (the 440 Hz test uses SinOsc, which needs
// no entropy or wall-clock).
// ---------------------------------------------------------------------------

// SC_ReplyImpl.hpp holes (SC_Reply.cpp excluded; mAddress dropped on wasm).
void null_reply_func(struct ReplyAddress*, char*, int) {}
bool operator==(const ReplyAddress& a, const ReplyAddress& b) {
    return a.mProtocol == b.mProtocol && a.mPort == b.mPort && a.mSocket == b.mSocket;
}
bool operator<(const ReplyAddress& a, const ReplyAddress& b) {
    if (a.mPort != b.mPort)
        return a.mPort < b.mPort;
    if (a.mSocket != b.mSocket)
        return a.mSocket < b.mSocket;
    return a.mProtocol < b.mProtocol;
}

// SC_ComPort.cpp holes (excluded). The asio thread is never started under mRealTime=false.
namespace scsynth {
void startAsioThread() {}
void stopAsioThread() {}
bool asioThreadStarted() { return true; }
}

// SC_NewAudioDriver is referenced by World_New's mRealTime branch (and so must link) but is never
// called under mRealTime=false. Return nullptr; reaching it would be a logic error.
SC_AudioDriver* SC_NewAudioDriver(World*) { return nullptr; }

// SC_CoreAudio.cpp references initializeScheduler() / server_timeseed() / oscTimeNow() that the
// device backends normally provide. With no driver these are trivial.
void initializeScheduler() {}

extern "C" {
int64 oscTimeNow() { return 0; }
int32 server_timeseed() {
    // A fixed seed: deterministic and sufficient (SinOsc needs no entropy). Real entropy would only
    // matter for Noise UGens, which the synthesis test does not use.
    static int32 count = 0;
    return count++;
}

// SC_ComPort.cpp holes (excluded): no real sockets are opened on wasm.
int World_OpenUDP(World*, const char*, int) { return 1; }
int World_OpenTCP(World*, const char*, int, int, int) { return 1; }
}