硬件采样率不准确导致陷波偏移的问题
问题背景: 仪器的标称采样率是2000Hz,但是在软件里面实际测试,采样率就很不准确,也不固定,采样率大致在1960 因为我软件的滤波系数是依据2000生成的,就导致50Hz的干扰信号没有完全滤除,残留的噪声会严重影响肌电信号的质量
解决思路:
-
我们依据1960Hz采样率重新生成一遍滤波系数
-
测试50Hz的陷波偏移到48Hz左右,按照偏移生成52Hz的陷波系数
public static float g_fNo_A[][][][] = new float[][][][]
{
{//2000Hz
{ //50Hz 为了解决陷波偏移的问题,这里实际是52Hz
{(float) 0.9968535391468, 0, 0},
{1, (float) -1.973410841856, 1},
{(float) 0.9968535391468, 0, 0},
{1, (float) -1.973410841856, 1},
{(float) 0.9937559649537, 0, 0},
{1, (float) -1.973410841856, 1},
{1, 0, 0}
},
{ //60Hz
{(float) 0.9968535391468, 0, 0},
{1, (float) -1.964613281242, 1},
{(float) 0.9968535391468, 0, 0},
{1, (float) -1.964613281242, 1},
{(float) 0.9937559649537, 0, 0},
{1, (float) -1.964613281242, 1},
{1, 0, 0}
}
}
};
public static float g_fNo_B[][][][] = new float[][][][]
{
{//2000Hz
{ //50Hz, 为了解决陷波偏移的问题,这里实际是52Hz
{0, 0},
{(float) -1.965204778951, 0.9935304176059F},
{0, 0},
{(float) -1.96914050235, 0.993942814767F},
{0, 0},
{(float) -1.961088795399, 0.9875119299073F},
{0, 0}
},
{ //60Hz
{0, 0},
{(float) -1.956195038874, (float) 0.9935584554446},
{0, 0},
{(float) -1.960610741601, (float) 0.9939147660818},
{0, 0},
{(float) -1.952346167061, (float) 0.9875119299073},
{0, 0}
}
}
};
- 使用线性插值重采样算法,将采样率稳定在2000Hz
class Resampler(private val targetSampleRate: Double, private val channelCount: Int) {
// 超时阈值:1秒(单位:纳秒)
private val timeThresholdNs = 1_000_000_000
// 目标采样间隔
private val targetInterval: Double = 1.0 / targetSampleRate
// 标记是否是第一个数据包
private var isFirstPacket: Boolean = true
// 上一次数据包到达的时间戳
private var lastPacketTime: Long = 0L
// 缓存上一次的数据值,用于作为插值的起点
private var lastDataCache: DoubleArray = DoubleArray(channelCount)
/**
* 输入原始数据
*/
fun putData(currentRawData: DoubleArray, listener: OnResampledDataListener?) {
val currentTime = System.nanoTime()
// 1. 处理第一包数据,只做缓存,不输出,因为没有前一个点无法插值
if (isFirstPacket) {
lastPacketTime = currentTime
currentRawData.copyInto(lastDataCache)
isFirstPacket = false
return
}
// 2. 计算真实经过的时间(秒), 1秒 = 1000000000纳秒(十亿)
val deltaTimeNs = currentTime - lastPacketTime
if (deltaTimeNs > timeThresholdNs) { // 自动检测超时并重置
lastPacketTime = currentTime
currentRawData.copyInto(lastDataCache)
isFirstPacket = true
return
}
val deltaTimeSeconds = deltaTimeNs / 1_000_000_000.0 // 纳秒转为秒
// 3. 异常处理(防御性编程)
// 防止时间倒流和断联画直线
if (deltaTimeSeconds <= 0.0 || deltaTimeSeconds > 0.5) {
lastPacketTime = currentTime
currentRawData.copyInto(lastDataCache)
return
}
// 4. 计算这段间隔内需要生成多少点
var numPointsToGenerate = (deltaTimeSeconds / targetInterval).roundToInt()
// 5. 边界情况处理
if (numPointsToGenerate <= 0) {
numPointsToGenerate = 1
}
// 6. 执行线性插值循环
for (i in 0 until numPointsToGenerate) {
// 插值比例
val t = i.toFloat() / numPointsToGenerate
val interpolatedData = DoubleArray(channelCount)
for (j in 0 until channelCount) { // 为每个通道独立计算插值
val startVal = lastDataCache[j] // 上一个采样点的第J通道值
val endVal = currentRawData[j] // 当前采样点的第J通道值
interpolatedData[j] = startVal + (endVal - startVal) * t // 插值结果 = 起点值 + (终点值 - 起点值) * 比例因子
}
// 7. 输出数据
listener?.onData(interpolatedData)
}
// 8. 更新状态,为下一次计算做准备
lastPacketTime = currentTime
currentRawData.copyInto(lastDataCache)
}
/**
* 回调接口
*/
interface OnResampledDataListener {
fun onData(data: DoubleArray)
}
}
基于上面的三种方案,前两种是治标,第三种是治本
记录与2026.04.23