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_Contextowns 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.
- CMake 3.15+
- C11 / C++17 compiler
- LabSound source tree (sibling directory or set
LABSOUND_ROOT)
mkdir build && cd build
cmake .. -DLABSOUND_ROOT=/path/to/LabSound
cmake --build .| 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.
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).
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.
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_createThe 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 defaultoutput_channels-- 0 for default (2)input_channels-- 0 for no mic; >0 enables mic inputoffline-- true for non-realtime renderingallocator-- custom malloc/free (NULL for system default)
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 outputThe 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 immediatelyEvery 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");To prevent accidentally swapping source and destination in connection calls, the API uses typed wrapper structs:
ls_Output-- wraps a BUS_OUT pinls_Input-- wraps a BUS_IN pinls_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 PARAMOr 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 nameAll typed wrappers embed the underlying ls_Pin in a .pin field for back-access.
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);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.).
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.
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 |
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)ls_Result ls_last_error(ls_Context* ctx)
const char* ls_result_string(ls_Result r)int ls_registry_count(ls_Context* ctx)
const char* ls_registry_name(ls_Context* ctx, int index)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)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)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)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)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)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)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)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)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)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);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)The Python bindings wrap LabSoundC via ctypes, providing a Pythonic interface with context managers, method-style access, and automation helpers.
export LABSOUNDC_LIB=/path/to/build/libLabSoundC.dylib
export PYTHONPATH=/path/to/LabSoundC/bindings/python:$PYTHONPATHfrom 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 automaticallyfrom 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)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)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)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)The Zig bindings provide a thin, idiomatic wrapper over the LabSoundC static library with method syntax, error sets, and defer-based cleanup.
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);
}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);
}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;
};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);
}These four effects from demo.c demonstrate fundamental procedural audio techniques using only the LabSoundC API. No sample files required.
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:
-
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.
-
Sawtooth oscillator -- Produces a harmonically rich tone whose pitch sweeps with the control signal. Sawtooth is chosen for its full harmonic spectrum.
-
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.
-
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)
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:
-
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. -
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).
-
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)
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:
-
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.
-
Noise source -- White noise provides the broadband "click" excitation. The gate chops it into tiny bursts.
-
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).
-
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
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:
-
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.
-
Noise + lowpass -- White noise through a lowpass filter at 800 Hz, Q=1.5 produces the breathy "chuff" texture of steam.
-
Stereo pan sweep -- Over 8 seconds (80 steps), the
StereoPannerpan value sweeps from -1.0 (hard left) to +1.0 (hard right). -
Distance attenuation -- The master gain follows a curve based on distance from center:
0.15 + 0.55 * (1 - dist^2)wheredist = |t - 0.5| * 2. Loudest at the midpoint (train beside listener), quieter at edges. -
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)
All node types registered in the LabSound registry, accessible via ls_node_create(ctx, "TypeName").
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
Applies volume change with de-zippering.
- Params:
gain(default 1.0, range 0--10000) - Settings: none
- Inputs: 1
- Outputs: 1
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
Audio delay line.
- Params: none
- Settings:
delayTime(float, seconds, max 2.0 by default) - Inputs: 1
- Outputs: 1
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
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
In-memory sample playback with scheduling, looping, and grain support.
- Params:
playbackRate(default 1.0),detune(cents),dopplerRate - Settings:
sourceBus(bus type -- assign vials_setting_set_bus) - Inputs: 0
- Outputs: 1
Use ls_node_schedule() for loop count and grain offset/duration control.
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
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.
Arbitrary waveshaping distortion via a transfer curve.
- Params: none
- Settings: (curve data set programmatically)
- Inputs: 1
- Outputs: 1
Merges multiple mono inputs into a multi-channel output.
- Params: none
- Settings: none
- Inputs: N (configurable)
- Outputs: 1
Splits a multi-channel input into separate mono outputs.
- Params: none
- Settings: none
- Inputs: 1
- Outputs: N (configurable)
FFT-based frequency analysis (does not modify signal).
- Params: none
- Settings: (FFT size configuration)
- Inputs: 1
- Outputs: 1 (pass-through)
The final output node (auto-created by context). Cannot be created via registry.
- Inputs: 1
- Outputs: 0
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.
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.
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.
Hard or soft clipping distortion.
- Params: (implementation-specific)
- Settings: (clip mode/threshold)
- Inputs: 1
- Outputs: 1
Diode distortion simulation.
- Params: (distortion curve parameters)
- Settings: none
- Inputs: 1
- Outputs: 1
Granular synthesis from a source buffer.
- Params: (grain parameters)
- Settings: (source buffer, grain size, etc.)
- Inputs: 0 or 1
- Outputs: 1
Peak compressor with lookahead.
- Params: (threshold, ratio, attack, release)
- Settings: none
- Inputs: 1
- Outputs: 1
Band-limited oscillator using polyBLEP anti-aliasing.
- Params: (frequency, etc.)
- Settings: (waveform type)
- Inputs: 0
- Outputs: 1
Pulse-width modulation oscillator.
- Params: (frequency, pulse width)
- Settings: none
- Inputs: 0
- Outputs: 1
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).
Retro sound effect generator (based on sfxr/bfxr).
- Params: (many -- frequency, envelope, waveform, etc.)
- Settings: (preset type, etc.)
- Inputs: 0
- Outputs: 1
Real-time spectral analysis node.
- Params: none
- Settings: none
- Inputs: 1
- Outputs: 1 (pass-through)
Multiple detuned sawtooth oscillators for a "supersaw" pad sound.
- Params: (frequency, detune amount, mix)
- Settings: none
- Inputs: 0
- Outputs: 1
Monitors signal power/RMS level.
- Params: none
- Settings: none
- Inputs: 1
- Outputs: 1 (pass-through)
#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