Leitura e gravação de arquivos WAV em Python – Real Python

Há uma abundância de ferramentas e bibliotecas de terceiros para manipular e analisar arquivos WAV de áudio em Python. Ao mesmo tempo, a linguagem vem com o pouco conhecido wave em sua biblioteca padrão, que oferece uma maneira rápida e direta de ler e gravar esses arquivos. Conhecer o módulo wave do Python pode ajudá-lo a mergulhar no processamento de áudio digital.

Se tópicos como análise de áudio, edição de som ou síntese de música o entusiasmam, então o senhor está prestes a ter um gostinho deles!

Embora não seja obrigatório, o senhor aproveitará ao máximo este tutorial se estiver familiarizado com NumPy e Matplotlibque simplificam muito o trabalho com dados de áudio. Além disso, saber sobre o matrizes numéricas em Python o ajudará a entender melhor a representação de dados subjacente na memória do computador.

Clique no link abaixo para acessar os materiais de bônus, onde encontrará arquivos de áudio de amostra para praticar, bem como o código-fonte completo de todos os exemplos demonstrados neste tutorial:

O senhor também pode responder ao questionário para testar seu conhecimento e ver o quanto aprendeu:

Entenda o formato de arquivo WAV

No início dos anos 90, a Microsoft e a IBM desenvolveram em conjunto o formato de arquivo Formato de arquivo de áudio de forma de onda, frequentemente abreviado como WAVE ou WAV, que deriva da extensão do arquivo (.wav). Apesar de sua idade avançada em termos de informática, o formato continua relevante até hoje. Há vários bons motivos para sua ampla adoção, incluindo:

  • Simplicidade: O formato de arquivo WAV tem uma estrutura simples, o que o torna relativamente simples de decodificar em software e de ser entendido por humanos.
  • Portabilidade: Muitos sistemas de software e plataformas de hardware suportam o formato de arquivo WAV como padrão, tornando-o adequado para a troca de dados.
  • Alta Fidelidade: Como a maioria dos arquivos WAV contém dados de áudio brutos e não compactados, eles são perfeitos para aplicativos que exigem a mais alta qualidade de som possível, como produção musical ou edição de áudio. Por outro lado, os arquivos WAV ocupam um espaço de armazenamento significativo em comparação com compressão com perdas como o MP3.

Vale a pena observar que os arquivos WAV são tipos especializados do formato Formato de arquivo de intercâmbio de recursos (RIFF), que é um formato de contêiner para fluxos de áudio e vídeo. Outros formatos de arquivo populares baseados no RIFF incluem AVI e MIDI. O próprio RIFF é uma extensão de um sistema ainda mais antigo, o IFF formato originalmente desenvolvido pela Electronic Arts para armazenar recursos de videogame.

Antes de começar, o senhor desconstruirá o próprio formato de arquivo WAV para entender melhor sua estrutura e como ele representa os sons. Sinta-se à vontade para pular adiante se quiser apenas ver como usar o wave módulo em Python.

A parte da forma de onda do WAV

O que o senhor percebe como som é um distúrbio de pressão que viaja por um meio físico, como o ar ou a água. No nível mais fundamental, todo som é um onda que o senhor pode descrever usando três atributos:

  1. Amplitude é a medida da força da onda sonora, que o senhor percebe como intensidade.
  2. Frequência é o recíproco do comprimento de onda ou o número de oscilações por segundo, que corresponde ao passo.
  3. Fase é o ponto do ciclo da onda em que ela começa, não registrado diretamente pelo ouvido humano.

A palavra forma de onda, que aparece no nome do formato de arquivo WAV, refere-se à representação gráfica da forma do sinal de áudio. Se o senhor já abriu um arquivo de som usando um software de edição de áudio, como o Audacityo senhor provavelmente já viu uma visualização do conteúdo do arquivo semelhante a esta:

Forma de onda no Audacity
Forma de onda no Audacity

Essa é a forma de onda do áudio, ilustrando como a amplitude muda com o tempo.

O eixo vertical representa o amplitude em um determinado momento. O ponto médio do gráfico, que é uma linha horizontal que passa pelo centro, representa a amplitude da linha de base ou o ponto de silêncio. Qualquer desvio desse equilíbrio corresponde a uma amplitude positiva ou negativa maior, que o senhor sente como um som mais alto.

À medida que o senhor se move da esquerda para a direita ao longo da escala horizontal do gráfico, que é a linha do tempoo senhor está basicamente avançando no tempo por meio da faixa de áudio.

Essa visualização pode ajudá-lo a inspecionar visualmente as características do seu arquivo de áudio. A série de picos e vales da amplitude reflete as alterações de volume. Portanto, é possível aproveitar a forma de onda para identificar partes em que ocorrem determinados sons ou encontrar seções silenciosas que talvez precisem de edição.

A seguir, o senhor aprenderá como os arquivos WAV armazenam esses níveis de amplitude em formato digital.

A estrutura de um arquivo WAV

O formato de arquivo de áudio WAV é um formato binário que exibe a seguinte estrutura no disco:

A estrutura de um arquivo WAV
A estrutura de um arquivo WAV

Como o senhor pode ver, um arquivo WAV começa com um cabeçalho composto de metadados, que descreve como interpretar a sequência de quadros de áudio que se seguem. Cada quadro consiste em canais que correspondem a alto-falantes, como esquerdo e direito ou dianteiro e traseiro. Por sua vez, cada canal contém um amostra de áudio, que é um valor numérico que representa o nível de amplitude em um determinado momento.

Os parâmetros mais importantes que o senhor encontrará em um cabeçalho WAV são:

  • Encoding (Codificação): A representação digital de um amostrado sinal de áudio. Os tipos de codificação disponíveis incluem linear não compactado Modulação por código de pulso (PCM) e alguns formatos compactados como ADPCM, Direitoou μ-Law.
  • Canais: O número de canais em cada quadro, que geralmente é igual a um para mono e dois para estéreo mas poderia ser mais para o som surround gravações.
  • Taxa de quadros: O número de quadros por segundo, também conhecido como taxa de amostragem ou frequência de amostragem, medido em hertz. Isso afeta a faixa de frequências representáveis, o que tem um impacto na qualidade sonora percebida.
  • Profundidade de bits: O número de bits por amostra de áudio, que determina o número máximo de níveis de amplitude distintos. Quanto maior a profundidade de bits, maior a alcance dinâmico do sinal codificado, tornando audíveis as nuances sutis do som.

O Python’s wave suporta apenas o módulo Modulação por código de pulso (PCM) que é, de longe, a mais popular, o que significa que o senhor geralmente pode ignorar outros formatos. Além disso, o Python está limitado a tipos de dados inteiros, enquanto o PCM não para por aí, definindo várias profundidades de bits para escolher, incluindo ponto flutuante :

Tipo de dados Assinado Bits Valor mínimo Valor máximo
Inteiro Não 8 0 255
Número inteiro Sim 16 -32,768 32,767
Inteiro Sim 24 -8,388,608 8,388,607
Inteiro Sim 32 -2,147,483,648 2,147,483,647
Ponto flutuante Sim 32 ≈ -3.40282 × 1038 ≈ 3.40282 × 1038
Ponto flutuante Sim 64 ≈ -1.79769 × 10308 ≈ 1.79769 × 10308

Na prática, os tipos de dados de ponto flutuante são um exagero para a maioria dos usos, pois exigem mais armazenamento e oferecem pouco retorno sobre o investimento. O senhor provavelmente não sentirá falta deles, a menos que precise de um intervalo dinâmico extra para edição de som realmente profissional.

A codificação de número inteiro de 8 bits é a única que depende do números sem sinal. Em contrapartida, todos os demais tipos de dados permitem valores de amostra positivos e negativos.

Outro detalhe importante que o senhor deve levar em conta ao ler amostras de áudio é o ordem dos bytes. O formato de arquivo WAV especifica que os valores de vários bytes são armazenados em little-endian ou começar com o byte menos significativo primeiro. Felizmente, o senhor normalmente não precisa se preocupar com isso quando usa o wave para ler ou gravar dados de áudio em Python. Ainda assim, pode haver casos extremos em que o senhor precise!

Os números inteiros de 8 bits, 16 bits e 32 bits têm representações padrão no Linguagem de programação C, que é o padrão CPython do CPython. No entanto, o inteiro de 24 bits é uma exceção sem um tipo de dados em C. Profundidades de bits exóticas como essa não são inéditas na produção musical, pois ajudam a encontrar um equilíbrio entre tamanho e qualidade. Mais tarde, o senhor aprenderá a emulá-los em Python.

Para representar fielmente a música, a maioria dos arquivos WAV usa codificação PCM estéreo com Inteiros assinados de 16 bits amostrado em 44,1 kHz ou 44.100 quadros por segundo. Esses parâmetros correspondem ao padrão Áudio com qualidade de CD. Coincidentemente, essa frequência de amostragem é aproximadamente o dobro da frequência mais alta que a maioria dos seres humanos pode ouvir. De acordo com o Teorema de amostragem de Nyquist-Shannon, que é suficiente para capturar sons em formato digital sem distorção.

