1use crate::drums::PulseDrumGrid;
4use crate::effects::PulseEffect;
5use crate::error::{PulseError, PulseResult};
6use crate::instruments::instrument_by_name;
7use crate::synthesis::PulseSynth;
8use crate::theory::transpose_notes;
9use tunes::composition::{Composition, Tempo};
10use tunes::synthesis::sample::Sample;
11use tunes::track::{AudioEvent, Mixer, Track};
12
13#[derive(Debug, Clone, PartialEq)]
15pub struct PulseNoteEvent {
16 frequencies: Vec<f32>,
17 duration: f32,
18}
19
20impl PulseNoteEvent {
21 pub fn new(frequencies: Vec<f32>, duration: f32) -> PulseResult<Self> {
23 validate_event_frequencies(&frequencies)?;
24 validate_sequence_duration(duration)?;
25 Ok(Self {
26 frequencies,
27 duration,
28 })
29 }
30
31 pub fn frequencies(&self) -> &[f32] {
33 &self.frequencies
34 }
35
36 pub fn duration(&self) -> f32 {
38 self.duration
39 }
40}
41
42#[derive(Debug, Clone, PartialEq)]
44pub struct PulseSequence {
45 notes: Vec<f32>,
46 chords: Vec<Vec<f32>>,
47 durations: Vec<f32>,
48 instrument: String,
49 synth: Option<PulseSynth>,
50 effects: Vec<PulseEffect>,
51 start_at: f32,
52 volume: f32,
53 pan: f32,
54 velocity: f32,
55}
56
57impl Default for PulseSequence {
58 fn default() -> Self {
59 Self::new()
60 }
61}
62
63impl PulseSequence {
64 pub fn new() -> Self {
66 Self {
67 notes: Vec::new(),
68 chords: Vec::new(),
69 durations: Vec::new(),
70 instrument: "electric_piano".to_string(),
71 synth: None,
72 effects: Vec::new(),
73 start_at: 0.0,
74 volume: 1.0,
75 pan: 0.0,
76 velocity: 0.8,
77 }
78 }
79
80 #[must_use]
82 pub fn with_notes(mut self, notes: Vec<f32>) -> Self {
83 self.notes = notes;
84 self.chords.clear();
85 self
86 }
87
88 #[must_use]
90 pub fn with_chords(mut self, chords: Vec<Vec<f32>>) -> Self {
91 self.chords = chords;
92 self.notes.clear();
93 self
94 }
95
96 #[must_use]
98 pub fn with_durations(mut self, durations: Vec<f32>) -> Self {
99 self.durations = durations;
100 self
101 }
102
103 #[must_use]
105 pub fn with_instrument(mut self, instrument: impl Into<String>) -> Self {
106 self.instrument = instrument.into();
107 self
108 }
109
110 #[must_use]
112 pub fn with_synth(mut self, synth: PulseSynth) -> Self {
113 self.synth = Some(synth);
114 self
115 }
116
117 pub fn with_start_at(mut self, start_at: f32) -> PulseResult<Self> {
119 validate_non_negative_sequence_option("at", start_at)?;
120 self.start_at = start_at;
121 Ok(self)
122 }
123
124 pub fn with_volume(mut self, volume: f32) -> PulseResult<Self> {
126 validate_sequence_range("volume", volume, 0.0, 2.0)?;
127 self.volume = volume;
128 Ok(self)
129 }
130
131 pub fn with_pan(mut self, pan: f32) -> PulseResult<Self> {
133 validate_sequence_range("pan", pan, -1.0, 1.0)?;
134 self.pan = pan;
135 Ok(self)
136 }
137
138 pub fn with_velocity(mut self, velocity: f32) -> PulseResult<Self> {
140 validate_sequence_range("velocity", velocity, 0.0, 1.0)?;
141 self.velocity = velocity;
142 Ok(self)
143 }
144
145 #[must_use]
147 pub fn transposed(&self, semitones: i32) -> Self {
148 let mut sequence = self.clone();
149 sequence.notes = transpose_notes(&sequence.notes, semitones);
150 sequence.chords = sequence
151 .chords
152 .iter()
153 .map(|chord| transpose_notes(chord, semitones))
154 .collect();
155 sequence
156 }
157
158 #[must_use]
160 pub fn with_effect(mut self, effect: PulseEffect) -> Self {
161 self.effects.push(effect);
162 self
163 }
164
165 pub fn validate(&self) -> PulseResult<()> {
167 let event_count = self.event_count();
168 if event_count != self.durations.len() {
169 return Err(PulseError::DurationCountMismatch {
170 notes: event_count,
171 durations: self.durations.len(),
172 });
173 }
174
175 for &duration in &self.durations {
176 validate_sequence_duration(duration)?;
177 }
178
179 if !self.chords.is_empty() {
180 for chord in &self.chords {
181 validate_event_frequencies(chord)?;
182 }
183 } else {
184 for &frequency in &self.notes {
185 validate_note_frequency(frequency)?;
186 }
187 }
188
189 if matches!(self.synth, Some(PulseSynth::KarplusStrong(_)))
190 && self.chords.iter().any(|chord| chord.len() > 1)
191 {
192 return Err(PulseError::InvalidSynthOption {
193 synth: "karplus_strong".to_string(),
194 option: "chords".to_string(),
195 value: "polyphonic".to_string(),
196 });
197 }
198
199 validate_non_negative_sequence_option("at", self.start_at)?;
200 validate_sequence_range("volume", self.volume, 0.0, 2.0)?;
201 validate_sequence_range("pan", self.pan, -1.0, 1.0)?;
202 validate_sequence_range("velocity", self.velocity, 0.0, 1.0)?;
203 instrument_by_name(&self.instrument)?;
204 Ok(())
205 }
206
207 pub fn events(&self) -> PulseResult<Vec<PulseNoteEvent>> {
209 self.validate()?;
210 if !self.chords.is_empty() {
211 return self
212 .chords
213 .iter()
214 .cloned()
215 .zip(self.durations.iter().copied())
216 .map(|(frequencies, duration)| PulseNoteEvent::new(frequencies, duration))
217 .collect();
218 }
219
220 self.notes
221 .iter()
222 .copied()
223 .zip(self.durations.iter().copied())
224 .map(|(frequency, duration)| PulseNoteEvent::new(vec![frequency], duration))
225 .collect()
226 }
227
228 pub fn notes(&self) -> &[f32] {
230 &self.notes
231 }
232
233 pub fn chords(&self) -> &[Vec<f32>] {
235 &self.chords
236 }
237
238 pub fn durations(&self) -> &[f32] {
240 &self.durations
241 }
242
243 pub fn instrument(&self) -> &str {
245 &self.instrument
246 }
247
248 pub fn synth(&self) -> Option<&PulseSynth> {
250 self.synth.as_ref()
251 }
252
253 pub fn effects(&self) -> &[PulseEffect] {
255 &self.effects
256 }
257
258 pub fn start_at(&self) -> f32 {
260 self.start_at
261 }
262
263 pub fn volume(&self) -> f32 {
265 self.volume
266 }
267
268 pub fn pan(&self) -> f32 {
270 self.pan
271 }
272
273 pub fn velocity(&self) -> f32 {
275 self.velocity
276 }
277
278 pub fn duration(&self) -> f32 {
280 if let Some(PulseSynth::Granular(synth)) = self.synth() {
281 return self.start_at + synth.duration;
282 }
283 self.start_at + self.durations.iter().sum::<f32>()
284 }
285
286 fn event_count(&self) -> usize {
287 if self.chords.is_empty() {
288 self.notes.len()
289 } else {
290 self.chords.len()
291 }
292 }
293}
294
295fn validate_sequence_duration(duration: f32) -> PulseResult<()> {
296 if duration.is_finite() && duration > 0.0 {
297 Ok(())
298 } else {
299 Err(PulseError::InvalidDuration { duration })
300 }
301}
302
303fn validate_note_frequency(frequency: f32) -> PulseResult<()> {
304 if frequency.is_finite() && frequency > 0.0 {
305 Ok(())
306 } else {
307 Err(PulseError::InvalidFrequency { frequency })
308 }
309}
310
311fn validate_event_frequencies(frequencies: &[f32]) -> PulseResult<()> {
312 if frequencies.is_empty() || frequencies.len() > 8 {
313 return Err(PulseError::InvalidChordVoiceCount {
314 count: frequencies.len(),
315 });
316 }
317
318 for &frequency in frequencies {
319 validate_note_frequency(frequency)?;
320 }
321
322 Ok(())
323}
324
325fn validate_sequence_range(option: &str, value: f32, min: f32, max: f32) -> PulseResult<()> {
326 if value.is_finite() && value >= min && value <= max {
327 Ok(())
328 } else {
329 Err(invalid_sequence_option(option, value))
330 }
331}
332
333fn validate_non_negative_sequence_option(option: &str, value: f32) -> PulseResult<()> {
334 if value.is_finite() && value >= 0.0 {
335 Ok(())
336 } else {
337 Err(invalid_sequence_option(option, value))
338 }
339}
340
341fn invalid_sequence_option(option: &str, value: f32) -> PulseError {
342 PulseError::InvalidSequenceOption {
343 option: option.to_string(),
344 value: value.to_string(),
345 }
346}
347
348#[derive(Debug, Clone, PartialEq)]
350pub struct PulseSampleClip {
351 path: String,
352 playback_rate: f32,
353 gain: f32,
354 pitch_shift: f32,
355 time_stretch: f32,
356 start_at: f32,
357 track_name: Option<String>,
358 volume: f32,
359 pan: f32,
360 effects: Vec<PulseEffect>,
361 slice_range: Option<(f32, f32)>,
362 reverse: bool,
363 loop_for: Option<f32>,
364 normalize: bool,
365 fade_in: f32,
366 fade_out: f32,
367}
368
369impl PulseSampleClip {
370 pub fn new(path: impl Into<String>) -> Self {
372 Self {
373 path: path.into(),
374 playback_rate: 1.0,
375 gain: 1.0,
376 pitch_shift: 0.0,
377 time_stretch: 1.0,
378 start_at: 0.0,
379 track_name: None,
380 volume: 1.0,
381 pan: 0.0,
382 effects: Vec::new(),
383 slice_range: None,
384 reverse: false,
385 loop_for: None,
386 normalize: false,
387 fade_in: 0.0,
388 fade_out: 0.0,
389 }
390 }
391
392 pub fn with_playback_rate(mut self, playback_rate: f32) -> PulseResult<Self> {
394 validate_positive_sample_option("rate", playback_rate)?;
395 self.playback_rate = playback_rate;
396 Ok(self)
397 }
398
399 pub fn with_gain(mut self, gain: f32) -> PulseResult<Self> {
401 if !gain.is_finite() || gain < 0.0 {
402 return Err(invalid_sample_option("gain", gain));
403 }
404 self.gain = gain;
405 Ok(self)
406 }
407
408 pub fn with_pitch_shift(mut self, semitones: f32) -> PulseResult<Self> {
410 if !semitones.is_finite() {
411 return Err(invalid_sample_option("pitch_shift", semitones));
412 }
413 self.pitch_shift = semitones;
414 Ok(self)
415 }
416
417 pub fn with_time_stretch(mut self, factor: f32) -> PulseResult<Self> {
419 validate_positive_sample_option("time_stretch", factor)?;
420 self.time_stretch = factor;
421 Ok(self)
422 }
423
424 pub fn with_start_at(mut self, start_at: f32) -> PulseResult<Self> {
426 if !start_at.is_finite() || start_at < 0.0 {
427 return Err(invalid_sample_option("at", start_at));
428 }
429 self.start_at = start_at;
430 Ok(self)
431 }
432
433 #[must_use]
435 pub fn with_track(mut self, track_name: impl Into<String>) -> Self {
436 self.track_name = Some(track_name.into());
437 self
438 }
439
440 pub fn with_volume(mut self, volume: f32) -> PulseResult<Self> {
442 validate_sample_volume(volume)?;
443 self.volume = volume;
444 Ok(self)
445 }
446
447 pub fn with_pan(mut self, pan: f32) -> PulseResult<Self> {
449 validate_sample_pan(pan)?;
450 self.pan = pan;
451 Ok(self)
452 }
453
454 #[must_use]
456 pub fn with_effect(mut self, effect: PulseEffect) -> Self {
457 self.effects.push(effect);
458 self
459 }
460
461 pub fn with_slice(mut self, start: f32, end: f32) -> PulseResult<Self> {
463 validate_sample_slice(start, end)?;
464 self.slice_range = Some((start, end));
465 Ok(self)
466 }
467
468 pub fn with_reverse(mut self) -> Self {
470 self.reverse = true;
471 self
472 }
473
474 pub fn with_loop_for(mut self, duration: f32) -> PulseResult<Self> {
476 validate_positive_sample_option("loop_for", duration)?;
477 self.loop_for = Some(duration);
478 Ok(self)
479 }
480
481 pub fn with_normalize(mut self) -> Self {
483 self.normalize = true;
484 self
485 }
486
487 pub fn with_fade_in(mut self, duration: f32) -> PulseResult<Self> {
489 validate_non_negative_sample_option("fade_in", duration)?;
490 self.fade_in = duration;
491 Ok(self)
492 }
493
494 pub fn with_fade_out(mut self, duration: f32) -> PulseResult<Self> {
496 validate_non_negative_sample_option("fade_out", duration)?;
497 self.fade_out = duration;
498 Ok(self)
499 }
500
501 pub fn path(&self) -> &str {
503 &self.path
504 }
505
506 pub fn playback_rate(&self) -> f32 {
508 self.playback_rate
509 }
510
511 pub fn start_at(&self) -> f32 {
513 self.start_at
514 }
515
516 pub fn track_name(&self) -> Option<&str> {
518 self.track_name.as_deref()
519 }
520
521 pub fn volume(&self) -> f32 {
523 self.volume
524 }
525
526 pub fn pan(&self) -> f32 {
528 self.pan
529 }
530
531 pub fn effects(&self) -> &[PulseEffect] {
533 &self.effects
534 }
535
536 pub fn slice_range(&self) -> Option<(f32, f32)> {
538 self.slice_range
539 }
540
541 pub fn reverse(&self) -> bool {
543 self.reverse
544 }
545
546 pub fn loop_for(&self) -> Option<f32> {
548 self.loop_for
549 }
550
551 pub fn normalize(&self) -> bool {
553 self.normalize
554 }
555
556 pub fn fade_in(&self) -> f32 {
558 self.fade_in
559 }
560
561 pub fn fade_out(&self) -> f32 {
563 self.fade_out
564 }
565
566 pub fn duration(&self) -> PulseResult<f32> {
568 if let Ok(duration) = self.metadata_duration() {
569 return Ok(duration);
570 }
571
572 let sample = self.transformed_sample()?;
573 Ok(self.start_at + sample.duration / self.playback_rate)
574 }
575
576 fn metadata_duration(&self) -> PulseResult<f32> {
577 validate_sample_timing_options(self)?;
578
579 let source_duration = wav_duration_seconds(&self.path)?;
580 let mut duration = if let Some((start, end)) = self.slice_range {
581 if end > source_duration {
582 return Err(PulseError::SampleLoadFailed {
583 message: format!("slice end {end} exceeds sample duration {source_duration}"),
584 });
585 }
586 end - start
587 } else {
588 source_duration
589 };
590
591 if let Some(loop_for) = self.loop_for {
592 validate_positive_sample_option("loop_for", loop_for)?;
593 duration = loop_for;
594 }
595
596 if (self.time_stretch - 1.0).abs() >= 0.01 {
597 duration *= self.time_stretch;
598 }
599
600 Ok(self.start_at + duration / self.playback_rate)
601 }
602
603 fn transformed_sample(&self) -> PulseResult<Sample> {
604 validate_sample_timing_options(self)?;
605 validate_sample_volume(self.volume)?;
606 validate_sample_pan(self.pan)?;
607 validate_non_negative_sample_option("fade_in", self.fade_in)?;
608 validate_non_negative_sample_option("fade_out", self.fade_out)?;
609 validate_non_negative_sample_option("gain", self.gain)?;
610 validate_finite_sample_option("pitch_shift", self.pitch_shift)?;
611
612 let mut sample =
613 Sample::from_file(&self.path).map_err(|error| PulseError::SampleLoadFailed {
614 message: error.to_string(),
615 })?;
616
617 if let Some((start, end)) = self.slice_range {
618 sample = sample
619 .slice(start, end)
620 .map_err(|error| PulseError::SampleLoadFailed {
621 message: error.to_string(),
622 })?;
623 }
624 if self.reverse {
625 sample = sample.reverse();
626 }
627 if let Some(duration) = self.loop_for {
628 validate_positive_sample_option("loop_for", duration)?;
629 sample = loop_sample_for_duration(&sample, duration)?;
630 }
631 if self.normalize {
632 sample = sample.normalize();
633 }
634 if self.fade_in > 0.0 {
635 sample = sample.with_fade_in(self.fade_in);
636 }
637 if self.fade_out > 0.0 {
638 sample = sample.with_fade_out(self.fade_out);
639 }
640 if self.pitch_shift.abs() >= 0.01 {
641 sample = sample.pitch_shift(self.pitch_shift);
642 }
643 if (self.time_stretch - 1.0).abs() >= 0.01 {
646 sample = sample.time_stretch(self.time_stretch);
647 }
648 if (self.gain - 1.0).abs() >= 0.001 {
649 sample = sample.with_gain(self.gain);
650 }
651
652 Ok(sample)
653 }
654}
655
656fn loop_sample_for_duration(sample: &Sample, duration: f32) -> PulseResult<Sample> {
657 validate_positive_sample_option("loop_for", duration)?;
658 if sample.duration <= 0.0 || !sample.duration.is_finite() {
659 return Err(PulseError::SampleLoadFailed {
660 message: "sample duration must be positive before loop_for".to_string(),
661 });
662 }
663
664 let sample_rate = sample.sample_rate();
665 let frame_count = sample_frame_count("loop_for", duration, sample_rate)?;
666 if frame_count == 0 {
667 return Err(invalid_sample_option("loop_for", duration));
668 }
669
670 let mut data = Vec::with_capacity(frame_count);
671 for frame in 0..frame_count {
672 let time = frame as f32 / sample_rate as f32;
673 let source_time = time % sample.duration;
674 let (left, right) = sample.sample_at_interpolated(source_time, 1.0);
675 data.push((left + right) * 0.5);
676 }
677
678 Ok(Sample::from_mono(data, sample_rate))
679}
680
681fn sample_frame_count(option: &str, duration: f32, sample_rate: u32) -> PulseResult<usize> {
682 let frames = f64::from(duration) * f64::from(sample_rate);
683 let max_frames = isize::MAX as usize / std::mem::size_of::<f32>();
684 if !frames.is_finite() || frames <= 0.0 || frames > max_frames as f64 {
685 return Err(invalid_sample_option(option, duration));
686 }
687
688 Ok(frames.round() as usize)
689}
690
691fn wav_duration_seconds(path: &str) -> PulseResult<f32> {
692 let reader = hound::WavReader::open(path).map_err(|error| PulseError::SampleLoadFailed {
693 message: error.to_string(),
694 })?;
695 let spec = reader.spec();
696 if spec.sample_rate == 0 {
697 return Err(PulseError::SampleLoadFailed {
698 message: "wav sample rate must be positive".to_string(),
699 });
700 }
701
702 Ok(reader.duration() as f32 / spec.sample_rate as f32)
703}
704
705fn validate_sample_timing_options(sample_clip: &PulseSampleClip) -> PulseResult<()> {
706 if sample_clip.path.trim().is_empty() {
707 return Err(PulseError::InvalidSampleOption {
708 option: "path".to_string(),
709 value: "empty".to_string(),
710 });
711 }
712 validate_positive_sample_option("rate", sample_clip.playback_rate)?;
713 validate_positive_sample_option("time_stretch", sample_clip.time_stretch)?;
714 if !sample_clip.start_at.is_finite() || sample_clip.start_at < 0.0 {
715 return Err(invalid_sample_option("at", sample_clip.start_at));
716 }
717 if let Some((start, end)) = sample_clip.slice_range {
718 validate_sample_slice(start, end)?;
719 }
720 Ok(())
721}
722
723fn validate_positive_sample_option(option: &str, value: f32) -> PulseResult<()> {
724 if value.is_finite() && value > 0.0 {
725 Ok(())
726 } else {
727 Err(invalid_sample_option(option, value))
728 }
729}
730
731fn validate_non_negative_sample_option(option: &str, value: f32) -> PulseResult<()> {
732 if value.is_finite() && value >= 0.0 {
733 Ok(())
734 } else {
735 Err(invalid_sample_option(option, value))
736 }
737}
738
739fn validate_finite_sample_option(option: &str, value: f32) -> PulseResult<()> {
740 if value.is_finite() {
741 Ok(())
742 } else {
743 Err(invalid_sample_option(option, value))
744 }
745}
746
747fn validate_sample_volume(value: f32) -> PulseResult<()> {
748 if value.is_finite() && (0.0..=2.0).contains(&value) {
749 Ok(())
750 } else {
751 Err(invalid_sample_option("volume", value))
752 }
753}
754
755fn validate_sample_pan(value: f32) -> PulseResult<()> {
756 if value.is_finite() && (-1.0..=1.0).contains(&value) {
757 Ok(())
758 } else {
759 Err(invalid_sample_option("pan", value))
760 }
761}
762
763fn invalid_sample_option(option: &str, value: f32) -> PulseError {
764 PulseError::InvalidSampleOption {
765 option: option.to_string(),
766 value: value.to_string(),
767 }
768}
769
770fn validate_sample_slice(start: f32, end: f32) -> PulseResult<()> {
771 if start.is_finite() && end.is_finite() && start >= 0.0 && end > start {
772 Ok(())
773 } else {
774 Err(PulseError::InvalidSampleOption {
775 option: "slice".to_string(),
776 value: format!("{start}..{end}"),
777 })
778 }
779}
780
781#[derive(Debug, Clone)]
783pub struct PulseMidiClip {
784 mixer: Mixer,
785 start_at: f32,
786 track_name: Option<String>,
787 volume: f32,
788 pan: Option<f32>,
789 repeat_times: usize,
790 effects: Vec<PulseEffect>,
791}
792
793impl PartialEq for PulseMidiClip {
794 fn eq(&self, other: &Self) -> bool {
795 self.start_at == other.start_at
796 && self.track_name == other.track_name
797 && self.volume == other.volume
798 && self.pan == other.pan
799 && self.repeat_times == other.repeat_times
800 && self.effects == other.effects
801 && self.mixer.total_duration() == other.mixer.total_duration()
802 }
803}
804
805impl PulseMidiClip {
806 pub fn new(mixer: Mixer) -> Self {
808 Self {
809 mixer,
810 start_at: 0.0,
811 track_name: None,
812 volume: 1.0,
813 pan: None,
814 repeat_times: 1,
815 effects: Vec::new(),
816 }
817 }
818
819 pub fn with_start_at(mut self, start_at: f32) -> PulseResult<Self> {
821 validate_midi_clip_non_negative("at", start_at)?;
822 self.start_at = start_at;
823 Ok(self)
824 }
825
826 #[must_use]
828 pub fn with_track(mut self, track_name: impl Into<String>) -> Self {
829 self.track_name = Some(track_name.into());
830 self
831 }
832
833 pub fn with_volume(mut self, volume: f32) -> PulseResult<Self> {
835 validate_midi_clip_volume(volume)?;
836 self.volume = volume;
837 Ok(self)
838 }
839
840 pub fn with_pan(mut self, pan: f32) -> PulseResult<Self> {
842 validate_midi_clip_pan(pan)?;
843 self.pan = Some(pan);
844 Ok(self)
845 }
846
847 pub fn with_repeat_times(mut self, repeat_times: usize) -> PulseResult<Self> {
849 if repeat_times == 0 {
850 return Err(PulseError::InvalidRepeatTimes { repeat_times });
851 }
852 self.repeat_times = repeat_times;
853 Ok(self)
854 }
855
856 #[must_use]
858 pub fn with_effect(mut self, effect: PulseEffect) -> Self {
859 self.effects.push(effect);
860 self
861 }
862
863 pub fn start_at(&self) -> f32 {
865 self.start_at
866 }
867
868 pub fn track_name(&self) -> Option<&str> {
870 self.track_name.as_deref()
871 }
872
873 pub fn volume(&self) -> f32 {
875 self.volume
876 }
877
878 pub fn pan(&self) -> Option<f32> {
880 self.pan
881 }
882
883 pub fn repeat_times(&self) -> usize {
885 self.repeat_times
886 }
887
888 pub fn effects(&self) -> &[PulseEffect] {
890 &self.effects
891 }
892
893 pub fn duration(&self) -> PulseResult<f32> {
895 self.validate()?;
896 let repeated_duration =
897 repeated_timeline_duration(self.mixer.total_duration(), self.repeat_times)?;
898 timeline_offset_duration(self.start_at, repeated_duration, self.repeat_times)
899 }
900
901 fn validate(&self) -> PulseResult<()> {
902 validate_midi_clip_non_negative("at", self.start_at)?;
903 validate_midi_clip_volume(self.volume)?;
904 if let Some(pan) = self.pan {
905 validate_midi_clip_pan(pan)?;
906 }
907 if self.repeat_times == 0 {
908 return Err(PulseError::InvalidRepeatTimes {
909 repeat_times: self.repeat_times,
910 });
911 }
912 repeated_timeline_duration(self.mixer.total_duration(), self.repeat_times)?;
913 Ok(())
914 }
915
916 fn arranged_tracks(&self, start_at: f32, default_name: &str) -> PulseResult<Vec<Track>> {
917 self.validate()?;
918
919 let source_tracks = self.mixer.all_tracks();
920 let source_track_count = source_tracks.len();
921 let source_duration = self.mixer.total_duration();
922 let mut arranged = Vec::with_capacity(source_track_count);
923
924 for (track_index, source_track) in source_tracks.into_iter().enumerate() {
925 let event_capacity =
926 midi_repeat_event_capacity(source_track.events.len(), self.repeat_times)?;
927 let mut events = Vec::with_capacity(event_capacity);
928 let should_expand_events =
929 repeat_expansion_capacity(source_track.events.len(), self.repeat_times)? > 0;
930
931 if should_expand_events {
932 for repeat_index in 0..self.repeat_times {
933 let repeat_offset =
934 start_at + self.start_at + source_duration * repeat_index as f32;
935 for event in &source_track.events {
936 let mut event = event.clone();
937 offset_audio_event(&mut event, repeat_offset);
938 events.push(event);
939 }
940 }
941 }
942 events.sort_by(|left, right| {
943 left.start_time()
944 .partial_cmp(&right.start_time())
945 .unwrap_or(std::cmp::Ordering::Equal)
946 });
947
948 let mut track = Track::new();
949 track.events = events;
950 track.name =
951 Some(self.arranged_track_name(default_name, track_index, source_track_count));
952 track.midi_program = source_track.midi_program;
953 track.volume = (source_track.volume * self.volume).clamp(0.0, 2.0);
954 track.pan = source_track.pan;
955 track.filter = source_track.filter;
956 track.effects = source_track.effects.clone();
957 track.modulation = source_track.modulation.clone();
958 if let Some(pan) = self.pan {
959 track.pan = pan;
960 }
961 for effect in &self.effects {
962 effect.apply_to_track(&mut track);
963 }
964 arranged.push(track);
965 }
966
967 Ok(arranged)
968 }
969
970 fn arranged_track_name(
971 &self,
972 default_name: &str,
973 track_index: usize,
974 source_track_count: usize,
975 ) -> String {
976 let base_name = self
977 .track_name
978 .as_deref()
979 .unwrap_or(default_name)
980 .trim()
981 .to_string();
982 let base_name = if base_name.is_empty() {
983 default_name.to_string()
984 } else {
985 base_name
986 };
987
988 if source_track_count == 1 {
989 base_name
990 } else {
991 format!("{base_name}_{track_index}")
992 }
993 }
994}
995
996fn midi_repeat_event_capacity(event_count: usize, repeat_times: usize) -> PulseResult<usize> {
997 let capacity = repeat_expansion_capacity(event_count, repeat_times)?;
998 let max_events = isize::MAX as usize / std::mem::size_of::<AudioEvent>();
999 if capacity > max_events {
1000 return Err(PulseError::InvalidRepeatTimes { repeat_times });
1001 }
1002 Ok(capacity)
1003}
1004
1005fn repeat_expansion_capacity(item_count: usize, repeat_times: usize) -> PulseResult<usize> {
1006 let capacity = item_count
1007 .checked_mul(repeat_times)
1008 .ok_or(PulseError::InvalidRepeatTimes { repeat_times })?;
1009 if capacity > isize::MAX as usize {
1010 return Err(PulseError::InvalidRepeatTimes { repeat_times });
1011 }
1012 Ok(capacity)
1013}
1014
1015fn repeated_timeline_duration(duration: f32, repeat_times: usize) -> PulseResult<f32> {
1016 if repeat_times == 0 {
1017 return Err(PulseError::InvalidRepeatTimes { repeat_times });
1018 }
1019
1020 let repeated = duration * repeat_times as f32;
1021 if duration.is_finite() && repeated.is_finite() {
1022 Ok(repeated)
1023 } else {
1024 Err(PulseError::InvalidRepeatTimes { repeat_times })
1025 }
1026}
1027
1028fn timeline_offset_duration(start_at: f32, duration: f32, repeat_times: usize) -> PulseResult<f32> {
1029 let total = start_at + duration;
1030 if start_at.is_finite() && duration.is_finite() && total.is_finite() {
1031 Ok(total)
1032 } else {
1033 Err(PulseError::InvalidRepeatTimes { repeat_times })
1034 }
1035}
1036
1037fn offset_audio_event(event: &mut AudioEvent, offset: f32) {
1038 match event {
1039 AudioEvent::Note(note) => note.start_time += offset,
1040 AudioEvent::Drum(drum) => drum.start_time += offset,
1041 AudioEvent::Sample(sample) => sample.start_time += offset,
1042 AudioEvent::TempoChange(tempo) => tempo.start_time += offset,
1043 AudioEvent::TimeSignature(time_signature) => time_signature.start_time += offset,
1044 AudioEvent::KeySignature(key_signature) => key_signature.start_time += offset,
1045 }
1046}
1047
1048fn validate_midi_clip_non_negative(option: &str, value: f32) -> PulseResult<()> {
1049 if value.is_finite() && value >= 0.0 {
1050 Ok(())
1051 } else {
1052 Err(invalid_midi_clip_option(option, value))
1053 }
1054}
1055
1056fn validate_midi_clip_volume(value: f32) -> PulseResult<()> {
1057 if value.is_finite() && (0.0..=2.0).contains(&value) {
1058 Ok(())
1059 } else {
1060 Err(invalid_midi_clip_option("volume", value))
1061 }
1062}
1063
1064fn validate_midi_clip_pan(value: f32) -> PulseResult<()> {
1065 if value.is_finite() && (-1.0..=1.0).contains(&value) {
1066 Ok(())
1067 } else {
1068 Err(invalid_midi_clip_option("pan", value))
1069 }
1070}
1071
1072fn invalid_midi_clip_option(option: &str, value: f32) -> PulseError {
1073 PulseError::InvalidMidiClipOption {
1074 option: option.to_string(),
1075 value: value.to_string(),
1076 }
1077}
1078
1079#[derive(Debug, Clone, PartialEq)]
1081pub struct PulsePhrase {
1082 sequences: Vec<PulseSequence>,
1083 drum_grids: Vec<PulseDrumGrid>,
1084 sample_clips: Vec<PulseSampleClip>,
1085 midi_clips: Vec<PulseMidiClip>,
1086 repeat_times: usize,
1087}
1088
1089impl Default for PulsePhrase {
1090 fn default() -> Self {
1091 Self::new()
1092 }
1093}
1094
1095impl PulsePhrase {
1096 pub fn new() -> Self {
1098 Self {
1099 sequences: Vec::new(),
1100 drum_grids: Vec::new(),
1101 sample_clips: Vec::new(),
1102 midi_clips: Vec::new(),
1103 repeat_times: 1,
1104 }
1105 }
1106
1107 #[must_use]
1109 pub fn add_sequence(mut self, sequence: PulseSequence) -> Self {
1110 self.sequences.push(sequence);
1111 self
1112 }
1113
1114 #[must_use]
1116 pub fn add_drum_grid(mut self, grid: PulseDrumGrid) -> Self {
1117 self.drum_grids.push(grid);
1118 self
1119 }
1120
1121 #[must_use]
1123 pub fn add_sample_clip(mut self, sample_clip: PulseSampleClip) -> Self {
1124 self.sample_clips.push(sample_clip);
1125 self
1126 }
1127
1128 #[must_use]
1130 pub fn add_midi_clip(mut self, midi_clip: PulseMidiClip) -> Self {
1131 self.midi_clips.push(midi_clip);
1132 self
1133 }
1134
1135 pub fn with_repeat_times(mut self, repeat_times: usize) -> PulseResult<Self> {
1137 if repeat_times == 0 {
1138 return Err(PulseError::InvalidRepeatTimes { repeat_times });
1139 }
1140 self.repeat_times = repeat_times;
1141 Ok(self)
1142 }
1143
1144 pub fn duration(&self) -> PulseResult<f32> {
1146 let sequence_duration = self
1147 .sequences
1148 .iter()
1149 .map(PulseSequence::duration)
1150 .fold(0.0, f32::max);
1151 let drum_duration = self
1152 .drum_grids
1153 .iter()
1154 .map(PulseDrumGrid::duration)
1155 .fold(0.0, f32::max);
1156 let mut sample_duration = 0.0_f32;
1157 for sample_clip in &self.sample_clips {
1158 sample_duration = sample_duration.max(sample_clip.duration()?);
1159 }
1160 let mut midi_duration = 0.0_f32;
1161 for midi_clip in &self.midi_clips {
1162 midi_duration = midi_duration.max(midi_clip.duration()?);
1163 }
1164 Ok(sequence_duration
1165 .max(drum_duration)
1166 .max(sample_duration)
1167 .max(midi_duration))
1168 }
1169
1170 pub fn total_duration(&self) -> PulseResult<f32> {
1172 repeated_timeline_duration(self.duration()?, self.repeat_times)
1173 }
1174
1175 pub fn sequences(&self) -> &[PulseSequence] {
1177 &self.sequences
1178 }
1179
1180 pub fn drum_grids(&self) -> &[PulseDrumGrid] {
1182 &self.drum_grids
1183 }
1184
1185 pub fn sample_clips(&self) -> &[PulseSampleClip] {
1187 &self.sample_clips
1188 }
1189
1190 pub fn midi_clips(&self) -> &[PulseMidiClip] {
1192 &self.midi_clips
1193 }
1194
1195 pub fn repeat_times(&self) -> usize {
1197 self.repeat_times
1198 }
1199}
1200
1201#[derive(Debug, Clone, PartialEq)]
1203pub struct PulseSong {
1204 tempo: f32,
1205 sequences: Vec<PulseSequence>,
1206 drum_grids: Vec<PulseDrumGrid>,
1207 phrases: Vec<PulsePhrase>,
1208 sample_clips: Vec<PulseSampleClip>,
1209 midi_clips: Vec<PulseMidiClip>,
1210 master_effects: Vec<PulseEffect>,
1211}
1212
1213impl Default for PulseSong {
1214 fn default() -> Self {
1215 Self::new()
1216 }
1217}
1218
1219impl PulseSong {
1220 pub fn new() -> Self {
1222 Self {
1223 tempo: 120.0,
1224 sequences: Vec::new(),
1225 drum_grids: Vec::new(),
1226 phrases: Vec::new(),
1227 sample_clips: Vec::new(),
1228 midi_clips: Vec::new(),
1229 master_effects: Vec::new(),
1230 }
1231 }
1232
1233 pub fn with_tempo(mut self, bpm: f32) -> PulseResult<Self> {
1235 if !bpm.is_finite() || bpm <= 0.0 {
1236 return Err(PulseError::InvalidTempo { bpm });
1237 }
1238 self.tempo = bpm;
1239 Ok(self)
1240 }
1241
1242 #[must_use]
1244 pub fn add_sequence(mut self, sequence: PulseSequence) -> Self {
1245 self.sequences.push(sequence);
1246 self
1247 }
1248
1249 #[must_use]
1251 pub fn add_drum_grid(mut self, grid: PulseDrumGrid) -> Self {
1252 self.drum_grids.push(grid);
1253 self
1254 }
1255
1256 #[must_use]
1258 pub fn add_phrase(mut self, phrase: PulsePhrase) -> Self {
1259 self.phrases.push(phrase);
1260 self
1261 }
1262
1263 #[must_use]
1265 pub fn add_sample_clip(mut self, sample_clip: PulseSampleClip) -> Self {
1266 self.sample_clips.push(sample_clip);
1267 self
1268 }
1269
1270 #[must_use]
1272 pub fn add_midi_clip(mut self, midi_clip: PulseMidiClip) -> Self {
1273 self.midi_clips.push(midi_clip);
1274 self
1275 }
1276
1277 #[must_use]
1279 pub fn with_master_effect(mut self, effect: PulseEffect) -> Self {
1280 self.master_effects.push(effect);
1281 self
1282 }
1283
1284 pub fn to_tunes_composition(&self) -> PulseResult<Composition> {
1286 let mut composition = Composition::new(Tempo::new(self.tempo));
1287
1288 for (index, sequence) in self.sequences.iter().enumerate() {
1289 let track_name = format!("sequence_{index}");
1290 apply_sequence_to_composition(&mut composition, &track_name, sequence, 0.0)?;
1291 }
1292
1293 for grid in &self.drum_grids {
1294 grid.apply_to_composition(&mut composition)?;
1295 }
1296
1297 for (index, sample_clip) in self.sample_clips.iter().enumerate() {
1298 let track_name = format!("sample_{index}");
1299 apply_sample_clip_to_composition(&mut composition, &track_name, sample_clip, 0.0)?;
1300 }
1301
1302 let mut phrase_start = 0.0;
1303 for (phrase_index, phrase) in self.phrases.iter().enumerate() {
1304 let phrase_duration = phrase.duration()?;
1305 let repeat_times = phrase.repeat_times();
1306 let phrase_total_duration = repeated_timeline_duration(phrase_duration, repeat_times)?;
1307 let arrangement_count =
1308 phrase.sequences().len() + phrase.drum_grids().len() + phrase.sample_clips().len();
1309 let should_expand_phrase =
1310 repeat_expansion_capacity(arrangement_count, repeat_times)? > 0;
1311
1312 if should_expand_phrase {
1313 for repeat_index in 0..repeat_times {
1314 let repeat_start = phrase_start + phrase_duration * repeat_index as f32;
1315
1316 for (sequence_index, sequence) in phrase.sequences().iter().enumerate() {
1317 let track_name = format!("phrase_{phrase_index}_sequence_{sequence_index}");
1318 apply_sequence_to_composition(
1319 &mut composition,
1320 &track_name,
1321 sequence,
1322 repeat_start,
1323 )?;
1324 }
1325
1326 for grid in phrase.drum_grids() {
1327 grid.apply_to_composition_at(&mut composition, repeat_start)?;
1328 }
1329
1330 for (sample_index, sample_clip) in phrase.sample_clips().iter().enumerate() {
1331 let track_name = format!("phrase_{phrase_index}_sample_{sample_index}");
1332 apply_sample_clip_to_composition(
1333 &mut composition,
1334 &track_name,
1335 sample_clip,
1336 repeat_start,
1337 )?;
1338 }
1339 }
1340 }
1341 phrase_start =
1342 timeline_offset_duration(phrase_start, phrase_total_duration, repeat_times)?;
1343 }
1344
1345 Ok(composition)
1346 }
1347
1348 pub fn to_mixer(&self) -> PulseResult<Mixer> {
1350 let mut mixer = self.to_arranged_mixer()?;
1351 for effect in &self.master_effects {
1352 effect.apply_to_master(&mut mixer)?;
1353 }
1354 Ok(mixer)
1355 }
1356
1357 pub(crate) fn to_arranged_mixer(&self) -> PulseResult<Mixer> {
1359 let mut mixer = self.to_tunes_composition()?.into_mixer();
1360 self.apply_midi_clips_to_mixer(&mut mixer)?;
1361 Ok(mixer)
1362 }
1363
1364 fn apply_midi_clips_to_mixer(&self, mixer: &mut Mixer) -> PulseResult<()> {
1365 let mut next_track_id = next_mixer_track_id(mixer);
1366
1367 for (index, midi_clip) in self.midi_clips.iter().enumerate() {
1368 append_midi_clip_to_mixer(
1369 mixer,
1370 midi_clip,
1371 0.0,
1372 &format!("midi_{index}"),
1373 &mut next_track_id,
1374 )?;
1375 }
1376
1377 let mut phrase_start = 0.0;
1378 for (phrase_index, phrase) in self.phrases.iter().enumerate() {
1379 let phrase_duration = phrase.duration()?;
1380 let repeat_times = phrase.repeat_times();
1381 let phrase_total_duration = repeated_timeline_duration(phrase_duration, repeat_times)?;
1382 let should_expand_midi =
1383 repeat_expansion_capacity(phrase.midi_clips().len(), repeat_times)? > 0;
1384
1385 if should_expand_midi {
1386 for repeat_index in 0..repeat_times {
1387 let repeat_start = phrase_start + phrase_duration * repeat_index as f32;
1388 for (midi_index, midi_clip) in phrase.midi_clips().iter().enumerate() {
1389 append_midi_clip_to_mixer(
1390 mixer,
1391 midi_clip,
1392 repeat_start,
1393 &format!("phrase_{phrase_index}_midi_{midi_index}"),
1394 &mut next_track_id,
1395 )?;
1396 }
1397 }
1398 }
1399 phrase_start =
1400 timeline_offset_duration(phrase_start, phrase_total_duration, repeat_times)?;
1401 }
1402
1403 Ok(())
1404 }
1405
1406 pub fn tempo(&self) -> f32 {
1408 self.tempo
1409 }
1410
1411 pub fn sequences(&self) -> &[PulseSequence] {
1413 &self.sequences
1414 }
1415
1416 pub fn drum_grids(&self) -> &[PulseDrumGrid] {
1418 &self.drum_grids
1419 }
1420
1421 pub fn phrases(&self) -> &[PulsePhrase] {
1423 &self.phrases
1424 }
1425
1426 pub fn sample_clips(&self) -> &[PulseSampleClip] {
1428 &self.sample_clips
1429 }
1430
1431 pub fn midi_clips(&self) -> &[PulseMidiClip] {
1433 &self.midi_clips
1434 }
1435
1436 pub fn master_effects(&self) -> &[PulseEffect] {
1438 &self.master_effects
1439 }
1440}
1441
1442fn apply_sequence_to_composition(
1443 composition: &mut Composition,
1444 track_name: &str,
1445 sequence: &PulseSequence,
1446 start_at: f32,
1447) -> PulseResult<()> {
1448 sequence.validate()?;
1449 let instrument = instrument_by_name(sequence.instrument())?;
1450 let mut builder = composition
1451 .instrument(track_name, &instrument)
1452 .at(start_at + sequence.start_at())
1453 .volume(sequence.volume())
1454 .pan(sequence.pan())
1455 .velocity(sequence.velocity());
1456 let events = sequence.events()?;
1457
1458 match sequence.synth() {
1459 Some(PulseSynth::KarplusStrong(synth)) => {
1460 for event in &events {
1461 let sample = synth.to_sample(event.frequencies()[0], event.duration(), 44_100)?;
1462 builder = builder.play_sample(&sample, 1.0);
1463 }
1464 }
1465 Some(PulseSynth::Granular(synth)) => {
1466 tunes::synthesis::sample::Sample::from_file(&synth.source).map_err(|error| {
1467 PulseError::InvalidSynthOption {
1468 synth: "granular".to_string(),
1469 option: "source".to_string(),
1470 value: error.to_string(),
1471 }
1472 })?;
1473 builder = builder.granular(&synth.source, synth.params.clone(), synth.duration);
1474 }
1475 synth => {
1476 if let Some(synth) = synth {
1477 builder = synth.apply_to_track_builder(builder);
1478 }
1479
1480 for event in &events {
1481 builder = builder.note(event.frequencies(), event.duration());
1482 }
1483 }
1484 }
1485
1486 for effect in sequence.effects() {
1487 builder = effect.apply_to_track_builder(builder);
1488 }
1489 let _ = builder;
1490
1491 Ok(())
1492}
1493
1494fn append_midi_clip_to_mixer(
1495 mixer: &mut Mixer,
1496 midi_clip: &PulseMidiClip,
1497 start_at: f32,
1498 default_name: &str,
1499 next_track_id: &mut u32,
1500) -> PulseResult<()> {
1501 for mut track in midi_clip.arranged_tracks(start_at, default_name)? {
1502 track.id = *next_track_id;
1503 *next_track_id = (*next_track_id).saturating_add(1);
1504 track.bus_id = 0;
1505 mixer.add_track(track);
1506 }
1507 Ok(())
1508}
1509
1510fn next_mixer_track_id(mixer: &Mixer) -> u32 {
1511 mixer
1512 .all_tracks()
1513 .into_iter()
1514 .map(|track| track.id)
1515 .max()
1516 .unwrap_or(0)
1517 .saturating_add(1)
1518}
1519
1520fn apply_sample_clip_to_composition(
1521 composition: &mut Composition,
1522 track_name: &str,
1523 sample_clip: &PulseSampleClip,
1524 start_at: f32,
1525) -> PulseResult<()> {
1526 let sample = sample_clip.transformed_sample()?;
1527 let effective_track_name = sample_clip.track_name().unwrap_or(track_name);
1528 let mut builder = composition
1529 .track(effective_track_name)
1530 .at(start_at + sample_clip.start_at())
1531 .volume(sample_clip.volume())
1532 .pan(sample_clip.pan())
1533 .play_sample(&sample, sample_clip.playback_rate());
1534 for effect in sample_clip.effects() {
1535 builder = effect.apply_to_track_builder(builder);
1536 }
1537 let _ = builder;
1538 Ok(())
1539}
1540
1541#[cfg(test)]
1542mod tests {
1543 use super::*;
1544 use crate::theory::parse_note_frequency;
1545 use tunes::track::AudioEvent;
1546
1547 fn test_delay_effect() -> crate::effects::PulseEffect {
1548 crate::effects::effect_from_options("delay", crate::effects::EffectOptions::Default)
1549 .expect("default delay should parse")
1550 }
1551
1552 fn test_eq_effect() -> crate::effects::PulseEffect {
1553 crate::effects::effect_from_options("eq", crate::effects::EffectOptions::Default)
1554 .expect("default eq should parse")
1555 }
1556
1557 fn test_filter_effect() -> crate::effects::PulseEffect {
1558 crate::effects::effect_from_options("filter", crate::effects::EffectOptions::Default)
1559 .expect("default filter should parse")
1560 }
1561
1562 #[test]
1563 fn sequence_validates_duration_count_and_positive_values() {
1564 let c4 = parse_note_frequency("C4").unwrap();
1565 let sequence = PulseSequence::new()
1566 .with_notes(vec![c4])
1567 .with_durations(vec![0.5])
1568 .with_instrument("electric_piano");
1569
1570 assert!(sequence.validate().is_ok());
1571
1572 let mismatch = PulseSequence::new()
1573 .with_notes(vec![c4, c4])
1574 .with_durations(vec![0.5])
1575 .with_instrument("electric_piano");
1576 assert_eq!(
1577 mismatch
1578 .validate()
1579 .expect_err("mismatch should fail")
1580 .to_string(),
1581 "duration count mismatch: 2 notes and 1 durations"
1582 );
1583
1584 let invalid_duration = PulseSequence::new()
1585 .with_notes(vec![c4])
1586 .with_durations(vec![0.0])
1587 .with_instrument("electric_piano");
1588 assert_eq!(
1589 invalid_duration
1590 .validate()
1591 .expect_err("zero duration should fail")
1592 .to_string(),
1593 "invalid duration: 0"
1594 );
1595 }
1596
1597 #[test]
1598 fn sequence_transposes_without_mutating_original() {
1599 let c4 = parse_note_frequency("C4").unwrap();
1600 let sequence = PulseSequence::new()
1601 .with_notes(vec![c4])
1602 .with_durations(vec![0.5])
1603 .with_instrument("electric_piano");
1604
1605 let transposed = sequence.transposed(12);
1606
1607 assert!(transposed.notes()[0] > sequence.notes()[0] * 1.99);
1608 assert!(sequence.notes()[0] < transposed.notes()[0]);
1609 }
1610
1611 #[test]
1612 fn sequence_can_render_polyphonic_chords_with_mix_controls() {
1613 let c4 = parse_note_frequency("C4").unwrap();
1614 let e4 = parse_note_frequency("E4").unwrap();
1615 let g4 = parse_note_frequency("G4").unwrap();
1616 let f4 = parse_note_frequency("F4").unwrap();
1617 let a4 = parse_note_frequency("A4").unwrap();
1618 let c5 = parse_note_frequency("C5").unwrap();
1619
1620 let sequence = PulseSequence::new()
1621 .with_chords(vec![vec![c4, e4, g4], vec![f4, a4, c5]])
1622 .with_durations(vec![0.5, 0.5])
1623 .with_instrument("electric_piano")
1624 .with_start_at(0.25)
1625 .unwrap()
1626 .with_volume(0.7)
1627 .unwrap()
1628 .with_pan(-0.25)
1629 .unwrap()
1630 .with_velocity(0.6)
1631 .unwrap();
1632
1633 let mixer = PulseSong::new().add_sequence(sequence).to_mixer().unwrap();
1634 let tracks = mixer.all_tracks();
1635 assert_eq!(tracks.len(), 1);
1636 assert_eq!(tracks[0].volume, 0.7);
1637 assert_eq!(tracks[0].pan, -0.25);
1638
1639 let AudioEvent::Note(note) = &tracks[0].events[0] else {
1640 panic!("expected note event");
1641 };
1642
1643 assert_eq!(note.start_time, 0.25);
1644 assert_eq!(note.duration, 0.5);
1645 assert_eq!(note.num_freqs, 3);
1646 assert_eq!(note.frequencies[0], c4);
1647 assert_eq!(note.frequencies[1], e4);
1648 assert_eq!(note.frequencies[2], g4);
1649 assert_eq!(note.velocity, 0.6);
1650 }
1651
1652 #[test]
1653 fn song_rejects_invalid_tempo() {
1654 let error = PulseSong::new()
1655 .with_tempo(0.0)
1656 .expect_err("zero tempo should fail");
1657
1658 assert_eq!(error.to_string(), "invalid tempo: 0");
1659 }
1660
1661 #[test]
1662 fn song_converts_valid_sequence_to_tunes_composition() {
1663 let c4 = parse_note_frequency("C4").unwrap();
1664 let sequence = PulseSequence::new()
1665 .with_notes(vec![c4])
1666 .with_durations(vec![0.25])
1667 .with_instrument("electric_piano");
1668
1669 let song = PulseSong::new()
1670 .with_tempo(120.0)
1671 .unwrap()
1672 .add_sequence(sequence);
1673
1674 let composition = song.to_tunes_composition();
1675 assert!(composition.is_ok());
1676 }
1677
1678 #[test]
1679 fn song_converts_drum_grid_to_tunes_composition() {
1680 let drum_grid = crate::drums::PulseDrumGrid::new()
1681 .with_steps(16)
1682 .unwrap()
1683 .with_step_duration(0.125)
1684 .unwrap()
1685 .sound("kick_808", vec![0, 4, 8, 12])
1686 .unwrap();
1687
1688 let song = PulseSong::new().add_drum_grid(drum_grid);
1689
1690 assert!(song.to_tunes_composition().is_ok());
1691 assert_eq!(song.drum_grids().len(), 1);
1692 }
1693
1694 #[test]
1695 fn phrase_repeats_sequences_and_drums_on_the_song_timeline() {
1696 let c4 = parse_note_frequency("C4").unwrap();
1697 let sequence = PulseSequence::new()
1698 .with_notes(vec![c4])
1699 .with_durations(vec![0.25])
1700 .with_instrument("electric_piano");
1701 let drum_grid = crate::drums::PulseDrumGrid::new()
1702 .with_steps(4)
1703 .unwrap()
1704 .with_step_duration(0.25)
1705 .unwrap()
1706 .sound("kick_808", vec![0])
1707 .unwrap();
1708
1709 let phrase = PulsePhrase::new()
1710 .add_sequence(sequence)
1711 .add_drum_grid(drum_grid)
1712 .with_repeat_times(2)
1713 .unwrap();
1714
1715 assert_eq!(phrase.repeat_times(), 2);
1716 assert_eq!(phrase.duration().unwrap(), 1.0);
1717
1718 let mixer = PulseSong::new().add_phrase(phrase).to_mixer().unwrap();
1719 let mut note_times = Vec::new();
1720 let mut drum_times = Vec::new();
1721
1722 for track in mixer.all_tracks() {
1723 for event in &track.events {
1724 match event {
1725 AudioEvent::Note(note) => note_times.push(note.start_time),
1726 AudioEvent::Drum(drum) => drum_times.push(drum.start_time),
1727 _ => {}
1728 }
1729 }
1730 }
1731
1732 note_times.sort_by(|left, right| left.partial_cmp(right).unwrap());
1733 drum_times.sort_by(|left, right| left.partial_cmp(right).unwrap());
1734
1735 assert_eq!(note_times, vec![0.0, 1.0]);
1736 assert_eq!(drum_times, vec![0.0, 1.0]);
1737 }
1738
1739 #[test]
1740 fn sequence_stores_and_applies_track_effects() {
1741 let c4 = parse_note_frequency("C4").unwrap();
1742 let sequence = PulseSequence::new()
1743 .with_notes(vec![c4])
1744 .with_durations(vec![0.25])
1745 .with_instrument("electric_piano")
1746 .with_effect(test_delay_effect())
1747 .with_effect(test_filter_effect());
1748
1749 assert_eq!(sequence.effects().len(), 2);
1750
1751 let mixer = PulseSong::new().add_sequence(sequence).to_mixer().unwrap();
1752 let tracks = mixer.all_tracks();
1753 assert_eq!(tracks.len(), 1);
1754 assert!(tracks[0].effects.delay.is_some());
1755 assert!(matches!(
1756 tracks[0].filter.filter_type,
1757 tunes::synthesis::filter::FilterType::LowPass
1758 ));
1759 }
1760
1761 #[test]
1762 fn song_stores_and_applies_master_effects() {
1763 let song = PulseSong::new().with_master_effect(test_eq_effect());
1764
1765 assert_eq!(song.master_effects().len(), 1);
1766
1767 let mixer = song.to_mixer().expect("song with master eq should build");
1768 assert!(mixer.master.eq.is_some());
1769 }
1770
1771 #[test]
1772 fn song_rejects_track_only_filter_on_master() {
1773 let error = PulseSong::new()
1774 .with_master_effect(test_filter_effect())
1775 .to_mixer()
1776 .expect_err("filter is not a master effect");
1777
1778 assert_eq!(error.to_string(), "invalid effect scope filter: master");
1779 }
1780
1781 #[test]
1782 fn sequence_applies_fm_synth_to_exported_notes() {
1783 let c4 = parse_note_frequency("C4").unwrap();
1784 let synth = crate::synthesis::synth_from_options(
1785 "fm",
1786 crate::synthesis::SynthOptions::Preset("bell".to_string()),
1787 )
1788 .expect("fm bell preset should parse");
1789 let sequence = PulseSequence::new()
1790 .with_notes(vec![c4])
1791 .with_durations(vec![0.25])
1792 .with_instrument("electric_piano")
1793 .with_synth(synth);
1794
1795 assert_eq!(
1796 sequence.synth().map(crate::synthesis::PulseSynth::name),
1797 Some("fm")
1798 );
1799
1800 let mixer = PulseSong::new().add_sequence(sequence).to_mixer().unwrap();
1801 let tracks = mixer.all_tracks();
1802 assert_eq!(tracks.len(), 1);
1803
1804 let AudioEvent::Note(note) = &tracks[0].events[0] else {
1805 panic!("expected note event");
1806 };
1807 assert_eq!(
1808 note.fm_params.mod_index,
1809 tunes::synthesis::fm_synthesis::FMParams::bell().mod_index
1810 );
1811 }
1812
1813 #[test]
1814 fn sequence_applies_additive_and_wavetable_synths_to_exported_notes() {
1815 let c4 = parse_note_frequency("C4").unwrap();
1816 let additive = crate::synthesis::synth_from_options(
1817 "additive",
1818 crate::synthesis::SynthOptions::Harmonics(vec![1.0, 0.5, 0.25]),
1819 )
1820 .expect("additive harmonics should parse");
1821 let wavetable = crate::synthesis::synth_from_options(
1822 "wavetable",
1823 crate::synthesis::SynthOptions::Default,
1824 )
1825 .expect("wavetable should parse");
1826
1827 let additive_sequence = PulseSequence::new()
1828 .with_notes(vec![c4])
1829 .with_durations(vec![0.25])
1830 .with_instrument("electric_piano")
1831 .with_synth(additive);
1832 let wavetable_sequence = PulseSequence::new()
1833 .with_notes(vec![c4])
1834 .with_durations(vec![0.25])
1835 .with_instrument("electric_piano")
1836 .with_synth(wavetable);
1837
1838 let mixer = PulseSong::new()
1839 .add_sequence(additive_sequence)
1840 .add_sequence(wavetable_sequence)
1841 .to_mixer()
1842 .unwrap();
1843 let tracks = mixer.all_tracks();
1844 assert_eq!(tracks.len(), 2);
1845
1846 for track in tracks {
1847 let AudioEvent::Note(note) = &track.events[0] else {
1848 panic!("expected note event");
1849 };
1850 assert!(note.custom_wavetable.is_some());
1851 }
1852 }
1853
1854 #[test]
1855 fn sequence_renders_karplus_strong_synth_as_sample_events() {
1856 let c4 = parse_note_frequency("C4").unwrap();
1857 let synth = crate::synthesis::synth_from_options(
1858 "karplus_strong",
1859 crate::synthesis::SynthOptions::Default,
1860 )
1861 .expect("karplus-strong should parse");
1862 let sequence = PulseSequence::new()
1863 .with_notes(vec![c4])
1864 .with_durations(vec![0.25])
1865 .with_instrument("electric_piano")
1866 .with_synth(synth);
1867
1868 assert_eq!(
1869 sequence.synth().map(crate::synthesis::PulseSynth::name),
1870 Some("karplus_strong")
1871 );
1872
1873 let mixer = PulseSong::new().add_sequence(sequence).to_mixer().unwrap();
1874 let tracks = mixer.all_tracks();
1875 assert_eq!(tracks.len(), 1);
1876
1877 let AudioEvent::Sample(sample) = &tracks[0].events[0] else {
1878 panic!("expected karplus-strong sample event");
1879 };
1880 assert_eq!(sample.start_time, 0.0);
1881 assert_eq!(sample.sample.sample_rate, 44_100);
1882 assert!((sample.sample.duration - 0.25).abs() < 0.001);
1883 }
1884
1885 #[test]
1886 fn midi_clip_rejects_unrenderable_repeat_expansion() {
1887 let c4 = parse_note_frequency("C4").unwrap();
1888 let source_mixer = PulseSong::new()
1889 .add_sequence(
1890 PulseSequence::new()
1891 .with_notes(vec![c4])
1892 .with_durations(vec![0.25])
1893 .with_instrument("electric_piano"),
1894 )
1895 .to_mixer()
1896 .unwrap();
1897 let clip = PulseMidiClip::new(source_mixer)
1898 .with_repeat_times(usize::MAX)
1899 .unwrap();
1900
1901 let error = clip
1902 .arranged_tracks(0.0, "midi")
1903 .expect_err("unrenderable repeat expansion should return an error");
1904
1905 assert!(matches!(error, PulseError::InvalidRepeatTimes { .. }));
1906 }
1907
1908 #[test]
1909 fn granular_sequence_duration_uses_output_duration_for_phrase_timing() {
1910 let synth = crate::synthesis::synth_from_options(
1911 "granular",
1912 crate::synthesis::SynthOptions::Params(std::collections::BTreeMap::from([
1913 (
1914 "source".to_string(),
1915 crate::synthesis::SynthOption::Text("source.wav".to_string()),
1916 ),
1917 (
1918 "duration".to_string(),
1919 crate::synthesis::SynthOption::Number(0.75),
1920 ),
1921 ])),
1922 )
1923 .expect("granular should parse");
1924 let sequence = PulseSequence::new().with_synth(synth);
1925 let phrase = PulsePhrase::new()
1926 .add_sequence(sequence)
1927 .with_repeat_times(2)
1928 .unwrap();
1929
1930 assert_eq!(phrase.duration().unwrap(), 0.75);
1931 assert_eq!(phrase.total_duration().unwrap(), 1.5);
1932 }
1933
1934 #[test]
1935 fn phrase_rejects_unrenderable_repeat_expansion_before_looping() {
1936 let c4 = parse_note_frequency("C4").unwrap();
1937 let phrase = PulsePhrase::new()
1938 .add_sequence(
1939 PulseSequence::new()
1940 .with_notes(vec![c4])
1941 .with_durations(vec![0.25])
1942 .with_instrument("electric_piano"),
1943 )
1944 .with_repeat_times(usize::MAX)
1945 .unwrap();
1946
1947 let error = match PulseSong::new().add_phrase(phrase).to_tunes_composition() {
1948 Ok(_) => panic!("unrenderable phrase repeat expansion should return an error"),
1949 Err(error) => error,
1950 };
1951
1952 assert!(matches!(error, PulseError::InvalidRepeatTimes { .. }));
1953 }
1954
1955 #[test]
1956 fn song_converts_sample_clips_to_sample_events() {
1957 let temp_dir = tempfile::tempdir().expect("temp dir should be created");
1958 let source = temp_dir.path().join("sample.wav");
1959 tunes::synthesis::sample::Sample::from_mono(vec![0.25; 4_410], 44_100)
1960 .export_wav(&source)
1961 .expect("test sample should be written");
1962
1963 let clip = PulseSampleClip::new(source.to_string_lossy().as_ref())
1964 .with_pitch_shift(12.0)
1965 .unwrap()
1966 .with_time_stretch(1.5)
1967 .unwrap()
1968 .with_playback_rate(0.5)
1969 .unwrap();
1970
1971 let mixer = PulseSong::new().add_sample_clip(clip).to_mixer().unwrap();
1972 let tracks = mixer.all_tracks();
1973 assert_eq!(tracks.len(), 1);
1974
1975 let AudioEvent::Sample(sample) = &tracks[0].events[0] else {
1976 panic!("expected sample event");
1977 };
1978 assert_eq!(sample.start_time, 0.0);
1979 assert_eq!(sample.playback_rate, 0.5);
1980 assert!(sample.sample.duration > 0.1);
1981 }
1982
1983 #[test]
1984 fn sample_clip_applies_track_name_volume_pan_and_effects() {
1985 let temp_dir = tempfile::tempdir().expect("temp dir should be created");
1986 let source = temp_dir.path().join("mix-sample.wav");
1987 tunes::synthesis::sample::Sample::from_mono(vec![0.25, 0.5, 0.25, -0.25], 44_100)
1988 .export_wav(&source)
1989 .expect("test sample should be written");
1990
1991 let clip = PulseSampleClip::new(source.to_string_lossy().as_ref())
1992 .with_track("loop_bus")
1993 .with_volume(0.42)
1994 .unwrap()
1995 .with_pan(-0.25)
1996 .unwrap()
1997 .with_effect(test_delay_effect());
1998
1999 let mixer = PulseSong::new().add_sample_clip(clip).to_mixer().unwrap();
2000 let tracks = mixer.all_tracks();
2001 assert_eq!(tracks.len(), 1);
2002 assert_eq!(tracks[0].name.as_deref(), Some("loop_bus"));
2003 assert!((tracks[0].volume - 0.42).abs() < f32::EPSILON);
2004 assert!((tracks[0].pan + 0.25).abs() < f32::EPSILON);
2005 assert!(tracks[0].effects.delay.is_some());
2006 }
2007
2008 #[test]
2009 fn sample_clip_rejects_invalid_track_volume_and_pan() {
2010 assert!(PulseSampleClip::new("source.wav")
2011 .with_volume(-0.01)
2012 .is_err());
2013 assert!(PulseSampleClip::new("source.wav")
2014 .with_volume(2.01)
2015 .is_err());
2016 assert!(PulseSampleClip::new("source.wav").with_pan(-1.01).is_err());
2017 assert!(PulseSampleClip::new("source.wav").with_pan(1.01).is_err());
2018 }
2019
2020 #[test]
2021 fn phrase_repeats_sample_clips_on_the_song_timeline() {
2022 let temp_dir = tempfile::tempdir().expect("temp dir should be created");
2023 let source = temp_dir.path().join("phrase-sample.wav");
2024 tunes::synthesis::sample::Sample::from_mono(vec![0.25; 4_410], 44_100)
2025 .export_wav(&source)
2026 .expect("test sample should be written");
2027
2028 let clip = PulseSampleClip::new(source.to_string_lossy().as_ref());
2029 let phrase = PulsePhrase::new()
2030 .add_sample_clip(clip)
2031 .with_repeat_times(2)
2032 .unwrap();
2033
2034 assert!((phrase.duration().unwrap() - 0.1).abs() < 0.001);
2035 assert!((phrase.total_duration().unwrap() - 0.2).abs() < 0.001);
2036
2037 let mixer = PulseSong::new().add_phrase(phrase).to_mixer().unwrap();
2038 let mut sample_times = Vec::new();
2039
2040 for track in mixer.all_tracks() {
2041 for event in &track.events {
2042 if let AudioEvent::Sample(sample) = event {
2043 sample_times.push(sample.start_time);
2044 }
2045 }
2046 }
2047
2048 sample_times.sort_by(|left, right| left.partial_cmp(right).unwrap());
2049 assert_eq!(sample_times, vec![0.0, 0.1]);
2050 }
2051
2052 #[test]
2053 fn sample_clip_offset_places_song_and_phrase_events_on_the_timeline() {
2054 let temp_dir = tempfile::tempdir().expect("temp dir should be created");
2055 let source = temp_dir.path().join("offset-sample.wav");
2056 tunes::synthesis::sample::Sample::from_mono(vec![0.25; 4_410], 44_100)
2057 .export_wav(&source)
2058 .expect("test sample should be written");
2059
2060 let song_clip = PulseSampleClip::new(source.to_string_lossy().as_ref())
2061 .with_start_at(0.25)
2062 .unwrap();
2063 let phrase_clip = PulseSampleClip::new(source.to_string_lossy().as_ref())
2064 .with_start_at(0.5)
2065 .unwrap();
2066 let phrase = PulsePhrase::new()
2067 .add_sample_clip(phrase_clip)
2068 .with_repeat_times(2)
2069 .unwrap();
2070
2071 let mixer = PulseSong::new()
2072 .add_sample_clip(song_clip)
2073 .add_phrase(phrase)
2074 .to_mixer()
2075 .unwrap();
2076 let mut sample_times = Vec::new();
2077
2078 for track in mixer.all_tracks() {
2079 for event in &track.events {
2080 if let AudioEvent::Sample(sample) = event {
2081 sample_times.push(sample.start_time);
2082 }
2083 }
2084 }
2085
2086 sample_times.sort_by(|left, right| left.partial_cmp(right).unwrap());
2087 assert_eq!(sample_times, vec![0.25, 0.5, 1.1]);
2088 }
2089
2090 #[test]
2091 fn sample_clip_slice_uses_only_the_selected_source_range() {
2092 let temp_dir = tempfile::tempdir().expect("temp dir should be created");
2093 let source = temp_dir.path().join("slice-sample.wav");
2094 tunes::synthesis::sample::Sample::from_mono(vec![0.25; 44_100], 44_100)
2095 .export_wav(&source)
2096 .expect("test sample should be written");
2097
2098 let clip = PulseSampleClip::new(source.to_string_lossy().as_ref())
2099 .with_slice(0.25, 0.5)
2100 .unwrap()
2101 .with_start_at(0.125)
2102 .unwrap();
2103
2104 assert!((clip.duration().unwrap() - 0.375).abs() < 0.01);
2105
2106 let mixer = PulseSong::new().add_sample_clip(clip).to_mixer().unwrap();
2107 let tracks = mixer.all_tracks();
2108 let AudioEvent::Sample(sample) = &tracks[0].events[0] else {
2109 panic!("expected sliced sample event");
2110 };
2111
2112 assert_eq!(sample.start_time, 0.125);
2113 assert!((sample.sample.duration - 0.25).abs() < 0.01);
2114 }
2115
2116 #[test]
2117 fn sample_clip_fade_in_and_out_shape_exported_sample_edges() {
2118 let temp_dir = tempfile::tempdir().expect("temp dir should be created");
2119 let source = temp_dir.path().join("fade-sample.wav");
2120 tunes::synthesis::sample::Sample::from_mono(vec![1.0; 44_100], 44_100)
2121 .export_wav(&source)
2122 .expect("test sample should be written");
2123
2124 let clip = PulseSampleClip::new(source.to_string_lossy().as_ref())
2125 .with_slice(0.0, 0.25)
2126 .unwrap()
2127 .with_fade_in(0.05)
2128 .unwrap()
2129 .with_fade_out(0.05)
2130 .unwrap();
2131
2132 let mixer = PulseSong::new().add_sample_clip(clip).to_mixer().unwrap();
2133 let tracks = mixer.all_tracks();
2134 let AudioEvent::Sample(sample) = &tracks[0].events[0] else {
2135 panic!("expected faded sample event");
2136 };
2137
2138 let first = sample.sample.data[0];
2139 let middle = sample.sample.data[sample.sample.data.len() / 2];
2140 let last = *sample
2141 .sample
2142 .data
2143 .last()
2144 .expect("sample should not be empty");
2145
2146 assert!(first.abs() < 0.001);
2147 assert!(middle > 0.9);
2148 assert!(last.abs() < 0.01);
2149 }
2150
2151 #[test]
2152 fn sample_clip_reverse_flips_the_selected_source_range() {
2153 let temp_dir = tempfile::tempdir().expect("temp dir should be created");
2154 let source = temp_dir.path().join("reverse-sample.wav");
2155 tunes::synthesis::sample::Sample::from_mono(vec![0.1, 0.2, 0.8, 1.0], 4)
2156 .export_wav(&source)
2157 .expect("test sample should be written");
2158
2159 let clip = PulseSampleClip::new(source.to_string_lossy().as_ref())
2160 .with_slice(0.0, 0.75)
2161 .unwrap()
2162 .with_reverse();
2163
2164 let mixer = PulseSong::new().add_sample_clip(clip).to_mixer().unwrap();
2165 let tracks = mixer.all_tracks();
2166 let AudioEvent::Sample(sample) = &tracks[0].events[0] else {
2167 panic!("expected reversed sample event");
2168 };
2169
2170 assert_eq!(sample.sample.data.len(), 3);
2171 assert!((sample.sample.data[0] - 0.8).abs() < 0.01);
2172 assert!((sample.sample.data[1] - 0.2).abs() < 0.01);
2173 assert!((sample.sample.data[2] - 0.1).abs() < 0.01);
2174 }
2175
2176 #[test]
2177 fn sample_clip_loop_for_expands_source_to_requested_duration() {
2178 let temp_dir = tempfile::tempdir().expect("temp dir should be created");
2179 let source = temp_dir.path().join("loop-for-sample.wav");
2180 tunes::synthesis::sample::Sample::from_mono(vec![0.1, 0.2], 4)
2181 .export_wav(&source)
2182 .expect("test sample should be written");
2183
2184 let clip = PulseSampleClip::new(source.to_string_lossy().as_ref())
2185 .with_loop_for(1.25)
2186 .unwrap();
2187
2188 assert!((clip.duration().unwrap() - 1.25).abs() < 0.01);
2189
2190 let mixer = PulseSong::new().add_sample_clip(clip).to_mixer().unwrap();
2191 let tracks = mixer.all_tracks();
2192 let AudioEvent::Sample(sample) = &tracks[0].events[0] else {
2193 panic!("expected looped sample event");
2194 };
2195
2196 assert!((sample.sample.duration - 1.25).abs() < 0.01);
2197 assert_eq!(sample.sample.data.len(), 5);
2198 assert!((sample.sample.data[0] - 0.1).abs() < 0.01);
2199 assert!((sample.sample.data[1] - 0.2).abs() < 0.01);
2200 assert!((sample.sample.data[2] - 0.1).abs() < 0.01);
2201 assert!((sample.sample.data[3] - 0.2).abs() < 0.01);
2202 assert!((sample.sample.data[4] - 0.1).abs() < 0.01);
2203 }
2204
2205 #[test]
2206 fn sample_clip_loop_for_rejects_unrenderable_durations() {
2207 let temp_dir = tempfile::tempdir().expect("temp dir should be created");
2208 let source = temp_dir.path().join("huge-loop-sample.wav");
2209 tunes::synthesis::sample::Sample::from_mono(vec![0.1, 0.2], 44_100)
2210 .export_wav(&source)
2211 .expect("test sample should be written");
2212
2213 let clip = PulseSampleClip::new(source.to_string_lossy().as_ref())
2214 .with_loop_for(f32::MAX)
2215 .unwrap();
2216
2217 let error = PulseSong::new()
2218 .add_sample_clip(clip)
2219 .to_mixer()
2220 .expect_err("unrenderable loop duration should return an error");
2221
2222 assert!(matches!(
2223 error,
2224 PulseError::InvalidSampleOption { ref option, .. } if option == "loop_for"
2225 ));
2226 }
2227
2228 #[test]
2229 fn sample_frame_count_rejects_vec_capacity_overflow() {
2230 let sample_rate = 44_100;
2231 let max_vec_frames = isize::MAX as usize / std::mem::size_of::<f32>();
2232 let duration = (max_vec_frames as f32 / sample_rate as f32) * 2.0;
2233
2234 let error = sample_frame_count("loop_for", duration, sample_rate)
2235 .expect_err("frame count should reject Vec capacity overflow");
2236
2237 assert!(matches!(
2238 error,
2239 PulseError::InvalidSampleOption { ref option, .. } if option == "loop_for"
2240 ));
2241 }
2242
2243 #[test]
2244 fn sample_clip_duration_accounts_for_loop_stretch_and_playback_rate() {
2245 let temp_dir = tempfile::tempdir().expect("temp dir should be created");
2246 let source = temp_dir.path().join("duration-sample.wav");
2247 tunes::synthesis::sample::Sample::from_mono(vec![0.25; 44_100], 44_100)
2248 .export_wav(&source)
2249 .expect("test sample should be written");
2250
2251 let clip = PulseSampleClip::new(source.to_string_lossy().as_ref())
2252 .with_start_at(0.25)
2253 .unwrap()
2254 .with_slice(0.1, 0.6)
2255 .unwrap()
2256 .with_loop_for(0.75)
2257 .unwrap()
2258 .with_time_stretch(2.0)
2259 .unwrap()
2260 .with_playback_rate(0.5)
2261 .unwrap();
2262
2263 assert!((clip.duration().unwrap() - 3.25).abs() < 0.001);
2264 }
2265
2266 #[test]
2267 fn sample_clip_metadata_duration_matches_public_duration_for_wav_transforms() {
2268 let temp_dir = tempfile::tempdir().expect("temp dir should be created");
2269 let source = temp_dir.path().join("metadata-duration.wav");
2270 tunes::synthesis::sample::Sample::from_mono(vec![0.25; 22_050], 44_100)
2271 .export_wav(&source)
2272 .expect("test sample should be written");
2273
2274 let clip = PulseSampleClip::new(source.to_string_lossy().as_ref())
2275 .with_start_at(0.125)
2276 .unwrap()
2277 .with_slice(0.1, 0.4)
2278 .unwrap()
2279 .with_time_stretch(1.5)
2280 .unwrap()
2281 .with_playback_rate(0.75)
2282 .unwrap()
2283 .with_pitch_shift(12.0)
2284 .unwrap()
2285 .with_reverse()
2286 .with_normalize()
2287 .with_fade_in(0.01)
2288 .unwrap()
2289 .with_fade_out(0.02)
2290 .unwrap()
2291 .with_gain(0.8)
2292 .unwrap();
2293
2294 let metadata_duration = clip.metadata_duration().unwrap();
2295 let public_duration = clip.duration().unwrap();
2296
2297 assert!((metadata_duration - public_duration).abs() < 0.01);
2298 assert!((metadata_duration - 0.725).abs() < 0.01);
2299 }
2300
2301 #[test]
2302 fn sample_clip_duration_matches_rendered_sample_for_subtle_time_stretch() {
2303 let temp_dir = tempfile::tempdir().expect("temp dir should be created");
2304 let source = temp_dir.path().join("subtle-stretch.wav");
2305 tunes::synthesis::sample::Sample::from_mono(vec![0.25; 441_000], 44_100)
2306 .export_wav(&source)
2307 .expect("test sample should be written");
2308
2309 let clip = PulseSampleClip::new(source.to_string_lossy().as_ref())
2312 .with_time_stretch(1.009)
2313 .unwrap();
2314
2315 let rendered_duration = clip
2316 .transformed_sample()
2317 .expect("sample should transform")
2318 .duration
2319 / clip.playback_rate();
2320 let metadata_duration = clip.metadata_duration().unwrap();
2321
2322 assert!(
2323 (rendered_duration - metadata_duration).abs() < 0.01,
2324 "metadata duration {metadata_duration} must match rendered duration {rendered_duration} for subtle time_stretch"
2325 );
2326 }
2327
2328 #[test]
2329 fn sample_clip_duration_rejects_slice_beyond_wav_duration() {
2330 let temp_dir = tempfile::tempdir().expect("temp dir should be created");
2331 let source = temp_dir.path().join("short-sample.wav");
2332 tunes::synthesis::sample::Sample::from_mono(vec![0.25; 4_410], 44_100)
2333 .export_wav(&source)
2334 .expect("test sample should be written");
2335
2336 let clip = PulseSampleClip::new(source.to_string_lossy().as_ref())
2337 .with_slice(0.0, 0.2)
2338 .unwrap();
2339
2340 assert!(clip.duration().is_err());
2341 }
2342
2343 #[test]
2344 fn sample_clip_normalize_scales_source_peak_before_gain() {
2345 let temp_dir = tempfile::tempdir().expect("temp dir should be created");
2346 let source = temp_dir.path().join("normalize-sample.wav");
2347 tunes::synthesis::sample::Sample::from_mono(vec![0.1, -0.25, 0.5], 3)
2348 .export_wav(&source)
2349 .expect("test sample should be written");
2350
2351 let clip = PulseSampleClip::new(source.to_string_lossy().as_ref())
2352 .with_normalize()
2353 .with_gain(0.5)
2354 .unwrap();
2355
2356 let mixer = PulseSong::new().add_sample_clip(clip).to_mixer().unwrap();
2357 let tracks = mixer.all_tracks();
2358 let AudioEvent::Sample(sample) = &tracks[0].events[0] else {
2359 panic!("expected normalized sample event");
2360 };
2361
2362 let peak = sample
2363 .sample
2364 .data
2365 .iter()
2366 .fold(0.0_f32, |current, value| current.max(value.abs()));
2367 assert!((peak - 0.5).abs() < 0.01);
2368 }
2369}