eptaccio

Como eu construi um BOT para mixagem de aúdios no telegram

April 11, 2020

mesa de audio

Esse bot é uma continução do meu primeiro post onde mostro como criar um BOT e subir no now.sh.

Primeiramente peço perdão a todos DJs, produtores e pessoas que realmente mixam coisas. Isso que vou mostrar é uma brincadeira de final de semana onde tava entediado.

Eu sempre gostei muito de música, mas infelizmente nunca aprendi tocar nada. E por não saber nada de música também, nunca consegui trabalhar em produções. Esse experimento é uma forma de eu conseguir demonstrar meu amor por música.

Antes que você desista do texto, porque já escrevi demais, esse é resultado final:

Converse com o DJ Marcinho no telegram


Ingredientes

ffmpeg

Mixagem de áudio programaticamente é uma coisa muito interessante: para esse bot utilizei o ffmpeg. Uma solução de linha de comando open source para manipulação de áudio.

now.sh

Uma plataforma para funções serverless que tem limites gratuitos bem generosos para códigos OPEN SOURCE.

node.js

Ah mano, uma plataforma que usa o V8 da google para tu conseguir criar servidores que rodam javascript.


Fluxo

I - Bot aguarda evento de áudio enviado

Pra brincadeira ficar mais legal, eu geralmente adiciono esse bot em grupos do Telegram. Como já mencionado, estou utilizando o telegraf que felizmente consegue fazer a filtragem de mensagens programaticamente e separar isso em eventos para facilitar nossa vida.

