diff --git a/src/dsp/mod.rs b/src/dsp/mod.rs index 5c9291b..0bac319 100644 --- a/src/dsp/mod.rs +++ b/src/dsp/mod.rs @@ -511,10 +511,10 @@ mod node_formfm; #[allow(non_upper_case_globals)] mod node_map; #[allow(non_upper_case_globals)] -mod node_midip; -#[allow(non_upper_case_globals)] mod node_midicc; #[allow(non_upper_case_globals)] +mod node_midip; +#[allow(non_upper_case_globals)] mod node_mix3; #[allow(non_upper_case_globals)] mod node_mux9; @@ -571,9 +571,9 @@ use crate::fa_cqnt_omax; use crate::fa_cqnt_omin; use crate::fa_delay_mode; use crate::fa_map_clip; +use crate::fa_midicc_cc; use crate::fa_midip_chan; use crate::fa_midip_gmode; -use crate::fa_midicc_cc; use crate::fa_mux9_in_cnt; use crate::fa_noise_mode; use crate::fa_out_mono; @@ -604,8 +604,8 @@ use node_fbwr_fbrd::FbRd; use node_fbwr_fbrd::FbWr; use node_formfm::FormFM; use node_map::Map; -use node_midip::MidiP; use node_midicc::MidiCC; +use node_midip::MidiP; use node_mix3::Mix3; use node_mux9::Mux9; use node_noise::Noise; diff --git a/src/dsp/node_midicc.rs b/src/dsp/node_midicc.rs index 6596577..e23bf48 100644 --- a/src/dsp/node_midicc.rs +++ b/src/dsp/node_midicc.rs @@ -2,9 +2,7 @@ // This file is a part of HexoDSP. Released under GPL-3.0-or-later. // See README.md and COPYING for details. -use crate::dsp::{ - at, denorm, inp, out_idx, DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom, -}; +use crate::dsp::{at, out_idx, DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom}; use crate::nodes::{HxMidiEvent, MidiEventPointer, NodeAudioContext, NodeExecContext}; #[macro_export] @@ -92,7 +90,6 @@ impl DspNode for MidiCC { while let Some(ev) = ptr.next_at(frame) { match ev { HxMidiEvent::CC { channel, cc, value } => { - println!("CC: {} {} {}", channel, cc, value); if channel != midicc_channel { continue; } diff --git a/src/dsp/node_midip.rs b/src/dsp/node_midip.rs index fd6cb31..432d05c 100644 --- a/src/dsp/node_midip.rs +++ b/src/dsp/node_midip.rs @@ -2,9 +2,7 @@ // This file is a part of HexoDSP. Released under GPL-3.0-or-later. // See README.md and COPYING for details. -use crate::dsp::{ - at, denorm, inp, out_idx, DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom, -}; +use crate::dsp::{at, inp, out_idx, DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom}; use crate::nodes::{HxMidiEvent, MidiEventPointer, NodeAudioContext, NodeExecContext}; #[macro_export] diff --git a/src/lib.rs b/src/lib.rs index 99ccd28..03d12d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -332,7 +332,7 @@ pub use log::log; pub use matrix::{Cell, Matrix}; pub use matrix_repr::load_patch_from_file; pub use matrix_repr::save_patch_to_file; -pub use nodes::{new_node_engine, NodeConfigurator, NodeExecutor}; +pub use nodes::{new_node_engine, HxMidiEvent, NodeConfigurator, NodeExecutor}; pub use sample_lib::{SampleLibrary, SampleLoadError}; pub use scope_handle::ScopeHandle; diff --git a/src/matrix.rs b/src/matrix.rs index 2404341..989bacd 100644 --- a/src/matrix.rs +++ b/src/matrix.rs @@ -7,7 +7,9 @@ use crate::dsp::{NodeId, NodeInfo, ParamId, SAtom}; use crate::matrix_repr::*; pub use crate::monitor::MON_SIG_CNT; pub use crate::nodes::MinMaxMonitorSamples; -use crate::nodes::{NodeConfigurator, NodeGraphOrdering, NodeProg, MAX_ALLOCATED_NODES}; +use crate::nodes::{ + GraphEvent, HxMidiEvent, NodeConfigurator, NodeGraphOrdering, NodeProg, MAX_ALLOCATED_NODES, +}; use crate::wblockdsp::{BlkJITCompileError, BlockFun, BlockFunSnapshot}; pub use crate::CellDir; use crate::ScopeHandle; @@ -467,6 +469,8 @@ pub trait MatrixObserver { /// The called then needs up update all it's internal state it knows /// about [Matrix]. fn update_all(&self); + /// Called when a MIDI event was received. + fn midi_event(&self, midi_ev: HxMidiEvent); } pub struct Matrix { @@ -1457,6 +1461,28 @@ impl Matrix { pub fn update_output_feedback(&mut self) { self.config.update_output_feedback(); } + + /// Injects a [HxMidiEvent] directly into audio thread, so that it can trickle + /// back to the GUI thread the standard way. This is mostly used for automated testing. + /// And maybe some day for some kind of remote control script from WLambda? + pub fn inject_midi_event(&mut self, midi_ev: HxMidiEvent) { + self.config.inject_midi_event(midi_ev); + } + + /// Handles events from the DSP graph. Such as MIDI events for MIDI learn + /// functionality! Call this regularily (every UI frame) if you want to + /// have MIDI learn to work and receive events such as MIDI events via the [MatrixObserver]. + pub fn handle_graph_events(&mut self) { + while let Some(event) = self.config.next_event() { + match event { + GraphEvent::MIDI(midi_ev) => { + if let Some(obs) = &self.observer { + obs.midi_event(midi_ev); + } + } + } + } + } } #[cfg(test)] diff --git a/src/nodes/midi.rs b/src/nodes/midi.rs new file mode 100644 index 0000000..eef2e10 --- /dev/null +++ b/src/nodes/midi.rs @@ -0,0 +1,104 @@ +// Copyright (c) 2022 Weird Constructor +// This file is a part of HexoDSP. Released under GPL-3.0-or-later. +// See README.md and COPYING for details. + +#[derive(Debug, Clone, Copy)] +pub struct HxTimedEvent { + /// The frame number in the current block by the audio driver or plugin API/DAW + timing: usize, + kind: HxMidiEvent, +} + +impl HxTimedEvent { + pub fn new_timed(timing: usize, kind: HxMidiEvent) -> Self { + Self { timing, kind } + } + + pub fn is_cc(&self) -> bool { + if let HxMidiEvent::CC { .. } = self.kind { + true + } else { + false + } + } + + pub fn kind(&self) -> HxMidiEvent { + self.kind + } + + pub fn cc(timing: usize, channel: u8, cc: u8, value: f32) -> Self { + Self { timing, kind: HxMidiEvent::CC { channel, cc, value } } + } + + pub fn note_on(timing: usize, channel: u8, note: u8, vel: f32) -> Self { + Self { timing, kind: HxMidiEvent::NoteOn { channel, note, vel } } + } + + pub fn note_off(timing: usize, channel: u8, note: u8) -> Self { + Self { timing, kind: HxMidiEvent::NoteOff { channel, note } } + } +} + +pub struct MidiEventPointer<'a> { + buf: &'a [HxTimedEvent], + idx: usize, +} + +impl<'a> MidiEventPointer<'a> { + pub fn new(buf: &'a [HxTimedEvent]) -> Self { + Self { buf, idx: 0 } + } + + pub fn next_at(&mut self, time: usize) -> Option { + if self.idx < self.buf.len() && self.buf[self.idx].timing <= time { + self.idx += 1; + Some(self.buf[self.idx - 1].kind) + } else { + None + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum HxMidiEvent { + NoteOn { channel: u8, note: u8, vel: f32 }, + NoteOff { channel: u8, note: u8 }, + CC { channel: u8, cc: u8, value: f32 }, +} + +pub struct EventWindowing { + pub event: Option, +} + +impl EventWindowing { + pub fn new() -> Self { + Self { event: None } + } + + #[inline] + pub fn feed_me(&self) -> bool { + self.event.is_none() + } + + #[inline] + pub fn feed(&mut self, event: HxTimedEvent) { + self.event = Some(event); + } + + #[inline] + pub fn next_event_in_range( + &mut self, + to_time: usize, + block_size: usize, + ) -> Option { + if let Some(event) = self.event.take() { + if event.timing < (to_time + block_size) { + return Some(HxTimedEvent { timing: event.timing - to_time, kind: event.kind }); + } else { + self.event = Some(event); + } + } + + None + } +} diff --git a/src/nodes/mod.rs b/src/nodes/mod.rs index 3c02f9f..a8a4330 100644 --- a/src/nodes/mod.rs +++ b/src/nodes/mod.rs @@ -7,6 +7,7 @@ pub const MAX_SCOPES: usize = 8; pub const SCOPE_SAMPLES: usize = 512; pub const MAX_INPUTS: usize = 32; pub const MAX_SMOOTHERS: usize = 36 + 4; // 6 * 6 modulator inputs + 4 UI Knobs +pub const MAX_INJ_MIDI_EVENTS: usize = 64; pub const MAX_AVAIL_TRACKERS: usize = 128; pub const MAX_AVAIL_CODE_ENGINES: usize = 32; pub const MAX_FB_DELAYS: usize = 256; // 256 feedback delays, thats roughly 1.2MB RAM @@ -17,23 +18,21 @@ pub const MAX_FB_DELAY_SIZE: usize = (MAX_FB_DELAY_SRATE * FB_DELAY_TIME_US) / 1 mod drop_thread; mod feedback_filter; +mod midi; mod node_conf; mod node_exec; mod node_graph_ordering; mod node_prog; -mod note_buffer; pub mod visual_sampling_filter; pub(crate) use visual_sampling_filter::*; pub use feedback_filter::*; +pub use midi::{EventWindowing, HxMidiEvent, HxTimedEvent, MidiEventPointer}; pub use node_conf::*; pub use node_exec::*; pub use node_graph_ordering::NodeGraphOrdering; pub use node_prog::*; -pub use note_buffer::{ - EventWindowing, HxMidiEvent, HxTimedEvent, MidiEventPointer, NoteBuffer, NoteChannelState, -}; use crate::dsp::{Node, SAtom}; pub use crate::monitor::MinMaxMonitorSamples; @@ -78,12 +77,21 @@ pub enum GraphMessage { mod_idx: usize, modamt: f32, }, + InjectMidi { + midi_ev: HxMidiEvent, + }, /// Sets the buffer indices to monitor with the FeedbackProcessor. SetMonitor { bufs: [usize; MON_SIG_CNT], }, } +/// Message from the DSP graph/backend to the frontend. Such as MIDI events +/// for MIDI learn for instance. +pub enum GraphEvent { + MIDI(HxMidiEvent), +} + pub const UNUSED_MONITOR_IDX: usize = 99999; /// Creates a NodeConfigurator and a NodeExecutor which are interconnected diff --git a/src/nodes/node_conf.rs b/src/nodes/node_conf.rs index 5f1b5f4..37e8b1c 100644 --- a/src/nodes/node_conf.rs +++ b/src/nodes/node_conf.rs @@ -3,8 +3,8 @@ // See README.md and COPYING for details. use super::{ - FeedbackFilter, GraphMessage, NodeOp, NodeProg, MAX_ALLOCATED_NODES, MAX_AVAIL_CODE_ENGINES, - MAX_AVAIL_TRACKERS, MAX_INPUTS, MAX_SCOPES, UNUSED_MONITOR_IDX, + FeedbackFilter, GraphEvent, GraphMessage, HxMidiEvent, NodeOp, NodeProg, MAX_ALLOCATED_NODES, + MAX_AVAIL_CODE_ENGINES, MAX_AVAIL_TRACKERS, MAX_INPUTS, MAX_SCOPES, UNUSED_MONITOR_IDX, }; use crate::dsp::tracker::{PatternData, Tracker}; use crate::dsp::{node_factory, Node, NodeId, NodeInfo, ParamId, SAtom}; @@ -16,7 +16,7 @@ use crate::ScopeHandle; #[cfg(feature = "synfx-dsp-jit")] use synfx_dsp_jit::engine::CodeEngine; -use ringbuf::{Producer, RingBuffer}; +use ringbuf::{Consumer, Producer, RingBuffer}; use std::collections::HashMap; use std::sync::{Arc, Mutex}; @@ -229,6 +229,8 @@ pub(crate) struct SharedNodeConf { pub(crate) node_ctx_values: Vec>, /// For updating the NodeExecutor with graph updates. pub(crate) graph_update_prod: Producer, + /// For receiving events from the DSP graph. + pub(crate) graph_event_cons: Consumer, /// For receiving monitor data from the backend thread. pub(crate) monitor: Monitor, /// Handles deallocation of dead nodes from the backend. @@ -242,9 +244,11 @@ impl SharedNodeConf { pub(crate) fn new() -> (Self, SharedNodeExec) { let rb_graph = RingBuffer::new(MAX_ALLOCATED_NODES * 2); let rb_drop = RingBuffer::new(MAX_ALLOCATED_NODES * 2); + let rb_ev = RingBuffer::new(MAX_ALLOCATED_NODES * 2); let (rb_graph_prod, rb_graph_con) = rb_graph.split(); let (rb_drop_prod, rb_drop_con) = rb_drop.split(); + let (rb_ev_prod, rb_ev_con) = rb_ev.split(); let drop_thread = DropThread::new(rb_drop_con); @@ -259,11 +263,18 @@ 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, + graph_event_cons: rb_ev_con, + monitor, + drop_thread, + }, SharedNodeExec { node_ctx_values: exec_node_ctx_vals, graph_update_con: rb_graph_con, graph_drop_prod: rb_drop_prod, + graph_event_prod: rb_ev_prod, monitor_backend, }, ) @@ -1093,4 +1104,16 @@ impl NodeConfigurator { pub fn get_minmax_monitor_samples(&mut self, idx: usize) -> &MinMaxMonitorSamples { self.shared.monitor.get_minmax_monitor_samples(idx) } + + /// Injects a [HxMidiEvent] directly into audio thread, so that it can trickle + /// back to the GUI thread the standard way. This is mostly used for automated testing. + /// And maybe some day for some kind of remote control script from WLambda? + pub fn inject_midi_event(&mut self, midi_ev: HxMidiEvent) { + let _ = self.shared.graph_update_prod.push(GraphMessage::InjectMidi { midi_ev }); + } + + /// Returns the next [GraphEvent] from the DSP/audio/backend thread. + pub fn next_event(&mut self) -> Option { + self.shared.graph_event_cons.pop() + } } diff --git a/src/nodes/node_exec.rs b/src/nodes/node_exec.rs index a28b30e..cc0beab 100644 --- a/src/nodes/node_exec.rs +++ b/src/nodes/node_exec.rs @@ -2,10 +2,10 @@ // This file is a part of HexoDSP. Released under GPL-3.0-or-later. // See README.md and COPYING for details. -use super::NoteBuffer; use super::{ - DropMsg, EventWindowing, GraphMessage, HxMidiEvent, HxTimedEvent, NodeProg, FB_DELAY_TIME_US, - MAX_ALLOCATED_NODES, MAX_FB_DELAY_SIZE, MAX_SMOOTHERS, UNUSED_MONITOR_IDX, + DropMsg, EventWindowing, GraphEvent, GraphMessage, HxMidiEvent, HxTimedEvent, NodeProg, + FB_DELAY_TIME_US, MAX_ALLOCATED_NODES, MAX_FB_DELAY_SIZE, MAX_INJ_MIDI_EVENTS, MAX_SMOOTHERS, + UNUSED_MONITOR_IDX, }; use crate::dsp::{Node, NodeContext, NodeId, MAX_BLOCK_SIZE}; use crate::monitor::{MonitorBackend, MON_SIG_CNT}; @@ -69,6 +69,9 @@ pub struct NodeExecutor { /// The connection with the [crate::nodes::NodeConfigurator]. shared: SharedNodeExec, + /// A small buffer for injected [HxMidiEvent] + injected_midi: Vec, + /// A flag to remember if we already initialized the logger on the audio thread. dsp_log_init: bool, } @@ -84,6 +87,9 @@ pub(crate) struct SharedNodeExec { pub(crate) graph_update_con: Consumer, /// For receiving deleted/overwritten nodes from the backend thread. pub(crate) graph_drop_prod: Producer, + /// For sending events from the DSP graph to the frontend. Such as MIDI events for + /// the MIDI learn functionality. + pub(crate) graph_event_prod: Producer, /// For sending feedback to the frontend thread. pub(crate) monitor_backend: MonitorBackend, } @@ -173,7 +179,6 @@ impl Default for FeedbackBuffer { /// This is used for instance to implement the feedbackd delay nodes. pub struct NodeExecContext { pub feedback_delay_buffers: Vec, - pub note_buffer: NoteBuffer, pub midi_notes: Vec, pub midi_ccs: Vec, } @@ -184,7 +189,7 @@ impl NodeExecContext { fbdb.resize_with(MAX_ALLOCATED_NODES, FeedbackBuffer::new); let midi_notes = Vec::with_capacity(MAX_MIDI_NOTES_PER_BLOCK); let midi_ccs = Vec::with_capacity(MAX_MIDI_CC_PER_BLOCK); - Self { feedback_delay_buffers: fbdb, note_buffer: NoteBuffer::new(), midi_notes, midi_ccs } + Self { feedback_delay_buffers: fbdb, midi_notes, midi_ccs } } fn set_sample_rate(&mut self, srate: f32) { @@ -209,6 +214,7 @@ impl NodeExecutor { smoothers.resize_with(MAX_SMOOTHERS, || (0, Smoother::new())); let target_refresh = Vec::with_capacity(MAX_SMOOTHERS); + let injected_midi = Vec::with_capacity(MAX_INJ_MIDI_EVENTS); NodeExecutor { nodes, @@ -219,6 +225,7 @@ impl NodeExecutor { monitor_signal_cur_inp_indices: [UNUSED_MONITOR_IDX; MON_SIG_CNT], exec_ctx: NodeExecContext::new(), dsp_log_init: false, + injected_midi, shared, } } @@ -345,6 +352,11 @@ impl NodeExecutor { GraphMessage::SetMonitor { bufs } => { self.monitor_signal_cur_inp_indices = bufs; } + GraphMessage::InjectMidi { midi_ev } => { + if self.injected_midi.len() < MAX_INJ_MIDI_EVENTS { + self.injected_midi.push(midi_ev); + } + } } } } @@ -366,6 +378,20 @@ impl NodeExecutor { self.exec_ctx.midi_notes.clear(); self.exec_ctx.midi_ccs.clear(); + if self.injected_midi.len() > 0 { + for ev in self.injected_midi.iter().rev() { + let ev = HxTimedEvent::new_timed(0, *ev); + if ev.is_cc() { + self.exec_ctx.midi_ccs.push(ev); + } else { + self.exec_ctx.midi_notes.push(ev); + } + let _ = self.shared.graph_event_prod.push(GraphEvent::MIDI(ev.kind())); + } + + self.injected_midi.clear(); + } + while let Some(ev) = f() { if ev.is_cc() { self.exec_ctx.midi_ccs.push(ev); @@ -373,6 +399,8 @@ impl NodeExecutor { self.exec_ctx.midi_notes.push(ev); } + let _ = self.shared.graph_event_prod.push(GraphEvent::MIDI(ev.kind())); + if self.exec_ctx.midi_ccs.len() == MAX_MIDI_CC_PER_BLOCK { break; } @@ -382,11 +410,6 @@ impl NodeExecutor { } } - #[inline] - pub fn get_note_buffer(&mut self) -> &mut NoteBuffer { - &mut self.exec_ctx.note_buffer - } - #[inline] pub fn get_nodes(&self) -> &Vec { &self.nodes @@ -553,7 +576,7 @@ impl NodeExecutor { &mut self, seconds: f32, realtime: bool, - mut events: Vec, + events: &[HxTimedEvent], ) -> (Vec, Vec) { const SAMPLE_RATE: f32 = 44100.0; self.set_sample_rate(SAMPLE_RATE); @@ -571,6 +594,7 @@ impl NodeExecutor { output_l[i] = 0.0; output_r[i] = 0.0; } + let mut ev_idx = 0; let mut offs = 0; while nframes > 0 { let cur_nframes = if nframes >= MAX_BLOCK_SIZE { MAX_BLOCK_SIZE } else { nframes }; @@ -578,11 +602,12 @@ impl NodeExecutor { self.feed_midi_events_from(|| { if ev_win.feed_me() { - if events.is_empty() { + if ev_idx >= events.len() { return None; } - ev_win.feed(events.remove(0)); + ev_win.feed(events[ev_idx]); + ev_idx += 1; } ev_win.next_event_in_range(offs, cur_nframes) diff --git a/src/nodes/note_buffer.rs b/src/nodes/note_buffer.rs deleted file mode 100644 index ed1f6b8..0000000 --- a/src/nodes/note_buffer.rs +++ /dev/null @@ -1,219 +0,0 @@ -// Copyright (c) 2022 Weird Constructor -// This file is a part of HexoDSP. Released under GPL-3.0-or-later. -// See README.md and COPYING for details. - -use crate::dsp::MAX_BLOCK_SIZE; - -#[derive(Debug, Clone, Copy)] -pub struct HxTimedEvent { - /// The frame number in the current block by the audio driver or plugin API/DAW - timing: usize, - kind: HxMidiEvent, -} - -impl HxTimedEvent { - pub fn is_cc(&self) -> bool { - if let HxMidiEvent::CC { .. } = self.kind { - true - } else { - false - } - } - - pub fn cc(timing: usize, channel: u8, cc: u8, value: f32) -> Self { - Self { timing, kind: HxMidiEvent::CC { channel, cc, value } } - } - - pub fn note_on(timing: usize, channel: u8, note: u8, vel: f32) -> Self { - Self { timing, kind: HxMidiEvent::NoteOn { channel, note, vel } } - } - - pub fn note_off(timing: usize, channel: u8, note: u8) -> Self { - Self { timing, kind: HxMidiEvent::NoteOff { channel, note } } - } -} - -pub struct MidiEventPointer<'a> { - buf: &'a [HxTimedEvent], - idx: usize, -} - -impl<'a> MidiEventPointer<'a> { - pub fn new(buf: &'a [HxTimedEvent]) -> Self { - Self { buf, idx: 0 } - } - - pub fn next_at(&mut self, time: usize) -> Option { - if self.idx < self.buf.len() && self.buf[self.idx].timing <= time { - self.idx += 1; - Some(self.buf[self.idx - 1].kind) - } else { - None - } - } -} - -#[derive(Debug, Clone, Copy)] -pub enum HxMidiEvent { - NoteOn { channel: u8, note: u8, vel: f32 }, - NoteOff { channel: u8, note: u8 }, - CC { channel: u8, cc: u8, value: f32 }, -} - -#[derive(Debug, Clone, Copy)] -pub struct NoteChannelState { - pub vel: f32, - pub note: u8, - pub gate: u8, -} - -impl std::fmt::Display for NoteChannelState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "C", self.note, self.gate, self.vel) - } -} - -impl NoteChannelState { - pub fn new() -> Self { - Self { vel: 0.0, note: 0, gate: 0 } - } -} - -pub struct NoteBuffer { - interleaved_chans: Vec, - buf_idx: usize, -} - -impl NoteBuffer { - pub fn new() -> Self { - Self { interleaved_chans: vec![NoteChannelState::new(); 16 * MAX_BLOCK_SIZE], buf_idx: 15 } - } - - #[inline] - pub fn reset(&mut self) { - let cur = self.buf_idx; - if cur != 0 { - self.interleaved_chans.copy_within((cur * 16)..((cur + 1) * 16), 0); - self.buf_idx = 0; - } - } - - #[inline] - pub fn step_to(&mut self, buf_idx: usize) { - while self.buf_idx < (buf_idx % MAX_BLOCK_SIZE) { - self.step(); - } - } - - #[inline] - pub fn step(&mut self) { - let cur = self.buf_idx; - let next = (self.buf_idx + 1) % MAX_BLOCK_SIZE; - //d// println!("COPY {}..{} => {}", (cur * 16), ((cur + 1) * 16), next * 16); - self.interleaved_chans.copy_within((cur * 16)..((cur + 1) * 16), next * 16); - self.buf_idx = next; - } - - #[inline] - pub fn note_on(&mut self, channel: u8, note: u8) { - let mut chan = &mut self.interleaved_chans[(self.buf_idx * 16) + (channel as usize % 16)]; - chan.gate = chan.gate % 2 + 1; - chan.gate |= 0x10; - chan.note = note; - } - - #[inline] - pub fn note_off(&mut self, channel: u8, note: u8) { - let mut chan = &mut self.interleaved_chans[(self.buf_idx * 16) + (channel as usize % 16)]; - if chan.gate > 0 && chan.note == note { - chan.gate = chan.gate & 0x0F; - } - } - - #[inline] - pub fn set_velocity(&mut self, channel: u8, vel: f32) { - self.interleaved_chans[(self.buf_idx * 16) + (channel as usize % 16)].vel = vel; - } - - #[inline] - pub fn get_chan_at(&self, channel: u8, frame: u8) -> &NoteChannelState { - &self.interleaved_chans[frame as usize * 16 + (channel as usize % 16)] - } -} - -pub struct EventWindowing { - pub event: Option, -} - -impl EventWindowing { - pub fn new() -> Self { - Self { event: None } - } - - #[inline] - pub fn feed_me(&self) -> bool { - self.event.is_none() - } - - #[inline] - pub fn feed(&mut self, event: HxTimedEvent) { - self.event = Some(event); - } - - #[inline] - pub fn next_event_in_range( - &mut self, - to_time: usize, - block_size: usize, - ) -> Option { - if let Some(event) = self.event.take() { - if event.timing < (to_time + block_size) { - return Some(HxTimedEvent { timing: event.timing - to_time, kind: event.kind }); - } else { - self.event = Some(event); - } - } - - None - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn check_note_buffer() { - let mut buf = NoteBuffer::new(); - - buf.reset(); - buf.note_on(0, 10); - buf.note_on(2, 12); - buf.set_velocity(0, 0.6); - buf.set_velocity(2, 0.8); - //d// println!("> {:?}", buf.get_chan_at(0, 0)); - buf.step(); - buf.note_on(0, 11); - for _ in 0..10 { - buf.step(); - } - buf.note_off(0, 11); - buf.step(); - buf.note_off(0, 10); - buf.step(); - buf.set_velocity(2, 0.4); - //d// println!("> {:?}", buf.get_chan_at(0, 0)); - for i in 0..(MAX_BLOCK_SIZE - 14) { - buf.step(); - } - - //d// for i in 0..MAX_BLOCK_SIZE { - //d// println!(">{} {}", i, buf.get_chan_at(2, i as u8)); - //d// } - - assert_eq!(buf.get_chan_at(0, 0).to_string(), "C"); - assert_eq!(buf.get_chan_at(0, 12).to_string(), "C"); - assert_eq!(buf.get_chan_at(2, 0).to_string(), "C"); - assert_eq!(buf.get_chan_at(2, 127).to_string(), "C"); - } -} diff --git a/src/util.rs b/src/util.rs index 7669d70..63014d5 100644 --- a/src/util.rs +++ b/src/util.rs @@ -42,6 +42,7 @@ impl Smoother { self.done = false; } + #[allow(dead_code)] #[inline] pub fn current(&self) -> f32 { if self.done { diff --git a/tests/basics.rs b/tests/basics.rs index e3c6791..eba48f5 100644 --- a/tests/basics.rs +++ b/tests/basics.rs @@ -976,7 +976,7 @@ fn check_matrix_output_feedback() { matrix.set_param(gain_p, SAtom::param(0.25)); for _ in 0..10 { - node_exec.test_run(0.11, true); + node_exec.test_run(0.11, true, &[]); matrix.update_filters(); matrix.filtered_out_fb_for(&sin, sin.out("sig").unwrap()); matrix.filtered_out_fb_for(&, amp.out("sig").unwrap()); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 78faa58..a8472ae 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -533,7 +533,7 @@ pub fn run_realtime_no_input( seconds: f32, sleep_a_bit: bool, ) -> (Vec, Vec) { - node_exec.test_run(seconds, sleep_a_bit, vec![]) + node_exec.test_run(seconds, sleep_a_bit, &[]) } pub fn calc_rms_mimax_each_ms(buf: &[f32], ms: f32) -> Vec<(f32, f32, f32)> { diff --git a/tests/matrix_observer.rs b/tests/matrix_observer.rs new file mode 100644 index 0000000..c552ada --- /dev/null +++ b/tests/matrix_observer.rs @@ -0,0 +1,46 @@ +// Copyright (c) 2022 Weird Constructor +// This file is a part of HexoDSP. Released under GPL-3.0-or-later. +// See README.md and COPYING for details. + +mod common; +use common::*; + +use std::sync::{Arc, Mutex}; + +struct MatrixTestRecorder { + records: Mutex>, +} + +impl MatrixObserver for MatrixTestRecorder { + fn update_prop(&self, _key: &str) {} + fn update_monitor(&self, _cell: &Cell) {} + fn update_param(&self, _param_id: &ParamId) {} + fn update_matrix(&self) {} + fn update_all(&self) {} + fn midi_event(&self, midi_ev: HxMidiEvent) { + self.records.lock().expect("recorder lock ok").push(format!("{:?}", midi_ev)); + } +} + +#[test] +fn check_matrix_observer() { + let (node_conf, mut node_exec) = new_node_engine(); + let mut matrix = Matrix::new(node_conf, 3, 3); + let recorder = Arc::new(MatrixTestRecorder { records: Mutex::new(vec![]) }); + matrix.set_observer(recorder.clone()); + + let mut chain = MatrixCellChain::new(CellDir::B); + chain.node_out("test", "sig").node_inp("out", "ch1").place(&mut matrix, 0, 0).unwrap(); + matrix.sync().unwrap(); + + matrix.inject_midi_event(HxMidiEvent::NoteOn { channel: 1, note: 57, vel: 0.751 }); + + // matrix.set_param(trig_p, SAtom::param(0.0)); + run_for_ms(&mut node_exec, 10.0); + matrix.handle_graph_events(); + + let rec = + recorder.records.lock().expect("lock recorder for pop").pop().expect("A record present"); + + assert_eq!(rec, "NoteOn { channel: 1, note: 57, vel: 0.751 }"); +} diff --git a/tests/node_midip.rs b/tests/node_midip.rs index 8ddc0b2..5527ed8 100644 --- a/tests/node_midip.rs +++ b/tests/node_midip.rs @@ -20,7 +20,7 @@ fn check_node_midip_gate_inserts() { let (ch1, _) = node_exec.test_run( 0.005, false, - vec![ + &[ HxTimedEvent::note_on(5, 0, 69, 1.0), HxTimedEvent::note_on(10, 0, 68, 1.0), HxTimedEvent::note_on(130, 0, 57, 1.0), @@ -32,7 +32,7 @@ fn check_node_midip_gate_inserts() { assert_eq!( changes, - vec![ + &[ (5, 100), // First note triggers right (11, 100), // Second note needs to shortly pause the gate, which has 1 sample delay (131, 100), // Third note also shortly pauses one sample later. @@ -55,7 +55,7 @@ fn check_node_midip_pitch_track() { let (ch1, _) = node_exec.test_run( 0.005, false, - vec![ + &[ HxTimedEvent::note_on(5, 0, 69, 1.0), HxTimedEvent::note_on(10, 0, 68, 1.0), HxTimedEvent::note_on(130, 0, 57, 1.0), @@ -86,7 +86,7 @@ fn check_node_midip_pitch_det() { let (ch1, _) = node_exec.test_run( 0.005, false, - vec![ + &[ HxTimedEvent::note_on(5, 0, 69, 1.0), HxTimedEvent::note_on(10, 0, 68, 1.0), HxTimedEvent::note_on(130, 0, 57, 1.0), @@ -112,7 +112,7 @@ fn check_node_midip_vel_track() { let (ch1, _) = node_exec.test_run( 0.005, false, - vec![ + &[ HxTimedEvent::note_on(5, 0, 69, 0.4), HxTimedEvent::note_on(10, 0, 68, 1.0), HxTimedEvent::note_on(130, 0, 57, 0.6), @@ -141,7 +141,7 @@ fn check_node_midip_off_on_test() { let (ch1, _) = node_exec.test_run( 0.005, false, - vec![ + &[ HxTimedEvent::note_on(0, 0, 69, 1.0), HxTimedEvent::note_off(5, 0, 69), HxTimedEvent::note_on(5, 0, 68, 1.0),