53 votes

Écriture de notes de musique dans un fichier wav

Je suis intéressé par la façon de prendre des notes de musique (par exemple A, B, C#, etc) ou des accords (plusieurs notes en même temps) et de les écrire dans un fichier wav.

D'après ce que j'ai compris, chaque note a une fréquence spécifique qui lui est associée (pour une hauteur parfaite) - par exemple A4 (le La au-dessus du Do moyen) est 440 Hz (liste complète aux 2/3 de la hauteur). Cette page ).

Si je comprends bien, ce pas est dans le domaine de la fréquence, et il faut donc lui appliquer la transformée de Fourier rapide inverse pour générer l'équivalent dans le domaine du temps ?

Ce que je veux savoir, c'est :

  • Comment fonctionnent les accords ? Sont-ils la moyenne des hauteurs ?
  • Comment la durée de lecture de chaque note est-elle spécifiée, lorsque le contenu du fichier wav est une forme d'onde ?
  • comment le résultat de la FFT inverse de plusieurs notes est-il converti en un tableau d'octets, qui constituent les données d'un fichier wav ?
  • toute autre information pertinente à ce sujet.

Merci pour toute aide que vous pourrez apporter. Si je donne des exemples de code, j'utilise C# et le code que j'utilise actuellement pour créer des fichiers wav est le suivant :

int channels = 1;
int bitsPerSample = 8;
//WaveFile is custom class to create a wav file.
WaveFile file = new WaveFile(channels, bitsPerSample, 11025);

int seconds = 60;
int samples = 11025 * seconds; //Create x seconds of audio

// Sound Data Size = Number Of Channels * Bits Per Sample * Samples

byte[] data = new byte[channels * bitsPerSample/8 * samples];

//Creates a Constant Sound
for(int i = 0; i < data.Length; i++)
{
    data[i] = (byte)(256 * Math.Sin(i));
}
file.SetData(data, samples);

Cela crée (d'une manière ou d'une autre) un son constant - mais je ne comprends pas complètement comment le code est corrélé avec le résultat.

0 votes

P.S., je comprends que la réponse ne sera pas triviale, donc même un lien vers un endroit expliquant cela serait utile.

0 votes

Vous pourriez trouver quelque chose ici sonicspot.com/guide/files d'ondes.html

0 votes

Commencez par les sox. Ensuite, si vous ressentez le besoin de "rouler votre propre", vous avez une bonne base de référence connue.

123voto

Eric Lippert Points 300275

Vous êtes sur la bonne voie.

Reprenons votre exemple :

for(int i = 0; i < data.Length; i++)
  data[i] = (byte)(256 * Math.Sin(i));

OK, vous avez 11025 échantillons par seconde. Vous avez 60 secondes d'échantillons. Chaque échantillon est un nombre compris entre 0 et 255 qui représente un petit changement dans pression de l'air en un point de l'espace à un moment donné.

Attendez une minute, le sinus va de -1 à 1, donc les échantillons vont de -256 à +256, et c'est plus grand que la gamme d'un octet, donc quelque chose de bizarre se passe ici. Retravaillons votre code pour que l'échantillon soit dans la bonne plage.

for(int i = 0; i < data.Length; i++)
  data[i] = (byte)(128 + 127 * Math.Sin(i));

Nous avons maintenant des données à variation douce qui vont de 1 à 255, nous sommes donc dans la gamme d'un octet.

Essayez-le et voyez ce que ça donne. Il devrait être beaucoup plus "fluide".

L'oreille humaine détecte des changements incroyablement minuscules de la pression atmosphérique. Si ces changements forment un motif répétitif alors le fréquence à laquelle le motif se répète est interprétée par la cochlée de votre oreille comme un son particulier. Le site taille du changement de pression est interprété comme le volume .

Votre forme d'onde dure soixante secondes. Le changement va du plus petit changement, 1, au plus grand changement, 255. Où sont les pics ? C'est-à-dire, où l'échantillon atteint-il une valeur de 255, ou proche de celle-ci ?

Eh bien, le sinus est égal à 1 pour /2 , 5/2, 9/2, 13/2, et ainsi de suite. Donc les pics sont atteints lorsque i est proche de l'un d'entre eux. C'est-à-dire, à 2, 8, 14, 20,...

