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    #[cfg(feature = "editing")]
182    /// Returns a zero-copy, borrowing iterator over fully-contained windows.
183    ///
184    /// Unlike [`windows`](AudioSampleIterators::windows), which yields an owned
185    /// [`AudioSamples`] per window (re-copying overlapping data), this iterator
186    /// yields a [`WindowView`] that borrows directly into the underlying buffer.
187    /// No allocation or copying occurs per window, making it well suited to the
188    /// STFT use case where windows overlap heavily.
189    ///
190    /// Only windows that lie fully within the signal are yielded — equivalent to
191    /// [`PaddingMode::Skip`]. Padded or partial trailing windows require the
192    /// owning [`windows`](AudioSampleIterators::windows).
193    ///
194    /// # Arguments
195    ///
196    /// – `window_size` — number of samples per channel in each window.
197    /// – `hop_size` — number of samples to advance between window starts.
198    ///
199    /// # Returns
200    ///
201    /// A [`WindowRefIterator`] yielding one borrowing [`WindowView`] per window.
202    /// If `window_size > samples_per_channel`, the iterator yields zero windows.
203    ///
204    /// # Panics
205    ///
206    /// Does not panic.
207    ///
208    /// ## Examples
209    ///
210    /// See [`AudioSamples::windows_ref`] for a runnable example.
211    fn windows_ref<'iter>(
212        &'iter self,
213        window_size: NonZeroUsize,
214        hop_size: NonZeroUsize,
215    ) -> WindowRefIterator<'iter, 'a, T>
216    where
217        'a: 'iter;
218}
219
220impl<'a, T> AudioSamples<'a, T>
221where
222    T: StandardSample,
223{
224    /// Returns an iterator over frames, where each frame is a snapshot of one
225    /// sample from each channel at the same point in time.
226    ///
227    /// For mono audio, each frame contains exactly one sample. For multi-channel
228    /// audio, each frame contains one sample per channel in channel-index order.
229    ///
230    /// # Returns
231    ///
232    /// A [`FrameIterator`] that yields one [`AudioSamples`] view per time index.
233    /// The iterator yields exactly `self.samples_per_channel()` frames.
234    ///
235    /// # Panics
236    ///
237    /// Does not panic.
238    ///
239    /// ## Examples
240    ///
241    /// ```
242    /// use audio_samples::{AudioSamples, sample_rate};
243    /// use ndarray::array;
244    ///
245    /// let audio = AudioSamples::new_multi_channel(
246    ///     array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
247    ///     sample_rate!(44100),
248    /// ).unwrap();
249    ///
250    /// // Three time steps → three frames.
251    /// assert_eq!(audio.frames().count(), 3);
252    ///
253    /// // Each frame spans all channels.
254    /// for frame in audio.frames() {
255    ///     assert_eq!(frame.num_channels().get(), 2);
256    /// }
257    /// ```
258    #[inline]
259    #[must_use]
260    pub fn frames<'iter>(&'iter self) -> FrameIterator<'iter, 'a, T>
261    where
262        'a: 'iter,
263    {
264        FrameIterator::new(self)
265    }
266
267    /// Returns an iterator over complete channels.
268    ///
269    /// Each iteration yields the full temporal sequence of samples belonging to
270    /// one channel. Channels are yielded in increasing channel-index order.
271    ///
272    /// Each yielded value is an owned [`AudioSamples`] instance containing exactly
273    /// one mono channel. This involves allocation and data copying.
274    ///
275    /// # Returns
276    ///
277    /// A [`ChannelIterator`] yielding one owned [`AudioSamples`] per channel.
278    /// The iterator yields exactly `self.num_channels()` items.
279    ///
280    /// # Panics
281    ///
282    /// Does not panic.
283    ///
284    /// ## Examples
285    ///
286    /// ```
287    /// use audio_samples::{AudioSamples, sample_rate};
288    /// use ndarray::array;
289    ///
290    /// let audio = AudioSamples::new_multi_channel(
291    ///     array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
292    ///     sample_rate!(44100),
293    /// ).unwrap();
294    ///
295    /// let channels: Vec<_> = audio.channels().collect();
296    /// assert_eq!(channels.len(), 2);
297    /// assert_eq!(channels[0].samples_per_channel().get(), 3);
298    /// ```
299    #[inline]
300    #[must_use]
301    pub fn channels<'iter>(&'iter self) -> ChannelIterator<'iter, 'a, T> {
302        ChannelIterator::new(self)
303    }
304
305    #[cfg(feature = "editing")]
306    /// Returns an iterator over fixed-size, optionally overlapping windows.
307    ///
308    /// Each window covers `window_size` samples per channel. Successive windows
309    /// start `hop_size` samples apart, so windows overlap when `hop_size < window_size`.
310    ///
311    /// The default boundary strategy is [`PaddingMode::Zero`], which zero-pads the
312    /// last window when the signal does not divide evenly. Call
313    /// [`WindowIterator::with_padding_mode`] on the returned iterator to change
314    /// this behaviour.
315    ///
316    /// # Arguments
317    ///
318    /// – `window_size` — number of samples per channel in each window.
319    /// – `hop_size` — number of samples to advance between window starts.
320    ///
321    /// # Returns
322    ///
323    /// A [`WindowIterator`] yielding one owned [`AudioSamples`] per window.
324    ///
325    /// # Panics
326    ///
327    /// Does not panic.
328    ///
329    /// ## Examples
330    ///
331    /// ```
332    /// # #[cfg(feature = "editing")] {
333    /// use audio_samples::{AudioSamples, sample_rate};
334    /// use ndarray::array;
335    /// use std::num::NonZeroUsize;
336    ///
337    /// let audio = AudioSamples::new_mono(
338    ///     array![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0],
339    ///     sample_rate!(44100),
340    /// ).unwrap();
341    ///
342    /// // Non-overlapping windows of size 3.
343    /// let windows: Vec<_> = audio
344    ///     .windows(NonZeroUsize::new(3).unwrap(), NonZeroUsize::new(3).unwrap())
345    ///     .collect();
346    /// assert_eq!(windows.len(), 2);
347    /// assert_eq!(windows[0].samples_per_channel().get(), 3);
348    /// # }
349    /// ```
350    #[inline]
351    #[must_use]
352    pub fn windows<'iter>(
353        &'iter self,
354        window_size: NonZeroUsize,
355        hop_size: NonZeroUsize,
356    ) -> WindowIterator<'iter, 'a, T>
357    where
358        'a: 'iter,
359    {
360        WindowIterator::new(self, window_size, hop_size)
361    }
362
363    #[cfg(feature = "editing")]
364    /// Returns a zero-copy, borrowing iterator over fully-contained windows.
365    ///
366    /// Each yielded [`WindowView`] borrows directly into this `AudioSamples`'
367    /// underlying buffer (lifetime-tied to `&self`); no per-window allocation or
368    /// copying is performed. This is the preferred iterator for read-only
369    /// windowed analysis such as STFT, where the owning
370    /// [`windows`](AudioSamples::windows) iterator would wastefully re-copy
371    /// overlapping samples.
372    ///
373    /// Only windows fully contained within the signal are yielded — equivalent
374    /// to [`PaddingMode::Skip`]. Trailing partial windows are not produced; if a
375    /// padded final window is required, use the owning
376    /// [`windows`](AudioSamples::windows) iterator instead.
377    ///
378    /// # Arguments
379    ///
380    /// – `window_size` — number of samples per channel in each window.
381    /// – `hop_size` — number of samples to advance between window starts.
382    ///
383    /// # Returns
384    ///
385    /// A [`WindowRefIterator`] yielding one borrowing [`WindowView`] per window.
386    /// If `window_size > samples_per_channel`, the iterator yields zero windows
387    /// (it does not panic).
388    ///
389    /// # Panics
390    ///
391    /// Does not panic.
392    ///
393    /// ## Examples
394    ///
395    /// ```
396    /// # #[cfg(feature = "editing")] {
397    /// use audio_samples::{AudioSamples, sample_rate};
398    /// use audio_samples::iterators::WindowView;
399    /// use ndarray::array;
400    /// use std::num::NonZeroUsize;
401    ///
402    /// let audio = AudioSamples::new_mono(
403    ///     array![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0],
404    ///     sample_rate!(44100),
405    /// ).unwrap();
406    ///
407    /// // Overlapping windows of size 4, hop 2 — zero-copy views.
408    /// let windows: Vec<_> = audio
409    ///     .windows_ref(NonZeroUsize::new(4).unwrap(), NonZeroUsize::new(2).unwrap())
410    ///     .collect();
411    /// assert_eq!(windows.len(), 2); // only fully-contained windows
412    /// match windows[0] {
413    ///     WindowView::Mono(view) => assert_eq!(view.to_vec(), vec![1.0, 2.0, 3.0, 4.0]),
414    ///     WindowView::Multi(_) => unreachable!(),
415    /// }
416    /// # }
417    /// ```
418    #[inline]
419    #[must_use]
420    pub fn windows_ref<'iter>(
421        &'iter self,
422        window_size: NonZeroUsize,
423        hop_size: NonZeroUsize,
424    ) -> WindowRefIterator<'iter, 'a, T>
425    where
426        'a: 'iter,
427    {
428        WindowRefIterator::new(self, window_size, hop_size)
429    }
430
431    /// Applies a mutable function to every frame without requiring a borrowing-safe iterator.
432    ///
433    /// The callback receives the frame index and a mutable slice containing the samples for
434    /// that frame across all channels. For mono audio the slice has length 1. For
435    /// multi-channel audio the slice is a temporary buffer ordered by channel index;
436    /// changes are written back into the underlying storage after the callback returns.
437    ///
438    /// Use this method when in-place, frame-wise mutation is needed and the immutable
439    /// [`AudioSamples::frames`] iterator is insufficient.
440    ///
441    /// # Arguments
442    ///
443    /// – `f` — a closure of the form `FnMut(frame_index: usize, frame_samples: &mut [T])`.
444    ///   – `frame_index` — zero-based index of the current frame.
445    ///   – `frame_samples` — mutable slice of length `num_channels()` for the current frame.
446    ///
447    /// # Returns
448    ///
449    /// `()` — the audio is modified in place.
450    ///
451    /// # Panics
452    ///
453    /// Does not panic.
454    ///
455    /// ## Examples
456    ///
457    /// ```
458    /// use audio_samples::{AudioSamples, sample_rate};
459    /// use ndarray::array;
460    ///
461    /// let mut audio = AudioSamples::new_multi_channel(
462    ///     array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
463    ///     sample_rate!(44100),
464    /// ).unwrap();
465    ///
466    /// // Double every sample frame-by-frame.
467    /// audio.apply_to_frames(|_frame_idx, frame| {
468    ///     for s in frame { *s *= 2.0; }
469    /// });
470    ///
471    /// assert_eq!(
472    ///     audio.as_multi_channel().unwrap(),
473    ///     &array![[2.0f32, 4.0, 6.0], [8.0, 10.0, 12.0]],
474    /// );
475    /// ```
476    #[inline]
477    pub fn apply_to_frames<F>(&mut self, mut f: F)
478    where
479        F: FnMut(usize, &mut [T]), // (frame_index, frame_samples)
480    {
481        match self.data_mut() {
482            AudioData::Mono(arr) => {
483                for (frame_idx, sample) in arr.iter_mut().enumerate() {
484                    f(frame_idx, std::slice::from_mut(sample));
485                }
486            }
487            AudioData::Multi(arr) => {
488                let (channels, samples_per_channel) = arr.dim();
489
490                // Hoist a single reusable frame buffer outside the loop; clear and
491                // refill it each iteration so the callback sees identical contents
492                // to the previous per-iteration `Vec::with_capacity` allocation.
493                let mut frame = Vec::with_capacity(channels.get());
494                for frame_idx in 0..samples_per_channel.get() {
495                    frame.clear();
496                    for ch in 0..channels.get() {
497                        frame.push(arr[[ch, frame_idx]]);
498                    }
499
500                    f(frame_idx, &mut frame);
501
502                    for ch in 0..channels.get() {
503                        arr[[ch, frame_idx]] = frame[ch];
504                    }
505                }
506            }
507        }
508    }
509
510    /// Applies a mutable function to each channel's contiguous sample slice.
511    ///
512    /// This is the fallible counterpart to [`AudioSamples::apply_to_channel_data`].
513    /// It requires that the underlying ndarray storage is contiguous in memory.
514    /// Non-contiguous layouts (such as after certain in-place reversals or
515    /// non-standard strides) will cause this method to return an error.
516    ///
517    /// The callback receives the channel index and a mutable slice of all samples
518    /// for that channel.
519    ///
520    /// # Arguments
521    ///
522    /// – `f` — a closure of the form `FnMut(channel_index: usize, channel_samples: &mut [T])`.
523    ///   – `channel_index` — zero-based index of the channel being processed.
524    ///   – `channel_samples` — mutable slice of all samples belonging to that channel.
525    ///
526    /// # Returns
527    ///
528    /// `Ok(())` if all channels were processed successfully.
529    ///
530    /// # Errors
531    ///
532    /// Returns [crate::AudioSampleError::Layout] with variant `NonContiguous` if the
533    /// underlying multi-channel storage is not contiguous in memory.
534    ///
535    /// # Panics
536    ///
537    /// Does not panic.
538    ///
539    /// ## Examples
540    ///
541    /// ```
542    /// use audio_samples::{AudioSamples, sample_rate};
543    /// use ndarray::array;
544    ///
545    /// let mut audio = AudioSamples::new_multi_channel(
546    ///     array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
547    ///     sample_rate!(44100),
548    /// ).unwrap();
549    ///
550    /// // Halve channel 0, double channel 1.
551    /// audio.try_apply_to_channel_data(|ch, samples| {
552    ///     let gain = if ch == 0 { 0.5 } else { 2.0 };
553    ///     for s in samples { *s *= gain; }
554    /// }).unwrap();
555    ///
556    /// assert_eq!(
557    ///     audio.as_multi_channel().unwrap(),
558    ///     &array![[0.5f32, 1.0, 1.5], [8.0, 10.0, 12.0]],
559    /// );
560    /// ```
561    #[inline]
562    pub fn try_apply_to_channel_data<F>(&mut self, mut f: F) -> AudioSampleResult<()>
563    where
564        F: FnMut(usize, &mut [T]), // (channel_index, channel_samples)
565    {
566        match self.data_mut() {
567            AudioData::Mono(arr) => {
568                let slice = arr.as_slice_mut();
569                f(0, slice);
570            }
571            AudioData::Multi(arr) => {
572                let (channels, samples_per_channel) = arr.dim();
573                let samples_per_channel = samples_per_channel.get();
574                let slice = arr.as_slice_mut().ok_or_else(|| {
575                    AudioSampleError::Layout(LayoutError::NonContiguous {
576                        operation: "multi-channel iterator access".to_string(),
577                        layout_type: "non-contiguous multi-channel data".to_string(),
578                    })
579                })?;
580
581                for ch in 0..channels.get() {
582                    let start_idx = ch * samples_per_channel;
583                    let channel_slice = &mut slice[start_idx..start_idx + samples_per_channel];
584                    f(ch, channel_slice);
585                }
586            }
587        }
588        Ok(())
589    }
590
591    /// Applies a mutable function to each channel's contiguous sample slice.
592    ///
593    /// This is the infallible counterpart to [`AudioSamples::try_apply_to_channel_data`].
594    /// It panics if the underlying storage is not contiguous; prefer the fallible
595    /// variant when working with audio that may have non-standard memory layouts.
596    ///
597    /// The callback receives the channel index and a mutable slice of all samples
598    /// for that channel.
599    ///
600    /// # Arguments
601    ///
602    /// – `f` — a closure of the form `FnMut(channel_index: usize, channel_samples: &mut [T])`.
603    ///   – `channel_index` — zero-based index of the channel being processed.
604    ///   – `channel_samples` — mutable slice of all samples belonging to that channel.
605    ///
606    /// # Returns
607    ///
608    /// `()` — the audio is modified in place.
609    ///
610    /// # Panics
611    ///
612    /// Panics if the underlying storage is not contiguous in memory. Use
613    /// [`AudioSamples::try_apply_to_channel_data`] to handle non-contiguous inputs
614    /// without panicking.
615    ///
616    /// ## Examples
617    ///
618    /// ```
619    /// use audio_samples::{AudioSamples, sample_rate};
620    /// use ndarray::array;
621    ///
622    /// let mut audio = AudioSamples::new_mono(
623    ///     array![1.0f32, 2.0, 3.0, 4.0],
624    ///     sample_rate!(44100),
625    /// ).unwrap();
626    ///
627    /// // Add 10.0 to every sample.
628    /// audio.apply_to_channel_data(|_ch, samples| {
629    ///     for s in samples { *s += 10.0; }
630    /// });
631    ///
632    /// assert_eq!(audio.as_mono().unwrap(), &array![11.0f32, 12.0, 13.0, 14.0]);
633    /// ```
634    #[inline]
635    pub fn apply_to_channel_data<F>(&mut self, mut f: F)
636    where
637        F: FnMut(usize, &mut [T]), // (channel_index, channel_samples)
638    {
639        self.try_apply_to_channel_data(|ch, data| f(ch, data))
640            .expect("apply_to_channel_data requires contiguous storage; use try_apply_to_channel_data to handle non-contiguous inputs");
641    }
642
643    /// Applies a mutable function to each temporal window of audio data.
644    ///
645    /// For mono audio, the callback receives a mutable slice directly into the
646    /// underlying buffer for each window. For multi-channel audio, the callback
647    /// receives a temporary interleaved buffer of length `window_size * num_channels`
648    /// laid out as `[ch0_s0, ch1_s0, …, ch0_s1, ch1_s1, …]`; changes are
649    /// written back into the underlying storage after the callback returns.
650    ///
651    /// Only fully-contained windows are visited; trailing samples that do not
652    /// form a complete window are not passed to the callback.
653    ///
654    /// Use this method for in-place windowed processing, such as applying window
655    /// functions or block-wise gain changes, when the read-only
656    /// [`AudioSamples::windows`] iterator is not sufficient.
657    ///
658    /// # Arguments
659    ///
660    /// – `window_size` — number of samples per channel in each window. If zero,
661    ///   the method returns immediately.
662    /// – `hop_size` — number of samples to advance between window starts. If zero,
663    ///   the method returns immediately.
664    /// – `f` — a closure of the form `FnMut(window_index: usize, window_samples: &mut [T])`.
665    ///   – `window_index` — zero-based index of the current window.
666    ///   – `window_samples` — mutable slice for the current window. For mono audio,
667    ///     length equals `window_size`. For multi-channel audio, length equals
668    ///     `window_size * num_channels`, laid out in interleaved channel order.
669    ///
670    /// # Returns
671    ///
672    /// `()` — the audio is modified in place.
673    ///
674    /// # Panics
675    ///
676    /// Does not panic.
677    ///
678    /// ## Examples
679    ///
680    /// ```
681    /// use audio_samples::{AudioSamples, sample_rate};
682    /// use ndarray::array;
683    ///
684    /// let mut audio = AudioSamples::new_mono(
685    ///     array![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0],
686    ///     sample_rate!(44100),
687    /// ).unwrap();
688    ///
689    /// // Halve every sample using non-overlapping windows of size 3.
690    /// audio.apply_to_windows(3, 3, |_window_idx, window| {
691    ///     for s in window { *s *= 0.5; }
692    /// });
693    ///
694    /// assert_eq!(
695    ///     audio.as_mono().unwrap(),
696    ///     &array![0.5f32, 1.0, 1.5, 2.0, 2.5, 3.0],
697    /// );
698    /// ```
699    #[inline]
700    pub fn apply_to_windows<F>(&mut self, window_size: usize, hop_size: usize, mut f: F)
701    where
702        F: FnMut(usize, &mut [T]), // (window_index, window_samples)
703    {
704        let total_samples = self.samples_per_channel().get();
705        // A zero hop would advance `pos` by 0 each iteration, looping forever.
706        if total_samples == 0 || window_size == 0 || hop_size == 0 {
707            return;
708        }
709
710        match self.data_mut() {
711            AudioData::Mono(arr) => {
712                let mut window_idx = 0;
713                let mut pos = 0;
714
715                while pos + window_size <= total_samples {
716                    let slice = arr.as_slice_mut();
717                    let window_slice = &mut slice[pos..pos + window_size];
718                    f(window_idx, window_slice);
719                    pos += hop_size;
720                    window_idx += 1;
721                }
722            }
723            AudioData::Multi(arr) => {
724                let (rows, cols) = arr.dim();
725                let rows = rows.get();
726                let samples_per_channel = cols;
727
728                let mut pos = 0;
729                let mut window_idx = 0;
730
731                // Hoist a single reusable interleaved window buffer outside the
732                // loop. It is fully overwritten each iteration (every element is
733                // assigned before the callback runs), so the callback sees the
734                // same contents as the previous per-iteration `vec![T::zero(); ..]`
735                // allocation.
736                let mut window_data = vec![T::zero(); window_size * rows];
737
738                while pos + window_size <= samples_per_channel.get() {
739                    // Copy window data from each channel into interleaved buffer.
740                    for ch in 0..rows {
741                        for sample_idx in 0..window_size {
742                            let dst_idx = sample_idx * rows + ch; // Interleaved layout
743                            window_data[dst_idx] = arr[[ch, pos + sample_idx]];
744                        }
745                    }
746
747                    // Call the user function
748                    f(window_idx, &mut window_data);
749
750                    // Copy modified data back to original channels.
751                    for ch in 0..rows {
752                        for sample_idx in 0..window_size {
753                            let src_idx = sample_idx * rows + ch; // Interleaved layout
754                            arr[[ch, pos + sample_idx]] = window_data[src_idx];
755                        }
756                    }
757
758                    pos += hop_size;
759                    window_idx += 1;
760                }
761            }
762        }
763    }
764}
765
766/// Iterates over time-aligned frames of an [`AudioSamples`] instance.
767///
768/// A *frame* represents the set of samples across all channels at a single
769/// time index. For mono audio, each frame contains exactly one sample. For
770/// multi-channel audio, each frame contains one sample per channel, preserving
771/// channel alignment.
772///
773/// ## Purpose
774///
775/// `FrameIterator` provides a structured, time-centric view of audio data.
776/// It is intended for algorithms that operate on synchronous samples across
777/// channels, such as frame-wise feature extraction, analysis, or inspection.
778///
779/// The iterator yields immutable views into the underlying audio data. No
780/// reordering, resampling, or interpolation is performed.
781///
782/// ## Invariants
783///
784/// - Frames are yielded in strictly increasing temporal order.
785/// - Each yielded frame corresponds to exactly one time index.
786/// - The number of frames is equal to the number of samples per channel.
787/// - All channels are assumed to have equal length.
788///
789/// ## Assumptions and Limitations
790///
791/// This iterator assumes that the underlying [`AudioSamples`] instance is
792/// channel-aligned and immutable for the lifetime of the iterator. It is not
793/// suitable for in-place mutation or algorithms that require overlapping or
794/// non-sequential access.
795///
796/// Use higher-level windowed or transformation APIs when temporal context
797/// beyond a single frame is required.
798pub struct FrameIterator<'iter, 'a, T>
799where
800    T: StandardSample,
801    'a: 'iter,
802{
803    /// The source audio data over which frames are iterated.
804    audio: &'iter AudioSamples<'a, T>,
805    current_frame: usize,
806    total_frames: usize,
807    _phantom: PhantomData<T>,
808}
809
810impl<'iter, 'a, T> FrameIterator<'iter, 'a, T>
811where
812    T: StandardSample,
813    'a: 'iter,
814{
815    /// Constructs a new frame iterator over the given audio.
816    ///
817    /// ## Purpose
818    ///
819    /// This constructor establishes a frame-wise traversal over the provided
820    /// audio data, yielding one frame per time index.
821    ///
822    /// # Arguments
823    ///
824    /// - `audio`: The source audio to iterate over. All channels must be
825    ///   time-aligned.
826    ///
827    /// ## Behavioural Guarantees
828    ///
829    /// - The iterator will yield exactly `audio.samples_per_channel()` frames.
830    /// - Frames are yielded in deterministic order.
831    ///
832    /// # Panics
833    ///
834    /// This function does not panic.
835    #[inline]
836    #[must_use]
837    pub fn new(audio: &'iter AudioSamples<'a, T>) -> Self {
838        let total_frames = audio.samples_per_channel().get();
839        Self {
840            audio,
841            current_frame: 0,
842            total_frames,
843            _phantom: PhantomData,
844        }
845    }
846}
847
848impl<'iter, 'a, T> Iterator for FrameIterator<'iter, 'a, T>
849where
850    T: StandardSample,
851    'a: 'iter,
852{
853    type Item = AudioSamples<'iter, T>;
854
855    #[inline]
856    fn next(&mut self) -> Option<Self::Item> {
857        if self.current_frame >= self.total_frames {
858            return None;
859        }
860
861        let frame_range = self.current_frame..self.current_frame + 1;
862        self.current_frame += 1;
863        // Copy the &'iter reference so that slice_samples returns AudioSamples<'iter, T>
864        // rather than a shorter-lived borrow through &mut self.
865        let audio: &'iter AudioSamples<'a, T> = self.audio;
866        audio.slice_samples(frame_range).ok()
867    }
868
869    #[inline]
870    fn size_hint(&self) -> (usize, Option<usize>) {
871        let remaining = self.total_frames - self.current_frame;
872        (remaining, Some(remaining))
873    }
874}
875
876impl<T> ExactSizeIterator for FrameIterator<'_, '_, T> where T: StandardSample {}
877
878/// Iterates over complete channels of an [`AudioSamples`] instance.
879///
880/// Each iteration yields the full sequence of samples belonging to a single
881/// channel, independent of other channels. Channels are yielded sequentially
882/// in channel index order.
883///
884/// ## Purpose
885///
886/// `ChannelIterator` provides a channel-centric view of audio data. It is
887/// intended for workflows that process or analyse channels independently,
888/// such as per-channel filtering, statistics, or visualisation.
889///
890/// Unlike frame-based iteration, this iterator exposes the *entire temporal
891/// extent* of one channel at a time.
892///
893/// ## Behaviour and Ownership
894///
895/// Each yielded item is an owned [`AudioSamples`] instance containing exactly
896/// one channel. This reflects the fact that channel-wise slicing produces
897/// independent audio objects rather than borrowed views.
898///
899/// As a result, channel iteration involves allocation and data copying.
900/// Callers should take this into account when iterating over large audio
901/// buffers or when allocation-free access is required.
902///
903/// ## Invariants
904///
905/// - Channels are yielded in increasing channel index order.
906/// - Each channel is yielded exactly once.
907/// - The number of yielded items is equal to the number of channels.
908/// - All samples within a yielded item belong to the same channel.
909pub struct ChannelIterator<'iter, 'data, T>
910where
911    T: StandardSample,
912{
913    /// The source audio from which channels are extracted.
914    audio: &'iter AudioSamples<'data, T>,
915    current_channel: usize,
916    total_channels: usize,
917}
918
919impl<'iter, 'data, T> ChannelIterator<'iter, 'data, T>
920where
921    T: StandardSample,
922{
923    /// Constructs a new iterator over the channels of the given audio.
924    ///
925    /// ## Purpose
926    ///
927    /// This constructor establishes a channel-wise traversal over the provided
928    /// audio data, yielding one complete channel per iteration.
929    ///
930    /// # Arguments
931    ///
932    /// - `audio`: The source audio whose channels will be iterated.
933    ///
934    /// ## Behavioural Guarantees
935    ///
936    /// - The iterator will yield exactly `audio.num_channels()` items.
937    /// - Channels are yielded in deterministic order.
938    ///
939    /// # Panics
940    ///
941    /// This function does not panic.
942    #[inline]
943    #[must_use]
944    pub fn new(audio: &'iter AudioSamples<'data, T>) -> Self {
945        let total_channels = audio.num_channels().get();
946
947        Self {
948            audio,
949            current_channel: 0,
950            total_channels: total_channels as usize,
951        }
952    }
953}
954
955impl<T> Iterator for ChannelIterator<'_, '_, T>
956where
957    T: StandardSample,
958{
959    type Item = AudioSamples<'static, T>;
960    #[inline]
961    fn next(&mut self) -> Option<Self::Item> {
962        if self.current_channel >= self.total_channels {
963            return None;
964        }
965
966        // Extract only the current channel by borrowing `self.audio` directly.
967        // `slice_channels` copies just that one channel into an owned
968        // `AudioSamples<'static, T>`, so there is no O(N) deep clone of every
969        // channel per step. For a valid `AudioSamples` with `current_channel`
970        // in range, this extraction is infallible.
971        let channel = self
972            .audio
973            .slice_channels(self.current_channel..=self.current_channel)
974            .ok()?;
975
976        self.current_channel += 1;
977
978        Some(channel)
979    }
980
981    #[inline]
982    fn size_hint(&self) -> (usize, Option<usize>) {
983        let remaining = self.total_channels - self.current_channel;
984        (remaining, Some(remaining))
985    }
986}
987
988impl<T> ExactSizeIterator for ChannelIterator<'_, '_, T> where T: StandardSample {}
989
990/// Defines how window iteration behaves when a window extends beyond the
991/// available audio data.
992///
993/// `PaddingMode` controls the treatment of trailing windows whose span exceeds
994/// the number of samples per channel. The selected mode determines whether such
995/// windows are padded, truncated, or omitted entirely.
996#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
997#[non_exhaustive]
998pub enum PaddingMode {
999    /// Pads incomplete windows with zeros so that all yielded windows have
1000    /// identical length.
1001    ///
1002    /// This mode guarantees a fixed window size and a deterministic number of
1003    /// windows, which is required by many spectral and frame-based algorithms.
1004    #[default]
1005    Zero,
1006
1007    /// Yields trailing windows without padding.
1008    ///
1009    /// Windows near the end of the signal may be shorter than the configured
1010    /// window size. Callers must be prepared to handle variable-length windows.
1011    None,
1012
1013    /// Omits any window that would extend beyond the available data.
1014    ///
1015    /// Only fully contained windows are yielded. This mode produces no padding
1016    /// and no partial windows.
1017    Skip,
1018}
1019
1020/// A zero-copy, borrowing view of a single window of audio data.
1021///
1022/// Yielded by [`WindowRefIterator`] (obtained via
1023/// [`AudioSamples::windows_ref`]). Each variant borrows directly into the
1024/// underlying [`AudioSamples`] buffer for the lifetime `'a`, so iterating
1025/// performs no per-window allocation or copying.
1026///
1027/// - [`WindowView::Mono`] wraps an [`ArrayView1`](ndarray::ArrayView1) over `window_size` samples.
1028/// - [`WindowView::Multi`] wraps an [`ArrayView2`](ndarray::ArrayView2) of shape
1029///   `(channels, window_size)`.
1030///
1031/// The variant matches the channel layout of the source audio: mono audio
1032/// yields `Mono` views, multi-channel audio yields `Multi` views.
1033#[cfg(feature = "editing")]
1034#[derive(Debug, Clone, Copy)]
1035pub enum WindowView<'a, T>
1036where
1037    T: StandardSample,
1038{
1039    /// A borrowed view over one mono window of `window_size` samples.
1040    Mono(ndarray::ArrayView1<'a, T>),
1041    /// A borrowed view over one multi-channel window, shape `(channels, window_size)`.
1042    Multi(ndarray::ArrayView2<'a, T>),
1043}
1044
1045/// A zero-copy, borrowing iterator over fully-contained windows of audio data.
1046///
1047/// Obtained via [`AudioSamples::windows_ref`]. Each iteration yields a
1048/// [`WindowView`] that borrows directly into the source [`AudioSamples`] buffer,
1049/// avoiding the per-window allocation and copying performed by the owning
1050/// [`WindowIterator`]. This makes it the preferred choice for read-only,
1051/// overlapping windowed analysis such as STFT.
1052///
1053/// ## Scope
1054///
1055/// Only windows that lie *fully within* the signal are yielded — this is the
1056/// fast path equivalent to [`PaddingMode::Skip`]. Trailing partial windows are
1057/// never produced; algorithms that need a zero-padded final window must use the
1058/// owning [`AudioSamples::windows`] iterator.
1059///
1060/// ## Degenerate Parameters
1061///
1062/// If `window_size` exceeds the number of samples per channel, no window fits
1063/// and the iterator yields zero windows without panicking.
1064///
1065/// ## Invariants
1066///
1067/// - Windows are yielded in strictly increasing temporal order.
1068/// - Every yielded window has exactly `window_size` samples per channel.
1069/// - The number of yielded windows is finite and deterministic.
1070#[cfg(feature = "editing")]
1071pub struct WindowRefIterator<'iter, 'a, T>
1072where
1073    T: StandardSample,
1074    'a: 'iter,
1075{
1076    /// The source audio from which window views are borrowed.
1077    audio: &'iter AudioSamples<'a, T>,
1078    window_size: usize,
1079    hop_size: usize,
1080    total_samples: usize,
1081    current_position: usize,
1082    current_window: usize,
1083    total_windows: usize,
1084}
1085
1086#[cfg(feature = "editing")]
1087impl<'iter, 'a, T> WindowRefIterator<'iter, 'a, T>
1088where
1089    T: StandardSample,
1090    'a: 'iter,
1091{
1092    /// Constructs a new borrowing window iterator over the given audio.
1093    ///
1094    /// Only fully-contained windows are produced (equivalent to
1095    /// [`PaddingMode::Skip`]). When `window_size` exceeds the number of samples
1096    /// per channel, the iterator yields zero windows.
1097    ///
1098    /// # Panics
1099    ///
1100    /// This function does not panic.
1101    #[inline]
1102    fn new(
1103        audio: &'iter AudioSamples<'a, T>,
1104        window_size: NonZeroUsize,
1105        hop_size: NonZeroUsize,
1106    ) -> Self {
1107        let total_samples = audio.samples_per_channel().get();
1108        let window_size = window_size.get();
1109        let hop_size = hop_size.get();
1110
1111        // Count only complete windows. When the window is larger than the
1112        // signal, no complete window fits, so yield 0. `saturating_sub` mirrors
1113        // the underflow fix in `WindowIterator` (BUG 3) so `window > len` is
1114        // safe in both debug and release builds.
1115        let total_windows = if total_samples < window_size {
1116            0
1117        } else {
1118            1 + (total_samples - window_size) / hop_size
1119        };
1120
1121        Self {
1122            audio,
1123            window_size,
1124            hop_size,
1125            total_samples,
1126            current_position: 0,
1127            current_window: 0,
1128            total_windows,
1129        }
1130    }
1131}
1132
1133#[cfg(feature = "editing")]
1134impl<'iter, 'a, T> Iterator for WindowRefIterator<'iter, 'a, T>
1135where
1136    T: StandardSample,
1137    'a: 'iter,
1138{
1139    type Item = WindowView<'iter, T>;
1140
1141    #[inline]
1142    fn next(&mut self) -> Option<Self::Item> {
1143        if self.current_window >= self.total_windows {
1144            return None;
1145        }
1146
1147        let start = self.current_position;
1148        let end = start + self.window_size;
1149        debug_assert!(end <= self.total_samples);
1150
1151        // Copy the stored reference so the produced view borrows from the audio
1152        // data (lifetime `'iter`) rather than from the short-lived `&mut self`.
1153        let audio: &'iter AudioSamples<'a, T> = self.audio;
1154
1155        let view = match audio.data() {
1156            AudioData::Mono(mono) => {
1157                WindowView::Mono(mono.as_view().slice_move(ndarray::s![start..end]))
1158            }
1159            AudioData::Multi(multi) => {
1160                WindowView::Multi(multi.as_view().slice_move(ndarray::s![.., start..end]))
1161            }
1162        };
1163
1164        self.current_position += self.hop_size;
1165        self.current_window += 1;
1166        Some(view)
1167    }
1168
1169    #[inline]
1170    fn size_hint(&self) -> (usize, Option<usize>) {
1171        let remaining = self.total_windows - self.current_window;
1172        (remaining, Some(remaining))
1173    }
1174}
1175
1176#[cfg(feature = "editing")]
1177impl<T> ExactSizeIterator for WindowRefIterator<'_, '_, T> where T: StandardSample {}
1178
1179/// Iterates over fixed-size temporal windows of audio data.
1180///
1181/// Each iteration yields a contiguous block of samples spanning all channels
1182/// over a fixed temporal extent. Successive windows are offset by a configurable
1183/// hop size and may overlap depending on the chosen parameters.
1184///
1185/// ## Purpose
1186///
1187/// `WindowIterator` provides a structured abstraction for windowed audio
1188/// processing. It is intended for algorithms that operate on local temporal
1189/// context, such as spectral analysis, feature extraction, and block-based
1190/// transformations.
1191///
1192/// Window iteration is defined in terms of *time*, not storage layout. All
1193/// windows preserve channel alignment and temporal ordering.
1194///
1195/// ## Window Boundaries and Padding
1196///
1197/// When a window would extend beyond the available data, its treatment is
1198/// determined by the configured [`PaddingMode`]. Depending on this mode,
1199/// trailing windows may be padded, truncated, or skipped entirely. This choice
1200/// directly affects both the number and shape of yielded windows.
1201///
1202/// ## Ownership and Allocation
1203///
1204/// Each yielded window is returned as an owned [`AudioSamples`] instance. This
1205/// allows windows to be processed independently but implies allocation and
1206/// copying **may** be performed. Whether or not this occurs is down to whether
1207/// the data is already owned or not. If not then yes, it will allocate,
1208/// otherwise the a borrow is used. For in-place or allocation-free processing,
1209/// prefer specialised higher-level APIs where available.
1210///
1211/// ## Invariants
1212///
1213/// - Windows are yielded in strictly increasing temporal order.
1214/// - All channels within a window remain time-aligned.
1215/// - The hop size between successive windows is constant.
1216/// - The iterator yields a finite, deterministic number of windows.
1217///
1218/// ## Assumptions and Limitations
1219///
1220/// This iterator assumes a fixed sampling rate for the
1221/// lifetime of iteration. It is not suitable for overlapping mutable access or
1222/// algorithms that require shared ownership of window data.
1223#[cfg(feature = "editing")]
1224pub struct WindowIterator<'iter, 'a, T>
1225where
1226    T: StandardSample,
1227    'a: 'iter,
1228{
1229    /// The source audio from which windows are extracted.
1230    audio: &'iter AudioSamples<'a, T>,
1231    window_size: NonZeroUsize,
1232    hop_size: NonZeroUsize,
1233    current_position: usize,
1234    total_samples: NonZeroUsize,
1235    total_windows: usize,
1236    current_window: usize,
1237    padding_mode: PaddingMode,
1238    _phantom: PhantomData<T>,
1239}
1240
1241#[cfg(feature = "editing")]
1242impl<'iter, 'a, T> WindowIterator<'iter, 'a, T>
1243where
1244    T: StandardSample,
1245    'a: 'iter,
1246{
1247    /// Constructs a new window iterator over the given audio.
1248    ///
1249    /// ## Purpose
1250    ///
1251    /// This constructor establishes a windowed traversal over the provided
1252    /// audio data using the specified window and hop sizes.
1253    ///
1254    /// # Arguments
1255    ///
1256    /// - `audio`: The source audio to iterate over.
1257    /// - `window_size`: The number of samples per channel in each window.
1258    /// - `hop_size`: The number of samples between the starts of successive windows.
1259    ///
1260    /// ## Behavioural Guarantees
1261    ///
1262    /// - Windows are generated deterministically from the start of the signal.
1263    /// - The default padding mode is [`PaddingMode::Zero`].
1264    ///
1265    /// ## Degenerate Parameters
1266    ///
1267    /// If either `window_size` or `hop_size` is zero, the iterator yields no
1268    /// windows.
1269    ///
1270    /// # Panics
1271    ///
1272    /// This function does not panic.
1273    fn new(
1274        audio: &'iter AudioSamples<'a, T>,
1275        window_size: NonZeroUsize,
1276        hop_size: NonZeroUsize,
1277    ) -> Self {
1278        let total_samples = audio.samples_per_channel();
1279
1280        let total_windows =
1281            Self::calculate_total_windows(total_samples, window_size, hop_size, PaddingMode::Zero);
1282
1283        Self {
1284            audio,
1285            window_size,
1286            hop_size,
1287            current_position: 0,
1288            total_samples,
1289            total_windows,
1290            current_window: 0,
1291            padding_mode: PaddingMode::Zero,
1292            _phantom: PhantomData,
1293        }
1294    }
1295
1296    const fn calculate_total_windows(
1297        total_samples: NonZeroUsize,
1298        window_size: NonZeroUsize,
1299        hop_size: NonZeroUsize,
1300        padding_mode: PaddingMode,
1301    ) -> usize {
1302        // Calculate the maximum number of windows we could have
1303        // This is the ceiling of total_samples / hop_size
1304        let max_windows = total_samples.get().div_ceil(hop_size.get());
1305
1306        match padding_mode {
1307            PaddingMode::Zero => {
1308                // With zero padding, we can always create max_windows
1309                max_windows
1310            }
1311            PaddingMode::None => {
1312                // With no padding, count windows that have at least some real data
1313                let mut count = 0;
1314                let mut pos = 0;
1315                while pos < total_samples.get() {
1316                    count += 1;
1317                    pos += hop_size.get();
1318                }
1319                count
1320            }
1321            PaddingMode::Skip => {
1322                // With skip, only count complete windows. When the window is
1323                // larger than the signal, no complete window fits, so yield 0.
1324                // `saturating_sub` avoids underflow when `window_size > total`.
1325                if total_samples.get() < window_size.get() {
1326                    0
1327                } else {
1328                    1 + total_samples.get().saturating_sub(window_size.get()) / hop_size.get()
1329                }
1330            }
1331        }
1332    }
1333
1334    /// Sets the padding strategy used for trailing windows.
1335    ///
1336    /// ## Purpose
1337    ///
1338    /// This method allows callers to control how incomplete windows at the end
1339    /// of the signal are handled.
1340    ///
1341    /// Changing the padding mode affects both the number of yielded windows and
1342    /// the shape of the final windows.
1343    ///
1344    /// ## Behavioural Guarantees
1345    ///
1346    /// - The iterator’s internal window count is updated consistently with the
1347    ///   selected mode.
1348    #[inline]
1349    #[must_use]
1350    pub const fn with_padding_mode(mut self, mode: PaddingMode) -> Self {
1351        self.padding_mode = mode;
1352        self.total_windows = Self::calculate_total_windows(
1353            self.total_samples,
1354            self.window_size,
1355            self.hop_size,
1356            mode,
1357        );
1358        self
1359    }
1360}
1361
1362#[cfg(feature = "editing")]
1363impl<T> Iterator for WindowIterator<'_, '_, T>
1364where
1365    T: StandardSample,
1366{
1367    type Item = AudioSamples<'static, T>;
1368
1369    #[inline]
1370    fn next(&mut self) -> Option<Self::Item> {
1371        if self.current_window >= self.total_windows {
1372            return None;
1373        }
1374
1375        let start_pos = self.current_position;
1376        let end_pos = start_pos + self.window_size.get();
1377        // Copy the stored reference so that slice_samples borrows from the
1378        // audio data rather than from the short-lived &mut self borrow.
1379        let audio = self.audio;
1380
1381        let window = if end_pos <= self.total_samples.get() {
1382            // Complete window within bounds
1383            audio
1384                .slice_samples(start_pos..end_pos)
1385                .ok()
1386                .map(super::repr::AudioSamples::into_owned)
1387        } else {
1388            // Window extends beyond available data
1389            match self.padding_mode {
1390                PaddingMode::Zero => {
1391                    // Zero-pad to maintain consistent window size
1392                    let available_samples = self.total_samples.get().saturating_sub(start_pos);
1393                    match audio.data() {
1394                        AudioData::Mono(_) => {
1395                            // Add available samples
1396                            let starting_slice = if available_samples > 0 {
1397                                let slice = audio
1398                                    .slice_samples(start_pos..self.total_samples.get())
1399                                    .ok()?
1400                                    .into_owned();
1401                                Some(slice)
1402                            } else {
1403                                None
1404                            };
1405
1406                            let silence_samples = self.window_size.get() - available_samples;
1407                            let length = NonZeroUsize::new(silence_samples)?;
1408                            let silence = if silence_samples > 0 {
1409                                let silence =
1410                                    AudioSamples::<T>::zeros_mono(length, audio.sample_rate());
1411                                Some(silence)
1412                            } else {
1413                                return starting_slice;
1414                            };
1415
1416                            match (starting_slice, silence) {
1417                                (None, None) => None,
1418                                (None, Some(silence)) => Some(silence),
1419                                (Some(starting_slice), None) => Some(starting_slice),
1420                                (Some(s), Some(z)) => {
1421                                    let slices = vec![s, z];
1422                                    let slices = NonEmptyVec::new(slices).ok()?;
1423                                    Some(AudioSamples::concatenate_owned(slices).ok()?)
1424                                }
1425                            }
1426                        }
1427                        AudioData::Multi(_) => {
1428                            let interleaved_slice = if available_samples > 0 {
1429                                let slice = audio
1430                                    .slice_samples(start_pos..self.total_samples.get())
1431                                    .ok()?
1432                                    .into_owned();
1433                                Some(slice)
1434                            } else {
1435                                None
1436                            };
1437
1438                            // Zero-pad remainder
1439                            let remaining_samples = self.window_size.get() - available_samples;
1440                            if remaining_samples == 0 {
1441                                return interleaved_slice;
1442                            }
1443
1444                            let length = NonZeroUsize::new(remaining_samples)?;
1445
1446                            let silence = AudioSamples::<T>::zeros_multi_channel(
1447                                audio.num_channels(),
1448                                length,
1449                                audio.sample_rate(),
1450                            );
1451
1452                            match interleaved_slice {
1453                                None => Some(silence),
1454                                Some(slice) => {
1455                                    AudioSamples::concatenate_owned(non_empty_vec![slice, silence])
1456                                        .ok()
1457                                }
1458                            }
1459                        }
1460                    }
1461                }
1462                PaddingMode::None => {
1463                    // Return available samples without padding
1464                    let available_samples = self.total_samples.get().saturating_sub(start_pos);
1465                    if available_samples == 0 {
1466                        return None;
1467                    }
1468
1469                    audio
1470                        .slice_samples(start_pos..self.total_samples.get())
1471                        .ok()
1472                        .map(super::repr::AudioSamples::into_owned)
1473                }
1474                PaddingMode::Skip => {
1475                    // Skip incomplete windows
1476                    return None;
1477                }
1478            }
1479        };
1480
1481        self.current_position += self.hop_size.get();
1482        self.current_window += 1;
1483        window
1484    }
1485
1486    #[inline]
1487    fn size_hint(&self) -> (usize, Option<usize>) {
1488        let remaining = self.total_windows - self.current_window;
1489        (remaining, Some(remaining))
1490    }
1491}
1492
1493#[cfg(feature = "editing")]
1494impl<T> ExactSizeIterator for WindowIterator<'_, '_, T> where T: StandardSample {}
1495
1496#[cfg(test)]
1497mod tests {
1498    use crate::AudioSamples;
1499    #[cfg(feature = "editing")]
1500    use crate::PaddingMode;
1501    use crate::sample_rate;
1502    use ndarray::{Array1, array};
1503    use non_empty_slice::non_empty_vec;
1504    #[cfg(feature = "editing")]
1505    use std::num::NonZeroUsize;
1506
1507    #[test]
1508    fn test_frame_iterator_mono() {
1509        let audio = AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0, 5.0], sample_rate!(44100))
1510            .unwrap();
1511        audio
1512            .frames()
1513            .zip([1.0f32, 2.0, 3.0, 4.0, 5.0])
1514            .for_each(|(f, x)| {
1515                assert_eq!(f.to_interleaved_vec(), non_empty_vec![x]);
1516            });
1517    }
1518
1519    #[test]
1520    fn test_frame_iterator_stereo() {
1521        let audio = AudioSamples::new_multi_channel(
1522            array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
1523            sample_rate!(44100),
1524        )
1525        .unwrap();
1526
1527        // Borrowing-first behavior: work with frames directly, don't collect into Vec
1528        let expected_frames = [vec![1.0, 4.0], vec![2.0, 5.0], vec![3.0, 6.0]];
1529
1530        for (i, frame) in audio.frames().enumerate() {
1531            assert_eq!(frame.to_interleaved_vec().to_vec(), expected_frames[i]);
1532        }
1533    }
1534
1535    #[test]
1536    fn test_channel_iterator_mono() {
1537        let audio =
1538            AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0], sample_rate!(44100)).unwrap();
1539
1540        // Borrowing-first behavior: work with channels directly
1541        let mut channel_count = 0;
1542        for channel in audio.channels() {
1543            channel_count += 1;
1544            assert_eq!(
1545                channel.to_interleaved_vec(),
1546                non_empty_vec![1.0, 2.0, 3.0, 4.0]
1547            );
1548        }
1549        assert_eq!(channel_count, 1);
1550    }
1551
1552    #[test]
1553    fn test_channel_iterator_stereo() {
1554        let audio = AudioSamples::new_multi_channel(
1555            array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
1556            sample_rate!(44100),
1557        )
1558        .unwrap();
1559
1560        // Borrowing-first behavior: work with channels directly
1561        let expected_channels = [vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
1562
1563        for (i, channel) in audio.channels().enumerate() {
1564            assert_eq!(channel.as_mono().unwrap().to_vec(), expected_channels[i]);
1565        }
1566    }
1567
1568    #[cfg(feature = "editing")]
1569    #[test]
1570    fn test_window_iterator_no_overlap() {
1571        let audio =
1572            AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], sample_rate!(44100))
1573                .unwrap();
1574        let windows: Vec<AudioSamples<f32>> =
1575            audio.windows(crate::nzu!(3), crate::nzu!(3)).collect();
1576
1577        assert_eq!(windows.len(), 2);
1578        assert_eq!(windows[0].as_mono().unwrap().to_vec(), vec![1.0, 2.0, 3.0]);
1579        assert_eq!(windows[1].as_mono().unwrap().to_vec(), vec![4.0, 5.0, 6.0]);
1580    }
1581
1582    #[cfg(feature = "editing")]
1583    #[test]
1584    fn test_window_iterator_with_overlap() {
1585        let audio =
1586            AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], sample_rate!(44100))
1587                .unwrap();
1588        let windows: Vec<AudioSamples<f32>> =
1589            audio.windows(crate::nzu!(4), crate::nzu!(2)).collect();
1590
1591        // For 6 samples, window_size=4, hop_size=2:
1592        // Window 1: position 0-3 (samples 0,1,2,3)
1593        // Window 2: position 2-5 (samples 2,3,4,5)
1594        // Window 3: position 4-7 (samples 4,5 + 2 zeros for padding)
1595        assert_eq!(windows.len(), 3);
1596        assert_eq!(
1597            windows[0].as_mono().unwrap().to_vec(),
1598            vec![1.0, 2.0, 3.0, 4.0]
1599        );
1600        assert_eq!(
1601            windows[1].as_mono().unwrap().to_vec(),
1602            vec![3.0, 4.0, 5.0, 6.0]
1603        );
1604        assert_eq!(
1605            windows[2].as_mono().unwrap().to_vec(),
1606            vec![5.0, 6.0, 0.0, 0.0]
1607        );
1608    }
1609
1610    #[cfg(feature = "editing")]
1611    #[test]
1612    fn test_window_iterator_zero_padding() {
1613        let audio = AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0, 5.0], sample_rate!(44100))
1614            .unwrap();
1615        let windows: Vec<AudioSamples<f32>> = audio
1616            .windows(crate::nzu!(4), crate::nzu!(3))
1617            .with_padding_mode(PaddingMode::Zero)
1618            .collect();
1619
1620        assert_eq!(windows.len(), 2);
1621        assert_eq!(
1622            windows[0].as_mono().unwrap().to_vec(),
1623            vec![1.0, 2.0, 3.0, 4.0]
1624        );
1625        assert_eq!(
1626            windows[1].as_mono().unwrap().to_vec(),
1627            vec![4.0, 5.0, 0.0, 0.0]
1628        ); // Zero-padded
1629    }
1630
1631    #[cfg(feature = "editing")]
1632    #[test]
1633    fn test_window_iterator_no_padding() {
1634        let audio = AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0, 5.0], sample_rate!(44100))
1635            .unwrap();
1636        let windows: Vec<AudioSamples<f32>> = audio
1637            .windows(crate::nzu!(4), crate::nzu!(3))
1638            .with_padding_mode(PaddingMode::None)
1639            .collect();
1640
1641        assert_eq!(windows.len(), 2);
1642        assert_eq!(
1643            windows[0].as_mono().unwrap().to_vec(),
1644            vec![1.0, 2.0, 3.0, 4.0]
1645        );
1646        assert_eq!(windows[1].as_mono().unwrap().to_vec(), vec![4.0, 5.0]); // Incomplete window
1647    }
1648
1649    #[cfg(feature = "editing")]
1650    #[test]
1651    fn test_window_iterator_skip_padding() {
1652        let audio = AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0, 5.0], sample_rate!(44100))
1653            .unwrap();
1654        let windows: Vec<AudioSamples<f32>> = audio
1655            .windows(crate::nzu!(4), crate::nzu!(3))
1656            .with_padding_mode(PaddingMode::Skip)
1657            .collect();
1658
1659        assert_eq!(windows.len(), 1);
1660        assert_eq!(
1661            windows[0].as_mono().unwrap().to_vec(),
1662            vec![1.0, 2.0, 3.0, 4.0]
1663        );
1664    }
1665
1666    #[cfg(feature = "editing")]
1667    #[test]
1668    fn test_window_iterator_stereo_interleaved() {
1669        let audio = AudioSamples::new_multi_channel(
1670            array![[1.0f32, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]],
1671            sample_rate!(44100),
1672        )
1673        .unwrap();
1674        let windows: Vec<AudioSamples<f32>> =
1675            audio.windows(crate::nzu!(2), crate::nzu!(2)).collect();
1676
1677        assert_eq!(windows.len(), 2);
1678        // First window: samples 0,1 interleaved across channels
1679        assert_eq!(
1680            windows[0].to_interleaved_vec(),
1681            non_empty_vec![1.0, 5.0, 2.0, 6.0]
1682        );
1683        // Second window: samples 2,3 interleaved across channels
1684        assert_eq!(
1685            windows[1].to_interleaved_vec(),
1686            non_empty_vec![3.0, 7.0, 4.0, 8.0]
1687        );
1688    }
1689
1690    #[test]
1691    fn test_exact_size_iterators() {
1692        let audio = AudioSamples::new_multi_channel(
1693            array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
1694            sample_rate!(44100),
1695        )
1696        .unwrap();
1697
1698        let frame_iter = audio.frames();
1699        assert_eq!(frame_iter.len(), 3);
1700
1701        let channel_iter = audio.channels();
1702        assert_eq!(channel_iter.len(), 2);
1703
1704        #[cfg(feature = "editing")]
1705        {
1706            let window_iter = audio.windows(crate::nzu!(2), crate::nzu!(1));
1707            assert_eq!(window_iter.len(), 3); // (3-2)/1 + 1 = 2, plus padding = 3
1708        }
1709    }
1710
1711    #[test]
1712    fn test_multiple_iterators_from_same_audio() {
1713        // This test verifies that our raw pointer approach allows multiple iterators
1714        let audio = AudioSamples::new_multi_channel(
1715            array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
1716            sample_rate!(44100),
1717        )
1718        .unwrap();
1719
1720        // This should compile and work correctly
1721        let frames = audio.frames();
1722        let channels = audio.channels();
1723        #[cfg(feature = "editing")]
1724        let windows = audio.windows(crate::nzu!(2), crate::nzu!(1));
1725
1726        // Verify they all work independently
1727        assert_eq!(frames.len(), 3);
1728        assert_eq!(channels.len(), 2);
1729        #[cfg(feature = "editing")]
1730        assert_eq!(windows.len(), 3);
1731    }
1732
1733    // ==============================
1734    // MUTABLE ITERATOR TESTS
1735    // ==============================
1736
1737    #[test]
1738    fn test_frame_iterator_mut_stereo() {
1739        let mut audio = AudioSamples::new_multi_channel(
1740            array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
1741            sample_rate!(44100),
1742        )
1743        .unwrap();
1744
1745        let expected = array![[0.5f32, 1.0, 1.5], [6.0, 7.5, 9.0]];
1746
1747        // Apply different processing to each channel
1748        audio.apply_to_channel_data(|ch, channel_data| {
1749            let gain = if ch == 0 { 0.5 } else { 1.5 };
1750            for sample in channel_data {
1751                *sample *= gain;
1752            }
1753        });
1754
1755        assert_eq!(audio.as_multi_channel().unwrap(), &expected);
1756    }
1757
1758    #[test]
1759    fn test_frame_iterator_mut_individual_access() {
1760        let mut audio =
1761            AudioSamples::new_multi_channel(array![[1.0f32, 2.0], [3.0, 4.0]], sample_rate!(44100))
1762                .unwrap();
1763
1764        let expected = array![[10.0f32, 20.0], [3.0, 4.0]];
1765
1766        // Modify only the left channel (channel 0)
1767        audio.apply_to_channel_data(|ch, channel_data| {
1768            if ch == 0 {
1769                for sample in channel_data {
1770                    *sample *= 10.0;
1771                }
1772            }
1773            // Leave right channel unchanged
1774        });
1775
1776        assert_eq!(audio.as_multi_channel().unwrap(), &expected);
1777    }
1778
1779    #[test]
1780    fn test_channel_iterator_mut_mono() {
1781        let mut audio =
1782            AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0], sample_rate!(44100)).unwrap();
1783
1784        audio.apply_to_channel_data(|_ch, channel_data| {
1785            for sample in channel_data {
1786                *sample += 10.0;
1787            }
1788        });
1789
1790        assert_eq!(audio.as_mono().unwrap(), &array![11.0f32, 12.0, 13.0, 14.0]);
1791    }
1792
1793    #[test]
1794    fn test_channel_iterator_mut_stereo() {
1795        let mut audio = AudioSamples::new_multi_channel(
1796            array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0]],
1797            sample_rate!(44100),
1798        )
1799        .unwrap();
1800
1801        let expected = array![[0.5f32, 1.0, 1.5], [8.0, 10.0, 12.0]];
1802
1803        // Apply different processing to each channel
1804        audio.apply_to_channel_data(|ch, channel_data| {
1805            let gain = if ch == 0 { 0.5 } else { 2.0 };
1806            for sample in channel_data {
1807                *sample *= gain;
1808            }
1809        });
1810
1811        assert_eq!(audio.as_multi_channel().unwrap(), &expected);
1812    }
1813
1814    #[test]
1815    fn test_window_iterator_mut_mono() {
1816        let mut audio =
1817            AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], sample_rate!(44100))
1818                .unwrap();
1819
1820        // Apply windowed processing (non-overlapping)
1821        audio.apply_to_windows(3, 3, |_window_idx, window_data| {
1822            for sample in window_data {
1823                *sample *= 0.5;
1824            }
1825        });
1826
1827        assert_eq!(
1828            audio.as_mono().unwrap(),
1829            &array![0.5f32, 1.0, 1.5, 2.0, 2.5, 3.0]
1830        );
1831    }
1832
1833    #[test]
1834    fn test_window_iterator_mut_stereo() {
1835        let mut audio = AudioSamples::new_multi_channel(
1836            array![[1.0f32, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]],
1837            sample_rate!(44100),
1838        )
1839        .unwrap();
1840
1841        let expected = array![[0.8f32, 1.6, 2.4, 3.2], [6.0, 7.2, 8.4, 9.6]];
1842
1843        // Apply windowed processing (non-overlapping)
1844        // For multi-channel, apply_to_windows provides interleaved data
1845        audio.apply_to_windows(2, 2, |_window_idx, window_data| {
1846            // window_data is interleaved: [L0, R0, L1, R1, ...] for 2-sample window
1847            let samples_per_channel = window_data.len() / 2; // 2 channels
1848            for sample_idx in 0..samples_per_channel {
1849                let left_idx = sample_idx * 2;
1850                let right_idx = sample_idx * 2 + 1;
1851                window_data[left_idx] *= 0.8; // Left channel gain
1852                window_data[right_idx] *= 1.2; // Right channel gain
1853            }
1854        });
1855
1856        let result = audio.as_multi_channel().unwrap();
1857        for (i, (&actual, &expected)) in result.iter().zip(expected.iter()).enumerate() {
1858            assert!(
1859                (actual - expected).abs() < 1e-6,
1860                "Mismatch at index {}: {} != {} (diff: {})",
1861                i,
1862                actual,
1863                expected,
1864                (actual - expected).abs()
1865            );
1866        }
1867    }
1868
1869    #[test]
1870    fn test_window_function_application() {
1871        let mut audio =
1872            AudioSamples::new_mono(array![1.0f32, 1.0, 1.0, 1.0], sample_rate!(44100)).unwrap();
1873
1874        // Apply Hann window function
1875        audio.apply_to_windows(4, 4, |_window_idx, window_data| {
1876            let window_size = window_data.len();
1877            for (i, sample) in window_data.iter_mut().enumerate() {
1878                let hann_weight = 0.5
1879                    * (1.0
1880                        - (2.0 * std::f32::consts::PI * i as f32 / (window_size - 1) as f32).cos());
1881                *sample *= hann_weight;
1882            }
1883        });
1884
1885        // Check that Hann window was applied (values should be different from 1.0)
1886        let result = audio.as_mono().unwrap();
1887        assert!(result[0] < 1.0); // Should be close to 0
1888        assert!(result[1] > 0.5); // Should be around 0.75
1889        assert!(result[2] > 0.5); // Should be around 0.75
1890        assert!(result[3] < 1.0); // Should be close to 0
1891    }
1892
1893    #[test]
1894    fn test_performance_comparison_apply_vs_iterator() {
1895        // This test demonstrates when to use each approach
1896        let mut audio1 =
1897            AudioSamples::new_mono(Array1::<f32>::ones(1000), sample_rate!(44100)).unwrap();
1898        let mut audio2 = audio1.clone();
1899
1900        // Method 1: Using optimized apply (recommended for simple operations)
1901        audio1.apply(|sample| sample * 0.5);
1902
1903        // Method 2: Using convenience method (alternative for complex operations)
1904        audio2.apply_to_frames(|_frame_idx, frame_data| {
1905            for sample in frame_data {
1906                *sample *= 0.5;
1907            }
1908        });
1909
1910        // Results should be identical
1911        assert_eq!(audio1.as_mono().unwrap(), audio2.as_mono().unwrap());
1912    }
1913
1914    // Regression: BUG 3 — WindowIterator window-count underflow in Skip mode.
1915    // With window (8) > total (5), the old `1 + (total - window) / hop`
1916    // underflowed (debug panic / release wrap) and NonZeroUsize could not even
1917    // represent 0 windows. Skip mode must yield 0 windows without panicking.
1918    #[cfg(feature = "editing")]
1919    #[test]
1920    fn test_window_iterator_skip_window_larger_than_signal_yields_zero() {
1921        let audio = AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0, 5.0], sample_rate!(44100))
1922            .unwrap();
1923        let iter = audio
1924            .windows(crate::nzu!(8), crate::nzu!(4))
1925            .with_padding_mode(PaddingMode::Skip);
1926
1927        // len()/size_hint must report 0 ...
1928        assert_eq!(iter.len(), 0);
1929
1930        // ... and iteration must produce 0 windows without panicking.
1931        assert_eq!(iter.count(), 0);
1932    }
1933
1934    // Regression: BUG 3 — Skip mode still counts complete windows correctly
1935    // when the window fits, confirming the saturating_sub change didn't regress
1936    // the normal path.
1937    #[cfg(feature = "editing")]
1938    #[test]
1939    fn test_window_iterator_skip_counts_complete_windows() {
1940        let audio =
1941            AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0], sample_rate!(44100))
1942                .unwrap();
1943        // window 3, hop 2 over 6 samples: starts at 0 (1..3) and 2 (3..5);
1944        // a third start at 4 would need samples 4..7 (incomplete) -> 2 windows.
1945        let windows: Vec<AudioSamples<f32>> = audio
1946            .windows(crate::nzu!(3), crate::nzu!(2))
1947            .with_padding_mode(PaddingMode::Skip)
1948            .collect();
1949        assert_eq!(windows.len(), 2);
1950        assert_eq!(windows[0].as_mono().unwrap().to_vec(), vec![1.0, 2.0, 3.0]);
1951        assert_eq!(windows[1].as_mono().unwrap().to_vec(), vec![3.0, 4.0, 5.0]);
1952    }
1953
1954    // Regression: BUG 4 — apply_to_windows infinite loop on hop_size == 0.
1955    // The old code only guarded window_size == 0, so `pos += 0` looped forever.
1956    // Must return immediately without invoking the callback.
1957    #[cfg(feature = "editing")]
1958    #[test]
1959    fn test_apply_to_windows_zero_hop_returns_without_hanging() {
1960        let mut audio =
1961            AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0], sample_rate!(44100)).unwrap();
1962        let original = audio.as_mono().unwrap().to_vec();
1963
1964        let mut called = false;
1965        audio.apply_to_windows(2, 0, |_idx, _window| {
1966            called = true;
1967        });
1968
1969        // Callback must never run, and data must be untouched.
1970        assert!(!called, "callback must not be invoked when hop_size == 0");
1971        assert_eq!(audio.as_mono().unwrap().to_vec(), original);
1972    }
1973
1974    // ==============================
1975    // BORROWING WINDOW ITERATOR (windows_ref) TESTS
1976    // ==============================
1977
1978    // windows_ref must yield the SAME window contents as the owning windows()
1979    // iterator (in Skip / fully-contained mode) for several (window, hop) pairs
1980    // on a mono signal, with matching window counts.
1981    #[cfg(feature = "editing")]
1982    #[test]
1983    fn test_windows_ref_matches_owning_windows_mono() {
1984        use crate::WindowView;
1985        let audio = AudioSamples::new_mono(
1986            array![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0],
1987            sample_rate!(44100),
1988        )
1989        .unwrap();
1990
1991        for (w, h) in [(3usize, 3usize), (4, 2), (5, 1), (2, 3), (8, 4)] {
1992            let wnz = NonZeroUsize::new(w).unwrap();
1993            let hnz = NonZeroUsize::new(h).unwrap();
1994
1995            // Reference: owning windows in Skip mode (fully-contained only).
1996            let owned: Vec<Vec<f32>> = audio
1997                .windows(wnz, hnz)
1998                .with_padding_mode(PaddingMode::Skip)
1999                .map(|win| win.as_mono().unwrap().to_vec())
2000                .collect();
2001
2002            // Borrowing zero-copy variant.
2003            let borrowed: Vec<Vec<f32>> = audio
2004                .windows_ref(wnz, hnz)
2005                .map(|view| match view {
2006                    WindowView::Mono(v) => v.to_vec(),
2007                    WindowView::Multi(_) => panic!("mono signal must yield Mono views"),
2008                })
2009                .collect();
2010
2011            assert_eq!(
2012                borrowed.len(),
2013                owned.len(),
2014                "count mismatch for window={w}, hop={h}"
2015            );
2016            assert_eq!(borrowed, owned, "content mismatch for window={w}, hop={h}");
2017
2018            // ExactSizeIterator len() must agree with what is yielded.
2019            assert_eq!(audio.windows_ref(wnz, hnz).len(), owned.len());
2020        }
2021    }
2022
2023    // Same equivalence check for multi-channel audio: contents must match the
2024    // owning windows() (Skip) channel-by-channel, and counts must match.
2025    #[cfg(feature = "editing")]
2026    #[test]
2027    fn test_windows_ref_matches_owning_windows_multi() {
2028        use crate::WindowView;
2029        let audio = AudioSamples::new_multi_channel(
2030            array![
2031                [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0],
2032                [7.0, 8.0, 9.0, 10.0, 11.0, 12.0]
2033            ],
2034            sample_rate!(44100),
2035        )
2036        .unwrap();
2037
2038        for (w, h) in [(2usize, 2usize), (3, 1), (4, 2), (6, 3)] {
2039            let wnz = NonZeroUsize::new(w).unwrap();
2040            let hnz = NonZeroUsize::new(h).unwrap();
2041
2042            // Reference: owning windows (Skip), captured as per-channel rows.
2043            let owned: Vec<Vec<Vec<f32>>> = audio
2044                .windows(wnz, hnz)
2045                .with_padding_mode(PaddingMode::Skip)
2046                .map(|win| {
2047                    let n = win.num_channels().get() as usize;
2048                    let mc = win.as_multi_channel().unwrap();
2049                    (0..n).map(|ch| mc.row(ch).to_vec()).collect()
2050                })
2051                .collect();
2052
2053            let borrowed: Vec<Vec<Vec<f32>>> = audio
2054                .windows_ref(wnz, hnz)
2055                .map(|view| match view {
2056                    WindowView::Multi(v) => v.rows().into_iter().map(|r| r.to_vec()).collect(),
2057                    WindowView::Mono(_) => panic!("multi-channel signal must yield Multi views"),
2058                })
2059                .collect();
2060
2061            assert_eq!(
2062                borrowed.len(),
2063                owned.len(),
2064                "count mismatch for window={w}, hop={h}"
2065            );
2066            assert_eq!(borrowed, owned, "content mismatch for window={w}, hop={h}");
2067        }
2068    }
2069
2070    // window_size > signal length must yield zero windows without panicking,
2071    // mirroring the WindowIterator Skip-mode underflow fix.
2072    #[cfg(feature = "editing")]
2073    #[test]
2074    fn test_windows_ref_window_larger_than_signal_yields_zero() {
2075        let audio = AudioSamples::new_mono(array![1.0f32, 2.0, 3.0, 4.0, 5.0], sample_rate!(44100))
2076            .unwrap();
2077
2078        let iter = audio.windows_ref(NonZeroUsize::new(8).unwrap(), NonZeroUsize::new(4).unwrap());
2079        assert_eq!(iter.len(), 0);
2080
2081        assert_eq!(iter.count(), 0);
2082    }
2083
2084    // Regression: BUG 5 — ChannelIterator::next must extract exactly one channel
2085    // per step (no O(N^2) deep clone, no eprintln!), advance current_channel
2086    // correctly, and keep len() consistent with what next() yields.
2087    #[test]
2088    fn test_channel_iterator_three_channels_len_and_data() {
2089        let audio = AudioSamples::new_multi_channel(
2090            array![[1.0f32, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]],
2091            sample_rate!(44100),
2092        )
2093        .unwrap();
2094
2095        let mut iter = audio.channels();
2096        assert_eq!(iter.len(), 3);
2097
2098        let c0 = iter.next().unwrap();
2099        assert_eq!(c0.as_mono().unwrap().to_vec(), vec![1.0, 2.0, 3.0]);
2100        assert_eq!(iter.len(), 2);
2101
2102        let c1 = iter.next().unwrap();
2103        assert_eq!(c1.as_mono().unwrap().to_vec(), vec![4.0, 5.0, 6.0]);
2104        assert_eq!(iter.len(), 1);
2105
2106        let c2 = iter.next().unwrap();
2107        assert_eq!(c2.as_mono().unwrap().to_vec(), vec![7.0, 8.0, 9.0]);
2108        assert_eq!(iter.len(), 0);
2109
2110        assert!(iter.next().is_none());
2111        assert_eq!(iter.len(), 0);
2112    }
2113}