modusa 0.2.23__py3-none-any.whl → 0.3.1__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.1.dist-info}/METADATA +24 -19
- modusa-0.3.1.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.1.dist-info}/WHEEL +0 -0
- {modusa-0.2.23.dist-info → modusa-0.3.1.dist-info}/entry_points.txt +0 -0
- {modusa-0.2.23.dist-info → modusa-0.3.1.dist-info}/licenses/LICENSE.md +0 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import soundfile as sf
|
|
5
|
+
from scipy.signal import resample
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import tempfile
|
|
8
|
+
from scipy.signal import resample
|
|
9
|
+
from .youtube_downloader import download
|
|
10
|
+
from .audio_converter import convert
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load(path, sr=None):
|
|
14
|
+
"""
|
|
15
|
+
Loads audio file from various sources.
|
|
16
|
+
|
|
17
|
+
.. code-block:: python
|
|
18
|
+
|
|
19
|
+
import modusa as ms
|
|
20
|
+
audio_fp = ms.load(
|
|
21
|
+
"https://www.youtube.com/watch?v=lIpw9-Y_N0g",
|
|
22
|
+
sr = None)
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
path: str
|
|
27
|
+
- Path to the audio
|
|
28
|
+
- Youtube URL
|
|
29
|
+
sr: int
|
|
30
|
+
- Sampling rate to load the audio in.
|
|
31
|
+
|
|
32
|
+
Return
|
|
33
|
+
------
|
|
34
|
+
np.ndarray
|
|
35
|
+
- Audio signal.
|
|
36
|
+
int
|
|
37
|
+
- Sampling rate of the loaded audio signal.
|
|
38
|
+
title
|
|
39
|
+
- Title of the loaded audio.
|
|
40
|
+
- Filename without extension or YouTube title.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# Check if the path is YouTube
|
|
44
|
+
if ".youtube." in str(path):
|
|
45
|
+
# Download the audio in temp directory using tempfile module
|
|
46
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
47
|
+
# Download
|
|
48
|
+
audio_fp: Path = download(url=path, content_type="audio", output_dir=Path(tmpdir))
|
|
49
|
+
|
|
50
|
+
# Convert the audio to ".wav" form for loading
|
|
51
|
+
wav_audio_fp: Path = convert(inp_audio_fp=audio_fp, output_audio_fp=audio_fp.with_suffix(".wav"))
|
|
52
|
+
|
|
53
|
+
# Load the audio in memory
|
|
54
|
+
audio_data, audio_sr = sf.read(wav_audio_fp)
|
|
55
|
+
title = audio_fp.stem
|
|
56
|
+
|
|
57
|
+
# Convert to mono if it's multi-channel
|
|
58
|
+
if audio_data.ndim > 1:
|
|
59
|
+
audio_data = audio_data.mean(axis=1)
|
|
60
|
+
|
|
61
|
+
# Resample if needed
|
|
62
|
+
if sr is not None:
|
|
63
|
+
if audio_sr != sr:
|
|
64
|
+
n_samples = int(len(audio_data) * sr / audio_sr)
|
|
65
|
+
audio_data = resample(audio_data, n_samples)
|
|
66
|
+
audio_sr = sr
|
|
67
|
+
|
|
68
|
+
else:
|
|
69
|
+
# Check if the file exists
|
|
70
|
+
fp = Path(path)
|
|
71
|
+
|
|
72
|
+
if not fp.exists():
|
|
73
|
+
raise FileNotFoundError(f"{path} does not exist.")
|
|
74
|
+
|
|
75
|
+
# Load the audio in memory
|
|
76
|
+
audio_data, audio_sr = sf.read(fp)
|
|
77
|
+
title = fp.stem
|
|
78
|
+
|
|
79
|
+
# Convert to mono if it's multi-channel
|
|
80
|
+
if audio_data.ndim > 1:
|
|
81
|
+
audio_data = audio_data.mean(axis=1)
|
|
82
|
+
|
|
83
|
+
# Resample if needed
|
|
84
|
+
if sr is not None:
|
|
85
|
+
if audio_sr != sr:
|
|
86
|
+
n_samples = int(len(audio_data) * sr / audio_sr)
|
|
87
|
+
audio_data = resample(audio_data, n_samples)
|
|
88
|
+
audio_sr = sr
|
|
89
|
+
|
|
90
|
+
return audio_data, audio_sr, title
|
modusa/tools/audio_player.py
CHANGED
|
@@ -1,83 +1,66 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
from modusa import excp
|
|
5
|
-
from modusa.decorators import validate_args_type
|
|
6
|
-
from modusa.tools.base import ModusaTool
|
|
7
|
-
from IPython.display import display, HTML, Audio
|
|
8
3
|
import numpy as np
|
|
4
|
+
from IPython.display import display, HTML, Audio
|
|
9
5
|
|
|
10
|
-
|
|
11
|
-
"""
|
|
12
|
-
Provides audio player in the jupyter notebook environment.
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
#--------Meta Information----------
|
|
16
|
-
_name = "Audio Player"
|
|
17
|
-
_description = ""
|
|
18
|
-
_author_name = "Ankit Anand"
|
|
19
|
-
_author_email = "ankit0.anand0@gmail.com"
|
|
20
|
-
_created_at = "2025-07-08"
|
|
21
|
-
#----------------------------------
|
|
22
|
-
|
|
23
|
-
@staticmethod
|
|
24
|
-
def play(
|
|
25
|
-
y: np.ndarray,
|
|
26
|
-
sr: int,
|
|
27
|
-
regions: list[tuple[float, float]] | None = None,
|
|
28
|
-
title: str | None = None
|
|
29
|
-
) -> None:
|
|
6
|
+
def play(y: np.ndarray, sr: float, t0: float = 0.0, regions = None, title = None) -> None:
|
|
30
7
|
"""
|
|
31
8
|
Plays audio clips for given regions in Jupyter Notebooks.
|
|
32
9
|
|
|
33
10
|
Parameters
|
|
34
11
|
----------
|
|
35
12
|
y : np.ndarray
|
|
36
|
-
Audio
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
13
|
+
- Audio data.
|
|
14
|
+
- Mono (1D) numpy array.
|
|
15
|
+
sr: float
|
|
16
|
+
- Sampling rate of the audio.
|
|
17
|
+
t0: float
|
|
18
|
+
- Starting timestamp, incase the audio is cropped
|
|
19
|
+
- Default: 0.0 → Starts from 0.0 sec
|
|
20
|
+
regions : list[tuple[float, float, str]] | tuple[float, float, str] | None
|
|
21
|
+
- Regions to extract and play (in sec), e.g. [(0, 10.2, "tag")]
|
|
22
|
+
- If there is only one region, a tuple should also work. e.g. (0, 10.2, "tag")
|
|
23
|
+
- Default: None → The entire song is selected.
|
|
24
|
+
title : str | None
|
|
25
|
+
- Title to display above audio players.
|
|
43
26
|
|
|
44
27
|
Returns
|
|
45
28
|
-------
|
|
46
29
|
None
|
|
47
30
|
"""
|
|
48
|
-
if not AudioPlayer._in_notebook():
|
|
49
|
-
return
|
|
50
|
-
|
|
51
31
|
if title:
|
|
52
32
|
display(HTML(f"<h4>{title}</h4>"))
|
|
53
|
-
|
|
54
|
-
|
|
33
|
+
|
|
34
|
+
clip_tags = []
|
|
55
35
|
timings = []
|
|
56
36
|
players = []
|
|
57
|
-
|
|
58
|
-
if regions:
|
|
59
|
-
|
|
37
|
+
|
|
38
|
+
if isinstance(regions, tuple): regions = [regions] # (10, 20, "Region 1") -> [(10, 20, "Region 1")]
|
|
39
|
+
|
|
40
|
+
if regions is not None:
|
|
41
|
+
for region in regions:
|
|
42
|
+
assert len(region) == 3
|
|
60
43
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
end_sec = y.shape[0] / sr
|
|
44
|
+
start_sec = region[0] - t0
|
|
45
|
+
end_sec = region[1] - t0
|
|
46
|
+
tag = region[2]
|
|
65
47
|
|
|
66
|
-
start_sample = int(start_sec * sr)
|
|
67
|
-
|
|
68
|
-
clip =
|
|
69
|
-
audio_tag = Audio(data=clip, rate=sr)._repr_html_()
|
|
48
|
+
start_sample, end_sample = int(start_sec * sr), int(end_sec * sr)
|
|
49
|
+
clip = y[start_sample: end_sample]
|
|
50
|
+
audio_player = Audio(data=clip, rate=sr)._repr_html_()
|
|
70
51
|
|
|
71
|
-
|
|
52
|
+
clip_tags.append(f"<td style='text-align:center; border-right:1px solid #ccc; padding:6px;'>{tag}</td>")
|
|
72
53
|
timings.append(f"<td style='text-align:center; border-right:1px solid #ccc; padding:6px;'>{start_sec:.2f}s → {end_sec:.2f}s</td>")
|
|
73
|
-
players.append(f"<td style='padding:6px;'>{
|
|
54
|
+
players.append(f"<td style='padding:6px;'>{audio_player}</td>")
|
|
74
55
|
else:
|
|
75
|
-
|
|
76
|
-
audio_tag = Audio(data=y, rate=sr)._repr_html_()
|
|
56
|
+
audio_player = Audio(data=y, rate=sr)._repr_html_()
|
|
77
57
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
58
|
+
start_sec = t0
|
|
59
|
+
end_sec = t0 + y.shape[0] / sr
|
|
60
|
+
|
|
61
|
+
clip_tags.append(f"<td style='text-align:center; border-right:1px solid #ccc; padding:6px;'>1</td>")
|
|
62
|
+
timings.append(f"<td style='text-align:center; border-right:1px solid #ccc; padding:6px;'>{start_sec:.2f}s → {end_sec:.2f}s</td>")
|
|
63
|
+
players.append(f"<td style='padding:6px;'>{audio_player}</td>")
|
|
81
64
|
|
|
82
65
|
# Wrap rows in a table with border
|
|
83
66
|
table_html = f"""
|
|
@@ -85,7 +68,7 @@ class AudioPlayer(ModusaTool):
|
|
|
85
68
|
<table style="border-collapse:collapse;">
|
|
86
69
|
<tr style="background-color:#f2f2f2;">
|
|
87
70
|
<th style="text-align:left; padding:6px 12px;">Clip</th>
|
|
88
|
-
{''.join(
|
|
71
|
+
{''.join(clip_tags)}
|
|
89
72
|
</tr>
|
|
90
73
|
<tr style="background-color:#fcfcfc;">
|
|
91
74
|
<th style="text-align:left; padding:6px 12px;">Timing</th>
|
|
@@ -98,17 +81,9 @@ class AudioPlayer(ModusaTool):
|
|
|
98
81
|
</table>
|
|
99
82
|
</div>
|
|
100
83
|
"""
|
|
101
|
-
|
|
84
|
+
|
|
102
85
|
return HTML(table_html)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
@staticmethod
|
|
106
|
-
def _in_notebook() -> bool:
|
|
107
|
-
try:
|
|
108
|
-
from IPython import get_ipython
|
|
109
|
-
shell = get_ipython()
|
|
110
|
-
return shell and shell.__class__.__name__ == "ZMQInteractiveShell"
|
|
111
|
-
except ImportError:
|
|
112
|
-
return False
|
|
86
|
+
|
|
113
87
|
|
|
114
88
|
|
|
89
|
+
|
modusa/tools/math_ops.py
CHANGED
|
@@ -217,6 +217,30 @@ class MathOps(ModusaTool):
|
|
|
217
217
|
raise excp.InputError(f"can't find abs for `a`") from e
|
|
218
218
|
return result
|
|
219
219
|
|
|
220
|
+
@staticmethod
|
|
221
|
+
def floor(a: Any) -> np.generic | np.ndarray:
|
|
222
|
+
try:
|
|
223
|
+
result = np.floor(a)
|
|
224
|
+
except Exception as e:
|
|
225
|
+
raise excp.InputError(f"can't find floor for `a`") from e
|
|
226
|
+
return result
|
|
227
|
+
|
|
228
|
+
@staticmethod
|
|
229
|
+
def ceil(a: Any) -> np.generic | np.ndarray:
|
|
230
|
+
try:
|
|
231
|
+
result = np.ceil(a)
|
|
232
|
+
except Exception as e:
|
|
233
|
+
raise excp.InputError(f"can't find ceil for `a`") from e
|
|
234
|
+
return result
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def round(a: Any) -> np.generic | np.ndarray:
|
|
238
|
+
try:
|
|
239
|
+
result = np.round(a)
|
|
240
|
+
except Exception as e:
|
|
241
|
+
raise excp.InputError(f"can't find round for `a`") from e
|
|
242
|
+
return result
|
|
243
|
+
|
|
220
244
|
#------------------------------------
|
|
221
245
|
# TODO: Add shape-changing ops like
|
|
222
246
|
# reshape, transpose, squeeze later
|
|
@@ -229,4 +253,83 @@ class MathOps(ModusaTool):
|
|
|
229
253
|
except Exception as e:
|
|
230
254
|
raise excp.InputError(f"can't reshape `a`") from e
|
|
231
255
|
return result
|
|
232
|
-
|
|
256
|
+
|
|
257
|
+
#------------------------------------
|
|
258
|
+
# Complex numbers operations
|
|
259
|
+
#------------------------------------
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
def real(a: Any) -> np.ndarray:
|
|
263
|
+
try:
|
|
264
|
+
result = np.real(a)
|
|
265
|
+
except Exception as e:
|
|
266
|
+
raise excp.InputError(f"can't find real for `a`") from e
|
|
267
|
+
return result
|
|
268
|
+
|
|
269
|
+
@staticmethod
|
|
270
|
+
def imag(a: Any) -> np.ndarray:
|
|
271
|
+
try:
|
|
272
|
+
result = np.imag(a)
|
|
273
|
+
except Exception as e:
|
|
274
|
+
raise excp.InputError(f"can't find imag for `a`") from e
|
|
275
|
+
return result
|
|
276
|
+
|
|
277
|
+
@staticmethod
|
|
278
|
+
def angle(a: Any) -> np.ndarray:
|
|
279
|
+
try:
|
|
280
|
+
result = np.angle(a)
|
|
281
|
+
except Exception as e:
|
|
282
|
+
raise excp.InputError(f"can't find angle for `a`") from e
|
|
283
|
+
return result
|
|
284
|
+
|
|
285
|
+
#------------------------------------
|
|
286
|
+
# Comparison
|
|
287
|
+
#------------------------------------
|
|
288
|
+
|
|
289
|
+
@staticmethod
|
|
290
|
+
def lt(a: Any, b: Any) -> np.ndarray:
|
|
291
|
+
try:
|
|
292
|
+
mask = a < b
|
|
293
|
+
except Exception as e:
|
|
294
|
+
raise excp.InputError(f"`a` and `b` can't be compared") from e
|
|
295
|
+
return mask
|
|
296
|
+
|
|
297
|
+
@staticmethod
|
|
298
|
+
def le(a: Any, b: Any) -> np.ndarray:
|
|
299
|
+
try:
|
|
300
|
+
mask = a <= b
|
|
301
|
+
except Exception as e:
|
|
302
|
+
raise excp.InputError(f"`a` and `b` can't be compared") from e
|
|
303
|
+
return mask
|
|
304
|
+
|
|
305
|
+
@staticmethod
|
|
306
|
+
def gt(a: Any, b: Any) -> np.ndarray:
|
|
307
|
+
try:
|
|
308
|
+
mask = a > b
|
|
309
|
+
except Exception as e:
|
|
310
|
+
raise excp.InputError(f"`a` and `b` can't be compared") from e
|
|
311
|
+
return mask
|
|
312
|
+
|
|
313
|
+
@staticmethod
|
|
314
|
+
def ge(a: Any, b: Any) -> np.ndarray:
|
|
315
|
+
try:
|
|
316
|
+
mask = a >= b
|
|
317
|
+
except Exception as e:
|
|
318
|
+
raise excp.InputError(f"`a` and `b` can't be compared") from e
|
|
319
|
+
return mask
|
|
320
|
+
|
|
321
|
+
@staticmethod
|
|
322
|
+
def eq(a: Any, b: Any) -> np.ndarray:
|
|
323
|
+
try:
|
|
324
|
+
mask = a == b
|
|
325
|
+
except Exception as e:
|
|
326
|
+
raise excp.InputError(f"`a` and `b` can't be compared") from e
|
|
327
|
+
return mask
|
|
328
|
+
|
|
329
|
+
@staticmethod
|
|
330
|
+
def ne(a: Any, b: Any) -> np.ndarray:
|
|
331
|
+
try:
|
|
332
|
+
mask = a != b
|
|
333
|
+
except Exception as e:
|
|
334
|
+
raise excp.InputError(f"`a` and `b` can't be compared") from e
|
|
335
|
+
return mask
|