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}