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.
Files changed (60) hide show
  1. modusa/decorators.py +4 -4
  2. modusa/devtools/docs/source/generators/audio_waveforms.rst +8 -0
  3. modusa/devtools/docs/source/generators/base.rst +8 -0
  4. modusa/devtools/docs/source/generators/index.rst +8 -0
  5. modusa/devtools/docs/source/io/audio_loader.rst +8 -0
  6. modusa/devtools/docs/source/io/base.rst +8 -0
  7. modusa/devtools/docs/source/io/index.rst +8 -0
  8. modusa/devtools/docs/source/plugins/base.rst +8 -0
  9. modusa/devtools/docs/source/plugins/index.rst +7 -0
  10. modusa/devtools/docs/source/signals/audio_signal.rst +8 -0
  11. modusa/devtools/docs/source/signals/base.rst +8 -0
  12. modusa/devtools/docs/source/signals/frequency_domain_signal.rst +8 -0
  13. modusa/devtools/docs/source/signals/index.rst +11 -0
  14. modusa/devtools/docs/source/signals/spectrogram.rst +8 -0
  15. modusa/devtools/docs/source/signals/time_domain_signal.rst +8 -0
  16. modusa/devtools/docs/source/tools/audio_converter.rst +8 -0
  17. modusa/devtools/docs/source/tools/audio_player.rst +8 -0
  18. modusa/devtools/docs/source/tools/base.rst +8 -0
  19. modusa/devtools/docs/source/tools/fourier_tranform.rst +8 -0
  20. modusa/devtools/docs/source/tools/index.rst +13 -0
  21. modusa/devtools/docs/source/tools/math_ops.rst +8 -0
  22. modusa/devtools/docs/source/tools/plotter.rst +8 -0
  23. modusa/devtools/docs/source/tools/youtube_downloader.rst +8 -0
  24. modusa/devtools/generate_doc_source.py +96 -0
  25. modusa/devtools/generate_template.py +8 -8
  26. modusa/devtools/main.py +3 -2
  27. modusa/devtools/templates/test.py +2 -3
  28. modusa/devtools/templates/{engine.py → tool.py} +3 -8
  29. modusa/generators/__init__.py +0 -2
  30. modusa/generators/audio_waveforms.py +22 -13
  31. modusa/generators/base.py +1 -1
  32. modusa/io/__init__.py +1 -5
  33. modusa/io/audio_loader.py +3 -33
  34. modusa/main.py +0 -30
  35. modusa/signals/__init__.py +1 -5
  36. modusa/signals/audio_signal.py +181 -124
  37. modusa/signals/base.py +1 -8
  38. modusa/signals/frequency_domain_signal.py +140 -93
  39. modusa/signals/spectrogram.py +197 -98
  40. modusa/signals/time_domain_signal.py +177 -74
  41. modusa/tools/__init__.py +2 -0
  42. modusa/{io → tools}/audio_converter.py +12 -4
  43. modusa/tools/audio_player.py +114 -0
  44. modusa/tools/base.py +43 -0
  45. modusa/tools/fourier_tranform.py +24 -0
  46. modusa/tools/math_ops.py +232 -0
  47. modusa/{io → tools}/plotter.py +155 -42
  48. modusa/{io → tools}/youtube_downloader.py +2 -2
  49. modusa/utils/excp.py +9 -42
  50. {modusa-0.2.21.dist-info → modusa-0.2.23.dist-info}/METADATA +2 -1
  51. modusa-0.2.23.dist-info/RECORD +70 -0
  52. modusa/engines/.DS_Store +0 -0
  53. modusa/engines/__init__.py +0 -3
  54. modusa/engines/base.py +0 -14
  55. modusa/io/audio_player.py +0 -72
  56. modusa/signals/signal_ops.py +0 -158
  57. modusa-0.2.21.dist-info/RECORD +0 -47
  58. {modusa-0.2.21.dist-info → modusa-0.2.23.dist-info}/WHEEL +0 -0
  59. {modusa-0.2.21.dist-info → modusa-0.2.23.dist-info}/entry_points.txt +0 -0
  60. {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
- This class is specifically designed to hold 1D signals that result
16
- from slicing a 2D representation like a spectrogram. For example,
17
- if you have a spectrogram `S` and you perform `S[10, :]`, the result
18
- is a 1D signal over time, this class provides a clean and consistent
19
- way to handle such slices.
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
- return self.t0 + np.arange(len(self._y)) / self.sr
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
- # Tools
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
- color: str = "b",
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
- ) -> plt.Figure:
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 time-domain signal.
144
+ Plot the audio waveform using matplotlib.
104
145
 
105
146
  .. code-block:: python
106
-
107
- signal.plot(color='g', marker='o', stem=True)
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
- scale_y : tuple[float, float], optional
112
- Min-max values to scale the y-axis data.
113
- ax : matplotlib.axes.Axes, optional
114
- Axes to plot on; if None, creates a new figure.
115
- color : str, default='b'
116
- Line or stem color.
117
- marker : str, optional
118
- Marker style for each data point.
119
- linestyle : str, optional
120
- Line style to use if not using stem plot.
121
- stem : bool, default=False
122
- Whether to draw a stem plot instead of a line plot.
123
- legend_loc : str, optional
124
- If given, adds a legend at the specified location.
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.axes.Axes
129
- The axes object containing the plot.
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.io import Plotter
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(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)
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 = np.add(self.y, other_data)
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
- result = np.add(other, self.y)
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 = np.subtract(self.y, other_data)
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
- result = np.subtract(other, self.y)
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 = np.multiply(self.y, other_data)
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
- result = np.multiply(other, self.y)
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 = np.true_divide(self.y, other_data)
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
- result = np.true_divide(other, self.y)
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 = np.floor_divide(self.y, other_data)
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
- result = np.floor_divide(other, self.y)
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 = np.power(self.y, other_data)
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
- result = np.power(other, self.y)
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
- result = np.abs(self.y)
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 = np.sin(self.y)
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 = np.cos(self.y)
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 = np.exp(self.y)
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 = np.tanh(self.y)
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 = np.log(self.y)
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 = np.log1p(self.y)
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 = np.log10(self.y)
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 = np.log2(self.y)
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) -> float:
360
+ def mean(self) -> "np.generic":
258
361
  """Compute the mean of the signal data."""
259
- return float(np.mean(self.y))
362
+ return MathOps.mean(self.y)
260
363
 
261
- def std(self) -> float:
364
+ def std(self) -> "np.generic":
262
365
  """Compute the standard deviation of the signal data."""
263
- return float(np.std(self.y))
366
+ return MathOps.std(self.y)
264
367
 
265
- def min(self) -> float:
368
+ def min(self) -> "np.generic":
266
369
  """Compute the minimum value in the signal data."""
267
- return float(np.min(self.y))
370
+ return MathOps.min(self.y)
268
371
 
269
- def max(self) -> float:
372
+ def max(self) -> "np.generic":
270
373
  """Compute the maximum value in the signal data."""
271
- return float(np.max(self.y))
374
+ return MathOps.max(self.y)
272
375
 
273
- def sum(self) -> float:
376
+ def sum(self) -> "np.generic":
274
377
  """Compute the sum of the signal data."""
275
- return float(np.sum(self.y))
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}, kind={cls})"
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}, kind={cls})"
412
+ return f"Signal({arr_str}, shape={data.shape}, type={cls})"
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env python3
2
+
@@ -3,15 +3,14 @@
3
3
 
4
4
  from modusa import excp
5
5
  from modusa.decorators import validate_args_type
6
- from modusa.io.base import ModusaIO
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(ModusaIO):
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
+