原文 Windows Phone 8初学者开发—第20部分:录制Wav音频文件
系列地址: http://channel9.msdn.com/Series/Windows-Phone-8-Development-for-Absolute-Beginners
源代码: http://aka.ms/absbeginnerdevwp8
PDF版本: http://aka.ms/absbeginnerdevwp8pdf
在本课中我们将编写录制自定义声音所需的代码。我们将利用Coding4Fun工具包以简化录制工作,但是我们还需要理解内存流 (MemeoryStream)和电话的独立存储 (IsolatedStorage)等概念。
本课的计划:
- 我们将修改ToggleButton以将事件处理程序方法关联到Checked和Unchecked事件。
- 我们将使用Code4Fun工具包Audio命名空间中的MicrophoneRecord类以开始和停止录制过程。
- 当我们停止录制时,我们需要将存储在手机内存中的声音临时保存到磁盘,这样就可以播放或永久保存它。
- 我们将添加一个MediaElement控件用于播放声音。
- 根据用户的录制动作管理播放声音按钮的状态(启用或禁用)。
我想说的是这可能是本系列中最具挑战的一课,因为它涉及一些稍微高级一些的内容。您应该主动接受这些内容,努力才能学到东西,挑战有难度的概念将帮 助您更快成长。请不要只观看本视频,您还需要同时阅读我引用的MSDN文章以获取更多信息。所以请调动您的思维,让我们开始吧。
1. 修改ToggleButton控件以关联事件处理程序方法
在RecordAudio.xaml页面中对ToggleButton进行如下编辑:
在39和40行,我们为ToggleButton的两个状态关联了处理程序。
2. 创建Coding4Fun.Toolkit.Audio.MicrophoneRecord类的私有实例
在RecordAudio.xaml.cs文件中添加以下代码行:
我们创建了一个新的MicrophoneRecorder类的私有实例,并使用悬停于蓝色虚线上的技术添加Coding4Fun.Toolkit.Audio命名空间的using语句。
现在我们可以在ToggleButton的Checked和Unchecked事件处理程序方法中添加代码以开始或停止MicrophoneRecorder。
3. 将MicrophoneRecorder收集的声音数据保存到文件
当我们进行录制时,MicrophoneRecorder对象在缓冲区中收集声音信息。缓冲区只是专门用于存储数据的一块内存区域。 缓冲区通常在接收数据和处理数据的速率之间存在差异或者在这些速率是可变的情况下被使用。因此,我们可能需要花10秒钟来录制10秒钟的声音,在这段时间 数据被添加到缓冲区中。尽管如此,计算机在几分之一秒内就可以处理这些数据。缓冲区就是一个队列,我们可以以一种速度写数据而以另一种速度读取数据。编程 中缓冲区通常用于物理硬件和软件之间,或者当数据在内存和磁盘之间或内存和网络连接之间来回移动时被使用。我不是一个计算机科学方面的专家,所以我将缓冲 区想象成一个桶,您可以慢慢收集东西直到您准备一次性地处理整个桶里的东西。所以,我在收集海滩上的贝壳并将它们放到我的桶里。一旦收集满一桶,我开始处 理它们,决定哪些需要保留,哪些要扔掉。收集要花很长时间,然后一旦收集满一桶,处理过程将很迅速。这就是我对缓冲区的理解。
当我们调用Stop()方法时,MicrophoneRecord将停止添加声音信息,但是在内存中的缓冲区持有数据,现在我们需要处理缓冲区中的数据。在本例中,当我使用术语“处理”时,我的意思是从缓冲区抓取声音数据并以WAV文件格式放置到一个临时文件中。
一旦它位于手机存储的文件中,我可以把文件交给MediaElement并让它播放临时的WAV文件。如果用户需要保留该文件,我可以重新命名并永久保存它。
让我们接着讨论在Windows Phone上存储文件。手机上的存储空间被分为独立的区域。手机上安装的每个应用程序将获得其中一个独立区域。我使用术语“独立”是因为一个应用程序不能 访问另一个应用程序的存储区域。这是出于安全的考虑,一个应用程序不能翻您的照片、笔记或其它机密数据并将它上载到一个秘密的恶意服务器或用恶意方式损坏 它。
这个专属于您应用程序的独立和持久的存储区域称为独立存储。这是一个保持每个应用程序数据安全的井然有序的方式,因为每个应用程序只能从它自己的存储区域进行读写。
所以回到手头的问题,MicrophoneRecorder对象有一组位于内存缓冲区的数据,即一个MemoryStream对象。我的工作是从内 存流中将数据保存到一个位于独立存储的临时文件中。为此,我将创建一个辅助方法,它将MemoryStream作为输入参数,在辅助方法中我将在独立存储 中创建一个新的文件,然后将所有内存流缓冲区的数据转储到该文件。
这就我们的计划,让我们用代码来构建它:
如上面的截图所示,输入参数的类型是MemoryStream,我们需要使用一个using语句以引用System.IO。
接着,我们将使用一个不同类型的using语句:
在这里using语句将创建System.IO.IsolatedStorage.IsolatedStorageFile实例的上下文。任意位于 using语句定义的代码块中的代码都将可以访问isoStore变量。一旦执行流离开右大括号,isoStore变量将从内存中被正确释放。
当您想使用实现了IDisposable接口的类时,您可以使用这个using语法。通常它们是.NET基类库(或者在本例中是Windows Phone API)中的托管类型,并且需要访问非托管资源。所以IDisposable接口的主要用途是释放非托管资源。当托管对象不再使用时,垃圾回收器将自动释 放分配给托管对象的内存。尽管如此,我们却无法预测垃圾收集将在何时出现。此外,垃圾回收器对窗口句柄或打开的文件和流等非托管资源一无所知。危险在于当两个或多个进程(应用程序)试图在同一时间访问同一资源时会造成不可恢复的错误。实现IDisposable接口的类型能够正确处理这些场景。如果需要了解有关该主题的更完整的解释,请参考:
using 语句(C# 参考)
http://msdn.microsoft.com/en-us/library/yh598w02.aspx
IDisposable
接口
http://msdn.microsoft.com/en-us/library/system.idisposable.aspx
IsolatedStorageFile类提供帮助您管理应用程序使用的文件和文件夹的方法。我们使用GetUserStoreForApplication()检索应用程序的独立存储区域。
接着,我们将抓取缓冲区并尝试在独立存储区域创建一个文件:
在第69行,我添加了一行代码,它从缓冲区(作为输入参数传递给本辅助方法)检索值并将它转换为wav(音频)文件格式。如您所
见,MemoryBuffer没有实现该方法。相反,我们需要使用Coding4Fun.Toolkit.Audio.Helpers命名空间中的扩展方
法。所以为了应用这个扩展方法,我们将在代码文件顶部添加另一个using语句:
.NET中的扩展方法允许您将方法附加到任意类型。所以我可以添加一些工具到int或string,或者在本例中添加到
System.IO.MemoryStream。扩展方法允许您像任意公共方法一样使用类型的成员。如需了解扩展方法的更多信息,或创建自己的扩展方法,
可以查看:
http://msdn.microsoft.com/en-us/library/vstudio/bb383977.aspx
注意:这是一个高级主题,现在只要明白它的功能和目的就可以了。以后您将学习如何创建自己的扩展方法。
好的,现在我们将缓冲区中的数据转换成了wav(音频)格式,我们只需要将它存放到设备独立存储区域的新文件中:
(1)我们为wav文件创建了一个临时名称。随后我们可以丢弃该文件(如果用户再次点击Record按钮)。
(2) CreateFile()方法使用我们传递的名称(在上面的73行)在文件系统创建了一个文件,并返回一个该文件的引用,我们可以利用该引用将内容写入文件。
(3)现在我们有一个空文件可以写入,我们将写入保存于bytes变量中的声音数据。Write()方法的第二个和第三个参数允许我们指定写入声音的哪个部分。本例中,我们将写入全部声音数据,从开始(0)到结束(bytes.Length)。
4. 添加MediaElement以播放新的临时文件
在RecordAudio.xaml文件中,我们将在ContentPanel grid控件的下方添加一个MediaElement控件,赋予其名称并设置AutoPlay为false:
回到RecordAudio.xaml.cs,我们将通过编程方式设置新的MediaElement的来源为新的临时声音文件:
现在我们已经准备好播放声音文件,我们仅需要为Play按钮关联事件处理程序。
5. 处理播放按钮的单击事件以测试声音
在RecordAudio.xaml文件中,我们将在content为"Play"的Button元素的定义部分添加一个名称为"PlayAudioClick"的事件处理程序:
导航到新的事件处理程序并添加以下代码行以调用MediaElement控件的Play()方法:
至此我已将它全部关联起来,但是它并不完善,还很脆弱。所以我们需要对代码重新梳理并进行防御性编程,我们将在本课后续部分完成它。
现在让我们测试应用程序。我们已经编写了很多代码,基于我们已创建功能的性质,分小部分进行测试并无可能。我们必须在全部完成后测试哪里会出现问题。
6. 为应用程序声明Microphone功能
运行应用程序,单击应用栏中的microphone图标,然后单击Record ToggleButton。
当您单击Record时,以下两种情况之一将可能发生。根据您所使用的Coding4Fun工具包的版本,或者应用程序将在没有错误信息的情况下消失,或者您会收到一个异常。
对于Windows Phone,在需要使用某些设备的功能时,您需要请求获得上述功能的权限。这样做的目的之一是通知用户我们打算如何使用他们的设备。如果我们的应用程序要使用他们的地理位置或相机但这些又与应用程序的用途无关时,他们就会产生怀疑。
获得手机硬件功能的方法是打开WMAppManifest.xml文件并转到第二个“功能”选项卡:
- 打开WMAppManifest.xml文件
- 选择第二个功能选项卡
- 在ID_CAP_MICROPHONE功能旁添加选中标记
如果我们重新运行程序并执行相同的步骤,它应该能够正常工作。注意:为了在手机模拟器上录制声音,您需要一个连接到计算机的麦克风。在开始前请确认麦克风能够正常工作,这样您就可以确认它不是造成问题的原因:
我认为现在最大的问题是代码相当脆弱并容易崩溃。如果我们点击Play按钮并立即再次点击record按钮,很可能造成应用程序的崩溃;我们没有提
供任何视觉反馈以让用户知道已经完成了什么,还有什么要做;我们并未真正永久保存声音,也没有允许用户为新的声音选择一个名称。我们将在适当时间解决所有
这些问题。
现在,让我们通过添加防御性编程语句来提高代码的质量。
7. 添加防御性编程语句以防止潜在的异常
首先,在SaveTempAudio辅助方法中,在我们尝试创建文件并向其中填充缓冲区的内容前,我们需要确认缓冲区中存在数据:
接着,当用户点击ToggleButton录制声音时我需要防止MediaElement正在播放的可能。为此,我需要重构代码,将IsolateStorageFileStream作为类的私有成员:
现在我将添加代码以确定_audioStream是否为空。如果它不为空就意味着有需要保存的数据(译者注:应
该是意味着需要释放_audioStream占用的文件资源),然后我需要将它清空。在这里我将确认MediaElement不再播放声音,并且它的
Source属性被设置为空。这将会释放上一次MediaElement持有的_audioStream,MediaPlayer有可能保留着一个来自上
次录制时的_audioStream的引用,在尝试使用相同变量名_audioStream创建一个新的文件前,我希望MediaPlayer释放该引
用。
- 我在这里检查_audioStream是否为空。如果不为空,我需要确保不在播放模式下使用_audioStream。在74和75行,我释放MediaElement持有的 _audioStream,然后
- 调用_audioStream的dispose方法。这将正确释放_audioStream使用的任意资源。它现在已准备好再次被新创建的文件句柄填充。
- 我将重新编写这里的代码。我不需要创建一个新的_audioStream的实例。它已经经过清理并且释放了所有资源。所以我仅需要将_audioStream设置为一个新的文件引用。
现在我需要考虑如何创建并引用临时文件的名称。我需要确保没有遗留老的临时文件,因为它们可以像额外文件占用太多硬盘空间那样塞满应用程序的独立存储。不同之处是独立存储远小于硬盘,并且音频文件可能很大。所以,首先我将它的声明移动到私有成员变量处(下图24行):
接着我将:
var tempFileName = "tempWav.wav";
替换为:
- 如果来自上一次录制尝试的临时文件已经存在于用户的设备上,我们需要调用.DeleteFile()方法以删除该文件。
- 我们希望给文件一个唯一的名称,所以我们使用DateTime.Now.ToFileTime()给它一个精确到秒的唯一名称。这应该足够满足我们的需求。
接着,我想在录制时禁用Play按钮。这个简单的状态管理应该会减少开发人员的麻烦并降低用户造成应用程序崩溃的可能性。我将按钮命名为"PlayAudio":
接着,我将在ToggleButton的事件处理程序中执行一些状态管理:
- 当我开始录制时,我将禁用PlayAudio按钮,并且
- 当我停止录制时,我将启用PlayAudio按钮。
回顾
综上所述,本课的重点包括利用Coding4Fun工具包的音频功能大大降低使用手机的麦克风录制声音的复杂性。如果您想一探究竟,您可以在必应中搜索类的完整名称并找到Codeplex上的代码清单:
http://coding4fun.codeplex.com/SourceControl/changeset/view/79216#1425409
我们学习了在C#中使用using语句以正确释放使用非托管资源的托管类。我们学习了缓冲区的功能,它通常作为以一种速度采集数据并以不同速度处理
数据的一种手段。我们使用了内置于Coding4Fun
MicrophoneRecorder类的缓冲区并检索缓冲区以将它保存到设备存储上的一个文件中。我们学习了如何利用独立存储保护每个应用程序的存储区
域,保持对于系统上其它应用程序的私有性。我们简要学习了扩展方法,因为我们使用了在Coding4Fun Audio
Helpers(Coding4Fun
音频辅助)命名空间中实现的一个扩展方法。最后,我们练习了一些防御性编程技术以确保我们的应用程序可以处理特殊或边界情况。