UI 最前線

Windows Phone 7 で録音する

Charles Petzold

コード サンプルのダウンロード

Charles Petzold1984 年に Apple が Macintosh を紹介したごく初期の宣伝ポスターの 1 つに、次のような非常に魅力的な表現でマウスの設計を宣伝したものがあります。「ボタンが 2 つあるマウスもありますが、Macintosh には 1 つです。ボタンを押し間違えるなんてあり得ません。」

もちろん、これはまったくの真実ではありません。1 つのボタンに複数の機能を割り当てれば、ボタンが複数あるのと同じくらい混乱を招きます。しかし、ボタンを押し間違える可能性がないことは、確かに UI 設計が簡潔になることについて、説得力のある主張です。

スマートフォンのプログラミングでは、UI の要素数を必要最低限まで削減することはかなり重要です。携帯電話はあまり大きくありません。多くのボタンを配置することはできず、そのボタンを押す指もマウスほど正確ではありません。ボタンが多すぎれば、間違ったボタンを押す可能性も高くなります。

反面、UI の要素数を制限するとプログラムの機能も制限されることが多いため、どこで線引きするかは非常に悩ましいことです。人生は妥協の産物です。

設計の進化

今回は、「ドライ クリーニングの受け取りを忘れないように」とか、「すばらしい映画のアイデアを思いついた。ボーイ ミーツ ガールだ」とか、短い音声メモを録音する Windows Phone 7 プログラムを作成したらおもしろいだろうと考えました。

このようなプログラムはもちろん便利なうえ、公共の場で新しい Windows Phone を自慢する口実にもなります。それ以上に重要だと思えたのは、携帯電話でサポートされる録音と再生のクラスを実際に使用してみるすばらしいチャンスである点です。

しかし、プログラムの設計は予想以上に厄介であることがわかりました。コードを 1 行も記述しないうちから、頭の中で何回もプログラムの設計をやり直しました。

最初は、録音と再生という 2 つのボタンを用意し、どちらもトグル ボタンとして機能させれば十分だと考えました。つまり、録音ボタンを押して録音を開始し、再度押して停止します。このプログラムでは、分離ストレージに音声データを保存します。再生ボタンを押すと、保存したオーディオ データが再生されます。録音ボタンを押すたびに前回のメモを置き換え、メモを削除する削除ボタンは用意しません。

さらに、音声認識機能を実装して、プログラムのボタンを再生ボタンだけにすることも考えました。プログラムでは連続録音し、ある音声が含まれるときのみデータを保存することにします。しかし、なんらかの方法で手動によるしきい値の設定を導入しない限り、周囲の雑音と実際の音声を区別することは途方もなく難しそうでした。このため、ボタン 1 つの設計はあきらめました。

当初の計画は、メモが 1 件であれば適しています、複数件のメモを録音するには不適切です。そこで、プログラムで保持する音声ファイルを 1 つだけにし、新しいメモを録音するたびに前回の末尾に追加していくことを考えました。大きなファイルを 1 つだけ保持することになるため、再生ボタンを押せば、すべてのメモが連続再生されます。もちろんプログラムでこのファイルを無限に拡張することはできないため、ファイル全体を削除してすべてのメモを削除する削除ボタンがどうしても必要です。

確かに、うまい設計ではありません。実際には、メモごとに個別のファイルを保持し、メモを個別に削除できるようにする必要があります。しかし、そのためには再生と削除が可能なように、すべての個別ファイルをユーザーに表示する必要が生じ、プログラムが一気に複雑になりました。ファイルを選択する ListBox に加え、各メモを特定する手段が絶対に必要です。メモを特定するには、ユーザーがキーワードを指定したり、さらに厄介なことには実際のファイル名を使用したりする必要があります。

どんどん複雑になります。これではだめです。そこで手元の留守番電話機能の付いた電話機を見てみました。通話やメモはそれぞれ個別に録音されていますが、それぞれ番号が振られた状態で簡単に表示されています。[再生] ボタンのほかに、前の通話や次の通話を参照するための [前へ] ボタンと [次へ] ボタンがあります。メモや通話を削除すると、番号が振り直されます。メモに番号を振るつもりはありませんでしたが、携帯電話の画面が留守番電話より大きいことを利用すれば、各メモの詳細 (録音日、長さ、ファイル サイズなど) を表示できそうです。

実際に事態が進展したのは、プログラムのメイン画面に ListBox を配置すれば、メモの選択だけでなく再生にも使用できることに気付いたときでした。

プログラムを使用する

最終的な設計は、当然ながら、シンプルさを最大限に追求することと、完全な機能を備えたメモ管理システムにすることの間で悩んだ末の妥協の産物です。今月のコラムのダウンロード可能な SpeakMemo プロジェクトは、Silverlight for Windows Phone 向けに作成しており、Windows Phone 7 Development Tools が必要です。プログラムは携帯電話エミュレーター上で実行できます。エミュレーターではプログラムが正常に動作しているように見えますが、実際には録音も再生も行われません。

初めて SpeakMemo プログラムを実行すると、図 1 のような画面が表示されます。

image: The Initial SpeakMemo

図 1 SpeakMemo の初期画面

ボタンは 1 つだけです。いや、少なくとも、"有効な" ボタンは 1 つだけです。非常にすっきりした画面です。ボタンには、分離ストレージの空き領域のサイズと、そのサイズに相当する録音可能な音声の長さを表示しています (ただし、17 時間連続でメモを録音できるわけではありません)。

[record] ボタンを押すと、赤い点滅ボタンに変わり、ボタン内では録音中のメモの長さを示すインジケーターが随時更新されます (図 2 参照)。

image: SpeakMemo While Recording

図 2 録音中の SpeakMemo

再度 [record] ボタンを押すと、録音したメモ、録音日時、長さ、ストレージ領域に占めるサイズ、および再生ボタンが画面に表示されます (図 3 参照)。

image: SpeakMemo with One Memo

図 3 1 件のメモを保存した SpeakMemo

当然、再生ボタンを押すとメモを再生でき、再生モードと一時停止モードの間でボタンの表示が切り替わります。

メモが 1 件だけだとわかりにくいかもしれませんが、録音済みのメモは図 4 のように新しいものから順番に ListBox に格納するため、録音済みメモが多数あれば一覧をスクロールして個別に再生できます。

image: The SpeakMemo ListBox

図 4 SpeakMemo の ListBox

Silverlight の強力な機能の 1 つは、ListBox の項目の外観を定義できる DataTemplate です。DataTemplate には、ボタンなど他のコントロールを含めることができます。DataTemplate にボタンを配置するという実用的なアプリケーションを考案できるのは嬉しいことです。

また、メモを個別に削除することにより、収集したメモを管理することもできます。メモを選択すると、[delete] ボタンが有効になります。DataTemplate にボタンを配置したことがヒントになり、Silverlight の別のテクニックを利用して、[delete] ボタン内にもう 2 つのボタンを配置しました。この 2 つのボタンは [delete] を押すと表示され、おなじみの確認機能に切り替わります (図 5 参照)。

image: Confirming a Delete

図 5 削除の確認

メモを再生することでもメモを選択できますが、再生ボタンの右側の領域を押せばメモを再生しないで選択できます。このプログラムでは、1 件のメモを再生しながら別のメモを録音し、同時にさらに別のメモを削除できます。

携帯電話と音

かつて、Windows Phone 7 では、Microsoft .NET Framework の System.Speech 名前空間に含まれる音声認識と音声合成の一部をサポートすることが検討されていました。おそらく、これらの機能は今後サポートされることでしょう。

それまでは、Microsoft.Xna.Framework.Audio 名前空間のクラスを使用して、携帯電話のマイクから音声を取り込み、携帯電話のスピーカーで再生します。この名前空間のクラスは XNA クラスですが、Silverlight プログラムでも使用できます。XNA クラスを Silverlight プロジェクトで使用するには、Microsoft.Xna.Framework.dll への参照をプロジェクトの参照に追加し、警告を無視するだけです。

