Browse Source
六轮真机定位出四个叠加的原因,每修掉一个概率降一档: 1. 发包定时器系统性漂移:scheduleWithFixedDelay 从任务**结束**起算,WSOLA+G.722+ 写 socket 的 1~3ms 全叠进周期,"20ms"实测 21~23ms、只有 0.9 倍速。改成 scheduleAtFixedRate 固定节拍且永不重排。 2. 起播缓冲在句中重新生效(杂音落在句中的真凶):队列空了就要求再攒 120ms,而 变速追赶时队列中途归零很常见,等于句中硬插 120ms 静音。加 playing 标志让 起播缓冲只在句首用一次;真正的断粮单独计数并打「句中空洞」日志。 3. 恢复发包时从空缓冲起播(杂音落在句首):两路都空就停发、耳机缓冲被榨干。 现在先塞 4 包编码静音垫底再发真音频,两路空后再续 500ms 静音才停。 4. 24k→16k 重采样是逐块独立的线性插值、无抗混叠、每块相位从零重算。换成 64 阶低通 FIR + 跨块连续(Resampler24kTo16k.kt/.swift,Python 复刻校验 1k/3k/6k 正弦)。 顺带:耳机 0xD6 帧 data[6] 的语义是「补一包」不是「改速率」,按此重写 onHeadsetPace(01 补 1 包、00 补 2 包、03 扣 1 包);空闲那一路的静音帧改用该路 自己的编码器编零(G.722 是自适应差分,固定码流会让解码器状态跳);裁静音加 滞回(300 进 / 600 出)+ 3ms 淡入;速率每帧最多变 0.03;150ms 内有新数据不补零 收尾;HARD_CAP_SEC 补回 30s 纯兜底。DEEPVOICE_PARITY 对照开关保持 false。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>main
9 changed files with 573 additions and 119 deletions
@ -0,0 +1,73 @@ |
|||
package com.yunqiinnovation.azure_speech |
|||
|
|||
/** |
|||
* 24 kHz → 16 kHz 重采样(比 3:2),**跨块连续、带抗混叠低通**。 |
|||
* |
|||
* 2026-09-23 之前用的是逐块独立的线性插值:每块相位从 0 重算、没有滤波, |
|||
* 8 kHz 以上的内容折回带内,块与块之间还有微小跳变——通话翻译耳机里"偶尔杂音"的嫌疑之一。 |
|||
* |
|||
* 实现:零插值到 48 kHz → 64 阶窗函数 sinc 低通(截止 7.4 kHz,Hamming)→ 每 3 个取 1 个。 |
|||
* 只在 (k-j) 为偶数的抽头上做乘加(零插值的奇数位恒为 0),每个输出样本 32 次乘加。 |
|||
* 系数已含 ×2 的零插值增益。Python 复刻校验:1k/3k/6k 正弦幅度 0.98/0.98/0.93,频率不变。 |
|||
* |
|||
* 一个实例只服务一路(有状态:保留最后 L/2 个输入样本与 48k 网格上的相位)。 |
|||
*/ |
|||
class Resampler24kTo16k { |
|||
private val hist = ShortArray(HALF) // 上一块的尾巴(最近 HALF 个输入样本,旧→新) |
|||
private var histLen = 0 |
|||
private var k = 0L // 下一个输出对应的 48k 网格位置(全局) |
|||
private var consumed = 0L // 已进入 hist 之前的输入总样本数(全局索引基准) |
|||
|
|||
fun reset() { histLen = 0; k = 0L; consumed = 0L } |
|||
|
|||
/** 输入 16bit 小端 PCM @24k,返回 16bit 小端 PCM @16k。 */ |
|||
fun process(input: ByteArray): ByteArray { |
|||
val n = input.size / 2 |
|||
if (n == 0) return ByteArray(0) |
|||
// 拼成 [hist | new],下标 i 对应全局输入索引 base + i |
|||
val buf = ShortArray(histLen + n) |
|||
System.arraycopy(hist, 0, buf, 0, histLen) |
|||
var i = 0 |
|||
while (i < n) { |
|||
buf[histLen + i] = ((input[i * 2].toInt() and 0xFF) or (input[i * 2 + 1].toInt() shl 8)).toShort() |
|||
i++ |
|||
} |
|||
val base = consumed - histLen |
|||
val lastGlobal = consumed + n - 1 // 当前可用的最大全局输入索引 |
|||
val out = java.io.ByteArrayOutputStream(n * 2 * 2 / 3 + 4) |
|||
// 输出 m 对应 48k 网格 k=3m;需要输入索引 (k-j)/2,j∈[0,L),最大为 k/2(要求 ≤ lastGlobal) |
|||
while (k / 2 <= lastGlobal) { |
|||
var acc = 0.0 |
|||
var j = if (k % 2 == 0L) 0 else 1 // 让 (k-j) 为偶数 |
|||
while (j < TAPS) { |
|||
val idx = (k - j) / 2 - base |
|||
if (idx >= 0 && idx < buf.size) acc += H[j] * buf[idx.toInt()] |
|||
j += 2 |
|||
} |
|||
val v = acc.toInt().coerceIn(-32768, 32767) |
|||
out.write(v and 0xFF); out.write((v shr 8) and 0xFF) |
|||
k += 3 |
|||
} |
|||
// 留下最后 HALF 个输入样本供下一块的抽头引用 |
|||
val keep = minOf(HALF, buf.size) |
|||
System.arraycopy(buf, buf.size - keep, hist, 0, keep) |
|||
histLen = keep |
|||
consumed += n |
|||
return out.toByteArray() |
|||
} |
|||
|
|||
companion object { |
|||
private const val TAPS = 64 |
|||
private const val HALF = TAPS / 2 + 2 |
|||
private val H = doubleArrayOf( |
|||
-0.001272, -0.001643, -0.000571, 0.001391, 0.002686, 0.001673, -0.001674, -0.004812, |
|||
-0.004142, 0.001400, 0.007913, 0.008684, 0.000477, -0.011471, -0.015874, -0.005303, |
|||
0.014505, 0.026149, 0.014823, -0.015487, -0.040008, -0.031737, 0.011975, 0.058826, |
|||
0.062003, 0.001164, -0.088277, -0.126869, -0.043596, 0.165759, 0.419929, 0.593380, |
|||
0.593380, 0.419929, 0.165759, -0.043596, -0.126869, -0.088277, 0.001164, 0.062003, |
|||
0.058826, 0.011975, -0.031737, -0.040008, -0.015487, 0.014823, 0.026149, 0.014505, |
|||
-0.005303, -0.015874, -0.011471, 0.000477, 0.008684, 0.007913, 0.001400, -0.004142, |
|||
-0.004812, -0.001674, 0.001673, 0.002686, 0.001391, -0.000571, -0.001643, -0.001272, |
|||
) |
|||
} |
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
import Foundation |
|||
|
|||
/// 24 kHz → 16 kHz 重采样(比 3:2),**跨块连续、带抗混叠低通**。Android `Resampler24kTo16k.kt` 的对应实现。 |
|||
/// |
|||
/// 2026-09-23 之前用的是逐块独立的线性插值:每块相位从 0 重算、没有滤波, |
|||
/// 8 kHz 以上的内容折回带内,块与块之间还有微小跳变——通话翻译耳机里"偶尔杂音"的嫌疑之一。 |
|||
/// |
|||
/// 实现:零插值到 48 kHz → 64 阶窗函数 sinc 低通(截止 7.4 kHz,Hamming)→ 每 3 个取 1 个。 |
|||
/// 只在 (k-j) 为偶数的抽头上做乘加,每个输出样本 32 次乘加。系数已含 ×2 的零插值增益。 |
|||
/// 一个实例只服务一路(有状态)。 |
|||
final class Resampler24kTo16k { |
|||
private static let taps = 64 |
|||
private static let half = taps / 2 + 2 |
|||
private static let h: [Double] = [ |
|||
-0.001272, -0.001643, -0.000571, 0.001391, 0.002686, 0.001673, -0.001674, -0.004812, |
|||
-0.004142, 0.001400, 0.007913, 0.008684, 0.000477, -0.011471, -0.015874, -0.005303, |
|||
0.014505, 0.026149, 0.014823, -0.015487, -0.040008, -0.031737, 0.011975, 0.058826, |
|||
0.062003, 0.001164, -0.088277, -0.126869, -0.043596, 0.165759, 0.419929, 0.593380, |
|||
0.593380, 0.419929, 0.165759, -0.043596, -0.126869, -0.088277, 0.001164, 0.062003, |
|||
0.058826, 0.011975, -0.031737, -0.040008, -0.015487, 0.014823, 0.026149, 0.014505, |
|||
-0.005303, -0.015874, -0.011471, 0.000477, 0.008684, 0.007913, 0.001400, -0.004142, |
|||
-0.004812, -0.001674, 0.001673, 0.002686, 0.001391, -0.000571, -0.001643, -0.001272, |
|||
] |
|||
|
|||
private var hist = [Int16](repeating: 0, count: Resampler24kTo16k.half) |
|||
private var histLen = 0 |
|||
private var k: Int64 = 0 // 下一个输出对应的 48k 网格位置(全局) |
|||
private var consumed: Int64 = 0 // 已进入 hist 之前的输入总样本数(全局索引基准) |
|||
|
|||
func reset() { histLen = 0; k = 0; consumed = 0 } |
|||
|
|||
/// 输入 16bit 小端 PCM @24k,返回 16bit 小端 PCM @16k。 |
|||
func process(_ input: Data) -> Data { |
|||
let n = input.count / 2 |
|||
if n == 0 { return Data() } |
|||
var buf = [Int16](repeating: 0, count: histLen + n) |
|||
for i in 0..<histLen { buf[i] = hist[i] } |
|||
input.withUnsafeBytes { (raw: UnsafeRawBufferPointer) in |
|||
for i in 0..<n { |
|||
let v = UInt16(raw[i * 2]) | (UInt16(raw[i * 2 + 1]) << 8) |
|||
buf[histLen + i] = Int16(bitPattern: v) |
|||
} |
|||
} |
|||
let base = consumed - Int64(histLen) |
|||
let lastGlobal = consumed + Int64(n) - 1 |
|||
var out = [Int16]() |
|||
out.reserveCapacity(n * 2 / 3 + 2) |
|||
let h = Resampler24kTo16k.h |
|||
let taps = Resampler24kTo16k.taps |
|||
while k / 2 <= lastGlobal { |
|||
var acc = 0.0 |
|||
var j = (k % 2 == 0) ? 0 : 1 // 让 (k-j) 为偶数 |
|||
while j < taps { |
|||
let idx = (k - Int64(j)) / 2 - base |
|||
if idx >= 0 && idx < Int64(buf.count) { acc += h[j] * Double(buf[Int(idx)]) } |
|||
j += 2 |
|||
} |
|||
out.append(Int16(clamping: Int(acc))) |
|||
k += 3 |
|||
} |
|||
let keep = min(Resampler24kTo16k.half, buf.count) |
|||
for i in 0..<keep { hist[i] = buf[buf.count - keep + i] } |
|||
histLen = keep |
|||
consumed += Int64(n) |
|||
return out.withUnsafeBufferPointer { Data(buffer: $0) } |
|||
} |
|||
} |
|||
Loading…
Reference in new issue