前書き
ディスコードで音楽かけてくれる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、音声認識、この記事では触れていませんが仮想サーバーでサービスをホスト、永続化など普段しない体験が出来て満足しました、 終わり。