modusa 0.1.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- modusa/.DS_Store +0 -0
- modusa/decorators.py +5 -5
- modusa/devtools/generate_template.py +80 -15
- modusa/devtools/main.py +6 -4
- modusa/devtools/templates/{engines.py → engine.py} +8 -7
- modusa/devtools/templates/{generators.py → generator.py} +8 -10
- modusa/devtools/templates/io.py +24 -0
- modusa/devtools/templates/{plugins.py → plugin.py} +7 -6
- modusa/devtools/templates/signal.py +40 -0
- modusa/devtools/templates/test.py +11 -0
- modusa/engines/.DS_Store +0 -0
- modusa/engines/__init__.py +1 -2
- modusa/generators/__init__.py +3 -1
- modusa/generators/audio_waveforms.py +227 -0
- modusa/generators/base.py +14 -25
- modusa/io/__init__.py +9 -0
- modusa/io/audio_converter.py +76 -0
- modusa/io/audio_loader.py +212 -0
- modusa/io/audio_player.py +72 -0
- modusa/io/base.py +43 -0
- modusa/io/plotter.py +430 -0
- modusa/io/youtube_downloader.py +139 -0
- modusa/main.py +15 -17
- modusa/plugins/__init__.py +1 -7
- modusa/signals/__init__.py +4 -6
- modusa/signals/audio_signal.py +421 -175
- modusa/signals/base.py +11 -271
- modusa/signals/frequency_domain_signal.py +329 -0
- modusa/signals/signal_ops.py +158 -0
- modusa/signals/spectrogram.py +465 -0
- modusa/signals/time_domain_signal.py +309 -0
- modusa/utils/excp.py +5 -0
- {modusa-0.1.0.dist-info → modusa-0.2.1.dist-info}/METADATA +16 -11
- modusa-0.2.1.dist-info/RECORD +47 -0
- modusa/devtools/templates/signals.py +0 -63
- modusa/engines/plot_1dsignal.py +0 -130
- modusa/engines/plot_2dmatrix.py +0 -159
- modusa/generators/basic_waveform.py +0 -185
- modusa/plugins/plot_1dsignal.py +0 -59
- modusa/plugins/plot_2dmatrix.py +0 -76
- modusa/plugins/plot_time_domain_signal.py +0 -59
- modusa/signals/signal1d.py +0 -311
- modusa/signals/signal2d.py +0 -226
- modusa/signals/uniform_time_domain_signal.py +0 -212
- modusa-0.1.0.dist-info/RECORD +0 -41
- {modusa-0.1.0.dist-info → modusa-0.2.1.dist-info}/WHEEL +0 -0
- {modusa-0.1.0.dist-info → modusa-0.2.1.dist-info}/entry_points.txt +0 -0
- {modusa-0.1.0.dist-info → modusa-0.2.1.dist-info}/licenses/LICENSE.md +0 -0
modusa/signals/audio_signal.py
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
from modusa import excp
|
|
5
5
|
from modusa.decorators import immutable_property, validate_args_type
|
|
6
6
|
from modusa.signals.base import ModusaSignal
|
|
7
|
+
from modusa.signals.signal_ops import SignalOps
|
|
7
8
|
from typing import Self, Any
|
|
8
9
|
import numpy as np
|
|
9
10
|
import matplotlib.pyplot as plt
|
|
@@ -11,220 +12,465 @@ from pathlib import Path
|
|
|
11
12
|
|
|
12
13
|
class AudioSignal(ModusaSignal):
|
|
13
14
|
"""
|
|
15
|
+
Represents a 1D audio signal within modusa framework.
|
|
14
16
|
|
|
17
|
+
Note
|
|
18
|
+
----
|
|
19
|
+
- It is highly recommended to use :class:`~modusa.io.AudioLoader` to instantiate an object of this class.
|
|
20
|
+
- This class assumes audio is mono (1D numpy array).
|
|
21
|
+
- Either `sr` (sampling rate) or `t` (time axis) must be provided.
|
|
22
|
+
- If both `t` and `sr` are given, `t` takes precedence for timing and `sr` is computed from that.
|
|
23
|
+
- If `t` is provided but `sr` is missing, `sr` is estimated from the `t`.
|
|
24
|
+
- If `t` is provided, the starting time `t0` will be overridden by `t[0]`.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
y : np.ndarray
|
|
29
|
+
1D numpy array representing the audio signal.
|
|
30
|
+
sr : int | None
|
|
31
|
+
Sampling rate in Hz. Required if `t` is not provided.
|
|
32
|
+
t : np.ndarray | None
|
|
33
|
+
Optional time axis corresponding to `y`. Must be the same length as `y`.
|
|
34
|
+
t0 : float, optional
|
|
35
|
+
Starting time in seconds. Defaults to 0.0. Set to `t[0]` if `t` is provided.
|
|
36
|
+
title : str | None, optional
|
|
37
|
+
Optional title for the signal. Defaults to `"Audio Signal"`.
|
|
15
38
|
"""
|
|
16
39
|
|
|
17
40
|
#--------Meta Information----------
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
41
|
+
_name = "Audio Signal"
|
|
42
|
+
_description = ""
|
|
43
|
+
_author_name = "Ankit Anand"
|
|
44
|
+
_author_email = "ankit0.anand0@gmail.com"
|
|
45
|
+
_created_at = "2025-07-04"
|
|
23
46
|
#----------------------------------
|
|
24
47
|
|
|
25
48
|
@validate_args_type()
|
|
26
|
-
def __init__(self, y: np.ndarray, t: np.ndarray | None = None):
|
|
27
|
-
|
|
28
|
-
if y.ndim != 1: # Mono
|
|
29
|
-
raise excp.InputValueError(f"`y` must have 1 dimension not {y.ndim}.")
|
|
30
|
-
if t.ndim != 1:
|
|
31
|
-
raise excp.InputValueError(f"`t` must have 1 dimension not {t.ndim}.")
|
|
32
|
-
|
|
33
|
-
if t is None:
|
|
34
|
-
t = np.arange(y.shape[0])
|
|
35
|
-
else:
|
|
36
|
-
if t.shape[0] != y.shape[0]:
|
|
37
|
-
raise excp.InputValueError(f"`y` and `t` must have same shape.")
|
|
38
|
-
dts = np.diff(t)
|
|
39
|
-
if not np.allclose(dts, dts[0]):
|
|
40
|
-
raise excp.InputValueError("`t` must be equally spaced")
|
|
41
|
-
|
|
42
|
-
super().__init__(data=y, data_idx=t) # Instantiating `ModusaSignal` class
|
|
43
|
-
|
|
44
|
-
self._y_unit = ""
|
|
45
|
-
self._t_unit = "sec"
|
|
46
|
-
|
|
47
|
-
self._title = "Audio Signal"
|
|
48
|
-
self._y_label = "Amplitude"
|
|
49
|
-
self._t_label = "Time"
|
|
50
|
-
|
|
51
|
-
def _with_data(self, new_data: np.ndarray, new_data_idx: np.ndarray) -> Self:
|
|
52
|
-
"""Subclasses must override this to return a copy with new data."""
|
|
53
|
-
new_signal = self.__class__(y=new_data, t=new_data_idx)
|
|
54
|
-
new_signal.set_units(y_unit=self.y_unit, t_unit=self.t_unit)
|
|
55
|
-
new_signal.set_plot_labels(title=self.title, y_label=self.y_label, t_label=self.t_label)
|
|
56
|
-
|
|
57
|
-
return new_signal
|
|
58
|
-
|
|
59
|
-
#----------------------
|
|
60
|
-
# From methods
|
|
61
|
-
#----------------------
|
|
62
|
-
@classmethod
|
|
63
|
-
def from_array(cls, y: np.ndarray, t: np.ndarray | None = None) -> Self:
|
|
64
|
-
|
|
65
|
-
signal = cls(y=y, t=t)
|
|
66
|
-
|
|
67
|
-
return signal
|
|
68
|
-
|
|
69
|
-
@classmethod
|
|
70
|
-
def from_array_with_sr(cls, y: np.ndarray, sr: int) -> Self:
|
|
71
|
-
t = np.arange(y.shape[0]) * (1.0 / sr)
|
|
72
|
-
|
|
73
|
-
signal = cls(y=y, t=t)
|
|
74
|
-
|
|
75
|
-
return signal
|
|
76
|
-
|
|
77
|
-
@classmethod
|
|
78
|
-
def from_list(cls, y: list, t: list) -> Self:
|
|
79
|
-
|
|
80
|
-
y = np.array(y)
|
|
81
|
-
t = np.array(t)
|
|
82
|
-
signal = cls(y=y, t=t)
|
|
83
|
-
|
|
84
|
-
return signal
|
|
85
|
-
|
|
86
|
-
@classmethod
|
|
87
|
-
def from_file(cls, fp: str | Path, sr: int | None = None) -> Self:
|
|
88
|
-
|
|
89
|
-
import librosa
|
|
90
|
-
|
|
91
|
-
fp = Path(fp)
|
|
92
|
-
y, sr = librosa.load(fp, sr=sr)
|
|
93
|
-
t = np.arange(y.shape[0]) * (1.0 / sr)
|
|
94
|
-
|
|
95
|
-
signal = cls(y=y, t=t)
|
|
96
|
-
signal.set_plot_labels(title=fp.stem)
|
|
97
|
-
|
|
98
|
-
return signal
|
|
49
|
+
def __init__(self, y: np.ndarray, sr: int | None = None, t: np.ndarray | None = None, t0: float = 0.0, title: str | None = None):
|
|
99
50
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
self._y_label = y_label
|
|
121
|
-
if t_label is not None:
|
|
122
|
-
self._t_label = t_label
|
|
123
|
-
|
|
124
|
-
return self
|
|
125
|
-
|
|
126
|
-
|
|
51
|
+
if y.ndim != 1:
|
|
52
|
+
raise excp.InputValueError(f"`y` must have 1 dimension, not {y.ndim}.")
|
|
53
|
+
|
|
54
|
+
if t is not None:
|
|
55
|
+
if len(t) != len(y):
|
|
56
|
+
raise excp.InputValueError("Length of `t` must match `y`.")
|
|
57
|
+
if sr is None:
|
|
58
|
+
# Estimate sr from t if not provided
|
|
59
|
+
dt = t[1] - t[0]
|
|
60
|
+
sr = round(1.0 / dt) # Round to avoid floating-point drift
|
|
61
|
+
t0 = float(t[0]) # Override t0 from first timestamp
|
|
62
|
+
|
|
63
|
+
elif sr is None:
|
|
64
|
+
raise excp.InputValueError("Either `sr` or `t` must be provided.")
|
|
65
|
+
|
|
66
|
+
self._y = y
|
|
67
|
+
self._sr = sr
|
|
68
|
+
self._t0 = t0
|
|
69
|
+
self.title = title or self._name
|
|
70
|
+
|
|
127
71
|
#----------------------
|
|
128
72
|
# Properties
|
|
129
73
|
#----------------------
|
|
130
|
-
|
|
131
74
|
@immutable_property("Create a new object instead.")
|
|
132
75
|
def y(self) -> np.ndarray:
|
|
133
|
-
""""""
|
|
134
|
-
return self.
|
|
76
|
+
"""Audio data."""
|
|
77
|
+
return self._y
|
|
135
78
|
|
|
136
79
|
@immutable_property("Create a new object instead.")
|
|
137
|
-
def
|
|
138
|
-
""""""
|
|
139
|
-
return self.
|
|
80
|
+
def sr(self) -> np.ndarray:
|
|
81
|
+
"""Sampling rate of the audio."""
|
|
82
|
+
return self._sr
|
|
140
83
|
|
|
141
84
|
@immutable_property("Create a new object instead.")
|
|
142
|
-
def
|
|
143
|
-
""""""
|
|
144
|
-
return
|
|
145
|
-
|
|
146
|
-
@immutable_property("
|
|
147
|
-
def
|
|
148
|
-
""""""
|
|
149
|
-
return self.
|
|
150
|
-
|
|
151
|
-
@immutable_property("Use `set_units` instead.")
|
|
152
|
-
def t_unit(self) -> str:
|
|
153
|
-
""""""
|
|
154
|
-
return self._t_unit
|
|
155
|
-
|
|
156
|
-
@immutable_property("Use `.set_plot_labels` instead.")
|
|
157
|
-
def title(self) -> str:
|
|
158
|
-
""""""
|
|
159
|
-
return self._title
|
|
160
|
-
|
|
161
|
-
@immutable_property("Use `.set_plot_labels` instead.")
|
|
162
|
-
def y_label(self) -> str:
|
|
163
|
-
""""""
|
|
164
|
-
return self._y_label
|
|
165
|
-
|
|
166
|
-
@immutable_property("Use `.set_plot_labels` instead.")
|
|
167
|
-
def t_label(self) -> str:
|
|
168
|
-
""""""
|
|
169
|
-
return self._t_label
|
|
85
|
+
def t0(self) -> np.ndarray:
|
|
86
|
+
"""Start timestamp of the audio."""
|
|
87
|
+
return self._t0
|
|
88
|
+
|
|
89
|
+
@immutable_property("Create a new object instead.")
|
|
90
|
+
def t(self) -> np.ndarray:
|
|
91
|
+
"""Timestamp array of the audio."""
|
|
92
|
+
return self.t0 + np.arange(len(self.y)) / self.sr
|
|
170
93
|
|
|
171
94
|
@immutable_property("Mutation not allowed.")
|
|
172
95
|
def Ts(self) -> int:
|
|
173
|
-
""""""
|
|
174
|
-
return
|
|
175
|
-
|
|
96
|
+
"""Sampling Period of the audio."""
|
|
97
|
+
return 1.0 / self.sr
|
|
98
|
+
|
|
176
99
|
@immutable_property("Mutation not allowed.")
|
|
177
100
|
def duration(self) -> int:
|
|
178
|
-
""""""
|
|
179
|
-
return self.
|
|
180
|
-
|
|
181
|
-
@immutable_property("Use `.set_labels` instead.")
|
|
182
|
-
def labels(self) -> tuple[str, str, str]:
|
|
183
|
-
"""Labels in a tuple format appropriate for the plots."""
|
|
184
|
-
return (self.title, f"{self.y_label} ({self.y_unit})", f"{self.t_label} ({self.t_unit})")
|
|
101
|
+
"""Duration of the audio."""
|
|
102
|
+
return len(self.y) / self.sr
|
|
185
103
|
|
|
104
|
+
@immutable_property("Mutation not allowed.")
|
|
105
|
+
def info(self) -> None:
|
|
106
|
+
"""Prints info about the audio."""
|
|
107
|
+
print("-" * 50)
|
|
108
|
+
print(f"{'Title':<20}: {self.title}")
|
|
109
|
+
print(f"{'Kind':<20}: {self._name}")
|
|
110
|
+
print(f"{'Duration':<20}: {self.duration:.2f} sec")
|
|
111
|
+
print(f"{'Sampling Rate':<20}: {self.sr} Hz")
|
|
112
|
+
print(f"{'Sampling Period':<20}: {(self.Ts*1000) :.4f} ms")
|
|
113
|
+
print("-" * 50)
|
|
186
114
|
|
|
187
115
|
#----------------------
|
|
188
|
-
#
|
|
116
|
+
# Methods
|
|
189
117
|
#----------------------
|
|
118
|
+
def __getitem__(self, key):
|
|
119
|
+
if isinstance(key, (int, slice)):
|
|
120
|
+
# Basic slicing of 1D signals
|
|
121
|
+
sliced_data = self._data[key]
|
|
122
|
+
sliced_axis = self._axes[0][key] # assumes only 1 axis
|
|
123
|
+
|
|
124
|
+
return self.replace(data=sliced_data, axes=(sliced_axis, ))
|
|
125
|
+
else:
|
|
126
|
+
raise TypeError(
|
|
127
|
+
f"Indexing with type {type(key)} is not supported. Use int or slice."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
@validate_args_type()
|
|
131
|
+
def crop(self, t_min: int | float | None = None, t_max: int | float | None = None) -> "AudioSignal":
|
|
132
|
+
"""
|
|
133
|
+
Crop the audio signal to a time range [t_min, t_max].
|
|
134
|
+
|
|
135
|
+
.. code-block:: python
|
|
136
|
+
|
|
137
|
+
from modusa.generators import AudioSignalGenerator
|
|
138
|
+
audio_example = AudioSignalGenerator.generate_example()
|
|
139
|
+
cropped_audio = audio_example.crop(1.5, 2)
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
t_min : float or None
|
|
144
|
+
Inclusive lower time bound. If None, no lower bound.
|
|
145
|
+
t_max : float or None
|
|
146
|
+
Exclusive upper time bound. If None, no upper bound.
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
AudioSignal
|
|
151
|
+
Cropped audio signal.
|
|
152
|
+
"""
|
|
153
|
+
y = self.y
|
|
154
|
+
t = self.t
|
|
155
|
+
|
|
156
|
+
mask = np.ones_like(t, dtype=bool)
|
|
157
|
+
if t_min is not None:
|
|
158
|
+
mask &= (t >= t_min)
|
|
159
|
+
if t_max is not None:
|
|
160
|
+
mask &= (t < t_max)
|
|
161
|
+
|
|
162
|
+
cropped_y = y[mask]
|
|
163
|
+
new_t0 = t[mask][0] if np.any(mask) else self.t0 # fallback to original t0 if mask is empty
|
|
164
|
+
|
|
165
|
+
return self.__class__(y=cropped_y, sr=self.sr, t0=new_t0, title=self.title)
|
|
166
|
+
|
|
167
|
+
|
|
190
168
|
@validate_args_type()
|
|
191
169
|
def plot(
|
|
192
170
|
self,
|
|
193
171
|
scale_y: tuple[float, float] | None = None,
|
|
194
|
-
scale_t: tuple[float, float] | None = None,
|
|
195
172
|
ax: plt.Axes | None = None,
|
|
196
173
|
color: str = "b",
|
|
197
174
|
marker: str | None = None,
|
|
198
175
|
linestyle: str | None = None,
|
|
199
|
-
stem: bool | None =
|
|
200
|
-
labels: tuple[str, str, str] | None = None,
|
|
176
|
+
stem: bool | None = False,
|
|
201
177
|
legend_loc: str | None = None,
|
|
202
|
-
|
|
178
|
+
title: str | None = None,
|
|
179
|
+
ylabel: str | None = "Amplitude",
|
|
180
|
+
xlabel: str | None = "Time (sec)",
|
|
181
|
+
ylim: tuple[float, float] | None = None,
|
|
182
|
+
xlim: tuple[float, float] | None = None,
|
|
203
183
|
highlight: list[tuple[float, float]] | None = None,
|
|
204
184
|
) -> plt.Figure:
|
|
205
185
|
"""
|
|
206
|
-
|
|
186
|
+
Plot the audio waveform using matplotlib.
|
|
187
|
+
|
|
188
|
+
.. code-block:: python
|
|
189
|
+
|
|
190
|
+
from modusa.generators import AudioSignalGenerator
|
|
191
|
+
audio_example = AudioSignalGenerator.generate_example()
|
|
192
|
+
audio_example.plot(color="orange", title="Example Audio")
|
|
193
|
+
|
|
194
|
+
Parameters
|
|
195
|
+
----------
|
|
196
|
+
scale_y : tuple of float, optional
|
|
197
|
+
Range to scale the y-axis data before plotting. Useful for normalization.
|
|
198
|
+
ax : matplotlib.axes.Axes, optional
|
|
199
|
+
Pre-existing axes to plot into. If None, a new figure and axes are created.
|
|
200
|
+
color : str, optional
|
|
201
|
+
Color of the waveform line. Default is `"b"` (blue).
|
|
202
|
+
marker : str or None, optional
|
|
203
|
+
Marker style for each point. Follows matplotlib marker syntax.
|
|
204
|
+
linestyle : str or None, optional
|
|
205
|
+
Line style for the waveform. Follows matplotlib linestyle syntax.
|
|
206
|
+
stem : bool, optional
|
|
207
|
+
If True, use a stem plot instead of a continuous line.
|
|
208
|
+
legend_loc : str or None, optional
|
|
209
|
+
If provided, adds a legend at the specified location (e.g., "upper right").
|
|
210
|
+
title : str or None, optional
|
|
211
|
+
Plot title. Defaults to the signal’s title.
|
|
212
|
+
ylabel : str or None, optional
|
|
213
|
+
Label for the y-axis. Defaults to `"Amplitude"`.
|
|
214
|
+
xlabel : str or None, optional
|
|
215
|
+
Label for the x-axis. Defaults to `"Time (sec)"`.
|
|
216
|
+
ylim : tuple of float or None, optional
|
|
217
|
+
Limits for the y-axis.
|
|
218
|
+
xlim : tuple of float or None, optional
|
|
219
|
+
Limits for the x-axis.
|
|
220
|
+
highlight : list of tuple of float or None, optional
|
|
221
|
+
List of time intervals to highlight on the plot, each as (start, end).
|
|
222
|
+
|
|
223
|
+
Returns
|
|
224
|
+
-------
|
|
225
|
+
matplotlib.figure.Figure
|
|
226
|
+
The figure object containing the plot.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
from modusa.io import Plotter
|
|
230
|
+
|
|
231
|
+
title = title or self.title
|
|
232
|
+
|
|
233
|
+
fig: plt.Figure | None = Plotter.plot_signal(y=self.y, x=self.t, scale_y=scale_y, ax=ax, color=color, marker=marker, linestyle=linestyle, stem=stem, legend_loc=legend_loc, title=title, ylabel=ylabel, xlabel=xlabel, ylim=ylim, xlim=xlim, highlight=highlight)
|
|
234
|
+
|
|
235
|
+
return fig
|
|
236
|
+
|
|
237
|
+
def play(self, regions: list[tuple[float, float], ...] | None = None, title: str | None = None):
|
|
238
|
+
"""
|
|
239
|
+
Play the audio signal inside a Jupyter Notebook.
|
|
240
|
+
|
|
241
|
+
.. code-block:: python
|
|
242
|
+
|
|
243
|
+
from modusa.generators import AudioSignalGenerator
|
|
244
|
+
audio = AudioSignalGenerator.generate_example()
|
|
245
|
+
audio.play(regions=[(0.0, 1.0), (2.0, 3.0)])
|
|
246
|
+
|
|
247
|
+
Parameters
|
|
248
|
+
----------
|
|
249
|
+
regions : list of tuple of float, optional
|
|
250
|
+
List of (start_time, end_time) pairs in seconds specifying the regions to play.
|
|
251
|
+
If None, the entire signal is played.
|
|
252
|
+
title : str or None, optional
|
|
253
|
+
Optional title for the player interface. Defaults to the signal’s internal title.
|
|
254
|
+
|
|
255
|
+
Returns
|
|
256
|
+
-------
|
|
257
|
+
IPython.display.Audio
|
|
258
|
+
An interactive audio player widget for Jupyter environments.
|
|
259
|
+
|
|
260
|
+
Note
|
|
261
|
+
----
|
|
262
|
+
- This method uses :class:`~modusa.io.AudioPlayer` to render an interactive audio player.
|
|
263
|
+
- Optionally, specific regions of the signal can be played back, each defined by a (start, end) time pair.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
from modusa.io import AudioPlayer
|
|
267
|
+
audio_player = AudioPlayer.play(y=self.y, sr=self.sr, regions=regions, title=self.title)
|
|
268
|
+
|
|
269
|
+
return audio_player
|
|
270
|
+
|
|
271
|
+
def to_spectrogram(
|
|
272
|
+
self,
|
|
273
|
+
n_fft: int = 2048,
|
|
274
|
+
hop_length: int = 512,
|
|
275
|
+
win_length: int | None = None,
|
|
276
|
+
window: str = "hann"
|
|
277
|
+
) -> "Spectrogram":
|
|
207
278
|
"""
|
|
279
|
+
Compute the Short-Time Fourier Transform (STFT) and return a Spectrogram object.
|
|
280
|
+
|
|
281
|
+
Parameters
|
|
282
|
+
----------
|
|
283
|
+
n_fft : int
|
|
284
|
+
FFT size.
|
|
285
|
+
win_length : int or None
|
|
286
|
+
Window length. Defaults to `n_fft` if None.
|
|
287
|
+
hop_length : int
|
|
288
|
+
Hop length between frames.
|
|
289
|
+
window : str
|
|
290
|
+
Type of window function to use (e.g., 'hann', 'hamming').
|
|
291
|
+
|
|
292
|
+
Returns
|
|
293
|
+
-------
|
|
294
|
+
Spectrogram
|
|
295
|
+
Spectrogram object containing S (complex STFT), t (time bins), and f (frequency bins).
|
|
296
|
+
"""
|
|
297
|
+
from modusa.signals.spectrogram import Spectrogram
|
|
298
|
+
import librosa
|
|
208
299
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
300
|
+
S = librosa.stft(self.y, n_fft=n_fft, win_length=win_length, hop_length=hop_length, window=window)
|
|
301
|
+
f = librosa.fft_frequencies(sr=self.sr, n_fft=n_fft)
|
|
302
|
+
t = librosa.frames_to_time(np.arange(S.shape[1]), sr=self.sr, hop_length=hop_length)
|
|
303
|
+
t += self.t0
|
|
304
|
+
spec = Spectrogram(S=S, f=f, t=t)
|
|
305
|
+
if self.title != self._name: # Means title of the audio was reset so we pass that info to spec
|
|
306
|
+
spec.title = self.title
|
|
307
|
+
|
|
308
|
+
return spec
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
#----------------------------
|
|
312
|
+
# Math ops
|
|
313
|
+
#----------------------------
|
|
314
|
+
|
|
315
|
+
def __array__(self, dtype=None):
|
|
316
|
+
return np.asarray(self._S, dtype=dtype)
|
|
317
|
+
|
|
318
|
+
def __add__(self, other):
|
|
319
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
320
|
+
result = np.add(self.y, other_data)
|
|
321
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
322
|
+
|
|
323
|
+
def __radd__(self, other):
|
|
324
|
+
result = np.add(other, self.y)
|
|
325
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
326
|
+
|
|
327
|
+
def __sub__(self, other):
|
|
328
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
329
|
+
result = np.subtract(self.y, other_data)
|
|
330
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
331
|
+
|
|
332
|
+
def __rsub__(self, other):
|
|
333
|
+
result = np.subtract(other, self.y)
|
|
334
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
335
|
+
|
|
336
|
+
def __mul__(self, other):
|
|
337
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
338
|
+
result = np.multiply(self.y, other_data)
|
|
339
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
340
|
+
|
|
341
|
+
def __rmul__(self, other):
|
|
342
|
+
result = np.multiply(other, self.y)
|
|
343
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
344
|
+
|
|
345
|
+
def __truediv__(self, other):
|
|
346
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
347
|
+
result = np.true_divide(self.y, other_data)
|
|
348
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
349
|
+
|
|
350
|
+
def __rtruediv__(self, other):
|
|
351
|
+
result = np.true_divide(other, self.y)
|
|
352
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
353
|
+
|
|
354
|
+
def __floordiv__(self, other):
|
|
355
|
+
other_data = other._y if isinstance(other, self.__class__) else other
|
|
356
|
+
result = np.floor_divide(self.y, other_data)
|
|
357
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
358
|
+
|
|
359
|
+
def __rfloordiv__(self, other):
|
|
360
|
+
result = np.floor_divide(other, self.y)
|
|
361
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
362
|
+
|
|
363
|
+
def __pow__(self, other):
|
|
364
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
365
|
+
result = np.power(self.y, other_data)
|
|
366
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
367
|
+
|
|
368
|
+
def __rpow__(self, other):
|
|
369
|
+
result = np.power(other, self.y)
|
|
370
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
371
|
+
|
|
372
|
+
def __abs__(self):
|
|
373
|
+
result = np.abs(self.y)
|
|
374
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
#--------------------------
|
|
378
|
+
# Other signal ops
|
|
379
|
+
#--------------------------
|
|
380
|
+
def sin(self) -> Self:
|
|
381
|
+
"""Compute the element-wise sine of the signal data."""
|
|
382
|
+
result = np.sin(self.y)
|
|
383
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
384
|
+
|
|
385
|
+
def cos(self) -> Self:
|
|
386
|
+
"""Compute the element-wise cosine of the signal data."""
|
|
387
|
+
result = np.cos(self.y)
|
|
388
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
389
|
+
|
|
390
|
+
def exp(self) -> Self:
|
|
391
|
+
"""Compute the element-wise exponential of the signal data."""
|
|
392
|
+
result = np.exp(self.y)
|
|
393
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
394
|
+
|
|
395
|
+
def tanh(self) -> Self:
|
|
396
|
+
"""Compute the element-wise hyperbolic tangent of the signal data."""
|
|
397
|
+
result = np.tanh(self.y)
|
|
398
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
399
|
+
|
|
400
|
+
def log(self) -> Self:
|
|
401
|
+
"""Compute the element-wise natural logarithm of the signal data."""
|
|
402
|
+
result = np.log(self.y)
|
|
403
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
404
|
+
|
|
405
|
+
def log1p(self) -> Self:
|
|
406
|
+
"""Compute the element-wise natural logarithm of (1 + signal data)."""
|
|
407
|
+
result = np.log1p(self.y)
|
|
408
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
409
|
+
|
|
410
|
+
def log10(self) -> Self:
|
|
411
|
+
"""Compute the element-wise base-10 logarithm of the signal data."""
|
|
412
|
+
result = np.log10(self.y)
|
|
413
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
414
|
+
|
|
415
|
+
def log2(self) -> Self:
|
|
416
|
+
"""Compute the element-wise base-2 logarithm of the signal data."""
|
|
417
|
+
result = np.log2(self.y)
|
|
418
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
#--------------------------
|
|
422
|
+
# Aggregation signal ops
|
|
423
|
+
#--------------------------
|
|
424
|
+
def mean(self) -> float:
|
|
425
|
+
"""Compute the mean of the signal data."""
|
|
426
|
+
return float(np.mean(self.y))
|
|
427
|
+
|
|
428
|
+
def std(self) -> float:
|
|
429
|
+
"""Compute the standard deviation of the signal data."""
|
|
430
|
+
return float(np.std(self.y))
|
|
431
|
+
|
|
432
|
+
def min(self) -> float:
|
|
433
|
+
"""Compute the minimum value in the signal data."""
|
|
434
|
+
return float(np.min(self.y))
|
|
435
|
+
|
|
436
|
+
def max(self) -> float:
|
|
437
|
+
"""Compute the maximum value in the signal data."""
|
|
438
|
+
return float(np.max(self.y))
|
|
439
|
+
|
|
440
|
+
def sum(self) -> float:
|
|
441
|
+
"""Compute the sum of the signal data."""
|
|
442
|
+
return float(np.sum(self.y))
|
|
443
|
+
|
|
444
|
+
#-----------------------------------
|
|
445
|
+
# Repr
|
|
446
|
+
#-----------------------------------
|
|
447
|
+
|
|
448
|
+
def __str__(self):
|
|
449
|
+
cls = self.__class__.__name__
|
|
450
|
+
data = self.y
|
|
451
|
+
|
|
452
|
+
arr_str = np.array2string(
|
|
453
|
+
data,
|
|
454
|
+
separator=", ",
|
|
455
|
+
threshold=50, # limit number of elements shown
|
|
456
|
+
edgeitems=3, # show first/last 3 rows and columns
|
|
457
|
+
max_line_width=120, # avoid wrapping
|
|
458
|
+
formatter={'float_kind': lambda x: f"{x:.4g}"}
|
|
227
459
|
)
|
|
228
460
|
|
|
229
|
-
return
|
|
230
|
-
|
|
461
|
+
return f"Signal({arr_str}, shape={data.shape}, kind={cls})"
|
|
462
|
+
|
|
463
|
+
def __repr__(self):
|
|
464
|
+
cls = self.__class__.__name__
|
|
465
|
+
data = self.y
|
|
466
|
+
|
|
467
|
+
arr_str = np.array2string(
|
|
468
|
+
data,
|
|
469
|
+
separator=", ",
|
|
470
|
+
threshold=50, # limit number of elements shown
|
|
471
|
+
edgeitems=3, # show first/last 3 rows and columns
|
|
472
|
+
max_line_width=120, # avoid wrapping
|
|
473
|
+
formatter={'float_kind': lambda x: f"{x:.4g}"}
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
return f"Signal({arr_str}, shape={data.shape}, kind={cls})"
|