MIDI Documentation and optional mouse event support

This commit is contained in:
2026-02-01 00:51:56 +01:00
parent 96e7fb6bc4
commit 5b4a6ddd14
14 changed files with 401 additions and 3 deletions

View File

@@ -333,6 +333,12 @@ impl App {
fill: false,
nudge_secs: 0.0,
cc_memory: None,
#[cfg(feature = "desktop")]
mouse_x: 0.5,
#[cfg(feature = "desktop")]
mouse_y: 0.5,
#[cfg(feature = "desktop")]
mouse_down: 0.0,
};
let cmds = self.script_engine.evaluate(script, &ctx)?;
@@ -384,6 +390,12 @@ impl App {
fill: false,
nudge_secs: 0.0,
cc_memory: None,
#[cfg(feature = "desktop")]
mouse_x: 0.5,
#[cfg(feature = "desktop")]
mouse_y: 0.5,
#[cfg(feature = "desktop")]
mouse_down: 0.0,
};
match self.script_engine.evaluate(&script, &ctx) {
@@ -462,6 +474,12 @@ impl App {
fill: false,
nudge_secs: 0.0,
cc_memory: None,
#[cfg(feature = "desktop")]
mouse_x: 0.5,
#[cfg(feature = "desktop")]
mouse_y: 0.5,
#[cfg(feature = "desktop")]
mouse_down: 0.0,
};
if let Ok(cmds) = self.script_engine.evaluate(&script, &ctx) {

View File

@@ -19,9 +19,10 @@ use soft_ratatui::{EmbeddedGraphics, SoftBackend};
use cagire::app::App;
use cagire::engine::{
build_stream, spawn_sequencer, AnalysisHandle, AudioStreamConfig, LinkState, ScopeBuffer,
SequencerConfig, SequencerHandle, SpectrumBuffer,
build_stream, spawn_sequencer, AnalysisHandle, AudioStreamConfig, LinkState, MidiCommand,
ScopeBuffer, SequencerConfig, SequencerHandle, SpectrumBuffer,
};
use crossbeam_channel::Receiver;
use cagire::input::{handle_key, InputContext, InputResult};
use cagire::input_egui::convert_egui_events;
use cagire::settings::Settings;
@@ -144,7 +145,11 @@ struct CagireDesktop {
sample_rate_shared: Arc<AtomicU32>,
_stream: Option<cpal::Stream>,
_analysis_handle: Option<AnalysisHandle>,
midi_rx: Receiver<MidiCommand>,
current_font: FontChoice,
mouse_x: Arc<AtomicU32>,
mouse_y: Arc<AtomicU32>,
mouse_down: Arc<AtomicU32>,
}
impl CagireDesktop {
@@ -201,13 +206,21 @@ impl CagireDesktop {
initial_samples.extend(index);
}
let mouse_x = Arc::new(AtomicU32::new(0.5_f32.to_bits()));
let mouse_y = Arc::new(AtomicU32::new(0.5_f32.to_bits()));
let mouse_down = Arc::new(AtomicU32::new(0.0_f32.to_bits()));
let seq_config = SequencerConfig {
audio_sample_pos: Arc::clone(&audio_sample_pos),
sample_rate: Arc::clone(&sample_rate_shared),
lookahead_ms: Arc::clone(&lookahead_ms),
cc_memory: Some(Arc::clone(&app.midi.cc_memory)),
mouse_x: Arc::clone(&mouse_x),
mouse_y: Arc::clone(&mouse_y),
mouse_down: Arc::clone(&mouse_down),
};
let (sequencer, initial_audio_rx) = spawn_sequencer(
let (sequencer, initial_audio_rx, midi_rx) = spawn_sequencer(
Arc::clone(&link),
Arc::clone(&playing),
Arc::clone(&app.variables),
@@ -268,7 +281,11 @@ impl CagireDesktop {
sample_rate_shared,
_stream: stream,
_analysis_handle: analysis_handle,
midi_rx,
current_font,
mouse_x,
mouse_y,
mouse_down,
}
}
@@ -285,6 +302,7 @@ impl CagireDesktop {
return;
};
let new_audio_rx = sequencer.swap_audio_channel();
self.midi_rx = sequencer.swap_midi_channel();
let new_config = AudioStreamConfig {
output_device: self.app.audio.config.output_device.clone(),
@@ -373,6 +391,18 @@ impl eframe::App for CagireDesktop {
self.handle_audio_restart();
self.update_metrics();
ctx.input(|i| {
if let Some(pos) = i.pointer.latest_pos() {
let screen = i.viewport_rect();
let nx = (pos.x / screen.width()).clamp(0.0, 1.0);
let ny = (pos.y / screen.height()).clamp(0.0, 1.0);
self.mouse_x.store(nx.to_bits(), Ordering::Relaxed);
self.mouse_y.store(ny.to_bits(), Ordering::Relaxed);
}
let down = if i.pointer.primary_down() { 1.0_f32 } else { 0.0_f32 };
self.mouse_down.store(down.to_bits(), Ordering::Relaxed);
});
let Some(ref sequencer) = self.sequencer else {
return;
};
@@ -395,6 +425,33 @@ impl eframe::App for CagireDesktop {
self.app.flush_queued_changes(&sequencer.cmd_tx);
self.app.flush_dirty_patterns(&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);
if should_quit {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);

View File

@@ -1,6 +1,8 @@
use arc_swap::ArcSwap;
use crossbeam_channel::{bounded, Receiver, Sender, TrySendError};
use std::collections::HashMap;
#[cfg(feature = "desktop")]
use std::sync::atomic::AtomicU32;
use std::sync::atomic::{AtomicI64, AtomicU64};
use std::sync::Arc;
use std::thread::{self, JoinHandle};
@@ -232,6 +234,12 @@ pub struct SequencerConfig {
pub sample_rate: Arc<std::sync::atomic::AtomicU32>,
pub lookahead_ms: Arc<std::sync::atomic::AtomicU32>,
pub cc_memory: Option<CcMemory>,
#[cfg(feature = "desktop")]
pub mouse_x: Arc<AtomicU32>,
#[cfg(feature = "desktop")]
pub mouse_y: Arc<AtomicU32>,
#[cfg(feature = "desktop")]
pub mouse_down: Arc<AtomicU32>,
}
#[allow(clippy::too_many_arguments)]
@@ -257,6 +265,13 @@ pub fn spawn_sequencer(
let audio_tx_for_thread = Arc::clone(&audio_tx);
let midi_tx_for_thread = Arc::clone(&midi_tx);
#[cfg(feature = "desktop")]
let mouse_x = config.mouse_x;
#[cfg(feature = "desktop")]
let mouse_y = config.mouse_y;
#[cfg(feature = "desktop")]
let mouse_down = config.mouse_down;
let thread = thread::Builder::new()
.name("sequencer".into())
.spawn(move || {
@@ -277,6 +292,12 @@ pub fn spawn_sequencer(
config.sample_rate,
config.lookahead_ms,
config.cc_memory,
#[cfg(feature = "desktop")]
mouse_x,
#[cfg(feature = "desktop")]
mouse_y,
#[cfg(feature = "desktop")]
mouse_down,
);
})
.expect("Failed to spawn sequencer thread");
@@ -407,6 +428,12 @@ pub(crate) struct TickInput {
pub current_time_us: i64,
pub engine_time: f64,
pub lookahead_secs: f64,
#[cfg(feature = "desktop")]
pub mouse_x: f64,
#[cfg(feature = "desktop")]
pub mouse_y: f64,
#[cfg(feature = "desktop")]
pub mouse_down: f64,
}
pub struct TimestampedCommand {
@@ -591,6 +618,12 @@ impl SequencerState {
input.current_time_us,
input.engine_time,
input.lookahead_secs,
#[cfg(feature = "desktop")]
input.mouse_x,
#[cfg(feature = "desktop")]
input.mouse_y,
#[cfg(feature = "desktop")]
input.mouse_down,
);
let vars = self.read_variables(&steps.completed_iterations, &stopped, steps.any_step_fired);
@@ -684,6 +717,9 @@ impl SequencerState {
_current_time_us: i64,
engine_time: f64,
lookahead_secs: f64,
#[cfg(feature = "desktop")] mouse_x: f64,
#[cfg(feature = "desktop")] mouse_y: f64,
#[cfg(feature = "desktop")] mouse_down: f64,
) -> StepResult {
self.buf_audio_commands.clear();
let mut result = StepResult {
@@ -746,6 +782,12 @@ impl SequencerState {
fill,
nudge_secs,
cc_memory: self.cc_memory.clone(),
#[cfg(feature = "desktop")]
mouse_x,
#[cfg(feature = "desktop")]
mouse_y,
#[cfg(feature = "desktop")]
mouse_down,
};
if let Some(script) = resolved_script {
let mut trace = ExecutionTrace::default();
@@ -902,6 +944,9 @@ fn sequencer_loop(
sample_rate: Arc<std::sync::atomic::AtomicU32>,
lookahead_ms: Arc<std::sync::atomic::AtomicU32>,
cc_memory: Option<CcMemory>,
#[cfg(feature = "desktop")] mouse_x: Arc<AtomicU32>,
#[cfg(feature = "desktop")] mouse_y: Arc<AtomicU32>,
#[cfg(feature = "desktop")] mouse_down: Arc<AtomicU32>,
) {
use std::sync::atomic::Ordering;
@@ -943,6 +988,12 @@ fn sequencer_loop(
current_time_us,
engine_time,
lookahead_secs,
#[cfg(feature = "desktop")]
mouse_x: f32::from_bits(mouse_x.load(Ordering::Relaxed)) as f64,
#[cfg(feature = "desktop")]
mouse_y: f32::from_bits(mouse_y.load(Ordering::Relaxed)) as f64,
#[cfg(feature = "desktop")]
mouse_down: f32::from_bits(mouse_down.load(Ordering::Relaxed)) as f64,
};
let output = seq_state.tick(input);
@@ -1141,6 +1192,12 @@ mod tests {
current_time_us: 0,
engine_time: 0.0,
lookahead_secs: 0.0,
#[cfg(feature = "desktop")]
mouse_x: 0.5,
#[cfg(feature = "desktop")]
mouse_y: 0.5,
#[cfg(feature = "desktop")]
mouse_down: 0.0,
}
}
@@ -1156,6 +1213,12 @@ mod tests {
current_time_us: 0,
engine_time: 0.0,
lookahead_secs: 0.0,
#[cfg(feature = "desktop")]
mouse_x: 0.5,
#[cfg(feature = "desktop")]
mouse_y: 0.5,
#[cfg(feature = "desktop")]
mouse_down: 0.0,
}
}

View File

@@ -135,11 +135,24 @@ fn main() -> io::Result<()> {
initial_samples.extend(index);
}
#[cfg(feature = "desktop")]
let mouse_x = Arc::new(AtomicU32::new(0.5_f32.to_bits()));
#[cfg(feature = "desktop")]
let mouse_y = Arc::new(AtomicU32::new(0.5_f32.to_bits()));
#[cfg(feature = "desktop")]
let mouse_down = Arc::new(AtomicU32::new(0.0_f32.to_bits()));
let seq_config = SequencerConfig {
audio_sample_pos: Arc::clone(&audio_sample_pos),
sample_rate: Arc::clone(&sample_rate_shared),
lookahead_ms: Arc::clone(&lookahead_ms),
cc_memory: Some(Arc::clone(&app.midi.cc_memory)),
#[cfg(feature = "desktop")]
mouse_x: Arc::clone(&mouse_x),
#[cfg(feature = "desktop")]
mouse_y: Arc::clone(&mouse_y),
#[cfg(feature = "desktop")]
mouse_down: Arc::clone(&mouse_down),
};
let (sequencer, initial_audio_rx, mut midi_rx) = spawn_sequencer(

View File

@@ -59,6 +59,11 @@ const DOCS: &[DocEntry] = &[
),
Topic("Space & Time", include_str!("../../docs/engine_space.md")),
Topic("Words & Sounds", include_str!("../../docs/engine_words.md")),
// MIDI
Section("MIDI"),
Topic("Introduction", include_str!("../../docs/midi_intro.md")),
Topic("MIDI Output", include_str!("../../docs/midi_output.md")),
Topic("MIDI Input", include_str!("../../docs/midi_input.md")),
];
pub fn topic_count() -> usize {

View File

@@ -71,6 +71,12 @@ fn compute_stack_display(lines: &[String], editor: &cagire_ratatui::Editor, cach
fill: false,
nudge_secs: 0.0,
cc_memory: None,
#[cfg(feature = "desktop")]
mouse_x: 0.5,
#[cfg(feature = "desktop")]
mouse_y: 0.5,
#[cfg(feature = "desktop")]
mouse_down: 0.0,
};
match forth.evaluate(&script, &ctx) {