Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Logue SDK

The Korg logue SDK allows developing custom oscillators and effects for Korg’s logue-compatible hardware. Units compile to ARM Cortex-M4/M7 bare-metal code - see ARM for toolchain setup.

Hardware targets

PlatformUnit extensionCPUNotes
NTS-1 mkI.ntkdigunitCortex-M4FOriginal pocket synth
NTS-1 mkII.nts1mkiiunitCortex-M7Current pocket synth
NTS-3 Kaoss.nts3unitCortex-M7Primary target
minilogue xd.mnlgxdunitCortex-M4FPolyphonic synth
prologue.prlgunitCortex-M4FPolyphonic synth
drumlogue.drmlgunitCortex-A7Drum machine

Unit types

TypeHeaderDescription
oscunit.hCustom oscillator / sound generator
modfxunit_modfx.hModulation effect (chorus, flanger)
delfxunit_delfx.hDelay effect
revfxunit_revfx.hReverb effect
genericfxunit_genericfx.hNTS-3 background effect (always running)

genericfx is NTS-3 Kaoss exclusive. Unlike other effect types, it can process audio continuously in the background regardless of touch pad state.

GenericFX - NTS-3 background effects

Normal effect units (modfx, delfx, revfx) receive audio via the in parameter in unit_render, which is routed through the effect on/off state. When the touch pad is not held (and HOLD/XY Freeze is off), the input is bypassed - the effect stops processing.

genericfx units can sidestep this by calling get_raw_input() through the runtime context, which returns the raw audio input unaffected by touch pad state. The effect runs continuously in the background.

Storing the runtime descriptor

Save the descriptor passed to unit_init - it carries the hooks needed later in unit_render:

#include "unit_genericfx.h"

static unit_runtime_desc_t s_runtime_desc;

__unit_callback int8_t unit_init(const unit_runtime_desc_t *desc) {
    if (!desc) return k_unit_err_undef;
    if (desc->target != unit_header.common.target) return k_unit_err_target;
    if (!UNIT_API_IS_COMPAT(desc->api)) return k_unit_err_api_version;

    s_runtime_desc = *desc;   // store for get_raw_input() access

    // optional: SDRAM allocation for delay buffers etc.
    if (s_effect_instance.getBufferSize() > 0) {
        float *buf = (float *)desc->hooks.sdram_alloc(
            s_effect_instance.getBufferSize() * sizeof(float));
        if (!buf) return k_unit_err_memory;
        s_effect_instance.init(buf);
    }
    return k_unit_err_none;
}

Background render

__unit_callback void unit_render(const float *in, float *out, uint32_t frames) {
    // Cast runtime context to genericfx type to access get_raw_input()
    const unit_runtime_genericfx_context_t *ctxt =
        static_cast<const unit_runtime_genericfx_context_t *>(
            s_runtime_desc.hooks.runtime_context);

    // get_raw_input() bypasses effect on/off routing - always live audio
    const float *raw_input = ctxt->get_raw_input();
    s_effect_instance.process(raw_input, out, frames);
}

Using raw_input instead of in means the effect processes audio continuously without requiring the HOLD button.

ApproachInput sourceRequires HOLD?
Normal effectin parameterYes, when pad not touched
Background effectctxt->get_raw_input()No - always running

NTS-3 extras

genericfx also receives XY pad events via unit_touch_event, not available on other platforms:

__unit_callback void unit_touch_event(uint8_t id, uint8_t phase, uint32_t x, uint32_t y) {
    s_effect_instance.touchEvent(id, phase, x, y);
}

Memory available: 32 KB SRAM + 3 MB SDRAM (allocated via desc->hooks.sdram_alloc).

Template: logue-sdk/platform/nts-3_kaoss/dummy-genericfx/

Callback architecture

The SDK calls MIDI callbacks (unit_note_on, unit_note_off) from a different context than unit_render. Doing DSP state changes inside MIDI callbacks races against the audio thread.

Rule: MIDI callbacks only enqueue events. All DSP work happens in unit_render.

unit_note_on  → push to ring buffer only
unit_note_off → push to ring buffer only
unit_render   → drain ring buffer → process events → render audio

SPSC ring buffer pattern

enum NoteEventType : uint8_t { EVT_NOTE_ON = 0, EVT_NOTE_OFF = 1, EVT_ALL_NOTE_OFF = 2 };
struct NoteEvent { NoteEventType type; uint8_t note; uint8_t velocity; };