Microsoft.Xna.Framework.Audio 名前空間のクラスは、Microsoft.Xna.Framework.Media 名前空間のクラスとはまったく別のものです。Media 名前空間には、携帯電話の音楽ライブラリに保存されている音楽を再生するクラスが含まれています。再生するのは MP3 形式か WMA 形式の圧縮されたオーディオ ファイルで、Song 型のオブジェクトに変換されます。このコラムでは、拙著『Programming Windows Phone 7』(Microsoft Press、2010 年) の第 18 章で紹介している、音楽ライブラリにアクセスする方法を説明します。この書籍は、bit.ly/dr0Hdz (英語) から無料でダウンロードできます。私の Web サイトのブログ エントリでは、プログラム自体に保存されている MP3 ファイルや WMA ファイルを再生する方法も紹介しており、サンプルをインターネット経由でダウンロードできます (bit.ly/ea73Fz、英語)。

これに対し、Microsoft.Xna.Framework.Audio 名前空間では、標準 PCM 形式の圧縮されていないオーディオ データを処理します。これは、オーディオ CD や Windows の WAV ファイルと同じ手法です。PCM では、アナログ音の振幅が一定レート (通常は毎秒 8,000 ~ 48,000 サンプル) でサンプリングされ、各サンプルは、通常は 8 ビット値または 16 ビット値で保存されます。特定のサウンド ファイルに必要なストレージのサイズは、長さ (秒単位)、サンプル レート、およびサンプルごとのバイト数を乗算した値です (ステレオの場合はその 2 倍です)。

Windows Phone 7 アプリケーションで音声認識をサポートする必要があれば、別途 (たいていは Web サービス経由で) その機能を提供する必要があります。同様に、テキストを音声に変換する必要があるプログラムでは、おそらく Web サービスを使用するか、携帯電話でこの機能がサポートされるまで待つ必要があります。Windows Phone 向けの Microsoft Translator アプリケーションでは、Microsoft Translator サービス (microsofttranslator.com) を使用して音声機能を提供しています。Translator Starter Kit のコードとドキュメントは、MSDN (msdn.microsoft.com/library/gg521144(VS.92).aspx、英語) と AppHub (create.msdn.com/education/catalog/sample/translatorstarterkit、英語) で公開されています。

XNA のオーディオ サービスを使用する場合、Silverlight プログラムでは、ビデオのリフレッシュ レートとほぼ同じレートで FrameworkDispatcher.Update 静的メソッドを呼び出す必要があります (Windows Phone 7 では、ビデオのリフレッシュ レートは毎秒約 30 回です)。XNA に関するオンライン ドキュメントの「Windows Phone アプリケーションでの XNA Framework イベントの有効化」(msdn.microsoft.com/library/ff842408) に、その方法が記載されています。SpeakMemo の場合は、XnaFrameworkDispatcherService クラスを使用してこの処理を行います。このクラスのインスタンスは、App.xaml ファイルで作成します。

録音する

携帯電話のマイクから録音するには、Microphone クラスを使用します。おそらく、このクラスのインスタンスは、次のように Default 静的プロパティを使用して作成することになります。

Microphone microphone = Microphone.Default;

また、All 静的プロパティで Microphone オブジェクトのコレクションを指定することもできますが、その場合はおそらくユーザーがマイクを選択できるように一覧を表示します。

サンプル レートは固定で変更できず、SampleRate プロパティで毎秒 16,000 サンプルであることがとわかります。サンプルに関するナイキスト定理によると、これは、周波数が 8,000 Hz までの音の録音に適しています。この周波数帯は人間の声には適していますが、音楽にはあまり高い音質を期待できません。各サンプルは 2 バイト幅のモノラルです。つまり、録音した音には、毎秒 32,000 バイト必要で、毎分換算で 1.9 MB になります。

