Added MIDI events to/from frontend thread

This commit is contained in:
Weird Constructor 2022-08-14 15:09:00 +02:00
parent db2e5b13ed
commit c175594b1f
15 changed files with 270 additions and 261 deletions

View file

@ -511,10 +511,10 @@ mod node_formfm;
#[allow(non_upper_case_globals)] #[allow(non_upper_case_globals)]
mod node_map; mod node_map;
#[allow(non_upper_case_globals)] #[allow(non_upper_case_globals)]
mod node_midip;
#[allow(non_upper_case_globals)]
mod node_midicc; mod node_midicc;
#[allow(non_upper_case_globals)] #[allow(non_upper_case_globals)]
mod node_midip;
#[allow(non_upper_case_globals)]
mod node_mix3; mod node_mix3;
#[allow(non_upper_case_globals)] #[allow(non_upper_case_globals)]
mod node_mux9; mod node_mux9;
@ -571,9 +571,9 @@ use crate::fa_cqnt_omax;
use crate::fa_cqnt_omin; use crate::fa_cqnt_omin;
use crate::fa_delay_mode; use crate::fa_delay_mode;
use crate::fa_map_clip; use crate::fa_map_clip;
use crate::fa_midicc_cc;
use crate::fa_midip_chan; use crate::fa_midip_chan;
use crate::fa_midip_gmode; use crate::fa_midip_gmode;
use crate::fa_midicc_cc;
use crate::fa_mux9_in_cnt; use crate::fa_mux9_in_cnt;
use crate::fa_noise_mode; use crate::fa_noise_mode;
use crate::fa_out_mono; use crate::fa_out_mono;
@ -604,8 +604,8 @@ use node_fbwr_fbrd::FbRd;
use node_fbwr_fbrd::FbWr; use node_fbwr_fbrd::FbWr;
use node_formfm::FormFM; use node_formfm::FormFM;
use node_map::Map; use node_map::Map;
use node_midip::MidiP;
use node_midicc::MidiCC; use node_midicc::MidiCC;
use node_midip::MidiP;
use node_mix3::Mix3; use node_mix3::Mix3;
use node_mux9::Mux9; use node_mux9::Mux9;
use node_noise::Noise; use node_noise::Noise;

View file

