Skip to content

tomara-x/lapis

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

yeah, cause when i think "fun", i think "lapis"

this is an interactive interpreter for FunDSP. it allows you to experiment/play without needing to compile your code.

if you notice something incorrect, missing, or confusing, please open an issue to tell me about it, or fix it in a pull request if you can.

wasm

there's a wasm version here: https://tomara-x.github.io/lapis/

execute set_out_device(0,0); for audio output to work

limitations

  • you don't have the rust compiler looking over your shoulder
  • this isn't rust, you have a very small subset of the syntax
  • for functions that accept Shape as input, Adaptive and ShapeFn aren't supported
  • no closures and therefore none of the functions that take closures as input (yet)
  • input() and file i/o Wave methods won't work in the wasm version

additions

  • the net.play() method allows you to listen to an audio net (net must have 0 inputs and 1 or 2 outputs)
sine_hz(110).play(); // you should hear a 110hz tone
dc(0).play();        // back to silence
  • the input() node has 2 output channels (left and right channels of a stereo mic)
(input() >> reverb_stereo(20,3,0.5)).play();
// you should hear the input from your mic being played back
// (⚠️ might cause feedback)
  • similar to the functionality of Snoop and Ring, you can use bounded to create a ring buffer
bounded

let (i, o) = bounded(4); // channel with capacity 4 (maximum capacity is 1000000)
// the sender and the receiver are both wrapped in nets
i
// Inputs         : 1
// Outputs        : 1
// Latency        : 0.0 samples
// Footprint      : 248 bytes
// Size           : 1
o
// Inputs         : 0
// Outputs        : 1
// Latency        : 0.0 samples
// Footprint      : 248 bytes
// Size           : 1

// tick the input to send samples
// it passes its input through, so it can be placed
// anywhere in an audio graph to monitor the latest n samples
i.tick([1]);
// [1.0]
i.tick([2]);
// [2.0]
i.tick([3]);
// [3.0]
i.tick([4]);
// [4.0]

// this won't make it, the channel is full
i.tick([5]);
// [5.0]

// tick the output to receive samples
o.tick([]);
// [1.0]
o.tick([]);
// [2.0]
o.tick([]);
// [3.0]
o.tick([]);
// [4.0]

// channel is empty
o.tick([]);
// [0.0]

