From bc384f6041c6f77f8e624622ebeb9d4e484d1951 Mon Sep 17 00:00:00 2001 From: lobo Date: Thu, 9 Apr 2026 23:02:32 +0200 Subject: [PATCH] various updeitse --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/app.rs | 283 +++++++++++---- src/audio/drum_voice.rs | 221 +++++++++--- src/keys.rs | 2 + src/mouse.rs | 4 + src/presets/drum_presets.rs | 521 +++++++++++++++++++++++---- src/presets/pattern_presets.rs | 12 + src/presets/synth_pattern_presets.rs | 24 ++ src/sequencer/drum_pattern.rs | 164 +++++++-- src/sequencer/project.rs | 167 +++++---- src/ui/knobs.rs | 38 +- src/ui/layout.rs | 8 +- src/ui/mod.rs | 2 + 14 files changed, 1116 insertions(+), 334 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aee041d..a589934 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1654,7 +1654,7 @@ dependencies = [ [[package]] name = "textstep" -version = "2.2.0" +version = "3.0.0" dependencies = [ "cpal", "crossbeam-channel", diff --git a/Cargo.toml b/Cargo.toml index a6604bb..75f325b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textstep" -version = "2.2.0" +version = "3.0.0" edition = "2024" [dependencies] diff --git a/src/app.rs b/src/app.rs index 9ab839c..ad013d8 100644 --- a/src/app.rs +++ b/src/app.rs @@ -21,10 +21,10 @@ use crate::sequencer::transport::Transport; pub enum FocusSection { DrumGrid, Knobs, - SynthAGrid, // was: SynthGrid - SynthAControls, // was: SynthControls - SynthBGrid, // new - SynthBControls, // new + SynthAGrid, // was: SynthGrid + SynthAControls, // was: SynthControls + SynthBGrid, // new + SynthBControls, // new Transport, } @@ -32,8 +32,13 @@ impl FocusSection { pub fn next(&self, vis: &PanelVisibility) -> Self { use FocusSection::*; let order = [ - Transport, SynthAControls, SynthAGrid, - SynthBControls, SynthBGrid, DrumGrid, Knobs, + Transport, + SynthAControls, + SynthAGrid, + SynthBControls, + SynthBGrid, + DrumGrid, + Knobs, ]; let cur = order.iter().position(|s| s == self).unwrap_or(0); for i in 1..=order.len() { @@ -48,8 +53,13 @@ impl FocusSection { pub fn prev(&self, vis: &PanelVisibility) -> Self { use FocusSection::*; let order = [ - Transport, SynthAControls, SynthAGrid, - SynthBControls, SynthBGrid, DrumGrid, Knobs, + Transport, + SynthAControls, + SynthAGrid, + SynthBControls, + SynthBGrid, + DrumGrid, + Knobs, ]; let cur = order.iter().position(|s| s == self).unwrap_or(0); for i in 1..=order.len() { @@ -107,6 +117,8 @@ pub enum DrumControlField { Sweep, Color, Snap, + Shape, + Attack, // Amp page Filter, Drive, @@ -121,12 +133,14 @@ pub enum DrumControlField { Solo, } -/// All 11 continuous params in knobs panel order. -pub const KNOB_FIELDS: [DrumControlField; 11] = [ +/// All 13 continuous params in knobs panel order. +pub const KNOB_FIELDS: [DrumControlField; 13] = [ DrumControlField::Tune, DrumControlField::Sweep, DrumControlField::Color, DrumControlField::Snap, + DrumControlField::Shape, + DrumControlField::Attack, DrumControlField::Filter, DrumControlField::Drive, DrumControlField::Decay, @@ -161,7 +175,9 @@ impl DrumControlField { #[allow(dead_code)] pub fn page(self) -> Option { match self { - Self::Tune | Self::Sweep | Self::Color | Self::Snap => Some(ParamPage::Synth), + Self::Tune | Self::Sweep | Self::Color | Self::Snap | Self::Shape | Self::Attack => { + Some(ParamPage::Synth) + } Self::Filter | Self::Drive | Self::Decay | Self::Volume => Some(ParamPage::Amp), Self::SendReverb | Self::SendDelay | Self::Pan => Some(ParamPage::Fx), Self::Mute | Self::Solo => None, @@ -171,9 +187,31 @@ impl DrumControlField { /// Fields visible on the given page, in order. pub fn fields_for_page(page: ParamPage) -> &'static [DrumControlField] { match page { - ParamPage::Synth => &[Self::Tune, Self::Sweep, Self::Color, Self::Snap, Self::Mute, Self::Solo], - ParamPage::Amp => &[Self::Filter, Self::Drive, Self::Decay, Self::Volume, Self::Mute, Self::Solo], - ParamPage::Fx => &[Self::SendReverb, Self::SendDelay, Self::Pan, Self::Mute, Self::Solo], + ParamPage::Synth => &[ + Self::Tune, + Self::Sweep, + Self::Color, + Self::Snap, + Self::Shape, + Self::Attack, + Self::Mute, + Self::Solo, + ], + ParamPage::Amp => &[ + Self::Filter, + Self::Drive, + Self::Decay, + Self::Volume, + Self::Mute, + Self::Solo, + ], + ParamPage::Fx => &[ + Self::SendReverb, + Self::SendDelay, + Self::Pan, + Self::Mute, + Self::Solo, + ], } } @@ -278,7 +316,9 @@ impl SplashState { *row += *speed; // Cycle the character *ch = (*ch).wrapping_add(1); - if *ch < 33 || *ch > 126 { *ch = 33; } + if *ch < 33 || *ch > 126 { + *ch = 33; + } // Mark cells as revealed up to current row let cur_row = *row as usize; if cur_row < h { @@ -462,9 +502,9 @@ pub struct PanelVisibility { impl Default for PanelVisibility { fn default() -> Self { Self { - synth_a_knobs: false, // Knobs collapsed by default + synth_a_knobs: false, // Knobs collapsed by default synth_a_grid: true, - synth_b_knobs: false, // Knobs collapsed by default + synth_b_knobs: false, // Knobs collapsed by default synth_b_grid: true, drum_grid: true, drum_knobs: true, @@ -606,16 +646,26 @@ pub struct App { } impl App { - pub fn new(tx: Sender, rx: Receiver, display_buf: Arc) -> Self { + pub fn new( + tx: Sender, + rx: Receiver, + display_buf: Arc, + ) -> Self { let project = project::demo_project(); // Use first scene if available, otherwise default to index 0 let (dp, dk, sap, sak, sbp, sbk, scene_bpm, scene_swing) = if let Some(Some(scene)) = project.scenes.first() { - (scene.drum_pattern, scene.drum_kit, - scene.synth_a_pattern, scene.synth_a_kit, - scene.synth_b_pattern, scene.synth_b_kit, - Some(scene.bpm), Some(scene.swing)) + ( + scene.drum_pattern, + scene.drum_kit, + scene.synth_a_pattern, + scene.synth_a_kit, + scene.synth_b_pattern, + scene.synth_b_kit, + Some(scene.bpm), + Some(scene.swing), + ) } else { (0, 0, 0, 0, 0, 0, None, None) }; @@ -649,8 +699,14 @@ impl App { // Send initial state to audio thread so it has the pattern from the start let _ = tx.send(UiToAudio::SetTransport(transport)); let _ = tx.send(UiToAudio::SetDrumPattern(drum_pattern.clone())); - let _ = tx.send(UiToAudio::SetSynthPattern(SynthId::A, synth_a_pattern.clone())); - let _ = tx.send(UiToAudio::SetSynthPattern(SynthId::B, synth_b_pattern.clone())); + let _ = tx.send(UiToAudio::SetSynthPattern( + SynthId::A, + synth_a_pattern.clone(), + )); + let _ = tx.send(UiToAudio::SetSynthPattern( + SynthId::B, + synth_b_pattern.clone(), + )); let _ = tx.send(UiToAudio::SetEffectParams(EffectParams::default())); let mut ui = UiState::default(); @@ -829,7 +885,9 @@ impl App { /// Send current transport state to the audio thread. pub fn send_transport(&self) { - let _ = self.tx_to_audio.send(UiToAudio::SetTransport(self.transport)); + let _ = self + .tx_to_audio + .send(UiToAudio::SetTransport(self.transport)); } /// Send the full drum pattern to the audio thread. @@ -859,7 +917,9 @@ impl App { /// Get the name of the current active pattern. pub fn current_pattern_name(&self) -> &str { - self.project.patterns.get(self.ui.active_pattern) + self.project + .patterns + .get(self.ui.active_pattern) .map(|p| p.name.as_str()) .unwrap_or("?") } @@ -889,7 +949,8 @@ impl App { fn store_current_to_project(&mut self) { let idx = self.ui.active_pattern; self.project.save_pattern_steps(idx, &self.drum_pattern); - self.project.save_kit_from_pattern(self.ui.active_kit, &self.drum_pattern); + self.project + .save_kit_from_pattern(self.ui.active_kit, &self.drum_pattern); self.project.active_kit = self.ui.active_kit; self.project.bpm = self.transport.bpm; self.project.loop_length = self.transport.loop_config.drum_length; @@ -900,8 +961,10 @@ impl App { pat.bpm = self.transport.bpm; } // Save synth pattern and kit - self.project.save_synth_pattern(self.ui.synth_a.active_pattern, &self.synth_a_pattern); - self.project.save_synth_kit(self.ui.synth_a.active_kit, &self.synth_a_pattern.params); + self.project + .save_synth_pattern(self.ui.synth_a.active_pattern, &self.synth_a_pattern); + self.project + .save_synth_kit(self.ui.synth_a.active_kit, &self.synth_a_pattern.params); self.project.active_synth_pattern = self.ui.synth_a.active_pattern; self.project.active_synth_kit = self.ui.synth_a.active_kit; } @@ -915,7 +978,9 @@ impl App { /// Save the current state as a scene in the given slot. pub fn save_scene(&mut self, slot: usize) { - if slot >= project::NUM_SCENES { return; } + if slot >= project::NUM_SCENES { + return; + } let scene = project::Scene { name: format!("Scene {}", slot + 1), drum_pattern: self.ui.active_pattern, @@ -981,7 +1046,9 @@ impl App { /// Switch to a different pattern immediately. pub fn switch_pattern(&mut self, index: usize) { - if index >= NUM_PATTERNS { return; } + if index >= NUM_PATTERNS { + return; + } // Save current pattern first self.store_current_to_project(); // Load new pattern @@ -989,7 +1056,8 @@ impl App { self.project.active_pattern = index; // Clear steps and load from project self.drum_pattern.steps = [[false; 32]; NUM_DRUM_TRACKS]; - self.project.load_pattern_steps(index, &mut self.drum_pattern); + self.project + .load_pattern_steps(index, &mut self.drum_pattern); self.send_drum_pattern(); // Apply per-pattern BPM if set if let Some(pat) = self.project.patterns.get(index) { @@ -1002,7 +1070,9 @@ impl App { /// Queue a pattern to switch at end of current loop. pub fn queue_pattern(&mut self, index: usize) { - if index >= NUM_PATTERNS { return; } + if index >= NUM_PATTERNS { + return; + } if index == self.ui.active_pattern { // Pressing the same pattern cancels the queue self.ui.queued_pattern = None; @@ -1015,26 +1085,34 @@ impl App { /// Switch to a different synth pattern immediately. pub fn switch_synth_pattern(&mut self, index: usize) { - if index >= NUM_PATTERNS { return; } + if index >= NUM_PATTERNS { + return; + } // Save current synth pattern first - self.project.save_synth_pattern(self.ui.synth_a.active_pattern, &self.synth_a_pattern); + self.project + .save_synth_pattern(self.ui.synth_a.active_pattern, &self.synth_a_pattern); // Load new synth pattern self.ui.synth_a.active_pattern = index; self.project.active_synth_pattern = index; self.synth_a_pattern = SynthPattern::default(); - self.project.load_synth_pattern(index, &mut self.synth_a_pattern); + self.project + .load_synth_pattern(index, &mut self.synth_a_pattern); self.send_synth_pattern(SynthId::A); } /// Switch to a different synth kit immediately. pub fn switch_synth_kit(&mut self, index: usize) { - if index >= NUM_KITS { return; } + if index >= NUM_KITS { + return; + } // Save current synth kit params back - self.project.save_synth_kit(self.ui.synth_a.active_kit, &self.synth_a_pattern.params); + self.project + .save_synth_kit(self.ui.synth_a.active_kit, &self.synth_a_pattern.params); // Load new synth kit self.ui.synth_a.active_kit = index; self.project.active_synth_kit = index; - self.project.load_synth_kit(index, &mut self.synth_a_pattern); + self.project + .load_synth_kit(index, &mut self.synth_a_pattern); self.send_synth_pattern(SynthId::A); } @@ -1042,22 +1120,28 @@ impl App { /// Switch to a different synth pattern for the specified synth. pub fn switch_synth_pattern_for(&mut self, synth_id: SynthId, index: usize) { - if index >= NUM_PATTERNS { return; } + if index >= NUM_PATTERNS { + return; + } match synth_id { SynthId::A => { - self.project.save_synth_pattern(self.ui.synth_a.active_pattern, &self.synth_a_pattern); + self.project + .save_synth_pattern(self.ui.synth_a.active_pattern, &self.synth_a_pattern); self.ui.synth_a.active_pattern = index; self.project.active_synth_pattern = index; self.synth_a_pattern = SynthPattern::default(); - self.project.load_synth_pattern(index, &mut self.synth_a_pattern); + self.project + .load_synth_pattern(index, &mut self.synth_a_pattern); self.send_synth_pattern(SynthId::A); } SynthId::B => { - self.project.save_synth_b_pattern(self.ui.synth_b.active_pattern, &self.synth_b_pattern); + self.project + .save_synth_b_pattern(self.ui.synth_b.active_pattern, &self.synth_b_pattern); self.ui.synth_b.active_pattern = index; self.project.active_synth_b_pattern = index; self.synth_b_pattern = SynthPattern::default(); - self.project.load_synth_b_pattern(index, &mut self.synth_b_pattern); + self.project + .load_synth_b_pattern(index, &mut self.synth_b_pattern); self.send_synth_pattern(SynthId::B); } } @@ -1065,7 +1149,9 @@ impl App { /// Queue a synth pattern for the specified synth. pub fn queue_synth_pattern_for(&mut self, synth_id: SynthId, index: usize) { - if index >= NUM_PATTERNS { return; } + if index >= NUM_PATTERNS { + return; + } let ui = match synth_id { SynthId::A => &mut self.ui.synth_a, SynthId::B => &mut self.ui.synth_b, @@ -1079,20 +1165,26 @@ impl App { /// Switch to a different synth kit for the specified synth. pub fn switch_synth_kit_for(&mut self, synth_id: SynthId, index: usize) { - if index >= NUM_KITS { return; } + if index >= NUM_KITS { + return; + } match synth_id { SynthId::A => { - self.project.save_synth_kit(self.ui.synth_a.active_kit, &self.synth_a_pattern.params); + self.project + .save_synth_kit(self.ui.synth_a.active_kit, &self.synth_a_pattern.params); self.ui.synth_a.active_kit = index; self.project.active_synth_kit = index; - self.project.load_synth_kit(index, &mut self.synth_a_pattern); + self.project + .load_synth_kit(index, &mut self.synth_a_pattern); self.send_synth_pattern(SynthId::A); } SynthId::B => { - self.project.save_synth_b_kit(self.ui.synth_b.active_kit, &self.synth_b_pattern.params); + self.project + .save_synth_b_kit(self.ui.synth_b.active_kit, &self.synth_b_pattern.params); self.ui.synth_b.active_kit = index; self.project.active_synth_b_kit = index; - self.project.load_synth_b_kit(index, &mut self.synth_b_pattern); + self.project + .load_synth_b_kit(index, &mut self.synth_b_pattern); self.send_synth_pattern(SynthId::B); } } @@ -1115,7 +1207,8 @@ impl App { match project::save_project(&self.project, path) { Ok(()) => { self.dirty = false; - let name = path.file_stem() + let name = path + .file_stem() .and_then(|s| s.to_str()) .unwrap_or("project") .to_string(); @@ -1165,19 +1258,25 @@ impl App { /// Switch to a different kit immediately. pub fn switch_kit(&mut self, index: usize) { - if index >= NUM_KITS { return; } + if index >= NUM_KITS { + return; + } // Save current kit params back - self.project.save_kit_from_pattern(self.ui.active_kit, &self.drum_pattern); + self.project + .save_kit_from_pattern(self.ui.active_kit, &self.drum_pattern); // Load new kit self.ui.active_kit = index; self.project.active_kit = index; - self.project.apply_kit_to_pattern(index, &mut self.drum_pattern); + self.project + .apply_kit_to_pattern(index, &mut self.drum_pattern); self.send_drum_pattern(); } /// Get the name of the current active kit. pub fn current_kit_name(&self) -> &str { - self.project.kits.get(self.ui.active_kit) + self.project + .kits + .get(self.ui.active_kit) .map(|k| k.name.as_str()) .unwrap_or("?") } @@ -1235,7 +1334,8 @@ impl App { let name = kit.name.clone(); let idx = self.ui.active_kit; self.project.kits[idx] = kit; - self.project.apply_kit_to_pattern(idx, &mut self.drum_pattern); + self.project + .apply_kit_to_pattern(idx, &mut self.drum_pattern); self.send_drum_pattern(); self.dirty = true; self.show_status(format!("Kit loaded: {}", name)); @@ -1247,7 +1347,13 @@ impl App { // ── Preset Browser ───────────────────────────────────────────────── pub fn open_preset_browser(&mut self) { - let is_synth = matches!(self.ui.focus, FocusSection::SynthAGrid | FocusSection::SynthAControls | FocusSection::SynthBGrid | FocusSection::SynthBControls); + let is_synth = matches!( + self.ui.focus, + FocusSection::SynthAGrid + | FocusSection::SynthAControls + | FocusSection::SynthBGrid + | FocusSection::SynthBControls + ); let mut browser = if is_synth { crate::presets::PresetBrowserState::for_synth() } else { @@ -1263,7 +1369,11 @@ impl App { self.ui.modal = ModalState::PresetBrowser(browser); } - pub fn apply_drum_preset(&mut self, track: usize, params: crate::sequencer::project::DrumSoundParams) { + pub fn apply_drum_preset( + &mut self, + track: usize, + params: crate::sequencer::project::DrumSoundParams, + ) { let tp = params.to_track_params(); let mute = self.drum_pattern.params[track].mute; let solo = self.drum_pattern.params[track].solo; @@ -1275,7 +1385,13 @@ impl App { } pub fn open_pattern_browser(&mut self) { - let is_synth = matches!(self.ui.focus, FocusSection::SynthAGrid | FocusSection::SynthAControls | FocusSection::SynthBGrid | FocusSection::SynthBControls); + let is_synth = matches!( + self.ui.focus, + FocusSection::SynthAGrid + | FocusSection::SynthAControls + | FocusSection::SynthBGrid + | FocusSection::SynthBControls + ); let mut pb = if is_synth { crate::presets::PatternBrowserState::new_synth() } else { @@ -1311,10 +1427,14 @@ impl App { let src = hex_to_steps(hex); // Find the effective length of the source pattern (last active step) - let src_len = src.iter().rposition(|&s| s).map_or(16, |i| { - // Round up to nearest 8-step boundary - ((i / 8) + 1) * 8 - }).min(MAX_STEPS); + let src_len = src + .iter() + .rposition(|&s| s) + .map_or(16, |i| { + // Round up to nearest 8-step boundary + ((i / 8) + 1) * 8 + }) + .min(MAX_STEPS); // Build the tiled pattern let mut tiled = [false; MAX_STEPS]; @@ -1346,7 +1466,7 @@ impl App { preset_steps: &[(u8, u8, u8); crate::sequencer::synth_pattern::MAX_STEPS], merge: crate::presets::PatternMergeMode, ) { - use crate::sequencer::synth_pattern::{MAX_STEPS, SynthStep}; + use crate::sequencer::synth_pattern::{SynthStep, MAX_STEPS}; let pattern = match synth_id { SynthId::A => &mut self.synth_a_pattern, @@ -1360,20 +1480,30 @@ impl App { }; // Find effective source length (last non-rest step, rounded to 8) - let src_len = preset_steps.iter().rposition(|s| s.1 > 0).map_or(16, |i| { - ((i / 8) + 1) * 8 - }).min(MAX_STEPS); + let src_len = preset_steps + .iter() + .rposition(|s| s.1 > 0) + .map_or(16, |i| ((i / 8) + 1) * 8) + .min(MAX_STEPS); for s in 0..fill_len { let (note, vel, len) = preset_steps[s % src_len]; if vel > 0 { match merge { crate::presets::PatternMergeMode::Replace => { - pattern.steps[s] = SynthStep { note, velocity: vel, length: len }; + pattern.steps[s] = SynthStep { + note, + velocity: vel, + length: len, + }; } crate::presets::PatternMergeMode::Layer => { if !pattern.steps[s].is_active() { - pattern.steps[s] = SynthStep { note, velocity: vel, length: len }; + pattern.steps[s] = SynthStep { + note, + velocity: vel, + length: len, + }; } } } @@ -1385,7 +1515,11 @@ impl App { self.dirty = true; } - pub fn apply_synth_preset(&mut self, synth_id: SynthId, params: crate::sequencer::synth_pattern::SynthParams) { + pub fn apply_synth_preset( + &mut self, + synth_id: SynthId, + params: crate::sequencer::synth_pattern::SynthParams, + ) { let pattern = match synth_id { SynthId::A => &mut self.synth_a_pattern, SynthId::B => &mut self.synth_b_pattern, @@ -1417,7 +1551,8 @@ impl App { let idx = self.project.active_pattern; self.ui.active_pattern = idx; self.drum_pattern = DrumPattern::default(); - self.project.apply_kit_to_pattern(kit_idx, &mut self.drum_pattern); + self.project + .apply_kit_to_pattern(kit_idx, &mut self.drum_pattern); self.project.load_pattern_steps(idx, &mut self.drum_pattern); self.send_drum_pattern(); @@ -1428,8 +1563,10 @@ impl App { self.ui.synth_a.active_pattern = self.project.active_synth_pattern; self.ui.synth_a.active_kit = self.project.active_synth_kit; self.synth_a_pattern = SynthPattern::default(); - self.project.load_synth_pattern(self.ui.synth_a.active_pattern, &mut self.synth_a_pattern); - self.project.load_synth_kit(self.ui.synth_a.active_kit, &mut self.synth_a_pattern); + self.project + .load_synth_pattern(self.ui.synth_a.active_pattern, &mut self.synth_a_pattern); + self.project + .load_synth_kit(self.ui.synth_a.active_kit, &mut self.synth_a_pattern); self.send_synth_pattern(SynthId::A); self.dirty = false; diff --git a/src/audio/drum_voice.rs b/src/audio/drum_voice.rs index 321492c..2d3db55 100644 --- a/src/audio/drum_voice.rs +++ b/src/audio/drum_voice.rs @@ -262,11 +262,13 @@ pub struct KickVoice { pitch_stage2: bool, // true once stage-1 envelope drops below threshold body_env: f32, // body amplitude envelope body_decay: f32, // per-sample body env decay - // Body LP filter - body_lp: OnePoleLP, - // Attack ramp: ~0.5ms linear rise to avoid DC click at onset + // Body filter: StateVariableFilter for resonance control + body_svf: StateVariableFilter, + body_lp_freq_base: f32, + // Amplitude envelopes + body_env_att: f32, // body attack envelope + body_env_att_inc: f32, // body attack ramp increment sample_count: u32, - attack_samples: u32, // Path B: click impulse → resonant BP/LP filter click_env: f32, // click amplitude envelope (fixed short decay) click_decay: f32, @@ -299,9 +301,7 @@ impl KickVoice { pitch_stage2: false, body_env: 0.0, body_decay: 0.0, - body_lp: OnePoleLP::new(), sample_count: 0, - attack_samples: (sr * 0.0005) as u32, // ~0.5ms click_env: 0.0, click_decay: 0.0, click_level: 0.5, @@ -315,6 +315,10 @@ impl KickVoice { sub_freq: 0.0, sub_env: 0.0, sub_decay: 0.0, + body_svf: StateVariableFilter::new(), + body_lp_freq_base: 0.0, + body_env_att: 0.0, + body_env_att_inc: 0.0, } } } @@ -327,8 +331,8 @@ impl DrumVoiceDsp for KickVoice { // ── Path A: sine body ──────────────────────────────────────── - // tune: fundamental freq 30-80 Hz - self.freq_base = 30.0 + p.tune * 50.0; + // tune: fundamental freq 20-120 Hz + self.freq_base = 20.0 + p.tune * 100.0; // sweep: pitch envelope depth (0-500 Hz above fundamental) self.freq_start = self.freq_base + p.sweep * 500.0; @@ -339,28 +343,33 @@ impl DrumVoiceDsp for KickVoice { // Color 0 → 5ms (tight thump), Color 1 → 200ms (long "zoop") self.pitch_env = 1.0; self.pitch_stage2 = false; + let shape_inv = 1.0 - p.shape; self.pitch_decay_fast = (-5.0_f64 / (0.0025 * self.sr)).exp() as f32; - let pitch_time_slow = 0.005 + p.color as f64 * 0.195; + let pitch_time_slow = (0.005 + p.color as f64 * 0.195) * (1.0 + shape_inv as f64 * 0.5); self.pitch_decay_slow = (-5.0_f64 / (pitch_time_slow * self.sr)).exp() as f32; self.pitch_decay = self.pitch_decay_fast; // start in stage 1 - // decay: body amplitude envelope (80-600ms) - let body_time = 0.08 + p.decay as f64 * 0.52; + // decay: body amplitude envelope (80-600ms), 808 shape gets ~40% longer body + let body_time = (0.08 + p.decay as f64 * 0.52) * (1.0 + shape_inv as f64 * 0.4); self.body_env = 1.0; self.body_decay = (-5.0_f64 / (body_time * self.sr)).exp() as f32; - // filter: body LP (500-8000 Hz), color opens it further (+2kHz at max) - let color_lp_boost = p.color * 2000.0; - let lp_freq = (500.0 + p.filter * 7500.0 + color_lp_boost).min(12000.0); - self.body_lp.set_freq(lp_freq, self.sr); - self.body_lp.prev_out = 0.0; + // Body LP filter + let body_lp_freq = 40.0 + p.filter * 760.0; // 40-800 Hz — actually sculpts kick body + let filter_reso = 0.1 + p.filter * 0.5; // resonance adds character + self.body_lp_freq_base = body_lp_freq; + self.body_svf.set_freq(body_lp_freq, filter_reso, self.sr); + self.body_svf.reset(); - // ── Path B: click impulse ──────────────────────────────────── + // Attack envelope + self.body_env_att = 0.0; + let attack_time = p.attack * 0.01; // 0-10ms + let attack_samps = (attack_time * self.sr as f32).max(1.0); + self.body_env_att_inc = 1.0 / attack_samps; - // snap: click level (the 909 "Attack" knob) - self.click_level = p.snap; + // Click level — 909 shape gets louder click + self.click_level = (0.3 + p.snap * 0.7) * (0.5 + p.shape * 0.5); - // Click envelope: fixed short decay (~4ms, like the 909) self.click_env = 1.0; self.click_decay = (-5.0_f64 / (0.004 * self.sr)).exp() as f32; @@ -377,8 +386,8 @@ impl DrumVoiceDsp for KickVoice { self.sub_freq = self.freq_base * 0.5; // octave below, not a fourth self.sub_phase = 0.0; - // Color 0 → heavy sub (0.35), Color 1 → minimal sub (0.10) - self.sub_env = 0.35 - p.color * 0.25; + // shape=0 (808) gets heavy sub ~0.40, shape=1 (909) minimal ~0.10, color still darkens + self.sub_env = (0.10 + shape_inv * 0.30) * (1.0 - p.color * 0.5); // Decay tracks body but slightly shorter — support without boom self.sub_decay = (-5.0_f64 / ((0.10 + p.decay as f64 * 0.3) * self.sr)).exp() as f32; @@ -420,13 +429,16 @@ impl DrumVoiceDsp for KickVoice { let harmonic2 = (self.phase * 2.0 * std::f64::consts::TAU).sin() as f32 * harm_amount; // Body LP filter + envelope - // Attack ramp: ~0.5ms linear rise to avoid DC click artifact at onset - let attack_amp = if self.sample_count < self.attack_samples { - self.sample_count as f32 / self.attack_samples as f32 - } else { - 1.0 - }; - let body = self.body_lp.tick(sine + harmonic2) * self.body_env * attack_amp; + // Attack ramp: linear rise to avoid DC click artifact at onset + let attack_amp = self.body_env_att.min(1.0); + if self.body_env_att < 1.0 { + self.body_env_att += self.body_env_att_inc; + } + // Filter follows pitch envelope for spectral evolution ("zoop → thud") + let dynamic_cutoff = self.body_lp_freq_base + (freq - self.freq_base) * 0.5; + self.body_svf.set_freq(dynamic_cutoff.max(20.0), 0.1 + self.drive * 0.3, self.sr); + self.body_svf.tick(sine + harmonic2); + let body = self.body_svf.lp() * self.body_env * attack_amp; self.body_env *= self.body_decay; // ── Path B: click impulse ── @@ -500,6 +512,8 @@ pub struct SnareVoice { drive: f32, active: bool, comb: CombFilter, + body_env_att: f32, + body_env_att_inc: f32, } impl SnareVoice { @@ -527,6 +541,8 @@ impl SnareVoice { drive: 0.0, active: false, comb: CombFilter::new(), + body_env_att: 0.0, + body_env_att_inc: 0.0, } } } @@ -543,31 +559,36 @@ impl DrumVoiceDsp for SnareVoice { self.pitch_decay = (-5.0_f64 / (0.02 * self.sr)).exp() as f32; // Body envelope: short, color extends it slightly (0.02-0.08s) - let body_time = 0.02 + p.color as f64 * 0.06; + let body_time = 0.02 + p.color as f64 * 0.04 + p.decay as f64 * 0.08; self.body_env = 1.0; self.body_decay = (-5.0_f64 / (body_time * self.sr)).exp() as f32; - // Noise envelope: controlled by decay param (0.05-0.4s) - let noise_time = 0.05 + p.decay as f64 * 0.35; + // Noise envelope: longer than body for snare sizzle + let noise_time = 0.04 + p.decay as f64 * 0.35; self.noise_env = 1.0; self.noise_decay = (-5.0_f64 / (noise_time * self.sr)).exp() as f32; - // color: tone/noise balance - self.tone_noise_mix = p.color; + // color + shape: tone-noise balance (shape adds warmth) + self.tone_noise_mix = 0.3 + p.color * 0.4 + p.shape * 0.1; - // snap: impact transient (~3ms noise burst) + // snap: impact transient (noise burst in first ~3ms) self.impact_samples = (self.sr * 0.003) as u32; self.impact_remaining = self.impact_samples; - self.impact_amp = 0.3 + p.snap * 0.7; + self.impact_amp = 0.5 + p.snap * 0.5; // filter: tightness/gate self.tightness = p.filter; - // Overall envelope for tightness gating - let overall_time = 0.05 + p.decay as f64 * 0.45; + // Overall envelope for tightness gating (shape opens it up) + let overall_time = 0.05 + p.decay as f64 * 0.45 + p.shape as f64 * 0.1; self.overall_env = 1.0; self.overall_decay = (-5.0_f64 / (overall_time * self.sr)).exp() as f32; + // Attack envelope + self.body_env_att = 0.0; + let att_samps = (p.attack * 0.01 * self.sr as f32).max(1.0); + self.body_env_att_inc = 1.0 / att_samps; + // Noise HP cutoff derived from tune (higher tuned = brighter wires) let hp_freq = 2000.0 + p.tune * 6000.0; self.hp.set_freq(hp_freq, self.sr); @@ -580,8 +601,9 @@ impl DrumVoiceDsp for SnareVoice { self.lp.prev_out = 0.0; // Shell resonance: comb filter tuned to 2x snare pitch for metallic ring + // shape increases resonance for fatter body let comb_freq = (120.0 + p.tune * 160.0) * 2.0; // ~240-560 Hz - let comb_fb = 0.3 + p.color * 0.3; // more color = more resonance + let comb_fb = (0.3 + p.color * 0.3) * (0.6 + p.shape * 0.4); self.comb.set(comb_freq, self.sr, comb_fb); self.drive = p.drive; @@ -627,10 +649,16 @@ impl DrumVoiceDsp for SnareVoice { let shaped = self.lp.tick(raw); let driven = apply_drive(shaped, self.drive); + // Attack ramp + let attack_amp = self.body_env_att.min(1.0); + if self.body_env_att < 1.0 { + self.body_env_att += self.body_env_att_inc; + } + // Tightness gate: pow(env, 1 + tightness * 3) self.overall_env *= self.overall_decay; let tight_env = self.overall_env.powf(1.0 + self.tightness * 3.0); - let out = driven * tight_env; + let out = driven * tight_env * attack_amp; if self.overall_env < 1e-6 { self.active = false; @@ -671,6 +699,8 @@ pub struct ClosedHiHatVoice { // Sizzle: high-shelf boost state sizzle_state: f32, sizzle_coeff: f32, + body_env_att: f32, + body_env_att_inc: f32, } impl ClosedHiHatVoice { @@ -699,6 +729,8 @@ impl ClosedHiHatVoice { transient_noise: Noise::new(789), sizzle_state: 0.0, sizzle_coeff: dt / (rc + dt), + body_env_att: 0.0, + body_env_att_inc: 0.0, } } } @@ -711,7 +743,8 @@ impl DrumVoiceDsp for ClosedHiHatVoice { self.base_freq = 300.0 + p.tune * 600.0; // color: cross-FM intensity (0 = clean shimmer, 1 = gritty/harsh) - self.fm_intensity = p.color * 1.5; + // shape adds metallic density + self.fm_intensity = p.color * 1.5 + p.shape * 0.5; // sweep: noise layer mix (0 = pure metallic, 1 = noisy/trashy) self.noise_mix = 0.3 + p.sweep * 0.7; @@ -741,6 +774,11 @@ impl DrumVoiceDsp for ClosedHiHatVoice { let transient_ms = 2.0; self.transient_decay = (-5.0_f64 / (transient_ms as f64 * 0.001 * self.sr)).exp() as f32; + // Attack envelope + self.body_env_att = 0.0; + let att_samps = (p.attack * 0.01 * self.sr as f32).max(1.0); + self.body_env_att_inc = 1.0 / att_samps; + self.drive = p.drive; self.active = true; } @@ -750,6 +788,12 @@ impl DrumVoiceDsp for ClosedHiHatVoice { return 0.0; } + // Attack ramp + let attack_amp = self.body_env_att.min(1.0); + if self.body_env_att < 1.0 { + self.body_env_att += self.body_env_att_inc; + } + let sr_recip = self.sr_recip; // --- 6-oscillator metallic bank with cross-FM --- @@ -805,7 +849,7 @@ impl DrumVoiceDsp for ClosedHiHatVoice { let sizzle_in = driven + transient; self.sizzle_state += self.sizzle_coeff * (sizzle_in - self.sizzle_state); let hi_content = sizzle_in - self.sizzle_state; // HP = input - LP - let out = (sizzle_in + hi_content * 0.4) * self.env; // boost highs by ~40% + let out = (sizzle_in + hi_content * 0.4) * self.env * attack_amp; self.env *= self.env_decay; if self.env < 1e-6 { @@ -934,7 +978,8 @@ impl DrumVoiceDsp for OpenHiHatVoice { self.base_freq = 200.0 + p.tune * 600.0; // color: phase modulation depth (clean shimmer → gritty/harsh) - self.pm_depth = p.color * 1.5; + // shape adds extra grit for thicker character + self.pm_depth = p.color * 1.5 + p.shape * 0.5; // sweep: ring modulation depth (pure noise ↔ metallic coloring) // 0.0 = mostly noise (trashy/white), 1.0 = strong metallic ring @@ -948,9 +993,10 @@ impl DrumVoiceDsp for OpenHiHatVoice { self.env_hf = 1.0; self.env_hf_decay = (-5.0_f64 / (hf_time as f64 * self.sr)).exp() as f32; - // Attack ramp (~0.5ms) + // Attack ramp (controlled by attack param: 0-10ms) self.attack_env = 0.0; - self.attack_inc = 1.0 / (0.0005 * self.sr as f32); + let att_time = (p.attack * 0.01).max(1.0 / self.sr as f32); + self.attack_inc = 1.0 / (att_time * self.sr as f32); // snap: stick transient (~3ms noise burst) self.snap_env = p.snap * 1.0; @@ -1114,6 +1160,8 @@ pub struct RideVoice { svf: StateVariableFilter, drive: f32, active: bool, + body_env_att: f32, + body_env_att_inc: f32, } impl RideVoice { @@ -1137,6 +1185,8 @@ impl RideVoice { svf: StateVariableFilter::new(), drive: 0.0, active: false, + body_env_att: 0.0, + body_env_att_inc: 0.0, } } } @@ -1150,7 +1200,8 @@ impl DrumVoiceDsp for RideVoice { self.base_freq = 150.0 + p.tune * 450.0; // sweep: inharmonicity spread (0 = tight cluster, 1 = wide spread) - self.inharmonicity = p.sweep * 400.0; + // shape widens the spread for thicker wash + self.inharmonicity = p.sweep * 400.0 + p.shape * 150.0; // color: FM cross-modulation intensity (0 = clean shimmer, 1 = screaming) self.fm_intensity = p.color * 2.0; @@ -1183,6 +1234,11 @@ impl DrumVoiceDsp for RideVoice { self.svf.set_freq(lp_freq, 0.1, self.sr); self.svf.reset(); + // Attack envelope + self.body_env_att = 0.0; + let att_samps = (p.attack * 0.01 * self.sr as f32).max(1.0); + self.body_env_att_inc = 1.0 / att_samps; + self.drive = p.drive; self.active = true; } @@ -1192,6 +1248,12 @@ impl DrumVoiceDsp for RideVoice { return 0.0; } + // Attack ramp + let attack_amp = self.body_env_att.min(1.0); + if self.body_env_att < 1.0 { + self.body_env_att += self.body_env_att_inc; + } + let sr_recip = self.sr_recip; // --- 6-oscillator metallic bank with cross-FM --- @@ -1261,7 +1323,7 @@ impl DrumVoiceDsp for RideVoice { self.svf.tick(hp_out); let filtered = self.svf.lp(); - let driven = apply_drive(filtered, self.drive); + let driven = apply_drive(filtered, self.drive) * attack_amp; // Use body envelope for overall gate (it's the longest) if self.env_body < 1e-6 { @@ -1302,6 +1364,8 @@ pub struct ClapVoice { noise: Noise, drive: f32, active: bool, + body_env_att: f32, + body_env_att_inc: f32, } impl ClapVoice { @@ -1335,6 +1399,8 @@ impl ClapVoice { noise: Noise::new(654), drive: 0.0, active: false, + body_env_att: 0.0, + body_env_att_inc: 0.0, } } } @@ -1361,8 +1427,8 @@ impl DrumVoiceDsp for ClapVoice { // snap: burst count (3-6) self.burst_count = 3 + (p.snap * 3.0).round() as u32; - // Fixed burst spacing ~4ms - let spacing = 0.004; + // Burst spacing: shape widens for roomier feel (3-6ms) + let spacing = 0.003 + p.shape as f64 * 0.003; self.burst_on_samples = (self.sr * spacing) as u32; self.burst_off_samples = (self.sr * spacing) as u32; @@ -1376,6 +1442,11 @@ impl DrumVoiceDsp for ClapVoice { self.punch_samples = (self.sr * 0.01) as u32; // 10ms window self.punch_remaining = self.punch_samples; + // Attack envelope + self.body_env_att = 0.0; + let att_samps = (p.attack * 0.01 * self.sr as f32).max(1.0); + self.body_env_att_inc = 1.0 / att_samps; + self.drive = p.drive; self.active = true; } @@ -1385,6 +1456,12 @@ impl DrumVoiceDsp for ClapVoice { return 0.0; } + // Attack ramp + let attack_amp = self.body_env_att.min(1.0); + if self.body_env_att < 1.0 { + self.body_env_att += self.body_env_att_inc; + } + // Noise generation with color blend let white = self.noise.next(); self.pink_state += self.pink_lp_coeff * (white - self.pink_state); @@ -1434,7 +1511,7 @@ impl DrumVoiceDsp for ClapVoice { }; let driven = apply_drive(filtered * punch_gain, self.drive); - let out = driven * self.env * burst_gate; + let out = driven * self.env * burst_gate * attack_amp; if !self.in_burst_phase { self.env *= self.env_decay; @@ -1467,6 +1544,8 @@ pub struct CowbellVoice { noise: Noise, drive: f32, active: bool, + body_env_att: f32, + body_env_att_inc: f32, } impl CowbellVoice { @@ -1487,6 +1566,8 @@ impl CowbellVoice { noise: Noise::new(987), drive: 0.0, active: false, + body_env_att: 0.0, + body_env_att_inc: 0.0, } } } @@ -1500,8 +1581,8 @@ impl DrumVoiceDsp for CowbellVoice { // sweep: detune amount between oscillators self.freq2 = self.freq1 * (1.3 + p.sweep * 0.4); - // color: pulse width of square waves - self.pulse_width = 0.3 + p.color * 0.4; + // color + shape: pulse width of square waves (shape adds warmth) + self.pulse_width = 0.3 + p.color * 0.3 + p.shape * 0.1; self.env = 1.0; let time = 0.05 + p.decay * 0.25; @@ -1519,6 +1600,11 @@ impl DrumVoiceDsp for CowbellVoice { self.hp.prev_out = 0.0; self.lp.prev_out = 0.0; + // Attack envelope + self.body_env_att = 0.0; + let att_samps = (p.attack * 0.01 * self.sr as f32).max(1.0); + self.body_env_att_inc = 1.0 / att_samps; + self.drive = p.drive; self.active = true; } @@ -1528,6 +1614,12 @@ impl DrumVoiceDsp for CowbellVoice { return 0.0; } + // Attack ramp + let attack_amp = self.body_env_att.min(1.0); + if self.body_env_att < 1.0 { + self.body_env_att += self.body_env_att_inc; + } + self.phase1 += self.freq1 as f64 / self.sr; if self.phase1 >= 1.0 { self.phase1 -= 1.0; @@ -1554,7 +1646,7 @@ impl DrumVoiceDsp for CowbellVoice { let sum = (sq1 + sq2) * 0.5 + snap; let filtered = self.lp.tick(self.hp.tick(sum)); let driven = apply_drive(filtered, self.drive); - let out = driven * self.env; + let out = driven * self.env * attack_amp; self.env *= self.env_decay; if self.env < 1e-6 { @@ -1596,6 +1688,8 @@ pub struct TomVoice { noise: Noise, drive: f32, active: bool, + body_env_att: f32, + body_env_att_inc: f32, } impl TomVoice { @@ -1623,6 +1717,8 @@ impl TomVoice { noise: Noise::new(555), drive: 0.0, active: false, + body_env_att: 0.0, + body_env_att_inc: 0.0, } } } @@ -1645,8 +1741,8 @@ impl DrumVoiceDsp for TomVoice { let time = 0.08 + p.decay * 0.42; self.env_decay = (-5.0_f64 / (time as f64 * self.sr)).exp() as f32; - // color: FM depth and grit - self.fm_depth = p.color * 1.2; + // color: FM depth and grit (shape adds boominess via reduced FM) + self.fm_depth = p.color * 1.2 * (1.0 - p.shape * 0.4); self.fm_feedback_amt = p.color * 0.25; self.mod_freq_ratio = 2.0 + p.color * 1.5; // FM envelope for attack character @@ -1667,6 +1763,11 @@ impl DrumVoiceDsp for TomVoice { self.lp.set_freq(lp_freq, self.sr); self.lp.prev_out = 0.0; + // Attack envelope + self.body_env_att = 0.0; + let att_samps = (p.attack * 0.01 * self.sr as f32).max(1.0); + self.body_env_att_inc = 1.0 / att_samps; + self.drive = p.drive; self.active = true; } @@ -1676,6 +1777,12 @@ impl DrumVoiceDsp for TomVoice { return 0.0; } + // Attack ramp + let attack_amp = self.body_env_att.min(1.0); + if self.body_env_att < 1.0 { + self.body_env_att += self.body_env_att_inc; + } + // Pitch envelope: cubed for punchier drop let pitch_env_shaped = self.pitch_env * self.pitch_env * self.pitch_env; let freq = self.freq_base * (1.0 + 0.8 * pitch_env_shaped); @@ -1718,7 +1825,7 @@ impl DrumVoiceDsp for TomVoice { let raw = shaped + noise + snap; let filtered = self.lp.tick(raw); let driven = apply_drive(filtered, self.drive); - let out = driven * self.env; + let out = driven * self.env * attack_amp; self.env *= self.env_decay; if self.env < 1e-6 { @@ -1759,6 +1866,8 @@ mod tests { sweep, color, snap: 0.45, + shape: 0.0, + attack: 0.1, filter: 0.7, drive: 0.20, decay: 0.45, diff --git a/src/keys.rs b/src/keys.rs index 417aea3..14abb5a 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -1394,6 +1394,8 @@ fn adjust_drum_field(app: &mut App, track: usize, delta: f32) { DrumControlField::Sweep => params.sweep = (params.sweep + delta).clamp(0.0, 1.0), DrumControlField::Color => params.color = (params.color + delta).clamp(0.0, 1.0), DrumControlField::Snap => params.snap = (params.snap + delta).clamp(0.0, 1.0), + DrumControlField::Shape => params.shape = (params.shape + delta).clamp(0.0, 1.0), + DrumControlField::Attack => params.attack = (params.attack + delta).clamp(0.0, 1.0), DrumControlField::Filter => params.filter = (params.filter + delta).clamp(0.0, 1.0), DrumControlField::Drive => params.drive = (params.drive + delta).clamp(0.0, 1.0), DrumControlField::Decay => params.decay = (params.decay + delta).clamp(0.0, 1.0), diff --git a/src/mouse.rs b/src/mouse.rs index a2153c3..44c24d8 100644 --- a/src/mouse.rs +++ b/src/mouse.rs @@ -859,6 +859,8 @@ fn get_param_value(params: &crate::sequencer::drum_pattern::DrumTrackParams, fie DrumControlField::Sweep => params.sweep, DrumControlField::Color => params.color, DrumControlField::Snap => params.snap, + DrumControlField::Shape => params.shape, + DrumControlField::Attack => params.attack, DrumControlField::Filter => params.filter, DrumControlField::Drive => params.drive, DrumControlField::Decay => params.decay, @@ -877,6 +879,8 @@ fn set_param_value(params: &mut crate::sequencer::drum_pattern::DrumTrackParams, DrumControlField::Sweep => params.sweep = v, DrumControlField::Color => params.color = v, DrumControlField::Snap => params.snap = v, + DrumControlField::Shape => params.shape = v, + DrumControlField::Attack => params.attack = v, DrumControlField::Filter => params.filter = v, DrumControlField::Drive => params.drive = v, DrumControlField::Decay => params.decay = v, diff --git a/src/presets/drum_presets.rs b/src/presets/drum_presets.rs index 5f5c821..f8b431e 100644 --- a/src/presets/drum_presets.rs +++ b/src/presets/drum_presets.rs @@ -1,12 +1,37 @@ //! Hand-crafted drum sound presets organized by category (808, 909, Acoustic, Lo-Fi, etc.). +use super::DrumSoundPreset; use crate::sequencer::drum_pattern::DrumTrackId; use crate::sequencer::project::DrumSoundParams; -use super::DrumSoundPreset; // Helper to construct DrumSoundParams concisely -const fn ds(tune: f32, sweep: f32, color: f32, snap: f32, filter: f32, drive: f32, decay: f32, volume: f32) -> DrumSoundParams { - DrumSoundParams { tune, sweep, color, snap, filter, drive, decay, volume, send_reverb: 0.0, send_delay: 0.0, pan: 0.5 } +const fn ds( + tune: f32, + sweep: f32, + color: f32, + snap: f32, + shape: f32, + attack: f32, + filter: f32, + drive: f32, + decay: f32, + volume: f32, +) -> DrumSoundParams { + DrumSoundParams { + tune, + sweep, + color, + snap, + shape, + attack, + filter, + drive, + decay, + volume, + send_reverb: 0.0, + send_delay: 0.0, + pan: 0.5, + } } // ── Kick Presets ───────────────────────────────────────────────────────────── @@ -20,123 +45,464 @@ const fn ds(tune: f32, sweep: f32, color: f32, snap: f32, filter: f32, drive: f3 // - 2nd harmonic: 5% (clean) → 25% + drive contribution pub static KICK_PRESETS: &[DrumSoundPreset] = &[ - // 808 — subharmonic at octave below; raise tune so sub stays audible - DrumSoundPreset { name: "Deep 808", category: "808", voice: DrumTrackId::Kick, params: ds(0.25, 0.65, 0.15, 0.10, 0.55, 0.18, 0.70, 0.78) }, - DrumSoundPreset { name: "Punchy 808", category: "808", voice: DrumTrackId::Kick, params: ds(0.30, 0.55, 0.20, 0.45, 0.70, 0.20, 0.45, 0.75) }, - DrumSoundPreset { name: "Sub 808", category: "808", voice: DrumTrackId::Kick, params: ds(0.22, 0.70, 0.10, 0.05, 0.35, 0.12, 0.80, 0.80) }, - DrumSoundPreset { name: "Short 808", category: "808", voice: DrumTrackId::Kick, params: ds(0.28, 0.50, 0.20, 0.35, 0.60, 0.20, 0.28, 0.75) }, - // 909 — BP click blend is sharper; ease snap slightly on softer presets - DrumSoundPreset { name: "Hard 909", category: "909", voice: DrumTrackId::Kick, params: ds(0.35, 0.45, 0.30, 0.55, 0.80, 0.28, 0.40, 0.80) }, - DrumSoundPreset { name: "Soft 909", category: "909", voice: DrumTrackId::Kick, params: ds(0.30, 0.40, 0.25, 0.25, 0.60, 0.15, 0.45, 0.72) }, - DrumSoundPreset { name: "Boom 909", category: "909", voice: DrumTrackId::Kick, params: ds(0.28, 0.60, 0.20, 0.40, 0.55, 0.20, 0.55, 0.75) }, - // Acoustic — bump drive for mid-range body (compensates lower harmonics at low drive) - DrumSoundPreset { name: "Tight Acoustic", category: "Acoustic", voice: DrumTrackId::Kick, params: ds(0.40, 0.30, 0.35, 0.65, 0.90, 0.18, 0.30, 0.75) }, - DrumSoundPreset { name: "Jazz Kick", category: "Acoustic", voice: DrumTrackId::Kick, params: ds(0.45, 0.20, 0.40, 0.45, 0.70, 0.12, 0.35, 0.68) }, - // Lo-Fi — drive already moderate-high, harmonics naturally present - DrumSoundPreset { name: "Dusty Kick", category: "Lo-Fi", voice: DrumTrackId::Kick, params: ds(0.30, 0.50, 0.45, 0.30, 0.45, 0.35, 0.50, 0.70) }, - DrumSoundPreset { name: "Tape Kick", category: "Lo-Fi", voice: DrumTrackId::Kick, params: ds(0.28, 0.55, 0.50, 0.18, 0.40, 0.45, 0.50, 0.68) }, - // Industrial — high drive benefits from asymmetric saturation; ease snap for BP blend - DrumSoundPreset { name: "Distorted Kick", category: "Industrial", voice: DrumTrackId::Kick, params: ds(0.30, 0.65, 0.60, 0.70, 0.90, 0.70, 0.35, 0.78) }, - DrumSoundPreset { name: "Metal Kick", category: "Industrial", voice: DrumTrackId::Kick, params: ds(0.40, 0.75, 0.70, 0.80, 1.00, 0.60, 0.30, 0.75) }, - // Minimal — short decay, BP click gives crisper transient; ease snap slightly - DrumSoundPreset { name: "Click Kick", category: "Minimal", voice: DrumTrackId::Kick, params: ds(0.35, 0.30, 0.10, 0.70, 0.70, 0.10, 0.18, 0.72) }, - DrumSoundPreset { name: "Micro Kick", category: "Minimal", voice: DrumTrackId::Kick, params: ds(0.40, 0.20, 0.15, 0.50, 0.50, 0.05, 0.12, 0.68) }, + // 808 — subharmonic at octave below; shape=0 for full 808 boom + // Filter range: 40-800 Hz (sculpts body tone). Shape: 0=808 boom, 1=909 tight. + DrumSoundPreset { + name: "Deep 808", + category: "808", + voice: DrumTrackId::Kick, + params: ds(0.25, 0.65, 0.15, 0.10, 0.0, 0.1, 0.65, 0.18, 0.70, 0.78), + }, + DrumSoundPreset { + name: "Punchy 808", + category: "808", + voice: DrumTrackId::Kick, + params: ds(0.30, 0.55, 0.20, 0.45, 0.15, 0.15, 0.75, 0.20, 0.45, 0.75), + }, + DrumSoundPreset { + name: "Sub 808", + category: "808", + voice: DrumTrackId::Kick, + params: ds(0.20, 0.70, 0.10, 0.05, 0.0, 0.05, 0.50, 0.12, 0.80, 0.80), + }, + DrumSoundPreset { + name: "Short 808", + category: "808", + voice: DrumTrackId::Kick, + params: ds(0.28, 0.50, 0.20, 0.35, 0.1, 0.08, 0.70, 0.20, 0.28, 0.75), + }, + // 909 — shape=0.7-0.9 for tight click character + DrumSoundPreset { + name: "Hard 909", + category: "909", + voice: DrumTrackId::Kick, + params: ds(0.28, 0.45, 0.30, 0.55, 0.9, 0.02, 0.90, 0.28, 0.40, 0.80), + }, + DrumSoundPreset { + name: "Soft 909", + category: "909", + voice: DrumTrackId::Kick, + params: ds(0.25, 0.40, 0.25, 0.25, 0.7, 0.05, 0.75, 0.15, 0.45, 0.72), + }, + DrumSoundPreset { + name: "Boom 909", + category: "909", + voice: DrumTrackId::Kick, + params: ds(0.24, 0.60, 0.20, 0.40, 0.5, 0.08, 0.70, 0.20, 0.55, 0.75), + }, + // Acoustic — tight body, shape high for snappy transient + DrumSoundPreset { + name: "Tight Acoustic", + category: "Acoustic", + voice: DrumTrackId::Kick, + params: ds(0.30, 0.30, 0.35, 0.65, 0.85, 0.05, 0.85, 0.18, 0.30, 0.75), + }, + DrumSoundPreset { + name: "Jazz Kick", + category: "Acoustic", + voice: DrumTrackId::Kick, + params: ds(0.32, 0.20, 0.40, 0.45, 0.7, 0.08, 0.80, 0.12, 0.35, 0.68), + }, + // Lo-Fi — filter low for muffled vintage character + DrumSoundPreset { + name: "Dusty Kick", + category: "Lo-Fi", + voice: DrumTrackId::Kick, + params: ds(0.25, 0.50, 0.45, 0.30, 0.3, 0.12, 0.45, 0.35, 0.50, 0.70), + }, + DrumSoundPreset { + name: "Tape Kick", + category: "Lo-Fi", + voice: DrumTrackId::Kick, + params: ds(0.24, 0.55, 0.50, 0.18, 0.2, 0.18, 0.40, 0.45, 0.50, 0.68), + }, + // Industrial — filter wide open, heavy drive + DrumSoundPreset { + name: "Distorted Kick", + category: "Industrial", + voice: DrumTrackId::Kick, + params: ds(0.25, 0.65, 0.60, 0.70, 0.8, 0.02, 0.95, 0.70, 0.35, 0.78), + }, + DrumSoundPreset { + name: "Metal Kick", + category: "Industrial", + voice: DrumTrackId::Kick, + params: ds(0.30, 0.75, 0.70, 0.80, 1.0, 0.02, 1.00, 0.60, 0.30, 0.75), + }, + // Minimal — clean, shape high for tight transient + DrumSoundPreset { + name: "Click Kick", + category: "Minimal", + voice: DrumTrackId::Kick, + params: ds(0.28, 0.30, 0.10, 0.70, 0.9, 0.02, 0.80, 0.10, 0.18, 0.72), + }, + DrumSoundPreset { + name: "Micro Kick", + category: "Minimal", + voice: DrumTrackId::Kick, + params: ds(0.30, 0.20, 0.15, 0.50, 0.8, 0.03, 0.65, 0.05, 0.12, 0.68), + }, ]; // ── Snare Presets ──────────────────────────────────────────────────────────── pub static SNARE_PRESETS: &[DrumSoundPreset] = &[ // 808 — comb filter adds body, reduce color to tame resonance - DrumSoundPreset { name: "Classic 808", category: "808", voice: DrumTrackId::Snare, params: ds(0.35, 0.15, 0.40, 0.40, 0.50, 0.10, 0.40, 0.78) }, - DrumSoundPreset { name: "Rimshot 808", category: "808", voice: DrumTrackId::Snare, params: ds(0.50, 0.05, 0.25, 0.70, 0.70, 0.15, 0.25, 0.78) }, - DrumSoundPreset { name: "Noisy 808", category: "808", voice: DrumTrackId::Snare, params: ds(0.30, 0.20, 0.55, 0.30, 0.40, 0.20, 0.50, 0.72) }, + DrumSoundPreset { + name: "Classic 808", + category: "808", + voice: DrumTrackId::Snare, + params: ds(0.35, 0.15, 0.40, 0.40, 0.2, 0.12, 0.50, 0.10, 0.40, 0.78), + }, + DrumSoundPreset { + name: "Rimshot 808", + category: "808", + voice: DrumTrackId::Snare, + params: ds(0.50, 0.05, 0.25, 0.70, 0.3, 0.1, 0.70, 0.15, 0.15, 0.78), + }, + DrumSoundPreset { + name: "Noisy 808", + category: "808", + voice: DrumTrackId::Snare, + params: ds(0.30, 0.20, 0.55, 0.30, 0.1, 0.15, 0.40, 0.20, 0.50, 0.72), + }, // 909 - DrumSoundPreset { name: "Crack 909", category: "909", voice: DrumTrackId::Snare, params: ds(0.45, 0.10, 0.50, 0.60, 0.65, 0.18, 0.35, 0.80) }, - DrumSoundPreset { name: "Fat 909", category: "909", voice: DrumTrackId::Snare, params: ds(0.40, 0.15, 0.45, 0.45, 0.55, 0.20, 0.45, 0.78) }, + DrumSoundPreset { + name: "Crack 909", + category: "909", + voice: DrumTrackId::Snare, + params: ds(0.45, 0.10, 0.50, 0.60, 0.5, 0.1, 0.65, 0.18, 0.22, 0.80), + }, + DrumSoundPreset { + name: "Fat 909", + category: "909", + voice: DrumTrackId::Snare, + params: ds(0.40, 0.15, 0.45, 0.45, 0.3, 0.12, 0.55, 0.20, 0.45, 0.78), + }, // Acoustic — comb gives natural shell resonance - DrumSoundPreset { name: "Tight Snare", category: "Acoustic", voice: DrumTrackId::Snare, params: ds(0.50, 0.05, 0.38, 0.65, 0.75, 0.10, 0.30, 0.78) }, - DrumSoundPreset { name: "Brush Snare", category: "Acoustic", voice: DrumTrackId::Snare, params: ds(0.45, 0.00, 0.55, 0.20, 0.50, 0.00, 0.35, 0.62) }, + DrumSoundPreset { + name: "Tight Snare", + category: "Acoustic", + voice: DrumTrackId::Snare, + params: ds(0.50, 0.05, 0.38, 0.65, 0.4, 0.1, 0.75, 0.10, 0.15, 0.78), + }, + DrumSoundPreset { + name: "Brush Snare", + category: "Acoustic", + voice: DrumTrackId::Snare, + params: ds(0.45, 0.00, 0.55, 0.20, 0.2, 0.15, 0.50, 0.00, 0.35, 0.62), + }, // Lo-Fi - DrumSoundPreset { name: "Crunchy Snare", category: "Lo-Fi", voice: DrumTrackId::Snare, params: ds(0.40, 0.10, 0.52, 0.50, 0.45, 0.45, 0.40, 0.72) }, - DrumSoundPreset { name: "Vinyl Snare", category: "Lo-Fi", voice: DrumTrackId::Snare, params: ds(0.35, 0.15, 0.48, 0.35, 0.40, 0.30, 0.45, 0.68) }, + DrumSoundPreset { + name: "Crunchy Snare", + category: "Lo-Fi", + voice: DrumTrackId::Snare, + params: ds(0.40, 0.10, 0.52, 0.50, 0.3, 0.2, 0.45, 0.45, 0.40, 0.72), + }, + DrumSoundPreset { + name: "Vinyl Snare", + category: "Lo-Fi", + voice: DrumTrackId::Snare, + params: ds(0.35, 0.15, 0.48, 0.35, 0.2, 0.18, 0.40, 0.30, 0.45, 0.68), + }, // Industrial — keep high color for aggressive resonance - DrumSoundPreset { name: "Noise Blast", category: "Industrial", voice: DrumTrackId::Snare, params: ds(0.30, 0.20, 0.75, 0.80, 0.80, 0.65, 0.30, 0.80) }, + DrumSoundPreset { + name: "Noise Blast", + category: "Industrial", + voice: DrumTrackId::Snare, + params: ds(0.30, 0.20, 0.75, 0.80, 0.8, 0.25, 0.80, 0.65, 0.30, 0.80), + }, // Minimal — low color = minimal comb effect - DrumSoundPreset { name: "Click Snare", category: "Minimal", voice: DrumTrackId::Snare, params: ds(0.55, 0.00, 0.25, 0.80, 0.80, 0.05, 0.15, 0.72) }, - DrumSoundPreset { name: "Ghost Snare", category: "Minimal", voice: DrumTrackId::Snare, params: ds(0.40, 0.05, 0.40, 0.15, 0.45, 0.00, 0.20, 0.48) }, + DrumSoundPreset { + name: "Click Snare", + category: "Minimal", + voice: DrumTrackId::Snare, + params: ds(0.55, 0.00, 0.25, 0.80, 0.9, 0.05, 0.80, 0.05, 0.08, 0.72), + }, + DrumSoundPreset { + name: "Ghost Snare", + category: "Minimal", + voice: DrumTrackId::Snare, + params: ds(0.40, 0.05, 0.40, 0.15, 0.1, 0.08, 0.45, 0.00, 0.20, 0.48), + }, ]; // ── Closed Hi-Hat Presets ──────────────────────────────────────────────────── pub static CHH_PRESETS: &[DrumSoundPreset] = &[ // Transient burst + sizzle add attack and brightness — reduce snap/volume accordingly - DrumSoundPreset { name: "Tight 808", category: "808", voice: DrumTrackId::ClosedHiHat, params: ds(0.60, 0.00, 0.50, 0.30, 0.65, 0.00, 0.08, 0.65) }, - DrumSoundPreset { name: "Sizzle 909", category: "909", voice: DrumTrackId::ClosedHiHat, params: ds(0.55, 0.00, 0.55, 0.28, 0.70, 0.10, 0.12, 0.65) }, - DrumSoundPreset { name: "Crisp Hat", category: "Acoustic", voice: DrumTrackId::ClosedHiHat, params: ds(0.70, 0.00, 0.40, 0.40, 0.80, 0.05, 0.06, 0.60) }, - DrumSoundPreset { name: "Dark Hat", category: "Lo-Fi", voice: DrumTrackId::ClosedHiHat, params: ds(0.45, 0.00, 0.60, 0.20, 0.40, 0.30, 0.10, 0.62) }, - DrumSoundPreset { name: "Gritty Hat", category: "Industrial", voice: DrumTrackId::ClosedHiHat, params: ds(0.50, 0.10, 0.70, 0.45, 0.55, 0.45, 0.08, 0.65) }, - DrumSoundPreset { name: "Thin Hat", category: "Minimal", voice: DrumTrackId::ClosedHiHat, params: ds(0.75, 0.00, 0.30, 0.15, 0.90, 0.00, 0.05, 0.52) }, - DrumSoundPreset { name: "Shaker", category: "Acoustic", voice: DrumTrackId::ClosedHiHat, params: ds(0.65, 0.00, 0.80, 0.10, 0.70, 0.00, 0.04, 0.55) }, - DrumSoundPreset { name: "Noisy Click", category: "Lo-Fi", voice: DrumTrackId::ClosedHiHat, params: ds(0.50, 0.05, 0.75, 0.40, 0.35, 0.35, 0.06, 0.60) }, + DrumSoundPreset { + name: "Tight 808", + category: "808", + voice: DrumTrackId::ClosedHiHat, + params: ds(0.60, 0.00, 0.50, 0.30, 0.4, 0.05, 0.65, 0.00, 0.08, 0.65), + }, + DrumSoundPreset { + name: "Sizzle 909", + category: "909", + voice: DrumTrackId::ClosedHiHat, + params: ds(0.55, 0.00, 0.55, 0.28, 0.5, 0.08, 0.70, 0.10, 0.12, 0.65), + }, + DrumSoundPreset { + name: "Crisp Hat", + category: "Acoustic", + voice: DrumTrackId::ClosedHiHat, + params: ds(0.70, 0.00, 0.40, 0.40, 0.6, 0.1, 0.80, 0.05, 0.06, 0.60), + }, + DrumSoundPreset { + name: "Dark Hat", + category: "Lo-Fi", + voice: DrumTrackId::ClosedHiHat, + params: ds(0.45, 0.00, 0.60, 0.20, 0.3, 0.15, 0.40, 0.30, 0.10, 0.62), + }, + DrumSoundPreset { + name: "Gritty Hat", + category: "Industrial", + voice: DrumTrackId::ClosedHiHat, + params: ds(0.50, 0.10, 0.70, 0.45, 0.7, 0.18, 0.55, 0.45, 0.08, 0.65), + }, + DrumSoundPreset { + name: "Thin Hat", + category: "Minimal", + voice: DrumTrackId::ClosedHiHat, + params: ds(0.75, 0.00, 0.30, 0.15, 0.8, 0.03, 0.90, 0.00, 0.05, 0.52), + }, + DrumSoundPreset { + name: "Shaker", + category: "Acoustic", + voice: DrumTrackId::ClosedHiHat, + params: ds(0.65, 0.00, 0.80, 0.10, 0.4, 0.05, 0.70, 0.00, 0.04, 0.55), + }, + DrumSoundPreset { + name: "Noisy Click", + category: "Lo-Fi", + voice: DrumTrackId::ClosedHiHat, + params: ds(0.50, 0.05, 0.75, 0.40, 0.2, 0.1, 0.35, 0.35, 0.06, 0.60), + }, ]; // ── Open Hi-Hat Presets ────────────────────────────────────────────────────── pub static OHH_PRESETS: &[DrumSoundPreset] = &[ // Transient burst + sizzle add brightness — reduce snap/volume accordingly - DrumSoundPreset { name: "Classic 808", category: "808", voice: DrumTrackId::OpenHiHat, params: ds(0.50, 0.60, 0.30, 0.25, 0.50, 0.00, 0.50, 0.65) }, - DrumSoundPreset { name: "Sizzle 909", category: "909", voice: DrumTrackId::OpenHiHat, params: ds(0.55, 0.50, 0.40, 0.28, 0.60, 0.10, 0.55, 0.65) }, - DrumSoundPreset { name: "Washy", category: "Acoustic", voice: DrumTrackId::OpenHiHat, params: ds(0.45, 0.70, 0.35, 0.15, 0.45, 0.00, 0.70, 0.60) }, - DrumSoundPreset { name: "Trash Open", category: "Industrial", voice: DrumTrackId::OpenHiHat, params: ds(0.40, 0.80, 0.60, 0.40, 0.70, 0.45, 0.45, 0.65) }, - DrumSoundPreset { name: "Short Open", category: "Minimal", voice: DrumTrackId::OpenHiHat, params: ds(0.55, 0.40, 0.30, 0.20, 0.55, 0.00, 0.30, 0.55) }, - DrumSoundPreset { name: "Lo-Fi Open", category: "Lo-Fi", voice: DrumTrackId::OpenHiHat, params: ds(0.45, 0.55, 0.50, 0.15, 0.35, 0.30, 0.55, 0.60) }, + DrumSoundPreset { + name: "Classic 808", + category: "808", + voice: DrumTrackId::OpenHiHat, + params: ds(0.50, 0.60, 0.30, 0.25, 0.2, 0.08, 0.50, 0.00, 0.50, 0.65), + }, + DrumSoundPreset { + name: "Sizzle 909", + category: "909", + voice: DrumTrackId::OpenHiHat, + params: ds(0.55, 0.50, 0.40, 0.28, 0.3, 0.1, 0.60, 0.10, 0.55, 0.65), + }, + DrumSoundPreset { + name: "Washy", + category: "Acoustic", + voice: DrumTrackId::OpenHiHat, + params: ds(0.45, 0.70, 0.35, 0.15, 0.1, 0.12, 0.45, 0.00, 0.70, 0.60), + }, + DrumSoundPreset { + name: "Trash Open", + category: "Industrial", + voice: DrumTrackId::OpenHiHat, + params: ds(0.40, 0.80, 0.60, 0.40, 0.6, 0.2, 0.70, 0.45, 0.45, 0.65), + }, + DrumSoundPreset { + name: "Short Open", + category: "Minimal", + voice: DrumTrackId::OpenHiHat, + params: ds(0.55, 0.40, 0.30, 0.20, 0.3, 0.05, 0.55, 0.00, 0.30, 0.55), + }, + DrumSoundPreset { + name: "Lo-Fi Open", + category: "Lo-Fi", + voice: DrumTrackId::OpenHiHat, + params: ds(0.45, 0.55, 0.50, 0.15, 0.2, 0.15, 0.35, 0.30, 0.55, 0.60), + }, ]; // ── Ride Presets ───────────────────────────────────────────────────────────── pub static RIDE_PRESETS: &[DrumSoundPreset] = &[ - DrumSoundPreset { name: "Bell Ride", category: "Acoustic", voice: DrumTrackId::Ride, params: ds(0.60, 0.00, 0.40, 0.20, 0.50, 0.00, 0.50, 0.60) }, - DrumSoundPreset { name: "Ping Ride", category: "Acoustic", voice: DrumTrackId::Ride, params: ds(0.70, 0.00, 0.30, 0.40, 0.65, 0.00, 0.40, 0.55) }, - DrumSoundPreset { name: "Dark Ride", category: "Lo-Fi", voice: DrumTrackId::Ride, params: ds(0.40, 0.00, 0.55, 0.10, 0.35, 0.20, 0.53, 0.55) }, - DrumSoundPreset { name: "Crash", category: "Acoustic", voice: DrumTrackId::Ride, params: ds(0.45, 0.10, 0.60, 0.30, 0.55, 0.10, 0.60, 0.65) }, - DrumSoundPreset { name: "Metal Ride", category: "Industrial", voice: DrumTrackId::Ride, params: ds(0.55, 0.05, 0.70, 0.50, 0.75, 0.40, 0.43, 0.60) }, - DrumSoundPreset { name: "Thin Ride", category: "Minimal", voice: DrumTrackId::Ride, params: ds(0.65, 0.00, 0.25, 0.15, 0.50, 0.00, 0.33, 0.50) }, + DrumSoundPreset { + name: "Bell Ride", + category: "Acoustic", + voice: DrumTrackId::Ride, + params: ds(0.60, 0.00, 0.40, 0.20, 0.3, 0.08, 0.50, 0.00, 0.50, 0.60), + }, + DrumSoundPreset { + name: "Ping Ride", + category: "Acoustic", + voice: DrumTrackId::Ride, + params: ds(0.70, 0.00, 0.30, 0.40, 0.5, 0.1, 0.65, 0.00, 0.40, 0.55), + }, + DrumSoundPreset { + name: "Dark Ride", + category: "Lo-Fi", + voice: DrumTrackId::Ride, + params: ds(0.40, 0.00, 0.55, 0.10, 0.2, 0.12, 0.35, 0.20, 0.53, 0.55), + }, + DrumSoundPreset { + name: "Crash", + category: "Acoustic", + voice: DrumTrackId::Ride, + params: ds(0.45, 0.10, 0.60, 0.30, 0.4, 0.15, 0.55, 0.10, 0.60, 0.65), + }, + DrumSoundPreset { + name: "Metal Ride", + category: "Industrial", + voice: DrumTrackId::Ride, + params: ds(0.55, 0.05, 0.70, 0.50, 0.7, 0.2, 0.75, 0.40, 0.43, 0.60), + }, + DrumSoundPreset { + name: "Thin Ride", + category: "Minimal", + voice: DrumTrackId::Ride, + params: ds(0.65, 0.00, 0.25, 0.15, 0.4, 0.05, 0.50, 0.00, 0.33, 0.50), + }, ]; // ── Clap Presets ───────────────────────────────────────────────────────────── pub static CLAP_PRESETS: &[DrumSoundPreset] = &[ - DrumSoundPreset { name: "Classic 808", category: "808", voice: DrumTrackId::Clap, params: ds(0.50, 0.30, 0.50, 0.50, 0.50, 0.10, 0.40, 0.70) }, - DrumSoundPreset { name: "Tight 909", category: "909", voice: DrumTrackId::Clap, params: ds(0.55, 0.25, 0.55, 0.60, 0.60, 0.15, 0.35, 0.75) }, - DrumSoundPreset { name: "Big Clap", category: "808", voice: DrumTrackId::Clap, params: ds(0.45, 0.40, 0.60, 0.40, 0.45, 0.20, 0.55, 0.75) }, - DrumSoundPreset { name: "Room Clap", category: "Acoustic", voice: DrumTrackId::Clap, params: ds(0.50, 0.35, 0.45, 0.55, 0.55, 0.05, 0.50, 0.70) }, - DrumSoundPreset { name: "Crushed Clap", category: "Industrial", voice: DrumTrackId::Clap, params: ds(0.45, 0.30, 0.70, 0.70, 0.70, 0.60, 0.35, 0.75) }, - DrumSoundPreset { name: "Snap", category: "Minimal", voice: DrumTrackId::Clap, params: ds(0.60, 0.10, 0.30, 0.80, 0.80, 0.00, 0.10, 0.65) }, - DrumSoundPreset { name: "Finger Snap", category: "Acoustic", voice: DrumTrackId::Clap, params: ds(0.65, 0.05, 0.25, 0.90, 0.85, 0.00, 0.08, 0.60) }, + DrumSoundPreset { + name: "Classic 808", + category: "808", + voice: DrumTrackId::Clap, + params: ds(0.50, 0.30, 0.50, 0.50, 0.4, 0.12, 0.50, 0.10, 0.40, 0.70), + }, + DrumSoundPreset { + name: "Tight 909", + category: "909", + voice: DrumTrackId::Clap, + params: ds(0.55, 0.25, 0.55, 0.60, 0.6, 0.1, 0.60, 0.15, 0.35, 0.75), + }, + DrumSoundPreset { + name: "Big Clap", + category: "808", + voice: DrumTrackId::Clap, + params: ds(0.45, 0.40, 0.60, 0.40, 0.5, 0.15, 0.45, 0.20, 0.55, 0.75), + }, + DrumSoundPreset { + name: "Room Clap", + category: "Acoustic", + voice: DrumTrackId::Clap, + params: ds(0.50, 0.35, 0.45, 0.55, 0.4, 0.1, 0.55, 0.05, 0.50, 0.70), + }, + DrumSoundPreset { + name: "Crushed Clap", + category: "Industrial", + voice: DrumTrackId::Clap, + params: ds(0.45, 0.30, 0.70, 0.70, 0.8, 0.2, 0.70, 0.60, 0.35, 0.75), + }, + DrumSoundPreset { + name: "Snap", + category: "Minimal", + voice: DrumTrackId::Clap, + params: ds(0.60, 0.10, 0.30, 0.80, 0.9, 0.05, 0.80, 0.00, 0.10, 0.65), + }, + DrumSoundPreset { + name: "Finger Snap", + category: "Acoustic", + voice: DrumTrackId::Clap, + params: ds(0.65, 0.05, 0.25, 0.90, 0.8, 0.08, 0.85, 0.00, 0.08, 0.60), + }, ]; // ── Cowbell Presets ────────────────────────────────────────────────────────── pub static COWBELL_PRESETS: &[DrumSoundPreset] = &[ - DrumSoundPreset { name: "Classic 808", category: "808", voice: DrumTrackId::Cowbell, params: ds(0.50, 0.30, 0.50, 0.20, 0.50, 0.10, 0.40, 0.70) }, - DrumSoundPreset { name: "High Bell", category: "Acoustic", voice: DrumTrackId::Cowbell, params: ds(0.70, 0.20, 0.40, 0.30, 0.60, 0.05, 0.45, 0.65) }, - DrumSoundPreset { name: "Low Bell", category: "Acoustic", voice: DrumTrackId::Cowbell, params: ds(0.30, 0.40, 0.55, 0.15, 0.40, 0.10, 0.50, 0.65) }, - DrumSoundPreset { name: "Agogo", category: "Acoustic", voice: DrumTrackId::Cowbell, params: ds(0.60, 0.15, 0.35, 0.40, 0.70, 0.00, 0.35, 0.60) }, - DrumSoundPreset { name: "Clanky", category: "Industrial", voice: DrumTrackId::Cowbell, params: ds(0.55, 0.35, 0.65, 0.50, 0.55, 0.40, 0.30, 0.70) }, - DrumSoundPreset { name: "Muted Bell", category: "Minimal", voice: DrumTrackId::Cowbell, params: ds(0.50, 0.10, 0.30, 0.25, 0.45, 0.00, 0.20, 0.55) }, + DrumSoundPreset { + name: "Classic 808", + category: "808", + voice: DrumTrackId::Cowbell, + params: ds(0.50, 0.30, 0.50, 0.20, 0.3, 0.1, 0.50, 0.10, 0.40, 0.70), + }, + DrumSoundPreset { + name: "High Bell", + category: "Acoustic", + voice: DrumTrackId::Cowbell, + params: ds(0.70, 0.20, 0.40, 0.30, 0.5, 0.08, 0.60, 0.05, 0.45, 0.65), + }, + DrumSoundPreset { + name: "Low Bell", + category: "Acoustic", + voice: DrumTrackId::Cowbell, + params: ds(0.30, 0.40, 0.55, 0.15, 0.2, 0.12, 0.40, 0.10, 0.50, 0.65), + }, + DrumSoundPreset { + name: "Agogo", + category: "Acoustic", + voice: DrumTrackId::Cowbell, + params: ds(0.60, 0.15, 0.35, 0.40, 0.7, 0.05, 0.70, 0.00, 0.35, 0.60), + }, + DrumSoundPreset { + name: "Clanky", + category: "Industrial", + voice: DrumTrackId::Cowbell, + params: ds(0.55, 0.35, 0.65, 0.50, 0.6, 0.2, 0.55, 0.40, 0.30, 0.70), + }, + DrumSoundPreset { + name: "Muted Bell", + category: "Minimal", + voice: DrumTrackId::Cowbell, + params: ds(0.50, 0.10, 0.30, 0.25, 0.4, 0.05, 0.45, 0.00, 0.20, 0.55), + }, ]; // ── Tom Presets ────────────────────────────────────────────────────────────── pub static TOM_PRESETS: &[DrumSoundPreset] = &[ - DrumSoundPreset { name: "Deep 808 Tom", category: "808", voice: DrumTrackId::Tom, params: ds(0.30, 0.60, 0.10, 0.30, 0.70, 0.10, 0.55, 0.80) }, - DrumSoundPreset { name: "High 808 Tom", category: "808", voice: DrumTrackId::Tom, params: ds(0.60, 0.50, 0.10, 0.35, 0.75, 0.10, 0.45, 0.75) }, - DrumSoundPreset { name: "Floor Tom", category: "Acoustic", voice: DrumTrackId::Tom, params: ds(0.35, 0.45, 0.15, 0.40, 0.80, 0.05, 0.50, 0.80) }, - DrumSoundPreset { name: "Rack Tom", category: "Acoustic", voice: DrumTrackId::Tom, params: ds(0.55, 0.40, 0.15, 0.45, 0.85, 0.05, 0.40, 0.75) }, - DrumSoundPreset { name: "Roto Tom", category: "909", voice: DrumTrackId::Tom, params: ds(0.50, 0.70, 0.20, 0.50, 0.70, 0.20, 0.45, 0.75) }, - DrumSoundPreset { name: "Dirty Tom", category: "Lo-Fi", voice: DrumTrackId::Tom, params: ds(0.40, 0.55, 0.30, 0.25, 0.50, 0.40, 0.55, 0.70) }, - DrumSoundPreset { name: "Pipe Tom", category: "Industrial", voice: DrumTrackId::Tom, params: ds(0.45, 0.80, 0.50, 0.60, 0.60, 0.60, 0.40, 0.75) }, - DrumSoundPreset { name: "Bleep", category: "Minimal", voice: DrumTrackId::Tom, params: ds(0.65, 0.20, 0.05, 0.50, 0.60, 0.00, 0.25, 0.60) }, + DrumSoundPreset { + name: "Deep 808 Tom", + category: "808", + voice: DrumTrackId::Tom, + params: ds(0.30, 0.60, 0.10, 0.30, 0.2, 0.1, 0.70, 0.10, 0.55, 0.80), + }, + DrumSoundPreset { + name: "High 808 Tom", + category: "808", + voice: DrumTrackId::Tom, + params: ds(0.60, 0.50, 0.10, 0.35, 0.3, 0.12, 0.75, 0.10, 0.45, 0.75), + }, + DrumSoundPreset { + name: "Floor Tom", + category: "Acoustic", + voice: DrumTrackId::Tom, + params: ds(0.35, 0.45, 0.15, 0.40, 0.4, 0.15, 0.80, 0.05, 0.50, 0.80), + }, + DrumSoundPreset { + name: "Rack Tom", + category: "Acoustic", + voice: DrumTrackId::Tom, + params: ds(0.55, 0.40, 0.15, 0.45, 0.5, 0.12, 0.85, 0.05, 0.40, 0.75), + }, + DrumSoundPreset { + name: "Roto Tom", + category: "909", + voice: DrumTrackId::Tom, + params: ds(0.50, 0.70, 0.20, 0.50, 0.4, 0.18, 0.70, 0.20, 0.45, 0.75), + }, + DrumSoundPreset { + name: "Dirty Tom", + category: "Lo-Fi", + voice: DrumTrackId::Tom, + params: ds(0.40, 0.55, 0.30, 0.25, 0.3, 0.2, 0.50, 0.40, 0.55, 0.70), + }, + DrumSoundPreset { + name: "Pipe Tom", + category: "Industrial", + voice: DrumTrackId::Tom, + params: ds(0.45, 0.80, 0.50, 0.60, 0.8, 0.25, 0.60, 0.60, 0.40, 0.75), + }, + DrumSoundPreset { + name: "Bleep", + category: "Minimal", + voice: DrumTrackId::Tom, + params: ds(0.65, 0.20, 0.05, 0.50, 0.9, 0.08, 0.60, 0.00, 0.25, 0.60), + }, ]; // ── Lookup functions ───────────────────────────────────────────────────────── @@ -164,4 +530,3 @@ pub fn categories_for_voice(voice: DrumTrackId) -> Vec<&'static str> { } cats } - diff --git a/src/presets/pattern_presets.rs b/src/presets/pattern_presets.rs index ef6d34f..85d5cf4 100644 --- a/src/presets/pattern_presets.rs +++ b/src/presets/pattern_presets.rs @@ -31,6 +31,18 @@ pub static PATTERN_PRESETS: &[PatternPreset] = &[ PatternPreset { name: "Giorgio", genre: "Daft Punk", steps: ["88880000", "08080000", "ffff0000", "22220000", "00000000", "00000000", "00000000", "00000000"] }, + // ── Classics ──────────────────────────────────────────────────────── + PatternPreset { name: "Blue Monday", genre: "Classics", + steps: ["88880000", "08080000", "aaaa0000", "01010000", "00000000", "00000000", "00000000", "00000000"] }, + PatternPreset { name: "I Feel Love", genre: "Classics", + steps: ["88880000", "08080000", "ffff0000", "00000000", "00000000", "00000000", "00000000", "00000000"] }, + PatternPreset { name: "Firestarter", genre: "Classics", + steps: ["88a80000", "08080000", "aaaa0000", "01000000", "00000000", "08080000", "00000000", "00000000"] }, + PatternPreset { name: "Born Slippy", genre: "Classics", + steps: ["88880000", "08080000", "ffff0000", "04040000", "00000000", "00000000", "00000000", "00000000"] }, + PatternPreset { name: "Im Blue", genre: "Classics", + steps: ["88880000", "00000000", "aaaa0000", "10100000", "00000000", "08080000", "00000000", "00000000"] }, + // ── Basics ────────────────────────────────────────────────────────── // Progressive building blocks: offbeat hats (3,7,11,15) for proper techno feel PatternPreset { name: "Kick Only", genre: "Basics", diff --git a/src/presets/synth_pattern_presets.rs b/src/presets/synth_pattern_presets.rs index 7b50980..827b23e 100644 --- a/src/presets/synth_pattern_presets.rs +++ b/src/presets/synth_pattern_presets.rs @@ -46,6 +46,30 @@ pub static SYNTH_PATTERN_PRESETS: &[SynthPatternPreset] = &[ steps: [(60, 100, 1), (0, 0, 1), (63, 100, 1), (60, 100, 1), (0, 0, 1), (0, 0, 1), (60, 100, 1), (63, 100, 1), (60, 100, 1), (0, 0, 1), (67, 100, 1), (0, 0, 1), (63, 100, 1), (60, 100, 1), (0, 0, 1), (0, 0, 1), (60, 100, 1), (0, 0, 1), (63, 100, 1), (60, 100, 1), (0, 0, 1), (0, 0, 1), (60, 100, 1), (63, 100, 1), (60, 100, 1), (0, 0, 1), (67, 100, 1), (0, 0, 1), (63, 100, 1), (60, 100, 1), (0, 0, 1), (0, 0, 1)] }, SynthPatternPreset { name: "Digital Love Lead", genre: "Daft Punk Lead", steps: [(54, 100, 1), (0, 0, 1), (57, 100, 1), (0, 0, 1), (61, 100, 1), (0, 0, 1), (64, 100, 1), (0, 0, 1), (61, 100, 1), (0, 0, 1), (57, 100, 1), (0, 0, 1), (54, 100, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (54, 100, 1), (0, 0, 1), (57, 100, 1), (0, 0, 1), (61, 100, 1), (0, 0, 1), (64, 100, 1), (0, 0, 1), (61, 100, 1), (0, 0, 1), (57, 100, 1), (0, 0, 1), (54, 100, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)] }, + // ── Classics Bass ──────────────────────────────────────────────────── + // Bass lines for iconic tracks — pair with matching Classics Lead on Synth B + SynthPatternPreset { name: "Blue Monday Bass", genre: "Classics Bass", + steps: [(38,100,2),(0,0,1),(38,80,1),(0,0,1), (41,100,2),(0,0,1),(41,80,1),(0,0,1), (40,100,2),(0,0,1),(40,80,1),(0,0,1), (36,100,2),(0,0,1),(38,80,1),(0,0,1), (38,100,2),(0,0,1),(38,80,1),(0,0,1), (41,100,2),(0,0,1),(41,80,1),(0,0,1), (40,100,2),(0,0,1),(40,80,1),(0,0,1), (36,100,2),(0,0,1),(38,100,1),(0,0,1)] }, + SynthPatternPreset { name: "I Feel Love Bass", genre: "Classics Bass", + steps: [(45,100,1),(48,90,1),(52,100,1),(48,90,1), (45,100,1),(48,90,1),(52,100,1),(48,90,1), (45,100,1),(48,90,1),(52,100,1),(48,90,1), (45,100,1),(48,90,1),(52,100,1),(48,90,1), (45,100,1),(48,90,1),(52,100,1),(48,90,1), (45,100,1),(48,90,1),(52,100,1),(48,90,1), (45,100,1),(48,90,1),(52,100,1),(48,90,1), (45,100,1),(48,90,1),(52,100,1),(57,90,1)] }, + SynthPatternPreset { name: "Firestarter Bass", genre: "Classics Bass", + steps: [(40,127,1),(0,0,1),(40,100,1),(0,0,1), (40,127,1),(0,0,1),(0,0,1),(40,100,1), (40,127,1),(0,0,1),(40,100,1),(0,0,1), (40,127,1),(0,0,1),(0,0,1),(0,0,1), (40,127,1),(0,0,1),(40,100,1),(0,0,1), (40,127,1),(0,0,1),(0,0,1),(40,100,1), (40,127,1),(0,0,1),(40,100,1),(0,0,1), (40,127,1),(0,0,1),(43,100,1),(0,0,1)] }, + SynthPatternPreset { name: "Born Slippy Bass", genre: "Classics Bass", + steps: [(45,100,1),(0,0,1),(45,80,1),(0,0,1), (45,100,1),(0,0,1),(45,80,1),(0,0,1), (45,100,1),(0,0,1),(45,80,1),(0,0,1), (45,100,1),(0,0,1),(45,80,1),(0,0,1), (43,100,1),(0,0,1),(43,80,1),(0,0,1), (43,100,1),(0,0,1),(43,80,1),(0,0,1), (45,100,1),(0,0,1),(45,80,1),(0,0,1), (45,100,1),(0,0,1),(48,80,1),(0,0,1)] }, + SynthPatternPreset { name: "Im Blue Bass", genre: "Classics Bass", + steps: [(39,100,1),(0,0,1),(39,80,1),(0,0,1), (0,0,1),(0,0,1),(39,100,1),(0,0,1), (44,100,1),(0,0,1),(44,80,1),(0,0,1), (0,0,1),(0,0,1),(44,100,1),(0,0,1), (46,100,1),(0,0,1),(46,80,1),(0,0,1), (44,100,1),(0,0,1),(0,0,1),(0,0,1), (39,100,1),(0,0,1),(39,80,1),(0,0,1), (0,0,1),(0,0,1),(39,100,1),(0,0,1)] }, + // ── Classics Lead ─────────────────────────────────────────────────── + // Lead/melody lines for iconic tracks — pair with matching Classics Bass on Synth A + SynthPatternPreset { name: "Blue Monday Lead", genre: "Classics Lead", + steps: [(62,100,4),(0,0,1),(0,0,1),(0,0,1), (65,100,4),(0,0,1),(0,0,1),(0,0,1), (64,100,4),(0,0,1),(0,0,1),(0,0,1), (60,100,4),(0,0,1),(0,0,1),(0,0,1), (62,100,4),(0,0,1),(0,0,1),(0,0,1), (65,100,4),(0,0,1),(0,0,1),(0,0,1), (64,100,2),(0,0,1),(62,100,2),(0,0,1), (60,100,4),(0,0,1),(0,0,1),(0,0,1)] }, + SynthPatternPreset { name: "I Feel Love Lead", genre: "Classics Lead", + steps: [(69,100,4),(0,0,1),(0,0,1),(0,0,1), (72,100,4),(0,0,1),(0,0,1),(0,0,1), (76,100,4),(0,0,1),(0,0,1),(0,0,1), (72,100,4),(0,0,1),(0,0,1),(0,0,1), (69,100,4),(0,0,1),(0,0,1),(0,0,1), (72,100,4),(0,0,1),(0,0,1),(0,0,1), (76,100,4),(0,0,1),(0,0,1),(0,0,1), (72,100,2),(0,0,1),(69,100,2),(0,0,1)] }, + SynthPatternPreset { name: "Firestarter Lead", genre: "Classics Lead", + steps: [(64,127,1),(0,0,1),(64,100,1),(67,100,1), (64,127,1),(0,0,1),(0,0,1),(64,100,1), (62,100,1),(0,0,1),(64,127,1),(0,0,1), (67,100,1),(0,0,1),(64,100,1),(0,0,1), (64,127,1),(0,0,1),(64,100,1),(67,100,1), (64,127,1),(0,0,1),(0,0,1),(64,100,1), (62,100,1),(0,0,1),(64,127,1),(0,0,1), (67,100,1),(69,100,1),(67,100,1),(0,0,1)] }, + SynthPatternPreset { name: "Born Slippy Lead", genre: "Classics Lead", + steps: [(57,100,1),(0,0,1),(60,100,1),(0,0,1), (64,100,1),(0,0,1),(69,100,1),(0,0,1), (64,100,1),(0,0,1),(60,100,1),(0,0,1), (57,100,1),(0,0,1),(0,0,1),(0,0,1), (57,100,1),(0,0,1),(60,100,1),(0,0,1), (64,100,1),(0,0,1),(69,100,1),(0,0,1), (64,100,1),(0,0,1),(60,100,1),(0,0,1), (57,100,1),(0,0,1),(55,100,1),(0,0,1)] }, + SynthPatternPreset { name: "Im Blue Lead", genre: "Classics Lead", + steps: [(63,100,1),(0,0,1),(63,100,1),(66,100,1), (68,100,2),(0,0,1),(66,100,1),(0,0,1), (63,100,2),(0,0,1),(0,0,1),(0,0,1), (63,100,1),(66,100,1),(68,100,2),(0,0,1), (70,100,2),(0,0,1),(68,100,1),(0,0,1), (66,100,2),(0,0,1),(63,100,1),(0,0,1), (63,100,1),(0,0,1),(63,100,1),(66,100,1), (68,100,2),(0,0,1),(66,100,2),(0,0,1)] }, // ── Techno (S-1) ──────────────────────────────────────────────────────── SynthPatternPreset { name: "Techno 1", genre: "Techno", steps: [(48, 100, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1)] }, diff --git a/src/sequencer/drum_pattern.rs b/src/sequencer/drum_pattern.rs index f385ceb..2f0f91a 100644 --- a/src/sequencer/drum_pattern.rs +++ b/src/sequencer/drum_pattern.rs @@ -34,18 +34,20 @@ impl DrumTrackId { #[derive(Clone, Copy, Debug)] pub struct DrumTrackParams { // Synthesis - pub tune: f32, // 0.0..=1.0 Pitch / frequency center - pub sweep: f32, // 0.0..=1.0 Pitch envelope depth - pub color: f32, // 0.0..=1.0 Timbre: noise/tone balance, waveform shape - pub snap: f32, // 0.0..=1.0 Transient click/attack character + pub tune: f32, // 0.0..=1.0 Pitch / frequency center + pub sweep: f32, // 0.0..=1.0 Pitch envelope depth + pub color: f32, // 0.0..=1.0 Timbre: noise/tone balance, waveform shape + pub snap: f32, // 0.0..=1.0 Transient click/attack character + pub shape: f32, // 0.0..=1.0 Body shape selector: tight→big boom + pub attack: f32, // 0.0..=1.0 Attack envelope length: short→long // Filter / Shape - pub filter: f32, // 0.0..=1.0 Filter cutoff frequency - pub drive: f32, // 0.0..=1.0 Saturation / overdrive + pub filter: f32, // 0.0..=1.0 Filter cutoff frequency + pub drive: f32, // 0.0..=1.0 Saturation / overdrive // Amplitude - pub decay: f32, // 0.0..=1.0 Amplitude envelope decay time - pub volume: f32, // 0.0..=1.0 Track output level + pub decay: f32, // 0.0..=1.0 Amplitude envelope decay time + pub volume: f32, // 0.0..=1.0 Track output level // Send effects (FX page) pub send_reverb: f32, // 0.0..=1.0 Send level to reverb @@ -62,44 +64,140 @@ impl DrumTrackParams { pub fn defaults_for(track: DrumTrackId) -> Self { match track { DrumTrackId::Kick => Self { - tune: 0.3, sweep: 0.6, color: 0.2, snap: 0.45, - filter: 0.7, drive: 0.20, decay: 0.45, volume: 0.75, - send_reverb: 0.05, send_delay: 0.0, pan: 0.5, mute: false, solo: false, + tune: 0.3, + sweep: 0.6, + color: 0.2, + snap: 0.45, + shape: 0.0, + attack: 0.1, + filter: 0.7, + drive: 0.20, + decay: 0.45, + volume: 0.75, + send_reverb: 0.05, + send_delay: 0.0, + pan: 0.5, + mute: false, + solo: false, }, DrumTrackId::Snare => Self { - tune: 0.4, sweep: 0.1, color: 0.5, snap: 0.4, - filter: 0.5, drive: 0.1, decay: 0.4, volume: 0.75, - send_reverb: 0.15, send_delay: 0.0, pan: 0.5, mute: false, solo: false, + tune: 0.4, + sweep: 0.1, + color: 0.5, + snap: 0.4, + shape: 0.0, + attack: 0.1, + filter: 0.5, + drive: 0.1, + decay: 0.4, + volume: 0.75, + send_reverb: 0.15, + send_delay: 0.0, + pan: 0.5, + mute: false, + solo: false, }, DrumTrackId::ClosedHiHat => Self { - tune: 0.6, sweep: 0.0, color: 0.5, snap: 0.25, - filter: 0.6, drive: 0.0, decay: 0.1, volume: 0.65, - send_reverb: 0.05, send_delay: 0.0, pan: 0.5, mute: false, solo: false, + tune: 0.6, + sweep: 0.0, + color: 0.5, + snap: 0.25, + shape: 0.0, + attack: 0.05, + filter: 0.6, + drive: 0.0, + decay: 0.1, + volume: 0.65, + send_reverb: 0.05, + send_delay: 0.0, + pan: 0.5, + mute: false, + solo: false, }, DrumTrackId::OpenHiHat => Self { - tune: 0.5, sweep: 0.6, color: 0.3, snap: 0.25, - filter: 0.5, drive: 0.0, decay: 0.5, volume: 0.65, - send_reverb: 0.1, send_delay: 0.0, pan: 0.5, mute: false, solo: false, + tune: 0.5, + sweep: 0.6, + color: 0.3, + snap: 0.25, + shape: 0.0, + attack: 0.08, + filter: 0.5, + drive: 0.0, + decay: 0.5, + volume: 0.65, + send_reverb: 0.1, + send_delay: 0.0, + pan: 0.5, + mute: false, + solo: false, }, DrumTrackId::Ride => Self { - tune: 0.5, sweep: 0.0, color: 0.5, snap: 0.1, - filter: 0.4, drive: 0.0, decay: 0.7, volume: 0.6, - send_reverb: 0.1, send_delay: 0.0, pan: 0.5, mute: false, solo: false, + tune: 0.5, + sweep: 0.0, + color: 0.5, + snap: 0.1, + shape: 0.0, + attack: 0.08, + filter: 0.4, + drive: 0.0, + decay: 0.7, + volume: 0.6, + send_reverb: 0.1, + send_delay: 0.0, + pan: 0.5, + mute: false, + solo: false, }, DrumTrackId::Clap => Self { - tune: 0.5, sweep: 0.3, color: 0.5, snap: 0.5, - filter: 0.5, drive: 0.1, decay: 0.4, volume: 0.7, - send_reverb: 0.2, send_delay: 0.0, pan: 0.5, mute: false, solo: false, + tune: 0.5, + sweep: 0.3, + color: 0.5, + snap: 0.5, + shape: 0.0, + attack: 0.12, + filter: 0.5, + drive: 0.1, + decay: 0.4, + volume: 0.7, + send_reverb: 0.2, + send_delay: 0.0, + pan: 0.5, + mute: false, + solo: false, }, DrumTrackId::Cowbell => Self { - tune: 0.5, sweep: 0.3, color: 0.5, snap: 0.2, - filter: 0.5, drive: 0.1, decay: 0.4, volume: 0.7, - send_reverb: 0.1, send_delay: 0.0, pan: 0.5, mute: false, solo: false, + tune: 0.5, + sweep: 0.3, + color: 0.5, + snap: 0.2, + shape: 0.0, + attack: 0.1, + filter: 0.5, + drive: 0.1, + decay: 0.4, + volume: 0.7, + send_reverb: 0.1, + send_delay: 0.0, + pan: 0.5, + mute: false, + solo: false, }, DrumTrackId::Tom => Self { - tune: 0.5, sweep: 0.5, color: 0.1, snap: 0.3, - filter: 0.8, drive: 0.1, decay: 0.5, volume: 0.8, - send_reverb: 0.1, send_delay: 0.0, pan: 0.5, mute: false, solo: false, + tune: 0.5, + sweep: 0.5, + color: 0.1, + snap: 0.3, + shape: 0.0, + attack: 0.1, + filter: 0.8, + drive: 0.1, + decay: 0.5, + volume: 0.8, + send_reverb: 0.1, + send_delay: 0.0, + pan: 0.5, + mute: false, + solo: false, }, } } @@ -112,6 +210,8 @@ impl Default for DrumTrackParams { sweep: 0.3, color: 0.5, snap: 0.3, + shape: 0.0, + attack: 0.1, filter: 0.5, drive: 0.0, decay: 0.5, diff --git a/src/sequencer/project.rs b/src/sequencer/project.rs index 7561204..da657ea 100644 --- a/src/sequencer/project.rs +++ b/src/sequencer/project.rs @@ -32,6 +32,10 @@ pub struct DrumSoundParams { pub color: f32, #[serde(default = "default_third")] pub snap: f32, + #[serde(default)] + pub shape: f32, + #[serde(default = "default_attack")] + pub attack: f32, #[serde(default = "default_half")] pub filter: f32, #[serde(default)] @@ -51,11 +55,13 @@ pub struct DrumSoundParams { fn default_half() -> f32 { 0.5 } fn default_third() -> f32 { 0.3 } fn default_volume() -> f32 { 0.8 } +fn default_attack() -> f32 { 0.1 } impl Default for DrumSoundParams { fn default() -> Self { Self { tune: 0.5, sweep: 0.3, color: 0.5, snap: 0.3, + shape: 0.0, attack: 0.1, filter: 0.5, drive: 0.0, decay: 0.5, volume: 0.8, send_reverb: 0.0, send_delay: 0.0, pan: 0.5, } @@ -67,6 +73,7 @@ impl DrumSoundParams { let p = DrumTrackParams::defaults_for(track); Self { tune: p.tune, sweep: p.sweep, color: p.color, snap: p.snap, + shape: p.shape, attack: p.attack, filter: p.filter, drive: p.drive, decay: p.decay, volume: p.volume, send_reverb: p.send_reverb, send_delay: p.send_delay, pan: p.pan, } @@ -75,6 +82,7 @@ impl DrumSoundParams { pub fn to_track_params(self) -> DrumTrackParams { DrumTrackParams { tune: self.tune, sweep: self.sweep, color: self.color, snap: self.snap, + shape: self.shape, attack: self.attack, filter: self.filter, drive: self.drive, decay: self.decay, volume: self.volume, send_reverb: self.send_reverb, send_delay: self.send_delay, pan: self.pan, mute: false, solo: false, @@ -84,6 +92,7 @@ impl DrumSoundParams { pub fn from_track_params(p: &DrumTrackParams) -> Self { Self { tune: p.tune, sweep: p.sweep, color: p.color, snap: p.snap, + shape: p.shape, attack: p.attack, filter: p.filter, drive: p.drive, decay: p.decay, volume: p.volume, send_reverb: p.send_reverb, send_delay: p.send_delay, pan: p.pan, } @@ -92,9 +101,9 @@ impl DrumSoundParams { // ── Genre Kit Presets ──────────────────────────────────────────────────────── -/// Helper: build a DrumSoundParams from (tune, sweep, color, snap, filter, drive, decay, volume, send_reverb, send_delay). -fn ds(t: f32, sw: f32, c: f32, sn: f32, f: f32, dr: f32, dc: f32, v: f32, sr: f32, sd: f32) -> DrumSoundParams { - DrumSoundParams { tune: t, sweep: sw, color: c, snap: sn, filter: f, drive: dr, decay: dc, volume: v, send_reverb: sr, send_delay: sd, pan: 0.5 } +/// Helper: build a DrumSoundParams from (tune, sweep, color, snap, shape, attack, filter, drive, decay, volume, send_reverb, send_delay). +fn ds(t: f32, sw: f32, c: f32, sn: f32, sh: f32, at: f32, f: f32, dr: f32, dc: f32, v: f32, sr: f32, sd: f32) -> DrumSoundParams { + DrumSoundParams { tune: t, sweep: sw, color: c, snap: sn, shape: sh, attack: at, filter: f, drive: dr, decay: dc, volume: v, send_reverb: sr, send_delay: sd, pan: 0.5 } } /// Build a named kit from 8 DrumSoundParams (one per voice in TRACK_IDS order). @@ -111,92 +120,92 @@ pub fn genre_kits() -> Vec { vec![ // Kit 1: 808 — deep, boomy, analog warmth make_kit("808", [ - // tune sweep color snap filt drive decay vol rvb dly - ds(0.20, 0.70, 0.15, 0.10, 0.50, 0.10, 0.80, 0.85, 0.05, 0.00), // Kick - ds(0.35, 0.15, 0.50, 0.40, 0.50, 0.10, 0.40, 0.80, 0.15, 0.00), // Snare - ds(0.60, 0.00, 0.50, 0.40, 0.65, 0.00, 0.08, 0.70, 0.05, 0.00), // CHH - ds(0.50, 0.00, 0.50, 0.30, 0.55, 0.00, 0.50, 0.65, 0.10, 0.00), // OHH - ds(0.55, 0.00, 0.45, 0.15, 0.50, 0.00, 0.70, 0.55, 0.10, 0.00), // Ride - ds(0.50, 0.30, 0.50, 0.50, 0.50, 0.10, 0.40, 0.70, 0.20, 0.00), // Clap - ds(0.50, 0.30, 0.50, 0.20, 0.50, 0.10, 0.40, 0.70, 0.10, 0.00), // Cowbell - ds(0.30, 0.60, 0.10, 0.30, 0.70, 0.10, 0.55, 0.80, 0.10, 0.00), // Tom + // tune sweep color snap shape attck filt drive decay vol rvb dly + ds(0.20, 0.70, 0.15, 0.10, 0.05, 0.08, 0.70, 0.10, 0.80, 0.85, 0.05, 0.00), // Kick + ds(0.35, 0.15, 0.50, 0.40, 0.30, 0.10, 0.50, 0.10, 0.40, 0.80, 0.15, 0.00), // Snare + ds(0.60, 0.00, 0.50, 0.40, 0.10, 0.02, 0.65, 0.00, 0.08, 0.70, 0.05, 0.00), // CHH + ds(0.50, 0.00, 0.50, 0.30, 0.20, 0.06, 0.55, 0.00, 0.50, 0.65, 0.10, 0.00), // OHH + ds(0.55, 0.00, 0.45, 0.15, 0.20, 0.06, 0.50, 0.00, 0.70, 0.55, 0.10, 0.00), // Ride + ds(0.50, 0.30, 0.50, 0.50, 0.30, 0.10, 0.50, 0.10, 0.40, 0.70, 0.20, 0.00), // Clap + ds(0.50, 0.30, 0.50, 0.20, 0.20, 0.05, 0.50, 0.10, 0.40, 0.70, 0.10, 0.00), // Cowbell + ds(0.30, 0.60, 0.10, 0.30, 0.20, 0.08, 0.70, 0.10, 0.55, 0.80, 0.10, 0.00), // Tom ]), // Kit 2: 909 — punchy, crisp, harder-hitting make_kit("909", [ - ds(0.35, 0.50, 0.30, 0.60, 0.80, 0.30, 0.45, 0.85, 0.05, 0.00), // Kick - ds(0.45, 0.10, 0.60, 0.60, 0.65, 0.20, 0.35, 0.85, 0.15, 0.00), // Snare - ds(0.55, 0.00, 0.55, 0.35, 0.70, 0.10, 0.12, 0.70, 0.05, 0.00), // CHH - ds(0.55, 0.00, 0.55, 0.35, 0.65, 0.10, 0.55, 0.70, 0.10, 0.00), // OHH - ds(0.60, 0.00, 0.50, 0.25, 0.60, 0.10, 0.65, 0.60, 0.10, 0.00), // Ride - ds(0.55, 0.25, 0.55, 0.60, 0.60, 0.15, 0.35, 0.75, 0.20, 0.00), // Clap - ds(0.55, 0.25, 0.45, 0.30, 0.60, 0.10, 0.35, 0.65, 0.10, 0.00), // Cowbell - ds(0.50, 0.70, 0.20, 0.50, 0.70, 0.20, 0.45, 0.75, 0.10, 0.00), // Tom + ds(0.35, 0.50, 0.30, 0.60, 0.80, 0.02, 0.80, 0.30, 0.45, 0.85, 0.05, 0.00), // Kick + ds(0.45, 0.10, 0.60, 0.60, 0.50, 0.05, 0.65, 0.20, 0.35, 0.85, 0.15, 0.00), // Snare + ds(0.55, 0.00, 0.55, 0.35, 0.30, 0.02, 0.70, 0.10, 0.12, 0.70, 0.05, 0.00), // CHH + ds(0.55, 0.00, 0.55, 0.35, 0.30, 0.03, 0.65, 0.10, 0.55, 0.70, 0.10, 0.00), // OHH + ds(0.60, 0.00, 0.50, 0.25, 0.30, 0.03, 0.60, 0.10, 0.65, 0.60, 0.10, 0.00), // Ride + ds(0.55, 0.25, 0.55, 0.60, 0.50, 0.06, 0.60, 0.15, 0.35, 0.75, 0.20, 0.00), // Clap + ds(0.55, 0.25, 0.45, 0.30, 0.30, 0.03, 0.60, 0.10, 0.35, 0.65, 0.10, 0.00), // Cowbell + ds(0.50, 0.70, 0.20, 0.50, 0.50, 0.04, 0.70, 0.20, 0.45, 0.75, 0.10, 0.00), // Tom ]), // Kit 3: Techno — dark, driving, industrial-leaning make_kit("Techno", [ - ds(0.22, 0.75, 0.25, 0.55, 0.60, 0.35, 0.70, 0.90, 0.05, 0.00), // Kick - ds(0.38, 0.10, 0.70, 0.65, 0.55, 0.35, 0.25, 0.75, 0.15, 0.00), // Snare - ds(0.50, 0.05, 0.60, 0.50, 0.55, 0.25, 0.06, 0.65, 0.05, 0.00), // CHH - ds(0.45, 0.10, 0.65, 0.40, 0.50, 0.30, 0.40, 0.60, 0.10, 0.10), // OHH - ds(0.50, 0.05, 0.60, 0.30, 0.45, 0.30, 0.55, 0.50, 0.10, 0.10), // Ride - ds(0.48, 0.20, 0.60, 0.70, 0.55, 0.30, 0.30, 0.75, 0.15, 0.00), // Clap - ds(0.55, 0.35, 0.55, 0.40, 0.50, 0.35, 0.25, 0.60, 0.10, 0.10), // Cowbell - ds(0.35, 0.75, 0.30, 0.55, 0.55, 0.35, 0.50, 0.75, 0.10, 0.00), // Tom + ds(0.22, 0.75, 0.25, 0.55, 0.30, 0.02, 0.60, 0.35, 0.70, 0.90, 0.05, 0.00), // Kick + ds(0.38, 0.10, 0.70, 0.65, 0.60, 0.04, 0.55, 0.35, 0.25, 0.75, 0.15, 0.00), // Snare + ds(0.50, 0.05, 0.60, 0.50, 0.40, 0.02, 0.55, 0.25, 0.06, 0.65, 0.05, 0.00), // CHH + ds(0.45, 0.10, 0.65, 0.40, 0.50, 0.04, 0.50, 0.30, 0.40, 0.60, 0.10, 0.10), // OHH + ds(0.50, 0.05, 0.60, 0.30, 0.50, 0.04, 0.45, 0.30, 0.55, 0.50, 0.10, 0.10), // Ride + ds(0.48, 0.20, 0.60, 0.70, 0.60, 0.05, 0.55, 0.30, 0.30, 0.75, 0.15, 0.00), // Clap + ds(0.55, 0.35, 0.55, 0.40, 0.50, 0.04, 0.50, 0.35, 0.25, 0.60, 0.10, 0.10), // Cowbell + ds(0.35, 0.75, 0.30, 0.55, 0.30, 0.03, 0.55, 0.35, 0.50, 0.75, 0.10, 0.00), // Tom ]), // Kit 4: House — classic house, warm and bouncy make_kit("House", [ - ds(0.28, 0.60, 0.20, 0.45, 0.65, 0.15, 0.60, 0.85, 0.05, 0.00), // Kick - ds(0.42, 0.10, 0.55, 0.50, 0.60, 0.15, 0.38, 0.78, 0.15, 0.00), // Snare - ds(0.58, 0.00, 0.45, 0.35, 0.65, 0.05, 0.10, 0.68, 0.05, 0.00), // CHH - ds(0.52, 0.00, 0.45, 0.30, 0.60, 0.05, 0.50, 0.65, 0.10, 0.00), // OHH - ds(0.58, 0.00, 0.40, 0.20, 0.55, 0.05, 0.65, 0.55, 0.10, 0.00), // Ride - ds(0.52, 0.30, 0.50, 0.55, 0.55, 0.10, 0.42, 0.72, 0.25, 0.00), // Clap - ds(0.55, 0.25, 0.40, 0.25, 0.55, 0.05, 0.35, 0.60, 0.10, 0.00), // Cowbell - ds(0.45, 0.55, 0.15, 0.40, 0.75, 0.10, 0.50, 0.75, 0.10, 0.00), // Tom + ds(0.28, 0.60, 0.20, 0.45, 0.10, 0.05, 0.65, 0.15, 0.60, 0.85, 0.05, 0.00), // Kick + ds(0.42, 0.10, 0.55, 0.50, 0.40, 0.08, 0.60, 0.15, 0.38, 0.78, 0.15, 0.00), // Snare + ds(0.58, 0.00, 0.45, 0.35, 0.10, 0.02, 0.65, 0.05, 0.10, 0.68, 0.05, 0.00), // CHH + ds(0.52, 0.00, 0.45, 0.30, 0.20, 0.05, 0.60, 0.05, 0.50, 0.65, 0.10, 0.00), // OHH + ds(0.58, 0.00, 0.40, 0.20, 0.20, 0.05, 0.55, 0.05, 0.65, 0.55, 0.10, 0.00), // Ride + ds(0.52, 0.30, 0.50, 0.55, 0.40, 0.10, 0.55, 0.10, 0.42, 0.72, 0.25, 0.00), // Clap + ds(0.55, 0.25, 0.40, 0.25, 0.20, 0.04, 0.55, 0.05, 0.35, 0.60, 0.10, 0.00), // Cowbell + ds(0.45, 0.55, 0.15, 0.40, 0.20, 0.06, 0.75, 0.10, 0.50, 0.75, 0.10, 0.00), // Tom ]), // Kit 5: Minimal — clean, sparse, subtle make_kit("Minimal", [ - ds(0.38, 0.30, 0.10, 0.75, 0.65, 0.00, 0.20, 0.75, 0.05, 0.00), // Kick - ds(0.50, 0.00, 0.35, 0.70, 0.75, 0.00, 0.15, 0.65, 0.10, 0.00), // Snare - ds(0.72, 0.00, 0.30, 0.25, 0.85, 0.00, 0.04, 0.55, 0.05, 0.10), // CHH - ds(0.65, 0.00, 0.30, 0.20, 0.60, 0.00, 0.25, 0.50, 0.10, 0.10), // OHH - ds(0.65, 0.00, 0.25, 0.15, 0.55, 0.00, 0.45, 0.45, 0.10, 0.10), // Ride - ds(0.58, 0.10, 0.30, 0.80, 0.78, 0.00, 0.10, 0.60, 0.10, 0.00), // Clap - ds(0.52, 0.10, 0.25, 0.30, 0.50, 0.00, 0.18, 0.50, 0.10, 0.10), // Cowbell - ds(0.60, 0.20, 0.05, 0.50, 0.60, 0.00, 0.22, 0.60, 0.10, 0.00), // Tom + ds(0.38, 0.30, 0.10, 0.75, 0.70, 0.02, 0.65, 0.00, 0.20, 0.75, 0.05, 0.00), // Kick + ds(0.50, 0.00, 0.35, 0.70, 0.30, 0.03, 0.75, 0.00, 0.15, 0.65, 0.10, 0.00), // Snare + ds(0.72, 0.00, 0.30, 0.25, 0.10, 0.01, 0.85, 0.00, 0.04, 0.55, 0.05, 0.10), // CHH + ds(0.65, 0.00, 0.30, 0.20, 0.10, 0.03, 0.60, 0.00, 0.25, 0.50, 0.10, 0.10), // OHH + ds(0.65, 0.00, 0.25, 0.15, 0.10, 0.03, 0.55, 0.00, 0.45, 0.45, 0.10, 0.10), // Ride + ds(0.58, 0.10, 0.30, 0.80, 0.20, 0.03, 0.78, 0.00, 0.10, 0.60, 0.10, 0.00), // Clap + ds(0.52, 0.10, 0.25, 0.30, 0.10, 0.02, 0.50, 0.00, 0.18, 0.50, 0.10, 0.10), // Cowbell + ds(0.60, 0.20, 0.05, 0.50, 0.40, 0.03, 0.60, 0.00, 0.22, 0.60, 0.10, 0.00), // Tom ]), // Kit 6: Lo-Fi — gritty, crushed, vintage make_kit("Lo-Fi", [ - ds(0.28, 0.55, 0.45, 0.30, 0.40, 0.45, 0.55, 0.78, 0.15, 0.00), // Kick - ds(0.38, 0.12, 0.65, 0.35, 0.40, 0.45, 0.42, 0.72, 0.20, 0.00), // Snare - ds(0.48, 0.00, 0.60, 0.25, 0.38, 0.35, 0.10, 0.62, 0.10, 0.05), // CHH - ds(0.42, 0.00, 0.55, 0.20, 0.35, 0.35, 0.50, 0.60, 0.15, 0.10), // OHH - ds(0.42, 0.00, 0.55, 0.12, 0.35, 0.25, 0.75, 0.52, 0.15, 0.10), // Ride - ds(0.45, 0.25, 0.60, 0.40, 0.42, 0.40, 0.40, 0.68, 0.20, 0.00), // Clap - ds(0.48, 0.30, 0.55, 0.20, 0.40, 0.35, 0.38, 0.58, 0.15, 0.10), // Cowbell - ds(0.38, 0.50, 0.35, 0.25, 0.45, 0.40, 0.52, 0.70, 0.15, 0.00), // Tom + ds(0.28, 0.55, 0.45, 0.30, 0.20, 0.10, 0.40, 0.45, 0.55, 0.78, 0.15, 0.00), // Kick + ds(0.38, 0.12, 0.65, 0.35, 0.50, 0.12, 0.40, 0.45, 0.42, 0.72, 0.20, 0.00), // Snare + ds(0.48, 0.00, 0.60, 0.25, 0.40, 0.04, 0.38, 0.35, 0.10, 0.62, 0.10, 0.05), // CHH + ds(0.42, 0.00, 0.55, 0.20, 0.40, 0.06, 0.35, 0.35, 0.50, 0.60, 0.15, 0.10), // OHH + ds(0.42, 0.00, 0.55, 0.12, 0.30, 0.06, 0.35, 0.25, 0.75, 0.52, 0.15, 0.10), // Ride + ds(0.45, 0.25, 0.60, 0.40, 0.50, 0.10, 0.42, 0.40, 0.40, 0.68, 0.20, 0.00), // Clap + ds(0.48, 0.30, 0.55, 0.20, 0.40, 0.06, 0.40, 0.35, 0.38, 0.58, 0.15, 0.10), // Cowbell + ds(0.38, 0.50, 0.35, 0.25, 0.30, 0.08, 0.45, 0.40, 0.52, 0.70, 0.15, 0.00), // Tom ]), // Kit 7: Electro — bright, aggressive, funk-influenced make_kit("Electro", [ - ds(0.30, 0.80, 0.25, 0.70, 0.85, 0.25, 0.50, 0.88, 0.05, 0.00), // Kick - ds(0.48, 0.15, 0.55, 0.75, 0.80, 0.25, 0.30, 0.82, 0.10, 0.00), // Snare - ds(0.65, 0.05, 0.45, 0.55, 0.80, 0.15, 0.07, 0.72, 0.05, 0.10), // CHH - ds(0.60, 0.05, 0.50, 0.45, 0.75, 0.15, 0.45, 0.68, 0.10, 0.10), // OHH - ds(0.65, 0.05, 0.45, 0.35, 0.70, 0.15, 0.55, 0.58, 0.10, 0.10), // Ride - ds(0.55, 0.20, 0.50, 0.70, 0.75, 0.20, 0.28, 0.78, 0.10, 0.00), // Clap - ds(0.60, 0.30, 0.40, 0.35, 0.70, 0.15, 0.30, 0.68, 0.10, 0.10), // Cowbell - ds(0.50, 0.80, 0.15, 0.60, 0.80, 0.20, 0.40, 0.78, 0.10, 0.00), // Tom + ds(0.30, 0.80, 0.25, 0.70, 0.60, 0.02, 0.85, 0.25, 0.50, 0.88, 0.05, 0.00), // Kick + ds(0.48, 0.15, 0.55, 0.75, 0.60, 0.03, 0.80, 0.25, 0.30, 0.82, 0.10, 0.00), // Snare + ds(0.65, 0.05, 0.45, 0.55, 0.30, 0.01, 0.80, 0.15, 0.07, 0.72, 0.05, 0.10), // CHH + ds(0.60, 0.05, 0.50, 0.45, 0.30, 0.03, 0.75, 0.15, 0.45, 0.68, 0.10, 0.10), // OHH + ds(0.65, 0.05, 0.45, 0.35, 0.40, 0.03, 0.70, 0.15, 0.55, 0.58, 0.10, 0.10), // Ride + ds(0.55, 0.20, 0.50, 0.70, 0.50, 0.04, 0.75, 0.20, 0.28, 0.78, 0.10, 0.00), // Clap + ds(0.60, 0.30, 0.40, 0.35, 0.30, 0.03, 0.70, 0.15, 0.30, 0.68, 0.10, 0.10), // Cowbell + ds(0.50, 0.80, 0.15, 0.60, 0.50, 0.03, 0.80, 0.20, 0.40, 0.78, 0.10, 0.00), // Tom ]), // Kit 8: Ambient — soft, washy, atmospheric make_kit("Ambient", [ - ds(0.25, 0.40, 0.20, 0.10, 0.40, 0.05, 0.70, 0.65, 0.30, 0.00), // Kick - ds(0.35, 0.05, 0.60, 0.12, 0.35, 0.00, 0.55, 0.55, 0.40, 0.00), // Snare - ds(0.55, 0.00, 0.55, 0.10, 0.40, 0.00, 0.15, 0.45, 0.30, 0.15), // CHH - ds(0.48, 0.00, 0.50, 0.08, 0.38, 0.00, 0.75, 0.45, 0.40, 0.20), // OHH - ds(0.50, 0.00, 0.45, 0.08, 0.35, 0.00, 0.90, 0.42, 0.40, 0.20), // Ride - ds(0.45, 0.15, 0.55, 0.15, 0.38, 0.00, 0.55, 0.50, 0.35, 0.00), // Clap - ds(0.48, 0.20, 0.40, 0.10, 0.40, 0.00, 0.50, 0.45, 0.30, 0.15), // Cowbell - ds(0.40, 0.45, 0.15, 0.12, 0.45, 0.00, 0.65, 0.58, 0.35, 0.00), // Tom + ds(0.25, 0.40, 0.20, 0.10, 0.00, 0.15, 0.40, 0.05, 0.70, 0.65, 0.30, 0.00), // Kick + ds(0.35, 0.05, 0.60, 0.12, 0.20, 0.15, 0.35, 0.00, 0.55, 0.55, 0.40, 0.00), // Snare + ds(0.55, 0.00, 0.55, 0.10, 0.00, 0.05, 0.40, 0.00, 0.15, 0.45, 0.30, 0.15), // CHH + ds(0.48, 0.00, 0.50, 0.08, 0.10, 0.08, 0.38, 0.00, 0.75, 0.45, 0.40, 0.20), // OHH + ds(0.50, 0.00, 0.45, 0.08, 0.10, 0.08, 0.35, 0.00, 0.90, 0.42, 0.40, 0.20), // Ride + ds(0.45, 0.15, 0.55, 0.15, 0.20, 0.12, 0.38, 0.00, 0.55, 0.50, 0.35, 0.00), // Clap + ds(0.48, 0.20, 0.40, 0.10, 0.10, 0.06, 0.40, 0.00, 0.50, 0.45, 0.30, 0.15), // Cowbell + ds(0.40, 0.45, 0.15, 0.12, 0.10, 0.12, 0.45, 0.00, 0.65, 0.58, 0.35, 0.00), // Tom ]), ] } @@ -586,7 +595,7 @@ fn synth_pattern_from_preset(preset_name: &str, display_name: &str) -> SynthPatt /// Create a demo project with 10 pre-filled genre patterns from classic drum programming. pub fn demo_project() -> ProjectFile { let patterns = vec![ - pattern_from_preset("Around the World","Around the World 121", 121.0), + pattern_from_preset("Robot Rock", "Robot Rock 110", 110.0), pattern_from_preset("Acid House", "Acid Techno 138", 138.0), pattern_from_preset("Classic House", "House 122", 122.0), pattern_from_preset("Deep House", "Deep House 120", 120.0), @@ -601,7 +610,7 @@ pub fn demo_project() -> ProjectFile { let kits = genre_kits(); let synth_patterns = vec![ - synth_pattern_from_preset("Around World Bass", "Daft Punk Bass"), + synth_pattern_from_preset("Robot Rock Bass", "Daft Punk Bass"), synth_pattern_from_preset("Acid Techno Bass 1", "Acid Techno Bass"), synth_pattern_from_preset("House Bass 1", "House Bass"), synth_pattern_from_preset("House Bass 3", "Deep House Bass"), @@ -613,7 +622,7 @@ pub fn demo_project() -> ProjectFile { synth_pattern_from_preset("Dub Techno Bass 1", "Dub Techno Bass"), ]; let synth_kits = vec![ - synth_kit_from_preset("Electric Piano", "Electric Piano"), // Kit 0: Around the World + synth_kit_from_preset("Reese Bass", "Reese Bass"), // Kit 0: Robot Rock synth_kit_from_preset("Wobble Bass", "Wobble Bass"), // Kit 1: Acid Techno synth_kit_from_preset("Acid Bass", "Acid Bass"), // Kit 2: House synth_kit_from_preset("Reese Bass", "Reese Bass"), // Kit 3: Deep House @@ -623,7 +632,7 @@ pub fn demo_project() -> ProjectFile { synth_kit_from_preset("Growl Bass", "Growl Bass"), // Kit 7: DnB ]; let synth_b_patterns = vec![ - synth_pattern_from_preset("Around World Lead", "Daft Punk Lead"), + synth_pattern_from_preset("Robot Rock Lead", "Daft Punk Lead"), synth_pattern_from_preset("Acid Techno 1", "Acid Techno Lead"), synth_pattern_from_preset("House 1", "House Keys"), synth_pattern_from_preset("House 3", "Deep House Pad"), @@ -656,7 +665,7 @@ pub fn demo_project() -> ProjectFile { active_kit: 0, patterns, active_pattern: 0, - bpm: 121.0, + bpm: 110.0, loop_length: 32, swing: 0.50, effects: EffectParams::default(), @@ -669,7 +678,7 @@ pub fn demo_project() -> ProjectFile { synth_b_patterns, active_synth_b_pattern: 0, scenes: vec![ - Some(Scene { name: "Around the World".into(), drum_pattern: 0, drum_kit: 0, synth_a_pattern: 0, synth_a_kit: 0, synth_b_pattern: 0, synth_b_kit: 0, bpm: 121.0, swing: 0.50 }), + Some(Scene { name: "Robot Rock".into(), drum_pattern: 0, drum_kit: 0, synth_a_pattern: 0, synth_a_kit: 0, synth_b_pattern: 0, synth_b_kit: 0, bpm: 110.0, swing: 0.50 }), Some(Scene { name: "Acid Techno".into(), drum_pattern: 1, drum_kit: 0, synth_a_pattern: 1, synth_a_kit: 1, synth_b_pattern: 1, synth_b_kit: 1, bpm: 138.0, swing: 0.50 }), Some(Scene { name: "Classic House".into(), drum_pattern: 2, drum_kit: 3, synth_a_pattern: 2, synth_a_kit: 2, synth_b_pattern: 2, synth_b_kit: 2, bpm: 122.0, swing: 0.50 }), Some(Scene { name: "Deep House".into(), drum_pattern: 3, drum_kit: 3, synth_a_pattern: 3, synth_a_kit: 3, synth_b_pattern: 3, synth_b_kit: 3, bpm: 120.0, swing: 0.50 }), @@ -1208,7 +1217,7 @@ mod demo_tests { #[test] fn demo_patterns_have_bpm() { let proj = demo_project(); - assert!((proj.patterns[0].bpm - 121.0).abs() < 0.01); // Around the World + assert!((proj.patterns[0].bpm - 110.0).abs() < 0.01); // Robot Rock assert!((proj.patterns[7].bpm - 174.0).abs() < 0.01); // D&B assert!((proj.patterns[9].bpm - 118.0).abs() < 0.01); // Dub Techno } @@ -1228,7 +1237,7 @@ mod demo_tests { assert_eq!(proj.synth_kits.len(), NUM_KITS); assert_eq!(proj.synth_b_kits.len(), NUM_KITS); // Verify kits have named presets (not default names) - assert_eq!(proj.synth_kits[0].name, "Electric Piano"); + assert_eq!(proj.synth_kits[0].name, "Reese Bass"); assert_eq!(proj.synth_b_kits[0].name, "Acid Bass"); } @@ -1253,9 +1262,9 @@ mod demo_tests { let json = serde_json::to_string(&proj).unwrap(); let loaded: ProjectFile = serde_json::from_str(&json).unwrap(); assert_eq!(loaded.patterns.len(), 10); - assert_eq!(loaded.patterns[0].name, "Around the World 121"); - assert!((loaded.patterns[0].bpm - 121.0).abs() < 0.01); - assert_eq!(loaded.synth_kits[0].name, "Electric Piano"); + assert_eq!(loaded.patterns[0].name, "Robot Rock 110"); + assert!((loaded.patterns[0].bpm - 110.0).abs() < 0.01); + assert_eq!(loaded.synth_kits[0].name, "Reese Bass"); } /// Render all genre kit voices to WAV files for auditioning. diff --git a/src/ui/knobs.rs b/src/ui/knobs.rs index 19b5c3f..2b24a2a 100644 --- a/src/ui/knobs.rs +++ b/src/ui/knobs.rs @@ -1,19 +1,19 @@ //! Drum parameter panel: vertical slider bars for the selected track's sound parameters. -use ratatui::Frame; use ratatui::layout::Rect; use ratatui::style::{Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, BorderType, Borders, Paragraph}; +use ratatui::Frame; use crate::app::{App, FocusSection, KNOB_FIELDS}; use crate::sequencer::drum_pattern::TRACK_IDS; use crate::ui::theme; -/// Full labels for the 11 slider columns. -const SLIDER_LABELS: [&str; 11] = [ - "Tune", "Sweep", "Color", "Snap", "Filter", - "Drive", "Decay", "Volume", "Reverb", "Delay", "Pan", +/// Full labels for the 13 slider columns. +const SLIDER_LABELS: [&str; 13] = [ + "Tune", "Sweep", "Color", "Snap", "Shape", "Attack", "Filter", "Drive", "Decay", "Volume", + "Reverb", "Delay", "Pan", ]; /// Number of vertical bar rows in each slider. @@ -33,7 +33,11 @@ pub fn render_knobs(f: &mut Frame, area: Rect, app: &App) { let block = Block::default() .title(title) - .title_style(Style::default().fg(theme::PINK).add_modifier(Modifier::BOLD)) + .title_style( + Style::default() + .fg(theme::PINK) + .add_modifier(Modifier::BOLD), + ) .borders(Borders::ALL) .border_type(BorderType::Thick) .border_style(border_style); @@ -47,9 +51,19 @@ pub fn render_knobs(f: &mut Frame, area: Rect, app: &App) { } let num_knobs = KNOB_FIELDS.len(); - let values: [f32; 11] = [ - params.tune, params.sweep, params.color, params.snap, params.filter, - params.drive, params.decay, params.volume, params.send_reverb, params.send_delay, + let values: [f32; 13] = [ + params.tune, + params.sweep, + params.color, + params.snap, + params.shape, + params.attack, + params.filter, + params.drive, + params.decay, + params.volume, + params.send_reverb, + params.send_delay, params.pan, ]; @@ -85,7 +99,11 @@ pub fn render_knobs(f: &mut Frame, area: Rect, app: &App) { let (ch, fg, bg) = if value > threshold { // Filled segment - let fill_color = if is_selected { theme::PINK } else { theme::AMBER }; + let fill_color = if is_selected { + theme::PINK + } else { + theme::AMBER + }; (theme::GAUGE_FILLED, fill_color, theme::BG) } else { // Empty segment diff --git a/src/ui/layout.rs b/src/ui/layout.rs index 2342757..10d6c46 100644 --- a/src/ui/layout.rs +++ b/src/ui/layout.rs @@ -97,9 +97,9 @@ pub fn compute_dual_layout(total: Rect, vis: &PanelVisibility) -> DualSynthLayou PanelSlot { expanded_height: SYNTH_GRID_HEIGHT, is_visible: vis.synth_a_grid, growable: false }, PanelSlot { expanded_height: SYNTH_KNOBS_HEIGHT, is_visible: vis.synth_b_knobs, growable: false }, PanelSlot { expanded_height: SYNTH_GRID_HEIGHT, is_visible: vis.synth_b_grid, growable: false }, - PanelSlot { expanded_height: DRUM_GRID_MIN_HEIGHT, is_visible: vis.drum_grid, growable: true }, + PanelSlot { expanded_height: DRUM_GRID_MIN_HEIGHT, is_visible: vis.drum_grid, growable: false }, PanelSlot { expanded_height: DRUM_KNOBS_HEIGHT, is_visible: vis.drum_knobs, growable: false }, - PanelSlot { expanded_height: WAVEFORM_HEIGHT, is_visible: vis.waveform, growable: false }, + PanelSlot { expanded_height: WAVEFORM_HEIGHT, is_visible: vis.waveform, growable: true }, ]; // Make a mutable copy of visibility so we can auto-collapse on overflow @@ -293,8 +293,8 @@ mod tests { let ly_exp = compute_dual_layout(term(h), &vis_expanded); let ly_col = compute_dual_layout(term(h), &vis_collapsed); - // Drum grid should be bigger when synth B is collapsed - assert!(ly_col.drum_grid.height > ly_exp.drum_grid.height); + // Waveform should be bigger when synth B is collapsed (it's the growable panel) + assert!(ly_col.waveform.height > ly_exp.waveform.height); } #[test] diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 726bd18..f07b132 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -211,6 +211,8 @@ fn render_activity_bar(f: &mut Frame, area: Rect, app: &App) { DrumControlField::Sweep => ("Sweep", params.sweep), DrumControlField::Color => ("Color", params.color), DrumControlField::Snap => ("Snap", params.snap), + DrumControlField::Shape => ("Shape", params.shape), + DrumControlField::Attack => ("Attack", params.attack), DrumControlField::Filter => ("Filter", params.filter), DrumControlField::Drive => ("Drive", params.drive), DrumControlField::Decay => ("Decay", params.decay),