// Copyright (c) 2021 Weird Constructor // This file is a part of HexoDSP. Released under GPL-3.0-or-later. // See README.md and COPYING for details. #[allow(non_upper_case_globals)] mod node_amp; #[allow(non_upper_case_globals)] mod node_sin; #[allow(non_upper_case_globals)] mod node_out; #[allow(non_upper_case_globals)] mod node_test; #[allow(non_upper_case_globals)] mod node_tseq; #[allow(non_upper_case_globals)] mod node_sampl; #[allow(non_upper_case_globals)] mod node_fbwr_fbrd; #[allow(non_upper_case_globals)] mod node_ad; #[allow(non_upper_case_globals)] mod node_delay; #[allow(non_upper_case_globals)] mod node_allp; #[allow(non_upper_case_globals)] mod node_noise; #[allow(non_upper_case_globals)] mod node_map; #[allow(non_upper_case_globals)] mod node_smap; #[allow(non_upper_case_globals)] mod node_sfilter; #[allow(non_upper_case_globals)] mod node_mix3; #[allow(non_upper_case_globals)] mod node_bosc; pub mod tracker; mod satom; pub mod helpers; use crate::nodes::NodeAudioContext; use crate::nodes::NodeExecContext; use crate::util::AtomicFloat; use std::sync::Arc; pub type LedPhaseVals<'a> = &'a [Arc]; pub use satom::*; use crate::fa_out_mono; use crate::fa_test_s; use crate::fa_amp_neg_att; use crate::fa_tseq_cmode; use crate::fa_sampl_dclick; use crate::fa_sampl_pmode; use crate::fa_sampl_dir; use crate::fa_ad_mult; use crate::fa_delay_mode; use crate::fa_noise_mode; use crate::fa_map_clip; use crate::fa_smap_clip; use crate::fa_smap_mode; use crate::fa_sfilter_type; use crate::fa_bosc_wtype; use node_amp::Amp; use node_sin::Sin; use node_out::Out; use node_test::Test; use node_tseq::TSeq; use node_sampl::Sampl; use node_fbwr_fbrd::FbWr; use node_fbwr_fbrd::FbRd; use node_ad::Ad; use node_delay::Delay; use node_allp::AllP; use node_noise::Noise; use node_map::Map; use node_smap::SMap; use node_sfilter::SFilter; use node_mix3::Mix3; use node_bosc::BOsc; pub const MIDI_MAX_FREQ : f32 = 13289.75; pub const MAX_BLOCK_SIZE : usize = 128; /// A context structure that holds temporary information about the /// currently executed node. /// This structure is created by the [crate::nodes::NodeExecutor] on the fly. pub struct NodeContext<'a> { /// The bitmask that indicates which input ports are used/connected /// to some output. pub in_connected: u64, /// The bitmask that indicates which output ports are used/connected /// to some input. pub out_connected: u64, /// The node parameters, which are usually not accessed directly. pub params: &'a [ProcBuf], } /// This trait is an interface between the graph functions /// and the AtomDataModel of the UI. pub trait GraphAtomData { fn get(&self, param_idx: u32) -> Option; fn get_denorm(&self, param_idx: u32) -> f32; fn get_norm(&self, param_idx: u32) -> f32; fn get_phase(&self) -> f32; fn get_led(&self) -> f32; } pub type GraphFun = Box f32>; /// This trait represents a DspNode for the [crate::matrix::Matrix] pub trait DspNode { /// Number of outputs this node has. fn outputs() -> usize; /// Updates the sample rate for the node. fn set_sample_rate(&mut self, _srate: f32); /// Reset any internal state of the node. fn reset(&mut self); /// The code DSP function. /// /// * `ctx` is the audio context, which informs the node about /// the number of samples to process. It also provides input/output /// ports for the in/out nodes. /// * `ectx` is the execution context, which provides global stuff /// for all nodes to potentially use. For instance it's used /// by the `FbWr` and `FbRd` nodes to share an audio buffer. /// * `atoms` are un-smoothed parameters. they can hold integer settings, /// samples or even strings. /// * `params` are smoother paramters, those who usually have a knob /// associated with them. /// * `inputs` contain all the possible inputs. In contrast to `params` /// these inputs might be overwritten by outputs of other nodes. /// * `outputs` are the output buffers of this node. fn process( &mut self, ctx: &mut T, ectx: &mut NodeExecContext, nctx: &NodeContext, atoms: &[SAtom], inputs: &[ProcBuf], outputs: &mut [ProcBuf], led: LedPhaseVals); /// A function factory for generating a graph for the generic node UI. fn graph_fun() -> Option { None } } /// A processing buffer with the exact right maximum size. #[derive(Clone, Copy)] pub struct ProcBuf(*mut [f32; MAX_BLOCK_SIZE]); impl ProcBuf { pub fn new() -> Self { ProcBuf(Box::into_raw(Box::new([0.0; MAX_BLOCK_SIZE]))) } pub fn null() -> Self { ProcBuf(std::ptr::null_mut()) } } impl crate::monitor::MonitorSource for &ProcBuf { fn copy_to(&self, len: usize, slice: &mut [f32]) { unsafe { slice.copy_from_slice(&(*self.0)[0..len]) } } } unsafe impl Send for ProcBuf {} //unsafe impl Sync for HexoSynthShared {} impl ProcBuf { #[inline] pub fn write(&mut self, idx: usize, v: f32) { unsafe { (*self.0)[idx] = v; } } #[inline] pub fn write_from(&mut self, slice: &[f32]) { unsafe { (*self.0)[0..slice.len()].copy_from_slice(slice); } } #[inline] pub fn read(&self, idx: usize) -> f32 { unsafe { (*self.0)[idx] } } #[inline] pub fn fill(&mut self, v: f32) { unsafe { (*self.0).fill(v); } } #[inline] pub fn is_null(&self) -> bool { self.0.is_null() } pub fn free(&self) { if !self.0.is_null() { drop(unsafe { Box::from_raw(self.0) }); } } } impl std::fmt::Debug for ProcBuf { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { unsafe { write!(f, "ProcBuf(")?; if self.0.is_null() { write!(f, "NULL ")?; } else { for i in 0..MAX_BLOCK_SIZE { write!(f, "{:5.4} ", (*self.0)[i])?; } } write!(f, ")") } } } impl std::fmt::Display for ProcBuf { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { unsafe { write!(f, "ProcBuf(0: {})", (*self.0)[0]) } } } //#[derive(Debug, Clone, Copy)] //enum UIParamDesc { // Knob { width: usize, prec: usize, unit: &'static str }, // Setting { labels: &'static [&'static str], unit: &'static str }, //} #[derive(Debug, Clone, Copy, PartialOrd, PartialEq)] pub enum UIType { Generic, LfoA, EnvA, OscA, } #[derive(Debug, Clone, Copy, PartialOrd, PartialEq)] pub enum UICategory { None, Osc, Mod, NtoM, Signal, CV, IOUtil, } // The following macros define normalize/denormalize functions: macro_rules! n_id { ($x: expr) => { $x } } macro_rules! d_id { ($x: expr) => { $x } } macro_rules! define_lin { ($n_id: ident $d_id: ident $min: expr, $max: expr) => { macro_rules! $n_id { ($x: expr) => { (($x - $min) / ($max - $min) as f32).abs() } } macro_rules! $d_id { ($x: expr) => { $min * (1.0 - $x) + $max * $x } } } } macro_rules! define_exp { ($n_id: ident $d_id: ident $min: expr, $max: expr) => { macro_rules! $n_id { ($x: expr) => { (($x - $min) / ($max - $min) as f32).abs().sqrt() } } macro_rules! $d_id { ($x: expr) => { { let x : f32 = $x * $x; $min * (1.0 - x) + $max * x } } } } } macro_rules! define_exp4 { ($n_id: ident $d_id: ident $min: expr, $max: expr) => { macro_rules! $n_id { ($x: expr) => { (($x - $min) / ($max - $min)).abs().sqrt().sqrt() } } macro_rules! $d_id { ($x: expr) => { { let x : f32 = $x * $x * $x * $x; $min * (1.0 - x) + $max * x } } } } } macro_rules! n_pit { ($x: expr) => { ((($x as f32).max(0.01) / 440.0).log2() / 10.0) } } macro_rules! d_pit { ($x: expr) => { { let note : f32 = ($x as f32) * 10.0; 440.0 * (2.0_f32).powf(note.clamp(-10.0, 10.0)) } } } // The following macros define detune parameter behaviour: // 0.2 => 24.0 // 0.1 => 12.0 // 0.008333333 => 1.0 // 0.000083333 => 0.001 macro_rules! n_det { ($x: expr) => { $x / 120.0 } } macro_rules! d_det { ($x: expr) => { $x * 120.0 } } /// The rounding function for detune UI knobs macro_rules! r_det { ($x: expr, $coarse: expr) => { if $coarse { n_det!((d_det!($x)).round()) } else { n_det!((d_det!($x) * 100.0).round() / 100.0) } } } /// The rounding function for -1 to 1 signal knobs macro_rules! r_s { ($x: expr, $coarse: expr) => { if $coarse { ($x * 10.0).round() / 10.0 } else { ($x * 100.0).round() / 100.0 } } } /// The rounding function for milliseconds knobs macro_rules! r_dc_ms { ($x: expr, $coarse: expr) => { if $coarse { n_declick!((d_declick!($x)).round()) } else { n_declick!((d_declick!($x) * 10.0).round() / 10.0) } } } /// The rounding function for milliseconds knobs macro_rules! r_ems { ($x: expr, $coarse: expr) => { if $coarse { n_env!((d_env!($x)).round()) } else { n_env!((d_env!($x) * 10.0).round() / 10.0) } } } /// The rounding function for milliseconds knobs macro_rules! r_tms { ($x: expr, $coarse: expr) => { if $coarse { if d_time!($x) > 1000.0 { n_time!((d_time!($x) / 100.0).round() * 100.0) } else if d_time!($x) > 100.0 { n_time!((d_time!($x) / 10.0).round() * 10.0) } else { n_time!((d_time!($x)).round()) } } else { n_time!((d_time!($x) * 10.0).round() / 10.0) } } } /// The rounding function for milliseconds knobs macro_rules! r_fms { ($x: expr, $coarse: expr) => { if $coarse { if d_ftme!($x) > 1000.0 { n_ftme!((d_ftme!($x) / 100.0).round() * 100.0) } else if d_ftme!($x) > 100.0 { n_ftme!((d_ftme!($x) / 10.0).round() * 10.0) } else { n_ftme!((d_ftme!($x)).round()) } } else { n_ftme!((d_ftme!($x) * 10.0).round() / 10.0) } } } /// The rounding function for freq knobs (n_pit / d_pit) macro_rules! r_fq { ($x: expr, $coarse: expr) => { if $coarse { ($x * 10.0).round() / 10.0 } else { let p = d_pit!($x); if p < 10.0 { n_pit!((p * 10.0).round() / 10.0) } else if p < 100.0 { n_pit!(p.round()) } else if p < 1000.0 { n_pit!((p / 10.0).round() * 10.0) } else if p < 10000.0 { n_pit!((p / 100.0).round() * 100.0) } else { n_pit!((p / 1000.0).round() * 1000.0) } } } } /// The default steps function: macro_rules! stp_d { () => { (20.0, 100.0) } } /// The UI steps to control parameters with a finer fine control: macro_rules! stp_m { () => { (20.0, 200.0) } } /// The UI steps to control parameters with a very fine fine control: macro_rules! stp_f { () => { (20.0, 1000.0) } } // Rounding function that does nothing macro_rules! r_id { ($x: expr, $coarse: expr) => { $x } } // Default formatting function macro_rules! f_def { ($formatter: expr, $v: expr, $denorm_v: expr) => { write!($formatter, "{:6.3}", $denorm_v) } } macro_rules! f_freq { ($formatter: expr, $v: expr, $denorm_v: expr) => { if ($denorm_v >= 1000.0) { write!($formatter, "{:6.0}Hz", $denorm_v) } else if ($denorm_v >= 100.0) { write!($formatter, "{:6.1}Hz", $denorm_v) } else { write!($formatter, "{:6.2}Hz", $denorm_v) } } } macro_rules! f_ms { ($formatter: expr, $v: expr, $denorm_v: expr) => { if $denorm_v >= 1000.0 { write!($formatter, "{:6.0}ms", $denorm_v) } else if $denorm_v >= 100.0 { write!($formatter, "{:5.1}ms", $denorm_v) } else { write!($formatter, "{:5.2}ms", $denorm_v) } } } macro_rules! f_det { ($formatter: expr, $v: expr, $denorm_v: expr) => { { let sign = if $denorm_v < 0.0 { -1.0 } else { 1.0 }; let semitones = $denorm_v.trunc().abs(); let cents = ($denorm_v.fract() * 100.0).round().abs(); if (cents > 0.1) { write!($formatter, "{:2.0}s {:3.0}c", sign * semitones, cents) } else { write!($formatter, "{:2.0}s", sign * semitones) } } } } // norm-fun denorm-min // denorm-fun denorm-max define_exp!{n_gain d_gain 0.0, 2.0} define_exp!{n_att d_att 0.0, 1.0} define_exp!{n_declick d_declick 0.0, 50.0} define_exp!{n_env d_env 0.0, 1000.0} define_exp!{n_time d_time 0.5, 5000.0} define_exp!{n_ftme d_ftme 0.25, 1000.0} // Special linear gain factor for the Out node, to be able // to reach more exact "1.0". define_lin!{n_ogin d_ogin 0.0, 2.0} // A note about the input-indicies: // // Atoms and Input parameters share the same global ID space // because thats how the client of the Matrix API needs to refer to // them. Beyond the Matrix API the atom data is actually split apart // from the parameters, because they are not smoothed. // // The index there only matters for addressing the atoms in the global atom vector. // // But the actually second index here is for referring to the atom index // relative to the absolute count of atom data a Node has. // It is used by the [Matrix] to get the global ParamId for the atom data // when iterating through the atoms of a Node and initializes the default data // for new nodes. macro_rules! node_list { ($inmacro: ident) => { $inmacro!{ nop => Nop, amp => Amp UIType::Generic UICategory::Signal // node_param_idx // name denorm round format steps norm norm denorm // norm_fun fun fun fun def min max default (0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (1 gain n_gain d_gain r_id f_def stp_d 0.0, 1.0, 1.0) (2 att n_att d_att r_id f_def stp_d 0.0, 1.0, 1.0) {3 0 neg_att setting(1) fa_amp_neg_att 0 1} [0 sig], mix3 => Mix3 UIType::Generic UICategory::NtoM (0 ch1 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (1 ch2 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (2 ch3 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (3 gain1 n_gain d_gain r_id f_def stp_d 0.0, 1.0, 1.0) (4 gain2 n_gain d_gain r_id f_def stp_d 0.0, 1.0, 1.0) (5 gain3 n_gain d_gain r_id f_def stp_d 0.0, 1.0, 1.0) (6 ogain n_gain d_gain r_id f_def stp_d 0.0, 1.0, 1.0) [0 sig], smap => SMap UIType::Generic UICategory::CV (0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (1 min n_id d_id r_s f_def stp_d -1.0, 1.0, -1.0) (2 max n_id d_id r_s f_def stp_d -1.0, 1.0, 1.0) {3 1 mode setting(0) fa_smap_mode 0 3} {4 0 clip setting(0) fa_smap_clip 0 1} [0 sig], map => Map UIType::Generic UICategory::CV (0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (1 atv n_id d_id r_id f_def stp_d -1.0, 1.0, 1.0) (2 offs n_id d_id r_s f_def stp_d -1.0, 1.0, 0.0) (3 imin n_id d_id r_s f_def stp_d -1.0, 1.0, -1.0) (4 imax n_id d_id r_s f_def stp_d -1.0, 1.0, 1.0) (5 min n_id d_id r_s f_def stp_d -1.0, 1.0, -1.0) (6 max n_id d_id r_s f_def stp_d -1.0, 1.0, 1.0) {7 0 clip setting(0) fa_map_clip 0 1} [0 sig], tseq => TSeq UIType::Generic UICategory::CV (0 clock n_id d_id r_id f_def stp_d 0.0, 1.0, 0.0) (1 trig n_id n_id r_id f_def stp_d -1.0, 1.0, 0.0) {2 0 cmode setting(1) fa_tseq_cmode 0 2} [0 trk1] [1 trk2] [2 trk3] [3 trk4] [4 trk5] [5 trk6] [6 gat1] [7 gat2] [8 gat3] [9 gat4] [10 gat5] [11 gat6], sampl => Sampl UIType::Generic UICategory::Osc (0 freq n_pit d_pit r_fq f_def stp_d -1.0, 0.564713133, 440.0) (1 trig n_id n_id r_id f_def stp_d -1.0, 1.0, 0.0) (2 offs n_id n_id r_id f_def stp_d 0.0, 1.0, 0.0) (3 len n_id n_id r_id f_def stp_d 0.0, 1.0, 1.0) (4 dcms n_declick d_declick r_dc_ms f_ms stp_m 0.0, 1.0, 3.0) (5 det n_det d_det r_det f_det stp_f -0.2, 0.2, 0.0) {6 0 sample audio_unloaded("") f_def 0 0} {7 1 pmode setting(0) fa_sampl_pmode 0 1} {8 2 dclick setting(0) fa_sampl_dclick 0 1} {9 3 dir setting(0) fa_sampl_dir 0 1} [0 sig], // node_param_idx // name denorm round format steps norm norm denorm // norm_fun fun fun fun def min max default sin => Sin UIType::Generic UICategory::Osc (0 freq n_pit d_pit r_fq f_freq stp_d -1.0, 0.5647131, 440.0) (1 det n_det d_det r_det f_det stp_f -0.2, 0.2, 0.0) [0 sig], bosc => BOsc UIType::Generic UICategory::Osc (0 freq n_pit d_pit r_fq f_freq stp_d -1.0, 0.5647131, 440.0) (1 det n_det d_det r_det f_det stp_f -0.2, 0.2, 0.0) (2 pw n_id n_id r_id f_def stp_d 0.0, 1.0, 0.5) {3 0 wtype setting(0) fa_bosc_wtype 0 3} [0 sig], out => Out UIType::Generic UICategory::IOUtil (0 ch1 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (1 ch2 n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (2 gain n_ogin d_ogin r_id f_def stp_d 0.0, 1.0, 1.0) // node_param_idx // | atom_idx format fun // | | name constructor| min max // | | | | def|ult_value | / // | | | | | | | | {3 0 mono setting(0) fa_out_mono 0 1}, fbwr => FbWr UIType::Generic UICategory::IOUtil (0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0), fbrd => FbRd UIType::Generic UICategory::IOUtil (0 atv n_id d_id r_id f_def stp_d -1.0, 1.0, 1.0) [0 sig], ad => Ad UIType::Generic UICategory::CV (0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 1.0) (1 trig n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (2 atk n_env d_env r_ems f_ms stp_m 0.0, 1.0, 3.0) (3 dcy n_env d_env r_ems f_ms stp_m 0.0, 1.0, 10.0) (4 ashp n_id d_id r_id f_def stp_d 0.0, 1.0, 0.5) (5 dshp n_id d_id r_id f_def stp_d 0.0, 1.0, 0.5) {6 0 mult setting(0) fa_ad_mult 0 2} [0 sig] [1 eoet], delay => Delay UIType::Generic UICategory::Signal (0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (1 trig n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (2 time n_time d_time r_tms f_ms stp_m 0.0, 1.0, 250.0) (3 fb n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (4 mix n_id d_id r_id f_def stp_d 0.0, 1.0, 0.5) {5 0 mode setting(0) fa_delay_mode 0 1} [0 sig], allp => AllP UIType::Generic UICategory::Signal (0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (1 time n_ftme d_ftme r_fms f_ms stp_m 0.0, 1.0, 25.0) (2 g n_id d_id r_id f_def stp_d -1.0, 1.0, 0.7) [0 sig], noise => Noise UIType::Generic UICategory::Osc (0 atv n_id d_id r_id f_def stp_d -1.0, 1.0, 0.5) (1 offs n_id d_id r_s f_def stp_d -1.0, 1.0, 0.0) {2 0 mode setting(0) fa_noise_mode 0 1} [0 sig], sfilter => SFilter UIType::Generic UICategory::Signal (0 inp n_id d_id r_id f_def stp_d -1.0, 1.0, 0.0) (1 freq n_pit d_pit r_fq f_freq stp_d -1.0, 0.5647131, 1000.0) (2 res n_id d_id r_id f_def stp_d 0.0, 1.0, 0.5) {3 0 ftype setting(8) fa_sfilter_type 0 16} [0 sig], test => Test UIType::Generic UICategory::IOUtil (0 f n_id d_id r_id f_def stp_d 0.0, 1.0, 0.5) {1 0 p param(0.0) fa_test_s 0 10} {2 1 trig param(0.0) fa_test_s 0 0} [0 sig] [1 tsig] [2 out2] [3 out3] [4 out4] [5 outc], } } } impl UICategory { #[allow(unused_assignments)] pub fn get_node_ids(&self, mut skip: usize, mut fun: F) { macro_rules! make_cat_lister { ($s1: ident => $v1: ident, $($str: ident => $variant: ident UIType:: $gui_type: ident UICategory:: $ui_cat: ident $(($in_idx: literal $para: ident $n_fun: ident $d_fun: ident $r_fun: ident $f_fun: ident $steps: ident $min: expr, $max: expr, $def: expr))* $({$in_at_idx: literal $at_idx: literal $atom: ident $at_fun: ident ($at_init: expr) $fa_fun: ident $amin: literal $amax: literal})* $([$out_idx: literal $out: ident])* ,)+ ) => { $(if UICategory::$ui_cat == *self { if skip == 0 { fun(NodeId::$variant(0)); } else { skip -= 1 } })+ } } node_list!{make_cat_lister}; } } #[derive(Debug, Clone, Copy)] pub enum RandNodeSelector { Any, OnlyUseful, } fn rand_node_satisfies_spec(nid: NodeId, sel: RandNodeSelector) -> bool { if let NodeId::Nop = nid { return false; } match sel { RandNodeSelector::Any => true, RandNodeSelector::OnlyUseful => { match nid { NodeId::Nop => false, NodeId::Out(_) => false, NodeId::FbRd(_) => false, NodeId::Test(_) => false, _ => true, } }, } } pub fn get_rand_node_id(count: usize, sel: RandNodeSelector) -> Vec { let mut sm = crate::dsp::helpers::SplitMix64::new_time_seed(); let mut out = vec![]; let mut cnt = 0; while cnt < 100 && out.len() < count { let cur = ALL_NODE_IDS[sm.next_u64() as usize % ALL_NODE_IDS.len()]; if rand_node_satisfies_spec(cur, sel) { out.push(cur); } cnt += 1; } while out.len() < count { out.push(NodeId::Nop); } out } macro_rules! make_node_info_enum { ($s1: ident => $v1: ident, $($str: ident => $variant: ident UIType:: $gui_type: ident UICategory:: $ui_cat: ident $(($in_idx: literal $para: ident $n_fun: ident $d_fun: ident $r_fun: ident $f_fun: ident $steps: ident $min: expr, $max: expr, $def: expr))* $({$in_at_idx: literal $at_idx: literal $atom: ident $at_fun: ident ($at_init: expr) $fa_fun: ident $amin: literal $amax: literal})* $([$out_idx: literal $out: ident])* ,)+ ) => { /// Holds information about the node type that was allocated. /// It stores the names of inputs, output and atoms for uniform /// access. /// /// The [crate::NodeConfigurator] allocates and holds instances /// of this type for access by [NodeId]. /// See also [crate::NodeConfigurator::node_by_id] and /// [crate::Matrix::info_for]. #[derive(Debug, Clone)] pub enum NodeInfo { $v1, $($variant((NodeId, crate::dsp::ni::$variant))),+ } impl NodeInfo { /// Allocates a new [NodeInfo] from a [NodeId]. /// Usually you access [NodeInfo] in the UI thread via /// [crate::NodeConfigurator::node_by_id] /// or [crate::Matrix::info_for]. pub fn from_node_id(nid: NodeId) -> NodeInfo { match nid { NodeId::$v1 => NodeInfo::$v1, $(NodeId::$variant(_) => NodeInfo::$variant((nid, crate::dsp::ni::$variant::new()))),+ } } } /// Refers to an input paramter or atom of a specific /// [Node] referred to by a [NodeId]. /// /// To obtain a [ParamId] you use one of these: /// * [NodeId::atom_param_by_idx] /// * [NodeId::inp_param_by_idx] /// * [NodeId::param_by_idx] /// * [NodeId::inp_param] /// /// To obtain an input and output index for a port use: /// * [NodeId::inp] /// * [NodeId::out] /// ///``` /// use hexodsp::*; /// let freq_param = NodeId::Sin(2).inp_param("freq").unwrap(); /// /// assert!(!freq_param.is_atom()); /// /// // Access the UI min/max and fine/coarse step values of this paramter: /// assert_eq!(freq_param.param_min_max().unwrap(), ((-1.0, 0.5647131), (20.0, 100.0))); /// /// // Access the default value: /// assert_eq!(freq_param.as_atom_def().f(), 0.0); /// /// // Normalize a value (convert frequency to the 0.0 to 1.0 range) /// assert_eq!(freq_param.norm(220.0), -0.1); /// /// // Denormalize a value (convert 0.0 to 1.0 range to frequency) /// assert_eq!(freq_param.denorm(-0.1), 220.0); ///``` #[derive(Debug, Clone, Copy, PartialOrd, PartialEq, Eq, Ord, Hash)] pub struct ParamId { name: &'static str, node: NodeId, idx: u8, } impl ParamId { pub fn none() -> Self { Self { name: "NOP", node: NodeId::Nop, idx: 0, } } pub fn node_id(&self) -> NodeId { self.node } pub fn inp(&self) -> u8 { self.idx } pub fn name(&self) -> &'static str { self.name } /// Returns true if the [ParamId] has been associated with /// the atoms of a Node, and not the paramters. Even if the /// Atom is a `param()`. pub fn is_atom(&self) -> bool { match self.node { NodeId::$v1 => false, $(NodeId::$variant(_) => { match self.idx { $($in_idx => false,)* $($in_at_idx => true,)* _ => false, } }),+ } } pub fn param_steps(&self) -> Option<(f32, f32)> { match self.node { NodeId::$v1 => None, $(NodeId::$variant(_) => { match self.idx { $($in_idx => Some(($min, $max)),)* _ => None, } }),+ } } pub fn param_min_max(&self) -> Option<((f32, f32), (f32, f32))> { match self.node { NodeId::$v1 => None, $(NodeId::$variant(_) => { match self.idx { $($in_idx => Some((($min, $max), $steps!())),)* _ => None, } }),+ } } pub fn format(&self, f: &mut dyn std::io::Write, v: f32) -> Option> { match self.node { NodeId::$v1 => None, $(NodeId::$variant(_) => { match self.idx { $($in_idx => Some($f_fun!(f, v, $d_fun!(v))),)* $($in_at_idx => Some($fa_fun!(f, v, v)),)* _ => None, } }),+ } } pub fn setting_min_max(&self) -> Option<(i64, i64)> { match self.node { NodeId::$v1 => None, $(NodeId::$variant(_) => { match self.idx { $($in_at_idx => Some(($amin, $amax)),)* _ => None, } }),+ } } pub fn as_atom_def(&self) -> SAtom { match self.node { NodeId::$v1 => SAtom::param(0.0), $(NodeId::$variant(_) => { match self.idx { $($in_idx => SAtom::param(crate::dsp::norm_def::$variant::$para()),)* $($in_at_idx => SAtom::$at_fun($at_init),)* _ => SAtom::param(0.0), } }),+ } } pub fn norm_def(&self) -> f32 { match self.node { NodeId::$v1 => 0.0, $(NodeId::$variant(_) => { match self.idx { $($in_idx => crate::dsp::norm_def::$variant::$para(),)* _ => 0.0, } }),+ } } pub fn round(&self, v: f32, coarse: bool) -> f32 { match self.node { NodeId::$v1 => 0.0, $(NodeId::$variant(_) => { match self.idx { $($in_idx => crate::dsp::round::$variant::$para(v, coarse),)* _ => 0.0, } }),+ } } pub fn norm(&self, v: f32) -> f32 { match self.node { NodeId::$v1 => 0.0, $(NodeId::$variant(_) => { match self.idx { $($in_idx => crate::dsp::norm_v::$variant::$para(v),)* _ => 0.0, } }),+ } } pub fn denorm(&self, v: f32) -> f32 { match self.node { NodeId::$v1 => 0.0, $(NodeId::$variant(_) => { match self.idx { $($in_idx => crate::dsp::denorm_v::$variant::$para(v),)* _ => 0.0, } }),+ } } } /// This enum is a collection of all implemented modules (aka nodes) /// that are implemented. The associated `u8` index is the so called /// _instance_ of the corresponding [Node] type. /// /// This is the primary way in this library to refer to a specific node /// in the node graph that is managed by [crate::NodeConfigurator] /// and executed by [crate::NodeExecutor]. /// /// To see how to actually use this, refer to the documentation /// of [crate::Cell], where you will find an example. #[derive(Debug, Clone, Copy, PartialOrd, PartialEq, Eq, Ord, Hash)] pub enum NodeId { $v1, $($variant(u8)),+ } impl std::fmt::Display for NodeId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { NodeId::$v1 => write!(f, "{}", stringify!($v1)), $(NodeId::$variant(i) => write!(f, "{} {}", stringify!($variant), i)),+ } } } impl NodeId { pub fn to_instance(&self, instance: usize) -> NodeId { match self { NodeId::$v1 => NodeId::$v1, $(NodeId::$variant(_) => NodeId::$variant(instance as u8)),+ } } pub fn graph_fun(&self) -> Option { match self { NodeId::$v1 => None, $(NodeId::$variant(_) => crate::dsp::$variant::graph_fun()),+ } } pub fn eq_variant(&self, other: &NodeId) -> bool { match self { NodeId::$v1 => *other == NodeId::$v1, $(NodeId::$variant(_) => if let NodeId::$variant(_) = other { true } else { false }),+ } } pub fn from_node_info(ni: &NodeInfo) -> NodeId { match ni { NodeInfo::$v1 => NodeId::$v1, $(NodeInfo::$variant(_) => NodeId::$variant(0)),+ } } pub fn label(&self) -> &'static str { match self { NodeId::$v1 => stringify!($v1), $(NodeId::$variant(_) => stringify!($variant)),+ } } pub fn name(&self) -> &'static str { match self { NodeId::$v1 => stringify!($s1), $(NodeId::$variant(_) => stringify!($str)),+ } } pub fn from_str(name: &str) -> Self { match name { stringify!($s1) => NodeId::$v1, $(stringify!($str) => NodeId::$variant(0)),+, _ => NodeId::Nop, } } pub fn ui_type(&self) -> UIType { match self { NodeId::$v1 => UIType::Generic, $(NodeId::$variant(_) => UIType::$gui_type),+ } } pub fn ui_category(&self) -> UICategory { match self { NodeId::$v1 => UICategory::None, $(NodeId::$variant(_) => UICategory::$ui_cat),+ } } /// Consistently initialize the phase for oscillators. /// This does some fixed phase offset for the first 3 /// instances, which is usually relied on by the automated /// tests. #[inline] pub fn init_phase(&self) -> f32 { // The first 3 instances get a fixed predefined phase to // not mess up the automated tests so easily. match self.instance() { 0 => 0.0, 1 => 0.05, 2 => 0.1, // 0.25 just to protect against sine cancellation _ => crate::dsp::helpers::rand_01() * 0.25 } } /// This maps the atom index of the node to the absolute /// ParamId in the GUI (and in the [crate::matrix::Matrix]). /// The Atom/Param duality is a bit weird because they share /// the same ID namespace for the UI. But in the actual /// backend, they are split. So the actual splitting happens /// in the [crate::matrix::Matrix]. pub fn atom_param_by_idx(&self, idx: usize) -> Option { match self { NodeId::$v1 => None, $(NodeId::$variant(_) => { match idx { $($at_idx => Some(ParamId { node: *self, name: stringify!($atom), idx: $in_at_idx, }),)* _ => None, } }),+ } } pub fn inp_param_by_idx(&self, idx: usize) -> Option { match self { NodeId::$v1 => None, $(NodeId::$variant(_) => { match idx { $($in_idx => Some(ParamId { node: *self, name: stringify!($para), idx: $in_idx, }),)* _ => None, } }),+ } } pub fn param_by_idx(&self, idx: usize) -> Option { match self { NodeId::$v1 => None, $(NodeId::$variant(_) => { match idx { $($in_idx => Some(ParamId { node: *self, name: stringify!($para), idx: $in_idx, }),)* $($in_at_idx => Some(ParamId { node: *self, name: stringify!($atom), idx: $in_at_idx, }),)* _ => None, } }),+ } } pub fn inp_param(&self, name: &str) -> Option { match self { NodeId::$v1 => None, $(NodeId::$variant(_) => { match name { $(stringify!($para) => Some(ParamId { node: *self, name: stringify!($para), idx: $in_idx, }),)* $(stringify!($atom) => Some(ParamId { node: *self, name: stringify!($atom), idx: $in_at_idx, }),)* _ => None, } }),+ } } pub fn inp(&self, name: &str) -> Option { match self { NodeId::$v1 => None, $(NodeId::$variant(_) => { match name { $(stringify!($para) => Some($in_idx),)* _ => None, } }),+ } } pub fn inp_name_by_idx(&self, idx: u8) -> Option<&'static str> { match self { NodeId::$v1 => None, $(NodeId::$variant(_) => { match idx { $($in_idx => Some(stringify!($para)),)* $($in_at_idx => Some(stringify!($atom)),)* _ => None, } }),+ } } pub fn out_name_by_idx(&self, idx: u8) -> Option<&'static str> { match self { NodeId::$v1 => None, $(NodeId::$variant(_) => { match idx { $($out_idx => Some(stringify!($out)),)* _ => None, } }),+ } } pub fn out(&self, name: &str) -> Option { match self { NodeId::$v1 => None, $(NodeId::$variant(_) => { match name { $(stringify!($out) => Some($out_idx),)* _ => None, } }),+ } } pub fn instance(&self) -> usize { match self { NodeId::$v1 => 0, $(NodeId::$variant(i) => *i as usize),+ } } } pub const ALL_NODE_IDS : &'static [NodeId] = &[$(NodeId::$variant(0)),+]; #[allow(non_snake_case, unused_variables)] pub mod round { $(pub mod $variant { $(#[inline] pub fn $para(x: f32, coarse: bool) -> f32 { $r_fun!(x, coarse) })* })+ } #[allow(non_snake_case)] pub mod denorm_v { $(pub mod $variant { $(#[inline] pub fn $para(x: f32) -> f32 { $d_fun!(x) })* })+ } #[allow(non_snake_case)] pub mod norm_def { $(pub mod $variant { $(#[inline] pub fn $para() -> f32 { $n_fun!($def) })* })+ } #[allow(non_snake_case)] pub mod norm_v { $(pub mod $variant { $(#[inline] pub fn $para(v: f32) -> f32 { $n_fun!(v) })* })+ } #[allow(non_snake_case)] pub mod denorm { $(pub mod $variant { $(#[inline] pub fn $para(buf: &crate::dsp::ProcBuf, frame: usize) -> f32 { $d_fun!(buf.read(frame)) })* })+ } #[allow(non_snake_case)] pub mod denorm_offs { $(pub mod $variant { $(#[inline] pub fn $para(buf: &crate::dsp::ProcBuf, offs_val: f32, frame: usize) -> f32 { $d_fun!(buf.read(frame) + offs_val) })* })+ } #[allow(non_snake_case)] pub mod inp_dir { $(pub mod $variant { $(#[inline] pub fn $para(buf: &crate::dsp::ProcBuf, frame: usize) -> f32 { buf.read(frame) })* })+ } #[allow(non_snake_case)] pub mod inp { $(pub mod $variant { $(#[inline] pub fn $para(inputs: &[crate::dsp::ProcBuf]) -> &crate::dsp::ProcBuf { &inputs[$in_idx] })* })+ } #[allow(non_snake_case)] pub mod at { $(pub mod $variant { $(#[inline] pub fn $atom(atoms: &[crate::dsp::SAtom]) -> &crate::dsp::SAtom { &atoms[$at_idx] })* })+ } #[allow(non_snake_case)] pub mod out_dir { $(pub mod $variant { $(#[inline] pub fn $out(outputs: &mut [crate::dsp::ProcBuf], frame: usize, v: f32) { outputs[$out_idx].write(frame, v); })* })+ } #[allow(non_snake_case)] pub mod out { $(pub mod $variant { $(#[inline] pub fn $out(outputs: &mut [crate::dsp::ProcBuf]) -> &mut crate::dsp::ProcBuf { &mut outputs[$out_idx] })* })+ } #[allow(non_snake_case)] pub mod out_buf { $(pub mod $variant { $(#[inline] pub fn $out(outputs: &mut [crate::dsp::ProcBuf]) -> crate::dsp::ProcBuf { outputs[$out_idx] })* })+ } #[allow(non_snake_case)] pub mod out_idx { $(pub mod $variant { $(#[inline] pub fn $out() -> usize { $out_idx })* })+ } #[allow(non_snake_case)] pub mod is_out_con { $(pub mod $variant { $(#[inline] pub fn $out(nctx: &crate::dsp::NodeContext) -> bool { nctx.out_connected & (1 << $out_idx) != 0x0 })* })+ } #[allow(non_snake_case)] pub mod is_in_con { $(pub mod $variant { $(#[inline] pub fn $para(nctx: &crate::dsp::NodeContext) -> bool { nctx.in_connected & (1 << $in_idx) != 0x0 })* })+ } mod ni { $( #[derive(Debug, Clone)] pub struct $variant { inputs: Vec<&'static str>, atoms: Vec<&'static str>, outputs: Vec<&'static str>, input_help: Vec<&'static str>, output_help: Vec<&'static str>, node_help: &'static str, node_desc: &'static str, } impl $variant { #[allow(unused_mut)] pub fn new() -> Self { let mut input_help = vec![$(crate::dsp::$variant::$para,)*]; $(input_help.push(crate::dsp::$variant::$atom);)* Self { inputs: vec![$(stringify!($para),)*], atoms: vec![$(stringify!($atom),)*], outputs: vec![$(stringify!($out),)*], input_help, output_help: vec![$(crate::dsp::$variant::$out,)*], node_help: crate::dsp::$variant::HELP, node_desc: crate::dsp::$variant::DESC, } } pub fn in_name(&self, in_idx: usize) -> Option<&'static str> { if let Some(s) = self.inputs.get(in_idx) { Some(*s) } else { Some(*(self.atoms.get(in_idx)?)) } } pub fn at_name(&self, in_idx: usize) -> Option<&'static str> { Some(*(self.atoms.get(in_idx)?)) } pub fn out_name(&self, out_idx: usize) -> Option<&'static str> { Some(*(self.outputs.get(out_idx)?)) } pub fn in_help(&self, in_idx: usize) -> Option<&'static str> { Some(*self.input_help.get(in_idx)?) } pub fn out_help(&self, out_idx: usize) -> Option<&'static str> { Some(*(self.output_help.get(out_idx)?)) } pub fn norm(&self, in_idx: usize, x: f32) -> f32 { match in_idx { $($in_idx => crate::dsp::norm_v::$variant::$para(x),)+ _ => x, } } pub fn denorm(&self, in_idx: usize, x: f32) -> f32 { match in_idx { $($in_idx => crate::dsp::denorm_v::$variant::$para(x),)+ _ => x, } } pub fn desc(&self) -> &'static str { self.node_desc } pub fn help(&self) -> &'static str { self.node_help } pub fn out_count(&self) -> usize { self.outputs.len() } pub fn in_count(&self) -> usize { self.inputs.len() } pub fn at_count(&self) -> usize { self.atoms.len() } } )+ } impl NodeInfo { pub fn from(s: &str) -> Self { match s { stringify!($s1) => NodeInfo::$v1, $(stringify!($str) => NodeInfo::$variant( (NodeId::$variant(0), crate::dsp::ni::$variant::new()))),+, _ => NodeInfo::Nop, } } pub fn in_name(&self, idx: usize) -> Option<&'static str> { match self { NodeInfo::$v1 => None, $(NodeInfo::$variant((_, ni)) => ni.in_name(idx)),+ } } pub fn out_name(&self, idx: usize) -> Option<&'static str> { match self { NodeInfo::$v1 => None, $(NodeInfo::$variant((_, ni)) => ni.out_name(idx)),+ } } pub fn in_help(&self, idx: usize) -> Option<&'static str> { match self { NodeInfo::$v1 => None, $(NodeInfo::$variant((_, ni)) => ni.in_help(idx)),+ } } pub fn out_help(&self, idx: usize) -> Option<&'static str> { match self { NodeInfo::$v1 => None, $(NodeInfo::$variant((_, ni)) => ni.out_help(idx)),+ } } pub fn to_id(&self) -> NodeId { match self { NodeInfo::$v1 => NodeId::$v1, $(NodeInfo::$variant((id, _)) => *id),+ } } pub fn at_count(&self) -> usize { match self { NodeInfo::$v1 => 0, $(NodeInfo::$variant(n) => n.1.at_count()),+ } } pub fn in_count(&self) -> usize { match self { NodeInfo::$v1 => 0, $(NodeInfo::$variant(n) => n.1.in_count()),+ } } pub fn default_output(&self) -> Option { if self.out_count() > 0 { Some(0) } else { None } } pub fn default_input(&self) -> Option { if self.in_count() > 0 { Some(0) } else { None } } pub fn out_count(&self) -> usize { match self { NodeInfo::$v1 => 0, $(NodeInfo::$variant(n) => n.1.out_count()),+ } } pub fn desc(&self) -> &'static str { match self { NodeInfo::$v1 => "", $(NodeInfo::$variant(n) => n.1.desc()),+ } } pub fn help(&self) -> &'static str { match self { NodeInfo::$v1 => "", $(NodeInfo::$variant(n) => n.1.help()),+ } } } } } macro_rules! make_node_enum { ($s1: ident => $v1: ident, $($str: ident => $variant: ident UIType:: $gui_type: ident UICategory:: $ui_cat: ident $(($in_idx: literal $para: ident $n_fun: ident $d_fun: ident $r_fun: ident $f_fun: ident $steps: ident $min: expr, $max: expr, $def: expr))* $({$in_at_idx: literal $at_idx: literal $atom: ident $at_fun: ident ($at_init: expr) $fa_fun: ident $amin: literal $amax: literal})* $([$out_idx: literal $out: ident])* ,)+ ) => { /// Represents the actually by the DSP thread ([crate::NodeExecutor]) /// executed [Node]. You don't construct this directly, but let the /// [crate::NodeConfigurator] or more abstract types like /// [crate::Matrix] do this for you. See also [NodeId] for a way to /// refer to these. /// /// The method [Node::process] is called by [crate::NodeExecutor] /// and comes with the overhead of a big `match` statement. /// /// This is the only point of primitive polymorphism inside /// the DSP graph. Dynamic polymorphism via the trait object /// is not done, as I hope the `match` dispatch is a slight bit faster /// because it's more static. /// /// The size of a [Node] is also limited and protected by a test /// in the test suite. The size should not be needlessly increased /// by implementations, in the hope to achieve better /// cache locality. All allocated [Node]s are held in a big /// continuous vector inside the [crate::NodeExecutor]. /// /// The function [node_factory] is responsible for actually creating /// the [Node]. #[derive(Debug, Clone)] pub enum Node { /// An empty node that does nothing. It's a placeholder /// for non allocated nodes. $v1, $($variant { node: $variant },)+ } impl Node { /// Returns the [NodeId] that can be used to refer to this node. /// The node does not store it's instance index, so you have to /// provide it. If the instance is of no meaning for the /// use case pass 0 to `instance`. pub fn to_id(&self, instance: usize) -> NodeId { match self { Node::$v1 => NodeId::$v1, $(Node::$variant { .. } => NodeId::$variant(instance as u8)),+ } } /// Resets any state of this [Node], such as /// any internal state variables or counters or whatever. /// The [Node] should just behave as if it was freshly returned /// from [node_factory]. pub fn reset(&mut self) { match self { Node::$v1 => {}, $(Node::$variant { node } => { node.reset(); }),+ } } /// Sets the current sample rate this [Node] should operate at. pub fn set_sample_rate(&mut self, sample_rate: f32) { match self { Node::$v1 => {}, $(Node::$variant { node } => { node.set_sample_rate(sample_rate); }),+ } } } } } node_list!{make_node_info_enum} node_list!{make_node_enum} pub fn node_factory(node_id: NodeId) -> Option<(Node, NodeInfo)> { macro_rules! make_node_factory_match { ($s1: expr => $v1: ident, $($str: ident => $variant: ident UIType:: $gui_type: ident UICategory:: $ui_cat: ident $(($in_idx: literal $para: ident $n_fun: ident $d_fun: ident $r_fun: ident $f_fun: ident $steps: ident $min: expr, $max: expr, $def: expr))* $({$in_at_idx: literal $at_idx: literal $atom: ident $at_fun: ident ($at_init: expr) $fa_fun: ident $amin: literal $amax: literal})* $([$out_idx: literal $out: ident])* ,)+ ) => { match node_id { $(NodeId::$variant(_) => Some(( Node::$variant { node: $variant::new(&node_id) }, NodeInfo::from_node_id(node_id), )),)+ _ => None, } } } node_list!{make_node_factory_match} } impl Node { /// This function is the heart of any DSP. /// It dispatches this call to the corresponding [Node] implementation. /// /// You don't want to call this directly, but let [crate::NodeConfigurator] and /// [crate::NodeExecutor] do their magic for you. /// /// The slices get passed a [ProcBuf] which is a super _unsafe_ /// buffer, that requires special care and invariants to work safely. /// /// Arguments: /// * `ctx`: The [NodeAudioContext] usually provides global context information /// such as access to the actual buffers of the audio driver or access to /// MIDI events. /// * `atoms`: The [SAtom] settings the user can set in the UI or via /// other means. These are usually non interpolated/smoothed settings. /// * `params`: The smoothed input parameters as set by the user (eg. in the UI). /// There is usually no reason to use these, because any parameter can be /// overridden by assigning an output port to the corresponding input. /// This is provided for the rare case that you still want to use the /// value the user set in the interface, and not the input CV signal. /// * `inputs`: For each `params` parameter there is a input port. /// This slice will contain either a buffer from `params` or some output /// buffer from some other (previously executed) [Node]s output. /// * `outputs`: The output buffers this node will write it's signal/CV /// results to. /// * `led`: Contains the feedback [LedPhaseVals], which are used /// to communicate the current value (set once per `process()` call, usually at the end) /// of the most important internal signal. Usually stuff like the output /// value of an oscillator, envelope or the current sequencer output /// value. It also provides a second value, a so called _phase_ /// which is usually used by graphical frontends to determine /// the phase of the oscillator, envelope or the sequencer to /// display some kind of position indicator. #[inline] pub fn process( &mut self, ctx: &mut T, ectx: &mut NodeExecContext, nctx: &NodeContext, atoms: &[SAtom], inputs: &[ProcBuf], outputs: &mut [ProcBuf], led: LedPhaseVals) { macro_rules! make_node_process { ($s1: ident => $v1: ident, $($str: ident => $variant: ident UIType:: $gui_type: ident UICategory:: $ui_cat: ident $(($in_idx: literal $para: ident $n_fun: ident $d_fun: ident $r_fun: ident $f_fun: ident $steps: ident $min: expr, $max: expr, $def: expr))* $({$in_at_idx: literal $at_idx: literal $atom: ident $at_fun: ident ($at_init: expr) $fa_fun: ident $amin: literal $amax: literal})* $([$out_idx: literal $out: ident])* ,)+ ) => { match self { Node::$v1 => {}, $(Node::$variant { node } => node.process(ctx, ectx, nctx, atoms, inputs, outputs, led),)+ } } } node_list!{make_node_process} } } #[cfg(test)] mod tests { use super::*; #[test] fn check_node_size_staying_small() { assert_eq!(std::mem::size_of::(), 48); assert_eq!(std::mem::size_of::(), 2); assert_eq!(std::mem::size_of::(), 24); } #[test] fn check_pitch() { assert_eq!(d_pit!(-0.2).round() as i32, 110_i32); assert_eq!((n_pit!(110.0) * 100.0).round() as i32, -20_i32); assert_eq!(d_pit!(0.0).round() as i32, 440_i32); assert_eq!((n_pit!(440.0) * 100.0).round() as i32, 0_i32); assert_eq!(d_pit!(0.3).round() as i32, 3520_i32); assert_eq!((n_pit!(3520.0) * 100.0).round() as i32, 30_i32); for i in 1..999 { let x = (((i as f32) / 1000.0) - 0.5) * 2.0; let r = d_pit!(x); println!("x={:8.5} => {:8.5}", x, r); assert_eq!( (n_pit!(r) * 10000.0).round() as i32, (x * 10000.0).round() as i32); } } }