Refactoring
This commit is contained in:
@@ -4,14 +4,15 @@ use std::collections::HashMap;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use crossbeam_channel::Sender;
|
||||||
|
|
||||||
use crate::commands::AppCommand;
|
use crate::commands::AppCommand;
|
||||||
use crate::config::MAX_SLOTS;
|
use crate::config::MAX_SLOTS;
|
||||||
use crate::file;
|
use crate::engine::{
|
||||||
use crate::link::LinkState;
|
LinkState, PatternSnapshot, SeqCommand, SequencerSnapshot, SlotChange, StepSnapshot,
|
||||||
use crate::model::Pattern;
|
};
|
||||||
|
use crate::model::{self, Pattern, Rng, ScriptEngine, StepContext, Variables};
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::script::{Rng, ScriptEngine, StepContext, Variables};
|
|
||||||
use crate::sequencer::{SequencerSnapshot, SlotChange};
|
|
||||||
use crate::services::pattern_editor;
|
use crate::services::pattern_editor;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
AudioSettings, EditorContext, Focus, Metrics, Modal, PatternField, PatternsViewLevel,
|
AudioSettings, EditorContext, Focus, Metrics, Modal, PatternField, PatternsViewLevel,
|
||||||
@@ -334,14 +335,14 @@ impl App {
|
|||||||
pattern: usize,
|
pattern: usize,
|
||||||
snapshot: &SequencerSnapshot,
|
snapshot: &SequencerSnapshot,
|
||||||
) -> Option<bool> {
|
) -> Option<bool> {
|
||||||
self.playback.queued_changes.iter().find_map(|c| match c {
|
self.playback.queued_changes.iter().find_map(|c| match *c {
|
||||||
SlotChange::Add {
|
SlotChange::Add {
|
||||||
slot: _,
|
slot: _,
|
||||||
bank: b,
|
bank: b,
|
||||||
pattern: p,
|
pattern: p,
|
||||||
} if *b == bank && *p == pattern => Some(true),
|
} if b == bank && p == pattern => Some(true),
|
||||||
SlotChange::Remove { slot } => {
|
SlotChange::Remove { slot } => {
|
||||||
let s = snapshot.slot_data[*slot];
|
let s = snapshot.slot_data[slot];
|
||||||
if s.active && s.bank == bank && s.pattern == pattern {
|
if s.active && s.bank == bank && s.pattern == pattern {
|
||||||
Some(false)
|
Some(false)
|
||||||
} else {
|
} else {
|
||||||
@@ -366,14 +367,14 @@ impl App {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let pending = self.playback.queued_changes.iter().position(|c| match c {
|
let pending = self.playback.queued_changes.iter().position(|c| match *c {
|
||||||
SlotChange::Add {
|
SlotChange::Add {
|
||||||
bank: b,
|
bank: b,
|
||||||
pattern: p,
|
pattern: p,
|
||||||
..
|
..
|
||||||
} => *b == bank && *p == pattern,
|
} => b == bank && p == pattern,
|
||||||
SlotChange::Remove { slot } => {
|
SlotChange::Remove { slot } => {
|
||||||
let s = snapshot.slot_data[*slot];
|
let s = snapshot.slot_data[slot];
|
||||||
s.bank == bank && s.pattern == pattern
|
s.bank == bank && s.pattern == pattern
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -428,7 +429,7 @@ impl App {
|
|||||||
|
|
||||||
pub fn save(&mut self, path: PathBuf) {
|
pub fn save(&mut self, path: PathBuf) {
|
||||||
self.save_editor_to_step();
|
self.save_editor_to_step();
|
||||||
match file::save(&self.project_state.project, &path) {
|
match model::save(&self.project_state.project, &path) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
self.ui.set_status(format!("Saved: {}", path.display()));
|
self.ui.set_status(format!("Saved: {}", path.display()));
|
||||||
self.project_state.file_path = Some(path);
|
self.project_state.file_path = Some(path);
|
||||||
@@ -440,7 +441,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(&mut self, path: PathBuf, link: &LinkState) {
|
pub fn load(&mut self, path: PathBuf, link: &LinkState) {
|
||||||
match file::load(&path) {
|
match model::load(&path) {
|
||||||
Ok(project) => {
|
Ok(project) => {
|
||||||
self.project_state.project = project;
|
self.project_state.project = project;
|
||||||
self.editor_ctx.step = 0;
|
self.editor_ctx.step = 0;
|
||||||
@@ -663,4 +664,49 @@ impl App {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn flush_queued_changes(&mut self, cmd_tx: &Sender<SeqCommand>) {
|
||||||
|
for change in self.playback.queued_changes.drain(..) {
|
||||||
|
match change {
|
||||||
|
SlotChange::Add {
|
||||||
|
slot,
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
} => {
|
||||||
|
let _ = cmd_tx.send(SeqCommand::SlotAdd {
|
||||||
|
slot,
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
SlotChange::Remove { slot } => {
|
||||||
|
let _ = cmd_tx.send(SeqCommand::SlotRemove { slot });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flush_dirty_patterns(&mut self, cmd_tx: &Sender<SeqCommand>) {
|
||||||
|
for (bank, pattern) in self.project_state.take_dirty() {
|
||||||
|
let pat = self.project_state.project.pattern_at(bank, pattern);
|
||||||
|
let snapshot = PatternSnapshot {
|
||||||
|
speed: pat.speed,
|
||||||
|
length: pat.length,
|
||||||
|
steps: pat
|
||||||
|
.steps
|
||||||
|
.iter()
|
||||||
|
.take(pat.length)
|
||||||
|
.map(|s| StepSnapshot {
|
||||||
|
active: s.active,
|
||||||
|
script: s.script.clone(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
let _ = cmd_tx.send(SeqCommand::PatternUpdate {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
data: snapshot,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::engine::SlotChange;
|
||||||
use crate::model::PatternSpeed;
|
use crate::model::PatternSpeed;
|
||||||
use crate::sequencer::SlotChange;
|
|
||||||
use crate::state::{Modal, PatternField};
|
use crate::state::{Modal, PatternField};
|
||||||
|
|
||||||
pub enum AppCommand {
|
pub enum AppCommand {
|
||||||
|
|||||||
@@ -5,29 +5,49 @@ use doux::{Engine, EngineMetrics};
|
|||||||
use std::sync::atomic::{AtomicU32, Ordering};
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::sequencer::AudioCommand;
|
use super::AudioCommand;
|
||||||
|
|
||||||
pub struct ScopeBuffer {
|
pub struct ScopeBuffer {
|
||||||
pub samples: [AtomicU32; 64],
|
pub samples: [AtomicU32; 64],
|
||||||
|
peak_left: AtomicU32,
|
||||||
|
peak_right: AtomicU32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScopeBuffer {
|
impl ScopeBuffer {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
samples: std::array::from_fn(|_| AtomicU32::new(0)),
|
samples: std::array::from_fn(|_| AtomicU32::new(0)),
|
||||||
|
peak_left: AtomicU32::new(0),
|
||||||
|
peak_right: AtomicU32::new(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(&self, data: &[f32]) {
|
pub fn write(&self, data: &[f32]) {
|
||||||
|
let mut peak_l: f32 = 0.0;
|
||||||
|
let mut peak_r: f32 = 0.0;
|
||||||
|
|
||||||
for (i, atom) in self.samples.iter().enumerate() {
|
for (i, atom) in self.samples.iter().enumerate() {
|
||||||
let val = data.get(i * 2).copied().unwrap_or(0.0);
|
let idx = i * 2;
|
||||||
atom.store(val.to_bits(), Ordering::Relaxed);
|
let left = data.get(idx).copied().unwrap_or(0.0);
|
||||||
|
let right = data.get(idx + 1).copied().unwrap_or(0.0);
|
||||||
|
peak_l = peak_l.max(left.abs());
|
||||||
|
peak_r = peak_r.max(right.abs());
|
||||||
|
atom.store(left.to_bits(), Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.peak_left.store(peak_l.to_bits(), Ordering::Relaxed);
|
||||||
|
self.peak_right.store(peak_r.to_bits(), Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read(&self) -> [f32; 64] {
|
pub fn read(&self) -> [f32; 64] {
|
||||||
std::array::from_fn(|i| f32::from_bits(self.samples[i].load(Ordering::Relaxed)))
|
std::array::from_fn(|i| f32::from_bits(self.samples[i].load(Ordering::Relaxed)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn peaks(&self) -> (f32, f32) {
|
||||||
|
let left = f32::from_bits(self.peak_left.load(Ordering::Relaxed));
|
||||||
|
let right = f32::from_bits(self.peak_right.load(Ordering::Relaxed));
|
||||||
|
(left, right)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AudioStreamConfig {
|
pub struct AudioStreamConfig {
|
||||||
@@ -56,7 +76,9 @@ pub fn build_stream(
|
|||||||
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)
|
||||||
.ok_or_else(|| format!("Device not found: {name}"))?,
|
.ok_or_else(|| format!("Device not found: {name}"))?,
|
||||||
None => host.default_output_device().ok_or("No default output device")?,
|
None => host
|
||||||
|
.default_output_device()
|
||||||
|
.ok_or("No default output device")?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let default_config = device.default_output_config().map_err(|e| e.to_string())?;
|
let default_config = device.default_output_config().map_err(|e| e.to_string())?;
|
||||||
@@ -104,7 +126,8 @@ pub fn build_stream(
|
|||||||
}
|
}
|
||||||
AudioCommand::ResetEngine => {
|
AudioCommand::ResetEngine => {
|
||||||
let old_samples = std::mem::take(&mut engine.sample_index);
|
let old_samples = std::mem::take(&mut engine.sample_index);
|
||||||
engine = Engine::new_with_metrics(sr, channels, Arc::clone(&metrics_clone));
|
engine =
|
||||||
|
Engine::new_with_metrics(sr, channels, Arc::clone(&metrics_clone));
|
||||||
engine.sample_index = old_samples;
|
engine.sample_index = old_samples;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,6 +142,8 @@ pub fn build_stream(
|
|||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to build stream: {e}"))?;
|
.map_err(|e| format!("Failed to build stream: {e}"))?;
|
||||||
|
|
||||||
stream.play().map_err(|e| format!("Failed to play stream: {e}"))?;
|
stream
|
||||||
|
.play()
|
||||||
|
.map_err(|e| format!("Failed to play stream: {e}"))?;
|
||||||
Ok((stream, sample_rate))
|
Ok((stream, sample_rate))
|
||||||
}
|
}
|
||||||
10
seq/src/engine/mod.rs
Normal file
10
seq/src/engine/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
mod audio;
|
||||||
|
mod link;
|
||||||
|
mod sequencer;
|
||||||
|
|
||||||
|
pub use audio::{build_stream, AudioStreamConfig, PatternSlot, ScopeBuffer};
|
||||||
|
pub use link::LinkState;
|
||||||
|
pub use sequencer::{
|
||||||
|
spawn_sequencer, AudioCommand, PatternSnapshot, SeqCommand, SequencerSnapshot, SlotChange,
|
||||||
|
StepSnapshot,
|
||||||
|
};
|
||||||
@@ -4,10 +4,9 @@ use std::sync::Arc;
|
|||||||
use std::thread::{self, JoinHandle};
|
use std::thread::{self, JoinHandle};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::audio::PatternSlot;
|
use super::{LinkState, PatternSlot};
|
||||||
use crate::config::{MAX_BANKS, MAX_PATTERNS, MAX_SLOTS};
|
use crate::config::{MAX_BANKS, MAX_PATTERNS, MAX_SLOTS};
|
||||||
use crate::link::LinkState;
|
use crate::model::{Rng, ScriptEngine, StepContext, Variables};
|
||||||
use crate::script::{Rng, ScriptEngine, StepContext, Variables};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum SlotChange {
|
pub enum SlotChange {
|
||||||
@@ -6,10 +6,9 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::commands::AppCommand;
|
use crate::commands::AppCommand;
|
||||||
use crate::link::LinkState;
|
use crate::engine::{AudioCommand, LinkState, SequencerSnapshot};
|
||||||
use crate::model::PatternSpeed;
|
use crate::model::PatternSpeed;
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::sequencer::{AudioCommand, SequencerSnapshot};
|
|
||||||
use crate::state::{AudioFocus, Focus, Modal, PatternField, PatternsViewLevel};
|
use crate::state::{AudioFocus, Focus, Modal, PatternField, PatternsViewLevel};
|
||||||
|
|
||||||
pub enum InputResult {
|
pub enum InputResult {
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
mod app;
|
mod app;
|
||||||
mod audio;
|
|
||||||
mod commands;
|
mod commands;
|
||||||
mod config;
|
mod config;
|
||||||
mod file;
|
mod engine;
|
||||||
mod input;
|
mod input;
|
||||||
mod link;
|
|
||||||
mod model;
|
mod model;
|
||||||
mod page;
|
mod page;
|
||||||
mod script;
|
|
||||||
mod sequencer;
|
|
||||||
mod services;
|
mod services;
|
||||||
mod state;
|
mod state;
|
||||||
mod ui;
|
|
||||||
mod views;
|
mod views;
|
||||||
mod widgets;
|
mod widgets;
|
||||||
|
|
||||||
@@ -32,10 +27,8 @@ use ratatui::prelude::CrosstermBackend;
|
|||||||
use ratatui::Terminal;
|
use ratatui::Terminal;
|
||||||
|
|
||||||
use app::App;
|
use app::App;
|
||||||
use audio::{AudioStreamConfig, ScopeBuffer};
|
use engine::{build_stream, spawn_sequencer, AudioStreamConfig, LinkState, ScopeBuffer};
|
||||||
use input::{handle_key, InputContext, InputResult};
|
use input::{handle_key, InputContext, InputResult};
|
||||||
use link::LinkState;
|
|
||||||
use sequencer::{spawn_sequencer, PatternSnapshot, SeqCommand, SlotChange, StepSnapshot};
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(name = "seq", about = "A step sequencer with Ableton Link support")]
|
#[command(name = "seq", about = "A step sequencer with Ableton Link support")]
|
||||||
@@ -103,7 +96,7 @@ fn main() -> io::Result<()> {
|
|||||||
buffer_size: app.audio.config.buffer_size,
|
buffer_size: app.audio.config.buffer_size,
|
||||||
};
|
};
|
||||||
|
|
||||||
let (mut stream, sample_rate) = audio::build_stream(
|
let (mut stream, sample_rate) = build_stream(
|
||||||
&stream_config,
|
&stream_config,
|
||||||
sequencer.audio_rx.clone(),
|
sequencer.audio_rx.clone(),
|
||||||
Arc::clone(&scope_buffer),
|
Arc::clone(&scope_buffer),
|
||||||
@@ -139,7 +132,7 @@ fn main() -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
app.audio.config.sample_count = restart_samples.len();
|
app.audio.config.sample_count = restart_samples.len();
|
||||||
|
|
||||||
match audio::build_stream(
|
match build_stream(
|
||||||
&new_config,
|
&new_config,
|
||||||
sequencer.audio_rx.clone(),
|
sequencer.audio_rx.clone(),
|
||||||
Arc::clone(&scope_buffer),
|
Arc::clone(&scope_buffer),
|
||||||
@@ -158,7 +151,7 @@ fn main() -> io::Result<()> {
|
|||||||
let index = doux::loader::scan_samples_dir(path);
|
let index = doux::loader::scan_samples_dir(path);
|
||||||
fallback_samples.extend(index);
|
fallback_samples.extend(index);
|
||||||
}
|
}
|
||||||
let (fallback_stream, _) = audio::build_stream(
|
let (fallback_stream, _) = build_stream(
|
||||||
&AudioStreamConfig {
|
&AudioStreamConfig {
|
||||||
output_device: None,
|
output_device: None,
|
||||||
channels: 2,
|
channels: 2,
|
||||||
@@ -183,53 +176,16 @@ fn main() -> io::Result<()> {
|
|||||||
app.metrics.cpu_load = metrics.load.get_load();
|
app.metrics.cpu_load = metrics.load.get_load();
|
||||||
app.metrics.schedule_depth = metrics.schedule_depth.load(Ordering::Relaxed) as usize;
|
app.metrics.schedule_depth = metrics.schedule_depth.load(Ordering::Relaxed) as usize;
|
||||||
app.metrics.scope = scope_buffer.read();
|
app.metrics.scope = scope_buffer.read();
|
||||||
|
(app.metrics.peak_left, app.metrics.peak_right) = scope_buffer.peaks();
|
||||||
}
|
}
|
||||||
|
|
||||||
let seq_snapshot = sequencer.snapshot();
|
let seq_snapshot = sequencer.snapshot();
|
||||||
app.metrics.event_count = seq_snapshot.event_count;
|
app.metrics.event_count = seq_snapshot.event_count;
|
||||||
|
|
||||||
for change in app.playback.queued_changes.drain(..) {
|
app.flush_queued_changes(&sequencer.cmd_tx);
|
||||||
match change {
|
app.flush_dirty_patterns(&sequencer.cmd_tx);
|
||||||
SlotChange::Add {
|
|
||||||
slot,
|
|
||||||
bank,
|
|
||||||
pattern,
|
|
||||||
} => {
|
|
||||||
let _ = sequencer.cmd_tx.send(SeqCommand::SlotAdd {
|
|
||||||
slot,
|
|
||||||
bank,
|
|
||||||
pattern,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
SlotChange::Remove { slot } => {
|
|
||||||
let _ = sequencer.cmd_tx.send(SeqCommand::SlotRemove { slot });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (bank, pattern) in app.project_state.take_dirty() {
|
terminal.draw(|frame| views::render(frame, &mut app, &link, &seq_snapshot))?;
|
||||||
let pat = app.project_state.project.pattern_at(bank, pattern);
|
|
||||||
let snapshot = PatternSnapshot {
|
|
||||||
speed: pat.speed,
|
|
||||||
length: pat.length,
|
|
||||||
steps: pat
|
|
||||||
.steps
|
|
||||||
.iter()
|
|
||||||
.take(pat.length)
|
|
||||||
.map(|s| StepSnapshot {
|
|
||||||
active: s.active,
|
|
||||||
script: s.script.clone(),
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
};
|
|
||||||
let _ = sequencer.cmd_tx.send(SeqCommand::PatternUpdate {
|
|
||||||
bank,
|
|
||||||
pattern,
|
|
||||||
data: snapshot,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
terminal.draw(|frame| ui::render(frame, &mut app, &link, &seq_snapshot))?;
|
|
||||||
|
|
||||||
if event::poll(Duration::from_millis(16))? {
|
if event::poll(Duration::from_millis(16))? {
|
||||||
if let Event::Key(key) = event::read()? {
|
if let Event::Key(key) = event::read()? {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use std::path::Path;
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::model::{Bank, Project};
|
use super::{Bank, Project};
|
||||||
|
|
||||||
const VERSION: u8 = 1;
|
const VERSION: u8 = 1;
|
||||||
|
|
||||||
7
seq/src/model/mod.rs
Normal file
7
seq/src/model/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
mod file;
|
||||||
|
mod project;
|
||||||
|
mod script;
|
||||||
|
|
||||||
|
pub use file::{load, save};
|
||||||
|
pub use project::{Bank, Pattern, PatternSpeed, Project};
|
||||||
|
pub use script::{Rng, ScriptEngine, StepContext, Variables};
|
||||||
@@ -9,9 +9,9 @@ pub enum PatternSpeed {
|
|||||||
Half, // 1/2x
|
Half, // 1/2x
|
||||||
#[default]
|
#[default]
|
||||||
Normal, // 1x
|
Normal, // 1x
|
||||||
Double, // 2x
|
Double, // 2x
|
||||||
Quad, // 4x
|
Quad, // 4x
|
||||||
Octo, // 8x
|
Octo, // 8x
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PatternSpeed {
|
impl PatternSpeed {
|
||||||
@@ -43,11 +43,7 @@ impl Cmd {
|
|||||||
|
|
||||||
impl std::fmt::Display for Cmd {
|
impl std::fmt::Display for Cmd {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let parts: Vec<String> = self
|
let parts: Vec<String> = self.pairs.iter().map(|(k, v)| format!("{k}/{v}")).collect();
|
||||||
.pairs
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| format!("{k}/{v}"))
|
|
||||||
.collect();
|
|
||||||
write!(f, "/{}", parts.join("/"))
|
write!(f, "/{}", parts.join("/"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,10 +73,7 @@ impl ScriptEngine {
|
|||||||
let vars_for_get = Arc::clone(&vars);
|
let vars_for_get = Arc::clone(&vars);
|
||||||
|
|
||||||
engine.register_fn("set", move |name: &str, value: Dynamic| {
|
engine.register_fn("set", move |name: &str, value: Dynamic| {
|
||||||
vars_for_set
|
vars_for_set.lock().unwrap().insert(name.to_string(), value);
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.insert(name.to_string(), value);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
engine.register_fn("get", move |name: &str| -> Dynamic {
|
engine.register_fn("get", move |name: &str| -> Dynamic {
|
||||||
@@ -102,11 +95,17 @@ impl ScriptEngine {
|
|||||||
rng_rand_ff.lock().unwrap().gen_range(min..max)
|
rng_rand_ff.lock().unwrap().gen_range(min..max)
|
||||||
});
|
});
|
||||||
engine.register_fn("rand", move |min: i64, max: i64| -> f64 {
|
engine.register_fn("rand", move |min: i64, max: i64| -> f64 {
|
||||||
rng_rand_ii.lock().unwrap().gen_range(min as f64..max as f64)
|
rng_rand_ii
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.gen_range(min as f64..max as f64)
|
||||||
});
|
});
|
||||||
|
|
||||||
engine.register_fn("rrand", move |min: f64, max: f64| -> i64 {
|
engine.register_fn("rrand", move |min: f64, max: f64| -> i64 {
|
||||||
rng_rrand_ff.lock().unwrap().gen_range(min as i64..=max as i64)
|
rng_rrand_ff
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.gen_range(min as i64..=max as i64)
|
||||||
});
|
});
|
||||||
engine.register_fn("rrand", move |min: i64, max: i64| -> i64 {
|
engine.register_fn("rrand", move |min: i64, max: i64| -> i64 {
|
||||||
rng_rrand_ii.lock().unwrap().gen_range(min..=max)
|
rng_rrand_ii.lock().unwrap().gen_range(min..=max)
|
||||||
@@ -162,28 +161,111 @@ fn register_cmd(engine: &mut Engine) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reg_both!(
|
reg_both!(
|
||||||
"time", "repeat", "dur", "gate",
|
"time",
|
||||||
"freq", "detune", "speed", "glide",
|
"repeat",
|
||||||
"pw", "spread", "mult", "warp", "mirror", "harmonics", "timbre", "morph", "begin", "end",
|
"dur",
|
||||||
"gain", "postgain", "velocity", "pan",
|
"gate",
|
||||||
"attack", "decay", "sustain", "release",
|
"freq",
|
||||||
"lpf", "lpq", "lpe", "lpa", "lpd", "lps", "lpr",
|
"detune",
|
||||||
"hpf", "hpq", "hpe", "hpa", "hpd", "hps", "hpr",
|
"speed",
|
||||||
"bpf", "bpq", "bpe", "bpa", "bpd", "bps", "bpr",
|
"glide",
|
||||||
|
"pw",
|
||||||
|
"spread",
|
||||||
|
"mult",
|
||||||
|
"warp",
|
||||||
|
"mirror",
|
||||||
|
"harmonics",
|
||||||
|
"timbre",
|
||||||
|
"morph",
|
||||||
|
"begin",
|
||||||
|
"end",
|
||||||
|
"gain",
|
||||||
|
"postgain",
|
||||||
|
"velocity",
|
||||||
|
"pan",
|
||||||
|
"attack",
|
||||||
|
"decay",
|
||||||
|
"sustain",
|
||||||
|
"release",
|
||||||
|
"lpf",
|
||||||
|
"lpq",
|
||||||
|
"lpe",
|
||||||
|
"lpa",
|
||||||
|
"lpd",
|
||||||
|
"lps",
|
||||||
|
"lpr",
|
||||||
|
"hpf",
|
||||||
|
"hpq",
|
||||||
|
"hpe",
|
||||||
|
"hpa",
|
||||||
|
"hpd",
|
||||||
|
"hps",
|
||||||
|
"hpr",
|
||||||
|
"bpf",
|
||||||
|
"bpq",
|
||||||
|
"bpe",
|
||||||
|
"bpa",
|
||||||
|
"bpd",
|
||||||
|
"bps",
|
||||||
|
"bpr",
|
||||||
"ftype",
|
"ftype",
|
||||||
"penv", "patt", "pdec", "psus", "prel",
|
"penv",
|
||||||
"vib", "vibmod", "vibshape",
|
"patt",
|
||||||
"fm", "fmh", "fmshape", "fme", "fma", "fmd", "fms", "fmr",
|
"pdec",
|
||||||
"am", "amdepth", "amshape",
|
"psus",
|
||||||
"rm", "rmdepth", "rmshape",
|
"prel",
|
||||||
"phaser", "phaserdepth", "phasersweep", "phasercenter",
|
"vib",
|
||||||
"flanger", "flangerdepth", "flangerfeedback",
|
"vibmod",
|
||||||
"chorus", "chorusdepth", "chorusdelay",
|
"vibshape",
|
||||||
"comb", "combfreq", "combfeedback", "combdamp",
|
"fm",
|
||||||
"coarse", "crush", "fold", "wrap", "distort", "distortvol",
|
"fmh",
|
||||||
"delay", "delaytime", "delayfeedback", "delaytype",
|
"fmshape",
|
||||||
"verb", "verbdecay", "verbdamp", "verbpredelay", "verbdiff",
|
"fme",
|
||||||
"voice", "orbit", "note", "size", "n", "cut"
|
"fma",
|
||||||
|
"fmd",
|
||||||
|
"fms",
|
||||||
|
"fmr",
|
||||||
|
"am",
|
||||||
|
"amdepth",
|
||||||
|
"amshape",
|
||||||
|
"rm",
|
||||||
|
"rmdepth",
|
||||||
|
"rmshape",
|
||||||
|
"phaser",
|
||||||
|
"phaserdepth",
|
||||||
|
"phasersweep",
|
||||||
|
"phasercenter",
|
||||||
|
"flanger",
|
||||||
|
"flangerdepth",
|
||||||
|
"flangerfeedback",
|
||||||
|
"chorus",
|
||||||
|
"chorusdepth",
|
||||||
|
"chorusdelay",
|
||||||
|
"comb",
|
||||||
|
"combfreq",
|
||||||
|
"combfeedback",
|
||||||
|
"combdamp",
|
||||||
|
"coarse",
|
||||||
|
"crush",
|
||||||
|
"fold",
|
||||||
|
"wrap",
|
||||||
|
"distort",
|
||||||
|
"distortvol",
|
||||||
|
"delay",
|
||||||
|
"delaytime",
|
||||||
|
"delayfeedback",
|
||||||
|
"delaytype",
|
||||||
|
"verb",
|
||||||
|
"verbdecay",
|
||||||
|
"verbdamp",
|
||||||
|
"verbpredelay",
|
||||||
|
"verbdiff",
|
||||||
|
"voice",
|
||||||
|
"orbit",
|
||||||
|
"note",
|
||||||
|
"size",
|
||||||
|
"n",
|
||||||
|
"cut"
|
||||||
);
|
);
|
||||||
|
|
||||||
engine.register_fn("reset", |c: &mut Cmd, v: bool| {
|
engine.register_fn("reset", |c: &mut Cmd, v: bool| {
|
||||||
@@ -43,6 +43,8 @@ pub struct Metrics {
|
|||||||
pub cpu_load: f32,
|
pub cpu_load: f32,
|
||||||
pub schedule_depth: usize,
|
pub schedule_depth: usize,
|
||||||
pub scope: [f32; 64],
|
pub scope: [f32; 64],
|
||||||
|
pub peak_left: f32,
|
||||||
|
pub peak_right: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Metrics {
|
impl Default for Metrics {
|
||||||
@@ -54,6 +56,8 @@ impl Default for Metrics {
|
|||||||
cpu_load: 0.0,
|
cpu_load: 0.0,
|
||||||
schedule_depth: 0,
|
schedule_depth: 0,
|
||||||
scope: [0.0; 64],
|
scope: [0.0; 64],
|
||||||
|
peak_left: 0.0,
|
||||||
|
peak_right: 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::sequencer::SlotChange;
|
use crate::engine::SlotChange;
|
||||||
|
|
||||||
pub struct PlaybackState {
|
pub struct PlaybackState {
|
||||||
pub playing: bool,
|
pub playing: bool,
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ use ratatui::widgets::{Block, Borders, Paragraph};
|
|||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::sequencer::SequencerSnapshot;
|
use crate::engine::SequencerSnapshot;
|
||||||
use crate::state::Focus;
|
use crate::state::Focus;
|
||||||
use crate::widgets::{Orientation, Scope};
|
use crate::widgets::{Orientation, Scope, VuMeter};
|
||||||
|
|
||||||
pub fn render(frame: &mut Frame, app: &mut App, snapshot: &SequencerSnapshot, area: Rect) {
|
pub fn render(frame: &mut Frame, app: &mut App, snapshot: &SequencerSnapshot, area: Rect) {
|
||||||
let [main_area, scope_area] =
|
let [main_area, scope_area, vu_area] = Layout::horizontal([
|
||||||
Layout::horizontal([Constraint::Fill(1), Constraint::Length(10)]).areas(area);
|
Constraint::Fill(1),
|
||||||
|
Constraint::Length(10),
|
||||||
|
Constraint::Length(10),
|
||||||
|
])
|
||||||
|
.areas(area);
|
||||||
|
|
||||||
let [seq_area, editor_area] =
|
let [seq_area, editor_area] =
|
||||||
Layout::vertical([Constraint::Fill(3), Constraint::Fill(2)]).areas(main_area);
|
Layout::vertical([Constraint::Fill(3), Constraint::Fill(2)]).areas(main_area);
|
||||||
@@ -18,6 +22,7 @@ pub fn render(frame: &mut Frame, app: &mut App, snapshot: &SequencerSnapshot, ar
|
|||||||
render_sequencer(frame, app, snapshot, seq_area);
|
render_sequencer(frame, app, snapshot, seq_area);
|
||||||
render_editor(frame, app, editor_area);
|
render_editor(frame, app, editor_area);
|
||||||
render_scope(frame, app, scope_area);
|
render_scope(frame, app, scope_area);
|
||||||
|
render_vu_meter(frame, app, vu_area);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_sequencer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
fn render_sequencer(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
||||||
@@ -186,8 +191,7 @@ fn render_editor(frame: &mut Frame, app: &mut App, area: Rect) {
|
|||||||
fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
|
fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::new().fg(Color::Rgb(70, 75, 85)))
|
.border_style(Style::new().fg(Color::Rgb(70, 75, 85)));
|
||||||
.title("Scope");
|
|
||||||
|
|
||||||
let inner = block.inner(area);
|
let inner = block.inner(area);
|
||||||
frame.render_widget(block, area);
|
frame.render_widget(block, area);
|
||||||
@@ -197,3 +201,15 @@ fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
.color(Color::Green);
|
.color(Color::Green);
|
||||||
frame.render_widget(scope, inner);
|
frame.render_widget(scope, inner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_vu_meter(frame: &mut Frame, app: &App, area: Rect) {
|
||||||
|
let block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::new().fg(Color::Rgb(70, 75, 85)));
|
||||||
|
|
||||||
|
let inner = block.inner(area);
|
||||||
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
|
let vu = VuMeter::new(app.metrics.peak_left, app.metrics.peak_right);
|
||||||
|
frame.render_widget(vu, inner);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,3 +2,6 @@ pub mod audio_view;
|
|||||||
pub mod doc_view;
|
pub mod doc_view;
|
||||||
pub mod main_view;
|
pub mod main_view;
|
||||||
pub mod patterns_view;
|
pub mod patterns_view;
|
||||||
|
mod render;
|
||||||
|
|
||||||
|
pub use render::render;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use ratatui::widgets::{Block, Borders, Paragraph};
|
|||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::sequencer::SequencerSnapshot;
|
use crate::engine::SequencerSnapshot;
|
||||||
use crate::state::PatternsViewLevel;
|
use crate::state::PatternsViewLevel;
|
||||||
|
|
||||||
pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
pub fn render(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, area: Rect) {
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Modifier, Style};
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
|
use ratatui::widgets::{Block, Borders, Paragraph};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
use crate::link::LinkState;
|
use crate::engine::{LinkState, SequencerSnapshot};
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::sequencer::SequencerSnapshot;
|
|
||||||
use crate::state::{Modal, PatternField};
|
use crate::state::{Modal, PatternField};
|
||||||
use crate::views::{audio_view, doc_view, main_view, patterns_view};
|
use crate::widgets::{ConfirmModal, TextInputModal};
|
||||||
|
|
||||||
|
use super::{audio_view, doc_view, main_view, patterns_view};
|
||||||
|
|
||||||
pub fn render(frame: &mut Frame, app: &mut App, link: &LinkState, snapshot: &SequencerSnapshot) {
|
pub fn render(frame: &mut Frame, app: &mut App, link: &LinkState, snapshot: &SequencerSnapshot) {
|
||||||
let [header_area, body_area, footer_area] = Layout::vertical([
|
let [header_area, body_area, footer_area] = Layout::vertical([
|
||||||
@@ -189,202 +190,61 @@ fn render_footer(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
frame.render_widget(footer, area);
|
frame.render_widget(footer, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn centered_rect(width: u16, height: u16, area: Rect) -> Rect {
|
|
||||||
let x = area.x + area.width.saturating_sub(width) / 2;
|
|
||||||
let y = area.y + area.height.saturating_sub(height) / 2;
|
|
||||||
Rect::new(x, y, width.min(area.width), height.min(area.height))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_modal(frame: &mut Frame, app: &App) {
|
fn render_modal(frame: &mut Frame, app: &App) {
|
||||||
let term = frame.area();
|
let term = frame.area();
|
||||||
match &app.ui.modal {
|
match &app.ui.modal {
|
||||||
Modal::None => {}
|
Modal::None => {}
|
||||||
Modal::ConfirmQuit { selected } => {
|
Modal::ConfirmQuit { selected } => {
|
||||||
let width = 30.min(term.width.saturating_sub(4));
|
ConfirmModal::new("Confirm", "Quit?", *selected).render_centered(frame, term);
|
||||||
let height = 5.min(term.height.saturating_sub(4));
|
|
||||||
let area = centered_rect(width, height, term);
|
|
||||||
frame.render_widget(Clear, area);
|
|
||||||
|
|
||||||
let block = Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.title("Confirm")
|
|
||||||
.border_style(Style::new().fg(Color::Yellow));
|
|
||||||
let inner = block.inner(area);
|
|
||||||
frame.render_widget(block, area);
|
|
||||||
|
|
||||||
let rows =
|
|
||||||
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).split(inner);
|
|
||||||
|
|
||||||
frame.render_widget(
|
|
||||||
Paragraph::new("Quit?").alignment(Alignment::Center),
|
|
||||||
rows[0],
|
|
||||||
);
|
|
||||||
|
|
||||||
let yes_style = if *selected {
|
|
||||||
Style::new().fg(Color::Black).bg(Color::Yellow)
|
|
||||||
} else {
|
|
||||||
Style::default()
|
|
||||||
};
|
|
||||||
let no_style = if !*selected {
|
|
||||||
Style::new().fg(Color::Black).bg(Color::Yellow)
|
|
||||||
} else {
|
|
||||||
Style::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let buttons = Line::from(vec![
|
|
||||||
Span::styled(" Yes ", yes_style),
|
|
||||||
Span::raw(" "),
|
|
||||||
Span::styled(" No ", no_style),
|
|
||||||
]);
|
|
||||||
frame.render_widget(
|
|
||||||
Paragraph::new(buttons).alignment(Alignment::Center),
|
|
||||||
rows[1],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Modal::SaveAs(path) => {
|
Modal::SaveAs(path) => {
|
||||||
let width = (term.width * 60 / 100)
|
TextInputModal::new("Save As (Enter to confirm, Esc to cancel)", path)
|
||||||
.clamp(40, 70)
|
.width(60)
|
||||||
.min(term.width.saturating_sub(4));
|
.border_color(Color::Green)
|
||||||
let height = 5.min(term.height.saturating_sub(4));
|
.render_centered(frame, term);
|
||||||
let area = centered_rect(width, height, term);
|
|
||||||
frame.render_widget(Clear, area);
|
|
||||||
let modal = Paragraph::new(Line::from(vec![
|
|
||||||
Span::raw("> "),
|
|
||||||
Span::styled(path, Style::new().fg(Color::Cyan)),
|
|
||||||
Span::styled("█", Style::new().fg(Color::White)),
|
|
||||||
]))
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.title("Save As (Enter to confirm, Esc to cancel)")
|
|
||||||
.border_style(Style::new().fg(Color::Green)),
|
|
||||||
);
|
|
||||||
frame.render_widget(modal, area);
|
|
||||||
}
|
}
|
||||||
Modal::LoadFrom(path) => {
|
Modal::LoadFrom(path) => {
|
||||||
let width = (term.width * 60 / 100)
|
TextInputModal::new("Load From (Enter to confirm, Esc to cancel)", path)
|
||||||
.clamp(40, 70)
|
.width(60)
|
||||||
.min(term.width.saturating_sub(4));
|
.border_color(Color::Blue)
|
||||||
let height = 5.min(term.height.saturating_sub(4));
|
.render_centered(frame, term);
|
||||||
let area = centered_rect(width, height, term);
|
|
||||||
frame.render_widget(Clear, area);
|
|
||||||
let modal = Paragraph::new(Line::from(vec![
|
|
||||||
Span::raw("> "),
|
|
||||||
Span::styled(path, Style::new().fg(Color::Cyan)),
|
|
||||||
Span::styled("█", Style::new().fg(Color::White)),
|
|
||||||
]))
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.title("Load From (Enter to confirm, Esc to cancel)")
|
|
||||||
.border_style(Style::new().fg(Color::Blue)),
|
|
||||||
);
|
|
||||||
frame.render_widget(modal, area);
|
|
||||||
}
|
}
|
||||||
Modal::RenameBank { bank, name } => {
|
Modal::RenameBank { bank, name } => {
|
||||||
let width = 40.min(term.width.saturating_sub(4));
|
TextInputModal::new(&format!("Rename Bank {:02}", bank + 1), name)
|
||||||
let height = 5.min(term.height.saturating_sub(4));
|
.width(40)
|
||||||
let area = centered_rect(width, height, term);
|
.border_color(Color::Magenta)
|
||||||
frame.render_widget(Clear, area);
|
.render_centered(frame, term);
|
||||||
let modal = Paragraph::new(Line::from(vec![
|
|
||||||
Span::raw("> "),
|
|
||||||
Span::styled(name, Style::new().fg(Color::Cyan)),
|
|
||||||
Span::styled("█", Style::new().fg(Color::White)),
|
|
||||||
]))
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.title(format!("Rename Bank {:02}", bank + 1))
|
|
||||||
.border_style(Style::new().fg(Color::Magenta)),
|
|
||||||
);
|
|
||||||
frame.render_widget(modal, area);
|
|
||||||
}
|
}
|
||||||
Modal::RenamePattern {
|
Modal::RenamePattern {
|
||||||
bank,
|
bank,
|
||||||
pattern,
|
pattern,
|
||||||
name,
|
name,
|
||||||
} => {
|
} => {
|
||||||
let width = 40.min(term.width.saturating_sub(4));
|
TextInputModal::new(
|
||||||
let height = 5.min(term.height.saturating_sub(4));
|
&format!("Rename B{:02}:P{:02}", bank + 1, pattern + 1),
|
||||||
let area = centered_rect(width, height, term);
|
name,
|
||||||
frame.render_widget(Clear, area);
|
)
|
||||||
let modal = Paragraph::new(Line::from(vec![
|
.width(40)
|
||||||
Span::raw("> "),
|
.border_color(Color::Magenta)
|
||||||
Span::styled(name, Style::new().fg(Color::Cyan)),
|
.render_centered(frame, term);
|
||||||
Span::styled("█", Style::new().fg(Color::White)),
|
|
||||||
]))
|
|
||||||
.block(
|
|
||||||
Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.title(format!("Rename B{:02}:P{:02}", bank + 1, pattern + 1))
|
|
||||||
.border_style(Style::new().fg(Color::Magenta)),
|
|
||||||
);
|
|
||||||
frame.render_widget(modal, area);
|
|
||||||
}
|
}
|
||||||
Modal::SetPattern { field, input } => {
|
Modal::SetPattern { field, input } => {
|
||||||
let (title, hint) = match field {
|
let (title, hint) = match field {
|
||||||
PatternField::Length => ("Set Length (2-32)", "Enter number"),
|
PatternField::Length => ("Set Length (2-32)", "Enter number"),
|
||||||
PatternField::Speed => ("Set Speed", "1/8x, 1/4x, 1/2x, 1x, 2x, 4x, 8x"),
|
PatternField::Speed => ("Set Speed", "1/8x, 1/4x, 1/2x, 1x, 2x, 4x, 8x"),
|
||||||
};
|
};
|
||||||
let width = 45.min(term.width.saturating_sub(4));
|
TextInputModal::new(title, input)
|
||||||
let height = 6.min(term.height.saturating_sub(4));
|
.hint(hint)
|
||||||
let area = centered_rect(width, height, term);
|
.width(45)
|
||||||
frame.render_widget(Clear, area);
|
.border_color(Color::Yellow)
|
||||||
|
.render_centered(frame, term);
|
||||||
let block = Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.title(title)
|
|
||||||
.border_style(Style::new().fg(Color::Yellow));
|
|
||||||
let inner = block.inner(area);
|
|
||||||
frame.render_widget(block, area);
|
|
||||||
|
|
||||||
let rows =
|
|
||||||
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).split(inner);
|
|
||||||
|
|
||||||
frame.render_widget(
|
|
||||||
Paragraph::new(Line::from(vec![
|
|
||||||
Span::raw("> "),
|
|
||||||
Span::styled(input, Style::new().fg(Color::Cyan)),
|
|
||||||
Span::styled("█", Style::new().fg(Color::White)),
|
|
||||||
])),
|
|
||||||
rows[0],
|
|
||||||
);
|
|
||||||
frame.render_widget(
|
|
||||||
Paragraph::new(Span::styled(hint, Style::new().fg(Color::DarkGray))),
|
|
||||||
rows[1],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Modal::AddSamplePath(path) => {
|
Modal::AddSamplePath(path) => {
|
||||||
let width = 60.min(term.width.saturating_sub(4));
|
TextInputModal::new("Add Sample Path", path)
|
||||||
let height = 6.min(term.height.saturating_sub(4));
|
.hint("Enter directory path containing samples")
|
||||||
let area = centered_rect(width, height, term);
|
.width(60)
|
||||||
frame.render_widget(Clear, area);
|
.border_color(Color::Magenta)
|
||||||
|
.render_centered(frame, term);
|
||||||
let block = Block::default()
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.title("Add Sample Path")
|
|
||||||
.border_style(Style::new().fg(Color::Magenta));
|
|
||||||
let inner = block.inner(area);
|
|
||||||
frame.render_widget(block, area);
|
|
||||||
|
|
||||||
let rows =
|
|
||||||
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).split(inner);
|
|
||||||
|
|
||||||
frame.render_widget(
|
|
||||||
Paragraph::new(Line::from(vec![
|
|
||||||
Span::raw("> "),
|
|
||||||
Span::styled(path, Style::new().fg(Color::Cyan)),
|
|
||||||
Span::styled("█", Style::new().fg(Color::White)),
|
|
||||||
])),
|
|
||||||
rows[0],
|
|
||||||
);
|
|
||||||
frame.render_widget(
|
|
||||||
Paragraph::new(Span::styled(
|
|
||||||
"Enter directory path containing samples",
|
|
||||||
Style::new().fg(Color::DarkGray),
|
|
||||||
)),
|
|
||||||
rows[1],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
60
seq/src/widgets/confirm.rs
Normal file
60
seq/src/widgets/confirm.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||||
|
use ratatui::style::{Color, Style};
|
||||||
|
use ratatui::text::{Line, Span};
|
||||||
|
use ratatui::widgets::Paragraph;
|
||||||
|
use ratatui::Frame;
|
||||||
|
|
||||||
|
use super::ModalFrame;
|
||||||
|
|
||||||
|
pub struct ConfirmModal<'a> {
|
||||||
|
title: &'a str,
|
||||||
|
message: &'a str,
|
||||||
|
selected: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ConfirmModal<'a> {
|
||||||
|
pub fn new(title: &'a str, message: &'a str, selected: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
selected,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_centered(self, frame: &mut Frame, term: Rect) {
|
||||||
|
let inner = ModalFrame::new(self.title)
|
||||||
|
.width(30)
|
||||||
|
.height(5)
|
||||||
|
.border_color(Color::Yellow)
|
||||||
|
.render_centered(frame, term);
|
||||||
|
|
||||||
|
let rows = Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).split(inner);
|
||||||
|
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(self.message).alignment(Alignment::Center),
|
||||||
|
rows[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
let yes_style = if self.selected {
|
||||||
|
Style::new().fg(Color::Black).bg(Color::Yellow)
|
||||||
|
} else {
|
||||||
|
Style::default()
|
||||||
|
};
|
||||||
|
let no_style = if !self.selected {
|
||||||
|
Style::new().fg(Color::Black).bg(Color::Yellow)
|
||||||
|
} else {
|
||||||
|
Style::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let buttons = Line::from(vec![
|
||||||
|
Span::styled(" Yes ", yes_style),
|
||||||
|
Span::raw(" "),
|
||||||
|
Span::styled(" No ", no_style),
|
||||||
|
]);
|
||||||
|
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(buttons).alignment(Alignment::Center),
|
||||||
|
rows[1],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,11 @@
|
|||||||
|
mod confirm;
|
||||||
|
mod modal;
|
||||||
mod scope;
|
mod scope;
|
||||||
|
mod text_input;
|
||||||
|
mod vu_meter;
|
||||||
|
|
||||||
|
pub use confirm::ConfirmModal;
|
||||||
|
pub use modal::ModalFrame;
|
||||||
pub use scope::{Orientation, Scope};
|
pub use scope::{Orientation, Scope};
|
||||||
|
pub use text_input::TextInputModal;
|
||||||
|
pub use vu_meter::VuMeter;
|
||||||
|
|||||||
58
seq/src/widgets/modal.rs
Normal file
58
seq/src/widgets/modal.rs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
use ratatui::layout::Rect;
|
||||||
|
use ratatui::style::{Color, Style};
|
||||||
|
use ratatui::widgets::{Block, Borders, Clear};
|
||||||
|
use ratatui::Frame;
|
||||||
|
|
||||||
|
pub struct ModalFrame<'a> {
|
||||||
|
title: &'a str,
|
||||||
|
width: u16,
|
||||||
|
height: u16,
|
||||||
|
border_color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ModalFrame<'a> {
|
||||||
|
pub fn new(title: &'a str) -> Self {
|
||||||
|
Self {
|
||||||
|
title,
|
||||||
|
width: 40,
|
||||||
|
height: 5,
|
||||||
|
border_color: Color::White,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(mut self, w: u16) -> Self {
|
||||||
|
self.width = w;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn height(mut self, h: u16) -> Self {
|
||||||
|
self.height = h;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn border_color(mut self, c: Color) -> Self {
|
||||||
|
self.border_color = c;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_centered(&self, frame: &mut Frame, term: Rect) -> Rect {
|
||||||
|
let width = self.width.min(term.width.saturating_sub(4));
|
||||||
|
let height = self.height.min(term.height.saturating_sub(4));
|
||||||
|
|
||||||
|
let x = term.x + (term.width.saturating_sub(width)) / 2;
|
||||||
|
let y = term.y + (term.height.saturating_sub(height)) / 2;
|
||||||
|
let area = Rect::new(x, y, width, height);
|
||||||
|
|
||||||
|
frame.render_widget(Clear, area);
|
||||||
|
|
||||||
|
let block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.title(self.title)
|
||||||
|
.border_style(Style::new().fg(self.border_color));
|
||||||
|
|
||||||
|
let inner = block.inner(area);
|
||||||
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
|
inner
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,7 +43,9 @@ impl Widget for Scope<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match self.orientation {
|
match self.orientation {
|
||||||
Orientation::Horizontal => render_horizontal(self.data, area, buf, self.color, self.gain),
|
Orientation::Horizontal => {
|
||||||
|
render_horizontal(self.data, area, buf, self.color, self.gain)
|
||||||
|
}
|
||||||
Orientation::Vertical => render_vertical(self.data, area, buf, self.color, self.gain),
|
Orientation::Vertical => render_vertical(self.data, area, buf, self.color, self.gain),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
82
seq/src/widgets/text_input.rs
Normal file
82
seq/src/widgets/text_input.rs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
use ratatui::layout::{Constraint, Layout, Rect};
|
||||||
|
use ratatui::style::{Color, Style};
|
||||||
|
use ratatui::text::{Line, Span};
|
||||||
|
use ratatui::widgets::Paragraph;
|
||||||
|
use ratatui::Frame;
|
||||||
|
|
||||||
|
use super::ModalFrame;
|
||||||
|
|
||||||
|
pub struct TextInputModal<'a> {
|
||||||
|
title: &'a str,
|
||||||
|
input: &'a str,
|
||||||
|
hint: Option<&'a str>,
|
||||||
|
border_color: Color,
|
||||||
|
width: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TextInputModal<'a> {
|
||||||
|
pub fn new(title: &'a str, input: &'a str) -> Self {
|
||||||
|
Self {
|
||||||
|
title,
|
||||||
|
input,
|
||||||
|
hint: None,
|
||||||
|
border_color: Color::White,
|
||||||
|
width: 50,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hint(mut self, h: &'a str) -> Self {
|
||||||
|
self.hint = Some(h);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn border_color(mut self, c: Color) -> Self {
|
||||||
|
self.border_color = c;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(mut self, w: u16) -> Self {
|
||||||
|
self.width = w;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_centered(self, frame: &mut Frame, term: Rect) {
|
||||||
|
let height = if self.hint.is_some() { 6 } else { 5 };
|
||||||
|
|
||||||
|
let inner = ModalFrame::new(self.title)
|
||||||
|
.width(self.width)
|
||||||
|
.height(height)
|
||||||
|
.border_color(self.border_color)
|
||||||
|
.render_centered(frame, term);
|
||||||
|
|
||||||
|
if self.hint.is_some() {
|
||||||
|
let rows =
|
||||||
|
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).split(inner);
|
||||||
|
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(Line::from(vec![
|
||||||
|
Span::raw("> "),
|
||||||
|
Span::styled(self.input, Style::new().fg(Color::Cyan)),
|
||||||
|
Span::styled("█", Style::new().fg(Color::White)),
|
||||||
|
])),
|
||||||
|
rows[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(hint) = self.hint {
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(Span::styled(hint, Style::new().fg(Color::DarkGray))),
|
||||||
|
rows[1],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
frame.render_widget(
|
||||||
|
Paragraph::new(Line::from(vec![
|
||||||
|
Span::raw("> "),
|
||||||
|
Span::styled(self.input, Style::new().fg(Color::Cyan)),
|
||||||
|
Span::styled("█", Style::new().fg(Color::White)),
|
||||||
|
])),
|
||||||
|
inner,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
67
seq/src/widgets/vu_meter.rs
Normal file
67
seq/src/widgets/vu_meter.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
use ratatui::buffer::Buffer;
|
||||||
|
use ratatui::layout::Rect;
|
||||||
|
use ratatui::style::Color;
|
||||||
|
use ratatui::widgets::Widget;
|
||||||
|
|
||||||
|
pub struct VuMeter {
|
||||||
|
left: f32,
|
||||||
|
right: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VuMeter {
|
||||||
|
pub fn new(left: f32, right: f32) -> Self {
|
||||||
|
Self { left, right }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn level_to_color(level: f32) -> Color {
|
||||||
|
if level > 0.9 {
|
||||||
|
Color::Red
|
||||||
|
} else if level > 0.7 {
|
||||||
|
Color::Yellow
|
||||||
|
} else {
|
||||||
|
Color::Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Widget for VuMeter {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||||
|
if area.width < 2 || area.height == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let height = area.height as usize;
|
||||||
|
let left_col = area.x;
|
||||||
|
let right_col = area.x + area.width - 1;
|
||||||
|
|
||||||
|
let left_level = (self.left.clamp(0.0, 1.0) * height as f32) as usize;
|
||||||
|
let right_level = (self.right.clamp(0.0, 1.0) * height as f32) as usize;
|
||||||
|
|
||||||
|
for row in 0..height {
|
||||||
|
let y = area.y + area.height - 1 - row as u16;
|
||||||
|
let level_at_row = (row as f32 + 0.5) / height as f32;
|
||||||
|
let color = Self::level_to_color(level_at_row);
|
||||||
|
|
||||||
|
if row < left_level {
|
||||||
|
buf[(left_col, y)].set_char('█').set_fg(color);
|
||||||
|
} else {
|
||||||
|
buf[(left_col, y)].set_char('░').set_fg(Color::DarkGray);
|
||||||
|
}
|
||||||
|
|
||||||
|
if row < right_level {
|
||||||
|
buf[(right_col, y)].set_char('█').set_fg(color);
|
||||||
|
} else {
|
||||||
|
buf[(right_col, y)].set_char('░').set_fg(Color::DarkGray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if area.width > 2 {
|
||||||
|
for row in 0..height {
|
||||||
|
let y = area.y + row as u16;
|
||||||
|
for x in (area.x + 1)..(area.x + area.width - 1) {
|
||||||
|
buf[(x, y)].set_char(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user