Skip to content

Latest commit

 

History

History
1081 lines (767 loc) · 32.5 KB

File metadata and controls

1081 lines (767 loc) · 32.5 KB

LabSoundC User's Manual

1. Overview

LabSoundC is a pure C binding for the LabSound audio engine, a WebAudio-derived C++ library for real-time audio synthesis and processing. It provides a flat, opaque-handle API suitable for embedding in game engines, synthesizers, and visual node-graph editors.

Design principles:

  • Opaque handles -- all objects (ls_Node, ls_Pin, ls_Conn, ls_Bus) are lightweight 32-bit IDs. The user never manages C++ memory.
  • Single ownership -- the ls_Context owns everything. Create it, use it, destroy it.
  • Pin model -- every node exposes a uniform list of pins (inputs, outputs, params, settings) suitable for driving a node-graph UI.
  • Type-safe connections -- typed wrappers (ls_Output, ls_Input, ls_ParamInput) prevent accidental in/out swaps at the call site.
  • Normalized names -- all string lookups are case-insensitive and space-insensitive ("Low Pass" = "lowpass" = "LOWPASS").

LabSoundC wraps approximately 30 node types from the LabSound registry, covering oscillators, filters, noise generators, dynamics, spatial audio, sample playback, custom DSP functions, and more.

Target environments: real-time game audio, procedural sound design, synthesizer UIs, live performance tools.


2. Building

Prerequisites

  • CMake 3.15+
  • C11 / C++17 compiler
  • LabSound source tree (sibling directory or set LABSOUND_ROOT)

Basic Build

mkdir build && cd build
cmake .. -DLABSOUND_ROOT=/path/to/LabSound
cmake --build .

CMake Options

Option Default Description
LABSOUNDC_BUILD_SHARED ON Build shared library (.dylib/.dll/.so) for FFI bindings
LABSOUNDC_BUILD_EXAMPLES ON Build labsoundc_smoke and labsoundc_demo executables
LABSOUND_USE_RTAUDIO (auto) Use RtAudio backend
LABSOUND_USE_MINIAUDIO (auto) Use MiniAudio backend

If neither backend is specified, the build picks RtAudio on macOS and MiniAudio elsewhere.

Platform Notes

macOS: Links against system frameworks: AudioToolbox, AudioUnit, Accelerate, CoreAudio, Cocoa. No additional dependencies needed.

Linux: Links against pthread and dl. The audio backend will need either ALSA or PulseAudio development headers depending on the selected backend.

Windows: Standard MSVC build. The shared library exports symbols via __declspec(dllexport).

Static vs. Shared

The static library (LabSoundC) is always built. The shared library (LabSoundC_shared, output name libLabSoundC.dylib / LabSoundC.dll) is built when LABSOUNDC_BUILD_SHARED=ON. The shared library is what you want for Python/ctypes or other FFI consumers.

Symbol Namespacing

Define LSNAMESPACE at compile time to prefix all public symbols, preventing collisions when multiple copies of LabSoundC link into one binary:

#define LSNAMESPACE myapp_ls_
#include <LabSoundC/LabSoundC.h>
// ls_context_create becomes myapp_ls_context_create

3. Core Concepts

Context

The ls_Context is the top-level object. It owns the audio device, the node graph, and all allocated handles. Typical lifecycle:

ls_ContextDesc desc = {0};  // all defaults
ls_Context* ctx = ls_context_create(&desc);
// ... use the API ...
ls_context_destroy(ctx);

Configuration via ls_ContextDesc:

  • sample_rate -- 0 for system default
  • output_channels -- 0 for default (2)
  • input_channels -- 0 for no mic; >0 enables mic input
  • offline -- true for non-realtime rendering
  • allocator -- custom malloc/free (NULL for system default)

Nodes

Nodes are the processing units. Create by type name from the registry:

ls_Node osc = ls_node_create(ctx, "Oscillator");
ls_Node gain = ls_node_create(ctx, "Gain");
ls_Node dest = ls_destination_node(ctx);  // built-in output

The destination node is created automatically with the context. It represents the audio device output.

Source nodes (Oscillator, Noise, SampledAudio, Function) must be started:

ls_node_start(ctx, osc, 0.0);  // start immediately
ls_node_stop(ctx, osc, 0.0);   // stop immediately

Pins

Every node exposes a flat list of pins -- a unified handle for all node attachment points. Four kinds exist:

Kind Enum Meaning
BUS_IN LS_PIN_BUS_IN Audio input bus (receives audio signal)
BUS_OUT LS_PIN_BUS_OUT Audio output bus (produces audio signal)
PARAM LS_PIN_PARAM Time-varying float parameter (connectable, automatable)
SETTING LS_PIN_SETTING Static configuration (bool, int, float, enum, or bus)

Pins are always ordered: all BUS_IN first, then BUS_OUT, then PARAM, then SETTING.

Enumerate pins on a node:

int count = ls_node_pin_count(ctx, osc);
for (int i = 0; i < count; ++i) {
    ls_Pin p = ls_node_pin(ctx, osc, i);
    printf("%s: %s\n",
        ls_pin_kind(ctx, p) == LS_PIN_PARAM ? "PARAM" : "other",
        ls_pin_name(ctx, p));
}

Look up pins by name (normalized, case-insensitive):

ls_Pin freq_pin = ls_node_pin_by_name(ctx, osc, "frequency");

Typed Wrappers

To prevent accidentally swapping source and destination in connection calls, the API uses typed wrapper structs:

  • ls_Output -- wraps a BUS_OUT pin
  • ls_Input -- wraps a BUS_IN pin
  • ls_ParamInput -- wraps a PARAM pin

Create them via assertion functions (returns invalid on mismatch):

ls_Output out = ls_as_output(ctx, some_pin);    // asserts BUS_OUT
ls_Input  in  = ls_as_input(ctx, some_pin);     // asserts BUS_IN
ls_ParamInput p = ls_as_param_input(ctx, pin);  // asserts PARAM

Or use the convenience direct accessors (recommended for programmatic use):

ls_Output out = ls_node_output(ctx, osc, 0);       // first output
ls_Input  in  = ls_node_input(ctx, gain, 0);       // first input
ls_ParamInput freq = ls_node_param(ctx, osc, "frequency");  // by name
ls_Pin type_setting = ls_node_setting(ctx, osc, "type");    // by name

All typed wrappers embed the underlying ls_Pin in a .pin field for back-access.

Connections

Two connection types:

// Audio routing: output bus -> input bus
ls_Conn c1 = ls_connect(ctx, ls_node_output(ctx, osc, 0),
                              ls_node_input(ctx, gain, 0));

// Parameter modulation: output bus -> param input
ls_Conn c2 = ls_connect_param(ctx, ls_node_output(ctx, lfo, 0),
                                   ls_node_param(ctx, osc, "frequency"));

Both return an ls_Conn handle for later disconnect:

ls_disconnect(ctx, c1);

Buses

Audio sample data loaded from files. Commonly used with SampledAudioNode:

ls_Bus bus = ls_bus_create_from_file(ctx, "kick.wav", false);
ls_Pin source_bus = ls_node_setting(ctx, sampler, "sourceBus");
ls_setting_set_bus(ctx, source_bus, bus);

Supported formats depend on the libnyquist backend (WAV, OGG, FLAC, MP3, etc.).

Normalized Names

All name lookups strip spaces and compare case-insensitively:

  • "Low Pass" = "lowpass" = "LOWPASS" = "Low Pass"
  • "BiquadFilter" = "biquadfilter" = "BIQUADFILTER"

This applies to: ls_node_create, ls_node_pin_by_name, ls_node_param, ls_node_setting, ls_setting_set_enum_by_name.

Error Handling

The context stores the last error. Reading it clears it:

ls_Result err = ls_last_error(ctx);
if (err != LS_OK) {
    printf("Error: %s\n", ls_result_string(err));
}

Error codes:

Code Meaning
LS_OK No error
LS_ERROR_INVALID_HANDLE Handle is stale or null
LS_ERROR_TYPE_MISMATCH Wrong pin kind for operation
LS_ERROR_NOT_FOUND Name lookup failed
LS_ERROR_INVALID_OPERATION Operation not valid for this node type
LS_ERROR_FILE_NOT_FOUND Audio file could not be loaded

4. C API Quick Reference

Context Lifecycle

ls_Context* ls_context_create(const ls_ContextDesc* desc)
void        ls_context_destroy(ls_Context* ctx)
double      ls_context_current_time(ls_Context* ctx)
float       ls_context_sample_rate(ls_Context* ctx)
void        ls_context_update(ls_Context* ctx)
void        ls_context_suspend(ls_Context* ctx)
void        ls_context_resume(ls_Context* ctx)

Error Reporting

ls_Result   ls_last_error(ls_Context* ctx)
const char* ls_result_string(ls_Result r)

Registry

int         ls_registry_count(ls_Context* ctx)
const char* ls_registry_name(ls_Context* ctx, int index)

