Blog Metlivi

Comment créer un filtre coupe-bande avec des commandes séparées pour la largeur et la fréquence centrale

Créez un filtre coupe-bande numérique doté de deux paramètres : la fréquence centrale en hertz et la largeur de bande de réjection en hertz. Recalculez les coefficients du filtre chaque fois que l’un des deux paramètres change. Si votre bibliothèque de filtres accepte Q au lieu de la largeur de bande, calculez Q = center / width à chaque mise à jour afin que le déplacement de la fréquence centrale préserve la largeur demandée.

Ce tutoriel permet de créer un prototype audio Python fonctionnel en n’utilisant que la bibliothèque standard. Il génère un signal de test, le traite, vérifie la largeur de bande de réjection et peut exporter des fichiers WAV pour l’écoute. Les commandes sont des paramètres de ligne de commande ; la même fonction de calcul des coefficients pourra ensuite être associée à deux boutons dans une application audio.

29 octobre 20269 min de lecturePar Metlivi Editorial Team
Section 1

Que doivent réellement modifier les deux commandes ?

La commande de fréquence centrale définit la fréquence de réjection maximale. La commande de largeur définit la distance entre les deux limites à mi-puissance, conventionnellement appelées points à −3 dB. Un filtre à encoche est une forme à bande étroite de filtre coupe-bande ; la documentation officielle de SciPy définit son facteur de qualité comme le quotient de la fréquence centrale par cette largeur de bande. SciPy : iirnotch

Choisissez l’unité de largeur avant de concevoir les commandes. Une largeur exprimée en hertz préserve un intervalle de fréquence absolu. Une largeur exprimée en octaves décrit un rapport de fréquences. Le document Audio EQ Cookbook du W3C prend explicitement en charge la largeur de bande en octaves pour les filtres à encoche et tient compte de la déformation des fréquences dans ses formules de coefficients numériques. Ces unités correspondent à des comportements différents ; indiquez clairement l’unité choisie. W3C : Audio EQ Cookbook

Pour cette réalisation, utilisez les hertz. Supposons que la fréquence centrale soit de 1 000 Hz et la largeur de 200 Hz : le Q correspondant est 5. Si la fréquence centrale passe à 2 000 Hz tout en conservant une largeur de 200 Hz, Q doit passer à 10. Conserver Q à 5 produirait plutôt une largeur de bande de 400 Hz. Ces calculs illustratifs expliquent pourquoi une commande à Q fixe ne répond pas à notre objectif.

Section 2

Quelle structure de filtre permet de disposer de commandes séparées ?

Utilisez un filtre à réponse impulsionnelle infinie du second ordre, également appelé biquad. Il comporte trois coefficients au numérateur et trois au dénominateur. Après avoir normalisé le premier coefficient du dénominateur à un, son équation aux différences est la suivante :

[ y[n]=b_0x[n]+b_1x[n-1]+b_2x[n-2]-a_1y[n-1]-a_2y[n-2]. ]

Cette relation du biquad est documentée dans l’Audio EQ Cookbook. W3C : Audio EQ Cookbook

Pour une fréquence d’échantillonnage \(F_s\), une fréquence centrale \(f_c\) et une largeur \(B\), calculez :

[ \omega_c=2\pi f_c/F_s,\qquad \beta=\tan(\pi B/F_s),\qquad g=\frac{1}{1+\beta}. ]

Utilisez ensuite :

[ b=g[1,-2\cos(\omega_c),1], ]

[ a=[1,-2g\cos(\omega_c),2g-1]. ]

Ce sont les équations de coefficients du filtre à encoche utilisées dans l’implémentation de SciPy, réécrites pour prendre directement en entrée une largeur de bande en hertz. Code source de conception des filtres de SciPy

Les deux commandes peuvent modifier plusieurs coefficients. Leur indépendance concerne la réponse obtenue : une modification de la largeur maintient la fréquence de réjection fixe, tandis qu’une modification de la fréquence centrale maintient la largeur de bande spécifiée. Affecter un seul coefficient à chaque bouton ne suffirait pas à garantir ce comportement.

Section 3

Comment créer un prototype audio fonctionnel ?

Enregistrez le programme complet ci-dessous sous le nom band_reject.py. Il nécessite Python 3.8 ou une version ultérieure, sans paquets supplémentaires. La fréquence d’échantillonnage est de 48 000 Hz et le signal d’entrée généré contient trois tonalités à 300, 1 000 et 5 000 Hz. Chaque tonalité a une amplitude de 0,2.

Le programme évalue directement la réponse du filtre, repère les deux limites à mi-puissance par dichotomie et traite deux secondes d’audio généré. Pour l’export WAV, il utilise l’interface wave documentée de Python afin d’écrire des fichiers mono PCM 16 bits. Si les fichiers de sortie existent déjà, une erreur est générée au lieu de les écraser. Python : wave — Lecture et écriture de fichiers WAV

import argparse
import cmath
import math
import struct
import wave


