wip
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2091,6 +2091,7 @@ dependencies = [
|
|||||||
"arboard",
|
"arboard",
|
||||||
"clap",
|
"clap",
|
||||||
"cpal",
|
"cpal",
|
||||||
|
"crossbeam-channel",
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"doux",
|
"doux",
|
||||||
"minimad",
|
"minimad",
|
||||||
|
|||||||
@@ -21,3 +21,4 @@ serde_json = "1"
|
|||||||
tui-textarea = "0.7"
|
tui-textarea = "0.7"
|
||||||
arboard = "3"
|
arboard = "3"
|
||||||
minimad = "0.13"
|
minimad = "0.13"
|
||||||
|
crossbeam-channel = "0.5"
|
||||||
|
|||||||
515
seq/src/app.rs
515
seq/src/app.rs
@@ -1,19 +1,26 @@
|
|||||||
use doux::audio::AudioDeviceInfo;
|
use doux::audio::AudioDeviceInfo;
|
||||||
use rand::rngs::StdRng;
|
use rand::rngs::StdRng;
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use tui_textarea::TextArea;
|
use tui_textarea::TextArea;
|
||||||
|
|
||||||
use crate::audio::{SlotChange, MAX_SLOTS};
|
use crate::config::{MAX_BANKS, MAX_PATTERNS, MAX_SLOTS};
|
||||||
use crate::file;
|
use crate::file;
|
||||||
use crate::link::LinkState;
|
use crate::link::LinkState;
|
||||||
use crate::model::{Pattern, Project};
|
use crate::model::{Pattern, Project};
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::script::{Rng, ScriptEngine, StepContext, Variables};
|
use crate::script::{ScriptEngine, StepContext, Variables, Rng};
|
||||||
|
use crate::sequencer::SlotState;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SlotChange {
|
||||||
|
Add { slot: usize, bank: usize, pattern: usize },
|
||||||
|
Remove { slot: usize },
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum Focus {
|
pub enum Focus {
|
||||||
@@ -81,6 +88,209 @@ pub enum AudioFocus {
|
|||||||
SamplePaths,
|
SamplePaths,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Metrics {
|
||||||
|
pub event_count: usize,
|
||||||
|
pub active_voices: usize,
|
||||||
|
pub peak_voices: usize,
|
||||||
|
pub cpu_load: f32,
|
||||||
|
pub schedule_depth: usize,
|
||||||
|
pub scope: [f32; 64],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Metrics {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
event_count: 0,
|
||||||
|
active_voices: 0,
|
||||||
|
peak_voices: 0,
|
||||||
|
cpu_load: 0.0,
|
||||||
|
schedule_depth: 0,
|
||||||
|
scope: [0.0; 64],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EditorContext {
|
||||||
|
pub bank: usize,
|
||||||
|
pub pattern: usize,
|
||||||
|
pub step: usize,
|
||||||
|
pub focus: Focus,
|
||||||
|
pub text: TextArea<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for EditorContext {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
bank: 0,
|
||||||
|
pattern: 0,
|
||||||
|
step: 0,
|
||||||
|
focus: Focus::Sequencer,
|
||||||
|
text: TextArea::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AudioSettings {
|
||||||
|
pub config: AudioConfig,
|
||||||
|
pub focus: AudioFocus,
|
||||||
|
pub output_devices: Vec<AudioDeviceInfo>,
|
||||||
|
pub input_devices: Vec<AudioDeviceInfo>,
|
||||||
|
pub restart_pending: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AudioSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
config: AudioConfig::default(),
|
||||||
|
focus: AudioFocus::default(),
|
||||||
|
output_devices: doux::audio::list_output_devices(),
|
||||||
|
input_devices: doux::audio::list_input_devices(),
|
||||||
|
restart_pending: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioSettings {
|
||||||
|
pub fn refresh_devices(&mut self) {
|
||||||
|
self.output_devices = doux::audio::list_output_devices();
|
||||||
|
self.input_devices = doux::audio::list_input_devices();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_focus(&mut self) {
|
||||||
|
self.focus = match self.focus {
|
||||||
|
AudioFocus::OutputDevice => AudioFocus::InputDevice,
|
||||||
|
AudioFocus::InputDevice => AudioFocus::Channels,
|
||||||
|
AudioFocus::Channels => AudioFocus::BufferSize,
|
||||||
|
AudioFocus::BufferSize => AudioFocus::SamplePaths,
|
||||||
|
AudioFocus::SamplePaths => AudioFocus::OutputDevice,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev_focus(&mut self) {
|
||||||
|
self.focus = match self.focus {
|
||||||
|
AudioFocus::OutputDevice => AudioFocus::SamplePaths,
|
||||||
|
AudioFocus::InputDevice => AudioFocus::OutputDevice,
|
||||||
|
AudioFocus::Channels => AudioFocus::InputDevice,
|
||||||
|
AudioFocus::BufferSize => AudioFocus::Channels,
|
||||||
|
AudioFocus::SamplePaths => AudioFocus::BufferSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_output_device(&mut self) {
|
||||||
|
if self.output_devices.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let current_idx = self.current_output_device_index();
|
||||||
|
let next_idx = (current_idx + 1) % self.output_devices.len();
|
||||||
|
self.config.output_device = Some(self.output_devices[next_idx].name.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev_output_device(&mut self) {
|
||||||
|
if self.output_devices.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let current_idx = self.current_output_device_index();
|
||||||
|
let prev_idx = (current_idx + self.output_devices.len() - 1) % self.output_devices.len();
|
||||||
|
self.config.output_device = Some(self.output_devices[prev_idx].name.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_output_device_index(&self) -> usize {
|
||||||
|
match &self.config.output_device {
|
||||||
|
Some(name) => self
|
||||||
|
.output_devices
|
||||||
|
.iter()
|
||||||
|
.position(|d| &d.name == name)
|
||||||
|
.unwrap_or(0),
|
||||||
|
None => self
|
||||||
|
.output_devices
|
||||||
|
.iter()
|
||||||
|
.position(|d| d.is_default)
|
||||||
|
.unwrap_or(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_input_device(&mut self) {
|
||||||
|
if self.input_devices.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let current_idx = self.current_input_device_index();
|
||||||
|
let next_idx = (current_idx + 1) % self.input_devices.len();
|
||||||
|
self.config.input_device = Some(self.input_devices[next_idx].name.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prev_input_device(&mut self) {
|
||||||
|
if self.input_devices.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let current_idx = self.current_input_device_index();
|
||||||
|
let prev_idx = (current_idx + self.input_devices.len() - 1) % self.input_devices.len();
|
||||||
|
self.config.input_device = Some(self.input_devices[prev_idx].name.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_input_device_index(&self) -> usize {
|
||||||
|
match &self.config.input_device {
|
||||||
|
Some(name) => self
|
||||||
|
.input_devices
|
||||||
|
.iter()
|
||||||
|
.position(|d| &d.name == name)
|
||||||
|
.unwrap_or(0),
|
||||||
|
None => self
|
||||||
|
.input_devices
|
||||||
|
.iter()
|
||||||
|
.position(|d| d.is_default)
|
||||||
|
.unwrap_or(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn adjust_channels(&mut self, delta: i16) {
|
||||||
|
let new_val = (self.config.channels as i16 + delta).clamp(1, 64) as u16;
|
||||||
|
self.config.channels = new_val;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn adjust_buffer_size(&mut self, delta: i32) {
|
||||||
|
let new_val = (self.config.buffer_size as i32 + delta).clamp(64, 4096) as u32;
|
||||||
|
self.config.buffer_size = new_val;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_output_device_name(&self) -> &str {
|
||||||
|
match &self.config.output_device {
|
||||||
|
Some(name) => name,
|
||||||
|
None => self
|
||||||
|
.output_devices
|
||||||
|
.iter()
|
||||||
|
.find(|d| d.is_default)
|
||||||
|
.map(|d| d.name.as_str())
|
||||||
|
.unwrap_or("Default"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_input_device_name(&self) -> &str {
|
||||||
|
match &self.config.input_device {
|
||||||
|
Some(name) => name,
|
||||||
|
None => self
|
||||||
|
.input_devices
|
||||||
|
.iter()
|
||||||
|
.find(|d| d.is_default)
|
||||||
|
.map(|d| d.name.as_str())
|
||||||
|
.unwrap_or("None"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_sample_path(&mut self, path: PathBuf) {
|
||||||
|
if !self.config.sample_paths.contains(&path) {
|
||||||
|
self.config.sample_paths.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_last_sample_path(&mut self) {
|
||||||
|
self.config.sample_paths.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trigger_restart(&mut self) {
|
||||||
|
self.restart_pending = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub tempo: f64,
|
pub tempo: f64,
|
||||||
pub beat: f64,
|
pub beat: f64,
|
||||||
@@ -91,49 +301,39 @@ pub struct App {
|
|||||||
pub quantum: f64,
|
pub quantum: f64,
|
||||||
|
|
||||||
pub project: Project,
|
pub project: Project,
|
||||||
pub focus: Focus,
|
|
||||||
pub page: Page,
|
pub page: Page,
|
||||||
pub current_step: usize,
|
pub editor_ctx: EditorContext,
|
||||||
|
|
||||||
pub edit_bank: usize,
|
|
||||||
pub edit_pattern: usize,
|
|
||||||
|
|
||||||
pub patterns_view_level: PatternsViewLevel,
|
pub patterns_view_level: PatternsViewLevel,
|
||||||
pub patterns_cursor: usize,
|
pub patterns_cursor: usize,
|
||||||
|
|
||||||
// Slot playback state (synced from audio thread)
|
pub slot_data: [SlotState; MAX_SLOTS],
|
||||||
pub slot_data: [(bool, usize, usize); MAX_SLOTS], // (active, bank, pattern)
|
|
||||||
pub slot_steps: [usize; MAX_SLOTS],
|
pub slot_steps: [usize; MAX_SLOTS],
|
||||||
pub queued_changes: Vec<SlotChange>,
|
pub queued_changes: Vec<SlotChange>,
|
||||||
|
|
||||||
pub event_count: usize,
|
pub metrics: Metrics,
|
||||||
pub active_voices: usize,
|
|
||||||
pub peak_voices: usize,
|
|
||||||
pub cpu_load: f32,
|
|
||||||
pub schedule_depth: usize,
|
|
||||||
pub sample_pool_mb: f32,
|
pub sample_pool_mb: f32,
|
||||||
pub scope: [f32; 64],
|
|
||||||
pub script_engine: ScriptEngine,
|
pub script_engine: ScriptEngine,
|
||||||
pub variables: Variables,
|
pub variables: Variables,
|
||||||
pub rng: Rng,
|
pub rng: Rng,
|
||||||
pub file_path: Option<PathBuf>,
|
pub file_path: Option<PathBuf>,
|
||||||
pub status_message: Option<String>,
|
pub status_message: Option<String>,
|
||||||
pub editor: TextArea<'static>,
|
|
||||||
pub flash_until: Option<Instant>,
|
pub flash_until: Option<Instant>,
|
||||||
pub modal: Modal,
|
pub modal: Modal,
|
||||||
pub clipboard: Option<arboard::Clipboard>,
|
pub clipboard: Option<arboard::Clipboard>,
|
||||||
pub doc_topic: usize,
|
pub doc_topic: usize,
|
||||||
pub doc_scroll: usize,
|
pub doc_scroll: usize,
|
||||||
|
|
||||||
pub audio_config: AudioConfig,
|
pub audio: AudioSettings,
|
||||||
pub audio_focus: AudioFocus,
|
pub dirty_patterns: HashSet<(usize, usize)>,
|
||||||
pub available_output_devices: Vec<AudioDeviceInfo>,
|
|
||||||
pub available_input_devices: Vec<AudioDeviceInfo>,
|
|
||||||
pub restart_pending: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new(tempo: f64, quantum: f64) -> Self {
|
pub fn new(tempo: f64, quantum: f64) -> Self {
|
||||||
|
let variables = Arc::new(Mutex::new(HashMap::new()));
|
||||||
|
let rng = Arc::new(Mutex::new(StdRng::seed_from_u64(0)));
|
||||||
|
let script_engine = ScriptEngine::new(Arc::clone(&variables), Arc::clone(&rng));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
tempo,
|
tempo,
|
||||||
beat: 0.0,
|
beat: 0.0,
|
||||||
@@ -143,44 +343,39 @@ impl App {
|
|||||||
quantum,
|
quantum,
|
||||||
|
|
||||||
project: Project::default(),
|
project: Project::default(),
|
||||||
focus: Focus::Sequencer,
|
|
||||||
page: Page::default(),
|
page: Page::default(),
|
||||||
current_step: 0,
|
editor_ctx: EditorContext::default(),
|
||||||
|
|
||||||
edit_bank: 0,
|
|
||||||
edit_pattern: 0,
|
|
||||||
|
|
||||||
patterns_view_level: PatternsViewLevel::default(),
|
patterns_view_level: PatternsViewLevel::default(),
|
||||||
patterns_cursor: 0,
|
patterns_cursor: 0,
|
||||||
|
|
||||||
slot_data: [(false, 0, 0); MAX_SLOTS],
|
slot_data: [SlotState::default(); MAX_SLOTS],
|
||||||
slot_steps: [0; MAX_SLOTS],
|
slot_steps: [0; MAX_SLOTS],
|
||||||
queued_changes: Vec::new(),
|
queued_changes: Vec::new(),
|
||||||
|
|
||||||
event_count: 0,
|
metrics: Metrics::default(),
|
||||||
active_voices: 0,
|
|
||||||
peak_voices: 0,
|
|
||||||
cpu_load: 0.0,
|
|
||||||
schedule_depth: 0,
|
|
||||||
sample_pool_mb: 0.0,
|
sample_pool_mb: 0.0,
|
||||||
scope: [0.0; 64],
|
variables,
|
||||||
script_engine: ScriptEngine::new(),
|
rng,
|
||||||
variables: Arc::new(Mutex::new(HashMap::new())),
|
script_engine,
|
||||||
rng: Arc::new(Mutex::new(StdRng::seed_from_u64(0))),
|
|
||||||
file_path: None,
|
file_path: None,
|
||||||
status_message: None,
|
status_message: None,
|
||||||
editor: TextArea::default(),
|
|
||||||
flash_until: None,
|
flash_until: None,
|
||||||
modal: Modal::None,
|
modal: Modal::None,
|
||||||
clipboard: arboard::Clipboard::new().ok(),
|
clipboard: arboard::Clipboard::new().ok(),
|
||||||
doc_topic: 0,
|
doc_topic: 0,
|
||||||
doc_scroll: 0,
|
doc_scroll: 0,
|
||||||
|
|
||||||
audio_config: AudioConfig::default(),
|
audio: AudioSettings::default(),
|
||||||
audio_focus: AudioFocus::default(),
|
dirty_patterns: HashSet::new(),
|
||||||
available_output_devices: doux::audio::list_output_devices(),
|
}
|
||||||
available_input_devices: doux::audio::list_input_devices(),
|
}
|
||||||
restart_pending: false,
|
|
||||||
|
pub fn mark_all_patterns_dirty(&mut self) {
|
||||||
|
for bank in 0..MAX_BANKS {
|
||||||
|
for pattern in 0..MAX_PATTERNS {
|
||||||
|
self.dirty_patterns.insert((bank, pattern));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,32 +402,32 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_focus(&mut self) {
|
pub fn toggle_focus(&mut self) {
|
||||||
match self.focus {
|
match self.editor_ctx.focus {
|
||||||
Focus::Sequencer => {
|
Focus::Sequencer => {
|
||||||
self.focus = Focus::Editor;
|
self.editor_ctx.focus = Focus::Editor;
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
}
|
}
|
||||||
Focus::Editor => {
|
Focus::Editor => {
|
||||||
self.save_editor_to_step();
|
self.save_editor_to_step();
|
||||||
self.compile_current_step();
|
self.compile_current_step();
|
||||||
self.focus = Focus::Sequencer;
|
self.editor_ctx.focus = Focus::Sequencer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_edit_pattern(&self) -> &Pattern {
|
pub fn current_edit_pattern(&self) -> &Pattern {
|
||||||
self.project.pattern_at(self.edit_bank, self.edit_pattern)
|
self.project.pattern_at(self.editor_ctx.bank, self.editor_ctx.pattern)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_step(&mut self) {
|
pub fn next_step(&mut self) {
|
||||||
let len = self.current_edit_pattern().length;
|
let len = self.current_edit_pattern().length;
|
||||||
self.current_step = (self.current_step + 1) % len;
|
self.editor_ctx.step = (self.editor_ctx.step + 1) % len;
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prev_step(&mut self) {
|
pub fn prev_step(&mut self) {
|
||||||
let len = self.current_edit_pattern().length;
|
let len = self.current_edit_pattern().length;
|
||||||
self.current_step = (self.current_step + len - 1) % len;
|
self.editor_ctx.step = (self.editor_ctx.step + len - 1) % len;
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,10 +441,10 @@ impl App {
|
|||||||
};
|
};
|
||||||
let steps_per_row = len.div_ceil(num_rows);
|
let steps_per_row = len.div_ceil(num_rows);
|
||||||
|
|
||||||
if self.current_step >= steps_per_row {
|
if self.editor_ctx.step >= steps_per_row {
|
||||||
self.current_step -= steps_per_row;
|
self.editor_ctx.step -= steps_per_row;
|
||||||
} else {
|
} else {
|
||||||
self.current_step = (self.current_step + len - steps_per_row) % len;
|
self.editor_ctx.step = (self.editor_ctx.step + len - steps_per_row) % len;
|
||||||
}
|
}
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
}
|
}
|
||||||
@@ -264,75 +459,81 @@ impl App {
|
|||||||
};
|
};
|
||||||
let steps_per_row = len.div_ceil(num_rows);
|
let steps_per_row = len.div_ceil(num_rows);
|
||||||
|
|
||||||
self.current_step = (self.current_step + steps_per_row) % len;
|
self.editor_ctx.step = (self.editor_ctx.step + steps_per_row) % len;
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_step(&mut self) {
|
pub fn toggle_step(&mut self) {
|
||||||
let step_idx = self.current_step;
|
let step_idx = self.editor_ctx.step;
|
||||||
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
|
let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern);
|
||||||
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
|
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
|
||||||
step.active = !step.active;
|
step.active = !step.active;
|
||||||
}
|
}
|
||||||
|
self.dirty_patterns.insert((bank, pattern));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn length_increase(&mut self) {
|
pub fn length_increase(&mut self) {
|
||||||
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
|
let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern);
|
||||||
let current_len = self.project.pattern_at(bank, pattern).length;
|
let current_len = self.project.pattern_at(bank, pattern).length;
|
||||||
self.project
|
self.project
|
||||||
.pattern_at_mut(bank, pattern)
|
.pattern_at_mut(bank, pattern)
|
||||||
.set_length(current_len + 1);
|
.set_length(current_len + 1);
|
||||||
|
self.dirty_patterns.insert((bank, pattern));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn length_decrease(&mut self) {
|
pub fn length_decrease(&mut self) {
|
||||||
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
|
let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern);
|
||||||
let current_len = self.project.pattern_at(bank, pattern).length;
|
let current_len = self.project.pattern_at(bank, pattern).length;
|
||||||
self.project
|
self.project
|
||||||
.pattern_at_mut(bank, pattern)
|
.pattern_at_mut(bank, pattern)
|
||||||
.set_length(current_len.saturating_sub(1));
|
.set_length(current_len.saturating_sub(1));
|
||||||
let new_len = self.project.pattern_at(bank, pattern).length;
|
let new_len = self.project.pattern_at(bank, pattern).length;
|
||||||
if self.current_step >= new_len {
|
if self.editor_ctx.step >= new_len {
|
||||||
self.current_step = new_len - 1;
|
self.editor_ctx.step = new_len - 1;
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
}
|
}
|
||||||
|
self.dirty_patterns.insert((bank, pattern));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn speed_increase(&mut self) {
|
pub fn speed_increase(&mut self) {
|
||||||
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
|
let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern);
|
||||||
let pat = self.project.pattern_at_mut(bank, pattern);
|
let pat = self.project.pattern_at_mut(bank, pattern);
|
||||||
pat.speed = pat.speed.next();
|
pat.speed = pat.speed.next();
|
||||||
|
self.dirty_patterns.insert((bank, pattern));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn speed_decrease(&mut self) {
|
pub fn speed_decrease(&mut self) {
|
||||||
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
|
let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern);
|
||||||
let pat = self.project.pattern_at_mut(bank, pattern);
|
let pat = self.project.pattern_at_mut(bank, pattern);
|
||||||
pat.speed = pat.speed.prev();
|
pat.speed = pat.speed.prev();
|
||||||
|
self.dirty_patterns.insert((bank, pattern));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_step_to_editor(&mut self) {
|
fn load_step_to_editor(&mut self) {
|
||||||
let step_idx = self.current_step;
|
let step_idx = self.editor_ctx.step;
|
||||||
if let Some(step) = self.current_edit_pattern().step(step_idx) {
|
if let Some(step) = self.current_edit_pattern().step(step_idx) {
|
||||||
let lines: Vec<String> = if step.script.is_empty() {
|
let lines: Vec<String> = if step.script.is_empty() {
|
||||||
vec![String::new()]
|
vec![String::new()]
|
||||||
} else {
|
} else {
|
||||||
step.script.lines().map(String::from).collect()
|
step.script.lines().map(String::from).collect()
|
||||||
};
|
};
|
||||||
self.editor = TextArea::new(lines);
|
self.editor_ctx.text = TextArea::new(lines);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save_editor_to_step(&mut self) {
|
pub fn save_editor_to_step(&mut self) {
|
||||||
let text = self.editor.lines().join("\n");
|
let text = self.editor_ctx.text.lines().join("\n");
|
||||||
let step_idx = self.current_step;
|
let step_idx = self.editor_ctx.step;
|
||||||
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
|
let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern);
|
||||||
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
|
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
|
||||||
step.script = text;
|
step.script = text;
|
||||||
}
|
}
|
||||||
|
self.dirty_patterns.insert((bank, pattern));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compile_current_step(&mut self) {
|
pub fn compile_current_step(&mut self) {
|
||||||
let step_idx = self.current_step;
|
let step_idx = self.editor_ctx.step;
|
||||||
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
|
let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern);
|
||||||
|
|
||||||
let script = self
|
let script = self
|
||||||
.project
|
.project
|
||||||
@@ -358,7 +559,7 @@ impl App {
|
|||||||
slot: 0,
|
slot: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
match self.script_engine.evaluate(&script, &ctx, &self.variables, &self.rng) {
|
match self.script_engine.evaluate(&script, &ctx) {
|
||||||
Ok(cmd) => {
|
Ok(cmd) => {
|
||||||
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
|
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
|
||||||
step.command = Some(cmd);
|
step.command = Some(cmd);
|
||||||
@@ -377,7 +578,7 @@ impl App {
|
|||||||
|
|
||||||
pub fn compile_all_steps(&mut self) {
|
pub fn compile_all_steps(&mut self) {
|
||||||
let pattern_len = self.current_edit_pattern().length;
|
let pattern_len = self.current_edit_pattern().length;
|
||||||
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
|
let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern);
|
||||||
|
|
||||||
for step_idx in 0..pattern_len {
|
for step_idx in 0..pattern_len {
|
||||||
let script = self
|
let script = self
|
||||||
@@ -404,7 +605,7 @@ impl App {
|
|||||||
slot: 0,
|
slot: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Ok(cmd) = self.script_engine.evaluate(&script, &ctx, &self.variables, &self.rng) {
|
if let Ok(cmd) = self.script_engine.evaluate(&script, &ctx) {
|
||||||
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
|
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
|
||||||
step.command = Some(cmd);
|
step.command = Some(cmd);
|
||||||
}
|
}
|
||||||
@@ -418,8 +619,8 @@ impl App {
|
|||||||
Some(true)
|
Some(true)
|
||||||
}
|
}
|
||||||
SlotChange::Remove { slot } => {
|
SlotChange::Remove { slot } => {
|
||||||
let (active, b, p) = self.slot_data[*slot];
|
let s = self.slot_data[*slot];
|
||||||
if active && b == bank && p == pattern {
|
if s.active && s.bank == bank && s.pattern == pattern {
|
||||||
Some(false)
|
Some(false)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -430,8 +631,8 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_pattern_playback(&mut self, bank: usize, pattern: usize) {
|
pub fn toggle_pattern_playback(&mut self, bank: usize, pattern: usize) {
|
||||||
let playing_slot = self.slot_data.iter().enumerate().find_map(|(i, (active, b, p))| {
|
let playing_slot = self.slot_data.iter().enumerate().find_map(|(i, s)| {
|
||||||
if *active && *b == bank && *p == pattern {
|
if s.active && s.bank == bank && s.pattern == pattern {
|
||||||
Some(i)
|
Some(i)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -441,8 +642,8 @@ impl App {
|
|||||||
let pending = self.queued_changes.iter().position(|c| match c {
|
let pending = self.queued_changes.iter().position(|c| match c {
|
||||||
SlotChange::Add { bank: b, pattern: p, .. } => *b == bank && *p == pattern,
|
SlotChange::Add { bank: b, pattern: p, .. } => *b == bank && *p == pattern,
|
||||||
SlotChange::Remove { slot } => {
|
SlotChange::Remove { slot } => {
|
||||||
let (_, b, p) = self.slot_data[*slot];
|
let s = self.slot_data[*slot];
|
||||||
b == bank && p == pattern
|
s.bank == bank && s.pattern == pattern
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -453,7 +654,7 @@ impl App {
|
|||||||
self.queued_changes.push(SlotChange::Remove { slot: slot_idx });
|
self.queued_changes.push(SlotChange::Remove { slot: slot_idx });
|
||||||
self.status_message = Some(format!("B{:02}:P{:02} queued to stop", bank + 1, pattern + 1));
|
self.status_message = Some(format!("B{:02}:P{:02} queued to stop", bank + 1, pattern + 1));
|
||||||
} else {
|
} else {
|
||||||
let free_slot = (0..MAX_SLOTS).find(|&i| !self.slot_data[i].0);
|
let free_slot = (0..MAX_SLOTS).find(|&i| !self.slot_data[i].active);
|
||||||
if let Some(slot_idx) = free_slot {
|
if let Some(slot_idx) = free_slot {
|
||||||
self.queued_changes.push(SlotChange::Add { slot: slot_idx, bank, pattern });
|
self.queued_changes.push(SlotChange::Add { slot: slot_idx, bank, pattern });
|
||||||
self.status_message = Some(format!("B{:02}:P{:02} queued to play", bank + 1, pattern + 1));
|
self.status_message = Some(format!("B{:02}:P{:02} queued to play", bank + 1, pattern + 1));
|
||||||
@@ -464,15 +665,15 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_edit_pattern(&mut self, pattern: usize) {
|
pub fn select_edit_pattern(&mut self, pattern: usize) {
|
||||||
self.edit_pattern = pattern;
|
self.editor_ctx.pattern = pattern;
|
||||||
self.current_step = 0;
|
self.editor_ctx.step = 0;
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn select_edit_bank(&mut self, bank: usize) {
|
pub fn select_edit_bank(&mut self, bank: usize) {
|
||||||
self.edit_bank = bank;
|
self.editor_ctx.bank = bank;
|
||||||
self.edit_pattern = 0;
|
self.editor_ctx.pattern = 0;
|
||||||
self.current_step = 0;
|
self.editor_ctx.step = 0;
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,9 +694,10 @@ impl App {
|
|||||||
match file::load(&path) {
|
match file::load(&path) {
|
||||||
Ok(project) => {
|
Ok(project) => {
|
||||||
self.project = project;
|
self.project = project;
|
||||||
self.current_step = 0;
|
self.editor_ctx.step = 0;
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
self.compile_all_steps();
|
self.compile_all_steps();
|
||||||
|
self.mark_all_patterns_dirty();
|
||||||
self.status_message = Some(format!("Loaded: {}", path.display()));
|
self.status_message = Some(format!("Loaded: {}", path.display()));
|
||||||
self.file_path = Some(path);
|
self.file_path = Some(path);
|
||||||
}
|
}
|
||||||
@@ -516,7 +718,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn copy_step(&mut self) {
|
pub fn copy_step(&mut self) {
|
||||||
let step_idx = self.current_step;
|
let step_idx = self.editor_ctx.step;
|
||||||
let script = self
|
let script = self
|
||||||
.current_edit_pattern()
|
.current_edit_pattern()
|
||||||
.step(step_idx)
|
.step(step_idx)
|
||||||
@@ -538,11 +740,12 @@ impl App {
|
|||||||
.and_then(|clip| clip.get_text().ok());
|
.and_then(|clip| clip.get_text().ok());
|
||||||
|
|
||||||
if let Some(text) = text {
|
if let Some(text) = text {
|
||||||
let step_idx = self.current_step;
|
let step_idx = self.editor_ctx.step;
|
||||||
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
|
let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern);
|
||||||
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
|
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
|
||||||
step.script = text;
|
step.script = text;
|
||||||
}
|
}
|
||||||
|
self.dirty_patterns.insert((bank, pattern));
|
||||||
self.load_step_to_editor();
|
self.load_step_to_editor();
|
||||||
self.compile_current_step();
|
self.compile_current_step();
|
||||||
}
|
}
|
||||||
@@ -556,142 +759,4 @@ impl App {
|
|||||||
self.modal = Modal::SetPattern { field, input: current };
|
self.modal = Modal::SetPattern { field, input: current };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh_audio_devices(&mut self) {
|
|
||||||
self.available_output_devices = doux::audio::list_output_devices();
|
|
||||||
self.available_input_devices = doux::audio::list_input_devices();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next_audio_focus(&mut self) {
|
|
||||||
self.audio_focus = match self.audio_focus {
|
|
||||||
AudioFocus::OutputDevice => AudioFocus::InputDevice,
|
|
||||||
AudioFocus::InputDevice => AudioFocus::Channels,
|
|
||||||
AudioFocus::Channels => AudioFocus::BufferSize,
|
|
||||||
AudioFocus::BufferSize => AudioFocus::SamplePaths,
|
|
||||||
AudioFocus::SamplePaths => AudioFocus::OutputDevice,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prev_audio_focus(&mut self) {
|
|
||||||
self.audio_focus = match self.audio_focus {
|
|
||||||
AudioFocus::OutputDevice => AudioFocus::SamplePaths,
|
|
||||||
AudioFocus::InputDevice => AudioFocus::OutputDevice,
|
|
||||||
AudioFocus::Channels => AudioFocus::InputDevice,
|
|
||||||
AudioFocus::BufferSize => AudioFocus::Channels,
|
|
||||||
AudioFocus::SamplePaths => AudioFocus::BufferSize,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next_output_device(&mut self) {
|
|
||||||
if self.available_output_devices.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let current_idx = self.current_output_device_index();
|
|
||||||
let next_idx = (current_idx + 1) % self.available_output_devices.len();
|
|
||||||
self.audio_config.output_device = Some(self.available_output_devices[next_idx].name.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prev_output_device(&mut self) {
|
|
||||||
if self.available_output_devices.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let current_idx = self.current_output_device_index();
|
|
||||||
let prev_idx = (current_idx + self.available_output_devices.len() - 1) % self.available_output_devices.len();
|
|
||||||
self.audio_config.output_device = Some(self.available_output_devices[prev_idx].name.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_output_device_index(&self) -> usize {
|
|
||||||
match &self.audio_config.output_device {
|
|
||||||
Some(name) => self
|
|
||||||
.available_output_devices
|
|
||||||
.iter()
|
|
||||||
.position(|d| &d.name == name)
|
|
||||||
.unwrap_or(0),
|
|
||||||
None => self
|
|
||||||
.available_output_devices
|
|
||||||
.iter()
|
|
||||||
.position(|d| d.is_default)
|
|
||||||
.unwrap_or(0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next_input_device(&mut self) {
|
|
||||||
if self.available_input_devices.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let current_idx = self.current_input_device_index();
|
|
||||||
let next_idx = (current_idx + 1) % self.available_input_devices.len();
|
|
||||||
self.audio_config.input_device = Some(self.available_input_devices[next_idx].name.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prev_input_device(&mut self) {
|
|
||||||
if self.available_input_devices.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let current_idx = self.current_input_device_index();
|
|
||||||
let prev_idx = (current_idx + self.available_input_devices.len() - 1) % self.available_input_devices.len();
|
|
||||||
self.audio_config.input_device = Some(self.available_input_devices[prev_idx].name.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_input_device_index(&self) -> usize {
|
|
||||||
match &self.audio_config.input_device {
|
|
||||||
Some(name) => self
|
|
||||||
.available_input_devices
|
|
||||||
.iter()
|
|
||||||
.position(|d| &d.name == name)
|
|
||||||
.unwrap_or(0),
|
|
||||||
None => self
|
|
||||||
.available_input_devices
|
|
||||||
.iter()
|
|
||||||
.position(|d| d.is_default)
|
|
||||||
.unwrap_or(0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn adjust_channels(&mut self, delta: i16) {
|
|
||||||
let new_val = (self.audio_config.channels as i16 + delta).clamp(1, 64) as u16;
|
|
||||||
self.audio_config.channels = new_val;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn adjust_buffer_size(&mut self, delta: i32) {
|
|
||||||
let new_val = (self.audio_config.buffer_size as i32 + delta).clamp(64, 4096) as u32;
|
|
||||||
self.audio_config.buffer_size = new_val;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn trigger_restart(&mut self) {
|
|
||||||
self.restart_pending = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current_output_device_name(&self) -> &str {
|
|
||||||
match &self.audio_config.output_device {
|
|
||||||
Some(name) => name,
|
|
||||||
None => self
|
|
||||||
.available_output_devices
|
|
||||||
.iter()
|
|
||||||
.find(|d| d.is_default)
|
|
||||||
.map(|d| d.name.as_str())
|
|
||||||
.unwrap_or("Default"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current_input_device_name(&self) -> &str {
|
|
||||||
match &self.audio_config.input_device {
|
|
||||||
Some(name) => name,
|
|
||||||
None => self
|
|
||||||
.available_input_devices
|
|
||||||
.iter()
|
|
||||||
.find(|d| d.is_default)
|
|
||||||
.map(|d| d.name.as_str())
|
|
||||||
.unwrap_or("None"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_sample_path(&mut self, path: PathBuf) {
|
|
||||||
if !self.audio_config.sample_paths.contains(&path) {
|
|
||||||
self.audio_config.sample_paths.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_last_sample_path(&mut self) {
|
|
||||||
self.audio_config.sample_paths.pop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
179
seq/src/audio.rs
179
seq/src/audio.rs
@@ -1,12 +1,34 @@
|
|||||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||||
use cpal::Stream;
|
use cpal::Stream;
|
||||||
use doux::Engine;
|
use crossbeam_channel::Receiver;
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
use doux::{Engine, EngineMetrics};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::link::LinkState;
|
use crate::sequencer::AudioCommand;
|
||||||
use crate::model::Project;
|
|
||||||
use crate::script::{Rng, ScriptEngine, StepContext, Variables};
|
pub struct ScopeBuffer {
|
||||||
|
pub samples: [AtomicU32; 64],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScopeBuffer {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
samples: std::array::from_fn(|_| AtomicU32::new(0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(&self, data: &[f32]) {
|
||||||
|
for (i, atom) in self.samples.iter().enumerate() {
|
||||||
|
let val = data.get(i * 2).copied().unwrap_or(0.0);
|
||||||
|
atom.store(val.to_bits(), Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(&self) -> [f32; 64] {
|
||||||
|
std::array::from_fn(|i| f32::from_bits(self.samples[i].load(Ordering::Relaxed)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct AudioStreamConfig {
|
pub struct AudioStreamConfig {
|
||||||
pub output_device: Option<String>,
|
pub output_device: Option<String>,
|
||||||
@@ -14,8 +36,6 @@ pub struct AudioStreamConfig {
|
|||||||
pub buffer_size: u32,
|
pub buffer_size: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const MAX_SLOTS: usize = 8;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default)]
|
#[derive(Clone, Copy, Default)]
|
||||||
pub struct PatternSlot {
|
pub struct PatternSlot {
|
||||||
pub bank: usize,
|
pub bank: usize,
|
||||||
@@ -24,38 +44,12 @@ pub struct PatternSlot {
|
|||||||
pub active: bool,
|
pub active: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum SlotChange {
|
|
||||||
Add { slot: usize, bank: usize, pattern: usize },
|
|
||||||
Remove { slot: usize },
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AudioState {
|
|
||||||
prev_beat: f64,
|
|
||||||
pub slots: [PatternSlot; MAX_SLOTS],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AudioState {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
prev_beat: -1.0,
|
|
||||||
slots: [PatternSlot::default(); MAX_SLOTS],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_stream(
|
pub fn build_stream(
|
||||||
config: &AudioStreamConfig,
|
config: &AudioStreamConfig,
|
||||||
engine: Arc<Mutex<Engine>>,
|
audio_rx: Receiver<AudioCommand>,
|
||||||
link: Arc<LinkState>,
|
scope_buffer: Arc<ScopeBuffer>,
|
||||||
playing: Arc<AtomicBool>,
|
metrics: Arc<EngineMetrics>,
|
||||||
project: Arc<Mutex<Project>>,
|
initial_samples: Vec<doux::sample::SampleEntry>,
|
||||||
slot_steps: [Arc<AtomicUsize>; MAX_SLOTS],
|
|
||||||
event_count: Arc<AtomicUsize>,
|
|
||||||
slot_data: Arc<Mutex<[(bool, usize, usize); MAX_SLOTS]>>,
|
|
||||||
slot_changes: Arc<Mutex<Vec<SlotChange>>>,
|
|
||||||
variables: Variables,
|
|
||||||
rng: Rng,
|
|
||||||
) -> Result<(Stream, f32), String> {
|
) -> Result<(Stream, f32), String> {
|
||||||
let host = cpal::default_host();
|
let host = cpal::default_host();
|
||||||
|
|
||||||
@@ -80,12 +74,13 @@ pub fn build_stream(
|
|||||||
buffer_size,
|
buffer_size,
|
||||||
};
|
};
|
||||||
|
|
||||||
let quantum = 4.0;
|
|
||||||
let audio_state = Arc::new(Mutex::new(AudioState::new()));
|
|
||||||
let script_engine = ScriptEngine::new();
|
|
||||||
|
|
||||||
let sr = sample_rate;
|
let sr = sample_rate;
|
||||||
let channels = config.channels as usize;
|
let channels = config.channels as usize;
|
||||||
|
let metrics_clone = Arc::clone(&metrics);
|
||||||
|
|
||||||
|
let mut engine = Engine::new_with_metrics(sample_rate, channels, Arc::clone(&metrics));
|
||||||
|
engine.sample_index = initial_samples;
|
||||||
|
|
||||||
let stream = device
|
let stream = device
|
||||||
.build_output_stream(
|
.build_output_stream(
|
||||||
&stream_config,
|
&stream_config,
|
||||||
@@ -93,97 +88,31 @@ pub fn build_stream(
|
|||||||
let buffer_samples = data.len() / channels;
|
let buffer_samples = data.len() / channels;
|
||||||
let buffer_time_ns = (buffer_samples as f64 / sr as f64 * 1e9) as u64;
|
let buffer_time_ns = (buffer_samples as f64 / sr as f64 * 1e9) as u64;
|
||||||
|
|
||||||
let is_playing = playing.load(Ordering::Relaxed);
|
while let Ok(cmd) = audio_rx.try_recv() {
|
||||||
|
match cmd {
|
||||||
if is_playing {
|
AudioCommand::Evaluate(s) => {
|
||||||
let state = link.capture_audio_state();
|
engine.evaluate(&s);
|
||||||
let time = link.clock_micros();
|
|
||||||
let beat = state.beat_at_time(time, quantum);
|
|
||||||
let tempo = state.tempo();
|
|
||||||
|
|
||||||
let mut audio = audio_state.lock().unwrap();
|
|
||||||
let proj = project.lock().unwrap();
|
|
||||||
|
|
||||||
// Apply queued slot changes at bar boundaries (every 4 beats)
|
|
||||||
let bar = (beat / quantum).floor() as i64;
|
|
||||||
let prev_bar = (audio.prev_beat / quantum).floor() as i64;
|
|
||||||
if bar != prev_bar && audio.prev_beat >= 0.0 {
|
|
||||||
let mut changes = slot_changes.lock().unwrap();
|
|
||||||
for change in changes.drain(..) {
|
|
||||||
match change {
|
|
||||||
SlotChange::Add { slot, bank, pattern } => {
|
|
||||||
audio.slots[slot] = PatternSlot {
|
|
||||||
bank,
|
|
||||||
pattern,
|
|
||||||
step_index: 0,
|
|
||||||
active: true,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
SlotChange::Remove { slot } => {
|
AudioCommand::Hush => {
|
||||||
audio.slots[slot].active = false;
|
engine.hush();
|
||||||
}
|
}
|
||||||
|
AudioCommand::Panic => {
|
||||||
|
engine.panic();
|
||||||
}
|
}
|
||||||
|
AudioCommand::LoadSamples(samples) => {
|
||||||
|
engine.sample_index.extend(samples);
|
||||||
}
|
}
|
||||||
}
|
AudioCommand::ResetEngine => {
|
||||||
|
let old_samples = std::mem::take(&mut engine.sample_index);
|
||||||
// Read prev_beat before the mutable borrow of slots
|
engine = Engine::new_with_metrics(sr, channels, Arc::clone(&metrics_clone));
|
||||||
let prev_beat = audio.prev_beat;
|
engine.sample_index = old_samples;
|
||||||
|
|
||||||
// Iterate all active slots
|
|
||||||
for (slot_idx, slot) in audio.slots.iter_mut().enumerate() {
|
|
||||||
if !slot.active {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let pattern = proj.pattern_at(slot.bank, slot.pattern);
|
|
||||||
let speed_mult = pattern.speed.multiplier();
|
|
||||||
|
|
||||||
let beat_int = (beat * 4.0 * speed_mult).floor() as i64;
|
|
||||||
let prev_beat_int = (prev_beat * 4.0 * speed_mult).floor() as i64;
|
|
||||||
|
|
||||||
if beat_int != prev_beat_int && prev_beat >= 0.0 {
|
|
||||||
let step_idx = slot.step_index % pattern.length;
|
|
||||||
|
|
||||||
slot_steps[slot_idx].store(step_idx, Ordering::Relaxed);
|
|
||||||
|
|
||||||
if let Some(step) = pattern.step(step_idx) {
|
|
||||||
if step.active && !step.script.trim().is_empty() {
|
|
||||||
let ctx = StepContext {
|
|
||||||
step: step_idx,
|
|
||||||
beat,
|
|
||||||
bank: slot.bank,
|
|
||||||
pattern: slot.pattern,
|
|
||||||
tempo,
|
|
||||||
phase: beat % quantum,
|
|
||||||
slot: slot_idx,
|
|
||||||
};
|
|
||||||
if let Ok(cmd) =
|
|
||||||
script_engine.evaluate(&step.script, &ctx, &variables, &rng)
|
|
||||||
{
|
|
||||||
engine.lock().unwrap().evaluate(&cmd);
|
|
||||||
event_count.fetch_add(1, Ordering::Relaxed);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
slot.step_index = (slot.step_index + 1) % pattern.length;
|
engine.metrics.load.set_buffer_time(buffer_time_ns);
|
||||||
}
|
engine.process_block(data, &[], &[]);
|
||||||
}
|
scope_buffer.write(&engine.output);
|
||||||
|
|
||||||
// Update shared slot data for UI
|
|
||||||
{
|
|
||||||
let mut sd = slot_data.lock().unwrap();
|
|
||||||
for (i, slot) in audio.slots.iter().enumerate() {
|
|
||||||
sd[i] = (slot.active, slot.bank, slot.pattern);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
audio.prev_beat = beat;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut eng = engine.lock().unwrap();
|
|
||||||
eng.metrics.load.set_buffer_time(buffer_time_ns);
|
|
||||||
eng.process_block(data, &[], &[]);
|
|
||||||
},
|
},
|
||||||
|err| eprintln!("stream error: {err}"),
|
|err| eprintln!("stream error: {err}"),
|
||||||
None,
|
None,
|
||||||
|
|||||||
7
seq/src/config.rs
Normal file
7
seq/src/config.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
pub const MAX_SLOTS: usize = 8;
|
||||||
|
pub const MAX_BANKS: usize = 16;
|
||||||
|
pub const MAX_PATTERNS: usize = 16;
|
||||||
|
pub const MAX_STEPS: usize = 32;
|
||||||
|
pub const DEFAULT_LENGTH: usize = 16;
|
||||||
|
pub const DEFAULT_TEMPO: f64 = 120.0;
|
||||||
|
pub const DEFAULT_QUANTUM: f64 = 4.0;
|
||||||
@@ -38,9 +38,9 @@ impl LinkState {
|
|||||||
self.link.commit_app_session_state(&state);
|
self.link.commit_app_session_state(&state);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn capture_audio_state(&self) -> SessionState {
|
pub fn capture_app_state(&self) -> SessionState {
|
||||||
let mut state = SessionState::new();
|
let mut state = SessionState::new();
|
||||||
self.link.capture_audio_session_state(&mut state);
|
self.link.capture_app_session_state(&mut state);
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
273
seq/src/main.rs
273
seq/src/main.rs
@@ -1,18 +1,20 @@
|
|||||||
mod app;
|
mod app;
|
||||||
mod audio;
|
mod audio;
|
||||||
|
mod config;
|
||||||
mod file;
|
mod file;
|
||||||
mod link;
|
mod link;
|
||||||
mod model;
|
mod model;
|
||||||
mod page;
|
mod page;
|
||||||
mod script;
|
mod script;
|
||||||
|
mod sequencer;
|
||||||
mod ui;
|
mod ui;
|
||||||
mod views;
|
mod views;
|
||||||
mod widgets;
|
mod widgets;
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
@@ -21,15 +23,15 @@ use crossterm::terminal::{
|
|||||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||||
};
|
};
|
||||||
use crossterm::ExecutableCommand;
|
use crossterm::ExecutableCommand;
|
||||||
use doux::Engine;
|
use doux::EngineMetrics;
|
||||||
use ratatui::prelude::CrosstermBackend;
|
use ratatui::prelude::CrosstermBackend;
|
||||||
use ratatui::Terminal;
|
use ratatui::Terminal;
|
||||||
|
|
||||||
use app::{App, AudioFocus, Focus, Modal, PatternField};
|
use app::{App, AudioFocus, Focus, Modal, PatternField};
|
||||||
use audio::{AudioStreamConfig, SlotChange, MAX_SLOTS};
|
use audio::{AudioStreamConfig, ScopeBuffer};
|
||||||
use link::LinkState;
|
use link::LinkState;
|
||||||
use model::Project;
|
|
||||||
use page::Page;
|
use page::Page;
|
||||||
|
use sequencer::{spawn_sequencer, AudioCommand, PatternSnapshot, SeqCommand, 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")]
|
||||||
@@ -55,72 +57,59 @@ struct Args {
|
|||||||
buffer: u32,
|
buffer: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
const TEMPO: f64 = 120.0;
|
use config::{DEFAULT_QUANTUM, DEFAULT_TEMPO};
|
||||||
const QUANTUM: f64 = 4.0;
|
|
||||||
|
|
||||||
fn main() -> io::Result<()> {
|
fn main() -> io::Result<()> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
let link = Arc::new(LinkState::new(TEMPO, QUANTUM));
|
let link = Arc::new(LinkState::new(DEFAULT_TEMPO, DEFAULT_QUANTUM));
|
||||||
link.enable();
|
link.enable();
|
||||||
|
|
||||||
let playing = Arc::new(AtomicBool::new(true));
|
let playing = Arc::new(AtomicBool::new(true));
|
||||||
let event_count = Arc::new(AtomicUsize::new(0));
|
|
||||||
|
|
||||||
// Slot state shared between audio thread and UI
|
let mut app = App::new(DEFAULT_TEMPO, DEFAULT_QUANTUM);
|
||||||
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);
|
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;
|
||||||
|
|
||||||
// Apply CLI args to audio config
|
let metrics = Arc::new(EngineMetrics::default());
|
||||||
app.audio_config.output_device = args.output;
|
let scope_buffer = Arc::new(ScopeBuffer::new());
|
||||||
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 mut initial_samples = Vec::new();
|
||||||
let project = Arc::new(Mutex::new(Project::default()));
|
for path in &app.audio.config.sample_paths {
|
||||||
|
|
||||||
// Load sample directories
|
|
||||||
for path in &app.audio_config.sample_paths {
|
|
||||||
let index = doux::loader::scan_samples_dir(path);
|
let index = doux::loader::scan_samples_dir(path);
|
||||||
let count = index.len();
|
app.audio.config.sample_count += index.len();
|
||||||
engine.lock().unwrap().sample_index.extend(index);
|
initial_samples.extend(index);
|
||||||
app.audio_config.sample_count += count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sequencer = spawn_sequencer(
|
||||||
|
Arc::clone(&link),
|
||||||
|
Arc::clone(&playing),
|
||||||
|
Arc::clone(&app.variables),
|
||||||
|
Arc::clone(&app.rng),
|
||||||
|
DEFAULT_QUANTUM,
|
||||||
|
);
|
||||||
|
|
||||||
let stream_config = AudioStreamConfig {
|
let stream_config = AudioStreamConfig {
|
||||||
output_device: app.audio_config.output_device.clone(),
|
output_device: app.audio.config.output_device.clone(),
|
||||||
channels: app.audio_config.channels,
|
channels: app.audio.config.channels,
|
||||||
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) = audio::build_stream(
|
||||||
&stream_config,
|
&stream_config,
|
||||||
Arc::clone(&engine),
|
sequencer.audio_rx.clone(),
|
||||||
Arc::clone(&link),
|
Arc::clone(&scope_buffer),
|
||||||
Arc::clone(&playing),
|
Arc::clone(&metrics),
|
||||||
Arc::clone(&project),
|
initial_samples,
|
||||||
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 start audio");
|
.expect("Failed to start audio");
|
||||||
|
|
||||||
app.audio_config.sample_rate = sample_rate;
|
app.audio.config.sample_rate = sample_rate;
|
||||||
|
app.mark_all_patterns_dirty();
|
||||||
{
|
|
||||||
let mut eng = engine.lock().unwrap();
|
|
||||||
eng.sr = sample_rate;
|
|
||||||
eng.isr = 1.0 / sample_rate;
|
|
||||||
}
|
|
||||||
|
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
io::stdout().execute(EnterAlternateScreen)?;
|
io::stdout().execute(EnterAlternateScreen)?;
|
||||||
@@ -129,62 +118,52 @@ fn main() -> io::Result<()> {
|
|||||||
let mut terminal = Terminal::new(backend)?;
|
let mut terminal = Terminal::new(backend)?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if app.restart_pending {
|
if app.audio.restart_pending {
|
||||||
app.restart_pending = false;
|
app.audio.restart_pending = false;
|
||||||
drop(stream);
|
drop(stream);
|
||||||
|
|
||||||
let new_config = AudioStreamConfig {
|
let new_config = AudioStreamConfig {
|
||||||
output_device: app.audio_config.output_device.clone(),
|
output_device: app.audio.config.output_device.clone(),
|
||||||
channels: app.audio_config.channels,
|
channels: app.audio.config.channels,
|
||||||
buffer_size: app.audio_config.buffer_size,
|
buffer_size: app.audio.config.buffer_size,
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
let mut restart_samples = Vec::new();
|
||||||
let mut eng = engine.lock().unwrap();
|
for path in &app.audio.config.sample_paths {
|
||||||
*eng = Engine::new_with_channels(eng.sr, new_config.channels as usize);
|
let index = doux::loader::scan_samples_dir(path);
|
||||||
|
restart_samples.extend(index);
|
||||||
}
|
}
|
||||||
|
app.audio.config.sample_count = restart_samples.len();
|
||||||
|
|
||||||
match audio::build_stream(
|
match audio::build_stream(
|
||||||
&new_config,
|
&new_config,
|
||||||
Arc::clone(&engine),
|
sequencer.audio_rx.clone(),
|
||||||
Arc::clone(&link),
|
Arc::clone(&scope_buffer),
|
||||||
Arc::clone(&playing),
|
Arc::clone(&metrics),
|
||||||
Arc::clone(&project),
|
restart_samples,
|
||||||
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)) => {
|
Ok((new_stream, sr)) => {
|
||||||
stream = new_stream;
|
stream = new_stream;
|
||||||
app.audio_config.sample_rate = sr;
|
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());
|
app.status_message = Some("Audio restarted".to_string());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
app.status_message = Some(format!("Restart failed: {e}"));
|
app.status_message = Some(format!("Restart failed: {e}"));
|
||||||
|
let mut fallback_samples = Vec::new();
|
||||||
|
for path in &app.audio.config.sample_paths {
|
||||||
|
let index = doux::loader::scan_samples_dir(path);
|
||||||
|
fallback_samples.extend(index);
|
||||||
|
}
|
||||||
let (fallback_stream, _) = audio::build_stream(
|
let (fallback_stream, _) = audio::build_stream(
|
||||||
&AudioStreamConfig {
|
&AudioStreamConfig {
|
||||||
output_device: None,
|
output_device: None,
|
||||||
channels: 2,
|
channels: 2,
|
||||||
buffer_size: 512,
|
buffer_size: 512,
|
||||||
},
|
},
|
||||||
Arc::clone(&engine),
|
sequencer.audio_rx.clone(),
|
||||||
Arc::clone(&link),
|
Arc::clone(&scope_buffer),
|
||||||
Arc::clone(&playing),
|
Arc::clone(&metrics),
|
||||||
Arc::clone(&project),
|
fallback_samples,
|
||||||
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");
|
.expect("Failed to restart with defaults");
|
||||||
stream = fallback_stream;
|
stream = fallback_stream;
|
||||||
@@ -194,37 +173,42 @@ fn main() -> io::Result<()> {
|
|||||||
|
|
||||||
app.update_from_link(&link);
|
app.update_from_link(&link);
|
||||||
app.playing = playing.load(Ordering::Relaxed);
|
app.playing = playing.load(Ordering::Relaxed);
|
||||||
app.event_count = event_count.load(Ordering::Relaxed);
|
|
||||||
|
|
||||||
{
|
{
|
||||||
let eng = engine.lock().unwrap();
|
app.metrics.active_voices = metrics.active_voices.load(Ordering::Relaxed) as usize;
|
||||||
app.active_voices = eng.active_voices;
|
app.metrics.peak_voices = app.metrics.peak_voices.max(app.metrics.active_voices);
|
||||||
app.peak_voices = app.peak_voices.max(eng.active_voices);
|
app.metrics.cpu_load = metrics.load.get_load();
|
||||||
app.cpu_load = eng.metrics.load.get_load();
|
app.metrics.schedule_depth = metrics.schedule_depth.load(Ordering::Relaxed) as usize;
|
||||||
app.schedule_depth = eng.schedule.len();
|
app.metrics.scope = scope_buffer.read();
|
||||||
for (i, s) in app.scope.iter_mut().enumerate() {
|
}
|
||||||
*s = eng.output.get(i * 2).copied().unwrap_or(0.0);
|
|
||||||
|
let seq_snapshot = sequencer.snapshot();
|
||||||
|
app.slot_data = seq_snapshot.slot_data;
|
||||||
|
app.slot_steps = seq_snapshot.slot_steps;
|
||||||
|
app.metrics.event_count = seq_snapshot.event_count;
|
||||||
|
|
||||||
|
for change in app.queued_changes.drain(..) {
|
||||||
|
match change {
|
||||||
|
app::SlotChange::Add { slot, bank, pattern } => {
|
||||||
|
let _ = sequencer.cmd_tx.send(SeqCommand::SlotAdd { slot, bank, pattern });
|
||||||
|
}
|
||||||
|
app::SlotChange::Remove { slot } => {
|
||||||
|
let _ = sequencer.cmd_tx.send(SeqCommand::SlotRemove { slot });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync slot state from audio thread
|
for (bank, pattern) in app.dirty_patterns.drain() {
|
||||||
{
|
let pat = app.project.pattern_at(bank, pattern);
|
||||||
let sd = slot_data.lock().unwrap();
|
let snapshot = PatternSnapshot {
|
||||||
app.slot_data = *sd;
|
speed: pat.speed,
|
||||||
}
|
length: pat.length,
|
||||||
for (i, step_atomic) in slot_steps.iter().enumerate() {
|
steps: pat.steps.iter().take(pat.length).map(|s| StepSnapshot {
|
||||||
app.slot_steps[i] = step_atomic.load(Ordering::Relaxed);
|
active: s.active,
|
||||||
}
|
script: s.script.clone(),
|
||||||
|
}).collect(),
|
||||||
// Push queued changes to audio thread
|
};
|
||||||
if !app.queued_changes.is_empty() {
|
let _ = sequencer.cmd_tx.send(SeqCommand::PatternUpdate { bank, pattern, data: snapshot });
|
||||||
let mut changes = slot_changes.lock().unwrap();
|
|
||||||
changes.extend(app.queued_changes.drain(..));
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut proj = project.lock().unwrap();
|
|
||||||
proj.banks = app.project.banks.clone();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
terminal.draw(|frame| ui::render(frame, &mut app))?;
|
terminal.draw(|frame| ui::render(frame, &mut app))?;
|
||||||
@@ -324,15 +308,16 @@ fn main() -> io::Result<()> {
|
|||||||
Modal::SetPattern { field, input } => match key.code {
|
Modal::SetPattern { field, input } => match key.code {
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
let field = *field;
|
let field = *field;
|
||||||
let (bank, pattern) = (app.edit_bank, app.edit_pattern);
|
let (bank, pattern) = (app.editor_ctx.bank, app.editor_ctx.pattern);
|
||||||
match field {
|
match field {
|
||||||
PatternField::Length => {
|
PatternField::Length => {
|
||||||
if let Ok(len) = input.parse::<usize>() {
|
if let Ok(len) = input.parse::<usize>() {
|
||||||
app.project.pattern_at_mut(bank, pattern).set_length(len);
|
app.project.pattern_at_mut(bank, pattern).set_length(len);
|
||||||
let new_len = app.project.pattern_at(bank, pattern).length;
|
let new_len = app.project.pattern_at(bank, pattern).length;
|
||||||
if app.current_step >= new_len {
|
if app.editor_ctx.step >= new_len {
|
||||||
app.current_step = new_len - 1;
|
app.editor_ctx.step = new_len - 1;
|
||||||
}
|
}
|
||||||
|
app.dirty_patterns.insert((bank, pattern));
|
||||||
app.status_message = Some(format!("Length set to {new_len}"));
|
app.status_message = Some(format!("Length set to {new_len}"));
|
||||||
} else {
|
} else {
|
||||||
app.status_message = Some("Invalid length".to_string());
|
app.status_message = Some("Invalid length".to_string());
|
||||||
@@ -341,6 +326,7 @@ fn main() -> io::Result<()> {
|
|||||||
PatternField::Speed => {
|
PatternField::Speed => {
|
||||||
if let Some(speed) = model::PatternSpeed::from_label(input) {
|
if let Some(speed) = model::PatternSpeed::from_label(input) {
|
||||||
app.project.pattern_at_mut(bank, pattern).speed = speed;
|
app.project.pattern_at_mut(bank, pattern).speed = speed;
|
||||||
|
app.dirty_patterns.insert((bank, pattern));
|
||||||
app.status_message = Some(format!("Speed set to {}", speed.label()));
|
app.status_message = Some(format!("Speed set to {}", speed.label()));
|
||||||
} else {
|
} else {
|
||||||
app.status_message = Some("Invalid speed (try 1/8x, 1/4x, 1/2x, 1x, 2x, 4x, 8x)".to_string());
|
app.status_message = Some("Invalid speed (try 1/8x, 1/4x, 1/2x, 1x, 2x, 4x, 8x)".to_string());
|
||||||
@@ -366,9 +352,9 @@ fn main() -> io::Result<()> {
|
|||||||
if sample_path.is_dir() {
|
if sample_path.is_dir() {
|
||||||
let index = doux::loader::scan_samples_dir(&sample_path);
|
let index = doux::loader::scan_samples_dir(&sample_path);
|
||||||
let count = index.len();
|
let count = index.len();
|
||||||
engine.lock().unwrap().sample_index.extend(index);
|
let _ = sequencer.audio_tx.send(AudioCommand::LoadSamples(index));
|
||||||
app.audio_config.sample_count += count;
|
app.audio.config.sample_count += count;
|
||||||
app.add_sample_path(sample_path);
|
app.audio.add_sample_path(sample_path);
|
||||||
app.status_message = Some(format!("Added {count} samples"));
|
app.status_message = Some(format!("Added {count} samples"));
|
||||||
} else {
|
} else {
|
||||||
app.status_message = Some("Path is not a directory".to_string());
|
app.status_message = Some("Path is not a directory".to_string());
|
||||||
@@ -407,7 +393,7 @@ fn main() -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match app.page {
|
match app.page {
|
||||||
Page::Main => match app.focus {
|
Page::Main => match app.editor_ctx.focus {
|
||||||
Focus::Sequencer => match key.code {
|
Focus::Sequencer => match key.code {
|
||||||
KeyCode::Char('q') => {
|
KeyCode::Char('q') => {
|
||||||
app.modal = Modal::ConfirmQuit { selected: false };
|
app.modal = Modal::ConfirmQuit { selected: false };
|
||||||
@@ -456,7 +442,7 @@ fn main() -> io::Result<()> {
|
|||||||
app.compile_current_step();
|
app.compile_current_step();
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
app.editor.input(Event::Key(key));
|
app.editor_ctx.text.input(Event::Key(key));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -537,57 +523,48 @@ fn main() -> io::Result<()> {
|
|||||||
app.modal = Modal::ConfirmQuit { selected: false };
|
app.modal = Modal::ConfirmQuit { selected: false };
|
||||||
}
|
}
|
||||||
KeyCode::Up | KeyCode::Char('k') => {
|
KeyCode::Up | KeyCode::Char('k') => {
|
||||||
app.prev_audio_focus();
|
app.audio.prev_focus();
|
||||||
}
|
}
|
||||||
KeyCode::Down | KeyCode::Char('j') => {
|
KeyCode::Down | KeyCode::Char('j') => {
|
||||||
app.next_audio_focus();
|
app.audio.next_focus();
|
||||||
}
|
}
|
||||||
KeyCode::Left => match app.audio_focus {
|
KeyCode::Left => match app.audio.focus {
|
||||||
AudioFocus::OutputDevice => app.prev_output_device(),
|
AudioFocus::OutputDevice => app.audio.prev_output_device(),
|
||||||
AudioFocus::InputDevice => app.prev_input_device(),
|
AudioFocus::InputDevice => app.audio.prev_input_device(),
|
||||||
AudioFocus::Channels => app.adjust_channels(-1),
|
AudioFocus::Channels => app.audio.adjust_channels(-1),
|
||||||
AudioFocus::BufferSize => app.adjust_buffer_size(-64),
|
AudioFocus::BufferSize => app.audio.adjust_buffer_size(-64),
|
||||||
AudioFocus::SamplePaths => app.remove_last_sample_path(),
|
AudioFocus::SamplePaths => app.audio.remove_last_sample_path(),
|
||||||
},
|
},
|
||||||
KeyCode::Right => match app.audio_focus {
|
KeyCode::Right => match app.audio.focus {
|
||||||
AudioFocus::OutputDevice => app.next_output_device(),
|
AudioFocus::OutputDevice => app.audio.next_output_device(),
|
||||||
AudioFocus::InputDevice => app.next_input_device(),
|
AudioFocus::InputDevice => app.audio.next_input_device(),
|
||||||
AudioFocus::Channels => app.adjust_channels(1),
|
AudioFocus::Channels => app.audio.adjust_channels(1),
|
||||||
AudioFocus::BufferSize => app.adjust_buffer_size(64),
|
AudioFocus::BufferSize => app.audio.adjust_buffer_size(64),
|
||||||
AudioFocus::SamplePaths => {}
|
AudioFocus::SamplePaths => {}
|
||||||
},
|
},
|
||||||
KeyCode::Char('R') => {
|
KeyCode::Char('R') => {
|
||||||
app.trigger_restart();
|
app.audio.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') => {
|
KeyCode::Char('A') => {
|
||||||
app.modal = Modal::AddSamplePath(String::new());
|
app.modal = Modal::AddSamplePath(String::new());
|
||||||
}
|
}
|
||||||
KeyCode::Char('D') => {
|
KeyCode::Char('D') => {
|
||||||
app.refresh_audio_devices();
|
app.audio.refresh_devices();
|
||||||
let out_count = app.available_output_devices.len();
|
let out_count = app.audio.output_devices.len();
|
||||||
let in_count = app.available_input_devices.len();
|
let in_count = app.audio.input_devices.len();
|
||||||
app.status_message = Some(format!("Found {out_count} output, {in_count} input devices"));
|
app.status_message = Some(format!("Found {out_count} output, {in_count} input devices"));
|
||||||
}
|
}
|
||||||
KeyCode::Char('h') => {
|
KeyCode::Char('h') => {
|
||||||
engine.lock().unwrap().hush();
|
let _ = sequencer.audio_tx.send(AudioCommand::Hush);
|
||||||
}
|
}
|
||||||
KeyCode::Char('p') => {
|
KeyCode::Char('p') => {
|
||||||
engine.lock().unwrap().panic();
|
let _ = sequencer.audio_tx.send(AudioCommand::Panic);
|
||||||
}
|
}
|
||||||
KeyCode::Char('r') => {
|
KeyCode::Char('r') => {
|
||||||
app.peak_voices = 0;
|
app.metrics.peak_voices = 0;
|
||||||
}
|
}
|
||||||
KeyCode::Char('t') => {
|
KeyCode::Char('t') => {
|
||||||
engine.lock().unwrap().evaluate("sin 440 * 0.3");
|
let _ = sequencer.audio_tx.send(AudioCommand::Evaluate("sin 440 * 0.3".into()));
|
||||||
}
|
}
|
||||||
KeyCode::Char(' ') => {
|
KeyCode::Char(' ') => {
|
||||||
app.toggle_playing();
|
app.toggle_playing();
|
||||||
@@ -628,5 +605,7 @@ fn main() -> io::Result<()> {
|
|||||||
disable_raw_mode()?;
|
disable_raw_mode()?;
|
||||||
io::stdout().execute(LeaveAlternateScreen)?;
|
io::stdout().execute(LeaveAlternateScreen)?;
|
||||||
|
|
||||||
|
sequencer.shutdown();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::config::{DEFAULT_LENGTH, MAX_BANKS, MAX_PATTERNS, MAX_STEPS};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq)]
|
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq)]
|
||||||
pub enum PatternSpeed {
|
pub enum PatternSpeed {
|
||||||
Eighth, // 1/8x
|
Eighth, // 1/8x
|
||||||
@@ -106,8 +108,8 @@ pub struct Pattern {
|
|||||||
impl Default for Pattern {
|
impl Default for Pattern {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
steps: (0..32).map(|_| Step::default()).collect(),
|
steps: (0..MAX_STEPS).map(|_| Step::default()).collect(),
|
||||||
length: 16,
|
length: DEFAULT_LENGTH,
|
||||||
speed: PatternSpeed::default(),
|
speed: PatternSpeed::default(),
|
||||||
name: None,
|
name: None,
|
||||||
}
|
}
|
||||||
@@ -124,7 +126,7 @@ impl Pattern {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_length(&mut self, length: usize) {
|
pub fn set_length(&mut self, length: usize) {
|
||||||
let length = length.clamp(2, 32);
|
let length = length.clamp(2, MAX_STEPS);
|
||||||
while self.steps.len() < length {
|
while self.steps.len() < length {
|
||||||
self.steps.push(Step::default());
|
self.steps.push(Step::default());
|
||||||
}
|
}
|
||||||
@@ -142,7 +144,7 @@ pub struct Bank {
|
|||||||
impl Default for Bank {
|
impl Default for Bank {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
patterns: (0..16).map(|_| Pattern::default()).collect(),
|
patterns: (0..MAX_PATTERNS).map(|_| Pattern::default()).collect(),
|
||||||
name: None,
|
name: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,7 +158,7 @@ pub struct Project {
|
|||||||
impl Default for Project {
|
impl Default for Project {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
banks: (0..16).map(|_| Bank::default()).collect(),
|
banks: (0..MAX_BANKS).map(|_| Bank::default()).collect(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,40 +62,20 @@ pub struct StepContext {
|
|||||||
pub slot: usize,
|
pub slot: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ScriptEngine;
|
pub struct ScriptEngine {
|
||||||
|
engine: Engine,
|
||||||
|
}
|
||||||
|
|
||||||
impl ScriptEngine {
|
impl ScriptEngine {
|
||||||
pub fn new() -> Self {
|
pub fn new(vars: Variables, rng: Rng) -> Self {
|
||||||
Self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn evaluate(
|
|
||||||
&self,
|
|
||||||
script: &str,
|
|
||||||
ctx: &StepContext,
|
|
||||||
vars: &Variables,
|
|
||||||
rng: &Rng,
|
|
||||||
) -> Result<String, String> {
|
|
||||||
if script.trim().is_empty() {
|
|
||||||
return Err("empty script".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut scope = Scope::new();
|
|
||||||
scope.push("step", ctx.step as i64);
|
|
||||||
scope.push("beat", ctx.beat);
|
|
||||||
scope.push("bank", ctx.bank as i64);
|
|
||||||
scope.push("pattern", ctx.pattern as i64);
|
|
||||||
scope.push("tempo", ctx.tempo);
|
|
||||||
scope.push("phase", ctx.phase);
|
|
||||||
scope.push("slot", ctx.slot as i64);
|
|
||||||
|
|
||||||
let vars_for_set = Arc::clone(vars);
|
|
||||||
let vars_for_get = Arc::clone(vars);
|
|
||||||
|
|
||||||
let mut engine = Engine::new();
|
let mut engine = Engine::new();
|
||||||
engine.set_max_expr_depths(64, 32);
|
engine.set_max_expr_depths(64, 32);
|
||||||
|
|
||||||
register_cmd(&mut engine);
|
register_cmd(&mut engine);
|
||||||
|
|
||||||
|
let vars_for_set = 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()
|
.lock()
|
||||||
@@ -112,11 +92,11 @@ impl ScriptEngine {
|
|||||||
.unwrap_or(Dynamic::UNIT)
|
.unwrap_or(Dynamic::UNIT)
|
||||||
});
|
});
|
||||||
|
|
||||||
let rng_rand_ff = Arc::clone(rng);
|
let rng_rand_ff = Arc::clone(&rng);
|
||||||
let rng_rand_ii = Arc::clone(rng);
|
let rng_rand_ii = Arc::clone(&rng);
|
||||||
let rng_rrand_ff = Arc::clone(rng);
|
let rng_rrand_ff = Arc::clone(&rng);
|
||||||
let rng_rrand_ii = Arc::clone(rng);
|
let rng_rrand_ii = Arc::clone(&rng);
|
||||||
let rng_seed = Arc::clone(rng);
|
let rng_seed = Arc::clone(&rng);
|
||||||
|
|
||||||
engine.register_fn("rand", move |min: f64, max: f64| -> f64 {
|
engine.register_fn("rand", move |min: f64, max: f64| -> f64 {
|
||||||
rng_rand_ff.lock().unwrap().gen_range(min..max)
|
rng_rand_ff.lock().unwrap().gen_range(min..max)
|
||||||
@@ -136,11 +116,28 @@ impl ScriptEngine {
|
|||||||
*rng_seed.lock().unwrap() = StdRng::seed_from_u64(s as u64);
|
*rng_seed.lock().unwrap() = StdRng::seed_from_u64(s as u64);
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Ok(cmd) = engine.eval_with_scope::<Cmd>(&mut scope, script) {
|
Self { engine }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn evaluate(&self, script: &str, ctx: &StepContext) -> Result<String, String> {
|
||||||
|
if script.trim().is_empty() {
|
||||||
|
return Err("empty script".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut scope = Scope::new();
|
||||||
|
scope.push("step", ctx.step as i64);
|
||||||
|
scope.push("beat", ctx.beat);
|
||||||
|
scope.push("bank", ctx.bank as i64);
|
||||||
|
scope.push("pattern", ctx.pattern as i64);
|
||||||
|
scope.push("tempo", ctx.tempo);
|
||||||
|
scope.push("phase", ctx.phase);
|
||||||
|
scope.push("slot", ctx.slot as i64);
|
||||||
|
|
||||||
|
if let Ok(cmd) = self.engine.eval_with_scope::<Cmd>(&mut scope, script) {
|
||||||
return Ok(cmd.to_string());
|
return Ok(cmd.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
engine
|
self.engine
|
||||||
.eval_with_scope::<String>(&mut scope, script)
|
.eval_with_scope::<String>(&mut scope, script)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|||||||
328
seq/src/sequencer.rs
Normal file
328
seq/src/sequencer.rs
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
use crossbeam_channel::{bounded, Receiver, Sender, TrySendError};
|
||||||
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread::{self, JoinHandle};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::audio::PatternSlot;
|
||||||
|
use crate::config::{MAX_BANKS, MAX_PATTERNS, MAX_SLOTS};
|
||||||
|
use crate::link::LinkState;
|
||||||
|
use crate::script::{ScriptEngine, StepContext, Variables, Rng};
|
||||||
|
|
||||||
|
pub enum AudioCommand {
|
||||||
|
Evaluate(String),
|
||||||
|
Hush,
|
||||||
|
Panic,
|
||||||
|
LoadSamples(Vec<doux::sample::SampleEntry>),
|
||||||
|
#[allow(dead_code)]
|
||||||
|
ResetEngine,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum SeqCommand {
|
||||||
|
PatternUpdate { bank: usize, pattern: usize, data: PatternSnapshot },
|
||||||
|
SlotAdd { slot: usize, bank: usize, pattern: usize },
|
||||||
|
SlotRemove { slot: usize },
|
||||||
|
Shutdown,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PatternSnapshot {
|
||||||
|
pub speed: crate::model::PatternSpeed,
|
||||||
|
pub length: usize,
|
||||||
|
pub steps: Vec<StepSnapshot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct StepSnapshot {
|
||||||
|
pub active: bool,
|
||||||
|
pub script: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default)]
|
||||||
|
pub struct SlotState {
|
||||||
|
pub active: bool,
|
||||||
|
pub bank: usize,
|
||||||
|
pub pattern: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AtomicSlotData {
|
||||||
|
active: AtomicBool,
|
||||||
|
bank: AtomicUsize,
|
||||||
|
pattern: AtomicUsize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AtomicSlotData {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
active: AtomicBool::new(false),
|
||||||
|
bank: AtomicUsize::new(0),
|
||||||
|
pattern: AtomicUsize::new(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(&self) -> SlotState {
|
||||||
|
SlotState {
|
||||||
|
active: self.active.load(Ordering::Relaxed),
|
||||||
|
bank: self.bank.load(Ordering::Relaxed),
|
||||||
|
pattern: self.pattern.load(Ordering::Relaxed),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn store(&self, state: SlotState) {
|
||||||
|
self.active.store(state.active, Ordering::Relaxed);
|
||||||
|
self.bank.store(state.bank, Ordering::Relaxed);
|
||||||
|
self.pattern.store(state.pattern, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SequencerSnapshot {
|
||||||
|
pub slot_data: [SlotState; MAX_SLOTS],
|
||||||
|
pub slot_steps: [usize; MAX_SLOTS],
|
||||||
|
pub event_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SequencerHandle {
|
||||||
|
pub cmd_tx: Sender<SeqCommand>,
|
||||||
|
pub audio_tx: Sender<AudioCommand>,
|
||||||
|
pub audio_rx: Receiver<AudioCommand>,
|
||||||
|
slot_data: Arc<[AtomicSlotData; MAX_SLOTS]>,
|
||||||
|
slot_steps: [Arc<AtomicUsize>; MAX_SLOTS],
|
||||||
|
event_count: Arc<AtomicUsize>,
|
||||||
|
thread: JoinHandle<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SequencerHandle {
|
||||||
|
pub fn snapshot(&self) -> SequencerSnapshot {
|
||||||
|
SequencerSnapshot {
|
||||||
|
slot_data: std::array::from_fn(|i| self.slot_data[i].load()),
|
||||||
|
slot_steps: std::array::from_fn(|i| self.slot_steps[i].load(Ordering::Relaxed)),
|
||||||
|
event_count: self.event_count.load(Ordering::Relaxed),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shutdown(self) {
|
||||||
|
let _ = self.cmd_tx.send(SeqCommand::Shutdown);
|
||||||
|
let _ = self.thread.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AudioState {
|
||||||
|
prev_beat: f64,
|
||||||
|
slots: [PatternSlot; MAX_SLOTS],
|
||||||
|
pending_changes: Vec<PendingChange>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PendingChange {
|
||||||
|
Add { slot: usize, bank: usize, pattern: usize },
|
||||||
|
Remove { slot: usize },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioState {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
prev_beat: -1.0,
|
||||||
|
slots: [PatternSlot::default(); MAX_SLOTS],
|
||||||
|
pending_changes: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_sequencer(
|
||||||
|
link: Arc<LinkState>,
|
||||||
|
playing: Arc<AtomicBool>,
|
||||||
|
variables: Variables,
|
||||||
|
rng: Rng,
|
||||||
|
quantum: f64,
|
||||||
|
) -> SequencerHandle {
|
||||||
|
let (cmd_tx, cmd_rx) = bounded::<SeqCommand>(64);
|
||||||
|
let (audio_tx, audio_rx) = bounded::<AudioCommand>(256);
|
||||||
|
|
||||||
|
let slot_data: Arc<[AtomicSlotData; MAX_SLOTS]> =
|
||||||
|
Arc::new(std::array::from_fn(|_| AtomicSlotData::new()));
|
||||||
|
let slot_steps: [Arc<AtomicUsize>; MAX_SLOTS] =
|
||||||
|
std::array::from_fn(|_| Arc::new(AtomicUsize::new(0)));
|
||||||
|
let event_count = Arc::new(AtomicUsize::new(0));
|
||||||
|
|
||||||
|
let slot_data_clone = Arc::clone(&slot_data);
|
||||||
|
let slot_steps_clone = slot_steps.clone();
|
||||||
|
let event_count_clone = Arc::clone(&event_count);
|
||||||
|
let audio_tx_clone = audio_tx.clone();
|
||||||
|
|
||||||
|
let thread = thread::Builder::new()
|
||||||
|
.name("sequencer".into())
|
||||||
|
.spawn(move || {
|
||||||
|
sequencer_loop(
|
||||||
|
cmd_rx,
|
||||||
|
audio_tx_clone,
|
||||||
|
link,
|
||||||
|
playing,
|
||||||
|
variables,
|
||||||
|
rng,
|
||||||
|
quantum,
|
||||||
|
slot_data_clone,
|
||||||
|
slot_steps_clone,
|
||||||
|
event_count_clone,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.expect("Failed to spawn sequencer thread");
|
||||||
|
|
||||||
|
SequencerHandle {
|
||||||
|
cmd_tx,
|
||||||
|
audio_tx,
|
||||||
|
audio_rx,
|
||||||
|
slot_data,
|
||||||
|
slot_steps,
|
||||||
|
event_count,
|
||||||
|
thread,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PatternCache {
|
||||||
|
patterns: [[Option<PatternSnapshot>; MAX_PATTERNS]; MAX_BANKS],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PatternCache {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
patterns: std::array::from_fn(|_| std::array::from_fn(|_| None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, bank: usize, pattern: usize) -> Option<&PatternSnapshot> {
|
||||||
|
self.patterns.get(bank).and_then(|b| b.get(pattern)).and_then(|p| p.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(&mut self, bank: usize, pattern: usize, data: PatternSnapshot) {
|
||||||
|
if bank < MAX_BANKS && pattern < MAX_PATTERNS {
|
||||||
|
self.patterns[bank][pattern] = Some(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sequencer_loop(
|
||||||
|
cmd_rx: Receiver<SeqCommand>,
|
||||||
|
audio_tx: Sender<AudioCommand>,
|
||||||
|
link: Arc<LinkState>,
|
||||||
|
playing: Arc<AtomicBool>,
|
||||||
|
variables: Variables,
|
||||||
|
rng: Rng,
|
||||||
|
quantum: f64,
|
||||||
|
slot_data: Arc<[AtomicSlotData; MAX_SLOTS]>,
|
||||||
|
slot_steps: [Arc<AtomicUsize>; MAX_SLOTS],
|
||||||
|
event_count: Arc<AtomicUsize>,
|
||||||
|
) {
|
||||||
|
let script_engine = ScriptEngine::new(variables, rng);
|
||||||
|
let mut audio_state = AudioState::new();
|
||||||
|
let mut pattern_cache = PatternCache::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
while let Ok(cmd) = cmd_rx.try_recv() {
|
||||||
|
match cmd {
|
||||||
|
SeqCommand::PatternUpdate { bank, pattern, data } => {
|
||||||
|
pattern_cache.set(bank, pattern, data);
|
||||||
|
}
|
||||||
|
SeqCommand::SlotAdd { slot, bank, pattern } => {
|
||||||
|
audio_state.pending_changes.push(PendingChange::Add { slot, bank, pattern });
|
||||||
|
}
|
||||||
|
SeqCommand::SlotRemove { slot } => {
|
||||||
|
audio_state.pending_changes.push(PendingChange::Remove { slot });
|
||||||
|
}
|
||||||
|
SeqCommand::Shutdown => {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !playing.load(Ordering::Relaxed) {
|
||||||
|
thread::sleep(Duration::from_micros(500));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = link.capture_app_state();
|
||||||
|
let time = link.clock_micros();
|
||||||
|
let beat = state.beat_at_time(time, quantum);
|
||||||
|
let tempo = state.tempo();
|
||||||
|
|
||||||
|
let bar = (beat / quantum).floor() as i64;
|
||||||
|
let prev_bar = (audio_state.prev_beat / quantum).floor() as i64;
|
||||||
|
if bar != prev_bar && audio_state.prev_beat >= 0.0 {
|
||||||
|
for change in audio_state.pending_changes.drain(..) {
|
||||||
|
match change {
|
||||||
|
PendingChange::Add { slot, bank, pattern } => {
|
||||||
|
audio_state.slots[slot] = PatternSlot {
|
||||||
|
bank,
|
||||||
|
pattern,
|
||||||
|
step_index: 0,
|
||||||
|
active: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
PendingChange::Remove { slot } => {
|
||||||
|
audio_state.slots[slot].active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prev_beat = audio_state.prev_beat;
|
||||||
|
|
||||||
|
for (slot_idx, slot) in audio_state.slots.iter_mut().enumerate() {
|
||||||
|
if !slot.active {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(pattern) = pattern_cache.get(slot.bank, slot.pattern) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let speed_mult = pattern.speed.multiplier();
|
||||||
|
let beat_int = (beat * 4.0 * speed_mult).floor() as i64;
|
||||||
|
let prev_beat_int = (prev_beat * 4.0 * speed_mult).floor() as i64;
|
||||||
|
|
||||||
|
if beat_int != prev_beat_int && prev_beat >= 0.0 {
|
||||||
|
let step_idx = slot.step_index % pattern.length;
|
||||||
|
|
||||||
|
slot_steps[slot_idx].store(step_idx, Ordering::Relaxed);
|
||||||
|
|
||||||
|
if let Some(step) = pattern.steps.get(step_idx) {
|
||||||
|
if step.active && !step.script.trim().is_empty() {
|
||||||
|
let ctx = StepContext {
|
||||||
|
step: step_idx,
|
||||||
|
beat,
|
||||||
|
bank: slot.bank,
|
||||||
|
pattern: slot.pattern,
|
||||||
|
tempo,
|
||||||
|
phase: beat % quantum,
|
||||||
|
slot: slot_idx,
|
||||||
|
};
|
||||||
|
if let Ok(cmd) = script_engine.evaluate(&step.script, &ctx)
|
||||||
|
{
|
||||||
|
match audio_tx.try_send(AudioCommand::Evaluate(cmd)) {
|
||||||
|
Ok(()) => {
|
||||||
|
event_count.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
Err(TrySendError::Full(_)) => {}
|
||||||
|
Err(TrySendError::Disconnected(_)) => {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slot.step_index = (slot.step_index + 1) % pattern.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, slot) in audio_state.slots.iter().enumerate() {
|
||||||
|
slot_data[i].store(SlotState {
|
||||||
|
active: slot.active,
|
||||||
|
bank: slot.bank,
|
||||||
|
pattern: slot.pattern,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
audio_state.prev_beat = beat;
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_micros(500));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ fn render_header(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
Color::Red
|
Color::Red
|
||||||
};
|
};
|
||||||
|
|
||||||
let cpu_pct = (app.cpu_load * 100.0).min(100.0);
|
let cpu_pct = (app.metrics.cpu_load * 100.0).min(100.0);
|
||||||
let cpu_color = if cpu_pct > 80.0 {
|
let cpu_color = if cpu_pct > 80.0 {
|
||||||
Color::Red
|
Color::Red
|
||||||
} else if cpu_pct > 50.0 {
|
} else if cpu_pct > 50.0 {
|
||||||
@@ -52,7 +52,7 @@ fn render_header(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
let left_spans = vec![
|
let left_spans = vec![
|
||||||
Span::styled("EDIT ", Style::new().fg(Color::Cyan)),
|
Span::styled("EDIT ", Style::new().fg(Color::Cyan)),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("B{:02}:P{:02}", app.edit_bank + 1, app.edit_pattern + 1),
|
format!("B{:02}:P{:02}", app.editor_ctx.bank + 1, app.editor_ctx.pattern + 1),
|
||||||
Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD),
|
Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
@@ -61,7 +61,7 @@ fn render_header(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
|
|
||||||
frame.render_widget(Paragraph::new(Line::from(left_spans)), left_area);
|
frame.render_widget(Paragraph::new(Line::from(left_spans)), left_area);
|
||||||
|
|
||||||
let pattern = app.project.pattern_at(app.edit_bank, app.edit_pattern);
|
let pattern = app.project.pattern_at(app.editor_ctx.bank, app.editor_ctx.pattern);
|
||||||
let right_spans = vec![
|
let right_spans = vec![
|
||||||
Span::styled(format!("L:{:02}", pattern.length), Style::new().fg(Color::Rgb(180, 140, 90))),
|
Span::styled(format!("L:{:02}", pattern.length), Style::new().fg(Color::Rgb(180, 140, 90))),
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
@@ -71,7 +71,7 @@ fn render_header(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
Span::styled(format!("CPU:{cpu_pct:.0}%"), Style::new().fg(cpu_color)),
|
Span::styled(format!("CPU:{cpu_pct:.0}%"), Style::new().fg(cpu_color)),
|
||||||
Span::raw(" "),
|
Span::raw(" "),
|
||||||
Span::styled(format!("V:{}", app.active_voices), Style::new().fg(Color::Cyan)),
|
Span::styled(format!("V:{}", app.metrics.active_voices), Style::new().fg(Color::Cyan)),
|
||||||
];
|
];
|
||||||
|
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ fn render_config(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
let normal = Style::new().fg(Color::White);
|
let normal = Style::new().fg(Color::White);
|
||||||
let dim = Style::new().fg(Color::DarkGray);
|
let dim = Style::new().fg(Color::DarkGray);
|
||||||
|
|
||||||
let output_name = truncate_name(app.current_output_device_name(), 25);
|
let output_name = truncate_name(app.audio.current_output_device_name(), 25);
|
||||||
let output_style = if app.audio_focus == AudioFocus::OutputDevice {
|
let output_style = if app.audio.focus == AudioFocus::OutputDevice {
|
||||||
highlight
|
highlight
|
||||||
} else {
|
} else {
|
||||||
normal
|
normal
|
||||||
@@ -62,8 +62,8 @@ fn render_config(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
]);
|
]);
|
||||||
frame.render_widget(Paragraph::new(output_line), output_area);
|
frame.render_widget(Paragraph::new(output_line), output_area);
|
||||||
|
|
||||||
let input_name = truncate_name(app.current_input_device_name(), 25);
|
let input_name = truncate_name(app.audio.current_input_device_name(), 25);
|
||||||
let input_style = if app.audio_focus == AudioFocus::InputDevice {
|
let input_style = if app.audio.focus == AudioFocus::InputDevice {
|
||||||
highlight
|
highlight
|
||||||
} else {
|
} else {
|
||||||
normal
|
normal
|
||||||
@@ -76,7 +76,7 @@ fn render_config(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
]);
|
]);
|
||||||
frame.render_widget(Paragraph::new(input_line), input_area);
|
frame.render_widget(Paragraph::new(input_line), input_area);
|
||||||
|
|
||||||
let channels_style = if app.audio_focus == AudioFocus::Channels {
|
let channels_style = if app.audio.focus == AudioFocus::Channels {
|
||||||
highlight
|
highlight
|
||||||
} else {
|
} else {
|
||||||
normal
|
normal
|
||||||
@@ -84,12 +84,12 @@ fn render_config(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
let channels_line = Line::from(vec![
|
let channels_line = Line::from(vec![
|
||||||
Span::styled("Channels ", dim),
|
Span::styled("Channels ", dim),
|
||||||
Span::styled("< ", channels_style),
|
Span::styled("< ", channels_style),
|
||||||
Span::styled(format!("{:2}", app.audio_config.channels), channels_style),
|
Span::styled(format!("{:2}", app.audio.config.channels), channels_style),
|
||||||
Span::styled(" >", channels_style),
|
Span::styled(" >", channels_style),
|
||||||
]);
|
]);
|
||||||
frame.render_widget(Paragraph::new(channels_line), channels_area);
|
frame.render_widget(Paragraph::new(channels_line), channels_area);
|
||||||
|
|
||||||
let buffer_style = if app.audio_focus == AudioFocus::BufferSize {
|
let buffer_style = if app.audio.focus == AudioFocus::BufferSize {
|
||||||
highlight
|
highlight
|
||||||
} else {
|
} else {
|
||||||
normal
|
normal
|
||||||
@@ -97,18 +97,18 @@ fn render_config(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
let buffer_line = Line::from(vec![
|
let buffer_line = Line::from(vec![
|
||||||
Span::styled("Buffer ", dim),
|
Span::styled("Buffer ", dim),
|
||||||
Span::styled("< ", buffer_style),
|
Span::styled("< ", buffer_style),
|
||||||
Span::styled(format!("{:4}", app.audio_config.buffer_size), buffer_style),
|
Span::styled(format!("{:4}", app.audio.config.buffer_size), buffer_style),
|
||||||
Span::styled(" >", buffer_style),
|
Span::styled(" >", buffer_style),
|
||||||
]);
|
]);
|
||||||
frame.render_widget(Paragraph::new(buffer_line), buffer_area);
|
frame.render_widget(Paragraph::new(buffer_line), buffer_area);
|
||||||
|
|
||||||
let rate_line = Line::from(vec![
|
let rate_line = Line::from(vec![
|
||||||
Span::styled("Rate ", dim),
|
Span::styled("Rate ", dim),
|
||||||
Span::styled(format!("{:.0} Hz", app.audio_config.sample_rate), normal),
|
Span::styled(format!("{:.0} Hz", app.audio.config.sample_rate), normal),
|
||||||
]);
|
]);
|
||||||
frame.render_widget(Paragraph::new(rate_line), rate_area);
|
frame.render_widget(Paragraph::new(rate_line), rate_area);
|
||||||
|
|
||||||
let samples_style = if app.audio_focus == AudioFocus::SamplePaths {
|
let samples_style = if app.audio.focus == AudioFocus::SamplePaths {
|
||||||
highlight
|
highlight
|
||||||
} else {
|
} else {
|
||||||
normal
|
normal
|
||||||
@@ -117,12 +117,12 @@ fn render_config(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
let mut sample_lines = vec![Line::from(vec![
|
let mut sample_lines = vec![Line::from(vec![
|
||||||
Span::styled("Samples ", dim),
|
Span::styled("Samples ", dim),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("{} paths, {} indexed", app.audio_config.sample_paths.len(), app.audio_config.sample_count),
|
format!("{} paths, {} indexed", app.audio.config.sample_paths.len(), app.audio.config.sample_count),
|
||||||
samples_style,
|
samples_style,
|
||||||
),
|
),
|
||||||
])];
|
])];
|
||||||
|
|
||||||
for (i, path) in app.audio_config.sample_paths.iter().take(2).enumerate() {
|
for (i, path) in app.audio.config.sample_paths.iter().take(2).enumerate() {
|
||||||
let path_str = path.to_string_lossy();
|
let path_str = path.to_string_lossy();
|
||||||
let display = truncate_name(&path_str, 35);
|
let display = truncate_name(&path_str, 35);
|
||||||
sample_lines.push(Line::from(vec![
|
sample_lines.push(Line::from(vec![
|
||||||
@@ -157,7 +157,7 @@ fn render_stats(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
])
|
])
|
||||||
.areas(inner);
|
.areas(inner);
|
||||||
|
|
||||||
let cpu_pct = (app.cpu_load * 100.0).min(100.0);
|
let cpu_pct = (app.metrics.cpu_load * 100.0).min(100.0);
|
||||||
let cpu_color = if cpu_pct > 80.0 {
|
let cpu_color = if cpu_pct > 80.0 {
|
||||||
Color::Red
|
Color::Red
|
||||||
} else if cpu_pct > 50.0 {
|
} else if cpu_pct > 50.0 {
|
||||||
@@ -174,9 +174,9 @@ fn render_stats(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
|
|
||||||
frame.render_widget(gauge, cpu_area);
|
frame.render_widget(gauge, cpu_area);
|
||||||
|
|
||||||
let voice_color = if app.active_voices > 24 {
|
let voice_color = if app.metrics.active_voices > 24 {
|
||||||
Color::Red
|
Color::Red
|
||||||
} else if app.active_voices > 16 {
|
} else if app.metrics.active_voices > 16 {
|
||||||
Color::Yellow
|
Color::Yellow
|
||||||
} else {
|
} else {
|
||||||
Color::Cyan
|
Color::Cyan
|
||||||
@@ -185,12 +185,12 @@ fn render_stats(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
let voices = Paragraph::new(Line::from(vec![
|
let voices = Paragraph::new(Line::from(vec![
|
||||||
Span::raw("Active: "),
|
Span::raw("Active: "),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("{:3}", app.active_voices),
|
format!("{:3}", app.metrics.active_voices),
|
||||||
Style::new().fg(voice_color).add_modifier(Modifier::BOLD),
|
Style::new().fg(voice_color).add_modifier(Modifier::BOLD),
|
||||||
),
|
),
|
||||||
Span::raw(" Peak: "),
|
Span::raw(" Peak: "),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("{:3}", app.peak_voices),
|
format!("{:3}", app.metrics.peak_voices),
|
||||||
Style::new().fg(Color::Yellow),
|
Style::new().fg(Color::Yellow),
|
||||||
),
|
),
|
||||||
]));
|
]));
|
||||||
@@ -200,7 +200,7 @@ fn render_stats(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
let extra = Paragraph::new(vec![
|
let extra = Paragraph::new(vec![
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::raw("Schedule: "),
|
Span::raw("Schedule: "),
|
||||||
Span::styled(format!("{}", app.schedule_depth), Style::new().fg(Color::White)),
|
Span::styled(format!("{}", app.metrics.schedule_depth), Style::new().fg(Color::White)),
|
||||||
]),
|
]),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::raw("Pool: "),
|
Span::raw("Pool: "),
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ pub fn render(frame: &mut Frame, app: &mut App, area: Rect) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_sequencer(frame: &mut Frame, app: &App, area: Rect) {
|
fn render_sequencer(frame: &mut Frame, app: &App, area: Rect) {
|
||||||
let focus_indicator = if app.focus == Focus::Sequencer {
|
let focus_indicator = if app.editor_ctx.focus == Focus::Sequencer {
|
||||||
"*"
|
"*"
|
||||||
} else {
|
} else {
|
||||||
" "
|
" "
|
||||||
};
|
};
|
||||||
|
|
||||||
let border_style = if app.focus == Focus::Sequencer {
|
let border_style = if app.editor_ctx.focus == Focus::Sequencer {
|
||||||
Style::new().fg(Color::Rgb(100, 160, 180))
|
Style::new().fg(Color::Rgb(100, 160, 180))
|
||||||
} else {
|
} else {
|
||||||
Style::new().fg(Color::Rgb(70, 75, 85))
|
Style::new().fg(Color::Rgb(70, 75, 85))
|
||||||
@@ -106,15 +106,14 @@ fn render_tile(frame: &mut Frame, area: Rect, app: &App, step_idx: usize) {
|
|||||||
let pattern = app.current_edit_pattern();
|
let pattern = app.current_edit_pattern();
|
||||||
let step = pattern.step(step_idx);
|
let step = pattern.step(step_idx);
|
||||||
let is_active = step.map(|s| s.active).unwrap_or(false);
|
let is_active = step.map(|s| s.active).unwrap_or(false);
|
||||||
let is_selected = step_idx == app.current_step;
|
let is_selected = step_idx == app.editor_ctx.step;
|
||||||
|
|
||||||
// Check if any slot is playing this step on the current edit pattern
|
|
||||||
let playing_slot = if app.playing {
|
let playing_slot = if app.playing {
|
||||||
(0..8).find(|&i| {
|
(0..8).find(|&i| {
|
||||||
let (slot_active, bank, pat) = app.slot_data[i];
|
let s = app.slot_data[i];
|
||||||
slot_active
|
s.active
|
||||||
&& bank == app.edit_bank
|
&& s.bank == app.editor_ctx.bank
|
||||||
&& pat == app.edit_pattern
|
&& s.pattern == app.editor_ctx.pattern
|
||||||
&& app.slot_steps[i] == step_idx
|
&& app.slot_steps[i] == step_idx
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -146,7 +145,7 @@ fn render_tile(frame: &mut Frame, area: Rect, app: &App, step_idx: usize) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_editor(frame: &mut Frame, app: &mut App, area: Rect) {
|
fn render_editor(frame: &mut Frame, app: &mut App, area: Rect) {
|
||||||
let focus_indicator = if app.focus == Focus::Editor {
|
let focus_indicator = if app.editor_ctx.focus == Focus::Editor {
|
||||||
"*"
|
"*"
|
||||||
} else {
|
} else {
|
||||||
" "
|
" "
|
||||||
@@ -154,13 +153,13 @@ fn render_editor(frame: &mut Frame, app: &mut App, area: Rect) {
|
|||||||
|
|
||||||
let border_style = if app.is_flashing() {
|
let border_style = if app.is_flashing() {
|
||||||
Style::new().fg(Color::Green)
|
Style::new().fg(Color::Green)
|
||||||
} else if app.focus == Focus::Editor {
|
} else if app.editor_ctx.focus == Focus::Editor {
|
||||||
Style::new().fg(Color::Rgb(100, 160, 180))
|
Style::new().fg(Color::Rgb(100, 160, 180))
|
||||||
} else {
|
} else {
|
||||||
Style::new().fg(Color::Rgb(70, 75, 85))
|
Style::new().fg(Color::Rgb(70, 75, 85))
|
||||||
};
|
};
|
||||||
|
|
||||||
let step_num = app.current_step + 1;
|
let step_num = app.editor_ctx.step + 1;
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(border_style)
|
.border_style(border_style)
|
||||||
@@ -169,14 +168,14 @@ fn render_editor(frame: &mut Frame, app: &mut App, area: Rect) {
|
|||||||
let inner = block.inner(area);
|
let inner = block.inner(area);
|
||||||
frame.render_widget(block, area);
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
let cursor_style = if app.focus == Focus::Editor {
|
let cursor_style = if app.editor_ctx.focus == Focus::Editor {
|
||||||
Style::new().bg(Color::White).fg(Color::Black)
|
Style::new().bg(Color::White).fg(Color::Black)
|
||||||
} else {
|
} else {
|
||||||
Style::default()
|
Style::default()
|
||||||
};
|
};
|
||||||
app.editor.set_cursor_style(cursor_style);
|
app.editor_ctx.text.set_cursor_style(cursor_style);
|
||||||
|
|
||||||
frame.render_widget(&app.editor, inner);
|
frame.render_widget(&app.editor_ctx.text, inner);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
|
fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
|
||||||
@@ -188,7 +187,7 @@ fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
let inner = block.inner(area);
|
let inner = block.inner(area);
|
||||||
frame.render_widget(block, area);
|
frame.render_widget(block, area);
|
||||||
|
|
||||||
let scope = Scope::new(&app.scope)
|
let scope = Scope::new(&app.metrics.scope)
|
||||||
.orientation(Orientation::Vertical)
|
.orientation(Orientation::Vertical)
|
||||||
.color(Color::Green);
|
.color(Color::Green);
|
||||||
frame.render_widget(scope, inner);
|
frame.render_widget(scope, inner);
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ fn render_banks(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
let banks_with_playback: Vec<usize> = app
|
let banks_with_playback: Vec<usize> = app
|
||||||
.slot_data
|
.slot_data
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(active, _, _)| *active)
|
.filter(|s| s.active)
|
||||||
.map(|(_, bank, _)| *bank)
|
.map(|s| s.bank)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let bank_names: Vec<Option<&str>> = app
|
let bank_names: Vec<Option<&str>> = app
|
||||||
@@ -44,7 +44,7 @@ fn render_banks(frame: &mut Frame, app: &App, area: Rect) {
|
|||||||
.map(|b| b.name.as_deref())
|
.map(|b| b.name.as_deref())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
render_grid(frame, inner, app.patterns_cursor, app.edit_bank, &banks_with_playback, &bank_names);
|
render_grid(frame, inner, app.patterns_cursor, app.editor_ctx.bank, &banks_with_playback, &bank_names);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_patterns(frame: &mut Frame, app: &App, area: Rect, bank: usize) {
|
fn render_patterns(frame: &mut Frame, app: &App, area: Rect, bank: usize) {
|
||||||
@@ -77,12 +77,12 @@ fn render_patterns(frame: &mut Frame, app: &App, area: Rect, bank: usize) {
|
|||||||
let playing_patterns: Vec<usize> = app
|
let playing_patterns: Vec<usize> = app
|
||||||
.slot_data
|
.slot_data
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(active, b, _)| *active && *b == bank)
|
.filter(|s| s.active && s.bank == bank)
|
||||||
.map(|(_, _, pattern)| *pattern)
|
.map(|s| s.pattern)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let edit_pattern = if app.edit_bank == bank {
|
let edit_pattern = if app.editor_ctx.bank == bank {
|
||||||
app.edit_pattern
|
app.editor_ctx.pattern
|
||||||
} else {
|
} else {
|
||||||
usize::MAX
|
usize::MAX
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,11 +34,6 @@ impl<'a> Scope<'a> {
|
|||||||
self.color = c;
|
self.color = c;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn gain(mut self, g: f32) -> Self {
|
|
||||||
self.gain = g;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for Scope<'_> {
|
impl Widget for Scope<'_> {
|
||||||
|
|||||||
49
src/lib.rs
49
src/lib.rs
@@ -32,7 +32,9 @@ use orbit::{EffectParams, Orbit};
|
|||||||
use sample::{FileSource, SampleEntry, SampleInfo, SamplePool, WebSampleSource};
|
use sample::{FileSource, SampleEntry, SampleInfo, SamplePool, WebSampleSource};
|
||||||
use schedule::Schedule;
|
use schedule::Schedule;
|
||||||
#[cfg(feature = "native")]
|
#[cfg(feature = "native")]
|
||||||
use telemetry::EngineMetrics;
|
use std::sync::Arc;
|
||||||
|
#[cfg(feature = "native")]
|
||||||
|
pub use telemetry::EngineMetrics;
|
||||||
use types::{DelayType, Source, BLOCK_SIZE, CHANNELS, MAX_ORBITS, MAX_VOICES};
|
use types::{DelayType, Source, BLOCK_SIZE, CHANNELS, MAX_ORBITS, MAX_VOICES};
|
||||||
use voice::{Voice, VoiceParams};
|
use voice::{Voice, VoiceParams};
|
||||||
|
|
||||||
@@ -55,7 +57,7 @@ pub struct Engine {
|
|||||||
pub effect_params: EffectParams,
|
pub effect_params: EffectParams,
|
||||||
// Telemetry (native only)
|
// Telemetry (native only)
|
||||||
#[cfg(feature = "native")]
|
#[cfg(feature = "native")]
|
||||||
pub metrics: EngineMetrics,
|
pub metrics: Arc<EngineMetrics>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
@@ -96,7 +98,48 @@ impl Engine {
|
|||||||
comb_damp: 0.1,
|
comb_damp: 0.1,
|
||||||
},
|
},
|
||||||
#[cfg(feature = "native")]
|
#[cfg(feature = "native")]
|
||||||
metrics: EngineMetrics::default(),
|
metrics: Arc::new(EngineMetrics::default()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "native")]
|
||||||
|
pub fn new_with_metrics(
|
||||||
|
sample_rate: f32,
|
||||||
|
output_channels: usize,
|
||||||
|
metrics: Arc<EngineMetrics>,
|
||||||
|
) -> Self {
|
||||||
|
let mut orbits = Vec::with_capacity(MAX_ORBITS);
|
||||||
|
for _ in 0..MAX_ORBITS {
|
||||||
|
orbits.push(Orbit::new(sample_rate));
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
sr: sample_rate,
|
||||||
|
isr: 1.0 / sample_rate,
|
||||||
|
voices: vec![Voice::default(); MAX_VOICES],
|
||||||
|
active_voices: 0,
|
||||||
|
orbits,
|
||||||
|
schedule: Schedule::new(),
|
||||||
|
time: 0.0,
|
||||||
|
tick: 0,
|
||||||
|
output_channels,
|
||||||
|
output: vec![0.0; BLOCK_SIZE * output_channels],
|
||||||
|
sample_pool: SamplePool::new(),
|
||||||
|
samples: Vec::with_capacity(256),
|
||||||
|
sample_index: Vec::new(),
|
||||||
|
effect_params: EffectParams {
|
||||||
|
delay_time: 0.333,
|
||||||
|
delay_feedback: 0.6,
|
||||||
|
delay_type: DelayType::Standard,
|
||||||
|
verb_decay: 0.75,
|
||||||
|
verb_damp: 0.95,
|
||||||
|
verb_predelay: 0.1,
|
||||||
|
verb_diff: 0.7,
|
||||||
|
comb_freq: 220.0,
|
||||||
|
comb_feedback: 0.9,
|
||||||
|
comb_damp: 0.1,
|
||||||
|
},
|
||||||
|
metrics,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user