Agora que o senhor sabe o que há dentro de um arquivo WAV, é hora de carregá-lo no Python!

Conheça os recursos do Python wave Módulo

O wave cuida da leitura e da gravação de arquivos WAV, mas, fora isso, é bastante minimalista. Ele foi implementado em cerca de quinhentas linhas de código Python puro, sem contar os comentários. Talvez o mais importante seja que o senhor não pode usá-lo para reprodução de áudio. Para reproduzir um som em Pythono senhor precisará instalar uma biblioteca separada.

Como mencionado anteriormente, o wave é compatível apenas com quatro módulos baseados em números inteiros e não compactados codificação PCM profundidades de bits:

  • Inteiro sem sinal de 8 bits
  • Inteiro assinado de 16 bits
  • Inteiro com sinal de 24 bits
  • Inteiro com sinal de 32 bits

Para fazer experimentos com a função wave do Python, o senhor pode fazer o download dos materiais de suporte, que incluem alguns arquivos WAV de amostra codificados nesses formatos.

Ler metadados e quadros de áudio WAV

Caso ainda não tenha obtido os materiais de bônus, o senhor pode fazer o download de um amostra da gravação de um Tambor de bongô diretamente do Wikimedia Commons para começar. Trata-se de um áudio mono com amostragem de 44,1 kHz e codificado no formato PCM de 16 bits. O arquivo está no formato domínio públicoportanto, o senhor pode usá-lo livremente para fins pessoais ou comerciais sem nenhuma restrição.

Para carregar esse arquivo no Python, importar o wave e chamar seu módulo open() com uma função string indicando o caminho para seu arquivo WAV como argumento da função:

Quando o senhor não passa nenhum argumento adicional, a função wave.open() que é a única função que faz parte do módulo do interface públicaabre o arquivo fornecido para leitura e retorna um Wave_read . O senhor pode usar esse objeto para recuperar as informações armazenadas no cabeçalho do arquivo WAV e ler os quadros de áudio codificados:

O senhor pode convenientemente obter todos os parâmetros do seu arquivo WAV em um arquivo tupla nomeada com atributos descritivos como .nchannels ou .framerate. Como alternativa, o senhor pode chamar os métodos individuais, como .getnchannels(), no Wave_read para selecionar as partes específicas de metadados que interessam ao senhor.

O objeto quadros de áudio são expostos ao senhor como uma imagem não processada bytes não processada, que é uma sequência muito longa de byte sem sinal valores. Infelizmente, não é possível fazer muito além do que o senhor viu aqui porque o wave simplesmente retorna os bytes brutos sem fornecer nenhuma ajuda na interpretação deles.

Sua gravação de amostra do tambor de bongô, que tem menos de cinco segundos de duração e usa apenas um canal, compreende quase meio milhão de bytes! Para entendê-los, o senhor precisa conhecer o formato de codificação e decodificar manualmente esses bytes em números inteiros.

De acordo com os metadados que o senhor acabou de obter, há apenas um canal em cada quadro e cada amostra de áudio ocupa dois bytes ou dezesseis bits. Portanto, o senhor pode concluir que seu arquivo foi codificado usando o formato PCM com números inteiros assinados de 16 bits. Essa representação corresponderia ao formato signed short em C, que não existe no Python.

Embora o Python não ofereça suporte direto ao tipo de dados necessário, o senhor pode usar a função array para declarar uma matriz de signed short e passar seu bytes como entrada. Nesse caso, o senhor deseja usar a letra minúscula "h" como código de tipo da matriz para informar ao Python como interpretar os bytes dos quadros:

Observe que a matriz resultante tem exatamente a metade do número de elementos que a sequência original de bytes. Isso ocorre porque cada número no array array representa uma amostra de áudio de 16 bits ou dois bytes.

Outra opção ao seu alcance é o struct que permite descompactar uma sequência de bytes em um arquivo tupla de números de acordo com o especificado pelo string de formato:

O símbolo de menor que (<) na string de formato indica explicitamente little-endian como a ordem de byte de cada amostra de áudio de dois bytes (h). Por outro lado, um array assume implicitamente a ordem de bytes da sua plataforma, o que significa que talvez seja necessário chamar seu .byteswap() quando necessário.

Por fim, o senhor pode usar o NumPy como uma alternativa eficiente e poderosa aos módulos da biblioteca padrão do Python, especialmente se o senhor já trabalha com dados numéricos:

Ao usar uma matriz NumPy para armazenar suas amostras de PCM, o senhor pode aproveitar sua função de elemento operações vetorizadas para normalizar a amplitude do sinal de áudio codificado. A maior magnitude de uma amplitude armazenada em um signed short é 32.768 negativo ou -215. Dividindo cada amostra por 215 os dimensiona para um intervalo aberto à direita entre -1,0 e 1,0, o que é conveniente para tarefas de processamento de áudio.

Isso o leva a um ponto em que finalmente poderá começar a executar tarefas interessantes nos dados de áudio em Python, como plotar a forma de onda ou aplicar efeitos especiais. Antes disso, porém, o senhor deve aprender a salvar seu trabalho em um arquivo WAV.

Escreva seu primeiro arquivo WAV em Python

Saber como usar o wave em Python abre possibilidades interessantes, como síntese de som. Não seria ótimo compor sua própria música ou efeitos sonoros do zero e ouvi-los? Agora, o senhor pode fazer exatamente isso!

Matematicamente, o senhor pode representar qualquer som complexo como a soma de um número suficiente de ondas senoidais de diferentes frequências, amplitudes e fases. Ao misturá-las nas proporções corretas, o senhor pode recriar o único timbre de diferentes instrumentos musicais tocando a mesma nota. Posteriormente, o senhor poderá combinar alguns notas musicais em acordes e usá-los para formar interessantes melodias.

Aqui está a fórmula geral para calcular a amplitude A

Equação da onda sonora

Quando o senhor dimensiona suas amplitudes para uma faixa de valores entre -1,0 e 1,0, pode desconsiderar o A na equação. O senhor também pode omitir o fator φ porque a mudança de fase geralmente é irrelevante para suas aplicações.

Vá em frente e crie um script Python nomeado synth_mono.py com a seguinte função auxiliar, que implementa a fórmula acima:

Primeiro, o senhor importa a função math do Python para acessar o módulo sin() e, em seguida, especificar uma função constante com a taxa de quadros do áudio, cujo padrão é 44,1 kHz. A função auxiliar, sound_wave()usa como parâmetros a frequência em hertz e a duração em segundos da onda esperada. Com base neles, ela calcula as amostras PCM inteiras sem sinal de 8 bits da onda senoidal correspondente e produz para o chamador.

Para determinar o instante de tempo antes de inseri-lo na fórmula, o senhor divide o número do quadro atual pela taxa de quadros, o que lhe dá o tempo atual em segundos. Em seguida, calcula a amplitude nesse ponto usando a fórmula matemática simplificada que o senhor viu anteriormente. Por fim, o senhor desloca, dimensiona e recorta a amplitude para ajustá-la ao intervalo de 0 a 255, correspondente a uma amostra de áudio PCM inteira sem sinal de 8 bits.

Agora o senhor pode sintetizar, por exemplo, um som puro do nota musical A gerando uma onda senoidal com uma frequência de 440 Hz com duração de dois segundos e meio. Em seguida, o senhor pode usar o wave para salvar as amostras de áudio PCM resultantes em um arquivo WAV:

O senhor começa adicionando o módulo import necessário e chamando o comando wave.open() com a função mode igual ao literal de string "wb"que representa a gravação no modo binário. Nesse caso, a função retorna um Wave_write . Observe que o Python sempre abre seus arquivos WAV em modo binário mesmo que o senhor não use explicitamente a letra b no mode no valor do parâmetro.

Em seguida, o senhor define o número de canais como um, o que representa áudio monoe a largura da amostra para um byte ou oito bits, o que corresponde à codificação PCM com Inteiro sem sinal de 8 bits samples. O senhor também passa sua taxa de quadros armazenada na constante. Por fim, o senhor converte as amostras de áudio PCM computadas em uma sequência de bytes antes de gravá-las no arquivo.

Agora que o senhor entendeu o que o código faz, pode prosseguir e executar o script:

Parabéns! O senhor sintetizou com sucesso seu primeiro som em Python. Tente reproduzi-lo em um reprodutor de mídia para ouvir o resultado. Em seguida, o senhor vai apimentar um pouco o som.

Mixar e salvar áudio estéreo

Produzir áudio mono é um ótimo ponto de partida. No entanto, salvar um sinal estéreo no formato de arquivo WAV fica mais complicado porque o senhor tem que intercalar as amostras de áudio dos canais esquerdo e direito em cada frame. Para fazer isso, o senhor pode modificar o código existente conforme mostrado abaixo ou colocá-lo em um novo arquivo Python, por exemplo, um chamado synth_stereo.py:

As linhas destacadas representam as alterações necessárias. Primeiro, o senhor importa o itertools para que o senhor possa chain() o compactado e desempacotado pares de amostras de áudio de ambos os canais no linha 15. Observe que essa é uma das muitas maneiras de implementar a intercalação de canais. Enquanto o canal esquerdo permanece como antes, o direito se torna outra onda sonora com uma frequência ligeiramente diferente. Ambas têm a mesma duração de dois segundos e meio.

O senhor também atualiza o número de canais nos metadados do arquivo e converte os quadros estéreo em bytes brutos. Quando reproduzido, o arquivo de áudio gerado deve se assemelhar ao som de um toque de celular usado pela maioria dos telefones na América do Norte. O senhor pode simular outros tons de vários países ajustando as duas frequências de som, 440 Hz e 480 Hz. Para usar frequências adicionais, o senhor pode precisar de mais de dois canais.

Como alternativa, em vez de alocar canais separados para as ondas sonoras, o senhor pode misturá-las para criar efeitos interessantes. Por exemplo, duas ondas sonoras com frequências muito próximas produzem um batida padrão de interferência. O senhor já deve ter experimentado esse fenômeno em primeira mão ao viajar de avião, pois os motores a jato de ambos os lados nunca giram exatamente na mesma velocidade. Isso cria um som pulsante característico na cabine.

Mistura de duas ondas sonoras se resume a somar suas amplitudes. Apenas certifique-se de fixar a soma depois, de modo que ela não exceda a faixa de amplitude disponível para evitar distorção:

Aqui, o senhor volta ao som mono por um momento. Depois de gerar duas ondas sonoras no linhas 9 e 10o senhor adiciona suas amplitudes correspondentes na linha seguinte. Às vezes, eles se cancelam mutuamente e, em outras ocasiões, amplificam o som geral. A função integrada min() e o max() ajudam o senhor a manter a amplitude resultante entre -1,0 e 1,0 o tempo todo.

Brinque com o script acima, aumentando ou diminuindo a distância entre as duas frequências para observar como isso afeta o ritmo de batida resultante.

Codificar com maior profundidade de bits

Até agora, o senhor tem representado cada amostra de áudio com um único byte ou oito bits para manter as coisas simples. Isso lhe proporcionou 256 níveis de amplitude distintos, o que foi suficientemente decente para suas necessidades. Entretanto, mais cedo ou mais tarde, o senhor desejará aumentar a profundidade de bits para obter uma faixa dinâmica muito maior e melhor qualidade de som. No entanto, isso tem um custo, e não se trata apenas de memória adicional.

Para usar um dos codificações PCM de vários bytes, como a de 16 bits, o senhor precisará considerar a conversão entre uma codificação Python int e uma representação de byte adequada. Isso envolve lidar com a ordem dos bytes e a bit de sinal, em particular. O Python oferece algumas ferramentas para ajudar com isso, que o senhor vai explorar agora:

  • array
  • bytearray e int.to_bytes()
  • numpy.ndarray

Ao mudar para uma profundidade de bits maior, o senhor deve ajustar o dimensionamento e a conversão de bytes de acordo. Por exemplo, para usar o inteiros assinados de 16 bits, o senhor pode usar o array e fazer os seguintes ajustes em seu synth_stereo.py script:

Aqui está uma análise rápida das peças mais importantes:

  • Linha 11 dimensiona e fixa a amplitude calculada no intervalo de números inteiros assinados de 16 bits, que se estende de -32.768 a 32.767. Observe a assimetria dos valores extremos.
  • Linhas 16 a 19 definem uma matriz de números inteiros curtos assinados e a preenchem com as amostras intercaladas de ambos os canais em um loop.
  • Linha 23 define a largura da amostra para dois bytes, o que corresponde à codificação PCM de 16 bits.
  • Linha 25 converte a matriz em uma sequência de bytes e os grava no arquivo.

Lembre-se de que a função array do Python se baseia no sistema ordem nativa de bytes. Para ter um controle mais granular sobre esses detalhes técnicos, o senhor pode usar uma solução alternativa baseada em um bytearray:

Novamente, as linhas destacadas indicam os bits mais essenciais:

  • Linha 13 define a função parcial com base na int.to_bytes() com alguns de seus atributos definidos como valores fixos. A função parcial recebe um comando Python int e a converte em uma representação de byte adequada exigida pelo formato de arquivo WAV. Nesse caso, ela aloca explicitamente dois bytes com o little-endian ordem de bytes e um bit de sinal para cada valor de entrada.
  • Linha 18 cria um bytearrayvazio, que é um mutável equivalente ao bytes . É como uma lista, mas para os bytes sem sinal do Python. As linhas a seguir continuam estendendo o objeto bytearray com os bytes codificados enquanto itera sobre suas amostras de áudio PCM.
  • Linha 27 passa o bytearray com seus quadros diretamente para o arquivo WAV para gravação.

Por fim, o senhor pode usar o NumPy para expressar de forma elegante a equação da onda sonora e lidar com a conversão de bytes de forma eficiente:

Agradecimentos a Operações vetorizadas do NumPyo senhor elimina a necessidade de looping explícito em seu sound_wave() . Em linha 7, o usuário gera uma matriz de instantes de tempo medidos em segundos e, em seguida, passa essa matriz como entrada para o np.sin() que calcula os valores de amplitude correspondentes. Posteriormente, em linha 17, o senhor pilha e intercalar os dois canais, criando o sinal estéreo pronto para ser gravado no seu arquivo WAV.

A conversão de números inteiros em amostras PCM de 16 bits é realizada chamando a função NumPy .astype() do NumPy com o literal de string "<h" como argumento, que é o mesmo que np.int16 em plataformas little-endian. Entretanto, antes de fazer isso, o senhor deve dimensionar e cortar os valores de amplitude para evitar que o NumPy silenciosamente transbordar ou abaixo do fluxo, o que pode causar cliques audíveis no arquivo WAV.

Esse código não parece mais compacto e legível do que as outras duas versões? De agora em diante, o senhor usará o NumPy na parte restante deste tutorial.

Dito isso, a codificação manual das amostras PCM é bastante complicada devido à necessidade de escalonamento da amplitude e conversão de bytes envolvidos. A falta de bytes assinados do Python às vezes torna isso ainda mais desafiador. De qualquer forma, o senhor ainda não abordou a codificação PCM de 24 bits, que exige um tratamento especial. Na próxima seção, o senhor simplificará esse processo envolvendo-o em uma abstração conveniente.

Decifrar as amostras de áudio codificadas em PCM

Esta parte será um pouco mais avançada, mas tornará o trabalho com arquivos WAV em Python muito mais acessível a longo prazo. Depois disso, você poderá criar todos os tipos de aplicativos interessantes relacionados a áudio, que serão explorados em detalhes nas próximas seções. Ao longo do caminho, o senhor aproveitará os recursos modernos do Python, como enumerações, classes de dadose correspondência de padrões.

Ao final deste tutorial, o senhor deverá ter um pacote Python personalizado chamado waveio que consiste nos seguintes módulos:

waveio/
│
├── __init__.py
├── encoding.py
├── metadata.py
├── reader.py
└── writer.py

O encoding será responsável pela conversão bidirecional entre os valores de amplitude normalizados e as amostras codificadas por PCM. O módulo metadata representará o cabeçalho do arquivo WAV, reader facilitará a leitura e a interpretação dos quadros de áudio, e o writer permitirá a criação de arquivos WAV.

Se o senhor não quiser implementar tudo isso do zero ou se ficar preso em algum ponto, fique à vontade para baixar e usar os materiais de apoio. Além disso, esses materiais incluem vários arquivos WAV codificados de forma diferente, que o senhor pode usar para testar:

Com isso resolvido, é hora de começar a programar!

Enumerar os formatos de codificação

Use seu formato favorito IDE ou editor de código favorito para criar um novo pacote Python e nomeá-lo waveio. Em seguida, defina o pacote encoding dentro do seu pacote e preencha-o com o seguinte código:

O PCMEncoding classe estende IntEnumque é um tipo especial de enumeração que combina o Enum com a classe base do Python com o int do Python. Como resultado, cada membro dessa enumeração torna-se sinônimo de um valor inteiro, que pode ser usado diretamente em expressões lógicas e aritméticas:

Isso se tornará útil para encontrar o número de bits e o intervalo de valores suportados por uma codificação.

Os valores 1 através de 4 em sua enumeração representam o número de bytes ocupados por uma única amostra de áudio em cada formato de codificação. Por exemplo, o SIGNED_16 tem um valor de dois porque a codificação PCM de 16 bits correspondente usa exatamente dois bytes por amostra de áudio. Graças a isso, o senhor pode aproveitar o sampwidth do cabeçalho de um arquivo WAV para instanciar rapidamente uma codificação adequada:

Passar um valor inteiro que represente a largura de amostra desejada por meio da função de enumeração construtor da enumeração retorna a instância de codificação correta.

Depois de determinar a codificação de um determinado arquivo WAV, você poderá usar o seu PCMEncoding para decodificar os quadros de áudio binários. Antes disso, porém, o senhor precisará conhecer o mínimo e máximo de amostras de áudio codificadas com o formato fornecido para que o senhor possa dimensioná-las corretamente para amplitudes de ponto flutuante para processamento posterior. Para isso, defina estes propriedades em seu tipo de enumeração:

Para encontrar o valor máximo representável na codificação atual, o senhor primeiro verifica se self, que significa o número de bytes por amostra, é igual a um. Quando isso acontece, significa que o senhor está lidando com um inteiro sem sinal de 8 bits cujo valor máximo é 255. Caso contrário, o valor máximo de um inteiro assinado é o negativo de seu valor mínimo menos um.

O valor mínimo de um número inteiro sem sinal é sempre zero. Por outro lado, encontrar o valor mínimo de um número inteiro com sinal requer o conhecimento do número de bits por amostra. O senhor o calcula em outra propriedade, multiplicando o número de bytes, que é representado por self, pelos oito bits em cada byte. Em seguida, o senhor obtém o negativo de dois elevado à potência do número de bits menos um.

Observe que, para ajustar o código de cada um dos .max e .min em uma única linha, o senhor usa a função expressão condicionalque é o equivalente à expressão operador condicional ternário em outras linguagens de programação.

Agora, o senhor tem todos os blocos de construção necessários para decodificar quadros de áudio em amplitudes numéricas. Nesse ponto, o senhor pode transformar os bytes brutos carregados de um arquivo WAV em amplitudes numéricas significativas no seu código Python.

Converter quadros de áudio em amplitudes

O senhor usará o NumPy para simplificar seu código e tornar a decodificação de amostras PCM mais eficiente do que com o Python puro. Vá em frente e adicione um novo método ao seu PCMEncoding que manipulará todos os quatro formatos de codificação que o Python entende:

A nova classe .decode() recebe um objeto do tipo bytes que representa o objeto bruto quadros de áudio carregados de um arquivo WAV como argumento. Independentemente do número de canais de áudio em cada quadro, o senhor pode tratar o frames como uma longa sequência de bytes a ser interpretada.

Para lidar com as diferentes codificações, o senhor ramifica seu código usando correspondência de padrão estrutural com a ajuda do match e do case palavras-chave flexíveis introduzidas em Python 3.10. Quando nenhum dos membros da enumeração corresponder à codificação atual, o senhor aumentar a TypeError exceção com uma mensagem adequada.

Além disso, o senhor usa o Ellipsis (...) como um espaço reservado em cada ramificação para evitar que o Python crie um erro de erro de sintaxe devido a um bloco de código vazio. Como alternativa, o senhor poderia ter usado o pass para obter um efeito semelhante. Em breve, o senhor preencherá esses espaços reservados com código adaptado para lidar com as codificações relevantes.

Comece cobrindo o Inteiro sem sinal de 8 bits Codificação PCM em sua primeira ramificação:

Nessa ramificação de código, o senhor transforma os quadros binários em uma matriz NumPy unidimensional de amplitudes de ponto flutuante assinado que variam de -1,0 a 1,0. Ao chamar np.frombuffer() com seu segundo parâmetro posicional (dtype) definido como a string "u1"o senhor diz ao NumPy para interpretar os valores subjacentes como inteiros sem sinal de um byte de comprimento. Em seguida, o senhor normaliza e desloca as amostras PCM decodificadas, o que faz com que o resultado se torne uma matriz de valores de ponto flutuante.

Agora, o senhor pode dar uma olhada no código para verificar se está funcionando como pretendido. Certifique-se de ter baixado os arquivos WAV de amostra antes de continuar e ajuste o caminho abaixo conforme necessário:

O senhor abre o arquivo mono de 8 bits com amostragem de 44,1 kHz. Depois de ler os metadados do cabeçalho do arquivo e carregar todos os quadros de áudio na memória, o senhor instancia o PCMEncoding e chama sua classe .decode() nos quadros. Como resultado, o senhor obtém uma matriz NumPy de valores de amplitude dimensionados.

A decodificação das amostras de números inteiros assinados de 16 e 32 bits funciona de forma semelhante, de modo que o senhor pode implementar ambas em uma única etapa:

Desta vez, o senhor normaliza as amostras dividindo-as por seu correspondente magnitude máxima correspondente sem nenhuma correção de deslocamento, pois os inteiros com sinal já estão centralizados em zero. No entanto, há uma ligeira assimetria, pois o mínimo tem uma maior valor absoluto do que o máximo, e é por isso que o senhor usa o mínimo negativo em vez do máximo como fator de escala. Isso resulta em um intervalo aberto à direita de -1,0 inclusive a 1,0 exclusivo.

Finalmente, é hora de abordar o elefante na sala: decodificar amostras de áudio representadas como inteiros assinados de 24 bits.

Interpretar a profundidade de 24 bits do áudio

Uma maneira de lidar com a decodificação de inteiros assinados de 24 bits é chamar repetidamente a função int.from_bytes() nos tripletos consecutivos de bytes:

Primeiro, o senhor cria um expressão geradora que itera sobre o fluxo de bytes em etapas de três, correspondendo aos três bytes de cada amostra de áudio. O usuário converte esse tripleto de bytes em uma expressão Python intem Python, especificando a ordem dos bytes e a interpretação do bit de sinal. Em seguida, o senhor passa a expressão do gerador para a função NumPy np.fromiter() do NumPy e usa a mesma técnica de escalonamento anterior.

Ela faz o trabalho e parece bastante legível, mas pode ser muito lenta para a maioria das finalidades práticas. Aqui está uma das muitas implementações equivalentes baseadas no NumPy, que é significativamente mais eficiente:

O truque aqui é remodelar sua matriz plana de bytes em uma matriz bidimensional matriz compreendendo três colunas, cada uma representando os bytes consecutivos de uma amostra. Quando o senhor especifica -1 como uma das dimensões, o NumPy calcula automaticamente o tamanho dessa dimensão com base no comprimento da matriz e no tamanho da outra dimensão.

Em seguida, o senhor pad sua matriz à direita, acrescentando o quarto coluna preenchida com zeros. Depois disso, o senhor remodela a matriz novamente por achatamento em outra sequência de bytes, com um zero extra para cada quarto elemento.

Por fim, o senhor reinterpreta os bytes como Inteiros assinados de 32 bits ("<i4"), que é o menor tipo de número inteiro disponível no NumPy que pode acomodar suas amostras de áudio de 24 bits. Entretanto, antes de normalizar as amplitudes reconstruídas, o senhor deve cuidar do bit de sinal, que atualmente está no lugar errado devido ao preenchimento do número com um byte extra. O bit de sinal deve ser sempre o bit mais significativo.

Em vez de realizar complicadas operações bit a bit, o senhor pode tirar proveito do fato de que um bit de sinal mal posicionado fará com que o valor transborde quando ligado. O senhor detecta isso por verificando se o valor é maior que self.maxe, se for, o senhor adiciona duas vezes o valor mínimo para corrigir o sinal. Essa etapa move efetivamente o bit de sinal para sua posição correta. Depois disso, o senhor normaliza as amostras como antes.

Para verificar se o código de decodificação funciona conforme o esperado, use os arquivos WAV de amostra incluídos nos materiais de bônus. O senhor pode comparar os valores de amplitude resultantes de um som codificado com diferentes profundidades de bits:

Logo de cara, o senhor pode dizer que todos os quatro arquivos representam o mesmo som, apesar de usarem diferentes profundidades de bits. Suas amplitudes são surpreendentemente semelhantes, com apenas pequenas variações devido à precisão variável. A codificação PCM de 8 bits é visivelmente mais imprecisa do que as demais, mas ainda capta a forma geral da mesma onda sonora.

Para manter a promessa de que o senhor encoding e seu módulo PCMEncoding em seus nomes, o senhor deve implementar a classe encoding parte dessa conversão bidirecional.

Codificar amplitudes como quadros de áudio

Adicione o .encode() à sua classe agora. Ele pegará o amplitudes normalizadas como argumento e retorna um bytes com os quadros de áudio codificados prontos para gravação em um arquivo WAV. Aqui está o scaffolding do método, que espelha o método .decode() que o senhor implementou anteriormente:

Cada ramo de código reverterá as etapas que o senhor seguiu anteriormente ao decodificar quadros de áudio expressos no formato correspondente.

Entretanto, para codificar as amplitudes processadas em um formato binário, o senhor não precisará apenas implementar escalonamento com base nos valores mínimo e máximo, mas também fixação para mantê-los dentro da faixa permitida de valores de PCM. Ao usar o NumPy, o senhor pode chamar np.clip() para fazer o trabalho para o senhor:

O senhor encerra a chamada para np.clip() em um método não público nomeado ._clamp()que espera as amostras de áudio PCM como seu único argumento. Os valores mínimo e máximo da codificação correspondente são determinados por suas propriedades anteriores, às quais o método delega.

Abaixo estão as etapas para codificar os três formatos padrão, 8 bits, 16 bits e 32 bits, que compartilham uma lógica comum:

Em cada caso, o senhor dimensiona as amplitudes de modo que elas usem todo o intervalo de valores PCM. Além disso, para a codificação de 8 bits, o senhor desloca esse intervalo para se livrar da parte negativa. Em seguida, o senhor fixa e converte as amostras dimensionadas para a representação de byte apropriada. Isso é necessário porque o dimensionamento pode resultar em valores fora do intervalo permitido.

