Skip to content
Open
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
994 changes: 140 additions & 854 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ trem = { path = "crates/trem", version = "0.1.0" }
trem-cpal = { path = "crates/trem-cpal", version = "0.1.0" }
trem-tui = { path = "crates/trem-tui", version = "0.1.0" }
trem-rung = { path = "crates/trem-rung", version = "0.1.0", features = ["midi"] }
ratatui = "0.30"
ratatui = { version = "0.30", default-features = false }
crossterm = "0.28"
cpal = "0.17"
rtrb = "0.3"
anyhow = "1"
clap = { version = "4.5", features = ["derive"] }
num-rational = "0.4"
ratzilla = "0.3"
wasm-bindgen = "0.2"
console_error_panic_hook = "0.1"

[package]
name = "trem-bin"
Expand Down Expand Up @@ -51,4 +54,4 @@ anyhow = { workspace = true }
clap = { workspace = true }
num-rational = { workspace = true }
crossterm = { workspace = true }
ratatui = { workspace = true }
ratatui = { workspace = true, default-features = false, features = ["std", "crossterm_0_28"] }
3 changes: 3 additions & 0 deletions crates/trem-cpal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ trem = { workspace = true }
cpal = { workspace = true }
rtrb = { workspace = true }
anyhow = { workspace = true }

[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
cpal = { workspace = true, features = ["wasm-bindgen"] }
9 changes: 7 additions & 2 deletions crates/trem-tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,19 @@ trem = { workspace = true, features = ["serde"] }
trem-rung = { workspace = true }
trem-cpal = { workspace = true }
num-rational = { workspace = true }
ratatui = { workspace = true }
crossterm = { workspace = true }
ratatui = { workspace = true, default-features = false, features = ["std", "all-widgets", "layout-cache"] }
anyhow = { workspace = true }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
realfft = "3.5.0"
sysinfo = "0.33"

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
crossterm = { workspace = true }

[target.'cfg(target_arch = "wasm32")'.dependencies]
js-sys = "0.3"

[dev-dependencies]
divan = "0.1.21"

Expand Down
110 changes: 75 additions & 35 deletions crates/trem-tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
//!
//! [`App::run`] is the event loop (draw, input, non-blocking audio poll).

use crate::input::{self, Action, BottomPane, Editor, InputContext, Mode};
#[cfg(not(target_arch = "wasm32"))]
use crate::input::{self, InputContext};
use crate::input::{Action, BottomPane, Editor, Mode};
use crate::project::ProjectData;
use crate::view::graph::GraphViewWidget;
use crate::view::help::HelpOverlay;
Expand All @@ -19,12 +21,14 @@ use trem::math::Rational;
use trem::pitch::Pitch;
use trem_cpal::{Bridge, Command, Notification, ScopeFocus};

#[cfg(not(target_arch = "wasm32"))]
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers};
use ratatui::layout::{Constraint, Direction, Layout};
use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use std::collections::{HashMap, HashSet};
use std::time::{Duration, Instant};
#[cfg(not(target_arch = "wasm32"))]
use std::time::Duration;

use sysinfo::{Pid, ProcessesToUpdate, System};

Expand All @@ -51,6 +55,22 @@ fn cycle_gate(current: Rational) -> Rational {
Rational::new(1, 4)
}

#[inline]
fn now_seconds() -> f64 {
#[cfg(not(target_arch = "wasm32"))]
{
use std::time::UNIX_EPOCH;
std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs_f64()
}
#[cfg(target_arch = "wasm32")]
{
js_sys::Date::now() * 0.001
}
}