Node Lifecycle & Scheduling

ls_Node     ls_node_create(ls_Context* ctx, const char* type_name)
void        ls_node_destroy(ls_Context* ctx, ls_Node node)
const char* ls_node_type_name(ls_Context* ctx, ls_Node node)
ls_Node     ls_destination_node(ls_Context* ctx)
void        ls_node_start(ls_Context* ctx, ls_Node node, double when)
void        ls_node_stop(ls_Context* ctx, ls_Node node, double when)
const char* ls_node_scheduling_state(ls_Context* ctx, ls_Node node)
void        ls_node_schedule(ls_Context* ctx, ls_Node node, double when, int loop_count, double grain_offset, double grain_duration)
void        ls_node_set_on_ended(ls_Context* ctx, ls_Node node, ls_OnEndedCallback callback, void* userdata)

Pin Enumeration & Introspection

int         ls_node_pin_count(ls_Context* ctx, ls_Node node)
ls_Pin      ls_node_pin(ls_Context* ctx, ls_Node node, int index)
ls_Pin      ls_node_pin_by_name(ls_Context* ctx, ls_Node node, const char* name)
ls_PinKind  ls_pin_kind(ls_Context* ctx, ls_Pin pin)
const char* ls_pin_name(ls_Context* ctx, ls_Pin pin)
ls_DataType ls_pin_data_type(ls_Context* ctx, ls_Pin pin)
float       ls_param_default(ls_Context* ctx, ls_Pin param)
float       ls_param_min(ls_Context* ctx, ls_Pin param)
float       ls_param_max(ls_Context* ctx, ls_Pin param)
int         ls_setting_enum_count(ls_Context* ctx, ls_Pin setting)
const char* ls_setting_enum_name(ls_Context* ctx, ls_Pin setting, int index)

Typed Pin Wrappers

ls_Output     ls_as_output(ls_Context* ctx, ls_Pin pin)
ls_Input      ls_as_input(ls_Context* ctx, ls_Pin pin)
ls_ParamInput ls_as_param_input(ls_Context* ctx, ls_Pin pin)

Direct Typed Accessors

ls_Output     ls_node_output(ls_Context* ctx, ls_Node node, int index)
ls_Input      ls_node_input(ls_Context* ctx, ls_Node node, int index)
ls_ParamInput ls_node_param(ls_Context* ctx, ls_Node node, const char* name)
ls_Pin        ls_node_setting(ls_Context* ctx, ls_Node node, const char* name)

Param Values & Automation

float ls_param_get_value(ls_Context* ctx, ls_Pin param)
void  ls_param_set_value(ls_Context* ctx, ls_Pin param, float value)
void  ls_param_set_value_at_time(ls_Context* ctx, ls_Pin param, float value, double time)
void  ls_param_linear_ramp(ls_Context* ctx, ls_Pin param, float value, double end_time)
void  ls_param_exponential_ramp(ls_Context* ctx, ls_Pin param, float value, double end_time)
void  ls_param_set_target(ls_Context* ctx, ls_Pin param, float target, double time, double time_constant)
void  ls_param_cancel_scheduled(ls_Context* ctx, ls_Pin param, double start_time)

Setting Values

bool        ls_setting_get_bool(ls_Context* ctx, ls_Pin setting)
int         ls_setting_get_int(ls_Context* ctx, ls_Pin setting)
float       ls_setting_get_float(ls_Context* ctx, ls_Pin setting)
uint32_t    ls_setting_get_enum_value(ls_Context* ctx, ls_Pin setting)
const char* ls_setting_get_enum_current_name(ls_Context* ctx, ls_Pin setting)
void        ls_setting_set_bool(ls_Context* ctx, ls_Pin setting, bool value)
void        ls_setting_set_int(ls_Context* ctx, ls_Pin setting, int value)
void        ls_setting_set_float(ls_Context* ctx, ls_Pin setting, float value)
void        ls_setting_set_enum(ls_Context* ctx, ls_Pin setting, int value)
void        ls_setting_set_enum_by_name(ls_Context* ctx, ls_Pin setting, const char* name)
void        ls_setting_set_bus(ls_Context* ctx, ls_Pin setting, ls_Bus bus)

Graph Connections

ls_Conn ls_connect(ls_Context* ctx, ls_Output from, ls_Input to)
ls_Conn ls_connect_param(ls_Context* ctx, ls_Output from, ls_ParamInput to)
void    ls_disconnect(ls_Context* ctx, ls_Conn conn)

Bus Management

