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.
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.22.dist-info → modusa-0.2.23.dist-info}/METADATA +1 -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.22.dist-info/RECORD +0 -47
  58. {modusa-0.2.22.dist-info → modusa-0.2.23.dist-info}/WHEEL +0 -0
  59. {modusa-0.2.22.dist-info → modusa-0.2.23.dist-info}/entry_points.txt +0 -0
  60. {modusa-0.2.22.dist-info → modusa-0.2.23.dist-info}/licenses/LICENSE.md +0 -0
@@ -14,11 +14,7 @@ class FrequencyDomainSignal(ModusaSignal):
14
14
 
15
15
  Note
16
16
  ----
17
- - The class is not intended to be instantiated directly
18
- This class stores the Complex spectrum of a signal
19
- along with its corresponding frequency axis. It optionally tracks the time
20
- origin (`t0`) of the spectral slice, which is useful when working with
21
- time-localized spectral data (e.g., from a spectrogram or short-time Fourier transform).
17
+ - The class is not intended to be instantiated directly.
22
18
 
23
19
  Parameters
24
20
  ----------
@@ -32,8 +28,8 @@ class FrequencyDomainSignal(ModusaSignal):
32
28
  An optional title for display or plotting purposes.
33
29
  """
34
30
  #--------Meta Information----------
35
- _name = ""
36
- _description = ""
31
+ _name = "Frequency Domain Signal"
32
+ _description = "Represents Frequency Domain Signal"
37
33
  _author_name = "Ankit Anand"
38
34
  _author_email = "ankit0.anand0@gmail.com"
39
35
  _created_at = "2025-07-09"
@@ -46,7 +42,6 @@ class FrequencyDomainSignal(ModusaSignal):
46
42
  if spectrum.shape != f.shape:
47
43
  raise excp.InputValueError(f"`spectrum` and `f` shape must match, got {spectrum.shape} and {f.shape}")
48
44
 
49
-
50
45
  self._spectrum = spectrum
51
46
  self._f = f
52
47
  self._t0 = float(t0)
@@ -73,14 +68,33 @@ class FrequencyDomainSignal(ModusaSignal):
73
68
  """Time origin (in seconds) of this spectral slice, e.g., from a spectrogram frame."""
74
69
  return self._t0
75
70
 
76
- def __len__(self):
77
- return len(self._y)
71
+ #----------------------
72
+ # Derived Properties
73
+ #----------------------
74
+ @immutable_property("Create a new object instead.")
75
+ def __len__(self) -> int:
76
+ return len(self.spectrum)
77
+
78
+ @immutable_property("Create a new object instead.")
79
+ def ndim(self) -> int:
80
+ return self.spectrum.ndim
78
81
 
82
+ @immutable_property("Create a new object instead.")
83
+ def shape(self) -> tuple:
84
+ return self.spectrum.shape
79
85
 
80
86
  #----------------------
81
- # Tools
87
+ # Methods
82
88
  #----------------------
83
89
 
90
+ def print_info(self) -> None:
91
+ """Prints info about the audio."""
92
+ print("-" * 50)
93
+ print(f"{'Title':<20}: {self.title}")
94
+ print(f"{'Type':<20}: {self._name}")
95
+ print(f"{'Frequency Range':<20}: ({self.f[0]:.2f}, {self.f[-1]:.2f}) Hz")
96
+ print("-" * 50)
97
+
84
98
  def __getitem__(self, key: slice) -> Self:
85
99
  sliced_spectrum = self._spectrum[key]
86
100
  sliced_f = self._f[key]
@@ -89,74 +103,89 @@ class FrequencyDomainSignal(ModusaSignal):
89
103
  @validate_args_type()
90
104
  def plot(
91
105
  self,
92
- scale_y: tuple[float, float] | None = None,
93
106
  ax: plt.Axes | None = None,
94
- color: str = "b",
95
- marker: str | None = None,
96
- linestyle: str | None = None,
97
- stem: bool | None = False,
98
- legend_loc: str | None = None,
107
+ fmt: str = "k-",
99
108
  title: str | None = None,
100
- ylabel: str | None = "Amplitude",
101
- xlabel: str | None = "Freq (Hz)",
109
+ label: str | None = None,
110
+ ylabel: str | None = "Strength",
111
+ xlabel: str | None = "Frequency (Hz)",
102
112
  ylim: tuple[float, float] | None = None,
103
113
  xlim: tuple[float, float] | None = None,
104
114
  highlight: list[tuple[float, float]] | None = None,
105
- ) -> plt.Figure:
115
+ vlines: list[float] | None = None,
116
+ hlines: list[float] | None = None,
117
+ show_grid: bool = False,
118
+ stem: bool | None = False,
119
+ legend_loc: str | None = None,
120
+ ) -> plt.Figure | None:
106
121
  """
