File size: 3,618 Bytes
c729991 | 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 | """Audio loading, mel-spectrogram extraction, and dataset preparation."""
import os
from pathlib import Path
from typing import Optional
import librosa
import numpy as np
from tqdm import tqdm
from audiovec.config import SAMPLE_RATE, N_MELS, FMAX, MAX_PAD_LEN
def load_and_process_audio(
file_path: str | Path,
max_pad_len: Optional[int] = None,
) -> np.ndarray:
"""Load a WAV file and return a normalized mel-spectrogram.
Steps:
1. Load audio at the configured sample rate.
2. Compute a mel-spectrogram (128 bands, 8 kHz max freq).
3. Convert to dB scale.
4. Pad or trim to ``max_pad_len`` time-steps.
5. Add a channel dimension.
"""
audio, sr = librosa.load(file_path, sr=SAMPLE_RATE)
# Mel-spectrogram
S = librosa.feature.melspectrogram(y=audio, sr=sr, n_mels=N_MELS, fmax=FMAX)
# Convert to dB scale
S_dB = librosa.power_to_db(S, ref=np.max)
# Normalise to [0, 1] so that ReLU activations retain signal
# power_to_db with default top_db=80 yields values in [-80, 0]
S_dB = (S_dB + 80.0) / 80.0
# Pad or trim to a fixed length
if max_pad_len is not None:
pad_width = max_pad_len - S_dB.shape[1]
if pad_width > 0:
S_dB = np.pad(S_dB, pad_width=((0, 0), (0, pad_width)), mode="constant")
else:
S_dB = S_dB[:, :max_pad_len]
# Add channel dimension: (n_mels, time, 1)
S_dB = S_dB[..., np.newaxis]
return S_dB
def parse_emotion_from_filename(filename: str) -> int:
"""Extract the emotion label from a RAVDESS filename.
The RAVDESS filename format is::
Modality-VocalChannel-Emotion-Intensity-Statement-Repetition-Actor.wav
The emotion is the third field (1 = neutral, 2 = calm, … 8 = surprised).
"""
return int(filename.split("-")[2])
def load_ravdess_dataset(
data_dir: str | Path,
max_pad_len: Optional[int] = MAX_PAD_LEN,
) -> tuple[np.ndarray, np.ndarray]:
"""Walk through *data_dir/* and load all ``.wav`` files.
Returns
-------
X : ndarray, shape (n_samples, n_mels, max_pad_len, 1)
Mel-spectrograms.
y : ndarray, shape (n_samples,)
Integer emotion labels (1–8).
"""
data_dir = Path(data_dir)
data: list[np.ndarray] = []
labels: list[int] = []
actor_dirs = sorted(
d for d in data_dir.iterdir()
if d.is_dir() and not d.name.startswith(".")
)
if not actor_dirs:
# Maybe the dataset is flat — try looking for wav files directly
wav_files = sorted(data_dir.glob("*.wav"))
if wav_files:
for file_path in tqdm(wav_files, desc="Loading audio"):
spectrogram = load_and_process_audio(file_path, max_pad_len)
emotion = parse_emotion_from_filename(file_path.name)
data.append(spectrogram)
labels.append(emotion)
X = np.array(data)
y = np.array(labels)
return X, y
for subdir in tqdm(actor_dirs, desc="Loading audio"):
for filename in os.listdir(subdir):
file_path = subdir / filename
if file_path.suffix.lower() != ".wav":
continue
try:
spectrogram = load_and_process_audio(file_path, max_pad_len)
emotion = parse_emotion_from_filename(filename)
data.append(spectrogram)
labels.append(emotion)
except Exception as exc:
print(f" ⚠ Skipping {file_path}: {exc}")
X = np.array(data)
y = np.array(labels)
return X, y
|