wip
This commit is contained in:
231
seq/src/main.rs
231
seq/src/main.rs
@@ -15,6 +15,7 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Parser;
|
||||
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
|
||||
use crossterm::terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
@@ -24,16 +25,42 @@ use doux::Engine;
|
||||
use ratatui::prelude::CrosstermBackend;
|
||||
use ratatui::Terminal;
|
||||
|
||||
use app::{App, Focus, Modal};
|
||||
use audio::{SlotChange, MAX_SLOTS};
|
||||
use app::{App, AudioFocus, Focus, Modal, PatternField};
|
||||
use audio::{AudioStreamConfig, SlotChange, MAX_SLOTS};
|
||||
use link::LinkState;
|
||||
use model::Project;
|
||||
use page::Page;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "seq", about = "A step sequencer with Ableton Link support")]
|
||||
struct Args {
|
||||
/// Directory containing audio samples to load (can be specified multiple times)
|
||||
#[arg(short, long)]
|
||||
samples: Vec<PathBuf>,
|
||||
|
||||
/// Output audio device (name or index)
|
||||
#[arg(short, long)]
|
||||
output: Option<String>,
|
||||
|
||||
/// Input audio device (name or index)
|
||||
#[arg(short, long)]
|
||||
input: Option<String>,
|
||||
|
||||
/// Number of output channels
|
||||
#[arg(short, long, default_value = "2")]
|
||||
channels: u16,
|
||||
|
||||
/// Audio buffer size in samples
|
||||
#[arg(short, long, default_value = "512")]
|
||||
buffer: u32,
|
||||
}
|
||||
|
||||
const TEMPO: f64 = 120.0;
|
||||
const QUANTUM: f64 = 4.0;
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
let link = Arc::new(LinkState::new(TEMPO, QUANTUM));
|
||||
link.enable();
|
||||
|
||||
@@ -48,10 +75,32 @@ fn main() -> io::Result<()> {
|
||||
|
||||
let mut app = App::new(TEMPO, QUANTUM);
|
||||
|
||||
// Apply CLI args to audio config
|
||||
app.audio_config.output_device = args.output;
|
||||
app.audio_config.input_device = args.input;
|
||||
app.audio_config.channels = args.channels;
|
||||
app.audio_config.buffer_size = args.buffer;
|
||||
app.audio_config.sample_paths = args.samples;
|
||||
|
||||
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(
|
||||
// Load sample directories
|
||||
for path in &app.audio_config.sample_paths {
|
||||
let index = doux::loader::scan_samples_dir(path);
|
||||
let count = index.len();
|
||||
engine.lock().unwrap().sample_index.extend(index);
|
||||
app.audio_config.sample_count += count;
|
||||
}
|
||||
|
||||
let stream_config = AudioStreamConfig {
|
||||
output_device: app.audio_config.output_device.clone(),
|
||||
channels: app.audio_config.channels,
|
||||
buffer_size: app.audio_config.buffer_size,
|
||||
};
|
||||
|
||||
let (mut stream, sample_rate) = audio::build_stream(
|
||||
&stream_config,
|
||||
Arc::clone(&engine),
|
||||
Arc::clone(&link),
|
||||
Arc::clone(&playing),
|
||||
@@ -62,7 +111,10 @@ fn main() -> io::Result<()> {
|
||||
Arc::clone(&slot_changes),
|
||||
Arc::clone(&app.variables),
|
||||
Arc::clone(&app.rng),
|
||||
);
|
||||
)
|
||||
.expect("Failed to start audio");
|
||||
|
||||
app.audio_config.sample_rate = sample_rate;
|
||||
|
||||
{
|
||||
let mut eng = engine.lock().unwrap();
|
||||
@@ -77,6 +129,69 @@ fn main() -> io::Result<()> {
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
loop {
|
||||
if app.restart_pending {
|
||||
app.restart_pending = false;
|
||||
drop(stream);
|
||||
|
||||
let new_config = AudioStreamConfig {
|
||||
output_device: app.audio_config.output_device.clone(),
|
||||
channels: app.audio_config.channels,
|
||||
buffer_size: app.audio_config.buffer_size,
|
||||
};
|
||||
|
||||
{
|
||||
let mut eng = engine.lock().unwrap();
|
||||
*eng = Engine::new_with_channels(eng.sr, new_config.channels as usize);
|
||||
}
|
||||
|
||||
match audio::build_stream(
|
||||
&new_config,
|
||||
Arc::clone(&engine),
|
||||
Arc::clone(&link),
|
||||
Arc::clone(&playing),
|
||||
Arc::clone(&project),
|
||||
slot_steps.clone(),
|
||||
Arc::clone(&event_count),
|
||||
Arc::clone(&slot_data),
|
||||
Arc::clone(&slot_changes),
|
||||
Arc::clone(&app.variables),
|
||||
Arc::clone(&app.rng),
|
||||
) {
|
||||
Ok((new_stream, sr)) => {
|
||||
stream = new_stream;
|
||||
app.audio_config.sample_rate = sr;
|
||||
{
|
||||
let mut eng = engine.lock().unwrap();
|
||||
eng.sr = sr;
|
||||
eng.isr = 1.0 / sr;
|
||||
}
|
||||
app.status_message = Some("Audio restarted".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
app.status_message = Some(format!("Restart failed: {e}"));
|
||||
let (fallback_stream, _) = audio::build_stream(
|
||||
&AudioStreamConfig {
|
||||
output_device: None,
|
||||
channels: 2,
|
||||
buffer_size: 512,
|
||||
},
|
||||
Arc::clone(&engine),
|
||||
Arc::clone(&link),
|
||||
Arc::clone(&playing),
|
||||
Arc::clone(&project),
|
||||
slot_steps.clone(),
|
||||
Arc::clone(&event_count),
|
||||
Arc::clone(&slot_data),
|
||||
Arc::clone(&slot_changes),
|
||||
Arc::clone(&app.variables),
|
||||
Arc::clone(&app.rng),
|
||||
)
|
||||
.expect("Failed to restart with defaults");
|
||||
stream = fallback_stream;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.update_from_link(&link);
|
||||
app.playing = playing.load(Ordering::Relaxed);
|
||||
app.event_count = event_count.load(Ordering::Relaxed);
|
||||
@@ -206,6 +321,71 @@ fn main() -> io::Result<()> {
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Modal::SetPattern { field, input } => match key.code {
|
||||
KeyCode::Enter => {
|
||||
let field = *field;
|
||||
let (bank, pattern) = (app.edit_bank, app.edit_pattern);
|
||||
match field {
|
||||
PatternField::Length => {
|
||||
if let Ok(len) = input.parse::<usize>() {
|
||||
app.project.pattern_at_mut(bank, pattern).set_length(len);
|
||||
let new_len = app.project.pattern_at(bank, pattern).length;
|
||||
if app.current_step >= new_len {
|
||||
app.current_step = new_len - 1;
|
||||
}
|
||||
app.status_message = Some(format!("Length set to {new_len}"));
|
||||
} else {
|
||||
app.status_message = Some("Invalid length".to_string());
|
||||
}
|
||||
}
|
||||
PatternField::Speed => {
|
||||
if let Some(speed) = model::PatternSpeed::from_label(input) {
|
||||
app.project.pattern_at_mut(bank, pattern).speed = speed;
|
||||
app.status_message = Some(format!("Speed set to {}", speed.label()));
|
||||
} else {
|
||||
app.status_message = Some("Invalid speed (try 1/8x, 1/4x, 1/2x, 1x, 2x, 4x, 8x)".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
app.modal = Modal::None;
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.modal = Modal::None;
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
input.pop();
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
input.push(c);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Modal::AddSamplePath(path) => match key.code {
|
||||
KeyCode::Enter => {
|
||||
let sample_path = PathBuf::from(path.as_str());
|
||||
if sample_path.is_dir() {
|
||||
let index = doux::loader::scan_samples_dir(&sample_path);
|
||||
let count = index.len();
|
||||
engine.lock().unwrap().sample_index.extend(index);
|
||||
app.audio_config.sample_count += count;
|
||||
app.add_sample_path(sample_path);
|
||||
app.status_message = Some(format!("Added {count} samples"));
|
||||
} else {
|
||||
app.status_message = Some("Path is not a directory".to_string());
|
||||
}
|
||||
app.modal = Modal::None;
|
||||
}
|
||||
KeyCode::Esc => {
|
||||
app.modal = Modal::None;
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
path.pop();
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
path.push(c);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Modal::None => {
|
||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
|
||||
@@ -263,6 +443,8 @@ fn main() -> io::Result<()> {
|
||||
}
|
||||
KeyCode::Char('[') => app.speed_decrease(),
|
||||
KeyCode::Char(']') => app.speed_increase(),
|
||||
KeyCode::Char('L') => app.open_pattern_modal(PatternField::Length),
|
||||
KeyCode::Char('S') => app.open_pattern_modal(PatternField::Speed),
|
||||
KeyCode::Char('c') if ctrl => app.copy_step(),
|
||||
KeyCode::Char('v') if ctrl => app.paste_step(),
|
||||
_ => {}
|
||||
@@ -354,6 +536,47 @@ fn main() -> io::Result<()> {
|
||||
KeyCode::Char('q') => {
|
||||
app.modal = Modal::ConfirmQuit { selected: false };
|
||||
}
|
||||
KeyCode::Up | KeyCode::Char('k') => {
|
||||
app.prev_audio_focus();
|
||||
}
|
||||
KeyCode::Down | KeyCode::Char('j') => {
|
||||
app.next_audio_focus();
|
||||
}
|
||||
KeyCode::Left => match app.audio_focus {
|
||||
AudioFocus::OutputDevice => app.prev_output_device(),
|
||||
AudioFocus::InputDevice => app.prev_input_device(),
|
||||
AudioFocus::Channels => app.adjust_channels(-1),
|
||||
AudioFocus::BufferSize => app.adjust_buffer_size(-64),
|
||||
AudioFocus::SamplePaths => app.remove_last_sample_path(),
|
||||
},
|
||||
KeyCode::Right => match app.audio_focus {
|
||||
AudioFocus::OutputDevice => app.next_output_device(),
|
||||
AudioFocus::InputDevice => app.next_input_device(),
|
||||
AudioFocus::Channels => app.adjust_channels(1),
|
||||
AudioFocus::BufferSize => app.adjust_buffer_size(64),
|
||||
AudioFocus::SamplePaths => {}
|
||||
},
|
||||
KeyCode::Char('R') => {
|
||||
app.trigger_restart();
|
||||
// Reload samples on restart
|
||||
let mut eng = engine.lock().unwrap();
|
||||
eng.sample_index.clear();
|
||||
app.audio_config.sample_count = 0;
|
||||
for path in &app.audio_config.sample_paths {
|
||||
let index = doux::loader::scan_samples_dir(path);
|
||||
app.audio_config.sample_count += index.len();
|
||||
eng.sample_index.extend(index);
|
||||
}
|
||||
}
|
||||
KeyCode::Char('A') => {
|
||||
app.modal = Modal::AddSamplePath(String::new());
|
||||
}
|
||||
KeyCode::Char('D') => {
|
||||
app.refresh_audio_devices();
|
||||
let out_count = app.available_output_devices.len();
|
||||
let in_count = app.available_input_devices.len();
|
||||
app.status_message = Some(format!("Found {out_count} output, {in_count} input devices"));
|
||||
}
|
||||
KeyCode::Char('h') => {
|
||||
engine.lock().unwrap().hush();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user