Added MIDI events to/from frontend thread
This commit is contained in:
parent
db2e5b13ed
commit
c175594b1f
15 changed files with 270 additions and 261 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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)]
|
||||
|
|
104
src/nodes/midi.rs
Normal file
104
src/nodes/midi.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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<Arc<AtomicFloat>>,
|
||||
/// For updating the NodeExecutor with graph updates.
|
||||
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.
|
||||
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<GraphEvent> {
|
||||
self.shared.graph_event_cons.pop()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<HxMidiEvent>,
|
||||
|
||||
/// 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<GraphMessage>,
|
||||
/// For receiving deleted/overwritten nodes from the backend thread.
|
||||
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.
|
||||
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<FeedbackBuffer>,
|
||||
pub note_buffer: NoteBuffer,
|
||||
pub midi_notes: Vec<HxTimedEvent>,
|
||||
pub midi_ccs: Vec<HxTimedEvent>,
|
||||
}
|
||||
|
@ -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<Node> {
|
||||
&self.nodes
|
||||
|
@ -553,7 +576,7 @@ impl NodeExecutor {
|
|||
&mut self,
|
||||
seconds: f32,
|
||||
realtime: bool,
|
||||
mut events: Vec<HxTimedEvent>,
|
||||
events: &[HxTimedEvent],
|
||||
) -> (Vec<f32>, Vec<f32>) {
|
||||
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)
|
||||
|
|
|
@ -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>");
|
||||
}
|
||||
}
|
|
@ -42,6 +42,7 @@ impl Smoother {
|
|||
self.done = false;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[inline]
|
||||
pub fn current(&self) -> f32 {
|
||||
if self.done {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -533,7 +533,7 @@ pub fn run_realtime_no_input(
|
|||
seconds: f32,
|
||||
sleep_a_bit: bool,
|
||||
) -> (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)> {
|
||||
|
|
46
tests/matrix_observer.rs
Normal file
46
tests/matrix_observer.rs
Normal 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 }");
|
||||
}
|
|
@ -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),
|
||||
|
|
Loading…
Reference in a new issue