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}