DEV Community

Cover image for Wake-word on a $15 Raspberry Pi Zero 2 W: 5.3% RTF always-on
VoxRT
VoxRT

Posted on

Wake-word on a $15 Raspberry Pi Zero 2 W: 5.3% RTF always-on

The problem: always-on wake-word is hard and expensive

A wake-word detector is the part of the voice stack that listens to the mic 24/7 and triggers the rest of the pipeline when it hears a specific phrase like "Alexa", "Hey Siri", or "Hey Google". Because it is always-on, wake-word dominates the energy and compute budget of any voice-first device.

Ready-made options for hobbyists are limited, and all have trade-offs:

  • Alexa/Google Nest/Apple HomePod work well but are locked to their vendor platforms, require cloud connectivity in some scenarios, and use closed hardware.
  • Picovoice / Porcupine is an on-device SDK that works offline after activation. The model is proprietary, and the free tier was deprecated as of 30 June 2026 (now a 7-day trial followed by paid).
  • openWakeWord is open source (Apache-2.0) with community models, but training a custom wake phrase is non-trivial. You need to gather data, train the model, and iterate.
  • Custom Python + TF Lite from scratch requires ML expertise and hours of work.

The problem: hobbyists want a simple recipe. Grab a $15 Pi, install a package with one line, hear a custom wake word, do not pay per activation.

This post covers our solution (proprietary, but with a free tier "Hey Assistant" by default). The headline number: 5.3% CPU on one A53 core of a $15 Raspberry Pi Zero 2 W, 24/7 sustained.

What a wake-word detector is: quick technical background

Three components usually connect:

  • VAD (Voice Activity Detection) is the lightest. It answers "is there any speech in this audio?" (regardless of what kind). Runs at ~1% CPU. Used as a pre-filter for heavier stages.
  • Wake-word detector answers "is there a specific phrase?". Heavier than VAD (needs actual pattern matching) but still light (typically 10-100 KB model). Runs 1-10% CPU in always-on mode.
  • ASR (Automatic Speech Recognition) does full transcription: "what exactly was said?". Heavy (~60 MB model, 30-40% CPU on mid-range Android). Only fires after wake-word success.

Wake-word sits in the sweet spot. Not as light as VAD (VAD does not distinguish "Hey Assistant" from "hello world"), not as heavy as ASR (no full vocabulary needed). Cache-friendly architecture, sub-100 ms decisions, runs on any ARM SoC from 2020+ without a DSP.

Why Pi Zero 2 W as target

Raspberry Pi Zero 2 W is the most popular "minimal" Pi (~$15). Specs:

  • ARM Cortex-A53 quad-core @ 1 GHz: same class as midrange smartphones from 2016-2018
  • 512 MB RAM: enough for small ML models, not for heavy Whisper
  • No GPU: CPU-only inference
  • ~$15 retail: cheap enough for DIY smart speakers, doorbell notifications, custom voice UI

The logic: if it works on Pi Zero 2 W, it works everywhere. More powerful Pi 4/5 and Jetson Nano are 4-10x faster, embedded ARM SoCs (Rockchip, Allwinner) are comparable class. So Pi Zero 2 W sets the lower bound: if the numbers hold there, they hold everywhere else.

Wake-word across the full Pi ladder:

Device RTF (Real-Time Factor) CPU budget
Pi Zero 2 W (Cortex-A53) 0.053 (5.3%) one core
Pi 3 A+/B/B+ (Cortex-A53) ~0.038-0.044 (3.8-4.4%) one core
Pi 4 B / 400 (Cortex-A72) ~0.018-0.024 (1.8-2.4%) one core
Pi 5 (Cortex-A76) ~0.008-0.012 (0.8-1.2%) one core

Pi Zero 2 W number is measured (60-second sustained live-mic push). Pi 3/4/5 numbers are estimates scaled by clock speed and architecture generation, not independently benchmarked.

RTF 0.053 means the processor spends 53 milliseconds per 1 second of audio. Wake-word takes ~5% of one core on Pi Zero 2 W, leaving 95% plus the other 3 cores for the application (UI, LEDs, LLM inference, storage). On Pi 5, wake-word is essentially free (<1%).

Model architecture

