NAduio 自制音乐播放器以及简陋的可视化音频
NAduio 自制音乐播放器
因为网上关于 NAudio 的教程真没多少,源代码的注解也不够,所以就自己研究了
使用 WPF MVVM
先用 NuGet 安装几个库:
- NAudio:音频库
- Prism.Core:MVVM 需要的库
- System.Windows.Interactivity.WPF:用于 MVVM 绑定事件的库
代码
先把界面仍出来,不懂 MVVM 和 Interactivity
可以自行查找资料,重点不在这说明
<Window x:Class="Music_Program.MainWindow"
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"
xmlns:local="clr-namespace:Music_Program"
mc:Ignorable="d"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Title="MainWindow" Height="450" Width="800">
<Grid Background="DarkGray">
<Grid.RowDefinitions>
<RowDefinition Height="30"></RowDefinition>
<RowDefinition Height="200"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Command="{Binding OpenMusicFileCommand}">打开文件</Button>
<Button Grid.Row="0" Grid.Column="1" Command="{Binding PlayMusicCommand}">播放</Button>
<Button Grid.Row="0" Grid.Column="2" Command="{Binding PauseMusicCommand}">暂停</Button>
<Button Grid.Row="0" Grid.Column="3" Command="{Binding StopMusicCommand}">停止</Button>
<Slider Grid.Row="1" Grid.Column="0" Orientation="Vertical" HorizontalAlignment="Center" Minimum="0" Maximum="100" Value="{Binding Volume}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="ValueChanged">
<!--<i:InvokeCommandAction Command="{Binding RelativeSource={RelativeSource AncestorType=Window},Path=DataContext.AdjustVolumeCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorLevel=1,AncestorType={x:Type Slider}}}">
</i:InvokeCommandAction>-->
<i:InvokeCommandAction Command="{Binding AdjustVolumeCommand}" >
</i:InvokeCommandAction>
</i:EventTrigger>
</i:Interaction.Triggers>
</Slider>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Center">音量</Label>
<TextBox Grid.Row="3" Grid.Column="0" TextWrapping="Wrap" Text="{Binding FilePath}"></TextBox>
<Slider Grid.Row="1" Grid.Column="1" Orientation="Horizontal" Grid.ColumnSpan="3" VerticalAlignment="Center" Minimum="0" Maximum="100" Value="{Binding Progress}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="PreviewMouseLeftButtonUp">
<i:InvokeCommandAction Command="{Binding AdjustProgressBarCommand}">
</i:InvokeCommandAction>
</i:EventTrigger>
</i:Interaction.Triggers>
</Slider>
<Label Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="3" HorizontalAlignment="Center">进度</Label>
</Grid>
</Window>
然后是 ViewModel
public class MusicViewModel : BindableBase
{
//输出设备
private WaveOutEvent _outputDevice;
public WaveOutEvent OutputDevice
{
get { return _outputDevice; }
set
{
_outputDevice = value;
this.RaisePropertyChanged(nameof(OutputDevice));
}
}
//要播放的音频文件
private AudioFileReader _audioFile;
public AudioFileReader AudioFile
{
get { return _audioFile; }
set
{
_audioFile = value;
this.RaisePropertyChanged(nameof(AudioFile));
}
}
//音量
private int _volume;
public int Volume
{
get { return _volume; }
set
{
_volume = value;
this.RaisePropertyChanged(nameof(Volume));
}
}
//音频进度
private int _progress;
public int Progress
{
get { return _progress; }
set
{
_progress = value;
this.RaisePropertyChanged(nameof(Progress));
}
}
//文件路径
private string _filePath;
public string FilePath
{
get { return _filePath; }
set
{
_filePath = value;
this.RaisePropertyChanged(nameof(FilePath));
}
}
//打开音乐文件
public DelegateCommand OpenMusicFileCommand { get; set; }
//播放音乐
public DelegateCommand PlayMusicCommand { get; set; }
//暂停音乐
public DelegateCommand PauseMusicCommand { get; set; }
//停止音乐
public DelegateCommand StopMusicCommand { get; set; }
//调节音量
public DelegateCommand AdjustVolumeCommand { get; set; }
//调节进度条
public DelegateCommand AdjustProgressBarCommand { get; set; }
public MusicViewModel()
{
this.Volume = 0;
this.Progress = 0;
this.FilePath = "";
this.OpenMusicFileCommand = new DelegateCommand(this.OpenMusicFileCommandExecute);
this.PlayMusicCommand = new DelegateCommand(this.PlayMusicCommandExecute);
this.PauseMusicCommand = new DelegateCommand(this.PauseMusicCommandExecute);
this.StopMusicCommand = new DelegateCommand(this.StopMusicCommandExecute);
this.AdjustVolumeCommand = new DelegateCommand(this.AdjustVolumeCommandExecute);
this.AdjustProgressBarCommand = new DelegateCommand(this.AdjustProgressBarCommandExecute);
//使用 Timer 去设置进度条
Timer timer = new Timer();
timer.Interval = 1000;
timer.Tick += new EventHandler((sender, args) =>
{
if (null != this.OutputDevice && null != this.AudioFile)
{
float position = this.AudioFile.Position;
this.Progress = (int)((position / this.AudioFile.Length) * 100);
}
});
timer.Start();
}
private void OpenMusicFileCommandExecute()
{
OpenFileDialog openFileDialog = new OpenFileDialog();
openFileDialog.InitialDirectory = @"c:\";
openFileDialog.Filter = "所有文件|*.*";
openFileDialog.RestoreDirectory = false;
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
this.FilePath = Path.GetFullPath(openFileDialog.FileName);
}
}
private void PlayMusicCommandExecute()
{
if (null == this.OutputDevice)
{
this.OutputDevice = new WaveOutEvent();
//播放结束事件,就是些清理工作
this.OutputDevice.PlaybackStopped += ((sender, args) =>
{
this.OutputDevice.Dispose();
this.OutputDevice = null;
this.AudioFile.Dispose();
this.AudioFile = null;
this.Progress = 0;
});
}
if (null == this.AudioFile)
{
this.AudioFile = new AudioFileReader(this.FilePath);
//将音频文件绑定至输出设备
this.OutputDevice.Init(this.AudioFile);
}
//开始播放
this.OutputDevice.Play();
//初始化音量,WaveOutEvent 的 Volume 为 0 到 1
this.Volume = (int)(this.OutputDevice.Volume * 100);
}
private void PauseMusicCommandExecute()
{
this.OutputDevice?.Pause();
}
private void StopMusicCommandExecute()
{
//Stop()函数会触发PlaybackStopped事件
this.OutputDevice?.Stop();
}
private void AdjustVolumeCommandExecute()
{
if (null != this.OutputDevice && null != this.AudioFile)
{
this.OutputDevice.Volume = this.Volume / 100F;
}
}
private void AdjustProgressBarCommandExecute()
{
if (null != this.OutputDevice && null != this.AudioFile)
{
this.AudioFile.Position = (long)((this.Progress / 100F) * this.AudioFile.Length);
}
}
}
最后绑定 ViewModel
public MainWindow()
{
InitializeComponent();
this.DataContext = new MusicViewModel();
}
讲解
因为上面的代码比较简单,我就只讲一下 NAudio 里的东西了
需要播放音乐,最基本的就是以下两个类:
WaveOutEvent
:用于播放、暂停、停止音乐AudioFileReader
:读取音乐文件
WaveOutEvent
常用属性:
public float Volume
:音量,介于 0 到 1 之间public WaveFormat OutputWaveFormat
:包含各种音频数据,本案例用不上publicPlaybackState PlaybackState
:播放状态
常用方法:
public void Init(IWaveProvider waveProvider)
:初始化 WaveOut 设备public void Play()
:开始播放来自 WaveStream 的音频public void Pause()
:暂停音频public void Stop()
:停止并重置 WaveOut 设备
AudioFileReader
常用属性:
public string FileName
:音频文件名称,可以算是路径public override long Length
:音频流的长度public override long Position
:当前播放位于的音频流的位置
效果图
很简陋,但是能用
NAidio 实现可视化音频
这里开始就很难了,网上的相关资料更是少
毕竟这玩意儿涉及到了数学知识,这可就给爷整傻了,所以我也花了挺多时间的
写几个可以参考的文章
NAudio可视化音频参考:https://www.cnblogs.com/slimenull/p/14749373.html
音频以及傅里叶变换的基础知识:https://zhuanlan.zhihu.com/p/19763358
首先,我们来新建一个用户控件,因为 WPF 的 ProgressBar 可以纵向,所以我就直接用了
<UserControl x:Class="Music_Program.Views.SpectrumView"
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"
xmlns:local="clr-namespace:Music_Program.Views"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<ProgressBar Grid.Column="0" Minimum="0.00" Maximum="100.00" Value="{Binding Value[0]}" Orientation="Vertical"></ProgressBar>
<ProgressBar Grid.Column="1" Minimum="0.00" Maximum="100.00" Value="{Binding Value[1]}" Orientation="Vertical"></ProgressBar>
<ProgressBar Grid.Column="2" Minimum="0.00" Maximum="100.00" Value="{Binding Value[2]}" Orientation="Vertical"></ProgressBar>
<ProgressBar Grid.Column="3" Minimum="0.00" Maximum="100.00" Value="{Binding Value[3]}" Orientation="Vertical"></ProgressBar>
<ProgressBar Grid.Column="4" Minimum="0.00" Maximum="100.00" Value="{Binding Value[4]}" Orientation="Vertical"></ProgressBar>
<ProgressBar Grid.Column="5" Minimum="0.00" Maximum="100.00" Value="{Binding Value[5]}" Orientation="Vertical"></ProgressBar>
<ProgressBar Grid.Column="6" Minimum="0.00" Maximum="100.00" Value="{Binding Value[6]}" Orientation="Vertical"></ProgressBar>
<ProgressBar Grid.Column="7" Minimum="0.00" Maximum="100.00" Value="{Binding Value[7]}" Orientation="Vertical"></ProgressBar>
<ProgressBar Grid.Column="8" Minimum="0.00" Maximum="100.00" Value="{Binding Value[8]}" Orientation="Vertical"></ProgressBar>
<ProgressBar Grid.Column="9" Minimum="0.00" Maximum="100.00" Value="{Binding Value[9]}" Orientation="Vertical"></ProgressBar>
</Grid>
</UserControl>
/// <summary>
/// SpectrumView.xaml 的交互逻辑
/// </summary>
public partial class SpectrumView : UserControl
{
public SpectrumViewModel SpectrumViewModel{ get; set; }
public SpectrumView()
{
InitializeComponent();
this.SpectrumViewModel = new SpectrumViewModel();
this.DataContext = SpectrumViewModel;
}
public void Init(ref WaveOutEvent outputDevice, ref AudioFileReader audioFile)
{
this.SpectrumViewModel.InitializeAudioInfo(ref outputDevice, ref audioFile);
}
}
然后稍微修改一下播放器的界面
使用方法是先打开音频文件,点击播放,再点击初始化,用户控件就开始工作了
<Window x:Class="Music_Program.MainWindow"
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"
xmlns:local="clr-namespace:Music_Program"
mc:Ignorable="d"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:views="clr-namespace:Music_Program.Views"
Title="MainWindow" Height="450" Width="800">
<Grid Background="DarkGray">
<Grid.RowDefinitions>
<RowDefinition Height="30"></RowDefinition>
<RowDefinition Height="200"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Command="{Binding OpenMusicFileCommand}">打开文件</Button>
<Button Grid.Row="0" Grid.Column="1" Width="100" HorizontalAlignment="Left" Command="{Binding PlayMusicCommand}">播放</Button>
<Button Grid.Row="0" Grid.Column="1" Width="100" HorizontalAlignment="Right" Click="Button_Click">初始化</Button>
<Button Grid.Row="0" Grid.Column="2" Command="{Binding PauseMusicCommand}">暂停</Button>
<Button Grid.Row="0" Grid.Column="3" Command="{Binding StopMusicCommand}">停止</Button>
<Slider Grid.Row="1" Grid.Column="0" Orientation="Vertical" HorizontalAlignment="Center" Minimum="0" Maximum="100" Value="{Binding Volume}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="ValueChanged">
<!--<i:InvokeCommandAction Command="{Binding RelativeSource={RelativeSource AncestorType=Window},Path=DataContext.AdjustVolumeCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorLevel=1,AncestorType={x:Type Slider}}}">
</i:InvokeCommandAction>-->
<i:InvokeCommandAction Command="{Binding AdjustVolumeCommand}" >
</i:InvokeCommandAction>
</i:EventTrigger>
</i:Interaction.Triggers>
</Slider>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Center">音量</Label>
<TextBox Grid.Row="3" Grid.Column="0" TextWrapping="Wrap" Text="{Binding FilePath}"></TextBox>
<Slider Grid.Row="1" Grid.Column="1" Orientation="Horizontal" Grid.ColumnSpan="3" VerticalAlignment="Center" Minimum="0" Maximum="100" Value="{Binding Progress}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="PreviewMouseLeftButtonUp">
<i:InvokeCommandAction Command="{Binding AdjustProgressBarCommand}">
</i:InvokeCommandAction>
</i:EventTrigger>
</i:Interaction.Triggers>
</Slider>
<Label Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="3" HorizontalAlignment="Center">进度</Label>
<views:SpectrumView x:Name="SpectrumView" Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="3">
</views:SpectrumView>
</Grid>
</Window>
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MusicViewModel MusicViewModel { get; set; }
public MainWindow()
{
InitializeComponent();
this.MusicViewModel = new MusicViewModel();
this.DataContext = this.MusicViewModel;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
WaveOutEvent outputDevice = this.MusicViewModel.OutputDevice;
AudioFileReader audioFile = this.MusicViewModel.AudioFile;
this.SpectrumView.Init(ref outputDevice, ref audioFile);
}
}
ViewModel
public class SpectrumViewModel : BindableBase
{
//输出设备
public WaveOutEvent OutputDevice { get; set; }
//音频文件
public AudioFileReader AudioFile { get; set; }
public SpectrumViewModel()
{
this.Value = new double[10];
this.SampleArray = new float[1024];
Timer timer = new Timer();
timer.Interval = 25;
timer.Tick += new EventHandler((sender, args) =>
{
if (null != this.OutputDevice && null != this.AudioFile && PlaybackState.Playing == this.OutputDevice.PlaybackState)
{
this.GetSampleArray();
this.Foo();
}
});
timer.Start();
}
#region 音频文件的信息
/// <summary>
/// 每个单声道数据样本的位数,例如 16位,24位,32位
/// </summary>
public int BitsPerSample { get; set; }
/// <summary>
/// 采样率,例如 44.1Khz ,就是 44100
/// </summary>
public int SampleRate { get; set; }
/// <summary>
/// 通道数,例如 2
/// </summary>
public int ChannelCount { get; set; }
/// <summary>
/// 初始化输出设备和音频文件
/// </summary>
/// <param name="outputDevice">输出设备</param>
/// <param name="audioFile">音频文件</param>
public void InitializeAudioInfo(ref WaveOutEvent outputDevice, ref AudioFileReader audioFile)
{
this.OutputDevice = outputDevice;
this.AudioFile = audioFile;
//因为我们读取音频文件,所以信息数据以音频信息为准
this.BitsPerSample = this.AudioFile.WaveFormat.BitsPerSample;
this.SampleRate = this.AudioFile.WaveFormat.SampleRate;
this.ChannelCount = this.AudioFile.WaveFormat.Channels;
}
#endregion
#region 获取音频采样信息
/// <summary>
/// 音频采样数据
/// </summary>
public float[] SampleArray { get; set; }
/// <summary>
/// 从音频文件获取采样数据
/// </summary>
private async void GetSampleArray()
{
//这个 1024 应该要根据音频文件动态设置(貌似一秒钟的音频需要的数组大小就是采样率,比如48000、96000等等),这里为了方便,所以写死
await Task.Run(() => { this.AudioFile.Read(this.SampleArray, 0, 1024); });
//这里的处理不太好,因为这个 Read() 貌似会改变 Position 的位置,导致输出的声音出现卡顿
//而且会抛异常,所以我扔到另外的线程去了
//你可以尝试把读取音频文件换成录制音频输出,这样应该就不会有卡顿了
}
#endregion
#region 获取频域数据
/// <summary>
/// 采样数据的对象锁,防止未分离左右通道就进入下一次采样
/// </summary>
private object _sampleLock = new object();
/// <summary>
/// 处理数据,不知道叫啥名,皆可Foo
/// </summary>
public async void Foo()
{
await Task.Run(() =>
{
#region 分离左右通道
//假设 SampleArray 中已经有数据
float[][] chanelSampleArray;
lock (this._sampleLock)//防止未分离完左右通道就进入下一次调用 SampleArray
{
chanelSampleArray = Enumerable
.Range(0, ChannelCount)//分离通道
.Select(chanel => Enumerable//对每个通过的数据进行处理
.Range(0, this.SampleArray.Length / this.ChannelCount)//每个通道的数组长度
.Select(i => this.SampleArray[chanel + i * this.ChannelCount])//左右左右,这样读取
.ToArray())
.ToArray();
}
#endregion
#region 合并左右通道并取平均值
float[] chanelAverageSample = Enumerable
.Range(0, chanelSampleArray[0].Length)
.Select(index => Enumerable//每次读取一个左右数据合并、取平均值
.Range(0, this.ChannelCount)
.Select(chanel => chanelSampleArray[chanel][index])
.Average())
.ToArray();
#endregion
#region 傅里叶变换
//NAudio 提供了快速傅里叶变换的方法, 通过傅里叶变换, 可以将时域数据转换为频域数据
// 取对数并向上取整
int log = (int)Math.Ceiling(Math.Log(chanelAverageSample.Length, 2));
//对于快速傅里叶变换算法, 需要数据长度为 2 的 n 次方
int length = (int)Math.Pow(2, log);
float[] filledSample = new float[length];
//拷贝到新数组
Array.Copy(chanelAverageSample, filledSample, chanelAverageSample.Length);
//将采样转化为复数
Complex[] complexArray = filledSample
.Select((value, index) => new Complex() { X = value })
.ToArray();
//进行傅里叶变换
FastFourierTransform.FFT(false, log, complexArray);
#endregion
#region 提取需要的频域信息
Complex[] halfComeplexArray = complexArray
.Take(complexArray.Length / 2)//数据是左右对称的,所以只取一半
.ToArray();
//这个已经是频域数据了
double[] resultArray = complexArray
.Select(value => Math.Sqrt(value.X * value.X + value.Y * value.Y))//复数取模
.ToArray();
//我们取 最小频率 ~ 20000Hz
//对于变换结果, 每两个数据之间所差的频率计算公式为 采样率/采样数, 那么我们要取的个数也可以由 20000 / (采样率 / 采样数) 来得出
//当然,因为我这里并没有指定频率与幅值,所以顺便取几个数就行,若有需要可以再去细分各个频率的幅值
int count = 20000 / (this.SampleRate / length);
double[] finalData = resultArray.Take(count).ToArray();
#endregion
#region 设置绑定数据
this.Value = finalData.Take(10).ToArray();
this.RaisePropertyChanged(nameof(this.Value));
#endregion
});
}
#endregion
/// <summary>
/// 频域数据
/// </summary>
public double[] Value { get; set; }
}
具体的注解我也写的比较清楚了,傅里叶变换那里涉及到数学知识我就不懂了,所以我也是照抄的
效果
后续补充
我闲着没事又查了查资料,大概理解了频域是怎么从时域里算出来的。
https://www.bilibili.com/video/BV1pW411J7s8
上面这个视频讲得挺详细的。数学,令人敬畏。
NAduio 自制音乐播放器以及简陋的可视化音频 结束
不得不说,Unity 和 虚幻引擎的音频可视化插件是真的好用,foobar 也很厉害,反正我写不出来 😦