Skip to main content

sequenced_arrangement/
sequenced_arrangement.rs

1use std::{error::Error, thread, time::Duration};
2
3use kira::{AudioManager, AudioManagerSettings, backend::cpal::CpalBackend};
4use melody_bay::{
5    AudioBuffer, AudioContext, BiquadFilterType, IndexedSequence, IndexedTrack, Instrument, Note,
6    SampleEnvelope, TrackId, Velocity, Waveform,
7};
8
9const SAMPLE_RATE: u32 = 48_000;
10
11fn main() -> Result<(), Box<dyn Error>> {
12    let mut manager = AudioManager::<CpalBackend>::new(AudioManagerSettings::default())?;
13    let sequence = build_arrangement()?;
14    let timed = sequence.resolve();
15    println!(
16        "Playing {:?} by {:?}: {} tracks, {:.2}s",
17        sequence.metadata().title,
18        sequence.metadata().composer,
19        sequence.tracks().len(),
20        timed.duration_seconds()
21    );
22    let handle = manager.play(timed.sound_data().sample_rate(SAMPLE_RATE))?;
23    thread::sleep(Duration::from_secs_f64(timed.duration_seconds() + 0.4));
24    handle.stop();
25    Ok(())
26}
27
28fn build_arrangement() -> Result<IndexedSequence, Box<dyn Error>> {
29    let mut sequence = IndexedSequence::new(4)
30        .title("Curated Sequencer Arrangement")
31        .composer("melody-bay examples");
32    sequence.tempo_at(0, 132.0);
33    sequence.tempo_at(16, 96.0);
34    sequence.tempo_at(24, 144.0);
35
36    sequence.add_track(
37        TrackId::named("lead.graph"),
38        IndexedTrack::new(Instrument::graph(lead_graph()?))
39            .note_with_velocity(0, Note::from_midi(60), 3, Velocity::new(0.55))
40            .note_with_velocity(4, Note::from_midi(64), 3, Velocity::new(0.75))
41            .note_with_velocity(8, Note::from_midi(67), 4, Velocity::new(0.85))
42            .note_with_velocity(16, Note::from_midi(72), 6, Velocity::new(0.95))
43            .automation_at(0, "voice.gain", 0.18)
44            .linear_ramp_to_value_at_index(16, "voice.gain", 0.11)
45            .value_curve_at_index(20, "filter.frequency", [700.0, 1_800.0, 900.0], 8),
46    );
47
48    sequence.add_track(
49        TrackId::named("pad.graph"),
50        IndexedTrack::new(Instrument::graph(pad_graph()?))
51            .note_with_velocity(0, Note::from_midi(48), 16, Velocity::new(0.5))
52            .note_with_velocity(16, Note::from_midi(53), 12, Velocity::new(0.55))
53            .linear_ramp_to_value_at_index(12, "pad.gain", 0.08)
54            .linear_ramp_to_value_at_index(28, "pad.gain", 0.0),
55    );
56
57    sequence.add_track(
58        TrackId::named("sample.kick"),
59        IndexedTrack::new(kick_instrument()?)
60            .note_with_velocity(0, Note::from_midi(36), 1, Velocity::new(0.95))
61            .note_with_velocity(8, Note::from_midi(36), 1, Velocity::new(0.8))
62            .note_with_velocity(16, Note::from_midi(36), 1, Velocity::new(0.95))
63            .note_with_velocity(24, Note::from_midi(36), 1, Velocity::new(0.8)),
64    );
65
66    Ok(sequence)
67}
68
69fn lead_graph() -> Result<AudioContext, Box<dyn Error>> {
70    let mut graph = AudioContext::try_new_with_sample_rate(SAMPLE_RATE)?;
71    let osc = graph.create_oscillator();
72    osc.set_type(Waveform::Sawtooth);
73    let filter = graph.create_biquad_filter();
74    filter.set_type(BiquadFilterType::Lowpass);
75    filter.frequency().set_value(1_200.0)?;
76    filter.q().set_value(0.8)?;
77    let gain = graph.create_gain();
78    gain.gain().set_value(0.16)?;
79    graph.label_node(&gain, "voice")?;
80    graph.label_node(&filter, "filter")?;
81    graph.connect(osc, &filter)?;
82    graph.connect(&filter, &gain)?;
83    graph.connect(&gain, graph.destination())?;
84    Ok(graph)
85}
86
87fn pad_graph() -> Result<AudioContext, Box<dyn Error>> {
88    let mut graph = AudioContext::try_new_with_sample_rate(SAMPLE_RATE)?;
89    let pad_gain = graph.create_gain();
90    pad_gain.gain().set_value(0.08)?;
91    graph.label_node(&pad_gain, "pad")?;
92    for (frequency, detune) in [(220.0, -7.0), (277.18, 0.0), (329.63, 5.0)] {
93        let osc = graph.create_oscillator();
94        osc.set_type(Waveform::Triangle);
95        osc.frequency().set_value(frequency)?;
96        osc.detune().set_value(detune)?;
97        graph.connect(osc, &pad_gain)?;
98    }
99    graph.connect(&pad_gain, graph.destination())?;
100    Ok(graph)
101}
102
103fn kick_instrument() -> Result<Instrument, Box<dyn Error>> {
104    let samples = (0..2_400).map(|i| {
105        let t = i as f32 / SAMPLE_RATE as f32;
106        let pitch = 90.0 - t * 120.0;
107        let env = (1.0 - t * 18.0).max(0.0);
108        (t * std::f32::consts::TAU * pitch).sin() * env
109    });
110    let buffer = AudioBuffer::try_from_mono(SAMPLE_RATE, 2_400, samples)?;
111    Ok(Instrument::sample(buffer, Note::from_midi(36))
112        .volume(0.75)
113        .envelope(SampleEnvelope {
114            points: vec![(0.0, 1.0), (0.08, 0.0)],
115        }))
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn arrangement_resolves_and_renders_audio() {
124        let sequence = build_arrangement().unwrap();
125        assert_eq!(sequence.tracks().len(), 3);
126        let rendered = sequence.resolve().try_render_offline(SAMPLE_RATE).unwrap();
127        let left = rendered.channel_data(0).unwrap();
128        assert!(left.iter().any(|sample| sample.abs() > 0.001));
129    }
130
131    #[test]
132    fn arrangement_has_metadata_and_named_tracks() {
133        let sequence = build_arrangement().unwrap();
134        assert_eq!(
135            sequence.metadata().title.as_deref(),
136            Some("Curated Sequencer Arrangement")
137        );
138        assert!(sequence.track(TrackId::named("lead.graph")).is_some());
139    }
140}