Skip to content

perf: keep openWakeWord's raw audio in a numpy buffer - #32

Open
Jacob Kaplan (qwertey6) wants to merge 1 commit into
viam-modules:mainfrom
qwertey6:oww-audio-tail
Open

Jacob Kaplan (qwertey6) wants to merge 1 commit into
viam-modules:mainfrom
qwertey6:oww-audio-tail

Conversation

@qwertey6

Copy link
Copy Markdown

Summary

openwakeword 0.6.0 keeps the last 10 s of audio in a deque(maxlen=160000) of Python ints. On every 80 ms chunk, AudioFeatures:

  • converts the new samples to a Python list to append them (_buffer_raw_data);
  • then copies the whole 160,000-sample deque into a list, only to keep the last 1,760 samples for the melspectrogram (_streaming_melspectrogram: list(self.raw_data_buffer)[-n_samples-160*3:]).

That copy was about half of openWakeWord's per-chunk time.

setup_oww now replaces the preprocessor's deque with _AudioTail, an int16 numpy buffer whose newest samples are a slice. It also replaces the two methods that write and read it.

  • Guarded: it applies only with openwakeword 0.6.0 and when the buffer really is a deque; other versions keep their own code.
  • Same input: the melspectrogram model gets the same int16 samples it got before.
  • Model.reset() still works.

Bit-identical

Every per-chunk score, plus the melspectrogram and feature buffers, matches stock openwakeword 0.6.0 exactly with real models (a custom wake-word model plus openWakeWord's bundled feature models):

  • 452 chunks (36 s) of speech and noise, enough to fill and wrap the 10 s buffer;
  • 1280-sample chunks and mixed chunk sizes;
  • with speex noise suppression on;
  • across Model.reset() calls mid-stream;
  • on macOS and on aarch64 Linux with the module's own frozen build (Python 3.11, numpy 2.4.2, onnxruntime 1.24.1).

Measured on a Raspberry Pi CM5

openWakeWord predict() per 80 ms chunk: the module's frozen toolchain, pinned to one core, the same 60 s of audio. Two runs of four interleaved stock/fix rounds; scores identical in every round.

mean median p90 % of one core at 12.5 chunks/s
stock 7.9 ms 7.87 ms 8.7 ms 9.9%
this change 5.1 ms 5.00 ms 5.5 ms 6.4%

Run-to-run spread is under 0.2 ms.

The whole module on the same device: base and fixed builds from the same toolchain, swapped in and out with interleaved 60 s windows. The device ran its normal workload, so these numbers are noisier.

  • Base: median 12.9% of one core (12.3–15.5%).
  • This change: median 10.9% (9.8–10.9%), plus one window at 18.1%.

Tests

  • New tests cover:
    • _AudioTail against a deque;
    • the streaming path against stock openwakeword, before and after the buffer fills;
    • setup_oww installing it;
    • the version and deque guards.
  • 228 tests pass; ruff check and ruff format --check are clean.
  • CI needs the "safe to test" label to run the cloud build.

🤖 Generated with Claude Code

@viam-ci-workflows

Copy link
Copy Markdown

For security reasons, this PR must be labeled with safe to test in order for tests to run.

@viam-overwatch

Copy link
Copy Markdown
Contributor

Hey Jacob Kaplan (@qwertey6) — CI is green and no reviewer is assigned yet. Could you request one when you have a chance?

Auto-comment from overwatch. Will not re-nudge for 7 days.

@qwertey6

Copy link
Copy Markdown
Author

Ale Paredes (@ale7714) / John Morales (@john-morales) when one of you gets a chance!

@ale7714 Ale Paredes (ale7714) added the safe to test Run CI on this PR (fork PRs need a maintainer to apply it) label Oct 16, 2026
openwakeword 0.6.0's AudioFeatures keeps the last 10 s of audio in a
deque(maxlen=160000) of Python ints. Every 80 ms chunk it converts the new
samples to a list to append them, then copies the whole deque into a list to
take the last 1,760 samples for the melspectrogram. That copy was about half
of the time openWakeWord spends on a chunk.

With openwakeword 0.6.0, setup_oww now replaces the deque with _AudioTail, an
int16 numpy buffer whose newest samples are a slice, along with the two
methods that write and read it. The melspectrogram gets the same int16
samples it got before, so the features and scores are bit-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ale7714

Copy link
Copy Markdown
Contributor

Jacob Kaplan (@qwertey6) could you try this alternative #34 on the pi? I'd prefer not to re-implement the internals of a library in the module and #34 gets you close enough to this gains (measured on my mac) with a much smaller change.

@ale7714

Copy link
Copy Markdown
Contributor

Jacob Kaplan (@qwertey6) also i think your changes make sense but we should try to push them directly on the library upstream.

@qwertey6

Copy link
Copy Markdown
Author

Ale Paredes (@ale7714) agreed, upstream is the right home. I opened dscripka/openWakeWord#356, which is smaller than either PR here: _streaming_melspectrogram reads only the newest n_samples + 480 samples from the end of the existing deque, so there's no new buffer type and no size change. Predictions, mel and feature buffers match the current code (80 ms and mixed chunks, after reset(), and with speex at 80 ms). Per-chunk predict() drops from 1.67 to 1.26 ms in an aarch64 container.

openWakeWord hasn't merged a PR since October 2025, so #34 is a sensible way to get most of the saving now. I've built an arm64 benchmark of stock, #32 and #34 with speex on, frozen with the module's toolchain, for our CM5 kiosks. None of my test kiosks is reachable at the moment, so I don't have Pi numbers for #34 yet. I'll post them here and close #32 if #34 holds up.

@qwertey6

Copy link
Copy Markdown
Author

Ale Paredes (@ale7714) a correction to my last comment: upstream isn't a realistic home. openWakeWord's last commit was 2025-12-30, and its last release, 0.6.0, was February 2024. So even a merged #356 would never reach PyPI, and the module will keep running 0.6.0 as it is. I'll leave #356 open as a record for anyone else hitting this.

So the fix lives in this module, as #32 or #34. Both are pinned to 0.6.0 by a version test, and since the library isn't changing, that pin is unlikely to break. I'll post CM5 numbers for both as soon as one of our kiosks is reachable. If #34 keeps most of the saving there, I'll close #32 in its favour.

Longer term, if Viam wants fixes like this in the library itself, a Viam-owned fork of openWakeWord would be the place.

@ale7714

Copy link
Copy Markdown
Contributor

Jacob Kaplan (@qwertey6) let me know how your test in the pi works with #34. we would prefer to keep the change small for now.

@ale7714

Copy link
Copy Markdown
Contributor

Jacob Kaplan (@qwertey6) have you had a chance to test #34?

@viam-overwatch

Copy link
Copy Markdown
Contributor

Hey Jacob Kaplan (@qwertey6) — CI is green and no reviewer is assigned yet. Could you request one when you have a chance?

Auto-comment from overwatch. Will not re-nudge for 7 days.

2 similar comments
@viam-overwatch

viam-overwatch Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Hey Jacob Kaplan (@qwertey6) — CI is green and no reviewer is assigned yet. Could you request one when you have a chance?

Auto-comment from overwatch. Will not re-nudge for 7 days.

@viam-overwatch

Copy link
Copy Markdown
Contributor

Hey Jacob Kaplan (@qwertey6) — CI is green and no reviewer is assigned yet. Could you request one when you have a chance?

Auto-comment from overwatch. Will not re-nudge for 7 days.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

safe to test Run CI on this PR (fork PRs need a maintainer to apply it)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants