Discord.jsの断続的な音声を穴埋めする

投稿日: 2024/04/27最終更新: 2025/06/27

ogp

この記事では、 Discord.js を用いたボイスチャットアプリケーション開発の際に便利なユーティリティ、 FillSilenceStream クラスについて詳しく解説します。
このクラスは、音声データが断続的にしか届かないような状況下でも、連続したオーディオストリームを維持する ための工夫として非常に有用です。 たとえば、音声がネットワーク遅延や間欠的なマイク入力によってブツブツと切れてしまう状況では、受信側での処理が困難になります。
そんなときに役立つのがこのクラスです。

背景と課題

リアルタイム音声ストリームを扱う場面では、常に安定したデータフローが前提とされることが多いです。
しかし現実には、

  • ネットワーク環境の変動によるパケットロス
  • マイクの入力が無音状態になる
  • 入力側で何らかの処理遅延が生じる

といった理由で、入力ストリームが空になる(または一時的に途切れる) ことがあります。

このような「無音の時間」は、オーディオ処理系においては特別な扱いが必要です。
なぜなら、音声再生や変換、さらにはボイスチェンジャーやスペクトラム解析などを行っている場合、データが来ない=処理が止まることを意味するからです。

解決策: FillSilenceStream クラス

この問題を解決するために作ったのが、 FillSilenceStream という Transform ストリームクラスです。

以下はそのコードです。

class FillSilenceStream extends stream.Transform {
    current: Buffer[];
    interval: NodeJS.Timeout;

    constructor(rate: number = 48000, channels: number = 1, frameSize: number = 960) {
        super();
        this.current = [];
        this.interval = setInterval(() => {
            if (this.current.length === 0) {
                this.push(Buffer.alloc(frameSize * 2 * channels));
            } else {
                this.push(this.current.shift());
            }
        }, 1000 / (rate / frameSize));
    }

    _write(chunk: Buffer, encoding: BufferEncoding, callback: (error?: Error | null | undefined) => void): void {
        this.current.push(chunk);
        callback();
    }

    _read() {}
}

コードの解説

constructor

  • rate: サンプリングレート(Hz)。デフォルトは 48000。
  • channels: 音声チャンネル数。モノラルなら 1、ステレオなら 2。
  • frameSize: フレームあたりのサンプル数。一般的に Opus などでは 960 サンプル程度。

コンストラクタでは、内部の current バッファに書き込まれた音声データを、定期的に出力するタイマー (setInterval) を開始します。

この間隔は、指定された rateframeSize に応じて計算されます。 たとえば、48000Hz のサンプルレートで、frameSize が 960 の場合、 rate / frameSize = 50fps になるため、20ミリ秒ごとに 1 フレーム出力されます。

_write

Transform ストリームの _write 関数は、上流(入力)から来た音声データを受け取って、バッファ current に積み上げていきます。

_read

この関数は空のままですが、Transform ストリームの仕様上問題ありません。
出力 (_read) はすべて setInterval 内で制御されています。

無音の補完とはどういうことか?

ポイントは、current.length === 0 のときの処理です。

this.push(Buffer.alloc(frameSize * 2 * channels));

これは、データが届いていない間に無音データ(ゼロで埋めたバッファ)を送出しているという意味です。

  • Buffer.alloc() は指定サイズ分のバッファを 0 で埋めた状態で確保します。
  • 16bit PCM を仮定すると、1サンプルあたり2バイト必要です。

これにより、音声が届いていない間でも常に一定の長さのデータが流れ続けるので、 下流で音声を扱っている処理系が「詰まる」「異常終了する」といったトラブルを防げます。

使用例

このストリームは、 @discordjs/voice などを使っているプロジェクトにおいて、音声入力に対して以下のように使うことができます。

const fillSilence = new FillSilenceStream();
audioInputStream.pipe(fillSilence).pipe(discordAudioPlayer);

終わりに

FillSilenceStream は非常にシンプルながら、リアルタイムオーディオ処理の現場で役立つユーティリティです。
Discord.js やボイスチャットシステムを作成している方、あるいは音声Bot開発者の方は、ぜひ活用してみてください。