コンテンツにスキップ

APIリファレンス

Wandasライブラリの主要コンポーネントと関数のAPIリファレンスです。

コアモジュール

コアモジュールはWandasの基本的な機能を提供します。

wandas.core

Attributes

__all__ = ['BaseFrame'] module-attribute

Classes

BaseFrame

Bases: ABC, Generic[T]

Abstract base class for all signal frame types.

This class provides the common interface and functionality for all frame types used in signal processing. It implements basic operations like indexing, iteration, and data manipulation that are shared across all frame types.

Parameters

data : DaArray The signal data to process. Must be a dask array. sampling_rate : float The sampling rate of the signal in Hz. label : str, optional A label for the frame. If not provided, defaults to "unnamed_frame". metadata : dict, optional Additional metadata for the frame. operation_history : list[dict], optional History of operations performed on this frame. channel_metadata : list[ChannelMetadata], optional Metadata for each channel in the frame. previous : BaseFrame, optional The frame that this frame was derived from.

Attributes

sampling_rate : float The sampling rate of the signal in Hz. label : str The label of the frame. metadata : dict Additional metadata for the frame. operation_history : list[dict] History of operations performed on this frame.

Source code in wandas/core/base_frame.py
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
class BaseFrame(ABC, Generic[T]):
    """
    Abstract base class for all signal frame types.

    This class provides the common interface and functionality for all frame types
    used in signal processing. It implements basic operations like indexing, iteration,
    and data manipulation that are shared across all frame types.

    Parameters
    ----------
    data : DaArray
        The signal data to process. Must be a dask array.
    sampling_rate : float
        The sampling rate of the signal in Hz.
    label : str, optional
        A label for the frame. If not provided, defaults to "unnamed_frame".
    metadata : dict, optional
        Additional metadata for the frame.
    operation_history : list[dict], optional
        History of operations performed on this frame.
    channel_metadata : list[ChannelMetadata], optional
        Metadata for each channel in the frame.
    previous : BaseFrame, optional
        The frame that this frame was derived from.

    Attributes
    ----------
    sampling_rate : float
        The sampling rate of the signal in Hz.
    label : str
        The label of the frame.
    metadata : dict
        Additional metadata for the frame.
    operation_history : list[dict]
        History of operations performed on this frame.
    """

    _data: DaArray
    sampling_rate: float
    label: str
    metadata: dict[str, Any]
    operation_history: list[dict[str, Any]]
    _channel_metadata: list[ChannelMetadata]
    _previous: Optional["BaseFrame[Any]"]

    def __init__(
        self,
        data: DaArray,
        sampling_rate: float,
        label: Optional[str] = None,
        metadata: Optional[dict[str, Any]] = None,
        operation_history: Optional[list[dict[str, Any]]] = None,
        channel_metadata: Optional[list[ChannelMetadata]] = None,
        previous: Optional["BaseFrame[Any]"] = None,
    ):
        self._data = data.rechunk(chunks=-1)  # type: ignore [unused-ignore]
        if self._data.ndim == 1:
            self._data = self._data.reshape((1, -1))
        self.sampling_rate = sampling_rate
        self.label = label or "unnamed_frame"
        self.metadata = metadata or {}
        self.operation_history = operation_history or []
        self._previous = previous

        if channel_metadata:
            self._channel_metadata = copy.deepcopy(channel_metadata)
        else:
            self._channel_metadata = [
                ChannelMetadata(label=f"ch{i}", unit="", extra={})
                for i in range(self._n_channels)
            ]

        try:
            # Display information for newer dask versions
            logger.debug(f"Dask graph layers: {list(self._data.dask.layers.keys())}")
            logger.debug(
                f"Dask graph dependencies: {len(self._data.dask.dependencies)}"
            )
        except Exception as e:
            logger.debug(f"Dask graph visualization details unavailable: {e}")

    @property
    @abstractmethod
    def _n_channels(self) -> int:
        """Returns the number of channels."""

    @property
    def n_channels(self) -> int:
        """Returns the number of channels."""
        return self._n_channels

    @property
    def channels(self) -> list[ChannelMetadata]:
        """Property to access channel metadata."""
        return self._channel_metadata

    @property
    def previous(self) -> Optional["BaseFrame[Any]"]:
        """
        Returns the previous frame.
        """
        return self._previous

    def get_channel(
        self: S,
        channel_idx: Union[
            int,
            list[int],
            tuple[int, ...],
            npt.NDArray[np.int_],
            npt.NDArray[np.bool_],
        ],
    ) -> S:
        """
        Get channel(s) by index.

        Parameters
        ----------
        channel_idx : int or sequence of int
            Single channel index or sequence of channel indices.
            Supports negative indices (e.g., -1 for the last channel).

        Returns
        -------
        S
            New instance containing the selected channel(s).

        Examples
        --------
        >>> frame.get_channel(0)  # Single channel
        >>> frame.get_channel([0, 2, 3])  # Multiple channels
        >>> frame.get_channel((-1, -2))  # Last two channels
        >>> frame.get_channel(np.array([1, 2]))  # NumPy array of indices
        """
        if isinstance(channel_idx, int):
            # Convert single channel to a list.
            channel_idx_list: list[int] = [channel_idx]
        else:
            channel_idx_list = list(channel_idx)

        new_data = self._data[channel_idx_list]
        new_channel_metadata = [self._channel_metadata[i] for i in channel_idx_list]
        return self._create_new_instance(
            data=new_data,
            operation_history=self.operation_history,
            channel_metadata=new_channel_metadata,
        )

    def __len__(self) -> int:
        """
        Returns the number of channels.
        """
        return len(self._channel_metadata)

    def __iter__(self: S) -> Iterator[S]:
        for idx in range(len(self)):
            yield self[idx]

    def __getitem__(
        self: S,
        key: Union[
            int,
            str,
            slice,
            list[int],
            list[str],
            tuple[
                Union[
                    int,
                    str,
                    slice,
                    list[int],
                    list[str],
                    npt.NDArray[np.int_],
                    npt.NDArray[np.bool_],
                ],
                ...,
            ],
            npt.NDArray[np.int_],
            npt.NDArray[np.bool_],
        ],
    ) -> S:
        """
        Get channel(s) by index, label, or advanced indexing.

        This method supports multiple indexing patterns similar to NumPy and pandas:

        - Single channel by index: `frame[0]`
        - Single channel by label: `frame["ch0"]`
        - Slice of channels: `frame[0:3]`
        - Multiple channels by indices: `frame[[0, 2, 5]]`
        - Multiple channels by labels: `frame[["ch0", "ch2"]]`
        - NumPy integer array: `frame[np.array([0, 2])]`
        - Boolean mask: `frame[mask]` where mask is a boolean array
        - Multidimensional indexing: `frame[0, 100:200]` (channel + time)

        Parameters
        ----------
        key : int, str, slice, list, tuple, or ndarray
            - int: Single channel index (supports negative indexing)
            - str: Single channel label
            - slice: Range of channels
            - list[int]: Multiple channel indices
            - list[str]: Multiple channel labels
            - tuple: Multidimensional indexing (channel_key, time_key, ...)
            - ndarray[int]: NumPy array of channel indices
            - ndarray[bool]: Boolean mask for channel selection

        Returns
        -------
        S
            New instance containing the selected channel(s).

        Raises
        ------
        ValueError
            If the key length is invalid for the shape or if boolean mask
            length doesn't match number of channels.
        IndexError
            If the channel index is out of range.
        TypeError
            If the key type is invalid or list contains mixed types.
        KeyError
            If a channel label is not found.

        Examples
        --------
        >>> # Single channel selection
        >>> frame[0]  # First channel
        >>> frame["acc_x"]  # By label
        >>> frame[-1]  # Last channel
        >>>
        >>> # Multiple channel selection
        >>> frame[[0, 2, 5]]  # Multiple indices
        >>> frame[["acc_x", "acc_z"]]  # Multiple labels
        >>> frame[0:3]  # Slice
        >>>
        >>> # NumPy array indexing
        >>> frame[np.array([0, 2, 4])]  # Integer array
        >>> mask = np.array([True, False, True])
        >>> frame[mask]  # Boolean mask
        >>>
        >>> # Time slicing (multidimensional)
        >>> frame[0, 100:200]  # Channel 0, samples 100-200
        >>> frame[[0, 1], ::2]  # Channels 0-1, every 2nd sample
        """

        # Single index (int)
        if isinstance(key, numbers.Integral):
            return self.get_channel(key)

        # Single label (str)
        if isinstance(key, str):
            index = self.label2index(key)
            return self.get_channel(index)

        # Phase 2: NumPy array support (bool mask and int array)
        if isinstance(key, np.ndarray):
            if key.dtype == bool or key.dtype == np.bool_:
                # Boolean mask
                if len(key) != self.n_channels:
                    raise ValueError(
                        f"Boolean mask length {len(key)} does not match "
                        f"number of channels {self.n_channels}"
                    )
                indices = np.where(key)[0]
                return self.get_channel(indices)
            elif np.issubdtype(key.dtype, np.integer):
                # Integer array
                return self.get_channel(key)
            else:
                raise TypeError(
                    f"NumPy array must be of integer or boolean type, got {key.dtype}"
                )

        # Phase 1: List support (int or str)
        if isinstance(key, list):
            if len(key) == 0:
                raise ValueError("Cannot index with an empty list")

            # Check if all elements are strings
            if all(isinstance(k, str) for k in key):
                # Multiple labels - type narrowing for mypy
                str_list = cast(list[str], key)
                indices_from_labels = [self.label2index(label) for label in str_list]
                return self.get_channel(indices_from_labels)

            # Check if all elements are integers
            elif all(isinstance(k, (int, np.integer)) for k in key):
                # Multiple indices - convert to list[int] for type safety
                int_list = [int(k) for k in key]
                return self.get_channel(int_list)

            else:
                raise TypeError(
                    f"List must contain all str or all int, got mixed types: "
                    f"{[type(k).__name__ for k in key]}"
                )

        # Tuple: multidimensional indexing
        if isinstance(key, tuple):
            return self._handle_multidim_indexing(key)

        # Slice
        if isinstance(key, slice):
            new_data = self._data[key]
            new_channel_metadata = self._channel_metadata[key]
            if isinstance(new_channel_metadata, ChannelMetadata):
                new_channel_metadata = [new_channel_metadata]
            return self._create_new_instance(
                data=new_data,
                operation_history=self.operation_history,
                channel_metadata=new_channel_metadata,
            )

        raise TypeError(
            f"Invalid key type: {type(key).__name__}. "
            f"Expected int, str, slice, list, tuple, or ndarray."
        )

    def _handle_multidim_indexing(
        self: S,
        key: tuple[
            Union[
                int,
                str,
                slice,
                list[int],
                list[str],
                npt.NDArray[np.int_],
                npt.NDArray[np.bool_],
            ],
            ...,
        ],
    ) -> S:
        """
        Handle multidimensional indexing (channel + time axis).

        Parameters
        ----------
        key : tuple
            Tuple of indices where the first element selects channels
            and subsequent elements select along other dimensions (e.g., time).

        Returns
        -------
        S
            New instance with selected channels and time range.

        Raises
        ------
        ValueError
            If the key length exceeds the data dimensions.
        """
        if len(key) > self._data.ndim:
            raise ValueError(f"Invalid key length: {len(key)} for shape {self.shape}")

        # First element: channel selection
        channel_key = key[0]
        time_keys = key[1:] if len(key) > 1 else ()

        # Select channels first (recursively call __getitem__)
        if isinstance(channel_key, (list, np.ndarray)):
            selected = self[channel_key]
        elif isinstance(channel_key, (int, str, slice)):
            selected = self[channel_key]
        else:
            raise TypeError(
                f"Invalid channel key type in tuple: {type(channel_key).__name__}"
            )

        # Apply time indexing if present
        if time_keys:
            new_data = selected._data[(slice(None),) + time_keys]
            return selected._create_new_instance(
                data=new_data,
                operation_history=selected.operation_history,
                channel_metadata=selected._channel_metadata,
            )

        return selected

    def label2index(self, label: str) -> int:
        """
        Get the index from a channel label.

        Parameters
        ----------
        label : str
            Channel label.

        Returns
        -------
        int
            Corresponding index.

        Raises
        ------
        KeyError
            If the channel label is not found.
        """
        for idx, ch in enumerate(self._channel_metadata):
            if ch.label == label:
                return idx
        raise KeyError(f"Channel label '{label}' not found.")

    @property
    def shape(self) -> tuple[int, ...]:
        _shape: tuple[int, ...] = self._data.shape
        if _shape[0] == 1:
            return _shape[1:]
        return _shape

    @property
    def data(self) -> T:
        """
        Returns the computed data.
        Calculation is executed the first time this is accessed.
        """
        data = self.compute()
        if self.n_channels == 1:
            return data.squeeze(axis=0)
        return data

    @property
    def labels(self) -> list[str]:
        """Get a list of all channel labels."""
        return [ch.label for ch in self._channel_metadata]

    def compute(self) -> T:
        """
        Compute and return the data.
        This method materializes lazily computed data into a concrete NumPy array.

        Returns
        -------
        NDArrayReal
            The computed data.

        Raises
        ------
        ValueError
            If the computed result is not a NumPy array.
        """
        logger.debug(
            "COMPUTING DASK ARRAY - This will trigger file reading and all processing"
        )
        result = self._data.compute()

        if not isinstance(result, np.ndarray):
            raise ValueError(f"Computed result is not a np.ndarray: {type(result)}")

        logger.debug(f"Computation complete, result shape: {result.shape}")
        return cast(T, result)

    @abstractmethod
    def plot(
        self, plot_type: str = "default", ax: Optional[Axes] = None, **kwargs: Any
    ) -> Union[Axes, Iterator[Axes]]:
        """Plot the data"""
        pass

    def persist(self: S) -> S:
        """Persist the data in memory"""
        persisted_data = self._data.persist()
        return self._create_new_instance(data=persisted_data)

    @abstractmethod
    def _get_additional_init_kwargs(self) -> dict[str, Any]:
        """
        Abstract method for derived classes to provide
        additional initialization arguments.
        """
        pass

    def _create_new_instance(self: S, data: DaArray, **kwargs: Any) -> S:
        """
        Create a new channel instance based on an existing channel.
        Keyword arguments can override or extend the original attributes.
        """

        sampling_rate = kwargs.pop("sampling_rate", self.sampling_rate)
        # if not isinstance(sampling_rate, int):
        #     raise TypeError("Sampling rate must be an integer")

        label = kwargs.pop("label", self.label)
        if not isinstance(label, str):
            raise TypeError("Label must be a string")

        metadata = kwargs.pop("metadata", copy.deepcopy(self.metadata))
        if not isinstance(metadata, dict):
            raise TypeError("Metadata must be a dictionary")

        channel_metadata = kwargs.pop(
            "channel_metadata", copy.deepcopy(self._channel_metadata)
        )
        if not isinstance(channel_metadata, list):
            raise TypeError("Channel metadata must be a list")

        # Get additional initialization arguments from derived classes
        additional_kwargs = self._get_additional_init_kwargs()
        kwargs.update(additional_kwargs)

        return type(self)(
            data=data,
            sampling_rate=sampling_rate,
            label=label,
            metadata=metadata,
            channel_metadata=channel_metadata,
            previous=self,
            **kwargs,
        )

    def __array__(self, dtype: npt.DTypeLike = None) -> NDArrayReal:
        """Implicit conversion to NumPy array"""
        result = self.compute()
        if dtype is not None:
            return result.astype(dtype)
        return result

    def visualize_graph(self, filename: Optional[str] = None) -> Optional[str]:
        """Visualize the computation graph and save it to a file"""
        try:
            filename = filename or f"graph_{uuid.uuid4().hex[:8]}.png"
            self._data.visualize(filename=filename)
            return filename
        except Exception as e:
            logger.warning(f"Failed to visualize the graph: {e}")
            return None

    @abstractmethod
    def _binary_op(
        self: S,
        other: Union[S, int, float, NDArrayReal, DaArray],
        op: Callable[[DaArray, Any], DaArray],
        symbol: str,
    ) -> S:
        """Basic implementation of binary operations"""
        # Basic logic
        # Actual implementation is left to derived classes
        pass

    def __add__(self: S, other: Union[S, int, float, NDArrayReal]) -> S:
        """Addition operator"""
        return self._binary_op(other, lambda x, y: x + y, "+")

    def __sub__(self: S, other: Union[S, int, float, NDArrayReal]) -> S:
        """Subtraction operator"""
        return self._binary_op(other, lambda x, y: x - y, "-")

    def __mul__(self: S, other: Union[S, int, float, NDArrayReal]) -> S:
        """Multiplication operator"""
        return self._binary_op(other, lambda x, y: x * y, "*")

    def __truediv__(self: S, other: Union[S, int, float, NDArrayReal]) -> S:
        """Division operator"""
        return self._binary_op(other, lambda x, y: x / y, "/")

    def apply_operation(self: S, operation_name: str, **params: Any) -> S:
        """
        Apply a named operation.

        Parameters
        ----------
        operation_name : str
            Name of the operation to apply.
        **params : Any
            Parameters to pass to the operation.

        Returns
        -------
        S
            A new instance with the operation applied.
        """
        # Apply the operation through abstract method
        return self._apply_operation_impl(operation_name, **params)

    @abstractmethod
    def _apply_operation_impl(self: S, operation_name: str, **params: Any) -> S:
        """Implementation of operation application"""
        pass

    def debug_info(self) -> None:
        """Output detailed debug information"""
        logger.debug(f"=== {self.__class__.__name__} Debug Info ===")
        logger.debug(f"Label: {self.label}")
        logger.debug(f"Shape: {self.shape}")
        logger.debug(f"Sampling rate: {self.sampling_rate} Hz")
        logger.debug(f"Operation history: {len(self.operation_history)} operations")
        self._debug_info_impl()
        logger.debug("=== End Debug Info ===")

    def _debug_info_impl(self) -> None:
        """Implement derived class-specific debug information"""
        pass
Attributes
sampling_rate = sampling_rate instance-attribute
label = label or 'unnamed_frame' instance-attribute
metadata = metadata or {} instance-attribute
operation_history = operation_history or [] instance-attribute
n_channels property

Returns the number of channels.

channels property

Property to access channel metadata.

previous property

Returns the previous frame.

shape property
data property

Returns the computed data. Calculation is executed the first time this is accessed.

labels property

Get a list of all channel labels.

Functions
__init__(data, sampling_rate, label=None, metadata=None, operation_history=None, channel_metadata=None, previous=None)
Source code in wandas/core/base_frame.py
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def __init__(
    self,
    data: DaArray,
    sampling_rate: float,
    label: Optional[str] = None,
    metadata: Optional[dict[str, Any]] = None,
    operation_history: Optional[list[dict[str, Any]]] = None,
    channel_metadata: Optional[list[ChannelMetadata]] = None,
    previous: Optional["BaseFrame[Any]"] = None,
):
    self._data = data.rechunk(chunks=-1)  # type: ignore [unused-ignore]
    if self._data.ndim == 1:
        self._data = self._data.reshape((1, -1))
    self.sampling_rate = sampling_rate
    self.label = label or "unnamed_frame"
    self.metadata = metadata or {}
    self.operation_history = operation_history or []
    self._previous = previous

    if channel_metadata:
        self._channel_metadata = copy.deepcopy(channel_metadata)
    else:
        self._channel_metadata = [
            ChannelMetadata(label=f"ch{i}", unit="", extra={})
            for i in range(self._n_channels)
        ]

    try:
        # Display information for newer dask versions
        logger.debug(f"Dask graph layers: {list(self._data.dask.layers.keys())}")
        logger.debug(
            f"Dask graph dependencies: {len(self._data.dask.dependencies)}"
        )
    except Exception as e:
        logger.debug(f"Dask graph visualization details unavailable: {e}")
get_channel(channel_idx)

Get channel(s) by index.

Parameters

channel_idx : int or sequence of int Single channel index or sequence of channel indices. Supports negative indices (e.g., -1 for the last channel).

Returns

S New instance containing the selected channel(s).

Examples

frame.get_channel(0) # Single channel frame.get_channel([0, 2, 3]) # Multiple channels frame.get_channel((-1, -2)) # Last two channels frame.get_channel(np.array([1, 2])) # NumPy array of indices

Source code in wandas/core/base_frame.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def get_channel(
    self: S,
    channel_idx: Union[
        int,
        list[int],
        tuple[int, ...],
        npt.NDArray[np.int_],
        npt.NDArray[np.bool_],
    ],
) -> S:
    """
    Get channel(s) by index.

    Parameters
    ----------
    channel_idx : int or sequence of int
        Single channel index or sequence of channel indices.
        Supports negative indices (e.g., -1 for the last channel).

    Returns
    -------
    S
        New instance containing the selected channel(s).

    Examples
    --------
    >>> frame.get_channel(0)  # Single channel
    >>> frame.get_channel([0, 2, 3])  # Multiple channels
    >>> frame.get_channel((-1, -2))  # Last two channels
    >>> frame.get_channel(np.array([1, 2]))  # NumPy array of indices
    """
    if isinstance(channel_idx, int):
        # Convert single channel to a list.
        channel_idx_list: list[int] = [channel_idx]
    else:
        channel_idx_list = list(channel_idx)

    new_data = self._data[channel_idx_list]
    new_channel_metadata = [self._channel_metadata[i] for i in channel_idx_list]
    return self._create_new_instance(
        data=new_data,
        operation_history=self.operation_history,
        channel_metadata=new_channel_metadata,
    )
__len__()

Returns the number of channels.

Source code in wandas/core/base_frame.py
172
173
174
175
176
def __len__(self) -> int:
    """
    Returns the number of channels.
    """
    return len(self._channel_metadata)
__iter__()
Source code in wandas/core/base_frame.py
178
179
180
def __iter__(self: S) -> Iterator[S]:
    for idx in range(len(self)):
        yield self[idx]
__getitem__(key)

Get channel(s) by index, label, or advanced indexing.

This method supports multiple indexing patterns similar to NumPy and pandas:

  • Single channel by index: frame[0]
  • Single channel by label: frame["ch0"]
  • Slice of channels: frame[0:3]
  • Multiple channels by indices: frame[[0, 2, 5]]
  • Multiple channels by labels: frame[["ch0", "ch2"]]
  • NumPy integer array: frame[np.array([0, 2])]
  • Boolean mask: frame[mask] where mask is a boolean array
  • Multidimensional indexing: frame[0, 100:200] (channel + time)
Parameters

key : int, str, slice, list, tuple, or ndarray - int: Single channel index (supports negative indexing) - str: Single channel label - slice: Range of channels - list[int]: Multiple channel indices - list[str]: Multiple channel labels - tuple: Multidimensional indexing (channel_key, time_key, ...) - ndarray[int]: NumPy array of channel indices - ndarray[bool]: Boolean mask for channel selection

Returns

S New instance containing the selected channel(s).

Raises

ValueError If the key length is invalid for the shape or if boolean mask length doesn't match number of channels. IndexError If the channel index is out of range. TypeError If the key type is invalid or list contains mixed types. KeyError If a channel label is not found.

Examples
Single channel selection

frame[0] # First channel frame["acc_x"] # By label frame[-1] # Last channel

Multiple channel selection

frame[[0, 2, 5]] # Multiple indices frame[["acc_x", "acc_z"]] # Multiple labels frame[0:3] # Slice

NumPy array indexing

frame[np.array([0, 2, 4])] # Integer array mask = np.array([True, False, True]) frame[mask] # Boolean mask

Time slicing (multidimensional)

frame[0, 100:200] # Channel 0, samples 100-200 frame[[0, 1], ::2] # Channels 0-1, every 2nd sample

Source code in wandas/core/base_frame.py
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
def __getitem__(
    self: S,
    key: Union[
        int,
        str,
        slice,
        list[int],
        list[str],
        tuple[
            Union[
                int,
                str,
                slice,
                list[int],
                list[str],
                npt.NDArray[np.int_],
                npt.NDArray[np.bool_],
            ],
            ...,
        ],
        npt.NDArray[np.int_],
        npt.NDArray[np.bool_],
    ],
) -> S:
    """
    Get channel(s) by index, label, or advanced indexing.

    This method supports multiple indexing patterns similar to NumPy and pandas:

    - Single channel by index: `frame[0]`
    - Single channel by label: `frame["ch0"]`
    - Slice of channels: `frame[0:3]`
    - Multiple channels by indices: `frame[[0, 2, 5]]`
    - Multiple channels by labels: `frame[["ch0", "ch2"]]`
    - NumPy integer array: `frame[np.array([0, 2])]`
    - Boolean mask: `frame[mask]` where mask is a boolean array
    - Multidimensional indexing: `frame[0, 100:200]` (channel + time)

    Parameters
    ----------
    key : int, str, slice, list, tuple, or ndarray
        - int: Single channel index (supports negative indexing)
        - str: Single channel label
        - slice: Range of channels
        - list[int]: Multiple channel indices
        - list[str]: Multiple channel labels
        - tuple: Multidimensional indexing (channel_key, time_key, ...)
        - ndarray[int]: NumPy array of channel indices
        - ndarray[bool]: Boolean mask for channel selection

    Returns
    -------
    S
        New instance containing the selected channel(s).

    Raises
    ------
    ValueError
        If the key length is invalid for the shape or if boolean mask
        length doesn't match number of channels.
    IndexError
        If the channel index is out of range.
    TypeError
        If the key type is invalid or list contains mixed types.
    KeyError
        If a channel label is not found.

    Examples
    --------
    >>> # Single channel selection
    >>> frame[0]  # First channel
    >>> frame["acc_x"]  # By label
    >>> frame[-1]  # Last channel
    >>>
    >>> # Multiple channel selection
    >>> frame[[0, 2, 5]]  # Multiple indices
    >>> frame[["acc_x", "acc_z"]]  # Multiple labels
    >>> frame[0:3]  # Slice
    >>>
    >>> # NumPy array indexing
    >>> frame[np.array([0, 2, 4])]  # Integer array
    >>> mask = np.array([True, False, True])
    >>> frame[mask]  # Boolean mask
    >>>
    >>> # Time slicing (multidimensional)
    >>> frame[0, 100:200]  # Channel 0, samples 100-200
    >>> frame[[0, 1], ::2]  # Channels 0-1, every 2nd sample
    """

    # Single index (int)
    if isinstance(key, numbers.Integral):
        return self.get_channel(key)

    # Single label (str)
    if isinstance(key, str):
        index = self.label2index(key)
        return self.get_channel(index)

    # Phase 2: NumPy array support (bool mask and int array)
    if isinstance(key, np.ndarray):
        if key.dtype == bool or key.dtype == np.bool_:
            # Boolean mask
            if len(key) != self.n_channels:
                raise ValueError(
                    f"Boolean mask length {len(key)} does not match "
                    f"number of channels {self.n_channels}"
                )
            indices = np.where(key)[0]
            return self.get_channel(indices)
        elif np.issubdtype(key.dtype, np.integer):
            # Integer array
            return self.get_channel(key)
        else:
            raise TypeError(
                f"NumPy array must be of integer or boolean type, got {key.dtype}"
            )

    # Phase 1: List support (int or str)
    if isinstance(key, list):
        if len(key) == 0:
            raise ValueError("Cannot index with an empty list")

        # Check if all elements are strings
        if all(isinstance(k, str) for k in key):
            # Multiple labels - type narrowing for mypy
            str_list = cast(list[str], key)
            indices_from_labels = [self.label2index(label) for label in str_list]
            return self.get_channel(indices_from_labels)

        # Check if all elements are integers
        elif all(isinstance(k, (int, np.integer)) for k in key):
            # Multiple indices - convert to list[int] for type safety
            int_list = [int(k) for k in key]
            return self.get_channel(int_list)

        else:
            raise TypeError(
                f"List must contain all str or all int, got mixed types: "
                f"{[type(k).__name__ for k in key]}"
            )

    # Tuple: multidimensional indexing
    if isinstance(key, tuple):
        return self._handle_multidim_indexing(key)

    # Slice
    if isinstance(key, slice):
        new_data = self._data[key]
        new_channel_metadata = self._channel_metadata[key]
        if isinstance(new_channel_metadata, ChannelMetadata):
            new_channel_metadata = [new_channel_metadata]
        return self._create_new_instance(
            data=new_data,
            operation_history=self.operation_history,
            channel_metadata=new_channel_metadata,
        )

    raise TypeError(
        f"Invalid key type: {type(key).__name__}. "
        f"Expected int, str, slice, list, tuple, or ndarray."
    )
label2index(label)

Get the index from a channel label.

Parameters

label : str Channel label.

Returns

int Corresponding index.

Raises

KeyError If the channel label is not found.

Source code in wandas/core/base_frame.py
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
def label2index(self, label: str) -> int:
    """
    Get the index from a channel label.

    Parameters
    ----------
    label : str
        Channel label.

    Returns
    -------
    int
        Corresponding index.

    Raises
    ------
    KeyError
        If the channel label is not found.
    """
    for idx, ch in enumerate(self._channel_metadata):
        if ch.label == label:
            return idx
    raise KeyError(f"Channel label '{label}' not found.")
compute()

Compute and return the data. This method materializes lazily computed data into a concrete NumPy array.

Returns

NDArrayReal The computed data.

Raises

ValueError If the computed result is not a NumPy array.

Source code in wandas/core/base_frame.py
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
def compute(self) -> T:
    """
    Compute and return the data.
    This method materializes lazily computed data into a concrete NumPy array.

    Returns
    -------
    NDArrayReal
        The computed data.

    Raises
    ------
    ValueError
        If the computed result is not a NumPy array.
    """
    logger.debug(
        "COMPUTING DASK ARRAY - This will trigger file reading and all processing"
    )
    result = self._data.compute()

    if not isinstance(result, np.ndarray):
        raise ValueError(f"Computed result is not a np.ndarray: {type(result)}")

    logger.debug(f"Computation complete, result shape: {result.shape}")
    return cast(T, result)
plot(plot_type='default', ax=None, **kwargs) abstractmethod

Plot the data

Source code in wandas/core/base_frame.py
479
480
481
482
483
484
@abstractmethod
def plot(
    self, plot_type: str = "default", ax: Optional[Axes] = None, **kwargs: Any
) -> Union[Axes, Iterator[Axes]]:
    """Plot the data"""
    pass
persist()

Persist the data in memory

Source code in wandas/core/base_frame.py
486
487
488
489
def persist(self: S) -> S:
    """Persist the data in memory"""
    persisted_data = self._data.persist()
    return self._create_new_instance(data=persisted_data)
__array__(dtype=None)

Implicit conversion to NumPy array

Source code in wandas/core/base_frame.py
537
538
539
540
541
542
def __array__(self, dtype: npt.DTypeLike = None) -> NDArrayReal:
    """Implicit conversion to NumPy array"""
    result = self.compute()
    if dtype is not None:
        return result.astype(dtype)
    return result
visualize_graph(filename=None)

Visualize the computation graph and save it to a file

Source code in wandas/core/base_frame.py
544
545
546
547
548
549
550
551
552
def visualize_graph(self, filename: Optional[str] = None) -> Optional[str]:
    """Visualize the computation graph and save it to a file"""
    try:
        filename = filename or f"graph_{uuid.uuid4().hex[:8]}.png"
        self._data.visualize(filename=filename)
        return filename
    except Exception as e:
        logger.warning(f"Failed to visualize the graph: {e}")
        return None
__add__(other)

Addition operator

Source code in wandas/core/base_frame.py
566
567
568
def __add__(self: S, other: Union[S, int, float, NDArrayReal]) -> S:
    """Addition operator"""
    return self._binary_op(other, lambda x, y: x + y, "+")
__sub__(other)

Subtraction operator

Source code in wandas/core/base_frame.py
570
571
572
def __sub__(self: S, other: Union[S, int, float, NDArrayReal]) -> S:
    """Subtraction operator"""
    return self._binary_op(other, lambda x, y: x - y, "-")
__mul__(other)

Multiplication operator

Source code in wandas/core/base_frame.py
574
575
576
def __mul__(self: S, other: Union[S, int, float, NDArrayReal]) -> S:
    """Multiplication operator"""
    return self._binary_op(other, lambda x, y: x * y, "*")
__truediv__(other)

Division operator

Source code in wandas/core/base_frame.py
578
579
580
def __truediv__(self: S, other: Union[S, int, float, NDArrayReal]) -> S:
    """Division operator"""
    return self._binary_op(other, lambda x, y: x / y, "/")
apply_operation(operation_name, **params)

Apply a named operation.

Parameters

operation_name : str Name of the operation to apply. **params : Any Parameters to pass to the operation.

Returns

S A new instance with the operation applied.

Source code in wandas/core/base_frame.py
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
def apply_operation(self: S, operation_name: str, **params: Any) -> S:
    """
    Apply a named operation.

    Parameters
    ----------
    operation_name : str
        Name of the operation to apply.
    **params : Any
        Parameters to pass to the operation.

    Returns
    -------
    S
        A new instance with the operation applied.
    """
    # Apply the operation through abstract method
    return self._apply_operation_impl(operation_name, **params)
debug_info()

Output detailed debug information

Source code in wandas/core/base_frame.py
606
607
608
609
610
611
612
613
614
def debug_info(self) -> None:
    """Output detailed debug information"""
    logger.debug(f"=== {self.__class__.__name__} Debug Info ===")
    logger.debug(f"Label: {self.label}")
    logger.debug(f"Shape: {self.shape}")
    logger.debug(f"Sampling rate: {self.sampling_rate} Hz")
    logger.debug(f"Operation history: {len(self.operation_history)} operations")
    self._debug_info_impl()
    logger.debug("=== End Debug Info ===")

Modules

base_frame

Attributes
logger = logging.getLogger(__name__) module-attribute
T = TypeVar('T', NDArrayComplex, NDArrayReal) module-attribute
S = TypeVar('S', bound='BaseFrame[Any]') module-attribute
Classes
BaseFrame

Bases: ABC, Generic[T]

Abstract base class for all signal frame types.

This class provides the common interface and functionality for all frame types used in signal processing. It implements basic operations like indexing, iteration, and data manipulation that are shared across all frame types.

Parameters

data : DaArray The signal data to process. Must be a dask array. sampling_rate : float The sampling rate of the signal in Hz. label : str, optional A label for the frame. If not provided, defaults to "unnamed_frame". metadata : dict, optional Additional metadata for the frame. operation_history : list[dict], optional History of operations performed on this frame. channel_metadata : list[ChannelMetadata], optional Metadata for each channel in the frame. previous : BaseFrame, optional The frame that this frame was derived from.

Attributes

sampling_rate : float The sampling rate of the signal in Hz. label : str The label of the frame. metadata : dict Additional metadata for the frame. operation_history : list[dict] History of operations performed on this frame.

Source code in wandas/core/base_frame.py
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
class BaseFrame(ABC, Generic[T]):
    """
    Abstract base class for all signal frame types.

    This class provides the common interface and functionality for all frame types
    used in signal processing. It implements basic operations like indexing, iteration,
    and data manipulation that are shared across all frame types.

    Parameters
    ----------
    data : DaArray
        The signal data to process. Must be a dask array.
    sampling_rate : float
        The sampling rate of the signal in Hz.
    label : str, optional
        A label for the frame. If not provided, defaults to "unnamed_frame".
    metadata : dict, optional
        Additional metadata for the frame.
    operation_history : list[dict], optional
        History of operations performed on this frame.
    channel_metadata : list[ChannelMetadata], optional
        Metadata for each channel in the frame.
    previous : BaseFrame, optional
        The frame that this frame was derived from.

    Attributes
    ----------
    sampling_rate : float
        The sampling rate of the signal in Hz.
    label : str
        The label of the frame.
    metadata : dict
        Additional metadata for the frame.
    operation_history : list[dict]
        History of operations performed on this frame.
    """

    _data: DaArray
    sampling_rate: float
    label: str
    metadata: dict[str, Any]
    operation_history: list[dict[str, Any]]
    _channel_metadata: list[ChannelMetadata]
    _previous: Optional["BaseFrame[Any]"]

    def __init__(
        self,
        data: DaArray,
        sampling_rate: float,
        label: Optional[str] = None,
        metadata: Optional[dict[str, Any]] = None,
        operation_history: Optional[list[dict[str, Any]]] = None,
        channel_metadata: Optional[list[ChannelMetadata]] = None,
        previous: Optional["BaseFrame[Any]"] = None,
    ):
        self._data = data.rechunk(chunks=-1)  # type: ignore [unused-ignore]
        if self._data.ndim == 1:
            self._data = self._data.reshape((1, -1))
        self.sampling_rate = sampling_rate
        self.label = label or "unnamed_frame"
        self.metadata = metadata or {}
        self.operation_history = operation_history or []
        self._previous = previous

        if channel_metadata:
            self._channel_metadata = copy.deepcopy(channel_metadata)
        else:
            self._channel_metadata = [
                ChannelMetadata(label=f"ch{i}", unit="", extra={})
                for i in range(self._n_channels)
            ]

        try:
            # Display information for newer dask versions
            logger.debug(f"Dask graph layers: {list(self._data.dask.layers.keys())}")
            logger.debug(
                f"Dask graph dependencies: {len(self._data.dask.dependencies)}"
            )
        except Exception as e:
            logger.debug(f"Dask graph visualization details unavailable: {e}")

    @property
    @abstractmethod
    def _n_channels(self) -> int:
        """Returns the number of channels."""

    @property
    def n_channels(self) -> int:
        """Returns the number of channels."""
        return self._n_channels

    @property
    def channels(self) -> list[ChannelMetadata]:
        """Property to access channel metadata."""
        return self._channel_metadata

    @property
    def previous(self) -> Optional["BaseFrame[Any]"]:
        """
        Returns the previous frame.
        """
        return self._previous

    def get_channel(
        self: S,
        channel_idx: Union[
            int,
            list[int],
            tuple[int, ...],
            npt.NDArray[np.int_],
            npt.NDArray[np.bool_],
        ],
    ) -> S:
        """
        Get channel(s) by index.

        Parameters
        ----------
        channel_idx : int or sequence of int
            Single channel index or sequence of channel indices.
            Supports negative indices (e.g., -1 for the last channel).

        Returns
        -------
        S
            New instance containing the selected channel(s).

        Examples
        --------
        >>> frame.get_channel(0)  # Single channel
        >>> frame.get_channel([0, 2, 3])  # Multiple channels
        >>> frame.get_channel((-1, -2))  # Last two channels
        >>> frame.get_channel(np.array([1, 2]))  # NumPy array of indices
        """
        if isinstance(channel_idx, int):
            # Convert single channel to a list.
            channel_idx_list: list[int] = [channel_idx]
        else:
            channel_idx_list = list(channel_idx)

        new_data = self._data[channel_idx_list]
        new_channel_metadata = [self._channel_metadata[i] for i in channel_idx_list]
        return self._create_new_instance(
            data=new_data,
            operation_history=self.operation_history,
            channel_metadata=new_channel_metadata,
        )

    def __len__(self) -> int:
        """
        Returns the number of channels.
        """
        return len(self._channel_metadata)

    def __iter__(self: S) -> Iterator[S]:
        for idx in range(len(self)):
            yield self[idx]

    def __getitem__(
        self: S,
        key: Union[
            int,
            str,
            slice,
            list[int],
            list[str],
            tuple[
                Union[
                    int,
                    str,
                    slice,
                    list[int],
                    list[str],
                    npt.NDArray[np.int_],
                    npt.NDArray[np.bool_],
                ],
                ...,
            ],
            npt.NDArray[np.int_],
            npt.NDArray[np.bool_],
        ],
    ) -> S:
        """
        Get channel(s) by index, label, or advanced indexing.

        This method supports multiple indexing patterns similar to NumPy and pandas:

        - Single channel by index: `frame[0]`
        - Single channel by label: `frame["ch0"]`
        - Slice of channels: `frame[0:3]`
        - Multiple channels by indices: `frame[[0, 2, 5]]`
        - Multiple channels by labels: `frame[["ch0", "ch2"]]`
        - NumPy integer array: `frame[np.array([0, 2])]`
        - Boolean mask: `frame[mask]` where mask is a boolean array
        - Multidimensional indexing: `frame[0, 100:200]` (channel + time)

        Parameters
        ----------
        key : int, str, slice, list, tuple, or ndarray
            - int: Single channel index (supports negative indexing)
            - str: Single channel label
            - slice: Range of channels
            - list[int]: Multiple channel indices
            - list[str]: Multiple channel labels
            - tuple: Multidimensional indexing (channel_key, time_key, ...)
            - ndarray[int]: NumPy array of channel indices
            - ndarray[bool]: Boolean mask for channel selection

        Returns
        -------
        S
            New instance containing the selected channel(s).

        Raises
        ------
        ValueError
            If the key length is invalid for the shape or if boolean mask
            length doesn't match number of channels.
        IndexError
            If the channel index is out of range.
        TypeError
            If the key type is invalid or list contains mixed types.
        KeyError
            If a channel label is not found.

        Examples
        --------
        >>> # Single channel selection
        >>> frame[0]  # First channel
        >>> frame["acc_x"]  # By label
        >>> frame[-1]  # Last channel
        >>>
        >>> # Multiple channel selection
        >>> frame[[0, 2, 5]]  # Multiple indices
        >>> frame[["acc_x", "acc_z"]]  # Multiple labels
        >>> frame[0:3]  # Slice
        >>>
        >>> # NumPy array indexing
        >>> frame[np.array([0, 2, 4])]  # Integer array
        >>> mask = np.array([True, False, True])
        >>> frame[mask]  # Boolean mask
        >>>
        >>> # Time slicing (multidimensional)
        >>> frame[0, 100:200]  # Channel 0, samples 100-200
        >>> frame[[0, 1], ::2]  # Channels 0-1, every 2nd sample
        """

        # Single index (int)
        if isinstance(key, numbers.Integral):
            return self.get_channel(key)

        # Single label (str)
        if isinstance(key, str):
            index = self.label2index(key)
            return self.get_channel(index)

        # Phase 2: NumPy array support (bool mask and int array)
        if isinstance(key, np.ndarray):
            if key.dtype == bool or key.dtype == np.bool_:
                # Boolean mask
                if len(key) != self.n_channels:
                    raise ValueError(
                        f"Boolean mask length {len(key)} does not match "
                        f"number of channels {self.n_channels}"
                    )
                indices = np.where(key)[0]
                return self.get_channel(indices)
            elif np.issubdtype(key.dtype, np.integer):
                # Integer array
                return self.get_channel(key)
            else:
                raise TypeError(
                    f"NumPy array must be of integer or boolean type, got {key.dtype}"
                )

        # Phase 1: List support (int or str)
        if isinstance(key, list):
            if len(key) == 0:
                raise ValueError("Cannot index with an empty list")

            # Check if all elements are strings
            if all(isinstance(k, str) for k in key):
                # Multiple labels - type narrowing for mypy
                str_list = cast(list[str], key)
                indices_from_labels = [self.label2index(label) for label in str_list]
                return self.get_channel(indices_from_labels)

            # Check if all elements are integers
            elif all(isinstance(k, (int, np.integer)) for k in key):
                # Multiple indices - convert to list[int] for type safety
                int_list = [int(k) for k in key]
                return self.get_channel(int_list)

            else:
                raise TypeError(
                    f"List must contain all str or all int, got mixed types: "
                    f"{[type(k).__name__ for k in key]}"
                )

        # Tuple: multidimensional indexing
        if isinstance(key, tuple):
            return self._handle_multidim_indexing(key)

        # Slice
        if isinstance(key, slice):
            new_data = self._data[key]
            new_channel_metadata = self._channel_metadata[key]
            if isinstance(new_channel_metadata, ChannelMetadata):
                new_channel_metadata = [new_channel_metadata]
            return self._create_new_instance(
                data=new_data,
                operation_history=self.operation_history,
                channel_metadata=new_channel_metadata,
            )

        raise TypeError(
            f"Invalid key type: {type(key).__name__}. "
            f"Expected int, str, slice, list, tuple, or ndarray."
        )

    def _handle_multidim_indexing(
        self: S,
        key: tuple[
            Union[
                int,
                str,
                slice,
                list[int],
                list[str],
                npt.NDArray[np.int_],
                npt.NDArray[np.bool_],
            ],
            ...,
        ],
    ) -> S:
        """
        Handle multidimensional indexing (channel + time axis).

        Parameters
        ----------
        key : tuple
            Tuple of indices where the first element selects channels
            and subsequent elements select along other dimensions (e.g., time).

        Returns
        -------
        S
            New instance with selected channels and time range.

        Raises
        ------
        ValueError
            If the key length exceeds the data dimensions.
        """
        if len(key) > self._data.ndim:
            raise ValueError(f"Invalid key length: {len(key)} for shape {self.shape}")

        # First element: channel selection
        channel_key = key[0]
        time_keys = key[1:] if len(key) > 1 else ()

        # Select channels first (recursively call __getitem__)
        if isinstance(channel_key, (list, np.ndarray)):
            selected = self[channel_key]
        elif isinstance(channel_key, (int, str, slice)):
            selected = self[channel_key]
        else:
            raise TypeError(
                f"Invalid channel key type in tuple: {type(channel_key).__name__}"
            )

        # Apply time indexing if present
        if time_keys:
            new_data = selected._data[(slice(None),) + time_keys]
            return selected._create_new_instance(
                data=new_data,
                operation_history=selected.operation_history,
                channel_metadata=selected._channel_metadata,
            )

        return selected

    def label2index(self, label: str) -> int:
        """
        Get the index from a channel label.

        Parameters
        ----------
        label : str
            Channel label.

        Returns
        -------
        int
            Corresponding index.

        Raises
        ------
        KeyError
            If the channel label is not found.
        """
        for idx, ch in enumerate(self._channel_metadata):
            if ch.label == label:
                return idx
        raise KeyError(f"Channel label '{label}' not found.")

    @property
    def shape(self) -> tuple[int, ...]:
        _shape: tuple[int, ...] = self._data.shape
        if _shape[0] == 1:
            return _shape[1:]
        return _shape

    @property
    def data(self) -> T:
        """
        Returns the computed data.
        Calculation is executed the first time this is accessed.
        """
        data = self.compute()
        if self.n_channels == 1:
            return data.squeeze(axis=0)
        return data

    @property
    def labels(self) -> list[str]:
        """Get a list of all channel labels."""
        return [ch.label for ch in self._channel_metadata]

    def compute(self) -> T:
        """
        Compute and return the data.
        This method materializes lazily computed data into a concrete NumPy array.

        Returns
        -------
        NDArrayReal
            The computed data.

        Raises
        ------
        ValueError
            If the computed result is not a NumPy array.
        """
        logger.debug(
            "COMPUTING DASK ARRAY - This will trigger file reading and all processing"
        )
        result = self._data.compute()

        if not isinstance(result, np.ndarray):
            raise ValueError(f"Computed result is not a np.ndarray: {type(result)}")

        logger.debug(f"Computation complete, result shape: {result.shape}")
        return cast(T, result)

    @abstractmethod
    def plot(
        self, plot_type: str = "default", ax: Optional[Axes] = None, **kwargs: Any
    ) -> Union[Axes, Iterator[Axes]]:
        """Plot the data"""
        pass

    def persist(self: S) -> S:
        """Persist the data in memory"""
        persisted_data = self._data.persist()
        return self._create_new_instance(data=persisted_data)

    @abstractmethod
    def _get_additional_init_kwargs(self) -> dict[str, Any]:
        """
        Abstract method for derived classes to provide
        additional initialization arguments.
        """
        pass

    def _create_new_instance(self: S, data: DaArray, **kwargs: Any) -> S:
        """
        Create a new channel instance based on an existing channel.
        Keyword arguments can override or extend the original attributes.
        """

        sampling_rate = kwargs.pop("sampling_rate", self.sampling_rate)
        # if not isinstance(sampling_rate, int):
        #     raise TypeError("Sampling rate must be an integer")

        label = kwargs.pop("label", self.label)
        if not isinstance(label, str):
            raise TypeError("Label must be a string")

        metadata = kwargs.pop("metadata", copy.deepcopy(self.metadata))
        if not isinstance(metadata, dict):
            raise TypeError("Metadata must be a dictionary")

        channel_metadata = kwargs.pop(
            "channel_metadata", copy.deepcopy(self._channel_metadata)
        )
        if not isinstance(channel_metadata, list):
            raise TypeError("Channel metadata must be a list")

        # Get additional initialization arguments from derived classes
        additional_kwargs = self._get_additional_init_kwargs()
        kwargs.update(additional_kwargs)

        return type(self)(
            data=data,
            sampling_rate=sampling_rate,
            label=label,
            metadata=metadata,
            channel_metadata=channel_metadata,
            previous=self,
            **kwargs,
        )

    def __array__(self, dtype: npt.DTypeLike = None) -> NDArrayReal:
        """Implicit conversion to NumPy array"""
        result = self.compute()
        if dtype is not None:
            return result.astype(dtype)
        return result

    def visualize_graph(self, filename: Optional[str] = None) -> Optional[str]:
        """Visualize the computation graph and save it to a file"""
        try:
            filename = filename or f"graph_{uuid.uuid4().hex[:8]}.png"
            self._data.visualize(filename=filename)
            return filename
        except Exception as e:
            logger.warning(f"Failed to visualize the graph: {e}")
            return None

    @abstractmethod
    def _binary_op(
        self: S,
        other: Union[S, int, float, NDArrayReal, DaArray],
        op: Callable[[DaArray, Any], DaArray],
        symbol: str,
    ) -> S:
        """Basic implementation of binary operations"""
        # Basic logic
        # Actual implementation is left to derived classes
        pass

    def __add__(self: S, other: Union[S, int, float, NDArrayReal]) -> S:
        """Addition operator"""
        return self._binary_op(other, lambda x, y: x + y, "+")

    def __sub__(self: S, other: Union[S, int, float, NDArrayReal]) -> S:
        """Subtraction operator"""
        return self._binary_op(other, lambda x, y: x - y, "-")

    def __mul__(self: S, other: Union[S, int, float, NDArrayReal]) -> S:
        """Multiplication operator"""
        return self._binary_op(other, lambda x, y: x * y, "*")

    def __truediv__(self: S, other: Union[S, int, float, NDArrayReal]) -> S:
        """Division operator"""
        return self._binary_op(other, lambda x, y: x / y, "/")

    def apply_operation(self: S, operation_name: str, **params: Any) -> S:
        """
        Apply a named operation.

        Parameters
        ----------
        operation_name : str
            Name of the operation to apply.
        **params : Any
            Parameters to pass to the operation.

        Returns
        -------
        S
            A new instance with the operation applied.
        """
        # Apply the operation through abstract method
        return self._apply_operation_impl(operation_name, **params)

    @abstractmethod
    def _apply_operation_impl(self: S, operation_name: str, **params: Any) -> S:
        """Implementation of operation application"""
        pass

    def debug_info(self) -> None:
        """Output detailed debug information"""
        logger.debug(f"=== {self.__class__.__name__} Debug Info ===")
        logger.debug(f"Label: {self.label}")
        logger.debug(f"Shape: {self.shape}")
        logger.debug(f"Sampling rate: {self.sampling_rate} Hz")
        logger.debug(f"Operation history: {len(self.operation_history)} operations")
        self._debug_info_impl()
        logger.debug("=== End Debug Info ===")

    def _debug_info_impl(self) -> None:
        """Implement derived class-specific debug information"""
        pass
Attributes
sampling_rate = sampling_rate instance-attribute
label = label or 'unnamed_frame' instance-attribute
metadata = metadata or {} instance-attribute
operation_history = operation_history or [] instance-attribute
n_channels property

Returns the number of channels.

channels property

Property to access channel metadata.

previous property

Returns the previous frame.

shape property
data property

Returns the computed data. Calculation is executed the first time this is accessed.

labels property

Get a list of all channel labels.

Functions
__init__(data, sampling_rate, label=None, metadata=None, operation_history=None, channel_metadata=None, previous=None)
Source code in wandas/core/base_frame.py
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def __init__(
    self,
    data: DaArray,
    sampling_rate: float,
    label: Optional[str] = None,
    metadata: Optional[dict[str, Any]] = None,
    operation_history: Optional[list[dict[str, Any]]] = None,
    channel_metadata: Optional[list[ChannelMetadata]] = None,
    previous: Optional["BaseFrame[Any]"] = None,
):
    self._data = data.rechunk(chunks=-1)  # type: ignore [unused-ignore]
    if self._data.ndim == 1:
        self._data = self._data.reshape((1, -1))
    self.sampling_rate = sampling_rate
    self.label = label or "unnamed_frame"
    self.metadata = metadata or {}
    self.operation_history = operation_history or []
    self._previous = previous

    if channel_metadata:
        self._channel_metadata = copy.deepcopy(channel_metadata)
    else:
        self._channel_metadata = [
            ChannelMetadata(label=f"ch{i}", unit="", extra={})
            for i in range(self._n_channels)
        ]

    try:
        # Display information for newer dask versions
        logger.debug(f"Dask graph layers: {list(self._data.dask.layers.keys())}")
        logger.debug(
            f"Dask graph dependencies: {len(self._data.dask.dependencies)}"
        )
    except Exception as e:
        logger.debug(f"Dask graph visualization details unavailable: {e}")
get_channel(channel_idx)

Get channel(s) by index.

Parameters

channel_idx : int or sequence of int Single channel index or sequence of channel indices. Supports negative indices (e.g., -1 for the last channel).

Returns

S New instance containing the selected channel(s).

Examples

frame.get_channel(0) # Single channel frame.get_channel([0, 2, 3]) # Multiple channels frame.get_channel((-1, -2)) # Last two channels frame.get_channel(np.array([1, 2])) # NumPy array of indices

Source code in wandas/core/base_frame.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def get_channel(
    self: S,
    channel_idx: Union[
        int,
        list[int],
        tuple[int, ...],
        npt.NDArray[np.int_],
        npt.NDArray[np.bool_],
    ],
) -> S:
    """
    Get channel(s) by index.

    Parameters
    ----------
    channel_idx : int or sequence of int
        Single channel index or sequence of channel indices.
        Supports negative indices (e.g., -1 for the last channel).

    Returns
    -------
    S
        New instance containing the selected channel(s).

    Examples
    --------
    >>> frame.get_channel(0)  # Single channel
    >>> frame.get_channel([0, 2, 3])  # Multiple channels
    >>> frame.get_channel((-1, -2))  # Last two channels
    >>> frame.get_channel(np.array([1, 2]))  # NumPy array of indices
    """
    if isinstance(channel_idx, int):
        # Convert single channel to a list.
        channel_idx_list: list[int] = [channel_idx]
    else:
        channel_idx_list = list(channel_idx)

    new_data = self._data[channel_idx_list]
    new_channel_metadata = [self._channel_metadata[i] for i in channel_idx_list]
    return self._create_new_instance(
        data=new_data,
        operation_history=self.operation_history,
        channel_metadata=new_channel_metadata,
    )
__len__()

Returns the number of channels.

Source code in wandas/core/base_frame.py
172
173
174
175
176
def __len__(self) -> int:
    """
    Returns the number of channels.
    """
    return len(self._channel_metadata)
__iter__()
Source code in wandas/core/base_frame.py
178
179
180
def __iter__(self: S) -> Iterator[S]:
    for idx in range(len(self)):
        yield self[idx]
__getitem__(key)

Get channel(s) by index, label, or advanced indexing.

This method supports multiple indexing patterns similar to NumPy and pandas:

  • Single channel by index: frame[0]
  • Single channel by label: frame["ch0"]
  • Slice of channels: frame[0:3]
  • Multiple channels by indices: frame[[0, 2, 5]]
  • Multiple channels by labels: frame[["ch0", "ch2"]]
  • NumPy integer array: frame[np.array([0, 2])]
  • Boolean mask: frame[mask] where mask is a boolean array
  • Multidimensional indexing: frame[0, 100:200] (channel + time)
Parameters

key : int, str, slice, list, tuple, or ndarray - int: Single channel index (supports negative indexing) - str: Single channel label - slice: Range of channels - list[int]: Multiple channel indices - list[str]: Multiple channel labels - tuple: Multidimensional indexing (channel_key, time_key, ...) - ndarray[int]: NumPy array of channel indices - ndarray[bool]: Boolean mask for channel selection

Returns

S New instance containing the selected channel(s).

Raises

ValueError If the key length is invalid for the shape or if boolean mask length doesn't match number of channels. IndexError If the channel index is out of range. TypeError If the key type is invalid or list contains mixed types. KeyError If a channel label is not found.

Examples
Single channel selection

frame[0] # First channel frame["acc_x"] # By label frame[-1] # Last channel

Multiple channel selection

frame[[0, 2, 5]] # Multiple indices frame[["acc_x", "acc_z"]] # Multiple labels frame[0:3] # Slice

NumPy array indexing

frame[np.array([0, 2, 4])] # Integer array mask = np.array([True, False, True]) frame[mask] # Boolean mask

Time slicing (multidimensional)

frame[0, 100:200] # Channel 0, samples 100-200 frame[[0, 1], ::2] # Channels 0-1, every 2nd sample

Source code in wandas/core/base_frame.py
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
def __getitem__(
    self: S,
    key: Union[
        int,
        str,
        slice,
        list[int],
        list[str],
        tuple[
            Union[
                int,
                str,
                slice,
                list[int],
                list[str],
                npt.NDArray[np.int_],
                npt.NDArray[np.bool_],
            ],
            ...,
        ],
        npt.NDArray[np.int_],
        npt.NDArray[np.bool_],
    ],
) -> S:
    """
    Get channel(s) by index, label, or advanced indexing.

    This method supports multiple indexing patterns similar to NumPy and pandas:

    - Single channel by index: `frame[0]`
    - Single channel by label: `frame["ch0"]`
    - Slice of channels: `frame[0:3]`
    - Multiple channels by indices: `frame[[0, 2, 5]]`
    - Multiple channels by labels: `frame[["ch0", "ch2"]]`
    - NumPy integer array: `frame[np.array([0, 2])]`
    - Boolean mask: `frame[mask]` where mask is a boolean array
    - Multidimensional indexing: `frame[0, 100:200]` (channel + time)

    Parameters
    ----------
    key : int, str, slice, list, tuple, or ndarray
        - int: Single channel index (supports negative indexing)
        - str: Single channel label
        - slice: Range of channels
        - list[int]: Multiple channel indices
        - list[str]: Multiple channel labels
        - tuple: Multidimensional indexing (channel_key, time_key, ...)
        - ndarray[int]: NumPy array of channel indices
        - ndarray[bool]: Boolean mask for channel selection

    Returns
    -------
    S
        New instance containing the selected channel(s).

    Raises
    ------
    ValueError
        If the key length is invalid for the shape or if boolean mask
        length doesn't match number of channels.
    IndexError
        If the channel index is out of range.
    TypeError
        If the key type is invalid or list contains mixed types.
    KeyError
        If a channel label is not found.

    Examples
    --------
    >>> # Single channel selection
    >>> frame[0]  # First channel
    >>> frame["acc_x"]  # By label
    >>> frame[-1]  # Last channel
    >>>
    >>> # Multiple channel selection
    >>> frame[[0, 2, 5]]  # Multiple indices
    >>> frame[["acc_x", "acc_z"]]  # Multiple labels
    >>> frame[0:3]  # Slice
    >>>
    >>> # NumPy array indexing
    >>> frame[np.array([0, 2, 4])]  # Integer array
    >>> mask = np.array([True, False, True])
    >>> frame[mask]  # Boolean mask
    >>>
    >>> # Time slicing (multidimensional)
    >>> frame[0, 100:200]  # Channel 0, samples 100-200
    >>> frame[[0, 1], ::2]  # Channels 0-1, every 2nd sample
    """

    # Single index (int)
    if isinstance(key, numbers.Integral):
        return self.get_channel(key)

    # Single label (str)
    if isinstance(key, str):
        index = self.label2index(key)
        return self.get_channel(index)

    # Phase 2: NumPy array support (bool mask and int array)
    if isinstance(key, np.ndarray):
        if key.dtype == bool or key.dtype == np.bool_:
            # Boolean mask
            if len(key) != self.n_channels:
                raise ValueError(
                    f"Boolean mask length {len(key)} does not match "
                    f"number of channels {self.n_channels}"
                )
            indices = np.where(key)[0]
            return self.get_channel(indices)
        elif np.issubdtype(key.dtype, np.integer):
            # Integer array
            return self.get_channel(key)
        else:
            raise TypeError(
                f"NumPy array must be of integer or boolean type, got {key.dtype}"
            )

    # Phase 1: List support (int or str)
    if isinstance(key, list):
        if len(key) == 0:
            raise ValueError("Cannot index with an empty list")

        # Check if all elements are strings
        if all(isinstance(k, str) for k in key):
            # Multiple labels - type narrowing for mypy
            str_list = cast(list[str], key)
            indices_from_labels = [self.label2index(label) for label in str_list]
            return self.get_channel(indices_from_labels)

        # Check if all elements are integers
        elif all(isinstance(k, (int, np.integer)) for k in key):
            # Multiple indices - convert to list[int] for type safety
            int_list = [int(k) for k in key]
            return self.get_channel(int_list)

        else:
            raise TypeError(
                f"List must contain all str or all int, got mixed types: "
                f"{[type(k).__name__ for k in key]}"
            )

    # Tuple: multidimensional indexing
    if isinstance(key, tuple):
        return self._handle_multidim_indexing(key)

    # Slice
    if isinstance(key, slice):
        new_data = self._data[key]
        new_channel_metadata = self._channel_metadata[key]
        if isinstance(new_channel_metadata, ChannelMetadata):
            new_channel_metadata = [new_channel_metadata]
        return self._create_new_instance(
            data=new_data,
            operation_history=self.operation_history,
            channel_metadata=new_channel_metadata,
        )

    raise TypeError(
        f"Invalid key type: {type(key).__name__}. "
        f"Expected int, str, slice, list, tuple, or ndarray."
    )
label2index(label)

Get the index from a channel label.

Parameters

label : str Channel label.

Returns

int Corresponding index.

Raises

KeyError If the channel label is not found.

Source code in wandas/core/base_frame.py
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
def label2index(self, label: str) -> int:
    """
    Get the index from a channel label.

    Parameters
    ----------
    label : str
        Channel label.

    Returns
    -------
    int
        Corresponding index.

    Raises
    ------
    KeyError
        If the channel label is not found.
    """
    for idx, ch in enumerate(self._channel_metadata):
        if ch.label == label:
            return idx
    raise KeyError(f"Channel label '{label}' not found.")
compute()

Compute and return the data. This method materializes lazily computed data into a concrete NumPy array.

Returns

NDArrayReal The computed data.

Raises

ValueError If the computed result is not a NumPy array.

Source code in wandas/core/base_frame.py
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
def compute(self) -> T:
    """
    Compute and return the data.
    This method materializes lazily computed data into a concrete NumPy array.

    Returns
    -------
    NDArrayReal
        The computed data.

    Raises
    ------
    ValueError
        If the computed result is not a NumPy array.
    """
    logger.debug(
        "COMPUTING DASK ARRAY - This will trigger file reading and all processing"
    )
    result = self._data.compute()

    if not isinstance(result, np.ndarray):
        raise ValueError(f"Computed result is not a np.ndarray: {type(result)}")

    logger.debug(f"Computation complete, result shape: {result.shape}")
    return cast(T, result)
plot(plot_type='default', ax=None, **kwargs) abstractmethod

Plot the data

Source code in wandas/core/base_frame.py
479
480
481
482
483
484
@abstractmethod
def plot(
    self, plot_type: str = "default", ax: Optional[Axes] = None, **kwargs: Any
) -> Union[Axes, Iterator[Axes]]:
    """Plot the data"""
    pass
persist()

Persist the data in memory

Source code in wandas/core/base_frame.py
486
487
488
489
def persist(self: S) -> S:
    """Persist the data in memory"""
    persisted_data = self._data.persist()
    return self._create_new_instance(data=persisted_data)
__array__(dtype=None)

Implicit conversion to NumPy array

Source code in wandas/core/base_frame.py
537
538
539
540
541
542
def __array__(self, dtype: npt.DTypeLike = None) -> NDArrayReal:
    """Implicit conversion to NumPy array"""
    result = self.compute()
    if dtype is not None:
        return result.astype(dtype)
    return result
visualize_graph(filename=None)

Visualize the computation graph and save it to a file

Source code in wandas/core/base_frame.py
544
545
546
547
548
549
550
551
552
def visualize_graph(self, filename: Optional[str] = None) -> Optional[str]:
    """Visualize the computation graph and save it to a file"""
    try:
        filename = filename or f"graph_{uuid.uuid4().hex[:8]}.png"
        self._data.visualize(filename=filename)
        return filename
    except Exception as e:
        logger.warning(f"Failed to visualize the graph: {e}")
        return None
__add__(other)

Addition operator

Source code in wandas/core/base_frame.py
566
567
568
def __add__(self: S, other: Union[S, int, float, NDArrayReal]) -> S:
    """Addition operator"""
    return self._binary_op(other, lambda x, y: x + y, "+")
__sub__(other)

Subtraction operator

Source code in wandas/core/base_frame.py
570
571
572
def __sub__(self: S, other: Union[S, int, float, NDArrayReal]) -> S:
    """Subtraction operator"""
    return self._binary_op(other, lambda x, y: x - y, "-")
__mul__(other)

Multiplication operator

Source code in wandas/core/base_frame.py
574
575
576
def __mul__(self: S, other: Union[S, int, float, NDArrayReal]) -> S:
    """Multiplication operator"""
    return self._binary_op(other, lambda x, y: x * y, "*")
__truediv__(other)

Division operator

Source code in wandas/core/base_frame.py
578
579
580
def __truediv__(self: S, other: Union[S, int, float, NDArrayReal]) -> S:
    """Division operator"""
    return self._binary_op(other, lambda x, y: x / y, "/")
apply_operation(operation_name, **params)

Apply a named operation.

Parameters

operation_name : str Name of the operation to apply. **params : Any Parameters to pass to the operation.

Returns

S A new instance with the operation applied.

Source code in wandas/core/base_frame.py
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
def apply_operation(self: S, operation_name: str, **params: Any) -> S:
    """
    Apply a named operation.

    Parameters
    ----------
    operation_name : str
        Name of the operation to apply.
    **params : Any
        Parameters to pass to the operation.

    Returns
    -------
    S
        A new instance with the operation applied.
    """
    # Apply the operation through abstract method
    return self._apply_operation_impl(operation_name, **params)
debug_info()

Output detailed debug information

Source code in wandas/core/base_frame.py
606
607
608
609
610
611
612
613
614
def debug_info(self) -> None:
    """Output detailed debug information"""
    logger.debug(f"=== {self.__class__.__name__} Debug Info ===")
    logger.debug(f"Label: {self.label}")
    logger.debug(f"Shape: {self.shape}")
    logger.debug(f"Sampling rate: {self.sampling_rate} Hz")
    logger.debug(f"Operation history: {len(self.operation_history)} operations")
    self._debug_info_impl()
    logger.debug("=== End Debug Info ===")

metadata

Classes
ChannelMetadata

Bases: BaseModel

Data class for storing channel metadata

Source code in wandas/core/metadata.py
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
class ChannelMetadata(BaseModel):
    """
    Data class for storing channel metadata
    """

    label: str = ""
    unit: str = ""
    ref: float = 1.0
    # Additional metadata for extensibility
    extra: dict[str, Any] = Field(default_factory=dict)

    def __init__(self, **data: Any):
        super().__init__(**data)
        # unitが指定されていてrefがデフォルト値ならunit_to_refで自動設定
        if self.unit and ("ref" not in data or data.get("ref", 1.0) == 1.0):
            self.ref = unit_to_ref(self.unit)

    def __setattr__(self, name: str, value: Any) -> None:
        """Override setattr to update ref when unit is changed directly"""
        super().__setattr__(name, value)
        # Only proceed if unit is being set to a non-empty value
        if name == "unit" and value and isinstance(value, str):
            super().__setattr__("ref", unit_to_ref(value))

    @property
    def label_value(self) -> str:
        """Get the label value"""
        return self.label

    @property
    def unit_value(self) -> str:
        """Get the unit value"""
        return self.unit

    @property
    def ref_value(self) -> float:
        """Get the ref value"""
        return self.ref

    @property
    def extra_data(self) -> dict[str, Any]:
        """Get the extra metadata dictionary"""
        return self.extra

    def __getitem__(self, key: str) -> Any:
        """Provide dictionary-like behavior"""
        if key == "label":
            return self.label
        elif key == "unit":
            return self.unit
        elif key == "ref":
            return self.ref
        else:
            return self.extra.get(key)

    def __setitem__(self, key: str, value: Any) -> None:
        """Provide dictionary-like behavior"""
        if key == "label":
            self.label = value
        elif key == "unit":
            self.unit = value
            self.ref = unit_to_ref(value)
        elif key == "ref":
            self.ref = value
        else:
            self.extra[key] = value

    def to_json(self) -> str:
        """Convert to JSON format"""
        json_data: str = self.model_dump_json(indent=4)
        return json_data

    @classmethod
    def from_json(cls, json_data: str) -> "ChannelMetadata":
        """Convert from JSON format"""
        root_model: ChannelMetadata = ChannelMetadata.model_validate_json(json_data)

        return root_model
Attributes
label = '' class-attribute instance-attribute
unit = '' class-attribute instance-attribute
ref = 1.0 class-attribute instance-attribute
extra = Field(default_factory=dict) class-attribute instance-attribute
label_value property

Get the label value

unit_value property

Get the unit value

ref_value property

Get the ref value

extra_data property

Get the extra metadata dictionary

Functions
__init__(**data)
Source code in wandas/core/metadata.py
19
20
21
22
23
def __init__(self, **data: Any):
    super().__init__(**data)
    # unitが指定されていてrefがデフォルト値ならunit_to_refで自動設定
    if self.unit and ("ref" not in data or data.get("ref", 1.0) == 1.0):
        self.ref = unit_to_ref(self.unit)
__setattr__(name, value)

Override setattr to update ref when unit is changed directly

Source code in wandas/core/metadata.py
25
26
27
28
29
30
def __setattr__(self, name: str, value: Any) -> None:
    """Override setattr to update ref when unit is changed directly"""
    super().__setattr__(name, value)
    # Only proceed if unit is being set to a non-empty value
    if name == "unit" and value and isinstance(value, str):
        super().__setattr__("ref", unit_to_ref(value))
__getitem__(key)

Provide dictionary-like behavior

Source code in wandas/core/metadata.py
52
53
54
55
56
57
58
59
60
61
def __getitem__(self, key: str) -> Any:
    """Provide dictionary-like behavior"""
    if key == "label":
        return self.label
    elif key == "unit":
        return self.unit
    elif key == "ref":
        return self.ref
    else:
        return self.extra.get(key)
__setitem__(key, value)

Provide dictionary-like behavior

Source code in wandas/core/metadata.py
63
64
65
66
67
68
69
70
71
72
73
def __setitem__(self, key: str, value: Any) -> None:
    """Provide dictionary-like behavior"""
    if key == "label":
        self.label = value
    elif key == "unit":
        self.unit = value
        self.ref = unit_to_ref(value)
    elif key == "ref":
        self.ref = value
    else:
        self.extra[key] = value
to_json()

Convert to JSON format

Source code in wandas/core/metadata.py
75
76
77
78
def to_json(self) -> str:
    """Convert to JSON format"""
    json_data: str = self.model_dump_json(indent=4)
    return json_data
from_json(json_data) classmethod

Convert from JSON format

Source code in wandas/core/metadata.py
80
81
82
83
84
85
@classmethod
def from_json(cls, json_data: str) -> "ChannelMetadata":
    """Convert from JSON format"""
    root_model: ChannelMetadata = ChannelMetadata.model_validate_json(json_data)

    return root_model
Functions

フレームモジュール

フレームモジュールは異なるタイプのデータフレームを定義します。

wandas.frames

Modules

channel

Attributes
logger = logging.getLogger(__name__) module-attribute
dask_delayed = dask.delayed module-attribute
da_from_delayed = da.from_delayed module-attribute
da_from_array = da.from_array module-attribute
S = TypeVar('S', bound='BaseFrame[Any]') module-attribute
Classes
ChannelFrame

Bases: BaseFrame[NDArrayReal], ChannelProcessingMixin, ChannelTransformMixin

Channel-based data frame for handling audio signals and time series data.

This frame represents channel-based data such as audio signals and time series data, with each channel containing data samples in the time domain.

Source code in wandas/frames/channel.py
  32
  33
  34
  35
  36
  37
  38
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
class ChannelFrame(
    BaseFrame[NDArrayReal], ChannelProcessingMixin, ChannelTransformMixin
):
    """Channel-based data frame for handling audio signals and time series data.

    This frame represents channel-based data such as audio signals and time series data,
    with each channel containing data samples in the time domain.
    """

    def __init__(
        self,
        data: DaskArray,
        sampling_rate: float,
        label: Optional[str] = None,
        metadata: Optional[dict[str, Any]] = None,
        operation_history: Optional[list[dict[str, Any]]] = None,
        channel_metadata: Optional[list[ChannelMetadata]] = None,
        previous: Optional["BaseFrame[Any]"] = None,
    ) -> None:
        """Initialize a ChannelFrame.

        Args:
            data: Dask array containing channel data.
            Shape should be (n_channels, n_samples).
            sampling_rate: The sampling rate of the data in Hz.
            label: A label for the frame.
            metadata: Optional metadata dictionary.
            operation_history: History of operations applied to the frame.
            channel_metadata: Metadata for each channel.
            previous: Reference to the previous frame in the processing chain.

        Raises:
            ValueError: If data has more than 2 dimensions.
        """
        if data.ndim == 1:
            data = da.reshape(data, (1, -1))
        elif data.ndim > 2:
            raise ValueError(
                f"Data must be 1-dimensional or 2-dimensional. Shape: {data.shape}"
            )
        super().__init__(
            data=data,
            sampling_rate=sampling_rate,
            label=label,
            metadata=metadata,
            operation_history=operation_history,
            channel_metadata=channel_metadata,
            previous=previous,
        )

    @property
    def _n_channels(self) -> int:
        """Returns the number of channels."""
        return int(self._data.shape[-2])

    @property
    def time(self) -> NDArrayReal:
        """Get time array for the signal.

        Returns:
            Array of time points in seconds.
        """
        return np.arange(self.n_samples) / self.sampling_rate

    @property
    def n_samples(self) -> int:
        """Returns the number of samples."""
        n: int = self._data.shape[-1]
        return n

    @property
    def duration(self) -> float:
        """Returns the duration in seconds."""
        return self.n_samples / self.sampling_rate

    @property
    def rms(self) -> NDArrayReal:
        """Calculate RMS (Root Mean Square) value for each channel.

        Returns:
            Array of RMS values, one per channel.

        Examples:
            >>> cf = ChannelFrame.read_wav("audio.wav")
            >>> rms_values = cf.rms
            >>> print(f"RMS values: {rms_values}")
            >>> # Select channels with RMS > threshold
            >>> active_channels = cf[cf.rms > 0.5]
        """
        # Compute RMS for each channel: sqrt(mean(x^2))
        data = self.data  # This will trigger computation if lazy

        # Ensure data is 2D (n_channels, n_samples)
        if data.ndim == 1:
            data = data.reshape(1, -1)

        # Convert to a concrete NumPy ndarray to satisfy numpy.mean typing
        # and to ensure dask arrays are materialized for this operation.
        arr: NDArrayReal = np.asarray(data)
        rms_values: NDArrayReal = np.sqrt(np.mean(arr**2, axis=1))  # type: ignore [arg-type]
        return rms_values

    def _apply_operation_impl(self: S, operation_name: str, **params: Any) -> S:
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")
        from ..processing import create_operation

        # Create operation instance
        operation = create_operation(operation_name, self.sampling_rate, **params)

        # Apply processing to data
        processed_data = operation.process(self._data)

        # Update metadata
        operation_metadata = {"operation": operation_name, "params": params}
        new_history = self.operation_history.copy()
        new_history.append(operation_metadata)
        new_metadata = {**self.metadata}
        new_metadata[operation_name] = params

        logger.debug(
            f"Created new ChannelFrame with operation {operation_name} added to graph"
        )
        if operation_name == "resampling":
            # For resampling, update sampling rate
            return self._create_new_instance(
                sampling_rate=params["target_sr"],
                data=processed_data,
                metadata=new_metadata,
                operation_history=new_history,
            )
        return self._create_new_instance(
            data=processed_data,
            metadata=new_metadata,
            operation_history=new_history,
        )

    def _binary_op(
        self,
        other: Union["ChannelFrame", int, float, NDArrayReal, "DaskArray"],
        op: Callable[["DaskArray", Any], "DaskArray"],
        symbol: str,
    ) -> "ChannelFrame":
        """
        Common implementation for binary operations
        - utilizing dask's lazy evaluation.

        Args:
            other: Right operand for the operation.
            op: Function to execute the operation (e.g., lambda a, b: a + b).
            symbol: Symbolic representation of the operation (e.g., '+').

        Returns:
            A new channel containing the operation result (lazy execution).
        """
        from .channel import ChannelFrame

        logger.debug(f"Setting up {symbol} operation (lazy)")

        # Handle potentially None metadata and operation_history
        metadata = {}
        if self.metadata is not None:
            metadata = self.metadata.copy()

        operation_history = []
        if self.operation_history is not None:
            operation_history = self.operation_history.copy()

        # Check if other is a ChannelFrame - improved type checking
        if isinstance(other, ChannelFrame):
            if self.sampling_rate != other.sampling_rate:
                raise ValueError(
                    "Sampling rates do not match. Cannot perform operation."
                )

            # Perform operation directly on dask array (maintaining lazy execution)
            result_data = op(self._data, other._data)

            # Merge channel metadata
            merged_channel_metadata = []
            for self_ch, other_ch in zip(
                self._channel_metadata, other._channel_metadata
            ):
                ch = self_ch.model_copy(deep=True)
                ch["label"] = f"({self_ch['label']} {symbol} {other_ch['label']})"
                merged_channel_metadata.append(ch)

            operation_history.append({"operation": symbol, "with": other.label})

            return ChannelFrame(
                data=result_data,
                sampling_rate=self.sampling_rate,
                label=f"({self.label} {symbol} {other.label})",
                metadata=metadata,
                operation_history=operation_history,
                channel_metadata=merged_channel_metadata,
                previous=self,
            )

        # Perform operation with scalar, NumPy array, or other types
        else:
            # Apply operation directly on dask array (maintaining lazy execution)
            result_data = op(self._data, other)

            # Operand display string
            if isinstance(other, (int, float)):
                other_str = str(other)
            elif isinstance(other, np.ndarray):
                other_str = f"ndarray{other.shape}"
            elif hasattr(other, "shape"):  # Check for dask.array.Array
                other_str = f"dask.array{other.shape}"
            else:
                other_str = str(type(other).__name__)

            # Update channel metadata
            updated_channel_metadata: list[ChannelMetadata] = []
            for self_ch in self._channel_metadata:
                ch = self_ch.model_copy(deep=True)
                ch["label"] = f"({self_ch.label} {symbol} {other_str})"
                updated_channel_metadata.append(ch)

            operation_history.append({"operation": symbol, "with": other_str})

            return ChannelFrame(
                data=result_data,
                sampling_rate=self.sampling_rate,
                label=f"({self.label} {symbol} {other_str})",
                metadata=metadata,
                operation_history=operation_history,
                channel_metadata=updated_channel_metadata,
                previous=self,
            )

    def add(
        self,
        other: Union["ChannelFrame", int, float, NDArrayReal],
        snr: Optional[float] = None,
    ) -> "ChannelFrame":
        """Add another signal or value to the current signal.

        If SNR is specified, performs addition with consideration for
        signal-to-noise ratio.

        Args:
            other: Signal or value to add.
            snr: Signal-to-noise ratio (dB). If specified, adjusts the scale of the
                other signal based on this SNR.
                self is treated as the signal, and other as the noise.

        Returns:
            A new channel frame containing the addition result (lazy execution).
        """
        logger.debug(f"Setting up add operation with SNR={snr} (lazy)")

        if isinstance(other, ChannelFrame):
            # Check if sampling rates match
            if self.sampling_rate != other.sampling_rate:
                raise ValueError(
                    "Sampling rates do not match. Cannot perform operation."
                )

        elif isinstance(other, np.ndarray):
            other = ChannelFrame.from_numpy(
                other, self.sampling_rate, label="array_data"
            )
        elif isinstance(other, (int, float)):
            return self + other
        else:
            raise TypeError(
                "Addition target with SNR must be a ChannelFrame or "
                f"NumPy array: {type(other)}"
            )

        # If SNR is specified, adjust the length of the other signal
        if other.duration != self.duration:
            other = other.fix_length(length=self.n_samples)

        if snr is None:
            return self + other
        return self.apply_operation("add_with_snr", other=other._data, snr=snr)

    def plot(
        self, plot_type: str = "waveform", ax: Optional["Axes"] = None, **kwargs: Any
    ) -> Union["Axes", Iterator["Axes"]]:
        """Plot the frame data.

        Args:
            plot_type: Type of plot. Default is "waveform".
            ax: Optional matplotlib axes for plotting.
            **kwargs: Additional arguments passed to the plot function.

        Returns:
            Single Axes object or iterator of Axes objects.
        """
        logger.debug(f"Plotting audio with plot_type={plot_type} (will compute now)")

        # Get plot strategy
        from ..visualization.plotting import create_operation

        plot_strategy = create_operation(plot_type)

        # Execute plot
        _ax = plot_strategy.plot(self, ax=ax, **kwargs)

        logger.debug("Plot rendering complete")

        return _ax

    def rms_plot(
        self,
        ax: Optional["Axes"] = None,
        title: Optional[str] = None,
        overlay: bool = True,
        Aw: bool = False,  # noqa: N803
        **kwargs: Any,
    ) -> Union["Axes", Iterator["Axes"]]:
        """Generate an RMS plot.

        Args:
            ax: Optional matplotlib axes for plotting.
            title: Title for the plot.
            overlay: Whether to overlay the plot on the existing axis.
            Aw: Apply A-weighting.
            **kwargs: Additional arguments passed to the plot function.

        Returns:
            Single Axes object or iterator of Axes objects.
        """
        kwargs = kwargs or {}
        ylabel = kwargs.pop("ylabel", "RMS")
        rms_ch: ChannelFrame = self.rms_trend(Aw=Aw, dB=True)
        return rms_ch.plot(ax=ax, ylabel=ylabel, title=title, overlay=overlay, **kwargs)

    def describe(
        self,
        normalize: bool = True,
        is_close: bool = True,
        *,
        fmin: float = 0,
        fmax: Optional[float] = None,
        cmap: str = "jet",
        vmin: Optional[float] = None,
        vmax: Optional[float] = None,
        xlim: Optional[tuple[float, float]] = None,
        ylim: Optional[tuple[float, float]] = None,
        Aw: bool = False,  # noqa: N803
        waveform: Optional[dict[str, Any]] = None,
        spectral: Optional[dict[str, Any]] = None,
        **kwargs: Any,
    ) -> None:
        """Display visual and audio representation of the frame.

        This method creates a comprehensive visualization with three plots:
        1. Time-domain waveform (top)
        2. Spectrogram (bottom-left)
        3. Frequency spectrum via Welch method (bottom-right)

        Args:
            normalize: Whether to normalize the audio data for playback.
                Default: True
            is_close: Whether to close the figure after displaying.
                Default: True
            fmin: Minimum frequency to display in the spectrogram (Hz).
                Default: 0
            fmax: Maximum frequency to display in the spectrogram (Hz).
                Default: Nyquist frequency (sampling_rate / 2)
            cmap: Colormap for the spectrogram.
                Default: 'jet'
            vmin: Minimum value for spectrogram color scale (dB).
                Auto-calculated if None.
            vmax: Maximum value for spectrogram color scale (dB).
                Auto-calculated if None.
            xlim: Time axis limits (seconds) for all time-based plots.
                Format: (start_time, end_time)
            ylim: Frequency axis limits (Hz) for frequency-based plots.
                Format: (min_freq, max_freq)
            Aw: Apply A-weighting to the frequency analysis.
                Default: False
            waveform: Additional configuration dict for waveform subplot.
                Can include 'xlabel', 'ylabel', 'xlim', 'ylim'.
            spectral: Additional configuration dict for spectral subplot.
                Can include 'xlabel', 'ylabel', 'xlim', 'ylim'.
            **kwargs: Deprecated parameters for backward compatibility:
                - axis_config: Old configuration format
                - cbar_config: Old colorbar configuration

        Examples:
            >>> cf = ChannelFrame.read_wav("audio.wav")
            >>> # Basic usage
            >>> cf.describe()
            >>>
            >>> # Custom frequency range
            >>> cf.describe(fmin=100, fmax=5000)
            >>>
            >>> # Custom color scale
            >>> cf.describe(vmin=-80, vmax=-20, cmap="viridis")
            >>>
            >>> # A-weighted analysis
            >>> cf.describe(Aw=True)
            >>>
            >>> # Custom time range
            >>> cf.describe(xlim=(0, 5))  # Show first 5 seconds
            >>>
            >>> # Custom waveform subplot settings
            >>> cf.describe(waveform={"ylabel": "Custom Label"})
        """
        # Prepare kwargs with explicit parameters
        plot_kwargs: dict[str, Any] = {
            "fmin": fmin,
            "fmax": fmax,
            "cmap": cmap,
            "vmin": vmin,
            "vmax": vmax,
            "xlim": xlim,
            "ylim": ylim,
            "Aw": Aw,
            "waveform": waveform or {},
            "spectral": spectral or {},
        }
        # Merge with additional kwargs
        plot_kwargs.update(kwargs)

        if "axis_config" in plot_kwargs:
            logger.warning(
                "axis_config is retained for backward compatibility but will "
                "be deprecated in the future."
            )
            axis_config = plot_kwargs["axis_config"]
            if "time_plot" in axis_config:
                plot_kwargs["waveform"] = axis_config["time_plot"]
            if "freq_plot" in axis_config:
                if "xlim" in axis_config["freq_plot"]:
                    vlim = axis_config["freq_plot"]["xlim"]
                    plot_kwargs["vmin"] = vlim[0]
                    plot_kwargs["vmax"] = vlim[1]
                if "ylim" in axis_config["freq_plot"]:
                    ylim_config = axis_config["freq_plot"]["ylim"]
                    plot_kwargs["ylim"] = ylim_config

        if "cbar_config" in plot_kwargs:
            logger.warning(
                "cbar_config is retained for backward compatibility but will "
                "be deprecated in the future."
            )
            cbar_config = plot_kwargs["cbar_config"]
            if "vmin" in cbar_config:
                plot_kwargs["vmin"] = cbar_config["vmin"]
            if "vmax" in cbar_config:
                plot_kwargs["vmax"] = cbar_config["vmax"]

        for ch in self:
            ax: Axes
            _ax = ch.plot("describe", title=f"{ch.label} {ch.labels[0]}", **plot_kwargs)
            if isinstance(_ax, Iterator):
                ax = next(iter(_ax))
            elif isinstance(_ax, Axes):
                ax = _ax
            else:
                raise TypeError(
                    f"Unexpected type for plot result: {type(_ax)}. Expected Axes or Iterator[Axes]."  # noqa: E501
                )
            # display関数とAudioクラスを使用
            display(ax.figure)
            if is_close:
                plt.close(ax.figure)
            display(Audio(ch.data, rate=ch.sampling_rate, normalize=normalize))

    @classmethod
    def from_numpy(
        cls,
        data: NDArrayReal,
        sampling_rate: float,
        label: Optional[str] = None,
        metadata: Optional[dict[str, Any]] = None,
        ch_labels: Optional[list[str]] = None,
        ch_units: Optional[Union[list[str], str]] = None,
    ) -> "ChannelFrame":
        """Create a ChannelFrame from a NumPy array.

        Args:
            data: NumPy array containing channel data.
            sampling_rate: The sampling rate in Hz.
            label: A label for the frame.
            metadata: Optional metadata dictionary.
            ch_labels: Labels for each channel.
            ch_units: Units for each channel.

        Returns:
            A new ChannelFrame containing the NumPy data.
        """
        if data.ndim == 1:
            data = data.reshape(1, -1)
        elif data.ndim > 2:
            raise ValueError(
                f"Data must be 1-dimensional or 2-dimensional. Shape: {data.shape}"
            )

        # Convert NumPy array to dask array
        dask_data = da_from_array(data)
        cf = cls(
            data=dask_data,
            sampling_rate=sampling_rate,
            label=label or "numpy_data",
        )
        if metadata is not None:
            cf.metadata = metadata
        if ch_labels is not None:
            if len(ch_labels) != cf.n_channels:
                raise ValueError(
                    "Number of channel labels does not match the number of channels"
                )
            for i in range(len(ch_labels)):
                cf._channel_metadata[i].label = ch_labels[i]
        if ch_units is not None:
            if isinstance(ch_units, str):
                ch_units = [ch_units] * cf.n_channels

            if len(ch_units) != cf.n_channels:
                raise ValueError(
                    "Number of channel units does not match the number of channels"
                )
            for i in range(len(ch_units)):
                cf._channel_metadata[i].unit = ch_units[i]

        return cf

    @classmethod
    def from_ndarray(
        cls,
        array: NDArrayReal,
        sampling_rate: float,
        labels: Optional[list[str]] = None,
        unit: Optional[Union[list[str], str]] = None,
        frame_label: Optional[str] = None,
        metadata: Optional[dict[str, Any]] = None,
    ) -> "ChannelFrame":
        """Create a ChannelFrame from a NumPy array.

        This method is deprecated. Use from_numpy instead.

        Args:
            array: Signal data. Each row corresponds to a channel.
            sampling_rate: Sampling rate (Hz).
            labels: Labels for each channel.
            unit: Unit of the signal.
            frame_label: Label for the frame.
            metadata: Optional metadata dictionary.

        Returns:
            A new ChannelFrame containing the data.
        """
        # Redirect to from_numpy for compatibility
        # However, from_ndarray is deprecated
        logger.warning("from_ndarray is deprecated. Use from_numpy instead.")
        return cls.from_numpy(
            data=array,
            sampling_rate=sampling_rate,
            label=frame_label,
            metadata=metadata,
            ch_labels=labels,
            ch_units=unit,
        )

    @classmethod
    def from_file(
        cls,
        path: Union[str, Path],
        channel: Optional[Union[int, list[int]]] = None,
        start: Optional[float] = None,
        end: Optional[float] = None,
        chunk_size: Optional[int] = None,
        ch_labels: Optional[list[str]] = None,
        **kwargs: Any,
    ) -> "ChannelFrame":
        """Create a ChannelFrame from an audio file.

        Args:
            path: Path to the audio file.
            channel: Channel(s) to load.
            start: Start time in seconds.
            end: End time in seconds.
            chunk_size: Chunk size for processing.
            Specifies the splitting size for lazy processing.
            ch_labels: Labels for each channel.
            **kwargs: Additional arguments passed to the file reader.

        Returns:
            A new ChannelFrame containing the loaded audio data.

        Raises:
            ValueError: If channel specification is invalid.
            TypeError: If channel parameter type is invalid.
            FileNotFoundError: If the file doesn't exist.
        """
        from .channel import ChannelFrame

        path = Path(path)
        if not path.exists():
            raise FileNotFoundError(f"File not found: {path}")

        # Get file reader
        reader = get_file_reader(path)

        # Get file info
        info = reader.get_file_info(path, **kwargs)
        sr = info["samplerate"]
        n_channels = info["channels"]
        n_frames = info["frames"]
        ch_labels = ch_labels or info.get("ch_labels", None)

        logger.debug(f"File info: sr={sr}, channels={n_channels}, frames={n_frames}")

        # Channel selection processing
        all_channels = list(range(n_channels))

        if channel is None:
            channels_to_load = all_channels
            logger.debug(f"Will load all channels: {channels_to_load}")
        elif isinstance(channel, int):
            if channel < 0 or channel >= n_channels:
                raise ValueError(
                    f"Channel specification is out of range: {channel} (valid range: 0-{n_channels - 1})"  # noqa: E501
                )
            channels_to_load = [channel]
            logger.debug(f"Will load single channel: {channel}")
        elif isinstance(channel, (list, tuple)):
            for ch in channel:
                if ch < 0 or ch >= n_channels:
                    raise ValueError(
                        f"Channel specification is out of range: {ch} (valid range: 0-{n_channels - 1})"  # noqa: E501
                    )
            channels_to_load = list(channel)
            logger.debug(f"Will load specific channels: {channels_to_load}")
        else:
            raise TypeError("channel must be int, list, or None")

        # Index calculation
        start_idx = 0 if start is None else max(0, int(start * sr))
        end_idx = n_frames if end is None else min(n_frames, int(end * sr))
        frames_to_read = end_idx - start_idx

        logger.debug(
            f"Setting up lazy load from file={path}, frames={frames_to_read}, "
            f"start_idx={start_idx}, end_idx={end_idx}"
        )

        # Settings for lazy loading
        expected_shape = (len(channels_to_load), frames_to_read)

        # Define the loading function using the file reader
        def _load_audio() -> NDArrayReal:
            logger.debug(">>> EXECUTING DELAYED LOAD <<<")
            # Use the reader to get audio data with parameters
            out = reader.get_data(path, channels_to_load, start_idx, frames_to_read)
            if not isinstance(out, np.ndarray):
                raise ValueError("Unexpected data type after reading file")
            return out

        logger.debug(
            f"Creating delayed dask task with expected shape: {expected_shape}"
        )

        # Create delayed operation
        delayed_data = dask_delayed(_load_audio)()
        logger.debug("Wrapping delayed function in dask array")

        # Create dask array from delayed computation
        dask_array = da_from_delayed(
            delayed_data, shape=expected_shape, dtype=np.float32
        )

        if chunk_size is not None:
            if chunk_size <= 0:
                raise ValueError("Chunk size must be a positive integer")
            logger.debug(f"Setting chunk size: {chunk_size} for sample axis")
            dask_array = dask_array.rechunk({0: -1, 1: chunk_size})

        logger.debug(
            "ChannelFrame setup complete - actual file reading will occur on compute()"  # noqa: E501
        )

        cf = ChannelFrame(
            data=dask_array,
            sampling_rate=sr,
            label=path.stem,
            metadata={
                "filename": str(path),
            },
        )
        if ch_labels is not None:
            if len(ch_labels) != len(cf):
                raise ValueError(
                    "Number of channel labels does not match the number of specified channels"  # noqa: E501
                )
            for i in range(len(ch_labels)):
                cf._channel_metadata[i].label = ch_labels[i]
        return cf

    @classmethod
    def read_wav(
        cls, filename: str, labels: Optional[list[str]] = None
    ) -> "ChannelFrame":
        """Utility method to read a WAV file.

        Args:
            filename: Path to the WAV file.
            labels: Labels to set for each channel.

        Returns:
            A new ChannelFrame containing the data (lazy loading).
        """
        from .channel import ChannelFrame

        cf = ChannelFrame.from_file(filename, ch_labels=labels)
        return cf

    @classmethod
    def read_csv(
        cls,
        filename: str,
        time_column: Union[int, str] = 0,
        labels: Optional[list[str]] = None,
        delimiter: str = ",",
        header: Optional[int] = 0,
    ) -> "ChannelFrame":
        """Utility method to read a CSV file.

        Args:
            filename: Path to the CSV file.
            time_column: Index or name of the time column.
            labels: Labels to set for each channel.
            delimiter: Delimiter character.
            header: Row number to use as header.

        Returns:
            A new ChannelFrame containing the data (lazy loading).
        """
        from .channel import ChannelFrame

        cf = ChannelFrame.from_file(
            filename,
            ch_labels=labels,
            time_column=time_column,
            delimiter=delimiter,
            header=header,
        )
        return cf

    def to_wav(self, path: Union[str, Path], format: Optional[str] = None) -> None:
        """Save the audio data to a WAV file.

        Args:
            path: Path to save the file.
            format: File format. If None, determined from file extension.
        """
        from wandas.io.wav_io import write_wav

        write_wav(str(path), self, format=format)

    def save(
        self,
        path: Union[str, Path],
        *,
        format: str = "hdf5",
        compress: Optional[str] = "gzip",
        overwrite: bool = False,
        dtype: Optional[Union[str, np.dtype[Any]]] = None,
    ) -> None:
        """Save the ChannelFrame to a WDF (Wandas Data File) format.

        This saves the complete frame including all channel data and metadata
        in a format that can be loaded back with full fidelity.

        Args:
            path: Path to save the file. '.wdf' extension will be added if not present.
            format: Format to use (currently only 'hdf5' is supported)
            compress: Compression method ('gzip' by default, None for no compression)
            overwrite: Whether to overwrite existing file
            dtype: Optional data type conversion before saving (e.g. 'float32')

        Raises:
            FileExistsError: If the file exists and overwrite=False.
            NotImplementedError: For unsupported formats.

        Example:
            >>> cf = ChannelFrame.read_wav("audio.wav")
            >>> cf.save("audio_analysis.wdf")
        """
        from ..io.wdf_io import save as wdf_save

        wdf_save(
            self,
            path,
            format=format,
            compress=compress,
            overwrite=overwrite,
            dtype=dtype,
        )

    @classmethod
    def load(cls, path: Union[str, Path], *, format: str = "hdf5") -> "ChannelFrame":
        """Load a ChannelFrame from a WDF (Wandas Data File) file.

        This loads data saved with the save() method, preserving all channel data,
        metadata, labels, and units.

        Args:
            path: Path to the WDF file
            format: Format of the file (currently only 'hdf5' is supported)

        Returns:
            A new ChannelFrame with all data and metadata loaded

        Raises:
            FileNotFoundError: If the file doesn't exist
            NotImplementedError: For unsupported formats

        Example:
            >>> cf = ChannelFrame.load("audio_analysis.wdf")
        """
        from ..io.wdf_io import load as wdf_load

        return wdf_load(path, format=format)

    def _get_additional_init_kwargs(self) -> dict[str, Any]:
        """Provide additional initialization arguments required for ChannelFrame."""
        return {}

    def add_channel(
        self,
        data: Union[np.ndarray[Any, Any], DaskArray, "ChannelFrame"],
        label: Optional[str] = None,
        align: str = "strict",
        suffix_on_dup: Optional[str] = None,
        inplace: bool = False,
        **kwargs: Any,
    ) -> "ChannelFrame":
        # ndarray/dask/同型Frame対応
        if isinstance(data, ChannelFrame):
            if self.sampling_rate != data.sampling_rate:
                raise ValueError("sampling_rate不一致")
            if data.n_samples != self.n_samples:
                if align == "pad":
                    pad_len = self.n_samples - data.n_samples
                    arr = data._data
                    if pad_len > 0:
                        arr = concatenate(
                            [
                                arr,
                                from_array(
                                    np.zeros((arr.shape[0], pad_len), dtype=arr.dtype)
                                ),
                            ],
                            axis=1,
                        )
                    else:
                        arr = arr[:, : self.n_samples]
                elif align == "truncate":
                    arr = data._data[:, : self.n_samples]
                    if arr.shape[1] < self.n_samples:
                        pad_len = self.n_samples - arr.shape[1]
                        arr = concatenate(
                            [
                                arr,
                                from_array(
                                    np.zeros((arr.shape[0], pad_len), dtype=arr.dtype)
                                ),
                            ],
                            axis=1,
                        )
                else:
                    raise ValueError("データ長不一致: align指定を確認")
            else:
                arr = data._data
            labels = [ch.label for ch in self._channel_metadata]
            new_labels = []
            new_metadata_list = []
            for chmeta in data._channel_metadata:
                new_label = chmeta.label
                if new_label in labels or new_label in new_labels:
                    if suffix_on_dup:
                        new_label += suffix_on_dup
                    else:
                        raise ValueError(f"label重複: {new_label}")
                new_labels.append(new_label)
                # Copy the entire channel_metadata and update only the label
                new_ch_meta = chmeta.model_copy(deep=True)
                new_ch_meta.label = new_label
                new_metadata_list.append(new_ch_meta)
            new_data = concatenate([self._data, arr], axis=0)

            new_chmeta = self._channel_metadata + new_metadata_list
            if inplace:
                self._data = new_data
                self._channel_metadata = new_chmeta
                return self
            else:
                return ChannelFrame(
                    data=new_data,
                    sampling_rate=self.sampling_rate,
                    label=self.label,
                    metadata=self.metadata,
                    operation_history=self.operation_history,
                    channel_metadata=new_chmeta,
                    previous=self,
                )
        if isinstance(data, np.ndarray):
            arr = from_array(data.reshape(1, -1))
        elif isinstance(data, DaskArray):
            arr = data[None, ...] if data.ndim == 1 else data
            if arr.shape[0] != 1:
                arr = arr.reshape((1, -1))
        else:
            raise TypeError("add_channel: ndarray/dask/同型Frameのみ対応")
        if arr.shape[1] != self.n_samples:
            if align == "pad":
                pad_len = self.n_samples - arr.shape[1]
                if pad_len > 0:
                    arr = concatenate(
                        [arr, from_array(np.zeros((1, pad_len), dtype=arr.dtype))],
                        axis=1,
                    )
                else:
                    arr = arr[:, : self.n_samples]
            elif align == "truncate":
                arr = arr[:, : self.n_samples]
                if arr.shape[1] < self.n_samples:
                    pad_len = self.n_samples - arr.shape[1]
                    arr = concatenate(
                        [arr, from_array(np.zeros((1, pad_len), dtype=arr.dtype))],
                        axis=1,
                    )
            else:
                raise ValueError("データ長不一致: align指定を確認")
        labels = [ch.label for ch in self._channel_metadata]
        new_label = label or f"ch{len(labels)}"
        if new_label in labels:
            if suffix_on_dup:
                new_label += suffix_on_dup
            else:
                raise ValueError("label重複")
        new_data = concatenate([self._data, arr], axis=0)
        from ..core.metadata import ChannelMetadata

        new_chmeta = self._channel_metadata + [ChannelMetadata(label=new_label)]
        if inplace:
            self._data = new_data
            self._channel_metadata = new_chmeta
            return self
        else:
            return ChannelFrame(
                data=new_data,
                sampling_rate=self.sampling_rate,
                label=self.label,
                metadata=self.metadata,
                operation_history=self.operation_history,
                channel_metadata=new_chmeta,
                previous=self,
            )

    def remove_channel(
        self, key: Union[int, str], inplace: bool = False
    ) -> "ChannelFrame":
        if isinstance(key, int):
            if not (0 <= key < self.n_channels):
                raise IndexError(f"index {key} out of range")
            idx = key
        else:
            labels = [ch.label for ch in self._channel_metadata]
            if key not in labels:
                raise KeyError(f"label {key} not found")
            idx = labels.index(key)
        new_data = self._data[[i for i in range(self.n_channels) if i != idx], :]
        new_chmeta = [ch for i, ch in enumerate(self._channel_metadata) if i != idx]
        if inplace:
            self._data = new_data
            self._channel_metadata = new_chmeta
            return self
        else:
            return ChannelFrame(
                data=new_data,
                sampling_rate=self.sampling_rate,
                label=self.label,
                metadata=self.metadata,
                operation_history=self.operation_history,
                channel_metadata=new_chmeta,
                previous=self,
            )
Attributes
time property

Get time array for the signal.

Returns:

Type Description
NDArrayReal

Array of time points in seconds.

n_samples property

Returns the number of samples.

duration property

Returns the duration in seconds.

rms property

Calculate RMS (Root Mean Square) value for each channel.

Returns:

Type Description
NDArrayReal

Array of RMS values, one per channel.

Examples:

>>> cf = ChannelFrame.read_wav("audio.wav")
>>> rms_values = cf.rms
>>> print(f"RMS values: {rms_values}")
>>> # Select channels with RMS > threshold
>>> active_channels = cf[cf.rms > 0.5]
Functions
__init__(data, sampling_rate, label=None, metadata=None, operation_history=None, channel_metadata=None, previous=None)

Initialize a ChannelFrame.

Parameters:

Name Type Description Default
data Array

Dask array containing channel data.

required
sampling_rate float

The sampling rate of the data in Hz.

required
label Optional[str]

A label for the frame.

None
metadata Optional[dict[str, Any]]

Optional metadata dictionary.

None
operation_history Optional[list[dict[str, Any]]]

History of operations applied to the frame.

None
channel_metadata Optional[list[ChannelMetadata]]

Metadata for each channel.

None
previous Optional[BaseFrame[Any]]

Reference to the previous frame in the processing chain.

None

Raises:

Type Description
ValueError

If data has more than 2 dimensions.

Source code in wandas/frames/channel.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def __init__(
    self,
    data: DaskArray,
    sampling_rate: float,
    label: Optional[str] = None,
    metadata: Optional[dict[str, Any]] = None,
    operation_history: Optional[list[dict[str, Any]]] = None,
    channel_metadata: Optional[list[ChannelMetadata]] = None,
    previous: Optional["BaseFrame[Any]"] = None,
) -> None:
    """Initialize a ChannelFrame.

    Args:
        data: Dask array containing channel data.
        Shape should be (n_channels, n_samples).
        sampling_rate: The sampling rate of the data in Hz.
        label: A label for the frame.
        metadata: Optional metadata dictionary.
        operation_history: History of operations applied to the frame.
        channel_metadata: Metadata for each channel.
        previous: Reference to the previous frame in the processing chain.

    Raises:
        ValueError: If data has more than 2 dimensions.
    """
    if data.ndim == 1:
        data = da.reshape(data, (1, -1))
    elif data.ndim > 2:
        raise ValueError(
            f"Data must be 1-dimensional or 2-dimensional. Shape: {data.shape}"
        )
    super().__init__(
        data=data,
        sampling_rate=sampling_rate,
        label=label,
        metadata=metadata,
        operation_history=operation_history,
        channel_metadata=channel_metadata,
        previous=previous,
    )
add(other, snr=None)

Add another signal or value to the current signal.

If SNR is specified, performs addition with consideration for signal-to-noise ratio.

Parameters:

Name Type Description Default
other Union[ChannelFrame, int, float, NDArrayReal]

Signal or value to add.

required
snr Optional[float]

Signal-to-noise ratio (dB). If specified, adjusts the scale of the other signal based on this SNR. self is treated as the signal, and other as the noise.

None

Returns:

Type Description
ChannelFrame

A new channel frame containing the addition result (lazy execution).

Source code in wandas/frames/channel.py
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
def add(
    self,
    other: Union["ChannelFrame", int, float, NDArrayReal],
    snr: Optional[float] = None,
) -> "ChannelFrame":
    """Add another signal or value to the current signal.

    If SNR is specified, performs addition with consideration for
    signal-to-noise ratio.

    Args:
        other: Signal or value to add.
        snr: Signal-to-noise ratio (dB). If specified, adjusts the scale of the
            other signal based on this SNR.
            self is treated as the signal, and other as the noise.

    Returns:
        A new channel frame containing the addition result (lazy execution).
    """
    logger.debug(f"Setting up add operation with SNR={snr} (lazy)")

    if isinstance(other, ChannelFrame):
        # Check if sampling rates match
        if self.sampling_rate != other.sampling_rate:
            raise ValueError(
                "Sampling rates do not match. Cannot perform operation."
            )

    elif isinstance(other, np.ndarray):
        other = ChannelFrame.from_numpy(
            other, self.sampling_rate, label="array_data"
        )
    elif isinstance(other, (int, float)):
        return self + other
    else:
        raise TypeError(
            "Addition target with SNR must be a ChannelFrame or "
            f"NumPy array: {type(other)}"
        )

    # If SNR is specified, adjust the length of the other signal
    if other.duration != self.duration:
        other = other.fix_length(length=self.n_samples)

    if snr is None:
        return self + other
    return self.apply_operation("add_with_snr", other=other._data, snr=snr)
plot(plot_type='waveform', ax=None, **kwargs)

Plot the frame data.

Parameters:

Name Type Description Default
plot_type str

Type of plot. Default is "waveform".

'waveform'
ax Optional[Axes]

Optional matplotlib axes for plotting.

None
**kwargs Any

Additional arguments passed to the plot function.

{}

Returns:

Type Description
Union[Axes, Iterator[Axes]]

Single Axes object or iterator of Axes objects.

Source code in wandas/frames/channel.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
def plot(
    self, plot_type: str = "waveform", ax: Optional["Axes"] = None, **kwargs: Any
) -> Union["Axes", Iterator["Axes"]]:
    """Plot the frame data.

    Args:
        plot_type: Type of plot. Default is "waveform".
        ax: Optional matplotlib axes for plotting.
        **kwargs: Additional arguments passed to the plot function.

    Returns:
        Single Axes object or iterator of Axes objects.
    """
    logger.debug(f"Plotting audio with plot_type={plot_type} (will compute now)")

    # Get plot strategy
    from ..visualization.plotting import create_operation

    plot_strategy = create_operation(plot_type)

    # Execute plot
    _ax = plot_strategy.plot(self, ax=ax, **kwargs)

    logger.debug("Plot rendering complete")

    return _ax
rms_plot(ax=None, title=None, overlay=True, Aw=False, **kwargs)

Generate an RMS plot.

Parameters:

Name Type Description Default
ax Optional[Axes]

Optional matplotlib axes for plotting.

None
title Optional[str]

Title for the plot.

None
overlay bool

Whether to overlay the plot on the existing axis.

True
Aw bool

Apply A-weighting.

False
**kwargs Any

Additional arguments passed to the plot function.

{}

Returns:

Type Description
Union[Axes, Iterator[Axes]]

Single Axes object or iterator of Axes objects.

Source code in wandas/frames/channel.py
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
def rms_plot(
    self,
    ax: Optional["Axes"] = None,
    title: Optional[str] = None,
    overlay: bool = True,
    Aw: bool = False,  # noqa: N803
    **kwargs: Any,
) -> Union["Axes", Iterator["Axes"]]:
    """Generate an RMS plot.

    Args:
        ax: Optional matplotlib axes for plotting.
        title: Title for the plot.
        overlay: Whether to overlay the plot on the existing axis.
        Aw: Apply A-weighting.
        **kwargs: Additional arguments passed to the plot function.

    Returns:
        Single Axes object or iterator of Axes objects.
    """
    kwargs = kwargs or {}
    ylabel = kwargs.pop("ylabel", "RMS")
    rms_ch: ChannelFrame = self.rms_trend(Aw=Aw, dB=True)
    return rms_ch.plot(ax=ax, ylabel=ylabel, title=title, overlay=overlay, **kwargs)
describe(normalize=True, is_close=True, *, fmin=0, fmax=None, cmap='jet', vmin=None, vmax=None, xlim=None, ylim=None, Aw=False, waveform=None, spectral=None, **kwargs)

Display visual and audio representation of the frame.

This method creates a comprehensive visualization with three plots: 1. Time-domain waveform (top) 2. Spectrogram (bottom-left) 3. Frequency spectrum via Welch method (bottom-right)

Parameters:

Name Type Description Default
normalize bool

Whether to normalize the audio data for playback. Default: True

True
is_close bool

Whether to close the figure after displaying. Default: True

True
fmin float

Minimum frequency to display in the spectrogram (Hz). Default: 0

0
fmax Optional[float]

Maximum frequency to display in the spectrogram (Hz). Default: Nyquist frequency (sampling_rate / 2)

None
cmap str

Colormap for the spectrogram. Default: 'jet'

'jet'
vmin Optional[float]

Minimum value for spectrogram color scale (dB). Auto-calculated if None.

None
vmax Optional[float]

Maximum value for spectrogram color scale (dB). Auto-calculated if None.

None
xlim Optional[tuple[float, float]]

Time axis limits (seconds) for all time-based plots. Format: (start_time, end_time)

None
ylim Optional[tuple[float, float]]

Frequency axis limits (Hz) for frequency-based plots. Format: (min_freq, max_freq)

None
Aw bool

Apply A-weighting to the frequency analysis. Default: False

False
waveform Optional[dict[str, Any]]

Additional configuration dict for waveform subplot. Can include 'xlabel', 'ylabel', 'xlim', 'ylim'.

None
spectral Optional[dict[str, Any]]

Additional configuration dict for spectral subplot. Can include 'xlabel', 'ylabel', 'xlim', 'ylim'.

None
**kwargs Any

Deprecated parameters for backward compatibility: - axis_config: Old configuration format - cbar_config: Old colorbar configuration

{}

Examples:

>>> cf = ChannelFrame.read_wav("audio.wav")
>>> # Basic usage
>>> cf.describe()
>>>
>>> # Custom frequency range
>>> cf.describe(fmin=100, fmax=5000)
>>>
>>> # Custom color scale
>>> cf.describe(vmin=-80, vmax=-20, cmap="viridis")
>>>
>>> # A-weighted analysis
>>> cf.describe(Aw=True)
>>>
>>> # Custom time range
>>> cf.describe(xlim=(0, 5))  # Show first 5 seconds
>>>
>>> # Custom waveform subplot settings
>>> cf.describe(waveform={"ylabel": "Custom Label"})
Source code in wandas/frames/channel.py
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
def describe(
    self,
    normalize: bool = True,
    is_close: bool = True,
    *,
    fmin: float = 0,
    fmax: Optional[float] = None,
    cmap: str = "jet",
    vmin: Optional[float] = None,
    vmax: Optional[float] = None,
    xlim: Optional[tuple[float, float]] = None,
    ylim: Optional[tuple[float, float]] = None,
    Aw: bool = False,  # noqa: N803
    waveform: Optional[dict[str, Any]] = None,
    spectral: Optional[dict[str, Any]] = None,
    **kwargs: Any,
) -> None:
    """Display visual and audio representation of the frame.

    This method creates a comprehensive visualization with three plots:
    1. Time-domain waveform (top)
    2. Spectrogram (bottom-left)
    3. Frequency spectrum via Welch method (bottom-right)

    Args:
        normalize: Whether to normalize the audio data for playback.
            Default: True
        is_close: Whether to close the figure after displaying.
            Default: True
        fmin: Minimum frequency to display in the spectrogram (Hz).
            Default: 0
        fmax: Maximum frequency to display in the spectrogram (Hz).
            Default: Nyquist frequency (sampling_rate / 2)
        cmap: Colormap for the spectrogram.
            Default: 'jet'
        vmin: Minimum value for spectrogram color scale (dB).
            Auto-calculated if None.
        vmax: Maximum value for spectrogram color scale (dB).
            Auto-calculated if None.
        xlim: Time axis limits (seconds) for all time-based plots.
            Format: (start_time, end_time)
        ylim: Frequency axis limits (Hz) for frequency-based plots.
            Format: (min_freq, max_freq)
        Aw: Apply A-weighting to the frequency analysis.
            Default: False
        waveform: Additional configuration dict for waveform subplot.
            Can include 'xlabel', 'ylabel', 'xlim', 'ylim'.
        spectral: Additional configuration dict for spectral subplot.
            Can include 'xlabel', 'ylabel', 'xlim', 'ylim'.
        **kwargs: Deprecated parameters for backward compatibility:
            - axis_config: Old configuration format
            - cbar_config: Old colorbar configuration

    Examples:
        >>> cf = ChannelFrame.read_wav("audio.wav")
        >>> # Basic usage
        >>> cf.describe()
        >>>
        >>> # Custom frequency range
        >>> cf.describe(fmin=100, fmax=5000)
        >>>
        >>> # Custom color scale
        >>> cf.describe(vmin=-80, vmax=-20, cmap="viridis")
        >>>
        >>> # A-weighted analysis
        >>> cf.describe(Aw=True)
        >>>
        >>> # Custom time range
        >>> cf.describe(xlim=(0, 5))  # Show first 5 seconds
        >>>
        >>> # Custom waveform subplot settings
        >>> cf.describe(waveform={"ylabel": "Custom Label"})
    """
    # Prepare kwargs with explicit parameters
    plot_kwargs: dict[str, Any] = {
        "fmin": fmin,
        "fmax": fmax,
        "cmap": cmap,
        "vmin": vmin,
        "vmax": vmax,
        "xlim": xlim,
        "ylim": ylim,
        "Aw": Aw,
        "waveform": waveform or {},
        "spectral": spectral or {},
    }
    # Merge with additional kwargs
    plot_kwargs.update(kwargs)

    if "axis_config" in plot_kwargs:
        logger.warning(
            "axis_config is retained for backward compatibility but will "
            "be deprecated in the future."
        )
        axis_config = plot_kwargs["axis_config"]
        if "time_plot" in axis_config:
            plot_kwargs["waveform"] = axis_config["time_plot"]
        if "freq_plot" in axis_config:
            if "xlim" in axis_config["freq_plot"]:
                vlim = axis_config["freq_plot"]["xlim"]
                plot_kwargs["vmin"] = vlim[0]
                plot_kwargs["vmax"] = vlim[1]
            if "ylim" in axis_config["freq_plot"]:
                ylim_config = axis_config["freq_plot"]["ylim"]
                plot_kwargs["ylim"] = ylim_config

    if "cbar_config" in plot_kwargs:
        logger.warning(
            "cbar_config is retained for backward compatibility but will "
            "be deprecated in the future."
        )
        cbar_config = plot_kwargs["cbar_config"]
        if "vmin" in cbar_config:
            plot_kwargs["vmin"] = cbar_config["vmin"]
        if "vmax" in cbar_config:
            plot_kwargs["vmax"] = cbar_config["vmax"]

    for ch in self:
        ax: Axes
        _ax = ch.plot("describe", title=f"{ch.label} {ch.labels[0]}", **plot_kwargs)
        if isinstance(_ax, Iterator):
            ax = next(iter(_ax))
        elif isinstance(_ax, Axes):
            ax = _ax
        else:
            raise TypeError(
                f"Unexpected type for plot result: {type(_ax)}. Expected Axes or Iterator[Axes]."  # noqa: E501
            )
        # display関数とAudioクラスを使用
        display(ax.figure)
        if is_close:
            plt.close(ax.figure)
        display(Audio(ch.data, rate=ch.sampling_rate, normalize=normalize))
from_numpy(data, sampling_rate, label=None, metadata=None, ch_labels=None, ch_units=None) classmethod

Create a ChannelFrame from a NumPy array.

Parameters:

Name Type Description Default
data NDArrayReal

NumPy array containing channel data.

required
sampling_rate float

The sampling rate in Hz.

required
label Optional[str]

A label for the frame.

None
metadata Optional[dict[str, Any]]

Optional metadata dictionary.

None
ch_labels Optional[list[str]]

Labels for each channel.

None
ch_units Optional[Union[list[str], str]]

Units for each channel.

None

Returns:

Type Description
ChannelFrame

A new ChannelFrame containing the NumPy data.

Source code in wandas/frames/channel.py
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
@classmethod
def from_numpy(
    cls,
    data: NDArrayReal,
    sampling_rate: float,
    label: Optional[str] = None,
    metadata: Optional[dict[str, Any]] = None,
    ch_labels: Optional[list[str]] = None,
    ch_units: Optional[Union[list[str], str]] = None,
) -> "ChannelFrame":
    """Create a ChannelFrame from a NumPy array.

    Args:
        data: NumPy array containing channel data.
        sampling_rate: The sampling rate in Hz.
        label: A label for the frame.
        metadata: Optional metadata dictionary.
        ch_labels: Labels for each channel.
        ch_units: Units for each channel.

    Returns:
        A new ChannelFrame containing the NumPy data.
    """
    if data.ndim == 1:
        data = data.reshape(1, -1)
    elif data.ndim > 2:
        raise ValueError(
            f"Data must be 1-dimensional or 2-dimensional. Shape: {data.shape}"
        )

    # Convert NumPy array to dask array
    dask_data = da_from_array(data)
    cf = cls(
        data=dask_data,
        sampling_rate=sampling_rate,
        label=label or "numpy_data",
    )
    if metadata is not None:
        cf.metadata = metadata
    if ch_labels is not None:
        if len(ch_labels) != cf.n_channels:
            raise ValueError(
                "Number of channel labels does not match the number of channels"
            )
        for i in range(len(ch_labels)):
            cf._channel_metadata[i].label = ch_labels[i]
    if ch_units is not None:
        if isinstance(ch_units, str):
            ch_units = [ch_units] * cf.n_channels

        if len(ch_units) != cf.n_channels:
            raise ValueError(
                "Number of channel units does not match the number of channels"
            )
        for i in range(len(ch_units)):
            cf._channel_metadata[i].unit = ch_units[i]

    return cf
from_ndarray(array, sampling_rate, labels=None, unit=None, frame_label=None, metadata=None) classmethod

Create a ChannelFrame from a NumPy array.

This method is deprecated. Use from_numpy instead.

Parameters:

Name Type Description Default
array NDArrayReal

Signal data. Each row corresponds to a channel.

required
sampling_rate float

Sampling rate (Hz).

required
labels Optional[list[str]]

Labels for each channel.

None
unit Optional[Union[list[str], str]]

Unit of the signal.

None
frame_label Optional[str]

Label for the frame.

None
metadata Optional[dict[str, Any]]

Optional metadata dictionary.

None

Returns:

Type Description
ChannelFrame

A new ChannelFrame containing the data.

Source code in wandas/frames/channel.py
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
@classmethod
def from_ndarray(
    cls,
    array: NDArrayReal,
    sampling_rate: float,
    labels: Optional[list[str]] = None,
    unit: Optional[Union[list[str], str]] = None,
    frame_label: Optional[str] = None,
    metadata: Optional[dict[str, Any]] = None,
) -> "ChannelFrame":
    """Create a ChannelFrame from a NumPy array.

    This method is deprecated. Use from_numpy instead.

    Args:
        array: Signal data. Each row corresponds to a channel.
        sampling_rate: Sampling rate (Hz).
        labels: Labels for each channel.
        unit: Unit of the signal.
        frame_label: Label for the frame.
        metadata: Optional metadata dictionary.

    Returns:
        A new ChannelFrame containing the data.
    """
    # Redirect to from_numpy for compatibility
    # However, from_ndarray is deprecated
    logger.warning("from_ndarray is deprecated. Use from_numpy instead.")
    return cls.from_numpy(
        data=array,
        sampling_rate=sampling_rate,
        label=frame_label,
        metadata=metadata,
        ch_labels=labels,
        ch_units=unit,
    )
from_file(path, channel=None, start=None, end=None, chunk_size=None, ch_labels=None, **kwargs) classmethod

Create a ChannelFrame from an audio file.

Parameters:

Name Type Description Default
path Union[str, Path]

Path to the audio file.

required
channel Optional[Union[int, list[int]]]

Channel(s) to load.

None
start Optional[float]

Start time in seconds.

None
end Optional[float]

End time in seconds.

None
chunk_size Optional[int]

Chunk size for processing.

None
ch_labels Optional[list[str]]

Labels for each channel.

None
**kwargs Any

Additional arguments passed to the file reader.

{}

Returns:

Type Description
ChannelFrame

A new ChannelFrame containing the loaded audio data.

Raises:

Type Description
ValueError

If channel specification is invalid.

TypeError

If channel parameter type is invalid.

FileNotFoundError

If the file doesn't exist.

Source code in wandas/frames/channel.py
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
@classmethod
def from_file(
    cls,
    path: Union[str, Path],
    channel: Optional[Union[int, list[int]]] = None,
    start: Optional[float] = None,
    end: Optional[float] = None,
    chunk_size: Optional[int] = None,
    ch_labels: Optional[list[str]] = None,
    **kwargs: Any,
) -> "ChannelFrame":
    """Create a ChannelFrame from an audio file.

    Args:
        path: Path to the audio file.
        channel: Channel(s) to load.
        start: Start time in seconds.
        end: End time in seconds.
        chunk_size: Chunk size for processing.
        Specifies the splitting size for lazy processing.
        ch_labels: Labels for each channel.
        **kwargs: Additional arguments passed to the file reader.

    Returns:
        A new ChannelFrame containing the loaded audio data.

    Raises:
        ValueError: If channel specification is invalid.
        TypeError: If channel parameter type is invalid.
        FileNotFoundError: If the file doesn't exist.
    """
    from .channel import ChannelFrame

    path = Path(path)
    if not path.exists():
        raise FileNotFoundError(f"File not found: {path}")

    # Get file reader
    reader = get_file_reader(path)

    # Get file info
    info = reader.get_file_info(path, **kwargs)
    sr = info["samplerate"]
    n_channels = info["channels"]
    n_frames = info["frames"]
    ch_labels = ch_labels or info.get("ch_labels", None)

    logger.debug(f"File info: sr={sr}, channels={n_channels}, frames={n_frames}")

    # Channel selection processing
    all_channels = list(range(n_channels))

    if channel is None:
        channels_to_load = all_channels
        logger.debug(f"Will load all channels: {channels_to_load}")
    elif isinstance(channel, int):
        if channel < 0 or channel >= n_channels:
            raise ValueError(
                f"Channel specification is out of range: {channel} (valid range: 0-{n_channels - 1})"  # noqa: E501
            )
        channels_to_load = [channel]
        logger.debug(f"Will load single channel: {channel}")
    elif isinstance(channel, (list, tuple)):
        for ch in channel:
            if ch < 0 or ch >= n_channels:
                raise ValueError(
                    f"Channel specification is out of range: {ch} (valid range: 0-{n_channels - 1})"  # noqa: E501
                )
        channels_to_load = list(channel)
        logger.debug(f"Will load specific channels: {channels_to_load}")
    else:
        raise TypeError("channel must be int, list, or None")

    # Index calculation
    start_idx = 0 if start is None else max(0, int(start * sr))
    end_idx = n_frames if end is None else min(n_frames, int(end * sr))
    frames_to_read = end_idx - start_idx

    logger.debug(
        f"Setting up lazy load from file={path}, frames={frames_to_read}, "
        f"start_idx={start_idx}, end_idx={end_idx}"
    )

    # Settings for lazy loading
    expected_shape = (len(channels_to_load), frames_to_read)

    # Define the loading function using the file reader
    def _load_audio() -> NDArrayReal:
        logger.debug(">>> EXECUTING DELAYED LOAD <<<")
        # Use the reader to get audio data with parameters
        out = reader.get_data(path, channels_to_load, start_idx, frames_to_read)
        if not isinstance(out, np.ndarray):
            raise ValueError("Unexpected data type after reading file")
        return out

    logger.debug(
        f"Creating delayed dask task with expected shape: {expected_shape}"
    )

    # Create delayed operation
    delayed_data = dask_delayed(_load_audio)()
    logger.debug("Wrapping delayed function in dask array")

    # Create dask array from delayed computation
    dask_array = da_from_delayed(
        delayed_data, shape=expected_shape, dtype=np.float32
    )

    if chunk_size is not None:
        if chunk_size <= 0:
            raise ValueError("Chunk size must be a positive integer")
        logger.debug(f"Setting chunk size: {chunk_size} for sample axis")
        dask_array = dask_array.rechunk({0: -1, 1: chunk_size})

    logger.debug(
        "ChannelFrame setup complete - actual file reading will occur on compute()"  # noqa: E501
    )

    cf = ChannelFrame(
        data=dask_array,
        sampling_rate=sr,
        label=path.stem,
        metadata={
            "filename": str(path),
        },
    )
    if ch_labels is not None:
        if len(ch_labels) != len(cf):
            raise ValueError(
                "Number of channel labels does not match the number of specified channels"  # noqa: E501
            )
        for i in range(len(ch_labels)):
            cf._channel_metadata[i].label = ch_labels[i]
    return cf
read_wav(filename, labels=None) classmethod

Utility method to read a WAV file.

Parameters:

Name Type Description Default
filename str

Path to the WAV file.

required
labels Optional[list[str]]

Labels to set for each channel.

None

Returns:

Type Description
ChannelFrame

A new ChannelFrame containing the data (lazy loading).

Source code in wandas/frames/channel.py
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
@classmethod
def read_wav(
    cls, filename: str, labels: Optional[list[str]] = None
) -> "ChannelFrame":
    """Utility method to read a WAV file.

    Args:
        filename: Path to the WAV file.
        labels: Labels to set for each channel.

    Returns:
        A new ChannelFrame containing the data (lazy loading).
    """
    from .channel import ChannelFrame

    cf = ChannelFrame.from_file(filename, ch_labels=labels)
    return cf
read_csv(filename, time_column=0, labels=None, delimiter=',', header=0) classmethod

Utility method to read a CSV file.

Parameters:

Name Type Description Default
filename str

Path to the CSV file.

required
time_column Union[int, str]

Index or name of the time column.

0
labels Optional[list[str]]

Labels to set for each channel.

None
delimiter str

Delimiter character.

','
header Optional[int]

Row number to use as header.

0

Returns:

Type Description
ChannelFrame

A new ChannelFrame containing the data (lazy loading).

Source code in wandas/frames/channel.py
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
@classmethod
def read_csv(
    cls,
    filename: str,
    time_column: Union[int, str] = 0,
    labels: Optional[list[str]] = None,
    delimiter: str = ",",
    header: Optional[int] = 0,
) -> "ChannelFrame":
    """Utility method to read a CSV file.

    Args:
        filename: Path to the CSV file.
        time_column: Index or name of the time column.
        labels: Labels to set for each channel.
        delimiter: Delimiter character.
        header: Row number to use as header.

    Returns:
        A new ChannelFrame containing the data (lazy loading).
    """
    from .channel import ChannelFrame

    cf = ChannelFrame.from_file(
        filename,
        ch_labels=labels,
        time_column=time_column,
        delimiter=delimiter,
        header=header,
    )
    return cf
to_wav(path, format=None)

Save the audio data to a WAV file.

Parameters:

Name Type Description Default
path Union[str, Path]

Path to save the file.

required
format Optional[str]

File format. If None, determined from file extension.

None
Source code in wandas/frames/channel.py
779
780
781
782
783
784
785
786
787
788
def to_wav(self, path: Union[str, Path], format: Optional[str] = None) -> None:
    """Save the audio data to a WAV file.

    Args:
        path: Path to save the file.
        format: File format. If None, determined from file extension.
    """
    from wandas.io.wav_io import write_wav

    write_wav(str(path), self, format=format)
save(path, *, format='hdf5', compress='gzip', overwrite=False, dtype=None)

Save the ChannelFrame to a WDF (Wandas Data File) format.

This saves the complete frame including all channel data and metadata in a format that can be loaded back with full fidelity.

Parameters:

Name Type Description Default
path Union[str, Path]

Path to save the file. '.wdf' extension will be added if not present.

required
format str

Format to use (currently only 'hdf5' is supported)

'hdf5'
compress Optional[str]

Compression method ('gzip' by default, None for no compression)

'gzip'
overwrite bool

Whether to overwrite existing file

False
dtype Optional[Union[str, dtype[Any]]]

Optional data type conversion before saving (e.g. 'float32')

None

Raises:

Type Description
FileExistsError

If the file exists and overwrite=False.

NotImplementedError

For unsupported formats.

Example

cf = ChannelFrame.read_wav("audio.wav") cf.save("audio_analysis.wdf")

Source code in wandas/frames/channel.py
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
def save(
    self,
    path: Union[str, Path],
    *,
    format: str = "hdf5",
    compress: Optional[str] = "gzip",
    overwrite: bool = False,
    dtype: Optional[Union[str, np.dtype[Any]]] = None,
) -> None:
    """Save the ChannelFrame to a WDF (Wandas Data File) format.

    This saves the complete frame including all channel data and metadata
    in a format that can be loaded back with full fidelity.

    Args:
        path: Path to save the file. '.wdf' extension will be added if not present.
        format: Format to use (currently only 'hdf5' is supported)
        compress: Compression method ('gzip' by default, None for no compression)
        overwrite: Whether to overwrite existing file
        dtype: Optional data type conversion before saving (e.g. 'float32')

    Raises:
        FileExistsError: If the file exists and overwrite=False.
        NotImplementedError: For unsupported formats.

    Example:
        >>> cf = ChannelFrame.read_wav("audio.wav")
        >>> cf.save("audio_analysis.wdf")
    """
    from ..io.wdf_io import save as wdf_save

    wdf_save(
        self,
        path,
        format=format,
        compress=compress,
        overwrite=overwrite,
        dtype=dtype,
    )
load(path, *, format='hdf5') classmethod

Load a ChannelFrame from a WDF (Wandas Data File) file.

This loads data saved with the save() method, preserving all channel data, metadata, labels, and units.

Parameters:

Name Type Description Default
path Union[str, Path]

Path to the WDF file

required
format str

Format of the file (currently only 'hdf5' is supported)

'hdf5'

Returns:

Type Description
ChannelFrame

A new ChannelFrame with all data and metadata loaded

Raises:

Type Description
FileNotFoundError

If the file doesn't exist

NotImplementedError

For unsupported formats

Example

cf = ChannelFrame.load("audio_analysis.wdf")

Source code in wandas/frames/channel.py
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
@classmethod
def load(cls, path: Union[str, Path], *, format: str = "hdf5") -> "ChannelFrame":
    """Load a ChannelFrame from a WDF (Wandas Data File) file.

    This loads data saved with the save() method, preserving all channel data,
    metadata, labels, and units.

    Args:
        path: Path to the WDF file
        format: Format of the file (currently only 'hdf5' is supported)

    Returns:
        A new ChannelFrame with all data and metadata loaded

    Raises:
        FileNotFoundError: If the file doesn't exist
        NotImplementedError: For unsupported formats

    Example:
        >>> cf = ChannelFrame.load("audio_analysis.wdf")
    """
    from ..io.wdf_io import load as wdf_load

    return wdf_load(path, format=format)
add_channel(data, label=None, align='strict', suffix_on_dup=None, inplace=False, **kwargs)
Source code in wandas/frames/channel.py
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
def add_channel(
    self,
    data: Union[np.ndarray[Any, Any], DaskArray, "ChannelFrame"],
    label: Optional[str] = None,
    align: str = "strict",
    suffix_on_dup: Optional[str] = None,
    inplace: bool = False,
    **kwargs: Any,
) -> "ChannelFrame":
    # ndarray/dask/同型Frame対応
    if isinstance(data, ChannelFrame):
        if self.sampling_rate != data.sampling_rate:
            raise ValueError("sampling_rate不一致")
        if data.n_samples != self.n_samples:
            if align == "pad":
                pad_len = self.n_samples - data.n_samples
                arr = data._data
                if pad_len > 0:
                    arr = concatenate(
                        [
                            arr,
                            from_array(
                                np.zeros((arr.shape[0], pad_len), dtype=arr.dtype)
                            ),
                        ],
                        axis=1,
                    )
                else:
                    arr = arr[:, : self.n_samples]
            elif align == "truncate":
                arr = data._data[:, : self.n_samples]
                if arr.shape[1] < self.n_samples:
                    pad_len = self.n_samples - arr.shape[1]
                    arr = concatenate(
                        [
                            arr,
                            from_array(
                                np.zeros((arr.shape[0], pad_len), dtype=arr.dtype)
                            ),
                        ],
                        axis=1,
                    )
            else:
                raise ValueError("データ長不一致: align指定を確認")
        else:
            arr = data._data
        labels = [ch.label for ch in self._channel_metadata]
        new_labels = []
        new_metadata_list = []
        for chmeta in data._channel_metadata:
            new_label = chmeta.label
            if new_label in labels or new_label in new_labels:
                if suffix_on_dup:
                    new_label += suffix_on_dup
                else:
                    raise ValueError(f"label重複: {new_label}")
            new_labels.append(new_label)
            # Copy the entire channel_metadata and update only the label
            new_ch_meta = chmeta.model_copy(deep=True)
            new_ch_meta.label = new_label
            new_metadata_list.append(new_ch_meta)
        new_data = concatenate([self._data, arr], axis=0)

        new_chmeta = self._channel_metadata + new_metadata_list
        if inplace:
            self._data = new_data
            self._channel_metadata = new_chmeta
            return self
        else:
            return ChannelFrame(
                data=new_data,
                sampling_rate=self.sampling_rate,
                label=self.label,
                metadata=self.metadata,
                operation_history=self.operation_history,
                channel_metadata=new_chmeta,
                previous=self,
            )
    if isinstance(data, np.ndarray):
        arr = from_array(data.reshape(1, -1))
    elif isinstance(data, DaskArray):
        arr = data[None, ...] if data.ndim == 1 else data
        if arr.shape[0] != 1:
            arr = arr.reshape((1, -1))
    else:
        raise TypeError("add_channel: ndarray/dask/同型Frameのみ対応")
    if arr.shape[1] != self.n_samples:
        if align == "pad":
            pad_len = self.n_samples - arr.shape[1]
            if pad_len > 0:
                arr = concatenate(
                    [arr, from_array(np.zeros((1, pad_len), dtype=arr.dtype))],
                    axis=1,
                )
            else:
                arr = arr[:, : self.n_samples]
        elif align == "truncate":
            arr = arr[:, : self.n_samples]
            if arr.shape[1] < self.n_samples:
                pad_len = self.n_samples - arr.shape[1]
                arr = concatenate(
                    [arr, from_array(np.zeros((1, pad_len), dtype=arr.dtype))],
                    axis=1,
                )
        else:
            raise ValueError("データ長不一致: align指定を確認")
    labels = [ch.label for ch in self._channel_metadata]
    new_label = label or f"ch{len(labels)}"
    if new_label in labels:
        if suffix_on_dup:
            new_label += suffix_on_dup
        else:
            raise ValueError("label重複")
    new_data = concatenate([self._data, arr], axis=0)
    from ..core.metadata import ChannelMetadata

    new_chmeta = self._channel_metadata + [ChannelMetadata(label=new_label)]
    if inplace:
        self._data = new_data
        self._channel_metadata = new_chmeta
        return self
    else:
        return ChannelFrame(
            data=new_data,
            sampling_rate=self.sampling_rate,
            label=self.label,
            metadata=self.metadata,
            operation_history=self.operation_history,
            channel_metadata=new_chmeta,
            previous=self,
        )
remove_channel(key, inplace=False)
Source code in wandas/frames/channel.py
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
def remove_channel(
    self, key: Union[int, str], inplace: bool = False
) -> "ChannelFrame":
    if isinstance(key, int):
        if not (0 <= key < self.n_channels):
            raise IndexError(f"index {key} out of range")
        idx = key
    else:
        labels = [ch.label for ch in self._channel_metadata]
        if key not in labels:
            raise KeyError(f"label {key} not found")
        idx = labels.index(key)
    new_data = self._data[[i for i in range(self.n_channels) if i != idx], :]
    new_chmeta = [ch for i, ch in enumerate(self._channel_metadata) if i != idx]
    if inplace:
        self._data = new_data
        self._channel_metadata = new_chmeta
        return self
    else:
        return ChannelFrame(
            data=new_data,
            sampling_rate=self.sampling_rate,
            label=self.label,
            metadata=self.metadata,
            operation_history=self.operation_history,
            channel_metadata=new_chmeta,
            previous=self,
        )
Functions

mixins

Channel frame mixins module.

Attributes
__all__ = ['ChannelProcessingMixin', 'ChannelTransformMixin'] module-attribute
Classes
ChannelProcessingMixin

Mixin that provides methods related to signal processing.

This mixin provides processing methods applied to audio signals and other time-series data, such as signal processing filters and transformation operations.

Source code in wandas/frames/mixins/channel_processing_mixin.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
class ChannelProcessingMixin:
    """Mixin that provides methods related to signal processing.

    This mixin provides processing methods applied to audio signals and
    other time-series data, such as signal processing filters and
    transformation operations.
    """

    def high_pass_filter(
        self: T_Processing, cutoff: float, order: int = 4
    ) -> T_Processing:
        """Apply a high-pass filter to the signal.

        Args:
            cutoff: Filter cutoff frequency (Hz)
            order: Filter order. Default is 4.

        Returns:
            New ChannelFrame after filter application
        """
        logger.debug(
            f"Setting up highpass filter: cutoff={cutoff}, order={order} (lazy)"
        )
        result = self.apply_operation("highpass_filter", cutoff=cutoff, order=order)
        return cast(T_Processing, result)

    def low_pass_filter(
        self: T_Processing, cutoff: float, order: int = 4
    ) -> T_Processing:
        """Apply a low-pass filter to the signal.

        Args:
            cutoff: Filter cutoff frequency (Hz)
            order: Filter order. Default is 4.

        Returns:
            New ChannelFrame after filter application
        """
        logger.debug(
            f"Setting up lowpass filter: cutoff={cutoff}, order={order} (lazy)"
        )
        result = self.apply_operation("lowpass_filter", cutoff=cutoff, order=order)
        return cast(T_Processing, result)

    def band_pass_filter(
        self: T_Processing, low_cutoff: float, high_cutoff: float, order: int = 4
    ) -> T_Processing:
        """Apply a band-pass filter to the signal.

        Args:
            low_cutoff: Lower cutoff frequency (Hz)
            high_cutoff: Higher cutoff frequency (Hz)
            order: Filter order. Default is 4.

        Returns:
            New ChannelFrame after filter application
        """
        logger.debug(
            f"Setting up bandpass filter: low_cutoff={low_cutoff}, "
            f"high_cutoff={high_cutoff}, order={order} (lazy)"
        )
        result = self.apply_operation(
            "bandpass_filter",
            low_cutoff=low_cutoff,
            high_cutoff=high_cutoff,
            order=order,
        )
        return cast(T_Processing, result)

    def normalize(
        self: T_Processing, target_level: float = -20, channel_wise: bool = True
    ) -> T_Processing:
        """Normalize signal levels.

        This method adjusts the signal amplitude to reach the target RMS level.

        Args:
            target_level: Target RMS level (dB). Default is -20.
            channel_wise: If True, normalize each channel individually.
                If False, apply the same scaling to all channels.

        Returns:
            New ChannelFrame containing the normalized signal
        """
        logger.debug(
            f"Setting up normalize: target_level={target_level}, "
            f"channel_wise={channel_wise} (lazy)"
        )
        result = self.apply_operation(
            "normalize", target_level=target_level, channel_wise=channel_wise
        )
        return cast(T_Processing, result)

    def a_weighting(self: T_Processing) -> T_Processing:
        """Apply A-weighting filter to the signal.

        A-weighting adjusts the frequency response to approximate human
        auditory perception, according to the IEC 61672-1:2013 standard.

        Returns:
            New ChannelFrame containing the A-weighted signal
        """
        result = self.apply_operation("a_weighting")
        return cast(T_Processing, result)

    def abs(self: T_Processing) -> T_Processing:
        """Compute the absolute value of the signal.

        Returns:
            New ChannelFrame containing the absolute values
        """
        result = self.apply_operation("abs")
        return cast(T_Processing, result)

    def power(self: T_Processing, exponent: float = 2.0) -> T_Processing:
        """Compute the power of the signal.

        Args:
            exponent: Exponent to raise the signal to. Default is 2.0.

        Returns:
            New ChannelFrame containing the powered signal
        """
        result = self.apply_operation("power", exponent=exponent)
        return cast(T_Processing, result)

    def _reduce_channels(self: T_Processing, op: str) -> T_Processing:
        """Helper to reduce all channels with the given operation ('sum' or 'mean')."""
        if op == "sum":
            reduced_data = self._data.sum(axis=0, keepdims=True)
            label = "sum"
        elif op == "mean":
            reduced_data = self._data.mean(axis=0, keepdims=True)
            label = "mean"
        else:
            raise ValueError(f"Unsupported reduction operation: {op}")

        units = [ch.unit for ch in self._channel_metadata]
        if all(u == units[0] for u in units):
            reduced_unit = units[0]
        else:
            reduced_unit = ""

        reduced_extra = {"source_extras": [ch.extra for ch in self._channel_metadata]}
        new_channel_metadata = [
            ChannelMetadata(
                label=label,
                unit=reduced_unit,
                extra=reduced_extra,
            )
        ]
        new_history = (
            self.operation_history.copy() if hasattr(self, "operation_history") else []
        )
        new_history.append({"operation": op})
        new_metadata = self.metadata.copy() if hasattr(self, "metadata") else {}
        result = self._create_new_instance(
            data=reduced_data,
            metadata=new_metadata,
            operation_history=new_history,
            channel_metadata=new_channel_metadata,
        )
        return result

    def sum(self: T_Processing) -> T_Processing:
        """Sum all channels.

        Returns:
            A new ChannelFrame with summed signal.
        """
        return cast(T_Processing, cast(Any, self)._reduce_channels("sum"))

    def mean(self: T_Processing) -> T_Processing:
        """Average all channels.

        Returns:
            A new ChannelFrame with averaged signal.
        """
        return cast(T_Processing, cast(Any, self)._reduce_channels("mean"))

    def trim(
        self: T_Processing,
        start: float = 0,
        end: Optional[float] = None,
    ) -> T_Processing:
        """Trim the signal to the specified time range.

        Args:
            start: Start time (seconds)
            end: End time (seconds)

        Returns:
            New ChannelFrame containing the trimmed signal

        Raises:
            ValueError: If end time is earlier than start time
        """
        if end is None:
            end = self.duration
        if start > end:
            raise ValueError("start must be less than end")
        result = self.apply_operation("trim", start=start, end=end)
        return cast(T_Processing, result)

    def fix_length(
        self: T_Processing,
        length: Optional[int] = None,
        duration: Optional[float] = None,
    ) -> T_Processing:
        """Adjust the signal to the specified length.

        Args:
            duration: Signal length in seconds
            length: Signal length in samples

        Returns:
            New ChannelFrame containing the adjusted signal
        """

        result = self.apply_operation("fix_length", length=length, duration=duration)
        return cast(T_Processing, result)

    def rms_trend(
        self: T_Processing,
        frame_length: int = 2048,
        hop_length: int = 512,
        dB: bool = False,  # noqa: N803
        Aw: bool = False,  # noqa: N803
    ) -> T_Processing:
        """Compute the RMS trend of the signal.

        This method calculates the root mean square value over a sliding window.

        Args:
            frame_length: Size of the sliding window in samples. Default is 2048.
            hop_length: Hop length between windows in samples. Default is 512.
            dB: Whether to return RMS values in decibels. Default is False.
            Aw: Whether to apply A-weighting. Default is False.

        Returns:
            New ChannelFrame containing the RMS trend
        """
        # Access _channel_metadata to retrieve reference values
        frame = cast(ProcessingFrameProtocol, self)

        # Ensure _channel_metadata exists before referencing
        ref_values = []
        if hasattr(frame, "_channel_metadata") and frame._channel_metadata:
            ref_values = [ch.ref for ch in frame._channel_metadata]

        result = self.apply_operation(
            "rms_trend",
            frame_length=frame_length,
            hop_length=hop_length,
            ref=ref_values,
            dB=dB,
            Aw=Aw,
        )

        # Update sampling rate
        result_obj = cast(T_Processing, result)
        if hasattr(result_obj, "sampling_rate"):
            result_obj.sampling_rate = frame.sampling_rate / hop_length

        return result_obj

    def channel_difference(
        self: T_Processing, other_channel: Union[int, str] = 0
    ) -> T_Processing:
        """Compute the difference between channels.

        Args:
            other_channel: Index or label of the reference channel. Default is 0.

        Returns:
            New ChannelFrame containing the channel difference
        """
        # label2index is a method of BaseFrame
        if isinstance(other_channel, str):
            if hasattr(self, "label2index"):
                other_channel = self.label2index(other_channel)

        result = self.apply_operation("channel_difference", other_channel=other_channel)
        return cast(T_Processing, result)

    def resampling(
        self: T_Processing,
        target_sr: float,
        **kwargs: Any,
    ) -> T_Processing:
        """Resample audio data.

        Args:
            target_sr: Target sampling rate (Hz)
            **kwargs: Additional resampling parameters

        Returns:
            Resampled ChannelFrame
        """
        return cast(
            T_Processing,
            self.apply_operation(
                "resampling",
                target_sr=target_sr,
                **kwargs,
            ),
        )

    def hpss_harmonic(
        self: T_Processing,
        kernel_size: Union[
            "_IntLike_co", tuple["_IntLike_co", "_IntLike_co"], list["_IntLike_co"]
        ] = 31,
        power: float = 2,
        margin: Union[
            "_FloatLike_co",
            tuple["_FloatLike_co", "_FloatLike_co"],
            list["_FloatLike_co"],
        ] = 1,
        n_fft: int = 2048,
        hop_length: Optional[int] = None,
        win_length: Optional[int] = None,
        window: "_WindowSpec" = "hann",
        center: bool = True,
        pad_mode: "_PadModeSTFT" = "constant",
    ) -> T_Processing:
        """
        Extract harmonic components using HPSS
         (Harmonic-Percussive Source Separation).

        This method separates the harmonic (tonal) components from the signal.

        Args:
            kernel_size: Median filter size for HPSS.
            power: Exponent for the Weiner filter used in HPSS.
            margin: Margin size for the separation.
            n_fft: Size of FFT window.
            hop_length: Hop length for STFT.
            win_length: Window length for STFT.
            window: Window type for STFT.
            center: If True, center the frames.
            pad_mode: Padding mode for STFT.

        Returns:
            A new ChannelFrame containing the harmonic components.
        """
        result = self.apply_operation(
            "hpss_harmonic",
            kernel_size=kernel_size,
            power=power,
            margin=margin,
            n_fft=n_fft,
            hop_length=hop_length,
            win_length=win_length,
            window=window,
            center=center,
            pad_mode=pad_mode,
        )
        return cast(T_Processing, result)

    def hpss_percussive(
        self: T_Processing,
        kernel_size: Union[
            "_IntLike_co", tuple["_IntLike_co", "_IntLike_co"], list["_IntLike_co"]
        ] = 31,
        power: float = 2,
        margin: Union[
            "_FloatLike_co",
            tuple["_FloatLike_co", "_FloatLike_co"],
            list["_FloatLike_co"],
        ] = 1,
        n_fft: int = 2048,
        hop_length: Optional[int] = None,
        win_length: Optional[int] = None,
        window: "_WindowSpec" = "hann",
        center: bool = True,
        pad_mode: "_PadModeSTFT" = "constant",
    ) -> T_Processing:
        """
        Extract percussive components using HPSS
        (Harmonic-Percussive Source Separation).

        This method separates the percussive (tonal) components from the signal.

        Args:
            kernel_size: Median filter size for HPSS.
            power: Exponent for the Weiner filter used in HPSS.
            margin: Margin size for the separation.

        Returns:
            A new ChannelFrame containing the harmonic components.
        """
        result = self.apply_operation(
            "hpss_percussive",
            kernel_size=kernel_size,
            power=power,
            margin=margin,
            n_fft=n_fft,
            hop_length=hop_length,
            win_length=win_length,
            window=window,
            center=center,
            pad_mode=pad_mode,
        )
        return cast(T_Processing, result)
Functions
high_pass_filter(cutoff, order=4)

Apply a high-pass filter to the signal.

Parameters:

Name Type Description Default
cutoff float

Filter cutoff frequency (Hz)

required
order int

Filter order. Default is 4.

4

Returns:

Type Description
T_Processing

New ChannelFrame after filter application

Source code in wandas/frames/mixins/channel_processing_mixin.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def high_pass_filter(
    self: T_Processing, cutoff: float, order: int = 4
) -> T_Processing:
    """Apply a high-pass filter to the signal.

    Args:
        cutoff: Filter cutoff frequency (Hz)
        order: Filter order. Default is 4.

    Returns:
        New ChannelFrame after filter application
    """
    logger.debug(
        f"Setting up highpass filter: cutoff={cutoff}, order={order} (lazy)"
    )
    result = self.apply_operation("highpass_filter", cutoff=cutoff, order=order)
    return cast(T_Processing, result)
low_pass_filter(cutoff, order=4)

Apply a low-pass filter to the signal.

Parameters:

Name Type Description Default
cutoff float

Filter cutoff frequency (Hz)

required
order int

Filter order. Default is 4.

4

Returns:

Type Description
T_Processing

New ChannelFrame after filter application

Source code in wandas/frames/mixins/channel_processing_mixin.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def low_pass_filter(
    self: T_Processing, cutoff: float, order: int = 4
) -> T_Processing:
    """Apply a low-pass filter to the signal.

    Args:
        cutoff: Filter cutoff frequency (Hz)
        order: Filter order. Default is 4.

    Returns:
        New ChannelFrame after filter application
    """
    logger.debug(
        f"Setting up lowpass filter: cutoff={cutoff}, order={order} (lazy)"
    )
    result = self.apply_operation("lowpass_filter", cutoff=cutoff, order=order)
    return cast(T_Processing, result)
band_pass_filter(low_cutoff, high_cutoff, order=4)

Apply a band-pass filter to the signal.

Parameters:

Name Type Description Default
low_cutoff float

Lower cutoff frequency (Hz)

required
high_cutoff float

Higher cutoff frequency (Hz)

required
order int

Filter order. Default is 4.

4

Returns:

Type Description
T_Processing

New ChannelFrame after filter application

Source code in wandas/frames/mixins/channel_processing_mixin.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def band_pass_filter(
    self: T_Processing, low_cutoff: float, high_cutoff: float, order: int = 4
) -> T_Processing:
    """Apply a band-pass filter to the signal.

    Args:
        low_cutoff: Lower cutoff frequency (Hz)
        high_cutoff: Higher cutoff frequency (Hz)
        order: Filter order. Default is 4.

    Returns:
        New ChannelFrame after filter application
    """
    logger.debug(
        f"Setting up bandpass filter: low_cutoff={low_cutoff}, "
        f"high_cutoff={high_cutoff}, order={order} (lazy)"
    )
    result = self.apply_operation(
        "bandpass_filter",
        low_cutoff=low_cutoff,
        high_cutoff=high_cutoff,
        order=order,
    )
    return cast(T_Processing, result)
normalize(target_level=-20, channel_wise=True)

Normalize signal levels.

This method adjusts the signal amplitude to reach the target RMS level.

Parameters:

Name Type Description Default
target_level float

Target RMS level (dB). Default is -20.

-20
channel_wise bool

If True, normalize each channel individually. If False, apply the same scaling to all channels.

True

Returns:

Type Description
T_Processing

New ChannelFrame containing the normalized signal

Source code in wandas/frames/mixins/channel_processing_mixin.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
def normalize(
    self: T_Processing, target_level: float = -20, channel_wise: bool = True
) -> T_Processing:
    """Normalize signal levels.

    This method adjusts the signal amplitude to reach the target RMS level.

    Args:
        target_level: Target RMS level (dB). Default is -20.
        channel_wise: If True, normalize each channel individually.
            If False, apply the same scaling to all channels.

    Returns:
        New ChannelFrame containing the normalized signal
    """
    logger.debug(
        f"Setting up normalize: target_level={target_level}, "
        f"channel_wise={channel_wise} (lazy)"
    )
    result = self.apply_operation(
        "normalize", target_level=target_level, channel_wise=channel_wise
    )
    return cast(T_Processing, result)
a_weighting()

Apply A-weighting filter to the signal.

A-weighting adjusts the frequency response to approximate human auditory perception, according to the IEC 61672-1:2013 standard.

Returns:

Type Description
T_Processing

New ChannelFrame containing the A-weighted signal

Source code in wandas/frames/mixins/channel_processing_mixin.py
113
114
115
116
117
118
119
120
121
122
123
def a_weighting(self: T_Processing) -> T_Processing:
    """Apply A-weighting filter to the signal.

    A-weighting adjusts the frequency response to approximate human
    auditory perception, according to the IEC 61672-1:2013 standard.

    Returns:
        New ChannelFrame containing the A-weighted signal
    """
    result = self.apply_operation("a_weighting")
    return cast(T_Processing, result)
abs()

Compute the absolute value of the signal.

Returns:

Type Description
T_Processing

New ChannelFrame containing the absolute values

Source code in wandas/frames/mixins/channel_processing_mixin.py
125
126
127
128
129
130
131
132
def abs(self: T_Processing) -> T_Processing:
    """Compute the absolute value of the signal.

    Returns:
        New ChannelFrame containing the absolute values
    """
    result = self.apply_operation("abs")
    return cast(T_Processing, result)
power(exponent=2.0)

Compute the power of the signal.

Parameters:

Name Type Description Default
exponent float

Exponent to raise the signal to. Default is 2.0.

2.0

Returns:

Type Description
T_Processing

New ChannelFrame containing the powered signal

Source code in wandas/frames/mixins/channel_processing_mixin.py
134
135
136
137
138
139
140
141
142
143
144
def power(self: T_Processing, exponent: float = 2.0) -> T_Processing:
    """Compute the power of the signal.

    Args:
        exponent: Exponent to raise the signal to. Default is 2.0.

    Returns:
        New ChannelFrame containing the powered signal
    """
    result = self.apply_operation("power", exponent=exponent)
    return cast(T_Processing, result)
sum()

Sum all channels.

Returns:

Type Description
T_Processing

A new ChannelFrame with summed signal.

Source code in wandas/frames/mixins/channel_processing_mixin.py
184
185
186
187
188
189
190
def sum(self: T_Processing) -> T_Processing:
    """Sum all channels.

    Returns:
        A new ChannelFrame with summed signal.
    """
    return cast(T_Processing, cast(Any, self)._reduce_channels("sum"))
mean()

Average all channels.

Returns:

Type Description
T_Processing

A new ChannelFrame with averaged signal.

Source code in wandas/frames/mixins/channel_processing_mixin.py
192
193
194
195
196
197
198
def mean(self: T_Processing) -> T_Processing:
    """Average all channels.

    Returns:
        A new ChannelFrame with averaged signal.
    """
    return cast(T_Processing, cast(Any, self)._reduce_channels("mean"))
trim(start=0, end=None)

Trim the signal to the specified time range.

Parameters:

Name Type Description Default
start float

Start time (seconds)

0
end Optional[float]

End time (seconds)

None

Returns:

Type Description
T_Processing

New ChannelFrame containing the trimmed signal

Raises:

Type Description
ValueError

If end time is earlier than start time

Source code in wandas/frames/mixins/channel_processing_mixin.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
def trim(
    self: T_Processing,
    start: float = 0,
    end: Optional[float] = None,
) -> T_Processing:
    """Trim the signal to the specified time range.

    Args:
        start: Start time (seconds)
        end: End time (seconds)

    Returns:
        New ChannelFrame containing the trimmed signal

    Raises:
        ValueError: If end time is earlier than start time
    """
    if end is None:
        end = self.duration
    if start > end:
        raise ValueError("start must be less than end")
    result = self.apply_operation("trim", start=start, end=end)
    return cast(T_Processing, result)
fix_length(length=None, duration=None)

Adjust the signal to the specified length.

Parameters:

Name Type Description Default
duration Optional[float]

Signal length in seconds

None
length Optional[int]

Signal length in samples

None

Returns:

Type Description
T_Processing

New ChannelFrame containing the adjusted signal

Source code in wandas/frames/mixins/channel_processing_mixin.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
def fix_length(
    self: T_Processing,
    length: Optional[int] = None,
    duration: Optional[float] = None,
) -> T_Processing:
    """Adjust the signal to the specified length.

    Args:
        duration: Signal length in seconds
        length: Signal length in samples

    Returns:
        New ChannelFrame containing the adjusted signal
    """

    result = self.apply_operation("fix_length", length=length, duration=duration)
    return cast(T_Processing, result)
rms_trend(frame_length=2048, hop_length=512, dB=False, Aw=False)

Compute the RMS trend of the signal.

This method calculates the root mean square value over a sliding window.

Parameters:

Name Type Description Default
frame_length int

Size of the sliding window in samples. Default is 2048.

2048
hop_length int

Hop length between windows in samples. Default is 512.

512
dB bool

Whether to return RMS values in decibels. Default is False.

False
Aw bool

Whether to apply A-weighting. Default is False.

False

Returns:

Type Description
T_Processing

New ChannelFrame containing the RMS trend

Source code in wandas/frames/mixins/channel_processing_mixin.py
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def rms_trend(
    self: T_Processing,
    frame_length: int = 2048,
    hop_length: int = 512,
    dB: bool = False,  # noqa: N803
    Aw: bool = False,  # noqa: N803
) -> T_Processing:
    """Compute the RMS trend of the signal.

    This method calculates the root mean square value over a sliding window.

    Args:
        frame_length: Size of the sliding window in samples. Default is 2048.
        hop_length: Hop length between windows in samples. Default is 512.
        dB: Whether to return RMS values in decibels. Default is False.
        Aw: Whether to apply A-weighting. Default is False.

    Returns:
        New ChannelFrame containing the RMS trend
    """
    # Access _channel_metadata to retrieve reference values
    frame = cast(ProcessingFrameProtocol, self)

    # Ensure _channel_metadata exists before referencing
    ref_values = []
    if hasattr(frame, "_channel_metadata") and frame._channel_metadata:
        ref_values = [ch.ref for ch in frame._channel_metadata]

    result = self.apply_operation(
        "rms_trend",
        frame_length=frame_length,
        hop_length=hop_length,
        ref=ref_values,
        dB=dB,
        Aw=Aw,
    )

    # Update sampling rate
    result_obj = cast(T_Processing, result)
    if hasattr(result_obj, "sampling_rate"):
        result_obj.sampling_rate = frame.sampling_rate / hop_length

    return result_obj
channel_difference(other_channel=0)

Compute the difference between channels.

Parameters:

Name Type Description Default
other_channel Union[int, str]

Index or label of the reference channel. Default is 0.

0

Returns:

Type Description
T_Processing

New ChannelFrame containing the channel difference

Source code in wandas/frames/mixins/channel_processing_mixin.py
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
def channel_difference(
    self: T_Processing, other_channel: Union[int, str] = 0
) -> T_Processing:
    """Compute the difference between channels.

    Args:
        other_channel: Index or label of the reference channel. Default is 0.

    Returns:
        New ChannelFrame containing the channel difference
    """
    # label2index is a method of BaseFrame
    if isinstance(other_channel, str):
        if hasattr(self, "label2index"):
            other_channel = self.label2index(other_channel)

    result = self.apply_operation("channel_difference", other_channel=other_channel)
    return cast(T_Processing, result)
resampling(target_sr, **kwargs)

Resample audio data.

Parameters:

Name Type Description Default
target_sr float

Target sampling rate (Hz)

required
**kwargs Any

Additional resampling parameters

{}

Returns:

Type Description
T_Processing

Resampled ChannelFrame

Source code in wandas/frames/mixins/channel_processing_mixin.py
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
def resampling(
    self: T_Processing,
    target_sr: float,
    **kwargs: Any,
) -> T_Processing:
    """Resample audio data.

    Args:
        target_sr: Target sampling rate (Hz)
        **kwargs: Additional resampling parameters

    Returns:
        Resampled ChannelFrame
    """
    return cast(
        T_Processing,
        self.apply_operation(
            "resampling",
            target_sr=target_sr,
            **kwargs,
        ),
    )
hpss_harmonic(kernel_size=31, power=2, margin=1, n_fft=2048, hop_length=None, win_length=None, window='hann', center=True, pad_mode='constant')

Extract harmonic components using HPSS (Harmonic-Percussive Source Separation).

This method separates the harmonic (tonal) components from the signal.

Parameters:

Name Type Description Default
kernel_size Union[_IntLike_co, tuple[_IntLike_co, _IntLike_co], list[_IntLike_co]]

Median filter size for HPSS.

31
power float

Exponent for the Weiner filter used in HPSS.

2
margin Union[_FloatLike_co, tuple[_FloatLike_co, _FloatLike_co], list[_FloatLike_co]]

Margin size for the separation.

1
n_fft int

Size of FFT window.

2048
hop_length Optional[int]

Hop length for STFT.

None
win_length Optional[int]

Window length for STFT.

None
window _WindowSpec

Window type for STFT.

'hann'
center bool

If True, center the frames.

True
pad_mode _PadModeSTFT

Padding mode for STFT.

'constant'

Returns:

Type Description
T_Processing

A new ChannelFrame containing the harmonic components.

Source code in wandas/frames/mixins/channel_processing_mixin.py
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
def hpss_harmonic(
    self: T_Processing,
    kernel_size: Union[
        "_IntLike_co", tuple["_IntLike_co", "_IntLike_co"], list["_IntLike_co"]
    ] = 31,
    power: float = 2,
    margin: Union[
        "_FloatLike_co",
        tuple["_FloatLike_co", "_FloatLike_co"],
        list["_FloatLike_co"],
    ] = 1,
    n_fft: int = 2048,
    hop_length: Optional[int] = None,
    win_length: Optional[int] = None,
    window: "_WindowSpec" = "hann",
    center: bool = True,
    pad_mode: "_PadModeSTFT" = "constant",
) -> T_Processing:
    """
    Extract harmonic components using HPSS
     (Harmonic-Percussive Source Separation).

    This method separates the harmonic (tonal) components from the signal.

    Args:
        kernel_size: Median filter size for HPSS.
        power: Exponent for the Weiner filter used in HPSS.
        margin: Margin size for the separation.
        n_fft: Size of FFT window.
        hop_length: Hop length for STFT.
        win_length: Window length for STFT.
        window: Window type for STFT.
        center: If True, center the frames.
        pad_mode: Padding mode for STFT.

    Returns:
        A new ChannelFrame containing the harmonic components.
    """
    result = self.apply_operation(
        "hpss_harmonic",
        kernel_size=kernel_size,
        power=power,
        margin=margin,
        n_fft=n_fft,
        hop_length=hop_length,
        win_length=win_length,
        window=window,
        center=center,
        pad_mode=pad_mode,
    )
    return cast(T_Processing, result)
hpss_percussive(kernel_size=31, power=2, margin=1, n_fft=2048, hop_length=None, win_length=None, window='hann', center=True, pad_mode='constant')

Extract percussive components using HPSS (Harmonic-Percussive Source Separation).

This method separates the percussive (tonal) components from the signal.

Parameters:

Name Type Description Default
kernel_size Union[_IntLike_co, tuple[_IntLike_co, _IntLike_co], list[_IntLike_co]]

Median filter size for HPSS.

31
power float

Exponent for the Weiner filter used in HPSS.

2
margin Union[_FloatLike_co, tuple[_FloatLike_co, _FloatLike_co], list[_FloatLike_co]]

Margin size for the separation.

1

Returns:

Type Description
T_Processing

A new ChannelFrame containing the harmonic components.

Source code in wandas/frames/mixins/channel_processing_mixin.py
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
def hpss_percussive(
    self: T_Processing,
    kernel_size: Union[
        "_IntLike_co", tuple["_IntLike_co", "_IntLike_co"], list["_IntLike_co"]
    ] = 31,
    power: float = 2,
    margin: Union[
        "_FloatLike_co",
        tuple["_FloatLike_co", "_FloatLike_co"],
        list["_FloatLike_co"],
    ] = 1,
    n_fft: int = 2048,
    hop_length: Optional[int] = None,
    win_length: Optional[int] = None,
    window: "_WindowSpec" = "hann",
    center: bool = True,
    pad_mode: "_PadModeSTFT" = "constant",
) -> T_Processing:
    """
    Extract percussive components using HPSS
    (Harmonic-Percussive Source Separation).

    This method separates the percussive (tonal) components from the signal.

    Args:
        kernel_size: Median filter size for HPSS.
        power: Exponent for the Weiner filter used in HPSS.
        margin: Margin size for the separation.

    Returns:
        A new ChannelFrame containing the harmonic components.
    """
    result = self.apply_operation(
        "hpss_percussive",
        kernel_size=kernel_size,
        power=power,
        margin=margin,
        n_fft=n_fft,
        hop_length=hop_length,
        win_length=win_length,
        window=window,
        center=center,
        pad_mode=pad_mode,
    )
    return cast(T_Processing, result)
ChannelTransformMixin

Mixin providing methods related to frequency transformations.

This mixin provides operations related to frequency analysis and transformations such as FFT, STFT, and Welch method.

Source code in wandas/frames/mixins/channel_transform_mixin.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
class ChannelTransformMixin:
    """Mixin providing methods related to frequency transformations.

    This mixin provides operations related to frequency analysis and
    transformations such as FFT, STFT, and Welch method.
    """

    def fft(
        self: T_Transform, n_fft: Optional[int] = None, window: str = "hann"
    ) -> "SpectralFrame":
        """Calculate Fast Fourier Transform (FFT).

        Args:
            n_fft: Number of FFT points. Default is the next power of 2 of the data
                length.
            window: Window type. Default is "hann".

        Returns:
            SpectralFrame containing FFT results
        """
        from wandas.frames.spectral import SpectralFrame
        from wandas.processing import FFT, create_operation

        params = {"n_fft": n_fft, "window": window}
        operation_name = "fft"
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

        # Create operation instance
        operation = create_operation(operation_name, self.sampling_rate, **params)
        operation = cast("FFT", operation)
        # Apply processing to data
        spectrum_data = operation.process(self._data)

        logger.debug(
            f"Created new SpectralFrame with operation {operation_name} added to graph"
        )

        if n_fft is None:
            is_even = spectrum_data.shape[-1] % 2 == 0
            _n_fft = (
                spectrum_data.shape[-1] * 2 - 2
                if is_even
                else spectrum_data.shape[-1] * 2 - 1
            )
        else:
            _n_fft = n_fft

        # Cast self as BaseFrame type
        base_self = cast(BaseFrame[Any], self)

        return SpectralFrame(
            data=spectrum_data,
            sampling_rate=self.sampling_rate,
            n_fft=_n_fft,
            window=operation.window,
            label=f"Spectrum of {self.label}",
            metadata={**self.metadata, "window": window, "n_fft": _n_fft},
            operation_history=[
                *self.operation_history,
                {"operation": "fft", "params": {"n_fft": _n_fft, "window": window}},
            ],
            channel_metadata=self._channel_metadata,
            previous=base_self,
        )

    def welch(
        self: T_Transform,
        n_fft: Optional[int] = None,
        hop_length: Optional[int] = None,
        win_length: int = 2048,
        window: str = "hann",
        average: str = "mean",
    ) -> "SpectralFrame":
        """Calculate power spectral density using Welch's method.

        Args:
            n_fft: Number of FFT points. Default is 2048.
            hop_length: Number of samples between frames.
                Default is n_fft//4.
            win_length: Window length. Default is n_fft.
            window: Window type. Default is "hann".
            average: Method for averaging segments. Default is "mean".

        Returns:
            SpectralFrame containing power spectral density
        """
        from wandas.frames.spectral import SpectralFrame
        from wandas.processing import Welch, create_operation

        params = dict(
            n_fft=n_fft or win_length,
            hop_length=hop_length,
            win_length=win_length,
            window=window,
            average=average,
        )
        operation_name = "welch"
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

        # Create operation instance
        operation = create_operation(operation_name, self.sampling_rate, **params)
        operation = cast("Welch", operation)
        # Apply processing to data
        spectrum_data = operation.process(self._data)

        logger.debug(
            f"Created new SpectralFrame with operation {operation_name} added to graph"
        )

        # Cast self as BaseFrame type
        base_self = cast(BaseFrame[Any], self)

        return SpectralFrame(
            data=spectrum_data,
            sampling_rate=self.sampling_rate,
            n_fft=operation.n_fft,
            window=operation.window,
            label=f"Spectrum of {self.label}",
            metadata={**self.metadata, **params},
            operation_history=[
                *self.operation_history,
                {"operation": "welch", "params": params},
            ],
            channel_metadata=self._channel_metadata,
            previous=base_self,
        )

    def noct_spectrum(
        self: T_Transform,
        fmin: float,
        fmax: float,
        n: int = 3,
        G: int = 10,  # noqa: N803
        fr: int = 1000,
    ) -> "NOctFrame":
        """Calculate N-octave band spectrum.

        Args:
            fmin: Minimum center frequency (Hz). Default is 20 Hz.
            fmax: Maximum center frequency (Hz). Default is 20000 Hz.
            n: Band division (1: octave, 3: 1/3 octave). Default is 3.
            G: Reference gain (dB). Default is 10 dB.
            fr: Reference frequency (Hz). Default is 1000 Hz.

        Returns:
            NOctFrame containing N-octave band spectrum
        """
        from wandas.processing import NOctSpectrum, create_operation

        from ..noct import NOctFrame

        params = {"fmin": fmin, "fmax": fmax, "n": n, "G": G, "fr": fr}
        operation_name = "noct_spectrum"
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

        # Create operation instance
        operation = create_operation(operation_name, self.sampling_rate, **params)
        operation = cast("NOctSpectrum", operation)
        # Apply processing to data
        spectrum_data = operation.process(self._data)

        logger.debug(
            f"Created new SpectralFrame with operation {operation_name} added to graph"
        )

        # Cast self as BaseFrame type
        base_self = cast(BaseFrame[Any], self)

        return NOctFrame(
            data=spectrum_data,
            sampling_rate=self.sampling_rate,
            fmin=fmin,
            fmax=fmax,
            n=n,
            G=G,
            fr=fr,
            label=f"1/{n}Oct of {self.label}",
            metadata={**self.metadata, **params},
            operation_history=[
                *self.operation_history,
                {
                    "operation": "noct_spectrum",
                    "params": params,
                },
            ],
            channel_metadata=self._channel_metadata,
            previous=base_self,
        )

    def stft(
        self: T_Transform,
        n_fft: int = 2048,
        hop_length: Optional[int] = None,
        win_length: Optional[int] = None,
        window: str = "hann",
    ) -> "SpectrogramFrame":
        """Calculate Short-Time Fourier Transform.

        Args:
            n_fft: Number of FFT points. Default is 2048.
            hop_length: Number of samples between frames.
                Default is n_fft//4.
            win_length: Window length. Default is n_fft.
            window: Window type. Default is "hann".

        Returns:
            SpectrogramFrame containing STFT results
        """
        from wandas.processing import STFT, create_operation

        from ..spectrogram import SpectrogramFrame

        # Set hop length and window length
        _hop_length = hop_length if hop_length is not None else n_fft // 4
        _win_length = win_length if win_length is not None else n_fft

        params = {
            "n_fft": n_fft,
            "hop_length": _hop_length,
            "win_length": _win_length,
            "window": window,
        }
        operation_name = "stft"
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

        # Create operation instance
        operation = create_operation(operation_name, self.sampling_rate, **params)
        operation = cast("STFT", operation)

        # Apply processing to data
        spectrogram_data = operation.process(self._data)

        logger.debug(
            f"Created new SpectrogramFrame with operation {operation_name} added to graph"  # noqa: E501
        )

        # Cast self as BaseFrame type
        base_self = cast(BaseFrame[Any], self)

        # Create new instance
        return SpectrogramFrame(
            data=spectrogram_data,
            sampling_rate=self.sampling_rate,
            n_fft=n_fft,
            hop_length=_hop_length,
            win_length=_win_length,
            window=window,
            label=f"stft({self.label})",
            metadata=self.metadata,
            operation_history=self.operation_history,
            channel_metadata=self._channel_metadata,
            previous=base_self,
        )

    def coherence(
        self: T_Transform,
        n_fft: int = 2048,
        hop_length: Optional[int] = None,
        win_length: Optional[int] = None,
        window: str = "hann",
        detrend: str = "constant",
    ) -> "SpectralFrame":
        """Calculate magnitude squared coherence.

        Args:
            n_fft: Number of FFT points. Default is 2048.
            hop_length: Number of samples between frames.
                Default is n_fft//4.
            win_length: Window length. Default is n_fft.
            window: Window type. Default is "hann".
            detrend: Detrend method. Options: "constant", "linear", None.

        Returns:
            SpectralFrame containing magnitude squared coherence
        """
        from wandas.core.metadata import ChannelMetadata
        from wandas.processing import Coherence, create_operation

        from ..spectral import SpectralFrame

        params = {
            "n_fft": n_fft,
            "hop_length": hop_length,
            "win_length": win_length,
            "window": window,
            "detrend": detrend,
        }
        operation_name = "coherence"
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

        # Create operation instance
        operation = create_operation(operation_name, self.sampling_rate, **params)
        operation = cast("Coherence", operation)

        # Apply processing to data
        coherence_data = operation.process(self._data)

        logger.debug(
            f"Created new SpectralFrame with operation {operation_name} added to graph"
        )

        # Cast self as BaseFrame type
        base_self = cast(BaseFrame[Any], self)

        # Create new channel metadata
        channel_metadata = []
        for in_ch in self._channel_metadata:
            for out_ch in self._channel_metadata:
                meta = ChannelMetadata()
                meta.label = f"$\\gamma_{{{in_ch.label}, {out_ch.label}}}$"
                meta.unit = ""
                meta.ref = 1
                meta["metadata"] = dict(
                    in_ch=in_ch["metadata"], out_ch=out_ch["metadata"]
                )
                channel_metadata.append(meta)

        # Create new instance
        return SpectralFrame(
            data=coherence_data,
            sampling_rate=self.sampling_rate,
            n_fft=operation.n_fft,
            window=operation.window,
            label=f"Coherence of {self.label}",
            metadata={**self.metadata, **params},
            operation_history=[
                *self.operation_history,
                {"operation": operation_name, "params": params},
            ],
            channel_metadata=channel_metadata,
            previous=base_self,
        )

    def csd(
        self: T_Transform,
        n_fft: int = 2048,
        hop_length: Optional[int] = None,
        win_length: Optional[int] = None,
        window: str = "hann",
        detrend: str = "constant",
        scaling: str = "spectrum",
        average: str = "mean",
    ) -> "SpectralFrame":
        """Calculate cross-spectral density matrix.

        Args:
            n_fft: Number of FFT points. Default is 2048.
            hop_length: Number of samples between frames.
                Default is n_fft//4.
            win_length: Window length. Default is n_fft.
            window: Window type. Default is "hann".
            detrend: Detrend method. Options: "constant", "linear", None.
            scaling: Scaling method. Options: "spectrum", "density".
            average: Method for averaging segments. Default is "mean".

        Returns:
            SpectralFrame containing cross-spectral density matrix
        """
        from wandas.core.metadata import ChannelMetadata
        from wandas.frames.spectral import SpectralFrame
        from wandas.processing import CSD, create_operation

        params = {
            "n_fft": n_fft,
            "hop_length": hop_length,
            "win_length": win_length,
            "window": window,
            "detrend": detrend,
            "scaling": scaling,
            "average": average,
        }
        operation_name = "csd"
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

        # Create operation instance
        operation = create_operation(operation_name, self.sampling_rate, **params)
        operation = cast("CSD", operation)

        # Apply processing to data
        csd_data = operation.process(self._data)

        logger.debug(
            f"Created new SpectralFrame with operation {operation_name} added to graph"
        )

        # Cast self as BaseFrame type
        base_self = cast(BaseFrame[Any], self)

        # Create new channel metadata
        channel_metadata = []
        for in_ch in self._channel_metadata:
            for out_ch in self._channel_metadata:
                meta = ChannelMetadata()
                meta.label = f"{operation_name}({in_ch.label}, {out_ch.label})"
                meta.unit = ""
                meta.ref = 1
                meta["metadata"] = dict(
                    in_ch=in_ch["metadata"], out_ch=out_ch["metadata"]
                )
                channel_metadata.append(meta)

        # Create new instance
        return SpectralFrame(
            data=csd_data,
            sampling_rate=self.sampling_rate,
            n_fft=operation.n_fft,
            window=operation.window,
            label=f"$C_{{{in_ch.label}, {out_ch.label}}}$",
            metadata={**self.metadata, **params},
            operation_history=[
                *self.operation_history,
                {"operation": operation_name, "params": params},
            ],
            channel_metadata=channel_metadata,
            previous=base_self,
        )

    def transfer_function(
        self: T_Transform,
        n_fft: int = 2048,
        hop_length: Optional[int] = None,
        win_length: Optional[int] = None,
        window: str = "hann",
        detrend: str = "constant",
        scaling: str = "spectrum",
        average: str = "mean",
    ) -> "SpectralFrame":
        """Calculate transfer function matrix.

        The transfer function represents the signal transfer characteristics between
        channels in the frequency domain and represents the input-output relationship
        of the system.

        Args:
            n_fft: Number of FFT points. Default is 2048.
            hop_length: Number of samples between frames.
                Default is n_fft//4.
            win_length: Window length. Default is n_fft.
            window: Window type. Default is "hann".
            detrend: Detrend method. Options: "constant", "linear", None.
            scaling: Scaling method. Options: "spectrum", "density".
            average: Method for averaging segments. Default is "mean".

        Returns:
            SpectralFrame containing transfer function matrix
        """
        from wandas.core.metadata import ChannelMetadata
        from wandas.frames.spectral import SpectralFrame
        from wandas.processing import TransferFunction, create_operation

        params = {
            "n_fft": n_fft,
            "hop_length": hop_length,
            "win_length": win_length,
            "window": window,
            "detrend": detrend,
            "scaling": scaling,
            "average": average,
        }
        operation_name = "transfer_function"
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

        # Create operation instance
        operation = create_operation(operation_name, self.sampling_rate, **params)
        operation = cast("TransferFunction", operation)

        # Apply processing to data
        tf_data = operation.process(self._data)

        logger.debug(
            f"Created new SpectralFrame with operation {operation_name} added to graph"
        )

        # Cast self as BaseFrame type
        base_self = cast(BaseFrame[Any], self)

        # Create new channel metadata
        channel_metadata = []
        for in_ch in self._channel_metadata:
            for out_ch in self._channel_metadata:
                meta = ChannelMetadata()
                meta.label = f"$H_{{{in_ch.label}, {out_ch.label}}}$"
                meta.unit = ""
                meta.ref = 1
                meta["metadata"] = dict(
                    in_ch=in_ch["metadata"], out_ch=out_ch["metadata"]
                )
                channel_metadata.append(meta)

        # Create new instance
        return SpectralFrame(
            data=tf_data,
            sampling_rate=self.sampling_rate,
            n_fft=operation.n_fft,
            window=operation.window,
            label=f"Transfer function of {self.label}",
            metadata={**self.metadata, **params},
            operation_history=[
                *self.operation_history,
                {"operation": operation_name, "params": params},
            ],
            channel_metadata=channel_metadata,
            previous=base_self,
        )
Functions
fft(n_fft=None, window='hann')

Calculate Fast Fourier Transform (FFT).

Parameters:

Name Type Description Default
n_fft Optional[int]

Number of FFT points. Default is the next power of 2 of the data length.

None
window str

Window type. Default is "hann".

'hann'

Returns:

Type Description
SpectralFrame

SpectralFrame containing FFT results

Source code in wandas/frames/mixins/channel_transform_mixin.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def fft(
    self: T_Transform, n_fft: Optional[int] = None, window: str = "hann"
) -> "SpectralFrame":
    """Calculate Fast Fourier Transform (FFT).

    Args:
        n_fft: Number of FFT points. Default is the next power of 2 of the data
            length.
        window: Window type. Default is "hann".

    Returns:
        SpectralFrame containing FFT results
    """
    from wandas.frames.spectral import SpectralFrame
    from wandas.processing import FFT, create_operation

    params = {"n_fft": n_fft, "window": window}
    operation_name = "fft"
    logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

    # Create operation instance
    operation = create_operation(operation_name, self.sampling_rate, **params)
    operation = cast("FFT", operation)
    # Apply processing to data
    spectrum_data = operation.process(self._data)

    logger.debug(
        f"Created new SpectralFrame with operation {operation_name} added to graph"
    )

    if n_fft is None:
        is_even = spectrum_data.shape[-1] % 2 == 0
        _n_fft = (
            spectrum_data.shape[-1] * 2 - 2
            if is_even
            else spectrum_data.shape[-1] * 2 - 1
        )
    else:
        _n_fft = n_fft

    # Cast self as BaseFrame type
    base_self = cast(BaseFrame[Any], self)

    return SpectralFrame(
        data=spectrum_data,
        sampling_rate=self.sampling_rate,
        n_fft=_n_fft,
        window=operation.window,
        label=f"Spectrum of {self.label}",
        metadata={**self.metadata, "window": window, "n_fft": _n_fft},
        operation_history=[
            *self.operation_history,
            {"operation": "fft", "params": {"n_fft": _n_fft, "window": window}},
        ],
        channel_metadata=self._channel_metadata,
        previous=base_self,
    )
welch(n_fft=None, hop_length=None, win_length=2048, window='hann', average='mean')

Calculate power spectral density using Welch's method.

Parameters:

Name Type Description Default
n_fft Optional[int]

Number of FFT points. Default is 2048.

None
hop_length Optional[int]

Number of samples between frames. Default is n_fft//4.

None
win_length int

Window length. Default is n_fft.

2048
window str

Window type. Default is "hann".

'hann'
average str

Method for averaging segments. Default is "mean".

'mean'

Returns:

Type Description
SpectralFrame

SpectralFrame containing power spectral density

Source code in wandas/frames/mixins/channel_transform_mixin.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def welch(
    self: T_Transform,
    n_fft: Optional[int] = None,
    hop_length: Optional[int] = None,
    win_length: int = 2048,
    window: str = "hann",
    average: str = "mean",
) -> "SpectralFrame":
    """Calculate power spectral density using Welch's method.

    Args:
        n_fft: Number of FFT points. Default is 2048.
        hop_length: Number of samples between frames.
            Default is n_fft//4.
        win_length: Window length. Default is n_fft.
        window: Window type. Default is "hann".
        average: Method for averaging segments. Default is "mean".

    Returns:
        SpectralFrame containing power spectral density
    """
    from wandas.frames.spectral import SpectralFrame
    from wandas.processing import Welch, create_operation

    params = dict(
        n_fft=n_fft or win_length,
        hop_length=hop_length,
        win_length=win_length,
        window=window,
        average=average,
    )
    operation_name = "welch"
    logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

    # Create operation instance
    operation = create_operation(operation_name, self.sampling_rate, **params)
    operation = cast("Welch", operation)
    # Apply processing to data
    spectrum_data = operation.process(self._data)

    logger.debug(
        f"Created new SpectralFrame with operation {operation_name} added to graph"
    )

    # Cast self as BaseFrame type
    base_self = cast(BaseFrame[Any], self)

    return SpectralFrame(
        data=spectrum_data,
        sampling_rate=self.sampling_rate,
        n_fft=operation.n_fft,
        window=operation.window,
        label=f"Spectrum of {self.label}",
        metadata={**self.metadata, **params},
        operation_history=[
            *self.operation_history,
            {"operation": "welch", "params": params},
        ],
        channel_metadata=self._channel_metadata,
        previous=base_self,
    )
noct_spectrum(fmin, fmax, n=3, G=10, fr=1000)

Calculate N-octave band spectrum.

Parameters:

Name Type Description Default
fmin float

Minimum center frequency (Hz). Default is 20 Hz.

required
fmax float

Maximum center frequency (Hz). Default is 20000 Hz.

required
n int

Band division (1: octave, 3: 1/3 octave). Default is 3.

3
G int

Reference gain (dB). Default is 10 dB.

10
fr int

Reference frequency (Hz). Default is 1000 Hz.

1000

Returns:

Type Description
NOctFrame

NOctFrame containing N-octave band spectrum

Source code in wandas/frames/mixins/channel_transform_mixin.py
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def noct_spectrum(
    self: T_Transform,
    fmin: float,
    fmax: float,
    n: int = 3,
    G: int = 10,  # noqa: N803
    fr: int = 1000,
) -> "NOctFrame":
    """Calculate N-octave band spectrum.

    Args:
        fmin: Minimum center frequency (Hz). Default is 20 Hz.
        fmax: Maximum center frequency (Hz). Default is 20000 Hz.
        n: Band division (1: octave, 3: 1/3 octave). Default is 3.
        G: Reference gain (dB). Default is 10 dB.
        fr: Reference frequency (Hz). Default is 1000 Hz.

    Returns:
        NOctFrame containing N-octave band spectrum
    """
    from wandas.processing import NOctSpectrum, create_operation

    from ..noct import NOctFrame

    params = {"fmin": fmin, "fmax": fmax, "n": n, "G": G, "fr": fr}
    operation_name = "noct_spectrum"
    logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

    # Create operation instance
    operation = create_operation(operation_name, self.sampling_rate, **params)
    operation = cast("NOctSpectrum", operation)
    # Apply processing to data
    spectrum_data = operation.process(self._data)

    logger.debug(
        f"Created new SpectralFrame with operation {operation_name} added to graph"
    )

    # Cast self as BaseFrame type
    base_self = cast(BaseFrame[Any], self)

    return NOctFrame(
        data=spectrum_data,
        sampling_rate=self.sampling_rate,
        fmin=fmin,
        fmax=fmax,
        n=n,
        G=G,
        fr=fr,
        label=f"1/{n}Oct of {self.label}",
        metadata={**self.metadata, **params},
        operation_history=[
            *self.operation_history,
            {
                "operation": "noct_spectrum",
                "params": params,
            },
        ],
        channel_metadata=self._channel_metadata,
        previous=base_self,
    )
stft(n_fft=2048, hop_length=None, win_length=None, window='hann')

Calculate Short-Time Fourier Transform.

Parameters:

Name Type Description Default
n_fft int

Number of FFT points. Default is 2048.

2048
hop_length Optional[int]

Number of samples between frames. Default is n_fft//4.

None
win_length Optional[int]

Window length. Default is n_fft.

None
window str

Window type. Default is "hann".

'hann'

Returns:

Type Description
SpectrogramFrame

SpectrogramFrame containing STFT results

Source code in wandas/frames/mixins/channel_transform_mixin.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
def stft(
    self: T_Transform,
    n_fft: int = 2048,
    hop_length: Optional[int] = None,
    win_length: Optional[int] = None,
    window: str = "hann",
) -> "SpectrogramFrame":
    """Calculate Short-Time Fourier Transform.

    Args:
        n_fft: Number of FFT points. Default is 2048.
        hop_length: Number of samples between frames.
            Default is n_fft//4.
        win_length: Window length. Default is n_fft.
        window: Window type. Default is "hann".

    Returns:
        SpectrogramFrame containing STFT results
    """
    from wandas.processing import STFT, create_operation

    from ..spectrogram import SpectrogramFrame

    # Set hop length and window length
    _hop_length = hop_length if hop_length is not None else n_fft // 4
    _win_length = win_length if win_length is not None else n_fft

    params = {
        "n_fft": n_fft,
        "hop_length": _hop_length,
        "win_length": _win_length,
        "window": window,
    }
    operation_name = "stft"
    logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

    # Create operation instance
    operation = create_operation(operation_name, self.sampling_rate, **params)
    operation = cast("STFT", operation)

    # Apply processing to data
    spectrogram_data = operation.process(self._data)

    logger.debug(
        f"Created new SpectrogramFrame with operation {operation_name} added to graph"  # noqa: E501
    )

    # Cast self as BaseFrame type
    base_self = cast(BaseFrame[Any], self)

    # Create new instance
    return SpectrogramFrame(
        data=spectrogram_data,
        sampling_rate=self.sampling_rate,
        n_fft=n_fft,
        hop_length=_hop_length,
        win_length=_win_length,
        window=window,
        label=f"stft({self.label})",
        metadata=self.metadata,
        operation_history=self.operation_history,
        channel_metadata=self._channel_metadata,
        previous=base_self,
    )
coherence(n_fft=2048, hop_length=None, win_length=None, window='hann', detrend='constant')

Calculate magnitude squared coherence.

Parameters:

Name Type Description Default
n_fft int

Number of FFT points. Default is 2048.

2048
hop_length Optional[int]

Number of samples between frames. Default is n_fft//4.

None
win_length Optional[int]

Window length. Default is n_fft.

None
window str

Window type. Default is "hann".

'hann'
detrend str

Detrend method. Options: "constant", "linear", None.

'constant'

Returns:

Type Description
SpectralFrame

SpectralFrame containing magnitude squared coherence

Source code in wandas/frames/mixins/channel_transform_mixin.py
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
def coherence(
    self: T_Transform,
    n_fft: int = 2048,
    hop_length: Optional[int] = None,
    win_length: Optional[int] = None,
    window: str = "hann",
    detrend: str = "constant",
) -> "SpectralFrame":
    """Calculate magnitude squared coherence.

    Args:
        n_fft: Number of FFT points. Default is 2048.
        hop_length: Number of samples between frames.
            Default is n_fft//4.
        win_length: Window length. Default is n_fft.
        window: Window type. Default is "hann".
        detrend: Detrend method. Options: "constant", "linear", None.

    Returns:
        SpectralFrame containing magnitude squared coherence
    """
    from wandas.core.metadata import ChannelMetadata
    from wandas.processing import Coherence, create_operation

    from ..spectral import SpectralFrame

    params = {
        "n_fft": n_fft,
        "hop_length": hop_length,
        "win_length": win_length,
        "window": window,
        "detrend": detrend,
    }
    operation_name = "coherence"
    logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

    # Create operation instance
    operation = create_operation(operation_name, self.sampling_rate, **params)
    operation = cast("Coherence", operation)

    # Apply processing to data
    coherence_data = operation.process(self._data)

    logger.debug(
        f"Created new SpectralFrame with operation {operation_name} added to graph"
    )

    # Cast self as BaseFrame type
    base_self = cast(BaseFrame[Any], self)

    # Create new channel metadata
    channel_metadata = []
    for in_ch in self._channel_metadata:
        for out_ch in self._channel_metadata:
            meta = ChannelMetadata()
            meta.label = f"$\\gamma_{{{in_ch.label}, {out_ch.label}}}$"
            meta.unit = ""
            meta.ref = 1
            meta["metadata"] = dict(
                in_ch=in_ch["metadata"], out_ch=out_ch["metadata"]
            )
            channel_metadata.append(meta)

    # Create new instance
    return SpectralFrame(
        data=coherence_data,
        sampling_rate=self.sampling_rate,
        n_fft=operation.n_fft,
        window=operation.window,
        label=f"Coherence of {self.label}",
        metadata={**self.metadata, **params},
        operation_history=[
            *self.operation_history,
            {"operation": operation_name, "params": params},
        ],
        channel_metadata=channel_metadata,
        previous=base_self,
    )
csd(n_fft=2048, hop_length=None, win_length=None, window='hann', detrend='constant', scaling='spectrum', average='mean')

Calculate cross-spectral density matrix.

Parameters:

Name Type Description Default
n_fft int

Number of FFT points. Default is 2048.

2048
hop_length Optional[int]

Number of samples between frames. Default is n_fft//4.

None
win_length Optional[int]

Window length. Default is n_fft.

None
window str

Window type. Default is "hann".

'hann'
detrend str

Detrend method. Options: "constant", "linear", None.

'constant'
scaling str

Scaling method. Options: "spectrum", "density".

'spectrum'
average str

Method for averaging segments. Default is "mean".

'mean'

Returns:

Type Description
SpectralFrame

SpectralFrame containing cross-spectral density matrix

Source code in wandas/frames/mixins/channel_transform_mixin.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
def csd(
    self: T_Transform,
    n_fft: int = 2048,
    hop_length: Optional[int] = None,
    win_length: Optional[int] = None,
    window: str = "hann",
    detrend: str = "constant",
    scaling: str = "spectrum",
    average: str = "mean",
) -> "SpectralFrame":
    """Calculate cross-spectral density matrix.

    Args:
        n_fft: Number of FFT points. Default is 2048.
        hop_length: Number of samples between frames.
            Default is n_fft//4.
        win_length: Window length. Default is n_fft.
        window: Window type. Default is "hann".
        detrend: Detrend method. Options: "constant", "linear", None.
        scaling: Scaling method. Options: "spectrum", "density".
        average: Method for averaging segments. Default is "mean".

    Returns:
        SpectralFrame containing cross-spectral density matrix
    """
    from wandas.core.metadata import ChannelMetadata
    from wandas.frames.spectral import SpectralFrame
    from wandas.processing import CSD, create_operation

    params = {
        "n_fft": n_fft,
        "hop_length": hop_length,
        "win_length": win_length,
        "window": window,
        "detrend": detrend,
        "scaling": scaling,
        "average": average,
    }
    operation_name = "csd"
    logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

    # Create operation instance
    operation = create_operation(operation_name, self.sampling_rate, **params)
    operation = cast("CSD", operation)

    # Apply processing to data
    csd_data = operation.process(self._data)

    logger.debug(
        f"Created new SpectralFrame with operation {operation_name} added to graph"
    )

    # Cast self as BaseFrame type
    base_self = cast(BaseFrame[Any], self)

    # Create new channel metadata
    channel_metadata = []
    for in_ch in self._channel_metadata:
        for out_ch in self._channel_metadata:
            meta = ChannelMetadata()
            meta.label = f"{operation_name}({in_ch.label}, {out_ch.label})"
            meta.unit = ""
            meta.ref = 1
            meta["metadata"] = dict(
                in_ch=in_ch["metadata"], out_ch=out_ch["metadata"]
            )
            channel_metadata.append(meta)

    # Create new instance
    return SpectralFrame(
        data=csd_data,
        sampling_rate=self.sampling_rate,
        n_fft=operation.n_fft,
        window=operation.window,
        label=f"$C_{{{in_ch.label}, {out_ch.label}}}$",
        metadata={**self.metadata, **params},
        operation_history=[
            *self.operation_history,
            {"operation": operation_name, "params": params},
        ],
        channel_metadata=channel_metadata,
        previous=base_self,
    )
transfer_function(n_fft=2048, hop_length=None, win_length=None, window='hann', detrend='constant', scaling='spectrum', average='mean')

Calculate transfer function matrix.

The transfer function represents the signal transfer characteristics between channels in the frequency domain and represents the input-output relationship of the system.

Parameters:

Name Type Description Default
n_fft int

Number of FFT points. Default is 2048.

2048
hop_length Optional[int]

Number of samples between frames. Default is n_fft//4.

None
win_length Optional[int]

Window length. Default is n_fft.

None
window str

Window type. Default is "hann".

'hann'
detrend str

Detrend method. Options: "constant", "linear", None.

'constant'
scaling str

Scaling method. Options: "spectrum", "density".

'spectrum'
average str

Method for averaging segments. Default is "mean".

'mean'

Returns:

Type Description
SpectralFrame

SpectralFrame containing transfer function matrix

Source code in wandas/frames/mixins/channel_transform_mixin.py
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
def transfer_function(
    self: T_Transform,
    n_fft: int = 2048,
    hop_length: Optional[int] = None,
    win_length: Optional[int] = None,
    window: str = "hann",
    detrend: str = "constant",
    scaling: str = "spectrum",
    average: str = "mean",
) -> "SpectralFrame":
    """Calculate transfer function matrix.

    The transfer function represents the signal transfer characteristics between
    channels in the frequency domain and represents the input-output relationship
    of the system.

    Args:
        n_fft: Number of FFT points. Default is 2048.
        hop_length: Number of samples between frames.
            Default is n_fft//4.
        win_length: Window length. Default is n_fft.
        window: Window type. Default is "hann".
        detrend: Detrend method. Options: "constant", "linear", None.
        scaling: Scaling method. Options: "spectrum", "density".
        average: Method for averaging segments. Default is "mean".

    Returns:
        SpectralFrame containing transfer function matrix
    """
    from wandas.core.metadata import ChannelMetadata
    from wandas.frames.spectral import SpectralFrame
    from wandas.processing import TransferFunction, create_operation

    params = {
        "n_fft": n_fft,
        "hop_length": hop_length,
        "win_length": win_length,
        "window": window,
        "detrend": detrend,
        "scaling": scaling,
        "average": average,
    }
    operation_name = "transfer_function"
    logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

    # Create operation instance
    operation = create_operation(operation_name, self.sampling_rate, **params)
    operation = cast("TransferFunction", operation)

    # Apply processing to data
    tf_data = operation.process(self._data)

    logger.debug(
        f"Created new SpectralFrame with operation {operation_name} added to graph"
    )

    # Cast self as BaseFrame type
    base_self = cast(BaseFrame[Any], self)

    # Create new channel metadata
    channel_metadata = []
    for in_ch in self._channel_metadata:
        for out_ch in self._channel_metadata:
            meta = ChannelMetadata()
            meta.label = f"$H_{{{in_ch.label}, {out_ch.label}}}$"
            meta.unit = ""
            meta.ref = 1
            meta["metadata"] = dict(
                in_ch=in_ch["metadata"], out_ch=out_ch["metadata"]
            )
            channel_metadata.append(meta)

    # Create new instance
    return SpectralFrame(
        data=tf_data,
        sampling_rate=self.sampling_rate,
        n_fft=operation.n_fft,
        window=operation.window,
        label=f"Transfer function of {self.label}",
        metadata={**self.metadata, **params},
        operation_history=[
            *self.operation_history,
            {"operation": operation_name, "params": params},
        ],
        channel_metadata=channel_metadata,
        previous=base_self,
    )
Modules
channel_collection_mixin

ChannelCollectionMixin: Common functionality for adding/removing channels in ChannelFrame

Attributes
T = TypeVar('T', bound='ChannelCollectionMixin') module-attribute
Classes
ChannelCollectionMixin
Source code in wandas/frames/mixins/channel_collection_mixin.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class ChannelCollectionMixin:
    def add_channel(
        self: T,
        data: Union[np.ndarray[Any, Any], da.Array, T],
        label: Optional[str] = None,
        align: Literal["strict", "pad", "truncate"] = "strict",
        suffix_on_dup: Optional[str] = None,
        inplace: bool = False,
        **kwargs: Any,
    ) -> T:
        """
        Add a channel
        Args:
            data: Channel to add (1ch ndarray/dask/ChannelFrame)
            label: Label for the added channel
            align: Behavior when lengths don't match
            suffix_on_dup: Suffix when label is duplicated
            inplace: True for self-modification
        Returns:
            New Frame or self
        Raises:
            ValueError, TypeError
        """
        raise NotImplementedError("add_channel() must be implemented in subclasses")

    def remove_channel(
        self: T,
        key: Union[int, str],
        inplace: bool = False,
    ) -> T:
        """
        Remove a channel
        Args:
            key: Target to remove (index or label)
            inplace: True for self-modification
        Returns:
            New Frame or self
        Raises:
            ValueError, KeyError, IndexError
        """
        raise NotImplementedError("remove_channel() must be implemented in subclasses")
Functions
add_channel(data, label=None, align='strict', suffix_on_dup=None, inplace=False, **kwargs)

Add a channel Args: data: Channel to add (1ch ndarray/dask/ChannelFrame) label: Label for the added channel align: Behavior when lengths don't match suffix_on_dup: Suffix when label is duplicated inplace: True for self-modification Returns: New Frame or self Raises: ValueError, TypeError

Source code in wandas/frames/mixins/channel_collection_mixin.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def add_channel(
    self: T,
    data: Union[np.ndarray[Any, Any], da.Array, T],
    label: Optional[str] = None,
    align: Literal["strict", "pad", "truncate"] = "strict",
    suffix_on_dup: Optional[str] = None,
    inplace: bool = False,
    **kwargs: Any,
) -> T:
    """
    Add a channel
    Args:
        data: Channel to add (1ch ndarray/dask/ChannelFrame)
        label: Label for the added channel
        align: Behavior when lengths don't match
        suffix_on_dup: Suffix when label is duplicated
        inplace: True for self-modification
    Returns:
        New Frame or self
    Raises:
        ValueError, TypeError
    """
    raise NotImplementedError("add_channel() must be implemented in subclasses")
remove_channel(key, inplace=False)

Remove a channel Args: key: Target to remove (index or label) inplace: True for self-modification Returns: New Frame or self Raises: ValueError, KeyError, IndexError

Source code in wandas/frames/mixins/channel_collection_mixin.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def remove_channel(
    self: T,
    key: Union[int, str],
    inplace: bool = False,
) -> T:
    """
    Remove a channel
    Args:
        key: Target to remove (index or label)
        inplace: True for self-modification
    Returns:
        New Frame or self
    Raises:
        ValueError, KeyError, IndexError
    """
    raise NotImplementedError("remove_channel() must be implemented in subclasses")
channel_processing_mixin

Module providing mixins related to signal processing.

Attributes
logger = logging.getLogger(__name__) module-attribute
Classes
ChannelProcessingMixin

Mixin that provides methods related to signal processing.

This mixin provides processing methods applied to audio signals and other time-series data, such as signal processing filters and transformation operations.

Source code in wandas/frames/mixins/channel_processing_mixin.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
class ChannelProcessingMixin:
    """Mixin that provides methods related to signal processing.

    This mixin provides processing methods applied to audio signals and
    other time-series data, such as signal processing filters and
    transformation operations.
    """

    def high_pass_filter(
        self: T_Processing, cutoff: float, order: int = 4
    ) -> T_Processing:
        """Apply a high-pass filter to the signal.

        Args:
            cutoff: Filter cutoff frequency (Hz)
            order: Filter order. Default is 4.

        Returns:
            New ChannelFrame after filter application
        """
        logger.debug(
            f"Setting up highpass filter: cutoff={cutoff}, order={order} (lazy)"
        )
        result = self.apply_operation("highpass_filter", cutoff=cutoff, order=order)
        return cast(T_Processing, result)

    def low_pass_filter(
        self: T_Processing, cutoff: float, order: int = 4
    ) -> T_Processing:
        """Apply a low-pass filter to the signal.

        Args:
            cutoff: Filter cutoff frequency (Hz)
            order: Filter order. Default is 4.

        Returns:
            New ChannelFrame after filter application
        """
        logger.debug(
            f"Setting up lowpass filter: cutoff={cutoff}, order={order} (lazy)"
        )
        result = self.apply_operation("lowpass_filter", cutoff=cutoff, order=order)
        return cast(T_Processing, result)

    def band_pass_filter(
        self: T_Processing, low_cutoff: float, high_cutoff: float, order: int = 4
    ) -> T_Processing:
        """Apply a band-pass filter to the signal.

        Args:
            low_cutoff: Lower cutoff frequency (Hz)
            high_cutoff: Higher cutoff frequency (Hz)
            order: Filter order. Default is 4.

        Returns:
            New ChannelFrame after filter application
        """
        logger.debug(
            f"Setting up bandpass filter: low_cutoff={low_cutoff}, "
            f"high_cutoff={high_cutoff}, order={order} (lazy)"
        )
        result = self.apply_operation(
            "bandpass_filter",
            low_cutoff=low_cutoff,
            high_cutoff=high_cutoff,
            order=order,
        )
        return cast(T_Processing, result)

    def normalize(
        self: T_Processing, target_level: float = -20, channel_wise: bool = True
    ) -> T_Processing:
        """Normalize signal levels.

        This method adjusts the signal amplitude to reach the target RMS level.

        Args:
            target_level: Target RMS level (dB). Default is -20.
            channel_wise: If True, normalize each channel individually.
                If False, apply the same scaling to all channels.

        Returns:
            New ChannelFrame containing the normalized signal
        """
        logger.debug(
            f"Setting up normalize: target_level={target_level}, "
            f"channel_wise={channel_wise} (lazy)"
        )
        result = self.apply_operation(
            "normalize", target_level=target_level, channel_wise=channel_wise
        )
        return cast(T_Processing, result)

    def a_weighting(self: T_Processing) -> T_Processing:
        """Apply A-weighting filter to the signal.

        A-weighting adjusts the frequency response to approximate human
        auditory perception, according to the IEC 61672-1:2013 standard.

        Returns:
            New ChannelFrame containing the A-weighted signal
        """
        result = self.apply_operation("a_weighting")
        return cast(T_Processing, result)

    def abs(self: T_Processing) -> T_Processing:
        """Compute the absolute value of the signal.

        Returns:
            New ChannelFrame containing the absolute values
        """
        result = self.apply_operation("abs")
        return cast(T_Processing, result)

    def power(self: T_Processing, exponent: float = 2.0) -> T_Processing:
        """Compute the power of the signal.

        Args:
            exponent: Exponent to raise the signal to. Default is 2.0.

        Returns:
            New ChannelFrame containing the powered signal
        """
        result = self.apply_operation("power", exponent=exponent)
        return cast(T_Processing, result)

    def _reduce_channels(self: T_Processing, op: str) -> T_Processing:
        """Helper to reduce all channels with the given operation ('sum' or 'mean')."""
        if op == "sum":
            reduced_data = self._data.sum(axis=0, keepdims=True)
            label = "sum"
        elif op == "mean":
            reduced_data = self._data.mean(axis=0, keepdims=True)
            label = "mean"
        else:
            raise ValueError(f"Unsupported reduction operation: {op}")

        units = [ch.unit for ch in self._channel_metadata]
        if all(u == units[0] for u in units):
            reduced_unit = units[0]
        else:
            reduced_unit = ""

        reduced_extra = {"source_extras": [ch.extra for ch in self._channel_metadata]}
        new_channel_metadata = [
            ChannelMetadata(
                label=label,
                unit=reduced_unit,
                extra=reduced_extra,
            )
        ]
        new_history = (
            self.operation_history.copy() if hasattr(self, "operation_history") else []
        )
        new_history.append({"operation": op})
        new_metadata = self.metadata.copy() if hasattr(self, "metadata") else {}
        result = self._create_new_instance(
            data=reduced_data,
            metadata=new_metadata,
            operation_history=new_history,
            channel_metadata=new_channel_metadata,
        )
        return result

    def sum(self: T_Processing) -> T_Processing:
        """Sum all channels.

        Returns:
            A new ChannelFrame with summed signal.
        """
        return cast(T_Processing, cast(Any, self)._reduce_channels("sum"))

    def mean(self: T_Processing) -> T_Processing:
        """Average all channels.

        Returns:
            A new ChannelFrame with averaged signal.
        """
        return cast(T_Processing, cast(Any, self)._reduce_channels("mean"))

    def trim(
        self: T_Processing,
        start: float = 0,
        end: Optional[float] = None,
    ) -> T_Processing:
        """Trim the signal to the specified time range.

        Args:
            start: Start time (seconds)
            end: End time (seconds)

        Returns:
            New ChannelFrame containing the trimmed signal

        Raises:
            ValueError: If end time is earlier than start time
        """
        if end is None:
            end = self.duration
        if start > end:
            raise ValueError("start must be less than end")
        result = self.apply_operation("trim", start=start, end=end)
        return cast(T_Processing, result)

    def fix_length(
        self: T_Processing,
        length: Optional[int] = None,
        duration: Optional[float] = None,
    ) -> T_Processing:
        """Adjust the signal to the specified length.

        Args:
            duration: Signal length in seconds
            length: Signal length in samples

        Returns:
            New ChannelFrame containing the adjusted signal
        """

        result = self.apply_operation("fix_length", length=length, duration=duration)
        return cast(T_Processing, result)

    def rms_trend(
        self: T_Processing,
        frame_length: int = 2048,
        hop_length: int = 512,
        dB: bool = False,  # noqa: N803
        Aw: bool = False,  # noqa: N803
    ) -> T_Processing:
        """Compute the RMS trend of the signal.

        This method calculates the root mean square value over a sliding window.

        Args:
            frame_length: Size of the sliding window in samples. Default is 2048.
            hop_length: Hop length between windows in samples. Default is 512.
            dB: Whether to return RMS values in decibels. Default is False.
            Aw: Whether to apply A-weighting. Default is False.

        Returns:
            New ChannelFrame containing the RMS trend
        """
        # Access _channel_metadata to retrieve reference values
        frame = cast(ProcessingFrameProtocol, self)

        # Ensure _channel_metadata exists before referencing
        ref_values = []
        if hasattr(frame, "_channel_metadata") and frame._channel_metadata:
            ref_values = [ch.ref for ch in frame._channel_metadata]

        result = self.apply_operation(
            "rms_trend",
            frame_length=frame_length,
            hop_length=hop_length,
            ref=ref_values,
            dB=dB,
            Aw=Aw,
        )

        # Update sampling rate
        result_obj = cast(T_Processing, result)
        if hasattr(result_obj, "sampling_rate"):
            result_obj.sampling_rate = frame.sampling_rate / hop_length

        return result_obj

    def channel_difference(
        self: T_Processing, other_channel: Union[int, str] = 0
    ) -> T_Processing:
        """Compute the difference between channels.

        Args:
            other_channel: Index or label of the reference channel. Default is 0.

        Returns:
            New ChannelFrame containing the channel difference
        """
        # label2index is a method of BaseFrame
        if isinstance(other_channel, str):
            if hasattr(self, "label2index"):
                other_channel = self.label2index(other_channel)

        result = self.apply_operation("channel_difference", other_channel=other_channel)
        return cast(T_Processing, result)

    def resampling(
        self: T_Processing,
        target_sr: float,
        **kwargs: Any,
    ) -> T_Processing:
        """Resample audio data.

        Args:
            target_sr: Target sampling rate (Hz)
            **kwargs: Additional resampling parameters

        Returns:
            Resampled ChannelFrame
        """
        return cast(
            T_Processing,
            self.apply_operation(
                "resampling",
                target_sr=target_sr,
                **kwargs,
            ),
        )

    def hpss_harmonic(
        self: T_Processing,
        kernel_size: Union[
            "_IntLike_co", tuple["_IntLike_co", "_IntLike_co"], list["_IntLike_co"]
        ] = 31,
        power: float = 2,
        margin: Union[
            "_FloatLike_co",
            tuple["_FloatLike_co", "_FloatLike_co"],
            list["_FloatLike_co"],
        ] = 1,
        n_fft: int = 2048,
        hop_length: Optional[int] = None,
        win_length: Optional[int] = None,
        window: "_WindowSpec" = "hann",
        center: bool = True,
        pad_mode: "_PadModeSTFT" = "constant",
    ) -> T_Processing:
        """
        Extract harmonic components using HPSS
         (Harmonic-Percussive Source Separation).

        This method separates the harmonic (tonal) components from the signal.

        Args:
            kernel_size: Median filter size for HPSS.
            power: Exponent for the Weiner filter used in HPSS.
            margin: Margin size for the separation.
            n_fft: Size of FFT window.
            hop_length: Hop length for STFT.
            win_length: Window length for STFT.
            window: Window type for STFT.
            center: If True, center the frames.
            pad_mode: Padding mode for STFT.

        Returns:
            A new ChannelFrame containing the harmonic components.
        """
        result = self.apply_operation(
            "hpss_harmonic",
            kernel_size=kernel_size,
            power=power,
            margin=margin,
            n_fft=n_fft,
            hop_length=hop_length,
            win_length=win_length,
            window=window,
            center=center,
            pad_mode=pad_mode,
        )
        return cast(T_Processing, result)

    def hpss_percussive(
        self: T_Processing,
        kernel_size: Union[
            "_IntLike_co", tuple["_IntLike_co", "_IntLike_co"], list["_IntLike_co"]
        ] = 31,
        power: float = 2,
        margin: Union[
            "_FloatLike_co",
            tuple["_FloatLike_co", "_FloatLike_co"],
            list["_FloatLike_co"],
        ] = 1,
        n_fft: int = 2048,
        hop_length: Optional[int] = None,
        win_length: Optional[int] = None,
        window: "_WindowSpec" = "hann",
        center: bool = True,
        pad_mode: "_PadModeSTFT" = "constant",
    ) -> T_Processing:
        """
        Extract percussive components using HPSS
        (Harmonic-Percussive Source Separation).

        This method separates the percussive (tonal) components from the signal.

        Args:
            kernel_size: Median filter size for HPSS.
            power: Exponent for the Weiner filter used in HPSS.
            margin: Margin size for the separation.

        Returns:
            A new ChannelFrame containing the harmonic components.
        """
        result = self.apply_operation(
            "hpss_percussive",
            kernel_size=kernel_size,
            power=power,
            margin=margin,
            n_fft=n_fft,
            hop_length=hop_length,
            win_length=win_length,
            window=window,
            center=center,
            pad_mode=pad_mode,
        )
        return cast(T_Processing, result)
Functions
high_pass_filter(cutoff, order=4)

Apply a high-pass filter to the signal.

Parameters:

Name Type Description Default
cutoff float

Filter cutoff frequency (Hz)

required
order int

Filter order. Default is 4.

4

Returns:

Type Description
T_Processing

New ChannelFrame after filter application

Source code in wandas/frames/mixins/channel_processing_mixin.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def high_pass_filter(
    self: T_Processing, cutoff: float, order: int = 4
) -> T_Processing:
    """Apply a high-pass filter to the signal.

    Args:
        cutoff: Filter cutoff frequency (Hz)
        order: Filter order. Default is 4.

    Returns:
        New ChannelFrame after filter application
    """
    logger.debug(
        f"Setting up highpass filter: cutoff={cutoff}, order={order} (lazy)"
    )
    result = self.apply_operation("highpass_filter", cutoff=cutoff, order=order)
    return cast(T_Processing, result)
low_pass_filter(cutoff, order=4)

Apply a low-pass filter to the signal.

Parameters:

Name Type Description Default
cutoff float

Filter cutoff frequency (Hz)

required
order int

Filter order. Default is 4.

4

Returns:

Type Description
T_Processing

New ChannelFrame after filter application

Source code in wandas/frames/mixins/channel_processing_mixin.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def low_pass_filter(
    self: T_Processing, cutoff: float, order: int = 4
) -> T_Processing:
    """Apply a low-pass filter to the signal.

    Args:
        cutoff: Filter cutoff frequency (Hz)
        order: Filter order. Default is 4.

    Returns:
        New ChannelFrame after filter application
    """
    logger.debug(
        f"Setting up lowpass filter: cutoff={cutoff}, order={order} (lazy)"
    )
    result = self.apply_operation("lowpass_filter", cutoff=cutoff, order=order)
    return cast(T_Processing, result)
band_pass_filter(low_cutoff, high_cutoff, order=4)

Apply a band-pass filter to the signal.

Parameters:

Name Type Description Default
low_cutoff float

Lower cutoff frequency (Hz)

required
high_cutoff float

Higher cutoff frequency (Hz)

required
order int

Filter order. Default is 4.

4

Returns:

Type Description
T_Processing

New ChannelFrame after filter application

Source code in wandas/frames/mixins/channel_processing_mixin.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def band_pass_filter(
    self: T_Processing, low_cutoff: float, high_cutoff: float, order: int = 4
) -> T_Processing:
    """Apply a band-pass filter to the signal.

    Args:
        low_cutoff: Lower cutoff frequency (Hz)
        high_cutoff: Higher cutoff frequency (Hz)
        order: Filter order. Default is 4.

    Returns:
        New ChannelFrame after filter application
    """
    logger.debug(
        f"Setting up bandpass filter: low_cutoff={low_cutoff}, "
        f"high_cutoff={high_cutoff}, order={order} (lazy)"
    )
    result = self.apply_operation(
        "bandpass_filter",
        low_cutoff=low_cutoff,
        high_cutoff=high_cutoff,
        order=order,
    )
    return cast(T_Processing, result)
normalize(target_level=-20, channel_wise=True)

Normalize signal levels.

This method adjusts the signal amplitude to reach the target RMS level.

Parameters:

Name Type Description Default
target_level float

Target RMS level (dB). Default is -20.

-20
channel_wise bool

If True, normalize each channel individually. If False, apply the same scaling to all channels.

True

Returns:

Type Description
T_Processing

New ChannelFrame containing the normalized signal

Source code in wandas/frames/mixins/channel_processing_mixin.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
def normalize(
    self: T_Processing, target_level: float = -20, channel_wise: bool = True
) -> T_Processing:
    """Normalize signal levels.

    This method adjusts the signal amplitude to reach the target RMS level.

    Args:
        target_level: Target RMS level (dB). Default is -20.
        channel_wise: If True, normalize each channel individually.
            If False, apply the same scaling to all channels.

    Returns:
        New ChannelFrame containing the normalized signal
    """
    logger.debug(
        f"Setting up normalize: target_level={target_level}, "
        f"channel_wise={channel_wise} (lazy)"
    )
    result = self.apply_operation(
        "normalize", target_level=target_level, channel_wise=channel_wise
    )
    return cast(T_Processing, result)
a_weighting()

Apply A-weighting filter to the signal.

A-weighting adjusts the frequency response to approximate human auditory perception, according to the IEC 61672-1:2013 standard.

Returns:

Type Description
T_Processing

New ChannelFrame containing the A-weighted signal

Source code in wandas/frames/mixins/channel_processing_mixin.py
113
114
115
116
117
118
119
120
121
122
123
def a_weighting(self: T_Processing) -> T_Processing:
    """Apply A-weighting filter to the signal.

    A-weighting adjusts the frequency response to approximate human
    auditory perception, according to the IEC 61672-1:2013 standard.

    Returns:
        New ChannelFrame containing the A-weighted signal
    """
    result = self.apply_operation("a_weighting")
    return cast(T_Processing, result)
abs()

Compute the absolute value of the signal.

Returns:

Type Description
T_Processing

New ChannelFrame containing the absolute values

Source code in wandas/frames/mixins/channel_processing_mixin.py
125
126
127
128
129
130
131
132
def abs(self: T_Processing) -> T_Processing:
    """Compute the absolute value of the signal.

    Returns:
        New ChannelFrame containing the absolute values
    """
    result = self.apply_operation("abs")
    return cast(T_Processing, result)
power(exponent=2.0)

Compute the power of the signal.

Parameters:

Name Type Description Default
exponent float

Exponent to raise the signal to. Default is 2.0.

2.0

Returns:

Type Description
T_Processing

New ChannelFrame containing the powered signal

Source code in wandas/frames/mixins/channel_processing_mixin.py
134
135
136
137
138
139
140
141
142
143
144
def power(self: T_Processing, exponent: float = 2.0) -> T_Processing:
    """Compute the power of the signal.

    Args:
        exponent: Exponent to raise the signal to. Default is 2.0.

    Returns:
        New ChannelFrame containing the powered signal
    """
    result = self.apply_operation("power", exponent=exponent)
    return cast(T_Processing, result)
sum()

Sum all channels.

Returns:

Type Description
T_Processing

A new ChannelFrame with summed signal.

Source code in wandas/frames/mixins/channel_processing_mixin.py
184
185
186
187
188
189
190
def sum(self: T_Processing) -> T_Processing:
    """Sum all channels.

    Returns:
        A new ChannelFrame with summed signal.
    """
    return cast(T_Processing, cast(Any, self)._reduce_channels("sum"))
mean()

Average all channels.

Returns:

Type Description
T_Processing

A new ChannelFrame with averaged signal.

Source code in wandas/frames/mixins/channel_processing_mixin.py
192
193
194
195
196
197
198
def mean(self: T_Processing) -> T_Processing:
    """Average all channels.

    Returns:
        A new ChannelFrame with averaged signal.
    """
    return cast(T_Processing, cast(Any, self)._reduce_channels("mean"))
trim(start=0, end=None)

Trim the signal to the specified time range.

Parameters:

Name Type Description Default
start float

Start time (seconds)

0
end Optional[float]

End time (seconds)

None

Returns:

Type Description
T_Processing

New ChannelFrame containing the trimmed signal

Raises:

Type Description
ValueError

If end time is earlier than start time

Source code in wandas/frames/mixins/channel_processing_mixin.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
def trim(
    self: T_Processing,
    start: float = 0,
    end: Optional[float] = None,
) -> T_Processing:
    """Trim the signal to the specified time range.

    Args:
        start: Start time (seconds)
        end: End time (seconds)

    Returns:
        New ChannelFrame containing the trimmed signal

    Raises:
        ValueError: If end time is earlier than start time
    """
    if end is None:
        end = self.duration
    if start > end:
        raise ValueError("start must be less than end")
    result = self.apply_operation("trim", start=start, end=end)
    return cast(T_Processing, result)
fix_length(length=None, duration=None)

Adjust the signal to the specified length.

Parameters:

Name Type Description Default
duration Optional[float]

Signal length in seconds

None
length Optional[int]

Signal length in samples

None

Returns:

Type Description
T_Processing

New ChannelFrame containing the adjusted signal

Source code in wandas/frames/mixins/channel_processing_mixin.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
def fix_length(
    self: T_Processing,
    length: Optional[int] = None,
    duration: Optional[float] = None,
) -> T_Processing:
    """Adjust the signal to the specified length.

    Args:
        duration: Signal length in seconds
        length: Signal length in samples

    Returns:
        New ChannelFrame containing the adjusted signal
    """

    result = self.apply_operation("fix_length", length=length, duration=duration)
    return cast(T_Processing, result)
rms_trend(frame_length=2048, hop_length=512, dB=False, Aw=False)

Compute the RMS trend of the signal.

This method calculates the root mean square value over a sliding window.

Parameters:

Name Type Description Default
frame_length int

Size of the sliding window in samples. Default is 2048.

2048
hop_length int

Hop length between windows in samples. Default is 512.

512
dB bool

Whether to return RMS values in decibels. Default is False.

False
Aw bool

Whether to apply A-weighting. Default is False.

False

Returns:

Type Description
T_Processing

New ChannelFrame containing the RMS trend

Source code in wandas/frames/mixins/channel_processing_mixin.py
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def rms_trend(
    self: T_Processing,
    frame_length: int = 2048,
    hop_length: int = 512,
    dB: bool = False,  # noqa: N803
    Aw: bool = False,  # noqa: N803
) -> T_Processing:
    """Compute the RMS trend of the signal.

    This method calculates the root mean square value over a sliding window.

    Args:
        frame_length: Size of the sliding window in samples. Default is 2048.
        hop_length: Hop length between windows in samples. Default is 512.
        dB: Whether to return RMS values in decibels. Default is False.
        Aw: Whether to apply A-weighting. Default is False.

    Returns:
        New ChannelFrame containing the RMS trend
    """
    # Access _channel_metadata to retrieve reference values
    frame = cast(ProcessingFrameProtocol, self)

    # Ensure _channel_metadata exists before referencing
    ref_values = []
    if hasattr(frame, "_channel_metadata") and frame._channel_metadata:
        ref_values = [ch.ref for ch in frame._channel_metadata]

    result = self.apply_operation(
        "rms_trend",
        frame_length=frame_length,
        hop_length=hop_length,
        ref=ref_values,
        dB=dB,
        Aw=Aw,
    )

    # Update sampling rate
    result_obj = cast(T_Processing, result)
    if hasattr(result_obj, "sampling_rate"):
        result_obj.sampling_rate = frame.sampling_rate / hop_length

    return result_obj
channel_difference(other_channel=0)

Compute the difference between channels.

Parameters:

Name Type Description Default
other_channel Union[int, str]

Index or label of the reference channel. Default is 0.

0

Returns:

Type Description
T_Processing

New ChannelFrame containing the channel difference

Source code in wandas/frames/mixins/channel_processing_mixin.py
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
def channel_difference(
    self: T_Processing, other_channel: Union[int, str] = 0
) -> T_Processing:
    """Compute the difference between channels.

    Args:
        other_channel: Index or label of the reference channel. Default is 0.

    Returns:
        New ChannelFrame containing the channel difference
    """
    # label2index is a method of BaseFrame
    if isinstance(other_channel, str):
        if hasattr(self, "label2index"):
            other_channel = self.label2index(other_channel)

    result = self.apply_operation("channel_difference", other_channel=other_channel)
    return cast(T_Processing, result)
resampling(target_sr, **kwargs)

Resample audio data.

Parameters:

Name Type Description Default
target_sr float

Target sampling rate (Hz)

required
**kwargs Any

Additional resampling parameters

{}

Returns:

Type Description
T_Processing

Resampled ChannelFrame

Source code in wandas/frames/mixins/channel_processing_mixin.py
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
def resampling(
    self: T_Processing,
    target_sr: float,
    **kwargs: Any,
) -> T_Processing:
    """Resample audio data.

    Args:
        target_sr: Target sampling rate (Hz)
        **kwargs: Additional resampling parameters

    Returns:
        Resampled ChannelFrame
    """
    return cast(
        T_Processing,
        self.apply_operation(
            "resampling",
            target_sr=target_sr,
            **kwargs,
        ),
    )
hpss_harmonic(kernel_size=31, power=2, margin=1, n_fft=2048, hop_length=None, win_length=None, window='hann', center=True, pad_mode='constant')

Extract harmonic components using HPSS (Harmonic-Percussive Source Separation).

This method separates the harmonic (tonal) components from the signal.

Parameters:

Name Type Description Default
kernel_size Union[_IntLike_co, tuple[_IntLike_co, _IntLike_co], list[_IntLike_co]]

Median filter size for HPSS.

31
power float

Exponent for the Weiner filter used in HPSS.

2
margin Union[_FloatLike_co, tuple[_FloatLike_co, _FloatLike_co], list[_FloatLike_co]]

Margin size for the separation.

1
n_fft int

Size of FFT window.

2048
hop_length Optional[int]

Hop length for STFT.

None
win_length Optional[int]

Window length for STFT.

None
window _WindowSpec

Window type for STFT.

'hann'
center bool

If True, center the frames.

True
pad_mode _PadModeSTFT

Padding mode for STFT.

'constant'

Returns:

Type Description
T_Processing

A new ChannelFrame containing the harmonic components.

Source code in wandas/frames/mixins/channel_processing_mixin.py
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
def hpss_harmonic(
    self: T_Processing,
    kernel_size: Union[
        "_IntLike_co", tuple["_IntLike_co", "_IntLike_co"], list["_IntLike_co"]
    ] = 31,
    power: float = 2,
    margin: Union[
        "_FloatLike_co",
        tuple["_FloatLike_co", "_FloatLike_co"],
        list["_FloatLike_co"],
    ] = 1,
    n_fft: int = 2048,
    hop_length: Optional[int] = None,
    win_length: Optional[int] = None,
    window: "_WindowSpec" = "hann",
    center: bool = True,
    pad_mode: "_PadModeSTFT" = "constant",
) -> T_Processing:
    """
    Extract harmonic components using HPSS
     (Harmonic-Percussive Source Separation).

    This method separates the harmonic (tonal) components from the signal.

    Args:
        kernel_size: Median filter size for HPSS.
        power: Exponent for the Weiner filter used in HPSS.
        margin: Margin size for the separation.
        n_fft: Size of FFT window.
        hop_length: Hop length for STFT.
        win_length: Window length for STFT.
        window: Window type for STFT.
        center: If True, center the frames.
        pad_mode: Padding mode for STFT.

    Returns:
        A new ChannelFrame containing the harmonic components.
    """
    result = self.apply_operation(
        "hpss_harmonic",
        kernel_size=kernel_size,
        power=power,
        margin=margin,
        n_fft=n_fft,
        hop_length=hop_length,
        win_length=win_length,
        window=window,
        center=center,
        pad_mode=pad_mode,
    )
    return cast(T_Processing, result)
hpss_percussive(kernel_size=31, power=2, margin=1, n_fft=2048, hop_length=None, win_length=None, window='hann', center=True, pad_mode='constant')

Extract percussive components using HPSS (Harmonic-Percussive Source Separation).

This method separates the percussive (tonal) components from the signal.

Parameters:

Name Type Description Default
kernel_size Union[_IntLike_co, tuple[_IntLike_co, _IntLike_co], list[_IntLike_co]]

Median filter size for HPSS.

31
power float

Exponent for the Weiner filter used in HPSS.

2
margin Union[_FloatLike_co, tuple[_FloatLike_co, _FloatLike_co], list[_FloatLike_co]]

Margin size for the separation.

1

Returns:

Type Description
T_Processing

A new ChannelFrame containing the harmonic components.

Source code in wandas/frames/mixins/channel_processing_mixin.py
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
def hpss_percussive(
    self: T_Processing,
    kernel_size: Union[
        "_IntLike_co", tuple["_IntLike_co", "_IntLike_co"], list["_IntLike_co"]
    ] = 31,
    power: float = 2,
    margin: Union[
        "_FloatLike_co",
        tuple["_FloatLike_co", "_FloatLike_co"],
        list["_FloatLike_co"],
    ] = 1,
    n_fft: int = 2048,
    hop_length: Optional[int] = None,
    win_length: Optional[int] = None,
    window: "_WindowSpec" = "hann",
    center: bool = True,
    pad_mode: "_PadModeSTFT" = "constant",
) -> T_Processing:
    """
    Extract percussive components using HPSS
    (Harmonic-Percussive Source Separation).

    This method separates the percussive (tonal) components from the signal.

    Args:
        kernel_size: Median filter size for HPSS.
        power: Exponent for the Weiner filter used in HPSS.
        margin: Margin size for the separation.

    Returns:
        A new ChannelFrame containing the harmonic components.
    """
    result = self.apply_operation(
        "hpss_percussive",
        kernel_size=kernel_size,
        power=power,
        margin=margin,
        n_fft=n_fft,
        hop_length=hop_length,
        win_length=win_length,
        window=window,
        center=center,
        pad_mode=pad_mode,
    )
    return cast(T_Processing, result)
channel_transform_mixin

Module providing mixins related to frequency transformations and transform operations.

Attributes
logger = logging.getLogger(__name__) module-attribute
Classes
ChannelTransformMixin

Mixin providing methods related to frequency transformations.

This mixin provides operations related to frequency analysis and transformations such as FFT, STFT, and Welch method.

Source code in wandas/frames/mixins/channel_transform_mixin.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
class ChannelTransformMixin:
    """Mixin providing methods related to frequency transformations.

    This mixin provides operations related to frequency analysis and
    transformations such as FFT, STFT, and Welch method.
    """

    def fft(
        self: T_Transform, n_fft: Optional[int] = None, window: str = "hann"
    ) -> "SpectralFrame":
        """Calculate Fast Fourier Transform (FFT).

        Args:
            n_fft: Number of FFT points. Default is the next power of 2 of the data
                length.
            window: Window type. Default is "hann".

        Returns:
            SpectralFrame containing FFT results
        """
        from wandas.frames.spectral import SpectralFrame
        from wandas.processing import FFT, create_operation

        params = {"n_fft": n_fft, "window": window}
        operation_name = "fft"
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

        # Create operation instance
        operation = create_operation(operation_name, self.sampling_rate, **params)
        operation = cast("FFT", operation)
        # Apply processing to data
        spectrum_data = operation.process(self._data)

        logger.debug(
            f"Created new SpectralFrame with operation {operation_name} added to graph"
        )

        if n_fft is None:
            is_even = spectrum_data.shape[-1] % 2 == 0
            _n_fft = (
                spectrum_data.shape[-1] * 2 - 2
                if is_even
                else spectrum_data.shape[-1] * 2 - 1
            )
        else:
            _n_fft = n_fft

        # Cast self as BaseFrame type
        base_self = cast(BaseFrame[Any], self)

        return SpectralFrame(
            data=spectrum_data,
            sampling_rate=self.sampling_rate,
            n_fft=_n_fft,
            window=operation.window,
            label=f"Spectrum of {self.label}",
            metadata={**self.metadata, "window": window, "n_fft": _n_fft},
            operation_history=[
                *self.operation_history,
                {"operation": "fft", "params": {"n_fft": _n_fft, "window": window}},
            ],
            channel_metadata=self._channel_metadata,
            previous=base_self,
        )

    def welch(
        self: T_Transform,
        n_fft: Optional[int] = None,
        hop_length: Optional[int] = None,
        win_length: int = 2048,
        window: str = "hann",
        average: str = "mean",
    ) -> "SpectralFrame":
        """Calculate power spectral density using Welch's method.

        Args:
            n_fft: Number of FFT points. Default is 2048.
            hop_length: Number of samples between frames.
                Default is n_fft//4.
            win_length: Window length. Default is n_fft.
            window: Window type. Default is "hann".
            average: Method for averaging segments. Default is "mean".

        Returns:
            SpectralFrame containing power spectral density
        """
        from wandas.frames.spectral import SpectralFrame
        from wandas.processing import Welch, create_operation

        params = dict(
            n_fft=n_fft or win_length,
            hop_length=hop_length,
            win_length=win_length,
            window=window,
            average=average,
        )
        operation_name = "welch"
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

        # Create operation instance
        operation = create_operation(operation_name, self.sampling_rate, **params)
        operation = cast("Welch", operation)
        # Apply processing to data
        spectrum_data = operation.process(self._data)

        logger.debug(
            f"Created new SpectralFrame with operation {operation_name} added to graph"
        )

        # Cast self as BaseFrame type
        base_self = cast(BaseFrame[Any], self)

        return SpectralFrame(
            data=spectrum_data,
            sampling_rate=self.sampling_rate,
            n_fft=operation.n_fft,
            window=operation.window,
            label=f"Spectrum of {self.label}",
            metadata={**self.metadata, **params},
            operation_history=[
                *self.operation_history,
                {"operation": "welch", "params": params},
            ],
            channel_metadata=self._channel_metadata,
            previous=base_self,
        )

    def noct_spectrum(
        self: T_Transform,
        fmin: float,
        fmax: float,
        n: int = 3,
        G: int = 10,  # noqa: N803
        fr: int = 1000,
    ) -> "NOctFrame":
        """Calculate N-octave band spectrum.

        Args:
            fmin: Minimum center frequency (Hz). Default is 20 Hz.
            fmax: Maximum center frequency (Hz). Default is 20000 Hz.
            n: Band division (1: octave, 3: 1/3 octave). Default is 3.
            G: Reference gain (dB). Default is 10 dB.
            fr: Reference frequency (Hz). Default is 1000 Hz.

        Returns:
            NOctFrame containing N-octave band spectrum
        """
        from wandas.processing import NOctSpectrum, create_operation

        from ..noct import NOctFrame

        params = {"fmin": fmin, "fmax": fmax, "n": n, "G": G, "fr": fr}
        operation_name = "noct_spectrum"
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

        # Create operation instance
        operation = create_operation(operation_name, self.sampling_rate, **params)
        operation = cast("NOctSpectrum", operation)
        # Apply processing to data
        spectrum_data = operation.process(self._data)

        logger.debug(
            f"Created new SpectralFrame with operation {operation_name} added to graph"
        )

        # Cast self as BaseFrame type
        base_self = cast(BaseFrame[Any], self)

        return NOctFrame(
            data=spectrum_data,
            sampling_rate=self.sampling_rate,
            fmin=fmin,
            fmax=fmax,
            n=n,
            G=G,
            fr=fr,
            label=f"1/{n}Oct of {self.label}",
            metadata={**self.metadata, **params},
            operation_history=[
                *self.operation_history,
                {
                    "operation": "noct_spectrum",
                    "params": params,
                },
            ],
            channel_metadata=self._channel_metadata,
            previous=base_self,
        )

    def stft(
        self: T_Transform,
        n_fft: int = 2048,
        hop_length: Optional[int] = None,
        win_length: Optional[int] = None,
        window: str = "hann",
    ) -> "SpectrogramFrame":
        """Calculate Short-Time Fourier Transform.

        Args:
            n_fft: Number of FFT points. Default is 2048.
            hop_length: Number of samples between frames.
                Default is n_fft//4.
            win_length: Window length. Default is n_fft.
            window: Window type. Default is "hann".

        Returns:
            SpectrogramFrame containing STFT results
        """
        from wandas.processing import STFT, create_operation

        from ..spectrogram import SpectrogramFrame

        # Set hop length and window length
        _hop_length = hop_length if hop_length is not None else n_fft // 4
        _win_length = win_length if win_length is not None else n_fft

        params = {
            "n_fft": n_fft,
            "hop_length": _hop_length,
            "win_length": _win_length,
            "window": window,
        }
        operation_name = "stft"
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

        # Create operation instance
        operation = create_operation(operation_name, self.sampling_rate, **params)
        operation = cast("STFT", operation)

        # Apply processing to data
        spectrogram_data = operation.process(self._data)

        logger.debug(
            f"Created new SpectrogramFrame with operation {operation_name} added to graph"  # noqa: E501
        )

        # Cast self as BaseFrame type
        base_self = cast(BaseFrame[Any], self)

        # Create new instance
        return SpectrogramFrame(
            data=spectrogram_data,
            sampling_rate=self.sampling_rate,
            n_fft=n_fft,
            hop_length=_hop_length,
            win_length=_win_length,
            window=window,
            label=f"stft({self.label})",
            metadata=self.metadata,
            operation_history=self.operation_history,
            channel_metadata=self._channel_metadata,
            previous=base_self,
        )

    def coherence(
        self: T_Transform,
        n_fft: int = 2048,
        hop_length: Optional[int] = None,
        win_length: Optional[int] = None,
        window: str = "hann",
        detrend: str = "constant",
    ) -> "SpectralFrame":
        """Calculate magnitude squared coherence.

        Args:
            n_fft: Number of FFT points. Default is 2048.
            hop_length: Number of samples between frames.
                Default is n_fft//4.
            win_length: Window length. Default is n_fft.
            window: Window type. Default is "hann".
            detrend: Detrend method. Options: "constant", "linear", None.

        Returns:
            SpectralFrame containing magnitude squared coherence
        """
        from wandas.core.metadata import ChannelMetadata
        from wandas.processing import Coherence, create_operation

        from ..spectral import SpectralFrame

        params = {
            "n_fft": n_fft,
            "hop_length": hop_length,
            "win_length": win_length,
            "window": window,
            "detrend": detrend,
        }
        operation_name = "coherence"
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

        # Create operation instance
        operation = create_operation(operation_name, self.sampling_rate, **params)
        operation = cast("Coherence", operation)

        # Apply processing to data
        coherence_data = operation.process(self._data)

        logger.debug(
            f"Created new SpectralFrame with operation {operation_name} added to graph"
        )

        # Cast self as BaseFrame type
        base_self = cast(BaseFrame[Any], self)

        # Create new channel metadata
        channel_metadata = []
        for in_ch in self._channel_metadata:
            for out_ch in self._channel_metadata:
                meta = ChannelMetadata()
                meta.label = f"$\\gamma_{{{in_ch.label}, {out_ch.label}}}$"
                meta.unit = ""
                meta.ref = 1
                meta["metadata"] = dict(
                    in_ch=in_ch["metadata"], out_ch=out_ch["metadata"]
                )
                channel_metadata.append(meta)

        # Create new instance
        return SpectralFrame(
            data=coherence_data,
            sampling_rate=self.sampling_rate,
            n_fft=operation.n_fft,
            window=operation.window,
            label=f"Coherence of {self.label}",
            metadata={**self.metadata, **params},
            operation_history=[
                *self.operation_history,
                {"operation": operation_name, "params": params},
            ],
            channel_metadata=channel_metadata,
            previous=base_self,
        )

    def csd(
        self: T_Transform,
        n_fft: int = 2048,
        hop_length: Optional[int] = None,
        win_length: Optional[int] = None,
        window: str = "hann",
        detrend: str = "constant",
        scaling: str = "spectrum",
        average: str = "mean",
    ) -> "SpectralFrame":
        """Calculate cross-spectral density matrix.

        Args:
            n_fft: Number of FFT points. Default is 2048.
            hop_length: Number of samples between frames.
                Default is n_fft//4.
            win_length: Window length. Default is n_fft.
            window: Window type. Default is "hann".
            detrend: Detrend method. Options: "constant", "linear", None.
            scaling: Scaling method. Options: "spectrum", "density".
            average: Method for averaging segments. Default is "mean".

        Returns:
            SpectralFrame containing cross-spectral density matrix
        """
        from wandas.core.metadata import ChannelMetadata
        from wandas.frames.spectral import SpectralFrame
        from wandas.processing import CSD, create_operation

        params = {
            "n_fft": n_fft,
            "hop_length": hop_length,
            "win_length": win_length,
            "window": window,
            "detrend": detrend,
            "scaling": scaling,
            "average": average,
        }
        operation_name = "csd"
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

        # Create operation instance
        operation = create_operation(operation_name, self.sampling_rate, **params)
        operation = cast("CSD", operation)

        # Apply processing to data
        csd_data = operation.process(self._data)

        logger.debug(
            f"Created new SpectralFrame with operation {operation_name} added to graph"
        )

        # Cast self as BaseFrame type
        base_self = cast(BaseFrame[Any], self)

        # Create new channel metadata
        channel_metadata = []
        for in_ch in self._channel_metadata:
            for out_ch in self._channel_metadata:
                meta = ChannelMetadata()
                meta.label = f"{operation_name}({in_ch.label}, {out_ch.label})"
                meta.unit = ""
                meta.ref = 1
                meta["metadata"] = dict(
                    in_ch=in_ch["metadata"], out_ch=out_ch["metadata"]
                )
                channel_metadata.append(meta)

        # Create new instance
        return SpectralFrame(
            data=csd_data,
            sampling_rate=self.sampling_rate,
            n_fft=operation.n_fft,
            window=operation.window,
            label=f"$C_{{{in_ch.label}, {out_ch.label}}}$",
            metadata={**self.metadata, **params},
            operation_history=[
                *self.operation_history,
                {"operation": operation_name, "params": params},
            ],
            channel_metadata=channel_metadata,
            previous=base_self,
        )

    def transfer_function(
        self: T_Transform,
        n_fft: int = 2048,
        hop_length: Optional[int] = None,
        win_length: Optional[int] = None,
        window: str = "hann",
        detrend: str = "constant",
        scaling: str = "spectrum",
        average: str = "mean",
    ) -> "SpectralFrame":
        """Calculate transfer function matrix.

        The transfer function represents the signal transfer characteristics between
        channels in the frequency domain and represents the input-output relationship
        of the system.

        Args:
            n_fft: Number of FFT points. Default is 2048.
            hop_length: Number of samples between frames.
                Default is n_fft//4.
            win_length: Window length. Default is n_fft.
            window: Window type. Default is "hann".
            detrend: Detrend method. Options: "constant", "linear", None.
            scaling: Scaling method. Options: "spectrum", "density".
            average: Method for averaging segments. Default is "mean".

        Returns:
            SpectralFrame containing transfer function matrix
        """
        from wandas.core.metadata import ChannelMetadata
        from wandas.frames.spectral import SpectralFrame
        from wandas.processing import TransferFunction, create_operation

        params = {
            "n_fft": n_fft,
            "hop_length": hop_length,
            "win_length": win_length,
            "window": window,
            "detrend": detrend,
            "scaling": scaling,
            "average": average,
        }
        operation_name = "transfer_function"
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

        # Create operation instance
        operation = create_operation(operation_name, self.sampling_rate, **params)
        operation = cast("TransferFunction", operation)

        # Apply processing to data
        tf_data = operation.process(self._data)

        logger.debug(
            f"Created new SpectralFrame with operation {operation_name} added to graph"
        )

        # Cast self as BaseFrame type
        base_self = cast(BaseFrame[Any], self)

        # Create new channel metadata
        channel_metadata = []
        for in_ch in self._channel_metadata:
            for out_ch in self._channel_metadata:
                meta = ChannelMetadata()
                meta.label = f"$H_{{{in_ch.label}, {out_ch.label}}}$"
                meta.unit = ""
                meta.ref = 1
                meta["metadata"] = dict(
                    in_ch=in_ch["metadata"], out_ch=out_ch["metadata"]
                )
                channel_metadata.append(meta)

        # Create new instance
        return SpectralFrame(
            data=tf_data,
            sampling_rate=self.sampling_rate,
            n_fft=operation.n_fft,
            window=operation.window,
            label=f"Transfer function of {self.label}",
            metadata={**self.metadata, **params},
            operation_history=[
                *self.operation_history,
                {"operation": operation_name, "params": params},
            ],
            channel_metadata=channel_metadata,
            previous=base_self,
        )
Functions
fft(n_fft=None, window='hann')

Calculate Fast Fourier Transform (FFT).

Parameters:

Name Type Description Default
n_fft Optional[int]

Number of FFT points. Default is the next power of 2 of the data length.

None
window str

Window type. Default is "hann".

'hann'

Returns:

Type Description
SpectralFrame

SpectralFrame containing FFT results

Source code in wandas/frames/mixins/channel_transform_mixin.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def fft(
    self: T_Transform, n_fft: Optional[int] = None, window: str = "hann"
) -> "SpectralFrame":
    """Calculate Fast Fourier Transform (FFT).

    Args:
        n_fft: Number of FFT points. Default is the next power of 2 of the data
            length.
        window: Window type. Default is "hann".

    Returns:
        SpectralFrame containing FFT results
    """
    from wandas.frames.spectral import SpectralFrame
    from wandas.processing import FFT, create_operation

    params = {"n_fft": n_fft, "window": window}
    operation_name = "fft"
    logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

    # Create operation instance
    operation = create_operation(operation_name, self.sampling_rate, **params)
    operation = cast("FFT", operation)
    # Apply processing to data
    spectrum_data = operation.process(self._data)

    logger.debug(
        f"Created new SpectralFrame with operation {operation_name} added to graph"
    )

    if n_fft is None:
        is_even = spectrum_data.shape[-1] % 2 == 0
        _n_fft = (
            spectrum_data.shape[-1] * 2 - 2
            if is_even
            else spectrum_data.shape[-1] * 2 - 1
        )
    else:
        _n_fft = n_fft

    # Cast self as BaseFrame type
    base_self = cast(BaseFrame[Any], self)

    return SpectralFrame(
        data=spectrum_data,
        sampling_rate=self.sampling_rate,
        n_fft=_n_fft,
        window=operation.window,
        label=f"Spectrum of {self.label}",
        metadata={**self.metadata, "window": window, "n_fft": _n_fft},
        operation_history=[
            *self.operation_history,
            {"operation": "fft", "params": {"n_fft": _n_fft, "window": window}},
        ],
        channel_metadata=self._channel_metadata,
        previous=base_self,
    )
welch(n_fft=None, hop_length=None, win_length=2048, window='hann', average='mean')

Calculate power spectral density using Welch's method.

Parameters:

Name Type Description Default
n_fft Optional[int]

Number of FFT points. Default is 2048.

None
hop_length Optional[int]

Number of samples between frames. Default is n_fft//4.

None
win_length int

Window length. Default is n_fft.

2048
window str

Window type. Default is "hann".

'hann'
average str

Method for averaging segments. Default is "mean".

'mean'

Returns:

Type Description
SpectralFrame

SpectralFrame containing power spectral density

Source code in wandas/frames/mixins/channel_transform_mixin.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def welch(
    self: T_Transform,
    n_fft: Optional[int] = None,
    hop_length: Optional[int] = None,
    win_length: int = 2048,
    window: str = "hann",
    average: str = "mean",
) -> "SpectralFrame":
    """Calculate power spectral density using Welch's method.

    Args:
        n_fft: Number of FFT points. Default is 2048.
        hop_length: Number of samples between frames.
            Default is n_fft//4.
        win_length: Window length. Default is n_fft.
        window: Window type. Default is "hann".
        average: Method for averaging segments. Default is "mean".

    Returns:
        SpectralFrame containing power spectral density
    """
    from wandas.frames.spectral import SpectralFrame
    from wandas.processing import Welch, create_operation

    params = dict(
        n_fft=n_fft or win_length,
        hop_length=hop_length,
        win_length=win_length,
        window=window,
        average=average,
    )
    operation_name = "welch"
    logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

    # Create operation instance
    operation = create_operation(operation_name, self.sampling_rate, **params)
    operation = cast("Welch", operation)
    # Apply processing to data
    spectrum_data = operation.process(self._data)

    logger.debug(
        f"Created new SpectralFrame with operation {operation_name} added to graph"
    )

    # Cast self as BaseFrame type
    base_self = cast(BaseFrame[Any], self)

    return SpectralFrame(
        data=spectrum_data,
        sampling_rate=self.sampling_rate,
        n_fft=operation.n_fft,
        window=operation.window,
        label=f"Spectrum of {self.label}",
        metadata={**self.metadata, **params},
        operation_history=[
            *self.operation_history,
            {"operation": "welch", "params": params},
        ],
        channel_metadata=self._channel_metadata,
        previous=base_self,
    )
noct_spectrum(fmin, fmax, n=3, G=10, fr=1000)

Calculate N-octave band spectrum.

Parameters:

Name Type Description Default
fmin float

Minimum center frequency (Hz). Default is 20 Hz.

required
fmax float

Maximum center frequency (Hz). Default is 20000 Hz.

required
n int

Band division (1: octave, 3: 1/3 octave). Default is 3.

3
G int

Reference gain (dB). Default is 10 dB.

10
fr int

Reference frequency (Hz). Default is 1000 Hz.

1000

Returns:

Type Description
NOctFrame

NOctFrame containing N-octave band spectrum

Source code in wandas/frames/mixins/channel_transform_mixin.py
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def noct_spectrum(
    self: T_Transform,
    fmin: float,
    fmax: float,
    n: int = 3,
    G: int = 10,  # noqa: N803
    fr: int = 1000,
) -> "NOctFrame":
    """Calculate N-octave band spectrum.

    Args:
        fmin: Minimum center frequency (Hz). Default is 20 Hz.
        fmax: Maximum center frequency (Hz). Default is 20000 Hz.
        n: Band division (1: octave, 3: 1/3 octave). Default is 3.
        G: Reference gain (dB). Default is 10 dB.
        fr: Reference frequency (Hz). Default is 1000 Hz.

    Returns:
        NOctFrame containing N-octave band spectrum
    """
    from wandas.processing import NOctSpectrum, create_operation

    from ..noct import NOctFrame

    params = {"fmin": fmin, "fmax": fmax, "n": n, "G": G, "fr": fr}
    operation_name = "noct_spectrum"
    logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

    # Create operation instance
    operation = create_operation(operation_name, self.sampling_rate, **params)
    operation = cast("NOctSpectrum", operation)
    # Apply processing to data
    spectrum_data = operation.process(self._data)

    logger.debug(
        f"Created new SpectralFrame with operation {operation_name} added to graph"
    )

    # Cast self as BaseFrame type
    base_self = cast(BaseFrame[Any], self)

    return NOctFrame(
        data=spectrum_data,
        sampling_rate=self.sampling_rate,
        fmin=fmin,
        fmax=fmax,
        n=n,
        G=G,
        fr=fr,
        label=f"1/{n}Oct of {self.label}",
        metadata={**self.metadata, **params},
        operation_history=[
            *self.operation_history,
            {
                "operation": "noct_spectrum",
                "params": params,
            },
        ],
        channel_metadata=self._channel_metadata,
        previous=base_self,
    )
stft(n_fft=2048, hop_length=None, win_length=None, window='hann')

Calculate Short-Time Fourier Transform.

Parameters:

Name Type Description Default
n_fft int

Number of FFT points. Default is 2048.

2048
hop_length Optional[int]

Number of samples between frames. Default is n_fft//4.

None
win_length Optional[int]

Window length. Default is n_fft.

None
window str

Window type. Default is "hann".

'hann'

Returns:

Type Description
SpectrogramFrame

SpectrogramFrame containing STFT results

Source code in wandas/frames/mixins/channel_transform_mixin.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
def stft(
    self: T_Transform,
    n_fft: int = 2048,
    hop_length: Optional[int] = None,
    win_length: Optional[int] = None,
    window: str = "hann",
) -> "SpectrogramFrame":
    """Calculate Short-Time Fourier Transform.

    Args:
        n_fft: Number of FFT points. Default is 2048.
        hop_length: Number of samples between frames.
            Default is n_fft//4.
        win_length: Window length. Default is n_fft.
        window: Window type. Default is "hann".

    Returns:
        SpectrogramFrame containing STFT results
    """
    from wandas.processing import STFT, create_operation

    from ..spectrogram import SpectrogramFrame

    # Set hop length and window length
    _hop_length = hop_length if hop_length is not None else n_fft // 4
    _win_length = win_length if win_length is not None else n_fft

    params = {
        "n_fft": n_fft,
        "hop_length": _hop_length,
        "win_length": _win_length,
        "window": window,
    }
    operation_name = "stft"
    logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

    # Create operation instance
    operation = create_operation(operation_name, self.sampling_rate, **params)
    operation = cast("STFT", operation)

    # Apply processing to data
    spectrogram_data = operation.process(self._data)

    logger.debug(
        f"Created new SpectrogramFrame with operation {operation_name} added to graph"  # noqa: E501
    )

    # Cast self as BaseFrame type
    base_self = cast(BaseFrame[Any], self)

    # Create new instance
    return SpectrogramFrame(
        data=spectrogram_data,
        sampling_rate=self.sampling_rate,
        n_fft=n_fft,
        hop_length=_hop_length,
        win_length=_win_length,
        window=window,
        label=f"stft({self.label})",
        metadata=self.metadata,
        operation_history=self.operation_history,
        channel_metadata=self._channel_metadata,
        previous=base_self,
    )
coherence(n_fft=2048, hop_length=None, win_length=None, window='hann', detrend='constant')

Calculate magnitude squared coherence.

Parameters:

Name Type Description Default
n_fft int

Number of FFT points. Default is 2048.

2048
hop_length Optional[int]

Number of samples between frames. Default is n_fft//4.

None
win_length Optional[int]

Window length. Default is n_fft.

None
window str

Window type. Default is "hann".

'hann'
detrend str

Detrend method. Options: "constant", "linear", None.

'constant'

Returns:

Type Description
SpectralFrame

SpectralFrame containing magnitude squared coherence

Source code in wandas/frames/mixins/channel_transform_mixin.py
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
def coherence(
    self: T_Transform,
    n_fft: int = 2048,
    hop_length: Optional[int] = None,
    win_length: Optional[int] = None,
    window: str = "hann",
    detrend: str = "constant",
) -> "SpectralFrame":
    """Calculate magnitude squared coherence.

    Args:
        n_fft: Number of FFT points. Default is 2048.
        hop_length: Number of samples between frames.
            Default is n_fft//4.
        win_length: Window length. Default is n_fft.
        window: Window type. Default is "hann".
        detrend: Detrend method. Options: "constant", "linear", None.

    Returns:
        SpectralFrame containing magnitude squared coherence
    """
    from wandas.core.metadata import ChannelMetadata
    from wandas.processing import Coherence, create_operation

    from ..spectral import SpectralFrame

    params = {
        "n_fft": n_fft,
        "hop_length": hop_length,
        "win_length": win_length,
        "window": window,
        "detrend": detrend,
    }
    operation_name = "coherence"
    logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

    # Create operation instance
    operation = create_operation(operation_name, self.sampling_rate, **params)
    operation = cast("Coherence", operation)

    # Apply processing to data
    coherence_data = operation.process(self._data)

    logger.debug(
        f"Created new SpectralFrame with operation {operation_name} added to graph"
    )

    # Cast self as BaseFrame type
    base_self = cast(BaseFrame[Any], self)

    # Create new channel metadata
    channel_metadata = []
    for in_ch in self._channel_metadata:
        for out_ch in self._channel_metadata:
            meta = ChannelMetadata()
            meta.label = f"$\\gamma_{{{in_ch.label}, {out_ch.label}}}$"
            meta.unit = ""
            meta.ref = 1
            meta["metadata"] = dict(
                in_ch=in_ch["metadata"], out_ch=out_ch["metadata"]
            )
            channel_metadata.append(meta)

    # Create new instance
    return SpectralFrame(
        data=coherence_data,
        sampling_rate=self.sampling_rate,
        n_fft=operation.n_fft,
        window=operation.window,
        label=f"Coherence of {self.label}",
        metadata={**self.metadata, **params},
        operation_history=[
            *self.operation_history,
            {"operation": operation_name, "params": params},
        ],
        channel_metadata=channel_metadata,
        previous=base_self,
    )
csd(n_fft=2048, hop_length=None, win_length=None, window='hann', detrend='constant', scaling='spectrum', average='mean')

Calculate cross-spectral density matrix.

Parameters:

Name Type Description Default
n_fft int

Number of FFT points. Default is 2048.

2048
hop_length Optional[int]

Number of samples between frames. Default is n_fft//4.

None
win_length Optional[int]

Window length. Default is n_fft.

None
window str

Window type. Default is "hann".

'hann'
detrend str

Detrend method. Options: "constant", "linear", None.

'constant'
scaling str

Scaling method. Options: "spectrum", "density".

'spectrum'
average str

Method for averaging segments. Default is "mean".

'mean'

Returns:

Type Description
SpectralFrame

SpectralFrame containing cross-spectral density matrix

Source code in wandas/frames/mixins/channel_transform_mixin.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
def csd(
    self: T_Transform,
    n_fft: int = 2048,
    hop_length: Optional[int] = None,
    win_length: Optional[int] = None,
    window: str = "hann",
    detrend: str = "constant",
    scaling: str = "spectrum",
    average: str = "mean",
) -> "SpectralFrame":
    """Calculate cross-spectral density matrix.

    Args:
        n_fft: Number of FFT points. Default is 2048.
        hop_length: Number of samples between frames.
            Default is n_fft//4.
        win_length: Window length. Default is n_fft.
        window: Window type. Default is "hann".
        detrend: Detrend method. Options: "constant", "linear", None.
        scaling: Scaling method. Options: "spectrum", "density".
        average: Method for averaging segments. Default is "mean".

    Returns:
        SpectralFrame containing cross-spectral density matrix
    """
    from wandas.core.metadata import ChannelMetadata
    from wandas.frames.spectral import SpectralFrame
    from wandas.processing import CSD, create_operation

    params = {
        "n_fft": n_fft,
        "hop_length": hop_length,
        "win_length": win_length,
        "window": window,
        "detrend": detrend,
        "scaling": scaling,
        "average": average,
    }
    operation_name = "csd"
    logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

    # Create operation instance
    operation = create_operation(operation_name, self.sampling_rate, **params)
    operation = cast("CSD", operation)

    # Apply processing to data
    csd_data = operation.process(self._data)

    logger.debug(
        f"Created new SpectralFrame with operation {operation_name} added to graph"
    )

    # Cast self as BaseFrame type
    base_self = cast(BaseFrame[Any], self)

    # Create new channel metadata
    channel_metadata = []
    for in_ch in self._channel_metadata:
        for out_ch in self._channel_metadata:
            meta = ChannelMetadata()
            meta.label = f"{operation_name}({in_ch.label}, {out_ch.label})"
            meta.unit = ""
            meta.ref = 1
            meta["metadata"] = dict(
                in_ch=in_ch["metadata"], out_ch=out_ch["metadata"]
            )
            channel_metadata.append(meta)

    # Create new instance
    return SpectralFrame(
        data=csd_data,
        sampling_rate=self.sampling_rate,
        n_fft=operation.n_fft,
        window=operation.window,
        label=f"$C_{{{in_ch.label}, {out_ch.label}}}$",
        metadata={**self.metadata, **params},
        operation_history=[
            *self.operation_history,
            {"operation": operation_name, "params": params},
        ],
        channel_metadata=channel_metadata,
        previous=base_self,
    )
transfer_function(n_fft=2048, hop_length=None, win_length=None, window='hann', detrend='constant', scaling='spectrum', average='mean')

Calculate transfer function matrix.

The transfer function represents the signal transfer characteristics between channels in the frequency domain and represents the input-output relationship of the system.

Parameters:

Name Type Description Default
n_fft int

Number of FFT points. Default is 2048.

2048
hop_length Optional[int]

Number of samples between frames. Default is n_fft//4.

None
win_length Optional[int]

Window length. Default is n_fft.

None
window str

Window type. Default is "hann".

'hann'
detrend str

Detrend method. Options: "constant", "linear", None.

'constant'
scaling str

Scaling method. Options: "spectrum", "density".

'spectrum'
average str

Method for averaging segments. Default is "mean".

'mean'

Returns:

Type Description
SpectralFrame

SpectralFrame containing transfer function matrix

Source code in wandas/frames/mixins/channel_transform_mixin.py
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
def transfer_function(
    self: T_Transform,
    n_fft: int = 2048,
    hop_length: Optional[int] = None,
    win_length: Optional[int] = None,
    window: str = "hann",
    detrend: str = "constant",
    scaling: str = "spectrum",
    average: str = "mean",
) -> "SpectralFrame":
    """Calculate transfer function matrix.

    The transfer function represents the signal transfer characteristics between
    channels in the frequency domain and represents the input-output relationship
    of the system.

    Args:
        n_fft: Number of FFT points. Default is 2048.
        hop_length: Number of samples between frames.
            Default is n_fft//4.
        win_length: Window length. Default is n_fft.
        window: Window type. Default is "hann".
        detrend: Detrend method. Options: "constant", "linear", None.
        scaling: Scaling method. Options: "spectrum", "density".
        average: Method for averaging segments. Default is "mean".

    Returns:
        SpectralFrame containing transfer function matrix
    """
    from wandas.core.metadata import ChannelMetadata
    from wandas.frames.spectral import SpectralFrame
    from wandas.processing import TransferFunction, create_operation

    params = {
        "n_fft": n_fft,
        "hop_length": hop_length,
        "win_length": win_length,
        "window": window,
        "detrend": detrend,
        "scaling": scaling,
        "average": average,
    }
    operation_name = "transfer_function"
    logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

    # Create operation instance
    operation = create_operation(operation_name, self.sampling_rate, **params)
    operation = cast("TransferFunction", operation)

    # Apply processing to data
    tf_data = operation.process(self._data)

    logger.debug(
        f"Created new SpectralFrame with operation {operation_name} added to graph"
    )

    # Cast self as BaseFrame type
    base_self = cast(BaseFrame[Any], self)

    # Create new channel metadata
    channel_metadata = []
    for in_ch in self._channel_metadata:
        for out_ch in self._channel_metadata:
            meta = ChannelMetadata()
            meta.label = f"$H_{{{in_ch.label}, {out_ch.label}}}$"
            meta.unit = ""
            meta.ref = 1
            meta["metadata"] = dict(
                in_ch=in_ch["metadata"], out_ch=out_ch["metadata"]
            )
            channel_metadata.append(meta)

    # Create new instance
    return SpectralFrame(
        data=tf_data,
        sampling_rate=self.sampling_rate,
        n_fft=operation.n_fft,
        window=operation.window,
        label=f"Transfer function of {self.label}",
        metadata={**self.metadata, **params},
        operation_history=[
            *self.operation_history,
            {"operation": operation_name, "params": params},
        ],
        channel_metadata=channel_metadata,
        previous=base_self,
    )
protocols

Common protocol definition module.

This module contains common protocols used by mixin classes.

Attributes
logger = logging.getLogger(__name__) module-attribute
T_Base = TypeVar('T_Base', bound='BaseFrameProtocol') module-attribute
T_Processing = TypeVar('T_Processing', bound=ProcessingFrameProtocol) module-attribute
T_Transform = TypeVar('T_Transform', bound=TransformFrameProtocol) module-attribute
__all__ = ['BaseFrameProtocol', 'ProcessingFrameProtocol', 'TransformFrameProtocol', 'T_Processing'] module-attribute
Classes
BaseFrameProtocol

Bases: Protocol

Protocol that defines basic frame operations.

Defines the basic methods and properties provided by all frame classes.

Source code in wandas/frames/mixins/protocols.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@runtime_checkable
class BaseFrameProtocol(Protocol):
    """Protocol that defines basic frame operations.

    Defines the basic methods and properties provided by all frame classes.
    """

    _data: DaArray
    sampling_rate: float
    _channel_metadata: list[ChannelMetadata]
    metadata: dict[str, Any]
    operation_history: list[dict[str, Any]]
    label: str

    @property
    def duration(self) -> float:
        """Returns the duration in seconds."""
        ...

    def label2index(self, label: str) -> int:
        """
        Get the index from a channel label.
        """
        ...

    def apply_operation(
        self, operation_name: str, **params: Any
    ) -> "BaseFrameProtocol":
        """Apply a named operation.

        Args:
            operation_name: Name of the operation to apply
            **params: Parameters to pass to the operation

        Returns:
            A new frame instance with the operation applied
        """
        ...

    def _create_new_instance(self: T_Base, data: DaArray, **kwargs: Any) -> T_Base:
        """Create a new instance of the frame with updated data and metadata.
        Args:
            data: The new data for the frame
            metadata: The new metadata for the frame
            operation_history: The new operation history for the frame
            channel_metadata: The new channel metadata for the frame
        Returns:
            A new instance of the frame with the updated data and metadata
        """
        ...
Attributes
sampling_rate instance-attribute
metadata instance-attribute
operation_history instance-attribute
label instance-attribute
duration property

Returns the duration in seconds.

Functions
label2index(label)

Get the index from a channel label.

Source code in wandas/frames/mixins/protocols.py
38
39
40
41
42
def label2index(self, label: str) -> int:
    """
    Get the index from a channel label.
    """
    ...
apply_operation(operation_name, **params)

Apply a named operation.

Parameters:

Name Type Description Default
operation_name str

Name of the operation to apply

required
**params Any

Parameters to pass to the operation

{}

Returns:

Type Description
BaseFrameProtocol

A new frame instance with the operation applied

Source code in wandas/frames/mixins/protocols.py
44
45
46
47
48
49
50
51
52
53
54
55
56
def apply_operation(
    self, operation_name: str, **params: Any
) -> "BaseFrameProtocol":
    """Apply a named operation.

    Args:
        operation_name: Name of the operation to apply
        **params: Parameters to pass to the operation

    Returns:
        A new frame instance with the operation applied
    """
    ...
ProcessingFrameProtocol

Bases: BaseFrameProtocol, Protocol

Protocol that defines operations related to signal processing.

Defines methods that provide frame operations related to signal processing.

Source code in wandas/frames/mixins/protocols.py
71
72
73
74
75
76
77
78
@runtime_checkable
class ProcessingFrameProtocol(BaseFrameProtocol, Protocol):
    """Protocol that defines operations related to signal processing.

    Defines methods that provide frame operations related to signal processing.
    """

    pass
TransformFrameProtocol

Bases: BaseFrameProtocol, Protocol

Protocol related to transform operations.

Defines methods that provide operations such as frequency analysis and spectral transformation.

Source code in wandas/frames/mixins/protocols.py
81
82
83
84
85
86
87
88
89
@runtime_checkable
class TransformFrameProtocol(BaseFrameProtocol, Protocol):
    """Protocol related to transform operations.

    Defines methods that provide operations such as frequency analysis and
    spectral transformation.
    """

    pass

noct

Attributes
dask_delayed = dask.delayed module-attribute
da_from_delayed = da.from_delayed module-attribute
da_from_array = da.from_array module-attribute
logger = logging.getLogger(__name__) module-attribute
S = TypeVar('S', bound='BaseFrame[Any]') module-attribute
Classes
NOctFrame

Bases: BaseFrame[NDArrayReal]

Class for handling N-octave band analysis data.

This class represents frequency data analyzed in fractional octave bands, typically used in acoustic and vibration analysis. It handles real-valued data representing energy or power in each frequency band, following standard acoustical band definitions.

Parameters

data : DaArray The N-octave band data. Must be a dask array with shape: - (channels, frequency_bins) for multi-channel data - (frequency_bins,) for single-channel data, which will be reshaped to (1, frequency_bins) sampling_rate : float The sampling rate of the original time-domain signal in Hz. fmin : float, default=0 Lower frequency bound in Hz. fmax : float, default=0 Upper frequency bound in Hz. n : int, default=3 Number of bands per octave (e.g., 3 for third-octave bands). G : int, default=10 Reference band number according to IEC 61260-1:2014. fr : int, default=1000 Reference frequency in Hz, typically 1000 Hz for acoustic analysis. label : str, optional A label for the frame. metadata : dict, optional Additional metadata for the frame. operation_history : list[dict], optional History of operations performed on this frame. channel_metadata : list[ChannelMetadata], optional Metadata for each channel in the frame. previous : BaseFrame, optional The frame that this frame was derived from.

Attributes

freqs : NDArrayReal The center frequencies of each band in Hz, calculated according to the standard fractional octave band definitions. dB : NDArrayReal The spectrum in decibels relative to channel reference values. dBA : NDArrayReal The A-weighted spectrum in decibels, applying frequency weighting for better correlation with perceived loudness. fmin : float Lower frequency bound in Hz. fmax : float Upper frequency bound in Hz. n : int Number of bands per octave. G : int Reference band number. fr : int Reference frequency in Hz.

Examples

Create an N-octave band spectrum from a time-domain signal:

signal = ChannelFrame.from_wav("audio.wav") spectrum = signal.noct_spectrum(fmin=20, fmax=20000, n=3)

Plot the N-octave band spectrum:

spectrum.plot()

Plot with A-weighting applied:

spectrum.plot(Aw=True)

Notes
  • Binary operations (addition, multiplication, etc.) are not currently supported for N-octave band data.
  • The actual frequency bands are determined by the parameters n, G, and fr according to IEC 61260-1:2014 standard for fractional octave band filters.
  • The class follows acoustic standards for band definitions and analysis, making it suitable for noise measurements and sound level analysis.
  • A-weighting is available for better correlation with human hearing perception, following IEC 61672-1:2013.
Source code in wandas/frames/noct.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
class NOctFrame(BaseFrame[NDArrayReal]):
    """
    Class for handling N-octave band analysis data.

    This class represents frequency data analyzed in fractional octave bands,
    typically used in acoustic and vibration analysis. It handles real-valued
    data representing energy or power in each frequency band, following standard
    acoustical band definitions.

    Parameters
    ----------
    data : DaArray
        The N-octave band data. Must be a dask array with shape:
        - (channels, frequency_bins) for multi-channel data
        - (frequency_bins,) for single-channel data, which will be
          reshaped to (1, frequency_bins)
    sampling_rate : float
        The sampling rate of the original time-domain signal in Hz.
    fmin : float, default=0
        Lower frequency bound in Hz.
    fmax : float, default=0
        Upper frequency bound in Hz.
    n : int, default=3
        Number of bands per octave (e.g., 3 for third-octave bands).
    G : int, default=10
        Reference band number according to IEC 61260-1:2014.
    fr : int, default=1000
        Reference frequency in Hz, typically 1000 Hz for acoustic analysis.
    label : str, optional
        A label for the frame.
    metadata : dict, optional
        Additional metadata for the frame.
    operation_history : list[dict], optional
        History of operations performed on this frame.
    channel_metadata : list[ChannelMetadata], optional
        Metadata for each channel in the frame.
    previous : BaseFrame, optional
        The frame that this frame was derived from.

    Attributes
    ----------
    freqs : NDArrayReal
        The center frequencies of each band in Hz, calculated according to
        the standard fractional octave band definitions.
    dB : NDArrayReal
        The spectrum in decibels relative to channel reference values.
    dBA : NDArrayReal
        The A-weighted spectrum in decibels, applying frequency weighting
        for better correlation with perceived loudness.
    fmin : float
        Lower frequency bound in Hz.
    fmax : float
        Upper frequency bound in Hz.
    n : int
        Number of bands per octave.
    G : int
        Reference band number.
    fr : int
        Reference frequency in Hz.

    Examples
    --------
    Create an N-octave band spectrum from a time-domain signal:
    >>> signal = ChannelFrame.from_wav("audio.wav")
    >>> spectrum = signal.noct_spectrum(fmin=20, fmax=20000, n=3)

    Plot the N-octave band spectrum:
    >>> spectrum.plot()

    Plot with A-weighting applied:
    >>> spectrum.plot(Aw=True)

    Notes
    -----
    - Binary operations (addition, multiplication, etc.) are not currently
      supported for N-octave band data.
    - The actual frequency bands are determined by the parameters n, G, and fr
      according to IEC 61260-1:2014 standard for fractional octave band filters.
    - The class follows acoustic standards for band definitions and analysis,
      making it suitable for noise measurements and sound level analysis.
    - A-weighting is available for better correlation with human hearing
      perception, following IEC 61672-1:2013.
    """

    fmin: float
    fmax: float
    n: int
    G: int
    fr: int

    def __init__(
        self,
        data: DaArray,
        sampling_rate: float,
        fmin: float = 0,
        fmax: float = 0,
        n: int = 3,
        G: int = 10,  # noqa: N803
        fr: int = 1000,
        label: Optional[str] = None,
        metadata: Optional[dict[str, Any]] = None,
        operation_history: Optional[list[dict[str, Any]]] = None,
        channel_metadata: Optional[list[ChannelMetadata]] = None,
        previous: Optional["BaseFrame[Any]"] = None,
    ) -> None:
        """
        Initialize a NOctFrame instance.

        Sets up N-octave band analysis parameters and prepares the frame for
        storing band-filtered data. Data shape is validated to ensure compatibility
        with N-octave band analysis.

        See class docstring for parameter descriptions.
        """
        self.n = n
        self.G = G
        self.fr = fr
        self.fmin = fmin
        self.fmax = fmax
        super().__init__(
            data=data,
            sampling_rate=sampling_rate,
            label=label,
            metadata=metadata,
            operation_history=operation_history,
            channel_metadata=channel_metadata,
            previous=previous,
        )

    @property
    def dB(self) -> NDArrayReal:  # noqa: N802
        """
        Get the spectrum in decibels relative to each channel's reference value.

        The reference value for each channel is specified in its metadata.
        A minimum value of -120 dB is enforced to avoid numerical issues.

        Returns
        -------
        NDArrayReal
            The spectrum in decibels. Shape matches the input data shape:
            (channels, frequency_bins).
        """
        # Collect dB reference values from _channel_metadata
        ref = np.array([ch.ref for ch in self._channel_metadata])
        # Convert to dB
        # Use either the maximum value or 1e-12 to avoid division by zero
        level: NDArrayReal = 20 * np.log10(
            np.maximum(self.data / ref[..., np.newaxis], 1e-12)
        )
        return level

    @property
    def dBA(self) -> NDArrayReal:  # noqa: N802
        """
        Get the A-weighted spectrum in decibels.

        A-weighting applies a frequency-dependent weighting filter that approximates
        the human ear's response to different frequencies. This is particularly useful
        for analyzing noise and acoustic measurements as it provides a better
        correlation with perceived loudness.

        The weighting is applied according to IEC 61672-1:2013 standard.

        Returns
        -------
        NDArrayReal
            The A-weighted spectrum in decibels. Shape matches the input data shape:
            (channels, frequency_bins).
        """
        # Collect dB reference values from _channel_metadata
        weighted: NDArrayReal = librosa.A_weighting(frequencies=self.freqs, min_db=None)
        return self.dB + weighted

    @property
    def _n_channels(self) -> int:
        """
        Get the number of channels in the data.

        Returns
        -------
        int
            The number of channels in the N-octave band data.
        """
        return int(self._data.shape[-2])

    @property
    def freqs(self) -> NDArrayReal:
        """
        Get the center frequencies of each band in Hz.

        These frequencies are calculated based on the N-octave band parameters
        (n, G, fr) and the frequency bounds (fmin, fmax) according to
        IEC 61260-1:2014 standard for fractional octave band filters.

        Returns
        -------
        NDArrayReal
            Array of center frequencies for each frequency band.

        Raises
        ------
        ValueError
            If the center frequencies cannot be calculated or the result
            is not a numpy array.
        """
        _, freqs = _center_freq(
            fmax=self.fmax,
            fmin=self.fmin,
            n=self.n,
            G=self.G,
            fr=self.fr,
        )
        if isinstance(freqs, np.ndarray):
            return freqs
        else:
            raise ValueError("freqs is not numpy array.")

    def _binary_op(
        self: S,
        other: Union[S, int, float, NDArrayReal, DaArray],
        op: Callable[[DaArray, Any], DaArray],
        symbol: str,
    ) -> S:
        """
        Binary operations are not currently supported for N-octave band data.

        Parameters
        ----------
        other : Union[S, int, float, NDArrayReal, DaArray]
            The right operand of the operation.
        op : callable
            Function to execute the operation.
        symbol : str
            String representation of the operation (e.g., '+', '-', '*', '/').

        Raises
        ------
        NotImplementedError
            Always raises this error as operations are not implemented
            for N-octave band data.
        """
        raise NotImplementedError(
            f"Operation {symbol} is not implemented for NOctFrame."
        )
        return self

    def _apply_operation_impl(self: S, operation_name: str, **params: Any) -> S:
        """
        Apply operations using lazy evaluation.
        """
        # Apply operations using lazy evaluation
        raise NotImplementedError(
            f"Operation {operation_name} is not implemented for NOctFrame."
        )
        return self

    def plot(
        self, plot_type: str = "noct", ax: Optional["Axes"] = None, **kwargs: Any
    ) -> Union["Axes", Iterator["Axes"]]:
        """
        Plot the N-octave band data using various visualization strategies.

        Supports standard plotting configurations for acoustic analysis,
        including decibel scales and A-weighting.

        Parameters
        ----------
        plot_type : str, default="noct"
            Type of plot to create. The default "noct" type creates a bar plot
            suitable for displaying N-octave band data.
        ax : matplotlib.axes.Axes, optional
            Axes to plot on. If None, creates new axes.
        **kwargs : dict
            Additional keyword arguments passed to the plot strategy.
            Common options include:
            - dB: Whether to plot in decibels
            - Aw: Whether to apply A-weighting
            - title: Plot title
            - xlabel, ylabel: Axis labels
            - xscale: Set to "log" for logarithmic frequency axis
            - grid: Whether to show grid lines

        Returns
        -------
        Union[Axes, Iterator[Axes]]
            The matplotlib axes containing the plot, or an iterator of axes
            for multi-plot outputs.
        """
        from wandas.visualization.plotting import create_operation

        logger.debug(f"Plotting audio with plot_type={plot_type} (will compute now)")

        # Get plot strategy
        plot_strategy: PlotStrategy[NOctFrame] = create_operation(plot_type)

        # Execute plot
        _ax = plot_strategy.plot(self, ax=ax, **kwargs)

        logger.debug("Plot rendering complete")

        return _ax

    def _get_additional_init_kwargs(self) -> dict[str, Any]:
        """
        Get additional initialization arguments for NOctFrame.

        This internal method provides the additional initialization arguments
        required by NOctFrame beyond those required by BaseFrame. These include
        the N-octave band analysis parameters that define the frequency bands.

        Returns
        -------
        dict[str, Any]
            Additional initialization arguments specific to NOctFrame:
            - n: Number of bands per octave
            - G: Reference band number
            - fr: Reference frequency
            - fmin: Lower frequency bound
            - fmax: Upper frequency bound
        """
        return {
            "n": self.n,
            "G": self.G,
            "fr": self.fr,
            "fmin": self.fmin,
            "fmax": self.fmax,
        }
Attributes
n = n instance-attribute
G = G instance-attribute
fr = fr instance-attribute
fmin = fmin instance-attribute
fmax = fmax instance-attribute
dB property

Get the spectrum in decibels relative to each channel's reference value.

The reference value for each channel is specified in its metadata. A minimum value of -120 dB is enforced to avoid numerical issues.

Returns

NDArrayReal The spectrum in decibels. Shape matches the input data shape: (channels, frequency_bins).

dBA property

Get the A-weighted spectrum in decibels.

A-weighting applies a frequency-dependent weighting filter that approximates the human ear's response to different frequencies. This is particularly useful for analyzing noise and acoustic measurements as it provides a better correlation with perceived loudness.

The weighting is applied according to IEC 61672-1:2013 standard.

Returns

NDArrayReal The A-weighted spectrum in decibels. Shape matches the input data shape: (channels, frequency_bins).

freqs property

Get the center frequencies of each band in Hz.

These frequencies are calculated based on the N-octave band parameters (n, G, fr) and the frequency bounds (fmin, fmax) according to IEC 61260-1:2014 standard for fractional octave band filters.

Returns

NDArrayReal Array of center frequencies for each frequency band.

Raises

ValueError If the center frequencies cannot be calculated or the result is not a numpy array.

Functions
__init__(data, sampling_rate, fmin=0, fmax=0, n=3, G=10, fr=1000, label=None, metadata=None, operation_history=None, channel_metadata=None, previous=None)

Initialize a NOctFrame instance.

Sets up N-octave band analysis parameters and prepares the frame for storing band-filtered data. Data shape is validated to ensure compatibility with N-octave band analysis.

See class docstring for parameter descriptions.

Source code in wandas/frames/noct.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def __init__(
    self,
    data: DaArray,
    sampling_rate: float,
    fmin: float = 0,
    fmax: float = 0,
    n: int = 3,
    G: int = 10,  # noqa: N803
    fr: int = 1000,
    label: Optional[str] = None,
    metadata: Optional[dict[str, Any]] = None,
    operation_history: Optional[list[dict[str, Any]]] = None,
    channel_metadata: Optional[list[ChannelMetadata]] = None,
    previous: Optional["BaseFrame[Any]"] = None,
) -> None:
    """
    Initialize a NOctFrame instance.

    Sets up N-octave band analysis parameters and prepares the frame for
    storing band-filtered data. Data shape is validated to ensure compatibility
    with N-octave band analysis.

    See class docstring for parameter descriptions.
    """
    self.n = n
    self.G = G
    self.fr = fr
    self.fmin = fmin
    self.fmax = fmax
    super().__init__(
        data=data,
        sampling_rate=sampling_rate,
        label=label,
        metadata=metadata,
        operation_history=operation_history,
        channel_metadata=channel_metadata,
        previous=previous,
    )
plot(plot_type='noct', ax=None, **kwargs)

Plot the N-octave band data using various visualization strategies.

Supports standard plotting configurations for acoustic analysis, including decibel scales and A-weighting.

Parameters

plot_type : str, default="noct" Type of plot to create. The default "noct" type creates a bar plot suitable for displaying N-octave band data. ax : matplotlib.axes.Axes, optional Axes to plot on. If None, creates new axes. **kwargs : dict Additional keyword arguments passed to the plot strategy. Common options include: - dB: Whether to plot in decibels - Aw: Whether to apply A-weighting - title: Plot title - xlabel, ylabel: Axis labels - xscale: Set to "log" for logarithmic frequency axis - grid: Whether to show grid lines

Returns

Union[Axes, Iterator[Axes]] The matplotlib axes containing the plot, or an iterator of axes for multi-plot outputs.

Source code in wandas/frames/noct.py
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
def plot(
    self, plot_type: str = "noct", ax: Optional["Axes"] = None, **kwargs: Any
) -> Union["Axes", Iterator["Axes"]]:
    """
    Plot the N-octave band data using various visualization strategies.

    Supports standard plotting configurations for acoustic analysis,
    including decibel scales and A-weighting.

    Parameters
    ----------
    plot_type : str, default="noct"
        Type of plot to create. The default "noct" type creates a bar plot
        suitable for displaying N-octave band data.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, creates new axes.
    **kwargs : dict
        Additional keyword arguments passed to the plot strategy.
        Common options include:
        - dB: Whether to plot in decibels
        - Aw: Whether to apply A-weighting
        - title: Plot title
        - xlabel, ylabel: Axis labels
        - xscale: Set to "log" for logarithmic frequency axis
        - grid: Whether to show grid lines

    Returns
    -------
    Union[Axes, Iterator[Axes]]
        The matplotlib axes containing the plot, or an iterator of axes
        for multi-plot outputs.
    """
    from wandas.visualization.plotting import create_operation

    logger.debug(f"Plotting audio with plot_type={plot_type} (will compute now)")

    # Get plot strategy
    plot_strategy: PlotStrategy[NOctFrame] = create_operation(plot_type)

    # Execute plot
    _ax = plot_strategy.plot(self, ax=ax, **kwargs)

    logger.debug("Plot rendering complete")

    return _ax

spectral

Attributes
dask_delayed = dask.delayed module-attribute
da_from_delayed = da.from_delayed module-attribute
da_from_array = da.from_array module-attribute
logger = logging.getLogger(__name__) module-attribute
S = TypeVar('S', bound='BaseFrame[Any]') module-attribute
Classes
SpectralFrame

Bases: BaseFrame[NDArrayComplex]

Class for handling frequency-domain signal data.

This class represents spectral data, providing methods for spectral analysis, manipulation, and visualization. It handles complex-valued frequency domain data obtained through operations like FFT.

Parameters

data : DaArray The spectral data. Must be a dask array with shape: - (channels, frequency_bins) for multi-channel data - (frequency_bins,) for single-channel data, which will be reshaped to (1, frequency_bins) sampling_rate : float The sampling rate of the original time-domain signal in Hz. n_fft : int The FFT size used to generate this spectral data. window : str, default="hann" The window function used in the FFT. label : str, optional A label for the frame. metadata : dict, optional Additional metadata for the frame. operation_history : list[dict], optional History of operations performed on this frame. channel_metadata : list[ChannelMetadata], optional Metadata for each channel in the frame. previous : BaseFrame, optional The frame that this frame was derived from.

Attributes

magnitude : NDArrayReal The magnitude spectrum of the data. phase : NDArrayReal The phase spectrum in radians. power : NDArrayReal The power spectrum (magnitude squared). dB : NDArrayReal The spectrum in decibels relative to channel reference values. dBA : NDArrayReal The A-weighted spectrum in decibels. freqs : NDArrayReal The frequency axis values in Hz.

Examples

Create a SpectralFrame from FFT:

signal = ChannelFrame.from_numpy(data, sampling_rate=44100) spectrum = signal.fft(n_fft=2048)

Plot the magnitude spectrum:

spectrum.plot()

Perform binary operations:

scaled = spectrum * 2.0 summed = spectrum1 + spectrum2 # Must have matching sampling rates

Convert back to time domain:

time_signal = spectrum.ifft()

Notes
  • All operations are performed lazily using dask arrays for efficient memory usage.
  • Binary operations (+, -, *, /) can be performed between SpectralFrames or with scalar values.
  • The class maintains the processing history and metadata through all operations.
Source code in wandas/frames/spectral.py
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
class SpectralFrame(BaseFrame[NDArrayComplex]):
    """
    Class for handling frequency-domain signal data.

    This class represents spectral data, providing methods for spectral analysis,
    manipulation, and visualization. It handles complex-valued frequency domain data
    obtained through operations like FFT.

    Parameters
    ----------
    data : DaArray
        The spectral data. Must be a dask array with shape:
        - (channels, frequency_bins) for multi-channel data
        - (frequency_bins,) for single-channel data, which will be
          reshaped to (1, frequency_bins)
    sampling_rate : float
        The sampling rate of the original time-domain signal in Hz.
    n_fft : int
        The FFT size used to generate this spectral data.
    window : str, default="hann"
        The window function used in the FFT.
    label : str, optional
        A label for the frame.
    metadata : dict, optional
        Additional metadata for the frame.
    operation_history : list[dict], optional
        History of operations performed on this frame.
    channel_metadata : list[ChannelMetadata], optional
        Metadata for each channel in the frame.
    previous : BaseFrame, optional
        The frame that this frame was derived from.

    Attributes
    ----------
    magnitude : NDArrayReal
        The magnitude spectrum of the data.
    phase : NDArrayReal
        The phase spectrum in radians.
    power : NDArrayReal
        The power spectrum (magnitude squared).
    dB : NDArrayReal
        The spectrum in decibels relative to channel reference values.
    dBA : NDArrayReal
        The A-weighted spectrum in decibels.
    freqs : NDArrayReal
        The frequency axis values in Hz.

    Examples
    --------
    Create a SpectralFrame from FFT:
    >>> signal = ChannelFrame.from_numpy(data, sampling_rate=44100)
    >>> spectrum = signal.fft(n_fft=2048)

    Plot the magnitude spectrum:
    >>> spectrum.plot()

    Perform binary operations:
    >>> scaled = spectrum * 2.0
    >>> summed = spectrum1 + spectrum2  # Must have matching sampling rates

    Convert back to time domain:
    >>> time_signal = spectrum.ifft()

    Notes
    -----
    - All operations are performed lazily using dask arrays for efficient memory usage.
    - Binary operations (+, -, *, /) can be performed between SpectralFrames or with
      scalar values.
    - The class maintains the processing history and metadata through all operations.
    """

    n_fft: int
    window: str

    def __init__(
        self,
        data: DaArray,
        sampling_rate: float,
        n_fft: int,
        window: str = "hann",
        label: Optional[str] = None,
        metadata: Optional[dict[str, Any]] = None,
        operation_history: Optional[list[dict[str, Any]]] = None,
        channel_metadata: Optional[list[ChannelMetadata]] = None,
        previous: Optional["BaseFrame[Any]"] = None,
    ) -> None:
        if data.ndim == 1:
            data = data.reshape(1, -1)
        elif data.ndim > 2:
            raise ValueError(
                f"Data must be 1-dimensional or 2-dimensional. Shape: {data.shape}"
            )
        self.n_fft = n_fft
        self.window = window
        super().__init__(
            data=data,
            sampling_rate=sampling_rate,
            label=label,
            metadata=metadata,
            operation_history=operation_history,
            channel_metadata=channel_metadata,
            previous=previous,
        )

    @property
    def magnitude(self) -> NDArrayReal:
        """
        Get the magnitude spectrum.

        Returns
        -------
        NDArrayReal
            The absolute values of the complex spectrum.
        """
        return np.abs(self.data)

    @property
    def phase(self) -> NDArrayReal:
        """
        Get the phase spectrum.

        Returns
        -------
        NDArrayReal
            The phase angles of the complex spectrum in radians.
        """
        return np.angle(self.data)

    @property
    def power(self) -> NDArrayReal:
        """
        Get the power spectrum.

        Returns
        -------
        NDArrayReal
            The squared magnitude spectrum.
        """
        return self.magnitude**2

    @property
    def dB(self) -> NDArrayReal:  # noqa: N802
        """
        Get the spectrum in decibels.

        The reference values are taken from channel metadata. If no reference
        is specified, uses 1.0.

        Returns
        -------
        NDArrayReal
            The spectrum in dB relative to channel references.
        """
        mag: NDArrayReal = self.magnitude
        ref_values: NDArrayReal = np.array([ch.ref for ch in self._channel_metadata])
        level: NDArrayReal = 20 * np.log10(
            np.maximum(mag / ref_values[:, np.newaxis], 1e-12)
        )

        return level

    @property
    def dBA(self) -> NDArrayReal:  # noqa: N802
        """
        Get the A-weighted spectrum in decibels.

        Applies A-weighting filter to the spectrum for better correlation with
        perceived loudness.

        Returns
        -------
        NDArrayReal
            The A-weighted spectrum in dB.
        """
        weighted: NDArrayReal = librosa.A_weighting(frequencies=self.freqs, min_db=None)
        return self.dB + weighted

    @property
    def _n_channels(self) -> int:
        """
        Get the number of channels in the data.

        Returns
        -------
        int
            The number of channels.
        """
        return int(self._data.shape[-2])

    @property
    def freqs(self) -> NDArrayReal:
        """
        Get the frequency axis values in Hz.

        Returns
        -------
        NDArrayReal
            Array of frequency values corresponding to each frequency bin.
        """
        return np.fft.rfftfreq(self.n_fft, 1.0 / self.sampling_rate)

    def _apply_operation_impl(self: S, operation_name: str, **params: Any) -> S:
        """
        Implementation of operation application for spectral data.

        This internal method handles the application of various operations to
        spectral data, maintaining lazy evaluation through dask.

        Parameters
        ----------
        operation_name : str
            Name of the operation to apply.
        **params : Any
            Parameters for the operation.

        Returns
        -------
        S
            A new instance with the operation applied.
        """
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")
        from ..processing import create_operation

        # Create operation instance
        operation = create_operation(operation_name, self.sampling_rate, **params)

        # Apply processing to data
        processed_data = operation.process(self._data)

        # Update metadata
        operation_metadata = {"operation": operation_name, "params": params}
        new_history = self.operation_history.copy()
        new_history.append(operation_metadata)
        new_metadata = {**self.metadata}
        new_metadata[operation_name] = params

        logger.debug(
            f"Created new ChannelFrame with operation {operation_name} added to graph"
        )
        return self._create_new_instance(
            data=processed_data,
            metadata=new_metadata,
            operation_history=new_history,
        )

    def _binary_op(
        self,
        other: Union[
            "SpectralFrame", int, float, complex, NDArrayComplex, NDArrayReal, "DaArray"
        ],
        op: Callable[["DaArray", Any], "DaArray"],
        symbol: str,
    ) -> "SpectralFrame":
        """
        Common implementation for binary operations.

        This method handles binary operations between SpectralFrames and various types
        of operands, maintaining lazy evaluation through dask arrays.

        Parameters
        ----------
        other : Union[SpectralFrame, int, float, complex,
                        NDArrayComplex, NDArrayReal, DaArray]
            The right operand of the operation.
        op : callable
            Function to execute the operation (e.g., lambda a, b: a + b)
        symbol : str
            String representation of the operation (e.g., '+')

        Returns
        -------
        SpectralFrame
            A new SpectralFrame containing the result of the operation.

        Raises
        ------
        ValueError
            If attempting to operate with a SpectralFrame
            with a different sampling rate.
        """
        logger.debug(f"Setting up {symbol} operation (lazy)")

        # Handle potentially None metadata and operation_history
        metadata = {}
        if self.metadata is not None:
            metadata = self.metadata.copy()

        operation_history = []
        if self.operation_history is not None:
            operation_history = self.operation_history.copy()

        # Check if other is a ChannelFrame - improved type checking
        if isinstance(other, SpectralFrame):
            if self.sampling_rate != other.sampling_rate:
                raise ValueError(
                    "Sampling rates do not match. Cannot perform operation."
                )

            # Directly operate on dask arrays (maintaining lazy execution)
            result_data = op(self._data, other._data)

            # Combine channel metadata
            merged_channel_metadata = []
            for self_ch, other_ch in zip(
                self._channel_metadata, other._channel_metadata
            ):
                ch = self_ch.copy(deep=True)
                ch["label"] = f"({self_ch['label']} {symbol} {other_ch['label']})"
                merged_channel_metadata.append(ch)

            operation_history.append({"operation": symbol, "with": other.label})

            return SpectralFrame(
                data=result_data,
                sampling_rate=self.sampling_rate,
                n_fft=self.n_fft,
                window=self.window,
                label=f"({self.label} {symbol} {other.label})",
                metadata=metadata,
                operation_history=operation_history,
                channel_metadata=merged_channel_metadata,
                previous=self,
            )

        # Operation with scalar, NumPy array, or other types
        else:
            # Apply operation directly to dask array (maintaining lazy execution)
            result_data = op(self._data, other)

            # String representation of operand for display
            if isinstance(other, (int, float)):
                other_str = str(other)
            elif isinstance(other, complex):
                other_str = f"complex({other.real}, {other.imag})"
            elif isinstance(other, np.ndarray):
                other_str = f"ndarray{other.shape}"
            elif hasattr(other, "shape"):  # Check for dask.array.Array
                other_str = f"dask.array{other.shape}"
            else:
                other_str = str(type(other).__name__)

            # Update channel metadata
            updated_channel_metadata: list[ChannelMetadata] = []
            for self_ch in self._channel_metadata:
                ch = self_ch.model_copy(deep=True)
                ch["label"] = f"({self_ch.label} {symbol} {other_str})"
                updated_channel_metadata.append(ch)

            operation_history.append({"operation": symbol, "with": other_str})

            return SpectralFrame(
                data=result_data,
                sampling_rate=self.sampling_rate,
                n_fft=self.n_fft,
                window=self.window,
                label=f"({self.label} {symbol} {other_str})",
                metadata=metadata,
                operation_history=operation_history,
                channel_metadata=updated_channel_metadata,
            )

    def plot(
        self,
        plot_type: str = "frequency",
        ax: Optional["Axes"] = None,
        **kwargs: Any,
    ) -> Union["Axes", Iterator["Axes"]]:
        """
        Plot the spectral data using various visualization strategies.

        Parameters
        ----------
        plot_type : str, default="frequency"
            Type of plot to create. Options include:
            - "frequency": Standard frequency plot
            - "matrix": Matrix plot for comparing channels
            - Other types as defined by available plot strategies
        ax : matplotlib.axes.Axes, optional
            Axes to plot on. If None, creates new axes.
        **kwargs : dict
            Additional keyword arguments passed to the plot strategy.
            Common options include:
            - title: Plot title
            - xlabel, ylabel: Axis labels
            - vmin, vmax: Value limits for plots
            - cmap: Colormap name
            - dB: Whether to plot in decibels
            - Aw: Whether to apply A-weighting

        Returns
        -------
        Union[Axes, Iterator[Axes]]
            The matplotlib axes containing the plot, or an iterator of axes
            for multi-plot outputs.
        """
        from wandas.visualization.plotting import create_operation

        logger.debug(f"Plotting audio with plot_type={plot_type} (will compute now)")

        # Get plot strategy
        plot_strategy: PlotStrategy[SpectralFrame] = create_operation(plot_type)

        # Execute plot
        _ax = plot_strategy.plot(self, ax=ax, **kwargs)

        logger.debug("Plot rendering complete")

        return _ax

    def ifft(self) -> "ChannelFrame":
        """
        Compute the Inverse Fast Fourier Transform (IFFT) to return to time domain.

        This method transforms the frequency-domain data back to the time domain using
        the inverse FFT operation. The window function used in the forward FFT is
        taken into account to ensure proper reconstruction.

        Returns
        -------
        ChannelFrame
            A new ChannelFrame containing the time-domain signal.
        """
        from ..processing import IFFT, create_operation
        from .channel import ChannelFrame

        params = {"n_fft": self.n_fft, "window": self.window}
        operation_name = "ifft"
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

        # Create operation instance
        operation = create_operation(operation_name, self.sampling_rate, **params)
        operation = cast("IFFT", operation)
        # Apply processing to data
        time_series = operation.process(self._data)

        logger.debug(
            f"Created new SpectralFrame with operation {operation_name} added to graph"
        )

        # Create new instance
        return ChannelFrame(
            data=time_series,
            sampling_rate=self.sampling_rate,
            label=f"ifft({self.label})",
            metadata=self.metadata,
            operation_history=self.operation_history,
            channel_metadata=self._channel_metadata,
        )

    def _get_additional_init_kwargs(self) -> dict[str, Any]:
        """
        Provide additional initialization arguments required for SpectralFrame.

        Returns
        -------
        dict[str, Any]
            Additional initialization arguments for SpectralFrame.
        """
        return {
            "n_fft": self.n_fft,
            "window": self.window,
        }

    def noct_synthesis(
        self,
        fmin: float,
        fmax: float,
        n: int = 3,
        G: int = 10,  # noqa: N803
        fr: int = 1000,
    ) -> "NOctFrame":
        """
        Synthesize N-octave band spectrum.

        This method combines frequency components into N-octave bands according to
        standard acoustical band definitions. This is commonly used in noise and
        vibration analysis.

        Parameters
        ----------
        fmin : float
            Lower frequency bound in Hz.
        fmax : float
            Upper frequency bound in Hz.
        n : int, default=3
            Number of bands per octave (e.g., 3 for third-octave bands).
        G : int, default=10
            Reference band number.
        fr : int, default=1000
            Reference frequency in Hz.

        Returns
        -------
        NOctFrame
            A new NOctFrame containing the N-octave band spectrum.

        Raises
        ------
        ValueError
            If the sampling rate is not 48000 Hz, which is required for this operation.
        """
        if self.sampling_rate != 48000:
            raise ValueError(
                "noct_synthesis can only be used with a sampling rate of 48000 Hz."
            )
        from ..processing import NOctSynthesis
        from .noct import NOctFrame

        params = {"fmin": fmin, "fmax": fmax, "n": n, "G": G, "fr": fr}
        operation_name = "noct_synthesis"
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")
        from ..processing import create_operation

        # Create operation instance
        operation = create_operation(operation_name, self.sampling_rate, **params)
        operation = cast("NOctSynthesis", operation)
        # Apply processing to data
        spectrum_data = operation.process(self._data)

        logger.debug(
            f"Created new SpectralFrame with operation {operation_name} added to graph"
        )

        return NOctFrame(
            data=spectrum_data,
            sampling_rate=self.sampling_rate,
            fmin=fmin,
            fmax=fmax,
            n=n,
            G=G,
            fr=fr,
            label=f"1/{n}Oct of {self.label}",
            metadata={**self.metadata, **params},
            operation_history=[
                *self.operation_history,
                {
                    "operation": "noct_synthesis",
                    "params": params,
                },
            ],
            channel_metadata=self._channel_metadata,
            previous=self,
        )

    def plot_matrix(
        self,
        plot_type: str = "matrix",
        **kwargs: Any,
    ) -> Union["Axes", Iterator["Axes"]]:
        """
        Plot channel relationships in matrix format.

        This method creates a matrix plot showing relationships between channels,
        such as coherence, transfer functions, or cross-spectral density.

        Parameters
        ----------
        plot_type : str, default="matrix"
            Type of matrix plot to create.
        **kwargs : dict
            Additional plot parameters:
            - vmin, vmax: Color scale limits
            - cmap: Colormap name
            - title: Plot title

        Returns
        -------
        Union[Axes, Iterator[Axes]]
            The matplotlib axes containing the plot.
        """
        from wandas.visualization.plotting import create_operation

        logger.debug(f"Plotting audio with plot_type={plot_type} (will compute now)")

        # Get plot strategy
        plot_strategy: PlotStrategy[SpectralFrame] = create_operation(plot_type)

        # Execute plot
        _ax = plot_strategy.plot(self, **kwargs)

        logger.debug("Plot rendering complete")

        return _ax
Attributes
n_fft = n_fft instance-attribute
window = window instance-attribute
magnitude property

Get the magnitude spectrum.

Returns

NDArrayReal The absolute values of the complex spectrum.

phase property

Get the phase spectrum.

Returns

NDArrayReal The phase angles of the complex spectrum in radians.

power property

Get the power spectrum.

Returns

NDArrayReal The squared magnitude spectrum.

dB property

Get the spectrum in decibels.

The reference values are taken from channel metadata. If no reference is specified, uses 1.0.

Returns

NDArrayReal The spectrum in dB relative to channel references.

dBA property

Get the A-weighted spectrum in decibels.

Applies A-weighting filter to the spectrum for better correlation with perceived loudness.

Returns

NDArrayReal The A-weighted spectrum in dB.

freqs property

Get the frequency axis values in Hz.

Returns

NDArrayReal Array of frequency values corresponding to each frequency bin.

Functions
__init__(data, sampling_rate, n_fft, window='hann', label=None, metadata=None, operation_history=None, channel_metadata=None, previous=None)
Source code in wandas/frames/spectral.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def __init__(
    self,
    data: DaArray,
    sampling_rate: float,
    n_fft: int,
    window: str = "hann",
    label: Optional[str] = None,
    metadata: Optional[dict[str, Any]] = None,
    operation_history: Optional[list[dict[str, Any]]] = None,
    channel_metadata: Optional[list[ChannelMetadata]] = None,
    previous: Optional["BaseFrame[Any]"] = None,
) -> None:
    if data.ndim == 1:
        data = data.reshape(1, -1)
    elif data.ndim > 2:
        raise ValueError(
            f"Data must be 1-dimensional or 2-dimensional. Shape: {data.shape}"
        )
    self.n_fft = n_fft
    self.window = window
    super().__init__(
        data=data,
        sampling_rate=sampling_rate,
        label=label,
        metadata=metadata,
        operation_history=operation_history,
        channel_metadata=channel_metadata,
        previous=previous,
    )
plot(plot_type='frequency', ax=None, **kwargs)

Plot the spectral data using various visualization strategies.

Parameters

plot_type : str, default="frequency" Type of plot to create. Options include: - "frequency": Standard frequency plot - "matrix": Matrix plot for comparing channels - Other types as defined by available plot strategies ax : matplotlib.axes.Axes, optional Axes to plot on. If None, creates new axes. **kwargs : dict Additional keyword arguments passed to the plot strategy. Common options include: - title: Plot title - xlabel, ylabel: Axis labels - vmin, vmax: Value limits for plots - cmap: Colormap name - dB: Whether to plot in decibels - Aw: Whether to apply A-weighting

Returns

Union[Axes, Iterator[Axes]] The matplotlib axes containing the plot, or an iterator of axes for multi-plot outputs.

Source code in wandas/frames/spectral.py
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
def plot(
    self,
    plot_type: str = "frequency",
    ax: Optional["Axes"] = None,
    **kwargs: Any,
) -> Union["Axes", Iterator["Axes"]]:
    """
    Plot the spectral data using various visualization strategies.

    Parameters
    ----------
    plot_type : str, default="frequency"
        Type of plot to create. Options include:
        - "frequency": Standard frequency plot
        - "matrix": Matrix plot for comparing channels
        - Other types as defined by available plot strategies
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, creates new axes.
    **kwargs : dict
        Additional keyword arguments passed to the plot strategy.
        Common options include:
        - title: Plot title
        - xlabel, ylabel: Axis labels
        - vmin, vmax: Value limits for plots
        - cmap: Colormap name
        - dB: Whether to plot in decibels
        - Aw: Whether to apply A-weighting

    Returns
    -------
    Union[Axes, Iterator[Axes]]
        The matplotlib axes containing the plot, or an iterator of axes
        for multi-plot outputs.
    """
    from wandas.visualization.plotting import create_operation

    logger.debug(f"Plotting audio with plot_type={plot_type} (will compute now)")

    # Get plot strategy
    plot_strategy: PlotStrategy[SpectralFrame] = create_operation(plot_type)

    # Execute plot
    _ax = plot_strategy.plot(self, ax=ax, **kwargs)

    logger.debug("Plot rendering complete")

    return _ax
ifft()

Compute the Inverse Fast Fourier Transform (IFFT) to return to time domain.

This method transforms the frequency-domain data back to the time domain using the inverse FFT operation. The window function used in the forward FFT is taken into account to ensure proper reconstruction.

Returns

ChannelFrame A new ChannelFrame containing the time-domain signal.

Source code in wandas/frames/spectral.py
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
def ifft(self) -> "ChannelFrame":
    """
    Compute the Inverse Fast Fourier Transform (IFFT) to return to time domain.

    This method transforms the frequency-domain data back to the time domain using
    the inverse FFT operation. The window function used in the forward FFT is
    taken into account to ensure proper reconstruction.

    Returns
    -------
    ChannelFrame
        A new ChannelFrame containing the time-domain signal.
    """
    from ..processing import IFFT, create_operation
    from .channel import ChannelFrame

    params = {"n_fft": self.n_fft, "window": self.window}
    operation_name = "ifft"
    logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

    # Create operation instance
    operation = create_operation(operation_name, self.sampling_rate, **params)
    operation = cast("IFFT", operation)
    # Apply processing to data
    time_series = operation.process(self._data)

    logger.debug(
        f"Created new SpectralFrame with operation {operation_name} added to graph"
    )

    # Create new instance
    return ChannelFrame(
        data=time_series,
        sampling_rate=self.sampling_rate,
        label=f"ifft({self.label})",
        metadata=self.metadata,
        operation_history=self.operation_history,
        channel_metadata=self._channel_metadata,
    )
noct_synthesis(fmin, fmax, n=3, G=10, fr=1000)

Synthesize N-octave band spectrum.

This method combines frequency components into N-octave bands according to standard acoustical band definitions. This is commonly used in noise and vibration analysis.

Parameters

fmin : float Lower frequency bound in Hz. fmax : float Upper frequency bound in Hz. n : int, default=3 Number of bands per octave (e.g., 3 for third-octave bands). G : int, default=10 Reference band number. fr : int, default=1000 Reference frequency in Hz.

Returns

NOctFrame A new NOctFrame containing the N-octave band spectrum.

Raises

ValueError If the sampling rate is not 48000 Hz, which is required for this operation.

Source code in wandas/frames/spectral.py
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
def noct_synthesis(
    self,
    fmin: float,
    fmax: float,
    n: int = 3,
    G: int = 10,  # noqa: N803
    fr: int = 1000,
) -> "NOctFrame":
    """
    Synthesize N-octave band spectrum.

    This method combines frequency components into N-octave bands according to
    standard acoustical band definitions. This is commonly used in noise and
    vibration analysis.

    Parameters
    ----------
    fmin : float
        Lower frequency bound in Hz.
    fmax : float
        Upper frequency bound in Hz.
    n : int, default=3
        Number of bands per octave (e.g., 3 for third-octave bands).
    G : int, default=10
        Reference band number.
    fr : int, default=1000
        Reference frequency in Hz.

    Returns
    -------
    NOctFrame
        A new NOctFrame containing the N-octave band spectrum.

    Raises
    ------
    ValueError
        If the sampling rate is not 48000 Hz, which is required for this operation.
    """
    if self.sampling_rate != 48000:
        raise ValueError(
            "noct_synthesis can only be used with a sampling rate of 48000 Hz."
        )
    from ..processing import NOctSynthesis
    from .noct import NOctFrame

    params = {"fmin": fmin, "fmax": fmax, "n": n, "G": G, "fr": fr}
    operation_name = "noct_synthesis"
    logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")
    from ..processing import create_operation

    # Create operation instance
    operation = create_operation(operation_name, self.sampling_rate, **params)
    operation = cast("NOctSynthesis", operation)
    # Apply processing to data
    spectrum_data = operation.process(self._data)

    logger.debug(
        f"Created new SpectralFrame with operation {operation_name} added to graph"
    )

    return NOctFrame(
        data=spectrum_data,
        sampling_rate=self.sampling_rate,
        fmin=fmin,
        fmax=fmax,
        n=n,
        G=G,
        fr=fr,
        label=f"1/{n}Oct of {self.label}",
        metadata={**self.metadata, **params},
        operation_history=[
            *self.operation_history,
            {
                "operation": "noct_synthesis",
                "params": params,
            },
        ],
        channel_metadata=self._channel_metadata,
        previous=self,
    )
plot_matrix(plot_type='matrix', **kwargs)

Plot channel relationships in matrix format.

This method creates a matrix plot showing relationships between channels, such as coherence, transfer functions, or cross-spectral density.

Parameters

plot_type : str, default="matrix" Type of matrix plot to create. **kwargs : dict Additional plot parameters: - vmin, vmax: Color scale limits - cmap: Colormap name - title: Plot title

Returns

Union[Axes, Iterator[Axes]] The matplotlib axes containing the plot.

Source code in wandas/frames/spectral.py
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
def plot_matrix(
    self,
    plot_type: str = "matrix",
    **kwargs: Any,
) -> Union["Axes", Iterator["Axes"]]:
    """
    Plot channel relationships in matrix format.

    This method creates a matrix plot showing relationships between channels,
    such as coherence, transfer functions, or cross-spectral density.

    Parameters
    ----------
    plot_type : str, default="matrix"
        Type of matrix plot to create.
    **kwargs : dict
        Additional plot parameters:
        - vmin, vmax: Color scale limits
        - cmap: Colormap name
        - title: Plot title

    Returns
    -------
    Union[Axes, Iterator[Axes]]
        The matplotlib axes containing the plot.
    """
    from wandas.visualization.plotting import create_operation

    logger.debug(f"Plotting audio with plot_type={plot_type} (will compute now)")

    # Get plot strategy
    plot_strategy: PlotStrategy[SpectralFrame] = create_operation(plot_type)

    # Execute plot
    _ax = plot_strategy.plot(self, **kwargs)

    logger.debug("Plot rendering complete")

    return _ax

spectrogram

Attributes
logger = logging.getLogger(__name__) module-attribute
S = TypeVar('S', bound='BaseFrame[Any]') module-attribute
Classes
SpectrogramFrame

Bases: BaseFrame[NDArrayComplex]

Class for handling time-frequency domain data (spectrograms).

This class represents spectrogram data obtained through Short-Time Fourier Transform (STFT) or similar time-frequency analysis methods. It provides methods for visualization, manipulation, and conversion back to time domain.

Parameters

data : DaArray The spectrogram data. Must be a dask array with shape: - (channels, frequency_bins, time_frames) for multi-channel data - (frequency_bins, time_frames) for single-channel data, which will be reshaped to (1, frequency_bins, time_frames) sampling_rate : float The sampling rate of the original time-domain signal in Hz. n_fft : int The FFT size used to generate this spectrogram. hop_length : int Number of samples between successive frames. win_length : int, optional The window length in samples. If None, defaults to n_fft. window : str, default="hann" The window function to use (e.g., "hann", "hamming", "blackman"). label : str, optional A label for the frame. metadata : dict, optional Additional metadata for the frame. operation_history : list[dict], optional History of operations performed on this frame. channel_metadata : list[ChannelMetadata], optional Metadata for each channel in the frame. previous : BaseFrame, optional The frame that this frame was derived from.

Attributes

magnitude : NDArrayReal The magnitude spectrogram. phase : NDArrayReal The phase spectrogram in radians. power : NDArrayReal The power spectrogram. dB : NDArrayReal The spectrogram in decibels relative to channel reference values. dBA : NDArrayReal The A-weighted spectrogram in decibels. n_frames : int Number of time frames. n_freq_bins : int Number of frequency bins. freqs : NDArrayReal The frequency axis values in Hz. times : NDArrayReal The time axis values in seconds.

Examples

Create a spectrogram from a time-domain signal:

signal = ChannelFrame.from_wav("audio.wav") spectrogram = signal.stft(n_fft=2048, hop_length=512)

Extract a specific time frame:

frame_at_1s = spectrogram.get_frame_at(int(1.0 * sampling_rate / hop_length))

Convert back to time domain:

reconstructed = spectrogram.to_channel_frame()

Plot the spectrogram:

spectrogram.plot()

Source code in wandas/frames/spectrogram.py
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
class SpectrogramFrame(BaseFrame[NDArrayComplex]):
    """
    Class for handling time-frequency domain data (spectrograms).

    This class represents spectrogram data obtained through
    Short-Time Fourier Transform (STFT)
    or similar time-frequency analysis methods. It provides methods for visualization,
    manipulation, and conversion back to time domain.

    Parameters
    ----------
    data : DaArray
        The spectrogram data. Must be a dask array with shape:
        - (channels, frequency_bins, time_frames) for multi-channel data
        - (frequency_bins, time_frames) for single-channel data, which will be
          reshaped to (1, frequency_bins, time_frames)
    sampling_rate : float
        The sampling rate of the original time-domain signal in Hz.
    n_fft : int
        The FFT size used to generate this spectrogram.
    hop_length : int
        Number of samples between successive frames.
    win_length : int, optional
        The window length in samples. If None, defaults to n_fft.
    window : str, default="hann"
        The window function to use (e.g., "hann", "hamming", "blackman").
    label : str, optional
        A label for the frame.
    metadata : dict, optional
        Additional metadata for the frame.
    operation_history : list[dict], optional
        History of operations performed on this frame.
    channel_metadata : list[ChannelMetadata], optional
        Metadata for each channel in the frame.
    previous : BaseFrame, optional
        The frame that this frame was derived from.

    Attributes
    ----------
    magnitude : NDArrayReal
        The magnitude spectrogram.
    phase : NDArrayReal
        The phase spectrogram in radians.
    power : NDArrayReal
        The power spectrogram.
    dB : NDArrayReal
        The spectrogram in decibels relative to channel reference values.
    dBA : NDArrayReal
        The A-weighted spectrogram in decibels.
    n_frames : int
        Number of time frames.
    n_freq_bins : int
        Number of frequency bins.
    freqs : NDArrayReal
        The frequency axis values in Hz.
    times : NDArrayReal
        The time axis values in seconds.

    Examples
    --------
    Create a spectrogram from a time-domain signal:
    >>> signal = ChannelFrame.from_wav("audio.wav")
    >>> spectrogram = signal.stft(n_fft=2048, hop_length=512)

    Extract a specific time frame:
    >>> frame_at_1s = spectrogram.get_frame_at(int(1.0 * sampling_rate / hop_length))

    Convert back to time domain:
    >>> reconstructed = spectrogram.to_channel_frame()

    Plot the spectrogram:
    >>> spectrogram.plot()
    """

    n_fft: int
    hop_length: int
    win_length: int
    window: str

    def __init__(
        self,
        data: DaArray,
        sampling_rate: float,
        n_fft: int,
        hop_length: int,
        win_length: Optional[int] = None,
        window: str = "hann",
        label: Optional[str] = None,
        metadata: Optional[dict[str, Any]] = None,
        operation_history: Optional[list[dict[str, Any]]] = None,
        channel_metadata: Optional[list[ChannelMetadata]] = None,
        previous: Optional["BaseFrame[Any]"] = None,
    ) -> None:
        if data.ndim == 2:
            data = da.expand_dims(data, axis=0)  # type: ignore [unused-ignore]
        elif data.ndim != 3:
            raise ValueError(
                f"データは2次元または3次元である必要があります。形状: {data.shape}"
            )
        if not data.shape[-2] == n_fft // 2 + 1:
            raise ValueError(
                f"データの形状が無効です。周波数ビン数は {n_fft // 2 + 1} である必要があります。"  # noqa: E501
            )

        self.n_fft = n_fft
        self.hop_length = hop_length
        self.win_length = win_length if win_length is not None else n_fft
        self.window = window
        super().__init__(
            data=data,
            sampling_rate=sampling_rate,
            label=label,
            metadata=metadata,
            operation_history=operation_history,
            channel_metadata=channel_metadata,
            previous=previous,
        )

    @property
    def magnitude(self) -> NDArrayReal:
        """
        Get the magnitude spectrogram.

        Returns
        -------
        NDArrayReal
            The absolute values of the complex spectrogram.
        """
        return np.abs(self.data)

    @property
    def phase(self) -> NDArrayReal:
        """
        Get the phase spectrogram.

        Returns
        -------
        NDArrayReal
            The phase angles of the complex spectrogram in radians.
        """
        return np.angle(self.data)

    @property
    def power(self) -> NDArrayReal:
        """
        Get the power spectrogram.

        Returns
        -------
        NDArrayReal
            The squared magnitude of the complex spectrogram.
        """
        return np.abs(self.data) ** 2

    @property
    def dB(self) -> NDArrayReal:  # noqa: N802
        """
        Get the spectrogram in decibels relative to each channel's reference value.

        The reference value for each channel is specified in its metadata.
        A minimum value of -120 dB is enforced to avoid numerical issues.

        Returns
        -------
        NDArrayReal
            The spectrogram in decibels.
        """
        # dB規定値を_channel_metadataから収集
        ref = np.array([ch.ref for ch in self._channel_metadata])
        # dB変換
        # 0除算を避けるために、最大値と1e-12のいずれかを使用
        level: NDArrayReal = 20 * np.log10(
            np.maximum(self.magnitude / ref[..., np.newaxis, np.newaxis], 1e-12)
        )
        return level

    @property
    def dBA(self) -> NDArrayReal:  # noqa: N802
        """
        Get the A-weighted spectrogram in decibels.

        A-weighting applies a frequency-dependent weighting filter that approximates
        the human ear's response. This is particularly useful for analyzing noise
        and acoustic measurements.

        Returns
        -------
        NDArrayReal
            The A-weighted spectrogram in decibels.
        """
        weighted: NDArrayReal = librosa.A_weighting(frequencies=self.freqs, min_db=None)
        return self.dB + weighted[:, np.newaxis]  # 周波数軸に沿ってブロードキャスト

    @property
    def _n_channels(self) -> int:
        """
        Get the number of channels in the data.

        Returns
        -------
        int
            The number of channels.
        """
        return int(self._data.shape[-3])

    @property
    def n_frames(self) -> int:
        """
        Get the number of time frames.

        Returns
        -------
        int
            The number of time frames in the spectrogram.
        """
        return self.shape[-1]

    @property
    def n_freq_bins(self) -> int:
        """
        Get the number of frequency bins.

        Returns
        -------
        int
            The number of frequency bins (n_fft // 2 + 1).
        """
        return self.shape[-2]

    @property
    def freqs(self) -> NDArrayReal:
        """
        Get the frequency axis values in Hz.

        Returns
        -------
        NDArrayReal
            Array of frequency values corresponding to each frequency bin.
        """
        return np.fft.rfftfreq(self.n_fft, 1.0 / self.sampling_rate)

    @property
    def times(self) -> NDArrayReal:
        """
        Get the time axis values in seconds.

        Returns
        -------
        NDArrayReal
            Array of time values corresponding to each time frame.
        """
        return np.arange(self.n_frames) * self.hop_length / self.sampling_rate

    def _apply_operation_impl(self: S, operation_name: str, **params: Any) -> S:
        """
        Implementation of operation application for spectrogram data.

        This internal method handles the application of various operations to
        spectrogram data, maintaining lazy evaluation through dask.

        Parameters
        ----------
        operation_name : str
            Name of the operation to apply.
        **params : Any
            Parameters for the operation.

        Returns
        -------
        S
            A new instance with the operation applied.
        """
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")
        from wandas.processing import create_operation

        operation = create_operation(operation_name, self.sampling_rate, **params)
        processed_data = operation.process(self._data)

        operation_metadata = {"operation": operation_name, "params": params}
        new_history = self.operation_history.copy()
        new_history.append(operation_metadata)
        new_metadata = {**self.metadata}
        new_metadata[operation_name] = params

        logger.debug(
            f"Created new SpectrogramFrame with operation {operation_name} added to graph"  # noqa: E501
        )
        return self._create_new_instance(
            data=processed_data,
            metadata=new_metadata,
            operation_history=new_history,
        )

    def _binary_op(
        self,
        other: Union[
            "SpectrogramFrame",
            int,
            float,
            complex,
            NDArrayComplex,
            NDArrayReal,
            "DaArray",
        ],
        op: Callable[["DaArray", Any], "DaArray"],
        symbol: str,
    ) -> "SpectrogramFrame":
        """
        Common implementation for binary operations.

        This method handles binary operations between
        SpectrogramFrames and various types
        of operands, maintaining lazy evaluation through dask arrays.

        Parameters
        ----------
        other : Union[SpectrogramFrame, int, float, complex,
            NDArrayComplex, NDArrayReal, DaArray]
            The right operand of the operation.
        op : callable
            Function to execute the operation (e.g., lambda a, b: a + b)
        symbol : str
            String representation of the operation (e.g., '+')

        Returns
        -------
        SpectrogramFrame
            A new SpectrogramFrame containing the result of the operation.

        Raises
        ------
        ValueError
            If attempting to operate with a SpectrogramFrame
            with a different sampling rate.
        """
        logger.debug(f"Setting up {symbol} operation (lazy)")

        metadata = {}
        if self.metadata is not None:
            metadata = self.metadata.copy()

        operation_history = []
        if self.operation_history is not None:
            operation_history = self.operation_history.copy()

        if isinstance(other, SpectrogramFrame):
            if self.sampling_rate != other.sampling_rate:
                raise ValueError(
                    "サンプリングレートが一致していません。演算できません。"
                )

            result_data = op(self._data, other._data)

            merged_channel_metadata = []
            for self_ch, other_ch in zip(
                self._channel_metadata, other._channel_metadata
            ):
                ch = self_ch.model_copy(deep=True)
                ch["label"] = f"({self_ch['label']} {symbol} {other_ch['label']})"
                merged_channel_metadata.append(ch)

            operation_history.append({"operation": symbol, "with": other.label})

            return SpectrogramFrame(
                data=result_data,
                sampling_rate=self.sampling_rate,
                n_fft=self.n_fft,
                hop_length=self.hop_length,
                win_length=self.win_length,
                window=self.window,
                label=f"({self.label} {symbol} {other.label})",
                metadata=metadata,
                operation_history=operation_history,
                channel_metadata=merged_channel_metadata,
                previous=self,
            )
        else:
            result_data = op(self._data, other)

            if isinstance(other, (int, float)):
                other_str = str(other)
            elif isinstance(other, complex):
                other_str = f"complex({other.real}, {other.imag})"
            elif isinstance(other, np.ndarray):
                other_str = f"ndarray{other.shape}"
            elif hasattr(other, "shape"):
                other_str = f"dask.array{other.shape}"
            else:
                other_str = str(type(other).__name__)

            updated_channel_metadata: list[ChannelMetadata] = []
            for self_ch in self._channel_metadata:
                ch = self_ch.model_copy(deep=True)
                ch["label"] = f"({self_ch.label} {symbol} {other_str})"
                updated_channel_metadata.append(ch)

            operation_history.append({"operation": symbol, "with": other_str})

            return SpectrogramFrame(
                data=result_data,
                sampling_rate=self.sampling_rate,
                n_fft=self.n_fft,
                hop_length=self.hop_length,
                win_length=self.win_length,
                window=self.window,
                label=f"({self.label} {symbol} {other_str})",
                metadata=metadata,
                operation_history=operation_history,
                channel_metadata=updated_channel_metadata,
            )

    def plot(
        self, plot_type: str = "spectrogram", ax: Optional["Axes"] = None, **kwargs: Any
    ) -> Union["Axes", Iterator["Axes"]]:
        """
        Plot the spectrogram using various visualization strategies.

        Parameters
        ----------
        plot_type : str, default="spectrogram"
            Type of plot to create.
        ax : matplotlib.axes.Axes, optional
            Axes to plot on. If None, creates new axes.
        **kwargs : dict
            Additional keyword arguments passed to the plot strategy.
            Common options include:
            - vmin, vmax: Colormap scaling
            - cmap: Colormap name
            - dB: Whether to plot in decibels
            - Aw: Whether to apply A-weighting

        Returns
        -------
        Union[Axes, Iterator[Axes]]
            The matplotlib axes containing the plot, or an iterator of axes
            for multi-plot outputs.
        """
        from wandas.visualization.plotting import create_operation

        logger.debug(f"Plotting audio with plot_type={plot_type} (will compute now)")

        # プロット戦略を取得
        plot_strategy: PlotStrategy[SpectrogramFrame] = create_operation(plot_type)

        # プロット実行
        _ax = plot_strategy.plot(self, ax=ax, **kwargs)

        logger.debug("Plot rendering complete")

        return _ax

    def plot_Aw(  # noqa: N802
        self, plot_type: str = "spectrogram", ax: Optional["Axes"] = None, **kwargs: Any
    ) -> Union["Axes", Iterator["Axes"]]:
        """
        Plot the A-weighted spectrogram.

        A convenience method that calls plot() with Aw=True, applying A-weighting
        to the spectrogram before plotting.

        Parameters
        ----------
        plot_type : str, default="spectrogram"
            Type of plot to create.
        ax : matplotlib.axes.Axes, optional
            Axes to plot on. If None, creates new axes.
        **kwargs : dict
            Additional keyword arguments passed to plot().

        Returns
        -------
        Union[Axes, Iterator[Axes]]
            The matplotlib axes containing the plot.
        """
        return self.plot(plot_type=plot_type, ax=ax, Aw=True, **kwargs)

    def abs(self) -> "SpectrogramFrame":
        """
        Compute the absolute value (magnitude) of the complex spectrogram.

        This method calculates the magnitude of each complex value in the
        spectrogram, converting the complex-valued data to real-valued magnitude data.
        The result is stored in a new SpectrogramFrame with complex dtype to maintain
        compatibility with other spectrogram operations.

        Returns
        -------
        SpectrogramFrame
            A new SpectrogramFrame containing the magnitude values as complex numbers
            (with zero imaginary parts).

        Examples
        --------
        >>> signal = ChannelFrame.from_wav("audio.wav")
        >>> spectrogram = signal.stft(n_fft=2048, hop_length=512)
        >>> magnitude_spectrogram = spectrogram.abs()
        >>> # The magnitude can be accessed via the magnitude property or data
        >>> print(magnitude_spectrogram.magnitude.shape)
        """
        logger.debug("Computing absolute value (magnitude) of spectrogram")

        # Compute the absolute value using dask for lazy evaluation
        magnitude_data = da.absolute(self._data)

        # Update operation history
        operation_metadata = {"operation": "abs", "params": {}}
        new_history = self.operation_history.copy()
        new_history.append(operation_metadata)
        new_metadata = {**self.metadata}
        new_metadata["abs"] = {}

        logger.debug("Created new SpectrogramFrame with abs operation added to graph")

        return SpectrogramFrame(
            data=magnitude_data,
            sampling_rate=self.sampling_rate,
            n_fft=self.n_fft,
            hop_length=self.hop_length,
            win_length=self.win_length,
            window=self.window,
            label=f"abs({self.label})",
            metadata=new_metadata,
            operation_history=new_history,
            channel_metadata=self._channel_metadata,
            previous=self,
        )

    def get_frame_at(self, time_idx: int) -> "SpectralFrame":
        """
        Extract spectral data at a specific time frame.

        Parameters
        ----------
        time_idx : int
            Index of the time frame to extract.

        Returns
        -------
        SpectralFrame
            A new SpectralFrame containing the spectral data at the specified time.

        Raises
        ------
        IndexError
            If time_idx is out of range.
        """
        from wandas.frames.spectral import SpectralFrame

        if time_idx < 0 or time_idx >= self.n_frames:
            raise IndexError(
                f"時間インデックス {time_idx} が範囲外です。有効範囲: 0-{self.n_frames - 1}"  # noqa: E501
            )

        frame_data = self._data[..., time_idx]

        return SpectralFrame(
            data=frame_data,
            sampling_rate=self.sampling_rate,
            n_fft=self.n_fft,
            window=self.window,
            label=f"{self.label} (Frame {time_idx}, Time {self.times[time_idx]:.3f}s)",
            metadata=self.metadata,
            operation_history=self.operation_history,
            channel_metadata=self._channel_metadata,
        )

    def to_channel_frame(self) -> "ChannelFrame":
        """
        Convert the spectrogram back to time domain using inverse STFT.

        This method performs an inverse Short-Time Fourier Transform (ISTFT) to
        reconstruct the time-domain signal from the spectrogram.

        Returns
        -------
        ChannelFrame
            A new ChannelFrame containing the reconstructed time-domain signal.

        See Also
        --------
        istft : Alias for this method with more intuitive naming.
        """
        from wandas.frames.channel import ChannelFrame
        from wandas.processing import ISTFT, create_operation

        params = {
            "n_fft": self.n_fft,
            "hop_length": self.hop_length,
            "win_length": self.win_length,
            "window": self.window,
        }
        operation_name = "istft"
        logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

        # 操作インスタンスを作成
        operation = create_operation(operation_name, self.sampling_rate, **params)
        operation = cast("ISTFT", operation)
        # データに処理を適用
        time_series = operation.process(self._data)

        logger.debug(
            f"Created new ChannelFrame with operation {operation_name} added to graph"
        )

        # 新しいインスタンスを作成
        return ChannelFrame(
            data=time_series,
            sampling_rate=self.sampling_rate,
            label=f"istft({self.label})",
            metadata=self.metadata,
            operation_history=self.operation_history,
            channel_metadata=self._channel_metadata,
        )

    def istft(self) -> "ChannelFrame":
        """
        Convert the spectrogram back to time domain using inverse STFT.

        This is an alias for `to_channel_frame()` with a more intuitive name.
        It performs an inverse Short-Time Fourier Transform (ISTFT) to
        reconstruct the time-domain signal from the spectrogram.

        Returns
        -------
        ChannelFrame
            A new ChannelFrame containing the reconstructed time-domain signal.

        See Also
        --------
        to_channel_frame : The underlying implementation.

        Examples
        --------
        >>> signal = ChannelFrame.from_wav("audio.wav")
        >>> spectrogram = signal.stft(n_fft=2048, hop_length=512)
        >>> reconstructed = spectrogram.istft()
        """
        return self.to_channel_frame()

    def _get_additional_init_kwargs(self) -> dict[str, Any]:
        """
        Get additional initialization arguments for SpectrogramFrame.

        This internal method provides the additional initialization arguments
        required by SpectrogramFrame beyond those required by BaseFrame.

        Returns
        -------
        dict[str, Any]
            Additional initialization arguments.
        """
        return {
            "n_fft": self.n_fft,
            "hop_length": self.hop_length,
            "win_length": self.win_length,
            "window": self.window,
        }
Attributes
n_fft = n_fft instance-attribute
hop_length = hop_length instance-attribute
win_length = win_length if win_length is not None else n_fft instance-attribute
window = window instance-attribute
magnitude property

Get the magnitude spectrogram.

Returns

NDArrayReal The absolute values of the complex spectrogram.

phase property

Get the phase spectrogram.

Returns

NDArrayReal The phase angles of the complex spectrogram in radians.

power property

Get the power spectrogram.

Returns

NDArrayReal The squared magnitude of the complex spectrogram.

dB property

Get the spectrogram in decibels relative to each channel's reference value.

The reference value for each channel is specified in its metadata. A minimum value of -120 dB is enforced to avoid numerical issues.

Returns

NDArrayReal The spectrogram in decibels.

dBA property

Get the A-weighted spectrogram in decibels.

A-weighting applies a frequency-dependent weighting filter that approximates the human ear's response. This is particularly useful for analyzing noise and acoustic measurements.

Returns

NDArrayReal The A-weighted spectrogram in decibels.

n_frames property

Get the number of time frames.

Returns

int The number of time frames in the spectrogram.

n_freq_bins property

Get the number of frequency bins.

Returns

int The number of frequency bins (n_fft // 2 + 1).

freqs property

Get the frequency axis values in Hz.

Returns

NDArrayReal Array of frequency values corresponding to each frequency bin.

times property

Get the time axis values in seconds.

Returns

NDArrayReal Array of time values corresponding to each time frame.

Functions
__init__(data, sampling_rate, n_fft, hop_length, win_length=None, window='hann', label=None, metadata=None, operation_history=None, channel_metadata=None, previous=None)
Source code in wandas/frames/spectrogram.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
def __init__(
    self,
    data: DaArray,
    sampling_rate: float,
    n_fft: int,
    hop_length: int,
    win_length: Optional[int] = None,
    window: str = "hann",
    label: Optional[str] = None,
    metadata: Optional[dict[str, Any]] = None,
    operation_history: Optional[list[dict[str, Any]]] = None,
    channel_metadata: Optional[list[ChannelMetadata]] = None,
    previous: Optional["BaseFrame[Any]"] = None,
) -> None:
    if data.ndim == 2:
        data = da.expand_dims(data, axis=0)  # type: ignore [unused-ignore]
    elif data.ndim != 3:
        raise ValueError(
            f"データは2次元または3次元である必要があります。形状: {data.shape}"
        )
    if not data.shape[-2] == n_fft // 2 + 1:
        raise ValueError(
            f"データの形状が無効です。周波数ビン数は {n_fft // 2 + 1} である必要があります。"  # noqa: E501
        )

    self.n_fft = n_fft
    self.hop_length = hop_length
    self.win_length = win_length if win_length is not None else n_fft
    self.window = window
    super().__init__(
        data=data,
        sampling_rate=sampling_rate,
        label=label,
        metadata=metadata,
        operation_history=operation_history,
        channel_metadata=channel_metadata,
        previous=previous,
    )
plot(plot_type='spectrogram', ax=None, **kwargs)

Plot the spectrogram using various visualization strategies.

Parameters

plot_type : str, default="spectrogram" Type of plot to create. ax : matplotlib.axes.Axes, optional Axes to plot on. If None, creates new axes. **kwargs : dict Additional keyword arguments passed to the plot strategy. Common options include: - vmin, vmax: Colormap scaling - cmap: Colormap name - dB: Whether to plot in decibels - Aw: Whether to apply A-weighting

Returns

Union[Axes, Iterator[Axes]] The matplotlib axes containing the plot, or an iterator of axes for multi-plot outputs.

Source code in wandas/frames/spectrogram.py
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
def plot(
    self, plot_type: str = "spectrogram", ax: Optional["Axes"] = None, **kwargs: Any
) -> Union["Axes", Iterator["Axes"]]:
    """
    Plot the spectrogram using various visualization strategies.

    Parameters
    ----------
    plot_type : str, default="spectrogram"
        Type of plot to create.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, creates new axes.
    **kwargs : dict
        Additional keyword arguments passed to the plot strategy.
        Common options include:
        - vmin, vmax: Colormap scaling
        - cmap: Colormap name
        - dB: Whether to plot in decibels
        - Aw: Whether to apply A-weighting

    Returns
    -------
    Union[Axes, Iterator[Axes]]
        The matplotlib axes containing the plot, or an iterator of axes
        for multi-plot outputs.
    """
    from wandas.visualization.plotting import create_operation

    logger.debug(f"Plotting audio with plot_type={plot_type} (will compute now)")

    # プロット戦略を取得
    plot_strategy: PlotStrategy[SpectrogramFrame] = create_operation(plot_type)

    # プロット実行
    _ax = plot_strategy.plot(self, ax=ax, **kwargs)

    logger.debug("Plot rendering complete")

    return _ax
plot_Aw(plot_type='spectrogram', ax=None, **kwargs)

Plot the A-weighted spectrogram.

A convenience method that calls plot() with Aw=True, applying A-weighting to the spectrogram before plotting.

Parameters

plot_type : str, default="spectrogram" Type of plot to create. ax : matplotlib.axes.Axes, optional Axes to plot on. If None, creates new axes. **kwargs : dict Additional keyword arguments passed to plot().

Returns

Union[Axes, Iterator[Axes]] The matplotlib axes containing the plot.

Source code in wandas/frames/spectrogram.py
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
def plot_Aw(  # noqa: N802
    self, plot_type: str = "spectrogram", ax: Optional["Axes"] = None, **kwargs: Any
) -> Union["Axes", Iterator["Axes"]]:
    """
    Plot the A-weighted spectrogram.

    A convenience method that calls plot() with Aw=True, applying A-weighting
    to the spectrogram before plotting.

    Parameters
    ----------
    plot_type : str, default="spectrogram"
        Type of plot to create.
    ax : matplotlib.axes.Axes, optional
        Axes to plot on. If None, creates new axes.
    **kwargs : dict
        Additional keyword arguments passed to plot().

    Returns
    -------
    Union[Axes, Iterator[Axes]]
        The matplotlib axes containing the plot.
    """
    return self.plot(plot_type=plot_type, ax=ax, Aw=True, **kwargs)
abs()

Compute the absolute value (magnitude) of the complex spectrogram.

This method calculates the magnitude of each complex value in the spectrogram, converting the complex-valued data to real-valued magnitude data. The result is stored in a new SpectrogramFrame with complex dtype to maintain compatibility with other spectrogram operations.

Returns

SpectrogramFrame A new SpectrogramFrame containing the magnitude values as complex numbers (with zero imaginary parts).

Examples

signal = ChannelFrame.from_wav("audio.wav") spectrogram = signal.stft(n_fft=2048, hop_length=512) magnitude_spectrogram = spectrogram.abs()

The magnitude can be accessed via the magnitude property or data

print(magnitude_spectrogram.magnitude.shape)

Source code in wandas/frames/spectrogram.py
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
def abs(self) -> "SpectrogramFrame":
    """
    Compute the absolute value (magnitude) of the complex spectrogram.

    This method calculates the magnitude of each complex value in the
    spectrogram, converting the complex-valued data to real-valued magnitude data.
    The result is stored in a new SpectrogramFrame with complex dtype to maintain
    compatibility with other spectrogram operations.

    Returns
    -------
    SpectrogramFrame
        A new SpectrogramFrame containing the magnitude values as complex numbers
        (with zero imaginary parts).

    Examples
    --------
    >>> signal = ChannelFrame.from_wav("audio.wav")
    >>> spectrogram = signal.stft(n_fft=2048, hop_length=512)
    >>> magnitude_spectrogram = spectrogram.abs()
    >>> # The magnitude can be accessed via the magnitude property or data
    >>> print(magnitude_spectrogram.magnitude.shape)
    """
    logger.debug("Computing absolute value (magnitude) of spectrogram")

    # Compute the absolute value using dask for lazy evaluation
    magnitude_data = da.absolute(self._data)

    # Update operation history
    operation_metadata = {"operation": "abs", "params": {}}
    new_history = self.operation_history.copy()
    new_history.append(operation_metadata)
    new_metadata = {**self.metadata}
    new_metadata["abs"] = {}

    logger.debug("Created new SpectrogramFrame with abs operation added to graph")

    return SpectrogramFrame(
        data=magnitude_data,
        sampling_rate=self.sampling_rate,
        n_fft=self.n_fft,
        hop_length=self.hop_length,
        win_length=self.win_length,
        window=self.window,
        label=f"abs({self.label})",
        metadata=new_metadata,
        operation_history=new_history,
        channel_metadata=self._channel_metadata,
        previous=self,
    )
get_frame_at(time_idx)

Extract spectral data at a specific time frame.

Parameters

time_idx : int Index of the time frame to extract.

Returns

SpectralFrame A new SpectralFrame containing the spectral data at the specified time.

Raises

IndexError If time_idx is out of range.

Source code in wandas/frames/spectrogram.py
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
def get_frame_at(self, time_idx: int) -> "SpectralFrame":
    """
    Extract spectral data at a specific time frame.

    Parameters
    ----------
    time_idx : int
        Index of the time frame to extract.

    Returns
    -------
    SpectralFrame
        A new SpectralFrame containing the spectral data at the specified time.

    Raises
    ------
    IndexError
        If time_idx is out of range.
    """
    from wandas.frames.spectral import SpectralFrame

    if time_idx < 0 or time_idx >= self.n_frames:
        raise IndexError(
            f"時間インデックス {time_idx} が範囲外です。有効範囲: 0-{self.n_frames - 1}"  # noqa: E501
        )

    frame_data = self._data[..., time_idx]

    return SpectralFrame(
        data=frame_data,
        sampling_rate=self.sampling_rate,
        n_fft=self.n_fft,
        window=self.window,
        label=f"{self.label} (Frame {time_idx}, Time {self.times[time_idx]:.3f}s)",
        metadata=self.metadata,
        operation_history=self.operation_history,
        channel_metadata=self._channel_metadata,
    )
to_channel_frame()

Convert the spectrogram back to time domain using inverse STFT.

This method performs an inverse Short-Time Fourier Transform (ISTFT) to reconstruct the time-domain signal from the spectrogram.

Returns

ChannelFrame A new ChannelFrame containing the reconstructed time-domain signal.

See Also

istft : Alias for this method with more intuitive naming.

Source code in wandas/frames/spectrogram.py
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
def to_channel_frame(self) -> "ChannelFrame":
    """
    Convert the spectrogram back to time domain using inverse STFT.

    This method performs an inverse Short-Time Fourier Transform (ISTFT) to
    reconstruct the time-domain signal from the spectrogram.

    Returns
    -------
    ChannelFrame
        A new ChannelFrame containing the reconstructed time-domain signal.

    See Also
    --------
    istft : Alias for this method with more intuitive naming.
    """
    from wandas.frames.channel import ChannelFrame
    from wandas.processing import ISTFT, create_operation

    params = {
        "n_fft": self.n_fft,
        "hop_length": self.hop_length,
        "win_length": self.win_length,
        "window": self.window,
    }
    operation_name = "istft"
    logger.debug(f"Applying operation={operation_name} with params={params} (lazy)")

    # 操作インスタンスを作成
    operation = create_operation(operation_name, self.sampling_rate, **params)
    operation = cast("ISTFT", operation)
    # データに処理を適用
    time_series = operation.process(self._data)

    logger.debug(
        f"Created new ChannelFrame with operation {operation_name} added to graph"
    )

    # 新しいインスタンスを作成
    return ChannelFrame(
        data=time_series,
        sampling_rate=self.sampling_rate,
        label=f"istft({self.label})",
        metadata=self.metadata,
        operation_history=self.operation_history,
        channel_metadata=self._channel_metadata,
    )
istft()

Convert the spectrogram back to time domain using inverse STFT.

This is an alias for to_channel_frame() with a more intuitive name. It performs an inverse Short-Time Fourier Transform (ISTFT) to reconstruct the time-domain signal from the spectrogram.

Returns

ChannelFrame A new ChannelFrame containing the reconstructed time-domain signal.

See Also

to_channel_frame : The underlying implementation.

Examples

signal = ChannelFrame.from_wav("audio.wav") spectrogram = signal.stft(n_fft=2048, hop_length=512) reconstructed = spectrogram.istft()

Source code in wandas/frames/spectrogram.py
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
def istft(self) -> "ChannelFrame":
    """
    Convert the spectrogram back to time domain using inverse STFT.

    This is an alias for `to_channel_frame()` with a more intuitive name.
    It performs an inverse Short-Time Fourier Transform (ISTFT) to
    reconstruct the time-domain signal from the spectrogram.

    Returns
    -------
    ChannelFrame
        A new ChannelFrame containing the reconstructed time-domain signal.

    See Also
    --------
    to_channel_frame : The underlying implementation.

    Examples
    --------
    >>> signal = ChannelFrame.from_wav("audio.wav")
    >>> spectrogram = signal.stft(n_fft=2048, hop_length=512)
    >>> reconstructed = spectrogram.istft()
    """
    return self.to_channel_frame()

処理モジュール

処理モジュールはオーディオデータに対する様々な処理機能を提供します。

wandas.processing

Audio time series processing operations.

This module provides audio processing operations for time series data.

Attributes

__all__ = ['AudioOperation', '_OPERATION_REGISTRY', 'create_operation', 'get_operation', 'register_operation', 'AWeighting', 'HighPassFilter', 'LowPassFilter', 'CSD', 'Coherence', 'FFT', 'IFFT', 'ISTFT', 'NOctSpectrum', 'NOctSynthesis', 'STFT', 'TransferFunction', 'Welch', 'ReSampling', 'RmsTrend', 'Trim', 'AddWithSNR', 'HpssHarmonic', 'HpssPercussive', 'ABS', 'ChannelDifference', 'Mean', 'Power', 'Sum'] module-attribute

Classes

AudioOperation

Bases: Generic[InputArrayType, OutputArrayType]

Abstract base class for audio processing operations.

Source code in wandas/processing/base.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
class AudioOperation(Generic[InputArrayType, OutputArrayType]):
    """Abstract base class for audio processing operations."""

    # Class variable: operation name
    name: ClassVar[str]

    def __init__(self, sampling_rate: float, **params: Any):
        """
        Initialize AudioOperation.

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        **params : Any
            Operation-specific parameters
        """
        self.sampling_rate = sampling_rate
        self.params = params

        # Validate parameters during initialization
        self.validate_params()

        # Create processor function (lazy initialization possible)
        self._setup_processor()

        logger.debug(
            f"Initialized {self.__class__.__name__} operation with params: {params}"
        )

    def validate_params(self) -> None:
        """Validate parameters (raises exception if invalid)"""
        pass

    def _setup_processor(self) -> None:
        """Set up processor function (implemented by subclasses)"""
        pass

    def _process_array(self, x: InputArrayType) -> OutputArrayType:
        """Processing function (implemented by subclasses)"""
        # Default is no-op function
        raise NotImplementedError("Subclasses must implement this method.")

    @dask.delayed  # type: ignore [misc, unused-ignore]
    def process_array(self, x: InputArrayType) -> OutputArrayType:
        """Processing function wrapped with @dask.delayed"""
        # Default is no-op function
        logger.debug(f"Default process operation on data with shape: {x.shape}")
        return self._process_array(x)

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation (implemented by subclasses)

        Parameters
        ----------
        input_shape : tuple
            Input data shape

        Returns
        -------
        tuple
            Output data shape
        """
        raise NotImplementedError("Subclasses must implement this method.")

    def process(self, data: DaArray) -> DaArray:
        """
        Execute operation and return result
        data shape is (channels, samples)
        """
        # Add task as delayed processing
        logger.debug("Adding delayed operation to computation graph")
        delayed_result = self.process_array(data)
        # Convert delayed result to dask array and return
        output_shape = self.calculate_output_shape(data.shape)
        return _da_from_delayed(delayed_result, shape=output_shape, dtype=data.dtype)
Attributes
name class-attribute
sampling_rate = sampling_rate instance-attribute
params = params instance-attribute
Functions
__init__(sampling_rate, **params)

Initialize AudioOperation.

Parameters

sampling_rate : float Sampling rate (Hz) **params : Any Operation-specific parameters

Source code in wandas/processing/base.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def __init__(self, sampling_rate: float, **params: Any):
    """
    Initialize AudioOperation.

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    **params : Any
        Operation-specific parameters
    """
    self.sampling_rate = sampling_rate
    self.params = params

    # Validate parameters during initialization
    self.validate_params()

    # Create processor function (lazy initialization possible)
    self._setup_processor()

    logger.debug(
        f"Initialized {self.__class__.__name__} operation with params: {params}"
    )
validate_params()

Validate parameters (raises exception if invalid)

Source code in wandas/processing/base.py
50
51
52
def validate_params(self) -> None:
    """Validate parameters (raises exception if invalid)"""
    pass
process_array(x)

Processing function wrapped with @dask.delayed

Source code in wandas/processing/base.py
63
64
65
66
67
68
@dask.delayed  # type: ignore [misc, unused-ignore]
def process_array(self, x: InputArrayType) -> OutputArrayType:
    """Processing function wrapped with @dask.delayed"""
    # Default is no-op function
    logger.debug(f"Default process operation on data with shape: {x.shape}")
    return self._process_array(x)
calculate_output_shape(input_shape)

Calculate output data shape after operation (implemented by subclasses)

Parameters

input_shape : tuple Input data shape

Returns

tuple Output data shape

Source code in wandas/processing/base.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation (implemented by subclasses)

    Parameters
    ----------
    input_shape : tuple
        Input data shape

    Returns
    -------
    tuple
        Output data shape
    """
    raise NotImplementedError("Subclasses must implement this method.")
process(data)

Execute operation and return result data shape is (channels, samples)

Source code in wandas/processing/base.py
86
87
88
89
90
91
92
93
94
95
96
def process(self, data: DaArray) -> DaArray:
    """
    Execute operation and return result
    data shape is (channels, samples)
    """
    # Add task as delayed processing
    logger.debug("Adding delayed operation to computation graph")
    delayed_result = self.process_array(data)
    # Convert delayed result to dask array and return
    output_shape = self.calculate_output_shape(data.shape)
    return _da_from_delayed(delayed_result, shape=output_shape, dtype=data.dtype)

AddWithSNR

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Addition operation considering SNR

Source code in wandas/processing/effects.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
class AddWithSNR(AudioOperation[NDArrayReal, NDArrayReal]):
    """Addition operation considering SNR"""

    name = "add_with_snr"

    def __init__(self, sampling_rate: float, other: DaArray, snr: float = 1.0):
        """
        Initialize addition operation considering SNR

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        other : DaArray
            Noise signal to add (channel-frame format)
        snr : float
            Signal-to-noise ratio (dB)
        """
        super().__init__(sampling_rate, other=other, snr=snr)

        self.other = other
        self.snr = snr
        logger.debug(f"Initialized AddWithSNR operation with SNR: {snr} dB")

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape

        Returns
        -------
        tuple
            Output data shape (same as input)
        """
        return input_shape

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Perform addition processing considering SNR"""
        logger.debug(f"Applying SNR-based addition with shape: {x.shape}")
        other: NDArrayReal = self.other.compute()

        # Use multi-channel versions of calculate_rms and calculate_desired_noise_rms
        clean_rms = util.calculate_rms(x)
        other_rms = util.calculate_rms(other)

        # Adjust noise gain based on specified SNR (apply per channel)
        desired_noise_rms = util.calculate_desired_noise_rms(clean_rms, self.snr)

        # Apply gain per channel using broadcasting
        gain = desired_noise_rms / other_rms
        # Add adjusted noise to signal
        result: NDArrayReal = x + other * gain
        return result
Attributes
name = 'add_with_snr' class-attribute instance-attribute
other = other instance-attribute
snr = snr instance-attribute
Functions
__init__(sampling_rate, other, snr=1.0)

Initialize addition operation considering SNR

Parameters

sampling_rate : float Sampling rate (Hz) other : DaArray Noise signal to add (channel-frame format) snr : float Signal-to-noise ratio (dB)

Source code in wandas/processing/effects.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def __init__(self, sampling_rate: float, other: DaArray, snr: float = 1.0):
    """
    Initialize addition operation considering SNR

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    other : DaArray
        Noise signal to add (channel-frame format)
    snr : float
        Signal-to-noise ratio (dB)
    """
    super().__init__(sampling_rate, other=other, snr=snr)

    self.other = other
    self.snr = snr
    logger.debug(f"Initialized AddWithSNR operation with SNR: {snr} dB")
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape

Returns

tuple Output data shape (same as input)

Source code in wandas/processing/effects.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape

    Returns
    -------
    tuple
        Output data shape (same as input)
    """
    return input_shape

HpssHarmonic

Bases: AudioOperation[NDArrayReal, NDArrayReal]

HPSS Harmonic operation

Source code in wandas/processing/effects.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class HpssHarmonic(AudioOperation[NDArrayReal, NDArrayReal]):
    """HPSS Harmonic operation"""

    name = "hpss_harmonic"

    def __init__(
        self,
        sampling_rate: float,
        **kwargs: Any,
    ):
        """
        Initialize HPSS Harmonic

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        """
        self.kwargs = kwargs
        super().__init__(sampling_rate, **kwargs)

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        return input_shape

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Create processor function for HPSS Harmonic"""
        logger.debug(f"Applying HPSS Harmonic to array with shape: {x.shape}")
        result: NDArrayReal = effects.harmonic(x, **self.kwargs)
        logger.debug(
            f"HPSS Harmonic applied, returning result with shape: {result.shape}"
        )
        return result
Attributes
name = 'hpss_harmonic' class-attribute instance-attribute
kwargs = kwargs instance-attribute
Functions
__init__(sampling_rate, **kwargs)

Initialize HPSS Harmonic

Parameters

sampling_rate : float Sampling rate (Hz)

Source code in wandas/processing/effects.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def __init__(
    self,
    sampling_rate: float,
    **kwargs: Any,
):
    """
    Initialize HPSS Harmonic

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    """
    self.kwargs = kwargs
    super().__init__(sampling_rate, **kwargs)
calculate_output_shape(input_shape)
Source code in wandas/processing/effects.py
35
36
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    return input_shape

HpssPercussive

Bases: AudioOperation[NDArrayReal, NDArrayReal]

HPSS Percussive operation

Source code in wandas/processing/effects.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class HpssPercussive(AudioOperation[NDArrayReal, NDArrayReal]):
    """HPSS Percussive operation"""

    name = "hpss_percussive"

    def __init__(
        self,
        sampling_rate: float,
        **kwargs: Any,
    ):
        """
        Initialize HPSS Percussive

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        """
        self.kwargs = kwargs
        super().__init__(sampling_rate, **kwargs)

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        return input_shape

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Create processor function for HPSS Percussive"""
        logger.debug(f"Applying HPSS Percussive to array with shape: {x.shape}")
        result: NDArrayReal = effects.percussive(x, **self.kwargs)
        logger.debug(
            f"HPSS Percussive applied, returning result with shape: {result.shape}"
        )
        return result
Attributes
name = 'hpss_percussive' class-attribute instance-attribute
kwargs = kwargs instance-attribute
Functions
__init__(sampling_rate, **kwargs)

Initialize HPSS Percussive

Parameters

sampling_rate : float Sampling rate (Hz)

Source code in wandas/processing/effects.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def __init__(
    self,
    sampling_rate: float,
    **kwargs: Any,
):
    """
    Initialize HPSS Percussive

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    """
    self.kwargs = kwargs
    super().__init__(sampling_rate, **kwargs)
calculate_output_shape(input_shape)
Source code in wandas/processing/effects.py
69
70
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    return input_shape

AWeighting

Bases: AudioOperation[NDArrayReal, NDArrayReal]

A-weighting filter operation

Source code in wandas/processing/filters.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
class AWeighting(AudioOperation[NDArrayReal, NDArrayReal]):
    """A-weighting filter operation"""

    name = "a_weighting"

    def __init__(self, sampling_rate: float):
        """
        Initialize A-weighting filter

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        """
        super().__init__(sampling_rate)

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        return input_shape

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Create processor function for A-weighting filter"""
        logger.debug(f"Applying A-weighting to array with shape: {x.shape}")
        result = A_weight(x, self.sampling_rate)

        # Handle case where A_weight returns a tuple
        if isinstance(result, tuple):
            # Use the first element of the tuple
            result = result[0]

        logger.debug(
            f"A-weighting applied, returning result with shape: {result.shape}"
        )
        return np.array(result)
Attributes
name = 'a_weighting' class-attribute instance-attribute
Functions
__init__(sampling_rate)

Initialize A-weighting filter

Parameters

sampling_rate : float Sampling rate (Hz)

Source code in wandas/processing/filters.py
194
195
196
197
198
199
200
201
202
203
def __init__(self, sampling_rate: float):
    """
    Initialize A-weighting filter

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    """
    super().__init__(sampling_rate)
calculate_output_shape(input_shape)
Source code in wandas/processing/filters.py
205
206
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    return input_shape

HighPassFilter

Bases: AudioOperation[NDArrayReal, NDArrayReal]

High-pass filter operation

Source code in wandas/processing/filters.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class HighPassFilter(AudioOperation[NDArrayReal, NDArrayReal]):
    """High-pass filter operation"""

    name = "highpass_filter"

    def __init__(self, sampling_rate: float, cutoff: float, order: int = 4):
        """
        Initialize high-pass filter

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        cutoff : float
            Cutoff frequency (Hz)
        order : int, optional
            Filter order, default is 4
        """
        self.cutoff = cutoff
        self.order = order
        super().__init__(sampling_rate, cutoff=cutoff, order=order)

    def validate_params(self) -> None:
        """Validate parameters"""
        if self.cutoff <= 0 or self.cutoff >= self.sampling_rate / 2:
            limit = self.sampling_rate / 2
            raise ValueError(f"Cutoff frequency must be between 0 Hz and {limit} Hz")

    def _setup_processor(self) -> None:
        """Set up high-pass filter processor"""
        # Calculate filter coefficients (once) - safely retrieve from instance variables
        nyquist = 0.5 * self.sampling_rate
        normal_cutoff = self.cutoff / nyquist

        # Precompute and save filter coefficients
        self.b, self.a = signal.butter(self.order, normal_cutoff, btype="high")  # type: ignore [unused-ignore]
        logger.debug(f"Highpass filter coefficients calculated: b={self.b}, a={self.a}")

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        return input_shape

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Filter processing wrapped with @dask.delayed"""
        logger.debug(f"Applying highpass filter to array with shape: {x.shape}")
        result: NDArrayReal = signal.filtfilt(self.b, self.a, x, axis=1)
        logger.debug(f"Filter applied, returning result with shape: {result.shape}")
        return result
Attributes
name = 'highpass_filter' class-attribute instance-attribute
cutoff = cutoff instance-attribute
order = order instance-attribute
Functions
__init__(sampling_rate, cutoff, order=4)

Initialize high-pass filter

Parameters

sampling_rate : float Sampling rate (Hz) cutoff : float Cutoff frequency (Hz) order : int, optional Filter order, default is 4

Source code in wandas/processing/filters.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def __init__(self, sampling_rate: float, cutoff: float, order: int = 4):
    """
    Initialize high-pass filter

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    cutoff : float
        Cutoff frequency (Hz)
    order : int, optional
        Filter order, default is 4
    """
    self.cutoff = cutoff
    self.order = order
    super().__init__(sampling_rate, cutoff=cutoff, order=order)
validate_params()

Validate parameters

Source code in wandas/processing/filters.py
35
36
37
38
39
def validate_params(self) -> None:
    """Validate parameters"""
    if self.cutoff <= 0 or self.cutoff >= self.sampling_rate / 2:
        limit = self.sampling_rate / 2
        raise ValueError(f"Cutoff frequency must be between 0 Hz and {limit} Hz")
calculate_output_shape(input_shape)
Source code in wandas/processing/filters.py
51
52
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    return input_shape

LowPassFilter

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Low-pass filter operation

Source code in wandas/processing/filters.py
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
class LowPassFilter(AudioOperation[NDArrayReal, NDArrayReal]):
    """Low-pass filter operation"""

    name = "lowpass_filter"
    a: NDArrayReal
    b: NDArrayReal

    def __init__(self, sampling_rate: float, cutoff: float, order: int = 4):
        """
        Initialize low-pass filter

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        cutoff : float
            Cutoff frequency (Hz)
        order : int, optional
            Filter order, default is 4
        """
        self.cutoff = cutoff
        self.order = order
        super().__init__(sampling_rate, cutoff=cutoff, order=order)

    def validate_params(self) -> None:
        """Validate parameters"""
        if self.cutoff <= 0 or self.cutoff >= self.sampling_rate / 2:
            raise ValueError(
                f"Cutoff frequency must be between 0 Hz and {self.sampling_rate / 2} Hz"
            )

    def _setup_processor(self) -> None:
        """Set up low-pass filter processor"""
        nyquist = 0.5 * self.sampling_rate
        normal_cutoff = self.cutoff / nyquist

        # Precompute and save filter coefficients
        self.b, self.a = signal.butter(self.order, normal_cutoff, btype="low")  # type: ignore [unused-ignore]
        logger.debug(f"Lowpass filter coefficients calculated: b={self.b}, a={self.a}")

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        return input_shape

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Filter processing wrapped with @dask.delayed"""
        logger.debug(f"Applying lowpass filter to array with shape: {x.shape}")
        result: NDArrayReal = signal.filtfilt(self.b, self.a, x, axis=1)

        logger.debug(f"Filter applied, returning result with shape: {result.shape}")
        return result
Attributes
name = 'lowpass_filter' class-attribute instance-attribute
a instance-attribute
b instance-attribute
cutoff = cutoff instance-attribute
order = order instance-attribute
Functions
__init__(sampling_rate, cutoff, order=4)

Initialize low-pass filter

Parameters

sampling_rate : float Sampling rate (Hz) cutoff : float Cutoff frequency (Hz) order : int, optional Filter order, default is 4

Source code in wandas/processing/filters.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def __init__(self, sampling_rate: float, cutoff: float, order: int = 4):
    """
    Initialize low-pass filter

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    cutoff : float
        Cutoff frequency (Hz)
    order : int, optional
        Filter order, default is 4
    """
    self.cutoff = cutoff
    self.order = order
    super().__init__(sampling_rate, cutoff=cutoff, order=order)
validate_params()

Validate parameters

Source code in wandas/processing/filters.py
86
87
88
89
90
91
def validate_params(self) -> None:
    """Validate parameters"""
    if self.cutoff <= 0 or self.cutoff >= self.sampling_rate / 2:
        raise ValueError(
            f"Cutoff frequency must be between 0 Hz and {self.sampling_rate / 2} Hz"
        )
calculate_output_shape(input_shape)
Source code in wandas/processing/filters.py
102
103
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    return input_shape

CSD

Bases: AudioOperation[NDArrayReal, NDArrayComplex]

Cross-spectral density estimation operation

Source code in wandas/processing/spectral.py
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
class CSD(AudioOperation[NDArrayReal, NDArrayComplex]):
    """Cross-spectral density estimation operation"""

    name = "csd"

    def __init__(
        self,
        sampling_rate: float,
        n_fft: int,
        hop_length: int,
        win_length: int,
        window: str,
        detrend: str,
        scaling: str,
        average: str,
    ):
        """
        Initialize cross-spectral density estimation operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        n_fft : int
            FFT size
        hop_length : int
            Hop length
        win_length : int
            Window length
        window : str
            Window function
        detrend : str
            Type of detrend
        scaling : str
            Type of scaling
        average : str
            Method of averaging
        """
        self.n_fft = n_fft
        self.win_length = win_length if win_length is not None else n_fft
        self.hop_length = hop_length if hop_length is not None else self.win_length // 4
        self.window = window
        self.detrend = detrend
        self.scaling = scaling
        self.average = average
        super().__init__(
            sampling_rate,
            n_fft=n_fft,
            hop_length=self.hop_length,
            win_length=self.win_length,
            window=window,
            detrend=detrend,
            scaling=scaling,
            average=average,
        )

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape (channels, samples)

        Returns
        -------
        tuple
            Output data shape (channels * channels, freqs)
        """
        n_channels = input_shape[0]
        n_freqs = self.n_fft // 2 + 1
        return (n_channels * n_channels, n_freqs)

    def _process_array(self, x: NDArrayReal) -> NDArrayComplex:
        """Processor function for cross-spectral density estimation operation"""
        logger.debug(f"Applying CSD estimation to array with shape: {x.shape}")
        from scipy import signal as ss

        # Calculate all combinations using scipy's csd function
        _, csd_result = ss.csd(
            x=x[:, np.newaxis],
            y=x[np.newaxis, :],
            fs=self.sampling_rate,
            nperseg=self.win_length,
            noverlap=self.win_length - self.hop_length,
            nfft=self.n_fft,
            window=self.window,
            detrend=self.detrend,
            scaling=self.scaling,
            average=self.average,
        )

        # Reshape result to (n_channels * n_channels, n_freqs)
        result: NDArrayComplex = csd_result.transpose(1, 0, 2).reshape(
            -1, csd_result.shape[-1]
        )

        logger.debug(f"CSD estimation applied, result shape: {result.shape}")
        return result
Attributes
name = 'csd' class-attribute instance-attribute
n_fft = n_fft instance-attribute
win_length = win_length if win_length is not None else n_fft instance-attribute
hop_length = hop_length if hop_length is not None else self.win_length // 4 instance-attribute
window = window instance-attribute
detrend = detrend instance-attribute
scaling = scaling instance-attribute
average = average instance-attribute
Functions
__init__(sampling_rate, n_fft, hop_length, win_length, window, detrend, scaling, average)

Initialize cross-spectral density estimation operation

Parameters

sampling_rate : float Sampling rate (Hz) n_fft : int FFT size hop_length : int Hop length win_length : int Window length window : str Window function detrend : str Type of detrend scaling : str Type of scaling average : str Method of averaging

Source code in wandas/processing/spectral.py
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
def __init__(
    self,
    sampling_rate: float,
    n_fft: int,
    hop_length: int,
    win_length: int,
    window: str,
    detrend: str,
    scaling: str,
    average: str,
):
    """
    Initialize cross-spectral density estimation operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    n_fft : int
        FFT size
    hop_length : int
        Hop length
    win_length : int
        Window length
    window : str
        Window function
    detrend : str
        Type of detrend
    scaling : str
        Type of scaling
    average : str
        Method of averaging
    """
    self.n_fft = n_fft
    self.win_length = win_length if win_length is not None else n_fft
    self.hop_length = hop_length if hop_length is not None else self.win_length // 4
    self.window = window
    self.detrend = detrend
    self.scaling = scaling
    self.average = average
    super().__init__(
        sampling_rate,
        n_fft=n_fft,
        hop_length=self.hop_length,
        win_length=self.win_length,
        window=window,
        detrend=detrend,
        scaling=scaling,
        average=average,
    )
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape (channels, samples)

Returns

tuple Output data shape (channels * channels, freqs)

Source code in wandas/processing/spectral.py
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape (channels, samples)

    Returns
    -------
    tuple
        Output data shape (channels * channels, freqs)
    """
    n_channels = input_shape[0]
    n_freqs = self.n_fft // 2 + 1
    return (n_channels * n_channels, n_freqs)

FFT

Bases: AudioOperation[NDArrayReal, NDArrayComplex]

FFT (Fast Fourier Transform) operation

Source code in wandas/processing/spectral.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class FFT(AudioOperation[NDArrayReal, NDArrayComplex]):
    """FFT (Fast Fourier Transform) operation"""

    name = "fft"
    n_fft: Optional[int]
    window: str

    def __init__(
        self, sampling_rate: float, n_fft: Optional[int] = None, window: str = "hann"
    ):
        """
        Initialize FFT operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        n_fft : int, optional
            FFT size, default is None (determined by input size)
        window : str, optional
            Window function type, default is 'hann'
        """
        self.n_fft = n_fft
        self.window = window
        super().__init__(sampling_rate, n_fft=n_fft, window=window)

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        操作後の出力データの形状を計算します

        Parameters
        ----------
        input_shape : tuple
            入力データの形状 (channels, samples)

        Returns
        -------
        tuple
            出力データの形状 (channels, freqs)
        """
        n_freqs = self.n_fft // 2 + 1 if self.n_fft else input_shape[-1] // 2 + 1
        return (*input_shape[:-1], n_freqs)

    def _process_array(self, x: NDArrayReal) -> NDArrayComplex:
        """FFT操作のプロセッサ関数を作成"""
        from scipy.signal import get_window

        if self.n_fft is not None and x.shape[-1] > self.n_fft:
            # If n_fft is specified and input length exceeds it, truncate
            x = x[..., : self.n_fft]

        win = get_window(self.window, x.shape[-1])
        x = x * win
        result: NDArrayComplex = np.fft.rfft(x, n=self.n_fft, axis=-1)
        result[..., 1:-1] *= 2.0
        # 窓関数補正
        scaling_factor = np.sum(win)
        result = result / scaling_factor
        return result
Attributes
name = 'fft' class-attribute instance-attribute
n_fft = n_fft instance-attribute
window = window instance-attribute
Functions
__init__(sampling_rate, n_fft=None, window='hann')

Initialize FFT operation

Parameters

sampling_rate : float Sampling rate (Hz) n_fft : int, optional FFT size, default is None (determined by input size) window : str, optional Window function type, default is 'hann'

Source code in wandas/processing/spectral.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def __init__(
    self, sampling_rate: float, n_fft: Optional[int] = None, window: str = "hann"
):
    """
    Initialize FFT operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    n_fft : int, optional
        FFT size, default is None (determined by input size)
    window : str, optional
        Window function type, default is 'hann'
    """
    self.n_fft = n_fft
    self.window = window
    super().__init__(sampling_rate, n_fft=n_fft, window=window)
calculate_output_shape(input_shape)

操作後の出力データの形状を計算します

Parameters

input_shape : tuple 入力データの形状 (channels, samples)

Returns

tuple 出力データの形状 (channels, freqs)

Source code in wandas/processing/spectral.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    操作後の出力データの形状を計算します

    Parameters
    ----------
    input_shape : tuple
        入力データの形状 (channels, samples)

    Returns
    -------
    tuple
        出力データの形状 (channels, freqs)
    """
    n_freqs = self.n_fft // 2 + 1 if self.n_fft else input_shape[-1] // 2 + 1
    return (*input_shape[:-1], n_freqs)

IFFT

Bases: AudioOperation[NDArrayComplex, NDArrayReal]

IFFT (Inverse Fast Fourier Transform) operation

Source code in wandas/processing/spectral.py
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
class IFFT(AudioOperation[NDArrayComplex, NDArrayReal]):
    """IFFT (Inverse Fast Fourier Transform) operation"""

    name = "ifft"
    n_fft: Optional[int]
    window: str

    def __init__(
        self, sampling_rate: float, n_fft: Optional[int] = None, window: str = "hann"
    ):
        """
        Initialize IFFT operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        n_fft : Optional[int], optional
            IFFT size, default is None (determined based on input size)
        window : str, optional
            Window function type, default is 'hann'
        """
        self.n_fft = n_fft
        self.window = window
        super().__init__(sampling_rate, n_fft=n_fft, window=window)

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape (channels, freqs)

        Returns
        -------
        tuple
            Output data shape (channels, samples)
        """
        n_samples = 2 * (input_shape[-1] - 1) if self.n_fft is None else self.n_fft
        return (*input_shape[:-1], n_samples)

    def _process_array(self, x: NDArrayComplex) -> NDArrayReal:
        """Create processor function for IFFT operation"""
        logger.debug(f"Applying IFFT to array with shape: {x.shape}")

        # Restore frequency component scaling (remove the 2.0 multiplier applied in FFT)
        _x = x.copy()
        _x[..., 1:-1] /= 2.0

        # Execute IFFT
        result: NDArrayReal = np.fft.irfft(_x, n=self.n_fft, axis=-1)

        # Window function correction (inverse of FFT operation)
        from scipy.signal import get_window

        win = get_window(self.window, result.shape[-1])

        # Correct the FFT window function scaling
        scaling_factor = np.sum(win) / result.shape[-1]
        result = result / scaling_factor

        logger.debug(f"IFFT applied, returning result with shape: {result.shape}")
        return result
Attributes
name = 'ifft' class-attribute instance-attribute
n_fft = n_fft instance-attribute
window = window instance-attribute
Functions
__init__(sampling_rate, n_fft=None, window='hann')

Initialize IFFT operation

Parameters

sampling_rate : float Sampling rate (Hz) n_fft : Optional[int], optional IFFT size, default is None (determined based on input size) window : str, optional Window function type, default is 'hann'

Source code in wandas/processing/spectral.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def __init__(
    self, sampling_rate: float, n_fft: Optional[int] = None, window: str = "hann"
):
    """
    Initialize IFFT operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    n_fft : Optional[int], optional
        IFFT size, default is None (determined based on input size)
    window : str, optional
        Window function type, default is 'hann'
    """
    self.n_fft = n_fft
    self.window = window
    super().__init__(sampling_rate, n_fft=n_fft, window=window)
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape (channels, freqs)

Returns

tuple Output data shape (channels, samples)

Source code in wandas/processing/spectral.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape (channels, freqs)

    Returns
    -------
    tuple
        Output data shape (channels, samples)
    """
    n_samples = 2 * (input_shape[-1] - 1) if self.n_fft is None else self.n_fft
    return (*input_shape[:-1], n_samples)

ISTFT

Bases: AudioOperation[NDArrayComplex, NDArrayReal]

Inverse Short-Time Fourier Transform operation

Source code in wandas/processing/spectral.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
class ISTFT(AudioOperation[NDArrayComplex, NDArrayReal]):
    """Inverse Short-Time Fourier Transform operation"""

    name = "istft"

    def __init__(
        self,
        sampling_rate: float,
        n_fft: int = 2048,
        hop_length: Optional[int] = None,
        win_length: Optional[int] = None,
        window: str = "hann",
        length: Optional[int] = None,
    ):
        self.n_fft = n_fft
        self.win_length = win_length if win_length is not None else n_fft
        self.hop_length = hop_length if hop_length is not None else self.win_length // 4
        self.window = window
        self.length = length

        # Instantiate ShortTimeFFT for ISTFT calculation
        self.SFT = ShortTimeFFT(
            win=get_window(window, self.win_length),
            hop=self.hop_length,
            fs=sampling_rate,
            mfft=self.n_fft,
            scale_to="magnitude",  # Consistent scaling with STFT
        )

        super().__init__(
            sampling_rate,
            n_fft=n_fft,
            win_length=self.win_length,
            hop_length=self.hop_length,
            window=window,
            length=length,
        )

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape (channels, freqs, time_frames)

        Returns
        -------
        tuple
            Output data shape (channels, samples)
        """
        k0: int = 0
        q_max = input_shape[-1] + self.SFT.p_min
        k_max = (q_max - 1) * self.SFT.hop + self.SFT.m_num - self.SFT.m_num_mid
        k_q0, k_q1 = self.SFT.nearest_k_p(k0), self.SFT.nearest_k_p(k_max, left=False)
        n_pts = k_q1 - k_q0 + self.SFT.m_num - self.SFT.m_num_mid

        return input_shape[:-2] + (n_pts,)

    def _process_array(self, x: NDArrayComplex) -> NDArrayReal:
        """
        Apply SciPy ISTFT processing to multiple channels at once using ShortTimeFFT"""
        logger.debug(
            f"Applying SciPy ISTFT (ShortTimeFFT) to array with shape: {x.shape}"
        )

        # Convert 2D input to 3D (assume single channel)
        if x.ndim == 2:
            x = x.reshape(1, *x.shape)

        # Adjust scaling back if STFT applied factor of 2
        _x = np.copy(x)
        _x[..., 1:-1, :] /= 2.0

        # Apply ISTFT using the ShortTimeFFT instance
        result: NDArrayReal = self.SFT.istft(_x)

        # Trim to desired length if specified
        if self.length is not None:
            result = result[..., : self.length]

        logger.debug(
            f"ShortTimeFFT applied, returning result with shape: {result.shape}"
        )
        return result
Attributes
name = 'istft' class-attribute instance-attribute
n_fft = n_fft instance-attribute
win_length = win_length if win_length is not None else n_fft instance-attribute
hop_length = hop_length if hop_length is not None else self.win_length // 4 instance-attribute
window = window instance-attribute
length = length instance-attribute
SFT = ShortTimeFFT(win=get_window(window, self.win_length), hop=self.hop_length, fs=sampling_rate, mfft=self.n_fft, scale_to='magnitude') instance-attribute
Functions
__init__(sampling_rate, n_fft=2048, hop_length=None, win_length=None, window='hann', length=None)
Source code in wandas/processing/spectral.py
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
def __init__(
    self,
    sampling_rate: float,
    n_fft: int = 2048,
    hop_length: Optional[int] = None,
    win_length: Optional[int] = None,
    window: str = "hann",
    length: Optional[int] = None,
):
    self.n_fft = n_fft
    self.win_length = win_length if win_length is not None else n_fft
    self.hop_length = hop_length if hop_length is not None else self.win_length // 4
    self.window = window
    self.length = length

    # Instantiate ShortTimeFFT for ISTFT calculation
    self.SFT = ShortTimeFFT(
        win=get_window(window, self.win_length),
        hop=self.hop_length,
        fs=sampling_rate,
        mfft=self.n_fft,
        scale_to="magnitude",  # Consistent scaling with STFT
    )

    super().__init__(
        sampling_rate,
        n_fft=n_fft,
        win_length=self.win_length,
        hop_length=self.hop_length,
        window=window,
        length=length,
    )
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape (channels, freqs, time_frames)

Returns

tuple Output data shape (channels, samples)

Source code in wandas/processing/spectral.py
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape (channels, freqs, time_frames)

    Returns
    -------
    tuple
        Output data shape (channels, samples)
    """
    k0: int = 0
    q_max = input_shape[-1] + self.SFT.p_min
    k_max = (q_max - 1) * self.SFT.hop + self.SFT.m_num - self.SFT.m_num_mid
    k_q0, k_q1 = self.SFT.nearest_k_p(k0), self.SFT.nearest_k_p(k_max, left=False)
    n_pts = k_q1 - k_q0 + self.SFT.m_num - self.SFT.m_num_mid

    return input_shape[:-2] + (n_pts,)

STFT

Bases: AudioOperation[NDArrayReal, NDArrayComplex]

Short-Time Fourier Transform operation

Source code in wandas/processing/spectral.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
class STFT(AudioOperation[NDArrayReal, NDArrayComplex]):
    """Short-Time Fourier Transform operation"""

    name = "stft"

    def __init__(
        self,
        sampling_rate: float,
        n_fft: int = 2048,
        hop_length: Optional[int] = None,
        win_length: Optional[int] = None,
        window: str = "hann",
    ):
        self.n_fft = n_fft
        self.win_length = win_length if win_length is not None else n_fft
        self.hop_length = hop_length if hop_length is not None else self.win_length // 4
        self.noverlap = (
            self.win_length - self.hop_length if hop_length is not None else None
        )
        self.window = window

        self.SFT = ShortTimeFFT(
            win=get_window(window, self.win_length),
            hop=self.hop_length,
            fs=sampling_rate,
            mfft=self.n_fft,
            scale_to="magnitude",
        )
        super().__init__(
            sampling_rate,
            n_fft=n_fft,
            win_length=self.win_length,
            hop_length=self.hop_length,
            window=window,
        )

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape

        Returns
        -------
        tuple
            Output data shape
        """
        n_samples = input_shape[-1]
        n_f = len(self.SFT.f)
        n_t = len(self.SFT.t(n_samples))
        return (input_shape[0], n_f, n_t)

    def _process_array(self, x: NDArrayReal) -> NDArrayComplex:
        """Apply SciPy STFT processing to multiple channels at once"""
        logger.debug(f"Applying SciPy STFT to array with shape: {x.shape}")

        # Convert 1D input to 2D
        if x.ndim == 1:
            x = x.reshape(1, -1)

        # Apply STFT to all channels at once
        result: NDArrayComplex = self.SFT.stft(x)
        result[..., 1:-1, :] *= 2.0
        logger.debug(f"SciPy STFT applied, returning result with shape: {result.shape}")
        return result
Attributes
name = 'stft' class-attribute instance-attribute
n_fft = n_fft instance-attribute
win_length = win_length if win_length is not None else n_fft instance-attribute
hop_length = hop_length if hop_length is not None else self.win_length // 4 instance-attribute
noverlap = self.win_length - self.hop_length if hop_length is not None else None instance-attribute
window = window instance-attribute
SFT = ShortTimeFFT(win=get_window(window, self.win_length), hop=self.hop_length, fs=sampling_rate, mfft=self.n_fft, scale_to='magnitude') instance-attribute
Functions
__init__(sampling_rate, n_fft=2048, hop_length=None, win_length=None, window='hann')
Source code in wandas/processing/spectral.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
def __init__(
    self,
    sampling_rate: float,
    n_fft: int = 2048,
    hop_length: Optional[int] = None,
    win_length: Optional[int] = None,
    window: str = "hann",
):
    self.n_fft = n_fft
    self.win_length = win_length if win_length is not None else n_fft
    self.hop_length = hop_length if hop_length is not None else self.win_length // 4
    self.noverlap = (
        self.win_length - self.hop_length if hop_length is not None else None
    )
    self.window = window

    self.SFT = ShortTimeFFT(
        win=get_window(window, self.win_length),
        hop=self.hop_length,
        fs=sampling_rate,
        mfft=self.n_fft,
        scale_to="magnitude",
    )
    super().__init__(
        sampling_rate,
        n_fft=n_fft,
        win_length=self.win_length,
        hop_length=self.hop_length,
        window=window,
    )
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape

Returns

tuple Output data shape

Source code in wandas/processing/spectral.py
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape

    Returns
    -------
    tuple
        Output data shape
    """
    n_samples = input_shape[-1]
    n_f = len(self.SFT.f)
    n_t = len(self.SFT.t(n_samples))
    return (input_shape[0], n_f, n_t)

Coherence

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Coherence estimation operation

Source code in wandas/processing/spectral.py
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
class Coherence(AudioOperation[NDArrayReal, NDArrayReal]):
    """Coherence estimation operation"""

    name = "coherence"

    def __init__(
        self,
        sampling_rate: float,
        n_fft: int,
        hop_length: int,
        win_length: int,
        window: str,
        detrend: str,
    ):
        """
        Initialize coherence estimation operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        n_fft : int
            FFT size
        hop_length : int
            Hop length
        win_length : int
            Window length
        window : str
            Window function
        detrend : str
            Type of detrend
        """
        self.n_fft = n_fft
        self.win_length = win_length if win_length is not None else n_fft
        self.hop_length = hop_length if hop_length is not None else self.win_length // 4
        self.window = window
        self.detrend = detrend
        super().__init__(
            sampling_rate,
            n_fft=n_fft,
            hop_length=self.hop_length,
            win_length=self.win_length,
            window=window,
            detrend=detrend,
        )

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape (channels, samples)

        Returns
        -------
        tuple
            Output data shape (channels * channels, freqs)
        """
        n_channels = input_shape[0]
        n_freqs = self.n_fft // 2 + 1
        return (n_channels * n_channels, n_freqs)

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Processor function for coherence estimation operation"""
        logger.debug(f"Applying coherence estimation to array with shape: {x.shape}")
        from scipy import signal as ss

        _, coh = ss.coherence(
            x=x[:, np.newaxis],
            y=x[np.newaxis, :],
            fs=self.sampling_rate,
            nperseg=self.win_length,
            noverlap=self.win_length - self.hop_length,
            nfft=self.n_fft,
            window=self.window,
            detrend=self.detrend,
        )

        # Reshape result to (n_channels * n_channels, n_freqs)
        result: NDArrayReal = coh.transpose(1, 0, 2).reshape(-1, coh.shape[-1])

        logger.debug(f"Coherence estimation applied, result shape: {result.shape}")
        return result
Attributes
name = 'coherence' class-attribute instance-attribute
n_fft = n_fft instance-attribute
win_length = win_length if win_length is not None else n_fft instance-attribute
hop_length = hop_length if hop_length is not None else self.win_length // 4 instance-attribute
window = window instance-attribute
detrend = detrend instance-attribute
Functions
__init__(sampling_rate, n_fft, hop_length, win_length, window, detrend)

Initialize coherence estimation operation

Parameters

sampling_rate : float Sampling rate (Hz) n_fft : int FFT size hop_length : int Hop length win_length : int Window length window : str Window function detrend : str Type of detrend

Source code in wandas/processing/spectral.py
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
def __init__(
    self,
    sampling_rate: float,
    n_fft: int,
    hop_length: int,
    win_length: int,
    window: str,
    detrend: str,
):
    """
    Initialize coherence estimation operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    n_fft : int
        FFT size
    hop_length : int
        Hop length
    win_length : int
        Window length
    window : str
        Window function
    detrend : str
        Type of detrend
    """
    self.n_fft = n_fft
    self.win_length = win_length if win_length is not None else n_fft
    self.hop_length = hop_length if hop_length is not None else self.win_length // 4
    self.window = window
    self.detrend = detrend
    super().__init__(
        sampling_rate,
        n_fft=n_fft,
        hop_length=self.hop_length,
        win_length=self.win_length,
        window=window,
        detrend=detrend,
    )
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape (channels, samples)

Returns

tuple Output data shape (channels * channels, freqs)

Source code in wandas/processing/spectral.py
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape (channels, samples)

    Returns
    -------
    tuple
        Output data shape (channels * channels, freqs)
    """
    n_channels = input_shape[0]
    n_freqs = self.n_fft // 2 + 1
    return (n_channels * n_channels, n_freqs)

NOctSpectrum

Bases: AudioOperation[NDArrayReal, NDArrayReal]

N-octave spectrum operation

Source code in wandas/processing/spectral.py
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
class NOctSpectrum(AudioOperation[NDArrayReal, NDArrayReal]):
    """N-octave spectrum operation"""

    name = "noct_spectrum"

    def __init__(
        self,
        sampling_rate: float,
        fmin: float,
        fmax: float,
        n: int = 3,
        G: int = 10,  # noqa: N803
        fr: int = 1000,
    ):
        """
        Initialize N-octave spectrum

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        fmin : float
            Minimum frequency (Hz)
        fmax : float
            Maximum frequency (Hz)
        n : int, optional
            Number of octave divisions, default is 3
        G : int, optional
            Reference level, default is 10
        fr : int, optional
            Reference frequency, default is 1000
        """
        super().__init__(sampling_rate, fmin=fmin, fmax=fmax, n=n, G=G, fr=fr)
        self.fmin = fmin
        self.fmax = fmax
        self.n = n
        self.G = G
        self.fr = fr

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape

        Returns
        -------
        tuple
            Output data shape
        """
        # Calculate output shape for octave spectrum
        _, fpref = _center_freq(
            fmin=self.fmin, fmax=self.fmax, n=self.n, G=self.G, fr=self.fr
        )
        return (input_shape[0], fpref.shape[0])

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Create processor function for octave spectrum"""
        logger.debug(f"Applying NoctSpectrum to array with shape: {x.shape}")
        spec, _ = noct_spectrum(
            sig=x.T,
            fs=self.sampling_rate,
            fmin=self.fmin,
            fmax=self.fmax,
            n=self.n,
            G=self.G,
            fr=self.fr,
        )
        if spec.ndim == 1:
            # Add channel dimension for 1D
            spec = np.expand_dims(spec, axis=0)
        else:
            spec = spec.T
        logger.debug(f"NoctSpectrum applied, returning result with shape: {spec.shape}")
        return np.array(spec)
Attributes
name = 'noct_spectrum' class-attribute instance-attribute
fmin = fmin instance-attribute
fmax = fmax instance-attribute
n = n instance-attribute
G = G instance-attribute
fr = fr instance-attribute
Functions
__init__(sampling_rate, fmin, fmax, n=3, G=10, fr=1000)

Initialize N-octave spectrum

Parameters

sampling_rate : float Sampling rate (Hz) fmin : float Minimum frequency (Hz) fmax : float Maximum frequency (Hz) n : int, optional Number of octave divisions, default is 3 G : int, optional Reference level, default is 10 fr : int, optional Reference frequency, default is 1000

Source code in wandas/processing/spectral.py
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
def __init__(
    self,
    sampling_rate: float,
    fmin: float,
    fmax: float,
    n: int = 3,
    G: int = 10,  # noqa: N803
    fr: int = 1000,
):
    """
    Initialize N-octave spectrum

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    fmin : float
        Minimum frequency (Hz)
    fmax : float
        Maximum frequency (Hz)
    n : int, optional
        Number of octave divisions, default is 3
    G : int, optional
        Reference level, default is 10
    fr : int, optional
        Reference frequency, default is 1000
    """
    super().__init__(sampling_rate, fmin=fmin, fmax=fmax, n=n, G=G, fr=fr)
    self.fmin = fmin
    self.fmax = fmax
    self.n = n
    self.G = G
    self.fr = fr
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape

Returns

tuple Output data shape

Source code in wandas/processing/spectral.py
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape

    Returns
    -------
    tuple
        Output data shape
    """
    # Calculate output shape for octave spectrum
    _, fpref = _center_freq(
        fmin=self.fmin, fmax=self.fmax, n=self.n, G=self.G, fr=self.fr
    )
    return (input_shape[0], fpref.shape[0])

NOctSynthesis

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Octave synthesis operation

Source code in wandas/processing/spectral.py
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
class NOctSynthesis(AudioOperation[NDArrayReal, NDArrayReal]):
    """Octave synthesis operation"""

    name = "noct_synthesis"

    def __init__(
        self,
        sampling_rate: float,
        fmin: float,
        fmax: float,
        n: int = 3,
        G: int = 10,  # noqa: N803
        fr: int = 1000,
    ):
        """
        Initialize octave synthesis

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        fmin : float
            Minimum frequency (Hz)
        fmax : float
            Maximum frequency (Hz)
        n : int, optional
            Number of octave divisions, default is 3
        G : int, optional
            Reference level, default is 10
        fr : int, optional
            Reference frequency, default is 1000
        """
        super().__init__(sampling_rate, fmin=fmin, fmax=fmax, n=n, G=G, fr=fr)

        self.fmin = fmin
        self.fmax = fmax
        self.n = n
        self.G = G
        self.fr = fr

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape

        Returns
        -------
        tuple
            Output data shape
        """
        # Calculate output shape for octave spectrum
        _, fpref = _center_freq(
            fmin=self.fmin, fmax=self.fmax, n=self.n, G=self.G, fr=self.fr
        )
        return (input_shape[0], fpref.shape[0])

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Create processor function for octave synthesis"""
        logger.debug(f"Applying NoctSynthesis to array with shape: {x.shape}")
        # Calculate n from shape[-1]
        n = x.shape[-1]  # Calculate n from shape[-1]
        if n % 2 == 0:
            n = n * 2 - 1
        else:
            n = (n - 1) * 2
        freqs = np.fft.rfftfreq(n, d=1 / self.sampling_rate)
        result, _ = noct_synthesis(
            spectrum=np.abs(x).T,
            freqs=freqs,
            fmin=self.fmin,
            fmax=self.fmax,
            n=self.n,
            G=self.G,
            fr=self.fr,
        )
        result = result.T
        logger.debug(
            f"NoctSynthesis applied, returning result with shape: {result.shape}"
        )
        return np.array(result)
Attributes
name = 'noct_synthesis' class-attribute instance-attribute
fmin = fmin instance-attribute
fmax = fmax instance-attribute
n = n instance-attribute
G = G instance-attribute
fr = fr instance-attribute
Functions
__init__(sampling_rate, fmin, fmax, n=3, G=10, fr=1000)

Initialize octave synthesis

Parameters

sampling_rate : float Sampling rate (Hz) fmin : float Minimum frequency (Hz) fmax : float Maximum frequency (Hz) n : int, optional Number of octave divisions, default is 3 G : int, optional Reference level, default is 10 fr : int, optional Reference frequency, default is 1000

Source code in wandas/processing/spectral.py
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
def __init__(
    self,
    sampling_rate: float,
    fmin: float,
    fmax: float,
    n: int = 3,
    G: int = 10,  # noqa: N803
    fr: int = 1000,
):
    """
    Initialize octave synthesis

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    fmin : float
        Minimum frequency (Hz)
    fmax : float
        Maximum frequency (Hz)
    n : int, optional
        Number of octave divisions, default is 3
    G : int, optional
        Reference level, default is 10
    fr : int, optional
        Reference frequency, default is 1000
    """
    super().__init__(sampling_rate, fmin=fmin, fmax=fmax, n=n, G=G, fr=fr)

    self.fmin = fmin
    self.fmax = fmax
    self.n = n
    self.G = G
    self.fr = fr
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape

Returns

tuple Output data shape

Source code in wandas/processing/spectral.py
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape

    Returns
    -------
    tuple
        Output data shape
    """
    # Calculate output shape for octave spectrum
    _, fpref = _center_freq(
        fmin=self.fmin, fmax=self.fmax, n=self.n, G=self.G, fr=self.fr
    )
    return (input_shape[0], fpref.shape[0])

TransferFunction

Bases: AudioOperation[NDArrayReal, NDArrayComplex]

Transfer function estimation operation

Source code in wandas/processing/spectral.py
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
class TransferFunction(AudioOperation[NDArrayReal, NDArrayComplex]):
    """Transfer function estimation operation"""

    name = "transfer_function"

    def __init__(
        self,
        sampling_rate: float,
        n_fft: int,
        hop_length: int,
        win_length: int,
        window: str,
        detrend: str,
        scaling: str = "spectrum",
        average: str = "mean",
    ):
        """
        Initialize transfer function estimation operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        n_fft : int
            FFT size
        hop_length : int
            Hop length
        win_length : int
            Window length
        window : str
            Window function
        detrend : str
            Type of detrend
        scaling : str
            Type of scaling
        average : str
            Method of averaging
        """
        self.n_fft = n_fft
        self.win_length = win_length if win_length is not None else n_fft
        self.hop_length = hop_length if hop_length is not None else self.win_length // 4
        self.window = window
        self.detrend = detrend
        self.scaling = scaling
        self.average = average
        super().__init__(
            sampling_rate,
            n_fft=n_fft,
            hop_length=self.hop_length,
            win_length=self.win_length,
            window=window,
            detrend=detrend,
            scaling=scaling,
            average=average,
        )

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape (channels, samples)

        Returns
        -------
        tuple
            Output data shape (channels * channels, freqs)
        """
        n_channels = input_shape[0]
        n_freqs = self.n_fft // 2 + 1
        return (n_channels * n_channels, n_freqs)

    def _process_array(self, x: NDArrayReal) -> NDArrayComplex:
        """Processor function for transfer function estimation operation"""
        logger.debug(
            f"Applying transfer function estimation to array with shape: {x.shape}"
        )
        from scipy import signal as ss

        # Calculate cross-spectral density between all channels
        f, p_yx = ss.csd(
            x=x[:, np.newaxis, :],
            y=x[np.newaxis, :, :],
            fs=self.sampling_rate,
            nperseg=self.win_length,
            noverlap=self.win_length - self.hop_length,
            nfft=self.n_fft,
            window=self.window,
            detrend=self.detrend,
            scaling=self.scaling,
            average=self.average,
            axis=-1,
        )
        # p_yx shape: (num_channels, num_channels, num_frequencies)

        # Calculate power spectral density for each channel
        f, p_xx = ss.welch(
            x=x,
            fs=self.sampling_rate,
            nperseg=self.win_length,
            noverlap=self.win_length - self.hop_length,
            nfft=self.n_fft,
            window=self.window,
            detrend=self.detrend,
            scaling=self.scaling,
            average=self.average,
            axis=-1,
        )
        # p_xx shape: (num_channels, num_frequencies)

        # Calculate transfer function H(f) = P_yx / P_xx
        h_f = p_yx / p_xx[np.newaxis, :, :]
        result: NDArrayComplex = h_f.transpose(1, 0, 2).reshape(-1, h_f.shape[-1])

        logger.debug(
            f"Transfer function estimation applied, result shape: {result.shape}"
        )
        return result
Attributes
name = 'transfer_function' class-attribute instance-attribute
n_fft = n_fft instance-attribute
win_length = win_length if win_length is not None else n_fft instance-attribute
hop_length = hop_length if hop_length is not None else self.win_length // 4 instance-attribute
window = window instance-attribute
detrend = detrend instance-attribute
scaling = scaling instance-attribute
average = average instance-attribute
Functions
__init__(sampling_rate, n_fft, hop_length, win_length, window, detrend, scaling='spectrum', average='mean')

Initialize transfer function estimation operation

Parameters

sampling_rate : float Sampling rate (Hz) n_fft : int FFT size hop_length : int Hop length win_length : int Window length window : str Window function detrend : str Type of detrend scaling : str Type of scaling average : str Method of averaging

Source code in wandas/processing/spectral.py
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
def __init__(
    self,
    sampling_rate: float,
    n_fft: int,
    hop_length: int,
    win_length: int,
    window: str,
    detrend: str,
    scaling: str = "spectrum",
    average: str = "mean",
):
    """
    Initialize transfer function estimation operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    n_fft : int
        FFT size
    hop_length : int
        Hop length
    win_length : int
        Window length
    window : str
        Window function
    detrend : str
        Type of detrend
    scaling : str
        Type of scaling
    average : str
        Method of averaging
    """
    self.n_fft = n_fft
    self.win_length = win_length if win_length is not None else n_fft
    self.hop_length = hop_length if hop_length is not None else self.win_length // 4
    self.window = window
    self.detrend = detrend
    self.scaling = scaling
    self.average = average
    super().__init__(
        sampling_rate,
        n_fft=n_fft,
        hop_length=self.hop_length,
        win_length=self.win_length,
        window=window,
        detrend=detrend,
        scaling=scaling,
        average=average,
    )
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape (channels, samples)

Returns

tuple Output data shape (channels * channels, freqs)

Source code in wandas/processing/spectral.py
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape (channels, samples)

    Returns
    -------
    tuple
        Output data shape (channels * channels, freqs)
    """
    n_channels = input_shape[0]
    n_freqs = self.n_fft // 2 + 1
    return (n_channels * n_channels, n_freqs)

Welch

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Welch

Source code in wandas/processing/spectral.py
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
class Welch(AudioOperation[NDArrayReal, NDArrayReal]):
    """Welch"""

    name = "welch"
    n_fft: int
    window: str
    hop_length: Optional[int]
    win_length: Optional[int]
    average: str
    detrend: str

    def __init__(
        self,
        sampling_rate: float,
        n_fft: int = 2048,
        hop_length: Optional[int] = None,
        win_length: Optional[int] = None,
        window: str = "hann",
        average: str = "mean",
        detrend: str = "constant",
    ):
        """
        Initialize Welch operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        n_fft : int, optional
            FFT size, default is 2048
        window : str, optional
            Window function type, default is 'hann'
        """
        self.n_fft = n_fft
        self.win_length = win_length if win_length is not None else n_fft
        self.hop_length = hop_length if hop_length is not None else self.win_length // 4
        self.noverlap = (
            self.win_length - self.hop_length if hop_length is not None else None
        )
        self.window = window
        self.average = average
        self.detrend = detrend
        super().__init__(
            sampling_rate,
            n_fft=n_fft,
            win_length=self.win_length,
            hop_length=self.hop_length,
            window=window,
            average=average,
            detrend=detrend,
        )

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape (channels, samples)

        Returns
        -------
        tuple
            Output data shape (channels, freqs)
        """
        n_freqs = self.n_fft // 2 + 1
        return (*input_shape[:-1], n_freqs)

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Create processor function for Welch operation"""
        from scipy import signal as ss

        _, result = ss.welch(
            x,
            nperseg=self.win_length,
            noverlap=self.noverlap,
            nfft=self.n_fft,
            window=self.window,
            average=self.average,
            detrend=self.detrend,
            scaling="spectrum",
        )

        if not isinstance(x, np.ndarray):
            # Trigger computation for Dask array
            raise ValueError(
                "Welch operation requires a Dask array, but received a non-ndarray."
            )
        return np.array(result)
Attributes
name = 'welch' class-attribute instance-attribute
n_fft = n_fft instance-attribute
window = window instance-attribute
hop_length = hop_length if hop_length is not None else self.win_length // 4 instance-attribute
win_length = win_length if win_length is not None else n_fft instance-attribute
average = average instance-attribute
detrend = detrend instance-attribute
noverlap = self.win_length - self.hop_length if hop_length is not None else None instance-attribute
Functions
__init__(sampling_rate, n_fft=2048, hop_length=None, win_length=None, window='hann', average='mean', detrend='constant')

Initialize Welch operation

Parameters

sampling_rate : float Sampling rate (Hz) n_fft : int, optional FFT size, default is 2048 window : str, optional Window function type, default is 'hann'

Source code in wandas/processing/spectral.py
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
def __init__(
    self,
    sampling_rate: float,
    n_fft: int = 2048,
    hop_length: Optional[int] = None,
    win_length: Optional[int] = None,
    window: str = "hann",
    average: str = "mean",
    detrend: str = "constant",
):
    """
    Initialize Welch operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    n_fft : int, optional
        FFT size, default is 2048
    window : str, optional
        Window function type, default is 'hann'
    """
    self.n_fft = n_fft
    self.win_length = win_length if win_length is not None else n_fft
    self.hop_length = hop_length if hop_length is not None else self.win_length // 4
    self.noverlap = (
        self.win_length - self.hop_length if hop_length is not None else None
    )
    self.window = window
    self.average = average
    self.detrend = detrend
    super().__init__(
        sampling_rate,
        n_fft=n_fft,
        win_length=self.win_length,
        hop_length=self.hop_length,
        window=window,
        average=average,
        detrend=detrend,
    )
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape (channels, samples)

Returns

tuple Output data shape (channels, freqs)

Source code in wandas/processing/spectral.py
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape (channels, samples)

    Returns
    -------
    tuple
        Output data shape (channels, freqs)
    """
    n_freqs = self.n_fft // 2 + 1
    return (*input_shape[:-1], n_freqs)

ABS

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Absolute value operation

Source code in wandas/processing/stats.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class ABS(AudioOperation[NDArrayReal, NDArrayReal]):
    """Absolute value operation"""

    name = "abs"

    def __init__(self, sampling_rate: float):
        """
        Initialize absolute value operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        """
        super().__init__(sampling_rate)

    def process(self, data: DaArray) -> DaArray:
        # map_blocksを使わず、直接Daskの集約関数を使用
        return da.abs(data)  # type: ignore [unused-ignore]
Attributes
name = 'abs' class-attribute instance-attribute
Functions
__init__(sampling_rate)

Initialize absolute value operation

Parameters

sampling_rate : float Sampling rate (Hz)

Source code in wandas/processing/stats.py
17
18
19
20
21
22
23
24
25
26
def __init__(self, sampling_rate: float):
    """
    Initialize absolute value operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    """
    super().__init__(sampling_rate)
process(data)
Source code in wandas/processing/stats.py
28
29
30
def process(self, data: DaArray) -> DaArray:
    # map_blocksを使わず、直接Daskの集約関数を使用
    return da.abs(data)  # type: ignore [unused-ignore]

ChannelDifference

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Channel difference calculation operation

Source code in wandas/processing/stats.py
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
class ChannelDifference(AudioOperation[NDArrayReal, NDArrayReal]):
    """Channel difference calculation operation"""

    name = "channel_difference"
    other_channel: int

    def __init__(self, sampling_rate: float, other_channel: int = 0):
        """
        Initialize channel difference calculation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        other_channel : int
            Channel to calculate difference with, default is 0
        """
        self.other_channel = other_channel
        super().__init__(sampling_rate, other_channel=other_channel)

    def process(self, data: DaArray) -> DaArray:
        # map_blocksを使わず、直接Daskの集約関数を使用
        result = data - data[self.other_channel]
        return result
Attributes
name = 'channel_difference' class-attribute instance-attribute
other_channel = other_channel instance-attribute
Functions
__init__(sampling_rate, other_channel=0)

Initialize channel difference calculation

Parameters

sampling_rate : float Sampling rate (Hz) other_channel : int Channel to calculate difference with, default is 0

Source code in wandas/processing/stats.py
83
84
85
86
87
88
89
90
91
92
93
94
95
def __init__(self, sampling_rate: float, other_channel: int = 0):
    """
    Initialize channel difference calculation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    other_channel : int
        Channel to calculate difference with, default is 0
    """
    self.other_channel = other_channel
    super().__init__(sampling_rate, other_channel=other_channel)
process(data)
Source code in wandas/processing/stats.py
 97
 98
 99
100
def process(self, data: DaArray) -> DaArray:
    # map_blocksを使わず、直接Daskの集約関数を使用
    result = data - data[self.other_channel]
    return result

Mean

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Mean calculation

Source code in wandas/processing/stats.py
67
68
69
70
71
72
73
74
class Mean(AudioOperation[NDArrayReal, NDArrayReal]):
    """Mean calculation"""

    name = "mean"

    def process(self, data: DaArray) -> DaArray:
        # Use Dask's aggregate function directly without map_blocks
        return data.mean(axis=0, keepdims=True)
Attributes
name = 'mean' class-attribute instance-attribute
Functions
process(data)
Source code in wandas/processing/stats.py
72
73
74
def process(self, data: DaArray) -> DaArray:
    # Use Dask's aggregate function directly without map_blocks
    return data.mean(axis=0, keepdims=True)

Power

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Power operation

Source code in wandas/processing/stats.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Power(AudioOperation[NDArrayReal, NDArrayReal]):
    """Power operation"""

    name = "power"

    def __init__(self, sampling_rate: float, exponent: float):
        """
        Initialize power operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        exponent : float
            Power exponent
        """
        super().__init__(sampling_rate)
        self.exp = exponent

    def process(self, data: DaArray) -> DaArray:
        # map_blocksを使わず、直接Daskの集約関数を使用
        return da.power(data, self.exp)  # type: ignore [unused-ignore]
Attributes
name = 'power' class-attribute instance-attribute
exp = exponent instance-attribute
Functions
__init__(sampling_rate, exponent)

Initialize power operation

Parameters

sampling_rate : float Sampling rate (Hz) exponent : float Power exponent

Source code in wandas/processing/stats.py
38
39
40
41
42
43
44
45
46
47
48
49
50
def __init__(self, sampling_rate: float, exponent: float):
    """
    Initialize power operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    exponent : float
        Power exponent
    """
    super().__init__(sampling_rate)
    self.exp = exponent
process(data)
Source code in wandas/processing/stats.py
52
53
54
def process(self, data: DaArray) -> DaArray:
    # map_blocksを使わず、直接Daskの集約関数を使用
    return da.power(data, self.exp)  # type: ignore [unused-ignore]

Sum

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Sum calculation

Source code in wandas/processing/stats.py
57
58
59
60
61
62
63
64
class Sum(AudioOperation[NDArrayReal, NDArrayReal]):
    """Sum calculation"""

    name = "sum"

    def process(self, data: DaArray) -> DaArray:
        # Use Dask's aggregate function directly without map_blocks
        return data.sum(axis=0, keepdims=True)
Attributes
name = 'sum' class-attribute instance-attribute
Functions
process(data)
Source code in wandas/processing/stats.py
62
63
64
def process(self, data: DaArray) -> DaArray:
    # Use Dask's aggregate function directly without map_blocks
    return data.sum(axis=0, keepdims=True)

ReSampling

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Resampling operation

Source code in wandas/processing/temporal.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class ReSampling(AudioOperation[NDArrayReal, NDArrayReal]):
    """Resampling operation"""

    name = "resampling"

    def __init__(self, sampling_rate: float, target_sr: float):
        """
        Initialize resampling operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        target_sampling_rate : float
            Target sampling rate (Hz)
        """
        super().__init__(sampling_rate, target_sr=target_sr)
        self.target_sr = target_sr

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape

        Returns
        -------
        tuple
            Output data shape
        """
        # Calculate length after resampling
        ratio = float(self.target_sr) / float(self.sampling_rate)
        n_samples = int(np.ceil(input_shape[-1] * ratio))
        return (*input_shape[:-1], n_samples)

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Create processor function for resampling operation"""
        logger.debug(f"Applying resampling to array with shape: {x.shape}")
        result: NDArrayReal = librosa.resample(
            x, orig_sr=self.sampling_rate, target_sr=self.target_sr
        )
        logger.debug(f"Resampling applied, returning result with shape: {result.shape}")
        return result
Attributes
name = 'resampling' class-attribute instance-attribute
target_sr = target_sr instance-attribute
Functions
__init__(sampling_rate, target_sr)

Initialize resampling operation

Parameters

sampling_rate : float Sampling rate (Hz) target_sampling_rate : float Target sampling rate (Hz)

Source code in wandas/processing/temporal.py
19
20
21
22
23
24
25
26
27
28
29
30
31
def __init__(self, sampling_rate: float, target_sr: float):
    """
    Initialize resampling operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    target_sampling_rate : float
        Target sampling rate (Hz)
    """
    super().__init__(sampling_rate, target_sr=target_sr)
    self.target_sr = target_sr
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape

Returns

tuple Output data shape

Source code in wandas/processing/temporal.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape

    Returns
    -------
    tuple
        Output data shape
    """
    # Calculate length after resampling
    ratio = float(self.target_sr) / float(self.sampling_rate)
    n_samples = int(np.ceil(input_shape[-1] * ratio))
    return (*input_shape[:-1], n_samples)

RmsTrend

Bases: AudioOperation[NDArrayReal, NDArrayReal]

RMS calculation

Source code in wandas/processing/temporal.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
class RmsTrend(AudioOperation[NDArrayReal, NDArrayReal]):
    """RMS calculation"""

    name = "rms_trend"
    frame_length: int
    hop_length: int
    Aw: bool

    def __init__(
        self,
        sampling_rate: float,
        frame_length: int = 2048,
        hop_length: int = 512,
        ref: Union[list[float], float] = 1.0,
        dB: bool = False,  # noqa: N803
        Aw: bool = False,  # noqa: N803
    ) -> None:
        """
        Initialize RMS calculation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        frame_length : int
            Frame length, default is 2048
        hop_length : int
            Hop length, default is 512
        ref : Union[list[float], float]
            Reference value(s) for dB calculation
        dB : bool
            Whether to convert to decibels
        Aw : bool
            Whether to apply A-weighting before RMS calculation
        """
        self.frame_length = frame_length
        self.hop_length = hop_length
        self.dB = dB
        self.Aw = Aw
        self.ref = np.array(ref if isinstance(ref, list) else [ref])
        super().__init__(
            sampling_rate,
            frame_length=frame_length,
            hop_length=hop_length,
            dB=dB,
            Aw=Aw,
            ref=self.ref,
        )

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape (channels, samples)

        Returns
        -------
        tuple
            Output data shape (channels, frames)
        """
        n_frames = librosa.feature.rms(
            y=np.ones((1, input_shape[-1])),
            frame_length=self.frame_length,
            hop_length=self.hop_length,
        ).shape[-1]
        return (*input_shape[:-1], n_frames)

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Create processor function for RMS calculation"""
        logger.debug(f"Applying RMS to array with shape: {x.shape}")

        if self.Aw:
            # Apply A-weighting
            _x = A_weight(x, self.sampling_rate)
            if isinstance(_x, np.ndarray):
                # A_weightがタプルを返す場合、最初の要素を使用
                x = _x
            elif isinstance(_x, tuple):
                # Use the first element if A_weight returns a tuple
                x = _x[0]
            else:
                raise ValueError("A_weighting returned an unexpected type.")

        # Calculate RMS
        result: NDArrayReal = librosa.feature.rms(
            y=x, frame_length=self.frame_length, hop_length=self.hop_length
        )[..., 0, :]

        if self.dB:
            # Convert to dB
            result = 20 * np.log10(
                np.maximum(result / self.ref[..., np.newaxis], 1e-12)
            )
        #
        logger.debug(f"RMS applied, returning result with shape: {result.shape}")
        return result
Attributes
name = 'rms_trend' class-attribute instance-attribute
frame_length = frame_length instance-attribute
hop_length = hop_length instance-attribute
Aw = Aw instance-attribute
dB = dB instance-attribute
ref = np.array(ref if isinstance(ref, list) else [ref]) instance-attribute
Functions
__init__(sampling_rate, frame_length=2048, hop_length=512, ref=1.0, dB=False, Aw=False)

Initialize RMS calculation

Parameters

sampling_rate : float Sampling rate (Hz) frame_length : int Frame length, default is 2048 hop_length : int Hop length, default is 512 ref : Union[list[float], float] Reference value(s) for dB calculation dB : bool Whether to convert to decibels Aw : bool Whether to apply A-weighting before RMS calculation

Source code in wandas/processing/temporal.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
def __init__(
    self,
    sampling_rate: float,
    frame_length: int = 2048,
    hop_length: int = 512,
    ref: Union[list[float], float] = 1.0,
    dB: bool = False,  # noqa: N803
    Aw: bool = False,  # noqa: N803
) -> None:
    """
    Initialize RMS calculation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    frame_length : int
        Frame length, default is 2048
    hop_length : int
        Hop length, default is 512
    ref : Union[list[float], float]
        Reference value(s) for dB calculation
    dB : bool
        Whether to convert to decibels
    Aw : bool
        Whether to apply A-weighting before RMS calculation
    """
    self.frame_length = frame_length
    self.hop_length = hop_length
    self.dB = dB
    self.Aw = Aw
    self.ref = np.array(ref if isinstance(ref, list) else [ref])
    super().__init__(
        sampling_rate,
        frame_length=frame_length,
        hop_length=hop_length,
        dB=dB,
        Aw=Aw,
        ref=self.ref,
    )
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape (channels, samples)

Returns

tuple Output data shape (channels, frames)

Source code in wandas/processing/temporal.py
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape (channels, samples)

    Returns
    -------
    tuple
        Output data shape (channels, frames)
    """
    n_frames = librosa.feature.rms(
        y=np.ones((1, input_shape[-1])),
        frame_length=self.frame_length,
        hop_length=self.hop_length,
    ).shape[-1]
    return (*input_shape[:-1], n_frames)

Trim

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Trimming operation

Source code in wandas/processing/temporal.py
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
class Trim(AudioOperation[NDArrayReal, NDArrayReal]):
    """Trimming operation"""

    name = "trim"

    def __init__(
        self,
        sampling_rate: float,
        start: float,
        end: float,
    ):
        """
        Initialize trimming operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        start : float
            Start time for trimming (seconds)
        end : float
            End time for trimming (seconds)
        """
        super().__init__(sampling_rate, start=start, end=end)
        self.start = start
        self.end = end
        self.start_sample = int(start * sampling_rate)
        self.end_sample = int(end * sampling_rate)
        logger.debug(
            f"Initialized Trim operation with start: {self.start}, end: {self.end}"
        )

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape

        Returns
        -------
        tuple
            Output data shape
        """
        # Calculate length after trimming
        # Exclude parts where there is no signal
        end_sample = min(self.end_sample, input_shape[-1])
        n_samples = end_sample - self.start_sample
        return (*input_shape[:-1], n_samples)

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Create processor function for trimming operation"""
        logger.debug(f"Applying trim to array with shape: {x.shape}")
        # Apply trimming
        result = x[..., self.start_sample : self.end_sample]
        logger.debug(f"Trim applied, returning result with shape: {result.shape}")
        return result
Attributes
name = 'trim' class-attribute instance-attribute
start = start instance-attribute
end = end instance-attribute
start_sample = int(start * sampling_rate) instance-attribute
end_sample = int(end * sampling_rate) instance-attribute
Functions
__init__(sampling_rate, start, end)

Initialize trimming operation

Parameters

sampling_rate : float Sampling rate (Hz) start : float Start time for trimming (seconds) end : float End time for trimming (seconds)

Source code in wandas/processing/temporal.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def __init__(
    self,
    sampling_rate: float,
    start: float,
    end: float,
):
    """
    Initialize trimming operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    start : float
        Start time for trimming (seconds)
    end : float
        End time for trimming (seconds)
    """
    super().__init__(sampling_rate, start=start, end=end)
    self.start = start
    self.end = end
    self.start_sample = int(start * sampling_rate)
    self.end_sample = int(end * sampling_rate)
    logger.debug(
        f"Initialized Trim operation with start: {self.start}, end: {self.end}"
    )
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape

Returns

tuple Output data shape

Source code in wandas/processing/temporal.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape

    Returns
    -------
    tuple
        Output data shape
    """
    # Calculate length after trimming
    # Exclude parts where there is no signal
    end_sample = min(self.end_sample, input_shape[-1])
    n_samples = end_sample - self.start_sample
    return (*input_shape[:-1], n_samples)

Functions

create_operation(name, sampling_rate, **params)

Create operation instance from name and parameters

Source code in wandas/processing/base.py
121
122
123
124
125
126
def create_operation(
    name: str, sampling_rate: float, **params: Any
) -> AudioOperation[Any, Any]:
    """Create operation instance from name and parameters"""
    operation_class = get_operation(name)
    return operation_class(sampling_rate, **params)

get_operation(name)

Get operation class by name

Source code in wandas/processing/base.py
114
115
116
117
118
def get_operation(name: str) -> type[AudioOperation[Any, Any]]:
    """Get operation class by name"""
    if name not in _OPERATION_REGISTRY:
        raise ValueError(f"Unknown operation type: {name}")
    return _OPERATION_REGISTRY[name]

register_operation(operation_class)

Register a new operation type

Source code in wandas/processing/base.py
103
104
105
106
107
108
109
110
111
def register_operation(operation_class: type) -> None:
    """Register a new operation type"""

    if not issubclass(operation_class, AudioOperation):
        raise TypeError("Strategy class must inherit from AudioOperation.")
    if inspect.isabstract(operation_class):
        raise TypeError("Cannot register abstract AudioOperation class.")

    _OPERATION_REGISTRY[operation_class.name] = operation_class

Modules

base

Attributes
logger = logging.getLogger(__name__) module-attribute
InputArrayType = TypeVar('InputArrayType', NDArrayReal, NDArrayComplex) module-attribute
OutputArrayType = TypeVar('OutputArrayType', NDArrayReal, NDArrayComplex) module-attribute
Classes
AudioOperation

Bases: Generic[InputArrayType, OutputArrayType]

Abstract base class for audio processing operations.

Source code in wandas/processing/base.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
class AudioOperation(Generic[InputArrayType, OutputArrayType]):
    """Abstract base class for audio processing operations."""

    # Class variable: operation name
    name: ClassVar[str]

    def __init__(self, sampling_rate: float, **params: Any):
        """
        Initialize AudioOperation.

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        **params : Any
            Operation-specific parameters
        """
        self.sampling_rate = sampling_rate
        self.params = params

        # Validate parameters during initialization
        self.validate_params()

        # Create processor function (lazy initialization possible)
        self._setup_processor()

        logger.debug(
            f"Initialized {self.__class__.__name__} operation with params: {params}"
        )

    def validate_params(self) -> None:
        """Validate parameters (raises exception if invalid)"""
        pass

    def _setup_processor(self) -> None:
        """Set up processor function (implemented by subclasses)"""
        pass

    def _process_array(self, x: InputArrayType) -> OutputArrayType:
        """Processing function (implemented by subclasses)"""
        # Default is no-op function
        raise NotImplementedError("Subclasses must implement this method.")

    @dask.delayed  # type: ignore [misc, unused-ignore]
    def process_array(self, x: InputArrayType) -> OutputArrayType:
        """Processing function wrapped with @dask.delayed"""
        # Default is no-op function
        logger.debug(f"Default process operation on data with shape: {x.shape}")
        return self._process_array(x)

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation (implemented by subclasses)

        Parameters
        ----------
        input_shape : tuple
            Input data shape

        Returns
        -------
        tuple
            Output data shape
        """
        raise NotImplementedError("Subclasses must implement this method.")

    def process(self, data: DaArray) -> DaArray:
        """
        Execute operation and return result
        data shape is (channels, samples)
        """
        # Add task as delayed processing
        logger.debug("Adding delayed operation to computation graph")
        delayed_result = self.process_array(data)
        # Convert delayed result to dask array and return
        output_shape = self.calculate_output_shape(data.shape)
        return _da_from_delayed(delayed_result, shape=output_shape, dtype=data.dtype)
Attributes
name class-attribute
sampling_rate = sampling_rate instance-attribute
params = params instance-attribute
Functions
__init__(sampling_rate, **params)

Initialize AudioOperation.

Parameters

sampling_rate : float Sampling rate (Hz) **params : Any Operation-specific parameters

Source code in wandas/processing/base.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def __init__(self, sampling_rate: float, **params: Any):
    """
    Initialize AudioOperation.

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    **params : Any
        Operation-specific parameters
    """
    self.sampling_rate = sampling_rate
    self.params = params

    # Validate parameters during initialization
    self.validate_params()

    # Create processor function (lazy initialization possible)
    self._setup_processor()

    logger.debug(
        f"Initialized {self.__class__.__name__} operation with params: {params}"
    )
validate_params()

Validate parameters (raises exception if invalid)

Source code in wandas/processing/base.py
50
51
52
def validate_params(self) -> None:
    """Validate parameters (raises exception if invalid)"""
    pass
process_array(x)

Processing function wrapped with @dask.delayed

Source code in wandas/processing/base.py
63
64
65
66
67
68
@dask.delayed  # type: ignore [misc, unused-ignore]
def process_array(self, x: InputArrayType) -> OutputArrayType:
    """Processing function wrapped with @dask.delayed"""
    # Default is no-op function
    logger.debug(f"Default process operation on data with shape: {x.shape}")
    return self._process_array(x)
calculate_output_shape(input_shape)

Calculate output data shape after operation (implemented by subclasses)

Parameters

input_shape : tuple Input data shape

Returns

tuple Output data shape

Source code in wandas/processing/base.py
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation (implemented by subclasses)

    Parameters
    ----------
    input_shape : tuple
        Input data shape

    Returns
    -------
    tuple
        Output data shape
    """
    raise NotImplementedError("Subclasses must implement this method.")
process(data)

Execute operation and return result data shape is (channels, samples)

Source code in wandas/processing/base.py
86
87
88
89
90
91
92
93
94
95
96
def process(self, data: DaArray) -> DaArray:
    """
    Execute operation and return result
    data shape is (channels, samples)
    """
    # Add task as delayed processing
    logger.debug("Adding delayed operation to computation graph")
    delayed_result = self.process_array(data)
    # Convert delayed result to dask array and return
    output_shape = self.calculate_output_shape(data.shape)
    return _da_from_delayed(delayed_result, shape=output_shape, dtype=data.dtype)
Functions
register_operation(operation_class)

Register a new operation type

Source code in wandas/processing/base.py
103
104
105
106
107
108
109
110
111
def register_operation(operation_class: type) -> None:
    """Register a new operation type"""

    if not issubclass(operation_class, AudioOperation):
        raise TypeError("Strategy class must inherit from AudioOperation.")
    if inspect.isabstract(operation_class):
        raise TypeError("Cannot register abstract AudioOperation class.")

    _OPERATION_REGISTRY[operation_class.name] = operation_class
get_operation(name)

Get operation class by name

Source code in wandas/processing/base.py
114
115
116
117
118
def get_operation(name: str) -> type[AudioOperation[Any, Any]]:
    """Get operation class by name"""
    if name not in _OPERATION_REGISTRY:
        raise ValueError(f"Unknown operation type: {name}")
    return _OPERATION_REGISTRY[name]
create_operation(name, sampling_rate, **params)

Create operation instance from name and parameters

Source code in wandas/processing/base.py
121
122
123
124
125
126
def create_operation(
    name: str, sampling_rate: float, **params: Any
) -> AudioOperation[Any, Any]:
    """Create operation instance from name and parameters"""
    operation_class = get_operation(name)
    return operation_class(sampling_rate, **params)

effects

Attributes
logger = logging.getLogger(__name__) module-attribute
Classes
HpssHarmonic

Bases: AudioOperation[NDArrayReal, NDArrayReal]

HPSS Harmonic operation

Source code in wandas/processing/effects.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class HpssHarmonic(AudioOperation[NDArrayReal, NDArrayReal]):
    """HPSS Harmonic operation"""

    name = "hpss_harmonic"

    def __init__(
        self,
        sampling_rate: float,
        **kwargs: Any,
    ):
        """
        Initialize HPSS Harmonic

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        """
        self.kwargs = kwargs
        super().__init__(sampling_rate, **kwargs)

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        return input_shape

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Create processor function for HPSS Harmonic"""
        logger.debug(f"Applying HPSS Harmonic to array with shape: {x.shape}")
        result: NDArrayReal = effects.harmonic(x, **self.kwargs)
        logger.debug(
            f"HPSS Harmonic applied, returning result with shape: {result.shape}"
        )
        return result
Attributes
name = 'hpss_harmonic' class-attribute instance-attribute
kwargs = kwargs instance-attribute
Functions
__init__(sampling_rate, **kwargs)

Initialize HPSS Harmonic

Parameters

sampling_rate : float Sampling rate (Hz)

Source code in wandas/processing/effects.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def __init__(
    self,
    sampling_rate: float,
    **kwargs: Any,
):
    """
    Initialize HPSS Harmonic

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    """
    self.kwargs = kwargs
    super().__init__(sampling_rate, **kwargs)
calculate_output_shape(input_shape)
Source code in wandas/processing/effects.py
35
36
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    return input_shape
HpssPercussive

Bases: AudioOperation[NDArrayReal, NDArrayReal]

HPSS Percussive operation

Source code in wandas/processing/effects.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class HpssPercussive(AudioOperation[NDArrayReal, NDArrayReal]):
    """HPSS Percussive operation"""

    name = "hpss_percussive"

    def __init__(
        self,
        sampling_rate: float,
        **kwargs: Any,
    ):
        """
        Initialize HPSS Percussive

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        """
        self.kwargs = kwargs
        super().__init__(sampling_rate, **kwargs)

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        return input_shape

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Create processor function for HPSS Percussive"""
        logger.debug(f"Applying HPSS Percussive to array with shape: {x.shape}")
        result: NDArrayReal = effects.percussive(x, **self.kwargs)
        logger.debug(
            f"HPSS Percussive applied, returning result with shape: {result.shape}"
        )
        return result
Attributes
name = 'hpss_percussive' class-attribute instance-attribute
kwargs = kwargs instance-attribute
Functions
__init__(sampling_rate, **kwargs)

Initialize HPSS Percussive

Parameters

sampling_rate : float Sampling rate (Hz)

Source code in wandas/processing/effects.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def __init__(
    self,
    sampling_rate: float,
    **kwargs: Any,
):
    """
    Initialize HPSS Percussive

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    """
    self.kwargs = kwargs
    super().__init__(sampling_rate, **kwargs)
calculate_output_shape(input_shape)
Source code in wandas/processing/effects.py
69
70
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    return input_shape
AddWithSNR

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Addition operation considering SNR

Source code in wandas/processing/effects.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
class AddWithSNR(AudioOperation[NDArrayReal, NDArrayReal]):
    """Addition operation considering SNR"""

    name = "add_with_snr"

    def __init__(self, sampling_rate: float, other: DaArray, snr: float = 1.0):
        """
        Initialize addition operation considering SNR

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        other : DaArray
            Noise signal to add (channel-frame format)
        snr : float
            Signal-to-noise ratio (dB)
        """
        super().__init__(sampling_rate, other=other, snr=snr)

        self.other = other
        self.snr = snr
        logger.debug(f"Initialized AddWithSNR operation with SNR: {snr} dB")

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape

        Returns
        -------
        tuple
            Output data shape (same as input)
        """
        return input_shape

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Perform addition processing considering SNR"""
        logger.debug(f"Applying SNR-based addition with shape: {x.shape}")
        other: NDArrayReal = self.other.compute()

        # Use multi-channel versions of calculate_rms and calculate_desired_noise_rms
        clean_rms = util.calculate_rms(x)
        other_rms = util.calculate_rms(other)

        # Adjust noise gain based on specified SNR (apply per channel)
        desired_noise_rms = util.calculate_desired_noise_rms(clean_rms, self.snr)

        # Apply gain per channel using broadcasting
        gain = desired_noise_rms / other_rms
        # Add adjusted noise to signal
        result: NDArrayReal = x + other * gain
        return result
Attributes
name = 'add_with_snr' class-attribute instance-attribute
other = other instance-attribute
snr = snr instance-attribute
Functions
__init__(sampling_rate, other, snr=1.0)

Initialize addition operation considering SNR

Parameters

sampling_rate : float Sampling rate (Hz) other : DaArray Noise signal to add (channel-frame format) snr : float Signal-to-noise ratio (dB)

Source code in wandas/processing/effects.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def __init__(self, sampling_rate: float, other: DaArray, snr: float = 1.0):
    """
    Initialize addition operation considering SNR

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    other : DaArray
        Noise signal to add (channel-frame format)
    snr : float
        Signal-to-noise ratio (dB)
    """
    super().__init__(sampling_rate, other=other, snr=snr)

    self.other = other
    self.snr = snr
    logger.debug(f"Initialized AddWithSNR operation with SNR: {snr} dB")
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape

Returns

tuple Output data shape (same as input)

Source code in wandas/processing/effects.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape

    Returns
    -------
    tuple
        Output data shape (same as input)
    """
    return input_shape
Functions
Modules

filters

Attributes
logger = logging.getLogger(__name__) module-attribute
Classes
HighPassFilter

Bases: AudioOperation[NDArrayReal, NDArrayReal]

High-pass filter operation

Source code in wandas/processing/filters.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class HighPassFilter(AudioOperation[NDArrayReal, NDArrayReal]):
    """High-pass filter operation"""

    name = "highpass_filter"

    def __init__(self, sampling_rate: float, cutoff: float, order: int = 4):
        """
        Initialize high-pass filter

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        cutoff : float
            Cutoff frequency (Hz)
        order : int, optional
            Filter order, default is 4
        """
        self.cutoff = cutoff
        self.order = order
        super().__init__(sampling_rate, cutoff=cutoff, order=order)

    def validate_params(self) -> None:
        """Validate parameters"""
        if self.cutoff <= 0 or self.cutoff >= self.sampling_rate / 2:
            limit = self.sampling_rate / 2
            raise ValueError(f"Cutoff frequency must be between 0 Hz and {limit} Hz")

    def _setup_processor(self) -> None:
        """Set up high-pass filter processor"""
        # Calculate filter coefficients (once) - safely retrieve from instance variables
        nyquist = 0.5 * self.sampling_rate
        normal_cutoff = self.cutoff / nyquist

        # Precompute and save filter coefficients
        self.b, self.a = signal.butter(self.order, normal_cutoff, btype="high")  # type: ignore [unused-ignore]
        logger.debug(f"Highpass filter coefficients calculated: b={self.b}, a={self.a}")

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        return input_shape

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Filter processing wrapped with @dask.delayed"""
        logger.debug(f"Applying highpass filter to array with shape: {x.shape}")
        result: NDArrayReal = signal.filtfilt(self.b, self.a, x, axis=1)
        logger.debug(f"Filter applied, returning result with shape: {result.shape}")
        return result
Attributes
name = 'highpass_filter' class-attribute instance-attribute
cutoff = cutoff instance-attribute
order = order instance-attribute
Functions
__init__(sampling_rate, cutoff, order=4)

Initialize high-pass filter

Parameters

sampling_rate : float Sampling rate (Hz) cutoff : float Cutoff frequency (Hz) order : int, optional Filter order, default is 4

Source code in wandas/processing/filters.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def __init__(self, sampling_rate: float, cutoff: float, order: int = 4):
    """
    Initialize high-pass filter

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    cutoff : float
        Cutoff frequency (Hz)
    order : int, optional
        Filter order, default is 4
    """
    self.cutoff = cutoff
    self.order = order
    super().__init__(sampling_rate, cutoff=cutoff, order=order)
validate_params()

Validate parameters

Source code in wandas/processing/filters.py
35
36
37
38
39
def validate_params(self) -> None:
    """Validate parameters"""
    if self.cutoff <= 0 or self.cutoff >= self.sampling_rate / 2:
        limit = self.sampling_rate / 2
        raise ValueError(f"Cutoff frequency must be between 0 Hz and {limit} Hz")
calculate_output_shape(input_shape)
Source code in wandas/processing/filters.py
51
52
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    return input_shape
LowPassFilter

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Low-pass filter operation

Source code in wandas/processing/filters.py
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
class LowPassFilter(AudioOperation[NDArrayReal, NDArrayReal]):
    """Low-pass filter operation"""

    name = "lowpass_filter"
    a: NDArrayReal
    b: NDArrayReal

    def __init__(self, sampling_rate: float, cutoff: float, order: int = 4):
        """
        Initialize low-pass filter

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        cutoff : float
            Cutoff frequency (Hz)
        order : int, optional
            Filter order, default is 4
        """
        self.cutoff = cutoff
        self.order = order
        super().__init__(sampling_rate, cutoff=cutoff, order=order)

    def validate_params(self) -> None:
        """Validate parameters"""
        if self.cutoff <= 0 or self.cutoff >= self.sampling_rate / 2:
            raise ValueError(
                f"Cutoff frequency must be between 0 Hz and {self.sampling_rate / 2} Hz"
            )

    def _setup_processor(self) -> None:
        """Set up low-pass filter processor"""
        nyquist = 0.5 * self.sampling_rate
        normal_cutoff = self.cutoff / nyquist

        # Precompute and save filter coefficients
        self.b, self.a = signal.butter(self.order, normal_cutoff, btype="low")  # type: ignore [unused-ignore]
        logger.debug(f"Lowpass filter coefficients calculated: b={self.b}, a={self.a}")

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        return input_shape

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Filter processing wrapped with @dask.delayed"""
        logger.debug(f"Applying lowpass filter to array with shape: {x.shape}")
        result: NDArrayReal = signal.filtfilt(self.b, self.a, x, axis=1)

        logger.debug(f"Filter applied, returning result with shape: {result.shape}")
        return result
Attributes
name = 'lowpass_filter' class-attribute instance-attribute
a instance-attribute
b instance-attribute
cutoff = cutoff instance-attribute
order = order instance-attribute
Functions
__init__(sampling_rate, cutoff, order=4)

Initialize low-pass filter

Parameters

sampling_rate : float Sampling rate (Hz) cutoff : float Cutoff frequency (Hz) order : int, optional Filter order, default is 4

Source code in wandas/processing/filters.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def __init__(self, sampling_rate: float, cutoff: float, order: int = 4):
    """
    Initialize low-pass filter

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    cutoff : float
        Cutoff frequency (Hz)
    order : int, optional
        Filter order, default is 4
    """
    self.cutoff = cutoff
    self.order = order
    super().__init__(sampling_rate, cutoff=cutoff, order=order)
validate_params()

Validate parameters

Source code in wandas/processing/filters.py
86
87
88
89
90
91
def validate_params(self) -> None:
    """Validate parameters"""
    if self.cutoff <= 0 or self.cutoff >= self.sampling_rate / 2:
        raise ValueError(
            f"Cutoff frequency must be between 0 Hz and {self.sampling_rate / 2} Hz"
        )
calculate_output_shape(input_shape)
Source code in wandas/processing/filters.py
102
103
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    return input_shape
BandPassFilter

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Band-pass filter operation

Source code in wandas/processing/filters.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
class BandPassFilter(AudioOperation[NDArrayReal, NDArrayReal]):
    """Band-pass filter operation"""

    name = "bandpass_filter"
    a: NDArrayReal
    b: NDArrayReal

    def __init__(
        self,
        sampling_rate: float,
        low_cutoff: float,
        high_cutoff: float,
        order: int = 4,
    ):
        """
        Initialize band-pass filter

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        low_cutoff : float
            Lower cutoff frequency (Hz)
        high_cutoff : float
            Higher cutoff frequency (Hz)
        order : int, optional
            Filter order, default is 4
        """
        self.low_cutoff = low_cutoff
        self.high_cutoff = high_cutoff
        self.order = order
        super().__init__(
            sampling_rate, low_cutoff=low_cutoff, high_cutoff=high_cutoff, order=order
        )

    def validate_params(self) -> None:
        """Validate parameters"""
        nyquist = self.sampling_rate / 2
        if self.low_cutoff <= 0 or self.low_cutoff >= nyquist:
            raise ValueError(
                f"Lower cutoff frequency must be between 0 Hz and {nyquist} Hz"
            )
        if self.high_cutoff <= 0 or self.high_cutoff >= nyquist:
            raise ValueError(
                f"Higher cutoff frequency must be between 0 Hz and {nyquist} Hz"
            )
        if self.low_cutoff >= self.high_cutoff:
            raise ValueError(
                f"Lower cutoff frequency ({self.low_cutoff} Hz) must be less than "
                f"higher cutoff frequency ({self.high_cutoff} Hz)"
            )

    def _setup_processor(self) -> None:
        """Set up band-pass filter processor"""
        nyquist = 0.5 * self.sampling_rate
        low_normal_cutoff = self.low_cutoff / nyquist
        high_normal_cutoff = self.high_cutoff / nyquist

        # Precompute and save filter coefficients
        self.b, self.a = signal.butter(
            self.order, [low_normal_cutoff, high_normal_cutoff], btype="band"
        )  # type: ignore [unused-ignore]
        logger.debug(f"Bandpass filter coefficients calculated: b={self.b}, a={self.a}")

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        return input_shape

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Filter processing wrapped with @dask.delayed"""
        logger.debug(f"Applying bandpass filter to array with shape: {x.shape}")
        result: NDArrayReal = signal.filtfilt(self.b, self.a, x, axis=1)
        logger.debug(f"Filter applied, returning result with shape: {result.shape}")
        return result
Attributes
name = 'bandpass_filter' class-attribute instance-attribute
a instance-attribute
b instance-attribute
low_cutoff = low_cutoff instance-attribute
high_cutoff = high_cutoff instance-attribute
order = order instance-attribute
Functions
__init__(sampling_rate, low_cutoff, high_cutoff, order=4)

Initialize band-pass filter

Parameters

sampling_rate : float Sampling rate (Hz) low_cutoff : float Lower cutoff frequency (Hz) high_cutoff : float Higher cutoff frequency (Hz) order : int, optional Filter order, default is 4

Source code in wandas/processing/filters.py
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
def __init__(
    self,
    sampling_rate: float,
    low_cutoff: float,
    high_cutoff: float,
    order: int = 4,
):
    """
    Initialize band-pass filter

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    low_cutoff : float
        Lower cutoff frequency (Hz)
    high_cutoff : float
        Higher cutoff frequency (Hz)
    order : int, optional
        Filter order, default is 4
    """
    self.low_cutoff = low_cutoff
    self.high_cutoff = high_cutoff
    self.order = order
    super().__init__(
        sampling_rate, low_cutoff=low_cutoff, high_cutoff=high_cutoff, order=order
    )
validate_params()

Validate parameters

Source code in wandas/processing/filters.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
def validate_params(self) -> None:
    """Validate parameters"""
    nyquist = self.sampling_rate / 2
    if self.low_cutoff <= 0 or self.low_cutoff >= nyquist:
        raise ValueError(
            f"Lower cutoff frequency must be between 0 Hz and {nyquist} Hz"
        )
    if self.high_cutoff <= 0 or self.high_cutoff >= nyquist:
        raise ValueError(
            f"Higher cutoff frequency must be between 0 Hz and {nyquist} Hz"
        )
    if self.low_cutoff >= self.high_cutoff:
        raise ValueError(
            f"Lower cutoff frequency ({self.low_cutoff} Hz) must be less than "
            f"higher cutoff frequency ({self.high_cutoff} Hz)"
        )
calculate_output_shape(input_shape)
Source code in wandas/processing/filters.py
178
179
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    return input_shape
AWeighting

Bases: AudioOperation[NDArrayReal, NDArrayReal]

A-weighting filter operation

Source code in wandas/processing/filters.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
class AWeighting(AudioOperation[NDArrayReal, NDArrayReal]):
    """A-weighting filter operation"""

    name = "a_weighting"

    def __init__(self, sampling_rate: float):
        """
        Initialize A-weighting filter

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        """
        super().__init__(sampling_rate)

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        return input_shape

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Create processor function for A-weighting filter"""
        logger.debug(f"Applying A-weighting to array with shape: {x.shape}")
        result = A_weight(x, self.sampling_rate)

        # Handle case where A_weight returns a tuple
        if isinstance(result, tuple):
            # Use the first element of the tuple
            result = result[0]

        logger.debug(
            f"A-weighting applied, returning result with shape: {result.shape}"
        )
        return np.array(result)
Attributes
name = 'a_weighting' class-attribute instance-attribute
Functions
__init__(sampling_rate)

Initialize A-weighting filter

Parameters

sampling_rate : float Sampling rate (Hz)

Source code in wandas/processing/filters.py
194
195
196
197
198
199
200
201
202
203
def __init__(self, sampling_rate: float):
    """
    Initialize A-weighting filter

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    """
    super().__init__(sampling_rate)
calculate_output_shape(input_shape)
Source code in wandas/processing/filters.py
205
206
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    return input_shape
Functions

spectral

Attributes
logger = logging.getLogger(__name__) module-attribute
Classes
FFT

Bases: AudioOperation[NDArrayReal, NDArrayComplex]

FFT (Fast Fourier Transform) operation

Source code in wandas/processing/spectral.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class FFT(AudioOperation[NDArrayReal, NDArrayComplex]):
    """FFT (Fast Fourier Transform) operation"""

    name = "fft"
    n_fft: Optional[int]
    window: str

    def __init__(
        self, sampling_rate: float, n_fft: Optional[int] = None, window: str = "hann"
    ):
        """
        Initialize FFT operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        n_fft : int, optional
            FFT size, default is None (determined by input size)
        window : str, optional
            Window function type, default is 'hann'
        """
        self.n_fft = n_fft
        self.window = window
        super().__init__(sampling_rate, n_fft=n_fft, window=window)

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        操作後の出力データの形状を計算します

        Parameters
        ----------
        input_shape : tuple
            入力データの形状 (channels, samples)

        Returns
        -------
        tuple
            出力データの形状 (channels, freqs)
        """
        n_freqs = self.n_fft // 2 + 1 if self.n_fft else input_shape[-1] // 2 + 1
        return (*input_shape[:-1], n_freqs)

    def _process_array(self, x: NDArrayReal) -> NDArrayComplex:
        """FFT操作のプロセッサ関数を作成"""
        from scipy.signal import get_window

        if self.n_fft is not None and x.shape[-1] > self.n_fft:
            # If n_fft is specified and input length exceeds it, truncate
            x = x[..., : self.n_fft]

        win = get_window(self.window, x.shape[-1])
        x = x * win
        result: NDArrayComplex = np.fft.rfft(x, n=self.n_fft, axis=-1)
        result[..., 1:-1] *= 2.0
        # 窓関数補正
        scaling_factor = np.sum(win)
        result = result / scaling_factor
        return result
Attributes
name = 'fft' class-attribute instance-attribute
n_fft = n_fft instance-attribute
window = window instance-attribute
Functions
__init__(sampling_rate, n_fft=None, window='hann')

Initialize FFT operation

Parameters

sampling_rate : float Sampling rate (Hz) n_fft : int, optional FFT size, default is None (determined by input size) window : str, optional Window function type, default is 'hann'

Source code in wandas/processing/spectral.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def __init__(
    self, sampling_rate: float, n_fft: Optional[int] = None, window: str = "hann"
):
    """
    Initialize FFT operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    n_fft : int, optional
        FFT size, default is None (determined by input size)
    window : str, optional
        Window function type, default is 'hann'
    """
    self.n_fft = n_fft
    self.window = window
    super().__init__(sampling_rate, n_fft=n_fft, window=window)
calculate_output_shape(input_shape)

操作後の出力データの形状を計算します

Parameters

input_shape : tuple 入力データの形状 (channels, samples)

Returns

tuple 出力データの形状 (channels, freqs)

Source code in wandas/processing/spectral.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    操作後の出力データの形状を計算します

    Parameters
    ----------
    input_shape : tuple
        入力データの形状 (channels, samples)

    Returns
    -------
    tuple
        出力データの形状 (channels, freqs)
    """
    n_freqs = self.n_fft // 2 + 1 if self.n_fft else input_shape[-1] // 2 + 1
    return (*input_shape[:-1], n_freqs)
IFFT

Bases: AudioOperation[NDArrayComplex, NDArrayReal]

IFFT (Inverse Fast Fourier Transform) operation

Source code in wandas/processing/spectral.py
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
class IFFT(AudioOperation[NDArrayComplex, NDArrayReal]):
    """IFFT (Inverse Fast Fourier Transform) operation"""

    name = "ifft"
    n_fft: Optional[int]
    window: str

    def __init__(
        self, sampling_rate: float, n_fft: Optional[int] = None, window: str = "hann"
    ):
        """
        Initialize IFFT operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        n_fft : Optional[int], optional
            IFFT size, default is None (determined based on input size)
        window : str, optional
            Window function type, default is 'hann'
        """
        self.n_fft = n_fft
        self.window = window
        super().__init__(sampling_rate, n_fft=n_fft, window=window)

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape (channels, freqs)

        Returns
        -------
        tuple
            Output data shape (channels, samples)
        """
        n_samples = 2 * (input_shape[-1] - 1) if self.n_fft is None else self.n_fft
        return (*input_shape[:-1], n_samples)

    def _process_array(self, x: NDArrayComplex) -> NDArrayReal:
        """Create processor function for IFFT operation"""
        logger.debug(f"Applying IFFT to array with shape: {x.shape}")

        # Restore frequency component scaling (remove the 2.0 multiplier applied in FFT)
        _x = x.copy()
        _x[..., 1:-1] /= 2.0

        # Execute IFFT
        result: NDArrayReal = np.fft.irfft(_x, n=self.n_fft, axis=-1)

        # Window function correction (inverse of FFT operation)
        from scipy.signal import get_window

        win = get_window(self.window, result.shape[-1])

        # Correct the FFT window function scaling
        scaling_factor = np.sum(win) / result.shape[-1]
        result = result / scaling_factor

        logger.debug(f"IFFT applied, returning result with shape: {result.shape}")
        return result
Attributes
name = 'ifft' class-attribute instance-attribute
n_fft = n_fft instance-attribute
window = window instance-attribute
Functions
__init__(sampling_rate, n_fft=None, window='hann')

Initialize IFFT operation

Parameters

sampling_rate : float Sampling rate (Hz) n_fft : Optional[int], optional IFFT size, default is None (determined based on input size) window : str, optional Window function type, default is 'hann'

Source code in wandas/processing/spectral.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def __init__(
    self, sampling_rate: float, n_fft: Optional[int] = None, window: str = "hann"
):
    """
    Initialize IFFT operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    n_fft : Optional[int], optional
        IFFT size, default is None (determined based on input size)
    window : str, optional
        Window function type, default is 'hann'
    """
    self.n_fft = n_fft
    self.window = window
    super().__init__(sampling_rate, n_fft=n_fft, window=window)
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape (channels, freqs)

Returns

tuple Output data shape (channels, samples)

Source code in wandas/processing/spectral.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape (channels, freqs)

    Returns
    -------
    tuple
        Output data shape (channels, samples)
    """
    n_samples = 2 * (input_shape[-1] - 1) if self.n_fft is None else self.n_fft
    return (*input_shape[:-1], n_samples)
STFT

Bases: AudioOperation[NDArrayReal, NDArrayComplex]

Short-Time Fourier Transform operation

Source code in wandas/processing/spectral.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
class STFT(AudioOperation[NDArrayReal, NDArrayComplex]):
    """Short-Time Fourier Transform operation"""

    name = "stft"

    def __init__(
        self,
        sampling_rate: float,
        n_fft: int = 2048,
        hop_length: Optional[int] = None,
        win_length: Optional[int] = None,
        window: str = "hann",
    ):
        self.n_fft = n_fft
        self.win_length = win_length if win_length is not None else n_fft
        self.hop_length = hop_length if hop_length is not None else self.win_length // 4
        self.noverlap = (
            self.win_length - self.hop_length if hop_length is not None else None
        )
        self.window = window

        self.SFT = ShortTimeFFT(
            win=get_window(window, self.win_length),
            hop=self.hop_length,
            fs=sampling_rate,
            mfft=self.n_fft,
            scale_to="magnitude",
        )
        super().__init__(
            sampling_rate,
            n_fft=n_fft,
            win_length=self.win_length,
            hop_length=self.hop_length,
            window=window,
        )

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape

        Returns
        -------
        tuple
            Output data shape
        """
        n_samples = input_shape[-1]
        n_f = len(self.SFT.f)
        n_t = len(self.SFT.t(n_samples))
        return (input_shape[0], n_f, n_t)

    def _process_array(self, x: NDArrayReal) -> NDArrayComplex:
        """Apply SciPy STFT processing to multiple channels at once"""
        logger.debug(f"Applying SciPy STFT to array with shape: {x.shape}")

        # Convert 1D input to 2D
        if x.ndim == 1:
            x = x.reshape(1, -1)

        # Apply STFT to all channels at once
        result: NDArrayComplex = self.SFT.stft(x)
        result[..., 1:-1, :] *= 2.0
        logger.debug(f"SciPy STFT applied, returning result with shape: {result.shape}")
        return result
Attributes
name = 'stft' class-attribute instance-attribute
n_fft = n_fft instance-attribute
win_length = win_length if win_length is not None else n_fft instance-attribute
hop_length = hop_length if hop_length is not None else self.win_length // 4 instance-attribute
noverlap = self.win_length - self.hop_length if hop_length is not None else None instance-attribute
window = window instance-attribute
SFT = ShortTimeFFT(win=get_window(window, self.win_length), hop=self.hop_length, fs=sampling_rate, mfft=self.n_fft, scale_to='magnitude') instance-attribute
Functions
__init__(sampling_rate, n_fft=2048, hop_length=None, win_length=None, window='hann')
Source code in wandas/processing/spectral.py
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
def __init__(
    self,
    sampling_rate: float,
    n_fft: int = 2048,
    hop_length: Optional[int] = None,
    win_length: Optional[int] = None,
    window: str = "hann",
):
    self.n_fft = n_fft
    self.win_length = win_length if win_length is not None else n_fft
    self.hop_length = hop_length if hop_length is not None else self.win_length // 4
    self.noverlap = (
        self.win_length - self.hop_length if hop_length is not None else None
    )
    self.window = window

    self.SFT = ShortTimeFFT(
        win=get_window(window, self.win_length),
        hop=self.hop_length,
        fs=sampling_rate,
        mfft=self.n_fft,
        scale_to="magnitude",
    )
    super().__init__(
        sampling_rate,
        n_fft=n_fft,
        win_length=self.win_length,
        hop_length=self.hop_length,
        window=window,
    )
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape

Returns

tuple Output data shape

Source code in wandas/processing/spectral.py
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape

    Returns
    -------
    tuple
        Output data shape
    """
    n_samples = input_shape[-1]
    n_f = len(self.SFT.f)
    n_t = len(self.SFT.t(n_samples))
    return (input_shape[0], n_f, n_t)
ISTFT

Bases: AudioOperation[NDArrayComplex, NDArrayReal]

Inverse Short-Time Fourier Transform operation

Source code in wandas/processing/spectral.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
class ISTFT(AudioOperation[NDArrayComplex, NDArrayReal]):
    """Inverse Short-Time Fourier Transform operation"""

    name = "istft"

    def __init__(
        self,
        sampling_rate: float,
        n_fft: int = 2048,
        hop_length: Optional[int] = None,
        win_length: Optional[int] = None,
        window: str = "hann",
        length: Optional[int] = None,
    ):
        self.n_fft = n_fft
        self.win_length = win_length if win_length is not None else n_fft
        self.hop_length = hop_length if hop_length is not None else self.win_length // 4
        self.window = window
        self.length = length

        # Instantiate ShortTimeFFT for ISTFT calculation
        self.SFT = ShortTimeFFT(
            win=get_window(window, self.win_length),
            hop=self.hop_length,
            fs=sampling_rate,
            mfft=self.n_fft,
            scale_to="magnitude",  # Consistent scaling with STFT
        )

        super().__init__(
            sampling_rate,
            n_fft=n_fft,
            win_length=self.win_length,
            hop_length=self.hop_length,
            window=window,
            length=length,
        )

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape (channels, freqs, time_frames)

        Returns
        -------
        tuple
            Output data shape (channels, samples)
        """
        k0: int = 0
        q_max = input_shape[-1] + self.SFT.p_min
        k_max = (q_max - 1) * self.SFT.hop + self.SFT.m_num - self.SFT.m_num_mid
        k_q0, k_q1 = self.SFT.nearest_k_p(k0), self.SFT.nearest_k_p(k_max, left=False)
        n_pts = k_q1 - k_q0 + self.SFT.m_num - self.SFT.m_num_mid

        return input_shape[:-2] + (n_pts,)

    def _process_array(self, x: NDArrayComplex) -> NDArrayReal:
        """
        Apply SciPy ISTFT processing to multiple channels at once using ShortTimeFFT"""
        logger.debug(
            f"Applying SciPy ISTFT (ShortTimeFFT) to array with shape: {x.shape}"
        )

        # Convert 2D input to 3D (assume single channel)
        if x.ndim == 2:
            x = x.reshape(1, *x.shape)

        # Adjust scaling back if STFT applied factor of 2
        _x = np.copy(x)
        _x[..., 1:-1, :] /= 2.0

        # Apply ISTFT using the ShortTimeFFT instance
        result: NDArrayReal = self.SFT.istft(_x)

        # Trim to desired length if specified
        if self.length is not None:
            result = result[..., : self.length]

        logger.debug(
            f"ShortTimeFFT applied, returning result with shape: {result.shape}"
        )
        return result
Attributes
name = 'istft' class-attribute instance-attribute
n_fft = n_fft instance-attribute
win_length = win_length if win_length is not None else n_fft instance-attribute
hop_length = hop_length if hop_length is not None else self.win_length // 4 instance-attribute
window = window instance-attribute
length = length instance-attribute
SFT = ShortTimeFFT(win=get_window(window, self.win_length), hop=self.hop_length, fs=sampling_rate, mfft=self.n_fft, scale_to='magnitude') instance-attribute
Functions
__init__(sampling_rate, n_fft=2048, hop_length=None, win_length=None, window='hann', length=None)
Source code in wandas/processing/spectral.py
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
def __init__(
    self,
    sampling_rate: float,
    n_fft: int = 2048,
    hop_length: Optional[int] = None,
    win_length: Optional[int] = None,
    window: str = "hann",
    length: Optional[int] = None,
):
    self.n_fft = n_fft
    self.win_length = win_length if win_length is not None else n_fft
    self.hop_length = hop_length if hop_length is not None else self.win_length // 4
    self.window = window
    self.length = length

    # Instantiate ShortTimeFFT for ISTFT calculation
    self.SFT = ShortTimeFFT(
        win=get_window(window, self.win_length),
        hop=self.hop_length,
        fs=sampling_rate,
        mfft=self.n_fft,
        scale_to="magnitude",  # Consistent scaling with STFT
    )

    super().__init__(
        sampling_rate,
        n_fft=n_fft,
        win_length=self.win_length,
        hop_length=self.hop_length,
        window=window,
        length=length,
    )
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape (channels, freqs, time_frames)

Returns

tuple Output data shape (channels, samples)

Source code in wandas/processing/spectral.py
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape (channels, freqs, time_frames)

    Returns
    -------
    tuple
        Output data shape (channels, samples)
    """
    k0: int = 0
    q_max = input_shape[-1] + self.SFT.p_min
    k_max = (q_max - 1) * self.SFT.hop + self.SFT.m_num - self.SFT.m_num_mid
    k_q0, k_q1 = self.SFT.nearest_k_p(k0), self.SFT.nearest_k_p(k_max, left=False)
    n_pts = k_q1 - k_q0 + self.SFT.m_num - self.SFT.m_num_mid

    return input_shape[:-2] + (n_pts,)
Welch

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Welch

Source code in wandas/processing/spectral.py
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
class Welch(AudioOperation[NDArrayReal, NDArrayReal]):
    """Welch"""

    name = "welch"
    n_fft: int
    window: str
    hop_length: Optional[int]
    win_length: Optional[int]
    average: str
    detrend: str

    def __init__(
        self,
        sampling_rate: float,
        n_fft: int = 2048,
        hop_length: Optional[int] = None,
        win_length: Optional[int] = None,
        window: str = "hann",
        average: str = "mean",
        detrend: str = "constant",
    ):
        """
        Initialize Welch operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        n_fft : int, optional
            FFT size, default is 2048
        window : str, optional
            Window function type, default is 'hann'
        """
        self.n_fft = n_fft
        self.win_length = win_length if win_length is not None else n_fft
        self.hop_length = hop_length if hop_length is not None else self.win_length // 4
        self.noverlap = (
            self.win_length - self.hop_length if hop_length is not None else None
        )
        self.window = window
        self.average = average
        self.detrend = detrend
        super().__init__(
            sampling_rate,
            n_fft=n_fft,
            win_length=self.win_length,
            hop_length=self.hop_length,
            window=window,
            average=average,
            detrend=detrend,
        )

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape (channels, samples)

        Returns
        -------
        tuple
            Output data shape (channels, freqs)
        """
        n_freqs = self.n_fft // 2 + 1
        return (*input_shape[:-1], n_freqs)

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Create processor function for Welch operation"""
        from scipy import signal as ss

        _, result = ss.welch(
            x,
            nperseg=self.win_length,
            noverlap=self.noverlap,
            nfft=self.n_fft,
            window=self.window,
            average=self.average,
            detrend=self.detrend,
            scaling="spectrum",
        )

        if not isinstance(x, np.ndarray):
            # Trigger computation for Dask array
            raise ValueError(
                "Welch operation requires a Dask array, but received a non-ndarray."
            )
        return np.array(result)
Attributes
name = 'welch' class-attribute instance-attribute
n_fft = n_fft instance-attribute
win_length = win_length if win_length is not None else n_fft instance-attribute
hop_length = hop_length if hop_length is not None else self.win_length // 4 instance-attribute
noverlap = self.win_length - self.hop_length if hop_length is not None else None instance-attribute
window = window instance-attribute
average = average instance-attribute
detrend = detrend instance-attribute
Functions
__init__(sampling_rate, n_fft=2048, hop_length=None, win_length=None, window='hann', average='mean', detrend='constant')

Initialize Welch operation

Parameters

sampling_rate : float Sampling rate (Hz) n_fft : int, optional FFT size, default is 2048 window : str, optional Window function type, default is 'hann'

Source code in wandas/processing/spectral.py
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
def __init__(
    self,
    sampling_rate: float,
    n_fft: int = 2048,
    hop_length: Optional[int] = None,
    win_length: Optional[int] = None,
    window: str = "hann",
    average: str = "mean",
    detrend: str = "constant",
):
    """
    Initialize Welch operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    n_fft : int, optional
        FFT size, default is 2048
    window : str, optional
        Window function type, default is 'hann'
    """
    self.n_fft = n_fft
    self.win_length = win_length if win_length is not None else n_fft
    self.hop_length = hop_length if hop_length is not None else self.win_length // 4
    self.noverlap = (
        self.win_length - self.hop_length if hop_length is not None else None
    )
    self.window = window
    self.average = average
    self.detrend = detrend
    super().__init__(
        sampling_rate,
        n_fft=n_fft,
        win_length=self.win_length,
        hop_length=self.hop_length,
        window=window,
        average=average,
        detrend=detrend,
    )
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape (channels, samples)

Returns

tuple Output data shape (channels, freqs)

Source code in wandas/processing/spectral.py
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape (channels, samples)

    Returns
    -------
    tuple
        Output data shape (channels, freqs)
    """
    n_freqs = self.n_fft // 2 + 1
    return (*input_shape[:-1], n_freqs)
NOctSpectrum

Bases: AudioOperation[NDArrayReal, NDArrayReal]

N-octave spectrum operation

Source code in wandas/processing/spectral.py
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
class NOctSpectrum(AudioOperation[NDArrayReal, NDArrayReal]):
    """N-octave spectrum operation"""

    name = "noct_spectrum"

    def __init__(
        self,
        sampling_rate: float,
        fmin: float,
        fmax: float,
        n: int = 3,
        G: int = 10,  # noqa: N803
        fr: int = 1000,
    ):
        """
        Initialize N-octave spectrum

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        fmin : float
            Minimum frequency (Hz)
        fmax : float
            Maximum frequency (Hz)
        n : int, optional
            Number of octave divisions, default is 3
        G : int, optional
            Reference level, default is 10
        fr : int, optional
            Reference frequency, default is 1000
        """
        super().__init__(sampling_rate, fmin=fmin, fmax=fmax, n=n, G=G, fr=fr)
        self.fmin = fmin
        self.fmax = fmax
        self.n = n
        self.G = G
        self.fr = fr

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape

        Returns
        -------
        tuple
            Output data shape
        """
        # Calculate output shape for octave spectrum
        _, fpref = _center_freq(
            fmin=self.fmin, fmax=self.fmax, n=self.n, G=self.G, fr=self.fr
        )
        return (input_shape[0], fpref.shape[0])

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Create processor function for octave spectrum"""
        logger.debug(f"Applying NoctSpectrum to array with shape: {x.shape}")
        spec, _ = noct_spectrum(
            sig=x.T,
            fs=self.sampling_rate,
            fmin=self.fmin,
            fmax=self.fmax,
            n=self.n,
            G=self.G,
            fr=self.fr,
        )
        if spec.ndim == 1:
            # Add channel dimension for 1D
            spec = np.expand_dims(spec, axis=0)
        else:
            spec = spec.T
        logger.debug(f"NoctSpectrum applied, returning result with shape: {spec.shape}")
        return np.array(spec)
Attributes
name = 'noct_spectrum' class-attribute instance-attribute
fmin = fmin instance-attribute
fmax = fmax instance-attribute
n = n instance-attribute
G = G instance-attribute
fr = fr instance-attribute
Functions
__init__(sampling_rate, fmin, fmax, n=3, G=10, fr=1000)

Initialize N-octave spectrum

Parameters

sampling_rate : float Sampling rate (Hz) fmin : float Minimum frequency (Hz) fmax : float Maximum frequency (Hz) n : int, optional Number of octave divisions, default is 3 G : int, optional Reference level, default is 10 fr : int, optional Reference frequency, default is 1000

Source code in wandas/processing/spectral.py
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
def __init__(
    self,
    sampling_rate: float,
    fmin: float,
    fmax: float,
    n: int = 3,
    G: int = 10,  # noqa: N803
    fr: int = 1000,
):
    """
    Initialize N-octave spectrum

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    fmin : float
        Minimum frequency (Hz)
    fmax : float
        Maximum frequency (Hz)
    n : int, optional
        Number of octave divisions, default is 3
    G : int, optional
        Reference level, default is 10
    fr : int, optional
        Reference frequency, default is 1000
    """
    super().__init__(sampling_rate, fmin=fmin, fmax=fmax, n=n, G=G, fr=fr)
    self.fmin = fmin
    self.fmax = fmax
    self.n = n
    self.G = G
    self.fr = fr
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape

Returns

tuple Output data shape

Source code in wandas/processing/spectral.py
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape

    Returns
    -------
    tuple
        Output data shape
    """
    # Calculate output shape for octave spectrum
    _, fpref = _center_freq(
        fmin=self.fmin, fmax=self.fmax, n=self.n, G=self.G, fr=self.fr
    )
    return (input_shape[0], fpref.shape[0])
NOctSynthesis

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Octave synthesis operation

Source code in wandas/processing/spectral.py
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
class NOctSynthesis(AudioOperation[NDArrayReal, NDArrayReal]):
    """Octave synthesis operation"""

    name = "noct_synthesis"

    def __init__(
        self,
        sampling_rate: float,
        fmin: float,
        fmax: float,
        n: int = 3,
        G: int = 10,  # noqa: N803
        fr: int = 1000,
    ):
        """
        Initialize octave synthesis

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        fmin : float
            Minimum frequency (Hz)
        fmax : float
            Maximum frequency (Hz)
        n : int, optional
            Number of octave divisions, default is 3
        G : int, optional
            Reference level, default is 10
        fr : int, optional
            Reference frequency, default is 1000
        """
        super().__init__(sampling_rate, fmin=fmin, fmax=fmax, n=n, G=G, fr=fr)

        self.fmin = fmin
        self.fmax = fmax
        self.n = n
        self.G = G
        self.fr = fr

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape

        Returns
        -------
        tuple
            Output data shape
        """
        # Calculate output shape for octave spectrum
        _, fpref = _center_freq(
            fmin=self.fmin, fmax=self.fmax, n=self.n, G=self.G, fr=self.fr
        )
        return (input_shape[0], fpref.shape[0])

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Create processor function for octave synthesis"""
        logger.debug(f"Applying NoctSynthesis to array with shape: {x.shape}")
        # Calculate n from shape[-1]
        n = x.shape[-1]  # Calculate n from shape[-1]
        if n % 2 == 0:
            n = n * 2 - 1
        else:
            n = (n - 1) * 2
        freqs = np.fft.rfftfreq(n, d=1 / self.sampling_rate)
        result, _ = noct_synthesis(
            spectrum=np.abs(x).T,
            freqs=freqs,
            fmin=self.fmin,
            fmax=self.fmax,
            n=self.n,
            G=self.G,
            fr=self.fr,
        )
        result = result.T
        logger.debug(
            f"NoctSynthesis applied, returning result with shape: {result.shape}"
        )
        return np.array(result)
Attributes
name = 'noct_synthesis' class-attribute instance-attribute
fmin = fmin instance-attribute
fmax = fmax instance-attribute
n = n instance-attribute
G = G instance-attribute
fr = fr instance-attribute
Functions
__init__(sampling_rate, fmin, fmax, n=3, G=10, fr=1000)

Initialize octave synthesis

Parameters

sampling_rate : float Sampling rate (Hz) fmin : float Minimum frequency (Hz) fmax : float Maximum frequency (Hz) n : int, optional Number of octave divisions, default is 3 G : int, optional Reference level, default is 10 fr : int, optional Reference frequency, default is 1000

Source code in wandas/processing/spectral.py
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
def __init__(
    self,
    sampling_rate: float,
    fmin: float,
    fmax: float,
    n: int = 3,
    G: int = 10,  # noqa: N803
    fr: int = 1000,
):
    """
    Initialize octave synthesis

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    fmin : float
        Minimum frequency (Hz)
    fmax : float
        Maximum frequency (Hz)
    n : int, optional
        Number of octave divisions, default is 3
    G : int, optional
        Reference level, default is 10
    fr : int, optional
        Reference frequency, default is 1000
    """
    super().__init__(sampling_rate, fmin=fmin, fmax=fmax, n=n, G=G, fr=fr)

    self.fmin = fmin
    self.fmax = fmax
    self.n = n
    self.G = G
    self.fr = fr
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape

Returns

tuple Output data shape

Source code in wandas/processing/spectral.py
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape

    Returns
    -------
    tuple
        Output data shape
    """
    # Calculate output shape for octave spectrum
    _, fpref = _center_freq(
        fmin=self.fmin, fmax=self.fmax, n=self.n, G=self.G, fr=self.fr
    )
    return (input_shape[0], fpref.shape[0])
Coherence

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Coherence estimation operation

Source code in wandas/processing/spectral.py
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
class Coherence(AudioOperation[NDArrayReal, NDArrayReal]):
    """Coherence estimation operation"""

    name = "coherence"

    def __init__(
        self,
        sampling_rate: float,
        n_fft: int,
        hop_length: int,
        win_length: int,
        window: str,
        detrend: str,
    ):
        """
        Initialize coherence estimation operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        n_fft : int
            FFT size
        hop_length : int
            Hop length
        win_length : int
            Window length
        window : str
            Window function
        detrend : str
            Type of detrend
        """
        self.n_fft = n_fft
        self.win_length = win_length if win_length is not None else n_fft
        self.hop_length = hop_length if hop_length is not None else self.win_length // 4
        self.window = window
        self.detrend = detrend
        super().__init__(
            sampling_rate,
            n_fft=n_fft,
            hop_length=self.hop_length,
            win_length=self.win_length,
            window=window,
            detrend=detrend,
        )

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape (channels, samples)

        Returns
        -------
        tuple
            Output data shape (channels * channels, freqs)
        """
        n_channels = input_shape[0]
        n_freqs = self.n_fft // 2 + 1
        return (n_channels * n_channels, n_freqs)

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Processor function for coherence estimation operation"""
        logger.debug(f"Applying coherence estimation to array with shape: {x.shape}")
        from scipy import signal as ss

        _, coh = ss.coherence(
            x=x[:, np.newaxis],
            y=x[np.newaxis, :],
            fs=self.sampling_rate,
            nperseg=self.win_length,
            noverlap=self.win_length - self.hop_length,
            nfft=self.n_fft,
            window=self.window,
            detrend=self.detrend,
        )

        # Reshape result to (n_channels * n_channels, n_freqs)
        result: NDArrayReal = coh.transpose(1, 0, 2).reshape(-1, coh.shape[-1])

        logger.debug(f"Coherence estimation applied, result shape: {result.shape}")
        return result
Attributes
name = 'coherence' class-attribute instance-attribute
n_fft = n_fft instance-attribute
win_length = win_length if win_length is not None else n_fft instance-attribute
hop_length = hop_length if hop_length is not None else self.win_length // 4 instance-attribute
window = window instance-attribute
detrend = detrend instance-attribute
Functions
__init__(sampling_rate, n_fft, hop_length, win_length, window, detrend)

Initialize coherence estimation operation

Parameters

sampling_rate : float Sampling rate (Hz) n_fft : int FFT size hop_length : int Hop length win_length : int Window length window : str Window function detrend : str Type of detrend

Source code in wandas/processing/spectral.py
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
def __init__(
    self,
    sampling_rate: float,
    n_fft: int,
    hop_length: int,
    win_length: int,
    window: str,
    detrend: str,
):
    """
    Initialize coherence estimation operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    n_fft : int
        FFT size
    hop_length : int
        Hop length
    win_length : int
        Window length
    window : str
        Window function
    detrend : str
        Type of detrend
    """
    self.n_fft = n_fft
    self.win_length = win_length if win_length is not None else n_fft
    self.hop_length = hop_length if hop_length is not None else self.win_length // 4
    self.window = window
    self.detrend = detrend
    super().__init__(
        sampling_rate,
        n_fft=n_fft,
        hop_length=self.hop_length,
        win_length=self.win_length,
        window=window,
        detrend=detrend,
    )
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape (channels, samples)

Returns

tuple Output data shape (channels * channels, freqs)

Source code in wandas/processing/spectral.py
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape (channels, samples)

    Returns
    -------
    tuple
        Output data shape (channels * channels, freqs)
    """
    n_channels = input_shape[0]
    n_freqs = self.n_fft // 2 + 1
    return (n_channels * n_channels, n_freqs)
CSD

Bases: AudioOperation[NDArrayReal, NDArrayComplex]

Cross-spectral density estimation operation

Source code in wandas/processing/spectral.py
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
class CSD(AudioOperation[NDArrayReal, NDArrayComplex]):
    """Cross-spectral density estimation operation"""

    name = "csd"

    def __init__(
        self,
        sampling_rate: float,
        n_fft: int,
        hop_length: int,
        win_length: int,
        window: str,
        detrend: str,
        scaling: str,
        average: str,
    ):
        """
        Initialize cross-spectral density estimation operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        n_fft : int
            FFT size
        hop_length : int
            Hop length
        win_length : int
            Window length
        window : str
            Window function
        detrend : str
            Type of detrend
        scaling : str
            Type of scaling
        average : str
            Method of averaging
        """
        self.n_fft = n_fft
        self.win_length = win_length if win_length is not None else n_fft
        self.hop_length = hop_length if hop_length is not None else self.win_length // 4
        self.window = window
        self.detrend = detrend
        self.scaling = scaling
        self.average = average
        super().__init__(
            sampling_rate,
            n_fft=n_fft,
            hop_length=self.hop_length,
            win_length=self.win_length,
            window=window,
            detrend=detrend,
            scaling=scaling,
            average=average,
        )

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape (channels, samples)

        Returns
        -------
        tuple
            Output data shape (channels * channels, freqs)
        """
        n_channels = input_shape[0]
        n_freqs = self.n_fft // 2 + 1
        return (n_channels * n_channels, n_freqs)

    def _process_array(self, x: NDArrayReal) -> NDArrayComplex:
        """Processor function for cross-spectral density estimation operation"""
        logger.debug(f"Applying CSD estimation to array with shape: {x.shape}")
        from scipy import signal as ss

        # Calculate all combinations using scipy's csd function
        _, csd_result = ss.csd(
            x=x[:, np.newaxis],
            y=x[np.newaxis, :],
            fs=self.sampling_rate,
            nperseg=self.win_length,
            noverlap=self.win_length - self.hop_length,
            nfft=self.n_fft,
            window=self.window,
            detrend=self.detrend,
            scaling=self.scaling,
            average=self.average,
        )

        # Reshape result to (n_channels * n_channels, n_freqs)
        result: NDArrayComplex = csd_result.transpose(1, 0, 2).reshape(
            -1, csd_result.shape[-1]
        )

        logger.debug(f"CSD estimation applied, result shape: {result.shape}")
        return result
Attributes
name = 'csd' class-attribute instance-attribute
n_fft = n_fft instance-attribute
win_length = win_length if win_length is not None else n_fft instance-attribute
hop_length = hop_length if hop_length is not None else self.win_length // 4 instance-attribute
window = window instance-attribute
detrend = detrend instance-attribute
scaling = scaling instance-attribute
average = average instance-attribute
Functions
__init__(sampling_rate, n_fft, hop_length, win_length, window, detrend, scaling, average)

Initialize cross-spectral density estimation operation

Parameters

sampling_rate : float Sampling rate (Hz) n_fft : int FFT size hop_length : int Hop length win_length : int Window length window : str Window function detrend : str Type of detrend scaling : str Type of scaling average : str Method of averaging

Source code in wandas/processing/spectral.py
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
def __init__(
    self,
    sampling_rate: float,
    n_fft: int,
    hop_length: int,
    win_length: int,
    window: str,
    detrend: str,
    scaling: str,
    average: str,
):
    """
    Initialize cross-spectral density estimation operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    n_fft : int
        FFT size
    hop_length : int
        Hop length
    win_length : int
        Window length
    window : str
        Window function
    detrend : str
        Type of detrend
    scaling : str
        Type of scaling
    average : str
        Method of averaging
    """
    self.n_fft = n_fft
    self.win_length = win_length if win_length is not None else n_fft
    self.hop_length = hop_length if hop_length is not None else self.win_length // 4
    self.window = window
    self.detrend = detrend
    self.scaling = scaling
    self.average = average
    super().__init__(
        sampling_rate,
        n_fft=n_fft,
        hop_length=self.hop_length,
        win_length=self.win_length,
        window=window,
        detrend=detrend,
        scaling=scaling,
        average=average,
    )
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape (channels, samples)

Returns

tuple Output data shape (channels * channels, freqs)

Source code in wandas/processing/spectral.py
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape (channels, samples)

    Returns
    -------
    tuple
        Output data shape (channels * channels, freqs)
    """
    n_channels = input_shape[0]
    n_freqs = self.n_fft // 2 + 1
    return (n_channels * n_channels, n_freqs)
TransferFunction

Bases: AudioOperation[NDArrayReal, NDArrayComplex]

Transfer function estimation operation

Source code in wandas/processing/spectral.py
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
class TransferFunction(AudioOperation[NDArrayReal, NDArrayComplex]):
    """Transfer function estimation operation"""

    name = "transfer_function"

    def __init__(
        self,
        sampling_rate: float,
        n_fft: int,
        hop_length: int,
        win_length: int,
        window: str,
        detrend: str,
        scaling: str = "spectrum",
        average: str = "mean",
    ):
        """
        Initialize transfer function estimation operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        n_fft : int
            FFT size
        hop_length : int
            Hop length
        win_length : int
            Window length
        window : str
            Window function
        detrend : str
            Type of detrend
        scaling : str
            Type of scaling
        average : str
            Method of averaging
        """
        self.n_fft = n_fft
        self.win_length = win_length if win_length is not None else n_fft
        self.hop_length = hop_length if hop_length is not None else self.win_length // 4
        self.window = window
        self.detrend = detrend
        self.scaling = scaling
        self.average = average
        super().__init__(
            sampling_rate,
            n_fft=n_fft,
            hop_length=self.hop_length,
            win_length=self.win_length,
            window=window,
            detrend=detrend,
            scaling=scaling,
            average=average,
        )

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape (channels, samples)

        Returns
        -------
        tuple
            Output data shape (channels * channels, freqs)
        """
        n_channels = input_shape[0]
        n_freqs = self.n_fft // 2 + 1
        return (n_channels * n_channels, n_freqs)

    def _process_array(self, x: NDArrayReal) -> NDArrayComplex:
        """Processor function for transfer function estimation operation"""
        logger.debug(
            f"Applying transfer function estimation to array with shape: {x.shape}"
        )
        from scipy import signal as ss

        # Calculate cross-spectral density between all channels
        f, p_yx = ss.csd(
            x=x[:, np.newaxis, :],
            y=x[np.newaxis, :, :],
            fs=self.sampling_rate,
            nperseg=self.win_length,
            noverlap=self.win_length - self.hop_length,
            nfft=self.n_fft,
            window=self.window,
            detrend=self.detrend,
            scaling=self.scaling,
            average=self.average,
            axis=-1,
        )
        # p_yx shape: (num_channels, num_channels, num_frequencies)

        # Calculate power spectral density for each channel
        f, p_xx = ss.welch(
            x=x,
            fs=self.sampling_rate,
            nperseg=self.win_length,
            noverlap=self.win_length - self.hop_length,
            nfft=self.n_fft,
            window=self.window,
            detrend=self.detrend,
            scaling=self.scaling,
            average=self.average,
            axis=-1,
        )
        # p_xx shape: (num_channels, num_frequencies)

        # Calculate transfer function H(f) = P_yx / P_xx
        h_f = p_yx / p_xx[np.newaxis, :, :]
        result: NDArrayComplex = h_f.transpose(1, 0, 2).reshape(-1, h_f.shape[-1])

        logger.debug(
            f"Transfer function estimation applied, result shape: {result.shape}"
        )
        return result
Attributes
name = 'transfer_function' class-attribute instance-attribute
n_fft = n_fft instance-attribute
win_length = win_length if win_length is not None else n_fft instance-attribute
hop_length = hop_length if hop_length is not None else self.win_length // 4 instance-attribute
window = window instance-attribute
detrend = detrend instance-attribute
scaling = scaling instance-attribute
average = average instance-attribute
Functions
__init__(sampling_rate, n_fft, hop_length, win_length, window, detrend, scaling='spectrum', average='mean')

Initialize transfer function estimation operation

Parameters

sampling_rate : float Sampling rate (Hz) n_fft : int FFT size hop_length : int Hop length win_length : int Window length window : str Window function detrend : str Type of detrend scaling : str Type of scaling average : str Method of averaging

Source code in wandas/processing/spectral.py
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
def __init__(
    self,
    sampling_rate: float,
    n_fft: int,
    hop_length: int,
    win_length: int,
    window: str,
    detrend: str,
    scaling: str = "spectrum",
    average: str = "mean",
):
    """
    Initialize transfer function estimation operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    n_fft : int
        FFT size
    hop_length : int
        Hop length
    win_length : int
        Window length
    window : str
        Window function
    detrend : str
        Type of detrend
    scaling : str
        Type of scaling
    average : str
        Method of averaging
    """
    self.n_fft = n_fft
    self.win_length = win_length if win_length is not None else n_fft
    self.hop_length = hop_length if hop_length is not None else self.win_length // 4
    self.window = window
    self.detrend = detrend
    self.scaling = scaling
    self.average = average
    super().__init__(
        sampling_rate,
        n_fft=n_fft,
        hop_length=self.hop_length,
        win_length=self.win_length,
        window=window,
        detrend=detrend,
        scaling=scaling,
        average=average,
    )
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape (channels, samples)

Returns

tuple Output data shape (channels * channels, freqs)

Source code in wandas/processing/spectral.py
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape (channels, samples)

    Returns
    -------
    tuple
        Output data shape (channels * channels, freqs)
    """
    n_channels = input_shape[0]
    n_freqs = self.n_fft // 2 + 1
    return (n_channels * n_channels, n_freqs)
Functions

stats

Attributes
logger = logging.getLogger(__name__) module-attribute
Classes
ABS

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Absolute value operation

Source code in wandas/processing/stats.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class ABS(AudioOperation[NDArrayReal, NDArrayReal]):
    """Absolute value operation"""

    name = "abs"

    def __init__(self, sampling_rate: float):
        """
        Initialize absolute value operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        """
        super().__init__(sampling_rate)

    def process(self, data: DaArray) -> DaArray:
        # map_blocksを使わず、直接Daskの集約関数を使用
        return da.abs(data)  # type: ignore [unused-ignore]
Attributes
name = 'abs' class-attribute instance-attribute
Functions
__init__(sampling_rate)

Initialize absolute value operation

Parameters

sampling_rate : float Sampling rate (Hz)

Source code in wandas/processing/stats.py
17
18
19
20
21
22
23
24
25
26
def __init__(self, sampling_rate: float):
    """
    Initialize absolute value operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    """
    super().__init__(sampling_rate)
process(data)
Source code in wandas/processing/stats.py
28
29
30
def process(self, data: DaArray) -> DaArray:
    # map_blocksを使わず、直接Daskの集約関数を使用
    return da.abs(data)  # type: ignore [unused-ignore]
Power

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Power operation

Source code in wandas/processing/stats.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Power(AudioOperation[NDArrayReal, NDArrayReal]):
    """Power operation"""

    name = "power"

    def __init__(self, sampling_rate: float, exponent: float):
        """
        Initialize power operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        exponent : float
            Power exponent
        """
        super().__init__(sampling_rate)
        self.exp = exponent

    def process(self, data: DaArray) -> DaArray:
        # map_blocksを使わず、直接Daskの集約関数を使用
        return da.power(data, self.exp)  # type: ignore [unused-ignore]
Attributes
name = 'power' class-attribute instance-attribute
exp = exponent instance-attribute
Functions
__init__(sampling_rate, exponent)

Initialize power operation

Parameters

sampling_rate : float Sampling rate (Hz) exponent : float Power exponent

Source code in wandas/processing/stats.py
38
39
40
41
42
43
44
45
46
47
48
49
50
def __init__(self, sampling_rate: float, exponent: float):
    """
    Initialize power operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    exponent : float
        Power exponent
    """
    super().__init__(sampling_rate)
    self.exp = exponent
process(data)
Source code in wandas/processing/stats.py
52
53
54
def process(self, data: DaArray) -> DaArray:
    # map_blocksを使わず、直接Daskの集約関数を使用
    return da.power(data, self.exp)  # type: ignore [unused-ignore]
Sum

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Sum calculation

Source code in wandas/processing/stats.py
57
58
59
60
61
62
63
64
class Sum(AudioOperation[NDArrayReal, NDArrayReal]):
    """Sum calculation"""

    name = "sum"

    def process(self, data: DaArray) -> DaArray:
        # Use Dask's aggregate function directly without map_blocks
        return data.sum(axis=0, keepdims=True)
Attributes
name = 'sum' class-attribute instance-attribute
Functions
process(data)
Source code in wandas/processing/stats.py
62
63
64
def process(self, data: DaArray) -> DaArray:
    # Use Dask's aggregate function directly without map_blocks
    return data.sum(axis=0, keepdims=True)
Mean

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Mean calculation

Source code in wandas/processing/stats.py
67
68
69
70
71
72
73
74
class Mean(AudioOperation[NDArrayReal, NDArrayReal]):
    """Mean calculation"""

    name = "mean"

    def process(self, data: DaArray) -> DaArray:
        # Use Dask's aggregate function directly without map_blocks
        return data.mean(axis=0, keepdims=True)
Attributes
name = 'mean' class-attribute instance-attribute
Functions
process(data)
Source code in wandas/processing/stats.py
72
73
74
def process(self, data: DaArray) -> DaArray:
    # Use Dask's aggregate function directly without map_blocks
    return data.mean(axis=0, keepdims=True)
ChannelDifference

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Channel difference calculation operation

Source code in wandas/processing/stats.py
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
class ChannelDifference(AudioOperation[NDArrayReal, NDArrayReal]):
    """Channel difference calculation operation"""

    name = "channel_difference"
    other_channel: int

    def __init__(self, sampling_rate: float, other_channel: int = 0):
        """
        Initialize channel difference calculation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        other_channel : int
            Channel to calculate difference with, default is 0
        """
        self.other_channel = other_channel
        super().__init__(sampling_rate, other_channel=other_channel)

    def process(self, data: DaArray) -> DaArray:
        # map_blocksを使わず、直接Daskの集約関数を使用
        result = data - data[self.other_channel]
        return result
Attributes
name = 'channel_difference' class-attribute instance-attribute
other_channel = other_channel instance-attribute
Functions
__init__(sampling_rate, other_channel=0)

Initialize channel difference calculation

Parameters

sampling_rate : float Sampling rate (Hz) other_channel : int Channel to calculate difference with, default is 0

Source code in wandas/processing/stats.py
83
84
85
86
87
88
89
90
91
92
93
94
95
def __init__(self, sampling_rate: float, other_channel: int = 0):
    """
    Initialize channel difference calculation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    other_channel : int
        Channel to calculate difference with, default is 0
    """
    self.other_channel = other_channel
    super().__init__(sampling_rate, other_channel=other_channel)
process(data)
Source code in wandas/processing/stats.py
 97
 98
 99
100
def process(self, data: DaArray) -> DaArray:
    # map_blocksを使わず、直接Daskの集約関数を使用
    result = data - data[self.other_channel]
    return result
Functions

temporal

Attributes
logger = logging.getLogger(__name__) module-attribute
Classes
ReSampling

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Resampling operation

Source code in wandas/processing/temporal.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class ReSampling(AudioOperation[NDArrayReal, NDArrayReal]):
    """Resampling operation"""

    name = "resampling"

    def __init__(self, sampling_rate: float, target_sr: float):
        """
        Initialize resampling operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        target_sampling_rate : float
            Target sampling rate (Hz)
        """
        super().__init__(sampling_rate, target_sr=target_sr)
        self.target_sr = target_sr

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape

        Returns
        -------
        tuple
            Output data shape
        """
        # Calculate length after resampling
        ratio = float(self.target_sr) / float(self.sampling_rate)
        n_samples = int(np.ceil(input_shape[-1] * ratio))
        return (*input_shape[:-1], n_samples)

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Create processor function for resampling operation"""
        logger.debug(f"Applying resampling to array with shape: {x.shape}")
        result: NDArrayReal = librosa.resample(
            x, orig_sr=self.sampling_rate, target_sr=self.target_sr
        )
        logger.debug(f"Resampling applied, returning result with shape: {result.shape}")
        return result
Attributes
name = 'resampling' class-attribute instance-attribute
target_sr = target_sr instance-attribute
Functions
__init__(sampling_rate, target_sr)

Initialize resampling operation

Parameters

sampling_rate : float Sampling rate (Hz) target_sampling_rate : float Target sampling rate (Hz)

Source code in wandas/processing/temporal.py
19
20
21
22
23
24
25
26
27
28
29
30
31
def __init__(self, sampling_rate: float, target_sr: float):
    """
    Initialize resampling operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    target_sampling_rate : float
        Target sampling rate (Hz)
    """
    super().__init__(sampling_rate, target_sr=target_sr)
    self.target_sr = target_sr
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape

Returns

tuple Output data shape

Source code in wandas/processing/temporal.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape

    Returns
    -------
    tuple
        Output data shape
    """
    # Calculate length after resampling
    ratio = float(self.target_sr) / float(self.sampling_rate)
    n_samples = int(np.ceil(input_shape[-1] * ratio))
    return (*input_shape[:-1], n_samples)
Trim

Bases: AudioOperation[NDArrayReal, NDArrayReal]

Trimming operation

Source code in wandas/processing/temporal.py
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
class Trim(AudioOperation[NDArrayReal, NDArrayReal]):
    """Trimming operation"""

    name = "trim"

    def __init__(
        self,
        sampling_rate: float,
        start: float,
        end: float,
    ):
        """
        Initialize trimming operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        start : float
            Start time for trimming (seconds)
        end : float
            End time for trimming (seconds)
        """
        super().__init__(sampling_rate, start=start, end=end)
        self.start = start
        self.end = end
        self.start_sample = int(start * sampling_rate)
        self.end_sample = int(end * sampling_rate)
        logger.debug(
            f"Initialized Trim operation with start: {self.start}, end: {self.end}"
        )

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape

        Returns
        -------
        tuple
            Output data shape
        """
        # Calculate length after trimming
        # Exclude parts where there is no signal
        end_sample = min(self.end_sample, input_shape[-1])
        n_samples = end_sample - self.start_sample
        return (*input_shape[:-1], n_samples)

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Create processor function for trimming operation"""
        logger.debug(f"Applying trim to array with shape: {x.shape}")
        # Apply trimming
        result = x[..., self.start_sample : self.end_sample]
        logger.debug(f"Trim applied, returning result with shape: {result.shape}")
        return result
Attributes
name = 'trim' class-attribute instance-attribute
start = start instance-attribute
end = end instance-attribute
start_sample = int(start * sampling_rate) instance-attribute
end_sample = int(end * sampling_rate) instance-attribute
Functions
__init__(sampling_rate, start, end)

Initialize trimming operation

Parameters

sampling_rate : float Sampling rate (Hz) start : float Start time for trimming (seconds) end : float End time for trimming (seconds)

Source code in wandas/processing/temporal.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
def __init__(
    self,
    sampling_rate: float,
    start: float,
    end: float,
):
    """
    Initialize trimming operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    start : float
        Start time for trimming (seconds)
    end : float
        End time for trimming (seconds)
    """
    super().__init__(sampling_rate, start=start, end=end)
    self.start = start
    self.end = end
    self.start_sample = int(start * sampling_rate)
    self.end_sample = int(end * sampling_rate)
    logger.debug(
        f"Initialized Trim operation with start: {self.start}, end: {self.end}"
    )
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape

Returns

tuple Output data shape

Source code in wandas/processing/temporal.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape

    Returns
    -------
    tuple
        Output data shape
    """
    # Calculate length after trimming
    # Exclude parts where there is no signal
    end_sample = min(self.end_sample, input_shape[-1])
    n_samples = end_sample - self.start_sample
    return (*input_shape[:-1], n_samples)
FixLength

Bases: AudioOperation[NDArrayReal, NDArrayReal]

信号の長さを指定された長さに調整する操作

Source code in wandas/processing/temporal.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
class FixLength(AudioOperation[NDArrayReal, NDArrayReal]):
    """信号の長さを指定された長さに調整する操作"""

    name = "fix_length"

    def __init__(
        self,
        sampling_rate: float,
        length: Optional[int] = None,
        duration: Optional[float] = None,
    ):
        """
        Initialize fix length operation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        length : Optional[int]
            Target length for fixing
        duration : Optional[float]
            Target length for fixing
        """
        if length is None:
            if duration is None:
                raise ValueError("Either length or duration must be provided.")
            else:
                length = int(duration * sampling_rate)
        self.target_length = length

        super().__init__(sampling_rate, target_length=self.target_length)

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape

        Returns
        -------
        tuple
            Output data shape
        """
        return (*input_shape[:-1], self.target_length)

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Create processor function for padding operation"""
        logger.debug(f"Applying padding to array with shape: {x.shape}")
        # Apply padding
        pad_width = self.target_length - x.shape[-1]
        if pad_width > 0:
            result = np.pad(x, ((0, 0), (0, pad_width)), mode="constant")
        else:
            result = x[..., : self.target_length]
        logger.debug(f"Padding applied, returning result with shape: {result.shape}")
        return result
Attributes
name = 'fix_length' class-attribute instance-attribute
target_length = length instance-attribute
Functions
__init__(sampling_rate, length=None, duration=None)

Initialize fix length operation

Parameters

sampling_rate : float Sampling rate (Hz) length : Optional[int] Target length for fixing duration : Optional[float] Target length for fixing

Source code in wandas/processing/temporal.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
def __init__(
    self,
    sampling_rate: float,
    length: Optional[int] = None,
    duration: Optional[float] = None,
):
    """
    Initialize fix length operation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    length : Optional[int]
        Target length for fixing
    duration : Optional[float]
        Target length for fixing
    """
    if length is None:
        if duration is None:
            raise ValueError("Either length or duration must be provided.")
        else:
            length = int(duration * sampling_rate)
    self.target_length = length

    super().__init__(sampling_rate, target_length=self.target_length)
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape

Returns

tuple Output data shape

Source code in wandas/processing/temporal.py
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape

    Returns
    -------
    tuple
        Output data shape
    """
    return (*input_shape[:-1], self.target_length)
RmsTrend

Bases: AudioOperation[NDArrayReal, NDArrayReal]

RMS calculation

Source code in wandas/processing/temporal.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
class RmsTrend(AudioOperation[NDArrayReal, NDArrayReal]):
    """RMS calculation"""

    name = "rms_trend"
    frame_length: int
    hop_length: int
    Aw: bool

    def __init__(
        self,
        sampling_rate: float,
        frame_length: int = 2048,
        hop_length: int = 512,
        ref: Union[list[float], float] = 1.0,
        dB: bool = False,  # noqa: N803
        Aw: bool = False,  # noqa: N803
    ) -> None:
        """
        Initialize RMS calculation

        Parameters
        ----------
        sampling_rate : float
            Sampling rate (Hz)
        frame_length : int
            Frame length, default is 2048
        hop_length : int
            Hop length, default is 512
        ref : Union[list[float], float]
            Reference value(s) for dB calculation
        dB : bool
            Whether to convert to decibels
        Aw : bool
            Whether to apply A-weighting before RMS calculation
        """
        self.frame_length = frame_length
        self.hop_length = hop_length
        self.dB = dB
        self.Aw = Aw
        self.ref = np.array(ref if isinstance(ref, list) else [ref])
        super().__init__(
            sampling_rate,
            frame_length=frame_length,
            hop_length=hop_length,
            dB=dB,
            Aw=Aw,
            ref=self.ref,
        )

    def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
        """
        Calculate output data shape after operation

        Parameters
        ----------
        input_shape : tuple
            Input data shape (channels, samples)

        Returns
        -------
        tuple
            Output data shape (channels, frames)
        """
        n_frames = librosa.feature.rms(
            y=np.ones((1, input_shape[-1])),
            frame_length=self.frame_length,
            hop_length=self.hop_length,
        ).shape[-1]
        return (*input_shape[:-1], n_frames)

    def _process_array(self, x: NDArrayReal) -> NDArrayReal:
        """Create processor function for RMS calculation"""
        logger.debug(f"Applying RMS to array with shape: {x.shape}")

        if self.Aw:
            # Apply A-weighting
            _x = A_weight(x, self.sampling_rate)
            if isinstance(_x, np.ndarray):
                # A_weightがタプルを返す場合、最初の要素を使用
                x = _x
            elif isinstance(_x, tuple):
                # Use the first element if A_weight returns a tuple
                x = _x[0]
            else:
                raise ValueError("A_weighting returned an unexpected type.")

        # Calculate RMS
        result: NDArrayReal = librosa.feature.rms(
            y=x, frame_length=self.frame_length, hop_length=self.hop_length
        )[..., 0, :]

        if self.dB:
            # Convert to dB
            result = 20 * np.log10(
                np.maximum(result / self.ref[..., np.newaxis], 1e-12)
            )
        #
        logger.debug(f"RMS applied, returning result with shape: {result.shape}")
        return result
Attributes
name = 'rms_trend' class-attribute instance-attribute
frame_length = frame_length instance-attribute
hop_length = hop_length instance-attribute
dB = dB instance-attribute
Aw = Aw instance-attribute
ref = np.array(ref if isinstance(ref, list) else [ref]) instance-attribute
Functions
__init__(sampling_rate, frame_length=2048, hop_length=512, ref=1.0, dB=False, Aw=False)

Initialize RMS calculation

Parameters

sampling_rate : float Sampling rate (Hz) frame_length : int Frame length, default is 2048 hop_length : int Hop length, default is 512 ref : Union[list[float], float] Reference value(s) for dB calculation dB : bool Whether to convert to decibels Aw : bool Whether to apply A-weighting before RMS calculation

Source code in wandas/processing/temporal.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
def __init__(
    self,
    sampling_rate: float,
    frame_length: int = 2048,
    hop_length: int = 512,
    ref: Union[list[float], float] = 1.0,
    dB: bool = False,  # noqa: N803
    Aw: bool = False,  # noqa: N803
) -> None:
    """
    Initialize RMS calculation

    Parameters
    ----------
    sampling_rate : float
        Sampling rate (Hz)
    frame_length : int
        Frame length, default is 2048
    hop_length : int
        Hop length, default is 512
    ref : Union[list[float], float]
        Reference value(s) for dB calculation
    dB : bool
        Whether to convert to decibels
    Aw : bool
        Whether to apply A-weighting before RMS calculation
    """
    self.frame_length = frame_length
    self.hop_length = hop_length
    self.dB = dB
    self.Aw = Aw
    self.ref = np.array(ref if isinstance(ref, list) else [ref])
    super().__init__(
        sampling_rate,
        frame_length=frame_length,
        hop_length=hop_length,
        dB=dB,
        Aw=Aw,
        ref=self.ref,
    )
calculate_output_shape(input_shape)

Calculate output data shape after operation

Parameters

input_shape : tuple Input data shape (channels, samples)

Returns

tuple Output data shape (channels, frames)

Source code in wandas/processing/temporal.py
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
def calculate_output_shape(self, input_shape: tuple[int, ...]) -> tuple[int, ...]:
    """
    Calculate output data shape after operation

    Parameters
    ----------
    input_shape : tuple
        Input data shape (channels, samples)

    Returns
    -------
    tuple
        Output data shape (channels, frames)
    """
    n_frames = librosa.feature.rms(
        y=np.ones((1, input_shape[-1])),
        frame_length=self.frame_length,
        hop_length=self.hop_length,
    ).shape[-1]
    return (*input_shape[:-1], n_frames)
Functions

入出力モジュール

入出力モジュールはファイルの読み書き機能を提供します。

wandas.io

Attributes

__all__ = ['read_wav', 'write_wav', 'load', 'save'] module-attribute

Functions

read_wav(filename, labels=None)

Read a WAV file and create a ChannelFrame object.

Parameters

filename : str Path to the WAV file or URL to the WAV file. labels : list of str, optional Labels for each channel.

Returns

ChannelFrame ChannelFrame object containing the audio data.

Source code in wandas/io/wav_io.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def read_wav(filename: str, labels: Optional[list[str]] = None) -> "ChannelFrame":
    """
    Read a WAV file and create a ChannelFrame object.

    Parameters
    ----------
    filename : str
        Path to the WAV file or URL to the WAV file.
    labels : list of str, optional
        Labels for each channel.

    Returns
    -------
    ChannelFrame
        ChannelFrame object containing the audio data.
    """
    from wandas.frames.channel import ChannelFrame

    # ファイル名がURLかどうかを判断
    if filename.startswith("http://") or filename.startswith("https://"):
        # URLの場合、requestsを使用してダウンロード

        response = requests.get(filename)
        file_obj = io.BytesIO(response.content)
        file_label = os.path.basename(filename)
        # メモリマッピングは使用せずに読み込む
        sampling_rate, data = wavfile.read(file_obj)
    else:
        # ローカルファイルパスの場合
        file_label = os.path.basename(filename)
        # データの読み込み(メモリマッピングを使用)
        sampling_rate, data = wavfile.read(filename, mmap=True)

    # データを(num_channels, num_samples)形状のNumPy配列に変換
    if data.ndim == 1:
        # モノラル:(samples,) -> (1, samples)
        data = np.expand_dims(data, axis=0)
    else:
        # ステレオ:(samples, channels) -> (channels, samples)
        data = data.T

    # NumPy配列からChannelFrameを作成
    channel_frame = ChannelFrame.from_numpy(
        data=data,
        sampling_rate=sampling_rate,
        label=file_label,
        ch_labels=labels,
    )

    return channel_frame

write_wav(filename, target, format=None)

Write a ChannelFrame object to a WAV file.

Parameters

filename : str Path to the WAV file. target : ChannelFrame ChannelFrame object containing the data to write. format : str, optional File format. If None, determined from file extension.

Raises

ValueError If target is not a ChannelFrame object.

Source code in wandas/io/wav_io.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def write_wav(
    filename: str, target: "ChannelFrame", format: Optional[str] = None
) -> None:
    """
    Write a ChannelFrame object to a WAV file.

    Parameters
    ----------
    filename : str
        Path to the WAV file.
    target : ChannelFrame
        ChannelFrame object containing the data to write.
    format : str, optional
        File format. If None, determined from file extension.

    Raises
    ------
    ValueError
        If target is not a ChannelFrame object.
    """
    from wandas.frames.channel import ChannelFrame

    if not isinstance(target, ChannelFrame):
        raise ValueError("target must be a ChannelFrame object.")

    logger.debug(f"Saving audio data to file: {filename} (will compute now)")
    data = target.compute()
    data = data.T
    if data.shape[1] == 1:
        data = data.squeeze(axis=1)
    if data.dtype == float and max([np.abs(data.max()), np.abs(data.min())]) < 1:
        sf.write(
            str(filename),
            data,
            int(target.sampling_rate),
            subtype="FLOAT",
            format=format,
        )
    else:
        sf.write(str(filename), data, int(target.sampling_rate), format=format)
    logger.debug(f"Save complete: {filename}")

load(path, *, format='hdf5')

Load a ChannelFrame object from a WDF (Wandas Data File) file.

Parameters:

Name Type Description Default
path Union[str, Path]

Path to the WDF file to load.

required
format str

Format of the file. Currently only "hdf5" is supported.

'hdf5'

Returns:

Type Description
ChannelFrame

A new ChannelFrame object with data and metadata loaded from the file.

Raises:

Type Description
FileNotFoundError

If the file doesn't exist.

NotImplementedError

If format is not "hdf5".

ValueError

If the file format is invalid or incompatible.

Example

cf = ChannelFrame.load("audio_data.wdf")

Source code in wandas/io/wdf_io.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
def load(path: Union[str, Path], *, format: str = "hdf5") -> "ChannelFrame":
    """Load a ChannelFrame object from a WDF (Wandas Data File) file.

    Args:
        path: Path to the WDF file to load.
        format: Format of the file. Currently only "hdf5" is supported.

    Returns:
        A new ChannelFrame object with data and metadata loaded from the file.

    Raises:
        FileNotFoundError: If the file doesn't exist.
        NotImplementedError: If format is not "hdf5".
        ValueError: If the file format is invalid or incompatible.

    Example:
        >>> cf = ChannelFrame.load("audio_data.wdf")
    """
    # Ensure ChannelFrame is imported here to avoid circular imports
    from ..core.metadata import ChannelMetadata
    from ..frames.channel import ChannelFrame

    if format != "hdf5":
        raise NotImplementedError(f"Format '{format}' is not supported")

    path = Path(path)
    if not path.exists():
        raise FileNotFoundError(f"File not found: {path}")

    logger.debug(f"Loading ChannelFrame from {path}")

    with h5py.File(path, "r") as f:
        # Check format version for compatibility
        version = f.attrs.get("version", "unknown")
        if version != WDF_FORMAT_VERSION:
            logger.warning(
                f"File format version mismatch: file={version}, current={WDF_FORMAT_VERSION}"  # noqa: E501
            )

        # Get global attributes
        sampling_rate = float(f.attrs["sampling_rate"])
        frame_label = f.attrs.get("label", "")

        # Get frame metadata
        frame_metadata = {}
        if "meta" in f:
            meta_json = f["meta"].attrs.get("json", "{}")
            frame_metadata = json.loads(meta_json)

        # Load channel data and metadata
        all_channel_data = []
        channel_metadata_list = []

        if "channels" in f:
            channels_group = f["channels"]
            # Sort channel indices numerically
            channel_indices = sorted([int(key) for key in channels_group.keys()])

            for idx in channel_indices:
                ch_group = channels_group[f"{idx}"]

                # Load channel data
                channel_data = ch_group["data"][()]

                # Append to combined array
                all_channel_data.append(channel_data)

                # Load channel metadata
                label = ch_group.attrs.get("label", f"Ch{idx}")
                unit = ch_group.attrs.get("unit", "")

                # Load additional metadata if present
                ch_extra = {}
                if "metadata_json" in ch_group.attrs:
                    ch_extra = json.loads(ch_group.attrs["metadata_json"])

                # Create ChannelMetadata object
                channel_metadata = ChannelMetadata(
                    label=label, unit=unit, extra=ch_extra
                )
                channel_metadata_list.append(channel_metadata)

        # Stack channel data into a single array
        if all_channel_data:
            combined_data = np.stack(all_channel_data, axis=0)
        else:
            raise ValueError("No channel data found in the file")

        # Create a new ChannelFrame
        dask_data = da_from_array(combined_data)

        cf = ChannelFrame(
            data=dask_data,
            sampling_rate=sampling_rate,
            label=frame_label if frame_label else None,
            metadata=frame_metadata,
            channel_metadata=channel_metadata_list,
        )

        logger.debug(
            f"ChannelFrame loaded from {path}: {len(cf)} channels, {cf.n_samples} samples"  # noqa: E501
        )
        return cf

save(frame, path, *, format='hdf5', compress='gzip', overwrite=False, dtype=None)

Save a frame to a file.

Parameters:

Name Type Description Default
frame BaseFrame[Any]

The frame to save.

required
path Union[str, Path]

Path to save the file. '.wdf' extension will be added if not present.

required
format str

Format to use (currently only 'hdf5' is supported)

'hdf5'
compress Optional[str]

Compression method ('gzip' by default, None for no compression)

'gzip'
overwrite bool

Whether to overwrite existing file

False
dtype Optional[Union[str, dtype[Any]]]

Optional data type conversion before saving (e.g. 'float32')

None

Raises:

Type Description
FileExistsError

If the file exists and overwrite=False.

NotImplementedError

For unsupported formats.

Source code in wandas/io/wdf_io.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
def save(
    frame: BaseFrame[Any],
    path: Union[str, Path],
    *,
    format: str = "hdf5",
    compress: Optional[str] = "gzip",
    overwrite: bool = False,
    dtype: Optional[Union[str, np.dtype[Any]]] = None,
) -> None:
    """Save a frame to a file.

    Args:
        frame: The frame to save.
        path: Path to save the file. '.wdf' extension will be added if not present.
        format: Format to use (currently only 'hdf5' is supported)
        compress: Compression method ('gzip' by default, None for no compression)
        overwrite: Whether to overwrite existing file
        dtype: Optional data type conversion before saving (e.g. 'float32')

    Raises:
        FileExistsError: If the file exists and overwrite=False.
        NotImplementedError: For unsupported formats.
    """
    # Handle path
    path = Path(path)
    if path.suffix != ".wdf":
        path = path.with_suffix(".wdf")

    # Check if file exists
    if path.exists() and not overwrite:
        raise FileExistsError(
            f"File {path} already exists. Set overwrite=True to overwrite."
        )

    # Currently only HDF5 is supported
    if format.lower() != "hdf5":
        raise NotImplementedError(
            f"Format {format} not supported. Only 'hdf5' is currently implemented."
        )

    # Compute data arrays (this triggers actual computation)
    logger.info("Computing data arrays for saving...")
    computed_data = frame.compute()
    if dtype is not None:
        computed_data = computed_data.astype(dtype)

    # Create file
    logger.info(f"Creating HDF5 file at {path}...")
    with h5py.File(path, "w") as f:
        # Set file version
        f.attrs["version"] = WDF_FORMAT_VERSION

        # Store frame metadata
        f.attrs["sampling_rate"] = frame.sampling_rate
        f.attrs["label"] = frame.label or ""
        f.attrs["frame_type"] = type(frame).__name__

        # Create channels group
        channels_grp = f.create_group("channels")

        # Store each channel
        for i, (channel_data, ch_meta) in enumerate(
            zip(computed_data, frame._channel_metadata)
        ):
            ch_grp = channels_grp.create_group(f"{i}")

            # Store channel data
            if compress:
                ch_grp.create_dataset("data", data=channel_data, compression=compress)
            else:
                ch_grp.create_dataset("data", data=channel_data)

            # Store metadata
            ch_grp.attrs["label"] = ch_meta.label
            ch_grp.attrs["unit"] = ch_meta.unit

            # Store extra metadata as JSON
            if ch_meta.extra:
                ch_grp.attrs["metadata_json"] = json.dumps(ch_meta.extra)

        # Store operation history
        if frame.operation_history:
            op_grp = f.create_group("operation_history")
            for i, op in enumerate(frame.operation_history):
                op_sub_grp = op_grp.create_group(f"operation_{i}")
                for k, v in op.items():
                    # Store simple attributes directly
                    if isinstance(v, (str, int, float, bool, np.number)):
                        op_sub_grp.attrs[k] = v
                    else:
                        # For complex types, serialize to JSON
                        try:
                            op_sub_grp.attrs[k] = json.dumps(v)
                        except (TypeError, OverflowError) as e:
                            logger.warning(
                                f"Could not serialize operation key '{k}': {e}"
                            )
                            op_sub_grp.attrs[k] = str(v)

        # Store frame metadata
        if frame.metadata:
            meta_grp = f.create_group("meta")
            # Store metadata as JSON
            meta_grp.attrs["json"] = json.dumps(frame.metadata)

            # Also store individual metadata items as attributes for compatibility
            for k, v in frame.metadata.items():
                if isinstance(v, (str, int, float, bool, np.number)):
                    meta_grp.attrs[k] = v

    logger.info(f"Frame saved to {path}")

Modules

readers

Attributes
logger = logging.getLogger(__name__) module-attribute
Classes
FileReader

Bases: ABC

Base class for audio file readers.

Source code in wandas/io/readers.py
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class FileReader(ABC):
    """Base class for audio file readers."""

    # Class attribute for supported file extensions
    supported_extensions: list[str] = []

    @classmethod
    @abstractmethod
    def get_file_info(cls, path: Union[str, Path], **kwargs: Any) -> dict[str, Any]:
        """Get basic information about the audio file."""
        pass

    @classmethod
    @abstractmethod
    def get_data(
        cls,
        path: Union[str, Path],
        channels: list[int],
        start_idx: int,
        frames: int,
        **kwargs: Any,
    ) -> ArrayLike:
        """Read audio data from the file."""
        pass

    @classmethod
    def can_read(cls, path: Union[str, Path]) -> bool:
        """Check if this reader can handle the file based on extension."""
        ext = Path(path).suffix.lower()
        return ext in cls.supported_extensions
Attributes
supported_extensions = [] class-attribute instance-attribute
Functions
get_file_info(path, **kwargs) abstractmethod classmethod

Get basic information about the audio file.

Source code in wandas/io/readers.py
20
21
22
23
24
@classmethod
@abstractmethod
def get_file_info(cls, path: Union[str, Path], **kwargs: Any) -> dict[str, Any]:
    """Get basic information about the audio file."""
    pass
get_data(path, channels, start_idx, frames, **kwargs) abstractmethod classmethod

Read audio data from the file.

Source code in wandas/io/readers.py
26
27
28
29
30
31
32
33
34
35
36
37
@classmethod
@abstractmethod
def get_data(
    cls,
    path: Union[str, Path],
    channels: list[int],
    start_idx: int,
    frames: int,
    **kwargs: Any,
) -> ArrayLike:
    """Read audio data from the file."""
    pass
can_read(path) classmethod

Check if this reader can handle the file based on extension.

Source code in wandas/io/readers.py
39
40
41
42
43
@classmethod
def can_read(cls, path: Union[str, Path]) -> bool:
    """Check if this reader can handle the file based on extension."""
    ext = Path(path).suffix.lower()
    return ext in cls.supported_extensions
SoundFileReader

Bases: FileReader

Audio file reader using SoundFile library.

Source code in wandas/io/readers.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
class SoundFileReader(FileReader):
    """Audio file reader using SoundFile library."""

    # SoundFile supported formats
    supported_extensions = [".wav", ".flac", ".ogg", ".aiff", ".aif", ".snd"]

    @classmethod
    def get_file_info(cls, path: Union[str, Path], **kwargs: Any) -> dict[str, Any]:
        """Get basic information about the audio file."""
        info = sf.info(str(path))
        return {
            "samplerate": info.samplerate,
            "channels": info.channels,
            "frames": info.frames,
            "format": info.format,
            "subtype": info.subtype,
            "duration": info.frames / info.samplerate,
        }

    @classmethod
    def get_data(
        cls,
        path: Union[str, Path],
        channels: list[int],
        start_idx: int,
        frames: int,
        **kwargs: Any,
    ) -> ArrayLike:
        """Read audio data from the file."""
        logger.debug(f"Reading {frames} frames from {path} starting at {start_idx}")

        with sf.SoundFile(str(path)) as f:
            if start_idx > 0:
                f.seek(start_idx)
            data = f.read(frames=frames, dtype="float32", always_2d=True)

            # Select requested channels
            if len(channels) < f.channels:
                data = data[:, channels]

            # Transpose to get (channels, samples) format
            result: ArrayLike = data.T
            if not isinstance(result, np.ndarray):
                raise ValueError("Unexpected data type after reading file")

        _shape = result.shape
        logger.debug(f"File read complete, returning data with shape {_shape}")
        return result
Attributes
supported_extensions = ['.wav', '.flac', '.ogg', '.aiff', '.aif', '.snd'] class-attribute instance-attribute
Functions
get_file_info(path, **kwargs) classmethod

Get basic information about the audio file.

Source code in wandas/io/readers.py
52
53
54
55
56
57
58
59
60
61
62
63
@classmethod
def get_file_info(cls, path: Union[str, Path], **kwargs: Any) -> dict[str, Any]:
    """Get basic information about the audio file."""
    info = sf.info(str(path))
    return {
        "samplerate": info.samplerate,
        "channels": info.channels,
        "frames": info.frames,
        "format": info.format,
        "subtype": info.subtype,
        "duration": info.frames / info.samplerate,
    }
get_data(path, channels, start_idx, frames, **kwargs) classmethod

Read audio data from the file.

Source code in wandas/io/readers.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
@classmethod
def get_data(
    cls,
    path: Union[str, Path],
    channels: list[int],
    start_idx: int,
    frames: int,
    **kwargs: Any,
) -> ArrayLike:
    """Read audio data from the file."""
    logger.debug(f"Reading {frames} frames from {path} starting at {start_idx}")

    with sf.SoundFile(str(path)) as f:
        if start_idx > 0:
            f.seek(start_idx)
        data = f.read(frames=frames, dtype="float32", always_2d=True)

        # Select requested channels
        if len(channels) < f.channels:
            data = data[:, channels]

        # Transpose to get (channels, samples) format
        result: ArrayLike = data.T
        if not isinstance(result, np.ndarray):
            raise ValueError("Unexpected data type after reading file")

    _shape = result.shape
    logger.debug(f"File read complete, returning data with shape {_shape}")
    return result
CSVFileReader

Bases: FileReader

CSV file reader for time series data.

Source code in wandas/io/readers.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
class CSVFileReader(FileReader):
    """CSV file reader for time series data."""

    # CSV supported formats
    supported_extensions = [".csv"]

    @classmethod
    def get_file_info(cls, path: Union[str, Path], **kwargs: Any) -> dict[str, Any]:
        delimiter = kwargs.get("delimiter", ",")
        header = kwargs.get("header", 0)
        """Get basic information about the CSV file."""
        # Read first few lines to determine structure
        df = pd.read_csv(path, delimiter=delimiter, header=header)

        # Estimate sampling rate from first column (assuming it's time)
        time_column = 0
        try:
            time_values = np.array(df.iloc[:, time_column].values)
            if len(time_values) > 1:
                estimated_sr = int(1 / np.mean(np.diff(time_values)))
            else:
                estimated_sr = 0  # Cannot determine from single row
        except Exception:
            estimated_sr = 0  # Default if can't calculate

        frames = df.shape[0]
        duration = frames / estimated_sr if estimated_sr > 0 else None

        # Return file info
        return {
            "samplerate": estimated_sr,
            "channels": df.shape[1] - 1,  # Assuming first column is time
            "frames": frames,
            "format": "CSV",
            "duration": duration,
            "ch_labels": df.columns[1:].tolist(),  # Assuming first column is time
        }

    @classmethod
    def get_data(
        cls,
        path: Union[str, Path],
        channels: list[int],
        start_idx: int,
        frames: int,
        **kwargs: Any,
    ) -> ArrayLike:
        """Read data from the CSV file."""
        logger.debug(f"Reading CSV data from {path} starting at {start_idx}")

        # Read the CSV file
        time_column = kwargs.get("time_column", 0)
        delimiter = kwargs.get("delimiter", ",")
        header = kwargs.get("header", 0)
        # Read first few lines to determine structure
        df = pd.read_csv(path, delimiter=delimiter, header=header)

        # Remove time column
        df = df.drop(
            columns=[time_column]
            if isinstance(time_column, str)
            else df.columns[time_column]
        )

        # Select requested channels - adjust indices to account for time column removal
        if channels:
            try:
                data_df = df.iloc[:, channels]
            except IndexError:
                raise ValueError(f"Requested channels {channels} out of range")
        else:
            data_df = df

        # Handle start_idx and frames for partial reading
        end_idx = start_idx + frames if frames > 0 else None
        data_df = data_df.iloc[start_idx:end_idx]

        # Convert to numpy array and transpose to (channels, samples) format
        result = data_df.values.T

        if not isinstance(result, np.ndarray):
            raise ValueError("Unexpected data type after reading file")

        _shape = result.shape
        logger.debug(f"CSV read complete, returning data with shape {_shape}")
        return result
Attributes
supported_extensions = ['.csv'] class-attribute instance-attribute
Functions
get_file_info(path, **kwargs) classmethod
Source code in wandas/io/readers.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
@classmethod
def get_file_info(cls, path: Union[str, Path], **kwargs: Any) -> dict[str, Any]:
    delimiter = kwargs.get("delimiter", ",")
    header = kwargs.get("header", 0)
    """Get basic information about the CSV file."""
    # Read first few lines to determine structure
    df = pd.read_csv(path, delimiter=delimiter, header=header)

    # Estimate sampling rate from first column (assuming it's time)
    time_column = 0
    try:
        time_values = np.array(df.iloc[:, time_column].values)
        if len(time_values) > 1:
            estimated_sr = int(1 / np.mean(np.diff(time_values)))
        else:
            estimated_sr = 0  # Cannot determine from single row
    except Exception:
        estimated_sr = 0  # Default if can't calculate

    frames = df.shape[0]
    duration = frames / estimated_sr if estimated_sr > 0 else None

    # Return file info
    return {
        "samplerate": estimated_sr,
        "channels": df.shape[1] - 1,  # Assuming first column is time
        "frames": frames,
        "format": "CSV",
        "duration": duration,
        "ch_labels": df.columns[1:].tolist(),  # Assuming first column is time
    }
get_data(path, channels, start_idx, frames, **kwargs) classmethod

Read data from the CSV file.

Source code in wandas/io/readers.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
@classmethod
def get_data(
    cls,
    path: Union[str, Path],
    channels: list[int],
    start_idx: int,
    frames: int,
    **kwargs: Any,
) -> ArrayLike:
    """Read data from the CSV file."""
    logger.debug(f"Reading CSV data from {path} starting at {start_idx}")

    # Read the CSV file
    time_column = kwargs.get("time_column", 0)
    delimiter = kwargs.get("delimiter", ",")
    header = kwargs.get("header", 0)
    # Read first few lines to determine structure
    df = pd.read_csv(path, delimiter=delimiter, header=header)

    # Remove time column
    df = df.drop(
        columns=[time_column]
        if isinstance(time_column, str)
        else df.columns[time_column]
    )

    # Select requested channels - adjust indices to account for time column removal
    if channels:
        try:
            data_df = df.iloc[:, channels]
        except IndexError:
            raise ValueError(f"Requested channels {channels} out of range")
    else:
        data_df = df

    # Handle start_idx and frames for partial reading
    end_idx = start_idx + frames if frames > 0 else None
    data_df = data_df.iloc[start_idx:end_idx]

    # Convert to numpy array and transpose to (channels, samples) format
    result = data_df.values.T

    if not isinstance(result, np.ndarray):
        raise ValueError("Unexpected data type after reading file")

    _shape = result.shape
    logger.debug(f"CSV read complete, returning data with shape {_shape}")
    return result
Functions
get_file_reader(path)

Get an appropriate file reader for the given path.

Source code in wandas/io/readers.py
188
189
190
191
192
193
194
195
196
197
198
199
200
def get_file_reader(path: Union[str, Path]) -> FileReader:
    """Get an appropriate file reader for the given path."""
    path_str = str(path)
    ext = Path(path).suffix.lower()

    # Try each reader in order
    for reader in _file_readers:
        if ext in reader.__class__.supported_extensions:
            logger.debug(f"Using {reader.__class__.__name__} for {path_str}")
            return reader

    # If no reader found, raise error
    raise ValueError(f"No suitable file reader found for {path_str}")
register_file_reader(reader_class)

Register a new file reader.

Source code in wandas/io/readers.py
203
204
205
206
207
def register_file_reader(reader_class: type) -> None:
    """Register a new file reader."""
    reader = reader_class()
    _file_readers.append(reader)
    logger.debug(f"Registered new file reader: {reader_class.__name__}")

wav_io

Attributes
logger = logging.getLogger(__name__) module-attribute
Classes
Functions
read_wav(filename, labels=None)

Read a WAV file and create a ChannelFrame object.

Parameters

filename : str Path to the WAV file or URL to the WAV file. labels : list of str, optional Labels for each channel.

Returns

ChannelFrame ChannelFrame object containing the audio data.

Source code in wandas/io/wav_io.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
def read_wav(filename: str, labels: Optional[list[str]] = None) -> "ChannelFrame":
    """
    Read a WAV file and create a ChannelFrame object.

    Parameters
    ----------
    filename : str
        Path to the WAV file or URL to the WAV file.
    labels : list of str, optional
        Labels for each channel.

    Returns
    -------
    ChannelFrame
        ChannelFrame object containing the audio data.
    """
    from wandas.frames.channel import ChannelFrame

    # ファイル名がURLかどうかを判断
    if filename.startswith("http://") or filename.startswith("https://"):
        # URLの場合、requestsを使用してダウンロード

        response = requests.get(filename)
        file_obj = io.BytesIO(response.content)
        file_label = os.path.basename(filename)
        # メモリマッピングは使用せずに読み込む
        sampling_rate, data = wavfile.read(file_obj)
    else:
        # ローカルファイルパスの場合
        file_label = os.path.basename(filename)
        # データの読み込み(メモリマッピングを使用)
        sampling_rate, data = wavfile.read(filename, mmap=True)

    # データを(num_channels, num_samples)形状のNumPy配列に変換
    if data.ndim == 1:
        # モノラル:(samples,) -> (1, samples)
        data = np.expand_dims(data, axis=0)
    else:
        # ステレオ:(samples, channels) -> (channels, samples)
        data = data.T

    # NumPy配列からChannelFrameを作成
    channel_frame = ChannelFrame.from_numpy(
        data=data,
        sampling_rate=sampling_rate,
        label=file_label,
        ch_labels=labels,
    )

    return channel_frame
write_wav(filename, target, format=None)

Write a ChannelFrame object to a WAV file.

Parameters

filename : str Path to the WAV file. target : ChannelFrame ChannelFrame object containing the data to write. format : str, optional File format. If None, determined from file extension.

Raises

ValueError If target is not a ChannelFrame object.

Source code in wandas/io/wav_io.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def write_wav(
    filename: str, target: "ChannelFrame", format: Optional[str] = None
) -> None:
    """
    Write a ChannelFrame object to a WAV file.

    Parameters
    ----------
    filename : str
        Path to the WAV file.
    target : ChannelFrame
        ChannelFrame object containing the data to write.
    format : str, optional
        File format. If None, determined from file extension.

    Raises
    ------
    ValueError
        If target is not a ChannelFrame object.
    """
    from wandas.frames.channel import ChannelFrame

    if not isinstance(target, ChannelFrame):
        raise ValueError("target must be a ChannelFrame object.")

    logger.debug(f"Saving audio data to file: {filename} (will compute now)")
    data = target.compute()
    data = data.T
    if data.shape[1] == 1:
        data = data.squeeze(axis=1)
    if data.dtype == float and max([np.abs(data.max()), np.abs(data.min())]) < 1:
        sf.write(
            str(filename),
            data,
            int(target.sampling_rate),
            subtype="FLOAT",
            format=format,
        )
    else:
        sf.write(str(filename), data, int(target.sampling_rate), format=format)
    logger.debug(f"Save complete: {filename}")

wdf_io

WDF (Wandas Data File) I/O module for saving and loading ChannelFrame objects.

This module provides functionality to save and load ChannelFrame objects in the WDF (Wandas Data File) format, which is based on HDF5. The format preserves all metadata including sampling rate, channel labels, units, and frame metadata.

Attributes
da_from_array = da.from_array module-attribute
logger = logging.getLogger(__name__) module-attribute
WDF_FORMAT_VERSION = '0.1' module-attribute
Classes
Functions
save(frame, path, *, format='hdf5', compress='gzip', overwrite=False, dtype=None)

Save a frame to a file.

Parameters:

Name Type Description Default
frame BaseFrame[Any]

The frame to save.

required
path Union[str, Path]

Path to save the file. '.wdf' extension will be added if not present.

required
format str

Format to use (currently only 'hdf5' is supported)

'hdf5'
compress Optional[str]

Compression method ('gzip' by default, None for no compression)

'gzip'
overwrite bool

Whether to overwrite existing file

False
dtype Optional[Union[str, dtype[Any]]]

Optional data type conversion before saving (e.g. 'float32')

None

Raises:

Type Description
FileExistsError

If the file exists and overwrite=False.

NotImplementedError

For unsupported formats.

Source code in wandas/io/wdf_io.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
def save(
    frame: BaseFrame[Any],
    path: Union[str, Path],
    *,
    format: str = "hdf5",
    compress: Optional[str] = "gzip",
    overwrite: bool = False,
    dtype: Optional[Union[str, np.dtype[Any]]] = None,
) -> None:
    """Save a frame to a file.

    Args:
        frame: The frame to save.
        path: Path to save the file. '.wdf' extension will be added if not present.
        format: Format to use (currently only 'hdf5' is supported)
        compress: Compression method ('gzip' by default, None for no compression)
        overwrite: Whether to overwrite existing file
        dtype: Optional data type conversion before saving (e.g. 'float32')

    Raises:
        FileExistsError: If the file exists and overwrite=False.
        NotImplementedError: For unsupported formats.
    """
    # Handle path
    path = Path(path)
    if path.suffix != ".wdf":
        path = path.with_suffix(".wdf")

    # Check if file exists
    if path.exists() and not overwrite:
        raise FileExistsError(
            f"File {path} already exists. Set overwrite=True to overwrite."
        )

    # Currently only HDF5 is supported
    if format.lower() != "hdf5":
        raise NotImplementedError(
            f"Format {format} not supported. Only 'hdf5' is currently implemented."
        )

    # Compute data arrays (this triggers actual computation)
    logger.info("Computing data arrays for saving...")
    computed_data = frame.compute()
    if dtype is not None:
        computed_data = computed_data.astype(dtype)

    # Create file
    logger.info(f"Creating HDF5 file at {path}...")
    with h5py.File(path, "w") as f:
        # Set file version
        f.attrs["version"] = WDF_FORMAT_VERSION

        # Store frame metadata
        f.attrs["sampling_rate"] = frame.sampling_rate
        f.attrs["label"] = frame.label or ""
        f.attrs["frame_type"] = type(frame).__name__

        # Create channels group
        channels_grp = f.create_group("channels")

        # Store each channel
        for i, (channel_data, ch_meta) in enumerate(
            zip(computed_data, frame._channel_metadata)
        ):
            ch_grp = channels_grp.create_group(f"{i}")

            # Store channel data
            if compress:
                ch_grp.create_dataset("data", data=channel_data, compression=compress)
            else:
                ch_grp.create_dataset("data", data=channel_data)

            # Store metadata
            ch_grp.attrs["label"] = ch_meta.label
            ch_grp.attrs["unit"] = ch_meta.unit

            # Store extra metadata as JSON
            if ch_meta.extra:
                ch_grp.attrs["metadata_json"] = json.dumps(ch_meta.extra)

        # Store operation history
        if frame.operation_history:
            op_grp = f.create_group("operation_history")
            for i, op in enumerate(frame.operation_history):
                op_sub_grp = op_grp.create_group(f"operation_{i}")
                for k, v in op.items():
                    # Store simple attributes directly
                    if isinstance(v, (str, int, float, bool, np.number)):
                        op_sub_grp.attrs[k] = v
                    else:
                        # For complex types, serialize to JSON
                        try:
                            op_sub_grp.attrs[k] = json.dumps(v)
                        except (TypeError, OverflowError) as e:
                            logger.warning(
                                f"Could not serialize operation key '{k}': {e}"
                            )
                            op_sub_grp.attrs[k] = str(v)

        # Store frame metadata
        if frame.metadata:
            meta_grp = f.create_group("meta")
            # Store metadata as JSON
            meta_grp.attrs["json"] = json.dumps(frame.metadata)

            # Also store individual metadata items as attributes for compatibility
            for k, v in frame.metadata.items():
                if isinstance(v, (str, int, float, bool, np.number)):
                    meta_grp.attrs[k] = v

    logger.info(f"Frame saved to {path}")
load(path, *, format='hdf5')

Load a ChannelFrame object from a WDF (Wandas Data File) file.

Parameters:

Name Type Description Default
path Union[str, Path]

Path to the WDF file to load.

required
format str

Format of the file. Currently only "hdf5" is supported.

'hdf5'

Returns:

Type Description
ChannelFrame

A new ChannelFrame object with data and metadata loaded from the file.

Raises:

Type Description
FileNotFoundError

If the file doesn't exist.

NotImplementedError

If format is not "hdf5".

ValueError

If the file format is invalid or incompatible.

Example

cf = ChannelFrame.load("audio_data.wdf")

Source code in wandas/io/wdf_io.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
def load(path: Union[str, Path], *, format: str = "hdf5") -> "ChannelFrame":
    """Load a ChannelFrame object from a WDF (Wandas Data File) file.

    Args:
        path: Path to the WDF file to load.
        format: Format of the file. Currently only "hdf5" is supported.

    Returns:
        A new ChannelFrame object with data and metadata loaded from the file.

    Raises:
        FileNotFoundError: If the file doesn't exist.
        NotImplementedError: If format is not "hdf5".
        ValueError: If the file format is invalid or incompatible.

    Example:
        >>> cf = ChannelFrame.load("audio_data.wdf")
    """
    # Ensure ChannelFrame is imported here to avoid circular imports
    from ..core.metadata import ChannelMetadata
    from ..frames.channel import ChannelFrame

    if format != "hdf5":
        raise NotImplementedError(f"Format '{format}' is not supported")

    path = Path(path)
    if not path.exists():
        raise FileNotFoundError(f"File not found: {path}")

    logger.debug(f"Loading ChannelFrame from {path}")

    with h5py.File(path, "r") as f:
        # Check format version for compatibility
        version = f.attrs.get("version", "unknown")
        if version != WDF_FORMAT_VERSION:
            logger.warning(
                f"File format version mismatch: file={version}, current={WDF_FORMAT_VERSION}"  # noqa: E501
            )

        # Get global attributes
        sampling_rate = float(f.attrs["sampling_rate"])
        frame_label = f.attrs.get("label", "")

        # Get frame metadata
        frame_metadata = {}
        if "meta" in f:
            meta_json = f["meta"].attrs.get("json", "{}")
            frame_metadata = json.loads(meta_json)

        # Load channel data and metadata
        all_channel_data = []
        channel_metadata_list = []

        if "channels" in f:
            channels_group = f["channels"]
            # Sort channel indices numerically
            channel_indices = sorted([int(key) for key in channels_group.keys()])

            for idx in channel_indices:
                ch_group = channels_group[f"{idx}"]

                # Load channel data
                channel_data = ch_group["data"][()]

                # Append to combined array
                all_channel_data.append(channel_data)

                # Load channel metadata
                label = ch_group.attrs.get("label", f"Ch{idx}")
                unit = ch_group.attrs.get("unit", "")

                # Load additional metadata if present
                ch_extra = {}
                if "metadata_json" in ch_group.attrs:
                    ch_extra = json.loads(ch_group.attrs["metadata_json"])

                # Create ChannelMetadata object
                channel_metadata = ChannelMetadata(
                    label=label, unit=unit, extra=ch_extra
                )
                channel_metadata_list.append(channel_metadata)

        # Stack channel data into a single array
        if all_channel_data:
            combined_data = np.stack(all_channel_data, axis=0)
        else:
            raise ValueError("No channel data found in the file")

        # Create a new ChannelFrame
        dask_data = da_from_array(combined_data)

        cf = ChannelFrame(
            data=dask_data,
            sampling_rate=sampling_rate,
            label=frame_label if frame_label else None,
            metadata=frame_metadata,
            channel_metadata=channel_metadata_list,
        )

        logger.debug(
            f"ChannelFrame loaded from {path}: {len(cf)} channels, {cf.n_samples} samples"  # noqa: E501
        )
        return cf

ユーティリティモジュール

ユーティリティモジュールは補助機能を提供します。

wandas.utils

Attributes

__all__ = ['filter_kwargs', 'accepted_kwargs'] module-attribute

Functions

accepted_kwargs(func)

Get the set of explicit keyword arguments accepted by a function and whether it accepts **kwargs.

Parameters:

Name Type Description Default
func Callable[..., Any]

The function to inspect.

required

Returns:

Type Description
set[str]

A tuple containing:

bool
  • set[str]: Set of explicit keyword argument names accepted by func.
tuple[set[str], bool]
  • bool: Whether the function accepts variable keyword arguments (**kwargs).
Source code in wandas/utils/introspection.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def accepted_kwargs(func: Callable[..., Any]) -> tuple[set[str], bool]:
    """
    Get the set of explicit keyword arguments accepted by
    a function and whether it accepts **kwargs.

    Args:
        func: The function to inspect.

    Returns:
        A tuple containing:
        - set[str]: Set of explicit keyword argument names accepted by func.
        - bool: Whether the function accepts variable keyword arguments (**kwargs).
    """
    # モックオブジェクトの場合は空セットと無制限フラグを返す
    if hasattr(func, "__module__") and func.__module__ == "unittest.mock":
        return set(), True
    try:
        params = signature(func).parameters.values()

        # 明示的に定義されている引数を収集
        explicit_kwargs = {
            p.name
            for p in params
            if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY)
        }

        # **kwargsを受け付けるかどうかのフラグ
        has_var_kwargs = any(p.kind is Parameter.VAR_KEYWORD for p in params)

        return explicit_kwargs, has_var_kwargs
    except (ValueError, TypeError):
        # シグネチャを取得できない場合は空セットと無制限フラグを返す
        return set(), True

filter_kwargs(func, kwargs, *, strict_mode=False)

Filter keyword arguments to only those accepted by the function.

This function examines the signature of func and returns a dictionary containing only the key-value pairs from kwargs that are valid keyword arguments for func.

Parameters:

Name Type Description Default
func Callable[..., Any]

The function to filter keyword arguments for.

required
kwargs Mapping[str, Any]

The keyword arguments to filter.

required
strict_mode bool

If True, only explicitly defined parameters are passed even when the function accepts kwargs. If False (default), all parameters are passed to functions that accept kwargs, but a warning is issued for parameters not explicitly defined.

False

Returns:

Type Description
dict[str, Any]

A dictionary containing only the key-value pairs that are valid for func.

Source code in wandas/utils/introspection.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def filter_kwargs(
    func: Callable[..., Any],
    kwargs: Mapping[str, Any],
    *,
    strict_mode: bool = False,
) -> dict[str, Any]:
    """
    Filter keyword arguments to only those accepted by the function.

    This function examines the signature of `func` and returns a dictionary
    containing only the key-value pairs from `kwargs` that are valid keyword
    arguments for `func`.

    Args:
        func: The function to filter keyword arguments for.
        kwargs: The keyword arguments to filter.
        strict_mode: If True, only explicitly defined parameters are passed even when
            the function accepts **kwargs. If False (default), all parameters are
            passed to functions that accept **kwargs, but a warning is issued for
            parameters not explicitly defined.

    Returns:
        A dictionary containing only the key-value pairs that are valid for `func`.
    """
    explicit_params, accepts_var_kwargs = accepted_kwargs(func)

    # **kwargsを受け付けない場合、または strict_mode が True の場合は、
    # 明示的なパラメータのみをフィルタリング
    if not accepts_var_kwargs or strict_mode:
        filtered = {k: v for k, v in kwargs.items() if k in explicit_params}
        return filtered

    # **kwargsを受け付ける場合(strict_modeがFalseの場合)は全キーを許可
    # ただし、明示的に定義されていないキーには警告を出す
    unknown = set(kwargs) - explicit_params
    if unknown:
        warnings.warn(
            f"Implicit kwargs for {func.__name__}: {unknown}",
            UserWarning,
            stacklevel=2,
        )
    return dict(kwargs)

Modules

frame_dataset

Attributes
logger = logging.getLogger(__name__) module-attribute
FrameType = Union[ChannelFrame, SpectrogramFrame] module-attribute
F = TypeVar('F', bound=FrameType) module-attribute
F_out = TypeVar('F_out', bound=FrameType) module-attribute
Classes
LazyFrame dataclass

Bases: Generic[F]

A class that encapsulates a frame and its loading state.

Attributes:

Name Type Description
file_path Path

File path associated with the frame

frame Optional[F]

Loaded frame object (None if not loaded)

is_loaded bool

Flag indicating if the frame is loaded

load_attempted bool

Flag indicating if loading was attempted (for error detection)

Source code in wandas/utils/frame_dataset.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@dataclass
class LazyFrame(Generic[F]):
    """
    A class that encapsulates a frame and its loading state.

    Attributes:
        file_path: File path associated with the frame
        frame: Loaded frame object (None if not loaded)
        is_loaded: Flag indicating if the frame is loaded
        load_attempted: Flag indicating if loading was attempted (for error detection)
    """

    file_path: Path
    frame: Optional[F] = None
    is_loaded: bool = False
    load_attempted: bool = False

    def ensure_loaded(self, loader: Callable[[Path], Optional[F]]) -> Optional[F]:
        """
        Ensures the frame is loaded, loading it if necessary.

        Args:
            loader: Function to load a frame from a file path

        Returns:
            The loaded frame, or None if loading failed
        """
        # Return the current frame if already loaded
        if self.is_loaded:
            return self.frame

        # Attempt to load if not loaded yet
        try:
            self.load_attempted = True
            self.frame = loader(self.file_path)
            self.is_loaded = True
            return self.frame
        except Exception as e:
            logger.error(f"Failed to load file {self.file_path}: {str(e)}")
            self.is_loaded = True  # Loading was attempted
            self.frame = None
            return None

    def reset(self) -> None:
        """
        Reset the frame state.
        """
        self.frame = None
        self.is_loaded = False
        self.load_attempted = False
Attributes
file_path instance-attribute
frame = None class-attribute instance-attribute
is_loaded = False class-attribute instance-attribute
load_attempted = False class-attribute instance-attribute
Functions
__init__(file_path, frame=None, is_loaded=False, load_attempted=False)
ensure_loaded(loader)

Ensures the frame is loaded, loading it if necessary.

Parameters:

Name Type Description Default
loader Callable[[Path], Optional[F]]

Function to load a frame from a file path

required

Returns:

Type Description
Optional[F]

The loaded frame, or None if loading failed

Source code in wandas/utils/frame_dataset.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def ensure_loaded(self, loader: Callable[[Path], Optional[F]]) -> Optional[F]:
    """
    Ensures the frame is loaded, loading it if necessary.

    Args:
        loader: Function to load a frame from a file path

    Returns:
        The loaded frame, or None if loading failed
    """
    # Return the current frame if already loaded
    if self.is_loaded:
        return self.frame

    # Attempt to load if not loaded yet
    try:
        self.load_attempted = True
        self.frame = loader(self.file_path)
        self.is_loaded = True
        return self.frame
    except Exception as e:
        logger.error(f"Failed to load file {self.file_path}: {str(e)}")
        self.is_loaded = True  # Loading was attempted
        self.frame = None
        return None
reset()

Reset the frame state.

Source code in wandas/utils/frame_dataset.py
63
64
65
66
67
68
69
def reset(self) -> None:
    """
    Reset the frame state.
    """
    self.frame = None
    self.is_loaded = False
    self.load_attempted = False
FrameDataset

Bases: Generic[F], ABC

Abstract base dataset class for processing files in a folder. Includes lazy loading capability to efficiently handle large datasets. Subclasses handle specific frame types (ChannelFrame, SpectrogramFrame, etc.).

Source code in wandas/utils/frame_dataset.py
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
class FrameDataset(Generic[F], ABC):
    """
    Abstract base dataset class for processing files in a folder.
    Includes lazy loading capability to efficiently handle large datasets.
    Subclasses handle specific frame types (ChannelFrame, SpectrogramFrame, etc.).
    """

    def __init__(
        self,
        folder_path: str,
        sampling_rate: Optional[int] = None,
        signal_length: Optional[int] = None,
        file_extensions: Optional[list[str]] = None,
        lazy_loading: bool = True,
        recursive: bool = False,
        source_dataset: Optional["FrameDataset[Any]"] = None,
        transform: Optional[Callable[[Any], Optional[F]]] = None,
    ):
        self.folder_path = Path(folder_path)
        if source_dataset is None and not self.folder_path.exists():
            raise FileNotFoundError(f"Folder does not exist: {self.folder_path}")

        self.sampling_rate = sampling_rate
        self.signal_length = signal_length
        self.file_extensions = file_extensions or [".wav"]
        self._recursive = recursive
        self._lazy_loading = lazy_loading

        # Changed to a list of LazyFrame
        self._lazy_frames: list[LazyFrame[F]] = []

        self._source_dataset = source_dataset
        self._transform = transform

        if self._source_dataset:
            self._initialize_from_source()
        else:
            self._initialize_from_folder()

    def _initialize_from_source(self) -> None:
        """Initialize from a source dataset."""
        if self._source_dataset is None:
            return

        # Copy file paths from source
        file_paths = self._source_dataset._get_file_paths()
        self._lazy_frames = [LazyFrame(file_path) for file_path in file_paths]

        # Inherit other properties
        self.sampling_rate = self.sampling_rate or self._source_dataset.sampling_rate
        self.signal_length = self.signal_length or self._source_dataset.signal_length
        self.file_extensions = (
            self.file_extensions or self._source_dataset.file_extensions
        )
        self._recursive = self._source_dataset._recursive
        self.folder_path = self._source_dataset.folder_path

    def _initialize_from_folder(self) -> None:
        """Initialize from a folder."""
        self._discover_files()
        if not self._lazy_loading:
            self._load_all_files()

    def _discover_files(self) -> None:
        """Discover files in the folder and store them in a list of LazyFrame."""
        file_paths = []
        for ext in self.file_extensions:
            pattern = f"**/*{ext}" if self._recursive else f"*{ext}"
            file_paths.extend(
                sorted(p for p in self.folder_path.glob(pattern) if p.is_file())
            )

        # Remove duplicates and sort
        file_paths = sorted(list(set(file_paths)))

        # Create a list of LazyFrame
        self._lazy_frames = [LazyFrame(file_path) for file_path in file_paths]

    def _load_all_files(self) -> None:
        """Load all files."""
        for i in tqdm(range(len(self._lazy_frames)), desc="Loading/transforming"):
            try:
                self._ensure_loaded(i)
            except Exception as e:
                filepath = self._lazy_frames[i].file_path
                logger.warning(
                    f"Failed to load/transform index {i} ({filepath}): {str(e)}"
                )
        self._lazy_loading = False

    @abstractmethod
    def _load_file(self, file_path: Path) -> Optional[F]:
        """Abstract method to load a frame from a file."""
        pass

    def _load_from_source(self, index: int) -> Optional[F]:
        """Load a frame from the source dataset and transform it if necessary."""
        if self._source_dataset is None or self._transform is None:
            return None

        source_frame = self._source_dataset._ensure_loaded(index)
        if source_frame is None:
            return None

        try:
            return self._transform(source_frame)
        except Exception as e:
            logger.warning(f"Failed to transform index {index}: {str(e)}")
            return None

    def _ensure_loaded(self, index: int) -> Optional[F]:
        """Ensure the frame at the given index is loaded."""
        if not (0 <= index < len(self._lazy_frames)):
            raise IndexError(
                f"Index {index} is out of range (0-{len(self._lazy_frames) - 1})"
            )

        lazy_frame = self._lazy_frames[index]

        # Return if already loaded
        if lazy_frame.is_loaded:
            return lazy_frame.frame

        try:
            # Convert from source dataset
            if self._transform and self._source_dataset:
                lazy_frame.load_attempted = True
                frame = self._load_from_source(index)
                lazy_frame.frame = frame
                lazy_frame.is_loaded = True
                return frame
            # Load directly from file
            else:
                return lazy_frame.ensure_loaded(self._load_file)
        except Exception as e:
            f_path = lazy_frame.file_path
            logger.error(
                f"Failed to load or initialize index {index} ({f_path}): {str(e)}"
            )
            lazy_frame.frame = None
            lazy_frame.is_loaded = True
            lazy_frame.load_attempted = True
            return None

    def _get_file_paths(self) -> list[Path]:
        """Get a list of file paths."""
        return [lazy_frame.file_path for lazy_frame in self._lazy_frames]

    def __len__(self) -> int:
        """Return the number of files in the dataset."""
        return len(self._lazy_frames)

    def __getitem__(self, index: int) -> Optional[F]:
        """Get the frame at the specified index."""
        return self._ensure_loaded(index)

    @overload
    def apply(self, func: Callable[[F], Optional[F_out]]) -> "FrameDataset[F_out]": ...

    @overload
    def apply(self, func: Callable[[F], Optional[Any]]) -> "FrameDataset[Any]": ...

    def apply(self, func: Callable[[F], Optional[Any]]) -> "FrameDataset[Any]":
        """Apply a function to the entire dataset to create a new dataset."""
        new_dataset = type(self)(
            folder_path=str(self.folder_path),
            lazy_loading=True,
            source_dataset=self,
            transform=func,
            sampling_rate=self.sampling_rate,
            signal_length=self.signal_length,
            file_extensions=self.file_extensions,
            recursive=self._recursive,
        )
        return cast("FrameDataset[Any]", new_dataset)

    def save(self, output_folder: str, filename_prefix: str = "") -> None:
        """Save processed frames to files."""
        raise NotImplementedError("The save method is not currently implemented.")

    def sample(
        self,
        n: Optional[int] = None,
        ratio: Optional[float] = None,
        seed: Optional[int] = None,
    ) -> "FrameDataset[F]":
        """Get a sample from the dataset."""
        if seed is not None:
            random.seed(seed)

        total = len(self._lazy_frames)
        if total == 0:
            return type(self)(
                str(self.folder_path),
                sampling_rate=self.sampling_rate,
                signal_length=self.signal_length,
                file_extensions=self.file_extensions,
                lazy_loading=self._lazy_loading,
                recursive=self._recursive,
            )

        # Determine sample size
        if n is None and ratio is None:
            n = max(1, min(10, int(total * 0.1)))
        elif n is None and ratio is not None:
            n = max(1, int(total * ratio))
        elif n is not None:
            n = max(1, n)
        else:
            n = 1

        n = min(n, total)

        # Randomly select indices
        sampled_indices = sorted(random.sample(range(total), n))

        return _SampledFrameDataset(self, sampled_indices)

    def get_metadata(self) -> dict[str, Any]:
        """Get metadata for the dataset."""
        actual_sr: Optional[Union[int, float]] = self.sampling_rate
        frame_type_name = "Unknown"

        # Count loaded frames
        loaded_count = sum(
            1 for lazy_frame in self._lazy_frames if lazy_frame.is_loaded
        )

        # Get metadata from the first frame (if possible)
        first_frame: Optional[F] = None
        if len(self._lazy_frames) > 0:
            try:
                if self._lazy_frames[0].is_loaded:
                    first_frame = self._lazy_frames[0].frame

                if first_frame:
                    actual_sr = getattr(
                        first_frame, "sampling_rate", self.sampling_rate
                    )
                    frame_type_name = type(first_frame).__name__
            except Exception as e:
                logger.warning(
                    f"Error accessing the first frame during metadata retrieval: {e}"
                )

        return {
            "folder_path": str(self.folder_path),
            "file_count": len(self._lazy_frames),
            "loaded_count": loaded_count,
            "target_sampling_rate": self.sampling_rate,
            "actual_sampling_rate": actual_sr,
            "signal_length": self.signal_length,
            "file_extensions": self.file_extensions,
            "lazy_loading": self._lazy_loading,
            "recursive": self._recursive,
            "frame_type": frame_type_name,
            "has_transform": self._transform is not None,
            "is_sampled": isinstance(self, _SampledFrameDataset),
        }
Attributes
folder_path = Path(folder_path) instance-attribute
sampling_rate = sampling_rate instance-attribute
signal_length = signal_length instance-attribute
file_extensions = file_extensions or ['.wav'] instance-attribute
Functions
__init__(folder_path, sampling_rate=None, signal_length=None, file_extensions=None, lazy_loading=True, recursive=False, source_dataset=None, transform=None)
Source code in wandas/utils/frame_dataset.py
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def __init__(
    self,
    folder_path: str,
    sampling_rate: Optional[int] = None,
    signal_length: Optional[int] = None,
    file_extensions: Optional[list[str]] = None,
    lazy_loading: bool = True,
    recursive: bool = False,
    source_dataset: Optional["FrameDataset[Any]"] = None,
    transform: Optional[Callable[[Any], Optional[F]]] = None,
):
    self.folder_path = Path(folder_path)
    if source_dataset is None and not self.folder_path.exists():
        raise FileNotFoundError(f"Folder does not exist: {self.folder_path}")

    self.sampling_rate = sampling_rate
    self.signal_length = signal_length
    self.file_extensions = file_extensions or [".wav"]
    self._recursive = recursive
    self._lazy_loading = lazy_loading

    # Changed to a list of LazyFrame
    self._lazy_frames: list[LazyFrame[F]] = []

    self._source_dataset = source_dataset
    self._transform = transform

    if self._source_dataset:
        self._initialize_from_source()
    else:
        self._initialize_from_folder()
__len__()

Return the number of files in the dataset.

Source code in wandas/utils/frame_dataset.py
220
221
222
def __len__(self) -> int:
    """Return the number of files in the dataset."""
    return len(self._lazy_frames)
__getitem__(index)

Get the frame at the specified index.

Source code in wandas/utils/frame_dataset.py
224
225
226
def __getitem__(self, index: int) -> Optional[F]:
    """Get the frame at the specified index."""
    return self._ensure_loaded(index)
apply(func)
apply(
    func: Callable[[F], Optional[F_out]],
) -> FrameDataset[F_out]
apply(
    func: Callable[[F], Optional[Any]],
) -> FrameDataset[Any]

Apply a function to the entire dataset to create a new dataset.

Source code in wandas/utils/frame_dataset.py
234
235
236
237
238
239
240
241
242
243
244
245
246
def apply(self, func: Callable[[F], Optional[Any]]) -> "FrameDataset[Any]":
    """Apply a function to the entire dataset to create a new dataset."""
    new_dataset = type(self)(
        folder_path=str(self.folder_path),
        lazy_loading=True,
        source_dataset=self,
        transform=func,
        sampling_rate=self.sampling_rate,
        signal_length=self.signal_length,
        file_extensions=self.file_extensions,
        recursive=self._recursive,
    )
    return cast("FrameDataset[Any]", new_dataset)
save(output_folder, filename_prefix='')

Save processed frames to files.

Source code in wandas/utils/frame_dataset.py
248
249
250
def save(self, output_folder: str, filename_prefix: str = "") -> None:
    """Save processed frames to files."""
    raise NotImplementedError("The save method is not currently implemented.")
sample(n=None, ratio=None, seed=None)

Get a sample from the dataset.

Source code in wandas/utils/frame_dataset.py
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
def sample(
    self,
    n: Optional[int] = None,
    ratio: Optional[float] = None,
    seed: Optional[int] = None,
) -> "FrameDataset[F]":
    """Get a sample from the dataset."""
    if seed is not None:
        random.seed(seed)

    total = len(self._lazy_frames)
    if total == 0:
        return type(self)(
            str(self.folder_path),
            sampling_rate=self.sampling_rate,
            signal_length=self.signal_length,
            file_extensions=self.file_extensions,
            lazy_loading=self._lazy_loading,
            recursive=self._recursive,
        )

    # Determine sample size
    if n is None and ratio is None:
        n = max(1, min(10, int(total * 0.1)))
    elif n is None and ratio is not None:
        n = max(1, int(total * ratio))
    elif n is not None:
        n = max(1, n)
    else:
        n = 1

    n = min(n, total)

    # Randomly select indices
    sampled_indices = sorted(random.sample(range(total), n))

    return _SampledFrameDataset(self, sampled_indices)
get_metadata()

Get metadata for the dataset.

Source code in wandas/utils/frame_dataset.py
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def get_metadata(self) -> dict[str, Any]:
    """Get metadata for the dataset."""
    actual_sr: Optional[Union[int, float]] = self.sampling_rate
    frame_type_name = "Unknown"

    # Count loaded frames
    loaded_count = sum(
        1 for lazy_frame in self._lazy_frames if lazy_frame.is_loaded
    )

    # Get metadata from the first frame (if possible)
    first_frame: Optional[F] = None
    if len(self._lazy_frames) > 0:
        try:
            if self._lazy_frames[0].is_loaded:
                first_frame = self._lazy_frames[0].frame

            if first_frame:
                actual_sr = getattr(
                    first_frame, "sampling_rate", self.sampling_rate
                )
                frame_type_name = type(first_frame).__name__
        except Exception as e:
            logger.warning(
                f"Error accessing the first frame during metadata retrieval: {e}"
            )

    return {
        "folder_path": str(self.folder_path),
        "file_count": len(self._lazy_frames),
        "loaded_count": loaded_count,
        "target_sampling_rate": self.sampling_rate,
        "actual_sampling_rate": actual_sr,
        "signal_length": self.signal_length,
        "file_extensions": self.file_extensions,
        "lazy_loading": self._lazy_loading,
        "recursive": self._recursive,
        "frame_type": frame_type_name,
        "has_transform": self._transform is not None,
        "is_sampled": isinstance(self, _SampledFrameDataset),
    }
ChannelFrameDataset

Bases: FrameDataset[ChannelFrame]

Dataset class for handling audio files as ChannelFrames in a folder.

Source code in wandas/utils/frame_dataset.py
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
class ChannelFrameDataset(FrameDataset[ChannelFrame]):
    """
    Dataset class for handling audio files as ChannelFrames in a folder.
    """

    def __init__(
        self,
        folder_path: str,
        sampling_rate: Optional[int] = None,
        signal_length: Optional[int] = None,
        file_extensions: Optional[list[str]] = None,
        lazy_loading: bool = True,
        recursive: bool = False,
        source_dataset: Optional["FrameDataset[Any]"] = None,
        transform: Optional[Callable[[Any], Optional[ChannelFrame]]] = None,
    ):
        _file_extensions = file_extensions or [
            ".wav",
            ".mp3",
            ".flac",
            ".csv",
        ]

        super().__init__(
            folder_path=folder_path,
            sampling_rate=sampling_rate,
            signal_length=signal_length,
            file_extensions=_file_extensions,
            lazy_loading=lazy_loading,
            recursive=recursive,
            source_dataset=source_dataset,
            transform=transform,
        )

    def _load_file(self, file_path: Path) -> Optional[ChannelFrame]:
        """Load an audio file and return a ChannelFrame."""
        try:
            frame = ChannelFrame.from_file(file_path)
            if self.sampling_rate and frame.sampling_rate != self.sampling_rate:
                logger.info(
                    f"Resampling file {file_path.name} ({frame.sampling_rate} Hz) to "
                    f"dataset rate ({self.sampling_rate} Hz)."
                )
                frame = frame.resampling(target_sr=self.sampling_rate)
            return frame
        except Exception as e:
            logger.error(f"Failed to load or initialize file {file_path}: {str(e)}")
            return None

    def resample(self, target_sr: int) -> "ChannelFrameDataset":
        """Resample all frames in the dataset."""

        def _resample_func(frame: ChannelFrame) -> Optional[ChannelFrame]:
            if frame is None:
                return None
            try:
                return frame.resampling(target_sr=target_sr)
            except Exception as e:
                logger.warning(f"Resampling error (target_sr={target_sr}): {e}")
                return None

        new_dataset = self.apply(_resample_func)
        return cast(ChannelFrameDataset, new_dataset)

    def trim(self, start: float, end: float) -> "ChannelFrameDataset":
        """Trim all frames in the dataset."""

        def _trim_func(frame: ChannelFrame) -> Optional[ChannelFrame]:
            if frame is None:
                return None
            try:
                return frame.trim(start=start, end=end)
            except Exception as e:
                logger.warning(f"Trimming error (start={start}, end={end}): {e}")
                return None

        new_dataset = self.apply(_trim_func)
        return cast(ChannelFrameDataset, new_dataset)

    def normalize(self, **kwargs: Any) -> "ChannelFrameDataset":
        """Normalize all frames in the dataset."""

        def _normalize_func(frame: ChannelFrame) -> Optional[ChannelFrame]:
            if frame is None:
                return None
            try:
                return frame.normalize(**kwargs)
            except Exception as e:
                logger.warning(f"Normalization error ({kwargs}): {e}")
                return None

        new_dataset = self.apply(_normalize_func)
        return cast(ChannelFrameDataset, new_dataset)

    def stft(
        self,
        n_fft: int = 2048,
        hop_length: Optional[int] = None,
        win_length: Optional[int] = None,
        window: str = "hann",
    ) -> "SpectrogramFrameDataset":
        """Apply STFT to all frames in the dataset."""
        _hop = hop_length or n_fft // 4

        def _stft_func(frame: ChannelFrame) -> Optional[SpectrogramFrame]:
            if frame is None:
                return None
            try:
                return frame.stft(
                    n_fft=n_fft,
                    hop_length=_hop,
                    win_length=win_length,
                    window=window,
                )
            except Exception as e:
                logger.warning(f"STFT error (n_fft={n_fft}, hop={_hop}): {e}")
                return None

        new_dataset = SpectrogramFrameDataset(
            folder_path=str(self.folder_path),
            lazy_loading=True,
            source_dataset=self,
            transform=_stft_func,
            sampling_rate=self.sampling_rate,
        )
        return new_dataset

    @classmethod
    def from_folder(
        cls,
        folder_path: str,
        sampling_rate: Optional[int] = None,
        file_extensions: Optional[list[str]] = None,
        recursive: bool = False,
        lazy_loading: bool = True,
    ) -> "ChannelFrameDataset":
        """Class method to create a ChannelFrameDataset from a folder."""
        extensions = (
            file_extensions
            if file_extensions is not None
            else [".wav", ".mp3", ".flac", ".csv"]
        )

        return cls(
            folder_path,
            sampling_rate=sampling_rate,
            file_extensions=extensions,
            lazy_loading=lazy_loading,
            recursive=recursive,
        )
Functions
__init__(folder_path, sampling_rate=None, signal_length=None, file_extensions=None, lazy_loading=True, recursive=False, source_dataset=None, transform=None)
Source code in wandas/utils/frame_dataset.py
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
def __init__(
    self,
    folder_path: str,
    sampling_rate: Optional[int] = None,
    signal_length: Optional[int] = None,
    file_extensions: Optional[list[str]] = None,
    lazy_loading: bool = True,
    recursive: bool = False,
    source_dataset: Optional["FrameDataset[Any]"] = None,
    transform: Optional[Callable[[Any], Optional[ChannelFrame]]] = None,
):
    _file_extensions = file_extensions or [
        ".wav",
        ".mp3",
        ".flac",
        ".csv",
    ]

    super().__init__(
        folder_path=folder_path,
        sampling_rate=sampling_rate,
        signal_length=signal_length,
        file_extensions=_file_extensions,
        lazy_loading=lazy_loading,
        recursive=recursive,
        source_dataset=source_dataset,
        transform=transform,
    )
resample(target_sr)

Resample all frames in the dataset.

Source code in wandas/utils/frame_dataset.py
496
497
498
499
500
501
502
503
504
505
506
507
508
509
def resample(self, target_sr: int) -> "ChannelFrameDataset":
    """Resample all frames in the dataset."""

    def _resample_func(frame: ChannelFrame) -> Optional[ChannelFrame]:
        if frame is None:
            return None
        try:
            return frame.resampling(target_sr=target_sr)
        except Exception as e:
            logger.warning(f"Resampling error (target_sr={target_sr}): {e}")
            return None

    new_dataset = self.apply(_resample_func)
    return cast(ChannelFrameDataset, new_dataset)
trim(start, end)

Trim all frames in the dataset.

Source code in wandas/utils/frame_dataset.py
511
512
513
514
515
516
517
518
519
520
521
522
523
524
def trim(self, start: float, end: float) -> "ChannelFrameDataset":
    """Trim all frames in the dataset."""

    def _trim_func(frame: ChannelFrame) -> Optional[ChannelFrame]:
        if frame is None:
            return None
        try:
            return frame.trim(start=start, end=end)
        except Exception as e:
            logger.warning(f"Trimming error (start={start}, end={end}): {e}")
            return None

    new_dataset = self.apply(_trim_func)
    return cast(ChannelFrameDataset, new_dataset)
normalize(**kwargs)

Normalize all frames in the dataset.

Source code in wandas/utils/frame_dataset.py
526
527
528
529
530
531
532
533
534
535
536
537
538
539
def normalize(self, **kwargs: Any) -> "ChannelFrameDataset":
    """Normalize all frames in the dataset."""

    def _normalize_func(frame: ChannelFrame) -> Optional[ChannelFrame]:
        if frame is None:
            return None
        try:
            return frame.normalize(**kwargs)
        except Exception as e:
            logger.warning(f"Normalization error ({kwargs}): {e}")
            return None

    new_dataset = self.apply(_normalize_func)
    return cast(ChannelFrameDataset, new_dataset)
stft(n_fft=2048, hop_length=None, win_length=None, window='hann')

Apply STFT to all frames in the dataset.

Source code in wandas/utils/frame_dataset.py
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
def stft(
    self,
    n_fft: int = 2048,
    hop_length: Optional[int] = None,
    win_length: Optional[int] = None,
    window: str = "hann",
) -> "SpectrogramFrameDataset":
    """Apply STFT to all frames in the dataset."""
    _hop = hop_length or n_fft // 4

    def _stft_func(frame: ChannelFrame) -> Optional[SpectrogramFrame]:
        if frame is None:
            return None
        try:
            return frame.stft(
                n_fft=n_fft,
                hop_length=_hop,
                win_length=win_length,
                window=window,
            )
        except Exception as e:
            logger.warning(f"STFT error (n_fft={n_fft}, hop={_hop}): {e}")
            return None

    new_dataset = SpectrogramFrameDataset(
        folder_path=str(self.folder_path),
        lazy_loading=True,
        source_dataset=self,
        transform=_stft_func,
        sampling_rate=self.sampling_rate,
    )
    return new_dataset
from_folder(folder_path, sampling_rate=None, file_extensions=None, recursive=False, lazy_loading=True) classmethod

Class method to create a ChannelFrameDataset from a folder.

Source code in wandas/utils/frame_dataset.py
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
@classmethod
def from_folder(
    cls,
    folder_path: str,
    sampling_rate: Optional[int] = None,
    file_extensions: Optional[list[str]] = None,
    recursive: bool = False,
    lazy_loading: bool = True,
) -> "ChannelFrameDataset":
    """Class method to create a ChannelFrameDataset from a folder."""
    extensions = (
        file_extensions
        if file_extensions is not None
        else [".wav", ".mp3", ".flac", ".csv"]
    )

    return cls(
        folder_path,
        sampling_rate=sampling_rate,
        file_extensions=extensions,
        lazy_loading=lazy_loading,
        recursive=recursive,
    )
SpectrogramFrameDataset

Bases: FrameDataset[SpectrogramFrame]

Dataset class for handling spectrogram data as SpectrogramFrames. Expected to be generated mainly as a result of ChannelFrameDataset.stft().

Source code in wandas/utils/frame_dataset.py
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
class SpectrogramFrameDataset(FrameDataset[SpectrogramFrame]):
    """
    Dataset class for handling spectrogram data as SpectrogramFrames.
    Expected to be generated mainly as a result of ChannelFrameDataset.stft().
    """

    def __init__(
        self,
        folder_path: str,
        sampling_rate: Optional[int] = None,
        signal_length: Optional[int] = None,
        file_extensions: Optional[list[str]] = None,
        lazy_loading: bool = True,
        recursive: bool = False,
        source_dataset: Optional["FrameDataset[Any]"] = None,
        transform: Optional[Callable[[Any], Optional[SpectrogramFrame]]] = None,
    ):
        super().__init__(
            folder_path=folder_path,
            sampling_rate=sampling_rate,
            signal_length=signal_length,
            file_extensions=file_extensions,
            lazy_loading=lazy_loading,
            recursive=recursive,
            source_dataset=source_dataset,
            transform=transform,
        )

    def _load_file(self, file_path: Path) -> Optional[SpectrogramFrame]:
        """Direct loading from files is not currently supported."""
        logger.warning(
            "No method defined for directly loading SpectrogramFrames. Normally "
            "created from ChannelFrameDataset.stft()."
        )
        raise NotImplementedError(
            "No method defined for directly loading SpectrogramFrames"
        )

    def plot(self, index: int, **kwargs: Any) -> None:
        """Plot the spectrogram at the specified index."""
        try:
            frame = self._ensure_loaded(index)

            if frame is None:
                logger.warning(
                    f"Cannot plot index {index} as it failed to load/transform."
                )
                return

            plot_method = getattr(frame, "plot", None)
            if callable(plot_method):
                plot_method(**kwargs)
            else:
                logger.warning(
                    f"Frame (index {index}, type {type(frame).__name__}) does not "
                    f"have a plot method implemented."
                )
        except Exception as e:
            logger.error(f"An error occurred while plotting index {index}: {e}")
Functions
__init__(folder_path, sampling_rate=None, signal_length=None, file_extensions=None, lazy_loading=True, recursive=False, source_dataset=None, transform=None)
Source code in wandas/utils/frame_dataset.py
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
def __init__(
    self,
    folder_path: str,
    sampling_rate: Optional[int] = None,
    signal_length: Optional[int] = None,
    file_extensions: Optional[list[str]] = None,
    lazy_loading: bool = True,
    recursive: bool = False,
    source_dataset: Optional["FrameDataset[Any]"] = None,
    transform: Optional[Callable[[Any], Optional[SpectrogramFrame]]] = None,
):
    super().__init__(
        folder_path=folder_path,
        sampling_rate=sampling_rate,
        signal_length=signal_length,
        file_extensions=file_extensions,
        lazy_loading=lazy_loading,
        recursive=recursive,
        source_dataset=source_dataset,
        transform=transform,
    )
plot(index, **kwargs)

Plot the spectrogram at the specified index.

Source code in wandas/utils/frame_dataset.py
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
def plot(self, index: int, **kwargs: Any) -> None:
    """Plot the spectrogram at the specified index."""
    try:
        frame = self._ensure_loaded(index)

        if frame is None:
            logger.warning(
                f"Cannot plot index {index} as it failed to load/transform."
            )
            return

        plot_method = getattr(frame, "plot", None)
        if callable(plot_method):
            plot_method(**kwargs)
        else:
            logger.warning(
                f"Frame (index {index}, type {type(frame).__name__}) does not "
                f"have a plot method implemented."
            )
    except Exception as e:
        logger.error(f"An error occurred while plotting index {index}: {e}")

generate_sample

Classes
Functions
generate_sin(freqs=1000, sampling_rate=16000, duration=1.0, label=None)

Generate sample sine wave signals.

Parameters

freqs : float or list of float, default=1000 Frequency of the sine wave(s) in Hz. If multiple frequencies are specified, multiple channels will be created. sampling_rate : int, default=16000 Sampling rate in Hz. duration : float, default=1.0 Duration of the signal in seconds. label : str, optional Label for the entire signal.

Returns

ChannelFrame ChannelFrame object containing the sine wave(s).

Source code in wandas/utils/generate_sample.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
def generate_sin(
    freqs: Union[float, list[float]] = 1000,
    sampling_rate: int = 16000,
    duration: float = 1.0,
    label: Optional[str] = None,
) -> "ChannelFrame":
    """
    Generate sample sine wave signals.

    Parameters
    ----------
    freqs : float or list of float, default=1000
        Frequency of the sine wave(s) in Hz.
        If multiple frequencies are specified, multiple channels will be created.
    sampling_rate : int, default=16000
        Sampling rate in Hz.
    duration : float, default=1.0
        Duration of the signal in seconds.
    label : str, optional
        Label for the entire signal.

    Returns
    -------
    ChannelFrame
        ChannelFrame object containing the sine wave(s).
    """
    # 直接、generate_sin_lazy関数を呼び出す
    return generate_sin_lazy(
        freqs=freqs, sampling_rate=sampling_rate, duration=duration, label=label
    )
generate_sin_lazy(freqs=1000, sampling_rate=16000, duration=1.0, label=None)

Generate sample sine wave signals using lazy computation.

Parameters

freqs : float or list of float, default=1000 Frequency of the sine wave(s) in Hz. If multiple frequencies are specified, multiple channels will be created. sampling_rate : int, default=16000 Sampling rate in Hz. duration : float, default=1.0 Duration of the signal in seconds. label : str, optional Label for the entire signal.

Returns

ChannelFrame Lazy ChannelFrame object containing the sine wave(s).

Source code in wandas/utils/generate_sample.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def generate_sin_lazy(
    freqs: Union[float, list[float]] = 1000,
    sampling_rate: int = 16000,
    duration: float = 1.0,
    label: Optional[str] = None,
) -> "ChannelFrame":
    """
    Generate sample sine wave signals using lazy computation.

    Parameters
    ----------
    freqs : float or list of float, default=1000
        Frequency of the sine wave(s) in Hz.
        If multiple frequencies are specified, multiple channels will be created.
    sampling_rate : int, default=16000
        Sampling rate in Hz.
    duration : float, default=1.0
        Duration of the signal in seconds.
    label : str, optional
        Label for the entire signal.

    Returns
    -------
    ChannelFrame
        Lazy ChannelFrame object containing the sine wave(s).
    """
    from wandas.frames.channel import ChannelFrame

    label = label or "Generated Sin"
    t = np.linspace(0, duration, int(sampling_rate * duration), endpoint=False)

    _freqs: list[float]
    if isinstance(freqs, float):
        _freqs = [freqs]
    elif isinstance(freqs, list):
        _freqs = freqs
    else:
        raise ValueError("freqs must be a float or a list of floats.")

    channels = []
    labels = []
    for idx, freq in enumerate(_freqs):
        data = np.sin(2 * np.pi * freq * t)
        labels.append(f"Channel {idx + 1}")
        channels.append(data)
    return ChannelFrame.from_numpy(
        data=np.array(channels),
        label=label,
        sampling_rate=sampling_rate,
        ch_labels=labels,
    )

introspection

Utilities for runtime signature introspection.

Attributes
__all__ = ['accepted_kwargs', 'filter_kwargs'] module-attribute
Functions
accepted_kwargs(func)

Get the set of explicit keyword arguments accepted by a function and whether it accepts **kwargs.

Parameters:

Name Type Description Default
func Callable[..., Any]

The function to inspect.

required

Returns:

Type Description
set[str]

A tuple containing:

bool
  • set[str]: Set of explicit keyword argument names accepted by func.
tuple[set[str], bool]
  • bool: Whether the function accepts variable keyword arguments (**kwargs).
Source code in wandas/utils/introspection.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
def accepted_kwargs(func: Callable[..., Any]) -> tuple[set[str], bool]:
    """
    Get the set of explicit keyword arguments accepted by
    a function and whether it accepts **kwargs.

    Args:
        func: The function to inspect.

    Returns:
        A tuple containing:
        - set[str]: Set of explicit keyword argument names accepted by func.
        - bool: Whether the function accepts variable keyword arguments (**kwargs).
    """
    # モックオブジェクトの場合は空セットと無制限フラグを返す
    if hasattr(func, "__module__") and func.__module__ == "unittest.mock":
        return set(), True
    try:
        params = signature(func).parameters.values()

        # 明示的に定義されている引数を収集
        explicit_kwargs = {
            p.name
            for p in params
            if p.kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY)
        }

        # **kwargsを受け付けるかどうかのフラグ
        has_var_kwargs = any(p.kind is Parameter.VAR_KEYWORD for p in params)

        return explicit_kwargs, has_var_kwargs
    except (ValueError, TypeError):
        # シグネチャを取得できない場合は空セットと無制限フラグを返す
        return set(), True
filter_kwargs(func, kwargs, *, strict_mode=False)

Filter keyword arguments to only those accepted by the function.

This function examines the signature of func and returns a dictionary containing only the key-value pairs from kwargs that are valid keyword arguments for func.

Parameters:

Name Type Description Default
func Callable[..., Any]

The function to filter keyword arguments for.

required
kwargs Mapping[str, Any]

The keyword arguments to filter.

required
strict_mode bool

If True, only explicitly defined parameters are passed even when the function accepts kwargs. If False (default), all parameters are passed to functions that accept kwargs, but a warning is issued for parameters not explicitly defined.

False

Returns:

Type Description
dict[str, Any]

A dictionary containing only the key-value pairs that are valid for func.

Source code in wandas/utils/introspection.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def filter_kwargs(
    func: Callable[..., Any],
    kwargs: Mapping[str, Any],
    *,
    strict_mode: bool = False,
) -> dict[str, Any]:
    """
    Filter keyword arguments to only those accepted by the function.

    This function examines the signature of `func` and returns a dictionary
    containing only the key-value pairs from `kwargs` that are valid keyword
    arguments for `func`.

    Args:
        func: The function to filter keyword arguments for.
        kwargs: The keyword arguments to filter.
        strict_mode: If True, only explicitly defined parameters are passed even when
            the function accepts **kwargs. If False (default), all parameters are
            passed to functions that accept **kwargs, but a warning is issued for
            parameters not explicitly defined.

    Returns:
        A dictionary containing only the key-value pairs that are valid for `func`.
    """
    explicit_params, accepts_var_kwargs = accepted_kwargs(func)

    # **kwargsを受け付けない場合、または strict_mode が True の場合は、
    # 明示的なパラメータのみをフィルタリング
    if not accepts_var_kwargs or strict_mode:
        filtered = {k: v for k, v in kwargs.items() if k in explicit_params}
        return filtered

    # **kwargsを受け付ける場合(strict_modeがFalseの場合)は全キーを許可
    # ただし、明示的に定義されていないキーには警告を出す
    unknown = set(kwargs) - explicit_params
    if unknown:
        warnings.warn(
            f"Implicit kwargs for {func.__name__}: {unknown}",
            UserWarning,
            stacklevel=2,
        )
    return dict(kwargs)

types

Attributes
Real = np.number[Any] module-attribute
Complex = np.complexfloating[Any, Any] module-attribute
NDArrayReal = npt.NDArray[Real] module-attribute
NDArrayComplex = npt.NDArray[Complex] module-attribute

util

Attributes
Functions
unit_to_ref(unit)

Convert unit to reference value.

Parameters

unit : str Unit string.

Returns

float Reference value for the unit. For 'Pa', returns 2e-5 (20 μPa). For other units, returns 1.0.

Source code in wandas/utils/util.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def unit_to_ref(unit: str) -> float:
    """
    Convert unit to reference value.

    Parameters
    ----------
    unit : str
        Unit string.

    Returns
    -------
    float
        Reference value for the unit. For 'Pa', returns 2e-5 (20 μPa).
        For other units, returns 1.0.
    """
    if unit == "Pa":
        return 2e-5

    else:
        return 1.0
calculate_rms(wave)

Calculate the root mean square of the wave.

Parameters

wave : NDArrayReal Input waveform data. Can be multi-channel (shape: [channels, samples]) or single channel (shape: [samples]).

Returns

Union[float, NDArray[np.float64]] RMS value(s). For multi-channel input, returns an array of RMS values, one per channel. For single-channel input, returns a single RMS value.

Source code in wandas/utils/util.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def calculate_rms(wave: "NDArrayReal") -> "NDArrayReal":
    """
    Calculate the root mean square of the wave.

    Parameters
    ----------
    wave : NDArrayReal
        Input waveform data. Can be multi-channel (shape: [channels, samples])
        or single channel (shape: [samples]).

    Returns
    -------
    Union[float, NDArray[np.float64]]
        RMS value(s). For multi-channel input, returns an array of RMS values,
        one per channel. For single-channel input, returns a single RMS value.
    """
    # Calculate RMS considering axis (over the last dimension)
    axis_to_use = -1 if wave.ndim > 1 else None
    rms_values: NDArrayReal = np.sqrt(
        np.mean(np.square(wave), axis=axis_to_use, keepdims=True)
    )
    return rms_values
calculate_desired_noise_rms(clean_rms, snr)

Calculate the desired noise RMS based on clean signal RMS and target SNR.

Parameters

clean_rms : "NDArrayReal" RMS value(s) of the clean signal. Can be a single value or an array for multi-channel. snr : float Target Signal-to-Noise Ratio in dB.

Returns

"NDArrayReal" Desired noise RMS value(s) to achieve the target SNR.

Source code in wandas/utils/util.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def calculate_desired_noise_rms(clean_rms: "NDArrayReal", snr: float) -> "NDArrayReal":
    """
    Calculate the desired noise RMS based on clean signal RMS and target SNR.

    Parameters
    ----------
    clean_rms : "NDArrayReal"
        RMS value(s) of the clean signal.
        Can be a single value or an array for multi-channel.
    snr : float
        Target Signal-to-Noise Ratio in dB.

    Returns
    -------
    "NDArrayReal"
        Desired noise RMS value(s) to achieve the target SNR.
    """
    a = snr / 20
    noise_rms = clean_rms / (10**a)
    return noise_rms
amplitude_to_db(amplitude, ref)

Convert amplitude to decibel.

Parameters

amplitude : NDArrayReal Input amplitude data. ref : float Reference value for conversion.

Returns

NDArrayReal Amplitude data converted to decibels.

Source code in wandas/utils/util.py
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def amplitude_to_db(amplitude: "NDArrayReal", ref: float) -> "NDArrayReal":
    """
    Convert amplitude to decibel.

    Parameters
    ----------
    amplitude : NDArrayReal
        Input amplitude data.
    ref : float
        Reference value for conversion.

    Returns
    -------
    NDArrayReal
        Amplitude data converted to decibels.
    """
    db: NDArrayReal = librosa.amplitude_to_db(
        np.abs(amplitude), ref=ref, amin=1e-15, top_db=None
    )
    return db
level_trigger(data, level, offset=0, hold=1)

Find points where the signal crosses the specified level from below.

Parameters

data : NDArrayReal Input signal data. level : float Threshold level for triggering. offset : int, default=0 Offset to add to trigger points. hold : int, default=1 Minimum number of samples between successive trigger points.

Returns

list of int List of sample indices where the signal crosses the level.

Source code in wandas/utils/util.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
def level_trigger(
    data: "NDArrayReal", level: float, offset: int = 0, hold: int = 1
) -> list[int]:
    """
    Find points where the signal crosses the specified level from below.

    Parameters
    ----------
    data : NDArrayReal
        Input signal data.
    level : float
        Threshold level for triggering.
    offset : int, default=0
        Offset to add to trigger points.
    hold : int, default=1
        Minimum number of samples between successive trigger points.

    Returns
    -------
    list of int
        List of sample indices where the signal crosses the level.
    """
    trig_point: list[int] = []

    sig_len = len(data)
    diff = np.diff(np.sign(data - level))
    level_point = np.where(diff > 0)[0]
    level_point = level_point[(level_point + hold) < sig_len]

    if len(level_point) == 0:
        return list()

    last_point = level_point[0]
    trig_point.append(last_point + offset)
    for i in level_point:
        if (last_point + hold) < i:
            trig_point.append(i + offset)
            last_point = i

    return trig_point
cut_sig(data, point_list, cut_len, taper_rate=0, dc_cut=False)

Cut segments from signal at specified points.

Parameters

data : NDArrayReal Input signal data. point_list : list of int List of starting points for cutting. cut_len : int Length of each segment to cut. taper_rate : float, default=0 Taper rate for Tukey window applied to segments. A value of 0 means no tapering, 1 means full tapering. dc_cut : bool, default=False Whether to remove DC component (mean) from segments.

Returns

NDArrayReal Array containing cut segments with shape (n_segments, cut_len).

Source code in wandas/utils/util.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
def cut_sig(
    data: "NDArrayReal",
    point_list: list[int],
    cut_len: int,
    taper_rate: float = 0,
    dc_cut: bool = False,
) -> "NDArrayReal":
    """
    Cut segments from signal at specified points.

    Parameters
    ----------
    data : NDArrayReal
        Input signal data.
    point_list : list of int
        List of starting points for cutting.
    cut_len : int
        Length of each segment to cut.
    taper_rate : float, default=0
        Taper rate for Tukey window applied to segments.
        A value of 0 means no tapering, 1 means full tapering.
    dc_cut : bool, default=False
        Whether to remove DC component (mean) from segments.

    Returns
    -------
    NDArrayReal
        Array containing cut segments with shape (n_segments, cut_len).
    """
    length = len(data)
    point_list_ = [p for p in point_list if p >= 0 and p + cut_len <= length]
    trial: NDArrayReal = np.zeros((len(point_list_), cut_len))

    for i, v in enumerate(point_list_):
        trial[i] = data[v : v + cut_len]
        if dc_cut:
            trial[i] = trial[i] - trial[i].mean()

    win: NDArrayReal = tukey(cut_len, taper_rate).astype(trial.dtype)[np.newaxis, :]
    trial = trial * win
    return trial

可視化モジュール

可視化モジュールはデータの視覚化機能を提供します。

wandas.visualization

Modules

plotting

Attributes
logger = logging.getLogger(__name__) module-attribute
TFrame = TypeVar('TFrame', bound='BaseFrame[Any]') module-attribute
Classes
PlotStrategy

Bases: ABC, Generic[TFrame]

Base class for plotting strategies

Source code in wandas/visualization/plotting.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class PlotStrategy(abc.ABC, Generic[TFrame]):
    """Base class for plotting strategies"""

    name: ClassVar[str]

    @abc.abstractmethod
    def channel_plot(self, x: Any, y: Any, ax: "Axes") -> None:
        """Implementation of channel plotting"""
        pass

    @abc.abstractmethod
    def plot(
        self,
        bf: TFrame,
        ax: Optional["Axes"] = None,
        title: Optional[str] = None,
        overlay: bool = False,
        **kwargs: Any,
    ) -> Union["Axes", Iterator["Axes"]]:
        """Implementation of plotting"""
        pass
Attributes
name class-attribute
Functions
channel_plot(x, y, ax) abstractmethod

Implementation of channel plotting

Source code in wandas/visualization/plotting.py
43
44
45
46
@abc.abstractmethod
def channel_plot(self, x: Any, y: Any, ax: "Axes") -> None:
    """Implementation of channel plotting"""
    pass
plot(bf, ax=None, title=None, overlay=False, **kwargs) abstractmethod

Implementation of plotting

Source code in wandas/visualization/plotting.py
48
49
50
51
52
53
54
55
56
57
58
@abc.abstractmethod
def plot(
    self,
    bf: TFrame,
    ax: Optional["Axes"] = None,
    title: Optional[str] = None,
    overlay: bool = False,
    **kwargs: Any,
) -> Union["Axes", Iterator["Axes"]]:
    """Implementation of plotting"""
    pass
WaveformPlotStrategy

Bases: PlotStrategy['ChannelFrame']

Strategy for waveform plotting

Source code in wandas/visualization/plotting.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
class WaveformPlotStrategy(PlotStrategy["ChannelFrame"]):
    """Strategy for waveform plotting"""

    name = "waveform"

    def channel_plot(
        self,
        x: Any,
        y: Any,
        ax: "Axes",
        **kwargs: Any,
    ) -> None:
        """Implementation of channel plotting"""
        ax.plot(x, y, **kwargs)
        ax.set_ylabel("Amplitude")
        ax.grid(True)
        if "label" in kwargs:
            ax.legend()

    def plot(
        self,
        bf: "ChannelFrame",
        ax: Optional["Axes"] = None,
        title: Optional[str] = None,
        overlay: bool = False,
        **kwargs: Any,
    ) -> Union["Axes", Iterator["Axes"]]:
        """Waveform plotting"""
        kwargs = kwargs or {}
        ylabel = kwargs.pop("ylabel", "Amplitude")
        xlabel = kwargs.pop("xlabel", "Time [s]")
        alpha = kwargs.pop("alpha", 1)
        plot_kwargs = filter_kwargs(
            Line2D,
            kwargs,
            strict_mode=True,
        )
        ax_set = filter_kwargs(
            Axes.set,
            kwargs,
            strict_mode=True,
        )
        data = bf.data
        data = _reshape_to_2d(data)
        if overlay:
            if ax is None:
                fig, ax = plt.subplots(figsize=(10, 4))

            self.channel_plot(
                bf.time, data.T, ax, label=bf.labels, alpha=alpha, **plot_kwargs
            )
            ax.set(
                ylabel=ylabel,
                title=title or bf.label or "Channel Data",
                xlabel=xlabel,
                **ax_set,
            )
            if ax is None:
                fig.suptitle(title or bf.label or None)
                plt.tight_layout()
                plt.show()
            return ax
        else:
            num_channels = bf.n_channels
            fig, axs = plt.subplots(
                num_channels, 1, figsize=(10, 4 * num_channels), sharex=True
            )
            # Convert axs to list if it is a single Axes object
            if not isinstance(axs, (list, np.ndarray)):
                axs = [axs]

            axes_list = list(axs)
            for ax_i, channel_data, ch_meta in zip(axes_list, data, bf.channels):
                self.channel_plot(
                    bf.time, channel_data, ax_i, alpha=alpha, **plot_kwargs
                )
                ax_i.set(
                    ylabel=ylabel + f" [{ch_meta.unit}]",
                    title=ch_meta.label,
                    **ax_set,
                )

            axes_list[-1].set(
                xlabel="Time [s]",
            )
            fig.suptitle(title or bf.label or "Channel Data")

            if ax is None:
                plt.tight_layout()
                plt.show()

            return _return_axes_iterator(fig.axes)
Attributes
name = 'waveform' class-attribute instance-attribute
Functions
channel_plot(x, y, ax, **kwargs)

Implementation of channel plotting

Source code in wandas/visualization/plotting.py
120
121
122
123
124
125
126
127
128
129
130
131
132
def channel_plot(
    self,
    x: Any,
    y: Any,
    ax: "Axes",
    **kwargs: Any,
) -> None:
    """Implementation of channel plotting"""
    ax.plot(x, y, **kwargs)
    ax.set_ylabel("Amplitude")
    ax.grid(True)
    if "label" in kwargs:
        ax.legend()
plot(bf, ax=None, title=None, overlay=False, **kwargs)

Waveform plotting

Source code in wandas/visualization/plotting.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
def plot(
    self,
    bf: "ChannelFrame",
    ax: Optional["Axes"] = None,
    title: Optional[str] = None,
    overlay: bool = False,
    **kwargs: Any,
) -> Union["Axes", Iterator["Axes"]]:
    """Waveform plotting"""
    kwargs = kwargs or {}
    ylabel = kwargs.pop("ylabel", "Amplitude")
    xlabel = kwargs.pop("xlabel", "Time [s]")
    alpha = kwargs.pop("alpha", 1)
    plot_kwargs = filter_kwargs(
        Line2D,
        kwargs,
        strict_mode=True,
    )
    ax_set = filter_kwargs(
        Axes.set,
        kwargs,
        strict_mode=True,
    )
    data = bf.data
    data = _reshape_to_2d(data)
    if overlay:
        if ax is None:
            fig, ax = plt.subplots(figsize=(10, 4))

        self.channel_plot(
            bf.time, data.T, ax, label=bf.labels, alpha=alpha, **plot_kwargs
        )
        ax.set(
            ylabel=ylabel,
            title=title or bf.label or "Channel Data",
            xlabel=xlabel,
            **ax_set,
        )
        if ax is None:
            fig.suptitle(title or bf.label or None)
            plt.tight_layout()
            plt.show()
        return ax
    else:
        num_channels = bf.n_channels
        fig, axs = plt.subplots(
            num_channels, 1, figsize=(10, 4 * num_channels), sharex=True
        )
        # Convert axs to list if it is a single Axes object
        if not isinstance(axs, (list, np.ndarray)):
            axs = [axs]

        axes_list = list(axs)
        for ax_i, channel_data, ch_meta in zip(axes_list, data, bf.channels):
            self.channel_plot(
                bf.time, channel_data, ax_i, alpha=alpha, **plot_kwargs
            )
            ax_i.set(
                ylabel=ylabel + f" [{ch_meta.unit}]",
                title=ch_meta.label,
                **ax_set,
            )

        axes_list[-1].set(
            xlabel="Time [s]",
        )
        fig.suptitle(title or bf.label or "Channel Data")

        if ax is None:
            plt.tight_layout()
            plt.show()

        return _return_axes_iterator(fig.axes)
FrequencyPlotStrategy

Bases: PlotStrategy['SpectralFrame']

Strategy for frequency domain plotting

Source code in wandas/visualization/plotting.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
class FrequencyPlotStrategy(PlotStrategy["SpectralFrame"]):
    """Strategy for frequency domain plotting"""

    name = "frequency"

    def channel_plot(
        self,
        x: Any,
        y: Any,
        ax: "Axes",
        **kwargs: Any,
    ) -> None:
        """Implementation of channel plotting"""
        ax.plot(x, y, **kwargs)
        ax.grid(True)
        if "label" in kwargs:
            ax.legend()

    def plot(
        self,
        bf: "SpectralFrame",
        ax: Optional["Axes"] = None,
        title: Optional[str] = None,
        overlay: bool = False,
        **kwargs: Any,
    ) -> Union["Axes", Iterator["Axes"]]:
        """Frequency domain plotting"""
        kwargs = kwargs or {}
        is_aw = kwargs.pop("Aw", False)
        if (
            len(bf.operation_history) > 0
            and bf.operation_history[-1]["operation"] == "coherence"
        ):
            unit = ""
            data = bf.magnitude
            ylabel = kwargs.pop("ylabel", "coherence")
        else:
            if is_aw:
                unit = "dBA"
                data = bf.dBA
            else:
                unit = "dB"
                data = bf.dB
            ylabel = kwargs.pop("ylabel", f"Spectrum level [{unit}]")
        data = _reshape_to_2d(data)
        xlabel = kwargs.pop("xlabel", "Frequency [Hz]")
        alpha = kwargs.pop("alpha", 1)
        plot_kwargs = filter_kwargs(Line2D, kwargs, strict_mode=True)
        ax_set = filter_kwargs(Axes.set, kwargs, strict_mode=True)
        if overlay:
            if ax is None:
                _, ax = plt.subplots(figsize=(10, 4))
            self.channel_plot(
                bf.freqs,
                data.T,
                ax,
                label=bf.labels,
                alpha=alpha,
                **plot_kwargs,
            )
            ax.set(
                ylabel=ylabel,
                xlabel=xlabel,
                title=title or bf.label or "Channel Data",
                **ax_set,
            )
            if ax is None:
                plt.tight_layout()
                plt.show()
            return ax
        else:
            num_channels = bf.n_channels
            fig, axs = plt.subplots(
                num_channels, 1, figsize=(10, 4 * num_channels), sharex=True
            )
            # Convert axs to list if it is a single Axes object
            if not isinstance(axs, (list, np.ndarray)):
                axs = [axs]

            axes_list = list(axs)
            for ax_i, channel_data, ch_meta in zip(axes_list, data, bf.channels):
                self.channel_plot(
                    bf.freqs,
                    channel_data,
                    ax_i,
                    label=ch_meta.label,
                    alpha=alpha,
                    **plot_kwargs,
                )
                ax_i.set(
                    ylabel=ylabel,
                    title=ch_meta.label,
                    xlabel=xlabel,
                    **ax_set,
                )
            axes_list[-1].set(ylabel=ylabel, xlabel=xlabel)
            fig.suptitle(title or bf.label or "Channel Data")
            if ax is None:
                plt.tight_layout()
                plt.show()
            return _return_axes_iterator(fig.axes)
Attributes
name = 'frequency' class-attribute instance-attribute
Functions
channel_plot(x, y, ax, **kwargs)

Implementation of channel plotting

Source code in wandas/visualization/plotting.py
214
215
216
217
218
219
220
221
222
223
224
225
def channel_plot(
    self,
    x: Any,
    y: Any,
    ax: "Axes",
    **kwargs: Any,
) -> None:
    """Implementation of channel plotting"""
    ax.plot(x, y, **kwargs)
    ax.grid(True)
    if "label" in kwargs:
        ax.legend()
plot(bf, ax=None, title=None, overlay=False, **kwargs)

Frequency domain plotting

Source code in wandas/visualization/plotting.py
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
def plot(
    self,
    bf: "SpectralFrame",
    ax: Optional["Axes"] = None,
    title: Optional[str] = None,
    overlay: bool = False,
    **kwargs: Any,
) -> Union["Axes", Iterator["Axes"]]:
    """Frequency domain plotting"""
    kwargs = kwargs or {}
    is_aw = kwargs.pop("Aw", False)
    if (
        len(bf.operation_history) > 0
        and bf.operation_history[-1]["operation"] == "coherence"
    ):
        unit = ""
        data = bf.magnitude
        ylabel = kwargs.pop("ylabel", "coherence")
    else:
        if is_aw:
            unit = "dBA"
            data = bf.dBA
        else:
            unit = "dB"
            data = bf.dB
        ylabel = kwargs.pop("ylabel", f"Spectrum level [{unit}]")
    data = _reshape_to_2d(data)
    xlabel = kwargs.pop("xlabel", "Frequency [Hz]")
    alpha = kwargs.pop("alpha", 1)
    plot_kwargs = filter_kwargs(Line2D, kwargs, strict_mode=True)
    ax_set = filter_kwargs(Axes.set, kwargs, strict_mode=True)
    if overlay:
        if ax is None:
            _, ax = plt.subplots(figsize=(10, 4))
        self.channel_plot(
            bf.freqs,
            data.T,
            ax,
            label=bf.labels,
            alpha=alpha,
            **plot_kwargs,
        )
        ax.set(
            ylabel=ylabel,
            xlabel=xlabel,
            title=title or bf.label or "Channel Data",
            **ax_set,
        )
        if ax is None:
            plt.tight_layout()
            plt.show()
        return ax
    else:
        num_channels = bf.n_channels
        fig, axs = plt.subplots(
            num_channels, 1, figsize=(10, 4 * num_channels), sharex=True
        )
        # Convert axs to list if it is a single Axes object
        if not isinstance(axs, (list, np.ndarray)):
            axs = [axs]

        axes_list = list(axs)
        for ax_i, channel_data, ch_meta in zip(axes_list, data, bf.channels):
            self.channel_plot(
                bf.freqs,
                channel_data,
                ax_i,
                label=ch_meta.label,
                alpha=alpha,
                **plot_kwargs,
            )
            ax_i.set(
                ylabel=ylabel,
                title=ch_meta.label,
                xlabel=xlabel,
                **ax_set,
            )
        axes_list[-1].set(ylabel=ylabel, xlabel=xlabel)
        fig.suptitle(title or bf.label or "Channel Data")
        if ax is None:
            plt.tight_layout()
            plt.show()
        return _return_axes_iterator(fig.axes)
NOctPlotStrategy

Bases: PlotStrategy['NOctFrame']

Strategy for N-octave band analysis plotting

Source code in wandas/visualization/plotting.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
class NOctPlotStrategy(PlotStrategy["NOctFrame"]):
    """Strategy for N-octave band analysis plotting"""

    name = "noct"

    def channel_plot(
        self,
        x: Any,
        y: Any,
        ax: "Axes",
        **kwargs: Any,
    ) -> None:
        """Implementation of channel plotting"""
        ax.step(x, y, **kwargs)
        ax.grid(True)
        if "label" in kwargs:
            ax.legend()

    def plot(
        self,
        bf: "NOctFrame",
        ax: Optional["Axes"] = None,
        title: Optional[str] = None,
        overlay: bool = False,
        **kwargs: Any,
    ) -> Union["Axes", Iterator["Axes"]]:
        """N-octave band analysis plotting"""
        kwargs = kwargs or {}
        is_aw = kwargs.pop("Aw", False)

        if is_aw:
            unit = "dBrA"
            data = bf.dBA
        else:
            unit = "dBr"
            data = bf.dB
        data = _reshape_to_2d(data)
        ylabel = kwargs.pop("ylabel", f"Spectrum level [{unit}]")
        xlabel = kwargs.pop("xlabel", "Center frequency [Hz]")
        alpha = kwargs.pop("alpha", 1)
        plot_kwargs = filter_kwargs(Line2D, kwargs, strict_mode=True)
        ax_set = filter_kwargs(Axes.set, kwargs, strict_mode=True)
        if overlay:
            if ax is None:
                _, ax = plt.subplots(figsize=(10, 4))
            self.channel_plot(
                bf.freqs,
                data.T,
                ax,
                label=bf.labels,
                alpha=alpha,
                **plot_kwargs,
            )
            default_title = f"1/{str(bf.n)}-Octave Spectrum"
            actual_title = title if title else (bf.label or default_title)
            ax.set(
                ylabel=ylabel,
                xlabel=xlabel,
                title=actual_title,
                **ax_set,
            )
            if ax is None:
                plt.tight_layout()
                plt.show()
            return ax
        else:
            num_channels = bf.n_channels
            fig, axs = plt.subplots(
                num_channels, 1, figsize=(10, 4 * num_channels), sharex=True
            )
            # Convert axs to list if it is a single Axes object
            if not isinstance(axs, (list, np.ndarray)):
                axs = [axs]

            axes_list = list(axs)
            for ax_i, channel_data, ch_meta in zip(axes_list, data, bf.channels):
                self.channel_plot(
                    bf.freqs,
                    channel_data,
                    ax_i,
                    label=ch_meta.label,
                    alpha=alpha,
                    **plot_kwargs,
                )
                ax_i.set(
                    ylabel=ylabel,
                    title=ch_meta.label,
                    xlabel=xlabel,
                    **ax_set,
                )
            axes_list[-1].set(ylabel=ylabel, xlabel=xlabel)
            fig.suptitle(title or bf.label or f"1/{str(bf.n)}-Octave Spectrum")
            if ax is None:
                plt.tight_layout()
                plt.show()
            return _return_axes_iterator(fig.axes)
Attributes
name = 'noct' class-attribute instance-attribute
Functions
channel_plot(x, y, ax, **kwargs)

Implementation of channel plotting

Source code in wandas/visualization/plotting.py
317
318
319
320
321
322
323
324
325
326
327
328
def channel_plot(
    self,
    x: Any,
    y: Any,
    ax: "Axes",
    **kwargs: Any,
) -> None:
    """Implementation of channel plotting"""
    ax.step(x, y, **kwargs)
    ax.grid(True)
    if "label" in kwargs:
        ax.legend()
plot(bf, ax=None, title=None, overlay=False, **kwargs)

N-octave band analysis plotting

Source code in wandas/visualization/plotting.py
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
def plot(
    self,
    bf: "NOctFrame",
    ax: Optional["Axes"] = None,
    title: Optional[str] = None,
    overlay: bool = False,
    **kwargs: Any,
) -> Union["Axes", Iterator["Axes"]]:
    """N-octave band analysis plotting"""
    kwargs = kwargs or {}
    is_aw = kwargs.pop("Aw", False)

    if is_aw:
        unit = "dBrA"
        data = bf.dBA
    else:
        unit = "dBr"
        data = bf.dB
    data = _reshape_to_2d(data)
    ylabel = kwargs.pop("ylabel", f"Spectrum level [{unit}]")
    xlabel = kwargs.pop("xlabel", "Center frequency [Hz]")
    alpha = kwargs.pop("alpha", 1)
    plot_kwargs = filter_kwargs(Line2D, kwargs, strict_mode=True)
    ax_set = filter_kwargs(Axes.set, kwargs, strict_mode=True)
    if overlay:
        if ax is None:
            _, ax = plt.subplots(figsize=(10, 4))
        self.channel_plot(
            bf.freqs,
            data.T,
            ax,
            label=bf.labels,
            alpha=alpha,
            **plot_kwargs,
        )
        default_title = f"1/{str(bf.n)}-Octave Spectrum"
        actual_title = title if title else (bf.label or default_title)
        ax.set(
            ylabel=ylabel,
            xlabel=xlabel,
            title=actual_title,
            **ax_set,
        )
        if ax is None:
            plt.tight_layout()
            plt.show()
        return ax
    else:
        num_channels = bf.n_channels
        fig, axs = plt.subplots(
            num_channels, 1, figsize=(10, 4 * num_channels), sharex=True
        )
        # Convert axs to list if it is a single Axes object
        if not isinstance(axs, (list, np.ndarray)):
            axs = [axs]

        axes_list = list(axs)
        for ax_i, channel_data, ch_meta in zip(axes_list, data, bf.channels):
            self.channel_plot(
                bf.freqs,
                channel_data,
                ax_i,
                label=ch_meta.label,
                alpha=alpha,
                **plot_kwargs,
            )
            ax_i.set(
                ylabel=ylabel,
                title=ch_meta.label,
                xlabel=xlabel,
                **ax_set,
            )
        axes_list[-1].set(ylabel=ylabel, xlabel=xlabel)
        fig.suptitle(title or bf.label or f"1/{str(bf.n)}-Octave Spectrum")
        if ax is None:
            plt.tight_layout()
            plt.show()
        return _return_axes_iterator(fig.axes)
SpectrogramPlotStrategy

Bases: PlotStrategy['SpectrogramFrame']

Strategy for spectrogram plotting

Source code in wandas/visualization/plotting.py
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
class SpectrogramPlotStrategy(PlotStrategy["SpectrogramFrame"]):
    """Strategy for spectrogram plotting"""

    name = "spectrogram"

    def channel_plot(
        self,
        x: Any,
        y: Any,
        ax: "Axes",
        **kwargs: Any,
    ) -> None:
        """Implementation of channel plotting"""
        pass

    def plot(
        self,
        bf: "SpectrogramFrame",
        ax: Optional["Axes"] = None,
        title: Optional[str] = None,
        overlay: bool = False,
        **kwargs: Any,
    ) -> Union["Axes", Iterator["Axes"]]:
        """Spectrogram plotting"""
        if overlay:
            raise ValueError("Overlay is not supported for SpectrogramPlotStrategy.")

        if ax is not None and bf.n_channels > 1:
            raise ValueError("ax must be None when n_channels > 1.")

        kwargs = kwargs or {}

        is_aw = kwargs.pop("Aw", False)
        if is_aw:
            unit = "dBA"
            data = bf.dBA
        else:
            unit = "dB"
            data = bf.dB
        data = _reshape_spectrogram_data(data)
        specshow_kwargs = filter_kwargs(display.specshow, kwargs, strict_mode=True)
        ax_set_kwargs = filter_kwargs(Axes.set, kwargs, strict_mode=True)

        cmap = kwargs.pop("cmap", "jet")
        vmin = kwargs.pop("vmin", None)
        vmax = kwargs.pop("vmax", None)

        if ax is not None:
            img = display.specshow(
                data=data[0],
                sr=bf.sampling_rate,
                hop_length=bf.hop_length,
                n_fft=bf.n_fft,
                win_length=bf.win_length,
                x_axis="time",
                y_axis="linear",
                cmap=cmap,
                ax=ax,
                vmin=vmin,
                vmax=vmax,
                **specshow_kwargs,
            )
            ax.set(
                title=title or bf.label or "Spectrogram",
                ylabel="Frequency [Hz]",
                xlabel="Time [s]",
                **ax_set_kwargs,
            )

            fig = ax.figure
            if fig is not None:
                try:
                    cbar = fig.colorbar(img, ax=ax)
                    cbar.set_label(f"Spectrum level [{unit}]")
                except (ValueError, AttributeError) as e:
                    # Handle case where img doesn't have proper colorbar properties
                    logger.warning(
                        f"Failed to create colorbar for spectrogram: "
                        f"{type(e).__name__}: {e}"
                    )
            return ax

        else:
            # Create a new figure if ax is None
            num_channels = bf.n_channels
            fig, axs = plt.subplots(
                num_channels, 1, figsize=(10, 5 * num_channels), sharex=True
            )
            if not isinstance(fig, Figure):
                raise ValueError("fig must be a matplotlib Figure object.")
            # Convert axs to array if it is a single Axes object
            if not isinstance(axs, np.ndarray):
                axs = np.array([axs])

            for ax_i, channel_data, ch_meta in zip(axs.flatten(), data, bf.channels):
                img = display.specshow(
                    data=channel_data,
                    sr=bf.sampling_rate,
                    hop_length=bf.hop_length,
                    n_fft=bf.n_fft,
                    win_length=bf.win_length,
                    x_axis="time",
                    y_axis="linear",
                    ax=ax_i,
                    cmap=cmap,
                    vmin=vmin,
                    vmax=vmax,
                    **specshow_kwargs,
                )
                ax_i.set(
                    title=ch_meta.label,
                    ylabel="Frequency [Hz]",
                    xlabel="Time [s]",
                    **ax_set_kwargs,
                )
                try:
                    cbar = ax_i.figure.colorbar(img, ax=ax_i)
                    cbar.set_label(f"Spectrum level [{unit}]")
                except (ValueError, AttributeError) as e:
                    # Handle case where img doesn't have proper colorbar properties
                    logger.warning(
                        f"Failed to create colorbar for spectrogram: "
                        f"{type(e).__name__}: {e}"
                    )
                fig.suptitle(title or "Spectrogram Data")
            plt.tight_layout()
            plt.show()

            return _return_axes_iterator(fig.axes)
Attributes
name = 'spectrogram' class-attribute instance-attribute
Functions
channel_plot(x, y, ax, **kwargs)

Implementation of channel plotting

Source code in wandas/visualization/plotting.py
415
416
417
418
419
420
421
422
423
def channel_plot(
    self,
    x: Any,
    y: Any,
    ax: "Axes",
    **kwargs: Any,
) -> None:
    """Implementation of channel plotting"""
    pass
plot(bf, ax=None, title=None, overlay=False, **kwargs)

Spectrogram plotting

Source code in wandas/visualization/plotting.py
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
def plot(
    self,
    bf: "SpectrogramFrame",
    ax: Optional["Axes"] = None,
    title: Optional[str] = None,
    overlay: bool = False,
    **kwargs: Any,
) -> Union["Axes", Iterator["Axes"]]:
    """Spectrogram plotting"""
    if overlay:
        raise ValueError("Overlay is not supported for SpectrogramPlotStrategy.")

    if ax is not None and bf.n_channels > 1:
        raise ValueError("ax must be None when n_channels > 1.")

    kwargs = kwargs or {}

    is_aw = kwargs.pop("Aw", False)
    if is_aw:
        unit = "dBA"
        data = bf.dBA
    else:
        unit = "dB"
        data = bf.dB
    data = _reshape_spectrogram_data(data)
    specshow_kwargs = filter_kwargs(display.specshow, kwargs, strict_mode=True)
    ax_set_kwargs = filter_kwargs(Axes.set, kwargs, strict_mode=True)

    cmap = kwargs.pop("cmap", "jet")
    vmin = kwargs.pop("vmin", None)
    vmax = kwargs.pop("vmax", None)

    if ax is not None:
        img = display.specshow(
            data=data[0],
            sr=bf.sampling_rate,
            hop_length=bf.hop_length,
            n_fft=bf.n_fft,
            win_length=bf.win_length,
            x_axis="time",
            y_axis="linear",
            cmap=cmap,
            ax=ax,
            vmin=vmin,
            vmax=vmax,
            **specshow_kwargs,
        )
        ax.set(
            title=title or bf.label or "Spectrogram",
            ylabel="Frequency [Hz]",
            xlabel="Time [s]",
            **ax_set_kwargs,
        )

        fig = ax.figure
        if fig is not None:
            try:
                cbar = fig.colorbar(img, ax=ax)
                cbar.set_label(f"Spectrum level [{unit}]")
            except (ValueError, AttributeError) as e:
                # Handle case where img doesn't have proper colorbar properties
                logger.warning(
                    f"Failed to create colorbar for spectrogram: "
                    f"{type(e).__name__}: {e}"
                )
        return ax

    else:
        # Create a new figure if ax is None
        num_channels = bf.n_channels
        fig, axs = plt.subplots(
            num_channels, 1, figsize=(10, 5 * num_channels), sharex=True
        )
        if not isinstance(fig, Figure):
            raise ValueError("fig must be a matplotlib Figure object.")
        # Convert axs to array if it is a single Axes object
        if not isinstance(axs, np.ndarray):
            axs = np.array([axs])

        for ax_i, channel_data, ch_meta in zip(axs.flatten(), data, bf.channels):
            img = display.specshow(
                data=channel_data,
                sr=bf.sampling_rate,
                hop_length=bf.hop_length,
                n_fft=bf.n_fft,
                win_length=bf.win_length,
                x_axis="time",
                y_axis="linear",
                ax=ax_i,
                cmap=cmap,
                vmin=vmin,
                vmax=vmax,
                **specshow_kwargs,
            )
            ax_i.set(
                title=ch_meta.label,
                ylabel="Frequency [Hz]",
                xlabel="Time [s]",
                **ax_set_kwargs,
            )
            try:
                cbar = ax_i.figure.colorbar(img, ax=ax_i)
                cbar.set_label(f"Spectrum level [{unit}]")
            except (ValueError, AttributeError) as e:
                # Handle case where img doesn't have proper colorbar properties
                logger.warning(
                    f"Failed to create colorbar for spectrogram: "
                    f"{type(e).__name__}: {e}"
                )
            fig.suptitle(title or "Spectrogram Data")
        plt.tight_layout()
        plt.show()

        return _return_axes_iterator(fig.axes)
DescribePlotStrategy

Bases: PlotStrategy['ChannelFrame']

Strategy for visualizing ChannelFrame data with describe plot

Source code in wandas/visualization/plotting.py
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
class DescribePlotStrategy(PlotStrategy["ChannelFrame"]):
    """Strategy for visualizing ChannelFrame data with describe plot"""

    name = "describe"

    def channel_plot(self, x: Any, y: Any, ax: "Axes", **kwargs: Any) -> None:
        """Implementation of channel plotting"""
        pass  # This method is not used for describe plot

    def plot(
        self,
        bf: "ChannelFrame",
        ax: Optional["Axes"] = None,
        title: Optional[str] = None,
        overlay: bool = False,
        **kwargs: Any,
    ) -> Union["Axes", Iterator["Axes"]]:
        """Implementation of describe method for visualizing ChannelFrame data"""

        fmin = kwargs.pop("fmin", 0)
        fmax = kwargs.pop("fmax", None)
        cmap = kwargs.pop("cmap", "jet")
        vmin = kwargs.pop("vmin", None)
        vmax = kwargs.pop("vmax", None)
        xlim = kwargs.pop("xlim", None)
        ylim = kwargs.pop("ylim", None)
        is_aw = kwargs.pop("Aw", False)
        waveform = kwargs.pop("waveform", {})
        spectral = kwargs.pop("spectral", dict(xlim=(vmin, vmax)))

        gs = gridspec.GridSpec(2, 3, height_ratios=[1, 3], width_ratios=[3, 1, 0.1])
        gs.update(wspace=0.2)

        fig = plt.figure(figsize=(12, 6))
        fig.subplots_adjust(wspace=0.0001)

        # First subplot (Time Plot)
        ax_1 = fig.add_subplot(gs[0])
        bf.plot(plot_type="waveform", ax=ax_1, overlay=True)
        ax_1.set(**waveform)
        ax_1.legend().set_visible(False)
        ax_1.set(xlabel="", title="")

        # Second subplot (STFT Plot)
        ax_2 = fig.add_subplot(gs[3], sharex=ax_1)
        stft_ch = bf.stft()
        if is_aw:
            unit = "dBA"
            channel_data = stft_ch.dBA
        else:
            unit = "dB"
            channel_data = stft_ch.dB
        if channel_data.ndim == 3:
            channel_data = channel_data[0]
        # Get the maximum value of the data and round it to a convenient value
        if vmax is None:
            data_max = np.nanmax(channel_data)
            # Round to a convenient number with increments of 10, 5, or 2
            for step in [10, 5, 2]:
                rounded_max = np.ceil(data_max / step) * step
                if rounded_max >= data_max:
                    vmax = rounded_max
                    vmin = vmax - 180
                    break
        img = display.specshow(
            data=channel_data,
            sr=bf.sampling_rate,
            hop_length=stft_ch.hop_length,
            n_fft=stft_ch.n_fft,
            win_length=stft_ch.win_length,
            x_axis="time",
            y_axis="linear",
            ax=ax_2,
            fmin=fmin,
            fmax=fmax,
            cmap=cmap,
            vmin=vmin,
            vmax=vmax,
        )
        ax_2.set(xlim=xlim, ylim=ylim)

        # Third subplot
        ax_3 = fig.add_subplot(gs[1])
        ax_3.axis("off")

        # Fourth subplot (Welch Plot)
        ax_4 = fig.add_subplot(gs[4], sharey=ax_2)
        welch_ch = bf.welch()
        if is_aw:
            unit = "dBA"
            data_db = welch_ch.dBA
        else:
            unit = "dB"
            data_db = welch_ch.dB
        ax_4.plot(data_db.T, welch_ch.freqs.T)
        ax_4.grid(True)
        ax_4.set(xlabel=f"Spectrum level [{unit}]", **spectral)

        cbar = fig.colorbar(img, ax=ax_4, format="%+2.0f")
        cbar.set_label(unit)
        fig.suptitle(title or bf.label or "Channel Data")

        return _return_axes_iterator(fig.axes)
Attributes
name = 'describe' class-attribute instance-attribute
Functions
channel_plot(x, y, ax, **kwargs)

Implementation of channel plotting

Source code in wandas/visualization/plotting.py
546
547
548
def channel_plot(self, x: Any, y: Any, ax: "Axes", **kwargs: Any) -> None:
    """Implementation of channel plotting"""
    pass  # This method is not used for describe plot
plot(bf, ax=None, title=None, overlay=False, **kwargs)

Implementation of describe method for visualizing ChannelFrame data

Source code in wandas/visualization/plotting.py
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
def plot(
    self,
    bf: "ChannelFrame",
    ax: Optional["Axes"] = None,
    title: Optional[str] = None,
    overlay: bool = False,
    **kwargs: Any,
) -> Union["Axes", Iterator["Axes"]]:
    """Implementation of describe method for visualizing ChannelFrame data"""

    fmin = kwargs.pop("fmin", 0)
    fmax = kwargs.pop("fmax", None)
    cmap = kwargs.pop("cmap", "jet")
    vmin = kwargs.pop("vmin", None)
    vmax = kwargs.pop("vmax", None)
    xlim = kwargs.pop("xlim", None)
    ylim = kwargs.pop("ylim", None)
    is_aw = kwargs.pop("Aw", False)
    waveform = kwargs.pop("waveform", {})
    spectral = kwargs.pop("spectral", dict(xlim=(vmin, vmax)))

    gs = gridspec.GridSpec(2, 3, height_ratios=[1, 3], width_ratios=[3, 1, 0.1])
    gs.update(wspace=0.2)

    fig = plt.figure(figsize=(12, 6))
    fig.subplots_adjust(wspace=0.0001)

    # First subplot (Time Plot)
    ax_1 = fig.add_subplot(gs[0])
    bf.plot(plot_type="waveform", ax=ax_1, overlay=True)
    ax_1.set(**waveform)
    ax_1.legend().set_visible(False)
    ax_1.set(xlabel="", title="")

    # Second subplot (STFT Plot)
    ax_2 = fig.add_subplot(gs[3], sharex=ax_1)
    stft_ch = bf.stft()
    if is_aw:
        unit = "dBA"
        channel_data = stft_ch.dBA
    else:
        unit = "dB"
        channel_data = stft_ch.dB
    if channel_data.ndim == 3:
        channel_data = channel_data[0]
    # Get the maximum value of the data and round it to a convenient value
    if vmax is None:
        data_max = np.nanmax(channel_data)
        # Round to a convenient number with increments of 10, 5, or 2
        for step in [10, 5, 2]:
            rounded_max = np.ceil(data_max / step) * step
            if rounded_max >= data_max:
                vmax = rounded_max
                vmin = vmax - 180
                break
    img = display.specshow(
        data=channel_data,
        sr=bf.sampling_rate,
        hop_length=stft_ch.hop_length,
        n_fft=stft_ch.n_fft,
        win_length=stft_ch.win_length,
        x_axis="time",
        y_axis="linear",
        ax=ax_2,
        fmin=fmin,
        fmax=fmax,
        cmap=cmap,
        vmin=vmin,
        vmax=vmax,
    )
    ax_2.set(xlim=xlim, ylim=ylim)

    # Third subplot
    ax_3 = fig.add_subplot(gs[1])
    ax_3.axis("off")

    # Fourth subplot (Welch Plot)
    ax_4 = fig.add_subplot(gs[4], sharey=ax_2)
    welch_ch = bf.welch()
    if is_aw:
        unit = "dBA"
        data_db = welch_ch.dBA
    else:
        unit = "dB"
        data_db = welch_ch.dB
    ax_4.plot(data_db.T, welch_ch.freqs.T)
    ax_4.grid(True)
    ax_4.set(xlabel=f"Spectrum level [{unit}]", **spectral)

    cbar = fig.colorbar(img, ax=ax_4, format="%+2.0f")
    cbar.set_label(unit)
    fig.suptitle(title or bf.label or "Channel Data")

    return _return_axes_iterator(fig.axes)
MatrixPlotStrategy

Bases: PlotStrategy[Union['SpectralFrame']]

Strategy for displaying relationships between channels in matrix format

Source code in wandas/visualization/plotting.py
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
class MatrixPlotStrategy(PlotStrategy[Union["SpectralFrame"]]):
    """Strategy for displaying relationships between channels in matrix format"""

    name = "matrix"

    def channel_plot(
        self,
        x: Any,
        y: Any,
        ax: "Axes",
        title: Optional[str] = None,
        ylabel: str = "",
        xlabel: str = "Frequency [Hz]",
        alpha: float = 0,
        **kwargs: Any,
    ) -> None:
        ax.plot(x, y, **kwargs)
        ax.grid(True)
        ax.set_xlabel(xlabel)
        ax.set_ylabel(ylabel)
        ax.set_title(title or "")

    def plot(
        self,
        bf: "SpectralFrame",
        ax: Optional["Axes"] = None,
        title: Optional[str] = None,
        overlay: bool = False,
        **kwargs: Any,
    ) -> Union["Axes", Iterator["Axes"]]:
        kwargs = kwargs or {}
        is_aw = kwargs.pop("Aw", False)
        if (
            len(bf.operation_history) > 0
            and bf.operation_history[-1]["operation"] == "coherence"
        ):
            unit = ""
            data = bf.magnitude
            ylabel = kwargs.pop("ylabel", "coherence")
        else:
            if is_aw:
                unit = "dBA"
                data = bf.dBA
            else:
                unit = "dB"
                data = bf.dB
            ylabel = kwargs.pop("ylabel", f"Spectrum level [{unit}]")

        data = _reshape_to_2d(data)

        xlabel = kwargs.pop("xlabel", "Frequency [Hz]")
        alpha = kwargs.pop("alpha", 1)
        plot_kwargs = filter_kwargs(Line2D, kwargs, strict_mode=True)
        ax_set = filter_kwargs(Axes.set, kwargs, strict_mode=True)
        num_channels = bf.n_channels
        if overlay:
            if ax is None:
                fig, ax = plt.subplots(1, 1, figsize=(6, 6))
            else:
                fig = ax.figure
            self.channel_plot(
                bf.freqs,
                data.T,
                ax,  # ここで必ずAxes型
                title=title or bf.label or "Spectral Data",
                ylabel=ylabel,
                xlabel=xlabel,
                alpha=alpha,
                **plot_kwargs,
            )
            ax.set(**ax_set)
            if fig is not None:
                fig.suptitle(title or bf.label or "Spectral Data")
            if ax.figure != fig:  # Only show if we created the figure
                plt.tight_layout()
                plt.show()
            return ax
        else:
            num_rows = int(np.ceil(np.sqrt(num_channels)))
            fig, axs = plt.subplots(
                num_rows,
                num_rows,
                figsize=(3 * num_rows, 3 * num_rows),
                sharex=True,
                sharey=True,
            )
            if isinstance(axs, np.ndarray):
                axes_list = axs.flatten().tolist()
            elif isinstance(axs, list):
                import itertools

                axes_list = list(itertools.chain.from_iterable(axs))
            else:
                axes_list = [axs]
            for ax_i, channel_data, ch_meta in zip(axes_list, data, bf.channels):
                self.channel_plot(
                    bf.freqs,
                    channel_data,
                    ax_i,
                    title=ch_meta.label,
                    ylabel=ylabel,
                    xlabel=xlabel,
                    alpha=alpha,
                    **plot_kwargs,
                )
                ax_i.set(**ax_set)
            fig.suptitle(title or bf.label or "Spectral Data")
            plt.tight_layout()
            plt.show()
            return _return_axes_iterator(fig.axes)

        raise NotImplementedError()
Attributes
name = 'matrix' class-attribute instance-attribute
Functions
channel_plot(x, y, ax, title=None, ylabel='', xlabel='Frequency [Hz]', alpha=0, **kwargs)
Source code in wandas/visualization/plotting.py
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
def channel_plot(
    self,
    x: Any,
    y: Any,
    ax: "Axes",
    title: Optional[str] = None,
    ylabel: str = "",
    xlabel: str = "Frequency [Hz]",
    alpha: float = 0,
    **kwargs: Any,
) -> None:
    ax.plot(x, y, **kwargs)
    ax.grid(True)
    ax.set_xlabel(xlabel)
    ax.set_ylabel(ylabel)
    ax.set_title(title or "")
plot(bf, ax=None, title=None, overlay=False, **kwargs)
Source code in wandas/visualization/plotting.py
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
def plot(
    self,
    bf: "SpectralFrame",
    ax: Optional["Axes"] = None,
    title: Optional[str] = None,
    overlay: bool = False,
    **kwargs: Any,
) -> Union["Axes", Iterator["Axes"]]:
    kwargs = kwargs or {}
    is_aw = kwargs.pop("Aw", False)
    if (
        len(bf.operation_history) > 0
        and bf.operation_history[-1]["operation"] == "coherence"
    ):
        unit = ""
        data = bf.magnitude
        ylabel = kwargs.pop("ylabel", "coherence")
    else:
        if is_aw:
            unit = "dBA"
            data = bf.dBA
        else:
            unit = "dB"
            data = bf.dB
        ylabel = kwargs.pop("ylabel", f"Spectrum level [{unit}]")

    data = _reshape_to_2d(data)

    xlabel = kwargs.pop("xlabel", "Frequency [Hz]")
    alpha = kwargs.pop("alpha", 1)
    plot_kwargs = filter_kwargs(Line2D, kwargs, strict_mode=True)
    ax_set = filter_kwargs(Axes.set, kwargs, strict_mode=True)
    num_channels = bf.n_channels
    if overlay:
        if ax is None:
            fig, ax = plt.subplots(1, 1, figsize=(6, 6))
        else:
            fig = ax.figure
        self.channel_plot(
            bf.freqs,
            data.T,
            ax,  # ここで必ずAxes型
            title=title or bf.label or "Spectral Data",
            ylabel=ylabel,
            xlabel=xlabel,
            alpha=alpha,
            **plot_kwargs,
        )
        ax.set(**ax_set)
        if fig is not None:
            fig.suptitle(title or bf.label or "Spectral Data")
        if ax.figure != fig:  # Only show if we created the figure
            plt.tight_layout()
            plt.show()
        return ax
    else:
        num_rows = int(np.ceil(np.sqrt(num_channels)))
        fig, axs = plt.subplots(
            num_rows,
            num_rows,
            figsize=(3 * num_rows, 3 * num_rows),
            sharex=True,
            sharey=True,
        )
        if isinstance(axs, np.ndarray):
            axes_list = axs.flatten().tolist()
        elif isinstance(axs, list):
            import itertools

            axes_list = list(itertools.chain.from_iterable(axs))
        else:
            axes_list = [axs]
        for ax_i, channel_data, ch_meta in zip(axes_list, data, bf.channels):
            self.channel_plot(
                bf.freqs,
                channel_data,
                ax_i,
                title=ch_meta.label,
                ylabel=ylabel,
                xlabel=xlabel,
                alpha=alpha,
                **plot_kwargs,
            )
            ax_i.set(**ax_set)
        fig.suptitle(title or bf.label or "Spectral Data")
        plt.tight_layout()
        plt.show()
        return _return_axes_iterator(fig.axes)

    raise NotImplementedError()
Functions
register_plot_strategy(strategy_cls)

Register a new plot strategy from a class

Source code in wandas/visualization/plotting.py
764
765
766
767
768
769
770
def register_plot_strategy(strategy_cls: type) -> None:
    """Register a new plot strategy from a class"""
    if not issubclass(strategy_cls, PlotStrategy):
        raise TypeError("Strategy class must inherit from PlotStrategy.")
    if inspect.isabstract(strategy_cls):
        raise TypeError("Cannot register abstract PlotStrategy class.")
    _plot_strategies[strategy_cls.name] = strategy_cls
get_plot_strategy(name)

Get plot strategy by name

Source code in wandas/visualization/plotting.py
779
780
781
782
783
def get_plot_strategy(name: str) -> type[PlotStrategy[Any]]:
    """Get plot strategy by name"""
    if name not in _plot_strategies:
        raise ValueError(f"Unknown plot type: {name}")
    return _plot_strategies[name]
create_operation(name, **params)

Create operation instance from operation name and parameters

Source code in wandas/visualization/plotting.py
786
787
788
789
def create_operation(name: str, **params: Any) -> PlotStrategy[Any]:
    """Create operation instance from operation name and parameters"""
    operation_class = get_plot_strategy(name)
    return operation_class(**params)

types

Type definitions for visualization parameters.

Classes
WaveformConfig

Bases: TypedDict

Configuration for waveform plot in describe view.

This corresponds to the time-domain plot shown at the top of the describe view.

Source code in wandas/visualization/types.py
 6
 7
 8
 9
10
11
12
13
14
15
16
class WaveformConfig(TypedDict, total=False):
    """Configuration for waveform plot in describe view.

    This corresponds to the time-domain plot shown at the top of the
    describe view.
    """

    xlabel: str
    ylabel: str
    xlim: tuple[float, float]
    ylim: tuple[float, float]
Attributes
xlabel instance-attribute
ylabel instance-attribute
xlim instance-attribute
ylim instance-attribute
SpectralConfig

Bases: TypedDict

Configuration for spectral plot in describe view.

This corresponds to the frequency-domain plot (Welch) shown on the right side.

Source code in wandas/visualization/types.py
19
20
21
22
23
24
25
26
27
28
29
class SpectralConfig(TypedDict, total=False):
    """Configuration for spectral plot in describe view.

    This corresponds to the frequency-domain plot (Welch) shown on the
    right side.
    """

    xlabel: str
    ylabel: str
    xlim: tuple[float, float]
    ylim: tuple[float, float]
Attributes
xlabel instance-attribute
ylabel instance-attribute
xlim instance-attribute
ylim instance-attribute
DescribeParams

Bases: TypedDict

Parameters for the describe visualization method.

This visualization creates a comprehensive view with three plots: 1. Time-domain waveform (top) 2. Spectrogram (bottom-left) 3. Frequency spectrum via Welch method (bottom-right)

Attributes:

Name Type Description
fmin float

Minimum frequency to display in the spectrogram (Hz). Default: 0

fmax Optional[float]

Maximum frequency to display in the spectrogram (Hz). Default: Nyquist frequency

cmap str

Colormap for the spectrogram. Default: 'jet'

vmin Optional[float]

Minimum value for spectrogram color scale (dB). Auto-calculated if None.

vmax Optional[float]

Maximum value for spectrogram color scale (dB). Auto-calculated if None.

xlim Optional[tuple[float, float]]

Time axis limits (seconds) for all time-based plots.

ylim Optional[tuple[float, float]]

Frequency axis limits (Hz) for frequency-based plots.

Aw bool

Apply A-weighting to the frequency analysis. Default: False

waveform WaveformConfig

Additional configuration dict for waveform subplot.

spectral SpectralConfig

Additional configuration dict for spectral subplot.

normalize bool

Normalize audio data for playback. Default: True

is_close bool

Close the figure after displaying. Default: True

Deprecated (for backward compatibility): axis_config: Old configuration format. Use specific parameters instead. cbar_config: Old colorbar configuration. Use vmin/vmax instead.

Examples:

>>> cf = ChannelFrame.read_wav("audio.wav")
>>> # Basic usage
>>> cf.describe()
>>>
>>> # Custom frequency range
>>> cf.describe(fmin=100, fmax=5000)
>>>
>>> # Custom color scale
>>> cf.describe(vmin=-80, vmax=-20, cmap="viridis")
>>>
>>> # A-weighted analysis
>>> cf.describe(Aw=True)
>>>
>>> # Custom time range
>>> cf.describe(xlim=(0, 5))  # Show first 5 seconds
Source code in wandas/visualization/types.py
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
class DescribeParams(TypedDict, total=False):
    """Parameters for the describe visualization method.

    This visualization creates a comprehensive view with three plots:
    1. Time-domain waveform (top)
    2. Spectrogram (bottom-left)
    3. Frequency spectrum via Welch method (bottom-right)

    Attributes:
        fmin: Minimum frequency to display in the spectrogram (Hz).
            Default: 0
        fmax: Maximum frequency to display in the spectrogram (Hz).
            Default: Nyquist frequency
        cmap: Colormap for the spectrogram. Default: 'jet'
        vmin: Minimum value for spectrogram color scale (dB).
            Auto-calculated if None.
        vmax: Maximum value for spectrogram color scale (dB).
            Auto-calculated if None.
        xlim: Time axis limits (seconds) for all time-based plots.
        ylim: Frequency axis limits (Hz) for frequency-based plots.
        Aw: Apply A-weighting to the frequency analysis. Default: False
        waveform: Additional configuration dict for waveform subplot.
        spectral: Additional configuration dict for spectral subplot.
        normalize: Normalize audio data for playback. Default: True
        is_close: Close the figure after displaying. Default: True

    Deprecated (for backward compatibility):
        axis_config: Old configuration format.
            Use specific parameters instead.
        cbar_config: Old colorbar configuration. Use vmin/vmax instead.

    Examples:
        >>> cf = ChannelFrame.read_wav("audio.wav")
        >>> # Basic usage
        >>> cf.describe()
        >>>
        >>> # Custom frequency range
        >>> cf.describe(fmin=100, fmax=5000)
        >>>
        >>> # Custom color scale
        >>> cf.describe(vmin=-80, vmax=-20, cmap="viridis")
        >>>
        >>> # A-weighted analysis
        >>> cf.describe(Aw=True)
        >>>
        >>> # Custom time range
        >>> cf.describe(xlim=(0, 5))  # Show first 5 seconds
    """

    # Spectrogram parameters
    fmin: float
    fmax: Optional[float]
    cmap: str
    vmin: Optional[float]
    vmax: Optional[float]

    # Axis limits
    xlim: Optional[tuple[float, float]]
    ylim: Optional[tuple[float, float]]

    # Weighting
    Aw: bool

    # Subplot configurations
    waveform: WaveformConfig
    spectral: SpectralConfig

    # Display options
    normalize: bool
    is_close: bool

    # Deprecated (backward compatibility)
    axis_config: dict[str, Any]
    cbar_config: dict[str, Any]
Attributes
fmin instance-attribute
fmax instance-attribute
cmap instance-attribute
vmin instance-attribute
vmax instance-attribute
xlim instance-attribute
ylim instance-attribute
Aw instance-attribute
waveform instance-attribute
spectral instance-attribute
normalize instance-attribute
is_close instance-attribute
axis_config instance-attribute
cbar_config instance-attribute

データセットモジュール

データセットモジュールはサンプルデータとデータセット機能を提供します。

wandas.datasets

Modules

sample_data

Attributes
Functions
load_sample_signal(frequency=5.0, sampling_rate=100, duration=1.0)

Generate a sample sine wave signal.

Parameters

frequency : float, default=5.0 Frequency of the signal in Hz. sampling_rate : int, default=100 Sampling rate in Hz. duration : float, default=1.0 Duration of the signal in seconds.

Returns

NDArrayReal Signal data as a NumPy array.

Source code in wandas/datasets/sample_data.py
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def load_sample_signal(
    frequency: float = 5.0, sampling_rate: int = 100, duration: float = 1.0
) -> NDArrayReal:
    """
    Generate a sample sine wave signal.

    Parameters
    ----------
    frequency : float, default=5.0
        Frequency of the signal in Hz.
    sampling_rate : int, default=100
        Sampling rate in Hz.
    duration : float, default=1.0
        Duration of the signal in seconds.

    Returns
    -------
    NDArrayReal
        Signal data as a NumPy array.
    """
    num_samples = int(sampling_rate * duration)
    t = np.arange(num_samples) / sampling_rate
    signal: NDArrayReal = np.sin(2 * np.pi * frequency * t, dtype=np.float64)
    return signal