☰
STM32 PWM播放WAV音频:零成本语音方案详解
2026/10/15 1:44:42 网站建设 项目流程

简介:STM32嵌入式音频播放工程包,面向单片机开发者与学生,重点演示在无DAC条件下,通过定时器PWM输出与占空比实时调节来播放SD卡中的WAV音频,涉及FatFS文件系统挂载、SPI接口读写、WAV头解析及PCM数据量化映射等关键环节。rar压缩包内共170个文件,体积约1.05MB,以45个h头文件、36个c源文件为核心,辅以18个o目标文件等工程中间产物,构成已配置好的Keil工程,readme.txt另含项目说明与编译指导。已有3891人学习。工程代码注释清晰、模块划分明确,便于读者梳理SD卡驱动、文件系统调用与PWM音频输出的完整流程,也可作为STM32裸机音频方案和PWM信号生成的参考模板。从SD卡数据读取到定时器中断实现占空比更新,整个链路都有对应源码支撑,适合有基础STM32开发经验、希望尝试音频相关项目的学习者在开发板上直接复用,整个方案无需额外音频解码芯片,简化硬件设计。

1. 用PWM把WAV放出声:一个几乎零成本的音频方案

嵌入式项目里经常遇到一个尴尬需求:不加音频芯片,不上DAC,不做I2S功放,就想让设备发出一段提示音或者语音播报。STM32的PWM本来是用来调LED亮度或者驱动电机的,但把WAV文件的采样值映射成PWM占空比序列,再经过一级RC低通滤波,竟然能还原出可听的音频波形。这个方案不需要额外的解码芯片,只用一个定时器、一个引脚、两颗电阻一颗电容,成本几乎为零,非常适合仪表提示音、小家电语音播报、玩具发声这类场景。本文从WAV格式解析、定时器参数设计、数据预处理到播放框架选型,把整个落地路径讲清楚,并给出中断法和DMA法的可复现代码,以及播放过程中最容易翻车的几个细节。

2. 为什么PWM能当DAC用:从WAV文件到定时器输出的几个关键环节

PWM播放WAV这套逻辑,核心不是WAV本身,而是“PWM等效DAC”这个物理过程。要把它做稳,得先把音频数据的组织方式和定时器的发声原理都弄明白,否则后面调音色、查爆音只会越调越乱。

2.1 先搞清楚WAV文件里到底装了什么

WAV是RIFF容器格式的一种,常见结构就是文件头加PCM数据区。不要把它想成MP3那种压缩格式,WAV的音频数据是裸采样的,直接把每个采样点的电压值按顺序排列。标准44字节的文件头记录采样率、位深和声道数,数据区则是一个挨一个的采样值。用读卡器或者Python的wave模块可以一次性把它读出来。

import wave with wave.open("input.wav", "rb") as w: print("采样率:", w.getframerate()) print("位深:", w.getsampwidth() * 8) print("声道数:", w.getnchannels()) print("总帧数:", w.getnframes())

上面这段代码的输出决定了后续所有处理参数。采样率是每秒钟采多少个点,位深决定每个点用8位还是16位表示,声道数决定数据里是不是要每两个值才是一个采样帧。做PWM播放时,通常先把WAV转成单声道,再把位深统一到8位或16位,这样生成C数组时才能直接和定时器比较寄存器对应上。注意,WAV文件头不总是44字节,遇到带扩展块的文件要按fmt块里的实际字段解析,不能盲目跳过固定长度。

数据区长度和播放时长的关系要记住:存储字节数等于采样率乘以位深除以8再乘以时长。8kHz、8位、单声道的一秒音频只有8KB,而16kHz、16位则需要32KB。对内部Flash只有几十到几百KB的MCU来说,这个账必须提前算清楚,不然数组生成完链接都过不了。

2.2 PWM发声不是玄学:占空比变化经过低通就是电压波形

PWM信号本身是数字方波,频率固定,占空比由比较寄存器CCR值决定。如果占空比随时间变化,那么在一个足够长的时间窗口内看平均电压,它就跟着占空比走。把这个平均电压提取出来的手段就是RC低通滤波器。载波频率远高于音频频率时,RC输出端得到的是一个平滑的模拟波形,这就是PWM等效DAC的全部物理基础,没有黑匣子。

