modusa 0.1.0__py3-none-any.whl → 0.2.0__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 (48) hide show
  1. modusa/.DS_Store +0 -0
  2. modusa/decorators.py +5 -5
  3. modusa/devtools/generate_template.py +80 -15
  4. modusa/devtools/main.py +6 -4
  5. modusa/devtools/templates/{engines.py → engine.py} +8 -7
  6. modusa/devtools/templates/{generators.py → generator.py} +8 -10
  7. modusa/devtools/templates/io.py +24 -0
  8. modusa/devtools/templates/{plugins.py → plugin.py} +7 -6
  9. modusa/devtools/templates/signal.py +40 -0
  10. modusa/devtools/templates/test.py +11 -0
  11. modusa/engines/.DS_Store +0 -0
  12. modusa/engines/__init__.py +1 -2
  13. modusa/generators/__init__.py +3 -1
  14. modusa/generators/audio_waveforms.py +227 -0
  15. modusa/generators/base.py +14 -25
  16. modusa/io/__init__.py +9 -0
  17. modusa/io/audio_converter.py +76 -0
  18. modusa/io/audio_loader.py +212 -0
  19. modusa/io/audio_player.py +72 -0
  20. modusa/io/base.py +43 -0
  21. modusa/io/plotter.py +430 -0
  22. modusa/io/youtube_downloader.py +139 -0
  23. modusa/main.py +15 -17
  24. modusa/plugins/__init__.py +1 -7
  25. modusa/signals/__init__.py +4 -6
  26. modusa/signals/audio_signal.py +421 -175
  27. modusa/signals/base.py +11 -271
  28. modusa/signals/frequency_domain_signal.py +329 -0
  29. modusa/signals/signal_ops.py +158 -0
  30. modusa/signals/spectrogram.py +465 -0
  31. modusa/signals/time_domain_signal.py +309 -0
  32. modusa/utils/excp.py +5 -0
  33. {modusa-0.1.0.dist-info → modusa-0.2.0.dist-info}/METADATA +15 -10
  34. modusa-0.2.0.dist-info/RECORD +47 -0
  35. modusa/devtools/templates/signals.py +0 -63
  36. modusa/engines/plot_1dsignal.py +0 -130
  37. modusa/engines/plot_2dmatrix.py +0 -159
  38. modusa/generators/basic_waveform.py +0 -185
  39. modusa/plugins/plot_1dsignal.py +0 -59
  40. modusa/plugins/plot_2dmatrix.py +0 -76
  41. modusa/plugins/plot_time_domain_signal.py +0 -59
  42. modusa/signals/signal1d.py +0 -311
  43. modusa/signals/signal2d.py +0 -226
  44. modusa/signals/uniform_time_domain_signal.py +0 -212
  45. modusa-0.1.0.dist-info/RECORD +0 -41
  46. {modusa-0.1.0.dist-info → modusa-0.2.0.dist-info}/WHEEL +0 -0
  47. {modusa-0.1.0.dist-info → modusa-0.2.0.dist-info}/entry_points.txt +0 -0
  48. {modusa-0.1.0.dist-info → modusa-0.2.0.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.signals.signal_ops import SignalOps
7
8
  from typing import Self, Any
8
9
  import numpy as np
9
10
  import matplotlib.pyplot as plt
@@ -11,220 +12,465 @@ from pathlib import Path
11
12
 
12
13
  class AudioSignal(ModusaSignal):
13
14
  """
15
+ Represents a 1D audio signal within modusa framework.
14
16
 
17
+ Note
18
+ ----
19
+ - It is highly recommended to use :class:`~modusa.io.AudioLoader` to instantiate an object of this class.
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
+
26
+ Parameters
27
+ ----------
28
+ y : np.ndarray
29
+ 1D numpy array representing the audio signal.
30
+ sr : int | None
31
+ 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
+ t0 : float, optional
35
+ Starting time in seconds. Defaults to 0.0. Set to `t[0]` if `t` is provided.
36
+ title : str | None, optional
37
+ Optional title for the signal. Defaults to `"Audio Signal"`.
15
38
  """
16
39
 
17
40
  #--------Meta Information----------
18
- name = "Audio Signal"
19
- description = ""
20
- author_name = "Ankit Anand"
21
- author_email = "ankit0.anand0@gmail.com"
22
- created_at = "2025-07-04"
41
+ _name = "Audio Signal"
42
+ _description = ""
43
+ _author_name = "Ankit Anand"
44
+ _author_email = "ankit0.anand0@gmail.com"
45
+ _created_at = "2025-07-04"
23
46
  #----------------------------------
24
47
 
25
48
  @validate_args_type()
26
- def __init__(self, y: np.ndarray, t: np.ndarray | None = None):
27
-
28
- if y.ndim != 1: # Mono
29
- raise excp.InputValueError(f"`y` must have 1 dimension not {y.ndim}.")
30
- if t.ndim != 1:
31
- raise excp.InputValueError(f"`t` must have 1 dimension not {t.ndim}.")
32
-
33
- if t is None:
34
- t = np.arange(y.shape[0])
35
- else:
36
- if t.shape[0] != y.shape[0]:
37
- raise excp.InputValueError(f"`y` and `t` must have same shape.")
38
- dts = np.diff(t)
39
- if not np.allclose(dts, dts[0]):
40
- raise excp.InputValueError("`t` must be equally spaced")
41
-
42
- super().__init__(data=y, data_idx=t) # Instantiating `ModusaSignal` class
43
-
44
- self._y_unit = ""
45
- self._t_unit = "sec"
46
-
47
- self._title = "Audio Signal"
48
- self._y_label = "Amplitude"
49
- self._t_label = "Time"
50
-
51
- def _with_data(self, new_data: np.ndarray, new_data_idx: np.ndarray) -> Self:
52
- """Subclasses must override this to return a copy with new data."""
53
- new_signal = self.__class__(y=new_data, t=new_data_idx)
54
- new_signal.set_units(y_unit=self.y_unit, t_unit=self.t_unit)
55
- new_signal.set_plot_labels(title=self.title, y_label=self.y_label, t_label=self.t_label)
56
-
57
- return new_signal
58
-
59
- #----------------------
60
- # From methods
61
- #----------------------
62
- @classmethod
63
- def from_array(cls, y: np.ndarray, t: np.ndarray | None = None) -> Self:
64
-
65
- signal = cls(y=y, t=t)
66
-
67
- return signal
68
-
69
- @classmethod
70
- def from_array_with_sr(cls, y: np.ndarray, sr: int) -> Self:
71
- t = np.arange(y.shape[0]) * (1.0 / sr)
72
-
73
- signal = cls(y=y, t=t)
74
-
75
- return signal
76
-
77
- @classmethod
78
- def from_list(cls, y: list, t: list) -> Self:
79
-
80
- y = np.array(y)
81
- t = np.array(t)
82
- signal = cls(y=y, t=t)
83
-
84
- return signal
85
-
86
- @classmethod
87
- def from_file(cls, fp: str | Path, sr: int | None = None) -> Self:
88
-
89
- import librosa
90
-
91
- fp = Path(fp)
92
- y, sr = librosa.load(fp, sr=sr)
93
- t = np.arange(y.shape[0]) * (1.0 / sr)
94
-
95
- signal = cls(y=y, t=t)
96
- signal.set_plot_labels(title=fp.stem)
97
-
98
- return signal
49
+ def __init__(self, y: np.ndarray, sr: int | None = None, t: np.ndarray | None = None, t0: float = 0.0, title: str | None = None):
99
50
 
100
- #----------------------
101
- # Setters
102
- #----------------------
103
-
104
- @validate_args_type()
105
- def set_units(self, y_unit: str | None = None, t_unit: str | None = None) -> Self:
106
-
107
- if y_unit is not None:
108
- self._y_unit = y_unit
109
- if t_unit is not None:
110
- self._t_unit = t_unit
111
-
112
- return self
113
-
114
- @validate_args_type()
115
- def set_plot_labels(self, title: str | None = None, y_label: str | None = None, t_label: str | None = None) -> Self:
116
-
117
- if title is not None:
118
- self._title = title
119
- if y_label is not None:
120
- self._y_label = y_label
121
- if t_label is not None:
122
- self._t_label = t_label
123
-
124
- return self
125
-
126
-
51
+ if y.ndim != 1:
52
+ raise excp.InputValueError(f"`y` must have 1 dimension, not {y.ndim}.")
53
+
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
+ self._y = y
67
+ self._sr = sr
68
+ self._t0 = t0
69
+ self.title = title or self._name
70
+
127
71
  #----------------------
128
72
  # Properties
129
73
  #----------------------
130
-
131
74
  @immutable_property("Create a new object instead.")
132
75
  def y(self) -> np.ndarray:
133
- """"""
134
- return self.data
76
+ """Audio data."""
77
+ return self._y
135
78
 
136
79
  @immutable_property("Create a new object instead.")
137
- def t(self) -> np.ndarray:
138
- """"""
139
- return self.data_idx
80
+ def sr(self) -> np.ndarray:
81
+ """Sampling rate of the audio."""
82
+ return self._sr
140
83
 
141
84
  @immutable_property("Create a new object instead.")
142
- def sr(self) -> np.ndarray:
143
- """"""
144
- return 1.0 / self.t[1] - self.t[0]
145
-
146
- @immutable_property("Use `.set_units` instead.")
147
- def y_unit(self) -> str:
148
- """"""
149
- return self._y_unit
150
-
151
- @immutable_property("Use `set_units` instead.")
152
- def t_unit(self) -> str:
153
- """"""
154
- return self._t_unit
155
-
156
- @immutable_property("Use `.set_plot_labels` instead.")
157
- def title(self) -> str:
158
- """"""
159
- return self._title
160
-
161
- @immutable_property("Use `.set_plot_labels` instead.")
162
- def y_label(self) -> str:
163
- """"""
164
- return self._y_label
165
-
166
- @immutable_property("Use `.set_plot_labels` instead.")
167
- def t_label(self) -> str:
168
- """"""
169
- return self._t_label
85
+ def t0(self) -> np.ndarray:
86
+ """Start timestamp of the audio."""
87
+ return self._t0
88
+
89
+ @immutable_property("Create a new object instead.")
90
+ def t(self) -> np.ndarray:
91
+ """Timestamp array of the audio."""
92
+ return self.t0 + np.arange(len(self.y)) / self.sr
170
93
 
171
94
  @immutable_property("Mutation not allowed.")
172
95
  def Ts(self) -> int:
173
- """"""
174
- return self.t[1] - self.t[0]
175
-
96
+ """Sampling Period of the audio."""
97
+ return 1.0 / self.sr
98
+
176
99
  @immutable_property("Mutation not allowed.")
177
100
  def duration(self) -> int:
178
- """"""
179
- return self.t[-1]
180
-
181
- @immutable_property("Use `.set_labels` instead.")
182
- def labels(self) -> tuple[str, str, str]:
183
- """Labels in a tuple format appropriate for the plots."""
184
- return (self.title, f"{self.y_label} ({self.y_unit})", f"{self.t_label} ({self.t_unit})")
101
+ """Duration of the audio."""
102
+ return len(self.y) / self.sr
185
103
 
104
+ @immutable_property("Mutation not allowed.")
105
+ def info(self) -> None:
106
+ """Prints info about the audio."""
107
+ print("-" * 50)
108
+ print(f"{'Title':<20}: {self.title}")
109
+ print(f"{'Kind':<20}: {self._name}")
110
+ print(f"{'Duration':<20}: {self.duration:.2f} sec")
111
+ print(f"{'Sampling Rate':<20}: {self.sr} Hz")
112
+ print(f"{'Sampling Period':<20}: {(self.Ts*1000) :.4f} ms")
113
+ print("-" * 50)
186
114
 
187
115
  #----------------------
188
- # Plugins Access
116
+ # Methods
189
117
  #----------------------
118
+ 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
+ )
129
+
130
+ @validate_args_type()
131
+ def crop(self, t_min: int | float | None = None, t_max: int | float | None = None) -> "AudioSignal":
132
+ """
133
+ Crop the audio signal to a time range [t_min, t_max].
134
+
135
+ .. code-block:: python
136
+
137
+ from modusa.generators import AudioSignalGenerator
138
+ audio_example = AudioSignalGenerator.generate_example()
139
+ cropped_audio = audio_example.crop(1.5, 2)
140
+
141
+ Parameters
142
+ ----------
143
+ t_min : float or None
144
+ Inclusive lower time bound. If None, no lower bound.
145
+ t_max : float or None
146
+ Exclusive upper time bound. If None, no upper bound.
147
+
148
+ Returns
149
+ -------
150
+ AudioSignal
151
+ Cropped audio signal.
152
+ """
153
+ y = self.y
154
+ t = self.t
155
+
156
+ mask = np.ones_like(t, dtype=bool)
157
+ if t_min is not None:
158
+ mask &= (t >= t_min)
159
+ if t_max is not None:
160
+ mask &= (t < t_max)
161
+
162
+ cropped_y = y[mask]
163
+ new_t0 = t[mask][0] if np.any(mask) else self.t0 # fallback to original t0 if mask is empty
164
+
165
+ return self.__class__(y=cropped_y, sr=self.sr, t0=new_t0, title=self.title)
166
+
167
+
190
168
  @validate_args_type()
