跳转至

06 通话录音

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

一、通话录音概述

  • 通话录音:在通话过程中实时录制通话音频数据并保存到存储设备的功能,可用于通话记录保存、语音取证等场景。
  • PCM 格式:原始音频数据格式,未经压缩,音质无损,但需要专用播放器播放。
  • 上行数据:本地麦克风采集的声音数据,包含本地说话声音和对方声音数据。
  • 下行数据:从网络接收的对方声音数据。

二、演示功能概述

本章节将演示如何使用 Air780EGH 实现呼入自动接听和通话录音功能:

  1. 呼入自动接听:来电响铃 2 声后自动接听
  2. 通话录音:通话接通后自动开始录音,对方挂断后自动停止
  3. SD 卡存储:录音文件保存到 SD 卡,支持自动挂载和空间检测

三、准备硬件环境

参考:Air780EGH 硬件环境清单,准备好硬件环境。以下两种环境,任选一种即可。

3.1 Air780EGH 整机开发板

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

由于通话录音文件大小一般都比较大,无法存入内存中,所以测试此 demo 必须接入 SD 卡。

3.2 Air780EGH 核心板 + AirAUDIO_1010 配件板 + AirMICROSD_1010 配件板

Air780EGH 核心板集成了移动网络通信功能。

AirAUDIO_1010 配件板

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

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

Air780EGH核心板
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

AirMICROSD_1010 配件板

由于通话录音文件大小一般都比较大,无法存入内存中,所以测试此 demo 必须接入 SD 卡。使用核心板测试需要外挂 AirMICROSD_1010 配件板来使用 SD 卡

Air780EGH 核心板和 AirMICROSD_1010 配件板的硬件接线方式为:

Air780EGH核心板
AirMICROSD_1010
GND(任意)
GND
VDD_EXT
3V3
GPIO8/SPI0_CS
spi_cs
SPI0_CLK
spi_clk,时钟
SPI0_MOSI
spi_mosi,主机输出,从机输入
SPI0_MISO
spi_miso,主机输入,从机输出

四、准备软件环境

4.1 工具 + 内核固件 + 脚本

1、烧录工具 Luatools

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

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

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

4.2 API 介绍

这里仅介绍本篇文档所使用的 API,详情请查看:API - cc

cc.record(enable, up1, up2, down1, down2)

启用或关闭通话录音功能

cc.on(event, callback)

注册通话事件回调函数

cc.lastNum()

获取来电号码

cc.accept(id)

接听来电

fatfs.mount(type, path, spi_id, cs_pin, speed)

挂载 SD 卡

五、代码演示

5.1 通话录音应用

--[[
@module  cc_record_save
@summary 通话录音功能模块
@version 1.0
@date    2026.3.10
@author  拓毅恒
@usage
本模块提供以下功能:
1. 实现呼入自动接听功能(响2声后自动接听)
2. 支持通话录音功能,保存为PCM格式
3. 录音文件仅支持SD卡存储

功能说明:
- 来电自动接听:响铃2声后自动接听来电
- 通话录音:自动开始录音,对方挂断后停止录音

录音功能特性:
- 录音文件保存为PCM格式:/sd/record_call.pcm
- 只保存上行数据(包含本地声音和网络回声)
- 下行数据自动跳过,避免重复存储
- 支持SD卡自动挂载和空间检测

注意事项:
1. 本模块仅支持呼入自动接听功能
2. 录音文件仅支持SD卡存储,必须插入SD卡才能使用录音功能
3. 录音文件为原始PCM格式,需要专用播放器播放
]]

-- 引入音频设备模块
local audio_drv = require "audio_drv"
local exaudio = require "exaudio"

-- ====================== 配置区域 ======================

-- 全局状态变量
local call_counter = 0                 -- 响铃计数器
local caller_number = ""               -- 来电号码

-- SD卡挂载路径和录音文件保存路径
local SD_MOUNT_PATH = "/sd"
local RECORD_FILE_PATH = SD_MOUNT_PATH .. "/record_call.pcm"  -- 录音文件路径
-- Air780EGH整机开发板上TF卡的的pin_cs为gpio16,spi_id为0.请根据实际硬件修改
local spi_id = 0
local pin_cs = 16
-- Air780EGH核心板CS为IO8
-- local pin_cs = 8

-- 录音功能相关函数
local is_recording_to_file = false  -- 录音状态标志:true表示正在录音到文件
local record_file = nil                -- 录音文件句柄
local record_start_time = 0            -- 录音开始时间戳(毫秒)
local record_duration = 0              -- 录音时长(秒)

-- 注意:缓冲区大小必须是640的倍数
local BUFFER_SIZE = 48000  -- 缓冲区大小不能太小,否则保存过程中有可能会溢出造成死机

-- ====================== sd卡挂载函数 ======================

-- 挂载SD卡
local function mount_sd_card()
    log.info("SD卡", "开始挂载SD卡")

    -- 检查SD卡是否已挂载
    if io.exists(SD_MOUNT_PATH) then
        log.info("SD卡", "SD卡已挂载:", SD_MOUNT_PATH)
        return true
    end

    -- 初始化SPI接口
    -- 打开ch390供电脚(使用开发板需要打开此注释)
    gpio.setup(20, 1, gpio.PULLUP) 
    --上拉ch390使用spi的cs引脚避免干扰(使用开发板需要打开此注释)
    gpio.setup(8,1)

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

    -- 尝试挂载SD卡
    local mount_ok, mount_err = fatfs.mount(fatfs.SPI, SD_MOUNT_PATH, spi_id, pin_cs, 24 * 1000 * 1000)

    if mount_ok then
        log.info("SD卡", "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卡", "SD卡挂载失败:", mount_err)
        return false
    end
end

-- ====================== 录音功能 ======================

-- 创建音频数据缓冲区
local up1 = zbuff.create(BUFFER_SIZE,0)      -- 上行数据保存区1
local up2 = zbuff.create(BUFFER_SIZE,0)      -- 上行数据保存区2
local down1 = zbuff.create(BUFFER_SIZE,0)    -- 下行数据保存区1
local down2 = zbuff.create(BUFFER_SIZE,0)    -- 下行数据保存区2

-- 打开录音文件
local function open_record_file()
    -- 先挂载SD卡
    if not mount_sd_card() then
        log.error("录音文件", "SD卡挂载失败,无法进行录音")
        return false
    end

    log.info("录音文件", "SD卡挂载成功,录音文件将保存到SD卡")

    -- 关闭已打开的文件
    if record_file then
        record_file:close()
        record_file = nil
    end

    -- 删除旧录音文件
    if io.exists(RECORD_FILE_PATH) then
        os.remove(RECORD_FILE_PATH)
        log.info("录音文件", "删除旧录音文件:", RECORD_FILE_PATH)
    end

    -- 创建录音文件
    record_file = io.open(RECORD_FILE_PATH, "wb")

    if record_file then
        log.info("录音文件", "创建录音文件成功:", RECORD_FILE_PATH)
        record_start_time = mcu.ticks()
        is_recording_to_file = true
        return true
    else
        log.error("录音文件", "创建录音文件失败:", RECORD_FILE_PATH)
        return false
    end
end

-- 关闭录音文件
local function close_record_file()
    if record_file then
        record_file:close()
        record_file = nil

        local file_size = io.fileSize(RECORD_FILE_PATH)
        record_duration = (mcu.ticks() - record_start_time) / 1000  -- 转换为秒

        log.info("录音文件", "录音完成", "文件大小:", file_size, "字节", "录音时长:", string.format("%.1f", record_duration), "秒", "路径:", RECORD_FILE_PATH)

        is_recording_to_file = false
        record_start_time = 0
        record_duration = 0
    end
end

-- 写入录音数据到文件
local function write_record_data(buff, is_downlink)
    if not record_file or not is_recording_to_file then
        return false
    end

    -- 保存数据
    if not is_downlink then
        local data_size = buff:used()
        if data_size > 0 then
            local start_time = mcu.ticks()

            -- 写入数据到文件
            record_file:write(buff:query())

            local end_time = mcu.ticks()
            local write_time = end_time - start_time
            local write_speed = data_size / (write_time / 1000)  -- 字节/秒

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

            return true
        end
    else
        -- 下行数据不保存,只记录日志
        -- 写入下行数据会导致文件内有回声
        local data_size = buff:used()
        if data_size > 0 then
            log.info("录音写入", "下行数据跳过", "数据大小:", data_size, "字节")
        end
    end
    return false
end

-- 音频数据回调函数
local function recordCallback(is_dl, point)
    if is_dl then
        log.info("录音", "下行数据,位于缓存", point+1, "缓存1数据量", down1:used(), "缓存2数据量", down2:used())

        -- 处理下行数据
        if point == 0 then
            write_record_data(down1, true)
            down1:del()  -- 清空缓冲区
        else
            write_record_data(down2, true)
            down2:del()  -- 清空缓冲区
        end
    else
        log.info("录音", "上行数据,位于缓存", point+1, "缓存1数据量", up1:used(), "缓存2数据量", up2:used())

        -- 处理上行数据
        if point == 0 then
            write_record_data(up1, false)
            up1:del()  -- 清空缓冲区
        else
            write_record_data(up2, false)
            up2:del()  -- 清空缓冲区
        end
    end
    log.info("通话质量", cc.quality())
end

-- 启用通话录音
local function enableRecording()
    cc.record(true, up1, up2, down1, down2)
    cc.on("record", recordCallback)
    log.info("cc_app", "通话录音已启用")
end

-- 开始通话录音到文件
local function start_call_recording()
    if open_record_file() then
        log.info("通话录音", "开始录音到文件:", RECORD_FILE_PATH)
        return true
    else
        log.error("通话录音", "无法开始录音到文件,请检查SD卡")
        return false
    end
end

-- 停止通话录音到文件
local function stop_call_recording()
    close_record_file()
    log.info("通话录音", "停止录音到文件")
end

-- 获取所有缓冲区
local function getRecordingBuffers()
    return {
        up1 = up1,
        up2 = up2,
        down1 = down1,
        down2 = down2
    }
end

-- 获取录音文件信息
local function get_record_file_info()
    if io.exists(RECORD_FILE_PATH) then
        local file_size = io.fileSize(RECORD_FILE_PATH)
        return {
            path = RECORD_FILE_PATH,
            size = file_size,
            duration = record_duration,
            exists = true
        }
    else
        return {
            path = RECORD_FILE_PATH,
            size = 0,
            duration = 0,
            exists = false
        }
    end
end

-- 呼入自动接听,等待对方挂断
local function handle_scenario(status)
    if status == "INCOMINGCALL" then
        -- 获取来电号码
        caller_number = cc.lastNum() or "未知号码"
        call_counter = call_counter + 1

        log.info("收到来电,号码:", caller_number, "响铃次数:", call_counter)

        -- 响铃2声后自动接听
        if call_counter >= 2 then
            log.info("自动接听来电")
            cc.accept(0)
            call_counter = 0  -- 重置计数器
        end
    elseif status == "SPEECH_START" then
        -- 语音通话真正开始
        log.info("电话已接通,电话号码:", caller_number)

        -- 开始通话录音到文件
        start_call_recording()
    elseif status == "DISCONNECTED" then
        -- 对方挂断通话
        log.info("通话结束对方挂断")

        -- 停止通话录音到文件
        stop_call_recording()

        call_counter = 0  -- 重置计数器
    end
end

-- ====================== 主事件处理器 ======================
sys.subscribe("CC_IND", function(status)
    log.info("CC状态", status)
    handle_scenario(status)

    -- 需要处理的通用状态
    if status == "READY" then
        sys.publish("CC_READY")  -- 发布系统就绪事件
    elseif status == "HANGUP_CALL_DONE" or status == "MAKE_CALL_FAILED" or status == "DISCONNECTED" then
        exaudio.pm(audio.SHUTDOWN)   --主动进入低功耗模式
    end
end)

-- ====================== 电话系统初始化 ======================
local function init_cc()
    -- 先尝试挂载SD卡
    mount_sd_card()

    -- 初始化音频设备
    audio_drv.initAudioDevice()

    -- 等待电话系统就绪
    sys.waitUntil("CC_READY")

    -- 初始化电话功能
    cc.init(audio_drv.getMultimediaId())

    -- 启用通话录音(录音功能在cc_app中)
    enableRecording()

    log.info("cc_app", "电话系统初始化完成")
end

-- 启动初始化任务
sys.taskInit(init_cc)

六、功能演示

6.1 选择功能

首先注释掉 main.lua 中默认通话功能模块,打开通话录音功能模块

6.2 烧录代码

使用 Luatools 将代码烧录到 Air780EGH 开发板或核心板中

6.3 SD 卡挂载成功

烧录完毕后,系统会自动初始化并尝试挂载 SD 卡,日志中会显示 SD 卡挂载成功信息

6.4 电话系统初始化完成

等待电话系统初始化完成,显示"CC_READY"状态

6.5 来电响铃

当有来电时,日志会显示来电号码和响铃次数

6.6 自动接听

响铃 2 声后自动接听来电,显示"SPEECH_START"状态

6.7 开始录音

通话接通后自动开始录音,日志显示录音文件创建成功

6.8 录音进行中

通话过程中,日志会实时显示录音数据写入信息,包括数据大小、写入耗时和写入速度

6.9 通话结束

对方挂断后,通话结束,日志显示"DISCONNECTED"状态和录音完成信息

6.10 查看录音文件

录音文件保存在 SD 卡的 /sd/record_call.pcm 路径下,可以通过读卡器在电脑上查看

音频文件需要使用专用播放器(如 Audacity)播放

七、 总结

至此,我们演示了使用 Air780EGH 实现呼入自动接听和通话录音的全过程:

  1. 呼入自动接听:来电响铃 2 声后自动接听,无需手动操作
  2. 通话录音:通话接通后自动开始录音,对方挂断后自动停止
  3. SD 卡存储:录音文件以 PCM 格式保存到 SD 卡,支持自动挂载和空间检测
  4. 数据优化:只保存上行数据,避免下行数据造成的回声问题

注意事项

  • 录音文件为原始 PCM 格式,需要使用专用播放器(如 Audacity)播放
  • 缓冲区大小必须是 640 的倍数,否则可能导致录音异常