bot.on(['voice', 'audio'], ctx => { // lógicas aqui pipipi popopo })

No final acabei inventando umas modas aqui, mas o conceito base é esse.

II - Envia menu com opções de áudio

menu do bot quando um áudio chega

O projeto tem uma pasta com os áudios na raiz, o bot faz uma lista com o nome dos arquivos utilizando o módulo fs. Isso serve para oferecer no menu posteriormente:

// avaibleBeats.js const { getBeatsPath } = require('lib/path') const fs = require('fs') const avaibleBeats = fs.readdirSync(getBeatsPath()) module.exports = { avaibleBeats }

À partir dessa listagem dos áudios disponíveis, montei uma lista de “callback button” (botão de retorno). O telegraf também abstrai isso de um jeito bem fácil.

const callBacks = avaibleBeats.map(beat => [Markup.callbackButton(beat, beat)] ) const inlineMessageRatingKeyboard = Markup .inlineKeyboard(callBacks).extra( // opção para responder diretamente no aúdio Extra.inReplyTo(ctx.message.message_id) ) return ctx.reply( 'Select a style', inlineMessageRatingKeyboard )

III - Faz download do áudio enviado

const saveFileLocal = async ({ filePath, bot, fileId }) => { const fileLink = await bot.telegram.getFileLink(fileId) const { data: fileStream } = await axios({ url: fileLink, responseType: 'stream' }) const writeFileStream = fs.createWriteStream(filePath) return new Promise((resolve, reject) => { writeFileStream.on('finish', resolve) writeFileStream.on('error', reject) fileStream .pipe(writeFileStream) }) }

Explicando em detalhes:

const fileLink = await bot.telegram.getFileLink(fileId)

A linha de código acima constrói o link para download do arquivo de áudio enviado pelo usuário.


const { data: fileStream } = await axios({ url: fileLink, responseType: 'stream' })

Aqui estou utilizando streams. Caso não esteja familiarizado, veja esse post sobre. Estou recebendo um fileStream da API do telegram e juntando isso à um writeFileStream, que é um stream de escrita para escrever o arquivo no disco.


return new Promise((resolve, reject) => { writeFileStream.on('finish', resolve) writeFileStream.on('error', reject) fileStream .pipe(writeFileStream) })

Como estou retornando uma promise, passei o resolve e reject da promise para os eventos de finish e error.

Esse bot está rodando em serverless no now.sh. No geral não é possivel escrever no disco de uma função serverless (perdi um tempinho para descobrir isso na primeira vez). Então todos os áudios são salvos na pasta TEMP do SO. Grazadeus no modulo os do node temos uma função chamada tmpdir que nos passa o caminho da pasta TEMP do SO em questão. Assim fica mais fácil!

Aqui acabamos entrando em um tópico de privacidade: todos os áudios são TEMPORÁRIOS, nada fica salvo em nenhuma base de dados.

IV - Aciona o ffmpeg para fazer o mix do aúdio do usário com a faixa selecionada

const { stdout, stderr } = spawn(binariePatch, args, {}) .on('error', reject) .on('close', () => resolve(outputDirectory))

Como comentei no inicio do post, estou utilizando o ffmpeg. Isso mesmo! Eu subi o binário juntamente ao deploy e estou executando o mesmo usando o metodo spawn do módulo child_process.

child_process é um modulo nativo no nodejs que permite que a gente crie novos processos à partir do processo principal. Com esse módulo, o nosso bot cria um processo no SO executando um binário (ffmpeg) com os argumentos que precisava para fazer o mix do áudio acontecer.

O módulo child_process possui alguns metodos para esse típo de ação, dentre eles: fork(), spawn(), exec() e execFile(). Estou utilizando o método spawn() lançando um novo comando no SO, recebendo como resposta um objeto do tipo ChildProcess, que implementa o EventEmitter API para nos permitir adicionar listeners (ouvintes), como fiz no código acima.

Argumentos de mix do ffmpeg:

const mergeArgs = [ '-y', '-i', filePath, '-i', beatPath, '-filter_complex', 'amerge=inputs=2', '-ac', '2', outputDirectory ]

Essa é a forma que o bot monta os args para o mix de aúdio, no fim isso vai virar algo assim:

ffmpeg -y -i kaio_do_quebradev.ogg -i NARUTO_SAD_SONG.mp3 -filter_complex amerge=inputs=2 -ac 2 final.mp3

Você pode executar isso no seu terminal se tiver o binário do ffmpeg, vai rolar!

Explicando os argumentos utilizados:

  • -y:
    Confirmação de que você quer sobrescrever o arquivo de output, caso ele já exista.
  • -i:
    Informa um INPUT, ou seja, um áudio que já existe e será utilizado.
  • -filter_complex:
    Quando precisamos fazer algo mais cabreiro no FFMPEG, usamos essa opção para conseguir chegar no resultado. O filter_complex permite que passemos como argumento para processamento do audio um “filter graph”. Como o nome já diz é bem complexo, então não vou me estender, até porque não sou um especialista no assunto.

Para utilizar esse argumento, estou sempre informando amerge=inputs=2 que quer dizer: Eu quero que você faça o merge de dois streams de áudio em um só.

  • -ac:
    Seta o número de canais de áudio. Nessa parte, a gente já começa entrar em uma ciência de foguete, coisa de quem manja de audio real (salve Gustavo). Então se quiser se aprofundar nessa parte, confira esse link

V - Envia o áudio pro usuário

E claro, a cereja do bolo: enviar o resultado para o usuário:

await ctx.replyWithAudio({ source: file })

Visão geral (programática) do processo

const selectedBeatName = info.actionName const { fileId, fileType } = getFileInfo(ctx) const filePath = getFilePath(fileId) await saveFileLocal({ filePath, bot, fileId }) const outputMixPath = await mergeAudios({ filePath, fileType, beat: selectedBeatName }) const mixResult = getFileBuffer(outputMixPath) await sendFile({ fileType, file: mixResult, ctx })

Deploy

Segui basicamente as instruções do meu primeiro post com um diferencial: na sessão de builds do now.json, informei que o projeto deveria levar os binários do FFMPEG e também as faixas de áudio disponíveis. Se atente ao includeFiles.

"builds": [ { "src": "index.js", "use": "@now/node-server", "config": { "includeFiles": [ "binaries/**", "audios/**" ] } } ]

Resultado final:

Converse com o DJ Marcinho no telegram

Código no github

Código rodando em PROD

Se tiver algum feedback ou conseguir construir algo legal à partir disso, por favor me chame no twitter ou telegram.

reginaldo

DJ Reginaldo, dono da foto do BOT.

Obrigado por ler até o final.


dev, parte do @afrotechbr, @quebradev e akatsuki. chaotic good (ele/he/el)