modusa 0.4.21__py3-none-any.whl → 0.4.23__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 ADDED
Binary file
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.21"
11
+ __version__ = "0.4.23"
@@ -1,109 +1,155 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
3
 
4
- import soundfile as sf
5
- from scipy.signal import resample
4
+ import subprocess
6
5
  import numpy as np
6
+ import imageio_ffmpeg as ffmpeg
7
7
  from pathlib import Path
8
- import tempfile
9
- from scipy.signal import resample
10
- from .youtube_downloader import download
11
- from .audio_converter import convert
8
+ import re
12
9
 
13
- def load(path, sr=None, trim=None, mono=True):
10
+ def _get_audio_info_ffmpeg(path: Path):
14
11
  """
15
- Loads audio file from various sources.
12
+ To get the original sampling rate and number of
13
+ channels of a given audio file by parsing the
14
+ metadata. (No extra tool required).
16
15
 
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, trim=(5, 10))
16
+ Parameters
17
+ ----------
18
+ audiofp: PathLike
19
+ - Audio filepath
20
+
21
+ Returns
22
+ -------
23
+ int
24
+ - Original sampling rate (hz)
25
+ int
26
+ - Number of channels
27
+ """
28
+ ffmpeg_exe = ffmpeg.get_ffmpeg_exe()
29
+ cmd = [ffmpeg_exe, "-i", str(path)]
30
+ proc = subprocess.run(cmd, stderr=subprocess.PIPE, text=True)
31
+ text = proc.stderr
32
+
33
+ # Example parse: "Stream #0:0: Audio: mp3, 44100 Hz, stereo, ..."
34
+ m = re.search(r'Audio:.*?(\d+)\s*Hz.*?(mono|stereo)', text)
35
+ if not m:
36
+ raise RuntimeError("Could not parse audio info")
37
+ sr = int(m.group(1))
38
+ channels = 1 if m.group(2) == "mono" else 2
39
+ return sr, channels
40
+
41
+ def _load_audio_from_youtube(url: str):
42
+ """
43
+ Download audio from a YouTube URL, convert it to WAV, and return the path.
23
44
 
24
45
  Parameters
25
46
  ----------
26
- path: str
27
- - Path to the audio file.
28
- - YouTube URL.
29
- sr: int | None
47
+ url : str
48
+ YouTube video URL.
49
+
50
+ Returns
51
+ -------
52
+ Path
53
+ Path to the converted WAV file (you can delete it later).
54
+ """
55
+ from modusa.tools.youtube_downloader import download
56
+ from modusa.tools.audio_converter import convert
57
+ import tempfile
58
+
59
+ # Temporary directory to hold files (auto-created, not auto-deleted)
60
+ tmpdir = Path(tempfile.mkdtemp())
61
+
62
+ # Download YouTube audio (e.g. .m4a or .webm)
63
+ audio_fp: Path = download(url=url, content_type="audio", output_dir=tmpdir)
64
+
65
+ # Convert downloaded file to .wav
66
+ wav_audio_fp: Path = convert(inp_audio_fp=audio_fp, output_audio_fp=audio_fp.with_suffix(".wav"))
67
+
68
+ # Return path to the WAV file
69
+ return wav_audio_fp
70
+
71
+ #---------------------
72
+ # Main Function
73
+ #---------------------
74
+ def load(path, sr=None, trim=None, ch=None):
75
+ """
76
+ Lightweight audio loader using imageio-ffmpeg.
77
+
78
+ Parameters
79
+ ----------
80
+ path: PathLike/str/URL
81
+ - Path to the audio file / YouTube video
82
+ sr: int
30
83
  - Sampling rate to load the audio in.
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
- - Default: None => Entire audio.
35
- mono: bool
36
- - If True, loads the signal in mono.
84
+ - Default: None => Use the original sampling rate
85
+ trim: tuple[number, number]
86
+ - (start, end) in seconds to trim the audio clip.
87
+ - Default: None => No trimming
88
+ ch: int
89
+ - 1 for mono and 2 for stereo
90
+ - Default: None => Use the original number of channels.
37
91
 
38
- Return
39
- ------
92
+ Returns
93
+ -------
40
94
  np.ndarray
41
- - Audio signal.
42
- int
43
- - Sampling rate of the loaded audio signal.
44
- title
45
- - Title of the loaded audio.
46
- - Filename without extension or YouTube title.
95
+ - Audio signal Float32 waveform in [-1, 1].
96
+ int:
97
+ Sampling rate.
98
+ str:
99
+ File name stem.
47
100
  """