191
169
  def plot(
192
170
  self,
193
171
  scale_y: tuple[float, float] | None = None,
194
- scale_t: tuple[float, float] | None = None,
195
172
  ax: plt.Axes | None = None,
196
173
  color: str = "b",
197
174
  marker: str | None = None,
198
175
  linestyle: str | None = None,
199
- stem: bool | None = None,
200
- labels: tuple[str, str, str] | None = None,
176
+ stem: bool | None = False,
201
177
  legend_loc: str | None = None,
202
- zoom: tuple[float, float] | None = None,
178
+ title: str | None = None,
179
+ ylabel: str | None = "Amplitude",
180
+ xlabel: str | None = "Time (sec)",
181
+ ylim: tuple[float, float] | None = None,
182
+ xlim: tuple[float, float] | None = None,
203
183
  highlight: list[tuple[float, float]] | None = None,
204
184
  ) -> plt.Figure:
205
185
  """
206
- Applies `modusa.plugins.PlotTimeDomainSignal` Plugin.
186
+ Plot the audio waveform using matplotlib.
187
+
188
+ .. code-block:: python
189
+
190
+ from modusa.generators import AudioSignalGenerator
191
+ audio_example = AudioSignalGenerator.generate_example()
192
+ audio_example.plot(color="orange", title="Example Audio")
193
+
194
+ Parameters
195
+ ----------
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
199
+ 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
211
+ Plot title. Defaults to the signal’s title.
212
+ ylabel : str or None, optional
213
+ Label for the y-axis. Defaults to `"Amplitude"`.
214
+ xlabel : str or None, optional
215
+ Label for the x-axis. Defaults to `"Time (sec)"`.
216
+ ylim : tuple of float or None, optional
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
221
+ List of time intervals to highlight on the plot, each as (start, end).
222
+
223
+ Returns
224
+ -------
225
+ matplotlib.figure.Figure
226
+ The figure object containing the plot.
227
+ """
228
+
229
+ from modusa.io import Plotter
230
+
231
+ title = title or self.title
232
+
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)
234
+
235
+ return fig
236
+
237
+ def play(self, regions: list[tuple[float, float], ...] | None = None, title: str | None = None):
238
+ """
239
+ Play the audio signal inside a Jupyter Notebook.
240
+
241
+ .. code-block:: python
242
+
243
+ from modusa.generators import AudioSignalGenerator
244
+ audio = AudioSignalGenerator.generate_example()
245
+ audio.play(regions=[(0.0, 1.0), (2.0, 3.0)])
246
+
247
+ Parameters
248
+ ----------
249
+ regions : list of tuple of float, optional
250
+ List of (start_time, end_time) pairs in seconds specifying the regions to play.
251
+ If None, the entire signal is played.
252
+ title : str or None, optional
253
+ Optional title for the player interface. Defaults to the signal’s internal title.
254
+
255
+ Returns
256
+ -------
257
+ IPython.display.Audio
258
+ An interactive audio player widget for Jupyter environments.
259
+
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.
264
+ """
265
+
266
+ from modusa.io import AudioPlayer
267
+ audio_player = AudioPlayer.play(y=self.y, sr=self.sr, regions=regions, title=self.title)
268
+
269
+ return audio_player
270
+
271
+ def to_spectrogram(
272
+ self,
273
+ n_fft: int = 2048,
274
+ hop_length: int = 512,
275
+ win_length: int | None = None,
276
+ window: str = "hann"
277
+ ) -> "Spectrogram":
207
278
  """
279
+ Compute the Short-Time Fourier Transform (STFT) and return a Spectrogram object.
280
+
281
+ Parameters
282
+ ----------
283
+ n_fft : int
284
+ FFT size.
285
+ win_length : int or None
286
+ Window length. Defaults to `n_fft` if None.
287
+ hop_length : int
288
+ Hop length between frames.
289
+ window : str
290
+ Type of window function to use (e.g., 'hann', 'hamming').
291
+
292
+ Returns
293
+ -------
294
+ Spectrogram
295
+ Spectrogram object containing S (complex STFT), t (time bins), and f (frequency bins).
296
+ """
297
+ from modusa.signals.spectrogram import Spectrogram
298
+ import librosa
208
299
 