static constexpr int NOTE_QUEUE_SIZE = 16;
NoteEvent note_queue_[NOTE_QUEUE_SIZE];
std::atomic<uint8_t> queue_write_;  // producer: MIDI callbacks
std::atomic<uint8_t> queue_read_;   // consumer: Render()

// Producer (MIDI callback context)
inline void enqueue(NoteEventType type, uint8_t note, uint8_t vel) {
    uint8_t w    = queue_write_.load(std::memory_order_relaxed);
    uint8_t next = (w + 1) % NOTE_QUEUE_SIZE;
    if (next != queue_read_.load(std::memory_order_acquire)) {
        note_queue_[w] = {type, note, vel};
        queue_write_.store(next, std::memory_order_release);
    }
}

// Consumer (render context)
inline void drainNoteQueue() {
    uint8_t w = queue_write_.load(std::memory_order_acquire);
    uint8_t r = queue_read_.load(std::memory_order_relaxed);
    while (r != w) {
        NoteEvent evt = note_queue_[r];
        queue_read_.store((r + 1) % NOTE_QUEUE_SIZE, std::memory_order_release);
        switch (evt.type) {
            case EVT_NOTE_ON:      processNoteOn(evt.note, evt.velocity); break;
            case EVT_NOTE_OFF:     processNoteOff(evt.note);              break;
            case EVT_ALL_NOTE_OFF: processAllNoteOff();                   break;
        }
        r = queue_read_.load(std::memory_order_relaxed);
        w = queue_write_.load(std::memory_order_acquire);
    }
}

fast_inline void Render(float* out, size_t frames) {
    drainNoteQueue();   // always first
    for (size_t i = 0; i < frames; i++) {
        // DSP processing
    }
}

VLA pitfall

unit_render receives frames as a runtime value. Stack-allocating with a runtime size is a C99 VLA - not valid C++ and dangerous on Cortex-M with limited stack:

// WRONG: VLA, not valid C++
float buf[frames * 2];

// CORRECT: fixed max (NTS-1 mkII block size is always ≤ 256 frames)
float buf[256 * 2];

Minimal unit structure

#include "unit.h"   // from logue-sdk platform headers

static UnitHeader unit_header = {
    .header_size  = sizeof(UnitHeader),
    .target       = UNIT_TARGET_PLATFORM | k_unit_module_osc,
    .api          = UNIT_API_VERSION,
    .dev_id       = 0x00000000,
    .unit_id      = 0x00000000,
    .version      = 0x00010000,
    .name         = "MyUnit",
    .num_params   = 2,
    .params       = {{ "Param1", 0, 0, 100, k_unit_param_type_none },
                     { "Param2", 0, 0, 100, k_unit_param_type_none }},
};

__unit_callback void unit_init(const UnitRuntimeDesc* desc) { }
__unit_callback void unit_teardown() { }
__unit_callback void unit_reset() { }
__unit_callback void unit_resume() { }
__unit_callback void unit_suspend() { }
__unit_callback void unit_note_on(uint8_t note, uint8_t vel) { /* enqueue only */ }
__unit_callback void unit_note_off(uint8_t note) { /* enqueue only */ }
__unit_callback void unit_all_note_off() { /* enqueue only */ }
__unit_callback void unit_set_param_value(uint8_t id, int32_t val) { }
__unit_callback void unit_render(float* in, float* out, uint32_t frames) {
    drainNoteQueue();
    // process audio
}

Building

# From an SDK platform directory, e.g. nts-3_kaoss/
cd ~/Projects/logue-sdk/platform/nts-3_kaoss/myunit
make

# Output: myunit.nts3unit
# Install via logue-tool or USB drag-and-drop

Toolchain: arm-none-eabi- for Cortex-M targets, arm-linux-gnueabihf- for drumlogue. See ARM for installation.

Deployment paths

The same DSP core can target multiple runtimes beyond bare-metal hardware:

PathFormatToolUse case
Hardware.nts3unit / .nts1mkiiunitlogue-tool / USBProduction on device
ARM Linux host.nts3unitRegroovelizerDesktop / Raspberry Pi / Android
WebAssembly.wasmunitRehostBrowser, cross-platform testing
Pure Data → C.nts3unithvccPd patches compiled to Logue units
Faust → C.nts3unitfaust2logueFaust DSP compiled to Logue units

The Regroovelizer loads .nts3unit ELF files on ARM Linux by parsing relocations and resolving logue SDK symbols at runtime - allowing the same unit binary to run on desktop for development before flashing to hardware.

Resources