pycordでボイスチャットの録音

Qitta初投稿です。
とりあえず練習がてら、記事を書きます。

DiscordAPIには音声を録音する機能があります。
ですがDiscord.pyではプライバシーの面で実装されておらず、Discord.jsは頻繁に変更が施されるため、簡単に録音できる手段は確立されていませんでした。

そんな中で22年3月、Discord.pyの派生ライブラリ、pycordに録音機能が実装されました。
日本語での解説が見当たらなかったので、ここで説明します。

# 環境
python 3.8.6 64bit(ここに関しては3.10とかでも問題なし)
```:pipインストール
pip install discord
pip install git+https://github.com/Pycord-Development/pycord
pip install pydub
```
***必ずDiscord.pyをインストールした後にpycordをインストールしましょう！！***
また、音声を使用するのでffmpegをインストールすることを忘れずに。

# サンプルコード

## 録音したファイルをDiscord上にアップロードする場合。

```python
import discord

intents = discord.Intents().all()
bot = discord.Bot(intents=intents)
Token=""

@bot.command()
async def start_record(ctx:discord.ApplicationContext):
    # コマンドを使用したユーザーのボイスチャンネルに接続
    try:
       vc = await ctx.author.voice.channel.connect()
       await ctx.respond("録音開始...")
    except AttributeError:
       await ctx.respond("ボイスチャンネルに入ってください。")
       return
        
    # 録音開始。mp3で帰ってくる。wavだとなぜか壊れる。
    ctx.voice_client.start_recording(discord.sinks.MP3Sink(), finished_callback, ctx)


async def finished_callback(sink:discord.sinks.MP3Sink, ctx:discord.ApplicationContext):
    # 録音したユーザーの音声を取り出す
    recorded_users = [
        f"<@{user_id}>"
        for user_id, audio in sink.audio_data.items()
    ]
    # discordにファイル形式で送信。拡張子はmp3。
    files = [discord.File(audio.file, f"{user_id}.{sink.encoding}") for user_id, audio in sink.audio_data.items()]
    await ctx.channel.send(f"録音終了! 音声ファイルはこちら! {', '.join(recorded_users)}.", files=files) 

@bot.command()
async def stop_recording(ctx:discord.ApplicationContext):
    # 録音停止
    ctx.voice_client.stop_recording()
    await ctx.respond("録音終了!")
    await ctx.voice_client.disconnect()


bot.run(Token)
```

## ローカルに保存する場合。

```python
import discord
from pydub import AudioSegment

intents = discord.Intents().all()
bot = discord.Bot(intents=intents)

Token=""

@bot.slash_command()
async def start_record(ctx:discord.ApplicationContext):

        # コマンドを使用したユーザーのボイスチャンネルに接続
        try:
            vc = await ctx.author.voice.channel.connect()
            await ctx.respond("録音開始...")
        except AttributeError:
            await ctx.respond("ボイスチャンネルに入ってください。")
            return
        
        # 録音開始。mp3で帰ってくる。wavだとなぜか壊れる。
        ctx.voice_client.start_recording(discord.sinks.MP3Sink(), finished_callback, ctx)

@bot.slash_command()
async def stop_recording(ctx:discord.ApplicationContext):
        # 録音停止
        ctx.voice_client.stop_recording() 
        await ctx.respond("録音終了!")
        await ctx.voice_client.disconnect()

# 録音終了時に呼び出される関数
async def finished_callback(sink:discord.sinks.MP3Sink, ctx:discord.ApplicationContext):
    # 録音したユーザーの音声を取り出す
    for user_id, audio in sink.audio_data.items():
        # mp3ファイルとして書き込み。その後wavファイルに変換。
        song = AudioSegment.from_file(audio.file, format="mp3")
        song.export(f"./{user_id}.wav", format='wav')


bot.run(Token)
```

ポイントとなる点はこちら。
```
# 録音開始。mp3で帰ってくる。wavだとなぜか壊れる。
ctx.voice_client.start_recording(discord.sinks.MP3Sink(), finished_callback, ctx)
```

これで録音が開始されます。
discord.sinks.MP3Sinkでmp3形式で録音することを指定しています。
これをdiscord.sinks.WaveSinkにするとwavファイル、discord.sinks.M4ASinkにするとm4aファイルとして録音を開始します。

また、finished_callbackは録音終了時に実行される関数でsink(録音データ)とctx(スラッシュコマンドのデータ)を引数として持ってきます。

sinkの中にはユーザーid毎に録音内容のバイナリデータが入っていて、forで一つ一つファイルに変換しています。

# その他
録音開始のコマンドを打っても、何か発言しなければ録音は開始されません。

例えばa,b,cの3人がボイスチャンネルにいたとします。
録音を開始し、aだけが発言した後終了するとaの録音データのみが返却されます。

ローカルに音声を保存する場合、mp3は問題ないのですが、***なぜかwavで保存するとファイルが壊れます***。

原因についてよくわからなかったのでサンプルコードではmp3にした後wavに変換しています。

(2022/09/26 追記)
また、ステージチャンネルでは録音できないようです。

# 最後に
初投稿なので色々雑な部分が多いですが、お役に立ったら幸いです。

# 参考
[stackoverflow-discord-receive-audio](https://stackoverflow.com/questions/51350976/discord-receive-audio "stackoverflow")