diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index 860ce49..6b0d7c0 100644 --- a/src/dsp/helpers.rs +++ b/src/dsp/helpers.rs @@ -2,6 +2,8 @@ // This is a part of HexoDSP. Released under (A)GPLv3 or any later. // See README.md and COPYING for details. +use std::cell::RefCell; + /// Logarithmic table size of the table in [fast_cos] / [fast_sin]. static FAST_COS_TAB_LOG2_SIZE : usize = 9; /// Table size of the table in [fast_cos] / [fast_sin]. @@ -168,6 +170,15 @@ impl Rng { } } +thread_local! { + static GLOBAL_RNG: RefCell = RefCell::new(Rng::new()); +} + +#[inline] +pub fn rand_01() -> f32 { + GLOBAL_RNG.with(|r| r.borrow_mut().next()) +} + // Copyright 2018 Developers of the Rand project. // // Licensed under the Apache License, Version 2.0 f64 { + if t < dt { + let t = t / dt; + 2. * t - (t * t) - 1. + } else if t > (1.0 - dt) { + let t = (t - 1.0) / dt; + (t * t) + 2. * t + 1. + + } else { + 0. + } +} + +fn poly_blep(t: f32, dt: f32) -> f32 { + if t < dt { + let t = t / dt; + 2. * t - (t * t) - 1. + + } else if t > (1.0 - dt) { + let t = (t - 1.0) / dt; + (t * t) + 2. * t + 1. + + } else { + 0. + } +} + +#[derive(Debug, Clone)] +pub struct PolyBlepOscillator { + phase: f32, + init_phase: f32, + last_output: f32, +} + +impl PolyBlepOscillator { + pub fn new(init_phase: f32) -> Self { + Self { + phase: 0.0, + last_output: 0.0, + init_phase, + } + } + + #[inline] + pub fn reset(&mut self) { + self.phase = self.init_phase; + self.last_output = 0.0; + } + +// #[inline] +// pub fn next_tri(&mut self) -> f32 { +// let value = -1.0 + (2.0 * self.phase); +// 2.0 * (value.abs() - 0.5) +// } + + #[inline] + pub fn next_sin(&mut self, freq: f32, israte: f32) -> f32 { + let phase_inc = freq * israte; + + let s = fast_sin(self.phase * 2.0 * std::f32::consts::PI); + + self.phase += phase_inc; + self.phase = self.phase.fract(); + + s as f32 + } + + #[inline] + pub fn next_tri(&mut self, freq: f32, israte: f32) -> f32 { + let phase_inc = freq * israte; + + let mut s = + if self.phase < 0.5 { 1.0 } + else { -1.0 }; + + s += poly_blep(self.phase, phase_inc); + s -= poly_blep((self.phase + 0.5).fract(), phase_inc); + + // leaky integrator: y[n] = A * x[n] + (1 - A) * y[n-1] + s = phase_inc * s + (1.0 - phase_inc) * self.last_output; + self.last_output = s; + + self.phase += phase_inc; + self.phase = self.phase.fract(); + + // the signal is a bit too weak, we need to amplify it + // or else the volume diff between the different waveforms + // is too big: + s * 4.0 + } + + #[inline] + pub fn next_saw(&mut self, freq: f32, israte: f32) -> f32 { + let phase_inc = freq * israte; + + let mut s = (2.0 * self.phase) - 1.0; + s -= poly_blep(self.phase, phase_inc); + + self.phase += phase_inc; + self.phase = self.phase.fract(); + + s + } + + #[inline] + pub fn next_pulse(&mut self, freq: f32, israte: f32, pw: f32) -> f32 { + let phase_inc = freq * israte; + + let pw = (0.1 * pw) + ((1.0 - pw) * 0.5); // some scaling + let dc_compensation = (0.5 - pw) * 2.0; + + let mut s = + if self.phase < pw { 1.0 } + else { -1.0 }; + + s += poly_blep(self.phase, phase_inc); + s -= poly_blep((self.phase + (1.0 - pw)).fract(), + phase_inc); + + s += dc_compensation; + + self.phase += phase_inc; + self.phase = self.phase.fract(); + + s + } +} + +//pub struct UnisonBlep { +// oscs: Vec, +//// dc_block: crate::filter::DCBlockFilter, +//} +// +//impl UnisonBlep { +// pub fn new(max_unison: usize) -> Self { +// let mut oscs = vec![]; +// let mut rng = RandGen::new(); +// +// let dis_init_phase = 0.05; +// for i in 0..(max_unison + 1) { +// // randomize phases so we fatten the unison, get +// // less DC and not an amplified signal until the +// // detune desyncs the waves. +// // But no random phase for first, so we reduce the click +// let init_phase = +// if i == 0 { 0.0 } else { rng.next_open01() }; +// oscs.push(PolyBlepOscillator::new(init_phase)); +// } +// +// Self { +// oscs, +//// dc_block: crate::filter::DCBlockFilter::new(), +// } +// } +// +// pub fn set_sample_rate(&mut self, srate: f32) { +//// self.dc_block.set_sample_rate(srate); +// for o in self.oscs.iter_mut() { +// o.set_sample_rate(srate); +// } +// } +// +// pub fn reset(&mut self) { +//// self.dc_block.reset(); +// for o in self.oscs.iter_mut() { +// o.reset(); +// } +// } +// +// pub fn next(&mut self, params: &P) -> f32 { +// let unison = +// (params.unison().floor() as usize) +// .min(self.oscs.len() - 1); +// let detune = params.detune() as f64; +// +// let mix = (1.0 / ((unison + 1) as f32)).sqrt(); +// +// let mut s = mix * self.oscs[0].next(params, 0.0); +// +// for u in 0..unison { +// let detune_factor = +// detune * (((u / 2) + 1) as f64 +// * if (u % 2) == 0 { 1.0 } else { -1.0 }); +// s += mix * self.oscs[u + 1].next(params, detune_factor * 0.01); +// } +// +//// self.dc_block.next(s) +// s +// } +//} #[cfg(test)] mod tests { diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 5441e9f..c7c59eb 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -32,6 +32,8 @@ mod node_smap; mod node_sfilter; #[allow(non_upper_case_globals)] mod node_mix3; +#[allow(non_upper_case_globals)] +mod node_bosc; pub mod tracker; mod satom; @@ -61,6 +63,7 @@ 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; @@ -78,6 +81,7 @@ 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; @@ -548,6 +552,12 @@ macro_rules! node_list { (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) diff --git a/src/dsp/node_bosc.rs b/src/dsp/node_bosc.rs new file mode 100644 index 0000000..1589ef3 --- /dev/null +++ b/src/dsp/node_bosc.rs @@ -0,0 +1,153 @@ +// Copyright (c) 2021 Weird Constructor +// This is a part of HexoDSP. Released under (A)GPLv3 or any later. +// See README.md and COPYING for details. + +use crate::nodes::{NodeAudioContext, NodeExecContext}; +use crate::dsp::{NodeId, SAtom, ProcBuf, DspNode, LedPhaseVals, NodeContext}; +use crate::dsp::helpers::{rand_01, PolyBlepOscillator}; + +#[macro_export] +macro_rules! fa_bosc_wtype { ($formatter: expr, $v: expr, $denorm_v: expr) => { { + let s = + match ($v.round() as usize) { + 0 => "Sin", + 1 => "Tri", + 2 => "Saw", + 3 => "Pulse", + _ => "?", + }; + write!($formatter, "{}", s) +} } } + +/// A simple amplifier +#[derive(Debug, Clone)] +pub struct BOsc { + osc: PolyBlepOscillator, + israte: f32, +} + +impl BOsc { + pub fn new(nid: &NodeId) -> Self { + let init_phase = + if nid.instance() > 0 { + // 0.5 just to protect against sine cancellation + rand_01() * 0.5 + } else { + 0.0 + }; + + Self { + osc: PolyBlepOscillator::new(init_phase), + israte: 1.0 / 44100.0, + } + } + + pub const freq : &'static str = + "BOsc freq\nBase frequency of the oscillator.\n\nRange: (-1..1)\n"; + pub const det : &'static str = + "BOsc det\nDetune the oscillator in semitones and cents. \ + the input of this value is rounded to semitones on coarse input. \ + Fine input lets you detune in cents (rounded). \ + A signal sent to this port is not rounded.\n\ + Note: The signal input allows detune +-10 octaves.\ + \nRange: (Knob -0.2 .. 0.2) / (Signal -1.0 .. 1.0)\n"; + pub const pw : &'static str = + "BOsc pw\n\nRange: (0..1)\n"; + pub const wtype : &'static str = + "BOsc wtype\nWaveform type\nAvailable waveforms:\n\ + Sin - Sine Waveform\n\ + Tri - Triangle Waveform\n\ + Saw - Sawtooth Waveform\n\ + Pulse - Pulse Waveform with configurable pulse width"; + pub const sig : &'static str = + "BOsc sig\nOscillator output\nRange: (-1..1)\n"; + pub const DESC : &'static str = +r#"Basic Oscillator + +A very basic oscillator with a sine, triangle, pulse and sawtooth waveform. +"#; + pub const HELP : &'static str = +r#"BOsc - Basic Waveform Oscillator + +A very basic oscillator with a sine, triangle, pulse and sawtooth waveform. +The pulse width `pw` parameter only has an effect for the `Pulse` waveform. +"#; + +} + +impl DspNode for BOsc { + fn outputs() -> usize { 1 } + + fn set_sample_rate(&mut self, srate: f32) { + self.israte = 1.0 / srate; + } + + fn reset(&mut self) { + self.osc.reset(); + } + + #[inline] + fn process( + &mut self, ctx: &mut T, _ectx: &mut NodeExecContext, + _nctx: &NodeContext, + atoms: &[SAtom], inputs: &[ProcBuf], + outputs: &mut [ProcBuf], ctx_vals: LedPhaseVals) + { + use crate::dsp::{out, inp, denorm, denorm_offs, at}; + + let freq = inp::BOsc::freq(inputs); + let det = inp::BOsc::det(inputs); + let pw = inp::BOsc::pw(inputs); + let out = out::BOsc::sig(outputs); + + let wtype = at::BOsc::wtype(atoms); + + let israte = self.israte; + + match wtype.i() { + 0 => { // sin + for frame in 0..ctx.nframes() { + let freq = + denorm_offs::BOsc::freq( + freq, det.read(frame), frame); + out.write( + frame, + self.osc.next_sin(freq, israte)); + } + }, + 1 => { // tri + for frame in 0..ctx.nframes() { + let freq = + denorm_offs::BOsc::freq( + freq, det.read(frame), frame); + out.write( + frame, + self.osc.next_tri(freq, israte)); + } + }, + 2 => { // saw + for frame in 0..ctx.nframes() { + let freq = + denorm_offs::BOsc::freq( + freq, det.read(frame), frame); + out.write( + frame, + self.osc.next_saw(freq, israte)); + } + }, + 3 | _ => { // pulse + for frame in 0..ctx.nframes() { + let freq = + denorm_offs::BOsc::freq( + freq, det.read(frame), frame); + let pw = denorm::BOsc::pw(pw, frame); + out.write( + frame, + self.osc.next_pulse(freq, israte, pw)); + } + } + } + + ctx_vals[0].set(out.read(ctx.nframes() - 1)); + } +}