modusa 0.4.20__tar.gz → 0.4.23__tar.gz
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-0.4.20 → modusa-0.4.23}/PKG-INFO +2 -4
- {modusa-0.4.20 → modusa-0.4.23}/pyproject.toml +2 -4
- modusa-0.4.23/src/modusa/.DS_Store +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/__init__.py +1 -1
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/tools/ann_loader.py +18 -18
- modusa-0.4.23/src/modusa/tools/audio_loader.py +155 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/tools/audio_player.py +20 -10
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/tools/plotter.py +4 -0
- modusa-0.4.23/tests/.DS_Store +0 -0
- modusa-0.4.23/tests/test_load.py +158 -0
- modusa-0.4.23/tests/testdata/.DS_Store +0 -0
- modusa-0.4.23/tests/testdata/audio-formats/sample.aac +0 -0
- modusa-0.4.23/tests/testdata/audio-formats/sample.aiff +0 -0
- modusa-0.4.23/tests/testdata/audio-formats/sample.flac +0 -0
- modusa-0.4.23/tests/testdata/audio-formats/sample.m4a +0 -0
- modusa-0.4.23/tests/testdata/audio-formats/sample.mp3 +0 -0
- modusa-0.4.23/tests/testdata/audio-formats/sample.opus +0 -0
- modusa-0.4.23/tests/testdata/audio-formats/sample.wav +0 -0
- modusa-0.4.20/src/modusa/tools/audio_loader.py +0 -106
- {modusa-0.4.20 → modusa-0.4.23}/LICENSE.md +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/README.md +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/config.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/decorators.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/devtools/generate_docs_source.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/devtools/generate_template.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/devtools/list_authors.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/devtools/list_plugins.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/devtools/main.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/devtools/templates/generator.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/devtools/templates/io.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/devtools/templates/model.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/devtools/templates/plugin.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/devtools/templates/test.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/devtools/templates/tool.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/fonts/NotoSansDevanagari-Regular.ttf +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/generators/__init__.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/generators/audio.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/generators/audio_waveforms.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/generators/base.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/generators/ftds.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/generators/s1d.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/generators/s2d.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/generators/s_ax.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/generators/t_ax.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/generators/tds.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/images/icon.png +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/models/__init__.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/models/audio.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/models/base.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/models/data.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/models/ftds.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/models/s1d.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/models/s2d.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/models/s_ax.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/models/t_ax.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/models/tds.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/plugins/__init__.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/plugins/base.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/tools/__init__.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/tools/_plotter_old.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/tools/audio_converter.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/tools/audio_recorder.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/tools/audio_saver.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/tools/base.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/tools/math_ops.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/tools/youtube_downloader.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/utils/__init__.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/utils/config.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/utils/excp.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/utils/logger.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/utils/np_func_cat.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/src/modusa/utils/plot.py +0 -0
- {modusa-0.4.20 → modusa-0.4.23}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: modusa
|
3
|
-
Version: 0.4.
|
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:
|
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
|
@@ -10,14 +10,12 @@ dependencies = [
|
|
10
10
|
"matplotlib>=3.10.3",
|
11
11
|
"yt-dlp==2025.9.23",
|
12
12
|
"IPython>=9.5.0",
|
13
|
-
"sounddevice>=0.5.2",
|
14
13
|
"ipywidgets>=8.1.7",
|
15
|
-
"
|
16
|
-
"scipy>=1.16.2",
|
14
|
+
"imageio-ffmpeg>=0.6.0",
|
17
15
|
]
|
18
16
|
requires-python = ">=3.11"
|
19
17
|
readme = "README.md"
|
20
|
-
version = "0.4.
|
18
|
+
version = "0.4.23"
|
21
19
|
|
22
20
|
[project.license]
|
23
21
|
text = "MIT"
|
Binary file
|
@@ -6,7 +6,7 @@
|
|
6
6
|
# Email: ankit0.anand0@gmail.com
|
7
7
|
#---------------------------------
|
8
8
|
|
9
|
-
def load_ann(path,
|
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
|
-
|
19
|
-
- Incase you
|
20
|
-
- If you
|
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
|
45
|
+
if trim is not None:
|
46
46
|
# Map clip input to the right format
|
47
|
-
if isinstance(
|
48
|
-
|
49
|
-
elif isinstance(
|
50
|
-
|
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(
|
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
|
64
|
-
offset =
|
63
|
+
if trim is not None:
|
64
|
+
offset = trim[0]
|
65
65
|
# Clamp annotation to clip boundaries
|
66
|
-
new_start = max(start,
|
67
|
-
new_end = min(end,
|
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
|
93
|
-
offset =
|
92
|
+
if trim is not None:
|
93
|
+
offset = trim[0]
|
94
94
|
# Clamp annotation to clip boundaries
|
95
|
-
new_start = max(start,
|
96
|
-
new_end = min(end,
|
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:
|
@@ -0,0 +1,155 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
|
4
|
+
import subprocess
|
5
|
+
import numpy as np
|
6
|
+
import imageio_ffmpeg as ffmpeg
|
7
|
+
from pathlib import Path
|
8
|
+
import re
|
9
|
+
|
10
|
+
def _get_audio_info_ffmpeg(path: Path):
|
11
|
+
"""
|
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).
|
15
|
+
|
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.
|
44
|
+
|
45
|
+
Parameters
|
46
|
+
----------
|
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
|
83
|
+
- Sampling rate to load the audio in.
|
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.
|
91
|
+
|
92
|
+
Returns
|
93
|
+
-------
|
94
|
+
np.ndarray
|
95
|
+
- Audio signal Float32 waveform in [-1, 1].
|
96
|
+
int:
|
97
|
+
Sampling rate.
|
98
|
+
str:
|
99
|
+
File name stem.
|
100
|
+
"""
|
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)
|
122
|
+
|
123
|
+
if ch not in [1, 2]:
|
124
|
+
raise Exception(f"Error reading the metadata for number of channels {ch}, please set `ch` explicitly")
|
125
|
+
|
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)]
|
133
|
+
|
134
|
+
cmd += ["-i", str(path), "-f", "s16le", "-acodec", "pcm_s16le"]
|
135
|
+
cmd += ["-ar", str(sr)]
|
136
|
+
cmd += ["-ac", str(ch)]
|
137
|
+
|
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
|
149
|
+
|
150
|
+
# Delete the file if downloaded from youtube
|
151
|
+
if yt:
|
152
|
+
path.unlink(missing_ok=True)
|
153
|
+
path.parent.rmdir()
|
154
|
+
|
155
|
+
return audio, sr, path.stem
|
@@ -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
|
-
|
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
|
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}
|
@@ -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)
|
Binary file
|
@@ -0,0 +1,158 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
|
3
|
+
#---------------------------------
|
4
|
+
# Author: Ankit Anand
|
5
|
+
# Date: 21/10/25
|
6
|
+
# Email: ankit0.anand0@gmail.com
|
7
|
+
#---------------------------------
|
8
|
+
|
9
|
+
import pytest
|
10
|
+
import modusa as ms
|
11
|
+
from pathlib import Path
|
12
|
+
|
13
|
+
|
14
|
+
#----------------------------------
|
15
|
+
# Loading different audio formats
|
16
|
+
#----------------------------------
|
17
|
+
this_dir = Path(__file__).parents[0].resolve()
|
18
|
+
def test_load_aac():
|
19
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.aac")
|
20
|
+
assert title == "sample"
|
21
|
+
|
22
|
+
def test_load_aiff():
|
23
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.aiff")
|
24
|
+
assert title == "sample"
|
25
|
+
|
26
|
+
def test_load_flac():
|
27
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.flac")
|
28
|
+
assert title == "sample"
|
29
|
+
|
30
|
+
def test_load_m4a():
|
31
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.m4a")
|
32
|
+
assert title == "sample"
|
33
|
+
|
34
|
+
def test_load_mp3():
|
35
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.mp3")
|
36
|
+
assert title == "sample"
|
37
|
+
|
38
|
+
def test_load_opus():
|
39
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.opus")
|
40
|
+
assert title == "sample"
|
41
|
+
|
42
|
+
def test_load_wav():
|
43
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.wav")
|
44
|
+
assert title == "sample"
|
45
|
+
|
46
|
+
#----------------------------------
|
47
|
+
# Resampe feature
|
48
|
+
#----------------------------------
|
49
|
+
SR = 16000 # Hz
|
50
|
+
def test_load_with_resample_aac():
|
51
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.aac", sr=SR)
|
52
|
+
assert sr == SR
|
53
|
+
assert title == "sample"
|
54
|
+
|
55
|
+
def test_load_with_resample_aiff():
|
56
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.aiff", sr=SR)
|
57
|
+
assert sr == SR
|
58
|
+
assert title == "sample"
|
59
|
+
|
60
|
+
def test_load_with_resample_flac():
|
61
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.flac", sr=SR)
|
62
|
+
assert sr == SR
|
63
|
+
assert title == "sample"
|
64
|
+
|
65
|
+
def test_load_with_resample_m4a():
|
66
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.m4a", sr=SR)
|
67
|
+
assert sr == SR
|
68
|
+
assert title == "sample"
|
69
|
+
|
70
|
+
def test_load_with_resample_mp3():
|
71
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.mp3", sr=SR)
|
72
|
+
assert sr == SR
|
73
|
+
assert title == "sample"
|
74
|
+
|
75
|
+
def test_load_with_resample_opus():
|
76
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.opus", sr=SR)
|
77
|
+
assert sr == SR
|
78
|
+
assert title == "sample"
|
79
|
+
|
80
|
+
def test_load_with_resample_wav():
|
81
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.wav", sr=SR)
|
82
|
+
assert sr == SR
|
83
|
+
assert title == "sample"
|
84
|
+
|
85
|
+
#----------------------------------
|
86
|
+
# Trim feature
|
87
|
+
#----------------------------------
|
88
|
+
def test_load_with_trim_aac():
|
89
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.aac", trim=(0, 5.3))
|
90
|
+
assert title == "sample"
|
91
|
+
|
92
|
+
def test_load_with_trim_aiff():
|
93
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.aiff", trim=(0, 5.3))
|
94
|
+
assert title == "sample"
|
95
|
+
|
96
|
+
def test_load_with_trim_flac():
|
97
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.flac", trim=(0, 5.3))
|
98
|
+
assert title == "sample"
|
99
|
+
|
100
|
+
def test_load_with_trim_m4a():
|
101
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.m4a", trim=(0, 5.3))
|
102
|
+
assert title == "sample"
|
103
|
+
|
104
|
+
def test_load_with_trim_mp3():
|
105
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.mp3", trim=(0, 5.3))
|
106
|
+
assert title == "sample"
|
107
|
+
|
108
|
+
def test_load_with_trim_opus():
|
109
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.opus", trim=(0, 5.3))
|
110
|
+
assert title == "sample"
|
111
|
+
|
112
|
+
def test_load_with_trim_wav():
|
113
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.wav", trim=(0, 5.3))
|
114
|
+
assert title == "sample"
|
115
|
+
|
116
|
+
#----------------------------------
|
117
|
+
# Mono feature
|
118
|
+
#----------------------------------
|
119
|
+
def test_load_in_stereo_aac():
|
120
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.aac", ch=2)
|
121
|
+
assert y.ndim == 2
|
122
|
+
assert title == "sample"
|
123
|
+
|
124
|
+
def test_load_in_stereo_aiff():
|
125
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.aiff", ch=2)
|
126
|
+
assert y.ndim == 2
|
127
|
+
assert title == "sample"
|
128
|
+
|
129
|
+
def test_load_in_stereo_flac():
|
130
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.flac", ch=2)
|
131
|
+
assert y.ndim == 2
|
132
|
+
assert title == "sample"
|
133
|
+
|
134
|
+
def test_load_in_stereo_m4a():
|
135
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.m4a", ch=2)
|
136
|
+
assert y.ndim == 2
|
137
|
+
assert title == "sample"
|
138
|
+
|
139
|
+
def test_load_in_stereo_mp3():
|
140
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.mp3", ch=2)
|
141
|
+
assert y.ndim == 2
|
142
|
+
assert title == "sample"
|
143
|
+
|
144
|
+
def test_load_in_stereo_opus():
|
145
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.opus", ch=2)
|
146
|
+
assert y.ndim == 2
|
147
|
+
assert title == "sample"
|
148
|
+
|
149
|
+
def test_load_in_stereo_wav():
|
150
|
+
y, sr, title = ms.load(this_dir / "testdata/audio-formats/sample.wav", ch=2)
|
151
|
+
assert y.ndim == 2
|
152
|
+
assert title == "sample"
|
153
|
+
|
154
|
+
#----------------------------------
|
155
|
+
# Load from YouTube
|
156
|
+
#----------------------------------
|
157
|
+
def test_load_youtube_1():
|
158
|
+
y, sr, title = ms.load("https://www.youtube.com/watch?v=DIU_vmElPkU", ch=1)
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -1,106 +0,0 @@
|
|
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, clip=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, clip=(5, 10))
|
23
|
-
|
24
|
-
Parameters
|
25
|
-
----------
|
26
|
-
path: str
|
27
|
-
- Path to the audio
|
28
|
-
- Youtube URL
|
29
|
-
sr: int | None
|
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
|
34
|
-
- Default: None => Entire audio.
|
35
|
-
|
36
|
-
Return
|
37
|
-
------
|
38
|
-
np.ndarray
|
39
|
-
- Audio signal.
|
40
|
-
int
|
41
|
-
- Sampling rate of the loaded audio signal.
|
42
|
-
title
|
43
|
-
- Title of the loaded audio.
|
44
|
-
- Filename without extension or YouTube title.
|
45
|
-
"""
|
46
|
-
|
47
|
-
# Check if the path is YouTube
|
48
|
-
if ".youtube." in str(path):
|
49
|
-
# Download the audio in temp directory using tempfile module
|
50
|
-
with tempfile.TemporaryDirectory() as tmpdir:
|
51
|
-
# Download
|
52
|
-
audio_fp: Path = download(url=path, content_type="audio", output_dir=Path(tmpdir))
|
53
|
-
|
54
|
-
# Convert the audio to ".wav" form for loading
|
55
|
-
wav_audio_fp: Path = convert(inp_audio_fp=audio_fp, output_audio_fp=audio_fp.with_suffix(".wav"))
|
56
|
-
|
57
|
-
# Load the audio in memory
|
58
|
-
audio_data, audio_sr = sf.read(wav_audio_fp)
|
59
|
-
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
|
-
else:
|
73
|
-
# Check if the file exists
|
74
|
-
fp = Path(path)
|
75
|
-
|
76
|
-
if not fp.exists():
|
77
|
-
raise FileNotFoundError(f"{path} does not exist.")
|
78
|
-
|
79
|
-
# Load the audio in memory
|
80
|
-
audio_data, audio_sr = sf.read(fp)
|
81
|
-
title = fp.stem
|
82
|
-
|
83
|
-
# Convert to mono if it's multi-channel
|
84
|
-
if audio_data.ndim > 1:
|
85
|
-
audio_data = audio_data.mean(axis=1)
|
86
|
-
|
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])
|
101
|
-
else:
|
102
|
-
raise ValueError(f"Invalid clip type or length: {type(clip)}, len={len(clip)}")
|
103
|
-
|
104
|
-
audio_data = audio_data[int(clip[0]*audio_sr):int(clip[1]*audio_sr)]
|
105
|
-
|
106
|
-
return audio_data, audio_sr, title
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|