输出电压的直流分量与占空比的关系是:Vout等于电源电压乘以(CCR加1)除以(ARR加1)。ARR是自动重装载值,决定PWM周期;CCR是输出比较值,决定高电平持续时间。想让喇叭发出指定幅度的声音,就把WAV采样值线性映射到0到ARR之间,每个采样周期更新一次CCR。WAV采样值越大,输出平均电压越高,波形幅度越大。

有两个频率混在一起,必须先分开理解。PWM载波频率由定时器时钟和ARR决定,是方波的重复频率;采样率则是CCR值更新的节奏,决定音频的还原带宽。很多人第一次做这个方案就是没分开这两个概念,直接把PWM频率设成了8kHz,结果高音全部被滤波器吃掉了,声音闷得像隔着枕头说话。

载波频率要远高于音频信号最高频率,建议至少100kHz以上。音频上限按人声可懂度取4kHz左右,RC低通的截止频率取10kHz到50kHz之间比较合适,低于10kHz会把中高频语音细节削掉,高于50kHz则载波残余会漏过去,听起来高频“嘶嘶”声明显。

2.3 采样率和位深怎么选:提示音和语音的实用参数表

WAV源文件可能是44.1kHz、16位、立体声的,直接拿来做PWM播放会很尴尬:数据量大,中断频率高,MCU吃不消。实际做产品时要把音频离线转成低采样率、低位数、单声道格式。不同场景的参数选择差别很大。

应用场景采样率位深单声道每秒数据量说明
按键提示音8kHz8位8KB能听清音调,做BI BO声足够
语音播报短语8kHz8位8KB能听懂,金属感略强
语音播报(稍高音质)16kHz16位32KB可懂度和自然度明显提升
音乐循环16kHz16位32KB高频截止,适合背景乐

选8位还是16位取决于后级电路。如果PWM输出经过RC滤波后直接进音频功放,功放会有几十倍增益,8位量化噪声会变成明显的底噪;如果只是驱动蜂鸣器或者小喇叭,8位完全够用。16位数据并不代表PWM精度变高,CCR的数值范围还是由ARR决定,16位样本最终还是要缩放映射到ARR范围里,但中间多出来的动态范围能保留更细的幅度变化。实际听感上,阻容滤波加功放方案里16位比8位底噪低不少,代价是Flash占用翻倍。

采样率的选择受中断开销限制。8kHz意味着每隔125微秒就要更新一次CCR,16kHz则是每隔62.5微秒。主频72MHz的芯片上,每次中断只做取数和赋值大概几十条指令,占用几个百分比的CPU,看起来不高,但如果系统里还有显示刷新、按键扫描、通信协议处理,就要考虑中断嵌套和调度抖动。我的经验是:短提示音用8kHz最稳妥,长语音播报用16kHz但配合DMA搬运,不然主循环会被打断得厉害。

2.4 三种播放框架对比:中断法、DMA单缓冲、DMA双缓冲

PWM播放的实现方式不是只有一种,选型取决于音频长度和CPU余量。

中断法最简单,用一个基础定时器产生采样率中断,中断里读取数组下一个样本,写入PWM定时器的CCR寄存器。逻辑清晰,音质稳定,适合播短提示音,缺点是每次样本更新都要进一次中断,音频越长CPU开销越难看。

DMA单缓冲是中断法的改良,预先把样本缩放到CCR范围,配置DMA在采样率定时器的触发下自动把数组搬运到CCR。CPU只在DMA配置好后基本不参与,适合几十秒的语音。麻烦在DMA是连续搬运的,播完一个数组就停了,想循环播放要处理传输完成中断重新装载。

DMA双缓冲是处理长音频的标准做法,把播放缓冲区拆成A、B两半,DMA搬A的时候CPU往B里填下一段数据,半传输中断和传输完成中断交替触发,实现无缝续传。适合配合外部串行Flash流式读取,也是量产方案里最常见的结构。

播放框架实现难度CPU占用适用音频长度
采样率中断逐点更新低中高10秒以内提示音
DMA单缓冲循环中低同一段音频循环播放
DMA双缓冲无缝续传较高低长语音、Flash流式播放

3. 把一首WAV变成STM32的PWM输出:从数据预处理到播放控制

原理清楚了,接下来就是动手环节。整个流程按“离线转数据、在线播放、控制启停”三步走。先通过Python脚本把WAV文件转成适合MCU播放的样本数组,再根据项目资源选中断法或DMA法把样本变成PWM占空比序列,最后处理播放结束时的电平问题。这三步每一步都直接影响最终能不能出声、音质是否可用。

