Logo

dev-resources.site

for different kinds of informations.

Desvendando Subprocessos: Criando um Bot de Música com Go

Published at
1/13/2025
Categories
go
discord
programming
ffmpeg
Author
agstrc
Categories
4 categories in total
go
open
discord
open
programming
open
ffmpeg
open
Author
6 person written this
agstrc
open
Desvendando Subprocessos: Criando um Bot de Música com Go

Ao tentar criar um bot de música para o Discord em Go, aprendi um pouco mais sobre alguns conceitos:

  • Subprocessos
  • Áudio Opus
  • Formato Ogg

Vou ensinar o que aprendi nesse post. Ressalto que esse post é para fins educacionais e há formas mais simples de atingir o objetivo do bot de música. O intuito aqui é ensinar um pouco do que aprendi durante minha primeira tentativa.

Visão geral

Em alto nível, o bot vai funcionar com uma pipeline. Usaremos o yt-dlp para extrair o áudio do YouTube (o programa aceita várias fontes, mas aqui iremos focar no YouTube) e o ffmpeg para garantir um encoding padrão e consistente do áudio. Por fim, vamos precisar extrair frames Opus do áudio providenciado pelo ffmpeg para enviar ao Discord, mas isso será feito diretamente no código Go. Dado isso, certifique-se que ambos o yt-dlp e o ffmpeg estejam instalados para o funcionamento correto.

Baixando o áudio

O yt-dlp, por padrão, baixa vídeos por completo do YouTube. No nosso caso, queremos apenas o áudio. Isso pode ser atingido com a flag --extract-audio (um alias mais curto dessa flag é -x):

yt-dlp --extract-audio "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
Enter fullscreen mode Exit fullscreen mode

Como podemos executar isso em código Go? É simples, usando o pacote exec da biblioteca padrão, conseguimos facilmente executar o mesmo comando. O pacote é responsável por configurar e iniciar subprocessos, que executam programas externos.

dlp := exec.Command("yt-dlp", "--extract-audio", "https://www.youtube.com/watch?v=dQw4w9WgXcQ")
dlp.Run()
Enter fullscreen mode Exit fullscreen mode

Com esse trecho, iniciamos um subprocesso que executa o yt-dlp. O uso do método cmd.Run inicia o subprocesso, e espera sua finalização na goroutine atual.

É possível que o resultado do yt-dlp sendo chamado manualmente gere um arquivo .opus válido. Isso acontece porque o yt-dlp já chama o ffmpeg nesse caso. Isso não será útil para nós porque ao usar pipes (visto posteriormente), o áudio não é gerado nesse formato

Codificando o áudio

A documentação da API do Discord menciona que o áudio deve ser enviado no formato Opus a um sample rate de 48kHz e com dois canais (stereo). A biblioteca que iremos usar mais a frente, discordgo, lida com todos os outros detalhes técnicos do envio, como criptografia end-to-end e gerenciamento do estado. Dito isso, ainda cabe a nós o envio apropriado dos bytes de áudio.

Tendo isso em mente, procurei a ferramenta mais famosa para processamento multimídia, o ffmpeg. Podemos gerar um arquivo opus da seguinte forma:

ffmpeg -i audio.mp3 -f opus -frame_duration 20 -ar 48000 -ac 2 audio.opus
Enter fullscreen mode Exit fullscreen mode
  • -i audio.mp3: arquivo de entrada para a codificação
  • -f opus: seleciona o formato Opus para saída
  • -frame_duration: define a duração de frames Opus a 20 milissegundos
  • -ar 48000: define o sample rate como 48kHz
  • ac 2: define a saída em modo stereo
  • audio.opus: arquivo de saída

De forma análoga ao yt-dlp, podemos executar o comando em Go:

    ffmpeg := exec.Command("ffmpeg", "-i", "audio.mp3", "-f", "opus", "-frame_duration", "20", "-ar", "48000", "-ac", "2", "audio.opus")
    ffmpeg.Run()
Enter fullscreen mode Exit fullscreen mode

Agora, podemos executar os dois comandos e obter um áudio apropriado para o Discord. Porém, ainda temos dois problemas. O primeiro é que tudo que fizemos foi executar os dois comandos a parte. Dessa forma, teriamos um trabalho manual de baixar a música, codifica-la e salva-lá em um arquivo que apenas então seria enviado ao Discord. O que buscamos aqui é criar uma stream de áudio, sem precisar salvar arquivos em cada etapa.

O segundo problema é que o Discord requer que enviemos Opus frames, um por um. Um arquivo .opus é, na verdade, um arquivo com metadados (normalmente usando o formato/container Ogg), que encapsulam os frames. Os frames são a unidade mais básica de áudio do formato. Por essa razão, não podemos simplesmente enviar os bytes do arquivo final para a API do Discord.

Gerando uma stream de dados

Para gerar uma stream de dados, evitando que precisemos salvar o arquivo a cada etapa do processo, vamos usar o de pipes, normalmente aplicado no shell de sistemas Unix-like. No fim dessa etapa, vamos ter uma cadeia de ações que baixa o áudio do YouTube, transmite-o diretamente para o ffmpeg e então escreve seus bytes na saída padrão. O código será equivalente ao seguinte:

yt-dlp --extract-audio "https://www.youtube.com/watch?v=dQw4w9WgXcQ" -o - | \
ffmpeg -i - -f opus -frame_duration 20 -ar 48000 -ac 2 -
Enter fullscreen mode Exit fullscreen mode

Atenção: não execute o comando diretamente no seu terminal, porque ele vai gerar uma saída ilegível e pode congelar seu terminal.

No comando acima, o uso de - indica entradas ou saídas padrão (stdin/stdout).
Esse processo permita que o ffmpeg leia "diratemente" do yt-dlp e então jogue sua saída para o stdout. Vamos usar isso no código Go para obter a saída sem necessidade de chamadas adicionais de leitura de arquivo.

Primeiramente, vamos criar uma pipe para a saída do yt-dlp. Isso pode ser feito com o método StdoutPipe.

dlp := exec.Command(
    "yt-dlp",
    "--extract-audio",
    "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
    "-o", "-",
)
pipe, err := dlp.StdoutPipe()
if err != nil {
    return err
}
Enter fullscreen mode Exit fullscreen mode

O código estabelece que a saída padrão do yt-dlp irá conter os bytes do arquivo de áudio extraído. Essa saída está pode ser acessada pela variável pipe, a qual é um io.ReadCloser. Ele implementa a interface padrão de leitura do Go. Não vamos nos preocupar em fechar essa pipe pois isso será tratado depois pelo próprio pacote, na chamada de cmd.Wait.

Como discutido, o ffmpeg vai receber seu arquivo de entrada a partir da entrada padrão stdin. Dessa forma, vamos associar a pipe à entrada padrão do ffmpeg.

// ... código anterior
ffmpeg := exec.Command(
    "ffmpeg",
    "-i", "-",
    "-f", "opus",
    "-frame_duration", "20",
    "-ar", "48000",
    "-ac", "2",
    "-",
)
ffmpeg.Stdin = pipe
audioPipe, err := ffmpeg.StdoutPipe()
if err != nil {
    return err
}
Enter fullscreen mode Exit fullscreen mode

Nós também fazemos um pipe com a saída do ffmpeg. Isso nos permite ler a saída diretamente no código. Agora, já podemos usufruir do resultado do ffmpeg. Para isso, basta iniciar os dois comandos.

// exemplo: usando os processos para exibir a hash sha256 do áudio
//
// ... código anterior
if err := dlp.Start(); err != nil {
    return err
}
if err := ffmpeg.Start(); err != nil {
    return err
}

data, _ := io.ReadAll(audioPipe) // ignorando tratativa de erro
fmt.Println(sha256.Sum256(data)) // exibe os bytes puros da hash

// ignorando erros
// esperamos os processos finalizar para garantir liberação de recursos
_ = dlp.Wait()
_ = ffmpeg.Wait()
Enter fullscreen mode Exit fullscreen mode

Extraindo os frames Opus

Antes de enviar o áudio para o Discord, agora precisamos extrair os frames de áudio Opus do arquivo. Isso acontece porque estamos gerando arquivos .opus, com container Ogg. Primeiro, uma breve explicação de codecs e containers:

