•前言
今天遇到一个技术上的难题,如何通过web端调用本地音频设备并将音频文件上传到服务器?在网上搜集了很多资料,最终整理出来了一个版本,但还是无法满足目前的需求,借着这个机会,将源码贴出来给大家分享,希望能得到高手的指点,解决自己的困惑。好了,废话不多说,进入正题。(部分代码源自其他网友,因为无法追溯出处,在本文中未署名原创作者信息,如有侵权,请告知)
•思路
目前调用本地音频设备常用的方式有三种:
1)在web项目中引用DirectX.dll。但是本人愚钝,结合网上的知识,没有得到想要的结果,所以pass;
2)在网上搜集Flash插件。好嘛,在网上搜集的一些Demo大多数都是一些极其简单的,并不能满足需求。缺点:只能将音频文件保存到本地;无法获取保存后的文件路径;样式太丑;很多东西无法自定义,如录音时间、保存格式等等,所以pass;
3)通过Silverlight调用本地音频设备。这是目前自己觉得最好的解决方案。
下面我将介绍如何通过Silverlight4调用本地音频设备并将音频文件上传到服务器。
•正文
项目整体目录结构
其中,AudioUpload.asmx在SL端进行引用,用于上传音频文件;VoiceControlTestPage.aspx引用了Silverlight xap包。
界面效果图:
点击开始录音,程序会调用音频设备,开始录音,计时开始;点击保存录音,将内存流转换为WAV文件,并调用webService上传。
下面将不一一介绍了,如实贴出源码:
MainPage.xaml
View Code <UserControl x:Class="VoiceControl.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignHeight="55" d:DesignWidth="175"> <Grid x:Name="LayoutRoot" Background="White"> <Grid.RowDefinitions> <RowDefinition Height="35"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <Button Name="btnRecord" Content="开始录音" Grid.Row="0" Grid.Column="0" Margin="5,5,5,5" Click="btnRecord_Click" /> <Button Name="btnSaveWav" Content="保存录音" Grid.Row="0" Grid.Column="1" Margin="5,5,5,5" Click="btnSaveWav_Click" /> <TextBlock Name="txtAudioTime" Grid.Row="2" Grid.ColumnSpan="2" TextAlignment="Center">00:00</TextBlock> </Grid> </UserControl>
MainPage.xaml.cs
View Code using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; using System.IO; using VoiceControl.AudioUploadServiceReference; using System.IO.IsolatedStorage; using System.Windows.Browser; using System.Windows.Threading; namespace VoiceControl { public partial class MainPage : UserControl { //声明私有变量 private WavAudioSink _wavSink; private CaptureSource _captureSource; private SaveFileDialog _saveFileDialog = new SaveFileDialog() { Filter = "Audio files (*.wav)|*.wav" }; private DispatcherTimer timer = new DispatcherTimer(); #region 自定义属性 /// <summary> /// 生成的WAV文件名 /// </summary> public string FileName { get; set; } /// <summary> /// 分钟 /// </summary> public int TimerMinute { get; set; } /// <summary> /// 秒 /// </summary> public int TimerSecond { get; set; } #endregion public MainPage() { InitializeComponent(); btnRecord.IsEnabled = true; btnSaveWav.IsEnabled = false; //计时器设置 timer.Interval = new TimeSpan(0, 0, 1); timer.Tick += new EventHandler(timer_Tick); } private void btnRecord_Click(object sender, RoutedEventArgs e) { //初始化_captureSource var audioDevice = CaptureDeviceConfiguration.GetDefaultAudioCaptureDevice(); _captureSource = new CaptureSource() { AudioCaptureDevice = audioDevice }; //有默认设置的设备且可以用来录制音频 if (CaptureDeviceConfiguration.AllowedDeviceAccess || CaptureDeviceConfiguration.RequestDeviceAccess()) { //判断当前没有开始录制音频 if (_captureSource.State == CaptureState.Stopped) { //初始化WavAudioSink _wavSink = new WavAudioSink(); _wavSink.CaptureSource = _captureSource; //开始录制音频 _captureSource.Start(); } btnRecord.IsEnabled = false; btnSaveWav.IsEnabled = true; //开始计时 timer.Start(); } else { MessageBox.Show("客户端未捕捉到音频设备,无法使用录音功能", "提示", MessageBoxButton.OK); btnRecord.IsEnabled = false; btnSaveWav.IsEnabled = false; } } //Timer计时事件 void timer_Tick(object sender, EventArgs e) { if (TimerSecond == 59) { TimerMinute += 1; TimerSecond = 0; } else { TimerSecond += 1; } txtAudioTime.Text = (TimerMinute > 9 ? TimerMinute.ToString() : "0" + TimerMinute.ToString()) + ":" + (TimerSecond > 9 ? TimerSecond.ToString() : "0" + TimerSecond.ToString()); } private void btnSaveWav_Click(object sender, RoutedEventArgs e) { //结束计时 timer.Stop(); this.TimerMinute = 0; this.TimerSecond = 0; this.txtAudioTime.Text = "00:00"; //如果当前状态为开始录制,则停止录制 if (_captureSource.State == CaptureState.Started) { _captureSource.Stop(); } #region 保存Wav文件 version 2.0 using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication() ) { //默认存储空间大小 long defaultQutaSize = store.Quota; //独立存储空间大小(1G) long newQutaSize = 1024 * 1024 * 1024; //客户端第一次运行本程序会提示将存储空间提升为1G容量 if (defaultQutaSize < newQutaSize) { store.IncreaseQuotaTo(newQutaSize); } using (IsolatedStorageFileStream stream = store.CreateFile(Guid.NewGuid().ToString() + ".wav")) { SaveWAVHelper.SavePcmToWav(_wavSink.BackingStream, stream, _wavSink.CurrentFormat); btnRecord.IsEnabled = true; btnSaveWav.IsEnabled = false; //将保存到本地的Wav文件保存到服务器端 FileName = Guid.NewGuid().ToString() + ".wav"; stream.Position = 0; byte[] buffer = new byte[stream.Length + 1]; stream.Read(buffer, 0, buffer.Length); AudioUploadSoapClient webClient = new AudioUploadSoapClient(); webClient.UploadMyAudioCompleted += new EventHandler<UploadMyAudioCompletedEventArgs>(webClient_UploadMyAudioCompleted); webClient.UploadMyAudioAsync(buffer, FileName); stream.Close(); } //清除独立存储空间,防止存储空间不足 var listFile = store.GetFileNames(); if (listFile != null && listFile.Count<string>() > 0) { foreach (string item in listFile) { store.DeleteFile(item); } } } #endregion #region 保存Wav文件 version 1.0 /* Stream stream = _saveFileDialog.OpenFile(); SaveWAVHelper.SavePcmToWav(_wavSink.BackingStream, stream, _wavSink.CurrentFormat); btnRecord.IsEnabled = true; btnSaveWav.IsEnabled = false; //将保存到本地的Wav文件保存到服务器端 string filename = Guid.NewGuid().ToString() + ".wav"; stream.Position = 0; byte[] buffer = new byte[stream.Length + 1]; stream.Read(buffer, 0, buffer.Length); AudioUploadSoapClient webClient = new AudioUploadSoapClient(); webClient.UploadMyAudioCompleted += new EventHandler<UploadMyAudioCompletedEventArgs>(webClient_UploadMyAudioCompleted); webClient.UploadMyAudioAsync(buffer, filename); stream.Close(); */ #endregion } void webClient_UploadMyAudioCompleted(object sender, UploadMyAudioCompletedEventArgs e) { if (e.Error == null) { var app = App.Current as App; HtmlPage.Window.CreateInstance("ajaxTest", HtmlPage.Document.QueryString["userid"], HtmlPage.Document.QueryString["age"], FileName); } } private string GetParameter(string key) { if (App.Current.Resources[key] != null) { return App.Current.Resources[key].ToString(); } else { return string.Empty; } } } }
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; using System.IO; using VoiceControl.AudioUploadServiceReference; using System.IO.IsolatedStorage; using System.Windows.Browser; using System.Windows.Threading; namespace VoiceControl { public partial class MainPage : UserControl { //声明私有变量 private WavAudioSink _wavSink; private CaptureSource _captureSource; private SaveFileDialog _saveFileDialog = new SaveFileDialog() { Filter = "Audio files (*.wav)|*.wav" }; private DispatcherTimer timer = new DispatcherTimer(); #region 自定义属性 /// <summary> /// 生成的WAV文件名 /// </summary> public string FileName { get; set; } /// <summary> /// 分钟 /// </summary> public int TimerMinute { get; set; } /// <summary> /// 秒 /// </summary> public int TimerSecond { get; set; } #endregion public MainPage() { InitializeComponent(); btnRecord.IsEnabled = true; btnSaveWav.IsEnabled = false; //计时器设置 timer.Interval = new TimeSpan(0, 0, 1); timer.Tick += new EventHandler(timer_Tick); } private void btnRecord_Click(object sender, RoutedEventArgs e) { //初始化_captureSource var audioDevice = CaptureDeviceConfiguration.GetDefaultAudioCaptureDevice(); _captureSource = new CaptureSource() { AudioCaptureDevice = audioDevice }; //有默认设置的设备且可以用来录制音频 if (CaptureDeviceConfiguration.AllowedDeviceAccess || CaptureDeviceConfiguration.RequestDeviceAccess()) { //判断当前没有开始录制音频 if (_captureSource.State == CaptureState.Stopped) { //初始化WavAudioSink _wavSink = new WavAudioSink(); _wavSink.CaptureSource = _captureSource; //开始录制音频 _captureSource.Start(); } btnRecord.IsEnabled = false; btnSaveWav.IsEnabled = true; //开始计时 timer.Start(); } else { MessageBox.Show("客户端未捕捉到音频设备,无法使用录音功能", "提示", MessageBoxButton.OK); btnRecord.IsEnabled = false; btnSaveWav.IsEnabled = false; } } //Timer计时事件 void timer_Tick(object sender, EventArgs e) { if (TimerSecond == 59) { TimerMinute += 1; TimerSecond = 0; } else { TimerSecond += 1; } txtAudioTime.Text = (TimerMinute > 9 ? TimerMinute.ToString() : "0" + TimerMinute.ToString()) + ":" + (TimerSecond > 9 ? TimerSecond.ToString() : "0" + TimerSecond.ToString()); } private void btnSaveWav_Click(object sender, RoutedEventArgs e) { //结束计时 timer.Stop(); this.TimerMinute = 0; this.TimerSecond = 0; this.txtAudioTime.Text = "00:00"; //如果当前状态为开始录制,则停止录制 if (_captureSource.State == CaptureState.Started) { _captureSource.Stop(); } #region 保存Wav文件 version 2.0 using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication() ) { //默认存储空间大小 long defaultQutaSize = store.Quota; //独立存储空间大小(1G) long newQutaSize = 1024 * 1024 * 1024; //客户端第一次运行本程序会提示将存储空间提升为1G容量 if (defaultQutaSize < newQutaSize) { store.IncreaseQuotaTo(newQutaSize); } using (IsolatedStorageFileStream stream = store.CreateFile(Guid.NewGuid().ToString() + ".wav")) { SaveWAVHelper.SavePcmToWav(_wavSink.BackingStream, stream, _wavSink.CurrentFormat); btnRecord.IsEnabled = true; btnSaveWav.IsEnabled = false; //将保存到本地的Wav文件保存到服务器端 FileName = Guid.NewGuid().ToString() + ".wav"; stream.Position = 0; byte[] buffer = new byte[stream.Length + 1]; stream.Read(buffer, 0, buffer.Length); AudioUploadSoapClient webClient = new AudioUploadSoapClient(); webClient.UploadMyAudioCompleted += new EventHandler<UploadMyAudioCompletedEventArgs>(webClient_UploadMyAudioCompleted); webClient.UploadMyAudioAsync(buffer, FileName); stream.Close(); } //清除独立存储空间,防止存储空间不足 var listFile = store.GetFileNames(); if (listFile != null && listFile.Count<string>() > 0) { foreach (string item in listFile) { store.DeleteFile(item); } } } #endregion #region 保存Wav文件 version 1.0 /* Stream stream = _saveFileDialog.OpenFile(); SaveWAVHelper.SavePcmToWav(_wavSink.BackingStream, stream, _wavSink.CurrentFormat); btnRecord.IsEnabled = true; btnSaveWav.IsEnabled = false; //将保存到本地的Wav文件保存到服务器端 string filename = Guid.NewGuid().ToString() + ".wav"; stream.Position = 0; byte[] buffer = new byte[stream.Length + 1]; stream.Read(buffer, 0, buffer.Length); AudioUploadSoapClient webClient = new AudioUploadSoapClient(); webClient.UploadMyAudioCompleted += new EventHandler<UploadMyAudioCompletedEventArgs>(webClient_UploadMyAudioCompleted); webClient.UploadMyAudioAsync(buffer, filename); stream.Close(); */ #endregion } void webClient_UploadMyAudioCompleted(object sender, UploadMyAudioCompletedEventArgs e) { if (e.Error == null) { var app = App.Current as App; HtmlPage.Window.CreateInstance("ajaxTest", HtmlPage.Document.QueryString["userid"], HtmlPage.Document.QueryString["age"], FileName); } } private string GetParameter(string key) { if (App.Current.Resources[key] != null) { return App.Current.Resources[key].ToString(); } else { return string.Empty; } } } }
SaveWAVHelper.cs (WAV文件转换辅助类)
View Code /********************************* * Author:袁形 * Date:2012-12-18 * Version:1.0 * Description:将内存流转换为wav文件 *********************************/ using System; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Ink; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; using System.IO; namespace VoiceControl { public class SaveWAVHelper { public static void SavePcmToWav(Stream rawData, Stream output, AudioFormat audioFormat) { if (audioFormat.WaveFormat != WaveFormatType.Pcm) throw new ArgumentException("不支持使用非脉冲编码调制(Pcm)编码的音频格式"); BinaryWriter bwOutput = new BinaryWriter(output); // -- RIFF 块 bwOutput.Write("RIFF".ToCharArray()); // 包的总长度 // 计算的数据长度加上数据头的长度没有数据 // 写数据(44 - 4 ("RIFF") - 4 (当前数据)) bwOutput.Write((uint)(rawData.Length + 36)); bwOutput.Write("WAVE".ToCharArray()); // -- FORMAT 块 bwOutput.Write("fmt ".ToCharArray()); // FORMAT 块的长度 (Binary, 总是 0x10) bwOutput.Write((uint)0x10); // 总是 0x01 bwOutput.Write((ushort)0x01); // 通道数( 0x01=单声道, 0x02=立体声) bwOutput.Write((ushort)audioFormat.Channels); // 采样率 (Binary, Hz为单位) bwOutput.Write((uint)audioFormat.SamplesPerSecond); // 字节每秒 bwOutput.Write((uint)(audioFormat.BitsPerSample * audioFormat.SamplesPerSecond * audioFormat.Channels / 8)); // 每个样品字节: 1=8 bit 单声道, 2=8 bit 立体声 or 16 bit 单声道, 4=16 bit 立体声 bwOutput.Write((ushort)(audioFormat.BitsPerSample * audioFormat.Channels / 8)); // 每个样品字节 bwOutput.Write((ushort)audioFormat.BitsPerSample); // -- DATA 块 bwOutput.Write("data".ToCharArray()); // DATA数据块的长度 bwOutput.Write((uint)rawData.Length); // 原始PCM数据如下 // 复位rawData地位,记住它的原点位置 // 恢复底。 long originalRawDataStreamPosition = rawData.Position; rawData.Seek(0, SeekOrigin.Begin); //追加到输出流中的所有数据从rawData流 byte[] buffer = new byte[4096]; int read; // 循环读取字节数据 while ((read = rawData.Read(buffer, 0, 4096)) > 0) { bwOutput.Write(buffer, 0, read); } //开始写入数据 rawData.Seek(originalRawDataStreamPosition, SeekOrigin.Begin); } } }
/********************************* * Author:袁形 * Date:2012-12-18 * Version:1.0 * Description:将内存流转换为wav文件 *********************************/ using System; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Ink; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; using System.IO; namespace VoiceControl { public class SaveWAVHelper { public static void SavePcmToWav(Stream rawData, Stream output, AudioFormat audioFormat) { if (audioFormat.WaveFormat != WaveFormatType.Pcm) throw new ArgumentException("不支持使用非脉冲编码调制(Pcm)编码的音频格式"); BinaryWriter bwOutput = new BinaryWriter(output); // -- RIFF 块 bwOutput.Write("RIFF".ToCharArray()); // 包的总长度 // 计算的数据长度加上数据头的长度没有数据 // 写数据(44 - 4 ("RIFF") - 4 (当前数据)) bwOutput.Write((uint)(rawData.Length + 36)); bwOutput.Write("WAVE".ToCharArray()); // -- FORMAT 块 bwOutput.Write("fmt ".ToCharArray()); // FORMAT 块的长度 (Binary, 总是 0x10) bwOutput.Write((uint)0x10); // 总是 0x01 bwOutput.Write((ushort)0x01); // 通道数( 0x01=单声道, 0x02=立体声) bwOutput.Write((ushort)audioFormat.Channels); // 采样率 (Binary, Hz为单位) bwOutput.Write((uint)audioFormat.SamplesPerSecond); // 字节每秒 bwOutput.Write((uint)(audioFormat.BitsPerSample * audioFormat.SamplesPerSecond * audioFormat.Channels / 8)); // 每个样品字节: 1=8 bit 单声道, 2=8 bit 立体声 or 16 bit 单声道, 4=16 bit 立体声 bwOutput.Write((ushort)(audioFormat.BitsPerSample * audioFormat.Channels / 8)); // 每个样品字节 bwOutput.Write((ushort)audioFormat.BitsPerSample); // -- DATA 块 bwOutput.Write("data".ToCharArray()); // DATA数据块的长度 bwOutput.Write((uint)rawData.Length); // 原始PCM数据如下 // 复位rawData地位,记住它的原点位置 // 恢复底。 long originalRawDataStreamPosition = rawData.Position; rawData.Seek(0, SeekOrigin.Begin); //追加到输出流中的所有数据从rawData流 byte[] buffer = new byte[4096]; int read; // 循环读取字节数据 while ((read = rawData.Read(buffer, 0, 4096)) > 0) { bwOutput.Write(buffer, 0, read); } //开始写入数据 rawData.Seek(originalRawDataStreamPosition, SeekOrigin.Begin); } } }
WavAudioSink.cs (音频信息类)
View Code using System; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Ink; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; using System.IO; namespace VoiceControl { public class WavAudioSink:AudioSink { // 设置需要记录的内存流 private MemoryStream _stream; // 设置当前的音频格式 private AudioFormat _format; public Stream BackingStream { get { return _stream; } } public AudioFormat CurrentFormat { get { return _format; } } protected override void OnCaptureStarted() { _stream = new MemoryStream(1024); } protected override void OnCaptureStopped() { } protected override void OnFormatChange(AudioFormat audioFormat) { if (audioFormat.WaveFormat != WaveFormatType.Pcm) throw new InvalidOperationException("WavAudioSink只支持PCM音频格式"); _format = audioFormat; } protected override void OnSamples(long sampleTime, long sampleDuration, byte[] sampleData) { // 新的音频数据到达,将它们写入流 _stream.Write(sampleData, 0, sampleData.Length); } } }
using System; using System.Net; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Ink; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Shapes; using System.IO; namespace VoiceControl { public class WavAudioSink:AudioSink { // 设置需要记录的内存流 private MemoryStream _stream; // 设置当前的音频格式 private AudioFormat _format; public Stream BackingStream { get { return _stream; } } public AudioFormat CurrentFormat { get { return _format; } } protected override void OnCaptureStarted() { _stream = new MemoryStream(1024); } protected override void OnCaptureStopped() { } protected override void OnFormatChange(AudioFormat audioFormat) { if (audioFormat.WaveFormat != WaveFormatType.Pcm) throw new InvalidOperationException("WavAudioSink只支持PCM音频格式"); _format = audioFormat; } protected override void OnSamples(long sampleTime, long sampleDuration, byte[] sampleData) { // 新的音频数据到达,将它们写入流 _stream.Write(sampleData, 0, sampleData.Length); } } }
AudioUpload.asmx
View Code [WebMethod] public int UploadMyAudio(byte [] fileByte,string fileName) { string audioFilePath = HttpContext.Current.Server.MapPath("~/Files/AudioRecorder"); string filePath = audioFilePath + "/" + fileName; FileStream stream = new FileStream(filePath, FileMode.CreateNew); stream.Write(fileByte, 0, fileByte.Length); stream.Close(); return fileByte.Length; }
[WebMethod] public int UploadMyAudio(byte [] fileByte,string fileName) { string audioFilePath = HttpContext.Current.Server.MapPath("~/Files/AudioRecorder"); string filePath = audioFilePath + "/" + fileName; FileStream stream = new FileStream(filePath, FileMode.CreateNew); stream.Write(fileByte, 0, fileByte.Length); stream.Close(); return fileByte.Length; }
web.Config配置文件:
View Code <?xml version="1.0"?> <configuration> <system.web> <compilation debug="true" targetFramework="4.0" /> <httpRuntime executionTimeout="648000" maxRequestLength="100000000" ></httpRuntime> </system.web> <system.webServer> <modules runAllManagedModulesForAllRequests="true"/> </system.webServer> </configuration>
<?xml version="1.0"?> <configuration> <system.web> <compilation debug="true" targetFramework="4.0" /> <httpRuntime executionTimeout="648000" maxRequestLength="100000000" ></httpRuntime> </system.web> <system.webServer> <modules runAllManagedModulesForAllRequests="true"/> </system.webServer> </configuration>
程序缺点:
首先是必须通过Isolated Storage对象在客户端拓展独立存储空间(当然大小可以根据实际需要自定义),因为我还没有找到一个更好的办法将WAV文件存储到本地,或者说满足SavePcmToWav方法的第二个参数;其次,最终得到的WAV文件大小实在不敢恭维,本人做了一个测试,30秒的录音最终可以得到一个35M左右的WAV文件,提醒,是WAV文件,然后还要上传到服务器,这些都是相当恐怖的。同样的,Win7再带的录音功能35秒的录音得到的WMA文件,提醒,是WMA文件,却不到1M大小。最后,在SL程序中,不能应用非SL运行库生成的其他DLL,导致无法借助外部程序压缩WAV文件或者优化本程序。
程序待优化部分:
1.有木有其他解决方案,即不用Silverlight同样可以完成类似功能;
2.能不能不需要先将WAV文件保存到独立存储空间(既保存到本地),然后将该文件Stream通过WebService传到服务器,或者直接将内存流在Web直接转换成音频文件;
3.在转换内存流的步骤中,可不可以将MemoryStream保存为如MP3,WMA或者其他格式的音频文件,来压缩音频文件大小。
4.求一个更好的文件上传方法。
转自:http://www.cnblogs.com/doyuanbest/archive/2012/12/26/2834325.html