マイクのデータは、単なるバイト配列としてバッファーに格納されてプログラムに提供されます。BufferReady イベントのハンドラーを実装し、Start メソッドを呼び出して録音を開始します。Microphone オブジェクトで BufferReady イベントが発生したら、コードからバイト配列を指定して GetData メソッドを呼び出します。GetData メソッドから値が返されると、バッファーには PCM データが格納されています。録音を停止する場合は、GetData メソッドを再度呼び出して、最後の部分バッファーを取得します。GetData メソッドは、配列に転送済みのバイト数を返します。最後に、Stop メソッドを呼び出します。

Microphone オブジェクトで指定できる唯一のオプションは、GetData に渡すバッファーのバイト サイズです。BufferSize プロパティの値は TimeSpan 型で、10 ミリ秒刻みで 100 ~ 1,000 ミリ秒 (1 秒) の範囲に収まる必要があります。SpeakMemo では、既定値の 1,000 をそのまま使用しています。

利便性を高めるために、Microphone クラスにはバッファー サイズと時間を相互に変換する 2 つのメソッドがあります。ただし、これらのメソッドは名前に "sample" が含まれているので少し紛らわしくなっています。GetSampleDuration メソッドは、バイト数を 32,000 で除算して、対応する秒数を表す TimeSpan を返します。GetSampleSizeInBytes メソッドは、秒単位の TimeSpan の長さに 32,000 を乗算します。

SpeakMemo では、録音中、ジェネリック List コレクションに 32,000 バイトずつのバッファーを複数累積します。録音が停止すると、すべてのバッファーを分離ストレージ内のファイルにまとめて保存します。

メモを特定するためのキーワード機能を実装しないと決めてから、ファイルの内容を PCM データだけにし、補足情報は含めないことにしました。しかし、Silverlight for Windows Phone の IsolatedStorageFile クラスではファイルの作成日時や最終書き込み日時にアクセスするメソッドがサポートされていないことを知って非常に驚き、これらの情報はユーザーにとって不可欠だと思いました。

そのため、ファイル名自体に日時を含める必要がありました。まず、書式設定オプションの "s "と "u "を使用して DateTime オブジェクトからファイル名を作成しようとしましたが、うまく行きませんでした (失敗の原因については、読者の課題として残しておきます)。その後、さまざまな日付と時刻の構成要素を組み合わせて、独自のファイル名を作成しました。

XNA 音声の再生

Microsoft.Xna.Framework.Audio 名前空間の関連する SoundEffect クラスと SoundEffectInstance クラスを使用すると、録音済みの音声を再生できます。XNA ゲームでのこれらのクラスの一般的な機能からは想像しづらい名前です。ただし、SoundEffect.FromStream 静的メソッドには、RIFF ヘッダーを含む標準の Windows WAV ファイルを参照する Stream オブジェクトが必要です。今回はファイル形式にわずらわされることは望んでいません。

WAV ファイルではなく PCM データを手を加えずにそのまま処理するには、SoundEffectInstance クラスから派生した DynamicSoundEffectInstance クラスを使用します。このクラスは、Microphone クラスから生成されたデータや、独自の波形データを動的に作成するプログラム (シンセサイザー プログラムなど) に最適です。

DynamicSoundEffectInstance のコンストラクターには、サンプル レートとチャネル数が必要です。つまり、マイクから生成されるデータにこのクラスを使用する場合は、次のようにして整合性を保つ必要があります。

DynamicSoundEffectInstance playback = 
  new DynamicSoundEffectInstance(
  microphone.SampleRate, AudioChannels.Mono);

一方、高速再生して早口のリスのような音声にする場合は、最初の引数を 2 倍にします。DynamicSoundEffectInstance クラスでは、データのサンプル サイズを 16 ビットと想定しています。このクラスには、再生を制御するための Play、Pause、Resume、および Stop の各メソッドと、現在の状態を示す State プロパティがあります。DynamicSoundEffectInstance クラスの動作は Microphone クラスとほぼ反対で、新しいバッファーが必要になると BufferNeeded イベントが発生します。開発者が行う必要があるのは、PCM データをバッファーに格納して、SubmitBuffer メソッドを呼び出すことです。