ls_Bus ls_bus_create_from_file(ls_Context* ctx, const char* path, bool mix_to_mono)
void   ls_bus_destroy(ls_Context* ctx, ls_Bus bus)

Listener (3D Audio)

void ls_listener_set_position(ls_Context* ctx, float x, float y, float z)
void ls_listener_set_forward(ls_Context* ctx, float x, float y, float z)
void ls_listener_set_up(ls_Context* ctx, float x, float y, float z)
void ls_listener_set_velocity(ls_Context* ctx, float x, float y, float z)

Extended: Dynamic Outputs & Custom DSP

void ls_node_create_output(ls_Context* ctx, ls_Node node, const char* name, int channels)
void ls_function_node_set_callback(ls_Context* ctx, ls_Node node, ls_FunctionCallback callback, void* userdata)

Callback signature:

typedef void (*ls_FunctionCallback)(int channel, float* buffer, int buffer_size, void* userdata);

Diagnostics

void   ls_node_diagnose(ls_Context* ctx, ls_Node node)
double ls_node_graph_time(ls_Context* ctx, ls_Node node)
double ls_node_self_time(ls_Context* ctx, ls_Node node)
void   ls_debug_print_graph(ls_Context* ctx)

5. Python Bindings Guide

The Python bindings wrap LabSoundC via ctypes, providing a Pythonic interface with context managers, method-style access, and automation helpers.

Installation

export LABSOUNDC_LIB=/path/to/build/libLabSoundC.dylib
export PYTHONPATH=/path/to/LabSoundC/bindings/python:$PYTHONPATH

Context Manager Pattern

from labsoundc import Context

with Context() as ctx:
    osc = ctx.create_node("Oscillator")
    gain = ctx.create_node("Gain")
    dest = ctx.destination_node()
    # ... build graph ...
# context destroyed automatically

Creating Nodes and Connecting

from labsoundc import Context
import time

with Context() as ctx:
    # Create nodes
    osc = ctx.create_node("Oscillator")
    gain = ctx.create_node("Gain")
    dest = ctx.destination_node()

    # Set parameters
    freq = osc.param("frequency")
    freq.set_value(440.0)
    gain.param("gain").set_value(0.25)

    # Set oscillator type
    osc.setting("type").set_enum_by_name("sine")

    # Connect: osc -> gain -> destination
    ctx.connect(osc.output(0), gain.input(0))
    ctx.connect(gain.output(0), dest.input(0))

    # Start and play
    osc.start(0.0)
    time.sleep(2.0)
    osc.stop(0.0)

Parameter Automation

freq = osc.param("frequency")
freq.set_value(440.0)
freq.ramp_linear(880.0, ctx.current_time() + 1.0)   # glide to 880 Hz over 1 second
freq.ramp_exponential(220.0, ctx.current_time() + 2.0)
freq.set_target(440.0, ctx.current_time() + 3.0, time_constant=0.1)

FunctionNode Callbacks with ctypes

import ctypes

# Define the callback signature
FUNC_CB = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.POINTER(ctypes.c_float),
                           ctypes.c_int, ctypes.POINTER(None))

phase = [0.0]

@FUNC_CB
def sine_generator(channel, buffer, frames, userdata):
    import math
    dt = 1.0 / 48000.0
    for i in range(frames):
        buffer[i] = math.sin(2.0 * math.pi * 440.0 * phase[0])
        phase[0] += dt

fn_node = ctx.create_node("Function")
fn_node.set_callback(sine_generator)

Complete Example: Red Alert in 20 Lines

from labsoundc import Context
import time

with Context() as ctx:
    osc = ctx.create_node("Oscillator")
    osc.setting("type").set_enum_by_name("sawtooth")
    osc.param("frequency").set_value(520.0)

    filt = ctx.create_node("BiquadFilter")
    filt.setting("type").set_enum_by_name("bandpass")
    filt.param("frequency").set_value(740.0)
    filt.param("q").set_value(12.0)

    gain = ctx.create_node("Gain")
    gain.param("gain").set_value(0.3)
    dest = ctx.destination_node()

    ctx.connect(osc.output(0), filt.input(0))
    ctx.connect(filt.output(0), gain.input(0))
    ctx.connect(gain.output(0), dest.input(0))

    osc.start(0.0)
    time.sleep(3.0)
    osc.stop(0.0)

6. Zig Bindings Guide

The Zig bindings provide a thin, idiomatic wrapper over the LabSoundC static library with method syntax, error sets, and defer-based cleanup.