Produzindo valores PCM no codificação de 24 bits é um pouco mais complicado. Como antes, o senhor pode usar o Python puro ou a remodelagem de matriz do NumPy para ajudar a alinhar os dados corretamente. Aqui está a primeira versão:

O senhor usa outra expressão geradora, que itera sobre as amostras escalonadas e fixadas. Cada amostra é arredondada para um valor inteiro e convertida em um bytes com uma chamada para int.to_bytes(). Em seguida, o senhor passa a expressão do gerador para a instância .join() de um arquivo bytes vazio para concatenar os bytes individuais em uma sequência mais longa.

E aqui está a versão otimizada da mesma tarefa com base no NumPy:

Depois de dimensionar e fixar as amplitudes de entrada, o senhor faz um visualização da memória sobre sua matriz de números inteiros para interpretá-los como uma sequência de bytes sem sinal. Em seguida, o senhor a remodela em uma matriz composta de quatro colunas e desconsidera a última coluna com a sintaxe de fatiamento antes de achatar a matriz.

Agora que o senhor pode decodificar e codificar amostras de áudio usando várias profundidades de bits, é hora de colocar sua nova habilidade em prática.

Visualizar amostras de áudio como uma forma de onda

Nesta seção, você terá a oportunidade de implementar o metadata e reader em seus módulos personalizados waveio personalizado. Quando o senhor os combina com o encoding que o senhor construiu anteriormente, poderá traçar um gráfico do conteúdo de áudio armazenado em um arquivo WAV.

Encapsular os metadados do arquivo WAV

Gerenciar vários parâmetros que compõem os metadados do arquivo WAV pode ser complicado. Para facilitar um pouco a sua vida, o senhor pode agrupá-los em um namespace comum definindo uma classe de dados personalizada como a que está abaixo:

Essa classe de dados é marcada como congeladoso que significa que o senhor não poderá alterar os valores dos atributos individuais depois de criar uma nova instância de WAVMetadata. Em outras palavras, os objetos dessa classe são imutáveis. Isso é bom porque evita que o senhor modifique acidentalmente o estado do objeto, garantindo a consistência em todo o programa.

O WAVMetadata lista quatro atributos, incluindo o próprio senhor PCMEncoding baseado no sampwidth do cabeçalho do arquivo WAV. A taxa de quadros é expressa como Python float porque o wave aceita valores fracionários, que são arredondados antes de serem gravados no arquivo. Os dois atributos restantes são números inteiros que representam os números de canais e quadros, respectivamente.

Observe que o uso de um valor padrão de None e o tipo de sindicato faz com que a declaração número de quadros opcional. Isso é para que o senhor possa usar seu WAVMetadata ao gravar um fluxo indeterminado de quadros de áudio sem saber antecipadamente o número total deles. Isso será útil em download de um fluxo de rádio on-line e salvando-o em um arquivo WAV mais adiante neste tutorial.

Os computadores podem processar facilmente quadros de áudio discretos, mas os seres humanos entendem naturalmente o som como um fluxo contínuo ao longo do tempo. Portanto, é mais conveniente pensar na duração do áudio em termos de número de segundos em vez de quadros. O senhor pode calcular a duração em segundos dividindo o número total de quadros pelo número de quadros por segundo:

Ao definir uma propriedade, o senhor poderá acessar o número de segundos como se fosse apenas mais um atributo na propriedade WAVMetadata da classe.

Saber como os quadros de áudio são convertidos em segundos permite que o senhor visualize o som no domínio do tempo. Entretanto, antes de plotar uma forma de onda, o senhor deve carregá-la primeiro de um arquivo WAV. Em vez de usar a função wave diretamente como antes, o senhor dependerá de outra abstração que será criada a seguir.

Carregar todos os quadros de áudio avidamente

A relativa simplicidade do wave em Python o torna uma porta de entrada acessível para análise, síntese e edição de som. Infelizmente, ao expor as complexidades de baixo nível do formato de arquivo WAV, ele o torna o único responsável pela manipulação de dados binários brutos. Isso pode se tornar rapidamente desgastante, mesmo antes de o senhor começar a resolver tarefas mais complexas de processamento de áudio.

Para ajudar com isso, o senhor pode criar um adaptador que envolverá o wave ocultando os detalhes técnicos do trabalho com arquivos WAV. Ele permitirá que o senhor acesse e interprete o amplitudes de som de uma forma mais fácil de usar.

Vá em frente e crie outro módulo chamado reader em seu waveio e defina o seguinte WAVReader nele:

O método inicializador de classe recebe um path que pode ser uma string simples ou um argumento pathlib.Path . Em seguida, ele abre o arquivo correspondente para leitura no modo binário usando o comando wave e instancia o WAVMetadata juntamente com o PCMEncoding.

Os dois métodos especiais, .__enter__() e .__exit__()trabalham em conjunto, executando as ações de configuração e limpeza associadas ao arquivo WAV. Eles fazem da sua classe um gerenciador de contextoportanto, o senhor pode instanciá-lo por meio do with statement:

O .__enter__() retorna o recém-criado WAVReader recém-criada, enquanto o método .__exit__() garante que seu arquivo WAV receba devidamente fechado antes de deixar o bloco de código atual.

Com essa nova classe instalada, o senhor pode acessar de forma conveniente os metadados do arquivo WAV juntamente com o formato de codificação PCM. Por sua vez, isso permite que o senhor converta os bytes brutos em uma longa sequência de níveis de amplitude numérica.

Ao lidar com arquivos relativamente pequenos, como o gravação de uma campainha de bicicletanão há problema em carregar ansiosamente todos os quadros na memória e convertê-los em uma matriz NumPy unidimensional de amplitudes. Para fazer isso, o senhor pode implementar o seguinte método auxiliar em sua classe:

Como chamar o .readframes() em um Wave_read move o ponteiro interno para frente, o senhor chama .rewind() para garantir que o arquivo seja lido desde o início, caso o senhor tenha chamado o método mais de uma vez. Em seguida, o senhor decodifica os quadros de áudio para um matriz plana de amplitudes usando a codificação apropriada.

A chamada desse método produzirá uma matriz NumPy composta de números de ponto flutuante semelhante ao exemplo a seguir:

No entanto, ao processar sinais de áudio, geralmente é mais conveniente ver os dados como uma sequência de quadros ou canais em vez de amostras de amplitude individuais. Felizmente, dependendo de suas necessidades, é possível remodelar rapidamente a matriz unidimensional do NumPy em uma matriz bidimensional bidimensional de quadros ou canais.

O senhor vai tirar proveito de uma matriz personalizada reutilizável decorador para organizar as amplitudes em linhas ou colunas:

O senhor encerra a chamada para seu ._read() interno em um método propriedade em cache nomeado .framespara que o senhor leia o arquivo WAV uma vez, no máximo, quando acessar essa propriedade pela primeira vez. Na próxima vez que a acessar, o senhor reutilizará o valor lembrado no cache. Enquanto sua segunda propriedade, .channels, delega à primeira, ela aplica um valor de parâmetro diferente ao seu @reshape decorator.

Agora o senhor pode adicionar a seguinte definição do decorador ausente:

Trata-se de um fábrica de decoradores parametrizadosque recebe uma string chamada shape, cujo valor pode ser "rows" ou "columns". Dependendo do valor fornecido, ele remodela a matriz NumPy retornada pelo método wrapped em uma sequência de quadros ou canais. O senhor usa menos um como o tamanho da primeira dimensão para permitir que o NumPy o derive do número de canais e do comprimento da matriz. Para o formato colunar, o senhor transpor da matriz.

Para ver isso em ação e notar a diferença na disposição da amplitude na matriz, o senhor pode executar o seguinte trecho de código no REPL do Python:

Como se trata de um áudio estéreo, cada item em .frames consiste em um par de amplitudes correspondentes aos canais esquerdo e direito. Por outro lado, cada .channels individuais compreendem uma sequência de amplitudes para seus respectivos lados, permitindo que o senhor os isole e processe independentemente, se necessário.

Ótimo! Agora o senhor está pronto para visualizar a forma de onda completa de cada canal em seus arquivos WAV. Antes de prosseguir, talvez o senhor queira adicionar as seguintes linhas ao arquivo __init__.py em seu arquivo waveio do senhor:

O primeiro permitirá que o senhor importe o pacote WAVReader diretamente do pacote, ignorando o intermediário reader intermediário. A variável especial __all__ contém uma lista de nomes disponíveis em um importação curinga.

Plotar uma forma de onda estática usando Matplotlib

Nesta seção, o senhor combinará as peças para representar visualmente uma forma de onda de um arquivo WAV. Ao construir em cima de seu waveio e aproveitando as abstrações do Matplotlib o senhor poderá criar gráficos como este:

Formas de onda estéreo de um som de campainha de bicicleta
Formas de onda estéreo de um som de campainha de bicicleta

É uma representação estática de toda a forma de onda, o que permite que o senhor estude as variações de amplitude do sinal de áudio ao longo do tempo.

