modusa 0.4.20__py3-none-any.whl → 0.4.21__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/__init__.py CHANGED
@@ -8,4 +8,4 @@ from modusa.tools import play, convert, record, save
8
8
  from modusa.tools import download
9
9
  from modusa.tools import load, load_ann
10
10
 
11
- __version__ = "0.4.20"
11
+ __version__ = "0.4.21"
@@ -6,7 +6,7 @@
6
6
  # Email: ankit0.anand0@gmail.com
7
7
  #---------------------------------
8
8
 
9
- def load_ann(path, clip=None):
9
+ def load_ann(path, trim=None):
10
10
  """
11
11
  Load annotation from audatity label
12
12
  text file and also ctm file.
@@ -15,9 +15,9 @@ def load_ann(path, clip=None):
15
15
  ----------
16
16
  path: str
17
17
  - label text/ctm file path.
18
- clip: tuple[number, number] | number | None
19
- - Incase you clipped the audio signal, this parameter will help clip the annotation.
20
- - If you clip the audio, say from (10, 20), set the clip to (10, 20).
18
+ trim: tuple[number, number] | number | None
19
+ - Incase you trimmed the audio signal, this parameter will help clip the annotation making sure that the timings are aligned to the trimmed audio.
20
+ - If you trimmed the audio, say from (10, 20), set the trim to (10, 20).
21
21
  - Default: None
22
22
 
23
23
  Returns
@@ -42,14 +42,14 @@ def load_ann(path, clip=None):
42
42
  ann = [] # This will store the annotation
43
43
 
44
44
  # Clipping the annotation to match with the clipped audio
45
- if clip is not None:
45
+ if trim is not None:
46
46
  # Map clip input to the right format
47
- if isinstance(clip, int or float):
48
- clip = (0, clip)
49
- elif isinstance(clip, tuple) and len(clip) > 1:
50
- clip = (clip[0], clip[1])
47
+ if isinstance(trim, int or float):
48
+ trim = (0, trim)
49
+ elif isinstance(trim, tuple) and len(trim) > 1:
50
+ trim = (trim[0], trim[1])
51
51
  else:
52
- raise ValueError(f"Invalid clip type or length: {type(clip)}, len={len(clip)}")
52
+ raise ValueError(f"Invalid clip type or length: {type(trim)}, len={len(trim)}")
53
53
 
54
54
  if path.suffix == ".txt":
55
55
  with open(str(path), "r") as f:
@@ -60,11 +60,11 @@ def load_ann(path, clip=None):
60
60
 
61
61
  # Incase user has clipped the audio signal, we adjust the annotation
62
62
  # to match the clipped audio
63
- if clip is not None:
64
- offset = clip[0]
63
+ if trim is not None:
64
+ offset = trim[0]
65
65
  # Clamp annotation to clip boundaries
66
- new_start = max(start, clip[0]) - offset
67
- new_end = min(end, clip[1]) - offset
66
+ new_start = max(start, trim[0]) - offset
67
+ new_end = min(end, trim[1]) - offset
68
68
 
69
69
  # only keep if there's still overlap
70
70
  if new_start < new_end:
@@ -89,11 +89,11 @@ def load_ann(path, clip=None):
89
89
 
90
90
  # Incase user has clipped the audio signal, we adjust the annotation
91
91
  # to match the clipped audio
92
- if clip is not None:
93
- offset = clip[0]
92
+ if trim is not None:
93
+ offset = trim[0]
94
94
  # Clamp annotation to clip boundaries
95
- new_start = max(start, clip[0]) - offset
96
- new_end = min(end, clip[1]) - offset
95
+ new_start = max(start, trim[0]) - offset
96
+ new_end = min(end, trim[1]) - offset
97
97
 
98
98
  # only keep if there's still overlap
99
99
  if new_start < new_end:
@@ -3,14 +3,14 @@
3
3
 
4
4
  import soundfile as sf
5
5
  from scipy.signal import resample
6
+ import numpy as np
6
7
  from pathlib import Path
7
8
  import tempfile
8
9
  from scipy.signal import resample
9
10
  from .youtube_downloader import download
10
11
  from .audio_converter import convert
11
12
 
12
-
13
- def load(path, sr=None, clip=None):
13
+ def load(path, sr=None, trim=None, mono=True):
14
14
  """
15
15
  Loads audio file from various sources.
16
16
 
@@ -19,19 +19,21 @@ def load(path, sr=None, clip=None):
19
19
  import modusa as ms
