×
Namespaces

Variants
Actions

Windows Phone上的声音录制与编码(一):录制与wav编码

From Nokia Developer Wiki
Jump to: navigation, search
WP Metro Icon Multimedia.png
SignpostIcon XAML 40.png
SignpostIcon WP7 70px.png
Article Metadata

代码示例
兼容于
文章
WS_YiLunLuo 在 23 Feb 2012 创建
最后由 hamishwillee 在 16 Jul 2013 编辑

Contents

简介

接下来我们会有几篇wiki介绍如何在Windows Phone上录制声音,并且进行编码,以及上传至服务器。

Windows Phone提供了一个麦克风,因此一个常见的情景是让用户录制声音。这很简单,你可以在网上找到大量的资源,本文也会简要介绍一下如何使用麦克风。但是,Windows Phone的麦克风录制出来的声音是未编码过的PCM格式,也就是指包括波形的信息,没有任何头,也没有进行过编码。这在大多数设备上都是不能够直接播放的。因此我们常常需要将录制的声音进行编码。

本文只介绍最简单的wav编码,这只需要Windows Phone本身就可以实现。今后的wiki会介绍如何将声音编码成其它格式,例如mp4。由于Windows Phone本身没有提供任何内置的编码支持,要直接在手机上实现编码会比较困难,因此我们需要使用web service利用服务器端的代码进行编码。

你可以自这里下载本文的示例程序:File:SimpleAudio1.zip

SimpleAudio.png

使用麦克风

使用Microphone类

为了在Windows Phone上使用麦克风,你可以参考这里的步骤。简单说来要做如下几步:

  • 通过Microphone.Default获得一个Microphone实例。
  • 处理它的BufferReady事件。
  • 在BufferReady中不断通过GetData获取数据,并且写入一个stream中。
  • 想要启动麦克风时,调用Start。
  • 想要关闭麦克风时,调用Stop。
  • 想要播放录制的声音时,调用XNA的SoundEffect。
        private Microphone _microphone = Microphone.Default;
private MemoryStream _stream;
 
private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
{
this._microphone.BufferReady += new EventHandler<EventArgs>(Microphone_BufferReady);
}
 
void Microphone_BufferReady(object sender, EventArgs e)
{
byte[] buffer = new byte[1024];
int offset = this._microphone.GetData(buffer, 0, 1024);
while (offset > 0)
{
this._stream.Write(buffer, 0, offset);
offset = this._microphone.GetData(buffer, 0, 1024);
}
}
 
private void StartButton_Click(object sender, RoutedEventArgs e)
{
if (this._stream != null)
{
this._stream.Close();
}
this._stream = new MemoryStream();
if (this._microphone.State != MicrophoneState.Started)
{
this._microphone.Start();
}
}
 
private void StopButton_Click(object sender, RoutedEventArgs e)
{
if (this._microphone.State != MicrophoneState.Stopped)
{
this._microphone.Stop();
}
}
 
private void PlayButton_Click(object sender, RoutedEventArgs e)
{
if (this._stream == null || this._stream.Length == 0)
{
return;
}
 
this._stream.Position = 0;
SoundEffect soundEffect = new SoundEffect(this._stream.ToArray(), this._microphone.SampleRate, AudioChannels.Mono);
soundEffect.Play();
}

调用FrameworkDispatcher.Update

请注意Microphone是一个XNA的类,不是Silverlight的类。在Silverlight程序中,XNA中的事件(例如BufferReady)不会自动触发。这些事件是在FrameworkDispatcher.Update被调用时触发的。在XNA程序中,这个方法会自动在Game.Update中调用,但是在Silverlight中,你必须手工调用。而且这个方法必须不断地被调用,才能导致这些事件正常触发。

为了不断调用FrameworkDispatcher.Update,你可以用一个定时器。上面引用的文章简单介绍了这一点,它在当前也没中使用定时器调用FrameworkDispatcher.Update。但是这样的做法会导致这些事件只能在当前页面使用。一个比较好地做法是写一个application service,并且放到ApplicationLifetimeObjects中。这样一来可以确保不管在哪个类中使用XNA事件,都能正常使用。

这里提供了创建application service并且调用FrameworkDispatcher.Update的非常完整的代码,我们就不再重复了。 好了,现在你可以运行程序,并且录音了。注意如果你使用的是模拟器,请确保你的电脑有一个麦克风,否则无法测试。你也可以直接将程序部署到真正的手机上。

wav编码

编码的理由和方式

