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

实现目标:

  1. 修改绘图时机,提高效率
    方案: 将控件中的绘图事件变成其data改变时才触发。
  2. 绘图的同时播放声音
    方案:模拟的不同频率声音
    编写自定义的音频数据生成类,播音时同时把数据写入buffer(short数组),并记录写的位置。在定时器中将buffer赋值给控件中的data(根据写位置重新排序)。
    Ps: 排序的目的是使控件刷新波形时能始终保证最新写入的数据在窗口的最右端显示,模拟实时播放的效果。

关键词: Naudio

  1. 本例拟将发音、绘图,以及一些开关控制等功能全部都集成到控件中。
    方案的合理性分析:
  • 如果播放在控件类之外,那么点击控件内的按钮如何能关闭播放?也许可以给控件的按钮注册一个控件外的事件,但还是麻烦。
  • 如果播放在控件类之内,那么播放的诸多控制都是容易实现的。结合面向对象的思维方式,一个控件就是一个可以自主发声的实例。

一. WaveProvider

在lib solution中添加波形生成的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class MyWave:WaveProvider16
{
public readonly object lockPos = new object();
public MyWave(): base(){}
public MyWave(int sampleRate): base(sampleRate, 1){}
public MyWave(int sampleRate, int AudioRate): base(sampleRate, 1){audioRate = AudioRate;}

public int audioRate = 1000;//声音的频率
double Amplitude = 0.5;//波形幅度
public short[] waveData = new short[2000];//2000个采样点
public int playPos = 0;//即将产生的最新采样点的索引
double t = 0;//时间
public override int Read(short[] buffer, int offset, int sampleCount)
{

short dataTemp = 0;
lock (lockPos)
{
for (int index = 0; index < sampleCount; index++)
{
t += 1/(double)base.WaveFormat.SampleRate;
dataTemp = (short)(short.MaxValue * Amplitude * Math.Sin(2 * Math.PI * audioRate * t));
buffer[offset + index] = dataTemp;
waveData[playPos] = dataTemp;
playPos++;
if (playPos == 2000)
playPos = 0;
}
}
return sampleCount;
}
}

需要在工程中添加Naudio.dll的引用。目前只能编译x86程序。

二. 控件外观设计

加入播放按钮和停止按钮,并允许用户输入声音的频率。外观如下:

2.1 Xaml代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<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">
<Grid Background="#FFECF3F5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="49"/>
<ColumnDefinition Width="60"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="1" Margin="4">
<Grid.RowDefinitions>
<RowDefinition Height="3*"/>
<RowDefinition Height="3*"/>
</Grid.RowDefinitions>
<Button x:Name="btn" Click="btn_Click" />
<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"/>
</StackPanel>
<Canvas x:Name="canvas" Grid.Column="2" HorizontalAlignment="Stretch" Height="60" Margin="10,10,10,0" VerticalAlignment="Top" Background="#FFEAE4E4" />
</Grid>
</UserControl>

