modusa 0.2.23__py3-none-any.whl → 0.3__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 (80) hide show
  1. modusa/.DS_Store +0 -0
  2. modusa/__init__.py +8 -1
  3. modusa/devtools/{generate_doc_source.py → generate_docs_source.py} +5 -5
  4. modusa/devtools/generate_template.py +5 -5
  5. modusa/devtools/main.py +3 -3
  6. modusa/devtools/templates/generator.py +1 -1
  7. modusa/devtools/templates/io.py +1 -1
  8. modusa/devtools/templates/{signal.py → model.py} +18 -11
  9. modusa/devtools/templates/plugin.py +1 -1
  10. modusa/generators/__init__.py +11 -1
  11. modusa/generators/audio.py +188 -0
  12. modusa/generators/audio_waveforms.py +1 -1
  13. modusa/generators/base.py +1 -1
  14. modusa/generators/ftds.py +298 -0
  15. modusa/generators/s1d.py +270 -0
  16. modusa/generators/s2d.py +300 -0
  17. modusa/generators/s_ax.py +102 -0
  18. modusa/generators/t_ax.py +64 -0
  19. modusa/generators/tds.py +267 -0
  20. modusa/models/__init__.py +14 -0
  21. modusa/models/__pycache__/signal1D.cpython-312.pyc.4443461152 +0 -0
  22. modusa/models/audio.py +90 -0
  23. modusa/models/base.py +70 -0
  24. modusa/models/data.py +457 -0
  25. modusa/models/ftds.py +584 -0
  26. modusa/models/s1d.py +578 -0
  27. modusa/models/s2d.py +619 -0
  28. modusa/models/s_ax.py +448 -0
  29. modusa/models/t_ax.py +335 -0
  30. modusa/models/tds.py +465 -0
  31. modusa/plugins/__init__.py +3 -1
  32. modusa/tmp.py +98 -0
  33. modusa/tools/__init__.py +5 -0
  34. modusa/tools/audio_converter.py +56 -67
  35. modusa/tools/audio_loader.py +90 -0
  36. modusa/tools/audio_player.py +42 -67
  37. modusa/tools/math_ops.py +104 -1
  38. modusa/tools/plotter.py +305 -497
  39. modusa/tools/youtube_downloader.py +31 -98
  40. modusa/utils/excp.py +6 -0
  41. modusa/utils/np_func_cat.py +44 -0
  42. modusa/utils/plot.py +142 -0
  43. {modusa-0.2.23.dist-info → modusa-0.3.dist-info}/METADATA +5 -16
  44. modusa-0.3.dist-info/RECORD +60 -0
  45. modusa/devtools/docs/source/generators/audio_waveforms.rst +0 -8
  46. modusa/devtools/docs/source/generators/base.rst +0 -8
  47. modusa/devtools/docs/source/generators/index.rst +0 -8
  48. modusa/devtools/docs/source/io/audio_loader.rst +0 -8
  49. modusa/devtools/docs/source/io/base.rst +0 -8
  50. modusa/devtools/docs/source/io/index.rst +0 -8
  51. modusa/devtools/docs/source/plugins/base.rst +0 -8
  52. modusa/devtools/docs/source/plugins/index.rst +0 -7
  53. modusa/devtools/docs/source/signals/audio_signal.rst +0 -8
  54. modusa/devtools/docs/source/signals/base.rst +0 -8
  55. modusa/devtools/docs/source/signals/frequency_domain_signal.rst +0 -8
  56. modusa/devtools/docs/source/signals/index.rst +0 -11
  57. modusa/devtools/docs/source/signals/spectrogram.rst +0 -8
  58. modusa/devtools/docs/source/signals/time_domain_signal.rst +0 -8
  59. modusa/devtools/docs/source/tools/audio_converter.rst +0 -8
  60. modusa/devtools/docs/source/tools/audio_player.rst +0 -8
  61. modusa/devtools/docs/source/tools/base.rst +0 -8
  62. modusa/devtools/docs/source/tools/fourier_tranform.rst +0 -8
  63. modusa/devtools/docs/source/tools/index.rst +0 -13
  64. modusa/devtools/docs/source/tools/math_ops.rst +0 -8
  65. modusa/devtools/docs/source/tools/plotter.rst +0 -8
  66. modusa/devtools/docs/source/tools/youtube_downloader.rst +0 -8
  67. modusa/io/__init__.py +0 -5
  68. modusa/io/audio_loader.py +0 -184
  69. modusa/io/base.py +0 -43
  70. modusa/signals/__init__.py +0 -3
  71. modusa/signals/audio_signal.py +0 -540
  72. modusa/signals/base.py +0 -27
  73. modusa/signals/frequency_domain_signal.py +0 -376
  74. modusa/signals/spectrogram.py +0 -564
  75. modusa/signals/time_domain_signal.py +0 -412
  76. modusa/tools/fourier_tranform.py +0 -24
  77. modusa-0.2.23.dist-info/RECORD +0 -70
  78. {modusa-0.2.23.dist-info → modusa-0.3.dist-info}/WHEEL +0 -0
  79. {modusa-0.2.23.dist-info → modusa-0.3.dist-info}/entry_points.txt +0 -0
  80. {modusa-0.2.23.dist-info → modusa-0.3.dist-info}/licenses/LICENSE.md +0 -0
