You are here: Home > Sound Processing Series, Tutorials > Introduction au traitement du son : partie 4

Introduction au traitement du son : partie 4

N.B. This tutorial series will be in French. Maybe in the future, I’ll prodive an English version. Cette série de tutorial sera d’abord écrite en français. Elle sera éventuellement traduite en anglais.

Dans ce dernier tutorial, nous allons implémenter un effet de réverbération très connu : la réverbération de Schroeder.

Dans sa version traditionnelle, ce filtre utilise quatre retards recirculant de forme simple (voir tutorial précédent) en parallèle et deux “allpass filter”. L’archive contenant les sources est téléchargeable ici : DelaySchroeder.tar.gz.

Schroeder reverb

Filtre de réverbération de Schroeder

Fonctionnement

Comme nous l’avons vu dans le précédent tutorial, un “comb filter” va générer toute une série de répétitions du signal initial. L’utilisation de plusieurs “comb filter” en parallèle va permettre de densifier le nombre de répétitions du signal d’entrée et de s’approcher de la réponse impulsionnelle réelle d’une salle de concert. En effet, la réponse impulsionnelle réelle dans une salle de concert ou une église va présenter un signal beaucoup plus dense surtout en fin de résonance.

Réponse impulsionnelle dans une salle de concert

On distingue d’ailleurs les réflexions initiales des réflexions tardives. Les réflexions initiales sont assez bien simulées par les “comb filters”. Il n’en est pas de même pour les réflexions tardives.

Filtre “Allpass”

Dans le graphe précédent, on voit que de nombreuses réflexions ont lieu et se densifient avec le temps. C’est exactement ce que l’on va essayer de simuler avec notre filtre. Malheureusement, l’utilisation des “comb filter” uniquement ne suffit pas à produire une sortie suffisamment dense. Pour cela on va ajouter deux “all pass filters” qui ont la propriété d’avoir une réponse fréquentielle plate (ne modifie pas l’amplitude des fréquences) tout en déphasant le signal en fonction de la fréquence d’entrée.

Diagramme d'un "allpass filter"

Calculons la fonction de transfert du filtre. On remarque tout d’abord que ce filtre se compose de deux filtres en série. Le premier filtre est un retard recirculant et le second filtre un retard non recirculant. Il nous suffit d’exprimer les fonctions de transfert des deux filtres et de les multiplier entre elles.

Étudions à présent le module de H. Ce qui nous intéresse est simplement de démontrer que le module vaut 1. En d’autres termes, l’amplitude des fréquences n’est pas modifiée. Etudions séparément le numérateur et le dénominateur :

Le premier terme ne nous intéresse pas car il s’agit simplement d’un déphasage (le module de Z vaut 1). Intéressons-nous au second terme et développons :

On regroupe les parties réelles et imaginaires et on obtient :

En procédant de la même manière pour le dénominateur on obtient :

Les deux termes ne sont pas identiques mais leur module le sont. En effet, les deux termes ne différent que par le terme facteur du sinus (-1-g) et (1+g). Or, élevé au carré, ces termes sont identiques !

Implémentation

L’implémentation du filtre de Schroeder est très simple puisqu’elle utilise simplement les éléments de base “Comb filter” et “Allpass filter”. Nous avons déjà implémenter le “Comb filter”. Il ne nous reste plus qu’à implémenter le “Allpass filter”.

Implémentation du “Allpass Filter”

Tout d’abord le header :

#ifndef __CALL_PASS_FILTER_H__
#define __CALL_PASS_FILTER_H__

#include "CDelay.h"

template<typename TType>
  class CAllPassFilter
  {
    public:
      CAllPassFilter(size_t i_delayTime, float i_gain) :
        m_delay(i_delayTime), m_gain(i_gain)
      {
      }

      void
      process(const TType& i_inputSample, TType& o_outputSample);

      size_t
      delayTime() const
      {
        return m_delay.delayTime();
      }

      float
      gain() const
      {
        return m_gain;
      }

    private:
      CDelay<TType> m_delay;
      float m_gain;
  };

#include "CAllPassFilter.hpp"

#endif //__CALL_PASS_FILTER_H__

Et l’implémentation :

#ifndef __CALL_PASS_FILTER_HPP__
#define __CALL_PASS_FILTER_HPP__

#include "common.h"

template<typename TType>
  void
  CAllPassFilter<TType>::process(const TType& i_inputSample, TType& o_outputSample)
  {
		//Retrieve the last delay value
		TType old = m_delay.read();

		//Compute current value
		TType current = i_inputSample + m_gain * old;

		//Put the new sample into the delay
    m_delay.push(current);

    //Compute the output
    o_outputSample = current * (-m_gain) + old;
  }

#endif //__CALL_PASS_FILTER_HPP__

L’implémentation du “Allpass filter” ne présente aucune difficulté.

Implémentation du filtre de Schroeder

L’implémentation du filtre de Schroeder est très simple elle aussi puisqu’elle n’utilise que des éléments déjà présentés.

#ifndef __CSCHROEDER_REVERB_H__
#define __CSCHROEDER_REVERB_H__

#include <vector>

#include "CCombFilter.h"
#include "CAllPassFilter.h"

template<typename TType>
  class CSchroederReverb
  {
    public:
      CSchroederReverb();

      /*Initialize the gains and delays
       * i_filterGains and i_filterDelayTimes must contain at least 6 values: 4 values for the comb filter and 2 values for the allpass filters.
      */
      CSchroederReverb(const std::vector<float>& i_filterGains, const std::vector<size_t>& i_filterDelayTimes);

      void
      process(const TType& i_inputSample, TType& o_outputSample);

    private:
      CCombFilter<TType> m_combFilter1;
      CCombFilter<TType> m_combFilter2;
      CCombFilter<TType> m_combFilter3;
      CCombFilter<TType> m_combFilter4;

      CAllPassFilter<TType> m_allPassFilter1;
      CAllPassFilter<TType> m_allPassFilter2;
  };

#include "CSchroederReverb.hpp"

#endif //__CSCHROEDER_REVERB_H__

Notons la présence d’un second constructeur qui permet d’initialiser les valeurs des gains et des retards des six éléments.

#ifndef __CSCHROEDER_REVERB_HPP__
#define __CSCHROEDER_REVERB_HPP__

#include "common.h"

template<typename TType>
  CSchroederReverb<TType>::CSchroederReverb() :
    m_combFilter1(1103, 0.9f), m_combFilter2(1109, 0.9f), m_combFilter3(1117, 0.9f), m_combFilter4(1123, 0.9f),
        m_allPassFilter1(443, 0.7), m_allPassFilter2(311, 0.5)
  {
  }

template<typename TType>
  CSchroederReverb<TType>::CSchroederReverb(const std::vector<float>& i_filterGains, const std::vector<size_t>& i_filterDelayTimes) :
    m_combFilter1(1367, 0.9f), m_combFilter2(1631, 0.9f), m_combFilter3(1808, 0.9f), m_combFilter4(1896, 0.9f),
        m_allPassFilter1(200, 0.03), m_allPassFilter2(882, 0.01)
  {
    if (i_filterGains.size() >= 6 && i_filterDelayTimes.size() >= 6)
    {
      m_combFilter1.setGain(i_filterGains[0]);
      m_combFilter2.setGain(i_filterGains[1]);
      m_combFilter3.setGain(i_filterGains[2]);
      m_combFilter4.setGain(i_filterGains[3]);
      m_allPassFilter1.setGain(i_filterGains[4]);
      m_allPassFilter2.setGain(i_filterGains[5]);

      m_combFilter1.setDelayTime(i_filterDelayTimes[0]);
      m_combFilter2.setDelayTime(i_filterDelayTimes[1]);
      m_combFilter3.setDelayTime(i_filterDelayTimes[2]);
      m_combFilter4.setDelayTime(i_filterDelayTimes[3]);
      m_allPassFilter1.setDelayTime(i_filterDelayTimes[4]);
      m_allPassFilter2.setDelayTime(i_filterDelayTimes[5]);

    }
  }

template<typename TType>
  void
  CSchroederReverb<TType>::process(const TType& i_inputSample, TType& o_outputSample)
  {
    TType aSample;

    //Four comb filters in parallel
    m_combFilter1.process(i_inputSample, aSample);
    o_outputSample = aSample;

    m_combFilter2.process(i_inputSample, aSample);
    o_outputSample += aSample;

    m_combFilter3.process(i_inputSample, aSample);
    o_outputSample += aSample;

    m_combFilter4.process(i_inputSample, aSample);
    o_outputSample += aSample;

    //Two all pass filters in series
    m_allPassFilter1.process(o_outputSample, aSample);
    m_allPassFilter2.process(aSample, o_outputSample);
  }

#endif //__CSCHROEDER_REVERB_HPP__

Le constructeur par défaut initialise les valeurs des gains et des retards. Il est très important de noter que les valeurs des retards doivent être premières entre elles. Ceci évitera d’amplifier certaines fréquences au détriment des autres.

Tests

Voilà ! Nous avons terminé l’implémentation de notre premier filtre de réverbération. Il ne nous reste plus qu’à le tester. Pour cela, rien de plus simple. Remplacez simplement les “comb filter” dans le fichier CAudioProcessing.h par notre nouveau filtre !

Après écoute, même si les résultats ne sont pas extraordinaires, ils sont tout à fait convenables. Pour ceux qui seraient intéressés de continuer, je leur propose de jeter un oeil sur une variante de ce filtre : “Schroeder Nested  Allpass filter”.

Ceci conclut notre série d’introduction au traitement du son. J’espère que vous avez apprécié lire ces quelques pages. Si des sujets en particulier vous intéressent, n’hésitez pas à m’en faire part.

  • Digg
  • Del.icio.us
  • StumbleUpon
  • Reddit
  • Twitter
  • RSS

Leave a Reply