/piezo

Prototype language for signal processing

Primary LanguageJavaScriptMIT LicenseMIT

piezo stability test

Low-level language for signal processing, synthesis and analysis.
Compiles to compact 0-runtime WASM with linear memory.

Reference

;; Operators
+ - * / % -- ++               ;; arithmetical (float)
** %% //                      ;; power, unsigned mod, flooring div
& | ^ ~ >> <<                 ;; binary (integer)
<<< >>>                       ;; rotate left, right
&& || !                       ;; logical
> >= < <= == !=               ;; comparisons (boolean)
?: ?                          ;; conditions
x[i] x[]                      ;; member access, length
a..b a.. ..b ..               ;; ranges
|> #                          ;; pipe/loop/map, topic reference
./ ../ /                      ;; continue/skip, break/stop, return
>< <>                         ;; inside, outside
~ ~/ ~*                       ;; clamp, normalize, lerp
* ^                           ;; static, defer

;; Numbers
16, 0x10, 0o755, 0b0;         ;; int, hex, oct or binary
16.0, .1, 2e-3;               ;; float
1k = 1000; 1pi = 3.1415926;   ;; units
1s = 44100; 1m = 60s;         ;; eg: sample indexes
10.1k, 2pi, 1m30s;            ;; 10100, 6.283..., 66150

;; Variables
foo=1, bar=2.0;               ;; declare vars
AbC, $0, Δx, x@1, A#;         ;; names permit alnum, unicodes, _$#@
foo == Foo, bar == bAr;       ;; case-insensitive
default=1, eval=fn, else=0;   ;; no reserved words
true = 0b1, false = 0b0;      ;; eg: alias bools
inf = 1/0, nan = 0/0;         ;; eg: alias infinity, NaN

;; Ranges
0..10;                        ;; from 1 to 9 (10 exclusive)
0.., ..10, ..;                ;; open ranges
10..1;                        ;; reverse range
1.08..108.0;                  ;; float range
(a-1)..(a+1);                 ;; computed range
0..3 * 2;                     ;; mapped range: 0*2, 1*2, 2*2
(a,b,c) = 0..3 * 2;           ;; destructure: a=0, b=2, c=4
a >< 0..10, a <> 0..10;       ;; a >= 0 && a < 10, a < 0 || a >= 10
a ~ 0..10, a ~= 0..10;        ;; clamp(a, 0, 10), a = clamp(a, 0, 10)
a ~* 0..10, a ~/ 0..10;       ;; lerp(a, 0, 10), normalize(a, 0, 10)

;; Groups
(a,b,c) = (1,2,3);            ;; assign: a=1, b=2, c=3
(a,b) = (b,a);                ;; swap
(a,b,c) = d;                  ;; duplicate: a=d, b=d, c=d
(a,,b) = (c,d,e);             ;; skip: a=c, b=e
(a,b) + (c,d);                ;; group binary: a+c, b+d
(a, b, c)++;                  ;; group unary: a++, b++, c++
(a,b)[1] = c[2,3];            ;; props: a[1]=c[2], b[1]=c[3]
(a,..,z) = (1,2,3,4);         ;; pick: a=1, z=4
a = (b,c,d);                  ;; pick first: a=b; see loops

