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]]
name = "doux"
version = "0.0.5"
source = "git+https://github.com/sova-org/doux#886702b4fe937d26ed681a2f6d7626d26d6890d0"
source = "git+https://github.com/sova-org/doux#2b62f896b855dd3da84906c7085835974fb56c8c"
dependencies = [
"arc-swap",
"clap",

View File

@@ -118,6 +118,7 @@ pub enum Op {
Euclid,
EuclidRot,
Times,
Map,
Chord(&'static [i64]),
Transpose,
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 => {
let count = pop_int(stack)?;
let ratio = pop_float(stack)?;

View File

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

View File

@@ -567,6 +567,16 @@ pub(super) const WORDS: &[Word] = &[
compile: Simple,
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
Word {
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>,
_analysis_handle: Option<AnalysisHandle>,
midi_rx: Receiver<MidiCommand>,
device_lost: Arc<AtomicBool>,
stream_error_rx: crossbeam_channel::Receiver<String>,
current_font: FontChoice,
zoom_factor: f32,
@@ -207,6 +208,7 @@ impl CagireDesktop {
_input_stream: b.input_stream,
_analysis_handle: b.analysis_handle,
midi_rx: b.midi_rx,
device_lost: b.device_lost,
stream_error_rx: b.stream_error_rx,
current_font,
zoom_factor,
@@ -277,6 +279,7 @@ impl CagireDesktop {
Arc::clone(&self.audio_sample_pos),
new_error_tx,
&self.app.audio.config.sample_paths,
Arc::clone(&self.device_lost),
) {
Ok((new_stream, new_input, info, new_analysis, registry)) => {
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) {
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() {
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>,
error_tx: Sender<String>,
sample_paths: &[std::path::PathBuf],
device_lost: Arc<AtomicBool>,
) -> Result<BuildStreamResult, String> {
let device = match &config.output_device {
Some(name) => doux::audio::find_output_device(name)
@@ -410,7 +411,13 @@ pub fn build_stream(
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,
)
.ok()?;
@@ -521,7 +528,10 @@ pub fn build_stream(
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,
)
.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 analysis_handle: Option<AnalysisHandle>,
pub midi_rx: Receiver<MidiCommand>,
pub device_lost: Arc<AtomicBool>,
pub stream_error_rx: crossbeam_channel::Receiver<String>,
#[cfg(feature = "desktop")]
pub settings: Settings,
@@ -202,6 +203,7 @@ pub fn init(args: InitArgs) -> Init {
seq_config,
);
let device_lost = Arc::new(AtomicBool::new(false));
let (stream_error_tx, stream_error_rx) = crossbeam_channel::bounded(16);
let stream_config = AudioStreamConfig {
@@ -222,6 +224,7 @@ pub fn init(args: InitArgs) -> Init {
Arc::clone(&audio_sample_pos),
stream_error_tx,
&app.audio.config.sample_paths,
Arc::clone(&device_lost),
) {
Ok((s, input, info, analysis, registry)) => {
app.audio.config.sample_rate = info.sample_rate;
@@ -267,6 +270,7 @@ pub fn init(args: InitArgs) -> Init {
input_stream,
analysis_handle,
midi_rx,
device_lost,
stream_error_rx,
#[cfg(feature = "desktop")]
settings,

View File

@@ -85,6 +85,7 @@ fn handle_live_keys(ctx: &mut InputContext, key: &KeyEvent) -> bool {
match (key.code, key.kind) {
_ if !matches!(ctx.app.ui.modal, Modal::None) => 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) => {
ctx.dispatch(AppCommand::ToggleLiveKeysFill);
true

View File

@@ -103,6 +103,7 @@ fn main() -> io::Result<()> {
let mut _input_stream = b.input_stream;
let mut _analysis_handle = b.analysis_handle;
let mut midi_rx = b.midi_rx;
let device_lost = b.device_lost;
let mut stream_error_rx = b.stream_error_rx;
enable_raw_mode()?;
@@ -167,6 +168,7 @@ fn main() -> io::Result<()> {
Arc::clone(&audio_sample_pos),
new_error_tx,
&app.audio.config.sample_paths,
Arc::clone(&device_lost),
) {
Ok((new_stream, new_input, info, new_analysis, registry)) => {
_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() {
app.ui.flash(&err, 3000, state::FlashKind::Error);
}

View File

@@ -112,7 +112,7 @@ impl Default for DisplaySettings {
fn default() -> Self {
Self {
fps: 60,
runtime_highlight: false,
runtime_highlight: true,
show_scope: true,
show_spectrum: 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() {
if body_area.width >= 120 {
let panel_width = body_area.width * 35 / 100;
let [main, side] =
Layout::horizontal([Constraint::Fill(1), Constraint::Length(panel_width)])
.areas(body_area);
(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))
}
let panel_width = body_area.width * 35 / 100;
let [main, side] =
Layout::horizontal([Constraint::Fill(1), Constraint::Length(panel_width)])
.areas(body_area);
(main, Some(side))
} else {
(body_area, None)
};

View File

@@ -66,3 +66,6 @@ mod case_statement;
#[path = "forth/harmony.rs"]
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");
}