| name | audio-processing |
| description | Traitement numérique du signal audio — librosa, sox/soxr, ffmpeg, filtrage FIR/IIR, noise reduction, normalisation, resampling, pitch shifting, time stretching. |
| tags | ["audio","dsp","signal-processing","librosa","sox","ffmpeg","filtering","noise-reduction","resampling"] |
| platforms | ["linux","macos","windows"] |
| related_skills | ["mfcc-spectrogram-analysis","asr-reconnaissance-vocale","tts-synthese-vocale"] |
Audio Processing — Traitement du Signal Audio
Guide complet du traitement numérique du signal audio : analyse, filtrage, transformation, restauration, effets.
1. Fondamentaux du DSP Audio
1.1 Concepts clés
Signal analogique → Échantillonnage → Quantification → Signal numérique (PCM)
ADC (Analog-to-Digital Converter)
| Concept | Formule | Description |
|---|
| Fréquence d'échantillonnage | fs | Nombre d'échantillons/s (8kHz-192kHz) |
| Quantification | 2^n niveaux | 16-bit = 65536 niveaux, 24-bit = 16M |
| Théorème de Nyquist | f_max ≤ fs/2 | ½ de fs = fréquence max représentable |
| Résolution temporelle | dt = 1/fs | Intervalle entre échantillons |
| Rapport signal/bruit | SNR = 6.02n + 1.76 dB | Théorique pour quantification n-bit |
1.2 Bibliothèques Python principales
import numpy as np
import librosa
import soundfile as sf
import sounddevice as sd
import scipy.signal as signal
import pydub
import pyrubberband
import noisereduce
import pyloudnorm
2. Manipulations fondamentales avec librosa
2.1 Chargement et sauvegarde
import librosa
import soundfile as sf
audio, sr = librosa.load("fichier.wav", sr=22050, mono=True, offset=0.0, duration=None)
sf.write("output.wav", audio, sr, subtype="PCM_16")
duration = librosa.get_duration(y=audio, sr=sr)
n_samples = len(audio)
2.2 Rééchantillonnage
audio_16k = librosa.resample(audio, orig_sr=sr, target_sr=16000)
from scipy import signal as sp_signal
audio_16k_scipy = sp_signal.resample_poly(
audio,
up=16000, down=sr,
window=('kaiser', 5.0)
)
import pyrubberband as pyrb
audio_stretched = pyrb.time_stretch(audio, sr, 1.5)
audio_shifted = pyrb.pitch_shift(audio, sr, 4)
2.3 Découpage et concaténation
segment = audio[int(5.0 * sr):int(10.0 * sr)]
audio_concat = np.concatenate([audio_a, audio_b])
def crossfade(a, b, duration=0.1, sr=22050):
"""Fondu-croisé entre deux signaux audio."""
fade_len = int(duration * sr)
fade_up = np.linspace(0, 1, fade_len)
fade_down = np.linspace(1, 0, fade_len)
a_out = a[:-fade_len]
b_out = b[fade_len:]
cross = a[-fade_len:] * fade_down + b[:fade_len] * fade_up
return np.concatenate([a_out, cross, b_out])
3. Filtrage numérique
3.1 Filtres IIR (Infinite Impulse Response)
from scipy import signal
b, a = signal.butter(4, 1000 / (sr/2), btype='low')
audio_filtered = signal.filtfilt(b, a, audio)
b_hp, a_hp = signal.butter(2, 80 / (sr/2), btype='high')
audio_hp = signal.filtfilt(b_hp, a_hp, audio)
b_bp, a_bp = signal.butter(4, [300 / (sr/2), 3400 / (sr/2)], btype='band')
audio_bp = signal.filtfilt(b_bp, a_bp, audio)
b_notch, a_notch = signal.iirnotch(50, 30, sr)
audio_notch = signal.filtfilt(b_notch, a_notch, audio)
3.2 Filtres FIR (Finite Impulse Response)
from scipy.signal import firwin, lfilter
taps = firwin(
numtaps=101,
cutoff=1000,
fs=sr,
window=('kaiser', 8.0),
pass_zero='lowpass'
)
audio_fir = lfilter(taps, 1.0, audio)
3.3 Filtres audio classiques
def pre_emphasis(signal, coeff=0.97):
"""Souligne les hautes fréquences."""
return np.append(signal[0], signal[1:] - coeff * signal[:-1])
def de_emphasis(signal, coeff=0.97):
"""Restaure le signal original après pre-emphasis."""
out = np.zeros_like(signal)
out[0] = signal[0]
for i in range(1, len(signal)):
out[i] = signal[i] + coeff * out[i-1]
return out
def a_weighting(frequencies):
"""Pondération A pour mesure de bruit perceptuelle."""
f = frequencies
return (12194**2 * f**4) / (
(f**2 + 20.6**2) * np.sqrt((f**2 + 107.7**2) * (f**2 + 737.9**2)) *
(f**2 + 12194**2)
)
4. Réduction de bruit
4.1 Spectral gating (noisereduce)
import noisereduce as nr
import librosa
noisy, sr = librosa.load("noisy_speech.wav", sr=16000)
noise_sample = noisy[:int(0.5 * sr)]
reduced = nr.reduce_noise(
y=noisy,
sr=sr,
y_noise=noise_sample,
prop_decrease=1.0,
n_fft=2048,
win_length=2048,
hop_length=512,
n_std_thresh_stationary=1.5,
stationary=True
)
reduced_auto = nr.reduce_noise(
y=noisy,
sr=sr,
prop_decrease=1.0,
stationary=False,
n_jobs=4
)
4.2 Wiener filter adaptatif
from scipy import signal as sp_signal
def wiener_filter(audio, noise_floor_db=-60, sr=16000):
"""Filtre de Wiener adaptatif."""
n_fft = 2048
hop_length = 512
f, t, Zxx = sp_signal.stft(audio, fs=sr, nperseg=n_fft, noverlap=n_fft - hop_length)
mag = np.abs(Zxx)
phase = np.angle(Zxx)
noise_est = np.median(mag[:, :10], axis=1, keepdims=True)
alpha = 0.98
snr_prior = 0.01 * np.ones_like(mag)
for t_idx in range(1, mag.shape[1]):
snr_post = (mag[:, t_idx] ** 2) / (noise_est[:, 0] ** 2 + 1e-10) - 1
snr_post = np.maximum(snr_post, 0)
snr_prior[:, t_idx] = alpha * (mag[:, t_idx-1] ** 2) / (noise_est[:, 0] ** 2 + 1e-10) + (1-alpha) * snr_post
gain = snr_prior / (1 + snr_prior)
Zxx_clean = gain * mag * np.exp(1j * phase)
_, audio_clean = sp_signal.istft(Zxx_clean, fs=sr)
return audio_clean
4.3 Réduction bruit avec FFmpeg / SoX (CLI)
sox noisy.wav reduced.wav noisered noise_profile.wav 0.2
sox noisy.wav -n trim 0 1 noiseprof noise_profile.wav
sox noisy.wav reduced.wav noisered noise_profile.wav 0.3
ffmpeg -i noisy.wav -af "afftdn=nf=-25" clean.wav
ffmpeg -i noisy.wav -af "anlmdn=s=1:p=0.5" clean.wav
5. Normalisation et Loudness
5.1 Normalisation du pic
def normalize_peak(audio, target_db=-1.0):
"""Normalise le pic à target_db."""
peak = np.max(np.abs(audio))
if peak == 0:
return audio
gain_db = target_db - 20 * np.log10(peak)
gain_linear = 10 ** (gain_db / 20)
return audio * gain_linear
5.2 Normalisation LUFS (EBU R128)
import pyloudnorm as pyln
meter = pyln.Meter(sr)
loudness = meter.integrated_loudness(audio)
print(f"Loudness intégré : {loudness:.1f} LUFS")
loudness_normalized = pyln.normalize.loudness(audio, loudness, -14.0)
loudness_range = pyln.LoudnessRange(meter)
lr_value = loudness_range(audio)
print(f"Plage dynamique : {lr_value:.1f} LU")
true_peak = np.max(np.abs(audio)) * (20 ** 0.5) / 2
5.3 Gate et expander
def noise_gate(audio, threshold_db=-40, sr=44100, attack_ms=5, release_ms=50):
"""Noise gate : coupe les sections silencieuses."""
threshold_linear = 10 ** (threshold_db / 20)
attack = int(sr * attack_ms / 1000)
release = int(sr * release_ms / 1000)
envelope = np.abs(audio)
b = np.ones(attack) / attack
envelope = signal.filtfilt(b, 1, envelope)
gain = np.ones_like(audio)
for i in range(1, len(audio)):
if envelope[i] < threshold_linear:
gain[i] = max(0, gain[i-1] - 1/release)
else:
gain[i] = min(1, gain[i-1] + 1/attack)
return audio * gain
6. Effets audio
6.1 Reverb (convolution avec IR)
import scipy.io.wavfile as wav
def apply_reverb(audio, ir_path, sr=44100, mix=0.5):
"""Applique une reverb par convolution."""
ir, ir_sr = librosa.load(ir_path, sr=sr, mono=True)
ir = ir / np.sum(ir)
wet = signal.fftconvolve(audio, ir, mode='full')[:len(audio)]
return (1 - mix) * audio + mix * wet
6.2 Compression
def compressor(audio, threshold_db=-24, ratio=4.0, attack_ms=2, release_ms=100, sr=44100):
"""Compresseur audio classique."""
threshold_linear = 10 ** (threshold_db / 20)
attack = int(sr * attack_ms / 1000)
release = int(sr * release_ms / 1000)
window = int(sr * 0.01)
rms = np.sqrt(np.mean(audio.reshape(-1, window) ** 2, axis=1))
rms = np.repeat(rms, window)
gain_reduction = np.ones_like(rms)
for i in range(len(rms)):
if rms[i] > threshold_linear:
gain_reduction[i] = (threshold_linear + (rms[i] - threshold_linear) / ratio) / rms[i]
smoothed = np.zeros_like(gain_reduction)
for i in range(1, len(gain_reduction)):
tau = attack if gain_reduction[i] < smoothed[i-1] else release
smoothed[i] = smoothed[i-1] + (gain_reduction[i] - smoothed[i-1]) / tau * window
return audio * smoothed
6.3 Égalisation paramétrique
from scipy.signal import iirpeak, iirnotch
class ParametricEQ:
"""Égaliseur paramétrique 3 bandes."""
def __init__(self, sr):
self.sr = sr
def peaking(self, audio, freq, gain_db, q=1.0):
"""Filtre en cloche (peaking)."""
A = 10 ** (gain_db / 40)
omega = 2 * np.pi * freq / self.sr
alpha = np.sin(omega) / (2 * q)
b0 = 1 + alpha * A
b1 = -2 * np.cos(omega)
b2 = 1 - alpha * A
a0 = 1 + alpha / A
a1 = -2 * np.cos(omega)
a2 = 1 - alpha / A
b = np.array([b0, b1, b2]) / a0
a = np.array([a0, a1, a2]) / a0
return signal.filtfilt(b, a, audio)
def lowshelf(self, audio, freq, gain_db):
"""Filtre shelving basse fréquence."""
A = 10 ** (gain_db / 40)
omega = 2 * np.pi * freq / self.sr
alpha = np.sin(omega) / np.sqrt(2)
sqrt2A = 2 * np.sqrt(A) * alpha
b0 = A * ((A + 1) - (A - 1) * np.cos(omega) + sqrt2A)
b1 = 2 * A * ((A - 1) - (A + 1) * np.cos(omega))
b2 = A * ((A + 1) - (A - 1) * np.cos(omega) - sqrt2A)
a0 = (A + ) + (A - ) * np.cos(omega) + sqrt2A
a1 = - * ((A - ) + (A + ) * np.cos(omega))
a2 = (A + ) + (A - ) * np.cos(omega) - sqrt2A
b = np.array([b0, b1, b2]) / a0
a = np.array([a0, a1, a2]) / a0
signal.filtfilt(b, a, audio)
7. SoX (Swiss Army Knife audio CLI)
sox --i input.wav
sox input.wav -r 16000 -b 16 -c 1 output.wav
sox input.wav output.wav pitch 300
sox input.wav output.wav tempo -s 1.5
sox input.wav output.wav trim 10 15
sox input.wav output.wav fade 3 30 1
sox -m input1.wav input2.wav mixed.wav
sox input.wav output.wav chorus 0.7 0.9 55 0.4 0.25 2 -t
sox input.wav output.wav flanger
sox input.wav -n spectrogram -o spectrogram.png
8. FFmpeg (audio processing avancé)
ffmpeg -i input.mp3 output.wav
ffmpeg -i input.wav -c:a libmp3lame -b:a 192k output.mp3
ffmpeg -i video.mp4 -vn -acodec pcm_s16le -ar 44100 audio.wav
ffmpeg -i input.wav -ar 16000 output.wav
ffmpeg -i a.wav -i b.wav -filter_complex "acrossfade=d=0.5" merged.wav
ffmpeg -i input.wav -af "loudnorm=I=-14:LRA=1:TP=-1" output.wav
ffmpeg -i input.wav -af "volume=2.0" output.wav
ffmpeg -i input.wav -af "equalizer=f=1000:t=q:w=1:g=5" output.wav
ffmpeg -i input.wav -af "silenceremove=start_periods=1:start_threshold=-50dB:start_silence=0.5" output.wav
ffmpeg -i input.wav -af "aresample=resampler=soxr" -ar 44100 output.wav
ffmpeg -f lavfi -i "sine=frequency=440:duration=5" 440hz.wav
ffmpeg -f lavfi -i "anoisesrc=d=5:c=pink:seed=42" pink_noise.wav
9. Analyse spectrale temps réel
import numpy as np
import sounddevice as sd
class RealtimeSpectrumAnalyzer:
"""Analyseur de spectre temps réel."""
def __init__(self, sr=44100, block_size=2048):
self.sr = sr
self.block_size = block_size
self.freqs = np.fft.rfftfreq(block_size, 1/sr)
def callback(self, indata, frames, time, status):
"""Callback audio en temps réel."""
if status:
print(f"Erreur: {status}")
spectrum = np.fft.rfft(indata[:, 0] * np.hanning(len(indata)))
magnitude = np.abs(spectrum)
bands = {
"Sub (20-60Hz)": self._band_energy(magnitude, 20, 60),
"Bass (60-250Hz)": self._band_energy(magnitude, 60, 250),
"Low Mid (250-500Hz)": self._band_energy(magnitude, 250, 500),
"Mid (500-2000Hz)": self._band_energy(magnitude, 500, 2000),
"Upper Mid (2-4kHz)": ._band_energy(magnitude, , ),
: ._band_energy(magnitude, , ),
: ._band_energy(magnitude, , ),
}
name, energy bands.items():
bars = (energy * )
()
():
mask = (.freqs >= f_min) & (.freqs <= f_max)
np.mean(magnitude[mask]) / (np.(magnitude) + )
():
sd.InputStream(device=, channels=, callback=.callback,
blocksize=.block_size, samplerate=.sr):
()
10. Pitfalls et solutions
| Problème | Cause | Solution |
|---|
| Alias (fréquences fantômes) | Échantillonnage < 2*f_max | Filtre anti-aliasing avant downsampling |
| Clics aux jonctions | Discontinuités de phase | Cross-fade aux coupures |
| Distorsion numérique | Écrêtage (clipping) | Normalisation avant effets |
| Réverbération de bruit | Gate trop agressif | Release plus long, threshold adaptatif |
| Phase non-linéaire | Filtres IIR | Utiliser filtfilt ou FIR |
| Artefacts MP3 | Compression avec pertes | Travailler en WAV/FLAC, convertir à la fin |
Références