Se ainda não o fez, instale o Matplotlib em seu ambiente virtual e crie um script Python chamado plot_waveform.py fora do diretório waveio . Em seguida, cole o seguinte código-fonte em seu novo script:

O senhor usa a tag argparse para ler os argumentos do script a partir da linha de comando. Atualmente, o script espera apenas um argumento posicional, que é o caminho que aponta para um arquivo WAV em algum lugar do seu disco. O senhor usa o módulo pathlib.Path para representar esse caminho de arquivo.

Seguindo o nome-principal expressão idiomática, o senhor chama seu main() que é a função do script ponto de entrada do script, na parte inferior do arquivo. Depois de analisar os argumentos da linha de comando e abrir o arquivo WAV correspondente com o seu WAVReader o senhor poderá prosseguir e plotar seu conteúdo de áudio.

O senhor plot() executa estas etapas:

  • Linhas 19 a 24 criar um figura com subparcelas correspondentes a cada canal no arquivo WAV. O número de canais determina o número de linhas, enquanto há apenas uma única coluna na figura. Cada subparcela compartilha o eixo horizontal para alinhar as formas de onda durante o zoom ou a panorâmica.
  • Linhas 26 e 27 garantem que o ax seja uma sequência de Axes ao envolver essa variável em uma lista quando houver apenas um canal no arquivo WAV.
  • Linhas 29 a 32 percorrem os canais de áudio, definindo o título de cada subparcela com o número do canal correspondente e usando o y-ticks para obter um dimensionamento consistente. Por fim, eles plotam a forma de onda do canal atual em seu respectivo Axes respectivo objeto.
  • Linhas 34 a 36 definem o título da janela, fazem com que os subplots se encaixem perfeitamente na figura e, em seguida, exibem o plot na tela.

Para mostrar o número de segundos no eixo horizontal compartilhado pelos subplots, o senhor precisa fazer alguns ajustes:

Ao chamar a função linspace() no linhas 24 a 28o senhor calcula a linha do tempo que compreende instantes de tempo uniformemente distribuídos, medidos em segundos. O número desses instantes de tempo é igual ao número de quadros de áudio, o que permite que o senhor os desenhe juntos no linha 34. Como resultado, o número de segundos no eixo horizontal corresponde aos respectivos valores de amplitude.

Além disso, o senhor define um formatador personalizado para sua linha do tempo e conecte-a ao eixo horizontal compartilhado. Sua função implementada em linhas 40 a 44 faz com que os rótulos de tique mostrem a unidade de tempo quando necessário. Para mostrar apenas os dígitos significativos, o senhor usa a letra g como o especificador de formato na seção f-strings.

Como a cereja do bolo, o senhor pode aplicar um dos estilos disponíveis que vêm com o Matplotlib para tornar o gráfico resultante mais atraente:

Nesse caso, o senhor escolhe uma folha de estilo inspirada nas visualizações encontradas no site FiveThirtyEight que é especializado em análise de pesquisas de opinião. Se esse estilo não estiver disponível, o senhor poderá recorrer ao estilo padrão do Matplotlib.

Vá em frente e teste seu script de plotagem com mono, estéreoe, potencialmente, até mesmo som surround arquivos de tamanhos variados.

Quando o senhor abre um arquivo WAV maior, a quantidade de informações dificulta o exame de detalhes finos. Embora a interface de usuário do Matplotlib ofereça ferramentas de zoom e panorâmica, pode ser mais rápido cortar os quadros de áudio no intervalo de tempo de interesse antes de plotar. Mais tarde, o senhor usará essa técnica para animar suas visualizações!

Ler uma fatia de quadros de áudio

Se o senhor tiver um arquivo de áudio particularmente longo, poderá reduzir o tempo necessário para carregar e decodificar os dados subjacentes pulando e reduzindo o intervalo de quadros de áudio de interesse:

Uma fatia da forma de onda do tambor de bongô
Uma fatia da forma de onda do tambor de bongô

Essa forma de onda começa em três segundos e meio e dura cerca de cento e cinquenta milissegundos. Agora o senhor está prestes a implementar esse recurso de corte.

Modifique seu script de plotagem para aceitar dois argumentos opcionais, marcando o início e o fim de uma fatia da linha do tempo. Ambos devem ser expressos em segundos, que o senhor traduzirá posteriormente para índices de quadros de áudio no arquivo WAV:

Quando o usuário não especifica a hora de início com -s ou --startseu script presume que o senhor deseja começar a ler os quadros de áudio desde o início, em zero segundos. Por outro lado, o valor padrão do parâmetro -e ou --end é igual a None, que o senhor tratará como a duração total de todo o arquivo.

Em seguida, o senhor desejará plotar as formas de onda de todos os canais de áudio cortados no intervalo de tempo especificado. O senhor implementará a lógica de corte dentro de um novo método em seu WAVReader que o senhor pode chamar agora:

Basicamente, o senhor substituiu uma referência à classe .channels por uma chamada à propriedade .channels_sliced() e passou seus novos argumentos de linha de comando – ou seus valores padrão – para ela. Quando o senhor omite o --start e --end seu script deve funcionar como antes, plotando toda a forma de onda.

Ao plotar uma fatia de quadros de áudio em seus canais, o senhor também desejará combinar os parâmetros linha do tempo com o tamanho dessa fatia. Em particular, sua linha do tempo pode não começar mais em zero segundos e pode terminar antes da duração total do arquivo. Para se ajustar a isso, o senhor pode anexar um range() de índices de quadros aos seus canais e usá-lo para calcular a nova linha do tempo:

Aqui, o senhor converte os índices inteiros dos quadros de áudio em seus instantes de tempo correspondentes em segundos a partir do início do arquivo. Nesse ponto, o senhor já terminou de editar o script de plotagem. É hora de atualizar seu waveio.reader agora.

Para permitir a leitura do arquivo WAV a partir de um índice de quadro arbitrário, o senhor pode aproveitar as vantagens do módulo wave.Wave_read do objeto .setpos() em vez de retroceder para o início do arquivo. Vá em frente e substitua .rewind() por .setpos(), adicione um novo parâmetro, start_frame, ao seu ._read() e dê a ele um valor padrão de None:

Quando o senhor ligar ._read() com algum valor para esse novo parâmetro, o senhor moverá o ponteiro interno para essa posição. Caso contrário, o senhor começará a partir da última posição conhecida, o que significa que a função não retrocederá mais o ponteiro para o senhor. Portanto, o senhor deve passar explicitamente zero como o quadro inicial quando o senhor lê todos os quadros ansiosamente na propriedade correspondente.

Agora, defina a propriedade .channels_sliced() que o senhor chamou anteriormente em seu script de plotagem:

Esse método aceita dois parâmetros opcionais, que representam o início e o fim do arquivo WAV em segundos. Quando o usuário não especifica o parâmetro final, o padrão é a duração total do arquivo.

Dentro desse método, o usuário cria um slice() dos índices de quadro correspondentes. Para levar em conta os valores negativos, o usuário expande a fatia em um objeto range() fornecendo o número total de quadros. Em seguida, o senhor usa esse intervalo para ler e decodificar uma fatia de quadros de áudio em valores de amplitude.

Antes de retornar da função, o senhor combina as amplitudes fatiadas e o intervalo correspondente de índices de quadros em um wrapper personalizado para a matriz NumPy. Esse invólucro se comportará exatamente como uma matriz normal, mas também exporá a função .frames_range que o senhor pode usar para calcular a linha do tempo correta para a plotagem.

Essa é a implementação do wrapper que o senhor pode adicionar ao atributo reader antes do seu módulo WAVReader definição de classe:

Sempre que o usuário tenta acessar um dos atributos da matriz NumPy em seu wrapper, o .__getattr__() delega ao método .values que é a matriz original de níveis de amplitude. O método .__iter__() possibilita fazer um loop no seu wrapper.

Infelizmente, o senhor também precisa substituir a matriz do .reshape() do array e o método .T que o senhor usa em seu método @reshape decorator. Isso ocorre porque eles retornam arrays NumPy simples em vez de seu wrapper, apagando efetivamente as informações extras sobre o intervalo de índices de quadro. O senhor pode resolver esse problema empacotando os resultados novamente.

Agora, o senhor pode aumentar o zoom em uma fatia específica de quadros de áudio em todos os canais fornecendo o parâmetro --start e --end parâmetros:

O comando acima traça a forma de onda que o senhor viu na captura de tela no início desta seção.

Como o senhor sabe como carregar uma fração de quadros de áudio por vez, pode ler arquivos WAV enormes ou até mesmo fluxos de áudio on-line de forma incremental. Isso permite animar uma visualização ao vivo do som nos domínios de tempo e frequência, o que o senhor fará a seguir.

Processe arquivos WAV grandes em Python com eficiência

Como os arquivos WAV geralmente contêm dados não compactados, não é incomum que eles atinjam tamanhos consideráveis. Isso pode tornar seu processamento extremamente lento ou até mesmo impedir que o senhor coloque o arquivo inteiro na memória de uma só vez.