i.tick([1729]);
// [1729.0]
o.tick([]);
// [1729.0]
o.tick([]);
// [0.0]

  • rfft and ifft nodes (since using the fft functions directly isn't possible here)
example

let len = 512; // window length

// generate a hann window
let win_wave = Wave::new(1, 44100);
for i in 0..len {
    let p = 0.5 + -cos(i/len * TAU)/2;
    win_wave.push(p);
}

// for overlap (note the starting points of 1 and 3)
// they're staring points, not offsets (unlike the fft nodes)
let w0 = wavech_at(win_wave, 0, 0, len, 0);
let w1 = wavech_at(win_wave, 0, len * 0.75, len, 0);
let w2 = wavech_at(win_wave, 0, len * 0.50, len, 0);
let w3 = wavech_at(win_wave, 0, len * 0.25, len, 0);
let window = w0 | w1 | w2 | w3;

// split the input into 4 copies
let input = input() >> (pass() | sink()) >> split::<U4>();

let ft = rfft(len, 0)
       | rfft(len, len * 0.25)
       | rfft(len, len * 0.50)
       | rfft(len, len * 0.75);

let ift = ifft(len, 0)
        | ifft(len, len * 0.25)
        | ifft(len, len * 0.50)
        | ifft(len, len * 0.75);

let real = pass() | sink()
         | pass() | sink()
         | pass() | sink()
         | pass() | sink();

// we only care about the real output of the inverse fft
let ift = ift >> real;

// generate delay times wave (for delaying the bins)
let delay_wave = Wave::new(1, 44100);
for i in 0..len/2 {
    // random numbers from 0 to 500
    let p = rnd1(i) * 500;
    // we want an integer multiple of the window duration (so we don't freq shift)
    let p = p.round() * len;
    // push that duration in seconds instead of samples
    delay_wave.push(p / 44100);
}
// delay each bin by the amount of time in delay_wave
let tmp = (pass() | wavech(delay_wave, 0, 0)) >> tap(0, 15);
// need 8 copies (4 overlaps, real and imaginary each)
let process = Net::new(0,0);
for i in 0..8 {
    process = process | tmp.clone();
}

let g = input * window.clone() >> ft >> process >> ift * window >> join::<U4>() >> pan(0);

g.play();

deviations

  • every nodes is wrapped in a Net, it's all nets (🌍 🧑‍🚀 🔫 🧑‍🚀)
  • mutability is ignored. everything is mutable
  • type annotations are ignored. types are inferred (f32, Net, Vec<f32>, bool, NodeId, Arc<Wave>, Shared, Sequencer, EventId, Source,)
  • all number variables are f32, even if you type it as 4 it's still 4.0
  • when a function takes an integer or usize, if you type it as a literal integer, then they are parsed to the corresponding type. otherwise (a variable or an expression) they are evaluated as floats then cast to the needed type
  • an expression, like variable, 2 + 2, lowpass(), or [x, x+1, x+2] will print that expression's value. for Net, Wave, Sequencer, Shared, NodeId, EventId, it will print info about them.
  • everything is global. nothing is limited to scope except for the loop variable in for loops
  • Meter modes Peak and Rms are actually passed cast f32 not f64

what's supported

  • all functions in the hacker32 module

    • except for: branchi, busi, pipei, stacki, sumi, (and f versions), biquad_bank, envelope, envelope2, envelope3, envelope_in (and lfo), fdn, fdn2, multitap, multitap_linear, feedback2, flanger, map, oversample, phaser, resample, resynth, shape_fn, snoop, unit, update, var_fn
  • all functions in the math module

    • except for: ease_noise, fractal_ease_noise, hash1, hash2, identity
some f32 methods

  • floor
  • ceil
  • round
  • trunc
  • fract
  • abs
  • signum
  • copysign
  • div_euclid
  • rem_euclid
  • powi
  • powf
  • sqrt
  • exp
  • exp2
  • ln
  • log
  • log2
  • log10
  • cbrt
  • hypot
  • sin
  • cos
  • tan
  • asin
  • acos
  • atan
  • sinh
  • cosh
  • tanh
  • asinh
  • acosh
  • atanh
  • atan2
  • recip
  • to_degrees
  • to_radians
  • max
  • min

assignment

let x = 9;
let y = 4 + 4 * x - (3 - x * 4);
let osc3 = sine() & mul(3) >> sine() & mul(0.5) >> sine(); 
let f = lowpass_hz(1729, 0.5);
let out = osc3 >> f;

reassignment

let x = 42;
x += 1;
x = 56;         // x is still a number. this works
x = sine();     // x is a number can't assign an audio node (x is still 56.0)
let x = sine(); // x is now a sine()

if conditions

let x = true && 2 < 8;
let y = 3 % 2 == 1;
if x {
    // red pill
} else if y {
    // blue pill
} else {
    // get rambunctious
}

for loops

// with ranges
for i in 0..5 {
    let x = i + 3;
    x;
}
let node = Net::new(0,0);
for i in 0..=9 {
    node = node | dc(i);
}
// over array elements
let arr = [1,2,3];
for i in [4,6,8] {
    for j in arr {
        i * j;
    }
}

vectors

deviations

  • when writing vectors you write them as you would an array literal. let arr = [1,2,3]; instead of let arr = vec![1,2,3];
  • only push, pop, insert, remove, resize, clear, len, clone, first, last, and get are supported
  • pop and remove don't return a value, they just remove

let v1 = [1,2,4,6];
let v2 = v1; // identical to let v2 = v1.clone();
v1;
// [1.0, 2.0, 4.0, 6.0]
v2;
// [1.0, 2.0, 4.0, 6.0]
v1[0] = 42;
let f = v1.first();
f;
// 42.0
v1.pop();
v1;
// [42.0, 2.0, 4.0]
v2;
// [1.0, 2.0, 4.0, 6.0]
v1.resize(10,0.1);
v1;
// [42.0, 2.0, 4.0, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1]
v2;
// [1.0, 2.0, 4.0, 6.0]
deviations

  • node, node_mut, wrap, wrap_id, check, has_backend aren't supported
  • you can't use the ids method directly, but you can use net.ids().nth(n)

let net = Net::new(0,2);
net.backend().play();
let id = net.push(sine_hz(440));
net.connect_output(id,0,0);
net.connect_output(id,0,1);
net.commit();

Important

nets are moved

let x = sine();
let y = dc(220) >> x;
// x is no longer usable here as it's been moved into y

but you can avoid this by cloning

let x = sine();
let y = dc(220) >> x.clone();
// x is still usable here

process one frame of samples of a graph

let net = mul(10);
net.tick([4]); // prints [40.0]
let i = [6];
let o = [];
net.tick(i, o); // o is now [60.0]
let s = shared(440);
let g = var(s) >> sine();
g.play();
s.set(220);
s.set(s.value() + 42);
deviations

  • the remove method removes the channel but doesn't return a vec
  • the channel method returns a cloned vec
  • output from methods channels, len, and duration is cast as f32
  • is_empty, channel_mut, write_wav16, write_wav32, load_slice, load_slice_track aren't implemented
  • methods on Waves can only be called on a stored variable. so you can't say Wave::zero(2,44100,1).channel(0) for example. you have to assign the wave to a variable then call the method on that variable
  • it's actually an Arc, methods are called using Arc::make_mut.
  • can't be cloned

Arc(Wave)

// waves use Arc. they're cloned when mutated while other references exist
// (Arc::make_mut)

// (keep a system monitor open to watch the memory use)
// load a song
let wave = Wave::load("song.mp3");

// this doesn't clone the wave, since no other references exist
wave.set_sample_rate(48000);

// no memory use increase. the players use the same copy
let w1 = wavech(wave, 0);
let w2 = wavech(wave, 1);

// this causes the wave to be cloned (one in graphs, and the new edited one here)
wave.set_sample_rate(48000);

// redefining the graphs, dropping the old wave
let w1 = wavech(wave, 0);
let w2 = wavech(wave, 1);

// useless knowledge:
// if you're using `play()`, it has to be called twice for an old graph to be dropped
// since it uses a Slot, which keeps the previous graph for morphing

let w = Wave::load("./guidance.wav");   // load from file
w;                                      // prints info about the loaded wave
// Wave(ch:1, sr:11025, len:1101250, dur:99.88662131519274)

let osc = sine_hz(134) | saw_hz(42);
let s = Wave::render(44100, 1, osc);    // render 1 second of the given graph
s;                                      // print info
// Wave(ch:2, sr:44100, len:44100, dur:1)
s.save_wav16("awawawa.wav");            // save the wave as a 16-bit wav file
deviations

  • .backend() returns a SequencerBackend wrapped in a net. this way it can be used anywhere a net is usable
  • Sequencer itself can't be played or ticked. do that to its backend (or a graph containing the backend)
  • methods has_backend, replay_events, and time aren't supported
  • time arguments are all f32 cast as f64
  • you can't clone them (or their backends) (and why would you want to?)

let s = Sequencer::new(true, 2);
s;
// Sequencer(outs: 2, has_backend: false)
let b = s.backend();
b;
// Inputs         : 0
// Outputs        : 2
// Latency        : 0.0 samples
// Footprint      : 224 bytes
// Size           : 1
s;
// Sequencer(outs: 2, has_backend: true)
let g = b >> reverb_stereo(30,3,0.8);
g;
// Inputs         : 0
// Outputs        : 2
// Latency        : 0.0 samples
// Footprint      : 224 bytes
// Size           : 2
g.play();
s.push_relative(0, 2, Fade::Smooth, 0.2, 0.1,
	sine_hz(124) | sine_hz(323)
);

drop

// calling drop on any variable will drop that value
let f = 40;
f + 2;
// 42.0
f.drop();
f; // prints nothing

keyboard shortcuts

you can bind snippets of code to keyboard shortcuts. keys follow the egui key names, and modifiers ctrl, shift, alt, and command are supported

"ctrl+shift+a" = "
    // statements
";

"shift+a" = "
    // statements
";

"a" = "
    // statements
";

// reassign to an empty string to remove the key binding
"shift+a" = "";

shortcuts can be enabled/disabled using the "keys" toggle at the top of the ui

note: always define the more specific shortcuts (more modifiers) involving the same key before the less specific ones, so ctrl+shift+a then ctrl+a and shift+a then a

device selection

list_in_devices and list_out_devices will print an indexed list of hosts and the devices within them. you can use the indexes with set_in_device and set_out_device to select the devices lapis uses

list_in_devices();
// input devices:
// 0: Jack:
//     0: Ok("cpal_client_in")
//     1: Ok("cpal_client_out")
// 1: Alsa:
//     0: Ok("pipewire")
//     1: Ok("default")
//     2: Ok("sysdefault:CARD=sofhdadsp")
list_out_devices();
// output devices:
// 0: Jack:
//     0: Ok("cpal_client_in")
//     1: Ok("cpal_client_out")
// 1: Alsa:
//     0: Ok("pipewire")
//     1: Ok("default")

set_in_device(1, 2); // selects host 1 (alsa), device 2 (sysdef...) from the input devices list
set_out_device(1, 0); // selects host 1 (alsa), device 0 (pipewire) from the output list

f

i can't support map. as a workaround we have f. it takes a str argument and outputs that function wrapped in a node. you don't get to define custom functions (at runtime), but at least you get a bunch of basic ones that can then be stitched together like other nodes

let x = f(">")
x;
// Inputs         : 2
// Outputs        : 1
// Latency        : 0.0 samples
// Footprint      : 232 bytes
// Size           : 1
function list

  • rise
  • fall
  • >
  • <
  • ==
  • !=
  • >=
  • <=
  • min
  • max
  • pow
  • mod (or rem or rem_euclid)
  • log
  • bitand (those 5 bitwise functions cast their inputs to integer)
  • bitor
  • bitxor
  • shl
  • shr
  • lerp
  • lerp11
  • delerp
  • delerp11
  • xerp
  • xerp11
  • dexerp
  • dexerp11
  • abs
  • signum
  • floor
  • fract
  • ceil
  • round
  • sqrt
  • exp
  • exp2
  • exp10
  • exp_m1
  • ln_1p
  • ln
  • log2
  • log10
  • hypot
  • atan2
  • sin
  • cos
  • tan
  • asin
  • acos
  • atan
  • sinh
  • cosh
  • tanh
  • asinh
  • acosh
  • atanh
  • squared
  • cubed
  • dissonance
  • dissonance_max
  • db_amp
  • amp_db
  • a_weight
  • m_weight
  • spline
  • spline_mono
  • softsign
  • softexp
  • softmix
  • smooth3
  • smooth5
  • smooth7
  • smooth9
  • uparc
  • downarc
  • sine_ease
  • sin_hz
  • cos_hz
  • sqr_hz
  • tri_hz
  • semitone_ratio
  • rnd1
  • rnd2
  • spline_noise
  • fractal_noise
  • pol (takes cartesian, outputs polar)
  • car (takes polar, outputs cartesian)
  • deg (takes radians, outputs degrees)
  • rad (takes degrees, outputs radians)
  • recip
  • normal (filters inf/-inf/nan)

building

git clone https://github.com/tomara-x/lapis.git
  • build it
cd lapis
cargo run --release

thanks

license

lapis is free and open source. all code in this repository is dual-licensed under either:

at your option.

your contributions

unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.