def coefficients(center, width, fs):
    if not all(math.isfinite(v) for v in (center, width, fs)):
        raise ValueError('Parameters must be finite')
    if not (fs > 0 and 0 < center < fs / 2 and 0 < width < fs / 2):
        raise ValueError('Require fs > 0 and center, width inside (0, fs/2)')
    beta = math.tan(math.pi * width / fs)
    g = 1 / (1 + beta)
    c = math.cos(2 * math.pi * center / fs)
    return (g, -2 * g * c, g), (1.0, -2 * g * c, 2 * g - 1)


def magnitude(f, b, a, fs):
    z = cmath.exp(-2j * math.pi * f / fs)
    return abs((b[0] + b[1]*z + b[2]*z*z) /
               (a[0] + a[1]*z + a[2]*z*z))


def edge(lo, hi, b, a, fs):
    target = 1 / math.sqrt(2)
    for _ in range(60):
        mid = (lo + hi) / 2
        if ((magnitude(lo, b, a, fs) - target) *
                (magnitude(mid, b, a, fs) - target)) > 0:
            lo = mid
        else:
            hi = mid
    return (lo + hi) / 2


def process(samples, b, a):
    x1 = x2 = y1 = y2 = 0.0
    result = []
    for x in samples:
        y = b[0]*x + b[1]*x1 + b[2]*x2 - a[1]*y1 - a[2]*y2
        result.append(y)
        x2, x1, y2, y1 = x1, x, y1, y
    return result


def write_wav(path, samples, fs):
    if any(not math.isfinite(x) or abs(x) > 1 for x in samples):
        raise ValueError('Output exceeds PCM range; lower the input level')
    data = b''.join(struct.pack('<h', round(x * 32767)) for x in samples)
    with open(path, 'xb') as stream:
        with wave.open(stream, 'wb') as wav:
            wav.setnchannels(1)
            wav.setsampwidth(2)
            wav.setframerate(fs)
            wav.writeframes(data)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--center', type=float, default=1000)
    parser.add_argument('--width', type=float, default=200)
    parser.add_argument('--write', action='store_true')
    args = parser.parse_args()
    fs = 48000
    b, a = coefficients(args.center, args.width, fs)
    low = edge(0, args.center, b, a, fs)
    high = edge(args.center, fs / 2, b, a, fs)
    print(f'Edges: {low:.3f}, {high:.3f} Hz; width: {high-low:.3f} Hz')
    assert math.isclose(high-low, args.width, abs_tol=1e-5)
    assert magnitude(args.center, b, a, fs) < 1e-7
    tones = (300, 1000, 5000)
    for f in tones:
        db = 20 * math.log10(max(magnitude(f, b, a, fs), 1e-12))
        print(f'{f} Hz: {db:.2f} dB')
    samples = [sum(0.2 * math.sin(2*math.pi*f*n/fs) for f in tones)
               for n in range(2*fs)]
    filtered = process(samples, b, a)
    assert len(filtered) == len(samples)
    assert all(math.isfinite(y) for y in filtered)
    if args.write:
        write_wav('input-demo.wav', samples, fs)
        write_wav('filtered-demo.wav', filtered, fs)


if __name__ == '__main__':
    main()

Lancez python band_reject.py --center 1000 --width 200 pour vérifier la réponse. Ajoutez --write pour créer input-demo.wav et filtered-demo.wav. Importez ces fichiers dans un éditeur audio sur des pistes distinctes, alignez leur début et comparez-les sans modifier le gain de lecture.

J’ai exécuté le programme complet sans export de fichiers. Les assertions concernant la largeur de bande, la réjection centrale, la longueur de sortie et la finitude des valeurs de sortie ont réussi. L’écriture WAV n’a pas été testée lors de cette exécution. La réponse affichée était d’environ −0,02 dB à 300 Hz et de −0,01 dB à 5 000 Hz ; la valeur au centre a atteint la limite d’affichage de −240 dB. Cette limite est une borne numérique d’affichage, et non une mesure de la profondeur de réjection d’un fichier audio.

Section 4

Comment vérifier l’indépendance des commandes ?

Testez trois réglages : une référence, une fréquence centrale modifiée avec une largeur inchangée, puis une largeur modifiée avec une fréquence centrale inchangée. Cela permet d’isoler chaque commande. Les valeurs suivantes ont été calculées en exécutant les routines de calcul des coefficients et de recherche des limites à 48 kHz ; elles décrivent la réponse mathématique et non une mesure d’enregistrement.

Réglage de fréquence centrale Réglage de largeur Limite inférieure à mi-puissance Limite supérieure à mi-puissance Intervalle calculé
1 000 Hz 200 Hz 904,959 Hz 1 104,959 Hz 200,000 Hz
2 000 Hz 200 Hz 1 902,441 Hz 2 102,441 Hz 200,000 Hz
1 000 Hz 400 Hz 819,692 Hz 1 219,692 Hz 400,000 Hz

Remarquez que les limites ne correspondent pas exactement à center ± width/2. Ici, la « fréquence centrale » désigne le zéro de réjection, tandis que la « largeur » désigne l’écart entre les croisements à mi-puissance. La réponse calculée montre que leur milieu arithmétique ne correspond pas nécessairement à la fréquence du zéro. Ne créez pas l’affichage des limites en ajoutant et en soustrayant simplement la moitié de la largeur.

Pour une vérification numérique supplémentaire dans un environnement équipé de SciPy, évaluez les coefficients obtenus avec scipy.signal.freqz. Fournissez fs et des fréquences explicites, en incluant la fréquence centrale exacte. La documentation explique qu’un tableau de fréquences explicite détermine les points d’évaluation de la réponse ; une grille par défaut trop grossière peut manquer un zéro étroit. SciPy : freqz

Section 5

Comment choisir la fréquence centrale et la largeur pour un son ?

Utilisez un court passage répétable du son que vous modifiez. Commencez par repérer la région de fréquences que vous souhaitez réduire, puis réglez la fréquence centrale sur celle-ci. Commencez par une largeur étroite, puis augmentez-la jusqu’à ce que la réduction couvre la région visée. Après chaque modification, comparez avec le filtre désactivé et gardez le même gain de lecture pour que la comparaison reste utile.

Utilisez ce guide pour choisir le prochain réglage :

Ce que révèle la comparaison Réglage suivant
La tonalité indésirable reste présente, mais le contenu voisin change Revérifiez la fréquence centrale avant d’élargir la bande
La fréquence centrale est bien réglée, mais la région indésirable la dépasse Augmentez progressivement la largeur
Une trop grande partie du timbre environnant disparaît Réduisez la largeur et comparez de nouveau
Plusieurs tonalités distinctes doivent être réduites Évaluez des sections à encoche séparées à ces fréquences
Une large région réjectée et plate est nécessaire Réexaminez la forme et l’ordre du filtre

Il s’agit de suggestions pratiques de réglage, et non de mesures tirées d’un enregistrement particulier. L’exemple à trois tonalités est volontairement simple : il permet de vérifier le bon fonctionnement du processeur avant de porter un jugement sur un son plus complexe.

Une seule encoche ne fournit pas non plus de commande de profondeur indépendante. L’Audio EQ Cookbook présente des familles de coefficients distinctes pour les filtres à encoche et les filtres d’égalisation en cloche. Si votre tâche exige une atténuation modérée et réglable plutôt qu’un zéro de réjection, un filtre d’égalisation en cloche est une autre conception à envisager. W3C : Audio EQ Cookbook

Section 6

Que faut-il modifier pour ajouter deux boutons en temps réel ?

Conservez le même contrat de paramètres : center_hz et width_hz sont transmis ensemble à la fonction de calcul des coefficients. Mettez à jour l’ensemble des coefficients chaque fois que l’un des paramètres change et refaites le calcul si la fréquence d’échantillonnage du traitement change. Pour ce prototype, les deux commandes doivent rester strictement supérieures à zéro et inférieures à la moitié de la fréquence d’échantillonnage ; tout réglage invalide déclenche une exception.

La fonction process() fournie traite un tampon mono complet et commence avec un historique nul à chaque appel. Une version en flux doit conserver l’historique des retards entre les blocs. La fonction lfilter de SciPy documente l’état initial des retards et l’état final renvoyé à cet effet. Conservez des historiques distincts pour chaque canal audio. SciPy : lfilter

Le déplacement des commandes constitue une étape d’implémentation supplémentaire. Cet exemple vérifie des réglages fixes ; il ne met pas en œuvre d’automatisation continue. Une stratégie de transition pratique consiste à exécuter brièvement en parallèle les filtres fixes ancien et nouveau, à laisser le nouveau filtre établir son historique, puis à effectuer un fondu enchaîné entre leurs sorties. Évaluez la transition avec le contenu réel et la vitesse de commande prévue.

Pour plusieurs filtres en série, conservez chaque filtre comme une section du second ordre distincte au lieu de développer la cascade en un seul polynôme d’ordre élevé. SciPy recommande les sections du second ordre pour la plupart des tâches de filtrage, car elles posent moins de problèmes numériques. SciPy : lfilter

Section 7

Quelles erreurs faut-il vérifier en premier ?

Si l’encoche apparaît à la mauvaise fréquence, vérifiez que la fréquence d’échantillonnage utilisée pour calculer les coefficients correspond à celle de l’audio. Si la largeur augmente lorsque vous déplacez la fréquence centrale, vérifiez si votre enveloppe maintient Q constant. Si la réponse affichée semble peu profonde, évaluez la fréquence centrale exacte avant de modifier la conception.

Si des tampons répétés produisent des discontinuités, examinez la gestion de l’état : la démonstration réinitialise l’historique à chaque appel de process(). Si l’export WAV génère une erreur de plage, réduisez l’amplitude d’entrée et relancez le programme ; celui-ci refuse intentionnellement d’écrêter silencieusement les échantillons hors plage. Pour exporter une autre paire, il faut renommer ou supprimer les fichiers de démonstration existants.

La réalisation est terminée lorsque le test de fréquence centrale place le zéro à l’endroit demandé, que les deux croisements de limite restent séparés de la distance souhaitée et que le son traité convient à la modification prévue. Conservez ces vérifications à côté des deux commandes lorsque vous passez de ce prototype hors ligne à un outil audio interactif.

Section 8

Sources

À lire aussi

Continuer sur ce thème