UI 最前線

WPF アプリケーションでの音の生成

Charles Petzold

数週間前、私はトヨタの新しいプリウスの社内に座って、レンタカー会社のセールスマンがダッシュボードに並んだ見慣れないコントロール パネルやインジケータについて説明するのを聞いていました。私は、「自動車のような古いテクノロジでも、メーカーは絶えずユーザー インターフェイスを改善しているんだな」と驚きました。

広い意味では、ユーザー インターフェイスとは、人間と機械が対話する場所です。概念はテクノロジ自体と同じくらい古くからありますが、ユーザー インターフェイスは、パーソナル コンピューターの大変革によって初めて本当に花開きました。

Apple Macintosh や Microsoft Windows のグラフィカル ユーザー インターフェイスが登場する前の時代について思い出せるのは、現在のパーソナル コンピューター ユーザーのほんの一握りでしょう。当時 (1980 年代半ばから後半)、一部の評論家は、ユーザー インターフェイスの標準化によりアプリケーションが耐えがたいほど画一化されることを懸念していました。実際はそうはなりませんでした。むしろ、標準のコントロールを使用できるようになったことで、デザイナーやプログラマはスクロール バーを最初から作り直す必要がなくなり、実際のところ、ユーザー インターフェイスは進化し始め、どんどん魅力的なものになりました。

この点において、Windows Presentation Foundation (WPF) によって持ち込まれた新しいパラダイムは、ユーザー インターフェイスをはるかに優れたものにすることを可能にしました。WPF には、保持モードのグラフィックス、アニメーション、および 3D という強固な基盤があり、さらに、親要素と子要素のツリー ベースの階層構造や、XAML という強力なマークアップ言語もサポートしています。そのため、かつてないほど柔軟に、テンプレートを通じて既存のコントロールをカスタマイズしたり、既存のコンポーネントを組み合わせて新しいコントロールを構築したりすることができます。

しかし、こうした新しい概念はクライアント プログラミングのためだけのものではありません。Microsoft .NET Framework、XAML、および WPF クラスのかなりの部分が、Silverlight を通じて Web ベースのプログラミングで使用できるようになりました。クライアント アプリケーションと Web アプリケーションの間でカスタム コントロールを実際に共有できる日が既に来ているのです。この傾向は、マルチタッチなどの新しいテクノロジを活用しながら、モバイル アプリケーションへと続き、ゆくゆくはさまざまな種類の情報やエンターテイメントのシステムにも広がるでしょう。

こうした理由から、私は、ユーザー インターフェイスがアプリケーション プログラミングのさらに重要な一部になったと確信しています。今回のコラムでは、WPF と Silverlight でのユーザー インターフェイス デザインの可能性を探り、できる限りプラットフォームに依存しないコードを使用します。

合図の音を鳴らす

ユーザー インターフェイス上の良い選択と悪い選択をすぐに見極めることは常に可能とは限りません。Clippy (Microsoft Office 97 で初めて登場した擬人化されたクリップ) も、当時はおそらく良いアイデアに思えたのでしょう。そのため、ここでは、デザインよりも技術的な可能性に重点を置き、"ベスト プラクティス" という言葉はなるべく使わないようにします。それは歴史や市場の問題です。

たとえば、ユーザーからの特定の命令に応じてビデオやサウンド ファイルを再生しているとき以外はコンピューターは音を立ててはならないという主張には十分な言い分があるかもしれません。私は、この批評を無視して、実行時に波形データを生成することにより WPF アプリケーションでカスタム音を再生する方法を紹介します。

この、音の発生機能はまだ正式に .NET Framework の一部にはなっていませんが、Codeplex (naudio.codeplex.com、英語) で入手できる NAudio ライブラリを使用して実現することができます。Codeplex サイトからリンクをたどっていくと、サンプル コードに関する Mark Heath のブログをチェックしたり、Sebastian Gray のサイト チュートリアルをチェックしたりすることができます。

NAudio ライブラリは、Windows フォーム アプリケーションまたは WPF アプリケーションで使用することができます。このライブラリは PInvoke を使用して Win32 API 関数にアクセスするため、Silverlight では使用することはできません。