到这里,我们的程序已经可以正常录制并播放声音了。你甚至可以将录制下来的stream保存到isolated storage中供今后使用。但是,如果你尝试将这个stream传到一个web service上,然后在非Windows Phone设备上播放,你会发现这是不行的。甚至你都无法在Windows Phone本身的MediaElement中播放。这是因为我们录制的stream只包含了最原始的波形信息(被称作PCM),没有进行过任何编码,因此算不上是音频文件。

想要让其他设备能够播放这段音频,你必须对其进行编码。最简单的是wav编码。其实wav无非就是在PCM的基础上加上一段头,声明音频文件的相关信息。因此只要拥有PCM数据,在任何平台上(包括Windows Phone),你都可以不依赖任何外部的SDK或者codec,直接加上这段头,形成一个wav文件。

其它编码,例如wma, mp3, aac,等等,就没那么简单了。它们往往需要很复杂的算法,没有专门的codec的帮助,靠自己写是很累的。Windows Phone(以及绝大多数其它的手机平台)都没有提供任何API帮助我们编码,但是Windows有,所以可以考虑使用一个web service将PCM或者wav stream传到一个Windows Server上,并且使用诸如Media Foundation这样的API进行编码。

为了节省篇幅,本文只介绍如何在Windows Phone上进行wav编码。有关web service和Media Foundation会在今后的wiki中介绍。注意Media Foundation需要使用C++编写代码,并且需要了解一些基础的COM知识。若是你不擅长C++或者不知道怎样使用COM,可以考虑在阅读该系列今后的文章之前先学习一下。你可以从这里开始。

wav格式

想要了解wav文件的格式,最好的方式是进行一下搜索。例如,这篇文章就介绍得满详细的。下面的说明会使用到这篇文章中列出的术语,所以请对照参考。

简单来说,一个wav文件分成三个chunk:RIFF type chunk, format chunk, data chunk。

RIFF type chunk说明了这个文件是一个wav文件。其中chunk ID必须是RIFF这串字符的二进制表示,而RIFF type必须是WAVE这串字符的二进制表示。中间还插了一个chunk data size,指明文件大小。

Format chunk说明了音频的一些信息,例如使单声道还是多声道(number of channels),每秒钟的采样数(sample rate或者samples per second),每一个sample占的位(significant bits per sample或者bits per sample),等等。注意不同的地方使用的术语可能稍有差异。为了避免造成歧义,我们将会使用Media Foundation中的术语,这和上面指出的那篇文章的差异在于两点:

上面的文章 Media Foundation
sample rate samples per second
significant bits per sample bits per sample

除了注意到这些属性之外,还要注意某些属性是通过公式计算出来的。例如block align = bits per sample / 8 * num channels,意味着每个sample占的字节数(位数除以8)。如果有多声道,要乘上声道的数量。具体公式在上面引用的文章中都能找到。

Data chunk就是真正的数据了,除了chunk ID以及真正的数据的大小(chunk size)之外,剩下的部分就是PCM的数据。

接下来的几个chunk,例如fact chunk,都是可以省略的。很多wav文件并不包括这些chunk。今天我们不会讨论它们。

因此,我们进行wav编码所要做的事,其实就是添加RIFF type chunk和format chunk,以及data chunk中的chunk ID和chunk size。

编码

为了进行编码,我们可以写一个类叫做WavEncoder。其中定义一个方法,接受一个Stream做参数,代表原始的PCM stream。它返回的则是一个wav stream:

public Stream Encode(Stream inputStream)

随后自然要做一些必要的参数检查:

            if (inputStream == null || !inputStream.CanRead)
{
throw new ArgumentNullException("inputStream");
}
 
if (inputStream.Length == 0 || inputStream.Length > (long)int.MaxValue)
{
throw new ArgumentOutOfRangeException("Length", "The stream is too short or too long.");
}

接下来我们就可以开始写入wav stream了,按照上面说的三段chunk格式。

第一段:

            Encoding utf8 = Encoding.UTF8;
MemoryStream wavStream = new MemoryStream();
 
// Capture the length of the raw audio stream.
int dataLength = (int)inputStream.Length;
 
// RIFF
wavStream.Write(utf8.GetBytes("RIFF"), 0, 4);
 
// We can't obtain the file size now. So we use a place holder, and skip to WAVE.
wavStream.Write(new byte[4], 0, 4);
wavStream.Write(utf8.GetBytes("WAVE"), 0, 4);

注意到这里我们还无法获得文件大小,所以相关数据可以先置零,今后在进行填充。

第二段:

            // Header chunk ID (always "fmt")
wavStream.Write(utf8.GetBytes("fmt "), 0, 4);
 
