Generación de entornos basados ​​en sonido y música en Unity3D. Parte 2. Crear una pista 2D a partir de música

Anotación


Hola a todos Hace relativamente poco tiempo, escribí un artículo Generando un entorno basado en sonido y música en Unity3D , en el que di varios ejemplos de juegos que utilizan la mecánica de generar contenido basado en música, y también hablé sobre los aspectos básicos de dichos juegos. Prácticamente no había código en el artículo y prometí que habría una secuela. Y aquí está, frente a ti. Esta vez intentaremos crear una pista para una carrera en 2D, al estilo de Hill Climb, a partir de tu música. Veamos que tenemos ...



Introduccion


Les recuerdo que esta serie de artículos está diseñada para desarrolladores principiantes y para aquellos que recientemente comenzaron a trabajar con sonido. Si está haciendo una rápida transformación de Fourier en su mente, entonces probablemente se aburrirá.


Aquí está nuestra hoja de ruta para hoy:


  1. Considere qué es la discretización.
  2. Descubra qué datos podemos obtener de Audio Clip Unity
  3. Comprenda cómo podemos trabajar con estos datos.
  4. Descubra lo que podemos generar a partir de estos datos.
  5. Aprende a hacer un juego con todo esto (bueno, o algo similar a un juego)

¡Entonces vamos!


Discretización de cingalés analógico


Como mucha gente sabe, para usar una señal en sistemas digitales, necesitamos convertirla. Uno de los pasos de conversión es el muestreo de señal, en el que la señal analógica se divide en partes (informes temporales), después de lo cual a cada informe se le asigna el valor de amplitud que estaba en el momento seleccionado.


La letra T indica el período de muestreo. Cuanto más corto sea el período, más precisa será la conversión de la señal. Pero la mayoría de las veces hablan de la inversa: frecuencia de muestreo (es lógico que sea F = 1 / T). 8,000 Hz es suficiente para una señal telefónica y, por ejemplo, una de las opciones para el formato DVD-Audio requiere una frecuencia de muestreo de 192,000 Hz. El estándar en grabación digital (en editores de juegos, editores de música) es 44 100 Hz: esta es la frecuencia de CD de audio.


Los valores numéricos de la amplitud se almacenan en las llamadas muestras y con ellos trabajaremos. El valor de la muestra es flotante y puede ser de -1 a 1. Simplificado, se ve así.



Representación de ondas sonoras (estática)


Información básica


La forma de onda (o forma de audio, y en la gente común: "pez") es una representación visual de la señal de sonido a lo largo del tiempo. La forma de onda puede mostrarnos en qué punto del sonido ocurre la fase activa y dónde ocurre la atenuación. A menudo, la forma de onda se presenta para cada canal por separado, por ejemplo, así:



Imagine que ya tenemos un AudioSource y un script en el que trabajamos. Veamos qué nos puede dar la Unidad.


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

Seleccionar número de informes


Antes de continuar, necesitamos hablar un poco sobre la profundidad de renderizado de nuestro sonido. Con una frecuencia de muestreo de 44100 Hz por segundo, podemos procesar 44100 informes. Digamos que necesitamos renderizar una pista de 10 segundos de duración. Dibujaremos cada informe con una línea en un píxel de ancho. Resulta que nuestra forma de onda tendrá 441,000 píxeles de largo. Obtienes una onda de sonido muy larga, alargada y poco comprendida. ¡Pero, en él puedes ver cada informe específico! Y cargará terriblemente el sistema, sin importar cómo lo dibuje.



Si no crea un software de audio profesional, no necesita tal precisión. Para una imagen de audio general, podemos dividir todas las muestras en períodos más largos y tomar, por ejemplo, el promedio de cada 100 muestras. Entonces nuestra ola tendrá una forma muy distinta:



Por supuesto, esto no es del todo exacto, ya que puede omitir los picos de volumen que pueda necesitar, por lo que puede probar no el valor promedio, sino el máximo de este segmento. Esto dará una imagen ligeramente diferente, pero sus picos no desaparecerán.