今回のコラムでは、NAudio Version 1.3.8 を使用しました。NAudio を使用するプロジェクトを作成する場合、32 ビット処理用にコンパイルする必要があります。[Properties] (プロパティ) ページの [Build] (ビルド) タブで、[Platform Target] (プラットフォーム ターゲット) ボックスの一覧から [x86] をクリックします。

ライブラリには、音を使用する必要がある特殊なアプリケーションのための機能も多く含まれていますが、今回は、より汎用的なアプリケーションに役立つ可能性のある技法を紹介します。

たとえば、ユーザーがウィンドウの周りでオブジェクトをドラッグできるアプリケーションがあるとします。そして、皆さんは、このようなドラッグ操作を行うと、オブジェクトがウィンドウの中心から離れれば離れるほど周波数が高くなる簡単な音 (正弦波など) が発生させたいと考えているとします。

これは波形オーディオの出番です。

最近ではほぼすべての PC に音を生成するハードウェアが搭載されており、それは多くの場合 1 ~ 2 個のチップとしてマザーボードに直接実装されています。通常、こうしたハードウェアは 1 組のデジタル アナログ コンバーター (DAC) にすぎません。波形を表現する整数の連続ストリームを 2 つの DAC に配信すると、ステレオ音が発生します。

どのくらいのデータが必要でしょうか。最近のアプリケーションは通常、"CD 品質" の音を生成します。サンプリング レートは一定で毎秒 44,100 サンプルです (ナイキスト定理によると、再現するにはサンプリング レートは少なくとも最高周波数の 2 倍である必要があります。人間には一般に周波数が 20 ~ 20,000 Hz の音が聞こえると言われているため、44,100 なら十分です)。各サンプルは符号付き 16 ビット整数です。これは、信号対雑音比が 96 デシベルとなるサイズです。

波を作る

Win32 API は、waveOut という単語から始まる一連の関数を通じて音生成ハードウェアにアクセスできるようにします。NAudio ライブラリは、Win32 相互運用性に対処し、厄介な処理の多くを隠ぺいする WaveOut クラス内にこうした関数をカプセル化します。

WaveOut を利用するには、IWaveProvider インターフェイスを実装するクラスを用意する必要があります。つまり、このクラスで、(少なくとも) サンプリング レートとチャネルの数を示す、WaveFormat 型の取得可能プロパティを定義します。また、このクラスでは、Read というメソッドも定義します。Read メソッドの引数には、このクラスが波形データを設定する必要があるバイト配列のバッファーがあります。既定の設定では、この Read メソッドは 1 秒間に 10 回呼び出されます。このバッファーにデータを設定するのが少し遅れると、音が不格好に途切れたり不快な雑音が発生したりします。

NAudio には、IWaveProvider を実装し一般的な
オーディオ処理を少し容易にする抽象クラスがいくつか用意されています。WaveProvider16 クラスは、バッファーに byte データではなく short データを設定することを可能にする抽象メソッド Read を実装するため、サンプルを半分ずつ分ける必要はありません。

図 1 は、WaveProvider16 から派生する単純な SineWaveOscillator クラスを示しています。コンストラクターでは、サンプリング レートを指定できますが、モノラル音用の 1 チャネルを示す 2 番目の引数を使用して基本クラスのコンストラクターを呼び出します。

図 1 NAudio 用の正弦波サンプルを生成するクラス

class SineWaveOscillator : WaveProvider16 {
  double phaseAngle;

  public SineWaveOscillator(int sampleRate): 
    base(sampleRate, 1) {
  }

  public double Frequency { set; get; }
  public short Amplitude { set; get; }

  public override int Read(short[] buffer, int offset, 
    int sampleCount) {

    for (int index = 0; index < sampleCount; index++) {
      buffer[offset + index] = 
        (short)(Amplitude * Math.Sin(phaseAngle));
      phaseAngle += 
        2 * Math.PI * Frequency / WaveFormat.SampleRate;

      if (phaseAngle > 2 * Math.PI)
        phaseAngle -= 2 * Math.PI;
    }
    return sampleCount;
  }
}