3.1 用Python把WAV转成可直接播放的PWM样本数组

在线把WAV文件直接放在MCU上解析不现实,Flash和RAM都伤不起。常见做法是离线用Python把目标WAV转成C语言能直接包含的头文件或二进制数组,转换过程包括声道合并、采样率抽取、位深变换和幅度缩放。下面是一个通用性较好的转换脚本核心部分。

import wave import array def wav_to_pwm_table(path, target_sr=8000, bits=8): wav = wave.open(path, "rb") sr = wav.getframerate() ch = wav.getnchannels() width = wav.getsampwidth() frames = wav.readframes(wav.getnframes()) fmt = "h" if width == 2 else "b" samples = array.array(fmt, frames) if ch > 1: samples = samples[::ch] # 只保留左声道 ratio = int(sr / target_sr) if ratio > 1: samples = samples[::ratio] # 简单抽取降采样 if bits == 8: pwm_values = [(s >> 8) & 0xFF for s in samples] else: pwm_values = [s + 32768 for s in samples] return pwm_values

这段脚本的逻辑是先把多声道数据抽成单声道,再用整数倍抽取的方式把采样率降到目标值。ratio是原采样率除以目标采样率的整数商,例如44100降到8000时ratio为5,也就是每5个点取1个。这种粗暴抽取不会保留原始信号的高频成分,会产生混叠,但对语音提示音这种窄带信号来说可接受;如果源文件是音乐,建议先用音频软件做低通滤波再抽取,否则会有明显的“金属声”。

bits参数决定输出是8位还是16位。8位时把16位采样值右移8位再取低8位,得到0到255的无符号值,这个值可以直接映射到PWM的CCR。16位时在原始16位有符号数上加32768,转成0到65535的无符号值,后续在MCU里再缩放到ARR范围。脚本生成的列表可以进一步格式化成C语言的const uint8_t pcm[]或const uint16_t pcm_scaled[],然后用xxd -i等工具生成头文件。

参数选择上,8kHz、8位适合提示音和短语音,16kHz、16位适合对可懂度有要求的语音播报。不要贪心把采样率设得过高,中断频率翻倍带来的是CPU占用翻倍,而通过8kHz小喇叭播放出来的听感提升非常有限。

3.2 最小播放方案:定时器中断里逐点更新占空比

PWM的载波由一个高级定时器产生,采样率节奏由另一个基础定时器产生,两个定时器各司其职。载波定时器配置为PWM模式,输出频率100kHz以上;采样率定时器配置为8kHz更新中断,中断服务函数里从数组取一个样本,写入载波定时器的CCR寄存器。下面以寄存器风格给出中断部分的核心代码,不同固件库写法虽有差异,但逻辑完全一致。

/* TIM1产生PWM载波,ARR=359,载波频率约200kHz */ /* TIM6产生8kHz更新中断,用于逐点刷新CCR */ volatile uint16_t play_pos = 0; volatile uint8_t playing = 0; extern const uint8_t pcm_table[]; /* Python生成的8位样本表 */ void TIM6_IRQHandler(void) { if (TIM6->SR & TIM_SR_UIF) { TIM6->SR &= ~TIM_SR_UIF; /* 清更新标志 */ if (!playing) return; uint8_t s = pcm_table[play_pos++]; if (play_pos >= TABLE_LEN) { play_pos = 0; /* 循环播放,回绕不用取模 */ } /* 8位样本0~255缩放到0~359,乘法代替除法 */ TIM1->CCR1 = ((uint32_t)s * 360) >> 8; } }

中断函数里只做三件事:读表、移动位置、写CCR。不要在里面调用任何库函数,也不要对数组做边界检查之外的复杂逻辑。play_pos到达末尾直接清零回绕,这里故意不用取模运算,因为8位MCU或者72MHz主频的Cortex-M3上取模涉及除法指令,几条指令的耗时在8kHz中断频率下影响不大,但如果以后把采样率提到32kHz,任何多余操作都会被放大。样本从0到255映射到0到359通过一次乘法和移位完成,CCR值永远不会超过ARR,占空比不会溢出。

