Skip to content

Sleep mode still wastes power: analyzer redraws keep running and the output device (DAC) is never released #36

Description

@takaeda

Symptom

After #33 (lower GUI-interaction CPU) and #35 (sleep mode fires even with DC-emitting plugins), sleep mode works - but a fully idle EffeTune still draws noticeable power on an always-on low-power host (Raspberry Pi 5 + external USB DAC), in two independent ways:

  1. Main-thread CPU. Analyzer plugins (level meter, spectrum analyzer, oscilloscope, spectrogram, stereo meter) keep their per-frame requestAnimationFrame redraw loops running, repainting the same idle picture forever. On a Pi 5 with a typical pipeline visible this is ~37% main-thread CPU even though nothing is happening.

  2. The output device never powers down. Even in sleep the AudioContext stays running, so the OS keeps a real-time output stream feeding the sink (silence). The downstream DAC/Amp therefore never sees an idle device and can't enter its own standby/power-save state.

Diagnosis

  1. The worklet's sleep mode (audioLevelMonitoring in plugins/audio-processor.js) only frees the audio render thread by bypassing DSP. The main thread drives analyzer redraws independently via startAnimation(), whose guard (added in Reduce main-thread CPU during GUI interaction (especially on low-power devices) #33) checks (this.enabled && this._sectionEnabled) but has no notion of the worklet being asleep.

  2. Sleep mode only changes what the worklet computes; it never touches the AudioContext. A running context keeps the output device open and fed, so neither the OS nor (on Linux) pipewire's suspend-on-idle ever releases it. The only thing that releases the device is audioContext.suspend() / close(). The catch: the worklet's process() - which also detects input returning and auto-wakes sleep - only runs while the context is running, so a naive suspend() would kill wake-on-input.

Proposed fix

  1. Propagate the worklet's existing sleepModeChanged transition to every plugin (PluginBase._setSleepMode()), extending the redraw guard so analyzer loops also pause while asleep. Any user interaction already wakes the worklet via the existing userActivity channel, so editing while idle stays safe.

  2. On sleep, suspend() the AudioContext to release the output device. Preserve auto-wake-on-audio by watching a clone of the live input track with a MediaStreamTrackProcessor - which opens no output device - and resuming on returning signal (plus user activity / window visibility). Gate the existing onstatechange auto-resume so it doesn't immediately undo the deliberate suspend.

    This second part is intentionally limited to non-macOS. On macOS EffeTune carries a carefully-tuned audio-device recovery path for HDMI hotplug / CoreAudio behavior that, of necessity, treats AudioContext suspend/close transitions as failure signals; a deliberate suspend there would mean threading through that hard-won machinery for little gain, since the power-saving target is a low-power always-on Linux host. If MediaStreamTrackProcessor is unavailable or the input isn't a live MediaStream (e.g. file playback), it falls back to not suspending so wake-on-input is never lost.

Measured effect (Raspberry Pi 5)

  • Idle main-thread CPU: ~37% -> ~2%.
  • Output device: with the worklet asleep, the DAC node drops from RUNNING to suspended (verified in pw-top), so the USB DAC enters standby. It resumes within a beat on returning audio or any UI activity (a brief relock is expected as the device re-opens).

Reference implementation

#37

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions