initial formant synth

This commit is contained in:
Dimas Leenman 2022-07-19 11:44:54 +02:00
parent 5216a88cc8
commit cbf00e8e39
6 changed files with 145 additions and 18 deletions

View file

@ -2182,13 +2182,13 @@ impl<F: Flt> TriSawLFO<F> {
};
if s.abs() > f(1.0) {
println!(
"RECALC TRISAW: rev={}, rise={}, fall={}, phase={}",
self.rev.to_f64().unwrap_or(0.0),
self.rise_r.to_f64().unwrap_or(0.0) as f32,
self.fall_r.to_f64().unwrap_or(0.0) as f32,
self.phase.to_f64().unwrap_or(0.0) as f32
);
println!(
"RECALC TRISAW: rev={}, rise={}, fall={}, phase={}",
self.rev.to_f64().unwrap_or(0.0),
self.rise_r.to_f64().unwrap_or(0.0) as f32,
self.fall_r.to_f64().unwrap_or(0.0) as f32,
self.phase.to_f64().unwrap_or(0.0) as f32
);
}
self.phase = self.phase + self.freq * self.israte;

View file

@ -23,6 +23,8 @@ mod node_delay;
#[allow(non_upper_case_globals)]
mod node_fbwr_fbrd;
#[allow(non_upper_case_globals)]
mod node_formant;
#[allow(non_upper_case_globals)]
mod node_map;
#[allow(non_upper_case_globals)]
mod node_mix3;
@ -108,6 +110,7 @@ use node_cqnt::CQnt;
use node_delay::Delay;
use node_fbwr_fbrd::FbRd;
use node_fbwr_fbrd::FbWr;
use node_formant::Formant;
use node_map::Map;
use node_mix3::Mix3;
use node_mux9::Mux9;
@ -1001,6 +1004,8 @@ macro_rules! node_list {
(14 mix n_id d_id r_id f_def stp_d 0.0, 1.0, 0.5)
[0 sig_l]
[1 sig_r],
formant => Formant UIType::Generic UICategory::Signal
(0),
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) knob fa_test_s 0 10}

127
src/dsp/node_formant.rs Normal file
View file

@ -0,0 +1,127 @@
use super::helpers::{sqrt4_to_pow4, TrigSignal, Trigger};
use crate::dsp::{
DspNode, GraphAtomData, GraphFun, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom,
};
use crate::nodes::{NodeAudioContext, NodeExecContext};
/// A simple amplifier
#[derive(Debug, Clone)]
pub struct Formant {
inv_sample_rate: f32,
phase: f32,
}
impl Formant {
pub fn new(_nid: &NodeId) -> Self {
Self { inv_sample_rate: 1.0 / 44100.0, phase: 0.0 }
}
pub const inp: &'static str =
"Ad inp\nSignal input. If you don't connect this, and set this to 1.0 \
this will act as envelope signal generator. But you can also just \
route a signal directly through this of course.\nRange: (-1..1)\n";
pub const trig: &'static str =
"Ad trig\nTrigger input that starts the attack phase.\nRange: (0..1)\n";
pub const atk: &'static str =
"Ad atk\nAttack time of the envelope. You can extend the maximum \
range of this with the 'mult' setting.\nRange: (0..1)\n";
pub const dcy: &'static str = "Ad atk\nDecay time of the envelope. You can extend the maximum \
range of this with the 'mult' setting.\nRange: (0..1)\n";
pub const ashp: &'static str = "Ad ashp\nAttack shape. This allows you to change the shape \
of the attack stage from a logarithmic, to a linear and to an \
exponential shape.\nRange: (0..1)\n";
pub const dshp: &'static str = "Ad dshp\nDecay shape. This allows you to change the shape \
of the decay stage from a logarithmic, to a linear and to an \
exponential shape.\nRange: (0..1)\n";
pub const mult: &'static str = "Ad mult\nAttack and Decay time range multiplier. \
This will extend the maximum range of the 'atk' and 'dcy' parameters.";
pub const sig: &'static str =
"Ad sig\nEnvelope signal output. If a signal is sent to the 'inp' port, \
you will receive an attenuated signal here. If you set 'inp' to a \
fixed value (for instance 1.0), this will output an envelope signal \
in the range 0.0 to 'inp' (1.0).\nRange: (-1..1)\n";
pub const eoet: &'static str =
"Ad eoet\nEnd of envelope trigger. This output sends a trigger once \
the end of the decay stage has been reached.\nRange: (0..1)";
pub const DESC: &'static str = r#"A direct formant synthesizer
This generates a single formant from a given frequency, formant frequency, as well as attack and decay frequencies.
The attack and decay frequencies both control the bandwidth of the formant, decay the peak of the bandwidth, attack peak.
"#;
pub const HELP: &'static str = r#"Formant - Single formant synthesizer
This is a formant synthesizer that directly generates the audio, no filters needed.
"#;
}
impl DspNode for Formant {
fn outputs() -> usize {
1
}
fn set_sample_rate(&mut self, srate: f32) {
self.inv_sample_rate = 1.0 / srate;
}
fn reset(&mut self) {
self.phase = 0.0;
}
#[inline]
fn process<T: NodeAudioContext>(
&mut self,
ctx: &mut T,
_ectx: &mut NodeExecContext,
_nctx: &NodeContext,
atoms: &[SAtom],
inputs: &[ProcBuf],
outputs: &mut [ProcBuf],
ctx_vals: LedPhaseVals,
) {
use crate::dsp::{at, denorm, inp, out};
let base_freq = inp::Formant::freq(inputs);
let formant_freq = inp::Formant::fmt(inputs);
let attack_freq = inp::Formant::atk(inputs);
let decay_freq = inp::Formant::dcy(inputs);
let out = out::Formant::sig(outputs);
for frame in 0..ctx.nframes() {
// where the two decays meet
let carrier_center = decay_freq / (attack_freq + decay_freq);
// where they meet in amplitude
let carrier_lowest_amplitude =
(-std::f32::consts::TAU * base_freq * carrier_center * decay_freq).exp();
// turn it into a triangle wave
let carrier_attack = (1.0 - self.phase) / carrier_center;
let carrier_decay = self.phase / (1.0 - carrier_center);
// actual triangle wave
let carrier_base = 1.0 - carrier_attack.min(carrier_decay);
// smoothstep
let carrier =
carrier_base * carrier_base * (3.0 - 2.0 * carrier_base) * carrier_lowest_amplitude
+ (1.0 - carrier_lowest_amplitude);
// multiple of the frequency the modulators are at
let multiple = formant_freq / base_freq;
// round them to the closest integer of the formant freq
let freq_a = multiple.floor();
let freq_b = freq_a + 1.0;
// and how much to lerp between them
let blend = multiple.fract();
// get the true modulator
let modulator = (1.0 - blend) * (std::f32::consts::TAU * self.phase * freq_a).cos()
+ blend * (std::f32::consts::TAU * self.phase * freq_b).cos();
// entire wave
let wave = carrier * modulator;
out.write(frame, wave);
}
}
}

