modusa 0.2.22__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 (70) hide show
  1. modusa/.DS_Store +0 -0
  2. modusa/__init__.py +8 -1
  3. modusa/decorators.py +4 -4
  4. modusa/devtools/generate_docs_source.py +96 -0
  5. modusa/devtools/generate_template.py +13 -13
  6. modusa/devtools/main.py +4 -3
  7. modusa/devtools/templates/generator.py +1 -1
  8. modusa/devtools/templates/io.py +1 -1
  9. modusa/devtools/templates/{signal.py → model.py} +18 -11
  10. modusa/devtools/templates/plugin.py +1 -1
  11. modusa/devtools/templates/test.py +2 -3
  12. modusa/devtools/templates/{engine.py → tool.py} +3 -8
  13. modusa/generators/__init__.py +9 -1
  14. modusa/generators/audio.py +188 -0
  15. modusa/generators/audio_waveforms.py +22 -13
  16. modusa/generators/base.py +1 -1
  17. modusa/generators/ftds.py +298 -0
  18. modusa/generators/s1d.py +270 -0
  19. modusa/generators/s2d.py +300 -0
  20. modusa/generators/s_ax.py +102 -0
  21. modusa/generators/t_ax.py +64 -0
  22. modusa/generators/tds.py +267 -0
  23. modusa/main.py +0 -30
  24. modusa/models/__init__.py +14 -0
  25. modusa/models/__pycache__/signal1D.cpython-312.pyc.4443461152 +0 -0
  26. modusa/models/audio.py +90 -0
  27. modusa/models/base.py +70 -0
  28. modusa/models/data.py +457 -0
  29. modusa/models/ftds.py +584 -0
  30. modusa/models/s1d.py +578 -0
  31. modusa/models/s2d.py +619 -0
  32. modusa/models/s_ax.py +448 -0
  33. modusa/models/t_ax.py +335 -0
  34. modusa/models/tds.py +465 -0
  35. modusa/plugins/__init__.py +3 -1
  36. modusa/tmp.py +98 -0
  37. modusa/tools/__init__.py +7 -0
  38. modusa/tools/audio_converter.py +73 -0
  39. modusa/tools/audio_loader.py +90 -0
  40. modusa/tools/audio_player.py +89 -0
  41. modusa/tools/base.py +43 -0
  42. modusa/tools/math_ops.py +335 -0
  43. modusa/tools/plotter.py +351 -0
  44. modusa/tools/youtube_downloader.py +72 -0
  45. modusa/utils/excp.py +15 -42
  46. modusa/utils/np_func_cat.py +44 -0
  47. modusa/utils/plot.py +142 -0
  48. {modusa-0.2.22.dist-info → modusa-0.3.dist-info}/METADATA +5 -16
  49. modusa-0.3.dist-info/RECORD +60 -0
  50. modusa/engines/.DS_Store +0 -0
  51. modusa/engines/__init__.py +0 -3
  52. modusa/engines/base.py +0 -14
  53. modusa/io/__init__.py +0 -9
  54. modusa/io/audio_converter.py +0 -76
  55. modusa/io/audio_loader.py +0 -214
  56. modusa/io/audio_player.py +0 -72
  57. modusa/io/base.py +0 -43
  58. modusa/io/plotter.py +0 -430
  59. modusa/io/youtube_downloader.py +0 -139
  60. modusa/signals/__init__.py +0 -7
  61. modusa/signals/audio_signal.py +0 -483
  62. modusa/signals/base.py +0 -34
  63. modusa/signals/frequency_domain_signal.py +0 -329
  64. modusa/signals/signal_ops.py +0 -158
  65. modusa/signals/spectrogram.py +0 -465
  66. modusa/signals/time_domain_signal.py +0 -309
  67. modusa-0.2.22.dist-info/RECORD +0 -47
  68. {modusa-0.2.22.dist-info → modusa-0.3.dist-info}/WHEEL +0 -0
  69. {modusa-0.2.22.dist-info → modusa-0.3.dist-info}/entry_points.txt +0 -0
  70. {modusa-0.2.22.dist-info → modusa-0.3.dist-info}/licenses/LICENSE.md +0 -0
modusa/.DS_Store CHANGED
Binary file
modusa/__init__.py CHANGED
@@ -1 +1,8 @@
1
- from modusa.utils import excp, config
1
+ from modusa.utils import excp, config
2
+
3
+ #=====Giving access to plot functions to plot multiple signals.=====
4
+ from modusa.tools import plot1d, plot2d
5
+ from modusa.tools import play, convert
6
+ from modusa.tools import download
7
+ from modusa.tools import load
8
+ #=====
modusa/decorators.py CHANGED
@@ -110,14 +110,14 @@ def validate_arg(arg_name: str, value: Any, expected_type: Any) -> None:
110
110
  expected_names = ", ".join(
111
111
  get_origin(t).__name__ if get_origin(t) else t.__name__ for t in union_args
112
112
  )
113
- raise excp.ValidationError(
113
+ raise excp.InputTypeError(
114
114
  f"Argument '{arg_name}' must be one of ({expected_names}), got {type(value).__name__}"
115
115
  )
116
116
 
117
117
  # Handle generic types like list[float], tuple[int, str]
118
118
  elif origin is not None:
119
119
  if not isinstance(value, origin):
120
- raise excp.ValidationError(
120
+ raise excp.InputTypeError(
121
121
  f"Argument '{arg_name}' must be of type {origin.__name__}, got {type(value).__name__}"
122
122
  )
123
123
  return
@@ -125,13 +125,13 @@ def validate_arg(arg_name: str, value: Any, expected_type: Any) -> None:
125
125
  # ✅ Handle plain types
126
126
  elif isinstance(expected_type, type):
127
127
  if not isinstance(value, expected_type):
128
- raise excp.ValidationError(
128
+ raise excp.InputTypeError(
129
129
  f"Argument '{arg_name}' must be of type {expected_type.__name__}, got {type(value).__name__}"
130
130
  )
131
131
  return
132
132
  # ❌ Unsupported type structure
133
133
  else:
134
- raise excp.ValidationError(f"Unsupported annotation for '{arg_name}': {expected_type}")
134
+ raise excp.InputTypeError(f"Unsupported annotation for '{arg_name}': {expected_type}")
135
135
 
136
136
  def validate_args_type() -> Callable:
137
137
  def decorator(func: Callable) -> Callable:
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import inspect
4
+ import importlib
5
+ import pkgutil
6
+ from pathlib import Path
7
+ from collections import defaultdict
8
+
9
+ # === Configuration ===
10
+ BASE_MODULES = [
11
+ 'modusa.tools',
12
+ 'modusa.models',
13
+ 'modusa.generators',
14
+ 'modusa.plugins',
15
+ # 'modusa.io'
16
+ ]
17
+ OUTPUT_DIRS = [
18
+ Path('docs/source/tools'),
19
+ Path('docs/source/models'),
20
+ Path('docs/source/generators'),
21
+ Path('docs/source/plugins'),
22
+ # Path('docs/source/io')
23
+ ]
24
+
25
+ # Ensure output directories exist
26
+ for output_dir in OUTPUT_DIRS:
27
+ output_dir.mkdir(parents=True, exist_ok=True)
28
+
29
+ # === Utils ===
30
+ def get_classes_grouped_by_module(base_module):
31
+ """
32
+ Returns a dictionary: { module_path: [class_name, ...] }
33
+ """
34
+ found = defaultdict(list)
35
+ module = importlib.import_module(base_module)
36
+
37
+ for _, modname, _ in pkgutil.walk_packages(module.__path__, base_module + "."):
38
+ try:
39
+ submodule = importlib.import_module(modname)
40
+ for name, obj in inspect.getmembers(submodule, inspect.isclass):
41
+ if obj.__module__ == modname:
42
+ found[modname].append(name)
43
+ except Exception as e:
44
+ print(f"⚠️ Skipping {modname} due to import error: {e}")
45
+ return found
46
+
47
+ def write_module_rst_file(module_path, class_names, output_dir):
48
+ """
49
+ Writes a .rst file for a module, documenting all its classes.
50
+ Filename is based on the module, but title is the first class (Modusa* gets priority).
51
+ """
52
+ filename = output_dir / f"{module_path.split('.')[-1]}.rst"
53
+
54
+ # Prioritize 'Modusa*' classes first, then alphabetical
55
+ sorted_classes = sorted(class_names, key=lambda x: (not x.startswith("Modusa"), x.lower()))
56
+ title = sorted_classes[0] if sorted_classes else module_path.split('.')[-1]
57
+
58
+ with open(filename, 'w') as f:
59
+ f.write(f"{title}\n{'=' * len(title)}\n\n")
60
+ for class_name in sorted_classes:
61
+ f.write(f".. autoclass:: {module_path}.{class_name}\n")
62
+ f.write(" :members:\n")
63
+ f.write(" :undoc-members:\n")
64
+ f.write(" :show-inheritance:\n\n")
65
+
66
+ return filename.name
67
+
68
+ def write_index_rst_file(tools_by_module, output_dir, section_name="Tools"):
69
+ """
70
+ Writes index.rst in the given output_dir with 'base' on top, then other files alphabetically.
71
+ """
72
+ index_file = output_dir / "index.rst"
73
+ with open(index_file, "w") as f:
74
+ f.write(f"{section_name}\n{'=' * len(section_name)}\n\n")
75
+ f.write(".. toctree::\n :maxdepth: 1\n\n")
76
+
77
+ filenames = [module_path.split('.')[-1] for module_path in tools_by_module]
78
+ sorted_filenames = sorted(filenames, key=lambda x: (x != "base", x.lower()))
79
+
80
+ for name in sorted_filenames:
81
+ f.write(f" {name}\n")
82
+
83
+ # === Main Script ===
84
+ def generate_docs_source():
85
+ for base_module, output_dir in zip(BASE_MODULES, OUTPUT_DIRS):
86
+ module_class_map = get_classes_grouped_by_module(base_module)
87
+
88
+ for module_path, class_list in module_class_map.items():
89
+ write_module_rst_file(module_path, class_list, output_dir)
90
+
91
+ section_name = base_module.split('.')[-1].capitalize()
92
+ if section_name == "Io":
93
+ section_name = "IO"
94
+
95
+ write_index_rst_file(module_class_map, output_dir, section_name=section_name)
96
+ print(f"✅ Documentation generated for {base_module} in {output_dir}")
@@ -28,17 +28,17 @@ GENERATOR_INFO = {
28
28
  "template_dump_dp": SRC_CODE_DIR / "generators",
29
29
  "test_template_dump_dp": TESTS_DIR / "test_generators"
30
30
  }
31
- SIGNAL_INFO = {
32
- "template_fp": TEMPLATES_DIR / "signal.py",
31
+ MODEL_INFO = {
32
+ "template_fp": TEMPLATES_DIR / "model.py",
33
33
  "test_template_fp": TEMPLATES_DIR / "test.py",
34
- "template_dump_dp": SRC_CODE_DIR / "signals",
35
- "test_template_dump_dp": TESTS_DIR / "test_signals"
34
+ "template_dump_dp": SRC_CODE_DIR / "models",
35
+ "test_template_dump_dp": TESTS_DIR / "test_models"
36
36
  }
37
- ENGINE_INFO = {
38
- "template_fp": TEMPLATES_DIR / "engine.py",
37
+ TOOL_INFO = {
38
+ "template_fp": TEMPLATES_DIR / "tool.py",
39
39
  "test_template_fp": TEMPLATES_DIR / "test.py",
40
- "template_dump_dp": SRC_CODE_DIR / "engines",
41
- "test_template_dump_dp": TESTS_DIR / "test_engines"
40
+ "template_dump_dp": SRC_CODE_DIR / "tools",
41
+ "test_template_dump_dp": TESTS_DIR / "test_tools"
42
42
  }
43
43
 
44
44
 
@@ -53,8 +53,8 @@ class TemplateGenerator():
53
53
  def get_path_info(for_what: str):
54
54
  if for_what == "plugin": return PLUGIN_INFO
55
55
  if for_what == "io": return IO_INFO
56
- if for_what == "signal": return SIGNAL_INFO
57
- if for_what == "engine": return ENGINE_INFO
56
+ if for_what == "model": return MODEL_INFO
57
+ if for_what == "tool": return TOOL_INFO
58
58
  if for_what == "generator": return GENERATOR_INFO
59
59
 
60
60
  @staticmethod
@@ -75,7 +75,7 @@ class TemplateGenerator():
75
75
  print(f"⚠️ File already exists, choose another name.")
76
76
  sys.exit(1)
77
77
 
78
- class_name = questionary.text(f"Class name (CamelCase{for_what.capitalize()}): ").ask()
78
+ class_name = questionary.text(f"Class name (CamelCase): ").ask()
79
79
  if class_name is None:
80
80
  sys.exit(1)
81
81
 
@@ -138,7 +138,7 @@ class TemplateGenerator():
138
138
 
139
139
  # Save it to a file and put it in the correct folder
140
140
  TemplateGenerator.save_file(content=template_code, output_path=path_info['template_dump_dp'] / answers['module_name'])
141
- TemplateGenerator.save_file(content=test_code, output_path=path_info['test_template_dump_dp'] / answers['module_name'])
141
+ TemplateGenerator.save_file(content=test_code, output_path=path_info['test_template_dump_dp'] / f"test_{answers['module_name']}")
142
142
 
143
143
  print(f"✅ {for_what}:", "open " + str(path_info['template_dump_dp'] / answers['module_name']))
144
- print(f"✅ test:", "open " + str(path_info['test_template_dump_dp'] / answers['module_name']))
144
+ print(f"✅ test:", "open " + str(path_info['test_template_dump_dp'] / f"test_{answers['module_name']}"))
modusa/devtools/main.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
 
4
4
  from .generate_template import TemplateGenerator
5
+ from .generate_docs_source import generate_docs_source
5
6
  from .list_plugins import list_plugins
6
7
  from . import list_authors
7
8
 
@@ -20,12 +21,12 @@ def main():
20
21
  create_parser = subparsers.add_parser("create", help="Create new Modusa components")
21
22
  create_subparsers = create_parser.add_subparsers(dest="what", required=True)
22
23
 
23
- create_subparsers.add_parser("engine", help="Create a new engine class").set_defaults(func=lambda:TemplateGenerator.create_template("engine"))
24
+ create_subparsers.add_parser("tool", help="Create a new tool class").set_defaults(func=lambda:TemplateGenerator.create_template("tool"))
24
25
  create_subparsers.add_parser("plugin", help="Create a new plugin class").set_defaults(func=lambda:TemplateGenerator.create_template("plugin"))
25
- create_subparsers.add_parser("signal", help="Create a new signal class").set_defaults(func=lambda:TemplateGenerator.create_template("signal"))
26
+ create_subparsers.add_parser("model", help="Create a new model class").set_defaults(func=lambda:TemplateGenerator.create_template("model"))
26
27
  create_subparsers.add_parser("generator", help="Create a new signal generator class").set_defaults(func=lambda:TemplateGenerator.create_template("generator"))
27
28
  create_subparsers.add_parser("io", help="Create a new IO class").set_defaults(func=lambda:TemplateGenerator.create_template("io"))
28
-
29
+ create_subparsers.add_parser("docs", help="Generate the docs").set_defaults(func=lambda:generate_docs_source())
29
30
 
30
31
  # --- LIST group ---
31
32
  list_parser = subparsers.add_parser("list", help="List information about Modusa components")
@@ -3,7 +3,7 @@
3
3
 
4
4
  from modusa import excp
5
5
  from modusa.decorators import validate_args_type
6
- from modusa.generators import ModusaGenerator
6
+ from modusa.generators.base import ModusaGenerator
7
7
 
8
8
 
9
9
  class {class_name}(ModusaGenerator):
@@ -3,7 +3,7 @@
3
3
 
4
4
  from modusa import excp
5
5
  from modusa.decorators import validate_args_type
6
- from modusa.io import ModusaIO
6
+ from modusa.io.base import ModusaIO
7
7
 
8
8
 
9
9
  class {class_name}(ModusaIO):
@@ -3,17 +3,18 @@
3
3
 
4
4
  from modusa import excp
5
5
  from modusa.decorators import immutable_property, validate_args_type
6
- from modusa.signals.base import ModusaSignal
6
+ from modusa.signals.base import ?
7
7
  from typing import Self, Any
8
8
  import numpy as np
9
9
 
10
- class {class_name}(ModusaSignal):
10
+ class {class_name}():
11
11
  """
12
12
 
13
13
  """
14
14
 
15
15
  #--------Meta Information----------
16
16
  _name = ""
17
+ _nickname = "" # This is to be used in repr/str methods
17
18
  _description = ""
18
19
  _author_name = "{author_name}"
19
20
  _author_email = "{author_email}"
@@ -26,15 +27,21 @@ class {class_name}(ModusaSignal):
26
27
 
27
28
  self.title = "" # This title will be used as plot title by default
28
29
 
29
- #----------------------
30
+ #-----------------------------------
30
31
  # Properties
31
- #----------------------
32
+ #-----------------------------------
32
33
 
33
-
34
-
35
-
36
-
37
-
38
- #----------------------
34
+
35
+ #===================================
36
+
37
+
38
+
39
+
40
+
41
+
42
+ #-----------------------------------
39
43
  # Tools
40
- #----------------------
44
+ #-----------------------------------
45
+
46
+
47
+ #===================================
@@ -3,7 +3,7 @@
3
3
 
4
4
  from modusa import excp
5
5
  from modusa.decorators import immutable_property, validate_args_type, plugin_safety_check
6
- from modusa.plugins import ModusaPlugin
6
+ from modusa.plugins.base import ModusaPlugin
7
7
 
8
8
 
9
9
  class {class_name}(ModusaPlugin):
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
+ from modusa import excp
3
4
 
4
5
  #--------Meta Information----------
5
6
  _class_name = "{class_name}"
6
7
  _author_name = "{author_name}"
7
8
  _author_email = "{author_email}"
8
9
  _created_at = "{date_created}"
9
- #----------------------------------
10
-
11
- from modusa.{for_what}s import {class_name}
10
+ #----------------------------------
@@ -3,11 +3,10 @@
3
3
 
4
4
  from modusa import excp
5
5
  from modusa.decorators import validate_args_type
6
- from modusa.engines import ModusaEngine
7
- from typing import Any
6
+ from modusa.tools.base import ModusaTool
8
7
 
9
8
 
10
- class {class_name}(ModusaEngine):
9
+ class {class_name}(ModusaTool):
11
10
  """
12
11
 
13
12
  """
@@ -22,8 +21,4 @@ class {class_name}(ModusaEngine):
22
21
 
23
22
  def __init__(self):
24
23
  super().__init__()
25
-
26
-
27
- @validate_args_type()
28
- def run(self) -> Any:
29
- pass
24
+
@@ -2,4 +2,12 @@
2
2
 
3
3
  from .base import ModusaGenerator
4
4
 
5
- from .audio_waveforms import AudioWaveformGenerator
5
+ from .s_ax import SAxGen
6
+ from .t_ax import TAxGen
7
+
8
+ from .s1d import S1DGen
9
+ from .tds import TDSGen
10
+ from .audio import AudioGen
11
+
12
+ from .s2d import S2DGen
13
+ from .ftds import FTDSGen
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ from modusa import excp
5
+ from modusa.decorators import validate_args_type
6
+ from .base import ModusaGenerator
7
+ from modusa.models.t_ax import TAx
8
+ from modusa.models.audio import Audio
9
+ from modusa.models.data import Data
10
+ import numpy as np
11
+ from pathlib import Path
12
+
13
+ class AudioGen(ModusaGenerator):
14
+ """
15
+ Provides user friendly APIs to generate instances of different
16
+ `AudioSignal` instances.
17
+ """
18
+
19
+ #--------Meta Information----------
20
+ _name = ""
21
+ _description = ""
22
+ _author_name = "Ankit Anand"
23
+ _author_email = "ankit0.anand0@gmail.com"
24
+ _created_at = "2025-07-27"
25
+ #----------------------------------
26
+
27
+ @staticmethod
28
+ def from_array(
29
+ y: np.ndarray,
30
+ sr: float | int = 1.0,
31
+ t0: float | int = 0.0,
32
+ y_label: str = "Amplitude",
33
+ t_label: str = "Time (sec)",
34
+ title: str = "Audio Signal"
35
+ ) -> Audio:
36
+ """
37
+ Create `AudioSignal` instance from basic data structures.
38
+
39
+ .. code-block:: python
40
+
41
+ import modusa as ms
42
+ t = ms.tds.from_array([1, 2, 3])
43
+ print(t)
44
+ t.print_info()
45
+
46
+ Parameters
47
+ ----------
48
+ y: np.ndarray
49
+ - Audio data array.
50
+ sr: float | int
51
+ - Sampling rate.
52
+ t0: float | int
53
+ - Start timestamp.
54
+ y_label: str
55
+ - Y label for the signal.
56
+ - Default: "Y"
57
+ t_label: str
58
+ - T label for the signal.
59
+ - Default: "Time (sec)"
60
+ title: str
61
+ - Title for the signal.
62
+ - Default: "1D Signal"
63
+ Returns
64
+ -------
65
+ Audio
66
+ An instance of Audio.
67
+ """
68
+ assert isinstance(y, np.ndarray)
69
+ assert isinstance(sr, (int, float)) and isinstance(t0, (int, float))
70
+ assert isinstance(y_label, str) and isinstance(t_label, str) and isinstance(title, str)
71
+
72
+ assert y.ndim == 1
73
+
74
+ sr = float(sr)
75
+ t0 = float(t0)
76
+
77
+ t = TAx(n_points=y.shape[0], sr=sr, t0=t0, label=t_label) # Creating a signal axis instance
78
+ y = Data(values=y, label=y_label)
79
+
80
+ return Audio(y=y, t=t, title=title)
81
+
82
+ @classmethod
83
+ def from_youtube(cls, url: str, sr: int | float = None):
84
+ """
85
+ Loads audio from youtube at a given sr.
86
+ The audio is deleted from the device
87
+ after loading.
88
+
89
+ .. code-block:: python
90
+
91
+ import modusa as ms
92
+ audio = ms.audio.from_youtube(
93
+ url="https://www.youtube.com/watch?v=lIpw9-Y_N0g",
94
+ sr=None
95
+ )
96
+
97
+ PARAMETERS
98
+ ----------
99
+ url: str
100
+ Link to the YouTube video.
101
+ sr: int
102
+ Sampling rate to load the audio in.
103
+
104
+ Returns
105
+ -------
106
+ Audio:
107
+ An `Audio` instance with loaded audio content from YouTube.
108
+ """
109
+
110
+ from modusa.tools.youtube_downloader import YoutubeDownloader
111
+ from modusa import convert
112
+ import soundfile as sf
113
+ from scipy.signal import resample
114
+ import tempfile
115
+
116
+ # Download the audio in temp directory using tempfile module
117
+ with tempfile.TemporaryDirectory() as tmpdir:
118
+ audio_fp: Path = YoutubeDownloader.download(url=url, content_type="audio", output_dir=Path(tmpdir))
119
+
120
+ # Convert the audio to ".wav" form for loading
121
+ wav_audio_fp: Path = convert(inp_audio_fp=audio_fp, output_audio_fp=audio_fp.with_suffix(".wav"))
122
+
123
+ # Load the audio in memory
124
+ audio_data, audio_sr = sf.read(wav_audio_fp)
125
+
126
+ # Convert to mono if it's multi-channel
127
+ if audio_data.ndim > 1:
128
+ audio_data = audio_data.mean(axis=1)
129
+
130
+ # Resample if needed
131
+ if sr is not None:
132
+ if audio_sr != sr:
133
+ n_samples = int(len(audio_data) * sr / audio_sr)
134
+ audio_data = resample(audio_data, n_samples)
135
+ audio_sr = sr
136
+
137
+ audio = cls.from_array(y=audio_data, sr=audio_sr, title=audio_fp.stem)
138
+
139
+ return audio
140
+
141
+ @classmethod
142
+ def from_filepath(cls, fp: str | Path, sr: int | float = None):
143
+ """
144
+ Loads audio from filepath at a given sr.
145
+
146
+ .. code-block:: python
147
+
148
+ import modusa as ms
149
+ audio = ms.audio.from_filepath(
150
+ fp="path/to/audio.wav",
151
+ sr=None
152
+ )
153
+
154
+ PARAMETERS
155
+ ----------
156
+ fp: str | Path
157
+ Audio file path.
158
+ sr: int
159
+ Sampling rate to load the audio in.
160
+
161
+ Returns
162
+ -------
163
+ Audio:
164
+ An `Audio` instance with loaded audio content.
165
+ """
166
+ import soundfile as sf
167
+ from scipy.signal import resample
168
+ from pathlib import Path
169
+
170
+ fp = Path(fp)
171
+ # Load the audio in memory
172
+ audio_data, audio_sr = sf.read(fp)
173
+
174
+ # Convert to mono if it's multi-channel
175
+ if audio_data.ndim > 1:
176
+ audio_data = audio_data.mean(axis=1)
177
+
178
+ # Resample if needed
179
+ if sr is not None:
180
+ if audio_sr != sr:
181
+ n_samples = int(len(audio_data) * sr / audio_sr)
182
+ audio_data = resample(audio_data, n_samples)
183
+ audio_sr = sr
184
+
185
+ audio = cls.from_array(y=audio_data, sr=audio_sr, title=fp.stem)
186
+
187
+ return audio
188
+
@@ -3,18 +3,20 @@
3
3
 
4
4
  from modusa import excp
5
5
  from modusa.decorators import validate_args_type
6
- from modusa.generators import ModusaGenerator
7
- from modusa.signals import AudioSignal
6
+ from modusa.generators.base import ModusaGenerator
7
+ from modusa.models.audio_signal import AudioSignal
8
8
  import numpy as np
9
9
 
10
10
  class AudioWaveformGenerator(ModusaGenerator):
11
11
  """
12
-
12
+ Generates different kinds of audio waveforms particulary helpful
13
+ in teaching signal processing concepts and testing out newly
14
+ created tools.
13
15
  """
14
16
 
15
17
  #--------Meta Information----------
16
18
  _name = "Audio Waveform Generator"
17
- _description = ""
19
+ _description = "Generates different kind of audio waveforms."
18
20
  _author_name = "Ankit Anand"
19
21
  _author_email = "ankit0.anand0@gmail.com"
20
22
  _created_at = "2025-07-07"
@@ -23,7 +25,7 @@ class AudioWaveformGenerator(ModusaGenerator):
23
25
  @staticmethod
24
26
  def generate_example() -> "AudioSignal":
25
27
  """
26
- Generates a simple sine wave audio signal for demonstration purposes.
28
+ Generates a simple sine wave audio signal as an example.
27
29
 
28
30
  Returns
29
31
  -------
@@ -65,6 +67,7 @@ class AudioWaveformGenerator(ModusaGenerator):
65
67
  y = np.random.uniform(low=-1.0, high=1.0, size=num_samples) # use uniform [-1, 1] for audio-like signal
66
68
 
67
69
  signal = AudioSignal(y=y, t=t, title="Random")
70
+
68
71
  return signal
69
72
 
70
73
  @staticmethod
@@ -77,7 +80,8 @@ class AudioWaveformGenerator(ModusaGenerator):
77
80
  sr: int = 1000,
78
81
  ) -> "AudioSignal":
79
82
  """
80
- Generates a sinusoidal AudioSignal with specified amplitude, frequency, phase, duration, and sample rate.
83
+ Generates a sinusoid audio signal with specified
84
+ amplitude, frequency, phase, duration, and sample rate.
81
85
 
82
86
  Parameters
83
87
  ----------
@@ -95,14 +99,15 @@ class AudioWaveformGenerator(ModusaGenerator):
95
99
  Returns
96
100
  -------
97
101
  AudioSignal
98
- A time-domain sinusoidal signal with the given parameters.
102
+ A sinusoidal signal with the given parameters.
99
103
  """
100
104
  A, f, phi, duration, sr = float(A), float(f), float(phi), float(duration), int(sr)
101
105
 
102
106
  t = np.arange(0, duration, 1 / sr)
103
107
  y = A * np.sin(2 * np.pi * f * t + phi)
104
108
 
105
- signal = AudioSignal(y=y, sr=sr, title="Sinusoid")
109
+ signal = AudioSignal(y=y, sr=sr, title=f"Sinusoid ({f} Hz)")
110
+
106
111
  return signal
107
112
 
108
113
  @staticmethod
@@ -115,7 +120,8 @@ class AudioWaveformGenerator(ModusaGenerator):
115
120
  sr: int = 1000,
116
121
  ) -> "AudioSignal":
117
122
  """
118
- Generates a square wave AudioSignal with specified amplitude, frequency, phase, duration, and sample rate.
123
+ Generates a square wave audio signal with specified
124
+ amplitude, frequency, phase, duration, and sample rate.
119
125
 
120
126
  Parameters
121
127
  ----------
@@ -140,7 +146,8 @@ class AudioWaveformGenerator(ModusaGenerator):
140
146
 
141
147
  y = A * np.sign(np.sin(2 * np.pi * f * t + phi))
142
148
 
143
- signal = AudioSignal(y=y, sr=sr, title="Square")
149
+ signal = AudioSignal(y=y, sr=sr, title=f"Square ({f} Hz)")
150
+
144
151
  return signal
145
152
 
146
153
 
@@ -181,7 +188,7 @@ class AudioWaveformGenerator(ModusaGenerator):
181
188
  phase_offset = phi / (2 * np.pi)
182
189
  y = A * (2 * ((f * t + phase_offset) % 1) - 1)
183
190
 
184
- signal = AudioSignal(y=y, sr=sr, title="Sawtooth")
191
+ signal = AudioSignal(y=y, sr=sr, title=f"Sawtooth ({f} Hz)")
185
192
  return signal
186
193
 
187
194
 
@@ -195,7 +202,8 @@ class AudioWaveformGenerator(ModusaGenerator):
195
202
  sr: int = 1000,
196
203
  ) -> "AudioSignal":
197
204
  """
198
- Generates a triangle wave AudioSignal with specified amplitude, frequency, phase, duration, and sample rate.
205
+ Generates a triangle wave AudioSignal with specified
206
+ amplitude, frequency, phase, duration, and sample rate.
199
207
 
200
208
  Parameters
201
209
  ----------
@@ -222,6 +230,7 @@ class AudioWaveformGenerator(ModusaGenerator):
222
230
  # Triangle wave formula: 2 * abs(2 * frac(x) - 1) - 1 scaled to amplitude
223
231
  y = A * (2 * np.abs(2 * ((f * t + phase_offset) % 1) - 1) - 1)
224
232
 
225
- signal = AudioSignal(y=y, sr=sr, title="Triangle")
233
+ signal = AudioSignal(y=y, sr=sr, title=f"Triangle ({f} Hz)")
234
+
226
235
  return signal
227
236
 
modusa/generators/base.py CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  from modusa import excp
4
4
  from modusa.decorators import validate_args_type, immutable_property
5
- from modusa.signals import ModusaSignal
5
+ from modusa.models.base import ModusaSignal
6
6
  from abc import ABC, abstractmethod
7
7
  from typing import Any
8
8