audio_samples/iterators.rs
1//! Structured iteration over audio sample data.
2//!
3//! This module defines the primary iteration abstractions for traversing
4//! [`AudioSamples`] in semantically meaningful ways. Rather than exposing raw
5//! indexing or layout-dependent access, the iterators in this module present
6//! audio data through three conceptual lenses:
7//!
8//! - **Frames** — a snapshot across all channels at one time index
9//! ([`FrameIterator`])
10//! - **Channels** — the full temporal sequence for one channel
11//! ([`ChannelIterator`])
12//! - **Windows** — fixed-size, optionally overlapping temporal blocks
13//! ([`WindowIterator`], requires the `editing` feature)
14//!
15//! Audio algorithms frequently need to traverse data in ways that reflect its
16//! *structure* rather than its *storage layout*. Centralising iteration logic here
17//! prevents duplicated indexing and boundary-handling code throughout the crate,
18//! while keeping each iterator's ownership and lifetime contract explicit and
19//! documented at the iterator type level.
20//!
21//! For in-place or overlapping mutation, specialised methods such as
22//! [`AudioSamples::apply_to_frames`], [`AudioSamples::apply_to_channel_data`], and
23//! [`AudioSamples::apply_to_windows`] are provided as counterparts to the
24//! read-oriented iterators defined here.
25//!
26//! Obtain an iterator by calling the corresponding method on any
27//! [`AudioSamples`] value. The method is also available through the
28//! [`AudioSampleIterators`] extension trait. Collect, chain, or consume the
29//! iterator using standard [`Iterator`] combinators.
30//!
31//! ```
32//! use audio_samples::{AudioSamples, sample_rate, iterators::AudioSampleIterators};
33//! use ndarray::array;
34//!
35//! let audio = AudioSamples::new_multi_channel(
36//! array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
37//! sample_rate!(44100),
38//! ).unwrap();
39//!
40//! // Iterate over time-aligned frames (one sample per channel per time step).
41//! for frame in audio.frames() {
42//! assert_eq!(frame.num_channels().get(), 2);
43//! }
44//!
45//! // Iterate over complete channels.
46//! let channels: Vec<_> = audio.channels().collect();
47//! assert_eq!(channels.len(), 2);
48//! ```
49
50#[cfg(feature = "editing")]
51use non_empty_slice::{NonEmptyVec, non_empty_vec};
52
53use crate::{
54 AudioData, AudioSampleError, AudioSampleResult, AudioSamples, LayoutError,
55 traits::StandardSample,
56};
57
58#[cfg(feature = "editing")]
59use crate::AudioEditing;
60
61use std::marker::PhantomData;
62
63#[cfg(feature = "editing")]
64use std::num::NonZeroUsize;
65
66/// Extension trait providing iterator methods for AudioSamples.
67pub trait AudioSampleIterators<'a, T>
68where
69 T: StandardSample,
70{
71 /// Returns an iterator over frames, where each frame is a snapshot of one
72 /// sample from each channel at the same point in time.
73 ///
74 /// For mono audio, each frame contains exactly one sample. For multi-channel
75 /// audio, each frame contains one sample per channel, preserving channel
76 /// alignment across time.
77 ///
78 /// # Returns
79 ///
80 /// A [`FrameIterator`] that yields one [`AudioSamples`] view per time index.
81 /// The total number of frames equals `self.samples_per_channel()`.
82 ///
83 /// # Panics
84 ///
85 /// Does not panic.
86 ///
87 /// ## Examples
88 ///
89 /// ```
90 /// use audio_samples::{AudioSamples, sample_rate, iterators::AudioSampleIterators};
91 /// use ndarray::array;
92 ///
93 /// let audio = AudioSamples::new_multi_channel(
94 /// array![[1.0f32, 2.0], [3.0, 4.0]],
95 /// sample_rate!(44100),
96 /// ).unwrap();
97 ///
98 /// // Each frame has one sample per channel; two time steps → two frames.
99 /// let mut count = 0;
100 /// for frame in audio.frames() {
101 /// assert_eq!(frame.num_channels().get(), 2);
102 /// count += 1;
103 /// }
104 /// assert_eq!(count, 2);
105 /// ```
106 fn frames<'iter>(&'iter self) -> FrameIterator<'iter, 'a, T>
107 where
108 'a: 'iter;
109
110 /// Returns an iterator over complete channels.
111 ///
112 /// Each iteration yields the full temporal sequence of samples belonging to
113 /// one channel. Channels are yielded in increasing channel-index order.
114 ///
115 /// # Returns
116 ///
117 /// A [`ChannelIterator`] that yields one owned [`AudioSamples`] per channel.
118 /// The total number of items equals `self.num_channels()`.
119 ///
120 /// # Panics
121 ///
122 /// Does not panic.
123 ///
124 /// ## Examples
125 ///
126 /// ```
127 /// use audio_samples::{AudioSamples, sample_rate, iterators::AudioSampleIterators};
128 /// use ndarray::array;
129 ///
130 /// let audio = AudioSamples::new_multi_channel(
131 /// array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
132 /// sample_rate!(44100),
133 /// ).unwrap();
134 ///
135 /// let channels: Vec<_> = audio.channels().collect();
136 /// assert_eq!(channels.len(), 2);
137 /// assert_eq!(channels[0].samples_per_channel().get(), 3);
138 /// ```
139 fn channels<'iter>(&'iter self) -> ChannelIterator<'iter, 'a, T>;
140
141 #[cfg(feature = "editing")]
142 /// Returns an iterator over fixed-size, optionally overlapping windows.
143 ///
144 /// Each window covers `window_size` samples per channel. Successive windows
145 /// start `hop_size` samples apart, so windows overlap when `hop_size < window_size`.
146 ///
147 /// The default boundary strategy is [`PaddingMode::Zero`]. Call
148 /// [`WindowIterator::with_padding_mode`] on the returned iterator to change it.
149 ///
150 /// # Arguments
151 ///
152 /// – `window_size` — number of samples per channel in each window. If zero,
153 /// no windows are yielded.
154 /// – `hop_size` — number of samples to advance between window starts. If zero,
155 /// no windows are yielded.
156 ///
157 /// # Returns
158 ///
159 /// A [`WindowIterator`] that yields one owned [`AudioSamples`] per window.
160 ///
161 /// # Panics
162 ///
163 /// Does not panic.
164 ///
165 /// ## Examples
166 ///
167 /// See [`AudioSamples::windows`] for a runnable usage example.
168 ///
169 /// ```ignore
170 /// // Conceptual usage via the trait interface (usize arguments):
171 /// let windows: Vec<_> = audio.windows(3_usize, 3_usize).collect();
172 /// ```
173 fn windows<'iter>(
174 &'iter self,
175 window_size: usize,
176 hop_size: usize,
177 ) -> WindowIterator<'iter, 'a, T>
178 where
179 'a: 'iter;
180}
181
182impl<'a, T> AudioSamples<'a, T>
183where
184 T: StandardSample,
185{
186 /// Returns an iterator over frames, where each frame is a snapshot of one
187 /// sample from each channel at the same point in time.
188 ///
189 /// For mono audio, each frame contains exactly one sample. For multi-channel
190 /// audio, each frame contains one sample per channel in channel-index order.
191 ///
192 /// # Returns
193 ///
194 /// A [`FrameIterator`] that yields one [`AudioSamples`] view per time index.
195 /// The iterator yields exactly `self.samples_per_channel()` frames.
196 ///
197 /// # Panics
198 ///
199 /// Does not panic.
200 ///
201 /// ## Examples
202 ///
203 /// ```
204 /// use audio_samples::{AudioSamples, sample_rate};
205 /// use ndarray::array;
206 ///
207 /// let audio = AudioSamples::new_multi_channel(
208 /// array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
209 /// sample_rate!(44100),
210 /// ).unwrap();
211 ///
212 /// // Three time steps → three frames.
213 /// assert_eq!(audio.frames().count(), 3);
214 ///
215 /// // Each frame spans all channels.
216 /// for frame in audio.frames() {
217 /// assert_eq!(frame.num_channels().get(), 2);
218 /// }
219 /// ```
220 #[inline]
221 #[must_use]
222 pub fn frames<'iter>(&'iter self) -> FrameIterator<'iter, 'a, T>
223 where
224 'a: 'iter,
225 {
226 FrameIterator::new(self)
227 }
228
229 /// Returns an iterator over complete channels.
230 ///
231 /// Each iteration yields the full temporal sequence of samples belonging to
232 /// one channel. Channels are yielded in increasing channel-index order.
233 ///
234 /// Each yielded value is an owned [`AudioSamples`] instance containing exactly
235 /// one mono channel. This involves allocation and data copying.
236 ///
237 /// # Returns
238 ///
239 /// A [`ChannelIterator`] yielding one owned [`AudioSamples`] per channel.
240 /// The iterator yields exactly `self.num_channels()` items.
241 ///
242 /// # Panics
243 ///
244 /// Does not panic.
245 ///
246 /// ## Examples
247 ///
248 /// ```
249 /// use audio_samples::{AudioSamples, sample_rate};
250 /// use ndarray::array;
251 ///
252 /// let audio = AudioSamples::new_multi_channel(
253 /// array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
254 /// sample_rate!(44100),
255 /// ).unwrap();
256 ///
257 /// let channels: Vec<_> = audio.channels().collect();
258 /// assert_eq!(channels.len(), 2);
259 /// assert_eq!(channels[0].samples_per_channel().get(), 3);
260 /// ```
261 #[inline]
262 #[must_use]
263 pub fn channels<'iter>(&'iter self) -> ChannelIterator<'iter, 'a, T> {
264 ChannelIterator::new(self)
265 }
266
267 #[cfg(feature = "editing")]
268 /// Returns an iterator over fixed-size, optionally overlapping windows.
269 ///
270 /// Each window covers `window_size` samples per channel. Successive windows
271 /// start `hop_size` samples apart, so windows overlap when `hop_size < window_size`.
272 ///
273 /// The default boundary strategy is [`PaddingMode::Zero`], which zero-pads the
274 /// last window when the signal does not divide evenly. Call
275 /// [`WindowIterator::with_padding_mode`] on the returned iterator to change
276 /// this behaviour.
277 ///
278 /// # Arguments
279 ///
280 /// – `window_size` — number of samples per channel in each window.
281 /// – `hop_size` — number of samples to advance between window starts.
282 ///
283 /// # Returns
284 ///
285 /// A [`WindowIterator`] yielding one owned [`AudioSamples`] per window.
286 ///
287 /// # Panics
288 ///
289 /// Does not panic.
290 ///
291 /// ## Examples
292 ///
293 /// ```
294 /// # #[cfg(feature = "editing")] {
295 /// use audio_samples::{AudioSamples, sample_rate};
296 /// use ndarray::array;
297 /// use std::num::NonZeroUsize;
298 ///
299 /// let audio = AudioSamples::new_mono(
300 /// array![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0],
301 /// sample_rate!(44100),
302 /// ).unwrap();
303 ///
304 /// // Non-overlapping windows of size 3.
305 /// let windows: Vec<_> = audio
306 /// .windows(NonZeroUsize::new(3).unwrap(), NonZeroUsize::new(3).unwrap())
307 /// .collect();
308 /// assert_eq!(windows.len(), 2);
309 /// assert_eq!(windows[0].samples_per_channel().get(), 3);
310 /// # }
311 /// ```
312 #[inline]
313 #[must_use]
314 pub fn windows<'iter>(
315 &'iter self,
316 window_size: NonZeroUsize,
317 hop_size: NonZeroUsize,
318 ) -> WindowIterator<'iter, 'a, T>
319 where
320 'a: 'iter,
321 {
322 WindowIterator::new(self, window_size, hop_size)
323 }
324
325 /// Applies a mutable function to every frame without requiring a borrowing-safe iterator.
326 ///
327 /// The callback receives the frame index and a mutable slice containing the samples for
328 /// that frame across all channels. For mono audio the slice has length 1. For
329 /// multi-channel audio the slice is a temporary buffer ordered by channel index;
330 /// changes are written back into the underlying storage after the callback returns.
331 ///
332 /// Use this method when in-place, frame-wise mutation is needed and the immutable
333 /// [`AudioSamples::frames`] iterator is insufficient.
334 ///
335 /// # Arguments
336 ///
337 /// – `f` — a closure of the form `FnMut(frame_index: usize, frame_samples: &mut [T])`.
338 /// – `frame_index` — zero-based index of the current frame.
339 /// – `frame_samples` — mutable slice of length `num_channels()` for the current frame.
340 ///
341 /// # Returns
342 ///
343 /// `()` — the audio is modified in place.
344 ///
345 /// # Panics
346 ///
347 /// Does not panic.
348 ///
349 /// ## Examples
350 ///
351 /// ```
352 /// use audio_samples::{AudioSamples, sample_rate};
353 /// use ndarray::array;
354 ///
355 /// let mut audio = AudioSamples::new_multi_channel(
356 /// array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
357 /// sample_rate!(44100),
358 /// ).unwrap();
359 ///
360 /// // Double every sample frame-by-frame.
361 /// audio.apply_to_frames(|_frame_idx, frame| {
362 /// for s in frame { *s *= 2.0; }
363 /// });
364 ///
365 /// assert_eq!(
366 /// audio.as_multi_channel().unwrap(),
367 /// &array![[2.0f32, 4.0, 6.0], [8.0, 10.0, 12.0]],
368 /// );
369 /// ```
370 #[inline]
371 pub fn apply_to_frames<F>(&mut self, mut f: F)
372 where
373 F: FnMut(usize, &mut [T]), // (frame_index, frame_samples)
374 {
375 match &mut self.data {
376 AudioData::Mono(arr) => {
377 for (frame_idx, sample) in arr.iter_mut().enumerate() {
378 f(frame_idx, std::slice::from_mut(sample));
379 }
380 }
381 AudioData::Multi(arr) => {
382 let (channels, samples_per_channel) = arr.dim();
383
384 for frame_idx in 0..samples_per_channel.get() {
385 let mut frame = Vec::with_capacity(channels.get());
386 for ch in 0..channels.get() {
387 frame.push(arr[[ch, frame_idx]]);
388 }
389
390 f(frame_idx, &mut frame);
391
392 for ch in 0..channels.get() {
393 arr[[ch, frame_idx]] = frame[ch];
394 }
395 }
396 }
397 }
398 }
399
400 /// Applies a mutable function to each channel's contiguous sample slice.
401 ///
402 /// This is the fallible counterpart to [`AudioSamples::apply_to_channel_data`].
403 /// It requires that the underlying ndarray storage is contiguous in memory.
404 /// Non-contiguous layouts (such as after certain in-place reversals or
405 /// non-standard strides) will cause this method to return an error.
406 ///
407 /// The callback receives the channel index and a mutable slice of all samples
408 /// for that channel.
409 ///
410 /// # Arguments
411 ///
412 /// – `f` — a closure of the form `FnMut(channel_index: usize, channel_samples: &mut [T])`.
413 /// – `channel_index` — zero-based index of the channel being processed.
414 /// – `channel_samples` — mutable slice of all samples belonging to that channel.
415 ///
416 /// # Returns
417 ///
418 /// `Ok(())` if all channels were processed successfully.
419 ///
420 /// # Errors
421 ///
422 /// Returns [crate::AudioSampleError::Layout] with variant `NonContiguous` if the
423 /// underlying multi-channel storage is not contiguous in memory.
424 ///
425 /// # Panics
426 ///
427 /// Does not panic.
428 ///
429 /// ## Examples
430 ///
431 /// ```
432 /// use audio_samples::{AudioSamples, sample_rate};
433 /// use ndarray::array;
434 ///
435 /// let mut audio = AudioSamples::new_multi_channel(
436 /// array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
437 /// sample_rate!(44100),
438 /// ).unwrap();
439 ///
440 /// // Halve channel 0, double channel 1.
441 /// audio.try_apply_to_channel_data(|ch, samples| {
442 /// let gain = if ch == 0 { 0.5 } else { 2.0 };
443 /// for s in samples { *s *= gain; }
444 /// }).unwrap();
445 ///
446 /// assert_eq!(
447 /// audio.as_multi_channel().unwrap(),
448 /// &array![[0.5f32, 1.0, 1.5], [8.0, 10.0, 12.0]],
449 /// );
450 /// ```
451 #[inline]
452 pub fn try_apply_to_channel_data<F>(&mut self, mut f: F) -> AudioSampleResult<()>
453 where
454 F: FnMut(usize, &mut [T]), // (channel_index, channel_samples)
455 {
456 match &mut self.data {
457 AudioData::Mono(arr) => {
458 let slice = arr.as_slice_mut();
459 f(0, slice);
460 }
461 AudioData::Multi(arr) => {
462 let (channels, samples_per_channel) = arr.dim();
463 let samples_per_channel = samples_per_channel.get();
464 let slice = arr.as_slice_mut().ok_or_else(|| {
465 AudioSampleError::Layout(LayoutError::NonContiguous {
466 operation: "multi-channel iterator access".to_string(),
467 layout_type: "non-contiguous multi-channel data".to_string(),
468 })
469 })?;
470
471 for ch in 0..channels.get() {
472 let start_idx = ch * samples_per_channel;
473 let channel_slice = &mut slice[start_idx..start_idx + samples_per_channel];
474 f(ch, channel_slice);
475 }
476 }
477 }
478 Ok(())
479 }
480
481 /// Applies a mutable function to each channel's contiguous sample slice.
482 ///
483 /// This is the infallible counterpart to [`AudioSamples::try_apply_to_channel_data`].
484 /// It panics if the underlying storage is not contiguous; prefer the fallible
485 /// variant when working with audio that may have non-standard memory layouts.
486 ///
487 /// The callback receives the channel index and a mutable slice of all samples
488 /// for that channel.
489 ///
490 /// # Arguments
491 ///
492 /// – `f` — a closure of the form `FnMut(channel_index: usize, channel_samples: &mut [T])`.
493 /// – `channel_index` — zero-based index of the channel being processed.
494 /// – `channel_samples` — mutable slice of all samples belonging to that channel.
495 ///
496 /// # Returns
497 ///
498 /// `()` — the audio is modified in place.
499 ///
500 /// # Panics
501 ///
502 /// Panics if the underlying storage is not contiguous in memory. Use
503 /// [`AudioSamples::try_apply_to_channel_data`] to handle non-contiguous inputs
504 /// without panicking.
505 ///
506 /// ## Examples
507 ///
508 /// ```
509 /// use audio_samples::{AudioSamples, sample_rate};
510 /// use ndarray::array;
511 ///
512 /// let mut audio = AudioSamples::new_mono(
513 /// array![1.0f32, 2.0, 3.0, 4.0],
514 /// sample_rate!(44100),
515 /// ).unwrap();
516 ///
517 /// // Add 10.0 to every sample.
518 /// audio.apply_to_channel_data(|_ch, samples| {
519 /// for s in samples { *s += 10.0; }
520 /// });
521 ///
522 /// assert_eq!(audio.as_mono().unwrap(), &array![11.0f32, 12.0, 13.0, 14.0]);
523 /// ```
524 #[inline]
525 pub fn apply_to_channel_data<F>(&mut self, mut f: F)
526 where
527 F: FnMut(usize, &mut [T]), // (channel_index, channel_samples)
528 {
529 self.try_apply_to_channel_data(|ch, data| f(ch, data))
530 .expect("apply_to_channel_data requires contiguous storage; use try_apply_to_channel_data to handle non-contiguous inputs");
531 }
532
533 /// Applies a mutable function to each temporal window of audio data.
534 ///
535 /// For mono audio, the callback receives a mutable slice directly into the
536 /// underlying buffer for each window. For multi-channel audio, the callback
537 /// receives a temporary interleaved buffer of length `window_size * num_channels`
538 /// laid out as `[ch0_s0, ch1_s0, …, ch0_s1, ch1_s1, …]`; changes are
539 /// written back into the underlying storage after the callback returns.
540 ///
541 /// Only fully-contained windows are visited; trailing samples that do not
542 /// form a complete window are not passed to the callback.
543 ///
544 /// Use this method for in-place windowed processing, such as applying window
545 /// functions or block-wise gain changes, when the read-only
546 /// [`AudioSamples::windows`] iterator is not sufficient.
547 ///
548 /// # Arguments
549 ///
550 /// – `window_size` — number of samples per channel in each window. If zero,
551 /// the method returns immediately.
552 /// – `hop_size` — number of samples to advance between window starts. If zero,
553 /// the method returns immediately.
554 /// – `f` — a closure of the form `FnMut(window_index: usize, window_samples: &mut [T])`.
555 /// – `window_index` — zero-based index of the current window.
556 /// – `window_samples` — mutable slice for the current window. For mono audio,
557 /// length equals `window_size`. For multi-channel audio, length equals
558 /// `window_size * num_channels`, laid out in interleaved channel order.
559 ///
560 /// # Returns
561 ///
562 /// `()` — the audio is modified in place.
563 ///
564 /// # Panics
565 ///
566 /// Does not panic.
567 ///
568 /// ## Examples
569 ///
570 /// ```
571 /// use audio_samples::{AudioSamples, sample_rate};
572 /// use ndarray::array;
573 ///
574 /// let mut audio = AudioSamples::new_mono(
575 /// array![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0],
576 /// sample_rate!(44100),
577 /// ).unwrap();
578 ///
579 /// // Halve every sample using non-overlapping windows of size 3.
580 /// audio.apply_to_windows(3, 3, |_window_idx, window| {
581 /// for s in window { *s *= 0.5; }
582 /// });
583 ///
584 /// assert_eq!(
585 /// audio.as_mono().unwrap(),
586 /// &array![0.5f32, 1.0, 1.5, 2.0, 2.5, 3.0],
587 /// );
588 /// ```
589 #[inline]
590 pub fn apply_to_windows<F>(&mut self, window_size: usize, hop_size: usize, mut f: F)
591 where
592 F: FnMut(usize, &mut [T]), // (window_index, window_samples)
593 {
594 let total_samples = self.samples_per_channel().get();
595 if total_samples == 0 || window_size == 0 {
596 return;
597 }
598
599 match &mut self.data {
600 AudioData::Mono(arr) => {
601 let mut window_idx = 0;
602 let mut pos = 0;
603
604 while pos + window_size <= total_samples {
605 let slice = arr.as_slice_mut();
606 let window_slice = &mut slice[pos..pos + window_size];
607 f(window_idx, window_slice);
608 pos += hop_size;
609 window_idx += 1;
610 }
611 }
612 AudioData::Multi(arr) => {
613 let (rows, cols) = arr.dim();
614 let rows = rows.get();
615 let samples_per_channel = cols;
616
617 let mut pos = 0;
618 let mut window_idx = 0;
619
620 while pos + window_size <= samples_per_channel.get() {
621 // Create a temporary buffer for the interleaved window
622 let mut window_data = vec![T::zero(); window_size * rows];
623
624 // Copy window data from each channel into interleaved buffer.
625 for ch in 0..rows {
626 for sample_idx in 0..window_size {
627 let dst_idx = sample_idx * rows + ch; // Interleaved layout
628 window_data[dst_idx] = arr[[ch, pos + sample_idx]];
629 }
630 }
631
632 // Call the user function
633 f(window_idx, &mut window_data);
634
635 // Copy modified data back to original channels.
636 for ch in 0..rows {
637 for sample_idx in 0..window_size {
638 let src_idx = sample_idx * rows + ch; // Interleaved layout
639 arr[[ch, pos + sample_idx]] = window_data[src_idx];
640 }
641 }
642
643 pos += hop_size;
644 window_idx += 1;
645 }
646 }
647 }
648 }
649}
650
651/// Iterates over time-aligned frames of an [`AudioSamples`] instance.
652///
653/// A *frame* represents the set of samples across all channels at a single
654/// time index. For mono audio, each frame contains exactly one sample. For
655/// multi-channel audio, each frame contains one sample per channel, preserving
656/// channel alignment.
657///
658/// ## Purpose
659///
660/// `FrameIterator` provides a structured, time-centric view of audio data.
661/// It is intended for algorithms that operate on synchronous samples across
662/// channels, such as frame-wise feature extraction, analysis, or inspection.
663///
664/// The iterator yields immutable views into the underlying audio data. No
665/// reordering, resampling, or interpolation is performed.
666///
667/// ## Invariants
668///
669/// - Frames are yielded in strictly increasing temporal order.
670/// - Each yielded frame corresponds to exactly one time index.
671/// - The number of frames is equal to the number of samples per channel.
672/// - All channels are assumed to have equal length.
673///
674/// ## Assumptions and Limitations
675///
676/// This iterator assumes that the underlying [`AudioSamples`] instance is
677/// channel-aligned and immutable for the lifetime of the iterator. It is not
678/// suitable for in-place mutation or algorithms that require overlapping or
679/// non-sequential access.
680///
681/// Use higher-level windowed or transformation APIs when temporal context
682/// beyond a single frame is required.
683pub struct FrameIterator<'iter, 'a, T>
684where
685 T: StandardSample,
686 'a: 'iter,
687{
688 /// The source audio data over which frames are iterated.
689 audio: &'iter AudioSamples<'a, T>,
690 current_frame: usize,
691 total_frames: usize,
692 _phantom: PhantomData<T>,
693}
694
695impl<'iter, 'a, T> FrameIterator<'iter, 'a, T>
696where
697 T: StandardSample,
698 'a: 'iter,
699{
700 /// Constructs a new frame iterator over the given audio.
701 ///
702 /// ## Purpose
703 ///
704 /// This constructor establishes a frame-wise traversal over the provided
705 /// audio data, yielding one frame per time index.
706 ///
707 /// # Arguments
708 ///
709 /// - `audio`: The source audio to iterate over. All channels must be
710 /// time-aligned.
711 ///
712 /// ## Behavioural Guarantees
713 ///
714 /// - The iterator will yield exactly `audio.samples_per_channel()` frames.
715 /// - Frames are yielded in deterministic order.
716 ///
717 /// # Panics
718 ///
719 /// This function does not panic.
720 #[inline]
721 #[must_use]
722 pub fn new(audio: &'iter AudioSamples<'a, T>) -> Self {
723 let total_frames = audio.samples_per_channel().get();
724 Self {
725 audio,
726 current_frame: 0,
727 total_frames,
728 _phantom: PhantomData,
729 }
730 }
731}
732
733impl<'iter, 'a, T> Iterator for FrameIterator<'iter, 'a, T>
734where
735 T: StandardSample,
736 'a: 'iter,
737{
738 type Item = AudioSamples<'iter, T>;
739
740 #[inline]
741 fn next(&mut self) -> Option<Self::Item> {
742 if self.current_frame >= self.total_frames {
743 return None;
744 }
745
746 let frame_range = self.current_frame..self.current_frame + 1;
747 self.current_frame += 1;
748 // Copy the &'iter reference so that slice_samples returns AudioSamples<'iter, T>
749 // rather than a shorter-lived borrow through &mut self.
750 let audio: &'iter AudioSamples<'a, T> = self.audio;
751 audio.slice_samples(frame_range).ok()
752 }
753
754 #[inline]
755 fn size_hint(&self) -> (usize, Option<usize>) {
756 let remaining = self.total_frames - self.current_frame;
757 (remaining, Some(remaining))
758 }
759}
760
761impl<T> ExactSizeIterator for FrameIterator<'_, '_, T> where T: StandardSample {}
762
763/// Iterates over complete channels of an [`AudioSamples`] instance.
764///
765/// Each iteration yields the full sequence of samples belonging to a single
766/// channel, independent of other channels. Channels are yielded sequentially
767/// in channel index order.
768///
769/// ## Purpose
770///
771/// `ChannelIterator` provides a channel-centric view of audio data. It is
772/// intended for workflows that process or analyse channels independently,
773/// such as per-channel filtering, statistics, or visualisation.
774///
775/// Unlike frame-based iteration, this iterator exposes the *entire temporal
776/// extent* of one channel at a time.
777///
778/// ## Behaviour and Ownership
779///
780/// Each yielded item is an owned [`AudioSamples`] instance containing exactly
781/// one channel. This reflects the fact that channel-wise slicing produces
782/// independent audio objects rather than borrowed views.
783///
784/// As a result, channel iteration involves allocation and data copying.
785/// Callers should take this into account when iterating over large audio
786/// buffers or when allocation-free access is required.
787///
788/// ## Invariants
789///
790/// - Channels are yielded in increasing channel index order.
791/// - Each channel is yielded exactly once.
792/// - The number of yielded items is equal to the number of channels.
793/// - All samples within a yielded item belong to the same channel.
794pub struct ChannelIterator<'iter, 'data, T>
795where
796 T: StandardSample,
797{
798 /// The source audio from which channels are extracted.
799 audio: &'iter AudioSamples<'data, T>,
800 current_channel: usize,
801 total_channels: usize,
802}
803
804impl<'iter, 'data, T> ChannelIterator<'iter, 'data, T>
805where
806 T: StandardSample,
807{
808 /// Constructs a new iterator over the channels of the given audio.
809 ///
810 /// ## Purpose
811 ///
812 /// This constructor establishes a channel-wise traversal over the provided
813 /// audio data, yielding one complete channel per iteration.
814 ///
815 /// # Arguments
816 ///
817 /// - `audio`: The source audio whose channels will be iterated.
818 ///
819 /// ## Behavioural Guarantees
820 ///
821 /// - The iterator will yield exactly `audio.num_channels()` items.
822 /// - Channels are yielded in deterministic order.
823 ///
824 /// # Panics
825 ///
826 /// This function does not panic.
827 #[inline]
828 #[must_use]
829 pub fn new(audio: &'iter AudioSamples<'data, T>) -> Self {
830 let total_channels = audio.num_channels().get();
831
832 Self {
833 audio,
834 current_channel: 0,
835 total_channels: total_channels as usize,
836 }
837 }
838}
839
840impl<T> Iterator for ChannelIterator<'_, '_, T>
841where
842 T: StandardSample,
843{
844 type Item = AudioSamples<'static, T>;
845 #[inline]
846 fn next(&mut self) -> Option<Self::Item> {
847 if self.current_channel >= self.total_channels {
848 return None;
849 }
850
851 let channel = match self
852 .audio
853 .clone()
854 .into_owned()
855 .slice_channels(self.current_channel..=self.current_channel)
856 {
857 Ok(ch) => ch,
858 Err(e) => {
859 eprintln!("Error slicing channel {}: {}", self.current_channel, e);
860 return None;
861 }
862 };
863
864 self.current_channel += 1;
865
866 Some(channel)
867 }
868
869 #[inline]
870 fn size_hint(&self) -> (usize, Option<usize>) {
871 let remaining = self.total_channels - self.current_channel;
872 (remaining, Some(remaining))
873 }
874}
875
876impl<T> ExactSizeIterator for ChannelIterator<'_, '_, T> where T: StandardSample {}
877
878/// Defines how window iteration behaves when a window extends beyond the
879/// available audio data.
880///
881/// `PaddingMode` controls the treatment of trailing windows whose span exceeds
882/// the number of samples per channel. The selected mode determines whether such
883/// windows are padded, truncated, or omitted entirely.
884#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
885#[non_exhaustive]
886pub enum PaddingMode {
887 /// Pads incomplete windows with zeros so that all yielded windows have
888 /// identical length.
889 ///
890 /// This mode guarantees a fixed window size and a deterministic number of
891 /// windows, which is required by many spectral and frame-based algorithms.
892 #[default]
893 Zero,
894
895 /// Yields trailing windows without padding.
896 ///
897 /// Windows near the end of the signal may be shorter than the configured
898 /// window size. Callers must be prepared to handle variable-length windows.
899 None,
900
901 /// Omits any window that would extend beyond the available data.
902 ///
903 /// Only fully contained windows are yielded. This mode produces no padding
904 /// and no partial windows.
905 Skip,
906}
907/// Iterates over fixed-size temporal windows of audio data.
908///
909/// Each iteration yields a contiguous block of samples spanning all channels
910/// over a fixed temporal extent. Successive windows are offset by a configurable
911/// hop size and may overlap depending on the chosen parameters.
912///
913/// ## Purpose
914///
915/// `WindowIterator` provides a structured abstraction for windowed audio
916/// processing. It is intended for algorithms that operate on local temporal
917/// context, such as spectral analysis, feature extraction, and block-based
918/// transformations.
919///
920/// Window iteration is defined in terms of *time*, not storage layout. All
921/// windows preserve channel alignment and temporal ordering.
922///
923/// ## Window Boundaries and Padding
924///
925/// When a window would extend beyond the available data, its treatment is
926/// determined by the configured [`PaddingMode`]. Depending on this mode,
927/// trailing windows may be padded, truncated, or skipped entirely. This choice
928/// directly affects both the number and shape of yielded windows.
929///
930/// ## Ownership and Allocation
931///
932/// Each yielded window is returned as an owned [`AudioSamples`] instance. This
933/// allows windows to be processed independently but implies allocation and
934/// copying **may** be performed. Whether or not this occurs is down to whether
935/// the data is already owned or not. If not then yes, it will allocate,
936/// otherwise the a borrow is used. For in-place or allocation-free processing,
937/// prefer specialised higher-level APIs where available.
938///
939/// ## Invariants
940///
941/// - Windows are yielded in strictly increasing temporal order.
942/// - All channels within a window remain time-aligned.
943/// - The hop size between successive windows is constant.
944/// - The iterator yields a finite, deterministic number of windows.
945///
946/// ## Assumptions and Limitations
947///
948/// This iterator assumes a fixed sampling rate for the
949/// lifetime of iteration. It is not suitable for overlapping mutable access or
950/// algorithms that require shared ownership of window data.
951#[cfg(feature = "editing")]
952pub struct WindowIterator<'iter, 'a, T>
953where
954 T: StandardSample,
955 'a: 'iter,
956{
957 /// The source audio from which windows are extracted.
958 audio: &'iter AudioSamples<'a, T>,
959 window_size: NonZeroUsize,
960 hop_size: NonZeroUsize,
961 current_position: usize,
962 total_samples: NonZeroUsize,
963 total_windows: NonZeroUsize,
964 current_window: usize,
965 padding_mode: PaddingMode,
966 _phantom: PhantomData<T>,
967}
968
969#[cfg(feature = "editing")]
970impl<'iter, 'a, T> WindowIterator<'iter, 'a, T>
971where
972 T: StandardSample,
973 'a: 'iter,
974{
975 /// Constructs a new window iterator over the given audio.
976 ///
977 /// ## Purpose
978 ///
979 /// This constructor establishes a windowed traversal over the provided
980 /// audio data using the specified window and hop sizes.
981 ///
982 /// # Arguments
983 ///
984 /// - `audio`: The source audio to iterate over.
985 /// - `window_size`: The number of samples per channel in each window.
986 /// - `hop_size`: The number of samples between the starts of successive windows.
987 ///
988 /// ## Behavioural Guarantees
989 ///
990 /// - Windows are generated deterministically from the start of the signal.
991 /// - The default padding mode is [`PaddingMode::Zero`].
992 ///
993 /// ## Degenerate Parameters
994 ///
995 /// If either `window_size` or `hop_size` is zero, the iterator yields no
996 /// windows.
997 ///
998 /// # Panics
999 ///
1000 /// This function does not panic.
1001 fn new(
1002 audio: &'iter AudioSamples<'a, T>,
1003 window_size: NonZeroUsize,
1004 hop_size: NonZeroUsize,
1005 ) -> Self {
1006 let total_samples = audio.samples_per_channel();
1007
1008 let total_windows =
1009 Self::calculate_total_windows(total_samples, window_size, hop_size, PaddingMode::Zero);
1010
1011 Self {
1012 audio,
1013 window_size,
1014 hop_size,
1015 current_position: 0,
1016 total_samples,
1017 total_windows,
1018 current_window: 0,
1019 padding_mode: PaddingMode::Zero,
1020 _phantom: PhantomData,
1021 }
1022 }
1023
1024 const fn calculate_total_windows(
1025 total_samples: NonZeroUsize,
1026 window_size: NonZeroUsize,
1027 hop_size: NonZeroUsize,
1028 padding_mode: PaddingMode,
1029 ) -> NonZeroUsize {
1030 // Calculate the maximum number of windows we could have
1031 // This is the ceiling of total_samples / hop_size
1032 let max_windows = total_samples.get().div_ceil(hop_size.get());
1033 let max_windows = unsafe {
1034 // safety: div_ceil on NonZeroUsize is not stable yet. Convert to usize, do div_ceil, and then go back. Never not valid
1035 NonZeroUsize::new_unchecked(max_windows)
1036 };
1037
1038 match padding_mode {
1039 PaddingMode::Zero => {
1040 // With zero padding, we can always create max_windows
1041 max_windows
1042 }
1043 PaddingMode::None => {
1044 // With no padding, count windows that have at least some real data
1045 let mut count = 0;
1046 let mut pos = 0;
1047 while pos < total_samples.get() {
1048 count += 1;
1049 pos += hop_size.get();
1050 }
1051 // safety: count is at least 1 because total_samples is non-zero
1052 unsafe { NonZeroUsize::new_unchecked(count) }
1053 }
1054 PaddingMode::Skip => {
1055 // With skip, only count complete windows
1056 // safety: we have already checked that window_size and hop_size are non-zero
1057 unsafe {
1058 NonZeroUsize::new_unchecked(
1059 1 + (total_samples.get() - window_size.get()) / hop_size.get(),
1060 )
1061 }
1062 }
1063 }
1064 }
1065
1066 /// Sets the padding strategy used for trailing windows.
1067 ///
1068 /// ## Purpose
1069 ///
1070 /// This method allows callers to control how incomplete windows at the end
1071 /// of the signal are handled.
1072 ///
1073 /// Changing the padding mode affects both the number of yielded windows and
1074 /// the shape of the final windows.
1075 ///
1076 /// ## Behavioural Guarantees
1077 ///
1078 /// - The iterator’s internal window count is updated consistently with the
1079 /// selected mode.
1080 #[inline]
1081 #[must_use]
1082 pub const fn with_padding_mode(mut self, mode: PaddingMode) -> Self {
1083 self.padding_mode = mode;
1084 self.total_windows = Self::calculate_total_windows(
1085 self.total_samples,
1086 self.window_size,
1087 self.hop_size,
1088 mode,
1089 );
1090 self
1091 }
1092}
1093
1094#[cfg(feature = "editing")]
1095impl<T> Iterator for WindowIterator<'_, '_, T>
1096where
1097 T: StandardSample,
1098{
1099 type Item = AudioSamples<'static, T>;
1100
1101 #[inline]
1102 fn next(&mut self) -> Option<Self::Item> {
1103 if self.current_window >= self.total_windows.get() {
1104 return None;
1105 }
1106
1107 let start_pos = self.current_position;
1108 let end_pos = start_pos + self.window_size.get();
1109 // Copy the stored reference so that slice_samples borrows from the
1110 // audio data rather than from the short-lived &mut self borrow.
1111 let audio = self.audio;
1112
1113 let window = if end_pos <= self.total_samples.get() {
1114 // Complete window within bounds
1115 audio
1116 .slice_samples(start_pos..end_pos)
1117 .ok()
1118 .map(super::repr::AudioSamples::into_owned)
1119 } else {
1120 // Window extends beyond available data
1121 match self.padding_mode {
1122 PaddingMode::Zero => {
1123 // Zero-pad to maintain consistent window size
1124 let available_samples = self.total_samples.get().saturating_sub(start_pos);
1125 match &audio.data {
1126 AudioData::Mono(_) => {
1127 // Add available samples
1128 let starting_slice = if available_samples > 0 {
1129 let slice = audio
1130 .slice_samples(start_pos..self.total_samples.get())
1131 .ok()?
1132 .into_owned();
1133 Some(slice)
1134 } else {
1135 None
1136 };
1137
1138 let silence_samples = self.window_size.get() - available_samples;
1139 let length = NonZeroUsize::new(silence_samples)?;
1140 let silence = if silence_samples > 0 {
1141 let silence =
1142 AudioSamples::<T>::zeros_mono(length, audio.sample_rate);
1143 Some(silence)
1144 } else {
1145 return starting_slice;
1146 };
1147
1148 match (starting_slice, silence) {
1149 (None, None) => None,
1150 (None, Some(silence)) => Some(silence),
1151 (Some(starting_slice), None) => Some(starting_slice),
1152 (Some(s), Some(z)) => {
1153 let slices = vec![s, z];
1154 let slices = NonEmptyVec::new(slices).ok()?;
1155 Some(AudioSamples::concatenate_owned(slices).ok()?)
1156 }
1157 }
1158 }
1159 AudioData::Multi(_) => {
1160 let interleaved_slice = if available_samples > 0 {
1161 let slice = audio
1162 .slice_samples(start_pos..self.total_samples.get())
1163 .ok()?
1164 .into_owned();
1165 Some(slice)
1166 } else {
1167 None
1168 };
1169
1170 // Zero-pad remainder
1171 let remaining_samples = self.window_size.get() - available_samples;
1172 if remaining_samples == 0 {
1173 return interleaved_slice;
1174 }
1175
1176 let length = NonZeroUsize::new(remaining_samples)?;
1177
1178 let silence = AudioSamples::<T>::zeros_multi_channel(
1179 audio.num_channels(),
1180 length,
1181 audio.sample_rate,
1182 );
1183
1184 match interleaved_slice {
1185 None => Some(silence),
1186 Some(slice) => {
1187 AudioSamples::concatenate_owned(non_empty_vec![slice, silence])
1188 .ok()
1189 }
1190 }
1191 }
1192 }
1193 }
1194 PaddingMode::None => {
1195 // Return available samples without padding
1196 let available_samples = self.total_samples.get().saturating_sub(start_pos);
1197 if available_samples == 0 {
1198 return None;
1199 }
1200
1201 audio
1202 .slice_samples(start_pos..self.total_samples.get())
1203 .ok()
1204 .map(super::repr::AudioSamples::into_owned)
1205 }
1206 PaddingMode::Skip => {
1207 // Skip incomplete windows
1208 return None;
1209 }
1210 }
1211 };
1212
1213 self.current_position += self.hop_size.get();
1214 self.current_window += 1;
1215 window
1216 }
1217
1218 #[inline]
1219 fn size_hint(&self) -> (usize, Option<usize>) {
1220 let remaining = self.total_windows.get() - self.current_window;
1221 (remaining, Some(remaining))
1222 }
1223}
1224
1225#[cfg(feature = "editing")]
1226impl<T> ExactSizeIterator for WindowIterator<'_, '_, T> where T: StandardSample {}
1227
1228#[cfg(test)]
1229mod tests {
1230 use crate::AudioSamples;
1231 #[cfg(feature = "editing")]
1232 use crate::PaddingMode;
1233 use crate::sample_rate;
1234 use ndarray::{Array1, array};
1235 use non_empty_slice::non_empty_vec;
1236
1237 #[test]
1238 fn test_frame_iterator_mono() {
1239 let audio = AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0, 5.0], sample_rate!(44100))
1240 .unwrap();
1241 audio
1242 .frames()
1243 .zip([1.0f32, 2.0, 3.0, 4.0, 5.0])
1244 .for_each(|(f, x)| {
1245 assert_eq!(f.to_interleaved_vec(), non_empty_vec![x]);
1246 });
1247 }
1248
1249 #[test]
1250 fn test_frame_iterator_stereo() {
1251 let audio = AudioSamples::new_multi_channel(
1252 array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
1253 sample_rate!(44100),
1254 )
1255 .unwrap();
1256
1257 // Borrowing-first behavior: work with frames directly, don't collect into Vec
1258 let expected_frames = vec![vec![1.0, 4.0], vec![2.0, 5.0], vec![3.0, 6.0]];
1259
1260 for (i, frame) in audio.frames().enumerate() {
1261 assert_eq!(frame.to_interleaved_vec().to_vec(), expected_frames[i]);
1262 }
1263 }
1264
1265 #[test]
1266 fn test_channel_iterator_mono() {
1267 let audio =
1268 AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0], sample_rate!(44100)).unwrap();
1269
1270 // Borrowing-first behavior: work with channels directly
1271 let mut channel_count = 0;
1272 for channel in audio.channels() {
1273 channel_count += 1;
1274 assert_eq!(
1275 channel.to_interleaved_vec(),
1276 non_empty_vec![1.0, 2.0, 3.0, 4.0]
1277 );
1278 }
1279 assert_eq!(channel_count, 1);
1280 }
1281
1282 #[test]
1283 fn test_channel_iterator_stereo() {
1284 let audio = AudioSamples::new_multi_channel(
1285 array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
1286 sample_rate!(44100),
1287 )
1288 .unwrap();
1289
1290 // Borrowing-first behavior: work with channels directly
1291 let expected_channels = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
1292
1293 for (i, channel) in audio.channels().enumerate() {
1294 assert_eq!(channel.as_mono().unwrap().to_vec(), expected_channels[i]);
1295 }
1296 }
1297
1298 #[cfg(feature = "editing")]
1299 #[test]
1300 fn test_window_iterator_no_overlap() {
1301 let audio =
1302 AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], sample_rate!(44100))
1303 .unwrap();
1304 let windows: Vec<AudioSamples<f32>> =
1305 audio.windows(crate::nzu!(3), crate::nzu!(3)).collect();
1306
1307 assert_eq!(windows.len(), 2);
1308 assert_eq!(windows[0].as_mono().unwrap().to_vec(), vec![1.0, 2.0, 3.0]);
1309 assert_eq!(windows[1].as_mono().unwrap().to_vec(), vec![4.0, 5.0, 6.0]);
1310 }
1311
1312 #[cfg(feature = "editing")]
1313 #[test]
1314 fn test_window_iterator_with_overlap() {
1315 let audio =
1316 AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], sample_rate!(44100))
1317 .unwrap();
1318 let windows: Vec<AudioSamples<f32>> =
1319 audio.windows(crate::nzu!(4), crate::nzu!(2)).collect();
1320
1321 // For 6 samples, window_size=4, hop_size=2:
1322 // Window 1: position 0-3 (samples 0,1,2,3)
1323 // Window 2: position 2-5 (samples 2,3,4,5)
1324 // Window 3: position 4-7 (samples 4,5 + 2 zeros for padding)
1325 assert_eq!(windows.len(), 3);
1326 assert_eq!(
1327 windows[0].as_mono().unwrap().to_vec(),
1328 vec![1.0, 2.0, 3.0, 4.0]
1329 );
1330 assert_eq!(
1331 windows[1].as_mono().unwrap().to_vec(),
1332 vec![3.0, 4.0, 5.0, 6.0]
1333 );
1334 assert_eq!(
1335 windows[2].as_mono().unwrap().to_vec(),
1336 vec![5.0, 6.0, 0.0, 0.0]
1337 );
1338 }
1339
1340 #[cfg(feature = "editing")]
1341 #[test]
1342 fn test_window_iterator_zero_padding() {
1343 let audio = AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0, 5.0], sample_rate!(44100))
1344 .unwrap();
1345 let windows: Vec<AudioSamples<f32>> = audio
1346 .windows(crate::nzu!(4), crate::nzu!(3))
1347 .with_padding_mode(PaddingMode::Zero)
1348 .collect();
1349
1350 assert_eq!(windows.len(), 2);
1351 assert_eq!(
1352 windows[0].as_mono().unwrap().to_vec(),
1353 vec![1.0, 2.0, 3.0, 4.0]
1354 );
1355 assert_eq!(
1356 windows[1].as_mono().unwrap().to_vec(),
1357 vec![4.0, 5.0, 0.0, 0.0]
1358 ); // Zero-padded
1359 }
1360
1361 #[cfg(feature = "editing")]
1362 #[test]
1363 fn test_window_iterator_no_padding() {
1364 let audio = AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0, 5.0], sample_rate!(44100))
1365 .unwrap();
1366 let windows: Vec<AudioSamples<f32>> = audio
1367 .windows(crate::nzu!(4), crate::nzu!(3))
1368 .with_padding_mode(PaddingMode::None)
1369 .collect();
1370
1371 assert_eq!(windows.len(), 2);
1372 assert_eq!(
1373 windows[0].as_mono().unwrap().to_vec(),
1374 vec![1.0, 2.0, 3.0, 4.0]
1375 );
1376 assert_eq!(windows[1].as_mono().unwrap().to_vec(), vec![4.0, 5.0]); // Incomplete window
1377 }
1378
1379 #[cfg(feature = "editing")]
1380 #[test]
1381 fn test_window_iterator_skip_padding() {
1382 let audio = AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0, 5.0], sample_rate!(44100))
1383 .unwrap();
1384 let windows: Vec<AudioSamples<f32>> = audio
1385 .windows(crate::nzu!(4), crate::nzu!(3))
1386 .with_padding_mode(PaddingMode::Skip)
1387 .collect();
1388
1389 assert_eq!(windows.len(), 1);
1390 assert_eq!(
1391 windows[0].as_mono().unwrap().to_vec(),
1392 vec![1.0, 2.0, 3.0, 4.0]
1393 );
1394 }
1395
1396 #[cfg(feature = "editing")]
1397 #[test]
1398 fn test_window_iterator_stereo_interleaved() {
1399 let audio = AudioSamples::new_multi_channel(
1400 array![[1.0f32, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]],
1401 sample_rate!(44100),
1402 )
1403 .unwrap();
1404 let windows: Vec<AudioSamples<f32>> =
1405 audio.windows(crate::nzu!(2), crate::nzu!(2)).collect();
1406
1407 assert_eq!(windows.len(), 2);
1408 // First window: samples 0,1 interleaved across channels
1409 assert_eq!(
1410 windows[0].to_interleaved_vec(),
1411 non_empty_vec![1.0, 5.0, 2.0, 6.0]
1412 );
1413 // Second window: samples 2,3 interleaved across channels
1414 assert_eq!(
1415 windows[1].to_interleaved_vec(),
1416 non_empty_vec![3.0, 7.0, 4.0, 8.0]
1417 );
1418 }
1419
1420 #[test]
1421 fn test_exact_size_iterators() {
1422 let audio = AudioSamples::new_multi_channel(
1423 array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
1424 sample_rate!(44100),
1425 )
1426 .unwrap();
1427
1428 let frame_iter = audio.frames();
1429 assert_eq!(frame_iter.len(), 3);
1430
1431 let channel_iter = audio.channels();
1432 assert_eq!(channel_iter.len(), 2);
1433
1434 #[cfg(feature = "editing")]
1435 {
1436 let window_iter = audio.windows(crate::nzu!(2), crate::nzu!(1));
1437 assert_eq!(window_iter.len(), 3); // (3-2)/1 + 1 = 2, plus padding = 3
1438 }
1439 }
1440
1441 #[test]
1442 fn test_multiple_iterators_from_same_audio() {
1443 // This test verifies that our raw pointer approach allows multiple iterators
1444 let audio = AudioSamples::new_multi_channel(
1445 array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
1446 sample_rate!(44100),
1447 )
1448 .unwrap();
1449
1450 // This should compile and work correctly
1451 let frames = audio.frames();
1452 let channels = audio.channels();
1453 #[cfg(feature = "editing")]
1454 let windows = audio.windows(crate::nzu!(2), crate::nzu!(1));
1455
1456 // Verify they all work independently
1457 assert_eq!(frames.len(), 3);
1458 assert_eq!(channels.len(), 2);
1459 #[cfg(feature = "editing")]
1460 assert_eq!(windows.len(), 3);
1461 }
1462
1463 // ==============================
1464 // MUTABLE ITERATOR TESTS
1465 // ==============================
1466
1467 #[test]
1468 fn test_frame_iterator_mut_stereo() {
1469 let mut audio = AudioSamples::new_multi_channel(
1470 array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
1471 sample_rate!(44100),
1472 )
1473 .unwrap();
1474
1475 let expected = array![[0.5f32, 1.0, 1.5], [6.0, 7.5, 9.0]];
1476
1477 // Apply different processing to each channel
1478 audio.apply_to_channel_data(|ch, channel_data| {
1479 let gain = if ch == 0 { 0.5 } else { 1.5 };
1480 for sample in channel_data {
1481 *sample *= gain;
1482 }
1483 });
1484
1485 assert_eq!(audio.as_multi_channel().unwrap(), &expected);
1486 }
1487
1488 #[test]
1489 fn test_frame_iterator_mut_individual_access() {
1490 let mut audio =
1491 AudioSamples::new_multi_channel(array![[1.0f32, 2.0], [3.0, 4.0]], sample_rate!(44100))
1492 .unwrap();
1493
1494 let expected = array![[10.0f32, 20.0], [3.0, 4.0]];
1495
1496 // Modify only the left channel (channel 0)
1497 audio.apply_to_channel_data(|ch, channel_data| {
1498 if ch == 0 {
1499 for sample in channel_data {
1500 *sample *= 10.0;
1501 }
1502 }
1503 // Leave right channel unchanged
1504 });
1505
1506 assert_eq!(audio.as_multi_channel().unwrap(), &expected);
1507 }
1508
1509 #[test]
1510 fn test_channel_iterator_mut_mono() {
1511 let mut audio =
1512 AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0], sample_rate!(44100)).unwrap();
1513
1514 audio.apply_to_channel_data(|_ch, channel_data| {
1515 for sample in channel_data {
1516 *sample += 10.0;
1517 }
1518 });
1519
1520 assert_eq!(audio.as_mono().unwrap(), &array![11.0f32, 12.0, 13.0, 14.0]);
1521 }
1522
1523 #[test]
1524 fn test_channel_iterator_mut_stereo() {
1525 let mut audio = AudioSamples::new_multi_channel(
1526 array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
1527 sample_rate!(44100),
1528 )
1529 .unwrap();
1530
1531 let expected = array![[0.5f32, 1.0, 1.5], [8.0, 10.0, 12.0]];
1532
1533 // Apply different processing to each channel
1534 audio.apply_to_channel_data(|ch, channel_data| {
1535 let gain = if ch == 0 { 0.5 } else { 2.0 };
1536 for sample in channel_data {
1537 *sample *= gain;
1538 }
1539 });
1540
1541 assert_eq!(audio.as_multi_channel().unwrap(), &expected);
1542 }
1543
1544 #[test]
1545 fn test_window_iterator_mut_mono() {
1546 let mut audio =
1547 AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], sample_rate!(44100))
1548 .unwrap();
1549
1550 // Apply windowed processing (non-overlapping)
1551 audio.apply_to_windows(3, 3, |_window_idx, window_data| {
1552 for sample in window_data {
1553 *sample *= 0.5;
1554 }
1555 });
1556
1557 assert_eq!(
1558 audio.as_mono().unwrap(),
1559 &array![0.5f32, 1.0, 1.5, 2.0, 2.5, 3.0]
1560 );
1561 }
1562
1563 #[test]
1564 fn test_window_iterator_mut_stereo() {
1565 let mut audio = AudioSamples::new_multi_channel(
1566 array![[1.0f32, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]],
1567 sample_rate!(44100),
1568 )
1569 .unwrap();
1570
1571 let expected = array![[0.8f32, 1.6, 2.4, 3.2], [6.0, 7.2, 8.4, 9.6]];
1572
1573 // Apply windowed processing (non-overlapping)
1574 // For multi-channel, apply_to_windows provides interleaved data
1575 audio.apply_to_windows(2, 2, |_window_idx, window_data| {
1576 // window_data is interleaved: [L0, R0, L1, R1, ...] for 2-sample window
1577 let samples_per_channel = window_data.len() / 2; // 2 channels
1578 for sample_idx in 0..samples_per_channel {
1579 let left_idx = sample_idx * 2;
1580 let right_idx = sample_idx * 2 + 1;
1581 window_data[left_idx] *= 0.8; // Left channel gain
1582 window_data[right_idx] *= 1.2; // Right channel gain
1583 }
1584 });
1585
1586 let result = audio.as_multi_channel().unwrap();
1587 for (i, (&actual, &expected)) in result.iter().zip(expected.iter()).enumerate() {
1588 assert!(
1589 (actual - expected).abs() < 1e-6,
1590 "Mismatch at index {}: {} != {} (diff: {})",
1591 i,
1592 actual,
1593 expected,
1594 (actual - expected).abs()
1595 );
1596 }
1597 }
1598
1599 #[test]
1600 fn test_window_function_application() {
1601 let mut audio =
1602 AudioSamples::new_mono(array![1.0f32, 1.0, 1.0, 1.0], sample_rate!(44100)).unwrap();
1603
1604 // Apply Hann window function
1605 audio.apply_to_windows(4, 4, |_window_idx, window_data| {
1606 let window_size = window_data.len();
1607 for (i, sample) in window_data.iter_mut().enumerate() {
1608 let hann_weight = 0.5
1609 * (1.0
1610 - (2.0 * std::f32::consts::PI * i as f32 / (window_size - 1) as f32).cos());
1611 *sample *= hann_weight;
1612 }
1613 });
1614
1615 // Check that Hann window was applied (values should be different from 1.0)
1616 let result = audio.as_mono().unwrap();
1617 assert!(result[0] < 1.0); // Should be close to 0
1618 assert!(result[1] > 0.5); // Should be around 0.75
1619 assert!(result[2] > 0.5); // Should be around 0.75
1620 assert!(result[3] < 1.0); // Should be close to 0
1621 }
1622
1623 #[test]
1624 fn test_performance_comparison_apply_vs_iterator() {
1625 // This test demonstrates when to use each approach
1626 let mut audio1 =
1627 AudioSamples::new_mono(Array1::<f32>::ones(1000), sample_rate!(44100)).unwrap();
1628 let mut audio2 = audio1.clone();
1629
1630 // Method 1: Using optimized apply (recommended for simple operations)
1631 audio1.apply(|sample| sample * 0.5);
1632
1633 // Method 2: Using convenience method (alternative for complex operations)
1634 audio2.apply_to_frames(|_frame_idx, frame_data| {
1635 for sample in frame_data {
1636 *sample *= 0.5;
1637 }
1638 });
1639
1640 // Results should be identical
1641 assert_eq!(audio1.as_mono().unwrap(), audio2.as_mono().unwrap());
1642 }
1643}