You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Prometheus/Micrometer指标中记录二维FFT数据?

将Android设备作为远程音频频率检测器并向Prometheus上报FFT数据的优化方案

背景

我计划将多台Android手机用作远程音频频率检测器,通过Prometheus/Grafana集中收集和分析设备数据。目前已基于Noise库开发完成Android应用,可录制音频并借助KissFFT计算各频率的幅度,FFT结果为FloatArray,且已实现热力图、直方图的可视化功能:

import android.media.AudioRecord
import com.paramsen.noise.Noise
import com.paramsen.noise.sample.R
import com.paramsen.noise.sample.source.AudioSource

/** 订阅麦克风数据更新 */
private fun start(recorder: AudioRecord) {

  val src = AudioSource(recorder).stream()
  val noise = Noise.real(SAMPLE_SIZE)

  // FFT可视化视图
  disposable.add(src.observeOn(Schedulers.newThread())
    .map { 
      // 计算FFT
      noise.fft(it, FloatArray(SAMPLE_SIZE + 2)) 
    }
    .subscribe({ fft: FloatArray ->
      // 将FFT数据传递给可视化组件
      fftHeatMapView.onFFT(fft)
      fftBandView.onFFT(fft)
    }, { e -> Log.e(TAG, e.message ?: "null") })
  )
}

const val RATE_HZ = 44100
const val SAMPLE_SIZE = 4096

FFT数据渲染正常(仅需调整窗函数),下一步需要将FFT数据上报至Prometheus。我已创建FFT数据处理器和Prometheus直方图指标:

object FFTMetrics : FFTView {

  private val registry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)

  private val fftDist = DistributionSummary
    .builder("fft.dist")
    .description("FFT分布")
    .baseUnit("hertz")
    //.scale(100.0)
    .register(registry)

  override fun onFFT(fft: FloatArray) {
    // TODO 实现数据上报逻辑
  }
}

当前问题

FFT数组是二维结构:索引对应频率(Hz),值对应幅度。但Micrometer指标仅支持一维数据,直接遍历记录幅度的方式效率极低且存在不必要的类型转换:

override fun onFFT(fft: FloatArray) {
  fft.forEachIndexed { i, magnitude ->
    val hz = computeHertzForBin(i)
    (0..magnitude.toLong()).forEach { j ->
      summary.record(hz.toDouble())
    }
  }
}

请问在Micrometer或Prometheus中,是否有更优的二维数据记录方式?

使用版本:

  • Kotlin 1.6.21
  • Android 7.3.1
  • Micrometer 1.10.4

可行优化方案

方案1:使用带标签的Gauge指标

FFT的每个频率桶对应一个幅度值,适合用Gauge指标,给每个频率桶添加frequency_hz和device_id标签(区分多设备),直接更新每个标签组合对应的幅度值,避免低效循环:

object FFTMetrics : FFTView {

  private val registry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
  private val appContext = ... // 全局应用上下文

  override fun onFFT(fft: FloatArray) {
    // 仅处理FFT前半部分(后半部分为镜像,无意义)
    val halfSize = SAMPLE_SIZE / 2
    for (i in 0 until halfSize) {
      val magnitude = fft[i]
      if (magnitude <= 0) continue // 忽略无效幅度值
      val hz = computeHertzForBin(i)
      
      // 创建或复用带标签的Gauge,更新幅度值
      Gauge.builder("fft.magnitude") { magnitude.toDouble() }
        .tags(
          "device_id", getDeviceId(),
          "frequency_hz", hz.toString()
        )
        .register(registry)
    }
  }

  private fun getDeviceId(): String {
    // 获取设备唯一标识,用于区分多台设备
    return android.provider.Settings.Secure.getString(
      appContext.contentResolver,
      android.provider.Settings.Secure.ANDROID_ID
    )
  }

  // 计算频率桶对应的实际Hz值
  private fun computeHertzForBin(binIndex: Int): Double {
    return (binIndex * RATE_HZ).toDouble() / SAMPLE_SIZE
  }
}

优势:直接映射频率-幅度的二维关系,Prometheus可按设备、频率过滤数据,Grafana可快速生成热力图/折线图,性能远优于循环上报。

方案2:利用DistributionSummary的权重参数

如果需要统计频率的“加权出现次数”(幅度作为权重),可使用Micrometer DistributionSummary的record(double value, double amount)方法,用幅度作为权重,一次调用完成上报:

object FFTMetrics : FFTView {

  private val registry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
  private val appContext = ...

  private val fftDist = DistributionSummary
    .builder("fft.dist")
    .description("FFT频率分布(幅度为权重)")
    .baseUnit("hertz")
    .tag("device_id", getDeviceId())
    .register(registry)

  override fun onFFT(fft: FloatArray) {
    val halfSize = SAMPLE_SIZE / 2
    for (i in 0 until halfSize) {
      val magnitude = fft[i]
      if (magnitude <= 0) continue
      val hz = computeHertzForBin(i)
      
      // 用幅度作为权重,一次记录等价于上报magnitude次该频率值
      fftDist.record(hz.toDouble(), magnitude.toDouble())
    }
  }

  private fun getDeviceId(): String {
    return android.provider.Settings.Secure.getString(
      appContext.contentResolver,
      android.provider.Settings.Secure.ANDROID_ID
    )
  }

  private fun computeHertzForBin(binIndex: Int): Double {
    return (binIndex * RATE_HZ).toDouble() / SAMPLE_SIZE
  }
}

优势:避免循环多次调用record,减少类型转换开销,适合需要统计频率分布权重的场景。

方案3:自定义Prometheus文本指标端点

若需要最高灵活性,可直接构造Prometheus标准文本格式的指标,通过HTTP端点暴露给Prometheus抓取。例如返回的指标格式如下:

# HELP fft_magnitude FFT各频率桶的幅度值
# TYPE fft_magnitude gauge
fft_magnitude{device_id="xxxx",frequency_hz="100"} 0.65
fft_magnitude{device_id="xxxx",frequency_hz="200"} 0.82
...

实现思路:在Android应用中启动轻量HTTP服务(如OkHttp或嵌入式Jetty),定义接口返回上述格式的指标文本,Prometheus配置该接口为抓取目标。


额外注意事项

  • 多设备区分:必须为每个设备添加唯一device_id标签,避免Prometheus混淆不同设备的数据。
  • 性能优化:FFT计算和指标上报需放在后台线程,避免阻塞UI;可设置上报间隔(如每1秒上报一次),减少网络请求频率。
  • FFT数据裁剪:FFT结果为对称结构,仅需处理前半部分(0到SAMPLE_SIZE/2)即可,减少无效数据处理。

内容的提问来源于stack exchange,提问作者aSemy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 07:57:53