2011:12:4:OCaml:Objets:Modulation

Un article de WikiProg.

Sommaire

Objets et modulations sonores

Dans ce TP nous allons utiliser les notions objets d'OCaml pour créer des modulateurs (base de la synthèse sonore.)

Modulateurs Simples

Un modulateur est un objet qui produit un signal oscillant. Techniquement pour nous, il s'agira d'un objet fournissant une méthode signal qui renverra une intensité en fonction du temps. Nous allons commencé avec des modulateurs très simple: une simple sinusoïdale avec un contrôle d'amplitude, de fréquence et de phase.

En simple, le signal pourra s'exprimer comme: amplitude *. sin (phase +. ffreq*.t)

Le contrôle de la fréquence (ffreq) s'exprime en utilisant le période du sinus (2*.pi) et la fréquence désirée (f): ffreq = f*.2*.pi de manière à ce que lorsque t atteint la période attendue (1/f) ffreq atteint 2*.pi.

Commencer par définir la valeur de pi en utilisant les fonctions trigonométriques d'OCaml.

Créer la classe simple_modulator paramétrée par l'amplitude, la fréquence et la phase (le tout en floatant, bien sûr) fournissant la méthode signal décrite plus haut.

let pi = (* fix me *)
 
class simple_modulator a f p =
object
  val amplitude = a
  val ffreq = f*.2.*.pi
  val phase = p
  method signal t = (* fix me *)
end

Envelope

Le contrôle de l'amplitude d'un modulateur se fait en règle général via ce que l'on appelle une envelope. Une envelope fournit l'amplitude en fonction du temps.

Envelope basique

La forme d'envelope la plus simple repose sur le découpage attack/sustain/release (voir la figure jointe.) On va utiliser une version simplifier de cette représentation: à partir du temps d'attack (a), de l'amplitude d'attack (ma), du temps de sustain (s), de l'amplitude de sustain (ms) et du temps de release (r) l'envelope suivra le découpage suivant, pour t dans l'intervalle:

  • 0 - a : progression linéaire de 0 à ma
  • a - s : progression linéaire de ma à ms
  • s - r : progression linéaire de ms à 0
Découpage attack/sustain/release
Découpage attack/sustain/release

Écrire la classe basic_envelope fournissant la méthode amplitude qui donne l'amplitude en fonction du temps:

class basic_envelope a ma s ms r =
object
  (* FIX ME *)
  method amplitude t = (* fix me *)
end

Modulateur avec enveloppe

On va ajouter le contrôle d'enveloppe au modulateur simple en utilisant de la composition. En gros, un modulateur contrôlé par une enveloppe correspond à un modulateur simple d'amplitude 1 dont le signal est multiplié par l'amplitude à l'instant t donnée par l'enveloppe (méthode amplitude.)

class env_modulator env f p =
object
  inherit simple_modulator 1. f p (* FIX ME *)
  val form = env
  method signal t = (* FIX ME *)
end

Compléter cette classe en utilisant le référencement de la classe mère.

Visualisation

On va maintenant utiliser le module Graphics pour visualiser nos modulateurs. Pour cette partie vous devrez compiler vos programme en incluant graphics.cma (pour ocamlc) ou graphics.cmxa (pour ocamlopt.)

La classe virtuelle suivante ajoute l'aspect "visualisation" à nos modulateur:

class virtual drawable_mod =
object (s)
  method virtual signal : float -> float
  method draw (x,y) (t0,t1) max_amp =
    let step = (t1 -. t0)/.(float (16*x)) in
    let val2point (fx,fy) = (
      int_of_float((fx -. t0)/. (16.*.step)),
      (int_of_float ((fy /. max_amp)  *. (float y))) + y/2
    ) in
    begin
      Graphics.open_graph (" "^(string_of_int x)^"x"^(string_of_int y));
      Graphics.clear_graph ();
      let t = ref t0 in
	while !t<=t1 do
	  let (a,b) = val2point (!t, s#signal !t) in
	    Graphics.plot a b;
	    t := !t +. step;
	done;
	ignore (Graphics.read_key ());
	Graphics.close_graph ();
    end
end

La méthode draw prend en paramètre un couple formé des dimensions de la fenêtre, un couple formé des bornes de l'intervalle de temps à visualiser et enfin l'amplitude maximale à afficher. Par exemple, pour ouvrir une fenêtre de 512 par 512 pixels et afficher le modulateur de 0 à 5 secondes avec une amplitude maximale de 2 on utilisera: o#draw (512,512) (0.,5.) 2.

Combiner cette classe avec les précédantes pour visualiser les différentes sortes de modulateurs déjà créer. Par exemple, pour le simple_modulator:

class drawable_simple_modulator a f p =
object
  inherit simple_modulator a f p
  inherit drawable_mod
end

Synthèse additive

La synthèse additive est la forme la plus simple et la plus ancienne de syntèse sonore. Elle est basée sur le même principe que les orgues classiques ou les premières formes d'orgues electronnique. En théorie, on devrait pouvoir recréer n'importe quel son avec (mais comme d'habitude entre théorie et pratique ... )

Le principe est simple, on additionne différents modulateurs pour retrouver la forme d'onde complexe désirée. Le signal d'un modulateur additif composé de n modulateur (m_1 à m_n) est donc: signal t = (m_1#signal t) +. ... +. (m_n#signal t).

Addition simple

Écrire la classe simple_add réalisant la syntèse additive de deux modulators pré-déterminés (à l'instantiation):

class simple_add m1 m2 =
object
  val mod1 = m1
  val mod2 = m2
  method signal t = (* fix me *)
end

Pour que l'amplitude reste cadrée, on décidera de diviser par 2 le signal engendré.

OCaml risque de refuser de compiler votre classe, sous prétexte qu'une variable de type n'est pas instantiée. Pour ce faire, il faut forcer le typage de la fonction signal à ne prendre que des float en paramètre:

class simple_add m1 m2 =
object
  val mod1 = m1
  val mod2 = m2
  method signal (t:float) = (* fix me *)
end

Utiliser la méthode vu précédement pour visualiser les résultats possibles avec différents modulateurs.

Addition multiple

Sur le même principe, on va créer une classe additive qui représente des modulateurs additifs dont le nombre maximum de membre est paramètré à l'instantiation. On commence par définir l'interface des modulateurs et le dummy_modulator:

(* Utile pour les cast plus loin ... *)
class type modulator_type =
object
  method signal: float -> float
end
 
class dummy_modulator =
object
 method signal (t:float) = 0.
end

Compléter la définition suivante (ne pas oublier de diviser le signal par le nombre de modulateur):

class ['a] additive n =
object
  val mutable nb_mods = 0
  val tab: 'a array = Array.make n (new dummy_modulator)
  method add (m:'a) = (* fix me *)
  method signal t = (* fix me *)
end

Au passage, plutôt que de crée un nouvel objet dummy pour chaque modulateur additif, on peut utiliser un objet sans classe définit comme:

let dummy_mod =
object
  method signal (t:float) = 0.
end

Addition pondérée

Par héritage compléter la classe précédante pour créer la classe weighted_additive où chaque modulateur se voit attribuer un coefficient. Attention, le signal doit ici être divisé par la somme des coefficients.

Addition controlée et visualisation

En utilisant les classes précédantes ajouter les envelopes et la visualisation à nos modulateurs additif.

Synthèse FM

La synthèse FM (Frequency Modulation) est basée sur la composition de modulateur: signal t = m2#signal (m1#signal t)

En clair, au lieu de travailler avec un temps linéaire le modulateur (m2 dans l'exemple) est modulé par le premier modulateur, mathématiquement c'est une simple composition de fonction et d'un point de vue programmation ce n'est pas plus compliqué non plus.

Un modulateur par synthèse sera donc la composition de deux modulateurs (génériques, ils ont juste besoins d'une méthode signal) avec la bonne fonction de signal résultant de la composition:

class ['a] composition m1 m2 =
object
  val mod1:'a = m1
  val mod2:'a = m2
  method signal (t:float) =
    (* FIX ME *)
end

Exemple d'effets: Écrétage

Pour appliquer des effets à nos modulateurs, on peut utiliser un design pattern très simple: Decorator. Le principe de ce design pattern est le suivant: le décorateur est un objet qui en englobe un autre en proposant exactement les mêmes méthodes que celui englobé. La plus part des méthodes seront juste retransmises, sauf certaines (dans notre cas signal) pour lesquels le décorateurs ajoutera sa contribution (en utilisant le résultat de la méthode de l'objet englobé la plus part du temps.)

Dans notre cas, notre décorateur va majorer (en valeur absolue) l'amplitude du signal: lorsque la valeur est supérieure à la borne (ou inférieure) celle-ci est remplacée par la borne. Cette effet simule (sans les transitoires qui font vraiment le son) la saturation d'un ampli alimenté (celui-ci ne peut pas produire un signal d'intensité supérieur à son alimentation.)

Écrire la class suivante:

class ['a] saturation m lvl =
object
  val mod:'a = m
  val mutable level:int = lvl
  method set_level l = level <- l
  method signal (t:float) =
    (* FIX ME *)
end

Avec la fonction signal qui ressemble à:


\texttt{signal}(t) = \left\{\begin{array}{l}
    \texttt{mod\#signal}~t\textrm{~si~} \texttt{mod\#signal}~t
    ~\leq~ \texttt{level}\\
    \texttt{level}\textrm{~~~~~~~~~~~sinon}\\
\end{array}
\right.

Tester

Pour tester votre code, il vous faut:

  • Créer une version drawable de chacune des classes que vous avez écrite
  • Écrire une fonction main dans la quelle vous instantierez au moins un objet de chaque classe (dans leur version drawable.)
  • Appeller la méthode draw de chacun de ces objets pour les afficher.
  • Compile le tout et exécuter.

Enfin, pour tester la syntèse additive, vous risquez de rencontrer des problèmes lors de l'ajout de modulateur dans les objets de la classe additive.

La solution consiste à faire un cast à l'aide de l'opérateur :>, voici un petit exemple:

let madd = new additive 2
let m1 = new simple_modulator 1. 1. 0.
let m2 = new simple_modulator 1. 2. 0.
let _ =
  begin
    madd#add (m1 :> modulator_type);
    madd#add (m2 :> modulator_type);
  end

Bonus

On peut aller plus loin pour compléter ce TP, la plus part des points suivant se font sans trop de difficultés:

  • Compositions diverses (addition + FM)
  • Envelopes complexes
  • Modulateur de formes d'onde différentes
  • Modulation d'amplitude (utilisation de modulateur pour contrôler l'envelope.)
  • Effet à base de LFO (modulateur basse fréquence jouant sur divers paramètres pour obtenir des phaseurs, vibrato et autres ... )
  • Génération de fichier son (bonne chance)
Outils personnels