跳转至

02 音频播放和录音

作者:拓毅恒 | 最后修改:2026-05-15

一、音频功能概述

Air780EHM 工业引擎提供了强大的音频处理能力,支持多种音频格式的播放和录制功能。

注意:Air780EPM 不支持此功能模块,如果要测试此功能请更换 Air780EHM 等其他支持 audio 库的模块

音频系统基于 exaudio 库实现,支持以下主要功能:

  1. 音频文件播放:支持 MP3、WAV、AMR 等格式的音频文件播放
  2. 文字转语音(TTS):支持中文语音合成功能
  3. 流式音频播放:支持 PCM 格式的流式音频播放
  4. 录音功能:支持 AMR 和 PCM 格式的音频录制

音频功能是嵌入式系统中多媒体应用的重要组成部分,掌握音频系统的使用方法对于实现语音提示、音频播放、录音等应用至关重要。

二、准备硬件环境

参考:Air780EPM/Air780EHM 硬件环境清单,准备好硬件环境。以下三种环境,任选一种即可。

2.1 Air780EHM 整机开发板

Air780EHM 开发板提供了丰富的音频接口资源,可通过开发板上的音频接口进行连接和测试。

2.2 Air780EHM 核心板 + AirAUDIO_1010 配件板

Air780EHM 核心板集成了移动网络通信和音频处理功能,需要正确安装 SIM 卡和音频设备才能正常工作。

AirAUDIO_1010 配件板

注意: 如果搭配 AirAUDIO_1010 扩展板测试,需将 AirAUDIO_1010 扩展板中 PA 开关拨到 OFF,让软件控制 PA,避免 pop 音

Air780EHM 核心板和 AirAUDIO_1010 配件板的硬件接线方式为:

Air780EHM核心板
AirAUDIO_1010配件板
26/I2S_MCLK
I2S_MCLK
30/I2S_BCK
I2S_BCK
31/I2S_LRCK
I2S_LRCK
32/I2S_DIN
I2S_DIN
33/I2S_DOUT
I2S_DOUT
67/I2C1_SCL
I2C_SCL
66/I2C1_SDA
I2C_SDA
25/GPIO26
PA_EN
23/GPIO2
8311_EN
VBAT
VCC
GND
GND

2.3 Air780EHM 核心板 + AirAUDIO_1020 配件板

AirAUDIO_1020 配件板

Air780EHM 核心板和 AirAUDIO_1020 配件板的硬件接线方式为:

Air780EHM核心板
AirAUDIO_1020配件板
30/I2S_BCK
I2S_BCK
31/I2S_LRCK
I2S_LRCK
33/I2S_DOUT
I2S_DOUT
25/GPIO26
PA_EN
23/GPIO2
I2S_EN
VBAT
VCC
GND
GND

三、准备软件环境

3.1 工具 + 内核固件 + 脚本

1、烧录工具 Luatools

2、本demo开发测试时使用的固件为Air780EHM V2016 版本固件(请选择支持 TTS 功能固件),所以你如果要测试本demo时,可以直接使用支持 TTS 功能最新版本的内核固件Air780EHM固件;如果发现最新版本的内核固件测试有问题,可以使用我们开发本demo时使用的内核固件版本来对比测试;

3、 luatos 需要的脚本和资源文件

4、合宙 LuatIO 工具(GPIO 复用初始化配置)使用说明

5、 lib 脚本文件:使用 Luatools 烧录时,勾选 添加默认 lib 选项,使用默认 lib 脚本文件;

3.2 API 介绍

这里仅介绍本篇文档所使用的 API,详情请查看:API - exaudioAPI - 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)

控制系统电源和工作模式

四、音频功能实现概述

本小节详细介绍 Air780EHM 开发板上音频各种功能模式的实现方法和核心代码逻辑。

音频配件板使用说明:

本示例默认使用核心板 + AirAUDIO_1010 ,如需使用核心板 + AirAUDIO_1020,请修改 audio_setup_param

-- AirAUDIO_1010 配置(默认)
local audio_setup_param ={
    model= "es8311",          -- 使用 ES8311 编解码器
    i2c_id = 1,               -- I2C总线ID,需要配置
    pa_ctrl = 26,             -- PA控制管脚
    dac_ctrl = 2,             -- ES8311电源控制管脚
}

-- AirAUDIO_1020 配置
local audio_setup_param ={
    model= "tm8211",          -- 使用 TM8211 编解码器
    -- i2c_id 无需配置,TM8211不需要I2C
    pa_ctrl = 26,             -- PA控制管脚
    dac_ctrl = 2,             -- TM8211电源控制管脚
}
-- 注意:AirAUDIO_1020 不支持录音功能(无麦克风接口)

4.1 音频文件播放功能

音频文件播放功能用于播放本地存储的音频文件,支持 MP3、WAV、AMR 等格式。

4.1.1 功能定义

配置音频系统参数,播放指定路径的音频文件,支持通过按键进行音频切换和停止播放。

  • 自动播放 sample-6s.mp3 音乐
  • 支持 MP3、WAV、AMR 格式音频文件播放
  • 通过 powerkey 按键进行音频切换(MP3↔AMR)
  • 通过 boot 按键停止音频播放

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","es8211"
    i2c_id = 0,          -- i2c_id,可填入0,1 并使用pins 工具配置对应的管脚

    -- 【注意:固件版本<V2026,这里单位为1ms,这里填600,否则可能第一个字播不出来】
    dac_delay = set_dac_delay,            -- DAC启动前冗余时间

    pa_ctrl = gpio.AUDIOPA_EN,         -- 音频放大器电源控制管脚
    dac_ctrl = 20,        --  音频编解码芯片电源控制管脚,780EHM 默认使用20
}

-- 播放结束回调
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-流式播放
    content = "/luadb/sample-6s.mp3",  -- 播放文件路径
    cbfnc = play_end,            -- 播放完毕回调函数
}

-- 通过 BOOT 按键进行播放停止操作
local function stop_audio()
    log.info("停止播放")
    sys.sendMsg(taskName, MSG_KEY_PRESS, "STOP_AUDIO")
end

-- 按下 boot 停止播放
gpio.setup(0, stop_audio, gpio.PULLDOWN, gpio.RISING)
gpio.debounce(0, 200, 1) -- 防抖,防止频繁触发

-- 通过 POWERKEY 按键进行音频切换
local function next_audio()
    log.info("切换播放")
    sys.sendMsg(taskName, MSG_KEY_PRESS, "NEXT_AUDIO")
end

-- 按下 powerkey 打断播放,播放优先级更高的音频
gpio.setup(gpio.PWR_KEY, next_audio, gpio.PULLUP, gpio.FALLING)
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.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 按键进行音色切换
3. 通过 boot 按键停止 TTS 播放
]]

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","es8211"
    i2c_id = 0,          -- i2c_id,可填入0,1 并使用pins 工具配置对应的管脚

    -- 【注意:固件版本<V2026,这里单位为1ms,这里填600,否则可能第一个字播不出来】
    dac_delay = set_dac_delay,            -- DAC启动前冗余时间

    pa_ctrl = gpio.AUDIOPA_EN,         -- 音频放大器电源控制管脚
    dac_ctrl = 20,        --  音频编解码芯片电源控制管脚,780EHM 默认使用20
}

-- 播放结束回调
local function play_end(event)
    if event == exaudio.PLAY_DONE then
        log.info("播放完成", exaudio.is_end())
    end
end

-- TTS 播放的配置
local audio_play_param = {
    type = 1,                    -- 播放类型:0-播放文件,1-播放 TTS,2-流式播放
    content = "欢迎使用 LuatOS 音频系统",  -- TTS 播放内容
    cbfnc = play_end,            -- 播放完毕回调函数
}

-- 通过 BOOT 按键进行播放停止操作
local function stop_audio()
    log.info("停止播放")
    sys.sendMsg(taskName, MSG_KEY_PRESS, "STOP_AUDIO")
end

-- 按下 boot 停止播放
gpio.setup(0, stop_audio, gpio.PULLDOWN, gpio.RISING)
gpio.debounce(0, 200, 1) -- 防抖,防止频繁触发

-- 通过 POWERKEY 按键进行音色切换
local function next_audio()
    log.info("切换播放")
    sys.sendMsg(taskName, MSG_KEY_PRESS, "NEXT_AUDIO")
end

