header

Vamos continuar o nosso gravador sem cabeça postagem anterior. Vimos que conseguimos gravar com mais de uma interface de áudio ao mesmo tempo. Eu recomendo muito você ler antes a parte #1 e a parte #2, mas vou te dar uma colher de chá.

Contexto

Eu quero criar um gravador em que você conecte interfaces de áudio USB e ele as grave, controlando por um tablet.

Pra isso eu preciso saber se com um Raspberry Pi 3B sem tela, eu:

  1. Consigo gravar de uma interface de áudio usando arecord?
  2. Consigo gravar de duas interfaces de áudio simultaneamente usando arecord?
  3. Consigo usar um serviço web pra disparar a gravação?
  4. Consigo conectar e enviar os comandos acima diretamente no pi como um hot spot?

Hoje vamos atacar o item #3: Consigo usar um serviço web pra disparar a gravação?

Como não vamos ter tela ou botões no nosso gravador, vamos precisar controlá-lo remotamente. O jeito mais fácil que eu consigo pensar de fazer isso, da perspectiva de usuário, é por WiFi, através de um serviço web. Ou seja: num computador, celular ou tablet, vamos abrir uma página no navegador capaz de controlar o gravador.

Pra isso vamos precisar programar um pouco. Finalmente! O foco dessa publicação será mais em software web do que Linux ou áudio, como os anteriores.

Bora lá!

Mão na massa

Primeiro eu preciso pensar em que comandos eu vou enviar. Pra manter as coisas simples, vou continuar o que estamos fazendo: gravar um áudio de 5 segundos, pra cada interface que temos. Lembre-se: esse é apenas um protótipo.

Então o mínimo que eu posso fazer é isso: uma página html com um link: Gravar.

Se bem me lembro de html, vamos testar uma coisinha na minha máquina mesmo:

<!-- hr.html -->

<html>
  <head>
    <title>O Gravador sem Cabeça</title>
  </head>

  <body>
    <a href="/gravar">Gravar</a>
  </body>
</html>

Mestre do html

Dá pra ver que eu levo jeito pra designer, diz aí!

Mas beleza, se clicar nesse link vai pra uma página de erro. Claro, não implementamos endereço do link /gravar.

O que eu quero que aconteça quando clicar no link? Que grave, oras. Mas como é que eu vou fazer uma página mandar um comando pro Raspberry Pi?

Aí vai entrar uma palavra da moda: vamos criar uma API, uma forma de um programa (a página que criamos, ou o navegador) se comunicar com outro programa, o arecord.

O que diabos é uma API?

Assim como sua TV tem entradas, como HDMI, USB e o controle remoto; e saídas como a óptica, fone de ouvido / áudio e a própria tela, programas podem definir suas entradas e saídas para se comunicarem com outros programas.

Sua TV tem um conjunto finito de entradas e saídas, vamos chamar isso de interface. Quando uma interface serve para a comunicação entre dois programas, nós chamamos de Application Programming Interface, ou simplesmente de API.

Protótipo com Python e FastAPI

Tá, e como a gente cria uma API? Já existe pronta?

Vamos ter que programar um pouquinho. Pra não me alongar, eu preciso que você segure minha mão e tome algumas coisas como garantidas. Você pode chamar de mágica, mas em breve entraremos em detalhes. Lembre-se: estamos fazendo apenas um protótipo.

Pra ser bem rápido, eu vou usar Python e uma biblioteca chamada FastAPI. Ela vai nos permitir, de uma maneira muito simples, subir um servidor com uma API que vamos definir.

O primeiro passo é ver se o Raspberry Pi já vem com alguma instalação do Python.

raphaelpaiva@hr:~/hr $ python --version
Python 3.9.2

Show, temos um Python 3.9 já instalado. Dá pro gasto, mas pra usar o FastApi vamos precisar de um outro programa que não veio instalado, o pip, um gerenciador de pacotes e bibliotecas pro python.

Pra instalar, é moleza sudo apt install -y python3-pip. Agora sim podemos usar o pip pra instalar o FastApi: pip install fastapi[all]

Agora vamos pra mão na massa de fato.

Implementando o ponto de entrada

Vamos implementar o /gravar que chamamos na nossa página de uma forma bem simples, só pra ver se a chamada funciona.

No pi, eu criei uma pasta chamada hr pra colocar os nossos arquivos python. Nele, criei um arquivo main.py com o seguinte código:

# main.py
from fastapi import FastAPI # importando a biblioteca FastAPI

app = FastAPI() # Inicializando o nosso aplicativo

@app.get('/gravar') # definindo a operação que vai ser executada quando chamarmos '/gravar'
async def gravar():
  return "opa!"    # Nesse caso, espero que retorne apenas um texto escrito "opa!"

Segundo as instruções na documentação do FastAPI pra rodar o programa, precisamos do uvicorn. O FastApi já o instala pra nós, então basta vamos rodar:

raphaelpaiva@hr:~/hr $ python -m uvicorn main:app --reload --host 0.0.0.0
INFO:     Will watch for changes in these directories: ['/home/raphaelpaiva/hr']
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [54511] using WatchFiles
INFO:     Started server process [54513]
INFO:     Waiting for application startup.
INFO:     Application startup complete.

