Génération d'un environnement sonore et musical dans Unity3D. Partie 2. Créer une piste 2D à partir de la musique

Annotation


Bonjour à tous. Relativement récemment, j'ai écrit un article Générer un environnement basé sur le son et la musique dans Unity3D , dans lequel j'ai donné plusieurs exemples de jeux qui utilisent la mécanique de génération de contenu basé sur la musique, et j'ai également parlé des aspects fondamentaux de ces jeux. Il n'y avait pratiquement pas de code dans l'article et j'ai promis qu'il y aurait une suite. Et le voici devant vous. Cette fois, nous allons essayer de créer une piste pour une course 2D, dans le style de Hill Climb, à partir de votre musique. Voyons voir ce que nous obtenons ..



Présentation


Je vous rappelle que cette série d'articles est conçue pour les développeurs débutants et pour ceux qui ont récemment commencé à travailler avec le son. Si vous faites une transformation de Fourier rapide dans votre esprit, vous vous ennuierez probablement.


Voici notre feuille de route pour aujourd'hui:


  1. Considérez ce qu'est la discrétisation.
  2. Découvrez quelles données nous pouvons obtenir d'Audio Clip Unity
  3. Comprenez comment nous pouvons travailler avec ces données.
  4. Découvrez ce que nous pouvons générer à partir de ces données.
  5. Apprenez à créer un jeu à partir de tout cela (enfin, ou quelque chose de similaire à un jeu)

Alors allons-y!


Discrétisation du cinghalais analogique


Comme beaucoup de gens le savent, pour utiliser un signal dans des systèmes numériques, nous devons le convertir. L'une des étapes de conversion est l'échantillonnage du signal, dans lequel le signal analogique est divisé en parties (rapports temporaires), après quoi chaque rapport se voit attribuer la valeur d'amplitude qui était au moment sélectionné.


La lettre T indique la période d'échantillonnage. Plus la période est courte, plus la conversion du signal sera précise. Mais le plus souvent, ils parlent de l'inverse: Taux d'échantillonnage (il est logique que ce soit F = 1 / T). 8 000 Hz est suffisant pour un téléphone téléphonique et, par exemple, l'une des options pour le format DVD-Audio nécessite une fréquence d'échantillonnage de 192 000 Hz. La norme en enregistrement numérique (dans les éditeurs de jeux, les éditeurs de musique) est de 44 100 Hz - c'est la fréquence du CD Audio.


Les valeurs numériques de l'amplitude sont stockées dans les soi-disant échantillons et c'est avec eux que nous travaillerons. La valeur de l'échantillon est float et elle peut aller de -1 à 1. Simplifié, il ressemble à ceci.



Rendu des ondes sonores (statique)


Informations basiques


La forme d'onde (ou forme audio, et chez les gens ordinaires - «poisson») est une représentation visuelle du signal sonore au fil du temps. La forme d'onde peut nous montrer à quel point du son se produit la phase active et où se produit l'atténuation. Souvent, la forme d'onde est présentée pour chaque canal séparément, par exemple, comme ceci:



Imaginez que nous avons déjà une AudioSource et un script dans lesquels nous travaillons. Voyons ce que Unity peut nous apporter.


//  AudioSource    AudioSource myAudio = gameObject.GetComponent<AudioSource>(); //     .     44100. int freq = myAudio.clip.frequency; 

Sélectionnez le nombre de rapports


Avant d'aller plus loin, nous devons parler un peu de la profondeur de rendu de notre son. Avec une fréquence d'échantillonnage de 44100 Hz par seconde, nous sommes en mesure de traiter 44100 rapports. Disons que nous devons rendre une piste de 10 secondes. Nous allons dessiner chaque rapport avec une ligne d'un pixel de large. Il s'avère que notre forme d'onde aura une longueur de 441 000 pixels. Vous obtenez une onde sonore très longue, allongée et peu comprise. Mais, vous y voyez chaque rapport spécifique! Et vous allez terriblement charger le système, peu importe comment vous le dessinez.



Si vous ne créez pas de logiciel audio professionnel, vous n'avez pas besoin d'une telle précision. Pour une image audio générale, nous pouvons diviser tous les échantillons en périodes plus longues et prendre, par exemple, la moyenne de chaque 100 échantillons. Ensuite, notre vague aura une forme très distincte:



Bien sûr, ce n'est pas tout à fait exact, car vous pouvez ignorer les pics de volume dont vous pourriez avoir besoin, vous pouvez donc essayer non pas la valeur moyenne, mais le maximum de ce segment. Cela donnera une image légèrement différente, mais vos pics ne disparaîtront pas.


Préparation à la réception audio


