ボイチャ録音できる！DiscordBotの開発

# はじめに
`Discord Bot作るならPython一択だよなぁ↑`という思考の元(?)Discord Botを開発されている皆さん、録音機能を実装したくなったことありませんか？

私が調べた限りでは、録音機能の実装方法を紹介をしている記事は少なく、数少ない検索結果の中にあったとある記事を読んでみましたが、DiscordのAPIを直接操作しているようで、難易度が高いように感じました。
しかし、Pycordというライブラリを使うことで、DiscordのAPIを直接操作しなくても簡単に録音機能を実装できちゃいます！

そんなわけで、今回はボイチャの録音ができるDiscord Botを作ってみたいと思います。
それでは早速作っていきましょう！

:::note warn
この記事はある程度Pythonが扱えることを前提とした表記・表現等があります。予めご了承ください。
:::

### 実行環境
 - OS: `Ubuntu Server 22.04`
 - Python: `v3.10.8` (Anaconda)
 - pycord: `v2.3.2`
 - ffmpeg: `v4.4.2`

# Botの制作
実行環境にさらっと書きましたが、`ffmpeg`が利用できることが**前提条件**としてあります。
Ubuntu等をご利用の方は、`sudo apt install ffmpeg`と実行するだけで良いのですが、Windowsをご利用の方は公式サイトからダウンロード後に**環境変数の設定を変更する必要があります**ので、若干手間がかかります。
この辺は本記事では省きますので、詳しくはググるなりなんなりしてください。

## ライブラリをインストール
使うライブラリは`pycord`です。PythonでDiscord Botを作ると言ったら`discord.py`が有名ですが、このライブラリは録音の非対応っぽいので使いません。
ボイスチャンネルのサポートが必要なので、`[voice]`を追加しておきます。

```bash
$ pip install -U "py-cord[voice]"
```

## Botのベースを作る
まずは、ベースとなるコードを書いていきます。とりあえずはボイスチャンネルに参加・退出できるようにしておきます。
ここはメインではないので、コードの説明は省きます。

```bot.py
import discord

bot = discord.Bot()

@bot.slash_command(description="ボイスチャンネルに参加します。")
async def join(
    ctx,
):
    if ctx.author.voice is None:
        embed = discord.Embed(title="エラー",description="あなたがボイスチャンネルに参加していません。",color=discord.Colour.red())
        await ctx.respond(embed=embed)
        return
    try:
        await ctx.author.voice.channel.connect()
    except:
        embed = discord.Embed(title="エラー",description="ボイスチャンネルに接続できません。\nボイスチャンネルの権限を確認してください。",color=discord.Colour.red())
        await ctx.respond(embed=embed)
        return
    embed = discord.Embed(title="成功",description="ボイスチャンネルに接続しました。",color=discord.Colour.green())
    await ctx.respond(embed=embed)

@bot.slash_command(description="ボイスチャンネルから切断します。")
async def leave(
    ctx,
):
    if ctx.guild.voice_client is None:
        embed = discord.Embed(title="エラー",description="ボイスチャンネルに接続していません。",color=discord.Colour.red())
        await ctx.respond(embed=embed)
        return
    await ctx.guild.voice_client.disconnect()
    embed = discord.Embed(title="成功",description="ボイスチャンネルから切断しました。",color=discord.Colour.green())
    await ctx.respond(embed=embed)

bot.run("Bot-Token")
```

## 録音機能の実装
それでは録音機能を先程のコードに実装していきます。

`Audio_Format`という変数に辞書型で定義しているのは、`Discord.Sink`というオブジェクトです。このオブジェクトは**各録音ごとに再生成する必要があり**、自分はここでハマりました。

`finished_callback`という関数は録音を停止したときに呼び出されます。呼び出す関数は、`start_recording`の部分で指定しています。pycordの仕様上、録音はユーザーごとに行われますので、全ユーザーの録音ファイルをアップロードするという処理を`for`文で行っています。

:::note warn
この方法ではDiscordの仕様上、ファイルサイズが大きくなるとアップロードに失敗してしまいます。
そのため、実際に運用する際は一度ローカル上に書き出しを行い、ダウンロード用のURLを発行するなりしたほうが良いと思います。（私のBotではpydubを使うことで書き出しをしています。）
:::

```python
# 先程のコードがあるという前提で追加します

@bot.slash_command(description="録音を開始します。")
async def record(
    ctx,
    format: Option(str, '録音フォーマットを選んでください。', choices=["MP3", "WAV", "PCM", "OGG", "MKA", "KMV", "MP4", "M4A"]),
):
    format = str(f"{format}")
    Audio_Format = {
        "MP3": discord.sinks.MP3Sink(),
        "WAV": discord.sinks.WaveSink(),
        "PCM": discord.sinks.PCMSink(),
        "OGG": discord.sinks.OGGSink(),
        "MKA": discord.sinks.MKASink(),
        "MKV": discord.sinks.MKVSink(),
        "MP4": discord.sinks.MP4Sink(),
        "M4A": discord.sinks.M4ASink()
    }
    format_sink = Audio_Format[format]
    if ctx.guild.voice_client is None:
        embed = discord.Embed(title="エラー",description="ボイスチャンネルに接続していません。",color=discord.Colour.red())
        await ctx.respond(embed=embed)
        return
    ctx.voice_client.start_recording(format_sink, finished_callback, ctx)
    embed = discord.Embed(title="成功",description="ボイスチャンネルの録音を開始しました。",color=discord.Colour.green())
    await ctx.respond(embed=embed)

@bot.slash_command(description="録音を停止します。")
async def record_stop(
    ctx,
):
    if ctx.guild.voice_client is None:
        embed = discord.Embed(title="エラー",description="ボイスチャンネルに接続していません。",color=discord.Colour.red())
        await ctx.respond(embed=embed)
        return
    ctx.voice_client.stop_recording()
    embed = discord.Embed(title="成功",description="ボイスチャンネルの録音を停止しました。",color=discord.Colour.green())
    await ctx.respond(embed=embed)

async def finished_callback(sink, ctx, *args):
    recorded_users = [f"<@{user_id}>" for user_id, audio in sink.audio_data.items()]
    files = [discord.File(audio.file, f"{user_id}.{sink.encoding}") for user_id, audio in sink.audio_data.items()]
    await ctx.respond(f"録音が完了しました！\n録音されたユーザー: {', '.join(recorded_users)}.", files=files)
```

