×
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 10:40.
158 page views in the last 30 days.