MIF_E31231708/.history/project_svm_suara/features_20260524003158.py

268 lines
7.9 KiB
Python

from collections import Counter
from pathlib import Path
import warnings
import librosa
import numpy as np
SAMPLE_RATE = 22050
LABEL_PD = "PD"
LABEL_TPD = "TPD"
VALID_LABELS = {LABEL_PD, LABEL_TPD}
N_MFCC = 13
MIN_DURATION_SECONDS = 0.30
MIN_RMS_FOR_USABLE_AUDIO = 0.001
def get_label_from_filename(file_path):
"""
Mengambil label dari nama file atau nama folder.
Urutan pengecekan penting:
- cek "_tpd" lebih dulu
- baru cek "_pd"
"""
path = Path(file_path)
filename = path.stem.lower()
parent = path.parent.name.lower()
if "_tpd" in filename or parent in {"tpd", "not_confident", "tidak_percaya_diri"}:
return LABEL_TPD
if "_pd" in filename or parent in {"pd", "confident", "percaya_diri"}:
return LABEL_PD
return None
def validate_label(label, file_path):
if label not in VALID_LABELS:
raise ValueError(f"Label tidak valid pada {Path(file_path).name}: {label}")
def load_and_preprocess_audio(file_path, sample_rate=SAMPLE_RATE):
"""
Membaca audio dengan preprocessing konsisten untuk training dan prediksi:
mono, sample rate 22050 Hz, trim silence, dan normalisasi volume.
"""
y, sr = librosa.load(file_path, sr=sample_rate, mono=True)
if y.size == 0:
raise ValueError(f"Audio kosong: {file_path}")
y, _ = librosa.effects.trim(y, top_db=30)
if y.size == 0:
raise ValueError(f"Audio hanya berisi silence: {file_path}")
duration = librosa.get_duration(y=y, sr=sr)
if duration < MIN_DURATION_SECONDS:
raise ValueError(
f"Audio terlalu pendek: {duration:.2f} detik. Minimal {MIN_DURATION_SECONDS:.2f} detik."
)
rms_value = float(np.sqrt(np.mean(y**2)))
if rms_value < MIN_RMS_FOR_USABLE_AUDIO:
raise ValueError(
f"Audio terlalu pelan/silent. RMS={rms_value:.5f}, "
f"minimal {MIN_RMS_FOR_USABLE_AUDIO:.5f}."
)
max_amplitude = np.max(np.abs(y))
if max_amplitude > 0:
y = y / max_amplitude
return y.astype(np.float32), sr
def mean_std(feature_matrix):
"""
Mengubah fitur frame-based menjadi statistik tetap.
Output selalu 1 dimensi dan stabil untuk SVM.
"""
feature_matrix = np.atleast_2d(feature_matrix)
return np.concatenate(
[
np.mean(feature_matrix, axis=1),
np.std(feature_matrix, axis=1),
]
)
def extract_pitch_features(y, sr):
"""
Mengambil ringkasan fundamental frequency (pitch) dengan pyin.
Jika pitch tidak terdeteksi, nilai pitch dibuat 0 agar fitur tetap konsisten.
"""
f0, _, _ = librosa.pyin(
y,
fmin=librosa.note_to_hz("C2"),
fmax=librosa.note_to_hz("C7"),
sr=sr,
)
voiced_f0 = f0[~np.isnan(f0)]
if voiced_f0.size == 0:
return np.array([0.0, 0.0, 0.0], dtype=np.float32)
voiced_ratio = voiced_f0.size / f0.size
return np.array(
[
np.mean(voiced_f0),
np.std(voiced_f0),
voiced_ratio,
],
dtype=np.float32,
)
def extract_features(file_path, sample_rate=SAMPLE_RATE):
"""
Ekstraksi fitur suara yang sama untuk training dan prediksi:
- MFCC mean dan std
- RMS Energy mean dan std
- Zero Crossing Rate mean dan std
- Spectral Centroid mean dan std
- Spectral Bandwidth mean dan std
- Spectral Rolloff mean dan std
- Pitch/fundamental frequency
- Durasi suara aktif
"""
y, sr = load_and_preprocess_audio(file_path, sample_rate=sample_rate)
mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=N_MFCC)
rms = librosa.feature.rms(y=y)
zcr = librosa.feature.zero_crossing_rate(y)
spectral_centroid = librosa.feature.spectral_centroid(y=y, sr=sr)
spectral_bandwidth = librosa.feature.spectral_bandwidth(y=y, sr=sr)
spectral_rolloff = librosa.feature.spectral_rolloff(y=y, sr=sr)
pitch_features = extract_pitch_features(y, sr)
active_duration = np.array([librosa.get_duration(y=y, sr=sr)], dtype=np.float32)
feature_vector = np.concatenate(
[
mean_std(mfcc),
mean_std(rms),
mean_std(zcr),
mean_std(spectral_centroid),
mean_std(spectral_bandwidth),
mean_std(spectral_rolloff),
pitch_features,
active_duration,
]
)
if feature_vector.ndim != 1:
raise ValueError("Fitur audio harus 1 dimensi.")
if not np.all(np.isfinite(feature_vector)):
raise ValueError("Fitur audio mengandung NaN atau infinity.")
return feature_vector.astype(np.float32)
def load_dataset(data_dir):
"""
Membaca semua file .wav pada folder data.
File tanpa label valid atau file rusak dilewati dengan peringatan.
"""
data_path = Path(data_dir)
audio_files = sorted(data_path.rglob("*.wav"))
if not audio_files:
raise FileNotFoundError(f"Tidak ada file .wav di folder: {data_path}")
features = []
labels = []
used_files = []
for audio_file in audio_files:
label = get_label_from_filename(audio_file)
if label is None:
warnings.warn(
f"File dilewati karena nama/folder tidak mengandung label PD atau TPD: "
f"{audio_file.name}"
)
continue
try:
validate_label(label, audio_file)
features.append(extract_features(audio_file))
labels.append(label)
used_files.append(audio_file)
except Exception as error:
warnings.warn(f"File dilewati karena gagal diproses: {audio_file.name} ({error})")
if not features:
raise ValueError("Tidak ada file audio valid yang berhasil diproses.")
label_counts = Counter(labels)
print("\n=== Distribusi Label Dataset ===")
print(f"PD : {label_counts.get(LABEL_PD, 0)}")
print(f"TPD: {label_counts.get(LABEL_TPD, 0)}")
invalid_labels = set(labels) - VALID_LABELS
if invalid_labels:
raise ValueError(f"Ditemukan label tidak valid: {sorted(invalid_labels)}")
return np.array(features), np.array(labels), used_files
def check_dataset_quality(data_dir):
"""
Mengecek kualitas dataset:
- jumlah data PD dan TPD
- durasi setiap audio
- audio terlalu pendek
- audio terlalu pelan/silent
- file rusak
- rekomendasi file yang perlu direkam ulang
"""
data_path = Path(data_dir)
audio_files = sorted(data_path.rglob("*.wav"))
label_counts = Counter()
problems = []
print("\n=== Cek Kualitas Dataset ===")
for audio_file in audio_files:
label = get_label_from_filename(audio_file)
if label is None:
problems.append((audio_file.name, "Label tidak ditemukan"))
continue
label_counts[label] += 1
try:
y_raw, sr = librosa.load(audio_file, sr=SAMPLE_RATE, mono=True)
duration_raw = librosa.get_duration(y=y_raw, sr=sr)
rms_raw = float(np.sqrt(np.mean(y_raw**2))) if y_raw.size else 0.0
issue_notes = []
if duration_raw < MIN_DURATION_SECONDS:
issue_notes.append(f"terlalu pendek ({duration_raw:.2f} detik)")
if rms_raw < MIN_RMS_FOR_USABLE_AUDIO:
issue_notes.append(f"terlalu pelan/silent (RMS={rms_raw:.5f})")
print(
f"{audio_file.name} | label={label} | durasi={duration_raw:.2f}s | "
f"rms={rms_raw:.5f}"
)
if issue_notes:
problems.append((audio_file.name, ", ".join(issue_notes)))
except Exception as error:
problems.append((audio_file.name, f"file rusak/gagal dibaca ({error})"))
print("\nJumlah data:")
print(f"PD : {label_counts.get(LABEL_PD, 0)}")
print(f"TPD: {label_counts.get(LABEL_TPD, 0)}")
print("\nRekomendasi rekam ulang/perbaikan:")
if not problems:
print("Tidak ada masalah kualitas audio yang jelas.")
else:
for filename, reason in problems:
print(f"- {filename}: {reason}")
return problems