Looks better now
This commit is contained in:
@@ -129,8 +129,7 @@ impl SequencerSnapshot {
|
||||
|
||||
pub struct SequencerHandle {
|
||||
pub cmd_tx: Sender<SeqCommand>,
|
||||
pub audio_tx: Sender<AudioCommand>,
|
||||
pub audio_rx: Receiver<AudioCommand>,
|
||||
pub audio_tx: Arc<ArcSwap<Sender<AudioCommand>>>,
|
||||
shared_state: Arc<ArcSwap<SharedSequencerState>>,
|
||||
thread: JoinHandle<()>,
|
||||
}
|
||||
@@ -146,6 +145,12 @@ impl SequencerHandle {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn swap_audio_channel(&self) -> Receiver<AudioCommand> {
|
||||
let (new_tx, new_rx) = bounded::<AudioCommand>(256);
|
||||
self.audio_tx.store(Arc::new(new_tx));
|
||||
new_rx
|
||||
}
|
||||
|
||||
pub fn shutdown(self) {
|
||||
let _ = self.cmd_tx.send(SeqCommand::Shutdown);
|
||||
let _ = self.thread.join();
|
||||
@@ -186,20 +191,21 @@ pub fn spawn_sequencer(
|
||||
rng: Rng,
|
||||
quantum: f64,
|
||||
live_keys: Arc<LiveKeyState>,
|
||||
) -> SequencerHandle {
|
||||
) -> (SequencerHandle, Receiver<AudioCommand>) {
|
||||
let (cmd_tx, cmd_rx) = bounded::<SeqCommand>(64);
|
||||
let (audio_tx, audio_rx) = bounded::<AudioCommand>(256);
|
||||
let audio_tx = Arc::new(ArcSwap::from_pointee(audio_tx));
|
||||
|
||||
let shared_state = Arc::new(ArcSwap::from_pointee(SharedSequencerState::default()));
|
||||
let shared_state_clone = Arc::clone(&shared_state);
|
||||
let audio_tx_clone = audio_tx.clone();
|
||||
let audio_tx_for_thread = Arc::clone(&audio_tx);
|
||||
|
||||
let thread = thread::Builder::new()
|
||||
.name("sequencer".into())
|
||||
.spawn(move || {
|
||||
sequencer_loop(
|
||||
cmd_rx,
|
||||
audio_tx_clone,
|
||||
audio_tx_for_thread,
|
||||
link,
|
||||
playing,
|
||||
variables,
|
||||
@@ -212,13 +218,13 @@ pub fn spawn_sequencer(
|
||||
})
|
||||
.expect("Failed to spawn sequencer thread");
|
||||
|
||||
SequencerHandle {
|
||||
let handle = SequencerHandle {
|
||||
cmd_tx,
|
||||
audio_tx,
|
||||
audio_rx,
|
||||
shared_state,
|
||||
thread,
|
||||
}
|
||||
};
|
||||
(handle, audio_rx)
|
||||
}
|
||||
|
||||
struct PatternCache {
|
||||
@@ -294,7 +300,7 @@ impl RunsCounter {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn sequencer_loop(
|
||||
cmd_rx: Receiver<SeqCommand>,
|
||||
audio_tx: Sender<AudioCommand>,
|
||||
audio_tx: Arc<ArcSwap<Sender<AudioCommand>>>,
|
||||
link: Arc<LinkState>,
|
||||
playing: Arc<std::sync::atomic::AtomicBool>,
|
||||
variables: Variables,
|
||||
@@ -430,7 +436,7 @@ fn sequencer_loop(
|
||||
std::mem::take(&mut trace),
|
||||
);
|
||||
for cmd in cmds {
|
||||
match audio_tx.try_send(AudioCommand::Evaluate(cmd)) {
|
||||
match audio_tx.load().try_send(AudioCommand::Evaluate(cmd)) {
|
||||
Ok(()) => {
|
||||
event_count += 1;
|
||||
}
|
||||
@@ -438,7 +444,9 @@ fn sequencer_loop(
|
||||
dropped_events += 1;
|
||||
}
|
||||
Err(TrySendError::Disconnected(_)) => {
|
||||
return;
|
||||
// Channel disconnected means old stream is gone, but
|
||||
// a new one will be swapped in. Don't exit - just skip.
|
||||
dropped_events += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
src/input.rs
14
src/input.rs
@@ -1,3 +1,4 @@
|
||||
use arc_swap::ArcSwap;
|
||||
use crossbeam_channel::Sender;
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -21,7 +22,7 @@ pub struct InputContext<'a> {
|
||||
pub link: &'a LinkState,
|
||||
pub snapshot: &'a SequencerSnapshot,
|
||||
pub playing: &'a Arc<AtomicBool>,
|
||||
pub audio_tx: &'a Sender<AudioCommand>,
|
||||
pub audio_tx: &'a ArcSwap<Sender<AudioCommand>>,
|
||||
}
|
||||
|
||||
impl<'a> InputContext<'a> {
|
||||
@@ -362,7 +363,7 @@ fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
if let Some(path) = sample_path {
|
||||
let index = doux::loader::scan_samples_dir(&path);
|
||||
let count = index.len();
|
||||
let _ = ctx.audio_tx.send(AudioCommand::LoadSamples(index));
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::LoadSamples(index));
|
||||
ctx.app.audio.config.sample_count += count;
|
||||
ctx.app.audio.add_sample_path(path);
|
||||
ctx.dispatch(AppCommand::SetStatus(format!("Added {count} samples")));
|
||||
@@ -515,7 +516,7 @@ fn handle_panel_input(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
let idx = entry.index;
|
||||
let cmd =
|
||||
format!("/sound/{folder}/n/{idx}/gain/0.5/dur/1");
|
||||
let _ = ctx.audio_tx.send(AudioCommand::Evaluate(cmd));
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::Evaluate(cmd));
|
||||
}
|
||||
_ => state.toggle_expand(),
|
||||
}
|
||||
@@ -846,15 +847,16 @@ fn handle_engine_page(ctx: &mut InputContext, key: KeyEvent) -> InputResult {
|
||||
}
|
||||
}
|
||||
KeyCode::Char('h') => {
|
||||
let _ = ctx.audio_tx.send(AudioCommand::Hush);
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::Hush);
|
||||
}
|
||||
KeyCode::Char('p') => {
|
||||
let _ = ctx.audio_tx.send(AudioCommand::Panic);
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::Panic);
|
||||
}
|
||||
KeyCode::Char('r') => ctx.app.metrics.peak_voices = 0,
|
||||
KeyCode::Char('t') => {
|
||||
let _ = ctx
|
||||
.audio_tx
|
||||
.load()
|
||||
.send(AudioCommand::Evaluate("/sound/sine/dur/0.5/decay/0.2".into()));
|
||||
}
|
||||
_ => {}
|
||||
@@ -986,7 +988,7 @@ fn load_project_samples(ctx: &mut InputContext) {
|
||||
let index = doux::loader::scan_samples_dir(path);
|
||||
let count = index.len();
|
||||
total_count += count;
|
||||
let _ = ctx.audio_tx.send(AudioCommand::LoadSamples(index));
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::LoadSamples(index));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ fn main() -> io::Result<()> {
|
||||
initial_samples.extend(index);
|
||||
}
|
||||
|
||||
let sequencer = spawn_sequencer(
|
||||
let (sequencer, initial_audio_rx) = spawn_sequencer(
|
||||
Arc::clone(&link),
|
||||
Arc::clone(&playing),
|
||||
Arc::clone(&app.variables),
|
||||
@@ -118,7 +118,7 @@ fn main() -> io::Result<()> {
|
||||
|
||||
let (mut _stream, mut _analysis_handle) = match build_stream(
|
||||
&stream_config,
|
||||
sequencer.audio_rx.clone(),
|
||||
initial_audio_rx,
|
||||
Arc::clone(&scope_buffer),
|
||||
Arc::clone(&spectrum_buffer),
|
||||
Arc::clone(&metrics),
|
||||
@@ -149,6 +149,8 @@ fn main() -> io::Result<()> {
|
||||
_stream = None;
|
||||
_analysis_handle = None;
|
||||
|
||||
let new_audio_rx = sequencer.swap_audio_channel();
|
||||
|
||||
let new_config = AudioStreamConfig {
|
||||
output_device: app.audio.config.output_device.clone(),
|
||||
channels: app.audio.config.channels,
|
||||
@@ -165,7 +167,7 @@ fn main() -> io::Result<()> {
|
||||
|
||||
match build_stream(
|
||||
&new_config,
|
||||
sequencer.audio_rx.clone(),
|
||||
new_audio_rx,
|
||||
Arc::clone(&scope_buffer),
|
||||
Arc::clone(&spectrum_buffer),
|
||||
Arc::clone(&metrics),
|
||||
|
||||
@@ -9,6 +9,9 @@ use crate::app::App;
|
||||
use crate::state::{DeviceKind, EngineSection, SettingKind};
|
||||
use crate::widgets::{Orientation, Scope, Spectrum};
|
||||
|
||||
const HEADER_COLOR: Color = Color::Rgb(100, 160, 180);
|
||||
const DIVIDER_COLOR: Color = Color::Rgb(60, 65, 70);
|
||||
|
||||
pub fn render(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let [left_col, _, right_col] = Layout::horizontal([
|
||||
Constraint::Percentage(55),
|
||||
@@ -42,9 +45,9 @@ fn render_settings_section(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let [devices_area, _, settings_area, _, samples_area] = Layout::vertical([
|
||||
Constraint::Length(devices_height),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(6),
|
||||
Constraint::Length(7),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(5),
|
||||
Constraint::Min(6),
|
||||
])
|
||||
.areas(padded);
|
||||
|
||||
@@ -109,23 +112,39 @@ fn list_height(item_count: usize) -> u16 {
|
||||
fn devices_section_height(app: &App) -> u16 {
|
||||
let output_h = list_height(app.audio.output_devices.len());
|
||||
let input_h = list_height(app.audio.input_devices.len());
|
||||
2 + output_h.max(input_h)
|
||||
3 + output_h.max(input_h)
|
||||
}
|
||||
|
||||
fn render_section_header(frame: &mut Frame, title: &str, focused: bool, area: Rect) {
|
||||
let [header_area, divider_area] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
]).areas(area);
|
||||
|
||||
let header_style = if focused {
|
||||
Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new().fg(HEADER_COLOR).add_modifier(Modifier::BOLD)
|
||||
};
|
||||
|
||||
frame.render_widget(Paragraph::new(title).style(header_style), header_area);
|
||||
|
||||
let divider = "─".repeat(area.width as usize);
|
||||
frame.render_widget(
|
||||
Paragraph::new(divider).style(Style::new().fg(DIVIDER_COLOR)),
|
||||
divider_area,
|
||||
);
|
||||
}
|
||||
|
||||
fn render_devices(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let section_focused = app.audio.section == EngineSection::Devices;
|
||||
let header_style = if section_focused {
|
||||
Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new().fg(Color::Rgb(100, 160, 180)).add_modifier(Modifier::BOLD)
|
||||
};
|
||||
|
||||
let [header_area, content_area] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(2),
|
||||
Constraint::Min(1),
|
||||
]).areas(area);
|
||||
|
||||
frame.render_widget(Paragraph::new("Devices").style(header_style), header_area);
|
||||
render_section_header(frame, "DEVICES", section_focused, header_area);
|
||||
|
||||
let [output_col, separator, input_col] = Layout::horizontal([
|
||||
Constraint::Percentage(48),
|
||||
@@ -206,16 +225,11 @@ fn render_device_column(
|
||||
|
||||
fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let section_focused = app.audio.section == EngineSection::Settings;
|
||||
let header_style = if section_focused {
|
||||
Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new().fg(Color::Rgb(100, 160, 180)).add_modifier(Modifier::BOLD)
|
||||
};
|
||||
|
||||
let [header_area, content_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).areas(area);
|
||||
Layout::vertical([Constraint::Length(2), Constraint::Min(1)]).areas(area);
|
||||
|
||||
frame.render_widget(Paragraph::new("Settings").style(header_style), header_area);
|
||||
render_section_header(frame, "SETTINGS", section_focused, header_area);
|
||||
|
||||
let highlight = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
|
||||
let normal = Style::new().fg(Color::White);
|
||||
@@ -269,14 +283,9 @@ fn render_settings(frame: &mut Frame, app: &App, area: Rect) {
|
||||
|
||||
fn render_samples(frame: &mut Frame, app: &App, area: Rect) {
|
||||
let section_focused = app.audio.section == EngineSection::Samples;
|
||||
let header_style = if section_focused {
|
||||
Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new().fg(Color::Rgb(100, 160, 180)).add_modifier(Modifier::BOLD)
|
||||
};
|
||||
|
||||
let [header_area, content_area, _, hint_area] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(2),
|
||||
Constraint::Min(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(1),
|
||||
@@ -285,8 +294,8 @@ fn render_samples(frame: &mut Frame, app: &App, area: Rect) {
|
||||
|
||||
let path_count = app.audio.config.sample_paths.len();
|
||||
let sample_count = app.audio.config.sample_count;
|
||||
let header_text = format!("Samples {path_count} paths · {sample_count} indexed");
|
||||
frame.render_widget(Paragraph::new(header_text).style(header_style), header_area);
|
||||
let header_text = format!("SAMPLES {path_count} paths · {sample_count} indexed");
|
||||
render_section_header(frame, &header_text, section_focused, header_area);
|
||||
|
||||
let dim = Style::new().fg(Color::Rgb(80, 85, 95));
|
||||
let path_style = Style::new().fg(Color::Rgb(120, 125, 135));
|
||||
|
||||
Reference in New Issue
Block a user