JsonDONSON commited on
Commit
2b668de
·
verified ·
1 Parent(s): 1919bbe

Upload 3 files

Browse files
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