1. 硬件采样率不准确导致陷波偏移的问题

7 阅读3分钟

硬件采样率不准确导致陷波偏移的问题

问题背景: 仪器的标称采样率是2000Hz,但是在软件里面实际测试,采样率就很不准确,也不固定,采样率大致在1960 因为我软件的滤波系数是依据2000生成的,就导致50Hz的干扰信号没有完全滤除,残留的噪声会严重影响肌电信号的质量

解决思路:

  1. 我们依据1960Hz采样率重新生成一遍滤波系数

  2. 测试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}
            }
        }
	};
  1. 使用线性插值重采样算法,将采样率稳定在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