DirectSound を使用したドラムマシンの構築

 

Ianier Munoz
Chronotron

2004 年 2 月 2 日

概要: ゲストコラムニストの Ianier Munoz は、マネージド Microsoft DirectX ライブラリと C# を使用してドラム マシンを構築し、その場でオーディオ ストリームを合成します。

この記事のソース コードをダウンロードします

ビートを破る

(Duncan Mackenzie の概要)。

Ianier にはクールな仕事があります。彼は DJ 用のコードを記述し、Microsoft® Windows Media® Player などのコンシューマー ソフトウェアを使用してプロのデジタル信号処理 (DSP) 作業を行うことができます。 非常にきちんとした仕事、そして私たちにとって幸運なことに、彼はマネージド コードの世界を掘り下げ、Microsoft® DirectX を管理しています®。 この記事では、Ianier がデモを構築しました (図 1 を参照)。 これは、サンプリングされた音楽の複数のチャンネルを構成して再生できるマネージドドラムマシンです。 コードは実際の構成なしで動作しますが、winrythm サンプル プロジェクトを開いて実行する前に、DirectX SDK ( ここから利用可能) をダウンロードしてインストール (再起動) する必要があります。

図 1: ドラムマシンのメイン形式
(Oh yes, oh yes...boom、boom、boom..)

はじめに

DirectX 9 SDK のリリース前は、Microsoft® .NET Frameworkは無音でした。 この制限を回避できる唯一の方法は、COM 相互運用機能または P 呼び出しを使用して Microsoft® Windows® API にアクセスすることでした。

DirectX 9 のコンポーネントであるマネージド Microsoft® DirectSound® を使用すると、COM 相互運用や P 呼び出しに頼ることなく、.NET でサウンドを再生できます。 この記事では、DirectSound を使用して結果のストリームを再生し、その場でオーディオ サンプルを合成することで、簡単なドラム マシン (図 1 を参照) を実装する方法について説明します。

この記事では、C# と .NET Frameworkについて理解していることを前提としています。 オーディオ処理に関する基本的な知識も、ここで説明するアイデアをより深く理解するのに役立ちます。

この記事に付属するコードは、Microsoft® Visual Studio® .NET 2003 でコンパイルされました。 DirectX 9 SDK が必要です。 ここからダウンロードできます。

DirectSound の概要

DirectSound は DirectX のコンポーネントであり、アプリケーションはハードウェアに依存しない方法でオーディオ リソースにアクセスできます。 DirectSound では、オーディオ再生の単位はサウンド バッファーです。 サウンド バッファーは、ホスト システムのサウンド カードを表すオーディオ デバイスに属します。 DirectSound を使用してサウンドを再生するアプリケーションでは、オーディオ デバイス オブジェクトが作成され、デバイス上にバッファーが作成され、バッファーにサウンド データが入力され、バッファーが再生されます。 さまざまな DirectSound オブジェクト間の関係の詳細については、DirectX SDK ドキュメントチェック。

サウンド バッファーは、目的に応じて静的バッファーとして、またはストリーミング バッファーとして分類できます。 静的バッファーは、事前に定義されたオーディオ データを使用して 1 回初期化され、必要な回数だけ再生されます。 この種のバッファーは、通常、ショットやその他の短い効果のためにゲームで使用されます。 一方、ストリーミング バッファーは、通常、メモリに収まらないコンテンツや、テレフォニー アプリケーションのスピーカーの音声など、長さやコンテンツを事前に決定できないサウンドを再生するために使用されます。 ストリーミング バッファーは、バッファーの再生中に新しいデータで常に更新される小さなバッファーを使用して実装されます。 マネージド DirectSound は静的バッファーの優れたドキュメントと例を提供しますが、現在、ストリーミング バッファーの例がありません。

ただし、Managed DirectX にはオーディオ ストリームを再生するためのクラス 、つまり AudioVideoPlayback 名前空間の Audio クラスが含まれています。 このクラスを使用すると、WAV や MP3 など、ほとんどの種類のオーディオ ファイルを再生できます。 ただし、 Audio クラスでは、プログラムで出力デバイスを選択することはできません。また、オーディオ サンプルを変更する場合は、オーディオ サンプルにアクセスできません。

ストリーミング オーディオ プレーヤー

ストリーミング オーディオ プレーヤーを、何らかのソースからオーディオ データをプルし、何らかのデバイスを介して再生するコンポーネントとして定義しました。 一般的なストリーミング オーディオ プレーヤー コンポーネントは、サウンド カード経由で受信ストリームを再生しますが、ネットワーク経由でオーディオ ストリームを送信したり、ファイルに保存したりすることもできます。

IAudioPlayer インターフェイスには、アプリケーションがプレーヤーについて知っておくべきすべてが含まれています。 このインターフェイスを使用すると、サウンド合成エンジンを実際のプレーヤーの実装から分離することもできます。これは、この例を別の再生テクノロジを使用する別の .NET プラットフォームに移植する場合に便利です。

/// <summary>
/// Delegate used to fill in a buffer
/// </summary>
public delegate void PullAudioCallback(IntPtr data, int count);

/// <summary>
/// Audio player interface
/// </summary>
public interface IAudioPlayer : IDisposable
{
    int SamplingRate { get; }
    int BitsPerSample { get; }
    int Channels { get; }

    int GetBufferedSize();
    void Play(PullAudioCallback onAudioData);
    void Stop();
}

SamplingRateBitsPerSampleChannels の各プロパティは、プレーヤーが認識するオーディオ形式を表します。 Play メソッドは PullAudioCallback デリゲートによって提供されるストリームの再生を開始し、Stop メソッドは驚くべきことではありませんが、オーディオの再生を停止します。

PullAudioCallback では、オーディオ データのカウント バイトがデータ バッファー (IntPtr) にコピーされることを想定しています。 IntPtr 内のデータを処理すると、アプリケーションはアンマネージド コード実行のアクセス許可を必要とする関数を強制的に呼び出すので、IntPtr ではなくバイト配列を使用する必要があると思うかもしれません。 ただし、マネージド DirectSound にはこのようなアクセス許可が必要であるため、 IntPtr を使用しても大きな影響はなく、さまざまなサンプル形式や他の再生テクノロジを扱うときに余分なデータコピーを回避できます。

GetBufferedSize は、 PullAudioCallback デリゲートの最後の呼び出し以降にプレーヤーにキューに登録されているバイト数を返します。 このメソッドを使用して、入力ストリームに対する現在の再生位置を計算します。

DirectSound を使用した IAudioPlayer の実装

前に説明したように、DirectSound のストリーミング バッファーは、バッファーの再生中に常に新しいデータで更新される小さなバッファーにすぎない。 StreamingPlayer クラスは、ストリーミング バッファーを使用して IAudioPlayer インターフェイスを実装します。

StreamingPlayer コンストラクターを見てみましょう。

public StreamingPlayer(Control owner, 
        Device device, WaveFormat format)
{
    m_Device = device;
    if (m_Device == null)
    {
        m_Device = new Device();
        m_Device.SetCooperativeLevel( 
            owner, CooperativeLevel.Normal);
        m_OwnsDevice = true;
    }

    BufferDescription desc = new BufferDescription(format);
    desc.BufferBytes = format.AverageBytesPerSecond;
    desc.ControlVolume = true;
    desc.GlobalFocus = true;

    m_Buffer = new SecondaryBuffer(desc, m_Device);
    m_BufferBytes = m_Buffer.Caps.BufferBytes;

    m_Timer = new System.Timers.Timer( 
          BytesToMs(m_BufferBytes) / 6);
    m_Timer.Enabled = false;
    m_Timer.Elapsed += new System.Timers.ElapsedEventHandler(Timer_Elapsed);
}

StreamingPlayer コンストラクターは、まず、操作する有効な DirectSound オーディオ デバイスがあることを確認し、何も指定されていない場合は新しいデバイスを作成します。 Device オブジェクトを作成するには、DirectSound がアプリケーション フォーカスの追跡に使用する Microsoft® Windows フォーム コントロールを指定する必要があります。したがって、owner パラメーターです。 DirectSound SecondaryBuffer インスタンスが作成されて初期化され、タイマーが割り当てられます。 私はこのタイマーの役割にすぐに戻ってきます。

IAudioPlayer.StartIAudioPlayer.Stop の実装は非常に簡単です。 Play メソッドを使用すると、再生するオーディオ データが確実に存在します。タイマーを有効にし、バッファーの再生を開始します。 対称的に、 Stop メソッドはタイマーを無効にし、バッファーを停止します。

public void Play( 
     Chronotron.AudioPlayer.PullAudioCallback pullAudio)
{
    Stop();

    m_PullStream = new PullStream(pullAudio);

    m_Buffer.SetCurrentPosition(0);
    m_NextWrite = 0;
    Feed(m_BufferBytes);
    m_Timer.Enabled = true;
    m_Buffer.Play(0, BufferPlayFlags.Looping);
}

public void Stop()
{
    if (m_Timer != null)
        m_Timer.Enabled = false;
    if (m_Buffer != null)
        m_Buffer.Stop();
}

この考え方は、デリゲートから送信されるサウンド データをバッファーに継続的に送り込み続けることです。 この目標を達成するために、タイマーは既に再生されているオーディオ データの量を定期的にチェックし、必要に応じてバッファーにさらにデータを追加します。

private void Timer_Elapsed( 
          object sender, 
          System.Timers.ElapsedEventArgs e)
{
    Feed(GetPlayedSize());
}

GetPlayedSize 関数は、バッファーの PlayPosition プロパティを使用して、再生カーソルが進んだバイト数を計算します。 バッファーはループ内で再生されるため、 GetPlayedSize は再生カーソルがラップするタイミングを検出し、それに応じて結果を調整する必要があることに注意してください。

private int GetPlayedSize()
{
    int pos = m_Buffer.PlayPosition;
    return 
       pos < m_NextWrite ? 
       pos + m_BufferBytes - m_NextWrite 
       : pos - m_NextWrite;
}

バッファーを埋めるルーチンは Feed と呼ばれ、次のコードに示されています。 このルーチンは SecondaryBuffer.Write を呼び出します。これにより、ストリームからオーディオ データがプルされ、バッファーに書き込まれます。 この場合、ストリームは Play メソッドで受け取った PullAudioCallback デリゲートのラッパーにすぎません。

private void Feed(int bytes)
{
    // limit latency to some milliseconds
    int tocopy = Math.Min(bytes, MsToBytes(MaxLatencyMs));

    if (tocopy > 0)
    {
        // restore buffer
        if (m_Buffer.Status.BufferLost)
            m_Buffer.Restore();

        // copy data to the buffer
        m_Buffer.Write(m_NextWrite, m_PullStream, 
                       tocopy, LockFlag.None);

        m_NextWrite += tocopy;
        if (m_NextWrite >= m_BufferBytes)
            m_NextWrite -= m_BufferBytes;
    }
}

再生待ち時間を短縮するために、バッファーに追加するデータの量を一定の制限の下に強制します。 待機時間は、受信オーディオ ストリームの変更が発生した時刻と、変更が実際に聞こえる時間の差として定義できます。 このような待機時間制御がないと、平均待機時間は合計バッファー長とほぼ同じになり、リアルタイム シンセサイザーでは受け入れられない可能性があります。

ドラムマシンエンジン

ドラムマシンは、リアルタイムシンセサイザーの例です。ドラムの演奏をシミュレートするために、可能なドラムサウンド(音楽専門用語では「パッチ」とも呼ばれます)を表すサンプル波形のセットが、いくつかのリズムパターンに従って出力ストリームに混合されます。 これは聞こえるのと同じくらい簡単なので、コードを詳しく調べてみましょう。

コア

ドラムマシンのメイン要素は、パッチトラックミキサーの各クラスに実装されています (図 2 を参照)。 これらはすべて、Rhythm.cs で実装されています。

画像を拡大するには、ここをクリックしてください。

図 2. Rhythm.cs のクラス図