209
- from modusa.plugins import PlotTimeDomainSignalPlugin
210
-
211
- labels = labels or self.labels
212
- stem = stem or False
213
-
214
- fig: plt.Figure | None = PlotTimeDomainSignalPlugin().apply(
215
- signal=self,
216
- scale_y=scale_y,
217
- scale_t=scale_t,
218
- ax=ax,
219
- color=color,
220
- marker=marker,
221
- linestyle=linestyle,
222
- stem=stem,
223
- labels=labels,
224
- legend_loc=legend_loc,
225
- zoom=zoom,
226
- highlight=highlight
300
+ S = librosa.stft(self.y, n_fft=n_fft, win_length=win_length, hop_length=hop_length, window=window)
301
+ f = librosa.fft_frequencies(sr=self.sr, n_fft=n_fft)
302
+ 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)
305
+ if self.title != self._name: # Means title of the audio was reset so we pass that info to spec
306
+ spec.title = self.title
307
+
308
+ return spec
309
+
310
+
311
+ #----------------------------
312
+ # Math ops
313
+ #----------------------------
314
+
315
+ def __array__(self, dtype=None):
316
+ return np.asarray(self._S, dtype=dtype)
317
+
318
+ def __add__(self, other):
319
+ other_data = other.y if isinstance(other, self.__class__) else other
320
+ result = np.add(self.y, other_data)
321
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
322
+
323
+ def __radd__(self, other):
324
+ result = np.add(other, self.y)
325
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
326
+
327
+ def __sub__(self, other):
328
+ other_data = other.y if isinstance(other, self.__class__) else other
329
+ result = np.subtract(self.y, other_data)
330
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
331
+
332
+ def __rsub__(self, other):
333
+ result = np.subtract(other, self.y)
334
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
335
+
336
+ def __mul__(self, other):
337
+ other_data = other.y if isinstance(other, self.__class__) else other
338
+ result = np.multiply(self.y, other_data)
339
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
340
+
341
+ def __rmul__(self, other):
342
+ result = np.multiply(other, self.y)
343
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
344
+
345
+ def __truediv__(self, other):
346
+ other_data = other.y if isinstance(other, self.__class__) else other
347
+ result = np.true_divide(self.y, other_data)
348
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
349
+
350
+ def __rtruediv__(self, other):
351
+ result = np.true_divide(other, self.y)
352
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
353
+
354
+ def __floordiv__(self, other):
355
+ other_data = other._y if isinstance(other, self.__class__) else other
356
+ result = np.floor_divide(self.y, other_data)
357
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
358
+
359
+ def __rfloordiv__(self, other):
360
+ result = np.floor_divide(other, self.y)
361
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
362
+
363
+ def __pow__(self, other):
364
+ other_data = other.y if isinstance(other, self.__class__) else other
365
+ result = np.power(self.y, other_data)
366
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
367
+
368
+ def __rpow__(self, other):
369
+ result = np.power(other, self.y)
370
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
371
+
372
+ def __abs__(self):
373
+ result = np.abs(self.y)
374
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
375
+
376
+
377
+ #--------------------------
378
+ # Other signal ops
379
+ #--------------------------
380
+ def sin(self) -> Self:
381
+ """Compute the element-wise sine of the signal data."""
382
+ result = np.sin(self.y)
383
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
384
+
385
+ def cos(self) -> Self:
386
+ """Compute the element-wise cosine of the signal data."""
387
+ result = np.cos(self.y)
388
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
389
+
390
+ def exp(self) -> Self:
391
+ """Compute the element-wise exponential of the signal data."""
392
+ result = np.exp(self.y)
393
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
394
+
395
+ def tanh(self) -> Self:
396
+ """Compute the element-wise hyperbolic tangent of the signal data."""
397
+ result = np.tanh(self.y)
398
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
399
+
400
+ def log(self) -> Self:
401
+ """Compute the element-wise natural logarithm of the signal data."""
402
+ result = np.log(self.y)
403
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
404
+
405
+ def log1p(self) -> Self:
406
+ """Compute the element-wise natural logarithm of (1 + signal data)."""
407
+ result = np.log1p(self.y)
408
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
409
+
410
+ def log10(self) -> Self:
411
+ """Compute the element-wise base-10 logarithm of the signal data."""
412
+ result = np.log10(self.y)
413
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
414
+
415
+ def log2(self) -> Self:
416
+ """Compute the element-wise base-2 logarithm of the signal data."""
417
+ result = np.log2(self.y)
418
+ return self.__class__(y=result, sr=self.sr, t0=self.t0, title=self.title)
419
+
420
+
421
+ #--------------------------
422
+ # Aggregation signal ops
423
+ #--------------------------
424
+ def mean(self) -> float:
425
+ """Compute the mean of the signal data."""
426
+ return float(np.mean(self.y))
427
+
428
+ def std(self) -> float:
429
+ """Compute the standard deviation of the signal data."""
430
+ return float(np.std(self.y))
431
+
432
+ def min(self) -> float:
433
+ """Compute the minimum value in the signal data."""
434
+ return float(np.min(self.y))
435
+
436
+ def max(self) -> float:
437
+ """Compute the maximum value in the signal data."""
438
+ return float(np.max(self.y))
439
+
440
+ def sum(self) -> float:
441
+ """Compute the sum of the signal data."""
442
+ return float(np.sum(self.y))
443
+
444
+ #-----------------------------------
445
+ # Repr
446
+ #-----------------------------------
447
+
448
+ def __str__(self):
449
+ cls = self.__class__.__name__
450
+ data = self.y
451
+
452
+ arr_str = np.array2string(
453
+ data,
454
+ separator=", ",
455
+ threshold=50, # limit number of elements shown
456
+ edgeitems=3, # show first/last 3 rows and columns
457
+ max_line_width=120, # avoid wrapping
458
+ formatter={'float_kind': lambda x: f"{x:.4g}"}
227
459
  )
228
460
 
229
- return fig
230
-
461
+ return f"Signal({arr_str}, shape={data.shape}, kind={cls})"
462
+
463
+ def __repr__(self):
464
+ cls = self.__class__.__name__
465
+ data = self.y
466
+
467
+ arr_str = np.array2string(
468
+ data,
469
+ separator=", ",
470
+ threshold=50, # limit number of elements shown
471
+ edgeitems=3, # show first/last 3 rows and columns
472
+ max_line_width=120, # avoid wrapping
473
+ formatter={'float_kind': lambda x: f"{x:.4g}"}
474
+ )
475
+
476
+ return f"Signal({arr_str}, shape={data.shape}, kind={cls})"