From 71c0e239acd5b9860163e4f287fec7b9eaca86ea Mon Sep 17 00:00:00 2001 From: lobo Date: Thu, 12 Mar 2026 12:17:02 +0100 Subject: [PATCH 1/5] feat(dsp): add RampedParam + per-track soft saturation - RampedParam: sample-accurate linear ramp to prevent zipper noise on real-time parameter changes (volume, cutoff, etc.) - per_track_saturate(): cubic soft-clip applied per drum voice before bus summing, adding individual punch without killing transients Co-Authored-By: Claude Opus 4.6 --- src/audio/effects.rs | 104 +++++++++++++++++++++++++++++++++++++++++++ src/audio/engine.rs | 4 +- src/audio/mixer.rs | 34 ++++++++++++++ 3 files changed, 140 insertions(+), 2 deletions(-) diff --git a/src/audio/effects.rs b/src/audio/effects.rs index 88dc5cc..81fcf08 100644 --- a/src/audio/effects.rs +++ b/src/audio/effects.rs @@ -1,6 +1,65 @@ //! Send effects: Schroeder reverb, tempo-synced delay, tube saturator, RMS glue compressor. //! Reverb/delay ported from zicbox applyReverb.h with adaptations for Rust. +// --------------------------------------------------------------------------- +// RampedParam: sample-accurate linear ramp for zipper-free parameter changes +// --------------------------------------------------------------------------- + +/// Sample-accurate linear parameter ramp to prevent zipper noise. +/// Use for any parameter that changes in real-time (volume, cutoff, etc.). +#[derive(Clone, Copy, Debug)] +pub struct RampedParam { + current: f32, + target: f32, + increment: f32, + remaining: u32, +} + +impl RampedParam { + pub fn new(initial: f32) -> Self { + Self { + current: initial, + target: initial, + increment: 0.0, + remaining: 0, + } + } + + /// Set a new target value with a ramp duration in samples. + /// For 10ms at 48kHz, use ramp_samples = 480. + pub fn set(&mut self, target: f32, ramp_samples: u32) { + self.target = target; + if ramp_samples <= 1 { + self.current = target; + self.remaining = 0; + self.increment = 0.0; + } else { + self.increment = (target - self.current) / ramp_samples as f32; + self.remaining = ramp_samples; + } + } + + /// Get the next sample value, advancing the ramp by one step. + #[inline] + pub fn next(&mut self) -> f32 { + if self.remaining > 0 { + self.remaining -= 1; + if self.remaining == 0 { + self.current = self.target; + } else { + self.current += self.increment; + } + } + self.current + } + + /// Get the current value without advancing. + #[inline] + pub fn value(&self) -> f32 { + self.current + } +} + // Base comb/allpass lengths tuned for 44100 Hz; scaled by sample_rate / 44100.0 const BASE_COMB_LENGTHS: [usize; 4] = [1117, 1301, 1571, 1787]; const BASE_ALLPASS_LENGTHS: [usize; 2] = [557, 443]; @@ -505,3 +564,48 @@ fn db_to_linear(db: f32) -> f32 { fn linear_to_db(linear: f32) -> f32 { 20.0 * linear.log10() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ramped_param_instant_set() { + let mut p = RampedParam::new(0.0); + p.set(1.0, 1); + assert!((p.next() - 1.0).abs() < 1e-6); + } + + #[test] + fn test_ramped_param_smooth_ramp() { + let mut p = RampedParam::new(0.0); + p.set(1.0, 4); + let v1 = p.next(); + let v2 = p.next(); + let v3 = p.next(); + let v4 = p.next(); + assert!(v1 > 0.0 && v1 < 0.5); + assert!(v2 > v1); + assert!(v3 > v2); + assert!((v4 - 1.0).abs() < 1e-6); + } + + #[test] + fn test_ramped_param_stays_at_target() { + let mut p = RampedParam::new(0.5); + assert!((p.next() - 0.5).abs() < 1e-6); + assert!((p.next() - 0.5).abs() < 1e-6); + } + + #[test] + fn test_ramped_param_retarget_mid_ramp() { + let mut p = RampedParam::new(0.0); + p.set(1.0, 100); + let _ = p.next(); + p.set(0.0, 100); + let v1 = p.next(); + let v2 = p.next(); + assert!(v2 < v1); + } + +} diff --git a/src/audio/engine.rs b/src/audio/engine.rs index 4732f72..17197ac 100644 --- a/src/audio/engine.rs +++ b/src/audio/engine.rs @@ -8,7 +8,7 @@ use crate::audio::clock::SequencerClock; use crate::audio::display_buffer::AudioDisplayBuffer; use crate::audio::drum_voice::{create_drum_voices, DrumVoiceDsp}; use crate::audio::effects::{DelayEffect, GlueCompressor, ReverbEffect, TubeSaturator}; -use crate::audio::mixer::{effective_mute, soft_clip}; +use crate::audio::mixer::{effective_mute, per_track_saturate, soft_clip}; use crate::audio::synth_voice::SynthVoice; use crate::messages::{AudioToUi, SynthId, UiToAudio}; use crate::params::EffectParams; @@ -433,7 +433,7 @@ impl AudioEngine { let sample = self.drum_voices[track].tick(); if !effective_mute(track, &muted, &soloed) { let p = &self.drum_pattern.params[track]; - let voiced = sample * p.volume; + let voiced = per_track_saturate(sample) * p.volume; // Equal-power pan law let pan_angle = p.pan * std::f32::consts::FRAC_PI_2; drum_dry_l += voiced * pan_angle.cos(); diff --git a/src/audio/mixer.rs b/src/audio/mixer.rs index 16614e8..6f110c1 100644 --- a/src/audio/mixer.rs +++ b/src/audio/mixer.rs @@ -20,6 +20,20 @@ pub fn soft_clip(x: f32) -> f32 { x.tanh() } +/// Gentle per-track saturation using cubic soft-clip. +/// Adds subtle odd harmonics and tames peaks without killing transients. +/// More transparent than tanh — preserves the first ~0.8 of dynamic range linearly. +#[inline] +pub fn per_track_saturate(x: f32) -> f32 { + if x > 1.0 { + 2.0 / 3.0 + (x - 1.0) / (1.0 + (x - 1.0) * (x - 1.0)) + } else if x < -1.0 { + -2.0 / 3.0 + (x + 1.0) / (1.0 + (x + 1.0) * (x + 1.0)) + } else { + x - (x * x * x) / 3.0 + } +} + #[cfg(test)] mod tests { use super::*; @@ -86,4 +100,24 @@ mod tests { let v = 0.7; assert!((soft_clip(v) + soft_clip(-v)).abs() < 1e-6); } + + #[test] + fn test_per_track_saturate_clean() { + let x = 0.1_f32; + let y = per_track_saturate(x); + assert!((y - x).abs() < 0.02); + } + + #[test] + fn test_per_track_saturate_limits() { + let y = per_track_saturate(2.0); + assert!(y > 0.8); + assert!(y < 2.0); + } + + #[test] + fn test_per_track_saturate_symmetry() { + let v = 0.8; + assert!((per_track_saturate(v) + per_track_saturate(-v)).abs() < 1e-6); + } } From c53e0877b9a0a15ae6f2b101525a13373763dd9d Mon Sep 17 00:00:00 2001 From: lobo Date: Thu, 12 Mar 2026 12:18:11 +0100 Subject: [PATCH 2/5] feat(dsp): peak detection + parallel (New York) compression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add peak envelope follower alongside RMS detector in GlueCompressor, using max(RMS, peak) for detection — catches transients RMS misses - Add parallel "crush" compressor in engine: heavily compressed copy blended at 30% for body/sustain without killing dynamics Co-Authored-By: Claude Opus 4.6 --- src/audio/effects.rs | 56 +++++++++++++++++++++++++++++++++++++++++++- src/audio/engine.rs | 17 +++++++++++--- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/audio/effects.rs b/src/audio/effects.rs index 81fcf08..87c5798 100644 --- a/src/audio/effects.rs +++ b/src/audio/effects.rs @@ -329,6 +329,10 @@ pub struct GlueCompressor { // RMS level detection (exponential moving average of input²) rms_sq: f32, rms_coeff: f32, + // Peak detection (fast attack for transients) + peak_level: f32, + peak_attack_coeff: f32, + peak_release_coeff: f32, // Gain smoothing (separate attack/release) gain_smooth: f32, // current smoothed gain (linear, not dB) attack_coeff: f32, @@ -347,6 +351,9 @@ impl GlueCompressor { let mut comp = Self { rms_sq: 0.0, rms_coeff: 0.0, + peak_level: 0.0, + peak_attack_coeff: 0.0, + peak_release_coeff: 0.0, gain_smooth: 1.0, attack_coeff: 0.0, release_coeff: 0.0, @@ -393,6 +400,13 @@ impl GlueCompressor { let rms_ms = 10.0; self.rms_coeff = (-1.0 / (rms_ms * 0.001 * sample_rate)).exp() as f32; + // Peak detector: very fast attack (0.1ms), moderate release (5ms) + // Catches transients that RMS misses + let peak_attack_ms = 0.1; + self.peak_attack_coeff = (-1.0 / (peak_attack_ms * 0.001 * sample_rate)).exp() as f32; + let peak_release_ms = 5.0; + self.peak_release_coeff = (-1.0 / (peak_release_ms * 0.001 * sample_rate)).exp() as f32; + // Auto makeup gain: approximate the average gain reduction // At threshold with the given ratio, max GR ≈ threshold * (1 - 1/ratio) // We compensate for roughly half of that (sounds natural) @@ -413,8 +427,23 @@ impl GlueCompressor { // Convert RMS to dB (with floor to avoid log(0)) let rms_db = linear_to_db(self.rms_sq.sqrt().max(1e-10)); + // Peak detection (envelope follower) + let abs_input = input.abs(); + if abs_input > self.peak_level { + self.peak_level = self.peak_attack_coeff * self.peak_level + + (1.0 - self.peak_attack_coeff) * abs_input; + } else { + self.peak_level = self.peak_release_coeff * self.peak_level + + (1.0 - self.peak_release_coeff) * abs_input; + } + let peak_db = linear_to_db(self.peak_level.max(1e-10)); + + // Use the louder of RMS and peak for detection + // Preserves transient response while still compressing sustained signals + let detect_db = rms_db.max(peak_db); + // Gain computation with soft knee - let gain_db = compute_gain_db(rms_db, self.threshold_db, self.ratio, self.knee_db); + let gain_db = compute_gain_db(detect_db, self.threshold_db, self.ratio, self.knee_db); // Convert to linear gain let target_gain = db_to_linear(gain_db); @@ -597,6 +626,31 @@ mod tests { assert!((p.next() - 0.5).abs() < 1e-6); } + #[test] + fn test_compressor_tames_peaks() { + let sr = 48000.0; + let mut comp = GlueCompressor::new(sr); + comp.set_amount(0.7, sr); + let loud = 0.9_f32; + let mut out = 0.0; + // Need enough samples for RMS + peak detector to respond + for _ in 0..2000 { + out = comp.tick(loud); + } + assert!(out < loud, "Compressed output {} should be less than input {}", out, loud); + assert!(out > 0.0); + } + + #[test] + fn test_compressor_bypass() { + let sr = 48000.0; + let mut comp = GlueCompressor::new(sr); + comp.set_amount(0.0, sr); + let input = 0.5; + let out = comp.tick(input); + assert!((out - input).abs() < 1e-6); + } + #[test] fn test_ramped_param_retarget_mid_ramp() { let mut p = RampedParam::new(0.0); diff --git a/src/audio/engine.rs b/src/audio/engine.rs index 17197ac..9f1c84d 100644 --- a/src/audio/engine.rs +++ b/src/audio/engine.rs @@ -135,6 +135,7 @@ pub struct AudioEngine { drum_reverb: ReverbEffect, drum_delay: DelayEffect, compressor: GlueCompressor, + crush_compressor: GlueCompressor, // parallel "New York" compression drum_saturator: TubeSaturator, effect_params: EffectParams, @@ -162,6 +163,8 @@ impl AudioEngine { ); let compressor = GlueCompressor::new(sample_rate); + let mut crush_compressor = GlueCompressor::new(sample_rate); + crush_compressor.set_amount(1.0, sample_rate); // always heavy let drum_saturator = TubeSaturator::new(sample_rate as f32); Self { @@ -177,6 +180,7 @@ impl AudioEngine { drum_reverb, drum_delay, compressor, + crush_compressor, drum_saturator, effect_params, display_buf, @@ -567,12 +571,19 @@ impl AudioEngine { let mono_wet = synth_a_out * gain_a + synth_b_out * gain_b + reverb_out + delay_out; let mixed_l = (drum_sat_l + mono_wet) * 0.5 * self.master_volume; let mixed_r = (drum_sat_r + mono_wet) * 0.5 * self.master_volume; - // Linked stereo compression: detect from mono sum, apply gain to both channels + // Linked stereo compression with parallel "crush" bus let mono = (mixed_l + mixed_r) * 0.5; let compressed = self.compressor.tick(mono); let comp_gain = if mono.abs() > 1e-10 { compressed / mono } else { 1.0 }; - let out_l = soft_clip(mixed_l * comp_gain); - let out_r = soft_clip(mixed_r * comp_gain); + + // Parallel "crush" compression: heavily compressed copy blended at 30% + // Adds body and sustain without killing transients (New York compression) + let crush = self.crush_compressor.tick(mono); + let crush_gain = if mono.abs() > 1e-10 { crush / mono } else { 1.0 }; + let parallel_gain = comp_gain + crush_gain * 0.3; + + let out_l = soft_clip(mixed_l * parallel_gain); + let out_r = soft_clip(mixed_r * parallel_gain); frame[0] = out_l; if frame.len() > 1 { From e4556bcba03d539783d22aa422f1c45e599b2132 Mon Sep 17 00:00:00 2001 From: lobo Date: Thu, 12 Mar 2026 12:21:38 +0100 Subject: [PATCH 3/5] feat(dsp): improve drum voice character - Kick: add sub-oscillator one octave below for chest-hitting low-end weight - Snare: add comb filter on noise path for shell resonance character, tuned to 2x snare pitch with color-controlled feedback - Hi-hats (closed + open): add bright transient noise burst (2-3ms) and high-shelf sizzle boost (~10kHz +40%) for crispness and shimmer Co-Authored-By: Claude Opus 4.6 --- src/audio/drum_voice.rs | 145 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 139 insertions(+), 6 deletions(-) diff --git a/src/audio/drum_voice.rs b/src/audio/drum_voice.rs index 208f83f..b449ac2 100644 --- a/src/audio/drum_voice.rs +++ b/src/audio/drum_voice.rs @@ -181,6 +181,48 @@ impl StateVariableFilter { } } +// --------------------------------------------------------------------------- +// Comb filter for shell resonance (used by SnareVoice) +// --------------------------------------------------------------------------- + +/// Simple feedback comb filter for adding resonant character to noise. +/// Models the shell resonance of an acoustic snare drum. +struct CombFilter { + buf: [f32; 512], + pos: usize, + delay: usize, + feedback: f32, +} + +impl CombFilter { + fn new() -> Self { + Self { + buf: [0.0; 512], + pos: 0, + delay: 100, + feedback: 0.0, + } + } + + fn set(&mut self, freq_hz: f32, sr: f64, fb: f32) { + self.delay = ((sr as f32 / freq_hz) as usize).clamp(1, 511); + self.feedback = fb.clamp(0.0, 0.8); + } + + #[inline] + fn tick(&mut self, input: f32) -> f32 { + let read_pos = if self.pos >= self.delay { + self.pos - self.delay + } else { + 512 - (self.delay - self.pos) + }; + let delayed = self.buf[read_pos]; + self.buf[self.pos] = input + delayed * self.feedback; + self.pos = (self.pos + 1) % 512; + input + delayed * self.feedback + } +} + // =================================================================== // 1. KickVoice (TR-909 inspired: sine osc + pitch env + click impulse) // @@ -219,6 +261,11 @@ pub struct KickVoice { noise: Noise, drive: f32, active: bool, + // Sub-oscillator: one octave below for low-end weight + sub_phase: f64, + sub_freq: f32, + sub_env: f32, + sub_decay: f32, } impl KickVoice { @@ -241,6 +288,10 @@ impl KickVoice { noise: Noise::new(42), drive: 0.0, active: false, + sub_phase: 0.0, + sub_freq: 0.0, + sub_env: 0.0, + sub_decay: 0.0, } } } @@ -289,6 +340,13 @@ impl DrumVoiceDsp for KickVoice { self.click_svf.set_freq(click_filter_freq, 0.18, self.sr); self.click_svf.reset(); + // Sub-oscillator: one octave below the body fundamental + self.sub_freq = self.freq_base * 0.5; + self.sub_phase = 0.0; + self.sub_env = 0.7; // slightly quieter than body + // Longer decay than body — sub lingers for chest-hitting low-end + self.sub_decay = (-5.0_f64 / ((0.15 + p.decay as f64 * 0.5) * self.sr)).exp() as f32; + self.drive = p.drive; self.active = true; } @@ -332,9 +390,18 @@ impl DrumVoiceDsp for KickVoice { let click = self.click_svf.lp() * self.click_env * self.click_level; self.click_env *= self.click_decay; - // ── Sum both paths ── + // ── Path C: sub-oscillator (one octave below for chest-hitting low-end) ── - let raw = body + click; + self.sub_phase += self.sub_freq as f64 / self.sr; + if self.sub_phase >= 1.0 { + self.sub_phase -= 1.0; + } + let sub = (self.sub_phase * std::f64::consts::TAU).sin() as f32 * self.sub_env; + self.sub_env *= self.sub_decay; + + // ── Sum all paths ── + + let raw = body + click + sub; let driven = apply_drive(raw, self.drive); // Deactivate when both envelopes are spent @@ -371,6 +438,7 @@ pub struct SnareVoice { noise: Noise, drive: f32, active: bool, + comb: CombFilter, } impl SnareVoice { @@ -397,6 +465,7 @@ impl SnareVoice { noise: Noise::new(123), drive: 0.0, active: false, + comb: CombFilter::new(), } } } @@ -449,6 +518,11 @@ impl DrumVoiceDsp for SnareVoice { self.lp.set_freq(lp_freq, self.sr); self.lp.prev_out = 0.0; + // Shell resonance: comb filter tuned to 2x snare pitch for metallic ring + 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 + self.comb.set(comb_freq, self.sr, comb_fb); + self.drive = p.drive; self.active = true; } @@ -470,10 +544,11 @@ impl DrumVoiceDsp for SnareVoice { let body = sine * self.body_env; self.body_env *= self.body_decay; - // Noise through highpass + // Noise through highpass → comb filter for shell resonance let raw_noise = self.noise.next(); let filtered_noise = self.hp.tick(raw_noise); - let noise_out = filtered_noise * self.noise_env; + let resonated_noise = self.comb.tick(filtered_noise); + let noise_out = resonated_noise * self.noise_env; self.noise_env *= self.noise_decay; // Impact transient: raw noise burst in first ~3ms @@ -528,10 +603,20 @@ pub struct ClosedHiHatVoice { svf: StateVariableFilter, drive: f32, active: bool, + // Bright click transient (~2ms noise burst at high frequency) + transient_env: f32, + transient_decay: f32, + transient_noise: Noise, + // Sizzle: high-shelf boost state + sizzle_state: f32, + sizzle_coeff: f32, } impl ClosedHiHatVoice { pub fn new(sr: f64) -> Self { + let sizzle_freq = 10000.0; + let rc = 1.0 / (2.0 * std::f32::consts::PI * sizzle_freq); + let dt = 1.0 / sr as f32; Self { sr, sr_recip: (1.0 / sr) as f32, @@ -548,6 +633,11 @@ impl ClosedHiHatVoice { svf: StateVariableFilter::new(), drive: 0.0, active: false, + transient_env: 0.0, + transient_decay: 0.0, + transient_noise: Noise::new(789), + sizzle_state: 0.0, + sizzle_coeff: dt / (rc + dt), } } } @@ -585,6 +675,11 @@ impl DrumVoiceDsp for ClosedHiHatVoice { self.svf.set_freq(lp_freq, 0.05, self.sr); self.svf.reset(); + // Bright transient: very short noise burst for attack definition + self.transient_env = 0.5 + p.snap * 0.5; + let transient_ms = 2.0; + self.transient_decay = (-5.0_f64 / (transient_ms as f64 * 0.001 * self.sr)).exp() as f32; + self.drive = p.drive; self.active = true; } @@ -640,7 +735,16 @@ impl DrumVoiceDsp for ClosedHiHatVoice { self.svf.tick(hp_out); let filtered = self.svf.lp(); let driven = apply_drive(filtered, self.drive); - let out = driven * self.env; + + // Add bright transient click + let transient = self.transient_noise.next() * self.transient_env; + self.transient_env *= self.transient_decay; + + // Sizzle: high-shelf boost (add high-passed version of signal) + 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% self.env *= self.env_decay; if self.env < 1e-6 { @@ -708,10 +812,20 @@ pub struct OpenHiHatVoice { lp_sweep_coeff: f32, drive: f32, active: bool, + // Bright click transient (~3ms noise burst) + transient_env: f32, + transient_decay: f32, + transient_noise: Noise, + // Sizzle: high-shelf boost state + sizzle_state: f32, + sizzle_coeff: f32, } impl OpenHiHatVoice { pub fn new(sr: f64) -> Self { + let sizzle_freq = 10000.0; + let rc = 1.0 / (2.0 * std::f32::consts::PI * sizzle_freq); + let dt = 1.0 / sr as f32; Self { sr, sr_recip: (1.0 / sr) as f32, @@ -739,6 +853,11 @@ impl OpenHiHatVoice { lp_sweep_coeff: 1.0, drive: 0.0, active: false, + transient_env: 0.0, + transient_decay: 0.0, + transient_noise: Noise::new(0xCAFE), + sizzle_state: 0.0, + sizzle_coeff: dt / (rc + dt), } } } @@ -795,6 +914,11 @@ impl DrumVoiceDsp for OpenHiHatVoice { self.svf.set_freq(lp_start, 0.1, self.sr); self.svf.reset(); + // Bright transient: slightly longer than closed hat (3ms) for diffuse attack + self.transient_env = 0.4 + p.snap * 0.6; + let transient_ms = 3.0; + self.transient_decay = (-5.0_f64 / (transient_ms as f64 * 0.001 * self.sr)).exp() as f32; + self.drive = p.drive; self.active = true; } @@ -863,7 +987,16 @@ impl DrumVoiceDsp for OpenHiHatVoice { let filtered = self.svf.lp(); let driven = apply_drive(filtered, self.drive); - let out = driven * self.attack_env; + + // Add bright transient click + let transient = self.transient_noise.next() * self.transient_env; + self.transient_env *= self.transient_decay; + + // Sizzle: high-shelf boost + let sizzle_in = driven + transient; + self.sizzle_state += self.sizzle_coeff * (sizzle_in - self.sizzle_state); + let hi_content = sizzle_in - self.sizzle_state; + let out = (sizzle_in + hi_content * 0.4) * self.attack_env; // Advance envelopes self.env_body *= self.env_body_decay; From fab6f1fbea4fc6fc9b2ff0f9aeafacf4cf0b5143 Mon Sep 17 00:00:00 2001 From: lobo Date: Thu, 12 Mar 2026 12:23:13 +0100 Subject: [PATCH 4/5] feat(dsp): add early reflections to reverb + reduce feedback ceiling - Add 5-tap early reflection delay line (3/7/11/17/23ms) for spatial definition before the diffuse Schroeder tail - Reduce feedback ceiling from 0.92 to 0.85 for cleaner decay - Early reflections mixed slightly louder than diffuse for clarity Co-Authored-By: Claude Opus 4.6 --- src/audio/effects.rs | 73 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/src/audio/effects.rs b/src/audio/effects.rs index 87c5798..d1c10bb 100644 --- a/src/audio/effects.rs +++ b/src/audio/effects.rs @@ -78,6 +78,13 @@ pub struct ReverbEffect { feedback: f32, damping: f32, wet: f32, + // Early reflections: 5 fixed taps for spatial definition before diffuse tail + er_buf: Vec, + er_buf_size: usize, + er_pos: usize, + er_taps: [usize; 5], + er_gains: [f32; 5], + er_wet: f32, } impl ReverbEffect { @@ -109,6 +116,18 @@ impl ReverbEffect { } let allpass_total = offset; + // Early reflections: 5 taps at 3ms, 7ms, 11ms, 17ms, 23ms + let er_delays_ms: [f64; 5] = [3.0, 7.0, 11.0, 17.0, 23.0]; + let er_buf_size = (sample_rate * 0.03) as usize + 1; // max 30ms + let er_taps: [usize; 5] = [ + (er_delays_ms[0] * 0.001 * sample_rate) as usize, + (er_delays_ms[1] * 0.001 * sample_rate) as usize, + (er_delays_ms[2] * 0.001 * sample_rate) as usize, + (er_delays_ms[3] * 0.001 * sample_rate) as usize, + (er_delays_ms[4] * 0.001 * sample_rate) as usize, + ]; + let er_gains: [f32; 5] = [0.35, 0.25, 0.20, 0.15, 0.10]; + Self { comb_buf: vec![0.0; comb_total], comb_lengths, @@ -122,19 +141,39 @@ impl ReverbEffect { feedback: 0.7, damping: 0.25, wet: 0.3, + er_buf: vec![0.0; er_buf_size], + er_buf_size, + er_pos: 0, + er_taps, + er_gains, + er_wet: 0.3, } } /// Update reverb parameters. amount: 0-1, damping: 0-1. pub fn set_params(&mut self, amount: f32, damping: f32) { - // Feedback: 0.50 at amount=0, up to 0.92 at amount=1 - self.feedback = (0.50 + amount * 0.42).min(0.92); + // Reduced feedback ceiling (0.85 max) for cleaner decay + self.feedback = (0.50 + amount * 0.35).min(0.85); self.damping = damping; self.wet = amount * 0.7; + self.er_wet = amount * 0.4; // early reflections slightly louder than diffuse tail } /// Process one sample of reverb input, return wet output. pub fn tick(&mut self, input: f32) -> f32 { + // Early reflections: read tapped delays for spatial definition + let mut er_sum = 0.0_f32; + for i in 0..5 { + let tap_pos = if self.er_pos >= self.er_taps[i] { + self.er_pos - self.er_taps[i] + } else { + self.er_buf_size - (self.er_taps[i] - self.er_pos) + }; + er_sum += self.er_buf[tap_pos] * self.er_gains[i]; + } + self.er_buf[self.er_pos] = input; + self.er_pos = (self.er_pos + 1) % self.er_buf_size; + let mut comb_sum = 0.0_f32; for i in 0..4 { @@ -170,7 +209,7 @@ impl ReverbEffect { self.allpass_pos[i] = (pos + 1) % len; } - out * self.wet + out * self.wet + er_sum * self.er_wet } } @@ -641,6 +680,34 @@ mod tests { assert!(out > 0.0); } + #[test] + fn test_reverb_early_reflections() { + let mut reverb = ReverbEffect::new(48000.0); + reverb.set_params(0.5, 0.3); + let _first = reverb.tick(1.0); + let mut found_reflection = false; + for _i in 0..960 { + let out = reverb.tick(0.0); + if out.abs() > 0.01 { + found_reflection = true; + break; + } + } + assert!(found_reflection, "Should hear early reflections within 20ms"); + } + + #[test] + fn test_reverb_decays() { + let mut reverb = ReverbEffect::new(48000.0); + reverb.set_params(1.0, 0.5); + reverb.tick(1.0); + let mut last = 1.0_f32; + for _ in 0..48000 { + last = reverb.tick(0.0); + } + assert!(last.abs() < 0.1, "Reverb should decay after 1s, got {}", last); + } + #[test] fn test_compressor_bypass() { let sr = 48000.0; From 3d0da665cd6d9e11e4f83841bb17dd7b1b358f3e Mon Sep 17 00:00:00 2001 From: lobo Date: Thu, 12 Mar 2026 12:34:43 +0100 Subject: [PATCH 5/5] fix(dsp): retune drum presets for new DSP characteristics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reduce kick sub-oscillator level 0.70→0.35 and shorten decay (was too boomy) - Retune all kick presets: lower volume/decay to compensate for sub energy, ease drive since per-track saturation adds punch - Retune snare presets: lower color to tame comb filter resonance, slight volume reduction - Retune hi-hat presets (closed+open): lower snap/volume since transient burst and sizzle boost add attack and brightness - Update default DrumTrackParams accordingly Co-Authored-By: Claude Opus 4.6 --- src/audio/drum_voice.rs | 6 +- src/presets/drum_presets.rs | 102 +++++++++++++++++----------------- src/sequencer/drum_pattern.rs | 14 ++--- 3 files changed, 62 insertions(+), 60 deletions(-) diff --git a/src/audio/drum_voice.rs b/src/audio/drum_voice.rs index b449ac2..0f04a24 100644 --- a/src/audio/drum_voice.rs +++ b/src/audio/drum_voice.rs @@ -343,9 +343,9 @@ impl DrumVoiceDsp for KickVoice { // Sub-oscillator: one octave below the body fundamental self.sub_freq = self.freq_base * 0.5; self.sub_phase = 0.0; - self.sub_env = 0.7; // slightly quieter than body - // Longer decay than body — sub lingers for chest-hitting low-end - self.sub_decay = (-5.0_f64 / ((0.15 + p.decay as f64 * 0.5) * self.sr)).exp() as f32; + self.sub_env = 0.35; // subtle reinforcement, not overwhelming + // 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; self.drive = p.drive; self.active = true; diff --git a/src/presets/drum_presets.rs b/src/presets/drum_presets.rs index d85b74a..bb3b0aa 100644 --- a/src/presets/drum_presets.rs +++ b/src/presets/drum_presets.rs @@ -12,74 +12,76 @@ const fn ds(tune: f32, sweep: f32, color: f32, snap: f32, filter: f32, drive: f3 // ── Kick Presets ───────────────────────────────────────────────────────────── pub static KICK_PRESETS: &[DrumSoundPreset] = &[ - // 808 - DrumSoundPreset { name: "Deep 808", category: "808", voice: DrumTrackId::Kick, params: ds(0.20, 0.70, 0.15, 0.10, 0.50, 0.10, 0.80, 0.85) }, - DrumSoundPreset { name: "Punchy 808", category: "808", voice: DrumTrackId::Kick, params: ds(0.30, 0.60, 0.20, 0.50, 0.70, 0.20, 0.50, 0.80) }, - DrumSoundPreset { name: "Sub 808", category: "808", voice: DrumTrackId::Kick, params: ds(0.15, 0.80, 0.10, 0.05, 0.30, 0.05, 0.90, 0.90) }, - DrumSoundPreset { name: "Short 808", category: "808", voice: DrumTrackId::Kick, params: ds(0.25, 0.50, 0.20, 0.40, 0.60, 0.15, 0.30, 0.80) }, - // 909 - DrumSoundPreset { name: "Hard 909", category: "909", voice: DrumTrackId::Kick, params: ds(0.35, 0.50, 0.30, 0.60, 0.80, 0.30, 0.45, 0.85) }, - DrumSoundPreset { name: "Soft 909", category: "909", voice: DrumTrackId::Kick, params: ds(0.30, 0.40, 0.25, 0.30, 0.60, 0.10, 0.50, 0.75) }, - DrumSoundPreset { name: "Boom 909", category: "909", voice: DrumTrackId::Kick, params: ds(0.25, 0.65, 0.20, 0.45, 0.55, 0.20, 0.65, 0.80) }, - // Acoustic - DrumSoundPreset { name: "Tight Acoustic", category: "Acoustic", voice: DrumTrackId::Kick, params: ds(0.40, 0.30, 0.35, 0.70, 0.90, 0.15, 0.35, 0.80) }, - DrumSoundPreset { name: "Jazz Kick", category: "Acoustic", voice: DrumTrackId::Kick, params: ds(0.45, 0.20, 0.40, 0.50, 0.70, 0.05, 0.40, 0.70) }, + // 808 — sub-osc adds low-end, so reduce volume/decay vs original + DrumSoundPreset { name: "Deep 808", category: "808", voice: DrumTrackId::Kick, params: ds(0.20, 0.65, 0.15, 0.10, 0.55, 0.10, 0.70, 0.78) }, + DrumSoundPreset { name: "Punchy 808", category: "808", voice: DrumTrackId::Kick, params: ds(0.30, 0.55, 0.20, 0.50, 0.70, 0.15, 0.45, 0.75) }, + DrumSoundPreset { name: "Sub 808", category: "808", voice: DrumTrackId::Kick, params: ds(0.15, 0.70, 0.10, 0.05, 0.35, 0.05, 0.80, 0.80) }, + DrumSoundPreset { name: "Short 808", category: "808", voice: DrumTrackId::Kick, params: ds(0.25, 0.50, 0.20, 0.40, 0.60, 0.15, 0.28, 0.75) }, + // 909 — per-track saturation adds punch, ease drive + DrumSoundPreset { name: "Hard 909", category: "909", voice: DrumTrackId::Kick, params: ds(0.35, 0.45, 0.30, 0.60, 0.80, 0.25, 0.40, 0.80) }, + DrumSoundPreset { name: "Soft 909", category: "909", voice: DrumTrackId::Kick, params: ds(0.30, 0.40, 0.25, 0.30, 0.60, 0.08, 0.45, 0.72) }, + DrumSoundPreset { name: "Boom 909", category: "909", voice: DrumTrackId::Kick, params: ds(0.25, 0.60, 0.20, 0.45, 0.55, 0.15, 0.55, 0.75) }, + // Acoustic — tighter decay, sub adds natural weight + DrumSoundPreset { name: "Tight Acoustic", category: "Acoustic", voice: DrumTrackId::Kick, params: ds(0.40, 0.30, 0.35, 0.70, 0.90, 0.12, 0.30, 0.75) }, + DrumSoundPreset { name: "Jazz Kick", category: "Acoustic", voice: DrumTrackId::Kick, params: ds(0.45, 0.20, 0.40, 0.50, 0.70, 0.05, 0.35, 0.68) }, // Lo-Fi - DrumSoundPreset { name: "Dusty Kick", category: "Lo-Fi", voice: DrumTrackId::Kick, params: ds(0.30, 0.55, 0.45, 0.35, 0.45, 0.40, 0.55, 0.75) }, - DrumSoundPreset { name: "Tape Kick", category: "Lo-Fi", voice: DrumTrackId::Kick, params: ds(0.25, 0.60, 0.50, 0.20, 0.40, 0.50, 0.60, 0.70) }, - // Industrial - DrumSoundPreset { name: "Distorted Kick", category: "Industrial", voice: DrumTrackId::Kick, params: ds(0.30, 0.70, 0.60, 0.80, 0.90, 0.80, 0.40, 0.85) }, - DrumSoundPreset { name: "Metal Kick", category: "Industrial", voice: DrumTrackId::Kick, params: ds(0.40, 0.80, 0.70, 0.90, 1.00, 0.70, 0.35, 0.80) }, - // Minimal - DrumSoundPreset { name: "Click Kick", category: "Minimal", voice: DrumTrackId::Kick, params: ds(0.35, 0.30, 0.10, 0.80, 0.70, 0.05, 0.20, 0.75) }, - DrumSoundPreset { name: "Micro Kick", category: "Minimal", voice: DrumTrackId::Kick, params: ds(0.40, 0.20, 0.15, 0.60, 0.50, 0.00, 0.15, 0.70) }, + DrumSoundPreset { name: "Dusty Kick", category: "Lo-Fi", voice: DrumTrackId::Kick, params: ds(0.30, 0.50, 0.45, 0.35, 0.45, 0.35, 0.50, 0.70) }, + DrumSoundPreset { name: "Tape Kick", category: "Lo-Fi", voice: DrumTrackId::Kick, params: ds(0.25, 0.55, 0.50, 0.20, 0.40, 0.45, 0.50, 0.68) }, + // Industrial — saturation stacks with per-track, ease drive + DrumSoundPreset { name: "Distorted Kick", category: "Industrial", voice: DrumTrackId::Kick, params: ds(0.30, 0.65, 0.60, 0.80, 0.90, 0.70, 0.35, 0.80) }, + DrumSoundPreset { name: "Metal Kick", category: "Industrial", voice: DrumTrackId::Kick, params: ds(0.40, 0.75, 0.70, 0.90, 1.00, 0.60, 0.30, 0.75) }, + // Minimal — short decay tames sub tail + DrumSoundPreset { name: "Click Kick", category: "Minimal", voice: DrumTrackId::Kick, params: ds(0.35, 0.30, 0.10, 0.80, 0.70, 0.05, 0.18, 0.72) }, + DrumSoundPreset { name: "Micro Kick", category: "Minimal", voice: DrumTrackId::Kick, params: ds(0.40, 0.20, 0.15, 0.60, 0.50, 0.00, 0.12, 0.68) }, ]; // ── Snare Presets ──────────────────────────────────────────────────────────── pub static SNARE_PRESETS: &[DrumSoundPreset] = &[ - // 808 - DrumSoundPreset { name: "Classic 808", category: "808", voice: DrumTrackId::Snare, params: ds(0.35, 0.15, 0.50, 0.40, 0.50, 0.10, 0.40, 0.80) }, - DrumSoundPreset { name: "Rimshot 808", category: "808", voice: DrumTrackId::Snare, params: ds(0.50, 0.05, 0.30, 0.70, 0.70, 0.15, 0.25, 0.80) }, - DrumSoundPreset { name: "Noisy 808", category: "808", voice: DrumTrackId::Snare, params: ds(0.30, 0.20, 0.70, 0.30, 0.40, 0.20, 0.50, 0.75) }, + // 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) }, // 909 - DrumSoundPreset { name: "Crack 909", category: "909", voice: DrumTrackId::Snare, params: ds(0.45, 0.10, 0.60, 0.60, 0.65, 0.20, 0.35, 0.85) }, - DrumSoundPreset { name: "Fat 909", category: "909", voice: DrumTrackId::Snare, params: ds(0.40, 0.15, 0.55, 0.45, 0.55, 0.25, 0.45, 0.80) }, - // Acoustic - DrumSoundPreset { name: "Tight Snare", category: "Acoustic", voice: DrumTrackId::Snare, params: ds(0.50, 0.05, 0.45, 0.65, 0.75, 0.10, 0.30, 0.80) }, - DrumSoundPreset { name: "Brush Snare", category: "Acoustic", voice: DrumTrackId::Snare, params: ds(0.45, 0.00, 0.70, 0.20, 0.50, 0.00, 0.35, 0.65) }, + 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) }, + // 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) }, // Lo-Fi - DrumSoundPreset { name: "Crunchy Snare", category: "Lo-Fi", voice: DrumTrackId::Snare, params: ds(0.40, 0.10, 0.65, 0.50, 0.45, 0.50, 0.40, 0.75) }, - DrumSoundPreset { name: "Vinyl Snare", category: "Lo-Fi", voice: DrumTrackId::Snare, params: ds(0.35, 0.15, 0.60, 0.35, 0.40, 0.35, 0.45, 0.70) }, - // Industrial - DrumSoundPreset { name: "Noise Blast", category: "Industrial", voice: DrumTrackId::Snare, params: ds(0.30, 0.20, 0.90, 0.80, 0.80, 0.70, 0.30, 0.85) }, - // Minimal - DrumSoundPreset { name: "Click Snare", category: "Minimal", voice: DrumTrackId::Snare, params: ds(0.55, 0.00, 0.30, 0.80, 0.80, 0.05, 0.15, 0.75) }, - DrumSoundPreset { name: "Ghost Snare", category: "Minimal", voice: DrumTrackId::Snare, params: ds(0.40, 0.05, 0.50, 0.15, 0.45, 0.00, 0.20, 0.50) }, + 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) }, + // 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) }, + // 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) }, ]; // ── Closed Hi-Hat Presets ──────────────────────────────────────────────────── pub static CHH_PRESETS: &[DrumSoundPreset] = &[ - DrumSoundPreset { name: "Tight 808", category: "808", voice: DrumTrackId::ClosedHiHat, params: ds(0.60, 0.00, 0.50, 0.40, 0.65, 0.00, 0.08, 0.70) }, - DrumSoundPreset { name: "Sizzle 909", category: "909", voice: DrumTrackId::ClosedHiHat, params: ds(0.55, 0.00, 0.55, 0.35, 0.70, 0.10, 0.12, 0.70) }, - DrumSoundPreset { name: "Crisp Hat", category: "Acoustic", voice: DrumTrackId::ClosedHiHat, params: ds(0.70, 0.00, 0.40, 0.50, 0.80, 0.05, 0.06, 0.65) }, - DrumSoundPreset { name: "Dark Hat", category: "Lo-Fi", voice: DrumTrackId::ClosedHiHat, params: ds(0.45, 0.00, 0.60, 0.25, 0.40, 0.30, 0.10, 0.65) }, - DrumSoundPreset { name: "Gritty Hat", category: "Industrial", voice: DrumTrackId::ClosedHiHat, params: ds(0.50, 0.10, 0.70, 0.60, 0.55, 0.50, 0.08, 0.70) }, - DrumSoundPreset { name: "Thin Hat", category: "Minimal", voice: DrumTrackId::ClosedHiHat, params: ds(0.75, 0.00, 0.30, 0.20, 0.90, 0.00, 0.05, 0.55) }, - DrumSoundPreset { name: "Shaker", category: "Acoustic", voice: DrumTrackId::ClosedHiHat, params: ds(0.65, 0.00, 0.80, 0.15, 0.70, 0.00, 0.04, 0.60) }, - DrumSoundPreset { name: "Noisy Click", category: "Lo-Fi", voice: DrumTrackId::ClosedHiHat, params: ds(0.50, 0.05, 0.75, 0.50, 0.35, 0.40, 0.06, 0.65) }, + // 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) }, ]; // ── Open Hi-Hat Presets ────────────────────────────────────────────────────── pub static OHH_PRESETS: &[DrumSoundPreset] = &[ - DrumSoundPreset { name: "Classic 808", category: "808", voice: DrumTrackId::OpenHiHat, params: ds(0.50, 0.60, 0.30, 0.30, 0.50, 0.00, 0.50, 0.70) }, - DrumSoundPreset { name: "Sizzle 909", category: "909", voice: DrumTrackId::OpenHiHat, params: ds(0.55, 0.50, 0.40, 0.35, 0.60, 0.10, 0.55, 0.70) }, - DrumSoundPreset { name: "Washy", category: "Acoustic", voice: DrumTrackId::OpenHiHat, params: ds(0.45, 0.70, 0.35, 0.20, 0.45, 0.00, 0.70, 0.65) }, - DrumSoundPreset { name: "Trash Open", category: "Industrial", voice: DrumTrackId::OpenHiHat, params: ds(0.40, 0.80, 0.60, 0.50, 0.70, 0.50, 0.45, 0.70) }, - DrumSoundPreset { name: "Short Open", category: "Minimal", voice: DrumTrackId::OpenHiHat, params: ds(0.55, 0.40, 0.30, 0.25, 0.55, 0.00, 0.30, 0.60) }, - DrumSoundPreset { name: "Lo-Fi Open", category: "Lo-Fi", voice: DrumTrackId::OpenHiHat, params: ds(0.45, 0.55, 0.50, 0.20, 0.35, 0.35, 0.55, 0.65) }, + // 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) }, ]; // ── Ride Presets ───────────────────────────────────────────────────────────── diff --git a/src/sequencer/drum_pattern.rs b/src/sequencer/drum_pattern.rs index 22c2c3e..33256b4 100644 --- a/src/sequencer/drum_pattern.rs +++ b/src/sequencer/drum_pattern.rs @@ -63,22 +63,22 @@ impl DrumTrackParams { match track { DrumTrackId::Kick => Self { tune: 0.3, sweep: 0.6, color: 0.2, snap: 0.5, - filter: 0.7, drive: 0.2, decay: 0.5, volume: 0.8, + filter: 0.7, drive: 0.15, 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.6, snap: 0.4, - filter: 0.5, drive: 0.1, decay: 0.4, volume: 0.8, + 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, }, DrumTrackId::ClosedHiHat => Self { - tune: 0.6, sweep: 0.0, color: 0.5, snap: 0.3, - filter: 0.6, drive: 0.0, decay: 0.1, volume: 0.7, + 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, }, DrumTrackId::OpenHiHat => Self { - tune: 0.5, sweep: 0.6, color: 0.3, snap: 0.3, - filter: 0.5, drive: 0.0, decay: 0.5, volume: 0.7, + 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, }, DrumTrackId::Ride => Self {