2.2 c#代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
public partial class User1 : UserControl
{
WaveOut waveOut = null;
public MyWave wave = new MyWave(16000);
public User1() {InitializeComponent();}
private void RefreshWave()
{

double cvsHeightHalf = canvas.RenderSize.Height / 2;
int cvsWidth = (int)canvas.RenderSize.Width;
PathGeometry Geometry = new PathGeometry();
PathFigure PathFigure = new PathFigure();
PolyLineSegment PolyLine = new PolyLineSegment();
PathFigure.StartPoint = new Point(0, cvsHeightHalf);
int[] maxIndexPlot = new int[cvsWidth + 1];
int SampleCount = data.Length;
for (int i = 0; i <= cvsWidth; i++)
{//将像素点的位置与数据在数组中的索引位置相对应
maxIndexPlot[i] = (int)((double)i / cvsWidth * SampleCount);
}
float maxVaulePlot = float.MinValue;
float minVaulePlot = float.MaxValue;
for (int i = 0; i < cvsWidth; i++)
{
maxVaulePlot = float.MinValue;
minVaulePlot = float.MaxValue;
for (int j = maxIndexPlot[i]; j < maxIndexPlot[i + 1]; j++)
{
if (maxVaulePlot < data[j])
maxVaulePlot = (float)data[j];
if (minVaulePlot > data[j])
minVaulePlot = (float)data[j];
}
maxVaulePlot /= 32767f;
if (maxVaulePlot > 1) maxVaulePlot = 1;
minVaulePlot /= 32767f;
if (minVaulePlot < -1) minVaulePlot = -1;
PolyLine.Points.Add(new Point(i, -maxVaulePlot * cvsHeightHalf * 1 + cvsHeightHalf));
PolyLine.Points.Add(new Point(i, -minVaulePlot * cvsHeightHalf * 1 + cvsHeightHalf));
}
PathFigure.Segments.Add(PolyLine);
Geometry.Figures.Add(PathFigure);
WavePath.Data = Geometry;
WavePath.Stroke = new SolidColorBrush(Colors.Blue);
WavePath.StrokeThickness = 1;
if (!canvas.Children.Contains(WavePath))
canvas.Children.Add(WavePath);
}
private int _Freq = 1000;
public int Freq
{
get { return _Freq; }
set
{
if(this._Freq!=value)
{
this._Freq = value;
this.txtFreq.Text = this._Freq.ToString();
}
}
}
private short[] _data = new short[1000];
public short[] data
{
get { return _data; }
set
{
_data = value;
RefreshWave();
}
}
private readonly DispatcherTimer PlotTimer = new DispatcherTimer(DispatcherPriority.ApplicationIdle);
public System.Windows.Shapes.Path WavePath = new System.Windows.Shapes.Path();
int _Num = 0;
public int Num
{
set { if (_Num != value) _Num = value; }
get { return _Num; }
}
private void btn_Click(object sender, RoutedEventArgs e)
{

btn.Content = _Num;
label.Content = _Num;
}
private void btnPlay_Click(object sender, RoutedEventArgs e)
{

if (waveOut != null)
{
waveOut.Stop();
waveOut.Dispose();
}
wave.audioRate = int.Parse(txtFreq.Text);
waveOut = new WaveOut();
waveOut.Init(wave);
waveOut.Play();
}
private void btnStop_Click(object sender, RoutedEventArgs e)
{

if (waveOut != null)
{
waveOut.Stop();
waveOut.Dispose();
}
}
}

主要的改动

  1. 图像刷新的时机改到了用户对data赋以不同值的时候,利用的c#中属性的set方法。
  2. 控件包含了Wavout和Mywave,通过按钮click事件可以开启或关闭播放;
  3. 控件的Mywave公开了audioRate,可以通过其在后台修改声音频率。当然也可以在TextBox中输入频率值。

三. 调用控件

3.1 Xaml代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:libUserC="clr-namespace:libUserC;assembly=libUserC" x:Class="wpfTest.MainWindow"
Title="MainWindow" Height="546.522" Width="841.418" SizeChanged="Window_SizeChanged" ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.VerticalScrollBarVisibility="Auto">
<Window.Resources>
<ItemsPanelTemplate x:Key="ItemsPanelTemplate1">
<WrapPanel IsItemsHost="True" MaxWidth="{Binding ElementName=MyMainUI,Path=ActualWidth}" ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.VerticalScrollBarVisibility="Auto"/>
</ItemsPanelTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="227*"/>
</Grid.RowDefinitions>
<ListBox Name="listShower" Grid.Row="1" ItemsPanel="{DynamicResource ItemsPanelTemplate1}" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Visible">
<libUserC:User1 Name="U0" Num="0" Margin="4"/>
<libUserC:User1 Name="U1" Num="1" Margin="4"/>
<libUserC:User1 Name="U2" Num="2" Margin="4"/>
<libUserC:User1 Name="U3" Num="3" Margin="4"/>
<libUserC:User1 Name="U4" Num="4" Margin="4"/>
<libUserC:User1 Name="U5" Num="5" Margin="4" BorderBrush="#FF5F5A5A" BorderThickness="1"/>
<libUserC:User1 Name="U6" Num="6" Margin="4" BorderBrush="#FF5F5A5A" BorderThickness="1"/>
<libUserC:User1 Name="U7" Num="7" Margin="4" BorderBrush="#FF5F5A5A" BorderThickness="1"/>
<libUserC:User1 Name="U8" Num="8" Margin="4" BorderBrush="#FF5F5A5A" BorderThickness="1"/>
<libUserC:User1 Name="U9" Num="9" Margin="4" BorderBrush="#FF5F5A5A" BorderThickness="1"/>
<libUserC:User1 Name="U10" Num="10" Margin="4" BorderBrush="#FF5F5A5A" BorderThickness="1"/>
<libUserC:User1 Name="U11" Num="11" Margin="4" BorderBrush="#FF5F5A5A" BorderThickness="1"/>
<libUserC:User1 Name="U12" Num="12" Margin="4" BorderBrush="#FF5F5A5A" BorderThickness="1"/>
<libUserC:User1 Name="U13" Num="13" Margin="4" BorderBrush="#FF5F5A5A" BorderThickness="1"/>
<libUserC:User1 Name="U14" Num="14" Margin="4" BorderBrush="#FF5F5A5A" BorderThickness="1"/>
<libUserC:User1 Name="U15" Num="15" Margin="4" BorderBrush="#FF5F5A5A" BorderThickness="1"/>
</ListBox>
</Grid>
</Window>

3.2 后台代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Threading;
using NAudio.Wave;
namespace wpfTest
{
public partial class MainWindow : Window
{
public MainWindow()
{

InitializeComponent();
for (int i = 0; i < listShower.Items.Count;i++ )
{
libUserC.User1 temp =(libUserC.User1) listShower.Items[i];
temp.Freq= 100 * i + 101;
}
PlotTimer.IsEnabled = true;
PlotTimer.Interval = TimeSpan.FromMilliseconds(150);
PlotTimer.Tick += PlotTimer_Tick;
}
private void PlotTimer_Tick(object sender, EventArgs e)
{

int i=0;
foreach (libUserC.User1 temp in listShower.Items)
{
libUserC.MyWave wavePro = temp.wave;
short[] sortData=new short[2000];
Console.WriteLine(wavePro.audioRate.ToString()+"Hz " + wavePro.playPos.ToString());
lock (wavePro.lockPos)
{
Array.Copy(wavePro.waveData, wavePro.playPos, sortData, 0, 2000 - wavePro.playPos);
if (wavePro.playPos > 0)
Array.Copy(wavePro.waveData, 0, sortData, 2000 - wavePro.playPos, wavePro.playPos);
}
temp.data = sortData;
i++;
}
}
private readonly DispatcherTimer PlotTimer = new DispatcherTimer(DispatcherPriority.ApplicationIdle);
private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
{

listShower.Width = this.RenderSize.Width;
foreach(libUserC.User1 item in listShower.Items)
{
item.Width = this.RenderSize.Width*0.48;
double newHeight = this.RenderSize.Height * 0.10;
if (this.RenderSize.Height * 0.10 < 80)
item.Height = 80;
else
item.Height = newHeight;
}
}
}
}

3.3 运行效果


点击播放按钮可以打开任意一个单频点播放器,多个频点的声音可以同时播放。
波形从右到左地移动,体现了实时播放的效果,当前播放的声音波形显示在最右边。
图中,频点数多设置为质数,因为如果有公约数的话,刷新时可能会看不出波形在动。

四. 总结

  1. 自定义的控件目前已经变身为一个单频点播放器
  2. 合理的使用自定义控件可以在最终的工程中极大地降低代码量,使逻辑更加清晰。
  3. 结合音乐中1(duo)、2(ruai)、3…7的频率,可以进一步开发一个乐器。
  4. 后续将单频点播放器进一步完善为一个可播放常见音乐格式的播放控件。

热评文章