Fix: MIDI precision
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -1824,8 +1824,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "doux"
|
name = "doux"
|
||||||
version = "0.0.14"
|
version = "0.0.15"
|
||||||
source = "git+https://github.com/sova-org/doux?tag=v0.0.14#f0de4f4047adfced8fb2116edd3b33d260ba75c8"
|
source = "git+https://github.com/sova-org/doux?tag=v0.0.15#1f11f795b877d9c15f65d88eb5576e8149092b17"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"clap",
|
"clap",
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ cagire-forth = { path = "crates/forth" }
|
|||||||
cagire-markdown = { path = "crates/markdown" }
|
cagire-markdown = { path = "crates/markdown" }
|
||||||
cagire-project = { path = "crates/project" }
|
cagire-project = { path = "crates/project" }
|
||||||
cagire-ratatui = { path = "crates/ratatui" }
|
cagire-ratatui = { path = "crates/ratatui" }
|
||||||
doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.14", features = ["native", "soundfont"] }
|
doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.15", features = ["native", "soundfont"] }
|
||||||
rusty_link = "0.4"
|
rusty_link = "0.4"
|
||||||
ratatui = "0.30"
|
ratatui = "0.30"
|
||||||
crossterm = "0.29"
|
crossterm = "0.29"
|
||||||
|
|||||||
@@ -1187,7 +1187,7 @@ impl Forth {
|
|||||||
}
|
}
|
||||||
let dur = steps * ctx.step_duration();
|
let dur = steps * ctx.step_duration();
|
||||||
cmd.set_param("fit", Value::Float(dur, None));
|
cmd.set_param("fit", Value::Float(dur, None));
|
||||||
cmd.set_param("dur", Value::Float(dur, None));
|
cmd.set_param("gate", Value::Float(steps, None));
|
||||||
}
|
}
|
||||||
|
|
||||||
Op::At => {
|
Op::At => {
|
||||||
@@ -1753,7 +1753,7 @@ fn cmd_param_float(cmd: &CmdRegister, name: &str) -> Option<f64> {
|
|||||||
fn is_tempo_scaled_param(name: &str) -> bool {
|
fn is_tempo_scaled_param(name: &str) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
name,
|
name,
|
||||||
"attack" | "decay" | "release" | "envdelay" | "hold" | "chorusdelay"
|
"attack" | "decay" | "release" | "envdelay" | "hold" | "chorusdelay" | "gate"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1769,7 +1769,7 @@ fn emit_output(
|
|||||||
let mut out = String::with_capacity(128);
|
let mut out = String::with_capacity(128);
|
||||||
out.push('/');
|
out.push('/');
|
||||||
|
|
||||||
let has_dur = params.iter().any(|(k, _)| *k == "dur");
|
let has_gate = params.iter().any(|(k, _)| *k == "gate");
|
||||||
let has_release = params.iter().any(|(k, _)| *k == "release");
|
let has_release = params.iter().any(|(k, _)| *k == "release");
|
||||||
let delaytime_idx = params.iter().position(|(k, _)| *k == "delaytime");
|
let delaytime_idx = params.iter().position(|(k, _)| *k == "delaytime");
|
||||||
|
|
||||||
@@ -1806,11 +1806,11 @@ fn emit_output(
|
|||||||
let _ = write!(&mut out, "delta/{delta_ticks}");
|
let _ = write!(&mut out, "delta/{delta_ticks}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if !has_dur {
|
if !has_gate {
|
||||||
if !out.ends_with('/') {
|
if !out.ends_with('/') {
|
||||||
out.push('/');
|
out.push('/');
|
||||||
}
|
}
|
||||||
let _ = write!(&mut out, "dur/{}", step_duration * 4.0);
|
let _ = write!(&mut out, "gate/{}", step_duration * 4.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !has_release {
|
if !has_release {
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ pub(super) const WORDS: &[Word] = &[
|
|||||||
aliases: &[],
|
aliases: &[],
|
||||||
category: "Sample",
|
category: "Sample",
|
||||||
stack: "(v.. --)",
|
stack: "(v.. --)",
|
||||||
desc: "Set duration",
|
desc: "Set MIDI note duration (for audio, use gate)",
|
||||||
example: "0.5 dur",
|
example: "0.5 dur",
|
||||||
compile: Param,
|
compile: Param,
|
||||||
varargs: true,
|
varargs: true,
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ mod tests {
|
|||||||
for i in 0..16 {
|
for i in 0..16 {
|
||||||
pattern.steps[i] = Step {
|
pattern.steps[i] = Step {
|
||||||
active: true,
|
active: true,
|
||||||
script: format!("kick {i} note 0.5 dur"),
|
script: format!("kick {i} note 0.5 gate"),
|
||||||
source: None,
|
source: None,
|
||||||
name: Some(format!("step_{i}")),
|
name: Some(format!("step_{i}")),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ Cagire includes a complete synthesis and sampling engine. No external software i
|
|||||||
|
|
||||||
```forth
|
```forth
|
||||||
;; sawtooth wave + lowpass filter with envelope + chorus + reverb
|
;; sawtooth wave + lowpass filter with envelope + chorus + reverb
|
||||||
100 199 freq saw sound 250 8000 0.01 0.3 0.5 0.3 env lpf 0.2 chorus 0.8 verb 2 dur .
|
100 199 freq saw sound 250 8000 0.01 0.3 0.5 0.3 env lpf 0.2 chorus 0.8 verb 2 gate .
|
||||||
```
|
```
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
@@ -61,7 +61,7 @@ Cagire includes a complete synthesis and sampling engine. No external software i
|
|||||||
|
|
||||||
```forth
|
```forth
|
||||||
;; white noise + sine wave + envelope = percussion
|
;; white noise + sine wave + envelope = percussion
|
||||||
white sine sound 100 freq 0.5 decay 2 dur .
|
white sine sound 100 freq 0.5 decay 2 gate .
|
||||||
```
|
```
|
||||||
|
|
||||||
```forth
|
```forth
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ cagire = { path = "../..", default-features = false, features = ["block-renderer
|
|||||||
cagire-forth = { path = "../../crates/forth" }
|
cagire-forth = { path = "../../crates/forth" }
|
||||||
cagire-project = { path = "../../crates/project" }
|
cagire-project = { path = "../../crates/project" }
|
||||||
cagire-ratatui = { path = "../../crates/ratatui" }
|
cagire-ratatui = { path = "../../crates/ratatui" }
|
||||||
doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.14", features = ["native", "soundfont"] }
|
doux = { git = "https://github.com/sova-org/doux", tag = "v0.0.15", features = ["native", "soundfont"] }
|
||||||
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", features = ["standalone"] }
|
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", features = ["standalone"] }
|
||||||
nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug" }
|
nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug" }
|
||||||
egui_ratatui = "2.1"
|
egui_ratatui = "2.1"
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ impl App {
|
|||||||
output_devices: {
|
output_devices: {
|
||||||
let outputs = crate::midi::list_midi_outputs();
|
let outputs = crate::midi::list_midi_outputs();
|
||||||
self.midi
|
self.midi
|
||||||
.selected_outputs
|
.selected_outputs()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|opt| {
|
.map(|opt| {
|
||||||
opt.and_then(|idx| outputs.get(idx).map(|d| d.name.clone()))
|
opt.and_then(|idx| outputs.get(idx).map(|d| d.name.clone()))
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use soft_ratatui::embedded_graphics_unicodefonts::{
|
|||||||
use soft_ratatui::{EmbeddedGraphics, SoftBackend};
|
use soft_ratatui::{EmbeddedGraphics, SoftBackend};
|
||||||
|
|
||||||
use cagire::engine::{
|
use cagire::engine::{
|
||||||
build_stream, AnalysisHandle, AudioStreamConfig, LinkState, MidiCommand, ScopeBuffer,
|
build_stream, AnalysisHandle, AudioStreamConfig, LinkState, ScopeBuffer,
|
||||||
SequencerHandle, SpectrumBuffer,
|
SequencerHandle, SpectrumBuffer,
|
||||||
};
|
};
|
||||||
use cagire::init::{init, InitArgs};
|
use cagire::init::{init, InitArgs};
|
||||||
@@ -27,7 +27,6 @@ use cagire::input::{handle_key, handle_mouse, InputContext, InputResult};
|
|||||||
use cagire::input_egui::{convert_egui_events, convert_egui_mouse, EguiMouseState};
|
use cagire::input_egui::{convert_egui_events, convert_egui_mouse, EguiMouseState};
|
||||||
use cagire::settings::Settings;
|
use cagire::settings::Settings;
|
||||||
use cagire::views;
|
use cagire::views;
|
||||||
use crossbeam_channel::Receiver;
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "cagire-desktop", about = "Cagire desktop application")]
|
#[command(name = "cagire-desktop", about = "Cagire desktop application")]
|
||||||
@@ -160,7 +159,6 @@ struct CagireDesktop {
|
|||||||
_stream: Option<cpal::Stream>,
|
_stream: Option<cpal::Stream>,
|
||||||
_input_stream: Option<cpal::Stream>,
|
_input_stream: Option<cpal::Stream>,
|
||||||
_analysis_handle: Option<AnalysisHandle>,
|
_analysis_handle: Option<AnalysisHandle>,
|
||||||
midi_rx: Receiver<MidiCommand>,
|
|
||||||
device_lost: Arc<AtomicBool>,
|
device_lost: Arc<AtomicBool>,
|
||||||
stream_error_rx: crossbeam_channel::Receiver<String>,
|
stream_error_rx: crossbeam_channel::Receiver<String>,
|
||||||
current_font: FontChoice,
|
current_font: FontChoice,
|
||||||
@@ -207,7 +205,6 @@ impl CagireDesktop {
|
|||||||
_stream: b.stream,
|
_stream: b.stream,
|
||||||
_input_stream: b.input_stream,
|
_input_stream: b.input_stream,
|
||||||
_analysis_handle: b.analysis_handle,
|
_analysis_handle: b.analysis_handle,
|
||||||
midi_rx: b.midi_rx,
|
|
||||||
device_lost: b.device_lost,
|
device_lost: b.device_lost,
|
||||||
stream_error_rx: b.stream_error_rx,
|
stream_error_rx: b.stream_error_rx,
|
||||||
current_font,
|
current_font,
|
||||||
@@ -237,7 +234,6 @@ impl CagireDesktop {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let new_audio_rx = sequencer.swap_audio_channel();
|
let new_audio_rx = sequencer.swap_audio_channel();
|
||||||
self.midi_rx = sequencer.swap_midi_channel();
|
|
||||||
|
|
||||||
let new_config = AudioStreamConfig {
|
let new_config = AudioStreamConfig {
|
||||||
output_device: self.app.audio.config.output_device.clone(),
|
output_device: self.app.audio.config.output_device.clone(),
|
||||||
@@ -288,6 +284,7 @@ impl CagireDesktop {
|
|||||||
self.app.audio.config.sample_rate = info.sample_rate;
|
self.app.audio.config.sample_rate = info.sample_rate;
|
||||||
self.app.audio.config.host_name = info.host_name;
|
self.app.audio.config.host_name = info.host_name;
|
||||||
self.app.audio.config.channels = info.channels;
|
self.app.audio.config.channels = info.channels;
|
||||||
|
self.app.audio.config.input_sample_rate = info.input_sample_rate;
|
||||||
self.sample_rate_shared
|
self.sample_rate_shared
|
||||||
.store(info.sample_rate as u32, Ordering::Relaxed);
|
.store(info.sample_rate as u32, Ordering::Relaxed);
|
||||||
self.app.audio.error = None;
|
self.app.audio.error = None;
|
||||||
@@ -414,59 +411,6 @@ impl eframe::App for CagireDesktop {
|
|||||||
self.app.flush_dirty_patterns(&sequencer.cmd_tx);
|
self.app.flush_dirty_patterns(&sequencer.cmd_tx);
|
||||||
self.app.flush_dirty_script(&sequencer.cmd_tx);
|
self.app.flush_dirty_script(&sequencer.cmd_tx);
|
||||||
|
|
||||||
while let Ok(midi_cmd) = self.midi_rx.try_recv() {
|
|
||||||
match midi_cmd {
|
|
||||||
MidiCommand::NoteOn {
|
|
||||||
device,
|
|
||||||
channel,
|
|
||||||
note,
|
|
||||||
velocity,
|
|
||||||
} => {
|
|
||||||
self.app.midi.send_note_on(device, channel, note, velocity);
|
|
||||||
}
|
|
||||||
MidiCommand::NoteOff {
|
|
||||||
device,
|
|
||||||
channel,
|
|
||||||
note,
|
|
||||||
} => {
|
|
||||||
self.app.midi.send_note_off(device, channel, note);
|
|
||||||
}
|
|
||||||
MidiCommand::CC {
|
|
||||||
device,
|
|
||||||
channel,
|
|
||||||
cc,
|
|
||||||
value,
|
|
||||||
} => {
|
|
||||||
self.app.midi.send_cc(device, channel, cc, value);
|
|
||||||
}
|
|
||||||
MidiCommand::PitchBend {
|
|
||||||
device,
|
|
||||||
channel,
|
|
||||||
value,
|
|
||||||
} => {
|
|
||||||
self.app.midi.send_pitch_bend(device, channel, value);
|
|
||||||
}
|
|
||||||
MidiCommand::Pressure {
|
|
||||||
device,
|
|
||||||
channel,
|
|
||||||
value,
|
|
||||||
} => {
|
|
||||||
self.app.midi.send_pressure(device, channel, value);
|
|
||||||
}
|
|
||||||
MidiCommand::ProgramChange {
|
|
||||||
device,
|
|
||||||
channel,
|
|
||||||
program,
|
|
||||||
} => {
|
|
||||||
self.app.midi.send_program_change(device, channel, program);
|
|
||||||
}
|
|
||||||
MidiCommand::Clock { device } => self.app.midi.send_realtime(device, 0xF8),
|
|
||||||
MidiCommand::Start { device } => self.app.midi.send_realtime(device, 0xFA),
|
|
||||||
MidiCommand::Stop { device } => self.app.midi.send_realtime(device, 0xFC),
|
|
||||||
MidiCommand::Continue { device } => self.app.midi.send_realtime(device, 0xFB),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let should_quit = self.handle_input(ctx);
|
let should_quit = self.handle_input(ctx);
|
||||||
if should_quit {
|
if should_quit {
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||||
|
|||||||
@@ -282,6 +282,7 @@ pub struct AudioStreamInfo {
|
|||||||
pub sample_rate: f32,
|
pub sample_rate: f32,
|
||||||
pub host_name: String,
|
pub host_name: String,
|
||||||
pub channels: u16,
|
pub channels: u16,
|
||||||
|
pub input_sample_rate: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "cli")]
|
#[cfg(feature = "cli")]
|
||||||
@@ -367,10 +368,16 @@ pub fn build_stream(
|
|||||||
dev
|
dev
|
||||||
});
|
});
|
||||||
|
|
||||||
let input_channels: usize = input_device
|
let input_config = input_device
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|dev| dev.default_input_config().ok());
|
||||||
|
let input_channels: usize = input_config
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|dev| dev.default_input_config().ok())
|
|
||||||
.map_or(0, |cfg| cfg.channels() as usize);
|
.map_or(0, |cfg| cfg.channels() as usize);
|
||||||
|
let input_sample_rate = input_config.and_then(|cfg| {
|
||||||
|
let rate = cfg.sample_rate() as f32;
|
||||||
|
(rate != sample_rate).then_some(rate)
|
||||||
|
});
|
||||||
|
|
||||||
engine.input_channels = input_channels;
|
engine.input_channels = input_channels;
|
||||||
|
|
||||||
@@ -519,6 +526,7 @@ pub fn build_stream(
|
|||||||
sample_rate,
|
sample_rate,
|
||||||
host_name,
|
host_name,
|
||||||
channels: effective_channels,
|
channels: effective_channels,
|
||||||
|
input_sample_rate,
|
||||||
};
|
};
|
||||||
Ok((stream, input_stream, info, analysis_handle, registry))
|
Ok((stream, input_stream, info, analysis_handle, registry))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
use arc_swap::ArcSwap;
|
use crossbeam_channel::{Receiver, RecvTimeoutError};
|
||||||
use crossbeam_channel::{Receiver, RecvTimeoutError, Sender};
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::collections::BinaryHeap;
|
use std::collections::BinaryHeap;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use super::link::LinkState;
|
use super::link::LinkState;
|
||||||
use super::realtime::{precise_sleep_us, set_realtime_priority, warn_no_rt};
|
use super::realtime::{precise_sleep_us, set_realtime_priority, warn_no_rt};
|
||||||
use super::sequencer::MidiCommand;
|
use super::sequencer::MidiCommand;
|
||||||
use super::timing::SyncTime;
|
use super::timing::SyncTime;
|
||||||
|
use crate::midi::{MidiOutputPorts, MAX_MIDI_OUTPUTS};
|
||||||
|
|
||||||
/// A MIDI command scheduled for dispatch at a specific time.
|
/// A MIDI command scheduled for dispatch at a specific time.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -46,13 +45,13 @@ impl Eq for TimedMidiCommand {}
|
|||||||
|
|
||||||
const SPIN_THRESHOLD_US: SyncTime = 100;
|
const SPIN_THRESHOLD_US: SyncTime = 100;
|
||||||
|
|
||||||
/// Dispatcher loop — handles MIDI timing only.
|
/// Dispatcher loop — handles MIDI timing and sends directly to MIDI ports.
|
||||||
/// Audio commands bypass the dispatcher entirely and go straight to doux's
|
/// Audio commands bypass the dispatcher entirely and go straight to doux's
|
||||||
/// sample-accurate scheduler via the audio thread channel.
|
/// sample-accurate scheduler via the audio thread channel.
|
||||||
pub fn dispatcher_loop(
|
pub fn dispatcher_loop(
|
||||||
cmd_rx: Receiver<TimedMidiCommand>,
|
cmd_rx: Receiver<TimedMidiCommand>,
|
||||||
midi_tx: Arc<ArcSwap<Sender<MidiCommand>>>,
|
ports: MidiOutputPorts,
|
||||||
link: Arc<LinkState>,
|
link: &LinkState,
|
||||||
) {
|
) {
|
||||||
let has_rt = set_realtime_priority();
|
let has_rt = set_realtime_priority();
|
||||||
if !has_rt {
|
if !has_rt {
|
||||||
@@ -84,8 +83,8 @@ pub fn dispatcher_loop(
|
|||||||
while let Some(cmd) = queue.peek() {
|
while let Some(cmd) = queue.peek() {
|
||||||
if cmd.target_time_us <= current_us + SPIN_THRESHOLD_US {
|
if cmd.target_time_us <= current_us + SPIN_THRESHOLD_US {
|
||||||
let cmd = queue.pop().expect("pop after peek");
|
let cmd = queue.pop().expect("pop after peek");
|
||||||
wait_until_dispatch(cmd.target_time_us, &link, has_rt);
|
wait_until_dispatch(cmd.target_time_us, link, has_rt);
|
||||||
dispatch_midi(cmd.command, &midi_tx);
|
dispatch_midi(cmd.command, &ports);
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -106,15 +105,15 @@ fn wait_until_dispatch(target_us: SyncTime, link: &LinkState, has_rt: bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_midi(cmd: MidiDispatch, midi_tx: &Arc<ArcSwap<Sender<MidiCommand>>>) {
|
fn dispatch_midi(cmd: MidiDispatch, ports: &MidiOutputPorts) {
|
||||||
match cmd {
|
match cmd {
|
||||||
MidiDispatch::Send(midi_cmd) => {
|
MidiDispatch::Send(midi_cmd) => {
|
||||||
let _ = midi_tx.load().try_send(midi_cmd);
|
ports.send_command(&midi_cmd);
|
||||||
}
|
}
|
||||||
MidiDispatch::FlushAll => {
|
MidiDispatch::FlushAll => {
|
||||||
for dev in 0..4u8 {
|
for dev in 0..MAX_MIDI_OUTPUTS as u8 {
|
||||||
for chan in 0..16u8 {
|
for chan in 0..16u8 {
|
||||||
let _ = midi_tx.load().try_send(MidiCommand::CC {
|
ports.send_command(&MidiCommand::CC {
|
||||||
device: dev,
|
device: dev,
|
||||||
channel: chan,
|
channel: chan,
|
||||||
cc: 123,
|
cc: 123,
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ pub use audio::AudioStreamInfo;
|
|||||||
|
|
||||||
pub use link::LinkState;
|
pub use link::LinkState;
|
||||||
pub use sequencer::{
|
pub use sequencer::{
|
||||||
spawn_sequencer, AudioCommand, MidiCommand, PatternChange, PatternSnapshot, SeqCommand,
|
spawn_sequencer, AudioCommand, PatternChange, PatternSnapshot, SeqCommand,
|
||||||
SequencerConfig, SequencerHandle, SequencerSnapshot, StepSnapshot,
|
SequencerConfig, SequencerHandle, SequencerSnapshot, StepSnapshot,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Re-exported for the plugin crate (not used by the terminal binary).
|
// Re-exported for the plugin crate (not used by the terminal binary).
|
||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
pub use sequencer::{
|
pub use sequencer::{
|
||||||
parse_midi_command, SequencerState, SharedSequencerState, TickInput, TickOutput,
|
parse_midi_command, MidiCommand, SequencerState, SharedSequencerState, TickInput, TickOutput,
|
||||||
TimestampedCommand,
|
TimestampedCommand,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -261,7 +261,6 @@ impl SequencerSnapshot {
|
|||||||
pub struct SequencerHandle {
|
pub struct SequencerHandle {
|
||||||
pub cmd_tx: Sender<SeqCommand>,
|
pub cmd_tx: Sender<SeqCommand>,
|
||||||
pub audio_tx: Arc<ArcSwap<Sender<AudioCommand>>>,
|
pub audio_tx: Arc<ArcSwap<Sender<AudioCommand>>>,
|
||||||
pub midi_tx: Arc<ArcSwap<Sender<MidiCommand>>>,
|
|
||||||
shared_state: Arc<ArcSwap<SharedSequencerState>>,
|
shared_state: Arc<ArcSwap<SharedSequencerState>>,
|
||||||
thread: JoinHandle<()>,
|
thread: JoinHandle<()>,
|
||||||
}
|
}
|
||||||
@@ -278,12 +277,6 @@ impl SequencerHandle {
|
|||||||
new_rx
|
new_rx
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn swap_midi_channel(&self) -> Receiver<MidiCommand> {
|
|
||||||
let (new_tx, new_rx) = bounded::<MidiCommand>(256);
|
|
||||||
self.midi_tx.store(Arc::new(new_tx));
|
|
||||||
new_rx
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn shutdown(self) {
|
pub fn shutdown(self) {
|
||||||
let _ = self.cmd_tx.send(SeqCommand::Shutdown);
|
let _ = self.cmd_tx.send(SeqCommand::Shutdown);
|
||||||
if let Err(e) = self.thread.join() {
|
if let Err(e) = self.thread.join() {
|
||||||
@@ -356,16 +349,11 @@ pub fn spawn_sequencer(
|
|||||||
live_keys: Arc<LiveKeyState>,
|
live_keys: Arc<LiveKeyState>,
|
||||||
nudge_us: Arc<AtomicI64>,
|
nudge_us: Arc<AtomicI64>,
|
||||||
config: SequencerConfig,
|
config: SequencerConfig,
|
||||||
) -> (
|
midi_ports: crate::midi::MidiOutputPorts,
|
||||||
SequencerHandle,
|
) -> (SequencerHandle, Receiver<AudioCommand>) {
|
||||||
Receiver<AudioCommand>,
|
|
||||||
Receiver<MidiCommand>,
|
|
||||||
) {
|
|
||||||
let (cmd_tx, cmd_rx) = bounded::<SeqCommand>(64);
|
let (cmd_tx, cmd_rx) = bounded::<SeqCommand>(64);
|
||||||
let (audio_tx, audio_rx) = unbounded::<AudioCommand>();
|
let (audio_tx, audio_rx) = unbounded::<AudioCommand>();
|
||||||
let (midi_tx, midi_rx) = bounded::<MidiCommand>(256);
|
|
||||||
let audio_tx = Arc::new(ArcSwap::from_pointee(audio_tx));
|
let audio_tx = Arc::new(ArcSwap::from_pointee(audio_tx));
|
||||||
let midi_tx = Arc::new(ArcSwap::from_pointee(midi_tx));
|
|
||||||
|
|
||||||
// Dispatcher channel — MIDI only (unbounded to avoid blocking the scheduler)
|
// Dispatcher channel — MIDI only (unbounded to avoid blocking the scheduler)
|
||||||
let (dispatch_tx, dispatch_rx) = unbounded::<TimedMidiCommand>();
|
let (dispatch_tx, dispatch_rx) = unbounded::<TimedMidiCommand>();
|
||||||
@@ -382,13 +370,12 @@ pub fn spawn_sequencer(
|
|||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
let mouse_down = config.mouse_down;
|
let mouse_down = config.mouse_down;
|
||||||
|
|
||||||
// Spawn dispatcher thread (MIDI only — audio goes direct to doux)
|
// Spawn dispatcher thread — sends MIDI directly to ports (no UI-thread hop)
|
||||||
let dispatcher_link = Arc::clone(&link);
|
let dispatcher_link = Arc::clone(&link);
|
||||||
let dispatcher_midi_tx = Arc::clone(&midi_tx);
|
|
||||||
thread::Builder::new()
|
thread::Builder::new()
|
||||||
.name("cagire-dispatcher".into())
|
.name("cagire-dispatcher".into())
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
dispatcher_loop(dispatch_rx, dispatcher_midi_tx, dispatcher_link);
|
dispatcher_loop(dispatch_rx, midi_ports, &dispatcher_link);
|
||||||
})
|
})
|
||||||
.expect("Failed to spawn dispatcher thread");
|
.expect("Failed to spawn dispatcher thread");
|
||||||
|
|
||||||
@@ -424,11 +411,10 @@ pub fn spawn_sequencer(
|
|||||||
let handle = SequencerHandle {
|
let handle = SequencerHandle {
|
||||||
cmd_tx,
|
cmd_tx,
|
||||||
audio_tx,
|
audio_tx,
|
||||||
midi_tx,
|
|
||||||
shared_state,
|
shared_state,
|
||||||
thread,
|
thread,
|
||||||
};
|
};
|
||||||
(handle, audio_rx, midi_rx)
|
(handle, audio_rx)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PatternCache {
|
struct PatternCache {
|
||||||
|
|||||||
@@ -2,13 +2,12 @@ use std::path::PathBuf;
|
|||||||
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, AtomicU64, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crossbeam_channel::Receiver;
|
|
||||||
use doux::EngineMetrics;
|
use doux::EngineMetrics;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::engine::{
|
use crate::engine::{
|
||||||
build_stream, preload_sample_heads, spawn_sequencer, AnalysisHandle, AudioStreamConfig,
|
build_stream, preload_sample_heads, spawn_sequencer, AnalysisHandle, AudioStreamConfig,
|
||||||
LinkState, MidiCommand, PatternChange, ScopeBuffer, SequencerConfig, SequencerHandle,
|
LinkState, PatternChange, ScopeBuffer, SequencerConfig, SequencerHandle,
|
||||||
SpectrumBuffer,
|
SpectrumBuffer,
|
||||||
};
|
};
|
||||||
use crate::midi;
|
use crate::midi;
|
||||||
@@ -42,7 +41,6 @@ pub struct Init {
|
|||||||
pub stream: Option<cpal::Stream>,
|
pub stream: Option<cpal::Stream>,
|
||||||
pub input_stream: Option<cpal::Stream>,
|
pub input_stream: Option<cpal::Stream>,
|
||||||
pub analysis_handle: Option<AnalysisHandle>,
|
pub analysis_handle: Option<AnalysisHandle>,
|
||||||
pub midi_rx: Receiver<MidiCommand>,
|
|
||||||
pub device_lost: Arc<AtomicBool>,
|
pub device_lost: Arc<AtomicBool>,
|
||||||
pub stream_error_rx: crossbeam_channel::Receiver<String>,
|
pub stream_error_rx: crossbeam_channel::Receiver<String>,
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
@@ -192,13 +190,14 @@ pub fn init(args: InitArgs) -> Init {
|
|||||||
mouse_down: Arc::clone(&mouse_down),
|
mouse_down: Arc::clone(&mouse_down),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (sequencer, initial_audio_rx, midi_rx) = spawn_sequencer(
|
let (sequencer, initial_audio_rx) = spawn_sequencer(
|
||||||
Arc::clone(&link),
|
Arc::clone(&link),
|
||||||
Arc::clone(&playing),
|
Arc::clone(&playing),
|
||||||
settings.link.quantum,
|
settings.link.quantum,
|
||||||
Arc::clone(&app.live_keys),
|
Arc::clone(&app.live_keys),
|
||||||
Arc::clone(&nudge_us),
|
Arc::clone(&nudge_us),
|
||||||
seq_config,
|
seq_config,
|
||||||
|
app.midi.output_ports.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let device_lost = Arc::new(AtomicBool::new(false));
|
let device_lost = Arc::new(AtomicBool::new(false));
|
||||||
@@ -228,6 +227,7 @@ pub fn init(args: InitArgs) -> Init {
|
|||||||
app.audio.config.sample_rate = info.sample_rate;
|
app.audio.config.sample_rate = info.sample_rate;
|
||||||
app.audio.config.host_name = info.host_name;
|
app.audio.config.host_name = info.host_name;
|
||||||
app.audio.config.channels = info.channels;
|
app.audio.config.channels = info.channels;
|
||||||
|
app.audio.config.input_sample_rate = info.input_sample_rate;
|
||||||
sample_rate_shared.store(info.sample_rate as u32, Ordering::Relaxed);
|
sample_rate_shared.store(info.sample_rate as u32, Ordering::Relaxed);
|
||||||
app.audio.sample_registry = Some(Arc::clone(®istry));
|
app.audio.sample_registry = Some(Arc::clone(®istry));
|
||||||
|
|
||||||
@@ -267,7 +267,6 @@ pub fn init(args: InitArgs) -> Init {
|
|||||||
stream,
|
stream,
|
||||||
input_stream,
|
input_stream,
|
||||||
analysis_handle,
|
analysis_handle,
|
||||||
midi_rx,
|
|
||||||
device_lost,
|
device_lost,
|
||||||
stream_error_rx,
|
stream_error_rx,
|
||||||
#[cfg(feature = "desktop")]
|
#[cfg(feature = "desktop")]
|
||||||
|
|||||||
@@ -48,22 +48,20 @@ pub(crate) fn cycle_link_setting(ctx: &mut InputContext, right: bool) {
|
|||||||
pub(crate) fn cycle_midi_output(ctx: &mut InputContext, right: bool) {
|
pub(crate) fn cycle_midi_output(ctx: &mut InputContext, right: bool) {
|
||||||
let slot = ctx.app.audio.midi_output_slot;
|
let slot = ctx.app.audio.midi_output_slot;
|
||||||
let all_devices = crate::midi::list_midi_outputs();
|
let all_devices = crate::midi::list_midi_outputs();
|
||||||
|
let selected = ctx.app.midi.selected_outputs();
|
||||||
let available: Vec<(usize, &crate::midi::MidiDeviceInfo)> = all_devices
|
let available: Vec<(usize, &crate::midi::MidiDeviceInfo)> = all_devices
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|(idx, _)| {
|
.filter(|(idx, _)| {
|
||||||
ctx.app.midi.selected_outputs[slot] == Some(*idx)
|
selected[slot] == Some(*idx)
|
||||||
|| !ctx
|
|| !selected
|
||||||
.app
|
|
||||||
.midi
|
|
||||||
.selected_outputs
|
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.any(|(s, sel)| s != slot && *sel == Some(*idx))
|
.any(|(s, sel)| s != slot && *sel == Some(*idx))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let total_options = available.len() + 1;
|
let total_options = available.len() + 1;
|
||||||
let current_pos = ctx.app.midi.selected_outputs[slot]
|
let current_pos = selected[slot]
|
||||||
.and_then(|idx| available.iter().position(|(i, _)| *i == idx))
|
.and_then(|idx| available.iter().position(|(i, _)| *i == idx))
|
||||||
.map(|p| p + 1)
|
.map(|p| p + 1)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
@@ -299,7 +297,7 @@ pub(super) fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> Input
|
|||||||
KeyCode::Char('r') => ctx.dispatch(AppCommand::ResetPeakVoices),
|
KeyCode::Char('r') => ctx.dispatch(AppCommand::ResetPeakVoices),
|
||||||
KeyCode::Char('t') if !ctx.app.plugin_mode => {
|
KeyCode::Char('t') if !ctx.app.plugin_mode => {
|
||||||
let _ = ctx.audio_tx.load().send(AudioCommand::Evaluate {
|
let _ = ctx.audio_tx.load().send(AudioCommand::Evaluate {
|
||||||
cmd: "/sound/sine/dur/0.5/decay/0.2".into(),
|
cmd: "/sound/sine/gate/0.5/decay/0.2".into(),
|
||||||
tick: None,
|
tick: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -790,7 +790,7 @@ fn execute_palette_entry(
|
|||||||
.audio_tx
|
.audio_tx
|
||||||
.load()
|
.load()
|
||||||
.send(crate::engine::AudioCommand::Evaluate {
|
.send(crate::engine::AudioCommand::Evaluate {
|
||||||
cmd: "/sound/sine/dur/0.5/decay/0.2".into(),
|
cmd: "/sound/sine/gate/0.5/decay/0.2".into(),
|
||||||
tick: None,
|
tick: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ pub(super) fn handle_sample_explorer(ctx: &mut InputContext, key: KeyEvent) -> I
|
|||||||
TreeLineKind::File => {
|
TreeLineKind::File => {
|
||||||
let folder = &entry.folder;
|
let folder = &entry.folder;
|
||||||
let idx = entry.index;
|
let idx = entry.index;
|
||||||
let cmd = format!("/sound/{folder}/n/{idx}/gain/1.00/dur/1");
|
let cmd = format!("/sound/{folder}/n/{idx}/gain/1.00/gate/1");
|
||||||
let _ = ctx
|
let _ = ctx
|
||||||
.audio_tx
|
.audio_tx
|
||||||
.load()
|
.load()
|
||||||
|
|||||||
56
src/main.rs
56
src/main.rs
@@ -101,7 +101,6 @@ fn main() -> io::Result<()> {
|
|||||||
let mut _stream = b.stream;
|
let mut _stream = b.stream;
|
||||||
let mut _input_stream = b.input_stream;
|
let mut _input_stream = b.input_stream;
|
||||||
let mut _analysis_handle = b.analysis_handle;
|
let mut _analysis_handle = b.analysis_handle;
|
||||||
let mut midi_rx = b.midi_rx;
|
|
||||||
let device_lost = b.device_lost;
|
let device_lost = b.device_lost;
|
||||||
let mut stream_error_rx = b.stream_error_rx;
|
let mut stream_error_rx = b.stream_error_rx;
|
||||||
|
|
||||||
@@ -125,7 +124,6 @@ fn main() -> io::Result<()> {
|
|||||||
_analysis_handle = None;
|
_analysis_handle = None;
|
||||||
|
|
||||||
let new_audio_rx = sequencer.swap_audio_channel();
|
let new_audio_rx = sequencer.swap_audio_channel();
|
||||||
midi_rx = sequencer.swap_midi_channel();
|
|
||||||
|
|
||||||
let new_config = AudioStreamConfig {
|
let new_config = AudioStreamConfig {
|
||||||
output_device: app.audio.config.output_device.clone(),
|
output_device: app.audio.config.output_device.clone(),
|
||||||
@@ -176,6 +174,7 @@ fn main() -> io::Result<()> {
|
|||||||
app.audio.config.sample_rate = info.sample_rate;
|
app.audio.config.sample_rate = info.sample_rate;
|
||||||
app.audio.config.host_name = info.host_name;
|
app.audio.config.host_name = info.host_name;
|
||||||
app.audio.config.channels = info.channels;
|
app.audio.config.channels = info.channels;
|
||||||
|
app.audio.config.input_sample_rate = info.input_sample_rate;
|
||||||
sample_rate_shared.store(info.sample_rate as u32, Ordering::Relaxed);
|
sample_rate_shared.store(info.sample_rate as u32, Ordering::Relaxed);
|
||||||
app.audio.error = None;
|
app.audio.error = None;
|
||||||
app.audio.sample_registry = Some(Arc::clone(®istry));
|
app.audio.sample_registry = Some(Arc::clone(®istry));
|
||||||
@@ -233,59 +232,6 @@ fn main() -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
was_playing = app.playback.playing;
|
was_playing = app.playback.playing;
|
||||||
|
|
||||||
while let Ok(midi_cmd) = midi_rx.try_recv() {
|
|
||||||
match midi_cmd {
|
|
||||||
engine::MidiCommand::NoteOn {
|
|
||||||
device,
|
|
||||||
channel,
|
|
||||||
note,
|
|
||||||
velocity,
|
|
||||||
} => {
|
|
||||||
app.midi.send_note_on(device, channel, note, velocity);
|
|
||||||
}
|
|
||||||
engine::MidiCommand::NoteOff {
|
|
||||||
device,
|
|
||||||
channel,
|
|
||||||
note,
|
|
||||||
} => {
|
|
||||||
app.midi.send_note_off(device, channel, note);
|
|
||||||
}
|
|
||||||
engine::MidiCommand::CC {
|
|
||||||
device,
|
|
||||||
channel,
|
|
||||||
cc,
|
|
||||||
value,
|
|
||||||
} => {
|
|
||||||
app.midi.send_cc(device, channel, cc, value);
|
|
||||||
}
|
|
||||||
engine::MidiCommand::PitchBend {
|
|
||||||
device,
|
|
||||||
channel,
|
|
||||||
value,
|
|
||||||
} => {
|
|
||||||
app.midi.send_pitch_bend(device, channel, value);
|
|
||||||
}
|
|
||||||
engine::MidiCommand::Pressure {
|
|
||||||
device,
|
|
||||||
channel,
|
|
||||||
value,
|
|
||||||
} => {
|
|
||||||
app.midi.send_pressure(device, channel, value);
|
|
||||||
}
|
|
||||||
engine::MidiCommand::ProgramChange {
|
|
||||||
device,
|
|
||||||
channel,
|
|
||||||
program,
|
|
||||||
} => {
|
|
||||||
app.midi.send_program_change(device, channel, program);
|
|
||||||
}
|
|
||||||
engine::MidiCommand::Clock { device } => app.midi.send_realtime(device, 0xF8),
|
|
||||||
engine::MidiCommand::Start { device } => app.midi.send_realtime(device, 0xFA),
|
|
||||||
engine::MidiCommand::Stop { device } => app.midi.send_realtime(device, 0xFC),
|
|
||||||
engine::MidiCommand::Continue { device } => app.midi.send_realtime(device, 0xFB),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
app.metrics.active_voices = metrics.active_voices.load(Ordering::Relaxed) as usize;
|
app.metrics.active_voices = metrics.active_voices.load(Ordering::Relaxed) as usize;
|
||||||
app.metrics.peak_voices = app.metrics.peak_voices.max(app.metrics.active_voices);
|
app.metrics.peak_voices = app.metrics.peak_voices.max(app.metrics.active_voices);
|
||||||
|
|||||||
204
src/midi.rs
204
src/midi.rs
@@ -1,12 +1,124 @@
|
|||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::engine::sequencer::MidiCommand;
|
||||||
use crate::model::CcAccess;
|
use crate::model::CcAccess;
|
||||||
|
|
||||||
pub const MAX_MIDI_OUTPUTS: usize = 4;
|
pub const MAX_MIDI_OUTPUTS: usize = 4;
|
||||||
pub const MAX_MIDI_INPUTS: usize = 4;
|
pub const MAX_MIDI_INPUTS: usize = 4;
|
||||||
pub const MAX_MIDI_DEVICES: usize = 4;
|
pub const MAX_MIDI_DEVICES: usize = 4;
|
||||||
|
|
||||||
|
/// Thread-safe MIDI output connections shared between the dispatcher (sending)
|
||||||
|
/// and the UI thread (connect/disconnect). The dispatcher calls `send_command`
|
||||||
|
/// directly after precise timing, eliminating the ~16ms jitter from UI polling.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MidiOutputPorts {
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
conns: Arc<Mutex<[Option<midir::MidiOutputConnection>; MAX_MIDI_OUTPUTS]>>,
|
||||||
|
pub selected: Arc<Mutex<[Option<usize>; MAX_MIDI_OUTPUTS]>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MidiOutputPorts {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
conns: Arc::new(Mutex::new([None, None, None, None])),
|
||||||
|
selected: Arc::new(Mutex::new([None; MAX_MIDI_OUTPUTS])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
pub fn connect(&self, slot: usize, port_index: usize) -> Result<(), String> {
|
||||||
|
if slot >= MAX_MIDI_OUTPUTS {
|
||||||
|
return Err("Invalid output slot".to_string());
|
||||||
|
}
|
||||||
|
let midi_out =
|
||||||
|
midir::MidiOutput::new(&format!("cagire-out-{slot}")).map_err(|e| e.to_string())?;
|
||||||
|
let ports = midi_out.ports();
|
||||||
|
let port = ports.get(port_index).ok_or("MIDI output port not found")?;
|
||||||
|
let conn = midi_out
|
||||||
|
.connect(port, &format!("cagire-midi-out-{slot}"))
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.conns.lock()[slot] = Some(conn);
|
||||||
|
self.selected.lock()[slot] = Some(port_index);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "cli"))]
|
||||||
|
pub fn connect(&self, _slot: usize, _port_index: usize) -> Result<(), String> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
pub fn disconnect(&self, slot: usize) {
|
||||||
|
if slot < MAX_MIDI_OUTPUTS {
|
||||||
|
self.conns.lock()[slot] = None;
|
||||||
|
self.selected.lock()[slot] = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "cli"))]
|
||||||
|
pub fn disconnect(&self, slot: usize) {
|
||||||
|
if slot < MAX_MIDI_OUTPUTS {
|
||||||
|
self.selected.lock()[slot] = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
fn send_message(&self, device: u8, message: &[u8]) {
|
||||||
|
let slot = (device as usize).min(MAX_MIDI_OUTPUTS - 1);
|
||||||
|
let mut conns = self.conns.lock();
|
||||||
|
if let Some(conn) = &mut conns[slot] {
|
||||||
|
let _ = conn.send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "cli"))]
|
||||||
|
fn send_message(&self, _device: u8, _message: &[u8]) {}
|
||||||
|
|
||||||
|
/// Send a MidiCommand directly — called by the dispatcher thread.
|
||||||
|
pub fn send_command(&self, cmd: &MidiCommand) {
|
||||||
|
match cmd {
|
||||||
|
MidiCommand::NoteOn { device, channel, note, velocity } => {
|
||||||
|
let status = 0x90 | (channel & 0x0F);
|
||||||
|
self.send_message(*device, &[status, note & 0x7F, velocity & 0x7F]);
|
||||||
|
}
|
||||||
|
MidiCommand::NoteOff { device, channel, note } => {
|
||||||
|
let status = 0x80 | (channel & 0x0F);
|
||||||
|
self.send_message(*device, &[status, note & 0x7F, 0]);
|
||||||
|
}
|
||||||
|
MidiCommand::CC { device, channel, cc, value } => {
|
||||||
|
let status = 0xB0 | (channel & 0x0F);
|
||||||
|
self.send_message(*device, &[status, cc & 0x7F, value & 0x7F]);
|
||||||
|
}
|
||||||
|
MidiCommand::PitchBend { device, channel, value } => {
|
||||||
|
let status = 0xE0 | (channel & 0x0F);
|
||||||
|
let lsb = (value & 0x7F) as u8;
|
||||||
|
let msb = ((value >> 7) & 0x7F) as u8;
|
||||||
|
self.send_message(*device, &[status, lsb, msb]);
|
||||||
|
}
|
||||||
|
MidiCommand::Pressure { device, channel, value } => {
|
||||||
|
let status = 0xD0 | (channel & 0x0F);
|
||||||
|
self.send_message(*device, &[status, value & 0x7F]);
|
||||||
|
}
|
||||||
|
MidiCommand::ProgramChange { device, channel, program } => {
|
||||||
|
let status = 0xC0 | (channel & 0x0F);
|
||||||
|
self.send_message(*device, &[status, program & 0x7F]);
|
||||||
|
}
|
||||||
|
MidiCommand::Clock { device } => self.send_message(*device, &[0xF8]),
|
||||||
|
MidiCommand::Start { device } => self.send_message(*device, &[0xFA]),
|
||||||
|
MidiCommand::Stop { device } => self.send_message(*device, &[0xFC]),
|
||||||
|
MidiCommand::Continue { device } => self.send_message(*device, &[0xFB]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MidiOutputPorts {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Raw CC memory storage type
|
/// Raw CC memory storage type
|
||||||
type CcMemoryInner = Arc<Mutex<[[[u8; 128]; 16]; MAX_MIDI_DEVICES]>>;
|
type CcMemoryInner = Arc<Mutex<[[[u8; 128]; 16]; MAX_MIDI_DEVICES]>>;
|
||||||
|
|
||||||
@@ -95,11 +207,9 @@ pub fn list_midi_inputs() -> Vec<MidiDeviceInfo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct MidiState {
|
pub struct MidiState {
|
||||||
#[cfg(feature = "cli")]
|
|
||||||
output_conns: [Option<midir::MidiOutputConnection>; MAX_MIDI_OUTPUTS],
|
|
||||||
#[cfg(feature = "cli")]
|
#[cfg(feature = "cli")]
|
||||||
input_conns: [Option<midir::MidiInputConnection<(CcMemoryInner, usize)>>; MAX_MIDI_INPUTS],
|
input_conns: [Option<midir::MidiInputConnection<(CcMemoryInner, usize)>>; MAX_MIDI_INPUTS],
|
||||||
pub selected_outputs: [Option<usize>; MAX_MIDI_OUTPUTS],
|
pub output_ports: MidiOutputPorts,
|
||||||
pub selected_inputs: [Option<usize>; MAX_MIDI_INPUTS],
|
pub selected_inputs: [Option<usize>; MAX_MIDI_INPUTS],
|
||||||
pub cc_memory: CcMemory,
|
pub cc_memory: CcMemory,
|
||||||
}
|
}
|
||||||
@@ -113,51 +223,24 @@ impl Default for MidiState {
|
|||||||
impl MidiState {
|
impl MidiState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
#[cfg(feature = "cli")]
|
|
||||||
output_conns: [None, None, None, None],
|
|
||||||
#[cfg(feature = "cli")]
|
#[cfg(feature = "cli")]
|
||||||
input_conns: [None, None, None, None],
|
input_conns: [None, None, None, None],
|
||||||
selected_outputs: [None; MAX_MIDI_OUTPUTS],
|
output_ports: MidiOutputPorts::new(),
|
||||||
selected_inputs: [None; MAX_MIDI_INPUTS],
|
selected_inputs: [None; MAX_MIDI_INPUTS],
|
||||||
cc_memory: CcMemory::new(),
|
cc_memory: CcMemory::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "cli")]
|
pub fn connect_output(&self, slot: usize, port_index: usize) -> Result<(), String> {
|
||||||
pub fn connect_output(&mut self, slot: usize, port_index: usize) -> Result<(), String> {
|
self.output_ports.connect(slot, port_index)
|
||||||
if slot >= MAX_MIDI_OUTPUTS {
|
|
||||||
return Err("Invalid output slot".to_string());
|
|
||||||
}
|
|
||||||
let midi_out =
|
|
||||||
midir::MidiOutput::new(&format!("cagire-out-{slot}")).map_err(|e| e.to_string())?;
|
|
||||||
let ports = midi_out.ports();
|
|
||||||
let port = ports.get(port_index).ok_or("MIDI output port not found")?;
|
|
||||||
let conn = midi_out
|
|
||||||
.connect(port, &format!("cagire-midi-out-{slot}"))
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
self.output_conns[slot] = Some(conn);
|
|
||||||
self.selected_outputs[slot] = Some(port_index);
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "cli"))]
|
pub fn disconnect_output(&self, slot: usize) {
|
||||||
pub fn connect_output(&mut self, _slot: usize, _port_index: usize) -> Result<(), String> {
|
self.output_ports.disconnect(slot);
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "cli")]
|
pub fn selected_outputs(&self) -> [Option<usize>; MAX_MIDI_OUTPUTS] {
|
||||||
pub fn disconnect_output(&mut self, slot: usize) {
|
*self.output_ports.selected.lock()
|
||||||
if slot < MAX_MIDI_OUTPUTS {
|
|
||||||
self.output_conns[slot] = None;
|
|
||||||
self.selected_outputs[slot] = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "cli"))]
|
|
||||||
pub fn disconnect_output(&mut self, slot: usize) {
|
|
||||||
if slot < MAX_MIDI_OUTPUTS {
|
|
||||||
self.selected_outputs[slot] = None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "cli")]
|
#[cfg(feature = "cli")]
|
||||||
@@ -215,51 +298,4 @@ impl MidiState {
|
|||||||
self.selected_inputs[slot] = None;
|
self.selected_inputs[slot] = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "cli")]
|
|
||||||
fn send_message(&mut self, device: u8, message: &[u8]) {
|
|
||||||
let slot = (device as usize).min(MAX_MIDI_OUTPUTS - 1);
|
|
||||||
if let Some(conn) = &mut self.output_conns[slot] {
|
|
||||||
let _ = conn.send(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "cli"))]
|
|
||||||
fn send_message(&mut self, _device: u8, _message: &[u8]) {}
|
|
||||||
|
|
||||||
pub fn send_note_on(&mut self, device: u8, channel: u8, note: u8, velocity: u8) {
|
|
||||||
let status = 0x90 | (channel & 0x0F);
|
|
||||||
self.send_message(device, &[status, note & 0x7F, velocity & 0x7F]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_note_off(&mut self, device: u8, channel: u8, note: u8) {
|
|
||||||
let status = 0x80 | (channel & 0x0F);
|
|
||||||
self.send_message(device, &[status, note & 0x7F, 0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_cc(&mut self, device: u8, channel: u8, cc: u8, value: u8) {
|
|
||||||
let status = 0xB0 | (channel & 0x0F);
|
|
||||||
self.send_message(device, &[status, cc & 0x7F, value & 0x7F]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_pitch_bend(&mut self, device: u8, channel: u8, value: u16) {
|
|
||||||
let status = 0xE0 | (channel & 0x0F);
|
|
||||||
let lsb = (value & 0x7F) as u8;
|
|
||||||
let msb = ((value >> 7) & 0x7F) as u8;
|
|
||||||
self.send_message(device, &[status, lsb, msb]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_pressure(&mut self, device: u8, channel: u8, value: u8) {
|
|
||||||
let status = 0xD0 | (channel & 0x0F);
|
|
||||||
self.send_message(device, &[status, value & 0x7F]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_program_change(&mut self, device: u8, channel: u8, program: u8) {
|
|
||||||
let status = 0xC0 | (channel & 0x0F);
|
|
||||||
self.send_message(device, &[status, program & 0x7F]);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send_realtime(&mut self, device: u8, msg: u8) {
|
|
||||||
self.send_message(device, &[msg]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ pub struct AudioConfig {
|
|||||||
pub lissajous_trails: bool,
|
pub lissajous_trails: bool,
|
||||||
pub spectrum_mode: SpectrumMode,
|
pub spectrum_mode: SpectrumMode,
|
||||||
pub spectrum_peaks: bool,
|
pub spectrum_peaks: bool,
|
||||||
|
pub input_sample_rate: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AudioConfig {
|
impl Default for AudioConfig {
|
||||||
@@ -154,6 +155,7 @@ impl Default for AudioConfig {
|
|||||||
lissajous_trails: false,
|
lissajous_trails: false,
|
||||||
spectrum_mode: SpectrumMode::default(),
|
spectrum_mode: SpectrumMode::default(),
|
||||||
spectrum_peaks: false,
|
spectrum_peaks: false,
|
||||||
|
input_sample_rate: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -519,6 +519,14 @@ fn render_status(frame: &mut Frame, app: &App, link: &LinkState, area: Rect) {
|
|||||||
label_style,
|
label_style,
|
||||||
)));
|
)));
|
||||||
|
|
||||||
|
if let Some(input_sr) = app.audio.config.input_sample_rate {
|
||||||
|
let warn_style = Style::new().fg(theme.flash.error_fg);
|
||||||
|
lines.push(Line::from(Span::styled(
|
||||||
|
format!(" Input {input_sr:.0} Hz !!"),
|
||||||
|
warn_style,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
if !app.plugin_mode {
|
if !app.plugin_mode {
|
||||||
// Host
|
// Host
|
||||||
lines.push(Line::from(Span::styled(
|
lines.push(Line::from(Span::styled(
|
||||||
@@ -946,7 +954,7 @@ fn render_midi_output(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
let mut lines: Vec<Line> = Vec::new();
|
let mut lines: Vec<Line> = Vec::new();
|
||||||
for slot in 0..4 {
|
for slot in 0..4 {
|
||||||
let is_focused = section_focused && app.audio.midi_output_slot == slot;
|
let is_focused = section_focused && app.audio.midi_output_slot == slot;
|
||||||
let display = midi_display_name(&midi_outputs, app.midi.selected_outputs[slot]);
|
let display = midi_display_name(&midi_outputs, app.midi.selected_outputs()[slot]);
|
||||||
let prefix = if is_focused { "> " } else { " " };
|
let prefix = if is_focused { "> " } else { " " };
|
||||||
let style = if is_focused { highlight } else { label_style };
|
let style = if is_focused { highlight } else { label_style };
|
||||||
let val_style = if is_focused { highlight } else { value_style };
|
let val_style = if is_focused { highlight } else { value_style };
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ fn with_params() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn auto_dur() {
|
fn auto_gate() {
|
||||||
let outputs = expect_outputs(r#""kick" snd ."#, 1);
|
let outputs = expect_outputs(r#""kick" snd ."#, 1);
|
||||||
assert!(outputs[0].contains("dur/"));
|
assert!(outputs[0].contains("gate/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -94,7 +94,7 @@ fn param_only_emit() {
|
|||||||
assert!(outputs[0].contains("voice/0"));
|
assert!(outputs[0].contains("voice/0"));
|
||||||
assert!(outputs[0].contains("freq/880"));
|
assert!(outputs[0].contains("freq/880"));
|
||||||
assert!(!outputs[0].contains("sound/"));
|
assert!(!outputs[0].contains("sound/"));
|
||||||
assert!(outputs[0].contains("dur/"));
|
assert!(outputs[0].contains("gate/"));
|
||||||
assert!(!outputs[0].contains("delaytime/"));
|
assert!(!outputs[0].contains("delaytime/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,8 +141,8 @@ fn polyphonic_with_at() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn explicit_dur_zero_is_infinite() {
|
fn explicit_dur_zero_is_infinite() {
|
||||||
let outputs = expect_outputs("880 freq 0 dur .", 1);
|
let outputs = expect_outputs("880 freq 0 gate .", 1);
|
||||||
assert!(outputs[0].contains("dur/0"));
|
assert!(outputs[0].contains("gate/0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ fn get_deltas(outputs: &[String]) -> Vec<f64> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_durs(outputs: &[String]) -> Vec<f64> {
|
fn get_gates(outputs: &[String]) -> Vec<f64> {
|
||||||
outputs
|
outputs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|o| parse_params(o).get("dur").copied().unwrap_or(0.0))
|
.map(|o| parse_params(o).get("gate").copied().unwrap_or(0.0))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,10 +88,10 @@ fn alternating_sounds() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dur_is_step_duration() {
|
fn gate_is_step_duration() {
|
||||||
let outputs = expect_outputs(r#""kick" snd ."#, 1);
|
let outputs = expect_outputs(r#""kick" snd ."#, 1);
|
||||||
let durs = get_durs(&outputs);
|
let gates = get_gates(&outputs);
|
||||||
assert!(approx_eq(durs[0], 0.5), "dur should be 4 * step_duration (0.5), got {}", durs[0]);
|
assert!(approx_eq(gates[0], 0.5), "gate should be 4 * step_duration (0.5), got {}", gates[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const SOUNDS = new Set([
|
|||||||
const PARAMS = new Set([
|
const PARAMS = new Set([
|
||||||
'freq', 'note', 'gain', 'decay', 'attack', 'release', 'lpf', 'hpf', 'bpf',
|
'freq', 'note', 'gain', 'decay', 'attack', 'release', 'lpf', 'hpf', 'bpf',
|
||||||
'verb', 'delay', 'pan', 'orbit', 'harmonics', 'distort', 'speed', 'voice',
|
'verb', 'delay', 'pan', 'orbit', 'harmonics', 'distort', 'speed', 'voice',
|
||||||
'dur', 'sustain', 'delaytime', 'delayfb', 'chorus', 'phaser', 'flanger',
|
'dur', 'gate', 'sustain', 'delaytime', 'delayfb', 'chorus', 'phaser', 'flanger',
|
||||||
'crush', 'fold', 'wrap', 'resonance', 'begin', 'end', 'velocity', 'chan',
|
'crush', 'fold', 'wrap', 'resonance', 'begin', 'end', 'velocity', 'chan',
|
||||||
'dev', 'ccnum', 'ccout', 'bend', 'pressure', 'program', 'tilt', 'slope',
|
'dev', 'ccnum', 'ccout', 'bend', 'pressure', 'program', 'tilt', 'slope',
|
||||||
'sub_gain', 'sub_oct', 'feedback', 'depth', 'sweep', 'comb', 'damping',
|
'sub_gain', 'sub_oct', 'feedback', 'depth', 'sweep', 'comb', 'damping',
|
||||||
|
|||||||
Reference in New Issue
Block a user