48
- # Check if the path is YouTube
49
- if ".youtube." in str(path):
50
- # Download the audio in temp directory using tempfile module
51
- with tempfile.TemporaryDirectory() as tmpdir:
52
- # Download
53
- audio_fp: Path = download(url=path, content_type="audio", output_dir=Path(tmpdir))
54
-
55
- # Convert the audio to ".wav" form for loading
56
- wav_audio_fp: Path = convert(inp_audio_fp=audio_fp, output_audio_fp=audio_fp.with_suffix(".wav"))
57
-
58
- # Load the audio in memory
59
- audio_data, audio_sr = sf.read(wav_audio_fp)
60
- title = audio_fp.stem
61
- else:
62
- # Check if the file exists
63
- fp = Path(path)
64
-
65
- if not fp.exists():
66
- raise FileNotFoundError(f"{path} does not exist.")
67
-
68
- # Load the audio in memory
69
- audio_data, audio_sr = sf.read(fp)
70
- title = fp.stem
101
+ path = Path(path)
102
+ ffmpeg_exe = ffmpeg.get_ffmpeg_exe()
103
+
104
+ yt = False # Is the path a youtube URL
105
+
106
+ if ".youtube" in str(path):
107
+ yt = True
108
+ try:
109
+ path: Path = _load_audio_from_youtube(url=str(path))
110
+ except Exception as e:
111
+ raise ConnectionRefusedError("unable to download from YouTube")
112
+
113
+ # Find the real sample rate from the file
114
+ if sr is None:
115
+ sr, _ = _get_audio_info_ffmpeg(path)
116
+ if not (sr > 100 and sr < 80000):
117
+ raise Exception(f"Error reading the metadata for original sampling rate {sr}, please set `sr` explicitly")
118
+
119
+ # Find the real number of channels from the file
120
+ if ch is None:
121
+ _, ch = _get_audio_info_ffmpeg(path)
71
122
 
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)
123
+ if ch not in [1, 2]:
124
+ raise Exception(f"Error reading the metadata for number of channels {ch}, please set `ch` explicitly")
75
125
 
76
- # Resample if needed
77
- if sr is not None and audio_sr != sr:
78
- n_samples = int(len(audio_data) * sr / audio_sr)
126
+ cmd = [ffmpeg_exe]
127
+
128
+ # Optional trimming
129
+ if trim is not None:
130
+ start, end = trim
131
+ duration = end - start
132
+ cmd += ["-ss", str(start), "-t", str(duration)]
79
133
 
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)
89
-
90
- audio_sr = sr
134
+ cmd += ["-i", str(path), "-f", "s16le", "-acodec", "pcm_s16le"]
135
+ cmd += ["-ar", str(sr)]
136
+ cmd += ["-ac", str(ch)]
91
137
 
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])
98
- else:
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]
138
+ cmd += ["-"]
139
+
140
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
141
+ raw = proc.stdout.read()
142
+ proc.wait()
143
+
144
+ audio = np.frombuffer(raw, np.int16).astype(np.float32) / 32768.0
145
+
146
+ # Stereo reshaping if forced
147
+ if ch == 2:
148
+ audio = audio.reshape(-1, 2).T
104
149
 
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)
150
+ # Delete the file if downloaded from youtube
151
+ if yt:
152
+ path.unlink(missing_ok=True)
153
+ path.parent.rmdir()
108
154
 
109
- return audio_data.T, audio_sr, title
155
+ return audio, sr, path.stem
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: modusa
3
- Version: 0.4.21
3
+ Version: 0.4.23
4
4
  Summary: A modular signal analysis python library.
5
5
  Author-Email: Ankit Anand <ankit0.anand0@gmail.com>
6
6
  License: MIT
@@ -9,10 +9,8 @@ Requires-Dist: numpy>=2.2.6
9
9
  Requires-Dist: matplotlib>=3.10.3
10
10
  Requires-Dist: yt-dlp==2025.9.23
11
11
  Requires-Dist: IPython>=9.5.0
