-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscan_mels.py
More file actions
244 lines (209 loc) · 7.33 KB
/
Copy pathscan_mels.py
File metadata and controls
244 lines (209 loc) · 7.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
#!/usr/bin/env python3
"""
Scan a directory for .ogg audio, create Mel spectrograms (power), and
aggregate them to visualize the most occurring frequencies.
Outputs (in output/):
- spectrograms/<file>.png : per-file Mel spectrogram previews
- aggregated_mel_profile.png : overall frequency-occurrence profile (Hz vs score)
- aggregated_mel_profile.csv : table with [mel_hz, score]
"""
import argparse
import glob
import os
from pathlib import Path
import librosa
import librosa.display
import matplotlib.pyplot as plt
import numpy as np
import soundfile as sf # only for duration fallback (optional)
from scipy import signal
# ---------- DSP helpers ----------
def butter_bandpass(y, sr, fmin, fmax, order=6):
if fmin is None and fmax is None:
return y.astype(np.float32, copy=False)
nyq = sr / 2.0
lo = 0.0 if fmin in (None, 0) else (float(fmin) / nyq)
hi = 1.0 if fmax in (None, 0) else (float(fmax) / nyq)
if fmin and fmax:
b, a = signal.butter(order, [lo, hi], btype="band")
elif fmin:
b, a = signal.butter(order, lo, btype="highpass")
else:
b, a = signal.butter(order, hi, btype="lowpass")
return signal.lfilter(b, a, y).astype(np.float32, copy=False)
def notch_lines(y, sr, freqs_hz=None, bw_hz=60.0):
"""Zero-phase IIR notches; freqs_hz is a list like [4910.0]."""
if not freqs_hz:
return y
y_f = y
for f0 in freqs_hz:
if f0 <= 0 or f0 >= sr / 2:
continue
Q = float(f0) / float(bw_hz)
b, a = signal.iirnotch(f0 / (sr / 2), Q)
y_f = signal.filtfilt(b, a, y_f).astype(np.float32, copy=False)
return y_f
# ---------- Core processing ----------
def compute_mel_power(y, sr, n_fft, hop, n_mels, fmin, fmax):
# Power Mel (power=2) to match your thesis convention
S = librosa.feature.melspectrogram(
y=y,
sr=sr,
n_fft=n_fft,
hop_length=hop,
n_mels=n_mels,
fmin=fmin,
fmax=fmax,
power=2.0,
)
return S # shape (n_mels, T), power scale
def normalize_per_file(S):
# Normalize per file so loud files don't dominate the aggregate
m = S.max()
return S / (m + 1e-12)
def aggregate_mel_profiles(mel_list):
"""
mel_list: list of per-file Mel power arrays (n_mels, T) already normalized.
We collapse time per file (mean over time) -> (n_mels,), then average across files.
"""
if not mel_list:
raise ValueError("No Mel spectrograms to aggregate.")
profiles = [m.mean(axis=1) for m in mel_list] # (n_mels,)
stacked = np.stack(profiles, axis=0) # (N, n_mels)
return stacked.mean(axis=0) # (n_mels,)
def save_spectrogram_preview(S, sr, hop, fmin, fmax, out_png, title=None):
S_db = librosa.power_to_db(S, ref=np.max)
plt.figure(figsize=(8, 4))
librosa.display.specshow(
S_db,
sr=sr,
hop_length=hop,
x_axis="time",
y_axis="mel",
fmin=fmin,
fmax=fmax,
cmap="magma",
)
plt.colorbar(label="dB")
if title:
plt.title(title)
plt.tight_layout()
plt.savefig(out_png, dpi=150)
plt.close()
def process_file(path, args):
y, sr = librosa.load(path, sr=args.sr) # sr=None uses native; else resamples
# Optional cleanup (cheap):
if args.hp or args.lp:
y = butter_bandpass(y, sr, args.hp, args.lp, order=args.bp_order)
if args.notch:
y = notch_lines(y, sr, freqs_hz=args.notch, bw_hz=args.notch_bw)
S = compute_mel_power(
y=y,
sr=sr,
n_fft=args.n_fft,
hop=args.hop,
n_mels=args.n_mels,
fmin=args.fmin,
fmax=args.fmax,
)
Sn = normalize_per_file(S)
return S, Sn, sr
def main():
p = argparse.ArgumentParser(
description="Overlap Mel spectrograms to find most occurring frequencies."
)
p.add_argument(
"--input",
default="input",
help="Input directory (searched recursively) for .ogg",
)
p.add_argument("--output", default="output", help="Output directory")
p.add_argument(
"--sr", type=int, default=None, help="Target sample rate (None keeps native)"
)
p.add_argument("--n_fft", type=int, default=1024)
p.add_argument("--hop", type=int, default=256)
p.add_argument("--n_mels", type=int, default=96)
p.add_argument("--fmin", type=float, default=200.0)
p.add_argument("--fmax", type=float, default=7500.0)
p.add_argument(
"--hp",
type=float,
default=350.0,
help="High-pass cutoff (Hz); 0/None to disable",
)
p.add_argument(
"--lp",
type=float,
default=6500.0,
help="Low-pass cutoff (Hz); 0/None to disable",
)
p.add_argument("--bp_order", type=int, default=6, help="Butterworth order")
p.add_argument(
"--notch",
type=float,
nargs="*",
default=None,
help="Frequencies to notch (Hz), e.g. --notch 4910",
)
p.add_argument(
"--notch_bw", type=float, default=60.0, help="Notch bandwidth ~ -3 dB (Hz)"
)
args = p.parse_args()
in_dir = Path(args.input)
out_dir = Path(args.output)
spec_dir = out_dir / "spectrograms"
out_dir.mkdir(parents=True, exist_ok=True)
spec_dir.mkdir(parents=True, exist_ok=True)
files = [Path(p) for p in glob.glob(str(in_dir / "**" / "*.ogg"), recursive=True)]
if not files:
print(f"No .ogg files found under {in_dir.resolve()}")
return
mel_list_norm = []
first_sr = None
print(f"Found {len(files)} .ogg files. Processing…")
for i, fp in enumerate(sorted(files)):
try:
S, Sn, sr = process_file(str(fp), args)
if first_sr is None:
first_sr = sr
# Save preview for this file
out_png = spec_dir / (fp.stem + ".png")
save_spectrogram_preview(
S, sr, args.hop, args.fmin, args.fmax, out_png, title=fp.stem
)
mel_list_norm.append(Sn)
if (i + 1) % 10 == 0:
print(f" {i+1}/{len(files)} done")
except Exception as e:
print(f" Skipping {fp} due to error: {e}")
if not mel_list_norm:
print("No valid spectrograms to aggregate.")
return
# Aggregate to get overall frequency-occurrence profile
agg = aggregate_mel_profiles(mel_list_norm) # shape (n_mels,)
mel_hz = librosa.mel_frequencies(n_mels=args.n_mels, fmin=args.fmin, fmax=args.fmax)
# Save CSV
import csv
csv_path = Path(args.output) / "aggregated_mel_profile.csv"
with open(csv_path, "w", newline="") as f:
w = csv.writer(f)
w.writerow(["mel_hz", "score"])
for hz, val in zip(mel_hz, agg):
w.writerow([f"{hz:.3f}", f"{val:.8f}"])
# Plot frequency-occurrence profile
plt.figure(figsize=(9, 4))
plt.plot(mel_hz, agg)
plt.xlabel("Frequency (Hz)")
plt.ylabel("Occurrence score (avg normalized Mel power)")
plt.title("Aggregated frequency occurrence (power Mel, per-file normalized)")
plt.xlim([args.fmin, args.fmax])
plt.grid(True, alpha=0.25)
plt.tight_layout()
plt.savefig(Path(args.output) / "aggregated_mel_profile.png", dpi=150)
plt.close()
print(f"Saved: {csv_path}")
print(f"Saved: {Path(args.output) / 'aggregated_mel_profile.png'}")
print(f"Per-file previews in: {spec_dir}")
if __name__ == "__main__":
main()