Feat: integrating workshop fixes
All checks were successful
Deploy Website / deploy (push) Has been skipped

This commit is contained in:
2026-03-03 19:46:50 +01:00
parent 16d6d76422
commit e8cf8c506b
15 changed files with 119 additions and 8387 deletions

2
Cargo.lock generated
View File

@@ -1810,7 +1810,7 @@ dependencies = [
[[package]] [[package]]
name = "doux" name = "doux"
version = "0.0.5" version = "0.0.5"
source = "git+https://github.com/sova-org/doux#886702b4fe937d26ed681a2f6d7626d26d6890d0" source = "git+https://github.com/sova-org/doux#2b62f896b855dd3da84906c7085835974fb56c8c"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"clap", "clap",

View File

@@ -118,6 +118,7 @@ pub enum Op {
Euclid, Euclid,
EuclidRot, EuclidRot,
Times, Times,
Map,
Chord(&'static [i64]), Chord(&'static [i64]),
Transpose, Transpose,
Invert, Invert,

View File

@@ -1374,6 +1374,15 @@ impl Forth {
} }
} }
Op::Map => {
let quot = pop(stack)?;
let items = std::mem::take(stack);
for item in items {
stack.push(item);
run_quotation(quot.clone(), stack, outputs, cmd)?;
}
}
Op::GeomRange => { Op::GeomRange => {
let count = pop_int(stack)?; let count = pop_int(stack)?;
let ratio = pop_float(stack)?; let ratio = pop_float(stack)?;

View File

@@ -110,6 +110,7 @@ pub(super) fn simple_op(name: &str) -> Option<Op> {
"euclid" => Op::Euclid, "euclid" => Op::Euclid,
"euclidrot" => Op::EuclidRot, "euclidrot" => Op::EuclidRot,
"times" => Op::Times, "times" => Op::Times,
"map" => Op::Map,
"m." => Op::MidiEmit, "m." => Op::MidiEmit,
"ccval" => Op::GetMidiCC, "ccval" => Op::GetMidiCC,
"mclock" => Op::MidiClock, "mclock" => Op::MidiClock,

View File

@@ -567,6 +567,16 @@ pub(super) const WORDS: &[Word] = &[
compile: Simple, compile: Simple,
varargs: false, varargs: false,
}, },
Word {
name: "map",
aliases: &[],
category: "Control",
stack: "(..vals quot -- ..results)",
desc: "Apply quotation to each stack element",
example: "1 2 3 ( 10 * ) map => 10 20 30",
compile: Simple,
varargs: false,
},
// Variables // Variables
Word { Word {
name: "@<var>", name: "@<var>",

File diff suppressed because it is too large Load Diff

View File

@@ -161,6 +161,7 @@ struct CagireDesktop {
_input_stream: Option<cpal::Stream>, _input_stream: Option<cpal::Stream>,
_analysis_handle: Option<AnalysisHandle>, _analysis_handle: Option<AnalysisHandle>,
midi_rx: Receiver<MidiCommand>, midi_rx: Receiver<MidiCommand>,
device_lost: Arc<AtomicBool>,
stream_error_rx: crossbeam_channel::Receiver<String>, stream_error_rx: crossbeam_channel::Receiver<String>,
current_font: FontChoice, current_font: FontChoice,
zoom_factor: f32, zoom_factor: f32,
@@ -207,6 +208,7 @@ impl CagireDesktop {
_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, midi_rx: b.midi_rx,
device_lost: b.device_lost,
stream_error_rx: b.stream_error_rx, stream_error_rx: b.stream_error_rx,
current_font, current_font,
zoom_factor, zoom_factor,
@@ -277,6 +279,7 @@ impl CagireDesktop {
Arc::clone(&self.audio_sample_pos), Arc::clone(&self.audio_sample_pos),
new_error_tx, new_error_tx,
&self.app.audio.config.sample_paths, &self.app.audio.config.sample_paths,
Arc::clone(&self.device_lost),
) { ) {
Ok((new_stream, new_input, info, new_analysis, registry)) => { Ok((new_stream, new_input, info, new_analysis, registry)) => {
self._stream = Some(new_stream); self._stream = Some(new_stream);
@@ -373,6 +376,11 @@ impl eframe::App for CagireDesktop {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.handle_audio_restart(); self.handle_audio_restart();
if self.device_lost.load(Ordering::Acquire) {
self.device_lost.store(false, Ordering::Release);
self.app.audio.restart_pending = true;
}
while let Ok(err) = self.stream_error_rx.try_recv() { while let Ok(err) = self.stream_error_rx.try_recv() {
self.app.ui.flash(&err, 3000, cagire::state::FlashKind::Error); self.app.ui.flash(&err, 3000, cagire::state::FlashKind::Error);
} }

View File

@@ -309,6 +309,7 @@ pub fn build_stream(
audio_sample_pos: Arc<AtomicU64>, audio_sample_pos: Arc<AtomicU64>,
error_tx: Sender<String>, error_tx: Sender<String>,
sample_paths: &[std::path::PathBuf], sample_paths: &[std::path::PathBuf],
device_lost: Arc<AtomicBool>,
) -> Result<BuildStreamResult, String> { ) -> Result<BuildStreamResult, String> {
let device = match &config.output_device { let device = match &config.output_device {
Some(name) => doux::audio::find_output_device(name) Some(name) => doux::audio::find_output_device(name)
@@ -410,7 +411,13 @@ pub fn build_stream(
drop(b.drain(..excess)); drop(b.drain(..excess));
} }
}, },
|err| eprintln!("input stream error: {err}"), {
let device_lost = Arc::clone(&device_lost);
move |err| {
eprintln!("input stream error: {err}");
device_lost.store(true, Ordering::Release);
}
},
None, None,
) )
.ok()?; .ok()?;
@@ -521,7 +528,10 @@ pub fn build_stream(
let _ = fft_producer.try_push(mono); let _ = fft_producer.try_push(mono);
} }
}, },
move |err| { let _ = error_tx.try_send(format!("stream error: {err}")); }, move |err| {
let _ = error_tx.try_send(format!("stream error: {err}"));
device_lost.store(true, Ordering::Release);
},
None, None,
) )
.map_err(|e| format!("Failed to build stream: {e}"))?; .map_err(|e| format!("Failed to build stream: {e}"))?;

View File

@@ -43,6 +43,7 @@ pub struct Init {
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 midi_rx: Receiver<MidiCommand>,
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")]
pub settings: Settings, pub settings: Settings,
@@ -202,6 +203,7 @@ pub fn init(args: InitArgs) -> Init {
seq_config, seq_config,
); );
let device_lost = Arc::new(AtomicBool::new(false));
let (stream_error_tx, stream_error_rx) = crossbeam_channel::bounded(16); let (stream_error_tx, stream_error_rx) = crossbeam_channel::bounded(16);
let stream_config = AudioStreamConfig { let stream_config = AudioStreamConfig {
@@ -222,6 +224,7 @@ pub fn init(args: InitArgs) -> Init {
Arc::clone(&audio_sample_pos), Arc::clone(&audio_sample_pos),
stream_error_tx, stream_error_tx,
&app.audio.config.sample_paths, &app.audio.config.sample_paths,
Arc::clone(&device_lost),
) { ) {
Ok((s, input, info, analysis, registry)) => { Ok((s, input, info, analysis, registry)) => {
app.audio.config.sample_rate = info.sample_rate; app.audio.config.sample_rate = info.sample_rate;
@@ -267,6 +270,7 @@ pub fn init(args: InitArgs) -> Init {
input_stream, input_stream,
analysis_handle, analysis_handle,
midi_rx, midi_rx,
device_lost,
stream_error_rx, stream_error_rx,
#[cfg(feature = "desktop")] #[cfg(feature = "desktop")]
settings, settings,

View File

@@ -85,6 +85,7 @@ fn handle_live_keys(ctx: &mut InputContext, key: &KeyEvent) -> bool {
match (key.code, key.kind) { match (key.code, key.kind) {
_ if !matches!(ctx.app.ui.modal, Modal::None) => false, _ if !matches!(ctx.app.ui.modal, Modal::None) => false,
_ if ctx.app.page == Page::Script && ctx.app.script_editor.focused => false, _ if ctx.app.page == Page::Script && ctx.app.script_editor.focused => false,
_ if ctx.app.ui.dict_search_active || ctx.app.ui.help_search_active => false,
(KeyCode::Char('f'), KeyEventKind::Press) if !key.modifiers.contains(KeyModifiers::ALT) => { (KeyCode::Char('f'), KeyEventKind::Press) if !key.modifiers.contains(KeyModifiers::ALT) => {
ctx.dispatch(AppCommand::ToggleLiveKeysFill); ctx.dispatch(AppCommand::ToggleLiveKeysFill);
true true

View File

@@ -103,6 +103,7 @@ fn main() -> io::Result<()> {
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 mut midi_rx = b.midi_rx;
let device_lost = b.device_lost;
let mut stream_error_rx = b.stream_error_rx; let mut stream_error_rx = b.stream_error_rx;
enable_raw_mode()?; enable_raw_mode()?;
@@ -167,6 +168,7 @@ fn main() -> io::Result<()> {
Arc::clone(&audio_sample_pos), Arc::clone(&audio_sample_pos),
new_error_tx, new_error_tx,
&app.audio.config.sample_paths, &app.audio.config.sample_paths,
Arc::clone(&device_lost),
) { ) {
Ok((new_stream, new_input, info, new_analysis, registry)) => { Ok((new_stream, new_input, info, new_analysis, registry)) => {
_stream = Some(new_stream); _stream = Some(new_stream);
@@ -197,6 +199,11 @@ fn main() -> io::Result<()> {
} }
} }
if device_lost.load(Ordering::Acquire) {
device_lost.store(false, Ordering::Release);
app.audio.restart_pending = true;
}
while let Ok(err) = stream_error_rx.try_recv() { while let Ok(err) = stream_error_rx.try_recv() {
app.ui.flash(&err, 3000, state::FlashKind::Error); app.ui.flash(&err, 3000, state::FlashKind::Error);
} }

View File

@@ -112,7 +112,7 @@ impl Default for DisplaySettings {
fn default() -> Self { fn default() -> Self {
Self { Self {
fps: 60, fps: 60,
runtime_highlight: false, runtime_highlight: true,
show_scope: true, show_scope: true,
show_spectrum: true, show_spectrum: true,
show_lissajous: true, show_lissajous: true,

View File

@@ -165,19 +165,11 @@ pub fn render(
} }
let (page_area, panel_area) = if app.panel.visible && app.panel.side.is_some() { let (page_area, panel_area) = if app.panel.visible && app.panel.side.is_some() {
if body_area.width >= 120 { let panel_width = body_area.width * 35 / 100;
let panel_width = body_area.width * 35 / 100; let [main, side] =
let [main, side] = Layout::horizontal([Constraint::Fill(1), Constraint::Length(panel_width)])
Layout::horizontal([Constraint::Fill(1), Constraint::Length(panel_width)]) .areas(body_area);
.areas(body_area); (main, Some(side))
(main, Some(side))
} else {
let panel_height = body_area.height * 40 / 100;
let [main, side] =
Layout::vertical([Constraint::Fill(1), Constraint::Length(panel_height)])
.areas(body_area);
(main, Some(side))
}
} else { } else {
(body_area, None) (body_area, None)
}; };

View File

@@ -66,3 +66,6 @@ mod case_statement;
#[path = "forth/harmony.rs"] #[path = "forth/harmony.rs"]
mod harmony; mod harmony;
#[path = "forth/map.rs"]
mod map;

55
tests/forth/map.rs Normal file
View File

@@ -0,0 +1,55 @@
use crate::harness::{expect_error, expect_int, expect_stack, run};
use cagire::forth::Value;
#[test]
fn map_add() {
expect_stack(
"1 2 3 4 5 ( 2 + ) map",
&[
Value::Int(3, None),
Value::Int(4, None),
Value::Int(5, None),
Value::Int(6, None),
Value::Int(7, None),
],
);
}
#[test]
fn map_multiply() {
expect_stack(
"1 2 3 ( 10 * ) map",
&[
Value::Int(10, None),
Value::Int(20, None),
Value::Int(30, None),
],
);
}
#[test]
fn map_single_element() {
expect_int("42 ( 1 + ) map", 43);
}
#[test]
fn map_empty_stack() {
run("( 1 + ) map");
}
#[test]
fn map_identity() {
expect_stack(
"1 2 3 ( ) map",
&[
Value::Int(1, None),
Value::Int(2, None),
Value::Int(3, None),
],
);
}
#[test]
fn map_missing_quotation() {
expect_error("1 2 3 map", "expected quotation");
}