View file

@ -3,8 +3,8 @@
// See README.md and COPYING for details.
use super::{
FeedbackFilter, GraphMessage, NodeOp, NodeProg, MAX_ALLOCATED_NODES,
MAX_AVAIL_TRACKERS, MAX_INPUTS, UNUSED_MONITOR_IDX,
FeedbackFilter, GraphMessage, NodeOp, NodeProg, MAX_ALLOCATED_NODES, MAX_AVAIL_TRACKERS,
MAX_INPUTS, UNUSED_MONITOR_IDX,
};
use crate::dsp::tracker::{PatternData, Tracker};
use crate::dsp::{node_factory, Node, NodeId, NodeInfo, ParamId, SAtom};
@ -245,12 +245,7 @@ impl SharedNodeConf {
}
(
Self {
node_ctx_values,
graph_update_prod: rb_graph_prod,
monitor,
drop_thread,
},
Self { node_ctx_values, graph_update_prod: rb_graph_prod, monitor, drop_thread },
SharedNodeExec {
node_ctx_values: exec_node_ctx_vals,
graph_update_con: rb_graph_con,

View file

@ -3,8 +3,8 @@
// See README.md and COPYING for details.
use super::{
DropMsg, GraphMessage, NodeProg, FB_DELAY_TIME_US, MAX_ALLOCATED_NODES,
MAX_FB_DELAY_SIZE, MAX_SMOOTHERS, UNUSED_MONITOR_IDX,
DropMsg, GraphMessage, NodeProg, FB_DELAY_TIME_US, MAX_ALLOCATED_NODES, MAX_FB_DELAY_SIZE,
MAX_SMOOTHERS, UNUSED_MONITOR_IDX,
};
use crate::dsp::{Node, NodeContext, NodeId, MAX_BLOCK_SIZE};
use crate::monitor::{MonitorBackend, MON_SIG_CNT};

View file

@ -119,7 +119,7 @@ fn check_node_delay_2() {
vec![
// 10ms smoothing time for "inp"
0.001133, // 30ms delaytime just mixing the 0.5:
0.5, 0.5, 0.5, // the delayed smoothing ramp (10ms):
0.5, 0.5, 0.5, // the delayed smoothing ramp (10ms):
0.951113, // the delay + input signal:
1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0
]