Traduzindo:

  • python -m uvicorn: Uvicorn é um módulo python que vai carregar a nossa API. Para rodar um módulo python, utilizamos o comando python -m <NOME DO MODULO>. Daí pra frente, passamos parâmetros pro módulo.
  • main:app: especifica qual aplicação o uvicorn vai carregar. No nosso caso main:app significa a aplicação definida na linha 4 do arquivo main.py. O padrão é <NOME DO ARQUIVO>:<NOME DA VARIAVEL QUE RECEBEU O FastAPI()>
  • --reload: instrui o uvicorn pra recarregar a aplicação toda vez que algum arquivo for modificado.
  • --host 0.0.0.0 faz com que o uvicorn escute requisições em todas as interfaces de rede. Isso vai garantir que vamos conseguir chamar a aplicação do meu computador e não apenas de dentro do Raspberry Pi.

Um detalhe importante da saída do programa é

INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

Alí ele nos indica a porta em que o uvicorn está escutando: 8000.

Vamos ver se o bichão funciona.

No navegador, vou tentar acessar o Raspberry Pi na porta 8000. Pode ser pelo endereço IP do Pi ou pelo nome que configurei nele. Vou pelo nome, é mais bonitinho: http://hr.local:8000/gravar.

opa Opa!

Estamos chegando lá. Duas coisas que precisamos fazer:

  1. Fazer aquela página html ser servida pelo uvicorn.
  2. fazer o link ‘Gravar’ realmente gravar alguma coisa.

Vamos fazer a nossa página ser exibida quando entrarmos em http://hr.local:8000/. Para isso, vamos adicionar uma outra operação no nosso arquivo main.py. Ele vai ficar dessa forma:

# main.py
from fastapi import FastAPI
from fastapi.responses import HTMLResponse

app = FastAPI()

@app.get('/') # quando chamarmos a raiz (representado pelo '/') da nossa aplicação
async def index():
  index_html = """
<html>
  <head>
    <title>O Gravador sem Cabeça</title>
  </head>

  <body>
    <a href="/gravar">Gravar</a>
  </body>
</html>
"""
  return HTMLResponse(content=index_html, status_code=200) # Vamos devolver uma resposta html, com o código da página que criamos anteriormente.

@app.get('/gravar')
async def gravar():
  return "opa!"

Se acessarmos a raiz da nossa aplicação…

index Agora a página mais bela da terra está sendo servida do Raspberry Pi e não mais no meu computador.

E se clicar no gravar, vamos ver o nosso ‘Opa!’ que implementamos anteriormente!

Chamando um programa através do python

O Python possui uma biblioteca já inclusa chamada subprocess. Ela nos permite chamar processos (ou programas) através de código python.

Dentro dela, temos uma função chamada run. É o método mais simples de chamar um programa.

Vamos modificar a nossa função gravar para chamar o arecord com os comandos que vimos anteriormente.

@app.get('/gravar')
async def gravar():
  # O comando que usamos pra gravar da GT1 no post anterior
  command = ["arecord", "-Dhw:CARD=GT1,DEV=0", "-d", "5", "-f", "S32_LE", "-r", "44100", "-c", "2", "opa.wav"]
  response = subprocess.run(command, capture_output=True) # Chamamos o processo
  return response.stdout + b' / ' + response.stderr # E devolvemos qualquer coisa que ele colocaria no terminal.

A subprocess.run() recebe o comando a rodar como uma lista. Em python, uma lista pode ser definida como acima: cada elemento é separado por uma virgula e todos ficam entre colchetes. Todos os elementos estão entre aspas pois todos são do tipo texto (ou string).

Passamos um outro parâmetro pra ela, chamado capture_output, quando ele é True, podemos inspecionar a saída do programa através das propriedades stdout e stderr da resposta da chamada da subprocess.run(), que é o que retornamos na última linha, separado por ` / `.

Então, esperamos que tenhamos uma resposta parecida com a de rodar o programa na linha de comando quando clicarmos em gravar.

Ihá!

Claro, que eu estava com a guitarra desplugada, mas aprecie o ruído girando na sua orelha.

Caso seu browser não carregue, clique aqui.

Beleza, gravamos de uma. Mas queremos gravar de várias ao mesmo tempo.

Chamadas bloqueantes e não bloqueantes

Quando clicamos em gravar, demora um pouco pra recebermos a resposta. Mais precisamente, 5.08s. Nós pedimos pra gravar 5 segundos (com o parâmetro -d 5 pro arecord), 0.08s pra requisição ir até o Pi, processar e voltar, parece justo.

E se quisermos gravar de duas interfaces em paralelo?

Vamos modificar a nossa função pra rodar os dois comandos.