-- 按下 powerkey 打断播放,播放优先级更高的音频
gpio.setup(gpio.PWR_KEY, next_audio, gpio.PULLUP, gpio.FALLING)
gpio.debounce(gpio.PWR_KEY, 200, 1) -- 防抖,防止频繁触发

-- 主 task,处理播放 TTS
local index_number = 1
local tts_content = nil
local function audio_task()
    log.info("开始播放 TTS")
    if exaudio.setup(audio_setup_param) then
        exaudio.play_start(audio_play_param) -- 仅支持 task 中运行
        while true do
            local msg = sys.waitMsg(taskName, MSG_KEY_PRESS) -- 等待按键触发
            if msg[2] == "NEXT_AUDIO" then
                -- 切换不同的 TTS 内容和音色
                if index_number % 5 == 0 then
                    tts_content = "欢迎使用 LuatOS 音频系统"
                elseif index_number % 5 == 1 then
                    tts_content = "当前时间:2024年12月"
                elseif index_number % 5 == 2 then
                    tts_content = "系统运行正常"
                elseif index_number % 5 == 3 then
                    tts_content = "音频播放功能测试"
                elseif index_number % 5 == 4 then
                    tts_content = "TTS 功能演示"
                end
                exaudio.play_start({type = 1, content = tts_content, 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.3 流式音频播放功能

流式音频播放功能用于实时播放 PCM 格式的音频数据流。

4.3.1 功能定义

配置流式播放参数,通过不断写入 PCM 数据实现实时音频播放,支持音量调节。

  • 使用 test.pcm 模拟音频来源进行流式播放
  • 通过流式传输不断填入播放的音频数据
  • 通过 powerkey 按键进行音量减小
  • 通过 boot 按键进行音量增加
  • 仅支持 PCM 格式音频

4.3.2 代码示例

--[[
流式音频播放功能模块
核心业务逻辑:
1. 模拟音频数据流式播放
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

-- 音频初始化设置参数
local audio_setup_param ={
    model= "es8311",          -- 音频编解码类型,可填入"es8311","es8211"
    i2c_id = 0,          -- i2c_id,可填入0,1 并使用pins 工具配置对应的管脚

    -- 【注意:固件版本<V2026,这里单位为1ms,这里填600,否则可能第一个字播不出来】
    dac_delay = set_dac_delay,            -- DAC启动前冗余时间

    pa_ctrl = gpio.AUDIOPA_EN,         -- 音频放大器电源控制管脚
    dac_ctrl = 20,        --  音频编解码芯片电源控制管脚,780EHM 默认使用20
}

-- 播放结束回调
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-流式播放
    sampling_rate = 16000,       -- 采样率
    sampling_depth = 16,         -- 采样位数
    channel_num = 1,             -- 声道数
    cbfnc = play_end,            -- 播放完毕回调函数
}

-- 通过 BOOT 按键进行播放停止操作
local function stop_audio()
    log.info("停止播放")
    sys.sendMsg(taskName, MSG_KEY_PRESS, "STOP_AUDIO")
end

-- 按下 boot 停止播放
gpio.setup(0, stop_audio, gpio.PULLDOWN, gpio.RISING)
gpio.debounce(0, 200, 1) -- 防抖,防止频繁触发

-- 通过 POWERKEY 按键进行音量调节
local function next_audio()
    log.info("调节音量")
    sys.sendMsg(taskName, MSG_KEY_PRESS, "NEXT_AUDIO")
end


-- 按下 powerkey 调节音量
gpio.setup(gpio.PWR_KEY, next_audio, gpio.PULLUP, gpio.FALLING)
gpio.debounce(gpio.PWR_KEY, 200, 1) -- 防抖,防止频繁触发

-- 模拟音频数据获取 task
local function get_audio_data_task()
    local file_path = "/luadb/test.pcm"
    local file = io.open(file_path, "rb")
    if not file then
        log.info("文件打开失败", file_path)
        return
    }

    -- 读取文件大小
    local file_size = file:seek("end")
    file:seek("set")
    log.info("文件大小", file_size)

    -- 循环读取并写入音频数据
    local write_size = 0
    while write_size < file_size do
        local data = file:read(1024)
        if not data then
            break
        }

        -- 写入音频数据到流式播放器
        exaudio.play_stream_write(data)
        write_size = write_size + #data
        log.info("写入音频数据", write_size, "/", file_size)

        sys.wait(100) -- 控制写入速度
    }

    file:close()
    log.info("音频数据写入完成")
