WPF音频播放与波形显示(三)

目标:
添加播放本地音频文件的功能。能够显示波形,不需要模拟实时效果,暂支持wav格式。

关键词::RadioButton;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即可,其他没有任何变化。

运行结果


不同文件可以同时播放,原有的单频播放功能也保留下来了。

四. 总结

  1. 调用代码几乎不需做任何修改,进一步说明了自定义控件的好处。
  2. 本文增加了文件播放的模式,下一篇将增加网络数据流的播放模式。

热评文章