modusa/models/tds.py ADDED
@@ -0,0 +1,465 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ from modusa import excp
5
+ from modusa.decorators import immutable_property, validate_args_type
6
+ from .s1d import S1D
7
+ from .t_ax import TAx
8
+ from .data import Data
9
+ from modusa.tools.math_ops import MathOps
10
+ from typing import Self, Any, Callable
11
+ from types import NoneType
12
+ import numpy as np
13
+ import matplotlib.pyplot as plt
14
+
15
+ class TDS(S1D):
16
+ """
17
+ Space to represent time domain signals.
18
+
19
+ Note
20
+ ----
21
+ - Use :class:`~modusa.generators.tds.TDSGen` to instantiate this class.
22
+
23
+ Parameters
24
+ ----------
25
+ y: Data
26
+ - Data object holding the main array.
27
+ t: TAx
28
+ - Time axis for the signal.
29
+ title: str
30
+ - Title for the signal.
31
+ - Default: None => ''
32
+ - e.g. "MySignal"
33
+ - This is used as the title while plotting.
34
+ """
35
+
36
+ #--------Meta Information----------
37
+ _name = "Time Domain Signal"
38
+ _nickname = "signal" # This is to be used in repr/str methods
39
+ _description = "Space to represent uniform time domain signal."
40
+ _author_name = "Ankit Anand"
41
+ _author_email = "ankit0.anand0@gmail.com"
42
+ _created_at = "2025-07-20"
43
+ #----------------------------------
44
+
45
+
46
+ def __init__(self, y, t, title = None):
47
+
48
+ if not (isinstance(y, Data) and isinstance(t, TAx)):
49
+ raise TypeError(f"`y` must be `Data` instance and `t` must be `TAx` object, got {type(y)} and {type(x)}")
50
+
51
+ assert y.ndim == 1
52
+
53
+ super().__init__(y=y, x=t, title=title) # Instantiating `Signal1D` class
54
+
55
+ #---------------------------------
56
+ # Properties (Hidden)
57
+ #---------------------------------
58
+
59
+ @property
60
+ def y(self) -> Data:
61
+ return self._y
62
+
63
+ @property
64
+ def t(self) -> TAx:
65
+ return self.x
66
+
67
+ @property
68
+ def title(self) -> str:
69
+ return self._title
70
+
71
+ @property
72
+ def shape(self) -> tuple:
73
+ return self.y.shape
74
+
75
+ @property
76
+ def ndim(self) -> tuple:
77
+ return self.y.ndim # Should be 1
78
+
79
+ @property
80
+ def size(self) -> int:
81
+ return self.y.size
82
+
83
+ #==================================
84
+
85
+ #-------------------------------
86
+ # NumPy Protocol
87
+ #-------------------------------
88
+
89
+ def __array__(self, dtype=None) -> np.ndarray:
90
+ return np.asarray(self.y.values, dtype=dtype)
91
+
92
+ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
93
+ """
94
+ Supports NumPy universal functions on the Signal1D object.
95
+ """
96
+ from .data import Data # Ensure this is the same Data class you're using
97
+ from modusa.utils import np_func_cat as nfc
98
+
99
+ raw_inputs = [
100
+ np.asarray(obj.y) if isinstance(obj, type(self)) else obj
101
+ for obj in inputs
102
+ ]
103
+
104
+ result = getattr(ufunc, method)(*raw_inputs, **kwargs)
105
+
106
+ y = Data(values=result, label=None) # label=None or you could copy from self.y.label
107
+ t = self.t.copy()
108
+
109
+ if y.shape != t.shape:
110
+ raise ValueError(f"`{ufunc.__name__}` caused shape mismatch between data and axis, please create a github issue")
111
+
112
+ return self.__class__(y=y, t=t, title=self.title)
113
+
114
+ def __array_function__(self, func, types, args, kwargs):
115
+ """
116
+ Additional numpy function support.
117
+ """
118
+ from modusa.utils import np_func_cat as nfc
119
+
120
+ if not all(issubclass(t, type(self)) for t in types):
121
+ return NotImplemented
122
+
123
+ # Not supporting concatenate like operations as axis any random axis can't be concatenated
124
+ if func in nfc.CONCAT_FUNCS:
125
+ raise NotImplementedError(f"`{func.__name__}` is not yet tested on modusa signal, please create a GitHub issue.")
126
+
127
+ # Single signal input expected
128
+ signal = args[0]
129
+ result: Data = func(signal.y, **kwargs)
130
+ axis = kwargs.get("axis", None)
131
+ keepdims = kwargs.get("keepdims", None)
132
+
133
+ if func in nfc.REDUCTION_FUNCS:
134
+ if keepdims is None or keepdims is True: # Default state is True => return 1D signal by wrapping the scalar
135
+ from .t_ax import TAx
136
+ dummy_t = TAx(n_points=1, sr=signal.t.sr, t0=0, label=None)
137
+ return self.__class__(y=result, t=dummy_t, title=signal.title)
138
+ elif keepdims is False: # Return Data
139
+ from .data import Data
140
+ return Data(values=result, label=None)
141
+
142
+ elif func in nfc.X_NEEDS_ADJUSTMENT_FUNCS:
143
+ # You must define logic for adjusting x
144
+ raise NotImplementedError(f"{func.__name__} requires x-axis adjustment logic.")
145
+
146
+ else:
147
+ raise NotImplementedError(f"`{func.__name__}` is not yet tested on modusa signal, please create a GitHub issue.")
148
+
149
+ #================================
150
+
151
+ #-------------------------------
152
+ # Indexing
153
+ #-------------------------------
154
+
155
+ def __getitem__(self, key):
156
+ """
157
+ Return a sliced or indexed view of the data.
158
+
159
+ Parameters
160
+ ----------
161
+ key : array-like
162
+ - Index to apply to the values.
163
+
164
+ Returns
165
+ -------
166
+ TDS
167
+ A new TDS object with sliced values and same meta data.
168
+ """
169
+ if not isinstance(key, (int, slice)):
170
+ raise TypeError(f"Invalid key type: {type(key)}")
171
+
172
+ sliced_y = self.y[key]
173
+ sliced_t = self.t[key]
174
+
175
+ if sliced_y.ndim == 0:
176
+ sliced_y = Data(values=sliced_y.values, label=sliced_y.label, ndim=1)
177
+
178
+ return self.__class__(y=sliced_y, t=sliced_t, title=self.title)
179
+
180
+ def __setitem__(self, key, value):
181
+ """
182
+ Set values at the specified index.
183
+
184
+ Parameters
185
+ ----------
186
+ key : int | slice | array-like | boolean array | S1D
187
+ Index to apply to the values.
188
+ value : int | float | array-like
189
+ Value(s) to set.
190
+ """
191
+
192
+ self.y[key] = value # In-place assignment
193
+
194
+ #===================================
195
+
196
+
197
+ #-----------------------------------
198
+ # Utility Methods
199
+ #-----------------------------------
200
+
201
+ def unpack(self):
202
+ """
203
+ Unpacks the object into easy to work
204
+ with data structures.
205
+
206
+ Returns
207
+ -------
208
+ (np.ndarray, float, float)
209
+ - y: Signal data array.
210
+ - sr: Sampling rate of the signal.
211
+ - t0: Starting timestamp.
212
+ """
213
+
214
+ arr = self.y.values
215
+ sr = self.t.sr
216
+ t0 = self.t.t0
217
+
218
+ return (arr, sr, t0)
219
+
220
+ def copy(self) -> Self:
221
+ """
222
+ Returns a new copy of the signal.
223
+
224
+ Returns
225
+ -------
226
+ Self
227
+ A new copy of the object.
228
+ """
229
+ copied_y = self.y.copy()
230
+ copied_x = self.x.copy()
231
+ title = self.title # Immutable, hence no need to copy
232
+
233
+ return self.__class__(y=copied_y, x=copied_x, title=title)
234
+
235
+
236
+ def set_meta_info(self, title = None, y_label = None, t_label = None) -> None:
237
+ """
238
+ Set meta info about the signal.
239
+
240
+ Parameters
241
+ ----------
242
+ title: str
243
+ - Title for the signal
244
+ - e.g. "Speedometer"
245
+ y_label: str
246
+ - Label for the y-axis.
247
+ - e.g. "Speeed (m/s)"
248
+ t_label: str
249
+ - Label for the time-axis.
250
+ - e.g. "Distance (m)"
251
+ """
252
+
253
+ new_title = str(title) if title is not None else self.title
254
+ new_y_label = str(y_label) if y_label is not None else self.y.label
255
+ new_t_label = str(t_label) if t_label is not None else self.t.label
256
+
257
+ # We create a new copy of the data and axis
258
+ new_y = self.y.copy().set_meta_info(y_label)
259
+ new_t = self.t.copy().set_meta_info(t_label)
260
+
261
+ return self.__class__(y=new_y, t=new_t, title=title)
262
+
263
+
264
+ def is_same_as(self, other: Self) -> bool:
265
+ """
266
+ Check if two `TDS` instances are equal.
267
+ """
268
+
269
+ if not isinstance(other, type(self)):
270
+ return False
271
+
272
+ if not self.y.is_same_as(other.y):
273
+ return False
274
+
275
+ if not self.t.is_same_as(other.t):
276
+ return False
277
+
278
+ return True
279
+
280
+ def has_same_axis_as(self, other) -> bool:
281
+ """
282
+ Check if two 'TDS' instances have same
283
+ axis. Many operations need to satify this.
284
+ """
285
+ return self.t.is_same_as(other.t)
286
+
287
+
288
+ def mask(self, condition, set_to=None) -> Self:
289
+ """
290
+ Mask the signal based on condition and
291
+ the values can be set.
292
+
293
+ Parameters
294
+ ----------
295
+ condition: Callable
296
+ - Condition function to apply on values of the signal.
297
+ - E.g. lambda x: x > 10
298
+ set_to: Number
299
+ - Number to replace the masked position values.
300
+
301
+ Returns
302
+ -------
303
+ TDS
304
+ Masked Signal
305
+ """
306
+
307
+ mask = condition(self)
308
+ new_val = set_to
309
+
310
+ if set_to is None: # Return the mask as the same signal but with booleans
311
+ return mask
312
+
313
+ else:
314
+ # We apply the mask and update the signal data
315
+ new_data = self.y.mask(condition=condition, set_to=new_val)
316
+
317
+ # Since we're just updating the data, there is no change in the axis
318
+ return self.__class__(y=new_data, t=self.t.copy(), title=self.title)
319
+ #===================================
320
+
321
+ #-------------------------------
322
+ # Tools
323
+ #-------------------------------
324
+
325
+ @validate_args_type()
326
+ def translate_t(self, n_samples: int):
327
+ """
328
+ Translate the signal along time axis.
329
+
330
+ Note
331
+ ----
332
+ - Negative indexing is allowed but just note that you might end up getting time < 0
333
+
334
+
335
+ .. code-block:: python
336
+
337
+ import modusa as ms
338
+ s1 = ms.tds([1, 2, 4, 4, 5, 3, 2, 1])
339
+ ms.plot(s1, s1.translate_t(-1), s1.translate_t(3))
340
+
341
+ Parameters
342
+ ----------
343
+ n_samples: int
344
+ By how many sample you would like to translate the signal.
345
+
346
+ Returns
347
+ -------
348
+ TDS
349
+ Translated signal.
350
+ """
351
+
352
+ translated_t = self.t.translate(n_samples=n_samples)
353
+
354
+ return self.__class__(y=self.y.copy(), t=translated_t, title=self.title)
355
+
356
+ def pad(self, left=None, right=None) -> Self:
357
+ """
358
+ Pad the signal with array like object from the
359
+ left or right.
360
+
361
+ Parameters
362
+ ----------
363
+ left: arraylike
364
+ - What to pad to the left of the signal.
365
+ - E.g. 1 or [1, 0, 1], np.array([1, 2, 3])
366
+ right: arraylike
367
+ - What to pad to the right of the signal.
368
+ - E.g. 1 or [1, 0, 1], np.array([1, 2, 3])
369
+
370
+ Returns
371
+ -------
372
+ TDS
373
+ Padded signal.
374
+ """
375
+
376
+ if right is None and left is None: # No padding applied
377
+ return self
378
+
379
+ # Pad the data
380
+ y_padded = self.y.pad(left=left, right=right)
381
+
382
+ # Find the new t0
383
+ if left is not None:
384
+ if np.ndim(left) == 0: left = np.asarray([left])
385
+ else: left = np.asarray(left)
386
+ new_t0 = self.t.t0 - (left.shape[0] / self.t.sr)
387
+ else:
388
+ new_t0 = self.t.t0
389
+
390
+ t_padded = self.t.__class__(n_points=y_padded.shape[0], sr=self.t.sr, t0=new_t0, label=self.t.label)
391
+
392
+ return self.__class__(y=y_padded, t=t_padded, title=self.title)
393
+
394
+
395
+
396
+ def crop(self, t_min = None, t_max = None, like = None) -> Self:
397
+ """
398
+ Crop the signal to a time range [t_min, t_max].
399
+
400
+ .. code-block:: python
401
+
402
+ import modusa as ms
403
+ s1 = ms.tds.random(1000, sr=10)
404
+ ms.plot(s1, s1.crop(5, 40), s1.crop(20), s1.crop(60, 80))
405
+
406
+
407
+ Parameters
408
+ ----------
409
+ t_min : float or None
410
+ Inclusive lower time bound in second (other units). If None, no lower bound.
411
+ t_max : float or None
412
+ Exclusive upper time bound in second (other units). If None, no upper bound.
413
+ like: TDS
414
+ - A `TDS` object whose start and end time will be used.
415
+ - If you have a window signal, you can crop the signal to get the correct portion.
416
+
417
+ Returns
418
+ -------
419
+ TDS
420
+ Cropped signal.
421
+ """
422
+
423
+ if like is not None:
424
+ ref_signal = like
425
+ assert self.t.sr == ref_signal.t.sr
426
+ # Set t_min and t_max as per the signal
427
+ t_min = ref_signal.t.t0
428
+ t_max = ref_signal.t.end_time
429
+
430
+ # We first will find out the time in samples
431
+ if t_min is not None:
432
+ t_min_sample = self.t.index_of(t_min)
433
+ else:
434
+ t_min_sample = 0
435
+
436
+ if t_max is not None:
437
+ t_max_sample = self.t.index_of(t_max)
438
+ else:
439
+ t_max_sample = -1
440
+
441
+ return self[t_min_sample: t_max_sample+1]
442
+
443
+ #===================================
444
+
445
+ #-----------------------------------
446
+ # Information
447
+ #-----------------------------------
448
+
449
+ def print_info(self) -> None:
450
+ """Prints info about the audio."""
451
+ print("-" * 50)
452
+ print(f"{'Title'}: {self.title}")
453
+ print("-" * 50)
454
+ print(f"{'Type':<20}: {self.__class__.__name__}")
455
+ print(f"{'Shape':<20}: {self.shape}")
456
+ print(f"{'Duration':<20}: {self.t.duration:.2f} sec")
457
+ print(f"{'Sampling Rate':<20}: {self.t.sr} Hz")
458
+ print(f"{'Sampling Period':<20}: {(1 / self.t.sr * 1000):.2f} ms")
459
+
460
+ # Inheritance chain
461
+ cls_chain = " → ".join(cls.__name__ for cls in reversed(self.__class__.__mro__[:-1]))
462
+ print(f"{'Inheritance':<20}: {cls_chain}")
463
+ print("=" * 50)
464
+
465
+ #======================================
@@ -1 +1,3 @@
1
- #!/usr/bin/env python3
1
+ #!/usr/bin/env python3
2
+
3
+ from .base import ModusaPlugin
modusa/tmp.py ADDED
@@ -0,0 +1,98 @@
1
+ #def autocorr(self) -> Self:
2
+ # """
3
+ #
4
+ # """
5
+ # raise NotImplementedError
6
+ # r = np.correlate(self.data, self.data, mode="full")
7
+ # r = r[self.data.shape[0] - 1:]
8
+ # r_signal = self.__class__(data=r, sr=self.sr, t0=self.t0, title=self.title + " [Autocorr]")
9
+ # return r_signal
10
+
11
+ # #----------------------------
12
+ # # To different signals
13
+ # #----------------------------
14
+ # def to_audio_signal(self) -> "AudioSignal":
15
+ # """
16
+ # Moves TimeDomainSignal to AudioSignal
17
+ # """
18
+ # raise NotImplementedError
19
+ # from modusa.signals.audio_signal import AudioSignal
20
+ #
21
+ # return AudioSignal(data=self.data, sr=self.sr, t0=self.t0, title=self.title)
22
+ #
23
+ # def to_spectrogram(
24
+ # self,
25
+ # n_fft: int = 2048,
26
+ # hop_length: int = 512,
27
+ # win_length: int | None = None,
28
+ # window: str = "hann"
29
+ # ) -> "Spectrogram":
30
+ # """
31
+ # Compute the Short-Time Fourier Transform (STFT) and return a Spectrogram object.
32
+ #
33
+ # Parameters
34
+ # ----------
35
+ # n_fft : int
36
+ # FFT size.
37
+ # win_length : int or None
38
+ # Window length. Defaults to `n_fft` if None.
39
+ # hop_length : int
40
+ # Hop length between frames.
41
+ # window : str
42
+ # Type of window function to use (e.g., 'hann', 'hamming').
43
+ #
44
+ # Returns
45
+ # -------
46
+ # Spectrogram
47
+ # Spectrogram object containing S (complex STFT), t (time bins), and f (frequency bins).
48
+ # """
49
+ # raise NotImplementedError
50
+ # import warnings
51
+ # warnings.filterwarnings("ignore", category=UserWarning, module="librosa.core.intervals")
52
+ #
53
+ # from modusa.signals.feature_time_domain_signal import FeatureTimeDomainSignal
54
+ # import librosa
55
+ #
56
+ # S = librosa.stft(self.data, n_fft=n_fft, win_length=win_length, hop_length=hop_length, window=window)
57
+ # f = librosa.fft_frequencies(sr=self.sr, n_fft=n_fft)
58
+ # t = librosa.frames_to_time(np.arange(S.shape[1]), sr=self.sr, hop_length=hop_length)
59
+ # frame_rate = self.sr / hop_length
60
+ # spec = FeatureTimeDomainSignal(data=S, feature=f, feature_label="Freq (Hz)", frame_rate=frame_rate, t0=self.t0, time_label="Time (sec)", title=self.title)
61
+ # if self.title != self._name: # Means title of the audio was reset so we pass that info to spec
62
+ # spec = spec.set_meta_info(title=self.title)
63
+ #
64
+ # return spec
65
+ # #=====================================
66
+
67
+ #=====================================
68
+
69
+ #--------------------------
70
+ # Other signal ops
71
+ #--------------------------
72
+
73
+ # def interpolate(self, to: TimeDomainSignal, kind: str = "linear", fill_value: str | float = "extrapolate") -> TimeDomainSignal:
74
+ # """
75
+ # Interpolate the current signal to match the time axis of `to`.
76
+ #
77
+ # Parameters:
78
+ # to (TimeDomainSignal): The signal whose time axis will be used.
79
+ # kind (str): Interpolation method ('linear', 'nearest', etc.)
80
+ # fill_value (str or float): Value used to fill out-of-bounds.
81
+ #
82
+ # Returns:
83
+ # TimeDomainSignal: A new signal with values interpolated at `to.t`.
84
+ # """
85
+ # assert self.y.ndim == 1, "Only 1D signals supported for interpolation"
86
+ #
87
+ # interpolator = interp1d(
88
+ # self.t,
89
+ # self.y,
90
+ # kind=kind,
91
+ # fill_value=fill_value,
92
+ # bounds_error=False,
93
+ # assume_sorted=True
94
+ # )
95
+ #
96
+ # y_interp = interpolator(to.y)
97
+
98
+ # return self.__class__(y=y_interp, sr=to.sr, t0=to.t0, title=f"{self.title} → interpolated")
modusa/tools/__init__.py CHANGED
@@ -1,2 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
+ from .plotter import plot1d, plot2d
4
+ from .audio_player import play
5
+ from .audio_converter import convert
6
+ from .youtube_downloader import download
7
+ from .audio_loader import load
@@ -1,84 +1,73 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
3
 
4
- from modusa import excp
5
- from modusa.decorators import validate_args_type
6
- from modusa.tools.base import ModusaTool
7
4
  import subprocess
8
5
  from pathlib import Path
9
6
 
10
-
11
- class AudioConverter(ModusaTool):
7
+ def convert(inp_audio_fp, output_audio_fp, sr = None, mono = False) -> Path:
12
8
  """
13
- Converts audio to any given format using FFmpeg.
9
+ Converts an audio file from one format to another using FFmpeg.
10
+
11
+ .. code-block:: python
12
+
13
+ import modusa as ms
14
+ converted_audio_fp = ms.convert(
15
+ inp_audio_fp="path/to/input/audio.webm",
16
+ output_audio_fp="path/to/output/audio.wav")
17
+
18
+ Parameters
19
+ ----------
20
+ inp_audio_fp: str | Path
21
+ - Filepath of audio to be converted.
22
+ output_audio_fp: str | Path
23
+ - Filepath of the converted audio. (e.g. name.mp3)
24
+ sr: int | float
25
+ - Resample it to any target sampling rate.
26
+ - Default: None => Keep the original sample rate.
27
+ mono: bool
28
+ - Do you want to convert the audio into mono?
29
+ - Default: False
30
+
31
+ Returns
32
+ -------
33
+ Path:
34
+ Filepath of the converted audio.
14
35
 
15
36
  Note
16
37
  ----
17
- - Use `convert()` to perform the actual format conversion.
18
- - Requires FFMPEG to be installed on the system.
38
+ - The conversion takes place based on the extensions of the input and output audio filepath.
19
39
  """
