Skip to main content

melody_bay/sequencer/
models.rs

1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2struct EventId(u64);
3
4#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
5struct SequenceTime(f64);
6
7impl SequenceTime {
8    #[must_use]
9    pub fn seconds(seconds: f64) -> Self {
10        Self(seconds.max(0.0))
11    }
12
13    #[must_use]
14    pub fn as_seconds(self) -> f64 {
15        self.0
16    }
17}
18
19impl From<f64> for SequenceTime {
20    fn from(value: f64) -> Self {
21        Self::seconds(value)
22    }
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26pub struct Note(u8);
27
28impl Note {
29    #[must_use]
30    pub fn from_midi(number: u8) -> Self {
31        Self(number.min(127))
32    }
33
34    #[must_use]
35    pub fn midi_number(self) -> u8 {
36        self.0
37    }
38
39    #[must_use]
40    pub fn frequency(self) -> f32 {
41        440.0 * 2.0f32.powf((self.0 as f32 - 69.0) / 12.0)
42    }
43
44    #[must_use]
45    pub fn name(self) -> String {
46        const NAMES: [&str; 12] = [
47            "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B",
48        ];
49        let pitch = self.0 as usize % 12;
50        let octave = self.0 as i16 / 12 - 1;
51        format!("{}{}", NAMES[pitch], octave)
52    }
53}
54
55impl Default for Note {
56    fn default() -> Self {
57        Self::from_midi(69)
58    }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
62pub struct Velocity(f32);
63
64impl Velocity {
65    pub const MIN: Self = Self(0.0);
66    pub const MAX: Self = Self(1.0);
67
68    #[must_use]
69    pub fn new(value: f32) -> Self {
70        Self(value.clamp(0.0, 1.0))
71    }
72
73    #[must_use]
74    pub fn value(self) -> f32 {
75        self.0
76    }
77}
78
79impl Default for Velocity {
80    fn default() -> Self {
81        Self::MAX
82    }
83}
84
85impl From<f32> for Velocity {
86    fn from(value: f32) -> Self {
87        Self::new(value)
88    }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Hash)]
92pub struct InstrumentId(String);
93
94impl InstrumentId {
95    #[must_use]
96    pub fn named(name: impl Into<String>) -> Self {
97        Self(name.into())
98    }
99
100    #[must_use]
101    pub fn as_str(&self) -> &str {
102        &self.0
103    }
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Hash)]
107pub struct TrackId(String);
108
109impl TrackId {
110    #[must_use]
111    pub fn named(name: impl Into<String>) -> Self {
112        Self(name.into())
113    }
114
115    #[must_use]
116    pub fn as_str(&self) -> &str {
117        &self.0
118    }
119}
120
121impl Default for TrackId {
122    fn default() -> Self {
123        Self::named("main")
124    }
125}
126
127#[derive(Debug, Clone, Default, PartialEq, Eq)]
128pub struct SequenceMetadata {
129    pub title: Option<String>,
130    pub composer: Option<String>,
131}
132
133impl SequenceMetadata {
134    #[must_use]
135    pub fn new() -> Self {
136        Self::default()
137    }
138
139    #[must_use]
140    pub fn title(mut self, title: impl Into<String>) -> Self {
141        self.title = Some(title.into());
142        self
143    }
144
145    #[must_use]
146    pub fn composer(mut self, composer: impl Into<String>) -> Self {
147        self.composer = Some(composer.into());
148        self
149    }
150}
151
152#[derive(Debug, Clone, PartialEq)]
153pub struct SampleLoop {
154    pub start_seconds: f64,
155    pub end_seconds: f64,
156}
157
158#[derive(Debug, Clone, PartialEq)]
159pub struct SampleInstrument {
160    pub buffer: AudioBuffer,
161    pub root_note: Note,
162    pub finetune_cents: f32,
163    pub loop_range: Option<SampleLoop>,
164    pub volume: f32,
165    pub pan: f32,
166    pub envelope: Option<SampleEnvelope>,
167}
168
169#[derive(Debug, Clone, PartialEq)]
170pub struct SampleEnvelope {
171    pub points: Vec<(f64, f32)>,
172}
173
174#[derive(Debug, Clone)]
175pub enum Instrument {
176    Graph {
177        graph: AudioContext,
178        base_note: Note,
179    },
180    Sample(SampleInstrument),
181}
182
183impl Instrument {
184    #[must_use]
185    pub fn graph(graph: AudioContext) -> Self {
186        Self::Graph {
187            graph,
188            base_note: Note::default(),
189        }
190    }
191
192    #[must_use]
193    pub fn sample(buffer: AudioBuffer, root_note: Note) -> Self {
194        Self::Sample(SampleInstrument {
195            buffer,
196            root_note,
197            finetune_cents: 0.0,
198            loop_range: None,
199            volume: 1.0,
200            pan: 0.0,
201            envelope: None,
202        })
203    }
204
205    #[must_use]
206    pub fn base_note(mut self, base_note: Note) -> Self {
207        if let Self::Graph {
208            base_note: current, ..
209        } = &mut self
210        {
211            *current = base_note;
212        }
213        self
214    }
215
216    #[must_use]
217    pub fn finetune_cents(mut self, cents: f32) -> Self {
218        if let Self::Sample(sample) = &mut self {
219            sample.finetune_cents = if cents.is_finite() { cents } else { 0.0 };
220        }
221        self
222    }
223
224    #[must_use]
225    pub fn loop_range(mut self, start_seconds: f64, end_seconds: f64) -> Self {
226        if let Self::Sample(sample) = &mut self
227            && start_seconds.is_finite()
228            && end_seconds.is_finite()
229        {
230            sample.loop_range = Some(SampleLoop {
231                start_seconds,
232                end_seconds,
233            });
234        }
235        self
236    }
237
238    #[must_use]
239    pub fn volume(mut self, volume: f32) -> Self {
240        if let Self::Sample(sample) = &mut self {
241            sample.volume = volume.max(0.0);
242        }
243        self
244    }
245
246    #[must_use]
247    pub fn pan(mut self, pan: f32) -> Self {
248        if let Self::Sample(sample) = &mut self {
249            sample.pan = pan.clamp(-1.0, 1.0);
250        }
251        self
252    }
253
254    #[must_use]
255    pub fn envelope(mut self, envelope: SampleEnvelope) -> Self {
256        if let Self::Sample(sample) = &mut self {
257            sample.envelope = Some(envelope);
258        }
259        self
260    }
261
262    fn base_frequency(&self) -> f32 {
263        match self {
264            Self::Graph { base_note, .. } => base_note.frequency(),
265            Self::Sample(sample) => {
266                sample.root_note.frequency() * 2.0f32.powf(sample.finetune_cents / 1200.0)
267            }
268        }
269    }
270
271    fn audio_context(&self) -> AudioContext {
272        match self {
273            Self::Graph { graph, .. } => graph.clone(),
274            Self::Sample(sample) => sample.to_audio_context(),
275        }
276    }
277}
278
279impl SampleInstrument {
280    fn to_audio_context(&self) -> AudioContext {
281        let mut graph = AudioContext::new();
282        let source = graph.create_buffer_source();
283        let _ = graph.label_node(&source, "source");
284        let _ = source.try_set_buffer(self.buffer.clone());
285        if let Some(loop_range) = &self.loop_range {
286            source.set_looping(true);
287            let _ = source.try_loop_range(loop_range.start_seconds, loop_range.end_seconds);
288        }
289        let envelope_gain = graph.create_gain();
290        let channel_gain = graph.create_gain();
291        let _ = graph.label_node(&channel_gain, "channel");
292        let pan = graph.create_stereo_panner();
293        if let Some(envelope) = &self.envelope {
294            let mut points = envelope.points.to_vec();
295            points.sort_by(|a, b| a.0.total_cmp(&b.0));
296            if let Some((first_time, first_value)) = points.first().copied() {
297                let _ = envelope_gain
298                    .gain()
299                    .set_value_at_time(first_value, first_time.max(0.0));
300                for (time, value) in points.into_iter().skip(1) {
301                    let _ = envelope_gain
302                        .gain()
303                        .linear_ramp_to_value_at_time(value, time.max(0.0));
304                }
305            }
306        } else {
307            let _ = envelope_gain.gain().set_value(1.0);
308        }
309        let _ = channel_gain.gain().set_value(self.volume.max(0.0));
310        let _ = pan.pan().set_value(self.pan.clamp(-1.0, 1.0));
311        let _ = graph.connect(source, &envelope_gain);
312        let _ = graph.connect(&envelope_gain, &channel_gain);
313        let _ = graph.connect(&channel_gain, &pan);
314        let _ = graph.connect(&pan, graph.destination());
315        graph
316    }
317}
318
319#[derive(Debug, Clone, PartialEq)]
320pub struct TimedNoteEvent {
321    pub start_seconds: f64,
322    pub duration_seconds: f64,
323    pub note: Note,
324    pub velocity: Velocity,
325}
326
327#[derive(Debug, Clone, PartialEq)]
328pub enum AutomationShape {
329    SetValue {
330        value: f32,
331    },
332    LinearRamp {
333        value: f32,
334    },
335    ValueCurve {
336        values: Vec<f32>,
337        duration_seconds: f64,
338    },
339}
340
341#[derive(Debug, Clone, PartialEq)]
342pub struct TimedAutomationEvent {
343    pub time_seconds: f64,
344    pub target: String,
345    pub shape: AutomationShape,
346}
347
348#[derive(Debug, Clone)]
349pub struct TimedTrack {
350    pub instrument: Instrument,
351    notes: Vec<TimedNoteEvent>,
352    automation: Vec<TimedAutomationEvent>,
353}
354
355impl TimedTrack {
356    #[must_use]
357    pub fn new(instrument: Instrument) -> Self {
358        Self {
359            instrument,
360            notes: Vec::new(),
361            automation: Vec::new(),
362        }
363    }
364
365    #[must_use]
366    pub fn note_at(
367        mut self,
368        start_seconds: f64,
369        note: Note,
370        duration_seconds: f64,
371        velocity: Velocity,
372    ) -> Self {
373        self.notes.push(TimedNoteEvent {
374            start_seconds: start_seconds.max(0.0),
375            duration_seconds: duration_seconds.max(0.0),
376            note,
377            velocity,
378        });
379        self
380    }
381
382    #[must_use]
383    pub fn automation_at(
384        mut self,
385        time_seconds: f64,
386        target: impl Into<String>,
387        value: f32,
388    ) -> Self {
389        self.automation.push(TimedAutomationEvent {
390            time_seconds,
391            target: target.into(),
392            shape: AutomationShape::SetValue { value },
393        });
394        self
395    }
396
397    #[must_use]
398    pub fn linear_ramp_to_value_at(
399        mut self,
400        end_seconds: f64,
401        target: impl Into<String>,
402        value: f32,
403    ) -> Self {
404        self.automation.push(TimedAutomationEvent {
405            time_seconds: end_seconds,
406            target: target.into(),
407            shape: AutomationShape::LinearRamp { value },
408        });
409        self
410    }
411
412    #[must_use]
413    pub fn value_curve_at(
414        mut self,
415        start_seconds: f64,
416        target: impl Into<String>,
417        values: impl IntoIterator<Item = f32>,
418        duration_seconds: f64,
419    ) -> Self {
420        self.automation.push(TimedAutomationEvent {
421            time_seconds: start_seconds,
422            target: target.into(),
423            shape: AutomationShape::ValueCurve {
424                values: values.into_iter().collect(),
425                duration_seconds,
426            },
427        });
428        self
429    }
430
431    #[must_use]
432    pub fn notes(&self) -> &[TimedNoteEvent] {
433        &self.notes
434    }
435
436    #[must_use]
437    pub fn automation(&self) -> &[TimedAutomationEvent] {
438        &self.automation
439    }
440}
441
442#[derive(Debug, Clone)]
443pub struct TimedSequence {
444    pub metadata: SequenceMetadata,
445    duration_seconds: Option<f64>,
446    tracks: HashMap<TrackId, TimedTrack>,
447}
448
449impl TimedSequence {
450    #[must_use]
451    pub fn new() -> Self {
452        Self {
453            metadata: SequenceMetadata::default(),
454            duration_seconds: None,
455            tracks: HashMap::new(),
456        }
457    }
458
459    #[must_use]
460    pub fn title(mut self, title: impl Into<String>) -> Self {
461        self.metadata.title = Some(title.into());
462        self
463    }
464
465    #[must_use]
466    pub fn composer(mut self, composer: impl Into<String>) -> Self {
467        self.metadata.composer = Some(composer.into());
468        self
469    }
470
471    #[must_use]
472    pub fn metadata(&self) -> &SequenceMetadata {
473        &self.metadata
474    }
475
476    #[must_use]
477    pub fn with_duration(mut self, duration_seconds: f64) -> Self {
478        self.duration_seconds = Some(duration_seconds.max(0.0));
479        self
480    }
481
482    #[must_use]
483    pub fn duration_seconds(&self) -> f64 {
484        self.resolved_duration_seconds()
485    }
486
487    #[must_use]
488    pub fn with_track(mut self, id: TrackId, track: TimedTrack) -> Self {
489        self.add_track(id, track);
490        self
491    }
492
493    pub fn add_track(&mut self, id: TrackId, track: TimedTrack) {
494        self.tracks.insert(id, track);
495    }
496
497    #[must_use]
498    pub fn track(&self, id: TrackId) -> Option<&TimedTrack> {
499        self.tracks.get(&id)
500    }
501
502    #[must_use]
503    pub fn tracks(&self) -> &HashMap<TrackId, TimedTrack> {
504        &self.tracks
505    }
506
507    /// Creates Kira sound data without validating sequencer automation.
508    ///
509    /// This mirrors Kira's ergonomic `SoundData` construction style. Invalid
510    /// sequencer automation targets or events are ignored during scheduling.
511    /// Use [`Self::try_sound_data`] or [`Self::validate`] when authoring code
512    /// should report automation mistakes before playback.
513    #[must_use]
514    pub fn sound_data(&self) -> SequencerSoundData {
515        SequencerSoundData {
516            sequence: Arc::new(self.clone()),
517            track_id: None,
518            sample_rate: 44_100,
519        }
520    }
521
522    /// Validates sequencer automation before creating Kira sound data.
523    pub fn try_sound_data(&self) -> Result<SequencerSoundData, SequencerValidationError> {
524        self.validate()?;
525        Ok(self.sound_data())
526    }
527
528    /// Creates Kira sound data for one track without validating sequencer
529    /// automation.
530    ///
531    /// Use [`Self::try_track_sound_data`] to validate automation on the
532    /// requested track before playback.
533    #[must_use]
534    pub fn track_sound_data(&self, track_id: TrackId) -> SequencerSoundData {
535        SequencerSoundData {
536            sequence: Arc::new(self.clone()),
537            track_id: Some(track_id),
538            sample_rate: 44_100,
539        }
540    }
541
542    /// Validates sequencer automation on the requested track before creating
543    /// Kira sound data for that track.
544    pub fn try_track_sound_data(
545        &self,
546        track_id: TrackId,
547    ) -> Result<SequencerSoundData, SequencerValidationError> {
548        self.validate_track(&track_id)?;
549        Ok(self.track_sound_data(track_id))
550    }
551
552    /// Renders the sequence offline without validating sequencer automation.
553    ///
554    /// Use [`Self::try_render_offline`] to validate automation before
555    /// rendering.
556    #[must_use]
557    pub fn render_offline(&self, sample_rate: u32) -> AudioBuffer {
558        render_timed_sequence_offline(self, None, sample_rate)
559    }
560
561    /// Validates sequencer automation before rendering the sequence offline.
562    pub fn try_render_offline(
563        &self,
564        sample_rate: u32,
565    ) -> Result<AudioBuffer, SequencerValidationError> {
566        self.validate()?;
567        Ok(self.render_offline(sample_rate))
568    }
569
570    /// Renders one track offline without validating sequencer automation.
571    ///
572    /// Use [`Self::try_render_track_offline`] to validate automation on the
573    /// requested track before rendering.
574    #[must_use]
575    pub fn render_track_offline(&self, track_id: TrackId, sample_rate: u32) -> AudioBuffer {
576        render_timed_sequence_offline(self, Some(&track_id), sample_rate)
577    }
578
579    /// Validates sequencer automation on the requested track before rendering
580    /// that track offline.
581    pub fn try_render_track_offline(
582        &self,
583        track_id: TrackId,
584        sample_rate: u32,
585    ) -> Result<AudioBuffer, SequencerValidationError> {
586        self.validate_track(&track_id)?;
587        Ok(self.render_track_offline(track_id, sample_rate))
588    }
589
590    /// Validates sequencer automation across every track.
591    pub fn validate(&self) -> Result<(), SequencerValidationError> {
592        for (track_id, track) in &self.tracks {
593            validate_timed_track(track_id, track)?;
594        }
595        Ok(())
596    }
597
598    fn validate_track(&self, track_id: &TrackId) -> Result<(), SequencerValidationError> {
599        if let Some(track) = self.tracks.get(track_id) {
600            validate_timed_track(track_id, track)?;
601        }
602        Ok(())
603    }
604
605    fn resolved_duration_seconds(&self) -> f64 {
606        let note_duration = self
607            .tracks
608            .values()
609            .flat_map(|track| track.notes.iter())
610            .map(|note| note.start_seconds + note.duration_seconds)
611            .fold(0.0, f64::max);
612        let automation_duration = self
613            .tracks
614            .values()
615            .flat_map(|track| track.automation.iter())
616            .map(|automation| match &automation.shape {
617                AutomationShape::ValueCurve {
618                    duration_seconds, ..
619                } => automation.time_seconds + duration_seconds,
620                _ => automation.time_seconds,
621            })
622            .fold(0.0, f64::max);
623        self.duration_seconds
624            .unwrap_or(0.0)
625            .max(note_duration)
626            .max(automation_duration)
627    }
628}
629
630impl Default for TimedSequence {
631    fn default() -> Self {
632        Self::new()
633    }
634}
635
636#[cfg(test)]
637mod timed_sequence_graph_native_tests {
638    use super::*;
639
640    #[test]
641    fn compiled_graph_renders_partial_quantum() {
642        let mut graph = AudioContext::new();
643        let source = graph.create_constant_source();
644        source.offset().set_value(0.5).unwrap();
645        source.try_start(0.0).unwrap();
646        graph
647            .connect(source, graph.destination())
648            .expect("source connects");
649
650        let compiled = graph.compiled().expect("graph compiles");
651        let mut runtime = compiled.runtime().expect("runtime builds");
652        let info = MockInfoBuilder::new().build();
653        let quantum = compiled.render_quantum_with_runtime(
654            RenderQuantum {
655                start: 0.0,
656                global_start: 0.0,
657                sample_dt: 1.0 / 48_000.0,
658                frames: 17,
659                commit_source_state: false,
660            },
661            None,
662            &mut runtime,
663            &info,
664        );
665
666        assert_eq!(quantum.len(), 17);
667        assert!(quantum.iter().all(|frame| *frame == Frame::new(0.5, 0.5)));
668    }
669
670    #[test]
671    fn sample_instruments_compile_to_webaudio_native_sample_voice() {
672        let buffer = AudioBuffer::try_from_mono(8_000, 4, [0.0, 1.0, 0.0, -1.0]).unwrap();
673        let sequence = TimedSequence::new().with_track(
674            TrackId::named("sample"),
675            TimedTrack::new(Instrument::sample(buffer, Note::from_midi(60))).note_at(
676                0.0,
677                Note::from_midi(60),
678                0.25,
679                Velocity::MAX,
680            ),
681        );
682
683        let compiled =
684            compile_timed_sequence_tracks(&sequence, None).expect("sample graph compiles");
685        let compiled = compiled
686            .get(&TrackId::named("sample"))
687            .expect("sample track exists");
688        assert!(
689            compiled.sample_voice.is_some(),
690            "sample instruments should render through a compiled WebAudio graph sample voice"
691        );
692    }
693
694    #[test]
695    fn sequencer_sound_data_stores_modern_sequence_directly() {
696        let track_id = TrackId::named("lead");
697        let sequence = TimedSequence::new().with_track(
698            track_id.clone(),
699            TimedTrack::new(Instrument::graph(AudioContext::new())),
700        );
701
702        let all_tracks = sequence.sound_data();
703        assert_eq!(all_tracks.sample_rate, 44_100);
704        assert!(all_tracks.track_id.is_none());
705        assert!(Arc::ptr_eq(
706            &all_tracks.sequence,
707            &all_tracks.sequence.clone()
708        ));
709
710        let track = sequence
711            .track_sound_data(track_id.clone())
712            .sample_rate(22_050);
713        assert_eq!(track.sample_rate, 22_050);
714        assert_eq!(track.track_id.as_ref(), Some(&track_id));
715        assert_eq!(track.sequence.tracks().len(), 1);
716    }
717}
718
719fn validate_timed_track(
720    track_id: &TrackId,
721    track: &TimedTrack,
722) -> Result<(), SequencerValidationError> {
723    let graph = track.instrument.audio_context();
724    for automation in &track.automation {
725        graph.validate_sequencer_automation_target(track_id, automation)?;
726    }
727    Ok(())
728}
729
730#[derive(Debug, Clone, PartialEq)]
731pub enum SequencerValidationError {
732    InvalidAutomationTarget {
733        track_id: TrackId,
734        target: String,
735    },
736    InvalidAutomationTime {
737        track_id: TrackId,
738        target: String,
739        time_seconds: f64,
740    },
741    InvalidAutomationValue {
742        track_id: TrackId,
743        target: String,
744        value: f32,
745    },
746    InvalidAutomationDuration {
747        track_id: TrackId,
748        target: String,
749        duration_seconds: f64,
750    },
751}
752
753impl fmt::Display for SequencerValidationError {
754    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
755        match self {
756            Self::InvalidAutomationTarget { track_id, target } => write!(
757                f,
758                "invalid automation target '{target}' on track '{}'",
759                track_id.as_str()
760            ),
761            Self::InvalidAutomationTime {
762                track_id,
763                target,
764                time_seconds,
765            } => write!(
766                f,
767                "invalid automation time {time_seconds} for target '{target}' on track '{}'",
768                track_id.as_str()
769            ),
770            Self::InvalidAutomationValue {
771                track_id,
772                target,
773                value,
774            } => write!(
775                f,
776                "invalid automation value {value} for target '{target}' on track '{}'",
777                track_id.as_str()
778            ),
779            Self::InvalidAutomationDuration {
780                track_id,
781                target,
782                duration_seconds,
783            } => write!(
784                f,
785                "invalid automation duration {duration_seconds} for target '{target}' on track '{}'",
786                track_id.as_str()
787            ),
788        }
789    }
790}
791
792impl std::error::Error for SequencerValidationError {}
793
794#[derive(Debug)]
795pub struct SequencerSoundData {
796    sequence: Arc<TimedSequence>,
797    track_id: Option<TrackId>,
798    sample_rate: u32,
799}
800
801impl SequencerSoundData {
802    #[must_use]
803    pub fn sample_rate(mut self, sample_rate: u32) -> Self {
804        self.sample_rate = sample_rate.max(1);
805        self
806    }
807}
808
809#[derive(Debug, Clone)]
810pub struct SequencerSoundHandle {
811    state: Arc<HandleState>,
812}
813
814impl SequencerSoundHandle {
815    pub fn set_gain(&self, gain: f32) {
816        self.state
817            .gain_bits
818            .store(gain.max(0.0).to_bits(), Ordering::Relaxed);
819    }
820
821    pub fn stop(&self) {
822        self.state.stopped.store(true, Ordering::Relaxed);
823    }
824}
825
826#[derive(Debug, Clone, Copy, PartialEq, Eq)]
827pub struct SequencerSoundError;
828
829impl fmt::Display for SequencerSoundError {
830    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
831        f.write_str("failed to build sequencer sound")
832    }
833}
834
835impl std::error::Error for SequencerSoundError {}
836
837impl SoundData for SequencerSoundData {
838    type Error = SequencerSoundError;
839    type Handle = SequencerSoundHandle;
840
841    fn into_sound(self) -> Result<(Box<dyn Sound>, Self::Handle), Self::Error> {
842        let state = Arc::new(HandleState::default());
843        let handle = SequencerSoundHandle {
844            state: Arc::clone(&state),
845        };
846        let sound = SequencerSound::new(self.sequence, self.track_id, self.sample_rate, state)
847            .map_err(|_| SequencerSoundError)?;
848        Ok((Box::new(sound), handle))
849    }
850}
851
852#[derive(Debug)]
853struct SequencerSound {
854    sequence: Arc<TimedSequence>,
855    track_id: Option<TrackId>,
856    sample_rate: u32,
857    elapsed: f64,
858    compiled_tracks: HashMap<TrackId, CompiledGraph>,
859    scheduled_notes: Vec<ScheduledTimedNote>,
860    next_scheduled_note: usize,
861    active_notes: Vec<ActiveTimedNote>,
862    state: Arc<HandleState>,
863}
864
865impl SequencerSound {
866    fn new(
867        sequence: Arc<TimedSequence>,
868        track_id: Option<TrackId>,
869        sample_rate: u32,
870        state: Arc<HandleState>,
871    ) -> Result<Self, GraphError> {
872        let compiled_tracks = compile_timed_sequence_tracks(&sequence, track_id.as_ref())?;
873        let scheduled_notes = timed_sequence_note_schedule(&sequence, track_id.as_ref());
874        let active_capacity = sequence.tracks.len().max(1);
875        Ok(Self {
876            sequence,
877            track_id,
878            sample_rate: sample_rate.max(1),
879            elapsed: 0.0,
880            compiled_tracks,
881            scheduled_notes,
882            next_scheduled_note: 0,
883            active_notes: Vec::with_capacity(active_capacity),
884            state,
885        })
886    }
887
888    fn render_quantum(
889        &mut self,
890        quantum_start: f64,
891        sample_dt: f64,
892        frames: usize,
893        info: &Info,
894    ) -> Vec<Frame> {
895        render_timed_sequence_quantum(
896            TimedSequenceQuantum {
897                track_id: self.track_id.as_ref(),
898                quantum_start,
899                sample_dt,
900                frames,
901                compiled_tracks: &self.compiled_tracks,
902                scheduled_notes: &self.scheduled_notes,
903                handle_state: &self.state,
904                info,
905            },
906            TimedSequenceRenderState {
907                next_scheduled_note: &mut self.next_scheduled_note,
908                active_notes: &mut self.active_notes,
909            },
910        )
911    }
912}
913
914impl Sound for SequencerSound {
915    fn process(&mut self, out: &mut [Frame], dt: f64, info: &Info) {
916        let sample_dt = if dt.is_finite() && dt > 0.0 {
917            dt
918        } else {
919            1.0 / self.sample_rate as f64
920        };
921        let mut offset = 0;
922        while offset < out.len() {
923            let quantum_frames = (out.len() - offset).min(RENDER_QUANTUM_SIZE_USIZE);
924            let quantum = self.render_quantum(self.elapsed, sample_dt, quantum_frames, info);
925            out[offset..offset + quantum_frames].copy_from_slice(&quantum);
926            self.elapsed += sample_dt * quantum_frames as f64;
927            offset += quantum_frames;
928        }
929    }
930
931    fn finished(&self) -> bool {
932        self.state.stopped.load(Ordering::Relaxed)
933            || self.elapsed >= self.sequence.resolved_duration_seconds()
934    }
935}
936
937#[derive(Debug)]
938struct ActiveTimedNote {
939    track_id: TrackId,
940    note: NoteEvent,
941    runtime: VoiceRuntime,
942}
943
944struct TimedSequenceQuantum<'a> {
945    track_id: Option<&'a TrackId>,
946    quantum_start: f64,
947    sample_dt: f64,
948    frames: usize,
949    compiled_tracks: &'a HashMap<TrackId, CompiledGraph>,
950    scheduled_notes: &'a [ScheduledTimedNote],
951    handle_state: &'a HandleState,
952    info: &'a Info<'a>,
953}
954
955struct TimedSequenceRenderState<'a> {
956    next_scheduled_note: &'a mut usize,
957    active_notes: &'a mut Vec<ActiveTimedNote>,
958}
959
960#[derive(Debug, Clone)]
961struct ScheduledTimedNote {
962    track_id: TrackId,
963    note: NoteEvent,
964}
965
966fn compile_timed_sequence_tracks(
967    sequence: &TimedSequence,
968    track_id: Option<&TrackId>,
969) -> Result<HashMap<TrackId, CompiledGraph>, GraphError> {
970    let mut compiled = HashMap::new();
971    for (candidate_id, track) in &sequence.tracks {
972        if track_id.is_some_and(|track_id| candidate_id != track_id) {
973            continue;
974        }
975        let mut graph = track.instrument.audio_context();
976        for automation in &track.automation {
977            graph.schedule_named_param_automation(automation);
978        }
979        compiled.insert(candidate_id.clone(), graph.compiled()?);
980    }
981    Ok(compiled)
982}
983
984fn timed_sequence_note_schedule(
985    sequence: &TimedSequence,
986    track_id: Option<&TrackId>,
987) -> Vec<ScheduledTimedNote> {
988    let mut notes = Vec::new();
989    for (candidate_id, track) in &sequence.tracks {
990        if track_id.is_some_and(|track_id| candidate_id != track_id) {
991            continue;
992        }
993        for (index, note) in track.notes.iter().enumerate() {
994            notes.push(ScheduledTimedNote {
995                track_id: candidate_id.clone(),
996                note: NoteEvent {
997                    id: EventId(index as u64 + 1),
998                    start: SequenceTime::seconds(note.start_seconds),
999                    duration: note.duration_seconds,
1000                    frequency: note.note.frequency(),
1001                    base_frequency: track.instrument.base_frequency(),
1002                    velocity: note.velocity.value(),
1003                },
1004            });
1005        }
1006    }
1007    notes.sort_by(|left, right| {
1008        left.note
1009            .start
1010            .as_seconds()
1011            .total_cmp(&right.note.start.as_seconds())
1012    });
1013    notes
1014}
1015
1016fn render_timed_sequence_offline(
1017    sequence: &TimedSequence,
1018    track_id: Option<&TrackId>,
1019    sample_rate: u32,
1020) -> AudioBuffer {
1021    let sample_rate = sample_rate.max(1);
1022    let frame_count =
1023        (sequence.resolved_duration_seconds().max(0.0) * sample_rate as f64).ceil() as usize;
1024    let compiled_tracks = compile_timed_sequence_tracks(sequence, track_id).unwrap_or_default();
1025    let scheduled_notes = timed_sequence_note_schedule(sequence, track_id);
1026    let mut next_scheduled_note = 0;
1027    let mut active_notes = Vec::with_capacity(sequence.tracks.len().max(1));
1028    let state = HandleState::default();
1029    let info = MockInfoBuilder::new().build();
1030    let sample_dt = 1.0 / sample_rate as f64;
1031    let mut frames = Vec::with_capacity(frame_count);
1032    let mut index = 0;
1033    while index < frame_count {
1034        let quantum_frames = (frame_count - index).min(RENDER_QUANTUM_SIZE_USIZE);
1035        let quantum = render_timed_sequence_quantum(
1036            TimedSequenceQuantum {
1037                track_id,
1038                quantum_start: index as f64 * sample_dt,
1039                sample_dt,
1040                frames: quantum_frames,
1041                compiled_tracks: &compiled_tracks,
1042                scheduled_notes: &scheduled_notes,
1043                handle_state: &state,
1044                info: &info,
1045            },
1046            TimedSequenceRenderState {
1047                next_scheduled_note: &mut next_scheduled_note,
1048                active_notes: &mut active_notes,
1049            },
1050        );
1051        frames.extend_from_slice(&quantum);
1052        index += quantum_frames;
1053    }
1054    AudioBuffer::from_frames(sample_rate, &frames)
1055}
1056
1057fn render_timed_sequence_quantum(
1058    quantum: TimedSequenceQuantum<'_>,
1059    state: TimedSequenceRenderState<'_>,
1060) -> Vec<Frame> {
1061    let TimedSequenceQuantum {
1062        track_id,
1063        quantum_start,
1064        sample_dt,
1065        frames,
1066        compiled_tracks,
1067        scheduled_notes,
1068        handle_state,
1069        info,
1070    } = quantum;
1071    let TimedSequenceRenderState {
1072        next_scheduled_note,
1073        active_notes,
1074    } = state;
1075    let frames = frames.min(RENDER_QUANTUM_SIZE_USIZE);
1076    let mut rendered = vec![Frame::ZERO; frames];
1077    if handle_state.stopped.load(Ordering::Relaxed) {
1078        return rendered;
1079    }
1080    let live_gain = f32::from_bits(handle_state.gain_bits.load(Ordering::Relaxed));
1081    let mut frame_offset = 0;
1082    while frame_offset < frames {
1083        let segment_start = quantum_start + frame_offset as f64 * sample_dt;
1084        while let Some(scheduled) = scheduled_notes.get(*next_scheduled_note) {
1085            if scheduled.note.start.as_seconds() > segment_start {
1086                break;
1087            }
1088            if scheduled.note.active_at(segment_start) {
1089                active_notes.push(ActiveTimedNote {
1090                    track_id: scheduled.track_id.clone(),
1091                    note: scheduled.note.clone(),
1092                    runtime: VoiceRuntime::default(),
1093                });
1094            }
1095            *next_scheduled_note += 1;
1096        }
1097        active_notes.retain(|active| {
1098            track_id.is_none_or(|track_id| &active.track_id == track_id)
1099                && active.note.active_at(segment_start)
1100        });
1101
1102        let mut next_boundary = frames;
1103        if let Some(next) = scheduled_notes.get(*next_scheduled_note) {
1104            let next_start = next.note.start.as_seconds();
1105            if next_start > segment_start {
1106                next_boundary = next_boundary.min(frame_index_for_time(
1107                    next_start,
1108                    quantum_start,
1109                    sample_dt,
1110                    frames,
1111                ));
1112            }
1113        }
1114        for active in active_notes.iter() {
1115            let note_end = active.note.start.as_seconds() + active.note.duration;
1116            if note_end > segment_start {
1117                next_boundary = next_boundary.min(frame_index_for_time(
1118                    note_end,
1119                    quantum_start,
1120                    sample_dt,
1121                    frames,
1122                ));
1123            }
1124        }
1125        let segment_end = next_boundary.max(frame_offset + 1).min(frames);
1126        let segment_frames = segment_end - frame_offset;
1127
1128        for active in active_notes.iter_mut() {
1129            let Some(compiled) = compiled_tracks.get(&active.track_id) else {
1130                continue;
1131            };
1132            let note_frames = compiled.render_note_quantum(
1133                &active.note,
1134                segment_start,
1135                sample_dt,
1136                segment_frames,
1137                &mut active.runtime,
1138                info,
1139            );
1140            for (frame, sample) in note_frames.into_iter().enumerate() {
1141                rendered[frame_offset + frame] += sample;
1142            }
1143        }
1144        frame_offset = segment_end;
1145    }
1146    for frame in &mut rendered {
1147        *frame *= live_gain;
1148    }
1149    rendered
1150}
1151
1152#[derive(Debug, Clone, PartialEq)]
1153pub struct TempoMap {
1154    steps_per_beat: u32,
1155    events: Vec<(u64, f64)>,
1156}
1157
1158impl TempoMap {
1159    #[must_use]
1160    pub fn new(steps_per_beat: u32) -> Self {
1161        Self {
1162            steps_per_beat: steps_per_beat.max(1),
1163            events: vec![(0, 120.0)],
1164        }
1165    }
1166
1167    pub fn tempo_at(&mut self, index: u64, bpm: f64) {
1168        let bpm = if bpm.is_finite() && bpm > 0.0 {
1169            bpm
1170        } else {
1171            120.0
1172        };
1173        if let Some((_, existing)) = self.events.iter_mut().find(|(event, _)| *event == index) {
1174            *existing = bpm;
1175        } else {
1176            self.events.push((index, bpm));
1177            self.events.sort_by_key(|(index, _)| *index);
1178        }
1179    }
1180
1181    #[must_use]
1182    pub fn steps_per_beat(&self) -> u32 {
1183        self.steps_per_beat
1184    }
1185
1186    #[must_use]
1187    pub fn tempo_events(&self) -> &[(u64, f64)] {
1188        &self.events
1189    }
1190
1191    #[must_use]
1192    pub fn bpm_at(&self, index: u64) -> f64 {
1193        self.events
1194            .iter()
1195            .take_while(|(event_index, _)| *event_index <= index)
1196            .last()
1197            .map_or(120.0, |(_, bpm)| *bpm)
1198    }
1199
1200    #[must_use]
1201    pub fn seconds_at(&self, index: u64) -> f64 {
1202        self.seconds_between(0, index)
1203    }
1204
1205    #[must_use]
1206    pub fn seconds_between(&self, start_index: u64, end_index: u64) -> f64 {
1207        if end_index <= start_index {
1208            return 0.0;
1209        }
1210        let mut seconds = 0.0;
1211        let mut cursor = start_index;
1212        let mut bpm = self.bpm_at(start_index);
1213        for (event_index, event_bpm) in self.events.iter().copied() {
1214            if event_index <= start_index {
1215                continue;
1216            }
1217            if event_index >= end_index {
1218                break;
1219            }
1220            seconds += self.segment_seconds(cursor, event_index, bpm);
1221            cursor = event_index;
1222            bpm = event_bpm;
1223        }
1224        seconds + self.segment_seconds(cursor, end_index, bpm)
1225    }
1226
1227    fn segment_seconds(&self, start_index: u64, end_index: u64, bpm: f64) -> f64 {
1228        let steps = end_index.saturating_sub(start_index) as f64;
1229        let beats = steps / self.steps_per_beat as f64;
1230        beats * 60.0 / bpm
1231    }
1232}
1233
1234#[derive(Debug, Clone, PartialEq)]
1235pub struct IndexedNoteEvent {
1236    pub start_index: u64,
1237    pub duration_indices: u64,
1238    pub note: Note,
1239    pub velocity: Velocity,
1240}
1241
1242#[derive(Debug, Clone, PartialEq)]
1243pub struct IndexedAutomationEvent {
1244    pub index: u64,
1245    pub target: String,
1246    pub shape: IndexedAutomationShape,
1247}
1248
1249#[derive(Debug, Clone, PartialEq)]
1250pub enum IndexedAutomationShape {
1251    SetValue {
1252        value: f32,
1253    },
1254    LinearRamp {
1255        value: f32,
1256    },
1257    ValueCurve {
1258        values: Vec<f32>,
1259        duration_indices: u64,
1260    },
1261}
1262
1263#[derive(Debug, Clone)]
1264pub struct IndexedTrack {
1265    pub instrument: Instrument,
1266    notes: Vec<IndexedNoteEvent>,
1267    automation: Vec<IndexedAutomationEvent>,
1268}
1269
1270impl IndexedTrack {
1271    #[must_use]
1272    pub fn new(instrument: Instrument) -> Self {
1273        Self {
1274            instrument,
1275            notes: Vec::new(),
1276            automation: Vec::new(),
1277        }
1278    }
1279
1280    #[must_use]
1281    pub fn note(mut self, start_index: u64, note: Note, duration_indices: u64) -> Self {
1282        self.notes.push(IndexedNoteEvent {
1283            start_index,
1284            duration_indices,
1285            note,
1286            velocity: Velocity::MAX,
1287        });
1288        self
1289    }
1290
1291    #[must_use]
1292    pub fn note_with_velocity(
1293        mut self,
1294        start_index: u64,
1295        note: Note,
1296        duration_indices: u64,
1297        velocity: Velocity,
1298    ) -> Self {
1299        self.notes.push(IndexedNoteEvent {
1300            start_index,
1301            duration_indices,
1302            note,
1303            velocity,
1304        });
1305        self
1306    }
1307
1308    #[must_use]
1309    pub fn automation_at(mut self, index: u64, target: impl Into<String>, value: f32) -> Self {
1310        self.automation.push(IndexedAutomationEvent {
1311            index,
1312            target: target.into(),
1313            shape: IndexedAutomationShape::SetValue { value },
1314        });
1315        self
1316    }
1317
1318    #[must_use]
1319    pub fn linear_ramp_to_value_at_index(
1320        mut self,
1321        end_index: u64,
1322        target: impl Into<String>,
1323        value: f32,
1324    ) -> Self {
1325        self.automation.push(IndexedAutomationEvent {
1326            index: end_index,
1327            target: target.into(),
1328            shape: IndexedAutomationShape::LinearRamp { value },
1329        });
1330        self
1331    }
1332
1333    #[must_use]
1334    pub fn value_curve_at_index(
1335        mut self,
1336        start_index: u64,
1337        target: impl Into<String>,
1338        values: impl IntoIterator<Item = f32>,
1339        duration_indices: u64,
1340    ) -> Self {
1341        self.automation.push(IndexedAutomationEvent {
1342            index: start_index,
1343            target: target.into(),
1344            shape: IndexedAutomationShape::ValueCurve {
1345                values: values.into_iter().collect(),
1346                duration_indices,
1347            },
1348        });
1349        self
1350    }
1351
1352    #[must_use]
1353    pub fn notes(&self) -> &[IndexedNoteEvent] {
1354        &self.notes
1355    }
1356
1357    #[must_use]
1358    pub fn automation(&self) -> &[IndexedAutomationEvent] {
1359        &self.automation
1360    }
1361}
1362
1363#[derive(Debug, Clone)]
1364pub struct IndexedSequence {
1365    pub metadata: SequenceMetadata,
1366    pub tempo_map: TempoMap,
1367    tracks: HashMap<TrackId, IndexedTrack>,
1368}
1369
1370impl IndexedSequence {
1371    #[must_use]
1372    pub fn new(steps_per_beat: u32) -> Self {
1373        Self {
1374            metadata: SequenceMetadata::default(),
1375            tempo_map: TempoMap::new(steps_per_beat),
1376            tracks: HashMap::new(),
1377        }
1378    }
1379
1380    #[must_use]
1381    pub fn title(mut self, title: impl Into<String>) -> Self {
1382        self.metadata.title = Some(title.into());
1383        self
1384    }
1385
1386    #[must_use]
1387    pub fn composer(mut self, composer: impl Into<String>) -> Self {
1388        self.metadata.composer = Some(composer.into());
1389        self
1390    }
1391
1392    #[must_use]
1393    pub fn metadata(&self) -> &SequenceMetadata {
1394        &self.metadata
1395    }
1396
1397    pub fn tempo_at(&mut self, index: u64, bpm: f64) {
1398        self.tempo_map.tempo_at(index, bpm);
1399    }
1400
1401    pub fn add_track(&mut self, id: TrackId, track: IndexedTrack) {
1402        self.tracks.insert(id, track);
1403    }
1404
1405    #[must_use]
1406    pub fn with_track(mut self, id: TrackId, track: IndexedTrack) -> Self {
1407        self.add_track(id, track);
1408        self
1409    }
1410
1411    #[must_use]
1412    pub fn track(&self, id: TrackId) -> Option<&IndexedTrack> {
1413        self.tracks.get(&id)
1414    }
1415
1416    #[must_use]
1417    pub fn tracks(&self) -> &HashMap<TrackId, IndexedTrack> {
1418        &self.tracks
1419    }
1420
1421    #[must_use]
1422    pub fn resolve(&self) -> TimedSequence {
1423        let mut timed = TimedSequence {
1424            metadata: self.metadata.clone(),
1425            duration_seconds: None,
1426            tracks: HashMap::new(),
1427        };
1428        for (track_id, track) in &self.tracks {
1429            let mut timed_track = TimedTrack::new(track.instrument.clone());
1430            for note in &track.notes {
1431                let start_seconds = self.tempo_map.seconds_at(note.start_index);
1432                let duration_seconds = self.tempo_map.seconds_between(
1433                    note.start_index,
1434                    note.start_index.saturating_add(note.duration_indices),
1435                );
1436                timed_track =
1437                    timed_track.note_at(start_seconds, note.note, duration_seconds, note.velocity);
1438            }
1439            for automation in &track.automation {
1440                let time_seconds = self.tempo_map.seconds_at(automation.index);
1441                timed_track = match &automation.shape {
1442                    IndexedAutomationShape::SetValue { value } => {
1443                        timed_track.automation_at(time_seconds, automation.target.clone(), *value)
1444                    }
1445                    IndexedAutomationShape::LinearRamp { value } => timed_track
1446                        .linear_ramp_to_value_at(time_seconds, automation.target.clone(), *value),
1447                    IndexedAutomationShape::ValueCurve {
1448                        values,
1449                        duration_indices,
1450                    } => timed_track.value_curve_at(
1451                        time_seconds,
1452                        automation.target.clone(),
1453                        values.iter().copied(),
1454                        self.tempo_map.seconds_between(
1455                            automation.index,
1456                            automation.index.saturating_add(*duration_indices),
1457                        ),
1458                    ),
1459                };
1460            }
1461            timed.add_track(track_id.clone(), timed_track);
1462        }
1463        timed
1464    }
1465}