;; Arrays
m = [..10];                   ;; array of 10 elements
m = [..10 |> 2];              ;; filled with 2
m = [1,2,3,4];                ;; array of 4 elements
m = [n[..]];                  ;; copy n
m = [1, 2..4, 5];             ;; mixed definition
m = [1, [2, 3, [4]]];         ;; nested arrays (tree)
m = [0..4 |> # ** 2];         ;; list comprehension
(a, z) = (m[0], m[-1]);       ;; get by index
(b, .., z) = m[1, 2..];       ;; get multiple values
length = m[];                 ;; get length
m[0] = 1;                     ;; set value
m[2..] = (1, 2..4, n[1..3]);  ;; set multiple values from offset 2
m[1,2] = m[2,1];              ;; swap
m[0..] = m[-1..];             ;; reverse
m[0..] = m[1..,0];            ;; rotate

;; Conditions
a ? b;                        ;; if a then b (else 0)
a ?: b;                       ;; elvis: if not a then b
sign = a < 0 ? -1 : +1;       ;; ternary conditional
(2+2 >= 4) ? log(1) :         ;; multiline/switch
  3 >< 1..2 ? log(2) :        ;; else if
  log(3);                     ;; else

;; Loops
(a, b, c) |> f(#);            ;; for each item in a, b, c do f(item)
(i = 10..) |> (               ;; descend over range
  i < 5 ? a ./;               ;; if item < 5 skip (continue)
  i < 0 ? a ../;              ;; if item < 0 stop (break)
);                            ;;
x[..] |> f(#) |> g(#);        ;; pipeline sequence
(i = 0..w) |> (               ;; nest iterations
  (j = 0..h) |> f(i, j);      ;; f(x,y)
);                            ;;
((a,b) = 0..10) |> a+b;       ;; iterate pairs
(x,,y) = (a,b,c) |> # * 2;    ;; capture result x = a*2, y = c*2;
.. |> i < 10 ? i++ : ../;     ;; while i < 10 i++

;; Functions
double(n) = n*2;              ;; define a function
times(m = 1, n ~= 1..) = (    ;; optional, clamped arg
  n == 0 ? /n;                ;; early return
  m * n;                      ;; returns last statement
);                            ;;
times(3,2);                   ;; 6
times(4), times(,5);          ;; 4, 5: optional, skipped arg
dup(x) = (x,x);               ;; return multiple
(a,b) = dup(b);               ;; destructure
a=1,b=1; x()=(a=2;b=2); x();  ;; a==1, b==2: first statement declares locals
fn() = ( x; ^log(x) );        ;; defer: calls log after returning x
f(a, cb) = cb(a[0]);      ;; array, func args
a() = ( *i=0; ++i );          ;; static var: i persists value
a(), a();                     ;; 1,2
a1() = ( *copy=a; copy() );   ;; clone function
a(), a(); a1(), a1();         ;; 3,4; 1,2;

;; Export
x, y, z;                      ;; exports last statement

Usage

piezo is available as CLI or JS package.

npm i -g piezo

CLI

piezo source.z -o dest.wasm

This produces compiled WASM binary.

JS

import piezo from 'piezo'

// create wasm arrayBuffer
const buffer = piezo.compile(`
  n=1;
  mult(x) = x*PI;
  arr=[1, 2, sin(1.08)];
  mult, n, arr;
`, {
  // js objects or paths to files
  imports: {
    math: Math,
    mylib: './path/to/my/lib.z'
  },
  // optional: import memory
  memory: true
})

// create wasm instance
const module = new WebAssembly.Module(buffer)
const instance = new WebAssembly.Instance(module, {
  imports: {
    math: Math,
    // imported memory
    memory: new WebAssembly.Memory({
      initial: 10,
      maximum: 100,
    })
  }
})

// use API
const { mult, n, arr, memory } = instance.exports

// number exported as global
n.value = 2;

// function exported directly
mult(108)

// array is a pointer to memory, get values via
const arrValues = new Float64Array(arr, memory)

Motivation

Audio processing has no cross-platform solution, various environments deal with audio differently, some don't have audio processing at all. Web Audio API is unreliable - it has unpredictable pauses, glitches and so on, so audio is better handled in WASM worklet (@stagas).

Piezo attempts to fill that gap, providing a common layer. It is also a personal attempt on language design - rethinking parts and providing safe haven. WASM target gives max performance and compatibility - browsers, audio/worklets, web-workers, nodejs, embedded systems etc.

Principles

  • Intuitivity: common syntax foundation, clear patterns for new operators.
  • Elegance: compact expressions, ideal for live coding.
  • Performance: compiles optimal code quickly, suited for live envs.
  • No keywords: variables are words, operators are symbols, allowing i18l code.
  • No runtime: statically analyzable, no OOP, no dynamic structures, no lamdas, no nested scopes.
  • No waste: linear memory, fixed heap, no GC.
  • Inferred types: derived from usage, focus on logic over language.
  • Explicit vars: no implicit globals, no wildcard imports, no hidden file conventions.
  • Space-agnostic: spaces and newlines can be removed or added freely (eg. for compression or formatting).
  • Case-agnostic: case changes don't break code (eg. sampleRate vs samplerate).
  • Normalized syntax: no complex parsing rules – just unary, binary or n-ary operators.
  • Readable output: produces readable WebAssembly text (eg. can serve as meta-language).
  • Low-level: no fancy features beyond math and buffers, compilable to ASM envs.

Inspiration

mono, zzfx, bytebeat, glitch, hxos, min, roland, porffor

Acknowledgement

  • @stagas for initial drive & ideas

⚡ 🕉