跳转至

03 音频编解码

作者:陈媛媛 | 最后修改:2026-05-09

一、概述

本篇文章演示的内容为: Air780EHV 核心板 +AirAUDIO_1000 音频小板,使用 codec 音频编解码核心库实现不同的音频格式相互转换功能,以适应平台格式要求,流量控制,或者播放器的要求。

二、演示功能概述

1、main.lua:主程序入口文件,加载以下 2-4 文件运行。

2、codec_g711_pcm.lua:G711 编解码并播放;

  • 使用 exaudio 流式播放原始 PCM 文件
  • 对 PCM 文件进行 G711 编码并保存
  • 对编码后的 G711 文件进行解码
  • 使用 exaudio 流式播放解码后的 PCM 数据

3、codec_mp3_to_pcm.lua:MP3 解码为 PCM 并流式播放;

  • 使用 exaudio 流式播放原始 MP3 文件
  • 对 MP3 文件进行解码得到 PCM 数据
  • 将解码后的 PCM 数据通过 exaudio 流式播放

4、codec_pcm_to_amr.lua:PCM 文件编码为 AMR_WB 并播放;

  • 使用 exaudio 播放原始 PCM 文件
  • 对 PCM 文件进行 AMR_WB 编码并保存
  • 播放编码后的 AMR_WB 文件

5、sample-6s.mp3:用于测试 MP3 解码为 PCM 并流式播放

6、test.pcm: 用于测试 pcm 文件 编码为 G711/AMR_WB 并播放

三、准备硬件环境

1、参考:Air780EHV 硬件环境清单,准备好硬件环境。

Air780EHV 核心板 +AirAUDIO_1000 音频小板 + 喇叭,点击购买

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

Air780EHV 核心板和 AirAUDIO_1000 配件板的硬件接线方式为:

Air780EHV核心板
AirAUDIO_1000配件板
3/MIC+
MIC+
4/MIC-
MIC-
5/SPK+
SPK+
6/SPK-
SPK-
19/GPIO22
PA_EN
VBAT
VCC
GND
GND

2、TYPE-C USB 数据线一根

  • Air780EHV 核心板通过 TYPE-C USB 口供电;
  • TYPE-C USB 数据线直接插到核心板的 TYPE-C USB 座子,另外一端连接电脑 USB 口;

四、准备软件环境

在开始实践本示例之前,先筹备一下软件环境:

1、 Luatools 工具

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

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

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

五、代码 API 和代码解析

5.1 代码 API

codec - 软件编解码核心库 https://docs.openluat.com/osapi/core/codec/

5.2 代码解析

5.2.1 主程序 (main.lua)

主程序文件 main.lua 是整个音频转换的入口点,负责初始化系统环境和选择功能模块。

--[[
@module  main
@summary LuatOS音频编解码演示
@version 1.0
@date    2025.11.05
@author  陈媛媛
@usage
本demo演示的核心功能为:
1、codec_mp3_to_pcm: MP3解码为PCM并流式播放
2、codec_g711_pcm: 对PCM文件进行G711编码并保存,对编码后的G711文件进行解码并播放
3、codec_pcm_to_amr: PCM编码为AMR并播放

更多说明参考本目录下的readme.md文件
]]

--[[
必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
PROJECT:项目名,ascii string类型
        可以随便定义,只要不使用,就行
VERSION:项目版本号,ascii string类型
        如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
            X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
            因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
        如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
]]


PROJECT = "codec"
VERSION = "1.0.0"

-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)


-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
if wdt then
    --配置喂狗超时时间为9秒钟
    wdt.init(9000)
    --启动一个循环定时器,每隔3秒钟喂一次狗
    sys.timerLoopStart(wdt.feed, 3000)
end

-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
--     errDump.config(true, 600)
-- end


-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
-- 可以使用合宙的iot.openluat.com平台进行远程升级
-- 也可以使用客户自己搭建的平台进行远程升级
-- 远程升级的详细用法,可以参考fota的demo进行使用


-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 音频对内存影响较大,不断的打印内存,用于判断是否异常
-- 方便分析内存使用是否有异常
sys.timerLoopStart(function()
    log.info("mem.lua", rtos.meminfo())
    log.info("mem.sys", rtos.meminfo("sys"))
end, 3000)

-- 根据需求选择需要的功能模块
 require "codec_mp3_to_pcm"     -- MP3解码为PCM并流式播放
-- require "codec_g711_pcm"           -- G711编解码演示
-- require "codec_pcm_to_amr"        -- PCM编码为AMR并播放



-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后后面不要加任何语句!!!!!

5.2.2 G711 编解码演示(codec_g711_pcm.lua)

核心功能:

  • 使用 exaudio 流式播放原始 PCM 文件
  • 对 PCM 文件进行 G711 编码并保存
  • 对编码后的 G711 文件进行解码
  • 使用 exaudio 流式播放解码后的 PCM 数据
--[[
@module codec_g711_pcm
@summary G711编解码演示
@version 13.3
@date 2025.11.16
@author 陈媛媛

本文件为G711编解码演示功能模块,核心业务逻辑为:
1、使用exaudio流式播放原始PCM文件
2、对PCM文件进行G711编码并保存
3、对编码后的G711文件进行解码
4、使用exaudio流式播放解码后的PCM数据

本文件没有对外接口,直接在main.lua中require "codec_g711"就可以加载运行;
]]

-- 使用exaudio库
local exaudio = require("exaudio")

-- 音频配置参数
local audio_setup_param = {
    model = "es8311",
    i2c_id = 0,
    pa_ctrl = gpio.AUDIOPA_EN,
    dac_ctrl = 20,
    pa_delay = 20,
    bits_per_sample = 16
}

-- 定义播放完成消息
local PLAY_COMPLETE_MSG = "AUDIO_PLAY_COMPLETE"

-- 定义播放完成回调函数
local function play_end_callback(event)
    if event == exaudio.PLAY_DONE then
        log.info("播放完成", "回调触发")
        -- 发布播放完成消息
        sys.publish(PLAY_COMPLETE_MSG)
    end
end

-- 等待播放完成的函数
local function wait_play_complete(timeout_ms)
    local result, data = sys.waitUntil(PLAY_COMPLETE_MSG, timeout_ms)

    if result then
        log.info("播放正常完成")
        return true
    else
        log.warn("等待播放完成超时", timeout_ms, "ms")
        return false
    end
end

-- 流式播放PCM文件的通用函数
local function stream_play_pcm_file(file_path, sampling_rate, sampling_depth, description)
    log.info("开始流式播放:", description)
    log.info("文件路径:", file_path)

    local stream_play_param = {
        type = 2,
        cbfnc = play_end_callback,
        sampling_rate = sampling_rate,
        sampling_depth = sampling_depth,
        signed_or_unsigned = true
    }

    -- 提前声明所有需要跨goto使用的变量
    local file_handle, total_written, chunk_count, play_success

    -- 启动播放
    if not exaudio.play_start(stream_play_param) then
        log.error("流式播放", "开始流式播放失败")
        return false
    end

    log.info("流式播放", "开始流式播放")

    local file_size = fs.fsize(file_path)
    log.info("文件大小", file_size, "字节")

    -- 打开文件 - 修改变量名
    file_handle = io.open(file_path, "rb")
    if not file_handle then
        log.error("流式播放", "无法打开文件:", file_path)
        goto CLEANUP_EXIT
    end

    -- 初始化变量(在goto标签之前)
    total_written = 0
    chunk_count = 0

    -- 文件读取和播放循环
    while true do
        local data = file_handle:read(4096)
        if not data or #data == 0 then
            break
        end

        if not exaudio.play_stream_write(data) then
            log.error("流式播放", "写入音频数据失败")
            goto CLEANUP_EXIT
        end

        chunk_count = chunk_count + 1
        total_written = total_written + #data

        if chunk_count % 10 == 0 then
            log.debug("流式播放", "写入音频数据块", chunk_count, "大小:", #data, "字节")
        end
        --写数据时不要死循环,每写一次,需要留出时间给其他task运行代码
        sys.wait(10)
    end

    -- 正常完成路径
    log.info("流式播放", "文件数据写入完成,总共", chunk_count, "块,", total_written, "字节")

    play_success = wait_play_complete(15000)

    -- 清理资源
    if file_handle then
        file_handle:close()
        file_handle = nil
        log.debug("资源清理", "文件句柄已关闭")
    end

    exaudio.play_stop()
    log.debug("资源清理", "播放器已停止")

    if play_success then
        log.info("流式播放", "文件播放完成:", description)
        return true
    else
        log.warn("流式播放", "文件播放未在预期时间内完成:", description)
        return false
    end

    -- 错误路径清理(放在函数末尾,避免作用域问题)
    ::CLEANUP_EXIT::
    if file_handle then
        file_handle:close()
        file_handle = nil
        log.debug("资源清理", "文件句柄已关闭")
    end

    exaudio.play_stop()
    log.debug("资源清理", "播放器已停止")

    return false
end

-- 解码G711文件到内存缓冲区
local function decode_g711_to_buffer(g711_file_path)
    log.info("开始完全解码G711文件:", g711_file_path)

    -- 提前声明所有需要跨goto使用的变量
    local decoder, decode_buffer, all_decoded_data, decode_count, total_decoded

    -- 创建解码器
    decoder = codec.create(codec.ALAW)
    if not decoder then
        log.error("解码", "创建G711解码器失败")
        goto CLEANUP_EXIT
    end

    result, audio_format, num_channels, sample_rate, bits_per_sample, is_signed = codec.info(decoder, g711_file_path)
    if not result then
        log.error("解码", "无法获取G711文件信息")
        goto CLEANUP_EXIT
    end

    log.info("G711文件信息", "采样率:", sample_rate, "声道数:", num_channels, "位深度:", bits_per_sample)

    -- 创建解码缓冲区
    decode_buffer = zbuff.create(16384)
    if not decode_buffer then
        log.error("解码", "创建解码缓冲区失败")
        goto CLEANUP_EXIT
    end

    -- 获取文件信息
    -- 初始化变量(在goto标签之前)
    all_decoded_data = ""
    decode_count = 0
    total_decoded = 0

    log.info("开始解码过程...")

    -- 解码循环
    while true do
        local decode_result = codec.data(decoder, decode_buffer)
        if decode_result then
            local data_size = decode_buffer:used()
            if data_size > 0 then
                decode_count = decode_count + 1
                total_decoded = total_decoded + data_size

                -- 将解码数据添加到总缓冲区
                local chunk_data = decode_buffer:toStr(0, data_size)
                all_decoded_data = all_decoded_data .. chunk_data

                if decode_count % 20 == 0 then
                    log.debug("解码", "解码数据块", decode_count, "大小:", data_size, "字节")
                    --让出CPU时间片,避免任务占用过多系统资源
                    sys.wait(1)
                end
            else
                log.info("解码", "解码完成,没有更多数据")
                break
            end
        else
            log.info("解码", "G711解码完成")
            break
        end
    end

    -- 检查是否有有效数据
    if #all_decoded_data == 0 then
        log.error("解码", "解码完成但无数据")
        goto CLEANUP_EXIT
    end

    log.info("解码完成", "总解码数据大小:", #all_decoded_data, "字节")
    log.info("解码完成", "总共解码块数:", decode_count)


    -- 资源清理(放在函数末尾)
    ::CLEANUP_EXIT::
    if decoder then
        codec.release(decoder)
        decoder = nil
        log.debug("资源清理", "解码器已释放")
    end

    if decode_buffer then
        decode_buffer:free()
        decode_buffer = nil
        log.debug("资源清理", "解码缓冲区已释放")
    end

    if #all_decoded_data > 0 then
          return all_decoded_data
    else
        log.error("解码","解码失败")
    end

end

-- 流式播放内存中的PCM数据(不使用goto避免作用域问题)
local function stream_play_pcm_data(pcm_data, sampling_rate, sampling_depth, description)
    log.info("开始流式播放内存数据:", description)
    log.info("数据大小:", #pcm_data, "字节")

    local stream_play_param = {
        type = 2,
        cbfnc = play_end_callback,
        sampling_rate = sampling_rate,
        sampling_depth = sampling_depth,
        signed_or_unsigned = true
    }

    -- 启动播放
    if not exaudio.play_start(stream_play_param) then
        log.error("流式播放", "开始流式播放失败")
        return false
    end

    log.info("流式播放", "开始流式播放内存数据")

    local total_written = 0
    local chunk_count = 0
    local chunk_size = 4096
    local success = true

    -- 内存数据播放循环
    while success and total_written < #pcm_data do
        local remaining = #pcm_data - total_written
        local current_chunk_size = math.min(chunk_size, remaining)

        local chunk_data = pcm_data:sub(total_written + 1, total_written + current_chunk_size)

        if not exaudio.play_stream_write(chunk_data) then
            log.error("内存播放", "写入音频数据失败")
            success = false
            break
        end

        chunk_count = chunk_count + 1
        total_written = total_written + current_chunk_size

        if chunk_count % 10 == 0 then
            log.debug("内存播放", "写入音频数据块", chunk_count, "大小:", #chunk_data, "字节")
        end

        ----写数据时不要死循环,每写一次,需要留出时间给其他task运行代码
        sys.wait(10)
    end

    -- 正常完成路径
    if success then
        log.info("内存播放", "数据写入完成,总共", chunk_count, "块,", total_written, "字节")

        local play_success = wait_play_complete(15000)

        -- 清理资源
        exaudio.play_stop()
        log.debug("资源清理", "播放器已停止")

        if play_success then
            log.info("内存播放", "数据播放完成:", description)
            return true
        else
            log.warn("内存播放", "数据播放未在预期时间内完成:", description)
            return false
        end
    else
        -- 资源清理
        exaudio.play_stop()
        log.debug("资源清理", "播放器已停止")
        return false
    end
end

local encoder, in_buffer, out_buffer, pcm_data, decoded_data, play_success
local pcm_file_handle = io.open("/luadb/test.pcm", "rb")  -- 修改变量名

-- G711编解码演示主函数
function demo()
    log.info("开始G711编解码测试")

    -- 提前声明所有需要跨goto使用的变量

    -- 初始化音频设备
    log.info("exaudio", "开始配置音频设备")
    if not exaudio.setup(audio_setup_param) then
        log.error("exaudio", "音频设备配置失败")
        goto FINAL_CLEANUP
    end

    log.info("exaudio", "音频设备配置成功")

    -- 设置音量
    exaudio.vol(50)

    -- 第一步:使用流式播放原始PCM文件
    log.info("第一步:流式播放原始PCM文件")
    if not stream_play_pcm_file("/luadb/test.pcm", 16000, 16, "原始PCM文件") then
        log.error("演示", "原始PCM文件播放失败,跳过后续步骤")
        goto FINAL_CLEANUP
    end

    -- 第二步:对/luadb/test.pcm进行G711编码
    log.info("第二步:对/luadb/test.pcm进行G711编码")

    encoder = codec.create(codec.ALAW, false)
    if not encoder then
        log.error("G711编码", "创建编码器失败")
        goto FINAL_CLEANUP
    end
    log.info("G711编码器创建成功")

    -- 文件操作使用局部作用域,确保及时关闭
    do

        if not pcm_file_handle then
            log.error("G711编码", "无法打开PCM文件")
            goto FINAL_CLEANUP
        end
        pcm_data = pcm_file_handle:read("*a")
        pcm_file_handle:close()
    end

    pcm_size = #pcm_data
    log.info("PCM文件大小:", pcm_size, "字节")

    in_buffer = zbuff.create(pcm_size)
    out_buffer = zbuff.create(pcm_size)

    if not in_buffer or not out_buffer then
        log.error("G711编码", "创建缓冲区失败")
        goto FINAL_CLEANUP
    end

    in_buffer:write(pcm_data)

    log.info("开始G711编码,PCM数据大小:", pcm_size, "字节")
    encode_result = codec.encode(encoder, in_buffer, out_buffer, 0)
    if not encode_result then
        log.error("G711编码失败")
        goto FINAL_CLEANUP
    end

    log.info("G711编码结果:", encode_result)

    encoded_size = out_buffer:used()
    log.info("编码成功,编码后数据大小:", encoded_size, "字节")

    encoded_data = out_buffer:toStr(0, encoded_size)
    if not io.writeFile("/aaa.g711", encoded_data) then
        log.error("保存编码文件失败")
        goto FINAL_CLEANUP
    end

    log.info("编码数据已保存到 /aaa.g711")

    -- 第三步:先完全解码,再播放
    log.info("第三步:先完全解码G711文件,再播放")

    -- 先完全解码到内存缓冲区
    decoded_data = decode_g711_to_buffer("/aaa.g711")
    if not decoded_data then
        log.error("解码", "G711文件解码失败")
        goto FINAL_CLEANUP
    end

    log.info("解码完成", "准备播放解码后的数据,大小:", #decoded_data, "字节")

    -- 使用内存数据播放
    play_success = stream_play_pcm_data(decoded_data, 16000, 16, "G711解码数据")

    if play_success then
        log.info("播放", "G711解码数据播放成功")
    else
        log.error("播放", "G711解码数据播放失败")
    end

    -- 正常完成路径
    log.info("G711编解码测试完成")

    -- 最终资源清理
    ::FINAL_CLEANUP::
    if encoder then
        codec.release(encoder)
        encoder = nil
        log.debug("资源清理", "编码器已释放")
    end

    if in_buffer then
        in_buffer:free()
        in_buffer = nil
        log.debug("资源清理", "输入缓冲区已释放")
    end

    if out_buffer then
        out_buffer:free()
        out_buffer = nil
        log.debug("资源清理", "输出缓冲区已释放")
    end

    -- 确保音频设备停止
    exaudio.play_stop()
    log.debug("资源清理", "播放器已停止")
end

-- 启动G711演示任务函数
local function start_g711_demo()
    demo()
end

-- 启动G711演示任务
sys.taskInit(start_g711_demo)

5.2.3 MP3 解码为 PCM 并流式播放演示 (codec_mp3_to_pcm.lua)

核心功能:

  • 使用 exaudio 流式播放原始 MP3 文件
  • 对 MP3 文件进行解码得到 PCM 数据
  • 将解码后的 PCM 数据通过 exaudio 流式播放
  • 等待播放完成并释放所有资源
--[[
@module codec_mp3_to_pcm
@summary MP3解码为PCM并流式播放演示
@version 2.4
@date 2025.11.18
@author 陈媛媛

本文件为MP3解码为PCM并流式播放演示功能模块,核心业务逻辑为:
1、 使用exaudio流式播放原始MP3文件
2、对MP3文件进行解码得到PCM数据
3、将解码后的PCM数据通过exaudio流式播放
4、等待播放完成并释放所有资源
本文件没有对外接口,直接在main.lua中require " codec_mp3_to_pcm"就可以加载运行;

]]

-- 使用exaudio库
local exaudio = require("exaudio")

-- 初始化exaudio音频设备
local audio_configs = {
    model = "es8311",
    i2c_id = 0,
    pa_ctrl = gpio.AUDIOPA_EN,
    dac_ctrl = 20,
    pa_delay = 20,
    bits_per_sample = 16,
    channels =2
}

-- 文件路径定义
 local MP3_FILE = "/luadb/sample-6s.mp3"
-- 定义播放完成消息
local PLAY_COMPLETE_MSG = "MP3_PLAY_COMPLETE"

-- 播放状态标志
local is_playing = false

-- 播放完成回调函数
local function play_end_callback(event)
    if event == exaudio.PLAY_DONE then
        log.info("MP3播放完成", "回调触发")
        is_playing = false
        -- 发布播放完成消息
        sys.publish(PLAY_COMPLETE_MSG)
    end
end

-- 等待播放完成的函数
local function wait_play_complete(timeout_ms)
    local result, data = sys.waitUntil(PLAY_COMPLETE_MSG, timeout_ms)

    if result then
        log.info("MP3播放正常完成")
        return true
    else
        log.warn("等待MP3播放完成超时", timeout_ms, "ms")
        return false
    end
end

-- MP3转PCM流式播放演示主函数
function demo()
    log.info("开始MP3解码为PCM并使用exaudio流式播放")

    -- 提前声明所有需要跨goto使用的变量
    local decoder, decode_buffer, play_success
    local sample_rate, bits_per_sample, num_channels, result, audio_format, channels, rate, bits, is_signed
    local audio_play_param, pre_decode_count, pre_decoded_data
    local decode_count, total_decoded  -- 将这两个变量也提前声明

    -- 使用exaudio.setup初始化音频设备
    log.info("使用exaudio.setup初始化音频设备")
    if exaudio.setup(audio_configs) then
        log.info("exaudio.setup初始化成功")
    else
        log.error("exaudio.setup初始化失败")
        goto FINAL_CLEANUP
    end

    -- 设置音量
    exaudio.vol(50)
    log.info("初始音量设置为50")

    -- 创建MP3解码器
    decoder = codec.create(codec.MP3, true)
    if not decoder then
        log.error("MP3解码器创建失败")
        goto FINAL_CLEANUP
    end

    -- 解析MP3文件信息
    result, audio_format, channels, rate, bits, is_signed = codec.info(decoder, MP3_FILE)
    if not result then
        log.error("解析MP3文件信息失败")
        goto FINAL_CLEANUP
    end

    -- 使用MP3文件的原始采样率
    sample_rate = rate
    bits_per_sample = bits
    num_channels = channels  

    log.info("MP3文件原始信息:")
    log.info("原始声道数:", channels)
    log.info("采样率:", sample_rate)
    log.info("位深度:", bits_per_sample)
    log.info("播放声道数:", num_channels)

    -- 创建解码缓冲区
    decode_buffer = zbuff.create(16384)
    if not decode_buffer then
        log.error("创建解码缓冲区失败")
        goto FINAL_CLEANUP
    end

    -- 预先解码一些数据,确保播放启动时有数据可播
    log.info("预先解码数据准备...")
    pre_decode_count = 0
    pre_decoded_data = ""

    -- 预先解码循环
    while pre_decode_count < 5 do  -- 预先解码5个块
        local decode_result = codec.data(decoder, decode_buffer, 4096)
        if decode_result then
            local data_size = decode_buffer:used()
            if data_size > 0 then
                local pcm_data = decode_buffer:toStr(0, data_size)
                pre_decoded_data = pre_decoded_data .. pcm_data
                decode_buffer:del(0, data_size)
                pre_decode_count = pre_decode_count + 1
                log.info("预先解码块", pre_decode_count, "大小:", data_size, "字节")
            else
                log.warn("预先解码数据大小为0")
                break
            end
        else
            log.warn("预先解码失败")
            break
        end
    end

    if #pre_decoded_data == 0 then
        log.error("预先解码失败,没有获得数据")
        goto FINAL_CLEANUP
    end

    -- 确保预先解码的数据是1024的倍数
    if #pre_decoded_data % 1024 ~= 0 then
        local remainder = 1024 - (#pre_decoded_data % 1024)
        pre_decoded_data = pre_decoded_data .. string.rep("\0", remainder)
    end

    log.info("预先解码完成,总数据大小:", #pre_decoded_data, "字节")

    -- 配置流式播放参数
    audio_play_param = {
        type = 2,  -- 流式播放
        cbfnc = play_end_callback,
        sampling_rate = sample_rate,
        sampling_depth = bits_per_sample,
        signed_or_unsigned = true
    }

    -- 启动流式播放
    if exaudio.play_start(audio_play_param) then
        log.info("exaudio流式播放启动成功")
        is_playing = true
    else
        log.error("exaudio流式播放启动失败")
        goto FINAL_CLEANUP
    end

    -- 立即写入预先解码的数据
    if not exaudio.play_stream_write(pre_decoded_data) then
        log.error("流式写入预先解码数据失败")
        goto FINAL_CLEANUP
    end
    log.info("预先解码数据已写入,大小:", #pre_decoded_data, "字节")

    -- 继续解码剩余数据
    log.info("开始持续解码剩余数据...")

    -- 初始化解码计数和总数据大小变量
    decode_count = pre_decode_count
    total_decoded = #pre_decoded_data

    while is_playing do
        -- 解码数据
        local decode_result = codec.data(decoder, decode_buffer, 4096)

        if decode_result then
            local data_size = decode_buffer:used()
            if data_size > 0 then
                decode_count = decode_count + 1
                total_decoded = total_decoded + data_size

                -- 将解码后的PCM数据转换为字符串
                local pcm_data = decode_buffer:toStr(0, data_size)

                -- 确保数据长度是1024的倍数(exaudio要求)
                if #pcm_data % 1024 ~= 0 then
                    local remainder = 1024 - (#pcm_data % 1024)
                    pcm_data = pcm_data .. string.rep("\0", remainder)
                end

                -- 使用exaudio流式写入音频数据
                if exaudio.play_stream_write(pcm_data) then
                    if decode_count % 10 == 0 then
                        log.info("解码并写入数据块", decode_count, "大小:", #pcm_data, "字节", "累计:", total_decoded, "字节")
                    end
                else
                    log.error("流式写入音频数据失败")
                    goto FINAL_CLEANUP
                end

                -- 清空缓冲区以便下次使用
                decode_buffer:del(0, data_size)

                -- 这个等待是必须的,用于让出CPU时间片,避免任务占用过多系统资源
                sys.wait(3)
            else
                log.info("解码完成,没有更多数据")
                break
            end
        else
            log.info("MP3解码完成")
            break
        end
    end

    log.info("MP3解码完成,总解码数据:", total_decoded, "字节")

    -- 等待播放完成
    if is_playing then
        play_success = wait_play_complete(15000)
    else
        log.warn("播放已提前结束")
        play_success = false
    end

    -- 正常完成路径
    log.info("MP3转PCM流式播放演示完成")

    -- 最终资源清理
    ::FINAL_CLEANUP::
    is_playing = false

    if decoder then
        codec.release(decoder)
        decoder = nil
        log.debug("资源清理", "MP3解码器已释放")
    end

    if decode_buffer then
        decode_buffer:free()
        decode_buffer = nil
        log.debug("资源清理", "解码缓冲区已释放")
    end

    -- 确保音频设备停止
    exaudio.play_stop()
    log.debug("资源清理", "播放器已停止")

    return play_success or false
end

-- 启动MP3演示任务函数
local function start_mp3_to_pcm_demo()
    demo()
end

-- 启动MP3演示任务
sys.taskInit(start_mp3_to_pcm_demo)

5.2.4 PCM 编码为 AMR_WB 并播放 (codec_pcm_to_amr)

核心功能:

  • 使用 exaudio 播放原始 PCM 文件
  • 对 PCM 文件进行 AMR_WB 编码并保存
  • 播放编码后的 AMR_WB 文件
  • 等待播放完成并释放所有资源
--[[
@module  codec_pcm_to_amr
@summary PCM编码为AMR_WB并播放
@version 2.3
@date    2025.11.18
@author  陈媛媛
@usage
本文件为PCM编码为AMR_WB功能模块,核心业务逻辑为:
1、使用exaudio播放原始PCM文件
2、 对PCM文件进行AMR_WB编码并保存
3、播放编码后的AMR_WB文件
4、等待播放完成并释放所有资源

本文件没有对外接口,直接在main.lua中require "codec_pcm_to_amr"就可以加载运行;
]]

local exaudio = require("exaudio")

-- 音频初始化设置参数
local audio_setup_param = {
    model = "es8311",
    i2c_id = 0,
    pa_ctrl = gpio.AUDIOPA_EN,
    dac_ctrl = 20,
    pa_delay = 20,
    bits_per_sample = 16,
    pa_on_level = 1
}

-- 文件路径定义
local PCM_FILE = "/luadb/test.pcm"
local AMR_WB_OUTPUT_FILE = "/encoded.amr.wb"

-- 定义播放完成消息
local PLAY_COMPLETE_MSG = "AMR_WB_PLAY_COMPLETE"

-- 播放完成回调
local function play_end_callback(event)
    if event == exaudio.PLAY_DONE then
        log.info("AMR_WB播放完成", "回调触发")
        sys.publish(PLAY_COMPLETE_MSG)
    end
end

-- 等待播放完成的函数
local function wait_play_complete(timeout_ms)
    local result, data = sys.waitUntil(PLAY_COMPLETE_MSG, timeout_ms)

    if result then
        log.info("AMR_WB播放正常完成")
        return true
    else
        log.warn("等待AMR_WB播放完成超时", timeout_ms, "ms")
        return false
    end
end

-- PCM转AMR_WB编码并播放演示主函数
function demo()
    log.info("开始PCM转AMR_WB编码并播放演示")

    -- 提前声明所有需要跨goto使用的变量
    local encoder, in_buffer, out_buffer, pcm_file, pcm_data, encode_result
    local encoded_size, encoded_data, play_success, file_check, pcm_size, amr_wb_size
    local audio_play_param

    -- 初始化音频设备
    log.info("初始化音频设备")
    if not exaudio.setup(audio_setup_param) then
        log.error("音频设备初始化失败")
        goto FINAL_CLEANUP
    end

    -- 设置音量
    exaudio.vol(60)
    log.info("音量设置为60")

    -- 检查PCM文件是否存在
    file_check = io.open(PCM_FILE, "r")
    if not file_check then
        log.error("PCM文件不存在:", PCM_FILE)
        goto FINAL_CLEANUP
    end
    file_check:close()

    pcm_size = fs.fsize(PCM_FILE)
    log.info("原始PCM文件大小:", pcm_size, "字节")

    -- 创建AMR_WB编码器
    encoder = codec.create(codec.AMR_WB, false, 4)
    if not encoder then
        log.error("AMR_WB编码器创建失败")
        goto FINAL_CLEANUP
    end
    log.info("AMR_WB编码器创建成功")

    -- 读取PCM文件
    pcm_file = io.open(PCM_FILE, "rb")
    if not pcm_file then
        log.error("无法打开PCM文件:", PCM_FILE)
        goto FINAL_CLEANUP
    end

    pcm_data = pcm_file:read("*a")
    pcm_file:close()
    log.info("实际读取的PCM数据大小:", #pcm_data, "字节")

    -- 创建输入和输出缓冲区
    in_buffer = zbuff.create(#pcm_data)
    out_buffer = zbuff.create(#pcm_data)

    if not in_buffer or not out_buffer then
        log.error("创建缓冲区失败")
        goto FINAL_CLEANUP
    end

    -- 将PCM数据写入输入缓冲区
    in_buffer:write(pcm_data)

    -- 执行AMR_WB编码
    log.info("开始AMR_WB编码")
    encode_result = codec.encode(encoder, in_buffer, out_buffer, 4)

    if not encode_result then
        log.error("AMR_WB编码失败")
        goto FINAL_CLEANUP
    end

    encoded_size = out_buffer:used()
    log.info("AMR_WB编码成功,编码后数据大小:", encoded_size, "字节")

    -- 保存编码后的AMR_WB数据到文件
    encoded_data = out_buffer:toStr(0, encoded_size)

    -- 添加AMR_WB文件头
    encoded_data = "#!AMR-WB\n" .. encoded_data

    -- 写入文件
    if not io.writeFile(AMR_WB_OUTPUT_FILE, encoded_data) then
        log.error("保存AMR_WB文件失败")
        goto FINAL_CLEANUP
    end

    amr_wb_size = fs.fsize(AMR_WB_OUTPUT_FILE)
    log.info("AMR_WB文件保存成功:", AMR_WB_OUTPUT_FILE, "大小:", amr_wb_size, "字节")

    -- 配置音频播放参数
    audio_play_param = {
        type = 0,  -- 文件播放
        content = AMR_WB_OUTPUT_FILE,
        cbfnc = play_end_callback
    }

    -- 开始播放
    log.info("开始播放AMR_WB文件")
    if not exaudio.play_start(audio_play_param) then
        log.error("AMR_WB文件播放失败")
        goto FINAL_CLEANUP
    end

    log.info("AMR_WB文件开始播放")

    -- 等待播放完成
    play_success = wait_play_complete(30000)

    -- 正常完成路径
    log.info("PCM转AMR_WB编码并播放演示完成")

    -- 最终资源清理
    ::FINAL_CLEANUP::
    if encoder then
        codec.release(encoder)
        encoder = nil
        log.debug("资源清理", "AMR_WB编码器已释放")
    end

    if in_buffer then
        in_buffer:free()
        in_buffer = nil
        log.debug("资源清理", "输入缓冲区已释放")
    end

    if out_buffer then
        out_buffer:free()
        out_buffer = nil
        log.debug("资源清理", "输出缓冲区已释放")
    end

    -- 确保音频设备停止
    exaudio.play_stop()
    log.debug("资源清理", "播放器已停止")

    return play_success or false
end

-- 启动AMR_WB演示任务函数
local function start_pcm_to_amr_wb_demo()
    demo()
end

-- 启动AMR_WB演示任务
sys.taskInit(start_pcm_to_amr_wb_demo)

六、运行结果展示

6.1 G711 编解码演示(codec_g711_pcm.lua)

6.2 MP3 解码为 PCM 并流式播放演示 (codec_mp3_to_pcm.lua)

6.3 PCM 编码为 AMR_WB 并播放 (codec_pcm_to_amr)

七、常见问题

7.1 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扩展库介绍文档。

八、总结

至此,我们已经完整演示了使用 codec 核心库实现不同的音频格式相互转换的全过程,快来实际操作一下吧!