Nesta parte do tutorial, o senhor lerá um arquivo WAV relativamente grande em partes usando avaliação preguiçosa para melhorar a eficiência do uso da memória. Além disso, o senhor gravará um fluxo contínuo de quadros de áudio provenientes de uma estação de rádio da Internet em um arquivo WAV local.

Para fins de teste, o senhor pode obter um arquivo WAV grande o suficiente de um artista que atende pelo nome de Waesto em várias plataformas de mídia social:

Olá! Sou Waesto, um produtor musical que cria músicas melódicas para criadores de conteúdo usarem aqui no YouTube ou em outras mídias sociais. O senhor pode usar a música gratuitamente, desde que eu seja creditado na descrição! (Fonte)

Para baixar as músicas do Waesto no formato WAV, o senhor precisa se inscrever no canal do artista no YouTube ou segui-lo em outras mídias sociais. Em geral, suas músicas são protegidas por direitos autorais, mas podem ser usadas livremente, desde que sejam devidamente creditadas.

O senhor encontrará um link direto para download e os créditos na descrição de cada vídeo no YouTube. Por exemplo, uma das primeiras músicas do artista, intitulada Sleeplessfornece um para o arquivo WAV correspondente hospedado na plataforma Hypeddit. É um áudio estéreo PCM de 24 bits sem compressão, com amostragem de 44,1 kHz, que pesa cerca de quarenta e seis megabytes.

Sinta-se à vontade para usar qualquer uma das faixas de Waesto ou um arquivo WAV completamente diferente que lhe agrade. Lembre-se apenas de que ele deve ser grande.

Animação do gráfico de forma de onda em tempo real

Em vez de plotar uma forma de onda estática de todo ou de parte de um arquivo WAV, o senhor pode usar a função janela deslizante para visualizar um pequeno segmento do áudio à medida que ele é reproduzido. Isso criará uma interessante osciloscópio atualizando o gráfico em tempo real:

Animação da forma de onda com o efeito de osciloscópio

Essa representação dinâmica do som é um exemplo de visualização de música semelhante aos efeitos visuais que o senhor encontraria no clássico Winamp player.

O senhor não precisa modificar seu WAVReader que já fornece os meios necessários para saltar para uma determinada posição em um arquivo WAV. Como anteriormente, o senhor criará um novo arquivo de script para lidar com a visualização. Nomeie o script plot_oscilloscope.py e preencha-o com o código-fonte abaixo:

A estrutura geral do script permanece análoga à que o senhor criou na seção anterior. O senhor começa analisando os argumentos da linha de comando, incluindo o caminho para um arquivo WAV e uma duração opcional da janela deslizante, cujo padrão é cinquenta milissegundos. Quanto mais curta for a janela, menos amplitudes aparecerão na tela. Ao mesmo tempo, a animação se tornará mais suave ao atualizar cada quadro com mais frequência.

Depois de abrir o arquivo WAV especificado para leitura, o senhor chama animate() com o nome do arquivo, a duração da janela e uma sequência de janelas avaliada de forma preguiçosa obtida de slide_window(), que é uma função geradora:

Uma única janela nada mais é do que um fatia de quadros de áudio que o senhor aprendeu a ler anteriormente. Dado o número total de segundos no arquivo e a duração da janela, essa função calcula a contagem de janelas e itera em um intervalo de índices. Para cada janela, ela determina onde começa e termina na linha do tempo para cortar os quadros de áudio adequadamente. Por fim, ela produz a amplitude média de todos os canais em um determinado instante de tempo.

A parte de animação consome seu gerador de janelas e plota cada uma delas com um pequeno atraso:

Primeiro, o senhor escolhe o tema do Matplotlib com um fundo escuro para obter um efeito visual mais dramático. Em seguida, o senhor configura a figura e um Axes remova a borda padrão em torno da visualização e itere sobre as janelas. Em cada iteração, o senhor limpa o gráfico, oculta os ticks e fixa a escala vertical para mantê-la consistente em todas as janelas. Depois de plotar a janela atual, o senhor pausa o loop pela duração de uma única janela.

Veja como o senhor pode iniciar a animação a partir da linha de comando para visualizar o arquivo WAV baixado com sua música favorita:

O efeito do osciloscópio pode lhe dar uma visão da dinâmica temporal de um arquivo de áudio no domínio do tempo. A seguir, o senhor reutilizará a maior parte desse código para mostrar alterações espectrais no domínio da frequência de cada janela.

Mostrar uma visualização de espectrograma em tempo real

Enquanto uma forma de onda informa sobre as alterações de amplitude ao longo do tempo, um espectrograma pode fornecer uma representação visual de como as intensidades de diferentes bandas de frequência variam com o tempo. Os reprodutores de mídia geralmente incluem uma visualização em tempo real da música, que é semelhante à imagem abaixo:

Animação do espectrograma de um arquivo WAV

A largura de cada barra vertical corresponde a um intervalo de frequências ou uma banda de frequência, com sua altura representando a relativa nível de energia dentro dessa banda em um determinado momento. As frequências aumentam da esquerda para a direita, com as frequências mais baixas representadas no lado esquerdo do espectro e as frequências mais altas à direita.

Agora, copie todo o código-fonte do plot_oscilloscope.py e cole-o em um novo script chamado plot_spectrogram.py, que o senhor modificará para criar uma nova visualização do arquivo WAV.

Como você calculará a FFT de segmentos curtos de áudio, convém sobrepor segmentos adjacentes para minimizar a vazamento espectral causada por descontinuidades abruptas nas bordas. Elas introduzem frequências fantasmas no espectro, que não existem no sinal real. Uma sobreposição de cinquenta por cento é um bom ponto de partida, mas o senhor pode torná-lo configurável por meio de outro argumento de linha de comando:

O --overlap deve ser um número inteiro entre zero, inclusive, e cem, exclusive, representando uma porcentagem. Quanto maior for a sobreposição, mais suave será a animação.

Agora o senhor pode modificar seu slide_window() para aceitar essa porcentagem de sobreposição como um parâmetro adicional:

Em vez de mover a janela por toda a sua duração, como antes, o senhor introduz uma etapa que pode ser menor, resultando em um número maior de janelas no total. Por outro lado, quando a porcentagem de sobreposição é zero, o senhor organiza as janelas uma ao lado da outra sem nenhuma sobreposição entre elas.

Agora, o senhor pode passar a sobreposição solicitada na linha de comando para a função do gerador, bem como o parâmetro animate() :

O senhor precisará saber a sobreposição para ajustar a velocidade da animação de acordo. Observe que o senhor envolveu a chamada para slide_window() com outra chamada para uma nova função, fft()que calcula as frequências e suas magnitudes correspondentes na janela deslizante:

Há muita coisa acontecendo, portanto, será útil detalhar essa função linha por linha:

  • Linha 4 calcula o período de amostragem, que é o intervalo de tempo entre as amostras de áudio no arquivo, como o recíproco da taxa de quadros.
  • Linha 6 usa o tamanho da janela e o período de amostragem para determinar as frequências no áudio. Observe que a frequência mais alta no espectro será exatamente a metade da taxa de quadros devido ao teorema de Nyquist-Shannon mencionado anteriormente.
  • Linhas 7 a 11 encontre as magnitudes de cada frequência determinada na linha 6. Antes de realizar a transformada de Fourier das amplitudes das ondas, o senhor subtrai o valor médio, geralmente chamado de polarização de CC, que é a parte não variável do sinal que representa o termo de frequência zero na transformada de Fourier. Além disso, o senhor aplica uma função de janela para suavizar ainda mais as bordas da janela.
  • Linha 12 produz uma tupla que inclui as frequências e suas magnitudes correspondentes.

Por fim, o senhor deve atualizar seu código de animação para desenhar um gráfico de barras de frequências em cada posição da janela deslizante:

O senhor passa a porcentagem de sobreposição para ajustar o atraso da animação entre os quadros subsequentes. Em seguida, o senhor itera sobre as frequências e suas magnitudes, plotando-as como barras verticais espaçadas pelo intervalo desejado. O senhor também atualiza os limites do eixo adequadamente.

Execute o comando abaixo para iniciar a animação do espectrograma:

Brinque com a duração da janela deslizante e a porcentagem de sobreposição para ver como elas afetam a animação. Quando estiver entediado e sedento por usos mais práticos do wave em Python, o senhor pode melhorar seu jogo tentando algo mais desafiador!

Gravar uma estação de rádio da Internet como um arquivo WAV

Até agora, o senhor tem usado abstrações do seu waveio para ler e decodificar arquivos WAV de forma conveniente, o que permitiu que o senhor se concentrasse em tarefas de nível mais alto. Agora é hora de adicionar a peça que faltava no quebra-cabeça e implementar o WAVReader . O senhor criará um objeto escritor preguiçoso capaz de gravar blocos de dados de áudio em um arquivo WAV.

Para esta tarefa, o senhor realizará um exemplo prático.ripagem de fluxo e Rádio na Internet para um arquivo WAV local.

