Ungoing refactoring

This commit is contained in:
2026-02-04 18:47:40 +01:00
parent c95c82169f
commit ed70b47c81
19 changed files with 749 additions and 451 deletions

View File

@@ -22,7 +22,7 @@ use crate::state::{
MuteState, OptionsState, PanelState, PatternField, PatternPropsField, PatternsNav,
PlaybackState, ProjectState, StagedChange, StagedPropChange, UiState,
};
use crate::views::{dict_view, help_view};
use crate::model::{categories, docs};
const STEPS_PER_PAGE: usize = 32;
@@ -1240,11 +1240,11 @@ impl App {
};
}
AppCommand::HelpNextTopic(n) => {
let count = help_view::topic_count();
let count = docs::topic_count();
self.ui.help_topic = (self.ui.help_topic + n) % count;
}
AppCommand::HelpPrevTopic(n) => {
let count = help_view::topic_count();
let count = docs::topic_count();
self.ui.help_topic = (self.ui.help_topic + count - (n % count)) % count;
}
AppCommand::HelpScrollDown(n) => {
@@ -1264,7 +1264,7 @@ impl App {
}
AppCommand::HelpSearchInput(c) => {
self.ui.help_search_query.push(c);
if let Some((topic, line)) = help_view::find_match(&self.ui.help_search_query) {
if let Some((topic, line)) = docs::find_match(&self.ui.help_search_query) {
self.ui.help_topic = topic;
self.ui.help_scrolls[topic] = line;
}
@@ -1274,7 +1274,7 @@ impl App {
if self.ui.help_search_query.is_empty() {
return;
}
if let Some((topic, line)) = help_view::find_match(&self.ui.help_search_query) {
if let Some((topic, line)) = docs::find_match(&self.ui.help_search_query) {
self.ui.help_topic = topic;
self.ui.help_scrolls[topic] = line;
}
@@ -1291,11 +1291,11 @@ impl App {
};
}
AppCommand::DictNextCategory => {
let count = dict_view::category_count();
let count = categories::category_count();
self.ui.dict_category = (self.ui.dict_category + 1) % count;
}
AppCommand::DictPrevCategory => {
let count = dict_view::category_count();
let count = categories::category_count();
self.ui.dict_category = (self.ui.dict_category + count - 1) % count;
}
AppCommand::DictScrollDown(n) => {

View File

@@ -15,15 +15,14 @@ use soft_ratatui::embedded_graphics_unicodefonts::{
};
use soft_ratatui::{EmbeddedGraphics, SoftBackend};
use cagire::app::App;
use cagire::init::{init, InitArgs};
use cagire::engine::{
build_stream, spawn_sequencer, AnalysisHandle, AudioStreamConfig, LinkState, MidiCommand,
ScopeBuffer, SequencerConfig, SequencerHandle, SpectrumBuffer,
build_stream, AnalysisHandle, AudioStreamConfig, LinkState, MidiCommand, ScopeBuffer,
SequencerHandle, SpectrumBuffer,
};
use cagire::input::{handle_key, InputContext, InputResult};
use cagire::input_egui::convert_egui_events;
use cagire::settings::Settings;
use cagire::state::audio::RefreshRate;
use cagire::views;
use crossbeam_channel::Receiver;
@@ -129,7 +128,7 @@ fn create_terminal(font: FontChoice) -> TerminalType {
}
struct CagireDesktop {
app: App,
app: cagire::app::App,
terminal: TerminalType,
link: Arc<LinkState>,
sequencer: Option<SequencerHandle>,
@@ -153,139 +152,39 @@ struct CagireDesktop {
impl CagireDesktop {
fn new(cc: &eframe::CreationContext<'_>, args: Args) -> Self {
let settings = Settings::load();
let b = init(InitArgs {
samples: args.samples,
output: args.output,
input: args.input,
channels: args.channels,
buffer: args.buffer,
});
let link = Arc::new(LinkState::new(settings.link.tempo, settings.link.quantum));
if settings.link.enabled {
link.enable();
}
let playing = Arc::new(AtomicBool::new(true));
let nudge_us = Arc::new(AtomicI64::new(0));
let mut app = App::new();
app.playback
.queued_changes
.push(cagire::state::StagedChange {
change: cagire::engine::PatternChange::Start {
bank: 0,
pattern: 0,
},
quantization: cagire::model::LaunchQuantization::Immediate,
sync_mode: cagire::model::SyncMode::Reset,
});
app.audio.config.output_device = args.output.or(settings.audio.output_device);
app.audio.config.input_device = args.input.or(settings.audio.input_device);
app.audio.config.channels = args.channels.unwrap_or(settings.audio.channels);
app.audio.config.buffer_size = args.buffer.unwrap_or(settings.audio.buffer_size);
app.audio.config.max_voices = settings.audio.max_voices;
app.audio.config.lookahead_ms = settings.audio.lookahead_ms;
app.audio.config.sample_paths = args.samples;
app.audio.config.refresh_rate = RefreshRate::from_fps(settings.display.fps);
app.ui.runtime_highlight = settings.display.runtime_highlight;
app.audio.config.show_scope = settings.display.show_scope;
app.audio.config.show_spectrum = settings.display.show_spectrum;
app.ui.show_completion = settings.display.show_completion;
let metrics = Arc::new(EngineMetrics::default());
let scope_buffer = Arc::new(ScopeBuffer::new());
let spectrum_buffer = Arc::new(SpectrumBuffer::new());
let audio_sample_pos = Arc::new(AtomicU64::new(0));
let sample_rate_shared = Arc::new(AtomicU32::new(44100));
let lookahead_ms = Arc::new(AtomicU32::new(settings.audio.lookahead_ms));
let mut initial_samples = Vec::new();
for path in &app.audio.config.sample_paths {
let index = doux::sampling::scan_samples_dir(path);
app.audio.config.sample_count += index.len();
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_access: Some(
Arc::new(app.midi.cc_memory.clone()) as Arc<dyn cagire::model::CcAccess>
),
mouse_x: Arc::clone(&mouse_x),
mouse_y: Arc::clone(&mouse_y),
mouse_down: Arc::clone(&mouse_down),
};
let (sequencer, initial_audio_rx, midi_rx) = spawn_sequencer(
Arc::clone(&link),
Arc::clone(&playing),
Arc::clone(&app.variables),
Arc::clone(&app.dict),
Arc::clone(&app.rng),
settings.link.quantum,
Arc::clone(&app.live_keys),
Arc::clone(&nudge_us),
seq_config,
);
let stream_config = AudioStreamConfig {
output_device: app.audio.config.output_device.clone(),
channels: app.audio.config.channels,
buffer_size: app.audio.config.buffer_size,
max_voices: app.audio.config.max_voices,
};
let (stream, analysis_handle) = match build_stream(
&stream_config,
initial_audio_rx,
Arc::clone(&scope_buffer),
Arc::clone(&spectrum_buffer),
Arc::clone(&metrics),
initial_samples,
Arc::clone(&audio_sample_pos),
) {
Ok((s, info, analysis)) => {
app.audio.config.sample_rate = info.sample_rate;
sample_rate_shared.store(info.sample_rate as u32, Ordering::Relaxed);
(Some(s), Some(analysis))
}
Err(e) => {
app.ui.set_status(format!("Audio failed: {e}"));
app.audio.error = Some(e);
(None, None)
}
};
app.mark_all_patterns_dirty();
let current_font = FontChoice::from_setting(&settings.display.font);
let current_font = FontChoice::from_setting(&b.settings.display.font);
let terminal = create_terminal(current_font);
cc.egui_ctx.set_visuals(egui::Visuals::dark());
Self {
app,
app: b.app,
terminal,
link,
sequencer: Some(sequencer),
playing,
nudge_us,
lookahead_ms,
metrics,
scope_buffer,
spectrum_buffer,
audio_sample_pos,
sample_rate_shared,
_stream: stream,
_analysis_handle: analysis_handle,
midi_rx,
link: b.link,
sequencer: Some(b.sequencer),
playing: b.playing,
nudge_us: b.nudge_us,
lookahead_ms: b.lookahead_ms,
metrics: b.metrics,
scope_buffer: b.scope_buffer,
spectrum_buffer: b.spectrum_buffer,
audio_sample_pos: b.audio_sample_pos,
sample_rate_shared: b.sample_rate_shared,
_stream: b.stream,
_analysis_handle: b.analysis_handle,
midi_rx: b.midi_rx,
current_font,
mouse_x,
mouse_y,
mouse_down,
mouse_x: b.mouse_x,
mouse_y: b.mouse_y,
mouse_down: b.mouse_down,
last_frame: std::time::Instant::now(),
}
}
@@ -334,6 +233,8 @@ impl CagireDesktop {
self._stream = Some(new_stream);
self._analysis_handle = Some(new_analysis);
self.app.audio.config.sample_rate = info.sample_rate;
self.app.audio.config.host_name = info.host_name;
self.app.audio.config.channels = info.channels;
self.sample_rate_shared
.store(info.sample_rate as u32, Ordering::Relaxed);
self.app.audio.error = None;
@@ -419,7 +320,6 @@ impl eframe::App for CagireDesktop {
let seq_snapshot = sequencer.snapshot();
self.app.metrics.event_count = seq_snapshot.event_count;
self.app.metrics.dropped_events = seq_snapshot.dropped_events;
self.app.flush_queued_changes(&sequencer.cmd_tx);
self.app.flush_dirty_patterns(&sequencer.cmd_tx);
@@ -562,7 +462,6 @@ fn load_icon() -> egui::IconData {
}
fn main() -> eframe::Result<()> {
// Lock memory BEFORE any threads are spawned to prevent page faults in RT context
#[cfg(unix)]
cagire::engine::realtime::lock_memory();

View File

@@ -154,14 +154,12 @@ pub struct SharedSequencerState {
pub active_patterns: Vec<ActivePatternState>,
pub step_traces: Arc<StepTracesMap>,
pub event_count: usize,
pub dropped_events: usize,
}
pub struct SequencerSnapshot {
pub active_patterns: Vec<ActivePatternState>,
step_traces: Arc<StepTracesMap>,
pub event_count: usize,
pub dropped_events: usize,
}
impl SequencerSnapshot {
@@ -205,12 +203,11 @@ impl SequencerHandle {
active_patterns: state.active_patterns.clone(),
step_traces: Arc::clone(&state.step_traces),
event_count: state.event_count,
dropped_events: state.dropped_events,
}
}
pub fn swap_audio_channel(&self) -> Receiver<AudioCommand> {
let (new_tx, new_rx) = bounded::<AudioCommand>(256);
let (new_tx, new_rx) = unbounded::<AudioCommand>();
self.audio_tx.store(Arc::new(new_tx));
new_rx
}
@@ -294,7 +291,7 @@ pub fn spawn_sequencer(
Receiver<MidiCommand>,
) {
let (cmd_tx, cmd_rx) = bounded::<SeqCommand>(64);
let (audio_tx, audio_rx) = bounded::<AudioCommand>(256);
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 midi_tx = Arc::new(ArcSwap::from_pointee(midi_tx));
@@ -535,7 +532,6 @@ pub(crate) struct SequencerState {
runs_counter: RunsCounter,
step_traces: Arc<StepTracesMap>,
event_count: usize,
dropped_events: usize,
script_engine: ScriptEngine,
variables: Variables,
speed_overrides: HashMap<(usize, usize), f64>,
@@ -564,7 +560,6 @@ impl SequencerState {
runs_counter: RunsCounter::new(),
step_traces: Arc::new(HashMap::new()),
event_count: 0,
dropped_events: 0,
script_engine,
variables,
speed_overrides: HashMap::with_capacity(MAX_PATTERNS),
@@ -1057,7 +1052,6 @@ impl SequencerState {
.collect(),
step_traces: Arc::clone(&self.step_traces),
event_count: self.event_count,
dropped_events: self.dropped_events,
}
}
}
@@ -1175,7 +1169,7 @@ fn sequencer_loop(
}
} else {
// Audio direct to doux — sample-accurate scheduling via /time/ parameter
let _ = audio_tx.load().try_send(AudioCommand::Evaluate {
let _ = audio_tx.load().send(AudioCommand::Evaluate {
cmd: tsc.cmd,
time: tsc.time,
});

217
src/init.rs Normal file
View File

@@ -0,0 +1,217 @@
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, AtomicU64, Ordering};
use std::sync::Arc;
use crossbeam_channel::Receiver;
use doux::EngineMetrics;
use crate::app::App;
use crate::engine::{
build_stream, spawn_sequencer, AnalysisHandle, AudioStreamConfig, LinkState, MidiCommand,
PatternChange, ScopeBuffer, SequencerConfig, SequencerHandle, SpectrumBuffer,
};
use crate::midi;
use crate::model;
use crate::settings::Settings;
use crate::state::audio::RefreshRate;
use crate::state::StagedChange;
use crate::theme;
pub struct InitArgs {
pub samples: Vec<PathBuf>,
pub output: Option<String>,
pub input: Option<String>,
pub channels: Option<u16>,
pub buffer: Option<u32>,
}
pub struct Init {
pub app: App,
pub link: Arc<LinkState>,
pub sequencer: SequencerHandle,
pub playing: Arc<AtomicBool>,
pub nudge_us: Arc<AtomicI64>,
pub lookahead_ms: Arc<AtomicU32>,
pub metrics: Arc<EngineMetrics>,
pub scope_buffer: Arc<ScopeBuffer>,
pub spectrum_buffer: Arc<SpectrumBuffer>,
pub audio_sample_pos: Arc<AtomicU64>,
pub sample_rate_shared: Arc<AtomicU32>,
pub stream: Option<cpal::Stream>,
pub analysis_handle: Option<AnalysisHandle>,
pub midi_rx: Receiver<MidiCommand>,
#[cfg(feature = "desktop")]
pub settings: Settings,
#[cfg(feature = "desktop")]
pub mouse_x: Arc<AtomicU32>,
#[cfg(feature = "desktop")]
pub mouse_y: Arc<AtomicU32>,
#[cfg(feature = "desktop")]
pub mouse_down: Arc<AtomicU32>,
}
pub fn init(args: InitArgs) -> Init {
let settings = Settings::load();
let link = Arc::new(LinkState::new(settings.link.tempo, settings.link.quantum));
if settings.link.enabled {
link.enable();
}
let playing = Arc::new(AtomicBool::new(true));
let nudge_us = Arc::new(AtomicI64::new(0));
let mut app = App::new();
app.playback.queued_changes.push(StagedChange {
change: PatternChange::Start {
bank: 0,
pattern: 0,
},
quantization: model::LaunchQuantization::Immediate,
sync_mode: model::SyncMode::Reset,
});
app.audio.config.output_device = args.output.or(settings.audio.output_device.clone());
app.audio.config.input_device = args.input.or(settings.audio.input_device.clone());
app.audio.config.channels = args.channels.unwrap_or(settings.audio.channels);
app.audio.config.buffer_size = args.buffer.unwrap_or(settings.audio.buffer_size);
app.audio.config.max_voices = settings.audio.max_voices;
app.audio.config.lookahead_ms = settings.audio.lookahead_ms;
app.audio.config.sample_paths = args.samples;
app.audio.config.refresh_rate = RefreshRate::from_fps(settings.display.fps);
app.ui.runtime_highlight = settings.display.runtime_highlight;
app.audio.config.show_scope = settings.display.show_scope;
app.audio.config.show_spectrum = settings.display.show_spectrum;
app.ui.show_completion = settings.display.show_completion;
app.ui.color_scheme = settings.display.color_scheme;
app.ui.hue_rotation = settings.display.hue_rotation;
app.audio.config.layout = settings.display.layout;
let base_theme = settings.display.color_scheme.to_theme();
let rotated =
cagire_ratatui::theme::transform::rotate_theme(base_theme, settings.display.hue_rotation);
theme::set(rotated);
// MIDI connections
let outputs = midi::list_midi_outputs();
let inputs = midi::list_midi_inputs();
for (slot, name) in settings.midi.output_devices.iter().enumerate() {
if !name.is_empty() {
if let Some(idx) = outputs.iter().position(|d| &d.name == name) {
let _ = app.midi.connect_output(slot, idx);
}
}
}
for (slot, name) in settings.midi.input_devices.iter().enumerate() {
if !name.is_empty() {
if let Some(idx) = inputs.iter().position(|d| &d.name == name) {
let _ = app.midi.connect_input(slot, idx);
}
}
}
let metrics = Arc::new(EngineMetrics::default());
let scope_buffer = Arc::new(ScopeBuffer::new());
let spectrum_buffer = Arc::new(SpectrumBuffer::new());
let audio_sample_pos = Arc::new(AtomicU64::new(0));
let sample_rate_shared = Arc::new(AtomicU32::new(44100));
let lookahead_ms = Arc::new(AtomicU32::new(settings.audio.lookahead_ms));
let mut initial_samples = Vec::new();
for path in &app.audio.config.sample_paths {
let index = doux::sampling::scan_samples_dir(path);
app.audio.config.sample_count += index.len();
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_access: Some(Arc::new(app.midi.cc_memory.clone()) as Arc<dyn model::CcAccess>),
#[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, midi_rx) = spawn_sequencer(
Arc::clone(&link),
Arc::clone(&playing),
Arc::clone(&app.variables),
Arc::clone(&app.dict),
Arc::clone(&app.rng),
settings.link.quantum,
Arc::clone(&app.live_keys),
Arc::clone(&nudge_us),
seq_config,
);
let stream_config = AudioStreamConfig {
output_device: app.audio.config.output_device.clone(),
channels: app.audio.config.channels,
buffer_size: app.audio.config.buffer_size,
max_voices: app.audio.config.max_voices,
};
let (stream, analysis_handle) = match build_stream(
&stream_config,
initial_audio_rx,
Arc::clone(&scope_buffer),
Arc::clone(&spectrum_buffer),
Arc::clone(&metrics),
initial_samples,
Arc::clone(&audio_sample_pos),
) {
Ok((s, info, analysis)) => {
app.audio.config.sample_rate = info.sample_rate;
app.audio.config.host_name = info.host_name;
app.audio.config.channels = info.channels;
sample_rate_shared.store(info.sample_rate as u32, Ordering::Relaxed);
(Some(s), Some(analysis))
}
Err(e) => {
app.ui.set_status(format!("Audio failed: {e}"));
app.audio.error = Some(e);
(None, None)
}
};
app.mark_all_patterns_dirty();
Init {
app,
link,
sequencer,
playing,
nudge_us,
lookahead_ms,
metrics,
scope_buffer,
spectrum_buffer,
audio_sample_pos,
sample_rate_shared,
stream,
analysis_handle,
midi_rx,
#[cfg(feature = "desktop")]
settings,
#[cfg(feature = "desktop")]
mouse_x,
#[cfg(feature = "desktop")]
mouse_y,
#[cfg(feature = "desktop")]
mouse_down,
}
}

View File

@@ -1435,7 +1435,7 @@ fn handle_options_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
ctx.dispatch(AppCommand::SetColorScheme(new_scheme));
}
OptionsFocus::HueRotation => {
let delta = if key.code == KeyCode::Left { -1.0 } else { 1.0 };
let delta = if key.code == KeyCode::Left { -5.0 } else { 5.0 };
let new_rotation = (ctx.app.ui.hue_rotation + delta).rem_euclid(360.0);
ctx.dispatch(AppCommand::SetHueRotation(new_rotation));
}

View File

@@ -1,6 +1,7 @@
pub use cagire_forth as forth;
pub mod app;
pub mod init;
pub mod commands;
pub mod engine;
pub mod input;

View File

@@ -1,4 +1,5 @@
mod app;
mod init;
mod commands;
mod engine;
mod input;
@@ -14,7 +15,7 @@ mod widgets;
use std::io;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU32, AtomicU64, Ordering};
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::time::{Duration, Instant};
@@ -24,18 +25,12 @@ use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use crossterm::ExecutableCommand;
use doux::EngineMetrics;
use ratatui::prelude::CrosstermBackend;
use ratatui::Terminal;
use app::App;
use engine::{
build_stream, spawn_sequencer, AudioStreamConfig, LinkState, ScopeBuffer, SequencerConfig,
SpectrumBuffer,
};
use engine::{build_stream, AudioStreamConfig};
use init::InitArgs;
use input::{handle_key, InputContext, InputResult};
use settings::Settings;
use state::audio::RefreshRate;
#[derive(Parser)]
#[command(name = "cagire", version, about = "Forth-based live coding sequencer")]
@@ -62,149 +57,33 @@ struct Args {
}
fn main() -> io::Result<()> {
// Lock memory BEFORE any threads are spawned to prevent page faults in RT context
#[cfg(unix)]
engine::realtime::lock_memory();
let args = Args::parse();
let settings = Settings::load();
let link = Arc::new(LinkState::new(settings.link.tempo, settings.link.quantum));
if settings.link.enabled {
link.enable();
}
let b = init::init(InitArgs {
samples: args.samples,
output: args.output,
input: args.input,
channels: args.channels,
buffer: args.buffer,
});
let playing = Arc::new(AtomicBool::new(true));
let nudge_us = Arc::new(AtomicI64::new(0));
let mut app = App::new();
app.playback
.queued_changes
.push(crate::state::StagedChange {
change: engine::PatternChange::Start {
bank: 0,
pattern: 0,
},
quantization: crate::model::LaunchQuantization::Immediate,
sync_mode: crate::model::SyncMode::Reset,
});
app.audio.config.output_device = args.output.or(settings.audio.output_device);
app.audio.config.input_device = args.input.or(settings.audio.input_device);
app.audio.config.channels = args.channels.unwrap_or(settings.audio.channels);
app.audio.config.buffer_size = args.buffer.unwrap_or(settings.audio.buffer_size);
app.audio.config.max_voices = settings.audio.max_voices;
app.audio.config.lookahead_ms = settings.audio.lookahead_ms;
app.audio.config.sample_paths = args.samples;
app.audio.config.refresh_rate = RefreshRate::from_fps(settings.display.fps);
app.ui.runtime_highlight = settings.display.runtime_highlight;
app.audio.config.show_scope = settings.display.show_scope;
app.audio.config.show_spectrum = settings.display.show_spectrum;
app.ui.show_completion = settings.display.show_completion;
app.ui.color_scheme = settings.display.color_scheme;
app.ui.hue_rotation = settings.display.hue_rotation;
app.audio.config.layout = settings.display.layout;
let base_theme = settings.display.color_scheme.to_theme();
let rotated =
cagire_ratatui::theme::transform::rotate_theme(base_theme, settings.display.hue_rotation);
theme::set(rotated);
// Load MIDI settings
let outputs = midi::list_midi_outputs();
let inputs = midi::list_midi_inputs();
for (slot, name) in settings.midi.output_devices.iter().enumerate() {
if !name.is_empty() {
if let Some(idx) = outputs.iter().position(|d| &d.name == name) {
let _ = app.midi.connect_output(slot, idx);
}
}
}
for (slot, name) in settings.midi.input_devices.iter().enumerate() {
if !name.is_empty() {
if let Some(idx) = inputs.iter().position(|d| &d.name == name) {
let _ = app.midi.connect_input(slot, idx);
}
}
}
let metrics = Arc::new(EngineMetrics::default());
let scope_buffer = Arc::new(ScopeBuffer::new());
let spectrum_buffer = Arc::new(SpectrumBuffer::new());
let audio_sample_pos = Arc::new(AtomicU64::new(0));
let sample_rate_shared = Arc::new(AtomicU32::new(44100));
let lookahead_ms = Arc::new(AtomicU32::new(settings.audio.lookahead_ms));
let mut initial_samples = Vec::new();
for path in &app.audio.config.sample_paths {
let index = doux::sampling::scan_samples_dir(path);
app.audio.config.sample_count += index.len();
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_access: Some(Arc::new(app.midi.cc_memory.clone()) as Arc<dyn crate::model::CcAccess>),
#[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(
Arc::clone(&link),
Arc::clone(&playing),
Arc::clone(&app.variables),
Arc::clone(&app.dict),
Arc::clone(&app.rng),
settings.link.quantum,
Arc::clone(&app.live_keys),
Arc::clone(&nudge_us),
seq_config,
);
let stream_config = AudioStreamConfig {
output_device: app.audio.config.output_device.clone(),
channels: app.audio.config.channels,
buffer_size: app.audio.config.buffer_size,
max_voices: app.audio.config.max_voices,
};
let (mut _stream, mut _analysis_handle) = match build_stream(
&stream_config,
initial_audio_rx,
Arc::clone(&scope_buffer),
Arc::clone(&spectrum_buffer),
Arc::clone(&metrics),
initial_samples,
Arc::clone(&audio_sample_pos),
) {
Ok((s, info, analysis)) => {
app.audio.config.sample_rate = info.sample_rate;
app.audio.config.host_name = info.host_name;
app.audio.config.channels = info.channels;
sample_rate_shared.store(info.sample_rate as u32, Ordering::Relaxed);
(Some(s), Some(analysis))
}
Err(e) => {
app.ui.set_status(format!("Audio failed: {e}"));
app.audio.error = Some(e);
(None, None)
}
};
app.mark_all_patterns_dirty();
let mut app = b.app;
let link = b.link;
let sequencer = b.sequencer;
let playing = b.playing;
let nudge_us = b.nudge_us;
let lookahead_ms = b.lookahead_ms;
let metrics = b.metrics;
let scope_buffer = b.scope_buffer;
let spectrum_buffer = b.spectrum_buffer;
let audio_sample_pos = b.audio_sample_pos;
let sample_rate_shared = b.sample_rate_shared;
let mut _stream = b.stream;
let mut _analysis_handle = b.analysis_handle;
let mut midi_rx = b.midi_rx;
enable_raw_mode()?;
io::stdout().execute(EnableBracketedPaste)?;
@@ -268,7 +147,6 @@ fn main() -> io::Result<()> {
app.playback.playing = playing.load(Ordering::Relaxed);
// Process pending MIDI commands
while let Ok(midi_cmd) = midi_rx.try_recv() {
match midi_cmd {
engine::MidiCommand::NoteOn {
@@ -335,7 +213,6 @@ fn main() -> io::Result<()> {
let seq_snapshot = sequencer.snapshot();
app.metrics.event_count = seq_snapshot.event_count;
app.metrics.dropped_events = seq_snapshot.dropped_events;
app.flush_queued_changes(&sequencer.cmd_tx);
app.flush_dirty_patterns(&sequencer.cmd_tx);

65
src/model/categories.rs Normal file
View File

@@ -0,0 +1,65 @@
pub enum CatEntry {
Section(&'static str),
Category(&'static str),
}
use CatEntry::{Category, Section};
pub const CATEGORIES: &[CatEntry] = &[
// Forth core
Section("Forth"),
Category("Stack"),
Category("Arithmetic"),
Category("Comparison"),
Category("Logic"),
Category("Control"),
Category("Variables"),
Category("Probability"),
Category("Definitions"),
// Live coding
Section("Live Coding"),
Category("Sound"),
Category("Time"),
Category("Context"),
Category("Music"),
Category("LFO"),
// Synthesis
Section("Synthesis"),
Category("Oscillator"),
Category("Wavetable"),
Category("Generator"),
Category("Envelope"),
Category("Sample"),
// Effects
Section("Effects"),
Category("Filter"),
Category("FM"),
Category("Modulation"),
Category("Mod FX"),
Category("Lo-fi"),
Category("Stereo"),
Category("Delay"),
Category("Reverb"),
// External I/O
Section("I/O"),
Category("MIDI"),
Category("Desktop"),
];
pub fn category_count() -> usize {
CATEGORIES
.iter()
.filter(|e| matches!(e, Category(_)))
.count()
}
pub fn get_category_name(index: usize) -> &'static str {
CATEGORIES
.iter()
.filter_map(|e| match e {
Category(name) => Some(*name),
Section(_) => None,
})
.nth(index)
.unwrap_or("Unknown")
}

86
src/model/docs.rs Normal file
View File

@@ -0,0 +1,86 @@
pub enum DocEntry {
Section(&'static str),
Topic(&'static str, &'static str),
}
use DocEntry::{Section, Topic};
pub const DOCS: &[DocEntry] = &[
// Getting Started
Section("Getting Started"),
Topic("Welcome", include_str!("../../docs/welcome.md")),
Topic("Moving Around", include_str!("../../docs/navigation.md")),
Topic(
"How Does It Work?",
include_str!("../../docs/how_it_works.md"),
),
Topic(
"Banks & Patterns",
include_str!("../../docs/banks_patterns.md"),
),
Topic("Stage / Commit", include_str!("../../docs/staging.md")),
Topic("Using the Sequencer", include_str!("../../docs/grid.md")),
Topic("Editing a Step", include_str!("../../docs/editing.md")),
// Forth fundamentals
Section("Forth"),
Topic("About Forth", include_str!("../../docs/about_forth.md")),
Topic("The Dictionary", include_str!("../../docs/dictionary.md")),
Topic("The Stack", include_str!("../../docs/stack.md")),
Topic("Creating Words", include_str!("../../docs/definitions.md")),
Topic("Oddities", include_str!("../../docs/oddities.md")),
// Audio Engine
Section("Audio Engine"),
Topic("Introduction", include_str!("../../docs/engine_intro.md")),
Topic("Settings", include_str!("../../docs/engine_settings.md")),
Topic("Sources", include_str!("../../docs/engine_sources.md")),
Topic("Samples", include_str!("../../docs/engine_samples.md")),
Topic("Wavetables", include_str!("../../docs/engine_wavetable.md")),
Topic("Filters", include_str!("../../docs/engine_filters.md")),
Topic(
"Modulation",
include_str!("../../docs/engine_modulation.md"),
),
Topic(
"Distortion",
include_str!("../../docs/engine_distortion.md"),
),
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 {
DOCS.iter().filter(|e| matches!(e, Topic(_, _))).count()
}
pub fn get_topic(index: usize) -> Option<(&'static str, &'static str)> {
DOCS.iter()
.filter_map(|e| match e {
Topic(name, content) => Some((*name, *content)),
Section(_) => None,
})
.nth(index)
}
pub fn find_match(query: &str) -> Option<(usize, usize)> {
let query = query.to_lowercase();
for (topic_idx, (_, content)) in DOCS
.iter()
.filter_map(|e| match e {
Topic(name, content) => Some((*name, *content)),
Section(_) => None,
})
.enumerate()
{
for (line_idx, line) in content.lines().enumerate() {
if line.to_lowercase().contains(&query) {
return Some((topic_idx, line_idx));
}
}
}
None
}

View File

@@ -1,3 +1,5 @@
pub mod categories;
pub mod docs;
mod script;
pub use cagire_forth::{lookup_word, Word, WordCompile, WORDS};

View File

@@ -188,7 +188,6 @@ impl CyclicEnum for SettingKind {
pub struct Metrics {
pub event_count: usize,
pub dropped_events: usize,
pub active_voices: usize,
pub peak_voices: usize,
pub cpu_load: f32,
@@ -204,7 +203,6 @@ impl Default for Metrics {
fn default() -> Self {
Self {
event_count: 0,
dropped_events: 0,
active_voices: 0,
peak_voices: 0,
cpu_load: 0.0,

View File

@@ -70,12 +70,12 @@ impl Default for UiState {
modal: Modal::None,
help_focus: HelpFocus::default(),
help_topic: 0,
help_scrolls: vec![0; crate::views::help_view::topic_count()],
help_scrolls: vec![0; crate::model::docs::topic_count()],
help_search_active: false,
help_search_query: String::new(),
dict_focus: DictFocus::default(),
dict_category: 0,
dict_scrolls: vec![0; crate::views::dict_view::category_count()],
dict_scrolls: vec![0; crate::model::categories::category_count()],
dict_search_query: String::new(),
dict_search_active: false,
show_title: true,

View File

@@ -5,58 +5,13 @@ use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph};
use ratatui::Frame;
use crate::app::App;
use crate::model::categories::{get_category_name, CatEntry, CATEGORIES};
use crate::model::{Word, WORDS};
use crate::state::DictFocus;
use crate::theme;
enum CatEntry {
Section(&'static str),
Category(&'static str),
}
use CatEntry::{Category, Section};
const CATEGORIES: &[CatEntry] = &[
// Forth core
Section("Forth"),
Category("Stack"),
Category("Arithmetic"),
Category("Comparison"),
Category("Logic"),
Category("Control"),
Category("Variables"),
Category("Probability"),
Category("Definitions"),
// Live coding
Section("Live Coding"),
Category("Sound"),
Category("Time"),
Category("Context"),
Category("Music"),
Category("LFO"),
// Synthesis
Section("Synthesis"),
Category("Oscillator"),
Category("Wavetable"),
Category("Generator"),
Category("Envelope"),
Category("Sample"),
// Effects
Section("Effects"),
Category("Filter"),
Category("FM"),
Category("Modulation"),
Category("Mod FX"),
Category("Lo-fi"),
Category("Stereo"),
Category("Delay"),
Category("Reverb"),
// External I/O
Section("I/O"),
Category("MIDI"),
Category("Desktop"),
];
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
let [header_area, body_area] =
Layout::vertical([Constraint::Length(5), Constraint::Fill(1)]).areas(area);
@@ -165,17 +120,6 @@ fn render_categories(frame: &mut Frame, app: &App, area: Rect, dimmed: bool) {
frame.render_widget(list, area);
}
fn get_category_name(index: usize) -> &'static str {
CATEGORIES
.iter()
.filter_map(|e| match e {
Category(name) => Some(*name),
Section(_) => None,
})
.nth(index)
.unwrap_or("Unknown")
}
fn render_words(frame: &mut Frame, app: &App, area: Rect, is_searching: bool) {
let theme = theme::get();
let focused = app.ui.dict_focus == DictFocus::Words;
@@ -299,9 +243,3 @@ fn render_search_bar(frame: &mut Frame, app: &App, area: Rect) {
frame.render_widget(Paragraph::new(vec![line]), area);
}
pub fn category_count() -> usize {
CATEGORIES
.iter()
.filter(|e| matches!(e, Category(_)))
.count()
}

View File

@@ -7,10 +7,13 @@ use ratatui::Frame;
use tui_big_text::{BigText, PixelSize};
use crate::app::App;
use crate::model::docs::{get_topic, DocEntry, DOCS};
use crate::state::HelpFocus;
use crate::theme;
use crate::views::highlight;
use DocEntry::{Section, Topic};
struct AppTheme;
impl MarkdownTheme for AppTheme {
@@ -83,74 +86,6 @@ impl CodeHighlighter for ForthHighlighter {
}
}
enum DocEntry {
Section(&'static str),
Topic(&'static str, &'static str),
}
use DocEntry::{Section, Topic};
const DOCS: &[DocEntry] = &[
// Getting Started
Section("Getting Started"),
Topic("Welcome", include_str!("../../docs/welcome.md")),
Topic("Moving Around", include_str!("../../docs/navigation.md")),
Topic(
"How Does It Work?",
include_str!("../../docs/how_it_works.md"),
),
Topic(
"Banks & Patterns",
include_str!("../../docs/banks_patterns.md"),
),
Topic("Stage / Commit", include_str!("../../docs/staging.md")),
Topic("Using the Sequencer", include_str!("../../docs/grid.md")),
Topic("Editing a Step", include_str!("../../docs/editing.md")),
// Forth fundamentals
Section("Forth"),
Topic("About Forth", include_str!("../../docs/about_forth.md")),
Topic("The Dictionary", include_str!("../../docs/dictionary.md")),
Topic("The Stack", include_str!("../../docs/stack.md")),
Topic("Creating Words", include_str!("../../docs/definitions.md")),
Topic("Oddities", include_str!("../../docs/oddities.md")),
// Audio Engine
Section("Audio Engine"),
Topic("Introduction", include_str!("../../docs/engine_intro.md")),
Topic("Settings", include_str!("../../docs/engine_settings.md")),
Topic("Sources", include_str!("../../docs/engine_sources.md")),
Topic("Samples", include_str!("../../docs/engine_samples.md")),
Topic("Wavetables", include_str!("../../docs/engine_wavetable.md")),
Topic("Filters", include_str!("../../docs/engine_filters.md")),
Topic(
"Modulation",
include_str!("../../docs/engine_modulation.md"),
),
Topic(
"Distortion",
include_str!("../../docs/engine_distortion.md"),
),
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 {
DOCS.iter().filter(|e| matches!(e, Topic(_, _))).count()
}
fn get_topic(index: usize) -> Option<(&'static str, &'static str)> {
DOCS.iter()
.filter_map(|e| match e {
Topic(name, content) => Some((*name, *content)),
Section(_) => None,
})
.nth(index)
}
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
let [topics_area, content_area] =
Layout::horizontal([Constraint::Length(24), Constraint::Fill(1)]).areas(area);
@@ -386,22 +321,3 @@ fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack.windows(needle.len()).position(|w| w == needle)
}
/// Find first line matching query across all topics. Returns (topic_index, line_index).
pub fn find_match(query: &str) -> Option<(usize, usize)> {
let query = query.to_lowercase();
for (topic_idx, (_, content)) in DOCS
.iter()
.filter_map(|e| match e {
Topic(name, content) => Some((*name, *content)),
Section(_) => None,
})
.enumerate()
{
for (line_idx, line) in content.lines().enumerate() {
if line.to_lowercase().contains(&query) {
return Some((topic_idx, line_idx));
}
}
}
None
}