Build Setup (build.zig)

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "my_audio_app",
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = optimize,
    });

    // Link LabSoundC static library
    exe.addLibraryPath(.{ .path = "/path/to/LabSoundC/build" });
    exe.addIncludePath(.{ .path = "/path/to/LabSoundC/include" });
    exe.linkSystemLibrary("LabSoundC");
    exe.linkSystemLibrary("LabSound");
    exe.linkSystemLibrary("c++");

    // macOS frameworks
    exe.linkFramework("AudioToolbox");
    exe.linkFramework("AudioUnit");
    exe.linkFramework("Accelerate");
    exe.linkFramework("CoreAudio");
    exe.linkFramework("Cocoa");

    b.installArtifact(exe);
}

Idiomatic Patterns

const ls = @import("labsoundc");

pub fn main() !void {
    var ctx = try ls.Context.init(.{});
    defer ctx.deinit();

    const osc = try ctx.createNode("Oscillator");
    defer ctx.destroyNode(osc);

    const gain = try ctx.createNode("Gain");
    defer ctx.destroyNode(gain);

    const dest = ctx.destinationNode();

    // Method syntax for parameters
    const freq = osc.param(ctx, "frequency");
    freq.setValue(ctx, 440.0);

    gain.param(ctx, "gain").setValue(ctx, 0.25);

    // Connect graph
    try ctx.connect(osc.output(ctx, 0), gain.input(ctx, 0));
    try ctx.connect(gain.output(ctx, 0), dest.input(ctx, 0));

    // Play
    osc.start(ctx, 0.0);
    std.time.sleep(2 * std.time.ns_per_s);
    osc.stop(ctx, 0.0);
}

Error Handling

The Zig bindings map ls_Result to a Zig error set:

const LsError = error{
    InvalidHandle,
    TypeMismatch,
    NotFound,
    InvalidOperation,
    FileNotFound,
};

Functions that can fail return LsError!T. Use standard Zig error handling:

const node = ctx.createNode("Oscillator") catch |err| {
    std.log.err("Failed to create node: {}", .{err});
    return err;
};

Complete Example: Oscillator

const std = @import("std");
const ls = @import("labsoundc");

pub fn main() !void {
    var ctx = try ls.Context.init(.{ .sample_rate = 48000 });
    defer ctx.deinit();

    const osc = try ctx.createNode("Oscillator");
    defer ctx.destroyNode(osc);

    osc.setting(ctx, "type").setEnumByName(ctx, "sawtooth");
    osc.param(ctx, "frequency").setValue(ctx, 220.0);
    osc.param(ctx, "amplitude").setValue(ctx, 0.5);

    const dest = ctx.destinationNode();
    try ctx.connect(osc.output(ctx, 0), dest.input(ctx, 0));

    osc.start(ctx, 0.0);
    std.log.info("Playing 220 Hz sawtooth...", .{});
    std.time.sleep(3 * std.time.ns_per_s);
    osc.stop(ctx, 0.0);
}

7. Procedural Audio Cookbook

These four effects from demo.c demonstrate fundamental procedural audio techniques using only the LabSoundC API. No sample files required.

7.1 Red Alert Warble

A classic alarm siren: an FM sweep drives a sawtooth oscillator through a parallel resonant filter bank.

Signal flow:

 [FunctionNode:sweep] -.  (freq control)
                        \
                         v
 [Oscillator:sawtooth] ---> [Gain] --+--> [BPF 740Hz  Q=12] --+
                                     +--> [BPF 1400Hz Q=12] --+--> [Gain:mix] --> [Dest]
                                     +--> [BPF 1500Hz Q=12] --+
                                     +--> [BPF 1600Hz Q=12] --+
                                                                ^
 [FunctionNode:env] -----------------------------------(gain control)

How it works:

  1. Sweep function -- A FunctionNode generates a cyclic control signal with a 1.2-second period. For 0.9 seconds, it ramps linearly from 487 Hz to 847 Hz. For the remaining 0.3 seconds, it outputs 0 (silence gap). This signal drives the oscillator's frequency param.

  2. Sawtooth oscillator -- Produces a harmonically rich tone whose pitch sweeps with the control signal. Sawtooth is chosen for its full harmonic spectrum.

  3. Resonant filter bank -- Four parallel bandpass filters (740, 1400, 1500, 1600 Hz) with Q=12 select formant-like resonances, giving the alarm its characteristic "vowel" quality.

  4. Envelope function -- A second FunctionNode outputs 0.333 during the sweep's on-time and 0.0 during silence, driving the mix gain param. This creates the on/off pulsing.

