語音通信方案
系統級方案和自建協議
參考:https://www.cnblogs.com/gaoyaguo/p/7858025.html
windows平台、linux平台、嵌入式linux平台、mcu平台
1,在嵌入式Linux上開發的有線通信語音解決方案
這方案是在嵌入式Linux上開發的,音頻方案基於ALSA,語音通信相關的都是在user space 做,算是一個上層的解決方案。由於是有線通信,網絡環境相對無線通信而言不是特別惡劣,用的丟包補償措施也不是很多,主要有PLC、RFC2198等。
2,在Android手機上開發的傳統無線通信語音解決方案
這方案是在Android手機上開發的,是手機上的傳統語音通信方案(相對於APP語音通信而言)。Android是基於Linux的,所以也會用到ALSA,但是主要是做控制用,如對codec芯片的配置等。跟音頻數據相關的驅動、編解碼、前后處理等在Audio DSP上開發,網絡側相關的在CP(通信處理器)上開發,算是一個底層解決方案。該方案的軟件框圖如下:
系統級
聲卡 (Sound Card)也叫音頻卡(港台稱之為聲效卡),是計算機多媒體系統中最基本的組成部分,是實現聲波/數字信號相互轉換的一種硬件。聲卡的基本功能是把來自話筒、磁帶、光盤的原始聲音信號加以轉換,輸出到耳機、揚聲器、擴音機、錄音機等聲響設備,或通過音樂設備數字接口(MIDI)發出合成樂器的聲音
所有的電腦主板基本都有集成聲卡的,如果有專業要求會再買個獨立聲卡,就像專業玩家一樣買個獨立顯卡,手動狗頭
聲卡驅動
對於音頻處理的技術,主要有如下幾種:
- 采集麥克風輸入
- 采集聲卡輸出
- 將音頻數據送入聲卡進行播放
- 對多路音頻輸入進行混音處理
Windows平台內核提供調用聲卡API
一、MME(MultiMedia Extensions)
MME就是winmm.dll提供的接口,也是Windows平台下第一代API。優點是使用簡單,一般場景下可以滿足業務需求,缺點是延遲高,某些高級功能無法實現。
二、XAudio2
也是DirextX的一部分,為了取代DirectSound。DirextX套件中的音頻組件,大多用於游戲中,支持硬件加速,所以比MME有更低的延遲。
Vista系統開始引入的新架構,它是以COM的方式提供的接口,用戶模式下處於最底層,上面提到的幾種API最終都將使用它!功能最強,性能最好,但是接口繁雜,使用起來很麻煩。
四、Wasapi 就可以了(高性能,但更復雜)
而Wave系列的API函數主要是用來實現對麥克風輸入的采集(使用WaveIn系列API函數)和控制聲音的播放(使用后WaveOut系列函數)。
1.使用WaveIn系列API函數實現麥克風輸入采集
涉及的API函數:
waveInOpen
開啟音頻采集設備,成功后會返回設備句柄,后續的API都需要使用該句柄
調用模塊需要提供一個回調函數(waveInProc),以接收采集的音頻數據
waveInClose
關閉音頻采集模塊
成功后,由waveInOpen返回的設備句柄將不再有效
waveInPrepareHeader
准備音頻采集數據緩存的空間
waveInUnprepareHeader
清空音頻采集的數據緩存
waveInAddBuffer
將准備好的音頻數據緩存提供給音頻采集設備
在調用該API之前需要先調用waveInPrepareHeader
waveInStart
控制音頻采集設備開始對音頻數據的采集
waveInStop
控制音頻采集設備停止對音頻數據的采集
音頻采集設備采集到音頻數據后,會調用在waveInOpen中設置的回調函數。
其中參數包括一個消息類型,根據其消息類型就可以進行相應的操作。
如接收到WIM_DATA消息,則說明有新的音頻數據被采集到,這樣就可以根據需要來對這些音頻數據進行處理。
(示例以后補上)
2.使用Core Audio實現對聲卡輸出的捕捉
涉及的接口有:
IMMDeviceEnumerator
IMMDevice
IAudioClient
IAudioCaptureClient
主要過程:
創建多媒體設備枚舉器(IMMDeviceEnumerator)
通過多媒體設備枚舉器獲取聲卡接口(IMMDevice)
通過聲卡接口獲取聲卡客戶端接口(IAudioClient)
通過聲卡客戶端接口(IAudioClient)可獲取聲卡輸出的音頻參數、初始化聲卡、獲取聲卡輸出緩沖區的大小、開啟/停止對聲卡輸出的采集
通過聲卡采集客戶端接口(IAudioCaptureClient)可獲取采集的聲卡輸出數據,並對內部緩沖區進行控制
(示例以后補上)
3.常用的混音算法
混音算法就是將多路音頻輸入信號根據某種規則進行運算(多路音頻信號相加后做限幅處理),得到一路混合后的音頻,並以此作為輸出的過程。
我目前還做過這一塊,搜索了一下基本有如下幾種混音算法:
將多路音頻輸入信號直接相加取和作為輸出
將多路音頻輸入信號直接相加取和后,再除以混音通道數,防止溢出
將多路音頻輸入信號直接相加取和后,做Clip操作(將數據限定在最大值和最小值之間),如有溢出就設最大值
將多路音頻輸入信號直接相加取和后,做飽和處理,接近最大值時進行扭曲
將多路音頻輸入信號直接相加取和后,做歸一化處理,全部乘個系數,使幅值歸一化
將多路音頻輸入信號直接相加取和后,使用衰減因子限制幅值
Linux平台內核提供調用聲卡API
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的進一步封裝,他針對嵌入式設備提供了一些列增強的功能。
1.操作說明
安裝
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_H
alsa_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 }
2.架構圖
硬件架構:
軟件架構:
3.初識alsa設備
注:
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:

4.linux內核中音頻驅動代碼分布
其中:
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驅動的公共頭文件目錄。
5.驅動分類
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 相關的文檔。
Android平台內核提供調用聲卡API
目前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編碼、WMA編碼、ADPCM編碼、LPC編碼、MP3編碼、AAC編碼、CELP編碼等,包括優缺點對比和主要應用領域。
PCM編碼(原始數字音頻信號流)
類型:Audio
制定者:ITU-T
所需頻寬:1411.2 Kbps
特性:音源信息完整,但冗余度過大
優點:音源信息保存完整,音質好
缺點:信息量大,體積大,冗余度過大
應用領域:voip
版稅方式:Free
備注:在計算機應用中,能夠達到最高保真水平的就是PCM編碼,被廣泛用於素材保存及音樂欣賞,CD、DVD以及我們常見的WAV文件中均有應用。因此,PCM約定俗成了無損編碼,因為PCM代表了數字音頻中最佳的保真水准,並不意味着PCM就能夠確保信號絕對保真,PCM也只能做到最大程度的無限接近。要算一個PCM音頻流的碼率是一件很輕松的事情,采樣率值×采樣大小值×聲道數bps。一個采樣率為44.1KHz,采樣大小為16bit,雙聲道的PCM編碼的WAV文件,它的數據速率則為 44.1K×16×2 =1411.2Kbps。我們常見的Audio CD就采用了PCM編碼,一張光盤的容量只能容納72分鍾的音樂信息。WMA(Windows Media Audio)
類型:Audio
制定者:微軟公司
所需頻寬:320~112kbps(壓縮10~12倍)
特性:當Bitrate小於128K時,WMA幾乎在同級別的所有有損編碼格式中表現得最出色,但似乎128k是WMA一個檻,當Bitrate再往上提升時,不會有太多的音質改變。
優點:當Bitrate小於128K時,WMA最為出色且編碼后得到的音頻文件很小。
缺點:當Bitrate大於128K時,WMA音質損失過大。WMA標准不開放,由微軟掌握。
應用領域:voip
版稅方式:按個收取
備注:WMA的全稱是Windows Media Audio,它是微軟公司推出的與MP3格式齊名的一種新的音頻格式。由於WMA在壓縮比和音質方面都超過了MP3,更是遠勝於RA(Real Audio),即使在較低的采樣頻率下也能產生較好的音質,再加上WMA有微軟的Windows Media Player做其強大的后盾,所以一經推出就贏得一片喝彩。ADPCM( 自適應差分PCM)
類型:Audio
制定者:ITU-T
所需頻寬:32Kbps
特性:ADPCM(adaptive difference pulse code modulation)綜合了APCM的自適應特性和DPCM系統的差分特性,是一種性能比較好的波形編碼。
它的核心想法是:
①利用自適應的思想改變量化階的大小,即使用小的量化階(step-size)去編碼小的差值,使用大的量化階去編碼大的差值;
②使用過去的樣本值估算下一個輸入樣本的預測值,使實際樣本值和預測值之間的差值總是最小。
優點:算法復雜度低,壓縮比小(CD音質>400kbps),編解碼延時最短(相對其它技術)
缺點:聲音質量一般
應用領域:voip
版稅方式:Free
備注:ADPCM (ADPCM Adaptive Differential Pulse Code Modulation), 是一種針對16bit (或者更高?) 聲音波形數據的一種有損壓縮算法, 它將聲音流中每次采樣的 16bit 數據以 4bit 存儲, 所以壓縮比 1:4而壓縮/解壓縮算法非常的簡單, 所以是一種低空間消耗,高質量聲音獲得的好途徑。LPC(Linear Predictive Coding,線性預測編碼)
類型:Audio
制定者:
所需頻寬:2Kbps-4.8Kbps
特性:壓縮比大,計算量大,音質不高,廉價
優點:壓縮比大,廉價
缺點:計算量大,語音質量不是很好,自然度較低
應用領域:voip
版稅方式:Free
備注:參數編碼又稱為聲源編碼,是將信源信號在頻率域或其它正交變換域提取特征參數,並將其變換成數字代碼進行傳輸。譯碼為其反過程,將收到的數字序列經變換恢復特征參量,再根據特征參量重建語音信號。具體說,參數編碼是通過對語音信號特征參數的提取和編碼,力圖使重建語音信號具有盡可能高的准確性,但重建信號的波形同原語音信號的波形可能會有相當大的差別。如:線性預測編碼(LPC)及其它各種改進型都屬於參數編碼。該編碼比特率可壓縮到2Kbit/s-4.8Kbit/s,甚至更低,但語音質量只能達到中等,特別是自然度較低。CELP(Code Excited Linear Prediction碼激勵線性預測編碼)
類型:Audio
制定者:歐洲通信標准協會(ETSI)
所需頻寬:4~16Kbps的速率
特性:改善語音的質量:
① 對誤差信號進行感覺加權,利用人類聽覺的掩蔽特性來提高語音的主觀質量;
②用分數延遲改進基音預測,使濁音的表達更為准確,尤其改善了女性語音的質量;
③ 使用修正的MSPE准則來尋找 “最佳”的延遲,使得基音周期延遲的外形更為平滑;
④根據長時預測的效率,調整隨機激勵矢量的大小,提高語音的主觀質量;
⑤ 使用基於信道錯誤率估計的自適應平滑器,在信道誤碼率較高的情況下也能合成自然度較高的語音。
結論:
① CELP算法在低速率編碼環境下可以得到令人滿意的壓縮效果;
②使用快速算法,可以有效地降低CELP算法的復雜度,使它完全可以實時地實現;
③CELP可以成功地對各種不同類型的語音信號進行編碼,這種適應性對於真實環境,尤其是背景噪聲存在時更為重要。
優點:用很低的帶寬提供了較清晰的語音
缺點:-
應用領域:voip
版稅方式:Free
備注:1999年歐洲通信標准協會(ETSI)推出了基於碼激勵線性預測編碼(CELP)的第三代移動通信語音編碼標准自適應多速率語音編碼器(AMR),其中最低速率為4.75kb/s,達到通信質量。CELP碼激勵線性預測編碼是Code Excited Linear Prediction的縮寫。CELP是近10年來最成功的語音編碼算法。CELP語音編碼算法用線性預測提取聲道參數,用一個包含許多典型的激勵矢量的碼本作為激勵參數,每次編碼時都在這個碼本中搜索一個最佳的激勵矢量,這個激勵矢量的編碼值就是這個序列的碼本中的序號。
CELP已經被許多語音編碼標准所采用,美國聯邦標准FS1016就是采用CELP的編碼方法,主要用於高質量的窄帶語音保密通信。CELP(Code-Excited Linear Prediction) 這是一個簡化的 LPC 算法,以其低比特率著稱(4800-9600Kbps),具有很清晰的語音品質和很高的背景噪音免疫性。CELP是一種在中低速率上廣泛使用的語音壓縮編碼方案。MPEG-1 audio layer 1
類型:Audio
制定者:MPEG
所需頻寬:384kbps(壓縮4倍)
特性:編碼簡單,用於數字盒式錄音磁帶,2聲道,VCD中使用的音頻壓縮方案就是MPEG-1層Ⅰ。
優點:壓縮方式相對時域壓縮技術而言要復雜得多,同時編碼效率、聲音質量也大幅提高,編碼延時相應增加。可以達到“完全透明”的聲音質量(EBU音質標准)
缺點:頻寬要求較高
應用領域:voip
版稅方式:Free
備注:MPEG-1聲音壓縮編碼是國際上第一個高保真聲音數據壓縮的國際標准,它分為三個層次:
--層1(Layer 1):編碼簡單,用於數字盒式錄音磁帶
--層2(Layer 2):算法復雜度中等,用於數字音頻廣播(DAB)和VCD等
--層3(Layer 3):編碼復雜,用於互聯網上的高質量聲音的傳輸,如MP3音樂壓縮10倍MUSICAM(MPEG-1 audio layer 2,即MP2)
類型:Audio
制定者:MPEG
所需頻寬:256~192kbps(壓縮6~8倍)
特性:算法復雜度中等,用於數字音頻廣播(DAB)和VCD等,2聲道,而MUSICAM由於其適當的復雜程度和優秀的聲音質量,在數字演播室、DAB、DVB等數字節目的制作、交換、存儲、傳送中得到廣泛應用。
優點:壓縮方式相對時域壓縮技術而言要復雜得多,同時編碼效率、聲音質量也大幅提高,編碼延時相應增加。可以達到“完全透明”的聲音質量(EBU音質標准)
缺點:
應用領域:voip
版稅方式:Free
備注:同MPEG-1 audio layer 1MP3(MPEG-1 audio layer 3)
類型:Audio
制定者:MPEG
所需頻寬:128~112kbps(壓縮10~12倍)
特性:編碼復雜,用於互聯網上的高質量聲音的傳輸,如MP3音樂壓縮10倍,2聲道。MP3是在綜合MUSICAM和ASPEC的優點的基礎上提出的混合壓縮技術,在當時的技術條件下,MP3的復雜度顯得相對較高,編碼不利於實時,但由於MP3在低碼率條件下高水准的聲音質量,使得它成為軟解壓及網絡廣播的寵兒。
優點:壓縮比高,適合用於互聯網上的傳播
缺點:MP3在128KBitrate及以下時,會出現明顯的高頻丟失
應用領域:voip
版稅方式:Free
備注:同MPEG-1 audio layer 1MPEG-2 audio layer
類型:Audio
制定者:MPEG
所需頻寬:與MPEG-1層1,層2,層3相同
特性:MPEG-2的聲音壓縮編碼采用與MPEG-1聲音相同的編譯碼器,層1、層2和層3的結構也相同,但它能支持5.1聲道和7.1聲道的環繞立體聲。
優點:支持5.1聲道和7.1聲道的環繞立體聲
缺點:-
應用領域:voip
版稅方式:按個收取
備注:MPEG-2的聲音壓縮編碼采用與MPEG-1聲音相同的編譯碼器,層1、層2和層3的結構也相同,但它能支持5.1聲道和7.1聲道的環繞立體聲。AAC(Advanced Audio Coding ,先進音頻編碼)
類型:Audio
制定者:MPEG
所需頻寬:96-128 kbps
特性:AAC可以支持1到48路之間任意數目的音頻聲道組合、包括15路低頻效果聲道、配音/多語音聲道,以及15路數據。它可同時傳送16套節目,每套節目的音頻及數據結構可任意規定。
AAC主要可能的應用范圍集中在因特網網絡傳播、數字音頻廣播,包括衛星直播和數字AM、以及數字電視及影院系統等方面。AAC使用了一種非常靈活的熵編碼核心去傳輸編碼頻譜數據。具有48個主要音頻通道,16 個低頻增強通道,16 個集成數據流, 16 個配音,16 種編排。
優點:支持多種音頻聲道組合,提供優質的音質
缺點:-
應用領域:voip
版稅方式:一次性收費
備注:AAC於1997年形成國際標准ISO 13818-7。先進音頻編碼(Advanced Audio Coding--AAC)開發成功,成為繼MPEG-2音頻標准(ISO/IEC13818-3)之后的新一代音頻壓縮標准。
在MPEG-2制訂的早期,本來是想將其音頻編碼部分保持與MPEG-1兼容的。但后來為了適應演播電視的要求而將其定義成為一個可以獲得更高質量的多聲道音頻標准。理所當然地,這個標准是不兼容MPEG-1的,因此被稱為MPEG-2AAC。換句話說,從表面上看,要制作和播放AAC,都需要使用與MP3完全不同的工具。HR
類型:Audio
制定者: 飛利浦
所需頻寬:8Kbps
特性:以增加GSM網絡容量為目的,但是會損害語音質量;由於現在網絡頻率緊缺,一些大的運營商已經在大城市密集地帶開通此方式以增加容量。
優點:系統容量大
缺點:語音質量差
應用領域:GSM
版稅方式:按個收費
備注:HR半速率,是一種GSM語音編碼方式。FR
類型:Audio
制定者:飛利浦
所需頻寬:13Kbps
特性:是一般的GSM手機的通信編碼方式,可以獲得達到4.1左右Qos的語音通信質量(國際電聯規定語音通信質量Qos滿分為5)
優點:語音質量得到了提高
缺點:系統容量降低
應用領域:GSM
版稅方式:按個收費
備注:FR全速率,是一種GSM語音編碼方式EFR
類型:Audio
制定者:飛利浦
所需頻寬:13Kbps
特性:用於GSM手機基於全速率13Kbps的語音編碼和發送,可以獲得更好更清晰的語音質量(接近Qos4.7)需要網絡服務商開通此項網絡功能,手機才能配合實現。
優點:音質好
缺點:需要網絡服務商開通此項網絡功能,且系統容量降低
應用領域:GSM
版稅方式:按個收費
備注:EFR增強型全速率,一種GSM網絡語音的編碼方式。GSM-AMR(Adaptive Multi-Rate)
類型:Audio
制定者:飛利浦
所需頻寬:8Kbps(4.75 Kbps~12.2 Kbps)
特性: 可以對語音進行替換和消音,平滑噪音,支持間斷式傳輸,對語音進行動態偵查。能在各種網絡條件下提供優質的語音效果。
優點:音質出色
缺點:-
應用領域:GSM
版稅方式:按個收費
備注:GSM-ASM是一種廣泛使用在GPRS和W-CDMA網絡上的音頻標准。在規范ETSI GSM06.90中對GSM-AMR進行了定義。AMR語音編碼是GSM2+和WCDMA的默認編碼標准,是第三代無線通訊系統的語音編碼標准。GSM-AMR標准基於ACELP(代數激勵線性預測)編碼。它能在廣泛的傳輸條件下提供高品質的語音效果。EVRC(Enhanced Variable Rate Coder,增強型可變速率編碼器)
類型:Audio
制定者:美國Qualcomm通信公司(即高通)
所需頻寬:8Kbps或13Kbps
特性:支持三種碼率(9.6 Kbps, 4.8 Kbps 和 1.2 Kbps),噪聲抑制,郵件過濾。能在各種網絡條件下提供優質的語音效果。
優點:音質出色
缺點:-
應用領域:CDMA
版稅方式:按個收費
備注:EVRC編碼廣泛使用於CDMA網絡。EVRC標准遵循規范TIA IS-127的內容。EVRC編碼基於RCELP(松弛碼激勵線性預測)標准。該編碼可以以Rate 1(171bits/packet),Rate1/2(80bits/packet)或是Rate1/8(16bits/packet)的容量進行操作。在要求下,它也能產生空包(0bits/packet)。QCELP(QualComm Code Excited Linear Predictive,受激線性預測編碼)
類型:Audio
制定者:美國Qualcomm通信公司(即高通)
所需頻寬:8k的語音編碼算法(可工作於4/4.8/8/9.6Kbps等固定速率上,而且可變速率地工作於800Kbps~9600Kbps之間)
特性:使用適當的門限值來決定所需速率。QCELP是一種8k的語音編碼算法(可以在8k的速率下提供接近13k的話音壓縮質量)。這是一種可變速率話音編碼,根據人的說話特性(大家應該能夠體會我們日常的溝通和交流時並不是一直保持某種恆定的方式講話,有間斷、有不同的聲音頻率等都是人的自然表達)而采取的一種優化技術。
優點:話音清晰、背景噪聲小,系統容量大
缺點: 不是Free
應用領域:CDMA
版稅方式:每年支付一筆使用權費用
備注:QCELP,即Qualcomm Code Excited Linear Predictive(Qualcomm受激線性預測編碼)。美國Qualcomm通信公司的專利語音編碼算法,是北美第二代數字移動電話(CDMA)的語音編碼標准(IS95)。這種算法不僅可工作於4/4.8/8/9.6kbit/s等固定速率上,而且可變速率地工作於800bit/s~9600bit/s之間。QCELP算法被認為是到目前為止效率最高的一種算法,它的主要特點之一,是使用適當的門限值來決定所需速率。門限值隨背景噪聲電平變化而變化,這樣就抑制了背景噪聲,使得即使在喧鬧的環境中,也能得到良好的話音質量,CDMA8Kbit/s的話音近似GSM 13Mbit/s的話音。CDMA采用QCELP編碼等一系列技術,具有話音清晰、背景噪聲小等優勢,其性能明顯優於其他無線移動通信系統,語音質量可以與有線電話媲美。 無線輻射低。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