A quelle distance dans le temps sont-elles ? Chaque échantillon représente 1/11025e de seconde, donc les pics sont espacés d'environ 2/11025 = environ 570 microsecondes. Combien de pics y a-t-il par seconde ? 11025/2 = 1755 Hz. (Le Hertz est la mesure de la fréquence ; combien de pics par seconde). 1760 Hz correspond à deux octaves au-dessus du la 440, il s'agit donc d'un ton la légèrement bémolisé.

Comment fonctionnent les accords ? Sont-ils la moyenne des hauteurs ?

Non. Un accord qui est A440 et une octave au-dessus, A880 n'est pas équivalent à 660 Hz. Vous n'avez pas moyennement le site terrain . Vous somme le site forme d'onde .

Pensez à la pression de l'air. Si vous avez une source vibrante qui fait monter et descendre la pression 440 fois par seconde, et une autre qui fait monter et descendre la pression 880 fois par seconde, le résultat net n'est pas le même qu'une vibration à 660 fois par seconde. Elle est égale à la somme des pressions à un moment donné. Rappelez-vous, c'est tout ce qu'est un fichier WAV : une grande liste de changements de pression d'air .

Supposons que vous vouliez faire une octave en dessous de votre échantillon. Quelle est la fréquence ? Deux fois moins. Donc faisons en sorte que ça arrive deux fois moins souvent :

for(int i = 0; i < data.Length; i++)
  data[i] = (byte)(128 + 127 * Math.Sin(i/2.0)); 

Notez que la valeur doit être 2.0, et non 2. Nous ne voulons pas arrondir les nombres entiers ! Le 2.0 indique au compilateur que vous voulez le résultat en virgule flottante, pas en entier.

Si vous faites cela, vous obtiendrez des pics deux fois moins souvent : à i = 4, 16, 28... et donc la tonalité sera une octave complète plus basse. (Chaque octave vers le bas moitiés la fréquence ; chaque octave vers le haut doubles le.)

Essayez cela et voyez si vous obtenez le même son, une octave plus bas.

Maintenant, ajoutez-les ensemble.

for(int i = 0; i < data.Length; i++)
  data[i] = (byte)(128 + 127 * Math.Sin(i)) + 
            (byte)(128 + 127 * Math.Sin(i/2.0)); 

Ça a probablement sonné comme de la merde. Que s'est-il passé ? Nous avons débordé à nouveau la somme était supérieure à 256 à de nombreux endroits. Divisez par deux le volume des deux vagues :

for(int i = 0; i < data.Length; i++)
  data[i] = (byte)(128 + (63 * Math.Sin(i/2.0) + 63 * Math.Sin(i))); 

Mieux. "63 sin x + 63 sin y" est compris entre -126 et +126, donc cela ne peut pas dépasser un octet.

(Alors là es une moyenne : nous prenons essentiellement la moyenne de la contribution à la pression de chaque ton et non la moyenne des fréquences .)

Si vous jouez cela, vous devriez obtenir les deux tons en même temps, l'un une octave plus haut que l'autre.

Cette dernière expression est compliquée et difficile à lire. Nous allons la décomposer en un code plus facile à lire. Mais d'abord, résumons l'histoire jusqu'à présent :

  • 128 est à mi-chemin entre la basse pression (0) et la haute pression (255).
  • le volume du ton est la pression maximale atteinte par l'onde
  • un ton est une onde sinusoïdale d'une fréquence donnée
  • la fréquence en Hz est la fréquence de l'échantillon (11025) divisée par 2

Alors, mettons tout ça ensemble :

double sampleFrequency = 11025.0;
double multiplier = 2.0 * Math.PI / sampleFrequency;
int volume = 20;

// initialize the data to "flat", no change in pressure, in the middle:
for(int i = 0; i < data.Length; i++)
  data[i] = 128;

// Add on a change in pressure equal to A440:
for(int i = 0; i < data.Length; i++)
  data[i] = (byte)(data[i] + volume * Math.Sin(i * multiplier * 440.0))); 

// Add on a change in pressure equal to A880:

for(int i = 0; i < data.Length; i++)
  data[i] = (byte)(data[i] + volume * Math.Sin(i * multiplier * 880.0))); 

Et voilà, vous pouvez maintenant générer n'importe quel son, de n'importe quelle fréquence et volume. Pour faire un accord, ajoutez-les ensemble, en veillant à ne pas aller trop fort et à ne pas dépasser l'octet.