Key characteristics of our wake-word model:

  • ~48 thousand parameters: very small neural network by modern standards (for comparison, Whisper base.en has ~74 million parameters, roughly 1500x larger).
  • File size: ~100 KB (encrypted .vxrt format).
  • Sampling rate: 16 kHz mono, 32 ms frames: standard for speech recognition.
  • Trained on 100% synthetic data with augmentation: no human voice samples in the training set, which addresses licensing concerns for commercial deployment.
  • Free tier phrase: "Hey Assistant": works out of the box.
  • Custom phrases: paid tier: training pipeline available on request (help@voxrt.com).

Accuracy metrics (measured on a test set of 5,240 positive + 6,416 negative utterances):

  • ROC AUC: 0.9966: very clean separation between positive and negative.
  • PR AUC (Average Precision): 0.9899.
  • Threshold 0.90: precision 0.993 / recall 0.982.

In practice: at threshold 0.90 the model produces ~1 false positive per 100+ triggers and misses ~2% of real wake-words. Acceptable for an always-on hot loop.

Benchmark: 5.3% RTF sustained on Pi Zero 2 W

RTF (Real-Time Factor) is CPU time divided by audio duration. RTF 5.3% means the processor spends 53 milliseconds of CPU time per 1 second of audio.

How we measured:

  • Pi Zero 2 W, Cortex-A53 @ 1 GHz, aarch64 Linux
  • 60-second continuous loop with microphone input
  • Used one core out of four
  • Sustained RTF (not peak) because sustained better reflects real-world thermal throttling

Why sustained matters more than peak: Pi Zero 2 W without a heat sink can throttle CPU after 2-3 minutes of sustained load. Peak RTF (first 5 seconds) is typically 10-20% better than sustained. We publish the sustained number, 5.3%, because that is what a hobbyist actually gets in production.

What to do with the remaining 95% CPU plus 3 cores: UI (LEDs / display), Bluetooth / WiFi for sending triggers to home assistant, local logic (dimmer, thermostat), or on a mid-tier board like Pi 4, running a smaller LLM for command interpretation.

Why NEON matters: 8.7x speedup

The wake-word runtime is written in Rust. But "pure Rust" is not enough for these RTF numbers. The key ingredient is ARM NEON SIMD intrinsics in the inner loops (matrix multiply, activation functions).

Measured on Snapdragon 662, Cortex-A73 big cluster pinned via HIGH_PERF affinity (SD662 is a big.LITTLE SoC with 4x A73 + 4x A53, and the perf cluster is used for stable clock):

Implementation RTF
Rust scalar (no SIMD) 0.182 (18.2%)
Rust + NEON SIMD 0.021 (2.1%)

Speedup: 8.7x.

What this means for the reader: even a small neural network (48K params) on CPU-only inference depends critically on SIMD acceleration. Writing your own solution in pure Python + NumPy gives you an RTF that is 10-20x higher than an optimized SIMD implementation.

On Pi Zero 2 W (A53, one generation older than A73), the NEON speedup ratio is similar, but A53 has fewer cycles per second per core. That is why we see 5.3% RTF on A53 vs 2.1% on A73 with the same code.

Practical takeaway: if you plan to write a custom wake-word, targeting ARM NEON is mandatory. Otherwise it will be too slow for always-on mode on any embedded ARM.

How to run: setup + code (4 languages)

The SDK is available for Python, Node.js, Go, and C with an identical API pattern: load a .vxrt file, set threshold and cooldown_frames, push PCM int16 chunks (16 kHz mono), receive detection events with frame_index, timestamp_sec, and score fields.

The voxrt_wake_word.vxrt model (~100 KB, free tier "Hey Assistant") for all examples below can be downloaded from the releases page or HuggingFace.

All examples below are simplified versions of the quickstarts in examples/{python,nodejs,go,c}/ in the repository. Full versions (with WAV parsing, ALSA live-mic, performance timing) live there too.

Python

pip install voxrt-wake-word
Enter fullscreen mode Exit fullscreen mode
from voxrt_wake_word import WakeWordEngine

engine = WakeWordEngine.from_path("voxrt_wake_word.vxrt")
engine.threshold = 0.9
engine.cooldown_frames = 100

