///|
/// Comprehensive configuration parameters for procedural retro sound synthesis (sfxr engine).
/// Implements 100% full feature parity with Tomas Pettersson's legendary sfxr algorithm,
/// covering frequency slides, vibrato, pitch jumps (arpeggios), cycle repeats, phaser sweeps,
/// and dynamic Low-Pass / High-Pass filter cutoff sweeps.
#derive(Eq, Debug)
pub(all) struct SfxParams {
mut wave_type : Waveform
mut start_freq : Double
mut min_freq : Double
mut freq_slide : Double
mut freq_delta_slide : Double
mut square_duty : Double
mut duty_sweep : Double
mut vibrato_depth : Double
mut vibrato_speed : Double
mut attack_time : Double
mut sustain_time : Double
mut sustain_punch : Double
mut decay_time : Double
mut lpf_cutoff : Double
mut lpf_resonance : Double
mut lpf_sweep : Double // Dynamic Low-Pass Filter Cutoff Sweep (Hz/sec)
mut hpf_cutoff : Double
mut hpf_sweep : Double // Dynamic High-Pass Filter Cutoff Sweep (Hz/sec)
mut phaser_offset : Double // Phaser delay buffer offset (seconds/normalized)
mut phaser_sweep : Double // Phaser dynamic phase sweep ramp
mut repeat_speed : Double // Periodic cycle repeat rate (0.0 = off, >0.0 = cyclic repeat)
mut change_amount : Double // Pitch change multiplier (arpeggio jump)
mut change_time : Double // Pitch change trigger time in seconds
mut master_volume : Double
}
///|
/// Creates a default baseline SfxParams structure.
pub fn SfxParams::default() -> SfxParams {
{
wave_type: Square(0.5),
start_freq: 440.0,
min_freq: 20.0,
freq_slide: 0.0,
freq_delta_slide: 0.0,
square_duty: 0.5,
duty_sweep: 0.0,
vibrato_depth: 0.0,
vibrato_speed: 0.0,
attack_time: 0.005,
sustain_time: 0.1,
sustain_punch: 0.0,
decay_time: 0.2,
lpf_cutoff: 20000.0,
lpf_resonance: 0.707,
lpf_sweep: 0.0,
hpf_cutoff: 10.0,
hpf_sweep: 0.0,
phaser_offset: 0.0,
phaser_sweep: 0.0,
repeat_speed: 0.0,
change_amount: 0.0,
change_time: 0.0,
master_volume: 0.8,
}
}
///|
pub fn SfxParams::set_master_volume(self : SfxParams, volume : Double) -> Unit {
self.master_volume = volume
}
///|
pub fn SfxParams::set_freq_delta_slide(
self : SfxParams,
delta : Double,
) -> Unit {
self.freq_delta_slide = delta
}
///|
pub fn SfxParams::set_duty_sweep(self : SfxParams, sweep : Double) -> Unit {
self.duty_sweep = sweep
}
///|
pub fn SfxParams::set_hpf_cutoff(self : SfxParams, cutoff : Double) -> Unit {
self.hpf_cutoff = cutoff
}
///|
pub fn SfxParams::set_lpf_sweep(self : SfxParams, sweep : Double) -> Unit {
self.lpf_sweep = sweep
}
///|
pub fn SfxParams::set_hpf_sweep(self : SfxParams, sweep : Double) -> Unit {
self.hpf_sweep = sweep
}
///|
pub fn SfxParams::set_phaser(
self : SfxParams,
offset : Double,
sweep : Double,
) -> Unit {
self.phaser_offset = offset
self.phaser_sweep = sweep
}
///|
pub fn SfxParams::set_repeat_speed(self : SfxParams, speed : Double) -> Unit {
self.repeat_speed = speed
}
///|
/// Generates a classic retro "Laser / Shoot" sound effect parameters.
/// Incorporates rapid downward frequency slide and classic sci-fi phaser sweep.
pub fn SfxParams::laser(seed : UInt32) -> SfxParams {
let rng = Rng::new(seed)
let p = SfxParams::default()
p.wave_type = if rng.next_double() < 0.5 { Square(0.5) } else { Sawtooth }
p.start_freq = 500.0 + rng.next_double() * 600.0
p.min_freq = 60.0
p.freq_slide = -0.4 - rng.next_double() * 0.3
p.attack_time = 0.002
p.sustain_time = 0.05 + rng.next_double() * 0.08
p.decay_time = 0.08 + rng.next_double() * 0.12
p.square_duty = 0.3 + rng.next_double() * 0.4
// Classic laser phaser offset and sweep
p.phaser_offset = 0.05 + rng.next_double() * 0.1
p.phaser_sweep = -0.05 - rng.next_double() * 0.05
p
}
///|
/// Generates a classic retro "Jump" sound effect parameters.
/// Features a prominent upward frequency slide and snappy decay.
pub fn SfxParams::jump(seed : UInt32) -> SfxParams {
let rng = Rng::new(seed)
let p = SfxParams::default()
p.wave_type = Square(0.5)
p.start_freq = 150.0 + rng.next_double() * 150.0
p.freq_slide = 0.25 + rng.next_double() * 0.25
p.attack_time = 0.003
p.sustain_time = 0.08 + rng.next_double() * 0.08
p.decay_time = 0.12 + rng.next_double() * 0.15
p.square_duty = 0.4 + rng.next_double() * 0.2
p
}
///|
/// Generates a thunderous retro "Explosion" sound effect parameters.
/// Leverages dynamic low-pass filter cutoff sweep to dive into deep rumble.
pub fn SfxParams::explosion(seed : UInt32) -> SfxParams {
let rng = Rng::new(seed)
let p = SfxParams::default()
p.wave_type = Noise
p.start_freq = 80.0 + rng.next_double() * 80.0
p.freq_slide = -0.15 - rng.next_double() * 0.2
p.attack_time = 0.002
p.sustain_time = 0.12 + rng.next_double() * 0.15
p.sustain_punch = 0.3
p.decay_time = 0.35 + rng.next_double() * 0.45
p.lpf_cutoff = 2200.0 + rng.next_double() * 1000.0
p.lpf_resonance = 1.2
// Dynamic filter sweep: cutoff plunges during explosion
p.lpf_sweep = -3000.0 - rng.next_double() * 1500.0
p
}
///|
/// Generates a bright, ascending two-tone retro "Coin / Pickup" sound effect.
/// Employs pitch change arpeggio jumping upward by approx 5 semitones.
pub fn SfxParams::coin(seed : UInt32) -> SfxParams {
let rng = Rng::new(seed)
let p = SfxParams::default()
p.wave_type = Square(0.5)
p.start_freq = 450.0 + rng.next_double() * 200.0
p.attack_time = 0.002
p.sustain_time = 0.08 + rng.next_double() * 0.05
p.decay_time = 0.18 + rng.next_double() * 0.15
// Pitch jump (arpeggio) after 0.05s
p.change_amount = 1.334 + rng.next_double() * 0.2
p.change_time = 0.05 + rng.next_double() * 0.02
p.square_duty = 0.5
p
}
///|
/// Generates a heavy crunchy retro "Hit / Hurt" sound effect.
/// Uses high-pass filter sweep and steep frequency slide.
pub fn SfxParams::hit(seed : UInt32) -> SfxParams {
let rng = Rng::new(seed)
let p = SfxParams::default()
p.wave_type = if rng.next_double() < 0.5 { Noise } else { Square(0.25) }
p.start_freq = 250.0 + rng.next_double() * 300.0
p.freq_slide = -0.35 - rng.next_double() * 0.3
p.attack_time = 0.002
p.sustain_time = 0.04 + rng.next_double() * 0.06
p.decay_time = 0.1 + rng.next_double() * 0.12
p.hpf_cutoff = 80.0
p.hpf_sweep = 250.0 // HPF sweeps up to strip low mud
p
}
///|
/// Generates an ascending, triumphant retro "Powerup" sound effect.
/// Features frequency slide, vibrato modulation, and expanding filter sweep.
pub fn SfxParams::powerup(seed : UInt32) -> SfxParams {
let rng = Rng::new(seed)
let p = SfxParams::default()
p.wave_type = if rng.next_double() < 0.5 { Sawtooth } else { Triangle }
p.start_freq = 180.0 + rng.next_double() * 120.0
p.freq_slide = 0.4 + rng.next_double() * 0.3
p.vibrato_speed = 8.0 + rng.next_double() * 8.0
p.vibrato_depth = 0.15 + rng.next_double() * 0.15
p.attack_time = 0.005
p.sustain_time = 0.25 + rng.next_double() * 0.15
p.decay_time = 0.2 + rng.next_double() * 0.2
// Expanding low-pass filter sweep
p.lpf_cutoff = 800.0
p.lpf_sweep = 2000.0
p
}
///|
/// Generates a crisp, snappy retro UI "Blip / Select" click sound effect.
pub fn SfxParams::blip(seed : UInt32) -> SfxParams {
let rng = Rng::new(seed)
let p = SfxParams::default()
p.wave_type = if rng.next_double() < 0.5 { Square(0.5) } else { Sine }
p.start_freq = 600.0 + rng.next_double() * 400.0
p.attack_time = 0.001
p.sustain_time = 0.02 + rng.next_double() * 0.03
p.decay_time = 0.04 + rng.next_double() * 0.04
p
}
///|
/// Generates a periodic stuttering burst sound effect, showcasing cycle repeat and phaser.
pub fn SfxParams::burst(seed : UInt32) -> SfxParams {
let rng = Rng::new(seed)
let p = SfxParams::default()
p.wave_type = Square(0.5)
p.start_freq = 300.0 + rng.next_double() * 200.0
p.freq_slide = -0.3
p.repeat_speed = 0.5 + rng.next_double() * 0.3 // Cycle repeats
p.phaser_offset = 0.1
p.phaser_sweep = -0.05
p.attack_time = 0.002
p.sustain_time = 0.15
p.decay_time = 0.25
p
}
///|
/// Generates a completely randomized retro sound effect covering all parameter dimensions.
pub fn SfxParams::randomize(seed : UInt32) -> SfxParams {
let rng = Rng::new(seed)
let wave = match (rng.next_u32() % 5U).reinterpret_as_int() {
0 => Waveform::Sine
1 => Waveform::Square(0.1 + rng.next_double() * 0.8)
2 => Waveform::Sawtooth
3 => Waveform::Triangle
_ => Waveform::Noise
}
{
wave_type: wave,
start_freq: 50.0 + rng.next_double() * 1200.0,
min_freq: 20.0,
freq_slide: rng.next_bipolar() * 0.5,
freq_delta_slide: rng.next_bipolar() * 0.05,
square_duty: 0.1 + rng.next_double() * 0.8,
duty_sweep: rng.next_bipolar() * 0.05,
vibrato_depth: if rng.next_double() < 0.4 {
rng.next_double() * 0.3
} else {
0.0
},
vibrato_speed: rng.next_double() * 20.0,
attack_time: 0.001 + rng.next_double() * 0.05,
sustain_time: 0.02 + rng.next_double() * 0.3,
sustain_punch: if rng.next_double() < 0.3 {
rng.next_double() * 0.4
} else {
0.0
},
decay_time: 0.05 + rng.next_double() * 0.5,
lpf_cutoff: 500.0 + rng.next_double() * 18000.0,
lpf_resonance: 0.5 + rng.next_double() * 1.5,
lpf_sweep: if rng.next_double() < 0.4 {
rng.next_bipolar() * 2000.0
} else {
0.0
},
hpf_cutoff: 10.0 + rng.next_double() * 100.0,
hpf_sweep: if rng.next_double() < 0.3 {
rng.next_double() * 400.0
} else {
0.0
},
phaser_offset: if rng.next_double() < 0.35 {
rng.next_double() * 0.2
} else {
0.0
},
phaser_sweep: if rng.next_double() < 0.35 {
rng.next_bipolar() * 0.1
} else {
0.0
},
repeat_speed: if rng.next_double() < 0.25 {
0.3 + rng.next_double() * 0.5
} else {
0.0
},
change_amount: if rng.next_double() < 0.3 {
1.0 + rng.next_double() * 0.8
} else {
0.0
},
change_time: 0.02 + rng.next_double() * 0.1,
master_volume: 0.8,
}
}
///|
/// Mutates existing parameters by a perturbation factor [0.0, 1.0] to create subtle variations.
pub fn SfxParams::mutate(
self : SfxParams,
amount : Double,
seed : UInt32,
) -> SfxParams {
let rng = Rng::new(seed)
let factor = if amount < 0.0 {
0.0
} else if amount > 1.0 {
1.0
} else {
amount
}
let perturb = fn(val : Double, scale : Double) -> Double {
val + rng.next_bipolar() * factor * scale
}
let mut new_freq = perturb(self.start_freq, 100.0)
if new_freq < 20.0 {
new_freq = 20.0
}
let mut new_decay = perturb(self.decay_time, 0.1)
if new_decay < 0.01 {
new_decay = 0.01
}
{
wave_type: self.wave_type,
start_freq: new_freq,
min_freq: self.min_freq,
freq_slide: perturb(self.freq_slide, 0.1),
freq_delta_slide: self.freq_delta_slide,
square_duty: perturb(self.square_duty, 0.1),
duty_sweep: self.duty_sweep,
vibrato_depth: perturb(self.vibrato_depth, 0.05),
vibrato_speed: perturb(self.vibrato_speed, 2.0),
attack_time: self.attack_time,
sustain_time: perturb(self.sustain_time, 0.05),
sustain_punch: self.sustain_punch,
decay_time: new_decay,
lpf_cutoff: self.lpf_cutoff,
lpf_resonance: self.lpf_resonance,
lpf_sweep: self.lpf_sweep,
hpf_cutoff: self.hpf_cutoff,
hpf_sweep: self.hpf_sweep,
phaser_offset: perturb(self.phaser_offset, 0.05),
phaser_sweep: perturb(self.phaser_sweep, 0.02),
repeat_speed: self.repeat_speed,
change_amount: self.change_amount,
change_time: self.change_time,
master_volume: self.master_volume,
}
}
///|
/// Synthesizes raw audio samples into an AudioBuffer according to SfxParams.
/// Full coverage of all 6 core sfxr algorithmic pillars:
/// 1. Frequency Slide & Acceleration (freq_slide, freq_delta_slide)
/// 2. LFO Vibrato (vibrato_speed, vibrato_depth)
/// 3. Pitch Change Arpeggio (change_amount, change_time)
/// 4. Cycle Repeat (repeat_speed)
/// 5. Phaser delay comb filtering (phaser_offset, phaser_sweep)
/// 6. Dynamic Filter Cutoff Sweeping (lpf_cutoff, lpf_sweep, hpf_cutoff, hpf_sweep)
pub fn SfxParams::synthesize(
self : SfxParams,
sample_rate : Int32,
) -> AudioBuffer {
let sr = if sample_rate <= 0 { 44100 } else { sample_rate }
let sr_f = sr.to_double()
let rng = Rng::new(1234567U)
let attack_time = if self.attack_time < 0.0001 {
0.0001
} else {
self.attack_time
}
let sustain_time = if self.sustain_time < 0.0001 {
0.0001
} else {
self.sustain_time
}
let decay_time = if self.decay_time < 0.0001 {
0.0001
} else {
self.decay_time
}
let total_dur = attack_time + sustain_time + decay_time
let num_samples = (total_dur * sr_f).to_int()
let buffer = AudioBuffer::new(sr, num_samples)
let mut current_freq = if self.start_freq < 1.0 {
1.0
} else {
self.start_freq
}
let min_f = if self.min_freq < 1.0 { 1.0 } else { self.min_freq }
let mut freq_slide_acc = self.freq_slide
let mut current_duty = self.square_duty
let mut phase = 0.0
// 1. Cycle repeat setup
let has_repeat = self.repeat_speed > 0.05
let rep_limit = if has_repeat {
let raw = (((1.0 - self.repeat_speed) * 0.35 + 0.03) * sr_f).to_int()
if raw < 1 {
1
} else {
raw
}
} else {
0
}
let mut rep_counter = 0
// 2. Pitch Arpeggio trigger setup
let change_sample_idx = (self.change_time * sr_f).to_int()
let mut arpeggio_triggered = false
// 3. Dynamic Filter setup
let mut cur_lpf = self.lpf_cutoff
let mut cur_hpf = self.hpf_cutoff
let use_lpf = cur_lpf < 18000.0 || self.lpf_sweep.abs() > 0.001
let use_hpf = cur_hpf > 20.0 || self.hpf_sweep.abs() > 0.001
let lpf = BiquadFilter::new(LowPass, cur_lpf, self.lpf_resonance, sr)
let hpf = BiquadFilter::new(HighPass, cur_hpf, 0.707, sr)
// 4. Phaser ring buffer setup
let use_phaser = self.phaser_offset.abs() > 0.001 ||
self.phaser_sweep.abs() > 0.001
let phaser_buf = Array::make(1024, 0.0)
let mut phaser_pos = 0
let mut cur_phaser_offset = self.phaser_offset * 1024.0
for i = 0; i < num_samples; i = i + 1 {
let t = i.to_double() / sr_f
// Cycle Repeat logic
if has_repeat {
rep_counter = rep_counter + 1
if rep_counter >= rep_limit {
rep_counter = 0
current_freq = self.start_freq
freq_slide_acc = self.freq_slide
arpeggio_triggered = false
}
}
// Pitch Arpeggio logic
let check_time_sample = if has_repeat { rep_counter } else { i }
if self.change_amount > 0.001 &&
!arpeggio_triggered &&
check_time_sample >= change_sample_idx {
current_freq = current_freq * self.change_amount
arpeggio_triggered = true
}
// Frequency slide acceleration
freq_slide_acc = freq_slide_acc + self.freq_delta_slide * (50.0 / sr_f)
current_freq = current_freq +
freq_slide_acc * (current_freq * (100.0 / sr_f))
if current_freq < min_f {
current_freq = min_f
} else if current_freq > 20000.0 {
current_freq = 20000.0
}
// Vibrato
let mut effective_freq = current_freq
if self.vibrato_depth > 0.001 && self.vibrato_speed > 0.0 {
let vib_phase = t * self.vibrato_speed * two_pi
let vib_offset = @math.sin(vib_phase) * self.vibrato_depth * current_freq
effective_freq = effective_freq + vib_offset
}
if effective_freq < 1.0 {
effective_freq = 1.0
}
// Duty sweep for square wave
current_duty = current_duty + self.duty_sweep * (10.0 / sr_f)
if current_duty < 0.05 {
current_duty = 0.05
} else if current_duty > 0.95 {
current_duty = 0.95
}
// Dynamic Filter Sweeps
if self.lpf_sweep.abs() > 0.001 {
cur_lpf = cur_lpf + self.lpf_sweep / sr_f
if cur_lpf < 20.0 {
cur_lpf = 20.0
} else if cur_lpf > 20000.0 {
cur_lpf = 20000.0
}
}
if self.hpf_sweep.abs() > 0.001 {
cur_hpf = cur_hpf + self.hpf_sweep / sr_f
if cur_hpf < 10.0 {
cur_hpf = 10.0
} else if cur_hpf > 15000.0 {
cur_hpf = 15000.0
}
}
// Update filter coefficients every 8 samples for smooth, artifact-free sweeping
if i % 8 == 0 {
if use_lpf {
lpf.update_cutoff(LowPass, cur_lpf, self.lpf_resonance, sr)
}
if use_hpf {
hpf.update_cutoff(HighPass, cur_hpf, 0.707, sr)
}
}
// Sample waveform
let wave = match self.wave_type {
Square(_) => Waveform::Square(current_duty)
other => other
}
let mut sample = sample_waveform(wave, phase, rng)
// Phase advance
let phase_inc = effective_freq / sr_f
phase = phase + phase_inc
if phase >= 1.0 {
phase = phase - phase.floor()
} else if phase < 0.0 {
phase = phase - phase.floor()
}
// Envelope computation
let env_gain = if t < attack_time {
t / attack_time
} else if t < attack_time + sustain_time {
1.0 + self.sustain_punch * (1.0 - (t - attack_time) / sustain_time)
} else {
let decay_progress = (t - attack_time - sustain_time) / decay_time
let d = if decay_progress > 1.0 {
1.0
} else if decay_progress < 0.0 {
0.0
} else {
decay_progress
}
1.0 - d
}
sample = sample * env_gain * self.master_volume
// Apply Low-Pass Filter
if use_lpf {
sample = lpf.process(sample)
}
// Apply High-Pass Filter
if use_hpf {
sample = hpf.process(sample)
}
// Apply Phaser (Comb Filtering with Sweeping Delay)
if use_phaser {
cur_phaser_offset = cur_phaser_offset +
self.phaser_sweep * (1024.0 / sr_f)
if cur_phaser_offset < 0.0 {
cur_phaser_offset = 0.0
} else if cur_phaser_offset > 1023.0 {
cur_phaser_offset = 1023.0
}
let tap_delta = cur_phaser_offset.to_int()
let tap_idx = (phaser_pos - tap_delta + 1024) % 1024
let phaser_sample = phaser_buf[tap_idx]
phaser_buf[phaser_pos] = sample
sample = (sample + phaser_sample) * 0.75
phaser_pos = (phaser_pos + 1) % 1024
}
if sample.is_nan() || sample.is_inf() {
sample = 0.0
}
buffer.push(sample)
}
buffer
}