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, e mesmo com filmes. Porém a duração do áudio também é modificada.
Para alterar o comprimento de um áudio sem modificar o tom, precisamos de um algoritmo mais sofisticado, o phase vocoder. Este algoritmo 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.
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, adicionando ou removendo amostras para deixá-lo no tamanho desejado, e então fazendo FFT inverso para obter o áudio novo.
O problema é que tanto a inserção quanto a remoçã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 crescendo/decrescendo suave no áudio. Isto diminui a distorção nas partes sobrepostas,
Como visto na figurinha acima, temos uma sobreposição de blocos FFT. Em tese, podemos mudar o comprimento do áudio reconstituído, simplesmente aumentando ou diminuindo a sobreposição entre blocos, "esticando" ou "encolhendo" o resultado.
Naturalmente, o tamanho e a sobreposição dos blocos tem de ser suficiente para atingir o resultado desejado, nem encavalando, nem deixando falhas. A função de janela também deve ser dimensionada para que o volume do áudio final permaneça constante.
Mas o principal problema desse método simplório é que ele possui 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, antes de recombiná-lo. 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 pulos.
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 a correção também tem de ser diferente.
Claro, quando se fala "todas as frequências", são apenas a paleta de frequencias representadas pelo FFT. Então, não são infinitas; se o bloco FFT tem 2048 amostras, são 2048 frequências. Usando bibliotecas como o numpy do Python, que suporta vetores e matrizes, o código lida com todas elas de uma vez só.
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:
Versão esticada em 1.5x:
Versão esticada em 0.75x (ou seja, reduzida em 25%):
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. 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.
Uma versão mais elaborada do algoritmo deve levar em conta questões perceptuais: acertar a fase apenas das frequências com mais amplitude (que serão mais provavelmente ouvidas), não acertar fase de frequências com amplitude tendendo a zero, procurar detectar e preservar a localidade de impulsos como os de percussão.
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. Talvez o Claude sofresse mais para produzir um phase vocoder de melhor qualidade, porque as melhorias no algoritmo que propiciariam essa qualidade são mais obscuras ou mesmo 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. E isso, feliz ou infelizmente, está demonstrado que as LLMs já estão fazendo até melhor que nós.