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.
- modusa/.DS_Store +0 -0
- modusa/__init__.py +8 -1
- modusa/devtools/{generate_doc_source.py → generate_docs_source.py} +5 -5
- modusa/devtools/generate_template.py +5 -5
- modusa/devtools/main.py +3 -3
- modusa/devtools/templates/generator.py +1 -1
- modusa/devtools/templates/io.py +1 -1
- modusa/devtools/templates/{signal.py → model.py} +18 -11
- modusa/devtools/templates/plugin.py +1 -1
- modusa/generators/__init__.py +11 -1
- modusa/generators/audio.py +188 -0
- modusa/generators/audio_waveforms.py +1 -1
- modusa/generators/base.py +1 -1
- modusa/generators/ftds.py +298 -0
- modusa/generators/s1d.py +270 -0
- modusa/generators/s2d.py +300 -0
- modusa/generators/s_ax.py +102 -0
- modusa/generators/t_ax.py +64 -0
- modusa/generators/tds.py +267 -0
- modusa/models/__init__.py +14 -0
- modusa/models/__pycache__/signal1D.cpython-312.pyc.4443461152 +0 -0
- modusa/models/audio.py +90 -0
- modusa/models/base.py +70 -0
- modusa/models/data.py +457 -0
- modusa/models/ftds.py +584 -0
- modusa/models/s1d.py +578 -0
- modusa/models/s2d.py +619 -0
- modusa/models/s_ax.py +448 -0
- modusa/models/t_ax.py +335 -0
- modusa/models/tds.py +465 -0
- modusa/plugins/__init__.py +3 -1
- modusa/tmp.py +98 -0
- modusa/tools/__init__.py +5 -0
- modusa/tools/audio_converter.py +56 -67
- modusa/tools/audio_loader.py +90 -0
- modusa/tools/audio_player.py +42 -67
- modusa/tools/math_ops.py +104 -1
- modusa/tools/plotter.py +305 -497
- modusa/tools/youtube_downloader.py +31 -98
- modusa/utils/excp.py +6 -0
- modusa/utils/np_func_cat.py +44 -0
- modusa/utils/plot.py +142 -0
- {modusa-0.2.23.dist-info → modusa-0.3.dist-info}/METADATA +5 -16
- modusa-0.3.dist-info/RECORD +60 -0
- modusa/devtools/docs/source/generators/audio_waveforms.rst +0 -8
- modusa/devtools/docs/source/generators/base.rst +0 -8
- modusa/devtools/docs/source/generators/index.rst +0 -8
- modusa/devtools/docs/source/io/audio_loader.rst +0 -8
- modusa/devtools/docs/source/io/base.rst +0 -8
- modusa/devtools/docs/source/io/index.rst +0 -8
- modusa/devtools/docs/source/plugins/base.rst +0 -8
- modusa/devtools/docs/source/plugins/index.rst +0 -7
- modusa/devtools/docs/source/signals/audio_signal.rst +0 -8
- modusa/devtools/docs/source/signals/base.rst +0 -8
- modusa/devtools/docs/source/signals/frequency_domain_signal.rst +0 -8
- modusa/devtools/docs/source/signals/index.rst +0 -11
- modusa/devtools/docs/source/signals/spectrogram.rst +0 -8
- modusa/devtools/docs/source/signals/time_domain_signal.rst +0 -8
- modusa/devtools/docs/source/tools/audio_converter.rst +0 -8
- modusa/devtools/docs/source/tools/audio_player.rst +0 -8
- modusa/devtools/docs/source/tools/base.rst +0 -8
- modusa/devtools/docs/source/tools/fourier_tranform.rst +0 -8
- modusa/devtools/docs/source/tools/index.rst +0 -13
- modusa/devtools/docs/source/tools/math_ops.rst +0 -8
- modusa/devtools/docs/source/tools/plotter.rst +0 -8
- modusa/devtools/docs/source/tools/youtube_downloader.rst +0 -8
- modusa/io/__init__.py +0 -5
- modusa/io/audio_loader.py +0 -184
- modusa/io/base.py +0 -43
- modusa/signals/__init__.py +0 -3
- modusa/signals/audio_signal.py +0 -540
- modusa/signals/base.py +0 -27
- modusa/signals/frequency_domain_signal.py +0 -376
- modusa/signals/spectrogram.py +0 -564
- modusa/signals/time_domain_signal.py +0 -412
- modusa/tools/fourier_tranform.py +0 -24
- modusa-0.2.23.dist-info/RECORD +0 -70
- {modusa-0.2.23.dist-info → modusa-0.3.dist-info}/WHEEL +0 -0
- {modusa-0.2.23.dist-info → modusa-0.3.dist-info}/entry_points.txt +0 -0
- {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
|
+
#======================================
|
modusa/plugins/__init__.py
CHANGED
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
modusa/tools/audio_converter.py
CHANGED
|
@@ -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
|
|
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
|
-
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|