日本xxxx18视频在线观看-日本xxxx1819-日本xxxwww在线观看-日本xxx-日本xx-日本www在线视频

LOGO OA教程 ERP教程 模切知識(shí)交流 PMS教程 CRM教程 開發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

C#播放音頻的正確姿勢:NAudio的簡介與基礎(chǔ)播放

admin
2021年2月1日 9:42 本文熱度 4950

前言

各網(wǎng)查了一圈,NAudio相關(guān)中文資料較少。鑒于本人最近在使用此庫的播放音頻方面有所涉及,在此將自己的學(xué)習(xí)過程與經(jīng)驗(yàn)總結(jié)與大家分享,同時(shí)也歡迎大佬探討和指正。

簡介

為什么使用NAudio

NAudio為.NET平臺(tái)下的開源庫,采用ML-PL協(xié)議,開源地址:https://github.com/naudio/NAudio。截至今日,已有約2.4k的stars。

NAudio功能強(qiáng)大,且其入門容易。

強(qiáng)大在于:它支持許多音頻操作,可實(shí)現(xiàn)多種API播放與錄制、多種不同音頻格式、音頻格式轉(zhuǎn)換(重采樣、位深、聲道等)、音頻編碼、多通道播放、音頻效果處理等等(詳細(xì)介紹可以看Github readme)。
入門容易在于:對(duì)C#的語法、結(jié)構(gòu)友好,且對(duì)于一個(gè)僅僅是播放聲音的需求,幾行即可搞定:

using(var audioFile = new AudioFileReader(audioFile))

using(var outputDevice = new WaveOutEvent())

{

    outputDevice.Init(audioFile);

    outputDevice.Play(); // 異步執(zhí)行

    

    while (outputDevice.PlaybackState == PlaybackState.Playing)

    {

        Thread.Sleep(1000);

    }

}?

Demo來自于官方Readme

另一方面,基于NAudio本身的架構(gòu)值得學(xué)習(xí)

其框架系統(tǒng)、完善,但實(shí)際開箱即用的功能并不是十分的齊全(相對(duì)于Bass),對(duì)于一個(gè)喜愛倒騰的人來說,容易激發(fā)學(xué)習(xí)研究的興趣,其官方教程與例子很是齊全。

快速入門:https://github.com/naudio/NAudio#tutorials

深入學(xué)習(xí):https://markheath.net/category/naudio(作者博客)

與其他播放方式對(duì)比

基于使用角度考慮,NAudio的優(yōu)勢在于,它是一個(gè)原生的.NET輕量庫(其底層與其他API交互,但透明于使用者)。在不需要COM、獨(dú)立SDK、手動(dòng)P/Invoke的同時(shí),對(duì)于音頻交互更加可控、并且可以完成比以上更加復(fù)雜的功能。當(dāng)然其也有一定的不足,例如目前無法跨平臺(tái),底層API強(qiáng)依賴于Windows(作者表示期待.NET Core的Span<T>的后續(xù)發(fā)展,時(shí)機(jī)成熟會(huì)考慮跨平臺(tái))。

目前常見的播放方案:

方式簡介備注
系統(tǒng)事件聲音僅播放系統(tǒng)事件聲音System.Media.SystemSounds 靜態(tài)類
SoundPlayer使用方便。但是僅支持PCM的wav播放、單通道播放System.Media.SoundPlayer
Windows Media Player COM組件要求電腦上安裝WMP,僅能完成簡單播放功能,不利于自定義化
MME API (Multimedia Extensions)自由度高。但是由于未經(jīng)封裝,若需求復(fù)雜則操作復(fù)雜,且P/Invoke不安全winmm.dll
DirectX自由度高,相較于MME更為現(xiàn)代化,能從硬件層完成更多音頻功能DirectX SDK
Bass功能強(qiáng)大的封裝,但常見交換庫對(duì)C#的語法、結(jié)構(gòu)不友好Bass.NET(需進(jìn)行授權(quán)使用) 或 ManagedBass

