Skip to content

Commit

Permalink
Finish up and integrate safety limiter
Browse files Browse the repository at this point in the history
 * Safety limiter works and produces (mostly) correct results
 * Add the same dB meter found in mixer small view to destination node small view
 * Fix issue that was causing crashes when loading saved subgraphs with some invalid data
   * There's probably another bug causing them to be created in a malformed state to begin with as well, but this works around it at least
 * Remove hard clipping to 0 dB from the Faust AWP template now that safety limiting is in place
   * I wonder how many weird issues that's been causing in the past...  It's been there for 4 years
  • Loading branch information
Ameobea committed Dec 15, 2024
1 parent cd45554 commit d78101e
Show file tree
Hide file tree
Showing 14 changed files with 269 additions and 59 deletions.
1 change: 1 addition & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ run:
cp ./engine/target/wasm32-unknown-unknown/release/oscilloscope.wasm ./public
cp ./engine/target/wasm32-unknown-unknown/release/spectrum_viz_full.wasm ./public
cp ./engine/target/wasm32-unknown-unknown/release/sampler.wasm ./public
cp ./engine/target/wasm32-unknown-unknown/release/safety_limiter.wasm ./public

just debug-sinsy

Expand Down
5 changes: 4 additions & 1 deletion engine/compressor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,9 @@ impl Compressor {
&mut self.lookback_period_squared_samples_sum,
),
};
if detected_level_linear.is_nan() || !detected_level_db.is_finite() {
detected_level_linear = 0.;
}

