第十三篇:语音端点检测后处理——连续有话段分割
在前几篇中,我们完成了语音预处理(去趋势)、信号预测延拓和峰值检测。现在我们把目光聚焦到语音信号处理的核心前端任务——端点检测(Voice Activity Detection, VAD)。通常,VAD 算法会以帧为单位输出一个 0/1 标记序列(1 表示该帧包含语音,0 表示静音或噪声),但这样的标记序列是离散且零散的,我们需要将它们聚合为连续的语音片段,以便后续逐段进行特征提取(如 MFCC)、识别或合成。findSegment.m 正是为此而生:它将一维的标记向量转换为结构体数组,清晰地给出了每个有声段的起始位置、结束位置和持续时长。本篇将深入解析其代码实现,并结合实际场景展示其用法。 
1. 端点检测背景简述
语音端点检测的目的是在一段录音中准确区分“有效语音”和“非语音”(包括静音、环境噪声、呼吸声等)。典型的 VAD 算法基于能量、过零率、频谱质心或深度学习模型,输出一个与帧数等长的逻辑向量 express,其中 express(i)=1 表示第 i 帧为语音,express(i)=0 表示非语音。
但在实际应用中,我们需要的不是帧级标记,而是连续的语音段落,因为:
- 语音识别模型通常以“句子”或“词组”为单位输入;
- 特征提取(如 LPC、MFCC)需要在连续语音段上滑动窗口;
- 后续处理(如基音跟踪、共振峰提取)依赖于段内信号的统计平稳性。
findSegment 将标记序列转换为易读的结构化数据,方便我们直接定位每一段语音的起止样本索引。
2. findSegment.m 代码逐行解析
function soundSegment = findSegment(express)
if express(1)==0
voicedIndex = find(express); % 寻找express中为1的位置
else
voicedIndex = express;
end
soundSegment = [];
k = 1;
soundSegment(k).begin = voicedIndex(1); % 设置第一组有话段的起始位置
for i = 1:length(voicedIndex)–1,
if voicedIndex(i+1) – voicedIndex(i) > 1, % 本组有话段结束
soundSegment(k).end = voicedIndex(i); % 设置本组有话段的结束位置
soundSegment(k+1).begin = voicedIndex(i+1);% 设置下一组有话段的起始位置
k = k + 1;
end
end
soundSegment(k).end = voicedIndex(end); % 最后一组有话段的结束位置
% 计算每组有话段的长度
for i = 1 : k
soundSegment(i).duration = soundSegment(i).end – soundSegment(i).begin + 1;
end
解析:
-
输入处理:函数输入 express 可以是逻辑/数值向量(帧标记)。如果 express(1)==0,表示第一帧不是语音,则使用 find(express) 提取所有 1 的索引,得到一个连续的索引列表 voicedIndex;若 express(1)==1(意味着整段都是语音,或第一帧即为语音),则直接将 express 本身作为索引列表(这里设计有些巧妙,但实际使用中 express 通常为 0/1 向量,很少直接作为索引,这个分支可能用于复用,我们暂按常规理解)。
-
初始化结构体数组:soundSegment = [],然后 k=1,定义第一个段的结构体,设置 begin 为第一个语音帧的索引。
-
循环扫描相邻索引:for i = 1:length(voicedIndex)-1,检查 voicedIndex(i+1) – voicedIndex(i) > 1。如果相差大于 1,说明这两帧之间至少有一帧为 0(静音),因此当前段到此结束,新段开始。
- 将当前段的 end 设为 voicedIndex(i);
- 将新段的 begin 设为 voicedIndex(i+1);
- k 自增,准备记录新段。
-
结束处理:循环结束后,将最后一个段的 end 设为最后一个语音帧的索引。
-
计算时长:遍历所有段,计算每段的持续时间(帧数),duration = end – begin + 1。
输出结构:soundSegment 是一个结构体数组,每个元素包含字段 begin、end 和 duration。例如 soundSegment(1).begin = 5,表示第一个语音段从第 5 帧开始。
3. 使用示例与可视化
假设我们有一段音频,通过简单的能量阈值法得到 VAD 标记(为演示,手动构造一个标记序列):
% 模拟 20 帧的标记,1 表示语音
express = [0 0 1 1 1 0 0 1 1 0 1 1 1 1 0 0 0 1 0 0];
% 调用 findSegment
seg = findSegment(express);
% 打印每个段的信息
for i = 1:length(seg)
fprintf('段 %d: 起始帧 %d, 结束帧 %d, 持续 %d 帧\\n', …
i, seg(i).begin, seg(i).end, seg(i).duration);
end
输出:
段 1: 起始帧 3, 结束帧 5, 持续 3 帧
段 2: 起始帧 8, 结束帧 9, 持续 2 帧
段 3: 起始帧 11, 结束帧 14, 持续 4 帧
段 4: 起始帧 18, 结束帧 18, 持续 1 帧
我们可以据此从原始音频中提取出各段信号:
% 假设 frames 是一个 N×帧数 的矩阵,每列为加窗后的帧数据
for i = 1:length(seg)
segFrames = frames(:, seg(i).begin : seg(i).end);
% 进行后续处理,如计算 MFCC
end
4. 与其他函数的协同
findSegment 通常出现在处理链的中间环节:
原始语音 → polydetrend(去趋势)→ 分帧加窗 → 计算 VAD 标记 → findSegment → 对每个段执行 forback_predictm(若需延拓)→ 特征提取
- 与 polydetrend:先去除趋势,VAD 阈值更稳定。
- 与 forback_predictm:在每个语音段的边界可进行延拓,避免边界效应。
- 与 findpeakm:在段内检测峰值,用于基音标记。
5. 注意事项
-
帧移和帧长:begin 和 end 是帧索引,若要对应到样本点,需乘以帧移(hop size)并适当偏移。例如采样率为 16000 Hz,帧长 25 ms,帧移 10 ms,那么第 b 帧的起始样本为 (b-1)*shift,结束样本为 (b-1)*shift + frameLen – 1。
-
第一个标记为 1 的情况:代码中若 express(1)==1,则直接将 express 本身当作 voicedIndex,但这只有在 express 不是 0/1 向量,而是直接给出语音帧索引列表时才有效。更稳健的写法是始终使用 voicedIndex = find(express ~= 0)。建议在使用时确保 express 是逻辑/数值标记。
-
短语音段:有时 VAD 会因噪声误判产生只有 1~2 帧的极短段,可根据需要设定最小长度阈值(如 ≥ 3 帧),在 findSegment 后过滤。
-
静音段保留:如果需要静音段的起止位置,可以取反标记再调用 findSegment。
6. 改进建议
若需要将段信息转换为样本索引,可编写一个包装函数:
function segSamples = segment2samples(seg, shift, frameLen)
for i = 1:length(seg)
segSamples(i).startSample = (seg(i).begin – 1) * shift + 1;
segSamples(i).endSample = (seg(i).end – 1) * shift + frameLen;
segSamples(i).durationSamples = segSamples(i).endSample – segSamples(i).startSample + 1;
end
end
7. 本讲小结
- 我们理解了帧级 VAD 标记聚合为连续语音段的必要性。
- 逐行剖析了 findSegment.m 的实现,包括结构体数组的构建和时长计算。
- 展示了使用示例及与其他预处理函数的协同方式。
- 讨论了边界条件和改进建议。
现在,您已经拥有了从原始语音到结构化段落的完整工具链。最后一篇(第十四篇)我们将把这些工具全部串联起来,完成一个从语音采集、预处理、VAD、分段、滤波到特征提取的综合实战项目,敬请期待!
📥 所有代码均已打包,点击下方链接免费获取: 下载链接
下篇预告:综合实战——从语音采集到滤波分析的系统级串联。我们将以一个真实语音文件为例,应用本系列全部工具:去趋势、端点检测、分段、设计 FIR 滤波器(结合 ideal_lp 和窗函数)、结构转换(dir2cas)、频率响应分析(freqz_m)、预测延拓(forback_predictm)以及滤波实现(casfiltr),完整体验一套专业信号处理流程。敬请期待!
网硕互联帮助中心



评论前必须登录!
注册