Site menu Python FM - ouvindo rádio com Python

Python FM - ouvindo rádio com Python

Nos artigos sobre modulação FM e AM, utilizamos arquivos de áudio para "transmitir" o sinal modulado. Era o que se podia fazer em 2010, quando os artigos foram escritos. Mas restava a desconfiança, inclusive de minha parte: será que isso realmente funciona com rádio de verdade?

Bem, estamos em 2019 e os dongles SDR (Software-defined Radio) estão por toda parte. Então podemos finalmente colocar nosso código à prova, pelo menos na recepção. Ainda não comprei um SDR capaz de transmitir, mas está nos planos!

Figura 1: Ouvindo FM usando o RTL-SDR e o programa GQRX

A grande maioria dos SDRs à venda são clones do RTL-SDR. Embora todos funcionem para sinais fortes, recomendo comprar o kit do dongle original, pois além do aparelho ser melhor, a antena telescópica inclusa é muito boa e prática.

Figura 2: Dongle RTL-SDR em seu posto

O que o dongle SDR faz

Seja qual for o modo (AM, SSB, FM, digital) a demodulação sempre começa pela "mixagem", que é a multiplicação do sinal de rádio por uma portadora gerada no receptor. Em essência, o dongle SDR só faz a mixagem e digitaliza o resultado. Daí para frente, é com o software.

Figura 3: Demodulador de quadratura digital ou I/Q, de conversão direta

O RTL-SDR é um receptor homódino ou de conversão direta, pois move o sinal para a banda desejada de uma só vez, com uma só mixagem. Receptores analógicos, e SDRs de alta qualidade, são heteródinos ou de dupla conversão: movem o sinal para uma freqüência intermediária fixa, e então demodulam uma segunda vez. Isto permite uma filtragem mais efetiva. Alguns rádios top chegam a fazer tripla conversão.

Alguns modos exigem mixagem por duas versões defasadas de 90º da portadora, a fim de extrair a informação de fase do sinal. Isto é conhecido como demodulação de quadratura ou I/Q, e é implementada pelo RTL-SDR a fim de ser um "receptor universal". (Da mesma forma que um modulador I/Q também é um "transmissor universal".)

As técnicas de SDR não estão restritas aos dongles. Muitos aparelhos comerciais, inclusive transceptores, walkie-talkies, etc. são SDR internamente. Por ora isto é mais comum em aparelhos de baixo custo.

Figura 4: Diagrama do funcionamento interno do chip RDA1846, muito utilizado em walkie-talkies FM de baixo custo, como o Baofeng UV-5R e similares.

Taxa de amostragem versus banda

Quando acionamos o RTL-SDR, devemos informar pelo menos dois parâmetros: portadora e taxa de amostragem. Por exemplo, o comando abaixo especifica uma portadora de 121MHz e uma taxa de 1920000 amostras por segundo:

rtl_sdr -f 121M -s 1920k amostras.rtl

O SDR filtra o sinal para evitar "aliasing", então a taxa de amostragem também significa a largura de banda capturada. No exemplo acima, estamos capturando uma banda de 1.92MHz, centrada em 121MHz, ou seja, de 120.04 a 121.96MHz. Como a filtragem não é perfeita, considera-se que a banda "limpa" do SDR é 80% da taxa de amostragem (no exemplo, em torno de 120.2 a 121.8MHz).

O RTL-SDR tem resolução de 8 bits, e cada amostra é composta de dois valores: I e Q. Uma amostragem de 1920k implica em 3.84MB por segundo. Os valores individuais vão de -127.5 a +127.5, adicionados de 127.5 para "caberem" num byte inteiro (ou seja, as amostras negativas não são expressas em complemento de 2).

Ouvindo uma rádio FM

Os programas Python e scripts citados daqui em diante podem ser encontrados neste repositório GitHub.

Demonstração do scripts para ouvir FM no computador. Repositório: https://github.com/elvis-epx/sdr

No artigo sobre modulação FM, a primeira etapa da recepção é demodulação I/Q seguida de filtragem. O dongle SDR já faz isto por nós, o que simplificará nosso "rádio Python".

Tudo que precisamos fazer é sintonizar a portadora do RTL-SDR na rádio, e usar uma taxa de amostragem que limite a banda a uma única emissora. Do script python_fm_mono:

rtl_sdr -f 89.5M -s 256k -n 2560000 teste.iq
cat teste.iq | ./fm1.py > teste.raw
sox -t raw -r 256000 -b 16 -c 1 -L -e signed-integer teste.raw \
	-d rate 32000

O RTL-SDR tem uma limitação: a taxa de amostragem deve ser menor que 300k ou maior que 900k. Se quisermos capturar uma largura de banda de e.g. 600kHz, precisamos capturar uma banda maior e filtrar em software.

