header

Vamos continuar o nosso gravador sem cabeça da postagem anterior. Esse post não requer a leitura dos anteriores, mas vou dar um pouco de contexto.

Contexto

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

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 hotspot?

Conseguimos validar todas as necessidades básicas para o protótipo. Já está na hora desse projeto ter mais cara de produto e encarar o campo.

Voltando aos trabalhos

Dois anos se passaram desde o último post. A vida mudou bastante e o projeto acabou perdendo prioridade. Muito porque eu troquei de equipamento. Vendi minha boa e velha GT-1 (saudades) e troquei por uma mais moderna, a HX Stomp e esta, infelizmente, não é compatível com Linux. Um tempo depois, acabei adquirindo um MVave IR BOX, um pequeno notável, que serve como uma interface USB e funciona no Linux :D.

MVave IRBox Um pequeno notável

Foram dois anos cheios de música, com shows, composições e gravações. Na sanha de registrar os eventos da banda com mais fidelidade pra poder gerar material, estava na hora de voltar à esse projeto.

Onde paramos?

Desde o último post, o projeto era apenas uma prova de conceito. A interface era apenas um link que gravava 10 segundos de áudio de interfaces usb pré configuradas (minha antiga pedaleira GT-1 e uma interface Behringer emprestada).

Só aí já temos alguns problemas:

  1. As gravações vão precisar de mais de 10 segundos de duração 😅.
  2. As interfaces de áudio USB não serão as mesmas. Eu mesmo não tenho mais a pedaleira que usava e o pessoal da banda têm outras (que espero que sejam compatíveis).

Então surgem alguns requisitos para que eu possa levar meu Raspberry Pi pra passear:

  1. Precisamos detectar e escolher uma interface de áudio para uma gravação
  2. Precisamos iniciar uma gravação sem limite de tempo
  3. Precisamos parar uma gravação
  4. Precisamos exportar uma gravação

Essencialmente, vamos transformar essa prova de conceito em um produto, ou na primeira versão de um.

Planejamento

Pra sair do zero, estabeleci um objetivo simples: criar uma API enxuta para controlar as gravações. Vamos manter os 5 segundos por enquanto, mas vamos permitir escolher a interface de áudio.

Vamos criar um endpoint /devices para listar os dispositivos de áudio conectados e modificar o /record para que use o dispositivo escolhido.

Listando dispositivos de áudio de entrada

No primeiro post exploramos como listar dispositivos de áudio usando o Alsa, sistema de som padrão do sistema operacional do Raspberry Pi. Vamos usar a mesma abordagem aqui, mas agora precisamos interpretar a saída do comando arecord -L para gerar uma lista de dispositivos.

A saída do comando arecord -L é algo assim:

null
    Discard all samples (playback) or generate zero samples (capture)
hw:CARD=CODEC,DEV=0
    USB Audio CODEC, USB Audio
    Direct hardware device without any conversions
plughw:CARD=CODEC,DEV=0
    USB Audio CODEC, USB Audio
    Hardware device with all software conversions
default:CARD=CODEC
    USB Audio CODEC, USB Audio
    Default Audio Device
sysdefault:CARD=CODEC
    USB Audio CODEC, USB Audio
    Default Audio Device
front:CARD=CODEC,DEV=0
    USB Audio CODEC, USB Audio
    Front output / input
dsnoop:CARD=CODEC,DEV=0
    USB Audio CODEC, USB Audio
    Direct sample snooping device

Vamos criar uma função para interpretar essa saída e gerar uma lista de dispositivos de áudio.

A ideia aqui é simples: cada dispositivo começa com uma linha que não tem indentação, seguida por uma ou mais linhas indentadas que descrevem o dispositivo. Vamos capturar o nome do dispositivo e sua descrição.

Não precisa entender o código linha a linha. Não é a forma mais elegante de implementar e isso já mudou, mas foi a primeira versão que escrevi.

Basta saber que criamos uma função chamada parse_arecord_L que recebe a saída do comando arecord -L como string e retorna uma lista de objetos SoundDevice, cada um representando um dispositivo de áudio.

Dá um confere aqui, se quiser.

E para criar o endpoint que lista os dispositivos, chamamos o comando arecord -L e passamos a saída para a função que acabamos de criar.

from subprocess import run

@app.get("/devices")
async def devices():
  devices: List[SoundDevice] = []
  
  output_str = run(['arecord', '-L'], capture_output=True).stdout.decode('utf-8')
  devices = parse_arecord_L(output_str)

  return {"devices": devices}

O endpoint /record precisa ser modificado pra receber o dispositivo que o usuário escolheu. Vamos fazer isso esperando um payload JSON com o nome do dispositivo no campo device.

@app.post("/record")
async def record(payload: dict): # Recebe o nome do dispositivo no payload
  device = payload['device'] # Exemplo: 'hw:CARD=CODEC,DEV=0'

  cmd = ['ffmpeg', '-y', '-f', 'alsa', '-i', device, '-ac', '2', 'bg.wav'] # Monta o comando para gravação
  
  process = Popen(cmd, stdin=PIPE, stdout=PIPE)

  sleep(10)

  try:
    process.communicate(b'q', timeout=10)
  except TimeoutExpired:
    process.kill()

  return {  
    "pid": process.pid,
    "rc": process.returncode
  }

Este código essencialmente recebe o nome do dispositivo e o usa para iniciar uma gravação com ffmpeg. Espera 10 segundos e então para a gravação. O nome do arquivo será sempre bg.wav por enquanto.

Está praticamente igual ao protótipo, com duas modificações importantes: Recebemos como parâmetro o dispositivo de áudio a ser usado, dentro de payload['device'].

Estamos usando ffmpeg ao invés de arecord. O ffmpeg é mais flexível e nos permitirá fazer coisas mais legais no futuro. Com ele, por exemplo, não precisamos nos preocupar em passar a taxa de amostragem e o formato da amostragem. Ele se vira.

Depois de disparar o comando na linha process = Popen(cmd, stdin=PIPE, stdout=PIPE), esperamos 10 segundos e então paramos a gravação com process.communicate(b'q', timeout=10). O ffmpeg espera que a gente aperte q para parar a gravação quando não especificamos uma duração. Cheira a gambiarra e é, mas funciona por enquanto.

Interface Web

Eu não sou designer. Eu não sou bom criando interfaces e não gosto muito de trabalhar com isso. Isso não quer dizer que eu seja ruim implementando, o problema mesmo é ter uma ideia de uma interface legal.

Mas estamos em 2026 e temos aí o Gemini pra nos ajudar. Expliquei os endpoints e pedi a página mais simples possível, com html e javascript puros e ele me deu isso aqui:

Voce pode conferir esse maravilhoso html completo aqui no github.

Resultado