Key parameter values:

  • Sweep range: 487--847 Hz over 0.9s, 0.3s silence
  • Filter bank: 740, 1400, 1500, 1600 Hz, all Q=12
  • Mix gain: 0.2 (static) modulated by envelope (0 or 0.333)

7.2 Twittering Bird

Rapid FM chirps with randomized timing simulate a small songbird.

Signal flow:

 [FunctionNode:freq_mod] ---.(frequency control)
                             \
                              v
 [Oscillator:sine 3000Hz] -----> [Gain] -----> [Dest]
                                    ^
 [FunctionNode:chirp_env] ----.(gain control)

How it works:

  1. Frequency modulation -- A FunctionNode outputs a sinusoidal sweep: 2500 + 2000 * sin(2*pi*8*t). This oscillates the carrier frequency between 500 and 4500 Hz at 8 Hz, producing the characteristic chirp FM pattern.

  2. Chirp envelope -- Another FunctionNode generates short amplitude bursts. Each chirp lasts 30--100 ms (randomized). The envelope shape is fast attack (10% of chirp duration), slow linear decay (90%). Gaps between chirps are 40--200 ms (randomized via LCG PRNG).

  3. Carrier oscillator -- A sine at ~3000 Hz base frequency, frequency-modulated by the FM function. The sine carrier keeps the tone pure and bird-like.

Key parameter values:

  • Carrier: sine, 3000 Hz base
  • FM: 2500 +/- 2000 Hz at 8 Hz modulation rate
  • Chirp duration: 30--100 ms
  • Gap duration: 40--200 ms
  • Envelope: 10% attack, 90% decay (linear)

7.3 Marble in a Tin Can

Short noise impulses excited through resonant filters simulate a small hard object bouncing inside a metal container.

Signal flow:

 [Noise:white] -----> [Gain:gate] --+--> [BPF 1800Hz Q=30] --+
                         ^          +--> [BPF 3200Hz Q=25] --+--> [Gain:mix 0.5] --> [Dest]
                         |          +--> [BPF 5400Hz Q=20] --+
 [FunctionNode:gate] ---.(gain control)

How it works:

  1. Gate function -- A FunctionNode generates very short impulses (2--8 ms) at irregular intervals (50--250 ms). Each impulse has a sharp linear decay (instant attack, linear fall to zero). This simulates the marble striking the can surface.

  2. Noise source -- White noise provides the broadband "click" excitation. The gate chops it into tiny bursts.

  3. Resonant filter bank -- Three bandpass filters model the tin can's resonant modes:

    • 1800 Hz, Q=30 -- fundamental mode
    • 3200 Hz, Q=25 -- second harmonic
    • 5400 Hz, Q=20 -- third harmonic

    High Q values produce ringing metallic tones. The descending Q values simulate higher modes decaying faster (realistic for metal).

  4. Randomized timing -- An LCG pseudo-random number generator varies both the hit duration and the interval, simulating the irregular bouncing of a marble under gravity.

Key parameter values:

  • Hit duration: 2--8 ms (randomized)
  • Hit interval: 50--250 ms (randomized)
  • Filter modes: 1800/3200/5400 Hz
  • Q values: 30/25/20
  • Mix gain: 0.5

7.4 Passing Train

Filtered noise with a rhythmic double-pulse "chug" pattern, sweeping from left to right with distance-based gain and Doppler-simulated filter shift.

Signal flow:

 [Noise:white] --> [LPF 800Hz Q=1.5] --> [Gain:gated] --> [StereoPanner] --> [Gain:master] --> [Dest]
                                             ^
 [FunctionNode:chug] ----------------------.(gain control)

How it works:

  1. Chug rhythm -- A FunctionNode generates a double-pulse pattern at ~3 Hz (0.33s period). Pulse 1: 40 ms linear decay from 1.0. Pulse 2: 30 ms linear decay from 0.7, offset 80 ms after pulse 1. This "chug-chug" mimics a steam locomotive's exhaust rhythm.

  2. Noise + lowpass -- White noise through a lowpass filter at 800 Hz, Q=1.5 produces the breathy "chuff" texture of steam.

  3. Stereo pan sweep -- Over 8 seconds (80 steps), the StereoPanner pan value sweeps from -1.0 (hard left) to +1.0 (hard right).

  4. Distance attenuation -- The master gain follows a curve based on distance from center: 0.15 + 0.55 * (1 - dist^2) where dist = |t - 0.5| * 2. Loudest at the midpoint (train beside listener), quieter at edges.

  5. Doppler shift -- The lowpass cutoff shifts: 800 + 200 * (0.5 - t). Approaching = higher (900 Hz), receding = lower (700 Hz). This approximates the perceived frequency shift.

