Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions console/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod animation_panel;
mod audio_panel;
pub mod bootstrap_controller;
mod midi_panel;
pub mod startup_config;
mod ui_util;

use std::sync::atomic::{AtomicBool, Ordering};
Expand Down
9 changes: 8 additions & 1 deletion console/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ fn main() -> Result<()> {
let (send_control_event, recv_control_event) = channel();
install_midi_device_change_handler(ControlEventHandler(send_control_event.clone()))?;

// Ask how many clock wings to configure before opening the main window.
// Closing the splash without starting exits cleanly.
let Some(config) = console::startup_config::run_startup_config()? else {
return Ok(());
};
let n_clock_wings = config.n_clock_wings;

let client = CommandClient::new(send_control_event.clone());

let admin: Arc<dyn console::admin_panel::AdminService> =
Expand Down Expand Up @@ -128,7 +135,7 @@ fn main() -> Result<()> {
let (envelope_tx, envelope_rx) = channel();

std::thread::spawn(move || {
let mut show = Show::new(send, recv, show_gui_state, envelope_tx)
let mut show = Show::new(send, recv, show_gui_state, envelope_tx, n_clock_wings)
.expect("show construction should not fail at startup");
loop {
if let Err(e) = show.run(RENDER_INTERVAL) {
Expand Down
105 changes: 105 additions & 0 deletions console/src/startup_config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use std::sync::{Arc, Mutex};

use anyhow::Result;
use eframe::egui;
use tunnels::clock_bank::{CLOCKS_PER_WING, MAX_CLOCKS};

/// The maximum number of clock wings the session may configure.
pub const MAX_WINGS: usize = MAX_CLOCKS / CLOCKS_PER_WING;

/// The session configuration chosen at startup.
#[derive(Debug, Clone, Copy)]
pub struct StartupConfig {
/// Number of clock-control wings, each contributing `CLOCKS_PER_WING` clocks.
pub n_clock_wings: usize,
}

/// Clamp a requested wing count into the valid `1..=MAX_WINGS` range.
fn clamp_wings(n: usize) -> usize {
n.clamp(1, MAX_WINGS)
}

struct StartupConfigApp {
/// Written when the user commits a choice, read after the window closes.
result: Arc<Mutex<Option<StartupConfig>>>,
n_clock_wings: usize,
}

impl eframe::App for StartupConfigApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.vertical_centered(|ui| {
ui.add_space(16.0);
ui.heading("Tunnels");
ui.add_space(24.0);

ui.horizontal(|ui| {
ui.label("Clock wings:");
ui.add(egui::DragValue::new(&mut self.n_clock_wings).range(1..=MAX_WINGS));
});
self.n_clock_wings = clamp_wings(self.n_clock_wings);

let n_clocks = self.n_clock_wings * CLOCKS_PER_WING;
ui.add_space(8.0);
ui.label(format!("{n_clocks} clocks"));

ui.add_space(24.0);
if ui.button("Start").clicked() {
*self.result.lock().expect("startup config mutex poisoned") =
Some(StartupConfig {
n_clock_wings: self.n_clock_wings,
});
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
});
});
}
}

/// Run the startup config splash, blocking until the user starts a session or
/// closes the window. Returns the chosen config, or `None` if the window was
/// closed without starting.
pub fn run_startup_config() -> Result<Option<StartupConfig>> {
let result: Arc<Mutex<Option<StartupConfig>>> = Arc::new(Mutex::new(None));
let app_result = result.clone();

let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([320.0, 220.0])
.with_resizable(false)
.with_icon(std::sync::Arc::new(egui::IconData::default())),
..Default::default()
};

eframe::run_native(
"Tunnels",
options,
Box::new(move |cc| {
stage_theme::apply(&cc.egui_ctx);
Ok(Box::new(StartupConfigApp {
result: app_result,
n_clock_wings: 1,
}))
}),
)
.map_err(|e| anyhow::anyhow!("eframe startup config window failed: {e}"))?;

let inner = Arc::try_unwrap(result)
.map_err(|_| anyhow::anyhow!("startup config Arc still shared"))?
.into_inner()
.expect("startup config mutex poisoned");
Ok(inner)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn clamps_wings_to_valid_range() {
assert_eq!(clamp_wings(0), 1);
assert_eq!(clamp_wings(1), 1);
assert_eq!(clamp_wings(MAX_WINGS), MAX_WINGS);
assert_eq!(clamp_wings(MAX_WINGS + 5), MAX_WINGS);
}
}
1 change: 1 addition & 0 deletions tunnels/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ rmp-serde.workspace = true

rosc = "0.11"
noise = "0.9.0"
arrayvec = { version = "0.7", features = ["serde"] }

egui_plot = "0.34"
eframe = "0.33"
Expand Down
35 changes: 14 additions & 21 deletions tunnels/src/animation.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
use crate::clock::Clock;
use crate::clock::ControllableClock;
use crate::clock::Ticks;
use crate::clock_bank::{ClockIdxExt, ClockStore};
use crate::clock_bank::ClockStore;
use crate::master_ui::EmitStateChange as EmitShowStateChange;
use crate::waveforms::WaveformArgs;
use crate::{clock_bank::ClockIdx, waveforms};
use log::error;
use noise::NoiseFn;
use noise::Simplex;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -106,14 +105,15 @@ impl Animation {
fn phase(&self, external_clocks: &impl ClockStore) -> Phase {
match self.clock_source {
None => self.internal_clock.phase(),
Some(id) => external_clocks.phase(id),
// A selected clock that no longer exists reads as the neutral default.
Some(id) => external_clocks.phase(id).unwrap_or_default(),
}
}

fn ticks(&self, external_clocks: &impl ClockStore) -> Ticks {
match self.clock_source {
None => self.internal_clock.ticks(),
Some(id) => external_clocks.ticks(id),
Some(id) => external_clocks.ticks(id).unwrap_or_default(),
}
}

Expand Down Expand Up @@ -236,8 +236,14 @@ impl Animation {
// scale this animation by submaster level if using external clock
let mut use_audio_size = self.use_audio_size;
if let Some(id) = self.clock_source {
v *= external_clocks.submaster_level(id).val();
use_audio_size = use_audio_size || external_clocks.use_audio_size(id);
// A selected clock that no longer exists reads as the neutral default
// (submaster 0 → this animation contributes nothing).
v *= external_clocks
.submaster_level(id)
.unwrap_or_default()
.val();
use_audio_size =
use_audio_size || external_clocks.use_audio_size(id).unwrap_or_default();
}
// scale this animation by audio envelope if set
if use_audio_size {
Expand Down Expand Up @@ -287,16 +293,6 @@ impl Animation {
match msg {
Set(sc) => self.handle_state_change(sc, emitter),
SetClockSource(source) => {
let source: Option<ClockIdx> = match source {
Some(s) => match s.try_into() {
Ok(s) => Some(s),
Err(e) => {
error!("could not process animation control message: {e}");
return;
}
},
None => None,
};
self.handle_state_change(StateChange::ClockSource(source), emitter);
}
TogglePulse => {
Expand Down Expand Up @@ -363,11 +359,8 @@ pub enum StateChange {
#[derive(Debug, Clone)]
pub enum ControlMessage {
Set(StateChange),
/// Since clock IDs need to be validated, this path handles the fallible case.
/// FIXME: it would be nicer to validate this at control message creation time,
/// but at the moment control message creator functions are infallible and
/// that's more refactoring than I want to deal with right now.
SetClockSource(Option<ClockIdxExt>),
/// Set the clock that drives this animation, or `None` for the internal clock.
SetClockSource(Option<ClockIdx>),
TogglePulse,
ToggleStanding,
ToggleInvert,
Expand Down
Loading
Loading