Comment connaître la fréquence d'une note autre que A220, A440, A880, etc ? Chaque demi-ton supérieur multiplie la fréquence précédente par la 12ème racine de 2. Calculez donc la 12ème racine de 2, multipliez-la par 440, et vous obtenez A#. Multipliez A# par la 12ème racine de 2, vous obtenez B. B multiplié par la 12ème racine de 2 donne C, puis C#, et ainsi de suite. Faites-le 12 fois et, comme il s'agit de la 12ème racine de 2, vous obtiendrez 880, soit le double de ce que vous aviez au départ.

Comment la durée de lecture de chaque note est-elle spécifiée, lorsque le contenu du fichier wav est une forme d'onde ?

Il suffit de remplir l'espace de l'échantillon où le son est émis. Supposons que vous vouliez jouer A440 pendant 30 secondes, puis A880 pendant 30 secondes :

// initialize the data to "flat", no change in pressure, in the middle:
for(int i = 0; i < data.Length; i++)
  data[i] = 128;

// Add on a change in pressure equal to A440 for 30 seconds:
for(int i = 0; i < data.Length / 2; i++)
  data[i] = (data[i] + volume * Math.Sin(i * multiplier * 440.0))); 

// Add on a change in pressure equal to A880 for the other 30 seconds:

for(int i = data.Length / 2; i < data.Length; i++)
  data[i] = (byte)(data[i] + volume * Math.Sin(i * multiplier * 880.0))); 

comment le résultat de la FFT inverse de plusieurs notes est-il converti en un tableau d'octets, qui constituent les données d'un fichier wav ?

La FFT inverse construit simplement les ondes sinusoïdales et les additionne, comme nous le faisons ici. C'est tout ce que c'est !

toute autre information pertinente à ce sujet ?

Voir mes articles sur le sujet.

http://blogs.msdn.com/b/ericlippert/archive/tags/music/

Les parties un à trois expliquent pourquoi les pianos ont douze notes par octave.

La quatrième partie est pertinente pour votre question ; c'est là que nous construisons un fichier WAV à partir de zéro.

Notez que dans mon exemple, j'utilise 44100 échantillons par seconde, et non 11025, et que j'utilise des échantillons 16 bits allant de -16000 à +16000 au lieu d'échantillons 8 bits allant de 0 à 255. Mais à part ces détails, c'est fondamentalement le même que le vôtre.

Je vous recommande d'opter pour un débit binaire plus élevé si vous souhaitez réaliser des formes d'onde complexes ; 8 bits à 11K échantillons par seconde ne conviendront pas pour des formes d'onde complexes. 16 bits par échantillon avec 44K échantillons par seconde est une qualité CD.

Et franchement, il est beaucoup plus facile de faire les bons calculs si vous les faites en shorts signés plutôt qu'en octets non signés.

La cinquième partie donne un exemple intéressant d'illusion auditive.

Essayez également de regarder vos formes d'onde avec la visualisation "scope" dans Windows Media Player. Cela vous donnera une bonne idée de ce qui se passe réellement.

UPDATE :

J'ai remarqué que lorsque l'on ajoute deux notes l'une à l'autre, on peut se retrouver avec un bruit sec, en raison d'une transition trop nette entre les deux formes d'onde (par exemple, la fin de l'une et le début de la suivante). Comment résoudre ce problème ?

Excellente question de suivi.

Il s'agit essentiellement d'un passage instantané de la haute pression à la basse pression, qui se traduit par un "pop". Il y a plusieurs façons de gérer ce phénomène.

Technique 1 : Déplacement de phase

Une façon de procéder serait de "déphaser" le son suivant d'une petite quantité telle que la différence entre la valeur initiale du son suivant et la valeur finale du son précédent. Vous pouvez ajouter un terme de déphasage comme ceci :

  data[i] = (data[i] + volume * Math.Sin(phaseshift + i * multiplier * 440.0))); 

Si le déphasage est nul, il n'y a évidemment pas de changement. Un déphasage de 2 (ou de tout multiple pair de ) n'est pas non plus un changement, puisque le sinus a une période de 2. Chaque valeur entre 0 et 2 déplace l'endroit où le son "commence" un peu plus loin sur l'onde.

Déterminer exactement quel est le bon déphasage peut être un peu délicat. Si vous lisez mes articles sur la génération d'un son d'illusion Shepard "descendant en continu", vous verrez que j'ai utilisé quelques calculs simples pour m'assurer que tout changeait en continu sans aucun pop. Vous pouvez utiliser des techniques similaires pour déterminer quel est le bon décalage pour faire disparaître le pop.

