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.
there's a wasm version here: https://tomara-x.github.io/lapis/
execute set_out_device(0,0);
for audio output to work
- 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
andShapeFn
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
- 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
andRing
, you can usebounded
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
andifft
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();
- 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 still4.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. forNet
,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
-
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
- all functions in the sound module
- std constants,
inf
,-inf
, andnan
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;
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()
let x = true && 2 < 8;
let y = 3 % 2 == 1;
if x {
// red pill
} else if y {
// blue pill
} else {
// get rambunctious
}
// 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;
}
}
deviations
- when writing vectors you write them as you would an array literal.
let arr = [1,2,3];
instead oflet arr = vec![1,2,3];
- only
push
,pop
,insert
,remove
,resize
,clear
,len
,clone
,first
,last
, andget
are supported pop
andremove
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 usenet.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
, andduration
is cast as f32 is_empty
,channel_mut
,write_wav16
,write_wav32
,load_slice
,load_slice_track
aren't implemented- methods on
Wave
s can only be called on a stored variable. so you can't sayWave::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
play
ed ortick
ed. do that to its backend (or a graph containing the backend) - methods
has_backend
,replay_events
, andtime
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)
);
// calling drop on any variable will drop that value
let f = 40;
f + 2;
// 42.0
f.drop();
f; // prints nothing
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
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
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
orrem_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)
- install rust: https://www.rust-lang.org/tools/install
- on linux you need
libjack-dev
andlibasound2-dev
(jack-devel
andalsa-lib-devel
on void) - clone lapis
git clone https://github.com/tomara-x/lapis.git
- build it
cd lapis
cargo run --release
- fundsp https://github.com/SamiPerttu/fundsp
- egui https://github.com/emilk/egui
- syn https://github.com/dtolnay/syn
- cpal https://github.com/rustaudio/cpal
- crossbeam_channel https://github.com/crossbeam-rs/crossbeam
- eframe_template https://github.com/emilk/eframe_template
lapis is free and open source. all code in this repository is dual-licensed under either:
- MIT License (LICENSE-MIT or http://opensource.org/licenses/MIT)
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
at your option.
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.