107
- Plot the frequency-domain signal as a line or stem plot.
122
+ Plot the audio waveform using matplotlib.
108
123
 
109
124
  .. code-block:: python
110
-
111
- spectrum.plot(stem=True, color="r", title="FFT Frame", xlim=(0, 5000))
125
+
126
+ from modusa.generators import AudioSignalGenerator
127
+ audio_example = AudioSignalGenerator.generate_example()
128
+ audio_example.plot(color="orange", title="Example Audio")
112
129
 
113
130
  Parameters
114
131
  ----------
115
- scale_y : tuple[float, float], optional
116
- Range to scale the spectrum values before plotting (min, max).
117
- ax : matplotlib.axes.Axes, optional
118
- Axis to plot on. If None, a new figure and axis are created.
119
- color : str, default="b"
120
- Color of the line or stem.
121
- marker : str, optional
122
- Marker style for points (ignored if stem=True).
123
- linestyle : str, optional
124
- Line style for the plot (ignored if stem=True).
125
- stem : bool, default=False
126
- Whether to use a stem plot instead of a line plot.
127
- legend_loc : str, optional
128
- Legend location (e.g., 'upper right'). If None, no legend is shown.
129
- title : str, optional
130
- Title of the plot. Defaults to signal title.
131
- ylabel : str, default="Amplitude"
132
- Label for the y-axis.
133
- xlabel : str, default="Freq (Hz)"
134
- Label for the x-axis.
135
- ylim : tuple[float, float], optional
132
+ ax : matplotlib.axes.Axes | None
133
+ Pre-existing axes to plot into. If None, a new figure and axes are created.
134
+ fmt : str | None
135
+ Format of the plot as per matplotlib standards (Eg. "k-" or "blue--o)
136
+ title : str | None
137
+ Plot title. Defaults to the signal’s title.
138
+ label: str | None
139
+ Label for the plot, shown as legend.
140
+ ylabel : str | None
141
+ Label for the y-axis. Defaults to `"Strength"`.
142
+ xlabel : str | None
143
+ Label for the x-axis. Defaults to `"Frequency (Hz)"`.
144
+ ylim : tuple[float, float] | None
136
145
  Limits for the y-axis.
137
- xlim : tuple[float, float], optional
146
+ xlim : tuple[float, float] | None
147
+ highlight : list[tuple[float, float]] | None
148
+ List of frequency intervals to highlight on the plot, each as (start, end).
149
+ vlines: list[float]
150
+ List of x values to draw vertical lines. (Eg. [10, 13.5])
151
+ hlines: list[float]
152
+ List of y values to draw horizontal lines. (Eg. [10, 13.5])
153
+ show_grid: bool
154
+ If true, shows grid.
155
+ stem : bool
156
+ If True, use a stem plot instead of a continuous line. Autorejects if signal is too large.
157
+ legend_loc : str | None
158
+ If provided, adds a legend at the specified location (e.g., "upper right" or "best").
138
159
  Limits for the x-axis.
139
- highlight : list[tuple[float, float]], optional
140
- Regions to highlight on the frequency axis as shaded spans.
141
160
 
142
161
  Returns
143
162
  -------
144
- matplotlib.figure.Figure
145
- The figure containing the plotted signal.
146
-
147
- Note
148
- ----
149
- - If `ax` is provided, the plot is drawn on it; otherwise, a new figure is created.
150
- - `highlight` can be used to emphasize frequency bands (e.g., formants, harmonics).
151
- - Use `scale_y` to clip or normalize extreme values before plotting.
163
+ matplotlib.figure.Figure | None
164
+ The figure object containing the plot or None in case an axis is provided.
152
165
  """
153
166
 
154
-
155
- from modusa.io import Plotter
167
+ from modusa.tools.plotter import Plotter
156
168
 
157
169
  title = title or self.title
158
170
 
159
- fig: plt.Figure | None = Plotter.plot_signal(y=self.spectrum, x=self.f, 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)
171
+ fig: plt.Figure | None = Plotter.plot_signal(
172
+ y=self.spectrum,
173
+ x=self.f,
174
+ ax=ax,
175
+ fmt=fmt,
176
+ title=title,
177
+ label=label,
178
+ ylabel=ylabel,
179
+ xlabel=xlabel,
180
+ ylim=ylim,
181
+ xlim=xlim,
182
+ highlight=highlight,
183
+ vlines=vlines,
184
+ hlines=hlines,
185
+ show_grid=show_grid,
186
+ stem=stem,
187
+ legend_loc=legend_loc,
188
+ )
160
189
 
161
190
  return fig
162
191
 
@@ -167,131 +196,149 @@ class FrequencyDomainSignal(ModusaSignal):
167
196
  def __array__(self, dtype=None):
168
197
  return np.asarray(self.spectrum, dtype=dtype)
169
198
 
199
+ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
200
+ if method == "__call__":
201
+ input_arrays = [x.spectrum if isinstance(x, self.__class__) else x for x in inputs]
202
+ result = ufunc(*input_arrays, **kwargs)
203
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
204
+ return NotImplemented
205
+
170
206
  def __add__(self, other):
171
207
  other_data = other.spectrum if isinstance(other, self.__class__) else other
172
- result = np.add(self.spectrum, other_data)
208
+ result = MathOps.add(self.spectrum, other_data)
173
209
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
174
210
 
175
211
  def __radd__(self, other):
176
- result = np.add(other, self.spectrum)
212
+ other_data = other.spectrum if isinstance(other, self.__class__) else other
213
+ result = MathOps.add(other_data, self.spectrum)
177
214
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
178
215
 
179
216
  def __sub__(self, other):
180
217
  other_data = other.spectrum if isinstance(other, self.__class__) else other
181
- result = np.subtract(self.spectrum, other_data)
218
+ result = MathOps.subtract(self.spectrum, other_data)
182
219
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
183
220
 
184
221
  def __rsub__(self, other):
185
- result = np.subtract(other, self.spectrum)
222
+ other_data = other.spectrum if isinstance(other, self.__class__) else other
223
+ result = MathOps.subtract(other_data, self.spectrum)
186
224
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
187
225
 
188
226
  def __mul__(self, other):
189
227
  other_data = other.spectrum if isinstance(other, self.__class__) else other
190
- result = np.multiply(self.spectrum, other_data)
228
+ result = MathOps.multiply(self.spectrum, other_data)
191
229
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
192
230
 
193
231
  def __rmul__(self, other):
194
- result = np.multiply(other, self.spectrum)
232
+ other_data = other.spectrum if isinstance(other, self.__class__) else other
233
+ result = MathOps.multiply(other_data, self.spectrum)
195
234
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
196
235
 
197
236
  def __truediv__(self, other):
198
237
  other_data = other.spectrum if isinstance(other, self.__class__) else other
199
- result = np.true_divide(self.spectrum, other_data)
238
+ result = MathOps.divide(self.spectrum, other_data)
200
239
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
201
240
 
202
241
  def __rtruediv__(self, other):
203
- result = np.true_divide(other, self.spectrum)
242
+ other_data = other.spectrum if isinstance(other, self.__class__) else other
243
+ result = MathOps.divide(other_data, self.spectrum)
204
244
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
205
245
 
206
246
  def __floordiv__(self, other):
207
247
  other_data = other.spectrum if isinstance(other, self.__class__) else other
208
- result = np.floor_divide(self.spectrum, other_data)
248
+ result = MathOps.floor_divide(self.spectrum, other_data)
209
249
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
210
250
 
211
251
  def __rfloordiv__(self, other):
212
- result = np.floor_divide(other, self.spectrum)
252
+ other_data = other.spectrum if isinstance(other, self.__class__) else other
253
+ result = MathOps.floor_divide(other_data, self.spectrum)
213
254
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
214
255
 
215
256
  def __pow__(self, other):
216
257
  other_data = other.spectrum if isinstance(other, self.__class__) else other
217
- result = np.power(self.spectrum, other_data)
258
+ result = MathOps.power(self.spectrum, other_data)
218
259
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
219
260
 
220
261
  def __rpow__(self, other):
221
- result = np.power(other, self.spectrum)
262
+ other_data = other.spectrum if isinstance(other, self.__class__) else other
263
+ result = MathOps.power(other_data, self.spectrum)
222
264
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
223
265
 
224
266
  def __abs__(self):
225
- result = np.abs(self.spectrum)
226
- return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
227
-
267
+ other_data = other.spectrum if isinstance(other, self.__class__) else other
268
+ result = MathOps.abs(self.spectrum)
269
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
228
270
 
229
271
  #--------------------------
230
272
  # Other signal ops
231
273
  #--------------------------
274
+ def abs(self) -> Self:
275
+ """Compute the element-wise abs of the signal data."""
276
+ result = MathOps.abs(self.y)
277
+ return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
278
+
232
279
  def sin(self) -> Self:
233
280
  """Compute the element-wise sine of the signal data."""
234
- result = np.sin(self.spectrum)
281
+ result = MathOps.sin(self.y)
235
282
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
236
283
 
237
284
  def cos(self) -> Self:
238
285
  """Compute the element-wise cosine of the signal data."""
239
- result = np.cos(self.spectrum)
286
+ result = MathOps.cos(self.y)
240
287
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
241
288
 
242
289
  def exp(self) -> Self:
243
290
  """Compute the element-wise exponential of the signal data."""
244
- result = np.exp(self.spectrum)
291
+ result = MathOps.exp(self.y)
245
292
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
246
293
 
247
294
  def tanh(self) -> Self:
248
295
  """Compute the element-wise hyperbolic tangent of the signal data."""
249
- result = np.tanh(self.spectrum)
296
+ result = MathOps.tanh(self.y)
250
297
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
251
298
 
252
299
  def log(self) -> Self:
253
300
  """Compute the element-wise natural logarithm of the signal data."""
254
- result = np.log(self.spectrum)
301
+ result = MathOps.log(self.y)
255
302
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
256
303
 
257
304
  def log1p(self) -> Self:
258
305
  """Compute the element-wise natural logarithm of (1 + signal data)."""
259
- result = np.log1p(self.spectrum)
306
+ result = MathOps.log1p(self.y)
260
307
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
261
308
 
262
309
  def log10(self) -> Self:
263
310
  """Compute the element-wise base-10 logarithm of the signal data."""
264
- result = np.log10(self.spectrum)
311
+ result = MathOps.log10(self.y)
265
312
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
266
313
 
267
314
  def log2(self) -> Self:
268
315
  """Compute the element-wise base-2 logarithm of the signal data."""
269
- result = np.log2(self.spectrum)
316
+ result = MathOps.log2(self.y)
270
317
  return self.__class__(spectrum=result, f=self.f, t0=self.t0, title=self.title)
271
318
 
272
319
 
273
320
  #--------------------------
274
321
  # Aggregation signal ops
275
322
  #--------------------------
276
- def mean(self) -> float:
323
+ def mean(self) -> "np.generic":
277
324
  """Compute the mean of the signal data."""
278
- return float(np.mean(self.spectrum))
325
+ return MathOps.mean(self.spectrum)
279
326
 
280
- def std(self) -> float:
327
+ def std(self) -> "np.generic":
281
328
  """Compute the standard deviation of the signal data."""
282
- return float(np.std(self.spectrum))
329
+ return MathOps.std(self.spectrum)
283
330
 
284
- def min(self) -> float:
331
+ def min(self) -> "np.generic":
285
332
  """Compute the minimum value in the signal data."""
286
- return float(np.min(self.spectrum))
333
+ return MathOps.min(self.spectrum)
287
334
 
288
- def max(self) -> float:
335
+ def max(self) -> "np.generic":
289
336
  """Compute the maximum value in the signal data."""
290
- return float(np.max(self.spectrum))
337
+ return MathOps.max(self.spectrum)
291
338
 
292
- def sum(self) -> float:
339
+ def sum(self) -> "np.generic":
293
340
  """Compute the sum of the signal data."""
294
- return float(np.sum(self.spectrum))
341
+ return MathOps.sum(self.spectrum)
295
342
 
296
343
  #-----------------------------------
297
344
  # Repr
@@ -310,7 +357,7 @@ class FrequencyDomainSignal(ModusaSignal):
310
357
  formatter={'float_kind': lambda x: f"{x:.4g}"}
311
358
  )
312
359
 
313
- return f"Signal({arr_str}, shape={data.shape}, kind={cls})"
360
+ return f"Signal({arr_str}, shape={data.shape}, type={cls})"
314
361
 
315
362
  def __repr__(self):
316
363
  cls = self.__class__.__name__
@@ -325,5 +372,5 @@ class FrequencyDomainSignal(ModusaSignal):
325
372
  formatter={'float_kind': lambda x: f"{x:.4g}"}
326
373
  )
327
374
 
328
- return f"Signal({arr_str}, shape={data.shape}, kind={cls})"
375
+ return f"Signal({arr_str}, shape={data.shape}, type={cls})"
329
376