Preparando para recibir audio


Definamos la precisión de nuestra muestra como calidad y el número final de informes como sampleCount.


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

Un ejemplo de cálculo de todos los números estará debajo.


Luego, necesitamos obtener las muestras nosotros mismos. Esto se puede hacer desde un clip de audio utilizando el método GetData .


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

Este método toma una matriz en la que escribe muestras. offsetSamples: parámetro responsable del punto de inicio de la lectura de la matriz de datos. Si lees la matriz desde el principio, entonces debería haber cero.


Para registrar muestras, necesitamos preparar una matriz para ellas. Por ejemplo, así:


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

¿Por qué multiplicamos la longitud por el número de canales? Ahora te diré ...


Información del canal de audio de Unity


Mucha gente sabe que en el sonido usualmente usamos dos canales: izquierdo y derecho. Alguien sabe que hay sistemas 2.1, así como 5.1, 7.1 en los que las fuentes de sonido rodean desde todos los lados. El tema de los canales está bien descrito en la wiki . ¿Cómo funciona esto en Unity?


Al descargar un archivo, al abrir un clip, puede encontrar la siguiente imagen:



Aquí se muestra que tenemos dos canales, e incluso puedes notar que son diferentes entre sí. Unity graba muestras de estos canales uno tras otro. Resulta esta imagen:
[L1,R1,L2,R2,L3,R3,L4,R4,L5,R5,L6,R6,L7,R7,L8,R8...]


Es por eso que necesitamos el doble de espacio en la matriz que solo para la cantidad de muestras.


Si selecciona la opción de clip Forzar a mono, el canal será uno y todo el sonido estará en el centro. La vista previa de su ola cambiará de inmediato.




Recibir datos de audio


Esto es lo que obtenemos:


 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; } } 

Total, si la pista dura 10 segundos y tiene dos canales, obtenemos lo siguiente:


  • El número de muestras en el clip (myAudio.clip.sample) = 44100 * 10 = 441000
  • La matriz de muestras para dos canales es larga (samples.Length) = 441000 * 2 = 882000
  • Número de informes (sampleCount) = 44100/100 = 441
  • La longitud de la matriz final = samples.Length / sampleCount = 2000

Como resultado, trabajaremos con 2000 puntos, que es suficiente para dibujar la ola. Ahora debe incluir la imaginación y pensar en cómo podemos usar estos datos.


Representación de información de audio


Cree una pista de audio simple con las herramientas de depuración


Como mucha gente sabe, Unity tiene medios convenientes para mostrar todo tipo de información de depuración. Un desarrollador inteligente basado en estas herramientas puede hacer, por ejemplo, extensiones muy potentes para el editor. Nuestro caso muestra un uso muy atípico de los métodos de depuración.


Para dibujar, necesitamos una línea. Podemos hacerlo con la ayuda de un vector que se creará a partir de los valores de nuestra matriz. Tenga en cuenta que para hacer una hermosa forma de audio espejo, necesitamos "pegar" las dos mitades de nuestra visualización.


 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); } 

Luego, solo usa Debug.DrawLine para dibujar nuestros vectores. Cualquier color puede elegir. Todos estos métodos deben llamarse en Actualización, por lo que actualizaremos la información en cada cuadro.


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

Si lo desea, puede agregar un "control deslizante" que mostrará la posición actual de la pista que se está reproduciendo. Esta información se puede obtener del campo "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, aquí está nuestro 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); } } 

Y aquí está el resultado:



Cree un paisaje sonoro suave con PolygonCollider2D


Antes de continuar con esta sección, quiero señalar lo siguiente: por supuesto, conducir por la pista generada por la música es divertido, pero desde el punto de vista del juego es prácticamente inútil. Y aquí está el por qué:


  1. Para que la pista sea transitable, necesitamos suavizar nuestros datos. Todos los picos desaparecen y prácticamente dejas de "sentir tu música"
  2. Por lo general, las pistas de música están muy comprimidas y representan un ladrillo de sonido, que no es adecuado para un juego en 2D.
  3. El problema no resuelto de la velocidad de nuestro transporte, que debería ser adecuado para la velocidad de la pista. Quiero considerar este problema en el próximo artículo.

Por lo tanto, como experimento, este tipo de generación es bastante divertido, pero es difícil crear una función de juego real basada en él. En cualquier caso, continuamos.


Entonces, necesitamos hacer PolygonCollider2D usando nuestros datos. Esto es fácil de hacer. PolygonCollider2D tiene un campo de puntos públicos que acepta Vector2 []. Primero, necesitamos transferir nuestros puntos a los vectores del tipo deseado. Hagamos una función para traducir la matriz de nuestras muestras en una matriz de vectores:


 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; } 

Después de eso, simplemente pase nuestra matriz de vectores resultante al colisionador:


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

Nos fijamos en el resultado. Aquí está el comienzo de nuestra pista ... hmm ... no parece muy aceptable (no pienses en la visualización todavía, los comentarios vendrán más tarde).



Tenemos una forma de audio demasiado nítida, por lo que la pista resulta extraña. Necesito suavizarlo. Aquí usamos el algoritmo de promedio móvil. Puede leer más al respecto en Habr, en el artículo El algoritmo de la media móvil (media móvil simple) .


En Unity, el algoritmo se implementa de la siguiente manera:


 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; } 

Modificamos nuestra creación de ruta:


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

Comprobando ...



Ahora nuestra pista se ve bastante normal. Utilicé un ancho de ventana de 10. Puede modificar este parámetro para elegir el suavizado que necesita.


Aquí está la secuencia de comandos completa para esta sección:


 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; } } 

Como dije al comienzo de la sección, con este suavizado, dejamos de sentir la pista, además, la velocidad de la máquina no está vinculada a la velocidad de la música (BPM). Analizaremos este problema en la siguiente parte de esta serie de artículos. Además, allí tocaremos el tema de los especiales. efectos bajo el ritmo. Por cierto, tomé una máquina de escribir de este activo gratuito .


Probablemente muchos de ustedes, mirando las capturas de pantalla, se preguntaron cómo dibujé la pista en sí. Después de todo, los colisionadores no son visibles.


Utilicé la sabiduría de Internet y encontré una manera por la cual puedes convertir un colisionador de polígonos en una malla a la que puedes asignar cualquier material, y el renderizador de líneas hará un contorno elegante. Este método se describe en detalle aquí . Triangulator puedes enfrentarte a Unity Community .


Finalización


Lo que aprendimos en este artículo es un boceto básico para juegos musicales. Sí, de esta forma es, hasta ahora, un poco feo, pero puedes decir con seguridad "¡Chicos, hice que la máquina siguiera la pista de audio!". Para que este sea un juego real, debes hacer un gran esfuerzo. Aquí hay una lista de lo que podemos hacer aquí:


  1. Ate la velocidad de la máquina a la pista BPM. El jugador solo puede controlar la inclinación del automóvil, pero no la velocidad. Entonces la música se sentirá mucho más fuerte durante el curso.
  2. Haga un detector de bits y agregue especiales. efectos que funcionarán bajo el ritmo. Además, puede agregar animación a la carrocería del automóvil, que rebotará al ritmo de un latido. Todo depende de tu imaginación.
  3. En lugar de la media móvil, debe procesar la pista de manera más competente y obtener una matriz de datos para que los picos no desaparezcan, pero fue fácil crear una traza.
  4. Bueno, y, por supuesto, debes hacer que el juego sea interesante. Puedes colocar una moneda en cada golpe, agregar zonas de peligro, etc.

Estudiaremos todo esto y mucho más en las partes restantes de esta serie de artículos. Gracias a todos por leer!

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


All Articles