40
+ inp_audio_fp = Path(inp_audio_fp)
41
+ output_audio_fp = Path(output_audio_fp)
20
42
 
21
- #--------Meta Information----------
22
- _name = "Audio Converter"
23
- _description = "Convert audio files using ffmpeg"
24
- _author_name = "Ankit Anand"
25
- _author_email = "ankit0.anand0@gmail.com"
26
- _created_at = "2025-07-05"
27
- #----------------------------------
43
+ if not inp_audio_fp.exists():
44
+ raise FileNotFoundError(f"`inp_audio_fp` does not exist, {inp_audio_fp}")
28
45
 
29
- @staticmethod
30
- @validate_args_type()
31
- def convert(inp_audio_fp: str | Path, output_audio_fp: str | Path) -> Path:
32
- """
33
- Converts an audio file from one format to another using FFmpeg.
34
-
35
- .. code-block:: python
36
-
37
- from modusa.engines import AudioConverter
38
- converted_audio_fp = AudioConverter.convert(
39
- inp_audio_fp="path/to/input/audio.webm",
40
- output_audio_fp="path/to/output/audio.wav"
41
- )
42
-
43
- Parameters
44
- ----------
45
- inp_audio_fp: str | Path
46
- Filepath of audio to be converted.
47
- output_audio_fp: str | Path
48
- Filepath of the converted audio. (e.g. name.mp3)
49
-
50
- Returns
51
- -------
52
- Path:
53
- Filepath of the converted audio.
54
-
55
- Note
56
- ----
57
- - The conversion takes place based on the extensions of the input and output audio filepath.
58
- """
59
- inp_audio_fp = Path(inp_audio_fp)
60
- output_audio_fp = Path(output_audio_fp)
46
+ if inp_audio_fp == output_audio_fp:
47
+ raise ValueError(f"`inp_fp` and `output_fp` must be different")
61
48
 