20
20
  audio_fp = ms.load(
21
21
  "https://www.youtube.com/watch?v=lIpw9-Y_N0g",
22
- sr = None, clip=(5, 10))
22
+ sr = None, trim=(5, 10))
23
23
 
24
24
  Parameters
25
25
  ----------
26
26
  path: str
27
- - Path to the audio
28
- - Youtube URL
27
+ - Path to the audio file.
28
+ - YouTube URL.
29
29
  sr: int | None
30
30
  - Sampling rate to load the audio in.
31
- clip: number | tuple[number, number] | None
32
- - Which segment of the audio you want.
33
- - Eg., 10 => First 10 sec, (5, 10) => 5 to 10 second
31
+ trim: number | tuple[number, number] | None
32
+ - Segment of the audio to load.
33
+ - Example: 10 => First 10 seconds, (5, 10) => 5 to 10 seconds.
34
34
  - Default: None => Entire audio.
35
+ mono: bool
36
+ - If True, loads the signal in mono.
35
37
 
36
38
  Return
37
39
  ------
@@ -43,7 +45,6 @@ def load(path, sr=None, clip=None):
43
45
  - Title of the loaded audio.
44
46
  - Filename without extension or YouTube title.
45
47
  """
46
-
47
48
  # Check if the path is YouTube
48
49
  if ".youtube." in str(path):
49
50
  # Download the audio in temp directory using tempfile module
@@ -57,50 +58,52 @@ def load(path, sr=None, clip=None):
57
58
  # Load the audio in memory
58
59
  audio_data, audio_sr = sf.read(wav_audio_fp)
59
60
  title = audio_fp.stem
60
-
61
- # Convert to mono if it's multi-channel
62
- if audio_data.ndim > 1:
63
- audio_data = audio_data.mean(axis=1)
64
-
65
- # Resample if needed
66
- if sr is not None:
67
- if audio_sr != sr:
68
- n_samples = int(len(audio_data) * sr / audio_sr)
69
- audio_data = resample(audio_data, n_samples)
70
- audio_sr = sr
71
-
72
61
  else:
73
62
  # Check if the file exists
74
63
  fp = Path(path)
75
64
 
76
65
  if not fp.exists():
77
66
  raise FileNotFoundError(f"{path} does not exist.")
78
-
67
+
79
68
  # Load the audio in memory
80
69
  audio_data, audio_sr = sf.read(fp)
81
70
  title = fp.stem
82
71
 
83
- # Convert to mono if it's multi-channel
84
- if audio_data.ndim > 1:
85
- audio_data = audio_data.mean(axis=1)
72
+ # Convert to mono if requested and it's multi-channel
73
+ if mono and audio_data.ndim > 1:
74
+ audio_data = audio_data.mean(axis=1)
75
+
76
+ # Resample if needed
77
+ if sr is not None and audio_sr != sr:
78
+ n_samples = int(len(audio_data) * sr / audio_sr)
79
+
80
+ if audio_data.ndim == 1:
81
+ # Mono
82
+ audio_data = resample(audio_data, n_samples)
83
+ else:
84
+ # Stereo or multi-channel: resample each channel independently
85
+ audio_data = np.stack([
86
+ resample(audio_data[:, ch], n_samples)
87
+ for ch in range(audio_data.shape[1])
88
+ ], axis=1)
86
89
 
87
- # Resample if needed
88
- if sr is not None:
89
- if audio_sr != sr:
90
- n_samples = int(len(audio_data) * sr / audio_sr)
91
- audio_data = resample(audio_data, n_samples)
92
- audio_sr = sr
93
-
94
- # Clip the audio signal as per needed
95
- if clip is not None:
96
- # Map clip input to the right format
97
- if isinstance(clip, int or float):
98
- clip = (0, clip)
99
- elif isinstance(clip, tuple) and len(clip) > 1:
100
- clip = (clip[0], clip[1])
90
+ audio_sr = sr
91
+
92
+ # Trim if requested
93
+ if trim is not None:
94
+ if isinstance(trim, (int, float)):
95
+ trim = (0, trim)
96
+ elif isinstance(trim, tuple) and len(trim) > 1:
97
+ trim = (trim[0], trim[1])
101
98
  else:
102
- raise ValueError(f"Invalid clip type or length: {type(clip)}, len={len(clip)}")
99
+ raise ValueError(f"Invalid trim type or length: {type(trim)}, len={len(trim)}")
100
+
101
+ start = int(trim[0] * audio_sr)
102
+ end = int(trim[1] * audio_sr)
103
+ audio_data = audio_data[start:end]
104
+
105
+ # Clip to avoid out-of-range playback issues
106
+ if np.issubdtype(audio_data.dtype, np.floating):
107
+ audio_data = np.clip(audio_data, -1.0, 1.0)
103
108
 
104
- audio_data = audio_data[int(clip[0]*audio_sr):int(clip[1]*audio_sr)]
105
-
106
- return audio_data, audio_sr, title
109
+ return audio_data.T, audio_sr, title
@@ -5,17 +5,27 @@ from pathlib import Path
5
5
  import numpy as np
6
6
  import base64
7
7
 
8
- def play(
9
- y: np.ndarray,
10
- sr: float,
11
- clip: tuple[float, float] | None = None,
12
- label: str | None = None,
13
- ) -> None:
8
+ def play(y, sr: float, clip=None, label=None):
14
9
  """
