请添加图片描述

1. 前言

松开铃声图标,选择“窗边阅读”,然后保存。对用户来说,这是一件连续的事:把刚才确认的声音留下来。

程序却需要跨过几个边界。手指停下时,最后一次位置更新可能还在队列里;参数提交成功时,播放器可能仍在播放旧缓冲;导出开始后,用户又可能切到“街角散步”。如果保存逻辑随时读取“当前配置”,一个文件就可能混入几个时刻的选择。

上一篇,我们区分了界面编辑值、待提交值和已成功应用值。这一区分在保存时尤其有用。导出需要绑定已经确认的版本与配置修订号,让用户后续的操作不会改变正在生成的文件。

声音明信片仍由rain、page、bell三路素材组成。工程长20秒,统一为48 kHz、双声道、S16LE,每路960,000个样本帧。短素材补静音,长素材裁切,不自动循环。A是“窗边阅读”,B是“街角散步”,两版共用素材与增益基线,只改变空间安排。

本篇保存的是最后确认的固定摆位。拖动过程没有被记录成运动轨迹。因此,要比较预览和导出,应当用最终配置从头听一遍,再检查同一配置的离线产物。边拖边听的历史片段,不能直接充当这次比较的基准。

2. 功能定义

OHAudioSuite的离线编辑通过管线渲染PCM,保存文件由应用负责。离线编辑始于API 22,空间渲染节点始于API 23;它们是这个HarmonyOS 7.0工程所使用的基础能力。

实时管线使用AUDIOSUITE_PIPELINE_REALTIME_MODE,离线管线使用AUDIOSUITE_PIPELINE_EDIT_MODE。两条路径可以根据相同参数构建节点图,但各自维护读取位置与效果状态。实时路径还要向OHAudio交付播放数据,离线路径则主动获取输出并写入文件。

我会把一次导出的验收拆成四项:参数能追溯,音频数据完整,格式能够解释,复听有据可查。这样的拆分有助于定位问题。文件写入成功,回答不了“是不是选中的版本”;长度正确,也回答不了“左右声道是否放反”。

裸PCM还需要格式说明。这里每个样本帧包含左右两个16位样本,占4字节。20秒对应960,000帧,目标有效数据为3,840,000字节。如果输出长度不同,应检查供数、结束处理和效果尾部,保留原始文件分析原因。直接补零或截断,会把问题藏进一个漂亮的长度数字里。

本篇使用能够显式指定PCM格式的工具复听。WAV封装与普通播放器验证放在第07篇;修改扩展名不会补上格式信息,渲染后的双声道PCM也不等于保存了可编辑的空间对象。

还有三个结束时刻需要区分:输入素材供数结束、管线渲染结束、文件保存结束。后面的实现会分别检查它们。

3. 方案与实现

最直接的做法,是在导出代码里再写一套位置和增益。初次运行时,两套参数很容易保持一致。等到编辑器支持拖动、版本切换和恢复工程,维护成本就会出现:每加一个参数,都要记得修改另一条路径。

我更愿意让两条路径读取同一份工程配方。配方描述声音应当如何生成,运行对象保存这一次生成进行到了哪里。这个边界让配置可以序列化,也让每次导出能够从相同起点重建。

初始摆位仍沿用前文,单位为米:

声源A:窗边阅读(X,Y,Z)B:街角散步(X,Y,Z)
rain(-2,0,1)(0,0,-2)
page(0,0,1)(-1,0,1)
bell(2,0,2)(2,0,1)

坐标以听者为中心,X向右、Y向上、Z向前,每轴范围为[-5,5]。用户修改bell后,配方采用最后成功确认的坐标,不能在导出时重新套用这张初始表。

下面的C++结构表达应用自己的配方模型。素材哈希指向归一后的只读PCM;起止位置统一使用样本帧,结束位置不包含在范围内。

#include <array>
#include <cstdint>
#include <string>

struct Position {
    float x, y, z;
};

struct SourceRecipe {
    std::string id;
    std::string pcmHash;
    uint64_t beginFrame;
    uint64_t endFrame;
    float gain;
    Position position;
};

