modusa 0.2.22__py3-none-any.whl → 0.2.23__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/decorators.py +4 -4
- modusa/devtools/docs/source/generators/audio_waveforms.rst +8 -0
- modusa/devtools/docs/source/generators/base.rst +8 -0
- modusa/devtools/docs/source/generators/index.rst +8 -0
- modusa/devtools/docs/source/io/audio_loader.rst +8 -0
- modusa/devtools/docs/source/io/base.rst +8 -0
- modusa/devtools/docs/source/io/index.rst +8 -0
- modusa/devtools/docs/source/plugins/base.rst +8 -0
- modusa/devtools/docs/source/plugins/index.rst +7 -0
- modusa/devtools/docs/source/signals/audio_signal.rst +8 -0
- modusa/devtools/docs/source/signals/base.rst +8 -0
- modusa/devtools/docs/source/signals/frequency_domain_signal.rst +8 -0
- modusa/devtools/docs/source/signals/index.rst +11 -0
- modusa/devtools/docs/source/signals/spectrogram.rst +8 -0
- modusa/devtools/docs/source/signals/time_domain_signal.rst +8 -0
- modusa/devtools/docs/source/tools/audio_converter.rst +8 -0
- modusa/devtools/docs/source/tools/audio_player.rst +8 -0
- modusa/devtools/docs/source/tools/base.rst +8 -0
- modusa/devtools/docs/source/tools/fourier_tranform.rst +8 -0
- modusa/devtools/docs/source/tools/index.rst +13 -0
- modusa/devtools/docs/source/tools/math_ops.rst +8 -0
- modusa/devtools/docs/source/tools/plotter.rst +8 -0
- modusa/devtools/docs/source/tools/youtube_downloader.rst +8 -0
- modusa/devtools/generate_doc_source.py +96 -0
- modusa/devtools/generate_template.py +8 -8
- modusa/devtools/main.py +3 -2
- modusa/devtools/templates/test.py +2 -3
- modusa/devtools/templates/{engine.py → tool.py} +3 -8
- modusa/generators/__init__.py +0 -2
- modusa/generators/audio_waveforms.py +22 -13
- modusa/generators/base.py +1 -1
- modusa/io/__init__.py +1 -5
- modusa/io/audio_loader.py +3 -33
- modusa/main.py +0 -30
- modusa/signals/__init__.py +1 -5
- modusa/signals/audio_signal.py +181 -124
- modusa/signals/base.py +1 -8
- modusa/signals/frequency_domain_signal.py +140 -93
- modusa/signals/spectrogram.py +197 -98
- modusa/signals/time_domain_signal.py +177 -74
- modusa/tools/__init__.py +2 -0
- modusa/{io → tools}/audio_converter.py +12 -4
- modusa/tools/audio_player.py +114 -0
- modusa/tools/base.py +43 -0
- modusa/tools/fourier_tranform.py +24 -0
- modusa/tools/math_ops.py +232 -0
- modusa/{io → tools}/plotter.py +155 -42
- modusa/{io → tools}/youtube_downloader.py +2 -2
- modusa/utils/excp.py +9 -42
- {modusa-0.2.22.dist-info → modusa-0.2.23.dist-info}/METADATA +1 -1
- modusa-0.2.23.dist-info/RECORD +70 -0
- modusa/engines/.DS_Store +0 -0
- modusa/engines/__init__.py +0 -3
- modusa/engines/base.py +0 -14
- modusa/io/audio_player.py +0 -72
- modusa/signals/signal_ops.py +0 -158
- modusa-0.2.22.dist-info/RECORD +0 -47
- {modusa-0.2.22.dist-info → modusa-0.2.23.dist-info}/WHEEL +0 -0
- {modusa-0.2.22.dist-info → modusa-0.2.23.dist-info}/entry_points.txt +0 -0
- {modusa-0.2.22.dist-info → modusa-0.2.23.dist-info}/licenses/LICENSE.md +0 -0
modusa/main.py
CHANGED
|
@@ -1,33 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
from modusa.io import Plotter
|
|
5
|
-
import numpy as np
|
|
6
|
-
import matplotlib.pyplot as plt
|
|
7
|
-
|
|
8
|
-
# Create a 50x50 random matrix
|
|
9
|
-
M = np.random.rand(50, 50)
|
|
10
|
-
|
|
11
|
-
# Coordinate axes
|
|
12
|
-
r = np.linspace(0, 1, M.shape[0])
|
|
13
|
-
c = np.linspace(0, 1, M.shape[1])
|
|
14
|
-
|
|
15
|
-
# Plot the matrix
|
|
16
|
-
fig = Plotter.plot_matrix(
|
|
17
|
-
M=M,
|
|
18
|
-
r=r,
|
|
19
|
-
c=c,
|
|
20
|
-
log_compression_factor=None,
|
|
21
|
-
ax=None,
|
|
22
|
-
labels=None,
|
|
23
|
-
zoom=None,
|
|
24
|
-
highlight=None,
|
|
25
|
-
cmap="viridis",
|
|
26
|
-
origin="lower",
|
|
27
|
-
show_colorbar=True,
|
|
28
|
-
cax=None,
|
|
29
|
-
show_grid=False,
|
|
30
|
-
tick_mode="cen",
|
|
31
|
-
n_ticks=(5, 5),
|
|
32
|
-
value_range=None
|
|
33
|
-
)
|
modusa/signals/__init__.py
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
-
from .
|
|
4
|
-
from .audio_signal import AudioSignal
|
|
5
|
-
from .time_domain_signal import TimeDomainSignal
|
|
6
|
-
from .frequency_domain_signal import FrequencyDomainSignal
|
|
7
|
-
from .spectrogram import Spectrogram
|
|
3
|
+
from .audio_signal import AudioSignal
|
modusa/signals/audio_signal.py
CHANGED
|
@@ -4,7 +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.
|
|
7
|
+
from modusa.tools.math_ops import MathOps
|
|
8
8
|
from typing import Self, Any
|
|
9
9
|
import numpy as np
|
|
10
10
|
import matplotlib.pyplot as plt
|
|
@@ -18,10 +18,6 @@ class AudioSignal(ModusaSignal):
|
|
|
18
18
|
----
|
|
19
19
|
- It is highly recommended to use :class:`~modusa.io.AudioLoader` to instantiate an object of this class.
|
|
20
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
21
|
|
|
26
22
|
Parameters
|
|
27
23
|
----------
|
|
@@ -29,10 +25,8 @@ class AudioSignal(ModusaSignal):
|
|
|
29
25
|
1D numpy array representing the audio signal.
|
|
30
26
|
sr : int | None
|
|
31
27
|
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
28
|
t0 : float, optional
|
|
35
|
-
Starting time in seconds. Defaults to 0.0.
|
|
29
|
+
Starting time in seconds. Defaults to 0.0.
|
|
36
30
|
title : str | None, optional
|
|
37
31
|
Optional title for the signal. Defaults to `"Audio Signal"`.
|
|
38
32
|
"""
|
|
@@ -46,23 +40,13 @@ class AudioSignal(ModusaSignal):
|
|
|
46
40
|
#----------------------------------
|
|
47
41
|
|
|
48
42
|
@validate_args_type()
|
|
49
|
-
def __init__(self, y: np.ndarray, sr: int
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
def __init__(self, y: np.ndarray, sr: int, t0: float = 0.0, title: str | None = None):
|
|
44
|
+
"""
|
|
45
|
+
Loads the audio signal.
|
|
46
|
+
"""
|
|
47
|
+
if y.ndim != 1: # Mono signal only
|
|
52
48
|
raise excp.InputValueError(f"`y` must have 1 dimension, not {y.ndim}.")
|
|
53
49
|
|
|
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
50
|
self._y = y
|
|
67
51
|
self._sr = sr
|
|
68
52
|
self._t0 = t0
|
|
@@ -73,59 +57,82 @@ class AudioSignal(ModusaSignal):
|
|
|
73
57
|
#----------------------
|
|
74
58
|
@immutable_property("Create a new object instead.")
|
|
75
59
|
def y(self) -> np.ndarray:
|
|
76
|
-
"""
|
|
60
|
+
"""Returns audio data."""
|
|
77
61
|
return self._y
|
|
78
62
|
|
|
79
63
|
@immutable_property("Create a new object instead.")
|
|
80
64
|
def sr(self) -> np.ndarray:
|
|
81
|
-
"""
|
|
65
|
+
"""Returns sampling rate of the audio."""
|
|
82
66
|
return self._sr
|
|
83
67
|
|
|
84
68
|
@immutable_property("Create a new object instead.")
|
|
85
69
|
def t0(self) -> np.ndarray:
|
|
86
|
-
"""
|
|
70
|
+
"""Returns start timestamp of the audio."""
|
|
87
71
|
return self._t0
|
|
88
72
|
|
|
73
|
+
#----------------------
|
|
74
|
+
# Derived Properties
|
|
75
|
+
#----------------------
|
|
89
76
|
@immutable_property("Create a new object instead.")
|
|
90
77
|
def t(self) -> np.ndarray:
|
|
91
78
|
"""Timestamp array of the audio."""
|
|
92
79
|
return self.t0 + np.arange(len(self.y)) / self.sr
|
|
93
80
|
|
|
94
81
|
@immutable_property("Mutation not allowed.")
|
|
95
|
-
def Ts(self) ->
|
|
82
|
+
def Ts(self) -> float:
|
|
96
83
|
"""Sampling Period of the audio."""
|
|
97
|
-
return 1.
|
|
84
|
+
return 1. / self.sr
|
|
98
85
|
|
|
99
86
|
@immutable_property("Mutation not allowed.")
|
|
100
|
-
def duration(self) ->
|
|
87
|
+
def duration(self) -> float:
|
|
101
88
|
"""Duration of the audio."""
|
|
102
89
|
return len(self.y) / self.sr
|
|
103
90
|
|
|
104
91
|
@immutable_property("Mutation not allowed.")
|
|
105
|
-
def
|
|
92
|
+
def shape(self) -> tuple:
|
|
93
|
+
"""Shape of the audio signal."""
|
|
94
|
+
return self.y.shape
|
|
95
|
+
|
|
96
|
+
@immutable_property("Mutation not allowed.")
|
|
97
|
+
def ndim(self) -> int:
|
|
98
|
+
"""Dimension of the audio."""
|
|
99
|
+
return self.y.ndim
|
|
100
|
+
|
|
101
|
+
@immutable_property("Mutation not allowed.")
|
|
102
|
+
def __len__(self) -> int:
|
|
103
|
+
"""Dimension of the audio."""
|
|
104
|
+
return len(self.y)
|
|
105
|
+
|
|
106
|
+
#----------------------
|
|
107
|
+
# Methods
|
|
108
|
+
#----------------------
|
|
109
|
+
|
|
110
|
+
def print_info(self) -> None:
|
|
106
111
|
"""Prints info about the audio."""
|
|
107
112
|
print("-" * 50)
|
|
108
113
|
print(f"{'Title':<20}: {self.title}")
|
|
109
|
-
print(f"{'
|
|
114
|
+
print(f"{'Type':<20}: {self._name}")
|
|
110
115
|
print(f"{'Duration':<20}: {self.duration:.2f} sec")
|
|
111
116
|
print(f"{'Sampling Rate':<20}: {self.sr} Hz")
|
|
112
117
|
print(f"{'Sampling Period':<20}: {(self.Ts*1000) :.4f} ms")
|
|
113
118
|
print("-" * 50)
|
|
114
119
|
|
|
115
|
-
#----------------------
|
|
116
|
-
# Methods
|
|
117
|
-
#----------------------
|
|
118
120
|
def __getitem__(self, key):
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
sliced_y = self.y[key]
|
|
122
|
+
|
|
123
|
+
# If key is a single integer, return just the sample value
|
|
124
|
+
if isinstance(key, int):
|
|
125
|
+
return sliced_y
|
|
126
|
+
|
|
127
|
+
# Otherwise, slicing: use self.t[key][0] as new t0
|
|
128
|
+
new_t0 = self.t[key][0]
|
|
129
|
+
|
|
130
|
+
return self.__class__(
|
|
131
|
+
y=sliced_y,
|
|
132
|
+
sr=self.sr,
|
|
133
|
+
t0=new_t0,
|
|
134
|
+
title=f"{self.title}[{key}]"
|
|
135
|
+
)
|
|
129
136
|
|
|
130
137
|
@validate_args_type()
|
|
131
138
|
def crop(self, t_min: int | float | None = None, t_max: int | float | None = None) -> "AudioSignal":
|
|
@@ -152,7 +159,7 @@ class AudioSignal(ModusaSignal):
|
|
|
152
159
|
"""
|
|
153
160
|
y = self.y
|
|
154
161
|
t = self.t
|
|
155
|
-
|
|
162
|
+
|
|
156
163
|
mask = np.ones_like(t, dtype=bool)
|
|
157
164
|
if t_min is not None:
|
|
158
165
|
mask &= (t >= t_min)
|
|
@@ -161,27 +168,28 @@ class AudioSignal(ModusaSignal):
|
|
|
161
168
|
|
|
162
169
|
cropped_y = y[mask]
|
|
163
170
|
new_t0 = t[mask][0] if np.any(mask) else self.t0 # fallback to original t0 if mask is empty
|
|
164
|
-
|
|
171
|
+
|
|
165
172
|
return self.__class__(y=cropped_y, sr=self.sr, t0=new_t0, title=self.title)
|
|
166
173
|
|
|
167
174
|
|
|
168
175
|
@validate_args_type()
|
|
169
176
|
def plot(
|
|
170
177
|
self,
|
|
171
|
-
scale_y: tuple[float, float] | None = None,
|
|
172
178
|
ax: plt.Axes | None = None,
|
|
173
|
-
|
|
174
|
-
marker: str | None = None,
|
|
175
|
-
linestyle: str | None = None,
|
|
176
|
-
stem: bool | None = False,
|
|
177
|
-
legend_loc: str | None = None,
|
|
179
|
+
fmt: str = "k-",
|
|
178
180
|
title: str | None = None,
|
|
181
|
+
label: str | None = None,
|
|
179
182
|
ylabel: str | None = "Amplitude",
|
|
180
183
|
xlabel: str | None = "Time (sec)",
|
|
181
184
|
ylim: tuple[float, float] | None = None,
|
|
182
185
|
xlim: tuple[float, float] | None = None,
|
|
183
186
|
highlight: list[tuple[float, float]] | None = None,
|
|
184
|
-
|
|
187
|
+
vlines: list[float] | None = None,
|
|
188
|
+
hlines: list[float] | None = None,
|
|
189
|
+
show_grid: bool = False,
|
|
190
|
+
stem: bool = False,
|
|
191
|
+
legend_loc: str | None = None,
|
|
192
|
+
) -> plt.Figure | None:
|
|
185
193
|
"""
|
|
186
194
|
Plot the audio waveform using matplotlib.
|
|
187
195
|
|
|
@@ -193,44 +201,64 @@ class AudioSignal(ModusaSignal):
|
|
|
193
201
|
|
|
194
202
|
Parameters
|
|
195
203
|
----------
|
|
196
|
-
|
|
197
|
-
Range to scale the y-axis data before plotting. Useful for normalization.
|
|
198
|
-
ax : matplotlib.axes.Axes, optional
|
|
204
|
+
ax : matplotlib.axes.Axes | None
|
|
199
205
|
Pre-existing axes to plot into. If None, a new figure and axes are created.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|
206
|
+
fmt : str | None
|
|
207
|
+
Format of the plot as per matplotlib standards (Eg. "k-" or "blue--o)
|
|
208
|
+
title : str | None
|
|
211
209
|
Plot title. Defaults to the signal’s title.
|
|
212
|
-
|
|
210
|
+
label: str | None
|
|
211
|
+
Label for the plot, shown as legend.
|
|
212
|
+
ylabel : str | None
|
|
213
213
|
Label for the y-axis. Defaults to `"Amplitude"`.
|
|
214
|
-
xlabel : str
|
|
214
|
+
xlabel : str | None
|
|
215
215
|
Label for the x-axis. Defaults to `"Time (sec)"`.
|
|
216
|
-
ylim : tuple
|
|
216
|
+
ylim : tuple[float, float] | None
|
|
217
217
|
Limits for the y-axis.
|
|
218
|
-
xlim : tuple
|
|
219
|
-
|
|
220
|
-
highlight : list of tuple of float or None, optional
|
|
218
|
+
xlim : tuple[float, float] | None
|
|
219
|
+
highlight : list[tuple[float, float]] | None
|
|
221
220
|
List of time intervals to highlight on the plot, each as (start, end).
|
|
221
|
+
vlines: list[float]
|
|
222
|
+
List of x values to draw vertical lines. (Eg. [10, 13.5])
|
|
223
|
+
hlines: list[float]
|
|
224
|
+
List of y values to draw horizontal lines. (Eg. [10, 13.5])
|
|
225
|
+
show_grid: bool
|
|
226
|
+
If true, shows grid.
|
|
227
|
+
stem : bool
|
|
228
|
+
If True, use a stem plot instead of a continuous line. Autorejects if signal is too large.
|
|
229
|
+
legend_loc : str | None
|
|
230
|
+
If provided, adds a legend at the specified location (e.g., "upper right" or "best").
|
|
231
|
+
Limits for the x-axis.
|
|
222
232
|
|
|
223
233
|
Returns
|
|
224
234
|
-------
|
|
225
|
-
matplotlib.figure.Figure
|
|
226
|
-
The figure object containing the plot.
|
|
235
|
+
matplotlib.figure.Figure | None
|
|
236
|
+
The figure object containing the plot or None in case an axis is provided.
|
|
227
237
|
"""
|
|
228
238
|
|
|
229
|
-
from modusa.
|
|
239
|
+
from modusa.tools.plotter import Plotter
|
|
230
240
|
|
|
231
|
-
|
|
241
|
+
if title is None:
|
|
242
|
+
title = self.title
|
|
232
243
|
|
|
233
|
-
fig: plt.Figure | None = Plotter.plot_signal(
|
|
244
|
+
fig: plt.Figure | None = Plotter.plot_signal(
|
|
245
|
+
y=self.y,
|
|
246
|
+
x=self.t,
|
|
247
|
+
ax=ax,
|
|
248
|
+
fmt=fmt,
|
|
249
|
+
title=title,
|
|
250
|
+
label=label,
|
|
251
|
+
ylabel=ylabel,
|
|
252
|
+
xlabel=xlabel,
|
|
253
|
+
ylim=ylim,
|
|
254
|
+
xlim=xlim,
|
|
255
|
+
highlight=highlight,
|
|
256
|
+
vlines=vlines,
|
|
257
|
+
hlines=hlines,
|
|
258
|
+
show_grid=show_grid,
|
|
259
|
+
stem=stem,
|
|
260
|
+
legend_loc=legend_loc,
|
|
261
|
+
)
|
|
234
262
|
|
|
235
263
|
return fig
|
|
236
264
|
|
|
@@ -257,14 +285,14 @@ class AudioSignal(ModusaSignal):
|
|
|
257
285
|
IPython.display.Audio
|
|
258
286
|
An interactive audio player widget for Jupyter environments.
|
|
259
287
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
- Optionally, specific regions of the signal can be played back, each defined by a (start, end) time pair.
|
|
288
|
+
See Also
|
|
289
|
+
--------
|
|
290
|
+
:class:`~modusa.tools.audio_player.AudioPlayer`
|
|
264
291
|
"""
|
|
265
292
|
|
|
266
|
-
from modusa.
|
|
267
|
-
|
|
293
|
+
from modusa.tools.audio_player import AudioPlayer
|
|
294
|
+
title = title or self.title
|
|
295
|
+
audio_player = AudioPlayer.play(y=self.y, sr=self.sr, regions=regions, title=title)
|
|
268
296
|
|
|
269
297
|
return audio_player
|
|
270
298
|
|
|
@@ -294,14 +322,17 @@ class AudioSignal(ModusaSignal):
|
|
|
294
322
|
Spectrogram
|
|
295
323
|
Spectrogram object containing S (complex STFT), t (time bins), and f (frequency bins).
|
|
296
324
|
"""
|
|
325
|
+
import warnings
|
|
326
|
+
warnings.filterwarnings("ignore", category=UserWarning, module="librosa.core.intervals")
|
|
327
|
+
|
|
297
328
|
from modusa.signals.spectrogram import Spectrogram
|
|
298
329
|
import librosa
|
|
299
330
|
|
|
300
331
|
S = librosa.stft(self.y, n_fft=n_fft, win_length=win_length, hop_length=hop_length, window=window)
|
|
301
332
|
f = librosa.fft_frequencies(sr=self.sr, n_fft=n_fft)
|
|
302
333
|
t = librosa.frames_to_time(np.arange(S.shape[1]), sr=self.sr, hop_length=hop_length)
|
|
303
|
-
|
|
304
|
-
spec = Spectrogram(S=S, f=f,
|
|
334
|
+
frame_rate = self.sr / hop_length
|
|
335
|
+
spec = Spectrogram(S=S, f=f, frame_rate=frame_rate, t0=self.t0)
|
|
305
336
|
if self.title != self._name: # Means title of the audio was reset so we pass that info to spec
|
|
306
337
|
spec.title = self.title
|
|
307
338
|
|
|
@@ -316,137 +347,163 @@ class AudioSignal(ModusaSignal):
|
|
|
316
347
|
return np.asarray(self.y, dtype=dtype)
|
|
317
348
|
|
|
318
349
|
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
|
|
319
|
-
if
|
|
320
|
-
|
|
321
|
-
result = ufunc(
|
|
322
|
-
return self.__class__(y=result, sr=self.sr, title=f"{self.title}
|
|
350
|
+
if method == "__call__":
|
|
351
|
+
input_arrays = [x.y if isinstance(x, self.__class__) else x for x in inputs]
|
|
352
|
+
result = ufunc(*input_arrays, **kwargs)
|
|
353
|
+
return self.__class__(y=result, sr=self.sr, title=f"{self.title}")
|
|
323
354
|
return NotImplemented
|
|
324
355
|
|
|
325
356
|
def __add__(self, other):
|
|
326
357
|
other_data = other.y if isinstance(other, self.__class__) else other
|
|
327
|
-
result =
|
|
358
|
+
result = MathOps.add(self.y, other_data)
|
|
328
359
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
329
360
|
|
|
330
361
|
def __radd__(self, other):
|
|
331
|
-
|
|
362
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
363
|
+
result = MathOps.add(other_data, self.y)
|
|
332
364
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
333
365
|
|
|
334
366
|
def __sub__(self, other):
|
|
335
367
|
other_data = other.y if isinstance(other, self.__class__) else other
|
|
336
|
-
result =
|
|
368
|
+
result = MathOps.subtract(self.y, other_data)
|
|
337
369
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
338
370
|
|
|
339
371
|
def __rsub__(self, other):
|
|
340
|
-
|
|
372
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
373
|
+
result = MathOps.subtract(other_data, self.y)
|
|
341
374
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
342
375
|
|
|
343
376
|
def __mul__(self, other):
|
|
344
377
|
other_data = other.y if isinstance(other, self.__class__) else other
|
|
345
|
-
result =
|
|
378
|
+
result = MathOps.multiply(self.y, other_data)
|
|
346
379
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
347
380
|
|
|
348
381
|
def __rmul__(self, other):
|
|
349
|
-
|
|
382
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
383
|
+
result = MathOps.multiply(other_data, self.y)
|
|
350
384
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
351
385
|
|
|
352
386
|
def __truediv__(self, other):
|
|
353
387
|
other_data = other.y if isinstance(other, self.__class__) else other
|
|
354
|
-
result =
|
|
388
|
+
result = MathOps.divide(self.y, other_data)
|
|
355
389
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
356
390
|
|
|
357
391
|
def __rtruediv__(self, other):
|
|
358
|
-
|
|
392
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
393
|
+
result = MathOps.divide(other_data, self.y)
|
|
359
394
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
360
395
|
|
|
361
396
|
def __floordiv__(self, other):
|
|
362
|
-
other_data = other.
|
|
363
|
-
result =
|
|
397
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
398
|
+
result = MathOps.floor_divide(self.y, other_data)
|
|
364
399
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
365
400
|
|
|
366
401
|
def __rfloordiv__(self, other):
|
|
367
|
-
|
|
402
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
403
|
+
result = MathOps.floor_divide(other_data, self.y)
|
|
368
404
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
369
405
|
|
|
370
406
|
def __pow__(self, other):
|
|
371
407
|
other_data = other.y if isinstance(other, self.__class__) else other
|
|
372
|
-
result =
|
|
408
|
+
result = MathOps.power(self.y, other_data)
|
|
373
409
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
374
410
|
|
|
375
411
|
def __rpow__(self, other):
|
|
376
|
-
|
|
412
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
413
|
+
result = MathOps.power(other_data, self.y)
|
|
377
414
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
378
415
|
|
|
379
416
|
def __abs__(self):
|
|
380
|
-
|
|
417
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
418
|
+
result = MathOps.abs(self.y)
|
|
381
419
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
420
|
+
|
|
421
|
+
def __or__(self, other):
|
|
422
|
+
if not isinstance(other, self.__class__):
|
|
423
|
+
raise excp.InputTypeError(f"Can only concatenate with another {self.__class__.__name__}")
|
|
424
|
+
|
|
425
|
+
if self.sr != other.sr:
|
|
426
|
+
raise excp.InputValueError(f"Cannot concatenate: Sampling rates differ ({self.sr} vs {other.sr})")
|
|
427
|
+
|
|
428
|
+
# Concatenate raw audio data
|
|
429
|
+
y_cat = np.concatenate([self.y, other.y])
|
|
430
|
+
|
|
431
|
+
# Preserve t0 of the first signal
|
|
432
|
+
new_title = f"{self.title} | {other.title}"
|
|
433
|
+
return self.__class__(y=y_cat, sr=self.sr, t0=self.t0, title=new_title)
|
|
382
434
|
|
|
383
435
|
|
|
384
436
|
#--------------------------
|
|
385
437
|
# Other signal ops
|
|
386
438
|
#--------------------------
|
|
439
|
+
def abs(self) -> Self:
|
|
440
|
+
"""Compute the element-wise abs of the signal data."""
|
|
441
|
+
result = MathOps.abs(self.y)
|
|
442
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
443
|
+
|
|
387
444
|
def sin(self) -> Self:
|
|
388
445
|
"""Compute the element-wise sine of the signal data."""
|
|
389
|
-
result =
|
|
446
|
+
result = MathOps.sin(self.y)
|
|
390
447
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
391
448
|
|
|
392
449
|
def cos(self) -> Self:
|
|
393
450
|
"""Compute the element-wise cosine of the signal data."""
|
|
394
|
-
result =
|
|
451
|
+
result = MathOps.cos(self.y)
|
|
395
452
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
396
453
|
|
|
397
454
|
def exp(self) -> Self:
|
|
398
455
|
"""Compute the element-wise exponential of the signal data."""
|
|
399
|
-
result =
|
|
456
|
+
result = MathOps.exp(self.y)
|
|
400
457
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
401
458
|
|
|
402
459
|
def tanh(self) -> Self:
|
|
403
460
|
"""Compute the element-wise hyperbolic tangent of the signal data."""
|
|
404
|
-
result =
|
|
461
|
+
result = MathOps.tanh(self.y)
|
|
405
462
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
406
463
|
|
|
407
464
|
def log(self) -> Self:
|
|
408
465
|
"""Compute the element-wise natural logarithm of the signal data."""
|
|
409
|
-
result =
|
|
466
|
+
result = MathOps.log(self.y)
|
|
410
467
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
411
468
|
|
|
412
469
|
def log1p(self) -> Self:
|
|
413
470
|
"""Compute the element-wise natural logarithm of (1 + signal data)."""
|
|
414
|
-
result =
|
|
471
|
+
result = MathOps.log1p(self.y)
|
|
415
472
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
416
473
|
|
|
417
474
|
def log10(self) -> Self:
|
|
418
475
|
"""Compute the element-wise base-10 logarithm of the signal data."""
|
|
419
|
-
result =
|
|
476
|
+
result = MathOps.log10(self.y)
|
|
420
477
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
421
478
|
|
|
422
479
|
def log2(self) -> Self:
|
|
423
480
|
"""Compute the element-wise base-2 logarithm of the signal data."""
|
|
424
|
-
result =
|
|
481
|
+
result = MathOps.log2(self.y)
|
|
425
482
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
426
483
|
|
|
427
484
|
|
|
428
485
|
#--------------------------
|
|
429
486
|
# Aggregation signal ops
|
|
430
487
|
#--------------------------
|
|
431
|
-
def mean(self) ->
|
|
488
|
+
def mean(self) -> "np.generic":
|
|
432
489
|
"""Compute the mean of the signal data."""
|
|
433
|
-
return
|
|
490
|
+
return MathOps.mean(self.y)
|
|
434
491
|
|
|
435
|
-
def std(self) ->
|
|
492
|
+
def std(self) -> "np.generic":
|
|
436
493
|
"""Compute the standard deviation of the signal data."""
|
|
437
|
-
return
|
|
494
|
+
return MathOps.std(self.y)
|
|
438
495
|
|
|
439
|
-
def min(self) ->
|
|
496
|
+
def min(self) -> "np.generic":
|
|
440
497
|
"""Compute the minimum value in the signal data."""
|
|
441
|
-
return
|
|
498
|
+
return MathOps.min(self.y)
|
|
442
499
|
|
|
443
|
-
def max(self) ->
|
|
500
|
+
def max(self) -> "np.generic":
|
|
444
501
|
"""Compute the maximum value in the signal data."""
|
|
445
|
-
return
|
|
502
|
+
return MathOps.max(self.y)
|
|
446
503
|
|
|
447
|
-
def sum(self) ->
|
|
504
|
+
def sum(self) -> "np.generic":
|
|
448
505
|
"""Compute the sum of the signal data."""
|
|
449
|
-
return
|
|
506
|
+
return MathOps.sum(self.y)
|
|
450
507
|
|
|
451
508
|
#-----------------------------------
|
|
452
509
|
# Repr
|
|
@@ -465,7 +522,7 @@ class AudioSignal(ModusaSignal):
|
|
|
465
522
|
formatter={'float_kind': lambda x: f"{x:.4g}"}
|
|
466
523
|
)
|
|
467
524
|
|
|
468
|
-
return f"Signal({arr_str}, shape={data.shape},
|
|
525
|
+
return f"Signal({arr_str}, shape={data.shape}, type={cls})"
|
|
469
526
|
|
|
470
527
|
def __repr__(self):
|
|
471
528
|
cls = self.__class__.__name__
|
|
@@ -480,4 +537,4 @@ class AudioSignal(ModusaSignal):
|
|
|
480
537
|
formatter={'float_kind': lambda x: f"{x:.4g}"}
|
|
481
538
|
)
|
|
482
539
|
|
|
483
|
-
return f"Signal({arr_str}, shape={data.shape},
|
|
540
|
+
return f"Signal({arr_str}, shape={data.shape}, type={cls})"
|
modusa/signals/base.py
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
from modusa.decorators import immutable_property, validate_args_type
|
|
5
|
-
from modusa.signals.signal_ops import SignalOps
|
|
3
|
+
|
|
6
4
|
from abc import ABC, abstractmethod
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
from typing import Self
|
|
9
|
-
import numpy as np
|
|
10
|
-
import matplotlib.pyplot as plt
|
|
11
5
|
|
|
12
6
|
class ModusaSignal(ABC):
|
|
13
7
|
"""
|
|
@@ -26,7 +20,6 @@ class ModusaSignal(ABC):
|
|
|
26
20
|
_created_at = "2025-06-23"
|
|
27
21
|
#----------------------------------
|
|
28
22
|
|
|
29
|
-
@validate_args_type()
|
|
30
23
|
def __init__(self):
|
|
31
24
|
self._plugin_chain = []
|
|
32
25
|
|