Feat: mixed bag
This commit is contained in:
@@ -139,6 +139,13 @@ impl App {
|
||||
} => {
|
||||
self.project_state.project.banks[bank].patterns[pattern].name = name;
|
||||
}
|
||||
AppCommand::DescribePattern {
|
||||
bank,
|
||||
pattern,
|
||||
description,
|
||||
} => {
|
||||
self.project_state.project.banks[bank].patterns[pattern].description = description;
|
||||
}
|
||||
AppCommand::RenameStep {
|
||||
bank,
|
||||
pattern,
|
||||
@@ -179,6 +186,7 @@ impl App {
|
||||
bank,
|
||||
pattern,
|
||||
name,
|
||||
description,
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
@@ -189,6 +197,7 @@ impl App {
|
||||
(bank, pattern),
|
||||
StagedPropChange {
|
||||
name,
|
||||
description,
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
|
||||
@@ -187,6 +187,7 @@ impl App {
|
||||
pattern,
|
||||
field: PatternPropsField::default(),
|
||||
name: pat.name.clone().unwrap_or_default(),
|
||||
description: pat.description.clone().unwrap_or_default(),
|
||||
length: pat.length.to_string(),
|
||||
speed: pat.speed,
|
||||
quantization: pat.quantization,
|
||||
|
||||
@@ -74,6 +74,7 @@ impl App {
|
||||
for ((bank, pattern), props) in self.playback.staged_prop_changes.drain() {
|
||||
let pat = self.project_state.project.pattern_at_mut(bank, pattern);
|
||||
pat.name = props.name;
|
||||
pat.description = props.description;
|
||||
if let Some(len) = props.length {
|
||||
pat.set_length(len);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ impl App {
|
||||
let data = self.project_state.project.pattern_at(*bank, *pattern).clone();
|
||||
Some(UndoScope::Pattern { bank: *bank, pattern: *pattern, data })
|
||||
}
|
||||
AppCommand::RenameStep { bank, pattern, .. } => {
|
||||
AppCommand::RenameStep { bank, pattern, .. }
|
||||
| AppCommand::DescribePattern { bank, pattern, .. } => {
|
||||
let data = self.project_state.project.pattern_at(*bank, *pattern).clone();
|
||||
Some(UndoScope::Pattern { bank: *bank, pattern: *pattern, data })
|
||||
}
|
||||
|
||||
@@ -263,6 +263,7 @@ impl CagireDesktop {
|
||||
restart_samples,
|
||||
Arc::clone(&self.audio_sample_pos),
|
||||
new_error_tx,
|
||||
&self.app.audio.config.sample_paths,
|
||||
) {
|
||||
Ok((new_stream, info, new_analysis, registry)) => {
|
||||
self._stream = Some(new_stream);
|
||||
|
||||
@@ -125,6 +125,11 @@ pub enum AppCommand {
|
||||
step: usize,
|
||||
name: Option<String>,
|
||||
},
|
||||
DescribePattern {
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
description: Option<String>,
|
||||
},
|
||||
Save(PathBuf),
|
||||
Load(PathBuf),
|
||||
|
||||
@@ -142,6 +147,7 @@ pub enum AppCommand {
|
||||
bank: usize,
|
||||
pattern: usize,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
length: Option<usize>,
|
||||
speed: PatternSpeed,
|
||||
quantization: LaunchQuantization,
|
||||
|
||||
@@ -294,6 +294,7 @@ pub fn build_stream(
|
||||
initial_samples: Vec<doux::sampling::SampleEntry>,
|
||||
audio_sample_pos: Arc<AtomicU64>,
|
||||
error_tx: Sender<String>,
|
||||
sample_paths: &[std::path::PathBuf],
|
||||
) -> Result<
|
||||
(
|
||||
Stream,
|
||||
@@ -338,6 +339,17 @@ pub fn build_stream(
|
||||
let mut engine =
|
||||
Engine::new_with_metrics(sample_rate, channels, max_voices, Arc::clone(&metrics));
|
||||
engine.sample_index = initial_samples;
|
||||
|
||||
for path in sample_paths {
|
||||
if let Some(sf2_path) = doux::soundfont::find_sf2_file(path) {
|
||||
if let Err(e) = engine.load_soundfont(&sf2_path) {
|
||||
eprintln!("Failed to load soundfont: {e}");
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let registry = Arc::clone(&engine.sample_registry);
|
||||
|
||||
let (mut fft_producer, analysis_handle) = spawn_analysis_thread(sample_rate, spectrum_buffer);
|
||||
@@ -382,6 +394,11 @@ pub fn build_stream(
|
||||
AudioCommand::LoadSamples(samples) => {
|
||||
engine.sample_index.extend(samples);
|
||||
}
|
||||
AudioCommand::LoadSoundfont(path) => {
|
||||
if let Err(e) = engine.load_soundfont(&path) {
|
||||
eprintln!("Failed to load soundfont: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ pub enum AudioCommand {
|
||||
Hush,
|
||||
Panic,
|
||||
LoadSamples(Vec<doux::sampling::SampleEntry>),
|
||||
LoadSoundfont(std::path::PathBuf),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -210,6 +210,7 @@ pub fn init(args: InitArgs) -> Init {
|
||||
initial_samples,
|
||||
Arc::clone(&audio_sample_pos),
|
||||
stream_error_tx,
|
||||
&app.audio.config.sample_paths,
|
||||
) {
|
||||
Ok((s, info, analysis, registry)) => {
|
||||
app.audio.config.sample_rate = info.sample_rate;
|
||||
|
||||
@@ -214,11 +214,13 @@ pub(super) fn handle_main_page(ctx: &mut InputContext, key: KeyEvent, ctrl: bool
|
||||
}
|
||||
KeyCode::Char('m') => {
|
||||
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
|
||||
ctx.dispatch(AppCommand::StageMute { bank, pattern });
|
||||
ctx.app.mute.toggle_mute(bank, pattern);
|
||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
||||
}
|
||||
KeyCode::Char('x') => {
|
||||
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
|
||||
ctx.dispatch(AppCommand::StageSolo { bank, pattern });
|
||||
ctx.app.mute.toggle_solo(bank, pattern);
|
||||
ctx.app.send_mute_state(ctx.seq_cmd_tx);
|
||||
}
|
||||
KeyCode::Char('M') => {
|
||||
ctx.dispatch(AppCommand::ClearMutes);
|
||||
|
||||
@@ -207,6 +207,13 @@ fn load_project_samples(ctx: &mut InputContext) {
|
||||
ctx.app.audio.config.sample_paths = paths;
|
||||
ctx.app.audio.config.sample_count = total_count;
|
||||
|
||||
for path in &ctx.app.audio.config.sample_paths {
|
||||
if let Some(sf2_path) = doux::soundfont::find_sf2_file(path) {
|
||||
let _ = ctx.audio_tx.load().send(AudioCommand::LoadSoundfont(sf2_path));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if total_count > 0 {
|
||||
if let Some(registry) = ctx.app.audio.sample_registry.clone() {
|
||||
let sr = ctx.app.audio.config.sample_rate;
|
||||
|
||||
@@ -204,6 +204,9 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
.map(|e| (e.name.clone(), e.path.clone()))
|
||||
.collect();
|
||||
let _ = ctx.audio_tx.load().send(crate::engine::AudioCommand::LoadSamples(index));
|
||||
if let Some(sf2_path) = doux::soundfont::find_sf2_file(&path) {
|
||||
let _ = ctx.audio_tx.load().send(crate::engine::AudioCommand::LoadSoundfont(sf2_path));
|
||||
}
|
||||
ctx.app.audio.config.sample_count += count;
|
||||
ctx.app.audio.add_sample_path(path);
|
||||
if let Some(registry) = ctx.app.audio.sample_registry.clone() {
|
||||
@@ -376,6 +379,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
pattern,
|
||||
field,
|
||||
name,
|
||||
description,
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
@@ -423,6 +427,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
},
|
||||
KeyCode::Char(c) => match field {
|
||||
PatternPropsField::Name => name.push(c),
|
||||
PatternPropsField::Description => description.push(c),
|
||||
PatternPropsField::Length if c.is_ascii_digit() => length.push(c),
|
||||
_ => {}
|
||||
},
|
||||
@@ -430,6 +435,9 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
PatternPropsField::Name => {
|
||||
name.pop();
|
||||
}
|
||||
PatternPropsField::Description => {
|
||||
description.pop();
|
||||
}
|
||||
PatternPropsField::Length => {
|
||||
length.pop();
|
||||
}
|
||||
@@ -441,6 +449,11 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
} else {
|
||||
Some(name.clone())
|
||||
};
|
||||
let desc_val = if description.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(description.clone())
|
||||
};
|
||||
let length_val = length.parse().ok();
|
||||
let speed_val = *speed;
|
||||
let quant_val = *quantization;
|
||||
@@ -450,6 +463,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
||||
bank,
|
||||
pattern,
|
||||
name: name_val,
|
||||
description: desc_val,
|
||||
length: length_val,
|
||||
speed: speed_val,
|
||||
quantization: quant_val,
|
||||
@@ -625,5 +639,8 @@ fn rename_command(target: &RenameTarget, name: Option<String>) -> AppCommand {
|
||||
RenameTarget::Step { bank, pattern, step } => AppCommand::RenameStep {
|
||||
bank: *bank, pattern: *pattern, step: *step, name,
|
||||
},
|
||||
RenameTarget::DescribePattern { bank, pattern } => AppCommand::DescribePattern {
|
||||
bank: *bank, pattern: *pattern, description: name,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,22 @@ pub(super) fn handle_patterns_page(ctx: &mut InputContext, key: KeyEvent) -> Inp
|
||||
ctx.dispatch(AppCommand::OpenPatternPropsModal { bank, pattern });
|
||||
}
|
||||
}
|
||||
KeyCode::Char('d') => {
|
||||
if ctx.app.patterns_nav.column == PatternsColumn::Patterns
|
||||
&& !ctx.app.patterns_nav.has_selection()
|
||||
{
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
let pattern = ctx.app.patterns_nav.pattern_cursor;
|
||||
let current_desc = ctx.app.project_state.project.banks[bank].patterns[pattern]
|
||||
.description
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
ctx.dispatch(AppCommand::OpenModal(Modal::Rename {
|
||||
target: RenameTarget::DescribePattern { bank, pattern },
|
||||
name: current_desc,
|
||||
}));
|
||||
}
|
||||
}
|
||||
KeyCode::Char('m') => {
|
||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||
for pattern in ctx.app.patterns_nav.selected_patterns() {
|
||||
|
||||
@@ -137,6 +137,7 @@ fn main() -> io::Result<()> {
|
||||
restart_samples,
|
||||
Arc::clone(&audio_sample_pos),
|
||||
new_error_tx,
|
||||
&app.audio.config.sample_paths,
|
||||
) {
|
||||
Ok((new_stream, info, new_analysis, registry)) => {
|
||||
_stream = Some(new_stream);
|
||||
|
||||
@@ -123,6 +123,10 @@ pub const DOCS: &[DocEntry] = &[
|
||||
"Recording",
|
||||
include_str!("../../docs/tutorials/recording.md"),
|
||||
),
|
||||
Topic(
|
||||
"Soundfonts",
|
||||
include_str!("../../docs/tutorials/soundfont.md"),
|
||||
),
|
||||
];
|
||||
|
||||
pub fn topic_count() -> usize {
|
||||
|
||||
@@ -20,6 +20,7 @@ pub enum PatternField {
|
||||
pub enum PatternPropsField {
|
||||
#[default]
|
||||
Name,
|
||||
Description,
|
||||
Length,
|
||||
Speed,
|
||||
Quantization,
|
||||
@@ -32,7 +33,8 @@ pub enum PatternPropsField {
|
||||
impl PatternPropsField {
|
||||
pub fn next(&self, follow_up_is_chain: bool) -> Self {
|
||||
match self {
|
||||
Self::Name => Self::Length,
|
||||
Self::Name => Self::Description,
|
||||
Self::Description => Self::Length,
|
||||
Self::Length => Self::Speed,
|
||||
Self::Speed => Self::Quantization,
|
||||
Self::Quantization => Self::SyncMode,
|
||||
@@ -47,7 +49,8 @@ impl PatternPropsField {
|
||||
pub fn prev(&self, follow_up_is_chain: bool) -> Self {
|
||||
match self {
|
||||
Self::Name => Self::Name,
|
||||
Self::Length => Self::Name,
|
||||
Self::Description => Self::Name,
|
||||
Self::Length => Self::Description,
|
||||
Self::Speed => Self::Length,
|
||||
Self::Quantization => Self::Speed,
|
||||
Self::SyncMode => Self::Quantization,
|
||||
|
||||
@@ -35,6 +35,7 @@ pub enum RenameTarget {
|
||||
Bank { bank: usize },
|
||||
Pattern { bank: usize, pattern: usize },
|
||||
Step { bank: usize, pattern: usize, step: usize },
|
||||
DescribePattern { bank: usize, pattern: usize },
|
||||
}
|
||||
|
||||
impl RenameTarget {
|
||||
@@ -43,6 +44,9 @@ impl RenameTarget {
|
||||
Self::Bank { bank } => format!("Rename Bank {:02}", bank + 1),
|
||||
Self::Pattern { bank, pattern } => format!("Rename {}", model::bp_label(*bank, *pattern)),
|
||||
Self::Step { step, .. } => format!("Name Step {:02}", step + 1),
|
||||
Self::DescribePattern { bank, pattern } => {
|
||||
format!("Describe {}", model::bp_label(*bank, *pattern))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,6 +77,7 @@ pub enum Modal {
|
||||
pattern: usize,
|
||||
field: PatternPropsField,
|
||||
name: String,
|
||||
description: String,
|
||||
length: String,
|
||||
speed: PatternSpeed,
|
||||
quantization: LaunchQuantization,
|
||||
|
||||
@@ -17,6 +17,7 @@ pub enum StagedMuteChange {
|
||||
|
||||
pub struct StagedPropChange {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub length: Option<usize>,
|
||||
pub speed: PatternSpeed,
|
||||
pub quantization: LaunchQuantization,
|
||||
|
||||
@@ -55,13 +55,17 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati
|
||||
}
|
||||
Page::Patterns => {
|
||||
bindings.push(("←→↑↓", "Navigate", "Move between banks/patterns"));
|
||||
bindings.push(("Shift+↑↓", "Select", "Extend selection"));
|
||||
bindings.push(("Alt+↑↓", "Shift", "Move patterns up/down"));
|
||||
bindings.push(("Enter", "Select", "Select pattern for editing"));
|
||||
if !plugin_mode {
|
||||
bindings.push(("Space", "Play", "Toggle pattern playback"));
|
||||
}
|
||||
bindings.push(("Esc", "Back", "Clear staged or go back"));
|
||||
bindings.push(("c", "Commit", "Commit staged changes"));
|
||||
bindings.push(("p", "Stage play", "Stage pattern play toggle"));
|
||||
bindings.push(("r", "Rename", "Rename bank/pattern"));
|
||||
bindings.push(("d", "Describe", "Add description to pattern"));
|
||||
bindings.push(("e", "Properties", "Edit pattern properties"));
|
||||
bindings.push(("m", "Mute", "Stage mute for pattern"));
|
||||
bindings.push(("x", "Solo", "Stage solo for pattern"));
|
||||
@@ -70,6 +74,8 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati
|
||||
bindings.push(("Ctrl+C", "Copy", "Copy bank/pattern"));
|
||||
bindings.push(("Ctrl+V", "Paste", "Paste bank/pattern"));
|
||||
bindings.push(("Del", "Reset", "Reset bank/pattern"));
|
||||
bindings.push(("Ctrl+Z", "Undo", "Undo last action"));
|
||||
bindings.push(("Ctrl+Shift+Z", "Redo", "Redo last action"));
|
||||
}
|
||||
Page::Engine => {
|
||||
bindings.push(("Tab", "Section", "Next section"));
|
||||
|
||||
@@ -496,13 +496,22 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
||||
height: 1,
|
||||
};
|
||||
|
||||
let label = format!(
|
||||
let right_label = format!(
|
||||
"{} · {}",
|
||||
pattern.quantization.label(),
|
||||
pattern.sync_mode.label()
|
||||
);
|
||||
let w = detail_area.width as usize;
|
||||
let padded_label = format!("{label:>w$}");
|
||||
let label = if let Some(desc) = &pattern.description {
|
||||
let right_len = right_label.chars().count();
|
||||
let max_desc = w.saturating_sub(right_len + 1);
|
||||
let truncated: String = desc.chars().take(max_desc).collect();
|
||||
let pad = w.saturating_sub(truncated.chars().count() + right_len);
|
||||
format!("{truncated}{}{right_label}", " ".repeat(pad))
|
||||
} else {
|
||||
format!("{right_label:>w$}")
|
||||
};
|
||||
let padded_label = label;
|
||||
|
||||
let filled_width = if is_playing {
|
||||
let ratio = snapshot.get_smooth_progress(bank, idx, length, speed.multiplier()).unwrap_or(0.0);
|
||||
@@ -725,6 +734,7 @@ fn render_properties(
|
||||
let pattern = &app.project_state.project.banks[bank].patterns[pattern_idx];
|
||||
|
||||
let name = pattern.name.as_deref().unwrap_or("-");
|
||||
let desc = pattern.description.as_deref().unwrap_or("-");
|
||||
let content_count = pattern.content_step_count();
|
||||
let steps_label = format!("{}/{}", content_count, pattern.length);
|
||||
let speed_label = pattern.speed.label();
|
||||
@@ -739,6 +749,10 @@ fn render_properties(
|
||||
Span::styled(" Name ", label_style),
|
||||
Span::styled(name, value_style),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" Desc ", label_style),
|
||||
Span::styled(desc, value_style),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled(" Steps ", label_style),
|
||||
Span::styled(steps_label, value_style),
|
||||
|
||||
@@ -650,6 +650,7 @@ fn render_modal(
|
||||
pattern,
|
||||
field,
|
||||
name,
|
||||
description,
|
||||
length,
|
||||
speed,
|
||||
quantization,
|
||||
@@ -660,7 +661,7 @@ fn render_modal(
|
||||
use crate::state::PatternPropsField;
|
||||
|
||||
let is_chain = matches!(follow_up, FollowUp::Chain { .. });
|
||||
let modal_height = if is_chain { 16 } else { 14 };
|
||||
let modal_height = if is_chain { 18 } else { 16 };
|
||||
|
||||
let inner = ModalFrame::new(&format!(" Pattern B{:02}:P{:02} ", bank + 1, pattern + 1))
|
||||
.width(50)
|
||||
@@ -678,6 +679,7 @@ fn render_modal(
|
||||
};
|
||||
let mut fields: Vec<(&str, String, bool)> = vec![
|
||||
("Name", name.clone(), *field == PatternPropsField::Name),
|
||||
("Desc", description.clone(), *field == PatternPropsField::Description),
|
||||
("Length", length.clone(), *field == PatternPropsField::Length),
|
||||
("Speed", speed_label, *field == PatternPropsField::Speed),
|
||||
("Quantization", quantization.label().to_string(), *field == PatternPropsField::Quantization),
|
||||
|
||||
Reference in New Issue
Block a user