struct RenderRecipe {
    uint32_t schemaVersion;
    std::string version;  // A或B
    uint64_t revision;
    uint32_t sampleRate = 48000;
    uint32_t channels = 2;
    std::string format = "S16LE";
    uint64_t projectFrames = 960000;
    std::array<SourceRecipe, 3> sources;
};

这个模型专用于固定摆位。序列化文件还应明确固定摆位模式、声道布局、交错顺序及节点图约定。素材清单保存授权来源与补静音、裁切规则。后续加入其他空间模式时,需要扩展配方结构,避免用几个没有明确含义的可选字段容纳所有效果。

不可变性要落实到对象所有权。导出任务取得一份深拷贝快照,此后只读;编辑器继续修改自己的配置。仅给一个共享可变对象套上只读引用,不能阻止别的持有者改动它。

请添加图片描述

图05-A:实时与离线根据同一应用配方建立节点图,分别持有游标和效果状态。配方标识连接两条路径的输出记录。

保存动作也需要明确的提交边界。以下流程中的函数属于应用层,参数确认和快照创建在同一个受控执行上下文中完成:

requestExport(version, finalSequence):
    waitUntilApplied(version, finalSequence)
    if submissionFailed:
        reportFailure()
        return

    snapshot = deepCopyConfirmedRecipe(version)
    job = createExportJob(snapshot)
    enqueue(job)

等待期间不能悄悄改用较早的已应用值。创建快照后,后续手势形成新修订,导出任务继续使用已经绑定的那一份。参数接口成功也不意味着播放器里的旧数据已经播完;复听参考应从冻结配方重新生成。

请添加图片描述

图05-D:最后一次参数提交确认后,选中版本与修订号进入独立快照。新的编辑留在编辑器中,不回写正在运行的导出任务。

节点图不变:三路输入各自进入空间节点,再汇入混音和单一输出。创建离线管线前查询节点支持,逐项检查创建、连接、设参和启动结果。离线任务新增三份读取游标,从项目零点开始;预览A/B的六份游标不参与导出,效果缓存也不直接复制。

基础比较按阶段执行:先从冻结配方生成实时参考,再离线导出。这样可以先检查内容一致性。边听边导出的资源竞争应另做测试,管线数量没有超过上限,并不能说明实时预算足够。

渲染循环调用OH_AudioSuiteEngine_RenderFrame(),取得返回码、有效长度和完成标志。缓冲容量只是本次能接收多少数据,不能直接当作写入长度。

下面的应用辅助函数负责校验并写出有效区间。它放在离线工作路径,使用调用方已经打开的临时文件:

#include <cstddef>
#include <cstdio>
#include <cstdint>

bool writeBlock(FILE* file, const uint8_t* data,
                size_t capacity, int32_t validBytes,
                uint64_t& totalBytes)
{
    if (!file || !data || validBytes < 0 ||
        size_t(validBytes) > capacity || validBytes % 4 != 0) {
        return false;
    }

    size_t written = 0;
    const size_t length = size_t(validBytes);
    while (written < length) {
        const size_t n = std::fwrite(
            data + written, 1, length - written, file);
        written += n;
        totalBytes += n;
        if (n == 0 || std::ferror(file)) {
            return false;
        }
    }
    return true;
}

渲染控制层先检查接口返回码,再处理有效数据,最后判断完成标志。最后一次调用可能同时带回音频和结束信息;先退出循环,就会丢掉那段音频。零输出且尚未完成不能登记为成功,应按接口契约处理无进展情况,保留日志并限制异常重试,避免无限循环。第01篇转换器的结束协议不能直接套到这里。

请添加图片描述

图05-B:文件只接收缓冲区的有效部分。最后一次有效长度、累计样本帧数和完成标志分别记录,输入结束不直接代表文件保存成功。

写入临时文件后,还要检查刷新和关闭结果。只有渲染完成、写入完成且结构检查通过,才将产物登记为完整输出。取消、短写或渲染失败保留独立状态;重新尝试从零开始,不能向半成品继续追加。退出时按生命周期停止并释放离线资源,错误清理也不能跳过。

输出清单连接配方标识、素材哈希、PCM哈希、有效字节数、格式和任务状态。渲染完成与复听验收使用不同字段,便于判断还剩哪一步。