還有很多未列出。

例1:制作一個(gè)簡易的音樂播放器

目標(biāo):制作一個(gè)Winform的音樂播放器,僅實(shí)現(xiàn)讀取mp3、播放、暫停、停止、進(jìn)度拖動(dòng)及顯示、音量控制功能。

為了直觀的展示,本例將弱化OOP封裝思想。

回顧開篇的代碼:

using(var audioFile = new AudioFileReader(audioFile))

using(var outputDevice = new WaveOutEvent())

{

    outputDevice.Init(audioFile);

    outputDevice.Play(); // 異步執(zhí)行

    

    while (outputDevice.PlaybackState == PlaybackState.Playing)

    {

        Thread.Sleep(1000);

    }

}?

顯然,這只能完成最基礎(chǔ)的播放功能。而且對(duì)于一個(gè)GUI播放器而言,這樣做會(huì)帶來很多問題。

首先它會(huì)在播放時(shí)阻塞線程,其次當(dāng)播放完畢就會(huì)立刻釋放資源,無法對(duì)其進(jìn)行任何控制。

針對(duì)以上缺陷完善代碼:

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.IO;

using System.Linq;

using System.Text;

using System.Threading;

using System.Threading.Tasks;

using System.Windows.Forms;

using NAudio.Wave;

using NAudio.Wave.SampleProviders;


namespace SimplePlayer

{

    public partial class FormPlayer : Form

    {

        private IWavePlayer _device;

        private AudioFileReader _reader;


        public FormPlayer()

        {

            InitializeComponent();

        }


        private void btnPlay_Click(object sender, EventArgs e)

        {

            PlayAction();

        }


        private void btnPause_Click(object sender, EventArgs e)

        {

            PauseAction();

        }


        private void btnStop_Click(object sender, EventArgs e)

        {

            StopAction();

        }


        private void btnOpen_Click(object sender, EventArgs e)

        {

            var ofd = new OpenFileDialog

            {

                Filter = "支持的文件|*.mp3;*.wav;*.aiff|所有文件|*.*",

                Multiselect = false

            };

            var result = ofd.ShowDialog();

            if (result != DialogResult.OK) return;


            DisposeAll();


            try

            {

                var fileName = ofd.FileName;


                if (!File.Exists(fileName))

                    throw new FileNotFoundException("所選文件不存在");

                _device = new WaveOutEvent(); // Create device

                _reader = new AudioFileReader(fileName); // Create reader


                _device.Init(_reader);

                _device.PlaybackStopped += Device_OnPlaybackStopped;

            }

            catch (Exception ex)

            {

                DisposeAll();

                MessageBox.Show(ex.Message);

            }

        }


        private void Form_Closed(object sender, EventArgs e)

        {

            DisposeAll();

        }


        private void Device_OnPlaybackStopped(object obj, StoppedEventArgs arg)

        {

            StopAction();

        }


        private void StopAction()

        {

            _device?.Stop();

            if (_reader != null) _reader.Position = 0;

        }


        private void PlayAction()

        {

            _device?.Play();

        }


        private void PauseAction()

        {

            _device?.Pause();

        }


        private void DisposeDevice()

        {

            if (_device != null)

            {

                _device.PlaybackStopped -= Device_OnPlaybackStopped;

                _device.Dispose();

            }

        }


        private void DisposeAll()

        {

            _reader?.Dispose();

            DisposeDevice();

        }

    }

}

以上完成了一個(gè)可以打開文件、播放、暫停、停止、釋放資源的基礎(chǔ)功能播放器。接下來完善一下進(jìn)度顯示以及進(jìn)度調(diào)整。

private CancellationTokenSource _cts;

private bool _sliderLock; // 邏輯鎖,當(dāng)為true時(shí)不更新界面上的進(jìn)度


private void sliderProgress_MouseDown(object sender, MouseEventArgs e)

