Categories
Code Demoscene Hacks Music

Síntesis musical para mí (o para torpes en general :) II

2) Senos!!!

Después del ruido del capitulo anterior, vamos a pasar a algo ligeramente más interesante: la onda senoidal. Como podéis ver si seguís el enlace a la pagina sobre la onda senoidal en la wikipedia, junto con dibujitos y explicaciones interesantes, la formula es:

y = A·sin(w·t + fase)

donde A es la amplitud de la onda (la distancia entre una cresta y un valle), w es la frecuencia en radianes/segundo y t es el tiempo. Vamos a ignorar la fase porque no nos hace falta :)

Como nos gusta trabajar con frecuencias en hercios, multiplicamos por 2·pi. Además, tenemos que t = samples generados / frecuencia de muestreo. Por lo tanto en el código tendremos algo como:

y[i] = A*sin(2*pi*frecuencia*(i/sampling_rate))

para el sample i-ésimo desde el inicio del sonido. Vamos a ver el trozo de código correspondiente (lo que falta es, básicamente, la inicialización del capítulo anterior):

void play(void *userdata, Uint8 *stream, int len)
{
    int num_samples = len / 2;
    Sint16 *dst_buf = (Sint16*) stream;
    for (int i=0; i<num_samples ; ++i)
        buffer[i] = sin(2.0*M_PI*440.0f*(i+pos)/(float)SAMPLING_RATE);
    
    // Clipping y conversion a Sint16
    for (int i=0; i<num_samples; ++i)
    {
        float v = buffer[i];
        if (v > 1.0f)
            v = 1.0f;
        else if (v< -1.0f)
            v = -1.0f;

        dst_buf[i] = (Sint16)(32767.0f*v);
    }
    pos += num_samples;

}

Detalles destacables de esta función:

  • se trabaja con un buffer de floats y se convierte a entero al final. No mola ir perdiendo precisión por el camino, sobre todo cuando hagamos cosas mas complejas
  • hay clipping a [-1.0, 1.0]. Ahora mismo es completamente innecesario porque sin() devuelve valores en ese intervalo, pero cuando empecemos a mezclar varios sonidos vendrá bien :)
  • en la llamada a sin(), el 440.0f es la frecuencia de la nota que suena (un La4), y pos es una variable global que guarda el í­ndice del ultimo sample generado entre llamadas a play(). Es feo, pero para un ejemplo pequeño va bien.

En el próximo episodio (ahora es tarde y tengo sueño), veremos otros osciladores típicos y cosas interesantes que se pueden hacer con ellos.

EDIT: herotyc me ha enviado una versión stand-alone del ejemplo de este capítulo. ¡Muchas gracias! :)

14 replies on “Síntesis musical para mí (o para torpes en general :) II”

Gracias por currarte un tutorial de este tipo, la verdad es que hace ya algun tiempo que me pica la curiosidad.
Queria hacer algun intento con series de fourier, pero no tenia claro como hacerlo… ahora haré intentos siguiendo tu tutorial :)

Saludos!

Hola y Gracias por el tutorial!
Con esto ya puedo empezar a hacer mi propio synth. :D no se si saldrá una patata o un melón, pero allá voy!
Una preguntilla:
Las frecuencias entre notas, no son proporcionales, es decir, enre un do y un re no hay la misma diferencia (en Hz) que entre un re y un mi
¿Donde está esa fórumla? ¿o hay que debemos guardar la escala en un array?
Saludos del n00b :D

Pensaba hablar de eso en breve, pero puedo hacer un pequeño adelanto :)

Las frecuencias son exponenciales. Si una nota la subes una octava (p.e.: de un do al siguiente do más agudo) tienes que duplicar la frecuencia. Teniendo en cuenta que el La4 tiene una frecuencia de 440 Hz, y que su número de nota en MIDI es el 69, podemos obtener la fórmula:

pitch = 440.0f*pow(2.0f,(nota-69.0f)/12.0f);

Como detalle tonto sin importancia, acabo de darme cuenta de que en mis intros hasta ahora estoy usando 45 como nota de referencia, asi que mis numeros de nota estan un par de octavas por debajo de los usados en MIDI X-)

¿He de suponer que donde has puesto 69 es en realidad un 49?
Lo pregunto por el link a la wikipedia que pusiste sobre el piano key frequencies, o ¿estas mirando otra escala?
eL_FRuTeRo está llegando!

Hablo de números de nota en MIDI, no de posiciones en un piano. Pero vamos, en realidad da lo mismo. El número que pongas en lugar de ese 69 será tu La4 a 440Hz, y luego tendras semitonos hacia arriba y hacia abajo :)

Muchas gracias. Ahora mismo lo subo y edito el post :)

Debería haberlo hecho yo mismo. De hecho, creo que llegué a compilarlo y comprobar que funcionaba y luego solo copié en el post el trozo relevante. A partir de ahora subiré fuentes completos aparte del código que comente en el post.

P.D.: No me he olvidado del tutorial, cuando tenga un rato escribiré la parte 3 :P

Ah, un par de notas sobre los includes ahora que reviso el código:

* math.h es de la libreria estandar, asi que #include <math.h> (sin comillas)
* poner #include "SDL/SDL.h" asume que las cabeceras van a estar colgando de un subdirectorio SDL/ de alguno de los directorios del path por defecto de busqueda de includes. La forma correcta de incluirlo (AFAIK) es no poner la ruta, y usar sdl-config o pkg-config para establecer las opciones de compilación adecuadas. Ejemplo:


slack@ommadawn:~$ sdl-config --cflags --libs
-I/usr/include/SDL -D_GNU_SOURCE=1 -D_REENTRANT -L/usr/lib -lSDL

De este modo el código sigue funcionando si, por ejemplo, el usuario tiene las SDL instaladas en su home de forma local. Puede compilarse exactamente igual en cualquier parte con algo tipo:

gcc -o ejemplo ejemplo.c `sdl-config --cflags --libs`

* Por último, ya que me pongo (y este ha sido fallo mío en el primer tutorial), falta un #include <stdio.h> para usar getchar() :D.

Bueno, el tema de las frecuencias asociado a las notas no es exactamente como lo pinta slack, aunque en los trackers por ejemplo se ha hecho así de toda la vida.

Esta asunción desemboca en que la música no suene realmente “como debería”, ya que se toma una especie de “sistema temperado deformado” como base para la afinación. Resumiendo, que un mi sostenido no es un fa, aunque sean “equivalentes” en cierta forma.

En sistemas de afinación más refinados (el pitagórico por ejemplo), las cosas son un poco más complicadas… Me pregunto cómo sonaría un sintetizador que tuviera en cuenta este tipo de detalles.

Os pongo aquí un video que ilustra bastante bien el rollazo que he soltado:

violinismo chupiguay

Slack, tienes razón en todo XD… por perrería compilaba solamente con “gcc blaeh.c -o blaeh -lSDL”.

Supongo que para hacer más serio el tutorial no estaría de más incluír el source de los ejemplos y un mini Makefile. Pero a mi me da igual XD, detalles.

Espero más tutoriales! :)

Ah, y nunca me pregunté qué diferencia había entre comillas y mayor-menor en los #includes. Y tampoco sé si hay diferencias en ello dependiendo de si el código es C o C++.

sergeeo: Eso se sale fuera del ámbito del tutorial (y de mis capacidades). Usar algo que no sea temperamento igual sería meterme en un berenjenal del que no saldría vivo XD. Cuando llegue a casa le echare un vistazo al vídeo :)

herotyc: la diferencia entre #include <blah> e #include "blah" es que la primera busca en los directorios estándar y la segunda busca primero en el directorio actual y si no encuentra blah va a parar a los estándar. No tiene por qué pasar nada si los intercambias, hasta el momento en el que tengas un conflicto de nombres con la libc.

Por otra parte, en C++ tienes #include "mi_cabecera.h" por un lado y #include <cabecera_estandar> (sin el .h) por el otro. Las cabeceras estandar de C pasan a llamarse cloquesea. Por ejemplo, #include <iostream> (libreria estandar de C++), o #include <cstdio> (el stdio.h de C de toda la vida). Para liar mas la cosa, se sigue pudiendo hacer los includes estilo C, y los compiladores te dejan incluir iostream.h y similares (gcc te dice que las formas acabadas en .h de librerias C++ están deprecated y dejaran de funcionar algun dia, pero se lo traga :P). Sí, C++ es un infierno.

Comments are closed.