Mono & Stereo Signals
Akkado has two kinds of audio signals: Mono (one channel) and Stereo (two channels, L and R). Channel count isn’t a runtime property — it’s a type the compiler tracks through every expression, so mismatches surface at compile time. Every audio effect is also stereo-native: it processes both channels in one pass, and a mono input automatically widens to stereo. You never duplicate a chain by hand.
Defaults
Generators (osc, noise, pulse) are mono by default; effects are stereo-native and preserve their input’s channel count. To start a chain in stereo, widen at the boundary:
// Mono — single channel
saw(220)
// Stereo — duplicate mono across both channels
saw(220) |> stereo(@)
// Stereo — distinct L and R
stereo(saw(218), saw(222)) out() accepts either:
saw(220) |> out(@) // Mono → duplicated to L and R
stereo(saw(218), saw(222))
|> out(@) // Stereo → split into L and R
out(saw(218), saw(222)) // Two mono signals → L, R explicitly Conversions
There are exactly two canonical conversions between the representations:
| Call | Direction | Effect |
|---|---|---|
stereo(x) | Mono → Stereo | Duplicate x to both L and R |
stereo(l, r) | two Mono → Stereo | Pair l as L, r as R |
mono(s) | Stereo → Mono | Sum-to-mono with 0.5 gain: (L + R) * 0.5 |
left(s) | Stereo → Mono | Extract the left channel only |
right(s) | Stereo → Mono | Extract the right channel only |
Calling a conversion on a signal that’s already the target channel count is a warning, not an error — it almost always means something upstream didn’t do what you thought, but the expression still compiles and evaluates to the input unchanged. Some typical hits:
stereo(already_stereo_signal)→W182mono(already_mono_signal)→W181left(mono_signal)/right(mono_signal)→W183/W184out(stereo_signal, other)as two-argout()→W185(auto-escalates)
A genuine type mismatch — an audio-rate signal in a non-signal slot, for example — is still a hard error (E186).
Panning a stereo signal
pan() has two signatures dispatched by the channel type of its first argument:
// Mono → Stereo: equal-power mono pan
mono_sig |> pan(@, 0.3)
// Stereo → Stereo: equal-power stereo balance
stereo_sig |> pan(@, 0.3) The stereo overload is DAW-style balance — L_out = L * cos θ, R_out = R * sin θ with θ = (p + 1) · π/4. At p = 0 both channels drop by ~3 dB (equal-power centre); at p = ±1 one channel is silenced. See the Stereo builtins reference for the math.
Stereo-native effects
Every audio effect is stereo-native: it processes both channels in a single dispatch with one per-channel state struct. A mono input automatically widens — the opcode reads it once and uses it for both the L and R lanes — so you never have to duplicate a chain or insert stereo() to get a stereo result.
bus = saw(220) // Mono — widens automatically
bus
|> lp(@, 500, 0.7) // Stereo: per-channel filter state
|> delay(@, 0.25, 0.5) // Stereo: per-channel delay line
|> out(@) // Stereo out For channel-independent effects (filters, distortion, EQ, plain delays) this is exactly equivalent to writing:
sig = saw(220)
left_out = sig
|> lp(@, 500, 0.7)
|> delay(@, 0.25, 0.5)
right_out = sig
|> lp(@, 500, 0.7)
|> delay(@, 0.25, 0.5)
out(left_out, right_out) Identical state handling, identical audio. Stateless effects (saturate, softclip, fold, distort) work the same way. Spatializing effects (reverbs, chorus, phaser, flanger) go further: a mono input widens into a genuinely decorrelated stereo output (cross-coupled reverb tanks, offset L/R LFO phases). A stereo input into a spatializing effect runs through a single cross-coupled instance — dattorro, freeverb, fdn each get one set of tanks with L↔R cross-bleed, not two independent mono reverbs in parallel.
Tuning stereo width on modulation FX
chorus, flanger, and phaser expose an extended parameter lfo_phase — the offset between the L and R LFOs, in turns (0.0–1.0). Default is 0.25 (= 90°), the classic stereo-modulation setting.
// Default: 90° offset, classic stereo chorus
saw(220)
|> chorus(@, 0.5, 0.4)
|> out(@)
// 0 = mono-equivalent (L = R); 0.5 = anti-phase
// (max width, may collapse on mono-summing)
saw(220)
|> chorus(@, 0.5, 0.4, lfo_phase: 0.5)
|> out(@)
saw(110)
|> phaser(@, 0.3, 0.8, lfo_phase: 0)
|> out(@) phaser also exposes feedback and stages the same way. Named-argument syntax (name: value) skips intervening defaults — pass only what you want to change.
What stereo-native processing doesn’t change
- Scalar parameters (cutoff, resonance, time, feedback) are shared between L and R. Both channels see the same value. If you want independent per-channel modulation, split the stream explicitly.
- Pattern events (
pat,seq,timeline) are always mono — a stereo synth driven by a mono pattern is the normal case. - Generators stay mono.
osc,noise,pulsereturn Mono; the widening happens at the boundary into the first effect. sample()is the exception — always Stereo. A mono file broadcasts to L=R; a stereo file preserves its L/R channels; files with 3+ channels keep the first two and drop the rest. Either way the output type is Stereo, so downstream effects skip the auto-widening step.- Cross-channel effects like
width,pingpong,ms_encode/ms_decodehave their own explicit stereo signatures.
Mixed-channel arithmetic
+, -, *, / on a mono and a stereo operand broadcast the mono side across both channels:
dry = saw(220) // Mono
// Stereo — freeverb auto-widens
wet = dry |> freeverb(@, 0.9, 0.5)
// Stereo out (mono dry broadcast onto wet)
dry * 0.3 + wet * 0.7 |> out(@) The dry * 0.3 stays mono, wet * 0.7 stays stereo, and mono + stereo promotes to stereo by dual-reading the mono buffer. No extra instructions.
Mono on mono stays mono. Stereo on stereo stays stereo (L op L, R op R). There’s no implicit mono-to-stereo promotion anywhere else — the compiler will complain if it can’t make sense of a mismatch.
Patterns and Signals
Patterns (n"…", v"…", c"…", s"…") carry a primary value buffer that doubles as a Signal. When you pass a pattern to a slot that expects a Signal — sine(n"c4 e4 g4"), lp(sig, v"<200 800>", 0.7) — the compiler implicitly extracts that buffer.
The Pattern → Signal coerce rule:
| Pattern shape | Coerces to Signal? | Where the buffer points |
|---|---|---|
Monophonic note (n"…") | Yes | freq field — Hz post-mtof |
Numeric (v"…") | Yes | freq field — raw scalar |
Sample (s"…") | Yes | Audio output (post-SAMPLE_PLAY) |
Polyphonic chord (c"…") | No — E160 | Use poly() to expand voices |
Polyphonic note (e.g. n"[c4,e4]") | No — E160 | Use poly() |
Operator type rules
Arithmetic between patterns and other types follows these rules:
| LHS | RHS | Result | Notes |
|---|---|---|---|
| Pattern | Pattern | Pattern | Pointwise op on freq buffers, longest wins |
| Pattern | Signal | Signal | Pattern coerces; sample-rate result |
| Pattern | Number | Pattern | n"c4 e4" + 12 is still a Pattern |
| Signal | Signal | Signal | Standard |
| Pattern | Stereo | E165 | Wrap with stereo(scalar(...)) explicitly |
scalar() — the explicit cast
scalar(p) unwraps a monophonic pattern’s freq buffer. It’s idempotent on Signals (scalar(scalar(p)) is safe) and errors E161 on sample / polyphonic patterns. See the Pattern Literals reference.
See also
- Pattern Literals — typed prefixes (
v"…",n"…",s"…",c"…") andscalar(). - Pattern Modulation tutorial — flagship walkthrough of
bend(notes, v"…")and custom-property accessors. - Stereo builtins reference — signatures and behaviour for
stereo,mono,left,right,pan,width,ms_encode,ms_decode,pingpong. - Cedar architecture — STEREO_OUTPUT / STEREO_INPUT flags — the VM-level mechanism behind stereo-native opcodes.