Définissons la précision de notre échantillon en tant que qualité et le nombre final de rapports en tant que sampleCount.


 int quality = 100; int sampleCount = 0; sampleCount = freq / quality; 

Un exemple de calcul de tous les nombres sera ci-dessous.


Ensuite, nous devons obtenir les échantillons nous-mêmes. Cela peut être fait à partir d'un clip audio à l'aide de la méthode GetData .


 public bool GetData(float[] data, int offsetSamples); 

Cette méthode prend un tableau dans lequel elle écrit des échantillons. offsetSamples - paramètre responsable du point de départ de la lecture du tableau de données. Si vous lisez le tableau depuis le début, il devrait y avoir zéro.


Pour enregistrer des échantillons, nous devons préparer un tableau pour eux. Par exemple, comme ceci:


 float[] samples; float[] waveFormArray; //      samples = new float[myAudio.clip.samples * myAudio.clip.channels]; 

Pourquoi avons-nous multiplié la longueur par le nombre de chaînes? Maintenant, je vais dire ...


Informations sur la chaîne audio Unity


Beaucoup de gens savent que dans le son, nous utilisons généralement deux canaux: gauche et droit. Quelqu'un sait qu'il existe des systèmes 2.1, ainsi que 5.1, 7.1 dans lesquels les sources sonores entourent de tous les côtés. Le thème des chaînes est bien décrit sur le wiki . Comment cela fonctionne-t-il dans Unity?


Lors du téléchargement d'un fichier, lors de l'ouverture d'un clip, vous pouvez trouver l'image suivante:



Il est juste montré ici que nous avons deux canaux, et vous pouvez même remarquer qu'ils sont différents l'un de l'autre. Unity enregistre les échantillons de ces canaux les uns après les autres. Il s'avère que cette image:


C'est pourquoi nous avons besoin de deux fois plus d'espace dans le tableau que juste pour le nombre d'échantillons.


Si vous sélectionnez l'option Forcer le clip mono, le canal sera un et tout le son sera au centre. L'aperçu de votre vague changera immédiatement.




Recevoir des données audio