なんとこれだけで録音Botの完成です！お疲れさまでした。
最後にコードの全体像を貼っておきます。

### 余談
録音中、データはどこに書き込まれているのかということですが、**メモリー**です。私が試した限りでは、録音中はそこそこのメモリー消費をするので、試す際はメモリーには余裕をもって実行してください。

## 全体像
```bot.py
import discord

bot = discord.Bot()

@bot.slash_command(description="ボイスチャンネルに参加します。")
async def join(
    ctx,
):
    if ctx.author.voice is None:
        embed = discord.Embed(title="エラー",description="あなたがボイスチャンネルに参加していません。",color=discord.Colour.red())
        await ctx.respond(embed=embed)
        return
    try:
        await ctx.author.voice.channel.connect()
    except:
        embed = discord.Embed(title="エラー",description="ボイスチャンネルに接続できません。\nボイスチャンネルの権限を確認してください。",color=discord.Colour.red())
        await ctx.respond(embed=embed)
        return
    embed = discord.Embed(title="成功",description="ボイスチャンネルに接続しました。",color=discord.Colour.green())
    await ctx.respond(embed=embed)

@bot.slash_command(description="ボイスチャンネルから切断します。")
async def leave(
    ctx,
):
    if ctx.guild.voice_client is None:
        embed = discord.Embed(title="エラー",description="ボイスチャンネルに接続していません。",color=discord.Colour.red())
        await ctx.respond(embed=embed)
        return
    await ctx.guild.voice_client.disconnect()
    embed = discord.Embed(title="成功",description="ボイスチャンネルから切断しました。",color=discord.Colour.green())
    await ctx.respond(embed=embed)

@bot.slash_command(description="録音を開始します。")
async def record(
    ctx,
    format: Option(str, '録音フォーマットを選んでください。', choices=["MP3", "WAV", "PCM", "OGG", "MKA", "KMV", "MP4", "M4A"]),
):
    format = str(f"{format}")
    Audio_Format = {
        "MP3": discord.sinks.MP3Sink(),
        "WAV": discord.sinks.WaveSink(),
        "PCM": discord.sinks.PCMSink(),
        "OGG": discord.sinks.OGGSink(),
        "MKA": discord.sinks.MKASink(),
        "MKV": discord.sinks.MKVSink(),
        "MP4": discord.sinks.MP4Sink(),
        "M4A": discord.sinks.M4ASink()
    }
    format_sink = Audio_Format[format]
    if ctx.guild.voice_client is None:
        embed = discord.Embed(title="エラー",description="ボイスチャンネルに接続していません。",color=discord.Colour.red())
        await ctx.respond(embed=embed)
        return
    ctx.voice_client.start_recording(format_sink, finished_callback, ctx)
    embed = discord.Embed(title="成功",description="ボイスチャンネルの録音を開始しました。",color=discord.Colour.green())
    await ctx.respond(embed=embed)

@bot.slash_command(description="録音を停止します。")
async def record_stop(
    ctx,
):
    if ctx.guild.voice_client is None:
        embed = discord.Embed(title="エラー",description="ボイスチャンネルに接続していません。",color=discord.Colour.red())
        await ctx.respond(embed=embed)
        return
    ctx.voice_client.stop_recording()
    embed = discord.Embed(title="成功",description="ボイスチャンネルの録音を停止しました。",color=discord.Colour.green())
    await ctx.respond(embed=embed)

async def finished_callback(sink, ctx, *args):
    recorded_users = [f"<@{user_id}>" for user_id, audio in sink.audio_data.items()]
    files = [discord.File(audio.file, f"{user_id}.{sink.encoding}") for user_id, audio in sink.audio_data.items()]
    await ctx.respond(f"録音が完了しました！\n録音されたユーザー: {', '.join(recorded_users)}.", files=files)

bot.run("Bot-Token")
```

:::note warn
上記のコードはあくまで録音機能の実装方法の紹介の為に、**爆速コーディング**したものです。**エラー処理等をしていません**ので、実際に運用される場合はその辺りも考慮した実装をしてください。
:::

# まとめ
今回はpycordを使ってボイチャ録音できるDiscordBotを作ってみました。
ライブラリ側で対応しているので非常に扱いは簡単だと思います。みなさんも是非つくってみてください。
不具合等をありましたら、コメントください。

最後に、よろしければ私が作ったSmartAid Botを導入してみてください。録音機能を備えています。詳細は[**こちら**](https://mwindows99.com/discordbot/?utm_source=qiita&utm_medium=qiita&utm_campaign=bot_link_clicked)から！

今回は以上です。

# 参考文献
 - [pycord 公式ドキュメント](https://docs.pycord.dev/en/stable/)
 - [pycord Githubリポジトリ](https://github.com/Pycord-Development/pycord)