62
- if not inp_audio_fp.exists():
63
- raise excp.FileNotFoundError(f"`inp_audio_fp` does not exist, {inp_audio_fp}")
49
+ output_audio_fp.parent.mkdir(parents=True, exist_ok=True)
50
+
51
+ cmd = [
52
+ "ffmpeg",
53
+ "-y",
54
+ "-i", str(inp_audio_fp),
55
+ "-vn", # No video
56
+ ]
57
+
58
+ # Optional sample rate
59
+ if sr is not None:
60
+ cmd += ["-ar", str(sr)]
64
61
 
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)
62
+ # Optional mono
63
+ if mono is True:
64
+ cmd += ["-ac", "1"]
69
65
 
66
+ cmd.append(str(output_audio_fp))
70
67
 
71
- cmd = [
72
- "ffmpeg",
73
- "-y", # Overwrite output
74
- "-i", str(inp_audio_fp),
75
- "-vn", # No video
76
- str(output_audio_fp)
77
- ]
78
-
79
- try:
80
- subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
81
- except subprocess.CalledProcessError:
82
- raise RuntimeError(f"FFmpeg failed to convert {inp_audio_fp} to {output_audio_fp}")
83
-
84
- return output_audio_fp
68
+ try:
69
+ subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
70
+ except subprocess.CalledProcessError:
71
+ raise RuntimeError(f"FFmpeg failed to convert {inp_audio_fp} to {output_audio_fp}")
72
+
73
+ return output_audio_fp