///|
/// Pure MoonBit FM (Frequency Modulation) synthesis engine.
/// Emulates classic Yamaha OPL2/OPL3 (AdLib, Sound Blaster) and YM2612 (Sega Genesis/Mega Drive) sound chips.
///|
/// Renders a basic static 2-operator FM tone where the carrier oscillator is frequency-modulated
/// by a sinusoidal modulator oscillator.
/// - `carrier_freq`: Base pitch frequency in Hz (fc)
/// - `mod_freq`: Modulator oscillator frequency in Hz (fm)
/// - `mod_index`: Modulation index I = Delta f / fm (higher index adds more bright sideband harmonics)
pub fn render_fm_tone(
carrier_freq : Double,
mod_freq : Double,
mod_index : Double,
duration : Double,
sample_rate : Int32,
volume : Double,
) -> AudioBuffer {
let sr = if sample_rate <= 0 { 44100 } else { sample_rate }
let sr_d = sr.to_double()
let dur = if duration <= 0.001 { 0.001 } else { duration }
let num_samples = (dur * sr_d).to_int()
let samples = Array::make(num_samples, 0.0)
let fc = if carrier_freq < 1.0 { 1.0 } else { carrier_freq }
let fm = if mod_freq < 1.0 { 1.0 } else { mod_freq }
let idx = if mod_index < 0.0 { 0.0 } else { mod_index }
let vol = if volume < 0.0 { 0.0 } else if volume > 1.0 { 1.0 } else { volume }
let dphi_c = two_pi * fc / sr_d
let dphi_m = two_pi * fm / sr_d
let mut phi_c = 0.0
let mut phi_m = 0.0
for i = 0; i < num_samples; i = i + 1 {
let mod_val = @math.sin(phi_m) * idx
let s = @math.sin(phi_c + mod_val) * vol
samples[i] = s
phi_c = phi_c + dphi_c
if phi_c >= two_pi {
phi_c = phi_c - two_pi
}
phi_m = phi_m + dphi_m
if phi_m >= two_pi {
phi_m = phi_m - two_pi
}
}
{ sample_rate: sr, samples, }
}
///|
/// Renders an expressive 2-operator dynamic FM tone with time-varying modulation index and ADSR envelope.
/// The modulation index dynamically transitions from `mod_index_start` to `mod_index_end`,
/// mimicking physical acoustic instruments where harmonic brightness decays faster than base loudness.
pub fn render_fm_dynamic(
carrier_freq : Double,
mod_freq : Double,
mod_index_start : Double,
mod_index_end : Double,
duration : Double,
sample_rate : Int32,
adsr : ADSR,
volume : Double,
) -> AudioBuffer {
let sr = if sample_rate <= 0 { 44100 } else { sample_rate }
let sr_d = sr.to_double()
let dur = if duration <= 0.001 { 0.001 } else { duration }
let num_samples = (dur * sr_d).to_int()
let samples = Array::make(num_samples, 0.0)
let fc = if carrier_freq < 1.0 { 1.0 } else { carrier_freq }
let fm = if mod_freq < 1.0 { 1.0 } else { mod_freq }
let vol = if volume < 0.0 { 0.0 } else if volume > 1.0 { 1.0 } else { volume }
let dphi_c = two_pi * fc / sr_d
let dphi_m = two_pi * fm / sr_d
let mut phi_c = 0.0
let mut phi_m = 0.0
for i = 0; i < num_samples; i = i + 1 {
let progress = i.to_double() / num_samples.to_double()
// Exponential decay of modulation brightness
let mod_factor = @math.exp(-progress * 3.0)
let current_index = mod_index_end +
(mod_index_start - mod_index_end) * mod_factor
let mod_val = @math.sin(phi_m) * current_index
let s = @math.sin(phi_c + mod_val) * vol
samples[i] = s
phi_c = phi_c + dphi_c
if phi_c >= two_pi {
phi_c = phi_c - two_pi
}
phi_m = phi_m + dphi_m
if phi_m >= two_pi {
phi_m = phi_m - two_pi
}
}
let buf = { sample_rate: sr, samples, }
buf.apply_adsr(adsr, Exponential)
buf
}
///|
/// FM Preset 1: Yamaha DX7 style retro Electric Piano (E-Piano).
/// Uses 1:1 harmonic ratio with snappy attack and warm metallic bell transient.
pub fn fm_electric_piano(
freq : Double,
duration : Double,
sample_rate : Int32,
) -> AudioBuffer {
let adsr = ADSR::new(0.005, 0.4, 0.35, 0.25)
render_fm_dynamic(
freq,
freq * 1.0, // 1:1 ratio
3.2, // Bright initial tine strike
0.2, // Soft acoustic sustain
duration,
sample_rate,
adsr,
0.85,
)
}
///|
/// FM Preset 2: Crystal Chime / Metallic Church Bell.
/// Uses inharmonic ratio (1 : sqrt(2) ≈ 1 : 1.4142) to produce authentic metallic overtones.
pub fn fm_bell(
freq : Double,
duration : Double,
sample_rate : Int32,
) -> AudioBuffer {
let adsr = ADSR::new(0.002, duration * 0.7, 0.1, duration * 0.3)
render_fm_dynamic(
freq,
freq * 1.41421356, // Inharmonic golden ratio
5.0, // Heavy metallic clang
0.05, // Settles into pure resonant hum
duration,
sample_rate,
adsr,
0.8,
)
}
///|
/// FM Preset 3: 80s Synth Brass / Synth Horn.
/// Features a dynamic filter swell characteristic of vintage polyphonic brass synthesizers.
pub fn fm_brass(
freq : Double,
duration : Double,
sample_rate : Int32,
) -> AudioBuffer {
let adsr = ADSR::new(0.06, 0.2, 0.7, 0.15)
render_fm_dynamic(
freq,
freq * 1.0,
3.8,
1.5,
duration,
sample_rate,
adsr,
0.85,
)
}
///|
/// FM Preset 4: Sega Genesis 16-bit Funk Slap Bass.
/// Uses 1:2 harmonic ratio for punching second harmonic transient.
pub fn fm_slap_bass(
freq : Double,
duration : Double,
sample_rate : Int32,
) -> AudioBuffer {
let adsr = ADSR::percussive(0.2)
render_fm_dynamic(
freq,
freq * 2.0, // 1:2 ratio for sharp low-end slap
6.0, // High-energy slap transient
0.3,
duration,
sample_rate,
adsr,
0.9,
)
}