01 音频播放和录音
作者:拓毅恒 | 最后修改:2026-07-22
一、音频功能概述
Air8201G 工业引擎提供了强大的音频处理能力,支持多种音频格式的播放和录制功能。音频系统基于 exaudio 库实现,支持以下主要功能:
- 音频文件播放:支持 MP3、WAV、AMR 等格式的音频文件播放
- 文字转语音(TTS):支持中文语音合成功能
- 流式音频播放:支持 PCM 格式的流式音频播放
- 录音功能:支持 AMR 和 PCM 格式的音频录制
音频功能是嵌入式系统中多媒体应用的重要组成部分,掌握音频系统的使用方法对于实现语音提示、音频播放、录音等应用至关重要。
二、准备硬件环境


1、Air8201H 开发板一块 + 可上网的 sim 卡一张 +4g 天线一根;
2、TYPE-C USB 数据线一根;
TYPE-C USB 数据线直接插到板子的 TYPE-C USB 座子,另外一端连接电脑 USB 口;
3、喇叭一个;
三、准备软件环境
3.1 工具 + 内核固件 + 脚本
在开始实践本示例之前,先筹备一下软件环境:
1.烧录工具: Luatools 工具;
2.内核固件:Air8201H固件(基于Air780EHM)
3.LuatOS 需要的脚本和资源文件
脚本和资源文件:https://gitee.com/openLuat/LuatOS/tree/master/module/Air8201/demo/audio
3.2 API 介绍
这里仅介绍本篇文档所使用的 API,详情请查看:API - exaudio;API - pm
exaudio.setup(config)
配置音频系统参数,包括声道数、采样率等
exaudio.play_start(file_path, callback)
开始播放音频文件
exaudio.play_stop()
停止音频播放
exaudio.play_tts(text, callback)
播放文字转语音内容
exaudio.play_stream_write(data, len)
写入流式音频数据
exaudio.record_start(config, callback)
开始录音
exaudio.record_stop()
停止录音
pm.power(device, enable)
控制系统电源和工作模式
四、音频功能实现概述
本小节详细介绍 Air8201G上音频各种功能模式的实现方法和核心代码逻辑。
4.1 音频文件播放功能
音频文件播放功能用于播放本地存储的音频文件,支持 MP3、WAV、AMR 等格式。
4.1.1 功能定义
配置音频系统参数,播放指定路径的音频文件,支持通过按键进行音频切换和停止播放。
- 自动播放 sample-6s.mp3 音乐
- 支持 MP3、WAV、AMR 格式音频文件播放
- 通过 powerkey 按键进行音频切换(MP3↔AMR)
4.1.2 代码示例
--[[
音频文件播放功能模块
核心业务逻辑:
1. 自动播放 sample-6s.mp3 音乐
2. 通过 powerkey 按键进行音频切换
3. 通过 boot 按键停止音频播放
]]
local exaudio = require "exaudio"
local taskName = "task_audio"
-- 根据版本号自适应设置dac_delay
local set_dac_delay = 0
local version = rtos.version()
local version_num = 0
if version then
-- 从版本号字符串中提取数字部分
local num_str = version:match("V(%d+)")
if num_str then
version_num = tonumber(num_str)
end
end
if version_num and version_num >= 2026 then
-- 固件版本≥V2026,dac_delay单位为100ms
set_dac_delay = 6
else
-- 固件版本<V2026,dac_delay单位为1ms
set_dac_delay = 600
end
-- 音频初始化设置参数,exaudio.setup 传入参数
local audio_setup_param ={
model= "es8311", -- 音频编解码类型,可填入"es8311","tm8211"
i2c_id = 0, -- i2c_id,可填入0,1 并使用pins 工具配置对应的管脚
-- 【注意:固件版本<V2026,这里单位为1ms,这里填600,否则可能第一个字播不出来】
dac_delay = set_dac_delay, -- DAC启动前冗余时间
pa_ctrl = (HARDWARE_ENV == "G") and 25 or 23, -- 音频放大器电源控制管脚, G:25, H:23
dac_ctrl = 2, -- 音频编解码芯片电源控制管脚
audio_mode = "new", -- 音频框架版本选择: "auto"用默认, "new"新框架, "old"旧框架
codec_voltage = (HARDWARE_ENV == "G") and 1 or 0 -- ES8311电压: 0=1.8V, 1=3.3V
}
-- 播放结束回调
local function play_end(event)
if event == exaudio.PLAY_DONE then
log.info("播放完成",exaudio.is_end())
end
end
-- 音频播放的配置
local audio_play_param ={
type= 0, -- 播放类型,有0,播放文件,1.播放tts 2. 流式播放
-- 如果是播放文件,支持mp3,amr,wav格式
-- 如果是tts,内容格式见:https://docs.openluat.com/osapi/ext/exaudio/#tts_2
-- 流式播放,支持PCM/MP3/AMR/WAV格式,如果是流式播放,则sampling_rate, sampling_depth,signed_or_unsigned 必填写
content = "/luadb/sample-6s.mp3", -- 如果播放类型为0时,则填入string 是播放单个音频文件,如果是表则是播放多段音频文件。
cbfnc = play_end, -- 播放完毕回调函数
}
-------------------------------------------
---PWRKEY单按键:短按切换 / 长按停止---
-------------------------------------------
local pwrkey_press_time = 0 -- 记录按下时刻(tick)
local KEY_LONG_PRESS_MS = 1000 -- 长按阈值:1秒
local function pwrkey_handler()
local key_level = gpio.get(gpio.PWR_KEY)
if key_level == 0 then
-- 下降沿:按键按下
pwrkey_press_time = mcu.ticks()
else
-- 上升沿:按键松开
if pwrkey_press_time > 0 then
local duration = mcu.ticks() - pwrkey_press_time
if duration >= KEY_LONG_PRESS_MS then
log.info("长按PWRKEY,停止播放")
sys.sendMsg(taskName, MSG_KEY_PRESS, "STOP_AUDIO")
else
log.info("短按PWRKEY,切换播放")
sys.sendMsg(taskName, MSG_KEY_PRESS, "NEXT_AUDIO")
end
pwrkey_press_time = 0
end
end
end
-- PWRKEY 同时检测上升沿和下降沿
gpio.setup(gpio.PWR_KEY, pwrkey_handler, gpio.PULLUP, gpio.BOTH)
gpio.debounce(gpio.PWR_KEY, 200, 1) -- 防抖,防止频繁触发
---------------------------------
-----主task,处理播放音频---------
---------------------------------
local index_number = 1
local audio_path = nil
local function audio_task()
log.info("开始播放音频文件")
if exaudio.setup(audio_setup_param) then
exaudio.vol(70) -- 喇叭音量
exaudio.play_start(audio_play_param) -- 仅仅支持task 中运行
while true do
local msg = sys.waitMsg(taskName, MSG_KEY_PRESS) -- 等待按键触发
if msg[2] == "NEXT_AUDIO" then
if index_number %2 == 0 then -- 切换音频路径
audio_path = "/luadb/sample-6s.mp3"
else
audio_path = "/luadb/10.amr"
end
exaudio.play_start({type= 0, content = audio_path,cbfnc = play_end,priority = index_number})
index_number= index_number +1
elseif msg[2] == "STOP_AUDIO" then
exaudio.play_stop(audio_play_param)
end
end
end
end
sys.taskInitEx(audio_task, taskName)
4.2 文字转语音功能
文字转语音功能将文本内容转换为语音播放,支持中文语音合成。
4.2.1 功能定义
配置 TTS 参数,播放指定的文本内容,支持通过按键进行音色切换和停止播放。
- 播放 TTS 语音合成内容
- 通过 powerkey 按键进行 TTS 音色切换
- 通过 boot 按键停止 TTS 播放
- 仅支持中文 TTS
4.2.2 代码示例
--[[
文字转语音功能模块
核心业务逻辑:
1. 自动播放 TTS 语音
2. 通过 powerkey 按键进行音色切换
]]
local exaudio = require "exaudio"
local taskName = "task_audio"
-- 根据版本号自适应设置dac_delay
local set_dac_delay = 0
local version = rtos.version()
local version_num = 0
if version then
-- 从版本号字符串中提取数字部分
local num_str = version:match("V(%d+)")
if num_str then
version_num = tonumber(num_str)
end
end
if version_num and version_num >= 2026 then
-- 固件版本≥V2026,dac_delay单位为100ms
set_dac_delay = 6
else
-- 固件版本<V2026,dac_delay单位为1ms
set_dac_delay = 600
end
-- 音频初始化设置参数,exaudio.setup 传入参数
local audio_setup_param ={
model= "es8311", -- 音频编解码类型,可填入"es8311","tm8211"
i2c_id = 0, -- i2c_id,可填入0,1 并使用pins 工具配置对应的管脚
-- 【注意:固件版本<V2026,这里单位为1ms,这里填600,否则可能第一个字播不出来】
dac_delay = set_dac_delay, -- DAC启动前冗余时间
pa_ctrl = (HARDWARE_ENV == "G") and 25 or 23, -- 音频放大器电源控制管脚, G:25, H:23
dac_ctrl = 2, -- 音频编解码芯片电源控制管脚
audio_mode = "new", -- 音频框架版本选择: "auto"用默认, "new"新框架, "old"旧框架
codec_voltage = (HARDWARE_ENV == "G") and 1 or 0 -- ES8311电压: 0=1.8V, 1=3.3V
}
local function play_end(event)
if event == exaudio.PLAY_DONE then
log.info("播放完成",exaudio.is_end())
exaudio.play_stop({type = 1})
end
end
local audio_play_param ={
type = 1, -- 播放类型,有0,播放文件,1.播放tts 2. 流式播放
-- 如果是播放文件,支持mp3,amr,wav格式
-- 如果是tts,内容格式见:https://docs.openluat.com/osapi/ext/exaudio/#tts_2
-- 流式播放,支持PCM/MP3/AMR/WAV格式,如果是流式播放,则sampling_rate, sampling_depth,signed_or_unsigned 必填写
content = "支付宝到账,1千万元", -- 如果播放类型为0时,则填入string 是播放单个音频文件,如果是表则是播放多段音频文件。
cbfnc = play_end, -- 播放完毕回调函数
}
-------------------------------------------
---PWRKEY单按键:短按切换 / 长按停止---
-------------------------------------------
local pwrkey_press_time = 0 -- 记录按下时刻(tick)
local KEY_LONG_PRESS_MS = 1000 -- 长按阈值:1秒
local function pwrkey_handler()
local key_level = gpio.get(gpio.PWR_KEY)
if key_level == 0 then
-- 下降沿:按键按下
pwrkey_press_time = mcu.ticks()
else
-- 上升沿:按键松开
if pwrkey_press_time > 0 then
local duration = mcu.ticks() - pwrkey_press_time
if duration >= KEY_LONG_PRESS_MS then
log.info("长按PWRKEY,停止播放")
sys.sendMsg(taskName, MSG_KEY_PRESS, "STOP_AUDIO")
else
log.info("短按PWRKEY,切换播放")
sys.sendMsg(taskName, MSG_KEY_PRESS, "NEXT_AUDIO")
end
pwrkey_press_time = 0
end
end
end
-- PWRKEY 同时检测上升沿和下降沿
gpio.setup(gpio.PWR_KEY, pwrkey_handler, gpio.PULLUP, gpio.BOTH)
gpio.debounce(gpio.PWR_KEY, 200, 1) -- 防抖,防止频繁触发
---------------------------------------------------------------------------------------------------
---------------主task------------------------------------------------------------------------------
--- 关于TTS 音色设置请见: https://docs.openluat.com/air780epm/common/tts/
---------------------------------------------------------------------------------------------------
local index_number = 1
local audio_path = nil
local function audio_task()
log.info("开始播放TTS")
if exaudio.setup(audio_setup_param) then
--设置音量
exaudio.vol(70) -- 默认音量,范围0-100
exaudio.play_start(audio_play_param)
while true do
local msg = sys.waitMsg(taskName, MSG_KEY_PRESS) -- 等待按键触发
if msg[2] == "NEXT_AUDIO" then
if index_number %5 == 0 then -- 切换播报音色
audio_path = "[m51]支付宝到账,1千万元" -- 许久
elseif index_number %5 == 1 then
audio_path = "[m52]支付宝到账,1千万元" -- 许多
elseif index_number %5 == 2 then
audio_path = "[m53]支付宝到账,1千万元" -- 晓萍
elseif index_number %5 == 3 then
audio_path = "[m54]支付宝到账,1千万元" -- 唐老鸭
elseif index_number %5 == 4 then
audio_path = "[m55]支付宝到账,1千万元" -- 许宝宝
end
exaudio.play_start({type= 1, content = audio_path,cbfnc = play_end,priority = index_number})
index_number= index_number +1
elseif msg[2] == "STOP_AUDIO" then
exaudio.play_stop({type = 1})
end
end
end
end
sys.taskInitEx(audio_task, taskName)
4.3 流式音频播放功能
流式音频播放功能用于实时播放 PCM 格式的音频数据流。
4.3.1 功能定义
配置流式播放参数,通过不断写入 PCM 数据实现实时音频播放,支持音量调节。
- 使用 test.pcm 模拟音频来源进行流式播放
- 通过流式传输不断填入播放的音频数据
- 通过 powerkey 按键进行音量减小
- 通过 boot 按键进行音量增加
- 仅支持 PCM 格式音频
4.3.2 代码示例
--[[
流式音频播放功能模块
核心业务逻辑:
1. 模拟音频数据流式播放
2. 通过 powerkey 按键进行音量调节
]]
exaudio = require("exaudio")
-- 根据版本号自适应设置dac_delay
local set_dac_delay = 0
local version = rtos.version()
local version_num = 0
if version then
-- 从版本号字符串中提取数字部分
local num_str = version:match("V(%d+)")
if num_str then
version_num = tonumber(num_str)
end
end
if version_num and version_num >= 2026 then
-- 固件版本≥V2026,dac_delay单位为100ms
set_dac_delay = 6
else
-- 固件版本<V2026,dac_delay单位为1ms
set_dac_delay = 600
end
-- 音频初始化设置参数,exaudio.setup 传入参数
local audio_setup_param ={
model= "es8311", -- dac类型,可填入"es8311","tm8211"
i2c_id = 0, -- i2c_id,可填入0,1 并使用pins 工具配置对应的管脚
-- 【注意:固件版本<V2026,这里单位为1ms,这里填600,否则可能第一个字播不出来】
dac_delay = set_dac_delay, -- DAC启动前冗余时间
pa_ctrl = (HARDWARE_ENV == "G") and 25 or 23, -- 音频放大器电源控制管脚, G:25, H:23
dac_ctrl = 2, -- 音频编解码芯片电源控制管脚
audio_mode = "new", -- 音频框架版本选择: "auto"用默认, "new"新框架, "old"旧框架
codec_voltage = (HARDWARE_ENV == "G") and 1 or 0 -- ES8311电压: 0=1.8V, 1=3.3V
}
-- 播放完成回调
local function play_end(event)
if event == exaudio.PLAY_DONE then
log.info("播放完成",exaudio.is_end())
end
end
-- 流式播放音频播放的配置
local audio_play_param ={
type= 2, -- 播放类型,有0,播放文件,1.播放tts 2. 流式播放
-- 如果是播放文件,支持mp3,amr,wav格式
-- 如果是tts,内容格式见:https://docs.openluat.com/osapi/ext/exaudio/#tts_2
-- 流式播放,仅支持PCM 格式音频,如果是流式播放,则sampling_rate, sampling_depth,signed_or_unsigned 必填写
cbfnc = play_end, -- 播放完毕回调函数
sampling_rate = 16000, -- 采样率,仅为流式播放起作用
sampling_depth = 16, -- 采样位位深,仅流式播放的时候才有作用
signed_or_unsigned = true -- PCM 的数据是否有符号,仅为流式播放起作用
}
-------------------------------------------
---PWRKEY单按键:短按音量- / 长按音量+---
-------------------------------------------
local volume_number = 50
local pwrkey_press_time = 0 -- 记录按下时刻(tick)
local KEY_LONG_PRESS_MS = 1000 -- 长按阈值:1秒
local function pwrkey_handler()
local key_level = gpio.get(gpio.PWR_KEY)
if key_level == 0 then
-- 下降沿:按键按下
pwrkey_press_time = mcu.ticks()
else
-- 上升沿:按键松开
if pwrkey_press_time > 0 then
local duration = mcu.ticks() - pwrkey_press_time
if duration >= KEY_LONG_PRESS_MS then
-- 长按:增大音量
volume_number = volume_number + 20
if volume_number > 100 then volume_number = 100 end
log.info("长按PWRKEY,增大音量", volume_number)
else
-- 短按:减小音量
volume_number = volume_number - 15
if volume_number < 0 then volume_number = 0 end
log.info("短按PWRKEY,减小音量", volume_number)
end
exaudio.vol(volume_number)
pwrkey_press_time = 0
end
end
end
-- PWRKEY 同时检测上升沿和下降沿
gpio.setup(gpio.PWR_KEY, pwrkey_handler, gpio.PULLUP, gpio.BOTH)
gpio.debounce(gpio.PWR_KEY, 200, 1) -- 防抖,防止频繁触发
---------------------------------
---------模拟获取音频task---------
---------------------------------
local function audio_get_data()
-- 等待播放初始化完成
sys.waitUntil("AUDIO_READY")
log.info("开始流式获取音频数据")
local file = io.open("/luadb/test.pcm", "rb") -- 模拟流式播放音源,实际的音频数据来源也可以来自网络或者本地存储
-- 获取推荐的缓冲区大小
local buffer_size = exaudio.get_stream_buffer_size() or 4096
log.info("流式播放缓冲区大小", buffer_size)
while true do
local read_data = file:read(buffer_size) -- 读取文件,模拟流式音频源,需要1024 的倍数
if read_data == nil then
file:close() -- 模拟音频获取完毕,关闭音频文件
-- 本API需要用V2024固件!!!
-- 写入数据完毕后,通知多媒体通道已经没有更多数据需要播放了
-- 开启后可以有效的降低pop音
exaudio.finish()
break
end
-- 如果读取的数据小于缓冲区大小,补充静音数据
if #read_data < buffer_size then
read_data = read_data .. string.rep("\0", buffer_size - #read_data)
end
exaudio.play_stream_write(read_data) -- 流式写入音频数据
sys.wait(20) -- 写数据需要留出时间给其他task 运行代码
end
end
sys.taskInitEx(audio_get_data, "audio_get_data")
---------------------------------
------------通过主task------------
---------------------------------
local taskName = "task_audio"
local function audio_task()
log.info("开始流式播报")
if exaudio.setup(audio_setup_param) then
exaudio.play_start(audio_play_param)
log.info("播放状态",exaudio.is_end())
sys.publish("AUDIO_READY") -- 通知数据task可以开始读取数据
else
log.error("流式播放启动失败")
end
end
sys.taskInitEx(audio_task, taskName)
4.4 录音到文件功能(AMR 格式)
录音到文件功能支持将音频录制为 AMR 格式的文件。
4.4.1 功能定义
配置录音参数,开始录音并保存到指定文件,支持录音时长控制和播放。
- 录音到文件(AMR 格式),默认保存到/record.amr
- 通过 powerkey按键开始或停止录音/播放
- 支持 5 秒录音时长,可提前结束
- 录音完成后自动播放录音文件
- 实测距离3米能清晰录音,距离5米能听到声音但杂音较多。测试环境:麦克风音量100,播放音量70
4.4.2 代码示例
--[[
录音到文件功能模块(AMR格式)
核心业务逻辑:
1. Power键:开始/停止录音,停止播放
2. 录音时长5秒,可提前结束
3. 录音完成后自动播放录音文件
]]
local exaudio = require "exaudio"
-- 硬件版本由 main.lua 中的 _G.HARDWARE_ENV 全局变量统一控制
-- 根据版本号自适应设置dac_delay
local set_dac_delay = 0
local version = rtos.version()
local version_num = 0
if version then
-- 从版本号字符串中提取数字部分
local num_str = version:match("V(%d+)")
if num_str then
version_num = tonumber(num_str)
end
end
if version_num and version_num >= 2026 then
-- 固件版本≥V2026,dac_delay单位为100ms
set_dac_delay = 6
else
-- 固件版本<V2026,dac_delay单位为1ms
set_dac_delay = 600
end
-- 录音文件路径
local recordPath = "/record.amr"
-- 硬件配置参数
local audio_setup_param = {
model = "es8311", -- dac类型,可填入"es8311","tm8211"
i2c_id = 0, -- I2C接口编号
pa_ctrl = (HARDWARE_ENV == "G") and 25 or 23, -- 音频放大器控制引脚, G:25, H:23
dac_ctrl = 2, -- 音频编解码芯片控制引脚
-- 【注意:固件版本<V2026,这里单位为1ms,这里填600,否则可能第一个字播不出来】
dac_delay = set_dac_delay, -- DAC启动前冗余时间
i2s_sample = 16000, -- I2S采样率
bits_per_sample = 16, -- I2S录音位深
i2s_framebit = 16, -- I2S通道位宽
audio_mode = "auto", -- 音频框架版本选择: "auto"用默认, "new"新框架, "old"旧框架
codec_voltage = (HARDWARE_ENV == "G") and 1 or 0 -- ES8311电压: 0=1.8V, 1=3.3V
}
-- 全局状态
local is_recording = false -- 是否正在录音
local is_playing = false -- 是否正在播放
local record_timer = nil -- 录音计时器
local record_seconds = 0 -- 录音计时秒数
-- 音量设置
local PLAY_VOLUME = 70 -- 播放音量
local RECORD_VOLUME = 70 -- 录音麦克风音量
-- 录音时长设置(秒)
local RECORD_DURATION = 5 -- 录音时长
-- ========== 播放相关函数 ==========
-- 播放完成回调函数
local function play_end_callback(event)
if event == exaudio.PLAY_DONE then
log.info("播放完成")
is_playing = false
end
end
-- 开始播放录音文件
local function start_playback()
log.info("录音文件路径", recordPath)
-- 如果录音文件存在,播放录音
if io.exists(recordPath) then
-- 播放设置
local audio_play_param = {
type = 0, -- 0=播放文件
content = recordPath, -- 播放录音文件
cbfnc = play_end_callback,
priority = 1
}
local file_size = io.fileSize(recordPath)
if file_size > 0 then
log.info("播放录音文件", "大小:", file_size, "字节")
is_playing = true
local play_result = exaudio.play_start(audio_play_param)
if not play_result then
log.error("播放启动失败")
is_playing = false
else
log.info("播放已开始")
end
else
log.warn("录音文件为空,无法播放")
end
else
log.warn("录音文件不存在,无法播放")
end
end
-- 停止播放
local function stop_playback()
if is_playing then
log.info("停止播放")
exaudio.play_stop({type = 0})
is_playing = false
end
end
-- ========== 录音相关函数 ==========
-- 停止录音计时
local function stop_record_timer()
if record_timer then
sys.timerStop(record_timer)
record_timer = nil
record_seconds = 0
end
end
-- 停止录音
local function stop_recording()
if is_recording then
log.info("停止录音", "已录制:", record_seconds, "秒")
exaudio.record_stop()
is_recording = false
stop_record_timer()
end
end
-- 录音完成回调函数
local function record_end_callback(event)
if event == exaudio.RECORD_DONE then
is_recording = false
local file_size = io.fileSize(recordPath)
log.info("录音完成", "大小:", file_size, "字节")
stop_record_timer()
-- 使用定时器延迟500ms后播放录音文件
sys.timerStart(start_playback, 500)
end
end
-- 录音计时器回调
local function record_timer_callback()
if is_recording then
record_seconds = record_seconds + 1
log.info("录音中...", record_seconds, "秒")
-- 如果达到设定时长,自动停止录音
if record_seconds >= RECORD_DURATION then
stop_recording()
log.info("录音时长已达", RECORD_DURATION, "秒,自动停止录音")
end
end
end
-- 开始录音计时
local function start_record_timer()
record_seconds = 0
record_timer = sys.timerLoopStart(record_timer_callback, 1000)
end
-- 开始录音
local function start_recording()
if is_recording then
log.info("已经在录音中")
return false
end
if is_playing then
log.info("正在播放中,停止播放")
stop_playback()
end
log.info("开始录音", "时长:", RECORD_DURATION, "秒")
-- 设置录音麦克风音量
exaudio.mic_vol(RECORD_VOLUME)
local audio_record_param = {
format = exaudio.AMR_NB, -- 使用AMR_NB格式(窄带)
time = RECORD_DURATION, -- 录制时长
path = recordPath, -- 录音文件路径
cbfnc = record_end_callback -- 录音完成回调函数
}
local record_result = exaudio.record_start(audio_record_param)
if record_result then
is_recording = true
start_record_timer()
log.info("录音已开始,按任意键可提前结束")
return true
else
log.error("录音启动失败")
return false
end
end
-- ========== 按键处理函数 ==========
local pwrkey_press_time = 0 -- 记录按下时刻(tick)
local KEY_LONG_PRESS_MS = 1000 -- 长按阈值:1秒
-- PWRKEY单按键:短按开始/停止,长按强制停止
local function pwrkey_handler()
local key_level = gpio.get(gpio.PWR_KEY)
if key_level == 0 then
-- 下降沿:按键按下
pwrkey_press_time = mcu.ticks()
else
-- 上升沿:按键松开
if pwrkey_press_time > 0 then
local duration = mcu.ticks() - pwrkey_press_time
if duration >= KEY_LONG_PRESS_MS then
-- 长按:强制停止(录音或播放)
log.info("长按PWRKEY,强制停止")
stop_recording()
stop_playback()
else
-- 短按:开始录音 / 停止录音 / 停止播放
if is_recording then
log.info("短按PWRKEY,停止录音")
stop_recording()
elseif is_playing then
log.info("短按PWRKEY,停止播放")
stop_playback()
else
log.info("短按PWRKEY,开始录音")
start_recording()
end
end
pwrkey_press_time = 0
end
end
end
-- ========== 音频主任务 ==========
local function main_audio_task()
log.info("音频系统初始化")
if exaudio.setup(audio_setup_param) then
-- 设置音量
exaudio.vol(PLAY_VOLUME) -- 播放音量
exaudio.mic_vol(RECORD_VOLUME) -- 录音麦克风音量
log.info("音量设置", "播放:", PLAY_VOLUME, "录音:", RECORD_VOLUME)
-- 检查是否有录音文件
if io.exists(recordPath) then
local file_size = io.fileSize(recordPath)
log.info("找到录音文件", "大小:", file_size, "字节", "路径:", recordPath)
else
log.info("无录音文件", "路径:", recordPath)
end
log.info("按键功能说明:")
log.info("1. 短按PWRKEY: 开始录音 / 停止录音 / 停止播放")
log.info("2. 长按PWRKEY: 强制停止(录音或播放)")
log.info("3. 录音时长: ", RECORD_DURATION, "秒,可提前结束")
log.info("4. 录音完成后自动播放")
log.info("5. 录音文件保存到:", recordPath)
else
log.error("音频硬件初始化失败")
end
end
-- ========== 初始化设置 ==========
-- PWRKEY 同时检测上升沿和下降沿(单按键操作)
gpio.setup(gpio.PWR_KEY, pwrkey_handler, gpio.PULLUP, gpio.BOTH)
gpio.debounce(gpio.PWR_KEY, 200, 1)
-- 启动音频主任务
sys.taskInit(main_audio_task)
4.5 流式录音功能(PCM 格式)
录音到文件功能支持将音频录制为 PCM 格式的文件。
4.5.1 功能定义
配置录音参数,开始录音并保存到指定文件,支持录音时长控制和播放。
- 录音到文件(PCM 格式),默认保存到/record.pcm
- 通过 powerkey 按键开始或停止录音/播放
- 支持流式录音和播放
- 支持 16kHz 采样率、16 位采样深度、有符号 PCM 数据
- 实测距离3米能清晰录音,距离5米能听到声音但杂音较多。测试环境:麦克风音量100,播放音量70
4.5.2 代码示例
--[[
录音到文件功能模块(PCM格式)
核心业务逻辑:
1. 初始化:挂载SD卡,设置音频硬件参数
2. 录音:流式录音
3. 播放:流式播放,按下BOOT按键读取文件并播放
4. 状态管理:互斥控制录音/播放状态
]]
local exaudio = require "exaudio"
-- 硬件版本由 main.lua 中的 _G.HARDWARE_ENV 全局变量统一控制
-- 根据版本号自适应设置dac_delay
local set_dac_delay = 0
local version = rtos.version()
local version_num = 0
if version then
-- 从版本号字符串中提取数字部分
local num_str = version:match("V(%d+)")
if num_str then
version_num = tonumber(num_str)
end
end
if version_num and version_num >= 2026 then
-- 固件版本≥V2026,dac_delay单位为100ms
set_dac_delay = 6
else
-- 固件版本<V2026,dac_delay单位为1ms
set_dac_delay = 600
end
-- 录音文件路径
local recordPath = "/record.pcm"
-- 全局状态
local is_recording = false -- 是否正在录音
local is_playing = false -- 是否正在播放
local record_timer = nil -- 录音计时器
local record_seconds = 0 -- 录音计时秒数
-- 音量设置
local PLAY_VOLUME = 70 -- 播放音量
local RECORD_VOLUME = 70 -- 录音麦克风音量
-- 录音时长设置(秒)
local RECORD_DURATION = 5 -- 录音时长
-- 硬件配置参数
local audio_setup_param = {
model = "es8311", -- dac类型,可填入"es8311","tm8211"
i2c_id = 0, -- I2C接口编号
pa_ctrl = (HARDWARE_ENV == "G") and 25 or 23, -- 音频放大器控制引脚, G:25, H:23
dac_ctrl = 2, -- 音频编解码芯片控制引脚
-- 【注意:固件版本<V2026,这里单位为1ms,这里填600,否则可能第一个字播不出来】
dac_delay = set_dac_delay, -- DAC启动前冗余时间
i2s_sample = 16000, -- I2S采样率
bits_per_sample = 16, -- I2S录音位深
i2s_framebit = 16, -- I2S通道位宽
audio_mode = "auto", -- 音频框架版本选择: "auto"用默认, "new"新框架, "old"旧框架
codec_voltage = (HARDWARE_ENV == "G") and 1 or 0 -- ES8311电压: 0=1.8V, 1=3.3V
}
-- ========== 播放相关函数 ==========
-- 播放完成回调函数
local function play_end_callback(event)
if event == exaudio.PLAY_DONE then
log.info("播放完成")
is_playing = false
-- 流式播放完成后,通知多媒体通道已经没有更多数据需要播放了
exaudio.finish()
end
end
-- 流式数据读取和写入任务
local function stream_audio_data()
log.info("开始流式读取录音数据")
local file = io.open(recordPath, "rb") -- 打开录音文件进行流式播放
if not file then
log.error("无法打开录音文件:", recordPath)
return
end
-- 获取推荐的缓冲区大小
local buffer_size = exaudio.get_stream_buffer_size() or 4096
log.info("流式播放缓冲区大小", buffer_size)
while is_playing do
local read_data = file:read(buffer_size) -- 读取文件数据
if read_data == nil then
-- 文件读取完毕,关闭文件
file:close()
file = nil -- 标记文件已关闭
-- 写入数据完毕后,通知多媒体通道已经没有更多数据需要播放了
exaudio.finish()
log.info("流式数据读取完成")
break
end
-- 如果读取的数据小于缓冲区大小,补充静音数据
if #read_data < buffer_size then
read_data = read_data .. string.rep("\0", buffer_size - #read_data)
end
exaudio.play_stream_write(read_data) -- 流式写入音频数据
sys.wait(20) -- 写数据需要留出时间给其他task运行代码
end
-- 如果播放被提前停止,确保文件被关闭
if file then
file:close()
log.info("播放被停止,文件已关闭")
end
end
-- 开始播放录音文件
local function start_playback()
log.info("录音文件路径", recordPath)
-- 如果录音文件存在,播放录音
if io.exists(recordPath) then
-- 播放设置
-- 需要注意:播放采样位深仅支持到24位,如果录制32位录音则无法播放,需要用电脑进行播放!!!
local audio_play_param = {
type = 2, -- 2=流式播放
cbfnc = play_end_callback,
priority = 1,
sampling_rate = 16000, -- 采样率
sampling_depth = 16, -- 采样位深
signed_or_unsigned = true -- PCM数据是否有符号
}
local file_size = io.fileSize(recordPath)
if file_size > 0 then
log.info("流式播放录音文件", "大小:", file_size, "字节")
is_playing = true
local play_result = exaudio.play_start(audio_play_param)
if not play_result then
log.error("流式播放启动失败")
is_playing = false
else
log.info("流式播放已开始")
-- 启动流式数据读取任务
sys.taskInit(stream_audio_data)
end
else
log.warn("录音文件为空,无法播放")
end
else
log.warn("录音文件不存在,无法播放")
end
end
-- ========== 录音相关函数 ==========
-- 停止录音计时
local function stop_record_timer()
if record_timer then
sys.timerStop(record_timer)
record_timer = nil
record_seconds = 0
end
end
-- 停止录音
local function stop_recording()
if is_recording then
log.info("停止录音", "已录制:", record_seconds, "秒")
exaudio.record_stop()
is_recording = false
stop_record_timer()
end
end
-- 录音完成回调函数
local function record_end_callback(event)
if event == exaudio.RECORD_DONE then
is_recording = false
local file_size = io.fileSize(recordPath)
log.info("录音完成", "大小:", file_size, "字节")
stop_record_timer()
-- 自动开始播放
sys.taskInit(function()
sys.wait(500)
start_playback()
end)
end
end
-- 计算时间差(毫秒)
local function calc_time_diff_ms(start_tick, end_tick)
-- 检查溢出:Lua中超过0x7fffffff会变成负数
if (start_tick > 0 and end_tick < 0) or (start_tick < 0 and end_tick > 0) then
log.warn("时间计算", "mcu.ticks()溢出,无法准确计算时长")
return nil
end
local diff_ticks = end_tick - start_tick
local hz = mcu.hz()
if hz == 0 then
hz = 1000 -- 默认1ms一个tick
end
return (diff_ticks * 1000) / hz
end
-- 录音设置
local audio_record_param = {
format = exaudio.PCM_16000, -- 使用16kHz PCM格式
time = RECORD_DURATION, -- 录制时长
path = function(buff, size)
-- 流式回调方式将录音数据写入文件
if buff and size > 0 then
-- 获取当前时间
local start_time = mcu.ticks() -- 记录开始时间
local file = io.open(recordPath, "ab") -- 追加模式打开文件
if file then
file:write(buff:query()) -- 将缓冲区数据写入文件
file:close() -- 写入完成后关闭文件
-- 计算写入速度
local end_time = mcu.ticks() -- 记录结束时间
local write_time_ms = calc_time_diff_ms(start_time, end_time)
if write_time_ms and write_time_ms > 0 then
local write_speed = size / (write_time_ms / 1000) -- 字节/秒
log.info("文件写入统计",
"数据大小:", size, "字节,",
"写入耗时:", string.format("%.2f", write_time_ms), "ms,",
"写入速度:", string.format("%.2f", write_speed / 1024), "KB/s")
else
log.info("文件写入统计",
"数据大小:", size, "字节,",
"写入耗时: 溢出无法计算")
end
else
log.error("无法打开录音文件")
end
end
end,
cbfnc = record_end_callback -- 录音完成回调函数
}
-- 录音计时器回调
local function record_timer_callback()
if is_recording then
record_seconds = record_seconds + 1
log.info("录音中...", record_seconds, "秒")
-- 如果达到设定时长,自动停止录音
if record_seconds >= RECORD_DURATION then
stop_recording()
log.info("录音时长已达", RECORD_DURATION, "秒,自动停止录音")
end
end
end
-- 开始录音计时
local function start_record_timer()
record_seconds = 0
record_timer = sys.timerLoopStart(record_timer_callback, 1000)
end
-- 开始录音
local function start_recording()
if is_recording then
log.info("已经在录音中")
return false
end
log.info("开始录音", "时长:", RECORD_DURATION, "秒")
-- 清空旧录音文件(流式模式需要手动管理文件)
if io.exists(recordPath) then
os.remove(recordPath)
log.info("删除旧录音文件")
end
-- 设置录音麦克风音量
exaudio.mic_vol(RECORD_VOLUME)
local record_result = exaudio.record_start(audio_record_param)
if record_result then
is_recording = true
start_record_timer()
log.info("录音已开始,按任意键可提前结束")
return true
else
log.error("录音启动失败")
return false
end
end
-- ========== 音频主任务 ==========
local function main_audio_task()
log.info("音频系统初始化")
if exaudio.setup(audio_setup_param) then
-- 设置音量
exaudio.vol(PLAY_VOLUME) -- 播放音量
exaudio.mic_vol(RECORD_VOLUME) -- 录音麦克风音量
log.info("音量设置", "播放:", PLAY_VOLUME, "录音:", RECORD_VOLUME)
-- 检查是否有录音文件
if io.exists(recordPath) then
local file_size = io.fileSize(recordPath)
log.info("找到录音文件", "大小:", file_size, "字节", "路径:", recordPath)
else
log.info("无录音文件", "路径:", recordPath)
end
log.info("录音时长:", RECORD_DURATION, "秒,录音完成后自动播放")
log.info("录音文件保存到:", recordPath)
-- 自动开始录音
sys.taskInit(function()
sys.wait(1000)
start_recording()
end)
else
log.error("音频硬件初始化失败")
end
end
-- 启动音频主任务
sys.taskInit(main_audio_task)
五、功能演示
5.1 音频文件播放功能演示
确保 main.lua 中保留 require "play_file" 语句,注释其他功能模块

使用 Luatools 将代码烧录到 Air8201G 开发板

烧录完毕后,日志中会打印音频播放的开始和完成信息

开发板将自动播放示例音频文件,通过按键可进行音频切换和停止播放

5.2 文字转语音功能演示
确保 main.lua 中保留 require "play_tts" 语句,注释其他功能模块

使用 Luatools 将代码烧录到 Air8201G 开发板

开发板将播放 TTS 语音内容,日志中会打印 TTS 播放的开始和完成信息

通过按键可进行音色切换和停止播放

5.3 流式音频播放功能演示
确保 main.lua 中保留 require "play_stream" 语句,注释其他功能模块

使用 Luatools 将代码烧录到 Air8201G 开发板

开发板将进行流式音频播放,日志中会显示流式播放的状态

通过按键可调节音量大小,日志中可以看到音量调节信息

5.4 录制音频并播放功能演示(AMR 格式)
确保 main.lua 中保留 require "record_amr_file" 语句,注释其他功能模块

使用 Luatools 将代码烧录到 Air8201G 开发板

烧录完毕后会开始挂载 sd 卡,挂载成功后可以看到卡容量和录音文件保存路径

通过按键开始录音,录音完成后自动播放录音文件

录制完成后,日志中会显示播放状态信息

5.5 流式录音并播放功能演示(PCM 格式)
确保 main.lua 中保留 require "record_pcm_file" 语句,注释其他功能模块

使用 Luatools 将代码烧录到 Air8201G 开发板

通过按键开始录音,录音完成后自动播放录音文件

录制完成后,日志中会提示按下 boot 播放录音,播放时也会显示状态信息

六、总结
本文档详细展示了 Air8201G 工业引擎音频各种功能模式的实现方法和使用场景。通过这些示例,我们可以看到:
- 音频文件播放功能:支持多种格式的音频文件播放,适用于音乐播放、语音提示等场景
- 文字转语音功能:实现中文语音合成,适用于语音播报、智能语音交互等应用
- 流式音频播放功能:支持实时音频数据流播放,适用于网络音频、实时通信等场景
- 录音到文件功能:支持高质量音频录制,适用于录音笔、语音记录等应用
- 流式录音到文件功能:实现实时音频数据采集,适用于语音识别、实时分析等高级应用
掌握这些音频功能模式,将为 Air8201G 工业引擎的多媒体应用开发提供强大的支持。用户可以根据实际需求,选择合适的音频模式,实现丰富的音频处理功能。
七、常见问题
7.1 音频播放无声音问题
解决方案:
- 检查音频硬件连接是否正确
- 确认喇叭是否工作正常
- 检查音频配件板的供电和使能引脚配置是否正确
- 验证 GPIO 管脚配置是否正确
7.2 TTS 播放无声音问题
解决方案:
- 确认所使用的固件是否支持 TTS 功能
- 检查 TTS 播放流程是否正常触发
7.3 录音功能无法工作问题
解决方案:
- 确认麦克风硬件连接正确且完好
- 检查录音参数格式设置是否支持
- 确保存储空间充足
7.4 流式播放或录音过程中中断问题
解决方案:
- 检查数据缓冲区管理逻辑确保数据持续供给
- 确认播放与录音的采样率及格式设置一致
- 优化系统任务调度避免阻塞
7.5 CODEC初始化时,I2C通讯失败
解决方案:
-
使用合宙开发板时,如出现I2C/SPI通讯异常的情况,请使用exmux扩展库的setup函数初始化外设分组开关状态,使用open函数打开外设分组,并跳转至exmux扩展库介绍文档中了解I2C/SPI总线上拉问题;https://docs.openluat.com/osapi/ext/exmux/
-
使用自己制作的板子时,如出现I2C通讯异常的情况,请根据各型号文档中”硬件设计资料“的I2C和SPI板块”常见的坑“栏目中的经验,检查板子上的I2C/SPI总线是正常上拉;也可使用exmux库来管理i2c和spi总线的上拉状态,详情请参考exmux扩展库介绍文档。