# Five Ways to Compute an STFT on a Qualcomm SoC: Accuracy, Latency, and Power, Measured Not Guessed

> Source: <https://pub.towardsai.net/five-ways-to-compute-an-stft-on-a-qualcomm-soc-accuracy-latency-and-power-measured-not-guessed-dcc69541793b?source=rss----98111c9905da---4>
> Published: 2026-07-30 21:01:02+00:00

Every audio-AI pipeline in a vehicle starts the same way: turn a raw microphone waveform into a spectrogram. The Short-Time Fourier Transform (STFT) is usually the very first block in the graph — feeding voice assistants, in-cabin sensing, or noise-suppression models — and it runs on every single audio chunk, continuously, for as long as the cockpit domain is powered. Get it wrong — too slow, too much memory, too much power — and it does not matter how good the model behind it is.

On a modern Qualcomm SoC there is no single “correct” way to run this block. You can run it on the CPU with a general-purpose FFT library, offload it to the low-power compute DSP (cDSP), or fold it into the NPU (HTP) graph as a Conv1d-equivalent operator running next to your model. I built and measured five such implementations on a Snapdragon SM8650 test unit and logged accuracy, latency, and *real, measured* power for each one.

This article is not a leaderboard. The honest engineering answer is: **it depends on what the rest of your pipeline looks like.** What follows is the data, the trade-offs, and a framework for picking the right backend for your own product constraints.

*Test methodology note: the remote-to-device benchmarking pipeline and the power-measurement rig used here are the same ones described in two earlier pieces — **Seamless Remote-to-Edge AI Benchmarking** and **The Sub-$3 Power Meter**. This article assumes that harness and focuses on the STFT results themselves.*

One command, five backends, three metrics.Every number in this article — accuracy, latency, and power, for all five backends — comes out of a single invocation ofrun_on_device_tests.sh from the AI server. That one call syncs whatever changed, deploys all five backends to the test unit over the SSH-tunneled pipeline from the first article, drives the on-device accuracy/latency sweep, and, when--power is passed, coordinates the INA226 capture from the second article and pulls the trace back automatically. Nothing here was run backend-by-backend by hand: the entire loop from "code changed" to "here is the updated chart" is one shell command. That is the actual point of building the two pipelines described earlier — this article is what they're for.

All five implementations compute the *same* STFT: 512-sample input chunks, a 64-sample left context, right-side reflect padding, 256-point FFT, 128-sample hop, Hann window, 4 output frames per chunk, 129 magnitude bins per frame. Same math, same chunking, same test signal (a 16 kHz WAV file) — only the execution path changes.

All tests ran on the same Snapdragon SM8650 test unit, back to back, in the same session, over the automated remote-to-device pipeline referenced above — so device thermal state, power-rail condition, and background load are shared across backends rather than being a confound.

Two things jump out immediately.

**Accuracy.** CPU PFFFT, CPU PocketFFT, DSP (QHL), and DSP (custom) all sit on top of each other at essentially zero error against the NumPy reference — these are all direct FFT implementations of the same math, in double- vs single-precision, and the small floating-point rounding differences don’t show up at this scale. The QNN(Conv1d, fp16) path is the outlier: because it re-expresses the DFT as a matmul running in fp16 on the NPU, its per-chunk max error sits in the 0.002–0.018 range, an order of magnitude above the others. That is not a bug — it is the expected cost of trading a mathematically exact transform for an operator that runs natively, and fast, inside an fp16 NPU graph next to the rest of the model. Whether that error budget is acceptable depends entirely on what consumes the spectrogram downstream.

**Latency.** The ranking here is almost the mirror image of what many people expect the first time they look at this kind of chart:

A 512-sample, 256-point FFT is a tiny amount of arithmetic. On a modern Arm core it finishes in microseconds — that is exactly what PFFFT and PocketFFT show. The cDSP and NPU numbers are not slower because the *math* is slower; they are slower because every call has to cross a process/core boundary through FastRPC (CPU → cDSP) or the QNN runtime (CPU → HTP), and that round-trip dominates a workload this small. This is a well-known property of heterogeneous compute on mobile SoCs: **offload only pays off once the compute-per-call is large enough to amortize the dispatch cost.** A lone 256-point FFT, called every 32 ms, essentially never clears that bar on its own.

