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.
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.1.dist-info}/METADATA +24 -19
  44. modusa-0.3.1.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.1.dist-info}/WHEEL +0 -0
  79. {modusa-0.2.23.dist-info → modusa-0.3.1.dist-info}/entry_points.txt +0 -0
  80. {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
@@ -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
- class AudioPlayer(ModusaTool):
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 time series.
37
- sr : int
38
- Sampling rate.
39
- regions : list of (float, float), optional
40
- Regions to extract and play (in seconds).
41
- title : str, optional
42
- Title to display above audio players.
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
- clip_numbers = []
33
+
34
+ clip_tags = []
55
35
  timings = []
56
36
  players = []
57
-
58
- if regions:
59
- for i, (start_sec, end_sec) in enumerate(regions):
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
- if start_sec is None:
62
- start_sec = 0.0
63
- if end_sec is None:
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
- end_sample = int(end_sec * sr)
68
- clip = y[start_sample:end_sample]
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
- clip_numbers.append(f"<td style='text-align:center; border-right:1px solid #ccc; padding:6px;'>{i+1}</td>")
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;'>{audio_tag}</td>")
54
+ players.append(f"<td style='padding:6px;'>{audio_player}</td>")
74
55
  else:
75
- total_duration = len(y) / sr
76
- audio_tag = Audio(data=y, rate=sr)._repr_html_()
56
+ audio_player = Audio(data=y, rate=sr)._repr_html_()
77
57
 
78
- clip_numbers.append(f"<td style='text-align:center; border-right:1px solid #ccc; padding:6px;'>1</td>")
79
- timings.append(f"<td style='text-align:center; border-right:1px solid #ccc; padding:6px;'>0.00s {total_duration:.2f}s</td>")
80
- players.append(f"<td style='padding:6px;'>{audio_tag}</td>")
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(clip_numbers)}
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