SYNTHÉTISEUR MUSICAL

Défi individuel de programmation Java, semestre 1.

MUSIQUE : NOTES et FRÉQUENCES

(Source Wikipédia)

Dans un modéle simple, chaque note de musique se caractérise par :

  • sa hauteur (notion de grave / aigu),
  • sa durée,
  • son intensité (notion de volume sonore),
  • son timbre (notion d'instrument).

La hauteur d'une note correspond à sa fréquence fondamentale (en Hz) et on distingue, dans la musique occidentale, douze catégories de hauteurs dont sept principales : do, , mi, fa, sol, la et si. Deux hauteurs consécutives de même nom ont des fréquences fondamentales dont l'une est la moitié de l'autre et définissent un intervalle appelé octave.

Pour distinguer les notes de même nom, on numérote les octaves et on accole éventuellement ce numéro au nom de la note. Par exemple le fameux \(\mathbf{la_3}\) et ses 440Hz. Ainsi, si l'on connait la fréquence fondamentale \(f_0\) d'une note donnée, on on déduit la fréquence \(f_n\) d'une autre notre située \(n\) demi tons (les douze catégories) au dessus ar la formule \(f_n = f_0\times 2^{n/12}\)

MUSIQUE : NOTES et FRÉQUENCES

L'application de la formule précedente permet de calculer les fréquences fondamentales des douze demi tons. Le tableau ci-dessous donne les résultats pour les octaves 0 à 7 et pourra être utile lors de la mise au point de votre programme, pour vérifier les valeurs calculées.

fréquences fondamentales (en Hz)

Note / Octave

0

1

2

3

4

5

6

7

do = si#

32.70

65.41

130.81

261.63

523.25

1046.50

2093.00

4186.01

do# = ré b

34.65

69.30

138.59

277.18

554.37

1108.73

2217.46

4434.92

36.71

73.42

146.83

293.66

587.33

1174.66

2349.32

4698.64

ré# = mi b

38.39

77.78

155.56

311.13

622.25

1244.51

2489.02

4978.03

mi = fa b

41.20

82.41

164.81

329.63

659.26

1318.51

2637.02

5274.04

fa = mi#

43.65

87.31

174.61

349.23

698.46

1396.91

2793.83

5587.65

fa# = solb

46.25

92.50

185.00

369.99

739.99

1479.98

2959.96

5919.91

sol

49.00

98.00

196.00

392.00

783.99

1567.98

3135.96

6271.93

sol#= la b

51.91

103.83

207.65

415.30

830.61

1661.22

3322.44

6644.88

la

55.00

110.00

220.00

440.00

880.00

1760.00

3520.00

7040.00

la# = si b

58.27

116.54

233.08

466.16

932.33

1864.66

3729.31

7458.62

si = do b

61.7

123.47

246.94

493.88

987.77

1975.53

3951.07

7902.13

MUSIQUE : NOTES et DURÉES

Sur la portée musicale, les figures des notes indiquent leur durée relativement à une pulsation de référence. Ainsi, comme le montre la figure ci-dessous, la ronde vaut deux blanches. La banche vaut deux noires, noire qui vaut encore deux croches, valant chacune deux double-croches, etc.

La pulsation de référence, ou tempo, est exprimée en nombre de noires par minutes et sera repérée tempo dans vos programmes.

MUSIQUE et SIGNAL

Une note de musique pure n est un signal sinusoïdal de fréquence f, d'amplitude A et de durée d dont l'expression en fonction du temps est la suivante :

\( \forall t \in [0; d],~~~n(t) = A.sin(2\pi ft)\)

Dans le monde numérique, un signal (sinusoïdal ou pas) n'est évalué que périodiquement tous les \(T_e\) secondes. On appelle \(T_e\) la période d'échantillonnage, elle est l'inverse de la fréquence d'échantillonnage \(F_e\).

Les CDs audio sont typiquement réalisés en utilisant à \(F_e = 44100~Hz\) et c'est également à cette fréquence d'échantillonnage que les sons que vous synthétiserez seront restitués.

MUSIQUE ET SIGNAL

Les harmoniques

Aucun instrument ne produit toutefois des notes pures. Les dispositifs mécaniques vibratoires mis en œuvre dans les instruments, voix comprise, génèrent des ondes acoustiques complexes, dont le spectre comporte plusieurs fréquences identifiables en plus de la fréquence fondamentale, ainsi que d'autres bruits caractéristiques de l'instrument. Parmi les fréquences identifiables des notes émises par un instrument, on distingue les harmoniques qui sont des multiples ou des sous-multiples de la fréquence fondamentale.

On donne ci-dessous deux allures possibles pour le spectre de la note la3, de fréquence fondamentale 440Hz.

Pour votre implémentation, on limitera les harmoniques à 0.5f, 2f et 3f, d'amplitudes respectives a/4, a/4 et a/8 si a est l'amplitude à la fréquence f.

Note pure f=440Hz (la3)

Note avec harmoniques à 0.5f, 2f, 3f et 4f

spectre1

spectre2

LA BIBLIOTHÈQUE StdAudio (1)

La bibliothèque StdAudio.java met à votre disposition un ensemble de méthodes permetant de traiter des signaux audio numériques. Parmi celles-ci, vous aurez plus particulièrement besoin d'utiliser les méthodes suivantes, qui fonctionnent à \(F_e = 44.1~kHz\) :

// envoie un échantillon *in* d'amplitude comprise entre -1 et +1
// sur la sortie audio
public static void play(double in)

// envoie la suite d'échantillon du tableau *input*
// sur la sortie audio (amplitudes entre -1 et +1)
public static void play(double[] input)

LA BIBLIOTHÈQUE StdAudio (2)

Exemple d'utilisation

/**
* Prend en argument la durée d en secondes
* et génère un blanc blanc de durée d,
* puis l'envoie sur la sortie audio à l'aide de la méthode StdAudio.play()
*/
       public class TestAudio {
           public static Random rand = new Random();
               public static void main(String[] args) {
                   final double F_ECH = 44100.0 ;
                   double duration = 1.0 ;
                   int i;
                   if (args.length == 1) duration = Double.parseDouble(args[0]);
                   double[] signal =  new double[(int)Math.round(duration * F_ECH)];
                   for (i = 0; i< signal.length; i++) {
                       signal[i] = rand.nextDouble()*2.0 -1.0 ;
                   }
                   for (i = 0; i< signal.length; i++) {
                       StdAudio.play(signal[i]);
                   }
               }
           }

LA BIBLIOTHÈQUE StdAudio (3)

Compilation

javac StdAudio
javac TestAudio

Exécution : joue un bruit blanc de 1.5 seconde

java TestAudio 1.5

Le fichier TestAudio.java est téléchargeable.

TRAVAIL DEMANDÉ

Le projet qui vous est proposé consiste à concevoir un programme capable de jouer des partitions musicales simples. Il pourra en particulier lire les notes qu'il doit jouer dans un fichier texte dont le format est préalablement défini. Les plus déterminés pourront ajouter à la partie audio la visualisation des formes d'onde jouées par l'ordinateur.

Les pages suivantes détaillent les étapes de travail.

La classe Note

La classe Note représente une note de musique par ses attributs

TRAVAIL DEMANDÉ

La classe Note : squelette

public class Note {
 final static double[] fondFreq = {32.70, 65.41, 130.81, 261.63, 523.25,
                                   1046.50, 2093.00, 4186.01};
 final static String[] tons = {"do", "re", "mi", "fa", "sol", "la", "si"};
 final static int[] haut = {0, 2, 4, 5, 7, 9, 11};
 public String toneBase ;
 public char alter ;
 public int octave ;
 public double freq ;
 public double duree;
 public double amp;
 double[] signal ;
 /*
 * Le constructeur permettant de déclarer/allouer une note par
 * Note note = new Note(ton, alter, octave, duree, amplitude);
 */
 public Note(String tB, char alt, int oct, double dur, double amp){
     duree = dur ;
     alter = alt ;
     toneBase = tB ;
     octave = oct ;
     freq = freqTone(toneBase, alter, octave) ;
     // compléter ici la définition de **signal**
 }}

TRAVAIL DEMANDÉ

La classe Note : squelette (suite)

Pour la mise au point de la classe Note, il est possible d'y ajouter une méthode main() comme celle proposée ci-dessous qui prend le numéro de l'octave en argument de la ligne de commande et joue ses 7 tons de base, à raison d'une seconde chacun. Les paramètres de chaque note sont également affichés dans la console.

/*
* méthode main() de test de la classe Note
*/
 public static void main(String[] args){
     int i, oct ;
     if (args.length < 1) oct = 3 ; else oct = Integer.parseInt(args[0]) ;
     for (i = 0; i< 7; i++){
         Note not = new Note(tons[i], ' ', oct, 1.0, 1.0);
         System.out.print(not.toneBase + ", octave " + not.octave
                             + "  f0 =" + fondFreq[not.octave] + "Hz, F =");
         System.out.format("%.2f Hz%n",not.freq);
         not.play();
     }
 }

TRAVAIL DEMANDÉ

La classe Note : synthèse de note pures (1)

Dans le fichier Note.java,

  1. Copier le squelette fourni.
  2. Compléter la définition du constructeur Note( String, char, int, double, double) pour générer convenablement les valeurs du tableau signal (voir commentaire dans le squelette).
  3. Concevoir une méthode qui calcule et retourne la valeur de la fréquence fondamentale de la note à jouer, correspondant à l'attribut freq. Elle aura pour prototype
    private static double freqTone(String toneBase, char alter, int octave)
  4. Écrire une méthode qui envoie la note vers la sortie audio. Elle utilise pour cela les méthodes de la bibliothèque StdAudio et a pour prototype
    public void play()
  5. Concevoir une méthode qui créé puis renvoie la Note décrite par les paramètres fournis. Le prototype en sera le suivant :
    public static Note sToNote(String tonalite, double amplitude, double duree, boolean harmon);
    • tonalite donne la hauteur et l'octave. Exemples : la3# sol4b re2
    • amplitude est l'amplitude de la sinusoïde, comprise entre 0 et 1.
    • duree est le duree de tenue de la note, en secondes
    • harmon vaut true si l'on génère des harmoniques, false sinon.

TRAVAIL DEMANDÉ

La classe Note : synthèse de note pures (2)

  1. Écrire une méthode qui retourne la duree correspondant à la figure (noire, croche,...) et au tempo (noires par minutes) passés en paramètres, de prototype :
    public static double faceToDuration(String figure, int tempo)
  2. Ajouter à la classe Note le constructeur de copie, dont le code est fourni ci-dessous, et qui permet de créer une note par copie d'une note existante avec l'instruction
    Note note = new Note(oldNote) ;
    Code du constructeur de copie, à insérer dans le code de la classe Note
    public Note(Note oldNote){
           duree = oldNote.duree ;
           alter = oldNote.alter ;
           toneBase = oldNote.toneBase ;
           octave = oldNote.octave ;
           amp = oldNote.amp ;
           freq = oldNote.freq ;
           signal = oldNote.signal.clone() ;
    }

TRAVAIL DEMANDÉ

La classe Accord : jouer des accords (1)

Pour jouer des accords, il faut pouvoir jouer plusieurs notes en même temps. Il suffit pour cela de sommer les signaux des différentes notes composant l'accord.

On fournit le squelette suivant pour la classe Accord :

public class Accord {
       Note[] notes ;
       double duree ;
       double[] signal ;

       public Accord(Note note1) {
           int i ;
           notes = new Note[4] ;
           notes[0] = new Note(note1) ;
           notes[1] = null ;
           notes[2] = null ;
           notes[3] = null ;
           duree = note1.duree;
           signal = note1.signal.clone() ;
       }
}

TRAVAIL DEMANDÉ

La classe Accord : jouer des accords (2)

Dans le squelette fourni en page précédente, on peut voir :

  • qu'un accord comprend au maximum 4 notes,
  • comment déclarer un accord en y affectant un première note
    Accord chord = new Accord(note1) ;
  1. Concevoir une méthode qui permette d'ajouter une note à un accord existant. Il faudra bien gérer l'équilibre et la somme des amplitudes, qui doit demeurer inférieure à 1. Son prototype sera :
    public void addNote(Note not)
  2. Écrire une méthode qui envoie l'accord vers la sortie audio :
    public void play()
  3. Concevoir une méthode main() à la classe Accord permettant de valider les méthodes précédentes. On l'exécutera simplement par
    java Accord

TRAVAIL DEMANDÉ

La classe Synthe : lire un fichier partition (1)

La classe Synthe a pour rôle la lecture d'un fichier formaté contenant sur chaque ligne la description d'un accord à jouer. Chaque accord peut ne comporter qu'une seule note. Le format d'une ligne suit les exemples suivants :

re3,croche,0.5
mi3#,la3,noire,0.125
  1. l'accord ne comprend qu'une note : le ré3. Sa durée est celle d'une croche et son amplitude de 0.5 (amplitude max. = 1.0)
  2. l'accord comprend 2 notes : le mi3# et le la3. Sa durée est celle d'une noire et son amplitude 0.125

Les durées réelles, en secondes, sont calculées grâce à la méthode Note.faceToDuration() déjà développée précédemment.

TRAVAIL DEMANDÉ

La classe Synthe : lire un fichier partition (2)

La méthode main() de la classe Synthe prend 2 ou 3 arguments sur la ligne de commande :

java Synthe <fichier> <tempo> [harm]|[guitar]

Les arguments harm et guitar sont mutuellement exclusifs et optionnels.

Elle lit ensuite le fichier ligne par ligne. Chaque ligne lue est passée en paramètre de type String à la méthode playLigne(), qui se charge de la décoder, créer l'accord correspondant et l'envoyer vers la sortie audio. Un exemple de fichier partition est téléchargeable.

Le code la méthode main() est fourni en page suivante.

  1. Concevoir la méthode playLigne de prototype
    public static void playLigne(String ligne, boolean harm, boolean guitar)

TRAVAIL DEMANDÉ

La classe Synthe : lire un fichier partition (3)

Le squelette de la classe Synthe

public class Synthe {
  static int tempo ;
  static boolean harm = false ;
  static boolean guitar = false ;

  public static void main(String[] args) {
      if (args.length < 2) {
          System.out.println("Usage : java Synthe <fichier> <tempo> <harm/guitar>");
          System.exit(-1);
      }
      tempo = Integer.parseInt(args[1]);
      if (args.length == 3){
          if (args[2].equals("harm")) harm = true ;
          else if (args[2].equals("guitar")) guitar = true ;}
      try {
          BufferedReader fichier = new BufferedReader(new FileReader(args[0]));
          String ligne;
          while ((ligne = fichier.readLine()) != null) {
              playLigne(ligne, harm, guitar);}
          fichier.close();
      } catch (IOException ex) { ex.printStackTrace(); }
      System.exit(0); }}

TRAVAIL DEMANDÉ pour les plus ambitieux

La classe Synthe : visualiser le signal (4)

  1. Concevoir une méthode permettant de tracer l'évolution temporelle du signal envoyé à la sortie audio. Elle utilisera pour cela la bibliothèque StdDraw déjà utilisée en TP. Il peut s'avérer intéressant de modifier quelque peu les autres méthodes de la classe, ainsi que les variables de classe.
  2. Reconnaître le morceau du fichier partitionTest ;-)

TRAVAIL DEMANDÉ

Contraintes et attendus

Les recommandations suivantes sont à suivre impérativement. Leur respect représentera une part non négligeable des points attribués à vos travaux.

Ces recommandations sont susceptaible de s'enrichir au fur et à mesure des semaines, pensez à la consulter fréquemment.

Bon courage à tous !

SpaceForward
Left, Down, Page DownNext slide
Right, Up, Page UpPrevious slide
POpen presenter console
HToggle this help