modusa 0.1.0__py3-none-any.whl → 0.2.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 (48) hide show
  1. modusa/.DS_Store +0 -0
  2. modusa/decorators.py +5 -5
  3. modusa/devtools/generate_template.py +80 -15
  4. modusa/devtools/main.py +6 -4
  5. modusa/devtools/templates/{engines.py → engine.py} +8 -7
  6. modusa/devtools/templates/{generators.py → generator.py} +8 -10
  7. modusa/devtools/templates/io.py +24 -0
  8. modusa/devtools/templates/{plugins.py → plugin.py} +7 -6
  9. modusa/devtools/templates/signal.py +40 -0
  10. modusa/devtools/templates/test.py +11 -0
  11. modusa/engines/.DS_Store +0 -0
  12. modusa/engines/__init__.py +1 -2
  13. modusa/generators/__init__.py +3 -1
  14. modusa/generators/audio_waveforms.py +227 -0
  15. modusa/generators/base.py +14 -25
  16. modusa/io/__init__.py +9 -0
  17. modusa/io/audio_converter.py +76 -0
  18. modusa/io/audio_loader.py +212 -0
  19. modusa/io/audio_player.py +72 -0
  20. modusa/io/base.py +43 -0
  21. modusa/io/plotter.py +430 -0
  22. modusa/io/youtube_downloader.py +139 -0
  23. modusa/main.py +15 -17
  24. modusa/plugins/__init__.py +1 -7
  25. modusa/signals/__init__.py +4 -6
  26. modusa/signals/audio_signal.py +421 -175
  27. modusa/signals/base.py +11 -271
  28. modusa/signals/frequency_domain_signal.py +329 -0
  29. modusa/signals/signal_ops.py +158 -0
  30. modusa/signals/spectrogram.py +465 -0
  31. modusa/signals/time_domain_signal.py +309 -0
  32. modusa/utils/excp.py +5 -0
  33. {modusa-0.1.0.dist-info → modusa-0.2.1.dist-info}/METADATA +16 -11
  34. modusa-0.2.1.dist-info/RECORD +47 -0
  35. modusa/devtools/templates/signals.py +0 -63
  36. modusa/engines/plot_1dsignal.py +0 -130
  37. modusa/engines/plot_2dmatrix.py +0 -159
  38. modusa/generators/basic_waveform.py +0 -185
  39. modusa/plugins/plot_1dsignal.py +0 -59
  40. modusa/plugins/plot_2dmatrix.py +0 -76
  41. modusa/plugins/plot_time_domain_signal.py +0 -59
  42. modusa/signals/signal1d.py +0 -311
  43. modusa/signals/signal2d.py +0 -226
  44. modusa/signals/uniform_time_domain_signal.py +0 -212
  45. modusa-0.1.0.dist-info/RECORD +0 -41
  46. {modusa-0.1.0.dist-info → modusa-0.2.1.dist-info}/WHEEL +0 -0
  47. {modusa-0.1.0.dist-info → modusa-0.2.1.dist-info}/entry_points.txt +0 -0
  48. {modusa-0.1.0.dist-info → modusa-0.2.1.dist-info}/licenses/LICENSE.md +0 -0
modusa/.DS_Store CHANGED
Binary file
modusa/decorators.py CHANGED
@@ -103,7 +103,7 @@ def validate_arg(arg_name: str, value: Any, expected_type: Any) -> None:
103
103
  union_args = get_args(expected_type)
104
104
  for typ in union_args:
105
105
  typ_origin = get_origin(typ) or typ
106
- if type(value) is typ_origin:
106
+ if isinstance(value, typ_origin):
107
107
  return
108
108
 
109
109
  # ❌ If none match
@@ -116,17 +116,17 @@ def validate_arg(arg_name: str, value: Any, expected_type: Any) -> None:
116
116
 
117
117
  # Handle generic types like list[float], tuple[int, str]
118
118
  elif origin is not None:
119
- if type(value) is not origin:
119
+ if not isinstance(value, origin):
120
120
  raise excp.ValidationError(
121
- f"Argument '{arg_name}' must be exactly of type {origin.__name__}, got {type(value).__name__}"
121
+ f"Argument '{arg_name}' must be of type {origin.__name__}, got {type(value).__name__}"
122
122
  )
123
123
  return
124
124
 
125
125
  # ✅ Handle plain types
126
126
  elif isinstance(expected_type, type):
127
- if type(value) is not expected_type:
127
+ if not isinstance(value, expected_type):
128
128
  raise excp.ValidationError(
129
- f"Argument '{arg_name}' must be exactly {expected_type.__name__}, got {type(value).__name__}"
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
@@ -6,26 +6,76 @@ import questionary
6
6
  import sys
7
7
 
8
8
  ROOT_DIR = Path(__file__).parents[3].resolve()
9
+ SRC_CODE_DIR = ROOT_DIR / "src/modusa"
10
+ TESTS_DIR = ROOT_DIR / "tests"
9
11
  TEMPLATES_DIR = ROOT_DIR / "src/modusa/devtools/templates"
10
12
 
13
+ PLUGIN_INFO = {
14
+ "template_fp": TEMPLATES_DIR / "plugin.py",
15
+ "test_template_fp": TEMPLATES_DIR / "test.py",
16
+ "template_dump_dp": SRC_CODE_DIR / "plugins",
17
+ "test_template_dump_dp": TESTS_DIR / "test_plugins"
18
+ }
19
+ IO_INFO = {
20
+ "template_fp": TEMPLATES_DIR / "io.py",
21
+ "test_template_fp": TEMPLATES_DIR / "test.py",
22
+ "template_dump_dp": SRC_CODE_DIR / "io",
23
+ "test_template_dump_dp": TESTS_DIR / "test_io"
24
+ }
25
+ GENERATOR_INFO = {
26
+ "template_fp": TEMPLATES_DIR / "generator.py",
27
+ "test_template_fp": TEMPLATES_DIR / "test.py",
28
+ "template_dump_dp": SRC_CODE_DIR / "generators",
29
+ "test_template_dump_dp": TESTS_DIR / "test_generators"
30
+ }
31
+ SIGNAL_INFO = {
32
+ "template_fp": TEMPLATES_DIR / "signal.py",
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"
36
+ }
37
+ ENGINE_INFO = {
38
+ "template_fp": TEMPLATES_DIR / "engine.py",
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"
42
+ }
43
+
44
+
11
45
  class TemplateGenerator():
12
46
  """
13
- Generates template for `plugin`, `engine`, `signal`, `generator`.
47
+ Generates template for `plugin`, `engine`, `signal`, `generator` along with its corresponding `test` file
48
+ in the `tests` directory.
14
49
  """
15
50
 
51
+
16
52
  @staticmethod
17
- def ask_questions(for_what: str) -> dict:
53
+ def get_path_info(for_what: str):
54
+ if for_what == "plugin": return PLUGIN_INFO
55
+ if for_what == "io": return IO_INFO
56
+ if for_what == "signal": return SIGNAL_INFO
57
+ if for_what == "engine": return ENGINE_INFO
58
+ if for_what == "generator": return GENERATOR_INFO
59
+
60
+ @staticmethod
61
+ def ask_questions(for_what: str, path_info: dict) -> dict:
62
+ """Asks question about the template to be generated."""
18
63
  print("----------------------")
19
64
  print(for_what.upper())
20
65
  print("----------------------")
21
66
  module_name = questionary.text("Module name (snake_case): ").ask()
67
+
22
68
  if module_name is None:
23
69
  sys.exit(1)
24
- if Path(f"src/modusa/{for_what}/{module_name}.py").exists():
70
+
71
+ if not module_name.endswith(".py"): # Adding extension
72
+ module_name = module_name + ".py"
73
+ # Checking if the module name already exists in the dump directory
74
+ if (path_info["template_dump_dp"] / module_name).exists():
25
75
  print(f"⚠️ File already exists, choose another name.")
26
76
  sys.exit(1)
27
77
 
28
- class_name = questionary.text("Class name (CamelCase): ").ask()
78
+ class_name = questionary.text(f"Class name (CamelCase{for_what.capitalize()}): ").ask()
29
79
  if class_name is None:
30
80
  sys.exit(1)
31
81
 
@@ -37,43 +87,58 @@ class TemplateGenerator():
37
87
  if author_email is None:
38
88
  sys.exit(1)
39
89
 
40
- answers = {"module_name": module_name, "class_name": class_name, "author_name": author_name, "author_email": author_email, "date_created": date.today()}
90
+ answers = {"for_what": for_what, "module_name": module_name, "class_name": class_name, "author_name": author_name, "author_email": author_email, "date_created": date.today()}
41
91
 
42
92
  return answers
43
93
 
44
94
  @staticmethod
45
- def load_template_file(for_what: str) -> str:
46
- template_path = TEMPLATES_DIR / f"{for_what}.py"
47
- if not template_path.exists():
48
- print(f"❌ Template not found: {template_path}")
95
+ def load_template_file(template_fp: Path) -> str:
96
+ """Loads template file."""
97
+ if not template_fp.exists():
98
+ print(f"❌ Template not found: {template_fp}")
49
99
  sys.exit(1)
50
100
 
51
- template_code = template_path.read_text()
101
+ template_code = template_fp.read_text()
52
102
 
53
103
  return template_code
54
104
 
55
105
  @staticmethod
56
106
  def fill_placeholders(template_code: str, placehoders_dict: dict) -> str:
107
+ """Fills placeholder in the template with the user input from CLI."""
57
108
  template_code = template_code.format(**placehoders_dict) # Fill placeholders
58
109
  return template_code
59
110
 
60
111
  @staticmethod
61
112
  def save_file(content: str, output_path: Path) -> None:
113
+ """Saves file in the correct directory with the right tempalate content."""
62
114
  output_path.parent.mkdir(parents=True, exist_ok=True)
63
115
  output_path.write_text(content)
64
- print(f"✅ Successfully created.\n\n open {output_path.resolve()}")
116
+
117
+ # Generating a corresponding test file too
118
+ what_for = output_path.parent.name # plugins, generators, ...
119
+ module_name = output_path.name # this.py
120
+
65
121
 
66
122
  @staticmethod
67
123
  def create_template(for_what: str) -> None:
68
124
 
125
+ # Load correct path location info for the templates and where to dump the files
126
+ path_info: dict = TemplateGenerator.get_path_info(for_what)
127
+
69
128
  # Ask basic questions to create the template for `plugin`, `generator`, ...
70
- answers: dict = TemplateGenerator.ask_questions(for_what)
129
+ answers: dict = TemplateGenerator.ask_questions(for_what, path_info)
71
130
 
72
- # Load the correct template file
73
- template_code: str = TemplateGenerator.load_template_file(for_what)
131
+ # Load the correct template file and test file
132
+ template_code: str = TemplateGenerator.load_template_file(template_fp=path_info['template_fp'])
133
+ test_code: str = TemplateGenerator.load_template_file(template_fp=path_info['test_template_fp'])
74
134
 
75
135
  # Update the dynamic values based on the answers
76
136
  template_code: str = TemplateGenerator.fill_placeholders(template_code, answers)
137
+ test_code: str = TemplateGenerator.fill_placeholders(test_code, answers)
77
138
 
78
139
  # Save it to a file and put it in the correct folder
79
- TemplateGenerator.save_file(content=template_code, output_path=ROOT_DIR / f"src/modusa/{for_what}/{answers['module_name']}.py")
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'])
142
+
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']))
modusa/devtools/main.py CHANGED
@@ -20,10 +20,12 @@ def main():
20
20
  create_parser = subparsers.add_parser("create", help="Create new Modusa components")
21
21
  create_subparsers = create_parser.add_subparsers(dest="what", required=True)
22
22
 
23
- create_subparsers.add_parser("engine", help="Create a new engine class").set_defaults(func=lambda:TemplateGenerator.create_template("engines"))
24
- create_subparsers.add_parser("plugin", help="Create a new plugin class").set_defaults(func=lambda:TemplateGenerator.create_template("plugins"))
25
- create_subparsers.add_parser("signal", help="Create a new signal class").set_defaults(func=lambda:TemplateGenerator.create_template("signals"))
26
- create_subparsers.add_parser("generator", help="Create a new signal generator class").set_defaults(func=lambda:TemplateGenerator.create_template("generators"))
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("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("generator", help="Create a new signal generator class").set_defaults(func=lambda:TemplateGenerator.create_template("generator"))
27
+ create_subparsers.add_parser("io", help="Create a new IO class").set_defaults(func=lambda:TemplateGenerator.create_template("io"))
28
+
27
29
 
28
30
  # --- LIST group ---
29
31
  list_parser = subparsers.add_parser("list", help="List information about Modusa components")
@@ -3,20 +3,21 @@
3
3
 
4
4
  from modusa import excp
5
5
  from modusa.decorators import validate_args_type
6
- from modusa.engines.base import ModusaEngine
6
+ from modusa.engines import ModusaEngine
7
7
  from typing import Any
8
8
 
9
+
9
10
  class {class_name}(ModusaEngine):
10
11
  """
11
-
12
+
12
13
  """
13
14
 
14
15
  #--------Meta Information----------
15
- name = ""
16
- description = ""
17
- author_name = "{author_name}"
18
- author_email = "{author_email}"
19
- created_at = "{date_created}"
16
+ _name = ""
17
+ _description = ""
18
+ _author_name = "{author_name}"
19
+ _author_email = "{author_email}"
20
+ _created_at = "{date_created}"
20
21
  #----------------------------------
21
22
 
22
23
  def __init__(self):
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
3
 
4
+ from modusa import excp
4
5
  from modusa.decorators import validate_args_type
5
- from modusa.generators.base import ModusaGenerator
6
+ from modusa.generators import ModusaGenerator
6
7
 
7
8
 
8
9
  class {class_name}(ModusaGenerator):
@@ -11,16 +12,13 @@ class {class_name}(ModusaGenerator):
11
12
  """
12
13
 
13
14
  #--------Meta Information----------
14
- name = ""
15
- description = ""
16
- author_name = "{author_name}"
17
- author_email = "{author_email}"
18
- created_at = "{date_created}"
15
+ _name = ""
16
+ _description = ""
17
+ _author_name = "{author_name}"
18
+ _author_email = "{author_email}"
19
+ _created_at = "{date_created}"
19
20
  #----------------------------------
20
21
 
21
22
  def __init__(self):
22
23
  super().__init__()
23
-
24
-
25
- def generate(self) -> Any:
26
- pass
24
+
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ from modusa import excp
5
+ from modusa.decorators import validate_args_type
6
+ from modusa.io import ModusaIO
7
+
8
+
9
+ class {class_name}(ModusaIO):
10
+ """
11
+
12
+ """
13
+
14
+ #--------Meta Information----------
15
+ _name = ""
16
+ _description = ""
17
+ _author_name = "{author_name}"
18
+ _author_email = "{author_email}"
19
+ _created_at = "{date_created}"
20
+ #----------------------------------
21
+
22
+
23
+
24
+
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
3
 
4
- from modusa.plugins.base import ModusaPlugin
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
7
 
7
8
 
8
9
  class {class_name}(ModusaPlugin):
@@ -11,11 +12,11 @@ class {class_name}(ModusaPlugin):
11
12
  """
12
13
 
13
14
  #--------Meta Information----------
14
- name = ""
15
- description = ""
16
- author_name = "{author_name}"
17
- author_email = "{author_email}"
18
- created_at = "{date_created}"
15
+ _name = ""
16
+ _description = ""
17
+ _author_name = "{author_name}"
18
+ _author_email = "{author_email}"
19
+ _created_at = "{date_created}"
19
20
  #----------------------------------
20
21
 
21
22
  def __init__(self):
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ from modusa import excp
5
+ from modusa.decorators import immutable_property, validate_args_type
6
+ from modusa.signals.base import ModusaSignal
7
+ from typing import Self, Any
8
+ import numpy as np
9
+
10
+ class {class_name}(ModusaSignal):
11
+ """
12
+
13
+ """
14
+
15
+ #--------Meta Information----------
16
+ _name = ""
17
+ _description = ""
18
+ _author_name = "{author_name}"
19
+ _author_email = "{author_email}"
20
+ _created_at = "{date_created}"
21
+ #----------------------------------
22
+
23
+ @validate_args_type()
24
+ def __init__(self):
25
+ super().__init__() # Instantiating `ModusaSignal` class
26
+
27
+ self.title = "" # This title will be used as plot title by default
28
+
29
+ #----------------------
30
+ # Properties
31
+ #----------------------
32
+
33
+
34
+
35
+
36
+
37
+
38
+ #----------------------
39
+ # Tools
40
+ #----------------------
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ #--------Meta Information----------
5
+ _class_name = "{class_name}"
6
+ _author_name = "{author_name}"
7
+ _author_email = "{author_email}"
8
+ _created_at = "{date_created}"
9
+ #----------------------------------
10
+
11
+ from modusa.{for_what}s import {class_name}
Binary file
@@ -1,4 +1,3 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- from .plot_1dsignal import Plot1DSignalEngine
4
- from .plot_2dmatrix import Plot2DMatrixEngine
3
+ from .base import ModusaEngine
@@ -1,3 +1,5 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
- from .basic_waveform import BasicWaveformGenerator
3
+ from .base import ModusaGenerator
4
+
5
+ from .audio_waveforms import AudioWaveformGenerator
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ from modusa import excp
5
+ from modusa.decorators import validate_args_type
6
+ from modusa.generators import ModusaGenerator
7
+ from modusa.signals import AudioSignal
8
+ import numpy as np
9
+
10
+ class AudioWaveformGenerator(ModusaGenerator):
11
+ """
12
+
13
+ """
14
+
15
+ #--------Meta Information----------
16
+ _name = "Audio Waveform Generator"
17
+ _description = ""
18
+ _author_name = "Ankit Anand"
19
+ _author_email = "ankit0.anand0@gmail.com"
20
+ _created_at = "2025-07-07"
21
+ #----------------------------------
22
+
23
+ @staticmethod
24
+ def generate_example() -> "AudioSignal":
25
+ """
26
+ Generates a simple sine wave audio signal for demonstration purposes.
27
+
28
+ Returns
29
+ -------
30
+ AudioSignal
31
+ A 600 Hz sine wave lasting 2 seconds, sampled at 10,000 Hz.
32
+ """
33
+
34
+ sr = 10000 # Hz
35
+ duration = 2 # sec
36
+ freq = 600 # Hz
37
+
38
+ t = np.arange(0, duration, 1 / sr)
39
+ y = np.sin(2 * np.pi * freq * t)
40
+
41
+ signal = AudioSignal(y=y, sr=sr, title="Example") # assuming AudioSignal accepts y and t
42
+
43
+ return signal
44
+
45
+
46
+ @staticmethod
47
+ def generate_random(duration: float = 1.0, sr: int = 10000) -> "AudioSignal":
48
+ """
49
+ Generates a random audio signal of given duration and sample rate.
50
+
51
+ Parameters
52
+ ----------
53
+ duration : float, optional
54
+ Duration of the signal in seconds (default is 1.0).
55
+ sr : int, optional
56
+ Sampling rate in Hz (default is 10,000).
57
+
58
+ Returns
59
+ -------
60
+ AudioSignal
61
+ A randomly generated signal of the specified duration and sample rate.
62
+ """
63
+ num_samples = int(duration * sr)
64
+ t = np.linspace(0, duration, num=num_samples, endpoint=False)
65
+ y = np.random.uniform(low=-1.0, high=1.0, size=num_samples) # use uniform [-1, 1] for audio-like signal
66
+
67
+ signal = AudioSignal(y=y, t=t, title="Random")
68
+ return signal
69
+
70
+ @staticmethod
71
+ @validate_args_type()
72
+ def generate_sinusoid(
73
+ A: float | int = 1.0,
74
+ f: float | int = 10.0,
75
+ phi: float | int = 0.0,
76
+ duration: float | int = 1.0,
77
+ sr: int = 1000,
78
+ ) -> "AudioSignal":
79
+ """
80
+ Generates a sinusoidal AudioSignal with specified amplitude, frequency, phase, duration, and sample rate.
81
+
82
+ Parameters
83
+ ----------
84
+ A : float
85
+ Amplitude of the sinusoid (default: 1.0)
86
+ f : float
87
+ Frequency in Hz (default: 10.0)
88
+ phi : float
89
+ Phase in radians (default: 0.0)
90
+ duration : float
91
+ Duration of the signal in seconds (default: 1.0)
92
+ sr : int
93
+ Sampling rate in Hz (default: 1000)
94
+
95
+ Returns
96
+ -------
97
+ AudioSignal
98
+ A time-domain sinusoidal signal with the given parameters.
99
+ """
100
+ A, f, phi, duration, sr = float(A), float(f), float(phi), float(duration), int(sr)
101
+
102
+ t = np.arange(0, duration, 1 / sr)
103
+ y = A * np.sin(2 * np.pi * f * t + phi)
104
+
105
+ signal = AudioSignal(y=y, sr=sr, title="Sinusoid")
106
+ return signal
107
+
108
+ @staticmethod
109
+ @validate_args_type()
110
+ def generate_square(
111
+ A: float | int = 1.0,
112
+ f: float | int = 10.0,
113
+ phi: float | int = 0.0,
114
+ duration: float | int = 1.0,
115
+ sr: int = 1000,
116
+ ) -> "AudioSignal":
117
+ """
118
+ Generates a square wave AudioSignal with specified amplitude, frequency, phase, duration, and sample rate.
119
+
120
+ Parameters
121
+ ----------
122
+ A : float
123
+ Amplitude of the square wave (default: 1.0)
124
+ f : float
125
+ Frequency in Hz (default: 10.0)
126
+ phi : float
127
+ Phase in radians (default: 0.0)
128
+ duration : float
129
+ Duration of the signal in seconds (default: 1.0)
130
+ sr : int
131
+ Sampling rate in Hz (default: 1000)
132
+
133
+ Returns
134
+ -------
135
+ AudioSignal
136
+ A square wave signal of the specified parameters.
137
+ """
138
+ A, f, phi, duration, sr = float(A), float(f), float(phi), float(duration), int(sr)
139
+ t = np.arange(0, duration, 1 / sr)
140
+
141
+ y = A * np.sign(np.sin(2 * np.pi * f * t + phi))
142
+
143
+ signal = AudioSignal(y=y, sr=sr, title="Square")
144
+ return signal
145
+
146
+
147
+ @staticmethod
148
+ @validate_args_type()
149
+ def generate_sawtooth(
150
+ A: float | int = 1.0,
151
+ f: float | int = 10.0,
152
+ phi: float | int = 0.0,
153
+ duration: float | int = 1.0,
154
+ sr: int = 1000,
155
+ ) -> "AudioSignal":
156
+ """
157
+ Generates a sawtooth wave AudioSignal with specified amplitude, frequency, phase, duration, and sample rate.
158
+
159
+ Parameters
160
+ ----------
161
+ A : float
162
+ Amplitude of the sawtooth wave (default: 1.0)
163
+ f : float
164
+ Frequency in Hz (default: 10.0)
165
+ phi : float
166
+ Phase in radians (default: 0.0)
167
+ duration : float
168
+ Duration of the signal in seconds (default: 1.0)
169
+ sr : int
170
+ Sampling rate in Hz (default: 1000)
171
+
172
+ Returns
173
+ -------
174
+ AudioSignal
175
+ A sawtooth wave signal of the specified parameters.
176
+ """
177
+ A, f, phi, duration, sr = float(A), float(f), float(phi), float(duration), int(sr)
178
+ t = np.arange(0, duration, 1 / sr)
179
+
180
+ # Convert phase from radians to fractional cycle offset
181
+ phase_offset = phi / (2 * np.pi)
182
+ y = A * (2 * ((f * t + phase_offset) % 1) - 1)
183
+
184
+ signal = AudioSignal(y=y, sr=sr, title="Sawtooth")
185
+ return signal
186
+
187
+
188
+ @staticmethod
189
+ @validate_args_type()
190
+ def generate_triangle(
191
+ A: float | int = 1.0,
192
+ f: float | int = 10.0,
193
+ phi: float | int = 0.0,
194
+ duration: float | int = 1.0,
195
+ sr: int = 1000,
196
+ ) -> "AudioSignal":
197
+ """
198
+ Generates a triangle wave AudioSignal with specified amplitude, frequency, phase, duration, and sample rate.
199
+
200
+ Parameters
201
+ ----------
202
+ A : float
203
+ Amplitude of the triangle wave (default: 1.0)
204
+ f : float
205
+ Frequency in Hz (default: 10.0)
206
+ phi : float
207
+ Phase in radians (default: 0.0)
208
+ duration : float
209
+ Duration of the signal in seconds (default: 1.0)
210
+ sr : int
211
+ Sampling rate in Hz (default: 1000)
212
+
213
+ Returns
214
+ -------
215
+ AudioSignal
216
+ A triangle wave signal of the specified parameters.
217
+ """
218
+ A, f, phi, duration, sr = float(A), float(f), float(phi), float(duration), int(sr)
219
+ t = np.arange(0, duration, 1 / sr)
220
+ phase_offset = phi / (2 * np.pi) # Convert radians to cycle offset
221
+
222
+ # Triangle wave formula: 2 * abs(2 * frac(x) - 1) - 1 scaled to amplitude
223
+ y = A * (2 * np.abs(2 * ((f * t + phase_offset) % 1) - 1) - 1)
224
+
225
+ signal = AudioSignal(y=y, sr=sr, title="Triangle")
226
+ return signal
227
+
modusa/generators/base.py CHANGED
@@ -8,33 +8,22 @@ from typing import Any
8
8
 
9
9
  class ModusaGenerator(ABC):
10
10
  """
11
- Base class for any generator.
11
+ Base class for any type of signal generators for modusa framework.
12
+
13
+ Note
14
+ ----
15
+ - This class is intended to be subclassed by any Generator related tools built for the modusa framework.
16
+ - In order to create a generator tool, you can use modusa-dev CLI to generate an generator template.
17
+ - It is recommended to treat subclasses of ModusaGenerator as namespaces and define @staticmethods with control parameters, rather than using instance-level __init__ methods.
18
+
12
19
 
13
- Generates instance of different `ModusaSignal` subclass.
14
20
  """
15
21
 
16
22
  #--------Meta Information----------
17
- name = ""
18
- description = ""
19
- author_name = "Ankit Anand"
20
- author_email = "ankit0.anand0@gmail.com"
21
- created_at = "2025-07-04"
23
+ _name = ""
24
+ _description = ""
25
+ _author_name = "Ankit Anand"
26
+ _author_email = "ankit0.anand0@gmail.com"
27
+ _created_at = "2025-07-04"
22
28
  #----------------------------------
23
-
24
- def __init__(self, signal_cls: Any):
25
-
26
- if not issubclass(signal_cls, ModusaSignal):
27
- raise excp.InputValueError(f"`signal_cls` must be a subclass of ModusaSignal, got {signal_cls.__name__}")
28
- if signal_cls not in self.allowed_output_signal_types:
29
- raise excp.InputValueError(f"`signal_cls` must be a one of the allowed types {self.allowed_output_signal_types}, got {signal_cls.__name__}")
30
-
31
- self._signal_cls = signal_cls
32
-
33
- @immutable_property("Mutation not allowed.")
34
- def signal_cls(self) -> Any:
35
- return self._signal_cls
36
-
37
- @property
38
- @abstractmethod
39
- def allowed_output_signal_types(self) -> tuple[type, ...]:
40
- return
29
+
modusa/io/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from .base import ModusaIO
4
+
5
+ from .audio_loader import AudioLoader
6
+ from .audio_converter import AudioConverter
7
+ from .youtube_downloader import YoutubeDownloader
8
+ from .plotter import Plotter
9
+ from .audio_player import AudioPlayer