这里的核心观念是:载波定时器TIM1只要配置好就不用再干预,它持续以200kHz的频率输出方波。TIM6中断的作用是每隔125微秒调整一次方波的占空比,让RC滤波后的平均电压跟随音频波形变化。把200kHz的脉宽调制和8kHz的占空比更新分开看,整套系统的实时性关系就清楚了。若把TIM1的更新中断也打开,会以200kHz频率打断主程序,完全没必要。

初始化顺序也有讲究:先开TIM1的PWM输出,但把CCR初始化为0,这时引脚输出恒定低电平;然后再开TIM6中断。反过来操作的话,在PWM还没有配置完的时候就启动采样定时器,第一个中断会写入一个未初始化的CCR,输出一个不确定占空比的脉冲,听感上就是“咔”一声。

3.3 降低CPU开销:用DMA把样本自动搬进CCR寄存器

中断法虽然简单,但每次样本更新都要进一次中断。音频只有三五秒的话问题不大,如果播放内容到了几十秒,8kHz中断会让系统实时性明显变差。DMA法把“从数组取数据写CCR”这个动作交给DMA控制器完成,采样率定时器每产生一次更新事件,DMA就搬运一个数据到CCR寄存器,CPU全程不参与。先看配置代码。

/* pcm_scaled 是预处理好、已经映射到0~359范围的半字数组 */ const uint16_t pcm_scaled[TABLE_LEN]; /* TIM4 配置为8kHz更新频率,不用中断,只作为DMA触发源 */ /* TIM1 继续产生200kHz PWM载波 */ DMA_InitTypeDef dma; dma.PeripheralBaseAddr = (uint32_t)&TIM1->CCR1; dma.MemoryBaseAddr = (uint32_t)pcm_scaled; dma.DIR = DMA_DIR_PeripheralDST; /* 内存搬运到外设寄存器 */ dma.BufferSize = TABLE_LEN; dma.PeripheralInc = DMA_PeripheralInc_Disable; dma.MemoryInc = DMA_MemoryInc_Enable; dma.PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; dma.MemoryDataSize = DMA_MemoryDataSize_HalfWord; dma.Mode = DMA_Mode_Circular; /* 循环搬运实现循环播放 */ dma.Priority = DMA_Priority_High; DMA_Init(DMA1_Channel4, &dma); /* 使能TIM4更新事件触发DMA请求 */ TIM4->DIER |= TIM_DIER_UDE; DMA_Cmd(DMA1_Channel4, ENABLE); TIM_Cmd(TIM4, ENABLE);

这里最容易出错的地方是DMA触发源。DMA的搬运节奏必须由采样率定时器TIM4决定,而不是由PWM载波定时器TIM1决定。如果误把DMA挂在TIM1的更新事件上,那么DMA将以200kHz的频率疯狂搬数据,整个样本数组一瞬间就被搬完,音频音调会变得极高。这个错误在示波器上看PWM波形完全正常,只有接了喇叭才能听出不对。配置DMA之前要查所用芯片参考手册中的DMA请求映射表,确认TIM4更新事件连到的是哪个DMA通道。

DMA传输宽度这里用的是半字,对应16位样本数组。如果使用8位样本,需要把PeripheralDataSize和MemoryDataSize都改成Byte。但要注意CCR寄存器是16位的,DMA用字节宽度写入CCR时,高字节被丢掉的危险取决于DMA和寄存器之间的对齐方式,稳妥做法是统一用半字。样本值在预处理阶段就缩放到0到359,避免每次搬运后在中断里做乘法和移位,DMA通道本身没有计算能力,这一点必须在生成数组时解决。

BufferSize受限于DMA通道寄存器的位数。大多数寄存器的传输数量寄存器是16位的,最大65535,数组长度超过这个值时不能一次性配置为循环模式,要么拆成两段手动续传,要么直接采用双缓冲结构。循环模式适合提示音这种音频长度可控的场景,播完一轮自动从头再来,适合做循环警告音。

3.4 播放控制与消除首尾爆音

播放控制很容易被忽略,但PWM播放WAV的听感好坏很大程度取决于启动和停止的瞬间处理。启动时如果PWM引脚在CCR未初始化时直接输出方波,喇叭会“啪”一声;停止时如果CCR还保持最后一个音频样本的值,关闭定时器瞬间电平突变,同样产生爆音。正确顺序是先静音再启停。