Isso não é um problema se queremos ouvir apenas uma estação, pois FM comercial tem largura de banda de 200kHz, e uma taxa de amostragem de 256k já resolve.

Por outro lado, poderíamos capturar uma banda maior (o RTL-SDR vai até 2.4MHz) e ouvir/gravar diversas estações ao mesmo tempo. Tornaria nosso código mais complexo, mas é uma ideia de projeto.

Quando o receptor é I/Q, e a portadora está centrada na freqüência da rádio FM, desvios de freqüência do sinal recebido podem ser detectados como variações de fase. A fase absoluta não importa; é a taxa de variação ou rotação de fase que nos interessa.

Também não importa se o sinal I/Q é fraco ou forte. Só nos interessa a proporção relativa entre I e Q. (Num receptor AM, seria o contrário; usaríamos a soma I+Q para extrair o áudio.)

O programa Python fm1.py implementa um receptor FM mínimo, que recebe amostras SDR e cospe amostras de áudio.

O script python_fm_mono grava 10 segundos de amostras SDR e transforma isso num arquivo de áudio. Já o script python_fm_rt funciona sem parar, como um rádio mesmo, desde que o computador seja rápido o suficiente.

FM estéreo

O "áudio" FM possui uma enorme largura de banda (75-100kHz), mas o áudio monofônico só vai até 15kHz. A parte ultrasônica do áudio carrega informação extra, variando conforme a região e a emissora.

Figura 5: Conteúdo do "áudio" FM estéreo

O programa fm1s.py implementa FM estéreo. (Infelizmente, ele ainda não extrai dados RDS.) Os scripts python_fm_stereo e python_fm_rt_stereo acionam o RTL-SDR e repassam os dados para o programa Python. Ambos usam o dongle da mesma forma; o tratamento estéreo acontece depois de finalizada a demodulação FM.

Figura 6: Decodificação FM estéreo

A implementação do estéreo deu bastante trabalho...

Conforme tentamos explicar no artigo sobre modulação AM, decodificar AM-SC é difícil; precisamos de uma réplica exata, e em fase, da portadora do transmissor. É para isto que serve o sinal piloto de 19kHz. As portadoras do áudio estéreo e do RDS são cossenóides múltiplas inteiras de 19kHz, e é garantido que estão todas em fase.

A forma usual de gerar as portadoras é usando um PLL (Phase-Locked Loop) com detecção de "zero crossing". A ideia é que, quando o sinal-piloto troca de polaridade, seu ângulo é 90º ou 270º (por ser uma cossenóide), e neste momento a portadora local deveria estar no ângulo de 180º, pois é uma cossenóide com o dobro da frequência.

Figura 7: Portadora no estado ideal, atinge o valor mais negativo quando o sinal-piloto cruza o eixo X.

Conforme a nossa portadora esteja "adiantando" ou "atrasando" em relação ao sinal-piloto, vamos ajustando a freqüência. A dificuldade é fazer este ajuste de modo que a portadora chegue no sincronismo logo, mas sem ficar oscilando.

Para implementar o PLL, precisamos isolar o sinal-piloto com um filtro forte. Também precisamos filtros para isolar o sinal mono, o sinal estéreo modulado, e o resultado da demodulação AM-SC. Os diversos filtros FIR utilizados pelo "receptor estéreo" estão no módulo filters.py. Fui teimoso e fiz eu mesmo os filtros, mas o pacote SciPy (e possivelmente muitos outros) oferece isso pronto.

Antes que alguém pergunte: não basta simplesmente gerar uma cossenóide de 38kHz para demodular o estéreo. Nem o seu computador nem a emissora FM têm precisão de 100%, então sempre vai haver uma pequena diferença das portadoras, suficiente para estragar o estéreo (que fica "indo e voltando" conforme a portadora entra e sai de fase).

Figura 8: Exemplo de áudio estéreo mal decodificado.

Mas sim, o sinal piloto fica sempre muito perto de 19000Hz. Durante a depuração do PLL, se constatamos que a portadora desvia muito deste valor, com certeza é problema do nosso algoritmo, não da emissora FM.

Deênfase

Talvez alguém tenha notado que a versão monofônica do nosso "receptor FM" tenha um excesso de agudos.

Na versão estéreo, o áudio é filtrado para ficar limitado à banda de 15kHz, mas também para que seja realizada a "deênfase". Deênfase é um filtro passa-baixas suave, uma rampa de 10dB ao longo da banda útil.

