From b77a1f72e250ca983bf67338b55cc9241d2ab7fa Mon Sep 17 00:00:00 2001 From: lobo Date: Wed, 18 Mar 2026 14:02:14 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20v2.2.0=20=E2=80=94=20kick=20color,=20Oc?= =?UTF-8?q?tatrack=20crossfader/delay,=20Daft=20Punk=20presets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kick Voice: - Color knob is a full timbre morph (dark/subby → bright/snappy) affecting pitch env decay, click filter, harmonics, and sub level - Quadratic pitch shaping for audible sweep/color response - Sweep range widened to 0-500Hz Crossfader: - Constant-power cosine/sine curve (Octatrack-style) - No volume dip at center position Delay: - Single-knob macro: 1/16 → 1/16D → 1/8 → 1/8D → 1/4 → 1/4D → 1/2 → 2/1 - Beat-locked dotted subdivisions for polyrhythmic bounce - Default: dotted eighth (1/8D) — echoes sit between kick hits - Clean mode for drums (LP-only), warm mode for synths (HP + saturation) - Last zone (>0.88) = near-infinite feedback - EffectParams sent on startup Presets: - 8 Daft Punk drum patterns (Around the World, Da Funk, Robot Rock, etc.) - 8 Basics + 10 Techno II drum patterns with proper offbeat hats - 16 Daft Punk synth presets (8 bass + 8 lead pairs per track) - Around the World boots as default scene at 121 BPM - Synth A: Electric Piano, Synth B: Acid Bass - Ride decays shortened by 1/3 - Default BPM 121 Co-Authored-By: Claude Opus 4.6 (1M context) --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/app.rs | 1 + src/audio/drum_voice.rs | 103 +++++++++++++++---- src/audio/effects.rs | 146 ++++++++++++++++++++++----- src/audio/engine.rs | 49 +++++---- src/params.rs | 14 +-- src/presets/drum_presets.rs | 24 ++--- src/presets/pattern_presets.rs | 61 +++++++++++ src/presets/synth_pattern_presets.rs | 36 +++++++ src/sequencer/project.rs | 76 +++++++------- src/sequencer/transport.rs | 2 +- 12 files changed, 391 insertions(+), 125 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 060d8e3..754e1a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1634,7 +1634,7 @@ dependencies = [ [[package]] name = "textstep" -version = "2.0.1" +version = "2.2.0" dependencies = [ "cpal", "crossbeam-channel", diff --git a/Cargo.toml b/Cargo.toml index 33bbbbc..493316e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textstep" -version = "2.0.3" +version = "2.2.0" edition = "2024" [dependencies] diff --git a/src/app.rs b/src/app.rs index b7fe07b..9ab839c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -651,6 +651,7 @@ impl App { 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::SetEffectParams(EffectParams::default())); let mut ui = UiState::default(); ui.active_pattern = dp; diff --git a/src/audio/drum_voice.rs b/src/audio/drum_voice.rs index 4594b85..7124f7e 100644 --- a/src/audio/drum_voice.rs +++ b/src/audio/drum_voice.rs @@ -242,7 +242,8 @@ impl CombFilter { // Parameters: // tune — fundamental frequency (30-80 Hz) // sweep — pitch envelope depth (how far above fundamental the pitch starts) -// color — pitch envelope stage-2 decay time (fast thump vs slow "zoop") +// color — timbre morph: dark/subby (0) → bright/snappy (1) +// affects: pitch env stage-2 decay, click brightness, harmonics, sub level // snap — click/impulse level (the "Attack" knob) // filter — body LP cutoff (500-8000 Hz) // drive — asymmetric saturation @@ -275,6 +276,7 @@ pub struct KickVoice { // noise: Noise, drive: f32, + color: f32, // cached for tick(): timbre morph (dark/subby → bright/snappy) active: bool, // Subharmonic: one octave below (0.5×) for phase-coherent low-end sub_phase: f64, @@ -307,6 +309,7 @@ impl KickVoice { click_svf: StateVariableFilter::new(), noise: Noise::new(42), drive: 0.0, + color: 0.0, active: false, sub_phase: 0.0, sub_freq: 0.0, @@ -327,16 +330,17 @@ impl DrumVoiceDsp for KickVoice { // tune: fundamental freq 30-80 Hz self.freq_base = 30.0 + p.tune * 50.0; - // sweep: pitch envelope depth (0-300 Hz above fundamental) - self.freq_start = self.freq_base + p.sweep * 300.0; + // sweep: pitch envelope depth (0-500 Hz above fundamental) + self.freq_start = self.freq_base + p.sweep * 500.0; // Dual-stage pitch envelope: - // Stage 1: always fast (~2-3ms) — the initial transient "click" pitch + // Stage 1: always fast (~2.5ms) — the initial transient "click" pitch // Stage 2: controlled by color — the body tone forming + // Color 0 → 5ms (tight thump), Color 1 → 200ms (long "zoop") self.pitch_env = 1.0; self.pitch_stage2 = false; self.pitch_decay_fast = (-5.0_f64 / (0.0025 * self.sr)).exp() as f32; - let pitch_time_slow = 0.010 + p.color as f64 * 0.070; + let pitch_time_slow = 0.005 + p.color as f64 * 0.195; 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 @@ -345,8 +349,9 @@ impl DrumVoiceDsp for KickVoice { self.body_env = 1.0; self.body_decay = (-5.0_f64 / (body_time * self.sr)).exp() as f32; - // filter: body LP (500-8000 Hz) - let lp_freq = 500.0 + p.filter * 7500.0; + // 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; @@ -359,21 +364,25 @@ impl DrumVoiceDsp for KickVoice { self.click_env = 1.0; self.click_decay = (-5.0_f64 / (0.004 * self.sr)).exp() as f32; - // Resonant BP/LP filter on click (~5kHz, resonance ~18%) - // Using bandpass blend for the sharper "knock" of a 909 bridged-T network - let click_filter_freq = 4000.0 + p.snap * 2000.0; - self.click_svf.set_freq(click_filter_freq, 0.18, self.sr); + // Resonant BP/LP filter on click — color sweeps from dark muffled to bright snappy + // Color 0 → 2kHz (warm thud), Color 1 → 7kHz (sharp click) + // Resonance also increases with color for more "ping" + let click_filter_freq = 2000.0 + p.color * 5000.0 + p.snap * 1000.0; + let click_reso = 0.10 + p.color * 0.20; + self.click_svf.set_freq(click_filter_freq, click_reso, self.sr); self.click_svf.reset(); // ── Path C: subharmonic (one octave below for phase-coherent low-end) ── self.sub_freq = self.freq_base * 0.5; // octave below, not a fourth self.sub_phase = 0.0; - self.sub_env = 0.25; // subtle reinforcement + // Color 0 → heavy sub (0.35), Color 1 → minimal sub (0.10) + self.sub_env = 0.35 - p.color * 0.25; // 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.color = p.color; self.active = true; } @@ -387,14 +396,14 @@ impl DrumVoiceDsp for KickVoice { // ── Path A: pitched sine body ── // Dual-stage pitch envelope: - // Stage 1 (fast): cubed envelope for sharp transient pitch drop - // Stage 2 (slow): switches once envelope drops below 0.4 for body tone forming + // Stage 1 (fast): squared envelope for transient pitch drop + // Stage 2 (slow): switches at 0.55 so color engages while pitch is still meaningful let pe = self.pitch_env; - if !self.pitch_stage2 && pe < 0.4 { + if !self.pitch_stage2 && pe < 0.55 { self.pitch_stage2 = true; self.pitch_decay = self.pitch_decay_slow; } - let pitch_shaped = pe * pe * pe; + let pitch_shaped = pe * pe; let freq = self.freq_base + (self.freq_start - self.freq_base) * pitch_shaped; self.pitch_env *= self.pitch_decay; @@ -404,8 +413,9 @@ impl DrumVoiceDsp for KickVoice { self.phase -= 1.0; } let sine = (self.phase * std::f64::consts::TAU).sin() as f32; - // 2nd harmonic: amount scales with drive (10-45%) for analog-like behavior - let harm_amount = 0.1 + self.drive * 0.35; + // 2nd harmonic: amount scales with drive AND color + // Color 0 → clean sine (5%), Color 1 → rich harmonics (up to 50% with drive) + let harm_amount = 0.05 + self.color * 0.20 + self.drive * 0.25; let harmonic2 = (self.phase * 2.0 * std::f64::consts::TAU).sin() as f32 * harm_amount; // Body LP filter + envelope @@ -1720,3 +1730,60 @@ pub fn create_drum_voices(sample_rate: f64) -> [Box; 8] { Box::new(TomVoice::new(sample_rate)), ] } + +#[cfg(test)] +mod tests { + use super::*; + use crate::sequencer::drum_pattern::DrumTrackParams; + + fn make_kick_params(sweep: f32, color: f32) -> DrumTrackParams { + DrumTrackParams { + tune: 0.3, sweep, color, snap: 0.45, + filter: 0.7, drive: 0.20, decay: 0.45, volume: 0.75, + send_reverb: 0.0, send_delay: 0.0, pan: 0.5, + mute: false, solo: false, + } + } + + fn generate_kick(sweep: f32, color: f32, num_samples: usize) -> Vec { + let mut voice = KickVoice::new(44100.0); + let params = make_kick_params(sweep, color); + voice.trigger(¶ms); + (0..num_samples).map(|_| voice.tick()).collect() + } + + fn rms_diff(a: &[f32], b: &[f32]) -> f32 { + let sum: f32 = a.iter().zip(b).map(|(x, y)| (x - y).powi(2)).sum(); + (sum / a.len() as f32).sqrt() + } + + #[test] + fn sweep_affects_waveform() { + let lo = generate_kick(0.0, 0.2, 4410); + let hi = generate_kick(1.0, 0.2, 4410); + let diff = rms_diff(&lo, &hi); + eprintln!("sweep RMS diff: {diff}"); + assert!(diff > 0.01, "sweep should change waveform, got RMS diff {diff}"); + } + + #[test] + fn color_affects_waveform() { + let lo = generate_kick(0.6, 0.0, 4410); + let hi = generate_kick(0.6, 1.0, 4410); + let diff = rms_diff(&lo, &hi); + eprintln!("color RMS diff: {diff}"); + assert!(diff > 0.01, "color should change waveform, got RMS diff {diff}"); + } + + #[test] + fn print_sample_comparison() { + let a = generate_kick(0.6, 0.0, 200); + let b = generate_kick(0.6, 1.0, 200); + eprintln!("\n--- color=0.0 vs color=1.0 (divergent samples) ---"); + for i in 0..200 { + if (a[i] - b[i]).abs() > 0.001 { + eprintln!(" [{i:3}] c0={:.5} c1={:.5} d={:.5}", a[i], b[i], a[i] - b[i]); + } + } + } +} diff --git a/src/audio/effects.rs b/src/audio/effects.rs index 56ee5b6..82105d0 100644 --- a/src/audio/effects.rs +++ b/src/audio/effects.rs @@ -224,6 +224,7 @@ const DELAY_BUF_SIZE: usize = 131072; // ~2.7s at 48kHz /// Musical subdivision for delay time. #[derive(Clone, Copy, Debug, PartialEq)] +#[allow(dead_code)] pub enum DelaySub { Sixteenth, // 1/16 SixteenthDotted, // 1/16D @@ -235,19 +236,20 @@ pub enum DelaySub { QuarterDotted, // 1/4D HalfTriplet, // 1/2T Half, // 1/2 + Whole, // 2/1 (two beats = half bar) } -pub const DELAY_SUBS: [DelaySub; 10] = [ - DelaySub::Sixteenth, - DelaySub::SixteenthDotted, - DelaySub::EighthTriplet, - DelaySub::Eighth, - DelaySub::EighthDotted, - DelaySub::QuarterTriplet, - DelaySub::Quarter, - DelaySub::QuarterDotted, - DelaySub::HalfTriplet, - DelaySub::Half, +/// Musical divisions: straight + dotted for polyrhythmic interest. +pub const DELAY_SUBS: [DelaySub; 9] = [ + DelaySub::Sixteenth, // 0.00-0.11 — tight slapback + DelaySub::SixteenthDotted, // 0.11-0.22 — dotted 1/16 bounce + DelaySub::Eighth, // 0.22-0.33 — straight 1/8 + DelaySub::EighthDotted, // 0.33-0.44 — dotted 1/8 (classic!) + DelaySub::Quarter, // 0.44-0.55 — straight 1/4 + DelaySub::QuarterDotted, // 0.55-0.66 — dotted 1/4 + DelaySub::Half, // 0.66-0.77 — spacious 1/2 + DelaySub::Whole, // 0.77-0.88 — full bar echo + DelaySub::Whole, // 0.88-1.00 — full bar + high feedback (infinite) ]; impl DelaySub { @@ -271,6 +273,7 @@ impl DelaySub { DelaySub::QuarterDotted => beat * 1.5, DelaySub::HalfTriplet => beat * 4.0 / 3.0, DelaySub::Half => beat * 2.0, + DelaySub::Whole => beat * 4.0, } } @@ -288,6 +291,7 @@ impl DelaySub { DelaySub::QuarterDotted => "1/4D", DelaySub::HalfTriplet => "1/2T", DelaySub::Half => "1/2", + DelaySub::Whole => "2/1", } } } @@ -297,10 +301,17 @@ pub struct DelayEffect { write_pos: usize, delay_samples: usize, feedback: f32, - // One-pole LP in feedback loop + // One-pole LP in feedback loop (darkening repeats) lp_state: f32, - lp_coeff: f32, // alpha + lp_coeff: f32, + // One-pole HP in feedback loop (thinning low-end buildup) — synth mode only + hp_state: f32, + hp_prev_in: f32, + hp_coeff: f32, wet: f32, + /// When true, feedback uses HP filter + saturation (musical for synths). + /// When false, feedback is clean LP-only (transparent for drums). + warm_mode: bool, } impl DelayEffect { @@ -308,33 +319,82 @@ impl DelayEffect { Self { buf: Box::new([0.0; DELAY_BUF_SIZE]), write_pos: 0, - delay_samples: 22050, // ~0.5s default + delay_samples: 22050, feedback: 0.4, lp_state: 0.0, lp_coeff: 0.5, + hp_state: 0.0, + hp_prev_in: 0.0, + hp_coeff: 0.995, wet: 0.3, + warm_mode: false, // default: clean/transparent (drums) } } - /// Update delay parameters. + /// Enable warm mode: HP filter + saturation in feedback (better for synths). + /// Default is clean mode (LP-only feedback, better for drums). + pub fn set_warm_mode(&mut self, warm: bool) { + self.warm_mode = warm; + } + + /// Update delay parameters (individual control). /// time: 0-1 (selects subdivision), feedback: 0-1, tone: 0-1 (LP cutoff in feedback). + #[allow(dead_code)] pub fn set_params(&mut self, time: f32, feedback: f32, tone: f32, bpm: f64, sample_rate: f64) { let sub = DelaySub::from_param(time); let delay_sec = sub.seconds(bpm); self.delay_samples = ((delay_sec * sample_rate) as usize).min(DELAY_BUF_SIZE - 1).max(1); - self.feedback = feedback.min(0.95); - self.wet = 0.4 + feedback * 0.4; // wet scales with feedback + self.feedback = feedback.min(0.80); + self.wet = 0.45; - // LP cutoff: 1000-12000 Hz mapped from tone - let freq = 1000.0 + tone as f64 * 11000.0; + // LP cutoff: 800-10000 Hz mapped from tone + let freq = 800.0 + tone as f64 * 9200.0; let rc = 1.0 / (2.0 * std::f64::consts::PI * freq); let dt = 1.0 / sample_rate; self.lp_coeff = (dt / (rc + dt)) as f32; + + // HP at ~60 Hz — tighter than 30Hz, removes more mud (Octatrack-style) + let hp_freq = 60.0_f64; + let hp_rc = 1.0 / (2.0 * std::f64::consts::PI * hp_freq); + self.hp_coeff = (hp_rc / (hp_rc + dt)) as f32; + } + + /// Single-knob delay macro: one 0-1 knob controls time, feedback, tone, and wet. + /// Beat-locked subdivisions including dotted for polyrhythmic bounce. + /// 0.0 = off + /// →1/16 → 1/16D → 1/8 → 1/8D → 1/4 → 1/4D → 1/2 → 2/1 → 2/1∞ + /// Last zone (>0.88) pushes feedback to 0.95 for near-infinite repeats. + pub fn set_single_knob(&mut self, amount: f32, bpm: f64, sample_rate: f64) { + // Snap to subdivision via the DELAY_SUBS table + // amount near 0 → shortest subdivision (1/16), not silence + let sub = DelaySub::from_param(amount.max(0.01)); + let delay_sec = sub.seconds(bpm); + self.delay_samples = ((delay_sec * sample_rate) as usize).min(DELAY_BUF_SIZE - 1).max(1); + + // Feedback: 0.30 (slapback) → 0.65 (spacious), last zone pushes to 0.95 (infinite) + self.feedback = if amount > 0.88 { + 0.95 + } else { + (0.30 + amount * 0.40).min(0.65) + }; + + // Wet: moderate, let send_delay control the balance + self.wet = 0.45 + amount * 0.15; + + // Tone: bright at low amounts, darker at high (LP cutoff 9kHz → 2kHz) + let tone_freq = 9000.0 - amount as f64 * 7000.0; + let rc = 1.0 / (2.0 * std::f64::consts::PI * tone_freq); + let dt = 1.0 / sample_rate; + self.lp_coeff = (dt / (rc + dt)) as f32; + + // HP at ~60 Hz + let hp_rc = 1.0 / (2.0 * std::f64::consts::PI * 60.0_f64); + self.hp_coeff = (hp_rc / (hp_rc + dt)) as f32; } /// Process one sample of delay input, return wet output. pub fn tick(&mut self, input: f32) -> f32 { - // Read from delay line + // Exact integer read position — tight tempo lock, no modulation let read_pos = if self.write_pos >= self.delay_samples { self.write_pos - self.delay_samples } else { @@ -343,11 +403,23 @@ impl DelayEffect { let delayed = self.buf[read_pos]; - // LP filter on feedback + // LP filter in feedback loop (each repeat gets darker) self.lp_state += self.lp_coeff * (delayed - self.lp_state); - // Write input + filtered feedback - self.buf[self.write_pos] = input + self.lp_state * self.feedback; + let saturated = if self.warm_mode { + // Warm mode (synths): HP filter + saturation for musical character + let hp_out = self.hp_coeff * (self.hp_state + self.lp_state - self.hp_prev_in); + self.hp_prev_in = self.lp_state; + self.hp_state = hp_out; + let sat_in = hp_out * self.feedback; + (sat_in * 1.5).tanh() / 1.5_f32.tanh() + } else { + // Clean mode (drums): LP-only, transparent repeats preserving transients + self.lp_state * self.feedback + }; + + // Write input + processed feedback + self.buf[self.write_pos] = input + saturated; self.write_pos = (self.write_pos + 1) % DELAY_BUF_SIZE; delayed * self.wet @@ -979,6 +1051,34 @@ impl SidechainEnvelope { mod tests { use super::*; + #[test] + fn delay_produces_output() { + let sr = 44100.0; + let mut delay = DelayEffect::new(); + delay.set_single_knob(0.5, 120.0, sr); // quarter note at 120 = 22050 samples + + // Feed a loud impulse for 100 samples (simulating a kick) + for _ in 0..100 { + delay.tick(0.8); + } + // Feed silence until the delay tap + let mut max_before = 0.0_f32; + for _ in 100..22050 { + let out = delay.tick(0.0); + max_before = max_before.max(out.abs()); + } + // Now read the delayed output (samples 22050-22200) + let mut max_delayed = 0.0_f32; + for _ in 0..200 { + let out = delay.tick(0.0); + max_delayed = max_delayed.max(out.abs()); + } + eprintln!("delay test: max_before_tap={max_before:.4}, max_after_tap={max_delayed:.4}"); + eprintln!(" delay_samples={}, feedback={}, wet={}", delay.delay_samples, delay.feedback, delay.wet); + assert!(max_before < 0.01, "should be silent before delay tap, got {max_before}"); + assert!(max_delayed > 0.05, "should hear delayed output, got {max_delayed}"); + } + #[test] fn test_ramped_param_instant_set() { let mut p = RampedParam::new(0.0); diff --git a/src/audio/engine.rs b/src/audio/engine.rs index 54bf318..16c94d4 100644 --- a/src/audio/engine.rs +++ b/src/audio/engine.rs @@ -99,7 +99,11 @@ impl SynthInstance { lfo2: Lfo::new(), saturator: TubeSaturator::new(sample_rate as f32), reverb: ReverbEffect::new(sample_rate), - delay: DelayEffect::new(), + delay: { + let mut d = DelayEffect::new(); + d.set_warm_mode(true); // synth delays: HP + saturation for character + d + }, } } } @@ -148,13 +152,7 @@ impl AudioEngine { let mut drum_reverb = FdnReverb::new(sample_rate); let mut drum_delay = DelayEffect::new(); drum_reverb.set_params(effect_params.reverb_amount, effect_params.reverb_damping); - drum_delay.set_params( - effect_params.delay_time, - effect_params.delay_feedback, - effect_params.delay_tone, - 120.0, - sample_rate, - ); + drum_delay.set_single_knob(effect_params.delay_time, 120.0, sample_rate); let compressor = GlueCompressor::new(sample_rate); let mut crush_compressor = GlueCompressor::new(sample_rate); @@ -206,14 +204,10 @@ impl AudioEngine { } // Update delay time when BPM changes if bpm_changed { - let ep = &self.effect_params; - self.drum_delay.set_params( - ep.delay_time, - ep.delay_feedback, - ep.delay_tone, - self.transport.bpm, - self.sample_rate, - ); + let dt = self.effect_params.delay_time; + self.drum_delay.set_single_knob(dt, self.transport.bpm, self.sample_rate); + self.synth_a.delay.set_single_knob(dt, self.transport.bpm, self.sample_rate); + self.synth_b.delay.set_single_knob(dt, self.transport.bpm, self.sample_rate); } } UiToAudio::SetDrumPattern(p) => { @@ -235,12 +229,8 @@ impl AudioEngine { self.effect_params = ep; self.drum_reverb .set_params(ep.reverb_amount, ep.reverb_damping); - self.drum_delay.set_params( - ep.delay_time, - ep.delay_feedback, - ep.delay_tone, - self.transport.bpm, - self.sample_rate, + self.drum_delay.set_single_knob( + ep.delay_time, self.transport.bpm, self.sample_rate, ); self.compressor .set_amount(ep.compressor_amount, self.sample_rate); @@ -249,6 +239,12 @@ impl AudioEngine { self.drum_saturator.set_drive(ep.drum_saturator_drive); self.synth_a.saturator.set_drive(ep.synth_saturator_drive); self.synth_b.saturator.set_drive(ep.synth_saturator_drive); + self.synth_a.delay.set_single_knob( + ep.delay_time, self.transport.bpm, self.sample_rate, + ); + self.synth_b.delay.set_single_knob( + ep.delay_time, self.transport.bpm, self.sample_rate, + ); } UiToAudio::TriggerDrum(track_id) => { let track = track_id as usize; @@ -560,10 +556,13 @@ impl AudioEngine { 1.0 }; - // Apply crossfader gain: center (0.5) = both full, extremes fade one out + // Apply crossfader gain: constant-power (Octatrack-style) cosine curve. + // Center (0.5) = both at ~0.707 (−3dB), extremes = one full / one silent. + // Total power stays constant across the full range — no volume dip. let xf = self.crossfader; - let gain_a = if xf <= 0.5 { 1.0 } else { 2.0 * (1.0 - xf) }; - let gain_b = if xf >= 0.5 { 1.0 } else { 2.0 * xf }; + let angle = xf * std::f32::consts::FRAC_PI_2; + let gain_a = angle.cos(); + let gain_b = angle.sin(); // Stereo mix: synths panned slightly to opposite sides, ducked by kick // Synth A slightly left (pan ~0.38), Synth B slightly right (pan ~0.62) diff --git a/src/params.rs b/src/params.rs index 9843ce4..257f811 100644 --- a/src/params.rs +++ b/src/params.rs @@ -8,9 +8,10 @@ use serde::{Deserialize, Serialize}; pub struct EffectParams { pub reverb_amount: f32, // 0.0-1.0: feedback/decay pub reverb_damping: f32, // 0.0-1.0: tail brightness (0=bright, 1=dark) - pub delay_time: f32, // 0.0-1.0: subdivision selector - pub delay_feedback: f32, // 0.0-1.0: echo repeats - pub delay_tone: f32, // 0.0-1.0: LP cutoff in feedback loop + #[serde(default = "default_delay_time")] + pub delay_time: f32, // 0.0-1.0: single-knob delay macro + pub delay_feedback: f32, // 0.0-1.0: (legacy, unused by single-knob) + pub delay_tone: f32, // 0.0-1.0: (legacy, unused by single-knob) #[serde(default)] pub compressor_amount: f32, // 0.0-1.0: master glue compressor (0=off) #[serde(default = "default_master_volume")] @@ -32,15 +33,16 @@ fn default_sidechain() -> f32 { 0.5 } fn default_master_volume() -> f32 { 0.8 } fn default_drum_volume() -> f32 { 1.0 } fn default_crossfader() -> f32 { 0.5 } +fn default_delay_time() -> f32 { 0.4 } // maps to ~1/8D (dotted eighth) impl Default for EffectParams { fn default() -> Self { Self { reverb_amount: 0.3, reverb_damping: 0.4, - delay_time: 0.5, // quarter note - delay_feedback: 0.3, - delay_tone: 0.6, + delay_time: 0.4, // dotted eighth (1/8D) — polyrhythmic bounce + delay_feedback: 0.45, + delay_tone: 0.5, compressor_amount: 0.0, master_volume: 0.8, drum_saturator_drive: 0.0, diff --git a/src/presets/drum_presets.rs b/src/presets/drum_presets.rs index a03a792..5f5c821 100644 --- a/src/presets/drum_presets.rs +++ b/src/presets/drum_presets.rs @@ -12,12 +12,12 @@ const fn ds(tune: f32, sweep: f32, color: f32, snap: f32, filter: f32, drive: f3 // ── Kick Presets ───────────────────────────────────────────────────────────── // Tuned for: dual-stage pitch env (stage1=2.5ms fast, stage2=color-controlled), // subharmonic at 0.5× (octave below), BP/LP click blend, -// drive-dependent 2nd harmonic (low drive = less harmonics). -// Key adjustments vs prior version: -// - Raise tune slightly on sub-heavy presets so subharmonic stays audible (>22Hz) -// - Bump drive on warm/full presets to compensate for reduced harmonics at low drive -// - Ease snap on presets that relied on softer LP click (BP blend is sharper) -// - color controls stage-2 only; low values still work but settle phase differs +// drive+color-dependent 2nd harmonic. +// Color is now a full timbre knob: dark/subby (0) → bright/snappy (1). +// - Pitch stage-2 decay: 5ms (color=0) → 200ms (color=1) +// - Click filter: 2kHz (warm) → 7kHz (sharp) +// - Sub level: 0.35 (heavy) → 0.10 (minimal) +// - 2nd harmonic: 5% (clean) → 25% + drive contribution pub static KICK_PRESETS: &[DrumSoundPreset] = &[ // 808 — subharmonic at octave below; raise tune so sub stays audible @@ -95,12 +95,12 @@ pub static OHH_PRESETS: &[DrumSoundPreset] = &[ // ── 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.75, 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.60, 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.80, 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.90, 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.65, 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.50, 0.50) }, + 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) }, ]; // ── Clap Presets ───────────────────────────────────────────────────────────── diff --git a/src/presets/pattern_presets.rs b/src/presets/pattern_presets.rs index d5ed073..ef6d34f 100644 --- a/src/presets/pattern_presets.rs +++ b/src/presets/pattern_presets.rs @@ -12,6 +12,67 @@ pub struct PatternPreset { // Track order: Kick, Snare, CHH, OHH, Ride, Clap, Cowbell, Tom pub static PATTERN_PRESETS: &[PatternPreset] = &[ + // ── Daft Punk ──────────────────────────────────────────────────────── + // Signature disco-house-electro patterns inspired by Daft Punk productions + PatternPreset { name: "Around the World", genre: "Daft Punk", + steps: ["88880000", "08080000", "a0a00000", "00000000", "00000000", "00000000", "00000000", "00000000"] }, + PatternPreset { name: "Da Funk", genre: "Daft Punk", + steps: ["c8880000", "08080000", "aaaa0000", "00000000", "00000000", "00000000", "00000000", "00000000"] }, + PatternPreset { name: "Revolution 909", genre: "Daft Punk", + steps: ["88880000", "00000000", "ffff0000", "00020000", "00000000", "08080000", "00000000", "00000000"] }, + PatternPreset { name: "One More Time", genre: "Daft Punk", + steps: ["88880000", "00000000", "aaaa0000", "01010000", "00000000", "08080000", "00000000", "00000000"] }, + PatternPreset { name: "Robot Rock", genre: "Daft Punk", + steps: ["c8c80000", "08080000", "00000000", "00000000", "aaaa0000", "00000000", "00000000", "00000000"] }, + PatternPreset { name: "Get Lucky", genre: "Daft Punk", + steps: ["82820000", "08080000", "aaaa0000", "04040000", "00000000", "00000000", "00000000", "00000000"] }, + PatternPreset { name: "Harder Better", genre: "Daft Punk", + steps: ["88880000", "00000000", "ffff0000", "00000000", "00000000", "08080000", "22220000", "00000000"] }, + PatternPreset { name: "Giorgio", genre: "Daft Punk", + steps: ["88880000", "08080000", "ffff0000", "22220000", "00000000", "00000000", "00000000", "00000000"] }, + + // ── Basics ────────────────────────────────────────────────────────── + // Progressive building blocks: offbeat hats (3,7,11,15) for proper techno feel + PatternPreset { name: "Kick Only", genre: "Basics", + steps: ["88880000", "00000000", "00000000", "00000000", "00000000", "00000000", "00000000", "00000000"] }, + PatternPreset { name: "Kick + Hat", genre: "Basics", + steps: ["88880000", "00000000", "22220000", "00000000", "00000000", "00000000", "00000000", "00000000"] }, + PatternPreset { name: "Kick + Snare", genre: "Basics", + steps: ["88880000", "08080000", "00000000", "00000000", "00000000", "00000000", "00000000", "00000000"] }, + PatternPreset { name: "Kick + Clap", genre: "Basics", + steps: ["88880000", "00000000", "00000000", "00000000", "00000000", "08080000", "00000000", "00000000"] }, + PatternPreset { name: "K + S + HH", genre: "Basics", + steps: ["88880000", "08080000", "22220000", "00000000", "00000000", "00000000", "00000000", "00000000"] }, + PatternPreset { name: "K + Clap + HH", genre: "Basics", + steps: ["88880000", "00000000", "22220000", "00000000", "00000000", "08080000", "00000000", "00000000"] }, + PatternPreset { name: "K + S + HH + OH", genre: "Basics", + steps: ["88880000", "08080000", "22220000", "04040000", "00000000", "00000000", "00000000", "00000000"] }, + PatternPreset { name: "Full Kit", genre: "Basics", + steps: ["88880000", "08080000", "22220000", "04040000", "00000000", "08080000", "00000000", "00000000"] }, + + // ── Techno II ────────────────────────────────────────────────────── + // Clean, functional patterns — offbeat hats for proper techno + PatternPreset { name: "Offbeat Hats", genre: "Techno II", + steps: ["88880000", "00000000", "00000000", "22220000", "00000000", "00000000", "00000000", "00000000"] }, + PatternPreset { name: "Ride Driver", genre: "Techno II", + steps: ["88880000", "00000000", "00000000", "00000000", "22220000", "08080000", "00000000", "00000000"] }, + PatternPreset { name: "Half Time", genre: "Techno II", + steps: ["80800000", "00800000", "22220000", "00000000", "00000000", "00000000", "00000000", "00000000"] }, + PatternPreset { name: "Double Kick", genre: "Techno II", + steps: ["c8c80000", "08080000", "22220000", "00000000", "00000000", "00000000", "00000000", "00000000"] }, + PatternPreset { name: "Syncopated K", genre: "Techno II", + steps: ["a0a00000", "08080000", "22220000", "00000000", "00000000", "00000000", "00000000", "00000000"] }, + PatternPreset { name: "Offbeat Kick", genre: "Techno II", + steps: ["82820000", "08080000", "22220000", "00000000", "00000000", "00000000", "00000000", "00000000"] }, + PatternPreset { name: "808 Cowbell", genre: "Techno II", + steps: ["88880000", "00000000", "22220000", "00000000", "00000000", "00000000", "22220000", "00000000"] }, + PatternPreset { name: "Tom Groove", genre: "Techno II", + steps: ["88880000", "00000000", "22220000", "00000000", "00000000", "08080000", "00000000", "22220000"] }, + PatternPreset { name: "Stripped 16ths", genre: "Techno II", + steps: ["88880000", "00000000", "ffff0000", "00000000", "00000000", "08080000", "00000000", "00000000"] }, + PatternPreset { name: "Perc Stack", genre: "Techno II", + steps: ["88880000", "00000000", "22220000", "04040000", "00000000", "08080000", "20200000", "00200020"] }, + // ── Techno ─────────────────────────────────────────────────────────── PatternPreset { name: "Four on the Floor", genre: "Techno", steps: ["88880000", "00000000", "aaaa0000", "00000000", "22220000", "08080000", "00000000", "00000000"] }, diff --git a/src/presets/synth_pattern_presets.rs b/src/presets/synth_pattern_presets.rs index dc174fd..7b50980 100644 --- a/src/presets/synth_pattern_presets.rs +++ b/src/presets/synth_pattern_presets.rs @@ -10,6 +10,42 @@ pub struct SynthPatternPreset { } pub static SYNTH_PATTERN_PRESETS: &[SynthPatternPreset] = &[ + // ── Daft Punk Bass ─────────────────────────────────────────────────── + // Bass lines (Synth A) — pair with matching Daft Punk Lead on Synth B + SynthPatternPreset { name: "Around World Bass", genre: "Daft Punk Bass", + steps: [(43, 100, 3), (0, 0, 1), (0, 0, 1), (0, 0, 1), (46, 100, 2), (0, 0, 1), (48, 100, 2), (0, 0, 1), (0, 0, 1), (0, 0, 1), (50, 100, 4), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (43, 100, 3), (0, 0, 1), (0, 0, 1), (0, 0, 1), (46, 100, 2), (0, 0, 1), (48, 100, 2), (0, 0, 1), (50, 100, 2), (0, 0, 1), (48, 100, 2), (0, 0, 1), (46, 100, 4), (0, 0, 1), (0, 0, 1), (0, 0, 1)] }, + SynthPatternPreset { name: "Da Funk Bass", genre: "Daft Punk Bass", + steps: [(43, 127, 1), (43, 100, 1), (0, 0, 1), (0, 0, 1), (43, 127, 1), (0, 0, 1), (0, 0, 1), (43, 100, 1), (43, 127, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (43, 127, 1), (0, 0, 1), (43, 100, 1), (0, 0, 1), (43, 127, 1), (43, 100, 1), (0, 0, 1), (0, 0, 1), (43, 127, 1), (0, 0, 1), (0, 0, 1), (43, 100, 1), (43, 127, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (43, 127, 1), (0, 0, 1), (43, 100, 1), (0, 0, 1)] }, + SynthPatternPreset { name: "One More Time Bass", genre: "Daft Punk Bass", + steps: [(48, 127, 1), (0, 0, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (48, 100, 1), (45, 127, 1), (0, 0, 1), (0, 0, 1), (45, 100, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (45, 100, 1), (48, 127, 1), (0, 0, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (48, 100, 1), (45, 127, 1), (0, 0, 1), (0, 0, 1), (45, 100, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (45, 100, 1)] }, + SynthPatternPreset { name: "Harder Better Bass", genre: "Daft Punk Bass", + steps: [(36, 127, 1), (0, 0, 1), (36, 100, 1), (0, 0, 1), (36, 127, 1), (0, 0, 1), (0, 0, 1), (36, 100, 1), (36, 127, 1), (0, 0, 1), (36, 100, 1), (0, 0, 1), (36, 127, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (36, 127, 1), (0, 0, 1), (36, 100, 1), (0, 0, 1), (36, 127, 1), (0, 0, 1), (0, 0, 1), (36, 100, 1), (36, 127, 1), (0, 0, 1), (36, 100, 1), (0, 0, 1), (36, 127, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)] }, + SynthPatternPreset { name: "Robot Rock Bass", genre: "Daft Punk Bass", + steps: [(36, 127, 1), (36, 100, 1), (0, 0, 1), (0, 0, 1), (36, 127, 1), (36, 100, 1), (0, 0, 1), (0, 0, 1), (36, 127, 1), (36, 100, 1), (0, 0, 1), (0, 0, 1), (36, 127, 1), (0, 0, 1), (36, 100, 1), (0, 0, 1), (36, 127, 1), (36, 100, 1), (0, 0, 1), (0, 0, 1), (36, 127, 1), (36, 100, 1), (0, 0, 1), (0, 0, 1), (36, 127, 1), (36, 100, 1), (0, 0, 1), (0, 0, 1), (36, 127, 1), (0, 0, 1), (36, 100, 1), (0, 0, 1)] }, + SynthPatternPreset { name: "Get Lucky Bass", genre: "Daft Punk Bass", + steps: [(47, 100, 1), (0, 0, 1), (0, 0, 1), (47, 100, 1), (0, 0, 1), (50, 100, 1), (0, 0, 1), (0, 0, 1), (54, 100, 1), (0, 0, 1), (0, 0, 1), (54, 100, 1), (0, 0, 1), (52, 100, 1), (0, 0, 1), (0, 0, 1), (47, 100, 1), (0, 0, 1), (0, 0, 1), (47, 100, 1), (0, 0, 1), (50, 100, 1), (0, 0, 1), (0, 0, 1), (54, 100, 1), (0, 0, 1), (0, 0, 1), (54, 100, 1), (0, 0, 1), (52, 100, 1), (0, 0, 1), (0, 0, 1)] }, + SynthPatternPreset { name: "Giorgio Bass", genre: "Daft Punk Bass", + steps: [(48, 100, 1), (48, 100, 1), (48, 100, 1), (60, 100, 1), (48, 100, 1), (48, 100, 1), (48, 100, 1), (60, 100, 1), (48, 100, 1), (48, 100, 1), (48, 100, 1), (60, 100, 1), (48, 100, 1), (48, 100, 1), (60, 127, 1), (48, 100, 1), (48, 100, 1), (48, 100, 1), (48, 100, 1), (60, 100, 1), (48, 100, 1), (48, 100, 1), (48, 100, 1), (60, 100, 1), (48, 100, 1), (48, 100, 1), (48, 100, 1), (60, 100, 1), (48, 100, 1), (48, 100, 1), (60, 127, 1), (48, 100, 1)] }, + SynthPatternPreset { name: "Digital Love Bass", genre: "Daft Punk Bass", + steps: [(42, 127, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (42, 100, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (42, 127, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (42, 100, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (42, 127, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (42, 100, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (42, 127, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (42, 100, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)] }, + // ── Daft Punk Lead ────────────────────────────────────────────────── + // Lead/melody lines (Synth B) — pair with matching Daft Punk Bass on Synth A + SynthPatternPreset { name: "Around World Lead", genre: "Daft Punk Lead", + steps: [(67, 100, 4), (0, 0, 1), (0, 0, 1), (0, 0, 1), (70, 100, 4), (0, 0, 1), (0, 0, 1), (0, 0, 1), (72, 100, 4), (0, 0, 1), (0, 0, 1), (0, 0, 1), (74, 100, 4), (0, 0, 1), (0, 0, 1), (0, 0, 1), (74, 100, 4), (0, 0, 1), (0, 0, 1), (0, 0, 1), (72, 100, 4), (0, 0, 1), (0, 0, 1), (0, 0, 1), (70, 100, 4), (0, 0, 1), (0, 0, 1), (0, 0, 1), (67, 100, 4), (0, 0, 1), (0, 0, 1), (0, 0, 1)] }, + SynthPatternPreset { name: "Da Funk Lead", genre: "Daft Punk Lead", + steps: [(55, 127, 1), (55, 100, 1), (0, 0, 1), (56, 100, 1), (55, 127, 1), (0, 0, 1), (0, 0, 1), (55, 100, 1), (53, 100, 1), (0, 0, 1), (55, 127, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (56, 100, 1), (0, 0, 1), (55, 127, 1), (55, 100, 1), (0, 0, 1), (56, 100, 1), (55, 127, 1), (0, 0, 1), (0, 0, 1), (55, 100, 1), (53, 100, 1), (0, 0, 1), (55, 127, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (56, 100, 1), (0, 0, 1)] }, + SynthPatternPreset { name: "One More Time Lead", genre: "Daft Punk Lead", + steps: [(60, 100, 1), (0, 0, 1), (64, 100, 1), (0, 0, 1), (67, 100, 1), (0, 0, 1), (72, 100, 1), (0, 0, 1), (67, 100, 1), (0, 0, 1), (64, 100, 1), (0, 0, 1), (60, 100, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1), (60, 100, 1), (0, 0, 1), (64, 100, 1), (0, 0, 1), (67, 100, 1), (0, 0, 1), (72, 100, 1), (0, 0, 1), (67, 100, 1), (0, 0, 1), (64, 100, 1), (0, 0, 1), (60, 100, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)] }, + SynthPatternPreset { name: "Harder Better Lead", genre: "Daft Punk Lead", + steps: [(48, 127, 1), (48, 100, 1), (0, 0, 1), (48, 100, 1), (48, 127, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1), (48, 127, 1), (48, 100, 1), (48, 100, 1), (0, 0, 1), (48, 127, 1), (0, 0, 1), (0, 0, 1), (48, 100, 1), (48, 127, 1), (48, 100, 1), (0, 0, 1), (48, 100, 1), (48, 127, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1), (48, 127, 1), (48, 100, 1), (48, 100, 1), (0, 0, 1), (48, 127, 1), (0, 0, 1), (0, 0, 1), (48, 100, 1)] }, + SynthPatternPreset { name: "Robot Rock Lead", genre: "Daft Punk Lead", + steps: [(48, 127, 1), (48, 100, 1), (0, 0, 1), (0, 0, 1), (48, 127, 1), (48, 100, 1), (0, 0, 1), (0, 0, 1), (48, 127, 1), (48, 100, 1), (0, 0, 1), (0, 0, 1), (48, 127, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1), (48, 127, 1), (48, 100, 1), (0, 0, 1), (0, 0, 1), (48, 127, 1), (48, 100, 1), (0, 0, 1), (0, 0, 1), (48, 127, 1), (48, 100, 1), (0, 0, 1), (0, 0, 1), (48, 127, 1), (0, 0, 1), (48, 100, 1), (0, 0, 1)] }, + SynthPatternPreset { name: "Get Lucky Lead", genre: "Daft Punk Lead", + steps: [(59, 100, 1), (0, 0, 1), (62, 100, 1), (59, 100, 1), (0, 0, 1), (62, 100, 1), (0, 0, 1), (62, 100, 1), (66, 100, 1), (0, 0, 1), (66, 100, 1), (66, 100, 1), (0, 0, 1), (64, 100, 1), (0, 0, 1), (64, 100, 1), (59, 100, 1), (0, 0, 1), (62, 100, 1), (59, 100, 1), (0, 0, 1), (62, 100, 1), (0, 0, 1), (62, 100, 1), (66, 100, 1), (0, 0, 1), (66, 100, 1), (66, 100, 1), (0, 0, 1), (64, 100, 1), (0, 0, 1), (64, 100, 1)] }, + SynthPatternPreset { name: "Giorgio Lead", genre: "Daft Punk Lead", + 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)] }, // ── 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/project.rs b/src/sequencer/project.rs index dd2c262..7561204 100644 --- a/src/sequencer/project.rs +++ b/src/sequencer/project.rs @@ -586,6 +586,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("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), @@ -595,12 +596,12 @@ pub fn demo_project() -> ProjectFile { pattern_from_preset("Amen Break", "Drum & Bass 174", 174.0), pattern_from_preset("Electro Funk", "Electro 128", 128.0), pattern_from_preset("Basic Chain", "Dub Techno 118", 118.0), - pattern_from_preset("Sparse Pulse", "Ambient 90", 90.0), ]; let kits = genre_kits(); let synth_patterns = vec![ + synth_pattern_from_preset("Around World 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"), @@ -610,19 +611,19 @@ pub fn demo_project() -> ProjectFile { synth_pattern_from_preset("Drum & Bass Bass 1", "DnB Bass"), synth_pattern_from_preset("Electro Bass 1", "Electro Bass"), synth_pattern_from_preset("Dub Techno Bass 1", "Dub Techno Bass"), - synth_pattern_from_preset("Ambient Bass 1", "Ambient Bass"), ]; let synth_kits = vec![ - synth_kit_from_preset("Wobble Bass", "Wobble Bass"), // Kit 0: Acid Techno - synth_kit_from_preset("Acid Bass", "Acid Bass"), // Kit 1: House - synth_kit_from_preset("Reese Bass", "Reese Bass"), // Kit 2: Deep House - synth_kit_from_preset("Pulse Bass", "Pulse Bass"), // Kit 3: Techno - synth_kit_from_preset("Sub Bass", "Sub Bass"), // Kit 4: Downtempo, Dub Techno - synth_kit_from_preset("FM Bass", "FM Bass"), // Kit 5: Trance, Ambient (reuse) - synth_kit_from_preset("Growl Bass", "Growl Bass"), // Kit 6: DnB - synth_kit_from_preset("Rubber Bass", "Rubber Bass"), // Kit 7: Electro + synth_kit_from_preset("Electric Piano", "Electric Piano"), // Kit 0: Around the World + 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 + synth_kit_from_preset("Pulse Bass", "Pulse Bass"), // Kit 4: Techno + synth_kit_from_preset("Sub Bass", "Sub Bass"), // Kit 5: Downtempo, Dub Techno + synth_kit_from_preset("FM Bass", "FM Bass"), // Kit 6: Trance, Ambient (reuse) + 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("Acid Techno 1", "Acid Techno Lead"), synth_pattern_from_preset("House 1", "House Keys"), synth_pattern_from_preset("House 3", "Deep House Pad"), @@ -632,17 +633,16 @@ pub fn demo_project() -> ProjectFile { synth_pattern_from_preset("Drum & Bass 1", "DnB Pluck"), synth_pattern_from_preset("Electro 1", "Electro Lead"), synth_pattern_from_preset("Dub Techno 1", "Dub Techno Pad"), - synth_pattern_from_preset("Ambient 1", "Ambient Bells"), ]; let synth_b_kits = vec![ - synth_kit_from_preset("Screamer", "Screamer"), // Kit 0: Acid Techno - synth_kit_from_preset("Electric Piano", "Electric Piano"), // Kit 1: House - synth_kit_from_preset("Shimmer Pad", "Shimmer Pad"), // Kit 2: Deep House, Dub Techno - synth_kit_from_preset("Saw Lead", "Saw Lead"), // Kit 3: Techno - synth_kit_from_preset("Warm Pad", "Warm Pad"), // Kit 4: Downtempo - synth_kit_from_preset("Trance Lead", "Trance Lead"), // Kit 5: Trance - synth_kit_from_preset("Basic Pluck", "Basic Pluck"), // Kit 6: DnB, Ambient - synth_kit_from_preset("Square Lead", "Square Lead"), // Kit 7: Electro + synth_kit_from_preset("Acid Bass", "Acid Bass"), // Kit 0: Around the World lead + synth_kit_from_preset("Screamer", "Screamer"), // Kit 1: Acid Techno + synth_kit_from_preset("Electric Piano", "Electric Piano"), // Kit 2: House + synth_kit_from_preset("Shimmer Pad", "Shimmer Pad"), // Kit 3: Deep House, Dub Techno + synth_kit_from_preset("Saw Lead", "Saw Lead"), // Kit 4: Techno + synth_kit_from_preset("Warm Pad", "Warm Pad"), // Kit 5: Downtempo + synth_kit_from_preset("Trance Lead", "Trance Lead"), // Kit 6: Trance + synth_kit_from_preset("Basic Pluck", "Basic Pluck"), // Kit 7: DnB, Ambient ]; ProjectFile { @@ -656,7 +656,7 @@ pub fn demo_project() -> ProjectFile { active_kit: 0, patterns, active_pattern: 0, - bpm: 138.0, + bpm: 121.0, loop_length: 32, swing: 0.50, effects: EffectParams::default(), @@ -669,16 +669,16 @@ pub fn demo_project() -> ProjectFile { synth_b_patterns, active_synth_b_pattern: 0, scenes: vec![ - Some(Scene { name: "bonza".into(), drum_pattern: 5, drum_kit: 7, synth_a_pattern: 5, synth_a_kit: 5, synth_b_pattern: 4, synth_b_kit: 5, bpm: 140.0, swing: 0.50 }), - Some(Scene { name: "Classic House".into(), drum_pattern: 1, drum_kit: 3, synth_a_pattern: 1, synth_a_kit: 1, synth_b_pattern: 1, synth_b_kit: 1, bpm: 122.0, swing: 0.50 }), - Some(Scene { name: "Deep 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: 120.0, swing: 0.50 }), - Some(Scene { name: "Driving Techno".into(),drum_pattern: 3, drum_kit: 2, synth_a_pattern: 3, synth_a_kit: 3, synth_b_pattern: 3, synth_b_kit: 3, bpm: 130.0, swing: 0.50 }), - Some(Scene { name: "Lo-Fi Hip Hop".into(), drum_pattern: 4, drum_kit: 5, synth_a_pattern: 4, synth_a_kit: 4, synth_b_pattern: 4, synth_b_kit: 4, bpm: 85.0, swing: 0.50 }), - Some(Scene { name: "Trance".into(), drum_pattern: 5, drum_kit: 1, synth_a_pattern: 5, synth_a_kit: 5, synth_b_pattern: 5, synth_b_kit: 5, bpm: 140.0, swing: 0.50 }), - Some(Scene { name: "Drum & Bass".into(), drum_pattern: 6, drum_kit: 6, synth_a_pattern: 6, synth_a_kit: 6, synth_b_pattern: 6, synth_b_kit: 6, bpm: 174.0, swing: 0.50 }), - Some(Scene { name: "Electro Funk".into(), drum_pattern: 7, drum_kit: 6, synth_a_pattern: 7, synth_a_kit: 7, synth_b_pattern: 7, synth_b_kit: 7, bpm: 128.0, swing: 0.50 }), - Some(Scene { name: "Dub Techno".into(), drum_pattern: 8, drum_kit: 2, synth_a_pattern: 8, synth_a_kit: 4, synth_b_pattern: 8, synth_b_kit: 2, bpm: 118.0, swing: 0.50 }), - Some(Scene { name: "Ambient".into(), drum_pattern: 9, drum_kit: 7, synth_a_pattern: 9, synth_a_kit: 5, synth_b_pattern: 9, synth_b_kit: 6, bpm: 90.0, swing: 0.50 }), + 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: "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 }), + Some(Scene { name: "Driving Techno".into(),drum_pattern: 4, drum_kit: 2, synth_a_pattern: 4, synth_a_kit: 4, synth_b_pattern: 4, synth_b_kit: 4, bpm: 130.0, swing: 0.50 }), + Some(Scene { name: "Lo-Fi Hip Hop".into(), drum_pattern: 5, drum_kit: 5, synth_a_pattern: 5, synth_a_kit: 5, synth_b_pattern: 5, synth_b_kit: 5, bpm: 85.0, swing: 0.50 }), + Some(Scene { name: "Trance".into(), drum_pattern: 6, drum_kit: 1, synth_a_pattern: 6, synth_a_kit: 6, synth_b_pattern: 6, synth_b_kit: 6, bpm: 140.0, swing: 0.50 }), + Some(Scene { name: "Drum & Bass".into(), drum_pattern: 7, drum_kit: 6, synth_a_pattern: 7, synth_a_kit: 7, synth_b_pattern: 7, synth_b_kit: 7, bpm: 174.0, swing: 0.50 }), + Some(Scene { name: "Electro Funk".into(), drum_pattern: 8, drum_kit: 6, synth_a_pattern: 8, synth_a_kit: 1, synth_b_pattern: 8, synth_b_kit: 2, bpm: 128.0, swing: 0.50 }), + Some(Scene { name: "Dub Techno".into(), drum_pattern: 9, drum_kit: 2, synth_a_pattern: 9, synth_a_kit: 5, synth_b_pattern: 9, synth_b_kit: 3, bpm: 118.0, swing: 0.50 }), ], } } @@ -1208,9 +1208,9 @@ mod demo_tests { #[test] fn demo_patterns_have_bpm() { let proj = demo_project(); - assert!((proj.patterns[0].bpm - 138.0).abs() < 0.01); // Acid Techno - assert!((proj.patterns[6].bpm - 174.0).abs() < 0.01); // D&B - assert!((proj.patterns[9].bpm - 90.0).abs() < 0.01); // Ambient + assert!((proj.patterns[0].bpm - 121.0).abs() < 0.01); // Around the World + assert!((proj.patterns[7].bpm - 174.0).abs() < 0.01); // D&B + assert!((proj.patterns[9].bpm - 118.0).abs() < 0.01); // Dub Techno } #[test] @@ -1228,8 +1228,8 @@ 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, "Wobble Bass"); - assert_eq!(proj.synth_b_kits[0].name, "Screamer"); + assert_eq!(proj.synth_kits[0].name, "Electric Piano"); + assert_eq!(proj.synth_b_kits[0].name, "Acid Bass"); } #[test] @@ -1253,9 +1253,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, "Acid Techno 138"); - assert!((loaded.patterns[0].bpm - 138.0).abs() < 0.01); - assert_eq!(loaded.synth_kits[0].name, "Wobble Bass"); + 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"); } /// Render all genre kit voices to WAV files for auditioning. diff --git a/src/sequencer/transport.rs b/src/sequencer/transport.rs index 0739126..62fa124 100644 --- a/src/sequencer/transport.rs +++ b/src/sequencer/transport.rs @@ -54,7 +54,7 @@ impl Default for Transport { fn default() -> Self { Self { state: PlayState::Stopped, - bpm: 120.0, + bpm: 121.0, record_mode: RecordMode::Off, loop_config: LoopConfig::default(), swing: 0.50,