@app.get('/gravar')
async def gravar():
  gt1_command = ["arecord", "-Dhw:CARD=GT1,DEV=0", "-d", "5", "-f", "S32_LE", "-r", "44100", "-c", "2", "opa.wav"]
  beh_command = ["arecord", "-Dhw:CARD=CODEC,DEV=0", "-d", "5", "-f", "S16_LE", "-r", "44100", "-c", "2", "beh.wav"]
  gt1_response = subprocess.run(gt1_command, capture_output=True)
  beh_response = subprocess.run(beh_command, capture_output=True)
  
  return {
    'gt1': gt1_response.stdout + b' / ' + gt1_response.stderr,
    'beh': beh_response.stdout + b' / ' + beh_response.stderr
  }

Especificamos os comandos para cada interface, rodamos ambos e vamos retornar agora um valor um pouco melhor estruturado, especificando a saída de cada uma.

A resposta ficou mais bonitinha desa vez.

Mas se formos olhar o tempo:

Demorou 10 segundos.

Isso foi porque um comando rodou e só depois que ele terminou, o outro começou. Lembra, na postagem anterior que utilizamos aquele & para que pudéssemos rodar ambos os comandos ao mesmo tempo? Precisamos de algo parecido aqui.

Por padrão, qualquer função que chamamos em um programa é bloqueante, ou seja, seu programa vai ficar parado até a função terminar o seu trabalho. É o caso da subprocess.run().

gt1_response = subprocess.run(gt1_command, capture_output=True)
beh_response = subprocess.run(beh_command, capture_output=True)

Neste caso, a primeira chamada, para a gt1, vai durar até o comando de gravação da gt1 acabar. Como especificamos 5 segundos de gravação serão 5s nele e mais 5 segundos abaixo. Na mesma biblioteca, temos um modo de chamar um programa externo de forma não bloqueante, servindo o mesmo propósito do & da linha de comando: o subprocess.Popen()

Modificando um pouco mais o nosso método:

@app.get('/gravar')
async def gravar():
  gt1_command = ["arecord", "-Dhw:CARD=GT1,DEV=0", "-d", "5", "-f", "S32_LE", "-r", "44100", "-c", "2", "opa.wav"]
  beh_command = ["arecord", "-Dhw:CARD=CODEC,DEV=0", "-d", "5", "-f", "S16_LE", "-r", "44100", "-c", "2", "beh.wav"]

  gt1_process = subprocess.Popen(gt1_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
  beh_process = subprocess.Popen(beh_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

  gt1_stdout, gt1_stderr = gt1_process.communicate()
  beh_stdout, beh_stderr = beh_process.communicate()
  
  return {
    'gt1': str(gt1_stdout) + ' / ' + str(gt1_stderr),
    'beh': str(beh_stdout) + ' / ' + str(beh_stderr)
  }

Tivemos algumas novidades:

  • Especificamos os parâmetros stdout e stderr na chamada do Popen(),
    • Isso serve para que possamos utilizar a saída do programa.
  • Chamamos, para os dois processos o método communicate().
    • Com ele, nós conseguimos capturar a saída e esperar que cada um deles termine sua execução antes de continuar nosso programa.
  • No retorno, transformamos a saída em string utilizando a função str(), só pra facilitar a geração da nossa resposta bonitinha.

Vamos ver agora o tempo.

Ah muleke!

Agora sim! Geramos nossos dois arquivos, paralelamente.

No fim das contas, nosso programa ficou assim:

# main.py

import subprocess
from fastapi import FastAPI
from fastapi.responses import HTMLResponse

app = FastAPI()

@app.get('/')
async def gravar():
  index_html = """
<html>
  <head>
    <title>O Gravador sem Cabeça</title>
  </head>

  <body>
    <a href="/gravar">Gravar</a>
  </body>
</html>
"""
  return HTMLResponse(content=index_html, status_code=200)

@app.get('/gravar')
async def gravar():
  gt1_command = ["arecord", "-Dhw:CARD=GT1,DEV=0", "-d", "5", "-f", "S32_LE", "-r", "44100", "-c", "2", "opa.wav"]
  beh_command = ["arecord", "-Dhw:CARD=CODEC,DEV=0", "-d", "5", "-f", "S16_LE", "-r", "44100", "-c", "2", "beh.wav"]

  gt1_process = subprocess.Popen(gt1_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) #subprocess.run(gt1_command, capture_output=True)
  beh_process = subprocess.Popen(beh_command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

  gt1_stdout, gt1_stderr = gt1_process.communicate()
  beh_stdout, beh_stderr = beh_process.communicate()
  
  return {
    'gt1': str(gt1_stdout) + ' / ' + str(gt1_stderr),
    'beh': str(beh_stdout) + ' / ' + str(beh_stderr)
  }

Conclusão

Fizemos bastante coisa nesse aqui.

  1. Criamos uma página com um link
  2. Implementamos um programa em python que roda lá no Raspberry Pi.
  3. Tornamos esse programa disponível pela rede com o uvicorn.
  4. O link da página que criamos chama o nosso programa e ele grava os nossos áudios, da mesma forma que nos post anteriores.

Já temos muita coisa encaminhada! Falta apenas um item da nossa lista: conectar diretamente ao Raspberry Pi com wifi, sem depender de um roteador.

Nos vemos no próximo!