{

    _sliderLock = true; // 拖動(dòng)開始,停止更新界面

}


private void sliderProgress_MouseUp(object sender, MouseEventArgs e)

{

    // 釋放鼠標(biāo)時(shí),應(yīng)用目標(biāo)進(jìn)度

    _reader.CurrentTime = TimeSpan.FromMilliseconds(sliderProgress.Value);

    UpdateProgress();

    _sliderLock = false; // 拖動(dòng)結(jié)束,恢復(fù)更新界面

}


private void sliderProgress_ValueChanged(object sender, EventArgs e)

{

    if (_sliderLock)

    {

        // 拖動(dòng)時(shí)可以直觀看到目標(biāo)進(jìn)度

        lblPosition.Text = TimeSpan.FromMilliseconds(sliderProgress.Value).ToString(@"mm\:ss");

    }

}


private void StartUpdateProgress()

{

  // 此處可用Timer完成而不是手動(dòng)循環(huán),但不建議使用UI線程上的Timer

    Task.Run(() =>

    {

        while (!_cts.IsCancellationRequested)

        {

            if (_device.PlaybackState == PlaybackState.Playing)

            {

              // 若為播放狀態(tài),持續(xù)更新界面

                BeginInvoke(new Action(UpdateProgress));

                Thread.Sleep(100);

            }

            else

            {

                Thread.Sleep(50);

            }

        }

    });

}


private void UpdateProgress()

{

    var currentTime = _reader?.CurrentTime ?? TimeSpan.Zero; // 當(dāng)前時(shí)間

    Console.WriteLine(currentTime);


    if (!_sliderLock)

    {

        sliderProgress.Value = (int)currentTime.TotalMilliseconds;

        lblPosition.Text = currentTime.ToString(@"mm\:ss");

    }

}


// 更新此方法

private void btnOpen_Click(object sender, EventArgs e)

{

    ...

        _device.Init(_reader);


        var duration = _reader.TotalTime; // 總時(shí)長

        sliderProgress.Maximum = (int)duration.TotalMilliseconds;

        lblDuration.Text = duration.ToString(@"mm\:ss");


        _cts = new CancellationTokenSource();

        StartUpdateProgress(); // 界面更新線程


        _device.PlaybackStopped += Device_OnPlaybackStopped;

    ...

}


// 更新此方法

private void StopAction()

{

    ...

    if (_reader != null) _reader.Position = 0;

    UpdateProgress(); 

}


// 更新此方法

private void DisposeAll()

{

    _cts?.Cancel();

    _cts?.Dispose();

    _reader?.Dispose();

    ...

}?

以上完成了進(jìn)度顯示以及進(jìn)度調(diào)整,里面包含了一些UI上的優(yōu)化后的交互邏輯。其中涉及到了個(gè)人常用的Task / Cancellation的線程模式,可用Timer代替。

那么最后一個(gè)功能,如何進(jìn)行音量控制?事實(shí)上,IWavePlayer接口包含了Volume這個(gè)屬性,所以如果僅僅要達(dá)成這個(gè)目標(biāo)十分簡單,只需進(jìn)行屬性設(shè)置即可:

private void SetVolume(float volume)

{

    if (_device != null) _device.Volume = volume;

}

然而,這樣做法并不推薦,因?yàn)閷?duì)于內(nèi)部的WaveOutEventIWavePlayer實(shí)現(xiàn),實(shí)際效果是從改變了系統(tǒng)的合成器中的音量,如圖:


也就意味著,這將改變整個(gè)應(yīng)用程序的音量,不利于之后進(jìn)行程序內(nèi)部混音。

那將如何實(shí)現(xiàn)內(nèi)部音量處理呢?這就涉及了DSP音頻處理。在NAudio中,通過實(shí)現(xiàn)接口ISampleProvider,得到WaveStream提供音頻原始數(shù)據(jù)并且進(jìn)行處理,再將處理后的數(shù)據(jù)返回。將多個(gè)ISampleProvider鏈接起來進(jìn)行順序處理,最終將最外層的ISampleProvider交給IWavePlayer進(jìn)行初始化Init()這樣的一個(gè)處理模式。也就是說,其實(shí)基于上面的代碼來看,AudioFileReader本身既是WaveStream,也實(shí)現(xiàn)了ISampleProvider

