WP7 - 寫一個會Record Audio的錄音程式

Windows Phone 7 – 寫一個會Record Audio的錄音程式

過去在唸書的時候總是會看到同學帶著錄音筆去參加meeting或是上課,擔心會漏掉上課的種種內容,隨著智慧手機的發展,

有很多App都方便地透過內鍵的麥克風錄下聲音,不需在另外帶一個設備來錄音。然而錄音的程式在Windows Mobile時代很容易做到,

透過一些內鍵的API與取得麥克風的Instance即可以完成功能,那麼在WP7內要怎麼實作呢?往下便仔細的來介紹有那些API可以使用。

 

Micorsoft.Xna.Framework.Audio

在WP7裡,要實作一個支援錄音的程式,主要是透過Micorsoft.Xna.Framework.Audio裡的Microphone類別,

由於相關多媒體資源存取的功能都納入在XNA系列裡,相關圖片、音樂、影片這些資源要使用到XNA的Dll就對了。

不過這有點扯遠了,接下來在介紹撰寫一個錄音程式前,有幾個重要的類別我覺得要先認識一下:

a. Microphone Class

    該類別提供了豐富的屬性、方法與事件來透過手機麥克風來擷取聲音。在使用前有幾個重要的屬性(均是唯讀):

類別 名稱 說明
屬性 All 靜態屬性,回傳目前設備可用的麥克風集合。例如:手機內鍵的麥克風、藍芽耳機的麥克風或連接式耳麥。
  Default 靜態屬性,回傳預設設備使用的麥克風類型。如果回傳是null代表該設備不支援麥克風,預設麥克風即為手機的內鍵麥克風。
  BufferDuration 設定/取得麥克風擷取聲音時,錄音資料寫入緩衝區對應的容納時間長度。換句話說,為擷取聲音長度達到設定的緩衝大小時,代表已達容納時間長度需觸發BufferReady事件。

舉個例子說明:當設定「Microphone.BufferDuration = TimeSpan.FromMilliseconds(1000)」代表當麥克風擷取聲音長達1秒後,立刻觸發BufferReady事件。

BufferDuration可設定容納時間範圍:100毫秒至1000毫秒(1秒)。超過或低於會發生ArgumentOutOfRangeException例外事件。
  IsHeadset 識別目前設備是否有外接麥克風,不管是接線式或藍芽耳機,如果有為true;沒有為false。
  SampleRate 回傳麥克風擷取聲音資料的採樣率,以Hertz (Hz)為單位。
  State 回傳麥克風目前運作的狀態,值為MicrophoneState列舉,包括:Started、Stoped。
     
方法 GetData 取得由麥克風擷取聲音資料的最新緩衝區資料,以byte array為單位。
該方法為多載的方式,可以指定要擷取的範圍(Byte[], start, length)或是整個Byte[]回傳。
  GetSampleDuration 回傳指定麥克風錄音的資料大小為對應的時間長度。舉例來說,錄音聲音byte array大小為3200,透過「TimeSpan duration = Microphone.GetSampleDuration(totalSize)」指定取得的回傳值為約7秒。
  GetSampleSizeInBytes 回傳指定麥克風錄音的容納時間長度對應於Bytes的大小,該回傳值代表該錄音檔案需要byte arrray大小。
  Start 啟動錄音程序。
  Stop 結束錄音程序。
     
事件 BufferReady 當擷取聲音的緩衝被填滿後,即觸發該事件。代表實作該事件時,可以透過GetData方法取得目前已被寫入緩衝的錄音資料(byte array)。

由上表可以看出幾個重要的屬性:

BufferDuration:指定要錄音的時間長度,透過TimSpan做為設定的時間長度,以毫秒為單位;

GetSampleSizeInBytes:根據指定的時間長度轉換成對於byte arrary的大小;

BufferReady:代表該事件觸發時,可以取得真正被錄好的binary資料,將binary資料寫成檔案即可以進行播放。

 

[注意事項]

在使用Microphone類別進行錄音時,有一件事是需要注意的那就是需要去執行:「FrameworkDispatcher.Update();」。

為什麼呢?由於在XNA中每33fp就會更新畫面一次,但在Silverlight Application並沒有這樣的機制,為了確保錄音的功能

持續的更新狀態與進行擷取動作,因此,需要透過指定一個定期執行「FrameworkDispatcher.Update();」的事件。

詳細說可參考<Enable XNA Framework Events in Windows Phone Applications>。

