O phase vocoder

O phase vocoder

No artigo sobre Modulação AM,, mencionamos que esta modulação pode ser usada para alterar o tom de um áudio, gerando distorções bem interessantes. Mas não serviria para fazer "autotune" (mudar o tom sem alterar a duração), pois a alteração de tom causada pela modulação AM é linear em todas as frequências.

Para fazer autotune, precisamos deslocar as frequências segundo uma proporção, o que significa que as harmônicas de um tom sofrem um deslocamento linear maior que a fundamental.

Uma forma simples de fazer isso é reproduzir um áudio mais rápido ou mais devagar, como fazíamos antigamente com LPs e fitas. Fazia-se isso até mesmo com filmes, para ajustar o framerate. Porém, este procedimento também muda a duração do áudio.

Para alterar o comprimento de um áudio sem modificar o tom, precisamos de um algoritmo mais sofisticado, o phase vocoder. Ele também abre a porta para implementar-se um autotune, pois podemos então a) alterar o comprimento do áudio e b) executá-lo mais rápido ou mais devagar para ajustar o tom.

O básico de um filtro FFT

Como dito neste artigo sobre transformada de Fourier, converte um sinal no domínio do tempo para o domínio da frequência ou vice-versa. Uma vez que o sinal esteja no domínio da frequência, podemos manipulá-lo como quisermos, e depois convertemos de volta para som audível.

Embora seja mais usual transformar um sinal de áudio em pequenos blocos, nada impede de transformar uma música inteira para o domínio da frequência, obtendo um único bloco FFT que representa a coisa toda.

Para tornar esta música mais comprida ou mais curta, bastaria então manipular esse bloco, aumentando ou diminuindo seu comprimento através de conversão de taxa de amostragem no domínio da frequência, e então fazendo FFT inverso para recuperar o áudio, com duração alterada.

O problema é que a manipulação de amostras FFT causa distorções de fase, que acabariam deixando o resultado final irreconhecível. (Aliás, este é um dos motivos por que não se implementa um filtro FFT desta forma.)

Para evitar esta desvantagem, processamos o áudio usando blocos FFT relativamente pequenos e.g. 2048 amostras num áudio de 44100 amostras por segundo.

Outra providência necessária para que o áudio processado por FFT fique bom, é sobrepor os blocos FFT, para evitar transições bruscas de fase. Cada bloco de áudio a ser processado recebe um prólogo e um epílogo de silêncio, é processado pelo FFT, e os resultados são combinados somando-se as partes sobrepostas.

Áudio
   |--------------------------------------|

 |ss--------ss|                   Blocos FFT com silêncio adicionado
         |ss--------ss|
                 |ss--------ss|
                         |ss--------ss|
                                 |ss------ssss|

 |ss------++++----++++----++++----++++----ssss|
Áudio reconstituído com os blocos FFT sobrepostos e somados

Um aprimoramento desta ideia é usar uma função de janela, em vez de simplesmente inserir silêncio nas pontas, A função de janela faz um fade-in e fade-out suave no áudio, nas pontas de cada bloco. Isto diminui a distorção nas partes sobrepostas.

Podemos fazer uma sobreposição mais agressiva, para melhorar ainda mais a resolução de fase:

Áudio
   |--------------------------------------|

 |ss--------ss|                  Blocos FFT com silêncio adicionado
     |ss--------ss|
         |ss--------ss|
             |ss--------ss|
                 |ss--------ss|
                     |ss--------ss|
                        |ss--------ss|
                            |ss--------ss|
                                |ss------ssss|

No exemplo acima, temos a sobreposição de até três blocos por amostra, o que significa que as amostras do áudio final podem ser o resultado da soma de até três blocos.

Para evitar que isso resulte em volume excessivo e oscilante, deve-se ajustar o volume do áudio final com base no número de blocos sobrepostos e na função de janela.

A ideia básica do phase vocoder

Vimos que é permitido sobrepor blocos FFT, tanto quanto quisermos. Em tese, com base nisso, podemos mudar facilmente o comprimento do áudio reconstituído, simplesmente "esticando" ou "encolhendo" a sobreposição dos blocos.

Áudio original
   |-------------------|

 |ss--------ss|                  Blocos FFT originais
     |ss--------ss|
         |ss--------ss|

 |ss--------ss|                  Sobreposição alterada
          |ss--------ss|
                   |ss--------ss|

   |--------------------------|
Áudio reconstituído e esticado

O principal problema desse método simplório é que ele cria variações bruscas de fase entre blocos, que se manifestam como cliques ou ruídos periódicos no áudio final.

Aqui entra o algoritmo phase vocoder. O que ele faz é acertar a fase de cada bloco, para que ele pareça ser uma continuação suave do bloco anterior. Com base no tamanho do bloco, e na fase observada no bloco anterior, detecta-se e corrige-se a diferença de fase, para que a sobreposição dos blocos seja um som contínuo, sem quebras.

O pulo do gato é que esse acerto de fase tem de ser feito em todas as frequências. Do ponto de vista de cada frequência, o comprimento de onda de cada bloco ou de cada amostra é diferente, então o desvio de fase de cada frequência (e a respectiva correção) também é diferente. Não adiantaria tentar ajustar apenas o offset do bloco, pois isto só acertaria a fase de uma única frequência.

