• 语音通信解决方案总结


    语音通信方案

    系统级方案和自建协议

    参考: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有更低的延迟。

    三、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
    ", 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
    ", val);
     61 
     62     if (ret < 0) {
     63         printf("error: unable to set hw parameters: %s
    ", 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!
    ");
     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
    ", 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
    ", snd_pcm_format_width(format));
    101     printf("m_capture_buffer_size:%ld
    ", 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!
    ");
    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
    ", strlen(m_capture_buffer));
    138     if (ret == -EPIPE)
    139     {
    140        /* EPIPE means overrun */
    141        printf("overrun occurred
    ");
    142        snd_pcm_prepare(m_capture_pcm);
    143     }
    144     else if (ret < 0)
    145     {
    146        printf("error from read: %s
    ", snd_strerror(ret));
    147     }
    148     else if (ret != static_cast<int>(m_capture_frames))
    149     {
    150        printf("short read, read %d frames
    ", ret);
    151     }
    152 
    153     if(m_capture_buffer == nullptr)
    154     {
    155         printf("error: alsa audio capture_buffer is empty!
    ");
    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!
    ");
    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
    ", 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!
    ");
    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
    ");
    230             snd_pcm_prepare(m_playback_pcm);
    231         }
    232         else if (ret < 0)
    233         {
    234            printf("error from write: %s
    ", 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
    ", 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编码、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 1

    MP3(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 1

    MPEG-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  
  • 相关阅读:
    js相关禁止
    单例模式 俗称单例3步曲+1曲
    轮廓线重建:二维平行轮廓线重建理论和方法
    一种面向三维地质剖面的形体表面重构算法
    在不使用gluSphere()的情况下在OpenGL中绘制Sphere
    Balabolka
    jQuery学习笔记之可见性过滤选择器
    Flask学习之四 数据库
    Flask学习之三 web表单
    Flask学习之二 模板
  • 原文地址:https://www.cnblogs.com/lzc978/p/12745317.html
Copyright © 2020-2023  润新知