再生中に音声が途切れないようにするには、DynamicSoundEffectInstance クラスでバッファーのキューを管理し、前のバッファーの再生中に新しいバッファーを送信します。このような処理に役立つよう、このクラスには、キュー内のバッファー数を示す PendingBufferCount プロパティがあります。PendingBufferCount の値が 2 以下になると、BufferNeeded イベントが発生します。

ただし、PCM データ全体を再生するだけでよければ、BufferNeeded イベントを無視して SubmitBuffer メソッドを呼び出すこともできます。そもそも SpeakMemo プログラムではこのような方法で DynamicSoundEffectInstance クラスを使用していたのですが、バッファーの再生が終了するタイミングを特定できないことがわかりました。DynamicSoundEffectInstance クラスには状態変更イベントがなく、あったとしても、バッファーの終了時に再生状態から停止状態に切り替わりません。次のバッファーを待機するだけです。この情報が認識されなかったため、プログラムでは再生ボタンと一時停止ボタンの外観が正しく切り替わりませんでした。

今回は BufferNeeded イベントを処理することにしましたが、用途は PendingBufferCount プロパティの値を確認することだけです。PendingBufferCount プロパティの値が 0 になったら、そのバッファーの再生は終了しています。

ストレージの問題

SpeakMemo では、録音したメモを分離ストレージに格納します。概念的には、分離ストレージはアプリケーションのプライベート領域ですが、物理的には、デスクトップ コンピューターのハード ドライブに似た携帯電話全体のストレージ領域の一部です。分離ストレージには、すべてのアプリケーションの実行可能ファイルに加え、携帯電話の写真ライブラリ、音楽ライブラリ、ビデオ ライブラリなどが格納されます。Windows Phone 7 のハードウェア仕様では、携帯電話には分離ストレージ領域用に少なくとも 8 GB のフラッシュ メモリを搭載し、ストレージの空き領域が少なくなったら携帯電話自体からユーザーに通知することを求めています。

メモのファイルを格納することについては、それほど心配していませんでした。むしろ心配だったのは、プログラムのヒープでした。Windows Phone 7 のハードウェア仕様では、フラッシュ メモリ ストレージ以外に 256 MB の RAM が必要です。これは、アプリケーションが実行中に占有するメモリで、プログラムのローカル ヒープを提供します。実験したところ、SpeakMemo では、メモリ不足例外が発生するまでに、1 つの配列に最大 90 MB を割り当てられることがわかりました。このサイズは、マイクから 47 分間音声を取り込むことに相当します。

だからと言って、Windows Phone 7 プログラムの録音時間を必ず 47 分以内に制限する必要はありません。しかし、47 分を超えて連続録音する場合は、バッファーを分離ストレージに絶えず保存してメモリを解放し、再生時にはファイルを段階的に読み込む必要があります。SpeakMemo はこのような設計を行っていません。SpeakMemo ではファイル全体を保存したり読み込んだりしており、この実に簡潔な構造をあきらめるのは気が進みませんでした。

このような理由から、メモの長さを最大 10 分に設定しました。録音しているメモがこの長さに達すると、録音を停止してメモを保存します (この処理自体には数秒かかります)。プログラムを簡潔にするため、警告は表示しません。ユーザーが停止ボタンを押したかのように録音を停止します。この自動停止と保存の処理は、プログラムを終了したり、非アクティブ (トゥームストーン中) にしたりするときにも行われます。

もちろん、10 分間メモを再生し続ける操作も、あまり便利ではありません。再生ボタンは再生モードと一時停止モードを切り替えますが、巻き戻しや早送りはできません。このような機能は追加可能ですが、そのために必要な要素はおわかりですね。

そのとおり、もっと多くのボタンです。Slider も適しているかもしれません。

Charles Petzold は MSDN マガジンの記事を長期にわたって担当している寄稿編集者です。新しく執筆した『Programming Windows Phone 7』(Microsoft Press、2010 年) は、bit.ly/dr0Hdz (英語) から無料でダウンロードできます。

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