Key parameter values:

  • Chug period: 0.33s (double pulse)
  • LPF: 800 Hz +/- 200 Hz (Doppler), Q=1.5
  • Pan sweep: -1.0 to +1.0 over 8 seconds
  • Gain curve: 0.15 (far) to 0.70 (near)

8. Appendix: Node Type Reference

All node types registered in the LabSound registry, accessible via ls_node_create(ctx, "TypeName").

Core Nodes

Oscillator

Generates periodic waveforms.

  • Params: frequency (default 440, range 0--100000), detune (cents), amplitude (default 1.0), bias (default 0.0)
  • Settings: type (enum: None, Sine, FastSine, Square, Sawtooth, FallingSawtooth, Triangle, Custom)
  • Inputs: 0
  • Outputs: 1

Gain

Applies volume change with de-zippering.

  • Params: gain (default 1.0, range 0--10000)
  • Settings: none
  • Inputs: 1
  • Outputs: 1

BiquadFilter

Second-order IIR filter with multiple modes.

  • Params: frequency (default 350, range 0--samplerate/2), Q (default 1.0), gain (dB, for shelving/peaking modes), detune (cents)
  • Settings: type (enum: None, LowPass, HighPass, BandPass, LowShelf, HighShelf, Peaking, Notch, AllPass)
  • Inputs: 1
  • Outputs: 1

Delay

Audio delay line.

  • Params: none
  • Settings: delayTime (float, seconds, max 2.0 by default)
  • Inputs: 1
  • Outputs: 1

StereoPanner

Equal-power stereo panning. Output is always 2 channels.

  • Params: pan (default 0.0, range -1.0 to 1.0; -1=left, +1=right)
  • Settings: none
  • Inputs: 1
  • Outputs: 1

Panner

Full 3D positional audio with distance and cone models.

  • Params: positionX, positionY, positionZ, orientationX, orientationY, orientationZ, velocityX, velocityY, velocityZ, distanceGain, coneGain
  • Settings: distanceModel (enum: Linear, Inverse, Exponential), refDistance (float), maxDistance (float), rolloffFactor (float), coneInnerAngle (float, degrees), coneOuterAngle (float, degrees), panningModel (enum: None, EqualPower, HRTF)
  • Inputs: 1
  • Outputs: 1

SampledAudio

In-memory sample playback with scheduling, looping, and grain support.

  • Params: playbackRate (default 1.0), detune (cents), dopplerRate
  • Settings: sourceBus (bus type -- assign via ls_setting_set_bus)
  • Inputs: 0
  • Outputs: 1

Use ls_node_schedule() for loop count and grain offset/duration control.

DynamicsCompressor

Dynamics compression with standard parameters.

  • Params: threshold (dB), knee (dB), ratio, attack (seconds), release (seconds), reduction (read-only, current gain reduction in dB)
  • Settings: none
  • Inputs: 1
  • Outputs: 1

Convolver

Convolution reverb using an impulse response.

  • Params: none
  • Settings: normalize (bool), impulseResponseClip (bus type)
  • Inputs: 1
  • Outputs: 1

Assign an impulse response via ls_setting_set_bus on the impulseResponseClip setting.

WaveShaper

Arbitrary waveshaping distortion via a transfer curve.

  • Params: none
  • Settings: (curve data set programmatically)
  • Inputs: 1
  • Outputs: 1

ChannelMerger

Merges multiple mono inputs into a multi-channel output.

  • Params: none
  • Settings: none
  • Inputs: N (configurable)
  • Outputs: 1

ChannelSplitter

Splits a multi-channel input into separate mono outputs.

  • Params: none
  • Settings: none
  • Inputs: 1
  • Outputs: N (configurable)

Analyser

FFT-based frequency analysis (does not modify signal).

  • Params: none
  • Settings: (FFT size configuration)
  • Inputs: 1
  • Outputs: 1 (pass-through)

AudioDestination

The final output node (auto-created by context). Cannot be created via registry.

  • Inputs: 1
  • Outputs: 0

Extended Nodes

Noise

White, pink, or brown noise generator.

  • Params: none
  • Settings: type (enum: White, Pink, Brown)
  • Inputs: 0
  • Outputs: 1

Must be started with ls_node_start.

ADSR