通常直接使用DispatcherTimer讓它自己一個執行緒,定期呼叫上述指令,例如:

   1: DispatcherTimer tDT = new DispatcherTimer();
   2: //定期每33毫秒執行一次FrameworkDispatcher.Update(); 方法
   3: tDT.Interval = TimeSpan.FromMilliseconds(33);
   4: tDT.Tick += delegate { try { FrameworkDispatcher.Update(); } catch { } };
   5: tDT.Start();

 

b. SoundEffect Class

    該類別提供載入聲音資源的功能,該類別最常用於XNA Game中播放特定的音效,由於它不像MediaElement需指定特定的處理才能播放,

又支援非前景下進行。因此,在錄音程式完成後通常也會配合該類別將聲音資源直接播放出來,等到確定儲存時,再將資料儲存為實體檔案。

但使用該類別要特別注意,SoundEffect針對載入的聲音資源是寫入記憶體中的它配合多個SoundEffectInstance來播放多個聲音,代表

手機記憶體的管理是需要特別被處理的

 

c. 需注意的Exception:NoAudioHardwareExceptionNoMicrophoneConnectedException

    這二個Exception發生於當麥克風硬體或連接Microphone API出現問題時,例如:安裝外接式麥克風後發生Driver不支援或無法擷取聲音時。

由於我在是試用手機連接藍芽耳機時所遇到的例外,所以特別標示出來。當時發生的情形是剛接上藍芽後,啟動App立即點擊錄音,API連接一

會後就出現該Exception了。

 

〉實作範例

該實作範例主要是撰寫透過麥克風錄下正在播放的音樂,在錄製20秒後結束並自動存成*.wav檔案。往下便加以說明如何實作:

 

a. 在畫面中加下幾個錄音與播放音樂用的控件

   1: <!--ContentPanel - place additional content here-->
   2: <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
   3:     <Grid.RowDefinitions>
   4:         <RowDefinition Height="50*" />
   5:         <RowDefinition Height="30*" />
   6:     </Grid.RowDefinitions>
   7:     <StackPanel Grid.Row="0">
   8:         <Button Name="btnRecord" Content="Start Record by Microphone" Click="btnRecord_Click" />
   9:         <Button Name="btnStopRecord" Content="Stop Record by Microphone" Click="btnStopRecord_Click" />
  10:         <Button Name="btnPlay" Content="Play Record" Click="btnPlay_Click" />
  11:         <StackPanel Orientation="Horizontal">
  12:             <TextBlock Text="Record Time: " Style="{StaticResource PhoneTextTitle3Style}" />
  13:             <TextBlock x:Name="tblSpendTime" Style="{StaticResource PhoneTextTitle3Style}" />
  14:         </StackPanel>
  15:     </StackPanel>
  16:     <StackPanel Grid.Row="1">
  17:         <MediaElement x:Name="media" Width="0" Height="0" 
  18:           Grid.Column="0" Grid.Row="0" />
  19:         <Button Name="btnPlayMusic" Content="Play Music" Click="btnPlayMusic_Click" />
  20:         <Button Name="btnStopMusic" Content="Stop Music" Click="btnStopMusic_Click" />
  21:         <Button Name="btnSaveRingtone" Content="Save Ringtone" Click="btnSaveRingtone_Click" />
  22:     </StackPanel>
  23: </Grid>

預覽畫面如圖:

000

 

