modusa 0.2.21__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.21.dist-info → modusa-0.2.23.dist-info}/METADATA +2 -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.21.dist-info/RECORD +0 -47
- {modusa-0.2.21.dist-info → modusa-0.2.23.dist-info}/WHEEL +0 -0
- {modusa-0.2.21.dist-info → modusa-0.2.23.dist-info}/entry_points.txt +0 -0
- {modusa-0.2.21.dist-info → modusa-0.2.23.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -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.tools.math_ops import MathOps
|
|
7
8
|
from typing import Self, Any
|
|
8
9
|
import numpy as np
|
|
9
10
|
import matplotlib.pyplot as plt
|
|
@@ -12,11 +13,12 @@ class TimeDomainSignal(ModusaSignal):
|
|
|
12
13
|
"""
|
|
13
14
|
Initialize a uniformly sampled 1D time-domain signal.
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
- Not to be instantiated directly.
|
|
17
|
+
- This class is specifically designed to hold 1D signals that result
|
|
18
|
+
from slicing a 2D representation like a spectrogram.
|
|
19
|
+
- For example, if you have a spectrogram `S` and you perform `S[10, :]`,
|
|
20
|
+
the result is a 1D signal over time, this class provides a
|
|
21
|
+
clean and consistent way to handle such slices.
|
|
20
22
|
|
|
21
23
|
Parameters
|
|
22
24
|
----------
|
|
@@ -66,17 +68,55 @@ class TimeDomainSignal(ModusaSignal):
|
|
|
66
68
|
""""""
|
|
67
69
|
return self._t0
|
|
68
70
|
|
|
71
|
+
#----------------------
|
|
72
|
+
# Derived Properties
|
|
73
|
+
#----------------------
|
|
74
|
+
|
|
69
75
|
@immutable_property("Create a new object instead.")
|
|
70
76
|
def t(self) -> np.ndarray:
|
|
71
|
-
|
|
77
|
+
"""Timestamp array of the audio."""
|
|
78
|
+
return self.t0 + np.arange(len(self.y)) / self.sr
|
|
79
|
+
|
|
80
|
+
@immutable_property("Mutation not allowed.")
|
|
81
|
+
def Ts(self) -> float:
|
|
82
|
+
"""Sampling Period of the audio."""
|
|
83
|
+
return 1. / self.sr
|
|
84
|
+
|
|
85
|
+
@immutable_property("Mutation not allowed.")
|
|
86
|
+
def duration(self) -> float:
|
|
87
|
+
"""Duration of the audio."""
|
|
88
|
+
return len(self.y) / self.sr
|
|
89
|
+
|
|
90
|
+
@immutable_property("Mutation not allowed.")
|
|
91
|
+
def shape(self) -> tuple:
|
|
92
|
+
"""Shape of the audio signal."""
|
|
93
|
+
return self.y.shape
|
|
94
|
+
|
|
95
|
+
@immutable_property("Mutation not allowed.")
|
|
96
|
+
def ndim(self) -> int:
|
|
97
|
+
"""Dimension of the audio."""
|
|
98
|
+
return self.y.ndim
|
|
99
|
+
|
|
100
|
+
@immutable_property("Mutation not allowed.")
|
|
101
|
+
def __len__(self) -> int:
|
|
102
|
+
"""Dimension of the audio."""
|
|
103
|
+
return len(self.y)
|
|
72
104
|
|
|
73
|
-
def __len__(self):
|
|
74
|
-
return len(self._y)
|
|
75
105
|
|
|
76
106
|
#----------------------
|
|
77
|
-
#
|
|
107
|
+
# Methods
|
|
78
108
|
#----------------------
|
|
79
109
|
|
|
110
|
+
def print_info(self) -> None:
|
|
111
|
+
"""Prints info about the audio."""
|
|
112
|
+
print("-" * 50)
|
|
113
|
+
print(f"{'Title':<20}: {self.title}")
|
|
114
|
+
print(f"{'Type':<20}: {self._name}")
|
|
115
|
+
print(f"{'Duration':<20}: {self.duration:.2f} sec")
|
|
116
|
+
print(f"{'Sampling Rate':<20}: {self.sr} Hz")
|
|
117
|
+
print(f"{'Sampling Period':<20}: {(self.Ts*1000) :.4f} ms")
|
|
118
|
+
print("-" * 50)
|
|
119
|
+
|
|
80
120
|
def __getitem__(self, key: slice) -> Self:
|
|
81
121
|
sliced_y = self._y[key]
|
|
82
122
|
t0_new = self.t[key.start] if key.start is not None else self.t0
|
|
@@ -85,59 +125,89 @@ class TimeDomainSignal(ModusaSignal):
|
|
|
85
125
|
@validate_args_type()
|
|
86
126
|
def plot(
|
|
87
127
|
self,
|
|
88
|
-
scale_y: tuple[float, float] | None = None,
|
|
89
128
|
ax: plt.Axes | None = None,
|
|
90
|
-
|
|
91
|
-
marker: str | None = None,
|
|
92
|
-
linestyle: str | None = None,
|
|
93
|
-
stem: bool | None = False,
|
|
94
|
-
legend_loc: str | None = None,
|
|
129
|
+
fmt: str = "k-",
|
|
95
130
|
title: str | None = None,
|
|
131
|
+
label: str | None = None,
|
|
96
132
|
ylabel: str | None = "Amplitude",
|
|
97
133
|
xlabel: str | None = "Time (sec)",
|
|
98
134
|
ylim: tuple[float, float] | None = None,
|
|
99
135
|
xlim: tuple[float, float] | None = None,
|
|
100
136
|
highlight: list[tuple[float, float]] | None = None,
|
|
101
|
-
|
|
137
|
+
vlines: list[float] | None = None,
|
|
138
|
+
hlines: list[float] | None = None,
|
|
139
|
+
show_grid: bool = False,
|
|
140
|
+
stem: bool | None = False,
|
|
141
|
+
legend_loc: str | None = None,
|
|
142
|
+
) -> plt.Figure | None:
|
|
102
143
|
"""
|
|
103
|
-
Plot the
|
|
144
|
+
Plot the audio waveform using matplotlib.
|
|
104
145
|
|
|
105
146
|
.. code-block:: python
|
|
106
|
-
|
|
107
|
-
|
|
147
|
+
|
|
148
|
+
from modusa.generators import AudioSignalGenerator
|
|
149
|
+
audio_example = AudioSignalGenerator.generate_example()
|
|
150
|
+
audio_example.plot(color="orange", title="Example Audio")
|
|
108
151
|
|
|
109
152
|
Parameters
|
|
110
153
|
----------
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
154
|
+
ax : matplotlib.axes.Axes | None
|
|
155
|
+
Pre-existing axes to plot into. If None, a new figure and axes are created.
|
|
156
|
+
fmt : str | None
|
|
157
|
+
Format of the plot as per matplotlib standards (Eg. "k-" or "blue--o)
|
|
158
|
+
title : str | None
|
|
159
|
+
Plot title. Defaults to the signal’s title.
|
|
160
|
+
label: str | None
|
|
161
|
+
Label for the plot, shown as legend.
|
|
162
|
+
ylabel : str | None
|
|
163
|
+
Label for the y-axis. Defaults to `"Amplitude"`.
|
|
164
|
+
xlabel : str | None
|
|
165
|
+
Label for the x-axis. Defaults to `"Time (sec)"`.
|
|
166
|
+
ylim : tuple[float, float] | None
|
|
167
|
+
Limits for the y-axis.
|
|
168
|
+
xlim : tuple[float, float] | None
|
|
169
|
+
highlight : list[tuple[float, float]] | None
|
|
170
|
+
List of time intervals to highlight on the plot, each as (start, end).
|
|
171
|
+
vlines: list[float]
|
|
172
|
+
List of x values to draw vertical lines. (Eg. [10, 13.5])
|
|
173
|
+
hlines: list[float]
|
|
174
|
+
List of y values to draw horizontal lines. (Eg. [10, 13.5])
|
|
175
|
+
show_grid: bool
|
|
176
|
+
If true, shows grid.
|
|
177
|
+
stem : bool
|
|
178
|
+
If True, use a stem plot instead of a continuous line. Autorejects if signal is too large.
|
|
179
|
+
legend_loc : str | None
|
|
180
|
+
If provided, adds a legend at the specified location (e.g., "upper right" or "best").
|
|
181
|
+
Limits for the x-axis.
|
|
125
182
|
|
|
126
183
|
Returns
|
|
127
184
|
-------
|
|
128
|
-
matplotlib.
|
|
129
|
-
The
|
|
130
|
-
|
|
131
|
-
Note
|
|
132
|
-
----
|
|
133
|
-
This is useful for visualizing 1D signals obtained from time slices of spectrograms.
|
|
185
|
+
matplotlib.figure.Figure | None
|
|
186
|
+
The figure object containing the plot or None in case an axis is provided.
|
|
134
187
|
"""
|
|
135
188
|
|
|
136
|
-
from modusa.
|
|
189
|
+
from modusa.tools.plotter import Plotter
|
|
137
190
|
|
|
138
191
|
title = title or self.title
|
|
139
192
|
|
|
140
|
-
fig: plt.Figure | None = Plotter.plot_signal(
|
|
193
|
+
fig: plt.Figure | None = Plotter.plot_signal(
|
|
194
|
+
y=self.y,
|
|
195
|
+
x=self.t,
|
|
196
|
+
ax=ax,
|
|
197
|
+
fmt=fmt,
|
|
198
|
+
title=title,
|
|
199
|
+
label=label,
|
|
200
|
+
ylabel=ylabel,
|
|
201
|
+
xlabel=xlabel,
|
|
202
|
+
ylim=ylim,
|
|
203
|
+
xlim=xlim,
|
|
204
|
+
highlight=highlight,
|
|
205
|
+
vlines=vlines,
|
|
206
|
+
hlines=hlines,
|
|
207
|
+
show_grid=show_grid,
|
|
208
|
+
stem=stem,
|
|
209
|
+
legend_loc=legend_loc,
|
|
210
|
+
)
|
|
141
211
|
|
|
142
212
|
return fig
|
|
143
213
|
|
|
@@ -148,131 +218,164 @@ class TimeDomainSignal(ModusaSignal):
|
|
|
148
218
|
def __array__(self, dtype=None):
|
|
149
219
|
return np.asarray(self.y, dtype=dtype)
|
|
150
220
|
|
|
221
|
+
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
|
|
222
|
+
if method == "__call__":
|
|
223
|
+
input_arrays = [x.y if isinstance(x, self.__class__) else x for x in inputs]
|
|
224
|
+
result = ufunc(*input_arrays, **kwargs)
|
|
225
|
+
return self.__class__(y=result, sr=self.sr, title=f"{self.title}")
|
|
226
|
+
return NotImplemented
|
|
227
|
+
|
|
151
228
|
def __add__(self, other):
|
|
152
229
|
other_data = other.y if isinstance(other, self.__class__) else other
|
|
153
|
-
result =
|
|
230
|
+
result = MathOps.add(self.y, other_data)
|
|
154
231
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
155
232
|
|
|
156
233
|
def __radd__(self, other):
|
|
157
|
-
|
|
234
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
235
|
+
result = MathOps.add(other_data, self.y)
|
|
158
236
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
159
237
|
|
|
160
238
|
def __sub__(self, other):
|
|
161
239
|
other_data = other.y if isinstance(other, self.__class__) else other
|
|
162
|
-
result =
|
|
240
|
+
result = MathOps.subtract(self.y, other_data)
|
|
163
241
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
164
242
|
|
|
165
243
|
def __rsub__(self, other):
|
|
166
|
-
|
|
244
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
245
|
+
result = MathOps.subtract(other_data, self.y)
|
|
167
246
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
168
247
|
|
|
169
248
|
def __mul__(self, other):
|
|
170
249
|
other_data = other.y if isinstance(other, self.__class__) else other
|
|
171
|
-
result =
|
|
250
|
+
result = MathOps.multiply(self.y, other_data)
|
|
172
251
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
173
252
|
|
|
174
253
|
def __rmul__(self, other):
|
|
175
|
-
|
|
254
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
255
|
+
result = MathOps.multiply(other_data, self.y)
|
|
176
256
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
177
257
|
|
|
178
258
|
def __truediv__(self, other):
|
|
179
259
|
other_data = other.y if isinstance(other, self.__class__) else other
|
|
180
|
-
result =
|
|
260
|
+
result = MathOps.divide(self.y, other_data)
|
|
181
261
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
182
262
|
|
|
183
263
|
def __rtruediv__(self, other):
|
|
184
|
-
|
|
264
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
265
|
+
result = MathOps.divide(other_data, self.y)
|
|
185
266
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
186
267
|
|
|
187
268
|
def __floordiv__(self, other):
|
|
188
269
|
other_data = other.y if isinstance(other, self.__class__) else other
|
|
189
|
-
result =
|
|
270
|
+
result = MathOps.floor_divide(self.y, other_data)
|
|
190
271
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
191
272
|
|
|
192
273
|
def __rfloordiv__(self, other):
|
|
193
|
-
|
|
274
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
275
|
+
result = MathOps.floor_divide(other_data, self.y)
|
|
194
276
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
195
277
|
|
|
196
278
|
def __pow__(self, other):
|
|
197
279
|
other_data = other.y if isinstance(other, self.__class__) else other
|
|
198
|
-
result =
|
|
280
|
+
result = MathOps.power(self.y, other_data)
|
|
199
281
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
200
282
|
|
|
201
283
|
def __rpow__(self, other):
|
|
202
|
-
|
|
284
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
285
|
+
result = MathOps.power(other_data, self.y)
|
|
203
286
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
204
287
|
|
|
205
288
|
def __abs__(self):
|
|
206
|
-
|
|
289
|
+
other_data = other.y if isinstance(other, self.__class__) else other
|
|
290
|
+
result = MathOps.abs(self.y)
|
|
207
291
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
208
292
|
|
|
293
|
+
def __or__(self, other):
|
|
294
|
+
if not isinstance(other, self.__class__):
|
|
295
|
+
raise excp.InputTypeError(f"Can only concatenate with another {self.__class__.__name__}")
|
|
296
|
+
|
|
297
|
+
if self.sr != other.sr:
|
|
298
|
+
raise excp.InputValueError(f"Cannot concatenate: Sampling rates differ ({self.sr} vs {other.sr})")
|
|
299
|
+
|
|
300
|
+
# Concatenate raw audio data
|
|
301
|
+
y_cat = np.concatenate([self.y, other.y])
|
|
302
|
+
|
|
303
|
+
# Preserve t0 of the first signal
|
|
304
|
+
new_title = f"{self.title} | {other.title}"
|
|
305
|
+
return self.__class__(y=y_cat, sr=self.sr, t0=self.t0, title=new_title)
|
|
306
|
+
|
|
209
307
|
|
|
210
308
|
#--------------------------
|
|
211
309
|
# Other signal ops
|
|
212
310
|
#--------------------------
|
|
311
|
+
def abs(self) -> Self:
|
|
312
|
+
"""Compute the element-wise abs of the signal data."""
|
|
313
|
+
result = MathOps.abs(self.y)
|
|
314
|
+
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
315
|
+
|
|
213
316
|
def sin(self) -> Self:
|
|
214
317
|
"""Compute the element-wise sine of the signal data."""
|
|
215
|
-
result =
|
|
318
|
+
result = MathOps.sin(self.y)
|
|
216
319
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
217
320
|
|
|
218
321
|
def cos(self) -> Self:
|
|
219
322
|
"""Compute the element-wise cosine of the signal data."""
|
|
220
|
-
result =
|
|
323
|
+
result = MathOps.cos(self.y)
|
|
221
324
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
222
325
|
|
|
223
326
|
def exp(self) -> Self:
|
|
224
327
|
"""Compute the element-wise exponential of the signal data."""
|
|
225
|
-
result =
|
|
328
|
+
result = MathOps.exp(self.y)
|
|
226
329
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
227
330
|
|
|
228
331
|
def tanh(self) -> Self:
|
|
229
332
|
"""Compute the element-wise hyperbolic tangent of the signal data."""
|
|
230
|
-
result =
|
|
333
|
+
result = MathOps.tanh(self.y)
|
|
231
334
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
232
335
|
|
|
233
336
|
def log(self) -> Self:
|
|
234
337
|
"""Compute the element-wise natural logarithm of the signal data."""
|
|
235
|
-
result =
|
|
338
|
+
result = MathOps.log(self.y)
|
|
236
339
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
237
340
|
|
|
238
341
|
def log1p(self) -> Self:
|
|
239
342
|
"""Compute the element-wise natural logarithm of (1 + signal data)."""
|
|
240
|
-
result =
|
|
343
|
+
result = MathOps.log1p(self.y)
|
|
241
344
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
242
345
|
|
|
243
346
|
def log10(self) -> Self:
|
|
244
347
|
"""Compute the element-wise base-10 logarithm of the signal data."""
|
|
245
|
-
result =
|
|
348
|
+
result = MathOps.log10(self.y)
|
|
246
349
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
247
350
|
|
|
248
351
|
def log2(self) -> Self:
|
|
249
352
|
"""Compute the element-wise base-2 logarithm of the signal data."""
|
|
250
|
-
result =
|
|
353
|
+
result = MathOps.log2(self.y)
|
|
251
354
|
return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
|
|
252
355
|
|
|
253
356
|
|
|
254
357
|
#--------------------------
|
|
255
358
|
# Aggregation signal ops
|
|
256
359
|
#--------------------------
|
|
257
|
-
def mean(self) ->
|
|
360
|
+
def mean(self) -> "np.generic":
|
|
258
361
|
"""Compute the mean of the signal data."""
|
|
259
|
-
return
|
|
362
|
+
return MathOps.mean(self.y)
|
|
260
363
|
|
|
261
|
-
def std(self) ->
|
|
364
|
+
def std(self) -> "np.generic":
|
|
262
365
|
"""Compute the standard deviation of the signal data."""
|
|
263
|
-
return
|
|
366
|
+
return MathOps.std(self.y)
|
|
264
367
|
|
|
265
|
-
def min(self) ->
|
|
368
|
+
def min(self) -> "np.generic":
|
|
266
369
|
"""Compute the minimum value in the signal data."""
|
|
267
|
-
return
|
|
370
|
+
return MathOps.min(self.y)
|
|
268
371
|
|
|
269
|
-
def max(self) ->
|
|
372
|
+
def max(self) -> "np.generic":
|
|
270
373
|
"""Compute the maximum value in the signal data."""
|
|
271
|
-
return
|
|
374
|
+
return MathOps.max(self.y)
|
|
272
375
|
|
|
273
|
-
def sum(self) ->
|
|
376
|
+
def sum(self) -> "np.generic":
|
|
274
377
|
"""Compute the sum of the signal data."""
|
|
275
|
-
return
|
|
378
|
+
return MathOps.sum(self.y)
|
|
276
379
|
|
|
277
380
|
#-----------------------------------
|
|
278
381
|
# Repr
|
|
@@ -291,7 +394,7 @@ class TimeDomainSignal(ModusaSignal):
|
|
|
291
394
|
formatter={'float_kind': lambda x: f"{x:.4g}"}
|
|
292
395
|
)
|
|
293
396
|
|
|
294
|
-
return f"Signal({arr_str}, shape={data.shape},
|
|
397
|
+
return f"Signal({arr_str}, shape={data.shape}, type={cls})"
|
|
295
398
|
|
|
296
399
|
def __repr__(self):
|
|
297
400
|
cls = self.__class__.__name__
|
|
@@ -306,4 +409,4 @@ class TimeDomainSignal(ModusaSignal):
|
|
|
306
409
|
formatter={'float_kind': lambda x: f"{x:.4g}"}
|
|
307
410
|
)
|
|
308
411
|
|
|
309
|
-
return f"Signal({arr_str}, shape={data.shape},
|
|
412
|
+
return f"Signal({arr_str}, shape={data.shape}, type={cls})"
|
modusa/tools/__init__.py
ADDED
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
|
|
4
4
|
from modusa import excp
|
|
5
5
|
from modusa.decorators import validate_args_type
|
|
6
|
-
from modusa.
|
|
7
|
-
from typing import Any
|
|
6
|
+
from modusa.tools.base import ModusaTool
|
|
8
7
|
import subprocess
|
|
9
8
|
from pathlib import Path
|
|
10
9
|
|
|
11
10
|
|
|
12
|
-
class AudioConverter(
|
|
11
|
+
class AudioConverter(ModusaTool):
|
|
13
12
|
"""
|
|
14
|
-
Converts audio using FFmpeg.
|
|
13
|
+
Converts audio to any given format using FFmpeg.
|
|
15
14
|
|
|
16
15
|
Note
|
|
17
16
|
----
|
|
@@ -59,6 +58,15 @@ class AudioConverter(ModusaIO):
|
|
|
59
58
|
"""
|
|
60
59
|
inp_audio_fp = Path(inp_audio_fp)
|
|
61
60
|
output_audio_fp = Path(output_audio_fp)
|
|
61
|
+
|
|
62
|
+
if not inp_audio_fp.exists():
|
|
63
|
+
raise excp.FileNotFoundError(f"`inp_audio_fp` does not exist, {inp_audio_fp}")
|
|
64
|
+
|
|
65
|
+
if inp_audio_fp == output_audio_fp:
|
|
66
|
+
raise excp.InputValueError(f"`inp_fp` and `output_fp` must be different")
|
|
67
|
+
|
|
68
|
+
output_audio_fp.parent.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
|
|
62
70
|
|
|
63
71
|
cmd = [
|
|
64
72
|
"ffmpeg",
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from modusa import excp
|
|
5
|
+
from modusa.decorators import validate_args_type
|
|
6
|
+
from modusa.tools.base import ModusaTool
|
|
7
|
+
from IPython.display import display, HTML, Audio
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
class AudioPlayer(ModusaTool):
|
|
11
|
+
"""
|
|
12
|
+
Provides audio player in the jupyter notebook environment.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
#--------Meta Information----------
|
|
16
|
+
_name = "Audio Player"
|
|
17
|
+
_description = ""
|
|
18
|
+
_author_name = "Ankit Anand"
|
|
19
|
+
_author_email = "ankit0.anand0@gmail.com"
|
|
20
|
+
_created_at = "2025-07-08"
|
|
21
|
+
#----------------------------------
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def play(
|
|
25
|
+
y: np.ndarray,
|
|
26
|
+
sr: int,
|
|
27
|
+
regions: list[tuple[float, float]] | None = None,
|
|
28
|
+
title: str | None = None
|
|
29
|
+
) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Plays audio clips for given regions in Jupyter Notebooks.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
y : np.ndarray
|
|
36
|
+
Audio time series.
|
|
37
|
+
sr : int
|
|
38
|
+
Sampling rate.
|
|
39
|
+
regions : list of (float, float), optional
|
|
40
|
+
Regions to extract and play (in seconds).
|
|
41
|
+
title : str, optional
|
|
42
|
+
Title to display above audio players.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
None
|
|
47
|
+
"""
|
|
48
|
+
if not AudioPlayer._in_notebook():
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
if title:
|
|
52
|
+
display(HTML(f"<h4>{title}</h4>"))
|
|
53
|
+
|
|
54
|
+
clip_numbers = []
|
|
55
|
+
timings = []
|
|
56
|
+
players = []
|
|
57
|
+
|
|
58
|
+
if regions:
|
|
59
|
+
for i, (start_sec, end_sec) in enumerate(regions):
|
|
60
|
+
|
|
61
|
+
if start_sec is None:
|
|
62
|
+
start_sec = 0.0
|
|
63
|
+
if end_sec is None:
|
|
64
|
+
end_sec = y.shape[0] / sr
|
|
65
|
+
|
|
66
|
+
start_sample = int(start_sec * sr)
|
|
67
|
+
end_sample = int(end_sec * sr)
|
|
68
|
+
clip = y[start_sample:end_sample]
|
|
69
|
+
audio_tag = Audio(data=clip, rate=sr)._repr_html_()
|
|
70
|
+
|
|
71
|
+
clip_numbers.append(f"<td style='text-align:center; border-right:1px solid #ccc; padding:6px;'>{i+1}</td>")
|
|
72
|
+
timings.append(f"<td style='text-align:center; border-right:1px solid #ccc; padding:6px;'>{start_sec:.2f}s → {end_sec:.2f}s</td>")
|
|
73
|
+
players.append(f"<td style='padding:6px;'>{audio_tag}</td>")
|
|
74
|
+
else:
|
|
75
|
+
total_duration = len(y) / sr
|
|
76
|
+
audio_tag = Audio(data=y, rate=sr)._repr_html_()
|
|
77
|
+
|
|
78
|
+
clip_numbers.append(f"<td style='text-align:center; border-right:1px solid #ccc; padding:6px;'>1</td>")
|
|
79
|
+
timings.append(f"<td style='text-align:center; border-right:1px solid #ccc; padding:6px;'>0.00s → {total_duration:.2f}s</td>")
|
|
80
|
+
players.append(f"<td style='padding:6px;'>{audio_tag}</td>")
|
|
81
|
+
|
|
82
|
+
# Wrap rows in a table with border
|
|
83
|
+
table_html = f"""
|
|
84
|
+
<div style="display:inline-block; border:1px solid #ccc; border-radius:6px; overflow:hidden;">
|
|
85
|
+
<table style="border-collapse:collapse;">
|
|
86
|
+
<tr style="background-color:#f2f2f2;">
|
|
87
|
+
<th style="text-align:left; padding:6px 12px;">Clip</th>
|
|
88
|
+
{''.join(clip_numbers)}
|
|
89
|
+
</tr>
|
|
90
|
+
<tr style="background-color:#fcfcfc;">
|
|
91
|
+
<th style="text-align:left; padding:6px 12px;">Timing</th>
|
|
92
|
+
{''.join(timings)}
|
|
93
|
+
</tr>
|
|
94
|
+
<tr>
|
|
95
|
+
<th style="text-align:left; padding:6px 12px;">Player</th>
|
|
96
|
+
{''.join(players)}
|
|
97
|
+
</tr>
|
|
98
|
+
</table>
|
|
99
|
+
</div>
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
return HTML(table_html)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def _in_notebook() -> bool:
|
|
107
|
+
try:
|
|
108
|
+
from IPython import get_ipython
|
|
109
|
+
shell = get_ipython()
|
|
110
|
+
return shell and shell.__class__.__name__ == "ZMQInteractiveShell"
|
|
111
|
+
except ImportError:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
|
modusa/tools/base.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
class ModusaTool(ABC):
|
|
6
|
+
"""
|
|
7
|
+
Base class for all tool: youtube downloader, audio converter, filter.
|
|
8
|
+
|
|
9
|
+
>>> modusa-dev create io
|
|
10
|
+
|
|
11
|
+
.. code-block:: python
|
|
12
|
+
|
|
13
|
+
# General template of a subclass of ModusaTool
|
|
14
|
+
from modusa.tools.base import ModusaTool
|
|
15
|
+
|
|
16
|
+
class MyCustomIOClass(ModusaIO):
|
|
17
|
+
#--------Meta Information----------
|
|
18
|
+
_name = "My Custom Tool"
|
|
19
|
+
_description = "My custom class for Tool."
|
|
20
|
+
_author_name = "Ankit Anand"
|
|
21
|
+
_author_email = "ankit0.anand0@gmail.com"
|
|
22
|
+
_created_at = "2025-07-06"
|
|
23
|
+
#----------------------------------
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def do_something():
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
Note
|
|
31
|
+
----
|
|
32
|
+
- This class is intended to be subclassed by any tool built for the modusa framework.
|
|
33
|
+
- In order to create a tool, you can use modusa-dev CLI to generate a template.
|
|
34
|
+
- It is recommended to treat subclasses of ModusaTool as namespaces and define @staticmethods with control parameters, rather than using instance-level __init__ methods.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
#--------Meta Information----------
|
|
38
|
+
_name: str = "Modusa Tool"
|
|
39
|
+
_description: str = "Base class for any tool in the Modusa framework."
|
|
40
|
+
_author_name = "Ankit Anand"
|
|
41
|
+
_author_email = "ankit0.anand0@gmail.com"
|
|
42
|
+
_created_at = "2025-07-11"
|
|
43
|
+
#----------------------------------
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from modusa import excp
|
|
5
|
+
from modusa.decorators import validate_args_type
|
|
6
|
+
from modusa.tools.base import ModusaTool
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FourierTransform(ModusaTool):
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
#--------Meta Information----------
|
|
15
|
+
_name = ""
|
|
16
|
+
_description = ""
|
|
17
|
+
_author_name = "Ankit Anand"
|
|
18
|
+
_author_email = "ankit0.anand0@gmail.com"
|
|
19
|
+
_created_at = "2025-07-11"
|
|
20
|
+
#----------------------------------
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
super().__init__()
|
|
24
|
+
|