// Chunk data size (always 16 in this case)
wavStream.Write(BitConverter.GetBytes(16), 0, 4);
 
// Compression code (1 for PCM)
wavStream.Write(BitConverter.GetBytes(1), 0, 2);
 
// Number of channels (1 since we recorded with a single microphone)
wavStream.Write(BitConverter.GetBytes(NumChannels), 0, 2);
 
// Samples per second
wavStream.Write(BitConverter.GetBytes(this.SamplesPerSecond), 0, 4);
 
// Average bytes per second
wavStream.Write(BitConverter.GetBytes(AverageBytesPerSecond), 0, 4);
 
// Block align
wavStream.Write(BitConverter.GetBytes(BlockAlign), 0, 2);
 
// Bits per sample
wavStream.Write(BitConverter.GetBytes(this.BitsPerSample), 0, 2);
 
 
// Extra format bytes don't exist for PCM. So we don't write it.

注意到这边为了获得二进制表示的整型数据我们使用了BitConverter

你可能会想,到底这些值应该取什么才好。事实上Windows Phone的麦克风是有固定值的,例如samples per second是16000,num channels是1,bits per sample是16。其它值可以通过上面引用的文章中提供的公式计算。

        public int SamplesPerSecond { get; set; }
public int NumChannels { get; set; }
public int BitsPerSample { get; set; }
public int BlockAlign { get; set; }
public int AverageBytesPerSecond { get; set; }
 
public WavEncoder()
{
this.SamplesPerSecond = 16000;
this.NumChannels = 1;
this.BitsPerSample = 16;
this.BlockAlign = this.BitsPerSample / 8 * this.NumChannels;
this.AverageBytesPerSecond = this.SamplesPerSecond * this.BlockAlign;
}

第三段:

            // Data chunk ID (always "data")
wavStream.Write(utf8.GetBytes("data"), 0, 4);
 
// Data chunk size (the raw audio stream's length)
wavStream.Write(BitConverter.GetBytes(dataLength), 0, 4);
 
 
inputStream.CopyTo(wavStream);

这里的dataLength是inputStream的大小,也就是PCM数据的长度。

            // Capture the length of the raw audio stream.
int dataLength = (int)inputStream.Length;

最后,我们需要计算整个文件大大小,并且减去8,放到最初的RIFF chunk的chunk data size一块。

            // Finally, populate the RIFF chunk data size, which begins from 4.
// The value is file size - 8.
wavStream.Position = 4;
wavStream.Write(BitConverter.GetBytes((int)inputStream.Length - 8), 0, 4);

至此,编码就完成了。我们可以返回wavStream。

            wavStream.Position = 0;
return wavStream;

检验

为了检验wav编码是否正确,我们可以调用上面的Encode方法,并且将返回的数据存储到isolated storage中,变成一个真正的wav文件。由于编码以及存储文件可能花较多时间,请不要在UI线程上进行。你可以使用一个BackgroundWorker创建一个后台线程执行那些任务。

        private void SaveWavButton_Click(object sender, RoutedEventArgs e)
{
if (this._stream == null || this._stream.Length == 0)
{
return;
}
this._stream.Position = 0;
 
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += new DoWorkEventHandler(worker_DoWork);
worker.RunWorkerAsync();
}
 
void worker_DoWork(object sender, DoWorkEventArgs e)
{
WavEncoder encoder = new WavEncoder(this._microphone.SampleRate);
Stream wavStream = encoder.Encode(this._stream);
 
// Store the file in isolated storage.
var iso = IsolatedStorageFile.GetUserStoreForApplication();
using (var fileStream = iso.CreateFile("test.wav"))
{
wavStream.CopyTo(fileStream);
}
}

有很多种方式可以查看isolated storage中的数据。我使用的是CodePlex上的Windows Phone Power Tools

如果你用的是模拟器,可以直接将isolated storage中的wav文件保存到本地电脑,随后使用Windows Media Player或者其它支持wav的播放器播放,以证明wav文件是正确的。

WPPowerTool.png

总结

本文介绍了如何在Windows Phone上录制声音并且编码成wav。今后还会有其它wiki文章介绍如何将这些wav文件上传到Windows Server上,并且使用Media Foundation编码成其它格式。等这些文章都完成后,你甚至可以尝试在这基础上为录音添加背景音乐,或者加入一段视频,并且混合成一个完整的音频/视频文件。

This page was last modified on 16 July 2013, at 07:40.
107 page views in the last 30 days.

Was this page helpful?

Your feedback about this content is important. Let us know what you think.

 

Thank you!

We appreciate your feedback.

×