https://stackoverflow.com/questions/46433790/how-to-chain-together-multiple-naudio-isampleprovider-effects

說了這么多有點(diǎn)繞口,用簡潔的方法表示,就是將之前的
AudioFileReader -> IWavePlayer.Init()
替換成
AudioFileReader -> 某種可以控制音量的處理 -> IWavePlayer.Init()

在NAudio內(nèi)置提供的DSP中,實(shí)現(xiàn)了音量處理相關(guān)的類VolumeSampleProvider,因此直接拿來用即可。

以上內(nèi)容推薦結(jié)合NAudio源碼食用

根據(jù)以上所述,更新代碼:

private VolumeSampleProvider _volumeProvider;


private void sliderVolume_ValueChanged(object sender, EventArgs e)

{

    UpdateVolume();

}


// 更新此方法

private void UpdateVolume()

{

    var volume = sliderVolume.Value / 100f;

    _volumeProvider.Volume = volume;

    //if (_device != null) _device.Volume = volume;  // 注釋這一句

}


// 更新此方法

private void btnOpen_Click(object sender, EventArgs e)

{

    ...

        _reader = new AudioFileReader(fileName); // Create reader


        // dsp start

        _volumeProvider = new VolumeSampleProvider(_reader)

        {

            Volume = sliderVolume.Value / 100f

        };

        // dsp end


        _device.Init(_volumeProvider);

        //_device.Init(_reader); // 之前是reader,現(xiàn)改為VolumeSampleProvider


        var duration = _reader.TotalTime; // 總時(shí)長

        ...

}

這樣就對(duì)原始音頻進(jìn)行了處理(改變音量),然后輸出。

完成后的全部代碼:

using System;

using System.IO;

using System.Threading;

using System.Threading.Tasks;

using System.Windows.Forms;

using NAudio.Wave;

using NAudio.Wave.SampleProviders;


namespace SimplePlayer

{

    public partial class FormPlayer : Form

    {

        private IWavePlayer _device;

        private AudioFileReader _reader;


        private VolumeSampleProvider _volumeProvider;


        private CancellationTokenSource _cts;


        private bool _sliderLock; // 邏輯鎖,當(dāng)為true時(shí)不更新界面上的進(jìn)度


        public FormPlayer()

        {

            InitializeComponent();

        }


        private void btnPlay_Click(object sender, EventArgs e)

        {

            PlayAction();

        }


        private void btnPause_Click(object sender, EventArgs e)

        {

            PauseAction();

        }


        private void btnStop_Click(object sender, EventArgs e)

        {

            StopAction();

        }


        private void btnOpen_Click(object sender, EventArgs e)

        {

            var ofd = new OpenFileDialog

            {

                Filter = "支持的文件|*.mp3;*.wav;*.aiff|所有文件|*.*",

                Multiselect = false

            };

            var result = ofd.ShowDialog();

            if (result != DialogResult.OK) return;


            DisposeAll();


            try

            {

                var fileName = ofd.FileName;


                if (!File.Exists(fileName))

                    throw new FileNotFoundException("所選文件不存在");

                _device = new WaveOutEvent(); // Create device

                _reader = new AudioFileReader(fileName); // Create reader


                // dsp start

                _volumeProvider = new VolumeSampleProvider(_reader)

                {

                    Volume = sliderVolume.Value / 100f

                };

                // dsp end


                _device.Init(_volumeProvider);

                //_device.Init(_reader); // 之前是reader,現(xiàn)改為VolumeSampleProvider

                // https://stackoverflow.com/questions/46433790/how-to-chain-together-multiple-naudio-isampleprovider-effects


                var duration = _reader.TotalTime; // 總時(shí)長

                sliderProgress.Maximum = (int)duration.TotalMilliseconds;

                lblDuration.Text = duration.ToString(@"mm\:ss");


                _cts = new CancellationTokenSource();

                StartUpdateProgress(); // 界面更新線程


                _device.PlaybackStopped += Device_OnPlaybackStopped;

            }

            catch (Exception ex)

            {

                DisposeAll();

                MessageBox.Show(ex.Message);

            }

        }


        private void sliderProgress_MouseDown(object sender, MouseEventArgs e)

        {

            _sliderLock = true; // 拖動(dòng)開始,停止更新界面

        }


        private void sliderProgress_MouseUp(object sender, MouseEventArgs e)

        {

            // 釋放鼠標(biāo)時(shí),應(yīng)用目標(biāo)進(jìn)度

            _reader.CurrentTime = TimeSpan.FromMilliseconds(sliderProgress.Value);

            UpdateProgress();

            _sliderLock = false; // 拖動(dòng)結(jié)束,恢復(fù)更新界面

        }


        private void sliderProgress_ValueChanged(object sender, EventArgs e)

        {

            if (_sliderLock)

            {

                // 拖動(dòng)時(shí)可以直觀看到目標(biāo)進(jìn)度

                lblPosition.Text = TimeSpan.FromMilliseconds(sliderProgress.Value).ToString(@"mm\:ss");

            }

        }


        private void sliderVolume_ValueChanged(object sender, EventArgs e)

        {

            UpdateVolume();

        }


        private void Form_Load(object sender, EventArgs e)

        {


        }


        private void Form_Closed(object sender, EventArgs e)

        {

            DisposeAll();

        }


        private void Device_OnPlaybackStopped(object obj, StoppedEventArgs arg)

        {

            StopAction();

        }


        private void StartUpdateProgress()

        {

            // 此處可用Timer完成而不是手動(dòng)循環(huán),但不建議使用UI線程上的Timer

            Task.Run(() =>

            {

                while (!_cts.IsCancellationRequested)

                {

                    if (_device.PlaybackState == PlaybackState.Playing)

                    {

                        // 若為播放狀態(tài),持續(xù)更新界面

                        BeginInvoke(new Action(UpdateProgress));

                        Thread.Sleep(100);

                    }

                    else

                    {

                        Thread.Sleep(50);

                    }

                }

            });

        }


        private void StopAction()

        {

            _device?.Stop();

            if (_reader != null) _reader.Position = 0;

            UpdateProgress();

        }


        private void PlayAction()

        {

            _device?.Play();

        }


        private void PauseAction()

        {

            _device?.Pause();

        }


        private void UpdateProgress()

        {

            var currentTime = _reader?.CurrentTime ?? TimeSpan.Zero; // 當(dāng)前時(shí)間

            Console.WriteLine(currentTime);


            if (!_sliderLock)

            {

                sliderProgress.Value = (int)currentTime.TotalMilliseconds;

                lblPosition.Text = currentTime.ToString(@"mm\:ss");

            }

        }


        private void UpdateVolume()

        {

            var volume = sliderVolume.Value / 100f;

            _volumeProvider.Volume = volume;

            //if (_device != null) _device.Volume = volume;  // 注釋這一句

        }


        private void DisposeDevice()

        {

            if (_device != null)

            {

                _device.PlaybackStopped -= Device_OnPlaybackStopped;

                _device.Dispose();

            }

        }


        private void DisposeAll()

        {

            _cts?.Cancel();

            _cts?.Dispose();

            _reader?.Dispose();

            DisposeDevice();

        }

    }

}

這樣本例目標(biāo)功能就實(shí)現(xiàn)完畢了,能實(shí)現(xiàn)最基礎(chǔ)但是同時(shí)也可靠的音頻播放功能。