Envelope generator with attack-decay-sustain-release shape.

  • Params: gate (0.0 or 1.0 -- trigger the envelope)
  • Settings: oneShot (bool), attackTime (float, seconds), attackLevel (float), decayTime (float, seconds), sustainTime (float, seconds), sustainLevel (float), releaseTime (float, seconds)
  • Inputs: 1 (pass-through modulated by envelope)
  • Outputs: 1

If oneShot is false, the envelope holds sustain until gate returns to 0. If true, sustainTime controls the hold duration.

Function

Custom DSP via a C callback. Called per-channel on the audio thread.

  • Params: none
  • Settings: none
  • Inputs: 0
  • Outputs: 1 (mono by default; add more with ls_node_create_output)

Setup:

ls_Node fn = ls_node_create(ctx, "Function");
ls_function_node_set_callback(ctx, fn, my_callback, my_userdata);
ls_node_start(ctx, fn, 0.0);

Callback fills the buffer with sample data each render quantum.

Clip

Hard or soft clipping distortion.

  • Params: (implementation-specific)
  • Settings: (clip mode/threshold)
  • Inputs: 1
  • Outputs: 1

Diode

Diode distortion simulation.

  • Params: (distortion curve parameters)
  • Settings: none
  • Inputs: 1
  • Outputs: 1

Granulation

Granular synthesis from a source buffer.

  • Params: (grain parameters)
  • Settings: (source buffer, grain size, etc.)
  • Inputs: 0 or 1
  • Outputs: 1

PeakComp

Peak compressor with lookahead.

  • Params: (threshold, ratio, attack, release)
  • Settings: none
  • Inputs: 1
  • Outputs: 1

PolyBLEP

Band-limited oscillator using polyBLEP anti-aliasing.

  • Params: (frequency, etc.)
  • Settings: (waveform type)
  • Inputs: 0
  • Outputs: 1

PWM

Pulse-width modulation oscillator.

  • Params: (frequency, pulse width)
  • Settings: none
  • Inputs: 0
  • Outputs: 1

Recorder

Records audio input to memory. Can export to WAV.

  • Params: none
  • Settings: none
  • Inputs: 1
  • Outputs: 1 (pass-through)

Recording is controlled programmatically (start/stop recording, then write to file).

SFXR

Retro sound effect generator (based on sfxr/bfxr).

  • Params: (many -- frequency, envelope, waveform, etc.)
  • Settings: (preset type, etc.)
  • Inputs: 0
  • Outputs: 1

SpectralMonitor

Real-time spectral analysis node.

  • Params: none
  • Settings: none
  • Inputs: 1
  • Outputs: 1 (pass-through)

SuperSaw

Multiple detuned sawtooth oscillators for a "supersaw" pad sound.

  • Params: (frequency, detune amount, mix)
  • Settings: none
  • Inputs: 0
  • Outputs: 1

PowerMonitor

Monitors signal power/RMS level.

  • Params: none
  • Settings: none
  • Inputs: 1
  • Outputs: 1 (pass-through)

Quick Start: Minimal C Program

#include <LabSoundC/LabSoundC.h>
#include <unistd.h>
#include <stdio.h>

int main(void) {
    ls_ContextDesc desc = {0};
    ls_Context* ctx = ls_context_create(&desc);

    ls_Node osc  = ls_node_create(ctx, "Oscillator");
    ls_Node gain = ls_node_create(ctx, "Gain");
    ls_Node dest = ls_destination_node(ctx);

    // Configure
    ls_param_set_value(ctx, ls_node_param(ctx, osc, "frequency").pin, 440.0f);
    ls_param_set_value(ctx, ls_node_param(ctx, gain, "gain").pin, 0.25f);

    // Connect: osc -> gain -> destination
    ls_connect(ctx, ls_node_output(ctx, osc, 0), ls_node_input(ctx, gain, 0));
    ls_connect(ctx, ls_node_output(ctx, gain, 0), ls_node_input(ctx, dest, 0));

    // Play
    ls_node_start(ctx, osc, 0.0);
    usleep(2000000);  // 2 seconds
    ls_node_stop(ctx, osc, 0.0);

    // Cleanup
    ls_node_destroy(ctx, gain);
    ls_node_destroy(ctx, osc);
    ls_context_destroy(ctx);
    return 0;
}

Compile:

cc -std=c11 -o hello_audio hello_audio.c -I/path/to/include -L/path/to/build -lLabSoundC -lLabSound -lLabSoundRtAudio -lstdc++ -framework CoreAudio -framework AudioToolbox -framework AudioUnit -framework Accelerate -framework Cocoa