void audio_play(uint8_t enable) { if (enable) { TIM1->CCR1 = 0; /* 先置静音电平 */ TIM_Cmd(TIM1, ENABLE); /* 让PWM输出稳定在低电平 */ play_pos = 0; playing = 1; TIM_Cmd(TIM6, ENABLE); /* 采样定时器最后启动 */ } else { playing = 0; TIM_Cmd(TIM6, DISABLE); /* 先停样本更新 */ TIM1->CCR1 = 0; /* 再把占空比拉到最低 */ TIM_Cmd(TIM1, DISABLE); /* 最后关PWM输出 */ } }

上面的顺序是经过实际测试的。启动时先让PWM定时器工作并输出低电平,给RC滤波电容一个充电到0电平的稳定时间,然后再让样本数据开始流动。停止时先停样本源,再置CCR为0,最后关PWM,这样喇叭不会因电平突变而产生“咔哒”声。这个“先静音再启停”的习惯,能省掉后面调爆音的大量时间。

另一个常见疑问是:停止播放后要不要把引脚配置成普通GPIO并输出低电平。如果PWM输出引脚直接驱动三极管或者经过功放,PWM关闭后引脚处于浮空状态,外部噪声会耦合进功放产生杂音。我一般会在音频停止后把引脚复用为GPIO推挽输出低电平,彻底消除悬空噪声。模拟开关之类的前端电路则不需要这么做,但加一个下拉电阻总是保险的。

4. PWM播放WAV的高频翻车点:现象、原因与排查手段

这套方案我对接过不少次,也看过别人在调音过程中反复折腾。有些问题换个思路几分钟就能定位,有些坑则需要在原理层面想明白。这章把最常遇到的五类问题按现象、原因、解决的顺序写出来,每条都是实际能复现的案例。

4.1 放出来的声音音调不对,像快进或者叽叽叫

现象:播放一段语音样本,内容听不清,男声变成尖声,语速明显加快,像磁带快进。

原因:DMA或者中断的触发节奏不是按采样率走的。最常见的配置错误是把DMA挂在载波PWM定时器的更新事件上,导致DMA以200kHz的速度搬运样本,等效采样率变成200kHz,音频被压缩到原来时长的1/25,同时频谱整体上移,音调拔高一大截。另一个常见原因是定时器预分频算错,8kHz中断实际变成了80kHz或800Hz。

解决:先用示波器或者逻辑分析仪确认定时器更新事件的频率。把采样率定时器的预分频和重装载值从时钟树一路推算出来,不要盲调。我的习惯是先在调试器里设置一个断点,每进中断十次读一次计数器,反推实际中断频率,确认是8kHz左右再连音频电路测试。DMA触发源务必查芯片手册的DMA请求映射表,确认到底用的是哪个定时器事件触发的搬运。

4.2 音频尾部出现“咔哒”声和金属声

现象:语音整体能听清,但最后一个字结束瞬间有明显爆音,或者句子反复循环播放时每次结尾都“啪”一下。

原因:样本表在末尾处波形幅度不在零附近。WAV文件截断时往往停在一个非零采样值上,CCR写入这个非零值后突然跳到回绕点,电平发生跳变,喇叭就会“啪”一声。循环播放时这个问题更严重,因为回绕点同样会选择一个电平突变的起点。

解决:在Python转换脚本里加一个后处理,找到接近静音的截断点。最简单实用的做法是:把WAV文件用音频编辑软件裁切到静音段末尾,确保结尾处采样值接近零;循环素材则要保证起点和终点都在过零点附近,通常取时域波形与零轴交叉的位置。代码层面无法完全消除样本跳变,只能从数据源头处理。如果必须播放固定素材且不允许改数据,可以在播放末尾加一小段数十毫秒的淡化输出,让CCR逐步衰减到零。

4.3 播放期间主循环卡顿,按键和显示明显变慢

现象:按下按键后要等几百毫秒才有反应,OLED刷新闪烁,Putty串口打印一卡一卡的,停止播放后一切恢复正常。

原因:采样率中断里做了不该做的事。最常见的是在中断里用取模运算回绕索引、调用库函数做浮点缩放、甚至把下一段语音从Flash拷到RAM。8kHz中断每125微秒触发一次,如果中断服务函数执行了50微秒,主循环就少了百分之四十的执行时间。

解决:中断服务函数只保留读数组、自增索引、回绕判断、写CCR四步,不超过二十条指令。所有数值缩放放到离线Python脚本或者上电初始化时完成。索引回绕用if判断替代取模。如果需要搬运下一段语音数据,放到主循环或者用DMA做,绝不在中断里搬长数据。另外检查中断优先级,采样率中断可以设为比通信外设低一档,避免它频繁打断UART和定时器PWM输出。

