use super::*;
pub(crate) const BASS_LINES: [[i32; 8]; 8] = [
[45, 47, 45, 43, 52, 53, 45, 45], [45, 50, 48, 43, 41, 52, 45, 43], [45, 41, 48, 43, 50, 52, 47, 43], [45, 41, 48, 43, 50, 52, 47, 43], [45, 46, 48, 50, 52, 43, 43, 45], [52, 47, 50, 45, 45, 52, 43, 52], [48, 55, 45, 53, 48, 55, 53, 48], [43, 50, 52, 48, 43, 50, 52, 48], ];
pub(crate) fn bass_root_note(progression: usize, step: usize, pad: &PadControls) -> i32 {
if is_custom_progression(progression) {
pad_chord_root_note(&pad.chord_slots[step])
} else {
BASS_LINES[progression % BASS_LINES.len()][step % 8]
}
}
pub(crate) const BASS_RHYTHMS: [[bool; 16]; 4] = [
[
true, false, false, false, true, false, false, false, true, false, false, false, true,
false, false, false,
],
[
true, false, false, true, false, false, true, false, true, true, false, false, true, true,
false, false,
],
[
true, false, true, false, true, false, true, false, true, false, true, false, true, false,
true, false,
],
[
true, false, false, true, false, false, true, false, true, false, false, true, false,
false, true, false,
],
];
pub(crate) const BASS_STEP_BEATS: f32 = 0.25;
const BASS_MONO_FADE_SECONDS: f32 = 0.003;
pub(crate) struct BassEngine {
pub(crate) sample_rate: f32,
pub(crate) progression: ProgressionFollower,
pub(crate) step_trigger: GridTrigger,
pub(crate) rhythm_step: usize,
pub(crate) voice: Option<BassVoice>,
pub(crate) fading_voice: Option<BassVoice>,
pub(crate) fade_samples_remaining: u32,
pub(crate) fade_total_samples: u32,
pub(crate) center_gains: (f32, f32),
}
impl BassEngine {
pub(crate) fn new(sample_rate: f32) -> Self {
Self {
sample_rate,
progression: ProgressionFollower::new(),
step_trigger: GridTrigger::new(),
rhythm_step: BASS_RHYTHMS[0].len() - 1,
voice: None,
fading_voice: None,
fade_samples_remaining: 0,
fade_total_samples: (sample_rate * BASS_MONO_FADE_SECONDS).max(1.0) as u32,
center_gains: StereoPanner::gains(0.0),
}
}
pub(crate) fn next(
&mut self,
c: &BassControls,
pad: &PadControls,
tune: f32,
timing: TimingContext,
) -> (f32, f32) {
let (progression, step) = self.progression.follow(pad, timing);
let loop_len = (c.interval_beats / BASS_STEP_BEATS)
.round()
.clamp(1.0, 32.0) as usize;
if self
.step_trigger
.pop_swung(timing, BASS_STEP_BEATS, c.offset_beats, c.swing)
{
self.rhythm_step = (self.rhythm_step + 1) % loop_len;
let rhythm = (c.rhythm.round() as usize) % BASS_RHYTHMS.len();
let hit = self.rhythm_step < BASS_RHYTHMS[rhythm].len()
&& BASS_RHYTHMS[rhythm][self.rhythm_step];
if hit {
let note = bass_root_note(progression, step, pad) + (c.octave.round() as i32) * 12;
let hz = note_hz(note, tune);
self.fading_voice = self.voice.take();
self.fade_samples_remaining = self.fade_total_samples;
self.voice = Some(BassVoice::new(
wrapped_index(c.voice_type, BASS_TYPES.len()),
hz,
c.attack_time,
c.decay_time,
self.sample_rate,
));
}
}
let (gain_l, gain_r) = self.center_gains;
let mut l = 0.0f32;
let mut r = 0.0f32;
if let Some(voice) = &mut self.voice {
let s = voice.next();
l += s * gain_l;
r += s * gain_r;
if voice.is_done() {
self.voice = None;
}
}
if let Some(voice) = &mut self.fading_voice {
let s = voice.next();
let fade_gain = self.fade_samples_remaining as f32 / self.fade_total_samples as f32;
l += s * gain_l * fade_gain;
r += s * gain_r * fade_gain;
self.fade_samples_remaining = self.fade_samples_remaining.saturating_sub(1);
if self.fade_samples_remaining == 0 {
self.fading_voice = None;
}
}
(l * c.level, r * c.level)
}
}
pub(crate) enum BassVoice {
Sub(SubBassVoice),
Saw(SawBassVoice),
Pluck(PluckBassVoice),
}
impl BassVoice {
pub(crate) fn new(
voice_type: usize,
hz: f32,
attack_time: f32,
decay_time: f32,
sample_rate: f32,
) -> Self {
match voice_type {
0 => Self::Sub(SubBassVoice::new(hz, attack_time, decay_time, sample_rate)),
1 => Self::Saw(SawBassVoice::new(hz, attack_time, decay_time, sample_rate)),
_ => Self::Pluck(PluckBassVoice::new(
hz,
attack_time,
decay_time,
sample_rate,
)),
}
}
pub(crate) fn next(&mut self) -> f32 {
match self {
Self::Sub(voice) => voice.next(),
Self::Saw(voice) => voice.next(),
Self::Pluck(voice) => voice.next(),
}
}
pub(crate) fn is_done(&self) -> bool {
match self {
Self::Sub(voice) => voice.is_done(),
Self::Saw(voice) => voice.is_done(),
Self::Pluck(voice) => voice.is_done(),
}
}
}
pub(crate) struct BassVoiceCore {
pub(crate) envelope: Adsr,
}
impl BassVoiceCore {
pub(crate) fn new(attack_time: f32, decay_time: f32, sample_rate: f32) -> Self {
Self {
envelope: Adsr::new(attack_time, decay_time, 0.0, decay_time, sample_rate),
}
}
pub(crate) fn is_done(&self) -> bool {
self.envelope.is_done()
}
}
pub(crate) struct SubBassVoice {
pub(crate) osc: SineOscillator,
pub(crate) core: BassVoiceCore,
}
impl SubBassVoice {
pub(crate) fn new(hz: f32, attack_time: f32, decay_time: f32, sample_rate: f32) -> Self {
Self {
osc: SineOscillator::new(hz, sample_rate),
core: BassVoiceCore::new(attack_time, decay_time, sample_rate),
}
}
pub(crate) fn next(&mut self) -> f32 {
self.osc.next() * self.core.envelope.next()
}
pub(crate) fn is_done(&self) -> bool {
self.core.is_done()
}
}
const BASS_SAW_HARMONIC_COUNT: usize = 4;
const BASS_SAW_HARMONIC_GAINS: [f32; BASS_SAW_HARMONIC_COUNT] = [1.0, 0.46, 0.24, 0.14];
const BASS_SAW_OUTPUT_GAIN: f32 = 0.62;
pub(crate) struct SawBassVoice {
pub(crate) oscillators: [SineOscillator; BASS_SAW_HARMONIC_COUNT],
pub(crate) core: BassVoiceCore,
}
impl SawBassVoice {
pub(crate) fn new(hz: f32, attack_time: f32, decay_time: f32, sample_rate: f32) -> Self {
Self {
oscillators: std::array::from_fn(|index| {
SineOscillator::new(hz * (index + 1) as f32, sample_rate)
}),
core: BassVoiceCore::new(attack_time, decay_time, sample_rate),
}
}
pub(crate) fn next(&mut self) -> f32 {
let mut raw = 0.0f32;
for (osc, gain) in self.oscillators.iter_mut().zip(BASS_SAW_HARMONIC_GAINS) {
raw += osc.next() * gain;
}
raw * BASS_SAW_OUTPUT_GAIN * self.core.envelope.next()
}
pub(crate) fn is_done(&self) -> bool {
self.core.is_done()
}
}
const BASS_PLUCK_ENVELOPE_SCALE: f32 = 0.4;
const BASS_PLUCK_TRANSIENT_DECAY_SECONDS: f32 = 0.02;
const BASS_PLUCK_TRANSIENT_MIX: f32 = 0.5;
const BASS_PLUCK_OUTPUT_GAIN: f32 = 0.82;
pub(crate) struct PluckBassVoice {
pub(crate) osc: SineOscillator,
pub(crate) transient_osc: SineOscillator,
pub(crate) core: BassVoiceCore,
pub(crate) transient_samples_remaining: u64,
pub(crate) transient_total_samples: f32,
}
impl PluckBassVoice {
pub(crate) fn new(hz: f32, attack_time: f32, decay_time: f32, sample_rate: f32) -> Self {
let attack_time = (attack_time * BASS_PLUCK_ENVELOPE_SCALE).max(0.001);
let decay_time = (decay_time * BASS_PLUCK_ENVELOPE_SCALE).max(0.001);
let transient_total_samples = (BASS_PLUCK_TRANSIENT_DECAY_SECONDS * sample_rate).max(1.0);
Self {
osc: SineOscillator::new(hz, sample_rate),
transient_osc: SineOscillator::new(hz * 2.0, sample_rate),
core: BassVoiceCore::new(attack_time, decay_time, sample_rate),
transient_samples_remaining: transient_total_samples as u64,
transient_total_samples,
}
}
pub(crate) fn next(&mut self) -> f32 {
let body = self.osc.next() * self.core.envelope.next();
let transient = if self.transient_samples_remaining > 0 {
let elapsed = self.transient_total_samples - self.transient_samples_remaining as f32;
let gain = (-elapsed / self.transient_total_samples * 5.0).exp();
self.transient_samples_remaining -= 1;
self.transient_osc.next() * gain
} else {
0.0
};
(body + transient * BASS_PLUCK_TRANSIENT_MIX) * BASS_PLUCK_OUTPUT_GAIN
}
pub(crate) fn is_done(&self) -> bool {
self.core.is_done()
}
}