Problem
There is a subtle, length-dependent lag when firing playback: the longer the
audio file, the more noticeable the delay between click and sound. Root cause:
play_sound_entry (src/app.rs) calls crate::audio::decode() synchronously
inside Iced's update() loop on every play, decoding the entire file to PCM
(decoder.rs:decode_packets). That cost scales with file length and blocks the
UI thread.
Secondary waste on the same hot path:
- The waveform envelope is cached in-memory per
id (good) but is lost on
every restart and its generation is coupled to the synchronous decode.
- The playhead length reads
decoded.duration — available only because we fully
decoded — even though duration_ms is already known cheaply from metadata.
- Per-sound volume is applied with an O(n)
map().collect() copy of the PCM on
every play.
The render side is already optimized (persistent canvas::Cache, ADR-009 / #137)
— this is purely the click → decode → fire path.
Goal
Make firing playback responsive regardless of file length, and stop redoing
length-scaling work on every play, in a memory-conscious way. Two slices:
A — Async decode + in-memory caches. Move decode off the UI thread into an
iced::Task keyed by the play_generation token (#149) so stale/superseded
decodes are dropped (correct rapid-fire ordering). Add a byte-capped LRU
decoded-PCM cache so a hot, rapidly re-fired sound (incl. long ones) fires
instantly without re-decoding, while cold sounds evict past the cap. Move
per-sound volume into the engine so the cached canonical PCM Arc is shared
with the engine (no per-play copy).
B — Persisted waveform envelope. Persist the 1024-bucket envelope
(~4 KB/sound) under $XDG_DATA_HOME/honkhonk/waveforms/<id>.bin, generated
lazily (piggybacks A's decode) and loaded on demand on later sessions.
Invalidate via a stored (size, mtime) fingerprint; format is versioned so a
future content-hash key (gated by a cheap stat pre-check) can be added without a
rewrite.
Explicitly out of scope (future)
Mix/overlap playback (multi-voice "mix vs. stop-and-retrigger" setting),
content-hash keying, eager whole-library pre-generation, and trimming — all
designed-around but not built here.
Acceptance
- Firing a long sound never blocks the UI; the tile highlights instantly.
- Re-firing the same (warm) sound does not re-decode.
- Envelope survives restart (no recompute when the file is unchanged).
- Covered by unit tests in the default
cargo test run.
Design spec: docs/superpowers/specs/2026-06-23-playback-decode-and-waveform-caching-design.md.
Problem
There is a subtle, length-dependent lag when firing playback: the longer the
audio file, the more noticeable the delay between click and sound. Root cause:
play_sound_entry(src/app.rs) callscrate::audio::decode()synchronouslyinside Iced's
update()loop on every play, decoding the entire file to PCM(
decoder.rs:decode_packets). That cost scales with file length and blocks theUI thread.
Secondary waste on the same hot path:
id(good) but is lost onevery restart and its generation is coupled to the synchronous decode.
decoded.duration— available only because we fullydecoded — even though
duration_msis already known cheaply from metadata.map().collect()copy of the PCM onevery play.
The render side is already optimized (persistent
canvas::Cache, ADR-009 / #137)— this is purely the click → decode → fire path.
Goal
Make firing playback responsive regardless of file length, and stop redoing
length-scaling work on every play, in a memory-conscious way. Two slices:
A — Async decode + in-memory caches. Move decode off the UI thread into an
iced::Taskkeyed by theplay_generationtoken (#149) so stale/supersededdecodes are dropped (correct rapid-fire ordering). Add a byte-capped LRU
decoded-PCM cache so a hot, rapidly re-fired sound (incl. long ones) fires
instantly without re-decoding, while cold sounds evict past the cap. Move
per-sound volume into the engine so the cached canonical PCM
Arcis sharedwith the engine (no per-play copy).
B — Persisted waveform envelope. Persist the 1024-bucket envelope
(~4 KB/sound) under
$XDG_DATA_HOME/honkhonk/waveforms/<id>.bin, generatedlazily (piggybacks A's decode) and loaded on demand on later sessions.
Invalidate via a stored
(size, mtime)fingerprint; format is versioned so afuture content-hash key (gated by a cheap stat pre-check) can be added without a
rewrite.
Explicitly out of scope (future)
Mix/overlap playback (multi-voice "mix vs. stop-and-retrigger" setting),
content-hash keying, eager whole-library pre-generation, and trimming — all
designed-around but not built here.
Acceptance
cargo testrun.Design spec:
docs/superpowers/specs/2026-06-23-playback-decode-and-waveform-caching-design.md.