注(坑):

  • IWavePlayer的創(chuàng)建,最好在STA線程中完成
  • 一個(gè)進(jìn)程中僅創(chuàng)建一個(gè)IWavePlayer為佳(若需進(jìn)行多個(gè)通道進(jìn)行播放,同時(shí)播放背景音與音效,可以關(guān)注一下我后面的更新)

相關(guān)源代碼會(huì)隨著本系列進(jìn)行更新(如果不鴿):
https://github.com/Milkitic/NAudioDemo

順便宣傳一下個(gè)人在應(yīng)用的一個(gè)NAudio相關(guān)的開源項(xiàng)目:
https://github.com/Milkitic/Osu-Player

參考:
[1] Windows legacy audio components


該文章在 2025/4/30 9:33:22 編輯過

全部評(píng)論1

admin
2021年2月1日 9:59
關(guān)鍵字查詢
相關(guān)文章
正在查詢...
點(diǎn)晴ERP是一款針對(duì)中小制造業(yè)的專業(yè)生產(chǎn)管理軟件系統(tǒng),系統(tǒng)成熟度和易用性得到了國內(nèi)大量中小企業(yè)的青睞。
點(diǎn)晴PMS碼頭管理系統(tǒng)主要針對(duì)港口碼頭集裝箱與散貨日常運(yùn)作、調(diào)度、堆場、車隊(duì)、財(cái)務(wù)費(fèi)用、相關(guān)報(bào)表等業(yè)務(wù)管理,結(jié)合碼頭的業(yè)務(wù)特點(diǎn),圍繞調(diào)度、堆場作業(yè)而開發(fā)的。集技術(shù)的先進(jìn)性、管理的有效性于一體,是物流碼頭及其他港口類企業(yè)的高效ERP管理信息系統(tǒng)。
點(diǎn)晴WMS倉儲(chǔ)管理系統(tǒng)提供了貨物產(chǎn)品管理,銷售管理,采購管理,倉儲(chǔ)管理,倉庫管理,保質(zhì)期管理,貨位管理,庫位管理,生產(chǎn)管理,WMS管理系統(tǒng),標(biāo)簽打印,條形碼,二維碼管理,批號(hào)管理軟件。
點(diǎn)晴免費(fèi)OA是一款軟件和通用服務(wù)都免費(fèi),不限功能、不限時(shí)間、不限用戶的免費(fèi)OA協(xié)同辦公管理系統(tǒng)。
Copyright 2010-2025 ClickSun All Rights Reserved

主站蜘蛛池模板: 欧美一级二级一区二区 | 国产欧美日韩精品成人动态 | 国产欧美日韩综合精品一级 | 国产欧美va欧美va香蕉在 | 日韩欧美亚洲一区精选 | 无限资源吧国产片1在线观看 | 伦理电影网 | 欧美日韩亚洲精品瑜伽裤 | 国语自产偷成人精品视频 | 成年免费a级毛 | 国产亚洲精品成人a在线 | 国产精品一区二区三区四区五区 | 欧美日韩国产免费看 | 国产综合色在线精品 | 日本不卡一 | 国产在线精品一区在线观看 | 国产久9视频这里只有精品 91三级视频在线观看 | 国产精品人妇一区二区三区 | 国产玉足脚交极品在线播放 | 成年片色大黄全免费网站观看 | 国产97色在线 | 欧美精产国 | 国产视频一区在线一区在线看 | 日本人伦一区二区三区 | 亚洲va| 精品国精品国产自在久国产87 | 精品一区二区三区 | www日本| 凹凸福利午 | 欧美日韩国产一区二区 | 精品亚洲成a人在线观看青青 | 国语自产偷拍精品视频偷97 | 日韩精品专区在线影院重磅 | 国产酒店揄拍视频在线观看 | 国自产精品手机在线观看 | 九一视频免费观看 | 日韩精品在线开放 | 欧美日韩综合不卡一区二区三区 | 老司机老色鬼精品免费视频 | 欧美激情视频一区二区 | 男女拍拍拍免费视频网站 |