Patch クラスは、特定の計測器の波形を保持します。 Patch は、WAV 形式のオーディオ データを含む Stream オブジェクトで初期化されます。 ここでは WAV ファイルの読み取りの詳細については説明しませんが、 WaveStream ヘルパー クラスを見て全体像を取得できます。

わかりやすくするために、 Patch は左と右の両方のチャネル (指定されたファイルがステレオの場合) を追加してオーディオ データをモノラルに変換し、結果を 32 ビット整数の配列に格納します。 実際のデータ範囲は -32768 +32767 であるため、オーバーフローを処理することなく複数のオーディオ ストリームを混在させることができます。

PatchReader クラスを使用すると、Patch からオーディオ データを読み取り、それを宛先バッファーに混在できます。 1 つの パッチ が異なる位置で複数回再生される可能性があるため、リーダーを実際の パッチ データから分離する必要があります。 これは特に、非常に短い時間に同じ音が複数回発生した場合に発生します。

Track クラスは、1 つのインストルメントを使用して再生するイベントのシーケンスを表します。 トラックは 、Patch、多数のタイム スロット (可能なビート位置)、および必要に応じて初期パターンで初期化されます。 パターンはブール値の配列であり、トラック内のタイム スロットの数と等しい長さです。配列の要素を true に設定すると、選択した Patch がそのビート位置で再生されます。 Track.GetBeat メソッドは、特定のビート位置の PatchReader インスタンスを返します。現在のビートで何も再生しない場合は null を返します。

Mixer クラスは、一連のトラックを指定して実際のオーディオ ストリームを生成するため、PullAudioCallback シグネチャに一致するメソッドを実装します。 ミキサーは、現在のビート位置と、現在再生中の PatchReader インスタンスの一覧も追跡します。

最も難しい作業は DoMix メソッド内で行われます。これは、次のコードで確認できます。 ミキサーは、ビート継続時間に対応するサンプルの数を計算し、出力ストリームが合成されるときに現在のビート位置を進めます。 サンプルブロックを生成するために、ミキサーは現在のビートで再生されているパッチを追加するだけです。

private void DoMix(int samples)
{
    // grow mix buffer as necessary
    if (m_MixBuffer == null || m_MixBuffer.Length < samples)
        m_MixBuffer = new int[samples];

    // clear mix buffer
    Array.Clear(m_MixBuffer, 0, m_MixBuffer.Length);

    int pos = 0;
    while(pos < samples)
    {
        // load current patches
        if (m_TickLeft == 0)
        {
            DoTick();
            lock(m_BPMLock)
                m_TickLeft = m_TickPeriod;
        }

        int tomix = Math.Min(samples - pos, m_TickLeft);

        // mix current streams
        for (int i = m_Readers.Count - 1; i >= 0; i--)
        {
            PatchReader r = (PatchReader)m_Readers[i];
            if (!r.Mix(m_MixBuffer, pos, tomix))
                m_Readers.RemoveAt(i);
        }

        m_TickLeft -= tomix;
        pos += tomix;
    }
}

特定のテンポのタイムスロットに対応するオーディオサンプルの数を計算するために、ミキサーは式(SamplingRate * 60 / BPM)/Resolutionを使用します。SamplingRateはヘルツで表されるプレーヤーのサンプリング周波数です。解像度は1ビートあたりのスロット数です。BPM は、1 分あたりのビート数で表されるテンポです。 BPM プロパティは、この数式を適用して、m_TickPeriodメンバー変数を初期化します。

まとめ

ドラムマシンを実装するために必要なすべての要素が揃ったので、物事を動かすために残っているのは、それらを結び付けるだけです。 操作のシーケンスを次に示します。

  • ストリーミング オーディオ プレーヤーを作成します。
  • ミキサーを作成します。
  • WAVファイルまたはリソースからドラムパッチ(サウンド)のセットを作成します。
  • ミキサーにトラックのセットを追加して、目的のパッチを再生します。
  • 再生する各トラックのパターンを定義します。
  • データ ソースとしてミキサーを使用してプレーヤーを起動します。

以下のコードでわかるように、 RythmMachineApp クラスはまさにこれを行います。

