use std::time::{Duration, Instant};
use anyhow::Result;
use synthie::audio::engine::setup_audio;
use synthie::params::{
AudioEvent, ChorusParams, CrusherParams, DelayParams, EnvParams, FilterMode, FilterParams,
FxParams, GlobalParams, LfoParams, LfoShape, LfoTarget, MidiNote, ModEnvParams, Osc2Params,
OscParams, RingModMode, SynthParams, Waveform,
};
const BPM: f32 = 90.0;
fn beats(n: f32) -> Duration {
Duration::from_secs_f32(n * 60.0 / BPM)
}
struct TimedEvent {
at: Duration,
on: bool,
midi: u8,
}
fn note(at: f32, hold: f32, midi: u8) -> [TimedEvent; 2] {
[
TimedEvent {
at: beats(at),
on: true,
midi,
},
TimedEvent {
at: beats(at + hold),
on: false,
midi,
},
]
}
fn build_phrase() -> Vec<TimedEvent> {
const NOTES: [(u8, f32); 8] = [
(60, 0.0), (64, 0.5), (67, 1.0), (72, 1.5), (71, 2.0), (67, 2.5), (64, 3.0), (60, 3.5), ];
let mut events: Vec<TimedEvent> = Vec::new();
for (midi, beat) in NOTES {
events.extend(note(beat, 0.4, midi));
}
events.sort_by_key(|e| e.at);
events
}
struct Phase {
label: &'static str,
desc: &'static str,
mode: RingModMode,
}
static PHASES: &[Phase] = &[
Phase {
label: "off",
desc: "no ring mod - two sines blended (reference)",
mode: RingModMode::Off,
},
Phase {
label: "SID",
desc: "OSC2 x sign(OSC1 MSB) - hollow, SID-chip flavour",
mode: RingModMode::Osc2ByOsc1Sign,
},
Phase {
label: "rev",
desc: "OSC1 x sign(OSC2 MSB) - asymmetric reversed-SID",
mode: RingModMode::Osc1ByOsc2Sign,
},
Phase {
label: "analog",
desc: "OSC1 x OSC2 - sum/difference tones, bell-like",
mode: RingModMode::Analog,
},
];
fn base_patch() -> SynthParams {
SynthParams {
osc: OscParams {
waveform: Waveform::Sine,
pulse_width: 0.5,
detune: 0.0,
noise_mix: 0.0,
},
env: EnvParams {
attack: 0.01,
decay: 0.05,
sustain: 0.9,
release: 0.15,
env_reverse: false,
},
filter: FilterParams {
filter_mode: FilterMode::LowPass,
cutoff: 18000.0,
resonance: 0.0,
drive: 0.0,
},
lfo: LfoParams {
lfo_rate: 0.0,
lfo_depth: 0.0,
lfo_target: LfoTarget::Pitch,
lfo_shape: LfoShape::Sine,
},
fx: FxParams {
reverb_mix: 0.05,
reverb_size: 0.3,
reverb_damping: 0.6,
},
osc2: Osc2Params {
waveform: Waveform::Sine,
detune: 700.0,
osc2_mix: 0.7,
hard_sync: false,
ring_mod: RingModMode::Off,
},
crusher: CrusherParams::default(),
chorus: ChorusParams::default(),
delay: DelayParams::default(),
lfo2: LfoParams::default(),
filter_env: ModEnvParams::default(),
pitch_env: ModEnvParams::default(),
global: GlobalParams {
volume: 0.7,
glide_time: 0.0,
},
#[cfg(feature = "arp")]
arp: synthie::params::ArpParams::default(),
}
}
fn main() -> Result<()> {
let (_stream, event_tx, _scope_rx) = setup_audio()?;
println!("=== Ring Modulation Demo ===");
println!("C major arpeggio | {BPM} BPM | sine+sine, OSC2 at +700 ct (perfect fifth)\n");
println!("{:<8} description", "mode");
println!("{}", "-".repeat(58));
let phrase = build_phrase();
for phase in PHASES {
let mut patch = base_patch();
patch.osc2.ring_mod = phase.mode;
event_tx.send(AudioEvent::LoadPatch(Box::new(patch)))?;
println!(" {:<6} {}", phase.label, phase.desc);
let started = Instant::now();
for ev in &phrase {
let deadline = started + ev.at;
let now = Instant::now();
if deadline > now {
std::thread::sleep(deadline.duration_since(now));
}
let msg = if ev.on {
AudioEvent::NoteOn(MidiNote(ev.midi))
} else {
AudioEvent::NoteOff(MidiNote(ev.midi))
};
event_tx.send(msg)?;
}
std::thread::sleep(beats(4.0));
}
println!("\nDone.");
event_tx.send(AudioEvent::Panic)?;
std::thread::sleep(Duration::from_millis(150));
Ok(())
}