Um codec (compressor/descompressor) é responsável pela codificação ou decodificação de dados multimídia. Eles definem com a mídia deve ser representada, buscando objetivos como ocupar menos espaço, ou maior eficiência de transmissão. Um container é um formato que permite várias streams, podendo elas serem áudio, vídeo ou até legendas, em um único arquivo. O container define formatos para representar metadados, e como representar os dados "crus" do codec sendo contido.

Nesse contexto, um arquivo .opus normalmente é um arquivo que utiliza o container Ogg para armazenar áudio com o codec Opus. O Ogg é responsável por definir metadados que podem armazenar tags, comentários e também onde a stream Opus se encontra.

O formato Ogg

O Ogg organiza dados em pages (páginas). Cada page contém packets (pacotes) que são unidades menores que contém os dados brutos.

No caso de áudio Opus, as duas primeiras pages contém metadados, os quais identificam o arquivo como uma stream Opus, e dados de tags e comentários. A terceira page contém, os packets de áudio puro, os frames Opus que queremos. Cada packet é um frame.

Extraindo os frames Opus em Go

Tendo essa breve introdução aos formatos em mente, vamos extrair os frames em Go. Para isso, vamos usar um decoder do formato Ogg. O pacote é github.com/jonas747/ogg.

A seguir, um exemplo em que exibimos o tamanho de cada frame:

// assumindo código preexistente com audioPipe saindo do ffmpeg
pageDecoder := ogg.NewDecoder(audioPipe)

// pulamos as duas primeiras páginas
pageDecoder.Decode()
pageDecoder.Decode()

packetDecoder := ogg.NewPacketDecoder(pageDecoder)
for {
    packet, _, err := packetDecoder.Decode()
    if err != nil {
        break
    }
    fmt.Println("Tamanho do frame:", len(packet))
}
Enter fullscreen mode Exit fullscreen mode

Isso é possível pois fizemos uma pipe da saída do ffmpeg. Ela implementa io.Reader, o qual deve ser usado para o decoder.

Enviando o áudio para o Discord

Agora que já conseguimos extrair o áudio, vamos exemplificar como fazer um bot musical do Discord. Ressalto que, como o foco dessa postagem não é sobre o bot, e sim sobre as tecnologias e o aprendizado. Portanto, a lógica do bot será simples e não interativa, focando apenas em tocar a música.

Com tudo que construimos, podemos gerar o código final. Ele consiste em usar o discordgo para inicializar um bot, conectar a um canal de voz e, então, tocar uma música utilizando os subprocessos.

package main

import (
    "fmt"
    "log/slog"
    "os"
    "os/exec"

    "github.com/bwmarrin/discordgo"
    "github.com/jonas747/ogg"
)

// atualize com seus valores
const (
    token     = "seuToken"
    guildID   = "seuGuildID"
    channelID = "seuChannelID"
)

func main() {
    if err := mainWithError(); err != nil {
        slog.Error(err.Error())
    }
}