J'essaie de trouver comment générer la valeur du déphasage. Est-ce que "ArcSin(((premier échantillon de données de la nouvelle note) - (dernier échantillon de données de la note précédente))/noteVolume)" est correct ?

Eh bien, la première chose à réaliser est qu'il n'y a peut-être pas être une "bonne valeur". Si la note de fin est très forte et se termine sur un pic, et que la note de départ est très calme, il se peut que le nouveau son ne corresponde pas à la valeur de l'ancien son.

En supposant qu'il existe une solution, quelle est-elle ? Vous avez un échantillon final, appelé y, et vous voulez trouver le déphasage x tel que

y = v * sin(x + i * freq)

lorsque i est égal à zéro. Donc, c'est

x = arcsin(y / v)

Cependant c'est peut-être pas tout à fait ça ! Supposons que vous ayez

sine wave 1

et vous voulez ajouter

sine wave 2

Il y a deux déphasages possibles :

sine wave 3

y

Sine wave 4

Devinez laquelle des deux sonne le mieux :-)

Il n'est pas toujours facile de savoir si l'on se trouve dans le sens ascendant ou descendant de la vague. Si vous ne voulez pas faire de véritables calculs, vous pouvez utiliser des méthodes heuristiques simples, comme "le signe de la différence entre les points de données successifs a-t-il changé à la transition ?".

Technique 2 : enveloppe ADSR

Si vous modélisez quelque chose qui est censé sonner comme un véritable instrument, vous pouvez obtenir de bons résultats en modifiant le volume comme suit.

Ce que vous voulez faire, c'est avoir quatre sections différentes pour chaque note, appelées attaque, déclin, maintien et relâchement. Le volume d'une note jouée sur un instrument peut être modélisé comme suit :

     /\
    /  \__________
   /              \
  /                \
   A  D   S       R

Le volume commence à zéro. Puis l'attaque se produit : le son monte rapidement jusqu'à son volume maximal. Puis il diminue légèrement jusqu'à son niveau de maintien. Il reste ensuite à ce niveau, en diminuant peut-être lentement pendant que la note est jouée, puis il redescend à zéro.

Si vous faites cela, il n'y a pas de pop car le début et la fin de chaque note sont à zéro volume. C'est ce que garantit le release.

Des instruments différents ont des "enveloppes" différentes. Un orgue à tuyaux, par exemple, a une attaque, un déclin et un relâchement incroyablement courts ; il n'y a que du sustain, et le sustain est infini. Votre code existant est comme un orgue à tuyaux. Comparez avec, disons, un piano. Là encore, l'attaque, la décroissance et le relâchement sont courts, mais le son devient progressivement plus faible pendant le maintien.

Les sections d'attaque, de décroissance et de relâchement peuvent être très courtes, trop courtes pour être entendues mais suffisamment longues pour éviter le pop. Essayez de modifier le volume pendant que la note est jouée et voyez ce qui se passe.

5voto

Markus Johnsson Points 2157

Vous êtes sur la bonne voie. :)

Signal audio

Vous n'avez pas besoin d'effectuer une FFT inverse (vous pourriez le faire, mais vous devriez trouver une librairie pour cela ou l'implémenter, en plus de générer un signal en entrée de la FFT). Il est beaucoup plus facile de générer directement le résultat que nous attendons de cette FFT inverse, qui est un signal sinusoïdal avec la fréquence donnée.

L'argument du sinus dépend à la fois de la note que vous voulez générer et de l'argument de l'instrument. fréquence d'échantillonnage du fichier wave que vous générez (souvent égal à 44100Hz, dans votre exemple vous utilisez 11025Hz).

Pour un son de 1 Hz, il faut un signal sinusoïdal dont la période est égale à une seconde. Avec 44100 Hz, il y a 44100 échantillons par seconde, ce qui signifie que nous devons avoir un signal sinusoïdal avec une période égale à 44100 échantillons. Puisque la période du sinus est égale à Tau (2*Pi) on obtient :

sin(44100*f) = sin(tau)
44100*f = tau
f = tau / 44100 = 2*pi / 44100

Pour 440 Hz, on obtient :

sin(44100*f) = sin(440*tau)
44100*f = 440*tau
f = 440 * tau / 44100 = 440 * 2 * pi / 44100

En C#, ce serait quelque chose comme ceci :

double toneFreq = 440d;
double f = toneFreq * 2d * Math.PI / 44100d;
for (int i = 0; i<data.Length; i++)
    data[i] = (byte)(128 + 127*Math.Sin(f*i));

NOTE : Je n'ai pas testé ceci pour vérifier l'exactitude du code. Je vais essayer de le faire et de corriger les erreurs éventuelles. Mise à jour : J'ai mis à jour le code pour qu'il fonctionne. Désolé de vous avoir fait mal aux oreilles ;-)

Accords

Les accords sont des combinaisons de notes (voir par exemple Accord mineur sur Wikipédia ). Le signal serait donc une combinaison (somme) de sinusoïdes de fréquences différentes.

Tons purs

Ces sons et ces accords n'auront cependant pas un son naturel, car les instruments traditionnels ne jouent pas de sons à fréquence unique. Au contraire, lorsque vous jouez un A4, il y a une large distribution de fréquences, avec une concentration autour de 440 Hz. Voir par exemple Timbre .

3voto

P i Points 6466

Personne n'a encore mentionné l'algorithme des cordes pincées de Karplus Strong.

Synthèse des cordes Karplus-Strong Il s'agit d'une méthode extrêmement simple pour générer un son de corde pincée réaliste. J'ai écrit des instruments de musique polyphoniques / des lecteurs MIDI en temps réel en utilisant cette méthode.

Tu fais comme ça :

Tout d'abord, quelle fréquence voulez-vous simuler ? Disons que la fréquence de concert A = 440Hz

En supposant que votre fréquence d'échantillonnage soit de 44,1 kHz, cela représente 44100 / 440 = 100,25 échantillons par longueur d'onde.

Arrondissons ce chiffre à l'entier le plus proche : 100, et créons un tampon circulaire de longueur 100.

Il contiendra donc une onde stationnaire de fréquence ~440Hz (notez que ce n'est pas exact, il y a des moyens de contourner cela).

Remplissez-le de statique aléatoire entre -1 et +1, et :

DECAY = 0.99
while( n < 99999 )
    outbuf[n++] = buf[k]

    newVal = DECAY  *  ( buf[k] + buf_prev ) / 2

    buf_prev = buf[k]
    buf[k] = newVal

    k = (k+1) % 100

C'est un algorithme étonnant car il est très simple et génère un super son.

La meilleure façon de comprendre ce qui se passe est de réaliser que la statique aléatoire dans le domaine temporel est un bruit blanc ; la statique aléatoire dans le domaine fréquentiel. Vous pouvez l'imaginer comme le composite de nombreuses ondes de différentes fréquences (aléatoires).

Les fréquences proches de 440Hz (ou 2*440Hz, 3*440Hz, etc.) créeront des interférences constructives avec elles-mêmes, en passant autour de l'anneau encore et encore. Elles seront donc préservées. Les autres fréquences vont créer des interférences destructives avec elles-mêmes.

De plus, le calcul de la moyenne agit comme un filtre passe-bas -- imaginez que votre séquence soit +1 -1 +1 -1 +1 -1, si vous calculez la moyenne de paires, chaque moyenne sera égale à 0. Mais si vous avez une onde plus lente comme 0 0,2 0,3 0,33 0,3 0,2 ... alors le calcul de la moyenne donne toujours une onde. Plus l'onde est longue, plus son énergie est préservée, c'est-à-dire que le calcul de la moyenne entraîne moins d'amortissement.

Le calcul de la moyenne peut donc être considéré comme un filtre passe-bas très simple.

Il y a bien sûr des complications, le fait de devoir choisir une longueur de tampon entière oblige à une quantification des fréquences possibles, ce qui devient perceptible vers le haut du piano. Tout est surmontable mais cela devient difficile !

Liens :

Delicious Max/MSP Tutorial 1 : Karplus-Strong

L'algorithme de Karplus-Strong

Pour autant que je sache, JOS est la principale autorité mondiale en matière de génération de sons synthétiques, tous les chemins mènent à son site web. Mais attention, cela devient vite compliqué et nécessite des mathématiques de niveau universitaire.

0 votes

L'algorithme semble intéressant ; il s'agit essentiellement de la combinaison d'un filtre FIR du premier ordre et d'un filtre boxcar. Je me suis amusé avec du JavaScript pour produire des accords de guitare en utilisant la synthèse FM. L'approche par cordes pincées semble intéressante, mais je me demande dans quelle mesure elle est cohérente ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X