end

-- 主 task,处理流式播放
local index_number = 1
local function audio_task()
    log.info("开始流式音频播放")
    if exaudio.setup(audio_setup_param) then
        exaudio.play_start(audio_play_param) -- 仅支持 task 中运行

        -- 启动音频数据获取 task
        sys.taskInitEx(get_audio_data_task, "task_audio_data")

        while true do
            local msg = sys.waitMsg(taskName, MSG_KEY_PRESS) -- 等待按键触发
            if msg[2] == "NEXT_AUDIO" then
                -- 调节音量
                local volume = (index_number % 10 + 1) * 10
                exaudio.set_volume(volume)
                log.info("设置音量", volume)
                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.4 录音到文件功能(AMR 格式)

录音到文件功能支持将音频录制为 AMR 格式的文件。

注意:Air780EHM核心板+AirAUDIO_1020的环境不支持此功能。

4.4.1 功能定义

配置录音参数,开始录音并保存到指定文件,支持录音时长控制和播放。

  • 录音到文件(AMR 格式),默认保存到/sd/record.amr
  • 通过 powerkey/boot 按键开始或停止录音/播放
  • 支持 5 秒录音时长,可提前结束
  • 录音完成后自动播放录音文件
  • 实测距离3米能清晰录音,距离5米能听到声音但杂音较多。测试环境:麦克风音量100,播放音量70

4.4.2 代码示例

--[[
录音到文件功能模块(AMR格式)
核心业务逻辑:
1. Power键:开始/停止录音,停止播放
2. Boot键:开始/停止播放,停止录音
3. 录音时长5秒,可提前结束
4. 录音完成后自动播放录音文件
]]

local 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

-- SD卡配置参数
local sd_spi_id = 1            -- SPI接口编号
local sd_cs_pin = 20           -- 片选引脚
local sd_mount_path = "/sd"    -- SD卡挂载路径

-- 录音文件路径(保存到SD卡)
local recordPath = sd_mount_path .. "/record.amr"

-- 硬件配置参数
local audio_setup_param = {
    model = "es8311",          -- 音频编解码芯片类型
    i2c_id = 0,                -- I2C接口编号
    pa_ctrl = gpio.AUDIOPA_EN,             -- 音频放大器控制引脚
    dac_ctrl = 20,            -- 音频编解码芯片控制引脚

    -- 【注意:固件版本<V2026,这里单位为1ms,这里填600,否则可能第一个字播不出来】
    dac_delay = set_dac_delay,            -- DAC启动前冗余时间

    i2s_sample = 16000,         -- I2S采样率
    bits_per_sample = 16,       -- I2S录音位深
    i2s_framebit = 16           -- I2S通道位宽
}

-- 全局状态
local is_recording = false     -- 是否正在录音
local is_playing = false       -- 是否正在播放
local record_timer = nil       -- 录音计时器
local record_seconds = 0       -- 录音计时秒数

-- 音量设置
local PLAY_VOLUME = 60         -- 播放音量
local RECORD_VOLUME = 60       -- 录音麦克风音量

-- ========== 播放相关函数 ==========

-- 播放完成回调函数
local function play_end_callback(event)
    if event == exaudio.PLAY_DONE then
        log.info("播放完成")
        is_playing = false
    end
end

local audio_play_param = {
    type = 0,              -- 0=播放文件
    content = recordPath,  -- 播放录音文件
    cbfnc = play_end_callback,
    priority = 1
}

-- 开始播放录音文件
local function start_playback()
    if io.exists(recordPath) then
        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(audio_play_param)
        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, "秒")

        -- 如果达到5秒,自动停止录音
        if record_seconds >= 5 then
            stop_recording()
            log.info("录音时长已达5秒,自动停止录音")
        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("开始录音", "时长:5秒")

    -- 设置录音麦克风音量
    exaudio.mic_vol(RECORD_VOLUME)

    local audio_record_param = {
        format = exaudio.AMR_NB,  -- 使用AMR_NB格式(窄带)
        time = 5,                -- 录制5秒
        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

-- ========== 按键处理函数 ==========

-- POWERKEY键:开始/停止录音,停止播放
local function powerkey_handler()
    log.info("按下POWERKEY键")

    if is_recording then
        -- 录音中:停止录音
        log.info("正在录音中,停止录音")
        stop_recording()
    elseif is_playing then
        -- 播放中:停止播放
        log.info("正在播放中,停止播放")
        stop_playback()
    else
        -- 空闲状态:开始录音
        log.info("空闲状态,开始录音")
        start_recording()
    end
end

-- BOOT键:开始/停止播放,停止录音
local function boot_key_handler()
    log.info("按下BOOT键")

    if is_recording then
        -- 录音中:停止录音
        log.info("正在录音中,停止录音")
        stop_recording()
    elseif is_playing then
        -- 播放中:停止播放
        log.info("正在播放中,停止播放")
        stop_playback()
    else
        -- 空闲状态:播放录音
        log.info("空闲状态,播放录音")
        start_playback()
    end
end

-- ========== 音频主任务 ==========

local function main_audio_task()
    log.info("音频系统初始化")
    -- 打开ch390供电脚(使用开发板需要打开此注释)
    gpio.setup(20, 1, gpio.PULLUP) 
    --上拉ch390使用spi的cs引脚避免干扰(使用开发板需要打开此注释)
    gpio.setup(8,1)

    -- 初始化SPI接口
    spi.setup(sd_spi_id, nil, 0, 0, 8, 2000000)
    -- 设置片选引脚为高电平
    gpio.setup(sd_cs_pin, 1)

    -- 挂载SD卡,挂载失败时自动格式化
    local mount_ok, mount_err = fatfs.mount(fatfs.SPI, sd_mount_path, sd_spi_id, sd_cs_pin, 24 * 1000 * 1000)

    if mount_ok then
        log.info("SD卡挂载成功", "挂载路径:", sd_mount_path)
    else
        log.error("SD卡挂载失败", mount_err)
        recordPath = "/record.pcm"
    end

    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. Power键: 开始/停止录音,停止播放")
        log.info("2. Boot键: 开始/停止播放,停止录音")  
        log.info("3. 录音时长: 5秒,可提前结束")
        log.info("4. 录音完成后自动播放")
        log.info("5. 录音文件保存到:", recordPath)
    else
        log.error("音频硬件初始化失败")
    end
end

-- ========== 初始化设置 ==========

-- 设置POWERKEY键(开始/停止录音)
gpio.setup(gpio.PWR_KEY, powerkey_handler, gpio.PULLUP, gpio.FALLING)
gpio.debounce(gpio.PWR_KEY, 200, 1)

-- 设置BOOT键(开始/停止播放,停止录音)
gpio.setup(0, boot_key_handler, gpio.PULLDOWN, gpio.RISING)
gpio.debounce(0, 200, 1)

-- 启动音频主任务
sys.taskInit(main_audio_task)

4.5 流式录音功能(PCM 格式)

录音到文件功能支持将音频录制为 PCM 格式的文件。

注意:Air780EHM核心板+AirAUDIO_1020的环境不支持此功能。

4.5.1 功能定义

配置录音参数,开始录音并保存到指定文件,支持录音时长控制和播放。

  • 录音到文件(PCM 格式),默认保存到/sd/record.pcm
  • 通过 powerkey/boot 按键开始或停止录音/播放
  • 支持流式录音和播放
  • 支持 16kHz 采样率、16 位采样深度、有符号 PCM 数据
  • 实测距离3米能清晰录音,距离5米能听到声音但杂音较多。测试环境:麦克风音量100,播放音量70

4.5.2 代码示例

--[[
录音到文件功能模块(PCM格式)
核心业务逻辑:
1. 初始化:挂载SD卡,设置音频硬件参数
2. 录音:流式录音,实时写入SD卡,显示写入速度统计
3. 播放:流式播放,按下BOOT按键读取文件并播放
4. 状态管理:互斥控制录音/播放状态
]]

local 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

-- SD卡配置参数
local sd_spi_id = 1            -- SPI接口编号
local sd_cs_pin = 20           -- 片选引脚
local sd_mount_path = "/sd"    -- SD卡挂载路径

-- 录音文件路径(保存到SD卡)
local recordPath = sd_mount_path .. "/record.pcm"

-- 全局状态
local is_recording = false     -- 是否正在录音
local is_playing = false       -- 是否正在播放
local record_timer = nil       -- 录音计时器
local record_seconds = 0       -- 录音计时秒数

-- 音量设置
local PLAY_VOLUME = 60         -- 播放音量
local RECORD_VOLUME = 60       -- 录音麦克风音量

-- 硬件配置参数
local audio_setup_param = {
    model = "es8311",          -- 音频编解码芯片类型
    i2c_id = 0,                -- I2C接口编号
    pa_ctrl = 162,             -- 音频放大器控制引脚
    dac_ctrl = 164,            -- 音频编解码芯片控制引脚

    -- 【注意:固件版本<V2026,这里单位为1ms,这里填600,否则可能第一个字播不出来】
    dac_delay = set_dac_delay,            -- DAC启动前冗余时间

    i2s_sample = 16000,         -- I2S采样率
    bits_per_sample = 16,       -- I2S录音位深
    i2s_framebit = 16           -- I2S通道位宽
}

-- ========== 播放相关函数 ==========

-- 播放完成回调函数
local function play_end_callback(event)
    if event == exaudio.PLAY_DONE then
        log.info("播放完成")
        is_playing = false
        -- 流式播放完成后,通知多媒体通道已经没有更多数据需要播放了
        exaudio.finish()
    end
end

-- 播放设置
-- 需要注意:播放采样位深仅支持到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 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()
    if io.exists(recordPath) then
        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_playback()
    if is_playing then
        log.info("停止流式播放")
        exaudio.play_stop(audio_play_param)  -- 停止流式播放
        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, "字节")
        log.info("按下BOOT键开始播放录音文件")
        stop_record_timer()
    end
