在Windows Phone上制作录音机(译文)
By S.F.
本文链接 https://www.kyfws.com/news/making-a-voice-recorder-on-windows-phone/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 14 分钟阅读 - 6576 个词 阅读量 0在Windows Phone上制作录音机(译文)
原文地址:https://www.codeproject.com/Articles/175122/Making-a-Voice-Recorder-on-Windows-Phone
原文作者:Joel Ivory Johnson
译文由本站翻译
前言
演示在Windows Phone 7上制作语音记录器需要做什么,包括将原始字节从录音转换为WAVE文件.
介绍
论坛上出现了一系列与使用麦克风,从中获取可用录音有关的常见问题.我确信问题会再次出现,并且我认为有一个例子可以在这些问题出现时参考.我已经制作了此语音备忘录应用程序,并通过了Marketplace认证,并使所有希望使用它的人都可以使用源代码.随意以您想要的任何方式使用此代码.如果您想在自己的应用程序中使用它,不胜感激,收到一条消息只是告诉我您发现该代码很有用.但是,如果您不这样做,我不会反对您.此代码没有义务.尽管我极力劝阻您以未经修改的形式将其提交回Windows Phone Marketplace.
我肯定还没有花任何力气使这个程序界面漂亮.这篇文章主要是关于功能的,并且由于我放弃了代码,所以我不想花太多钱在我的图形艺术家身上,只放弃图像资产.如上所述,如果使用代码,则取决于您自己应用图形.由于我已经将该程序组合在一起,因此我计划在下周对其进行更多更改(当务之急:使该程序看起来不错!).
我需要什么?
您需要使用此代码的唯一软件是Windows PC和Windows Phone开发者工具,可从http://developer.windowsphone.com下载(免费下载!) .我正在使用Visual Studio 2010 Ultimate.但是开发人员工具中的Express版本也可以正常工作.
确定功能集
在开始该应用程序之前,我坐下来列出了我希望该应用程序具有的功能并对其进行了优先排序.我想到的一些事情没有特别的顺序,包括:
如您所见,可以在语音备忘录应用程序中添加许多不同的内容.它可以快速地从简单的事情发展到复杂的事情.我没有使应用程序过于复杂,而是选择了一个最小的功能集,以便可以实现一个主要目标,即实际生成要交付的东西要足够简单,以使我没有很多可能发生错误的地方.精简功能集如下:
这是一个简单的开始,以后可以添加其他功能.
使用Silverlight应用程序中的XNA类
您可以在Windows Phone上创建两种类型的应用程序:使用Silverlight作为其UI的应用程序以及利用XNA渲染类作为UI的应用程序.您必须专门使用一种类型的UI表示层或另一种类型.您无法使用Silverlight应用程序中的XNA渲染类,反之亦然. Silverlight提供了一些控件,这些控件可用于从设计人员构建应用程序的UI,例如按钮,文本框,标签等.在XNA中,您负责构建自己的解决方案来呈现信息.因此,我正在为此应用程序使用Silverlight UI.
为了录制音频,我必须使用Microsoft.Xna.Framework.Audio中的Microphone类.虽然不能在Silverlight应用程序中使用XNA渲染类,但可以使用许多其他XNA类.使用与音频相关的XNA类需要定期调用FrameworkDispatcher.Update()
.您可以使用Microsoft提供的示例ApplicationService来执行相同的功能,而不必使用调用此函数的计时器来使程序逻辑复杂化.该类将为您调用此函数.整个课程如下.
public class XNAFrameworkDispatcherService : IApplicationService
{
private DispatcherTimer frameworkDispatcherTimer;
public XNAFrameworkDispatcherService()
{
this.frameworkDispatcherTimer = new DispatcherTimer();
this.frameworkDispatcherTimer.Interval = TimeSpan.FromTicks(333333);
this.frameworkDispatcherTimer.Tick += frameworkDispatcherTimer_Tick;
FrameworkDispatcher.Update();
}
void frameworkDispatcherTimer_Tick(object sender, EventArgs e)
{ FrameworkDispatcher.Update(); }
void IApplicationService.StartService(ApplicationServiceContext context)
{ this.frameworkDispatcherTimer.Start(); }
void IApplicationService.StopService() { this.frameworkDispatcherTimer.Stop(); }
}
在项目中声明该类后,需要将其添加为应用程序生存期对象.有多种方法可以做到这一点.但是我的首选方法是将其添加到App.xaml.
<Application.ApplicationLifetimeObjects>
<!--Required object that handles lifetime events for the application-->
<shell:PhoneApplicationService
Launching="Application_Launching" Closing="Application_Closing"
Activated="Application_Activated" Deactivated="Application_Deactivated"/>
<local:XNAFrameworkDispatcherService />
</Application.ApplicationLifetimeObjects>
完成此操作后,我无需再考虑FrameworkDispatcher.Update
了.它会在程序启动时自动启动,并在程序结束时自动关闭.
使用Microphone类录制音频
互联网上有很多关于如何在WP7上录制音频的示例.不幸的是,它们中的许多也包含相同的错误.在介绍有关如何实现录制的代码之前,我想直观地说明录制的工作方式,以便我也可以演示该错误. “麦克风"类以块的形式记录音频,并将每个块传回程序,同时继续记录新的块.为此,“麦克风"类具有自己的内存缓冲区,它将填充.假设您正在录制这样的短语:“那只棕色的狐狸跳过了那只懒狗.“现在,我们还假设麦克风的缓冲区一次只能记录一个单词(在现实生活中,事情通常不会像现在这样干净,但是我请您暂时中止运用该思想的能力) .
您开始说出该短语,并且麦克风的缓冲区充满了您说” the"一词的声音.
一旦缓冲区已满,它将被传递到程序,并且麦克风开始使用记录下一个单词的新缓冲区填充.该程序将接收缓冲区,并有机会对其进行处理.由于此程序用于保存和重放记录的音频剪辑,因此该程序将保存音频块并等待下一个块附加到前一个块.
记录每个块时,它将传递给程序,然后程序将其附加到已接收的块中.当用户说出最后一个单词时,就会出现许多在线示例中的错误.
在许多在线示例中,当用户说出"狗"字并按下"停止"按钮时,程序将停止从麦克风接收更多信息.但是尚未将最后一个单词从麦克风缓冲区传递到程序!最终结果是程序已接收到除最后一个单词以外的所有内容.为避免此问题,应该发生的情况是,当用户停止记录器时,而不是立即停止,该程序应等到接收到一个缓冲区再停止.在最坏的情况下,句子结尾后可能还会有一些声音也被记录下来,但这比丢失数据要好.可以通过减小缓冲区的大小来减少捕获的额外数据量.
创建执行上述操作的代码相当容易.要获取Microphone''类的实例,我们可以从
Microphone''.Current中获取它.麦克风录音时,它将通过引发BufferReady
事件来通知我们的程序已准备好读取缓冲区.发生这种情况时,我们可以通过调用GetBuffer(byte [] destination)来获取缓冲区数据.对于此方法,我们必须传递一个将接收数据的字节数组.这个缓冲区需要多大? “麦克风"类还有另外两个成员,这些成员将帮助我们确定所需的大小. Microphone''.BufferDuration将使我们知道可以在麦克风的缓冲区中存储多少秒,而
Microphone''.GetSampleSizeInBytes(Timespan)方法将告诉我们记录特定长度需要多少字节.将两者放在一起,可以通过Microphone''.GetSampleSizeInBytes(
Microphone``.BufferDuration)找到我们需要的缓冲区大小.一旦拥有了Microphone类的实例,订阅了BufferReady事件并创建了用于接收数据的缓冲区,就可以通过调用Microphone来启动录音过程. .
在” BufferReady"的事件处理程序中,需要完成一些事情.从缓冲区中检索数据时,需要在某些位置累积数据.积累数据后,我们需要检查是否已发出停止记录的请求.如果已经存在,则告诉"麦克风"实例停止使用"麦克风” .Stop()发送数据,并执行需要进行任何操作以保持记录.为了积累数据,我将使用一个存储流,然后在记录完成时将其写入隔离存储.我的要求之一是音频数据将以WAV格式保存.在我全部写出所有内容之前,可以通过编写适当的wave标头来满足此要求.收到的字节.我没有在这里详细介绍如何做到这一点,而是让您参考我在该主题上撰写的上一篇博客文章.执行以上所有操作的代码如下:
public void StartRecording()
{
if (_currentMicrophone == null)
{
_currentMicrophone = Microphone.Default;
_currentMicrophone.BufferReady +=
new EventHandler<EventArgs>(_currentMicrophone_BufferReady);
_audioBuffer = new byte[_currentMicrophone.GetSampleSizeInBytes(
_currentMicrophone.BufferDuration)];
_sampleRate = _currentMicrophone.SampleRate;
}
_stopRequested = false;
_currentRecordingStream = new MemoryStream(1048576);
_currentMicrophone.Start();
}
public void RequestStopRecording()
{
_stopRequested = true;
}
void _currentMicrophone_BufferReady(object sender, EventArgs e)
{
_currentMicrophone.GetData(_audioBuffer);
_currentRecordingStream.Write(_audioBuffer,0,_audioBuffer.Length);
if (!_stopRequested)
return;
_currentMicrophone.Stop();
var isoStore =
System.IO.IsolatedStorage.IsolatedStorageFile.GetUserStoreForApplication();
using (var targetFile = isoStore.CreateFile(FileName))
{
WaveHeaderWriter.WriteHeader(targetFile,
(int)_currentRecordingStream.Length, 1, _sampleRate);
var dataBuffer = _currentRecordingStream.GetBuffer();
targetFile.Write(dataBuffer,0,(int)_currentRecordingStream.Length);
targetFile.Flush();
targetFile.Close();
}
}
音频播放
为了播放音频,我将使用” SoundEffect"类.与Microphone类类似,SoundEffect是XNA音频类,需要周期性调用FrameworkDispatcher.Update()方法.有两种方式可以加载WAVE文件.我可以自己解码标头,也可以让” SoundEffect"类来实现.如果有人需要对该文件进行其他修改,我将在此处显示手动解码以供参考.
当通过其构造函数实例化" SoundEffect"时,需要三项数据:记录的音频数据,采样率和记录中的音频通道数.此应用程序将仅以单声道而不是立体声进行录制.因此,总会有一个音频通道.我可以通过在此字段中传递AudioChannels.Mono
逃脱.但是将来,我可能会添加导入录音的功能(可能是立体声的),因此我将从Wave标头中提取此数据.同样,我也可以从"麦克风"类中获取采样率,而不是从Wave标头中获取采样率.但是为了我将来考虑的事情,我也将从头文件中获取它. Wave数据本身就是标题之后的所有内容.初始化SoundEffect
之后,要播放它,我必须先获得SoundEffect
Instance实例,然后调用其Play
方法.
我认为不需要解释为什么每次只播放一张录音.因此,在播放新的音频剪辑之前,请检查内存中是否有现有的音频剪辑,然后将其停止.
public void PlayRecording(RecordingDetails source)
{
if(_currentSound!=null)
{
_currentSound.Stop();
_currentSound = null;
}
var isoStore = System.IO.IsolatedStorage.IsolatedStorageFile.
GetUserStoreForApplication();
if(isoStore.FileExists(source.FilePath))
{
byte[] fileContents;
using (var fileStream = isoStore.OpenFile(source.FilePath, FileMode.Open))
{
fileContents = new byte[(int) fileStream.Length];
fileStream.Read(fileContents, 0, fileContents.Length);
fileStream.Close();//not really needed, but it makes me feel better.
}
int sampleRate =((fileContents[24] << 0) | (fileContents[25] << 8) |
(fileContents[26] << 16) | (fileContents[27] << 24));
AudioChannels channels = (fileContents[22] == 1) ?
AudioChannels.Mono : AudioChannels.Stereo;
var se = new SoundEffect(fileContents, 44,
fileContents.Length - 44, sampleRate, channels, 0,
0);
_currentSound = se.CreateInstance();
_currentSound.Play();
}
}
通过SoundEffect.FromFile
加载声音很简单.
public void PlayRecording(RecordingDetails source)
{
SoundEffect se;
if(_currentSound!=null)
{
_currentSound.Stop();
_currentSound = null;
}
var isoStore = System.IO.IsolatedStorage.
IsolatedStorageFile.GetUserStoreForApplication();
if(isoStore.FileExists(source.FilePath))
{
byte[] fileContents;
using (var fileStream = isoStore.OpenFile(source.FilePath, FileMode.Open))
{
se = SoundEffect.FromStream(fileStream);
fileStream.Close();//not really needed, but it makes me feel better.
}
_currentSound = se.CreateInstance();
_currentSound.Play();
}
}
跟踪记录
除了将录音保存在单独的存储中之外,我还想跟踪其他一些事情,例如录音的日期,录音的标题和录音的注释.可以通过文件名为记录命名,也可以从文件上的日期推断出记录的日期,但是这种解决方案似乎并不持久.文件名中可能会出现一些字符限制,将来,当我添加导入和导出文件的功能时,文件日期可能会丢失.相反,我制作了一个类,其中包含我想在录音中跟踪的所有信息.下面是该类的简化视图.
public class RecordingDetails
{
public string Title { get; set; }
public string Details { get; set; }
public DateTime TimeStamp { get; set; }
public string FilePath { get; set; }
public string SourcePath { get; set; }
}
为了使该类易于阅读,我给出了一个简化的视图.此类需要可序列化,以便我可以从隔离的存储中读取和写入它.因此,该类以[DataContract]属性修饰,而属性以[DataMember]属性修饰.我还计划将此类的实例绑定到UI元素.因此,此类需要实现INotifyPropertyChanged接口.此类的版本如下.它看起来不像打字.我使用Visual Studio代码段自动生成部分代码.
[DataContract]
public class RecordingDetails: INotifyPropertyChanged
{
// Title - generated from ObservableField snippet - Joel Ivory Johnson
private string _title;
[DataMember]
public string Title
{
get { return _title; }
set
{
if (_title != value)
{
_title = value;
OnPropertyChanged("Title");
}
}
}
//-----
// Details - generated from ObservableField snippet - Joel Ivory Johnson
private string _details;
[DataMember]
public string Details
{
get { return _details; }
set
{
if (_details != value)
{
_details = value;
OnPropertyChanged("Details");
}
}
}
//-----
// FilePath - generated from ObservableField snippet - Joel Ivory Johnson
private string _filePath;
[DataMember]
public string FilePath
{
get { return _filePath; }
set
{
if (_filePath != value)
{
_filePath = value;
OnPropertyChanged("FilePath");
}
}
}
//-----
// TimeStamp - generated from ObservableField snippet - Joel Ivory Johnson
private DateTime _timeStamp;
[DataMember]
public DateTime TimeStamp
{
get { return _timeStamp; }
set
{
if (_timeStamp != value)
{
_timeStamp = value;
OnPropertyChanged("TimeStamp");
}
}
}
//-----
// SourceFileName - generated from ObservableField snippet - Joel Ivory Johnson
private string _sourceFileName;
[IgnoreDataMember]
public string SourceFileName
{
get { return _sourceFileName; }
set
{
if (_sourceFileName != value)
{
_sourceFileName = value;
OnPropertyChanged("SourceFileName");
}
}
}
//-----
// IsNew - generated from ObservableField snippet - Joel Ivory Johnson
private bool _isNew = false;
[IgnoreDataMember]
public bool IsNew
{
get { return _isNew; }
set
{
if (_isNew != value)
{
_isNew = value;
OnPropertyChanged("IsNew");
}
}
}
//-----
// IsDirty - generated from ObservableField snippet - Joel Ivory Johnson
private bool _isDirty = false;
[IgnoreDataMember]
public bool IsDirty
{
get { return _isDirty; }
set
{
if (_isDirty != value)
{
_isDirty = value;
OnPropertyChanged("IsDirty");
}
}
}
//-----
public void Copy(RecordingDetails source)
{
this.Details = source.Details;
this.FilePath = source.FilePath;
this.SourceFileName = source.SourceFileName;
this.TimeStamp = source.TimeStamp;
this.Title = source.Title;
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
遍布代码的[DataMember]
属性使我可以使用数据协定序列化来读写此类.由于我使用的是DataContractSerializer,因此不必担心文件在保存和加载时如何编码的细节.虽然使用隔离存储并不难,但我正在使用[以前的博客条目](http://www.j2i.net/BlogEngine/post/2011/02/10/Simple-Data -Serialization-on-WP7.aspx),以简化几行代码的序列化和反序列化.当用户创建新记录时,还将创建此类的新实例.除了标题,注释和时间戳记,此类还包含描述的记录的路径,并且包含非序列化成员SourceFileName
,该成员包含已从中加载此数据的原始文件的名称.没有这些信息,如果用户决定更新数据,则无法知道保存内容时应覆盖哪个文件.
//Saving Data
var myDataSaver = new DataSaver<RecordingDetails>() {};
myDataSaver.SaveMyData(LastSelectedRecording,
LastSelectedRecording.SourceFileName);
//Loading Data
var myDataSaver = new DataSaver<RecordingDetails>();
var item = myDataSaver.LoadMyData(LastSelectedRecording.SourceFileName);
这样,您便拥有了执行记录,保存记录和加载记录所需的所有信息.当程序第一次启动时,我将其加载所有的RecordingDetails并将它们添加到我的View模型的ObservableCollection中.从那里,它们可以绑定到显示给用户的列表.
public void LoadData()
{
var isoStore =
System.IO.IsolatedStorage.IsolatedStorageFile.GetUserStoreForApplication();
var recordingList = isoStore.GetFileNames("data/*.xml");
var myDataSaver = new DataSaver<RecordingDetails>();
Items.Clear();
foreach (var desc in recordingList.Select(item =>
{
var result =myDataSaver.LoadMyData(String.Format("data/{0}", item));
result.SourceFileName = String.Format("data/{0}", item);
return result;
}))
{
Items.Add(desc);
}
this.IsDataLoaded = true;
}
保存状态和墓碑
您的程序可以随时被打来的电话或用户中断搜索或其他操作而中断.发生这种情况时,您的应用程序将被删除.操作系统将保存用户所在的页面,并使您的程序有机会保存其他数据.重新加载程序时,开发人员必须确保已采取步骤正确重新加载状态.在大多数情况下,我不需要担心逻辑删除,因为程序的大多数状态数据都会立即保存到隔离的存储中.而且没有太多的状态数据要保存.记录会随着程序设置的更改立即提交.如果您想了解有关墓碑的更多信息,我强烈建议您不要将其用作探索墓碑的资源.
那么您可以记录和播放,现在呢?
市场中有很多备忘录记录器.制作另一个的目的是什么?还有其他与声音相关的应用程序,可以利用此代码中实现的功能.语音备忘录音机不是我的最终目标.我的最终目标实际上并不是单一的,可以从此代码派生很多应用程序.现在,下面是我希望从该应用程序中得到的源代码系统发育.
不要从字面上看图表.只是为了说明一个概念.但是从上图可以看到,我可以采用此代码,进行一些更改,并产生具有不同目的的东西.如果我在程序中添加了对录音进行转换的内容,那么我将拥有一个语音转换器.通过一些傅立叶分析和一些其他代码,我可以生产出可以从唱片中打印出活页乐谱的东西(请注意:我在上面称其为抄写员,但我可能不在这里使用术语).
准备认证
认证可能需要几个小时到几天的时间(此处为http://go.microsoft.com/fwlink/?LinkID=202116&clcid=0x409).准备用于认证的应用程序时,所需的最少文件集是包含您的应用程序的XAP(请记住要进行发布!),至少一个屏幕截图以及一些具有各种尺寸(200x200\173x173和99x99像素)的图像图标).我不会在此处上介绍认证过程,但稍后会详细介绍.在等待认证时,您可能希望通过在网站上准备促销页面来打发时间.这里的(http://go.microsoft.com/fwlink/?LinkID=202116&clcid=0x804)是一组标准图像,用于将某些图像引向Marketplace.您可以从此处上获取图像,它们具有各种大小,颜色和语言. 您的应用通过认证后,您将能够看到指向您应用的直接链接.在此应用程序的情况下,它是[http://social.zune.net/redirect?type=phoneApp&id=268c6119-d755-e011-854c-00237de2db9e](http://social.zune.net/redirect?type =phoneApp&id =268c6119-d755-e011-854c-00237de2db9e).结合图片,我有一个可重新调整的下载链接,可以将其放在促销页面上:
下一步是什么?
我仅将此代码作为示例.从这里开始,我将改进自己的应用程序版本,并且可能会在未解决一些小错误的情况下更新本文中的版本.
历史
- 2010年3月31日
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
C#3.0 .NET3.5 C# .NET Mobile Dev Intermediate Silverlight Windows-Phone-7 新闻 翻译