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