視頻播放器-FFMPEG官方庫,包含lib,include,bin x64和x86平台的所有文件,提取碼4v2c
視頻播放器-LQVideo實現視頻解碼C++源代碼,提取碼br9u
視頻播放器-SoundTouch實現聲音變速的C++源代碼,提取碼6htk
根據上一篇的文章,我們已經能夠獲取變速后的聲音數據了,接下來我們需要能夠播放聲音,剛開始我使用的是NAudio技術,因為畢竟游戲的首要發布平台是steam,並且主要是兼容windows系統。但是我遇到了兩個問題:
- 莫名其妙的死鎖問題導致的崩潰,並且問題出在最底層的系統dll中,我完全沒辦法,雖然我通過上層的處理避免了死鎖(避免的辦法就是多個聲源的播放暫停和停止加鎖,不要同時執行),但是我仍然擔心在用戶的電腦上會出問題,尤其是這種幾乎沒法修復的問題。
- 第二個問題,已經寫入到緩存中的聲音數據我沒辦法清除。也就是我每次拖動視頻的時候聲音都會有短暫的停留,因為之前的聲音有緩存駐留在系統中。
因為這兩個問題的原因,我決定使用原生OpenAL接口,重新寫一套聲音播放插件,其實還有很關鍵的一點,OpenAL和OpenGL很像,比較偏底層,我自身很喜歡這些底層的技術。
本文分為四部分接收,簡短的加了第一部分,OpenAL介紹入門,剩下的三部分和之前一樣,所以,目錄如下:
- OpenAL入門
- OpenAL插件開發環境配置
- 接口設計及接口實現
- 需要注意的問題
OpenAL入門
我們只要打開搜索引擎輸入openal,首先映入眼簾的就是OpenAL: Cross Platform 3D Audio,這是OpenAL的官方網站,在這個網站我們完全可以正確的學習OpenAL的知識。通過首頁的介紹,我們可以得到如下信息:
- OpenAL是跨平台的
- OpenAL實現的是3D立體的聲音,也就是說其實我們生活中使用的很多聲音的特效(比如K歌軟件,音樂軟件),是可以通過OpenAL接口設計算法實現的
- 使用OpenAL播放聲音的三要素:
Listener:顧名思義,聽眾
Source:顧名思義,聲源
Buffer:緩存數據
然后是OpenAL文檔,官方提供的文檔有兩個
文檔1是OpenAL 1.1規范和參考,詳細介紹OpenAL的使用和部分設計原理,這部分可以挑選着看,我是挑選着看的。
文檔2是關於OpenAL API的介紹,這部分對初學者很重要,尤其是對我們打算開發的插件很重要,並且這部分很容易就看懂
最后是OpenAL的下載相關,既然是關於OpenAL的使用,我們直接進入環境配置介紹
OpenAL插件開發環境配置
- OpenAL SDK下載路徑http://www.openal.org/downloads/
- 只需要下載第一個OpenAL 1.1 Core SDK(zip)就可以了,然后雙擊安裝,記住安裝路徑,安裝完成后,在安裝路徑下的redist文件夾下有個oalinst.exe,雙擊一下,這步操作很重要,我們后面還會提到。
- 創建C++工程,為了方便測試,創建控制台程序。接下來就是之前的老三樣,配置附件包含目錄(在安裝路徑下的include文件夾),配置附加庫目錄(在安裝路徑下的Lib文件夾),附加依賴項添加OpenAL32.lib,這步完成后我們可以在代碼中添加OpenAL的頭文件了 alc.h和al.h
- 為了便於調試,我們使用alut工具庫,這個工具庫可以直接下載源代碼把代碼添加到剛才自己創建的工程中,alut的代碼結構很簡單
基本上就這幾個文件,添加一下就行了。
接口設計及接口實現
考慮一下我們需要實現的功能,首先必須支持多個聲音同時播放,因為我們之前設置視頻插件的時候視頻是支持10個同時播放的,所以這里我們也需要支持10個音頻同時播放。跟視頻插件的結構類似的,我們需要有一個音頻數據結構的類,我們命名為AudioData,同樣跟視頻插件相似的,需要一個存儲數據緩存的環形隊列,我們命名為BufferData,接下來,列出需要實現的接口:
- int InitOpenAL():初始化OpenAL設備信息,這里需要注意的是,就算同時播放多個音頻,打開設備和創建上下文只需要一次
- int SetSoundPitch(int key,float value):設置音頻的播放速度
- int SetSoundPlay(int key):播放音頻
- int SetSoundPause(int key):暫停音頻
- int SetSoundStop(int key):停止音頻播放
- bool HasProcessedBuffer(int key):是否有可用的緩存
- int SendBuffer(int key, ALvoid * data, ALsizei length):發送音頻數據
- int SetSampleRate(int key, short channels, short bit, int samples):設置音頻聲道,采樣點位數和采樣率
- int SetVolumn(int key, float value):設置音量,這里需要注意,如果設置了聲音衰減模型,這里設置音量是不好使的,因為聲音衰減模型針對的是單聲道音頻,而我們使用的是雙聲道音頻,所以我們不需要設置聲音衰減模型,具體怎么調整音量可以看一下該方法的實現。
- int Reset(int key):重置音頻數據信息,比如我們在拖動視頻的時候需要清空之前添加到緩存中的音頻數據,這里的實現原理是:停止音頻播放,停止播放后所有緩存隊列中的音頻數據就都不可用了,我們可以把所有的緩存取出來。這樣就能達到清空緩存音頻數據的目的了
- int Clear(int key):銷毀音頻數據對象,這里也需要注意,因為我們只打開一次設備和創建一次上下文,所以該方法只有在沒有音頻播放的情況下才會銷毀設備信息和上下文信息。
首先是BufferData.h
#pragma once #include <alc.h> #include <al.h> const ALint maxValue = 20; class BufferData { public: BufferData(); inline bool isFull() { return (circleEnd == circleStart - 1 || (circleEnd == maxValue - 1 && circleStart == 0)); } inline bool isEmpty() { return (circleStart == circleEnd); } inline void PushAll(int length) { for (int index = 0; index < length; index++) { Push(tempBuffer[index]); } } bool Push(ALuint value); ALuint Pop(); ALuint tempBuffer[maxValue]; ALuint acheBuffers[maxValue]; ALint circleStart = 0; ALint circleEnd = 0; ~BufferData(); private: };
BufferData.cpp
#include "BufferData.h" bool BufferData::Push(ALuint value) { if (isFull()) { return false; } if (circleEnd < maxValue - 1) { acheBuffers[++circleEnd] = value; } else if (circleStart > 0) { circleEnd = 0; acheBuffers[circleEnd] = value; } return true; } ALuint BufferData::Pop() { if (circleStart == circleEnd) { return -1; } if (circleStart == maxValue - 1) { circleStart = 0; return acheBuffers[maxValue - 1]; } else { return acheBuffers[circleStart++]; } } BufferData::BufferData() { } BufferData::~BufferData() { }
AudioData.h
#pragma once #include "BufferData.h" #include <queue> using namespace std; class AudioData { public: AudioData(); /*音頻播放源*/ ALuint source; /*音頻數據*/ ALuint* buffer; /*音頻當前播放狀態*/ ALint state; /*音頻格式*/ ALenum audioFormat = AL_FORMAT_STEREO16; /*聲道數*/ ALshort channel; /*音頻采樣率*/ ALsizei sample = 44100; queue<ALuint> bufferQueue; BufferData bufferData; bool isUsed = false; ~AudioData(); };
OpenALSound.h
#pragma once #include <iostream> #include "AL/alut.h" #include <string> #include "AudioData.h" #include "BufferData.h" using namespace std; extern "C" _declspec(dllexport) int InitOpenAL(); extern "C" _declspec(dllexport) int SetSoundPitch(int key,float value); extern "C" _declspec(dllexport) int SetSoundPlay(int key); extern "C" _declspec(dllexport) int SetSoundPause(int key); extern "C" _declspec(dllexport) int SetSoundStop(int key); extern "C" _declspec(dllexport) int SetSoundRewind(int key); extern "C" _declspec(dllexport) bool HasProcessedBuffer(int key); extern "C" _declspec(dllexport) int SendBuffer(int key, ALvoid * data, ALsizei length); extern "C" _declspec(dllexport) int SetSampleRate(int key, short channels, short bit, int samples); extern "C" _declspec(dllexport) int SetVolumn(int key, float value); extern "C" _declspec(dllexport) int Reset(int key); extern "C" _declspec(dllexport) int Clear(int key);
#include "OpenALSound.h" /*音頻播放設備*/ ALCdevice* device; /*音頻播放上下文*/ ALCcontext* context; static const size_t maxData = 10; AudioData audioArray[maxData]; int initCount = 0; /* 設置聲源 */ ALuint SetSource(int key) { alGenSources((ALuint)1, &audioArray[key].source); if (alGetError() != AL_NO_ERROR) { cout << "create source failed" << endl; return -1; } alSourcef(audioArray[key].source, AL_GAIN, 1); if (alGetError() != AL_NO_ERROR) { cout << "set source gain error" << endl; return -1; } alSource3f(audioArray[key].source, AL_POSITION, 0, 0, 0); if (alGetError() != AL_NO_ERROR) { cout << "set source position error" << endl; return -1; } alSource3f(audioArray[key].source, AL_VELOCITY, 0, 0, 0); if (alGetError() != AL_NO_ERROR) { cout << "set source velocity error" << endl; return -1; } alSourcei(audioArray[key].source, AL_LOOPING, AL_FALSE); if (alGetError() != AL_NO_ERROR) { cout << "set source looping error" << endl; return -1; } return 0; } int InitOpenAL() { int ret = -1; for (size_t index = 0; index < maxData; index++) { if (!audioArray[index].isUsed) { ret = index; break; } } if (ret==-1) { return ret; } if (initCount ==0) { const ALchar* deviceName = alcGetString(NULL, ALC_DEVICE_SPECIFIER); if (alGetError() != AL_NO_ERROR) { cout << "get device error" << endl; return -1; } // 打開device device = alcOpenDevice(deviceName); if (!device) { cout << "open device failed" << endl; return -1; } //創建context context = alcCreateContext(device, NULL); if (!alcMakeContextCurrent(context)) { cout << "make context failed" << endl; return -1; } } ALuint source = SetSource(ret); if (source == -1) { return -1; } alGenBuffers(maxValue, audioArray[ret].bufferData.acheBuffers); if (alGetError() != AL_NO_ERROR) { return -1; } audioArray[ret].bufferData.circleStart = 0; audioArray[ret].bufferData.circleEnd = maxValue - 1; audioArray[ret].isUsed = true; initCount++; return ret; } /*根據聲道數和采樣率返回聲音格式*/ ALenum to_al_format(int key, short channels, short samples) { bool stereo = (channels > 1); switch (samples) { case 16: if (stereo) return AL_FORMAT_STEREO16; else return AL_FORMAT_MONO16; case 8: if (stereo) return AL_FORMAT_STEREO8; else return AL_FORMAT_MONO8; default: return -1; } } int SetVolumn(int key, float value) { alSourcef(audioArray[key].source,AL_GAIN,value); if (alGetError() != AL_NO_ERROR) { return -1; } return 0; } /*設置語速*/ int SetSoundPitch(int key, float value) { if (audioArray[key].source!=NULL) { alSourcef(audioArray[key].source, AL_PITCH, 1); if (alGetError()!=AL_NO_ERROR) { return -1; } else { return 0; } } return -1; } /*播放語音*/ int SetSoundPlay(int key) { if (audioArray[key].source==NULL) { return -1; } alGetSourcei(audioArray[key].source, AL_SOURCE_STATE, &audioArray[key].state); if (audioArray[key].state == AL_PLAYING) { return 0; } else { alSourcePlay(audioArray[key].source); if (alGetError() != AL_NO_ERROR) { return -1; } return 0; } } /*暫停語音播放*/ int SetSoundPause(int key) { if (audioArray[key].source == NULL) { return -1; } alGetSourcei(audioArray[key].source, AL_SOURCE_STATE, &audioArray[key].state); if (audioArray[key].state == AL_PAUSED) { return 0; } else { alSourcePause(audioArray[key].source); if (alGetError() != AL_NO_ERROR) { return -1; } return 0; } } /*停止語音播放*/ int SetSoundStop(int key) { if (audioArray[key].source == NULL) { return -1; } alGetSourcei(audioArray[key].source, AL_SOURCE_STATE, &audioArray[key].state); if (audioArray[key].state == AL_STOPPED) { return 0; } else { alSourceStop(audioArray[key].source); if (alGetError() != AL_NO_ERROR) { return -1; } return 0; } } int SetSoundRewind(int key) { if (audioArray[key].source == NULL) { return -1; } alGetSourcei(audioArray[key].source, AL_SOURCE_STATE, &audioArray[key].state); if (audioArray[key].state == AL_INITIAL) { return 0; } else { alSourceRewind(audioArray[key].source); if (alGetError() != AL_NO_ERROR) { return -1; } return 0; } } bool HasProcessedBuffer(int key) { ALint a=-1; alGetSourcei(audioArray[key].source, AL_BUFFERS_PROCESSED, &a); if (alGetError() != AL_NO_ERROR) { return false; } if (a>0) { alSourceUnqueueBuffers(audioArray[key].source, a, audioArray[key].bufferData.tempBuffer); audioArray[key].bufferData.PushAll(a); } return (!audioArray[key].bufferData.isEmpty())?true:false; } /*發送音頻數據*/ int SendBuffer(int key, ALvoid * data,ALsizei length) { ALuint temp = audioArray[key].bufferData.Pop(); if (temp==-1) { return -1; } if (alGetError() != AL_NO_ERROR) { return -1; } alBufferData(temp, audioArray[key].audioFormat, data, length, audioArray[key].sample); if (alGetError() != AL_NO_ERROR) { return -1; } alSourceQueueBuffers(audioArray[key].source, 1, &temp); return 0; } /*設置采樣率*/ int SetSampleRate(int key, short channels,short bit, int samples) { audioArray[key].channel = channels; audioArray[key].sample = samples; to_al_format(key,channels, bit); return 0; } int Reset(int key) { SetSoundStop(key); ALint a = -1; alGetSourcei(audioArray[key].source, AL_BUFFERS_PROCESSED, &a); if (alGetError() != AL_NO_ERROR) { return -1; } if (a > 0) { alSourceUnqueueBuffers(audioArray[key].source, a, audioArray[key].bufferData.tempBuffer); audioArray[key].bufferData.PushAll(a); } return 0; } int Clear(int key) { alDeleteSources(1, &audioArray[key].source); alDeleteBuffers(maxValue, audioArray[key].bufferData.acheBuffers); initCount--; if (initCount==0) { device = alcGetContextsDevice(context); alcMakeContextCurrent(NULL); alcDestroyContext(context); alcCloseDevice(device); } audioArray[key].isUsed = false; return 0; }
需要注意的問題
1. 全局聲明了initCount值,這個值為了驗證當前是否已經初始化過設備和上下文信息,如果initCount==0,表示沒有初始化過,每次添加一個音頻該值加1,每次釋放音頻該值減1,如果釋放的時候該值是0,釋放設備和上下文信息。
2. 因為是根據輸入的音頻的數據段播放聲音,所以插件采用的是緩存隊列來加載音頻數據和獲取播放完成的緩存數據,具體的操作方法可以參考官方API。
3. 下面的代碼是我測試10個視頻同時播放的例子,我測試是沒問題的
ALvoid * LoadFile(int key,ALsizei& size) { ALsizei freq; ALvoid * data; ALenum format; ALboolean loop = AL_FALSE; ALbyte name1[] = "2954.wav"; ALbyte *name = name1; alutLoadWAVFile(name, &audioArray[key].audioFormat, &data, &size, &freq, &loop); if (alGetError() != AL_NO_ERROR) { return NULL; } return data; } int main() { float key; ALsizei size; for (int index=0;index<10;index++) { if (InitOpenAL() == -1) { cin >> key; return -1; } if (SetSampleRate(index, 1, 16, 11025) == -1) { cin >> key; return -1; } ALvoid *data = LoadFile(index, size); while ((HasProcessedBuffer(index))) { SendBuffer(index, data, size); } SetSoundPlay(index); } while (cin >> key) { SetVolumn(0, key); if (key == 0) { break; } } Clear(0); }
到這里基本所有視頻播放器使用的插件部分就全部介紹完了,從下一篇我們開始介紹在Unity3d中實現視頻播放器插件的部分。