end

-- 录音设置
local audio_record_param = {
    format = exaudio.PCM_16000,  -- 使用16kHz PCM格式
    time = 5,                -- 录制5秒
    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 = end_time - start_time  -- 毫秒
                local write_speed = size / (write_time / 1000)  -- 字节/秒

                log.info("SD卡写入统计", 
                    "数据大小:", size, "字节,", 
                    "写入耗时:", string.format("%.2f", write_time), "ms,",
                    "写入速度:", string.format("%.2f", write_speed / 1024), "KB/s")

            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, "秒")

        -- 如果达到5秒,自动停止录音
        if record_seconds >= 5 then
            stop_recording()
            log.info("录音时长已达5秒,自动停止录音")
        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("开始录音", "时长:5秒")

    -- 清空旧录音文件(流式模式需要手动管理文件)
    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

-- ========== 按键处理函数 ==========

-- POWERKEY键:开始/停止录音,停止播放
local function powerkey_handler()
    log.info("按下POWERKEY键")

    if is_recording then
        -- 录音中:停止录音
        log.info("正在录音中,停止录音")
        stop_recording()
    elseif is_playing then
        -- 播放中:停止播放
        log.info("正在播放中,停止播放")
        stop_playback()
    else
        -- 空闲状态:开始录音
        log.info("空闲状态,开始录音")
        start_recording()
    end
end

-- BOOT键:开始/停止播放,停止录音
local function boot_key_handler()
    log.info("按下BOOT键")

    if is_recording then
        -- 录音中:停止录音
        log.info("正在录音中,停止录音")
        stop_recording()
    elseif is_playing then
        -- 播放中:停止播放
        log.info("正在播放中,停止播放")
        stop_playback()
    else
        -- 空闲状态:播放录音
        log.info("空闲状态,播放录音")
        start_playback()
    end
end

-- ========== SD卡挂载函数 ==========

-- 挂载SD卡
local function mount_sd_card()
    log.info("开始挂载SD卡")
    -- 打开ch390供电脚(使用开发板需要打开此注释)
    gpio.setup(20, 1, gpio.PULLUP) 
    --上拉ch390使用spi的cs引脚避免干扰(使用开发板需要打开此注释)
    gpio.setup(8,1)

    -- 初始化SPI接口
    spi.setup(sd_spi_id, nil, 0, 0, 8, 2000000)
    -- 设置片选引脚为高电平
    gpio.setup(sd_cs_pin, 1)

    -- 挂载SD卡,挂载失败时自动格式化
    local mount_ok, mount_err = fatfs.mount(fatfs.SPI, sd_mount_path, sd_spi_id, sd_cs_pin, 24 * 1000 * 1000)

    if mount_ok then
        log.info("SD卡挂载成功", "挂载路径:", sd_mount_path)

        -- 获取SD卡空间信息
        local data, err = fatfs.getfree(sd_mount_path)
        if data then
            log.info("SD卡空间信息", json.encode(data))
        else
            log.warn("获取SD卡空间信息失败", err)
        end

        return true
    else
        log.error("SD卡挂载失败", mount_err)
        return false
    end
end

-- ========== 音频主任务 ==========

local function main_audio_task()

    log.info("音频系统初始化")

    -- 先挂载SD卡
    if not mount_sd_card() then
        log.error("SD卡挂载失败,录音文件将无法保存到SD卡")
        -- 如果SD卡挂载失败,使用默认路径
        recordPath = "/record.pcm"
    else
        log.error("SD卡挂载成功!!!")
    end

    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. Power键: 开始/停止录音,停止播放")
        log.info("2. Boot键: 开始/停止播放,停止录音")  
        log.info("3. 录音时长: 5秒,可提前结束")
        log.info("4. 录音完成后自动播放")
        log.info("5. 录音文件保存到:", recordPath)
    else
        log.error("音频硬件初始化失败")
    end
end

-- ========== 初始化设置 ==========

-- 设置POWERKEY键(开始/停止录音)
gpio.setup(gpio.PWR_KEY, powerkey_handler, gpio.PULLUP, gpio.FALLING)
gpio.debounce(gpio.PWR_KEY, 200, 1)

-- 设置BOOT键(开始/停止播放,停止录音)
gpio.setup(0, boot_key_handler, gpio.PULLDOWN, gpio.RISING)
gpio.debounce(0, 200, 1)

-- 启动音频主任务
sys.taskInit(main_audio_task)

五、功能演示

5.1 音频文件播放功能演示

确保 main.lua 中保留 require "play_file" 语句,注释其他功能模块

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

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

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

5.2 文字转语音功能演示

确保 main.lua 中保留 require "play_tts" 语句,注释其他功能模块

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

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

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

5.3 流式音频播放功能演示

确保 main.lua 中保留 require "play_stream" 语句,注释其他功能模块

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

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

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

5.4 录制音频并播放功能演示(AMR 格式)

确保 main.lua 中保留 require "record_amr_file" 语句,注释其他功能模块

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

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

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

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

5.5 流式录音并播放功能演示(PCM 格式)

确保 main.lua 中保留 require "record_pcm_file" 语句,注释其他功能模块

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

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

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

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

六、总结

本文档详细展示了 Air780EHM 工业引擎音频各种功能模式的实现方法和使用场景。通过这些示例,我们可以看到:

  1. 音频文件播放功能:支持多种格式的音频文件播放,适用于音乐播放、语音提示等场景
  2. 文字转语音功能:实现中文语音合成,适用于语音播报、智能语音交互等应用
  3. 流式音频播放功能:支持实时音频数据流播放,适用于网络音频、实时通信等场景
  4. 录音到文件功能:支持高质量音频录制,适用于录音笔、语音记录等应用
  5. 流式录音到文件功能:实现实时音频数据采集,适用于语音识别、实时分析等高级应用

掌握这些音频功能模式,将为 Air780EHM 工业引擎的多媒体应用开发提供强大的支持。用户可以根据实际需求,选择合适的音频模式,实现丰富的音频处理功能。

七、常见问题

7.1 音频播放无声音问题

解决方案

  • 检查音频硬件连接是否正确
  • 确认喇叭是否工作正常
  • 检查音频配件板的供电和使能引脚配置是否正确
  • 验证 GPIO 管脚配置是否正确

7.2 TTS 播放无声音问题

解决方案

  • 确认所使用的固件是否支持 TTS 功能
  • 检查 TTS 播放流程是否正常触发

7.3 录音功能无法工作问题

解决方案

  • 确认麦克风硬件连接正确且完好
  • 检查录音参数格式设置是否支持
  • 确保存储空间充足

7.4 流式播放或录音过程中中断问题

解决方案

  • 检查数据缓冲区管理逻辑确保数据持续供给
  • 确认播放与录音的采样率及格式设置一致
  • 优化系统任务调度避免阻塞