Upload 3 files
Browse files- features/augmentation.py +55 -0
- features/mel_spectrogram.py +66 -0
- features/padding.py +73 -0
features/augmentation.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
===============================================================================
|
| 3 |
+
features/augmentation.py — Data Augmentation for Training
|
| 4 |
+
===============================================================================
|
| 5 |
+
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
from config import FREQ_MASK_PARAM, TIME_MASK_PARAM, NOISE_STD, AUGMENT_PROB
|
| 10 |
+
|
| 11 |
+
AUGMENT_PROB = 0.5 # 50% chance to apply each augmentation independently
|
| 12 |
+
FREQ_MASK_PARAM = 24 # Max number of consecutive frequency bins to mask (out of 128)
|
| 13 |
+
TIME_MASK_PARAM = 40 # Max number of consecutive time frames to mask
|
| 14 |
+
NOISE_STD = 0.05 # Standard deviation of the Gaussian noise
|
| 15 |
+
|
| 16 |
+
# note regarding the noise the random noise might push data over the limit we made
|
| 17 |
+
# in the mel spectro gram to be between 0 and 1 so will need to clip
|
| 18 |
+
|
| 19 |
+
def apply_augmentation(mel_spec):
|
| 20 |
+
"""
|
| 21 |
+
Apply data augmentation to a mel spectrogram (training only).
|
| 22 |
+
|
| 23 |
+
Notes
|
| 24 |
+
-----
|
| 25 |
+
- This function creates a COPY of the input — the original is not modified.
|
| 26 |
+
- Each call produces a DIFFERENT augmentation due to randomness.
|
| 27 |
+
So each epoch, the same sample looks slightly different → diversity.
|
| 28 |
+
- The augmentation is seeded by numpy's global RNG. For reproducibility
|
| 29 |
+
across runs, set np.random.seed() in the training script.
|
| 30 |
+
|
| 31 |
+
"""
|
| 32 |
+
augmented = mel_spec.copy()
|
| 33 |
+
n_mels, time_frames = augmented.shape
|
| 34 |
+
|
| 35 |
+
# Frequency Masking (SpecAugment)
|
| 36 |
+
if np.random.random() < AUGMENT_PROB:
|
| 37 |
+
f = np.random.randint(0, FREQ_MASK_PARAM) # Width of freq mask
|
| 38 |
+
f0 = np.random.randint(0, n_mels - f) # Starting freq bin
|
| 39 |
+
augmented[f0:f0 + f, :] = 0.0 # Zero out the horizontal band
|
| 40 |
+
|
| 41 |
+
# Time Masking (SpecAugment)
|
| 42 |
+
if np.random.random() < AUGMENT_PROB:
|
| 43 |
+
t = np.random.randint(0, TIME_MASK_PARAM) # Width of time mask
|
| 44 |
+
t0 = np.random.randint(0, time_frames - t) # Starting time frame
|
| 45 |
+
augmented[:, t0:t0 + t] = 0.0 # Zero out the vertical block
|
| 46 |
+
|
| 47 |
+
# Gaussian Noise Injection
|
| 48 |
+
if np.random.random() < AUGMENT_PROB:
|
| 49 |
+
noise = np.random.normal(0, NOISE_STD, augmented.shape)
|
| 50 |
+
augmented = augmented + noise
|
| 51 |
+
|
| 52 |
+
# Safety clip to ensure noise doesn't break the [0, 1] normalization bounds
|
| 53 |
+
augmented = np.clip(augmented, 0.0, 1.0)
|
| 54 |
+
|
| 55 |
+
return augmented
|
features/mel_spectrogram.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
===============================================================================
|
| 3 |
+
features/mel_spectrogram.py — Mel Spectrogram Extraction
|
| 4 |
+
===============================================================================
|
| 5 |
+
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import librosa
|
| 10 |
+
from config import N_MELS, N_FFT, HOP_LENGTH, FMAX, MEL_POWER
|
| 11 |
+
|
| 12 |
+
N_MELS = 128
|
| 13 |
+
N_FFT = 1024
|
| 14 |
+
HOP_LENGTH = 512
|
| 15 |
+
FMAX = 8000
|
| 16 |
+
MEL_POWER = 2.0 # 2.0 = power spectrogram (standard), 1.0 = energy spectrogram
|
| 17 |
+
|
| 18 |
+
# Toggle whether to scale the final output dB array to exactly [0.0, 1.0]
|
| 19 |
+
# HIGHLY RECOMMENDED for CNN inputs to keep gradients stable.
|
| 20 |
+
# why did I choose to use min-max scaling here?
|
| 21 |
+
# because the log-mel spectrogram can have a wide range of values depending on the loudness of the audio,
|
| 22 |
+
# and scaling it to [0.0, 1.0] ensures that the CNN receives inputs in a consistent range
|
| 23 |
+
# which will make the weights explode in different directions when The data is fed to the CNN
|
| 24 |
+
# all weights will be standradized
|
| 25 |
+
APPLY_MIN_MAX_SCALING = True
|
| 26 |
+
|
| 27 |
+
def extract_mel_spectrogram(audio, sr):
|
| 28 |
+
"""
|
| 29 |
+
Extract a log-mel spectrogram from an audio waveform.
|
| 30 |
+
|
| 31 |
+
Notes
|
| 32 |
+
-----
|
| 33 |
+
- The output is in log (dB) scale, normalized per-sample.
|
| 34 |
+
- ref=np.max in power_to_db means 0 dB = the loudest point in this sample.
|
| 35 |
+
All other values are negative dB below the peak.
|
| 36 |
+
- This per-sample normalization is important because sala7's augmentation
|
| 37 |
+
thresholds and EL sir's CNN batch normalization depend on consistent
|
| 38 |
+
value ranges.
|
| 39 |
+
|
| 40 |
+
"""
|
| 41 |
+
mel_spec = librosa.feature.melspectrogram(
|
| 42 |
+
y=audio,
|
| 43 |
+
sr=sr,
|
| 44 |
+
n_fft=N_FFT,
|
| 45 |
+
hop_length=HOP_LENGTH,
|
| 46 |
+
n_mels=N_MELS,
|
| 47 |
+
fmax=FMAX,
|
| 48 |
+
power=MEL_POWER
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
# Convert to log scale (dB)
|
| 52 |
+
# Using ref=np.max sets the absolute loudest point in this file to 0 dB,
|
| 53 |
+
# making all other values negative relative to the peak.
|
| 54 |
+
log_mel_spec = librosa.power_to_db(mel_spec, ref=np.max) # so here I get the loudest point in the array and make it my refrence to be the zero decibel
|
| 55 |
+
|
| 56 |
+
# Per-sample normalization to [0.0, 1.0]
|
| 57 |
+
if APPLY_MIN_MAX_SCALING:
|
| 58 |
+
# Prevent division by zero if the file is completely silent
|
| 59 |
+
ptp = log_mel_spec.max() - log_mel_spec.min()
|
| 60 |
+
if ptp > 1e-6:
|
| 61 |
+
log_mel_spec = (log_mel_spec - log_mel_spec.min()) / ptp
|
| 62 |
+
else:
|
| 63 |
+
# If the file is just silent static, zero it out entirely
|
| 64 |
+
log_mel_spec = np.zeros_like(log_mel_spec)
|
| 65 |
+
|
| 66 |
+
return log_mel_spec
|
features/padding.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
===============================================================================
|
| 3 |
+
features/padding.py — Fixed-Size Spectrogram Padding / Trimming
|
| 4 |
+
===============================================================================
|
| 5 |
+
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
|
| 10 |
+
# we will use (top_db=40), trimmed clips average ~8.5 to 9.0 seconds.
|
| 11 |
+
# Calculation: 9.0 seconds * 16000 Hz / 512 hop_length = ~281 frames.
|
| 12 |
+
DEFAULT_FIXED_FRAMES = 281
|
| 13 |
+
|
| 14 |
+
# Padding strategy. Options: "constant" (fill with a number) or "edge" (repeat last frame)
|
| 15 |
+
PAD_MODE = "constant"
|
| 16 |
+
|
| 17 |
+
# Log-mel spectrograms use negative numbers for silence (e.g., -80.0).
|
| 18 |
+
# Padding with absolute 0.0 might accidentally represent maximum volume.
|
| 19 |
+
# Setting this to True forces the pad to use the quietest part of the current file.
|
| 20 |
+
USE_MIN_VALUE = True
|
| 21 |
+
CONSTANT_PAD_VALUE = 0.0
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def pad_or_trim_spectrogram(mel_spec, target_length=None):
|
| 25 |
+
"""
|
| 26 |
+
Pad or trim a mel spectrogram to a fixed number of time frames.
|
| 27 |
+
|
| 28 |
+
Notes
|
| 29 |
+
-----
|
| 30 |
+
- Zero-padding doesn't add energy — padded regions are "silence" in the
|
| 31 |
+
spectrogram and the CNN will learn to ignore them.
|
| 32 |
+
- Trimming from the right assumes the important machine sound is at the
|
| 33 |
+
beginning. After silence removal, this is guaranteed.
|
| 34 |
+
|
| 35 |
+
"""
|
| 36 |
+
if target_length is None:
|
| 37 |
+
target_length = DEFAULT_FIXED_FRAMES
|
| 38 |
+
|
| 39 |
+
n_mels, current_length = mel_spec.shape
|
| 40 |
+
|
| 41 |
+
if current_length < target_length:
|
| 42 |
+
# --- PAD: Add values on the right ---
|
| 43 |
+
pad_width = target_length - current_length
|
| 44 |
+
|
| 45 |
+
if PAD_MODE == "constant":
|
| 46 |
+
# Decide what "silence" means mathematically
|
| 47 |
+
# so here I try to use the lowest possible sound in the current spectrogram as the padding value, which is more realistic than using a fixed constant like 0.0 that might not represent silence in log-mel space.
|
| 48 |
+
pad_val = mel_spec.min() if USE_MIN_VALUE else CONSTANT_PAD_VALUE
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
mel_spec = np.pad(
|
| 52 |
+
mel_spec,
|
| 53 |
+
pad_width=((0, 0), (0, pad_width)), # Only pad the time axis, not the mel axis
|
| 54 |
+
mode="constant",
|
| 55 |
+
constant_values=pad_val
|
| 56 |
+
)
|
| 57 |
+
else:
|
| 58 |
+
# Fallback for other numpy pad modes "edge"
|
| 59 |
+
# so in this method we just repeat the last frame of the spectrogram, which is a common padding strategy that doesn't introduce new values but extends the existing pattern.
|
| 60 |
+
# This can be useful if the end of the spectrogram contains relevant information that we want to preserve.
|
| 61 |
+
# also depending on the CNN we need to try and error for this as in the CNN it might see the sudden
|
| 62 |
+
# silience as a FEATURE
|
| 63 |
+
mel_spec = np.pad(
|
| 64 |
+
mel_spec,
|
| 65 |
+
pad_width=((0, 0), (0, pad_width)),
|
| 66 |
+
mode=PAD_MODE
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
elif current_length > target_length:
|
| 70 |
+
# --- TRIM: Cut from the right ---
|
| 71 |
+
mel_spec = mel_spec[:, :target_length]
|
| 72 |
+
|
| 73 |
+
return mel_spec
|