// I have no idea if this is right, and if I had to guess I'd say it's wrong
self.detected_level_history.set(detected_level_linear);
Expand Down Expand Up @@ -556,7 +559,7 @@ pub extern "C" fn init_compressor() -> *mut MultibandCompressor {
std::panic::set_hook(Box::new(|panic_info| {
// log with `error`
let mut buf = String::new();
let _ = write!(buf, "panic: {:?}", panic_info);
let _ = write!(buf, "panic: {}", panic_info.to_string());
error(&buf);
}));

Expand Down
71 changes: 60 additions & 11 deletions engine/engine/src/view_context/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -859,11 +859,11 @@ impl ViewContextManager {
new_uuid_by_old_uuid: &FxHashMap<Uuid, Uuid>,
new_sid_by_old_sid: &FxHashMap<String, String>,
old_id: &str,
) -> String {
) -> Option<String> {
if let Ok(uuid) = Uuid::from_str(old_id) {
new_uuid_by_old_uuid[&uuid].to_string()
new_uuid_by_old_uuid.get(&uuid).map(|u| u.to_string())
} else {
new_sid_by_old_sid[old_id].clone()
new_sid_by_old_sid.get(old_id).cloned()
}
}

Expand All @@ -887,23 +887,60 @@ impl ViewContextManager {

// Keys to update: `last_node_id`, `nodes`, `selectedNodeVcId`
if let Some(serde_json::Value::String(last_node_id)) = state.get_mut("last_node_id") {
*last_node_id = map_id(&new_uuid_by_old_uuid, &new_sid_by_old_sid, last_node_id);
if let Some(mapped_last_node_id) =
map_id(&new_uuid_by_old_uuid, &new_sid_by_old_sid, last_node_id)
{
*last_node_id = mapped_last_node_id
} else {
error!(
"`last_node_id` {last_node_id} from graph editor id {} in serialized subgraph is \
not in the subgraph",
vc.def.uuid
);
*last_node_id = String::new();
};
}
if let Some(serde_json::Value::String(selected_node_vc_id)) =
state.get_mut("selectedNodeVcId")
{
*selected_node_vc_id = map_id(
if let Some(mapped_selected_node_id) = map_id(
&new_uuid_by_old_uuid,
&new_sid_by_old_sid,
selected_node_vc_id,
);
) {
*selected_node_vc_id = mapped_selected_node_id
} else {
error!(
"`selected_node_vc_id` {selected_node_vc_id} from graph editor id {} in \
serialized subgraph is not in the subgraph",
vc.def.uuid
);
*selected_node_vc_id = String::new();
};
}
if let Some(serde_json::Value::Array(nodes)) = state.get_mut("nodes") {
for node in nodes {
// for node in nodes {

// }
nodes.retain_mut(|node| {
if let Some(serde_json::Value::String(vc_id)) = node.get_mut("id") {
*vc_id = map_id(&new_uuid_by_old_uuid, &new_sid_by_old_sid, vc_id);
if let Some(mapped_vc_id) =
map_id(&new_uuid_by_old_uuid, &new_sid_by_old_sid, vc_id)
{
*vc_id = mapped_vc_id;
true
} else {
error!(
"`node.id` {vc_id} from graph editor id {} in serialized subgraph is not in \
the subgraph",
vc.def.uuid
);
false
}
} else {
false
}
}
})
}

*val = serde_json::to_string(&state).unwrap();
Expand All @@ -927,8 +964,20 @@ impl ViewContextManager {
);

for conn in &mut serialized.intra_conns {
let new_tx_id = map_id(&new_uuid_by_old_uuid, &new_sid_by_old_sid, &conn.0.vc_id);
let new_rx_id = map_id(&new_uuid_by_old_uuid, &new_sid_by_old_sid, &conn.1.vc_id);
let new_tx_id = map_id(&new_uuid_by_old_uuid, &new_sid_by_old_sid, &conn.0.vc_id)
.unwrap_or_else(|| {
panic!(
"Intra conn {conn:?} contains tx ID ({}) which wasn't found in the subgraph",
conn.0.vc_id
)
});
let new_rx_id = map_id(&new_uuid_by_old_uuid, &new_sid_by_old_sid, &conn.1.vc_id)
.unwrap_or_else(|| {
panic!(
"Intra conn {conn:?} contains tx ID ({}) which wasn't found in the subgraph",
conn.1.vc_id
)
});

self.connections.push((
ConnectionDescriptor {
Expand Down
126 changes: 102 additions & 24 deletions engine/safety_limiter/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
use std::ptr::addr_of_mut;
use std::ptr::{addr_of, addr_of_mut};

use dsp::db_to_gain;

static mut IO_BUFFER: [f32; dsp::FRAME_SIZE] = [0.0; dsp::FRAME_SIZE];

const LOOKAHEAD_SAMPLE_COUNT: usize = 4;
// SAB format:
// 0: detected level dB
// 1: output level dB
// 2: applied gain
const SAB_SIZE: usize = 3;
static mut SAB: [f32; SAB_SIZE] = [0.; SAB_SIZE];

#[no_mangle]
pub extern "C" fn safety_limiter_get_sab_buf_ptr() -> *const f32 { addr_of!(SAB) as *const _ }

fn sab() -> &'static mut [f32; SAB_SIZE] { unsafe { &mut *addr_of_mut!(SAB) } }

const LOOKAHEAD_SAMPLE_COUNT: usize = 40;

struct SafetyLimiterState {
pub lookahead_buffer: [f32; LOOKAHEAD_SAMPLE_COUNT],
Expand All @@ -22,54 +34,60 @@ impl SafetyLimiterState {

static mut STATE: SafetyLimiterState = SafetyLimiterState::new();

const ATTACK_COEFFICIENT: f32 = 0.3;
const ATTACK_COEFFICIENT: f32 = 0.08;

const RELEASE_COEFFICIENT: f32 = 0.05;
const RELEASE_COEFFICIENT: f32 = 0.003;

const THRESHOLD: f32 = 10.;
const RATIO: f32 = 40.;
const THRESHOLD: f32 = 6.;
const RATIO: f32 = 200.;

fn io_buf() -> &'static mut [f32; dsp::FRAME_SIZE] { unsafe { &mut *addr_of_mut!(IO_BUFFER) } }

fn state() -> &'static mut SafetyLimiterState { unsafe { &mut *addr_of_mut!(STATE) } }

fn detect_level_peakand_apply_envelope(envelope: &mut f32, sample: f32) -> f32 {
let abs_sample = sample.abs();
println!("abs_sample={abs_sample}, envelope={envelope}");
fn detect_level_peak_and_apply_envelope(envelope: &mut f32, lookahead_sample: f32) -> f32 {
let abs_lookahead_sample = if lookahead_sample.is_normal() {
lookahead_sample.abs()
} else {
0.
};

dsp::one_pole(
envelope,
abs_sample,
if abs_sample > *envelope {
abs_lookahead_sample,
if abs_lookahead_sample > *envelope {
ATTACK_COEFFICIENT
} else {
RELEASE_COEFFICIENT
},
)
}

fn compute_output_level_db(detected_level_db: f32) -> f32 {
THRESHOLD + (detected_level_db - THRESHOLD) / RATIO
}

fn compute_gain_to_apply(detected_level_db: f32) -> f32 {
let target_level_db = THRESHOLD + (detected_level_db - THRESHOLD) / RATIO;
let db_to_reduce = detected_level_db - target_level_db;
let output_level_db = compute_output_level_db(detected_level_db);
let db_to_reduce = detected_level_db - output_level_db;
db_to_gain(-db_to_reduce)
}

fn process(envelope: &mut f32, sample: f32) -> f32 {
fn process(envelope: &mut f32, sample: f32, lookahead_sample: f32) -> f32 {
// some audio drivers behave badly when you send them `NaN` or `Infinity`...
if !sample.is_normal() {
return 0.;
}

// default to limiting with a very short attack and release
let detected_level_linear = detect_level_peakand_apply_envelope(envelope, sample);
dbg!(detected_level_linear);
let detected_level_linear = detect_level_peak_and_apply_envelope(envelope, lookahead_sample);
let detected_level_db = dsp::gain_to_db(detected_level_linear);

if detected_level_db < THRESHOLD {
return sample;
}

let gain_to_apply = compute_gain_to_apply(detected_level_db);
println!("sample={sample}, gain_to_apply={gain_to_apply}");
let sample = sample * gain_to_apply;

// apply hard clipping as a last resort
Expand All @@ -84,34 +102,94 @@ pub extern "C" fn safety_limiter_process() {
let state = state();
let io_buf = io_buf();

for &sample in &state.lookahead_buffer {
process(&mut state.envelope, sample);
}
let input_samples: [f32; dsp::FRAME_SIZE - LOOKAHEAD_SAMPLE_COUNT] =
std::array::from_fn(|i| io_buf[i]);

for &sample in &io_buf[..LOOKAHEAD_SAMPLE_COUNT] {
process(&mut state.envelope, sample);
for i in 0..LOOKAHEAD_SAMPLE_COUNT {
let sample = state.lookahead_buffer[i];
let lookahead_sample = io_buf[i];
io_buf[i] = process(&mut state.envelope, sample, lookahead_sample);
}

state
.lookahead_buffer
.copy_from_slice(&io_buf[io_buf.len() - LOOKAHEAD_SAMPLE_COUNT..]);

for i in 0..input_samples.len() {
let sample = input_samples[i];
let lookahead_sample: f32 = input_samples[i];
let sample = process(&mut state.envelope, sample, lookahead_sample);
io_buf[LOOKAHEAD_SAMPLE_COUNT + i] = sample;
}

let detected_level_linear = state.envelope;
let detected_level_db = dsp::gain_to_db(detected_level_linear);
let output_level_db = if detected_level_db > THRESHOLD {
compute_output_level_db(detected_level_db)
} else {
detected_level_db
};

let sab = sab();
sab[0] = detected_level_db;
sab[1] = output_level_db;
sab[2] = if detected_level_db < THRESHOLD {
1.
} else {
compute_gain_to_apply(detected_level_db)
};
}

#[test]
fn coefficients() {
println!("100. to db: {}", dsp::gain_to_db(100.));
println!("40. to db: {}", dsp::gain_to_db(40.));
println!("8. to db: {}", dsp::gain_to_db(8.));
println!("4. to db: {}", dsp::gain_to_db(4.));
println!("2.5 to db: {}", dsp::gain_to_db(2.5));

let mut envelope = 0.;
let signal = vec![
0., 0., 40., 40., 40., 40., 40., 40., 40., 40., 40., 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
];
let mut applied = vec![0.; signal.len()];

for i in 0..signal.len() {
applied[i] = process(&mut envelope, signal[i]);
applied[i] = process(
&mut envelope,
signal[i],
signal.get(i + 4).copied().unwrap_or(0.),
);
}

println!("{applied:?}")
}

#[test]
fn lookback_correctness() {
let mut data = Vec::new();
for i in 0..dsp::FRAME_SIZE * 8 {
data.push(0.0001 * i as f32);
}

let mut out = Vec::new();
for frame in data.chunks_exact(dsp::FRAME_SIZE) {
io_buf().copy_from_slice(frame);
safety_limiter_process();
println!("OUT: {:?}\n", io_buf());
out.extend(io_buf().iter().copied());
}

for i in 0..LOOKAHEAD_SAMPLE_COUNT {
assert_eq!(out[i], 0.);
}

for i in LOOKAHEAD_SAMPLE_COUNT..out.len() {
let val = i - LOOKAHEAD_SAMPLE_COUNT;
assert_eq!(out[i], val as f32 * 0.0001);
}
}
15 changes: 7 additions & 8 deletions faust-compiler/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
FROM golang:1.14beta1-buster as builder
FROM golang:1.23-bookworm as builder

RUN apt update && apt install -y cmake build-essential git pkg-config python3

RUN mkdir /faust
WORKDIR /
RUN git clone --depth 1 https://github.com/grame-cncm/faust.git
RUN git clone https://github.com/grame-cncm/faust.git faust
WORKDIR /faust
RUN git checkout 776d0ce3b2efb0303f82f1ce20a2aa97d1b790db
RUN make
RUN git checkout bfbbad9d5d4fcfcfcb9a0928e12ac19a22912f09
RUN make -j18
RUN make install

# Install `wasm-opt` via `binaryen`
RUN git clone --depth 1 https://github.com/WebAssembly/binaryen.git /tmp/binaryen
WORKDIR /tmp/binaryen
RUN git submodule init && git submodule update
RUN cmake . && make install
RUN cmake -DBUILD_TESTS=OFF . && make install
WORKDIR /
RUN rm -rf /tmp/binaryen

Expand All @@ -24,17 +24,16 @@ ADD . /build
RUN go build -o faust-compiler-server .
RUN cp faust-compiler-server /usr/local/bin/

FROM buildpack-deps:buster-scm
FROM buildpack-deps:bookworm-scm
COPY --from=builder /usr/local/bin/faust /usr/local/bin/faust
COPY --from=builder /usr/local/bin/faust2wasm /usr/local/bin/faust2wasm
COPY --from=builder /usr/local/lib/libOSCFaust.a /usr/local/lib/libOSCFaust.a
COPY --from=builder /usr/local/share/faust/ /usr/local/share/faust/
COPY --from=builder /build/faust-compiler-server /usr/local/bin/faust-compiler-server
COPY --from=builder /usr/local/bin/wasm-opt /usr/local/bin/wasm-opt
COPY --from=builder /usr/local/lib/libbinaryen.so /usr/local/lib/libbinaryen.so

# Install soul
RUN curl https://ameo.link/u/8qf.soul > /usr/bin/soul && chmod +x /usr/bin/soul
RUN curl https://i.ameo.link/8qf.soul > /usr/bin/soul && chmod +x /usr/bin/soul

RUN apt-get update && apt-get install -y ca-certificates libncurses5 libasound2 libfreetype6 && update-ca-certificates && curl https://get.wasmer.io -sSfL | sh && rm -rf /root/.wasmer/bin /root/.wasmer/lib/libwasmer.a
ENV LD_LIBRARY_PATH=/root/.wasmer/lib
Expand Down
8 changes: 8 additions & 0 deletions faust-compiler/Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,11 @@ docker-run:

run:
go build && ./faust-compiler-server

build-and-deploy:
#!/bin/bash

just docker-build
docker save ameo/faust-compiler-server:latest | bzip2 > /tmp/faust-compiler-server.tar.bz2
scp /tmp/faust-compiler-server.tar.bz2 [email protected]:/tmp/faust-compiler-server.tar.bz2
ssh [email protected] -t "cat /tmp/faust-compiler-server.tar.bz2 | bunzip2 | docker load && docker kill web-synth-faust-compiler && docker container rm web-synth-faust-compiler && docker run -d --name web-synth-faust-compiler -p 5401:5401 --restart=always -e SOUL_WORKLET_TEMPLATE_FILE_NAME=/opt/SoulAWP.template.js -e AUTH_TOKEN=jkl23489234lkiJKJjk892384928 -e GOOGLE_APPLICATION_CREDENTIALS=/opt/svc.json -e PORT=5401 -e FAUST_WORKLET_TEMPLATE_FILE_NAME=/opt/faustWorkletTemplate.template.js -v /opt/conf/web-synth/service_account.json:/opt/svc.json ameo/faust-compiler-server:latest && rm /tmp/faust-compiler-server.tar.bz2" && rm /tmp/faust-compiler-server.tar.bz2
Loading

0 comments on commit d78101e

Please sign in to comment.