Skip to content

Instantly share code, notes, and snippets.

@danigb
Created March 13, 2022 17:50
Show Gist options
  • Save danigb/78f564df679f62337cb604c22081ccef to your computer and use it in GitHub Desktop.
Save danigb/78f564df679f62337cb604c22081ccef to your computer and use it in GitHub Desktop.
sonl draft

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.

Examples

Gain

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, direct arg 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.
  • validationa 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

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,fa=d, b=e, c=f or a,b,c + d,e,fa+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 operatora,b,c >- fn acts as reduce((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 operatorcond ?.. 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.

🕉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment