BandLimitedPulse es un tipo de generador de señal, similar a los generadores de onda cuadrada, pulsos o dientes de sierra, pero con la diferencia de que su espectro consta de una cantidad exacta de armónicos, todos con la misma amplitud, que se puede especificar al instanciarlo. Matemáticamente, BandLimitedPulse usa lo que se conoce como la “fórmula cerrada” de una Serie de Fourier Truncada, de modo que su consumo de cómputo equivale al de tres sinusoides y una división, cualquiera sea el número de parciales elegido (ver F. Richard Moore “Elements of Computer Music” Prentice-Hall, 1990, pág. 271).
Ejemplo #11 – 2 BandLimitedPulse + 1 PulseWave
BandLimitedPulse blp1 = new BandLimitedPulse (261.63, .3, 10, sr); // 10 = número de parciales PulseWave pulse = new PulseWave (.1, .5, sr); blp1.setAmplitudeModulator (pulse); ADSR env1 = new ADSR (20, 200, .1, 100, sr, gate); pulse.setFrequencyModulator (env1); ADSR env2 = new ADSR (5, 400, .85, 400, sr, gate); blp1.setEnvelope (env2); BandLimitedPulse blp2 = new BandLimitedPulse (98.12, .7, 5, sr); // 5 parciales SignalClone blp11 = new SignalClone (blp1); blp2.setFrequencyModulator (blp1, Oscillator.RELATIVE); blp2.setAmplitudeModulator (blp11); ADSR env3 = new ADSR (5, 800, .85, 200, sr, gate); blp2.setEnvelope (env3); SineWave lfo = new SineWave (7.7, .77, sr); pulse.setPWM (lfo); out1.addSignal (blp2, Oscillator.RELATIVE);
La clase IIRFilter proporciona el modelo para casi todos los filtros que vienen con Minim. Estos filtros están implementados de forma recursiva, lo que los hace bastante eficientes. Introduciendo una pequeña modificación en la clase IIRFilter, habilitamos la posibilidad de variar los parámetros de un filtro en función de una señal de control, tal como lo hicimos antes con Oscillator.
Los métodos agregados son setFrequencyEnvelope, setFrequencyModulator, setBandWidthEnvelope, y setBandWidthModulator. Todos ellos permiten usar cualquier tipo señales (cualquier objeto de una clase que implemente AudioSignal) para variar la frecuencia de corte, en el caso de los dos primeros, y el ancho de banda del filtro, en el caso de los dos restantes (siempre que se trate de un filtro pasabanda).
El funcionamiento de estos métodos tiene cierta similitud con el de sus pares de la clase Oscillator, pero también algunas diferencias con ellos. Igual que en el caso de la modulación en amplitud, la diferencia entre “envelope” y “modulator” aquí, es que “envelope” está preparada para señales de tipo ADSR, que son siempre positivas, y “modulator” en cambio introduce un “DC offset” para señales oscilatorias. Sin embargo, a diferencia de lo que ocurre en Oscillator, no se pueden usar ambos métodos a la vez, es decir, hay una única señal de control para cada parámetro, y es la misma en el caso de “setFrequencyEnvelope” y “setFrequencyModulator“, por ejeplo.
Otra diferencia con el funcionamiento de los moduladores en Oscillator, es que aquí el valor “central” del parámetro del propio objeto -es decir, la propia frecuencia del filtro o su propio ancho de banda- es ignorado, y el rango de acción de las señales de control se especifica con los parámetros “bottom” y “top“, en Hz, que corresponden a la frecuencia mínima y máxima, respectivamente, de la variación. (Nota: los valores de bottom y top pueden intercambiarse, produciendo el mismo efecto que invertir la envolvente; en señales periódicas no se notará la diferencia)
Veamos algunos ejemplos:
Ejemplo #12 – parecido al ejemplo #9 pero con pasabanda modulado
Adder adder = new Adder(armonico); // el mismo sintetizador aditivo del ejemplo #8 adder.setFreq (130.82); // seteamos la frecuencia fundamental a 130.82Hz (1/2 fc) WhiteNoise wn = new WhiteNoise (.16); // definimos un ruido blanco adder.addSignal (wn); // lo agregamos al Adder BandPass bp = new BandPass (2000, 200, sr); // definimos un filtro pasabanda; adder.addEffect (bp); // agregamos el pasabanda al Adder adder.setAmp (2.5/adder.amplitude()); // normalizamos la amplitud del Adder a 2.5 ADSR env1 = new ADSR (8, 150, .7, 300, sr, gate); // definimos las envolventes 1, 2 y 3 ADSR env2 = new ADSR (1100, 350, .6, 200, sr, gate); ADSR env3 = new ADSR (110, 1450, .1, 200, sr, gate); BandLimitedPulse crr = new BandLimitedPulse (261.63, .8, 2, sr); // definimos la portadora, compuesta de 2 sinusoides crr.setEnvelope (env1); // asignamos la envolvente 1 a la portadora crr.setFrequencyModulator (adder, Oscillator.RELATIVE); // agregamos el Adder a la portadora como modulador de frecuencia relativo bp.setFrequencyEnvelope (env2, 100, 5000); // asignamos la envolvente 2 a la frecuecia del pasabanda; bottom=100Hz, top=5KHz bp.setBandWidthEnvelope (env3, 700, 60); // asignamos la envolvente 3 al ancho de banda del pasabanda; bottom=700Hz, top=60Hz out1.addSignal (crr, Oscillator.RELATIVE); // agregamos la portadora a la salida, etc.
Ejemplo #13 – Sintetizador Sustractivo Simple
ADSR env1 = new ADSR (5, 800, .5, 100, sr, gate); // envolvente 1 (de amplitud) SignalClone env11 = new SignalClone (env1); // clonamos envolvente 1 SawWave saw = new SawWave (261.63, .6, sr); // nueva 'dientes de sierra' SquareWave sqr = new SquareWave (130.82, .4, sr); // nueva onda cuadrada saw.setEnvelope (env1); // agregamos la envolvente 1 a la 'dientes de sierra' saw.setPan (1); // paneamos la 'dientes de sierra' sqr.setEnvelope (env11); // agregamos el clon de la envolvente 1 a la onda cuadrada sqr.setPan (-1); // paneamos la onda cuadrada ADSR env2 = new ADSR (400, 400, .2, 300, sr, gate); // envolvente 2 (de frecuencia) LowPassFS lp = new LowPassFS (1000, sr); // nuevo filtro pasabajos lp.setFrequencyEnvelope (env2, 100, 4500); // asignamos la envolvente 2 a la frecuencia de corte del pasabajos; bottom=100Hz, top=4.5KHz out1.addSignal (saw, Oscillator.RELATIVE); // agregamos la 'dientes de sierra' a la salida out1.addSignal (sqr, Oscillator.RELATIVE); // agregamos la onda cuadrada a la salida out1.addEffect (lp); // agregamos el pasabajos a la salida
No podía faltar el filtro clásico de los sintetizadores analógicos, el pasabajos “resonante”, con su correspondiente método setResonanceEnvelope (en este caso no existe “setResonanceModulator”, aunque de todas maneras se puede usar cualquier señal en lugar de una envolvente). Veámoslo en acción:
Ejemplo #14 – Sinte sustractivo con generador de pulsos modulado y filtro resonante
// pulse1 contiene el mismo generador de señal del ejemplo #6 ADSR env4 = new ADSR (5, 800, .5, 100, sr, gate); // envolvente 4 (de amplitud) SignalClone env14 = new SignalClone (env4); // clonamos envolvente 4 SawWave saw = new SawWave (261.63, .95, sr); // nueva 'dientes de sierra' PulseWave pulse2 = new PulseWave (65.41, .95, sr); // nuevo generador de pulsos saw.setEnvelope (env4); // agregamos la envolvente 4 a la 'dientes de sierra' saw.setPan (.8); // paneamos la 'dientes de sierra' pulse2.setPan (-.8); // paneamos los pulsos pulse2.setEnvelope (env14); // agregamos el clon de la envolvente 4 a los pulsos ADSR env5 = new ADSR (1180, 400, .5, 300, sr, gate); // envolvente 5 (de resonancia) SineWave lfo2 = new SineWave (4, .5, sr); // nuevo lfo ADSR env6 = new ADSR (2700, 10, 1, 300, sr, gate); // envolvente 6 lfo2.setEnvelope (env6); // asignamos la envolvente 6 al lfo LowPassResonant lp = new LowPassResonant (1000, .7, sr); // nuevo filtro pasabajos resonante (resonancia=7) lp.setFrequencyModulator (lfo2, 80, 5500); // asignamos el lfo a la frecuencia de corte del pasabajos lp.setResonanceEnvelope (env5); // asignamos la envolvente 5 a la resonancia del filtro out1.addSignal (saw, Oscillator.RELATIVE); // agregamos la 'dientes de sierra' a la salida out1.addSignal (pulse2, Oscillator.RELATIVE); // agregamos los pulsos la salida out1.addSignal (pulse1, Oscillator.RELATIVE); // agregamos la onda del ejemplo #6 la salida out1.addEffect (lp); // agregamos el filtro resonante a la salida
La técnica del waveshaping consiste en procesar la señal con algún tipo de función no lineal, a efectos de obtener espectros complejos, a partir incluso de una simple sinusoide. Parte del interés que esto despierta, proviene de que la complejidad del espectro generado varía según la amplitud de la señal de entrada, lo que guarda cierta semejanza con lo que ocurre en instrumentos acústicos. El waveshaping es, ni más ni menos que, una distorsión.
En el caso de nuestro WaveShaper, la función elegida son los polinomios de chebyshev, calculados recursivamente, cuyo orden se puede especificar al instanciar el efecto. Escuchémoslo:
Ejemplo #15 – Sintetizador FM con filtro resonante y WaveShaper
ADSR env1 = new ADSR (940, 360, .35, 120, sr, gate); ADSR env2 = new ADSR (240, 280, .25, 60, sr, gate); ADSR env3 = new ADSR (5, 280, .15, 900, sr, gate); SineWave modulator1 = new SineWave (261.63*2.5, .1, sr); SineWave modulator2 = new SineWave (261.63*3.5, .2, sr); modulator2.setEnvelope (env1); modulator1.setEnvelope (env2); Adder modulator3 = new Adder(); modulator3.addSignal (modulator1, Oscillator.RELATIVE); modulator3.addSignal (modulator2, Oscillator.RELATIVE); WhiteNoise wn = new WhiteNoise (.001); SignalClone wn1 = new SignalClone (wn); modulator3.addSignal (wn1); SineWave sine = new SineWave(261.63, 0.23, sr); SignalClone env12 = new SignalClone (env2); sine.setEnvelope (env12); sine.setFrequencyModulator (modulator3, Oscillator.RELATIVE); BandLimitedPulse blp = new BandLimitedPulse (261.63/4, 1.3, 10, sr); blp.setEnvelope (env3); sine.setPan(.4); blp.setPan(-.2); ADSR adsr4 = new ADSR (10, 1500, .08, 100, sr, gate); LowPassResonant lpr = new LowPassResonant (500, .33, sr); lpr.setFrequencyEnvelope (adsr4, 100, 8000); WaveShaper ws = new WaveShaper (7, 1.1); // waveshaper de orden 7, amplitud 1.1 (el orden tiene que ser impar) out1.addSignal (sine, Adder.RELATIVE); out1.addSignal (blp, Adder.RELATIVE); out1.addSignal (wn); out1.addEffect (lpr); out1.addEffect (ws); // agregamos el waveshaper a la salida
Una cosa que le faltaba a Minim, era una manera sencilla de insertar la señal de entrada en la cadena del audio de salida. Al hacer que AudioInput implemente AudioSignal, básicamente, logramos que el audio de entrada pueda no sólo escucharse, sino también insertarse como señal de control en todos los puntos antes vistos. AudioInput ya contaba con la posibilidad de agregar efectos, lo que sumado a su nueva funcionalidad, nos abre un interesante abanico de posibilidades.
Para los siguientes ejemplos necesitaremos tener un micrófono conectado a la tarjeta de sonido, y ruteado como señal de “record” desde el mixer de Windows (o su equivalente en OSX o Linux). Lamentablemente, por un tema de seguridad de Java, no es posible disponer del audio de entrada en un applet de internet; por esta razón, los ejemplos 16 y 17 están para bajar como archivos .zip. Los usuarios Windows pueden sencillamente correr el .exe que viene adentro; los usuarios Mac y Linux tienen que correr la clase “ejemplo_xx” dentro del .jar del mismo nombre.
Ejemplo #16 – Audio de entrada con filtro pasabanda modulado y WaveShaper
AudioInput in = minim.getLineIn (Minim.MONO, 1024, sr, bd); SignalClone in1 = new SignalClone (in); WhiteNoise wn = new WhiteNoise (.01); BandPass bp = new BandPass (500, 300, sr); SineWave sine = new SineWave (261.63, .001, sr); SineWave lfo = new SineWave (.9, 1, sr); SignalClone lfo1 = new SignalClone (lfo); bp.setFrequencyModulator (lfo1, 300, 3000); bp.setBandWidthModulator (lfo1, 100, 1000); WaveShaper ws = new WaveShaper (5, .45); out1.addSignal (sine, Adder.RELATIVE); out1.addSignal (lfo, Adder.RELATIVE); out1.addSignal (wn); out1.addSignal (in); out1.addEffect (bp); out1.addEffect (ws);
Y hablando de procesar el audio de entrada y usarlo como señal de control de un sintetizador, nada mejor que extraer su nivel RMS. Esto es lo que hace EnvelopeDetect, con su único parámetro “decay” que determina la cantidad de muestras a promediar.
Veamos lo que puede conseguirse gracias a este efecto:
Ejemplo #17 – Audio de entrada con EnvelopeDetect, controlando diversas cosas
AudioInput in = minim.getLineIn(Minim.MONO, 512, sr, bd); // definimos una entrada de audio EnvelopeDetect ed = new EnvelopeDetect (1500); // definimos un detector de envolvente; decay=1500 muestras in.addEffect (ed); // agregamos el detector de envolvente a la entrada SignalClone in1 = new SignalClone (in); // clonamos dicha señal SineWave s = new SineWave (10, 0, sr); SawWave saw = new SawWave (130.82, 300, sr); saw.setFrequencyModulator (in); saw.setEnvelope (in1); Adder adder0 = new Adder (); adder0.addSignal (s, Adder.RELATIVE); adder0.addSignal (saw, Adder.RELATIVE); BandPass bp1 = new BandPass (2000, 200, sr); bp1.setFrequencyEnvelope (in1, 80, 8000); adder0.addEffect (bp1); Adder adder1 = new Adder (); adder1.addSignal (adder0, Adder.RELATIVE); adder1.addSignal (in1); BandLimitedPulse crr = new BandLimitedPulse (261.63, .8, 2, sr); crr.setEnvelope (in1); crr.setFrequencyModulator (adder1, Oscillator.RELATIVE); out1.addSignal (crr, Oscillator.RELATIVE);
La gran virtud de Minim es el ser lo suficientemente simple, clara (me hace acordar a cierta empresa de telefonía celular) abierta y bien documentada, como para “entrarle” sin tener que estudiar la biblioteca durante meses.
No obstante, incluso con las modificaciones introducidas, el desempeño es limitado, si se lo compara con el de algunos de los productos que mencioné al principio (Reaktor, Max-MSP, SuperCollider, Csound, etc.), los cuales, hay que decirlo también, cuentan en su mayoría con más de 10 años de desarrollo.
Aun así, Minim sigue siendo un excelente punto de partida para seguir trabajando, con miras a disponer algun día, en el entorno Processing, de una herramienta tan poderosa como lo son aquellos productos. Enumero a continuación las funciones que me gustaría agregar en el futuro, haciendo un gran paréntesis con respecto a los temas de fondo: “calidad” de los algoritmos, consumo de recursos, estabilidad, latencia, soportes ASIO y VST, etc., etc.
Para descargar el código fuente de Minim 2.02 PE, ir a la zona de descargas, siguiendo este link.
P. G. 18-01-2010