seq continues

This commit is contained in:
2026-01-19 16:44:17 +01:00
parent 2900f84b7d
commit ac9e64dcb7
18 changed files with 1568 additions and 361 deletions

View File

@@ -7,6 +7,7 @@ mod page;
mod script;
mod ui;
mod views;
mod widgets;
use std::io;
use std::path::PathBuf;
@@ -24,6 +25,7 @@ use ratatui::prelude::CrosstermBackend;
use ratatui::Terminal;
use app::{App, Focus, Modal};
use audio::{SlotChange, MAX_SLOTS};
use link::LinkState;
use model::Project;
use page::Page;
@@ -36,12 +38,14 @@ fn main() -> io::Result<()> {
link.enable();
let playing = Arc::new(AtomicBool::new(true));
let playback_step = Arc::new(AtomicUsize::new(0));
let event_count = Arc::new(AtomicUsize::new(0));
let playback_bank = Arc::new(AtomicUsize::new(0));
let playback_pattern = Arc::new(AtomicUsize::new(0));
let queued_bank = Arc::new(AtomicUsize::new(usize::MAX));
let queued_pattern = Arc::new(AtomicUsize::new(usize::MAX));
// Slot state shared between audio thread and UI
let slot_steps: [Arc<AtomicUsize>; MAX_SLOTS] = std::array::from_fn(|_| Arc::new(AtomicUsize::new(0)));
let slot_data: Arc<Mutex<[(bool, usize, usize); MAX_SLOTS]>> =
Arc::new(Mutex::new([(false, 0, 0); MAX_SLOTS]));
let slot_changes: Arc<Mutex<Vec<SlotChange>>> = Arc::new(Mutex::new(Vec::new()));
let mut app = App::new(TEMPO, QUANTUM);
let engine = Arc::new(Mutex::new(Engine::new(44100.0)));
@@ -52,12 +56,12 @@ fn main() -> io::Result<()> {
Arc::clone(&link),
Arc::clone(&playing),
Arc::clone(&project),
Arc::clone(&playback_step),
slot_steps.clone(),
Arc::clone(&event_count),
Arc::clone(&playback_bank),
Arc::clone(&playback_pattern),
Arc::clone(&queued_bank),
Arc::clone(&queued_pattern),
Arc::clone(&slot_data),
Arc::clone(&slot_changes),
Arc::clone(&app.variables),
Arc::clone(&app.rng),
);
{
@@ -75,7 +79,6 @@ fn main() -> io::Result<()> {
loop {
app.update_from_link(&link);
app.playing = playing.load(Ordering::Relaxed);
app.playback_step = playback_step.load(Ordering::Relaxed);
app.event_count = event_count.load(Ordering::Relaxed);
{
@@ -89,14 +92,19 @@ fn main() -> io::Result<()> {
}
}
app.playback_bank = playback_bank.load(Ordering::Relaxed);
app.playback_pattern = playback_pattern.load(Ordering::Relaxed);
// Sync slot state from audio thread
{
let sd = slot_data.lock().unwrap();
app.slot_data = *sd;
}
for (i, step_atomic) in slot_steps.iter().enumerate() {
app.slot_steps[i] = step_atomic.load(Ordering::Relaxed);
}
if app.queued_bank.is_some() {
queued_bank.store(app.queued_bank.unwrap(), Ordering::Relaxed);
queued_pattern.store(app.queued_pattern.unwrap(), Ordering::Relaxed);
app.queued_bank = None;
app.queued_pattern = None;
// Push queued changes to audio thread
if !app.queued_changes.is_empty() {
let mut changes = slot_changes.lock().unwrap();
changes.extend(app.queued_changes.drain(..));
}
{
@@ -111,11 +119,21 @@ fn main() -> io::Result<()> {
app.clear_status();
match &mut app.modal {
Modal::ConfirmQuit => match key.code {
Modal::ConfirmQuit { ref mut selected } => match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => break,
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
app.modal = Modal::None;
}
KeyCode::Left | KeyCode::Right => {
*selected = !*selected;
}
KeyCode::Enter => {
if *selected {
break;
} else {
app.modal = Modal::None;
}
}
_ => {}
},
Modal::SaveAs(path) => match key.code {
@@ -152,56 +170,42 @@ fn main() -> io::Result<()> {
}
_ => {}
},
Modal::PatternPicker { ref mut cursor } => {
match key.code {
KeyCode::Enter => {
let selected = *cursor;
app.modal = Modal::None;
app.select_edit_pattern(selected);
}
KeyCode::Esc => {
app.modal = Modal::None;
}
KeyCode::Left => {
*cursor = (*cursor + 15) % 16;
}
KeyCode::Right => {
*cursor = (*cursor + 1) % 16;
}
KeyCode::Up => {
*cursor = (*cursor + 12) % 16;
}
KeyCode::Down => {
*cursor = (*cursor + 4) % 16;
}
_ => {}
Modal::RenameBank { bank, name } => match key.code {
KeyCode::Enter => {
let bank_idx = *bank;
let new_name = if name.trim().is_empty() { None } else { Some(name.clone()) };
app.project.banks[bank_idx].name = new_name;
app.modal = Modal::None;
}
}
Modal::BankPicker { ref mut cursor } => {
match key.code {
KeyCode::Enter => {
let selected = *cursor;
app.modal = Modal::None;
app.select_edit_bank(selected);
}
KeyCode::Esc => {
app.modal = Modal::None;
}
KeyCode::Left => {
*cursor = (*cursor + 15) % 16;
}
KeyCode::Right => {
*cursor = (*cursor + 1) % 16;
}
KeyCode::Up => {
*cursor = (*cursor + 12) % 16;
}
KeyCode::Down => {
*cursor = (*cursor + 4) % 16;
}
_ => {}
KeyCode::Esc => {
app.modal = Modal::None;
}
}
KeyCode::Backspace => {
name.pop();
}
KeyCode::Char(c) => {
name.push(c);
}
_ => {}
},
Modal::RenamePattern { bank, pattern, name } => match key.code {
KeyCode::Enter => {
let (bank_idx, pattern_idx) = (*bank, *pattern);
let new_name = if name.trim().is_empty() { None } else { Some(name.clone()) };
app.project.banks[bank_idx].patterns[pattern_idx].name = new_name;
app.modal = Modal::None;
}
KeyCode::Esc => {
app.modal = Modal::None;
}
KeyCode::Backspace => {
name.pop();
}
KeyCode::Char(c) => {
name.push(c);
}
_ => {}
},
Modal::None => {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
@@ -213,12 +217,20 @@ fn main() -> io::Result<()> {
app.page.right();
continue;
}
if ctrl && key.code == KeyCode::Up {
app.page.up();
continue;
}
if ctrl && key.code == KeyCode::Down {
app.page.down();
continue;
}
match app.page {
Page::Main => match app.focus {
Focus::Sequencer => match key.code {
KeyCode::Char('q') => {
app.modal = Modal::ConfirmQuit;
app.modal = Modal::ConfirmQuit { selected: false };
}
KeyCode::Char(' ') => {
app.toggle_playing();
@@ -230,16 +242,6 @@ fn main() -> io::Result<()> {
KeyCode::Up => app.step_up(),
KeyCode::Down => app.step_down(),
KeyCode::Enter => app.toggle_step(),
KeyCode::Char('p') => {
app.modal =
Modal::PatternPicker { cursor: app.edit_pattern };
}
KeyCode::Char('b') => {
app.modal = Modal::BankPicker { cursor: app.edit_bank };
}
KeyCode::Char('g') => {
app.queue_current_for_playback();
}
KeyCode::Char('s') => {
let default = app
.file_path
@@ -253,6 +255,14 @@ fn main() -> io::Result<()> {
}
KeyCode::Char('+') | KeyCode::Char('=') => app.tempo_up(&link),
KeyCode::Char('-') => app.tempo_down(&link),
KeyCode::Char('<') | KeyCode::Char(',') => {
app.length_decrease()
}
KeyCode::Char('>') | KeyCode::Char('.') => {
app.length_increase()
}
KeyCode::Char('[') => app.speed_decrease(),
KeyCode::Char(']') => app.speed_increase(),
KeyCode::Char('c') if ctrl => app.copy_step(),
KeyCode::Char('v') if ctrl => app.paste_step(),
_ => {}
@@ -268,9 +278,81 @@ fn main() -> io::Result<()> {
}
},
},
Page::Patterns => {
use app::PatternsViewLevel;
match key.code {
KeyCode::Left => {
app.patterns_cursor = (app.patterns_cursor + 15) % 16;
}
KeyCode::Right => {
app.patterns_cursor = (app.patterns_cursor + 1) % 16;
}
KeyCode::Up => {
app.patterns_cursor = (app.patterns_cursor + 12) % 16;
}
KeyCode::Down => {
app.patterns_cursor = (app.patterns_cursor + 4) % 16;
}
KeyCode::Esc | KeyCode::Backspace => {
match app.patterns_view_level {
PatternsViewLevel::Banks => {
app.page.down();
}
PatternsViewLevel::Patterns { .. } => {
app.patterns_view_level = PatternsViewLevel::Banks;
app.patterns_cursor = 0;
}
}
}
KeyCode::Enter => {
match app.patterns_view_level {
PatternsViewLevel::Banks => {
let bank = app.patterns_cursor;
app.patterns_view_level =
PatternsViewLevel::Patterns { bank };
app.patterns_cursor = 0;
}
PatternsViewLevel::Patterns { bank } => {
let pattern = app.patterns_cursor;
app.select_edit_bank(bank);
app.select_edit_pattern(pattern);
app.patterns_view_level = PatternsViewLevel::Banks;
app.patterns_cursor = 0;
app.page.down();
}
}
}
KeyCode::Char(' ') => {
if let PatternsViewLevel::Patterns { bank } =
app.patterns_view_level
{
let pattern = app.patterns_cursor;
app.toggle_pattern_playback(bank, pattern);
}
}
KeyCode::Char('q') => {
app.modal = Modal::ConfirmQuit { selected: false };
}
KeyCode::Char('r') => {
match app.patterns_view_level {
PatternsViewLevel::Banks => {
let bank = app.patterns_cursor;
let current_name = app.project.banks[bank].name.clone().unwrap_or_default();
app.modal = Modal::RenameBank { bank, name: current_name };
}
PatternsViewLevel::Patterns { bank } => {
let pattern = app.patterns_cursor;
let current_name = app.project.banks[bank].patterns[pattern].name.clone().unwrap_or_default();
app.modal = Modal::RenamePattern { bank, pattern, name: current_name };
}
}
}
_ => {}
}
}
Page::Audio => match key.code {
KeyCode::Char('q') => {
app.modal = Modal::ConfirmQuit;
app.modal = Modal::ConfirmQuit { selected: false };
}
KeyCode::Char('h') => {
engine.lock().unwrap().hush();
@@ -290,6 +372,29 @@ fn main() -> io::Result<()> {
}
_ => {}
},
Page::Doc => {
let topic_count = views::doc_view::topic_count();
match key.code {
KeyCode::Char('j') | KeyCode::Down => {
app.doc_topic = (app.doc_topic + 1) % topic_count;
app.doc_scroll = 0;
}
KeyCode::Char('k') | KeyCode::Up => {
app.doc_topic = (app.doc_topic + topic_count - 1) % topic_count;
app.doc_scroll = 0;
}
KeyCode::PageDown => {
app.doc_scroll = app.doc_scroll.saturating_add(10);
}
KeyCode::PageUp => {
app.doc_scroll = app.doc_scroll.saturating_sub(10);
}
KeyCode::Char('q') => {
app.modal = Modal::ConfirmQuit { selected: false };
}
_ => {}
}
}
}
}
}