diff --git a/backend/src/models/synth_preset.rs b/backend/src/models/synth_preset.rs index 4a1e8789..e11be705 100644 --- a/backend/src/models/synth_preset.rs +++ b/backend/src/models/synth_preset.rs @@ -146,10 +146,17 @@ pub struct AudioThreadData { debug_name: Option, } +#[derive(Serialize, Deserialize)] +pub struct Point2D { + x: f32, + y: f32, +} + // export type RampFn = // | { type: 'linear' } // | { type: 'instant' } // | { type: 'exponential'; exponent: number }; +// | { type: 'bezier'; controlPoints: { x: number; y: number }[]; } #[derive(Serialize, Deserialize)] #[serde(tag = "type")] pub enum RampFn { @@ -159,6 +166,8 @@ pub enum RampFn { Instant, #[serde(rename = "exponential")] Exponential { exponent: f32 }, + #[serde(rename = "bezier")] + Bezier { control_points: Vec }, } // export interface AdsrStep { diff --git a/engine/Cargo.lock b/engine/Cargo.lock index 9ac7508a..7dc74a2b 100644 --- a/engine/Cargo.lock +++ b/engine/Cargo.lock @@ -6,6 +6,7 @@ version = 4 name = "adsr" version = "0.1.0" dependencies = [ + "common", "dsp", ] @@ -497,6 +498,7 @@ dependencies = [ name = "midi_renderer" version = "0.1.0" dependencies = [ + "common", "svg", ] @@ -877,6 +879,7 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" name = "safety_limiter" version = "0.1.0" dependencies = [ + "common", "compressor", "dsp", ] diff --git a/engine/adsr/Cargo.toml b/engine/adsr/Cargo.toml index 71c1f69a..398648b6 100644 --- a/engine/adsr/Cargo.toml +++ b/engine/adsr/Cargo.toml @@ -8,8 +8,9 @@ version = "0.1.0" crate-type = ["cdylib", "rlib"] [dependencies] -dsp = {path = "../dsp" } +dsp = { path = "../dsp", default-features = false } +common = { path = "../common", default-features = false } [features] exports = [] -default = ["exports"] # TODO REVERT +default = ["exports"] diff --git a/engine/adsr/src/exports.rs b/engine/adsr/src/exports.rs index 00d90566..02ffefd7 100644 --- a/engine/adsr/src/exports.rs +++ b/engine/adsr/src/exports.rs @@ -1,6 +1,10 @@ use std::rc::Rc; -use crate::{managed_adsr::ManagedAdsr, Adsr, AdsrStep, RampFn, RENDERED_BUFFER_SIZE}; +use common::ref_static_mut; + +use crate::{ + managed_adsr::ManagedAdsr, Adsr, AdsrLengthMode, AdsrStep, RampFn, RENDERED_BUFFER_SIZE, +}; extern "C" { fn log_err(msg: *const u8, len: usize); @@ -28,23 +32,32 @@ fn round_tiny_to_zero(val: f32) -> f32 { } } +/// Number of `f32`s needed to encode one step in the encoded ADSR step format +const STEP_F32_COUNT: usize = 7; + fn decode_steps(encoded_steps: &[f32]) -> Vec { assert_eq!( - encoded_steps.len() % 4, + encoded_steps.len() % STEP_F32_COUNT, 0, - "`encoded_steps` length must be divisible by 4" + "`encoded_steps` length must be divisible by {STEP_F32_COUNT}" ); encoded_steps - .chunks_exact(4) - .map(|vals| match vals { - &[x, y, ramp_fn_type, ramp_fn_param] => { + .array_chunks::() + .map( + |&[x, y, ramp_fn_type, ramp_fn_param_0, ramp_fn_param_1, ramp_fn_param_2, ramp_fn_param_3]| { let ramper = match ramp_fn_type { x if x == 0. => RampFn::Instant, x if x == 1. => RampFn::Linear, x if x == 2. => RampFn::Exponential { - exponent: ramp_fn_param, + exponent: ramp_fn_param_0, + }, + x if x == 3. => RampFn::Bezier { + x1: ramp_fn_param_0, + y1: ramp_fn_param_1, + x2: ramp_fn_param_2, + y2: ramp_fn_param_3, }, - _ => unreachable!("Invalid ramp fn type val"), + other => unreachable!("Invalid ramp fn type val: {other}"), }; AdsrStep { x: round_tiny_to_zero(x), @@ -52,30 +65,23 @@ fn decode_steps(encoded_steps: &[f32]) -> Vec { ramper, } }, - _ => unreachable!(), - }) + ) .collect() } static mut ENCODED_ADSR_STEP_BUF: Vec = Vec::new(); -/// Resizes the step buffer to hold at least `step_count` steps (`step_count * 4` f32s) +/// Resizes the step buffer to hold at least `step_count` steps (`step_count * STEP_F32_COUNT` f32s) #[no_mangle] pub unsafe extern "C" fn get_encoded_adsr_step_buf_ptr(step_count: usize) -> *mut f32 { - let needed_capacity = step_count * 4; - if ENCODED_ADSR_STEP_BUF.capacity() < needed_capacity { - let additional = needed_capacity - ENCODED_ADSR_STEP_BUF.capacity(); - ENCODED_ADSR_STEP_BUF.reserve(additional); + let needed_capacity = step_count * STEP_F32_COUNT; + let encoded_step_buf = ref_static_mut!(ENCODED_ADSR_STEP_BUF); + if encoded_step_buf.capacity() < needed_capacity { + let additional = needed_capacity - encoded_step_buf.capacity(); + encoded_step_buf.reserve(additional); } - ENCODED_ADSR_STEP_BUF.set_len(needed_capacity); - ENCODED_ADSR_STEP_BUF.as_mut_ptr() -} - -#[derive(Clone, Copy)] -#[repr(u32)] -pub enum AdsrLengthMode { - Ms = 0, - Beats = 1, + encoded_step_buf.set_len(needed_capacity); + encoded_step_buf.as_mut_ptr() } impl AdsrLengthMode { @@ -126,7 +132,7 @@ pub unsafe extern "C" fn create_adsr_ctx( let length_mode = AdsrLengthMode::from_u32(length_mode); let rendered: Rc<[f32; RENDERED_BUFFER_SIZE]> = Rc::new([0.0f32; RENDERED_BUFFER_SIZE]); - let decoded_steps = decode_steps(ENCODED_ADSR_STEP_BUF.as_slice()); + let decoded_steps = decode_steps(ref_static_mut!(ENCODED_ADSR_STEP_BUF).as_slice()); assert!(adsr_count > 0); let mut adsrs = Vec::with_capacity(adsr_count); @@ -164,7 +170,7 @@ pub unsafe extern "C" fn create_adsr_ctx( #[no_mangle] pub unsafe extern "C" fn update_adsr_steps(ctx: *mut AdsrContext) { - let decoded_steps = decode_steps(ENCODED_ADSR_STEP_BUF.as_slice()); + let decoded_steps = decode_steps(ref_static_mut!(ENCODED_ADSR_STEP_BUF).as_slice()); for adsr in &mut (*ctx).adsrs { adsr.adsr.set_steps(decoded_steps.clone()); } diff --git a/engine/adsr/src/lib.rs b/engine/adsr/src/lib.rs index 584e5fbc..0182dabb 100644 --- a/engine/adsr/src/lib.rs +++ b/engine/adsr/src/lib.rs @@ -1,4 +1,4 @@ -#![feature(get_mut_unchecked, array_windows)] +#![feature(get_mut_unchecked, array_windows, array_chunks)] use std::rc::Rc; @@ -19,24 +19,43 @@ const SAMPLE_RATE: usize = 44_100; pub const RENDERED_BUFFER_SIZE: usize = SAMPLE_RATE; const FRAME_SIZE: usize = 128; +#[derive(Clone, Copy)] +#[repr(u32)] +pub enum AdsrLengthMode { + Ms = 0, + Beats = 1, +} + #[derive(Clone, Copy)] pub enum RampFn { Instant, Linear, Exponential { exponent: f32 }, + Bezier { x1: f32, y1: f32, x2: f32, y2: f32 }, } impl RampFn { - pub fn from_u32(type_val: u32, param: f32) -> Self { + pub fn from_u32(type_val: u32, param0: f32, param1: f32, param2: f32, param3: f32) -> Self { match type_val { 0 => Self::Instant, 1 => Self::Linear, - 2 => Self::Exponential { exponent: param }, + 2 => Self::Exponential { exponent: param0 }, + 3 => Self::Bezier { + x1: param0, + y1: param1, + x2: param2, + y2: param3, + }, _ => panic!("Invlaid ramper fn type: {}", type_val), } } } +fn eval_cubic_bezier(y0: f32, y1: f32, y2: f32, y3: f32, t: f32) -> f32 { + let mt = 1. - t; + y0 * mt.powi(3) + 3. * y1 * mt.powi(2) * t + 3. * y2 * mt * t.powi(2) + y3 * t.powi(3) +} + fn compute_pos(prev_step: &AdsrStep, next_step: &AdsrStep, phase: f32) -> f32 { let distance = next_step.x - prev_step.x; debug_assert!(distance > 0.); @@ -53,7 +72,12 @@ fn compute_pos(prev_step: &AdsrStep, next_step: &AdsrStep, phase: f32) -> f32 { let y_diff = next_step.y - prev_step.y; let x = (phase - prev_step.x) / distance; prev_step.y + x.powf(exponent) * y_diff - // prev_step.y + even_faster_pow(x, exponent) * y_diff + }, + RampFn::Bezier { y1, y2, .. } => { + let y0 = prev_step.y; + let y3 = next_step.y; + let t = (phase - prev_step.x) / (next_step.x - prev_step.x); + eval_cubic_bezier(y0, y1, y2, y3, t) }, } } @@ -120,6 +144,7 @@ pub struct EarlyReleaseConfig { } impl EarlyReleaseConfig { + #[cfg(feature = "exports")] pub(crate) fn from_parts( early_release_mode_type: usize, early_release_mode_param: usize, diff --git a/engine/adsr/src/managed_adsr.rs b/engine/adsr/src/managed_adsr.rs index 2f7d2a3a..95fa9ac1 100644 --- a/engine/adsr/src/managed_adsr.rs +++ b/engine/adsr/src/managed_adsr.rs @@ -1,4 +1,4 @@ -use crate::{exports::AdsrLengthMode, Adsr, SAMPLE_RATE}; +use crate::{Adsr, AdsrLengthMode, SAMPLE_RATE}; fn ms_to_samples(ms: f32) -> f32 { (ms / 1000.) * SAMPLE_RATE as f32 } diff --git a/engine/common/src/lib.rs b/engine/common/src/lib.rs index 47dfcfc5..9999c3d1 100644 --- a/engine/common/src/lib.rs +++ b/engine/common/src/lib.rs @@ -35,11 +35,11 @@ pub fn set_raw_panic_hook(log_err: unsafe extern "C" fn(ptr: *const u8, len: usi std::panic::set_hook(Box::new(hook)) } -/// Implements `&mut *std::ptr::addr_of_mut!(x)` to work around the annoying new Rust rules on +/// Implements `&mut *&raw mut x` to work around the annoying new Rust rules on /// referencing static muts #[macro_export] macro_rules! ref_static_mut { ($x:expr) => { - unsafe { &mut *std::ptr::addr_of_mut!($x) } + unsafe { &mut *&raw mut $x } }; } diff --git a/engine/compressor/src/lib.rs b/engine/compressor/src/lib.rs index 62de0ade..3f269b6e 100644 --- a/engine/compressor/src/lib.rs +++ b/engine/compressor/src/lib.rs @@ -1,5 +1,3 @@ -#![feature(const_float_methods)] - use dsp::{ circular_buffer::CircularBuffer, db_to_gain, diff --git a/engine/dsp/src/lookup_tables.rs b/engine/dsp/src/lookup_tables.rs index 71352f9f..96be0502 100644 --- a/engine/dsp/src/lookup_tables.rs +++ b/engine/dsp/src/lookup_tables.rs @@ -1,22 +1,24 @@ const LOOKUP_TABLE_SIZE: usize = 1024 * 16; -static mut SINE_LOOKUP_TABLE: *mut [f32; LOOKUP_TABLE_SIZE] = std::ptr::null_mut(); +static mut SINE_LOOKUP_TABLE: [f32; LOOKUP_TABLE_SIZE] = [0.; LOOKUP_TABLE_SIZE]; + +fn get_sine_lookup_table_ptr() -> *const [f32; LOOKUP_TABLE_SIZE] { &raw const SINE_LOOKUP_TABLE } + +fn get_sine_lookup_table_ptr_mut() -> *mut [f32; LOOKUP_TABLE_SIZE] { &raw mut SINE_LOOKUP_TABLE } + pub fn get_sine_lookup_table() -> &'static [f32; LOOKUP_TABLE_SIZE] { - unsafe { &*SINE_LOOKUP_TABLE } + unsafe { &*get_sine_lookup_table_ptr() } } -#[inline(always)] -fn uninit() -> T { unsafe { std::mem::MaybeUninit::uninit().assume_init() } } - #[cold] pub fn maybe_init_lookup_tables() { unsafe { - if SINE_LOOKUP_TABLE.is_null() { - SINE_LOOKUP_TABLE = Box::into_raw(Box::new(uninit())); - + if SINE_LOOKUP_TABLE[1] == 0. { + let base_ptr = get_sine_lookup_table_ptr_mut() as *mut f32; for i in 0..LOOKUP_TABLE_SIZE { - *(*SINE_LOOKUP_TABLE).get_unchecked_mut(i) = - (std::f32::consts::PI * 2. * (i as f32 / LOOKUP_TABLE_SIZE as f32)).sin(); + let val = (std::f32::consts::PI * 2. * (i as f32 / LOOKUP_TABLE_SIZE as f32)).sin(); + let ptr = base_ptr.add(i); + std::ptr::write(ptr, val); } } } diff --git a/engine/engine/src/lib.rs b/engine/engine/src/lib.rs index 63f8e2ca..6b803230 100644 --- a/engine/engine/src/lib.rs +++ b/engine/engine/src/lib.rs @@ -228,12 +228,13 @@ pub fn set_foreign_connectables(foreign_connectables_json: &str) { #[wasm_bindgen] pub fn render_small_view(vc_id: &str, target_dom_id: &str) { let uuid = Uuid::from_str(&vc_id).expect("Invalid UUID string passed to `render_small_view`!"); - let vc_entry = get_vcm().get_vc_by_id_mut(uuid).unwrap_or_else(|| { - panic!( - "Attempted to get audio connectables of VC with ID {} but it wasn't found", - vc_id - ) - }); + let Some(vc_entry) = get_vcm().get_vc_by_id_mut(uuid) else { + error!( + "Attempted to get audio connectables of VC with ID {vc_id} while trying to render small \ + view, but it wasn't found", + ); + return; + }; vc_entry.context.render_small_view(target_dom_id); } @@ -241,12 +242,13 @@ pub fn render_small_view(vc_id: &str, target_dom_id: &str) { #[wasm_bindgen] pub fn cleanup_small_view(vc_id: &str, target_dom_id: &str) { let uuid = Uuid::from_str(&vc_id).expect("Invalid UUID string passed to `cleanup_small_view`!"); - let vc_entry = get_vcm().get_vc_by_id_mut(uuid).unwrap_or_else(|| { - panic!( - "Attempted to get audio connectables of VC with ID {} but it wasn't found", - vc_id - ) - }); + let Some(vc_entry) = get_vcm().get_vc_by_id_mut(uuid) else { + error!( + "Attempted to get audio connectables of VC with ID {vc_id} while cleaning up small view, \ + but it wasn't found", + ); + return; + }; vc_entry.context.cleanup_small_view(target_dom_id); } diff --git a/engine/event_scheduler/src/lib.rs b/engine/event_scheduler/src/lib.rs index c14b8963..0ab9c1e6 100644 --- a/engine/event_scheduler/src/lib.rs +++ b/engine/event_scheduler/src/lib.rs @@ -13,7 +13,7 @@ extern "C" { fn debug1(v: i32); } -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Debug)] struct MidiEvent { pub mailbox_ix: usize, pub param_0: f32, @@ -21,7 +21,7 @@ struct MidiEvent { pub event_type: u8, } -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Debug)] struct ScheduledEvent { pub time: f64, pub cb_id: i32, @@ -57,10 +57,18 @@ impl PartialOrd for ScheduledEvent { static mut SCHEDULED_EVENTS: BinaryHeap = BinaryHeap::new(); static mut SCHEDULED_BEAT_EVENTS: BinaryHeap = BinaryHeap::new(); +fn scheduled_events() -> &'static mut BinaryHeap { + ref_static_mut!(SCHEDULED_EVENTS) +} + +fn scheduled_beat_events() -> &'static mut BinaryHeap { + ref_static_mut!(SCHEDULED_BEAT_EVENTS) +} + #[no_mangle] pub unsafe extern "C" fn stop() { - SCHEDULED_EVENTS.clear(); - SCHEDULED_BEAT_EVENTS.clear(); + scheduled_events().clear(); + scheduled_beat_events().clear(); } #[no_mangle] @@ -69,13 +77,13 @@ pub extern "C" fn schedule(time: f64, cb_id: i32) { panic!(); } - unsafe { - SCHEDULED_EVENTS.push_unchecked(ScheduledEvent { + scheduled_events() + .push(ScheduledEvent { time, cb_id, midi_evt: None, }) - } + .expect("`SCHEDULED_EVENTS` is full"); } #[no_mangle] @@ -102,13 +110,13 @@ pub extern "C" fn schedule_beats( None }; - unsafe { - SCHEDULED_BEAT_EVENTS.push_unchecked(ScheduledEvent { + scheduled_beat_events() + .push(ScheduledEvent { time: beats, cb_id, midi_evt, }) - } + .expect("`SCHEDULED_BEAT_EVENTS` is full"); } fn handle_event(evt: ScheduledEvent) { @@ -128,27 +136,25 @@ fn handle_event(evt: ScheduledEvent) { #[no_mangle] pub extern "C" fn run(raw_cur_time: f64, cur_beats: f64) { - let scheduled_events = ref_static_mut!(SCHEDULED_EVENTS); loop { - match scheduled_events.peek() { + match scheduled_events().peek() { None => break, Some(evt) if evt.time > raw_cur_time => break, _ => (), } - let evt = unsafe { scheduled_events.pop_unchecked() }; + let evt = unsafe { scheduled_events().pop_unchecked() }; handle_event(evt); } - let scheduled_beat_events = ref_static_mut!(SCHEDULED_BEAT_EVENTS); loop { - match scheduled_beat_events.peek() { + match scheduled_beat_events().peek() { None => break, Some(evt) if evt.time > cur_beats => break, _ => (), } - let evt = unsafe { scheduled_beat_events.pop_unchecked() }; + let evt = unsafe { scheduled_beat_events().pop_unchecked() }; handle_event(evt); } } @@ -173,12 +179,10 @@ pub unsafe extern "C" fn alloc_ids_buffer(count: usize) -> *mut i32 { #[no_mangle] pub extern "C" fn cancel_events_by_ids() -> usize { let ids = unsafe { &*IDS_BUFFER }.as_slice(); - let scheduled_events = ref_static_mut!(SCHEDULED_EVENTS); - let scheduled_beat_events = ref_static_mut!(SCHEDULED_BEAT_EVENTS); let mut actually_cancelled_evt_count = 0; - let new_scheduled_events = scheduled_events + let new_scheduled_events = scheduled_events() .iter() .filter(|evt| { let should_remove = ids.contains(&evt.cb_id); @@ -189,12 +193,14 @@ pub extern "C" fn cancel_events_by_ids() -> usize { }) .cloned() .collect::>(); - scheduled_events.clear(); + scheduled_events().clear(); for evt in new_scheduled_events { - unsafe { scheduled_events.push_unchecked(evt) } + scheduled_events() + .push(evt) + .expect("`SCHEDULED_EVENTS` is full") } - let new_scheduled_beat_events = scheduled_beat_events + let new_scheduled_beat_events = scheduled_beat_events() .iter() .filter(|evt| { let should_remove = ids.contains(&evt.cb_id); @@ -205,9 +211,11 @@ pub extern "C" fn cancel_events_by_ids() -> usize { }) .cloned() .collect::>(); - scheduled_beat_events.clear(); + scheduled_beat_events().clear(); for evt in new_scheduled_beat_events { - unsafe { scheduled_beat_events.push_unchecked(evt) } + scheduled_beat_events() + .push(evt) + .expect("`SCHEDULED_BEAT_EVENTS` is full") } actually_cancelled_evt_count diff --git a/engine/midi_renderer/Cargo.toml b/engine/midi_renderer/Cargo.toml index 6081143e..c77c0e59 100644 --- a/engine/midi_renderer/Cargo.toml +++ b/engine/midi_renderer/Cargo.toml @@ -9,3 +9,4 @@ crate-type = ["cdylib", "rlib"] [dependencies] svg = "0.13" +common = { path = "../common", default-features = false} diff --git a/engine/midi_renderer/src/lib.rs b/engine/midi_renderer/src/lib.rs index 16fba3f3..0f8291be 100644 --- a/engine/midi_renderer/src/lib.rs +++ b/engine/midi_renderer/src/lib.rs @@ -1,5 +1,6 @@ //! Renders MIDI notes into a SVG that can be displayed as a minimap. +use common::ref_static_mut; use svg::{node::element::Rectangle, Document}; use crate::conf::{MINIMAP_HEIGHT_PX, MIN_MIDI_NUMBER_RANGE, NOTE_COLOR}; @@ -10,11 +11,12 @@ static mut ENCODED_DATA_BUFFER: Vec = Vec::new(); #[no_mangle] pub extern "C" fn get_encoded_notes_buf_ptr(encoded_byte_length: usize) -> *mut u8 { + let mut buf = Vec::with_capacity(encoded_byte_length); unsafe { - ENCODED_DATA_BUFFER = Vec::with_capacity(encoded_byte_length); - ENCODED_DATA_BUFFER.set_len(encoded_byte_length); - ENCODED_DATA_BUFFER.as_mut_ptr() + buf.set_len(encoded_byte_length); + ENCODED_DATA_BUFFER = buf; } + ref_static_mut!(ENCODED_DATA_BUFFER).as_mut_ptr() } static mut RENDERED_SVG_TEXT: String = String::new(); @@ -28,7 +30,7 @@ struct EncodedMIDINote { #[no_mangle] pub extern "C" fn midi_minimap_render_minimap(_beats_per_measure: f32) -> *const u8 { - let data_buf: &[u8] = unsafe { ENCODED_DATA_BUFFER.as_slice() }; + let data_buf: &[u8] = ref_static_mut!(ENCODED_DATA_BUFFER).as_slice(); assert_eq!(data_buf.len() % 12, 0); let encoded_notes: &[EncodedMIDINote] = unsafe { std::slice::from_raw_parts( @@ -87,9 +89,11 @@ pub extern "C" fn midi_minimap_render_minimap(_beats_per_measure: f32) -> *const let svg_text: String = doc.to_string(); unsafe { RENDERED_SVG_TEXT = svg_text; - RENDERED_SVG_TEXT.as_bytes().as_ptr() } + ref_static_mut!(RENDERED_SVG_TEXT).as_bytes().as_ptr() } #[no_mangle] -pub extern "C" fn midi_minimap_get_svg_text_length() -> usize { unsafe { RENDERED_SVG_TEXT.len() } } +pub extern "C" fn midi_minimap_get_svg_text_length() -> usize { + ref_static_mut!(RENDERED_SVG_TEXT).len() +} diff --git a/engine/multiband_diode_ladder_distortion/src/lib.rs b/engine/multiband_diode_ladder_distortion/src/lib.rs index dd99d3b1..a2161527 100644 --- a/engine/multiband_diode_ladder_distortion/src/lib.rs +++ b/engine/multiband_diode_ladder_distortion/src/lib.rs @@ -15,23 +15,21 @@ pub extern "C" fn init() { } #[no_mangle] -pub extern "C" fn get_input_buf_ptr() -> *mut f32 { - std::ptr::addr_of_mut!(INPUT_BUFFER) as *mut f32 -} +pub extern "C" fn get_input_buf_ptr() -> *mut f32 { &raw mut INPUT_BUFFER as *mut f32 } #[no_mangle] pub extern "C" fn get_low_output_buf_ptr() -> *mut f32 { - std::ptr::addr_of_mut!(LOW_BAND_OUTPUT_BUFFER) as *mut f32 + &raw mut LOW_BAND_OUTPUT_BUFFER as *mut f32 } #[no_mangle] pub extern "C" fn get_mid_output_buf_ptr() -> *mut f32 { - std::ptr::addr_of_mut!(MID_BAND_OUTPUT_BUFFER) as *mut f32 + &raw mut MID_BAND_OUTPUT_BUFFER as *mut f32 } #[no_mangle] pub extern "C" fn get_high_output_buf_ptr() -> *mut f32 { - std::ptr::addr_of_mut!(HIGH_BAND_OUTPUT_BUFFER) as *mut f32 + &raw mut HIGH_BAND_OUTPUT_BUFFER as *mut f32 } #[no_mangle] diff --git a/engine/noise_gen/src/lib.rs b/engine/noise_gen/src/lib.rs index 53d33b17..701f75d2 100644 --- a/engine/noise_gen/src/lib.rs +++ b/engine/noise_gen/src/lib.rs @@ -106,5 +106,5 @@ pub unsafe extern "C" fn generate() -> *const f32 { *out = LAST_VAL; } - OUTPUT.as_ptr() + &raw const OUTPUT as *const f32 } diff --git a/engine/oscilloscope/src/lib.rs b/engine/oscilloscope/src/lib.rs index 2664b215..db64aa6e 100644 --- a/engine/oscilloscope/src/lib.rs +++ b/engine/oscilloscope/src/lib.rs @@ -105,7 +105,7 @@ pub extern "C" fn oscilloscope_renderer_process(cur_bpm: f32, cur_beat: f32, cur #[no_mangle] pub extern "C" fn oscilloscope_renderer_get_frame_data_ptr() -> *const f32 { - unsafe { FRAME_DATA_BUFFER.as_ptr() } + &raw const FRAME_DATA_BUFFER as *const _ } #[no_mangle] diff --git a/engine/safety_limiter/Cargo.toml b/engine/safety_limiter/Cargo.toml index 7698764a..44f0825f 100644 --- a/engine/safety_limiter/Cargo.toml +++ b/engine/safety_limiter/Cargo.toml @@ -10,3 +10,4 @@ crate-type = ["cdylib", "rlib"] [dependencies] dsp = { path = "../dsp" } compressor = { path = "../compressor", default-features = false } +common = { path = "../common", default-features = false } diff --git a/engine/safety_limiter/src/lib.rs b/engine/safety_limiter/src/lib.rs index ceef0eb4..f8ae92c9 100644 --- a/engine/safety_limiter/src/lib.rs +++ b/engine/safety_limiter/src/lib.rs @@ -1,5 +1,4 @@ -use std::ptr::{addr_of, addr_of_mut}; - +use common::ref_static_mut; use dsp::db_to_gain; static mut IO_BUFFER: [f32; dsp::FRAME_SIZE] = [0.0; dsp::FRAME_SIZE]; @@ -12,9 +11,9 @@ 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 _ } +pub extern "C" fn safety_limiter_get_sab_buf_ptr() -> *const f32 { &raw const SAB as *const _ } -fn sab() -> &'static mut [f32; SAB_SIZE] { unsafe { &mut *addr_of_mut!(SAB) } } +fn sab() -> &'static mut [f32; SAB_SIZE] { ref_static_mut!(SAB) } const LOOKAHEAD_SAMPLE_COUNT: usize = 40; @@ -41,9 +40,9 @@ const RELEASE_COEFFICIENT: f32 = 0.003; 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 io_buf() -> &'static mut [f32; dsp::FRAME_SIZE] { ref_static_mut!(IO_BUFFER) } -fn state() -> &'static mut SafetyLimiterState { unsafe { &mut *addr_of_mut!(STATE) } } +fn state() -> &'static mut SafetyLimiterState { ref_static_mut!(STATE) } fn detect_level_peak_and_apply_envelope(envelope: &mut f32, lookahead_sample: f32) -> f32 { let abs_lookahead_sample = if lookahead_sample.is_normal() { diff --git a/engine/sample_editor/src/lib.rs b/engine/sample_editor/src/lib.rs index a50294f7..a559275f 100644 --- a/engine/sample_editor/src/lib.rs +++ b/engine/sample_editor/src/lib.rs @@ -198,8 +198,8 @@ pub extern "C" fn set_gain_envelope_ptr(len: usize) -> *mut f32 { unsafe { GAIN_ENVELOPE_LEN = len; - GAIN_ENVELOPE_BUF.as_mut_ptr() } + &raw mut GAIN_ENVELOPE_BUF as *mut _ } fn decode_gain_envelope() -> Vec { diff --git a/engine/vocoder/src/lib.rs b/engine/vocoder/src/lib.rs index 2a5b9d6e..b75763eb 100644 --- a/engine/vocoder/src/lib.rs +++ b/engine/vocoder/src/lib.rs @@ -47,6 +47,7 @@ pub struct LevelDetectionBand(RMSLevelDetector); impl LevelDetectionBand { fn new(band_center_freq_hz: f32) -> Self { + // TODO: I'm pretty sure this value isn't good and contributes to poor vocoder quality let window_size_samples = compute_level_detection_window_samples(band_center_freq_hz); LevelDetectionBand(RMSLevelDetector::new(window_size_samples.ceil() as usize)) } @@ -98,9 +99,7 @@ const FILTER_PARAMS_BUF_LEN: usize = BAND_COUNT * FILTERS_PER_BAND * 2; pub static mut FILTER_PARAMS_BUF: [f32; FILTER_PARAMS_BUF_LEN] = [0.; FILTER_PARAMS_BUF_LEN]; #[no_mangle] -pub extern "C" fn get_filter_params_buf_ptr() -> *mut f32 { - unsafe { FILTER_PARAMS_BUF.as_mut_ptr() } -} +pub extern "C" fn get_filter_params_buf_ptr() -> *mut f32 { &raw mut FILTER_PARAMS_BUF as *mut _ } #[inline(always)] fn uninit() -> T { unsafe { MaybeUninit::uninit().assume_init() } } diff --git a/engine/wav_decoder/src/lib.rs b/engine/wav_decoder/src/lib.rs index 0c8b4b09..49d90e4e 100644 --- a/engine/wav_decoder/src/lib.rs +++ b/engine/wav_decoder/src/lib.rs @@ -1,12 +1,13 @@ #[macro_use] extern crate log; +use common::ref_static_mut; use wasm_bindgen::prelude::*; static mut ERROR_MESSAGE: String = String::new(); #[wasm_bindgen] -pub fn get_error_message() -> String { unsafe { ERROR_MESSAGE.clone() } } +pub fn get_error_message() -> String { ref_static_mut!(ERROR_MESSAGE).clone() } #[wasm_bindgen] pub fn decode_wav(data: Vec) -> Vec { @@ -16,9 +17,9 @@ pub fn decode_wav(data: Vec) -> Vec { let mut reader = match hound::WavReader::new(data.as_slice()) { Ok(r) => r, Err(e) => { - error!("Error parsing wav file: {}", e); + error!("Error parsing wav file: {e}"); unsafe { - ERROR_MESSAGE = format!("Error parsing wav file: {}", e); + ERROR_MESSAGE = format!("Error parsing wav file: {e}"); } return Vec::new(); }, @@ -41,9 +42,9 @@ pub fn decode_wav(data: Vec) -> Vec { match res { Ok(samples) => samples, Err(err) => { - error!("Error decoding wav file: {:?}", err); + error!("Error decoding wav file: {err:?}"); unsafe { - ERROR_MESSAGE = format!("Error decoding wav file: {:?}", err); + ERROR_MESSAGE = format!("Error decoding wav file: {err:?}"); } Vec::new() }, diff --git a/engine/wavegen/src/bindings.rs b/engine/wavegen/src/bindings.rs index cdc103fa..49ac5416 100644 --- a/engine/wavegen/src/bindings.rs +++ b/engine/wavegen/src/bindings.rs @@ -56,9 +56,7 @@ static mut WAVEFORM_RENDERER_CTX: *mut WaveformRendererCtx = std::ptr::null_mut( static mut ENCODED_STATE_BUF: [f32; HARMONIC_COUNT * 2] = [0.; HARMONIC_COUNT * 2]; #[no_mangle] -pub extern "C" fn get_encoded_state_buf_ptr() -> *mut f32 { - unsafe { ENCODED_STATE_BUF.as_mut_ptr() } -} +pub extern "C" fn get_encoded_state_buf_ptr() -> *mut f32 { &raw mut ENCODED_STATE_BUF as *mut _ } fn get_waveform_renderer_ctx() -> &'static mut WaveformRendererCtx { if !unsafe { WAVEFORM_RENDERER_CTX.is_null() } { diff --git a/engine/wavetable/src/fm/effects/chorus.rs b/engine/wavetable/src/fm/effects/chorus.rs index ae6642c7..c5fad504 100644 --- a/engine/wavetable/src/fm/effects/chorus.rs +++ b/engine/wavetable/src/fm/effects/chorus.rs @@ -4,7 +4,7 @@ use super::Effect; use crate::fm::{ParamSource, SAMPLE_RATE}; use dsp::circular_buffer::CircularBuffer; -const MAX_CHORUS_DELAY_SAMPLES: usize = SAMPLE_RATE / 20; // 50ms +pub const MAX_CHORUS_DELAY_SAMPLES: usize = SAMPLE_RATE / 20; // 50ms const NUM_TAPS: usize = 8; const TWO_PI: f32 = PI * 2.; diff --git a/engine/wavetable/src/fm/effects/comb_filter.rs b/engine/wavetable/src/fm/effects/comb_filter.rs index 6ef7ec0d..c35720fc 100644 --- a/engine/wavetable/src/fm/effects/comb_filter.rs +++ b/engine/wavetable/src/fm/effects/comb_filter.rs @@ -3,7 +3,7 @@ use dsp::circular_buffer::CircularBuffer; use super::Effect; use crate::fm::{ParamSource, FRAME_SIZE, SAMPLE_RATE}; -const MAX_DELAY_SAMPLES: usize = SAMPLE_RATE * 4; +pub const MAX_DELAY_SAMPLES: usize = SAMPLE_RATE * 4; #[derive(Clone)] pub struct CombFilter { diff --git a/engine/wavetable/src/fm/effects/delay.rs b/engine/wavetable/src/fm/effects/delay.rs index e120ae3b..7c69c6d5 100644 --- a/engine/wavetable/src/fm/effects/delay.rs +++ b/engine/wavetable/src/fm/effects/delay.rs @@ -3,7 +3,7 @@ use dsp::circular_buffer::CircularBuffer; use super::Effect; use crate::fm::{ParamSource, SAMPLE_RATE}; -const MAX_DELAY_SAMPLES: usize = SAMPLE_RATE * 10; +pub const MAX_DELAY_SAMPLES: usize = SAMPLE_RATE * 10; #[derive(Clone)] pub struct Delay { diff --git a/engine/wavetable/src/fm/effects/mod.rs b/engine/wavetable/src/fm/effects/mod.rs index fb5df460..28c0826b 100644 --- a/engine/wavetable/src/fm/effects/mod.rs +++ b/engine/wavetable/src/fm/effects/mod.rs @@ -93,7 +93,7 @@ pub enum EffectInstance { } impl EffectInstance { - /// Construts a new effect instance from the raw params passed over from JS + /// Constructs a new effect instance from the raw params passed over from JS pub fn from_parts( effect_type: usize, param_1_type: usize, @@ -266,8 +266,11 @@ impl EffectInstance { EffectInstance::ButterworthFilter(ButterworthFilter::new(mode, cutoff_freq)) }, 6 => { + let buffer: Box> = + unsafe { Box::new_zeroed().assume_init() }; + let delay = Delay { - buffer: Box::new(CircularBuffer::new()), + buffer, delay_samples: ParamSource::from_parts( param_1_type, param_1_int_val, @@ -328,9 +331,14 @@ impl EffectInstance { EffectInstance::MoogFilter(moog_filter) }, 8 => { + let input_buffer: Box> = + unsafe { Box::new_zeroed().assume_init() }; + let feedback_buffer: Box> = + unsafe { Box::new_zeroed().assume_init() }; + let comb_filter = CombFilter { - input_buffer: Box::new(CircularBuffer::new()), - feedback_buffer: Box::new(CircularBuffer::new()), + input_buffer, + feedback_buffer, delay_samples: ParamSource::from_parts( param_1_type, param_1_int_val, @@ -378,8 +386,11 @@ impl EffectInstance { lfo_phases[i] = common::rng().gen_range(0., std::f32::consts::PI * 2.); } + let buffer: Box> = + unsafe { Box::new_zeroed().assume_init() }; + let chorus = ChorusEffect { - buffer: Box::new(CircularBuffer::new()), + buffer, modulation_depth: ParamSource::from_parts( param_1_type, param_1_int_val, diff --git a/engine/wavetable/src/fm/mod.rs b/engine/wavetable/src/fm/mod.rs index 3e164061..c0ee10cb 100644 --- a/engine/wavetable/src/fm/mod.rs +++ b/engine/wavetable/src/fm/mod.rs @@ -2,10 +2,10 @@ use core::arch::wasm32::*; use polysynth::{PolySynth, SynthCallbacks}; use rand::Rng; -use std::{cell::Cell, rc::Rc}; +use std::{cell::Cell, mem::MaybeUninit, rc::Rc}; use adsr::{ - exports::AdsrLengthMode, managed_adsr::ManagedAdsr, Adsr, AdsrStep, EarlyReleaseConfig, + managed_adsr::ManagedAdsr, Adsr, AdsrLengthMode, AdsrStep, EarlyReleaseConfig, EarlyReleaseStrategy, GateStatus, RampFn, RENDERED_BUFFER_SIZE, }; use dsp::{midi_number_to_frequency, oscillator::PhasedOscillator}; @@ -37,7 +37,9 @@ extern "C" { pub fn log_err_str(s: &str) { unsafe { log_err(s.as_ptr(), s.len()) } } -pub static mut MIDI_CONTROL_VALUES: [f32; 1024] = [0.; 1024]; +const MAX_MIDI_CONTROL_VALUE_COUNT: usize = 1024; +pub static mut MIDI_CONTROL_VALUES: [f32; MAX_MIDI_CONTROL_VALUE_COUNT] = + [0.; MAX_MIDI_CONTROL_VALUE_COUNT]; const GAIN_ENVELOPE_PHASE_BUF_INDEX: usize = 255; const FILTER_ENVELOPE_PHASE_BUF_INDEX: usize = 254; @@ -47,7 +49,7 @@ const VOICE_COUNT: usize = 32; #[no_mangle] pub unsafe extern "C" fn fm_synth_set_midi_control_value(index: usize, value: usize) { - if index >= MIDI_CONTROL_VALUES.len() || value > 127 { + if index >= MAX_MIDI_CONTROL_VALUE_COUNT || value > 127 { panic!(); } @@ -986,16 +988,7 @@ impl FMSynthVoice { output: 0., adsrs: Vec::new(), adsr_params: Vec::new(), - operators: [ - Operator::default(), - Operator::default(), - Operator::default(), - Operator::default(), - Operator::default(), - Operator::default(), - Operator::default(), - Operator::default(), - ], + operators: std::array::from_fn(|_| Operator::default()), last_samples: [0.0; OPERATOR_COUNT], last_sample_frequencies_per_operator: [0.0; OPERATOR_COUNT], effect_chain: EffectChain::default(), @@ -1923,25 +1916,31 @@ pub unsafe extern "C" fn init_fm_synth_ctx() -> *mut FMSynthContext { init_sample_manager(); common::set_raw_panic_hook(log_err); - let ctx = Box::into_raw(Box::new(FMSynthContext { - voices: Box::new(uninit()), - modulation_matrix: ModulationMatrix::default(), - param_buffers: uninit(), - filter_param_buffers: uninit(), - operator_base_frequency_sources: uninit(), - base_frequency_input_buffer: Box::new(uninit()), - output_buffers: Box::new(uninit()), - main_output_buffer: uninit(), - frequency_multiplier: 1., - most_recent_gated_voice_ix: 0, - adsr_phase_buf: [0.; 256], - detune: None, - wavetables: Vec::new(), - sample_mapping_manager: SampleMappingManager::default(), - polysynth: uninit(), - master_gain: 1., - last_master_gain: 1., - })); + let mut ctx: Box> = Box::new_uninit(); + unsafe { + let voices_ptr = &mut (*ctx.as_mut_ptr()).voices; + std::ptr::write(voices_ptr, Box::new_uninit().assume_init()); + let modulation_matrix_ptr = &mut (*ctx.as_mut_ptr()).modulation_matrix; + std::ptr::write(modulation_matrix_ptr, ModulationMatrix::default()); + let base_frequency_input_buffer_ptr = &mut (*ctx.as_mut_ptr()).base_frequency_input_buffer; + std::ptr::write( + base_frequency_input_buffer_ptr, + Box::new_uninit().assume_init(), + ); + let output_buffers_ptr = &mut (*ctx.as_mut_ptr()).output_buffers; + std::ptr::write(output_buffers_ptr, Box::new_uninit().assume_init()); + (*ctx.as_mut_ptr()).frequency_multiplier = 1.; + (*ctx.as_mut_ptr()).most_recent_gated_voice_ix = 0; + (*ctx.as_mut_ptr()).adsr_phase_buf = [0.; 256]; + (*ctx.as_mut_ptr()).detune = None; + let wavetables_ptr = &mut (*ctx.as_mut_ptr()).wavetables; + std::ptr::write(wavetables_ptr, Vec::new()); + let sample_mapping_manager_ptr = &mut (*ctx.as_mut_ptr()).sample_mapping_manager; + std::ptr::write(sample_mapping_manager_ptr, SampleMappingManager::default()); + (*ctx.as_mut_ptr()).master_gain = 1.; + (*ctx.as_mut_ptr()).last_master_gain = 1.; + } + let ctx = Box::into_raw(ctx.assume_init()); std::ptr::write( &mut (*ctx).polysynth, @@ -1973,13 +1972,17 @@ pub unsafe extern "C" fn init_fm_synth_ctx() -> *mut FMSynthContext { offset_hz: 0., }); } + // let shared_gain_adsr_rendered_buffer: Box<[f32; RENDERED_BUFFER_SIZE]> = + // Box::new([0.242424; RENDERED_BUFFER_SIZE]); let shared_gain_adsr_rendered_buffer: Box<[f32; RENDERED_BUFFER_SIZE]> = - Box::new([0.242424; RENDERED_BUFFER_SIZE]); + Box::new_uninit().assume_init(); let shared_gain_adsr_rendered_buffer: Rc<[f32; RENDERED_BUFFER_SIZE]> = shared_gain_adsr_rendered_buffer.into(); + // let shared_filter_adsr_rendered_buffer: Box<[f32; RENDERED_BUFFER_SIZE]> = + // Box::new([0.424242; RENDERED_BUFFER_SIZE]); let shared_filter_adsr_rendered_buffer: Box<[f32; RENDERED_BUFFER_SIZE]> = - Box::new([0.424242; RENDERED_BUFFER_SIZE]); + Box::new_uninit().assume_init(); let shared_filter_adsr_rendered_buffer: Rc<[f32; RENDERED_BUFFER_SIZE]> = shared_filter_adsr_rendered_buffer.into(); @@ -2507,11 +2510,20 @@ static mut ADSR_STEP_BUFFER: [AdsrStep; 512] = [AdsrStep { }; 512]; #[no_mangle] -pub unsafe extern "C" fn set_adsr_step_buffer(i: usize, x: f32, y: f32, ramper: u32, param: f32) { +pub unsafe extern "C" fn set_adsr_step_buffer( + i: usize, + x: f32, + y: f32, + ramper: u32, + param0: f32, + param1: f32, + param2: f32, + param3: f32, +) { ADSR_STEP_BUFFER[i] = AdsrStep { x, y, - ramper: RampFn::from_u32(ramper, param), + ramper: RampFn::from_u32(ramper, param0, param1, param2, param3), } } diff --git a/engine/wavetable/src/lib.rs b/engine/wavetable/src/lib.rs index c7fddd67..69d5ef30 100644 --- a/engine/wavetable/src/lib.rs +++ b/engine/wavetable/src/lib.rs @@ -1,4 +1,4 @@ -#![feature(get_mut_unchecked)] +#![feature(get_mut_unchecked, new_zeroed_alloc)] pub mod fm; diff --git a/public/FMSynthAWP.js b/public/FMSynthAWP.js index 435ee6fc..e97c0bac 100644 --- a/public/FMSynthAWP.js +++ b/public/FMSynthAWP.js @@ -204,9 +204,9 @@ class FMSynthAWP extends AudioWorkletProcessor { } const { adsrIx, steps, lenSamples, releasePoint, loopPoint, logScale } = evt.data; - steps.forEach(({ x, y, ramper, param }, stepIx) => { - this.wasmInstance.exports.set_adsr_step_buffer(stepIx, x, y, ramper, param); - }); + steps.forEach(({ x, y, ramper, params }, stepIx) => + this.wasmInstance.exports.set_adsr_step_buffer(stepIx, x, y, ramper, ...params) + ); this.wasmInstance.exports.set_adsr( this.ctxPtr, adsrIx, @@ -532,9 +532,9 @@ class FMSynthAWP extends AudioWorkletProcessor { ) ); adsrs.forEach(({ steps, lenSamples, releasePoint, loopPoint, logScale, adsrIx }) => { - steps.forEach(({ x, y, ramper, param }, stepIx) => { - this.wasmInstance.exports.set_adsr_step_buffer(stepIx, x, y, ramper, param); - }); + steps.forEach(({ x, y, ramper, params }, stepIx) => + this.wasmInstance.exports.set_adsr_step_buffer(stepIx, x, y, ramper, ...params) + ); this.wasmInstance.exports.set_adsr( this.ctxPtr, adsrIx, diff --git a/src/controls/adsr2/adsr2.tsx b/src/controls/adsr2/adsr2.tsx index d13a906b..702e6f40 100644 --- a/src/controls/adsr2/adsr2.tsx +++ b/src/controls/adsr2/adsr2.tsx @@ -51,6 +51,63 @@ if (PIXI.settings.RENDER_OPTIONS) { PIXI.settings.RENDER_OPTIONS.hello = false; } +const evalCubicBezier = ( + x0: number, + y0: number, + x1: number, + y1: number, + x2: number, + y2: number, + x3: number, + y3: number, + t: number +) => { + // the control point X values (x1 and x2) are normalized to be between x0 and x3, + // so they must be scaled back to the original range first + x1 = x0 + x1 * (x3 - x0); + x2 = x0 + x2 * (x3 - x0); + + const mt = 1 - t; + const x = mt * mt * mt * x0 + 3 * mt * mt * t * x1 + 3 * mt * t * t * x2 + t * t * t * x3; + const y = mt * mt * mt * y0 + 3 * mt * mt * t * y1 + 3 * mt * t * t * y2 + t * t * t * y3; + return { x, y }; +}; + +interface Point { + x: number; + y: number; +} + +const computeCubicBezierControlPoints = ( + startPoint: Point, + handlePos: Point, + endPoint: Point +): { clampedHandlePos: Point; controlPoints: [Point, Point] } => { + // TODO: Add link to note deriving this + const controlPoint = { + x: (4 / 3) * handlePos.x - (1 / 6) * startPoint.x - (1 / 6) * endPoint.x, + y: (4 / 3) * handlePos.y - (1 / 6) * startPoint.y - (1 / 6) * endPoint.y, + }; + const xMin = Math.min(startPoint.x, endPoint.x); + const xMax = Math.max(startPoint.x, endPoint.x); + controlPoint.x = R.clamp(xMin, xMax, controlPoint.x); + controlPoint.y = R.clamp(0, 1, controlPoint.y); + + const clampedHandlePos = { + x: (1 / 8) * startPoint.x + (3 / 4) * controlPoint.x + (1 / 8) * endPoint.x, + y: (1 / 8) * startPoint.y + (3 / 4) * controlPoint.y + (1 / 8) * endPoint.y, + }; + + // The control point's X value needs to be normalized to the range between + // the start and end points. + // + // This allows the start and end points to be adjusted without the possibility + // of invalidating the control points. + controlPoint.x = (controlPoint.x - startPoint.x) / (endPoint.x - startPoint.x); + + return { controlPoints: [controlPoint, controlPoint], clampedHandlePos }; +}; + /** * Controls the properties of a ramp curve. Can be dragged, but must be bounded by the marks that define * the start and stop of the ramp it belongs to. @@ -87,6 +144,23 @@ class RampHandle { this.inst.height - (this.startStep.y * this.inst.height + y * rampHeightPx) ); } + case 'bezier': { + const { x, y } = evalCubicBezier( + this.startStep.x, + this.startStep.y, + this.endStep.ramper.controlPoints[0].x, + this.endStep.ramper.controlPoints[0].y, + this.endStep.ramper.controlPoints[1].x, + this.endStep.ramper.controlPoints[1].y, + this.endStep.x, + this.endStep.y, + 0.5 + ); + return new PIXI.Point( + computeTransformedXPosition(this.renderedRegion, this.inst.width, x), + (1 - y) * this.inst.height + ); + } default: { throw new UnreachableError( 'Ramp type does not support modifying curve: ' + this.endStep.ramper.type @@ -96,11 +170,11 @@ class RampHandle { } private computeNewEndPoint(pos: PIXI.Point) { - // handle inverted direction of y axis compared to what we want - pos.y = this.inst.height - pos.y; - switch (this.endStep.ramper.type) { case 'exponential': { + // handle inverted direction of y axis compared to what we want + pos.y = this.inst.height - pos.y; + const x = R.clamp( 0.01, 0.99, @@ -124,6 +198,24 @@ class RampHandle { this.endStep.ramper.exponent = exponent; break; } + case 'bezier': { + const { controlPoints: newControlPoints, clampedHandlePos } = + computeCubicBezierControlPoints( + { x: this.startStep.x, y: this.startStep.y }, + { + x: computeReverseTransformedXPosition(this.renderedRegion, this.inst.width, pos.x), + y: 1 - pos.y / this.inst.height, + }, + { x: this.endStep.x, y: this.endStep.y } + ); + + this.endStep.ramper.controlPoints = newControlPoints ?? []; + this.graphics.position.set( + computeTransformedXPosition(this.renderedRegion, this.inst.width, clampedHandlePos.x), + (1 - clampedHandlePos.y) * this.inst.height + ); + break; + } default: { throw new UnreachableError( 'Ramp type does not support modifying curve: ' + this.endStep.ramper.type @@ -133,17 +225,29 @@ class RampHandle { } private handleDrag(newPos: PIXI.Point) { - // Always constrain drags to the area defined by the marks + // Always constrain drags to the area defined by the marks, except for bezier + // curve which can have its handle's Y coord go outside of the Y bounds of the + // start/end points. newPos.x = R.clamp( computeTransformedXPosition(this.renderedRegion, this.inst.width, this.startStep.x), computeTransformedXPosition(this.renderedRegion, this.inst.width, this.endStep.x), newPos.x ); - newPos.y = R.clamp( - Math.min((1 - this.startStep.y) * this.inst.height, (1 - this.endStep.y) * this.inst.height), - Math.max((1 - this.startStep.y) * this.inst.height, (1 - this.endStep.y) * this.inst.height), - newPos.y - ); + const minY = + this.endStep.ramper.type === 'bezier' + ? 0 + : Math.min( + (1 - this.startStep.y) * this.inst.height, + (1 - this.endStep.y) * this.inst.height + ); + const maxY = + this.endStep.ramper.type === 'bezier' + ? this.inst.height + : Math.max( + (1 - this.startStep.y) * this.inst.height, + (1 - this.endStep.y) * this.inst.height + ); + newPos.y = R.clamp(minY, maxY, newPos.y); this.graphics.position.set(newPos.x, newPos.y); this.computeNewEndPoint(newPos); @@ -242,7 +346,8 @@ class RampCurve { private buildRampHandle(startStep: AdsrStep, endStep: AdsrStep) { switch (endStep.ramper.type) { - case 'exponential': { + case 'exponential': + case 'bezier': { return new RampHandle(this.inst, this, startStep, endStep, this.renderedRegion); } default: { @@ -251,7 +356,7 @@ class RampCurve { } } - private computeRampCurve(step1: AdsrStep, step2: AdsrStep): { x: number; y: number }[] { + private computeRampCurve(step1: AdsrStep, step2: AdsrStep): Point[] { const step1PosXPx = computeTransformedXPosition(this.renderedRegion, this.inst.width, step1.x); const step2PosXPx = computeTransformedXPosition(this.renderedRegion, this.inst.width, step2.x); @@ -286,6 +391,37 @@ class RampCurve { { x: step2PosXPx, y: (1 - step2.y) * this.inst.height }, ]; } + case 'bezier': { + if (step2.ramper.controlPoints.length !== 2) { + throw new Error( + `Invalid number of control points; expected 2, got ${step2.ramper.controlPoints.length}` + ); + } + + const x0 = step1.x; + const y0 = step1.y; + const x1 = step2.ramper.controlPoints[0].x; + const y1 = step2.ramper.controlPoints[0].y; + const x2 = step2.ramper.controlPoints[1].x; + const y2 = step2.ramper.controlPoints[1].y; + const x3 = step2.x; + const y3 = step2.y; + + const widthPx = (x3 - x0) * this.inst.width; + const pointCount = Math.ceil(widthPx / INTERPOLATED_SEGMENT_LENGTH_PX) + 1; + + const pts = []; + for (let i = 0; i <= pointCount; i++) { + const t = i / pointCount; + const { x, y } = evalCubicBezier(x0, y0, x1, y1, x2, y2, x3, y3, t); + pts.push({ + x: computeTransformedXPosition(this.renderedRegion, this.inst.width, x), + y: (1 - y) * this.inst.height, + }); + } + + return pts; + } } } @@ -1097,9 +1233,19 @@ export class ADSR2Instance { this.deserialize(initialState); } else { this.steps = [ - { x: 0, y: 0.5, ramper: { type: 'linear' as const } }, - { x: 0.5, y: 0.8, ramper: { type: 'exponential' as const, exponent: 1.5 } }, - { x: 1, y: 0.5, ramper: { type: 'exponential' as const, exponent: 1.1 } }, + { ramper: { type: 'instant' as const }, x: 0, y: 0 }, + { + x: 0.006, + y: 1, + ramper: { + type: 'bezier' as const, + controlPoints: [ + { x: 0.4, y: 0.47 }, + { x: 0.4, y: 0.47 }, + ], + }, + }, + { ramper: { type: 'instant' as const }, x: 1, y: 0 }, ].map(step => new StepHandle(this, step, this.renderedRegion, this.infiniteMode)); this.releasePoint = 0.8; } @@ -1112,12 +1258,32 @@ export class ADSR2Instance { } private addStep(pos: PIXI.Point) { + const normalizedPosX = computeReverseTransformedXPosition( + this.renderedRegion, + this.width, + pos.x + ); + const prevStep = this.steps.findLast(step => step.step.x < normalizedPosX)?.step ?? { + x: 0, + y: 0.5, + ramper: { type: 'linear' }, + }; + const normalizedPosY = 1 - pos.y / this.height; + const midpointX = (prevStep.x + normalizedPosX) / 2; + const midpointY = (prevStep.y + normalizedPosY) / 2; + + const { controlPoints } = computeCubicBezierControlPoints( + prevStep, + { x: midpointX, y: midpointY }, + { x: normalizedPosX, y: normalizedPosY } + ); + const step = new StepHandle( this, { x: computeReverseTransformedXPosition(this.renderedRegion, this.width, pos.x), y: 1 - pos.y / this.height, - ramper: { type: 'exponential' as const, exponent: 0.1 }, + ramper: { type: 'bezier' as const, controlPoints }, }, this.renderedRegion, this.infiniteMode @@ -1251,9 +1417,9 @@ export class ADSR2Instance { } public openStepHandleConfigurator( - step: { x: number; y: number }, + step: Point, evt: PointerEvent, - onSubmit: (newStep: { x: number; y: number }) => void, + onSubmit: (newStep: Point) => void, enableY = true ) { this.closeStepHandleConfigurator(); diff --git a/src/controls/adsr2/adsr2Helpers.ts b/src/controls/adsr2/adsr2Helpers.ts index 851f184e..0b548e12 100644 --- a/src/controls/adsr2/adsr2Helpers.ts +++ b/src/controls/adsr2/adsr2Helpers.ts @@ -4,13 +4,33 @@ import { SAMPLE_RATE } from 'src/util'; export const buildDefaultADSR2Envelope = (audioThreadData: AudioThreadData): Adsr => ({ steps: [ - { x: 0, y: 0.2, ramper: { type: 'exponential', exponent: 0.5 } }, - { x: 0.5, y: 0.8, ramper: { type: 'exponential', exponent: 0.5 } }, - { x: 1, y: 0.2, ramper: { type: 'exponential', exponent: 0.5 } }, + { ramper: { type: 'instant' as const }, x: 0, y: 0.9 }, + { + x: 0.8, + y: 0.45, + ramper: { + type: 'bezier' as const, + controlPoints: [ + { x: 0.3, y: 0.6 }, + { x: 0.3, y: 0.6 }, + ], + }, + }, + { + x: 1, + y: 0, + ramper: { + type: 'bezier' as const, + controlPoints: [ + { x: 0.2, y: 0.2 }, + { x: 0.2, y: 0.2 }, + ], + }, + }, ], lenSamples: SAMPLE_RATE / 4, loopPoint: 0, - releasePoint: 0.7, + releasePoint: 0.8, audioThreadData, logScale: true, }); diff --git a/src/graphEditor/nodes/CustomAudio/FMSynth/FMSynth.tsx b/src/graphEditor/nodes/CustomAudio/FMSynth/FMSynth.tsx index d4520ead..ca0f360d 100644 --- a/src/graphEditor/nodes/CustomAudio/FMSynth/FMSynth.tsx +++ b/src/graphEditor/nodes/CustomAudio/FMSynth/FMSynth.tsx @@ -72,7 +72,46 @@ const buildDefaultModulationIndices = (): ParamSource[][] => { export type RampFn = | { type: 'linear' } | { type: 'instant' } - | { type: 'exponential'; exponent: number }; + | { type: 'exponential'; exponent: number } + | { + type: 'bezier'; + /** + * Control points' X values are normalized to be between that of the start + * and end point of the curve it defines + */ + controlPoints: { x: number; y: number }[]; + }; + +export const encodeRampFnType = (rampFnType: RampFn['type']): number => + ({ linear: 0, instant: 1, exponential: 2, bezier: 3 })[rampFnType]; + +export const encodeRampFnParams = ( + startStep: AdsrStep | undefined, + endStep: AdsrStep, + ramper: RampFn +): [number, number, number, number] => { + switch (ramper.type) { + case 'exponential': + return [ramper.exponent, 0, 0, 0]; + case 'bezier': + if (ramper.controlPoints.length === 2) { + // control points' X coords are stored as normalized values between the curve's + // start and end points, so we have to convert them back into absolute values + const startX = startStep?.x ?? 0; + const endX = endStep.x; + const x1 = startX + ramper.controlPoints[0].x * (endX - startX); + const x2 = startX + ramper.controlPoints[1].x * (endX - startX); + + return [x1, ramper.controlPoints[0].y, x2, ramper.controlPoints[1].y]; + } else { + throw new Error( + `Invalid number of control points for bezier ramp; expected 2, got ${ramper.controlPoints.length}` + ); + } + default: + return [0, 0, 0, 0]; + } +}; /** * Corresponds to `AdsrStep` in the Wasm engine @@ -344,16 +383,16 @@ export default class FMSynth implements ForeignNode { this.cleanupSmallView = mkContainerCleanupHelper({ preserveRoot: true }); } - private encodeAdsrStep(step: AdsrStep) { - const param = step.ramper.type === 'exponential' ? step.ramper.exponent : 0; - const ramper = { linear: 0, instant: 1, exponential: 2 }[step.ramper.type]; - return { x: step.x, y: step.y, ramper, param }; + private encodeAdsrStep(prevStep: AdsrStep | undefined, step: AdsrStep) { + const params = encodeRampFnParams(prevStep, step, step.ramper); + const ramper = encodeRampFnType(step.ramper.type); + return { x: step.x, y: step.y, ramper, params }; } private encodeAdsr(adsr: AdsrParams, adsrIx: number) { return { adsrIx, - steps: adsr.steps.map(step => this.encodeAdsrStep(step)), + steps: adsr.steps.map((step, stepIx) => this.encodeAdsrStep(adsr.steps[stepIx - 1], step)), lenSamples: encodeParamSource(adsr.lenSamples), releasePoint: adsr.releasePoint, loopPoint: adsr.loopPoint, @@ -824,7 +863,9 @@ export default class FMSynth implements ForeignNode { this.awpHandle.port.postMessage({ type: 'setAdsr', adsrIx, - steps: newAdsr.steps.map(step => this.encodeAdsrStep(step)), + steps: newAdsr.steps.map((step, stepIx) => + this.encodeAdsrStep(newAdsr.steps[stepIx - 1], step) + ), lenSamples: encodeParamSource(newAdsr.lenSamples), releasePoint: newAdsr.releasePoint, loopPoint: newAdsr.loopPoint, diff --git a/src/graphEditor/nodes/CustomAudio/Subgraph/SubgraphPortalNode.ts b/src/graphEditor/nodes/CustomAudio/Subgraph/SubgraphPortalNode.ts index 5e18f0c1..4fbbd098 100644 --- a/src/graphEditor/nodes/CustomAudio/Subgraph/SubgraphPortalNode.ts +++ b/src/graphEditor/nodes/CustomAudio/Subgraph/SubgraphPortalNode.ts @@ -414,10 +414,7 @@ export class SubgraphPortalNode implements ForeignNode { node: this.placeholderInput, }); for (const [name, descriptor] of Object.entries(get(this.registeredInputs))) { - inputs = inputs.set(name, { - type: 'any', - node: descriptor.node, - }); + inputs = inputs.set(name, descriptor); } let outputs = ImmMap().set(this.placeholderOutput.label, { @@ -425,10 +422,7 @@ export class SubgraphPortalNode implements ForeignNode { node: this.placeholderOutput, }); for (const [name, descriptor] of Object.entries(get(this.registeredOutputs))) { - outputs = outputs.set(name, { - type: 'any', - node: descriptor.node, - }); + outputs = outputs.set(name, descriptor); } return { diff --git a/src/midiEditor/CVOutput/CVOutput.ts b/src/midiEditor/CVOutput/CVOutput.ts index 3d378e07..60d16a66 100644 --- a/src/midiEditor/CVOutput/CVOutput.ts +++ b/src/midiEditor/CVOutput/CVOutput.ts @@ -40,8 +40,28 @@ export const buildDefaultCVOutputState = ( // temp value that will be changed when steps are added to the envelope lenSamples: 44_100 * 100, steps: [ - { x: 0, y: 0, ramper: { type: 'exponential', exponent: 1 } }, - { x: 4, y: 1, ramper: { type: 'exponential', exponent: 1 } }, + { + x: 0, + y: 0, + ramper: { + type: 'bezier', + controlPoints: [ + { x: 0.5, y: 0.5 }, + { x: 0.5, y: 0.5 }, + ], + }, + }, + { + x: 4, + y: 1, + ramper: { + type: 'bezier', + controlPoints: [ + { x: 0.5, y: 0.5 }, + { x: 0.5, y: 0.5 }, + ], + }, + }, ], loopPoint: null, releasePoint: 1, @@ -123,7 +143,13 @@ export class CVOutput { normalizedSteps.push({ ...normalizedSteps[normalizedSteps.length - 1], x: 1, - ramper: { type: 'linear' }, + ramper: { + type: 'bezier', + controlPoints: [ + { x: 0.5, y: 0.5 }, + { x: 0.5, y: 0.5 }, + ], + }, }); } diff --git a/src/synthDesigner/ADSRModule.tsx b/src/synthDesigner/ADSRModule.tsx index 48f97fa7..50e4d6b1 100644 --- a/src/synthDesigner/ADSRModule.tsx +++ b/src/synthDesigner/ADSRModule.tsx @@ -4,6 +4,8 @@ import { buildDefaultAdsrEnvelope, type ADSRValues } from 'src/controls/adsr'; import type { AudioThreadData } from 'src/controls/adsr2/adsr2'; import { AdsrLengthMode, + encodeRampFnParams, + encodeRampFnType, type Adsr, type AdsrStep, } from 'src/graphEditor/nodes/CustomAudio/FMSynth/FMSynth'; @@ -87,12 +89,22 @@ export class ADSR2Module { } private static encodeADSRSteps(steps: AdsrStep[]): Float32Array { - const encoded = new Float32Array(steps.length * 4); + const ENCODED_STEP_SIZE = 7; + + const encoded = new Float32Array(steps.length * ENCODED_STEP_SIZE); steps.forEach((step, i) => { - encoded[i * 4] = step.x; - encoded[i * 4 + 1] = step.y; - encoded[i * 4 + 2] = { instant: 0, linear: 1, exponential: 2 }[step.ramper.type]; - encoded[i * 4 + 3] = step.ramper.type === 'exponential' ? step.ramper.exponent : 0; + const params: [number, number, number, number] = encodeRampFnParams( + steps[i - 1], + step, + step.ramper + ); + encoded[i * ENCODED_STEP_SIZE] = step.x; + encoded[i * ENCODED_STEP_SIZE + 1] = step.y; + encoded[i * ENCODED_STEP_SIZE + 2] = encodeRampFnType(step.ramper.type); + encoded[i * ENCODED_STEP_SIZE + 3] = params[0]; + encoded[i * ENCODED_STEP_SIZE + 4] = params[1]; + encoded[i * ENCODED_STEP_SIZE + 5] = params[2]; + encoded[i * ENCODED_STEP_SIZE + 6] = params[3]; }); return encoded; }