ストリーミング処理って結局なに? VST3 AGain のログから見るオーディオブロックとsampleOffsetの正体

ストリーミング処理って結局なに? VST3 AGain のログから見るオーディオブロックとsampleOffsetの正体

本記事では VST3 サンプルプラグイン AGain にログ出力を仕込み、process 関数に渡されるオーディオブロックや sampleOffset の中身を数字ベースで観察します。ストリーミング処理やバッファサイズといった概念を、ゲームエンジニアやアプリエンジニアでもイメージしやすい形で整理します。
2025.12.06

はじめに

本記事では、VST3 プラグインの公式サンプルである AGain を使って、ホストとプラグインの間で行われるデータのやり取りを観察してみます。特に、process 関数が 1,024 samples ごとのオーディオブロック をどう受け取っているのかという点に焦点を当てます。

本記事のゴール

本記事を読み終えると、ざっくり次のようなイメージが持てることを目標とします。

  • VST3 プラグインの process 関数に、どういう種類のデータ が飛んでくるのか
  • なぜオーディオは 1,024 samples などのブロック単位で処理 されるのか

「AGain の中で具体的にどんなログが出るのか」を見ながら、ストリーミング処理の感覚をつかんでもらえればと思います。

対象読者

  • C++ で VST3 プラグインを書いてみたいが、オーディオの前提をあまり知らない方
  • ゲーム開発やアプリ開発の文脈で、たまたま VST に触る必要が出てきたエンジニア
  • 「ストリーミング処理」「バッファサイズ」といった用語を、具体的な例でイメージしたい方

なぜこの検証をしようと思ったか

過去にゲーム開発の現場で VST プラグインを書くことになった経験があります。当時の自分は、そもそも VST プラグインとは何か、オーディオデータがどんな単位で渡されてくるのか、そして なぜ 44,100 samples あるはずの 1 秒分の音を、1,024 samples ずつ区切って処理しても破綻しないのか といったことをきちんと理解できておらず、公式サンプルの中身を見てもピンと来ませんでした。

今回の検証は、当時の自分を少しでも救えるような記事を書きたいというモチベーションから始めています。VST やオーディオエンジニアリングの専門家向けではなく、ゲームエンジニアやアプリエンジニアが

  • 「急に VST プラグインを書いてほしいと言われた」
  • 「ストリーミング処理とかバッファとか言われてもイメージが湧かない」

という場面で、内部のデータの流れをざっくり掴むための足がかりになることを目標にしています。

VST プラグインとは何か

VST (Virtual Studio Technology) は、DAW などのホストアプリケーションと、エフェクトやシンセサイザーなどのオーディオプラグインをつなぐための共通インターフェースです。

  • ホストが、オーディオバッファやイベント、パラメータをプラグインに渡す
  • プラグインが、それをもとに信号処理を行い、処理結果をホストに返す

という約束ごとを決めたものだとイメージしてください。

VST3 SDK の MIT ライセンス化について

2025 年 10 月、Steinberg は VST 3.8 SDK を公開すると同時に、ライセンスを MIT ライセンスに変更 しました (参考)。以前は Steinberg 独自ライセンス (+ 場合によっては個別契約) が必要でしたが、現在は MIT ライセンスに従うことで、商用製品への組み込みも含めて自由に利用できる ようになりました。主な条件は、著作権表示とライセンス文を残すことです。詳しくはライセンス本文を確認してください。

一方で、VST ロゴや「VST」という商標の扱いは別 で、これらを表示したい場合は SDK に同梱されている VST Usage Guidelines に従う必要があります。「コードのライセンスはかなり自由になったが、ブランド表示にはルールがある」という点だけ頭の片隅に置いておくと安心です。

参考

VST3 プラグインの process には何が渡ってくるのか

VST3 プラグインのオーディオ処理は、AudioEffect::process という関数から始まります。

tresult PLUGIN_API AGain::process (ProcessData& data)

この ProcessData の中には、主に次の 3 種類の情報が入っています。

  • オーディオバッファ (inputs / outputs)
  • パラメータの変化 (inputParameterChanges / outputParameterChanges)
  • ノートオン / ノートオフなどのイベント (inputEvents / outputEvents)

data flow

なぜオーディオは 1,024 samples 単位で処理されるのか

いきなり AGain のコードに手を入れる前に、そもそも なぜ 1 sample ずつではなく、ブロック単位で処理するのか というストリーミング処理の前提を軽く整理しておきます。

デジタルオーディオのおさらい

デジタルオーディオは、空気の振動を一定の間隔でサンプリングした数列です。一つひとつの値を sample 、1 秒あたりに何個サンプリングするかを sample rate と呼びます。今回の検証では、オーディオインターフェースの設定が sample rate: 44.1 kHz になっていました。つまり、1 秒あたり 44,100 個の sample を扱っていることになります。

data samples

sample を 1 つずつリアルタイム処理しない理由

では、この 44,100 個の sample を、本当に 1 sample ずつプラグインに渡して処理しているのでしょうか?実際にはそうなっていません。OS や CPU は、1 sample ごとに process を呼び出すような処理はあまり得意ではありません。そこで、ある程度まとめたかたまり を単位として処理します。この「かたまりの大きさ」を、オーディオでは バッファサイズ と呼びます。

buffer processing

これはゲームでの 1 フレームごとに画面全体を描き直す処理に近いです。ゲームにおける処理でも、1 ピクセルずつ別々のタイミングで描画しているわけではなく、フレーム単位でまとめて処理 しています。

バッファサイズとレイテンシのトレードオフ

今回の環境では、ASIO ドライバの設定で Buffer Size: 1,024 samples になっていました。バッファサイズには、次のようなトレードオフがあります。

  • 小さいバッファ (例: 64 samples, 128 samples)

    • 1 ブロックの時間が短いので、入力から出力までのレイテンシが小さくなる
    • 短い間隔で頻繁に処理を回す必要があり、CPU 負荷が高くなる
    • 負荷やドライバの相性によっては、音切れ (ドロップアウト) が起きやすい
  • 大きいバッファ (例: 1,024 samples, 2,048 samples)

    • 1 ブロックの時間が長くなるので、レイテンシは大きくなる
    • 1 ブロックを処理する猶予が増え、安定はしやすい

今回の設定 (44.1 kHz, 1,024 samples) の場合、1 ブロックの長さは次のようになります。

1 ブロックの時間 [ms] = 1000 × 1024 / 44100 ≒ 23.22 ms

プラグインは「1 ブロック分ずつ」処理する

VST3 プラグインは、このオーディオバッファ 1 ブロック分を ProcessData 経由で受け取ります。AGain の process 関数の先頭付近では、このように書かれています。

// 3) Process the gain of the input buffer to the output buffer
// 4) Write the new VUmeter value to the output Parameters queue

ここで、data.numSamples は「今回のブロックに含まれる sample 数」を意味します。今回の環境では、ログを取ると常に次のようになっていました。

[again] block=8749, numSamples=1024, duration=23.220 ms, processMode=0
[again] block=8750, numSamples=1024, duration=23.220 ms, processMode=0
[again] block=8751, numSamples=1024, duration=23.220 ms, processMode=0
...

オーディオインターフェースのバッファ設定が 1,024 samples であり、ホストがその単位で process を呼んでいるという結果が、そのまま numSamples=1024 に現れています。AGain 自体が 1,024 という数字を決めているわけではありません。外側のオーディオシステムの設定が 1,024 なので、その単位でオーディオデータが流れてくる という構造になっている、と考えると理解しやすいと思います。

検証環境と準備

ここから、実際に AGain にログ出力を仕込んで観察した手順をまとめます。

検証環境

  • OS: Windows 11 (64 bit)
  • Visual Studio 2022 (MSVC)
  • VST3 SDK 3.8 系
  • Steinberg VST3PluginTestHost
  • ASIO 対応オーディオインターフェース
    • sample rate : 44.1 kHz
    • buffer size : 1,024 samples

ASIO ドライバの選択については、環境によって挙動が大きく変わることがあります。今回の手元環境では、汎用的な Realtek ASIO ドライバではうまく動かなかったため、オーディオインターフェースの専用 ASIO ドライバで動作させました。

SDK の取得とビルド

まず、VST3 SDK を取得し、公式サンプルをビルドできる状態にします。

  1. GitHub から VST3 SDK を clone します。

    git clone --recursive https://github.com/steinbergmedia/vst3sdk.git
    cd vst3sdk
    
  2. ビルド用ディレクトリを作成し、CMake で Visual Studio 用プロジェクトを生成します。

    mkdir build
    cd build
    
    cmake -G "Visual Studio 17 2022" -A x64 .. -DSMTG_CREATE_PLUGIN_LINK=OFF
    
  3. 生成された vstsdk.sln を Visual Studio で開きます。
    open solution

  4. ソリューションのプロパティを開き、 Common Properties > Configure Startup ProjectsSingle startup project > again に設定します。
    configure startup projects

  5. Configuration Properties > Configuration を開き、Configuration: Debug Platform: x64 であることを確認してから、 again の Build のみチェックを入れます。
    configuration properties

  6. プロパティのウインドウを OK で閉じます。構成が Debug、プラットフォームが x64 であることを確認します。 Build > Build Solution でビルドファイルを作成します。
    configuration and platform
    build solution

ビルドが通ると、build/VST3/Debugagain.vst3 が生成されます。

built plugin

VST3PluginTestHost の準備

VST3PluginTestHost は、フル版 VST3 SDK の同梱物として提供されています。公式サイト からフル SDK の zip を取得します。解凍後、下記のパスにある VST3PluginTestHost_x64_Installer_x.xx.xx.zip を解凍します。

VST_SDK/
  vst3sdk/
    bin/
      Windows_x64/
        VST3PluginTestHost_x64_Installer_x.xx.xx.zip

解凍して得られる VST3PluginTestHost_x64.msi を実行すると、VST3PluginTestHost がインストールされます。

VST3PluginTestHost

Visual Studio からホストを起動してデバッグする

AGain の挙動を観察するため、Visual Studio から VST3PluginTestHost を起動し、デバッガをアタッチした状態で OutputDebugString のログを眺める構成にします。

  1. Visual Studio のソリューションエクスプローラーで Plugin-Examples > again プロジェクトを右クリックし、「プロパティ」を開きます。
    open again properties

  2. Configuration: Debug Platform: x64 になっていることを確認し、 「Configuration Properties > デバッグ」を選び、次のように設定します。

    • Command : C:\path\to\VST3PluginTestHost.exe (先の手順でインストールした VST3PluginTestHost.exe のパス)
    • Command Arguments : --pluginfolder "C:\path\to\build\VST3\Debug" (先にビルドした again.vst3 が格納されたフォルダー)
      again project properties
  3. プロパティ画面を OK で閉じ、構成を Debug x64 として Local Windows Debugger または F5 を押すと、VST3PluginTestHost がデバッガ付きで起動します。
    Local Windows Debugger button
    debugger view

この状態で AGain をロードし、再生を開始すると、AGain のコードから OutputDebugStringA で出力した内容が Visual Studio の Output ウィンドウに表示されるようになります。

output viewing

AGain にログを仕込んで、ブロックの流れを観察する

ここからが本題です。AGain のソースを少しだけ改造して、process のたびに「いま何ブロック目なのか」「ブロックの長さが何 samples で、何 ms なのか」をログに出してみます。

debugLog ヘルパ関数の追加

まず、again.cpp の先頭付近に、デバッグログ用のヘルパ関数を追加します。

#include <cstdio>

#if SMTG_OS_WINDOWS
#include <windows.h>
#endif

namespace {

    inline void debugLog (const char* msg)
    {
    #if defined (SMTG_OS_WINDOWS) && defined (_DEBUG)
        ::OutputDebugStringA (msg);
    #else
        (void)msg;
    #endif
    }

} // anonymous namespace

これで、_DEBUG ビルド時に限り、debugLog を通して Output ウィンドウにログを出せるようになります。

ブロック番号カウンタの追加

次に、AGain のクラスにブロック番号を数えるカウンタを追加します。again.h のメンバ変数定義に、次のようなフィールドを足します。

Steinberg::int64 blockCounter_ = 0;
double sampleRate_ = 0.0;

setupProcessing の中で sample rate を保存します。

tresult PLUGIN_API AGain::setupProcessing (ProcessSetup& newSetup)
{
    sampleRate_ = newSetup.sampleRate;
    currentProcessMode = newSetup.processMode;
    return AudioEffect::setupProcessing (newSetup);
}

process 内でブロック情報をログ出力する

AGain::process の中、コメントで

//---3) Process Audio---------------------

と書かれているあたりの直前に、ブロック情報のログ出力を追加します。

//--- ----------------------------------
//---3) Process Audio---------------------
//--- ----------------------------------
if (data.numInputs == 0 || data.numOutputs == 0)
{
    // nothing to do
    return kResultOk;
}

// --- debug: block info ---------------------------------------------------
#if defined (_DEBUG)
const double blockMs =
    (sampleRate_ > 0.0)
        ? (1000.0 * static_cast<double> (data.numSamples) / sampleRate_)
        : 0.0;

char buf[256];
std::snprintf (
    buf, sizeof (buf),
    "[again] block=%lld, numSamples=%d, duration=%.3f ms, processMode=%d\n",
    static_cast<long long> (blockCounter_++),
    static_cast<int> (data.numSamples),
    blockMs,
    static_cast<int> (currentProcessMode));
debugLog (buf);
#endif
// -------------------------------------------------------------------------

Debug ビルドして VST3PluginTestHost から再生すると、Output ウィンドウに次のようなログが流れます。

[again] block=8749, numSamples=1024, duration=23.220 ms, processMode=0
[again] block=8750, numSamples=1024, duration=23.220 ms, processMode=0
[again] block=8751, numSamples=1024, duration=23.220 ms, processMode=0
...

ここから分かることは次の通りです。

  • data.numSamples は毎回 1,024 で一定
  • duration は約 23.22 ms で一定
  • blockCounter_ が 0, 1, 2, ... と増えている

つまり、ホストとオーディオインターフェースが決めたバッファサイズ (1,024 samples) に合わせて、AGain の process が 23 ms 前後おきに呼ばれ続けている、ということが実測で確認できました。

ノートイベントの sampleOffset を観察する

次は、ProcessData の中でもう 1 つ重要な存在である「イベント」を覗いてみます。AGain のコードでは、inputEvents を使ってノートオン / ノートオフイベントを受け取り、そのベロシティをゲインの減衰に使っています。

元のイベント処理コード

AGain の process には、すでに次のようなイベント処理が入っています。

//---2) Read input events-------------
if (IEventList* eventList = data.inputEvents)
{
    int32 numEvent = eventList->getEventCount ();
    for (int32 i = 0; i < numEvent; i++)
    {
        Event event {};
        if (eventList->getEvent (i, event) == kResultOk)
        {
            switch (event.type)
            {
                case Event::kNoteOnEvent:
                    // use the velocity as gain modifier
                    fGainReduction = event.noteOn.velocity;
                    break;

                case Event::kNoteOffEvent:
                    // noteOff reset the reduction
                    fGainReduction = 0.f;
                    break;
            }
        }
    }
}

ここにログ出力を差し込んで、「どのブロックのどの位置でノートイベントが飛んできたのか」を観察してみます。

イベント内容のログ出力を追加する

先ほどの for ループの中で、getEvent に成功した直後にログを追加します。

if (eventList->getEvent (i, event) == kResultOk)
{
#if defined (_DEBUG)
    {
        char buf[256];

        const char* typeStr = "";
        switch (event.type)
        {
            case Event::kNoteOnEvent:  typeStr = "NoteOn ";  break;
            case Event::kNoteOffEvent: typeStr = "NoteOff";  break;
            default:                   typeStr = "Other  ";  break;
        }

        // イベント種別に応じて、正しい union フィールドから読む
        int32 channel = 0;
        int32 pitch   = 0;

        switch (event.type)
        {
            case Event::kNoteOnEvent:
                channel = event.noteOn.channel;
                pitch   = event.noteOn.pitch;
                break;

            case Event::kNoteOffEvent:
                channel = event.noteOff.channel;
                pitch   = event.noteOff.pitch;
                break;

            default:
                // ここでは channel / pitch は 0 のままにしておく
                break;
        }

        std::snprintf (
            buf, sizeof (buf),
            "[again] event[%d]: %s sampleOffset=%d, channel=%d, pitch=%d\n",
            static_cast<int> (i),
            typeStr,
            static_cast<int> (event.sampleOffset),
            static_cast<int> (channel),
            static_cast<int> (pitch));
        debugLog (buf);
    }
#endif

    switch (event.type)
    {
        case Event::kNoteOnEvent:
            // use the velocity as gain modifier
            fGainReduction = event.noteOn.velocity;
            break;

        case Event::kNoteOffEvent:
            // noteOff reset the reduction
            fGainReduction = 0.f;
            break;
    }
}

再度ビルドして、VST3PluginTestHost のイベントエディタでいくつかノートを配置し、再生してみます。
すると、Output ウィンドウには次のようなログが流れました。

[again] event[0]: NoteOff sampleOffset=526, channel=0, pitch=53
[again] event[1]: NoteOn  sampleOffset=526, channel=0, pitch=38, velocity=0.750
[again] block=8749, numSamples=1024, duration=23.220 ms, processMode=0
[again] block=8750, numSamples=1024, duration=23.220 ms, processMode=0
...
[again] event[0]: NoteOff sampleOffset=287, channel=0, pitch=38
[again] event[1]: NoteOn  sampleOffset=287, channel=0, pitch=41, velocity=0.750
[again] block=8760, numSamples=1024, duration=23.220 ms, processMode=0
...

ここから読み取れるポイントは次の通りです。

  • ノートの切り替わりのタイミングで、NoteOffNoteOn がペアで飛んできている
  • sampleOffset0 以上 1,023 以下の値 になっている
  • block の番号とセットで見ると、「どのブロックの何 sample 目でイベントが発生したか」が分かる

つまり、sampleOffset は「今回のオーディオブロック (1,024 samples) の中で、先頭から何 sample 目のタイミングか」を指していると解釈できます。テストホスト側でグリッドにノートを置く位置を変えると、この sampleOffset の値も変化し、ブロックの先頭寄りのノートでは小さい値、終わり寄りのノートでは大きい値になることが確認できました。

ストリーミングとバッチを VST3 の視点で見てみる

ここまでの観察を踏まえると、VST3 の process

  • 外側から見ると「ストリームをリアルタイムに処理している」
  • プラグインの中から見ると「一定サイズのバッファを、ひたすらバッチ処理している」

という二重の顔を持っていると捉えられます。

外側から見たストリーミング

ユーザーの感覚から見ると、オーディオは途切れずに鳴り続けています。DAW のタイムラインも、時間軸に沿って連続的に進んでいきます。この意味では、オーディオは典型的なストリーミング処理です。新しい音が録音され続け、それがスピーカーから途切れずに出力されるという連続性を壊さないことが、オーディオシステムの最重要要件です。

プラグインから見たバッチ処理

一方で、AGain の process 関数はもう少し離散的です。毎回 data.numSamples samples 分のバッファが渡され、そのバッファが終わると、次のバッファがやってくる形です。つまり、プラグインは「numSamples 個の sample とイベントをまとめて渡されるので、それを処理して返してください」という 小さなバッチ処理を、ひたすら連続でこなしている ような構造になっています。

ゲームエンジンであれば「1 フレームごとに updaterender を呼ぶ」というループ構造がよく見られますが、オーディオではそれが「1 ブロック (例: 1,024 samples) ごとに process を呼ぶ」という形で現れます。

今回のログ観察によって次のようなことが具体的な数字として見えました。

  • ブロックサイズと sample rate の組み合わせが、1 回の process の時間的な長さを決めている
  • ノートイベントの sampleOffset は、そのブロックの中での相対位置を指している

まとめ

本記事では、VST3 SDK の AGain サンプルに少しだけログ出力を追加し、process 関数の中で、以下の点を実際の数字として観察しました。

  • オーディオブロックがどのような単位で渡ってくるか
  • ノートイベントの sampleOffset が何を意味しているか

ストリーミング処理と聞くと一見難しそうですが、VST3 プラグインの視点から見ると「外側からは連続した音の流れだが、内側では一定サイズのバッファをバッチ処理し続けている」という分かりやすい構造になっています。このイメージを持っておくと、ゲームやアプリのエンジニアが VST プラグインを書かなければならなくなったときでも、「まずは 1 ブロック単位で理解していこう」と落ち着いてコードを読めるのではないでしょうか。

今回の検証が、過去の自分と同じように VST3 の世界に突然放り込まれた方の、一歩目の足場になればうれしいです。

この記事をシェアする

FacebookHatena blogX

関連記事