系統級方案和自建協議html
windows平臺、linux平臺、嵌入式linux平臺、mcu平臺linux
1,在嵌入式Linux上開發的有線通訊語音解決方案ios
這方案是在嵌入式Linux上開發的,音頻方案基於ALSA,語音通訊相關的都是在user space 作,算是一個上層的解決方案。因爲是有線通訊,網絡環境相對無線通訊而言不是特別惡劣,用的丟包補償措施也不是不少,主要有PLC、RFC2198等。git
2,在Android手機上開發的傳統無線通訊語音解決方案 github
這方案是在Android手機上開發的,是手機上的傳統語音通訊方案(相對於APP語音通訊而言)。Android是基於Linux的,因此也會用到ALSA,可是主要是作控制用,如對codec芯片的配置等。跟音頻數據相關的驅動、編解碼、先後處理等在Audio DSP上開發,網絡側相關的在CP(通訊處理器)上開發,算是一個底層解決方案。該方案的軟件框圖以下: 算法
聲卡 (Sound Card)也叫音頻卡(港臺稱之爲聲效卡),是計算機多媒體系統中最基本的組成部分,是實現聲波/數字信號相互轉換的一種硬件。聲卡的基本功能是把來自話筒、磁帶、光盤的原始聲音信號加以轉換,輸出到耳機、揚聲器、擴音機、錄音機等聲響設備,或經過音樂設備數字接口(MIDI)發出合成樂器的聲音編程
全部的電腦主板基本都有集成聲卡的,若是有專業要求會再買個獨立聲卡,就像專業玩家同樣買個獨立顯卡,手動狗頭windows
聲卡驅動api
對於音頻處理的技術,主要有以下幾種:緩存
1、MME(MultiMedia Extensions)
MME就是winmm.dll提供的接口,也是Windows平臺下第一代API。優勢是使用簡單,通常場景下能夠知足業務需求,缺點是延遲高,某些高級功能沒法實現。
2、XAudio2
也是DirextX的一部分,爲了取代DirectSound。DirextX套件中的音頻組件,大多用於遊戲中,支持硬件加速,因此比MME有更低的延遲。
Vista系統開始引入的新架構,它是以COM的方式提供的接口,用戶模式下處於最底層,上面提到的幾種API最終都將使用它!功能最強,性能最好,可是接口繁雜,使用起來很麻煩。
4、Wasapi 就能夠了(高性能,但更復雜)
而Wave系列的API函數主要是用來實現對麥克風輸入的採集(使用WaveIn系列API函數)和控制聲音的播放(使用後WaveOut系列函數)。
涉及的API函數:
waveInOpen
開啓音頻採集設備,成功後會返回設備句柄,後續的API都須要使用該句柄
調用模塊須要提供一個回調函數(waveInProc),以接收採集的音頻數據
waveInClose
關閉音頻採集模塊
成功後,由waveInOpen返回的設備句柄將再也不有效
waveInPrepareHeader
準備音頻採集數據緩存的空間
waveInUnprepareHeader
清空音頻採集的數據緩存
waveInAddBuffer
將準備好的音頻數據緩存提供給音頻採集設備
在調用該API以前須要先調用waveInPrepareHeader
waveInStart
控制音頻採集設備開始對音頻數據的採集
waveInStop
控制音頻採集設備中止對音頻數據的採集
音頻採集設備採集到音頻數據後,會調用在waveInOpen中設置的回調函數。
其中參數包括一個消息類型,根據其消息類型就能夠進行相應的操做。
如接收到WIM_DATA消息,則說明有新的音頻數據被採集到,這樣就能夠根據須要來對這些音頻數據進行處理。
(示例之後補上)
涉及的接口有:
IMMDeviceEnumerator
IMMDevice
IAudioClient
IAudioCaptureClient
主要過程:
建立多媒體設備枚舉器(IMMDeviceEnumerator)
經過多媒體設備枚舉器獲取聲卡接口(IMMDevice)
經過聲卡接口獲取聲卡客戶端接口(IAudioClient)
經過聲卡客戶端接口(IAudioClient)可獲取聲卡輸出的音頻參數、初始化聲卡、獲取聲卡輸出緩衝區的大小、開啓/中止對聲卡輸出的採集
經過聲卡採集客戶端接口(IAudioCaptureClient)可獲取採集的聲卡輸出數據,並對內部緩衝區進行控制
(示例之後補上)
混音算法就是將多路音頻輸入信號根據某種規則進行運算(多路音頻信號相加後作限幅處理),獲得一路混合後的音頻,並以此做爲輸出的過程。
我目前還作過這一塊,搜索了一下基本有以下幾種混音算法:
將多路音頻輸入信號直接相加取和做爲輸出
將多路音頻輸入信號直接相加取和後,再除以混音通道數,防止溢出
將多路音頻輸入信號直接相加取和後,作Clip操做(將數據限定在最大值和最小值之間),若有溢出就設最大值
將多路音頻輸入信號直接相加取和後,作飽和處理,接近最大值時進行扭曲
將多路音頻輸入信號直接相加取和後,作歸一化處理,所有乘個係數,使幅值歸一化
將多路音頻輸入信號直接相加取和後,使用衰減因子限制幅值
ALSA是目前linux的主流音頻體系架構
是一個有社區維護的開源項目:http://www.alsa-project.org/
包括:
1.內核驅動包 alsa-driver
2.用戶空間庫 alsa-lib
3.附加庫插件包 alsa-libplugins
4.音頻處理工具集 alsa-utils
5.其餘音頻處理小工具包 alsa-tools
6.特殊音頻固件支持包 alsa-firmware
7.alsa-lib的Python綁定包 pyalsa
8.OSS接口兼容包 alsa-oss
9.內核空間中,alsa-soc實際上是對alsa-driver的進一步封裝,他針對嵌入式設備提供了一些列加強的功能。
安裝
sudo apt install libasound2-dev流程
- 打開設備
- 分配參數內存
- 填充默認參數
- 設置參數(詳細的參見 ALSA - PCM接口)
- 通道數
- 採樣率(碼率,用來指定時間和文件大小,frames/s)
- 幀數(每次讀取的數據長度與該參數有關)
- 數據格式(影響輸出數據、緩存大小)
- 設備訪問類型(直接讀寫、內存映射,交錯模式、非交錯模式)
- 讀取、寫入數據
簡單的例子
包含頭文件
#include <alsa/asoundlib.h>查看設備,根據最後兩個數字肯定設備名稱,一般default就好了
aplay -L定義相關參數,錄放音都要通過相同的步驟,放一塊兒定義
// 設備名稱,這裏採用默認,還能夠選取"hw:0,0","plughw:0,0"等 const char *device = "default"; // 設備句柄 // 如下均定義兩個,根據前綴區分,c->capture,p->playback,沒有前綴的表示參數相同 snd_pcm_t *chandle; snd_pcm_t *phandle; // 硬件參數 snd_pcm_hw_params_t *cparams; snd_pcm_hw_params_t *pparams; // 數據訪問類型,讀寫方式:內存映射或者讀寫,數據 snd_pcm_access_t access_type = SND_PCM_ACCESS_RW_INTERLEAVED; // 格式, snd_pcm_format_t format = SND_PCM_FORMAT_S16_LE; // 碼率,採樣率,8000Hz,44100Hz unsigned int rate = 44100; // 通道數 unsigned int channels = 2; // 幀數,這裏取32 snd_pcm_uframes_t frames = 32; // 如下爲可選參數 unsigned int bytes_per_frame; // 軟件重採樣 unsigned int soft_resample;
打開設備
snd_pcm_open(&chandle, device, SND_PCM_STREAM_CAPTURE, 0); snd_pcm_open(&phandle, device, SND_PCM_STREAM_PLAYBACK, 0);
增長一個錯誤判斷
int err; if ((err = snd_pcm_open(&chandle, device, SND_PCM_STREAM_CAPTURE, 0)) < 0) { std::cout << "Capture device open failed."; } if ((err = snd_pcm_open(&phandle, device, SND_PCM_STREAM_PLAYBACK, 0)) < 0) { std::cout << "Playback device open failed."; }
設置參數,這裏就不增長錯誤判斷了,否則顯得有些長了
// 先計算每幀數據的大小 bytes_per_frame = snd_pcm_format_width(format) / 8 * 2; // 計算須要分配的緩存空間的大小 buffer_size = frames * bytes_per_frame; // 爲參數分配空間 snd_pcm_hw_params_alloca(¶ms); // 填充參數空間 snd_pcm_hw_params_any(handle, params); // 設置數據訪問方式 snd_pcm_hw_params_set_access(handle, params, access_type); // 設置格式 snd_pcm_hw_params_set_format(handle, params, format); // 設置通道 snd_pcm_hw_params_set_channels(handle, params, channels); // 設置採樣率 snd_pcm_hw_params_set_rate_near(handle, params, &rate, 0); // 可選項,不改不影響 // 設置緩存大小 buffer_size = period_size * 2; snd_pcm_hw_params_set_buffer_size_near(handle, params, &buffer_size); // 設置段大小,period與OSS中的segment相似 period_size = buffer_size / 2; snd_pcm_hw_params_set_period_size_near(handle, params, &period_size, 0)); //設置參數 snd_pcm_hw_params(handle, params);
讀寫數據
// 分配緩存空間,大小上面經過buffer_size計算出了 char *buffer = (char *)malloc(buffer_size); // 讀寫數據 snd_pcm_readi(chandle, buffer, frames); snd_pcm_writei(phandle, buffer, frames);
循環播放
while(1) { snd_pcm_readi(chandle, buffer, frames); snd_pcm_writei(phandle, buffer, frames); }
捕獲必定時間的音頻數據到文件流
ofstream output("test.pcm", ios::trunc); int loop_sec; int frames_readed; loop_sec = 10; unsigned long loop_limit; // 計算循環大小 loop_limit = loop_sec * rate; for (size_t i = 0; i < loop_limit; ) { // 這裏還須要判斷一下返回值是否爲負 frames_readed = snd_pcm_readi(chandle, buffer, frames); output.write(buffer, buffer_size); i += frames_readed; }
關閉設備、釋放指針
snd_pcm_close(chandle); snd_pcm_close(phandle); free(buffer);
放音過程當中也許會出現"Broken pipe"的錯誤,添加以下須要從新準備設備
err = snd_pcm_writei(handle, input_buffer, frames); if (err == -EPIPE) { snd_pcm_prepare(handle); continue; // 或者 // return 0; }
完整例子
alsa_audio.h1 #ifndef ALSA_AUDIO_H 2 #define ALSA_AUDIO_H 3 4 #include <QObject> 5 6 #include <alsa/asoundlib.h> 7 8 class ALSA_Audio : public QObject 9 { 10 Q_OBJECT 11 public: 12 explicit ALSA_Audio(QObject *parent = nullptr); 13 14 15 void capture_start(); 16 void capture_stop(); 17 /** 18 * @brief 讀取音頻數據 19 * @param buffer 音頻數據 20 * @param buffer_size 音頻數據大小 21 * @param frames 讀取的音頻幀數 22 * @return 0 成功,-1 失敗 23 */ 24 int audio_read(char **buffer, int *buffer_size, unsigned long *frames); 25 26 void playback_start(); 27 void playback_stop(); 28 /** 29 * @brief audio_write 播放音頻 30 * @param buffer 音頻數據 31 * @param frames 播放的音頻幀數 32 * @return 0 成功,-1 失敗 33 */ 34 int audio_write(char *buffer); 35 36 37 38 private: 39 bool m_is_capture_start; 40 snd_pcm_t *m_capture_pcm; 41 char *m_capture_buffer; 42 unsigned long m_capture_buffer_size; 43 snd_pcm_uframes_t m_capture_frames; // 一次讀的幀數 44 45 46 bool m_is_playback_start; 47 snd_pcm_t *m_playback_pcm; 48 snd_pcm_uframes_t m_playback_frames; // 一次寫的幀數 49 50 /** 51 * @brief ALSA_Audio::set_hw_params 52 * @param pcm 53 * @param hw_params 54 * @param rate 採樣頻率 55 * @param format 格式 56 * @param channels 通道數 57 * @param frames 一次讀寫的幀數 58 * @return 59 */ 60 int set_hw_params(snd_pcm_t *pcm, unsigned int rate, snd_pcm_format_t format, unsigned int channels, snd_pcm_uframes_t frames); 61 62 63 64 signals: 65 66 public slots: 67 }; 68 69 #endif // ALSA_AUDIO_Halsa_audio.cpp1 #include "alsa_audio.h" 2 #include "global.h" 3 4 #include <QDebug> 5 6 #include <math.h> 7 #include <inttypes.h> 8 9 10 11 ALSA_Audio::ALSA_Audio(QObject *parent) : QObject(parent) 12 { 13 m_is_capture_start = false; 14 m_is_playback_start = false; 15 } 16 17 18 19 int ALSA_Audio::set_hw_params(snd_pcm_t *pcm, unsigned int rate, snd_pcm_format_t format, unsigned int channels, snd_pcm_uframes_t frames) 20 { 21 snd_pcm_uframes_t period_size; // 一個處理週期須要的幀數 22 snd_pcm_uframes_t hw_buffer_size; // 硬件緩衝區大小 23 snd_pcm_hw_params_t *hw_params; 24 int ret; 25 int dir = 0; 26 27 28 29 // 初始化硬件參數結構體 30 snd_pcm_hw_params_malloc(&hw_params); 31 // 設置默認的硬件參數 32 snd_pcm_hw_params_any(pcm, hw_params); 33 34 // 如下爲設置所需的硬件參數 35 36 // 設置音頻數據記錄方式 37 CHECK_RETURN(snd_pcm_hw_params_set_access(pcm, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)); 38 // 格式。使用16位採樣大小,小端模式(SND_PCM_FORMAT_S16_LE) 39 CHECK_RETURN(snd_pcm_hw_params_set_format(pcm, hw_params, format)); 40 // 設置音頻通道數 41 CHECK_RETURN(snd_pcm_hw_params_set_channels(pcm, hw_params, channels)); 42 // 採樣頻率,一次採集爲一幀數據 43 //CHECK_RETURN(snd_pcm_hw_params_set_rate_near(pcm, hw_params, &rate, &dir)); // 設置相近的值 44 CHECK_RETURN(snd_pcm_hw_params_set_rate(pcm, hw_params, rate, dir)); 45 // 一個處理週期須要的幀數 46 period_size = frames * 5; 47 CHECK_RETURN(snd_pcm_hw_params_set_period_size_near(pcm, hw_params, &period_size, &dir)); // 設置相近的值 48 // // 硬件緩衝區大小, 單位:幀(frame) 49 // hw_buffer_size = period_size * 16; 50 // CHECK_RETURN(snd_pcm_hw_params_set_buffer_size_near(pcm, hw_params, &hw_buffer_size)); 51 52 // 將參數寫入pcm驅動 53 CHECK_RETURN(snd_pcm_hw_params(pcm, hw_params)); 54 55 snd_pcm_hw_params_free(hw_params); // 釋放再也不使用的hw_params空間 56 57 printf("one frames=%ldbytes\n", snd_pcm_frames_to_bytes(pcm, 1)); 58 unsigned int val; 59 snd_pcm_hw_params_get_channels(hw_params, &val); 60 printf("channels=%d\n", val); 61 62 if (ret < 0) { 63 printf("error: unable to set hw parameters: %s\n", snd_strerror(ret)); 64 return -1; 65 } 66 return 0; 67 } 68 69 70 void ALSA_Audio::capture_start() 71 { 72 m_capture_frames = 160; // 此處160爲固定值,發送接收均使用此值 73 unsigned int rate = 8000; // 採樣頻率 74 snd_pcm_format_t format = SND_PCM_FORMAT_S16_LE; // 使用16位採樣大小,小端模式 75 unsigned int channels = 1; // 通道數 76 int ret; 77 78 if(m_is_capture_start) 79 { 80 printf("error: alsa audio capture is started!\n"); 81 return; 82 } 83 84 ret = snd_pcm_open(&m_capture_pcm, "plughw:1,0", SND_PCM_STREAM_CAPTURE, 0); // 使用plughw:0,0 85 if(ret < 0) 86 { 87 printf("snd_pcm_open error: %s\n", snd_strerror(ret)); 88 return; 89 } 90 91 // 設置硬件參數 92 if(set_hw_params(m_capture_pcm, rate, format, channels, m_capture_frames) < 0) 93 { 94 return; 95 } 96 97 // 使用buffer保存一次處理獲得的數據 98 m_capture_buffer_size = m_capture_frames * static_cast<unsigned long>(snd_pcm_format_width(format) / 8 * static_cast<int>(channels)); 99 m_capture_buffer_size *= 5; // * 5 表示使用5倍的緩存空間 100 printf("snd_pcm_format_width(format):%d\n", snd_pcm_format_width(format)); 101 printf("m_capture_buffer_size:%ld\n", m_capture_buffer_size); 102 m_capture_buffer = static_cast<char *>(malloc(sizeof(char) * m_capture_buffer_size)); 103 memset(m_capture_buffer, 0, m_capture_buffer_size); 104 105 // 獲取一次處理所須要的時間,單位us 106 // 1/rate * frames * 10^6 = period_time, 即:採集一幀所需的時間 * 一次處理所需的幀數 * 10^6 = 一次處理所需的時間(單位us) 107 // snd_pcm_hw_params_get_period_time(m_capture_hw_params, &m_period_time, &dir); 108 109 m_is_capture_start = true; 110 } 111 112 void ALSA_Audio::capture_stop() 113 { 114 if(m_is_capture_start == false) 115 { 116 printf("error: alsa audio capture is not start!"); 117 return; 118 } 119 120 m_is_capture_start = false; 121 122 snd_pcm_drain(m_capture_pcm); 123 snd_pcm_close(m_capture_pcm); 124 free(m_capture_buffer); 125 } 126 127 int ALSA_Audio::audio_read(char **buffer, int *buffer_size, unsigned long *frames) 128 { 129 int ret; 130 if(m_is_capture_start == false) 131 { 132 printf("error: alsa audio capture is stopped!\n"); 133 return -1; 134 } 135 memset(m_capture_buffer, 0, m_capture_buffer_size); 136 ret = static_cast<int>(snd_pcm_readi(m_capture_pcm, m_capture_buffer, m_capture_frames)); 137 printf("strlen(m_capture_buffer)=%ld\n", strlen(m_capture_buffer)); 138 if (ret == -EPIPE) 139 { 140 /* EPIPE means overrun */ 141 printf("overrun occurred\n"); 142 snd_pcm_prepare(m_capture_pcm); 143 } 144 else if (ret < 0) 145 { 146 printf("error from read: %s\n", snd_strerror(ret)); 147 } 148 else if (ret != static_cast<int>(m_capture_frames)) 149 { 150 printf("short read, read %d frames\n", ret); 151 } 152 153 if(m_capture_buffer == nullptr) 154 { 155 printf("error: alsa audio capture_buffer is empty!\n"); 156 return -1; 157 } 158 *buffer = m_capture_buffer; 159 *buffer_size = static_cast<int>(m_capture_buffer_size / 5); 160 *frames = m_capture_frames; 161 162 return 0; 163 } 164 165 166 167 void ALSA_Audio::playback_start() 168 { 169 m_playback_frames = 160; // 此處160爲固定值,發送接收均使用此值 170 unsigned int rate = 8000; // 採樣頻率 171 snd_pcm_format_t format = SND_PCM_FORMAT_S16_LE; // 使用16位採樣大小,小端模式 172 unsigned int channels = 1; // 通道數 173 int ret; 174 175 176 if(m_is_playback_start) 177 { 178 printf("error: alsa audio playback is started!\n"); 179 return; 180 } 181 182 ret = snd_pcm_open(&m_playback_pcm, "plughw:1,0", SND_PCM_STREAM_PLAYBACK, 0); // 使用plughw:0,0 183 if(ret < 0) 184 { 185 printf("snd_pcm_open error: %s\n", snd_strerror(ret)); 186 return; 187 } 188 189 // 設置硬件參數 190 if(set_hw_params(m_playback_pcm, rate, format, channels, m_playback_frames) < 0) 191 { 192 return; 193 } 194 195 196 m_is_playback_start = true; 197 198 } 199 200 void ALSA_Audio::playback_stop() 201 { 202 if(m_is_playback_start == false) 203 { 204 printf("error: alsa audio playback is not start!"); 205 return; 206 } 207 208 m_is_playback_start = false; 209 210 snd_pcm_drain(m_playback_pcm); 211 snd_pcm_close(m_playback_pcm); 212 } 213 214 215 int ALSA_Audio::audio_write(char *buffer) 216 { 217 long ret; 218 if(m_is_playback_start == false) 219 { 220 printf("error: alsa audio playback is stopped!\n"); 221 return -1; 222 } 223 else 224 { 225 ret = snd_pcm_writei(m_playback_pcm, buffer, m_playback_frames); 226 if(ret == -EPIPE) 227 { 228 /* EPIPE means underrun */ 229 printf("underrun occurred\n"); 230 snd_pcm_prepare(m_playback_pcm); 231 } 232 else if (ret < 0) 233 { 234 printf("error from write: %s\n", snd_strerror(static_cast<int>(ret))); 235 } 236 else if (ret != static_cast<long>(m_playback_frames)) 237 { 238 printf("short write, write %ld frames\n", ret); 239 } 240 } 241 return 0; 242 }
硬件架構:
軟件架構:
注:
controlC0:控制接口,用於控制聲卡,如通道選擇,混音,麥克風輸入增益調節等。
midiC0D0:Raw迷笛接口,用於播放midi音頻。
pcmC0D0c:pcm接口,用於錄音的pcm設備。
pcmC0D0p:用於播放的pcm設備。
pcmC0D1p:
seq:音序器接口。
timer:定時器接口。
即該聲卡下掛載了7個設備。根據聲卡實際能力,驅動實際上能夠掛載更多種類的設備
其中
C0D0表示聲卡0中的設備0。
pcmC0D0c:最後的c表示capture。
pcmC0D0p:最後一個p表示playback。
設備種類 include/sound/core.h:
其中:
core:包含 ALSA 驅動的核心層代碼實現。
core/oss:包含模擬舊的OSS架構的PCM和Mixer模塊。
core/seq:音序器相關的代碼。
drivers:存放一些與CPU,bus架構無關的公用代碼。
i2c:ALSA的i2c控制代碼。
pci:PCI總線 聲卡的頂層目錄,其子目錄包含各類PCI聲卡代碼。
isa:ISA總線 聲卡的頂層目錄,其子目錄包含各類ISA聲卡代碼。
soc:ASoC(ALSA System on Chip)層實現代碼,針對嵌入式音頻設備。
soc/codecs:針對ASoC體系的各類音頻編碼器的驅動實現,與平臺無關。
include/sound:ALSA驅動的公共頭文件目錄。
OSS音頻設備驅動:
OSS 標準中有兩個最基本的音頻設備: mixer(混音器)和 dsp(數字信號處理器)。
ALSA音頻設備驅動:
雖然 OSS 已經很是成熟,但它畢竟是一個沒有徹底開放源代碼的商業產品,並且目前基本上在 Linux mainline 中失去了更新。而 ALSA (Advanced Linux Sound Architecture)剛好彌補了這一空白,它符合 GPL,是在 Linux 下進行音頻編程時另外一種可供選擇的聲卡驅動體系結構。 ALSA 除了像 OSS 那樣提供了一組內核驅動程序模塊以外,還專門爲簡化應用程序的編寫提供了相應的函數庫,與 OSS 提供的基於 ioctl 的原始編程接口相比, ALSA 函數庫使用起來要更加方便一些。 ALSA 的主要特色以下。支持多種聲卡設備。
模塊化的內核驅動程序。
支持 SMP 和多線程。
提供應用開發函數庫(alsa-lib)以簡化應用程序開發。
支持 OSS API,兼容 OSS 應用程序。
ASoC音頻設備驅動:
ASoC(ALSA System on Chip)是 ALSA 在 SoC 方面的發展和演變,它在本質上仍然屬於ALSA,可是在 ALSA 架構基礎上對 CPU 相關的代碼和 Codec 相關的代碼進行了分離。其緣由是,採用傳統 ALSA 架構的狀況下,同一型號的 Codec 工做於不一樣的 CPU 時,須要不一樣的驅動,這不符合代碼重用的要求。對於目前嵌入式系統上的聲卡驅動開發,咱們建議讀者儘可能採用 ASoC 框架, ASoC 主要由 3 部分組成。
Codec 驅動。這一部分只關心 Codec 自己,與 CPU 平臺相關的特性不禁此部分操做。
平臺驅動。這一部分只關心 CPU 自己,不關心 Codec。它主要處理兩個問題: DMA 引擎和 SoC 集成的 PCM、 I2S 或 AC ‘97 數字接口控制。
板驅動(也稱爲 machine 驅動)。這一部分將平臺驅動和 Codec 驅動綁定在一塊兒,描述了板一級的硬件特徵。
在以上 3 部分中, 1 和 2 基本均可以仍然是通用的驅動了,也就是說, Codec 驅動認爲本身能夠鏈接任意 CPU,而 CPU 的 I2S、 PCM 或 AC ‘97 接口對應的平臺驅動則認爲本身能夠鏈接任意符合其接口類型的 Codec,只有 3 是不通用的,由特定的電路板上具體的 CPU 和 Codec 肯定,所以它很像一個插座,上面插上了 Codec 和平臺這兩個插頭。在以上三部分之上的是 ASoC 核心層,由內核源代碼中的 sound/soc/soc-core.c 實現,查看其源代碼發現它徹底是一個傳統的 ALSA 驅動。所以,對於基於 ASoC 架構的聲卡驅動而言, alsa-lib以及 ALSA 的一系列 utility 仍然是可用的,如 amixer、 aplay 均無需針對 ASoC 進行任何改動。而ASoC 的用戶編程方法也與 ALSA 徹底一致。內核源代碼的 Documentation/sound/alsa/soc/目錄包含了 ASoC 相關的文檔。
目前linux中主流的音頻體系結構是ALSA(Advanced Linux Sound Architecture),ALSA在內核驅動層提供了alsa-driver,在應用層提供了alsa-lib,應用程序只須要調用alsa-lib(libtinyalsa.so)提供的API就能夠完
成對底層硬件的操做。說的這麼好,可是Android中沒有使用標準的ALSA,而是一個ALSA的簡化版叫作tinyalsa。Android中使用tinyalsa控制管理全部模式的音頻通路,咱們也可使用tinyalsa提供的工具進行查看、
調試。
tinycap.c 實現錄音相關代碼 tinycap
Tinyplay.c 實現放音相關代碼 tinyplay
Pcm.c 與驅動層alsa-driver調用接口,爲audio_hw提供api接口
Tinymix 查看和設置混音器 tinymix
Tinypcminfo.c 查看聲卡信息tinypcminfo
音頻跟視頻很不同,視頻每一幀就是一張圖像,而從上面的正玄波能夠看出,音頻數據是流式的,自己沒有明確的一幀幀的概念,在實際的應用中,爲了音頻算法處理/傳輸的方便,通常約定俗成取2.5ms~60ms爲單位的數據量爲一幀音頻。
這個時間被稱之爲「採樣時間」,其長度沒有特別的標準,它是根據編解碼器和具體應用的需求來決定的,咱們能夠計算一下一幀音頻幀的大小:
假設某音頻信號是採樣率爲8kHz、雙通道、位寬爲16bit,20ms一幀,則一幀音頻數據的大小爲:
int size = 8000 x 2 x 16bit x 0.02s = 5120bit = 640 byte
音頻幀總結
period(週期):硬件中斷間的間隔時間。它表示輸入延時。
聲卡接口中有一個指針來指示聲卡硬件緩存區中當前的讀寫位置。只要接口在運行,這個指針將循環地指向緩存區中的某個位置。
frame size =sizeof(one sample) * nChannels
alsa中配置的緩存(buffer)和週期(size)大小在runtime中是以幀(frames)形式存儲的。
period_bytes =pcm_format_to_bits 用來計算一個幀有多少bits,實際應用的時候常常用到
簡單流程:
MIC採集天然聲轉成模擬電信號,經過運算放大電路放大信號幅度,而後用ADC轉換爲數字信號,(能夠進行音頻的編碼工做,好比編成mp3),(而後進行音頻解碼工做),(經過DAC轉換爲模擬信號)(或者脈衝寬度調製PWM用來對模擬信號的電平進行數字編碼),經過功率放大器放大後輸出給喇叭
看什麼方案了,若是涉及比較複雜的運算,MCU的算力是遠遠不夠的,必須上嵌入式硬件了,這就涉及到系統層面的開發。若是隻是簡單的音頻處理沒事(好比MP3節奏彩燈錄放等等)
其餘方案:
1 利用語言集成芯片如:ISD2560,ISD2560採用多電平直接模擬量存儲技術,能夠很是真實、天然的再現語音、音樂、音調和效果聲,錄音時間爲60s,可重複錄放10萬次。
2 PWM+SPI PWM模擬時鐘時序,SPI傳輸數據,採用PCM編碼方式,而後接放大器+喇叭;
(軟件編寫很簡單,只把wave文件的採樣值往pwm裏面丟就能夠了。固然,pwm信號通常須要加濾波電路才能送往功放、喇叭。通常採用16kbps的採樣率,濾波電路會簡單。)
3 DAC DAC+放大器+喇叭,通常語音芯片都是用這種方式作的,可是應該是專用的DAC語音芯片;
4 IIS+語音解碼芯片
這些總線協議什麼I2C SPI等都是用來接外圍集成電路的
其實所謂的音頻編碼器、解碼器。實際上就是普通的AD或DA後,再由運算芯片進行算法壓縮或解壓來的
編碼方案:
波形編碼的話音質量高,但編碼速率也很高(WAV);
參數編碼的編碼速率很低,產生的合成語音的音質不高(MP3);
混合編碼使用參數編碼技術和波形編碼技術,編碼速率和音質介於它們之間。
方案名詞介紹:
波形編碼PCM
波形編碼是基於對語音信號波形的數字化處理,試圖使處理後重建的語音信號波形與原語音信號波形保持一致。波形編碼的優勢是實現簡單、語音質量較好、適應性強等;缺點是話音信號的壓縮程度不是很高,實現的碼速率比較高。常見的波形壓縮編碼方法有脈衝編碼調製(PCM)
參數編碼MP3
MP3文件實際上是一種通過MP3(即動態影像專家壓縮標準音頻層面)編碼算法壓縮的數據,不能直接送給功放,必須先經過解碼還原出原始音頻數據再進行播放。
PWM原理
脈寬調製(PWM)基本原理:控制方式就是對逆變電路開關器件的通斷進行控制,使輸出端獲得一系列幅值相等的脈衝,用這些脈衝來代替正弦波或所須要的波形。也就是在輸出波形的半個週期中產生多個脈衝,使各脈衝的等值電壓爲正弦波形,所得到的輸出平滑且低次諧波少。按必定的規則對各脈衝的寬度進行調製,便可改變逆變電路輸出電壓的大小,也可改變輸出頻率。 例如,把正弦半波波形分紅N等份,就可把正弦半波當作由N個彼此相連的脈衝所組成的波形。這些脈衝寬度相等,都等於 π/n ,但幅值不等,且脈衝頂部不是水平直線,而是曲線,各脈衝的幅值按正弦規律變化。若是把上述脈衝序列用一樣數量的等幅而不等寬的矩形脈衝序列代替,使矩形脈衝的中點和相應正弦等分的中點重合,且使矩形脈衝和相應正弦部分面積(即衝量)相等,就獲得一組脈衝序列,這就是PWM波形。能夠看出,各脈衝寬度是按正弦規律變化的。根據衝量相等效果相同的原理,PWM波形和正弦半波是等效的。對於正弦的負半周,也能夠用一樣的方法獲得PWM波形。在PWM波形中,各脈衝的幅值是相等的,要改變等效輸出正弦波的幅值時,只要按同一比例係數改變各脈衝的寬度便可,所以在交-直-交變頻器中,PWM逆變電路輸出的脈衝電壓就是直流側電壓的幅值。
MCU裸板開發
1 #include <reg52.h> 2 #include <intrins.h> 3 #define uchar unsigned char 4 #define uint unsigned int 5 //錄音和放音鍵IO口定義: 6 sbit AN=P2^6;//放音鍵控制接口 7 sbit set_key=P2^7;//錄音鍵控制口 8 // ISD4004控制口定義: 9 sbit SS =P1^0; //4004片選 10 sbit MOSI=P1^1; //4004數據輸入 11 sbit MISO=P1^2; //4004數據輸出 12 sbit SCLK=P1^3; //ISD4004時鐘 13 sbit INT =P1^4; //4004中斷 14 sbit STOP=P3^4; //4004復位 15 sbit LED1 =P1^6; //錄音指示燈 16 //===============================LCD1602接口定義===================== 17 /*----------------------------------------------------- 18 |DB0-----P2.0 | DB4-----P2.4 | RW-------P0.1 | 19 |DB1-----P2.1 | DB5-----P2.5 | RS-------P0.2 | 20 |DB2-----P2.2 | DB6-----P2.6 | E--------P0.0 | 21 |DB3-----P2.3 | DB7-----P2.7 | 注意,P0.0到P0.2須要接上拉電阻 22 --------------------------------------------------- 23 =============================================================*/ 24 #define LCM_Data P0 //LCD1602數據接口 25 sbit LCM_RW = P2^3; //讀寫控制輸入端,LCD1602的第五腳 26 sbit LCM_RS = P2^4; //寄存器選擇輸入端,LCD1602的第四腳 27 sbit LCM_E = P2^2; //使能信號輸入端,LCD1602的第6腳 28 //***************函數聲明************************************************ 29 void WriteDataLCM(uchar WDLCM);//LCD模塊寫數據 30 void WriteCommandLCM(uchar WCLCM,BuysC); //LCD模塊寫指令 31 uchar ReadStatusLCM(void);//讀LCD模塊的忙標 32 void DisplayOneChar(uchar X,uchar Y,uchar ASCII);//在第X+1行的第Y+1位置顯示一個字符 33 void LCMInit(void); 34 void DelayUs(uint us); //微妙延時程序 35 void DelayMs(uint Ms);//毫秒延時程序 36 void init_t0();//定時器0初始化函數 37 void setkey_treat(void);//錄音鍵處理程序 38 void upkey_treat(void);//播放鍵處理程序 39 void display();//顯示處理程序 40 void isd_setrec(uchar adl,uchar adh);//發送setrec指令 41 void isd_rec();//發送rec指令 42 void isd_stop();//stop指令(中止當前操做) 43 void isd_powerup();//發送上電指令 44 void isd_stopwrdn();//發送掉電指令 45 void isd_send(uchar isdx);//spi串行發送子程序,8位數據 46 void isd_setplay(uchar adl,uchar adh); 47 void isd_play(); 48 //程序中的一些常量定義 49 uint time_total,st_add,end_add=0; 50 uint adds[25];//25段語音的起始地址暫存 51 uint adde[25];//25段語音的結束地址暫時 52 uchar t0_crycle,count,count_flag,flag2,flag3,flag4; 53 uchar second_count=170,msecond_count=0; 54 //second_count爲芯片錄音的起始地址,起始地址原本是A0,也就是160, 55 //咱們從170開始錄音吧。 56 #define Busy 0x80 //用於檢測LCM狀態字中的Busy標識 57 58 /*=========================================================================== 59 主程序 60 =============================================================================*/ 61 void main(void) 62 { 63 LED1=0;//滅錄音指示燈 64 flag3=0; 65 flag4=0; 66 time_total=340;//錄音地址從170開始,對應的單片機開始計時的時間就是340*0.1秒 67 adds[0]=170; 68 count=0; 69 LCMInit(); //1602初始化 70 init_t0();//定時器初始化 71 DisplayOneChar( 0,5,'I'); //開機時顯示000 ISD4004-X 72 DisplayOneChar( 0,6,'S'); 73 DisplayOneChar( 0,7,'D'); 74 DisplayOneChar( 0,8,'4'); 75 DisplayOneChar( 0,9,'0'); 76 DisplayOneChar( 0,10,'0'); 77 DisplayOneChar( 0,11,'4'); 78 DisplayOneChar( 0,12,'-'); 79 DisplayOneChar( 0,13,'X'); 80 while(1) 81 { 82 display();//顯示處理 83 upkey_treat();//放音鍵處理 84 setkey_treat();//錄音鍵處理 85 } 86 } 87 //******************************************* 88 //錄音鍵處理程序 89 //從指定地址開始錄音的程序就是在這段裏面 90 void setkey_treat(void) 91 { 92 set_key=1;//置IO口爲1,準備讀入數據 93 DelayUs(1); 94 if(set_key==0) 95 { 96 if(flag3==0)//錄音鍵和放音鍵互鎖,錄音好後,禁止再次錄音。若是要再次錄音,那就要復位單片機,從新開始錄音 97 { 98 if(count==0)//判斷是否爲上電或復位以來第一次按錄音鍵 99 { 100 st_add=170; 101 } 102 else 103 { 104 st_add=end_add+3; 105 }//每段語言間隔3個地址 106 adds[count]=st_add;//每段語音的起始地址暫時 107 if(count>=25)//判斷語音段數時候超過25段,由於單片機內存的關係? 108 //本程序只錄音25段,若是要錄更多的語音,改成不可查詢的便可 109 {//若是超過25段,則覆蓋以前的語音,重新開始錄音 110 count=0; 111 st_add=170; 112 time_total=340; 113 } 114 isd_powerup(); //AN鍵按下,ISD上電並延遲50ms 115 isd_stopwrdn(); 116 isd_powerup(); 117 LED1=1;//錄音指示燈亮,表示錄音模式 118 isd_setrec(st_add&0x00ff,st_add>>8); //從指定的地址 119 if(INT==1)// 斷定芯片有沒有溢出 120 { 121 isd_rec(); //發送錄音指令 122 } 123 time_total=st_add*2;//計時初始值計算 124 TR0=1;//開計時器 125 while(set_key==0);//等待本次錄音結束 126 TR0=0;//錄音結束後中止計時 127 isd_stop(); //發送4004中止命令 128 end_add=time_total/2+2;//計算語音的結束地址 129 adde[count]=end_add;//本段語音結束地址暫存 130 LED1=0; //錄音完畢,LED熄滅 131 count++;//錄音段數自加 132 count_flag=count;//錄音段數寄存 133 flag2=1; 134 flag4=1;//解鎖放音鍵 135 } 136 } 137 } 138 //================================================= 139 //放音機處理程序 140 //從指定地址開始放本段語音就是這段程序 141 void upkey_treat(void) 142 { 143 uchar ovflog; 144 AN=1;//準備讀入數據 145 DelayUs(1); 146 if(AN==0)//判斷放音鍵是否動做 147 { 148 // if(flag4==1)//互鎖錄音鍵 149 // { 150 if(flag2==1)//判斷是否爲錄音好後的第一次放音 151 { 152 count=0;//從第0段開始播放 153 } 154 isd_powerup(); //AN鍵按下,ISD上電並延遲50ms 155 isd_stopwrdn(); 156 isd_powerup(); 157 //170 184 196 211 158 // st_add=adds[count];//送當前語音的起始地址 159 st_add=211;//送當前語音的起始地址 160 isd_setplay(st_add&0x00ff,st_add>>8); //發送setplay指令,從指定地址開始放音 161 isd_play(); //發送放音指令 162 DelayUs(20); 163 while(INT==1); //等待放音完畢的EOM中斷信號 164 isd_stop(); //放音完畢,發送stop指令 165 while(AN==0); // 166 isd_stop(); 167 count++;//語音段數自加 168 flag2=0; 169 flag3=1; 170 if(count>=count_flag)//若是播放到最後一段後還按加鍵,則從第一段從新播放 171 { 172 count=0; 173 } 174 175 // } 176 } 177 } 178 //************************************************? 179 //發送rec指令 180 void isd_rec() 181 { 182 isd_send(0xb0); 183 SS=1; 184 } 185 //**************************************** 186 //發送setrec指令 187 void isd_setrec(unsigned char adl,unsigned char adh) 188 { 189 DelayMs(1); 190 isd_send(adl); //發送放音起始地址低位 191 DelayUs(2); 192 isd_send(adh); //發送放音起始地址高位 193 DelayUs(2); 194 isd_send(0xa0); //發送setplay指令字節 195 SS=1; 196 } 197 //============================================================================= 198 //********************************************** 199 //定時器0中斷程序 200 void timer0() interrupt 1 201 { 202 TH0=(65536-50000)/256; 203 TL0=(65536-50000)%256; 204 t0_crycle++; 205 if(t0_crycle==2)// 0.1秒 206 { 207 t0_crycle=0; 208 time_total++; 209 msecond_count++; 210 if(msecond_count==10)//1秒 211 { 212 msecond_count=0; 213 second_count++; 214 if(second_count==60) 215 { 216 second_count=0; 217 } 218 } 219 if(time_total==4800)time_total=0; 220 } 221 } 222 //******************************************************************************************** 223 //定時器0初始化函數 224 void init_t0() 225 { 226 TMOD=0x01;//設定定時器工做方式1,定時器定時50毫秒 227 TH0=(65536-50000)/256; 228 TL0=(65536-50000)%256; 229 EA=1;//開總中斷 230 ET0=1;//容許定時器0中斷 231 t0_crycle=0;//定時器中斷次數計數單元 232 } 233 //****************************************** 234 //顯示處理程序 235 void display() 236 { 237 uchar x; 238 if(flag3==1||flag4==1)//判斷是否有錄音過或者放音過 239 { 240 x=count-1; 241 if(x==255){x=count_flag-1;} 242 } 243 DisplayOneChar( 0,0,x/100+0x30); //顯示當前語音是第幾段 244 DisplayOneChar( 0,1,x/10%10+0x30); 245 DisplayOneChar( 0,2,x%10+0x30); 246 if(flag3==0)//錄音時顯示本段語音的起始和結束地址 247 { 248 DisplayOneChar( 1,0,st_add/1000+0x30);//計算並顯示千位 249 DisplayOneChar( 1,1,st_add/100%10+0x30); 250 DisplayOneChar( 1,2,st_add/10%10+0x30); 251 DisplayOneChar( 1,3,st_add%10+0x30); 252 DisplayOneChar( 1,4,'-'); 253 DisplayOneChar( 1,5,'-'); 254 DisplayOneChar( 1,6,end_add/1000+0x30); 255 DisplayOneChar( 1,7,end_add/100%10+0x30); 256 DisplayOneChar( 1,8,end_add/10%10+0x30); 257 DisplayOneChar( 1,9,end_add%10+0x30); 258 } 259 if(flag4==1)//放音時顯示本段語音的起始和結束地址 260 { 261 DisplayOneChar( 1,0,adds[x]/1000+0x30); 262 DisplayOneChar( 1,1,adds[x]/100%10+0x30); 263 DisplayOneChar( 1,2,adds[x]/10%10+0x30); 264 DisplayOneChar( 1,3,adds[x]%10+0x30); 265 DisplayOneChar( 1,4,'-'); 266 DisplayOneChar( 1,5,'-'); 267 DisplayOneChar( 1,6,adde[x]/1000+0x30); 268 DisplayOneChar( 1,7,adde[x]/100%10+0x30); 269 DisplayOneChar( 1,8,adde[x]/10%10+0x30); 270 DisplayOneChar( 1,9,adde[x]%10+0x30); 271 } 272 } 273 //====================================================================== 274 // LCM初始化 275 //====================================================================== 276 void LCMInit(void) 277 { 278 LCM_Data = 0; 279 WriteCommandLCM(0x38,0); //三次顯示模式設置,不檢測忙信號 280 DelayMs(5); 281 WriteCommandLCM(0x38,0); 282 DelayMs(5); 283 WriteCommandLCM(0x38,0); 284 DelayMs(5); 285 WriteCommandLCM(0x38,1); //顯示模式設置,開始要求每次檢測忙信號 286 WriteCommandLCM(0x08,1); //關閉顯示 287 WriteCommandLCM(0x01,1); //顯示清屏 288 WriteCommandLCM(0x06,1); // 顯示光標移動設置 289 WriteCommandLCM(0x0C,1); // 顯示開及光標設置 290 DelayMs(100); 291 } 292 //*===================================================================== 293 // 寫數據函數: E =高脈衝 RS=1 RW=0 294 //====================================================================== 295 void WriteDataLCM(uchar WDLCM) 296 { 297 ReadStatusLCM(); //檢測忙 298 LCM_Data = WDLCM; 299 LCM_RS = 1; 300 LCM_RW = 0; 301 LCM_E = 0; //若晶振速度過高能夠在這後加小的延時 302 LCM_E = 0; //延時 303 LCM_E = 1; 304 } 305 //*==================================================================== 306 // 寫指令函數: E=高脈衝 RS=0 RW=0 307 //====================================================================== 308 void WriteCommandLCM(unsigned char WCLCM,BuysC) //BuysC爲0時忽略忙檢測 309 { 310 if (BuysC) ReadStatusLCM(); //根據須要檢測忙 311 LCM_Data = WCLCM; 312 LCM_RS = 0; 313 LCM_RW = 0; 314 LCM_E = 0; 315 LCM_E = 0; 316 LCM_E = 1; 317 } 318 //*==================================================================== 319 // 正常讀寫操做以前必須檢測LCD控制器狀態:E=1 RS=0 RW=1; 320 // DB7: 0 LCD控制器空閒,1 LCD控制器忙。 321 // 讀狀態 322 //====================================================================== 323 unsigned char ReadStatusLCM(void) 324 { 325 LCM_Data = 0xFF; 326 LCM_RS = 0; 327 LCM_RW = 1; 328 LCM_E = 0; 329 LCM_E = 0; 330 LCM_E = 1; 331 while (LCM_Data & Busy); //檢測忙信號 332 return(LCM_Data); 333 } 334 //====================================================================== 335 //功 能: 在1602 指定位置顯示一個字符:第一行位置0~15,第二行16~31 336 //說 明: 第 X 行,第 y 列 注意:字符串不能長於16個字符 337 //====================================================================== 338 void DisplayOneChar( unsigned char X, unsigned char Y, unsigned char ASCII) 339 { 340 X &= 0x1; 341 Y &= 0xF; //限制Y不能大於15,X不能大於1 342 if (X) Y |= 0x40; //當要顯示第二行時地址碼+0x40; 343 Y |= 0x80; // 算出指令碼 344 WriteCommandLCM(Y, 0); //這裏不檢測忙信號,發送地址碼 345 WriteDataLCM(ASCII); 346 } 347 //====================================================================== 348 //spi串行發送子程序,8位數據 349 void isd_send(uchar isdx) 350 { 351 uchar isx_counter; 352 SS=0;//ss=0,打開spi通訊端 353 SCLK=0; 354 for(isx_counter=0;isx_counter<8;isx_counter++)//先發低位再發高位,依次發送。 355 { 356 if((isdx&0x01)==1) 357 MOSI=1; 358 else 359 MOSI=0; 360 isdx=isdx>>1; 361 SCLK=1; 362 DelayUs(2); 363 SCLK=0; 364 DelayUs(2); 365 } 366 } 367 //====================================================================== 368 //stop指令(中止當前操做) 369 void isd_stop()// 370 { 371 DelayUs(10); 372 isd_send(0x30); 373 SS=1; 374 DelayMs(50); 375 } 376 //====================================================================== 377 //發送上電指令 378 void isd_powerup()// 379 { 380 DelayUs(10); 381 SS=0; 382 isd_send(0x20); 383 SS=1; 384 DelayMs(50); 385 } 386 //====================================================================== 387 //發送掉電指令 388 void isd_stopwrdn()// 389 { 390 DelayUs(10); 391 isd_send(0x10); 392 SS=1; 393 DelayMs(50); 394 } 395 396 void isd_play()//發送play指令 397 { 398 isd_send(0xf0); 399 SS=1; 400 } 401 void isd_setplay(uchar adl,uchar adh)//發送setplay指令 402 { 403 DelayMs(1); 404 isd_send(adl); //發送放音起始地址低位 405 DelayUs(2); 406 isd_send(adh); //發送放音起始地址高位 407 DelayUs(2); 408 isd_send(0xe0); //發送setplay指令字節 409 SS=1; 410 } 411 void DelayUs(uint us) 412 { 413 while(us--); 414 } 415 //==================================================================== 416 // 設定延時時間:x*1ms 417 //==================================================================== 418 void DelayMs(uint Ms) 419 { 420 uint i,TempCyc; 421 for(i=0;i<Ms;i++) 422 { 423 TempCyc = 250; 424 while(TempCyc--); 425 } 426 } 427