/// Mutable state for the full terminal UI: pattern/graph views, audio bridge, and layout data.
pub struct App {
pub grid: trem::grid::Grid,
Expand Down Expand Up @@ -78,7 +98,7 @@ pub struct App {
/// This-process CPU / RSS refreshed ~2× per second for the info panel.
pub host_stats: HostStatsSnapshot,
sys: System,
host_stats_last_refresh: Instant,
host_stats_last_refresh: f64,
/// Peak-decay time constant for spectrum bars (ms); lower = snappier, higher = longer “tail”.
pub spectrum_fall_ms: f64,
spectrum_analyzer_in: SpectrumAnalyzerState,
Expand All @@ -103,7 +123,7 @@ pub struct App {
pub undo_stack: Vec<Vec<Option<NoteEvent>>>,
pub redo_stack: Vec<Vec<Option<NoteEvent>>>,
rng_state: u64,
preview_note_off: Option<(u32, Instant)>,
preview_note_off: Option<(u32, f64)>,
pub bottom_pane: BottomPane,
/// Path into nested graphs for the graph editor (empty = root).
pub graph_path: Vec<u32>,
Expand Down Expand Up @@ -165,9 +185,7 @@ impl App {
scope_graph_in: Vec::new(),
host_stats: HostStatsSnapshot::default(),
sys: System::new(),
host_stats_last_refresh: Instant::now()
.checked_sub(Duration::from_secs(1))
.unwrap_or_else(Instant::now),
host_stats_last_refresh: now_seconds() - 1.0,
spectrum_fall_ms: 18.0,
spectrum_analyzer_in: SpectrumAnalyzerState::new(18.0),
spectrum_analyzer_out: SpectrumAnalyzerState::new(18.0),
Expand All @@ -190,10 +208,19 @@ impl App {
euclidean_k: 0,
undo_stack: Vec::new(),
redo_stack: Vec::new(),
rng_state: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64,
rng_state: {
#[cfg(not(target_arch = "wasm32"))]
{
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64
}
#[cfg(target_arch = "wasm32")]
{
js_sys::Date::now() as u64
}
},
preview_note_off: None,
bottom_pane: BottomPane::Spectrum,
graph_path: Vec::new(),
Expand Down Expand Up @@ -283,28 +310,40 @@ impl App {
}

fn refresh_host_stats(&mut self) {
if self.host_stats_last_refresh.elapsed() < Duration::from_millis(520) {
#[cfg(target_arch = "wasm32")]
{
// Browser/wasm has no process id API; keep perf panel stable.
self.host_stats.process_cpu_pct = 0.0;
self.host_stats.process_rss_mb = 0;
return;
}
self.host_stats_last_refresh = Instant::now();
self.sys.refresh_cpu_usage();
let pid = Pid::from_u32(std::process::id());
self.sys
.refresh_processes(ProcessesToUpdate::Some(&[pid]), false);
if let Some(p) = self.sys.process(pid) {
// Smoothed: raw `cpu_usage` is per refresh window and can spike (UI redraw + audio).
let raw = p.cpu_usage();
let prev = self.host_stats.process_cpu_pct;
const SMOOTH: f32 = 0.22;
self.host_stats.process_cpu_pct = if prev <= f32::EPSILON {
raw

#[cfg(not(target_arch = "wasm32"))]
{
let now = now_seconds();
if (now - self.host_stats_last_refresh) < 0.520 {
return;
}
self.host_stats_last_refresh = now;
self.sys.refresh_cpu_usage();
let pid = Pid::from_u32(std::process::id());
self.sys
.refresh_processes(ProcessesToUpdate::Some(&[pid]), false);
if let Some(p) = self.sys.process(pid) {
// Smoothed: raw `cpu_usage` is per refresh window and can spike (UI redraw + audio).
let raw = p.cpu_usage();
let prev = self.host_stats.process_cpu_pct;
const SMOOTH: f32 = 0.22;
self.host_stats.process_cpu_pct = if prev <= f32::EPSILON {
raw
} else {
prev * (1.0 - SMOOTH) + raw * SMOOTH
};
self.host_stats.process_rss_mb = p.memory() / 1024 / 1024;
} else {
prev * (1.0 - SMOOTH) + raw * SMOOTH
};
self.host_stats.process_rss_mb = p.memory() / 1024 / 1024;
} else {
self.host_stats.process_cpu_pct = 0.0;
self.host_stats.process_rss_mb = 0;
self.host_stats.process_cpu_pct = 0.0;
self.host_stats.process_rss_mb = 0;
}
}
}

Expand Down Expand Up @@ -534,7 +573,7 @@ impl App {
velocity: vel,
voice: voice_id,
});
self.preview_note_off = Some((voice_id, Instant::now()));
self.preview_note_off = Some((voice_id, now_seconds()));

self.grid.set(self.cursor_row, self.cursor_col, Some(event));

Expand Down Expand Up @@ -780,7 +819,7 @@ impl App {
velocity: 0.6,
voice,
});
self.preview_note_off = Some((voice, Instant::now()));
self.preview_note_off = Some((voice, now_seconds()));
}

fn next_rng(&mut self) -> u64 {
Expand Down Expand Up @@ -940,8 +979,8 @@ impl App {
/// Drains pending [`Notification`]s and timed preview note-off; call each frame from the UI loop.
pub fn poll_audio(&mut self) {
// Handle preview note release
if let Some((voice, time)) = self.preview_note_off {
if time.elapsed() > Duration::from_millis(120) {
if let Some((voice, at)) = self.preview_note_off {
if (now_seconds() - at) > 0.120 {
self.bridge.send(Command::NoteOff { voice });
self.preview_note_off = None;
}
Expand Down Expand Up @@ -1165,7 +1204,7 @@ impl App {
}
}

let now = Instant::now();
let now = now_seconds();
self.spectrum_analyzer_in.fall_ms = self.spectrum_fall_ms;
self.spectrum_analyzer_out.fall_ms = self.spectrum_fall_ms;
let (spec_in, nr_in) = self.spectrum_analyzer_in.analyze(&self.scope_graph_in, now);
Expand Down Expand Up @@ -1242,6 +1281,7 @@ impl App {
}

/// Terminal main loop until quit: render, handle keys, poll notifications.
#[cfg(not(target_arch = "wasm32"))]
pub fn run<B>(mut self, terminal: &mut ratatui::Terminal<B>) -> anyhow::Result<()>
where
B: ratatui::backend::Backend,
Expand Down
Loading
Loading