Voici ce que nous obtenons:


 private int quality = 100; private int sampleCount = 0; private float[] waveFormArray; private float[] samples; private AudioSource myAudio; void Start() { myAudio = gameObject.GetComponent<AudioSource>(); int freq = myAudio.clip.frequency; sampleCount = freq / quality; samples = new float[myAudio.clip.samples * myAudio.clip.channels]; myAudio.clip.GetData(samples,0); //  ,    .       waveFormArray = new float[(samples.Length / sampleCount)]; //             for (int i = 0; i < waveFormArray.Length; i++) { waveFormArray[i] = 0; for (int j = 0; j < sampleCount; j++) { //Abs     ""    . .  waveFormArray[i] += Mathf.Abs(samples[(i * sampleCount) + j]); } waveFormArray[i] /= sampleCount; } } 

Au total, si la piste dure 10 secondes et qu'elle est à deux canaux, alors nous obtenons ce qui suit:


  • Le nombre d'échantillons dans le clip (myAudio.clip.sample) = 44100 * 10 = 441000
  • Le réseau d'échantillons pour deux canaux est long (samples.Length) = 441000 * 2 = 882000
  • Nombre de rapports (sampleCount) = 44100/100 = 441
  • La longueur du tableau final = samples.Length / sampleCount = 2000

En conséquence, nous travaillerons avec 2000 points, ce qui nous suffit pour dessiner la vague. Vous devez maintenant faire preuve d'imagination et réfléchir à la manière dont nous pouvons utiliser ces données.


Rendu des informations audio


Créer une piste audio simple à l'aide des outils de débogage


Comme beaucoup de gens le savent, Unity dispose de moyens pratiques pour afficher toutes sortes d'informations de débogage. Un développeur intelligent basé sur ces outils peut réaliser, par exemple, des extensions très puissantes pour l'éditeur. Notre cas montre une utilisation très atypique des méthodes de débogage.


Pour dessiner, nous avons besoin d'une ligne. Nous pouvons le faire à l'aide d'un vecteur qui sera créé à partir des valeurs de notre tableau. Veuillez noter que pour créer une belle forme audio miroir, nous devons «coller» les deux moitiés de notre visualisation.


 for (int i = 0; i < waveFormArray.Length - 1; i++) { //      Vector3 upLine = new Vector3(i * .01f, waveFormArray[i] * 10, 0); //      Vector3 downLine = new Vector3(i * .01f, -waveFormArray[i] * 10, 0); } 

Ensuite, utilisez simplement Debug.DrawLine pour dessiner nos vecteurs. N'importe quelle couleur peut choisir. Toutes ces méthodes doivent être appelées dans Update, nous mettrons donc à jour les informations à chaque image.


 Debug.DrawLine(upLine, downLine, Color.green); 

Si vous le souhaitez, vous pouvez ajouter un «curseur» qui montrera la position actuelle de la piste en cours de lecture. Ces informations peuvent être obtenues à partir du champ "AudioSource.timeSamples".


 private float debugLineWidth = 5; // ""  .       int currentPosition = (myAudio.timeSamples / quality) * 2; Vector3 drawVector = new Vector3(currentPosition * 0.01f, 0, 0); Debug.DrawLine(drawVector - Vector3.up * debugLineWidth, drawVector + Vector3.up * debugLineWidth, Color.white); 

Total, voici notre script:


 using UnityEngine; public class WaveFormDebug : MonoBehaviour { private readonly int quality = 100; private int sampleCount = 0; private int freq; private readonly float debugLineWidth = 5; private float[] waveFormArray; private float[] samples; private AudioSource myAudio; private void Start() { myAudio = gameObject.GetComponent<AudioSource>(); //  freq = myAudio.clip.frequency; sampleCount = freq / quality; //  samples = new float[myAudio.clip.samples * myAudio.clip.channels]; myAudio.clip.GetData(samples, 0); //       waveFormArray = new float[(samples.Length / sampleCount)]; for (int i = 0; i < waveFormArray.Length; i++) { waveFormArray[i] = 0; for (int j = 0; j < sampleCount; j++) { waveFormArray[i] += Mathf.Abs(samples[(i * sampleCount) + j]); } waveFormArray[i] /= sampleCount; } } private void Update() { for (int i = 0; i < waveFormArray.Length - 1; i++) { //      Vector3 upLine = new Vector3(i * 0.01f, waveFormArray[i] * 10, 0); //      Vector3 downLine = new Vector3(i * 0.01f, -waveFormArray[i] * 10, 0); // Debug  Debug.DrawLine(upLine, downLine, Color.green); } // ""  .       int currentPosition = (myAudio.timeSamples / quality) * 2; Vector3 drawVector = new Vector3(currentPosition * 0.01f, 0, 0); Debug.DrawLine(drawVector - Vector3.up * debugLineWidth, drawVector + Vector3.up * debugLineWidth, Color.white); } } 

Et voici le résultat:



Créez un paysage sonore fluide avec PolygonCollider2D


Avant de passer à cette section, je tiens à noter ce qui suit: bien sûr, conduire le long de la piste générée à partir de la musique est amusant, mais du point de vue du gameplay, il est pratiquement inutile. Et voici pourquoi:


  1. Pour que la piste soit passable, nous devons lisser nos données. Tous les pics disparaissent et vous arrêtez pratiquement de "sentir votre musique"
  2. Habituellement, les morceaux de musique sont fortement compressés et représentent une brique sonore, ce qui est mal adapté à un jeu 2D.
  3. La question non résolue de la vitesse de notre transport, qui devrait convenir à la vitesse de la piste. Je veux examiner cette question dans le prochain article.

Par conséquent, à titre expérimental, ce type de génération est assez drôle, mais il est difficile de créer une véritable fonctionnalité de gameplay sur cette base. En tout cas, nous continuons.


Nous devons donc créer PolygonCollider2D en utilisant nos données. C'est facile à faire. PolygonCollider2D possède un champ de points publics qui accepte Vector2 []. Tout d'abord, nous devons transférer nos points aux vecteurs du type souhaité. Faisons une fonction pour traduire le tableau de nos échantillons en un tableau vectoriel:


 private Vector2[] CreatePath(float[] src) { Vector2[] result = new Vector2[src.Length]; for (int i = 0; i < size; i++) { result[i] = new Vector2(i * 0.01f, Mathf.Abs(src[i] * lineScale)); } return result; } 

Après cela, passez simplement notre tableau de vecteurs résultant au collisionneur:


 path = CreatePath(waveFormArray); poly.points = path; 

Nous regardons le résultat. Voici le début de notre piste ... hmm ... ça n'a pas l'air très passable (ne pensez pas encore à la visualisation, les commentaires viendront plus tard).



Nous avons une forme audio trop nette, donc la piste sort bizarrement. Besoin de le lisser. Ici, nous utilisons l'algorithme de moyenne mobile. Vous pouvez en savoir plus sur Habr, dans l'article L'algorithme de moyenne mobile (Moyenne mobile simple) .


Dans Unity, l'algorithme est implémenté comme suit:


 private float[] MovingAverage(int frameSize, float[] data) { float sum = 0; float[] avgPoints = new float[data.Length - frameSize + 1]; for (int counter = 0; counter <= data.Length - frameSize; counter++) { int innerLoopCounter = 0; int index = counter; while (innerLoopCounter < frameSize) { sum = sum + data[index]; innerLoopCounter += 1; index += 1; } avgPoints[counter] = sum / frameSize; sum = 0; } return avgPoints; } 

Nous modifions notre création de chemin:


 float[] avgArray = MovingAverage(frameSize, waveFormArray); path = CreatePath(avgArray); poly.points = path; 

Vérification ...



Maintenant, notre piste semble tout à fait normale. J'ai utilisé une largeur de fenêtre de 10. Vous pouvez modifier ce paramètre pour choisir le lissage dont vous avez besoin.


Voici le script complet de cette section:


 using UnityEngine; public class WaveFormTest : MonoBehaviour { private const int frameSize = 10; public int size = 2048; public PolygonCollider2D poly; private readonly int lineScale = 5; private readonly int quality = 100; private int sampleCount = 0; private float[] waveFormArray; private float[] samples; private Vector2[] path; private AudioSource myAudio; private void Start() { myAudio = gameObject.GetComponent<AudioSource>(); int freq = myAudio.clip.frequency; sampleCount = freq / quality; samples = new float[myAudio.clip.samples * myAudio.clip.channels]; myAudio.clip.GetData(samples, 0); waveFormArray = new float[(samples.Length / sampleCount)]; for (int i = 0; i < waveFormArray.Length; i++) { waveFormArray[i] = 0; for (int j = 0; j < sampleCount; j++) { waveFormArray[i] += Mathf.Abs(samples[(i * sampleCount) + j]); } waveFormArray[i] /= sampleCount * 2; } //  ,    frameSize float[] avgArray = MovingAverage(frameSize, waveFormArray); path = CreatePath(avgArray); poly.points = path; } private Vector2[] CreatePath(float[] src) { Vector2[] result = new Vector2[src.Length]; for (int i = 0; i < size; i++) { result[i] = new Vector2(i * 0.01f, Mathf.Abs(src[i] * lineScale)); } return result; } private float[] MovingAverage(int frameSize, float[] data) { float sum = 0; float[] avgPoints = new float[data.Length - frameSize + 1]; for (int counter = 0; counter <= data.Length - frameSize; counter++) { int innerLoopCounter = 0; int index = counter; while (innerLoopCounter < frameSize) { sum = sum + data[index]; innerLoopCounter += 1; index += 1; } avgPoints[counter] = sum / frameSize; sum = 0; } return avgPoints; } } 

Comme je l'ai dit au début de la section, avec ce lissage, on arrête de sentir la piste, en plus, la vitesse de la machine n'est pas liée à la vitesse de la musique (BPM). Nous analyserons ce problème dans la prochaine partie de cette série d'articles. De plus, nous aborderons le sujet des spéciaux. effets sous le rythme. Au fait, j'ai pris une machine à écrire de cet actif gratuit .


Beaucoup d'entre vous, en regardant les captures d'écran, se demandent probablement comment j'ai dessiné le morceau lui-même? Après tout, les collisionneurs ne sont pas visibles.


J'ai utilisé la sagesse d'Internet et trouvé un moyen par lequel vous pouvez transformer un collisionneur de polygones en un maillage auquel vous pouvez affecter n'importe quel matériau, et le rendu de ligne fera un contour élégant. Cette méthode est décrite en détail ici . Triangulator vous pouvez prendre sur la communauté Unity .


Achèvement


Ce que nous avons appris dans cet article est un croquis de base pour les jeux musicaux. Oui, sous cette forme, il est, jusqu'à présent, un peu moche, mais vous pouvez dire en toute sécurité "Les gars, j'ai fait rouler la machine le long de la piste audio!". Pour en faire un vrai jeu, vous devez faire beaucoup d'efforts. Voici une liste de ce que nous pouvons faire ici:


  1. Liez la vitesse de la machine à la piste BPM. Le joueur ne peut contrôler que l'inclinaison de la voiture, mais pas la vitesse. Ensuite, la musique se sentira beaucoup plus forte pendant le cours.
  2. Faites un détecteur de bits et ajoutez des spéciaux. effets qui fonctionneront sous le rythme. De plus, vous pouvez ajouter une animation au corps de la voiture, qui rebondira sur le rythme d'un battement. Tout dépend de votre imagination.
  3. Au lieu de déplacer la moyenne, vous devez traiter la piste de manière plus compétente et obtenir un tableau de données afin que les pics ne disparaissent pas, mais il était facile de créer une trace.
  4. Et bien sûr, vous devez rendre le gameplay intéressant. Vous pouvez placer un monnayeur sur chaque coup, ajouter des zones de danger, etc.

Nous étudierons tout cela et bien plus dans les parties restantes de cette série d'articles. Merci à tous d'avoir lu!

Source: https://habr.com/ru/post/fr432134/


All Articles