SineWaveOscillator では、(double 型の) Frequency と (short 型の) Amplitude という 2 つのプロパティを定義しています。プログラムでは、常に 0 ~ 2π の範囲内にある phaseAngle というフィールドを保持します。サンプルごとに、phaseAngle を Math.Sin 関数に渡し、その後、phaseAngle に位相角増分という値 (これは周波数とサンプリング レートを使用した単純な計算です) を加えます。

(多数の波形を同時に生成する場合は、正弦波テーブルを short 配列として実装するなどして、可能な限り整数演算を使用して処理速度を最適化したいと思われるでしょう。ですが、波形オーディオを単純に使用する場合は、浮動小数点計算で問題ありません。)

プログラムから SineWaveOscillator を使用するには、次のように、NAudio.dll ライブラリへの参照と using ディレクティブが必要です。

using NAudio.Wave;

音の再生を開始するコードを以下に示します。

WaveOut waveOut = new WaveOut();
SineWaveOscillator osc = new SineWaveOscillator(44100);
osc.Frequency = 440;
osc.Amplitude = 8192;
waveOut.Init(osc);
waveOut.Play();

ここでは、Frequency プロパティを 440 Hz に初期化しています。音楽界では、これは中央ハ音の上のイ音 (ピアノの中央にあるド音の上のラ音) であり、しばしば標準ピッチとして、また、チューニングのために使用されます。もちろん、音の再生中に Frequency プロパティを変更することができます。Amplitude を 0 に設定すると音を消すことができますが、SineWaveOscillator は Play メソッドへの呼び出しを受け取り続けます。このような呼び出しを停止するには、WaveOut オブジェクトの Stop を呼び出します。WaveOut オブジェクトが不要になった場合は、このオブジェクトの Dispose を呼び出してきちんとリソースを解放する必要があります。

調子外れ

私がサンプル プログラムで SineWaveOscillator を使用したところ、望みどおりの結果は得られませんでした。私は、ウィンドウの周りでオブジェクトをドラッグすると音が発生するようにし、その音の周波数がオブジェクトとウィンドウの中心との間の距離に基づくようにしたいと考えていました。ですが、私がオブジェクトを動かすと、周波数はスムーズに遷移しませんでした。私が求めていたのは (トロンボーンや、ガーシュウィンの "ラプソディ イン ブルー" の最初のクラリネットのような) スムーズなポルタメントだったのですが、実際に聞こえたのは (ピアノの鍵盤やハープの弦を指でさっとなぞったような) ぎくしゃくしたリズムのグリッサンドでした。

問題は、WaveOut の Play メソッドを呼び出すたびに、同じ周波数値に基づいてバッファー全体にデータが設定されることです。Play メソッドによってバッファーにデータが設定されている間は、ユーザー インターフェイス スレッドで Play が実行されているので、ユーザーがマウスをドラッグしても周波数を変更することができません。

では、この問題はどれほど重大で、このようなバッファーの大きさはどれくらいなのでしょうか。

NAudio の WaveOut クラスには、既定で 300 ミリ秒に設定される DesiredLatency というプロパティがあります。また、3 に設定される NumberOfBuffers というプロパティもあります (バッファーが複数あると、スループットの向上につながります。アプリケーションがあるバッファーにデータを設定している間に API が別のバッファーを読み取ることができるからです)。したがって、各バッファーは 0.1 秒間のサンプルに相当します。実験により、私は、耳に聞こえる途切れを生じさせることなく DesiredLatency を大きく低下させることは不可能だと気付きました。
バッファーの数を増やすことはできますが (バッファー サイズ (バイト単位) が 4 の倍数になるような値を選ぶようにしてください)、増やしてもそれほど効果はないようでした。また、静的メソッド呼び出し WaveCallbackInfo.FunctionCallback を WaveOut コンストラクターに渡して Play メソッドをセカンダリ スレッドで実行することもできますが、これもそれほど効果はありませんでした。

まもなく、私が必要としていたのは、バッファーへのデータ設定を行いながらそれ自体がポルタメントを実行する発振器だったことがわかりました。必要だったのは、SineWaveOscillator ではなく PortamentoSineWaveOscillator だったのです。