b. 實作錄音的功能

    b-1. 先定義公用變數,記錄microphone實體、緩衝用的byte[]、記錄音檔資料的stream與要播放的SoundEffect。

   1: Microphone gMicrophone = Microphone.Default;
   2: byte[] gAudioBuffer;
   3: MemoryStream gStream = new MemoryStream();
   4:  
   5: SoundEffect gSoundEffect;
   6: //定義記錄播放20秒的token
   7: int gSpendTime = 0;
   8:  
   9: public MainPage()
  10: {
  11:     InitializeComponent();
  12:  
  13:     init();
  14: }

    b-2. 撰寫程式啟動後,初始化的任務,包括:

           (1) 定義要週期性呼叫FrameworkDispatcher.Update();

           (2) 註冊microphone的BufferReady事件處理;

   1: //定義初始化副程式
   2: private void init()
   3: {
   4:     DispatcherTimer tDT = new DispatcherTimer();
   5:     //定期每33毫秒執行一次FrameworkDispatcher.Update(); 方法
   6:     tDT.Interval = TimeSpan.FromMilliseconds(33);
   7:     tDT.Tick += delegate { try { FrameworkDispatcher.Update(); } catch { } };
   8:     tDT.Start();
   9:     //註冊BufferReady事件的處理者
  10:     gMicrophone.BufferReady += new EventHandler<EventArgs>(gMicrophone_BufferReady);
  11: }
  12:  
  13: void gMicrophone_BufferReady(object sender, EventArgs e)
  14: {
  15:     //設定每1秒加1次目前錄音花掉的時間
  16:     gSpendTime += 1;
  17:     Dispatcher.BeginInvoke(() =>
  18:     {
  19:         tblSpendTime.Text = string.Format("{0} seconds.", gSpendTime.ToString());
  20:     });
  21:  
  22:     //每1秒到時,取出在緩衝的Buffer資料,並寫入Stream中。
  23:     gMicrophone.GetData(gAudioBuffer);
  24:     gStream.Write(gAudioBuffer, 0, gAudioBuffer.Length);
  25:  
  26:     //滿足20秒後自動呼叫關閉錄音
  27:     if (gSpendTime == 20)
  28:     {
  29:         btnStopRecord_Click(sender, new RoutedEventArgs());
  30:         gSpendTime = 0;
  31:         Dispatcher.BeginInvoke(() =>
  32:         {
  33:             tblSpendTime.Text = "Record finished!";
  34:         });
  35:     }
  36: }

    b-3. 註冊"啟動錄音”與”結束錄音”的按鈕事件:

           在實作啟動錄音與結束錄音,出現了WavHedaer的類別功能,主要是用於將Stream中的資料按造Wav Format的格式進行

           編譯,為何需要這樣呢?由於WP7目前還不支援將錄好的Audio File直接轉成mp3或wav,因此,如何將Byte Array轉成指

           定的音檔格式,目前我找到的可以參考:<Storing WP7 recorded audio as WAV format streams >的做法,直接把Byte Array

           wav檔案,並且寫入Isolated Storage之中。

   1: private void btnRecord_Click(object sender, RoutedEventArgs e)
   2: {
   3:     //指定要將錄音的資料轉成WAV格式
   4:     WavHeader.WriteWavHeader(gStream, gMicrophone.SampleRate);
   5:  
   6:     //指定錄音的BufferDuration為1秒觸發一次BufferReady事件
   7:     gMicrophone.BufferDuration = TimeSpan.FromMilliseconds(1000);
   8:  
   9:     //透過指定的BufferDuration,轉換出大約需要的byte array size
  10:     gAudioBuffer = new byte[gMicrophone.GetSampleSizeInBytes(gMicrophone.BufferDuration)];
  11:  
  12:     //啟動錄音
  13:     gMicrophone.Start();            
  14: }
  15:  
  16: private void btnStopRecord_Click(object sender, RoutedEventArgs e)
  17: {
  18:     if (gMicrophone.State == MicrophoneState.Started)
  19:     {
  20:         //結束錄音
  21:         gMicrophone.Stop();
  22:  
  23:         //指定將錄音的資料結束時補上特定標籤
  24:         WavHeader.UpdateWavHeader(gStream);
  25:  
  26:         //結束寫入記憶體
  27:         gStream.Close();
  28:     }
  29: }

    b-4.  註冊播放錄好音的聲音資料按鈕事件:

   1: private void btnPlay_Click(object sender, RoutedEventArgs e)
   2: {
   3:     //由於錄好音的資料是記錄在MemoryStream之中,
   4:     //因此,直接使用SoundEffect配合是最好的作法,Stream.ToArray把資料轉換出來。
   5:     //設定SampleRate告訴SoundEffect音軌的頻率,AudioChannels.Mono與頻道的模式
   6:     gSoundEffect = new SoundEffect(gStream.ToArray(), 
   7:                                    gMicrophone.SampleRate, AudioChannels.Mono);   
   8:     gSoundEffect.Play();
   9: }

 

