1,在嵌入式Linux上开发的有线通信语音解决方案
这方案是在嵌入式Linux上开发的,音频方案基于ALSA,语音通信相关的都是在user space 做,算是一个上层的解决方案。由于是有线通信,网络环境相对无线通信而言不是特别恶劣,用的丢包补偿措施也不是很多,主要有PLC、RFC2198等。
2,在Android手机上开发的传统无线通信语音解决方案
这方案是在Android手机上开发的,是手机上的传统语音通信方案(相对于APP语音通信而言)。Android是基于Linux的,所以也会用到ALSA,但是主要是做控制用,如对codec芯片的配置等。跟音频数据相关的驱动、编解码、前后处理等在Audio DSP上开发,网络侧相关的在CP(通信处理器)上开发,算是一个底层解决方案。该方案的软件框图如下:
声卡(Sound Card)也叫 音频 卡(港台称之为声效卡),是计算机多媒体系统中最基本的组成部分,是实现 声波 / 数字信号 相互转换的一种 硬件 。声卡的基本功能是把来自 话筒 、磁带、光盘的原始声音 信号 加以转换,输出到 耳机 、 扬声器 、 扩音机 、 录音机 等声响 设备 ,或通过 音乐 设备数字接口( MIDI )发出合成乐器的声音
所有的电脑主板基本都有集成声卡的,如果有专业要求会再买个独立声卡,就像专业玩家一样买个独立显卡,手动狗头
对于音频处理的技术,主要有如下几种:
MME就是winmm.dll提供的接口,也是Windows平台下第一代API。优点是使用简单,一般场景下可以满足业务需求,缺点是延迟高,某些高级功能无法实现。
也是DirextX的一部分,为了取代DirectSound。DirextX套件中的音频组件,大多用于游戏中,支持硬件加速,所以比MME有更低的延迟。
三、 Core Audio API
Vista系统开始引入的新架构,它是以COM的方式提供的接口,用户模式下处于最底层,上面提到的几种API最终都将使用它!功能最强,性能最好,但是接口繁杂,使用起来很麻烦。
而Wave系列的API函数主要是用来实现对麦克风输入的采集(使用WaveIn系列API函数)和控制声音的播放(使用后WaveOut系列函数)。
waveInOpen
开启音频采集设备,成功后会返回设备句柄,后续的API都需要使用该句柄
调用模块需要提供一个回调函数(waveInProc),以接收采集的音频数据
waveInClose
关闭音频采集模块
成功后,由waveInOpen返回的设备句柄将不再有效
waveInPrepareHeader
准备音频采集数据缓存的空间
waveInUnprepareHeader
清空音频采集的数据缓存
waveInAddBuffer
将准备好的音频数据缓存提供给音频采集设备
在调用该API之前需要先调用waveInPrepareHeader
waveInStart
控制音频采集设备开始对音频数据的采集
waveInStop
控制音频采集设备停止对音频数据的采集
音频采集设备采集到音频数据后,会调用在waveInOpen中设置的回调函数。
其中参数包括一个消息类型,根据其消息类型就可以进行相应的操作。
如接收到WIM_DATA消息,则说明有新的音频数据被采集到,这样就可以根据需要来对这些音频数据进行处理。
(示例以后补上)
IMMDeviceEnumerator
IMMDevice
IAudioClient
IAudioCaptureClient
主要过程:
创建多媒体设备枚举器(IMMDeviceEnumerator)
通过多媒体设备枚举器获取声卡接口(IMMDevice)
通过声卡接口获取声卡客户端接口(IAudioClient)
通过声卡客户端接口(IAudioClient)可获取声卡输出的音频参数、初始化声卡、获取声卡输出缓冲区的大小、开启/停止对声卡输出的采集
通过声卡采集客户端接口(IAudioCaptureClient)可获取采集的声卡输出数据,并对内部缓冲区进行控制
(示例以后补上)
混音算法就是将多路音频输入信号根据某种规则进行运算(多路音频信号相加后做限幅处理),得到一路混合后的音频,并以此作为输出的过程。
我目前还做过这一块,搜索了一下基本有如下几种混音算法:
将多路音频输入信号直接相加取和作为输出
将多路音频输入信号直接相加取和后,再除以混音通道数,防止溢出
将多路音频输入信号直接相加取和后,做Clip操作(将数据限定在最大值和最小值之间),如有溢出就设最大值
将多路音频输入信号直接相加取和后,做饱和处理,接近最大值时进行扭曲
将多路音频输入信号直接相加取和后,做归一化处理,全部乘个系数,使幅值归一化
将多路音频输入信号直接相加取和后,使用衰减因子限制幅值
是一个有社区维护的开源项目:http://www.alsa-project.org/
1.内核驱动包 alsa-driver
2.用户空间库 alsa-lib
3.附加库插件包 alsa-libplugins
4.音频处理工具集 alsa-utils
5.其他音频处理小工具包 alsa-tools
6.特殊音频固件支持包 alsa-firmware
7.alsa-lib的Python绑定包 pyalsa
8.OSS接口兼容包 alsa-oss
9.内核空间中,alsa-soc其实是对alsa-driver的进一步封装,他针对嵌入式设备提供了一些列增强的功能。
sudo apt install libasound2-dev
#include <alsa/asoundlib.h>
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, ._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);
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, ._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
注:
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
其中:
core:包含 ALSA 驱动的核心层代码实现。
core/oss:包含模拟旧的OSS架构的PCM和Mixer模块。
core/seq:音序器相关的代码。
drivers:存放一些与CPU,bus架构无关的公用代码。
i2c:ALSA的i2c控制代码。
pci:PCI总线 声卡的顶层目录,其子目录包含各种PCI声卡代码。
isa:ISA总线 声卡的顶层目录,其子目录包含各种ISA声卡代码。
soc:ASoC(ALSA System on Chip)层实现代码,针对嵌入式音频设备。
soc/codecs:针对ASoC体系的各种音频编码器的驱动实现,与平台无关。
include/sound:ALSA驱动的公共头文件目录。
OSS音频设备驱动:
OSS 标准中有两个最基本的音频设备: mixer(混音器)和 dsp(数字信号处理器)。
虽然 OSS 已经非常成熟,但它毕竟是一个没有完全开放源代码的商业产品,而且目前基本上在 Linux mainline 中失去了更新。而 ALSA (Advanced Linux Sound Architecture)恰好弥补了这一空白,它符合 GPL,是在 Linux 下进行音频编程时另一种可供选择的声卡驱动体系结构。 ALSA 除了像 OSS 那样提供了一组内核驱动程序模块之外,还专门为简化应用程序的编写提供了相应的函数库,与 OSS 提供的基于 ioctl 的原始编程接口相比, ALSA 函数库使用起来要更加方便一些。 ALSA 的主要特点如下。支持多种声卡设备。
模块化的内核驱动程序。
支持 SMP 和多线程。
提供应用开发函数库(alsa-lib)以简化应用程序开发。
支持 OSS API,兼容 OSS 应用程序。
ASoC音频设备驱动:
ASoC(ALSA System on Chip)是 ALSA 在 SoC 方面的发展和演变,它在本质上仍然属于
ALSA,但是在 ALSA 架构基础上对 CPU 相关的代码和 Codec 相关的代码进行了分离。其原因是, 采用传统 ALSA 架构的情况下,同一型号的 Codec 工作于不同的 CPU 时,需要不同的驱动,这 不符合代码重用的要求。 对于目前嵌入式系统上的声卡驱动开发,我们建议读者尽量采用 ASoC 框架, ASoC 主要 由 3 部分组成。Codec 驱动。这一部分只关心 Codec 本身,与 CPU 平台相关的特性不由此部分操作。
平台驱动 。这一部分只关心 CPU 本身,不关心 Codec。它主要处理两个问题: DMA 引 擎和 SoC 集成的 PCM、 I2S 或 AC ‘97 数字接口控制。
板驱动 (也称为 machine 驱动)。这一部分将平台驱动和 Codec 驱动绑定在一起,描述了 板一级的硬件特征。
在以上 3 部分中, 1 和 2 基本都可以仍然是通用的驱动了,也就是说, Codec 驱动认为自己 可以连接任意 CPU,而 CPU 的 I2S、 PCM 或 AC ‘97 接口对应的平台驱动则认为自己可以连接任 意符合其接口类型的 Codec,只有 3 是不通用的,由特定的电路板上具体的 CPU 和 Codec 确定, 因此它很像一个插座,上面插上了 Codec 和平台这两个插头。 在以上三部分之上的是 ASoC 核心层,由内核源代码中的 sound/soc/soc-core.c 实现,查看其 源代码发现它完全是一个传统的 ALSA 驱动。因此,对于基于 ASoC 架构的声卡驱动而言, alsa-lib 以及 ALSA 的一系列 utility 仍然是可用的,如 amixer、 aplay 均无需针对 ASoC 进行任何改动。而 ASoC 的用户编程方法也与 ALSA 完全一致。 内核源代码的 Documentation/sound/alsa/soc/目录包含了 ASoC 相关的文档。
目前linux中主流的音频体系结构是ALSA(Advanced Linux Sound Architecture),ALSA在内核驱动层提供了alsa-driver,在应用层提供了alsa-lib,应用程序只需要调用alsa-lib(libtinyalsa.so)提供的API就可以完
成对底层硬件的操作。说的这么好,但是Android中没有使用标准的ALSA,而是一个ALSA的简化版叫做tinyalsa。Android中使用tinyalsa控制管理所有模式的音频通路,我们也可以使用tinyalsa提供的工具进行查看、
调试。
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一帧,则一帧音频数据的大小为:
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)
MP3文件其实是一种经过MP3(即动态影像专家压缩标准音频层面)编码算法压缩的数据,不能直接送给功放,必须先通过解码还原出原始音频数据再进行播放。
脉宽调制(PWM)基本原理:控制方式就是对逆变电路开关器件的通断进行控制,使输出端得到一系列幅值相等的脉冲,用这些脉冲来代替正弦波或所需要的波形。也就是在输出波形的半个周期中产生多个脉冲,使各脉冲的等值电压为正弦波形,所获得的输出平滑且低次谐波少。按一定的规则对各脉冲的宽度进行调制,即可改变逆变电路输出电压的大小,也可改变输出频率。例如,把正弦半波波形分成N等份,就可把正弦半波看成由N个彼此相连的脉冲所组成的波形。这些脉冲宽度相等,都等于 π/n ,但幅值不等,且脉冲顶部不是水平直线,而是曲线,各脉冲的幅值按正弦规律变化。如果把上述脉冲序列用同样数量的等幅而不等宽的矩形脉冲序列代替,使矩形脉冲的中点和相应正弦等分的中点重合,且使矩形脉冲和相应正弦部分面积(即冲量)相等,就得到一组脉冲序列,这就是PWM波形。可以看出,各脉冲宽度是按正弦规律变化的。根据冲量相等效果相同的原理,PWM波形和正弦半波是等效的。对于正弦的负半周,也可以用同样的方法得到PWM波形。
在PWM波形中,各脉冲的幅值是相等的,要改变等效输出正弦波的幅值时,只要按同一比例系数改变各脉冲的宽度即可,因此在交-直-交变频器中,PWM逆变电路输出的脉冲电压就是直流侧电压的幅值。
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