4. 测试

测试先固定配方,再固定运行条件。记录设备、系统、SDK、素材哈希、节点图和输出格式。实时参考与离线输出都从零开始,完整处理20秒,期间不修改空间参数。

实时参考应在明确的渲染输出边界捕获,通过有界缓冲交给非实时路径写盘,并记录采集丢失。参考文件本身缺了数据,就不能据此判断离线输出多了一段。软件PCM比较与实际播放听辨也应分别保留记录。

我会按下面的矩阵组织用例:

操作主要检查
导出初始A,再导出修改后的A修订号与bell位置对应,其他参数不变
选中B并导出产物绑定B,没有沿用A的位置
松手后立即保存最后提交得到确认,失败时明确结束任务
导出期间切换版本或继续编辑冻结配方不变,新编辑属于新修订
最后一块不足缓冲容量只写有效数据,没有旧字节或重复尾部
注入写入失败或取消不发布完整产物,重新导出从零开始
素材缺失或哈希不符准备阶段拒绝错误输入
同配方重复导出独立初始化,比较重复性并保留差异

首先检查文件长度能否被4整除,再用有效字节数除以4得到帧数,除以48,000得到秒数。20秒目标是3,840,000字节。尾帧表另外记录最后请求容量、有效长度、实际写入数和完成标志。只核对总长度,可能漏掉“一段重复、另一段丢失”的错误。

检查末尾时,先听正式素材中的可辨识内容。需要更精确定位时,另建包含末尾提示音的诊断素材组,保持相同格式、时长和节点图,记录被替换素材的哈希。诊断组仍是三路输入,不向正式作品增加第四条轨道。

左右声道可用可区分的诊断输入检查,并核对通道顺序。空间渲染会影响两侧输出,不能用“一侧必须完全静音”作为通用判断。峰值检查记录满幅样本和异常波形,未达到满幅也不能单独证明没有失真。

请添加图片描述

图05-E:身份、数据完整性和听辨逐层检查。软件输出比较与播放链路记录分开保存,避免把采集问题或设备差异归入离线渲染。

比较两份PCM时,先核对起点、帧范围、增益和通道,再分析差异。需要对齐时保留未处理的原始文件,记录偏移量;响度调整同样生成单独的听辨副本。不能把加工后更相似的结果,回填成原始文件已经通过验收。

匿名复听比较的是“选定A的实时参考”和“同配方离线输出”。用随机样本编号隐藏来源,避免与工程的A/B版本命名混淆。提前约定人数、重复次数和播放顺序,固定输出设备、路由、音量以及额外音效设置,记录响度控制方法与调整量。

记录表分别询问声源安排、末尾完整性、可闻异常与偏好,并保留“不确定”。喜欢某个样本不等于另一个有错误,听不出差别也不证明逐样本相等。任何一致性结论,都应附带对应配置与播放条件。

旋转模式留到下一篇。素材处于同一帧,并不足以证明动态效果处于同一运动位置;这需要独立的时间基准实验。

5. 结论

导出的明信片需要再听一遍,因为保存跨越了配置、渲染、文件和播放几个边界。每个边界都有自己的成功条件,也有自己的失败方式。让它们留下可关联的记录,问题就能沿着数据回到具体环节。

前四篇已经为这件事准备了素材时间基线、声源摆位、版本位置关系和最终参数确认。本篇用不可变配方把这些信息固定下来,让实时参考与离线产物拥有共同的来源。

交付目录可以保持简单:

experiment-05/
  recipe.json
  assets-manifest.json
  output/
    selected-version.pcm
    format.json
    output-manifest.json
  reference/
    realtime-reference.pcm
  records/
    render-log.csv
    tail-check.csv
    listening.csv
    environment.json
  failures/
  README.md

请添加图片描述

图05-C:工程配方、素材清单、PCM格式说明与验证记录共同组成交付。验收项关联实际样本,容器封装单独记录完成状态。

下一篇将沿用这份固定摆位基线,检查旋转模式在重播、重启和离线导出时能否保留相同的运动顺序。第07篇再将验收后的PCM封装为WAV,并检查普通播放器中的播放结果。工程配置继续独立保留,供后续修改与重建使用。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