This commit is contained in:
2026-01-20 02:11:51 +01:00
parent 4391995eae
commit ce0014020f
16 changed files with 941 additions and 595 deletions

1
Cargo.lock generated
View File

@@ -2091,6 +2091,7 @@ dependencies = [
"arboard",
"clap",
"cpal",
"crossbeam-channel",
"crossterm",
"doux",
"minimad",

View File

@@ -21,3 +21,4 @@ serde_json = "1"
tui-textarea = "0.7"
arboard = "3"
minimad = "0.13"
crossbeam-channel = "0.5"

View File

@@ -1,19 +1,26 @@
use doux::audio::AudioDeviceInfo;
use rand::rngs::StdRng;
use rand::SeedableRng;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::Instant;
use tui_textarea::TextArea;
use crate::audio::{SlotChange, MAX_SLOTS};
use crate::config::{MAX_BANKS, MAX_PATTERNS, MAX_SLOTS};
use crate::file;
use crate::link::LinkState;
use crate::model::{Pattern, Project};
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)]
pub enum Focus {
@@ -81,6 +88,209 @@ pub enum AudioFocus {
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 tempo: f64,
pub beat: f64,
@@ -91,49 +301,39 @@ pub struct App {
pub quantum: f64,
pub project: Project,
pub focus: Focus,
pub page: Page,
pub current_step: usize,
pub edit_bank: usize,
pub edit_pattern: usize,
pub editor_ctx: EditorContext,
pub patterns_view_level: PatternsViewLevel,
pub patterns_cursor: usize,
// Slot playback state (synced from audio thread)
pub slot_data: [(bool, usize, usize); MAX_SLOTS], // (active, bank, pattern)
pub slot_data: [SlotState; MAX_SLOTS],
pub slot_steps: [usize; MAX_SLOTS],
pub queued_changes: Vec<SlotChange>,
pub event_count: usize,
pub active_voices: usize,
pub peak_voices: usize,
pub cpu_load: f32,
pub schedule_depth: usize,
pub metrics: Metrics,
pub sample_pool_mb: f32,
pub scope: [f32; 64],
pub script_engine: ScriptEngine,
pub variables: Variables,
pub rng: Rng,
pub file_path: Option<PathBuf>,
pub status_message: Option<String>,
pub editor: TextArea<'static>,
pub flash_until: Option<Instant>,
pub modal: Modal,
pub clipboard: Option<arboard::Clipboard>,
pub doc_topic: usize,
pub doc_scroll: usize,
pub audio_config: AudioConfig,
pub audio_focus: AudioFocus,
pub available_output_devices: Vec<AudioDeviceInfo>,
pub available_input_devices: Vec<AudioDeviceInfo>,
pub restart_pending: bool,
pub audio: AudioSettings,
pub dirty_patterns: HashSet<(usize, usize)>,
}
impl App {
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 {
tempo,
beat: 0.0,
@@ -143,44 +343,39 @@ impl App {
quantum,
project: Project::default(),
focus: Focus::Sequencer,
page: Page::default(),
current_step: 0,
edit_bank: 0,
edit_pattern: 0,
editor_ctx: EditorContext::default(),
patterns_view_level: PatternsViewLevel::default(),
patterns_cursor: 0,
slot_data: [(false, 0, 0); MAX_SLOTS],
slot_data: [SlotState::default(); MAX_SLOTS],
slot_steps: [0; MAX_SLOTS],
queued_changes: Vec::new(),
event_count: 0,
active_voices: 0,
peak_voices: 0,
cpu_load: 0.0,
schedule_depth: 0,
metrics: Metrics::default(),
sample_pool_mb: 0.0,
scope: [0.0; 64],
script_engine: ScriptEngine::new(),
variables: Arc::new(Mutex::new(HashMap::new())),
rng: Arc::new(Mutex::new(StdRng::seed_from_u64(0))),
variables,
rng,
script_engine,
file_path: None,
status_message: None,
editor: TextArea::default(),
flash_until: None,
modal: Modal::None,
clipboard: arboard::Clipboard::new().ok(),
doc_topic: 0,
doc_scroll: 0,
audio_config: AudioConfig::default(),
audio_focus: AudioFocus::default(),
available_output_devices: doux::audio::list_output_devices(),
available_input_devices: doux::audio::list_input_devices(),
restart_pending: false,
audio: AudioSettings::default(),
dirty_patterns: HashSet::new(),
}
}
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) {
match self.focus {
match self.editor_ctx.focus {
Focus::Sequencer => {
self.focus = Focus::Editor;
self.editor_ctx.focus = Focus::Editor;
self.load_step_to_editor();
}
Focus::Editor => {
self.save_editor_to_step();
self.compile_current_step();
self.focus = Focus::Sequencer;
self.editor_ctx.focus = Focus::Sequencer;
}
}
}
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) {
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();
}
pub fn prev_step(&mut self) {
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();
}
@@ -246,10 +441,10 @@ impl App {
};
let steps_per_row = len.div_ceil(num_rows);
if self.current_step >= steps_per_row {
self.current_step -= steps_per_row;
if self.editor_ctx.step >= steps_per_row {
self.editor_ctx.step -= steps_per_row;
} 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();
}
@@ -264,75 +459,81 @@ impl App {
};
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();
}
pub fn toggle_step(&mut self) {
let step_idx = self.current_step;
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
let step_idx = self.editor_ctx.step;
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) {
step.active = !step.active;
}
self.dirty_patterns.insert((bank, pattern));
}
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;
self.project
.pattern_at_mut(bank, pattern)
.set_length(current_len + 1);
self.dirty_patterns.insert((bank, pattern));
}
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;
self.project
.pattern_at_mut(bank, pattern)
.set_length(current_len.saturating_sub(1));
let new_len = self.project.pattern_at(bank, pattern).length;
if self.current_step >= new_len {
self.current_step = new_len - 1;
if self.editor_ctx.step >= new_len {
self.editor_ctx.step = new_len - 1;
self.load_step_to_editor();
}
self.dirty_patterns.insert((bank, pattern));
}
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);
pat.speed = pat.speed.next();
self.dirty_patterns.insert((bank, pattern));
}
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);
pat.speed = pat.speed.prev();
self.dirty_patterns.insert((bank, pattern));
}
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) {
let lines: Vec<String> = if step.script.is_empty() {
vec![String::new()]
} else {
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) {
let text = self.editor.lines().join("\n");
let step_idx = self.current_step;
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
let text = self.editor_ctx.text.lines().join("\n");
let step_idx = self.editor_ctx.step;
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) {
step.script = text;
}
self.dirty_patterns.insert((bank, pattern));
}
pub fn compile_current_step(&mut self) {
let step_idx = self.current_step;
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
let step_idx = self.editor_ctx.step;
let (bank, pattern) = (self.editor_ctx.bank, self.editor_ctx.pattern);
let script = self
.project
@@ -358,7 +559,7 @@ impl App {
slot: 0,
};
match self.script_engine.evaluate(&script, &ctx, &self.variables, &self.rng) {
match self.script_engine.evaluate(&script, &ctx) {
Ok(cmd) => {
if let Some(step) = self.project.pattern_at_mut(bank, pattern).step_mut(step_idx) {
step.command = Some(cmd);
@@ -377,7 +578,7 @@ impl App {
pub fn compile_all_steps(&mut self) {
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 {
let script = self
@@ -404,7 +605,7 @@ impl App {
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) {
step.command = Some(cmd);
}
@@ -418,8 +619,8 @@ impl App {
Some(true)
}
SlotChange::Remove { slot } => {
let (active, b, p) = self.slot_data[*slot];
if active && b == bank && p == pattern {
let s = self.slot_data[*slot];
if s.active && s.bank == bank && s.pattern == pattern {
Some(false)
} else {
None
@@ -430,8 +631,8 @@ impl App {
}
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))| {
if *active && *b == bank && *p == pattern {
let playing_slot = self.slot_data.iter().enumerate().find_map(|(i, s)| {
if s.active && s.bank == bank && s.pattern == pattern {
Some(i)
} else {
None
@@ -441,8 +642,8 @@ impl App {
let pending = self.queued_changes.iter().position(|c| match c {
SlotChange::Add { bank: b, pattern: p, .. } => *b == bank && *p == pattern,
SlotChange::Remove { slot } => {
let (_, b, p) = self.slot_data[*slot];
b == bank && p == pattern
let s = self.slot_data[*slot];
s.bank == bank && s.pattern == pattern
}
});
@@ -453,7 +654,7 @@ impl App {
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));
} 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 {
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));
@@ -464,15 +665,15 @@ impl App {
}
pub fn select_edit_pattern(&mut self, pattern: usize) {
self.edit_pattern = pattern;
self.current_step = 0;
self.editor_ctx.pattern = pattern;
self.editor_ctx.step = 0;
self.load_step_to_editor();
}
pub fn select_edit_bank(&mut self, bank: usize) {
self.edit_bank = bank;
self.edit_pattern = 0;
self.current_step = 0;
self.editor_ctx.bank = bank;
self.editor_ctx.pattern = 0;
self.editor_ctx.step = 0;
self.load_step_to_editor();
}
@@ -493,9 +694,10 @@ impl App {
match file::load(&path) {
Ok(project) => {
self.project = project;
self.current_step = 0;
self.editor_ctx.step = 0;
self.load_step_to_editor();
self.compile_all_steps();
self.mark_all_patterns_dirty();
self.status_message = Some(format!("Loaded: {}", path.display()));
self.file_path = Some(path);
}
@@ -516,7 +718,7 @@ impl App {
}
pub fn copy_step(&mut self) {
let step_idx = self.current_step;
let step_idx = self.editor_ctx.step;
let script = self
.current_edit_pattern()
.step(step_idx)
@@ -538,11 +740,12 @@ impl App {
.and_then(|clip| clip.get_text().ok());
if let Some(text) = text {
let step_idx = self.current_step;
let (bank, pattern) = (self.edit_bank, self.edit_pattern);
let step_idx = self.editor_ctx.step;
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) {
step.script = text;
}
self.dirty_patterns.insert((bank, pattern));
self.load_step_to_editor();
self.compile_current_step();
}
@@ -556,142 +759,4 @@ impl App {
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();
}
}

View File

@@ -1,12 +1,34 @@
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::Stream;
use doux::Engine;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use crossbeam_channel::Receiver;
use doux::{Engine, EngineMetrics};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use crate::link::LinkState;
use crate::model::Project;
use crate::script::{Rng, ScriptEngine, StepContext, Variables};
use crate::sequencer::AudioCommand;
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 output_device: Option<String>,
@@ -14,8 +36,6 @@ pub struct AudioStreamConfig {
pub buffer_size: u32,
}
pub const MAX_SLOTS: usize = 8;
#[derive(Clone, Copy, Default)]
pub struct PatternSlot {
pub bank: usize,
@@ -24,38 +44,12 @@ pub struct PatternSlot {
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(
config: &AudioStreamConfig,
engine: Arc<Mutex<Engine>>,
link: Arc<LinkState>,
playing: Arc<AtomicBool>,
project: Arc<Mutex<Project>>,
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,
audio_rx: Receiver<AudioCommand>,
scope_buffer: Arc<ScopeBuffer>,
metrics: Arc<EngineMetrics>,
initial_samples: Vec<doux::sample::SampleEntry>,
) -> Result<(Stream, f32), String> {
let host = cpal::default_host();
@@ -80,12 +74,13 @@ pub fn build_stream(
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 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
.build_output_stream(
&stream_config,
@@ -93,97 +88,31 @@ pub fn build_stream(
let buffer_samples = data.len() / channels;
let buffer_time_ns = (buffer_samples as f64 / sr as f64 * 1e9) as u64;
let is_playing = playing.load(Ordering::Relaxed);
if is_playing {
let state = link.capture_audio_state();
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,
};
while let Ok(cmd) = audio_rx.try_recv() {
match cmd {
AudioCommand::Evaluate(s) => {
engine.evaluate(&s);
}
SlotChange::Remove { slot } => {
audio.slots[slot].active = false;
AudioCommand::Hush => {
engine.hush();
}
AudioCommand::Panic => {
engine.panic();
}
AudioCommand::LoadSamples(samples) => {
engine.sample_index.extend(samples);
}
}
// Read prev_beat before the mutable borrow of slots
let prev_beat = audio.prev_beat;
// 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);
AudioCommand::ResetEngine => {
let old_samples = std::mem::take(&mut engine.sample_index);
engine = Engine::new_with_metrics(sr, channels, Arc::clone(&metrics_clone));
engine.sample_index = old_samples;
}
}
}
slot.step_index = (slot.step_index + 1) % pattern.length;
}
}
// 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, &[], &[]);
engine.metrics.load.set_buffer_time(buffer_time_ns);
engine.process_block(data, &[], &[]);
scope_buffer.write(&engine.output);
},
|err| eprintln!("stream error: {err}"),
None,

7
seq/src/config.rs Normal file
View 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;

View File

@@ -38,9 +38,9 @@ impl LinkState {
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();
self.link.capture_audio_session_state(&mut state);
self.link.capture_app_session_state(&mut state);
state
}
}

View File

@@ -1,18 +1,20 @@
mod app;
mod audio;
mod config;
mod file;
mod link;
mod model;
mod page;
mod script;
mod sequencer;
mod ui;
mod views;
mod widgets;
use std::io;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use clap::Parser;
@@ -21,15 +23,15 @@ use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use crossterm::ExecutableCommand;
use doux::Engine;
use doux::EngineMetrics;
use ratatui::prelude::CrosstermBackend;
use ratatui::Terminal;
use app::{App, AudioFocus, Focus, Modal, PatternField};
use audio::{AudioStreamConfig, SlotChange, MAX_SLOTS};
use audio::{AudioStreamConfig, ScopeBuffer};
use link::LinkState;
use model::Project;
use page::Page;
use sequencer::{spawn_sequencer, AudioCommand, PatternSnapshot, SeqCommand, StepSnapshot};
#[derive(Parser)]
#[command(name = "seq", about = "A step sequencer with Ableton Link support")]
@@ -55,72 +57,59 @@ struct Args {
buffer: u32,
}
const TEMPO: f64 = 120.0;
const QUANTUM: f64 = 4.0;
use config::{DEFAULT_QUANTUM, DEFAULT_TEMPO};
fn main() -> io::Result<()> {
let args = Args::parse();
let link = Arc::new(LinkState::new(TEMPO, QUANTUM));
let link = Arc::new(LinkState::new(DEFAULT_TEMPO, DEFAULT_QUANTUM));
link.enable();
let playing = Arc::new(AtomicBool::new(true));
let event_count = Arc::new(AtomicUsize::new(0));
// Slot state shared between audio thread and UI
let slot_steps: [Arc<AtomicUsize>; MAX_SLOTS] = std::array::from_fn(|_| Arc::new(AtomicUsize::new(0)));
let slot_data: Arc<Mutex<[(bool, usize, usize); MAX_SLOTS]>> =
Arc::new(Mutex::new([(false, 0, 0); MAX_SLOTS]));
let slot_changes: Arc<Mutex<Vec<SlotChange>>> = Arc::new(Mutex::new(Vec::new()));
let mut app = App::new(DEFAULT_TEMPO, DEFAULT_QUANTUM);
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
app.audio_config.output_device = args.output;
app.audio_config.input_device = args.input;
app.audio_config.channels = args.channels;
app.audio_config.buffer_size = args.buffer;
app.audio_config.sample_paths = args.samples;
let metrics = Arc::new(EngineMetrics::default());
let scope_buffer = Arc::new(ScopeBuffer::new());
let engine = Arc::new(Mutex::new(Engine::new(44100.0)));
let project = Arc::new(Mutex::new(Project::default()));
// Load sample directories
for path in &app.audio_config.sample_paths {
let mut initial_samples = Vec::new();
for path in &app.audio.config.sample_paths {
let index = doux::loader::scan_samples_dir(path);
let count = index.len();
engine.lock().unwrap().sample_index.extend(index);
app.audio_config.sample_count += count;
app.audio.config.sample_count += index.len();
initial_samples.extend(index);
}
let sequencer = spawn_sequencer(
Arc::clone(&link),
Arc::clone(&playing),
Arc::clone(&app.variables),
Arc::clone(&app.rng),
DEFAULT_QUANTUM,
);
let stream_config = AudioStreamConfig {
output_device: app.audio_config.output_device.clone(),
channels: app.audio_config.channels,
buffer_size: app.audio_config.buffer_size,
output_device: app.audio.config.output_device.clone(),
channels: app.audio.config.channels,
buffer_size: app.audio.config.buffer_size,
};
let (mut stream, sample_rate) = audio::build_stream(
&stream_config,
Arc::clone(&engine),
Arc::clone(&link),
Arc::clone(&playing),
Arc::clone(&project),
slot_steps.clone(),
Arc::clone(&event_count),
Arc::clone(&slot_data),
Arc::clone(&slot_changes),
Arc::clone(&app.variables),
Arc::clone(&app.rng),
sequencer.audio_rx.clone(),
Arc::clone(&scope_buffer),
Arc::clone(&metrics),
initial_samples,
)
.expect("Failed to start audio");
app.audio_config.sample_rate = sample_rate;
{
let mut eng = engine.lock().unwrap();
eng.sr = sample_rate;
eng.isr = 1.0 / sample_rate;
}
app.audio.config.sample_rate = sample_rate;
app.mark_all_patterns_dirty();
enable_raw_mode()?;
io::stdout().execute(EnterAlternateScreen)?;
@@ -129,62 +118,52 @@ fn main() -> io::Result<()> {
let mut terminal = Terminal::new(backend)?;
loop {
if app.restart_pending {
app.restart_pending = false;
if app.audio.restart_pending {
app.audio.restart_pending = false;
drop(stream);
let new_config = AudioStreamConfig {
output_device: app.audio_config.output_device.clone(),
channels: app.audio_config.channels,
buffer_size: app.audio_config.buffer_size,
output_device: app.audio.config.output_device.clone(),
channels: app.audio.config.channels,
buffer_size: app.audio.config.buffer_size,
};
{
let mut eng = engine.lock().unwrap();
*eng = Engine::new_with_channels(eng.sr, new_config.channels as usize);
let mut restart_samples = Vec::new();
for path in &app.audio.config.sample_paths {
let index = doux::loader::scan_samples_dir(path);
restart_samples.extend(index);
}
app.audio.config.sample_count = restart_samples.len();
match audio::build_stream(
&new_config,
Arc::clone(&engine),
Arc::clone(&link),
Arc::clone(&playing),
Arc::clone(&project),
slot_steps.clone(),
Arc::clone(&event_count),
Arc::clone(&slot_data),
Arc::clone(&slot_changes),
Arc::clone(&app.variables),
Arc::clone(&app.rng),
sequencer.audio_rx.clone(),
Arc::clone(&scope_buffer),
Arc::clone(&metrics),
restart_samples,
) {
Ok((new_stream, sr)) => {
stream = new_stream;
app.audio_config.sample_rate = sr;
{
let mut eng = engine.lock().unwrap();
eng.sr = sr;
eng.isr = 1.0 / sr;
}
app.audio.config.sample_rate = sr;
app.status_message = Some("Audio restarted".to_string());
}
Err(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(
&AudioStreamConfig {
output_device: None,
channels: 2,
buffer_size: 512,
},
Arc::clone(&engine),
Arc::clone(&link),
Arc::clone(&playing),
Arc::clone(&project),
slot_steps.clone(),
Arc::clone(&event_count),
Arc::clone(&slot_data),
Arc::clone(&slot_changes),
Arc::clone(&app.variables),
Arc::clone(&app.rng),
sequencer.audio_rx.clone(),
Arc::clone(&scope_buffer),
Arc::clone(&metrics),
fallback_samples,
)
.expect("Failed to restart with defaults");
stream = fallback_stream;
@@ -194,37 +173,42 @@ fn main() -> io::Result<()> {
app.update_from_link(&link);
app.playing = playing.load(Ordering::Relaxed);
app.event_count = event_count.load(Ordering::Relaxed);
{
let eng = engine.lock().unwrap();
app.active_voices = eng.active_voices;
app.peak_voices = app.peak_voices.max(eng.active_voices);
app.cpu_load = eng.metrics.load.get_load();
app.schedule_depth = eng.schedule.len();
for (i, s) in app.scope.iter_mut().enumerate() {
*s = eng.output.get(i * 2).copied().unwrap_or(0.0);
app.metrics.active_voices = metrics.active_voices.load(Ordering::Relaxed) as usize;
app.metrics.peak_voices = app.metrics.peak_voices.max(app.metrics.active_voices);
app.metrics.cpu_load = metrics.load.get_load();
app.metrics.schedule_depth = metrics.schedule_depth.load(Ordering::Relaxed) as usize;
app.metrics.scope = scope_buffer.read();
}
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
{
let sd = slot_data.lock().unwrap();
app.slot_data = *sd;
}
for (i, step_atomic) in slot_steps.iter().enumerate() {
app.slot_steps[i] = step_atomic.load(Ordering::Relaxed);
}
// Push queued changes to audio thread
if !app.queued_changes.is_empty() {
let mut changes = slot_changes.lock().unwrap();
changes.extend(app.queued_changes.drain(..));
}
{
let mut proj = project.lock().unwrap();
proj.banks = app.project.banks.clone();
for (bank, pattern) in app.dirty_patterns.drain() {
let pat = app.project.pattern_at(bank, pattern);
let snapshot = PatternSnapshot {
speed: pat.speed,
length: pat.length,
steps: pat.steps.iter().take(pat.length).map(|s| StepSnapshot {
active: s.active,
script: s.script.clone(),
}).collect(),
};
let _ = sequencer.cmd_tx.send(SeqCommand::PatternUpdate { bank, pattern, data: snapshot });
}
terminal.draw(|frame| ui::render(frame, &mut app))?;
@@ -324,15 +308,16 @@ fn main() -> io::Result<()> {
Modal::SetPattern { field, input } => match key.code {
KeyCode::Enter => {
let field = *field;
let (bank, pattern) = (app.edit_bank, app.edit_pattern);
let (bank, pattern) = (app.editor_ctx.bank, app.editor_ctx.pattern);
match field {
PatternField::Length => {
if let Ok(len) = input.parse::<usize>() {
app.project.pattern_at_mut(bank, pattern).set_length(len);
let new_len = app.project.pattern_at(bank, pattern).length;
if app.current_step >= new_len {
app.current_step = new_len - 1;
if app.editor_ctx.step >= new_len {
app.editor_ctx.step = new_len - 1;
}
app.dirty_patterns.insert((bank, pattern));
app.status_message = Some(format!("Length set to {new_len}"));
} else {
app.status_message = Some("Invalid length".to_string());
@@ -341,6 +326,7 @@ fn main() -> io::Result<()> {
PatternField::Speed => {
if let Some(speed) = model::PatternSpeed::from_label(input) {
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()));
} else {
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() {
let index = doux::loader::scan_samples_dir(&sample_path);
let count = index.len();
engine.lock().unwrap().sample_index.extend(index);
app.audio_config.sample_count += count;
app.add_sample_path(sample_path);
let _ = sequencer.audio_tx.send(AudioCommand::LoadSamples(index));
app.audio.config.sample_count += count;
app.audio.add_sample_path(sample_path);
app.status_message = Some(format!("Added {count} samples"));
} else {
app.status_message = Some("Path is not a directory".to_string());
@@ -407,7 +393,7 @@ fn main() -> io::Result<()> {
}
match app.page {
Page::Main => match app.focus {
Page::Main => match app.editor_ctx.focus {
Focus::Sequencer => match key.code {
KeyCode::Char('q') => {
app.modal = Modal::ConfirmQuit { selected: false };
@@ -456,7 +442,7 @@ fn main() -> io::Result<()> {
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 };
}
KeyCode::Up | KeyCode::Char('k') => {
app.prev_audio_focus();
app.audio.prev_focus();
}
KeyCode::Down | KeyCode::Char('j') => {
app.next_audio_focus();
app.audio.next_focus();
}
KeyCode::Left => match app.audio_focus {
AudioFocus::OutputDevice => app.prev_output_device(),
AudioFocus::InputDevice => app.prev_input_device(),
AudioFocus::Channels => app.adjust_channels(-1),
AudioFocus::BufferSize => app.adjust_buffer_size(-64),
AudioFocus::SamplePaths => app.remove_last_sample_path(),
KeyCode::Left => match app.audio.focus {
AudioFocus::OutputDevice => app.audio.prev_output_device(),
AudioFocus::InputDevice => app.audio.prev_input_device(),
AudioFocus::Channels => app.audio.adjust_channels(-1),
AudioFocus::BufferSize => app.audio.adjust_buffer_size(-64),
AudioFocus::SamplePaths => app.audio.remove_last_sample_path(),
},
KeyCode::Right => match app.audio_focus {
AudioFocus::OutputDevice => app.next_output_device(),
AudioFocus::InputDevice => app.next_input_device(),
AudioFocus::Channels => app.adjust_channels(1),
AudioFocus::BufferSize => app.adjust_buffer_size(64),
KeyCode::Right => match app.audio.focus {
AudioFocus::OutputDevice => app.audio.next_output_device(),
AudioFocus::InputDevice => app.audio.next_input_device(),
AudioFocus::Channels => app.audio.adjust_channels(1),
AudioFocus::BufferSize => app.audio.adjust_buffer_size(64),
AudioFocus::SamplePaths => {}
},
KeyCode::Char('R') => {
app.trigger_restart();
// Reload samples on restart
let mut eng = engine.lock().unwrap();
eng.sample_index.clear();
app.audio_config.sample_count = 0;
for path in &app.audio_config.sample_paths {
let index = doux::loader::scan_samples_dir(path);
app.audio_config.sample_count += index.len();
eng.sample_index.extend(index);
}
app.audio.trigger_restart();
}
KeyCode::Char('A') => {
app.modal = Modal::AddSamplePath(String::new());
}
KeyCode::Char('D') => {
app.refresh_audio_devices();
let out_count = app.available_output_devices.len();
let in_count = app.available_input_devices.len();
app.audio.refresh_devices();
let out_count = app.audio.output_devices.len();
let in_count = app.audio.input_devices.len();
app.status_message = Some(format!("Found {out_count} output, {in_count} input devices"));
}
KeyCode::Char('h') => {
engine.lock().unwrap().hush();
let _ = sequencer.audio_tx.send(AudioCommand::Hush);
}
KeyCode::Char('p') => {
engine.lock().unwrap().panic();
let _ = sequencer.audio_tx.send(AudioCommand::Panic);
}
KeyCode::Char('r') => {
app.peak_voices = 0;
app.metrics.peak_voices = 0;
}
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(' ') => {
app.toggle_playing();
@@ -628,5 +605,7 @@ fn main() -> io::Result<()> {
disable_raw_mode()?;
io::stdout().execute(LeaveAlternateScreen)?;
sequencer.shutdown();
Ok(())
}

View File

@@ -1,5 +1,7 @@
use serde::{Deserialize, Serialize};
use crate::config::{DEFAULT_LENGTH, MAX_BANKS, MAX_PATTERNS, MAX_STEPS};
#[derive(Clone, Copy, Serialize, Deserialize, Default, PartialEq)]
pub enum PatternSpeed {
Eighth, // 1/8x
@@ -106,8 +108,8 @@ pub struct Pattern {
impl Default for Pattern {
fn default() -> Self {
Self {
steps: (0..32).map(|_| Step::default()).collect(),
length: 16,
steps: (0..MAX_STEPS).map(|_| Step::default()).collect(),
length: DEFAULT_LENGTH,
speed: PatternSpeed::default(),
name: None,
}
@@ -124,7 +126,7 @@ impl Pattern {
}
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 {
self.steps.push(Step::default());
}
@@ -142,7 +144,7 @@ pub struct Bank {
impl Default for Bank {
fn default() -> Self {
Self {
patterns: (0..16).map(|_| Pattern::default()).collect(),
patterns: (0..MAX_PATTERNS).map(|_| Pattern::default()).collect(),
name: None,
}
}
@@ -156,7 +158,7 @@ pub struct Project {
impl Default for Project {
fn default() -> Self {
Self {
banks: (0..16).map(|_| Bank::default()).collect(),
banks: (0..MAX_BANKS).map(|_| Bank::default()).collect(),
}
}
}

View File

@@ -62,40 +62,20 @@ pub struct StepContext {
pub slot: usize,
}
pub struct ScriptEngine;
pub struct ScriptEngine {
engine: Engine,
}
impl ScriptEngine {
pub fn new() -> 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);
pub fn new(vars: Variables, rng: Rng) -> Self {
let mut engine = Engine::new();
engine.set_max_expr_depths(64, 32);
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| {
vars_for_set
.lock()
@@ -112,11 +92,11 @@ impl ScriptEngine {
.unwrap_or(Dynamic::UNIT)
});
let rng_rand_ff = Arc::clone(rng);
let rng_rand_ii = Arc::clone(rng);
let rng_rrand_ff = Arc::clone(rng);
let rng_rrand_ii = Arc::clone(rng);
let rng_seed = Arc::clone(rng);
let rng_rand_ff = Arc::clone(&rng);
let rng_rand_ii = Arc::clone(&rng);
let rng_rrand_ff = Arc::clone(&rng);
let rng_rrand_ii = Arc::clone(&rng);
let rng_seed = Arc::clone(&rng);
engine.register_fn("rand", move |min: f64, max: f64| -> f64 {
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);
});
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());
}
engine
self.engine
.eval_with_scope::<String>(&mut scope, script)
.map_err(|e| e.to_string())
}

328
seq/src/sequencer.rs Normal file
View 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));
}
}

View File

@@ -40,7 +40,7 @@ fn render_header(frame: &mut Frame, app: &App, area: Rect) {
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 {
Color::Red
} else if cpu_pct > 50.0 {
@@ -52,7 +52,7 @@ fn render_header(frame: &mut Frame, app: &App, area: Rect) {
let left_spans = vec![
Span::styled("EDIT ", Style::new().fg(Color::Cyan)),
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),
),
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);
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![
Span::styled(format!("L:{:02}", pattern.length), Style::new().fg(Color::Rgb(180, 140, 90))),
Span::raw(" "),
@@ -71,7 +71,7 @@ fn render_header(frame: &mut Frame, app: &App, area: Rect) {
Span::raw(" "),
Span::styled(format!("CPU:{cpu_pct:.0}%"), Style::new().fg(cpu_color)),
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(

View File

@@ -48,8 +48,8 @@ fn render_config(frame: &mut Frame, app: &App, area: Rect) {
let normal = Style::new().fg(Color::White);
let dim = Style::new().fg(Color::DarkGray);
let output_name = truncate_name(app.current_output_device_name(), 25);
let output_style = if app.audio_focus == AudioFocus::OutputDevice {
let output_name = truncate_name(app.audio.current_output_device_name(), 25);
let output_style = if app.audio.focus == AudioFocus::OutputDevice {
highlight
} else {
normal
@@ -62,8 +62,8 @@ fn render_config(frame: &mut Frame, app: &App, area: Rect) {
]);
frame.render_widget(Paragraph::new(output_line), output_area);
let input_name = truncate_name(app.current_input_device_name(), 25);
let input_style = if app.audio_focus == AudioFocus::InputDevice {
let input_name = truncate_name(app.audio.current_input_device_name(), 25);
let input_style = if app.audio.focus == AudioFocus::InputDevice {
highlight
} else {
normal
@@ -76,7 +76,7 @@ fn render_config(frame: &mut Frame, app: &App, area: Rect) {
]);
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
} else {
normal
@@ -84,12 +84,12 @@ fn render_config(frame: &mut Frame, app: &App, area: Rect) {
let channels_line = Line::from(vec![
Span::styled("Channels ", dim),
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),
]);
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
} else {
normal
@@ -97,18 +97,18 @@ fn render_config(frame: &mut Frame, app: &App, area: Rect) {
let buffer_line = Line::from(vec![
Span::styled("Buffer ", dim),
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),
]);
frame.render_widget(Paragraph::new(buffer_line), buffer_area);
let rate_line = Line::from(vec![
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);
let samples_style = if app.audio_focus == AudioFocus::SamplePaths {
let samples_style = if app.audio.focus == AudioFocus::SamplePaths {
highlight
} else {
normal
@@ -117,12 +117,12 @@ fn render_config(frame: &mut Frame, app: &App, area: Rect) {
let mut sample_lines = vec![Line::from(vec![
Span::styled("Samples ", dim),
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,
),
])];
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 display = truncate_name(&path_str, 35);
sample_lines.push(Line::from(vec![
@@ -157,7 +157,7 @@ fn render_stats(frame: &mut Frame, app: &App, area: Rect) {
])
.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 {
Color::Red
} 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);
let voice_color = if app.active_voices > 24 {
let voice_color = if app.metrics.active_voices > 24 {
Color::Red
} else if app.active_voices > 16 {
} else if app.metrics.active_voices > 16 {
Color::Yellow
} else {
Color::Cyan
@@ -185,12 +185,12 @@ fn render_stats(frame: &mut Frame, app: &App, area: Rect) {
let voices = Paragraph::new(Line::from(vec![
Span::raw("Active: "),
Span::styled(
format!("{:3}", app.active_voices),
format!("{:3}", app.metrics.active_voices),
Style::new().fg(voice_color).add_modifier(Modifier::BOLD),
),
Span::raw(" Peak: "),
Span::styled(
format!("{:3}", app.peak_voices),
format!("{:3}", app.metrics.peak_voices),
Style::new().fg(Color::Yellow),
),
]));
@@ -200,7 +200,7 @@ fn render_stats(frame: &mut Frame, app: &App, area: Rect) {
let extra = Paragraph::new(vec![
Line::from(vec![
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![
Span::raw("Pool: "),

View File

@@ -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) {
let focus_indicator = if app.focus == Focus::Sequencer {
let focus_indicator = if app.editor_ctx.focus == Focus::Sequencer {
"*"
} 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))
} else {
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 step = pattern.step(step_idx);
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 {
(0..8).find(|&i| {
let (slot_active, bank, pat) = app.slot_data[i];
slot_active
&& bank == app.edit_bank
&& pat == app.edit_pattern
let s = app.slot_data[i];
s.active
&& s.bank == app.editor_ctx.bank
&& s.pattern == app.editor_ctx.pattern
&& app.slot_steps[i] == step_idx
})
} 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) {
let focus_indicator = if app.focus == Focus::Editor {
let focus_indicator = if app.editor_ctx.focus == Focus::Editor {
"*"
} else {
" "
@@ -154,13 +153,13 @@ fn render_editor(frame: &mut Frame, app: &mut App, area: Rect) {
let border_style = if app.is_flashing() {
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))
} else {
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()
.borders(Borders::ALL)
.border_style(border_style)
@@ -169,14 +168,14 @@ fn render_editor(frame: &mut Frame, app: &mut App, area: Rect) {
let inner = block.inner(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)
} else {
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) {
@@ -188,7 +187,7 @@ fn render_scope(frame: &mut Frame, app: &App, area: Rect) {
let inner = block.inner(area);
frame.render_widget(block, area);
let scope = Scope::new(&app.scope)
let scope = Scope::new(&app.metrics.scope)
.orientation(Orientation::Vertical)
.color(Color::Green);
frame.render_widget(scope, inner);

View File

@@ -33,8 +33,8 @@ fn render_banks(frame: &mut Frame, app: &App, area: Rect) {
let banks_with_playback: Vec<usize> = app
.slot_data
.iter()
.filter(|(active, _, _)| *active)
.map(|(_, bank, _)| *bank)
.filter(|s| s.active)
.map(|s| s.bank)
.collect();
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())
.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) {
@@ -77,12 +77,12 @@ fn render_patterns(frame: &mut Frame, app: &App, area: Rect, bank: usize) {
let playing_patterns: Vec<usize> = app
.slot_data
.iter()
.filter(|(active, b, _)| *active && *b == bank)
.map(|(_, _, pattern)| *pattern)
.filter(|s| s.active && s.bank == bank)
.map(|s| s.pattern)
.collect();
let edit_pattern = if app.edit_bank == bank {
app.edit_pattern
let edit_pattern = if app.editor_ctx.bank == bank {
app.editor_ctx.pattern
} else {
usize::MAX
};

View File

@@ -34,11 +34,6 @@ impl<'a> Scope<'a> {
self.color = c;
self
}
pub fn gain(mut self, g: f32) -> Self {
self.gain = g;
self
}
}
impl Widget for Scope<'_> {

View File

@@ -32,7 +32,9 @@ use orbit::{EffectParams, Orbit};
use sample::{FileSource, SampleEntry, SampleInfo, SamplePool, WebSampleSource};
use schedule::Schedule;
#[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 voice::{Voice, VoiceParams};
@@ -55,7 +57,7 @@ pub struct Engine {
pub effect_params: EffectParams,
// Telemetry (native only)
#[cfg(feature = "native")]
pub metrics: EngineMetrics,
pub metrics: Arc<EngineMetrics>,
}
impl Engine {
@@ -96,7 +98,48 @@ impl Engine {
comb_damp: 0.1,
},
#[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,
}
}