Este sería el diagrama de bloques de esta segunda iteración:
Parser MIDI mejorado
En la versión iniciar el parser MIDI no se tuvieron en cuenta algunas características "raras" que se dan el algunos teclados controladores y al mismo tiempo se asumía que un "note off" posterior a un "note on" siempre era de la misma tecla, lo cual es demasiado suponer, sobre todo cuando quien toca es un humano. Cuando un humano toca una secuencia de notas en un teclado (por ejemplo: La, Mi, Do) uno puede pensar que los mensaje que manda el teclado controlador son los siguientes:
noteOn(La), noteOff(La), noteOn(Mi), noteOff(Mi), noteOn(Do), noteOff(Do)
Sin embargo lo cierto es que a veces un humano pulsa la siguiente tecla al mismo tiempo o antes de soltar la anterior:
noteOn(La), noteOn(Mi), noteOff(La), noteOff(Mi), noteOn(Do), noteOff(Do)
Con la anterior versión del parser, que asumía que un noteOff se correspondía siempre con el noteOn inmediatamente anterior, lo que ocurría era que cuando al sinte le llegaba el noteOff(La) callaba la nota Mi disparada justo antes porque asumía que ese noteOff se correspondía con dicha nota Mi. En la nueva versión del parse este noteOff(Mi) es ignorado por la máquina de estados por lo que la respuesta del sintetizador es más natural.
Para mejorar el comportamiento y la funcionalidad del parser MIDI se ha optado por un diseño basado en máquinas de estado en serie y en paralelo en lugar de una única máquina de estados grande. El parser MIDI se ha divido en dos etapas (Stage1 y Stage2), la primera etapa genera señales "KeyOn" y "KeyOff" limpias por cables separados y además implementa en paralelo una máquina de estados aparte para procesar los mensajes de "Control Change". En la segunda etapa se implementa la lógica anteriormente descrita de ignorar los "Note Off" que no se corresponden con el mensaje "Note On" inmediatamente anterior.
De esta forma, aunque aparentemente se ha complicado el diseño, se han separado los problemas y es más sencillo introducir modificaciones y depurar errores en las máquinas de estado. Cada una por separado es más sencilla y fácil de trazar que una hipotética máquina de estados única para todo.
Además de la mejora en el procesado de los mensajes "Note On" y "Note Off", este parser ya reconoce mensajes de tipo "Control Change", en concreto para tres valores prefijados de controlador: 71, 74 y 16, que se asignarán en el sintetizador a la frecuencia de corte del filtro, la resonancia del filtro y la ganancia de entrada del filtro.
Filtro paso bajo de segundo orden
Se ha optado por la implementación estándar de un filtro de estado variable (state variable filter). Se trata de un filtro de segundo orden (dos polos) que genera simultáneamente 3 salidas:
- paso bajo (con pendiente de filtrado de 12 dB/octava)
- paso alto (con pendiente de filtrado de 12 dB/octava)
- paso banda (con pendiente de filtrado de 6 dB/octava)
No son grandes pendientes de filtrado pero siempre se pueden mejorar poniendo varios filtros en cascada. La implementación que se ha utilizado es la descrita en el libro "Musical Applications of Microprocessors" de Hal Chamberlin (dicha implementación ya fue usada sobre un microcontrolador en este post). El filtro de estado variable viene determinado por el siguiente sistema de ecuaciones en diferencias finitas:
$$pasoAlto[n] = entrada - ({r \times pasoBanda[n-1]}) - pasoBajo[n]$$
$$pasoBanda[n] = ({f \times pasoAlto[n]}) + pasoBanda[n - 1]$$
$$pasoBajo[n] = ({f \times pasoBanda[n - 1]}) + pasoBajo[n - 1]$$
Siendo:
$$f = 2\sin\left({\pi F_c \over F_s}\right)$$
$$r = {1 \over Q}$$
Siendo $F_c$ la frecuencia de corte del filtro, $F_s$ la frecuencia de muestreo y $Q$ la Q del filtro (la resonancia).
Si se reordenan las ecuaciones en diferencias:
$$pasoBajo[n] = ({f \times pasoBanda[n - 1]}) + pasoBajo[n - 1]$$
$$pasoAlto[n] = entrada - ({r \times pasoBanda[n - 1]}) - pasoBajo[n]$$
$$pasoBanda[n] = ({f \times pasoAlto[n]}) + pasoBanda[n - 1]$$
Podemos olvidarnos de los índices:
pasoBajo += f * pasoBanda
pasoAlto = entrada - (r * pasoBanda) - pasoBajo
pasoBanda += f * pasoAlto
Como se puede apreciar es preciso mantener en memoria (registro) al menos las variables pasoBajo y pasoBanda entre que se procesa una muestra y la siguiente (se trata de un filtro digital de segundo orden).
Para implementar dicho filtro sobre FPGA lo que necesitaremos serán básicamente los siguientes elementos:
- Al menos tres registros en los que almacenaremos los valores "pasoBajo", "pasoBanda" y "pasoAlto" (aunque realmente podríamos no gastar un registro para "pasoAlto", lo vamos a incluir para poder disponer de esa salida en el módulo).
- Una unidad de suma con multiplicación: Un módulo combinacional que realiza la operación: A = (B * C) + D (en muchos casos D = A, por lo que se puede ver como A += B * C)
- Una máquina de estados para controlar qué operandos y operaciones se hacen en cada momento.
Con estos elemento y teniendo en cuenta las ecuaciones anteriores, podemos hacer una propuesta de secuenciación de operaciones como sigue:
1. LP := (cutoff * BP) + LP
2. HP := (0 * x ) + IN
3. HP := (-reso * BP) + HP
4. HP := (-1 * LP) + HP
5. BP := (cutoff * HP) + BP
Cada paso requiere un único ciclo de reloj por lo que bastará con implementar una máquina de estados que, por cada muestra que llegue, pase por los 5 estados de forma secuencial para que los registros LP, BP y HP (LowPass, BandPass y HighPass) tengan los valores de salida del filtro que necesitamos. Nótese que será preciso utilizar aritmética de punto fijo y en nuestro caso se ha optado por un formato Q16.16 (16 bits de parte entera y 16 bits de parte fraccionaria).
A continuación puede verse como quedaría la implementación del filtro en VHDL:
library ieee; use ieee.std_logic_1164.all; use ieee.numeric_std.all; entity StateVariableFilter is port ( Reset : in std_logic; Clk : in std_logic; EnableIn : in std_logic; SampleIn : in std_logic_vector(15 downto 0); CutOffIn : in std_logic_vector(31 downto 0); -- 0..1 fixed point Q16.16 ResonanceIn : in std_logic_vector(31 downto 0); -- 0..1 fixed point Q16.16 SampleOut : out std_logic_vector(15 downto 0); EnableOut : out std_logic ); end entity; architecture RTL of StateVariableFilter is signal LPDBus : std_logic_vector(31 downto 0); signal LPQBus : std_logic_vector(31 downto 0); signal HPDBus : std_logic_vector(31 downto 0); signal HPQBus : std_logic_vector(31 downto 0); signal BPDBus : std_logic_vector(31 downto 0); signal BPQBus : std_logic_vector(31 downto 0); signal MultOperandA : std_logic_vector(31 downto 0); signal MultOperandB : std_logic_vector(31 downto 0); signal MultResult64 : std_logic_vector(63 downto 0); signal MultResult : std_logic_vector(31 downto 0); signal AddOperandB : std_logic_vector(31 downto 0); signal AddResult : std_logic_vector(31 downto 0); signal NegResonance : std_logic_vector(31 downto 0); signal FSMDBus : std_logic_vector(2 downto 0); signal FSMQBus : std_logic_vector(2 downto 0); begin process (Clk) begin if (Clk'event and (Clk = '1')) then LPQBus <= LPDBus; end if; end process; process (Clk) begin if (Clk'event and (Clk = '1')) then HPQBus <= HPDBus; end if; end process; process (Clk) begin if (Clk'event and (Clk = '1')) then BPQBus <= BPDBus; end if; end process; process (Clk) begin if (Clk'event and (Clk = '1')) then FSMQBus <= FSMDBus; end if; end process; NegResonance <= std_logic_vector(to_signed(-to_integer(signed(ResonanceIn)), 32)); MultOperandA <= CutOffIn when ((FSMQBus = "001") or (FSMQBus = "101")) else NegResonance when (FSMQBus = "011") else std_logic_vector(to_signed(-65536, 32)) when (FSMQBus = "100") else -- -65536 es -1 en notación Q16.16 std_logic_vector(to_signed(0, 32)); MultOperandB <= LPQBus when (FSMQBus = "100") else BPQBus when ((FSMQBus = "001") or (FSMQBus = "011")) else HPQBus; AddOperandB <= LPQBus when (FSMQBus = "001") else BPQBus when (FSMQBus = "101") else HPQBus when ((FSMQBus = "011") or (FSMQBus = "100")) else std_logic_vector(to_signed(to_integer(signed(SampleIn)), 32)); --MultResult64 <= std_logic_vector(to_signed(to_integer(signed(MultOperandA)) * to_integer(signed(MultOperandB)), 64)); MultResult64 <= std_logic_vector(signed(MultOperandA) * signed(MultOperandB)); MultResult <= MultResult64(47 downto 16); --AddResult <= std_logic_vector(to_signed(to_integer(signed(MultResult)) + to_integer(signed(AddOperandB)), 32)); AddResult <= std_logic_vector(signed(MultResult) + signed(AddOperandB)); LPDBus <= std_logic_vector(to_signed(0, 32)) when (Reset = '1') else AddResult when (FSMQBus = "001") else LPQBus; HPDBus <= std_logic_vector(to_signed(0, 32)) when (Reset = '1') else AddResult when ((FSMQBus = "011") or (FSMQBus = "100") or (FSMQBus = "010")) else HPQBus; BPDBus <= std_logic_vector(to_signed(0, 32)) when (Reset = '1') else AddResult when (FSMQBus = "101") else BPQBus; -- fsm -- LP += cutoff * BP -- HP = in - (resonance * BP) - LP -- BP += cutoff * HP FSMDBus <= "000" when ((Reset = '1') or (FSMQBus = "110")) else -- MultOperandA MultOperandB AddOperandB "001" when ((FSMQBus = "000") and (EnableIn = '1')) else -- LP := cutoff * BP + LP "010" when (FSMQBus = "001") else -- HP := 0 * x + IN "011" when (FSMQBus = "010") else -- HP := -reso * BP + HP "100" when (FSMQBus = "011") else -- HP := -1 * LP + HP "101" when (FSMQBus = "100") else -- BP := cutoff * HP + BP "110" when (FSMQBus = "101") else "000"; EnableOut <= '1' when (FSMQBus = "110") else '0'; SampleOut <= std_logic_vector(to_signed(-32768, 16)) when (to_integer(signed(LPQBus)) < -32768) else std_logic_vector(to_signed(32767, 16)) when (to_integer(signed(LPQBus)) > 32767) else LPQBus(15 downto 0); end architecture;
La máquina de estados espera hasta que la entrada "EnableIn" se ponga a "1", dicho evento es la señal que indica al filtro que debe realizar una iteración (i.e. calcular la siguiente muestra a partir de la entrada "SampleIn").
Todo el código está disponible en la sección soft.
[ 1 comentario ] ( 1333 visualizaciones ) | [ 0 trackbacks ] | enlace permanente | ( 3 / 2809 )