1. 程式人生 > >語音通訊解決方案總結

語音通訊解決方案總結

語音通訊方案

系統級方案和自建協議

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有更低的延遲。

三、Core Audio API

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(&params);
// 填充引數空間
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;
}

完整例子

 1 #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.h
  1 #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 }
alsa_audio.cpp

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:

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提供的工具進行檢視、

除錯。

TINYALSA子系統

tinycap.c 實現錄音相關程式碼  tinycap

Tinyplay.c 實現放音相關程式碼 tinyplay

Pcm.c 與驅動層alsa-driver呼叫介面,為audio_hw提供api介面

Tinymix 檢視和設定混音器 tinymix

Tinypcminfo.c 檢視音效卡資訊tinypcminfo

音訊幀(frame)   這個概念在應用開發中非常重要,網上很多文章都沒有專門介紹這個概念。

音訊跟視訊很不一樣,視訊每一幀就是一張影象,而從上面的正玄波可以看出,音訊資料是流式的,本身沒有明確的一幀幀的概念,在實際的應用中,為了音訊演算法處理/傳輸的方便,一般約定俗成取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  

&n