Sound language compiling to WASM
Designed to be useful for writing sound formulas / audio processing code for various audio targets, such as AudioWorkletProcessor, audio engines, individual audio nodes etc.
Initially inspired by zzfx, bytebeat, hxos, web-audio-engine and others, but soon it became clear that JS limitations are no-go for sound processing and it needs something more foundational with better low-level control, which WASM perfectly provides.
Gain processor, providing k-rate amplification of mono, stereo or generic input.
range = 0..1000;
gain([left], volume in range) = [left * volume];
gain([left, right], volume in range) = [left * volume, right * volume];
gain([..channels], volume in range) = [..channels * volume];
Features:
- function overload − function clause is matched automatically by call signature.
- channeled input/output −
[left]
for mono,[left, right]
for stereo,[..channels]
for any number of input channels. - a-rate/k-rate param type −
[arg]
indicates a-rate (accurate) param, directarg
param is k-rate (controlling), per-block. - range − is language-level primitive with
from..to
,from..<to
,from>..to
signature, useful in arguments validation, array constructor etc. - validation −
a in range
asserts and clamps argument to provided range, to avoid blowing up state. - destructuring − collects channels or group as
[a,..bc] = [a,b,c]
.
Biquad filter processor for single channel input.
import sin, cos, pi from "math";
pi2 = pi*2;
sampleRate = 44100;
lp([x0], freq = 100 in 1..10000, Q = 1.0 in 0.001..3.0) = (
...x1, x2, y1, y2 = 0; // internal state
w = pi2 * freq / sampleRate;
sin_w, cos_w = sin(w), cos(w);
a = sin_w / (2.0 * Q);
b0, b1, b2 = (1.0 - cos_w) / 2.0, 1.0 - cos_w, b0;
a0, a1, a2 = 1.0 + a, -2.0 * cos_w, 1.0 - a;
b0, b1, b2, a1, a2 *= 1.0 / a0;
y0 = b0*x0 + b1*x1 + b2*x2 - a1*y1 - a2*y2;
x1, x2 = x0, x1;
y1, y2 = y0, y1;
[y0].
)
Features:
- import − by default, all top-level functions and variables are exported. Unused functions are tree-shaken from compiled code. Built-in libs are:
math
,std
. Additional libs:latr
,musi
and others. - scope − block scope is defined by nesting
()
(unlike{}
in JS) − variables defined in block act within its scope. - state − internal function state is persisted between fn calls via ellipsis operator
...state=init
. State is identified by function callsite for current module instance. That is like language-level react hooks. - grouping − comma operator allows bulk operations on many variables, such as
a,b,c = d,e,f
→a=d, b=e, c=f
ora,b,c + d,e,f
→a+d, b+e, c+f
etc. - end operator −
.
at the end of scoped function definition acts as indicator of returned value, acts as a replacement to semicolon.
zzfx(...[,,1675,,.06,.24,1,1.82,,,837,.06])
:
import pow, sign, round, abs, max, pi, inf, sin from "math";
pi2 = pi*2;
sampleRate = 44100;
// waveshape generators
oscillator = [
phase -> [1 - 4 * abs( round(phase/pi2) - phase/pi2 )],
phase -> [sin(phase)]
];
// adsr weighting
adsr(x, a, d, (s, sv=1), r) = (
...i=0, t=i++/sampleRate;
a = max(a, 0.0001); // prevent click
total = a + d + s + r;
t >= total ? 0 : x * (
t < a ? t/a : // attack
t < a + d ? // decay
1-((t-a)/d)*(1-sv) : // decay falloff
t < a + d + s ? // sustain
sv : // sustain volume
(total - t)/r * sv
).
);
adsr(a, d, s, r) = x -> adsr(x, a, d, s, r); // pipe
// curve effect
curve(x, amt=1.82 in 0..10) = pow(sign(x) * abs(x), amt);
curve(amt) = x -> curve(x, amt);
// coin = triangle with pitch jump
coin(freq=1675, jump=freq/2, delay=0.06, shape=0) = (
...i=0, phase=0;
t = i++/sampleRate;
phase += (freq + t > delay ? jump : 0) * pi2 / sampleRate;
oscillator[shape](phase) | adsr(0, 0, .06, .24) | curve(1.82).
);
Features:
- pipes −
|
operator for a function calls that function with argument from the left side, eg.a | b
==b(a)
. - lambda functions − useful for organizing pipe transforms.
- arrays − linear collection of same-type elements with fixed size. Useful for organizing enums, dicts, buffers etc. Arrays support alias name for items:
a = [first: 1, second: 2]
→a[0] == a.first == 1
.
import comb from "./combfilter.son";
import allpass from "./allpass.son";
import floor from "math";
sampleRate = 44100;
a1,a2,a3,a4 = 1116,1188,1277,1356;
b1,b2,b3,b4 = 1422,1491,1557,1617;
p1,p2,p3,p4 = 225,556,441,341;
stretch(n) = floor(n * sampleRate / 44100);
sum(a, b) = a + b;
reverb((..input), room=0.5, damp=0.5) = (
...combs_a = a0,a1,a2,a3 | stretch;
...combs_b = b0,b1,b2,b3 | stretch;
...aps = p0,p1,p2,p3 | stretch;
..combs_a | a -> comb(a, input, room, damp) >- sum +
..combs_b | a -> comb(a, input, room, damp) >- sum;
^, ..aps >- (input, coef) -> p + allpass(p, coef, room, damp).
);
This features:
- multiarg pipes − pipe transforms can be applied to multiple arguments (similar to jQuery style).
- fold operator −
a,b,c >- fn
acts asreduce((a,b,c), fn)
, provides native way to efficiently apply reducer to a group or an array. - topic reference −
^
refers to result of last expression, so that expressions can be joined in flow fashion without intermediary variables. (that's similar to Hack pipeline or JS pipeline, without special operator).
Transpiled floatbeat/bytebeat song:
import pi, asin, sin from "math"
sampleRate = 44100
fract(x) = x % 1;
mix(a, b, c) = (a * (1 - c)) + (b * c);
tri(x) = 2 * asin(sin(x)) / pi;
noise(x) = sin((x + 10) * sin((x + 10) ** (fract(x) + 10)));
melodytest(time) = (
melodyString = "00040008";
melody = 0;
i = 0;
i++ < 5 ?..
melody += tri(
time * mix(
200 + (i * 900),
500 + (i * 900),
melodyString[floor(time * 2) % melodyString.length] / 16
)
) * (1 - fract(time * 4));
melody.
)
hihat(time) = noise(time) * (1 - fract(time * 4)) ** 10;
kick(time) = sin((1 - fract(time * 2)) ** 17 * 100);
snare(time) = noise(floor((time) * 108000)) * (1 - fract(time + 0.5)) ** 12;
melody(time) = melodytest(time) * fract(time * 2) ** 6 * 1;
song() = (
...t=0, time = t++ / sampleRate;
[(kick(time) + snare(time)*.15 + hihat(time)*.05 + melody(time)) / 4].
);
Features:
- loop operator −
cond ?.. expr
acts as while/until loop, calling expression until condition holds true. Can also be used in array comprehension as[x in 0..10 ?.. x*2]
. - string literal −
""
acts as array with ASCII codes.
🕉