aetherdsp-nodes 0.2.3

Built-in DSP nodes for AetherDSP — oscillator, filters, reverb, LFO, granular, Karplus-Strong, compressor, waveshaper, chorus
Documentation
//! ADSR envelope demonstration
//!
//! This example demonstrates:
//! - ADSR envelope (Attack, Decay, Sustain, Release)
//! - Gate triggering (note on/off)
//! - Envelope modulation of audio signal
//! - Multiple note triggers
//!
//! # Usage
//!
//! ```bash
//! cargo run --example envelope_test -p aetherdsp-nodes
//! ```
//!
//! Output: `envelope_test.wav` (6 seconds, 48kHz, mono)
//!
//! # What You'll Hear
//!
//! Three notes played with an ADSR envelope:
//! 1. Short note (0.5s gate)
//! 2. Medium note (1.0s gate)
//! 3. Long note (1.5s gate)
//!
//! Each note demonstrates the full ADSR cycle:
//! - Attack: 0.1s (fade in)
//! - Decay: 0.2s (drop to sustain level)
//! - Sustain: 0.7 level (held while gate is on)
//! - Release: 0.3s (fade out after gate off)

use aether_core::{
    command::Command,
    graph::DspGraph,
    param::Param,
    scheduler::Scheduler,
    BUFFER_SIZE,
};
use aether_nodes::{envelope::AdsrEnvelope, oscillator::Oscillator};
use hound::{WavSpec, WavWriter};
use ringbuf::{traits::{Producer, Split}, HeapRb};