12
- Requires-Dist: sounddevice>=0.5.2
13
12
  Requires-Dist: ipywidgets>=8.1.7
14
- Requires-Dist: soundfile>=0.13.1
15
- Requires-Dist: scipy>=1.16.2
13
+ Requires-Dist: imageio-ffmpeg>=0.6.0
16
14
  Description-Content-Type: text/markdown
17
15
 
18
16
  # modusa
@@ -1,8 +1,9 @@
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
1
+ modusa-0.4.23.dist-info/METADATA,sha256=VcG0sWHvNgl2n0jMCiKiWM2BAeGHUJorSp0pFogxx90,1408
2
+ modusa-0.4.23.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90
3
+ modusa-0.4.23.dist-info/entry_points.txt,sha256=fmKpleVXj6CdaBVL14WoEy6xx7JQCs85jvzwTi3lePM,73
4
+ modusa-0.4.23.dist-info/licenses/LICENSE.md,sha256=JTaXAjx5awk76VArKCx5dUW8vmLEWsL_ZlR7-umaHbA,1078
5
+ modusa/.DS_Store,sha256=hHecruWmea5u7ITMSdq62sVVdvT2I5lTN20gTfD3Btk,8196
6
+ modusa/__init__.py,sha256=90isCiArafICPkaTgChbljh2F5o_iDKhqMz1gArlDOE,324
6
7
  modusa/config.py,sha256=bTqK4t00FZqERVITrxW_q284aDDJAa9aMSfFknfR-oU,280
7
8
  modusa/decorators.py,sha256=8zeNX_wE37O6Vp0ysR4-WCZaEL8mq8dyCF_I5DHOzks,5905
8
9
  modusa/devtools/generate_docs_source.py,sha256=UDflHsk-Yh9-3YJTVBzKL32y8hcxiRgAlFEBTMiDqwM,3301
@@ -44,7 +45,7 @@ modusa/tools/__init__.py,sha256=S7P1uYckyUdkha2UX9uj4P7mqpF6cc5SHqiW6NEupgs,342
44
45
  modusa/tools/_plotter_old.py,sha256=KGow7mihA2H1WNq7s5bpivhCgGo2qVIeDaO6iabpsrg,19294
45
46
  modusa/tools/ann_loader.py,sha256=m6Qu6jXnQ8LfUhKItoHSaHlGxUyzUJlGEyu4_50qJ8w,3099
46
47
  modusa/tools/audio_converter.py,sha256=415qBoPm2sBIuBSI7m1XBKm0AbmVmPydIPPr-uO8D3c,1778
47
- modusa/tools/audio_loader.py,sha256=vzoEIhuWedHcMbqxMXQkjiUAvkpXh5vTsR-L-NnFjHM,2928
48
+ modusa/tools/audio_loader.py,sha256=nQl-E8xM1wdoYzZ3yGw25FJY8EogbqIZzPSMFt4Fv1E,3899
48
49
  modusa/tools/audio_player.py,sha256=kyBUnodkOE9Ox-hKHkfPeGAQ1RPTddbZYXO1ezz6-9w,2494
49
50
  modusa/tools/audio_recorder.py,sha256=K_LGqsPdjTdf3figEZTSQLmgMzYWgz18HTO8C1j5fE4,2788
50
51
  modusa/tools/audio_saver.py,sha256=ldzfr_AydsHTnKbxmBLJblN-hLzTmOlppOm306xI4Ug,510
@@ -58,4 +59,4 @@ modusa/utils/excp.py,sha256=L9vhaGjKpv9viJYdmC9n5ndmk2GVbUBuFyZyhAQZmWY,906
58
59
  modusa/utils/logger.py,sha256=K0rsnObeNKCxlNeSnVnJeRhgfmob6riB2uyU7h3dDmA,571
59
60
  modusa/utils/np_func_cat.py,sha256=TyIFgRc6bARRMDnZxlVURO5Z0I-GWhxRONYyIv-Vwxs,1007
60
61
  modusa/utils/plot.py,sha256=s_vNdxvKfwxEngvJPgrF1PcmxZNnNaaXPViHWjyjJ-c,5335
61
- modusa-0.4.21.dist-info/RECORD,,
62
+ modusa-0.4.23.dist-info/RECORD,,