3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

音声認識で動くDiscord音楽botを作った

Last updated at Posted at 2023-02-11

前書き

ディスコードで音楽かけてくれるbotありますよね?
よく見かけるのって「!play ”URL”」という感じでテキストチャンネルに文字とURLを打って再生すると思うんですが、今回はボットをボイスチャンネルに参加させ、音声認識で音楽を流そうという試みです。

Discordで喋っている時に友達が「Alexa、音楽かけて」と言っているのを聞いて、ディスコード上で喋って動く音楽botあったら良いなと思い今回に至ります。

実現イメージ

・音声認識で動くDiscordBot
・Botがボイスチャンネルに参加した状態でユーザが「〇〇かけて」と言うと、Botが反応してその曲が流れるイメージ
※今回はアーティスト名や曲名を直接言う形で実装しています。

開発説明

通常のnode.js開発環境に加え、ffmpegというソフトウェアが必要になります。(音声ファイル形式をいじるので)

ライブラリ・環境

Speech-to-Text: Googleクラウド上の音声認識サービスです。
YouTube Data API: 再生にURLが必要なので、YouTube Data APIで検索しURLを取得します。
ytdl-core:YouTube動画からオーディオを抽出するためのNode.jsパッケージ。今回はYouTube Data APIで取得したURLを使って音楽再生します。
discord.js:Node.jsで書かれたDiscord APIのラッパーライブラリ。
discordjs/voice:Discordの音声通話機能を制御するためのNode.jsベースのライブラリ。

開発環境のヴァージョン等
ffmpeg version 5.1.2
npm 9.4.0
node v18.13.0

"@discordjs/opus": "0.8.0",
"@discordjs/voice": "0.14.0",
"@google-cloud/speech": "5.1.1",
"chalk": "5.2.0",
"discord.js": "14.7.1",
"dotenv": "16.0.3",
"prism-media": "1.3.4",
"youtube-node": 1.3.3",
"ytdl-core": "4.11.2"
※2023/02/11の最新パッケージを想定しています
⚠️ 音楽botの為、導入の際は自己責任でお願いいたします。

サービスフロー

ざっくり流れを説明すると、ユーザーがディスコード入る、コマンドでbotを同じボイスチャンネルに参加させる、ユーザーが喋る、喋った内容が文字としてグーグルから送られてくる→内部処理→再生の流れです。

以下詳細です。
1.Discordで喋る
2.喋った内容を録音→ローカルファイルに出力
3.ローカルファイルのままでは音声認識のデータ形式と一致しない為、ffmpegで変換します。
4.ffmpegで変換した音声ファイルをSpeech-to-Textのリクエストとして送ります。
5.Speech-to-Textから音声を文字に変換した結果がレスポンスされます。
6.結果から流したい曲のタイトルだけ抽出
7.抽出結果を使ってYouTube Data APIで再生するURLを取得
8.URLからytdl-coreで音楽を再生します

以上の流れになっています。

コード

index.js
client.on('messageCreate', message => {
  if(message.content === '!sjoin') {
    const guild = message.guild
    const vc = message.member.voice.channel;

    const connection = joinVoiceChannel({
      guildId: guild.id,
      channelId: vc.id,
      adapterCreator: guild.voiceAdapterCreator,
      selfMute: false,
      selfDeaf: false,
    });

    const receiver = connection.receiver;

    receiver.speaking.on('start', (userId) => {
      startRecognizeStream(guild, connection, userId);
    })
  }
})

//speech-to-textに送る音声ファイル形式
const encoding = 'FLAC';
const sampleRateHertz = 44100;
const languageCode = 'ja-JP';

async function startRecognizeStream(guild, connection, userId) {

  const receiver = connection.receiver;

 //recordingsディレクトリを作ってそこに録音ファイルを保存
  await fs.promises.mkdir('./recordings', { recursive: true })

    const voiceStream = receiver.subscribe(userId, { 
      end: {
        behavior: EndBehaviorType.AfterSilence,
        duration: 1000,
      }
    })
    
    const decoder = new prism.opus.Decoder({ rate: 48000, channels: 1, frameSize: 960 });

    //ファイル名は何でもOK
    const filename = `./recordings/${Date.now()}-${userId}.pcm`;

    console.log(`👂 Started recording ${filename}`);

    //音声からcreateWriteStreamでファイル作成→ファイルの作成が終わったタイミングでffmpegを実行
    const writer = voiceStream
    .pipe(decoder)
    .pipe(fs.createWriteStream(`${filename}`))

    writer.on("finish", () => {
      
      //speech-to-textに送る音声ファイル形式に変換
      const ffmpeg = exec(`ffmpeg -f s16le -ar 44.1k -ac 1 -i ${filename} ./recordings/output.flac`)

      const request = {
        config: {
          encoding: encoding,
          sampleRateHertz: sampleRateHertz,
          languageCode: languageCode,
        },
        interimResults: false,
      };
    
      const recognizeStream = speechClient
      .streamingRecognize(request)
      .on('error', console.error)
      .on('data', data => {
        console.log(
          `Transcription: ${data.results[0].alternatives[0].transcript}`
        );

        //(中略)

      });

      //ffmpegのデータ変換が終わったタイミングでrecognizeStreamを発火→発火後ディレクトリ毎削除して再帰的な処理に
      ffmpeg.on("exit", async () => {
        try {
          fs.readFileSync('./recordings/output.flac')
          fs.createReadStream('./recordings/output.flac').pipe(recognizeStream)
          await fs.promises.rm('./recordings', { recursive: true, force: true })
        } catch (error) {
          console.log(error)
        }
      })
    })
  
}

参考資料

参考:下の「ローカル ファイルでストリーミング音声認識を実行する」を参考にしました。


音声認識のデータ形式が一致しないと空のレスポンスが返ってきます。公式トラブルシューティングを参照↓

感想

無事音声認識で動くDiscord音楽Botが作れました。
途中でphpから切り替えたのでnode.js自体も初めてで、GCP、音声認識、この記事では触れていませんが仮想サーバーでサービスをホスト、永続化など普段しない体験が出来て満足しました、 終わり。
3
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?