06 通话录音
作者:拓毅恒 | 最后修改:2026-05-09
一、通话录音概述
- 通话录音:在通话过程中实时录制通话音频数据并保存到存储设备的功能,可用于通话记录保存、语音取证等场景。
- PCM 格式:原始音频数据格式,未经压缩,音质无损,但需要专用播放器播放。
- 上行数据:本地麦克风采集的声音数据,包含本地说话声音和对方声音数据。
- 下行数据:从网络接收的对方声音数据。
二、演示功能概述
本章节将演示如何使用 Air780EGH 实现呼入自动接听和通话录音功能:
- 呼入自动接听:来电响铃 2 声后自动接听
- 通话录音:通话接通后自动开始录音,对方挂断后自动停止
- 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 需要的脚本和资源文件
- 脚本和资源文件点我浏览所有文件
- 准备好软件环境之后,接下来查看如何使用 LuaTools 烧录软件,将本篇文章中演示使用的项目文件烧录到 Air780EGH 核心板中,或者查看 Air780EGH 系列整机开发板使用手册 V1.4,将本篇文章中演示使用的项目文件烧录到 Air780EGH 开发板中。
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 实现呼入自动接听和通话录音的全过程:
- 呼入自动接听:来电响铃 2 声后自动接听,无需手动操作
- 通话录音:通话接通后自动开始录音,对方挂断后自动停止
- SD 卡存储:录音文件以 PCM 格式保存到 SD 卡,支持自动挂载和空间检测
- 数据优化:只保存上行数据,避免下行数据造成的回声问题
注意事项:
- 录音文件为原始 PCM 格式,需要使用专用播放器(如 Audacity)播放
- 缓冲区大小必须是 640 的倍数,否则可能导致录音异常