@ -2,9 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later. // This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details. // See README.md and COPYING for details.
use crate::dsp::{ use crate::dsp::{at, out_idx, DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom};
at, denorm, inp, out_idx, DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom,
};
use crate::nodes::{HxMidiEvent, MidiEventPointer, NodeAudioContext, NodeExecContext}; use crate::nodes::{HxMidiEvent, MidiEventPointer, NodeAudioContext, NodeExecContext};
#[macro_export] #[macro_export]
@ -92,7 +90,6 @@ impl DspNode for MidiCC {
while let Some(ev) = ptr.next_at(frame) { while let Some(ev) = ptr.next_at(frame) {
match ev { match ev {
HxMidiEvent::CC { channel, cc, value } => { HxMidiEvent::CC { channel, cc, value } => {
println!("CC: {} {} {}", channel, cc, value);
if channel != midicc_channel { if channel != midicc_channel {
continue; continue;
} }

View file

@ -2,9 +2,7 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later. // This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details. // See README.md and COPYING for details.
use crate::dsp::{ use crate::dsp::{at, inp, out_idx, DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom};
at, denorm, inp, out_idx, DspNode, LedPhaseVals, NodeContext, NodeId, ProcBuf, SAtom,
};
use crate::nodes::{HxMidiEvent, MidiEventPointer, NodeAudioContext, NodeExecContext}; use crate::nodes::{HxMidiEvent, MidiEventPointer, NodeAudioContext, NodeExecContext};
#[macro_export] #[macro_export]

View file

@ -332,7 +332,7 @@ pub use log::log;
pub use matrix::{Cell, Matrix}; pub use matrix::{Cell, Matrix};
pub use matrix_repr::load_patch_from_file; pub use matrix_repr::load_patch_from_file;
pub use matrix_repr::save_patch_to_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 sample_lib::{SampleLibrary, SampleLoadError};
pub use scope_handle::ScopeHandle; pub use scope_handle::ScopeHandle;

View file

@ -7,7 +7,9 @@ use crate::dsp::{NodeId, NodeInfo, ParamId, SAtom};
use crate::matrix_repr::*; use crate::matrix_repr::*;
pub use crate::monitor::MON_SIG_CNT; pub use crate::monitor::MON_SIG_CNT;
pub use crate::nodes::MinMaxMonitorSamples; 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}; use crate::wblockdsp::{BlkJITCompileError, BlockFun, BlockFunSnapshot};
pub use crate::CellDir; pub use crate::CellDir;
use crate::ScopeHandle; use crate::ScopeHandle;
@ -467,6 +469,8 @@ pub trait MatrixObserver {
/// The called then needs up update all it's internal state it knows /// The called then needs up update all it's internal state it knows
/// about [Matrix]. /// about [Matrix].
fn update_all(&self); fn update_all(&self);
/// Called when a MIDI event was received.
fn midi_event(&self, midi_ev: HxMidiEvent);
} }
pub struct Matrix { pub struct Matrix {
@ -1457,6 +1461,28 @@ impl Matrix {
pub fn update_output_feedback(&mut self) { pub fn update_output_feedback(&mut self) {
self.config.update_output_feedback(); 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)] #[cfg(test)]

104
src/nodes/midi.rs Normal file
View file

@ -0,0 +1,104 @@
// Copyright (c) 2022 Weird Constructor <weirdconstructor@gmail.com>
// 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<HxMidiEvent> {
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<HxTimedEvent>,
}
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<HxTimedEvent> {
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
}
}

View file

@ -7,6 +7,7 @@ pub const MAX_SCOPES: usize = 8;
pub const SCOPE_SAMPLES: usize = 512; pub const SCOPE_SAMPLES: usize = 512;
pub const MAX_INPUTS: usize = 32; pub const MAX_INPUTS: usize = 32;
pub const MAX_SMOOTHERS: usize = 36 + 4; // 6 * 6 modulator inputs + 4 UI Knobs 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_TRACKERS: usize = 128;
pub const MAX_AVAIL_CODE_ENGINES: usize = 32; pub const MAX_AVAIL_CODE_ENGINES: usize = 32;
pub const MAX_FB_DELAYS: usize = 256; // 256 feedback delays, thats roughly 1.2MB RAM 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 drop_thread;
mod feedback_filter; mod feedback_filter;
mod midi;
mod node_conf; mod node_conf;
mod node_exec; mod node_exec;
mod node_graph_ordering; mod node_graph_ordering;
mod node_prog; mod node_prog;
mod note_buffer;
pub mod visual_sampling_filter; pub mod visual_sampling_filter;
pub(crate) use visual_sampling_filter::*; pub(crate) use visual_sampling_filter::*;
pub use feedback_filter::*; pub use feedback_filter::*;
pub use midi::{EventWindowing, HxMidiEvent, HxTimedEvent, MidiEventPointer};
pub use node_conf::*; pub use node_conf::*;
pub use node_exec::*; pub use node_exec::*;
pub use node_graph_ordering::NodeGraphOrdering; pub use node_graph_ordering::NodeGraphOrdering;
pub use node_prog::*; pub use node_prog::*;
pub use note_buffer::{
EventWindowing, HxMidiEvent, HxTimedEvent, MidiEventPointer, NoteBuffer, NoteChannelState,
};
use crate::dsp::{Node, SAtom}; use crate::dsp::{Node, SAtom};
pub use crate::monitor::MinMaxMonitorSamples; pub use crate::monitor::MinMaxMonitorSamples;
@ -78,12 +77,21 @@ pub enum GraphMessage {
mod_idx: usize, mod_idx: usize,
modamt: f32, modamt: f32,
}, },
InjectMidi {
midi_ev: HxMidiEvent,
},
/// Sets the buffer indices to monitor with the FeedbackProcessor. /// Sets the buffer indices to monitor with the FeedbackProcessor.
SetMonitor { SetMonitor {
bufs: [usize; MON_SIG_CNT], 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; pub const UNUSED_MONITOR_IDX: usize = 99999;
/// Creates a NodeConfigurator and a NodeExecutor which are interconnected /// Creates a NodeConfigurator and a NodeExecutor which are interconnected

View file

@ -3,8 +3,8 @@
// See README.md and COPYING for details. // See README.md and COPYING for details.
use super::{ use super::{
FeedbackFilter, GraphMessage, NodeOp, NodeProg, MAX_ALLOCATED_NODES, MAX_AVAIL_CODE_ENGINES, FeedbackFilter, GraphEvent, GraphMessage, HxMidiEvent, NodeOp, NodeProg, MAX_ALLOCATED_NODES,
MAX_AVAIL_TRACKERS, MAX_INPUTS, MAX_SCOPES, UNUSED_MONITOR_IDX, MAX_AVAIL_CODE_ENGINES, MAX_AVAIL_TRACKERS, MAX_INPUTS, MAX_SCOPES, UNUSED_MONITOR_IDX,
}; };
use crate::dsp::tracker::{PatternData, Tracker}; use crate::dsp::tracker::{PatternData, Tracker};
use crate::dsp::{node_factory, Node, NodeId, NodeInfo, ParamId, SAtom}; use crate::dsp::{node_factory, Node, NodeId, NodeInfo, ParamId, SAtom};
@ -16,7 +16,7 @@ use crate::ScopeHandle;
#[cfg(feature = "synfx-dsp-jit")] #[cfg(feature = "synfx-dsp-jit")]
use synfx_dsp_jit::engine::CodeEngine; use synfx_dsp_jit::engine::CodeEngine;
use ringbuf::{Producer, RingBuffer}; use ringbuf::{Consumer, Producer, RingBuffer};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@ -229,6 +229,8 @@ pub(crate) struct SharedNodeConf {
pub(crate) node_ctx_values: Vec<Arc<AtomicFloat>>, pub(crate) node_ctx_values: Vec<Arc<AtomicFloat>>,
/// For updating the NodeExecutor with graph updates. /// For updating the NodeExecutor with graph updates.
pub(crate) graph_update_prod: Producer<GraphMessage>, pub(crate) graph_update_prod: Producer<GraphMessage>,
/// For receiving events from the DSP graph.
pub(crate) graph_event_cons: Consumer<GraphEvent>,
/// For receiving monitor data from the backend thread. /// For receiving monitor data from the backend thread.
pub(crate) monitor: Monitor, pub(crate) monitor: Monitor,
/// Handles deallocation of dead nodes from the backend. /// Handles deallocation of dead nodes from the backend.
@ -242,9 +244,11 @@ impl SharedNodeConf {
pub(crate) fn new() -> (Self, SharedNodeExec) { pub(crate) fn new() -> (Self, SharedNodeExec) {
let rb_graph = RingBuffer::new(MAX_ALLOCATED_NODES * 2); let rb_graph = RingBuffer::new(MAX_ALLOCATED_NODES * 2);
let rb_drop = 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_graph_prod, rb_graph_con) = rb_graph.split();
let (rb_drop_prod, rb_drop_con) = rb_drop.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); 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 { SharedNodeExec {
node_ctx_values: exec_node_ctx_vals, node_ctx_values: exec_node_ctx_vals,
graph_update_con: rb_graph_con, graph_update_con: rb_graph_con,
graph_drop_prod: rb_drop_prod, graph_drop_prod: rb_drop_prod,
graph_event_prod: rb_ev_prod,
monitor_backend, monitor_backend,
}, },
) )
@ -1093,4 +1104,16 @@ impl NodeConfigurator {
pub fn get_minmax_monitor_samples(&mut self, idx: usize) -> &MinMaxMonitorSamples { pub fn get_minmax_monitor_samples(&mut self, idx: usize) -> &MinMaxMonitorSamples {
self.shared.monitor.get_minmax_monitor_samples(idx) 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<GraphEvent> {
self.shared.graph_event_cons.pop()
}
} }

View file

@ -2,10 +2,10 @@
// This file is a part of HexoDSP. Released under GPL-3.0-or-later. // This file is a part of HexoDSP. Released under GPL-3.0-or-later.
// See README.md and COPYING for details. // See README.md and COPYING for details.
use super::NoteBuffer;
use super::{ use super::{
DropMsg, EventWindowing, GraphMessage, HxMidiEvent, HxTimedEvent, NodeProg, FB_DELAY_TIME_US, DropMsg, EventWindowing, GraphEvent, GraphMessage, HxMidiEvent, HxTimedEvent, NodeProg,
MAX_ALLOCATED_NODES, MAX_FB_DELAY_SIZE, MAX_SMOOTHERS, UNUSED_MONITOR_IDX, 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::dsp::{Node, NodeContext, NodeId, MAX_BLOCK_SIZE};
use crate::monitor::{MonitorBackend, MON_SIG_CNT}; use crate::monitor::{MonitorBackend, MON_SIG_CNT};
@ -69,6 +69,9 @@ pub struct NodeExecutor {
/// The connection with the [crate::nodes::NodeConfigurator]. /// The connection with the [crate::nodes::NodeConfigurator].
shared: SharedNodeExec, shared: SharedNodeExec,
/// A small buffer for injected [HxMidiEvent]
injected_midi: Vec<HxMidiEvent>,
/// A flag to remember if we already initialized the logger on the audio thread. /// A flag to remember if we already initialized the logger on the audio thread.
dsp_log_init: bool, dsp_log_init: bool,
} }
@ -84,6 +87,9 @@ pub(crate) struct SharedNodeExec {
pub(crate) graph_update_con: Consumer<GraphMessage>, pub(crate) graph_update_con: Consumer<GraphMessage>,
/// For receiving deleted/overwritten nodes from the backend thread. /// For receiving deleted/overwritten nodes from the backend thread.
pub(crate) graph_drop_prod: Producer<DropMsg>, pub(crate) graph_drop_prod: Producer<DropMsg>,
/// 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<GraphEvent>,
/// For sending feedback to the frontend thread. /// For sending feedback to the frontend thread.
pub(crate) monitor_backend: MonitorBackend, pub(crate) monitor_backend: MonitorBackend,
} }
@ -173,7 +179,6 @@ impl Default for FeedbackBuffer {
/// This is used for instance to implement the feedbackd delay nodes. /// This is used for instance to implement the feedbackd delay nodes.
pub struct NodeExecContext { pub struct NodeExecContext {
pub feedback_delay_buffers: Vec<FeedbackBuffer>, pub feedback_delay_buffers: Vec<FeedbackBuffer>,
pub note_buffer: NoteBuffer,
pub midi_notes: Vec<HxTimedEvent>, pub midi_notes: Vec<HxTimedEvent>,
pub midi_ccs: Vec<HxTimedEvent>, pub midi_ccs: Vec<HxTimedEvent>,
} }
@ -184,7 +189,7 @@ impl NodeExecContext {
fbdb.resize_with(MAX_ALLOCATED_NODES, FeedbackBuffer::new); fbdb.resize_with(MAX_ALLOCATED_NODES, FeedbackBuffer::new);
let midi_notes = Vec::with_capacity(MAX_MIDI_NOTES_PER_BLOCK); let midi_notes = Vec::with_capacity(MAX_MIDI_NOTES_PER_BLOCK);
let midi_ccs = Vec::with_capacity(MAX_MIDI_CC_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) { fn set_sample_rate(&mut self, srate: f32) {
@ -209,6 +214,7 @@ impl NodeExecutor {
smoothers.resize_with(MAX_SMOOTHERS, || (0, Smoother::new())); smoothers.resize_with(MAX_SMOOTHERS, || (0, Smoother::new()));
let target_refresh = Vec::with_capacity(MAX_SMOOTHERS); let target_refresh = Vec::with_capacity(MAX_SMOOTHERS);
let injected_midi = Vec::with_capacity(MAX_INJ_MIDI_EVENTS);
NodeExecutor { NodeExecutor {
nodes, nodes,
@ -219,6 +225,7 @@ impl NodeExecutor {
monitor_signal_cur_inp_indices: [UNUSED_MONITOR_IDX; MON_SIG_CNT], monitor_signal_cur_inp_indices: [UNUSED_MONITOR_IDX; MON_SIG_CNT],
exec_ctx: NodeExecContext::new(), exec_ctx: NodeExecContext::new(),
dsp_log_init: false, dsp_log_init: false,
injected_midi,
shared, shared,
} }
} }
@ -345,6 +352,11 @@ impl NodeExecutor {
GraphMessage::SetMonitor { bufs } => { GraphMessage::SetMonitor { bufs } => {
self.monitor_signal_cur_inp_indices = 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_notes.clear();
self.exec_ctx.midi_ccs.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() { while let Some(ev) = f() {
if ev.is_cc() { if ev.is_cc() {
self.exec_ctx.midi_ccs.push(ev); self.exec_ctx.midi_ccs.push(ev);
@ -373,6 +399,8 @@ impl NodeExecutor {
self.exec_ctx.midi_notes.push(ev); 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 { if self.exec_ctx.midi_ccs.len() == MAX_MIDI_CC_PER_BLOCK {
break; break;
} }
@ -382,11 +410,6 @@ impl NodeExecutor {
} }
} }
#[inline]
pub fn get_note_buffer(&mut self) -> &mut NoteBuffer {
&mut self.exec_ctx.note_buffer
}
#[inline] #[inline]
pub fn get_nodes(&self) -> &Vec<Node> { pub fn get_nodes(&self) -> &Vec<Node> {
&self.nodes &self.nodes
@ -553,7 +576,7 @@ impl NodeExecutor {
&mut self, &mut self,
seconds: f32, seconds: f32,
realtime: bool, realtime: bool,
mut events: Vec<HxTimedEvent>, events: &[HxTimedEvent],
) -> (Vec<f32>, Vec<f32>) { ) -> (Vec<f32>, Vec<f32>) {
const SAMPLE_RATE: f32 = 44100.0; const SAMPLE_RATE: f32 = 44100.0;
self.set_sample_rate(SAMPLE_RATE); self.set_sample_rate(SAMPLE_RATE);
@ -571,6 +594,7 @@ impl NodeExecutor {
output_l[i] = 0.0; output_l[i] = 0.0;
output_r[i] = 0.0; output_r[i] = 0.0;
} }
let mut ev_idx = 0;
let mut offs = 0; let mut offs = 0;
while nframes > 0 { while nframes > 0 {
let cur_nframes = if nframes >= MAX_BLOCK_SIZE { MAX_BLOCK_SIZE } else { nframes }; let cur_nframes = if nframes >= MAX_BLOCK_SIZE { MAX_BLOCK_SIZE } else { nframes };
@ -578,11 +602,12 @@ impl NodeExecutor {
self.feed_midi_events_from(|| { self.feed_midi_events_from(|| {
if ev_win.feed_me() { if ev_win.feed_me() {
if events.is_empty() { if ev_idx >= events.len() {
return None; 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) ev_win.next_event_in_range(offs, cur_nframes)

View file

@ -1,219 +0,0 @@
// Copyright (c) 2022 Weird Constructor <weirdconstructor@gmail.com>
// 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<HxMidiEvent> {
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<N={},G={},V={:5.3}>", 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<NoteChannelState>,
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<HxTimedEvent>,
}
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<HxTimedEvent> {
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<N=10,G=1,V=0.600>");
assert_eq!(buf.get_chan_at(0, 12).to_string(), "C<N=0,G=0,V=0.600>");
assert_eq!(buf.get_chan_at(2, 0).to_string(), "C<N=12,G=1,V=0.800>");
assert_eq!(buf.get_chan_at(2, 127).to_string(), "C<N=12,G=1,V=0.400>");
}
}

View file

@ -42,6 +42,7 @@ impl Smoother {
self.done = false; self.done = false;
} }
#[allow(dead_code)]
#[inline] #[inline]
pub fn current(&self) -> f32 { pub fn current(&self) -> f32 {
if self.done { if self.done {

View file

@ -976,7 +976,7 @@ fn check_matrix_output_feedback() {
matrix.set_param(gain_p, SAtom::param(0.25)); matrix.set_param(gain_p, SAtom::param(0.25));
for _ in 0..10 { for _ in 0..10 {
node_exec.test_run(0.11, true); node_exec.test_run(0.11, true, &[]);
matrix.update_filters(); matrix.update_filters();
matrix.filtered_out_fb_for(&sin, sin.out("sig").unwrap()); matrix.filtered_out_fb_for(&sin, sin.out("sig").unwrap());
matrix.filtered_out_fb_for(&amp, amp.out("sig").unwrap()); matrix.filtered_out_fb_for(&amp, amp.out("sig").unwrap());

View file

@ -533,7 +533,7 @@ pub fn run_realtime_no_input(
seconds: f32, seconds: f32,
sleep_a_bit: bool, sleep_a_bit: bool,
) -> (Vec<f32>, Vec<f32>) { ) -> (Vec<f32>, Vec<f32>) {
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)> { pub fn calc_rms_mimax_each_ms(buf: &[f32], ms: f32) -> Vec<(f32, f32, f32)> {

46
tests/matrix_observer.rs Normal file
View file

@ -0,0 +1,46 @@
// Copyright (c) 2022 Weird Constructor <weirdconstructor@gmail.com>
// 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<Vec<String>>,
}
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 }");
}

View file

@ -20,7 +20,7 @@ fn check_node_midip_gate_inserts() {
let (ch1, _) = node_exec.test_run( let (ch1, _) = node_exec.test_run(
0.005, 0.005,
false, false,
vec![ &[
HxTimedEvent::note_on(5, 0, 69, 1.0), HxTimedEvent::note_on(5, 0, 69, 1.0),
HxTimedEvent::note_on(10, 0, 68, 1.0), HxTimedEvent::note_on(10, 0, 68, 1.0),
HxTimedEvent::note_on(130, 0, 57, 1.0), HxTimedEvent::note_on(130, 0, 57, 1.0),
@ -32,7 +32,7 @@ fn check_node_midip_gate_inserts() {
assert_eq!( assert_eq!(
changes, changes,
vec![ &[
(5, 100), // First note triggers right (5, 100), // First note triggers right
(11, 100), // Second note needs to shortly pause the gate, which has 1 sample delay (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. (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( let (ch1, _) = node_exec.test_run(
0.005, 0.005,
false, false,
vec![ &[
HxTimedEvent::note_on(5, 0, 69, 1.0), HxTimedEvent::note_on(5, 0, 69, 1.0),
HxTimedEvent::note_on(10, 0, 68, 1.0), HxTimedEvent::note_on(10, 0, 68, 1.0),
HxTimedEvent::note_on(130, 0, 57, 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( let (ch1, _) = node_exec.test_run(
0.005, 0.005,
false, false,
vec![ &[
HxTimedEvent::note_on(5, 0, 69, 1.0), HxTimedEvent::note_on(5, 0, 69, 1.0),
HxTimedEvent::note_on(10, 0, 68, 1.0), HxTimedEvent::note_on(10, 0, 68, 1.0),
HxTimedEvent::note_on(130, 0, 57, 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( let (ch1, _) = node_exec.test_run(
0.005, 0.005,
false, false,
vec![ &[
HxTimedEvent::note_on(5, 0, 69, 0.4), HxTimedEvent::note_on(5, 0, 69, 0.4),
HxTimedEvent::note_on(10, 0, 68, 1.0), HxTimedEvent::note_on(10, 0, 68, 1.0),
HxTimedEvent::note_on(130, 0, 57, 0.6), 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( let (ch1, _) = node_exec.test_run(
0.005, 0.005,
false, false,
vec![ &[
HxTimedEvent::note_on(0, 0, 69, 1.0), HxTimedEvent::note_on(0, 0, 69, 1.0),
HxTimedEvent::note_off(5, 0, 69), HxTimedEvent::note_off(5, 0, 69),
HxTimedEvent::note_on(5, 0, 68, 1.0), HxTimedEvent::note_on(5, 0, 68, 1.0),