Feat: mixed bag
This commit is contained in:
@@ -51,7 +51,7 @@ cagire-forth = { path = "crates/forth" }
|
|||||||
cagire-markdown = { path = "crates/markdown" }
|
cagire-markdown = { path = "crates/markdown" }
|
||||||
cagire-project = { path = "crates/project" }
|
cagire-project = { path = "crates/project" }
|
||||||
cagire-ratatui = { path = "crates/ratatui" }
|
cagire-ratatui = { path = "crates/ratatui" }
|
||||||
doux = { path = "/Users/bubo/doux", features = ["native"] }
|
doux = { path = "/Users/bubo/doux", features = ["native", "soundfont"] }
|
||||||
rusty_link = "0.4"
|
rusty_link = "0.4"
|
||||||
ratatui = "0.30"
|
ratatui = "0.30"
|
||||||
crossterm = "0.29"
|
crossterm = "0.29"
|
||||||
|
|||||||
@@ -283,6 +283,7 @@ pub struct Pattern {
|
|||||||
pub length: usize,
|
pub length: usize,
|
||||||
pub speed: PatternSpeed,
|
pub speed: PatternSpeed,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
pub quantization: LaunchQuantization,
|
pub quantization: LaunchQuantization,
|
||||||
pub sync_mode: SyncMode,
|
pub sync_mode: SyncMode,
|
||||||
pub follow_up: FollowUp,
|
pub follow_up: FollowUp,
|
||||||
@@ -317,6 +318,8 @@ struct SparsePattern {
|
|||||||
speed: PatternSpeed,
|
speed: PatternSpeed,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
description: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "is_default_quantization")]
|
#[serde(default, skip_serializing_if = "is_default_quantization")]
|
||||||
quantization: LaunchQuantization,
|
quantization: LaunchQuantization,
|
||||||
#[serde(default, skip_serializing_if = "is_default_sync_mode")]
|
#[serde(default, skip_serializing_if = "is_default_sync_mode")]
|
||||||
@@ -342,6 +345,8 @@ struct LegacyPattern {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
description: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
quantization: LaunchQuantization,
|
quantization: LaunchQuantization,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
sync_mode: SyncMode,
|
sync_mode: SyncMode,
|
||||||
@@ -370,6 +375,7 @@ impl Serialize for Pattern {
|
|||||||
length: self.length,
|
length: self.length,
|
||||||
speed: self.speed,
|
speed: self.speed,
|
||||||
name: self.name.clone(),
|
name: self.name.clone(),
|
||||||
|
description: self.description.clone(),
|
||||||
quantization: self.quantization,
|
quantization: self.quantization,
|
||||||
sync_mode: self.sync_mode,
|
sync_mode: self.sync_mode,
|
||||||
follow_up: self.follow_up,
|
follow_up: self.follow_up,
|
||||||
@@ -405,6 +411,7 @@ impl<'de> Deserialize<'de> for Pattern {
|
|||||||
length: sparse.length,
|
length: sparse.length,
|
||||||
speed: sparse.speed,
|
speed: sparse.speed,
|
||||||
name: sparse.name,
|
name: sparse.name,
|
||||||
|
description: sparse.description,
|
||||||
quantization: sparse.quantization,
|
quantization: sparse.quantization,
|
||||||
sync_mode: sparse.sync_mode,
|
sync_mode: sparse.sync_mode,
|
||||||
follow_up: sparse.follow_up,
|
follow_up: sparse.follow_up,
|
||||||
@@ -415,6 +422,7 @@ impl<'de> Deserialize<'de> for Pattern {
|
|||||||
length: legacy.length,
|
length: legacy.length,
|
||||||
speed: legacy.speed,
|
speed: legacy.speed,
|
||||||
name: legacy.name,
|
name: legacy.name,
|
||||||
|
description: legacy.description,
|
||||||
quantization: legacy.quantization,
|
quantization: legacy.quantization,
|
||||||
sync_mode: legacy.sync_mode,
|
sync_mode: legacy.sync_mode,
|
||||||
follow_up: legacy.follow_up,
|
follow_up: legacy.follow_up,
|
||||||
@@ -430,6 +438,7 @@ impl Default for Pattern {
|
|||||||
length: DEFAULT_LENGTH,
|
length: DEFAULT_LENGTH,
|
||||||
speed: PatternSpeed::default(),
|
speed: PatternSpeed::default(),
|
||||||
name: None,
|
name: None,
|
||||||
|
description: None,
|
||||||
quantization: LaunchQuantization::default(),
|
quantization: LaunchQuantization::default(),
|
||||||
sync_mode: SyncMode::default(),
|
sync_mode: SyncMode::default(),
|
||||||
follow_up: FollowUp::default(),
|
follow_up: FollowUp::default(),
|
||||||
|
|||||||
111
docs/tutorials/soundfont.md
Normal file
111
docs/tutorials/soundfont.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# Soundfonts
|
||||||
|
|
||||||
|
General MIDI playback via SF2 soundfont files. Native only -- not available in the WASM build.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Drop an `.sf2` file into one of your samples directories. The engine finds and loads it automatically when Cagire starts. Only one soundfont is active at a time.
|
||||||
|
|
||||||
|
## Playing
|
||||||
|
|
||||||
|
Use `gm` as the sound source. The `n` parameter selects a program by name or number (0-127):
|
||||||
|
|
||||||
|
```forth
|
||||||
|
gm s piano n . ;; acoustic piano
|
||||||
|
gm s strings n c4 note . ;; strings playing middle C
|
||||||
|
gm s 0 n e4 note . ;; program 0 (piano) playing E4
|
||||||
|
```
|
||||||
|
|
||||||
|
## Drums
|
||||||
|
|
||||||
|
Drums live on a separate bank. Use `drums` or `percussion` as the `n` value. Each MIDI note triggers a different instrument:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
gm s drums n 36 note . ;; kick
|
||||||
|
gm s drums n 38 note . ;; snare
|
||||||
|
gm s drums n 42 note . ;; closed hi-hat
|
||||||
|
gm s percussion n 49 note . ;; crash cymbal
|
||||||
|
```
|
||||||
|
|
||||||
|
## Envelope
|
||||||
|
|
||||||
|
The soundfont embeds ADSR envelope data per preset. The engine applies it automatically. Override any parameter explicitly:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
gm s piano n 0.01 attack 0.3 decay .
|
||||||
|
gm s strings n 0.5 attack 2.0 release .
|
||||||
|
```
|
||||||
|
|
||||||
|
If you set `attack`, `decay`, `sustain`, or `release`, your value wins. Unspecified parameters keep the soundfont default.
|
||||||
|
|
||||||
|
## Effects
|
||||||
|
|
||||||
|
All standard engine parameters work on GM voices. Filter, distort, spatialize:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
gm s bass n 800 lpf 0.3 verb .
|
||||||
|
gm s epiano n 0.5 delay 1.5 distort .
|
||||||
|
gm s choir n 0.8 pan 2000 hpf .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Preset Names
|
||||||
|
|
||||||
|
| # | Name | # | Name | # | Name | # | Name |
|
||||||
|
|---|------|---|------|---|------|---|------|
|
||||||
|
| 0 | piano | 24 | guitar | 48 | strings | 72 | piccolo |
|
||||||
|
| 1 | brightpiano | 25 | steelguitar | 49 | slowstrings | 73 | flute |
|
||||||
|
| 4 | epiano | 26 | jazzguitar | 52 | choir | 74 | recorder |
|
||||||
|
| 6 | harpsichord | 27 | cleangt | 56 | trumpet | 75 | panflute |
|
||||||
|
| 7 | clavinet | 29 | overdrive | 57 | trombone | 79 | whistle |
|
||||||
|
| 8 | celesta | 30 | distgt | 58 | tuba | 80 | ocarina |
|
||||||
|
| 9 | glockenspiel | 33 | bass | 60 | horn | 81 | lead |
|
||||||
|
| 10 | musicbox | 34 | pickbass | 61 | brass | 82 | sawlead |
|
||||||
|
| 11 | vibraphone | 35 | fretless | 64 | sopranosax | 89 | pad |
|
||||||
|
| 12 | marimba | 36 | slapbass | 65 | altosax | 90 | warmpad |
|
||||||
|
| 13 | xylophone | 38 | synthbass | 66 | tenorsax | 91 | polysynth |
|
||||||
|
| 14 | bells | 40 | violin | 67 | barisax | 104 | sitar |
|
||||||
|
| 16 | organ | 41 | viola | 68 | oboe | 105 | banjo |
|
||||||
|
| 19 | churchorgan | 42 | cello | 70 | bassoon | 108 | kalimba |
|
||||||
|
| 21 | accordion | 43 | contrabass | 71 | clarinet | 114 | steeldrum |
|
||||||
|
| 22 | harmonica | 45 | pizzicato | | | | |
|
||||||
|
| | | 46 | harp | | | | |
|
||||||
|
| | | 47 | timpani | | | | |
|
||||||
|
|
||||||
|
Drums are on a separate bank: use `drums` or `percussion` as the `n` value.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
A simple GM drum pattern across four steps:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
;; step 1: kick
|
||||||
|
gm s drums n 36 note .
|
||||||
|
|
||||||
|
;; step 2: closed hat
|
||||||
|
gm s drums n 42 note 0.6 gain .
|
||||||
|
|
||||||
|
;; step 3: snare
|
||||||
|
gm s drums n 38 note .
|
||||||
|
|
||||||
|
;; step 4: closed hat
|
||||||
|
gm s drums n 42 note 0.6 gain .
|
||||||
|
```
|
||||||
|
|
||||||
|
Layer piano chords with randomness:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
gm s piano n
|
||||||
|
c4 e4 g4 3 choose note
|
||||||
|
0.3 0.8 rand gain
|
||||||
|
0.1 0.4 rand verb
|
||||||
|
.
|
||||||
|
```
|
||||||
|
|
||||||
|
A bass line with envelope override:
|
||||||
|
|
||||||
|
```forth
|
||||||
|
gm s bass n
|
||||||
|
c2 e2 g2 a2 4 cycle note
|
||||||
|
0.01 attack 0.2 decay 0.0 sustain
|
||||||
|
.
|
||||||
|
```
|
||||||
@@ -14,7 +14,7 @@ cagire = { path = "../..", default-features = false, features = ["block-renderer
|
|||||||
cagire-forth = { path = "../../crates/forth" }
|
cagire-forth = { path = "../../crates/forth" }
|
||||||
cagire-project = { path = "../../crates/project" }
|
cagire-project = { path = "../../crates/project" }
|
||||||
cagire-ratatui = { path = "../../crates/ratatui" }
|
cagire-ratatui = { path = "../../crates/ratatui" }
|
||||||
doux = { path = "/Users/bubo/doux", features = ["native"] }
|
doux = { path = "/Users/bubo/doux", features = ["native", "soundfont"] }
|
||||||
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", features = ["standalone"] }
|
nih_plug = { git = "https://github.com/robbert-vdh/nih-plug", features = ["standalone"] }
|
||||||
nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug" }
|
nih_plug_egui = { git = "https://github.com/robbert-vdh/nih-plug" }
|
||||||
egui_ratatui = "2.1"
|
egui_ratatui = "2.1"
|
||||||
|
|||||||
@@ -139,6 +139,13 @@ impl App {
|
|||||||
} => {
|
} => {
|
||||||
self.project_state.project.banks[bank].patterns[pattern].name = name;
|
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 {
|
AppCommand::RenameStep {
|
||||||
bank,
|
bank,
|
||||||
pattern,
|
pattern,
|
||||||
@@ -179,6 +186,7 @@ impl App {
|
|||||||
bank,
|
bank,
|
||||||
pattern,
|
pattern,
|
||||||
name,
|
name,
|
||||||
|
description,
|
||||||
length,
|
length,
|
||||||
speed,
|
speed,
|
||||||
quantization,
|
quantization,
|
||||||
@@ -189,6 +197,7 @@ impl App {
|
|||||||
(bank, pattern),
|
(bank, pattern),
|
||||||
StagedPropChange {
|
StagedPropChange {
|
||||||
name,
|
name,
|
||||||
|
description,
|
||||||
length,
|
length,
|
||||||
speed,
|
speed,
|
||||||
quantization,
|
quantization,
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ impl App {
|
|||||||
pattern,
|
pattern,
|
||||||
field: PatternPropsField::default(),
|
field: PatternPropsField::default(),
|
||||||
name: pat.name.clone().unwrap_or_default(),
|
name: pat.name.clone().unwrap_or_default(),
|
||||||
|
description: pat.description.clone().unwrap_or_default(),
|
||||||
length: pat.length.to_string(),
|
length: pat.length.to_string(),
|
||||||
speed: pat.speed,
|
speed: pat.speed,
|
||||||
quantization: pat.quantization,
|
quantization: pat.quantization,
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ impl App {
|
|||||||
for ((bank, pattern), props) in self.playback.staged_prop_changes.drain() {
|
for ((bank, pattern), props) in self.playback.staged_prop_changes.drain() {
|
||||||
let pat = self.project_state.project.pattern_at_mut(bank, pattern);
|
let pat = self.project_state.project.pattern_at_mut(bank, pattern);
|
||||||
pat.name = props.name;
|
pat.name = props.name;
|
||||||
|
pat.description = props.description;
|
||||||
if let Some(len) = props.length {
|
if let Some(len) = props.length {
|
||||||
pat.set_length(len);
|
pat.set_length(len);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ impl App {
|
|||||||
let data = self.project_state.project.pattern_at(*bank, *pattern).clone();
|
let data = self.project_state.project.pattern_at(*bank, *pattern).clone();
|
||||||
Some(UndoScope::Pattern { bank: *bank, pattern: *pattern, data })
|
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();
|
let data = self.project_state.project.pattern_at(*bank, *pattern).clone();
|
||||||
Some(UndoScope::Pattern { bank: *bank, pattern: *pattern, data })
|
Some(UndoScope::Pattern { bank: *bank, pattern: *pattern, data })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ impl CagireDesktop {
|
|||||||
restart_samples,
|
restart_samples,
|
||||||
Arc::clone(&self.audio_sample_pos),
|
Arc::clone(&self.audio_sample_pos),
|
||||||
new_error_tx,
|
new_error_tx,
|
||||||
|
&self.app.audio.config.sample_paths,
|
||||||
) {
|
) {
|
||||||
Ok((new_stream, info, new_analysis, registry)) => {
|
Ok((new_stream, info, new_analysis, registry)) => {
|
||||||
self._stream = Some(new_stream);
|
self._stream = Some(new_stream);
|
||||||
|
|||||||
@@ -125,6 +125,11 @@ pub enum AppCommand {
|
|||||||
step: usize,
|
step: usize,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
},
|
},
|
||||||
|
DescribePattern {
|
||||||
|
bank: usize,
|
||||||
|
pattern: usize,
|
||||||
|
description: Option<String>,
|
||||||
|
},
|
||||||
Save(PathBuf),
|
Save(PathBuf),
|
||||||
Load(PathBuf),
|
Load(PathBuf),
|
||||||
|
|
||||||
@@ -142,6 +147,7 @@ pub enum AppCommand {
|
|||||||
bank: usize,
|
bank: usize,
|
||||||
pattern: usize,
|
pattern: usize,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
length: Option<usize>,
|
length: Option<usize>,
|
||||||
speed: PatternSpeed,
|
speed: PatternSpeed,
|
||||||
quantization: LaunchQuantization,
|
quantization: LaunchQuantization,
|
||||||
|
|||||||
@@ -294,6 +294,7 @@ pub fn build_stream(
|
|||||||
initial_samples: Vec<doux::sampling::SampleEntry>,
|
initial_samples: Vec<doux::sampling::SampleEntry>,
|
||||||
audio_sample_pos: Arc<AtomicU64>,
|
audio_sample_pos: Arc<AtomicU64>,
|
||||||
error_tx: Sender<String>,
|
error_tx: Sender<String>,
|
||||||
|
sample_paths: &[std::path::PathBuf],
|
||||||
) -> Result<
|
) -> Result<
|
||||||
(
|
(
|
||||||
Stream,
|
Stream,
|
||||||
@@ -338,6 +339,17 @@ pub fn build_stream(
|
|||||||
let mut engine =
|
let mut engine =
|
||||||
Engine::new_with_metrics(sample_rate, channels, max_voices, Arc::clone(&metrics));
|
Engine::new_with_metrics(sample_rate, channels, max_voices, Arc::clone(&metrics));
|
||||||
engine.sample_index = initial_samples;
|
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 registry = Arc::clone(&engine.sample_registry);
|
||||||
|
|
||||||
let (mut fft_producer, analysis_handle) = spawn_analysis_thread(sample_rate, spectrum_buffer);
|
let (mut fft_producer, analysis_handle) = spawn_analysis_thread(sample_rate, spectrum_buffer);
|
||||||
@@ -382,6 +394,11 @@ pub fn build_stream(
|
|||||||
AudioCommand::LoadSamples(samples) => {
|
AudioCommand::LoadSamples(samples) => {
|
||||||
engine.sample_index.extend(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,
|
Hush,
|
||||||
Panic,
|
Panic,
|
||||||
LoadSamples(Vec<doux::sampling::SampleEntry>),
|
LoadSamples(Vec<doux::sampling::SampleEntry>),
|
||||||
|
LoadSoundfont(std::path::PathBuf),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ pub fn init(args: InitArgs) -> Init {
|
|||||||
initial_samples,
|
initial_samples,
|
||||||
Arc::clone(&audio_sample_pos),
|
Arc::clone(&audio_sample_pos),
|
||||||
stream_error_tx,
|
stream_error_tx,
|
||||||
|
&app.audio.config.sample_paths,
|
||||||
) {
|
) {
|
||||||
Ok((s, info, analysis, registry)) => {
|
Ok((s, info, analysis, registry)) => {
|
||||||
app.audio.config.sample_rate = info.sample_rate;
|
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') => {
|
KeyCode::Char('m') => {
|
||||||
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
|
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') => {
|
KeyCode::Char('x') => {
|
||||||
let (bank, pattern) = (ctx.app.editor_ctx.bank, ctx.app.editor_ctx.pattern);
|
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') => {
|
KeyCode::Char('M') => {
|
||||||
ctx.dispatch(AppCommand::ClearMutes);
|
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_paths = paths;
|
||||||
ctx.app.audio.config.sample_count = total_count;
|
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 total_count > 0 {
|
||||||
if let Some(registry) = ctx.app.audio.sample_registry.clone() {
|
if let Some(registry) = ctx.app.audio.sample_registry.clone() {
|
||||||
let sr = ctx.app.audio.config.sample_rate;
|
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()))
|
.map(|e| (e.name.clone(), e.path.clone()))
|
||||||
.collect();
|
.collect();
|
||||||
let _ = ctx.audio_tx.load().send(crate::engine::AudioCommand::LoadSamples(index));
|
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.config.sample_count += count;
|
||||||
ctx.app.audio.add_sample_path(path);
|
ctx.app.audio.add_sample_path(path);
|
||||||
if let Some(registry) = ctx.app.audio.sample_registry.clone() {
|
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,
|
pattern,
|
||||||
field,
|
field,
|
||||||
name,
|
name,
|
||||||
|
description,
|
||||||
length,
|
length,
|
||||||
speed,
|
speed,
|
||||||
quantization,
|
quantization,
|
||||||
@@ -423,6 +427,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
|||||||
},
|
},
|
||||||
KeyCode::Char(c) => match field {
|
KeyCode::Char(c) => match field {
|
||||||
PatternPropsField::Name => name.push(c),
|
PatternPropsField::Name => name.push(c),
|
||||||
|
PatternPropsField::Description => description.push(c),
|
||||||
PatternPropsField::Length if c.is_ascii_digit() => length.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 => {
|
PatternPropsField::Name => {
|
||||||
name.pop();
|
name.pop();
|
||||||
}
|
}
|
||||||
|
PatternPropsField::Description => {
|
||||||
|
description.pop();
|
||||||
|
}
|
||||||
PatternPropsField::Length => {
|
PatternPropsField::Length => {
|
||||||
length.pop();
|
length.pop();
|
||||||
}
|
}
|
||||||
@@ -441,6 +449,11 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
|||||||
} else {
|
} else {
|
||||||
Some(name.clone())
|
Some(name.clone())
|
||||||
};
|
};
|
||||||
|
let desc_val = if description.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(description.clone())
|
||||||
|
};
|
||||||
let length_val = length.parse().ok();
|
let length_val = length.parse().ok();
|
||||||
let speed_val = *speed;
|
let speed_val = *speed;
|
||||||
let quant_val = *quantization;
|
let quant_val = *quantization;
|
||||||
@@ -450,6 +463,7 @@ pub(super) fn handle_modal_input(ctx: &mut InputContext, key: KeyEvent) -> Input
|
|||||||
bank,
|
bank,
|
||||||
pattern,
|
pattern,
|
||||||
name: name_val,
|
name: name_val,
|
||||||
|
description: desc_val,
|
||||||
length: length_val,
|
length: length_val,
|
||||||
speed: speed_val,
|
speed: speed_val,
|
||||||
quantization: quant_val,
|
quantization: quant_val,
|
||||||
@@ -625,5 +639,8 @@ fn rename_command(target: &RenameTarget, name: Option<String>) -> AppCommand {
|
|||||||
RenameTarget::Step { bank, pattern, step } => AppCommand::RenameStep {
|
RenameTarget::Step { bank, pattern, step } => AppCommand::RenameStep {
|
||||||
bank: *bank, pattern: *pattern, step: *step, name,
|
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 });
|
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') => {
|
KeyCode::Char('m') => {
|
||||||
let bank = ctx.app.patterns_nav.bank_cursor;
|
let bank = ctx.app.patterns_nav.bank_cursor;
|
||||||
for pattern in ctx.app.patterns_nav.selected_patterns() {
|
for pattern in ctx.app.patterns_nav.selected_patterns() {
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ fn main() -> io::Result<()> {
|
|||||||
restart_samples,
|
restart_samples,
|
||||||
Arc::clone(&audio_sample_pos),
|
Arc::clone(&audio_sample_pos),
|
||||||
new_error_tx,
|
new_error_tx,
|
||||||
|
&app.audio.config.sample_paths,
|
||||||
) {
|
) {
|
||||||
Ok((new_stream, info, new_analysis, registry)) => {
|
Ok((new_stream, info, new_analysis, registry)) => {
|
||||||
_stream = Some(new_stream);
|
_stream = Some(new_stream);
|
||||||
|
|||||||
@@ -123,6 +123,10 @@ pub const DOCS: &[DocEntry] = &[
|
|||||||
"Recording",
|
"Recording",
|
||||||
include_str!("../../docs/tutorials/recording.md"),
|
include_str!("../../docs/tutorials/recording.md"),
|
||||||
),
|
),
|
||||||
|
Topic(
|
||||||
|
"Soundfonts",
|
||||||
|
include_str!("../../docs/tutorials/soundfont.md"),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn topic_count() -> usize {
|
pub fn topic_count() -> usize {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub enum PatternField {
|
|||||||
pub enum PatternPropsField {
|
pub enum PatternPropsField {
|
||||||
#[default]
|
#[default]
|
||||||
Name,
|
Name,
|
||||||
|
Description,
|
||||||
Length,
|
Length,
|
||||||
Speed,
|
Speed,
|
||||||
Quantization,
|
Quantization,
|
||||||
@@ -32,7 +33,8 @@ pub enum PatternPropsField {
|
|||||||
impl PatternPropsField {
|
impl PatternPropsField {
|
||||||
pub fn next(&self, follow_up_is_chain: bool) -> Self {
|
pub fn next(&self, follow_up_is_chain: bool) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::Name => Self::Length,
|
Self::Name => Self::Description,
|
||||||
|
Self::Description => Self::Length,
|
||||||
Self::Length => Self::Speed,
|
Self::Length => Self::Speed,
|
||||||
Self::Speed => Self::Quantization,
|
Self::Speed => Self::Quantization,
|
||||||
Self::Quantization => Self::SyncMode,
|
Self::Quantization => Self::SyncMode,
|
||||||
@@ -47,7 +49,8 @@ impl PatternPropsField {
|
|||||||
pub fn prev(&self, follow_up_is_chain: bool) -> Self {
|
pub fn prev(&self, follow_up_is_chain: bool) -> Self {
|
||||||
match self {
|
match self {
|
||||||
Self::Name => Self::Name,
|
Self::Name => Self::Name,
|
||||||
Self::Length => Self::Name,
|
Self::Description => Self::Name,
|
||||||
|
Self::Length => Self::Description,
|
||||||
Self::Speed => Self::Length,
|
Self::Speed => Self::Length,
|
||||||
Self::Quantization => Self::Speed,
|
Self::Quantization => Self::Speed,
|
||||||
Self::SyncMode => Self::Quantization,
|
Self::SyncMode => Self::Quantization,
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ pub enum RenameTarget {
|
|||||||
Bank { bank: usize },
|
Bank { bank: usize },
|
||||||
Pattern { bank: usize, pattern: usize },
|
Pattern { bank: usize, pattern: usize },
|
||||||
Step { bank: usize, pattern: usize, step: usize },
|
Step { bank: usize, pattern: usize, step: usize },
|
||||||
|
DescribePattern { bank: usize, pattern: usize },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenameTarget {
|
impl RenameTarget {
|
||||||
@@ -43,6 +44,9 @@ impl RenameTarget {
|
|||||||
Self::Bank { bank } => format!("Rename Bank {:02}", bank + 1),
|
Self::Bank { bank } => format!("Rename Bank {:02}", bank + 1),
|
||||||
Self::Pattern { bank, pattern } => format!("Rename {}", model::bp_label(*bank, *pattern)),
|
Self::Pattern { bank, pattern } => format!("Rename {}", model::bp_label(*bank, *pattern)),
|
||||||
Self::Step { step, .. } => format!("Name Step {:02}", step + 1),
|
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,
|
pattern: usize,
|
||||||
field: PatternPropsField,
|
field: PatternPropsField,
|
||||||
name: String,
|
name: String,
|
||||||
|
description: String,
|
||||||
length: String,
|
length: String,
|
||||||
speed: PatternSpeed,
|
speed: PatternSpeed,
|
||||||
quantization: LaunchQuantization,
|
quantization: LaunchQuantization,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ pub enum StagedMuteChange {
|
|||||||
|
|
||||||
pub struct StagedPropChange {
|
pub struct StagedPropChange {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
pub length: Option<usize>,
|
pub length: Option<usize>,
|
||||||
pub speed: PatternSpeed,
|
pub speed: PatternSpeed,
|
||||||
pub quantization: LaunchQuantization,
|
pub quantization: LaunchQuantization,
|
||||||
|
|||||||
@@ -55,13 +55,17 @@ pub fn bindings_for(page: Page, plugin_mode: bool) -> Vec<(&'static str, &'stati
|
|||||||
}
|
}
|
||||||
Page::Patterns => {
|
Page::Patterns => {
|
||||||
bindings.push(("←→↑↓", "Navigate", "Move between banks/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"));
|
bindings.push(("Enter", "Select", "Select pattern for editing"));
|
||||||
if !plugin_mode {
|
if !plugin_mode {
|
||||||
bindings.push(("Space", "Play", "Toggle pattern playback"));
|
bindings.push(("Space", "Play", "Toggle pattern playback"));
|
||||||
}
|
}
|
||||||
bindings.push(("Esc", "Back", "Clear staged or go back"));
|
bindings.push(("Esc", "Back", "Clear staged or go back"));
|
||||||
bindings.push(("c", "Commit", "Commit staged changes"));
|
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(("r", "Rename", "Rename bank/pattern"));
|
||||||
|
bindings.push(("d", "Describe", "Add description to pattern"));
|
||||||
bindings.push(("e", "Properties", "Edit pattern properties"));
|
bindings.push(("e", "Properties", "Edit pattern properties"));
|
||||||
bindings.push(("m", "Mute", "Stage mute for pattern"));
|
bindings.push(("m", "Mute", "Stage mute for pattern"));
|
||||||
bindings.push(("x", "Solo", "Stage solo 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+C", "Copy", "Copy bank/pattern"));
|
||||||
bindings.push(("Ctrl+V", "Paste", "Paste bank/pattern"));
|
bindings.push(("Ctrl+V", "Paste", "Paste bank/pattern"));
|
||||||
bindings.push(("Del", "Reset", "Reset 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 => {
|
Page::Engine => {
|
||||||
bindings.push(("Tab", "Section", "Next section"));
|
bindings.push(("Tab", "Section", "Next section"));
|
||||||
|
|||||||
@@ -496,13 +496,22 @@ fn render_patterns(frame: &mut Frame, app: &App, snapshot: &SequencerSnapshot, a
|
|||||||
height: 1,
|
height: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
let label = format!(
|
let right_label = format!(
|
||||||
"{} · {}",
|
"{} · {}",
|
||||||
pattern.quantization.label(),
|
pattern.quantization.label(),
|
||||||
pattern.sync_mode.label()
|
pattern.sync_mode.label()
|
||||||
);
|
);
|
||||||
let w = detail_area.width as usize;
|
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 filled_width = if is_playing {
|
||||||
let ratio = snapshot.get_smooth_progress(bank, idx, length, speed.multiplier()).unwrap_or(0.0);
|
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 pattern = &app.project_state.project.banks[bank].patterns[pattern_idx];
|
||||||
|
|
||||||
let name = pattern.name.as_deref().unwrap_or("-");
|
let name = pattern.name.as_deref().unwrap_or("-");
|
||||||
|
let desc = pattern.description.as_deref().unwrap_or("-");
|
||||||
let content_count = pattern.content_step_count();
|
let content_count = pattern.content_step_count();
|
||||||
let steps_label = format!("{}/{}", content_count, pattern.length);
|
let steps_label = format!("{}/{}", content_count, pattern.length);
|
||||||
let speed_label = pattern.speed.label();
|
let speed_label = pattern.speed.label();
|
||||||
@@ -739,6 +749,10 @@ fn render_properties(
|
|||||||
Span::styled(" Name ", label_style),
|
Span::styled(" Name ", label_style),
|
||||||
Span::styled(name, value_style),
|
Span::styled(name, value_style),
|
||||||
]),
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled(" Desc ", label_style),
|
||||||
|
Span::styled(desc, value_style),
|
||||||
|
]),
|
||||||
Line::from(vec![
|
Line::from(vec![
|
||||||
Span::styled(" Steps ", label_style),
|
Span::styled(" Steps ", label_style),
|
||||||
Span::styled(steps_label, value_style),
|
Span::styled(steps_label, value_style),
|
||||||
|
|||||||
@@ -650,6 +650,7 @@ fn render_modal(
|
|||||||
pattern,
|
pattern,
|
||||||
field,
|
field,
|
||||||
name,
|
name,
|
||||||
|
description,
|
||||||
length,
|
length,
|
||||||
speed,
|
speed,
|
||||||
quantization,
|
quantization,
|
||||||
@@ -660,7 +661,7 @@ fn render_modal(
|
|||||||
use crate::state::PatternPropsField;
|
use crate::state::PatternPropsField;
|
||||||
|
|
||||||
let is_chain = matches!(follow_up, FollowUp::Chain { .. });
|
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))
|
let inner = ModalFrame::new(&format!(" Pattern B{:02}:P{:02} ", bank + 1, pattern + 1))
|
||||||
.width(50)
|
.width(50)
|
||||||
@@ -678,6 +679,7 @@ fn render_modal(
|
|||||||
};
|
};
|
||||||
let mut fields: Vec<(&str, String, bool)> = vec![
|
let mut fields: Vec<(&str, String, bool)> = vec![
|
||||||
("Name", name.clone(), *field == PatternPropsField::Name),
|
("Name", name.clone(), *field == PatternPropsField::Name),
|
||||||
|
("Desc", description.clone(), *field == PatternPropsField::Description),
|
||||||
("Length", length.clone(), *field == PatternPropsField::Length),
|
("Length", length.clone(), *field == PatternPropsField::Length),
|
||||||
("Speed", speed_label, *field == PatternPropsField::Speed),
|
("Speed", speed_label, *field == PatternPropsField::Speed),
|
||||||
("Quantization", quantization.label().to_string(), *field == PatternPropsField::Quantization),
|
("Quantization", quantization.label().to_string(), *field == PatternPropsField::Quantization),
|
||||||
|
|||||||
Reference in New Issue
Block a user