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 #[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 pub fn try_sound_data(&self) -> Result<SequencerSoundData, SequencerValidationError> {
524 self.validate()?;
525 Ok(self.sound_data())
526 }
527
528 #[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 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 #[must_use]
557 pub fn render_offline(&self, sample_rate: u32) -> AudioBuffer {
558 render_timed_sequence_offline(self, None, sample_rate)
559 }
560
561 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 #[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 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 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}