PortamentoSineWaveOscillator

私は、他にも変更を加えたいと考えました。人間は周波数を対数的に認識します。オクターブは周波数を倍にしたものと定義されており、オクターブは周波数域全体にわたって同じように聞こえます。人間の神経系にとっては、100 Hz と 200 Hz の違いは 1000 Hz と 2000 Hz の違いと変わりありません。音楽では、各オクターブは、半音と呼ばれる 12 個の聴覚上同等の音階で構成されています。したがって、これらの音階の周波数は 2 の 12 乗根という倍数因子ずつ順次的に増加します。

私はサンプル プログラムのポルタメントも対数的になるようにしたかったので、PortamentoSineWaveOscillator 内に、次のように周波数を計算する Pitch という新しいプロパティを定義しました。

Frequency = 440 * Math.Pow(2, (Pitch - 69) / 12)

これは、Musical Instrument Digital Interface (MIDI) で使用される慣例に由来するかなり標準的な公式です。MIDI については、今後のコラムで取り上げるつもりです。中央ハ音 (ピアノの中央にあるド音) に割り当てる Pitch 値を 60 としてピアノの鍵盤の低い方から高い方まですべてのキーに番号を付けると、中央ハ音の上のイ音 (ピアノの中央にあるド音の上のラ音) は 69 となり、この公式では周波数は 440 Hz であると算出されます。MIDI ではこのような Pitch 値は整数ですが、PortamentoSineWaveOscillator クラスでは、Pitch は double 型なので、キー間の段階的な変化が可能です。

PortamentoSineWaveOscillator の Play メソッドは、Pitch が変化するとそれを検知し、バッファーの残りサイズに基づいて周波数の計算に使用される値を (したがって、位相角増分も) 段階的に変更します。このロジックにより、メソッドの実行中に Pitch を変更することが可能になりますが、これが行われるのは Play がセカンダリ スレッドで実行されている場合のみです。

ダウンロードできるコードの AudibleDragging プログラムを実行していただくとわかるように、これはうまくいきました。このプログラムでは、ウィンドウの中心近くに、異なる色の 7 つの小さなブロックを作成します。マウスを使用してこれらのブロックをつかむと、プログラムでは、PortamentoSineWaveOscillator を使用して WaveOut オブジェクトを作成します。オブジェクトがドラッグされると、プログラムでは、単純にウィンドウの中心からの距離を求め、次の数式に基づいて発振器のピッチを設定します。

60 + 12 * distance / 200;

つまり、中央ハ音 (ピアノの中央にあるド音) をベースとして、距離が 200 離れるごとに 1 オクターブがプラスされるということです。もちろん、AudibleDragging は他愛ないプログラムで、皆さんはこれを使用すると、アプリケーションは決して音を立てるべきではないという確信をかつてないほど強く持ってしまうかもしれません。ですが、実行時にカスタム音を生成できる可能性は、頭から否定できないほど強力なものです。

演奏する

もちろん、実現できるのは 1 つの正弦波発振器だけではありません。WaveProvider16 からミキサーを派生し、それを使用していくつかの発振器を組み合わせることもできます。単純な波形を組み合わせて、より複雑な波形を作り出すことができます。Pitch プロパティを使用すると、簡単に音符を指定することができます。

ですが、アプリケーションでスピーカーから音楽や楽器の音を鳴り響かせたいと考えていらっしゃる方は、NAudio には Windows フォーム アプリケーションまたは WPF アプリケーションから MIDI メッセージを生成できるクラスも用意されていることを知って喜ばれるでしょう。その方法については、近いうちにご説明します。

Charles Petzold は MSDN Magazine の記事を長期にわたって担当している寄稿編集者です。最新の著書には『The Annotated Turing: A Guided Tour through Alan Turing's Historic Paper on Computability and the Turing Machine』(Wiley、2008 年) があります。Petzold は、自身の Web サイト (charlespetzold.com、英語) でブログを公開しています。

この記事のレビューに協力してくれた技術スタッフの Mark Heath に心より感謝いたします。