This commit is contained in:
2026-01-19 14:42:14 +01:00
parent 9938b356cd
commit 2900f84b7d
17 changed files with 20059 additions and 12 deletions

304
seq/src/main.rs Normal file
View File

@@ -0,0 +1,304 @@
mod app;
mod audio;
mod file;
mod link;
mod model;
mod page;
mod script;
mod ui;
mod views;
use std::io;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use crossterm::ExecutableCommand;
use doux::Engine;
use ratatui::prelude::CrosstermBackend;
use ratatui::Terminal;
use app::{App, Focus, Modal};
use link::LinkState;
use model::Project;
use page::Page;
const TEMPO: f64 = 120.0;
const QUANTUM: f64 = 4.0;
fn main() -> io::Result<()> {
let link = Arc::new(LinkState::new(TEMPO, QUANTUM));
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));
let mut app = App::new(TEMPO, QUANTUM);
let engine = Arc::new(Mutex::new(Engine::new(44100.0)));
let project = Arc::new(Mutex::new(Project::default()));
let (_stream, sample_rate) = audio::build_stream(
Arc::clone(&engine),
Arc::clone(&link),
Arc::clone(&playing),
Arc::clone(&project),
Arc::clone(&playback_step),
Arc::clone(&event_count),
Arc::clone(&playback_bank),
Arc::clone(&playback_pattern),
Arc::clone(&queued_bank),
Arc::clone(&queued_pattern),
);
{
let mut eng = engine.lock().unwrap();
eng.sr = sample_rate;
eng.isr = 1.0 / sample_rate;
}
enable_raw_mode()?;
io::stdout().execute(EnterAlternateScreen)?;
let backend = CrosstermBackend::new(io::stdout());
let mut terminal = Terminal::new(backend)?;
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);
{
let eng = engine.lock().unwrap();
app.active_voices = eng.active_voices;
app.peak_voices = app.peak_voices.max(eng.active_voices);
app.cpu_load = eng.metrics.load.get_load();
app.schedule_depth = eng.schedule.len();
for (i, s) in app.scope.iter_mut().enumerate() {
*s = eng.output.get(i * 2).copied().unwrap_or(0.0);
}
}
app.playback_bank = playback_bank.load(Ordering::Relaxed);
app.playback_pattern = playback_pattern.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;
}
{
let mut proj = project.lock().unwrap();
proj.banks = app.project.banks.clone();
}
terminal.draw(|frame| ui::render(frame, &mut app))?;
if event::poll(Duration::from_millis(16))? {
if let Event::Key(key) = event::read()? {
app.clear_status();
match &mut app.modal {
Modal::ConfirmQuit => match key.code {
KeyCode::Char('y') | KeyCode::Char('Y') => break,
KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
app.modal = Modal::None;
}
_ => {}
},
Modal::SaveAs(path) => match key.code {
KeyCode::Enter => {
let save_path = PathBuf::from(path.as_str());
app.modal = Modal::None;
app.save(save_path);
}
KeyCode::Esc => {
app.modal = Modal::None;
}
KeyCode::Backspace => {
path.pop();
}
KeyCode::Char(c) => {
path.push(c);
}
_ => {}
},
Modal::LoadFrom(path) => match key.code {
KeyCode::Enter => {
let load_path = PathBuf::from(path.as_str());
app.modal = Modal::None;
app.load(load_path);
}
KeyCode::Esc => {
app.modal = Modal::None;
}
KeyCode::Backspace => {
path.pop();
}
KeyCode::Char(c) => {
path.push(c);
}
_ => {}
},
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::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;
}
_ => {}
}
}
Modal::None => {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
if ctrl && key.code == KeyCode::Left {
app.page.left();
continue;
}
if ctrl && key.code == KeyCode::Right {
app.page.right();
continue;
}
match app.page {
Page::Main => match app.focus {
Focus::Sequencer => match key.code {
KeyCode::Char('q') => {
app.modal = Modal::ConfirmQuit;
}
KeyCode::Char(' ') => {
app.toggle_playing();
playing.store(app.playing, Ordering::Relaxed);
}
KeyCode::Tab => app.toggle_focus(),
KeyCode::Left => app.prev_step(),
KeyCode::Right => app.next_step(),
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
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "project.buboseq".to_string());
app.modal = Modal::SaveAs(default);
}
KeyCode::Char('l') => {
app.modal = Modal::LoadFrom(String::new());
}
KeyCode::Char('+') | KeyCode::Char('=') => app.tempo_up(&link),
KeyCode::Char('-') => app.tempo_down(&link),
KeyCode::Char('c') if ctrl => app.copy_step(),
KeyCode::Char('v') if ctrl => app.paste_step(),
_ => {}
},
Focus::Editor => match key.code {
KeyCode::Tab | KeyCode::Esc => app.toggle_focus(),
KeyCode::Char('e') if ctrl => {
app.save_editor_to_step();
app.compile_current_step();
}
_ => {
app.editor.input(Event::Key(key));
}
},
},
Page::Audio => match key.code {
KeyCode::Char('q') => {
app.modal = Modal::ConfirmQuit;
}
KeyCode::Char('h') => {
engine.lock().unwrap().hush();
}
KeyCode::Char('p') => {
engine.lock().unwrap().panic();
}
KeyCode::Char('r') => {
app.peak_voices = 0;
}
KeyCode::Char('t') => {
engine.lock().unwrap().evaluate("sin 440 * 0.3");
}
KeyCode::Char(' ') => {
app.toggle_playing();
playing.store(app.playing, Ordering::Relaxed);
}
_ => {}
},
}
}
}
}
}
}
disable_raw_mode()?;
io::stdout().execute(LeaveAlternateScreen)?;
Ok(())
}