This is precisely why the “DSP (custom)” backend exists in this comparison at all: not because it beats QHL on raw FFT speed for this one operator, but because it is one building block in a larger cDSP pipeline where several stages are fused into a single cross-core call, amortizing that FastRPC cost across more work. Looked at as an isolated micro-benchmark, this makes DSP (custom) look unremarkable. Looked at as part of the pipeline it was designed for, the calculus changes — which is exactly the point of Section 4.

Latency tells you how long the CPU is blocked. It does not tell you what the offload actually costs in system-level energy — and for a block that runs continuously in the background of an in-vehicle audio pipeline, energy is usually the number product teams actually care about.

Power was measured with the same $3 INA226 + ESP32-C3 rig described in [ The Sub-$3 Power Meter](https://pub.towardsai.net/the-sub-3-power-meter-measuring-edge-ai-energy-consumption-without-an-smu-c3a24ca4069a): the device is placed in series with the test unit’s power rail, sampled at ~850 Sa/s, and each backend is run back-to-back in its own labeled window inside a single capture, with an idle baseline measured before and after. All power numbers below are

Three separate metrics are worth pulling apart here, because they answer three different product questions:

Here the story flips again. **Average power** is lowest on the offload paths — cDSP and HTP genuinely do draw less instantaneous power than spinning up the big Arm cores, which matches Qualcomm’s own positioning of these blocks as low-power co-processors. But **energy per chunk** — the number that actually matters for a background-running, always-on STFT — is *lowest on the CPU* by nearly two orders of magnitude. The reason is throughput: PFFFT finishes a chunk in 5 microseconds, so even at a higher instantaneous wattage it is powered-up for a vanishingly short time. The cDSP and NPU paths hold a lower-power state open for over 100× longer per chunk (matching the latency numbers in Section 2), and that extra *dwell time* outweighs their lower instantaneous draw.

**The takeaway is not “CPU wins.”** It is that for an isolated, tiny, high-frequency operator like a lone STFT call, dispatch overhead dominates both latency and energy on any offload path — the same root cause showing up in two different units. The calculus is designed to change once the STFT stops being called in isolation.

None of them is categorically “better.” Each one is a different point in a trade space that only makes sense next to the rest of your product:

And the deciding factor that never shows up in a per-operator chart: **what else is competing for CPU headroom at the same time.** If the big Arm cores are already busy running the rest of your software stack or another model — for example, a vision pipeline sharing the same SoC — moving the STFT to cDSP frees that headroom even though the STFT itself gets slower and, on a per-chunk basis, less power-efficient in isolation. FastRPC has its own (non-zero) cost, cDSP wake-up has its own cost — but those costs may be worth paying if the alternative is CPU contention that stalls something more important. This benchmark measures the operator in isolation; the right choice for a shipping product also has to account for that contention, which only shows up under real, concurrent system load.

This project’s implementation code (the cDSP kernels, the QNN context binary build, and the on-device test harness itself) is internal to the author’s employer and is not open-sourced. The benchmarking pipeline and the power-measurement tooling referenced above **are** open — the pipeline pattern is documented in the linked article, and the power-meter firmware and analysis scripts are MIT-licensed at github.com/CobenGao/esp32_power_mon, as described in that piece.

“Which STFT backend is fastest” turns out to be the wrong first question. The right one is: what is this call being fused with, what else is competing for the same cores at the same moment, and what accuracy budget does the next stage in the pipeline actually need? Measure all three axes — accuracy, latency, and *real* energy, not a datasheet number — before deciding, and the answer usually turns out to be “more than one of these, in different parts of the graph,” rather than a single winner.

**If you found this article helpful, feel free to connect with me on LinkedIn:**[https://www.linkedin.com/in/cobengao](https://www.linkedin.com/in/cobengao)

[Five Ways to Compute an STFT on a Qualcomm SoC: Accuracy, Latency, and Power, Measured Not Guessed](https://pub.towardsai.net/five-ways-to-compute-an-stft-on-a-qualcomm-soc-accuracy-latency-and-power-measured-not-guessed-dcc69541793b) was originally published in [Towards AI](https://pub.towardsai.net) on Medium, where people are continuing the conversation by highlighting and responding to this story.