O ajuste em todas as frequências é menos difícil do que parece. A representação FFT do sinal é no domínio da freqüência, usando números complexos. Alterar a fase simplesmente é alterar o ângulo de cada amostra, mantendo o valor absoluto. Usando bibliotecas como numpy, a tarefa fica ainda mais leve.

Implementação

Sem mais delongas, aqui está a implementação em Python. Ela usa apenas as bibliotecas numpy e scipy, esta última de forma econômica, pois a intenção do código é demonstrar o algoritmo, não ser o mais eficiente nem produzir o melhor áudio possível.

Julgue o resultado por você mesmo. Versão original:


(link to audio)

Versão esticada em 1.5x:


(link to audio)

Versão esticada em 0.75x (ou seja, reduzida em 25%):


(link to audio)

Para um script didático, considero os resultados bastante bons. De cara, o defeito mais visível é o som "borrado" dos instrumentos de percussão, o que é típico de FFT com blocos grandes demais. Diminuir o tamanho do bloco aparentemente melhora um pouco esse ponto. Fica a sugestão de tentar calibrar o script, usando diferentes tamanhos de bloco e diferentes superposições.

Se pedirmos para o script transformar um áudio com stretch 1.0x (ou seja, nulo) o áudio de saída é idêntico ao original, sem distorções. Portanto, a meu ver, isto mostra que a distorção é introduzida pela mudança de velocidade, não por algum bug crasso.

Analisando o script mais a fundo, pode-se encontrar a sua principal limitação. Ele simplesmente presume que a fase de cada frequência mantém-se fixa para sempre, do início ao fim da música. O código corrige a fase do próximo bloco baseando-se na fase do bloco anterior, o que significa que a fase observada no primeiro bloco influencia tudo que vem em seguida.

Essa lógica simplória funciona razoavelmente bem porque o ouvido humano não distingue fase. Só notamos que há algo errado com a fase quando o phase vocoder "borra" os sons impulsivos, como os de instrumentos de percussão.

Uma versão mais elaborada do algoritmo deve levar em conta questões perceptuais: acertar a fase apenas das frequências com maior amplitude (que serão mais provavelmente ouvidas), não acertar fase de frequências com amplitude tendendo a zero, procurar detectar e preservar a coerência de fase de sons de percussão.

Claude

Este artigo tem uma novidade grande: o script foi gerado por IA. Mais especificamente, pelo Claude da Anthropic.

Produzir um script phase vocoder viável é um teste que tenho feito com LLMs nos últimos 18 meses. Nas três tentativas anteriores, os LLMs falharam em produzir algo decente. Eles chegavam, no máximo,; ao algoritmo "simplório" mencionado antes, que apenas muda a sobreposição dos blocos FFT sem acertar a fase. Que era algo que eu já tinha conseguido fazer sozinho.

Desta vez, o Claude surpreendeu. Não só acertou de primeira, com base em apenas um parágrafo de instrução, como rodou o script por conta própria para testá-lo, gerando ele mesmo os dois exemplos de áudio listados mais acima. (Apenas mencionei o arquivo WAV original presente na pasta, não exigi explicitamente que o usasse.)

Também atendeu aos meus pedidos de usar apenas módulos genéricos, e produzir um código bem comentado, voltado para quem quer aprender como o phase vocoder funciona.

Por um lado é assustador quão rápido um LLM atual pode gerar código, e código de qualidade, perfeitamente inteligível. Dá o que pensar sobre o que se anda falando sobre o futuro da profissión.

Por outro lado, o Claude me ajudou a finalmente entender esse algoritmo. Há tempos eu tentava compreendê-lo a partir de outras fontes e não estava logrando sucesso.

É claro, ele pegou esse exemplo de algum lugar. Pedir ao Claude para produzir um phase vocoder de qualidade profissional seria outra conversa, porque quem as inventou, manteve-as proprietárias. As LLMs ainda não produzem algo assim do zero.

Gostamos de pensar que absolutamente toda produção de código é um trabalho intelectual, mas não é. 99% do nosso trabalho é reproduzir padronagens preexistentes. Atire a primeira pedra quem nunca colou um trechão de código do Stack Overflow. E isso, feliz ou infelizmente, está demonstrado que as LLMs já estão fazendo até melhor que nós.

Segundo round!

Na primeira sessão, o próprio Claude sugeriu adicionar algumas melhorias ao algoritmo básico. Retomamos pra ver o que ia dar.

Aqui está a implementação melhorada. Outro toque simpático do Claude foi tornar opcional o uso dessas melhorias, o que tem o efeito colateral de evidenciar as diferenças entre o algoritmo básico e o melhorado.

Versão esticada em 1.5x produzida pelo código melhorado:


(link to audio)

E a versão nova 0.75x:


(link to audio)

A mim, o áudio processado (que já não era ruim) ficou bem melhor.

As melhorias do algoritmo são as seguintes. 1) Quando aparece um bloco com muito mais energia que os anteriores, o phase vocoder desconsidera a fase anterior, e toma este bloco como a nova referência. 2) Quando um bloco possui um pico de energia, como um instrumento de percussão, a fase daquele ponto é utilizada como a nova referência.

No fundo, não é nada complicado, e soluciona o grande problema do algoritmo original, que é presumir que a fase de cada frequência é a mesma do início ao fim da música.