diff --git a/src/dsp/helpers.rs b/src/dsp/helpers.rs index abe4a59..560bc52 100644 --- a/src/dsp/helpers.rs +++ b/src/dsp/helpers.rs @@ -453,6 +453,11 @@ impl TriggerPhaseClock { self.clock_samples = 0; } + #[inline] + pub fn sync(&mut self) { + self.clock_phase = 0.0; + } + #[inline] pub fn next_phase(&mut self, clock_limit: f64, trigger_in: f32) -> f64 { if self.prev_trigger { diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 6e8ac74..256e410 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -405,7 +405,8 @@ macro_rules! node_list { [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 0 cmode setting(1) fa_tseq_cmode 0 2} + (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] diff --git a/src/dsp/node_tseq.rs b/src/dsp/node_tseq.rs index f4118b3..d400b18 100644 --- a/src/dsp/node_tseq.rs +++ b/src/dsp/node_tseq.rs @@ -3,7 +3,7 @@ // See README.md and COPYING for details. use crate::nodes::{NodeAudioContext, NodeExecContext}; -use crate::dsp::helpers::TriggerPhaseClock; +use crate::dsp::helpers::{TriggerPhaseClock, Trigger}; use crate::dsp::{NodeId, SAtom, ProcBuf, DspNode, LedPhaseVals}; use crate::dsp::tracker::TrackerBackend; @@ -22,12 +22,18 @@ macro_rules! fa_tseq_cmode { ($formatter: expr, $v: expr, $denorm_v: expr) => { } } } +#[derive(Debug)] +pub struct TSeqTime { + clock: TriggerPhaseClock, + trigger: Trigger, +} + /// A tracker based sequencer #[derive(Debug)] pub struct TSeq { backend: Option>, - clock: TriggerPhaseClock, srate: f64, + time: Box, } impl Clone for TSeq { @@ -39,7 +45,10 @@ impl TSeq { Self { backend: None, srate: 48000.0, - clock: TriggerPhaseClock::new(), + time: Box::new(TSeqTime { + clock: TriggerPhaseClock::new(), + trigger: Trigger::new(), + }), } } @@ -49,6 +58,8 @@ impl TSeq { pub const clock : &'static str = "TSeq clock\nClock input\nRange: (0..1)\n"; + pub const trig : &'static str = + "TSeq trig\nSynchronization trigger which restarts the sequence.\nRange: (-1..1)\n"; pub const cmode : &'static str = "TSeq cmode\n'clock' input signal mode:\n\ - RowT: Trigger = advance row\n\ @@ -89,12 +100,25 @@ impl TSeq { pub const HELP : &'static str = r#"Tracker (based) Sequencer +This sequencer gets it's speed from the clock source. The 'clock' +signal can be interpreted in different modes. But if you want to +run multiple sequencers in parallel, you want to synchronize them. +For this you can use the 'trig' input, it resets the played row to +the beginning of the sequence every time a trigger is received. + +Alternatively you can run the sequencer clock using the phase mode. +With that the phase (0..1) signal on the 'clock' input determines the +exact play head position in the pattern. With this you just need to +synchronize the phase generators for different sequencers. + +For an idea how to chain multiple tracker sequencers, see the next page. + This tracker provides 6 columns that each can have one of the following types: - Note column: for specifying pitches. - Step column: for specifying non interpolated CV signals. -- Value column: for specifying linearily interpolated CV signals. +- Value column: for specifying linearly interpolated CV signals. - Gate column: for specifying gates, with probability and ratcheting. Step, value and gate cells can be set to 4096 (0xFFF) different values @@ -102,6 +126,10 @@ or contain nothing at all. For step and value columns these values are mapped to the 0.0-1.0 CV signal range, with 0xFFF being 1.0 and 0x000 being 0.0. +On the next page you can read about the gate cells and the gate outputs. +---page--- +Gate Input and Output + The gate cells are differently coded: - 0x00F: The least significant nibble controls the gate length. @@ -109,7 +137,7 @@ The gate cells are differently coded: - 0x0F0: The second nibble controls ratcheting, with 0x0F0 being one gate per row, and 0x000 being 16 gates per row. - 0xF00: The most significant nibble controls probability of the - whole gate cell. With 0xF00 meaing the gate will always be + whole gate cell. With 0xF00 meaning the gate will always be triggered, and 0x000 means that the gate is only triggered with 6% probability. 50% is 0x070. @@ -118,7 +146,7 @@ column type: - Step gat1-gat6: Like note columns, this will output a 1.0 for the whole row if a step value is set. With two step values directly - following each other no 0.0 will be emitted inbetween + following each other no 0.0 will be emitted in between the rows. This means if you want to drive an envelope with release phase with this signal, you need to make space for the release phase. @@ -127,6 +155,12 @@ column type: - Value gat1-gat6: Outputs a 1.0 value for the duration of the last row. You can use this to trigger other things once the sequence has been played. + +Tip: + If you want to use the end of a tracker sequence as trigger for + something else, eg. switching to a different 'tseq' and restart + it using it's 'trig' input, you will need to use the gate output + of a value column and invert it. "#; } @@ -139,7 +173,8 @@ impl DspNode for TSeq { fn reset(&mut self) { self.backend = None; - self.clock.reset(); + self.time.clock.reset(); + self.time.trigger.reset(); } #[inline] @@ -148,8 +183,9 @@ impl DspNode for TSeq { atoms: &[SAtom], _params: &[ProcBuf], inputs: &[ProcBuf], outputs: &mut [ProcBuf], ctx_vals: LedPhaseVals) { - use crate::dsp::{out, inp, at}; + use crate::dsp::{out, inp, at, denorm}; let clock = inp::TSeq::clock(inputs); + let trig = inp::TSeq::trig(inputs); let cmode = at::TSeq::cmode(atoms); let backend = @@ -163,13 +199,21 @@ impl DspNode for TSeq { [0.0; MAX_BLOCK_SIZE]; let cmode = cmode.i(); - let plen = backend.pattern_len() as f64; + let plen = backend.pattern_len().max(1) as f64; + + let time = &mut self.time; for frame in 0..ctx.nframes() { + if time.trigger.check_trigger( + denorm::TSeq::trig(trig, frame)) + { + time.clock.sync(); + } + let phase = match cmode { - 0 => self.clock.next_phase(plen, clock.read(frame)) / plen, - 1 => self.clock.next_phase(1.0, clock.read(frame)), + 0 => time.clock.next_phase(plen, clock.read(frame)) / plen, + 1 => time.clock.next_phase(1.0, clock.read(frame)), 2 | _ => (clock.read(frame).abs() as f64).fract(), };