Skip to main content

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}