c. 實作播放音樂的功能

    實作b步驟中的所有流程,即可以進行錄音與播放錄好的聲音功能。接下來c的部分,主要透過MediaElement載入要播放的音樂。

   1: private void btnPlayMusic_Click(object sender, RoutedEventArgs e)
   2: {
   3:     if (media.CurrentState == MediaElementState.Closed)
   4:     {
   5:         //指定播放的音樂
   6:         Uri tUri = new Uri("mms://bcr.media.hinet.net/RA000040", 
   7:                             UriKind.Absolute);
   8:         media.Source = tUri;
   9:         media.Play();
  10:     }
  11: }
  12:  
  13: private void btnStopMusic_Click(object sender, RoutedEventArgs e)
  14: {
  15:     if (media.CurrentState == MediaElementState.Playing)
  16:         media.Stop();
  17: }

 

d. 實作寫入Isolated Storage與播放WAV的功能的功能

   1: private void btnSaveFile_Click(object sender, RoutedEventArgs e)
   2: {
   3:     //將檔案存在IsolatedStorage之中
   4:     WriteStraminIsolatedStorage("cusrecord.wav", gStream);
   5:  
   6:     //透過SoundEffect載入放在IsolatedStorage中的wav
   7:     using (var tStore = IsolatedStorageFile.GetUserStoreForApplication())
   8:     {
   9:         using (var tFStream = new IsolatedStorageFileStream("cusrecord.wav", FileMode.Open, tStore))
  10:         {                    
  11:             //使用SoundEffect直接播放wav檔案
  12:             SoundEffect effect = SoundEffect.FromStream(tFStream);
  13:             //記得在加上一次update,告訴mediaplayer要進行播放
  14:             FrameworkDispatcher.Update(); 
  15:             effect.Play();
  16:         }
  17:     }
  18: }
  19:  
  20: private void WriteStraminIsolatedStorage(string pFileName, Stream pStream)
  21: {
  22:     using (var tStore = IsolatedStorageFile.GetUserStoreForApplication())
  23:     {
  24:         //取得檔案名稱,並且檢查是否已經有檔案存在,如果有則先刪除,再重新寫入。
  25:         if (tStore.FileExists(pFileName))
  26:         {
  27:             tStore.DeleteFile(pFileName);
  28:         }
  29:         //建立檔案
  30:         using (var tFStream = new IsolatedStorageFileStream(pFileName, FileMode.Create, tStore))
  31:         {
  32:             byte[] tByteInStream = gStream.ToArray();
  33:             tFStream.Write(tByteInStream, 0, tByteInStream.Length);
  34:             tFStream.Flush();
  35:         }
  36:     }
  37: }

 

e. 測試流程

    1. 點擊[Play Music] ;

    2. 點擊[Start Record by Microphone],等錄製20秒後會自動關閉錄音出現「record finished」;

    3. 點擊[Save File],程式會自動產生*.wav與播放wav檔案。

 

[範例程式]

======

以上是說明如何實作一個錄音程式的功能。其實錄音程式在XNA類別裡還有很多相關的應用,值得去研讀。

不過目前範例中介紹的功能我覺得非常適用。不過由於目前WP7針對錄音資料沒有辦法直接儲存成mp3, wav或wma,

老實說真的不是很方便,希望能夠快點支援才好。不然就要來找看看怎麼把它轉過去了。

 

References:

在App中讀取Windows Phone 7手機內的音樂資源(Music Hub整合) (必讀)

Sound Recording in Windows Phone 7

How to capture audio from your microphone in WP7

WP7 and Microsoft.Xna.Framework.Audio.Microphone

梦想成真 XNA (6) - 声音和音效 & 梦想成真XNA(6)-声音和音效(2)

Recording Audio in Silverlight on Windows Phone 7

How To Record Audio On Windows Phone 7 And Copy Recorded Files To Your PC ()

How to: Record Video in a Camera Application for Windows Phone

How to: Use the DeviceStatus Class for Windows Phone

Audio recorder Silverlight 4 sample & Silverlight 4 – WebCams.OnceMoreWithAudio()

 

Saving Microphone Stream to mp3 or wave & Storing WP7 recorded audio as WAV format streams (必讀)

Upload Files from Windows Phone

Silverlight 4 Rough Notes: Camera and Microphone Support (重要)

Saving the microphone recording byte[] in a file ?  & Aumplib: C# Namespace And Classes For Audio Conversion

silverlight中播放Wav文件 & Windows Phone7 播放WAV 和 MP3 & Playing .MP3 and .WMA Audio Files in XNA

Articles about Alvas.Audio

Video Recording in Windows Phone 7

Record Silverlight 4 MediaElement content & Steps to Record Voice & Podcast on Windows Phone 7

WPF save streamed video using MediaElement & MediaElement Record

 

Dotblogs Tags: