Processing Expression
Up to this point, you’ve learned how audio flows:
- containers feed buffers
- buffers run processors
- processors shape data.
Now we expand the vocabulary of processors themselves. In MayaFlux, mathematics, logic, feedback, and generation are not side features, they are first-class sculpting tools. This tutorial explores how computational expressions become sound-shaping primitives.
In MayaFlux, polynomials sculpt rather than calculate. Logic decides rather than branches. This tutorial shows you how mathematical expressions become sonic transformations.
Polynomial Waveshaping
The Simplest Path
Run this code. Your file plays with harmonic distortion.
void compose() {
auto sound = vega.read_audio("path/to/file.wav") | Audio;
auto buffers = get_io_manager::get_audio_buffers();
// Polynomial: x² generates harmonics
auto poly = vega.Polynomial([](double x) { return x * x; });
auto processor = MayaFlux::create_processor<PolynomialProcessor>(buffers[0], poly);
}
Replace "path/to/file.wav" with an actual path.
The audio sounds richer and warmer, with subtle saturation. That’s harmonic content added by the squaring function.
Expansion 1: Why Polynomials Shape Sound
When you write x * x, you’re not “squaring numbers.” You’re defining a transfer curve:
- Input -1.0 → Output 1.0
- Input 0.5 → Output 0.25 (quieter)
- Input 1.0 → Output 1.0 (same)
This asymmetry adds harmonics. The waveform’s shape bends: its geometry changes.
Analog distortion (tubes, tape) works this way: input voltage doesn’t map linearly to output. The circuit’s response curve adds character.
Polynomials let you design that curve digitally. x * x is gentle. x * x * x adds different harmonics (odd instead of even). std::tanh(x) mimics tube saturation.
You’re sculpting frequency response through function shape.
Expansion 2: What `vega.Polynomial()` Creates
vega.Polynomial([](double x) { return x * x; }) creates a Polynomial node, a mathematical expression that processes one sample at a time.
By itself, the node doesn’t touch your audio. You wrap it in a PolynomialProcessor:
auto processor = MayaFlux::create_processor<PolynomialProcessor>(buffers[0], poly);
Why this separation?
- Node: The math itself (reusable, chainable, inspectable)
- Processor: The attachment mechanism that knows how to apply the node to a buffer
Same node, different processors → different results. You’ll see this pattern everywhere in MayaFlux.
The node is the idea. The processor is the application.
Expansion 3: PolynomialMode::DIRECT
Polynomials have three modes:
- DIRECT:
f(x)where x is the current sample - RECURSIVE:
f(y[n-1], y[n-2], ...)where output depends on previous outputs - FEEDFORWARD:
f(x[n], x[n-1], ...)where output depends on input history
In DIRECT mode (what you’re using now), each sample is transformed independently. This is memoryless waveshaping.
Later sections explore time-aware modes. RECURSIVE creates filters and feedback. FEEDFORWARD creates delay-based effects.
For now: DIRECT mode = instant transformation. No memory. No delay.
Expansion 4: What `create_processor()` Does
When you call:
auto processor = MayaFlux::create_processor<PolynomialProcessor>(buffers[0], poly);
MayaFlux does this:
- Creates a
PolynomialProcessorwrapping your polynomial node - Gets
buffers[0]’s processing chain - Adds the processor to that chain
- Returns the processor handle
The buffer now runs your polynomial on every cycle:
- 512 samples arrive from the Container
- Your polynomial processes each sample:
y = x * x - Transformed samples continue to speakers
The processor is now part of the buffer’s flow. It runs automatically every cycle until removed.
Try It
// Cubic distortion (more aggressive, odd harmonics)
auto poly = vega.Polynomial([](double x) { return x * x * x; });
// Chebyshev waveshaping (precise harmonic control)
auto poly = vega.Polynomial([](double x) { return 2*x*x - 1; });
// Soft clipping (analog-style limiting)
auto poly = vega.Polynomial([](double x) {
return x / (1.0 + std::abs(x));
});
// Extreme fold-back distortion
auto poly = vega.Polynomial([](double x) {
return std::sin(x * 5.0);
});
Listen to each. Same structure, different curves. Each curve generates different harmonic content.
Think of it not as “processing audio” but as sculpting the transfer function.
Recursive Polynomials (Filters and Feedback)
The Next Step
You have memoryless waveshaping. Now add memory.
void compose() {
auto sound = vega.read_audio("path/to/file.wav") | Audio;
auto buffers = get_io_manager::get_audio_buffers(sound);
// Recursive: output depends on previous outputs
auto recursive = vega.Polynomial(
[](std::span<double> history) {
// history[0] = previous output, history[1] = two samples ago
return 0.5 * history[0] + 0.3 * history[1];
},
PolynomialMode::RECURSIVE,
2 // remember 2 previous outputs
);
auto processor = MayaFlux::create_processor<PolynomialProcessor>(buffers[0], recursive);
}
Run this. You hear echo and resonance as the signal feeds back into itself.
Expansion 1: Why This Is a Filter
Classic IIR filter equation:
y[n] = b0*x[n] + a1*y[n-1] + a2*y[n-2]
Your recursive polynomial is that filter, just written as a lambda:
[](std::span<double> history) {
return 0.5 * history[0] + 0.3 * history[1];
}
Difference: You can write nonlinear feedback:
[](std::span<double> history) {
return history[0] * std::sin(history[1]); // nonlinear!
}
Traditional DSP libraries can’t do this. Fixed coefficients only.
Polynomials let you design arbitrary recursive functions, not just linear filters.
Expansion 2: The History Buffer
When you write:
PolynomialMode::RECURSIVE, 2
The polynomial maintains a buffer of previous outputs:
history[0] = y[n-1] (last output)
history[1] = y[n-2] (two samples ago)
Each cycle:
- Your lambda reads from
history - Computes new output
- Polynomial pushes output into
history(shifts everything down) - Loop repeats
The buffer size determines how far back you can look. Larger buffers = longer memory.
For a 100-sample buffer at 48 kHz:
100 samples ÷ 48000 Hz ≈ 2 ms of history
This is how you build delays, reverbs, resonant filters, and anything else that needs temporal memory.
Expansion 3: Stability Warning
Critical rule: Keep feedback coefficients summing to < 1.0 for guaranteed stability.
Safe:
return 0.6*history[0] + 0.3*history[1]; // sum = 0.9 < 1.0
Dangerous:
return 1.2*history[0]; // WILL EXPLODE (unbounded growth)
Why? Each cycle multiplies previous output by 1.2. Exponential growth. Your speakers won’t thank you.
MayaFlux won’t stop you. This is a creative tool, not a safety guard. Instability can be interesting (briefly). Controlled feedback explosion creates chaotic textures.
But for stable filters: keep gain < 1.0.
Expansion 4: Initial Conditions
Recursive polynomials need starting values. Default: [0.0, 0.0, ...]
You can seed them:
recursive->set_initial_conditions({0.5, -0.3, 0.1});
Why?
- Impulse responses: Inject energy without external input. The filter “pings” on its own.
- Self-oscillation: Non-zero initial conditions + feedback gain ≥ 1.0 = continuous tone.
- Warm start: Resume from previous state instead of cold-starting at zero.
Example (resonant ping):
auto resonator = vega.Polynomial(
[](std::span<double> history) {
return 0.99 * history[0] - 0.5 * history[1];
},
PolynomialMode::RECURSIVE,
2
);
resonator->set_initial_conditions({1.0, 0.0}); // kick-start the resonance
Try It
// Karplus-Strong string synthesis (plucked string)
auto string = vega.Polynomial(
[](std::span<double> history) {
return 0.996 * (history[0] + history[1]) / 2.0; // lowpass + feedback
},
PolynomialMode::RECURSIVE,
100 // ~480 Hz at 48kHz
);
string->set_initial_conditions(
std::vector<double>(100, vega.Random(-1.0, 1.0))
); // noise burst
// Nonlinear resonator (saturating feedback)
auto nonlinear = vega.Polynomial(
[](std::span<double> history) {
double fb = 0.8 * history[0];
return std::tanh(fb * 3.0); // soft saturation in loop
},
PolynomialMode::RECURSIVE,
1
);
// Comb filter (delay-based coloration)
auto comb = vega.Polynomial(
[](std::span<double> history) {
return history[0] + 0.5 * history[50]; // 50-sample delay
},
PolynomialMode::RECURSIVE,
50
);
Logic as Decision Maker
The Simplest Path
Run this code. You’ll hear rhythmic pulses.
void compose() {
auto buffer = vega.AudioBuffer() | Audio[0];
// Logic node: threshold detection
auto logic = vega.Logic(LogicOperator::THRESHOLD, 0.0);
auto processor = MayaFlux::create_processor<LogicProcessor>(
buffer,
logic
);
processor->set_modulation_type(LogicProcessor::ModulationType::REPLACE);
// Feed a sine wave into the logic node
auto sine = vega.Sine(2.0);
logic->set_input_node(sine);
}
What you hear: a 2 Hz pulse train, beeping every half second.
The sine wave crosses zero twice per cycle. Logic detects the crossings. Output becomes binary: 1.0 (high) or 0.0 (low).
Expansion 1: What Logic Does
LogicProcessor makes binary decisions about audio.
Every sample asks: “Is this value TRUE or FALSE?” (based on threshold)
Output: 0.0 or 1.0.
Uses:
- Gate: Silence audio below threshold (noise reduction)
- Trigger: Fire events when signal crosses boundary (drums, envelopes)
- Rhythm: Convert continuous modulation into discrete beats
Example: Feed a slow LFO (0.5 Hz sine) into logic → square wave clock.
Digital doesn’t care what the input “means”, only whether it passes the test.
Expansion 2: Logic node needs an input
Logic nodes need an input signal to evaluate. This is also true for other nodes like Polynomial.
So far, you did not have to manually set inputs because you used SoundContainerBuffer which automatically feeds audio into processors.
So, instead of creating an AudioBuffer, you can load a file:
auto sound = vega.read_audio("path/to/file.wav") | Audio;
auto buffers = get_io_manager::get_audio_buffers(sound);
auto logic = vega.Logic(LogicOperator::THRESHOLD, 0.0);
auto processor = MayaFlux::create_processor<LogicProcessor>(
buffer[0],
logic
);
processor->set_modulation_type(LogicProcessor::ModulationType::REPLACE);
The audio from the file is automatically fed into the logic node. Considering how all previous examples relied on file contents, and the natutre of rhythmic pulses not exploiting the intricacies or richness of audio files, we are using a sine wave as inputs of the logic node in the main example.
Expansion 3: LogicOperator Types
LogicOperator defines the test:
- THRESHOLD:
x > threshold→ 1.0, else 0.0 - HYSTERESIS: Two thresholds (open/close) to avoid flutter
- EDGE: Trigger on transitions (0→1 or 1→0)
- AND/OR/XOR/NOT: Boolean algebra on current vs. previous sample
- CUSTOM: Your function
Right now you’re using THRESHOLD, the simplest test.
Example (hysteresis gate for noisy signals):
auto gate = vega.Logic(LogicOperator::HYSTERESIS);
gate->set_hysteresis_thresholds(0.1, 0.3); // open at 0.3, close at 0.1
Signal must exceed 0.3 to open, then drops below 0.1 to close. Prevents rapid on/off flickering.
Expansion 4: ModulationType - Readymade Transformations
ModulationType provides readymade ways to apply binary logic to audio:
Basic Operations:
- REPLACE: Audio becomes 0.0 or 1.0 (bit reduction)
- MULTIPLY: Audio × logic (standard gate - preserves timbre)
- ADD: Audio + logic (adds impulse on logic high)
Creative Operations:
- INVERT_ON_TRUE: Phase flip when logic high (ring mod effect)
- HOLD_ON_FALSE: Freeze audio when logic low (granular stutter)
- ZERO_ON_FALSE: Hard silence when logic low (noise gate)
- CROSSFADE: Smooth fade based on logic (dynamic blending)
- THRESHOLD_REMAP: Binary amplitude switch (tremolo from logic)
- SAMPLE_AND_HOLD: Freeze on logic changes (glitch/stutter)
- CUSTOM: Your function
Example (granular freeze effect):
processor->set_modulation_type(
LogicProcessor::ModulationType::HOLD_ON_FALSE
);
// Audio freezes whenever logic goes low - creates stuttering repeats
Example (amplitude tremolo):
processor->set_modulation_type(
LogicProcessor::ModulationType::THRESHOLD_REMAP
);
processor->set_threshold_remap_values(1.0, 0.2);
// Creates rhythmic volume changes based on logic pattern
Logic becomes a compositional control for transforming audio in musical ways.
Try It
// Hard gate (silence below threshold)
auto gate = vega.Logic(LogicOperator::THRESHOLD, 0.2);
auto proc = MayaFlux::create_processor<LogicProcessor>(buffer, gate);
proc->set_modulation_type(LogicProcessor::ModulationType::ZERO_ON_FALSE);
// Granular stutter (freeze on quiet passages)
auto freeze = vega.Logic(LogicOperator::THRESHOLD, 0.3);
auto proc = MayaFlux::create_processor<LogicProcessor>(buffer, freeze);
proc->set_modulation_type(LogicProcessor::ModulationType::HOLD_ON_FALSE);
// Bit crusher (reduce to 1-bit audio)
auto crusher = vega.Logic(LogicOperator::THRESHOLD, 0.0);
auto proc = MayaFlux::create_processor<LogicProcessor>(buffer, crusher);
proc->set_modulation_type(LogicProcessor::ModulationType::REPLACE);
// Rhythmic tremolo from LFO
auto lfo = vega.Sine(4.0); // 4 Hz
auto trem_logic = vega.Logic(LogicOperator::THRESHOLD, 0.0);
trem_logic->set_input_node(lfo);
auto proc = MayaFlux::create_processor<LogicProcessor>(buffer, trem_logic);
proc->set_modulation_type(
LogicProcessor::ModulationType::THRESHOLD_REMAP
);
proc->set_threshold_remap_values(1.0, 0.3); // Pumping rhythm
Combining Polynomial + Logic
The Pattern
Load a file. Detect transients with logic. Apply polynomial only when transient detected.
void compose() {
auto sound = vega.read_audio("drums.wav") | Audio;
auto buffers = get_io_manager::get_audio_buffers(sound);
auto bitcrush = vega.Logic(LogicOperator::THRESHOLD, 0.0);
auto crush_proc =
MayaFlux::create_processor<LogicProcessor>(
buffers[0], bitcrush
);
crush_proc->set_modulation_type(
LogicProcessor::ModulationType::REPLACE
);
// Step 2: Freeze audio in chunks - granular stutter
auto clock = vega.Sine(4.0); // 4 Hz freeze rate
auto freeze_logic = vega.Logic(LogicOperator::THRESHOLD, 0.0);
freeze_logic->set_input_node(clock);
auto freeze_proc =
MayaFlux::create_processor<LogicProcessor>(
buffers[0], freeze_logic
);
freeze_proc->set_modulation_type(
LogicProcessor::ModulationType::HOLD_ON_FALSE
);
// Step 3: Extreme waveshaping distortion
auto destroyer = std::make_shared<Polynomial>([](double x) {
return std::copysign(1.0, x) *
std::pow(std::abs(x), 0.3); // Extreme compression
});
auto poly_proc =
MayaFlux::create_processor<PolynomialProcessor>(
buffers[0], destroyer
);
chain->add_processor(crush_proc, buffers[0]);
chain->add_processor(freeze_proc, buffers[0]);
chain->add_processor(poly_proc, buffers[0]);
buffers[0]->set_processing_chain(chain);
}
Expansion 1: Processing Chains as Transformation Pipelines
You just built a transformation pipeline:
bitcrush → freeze → destroy
Each processor transforms the output of the previous one. This is compositional signal processing: you build complex effects by chaining simple operations.
The power comes from order dependency:
gate → distort // Clean transients, heavy saturation
distort → gate // Distorted everything, then choppy
Swap the order = completely different sound.
Extend it:
detect transients → sample-and-hold → bitcrush → wavefold → compress
Traditional plugins give you “distortion with 3 knobs.” You compose the distortion algorithm itself.
Every processor is a building block. Chain them to create effects that don’t exist as plugins:
- Bitcrush → Freeze → Invert = Glitch stutterer
- Remap → Fold → Gate = Rhythmic harmonizer
- Threshold → Hold → Distort = Transient emphasizer
Logic + Polynomial + Chains = programmable audio transformation system.
Expansion 2: Chain Order Matters
Swap the order of logic and polynomial → different result:
Logic → Polynomial // Detect, then distort
Polynomial → Logic // Distort, then detect
Processors are non-commutative. Audio math doesn’t follow algebra rules.
Order determines signal flow. You’re building a graph, not an equation.
Try It
// Adaptive dynamics (compress quiet, expand loud)
auto logic = vega.Logic(LogicOperator::THRESHOLD, 0.3);
auto poly_compress =
vega.Polynomial([](double x) { return x * 2.0; });
auto poly_expand =
vega.Polynomial([](double x) { return x * 0.5; });
// Route based on logic state (requires custom modulation)
Processing Chains and Buffer Architecture
Tutorial: Explicit Chain Building
The Simplest Path
You’ve been adding processors one at a time. Now control their order explicitly.
void compose() {
auto sound = vega.read_audio("path/to/file.wav") | Audio;
auto buffer = get_io_manager::get_audio_buffers(sound)[0];
// Create an empty chain
auto chain = MayaFlux::create_processing_chain();
// Build the chain: Distortion → Gate → Compression
auto distortion =
vega.Polynomial([](double x) { return std::tanh(x * 2.0); });
auto gate =
vega.Logic(LogicOperator::THRESHOLD, 0.1);
auto compression =
vega.Polynomial([](double x) {
return x / (1.0 + std::abs(x));
});
chain->add_processor(
std::make_shared<PolynomialProcessor>(distortion),
buffer
);
chain->add_processor(
std::make_shared<LogicProcessor>(gate),
buffer
);
chain->add_processor(
std::make_shared<PolynomialProcessor>(compression),
buffer
);
// Attach the chain to the buffer
buffer->set_processing_chain(chain);
}
Run this. You hear: clean audio → saturated → gated (silence below threshold) → compressed (controlled peaks).
Swap the order:
chain->add_processor(gate_processor); // Gate first
chain->add_processor(distortion_processor); // Then distort
chain->add_processor(compression_processor);
Different sound. Order matters.
Expansion 1: What `create_processor()` Was Doing
Previously, when you wrote:
auto processor =
MayaFlux::create_processor<PolynomialProcessor>(buffer, poly);
MayaFlux did this behind the scenes:
- Created the processor
- Got the buffer’s existing processing chain
- Automatically added the processor to that chain
- Returned the processor
You didn’t see this because it was implicit. The processor was silently appended to whatever chain existed.
Now you’re building chains explicitly:
auto chain = MayaFlux::create_processing_chain(); // Empty chain
chain->add_processor(proc1); // Manual control
chain->add_processor(proc2);
buffer->set_processing_chain(chain); // Replace buffer's chain
When to use explicit chains:
- You need precise order control
- You’re building reusable processor “presets”
- You want to swap entire chains dynamically (e.g., switch between clean/distorted modes)
- You’re debugging processor interactions
When implicit is fine:
- Simple cases (1-2 processors)
- Order doesn’t matter (parallel-like effects)
- Rapid prototyping
Expansion 2: Chain Execution Order
Chains execute like a for-loop over processors:
for (auto& processor : chain->get_processors()) {
processor->process(buffer);
}
Data flows sequentially:
Container → Buffer (512 samples)
↓
Processor₁: Distortion (modifies samples in-place)
↓
Processor₂: Gate (zeroes out quiet samples)
↓
Processor₃: Compression (reduces peaks)
↓
Speakers
Each processor sees the output of the previous processor.
This is not parallel processing. No branches. No simultaneous paths. Pure sequential transformation.
(Parallel routing requires BufferPipeline, which is covered in a later tutorial.)
Expansion 3: Default Processors vs. Chain Processors
Every buffer has two processing stages:
Stage 1: Default Processor (runs first, always)
- Defined by buffer type
- Handles data acquisition or generation
- Examples:
SoundContainerBuffer: reads from file/streamNodeBuffer: evaluates a nodeFeedbackBuffer: mixes current + previous bufferAudioBuffer: none (generic accumulator)
Stage 2: Processing Chain (runs second)
- Your custom processors
- Handles data transformation
- Examples: filters, waveshaping, logic, etc.
Execution flow:
1. Buffer's default processor runs (fills buffer with data)
2. Processing chain runs (transforms that data)
3. Result goes to speakers
When you add processors via create_processor(), they go into Stage 2 (the chain).
The default processor is fixed per buffer type. You can replace it, but usually you don’t need to. The chain is where creativity happens.
Try It
// Stack multiple distortions (cascading saturation)
auto chain = MayaFlux::create_processing_chain();
auto light =
vega.Polynomial([](double x) { return std::tanh(x * 1.5); });
auto heavy =
vega.Polynomial([](double x) { return std::tanh(x * 5.0); });
auto fold =
vega.Polynomial([](double x) { return std::sin(x * 3.0); });
chain->add_processor(
std::make_shared<PolynomialProcessor>(light),
buffer
);
chain->add_processor(
std::make_shared<PolynomialProcessor>(heavy),
buffer
);
chain->add_processor(
std::make_shared<PolynomialProcessor>(fold),
buffer
);
// Insert gating between stages
auto gate = vega.Logic(LogicOperator::THRESHOLD, 0.2);
chain->add_processor(
std::make_shared<PolynomialProcessor>(light),
buffer
);
chain->add_processor(
std::make_shared<LogicProcessor>(gate),
buffer
); // Gate the distortion
chain->add_processor(
std::make_shared<PolynomialProcessor>(heavy),
buffer
); // Distort the gated signal
buffer->set_processing_chain(chain);
Various Buffer Types
Buffer Ecosystem Overview
MayaFlux provides specialized buffer types for different generation and processing patterns:
- NodeBuffer: Generate audio from mathematical nodes
- FeedbackBuffer: Recursive temporal processing with memory
- SoundStreamWriter: Capture processed audio to containers
Each buffer type has default processors and specific use cases. Click to explore each in detail.
Generating from Nodes (NodeBuffer)
The Next Pattern
So far: buffers read from files, nodes affect buffer processing.
Now: buffers generate from nodes.
void compose() {
// Create a sine node
auto sine = vega.Sine(440.0);
// Create a NodeBuffer that captures the sine's output
auto node_buffer = vega.NodeBuffer(0, 512, sine) | Audio[0];
// Add processing to the generated audio
auto distortion = vega.Polynomial([](double x) { return x * x * x; });
MayaFlux::create_processor<PolynomialProcessor>(node_buffer, distortion);
}
Run this. You hear a 440 Hz sine wave with cubic distortion.
No file loaded. The buffer generates audio by evaluating the node 512 times per cycle.
Expansion 1: What NodeBuffer Does
NodeBuffer connects the node system (sample-by-sample evaluation) to the buffer system (block-based processing).
Default processor: NodeSourceProcessor
Each cycle:
- Node is evaluated 512 times:
node->process_sample() - Results fill the buffer
- Processing chain runs (your custom processors)
- Buffer outputs to speakers
Why this matters:
Nodes are mathematical expressions (infinite generators). Buffers are temporal accumulators (finite chunks).
NodeBuffer bridges the two: continuous expression → discrete blocks.
Without NodeBuffer, you’d manually call node->process_sample() 512 times and copy results into a buffer. NodeBuffer automates this.
Expansion 2: The `clear_before_process` Parameter
NodeBuffer has a flag: clear_before_process
auto node_buffer = vega.NodeBuffer(0, 512, sine, true); // Clear first (default)
true (default): Buffer is zeroed, then filled with node output
- Result: pure node output
false: Node output is added to existing buffer content
- Result: node output + previous buffer state
Why use false?
- Layering: Multiple nodes contributing to the same buffer
- Feedback: Previous cycle’s output influences current cycle
- Additive synthesis: Mix multiple generators
Example (layering):
auto sine = vega.Sine(440.0);
auto buffer = vega.NodeBuffer(0, 512, sine, true) | Audio[0]; // First node clears
auto noise = vega.Random();
auto noise_buffer = vega.NodeBuffer(0, 512, noise, false) | Audio[0]; // Adds to sine
Result: sine + noise.
Expansion 3: NodeSourceProcessor Mix Parameter
NodeSourceProcessor has a mix parameter (default: 0.5):
auto processor = std::make_shared<NodeSourceProcessor>(node, 0.7f);
Mix = 0.0: Preserve existing buffer content (node output ignored)
Mix = 0.5: Equal blend of existing + node output
Mix = 1.0: Replace with node output (existing content overwritten)
This is a cross-fade between what’s in the buffer and what the node generates.
Use case: Smoothly transition between sources, or create feedback loops where node output gradually replaces decaying buffer content.
Most of the time, you’ll use the default (1.0 via clear_before_process=true). But for creative effects, mix is powerful.
Try It
// Additive synthesis (multiple generators in one buffer)
auto fund = vega.Sine(220.0);
auto harm2 = vega.Sine(440.0);
auto harm3 = vega.Sine(660.0);
auto buffer = vega.NodeBuffer(0, 512, fund, true) | Audio[0]; // First clears
vega.NodeBuffer(0, 512, harm2, false) | Audio[0]; // Adds harmonic 2
vega.NodeBuffer(0, 512, harm3, false) | Audio[0]; // Adds harmonic 3
// Waveshaping a generated tone
auto sine = vega.Sine(110.0);
auto buffer2 = vega.NodeBuffer(0, 512, sine) | Audio[1];
auto waveshape = vega.Polynomial([](double x) { return std::tanh(x * 10.0); });
MayaFlux::create_processor<PolynomialProcessor>(buffer2, waveshape);
FeedbackBuffer (Recursive Audio)
The Pattern
Buffers that remember their previous state.
void compose() {
// FeedbackBuffer: 70% feedback, 512 samples delay
auto feedback_buf = vega.FeedbackBuffer(0, 512, 0.7f, 512) | Audio[0];
// Feed an impulse into the buffer to kick-start resonance
auto impulse = vega.Impulse(2.0); // 2 Hz pulse train
vega.NodeBuffer(0, 512, impulse, false) | Audio[0]; // Adds to feedback buffer
// WARN: Remember to turn OFF after a few seconds as feedback can build up!
}
Run this. You hear: repeating echoes, each 70% of the previous amplitude.
The buffer feeds back into itself: output becomes input next cycle.
Expansion 1: What FeedbackBuffer Does
Default processor: FeedbackProcessor
Each cycle:
- Current buffer content:
buffer[n] - Previous buffer content:
previous_buffer[n-1] - Output:
buffer[n] + (feedback_amount * previous_buffer[n-1]) - Store output as next cycle’s “previous”
This is a simple delay line with feedback.
Parameters:
feedback_amount: 0.0–1.0 (how much previous state contributes)feed_samples: Delay length in samples
Example: FeedbackBuffer(0, 512, 0.7, 512) creates:
- 512-sample delay (~10.6 ms at 48 kHz)
- 70% feedback (echoes decay to 0.7 → 0.49 → 0.343 → …)
Stability: Keep feedback_amount < 1.0 or output will grow unbounded.
Expansion 2: FeedbackBuffer Limitations
FeedbackBuffer is deliberately simple. It implements one specific recursive algorithm: linear feedback delay.
Limitations:
- Fixed feedback coefficient: Can’t modulate feedback amount per sample (it’s buffer-wide)
- No filtering in loop: Can’t insert lowpass/highpass in the feedback path
- No cross-channel feedback: Single-channel only
- No time-varying delay: Delay length is fixed at creation
Why these limitations?
FeedbackBuffer is a building block, not a complete reverb/delay effect.
For complex feedback systems:
- Use
PolynomialProcessorinRECURSIVEmode (per-sample nonlinear feedback) - Use
BufferPipelineto route buffers back to themselves with processing - Build custom feedback networks with multiple buffers
FeedbackBuffer is for simple echoes and resonances: quick and efficient.
Expansion 3: When to Use FeedbackBuffer
Use FeedbackBuffer when:
- You need a simple delay line with fixed feedback
- Building Karplus-Strong string synthesis
- Creating rhythmic echoes
- Implementing comb filters
Use PolynomialProcessor(RECURSIVE) when:
- You need nonlinear feedback (saturation, distortion in loop)
- Feedback amount varies per sample
- Building filters with arbitrary feedback functions
Use BufferPipeline when:
- You need complex routing (buffer A → process → buffer B → back to A)
- Multi-buffer feedback networks
- Cross-channel feedback
Example: Filtered feedback (requires multiple approaches):
// FeedbackBuffer can't do this alone:
// current + lowpass(feedback * previous)
// Solution: Use PolynomialProcessor RECURSIVE mode with filtering
auto filtered_feedback = vega.Polynomial(
[](std::span<double> amp; history) {
double fb = 0.7 * history[0];
return fb * 0.5 + history[1] * 0.5; // Simple lowpass
},
PolynomialMode::RECURSIVE,
2
);
Try It
// Karplus-Strong string (plucked string synthesis)
auto feedback_buf = vega.FeedbackBuffer(0, 512, 0.996f, 100) | Audio[0]; // ~480 Hz
// Excite with noise burst
auto noise = vega.Random();
vega.NodeBuffer(0, 512, noise, false) | Audio[0];
// Ping-pong delay (two-buffer technique, covered later)
// auto left = vega.FeedbackBuffer(0, 512, 0.6f, 2400) | Audio[0];
// auto right = vega.FeedbackBuffer(1, 512, 0.6f, 2400) | Audio[1];
// Route left → right, right → left (needs BufferPipeline)
// Resonant comb filter
auto feedback_buf2 = vega.FeedbackBuffer(0, 512, 0.95f, 50) | Audio[1];
auto input = vega.Sine(220.0);
vega.NodeBuffer(0, 512, input, false) | Audio[1];
SoundStreamWriter (Capturing Audio)
The Pattern
Processors that write buffer data somewhere (instead of transforming it).
void compose() {
auto sound = vega.read_audio("path/to/file.wav") | Audio;
auto buffer = get_io_manager::get_audio_buffers(sound)[0];
// Create a DynamicSoundStream (accumulator for captured audio)
auto capture_stream = std::make_shared<DynamicSoundStream>(48000, 2);
// Create a processor that writes buffer data to the stream
auto writer = std::make_shared<SoundStreamWriter>(capture_stream);
// Add to buffer's processing chain
auto chain = buffer->get_processing_chain();
chain->add_processor(writer);
// File plays AND is captured to stream simultaneously
}
Run this. The file plays and is written to capture_stream every cycle.
After playback, capture_stream contains a copy of the entire file (processed through any other processors in the chain before the writer).
Expansion 1: What SoundStreamWriter Does
SoundStreamWriter is the inverse of SoundContainerBuffer:
- SoundContainerBuffer: reads from container → fills buffer (source)
- SoundStreamWriter: reads from buffer → writes to container (sink)
Each cycle:
- Extract 512 samples from the buffer
- Write them to the
DynamicSoundStreamat the current write position - Increment write position by 512
The stream grows dynamically as data arrives. No pre-allocation needed (though you can for performance).
Use cases:
- Record processed audio to memory
- Capture intermediate processing stages for analysis
- Build delay lines / loopers
- Create feedback paths (buffer → stream → buffer)
Expansion 2: Channel-Aware Writing
SoundStreamWriter respects buffer channel IDs:
auto left_buffer = buffers[0]; // channel 0
auto right_buffer = buffers[1]; // channel 1
auto stream = std::make_shared<DynamicSoundStream>(48000, 2); // stereo
auto writer_L = std::make_shared<SoundStreamWriter>(stream);
auto writer_R = std::make_shared<SoundStreamWriter>(stream);
// Add to respective buffers
left_buffer->get_processing_chain()->add_processor(writer_L);
right_buffer->get_processing_chain()->add_processor(writer_R);
Result: Stereo file captured to stereo stream, with channels preserved.
Critical: Buffer’s channel_id determines which stream channel receives data. Mismatch = warning + skip.
Expansion 3: Position Management
SoundStreamWriter tracks where it’s writing:
writer->set_write_position(0); // Write from start
writer->set_write_position(48000); // Write from 1-second mark
writer->reset_position(); // Reset to 0
// Time-based positioning
writer->set_write_position_time(2.5); // Write from 2.5 seconds
uint64_t pos = writer->get_write_position(); // Get current frame position
double time = writer->get_write_position_time(); // Get current time position
Why control position?
- Overdubbing: Write new audio over existing content
- Looping: Reset position to create cyclic recording
- Multi-pass recording: Capture different takes at different positions
Default behavior: append at end. Position auto-increments.
Expansion 4: Circular Mode
DynamicSoundStream can operate in circular mode:
auto stream = std::make_shared<DynamicSoundStream>(48000, 2);
stream->enable_circular_buffer(48000); // 1 second capacity
auto writer = std::make_shared<SoundStreamWriter>(stream);
Behavior:
When write position reaches capacity, it wraps to 0. Old data is overwritten.
Use cases:
- Delay lines: Fixed-length delays for effects
- Loopers: Record N seconds, then loop
- Rolling analysis: Keep only the most recent N seconds
Without circular mode, the stream grows unbounded: useful for full recording, but problematic for long-running systems.
Try It
// Record 5 seconds of audio
auto sound = vega.read_audio("path/to/file.wav") | Audio;
auto buffer = get_io_manager::get_audio_buffers(sound)[0];
auto stream = std::make_shared<DynamicSoundStream>(48000, 1);
stream->ensure_capacity(48000 * 5); // Pre-allocate 5 seconds
auto writer = std::make_shared<SoundStreamWriter>(stream);
buffer->get_processing_chain()->add_processor(writer, buffer);
// After playback, stream contains the audio
// You can analyze it, write to disk, or feed it back
// Circular delay (1 second)
auto stream2 = std::make_shared<DynamicSoundStream>(48000, 1);
stream2->enable_circular_buffer(48000); // Loop after 1 second
auto writer2 = std::make_shared<SoundStreamWriter>(stream);
buffer->get_processing_chain()->add_processor(writer, buffer);
// Stream now acts as a 1-second tape loop
Closing: The Buffer Ecosystem
You now understand:
Buffer Types:
AudioBuffer: Generic accumulatorSoundContainerBuffer: Reads from files/streams (default:SoundStreamReader)NodeBuffer: Generates from nodes (default:NodeSourceProcessor)FeedbackBuffer: Recursive delay (default:FeedbackProcessor)
Processor Types:
PolynomialProcessor: Waveshaping, filters, recursive mathLogicProcessor: Decisions, gates, triggersSoundStreamWriter: Capture to containers
Processing Flow:
Default Processor (acquire/generate data)
↓
Processing Chain (transform data)
↓
Output (speakers/containers/other buffers)
Next: Buffer routing, cloning, and supply mechanics: how to send processed buffers to multiple channels and domains.
Audio Input, Routing, and Multi-Channel Distribution
Routing Ecosystem Overview
MayaFlux provides sophisticated routing capabilities for capturing, distributing, and cloning audio across multiple channels:
- Audio Input: Capture live microphone input with real-time processing
- Buffer Supply: Route one buffer to multiple output channels efficiently
- Buffer Cloning: Create independent copies for parallel processing
These systems enable complex signal routing, multi-channel processing, and efficient resource utilization. Click to explore each in detail.
Capturing Audio Input
The Simplest Path
So far: buffers read from files or generate from nodes. Now: capture from your microphone.
void settings() {
auto& stream = MayaFlux::Config::get_global_stream_info();
stream.input.enabled = true; // Enable microphone input
stream.input.channels = 1; // Mono input
}
void compose() {
// Create a buffer that listens to microphone channel 0
auto mic_buffer = MayaFlux::create_input_listener_buffer(0, true);
// Add processing to the live input
auto distortion = vega.Polynomial([](double x) { return std::tanh(x * 3.0); });
MayaFlux::create_processor<PolynomialProcessor>(mic_buffer, distortion);
}
Run this. Speak into your microphone. You hear yourself with distortion applied in real-time.
Expansion 1: What `create_input_listener_buffer()` Does
MayaFlux has a dedicated input subsystem parallel to the output system.
Architecture:
Hardware (Microphone)
↓
Audio Driver (RtAudio)
↓
BufferManager::process_input()
↓
InputAudioBuffer (per input channel)
↓
InputAccessProcessor (dispatches to listeners)
↓
Your listener buffers
When you call create_input_listener_buffer(channel, add_to_output):
- Creates a new
AudioBuffer - Registers it with
InputAudioBuffer[channel]as a listener - If
add_to_output=true: Also registers it with output channel (so it plays back)
Each audio cycle:
- Driver captures microphone data
InputAudioBufferreceives itInputAccessProcessorcopies data to all registered listeners- Your buffer gets fresh input every cycle
Key insight: InputAudioBuffer is a hub. Multiple buffers can listen to the same input channel simultaneously.
Expansion 2: Manual Input Registration
create_input_listener_buffer() is convenience. You can do it manually:
// Create your own buffer
auto buffer = vega.AudioBuffer() | Audio[0];
// Register it as input listener
MayaFlux::read_from_audio_input(buffer, 0); // Listen to input channel 0
// Later, stop listening:
MayaFlux::detach_from_audio_input(buffer, 0);
When to use manual registration:
- You already have a buffer (don’t want to create a new one)
- You want to dynamically start/stop listening (e.g., record button)
- You need finer control over buffer lifecycle
Example: Record button
auto recorder = vega.AudioBuffer() | Audio[0];
// Start recording
MayaFlux::read_from_audio_input(recorder, 0);
// Stop recording (after some time)
MayaFlux::detach_from_audio_input(recorder, 0);
The buffer continues to exist and process, but stops receiving new input.
Expansion 3: Input Without Playback
Often you want to capture input without playing it back:
// Create listener but don't add to output
auto mic_capture = MayaFlux::create_input_listener_buffer(0, false); // false = silent
// Capture to a stream for analysis
auto stream = std::make_shared<DynamicSoundStream>(48000, 1);
auto writer = std::make_shared<SoundStreamWriter>(stream);
mic_capture->get_processing_chain()->add_processor(writer);
Result: Microphone data is captured to stream, but you don’t hear it.
Use cases:
- Recording without monitoring
- Voice analysis (pitch detection, speech recognition)
- Trigger detection (clap to start/stop)
- Level metering / VU display
Try It
// Real-time vocal effects chain
auto mic = MayaFlux::create_input_listener_buffer(0, true);
auto pitch_shift = vega.Polynomial([](double x) { return x * 1.5; }); // Naive pitch shift
auto reverb = vega.FeedbackBuffer(0, 512, 0.3f, 2400); // Simple reverb
auto gate = vega.Logic(LogicOperator::THRESHOLD, 0.05); // Noise gate
MayaFlux::create_processor<PolynomialProcessor>(mic, pitch_shift);
MayaFlux::create_processor<LogicProcessor>(mic, gate);
// Record to disk simultaneously
auto stream = std::make_shared<DynamicSoundStream>(48000, 1);
auto writer = std::make_shared<SoundStreamWriter>(stream);
mic->get_processing_chain()->add_processor(writer, mic);
// After session: save 'stream' to file
// Voice-triggered synthesis
auto mic_silent = MayaFlux::create_input_listener_buffer(0, false);
auto trigger = vega.Logic(LogicOperator::EDGE);
trigger->set_edge_detection(EdgeType::RISING, 0.3);
auto trigger_proc = MayaFlux::create_processor<LogicProcessor>(mic_silent, trigger);
// When trigger fires, start a synthesizer...
Buffer Supply (Routing to Multiple Channels)
The Pattern
One buffer, multiple output channels.
void compose() {
auto sine = vega.Sine(440.0);
auto buffer = vega.NodeBuffer(0, 512, sine) | Audio[0]; // Registered to channel 0
// Supply this buffer to channels 1 and 2 as well
MayaFlux::supply_buffer_to_channels(buffer, {1, 2}, 0.5); // 50% mix level
}
Run this. You hear the same 440 Hz sine on all three channels (left, center, right in surround setup).
The buffer processes once, but outputs to three channels.
Expansion 1: What "Supply" Means
Registration (vega.AudioBuffer() | Audio[0]):
- Adds buffer as a child of
RootAudioBuffer[0] - Buffer processes during channel 0’s cycle
- Output accumulates into channel 0
Supply (supply_buffer_to_channels):
- Adds buffer’s output to other channels
- Buffer still processes in its original channel
- Output is copied to supplied channels
Analogy:
- Registration = “This buffer lives in channel 0”
- Supply = “After processing in channel 0, send copies to channels 1 and 2”
Architecture:
Buffer processes in channel 0
↓
Output goes to RootAudioBuffer[0]
↓
MixProcessor copies output to RootAudioBuffer[1]
↓
MixProcessor copies output to RootAudioBuffer[2]
Key: The buffer only processes once. Supply is a routing operation, not a duplication of processing.
Expansion 2: Mix Levels
The mix parameter controls how much of the buffer’s output is sent:
MayaFlux::supply_buffer_to_channel(buffer, 1, 1.0); // 100% (unity gain)
MayaFlux::supply_buffer_to_channel(buffer, 2, 0.5); // 50% (half amplitude)
MayaFlux::supply_buffer_to_channel(buffer, 3, 0.1); // 10% (quiet)
Use case: Stereo width control
auto mono_source = vega.Sine(440.0);
auto buffer = vega.NodeBuffer(0, 512, mono_source) | Audio[0];
// Send to left (full) and right (half) for asymmetric stereo
MayaFlux::supply_buffer_to_channel(buffer, 0, 1.0); // Left
MayaFlux::supply_buffer_to_channel(buffer, 1, 0.5); // Right (quieter)
Use case: Send effects
auto dry = vega.NodeBuffer(0, 512, sine) | Audio[0]; // Dry signal, channel 0
// Send 30% to reverb channel
MayaFlux::supply_buffer_to_channel(dry, 2, 0.3); // Channel 2 = reverb bus
Mix is additive. If channel already has content, supply adds to it.
Expansion 3: Removing Supply
You can remove supply relationships:
auto buffer = vega.NodeBuffer(0, 512, sine) | Audio[0];
MayaFlux::supply_buffer_to_channel(buffer, 1);
// Later: stop sending to channel 1
MayaFlux::remove_supplied_buffer_from_channel(buffer, 1);
// Or remove from multiple channels at once
MayaFlux::remove_supplied_buffer_from_channels(buffer, {1, 2, 3});
Use case: Mute individual sends
- Buffer still processes
- Output still goes to its registered channel
- Supplied channels no longer receive it
Use case: Dynamic routing matrices
if (user_pressed_button_A) {
MayaFlux::supply_buffer_to_channel(buffer, 1);
} else {
MayaFlux::remove_supplied_buffer_from_channel(buffer, 1);
}
Try It
// Quad panning (4-channel surround)
auto source = vega.Sine(220.0);
auto buffer = vega.NodeBuffer(0, 512, source) | Audio[0];
// Distribute to 4 corners with different levels (panning)
MayaFlux::supply_buffer_to_channel(buffer, 0, 0.7); // Front-left
MayaFlux::supply_buffer_to_channel(buffer, 1, 0.3); // Front-right
MayaFlux::supply_buffer_to_channel(buffer, 2, 0.2); // Rear-left
MayaFlux::supply_buffer_to_channel(buffer, 3, 0.1); // Rear-right
// Send effects architecture
auto guitar = vega.NodeBuffer(0, 512, source) | Audio[0]; // Channel 0 = dry
auto reverb_bus = vega.FeedbackBuffer(1, 512, 0.7f, 4800) | Audio[1]; // Channel 1 = reverb
auto delay_bus = vega.FeedbackBuffer(2, 512, 0.6f, 9600) | Audio[2]; // Channel 2 = delay
MayaFlux::supply_buffer_to_channel(guitar, 1, 0.4); // 40% to reverb
MayaFlux::supply_buffer_to_channel(guitar, 2, 0.2); // 20% to delay
// Multi-band processing (split frequency ranges across channels)
// Process each band independently, then sum
Buffer Cloning
The Pattern
One buffer specification, multiple independent instances.
void compose() {
auto sine = vega.Sine(440.0);
auto buffer = vega.NodeBuffer(0, 512, sine); // Don't register yet
// Clone to channels 0, 1, 2
MayaFlux::clone_buffer_to_channels(buffer, {0, 1, 2});
}
Run this. You hear three independent sine waves on three channels.
Each clone processes independently. The clones don’t share data.
Expansion 1: Clone vs. Supply
Supply:
- One buffer processes once
- Output is copied to multiple channels
- Processing cost: 1× processing
- Memory: One buffer
- Use when: Same signal needs to go to multiple places
Clone:
- Multiple buffers process independently
- Each has its own data, state, processing chain
- Processing cost: N× processing (N = number of clones)
- Memory: N buffers
- Use when: Similar buffers need independent processing
Example: Supply use case
// One reverb output to stereo speakers
auto reverb = vega.FeedbackBuffer(0, 512, 0.8f, 4800) | Audio[0];
MayaFlux::supply_buffer_to_channel(reverb, 1); // Copy to right channel
// Cost: 1× reverb processing
Example: Clone use case
// Independent noise generators per channel
auto noise_template = vega.NodeBuffer(0, 512, vega.Random(-1.0, 1.0));
MayaFlux::clone_buffer_to_channels(noise_template, {0, 1, 2, 3});
// Cost: 4× noise processing (each with different random seed/state)
// Result: Decorrelated noise on each channel
Expansion 2: Cloning Preserves Structure
When you clone a buffer, each clone receives:
- Same buffer type (NodeBuffer, FeedbackBuffer, etc.)
- Same default processor configuration
- Same processing chain (all added processors)
- Independent data (not shared; each clone has its own samples)
- Independent state (feedback buffers have separate history)
Example: Clone a processed buffer
auto sine = vega.Sine(440.0);
auto buffer = vega.NodeBuffer(0, 512, sine);
// Add processing before cloning
auto distortion = vega.Polynomial([](double x) { return std::tanh(x * 2.0); });
MayaFlux::create_processor<PolynomialProcessor>(buffer, distortion);
// Now clone
MayaFlux::clone_buffer_to_channels(buffer, {0, 1, 2});
// Result: Each channel gets sine → distortion (independently processed)
Each clone has its own instance of the distortion processor. They don’t share state.
Expansion 3: Post-Clone Modification
After cloning, you can modify individual clones:
auto buffer = vega.NodeBuffer(0, 512, vega.Sine(440.0));
// Store cloned buffers for later reference
auto cloned_buffers = MayaFlux::clone_buffer_to_channels(buffer, { 0, 1, 2 });
// Add different processing to each
std::vector<double> coeffs_a_2 = { 0.2, 0.3, 0.2 };
std::vector<double> coeffs_b_2 = { 1.0, -0.7 };
auto filter1 = vega.IIR(coeffs_a_1, coeffs_b_1);
auto filter2 = vega.IIR(coeffs_a_2, coeffs_b_2);
MayaFlux::create_processor<FilterProcessor>(cloned_buffers[0], filter1);
MayaFlux::create_processor<FilterProcessor>(cloned_buffers[1], filter2);
// Now channel 0 has one filter, channel 1 has a different filter
Use case: Stereo decorrelation (same source, slightly different processing per channel)
Try It
// Stereo chorus (cloned with phase offset)
auto lfo = vega.Sine(0.5); // Slow LFO
auto buffer = vega.NodeBuffer(0, 512, lfo);
MayaFlux::clone_buffer_to_channels(buffer, {0, 1});
// Modify one clone to have phase offset (requires accessing clone directly)
// Multi-channel granular synthesis
auto grain_template = vega.NodeBuffer(0, 512, vega.Random(-0.1, 0.1));
MayaFlux::clone_buffer_to_channels(grain_template, {0, 1, 2, 3, 4, 5, 6, 7});
// Each channel generates independent grains
// Independent feedback loops per channel
auto feedback_template = vega.FeedbackBuffer(0, 512, 0.8f, 1000);
MayaFlux::clone_buffer_to_channels(feedback_template, {0, 1, 2, 3});
// Excite each with different input → 4 independent resonances
Closing: The Routing Ecosystem
You now understand:
Input Capture:
InputAudioBuffer: Hardware input hubInputAccessProcessor: Dispatches to listenerscreate_input_listener_buffer(): Quick setupread_from_audio_input()/detach_from_audio_input(): Manual control
Buffer Supply:
supply_buffer_to_channel(): Route one buffer to multiple outputs- Mix levels: Control send amounts
- Efficiency: Process once, output many times
remove_supplied_buffer_from_channel(): Dynamic routing changes
Buffer Cloning:
clone_buffer_to_channels(): Create independent copies- Preserves structure: Type, processors, chains
- Independent state: Each clone processes separately
- Post-clone modification: Differentiate behavior after creation
Mental Model:
Input (Microphone)
↓
InputAudioBuffer → Listener buffers (capture)
↓
Processing chains (transform)
↓
Supply (route to multiple channels)
OR
Clone (create independent instances)
↓
RootAudioBuffer (mix per channel)
↓
Output (Speakers)
Next: BufferPipeline (declarative multi-stage workflows with temporal control)