func mainWithError() error {
    session, err := discordgo.New("Bot " + token)
    if err != nil {
        return fmt.Errorf("failed to create bot session: %w", err)
    }

    slog.Info("Opening bot session")
    if err := session.Open(); err != nil {
        return fmt.Errorf("failed to open bot session: %w", err)
    }
    defer session.Close()

    // entrar no canal de voz
    voice, err := session.ChannelVoiceJoin(guildID, channelID, false, false)
    if err != nil {
        return fmt.Errorf("failed to join voice channel: %w", err)
    }

    // configurando fonte de áudio
    dlp := exec.Command(
        "yt-dlp",
        "--extract-audio", "https://youtu.be/jDHmg8Mb9wo",
        "-o", "-",
    )
    dlpPipe, err := dlp.StdoutPipe()
    if err != nil {
        return fmt.Errorf("failed to get yt-dlp pipe: %w", err)
    }
    dlp.Stderr = os.Stderr // obter saída informativa do yt-dlp

    ffmpeg := exec.Command(
        "ffmpeg",
        "-i", "-",
        "-f", "opus",
        "-frame_duration", "20",
        "-ar", "48000",
        "-ac", "2",
        "-",
    )
    ffmpegPipe, err := ffmpeg.StdoutPipe()
    if err != nil {
        return fmt.Errorf("failed to get ffmpeg pipe: %w", err)
    }
    ffmpeg.Stdin = dlpPipe // associa saída do yt-dlp à entrada do ffmpeg
    ffmpeg.Stderr = os.Stderr

    if err := dlp.Start(); err != nil {
        return err
    }
    if err := ffmpeg.Start(); err != nil {
        return err
    }

    pageDecoder := ogg.NewDecoder(ffmpegPipe)
    pageDecoder.Decode()
    pageDecoder.Decode()

    // sinaliza ao Discord que estamos enviando áudio
    voice.Speaking(true)
    packetDecoder := ogg.NewPacketDecoder(pageDecoder)
    for {
        packet, _, err := packetDecoder.Decode()
        if err != nil {
            // esperamos que o único erro seja io.EOF
            return err
        }

        // enviando o áudio para o Discord
        voice.OpusSend <- packet
    }
}
Enter fullscreen mode Exit fullscreen mode

Finalizando

Assim, podemos criar um bot simples de música. Para projetos reais, eu recomendaria outras abordagens, evitando o overhead e complexidade de subprocessos. Um projeto em potencial seria esse. Ainda seria necessário o uso do ffmpeg, para a geração de Opus, mas a coordenação de um único subprocesso é consideravelmente mais fácil.

Comente se aprendeu algo novo, se sobraram dúvidas, ou se encontrou algum erro. Eu não sou da área multimídia, e tudo que aprendi foi com esse projeto.

ffmpeg Article's
30 articles in total
Favicon
Desvendando Subprocessos: Criando um Bot de Música com Go
Favicon
Video data IO through ffmpeg subprocess
Favicon
Wisper, ffmpeg을 활용한 비디오 자막 자동 생성
Favicon
Integrating MinIO notifications with your Node.js service, FFmpeg, and Mozilla convert API.
Favicon
Cliet-side WebM/MP4 export from React.js Canavs Animation using ffmpeg.wasm for an Upwork client
Favicon
Reduce bitrate using FFMPEG
Favicon
Add a Watermark to a Video Using VideoAlchemy
Favicon
No Bullshit Guide to Youtube shorts automation in NodeJS, OpenAI, Ollama, ElevanLabs & ffmpeg
Favicon
Building a Video Streaming Platform with Node.js, FFmpeg, and Next.js
Favicon
Record Windows Screen using ffmpeg and convert to time lapse video
Favicon
Introducing Comet: A Free, Cross-Platform Video Converter Powered by FFmpeg
Favicon
Compress, Convert and Trim Videos with Command Line
Favicon
เผื่อใครอยากทำ mp4 to gif แบบคมๆ
Favicon
How to generate thumbnails from video ?
Favicon
Convert .caf to mp3 by Directory
Favicon
FFMPEG
Favicon
Run ffmpeg within a Docker Container: A Step-by-Step Guide
Favicon
New to DEV.to - About me
Favicon
Streaming Video to AWS MediaConnect Using FFmpeg and SRT Protocol: A Complete Guide
Favicon
Displaying a video on a ESP32 powered SSD1306 OLED screen
Favicon
FFMPEG Libraries - RTSP Client Keep Alive
Favicon
From Pixels to Playbacks: Dominate Multimedia with FFmpeg in Python
Favicon
Access webcam by ffmpeg in Windows
Favicon
OSCAR 2022 sea surface velocity streamplot animation
Favicon
Mastering Video Previews: A Guide to Compressed Videos and Thumbnails
Favicon
Dall.E Image Gen, And Size Comparison Of Image Formats
Favicon
AIS vessel density maps with pyspark and h3 and animations with ffmpeg
Favicon
Using Electron to create videos (Canvas + FFmpeg)
Favicon
BMF 📹 + Hugging Face🤗, The New Video Processing BFFs
Favicon
Leveraging GPU Acceleration in BMF for High-Performance Video Processing

Featured ones: