File size: 3,966 Bytes
15ff271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Audio Standardization Pipeline for Bangla Speech Emotion Recognition.

Converts all raw audio files non-destructively:
- Downmixes 2-channel stereo to 1-channel mono ((L+R)/2)
- Resamples varying sample rates (48kHz, 44.1kHz) to standard 16,000 Hz
- Normalizes peak amplitude to -1.0 dBFS (0.891)
- Caps extreme duration outliers (> 10.0 seconds) to 10.0 seconds maximum
- Saves clean, standardized 16-bit PCM WAV files to dataset/processed/<emotion>/
- Preserves the original raw dataset in dataset/BASE/ completely intact.
"""

import os
from pathlib import Path
from concurrent.futures import ProcessPoolExecutor
import numpy as np
import soundfile as sf
import librosa
from tqdm import tqdm

from src.utils.config import (
    RAW_DATA_DIR,
    PROCESSED_DATA_DIR,
    TARGET_SAMPLE_RATE,
    MAX_DURATION_SECONDS,
    EMOTION_CLASSES
)


def process_single_audio(args):
    raw_path, out_path = args
    try:
        if os.path.exists(out_path):
            return True, raw_path, None

        # Read raw audio
        audio, sr = sf.read(raw_path, dtype='float32')

        # 1. Downmix stereo to mono if needed
        if audio.ndim > 1:
            audio = np.mean(audio, axis=1)

        # 2. Resample to 16,000 Hz if needed
        if sr != TARGET_SAMPLE_RATE:
            audio = librosa.resample(audio, orig_sr=sr, target_sr=TARGET_SAMPLE_RATE, res_type="soxr_hq")

        # 3. Handle extreme durations (cap at MAX_DURATION_SECONDS = 10.0s)
        max_samples = int(MAX_DURATION_SECONDS * TARGET_SAMPLE_RATE)
        if len(audio) > max_samples:
            # Keep the first 10 seconds of speech
            audio = audio[:max_samples]

        # 4. Normalize peak amplitude to -1.0 dBFS (~0.891)
        peak = np.max(np.abs(audio))
        if peak > 0:
            target_peak = 10 ** (-1.0 / 20.0)  # ~0.89125
            audio = audio * (target_peak / peak)

        # 5. Save standardized audio as 16-bit PCM WAV
        os.makedirs(os.path.dirname(out_path), exist_ok=True)
        sf.write(out_path, audio, TARGET_SAMPLE_RATE, subtype='PCM_16')
        return True, raw_path, None

    except Exception as e:
        return False, raw_path, str(e)


def run_audio_standardization(max_workers: int = 4):
    print("=" * 70)
    print("STARTING AUDIO STANDARDIZATION PIPELINE")
    print(f"Target format: Mono, {TARGET_SAMPLE_RATE} Hz, 16-bit PCM WAV")
    print(f"Target duration cap: {MAX_DURATION_SECONDS} seconds")
    print(f"Source Directory: {RAW_DATA_DIR}")
    print(f"Destination Directory: {PROCESSED_DATA_DIR}")
    print("=" * 70)

    tasks = []
    for emotion in EMOTION_CLASSES:
        raw_emotion_dir = RAW_DATA_DIR / emotion
        processed_emotion_dir = PROCESSED_DATA_DIR / emotion
        processed_emotion_dir.mkdir(parents=True, exist_ok=True)

        for root, _, files in os.walk(raw_emotion_dir):
            for f in files:
                if f.lower().endswith(".wav"):
                    raw_path = os.path.join(root, f)
                    out_path = os.path.join(processed_emotion_dir, f)
                    tasks.append((raw_path, out_path))

    print(f"Total files queued for standardization: {len(tasks)}")

    failed = []
    success_count = 0
    with ProcessPoolExecutor(max_workers=max_workers) as executor:
        results = list(tqdm(executor.map(process_single_audio, tasks), total=len(tasks), desc="Standardizing Audio"))
        for success, raw_path, error_msg in results:
            if success:
                success_count += 1
            else:
                failed.append((raw_path, error_msg))

    print(f"\nStandardization Complete: {success_count}/{len(tasks)} files processed.")
    if failed:
        print(f"WARNING: {len(failed)} files failed:")
        for fp, err in failed[:5]:
            print(f"  - {fp}: {err}")
    else:
        print("All 5,000 files standardized with ZERO errors.")
    print("=" * 70)


if __name__ == "__main__":
    run_audio_standardization()