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
É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 à:
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)