15
- Audio player with optional clip selection, transcription-style label,
16
- and an embedded bottom-right logo (../images/icon.png).
10
+ Audio player with optional clip selection, transcription-style label.
11
+
12
+ Parameters
13
+ ----------
14
+ y: ndarray
15
+ - Audio signal.
16
+ sr: float
17
+ - Sampling rate.
18
+ clip: tuple[float, float] | None
19
+ - The portion from the audio signal to be played.
20
+ label: str | None
21
+ - Could be transcription/labels attached to the audio.
22
+
23
+ Returns
24
+ -------
25
+ None
17
26
  """
18
- start_time, end_time = 0.0, len(y) / sr
27
+ start_time = 0.0
28
+ end_time = len(y) / sr if y.ndim < 2 else y[0].size / sr
19
29
 
20
30
  # Optional clip selection
21
31
  if clip is not None:
@@ -84,7 +94,7 @@ def play(
84
94
  box-shadow:0 1px 3px rgba(0,0,0,0.05);
85
95
  ">
86
96
  {label_html}
87
- <div style="margin-top:10px;">
97
+ <div style="margin-top:10px; margin-bottom:10px">
88
98
  {audio_html}
89
99
  </div>
90
100
  {logo_html}
modusa/tools/plotter.py CHANGED
@@ -209,6 +209,10 @@ class Fig:
209
209
 
210
210
  axs[-1, 0].tick_params(bottom=True, labelbottom=True)
211
211
 
212
+ # Add the figure title on top-left (if any)
213
+ if self._fig_num is not None:
214
+ fig.suptitle(f'fig - {self._fig_num}', fontsize=12, fontweight='bold', x=0.01, ha='left', va='top', y=0.98)
215
+
212
216
  # xlim should be applied on reference subplot, rest all subplots will automatically adjust
213
217
  if xlim is not None:
214
218
  axs[0, 0].set_xlim(xlim)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modusa
3
- Version: 0.4.20
3
+ Version: 0.4.21
4
4
  Summary: A modular signal analysis python library.
5
5
  Author-Email: Ankit Anand <ankit0.anand0@gmail.com>
6
6
  License: MIT
@@ -1,8 +1,8 @@
1
- modusa-0.4.20.dist-info/METADATA,sha256=pJ932mbsVsJ8XTbIHPX3h1RZa5k-GUBk0i5zgsqGMKY,1467
2
- modusa-0.4.20.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
- modusa-0.4.20.dist-info/entry_points.txt,sha256=fmKpleVXj6CdaBVL14WoEy6xx7JQCs85jvzwTi3lePM,73
4
- modusa-0.4.20.dist-info/licenses/LICENSE.md,sha256=JTaXAjx5awk76VArKCx5dUW8vmLEWsL_ZlR7-umaHbA,1078
5
- modusa/__init__.py,sha256=x95HzUy8e7c6XeEIZsuy6QmE82fcfJsnWvTUm8u6WyE,324
1
+ modusa-0.4.21.dist-info/METADATA,sha256=Rlr19wBffotWSOhku0JBPBSn91tUW7mRKSCxVsm7pMg,1467
2
+ modusa-0.4.21.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
+ modusa-0.4.21.dist-info/entry_points.txt,sha256=fmKpleVXj6CdaBVL14WoEy6xx7JQCs85jvzwTi3lePM,73
4
+ modusa-0.4.21.dist-info/licenses/LICENSE.md,sha256=JTaXAjx5awk76VArKCx5dUW8vmLEWsL_ZlR7-umaHbA,1078
5
+ modusa/__init__.py,sha256=PwHhnV8JyHP8unMnnTXjJmqQtd1E-b4vUbuo6m6n_tw,324
6
6
  modusa/config.py,sha256=bTqK4t00FZqERVITrxW_q284aDDJAa9aMSfFknfR-oU,280
7
7
  modusa/decorators.py,sha256=8zeNX_wE37O6Vp0ysR4-WCZaEL8mq8dyCF_I5DHOzks,5905
8
8
  modusa/devtools/generate_docs_source.py,sha256=UDflHsk-Yh9-3YJTVBzKL32y8hcxiRgAlFEBTMiDqwM,3301
@@ -42,15 +42,15 @@ modusa/plugins/__init__.py,sha256=r1Bf5mnrVKRIwxboutY1iGzDy4EPQhqpk1kSW7iJj_Q,54
42
42
  modusa/plugins/base.py,sha256=Bh_1Bja7fOymFsCgwhXDbV6ys3D8muNrPwrfDrG_G_A,2382
43
43
  modusa/tools/__init__.py,sha256=S7P1uYckyUdkha2UX9uj4P7mqpF6cc5SHqiW6NEupgs,342
44
44
  modusa/tools/_plotter_old.py,sha256=KGow7mihA2H1WNq7s5bpivhCgGo2qVIeDaO6iabpsrg,19294
45
- modusa/tools/ann_loader.py,sha256=BEdwAh_lccx_SnAO3bNMeY3O5zGiJlH2o4snWmXj8eQ,3034
45
+ modusa/tools/ann_loader.py,sha256=m6Qu6jXnQ8LfUhKItoHSaHlGxUyzUJlGEyu4_50qJ8w,3099
46
46
  modusa/tools/audio_converter.py,sha256=415qBoPm2sBIuBSI7m1XBKm0AbmVmPydIPPr-uO8D3c,1778
47
- modusa/tools/audio_loader.py,sha256=n9Q9t_GmlE8AtioVwRcXX3rnd6PkbGTx-hAoNgUnNOQ,2780
48
- modusa/tools/audio_player.py,sha256=BI1ZmETxnmJACDHZMsbplzspwAx-_oKe3SpaHz-MFds,2295
47
+ modusa/tools/audio_loader.py,sha256=vzoEIhuWedHcMbqxMXQkjiUAvkpXh5vTsR-L-NnFjHM,2928
48
+ modusa/tools/audio_player.py,sha256=kyBUnodkOE9Ox-hKHkfPeGAQ1RPTddbZYXO1ezz6-9w,2494
49
49
  modusa/tools/audio_recorder.py,sha256=K_LGqsPdjTdf3figEZTSQLmgMzYWgz18HTO8C1j5fE4,2788
50
50
  modusa/tools/audio_saver.py,sha256=ldzfr_AydsHTnKbxmBLJblN-hLzTmOlppOm306xI4Ug,510
51
51
  modusa/tools/base.py,sha256=C0ESJ0mIfjjRlAkRbSetNtMoOfS6IrHBjexRp3l_Mh4,1293
52
52
  modusa/tools/math_ops.py,sha256=ZZ7U4DgqT7cOeE7_Lzi_Qq-48WYfwR9_osbZwTmE9eg,8690
53
- modusa/tools/plotter.py,sha256=V0CcRnXkE4tkTZQe6v0LI0ucEagqeudOZ-xoQw2clTc,30667
53
+ modusa/tools/plotter.py,sha256=RrELZYgnyUVhGzcNfpTvyZvVTmZ6dvQmN9YSQoUjmaM,30860
54
54
  modusa/tools/youtube_downloader.py,sha256=hB_X8-7nOHXOlxg6vv3wyhBLoAsWyomrULP6_uCQL7s,1698
55
55
  modusa/utils/__init__.py,sha256=1oLL20yLB1GL9IbFiZD8OReDqiCpFr-yetIR6x1cNkI,23
56
56
  modusa/utils/config.py,sha256=cuGbqbovx5WDQq5rw3hIKcv3CnE5NttjacSOWnP1yu4,576
@@ -58,4 +58,4 @@ modusa/utils/excp.py,sha256=L9vhaGjKpv9viJYdmC9n5ndmk2GVbUBuFyZyhAQZmWY,906
58
58
  modusa/utils/logger.py,sha256=K0rsnObeNKCxlNeSnVnJeRhgfmob6riB2uyU7h3dDmA,571
59
59
  modusa/utils/np_func_cat.py,sha256=TyIFgRc6bARRMDnZxlVURO5Z0I-GWhxRONYyIv-Vwxs,1007
60
60
  modusa/utils/plot.py,sha256=s_vNdxvKfwxEngvJPgrF1PcmxZNnNaaXPViHWjyjJ-c,5335
61
- modusa-0.4.20.dist-info/RECORD,,
61
+ modusa-0.4.21.dist-info/RECORD,,