for chunk in mic_iter():  # int16 mono @ 16 kHz, 512-sample chunks
    for d in engine.push_pcm_i16(list(chunk)):
        print(f"wake! t={d.timestamp_sec:.3f}s score={d.score:.4f}")
Enter fullscreen mode Exit fullscreen mode

For live mic, install sounddevice (pip install sounddevice) and feed sd.InputStream(samplerate=16000, channels=1, dtype='int16', blocksize=512, callback=...) chunks into engine.push_pcm_i16 inside the callback.

Node.js

npm install @voxrt/wake-word
Enter fullscreen mode Exit fullscreen mode
const { WakeWordEngine } = require("@voxrt/wake-word");

const engine = WakeWordEngine.fromPath("voxrt_wake_word.vxrt");
engine.threshold = 0.9;
engine.cooldownFrames = 100;

for (const chunk of micIter()) {  // Int16Array, 512 samples, 16 kHz mono
  for (const d of engine.pushPcmI16(chunk)) {
    console.log(`wake! t=${d.timestampSec.toFixed(3)}s score=${d.score.toFixed(4)}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

For live mic, use the mic or node-record-lpcm16 npm packages. Both return Node Buffer objects, so convert to Int16Array before pushing: new Int16Array(buf.buffer, buf.byteOffset, buf.length / 2).

Go

go get github.com/VoxRT/voxrt-wake-word-linux/go
Enter fullscreen mode Exit fullscreen mode
package main

import (
    "fmt"
    "log"
    wakeword "github.com/VoxRT/voxrt-wake-word-linux/go"
)

func main() {
    engine, err := wakeword.OpenFromPath("voxrt_wake_word.vxrt")
    if err != nil {
        log.Fatal(err)
    }
    defer engine.Close()
    engine.SetThreshold(0.9)
    engine.SetCooldownFrames(100)

    // micIter returns <-chan []int16 (512-sample chunks, 16 kHz mono)
    for chunk := range micIter() {
        for _, d := range engine.PushPcmI16(chunk) {
            fmt.Printf("wake! t=%.3fs score=%.4f\n", d.TimestampSec, d.Score)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

For live mic on Linux, use github.com/gordonklaus/portaudio or pipe from arecord (simpler for a quickstart). Have your mic reader emit <-chan []int16 for the range loop above.

C

Install headers and shared library from the tarball release, then build with pkg-config for flags:

gcc main.c $(pkg-config --cflags --libs voxrt-wake-word) -lasound -o wake-word-app
Enter fullscreen mode Exit fullscreen mode
#include <stdint.h>
#include <stdio.h>
#include <voxrt_wake_word.h>

int main(int argc, char **argv) {
    // Load model bytes via mmap. See full example in repo for the ~30-line
    // helper (open + fstat + mmap). Placeholders here for brevity:
    const uint8_t *model_bytes = NULL;   // TODO: mmap voxrt_wake_word.vxrt
    size_t model_len = 0;                // TODO: file size

    voxrt_wake_word_t *engine = NULL;
    voxrt_status_t rc = voxrt_wake_word_create(model_bytes, model_len, &engine);
    if (rc != VOXRT_OK) {
        fprintf(stderr, "voxrt_wake_word_create failed: %d\n", rc);
        return 1;
    }
    voxrt_wake_word_set_threshold(engine, 0.9f);
    voxrt_wake_word_set_cooldown_frames(engine, 100);

    int16_t buf[512];
    voxrt_wake_word_detection_t dets[8];
    for (;;) {
        // Read 512 int16 samples from ALSA / mic into buf.
        size_t written = 0;
        voxrt_wake_word_push_pcm_i16(engine, buf, 512, dets, 8, &written);
        for (size_t i = 0; i < written; ++i) {
            printf("wake! t=%.3fs score=%.4f\n",
                   dets[i].timestamp_sec, dets[i].score);
        }
    }
    voxrt_wake_word_destroy(engine);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

The full C example with all ALSA setup (~150 lines) is at examples/c/alsa-mic-quickstart/main.c. There is also a C++ variant (alsa-mic-quickstart-cpp) and a CMake consumer example for integrating into existing projects.

Parameters and what is not shown

All four languages use identical parameters:

  • threshold (default 0.5): confidence threshold for firing. 0.9 is strict (fewer false positives). For always-on in a noisy environment, 0.85 to 0.90 is a reasonable baseline.
  • cooldown_frames (default 50, ~1.6 sec): how many silence frames after a detection before the next trigger. Prevents double-fire.

Not shown in the snippets (standard boilerplate): microphone init, error handling, action on detection (starting ASR, HTTP webhook to Home Assistant, turning on an LED).

Wake-word alternatives: honest comparison

Three main options for a hobbyist in 2026:

Porcupine (Picovoice) openWakeWord VoxRT wake-word
Model size ~200 KB - 1 MB (varies by tier) ~50-400 KB per model + ~3 MB shared runtime ~100 KB
RTF on Pi Zero 2 W ~3.8% on Pi 3 per Picovoice docs, no Pi Zero 2 W numbers published (likely 4-8% scaled by clock) reported CPU-heavy on Pi Zero 2 W, no public RTF, on Pi 4 <5 ms per 80 ms chunk 5.3% (measured)
Runtime proprietary C/C++ SDK Python + ONNX Runtime proprietary Rust SDK
License commercial (7-day trial then paid, no free tier since 30 June 2026) Apache-2.0 (fully open) proprietary (LICENSE-BINARY), free default phrase, paid custom
Custom wake phrase commercial console (paid) DIY training pipeline (Python, data collection, hours of work) paid tier (email us)
Setup complexity (out-of-the-box "Hey X") medium (auth flow) high (need to train or download a community model) low (pip install + download)

Each tool has its fit:

  • Porcupine: if you need a large catalog of pre-trained wake phrases and you are fine with commercial licensing.
  • openWakeWord: if 100% open source is critical and you have time for DIY training.
  • VoxRT wake-word: if you need the smallest footprint and simplest deployment ("Hey Assistant" out of the box, custom phrase via paid tier).

No tool is universally "better" than the others. It depends on requirements.

Licensing model

Important disclosure: VoxRT wake-word is a proprietary tool (unlike our Silero VAD packaging, which is MIT).

  • Runtime + model: LICENSE-BINARY (Elephant Enterprises LLC). Distributable as part of an unmodified SDK, no forking, no rebuilding.
  • Free tier phrase: "Hey Assistant" works out of the box without activation.
  • Custom wake phrases: paid tier. We train a custom model on your phrase (typical 2-3 weeks turnaround). Contact: help@voxrt.com.
  • No per-activation billing: unlike Porcupine, no ongoing fees. A one-time custom-phrase fee covers redistribution in your product.

This is a trade-off: you get the smallest footprint and simplest deployment, but you cannot modify the runtime or train your own model. For a 100% open-source requirement, use openWakeWord (Apache-2.0).

WASM bonus: 0.16% RTF in Chrome

The same ~100 KB .vxrt file runs via WASM SIMD128 in the browser:

Environment RTF
Chrome / MacBook Pro M4 (WASM SIMD128) 0.16%
Safari / iPhone A15 (WASM) 0.23%

Essentially free. The full npm bundle @voxrt/wake-word-browser is ~275 KB (model + WASM runtime).

Not the primary story of this post, but worth mentioning because it is unusual. Voice AI in the browser without a backend, integrated into a web app via npm install. The same model runs on Pi, iPhone, Android, and in a user's Chrome tab.

Wrapping up

Wake-word detection on a $15 Raspberry Pi Zero 2 W, always on, 5.3% CPU is not an abstract idea. One-line install with pip install voxrt-wake-word, add a microphone reader loop, done.

Three things worth remembering:

  • RTF sustained (60+ sec) matters more than peak. Thermal throttling on a Pi Zero 2 W without a heat sink makes peak numbers misleading.
  • ARM NEON SIMD is critical for CPU inference. 8.7x speedup vs scalar Rust. Custom implementations without SIMD will be too slow for always-on.
  • Wake-word is not VAD, not ASR. Three tiers with different CPU budgets. Wake-word is the middle tier: heavier than VAD, light enough for always-on.

Tools from this post:

Previous posts in this voice AI series:

Learn more about the runtime and product family: voxrt.com.

Custom wake phrase for your product: help@voxrt.com.

Top comments (0)