Subsections

Solução Adotada

O projeto foi separado em dois blocos principais: um para Detecção de Frequências e Duração e um para a Geração de MIDI. Essa metodologia foi escolhida visando paralelizar o desenvolvimento do projeto, aumentando a produtividade, além de aumentar a reusabilidade do projeto. A separação em blocos ajudou muito nos testes durante o desenvolvimento, permitindo que eles pudessem ser testados individualmente.

O objetivo inicial do projeto era utilizar a entrada de áudio da placa para fazer captura direta da guitarra, passando pelo ADC5.1 e aplicando uma FFT5.2 sobre essas amostras, para então identificar a frequência e duração das notas tocadas e fazer uma tradução para o formato MIDI. Porém, a captura da placa usada no projeto não pode ser utilizada, e substituimos a entrada do programa para um arquivo de áudio puro, gravado num computador e passado para a placa.

Visão geral

A função principal do módulo de detecção de frequências segue a estrutura de um executivo cíclico. Os dados de entrada (áudio) são obtidos a partir de um arquivo e a cada ciclo são lidas FFT_SIZE amostras, onde FFT_SIZE é o tamanho do vetor que serve de entrada para a FFT.

Após a leitura é realizada a transformada e seu resultado armazenado em um buffer de números complexos, também com tamanho FFT_SIZE. Este resultado, por sua vez, serve commo entrada para o bloco de detecção da frequência fundamental.

Após a detecção da frequência, cabe à função is_event() determinar se o aúdio sendo analisado constitui uma nova nota (ou silêncio), ou é apenas a continuação de uma nota anteriormente detectada.

Todos os blocos funcionais serão detalhados adiante.

Detecção de Frequências e Duração

A função do primeiro grande bloco do projeto era pegar amostras da entrada de áudio, aplicar a FFTs nas amostras recolhidas, identificar a frequência e a duração das notas tocadas e gerar uma saída definida pela seguinte struct C. Tal saída é uma representação simbólica da nota:

            typedef struc note {
                pitch_t pitch;
                int time;
                bool on;
            } note_t;

A struct possui três campos inteiros (com pitch_t e bool sendo dois typedef para int), onde o primeiro, pitch, determina o valor no formato MIDI para a frquência detectada, o segundo, time, o instante no tempo em que a nota foi detectada e o terceiro, on, determina se o evento MIDI gerado será um NOTEON ou NOTEOFF.

Essa saída do primeiro bloco do projeto, uma stream de elementos do tipo note_t, era a entrada esperada pelo segundo bloco.

Conversão MIDI

A partir da saída do bloco anterior, a função do bloco de Conversão MIDI era criar um arquivo a partir das informações obtidas dos elementos note_t, assim como todo o cabeçalho e demais dados necessários ao formato MIDI. Por ter sido feito separadamente, foi mais fácil testar esse bloco, com entradas e saídas esperadas criadas especialmente para esse fim, verificando assim quaisquer possíveis erros na conversão.

FFT

A parte mais importante do trabalho foi certamente o uso da Transformada de Fourier[3] (FFT), para obter a distribuição (histograma) das frequência presentes no sinal de entrada. A FFT realiza uma conversão de domínio, transformando o sinal de entrada do domínio do tempo para o domínio das frequências. A figura[] ilustra esse processo.

Figura: Entrada da FFT, um sinal digital no domínio do tempo
Image fftin

Figura: Saída da FFT, sinal onde o eixo x representa a frequência
Image fftout

A implementação de FFT utilizada no projeto foi a presente na biblioteca de processamento digital de sinais do Blackfin, a libbfdsp[6]. Mais especificamente, a função utilizada foi a rfftrad4_fr16, uma FFT com entrada real e saída complexa. Abaixo está a assinatura da função:

        void rfftrad4_fr16(const fract16
            input[],
            complex_fract16 temp[],
            complex_fract16 output[],
            const complex_fract16 twiddle_table[],
            int twiddle_stride,
            int fft_size,
            int block_exponent,
            int scale_method);

Para análise das frequências foram utilizados apenas os valores absolutos da saída da transformada.

Detecção de Frequência Fundamental

O algoritmo de detecção da frequência fundamental recebe a saída da FFT, ou seja, um histograma contendo informações da intensidade relativa de cada um dos harmônicos presentes no sinal. Cabe a este bloco determinar qual é a frequência fundamental, que é a frequência percebida pelo ouvinte.

Inicialmente havíamos adotado um critério bastante simples para a determinação de tal frequência: selecionávamos a frequência com a maior intensidade. A partir de experimentos realizados com a captura da própria guitarra percebemos, porém, que nem sempre a frequência mais intensa era a fundamental. As figuras 5.3 e 5.4 demonstram, respectivamente, os casos onde a fundamental é mais intensa e onde isso não ocorre.

Figura: Espectro de frequência para G4
Image g4

Figura: Espectro de frequência para G#4
Image gs4

A partir da observação dos espectros das notas, percebemos que era suficiente obter os picos da função e, dentre estes picos, escolher o de menor frequência, que é a fundamental da nota.

            get_fundamental_index(frequency_spectrum) {
                peaks[FFT_PARTITIONS];
                
	            for(i = 0; i < FFT_PARTITIONS; i++) {
	                max_index = 0;
	                max = 0;
	                
	                for(j = 0; j < FFT_PARTITION_SIZE; j++) {
	                    index = i*FFT_PARTITION_SIZE + j;
	                    cur = abs(frequency_spectrum[index]);
	                    if(cur > max) {
	                        max = cur;
	                        max_index = index;
	                    }
	                }
	    
	                peaks[i].index = max_index;
	                peaks[i].val = max;
	            }
	         
	            sort(peaks, FFT_PARTITIONS, peaks.val);
	            sort(peaks, FIRST_HARMONICS, peaks.index);
	               
	        }

Detecção de Eventos

Como uma nota tocada pode ter uma duração maior que o tempo de execução de uma FFT, vimos que seria necessário algum tipo de detecção de eventos, capaz de dizer se a entrada da FFT atual caracterizava um evento novo, ou se era somente a continuação de uma nota anterior.

A idéia original para se resolver este problema, seria aplicar algum filtro no sinal de entrada, antes de se aplicar a FFT em si, mas acabamos por adotar uma solução mais simples. Basicamente comparamos a frequência da nota atual com a da anterior e, caso as notas detectadas sejam diferentes, um evento está caracterizado, seja por uma nova nota sendo tocada, ou por silêncio sendo reconhecido.

Este modelo criou um outro problema, pois após uma nota ser tocada no instrumento, a frequência obtida pelo programa varia muito, e se estabiliza após algum tempo. Por este motivo, logo após a detecção de um evento fazemos um descarte de algumas FFTs, até que a entrada esteja estável. O número mais adequado de FFTs a se descartar foi encontrado através de testes, variando a quantidade de descartes até encontrar um valor que obtivesse o melhor resultado.

João Paulo Pizani Flor 2010-07-12