4.4 声音小且发闷,音量调到最大也只有蚊子声

现象:波形用示波器看完全正常,PWM输出端方波干净,RC滤波后端也有信号,但喇叭声音非常小,听起来像隔着几层被子在说话。

原因:RC低通截止频率太低,把语音的中高频成分全部衰减掉了。比如用了10k对阻加100nF电容,截止频率约159Hz,这只适合滤除超低频纹波,语音信号经过它以后基本消失。另一个原因是载波频率过低,例如PWM载波只有8kHz,RC滤波既要滤除载波又要保留音频,两者频率太接近,结果只能折中,两头都兼顾不好。

解决:RC截止频率按载波频率和音频带宽折中选取。载波100kHz以上时,RC截止频率取10kHz到50kHz之间比较合适。一个常用组合是1k对阻加10nF电容,计算截止频率大约16kHz,对4kHz以内的语音几乎没有衰减,对200kHz载波抑制也比较可观。如果后级是功放电路,可以再把RC级数增加一级,做成二阶低通,但要注意高频相位变化对语音清晰度的影响,通常一阶就够用了。

4.5 播放长语音时编译报错,Flash装不下

现象:把一段几秒钟的WAV转成数组放进工程,编译提示region Flash overflow exceeded,或者链接失败。

原因:没算内存账。16kHz、16位、单声道的一分钟音频数据量约1.92MB,普通MCU内部Flash根本装不下。一次把整段语音放进静态数组,不是Flash爆就是RAM爆。

解决:长语音必须分块播放。方案A是降低采样率到8kHz、8位,音频可以压缩到每秒8KB,音质尚可接受;方案B是把语音数据放在外部串行Flash里,播放时按块读取到RAM双缓冲,再交给DMA搬运到CCR。方案B是量产项目的主流做法,缓冲区只需预留两个2KB到4KB的块,就能实现无缝插播。如果外部Flash容量有限,还可以把PCM数据做简单差分压缩,播放时在双缓冲填充阶段解压,CPU占用会上升,但能换回一半存储空间。

5. 让声音更好听:验证方法、音量控制和两个进阶技巧

播放出声只是第一步,出声质量是不是能用,得靠工具和耳朵一起验证。我的习惯是先用8Ω小喇叭直接听,因为人耳对语音可懂度的判断比示波器快得多。先放一段8kHz、8位的语音样本,如果每个字都能听清、没有嘶嘶尾音和爆音,说明采样率和滤波参数基本合理;然后再用示波器测量PWM输出端和RC滤波后端的波形,观察占空比是否随时间平滑变化。RC后端波形应该是平滑的音频包络线,而不是有明显锯齿的方波残余。量到包络线但喇叭无声,问题出在后级功放;包络畸形,回头查RC参数和载波频率。

音量控制可以通过预处理阶段完成。样本值乘一个0到1的音量系数,再缩放到CCR范围,这样播放时不需要每次中断做乘法。音量系数要放在离线脚本里,而不是运行期处理,原因是中断里任何一个多余计算都会积累成可听噪声。如果确实需要运行期调整音量,可以在双缓冲填充时处理新数据,不让音频中断碰乘法。

进阶技巧一:双缓冲无缝续传。把播放缓冲区拆成A、B两半,DMA半传输中断和传输完成中断交替通知主程序填充下一段数据。这个模式适合从外部串行Flash流式读取长语音,也可以配合按键实现“上一段”“下一段”切换而不打断播放。

进阶技巧二:变调播放。在不改变音频数据的前提下,播放时切换采样率定时器的预分频值,比如把8kHz采样率临时改成16kHz,音频会变快变尖,实现“倍速”和“慢速”效果。这个技巧常用于设备报数或倒计时提醒场景,配置定时器PSC寄存器即可完成切换,对正在播放的音频数据无影响。

最后说个我的习惯:无论工程多小,都要在播放启动和关闭的位置加静音延迟,并在每块音频数据边界处留出过零衔接。最早做这个方案时,我图省事直接让循环数组在任意位置回绕,结果每次循环都有“咔”一下的爆音,后来在转换脚本里对齐了过零点,世界瞬间清净了。这个习惯一直保留到现在,也成了我做所有音频播放方案的第一条准则。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询