Isto é necessário porque as emissoras fazem "preênfase", ou seja, amplificam os sinais agudos segundo uma rampa de 10dB. O objetivo é aumentar a resistência dos sons agudos ao ruído de fundo, que aumenta linearmente com a freqüência. (A mesma técnica é utilizada em vinil.)

Figura 9: Áudio FM "cru" e mal sintonizado. Note que o ruído aumenta linearmente com a freqüência, chegando ao ponto de "enterrar" quase toda a faixa estéreo.

Assim como acontece com tantas tecnologias de legado, a definição da ênfase é meio barroca e dá muita margem a interpretação. Em tese, ela é definida como a constante de tempo de um filtro RC que implementa a rampa de ênfase. Por exemplo, uma ênfase de 75µs (padrão para rádios FM) corresponde a uma freqüência de corte (-3dB) de aprox. 2100Hz.

Um filtro RC apresenta uma rampa de 6dB/oitava, o que significaria 15dB de rampa entre 2100 e 15000Hz. Porém, na prática se usa uma rampa de 10dB, e mesmo a constante de tempo é diferente conforme a região (e.g. é 50µs na Europa). Não duvido que algum fabricante de rádios use uma rampa ainda menor para que o seu produto tenha mais agudos e pareça mais "Hi-Fi". Enfim, é uma sopa.

Mono e estéreo

As rádios FM começaram monofônicas. Quando o estéreo foi introduzido, tinha de ser compatível com receptores antigos ou mais simples. No nível da modulação, o estéreo fica na faixa ultrasônica de áudio, então é inaudível e/ou é filtrada fora num receptor mono.

No nível do áudio, o canal 0-15kHz carrega a soma dos canais esquerdo e direito (L+R). É o que um ouvinte monofônico espera ouvir. A banda adicional 23-53kHz carrega a diferença dos canais (L-R). Esta técnica é conhecida como "joint-stereo", também utilizada em arquivos MP3.

O receptor reconstitui os canais L e R calculando a soma e a diferença dos canais mono e joint-stereo:

Mudança da taxa de amostragem

Como a banda de áudio FM é 75-100kHz, não fazemos miséria e usamos uma confortável taxa de 256k amostras, do SDR até a decodificação dos canais mono e joint-stereo. Quase no final, convertemos o áudio para 32k amostras.

O "downsampling" de 256k para 32k é muito simples; por ser uma relação inteira, basta decimar, ou seja, pegar 1 de cada 8 amostras. (*) O único requisito é filtrar antes o sinal para que caiba na banda nova, mas isto nosso filtro de deênfase já tinha feito.

A propósito, fazer o "upsampling" é igualmente simples: basta inserir amostras zeradas e filtrar o resultado pela banda original, a fim de remover os artefatos. O "resampling" por uma relação racional é a composição de um upsampling seguida de downsampling; os dois filtros podem então ser combinados, e quiçá usar uma otimização conhecida como filtro polyphase.

Depuração

Depurar este código exigiu muito teste 'de ouvido' e abrir áudios no Audacity. O componente mais complicado foi o PLL que gera a portadora para extrair o sinal estéreo. Mas não é fácil perceber um estéreo defeituoso se o áudio mono é bom e forte.

Então, o modo debug (-d) grava os canais L+R e L-R sem combiná-los, mais um canal com o sinal-piloto de 19kHz. Em geral, um bug no PLL resulta num canal L-R que "vem e vai" (imediatamente visível no Audacity) ou então distorcido.

A decimação não é feita, o áudio de debug é gravado com a taxa de 256k. O canal monofônico não é filtrado, para que seja possível conferir o espectro de todos os elementos (mono, stereo, piloto, RDS), e verificar que a demodulação está funcionando.

Otimização

No geral, o código do "Python FM" visa mais a clareza do que a velocidade. A versão mono é particularmente minimalista. (O NumPy nos proporciona uma boa performance logo de saída, sem complicar o código.)

Infelizmente, a versão estéreo puro Python não conseguia tocar em tempo real, então tivemos de apelar para o Cython. O código pode rodar em dois modos: puro Python, ou otimizado com Cython (em que é necessário compilar o módulo).

No momento, o decodificador estéreo usa 50% de uma CPU, então ainda há muito espaço para otimização, que não vou perseguir porque a velocidade atual é suficiente para nossos fins.

O que ainda falta

A decodificação estéreo só deveria ser ativada quando o sinal-piloto estiver "audível". Notificar quando o estéreo for ativado.

Remoção de sinal DC (0Hz) sem prejudicar os sons graves.

Decodificação do texto RDS.

Notas

(*) "Decimação" quer dizer "eliminar um décimo". Era a punição mais severa do Exército Romano, em que os soldados eram agrupados de dez em dez, e então ordenados a sortear e executar um membro do grupo.