MayaFlux 0.1.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
ProcessingExpression

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 don't calculate—they sculpt. Logic doesn't branch—it decides. This tutorial shows you how mathematical expressions become sonic transformations.


  • Tutorial: Polynomial Waveshaping{#toc-tutorial-polynomial-waveshaping}
    • The Simplest Path{#toc-the-simplest-path}
    • Expansion 1: Why Polynomials Shape Sound{#toc-expansion-1-why-polynomials-shape-sound}
    • Expansion 2: What `vega.Polynomial()` Creates{#toc-expansion-2-what-vega.polynomial-creates}
    • Expansion 3: PolynomialMode::DIRECT{#toc-expansion-3-polynomialmodedirect}
    • Expansion 4: What `create_processor()` Does{#toc-expansion-4-what-create_processor-does}
    • Try It{#toc-try-it}
    • Tutorial: Recursive Polynomials (Filters and Feedback){#toc-tutorial-recursive-polynomials-filters-and-feedback}
      • The Next Step{#toc-the-next-step}
    • Expansion 1: Why This Is a Filter{#toc-expansion-1-why-this-is-a-filter}
    • Expansion 2: The History Buffer{#toc-expansion-2-the-history-buffer}
    • Expansion 3: Stability Warning{#toc-expansion-3-stability-warning}
    • Expansion 4: Initial Conditions{#toc-expansion-4-initial-conditions}
    • Try It{#toc-try-it-1}
  • Tutorial: Logic as Decision Maker{#toc-tutorial-logic-as-decision-maker}
    • The Simplest Path{#toc-the-simplest-path-1}
    • Expansion 1: What Logic Does{#toc-expansion-1-what-logic-does}
    • Expansion 2: Logic node needs an input{#toc-expansion-2-logic-node-needs-an-input}
    • Expansion 3: LogicOperator Types{#toc-expansion-3-logicoperator-types}
    • Expansion 4: ModulationType - Readymade Transformations{#toc-expansion-4-modulationtype—readymade-transformations}
    • Try It{#toc-try-it-2}
  • Tutorial: Combining Polynomial + Logic{#toc-tutorial-combining-polynomial-logic}
    • The Pattern{#toc-the-pattern}
    • Expansion 1: Decision Trees in Audio{#toc-expansion-1-decision-trees-in-audio}
    • Expansion 2: Chain Order Matters{#toc-expansion-2-chain-order-matters}
    • Try It{#toc-try-it-3}
  • Tutorial: Processing Chains and Buffer Architecture{#toc-tutorial-processing-chains-and-buffer-architecture}
    • Tutorial: Explicit Chain Building{#toc-tutorial-explicit-chain-building}
      • The Simplest Path{#toc-the-simplest-path-2}
    • Expansion 1: What `create_processor()` Was Doing{#toc-expansion-1-what-create_processor-was-doing}
    • Expansion 2: Chain Execution Order{#toc-expansion-2-chain-execution-order}
    • Expansion 3: Default Processors vs. Chain Processors{#toc-expansion-3-default-processors-vs.-chain-processors}
    • Try It{#toc-try-it-4}
  • Tutorial: Various Buffer Types{#toc-tutorial-various-buffer-types}
    • Generating from Nodes (NodeBuffer){#toc-generating-from-nodes-nodebuffer}
      • The Next Pattern{#toc-the-next-pattern}
      • Expansion 1: What NodeBuffer Does{#toc-expansion-1-what-nodebuffer-does}
      • Expansion 2: The `clear_before_process` Parameter{#toc-expansion-2-the-clear_before_process-parameter}
      • Expansion 3: NodeSourceProcessor Mix Parameter{#toc-expansion-3-nodesourceprocessor-mix-parameter}
      • Try It{#toc-try-it-5}
    • FeedbackBuffer (Recursive Audio){#toc-feedbackbuffer-recursive-audio}
      • The Pattern{#toc-the-pattern-1}
      • Expansion 1: What FeedbackBuffer Does{#toc-expansion-1-what-feedbackbuffer-does}
      • Expansion 2: FeedbackBuffer Limitations{#toc-expansion-2-feedbackbuffer-limitations}
      • Expansion 3: When to Use FeedbackBuffer{#toc-expansion-3-when-to-use-feedbackbuffer}
      • Try It{#toc-try-it-6}
    • StreamWriteProcessor (Capturing Audio){#toc-streamwriteprocessor-capturing-audio}
      • The Pattern{#toc-the-pattern-2}
      • Expansion 1: What StreamWriteProcessor Does{#toc-expansion-1-what-streamwriteprocessor-does}
      • Expansion 2: Channel-Aware Writing{#toc-expansion-2-channel-aware-writing}
      • Expansion 3: Position Management{#toc-expansion-3-position-management}
      • Expansion 4: Circular Mode{#toc-expansion-4-circular-mode}
      • Try It{#toc-try-it-7}
    • Closing: The Buffer Ecosystem{#toc-closing-the-buffer-ecosystem}
  • Tutorial: Audio Input, Routing, and Multi-Channel Distribution{#toc-tutorial-audio-input-routing-and-multi-channel-distribution}
    • Tutorial: Capturing Audio Input{#toc-tutorial-capturing-audio-input}
      • The Simplest Path{#toc-the-simplest-path-3}
    • Expansion 1: What `create_input_listener_buffer()` Does{#toc-expansion-1-what-create_input_listener_buffer-does}
    • Expansion 2: Manual Input Registration{#toc-expansion-2-manual-input-registration}
    • Expansion 3: Input Without Playback{#toc-expansion-3-input-without-playback}
    • Try It{#toc-try-it-8}
    • Tutorial: Buffer Supply (Routing to Multiple Channels){#toc-tutorial-buffer-supply-routing-to-multiple-channels}
      • The Pattern{#toc-the-pattern-3}
    • Expansion 1: What "Supply" Means{#toc-expansion-1-what-supply-means}
    • Expansion 2: Mix Levels{#toc-expansion-2-mix-levels}
    • Expansion 3: Removing Supply{#toc-expansion-3-removing-supply}
    • Try It{#toc-try-it-9}
    • Tutorial: Buffer Cloning{#toc-tutorial-buffer-cloning}
      • The Pattern{#toc-the-pattern-4}
    • Expansion 1: Clone vs. Supply{#toc-expansion-1-clone-vs.-supply}
    • Expansion 2: Cloning Preserves Structure{#toc-expansion-2-cloning-preserves-structure}
    • Expansion 3: Post-Clone Modification{#toc-expansion-3-post-clone-modification}
    • Try It{#toc-try-it-10}
    • Closing: The Routing Ecosystem{#toc-closing-the-routing-ecosystem}

Tutorial: 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;
// Polynomial: x² generates harmonics
auto poly = vega.Polynomial([](double x) { return x * x; });
auto processor = MayaFlux::create_processor<PolynomialProcessor>(buffers[0], poly);
}
auto Polynomial(Args &&... args) -> CreationHandle< MayaFlux::Nodes::Generator::Polynomial >
Definition Creator.hpp:190
auto read_audio(const std::string &filepath) -> CreationHandle< Kakshya::SoundFileContainer >
Definition Creator.hpp:213
std::vector< std::shared_ptr< Buffers::ContainerBuffer > > get_last_created_container_buffers()
Retrieves the last created container buffers from the Creator.
Definition Creator.cpp:99

Replace "path/to/file.wav" with an actual path.

The audio sounds richer, warmer—subtle saturation. That's harmonic content added by the squaring function.


Expansion 1: Why Polynomials Shape Sound

Click to expand: Transfer Functions as Geometry

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 <tt>vega.Polynomial()</tt> Creates

Click to expand: Nodes vs. Processors

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—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

Click to expand: Three Processing Modes

Polynomials have three modes:

  • DIRECT: f(x) where x is the current sample (what you just used)
  • 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

Right now you're using DIRECT mode—each sample 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 <tt>create_processor()</tt> Does

Click to expand: Attaching to Buffers

When you call:

auto processor = MayaFlux::create_processor<PolynomialProcessor>(buffers[0], poly);

MayaFlux does this:

  1. Creates a PolynomialProcessor wrapping your polynomial node
  2. Gets buffers[0]'s processing chain (every buffer has one)
  3. Adds the processor to that chain
  4. 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);
});
Creator vega
Global Creator instance for creating nodes, buffers, and containers.
Definition Creator.cpp:14

Listen to each. Same structure, different curves. Each curve generates different harmonic content.

You're not "processing audio"—you're sculpting the transfer function.


Tutorial: 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;
// Recursive: output depends on previous outputs
auto recursive = vega.Polynomial(
[](const std::deque<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/resonance—the signal feeds back into itself.


Expansion 1: Why This Is a Filter

Click to expand: IIR Filters Are Recursive Polynomials

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:

[](const std::deque<double>& history) {
return 0.5 * history[0] + 0.3 * history[1];
}

Difference: You can write nonlinear feedback:

[](const std::deque<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

Click to expand: How RECURSIVE Mode Works

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:

  1. Your lambda reads from history
  2. Computes new output
  3. Polynomial pushes output into history (shifts everything down)
  4. 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—anything that needs temporal memory.


Expansion 3: Stability Warning

Click to expand: Recursive Systems Can Explode

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

Click to expand: Seeding the History Buffer

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?

  1. Impulse responses: Inject energy without external input. The filter "pings" on its own.
  2. Self-oscillation: Non-zero initial conditions + feedback gain ≥ 1.0 = continuous tone.
  3. Warm start: Resume from previous state instead of cold-starting at zero.

Example (resonant ping):

auto resonator = vega.Polynomial(
[](const std::deque<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(
[](const std::deque<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(
[](const std::deque<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(
[](const std::deque<double>& history) {
return history[0] + 0.5 * history[50]; // 50-sample delay
},
PolynomialMode::RECURSIVE,
50
);
auto Random(Args &&... args) -> CreationHandle< MayaFlux::Nodes::Generator::Stochastics::Random >
Definition Creator.hpp:190

Tutorial: Logic as Decision Maker

The Simplest Path

Run this code. You'll hear rhythmic pulses.

void compose() {
auto buffer = vega.AudioBuffer()[0] | Audio;
// 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);
}
auto AudioBuffer(Args &&... args) -> CreationHandle< MayaFlux::Buffers::AudioBuffer >
Definition Creator.hpp:210
auto Logic(Args &&... args) -> CreationHandle< MayaFlux::Nodes::Generator::Logic >
Definition Creator.hpp:190
auto Sine(Args &&... args) -> CreationHandle< MayaFlux::Nodes::Generator::Sine >
Definition Creator.hpp:190

What you hear: 2 Hz pulse train—beeps 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

Click to expand: Continuous → Discrete Conversion

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"—just whether it passes the test.


Expansion 2: Logic node needs an input

Click to expand: Continuous → input signal

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 ContainerBuffer 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 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

Click to expand: Binary Operations

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

Click to expand: Creative Logic Applications

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); // High = full volume, Low = quiet
// 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

Tutorial: 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;
// Step 1: Detect transients (drum hits)
// Step 1: Brutal bitcrushing - reduce to 1-bit
auto bitcrush = vega.Logic(LogicOperator::THRESHOLD, 0.0);
auto crush_proc = std::make_shared<LogicProcessor>(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 = std::make_shared<LogicProcessor>(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 = std::make_shared<PolynomialProcessor>(destroyer);
// Build chain: bitcrush → freeze → destroy
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);
}
std::shared_ptr< Buffers::BufferProcessingChain > create_processing_chain()
Creates a new processing chain for the default engine.
Definition Graph.cpp:101

Or if you want direct control without manual processor creation, you can use the fluent API

auto sound = vega.read_audio("drums.wav") | Audio;
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);

Expansion 1: Processing Chains as Transformation Pipelines

Click to expand: Sequential Audio Surgery

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

Click to expand: Non-Commutative Processing

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)

Tutorial: 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;
// Create an empty 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 <tt>create_processor()</tt> Was Doing

Click to expand: Implicit vs. Explicit Chain Management

Previously, when you wrote:

auto processor = MayaFlux::create_processor<PolynomialProcessor>(buffer, poly);

MayaFlux did this behind the scenes:

  1. Created the processor
  2. Got the buffer's existing processing chain
  3. Automatically added the processor to that chain
  4. 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

Click to expand: Sequential Data Flow

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—covered in a later tutorial.)


Expansion 3: Default Processors vs. Chain Processors

Click to expand: The Two-Stage Processing Model

Every buffer has two processing stages:

Stage 1: Default Processor (runs first, always)

  • Defined by buffer type
  • Handles data acquisition or generation
  • Examples:
    • ContainerBuffer: reads from file/stream
    • NodeBuffer: evaluates a node
    • FeedbackBuffer: mixes current + previous buffer
    • AudioBuffer: 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 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);

Tutorial: Various Buffer Types

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)[0] | Audio;
// Add processing to the generated audio
auto distortion = vega.Polynomial([](double x) { return x * x * x; });
MayaFlux::create_processor<PolynomialProcessor>(node_buffer, distortion);
}
auto NodeBuffer(Args &&... args) -> CreationHandle< MayaFlux::Buffers::NodeBuffer >
Definition Creator.hpp:210

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

Click to expand: Nodes → Buffers Bridge

NodeBuffer connects the node system (sample-by-sample evaluation) to the buffer system (block-based processing).

Default processor: NodeSourceProcessor

Each cycle:

  1. Node is evaluated 512 times: node->process_sample()
  2. Results fill the buffer
  3. Processing chain runs (your custom processors)
  4. 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 <tt>clear_before_process</tt> Parameter

Click to expand: Accumulation vs. Replacement

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)[0] | Audio; // First node clears
auto noise = vega.Random();
auto noise_buffer = vega.NodeBuffer(0, 512, noise, false)[0] | Audio; // Adds to sine

Result: sine + noise.


Expansion 3: NodeSourceProcessor Mix Parameter

Click to expand: Interpolation Between Existing and Incoming Data

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)[0] | Audio; // First clears
vega.NodeBuffer(0, 512, harm2, false)[0] | Audio; // Adds harmonic 2
vega.NodeBuffer(0, 512, harm3, false)[0] | Audio; // Adds harmonic 3
// Waveshaping a generated tone
auto sine = vega.Sine(110.0);
auto buffer2 = vega.NodeBuffer(0, 512, sine)[1] | Audio;
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)[0] | Audio;
// 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)[0] | Audio; // Adds to feedback buffer
// WARN: Remember to turn OFF aftera a few seconds as feedback can build up!
}
auto Impulse(Args &&... args) -> CreationHandle< MayaFlux::Nodes::Generator::Impulse >
Definition Creator.hpp:190
auto FeedbackBuffer(Args &&... args) -> CreationHandle< MayaFlux::Buffers::FeedbackBuffer >
Definition Creator.hpp:210

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

Click to expand: Recursive Temporal Processing

Default processor: FeedbackProcessor

Each cycle:

  1. Current buffer content: buffer[n]
  2. Previous buffer content: previous_buffer[n-1]
  3. Output: buffer[n] + (feedback_amount * previous_buffer[n-1])
  4. 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

Click to expand: What FeedbackBuffer Cannot Do

FeedbackBuffer is simple—intentionally. It implements one specific recursive algorithm: linear feedback delay.

Limitations:

  1. Fixed feedback coefficient: Can't modulate feedback amount per sample (it's buffer-wide)
  2. No filtering in loop: Can't insert lowpass/highpass in the feedback path
  3. No cross-channel feedback: Single-channel only
  4. 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 PolynomialProcessor in RECURSIVE mode (per-sample nonlinear feedback)
  • Use BufferPipeline to 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

Click to expand: Use Cases and Alternatives

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(
[](const std::deque<double>& 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)[0] | Audio; // ~480 Hz
// Excite with noise burst
auto noise = vega.Random();
vega.NodeBuffer(0, 512, noise, false)[0] | Audio;
// Ping-pong delay (requires two buffers—teaser for later)
// auto left = vega.FeedbackBuffer(0, 512, 0.6f, 2400)[0] | Audio;
// auto right = vega.FeedbackBuffer(1, 512, 0.6f, 2400)[1] | Audio;
// Route left → right, right → left (needs BufferPipeline)
// Resonant comb filter
auto feedback_buf2 = vega.FeedbackBuffer(0, 512, 0.95f, 50)[1] | Audio;
auto input = vega.Sine(220.0);
vega.NodeBuffer(0, 512, input, false)[1] | Audio;

StreamWriteProcessor (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;
// 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<StreamWriteProcessor>(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 StreamWriteProcessor Does

Click to expand: Buffers → Containers Bridge

StreamWriteProcessor is the inverse of ContainerBuffer:

  • ContainerBuffer: reads from container → fills buffer (source)
  • StreamWriteProcessor: reads from buffer → writes to container (sink)

Each cycle:

  1. Extract 512 samples from the buffer
  2. Write them to the DynamicSoundStream at the current write position
  3. 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

Click to expand: Multi-Channel Capture

StreamWriteProcessor 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<StreamWriteProcessor>(stream);
auto writer_R = std::make_shared<StreamWriteProcessor>(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—channels preserved.

Critical: Buffer's channel_id determines which stream channel receives data. Mismatch = warning + skip.


Expansion 3: Position Management

Click to expand: Write Position Control

StreamWriteProcessor 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

Click to expand: Fixed-Size Circular Buffers

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<StreamWriteProcessor>(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, problematic for long-running systems.


Try It

// Record 5 seconds of audio
auto sound = vega.read_audio("path/to/file.wav") | Audio;
auto stream = std::make_shared<DynamicSoundStream>(48000, 1);
stream->ensure_capacity(48000 * 5); // Pre-allocate 5 seconds
auto writer = std::make_shared<StreamWriteProcessor>(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<StreamWriteProcessor>(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 accumulator
  • ContainerBuffer: Reads from files/streams (default: ContainerToBufferAdapter)
  • NodeBuffer: Generates from nodes (default: NodeSourceProcessor)
  • FeedbackBuffer: Recursive delay (default: FeedbackProcessor)

Processor Types:

  • PolynomialProcessor: Waveshaping, filters, recursive math
  • LogicProcessor: Decisions, gates, triggers
  • StreamWriteProcessor: 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/domains.


Tutorial: Audio Input, Routing, and Multi-Channel Distribution

Tutorial: Capturing Audio Input

The Simplest Path

So far: buffers read from files or generate from nodes. Now: capture from your microphone.

void settings() {
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);
}
Core::GlobalStreamInfo & get_global_stream_info()
Gets the stream configuration from the default engine.
Definition Config.cpp:26
std::shared_ptr< Buffers::AudioBuffer > create_input_listener_buffer(uint32_t channel, bool add_to_output)
Creates a new AudioBuffer for input listening.
Definition Graph.cpp:180

Run this. Speak into your microphone. You hear yourself with distortion applied in real-time.


Expansion 1: What <tt>create_input_listener_buffer()</tt> Does

Click to expand: Input System Architecture

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):

  1. Creates a new AudioBuffer
  2. Registers it with InputAudioBuffer[channel] as a listener
  3. If add_to_output=true: Also registers it with output channel (so it plays back)

Each audio cycle:

  • Driver captures microphone data
  • InputAudioBuffer receives it
  • InputAccessProcessor copies 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

Click to expand: Fine-Grained Control

create_input_listener_buffer() is convenience. You can do it manually:

// Create your own buffer
auto buffer = vega.AudioBuffer()[0] | Audio;
// Register it as input listener
MayaFlux::read_from_audio_input(buffer, 0); // Listen to input channel 0
// Later, stop listening:
void detach_from_audio_input(const std::shared_ptr< Buffers::AudioBuffer > &buffer, uint32_t channel)
Stops reading audio data from the default input source.
Definition Graph.cpp:175
void read_from_audio_input(const std::shared_ptr< Buffers::AudioBuffer > &buffer, uint32_t channel)
Reads audio data from the default input source into a buffer.
Definition Graph.cpp:170

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()[0] | Audio;
// Start recording
// Stop recording (after some time)

The buffer continues to exist and process, but stops receiving new input.


Expansion 3: Input Without Playback

Click to expand: Silent Capture

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<StreamWriteProcessor>(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 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<StreamWriteProcessor>(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...

Tutorial: 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)[0] | Audio; // 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
}
void supply_buffer_to_channels(const std::shared_ptr< Buffers::AudioBuffer > &buffer, const std::vector< uint32_t > &channels, double mix)
Supplies a buffer to multiple channels with mixing.
Definition Graph.cpp:212

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

Click to expand: The Difference Between Registration and Supply

Registration (vega.AudioBuffer()[0] | Audio):

  • 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

Click to expand: Controlling Supply Amplitude

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)
void supply_buffer_to_channel(const std::shared_ptr< Buffers::AudioBuffer > &buffer, uint32_t channel, double mix)
Supplies a buffer to a single channel with mixing.
Definition Graph.cpp:203

Use case: Stereo width control

auto mono_source = vega.Sine(440.0);
auto buffer = vega.NodeBuffer(0, 512, mono_source)[0] | Audio;
// 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)[0] | Audio; // 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

Click to expand: Dynamic Routing Changes

You can remove supply relationships:

auto buffer = vega.NodeBuffer(0, 512, sine)[0] | Audio;
// Later: stop sending to channel 1
// Or remove from multiple channels at once
void remove_supplied_buffer_from_channels(const std::shared_ptr< Buffers::AudioBuffer > &buffer, const std::vector< uint32_t > &channels)
Removes a supplied buffer from multiple channels.
Definition Graph.cpp:231
void remove_supplied_buffer_from_channel(const std::shared_ptr< Buffers::AudioBuffer > &buffer, const uint32_t channel)
Removes a supplied buffer from multiple channels.
Definition Graph.cpp:221

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) {
} else {
}


Try It

// Quad panning (4-channel surround)
auto source = vega.Sine(220.0);
auto buffer = vega.NodeBuffer(0, 512, source)[0] | Audio;
// 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)[0] | Audio; // Channel 0 = dry
auto reverb_bus = vega.FeedbackBuffer(1, 512, 0.7f, 4800)[1] | Audio; // Channel 1 = reverb
auto delay_bus = vega.FeedbackBuffer(2, 512, 0.6f, 9600)[2] | Audio; // 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

Tutorial: 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
}
std::vector< std::shared_ptr< Buffers::AudioBuffer > > clone_buffer_to_channels(const std::shared_ptr< Buffers::AudioBuffer > &buffer, const std::vector< uint32_t > &channels)
Clones a buffer to multiple channels.
Definition Graph.cpp:192

Run this. You hear three independent sine waves on three channels.

Each clone processes **independently**—they don't share data.


Expansion 1: Clone vs. Supply

Click to expand: When to Use Each

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)[0] | Audio;
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

Click to expand: What Gets Cloned

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
// 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

Click to expand: Differentiating Clones After Creation

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
auto IIR(Args &&... args) -> CreationHandle< MayaFlux::Nodes::Filters::IIR >
Definition Creator.hpp:190

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);
// 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 hub
  • InputAccessProcessor: Dispatches to listeners
  • create_input_listener_buffer(): Quick setup
  • read_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)