public RythmMachineApp(Control control, IAudioPlayer player)
{
    int measuresPerBeat = 2;

    Type resType = control.GetType();
    Mixer = new Chronotron.Rythm.Mixer(
          player, measuresPerBeat);
    Mixer.Add(new Track("Bass drum", 
          new Patch(resType, "media.bass.wav"), TrackLength));
    Mixer.Add(new Track("Snare drum", 
          new Patch(resType, "media.snare.wav"), TrackLength));
    Mixer.Add(new Track("Closed hat", 
          new Patch(resType, "media.closed.wav"), TrackLength));
    Mixer.Add(new Track("Open hat", 
          new Patch(resType, "media.open.wav"), TrackLength));
    Mixer.Add(new Track("Toc", 
          new Patch(resType, "media.rim.wav"), TrackLength));
    // Init with any preset
    Mixer["Bass drum"].Init(new byte[] 
          { 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0 } );
    Mixer["Snare drum"].Init(new byte[] 
          { 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0 } );
    Mixer["Closed hat"].Init(new byte[] 
          { 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0 } );
    Mixer["Open hat"].Init(new byte[] 
          { 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1 } );
    BuildUI(control);
    m_Timer = new Timer();
    m_Timer.Interval = 250;
    m_Timer.Tick += new EventHandler(m_Timer_Tick);
    m_Timer.Enabled = true;
}

これで完成です。 コードの残りの部分は、ユーザーがコンピュータ画面にリズミカルパターンを作成できるようにドラムマシンのためのシンプルなユーザーインターフェイスを実装しています。

まとめ

この記事では、マネージド DirectSound API を使用してストリーミング バッファーを作成する方法と、その場でオーディオ ストリームを生成する方法について説明します。 私はあなたが提供されたサンプルコードで遊んでいくつかの楽しみを持っていることを願っています。 また、パターンの読み込みと保存のサポート、テンポを変更するためのユーザー インターフェイス コントロール、ステレオ再生など、さまざまな改善を行うことも検討できます。 私はすべての楽しみを持っているのは公平ではないでしょうので、私はあなたのためにこれらを残します...

最後に、Duncan が Coding4Fun 列にこの記事を投稿してくれたことに感謝します。 私はあなたが私がそれを書くのを楽しんだのと同じくらいこのコードで遊ぶことを楽しむことを願っています。 今後の記事では、ドラムマシンをコンパクトフレームワークに移植して、ポケット PCで実行する方法について説明します。

コーディングの課題

これらの Coding4Fun 列の最後に、Duncan には通常、少しコーディングの課題があります。関心がある場合は、作業に取り組む必要があります。 この記事を読んだ後、私の例に従って DirectX で動作するいくつかのコードを作成するようにチャレンジしたいと思います。 GotDotNet に生成した内容を投稿し、Duncan に電子メール メッセージ (でduncanma@microsoft.com) を送信し、自分が行ったことと、なぜそれが面白いと感じているかを説明します。 必要に応じて Duncan にアイデアを送信できますが、サンプル自体ではなく、コード サンプルへのリンクを送信してください。

趣味のコンテンツに関する独自のアイデアをお持ちですか? ゲスト列学者として表示する他のユーザー で Duncan にお duncanma@microsoft.com知らせください。

リソース

この記事の中核となるのは、ここから入手できる DirectX 9 SDK を使用して作成されましたが、 の MSDN ライブラリhttps://msdn.microsoft.com/nhp/default.asp?contentid=28000410の DirectX セクションもチェックする必要があります。 このトピックのマルチメディアの概要を探している場合は、.NET ショー (https://msdn.microsoft.com/theshow/episode037/default.asp) のエピソードもマネージド DirectX に焦点を当てていました。

 

Coding4Fun

Ianier Munoz はフランスのメスに住み、ルクセンブルクの国際コンサルティング会社のシニア コンサルタント兼アナリストとして働いています。 クロノトロン、Adapt-X、アメリカンDJのプロミックスなど、人気のあるマルチメディアソフトウェアを執筆。 あなたはで彼に http://www.chronotron.com到達することができます.