(******************************************************************************) (* SYNTHÈSE SONORE ÉLÉMENTAIRE EN HEPTAGON *) (******************************************************************************) (* Le but de ce fichier est de démontrer quelques techniques élémentaires de génération de son en Heptagon, à travers de modestes expérimentations. Il n'a bien sûr pas vocation à se substituer à un cours de traitement du signal ou d'acoustique. En revanche, il peut facilement servir d'illustration de diverses techniques de programmation en Heptagon. *) (* Le flot d'échantillons sonores produits par ce programme synchrone est branché à un petit bout de code C qui les envoie au système sonore de votre système d'exploitation par le biais de la bibliothèque SDL2. Votre OS les transmet à la carte son qui elle même les envoie à vos enceintes, casque ou écouteurs. *) (* Les langages synchrones ont été utilisés pour la synthèse sonore. Si ce sujet vous intéresse, vous pouvez par exemple consulter la page du langage Faust, à la syntaxe rudimentaire mais aux bibliothèques acoustiques, sonores et musicales très développées : http://faust.grame.fr *) (* On va utiliser une petite bibliothèque de composants mathématiques. Les curieuses et curieux pourront aller voir mathext.epci. *) open Mathext (* Avant de commencer, on a besoin de quelques définitions et outils. *) (* Le nombre d'échantillons, c'est à dire ici de pas synchrones, que le système sonore va consommer une seconde. *) const period : int = 44100 (* Un signal mono est un simple flot de nombres à virgule flottante. *) type mono = float (* Un signal stéréo fournit deux échantillons, gauche et droit, la carte son se chargeant de les mixer pour donner l'impression d'un son 'surround'. *) type stereo = { l : float; r : float } (* Le signal constant silencieux. *) const silence : stereo = { l = 0.0; r = 0.0 } (* On peut dupliquer un signal mono pour obtenir un signal stéréo inintéressant, les deux canaux portant la même valeur. *) fun stereo_of_mono(a : mono) returns (o : stereo) let o = { l = a; r = a } tel (* On peut appliquer un gain à un signal stéréo, c'est à dire le multiplier par un flottant pour l'amener à une amplitude différente. *) fun stereo_gain(g : float; s : stereo) returns (o : stereo) let o = { l = g *. s.l; r = g *. s.r }; tel (* Étant donné deux signaux, on peut les combiner via leur somme. *) fun stereo_sum(s1, s2 : stereo) returns (o : stereo) let o = { l = s1.l +. s2.l; r = s1.r +. s2.r } tel (* Quand on utilise les deux fonctions qu'on vient de définir, gare à l'amplitude en sortie ! Une amplitude trop élevée risque de dépasser la capacité de votre carte son, enceintes ou écouteurs, ce qui cause un phénomène de saturation : tous les échantillons d'amplitude trop élevée sont écrasés sur l'amplitude maximale. *) (* La fonction mix ci-dessous pallie le défaut de la fonction stereo_sum en renormalisant le résultat. De plus, elle traite un tableau de signaux, et donc moralement un nombre d'entrées arbitraires. *) fun stereo_mix<>(s : stereo^n) returns (o : stereo) let o = stereo_gain(1.0 /. float(n), fold<> stereo_sum(s, silence)); tel (* On peut commencer à écouter un peu de son, par exemple celui du silence. *) node main0() returns (o : stereo) let o = silence; tel (* Quid du noeud suivant ? *) node cracks() returns (o : stereo) let o = { l = 4200.0; r = 4200.0 }; tel (* On entendu un craquement, puis plus rien, puis un craquement lorsqu'on interromp le programme. Pourquoi ? Physiquement, le son est une vibration produit par une onde acoustique, c'est à dire une oscillation de la pression de l'air. Autrement dit, il s'agit d'une *variation*. Donc, le signal constant ne peut pas donner lieu à un son, sauf au premier instant (passage de 0 à 4200) puis lorsqu'on interromp le programme (passage de 4200 à 0). Et si on essayait un signal qui varie ? Par exemple, un signal carré qui passe de 1 à 0 toutes les demi-secondes. *) node periodic(p : int) returns (o : int) var n : int; let o = 0 fby (if n = p then 0 else n); n = o + 1; tel node beats_1() returns (o : stereo) let o = stereo_of_mono(if periodic(period) <= period / 2 then 1.0 else -. 1.0); tel (* On obtient une série de battements simples. Faire en sorte que le canal droit soit l'opposé du canal gauche produit un effet intéressant. *) node beats_2() returns (o : stereo) var l : float; let l = if periodic(period) <= period / 2 then 1.0 else -. 1.0; o = { l = l; r = -. l }; tel (* Essayons maintenant de générer un signal qui croît indéfiniment. *) node fcnt(ini : float; step : float) returns (o : float) let o = ini fby (o +. step); tel node sawtooth_1() returns (o : stereo) let o = stereo_of_mono(fcnt(0.0, 1.0)); tel (* On entend quelques craquements, puis plus rien. Normal : ce signal n'oscille pas vraiment, ou du moins pas avant d'atteindre l'overflow. Pourquoi ne pas tester un signal périodique en dents de scie, dans ce cas ? *) node sawtooth_2() returns (o : stereo) var t : float; let t = float(periodic(128)); o = stereo_of_mono(t); tel (* Tiens, un son à peu près constant ! Pas très harmonieux cependant. *) (* Est-ce qu'appliquer un gain ferait une différence ? Pour bien observer la différence, on n'a qu'à faire passer le gain de 0 à 1 à chaque seconde. C'est très facile à programmer en Heptagon. *) node sawtooth_3() returns (o : stereo) var t : float; g : float; let t = float(periodic(128)); g = float(periodic(period)) /. float(period); o = stereo_gain(g, stereo_of_mono(t)); tel (* On entend nettement le signal en dent de scie, avec un pic à la fin de la seconde. De façon intéressante, si on augmente le gain, le son apparaît comme plus pincé, un peu comme les notes d'une guitare. *) node sawtooth_4() returns (o : stereo) var t : float; g : float; let t = float(periodic(128)); g = 3.0 *. float(periodic(period)) /. float(period); o = stereo_gain(g, stereo_of_mono(t)); tel (* En augmentant la période, les pics s'éloignent, en la diminuant, les pics se rapprochent. *) node period_per_sec(a : int) returns (o : float) let o = float(periodic(period / a)) /. float(period / a); tel node sawtooth_5() returns (o : stereo) var t : float; g : float; let t = float(periodic(128)); g = period_per_sec(2); o = stereo_gain(g, stereo_of_mono(t)); tel (* On peut aussi appliquer des gains différents sur le canal mono et stéréo. *) node every_sec(s : int) returns (c : bool) let c = periodic(period * s) = ((- 1) fby 0); tel node sawtooth_6() returns (o : stereo) var t : float; g1, g2 : float; let t = float(periodic(128)); o = { l = g1 *. t; r = g2 *. t }; automaton state FastLeftSlowRight do g1 = period_per_sec(1); g2 = period_per_sec(8); until every_sec(5) then SlowLeftFastRight state SlowLeftFastRight do g1 = period_per_sec(5); g2 = period_per_sec(1); until every_sec(5) then FastLeftSlowRight end tel (* Tous ces sons ne sont pas très harmonieux. Peut-on en obtenir de plus purs ? Le traitement du signal nous enseigne, via la théorie de la transformée de Fourier, que tout signal raisonnablement régulier peut se décomposer en une somme (infinie) de sinusoïde. Autrement dit, les signaux sinusoïdaux peuvent servir de briques de base élémentaires mais universelles. Considérés comme des signaux audio, ils forment des tons purs, élémentaires. *) node pure_tone(p : float) returns (o : float) var t : float; let t = fcnt(0.0, 1.0); o = sin(t *. (p /. float(period)) *. 2.0 *. Mathext.pi); tel (* Par exemple, la sinusoïde de fréquence 440.1 Hz, communément désignée sous le nom de La 440, devrait vous être familière. *) node main_pure_1() returns (o : stereo) let o = stereo_of_mono(pure_tone(440.0)); tel (* En plus d'être la tonalité du téléphone, elle sert de référence pour l'accordage des pianos, violons et d'autres instruments. https://fr.wikipedia.org/wiki/La_440 *) (* En mélangeant plusieurs sinusoïdes ensembles, on peut obtenir des effets rétro assez amusants. *) node some_pure_tone(p : float; i : int) returns (s : stereo) let s = stereo_gain(period_per_sec(i + 1), stereo_of_mono(pure_tone(p))); tel node oscillating_counter<>(i : int) returns (last o : int = 0) var step : int; let step = if every_sec(1) then 1 else 0; automaton state Init do o = i until true then Increase state Increase do o = last o + step until o >= m then Decrease state Decrease do o = last o - step until o <= 0 then Increase end tel node main_pure_2() returns (o : stereo) var periods : float^3; speeds : int^3; let periods = [440.0, 261.6256, 4186.009]; speeds = map<<3>>(oscillating_counter<<10>>)([1, 3, 7]); o = stereo_mix<<3>>(map<<3>> some_pure_tone(periods, speeds)); tel (* Enfin, les amatrices et amateurs de piano pourront trouver sur la page https://en.wikipedia.org/wiki/Piano_key_frequencies une formule associant une fréquence de sinusoïde à une note de piano. On peut l'utiliser comme suit. *) fun piano_freq_of_key(k : int) returns (f : float) let f = Mathext.pow(2.0, (Mathext.float(k) -. 49.0) /. 12.0) *. 440.0; tel node tone_of_piano_key(k : int) returns (o : stereo) let o = stereo_of_mono(pure_tone(piano_freq_of_key(k))); tel node maintain(c : bool; x : int on c; ini : int) returns (o : int) let o = merge c x ((ini fby o) whenot c); tel node main_pure_3() returns (o : stereo) var k : int; c : bool; let o = tone_of_piano_key(k); k = maintain(c, 40 + periodic(53 - 40), 40); c = periodic(period) = 0; tel (* On peut essayer de programmer un piano midi. *) (* Pour générer des transitions propres entre les notes, on a besoin de modifier nos tons à travers une "enveloppe". La plus classique est l'enveloppe dite "Attack-Decay-Sustain-Release", cf. Wikipédia. https://en.wikipedia.org/wiki/Envelope_(music)#ADSR Le noeud ci-dessous produit une telle enveloppe périodiquement, tous les t instants. L'enveloppe prend la forme d'un gain entre 0 et 1. Les paramètres a, d et s doivent-être tels que 0.0 < a + d + s < 1.0. Ils expriment la fraction de t correspondant à chacune des quatre phases, la phase d étant la fraction de t définie comme 1 - a - d - s. Le paramètre s_level est le niveau de la phase S, entre 0 et 1 donc. *) node adsr_envelope(t : int; a, d, s : float; s_level : float) returns (e : float) var c, a_stop, d_stop, s_stop : int; let a_stop = int(float(t) *. a); d_stop = a_stop + int(float(t) *. d); s_stop = d_stop + int(float(t) *. s); c = periodic(t); automaton state Attack do e = float(c) /. float(a_stop); unless c >= a_stop continue Decay state Decay var f : float; do e = 1.0 -. (1.0 -. s_level) *. f; f = float(c - a_stop) /. float(d_stop - a_stop); unless c >= d_stop continue Sustain state Sustain do e = s_level; unless c >= s_stop continue Release state Release do e = s_level *. (1.0 -. float(c - s_stop) /. float(t - s_stop)); until c + 1 >= t continue Attack end tel node midi_piano<>(keys : int^2^n; time : int^n) returns (o : stereo) var i, j : int; next : bool; duree_mesure : int; e : float; let duree_mesure = 2 * period; (* 1 mesure = 8 noires = 4 sec à 120 BPM. *) i = periodic(n); j = maintain(next, i, 0); o = stereo_gain(e, stereo_mix<<2>>(map<<2>> tone_of_piano_key(keys[>j<]))); e = adsr_envelope(duree_mesure / time[> j <], 0.3, 0.1, 0.4, 0.5); automaton state Next do next = true until true then Wait state Wait var c : int; do next = false; c = 0 fby (c + 1); until c >= (duree_mesure / time[> j <]) then Next end tel const num_keys : int = 82 node main_pure_4() returns (o : stereo) var keys : int^2^num_keys; time : int^num_keys; let keys = [ [44, 00], [37, 00], [40, 00], [42, 00], [44, 00], [37, 00], [40, 00], [42, 00], [44, 00], [37, 00], [40, 00], [42, 00], [44, 00], [37, 00], [40, 00], [42, 00], [44, 00], [37, 00], [41, 00], [42, 00], [44, 00], [37, 00], [41, 00], [42, 00], [44, 00], [37, 00], [41, 00], [42, 00], [44, 00], [37, 00], [41, 00], [42, 00], [44, 00], [37, 00], [40, 37], [42, 00], [44, 00], [37, 00], [40, 00], [42, 00], [35, 39], [32, 00], [35, 00], [37, 00], [39, 00], [32, 00], [35, 00], [37, 00], [35, 39], [32, 00], [35, 00], [37, 00], [39, 00], [32, 00], [35, 00], [42, 00], [35, 00], [35, 40], [39, 00], [42, 00], [35, 00], [40, 00], [39, 00], [33, 37], [30, 00], [33, 00], [35, 00], [33, 00], [30, 00], [33, 00], [35, 00], [33, 37], [30, 00], [33, 00], [35, 00], [37, 00], [30, 00], [33, 00], [0, 0], [0, 0], [0, 0], [0, 0] (* silence *) ]; time = [ 4, 4, 8, 8, 4, 4, 8, 8, 4, 4, 8, 8, 4, 4, 8, 8, 4, 4, 8, 8, 4, 4, 8, 8, 4, 4, 8, 8, 4, 4, 8, 8, 2, 2, 8, 8, 2, 2, 8, 8, 4, 4, 8, 8, 4, 4, 8, 8, 4, 4, 8, 8, 4, 4, 4, 2, 2, 8, 8, 2, 2, 8, 8, 4, 4, 8, 8, 4, 4, 8, 8, 4, 4, 8, 8, 4, 4, 4, 1, 1, 1, 1 (* silence *) ]; o = midi_piano<>(keys, time); tel (* Bonus : la méthode de Karplus-Strong pour la synthèse de son de guitare. https://en.wikipedia.org/wiki/Karplus%E2%80%93Strong_string_synthesis http://sites.music.columbia.edu/cmc/MusicAndComputers/chapter4/04_09.php *) node flip(i: int) returns (o: float) let o = if (i % 2 = 0) then 1.0 else -.1.0 tel node karplus_strong<>() returns (y : float) var b : float^l; i: int; let i = 0 fby ((i+1) % l); y = 0.5 *. (b[>i<] +. 0.0 fby y); b = (mapi<> flip ()) fby ([b with [i] = y]); tel node repeat<>(x : stereo) returns (o : stereo) var last t : stereo^n = silence^n; let automaton state Fill do o = x; t = [ last t with [ periodic(n) ] = x ] until periodic(n) = n - 1 then Repeat state Repeat do o = t[> periodic(n) <] end tel node saturating_counter(max : int) returns (o : int) var c : int; let c = 0 fby (c + 1); o = if c < max then c else max; tel node main_kp() returns (o : stereo) var s : stereo; let s = repeat<>({ l = karplus_strong<<115>>(); r = karplus_strong<<55>>() }); o = stereo_gain(float(saturating_counter(5 * period)) /. float(5 * period), s); tel (* Le noeud principal du programme. *) (* Vous pouvez choisir un des noeuds principaux main_XXX écrits ci-dessus, ou bien écrire le votre. *) node main() returns (o : stereo) let o = main_pure_4(); tel