const SAMPLE_RATE: f32 = 48_000.0;
const DURATION_SECS: f32 = 6.0;
const TOTAL_SAMPLES: usize = (SAMPLE_RATE * DURATION_SECS) as usize;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("🎵 ADSR Envelope Test");
    println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    println!();
    println!("Rendering: Oscillator → ADSR Envelope → Output");
    println!("Duration: {} seconds", DURATION_SECS);
    println!("Sample Rate: {} Hz", SAMPLE_RATE);
    println!();

    // Create command ring buffer
    let (mut producer, mut consumer) = HeapRb::<Command>::new(1024).split();

    // Create scheduler and graph
    let mut scheduler = Scheduler::new(SAMPLE_RATE);
    let mut graph = DspGraph::new();

    // Build audio graph: oscillator → envelope → output
    println!("🔧 Building audio graph...");

    // Create oscillator (sine wave at 440 Hz)
    let osc = Box::new(Oscillator::new());
    let osc_id = graph.add_node(osc).expect("Failed to add oscillator");

    // Create ADSR envelope
    let envelope = Box::new(AdsrEnvelope::new());
    let env_id = graph.add_node(envelope).expect("Failed to add envelope");

    // Connect: oscillator → envelope input 0
    graph.connect(osc_id, env_id, 0);

    // Set envelope as output
    graph.set_output_node(env_id);

    // Send commands to scheduler
    producer.try_push(Command::AddNode { id: osc_id }).ok();
    producer.try_push(Command::AddNode { id: env_id }).ok();
    producer.try_push(Command::Connect { src: osc_id, dst: env_id, slot: 0 }).ok();
    producer.try_push(Command::SetOutputNode { id: env_id }).ok();

    // Set oscillator parameters: frequency=440Hz, amplitude=1.0, waveform=0 (sine)
    producer.try_push(Command::UpdateParam {
        node: osc_id,
        param_index: 0,
        new_param: Param::new(440.0),  // frequency
    }).ok();
    producer.try_push(Command::UpdateParam {
        node: osc_id,
        param_index: 1,
        new_param: Param::new(1.0),    // amplitude
    }).ok();
    producer.try_push(Command::UpdateParam {
        node: osc_id,
        param_index: 2,
        new_param: Param::new(0.0),    // waveform (0=sine)
    }).ok();

    // Set envelope parameters: A=0.1s, D=0.2s, S=0.7, R=0.3s, gate=0
    producer.try_push(Command::UpdateParam {
        node: env_id,
        param_index: 0,
        new_param: Param::new(0.1),    // attack
    }).ok();
    producer.try_push(Command::UpdateParam {
        node: env_id,
        param_index: 1,
        new_param: Param::new(0.2),    // decay
    }).ok();
    producer.try_push(Command::UpdateParam {
        node: env_id,
        param_index: 2,
        new_param: Param::new(0.7),    // sustain
    }).ok();
    producer.try_push(Command::UpdateParam {
        node: env_id,
        param_index: 3,
        new_param: Param::new(0.3),    // release
    }).ok();
    producer.try_push(Command::UpdateParam {
        node: env_id,
        param_index: 4,
        new_param: Param::new(0.0),    // gate (off)
    }).ok();

    println!("  ✅ Oscillator (440 Hz) → ADSR Envelope → Output");
    println!("  📊 Envelope: A=0.1s, D=0.2s, S=0.7, R=0.3s");
    println!();

    // Create WAV file
    let spec = WavSpec {
        channels: 1,
        sample_rate: SAMPLE_RATE as u32,
        bits_per_sample: 16,
        sample_format: hound::SampleFormat::Int,
    };
    let mut writer = WavWriter::create("envelope_test.wav", spec)?;

    println!("🎬 Rendering audio...");

    // Define note triggers (time in seconds, gate duration in seconds)
    let notes = vec![
        (0.5, 0.5),   // Note 1: starts at 0.5s, held for 0.5s
        (2.0, 1.0),   // Note 2: starts at 2.0s, held for 1.0s
        (4.0, 1.5),   // Note 3: starts at 4.0s, held for 1.5s
    ];

    let mut current_note = 0;
    let mut gate_on_time: Option<f32> = None;

    // Process audio in blocks
    let mut samples_rendered = 0;
    let mut output_buffer = vec![0.0f32; BUFFER_SIZE];

    while samples_rendered < TOTAL_SAMPLES {
        let current_time = samples_rendered as f32 / SAMPLE_RATE;

        // Check if we need to trigger a note
        if current_note < notes.len() {
            let (trigger_time, duration) = notes[current_note];

            // Gate ON
            if gate_on_time.is_none() && current_time >= trigger_time {
                producer.try_push(Command::UpdateParam {
                    node: env_id,
                    param_index: 4,
                    new_param: Param::new(1.0),  // gate ON
                }).ok();
                gate_on_time = Some(current_time);
                println!("  🎹 Note {} ON  @ {:.2}s", current_note + 1, current_time);
            }

            // Gate OFF
            if let Some(on_time) = gate_on_time {
                if current_time >= on_time + duration {
                    producer.try_push(Command::UpdateParam {
                        node: env_id,
                        param_index: 4,
                        new_param: Param::new(0.0),  // gate OFF
                    }).ok();
                    println!("  🎹 Note {} OFF @ {:.2}s", current_note + 1, current_time);
                    gate_on_time = None;
                    current_note += 1;
                }
            }
        }

        // Process one block
        scheduler.process_block(&mut consumer, &mut output_buffer);

        // Write to WAV file
        for &sample in output_buffer.iter() {
            let sample_i16 = (sample.clamp(-1.0, 1.0) * 32767.0) as i16;
            writer.write_sample(sample_i16)?;
        }

        samples_rendered += BUFFER_SIZE;
    }

    writer.finalize()?;

    println!();
    println!("✅ Rendering complete!");
    println!("📁 Output: envelope_test.wav");
    println!();
    println!("🎧 What to listen for:");
    println!("   - Note 1: Quick attack, short sustain, smooth release");
    println!("   - Note 2: Same envelope, longer sustain phase");
    println!("   - Note 3: Same envelope, longest sustain phase");
    println!("   - Each note fades in (attack), drops (decay), holds (sustain), fades out (release)");
    println!();
    println!("💡 Try modifying:");
    println!("   - Attack time (line 107)");
    println!("   - Decay time (line 112)");
    println!("   - Sustain level (line 117)");
    println!("   - Release time (line 122)");
    println!("   - Note timings (line 153)");

    Ok(())
}