firewheel_sampler/lib.rs
1/*
2use std::{
3 fmt::Debug,
4 num::NonZeroUsize,
5 ops::Range,
6 sync::{atomic::Ordering, Arc},
7};
8
9use firewheel_core::{
10 clock::{ClockTime, EventDelay},
11 node::{AudioNode, AudioNodeInfo, AudioNodeProcessor, ProcInfo, ProcessStatus},
12 param::{range::percent_volume_to_raw_gain, smoother::ParamSmoother},
13 sample_resource::SampleResource,
14 ChannelConfig, ChannelCount, SilenceMask, StreamInfo,
15};
16
17/// Configuration of a [`SamplerNode`]
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct SamplerConfig {
20 /// The size of the command queue.
21 ///
22 /// By default this is set to `128`.
23 pub command_queue_size: NonZeroUsize,
24
25 /// The maximum number of samples (voiced) that can be played
26 /// concurrently in this node.
27 ///
28 /// By default this is set to `4`.
29 pub voice_limit: NonZeroUsize,
30}
31
32impl Default for SamplerConfig {
33 fn default() -> Self {
34 Self {
35 command_queue_size: NonZeroUsize::new(128).unwrap(),
36 voice_limit: NonZeroUsize::new(4).unwrap(),
37 }
38 }
39}
40
41/// Additional options for playing a sample.
42#[derive(Debug, Clone, PartialEq)]
43pub struct PlaySampleOpts {
44 /// The range of the sample to play.
45 ///
46 /// By default this is set to [`SampleRange::FullSample`].
47 pub range: SampleRange,
48
49 /// If `true`, then all previously playing samples on this node will
50 /// be stopped.
51 ///
52 /// By default this is set to `false`.
53 pub stop_previous: bool,
54
55 /// The percent volume to play this sample at (where `0.0` is silence
56 /// and `100.0` is unity gain).
57 ///
58 /// Note, this is more for creating advanced sequences when chaining
59 /// commands together, not for simple sample playback. It is
60 /// recommended to use the `Volume` node instead in that case as it
61 /// can be dynamically changed.
62 ///
63 /// If this value is less than or equal to `0.0`, then this command
64 /// will be ignored.
65 ///
66 /// By default this is set to `100.0`.
67 pub percent_volume: f32,
68
69 /// The stereo panning of this sample. (Where `0.0` is center, `-1.0`
70 /// is full-left, and `1.0` is full-right).
71 ///
72 /// This has no effect if the sample is not stereo.
73 ///
74 /// Note, this is more for creating advanced sequences when chaining
75 /// commands together, not for simple sample playback. It is
76 /// recommended to use the `Volume` node instead in that case as it
77 /// can be dynamically changed.
78 ///
79 /// By default this is set to `0.0`.
80 pub pan: f32,
81 // TODO: Pitch (doppler stretching) parameter.
82}
83
84impl Default for PlaySampleOpts {
85 fn default() -> Self {
86 Self {
87 range: SampleRange::FullSample,
88 stop_previous: false,
89 percent_volume: 100.0,
90 pan: 0.0,
91 }
92 }
93}
94
95/// Additional options for looping a sample.
96#[derive(Debug, Clone, PartialEq)]
97pub struct LoopSampleOpts {
98 /// The range in the sample to loop in.
99 ///
100 /// By default this is set to [`SampleRange::FullSample`].
101 pub range: SampleRange,
102
103 /// How many times to repeat the loop.
104 ///
105 /// By default this is set to [`LoopMode::Endless`].
106 pub mode: LoopMode,
107
108 /// Where to begin playing in the sample relative to the start of the
109 /// loop range in units of seconds. If the given time it outside the
110 /// loop range, then the beginning of the loop range will be used
111 /// instead.
112 ///
113 /// By default this is set to `0.0`.
114 pub start_from_secs: f64,
115
116 /// If `true`, then the previous loop iteration will be stopped (choked)
117 /// when the range is less than the length of the sample.
118 ///
119 /// If `false`, then the previous loop iteration will continue playing
120 /// until completion, overlapping with the next loop iteration. This
121 /// can be useful for example making a "rapid fire" of gunshot sounds.
122 ///
123 /// This has no effect when the range is [`SampleRange::FullSample`] or
124 /// when it is longer than the length of the sample.
125 ///
126 /// By default this is set to `true`.
127 pub choke: bool,
128
129 /// The percent volume to play this sample at (where `0.0` is silence
130 /// and `100.0` is unity gain).
131 ///
132 /// Note that gain and pan cannot be changed later for this sample
133 /// playback. Dynamic gain and pan are done using separate audio
134 /// nodes.
135 ///
136 /// If this value is less than or equal to `0.0`, then this command
137 /// will be ignored.
138 ///
139 /// By default this is set to `100.0`.
140 pub percent_volume: f32,
141
142 /// The stereo panning of this sample. (Where `0.0` is center, `-1.0`
143 /// is full-left, and `1.0` is full-right).
144 ///
145 /// This has no effect if the the output of this node is not stereo.
146 ///
147 /// Note that gain and pan cannot be changed later for this sample
148 /// playback. Dynamic gain and pan are done using separate audio
149 /// nodes.
150 ///
151 /// By default this is set to `0.0`.
152 pub pan: f32,
153 // TODO: Pitch (doppler stretching) parameter.
154}
155
156impl Default for LoopSampleOpts {
157 fn default() -> Self {
158 Self {
159 range: SampleRange::FullSample,
160 mode: LoopMode::Endless,
161 start_from_secs: 0.0,
162 choke: true,
163 percent_volume: 100.0,
164 pan: 0.0,
165 }
166 }
167}
168
169/// A command for a [`SamplerNode`].
170#[derive(Debug, Clone, PartialEq)]
171pub struct SamplerCommand<S: SampleResource> {
172 /// When the command should occur.
173 pub delay: EventDelay,
174 /// The type of command to execute.
175 pub command: SamplerCommandType<S>,
176}
177
178/// The type of command for a [`SamplerNode`].
179#[derive(Debug, Clone, PartialEq)]
180pub enum SamplerCommandType<S: SampleResource> {
181 /// Play a sample to completion.
182 ///
183 /// If the number of samples being played in this node exceeds the sample
184 /// (voice) limit, the the oldest voice will be stopped and replaced with
185 /// this one.
186 PlaySample {
187 /// The sample to play.
188 ///
189 /// If this is `None`, then the previously played sample will be played
190 /// again. If there was no previously played sample, then this command
191 /// will be ignored.
192 sample: Option<S>,
193 /// Additional options for playing a sample.
194 opts: PlaySampleOpts,
195 },
196 /// Play a sample, seamlessly looping back to the start of the range.
197 ///
198 /// If the number of samples being played in this node exceeds the sample
199 /// (voice) limit, the the oldest voice will be stopped and replaced with
200 /// this one.
201 LoopSample {
202 /// The sample to play.
203 ///
204 /// If this is `None`, then the previously played sample will be played
205 /// again. If there was no previously played sample, then this command
206 /// will be ignored.
207 sample: Option<S>,
208 /// Additional options for looping a sample.
209 opts: LoopSampleOpts,
210 },
211 /// Pause sample playback.
212 Pause,
213 /// Resume sample playback.
214 Resume,
215 /// Stop all currently playing samples on this node.
216 ///
217 /// Sending [`SamplerCommandType::Resume` afterwards will *NOT* resume
218 /// playback of the previously playing samples.
219 Stop,
220}
221
222/// The range in a sample resource [`SampleResource`]
223#[derive(Default, Debug, Clone, PartialEq)]
224pub enum SampleRange {
225 /// Use the full length of the sample.
226 #[default]
227 FullSample,
228 /// Use only a section of the sample (units are in seconds).
229 ///
230 /// The start of the range must be greater than or equal to 0.0.
231 ///
232 /// The end of the range may extend past the length of the sample,
233 /// in which case silence will be played to fill in the gaps.
234 RangeSecs(Range<f64>),
235}
236
237/// The number of times to loop an audio sample.
238#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
239pub enum LoopMode {
240 /// Loop endlessly. Useful for music playback.
241 #[default]
242 Endless,
243 /// Loop the given number of times
244 Count(u32),
245}
246
247#[derive(Debug, Clone, PartialEq)]
248pub enum CommandType<S: SampleResource> {
249 Single(SamplerCommand<S>),
250 Group(Vec<SamplerCommand<S>>),
251}
252
253enum ProcessorToNodeMsg<S: SampleResource> {
254 ReturnSample(S),
255 ReturnCommandGroup(Vec<SamplerCommand<S>>),
256}
257
258pub struct ActiveSamplerNode<S: SampleResource> {
259 // TODO: Find a good solution for webassembly.
260 to_processor_tx: rtrb::Producer<CommandType<S>>,
261 from_processor_rx: rtrb::Consumer<ProcessorToNodeMsg<S>>,
262}
263
264impl<S: SampleResource> ActiveSamplerNode<S> {
265 /// Play a sample to completion.
266 ///
267 /// If the number of samples being played in this node exceeds the sample
268 /// (voice) limit, the the oldest voice will be stopped and replaced with
269 /// this one.
270 ///
271 /// * `sample` - The sample to play. If this is `None`, then the previously
272 /// played sample will be played again. If there was no previously played
273 /// sample, then this command will be ignored.
274 /// * `delay` - When the command should occur.
275 /// * `opts` - Additional options for playing a sample.
276 ///
277 /// Returns an error if the command queue is full.
278 pub fn play_sample(
279 &mut self,
280 sample: Option<S>,
281 delay: EventDelay,
282 opts: PlaySampleOpts,
283 ) -> Result<(), rtrb::PushError<CommandType<S>>> {
284 self.push_command(SamplerCommand {
285 delay,
286 command: SamplerCommandType::PlaySample { sample, opts },
287 })
288 }
289
290 /// Play a sample, seamlessly looping back to the start of the range.
291 ///
292 /// If the number of samples being played in this node exceeds the sample
293 /// (voice) limit, the the oldest voice will be stopped and replaced with
294 /// this one.
295 ///
296 /// * `sample` - The sample to loop. If this is `None`, then the previously
297 /// played sample will be played again. If there was no previously played
298 /// sample, then this command will be ignored.
299 /// * `delay` - When the command should occur.
300 /// * `opts` - Additional options for looping a sample.
301 ///
302 /// Returns an error if the command queue is full.
303 pub fn loop_sample(
304 &mut self,
305 sample: Option<S>,
306 delay: EventDelay,
307 opts: LoopSampleOpts,
308 ) -> Result<(), rtrb::PushError<CommandType<S>>> {
309 self.push_command(SamplerCommand {
310 delay,
311 command: SamplerCommandType::LoopSample { sample, opts },
312 })
313 }
314
315 /// Pause sample playback.
316 ///
317 /// * `delay` - When the command should occur.
318 ///
319 /// Returns an error if the command queue is full.
320 pub fn pause(&mut self, delay: EventDelay) -> Result<(), rtrb::PushError<CommandType<S>>> {
321 self.push_command(SamplerCommand {
322 delay,
323 command: SamplerCommandType::Pause,
324 })
325 }
326
327 /// Resume sample playback.
328 ///
329 /// * `delay` - When the command should occur.
330 ///
331 /// Returns an error if the command queue is full.
332 pub fn resume(&mut self, delay: EventDelay) -> Result<(), rtrb::PushError<CommandType<S>>> {
333 self.push_command(SamplerCommand {
334 delay,
335 command: SamplerCommandType::Resume,
336 })
337 }
338
339 /// Stop all currently playing samples on this node.
340 ///
341 /// Calling [`ActiveSamplerNode::resume()`] afterwards will *NOT* resume
342 /// playback of the previously playing samples.
343 ///
344 /// * `delay` - When the command should occur.
345 ///
346 /// Returns an error if the command queue is full.
347 pub fn stop(&mut self, delay: EventDelay) -> Result<(), rtrb::PushError<CommandType<S>>> {
348 self.push_command(SamplerCommand {
349 delay,
350 command: SamplerCommandType::Stop,
351 })
352 }
353
354 /// Push a new [`SamplerCommand`] to execute.
355 ///
356 /// Returns an error if the command queue is full.
357 pub fn push_command(
358 &mut self,
359 command: SamplerCommand<S>,
360 ) -> Result<(), rtrb::PushError<CommandType<S>>> {
361 self.to_processor_tx.push(CommandType::Single(command))
362 }
363
364 /// Push a new group of [`SamplerCommand`]s to execute.
365 ///
366 /// Returns an error if the command queue is full.
367 pub fn push_command_group(
368 &mut self,
369 commands: Vec<SamplerCommand<S>>,
370 ) -> Result<(), rtrb::PushError<CommandType<S>>> {
371 self.to_processor_tx.push(CommandType::Group(commands))
372 }
373}
374
375pub struct SamplerNode<S: SampleResource> {
376 active_state: Option<ActiveSamplerNode<S>>,
377 config: SamplerConfig,
378}
379
380impl<S: SampleResource> SamplerNode<S> {
381 pub fn new(config: SamplerConfig) -> Self {
382 Self {
383 active_state: None,
384 config,
385 }
386 }
387
388 /// Get an immutable reference the active context.
389 ///
390 /// Returns `None` if this node is not currently activated.
391 pub fn get(&self) -> Option<&ActiveSamplerNode<S>> {
392 self.active_state.as_ref()
393 }
394
395 /// Get a mutable reference the active context.
396 ///
397 /// Returns `None` if this node is not currently activated.
398 pub fn get_mut(&mut self) -> Option<&mut ActiveSamplerNode<S>> {
399 self.active_state.as_mut()
400 }
401}
402
403impl<S: SampleResource> AudioNode for SamplerNode<S> {
404 fn debug_name(&self) -> &'static str {
405 "sampler"
406 }
407
408 fn info(&self) -> AudioNodeInfo {
409 AudioNodeInfo {
410 num_min_supported_outputs: ChannelCount::MONO,
411 num_max_supported_outputs: ChannelCount::MAX,
412 updates: true,
413 ..Default::default()
414 }
415 }
416
417 fn activate(
418 &mut self,
419 stream_info: StreamInfo,
420 _channel_config: ChannelConfig,
421 ) -> Result<Box<dyn AudioNodeProcessor>, Box<dyn std::error::Error>> {
422 let (to_processor_tx, from_node_rx) =
423 rtrb::RingBuffer::<CommandType<S>>::new(self.config.command_queue_size.get());
424 let (to_node_tx, from_processor_rx) =
425 rtrb::RingBuffer::<ProcessorToNodeMsg<S>>::new(self.config.command_queue_size.get());
426
427 self.active_state = Some(ActiveSamplerNode {
428 to_processor_tx,
429 from_processor_rx,
430 });
431
432 Ok(Box::new(SamplerProcessor::new(
433 stream_info.sample_rate,
434 stream_info.stream_latency_samples as usize,
435 stream_info.max_block_samples as usize,
436 from_node_rx,
437 to_node_tx,
438 )))
439 }
440
441 fn update(&mut self) {
442 if let Some(active_state) = &mut self.active_state {
443 while let Ok(msg) = active_state.from_processor_rx.pop() {
444 // Clean up resources.
445 match msg {
446 ProcessorToNodeMsg::ReturnSample(_smp) => {}
447 ProcessorToNodeMsg::ReturnCommandGroup(_cmds) => {}
448 }
449 }
450 }
451 }
452}
453
454struct SamplerProcessor<S: SampleResource> {
455 playing: bool,
456 sample_rate: u32,
457 stream_latency_samples: usize,
458 playhead: u64,
459 loop_range: Option<ProcSampleRange>,
460
461 sample: Option<S>,
462
463 from_node_rx: rtrb::Consumer<CommandType<S>>,
464 to_node_tx: rtrb::Producer<ProcessorToNodeMsg<S>>,
465}
466
467impl<S: SampleResource> SamplerProcessor<S> {
468 fn new(
469 sample_rate: u32,
470 stream_latency_samples: usize,
471 max_block_samples: usize,
472 from_node_rx: rtrb::Consumer<CommandType<S>>,
473 to_node_tx: rtrb::Producer<ProcessorToNodeMsg<S>>,
474 ) -> Self {
475 Self {
476 playing: false,
477 sample_rate,
478 stream_latency_samples,
479 playhead: 0,
480 loop_range: None,
481 sample: None,
482 from_node_rx,
483 to_node_tx,
484 }
485 }
486}
487
488impl<S: SampleResource> AudioNodeProcessor for SamplerProcessor<S> {
489 fn process(
490 &mut self,
491 samples: usize,
492 _inputs: &[&[f32]],
493 outputs: &mut [&mut [f32]],
494 _proc_info: ProcInfo,
495 ) -> ProcessStatus {
496 while let Ok(msg) = self.from_node_rx.pop() {
497 match msg {
498 NodeToProcessorMsg::SetSample {
499 sample,
500 stop_playback,
501 } => {
502 if let Some(old_sample) = self.sample.take() {
503 let _ = self
504 .to_node_tx
505 .push(ProcessorToNodeMsg::ReturnSample(old_sample));
506 }
507
508 self.sample = Some(sample);
509
510 if let Some(loop_range) = &mut self.loop_range {
511 loop_range.update_sample(&self.sample);
512 }
513
514 if stop_playback {
515 self.playhead = self
516 .loop_range
517 .as_ref()
518 .map(|l| l.playhead_range.start)
519 .unwrap_or(0);
520
521 if self.playing {
522 self.playing = false;
523
524 // TODO
525 }
526 }
527
528 // TODO: Declick
529 }
530 NodeToProcessorMsg::Play => {
531 if !self.playing {
532 self.playing = true;
533
534 // TODO: Declick
535 }
536 }
537 NodeToProcessorMsg::Pause => {
538 if self.playing {
539 self.playing = false;
540
541 // TODO: Declick
542 }
543 }
544 NodeToProcessorMsg::Stop => {
545 self.playhead = self
546 .loop_range
547 .as_ref()
548 .map(|l| l.playhead_range.start)
549 .unwrap_or(0);
550
551 if self.playing {
552 self.playing = false;
553
554 // TODO: Declick
555 }
556 }
557 NodeToProcessorMsg::SetPlayheadSecs(playhead_secs) => {
558 let sample = (playhead_secs * f64::from(self.sample_rate)).round() as u64;
559
560 if sample != self.playhead {
561 self.playhead = sample;
562 // TODO: Declick
563 }
564 }
565 NodeToProcessorMsg::SetSampleRange(loop_range) => {
566 self.loop_range = loop_range.map(|loop_range| {
567 ProcSampleRange::new(loop_range, self.sample_rate, &self.sample)
568 });
569
570 if let Some(loop_range) = &self.loop_range {
571 if loop_range.playhead_range.contains(&self.playhead) {
572 self.playhead = loop_range.playhead_range.start;
573
574 // TODO: Declick
575 }
576 }
577 }
578 }
579 }
580
581 let Some(sample) = &self.sample else {
582 // TODO: Declick
583
584 // No sample data, output silence.
585 return ProcessStatus::NoOutputsModified;
586 };
587
588 if !self.playing {
589 // TODO: Declick
590
591 // Not playing, output silence.
592 return ProcessStatus::NoOutputsModified;
593 }
594
595 let raw_gain = self.raw_gain.load(Ordering::Relaxed);
596 let gain = self.gain_smoother.set_and_process(raw_gain, samples);
597 // Hint to the compiler to optimize loop.
598 assert_eq!(gain.values.len(), samples);
599
600 if !gain.is_smoothing() && gain.values[0] < 0.00001 {
601 // TODO: Reset declick.
602
603 // Muted, so there is no need to process.
604 return ProcessStatus::NoOutputsModified;
605 }
606
607 if let Some(loop_range) = &self.loop_range {
608 if self.playhead >= loop_range.playhead_range.end {
609 // Playhead is out of range. Return to the start.
610 self.playhead = self
611 .loop_range
612 .as_ref()
613 .map(|l| l.playhead_range.start)
614 .unwrap_or(0);
615 }
616
617 // Copy first block of samples.
618
619 let samples_left = if loop_range.playhead_range.end - self.playhead <= usize::MAX as u64
620 {
621 (loop_range.playhead_range.end - self.playhead) as usize
622 } else {
623 usize::MAX
624 };
625 let first_copy_samples = samples.min(samples_left);
626
627 sample.fill_buffers(outputs, 0..first_copy_samples, self.playhead);
628
629 if first_copy_samples < samples {
630 // Loop back to the start.
631 self.playhead = self
632 .loop_range
633 .as_ref()
634 .map(|l| l.playhead_range.start)
635 .unwrap_or(0);
636
637 // Copy second block of samples.
638
639 let second_copy_samples = samples - first_copy_samples;
640
641 sample.fill_buffers(outputs, first_copy_samples..samples, self.playhead);
642
643 self.playhead += second_copy_samples as u64;
644 } else {
645 self.playhead += samples as u64;
646 }
647 } else {
648 if self.playhead >= sample.len_samples() {
649 // Playhead is out of range. Output silence.
650 return ProcessStatus::NoOutputsModified;
651
652 // TODO: Notify node that sample has finished.
653 }
654
655 let copy_samples = samples.min((sample.len_samples() - self.playhead) as usize);
656
657 sample.fill_buffers(outputs, 0..copy_samples, self.playhead);
658
659 if copy_samples < samples {
660 // Finished playing sample.
661 self.playing = false;
662 self.playhead = 0;
663
664 // Fill any remaining samples with zeros
665 for out_ch in outputs.iter_mut() {
666 out_ch[copy_samples..].fill(0.0);
667 }
668
669 // TODO: Notify node that sample has finished.
670 } else {
671 self.playhead += samples as u64;
672 }
673 }
674
675 let sample_channels = sample.num_channels().get();
676
677 // Apply gain and declick
678 // TODO: Declick
679 if outputs.len() >= 2 && sample_channels == 2 {
680 // Provide an optimized stereo loop.
681
682 // Hint to the compiler to optimize loop.
683 assert_eq!(outputs[0].len(), samples);
684 assert_eq!(outputs[1].len(), samples);
685
686 for i in 0..samples {
687 outputs[0][i] *= gain.values[i];
688 outputs[1][i] *= gain.values[i];
689 }
690 } else {
691 for (out_ch, _) in outputs.iter_mut().zip(0..sample_channels) {
692 // Hint to the compiler to optimize loop.
693 assert_eq!(out_ch.len(), samples);
694
695 for i in 0..samples {
696 out_ch[i] *= gain.values[i];
697 }
698 }
699 }
700
701 let mut out_silence_mask = SilenceMask::NONE_SILENT;
702
703 if outputs.len() > sample_channels {
704 if outputs.len() == 2 && sample_channels == 1 {
705 // If the output of this node is stereo and the sample is mono,
706 // assume that the user wants both channels filled with the
707 // sample data.
708 let (out_first, outs) = outputs.split_first_mut().unwrap();
709 outs[0].copy_from_slice(out_first);
710 } else {
711 // Fill the rest of the channels with zeros.
712 for (i, out_ch) in outputs.iter_mut().enumerate().skip(sample_channels) {
713 out_ch.fill(0.0);
714 out_silence_mask.set_channel(i, true);
715 }
716 }
717 }
718
719 ProcessStatus::outputs_modified(out_silence_mask)
720 }
721}
722
723impl<S: SampleResource> Drop for SamplerProcessor<S> {
724 fn drop(&mut self) {
725 if let Some(sample) = self.sample.take() {
726 let _ = self
727 .to_node_tx
728 .push(ProcessorToNodeMsg::ReturnSample(sample));
729 }
730 }
731}
732
733impl<S: SampleResource> Into<Box<dyn AudioNode>> for SamplerNode<S> {
734 fn into(self) -> Box<dyn AudioNode> {
735 Box::new(self)
736 }
737}
738
739struct Voice<S: SampleResource> {
740 sample: S,
741
742 raw_gain_l: f32,
743 raw_gain_r: Option<f32>,
744}
745
746impl<S: SampleResource> Voice<S> {
747
748}
749
750struct ProcSampleRange {
751 playhead_range: Range<u64>,
752 full_range: bool,
753}
754
755impl ProcSampleRange {
756 fn new<S: SampleResource>(
757 loop_range: SampleRange,
758 sample_rate: u32,
759 sample: &Option<S>,
760 ) -> Self {
761 let (start_frame, end_frame, full_range) = match &loop_range {
762 SampleRange::FullSample => {
763 let end_frame = if let Some(sample) = sample {
764 sample.len_samples()
765 } else {
766 0
767 };
768
769 (0, end_frame, true)
770 }
771 SampleRange::RangeSecs(range) => (
772 (range.start * f64::from(sample_rate)).round() as u64,
773 (range.end * f64::from(sample_rate)).round() as u64,
774 false,
775 ),
776 };
777
778 Self {
779 playhead_range: start_frame..end_frame,
780 full_range,
781 }
782 }
783
784 fn update_sample<S: SampleResource>(&mut self, sample: &Option<S>) {
785 let Some(sample) = sample else {
786 return;
787 };
788
789 if !self.full_range {
790 return;
791 }
792
793 let end_frame = sample.len_samples();
794
795 self.playhead_range = 0..end_frame;
796 }
797}
798*/