目标:
添加播放本地音频文件的功能。能够显示波形,不需要模拟实时效果,暂支持wav格式。
一. 控件外观修改
添加一个RadioButton,被选中时为默认的单频声音,否则为文件播放。相当于控件具有两
种工作模式。处于文件播放模式时,频率的输入被禁用,这是用WPF的绑定实现的,将
TextBox的IsEnable属性绑定到RadioButton的IsCheck属性。
1.1 Xaml代码
<UserControl x:Class="libUserC.User1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" Height="80" d:Width="485" SizeChanged="UserControl_SizeChanged">
<Grid Background="#FFECF3F5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="44"/>
<ColumnDefinition Width="73"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="1" Margin="4">
<Grid.RowDefinitions>
<RowDefinition Height="3*"/>
<RowDefinition Height="3*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button x:Name="btn_Open" HorizontalAlignment="Center" VerticalAlignment="Center" Click="btn_Open_Click" >
<Image Source="Images/open.ico"/>
</Button>
<RadioButton Name="rbtn_ToneOnOff" Margin="2,0,2,0" IsChecked="True" VerticalAlignment="Center" Checked="rbtn_ToneOnOff_Checked"></RadioButton>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Grid.Row="1">
<Button x:Name="btnPlay" Height="25" Click="btnPlay_Click" >
<Image Source="Images/play.ico"/>
</Button>
<Button x:Name="btnStop" Height="25" Click="btnStop_Click" >
<Image Source="Images/stop.ico"/>
</Button>
</StackPanel>
</Grid>
<StackPanel Margin="4" Orientation="Vertical">
<Label x:Name="label" Background="#FFEAC0C0" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" Padding="0" Height="20"/>
<TextBox Name="txtFreq" Text="1000" IsEnabled="{Binding ElementName=rbtn_ToneOnOff, Path=IsChecked}"/>
</StackPanel>
<Canvas x:Name="canvas" Grid.Column="2" HorizontalAlignment="Stretch" Height="60" Margin="10,10,10,0" VerticalAlignment="Top" Background="#FFEAE4E4" />
</Grid>
</UserControl>
最大的按钮功能改变为文件选择。
1.2 C#代码
(与之前相重复的部分代码已略去)
public partial class User1 : UserControl
{
public WaveOut waveOut = null;
public MyWave wave = new MyWave(16000);
WaveStream wavFileStream = null;
public int ChoiceId = 0;
public User1()
{
InitializeComponent();
}
public void RefreshWave() {//略}
string _NameStr = "0";
public string NameStr
{
set { if (_NameStr != value) _NameStr = value; this.label.Content = value; }
get { return _NameStr; }
}
private void btnPlay_Click(object sender, RoutedEventArgs e)
{
if (waveOut != null)
{
waveOut.Stop();
waveOut.Dispose();
}
waveOut = new WaveOut();
if (rbtn_ToneOnOff.IsChecked==true)
{
wave.audioRate = int.Parse(txtFreq.Text);
waveOut.Init(wave);
}
else
{
if (wavFileStream != null)
waveOut.Init(wavFileStream);
else
return;
}
waveOut.Play();
}
private void btnStop_Click(object sender, RoutedEventArgs e)
{
if (waveOut != null)
{
waveOut.Stop();
waveOut.Dispose();
waveOut = null;
}
wavFileStream = null;
this.data = new short[2000];
}
private void btn_Open_Click(object sender, RoutedEventArgs e)
{
Microsoft.Win32.OpenFileDialog openDialog = new Microsoft.Win32.OpenFileDialog();
openDialog.Filter = "Wave File (*.wav)|*.wav;";
if (openDialog.ShowDialog() != true)
{
rbtn_ToneOnOff.IsChecked = true;
this.ChoiceId = 0;
return;
}
rbtn_ToneOnOff.IsChecked = false;
this.ChoiceId = 1;
wavFileStream = new WaveFileReader(openDialog.FileName);
//读出波形数据,绘制波形
FileStream fstream = new FileStream(openDialog.FileName, FileMode.Open, FileAccess.Read);
byte[] FileHead = new byte[44];
int lengthData = fstream.Read(FileHead, 0, 44);//
object TempParaObj = ByteStructTrans.BytesToStruct(FileHead, 0, typeof(HeaderWAV44Byte));
HeaderWAV44Byte WavFileInfo = (HeaderWAV44Byte)TempParaObj;//前44字节转为标准的WAV文件头
int sampleNum = WavFileInfo.ByteData / WavFileInfo.BytePerSample;
this.data = new short[sampleNum];
byte[] temp=new byte[WavFileInfo.BytePerSample];
for(int i=0;i<sampleNum;i++)
{
fstream.Read(temp,0,WavFileInfo.BytePerSample);
this.data[i] = BitConverter.ToInt16(temp, 0);
}
this.RefreshWave();
}
private void rbtn_ToneOnOff_Checked(object sender, RoutedEventArgs e)
{
this.ChoiceId = 0;
}
private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e)
{
if(e.WidthChanged==true)
this.RefreshWave();
}
}
二. WAV文件读取
WAV文件有44字节的文件头,结合这44字节的定义,可以读出文件的基本信息。本文使用了
划分内存的方式直接对44字节进行赋值,这并不是最简单的方案,不过该方法在较大的
工程中会方便一些。
定义一个名为协议的名空间,工程的许多自定义的协议都可以放在这里:
namespace Protocol
{
[StructLayout(LayoutKind.Explicit, Pack = 2)]//Pack表示最小移动为2字节
public struct HeaderWAV44Byte
{//44字节的文件头
[FieldOffset(0)]
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 5)]
public string riff_id;//"RIFF"
[FieldOffset(4)]
public int FileLength;//去除8字节后的文件长度
[FieldOffset(8)]
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 9)]
public string wave_fmt;//"WAVEfmt "
[FieldOffset(16)]
public int PCM;//0x10, PCM方式
[FieldOffset(20)]
public short fmttag;//0x01
[FieldOffset(22)]
public short channel;//通道数
[FieldOffset(24)]
public int FreqSample;//采样率
[FieldOffset(28)]
public int BitPerSec;//每秒播放的字节数
[FieldOffset(32)]
public short BytePerSample;//采样一次占的字节数, blockalign, =声道数*量化位数/8
[FieldOffset(34)]
public short BitPerSample;//采样一次占的Bit数
[FieldOffset(36)]
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 5)]
public string data;//"data"
[FieldOffset(40)]
public int ByteData;//数据区的字节数, 文件长度-44
}
public class ByteStructTrans
{
public static byte[] StructToBytes(object structObj)
{
int size = Marshal.SizeOf(structObj);
IntPtr buffer = Marshal.AllocHGlobal(size);
try
{
Marshal.StructureToPtr(structObj, buffer, false);
byte[] bytes = new byte[size];
Marshal.Copy(buffer, bytes, 0, size);
return bytes;
}
finally
{
Marshal.FreeHGlobal(buffer);
}
}
public static object BytesToStruct(byte[] bytes, int startIndex, Type strcutType)
{
int size = Marshal.SizeOf(strcutType);
IntPtr buffer = Marshal.AllocHGlobal(size);
try
{
Marshal.Copy(bytes, startIndex, buffer, size);
return Marshal.PtrToStructure(buffer, strcutType);
}
finally
{
Marshal.FreeHGlobal(buffer);
}
}
}
}
需要引用System.Runtime.InteropServices
。 WAV文件的44字节头的含义可以通过观察HeaderWAV44Byte得到,不再赘述了。 该名空间中也给出了字节数组和非托管内存的转换方法。
三. 调用控件
在控件后台代码中,我们把原来int 型的Num修改成string 型的NameStr,相应的在调用控
件部分的xaml代码中将Num改为NameStr即可,其他没有任何变化。
运行结果
不同文件可以同时播放,原有的单频播放功能也保留下来了。
四. 总结
- 调用代码几乎不需做任何修改,进一步说明了自定义控件的好处。
- 本文增加了文件播放的模式,下一篇将增加网络数据流的播放模式。