Para simplificar a conexão com uma transmissão on-line, o senhor usará uma pequena classe auxiliar para obter quadros de áudio em tempo real. Expanda a seção dobrável abaixo para revelar o código-fonte e as instruções sobre como usar a classe auxiliar RadioStream de que o senhor precisará mais tarde:

O código a seguir depende de pyavque é uma ligação Python para o FFmpeg biblioteca. Depois de instalar as duas bibliotecas, coloque esse módulo ao lado do seu script de extração de fluxo para que você possa importar a biblioteca RadioStream a partir dele:

O módulo define a classe RadioStream que usa a classe endereço de URL de uma estação de rádio da Internet. Ele expõe os metadados WAV subjacentes e permite que o usuário itere sobre os canais de áudio em partes. Cada bloco é representado como uma matriz NumPy familiar.

Agora, crie o writer em seu módulo waveio e use o código abaixo para implementar a funcionalidade de gravação incremental de quadros de áudio em um novo arquivo WAV:

O WAVWriter recebe um WAVMetadata e um caminho para o arquivo WAV de saída. Em seguida, abre o arquivo para gravação no modo binário e usa os metadados para definir os valores de cabeçalho apropriados. Observe que o número de quadros de áudio permanece desconhecido nesse estágio; portanto, em vez de especificá-lo, o usuário permite que o wave atualize-o posteriormente quando o arquivo for fechado.

Assim como o leitor, seu objeto escritor segue o padrão gerenciador de contexto protocolo. Quando o usuário insere um novo contexto usando o with a nova palavra-chave WAVWriter retornará a si mesma. Por outro lado, sair do contexto garantirá que o arquivo WAV seja fechado corretamente, mesmo que ocorra um erro.

Depois de criar uma instância de WAVWritero senhor pode adicionar um pedaço de dados ao seu arquivo WAV chamando .append_channels() com uma matriz bidimensional NumPy de canais como argumento. O método reformulará os canais em uma matriz plana de valores de amplitude e os codificará usando o formato especificado nos metadados.

Lembre-se de adicionar o seguinte import ao seu waveio do seu pacote __init__.py antes de prosseguir:

Isso permite a importação direta do arquivo WAVWriter do seu pacote, ignorando a classe intermediária writer intermediário.

Finalmente, o senhor pode ligar os pontos e criar seu próprio ripper de fluxo em Python:

O senhor abre um fluxo de rádio usando o URL fornecido como argumento de linha de comando e usa os metadados obtidos para o arquivo WAV de saída. Normalmente, a primeira parte do fluxo contém informações como o formato do contêiner de mídia, a codificação, a taxa de quadros, o número de canais e a profundidade de bits. Em seguida, o usuário faz um loop no fluxo e anexa cada trecho decodificado do pedaço de canais de áudio para o arquivo, capturando o momento volátil de uma transmissão ao vivo.

Aqui está um exemplo de comando que mostra como o senhor pode gravar o EuroDance clássico channel:

Observe que o senhor não verá nenhuma saída enquanto o programa estiver em execução. Para parar de gravar a estação de rádio escolhida e sair do script, pressione Ctrl+C no Linux e no Windows ou Cmd+C se o senhor estiver usando o macOS.

Embora agora seja possível gravar um arquivo WAV em partes, o senhor ainda não implementou a lógica adequada para o análogo leitura preguiçosa antes. Embora o senhor possa carregar uma fatia de dados de áudio delimitada pelos registros de data e hora fornecidos, isso não é o mesmo que iterar sobre uma sequência de pedaços de tamanho fixo em um loop. O senhor terá a oportunidade de implementar esse mecanismo de leitura baseado em pedaços no que vem a seguir.

Ampliar o campo estéreo de um arquivo WAV

Nesta seção, o senhor lerá simultaneamente um trecho de quadros de áudio de um arquivo WAV e gravará sua versão modificada em outro arquivo de forma preguiçosa. Para fazer isso, o senhor precisará aprimorar seu WAVReader adicionando o seguinte método:

É um método gerador, que recebe um parâmetro opcional que indica o número máximo de quadros de áudio a serem lidos por vez. Esse parâmetro retorna a um valor padrão definido em um constante atributo de classe. Depois de rebobinar o arquivo até sua posição inicial, o método entra em um loop infinitoque lê e produz os blocos subsequentes de dados do canal até que não haja mais quadros de áudio para ler.

Como a maioria dos outros métodos e propriedades dessa classe, .channels_lazy() é decorado com @reshape para organizar as amplitudes decodificadas de uma forma mais conveniente. Infelizmente, esse decorador atua em uma matriz NumPy, enquanto seu novo método retorna um objeto gerador. Para torná-los compatíveis, o senhor deve atualizar a definição do decorador, tratando de dois casos:

O senhor usa o inspect para determinar se o decorador envolve um método regular ou um método gerador. Ambos os wrappers fazem a mesma coisa, mas o wrapper do gerador produz os valores remodelados em cada iteração, enquanto o wrapper do método regular retorna eles.

Por fim, o senhor pode adicionar outra propriedade que informará se o arquivo WAV é estéreo ou não:

Ela verifica se o número de canais declarados no cabeçalho do arquivo é igual a dois.

Com essas alterações em vigor, o senhor pode ler os arquivos WAV em partes e começar a aplicar vários efeitos sonoros. Por exemplo, o senhor pode ampliar ou reduzir o campo estéreo de um arquivo de áudio para aumentar ou diminuir a sensação de espaço. Uma dessas técnicas envolve a conversão de um sinal estéreo convencional, composto pelos canais esquerdo e direito, em médio e lateral canais.

O canal intermediário (M) contém um componente monofônico que é comum a ambos os lados, enquanto o canal lateral (S) captura as diferenças entre os esquerda (L) e direita (R) canais. O senhor pode converter entre as duas representações usando as seguintes fórmulas:

Conversão entre os canais médio-lateral e esquerdo-direito
Conversão entre os canais médio-lateral e esquerdo-direito

Depois de separar o canal lateral, o senhor pode aumentá-lo independentemente do canal médio antes de recombiná-los com os canais esquerdo e direito novamente. Para ver isso em ação, crie um script chamado stereo_booster.py que usa os caminhos para os arquivos WAV de entrada e saída como argumentos com um parâmetro opcional de intensidade:

O --strength é um multiplicador para o canal lateral. Use um valor maior que um para ampliar o campo estéreo e um valor entre zero e um para reduzi-lo.

Em seguida, implemente as fórmulas de conversão de canal como funções Python:

Ambas recebem dois parâmetros correspondentes aos canais esquerdo e direito, ou aos canais médio e lateral, e retornam uma tupla dos canais convertidos.

Por fim, o senhor pode abrir um arquivo WAV estéreo para leitura, percorrer seus canais em partes e aplicar o processamento do lado intermediário explicado:

O arquivo WAV de saída tem o mesmo formato de codificação que o arquivo de entrada. Depois de converter cada bloco nos canais médio e lateral, o senhor os converte novamente nos canais esquerdo e direito, aumentando apenas o canal lateral.

Observe que agora o senhor anexa os canais modificados como argumentos separados, enquanto o script de gravação de rádio passou uma única matriz NumPy de canais combinados. Para fazer o .append_channels() funcione com os dois tipos de invocações, o senhor pode atualizar o método WAVWriter da seguinte forma:

O senhor usa correspondência de padrões estruturais novamente para diferenciar entre uma única matriz multicanal e várias matrizes de canal único, reformulando-as em uma sequência plana de amplitudes para codificação adequada.

Tente aumentar um dos arquivos WAV de amostra, como o som da campainha da bicicleta, em um fator de cinco:

Lembre-se de escolher um som estéreo e, para obter a melhor experiência auditiva, use os alto-falantes externos em vez de fones de ouvido para reproduzir o arquivo de saída. O senhor consegue ouvir a diferença?

Conclusão

O senhor percorreu um longo caminho neste tutorial. Depois de aprender sobre a estrutura do arquivo WAV, o senhor se familiarizou com a função wave do Python para ler e gravar dados binários brutos. Em seguida, o senhor criou suas próprias abstrações sobre o módulo wave para que o senhor pudesse pensar sobre os dados de áudio em termos de nível superior. Isso, por sua vez, permitiu que o senhor implementasse várias ferramentas práticas e divertidas de análise e processamento de áudio.

Neste tutorial, o senhor aprendeu a:

  • Ler e escrever arquivos WAV usando Python puro
  • Lidar com os arquivos WAV de 24 bits codificação PCM de amostras de áudio
  • Interpretar e trama o subjacente níveis de amplitude
  • Registro transmissões de áudio on-line como estações de rádio na Internet
  • Animar visualizações na seção tempo e frequência domínios
  • Sintetizar sons e aplicação efeitos especiais

Agora que o senhor já tem muita experiência em trabalhar com arquivos WAV em Python, pode se dedicar a projetos ainda mais ambiciosos. Por exemplo, com base em seu conhecimento sobre o cálculo da transformada de Fourier e o processamento de arquivos WAV de forma preguiçosa, o senhor pode implementar um vocoder popularizado pela banda alemã Kraftwerk nos anos setenta. Isso fará com que sua voz soe como um instrumento musical ou um robô!