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
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
- )
@@ -1,7 +1,3 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- from .base import ModusaSignal
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
@@ -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.signals.signal_ops import SignalOps
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. Set to `t[0]` if `t` is provided.
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 | None = None, t: np.ndarray | None = None, t0: float = 0.0, title: str | None = None):
50
-
51
- if y.ndim != 1:
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
- """Audio data."""
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
- """Sampling rate of the audio."""
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
- """Start timestamp of the audio."""
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) -> int:
82
+ def Ts(self) -> float:
96
83
  """Sampling Period of the audio."""
97
- return 1.0 / self.sr
84
+ return 1. / self.sr
98
85
 
99
86
  @immutable_property("Mutation not allowed.")
100
- def duration(self) -> int:
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 info(self) -> None:
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"{'Kind':<20}: {self._name}")
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
- 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
- )
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
- color: str = "b",
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
- ) -> plt.Figure:
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
- 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
204
+ ax : matplotlib.axes.Axes | None
199
205
  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
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
- ylabel : str or None, optional
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 or None, optional
214
+ xlabel : str | None
215
215
  Label for the x-axis. Defaults to `"Time (sec)"`.
216
- ylim : tuple of float or None, optional
216
+ ylim : tuple[float, float] | None
217
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
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.io import Plotter
239
+ from modusa.tools.plotter import Plotter
230
240
 
231
- title = title or self.title
241
+ if title is None:
242
+ title = self.title
232
243
 
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)
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
- 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.
288
+ See Also
289
+ --------
290
+ :class:`~modusa.tools.audio_player.AudioPlayer`
264
291
  """
265
292
 
266
- from modusa.io import AudioPlayer
267
- audio_player = AudioPlayer.play(y=self.y, sr=self.sr, regions=regions, title=self.title)
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
- t += self.t0
304
- spec = Spectrogram(S=S, f=f, t=t)
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 ufunc == np.abs and method == "__call__":
320
- # Extract the actual array from self or others
321
- result = ufunc(self.y, **kwargs)
322
- return self.__class__(y=result, sr=self.sr, title=f"{self.title} (abs)")
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 = np.add(self.y, other_data)
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
- result = np.add(other, self.y)
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 = np.subtract(self.y, other_data)
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
- result = np.subtract(other, self.y)
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 = np.multiply(self.y, other_data)
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
- result = np.multiply(other, self.y)
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 = np.true_divide(self.y, other_data)
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
- result = np.true_divide(other, self.y)
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._y if isinstance(other, self.__class__) else other
363
- result = np.floor_divide(self.y, other_data)
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
- result = np.floor_divide(other, self.y)
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 = np.power(self.y, other_data)
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
- result = np.power(other, self.y)
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
- result = np.abs(self.y)
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 = np.sin(self.y)
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 = np.cos(self.y)
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 = np.exp(self.y)
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 = np.tanh(self.y)
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 = np.log(self.y)
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 = np.log1p(self.y)
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 = np.log10(self.y)
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 = np.log2(self.y)
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) -> float:
488
+ def mean(self) -> "np.generic":
432
489
  """Compute the mean of the signal data."""
433
- return float(np.mean(self.y))
490
+ return MathOps.mean(self.y)
434
491
 
435
- def std(self) -> float:
492
+ def std(self) -> "np.generic":
436
493
  """Compute the standard deviation of the signal data."""
437
- return float(np.std(self.y))
494
+ return MathOps.std(self.y)
438
495
 
439
- def min(self) -> float:
496
+ def min(self) -> "np.generic":
440
497
  """Compute the minimum value in the signal data."""
441
- return float(np.min(self.y))
498
+ return MathOps.min(self.y)
442
499
 
443
- def max(self) -> float:
500
+ def max(self) -> "np.generic":
444
501
  """Compute the maximum value in the signal data."""
445
- return float(np.max(self.y))
502
+ return MathOps.max(self.y)
446
503
 
447
- def sum(self) -> float:
504
+ def sum(self) -> "np.generic":
448
505
  """Compute the sum of the signal data."""
449
- return float(np.sum(self.y))
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}, kind={cls})"
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}, kind={cls})"
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
- from modusa import excp
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