Skip to content

Reduce idle power in sleep mode: pause analyzer redraws and release the output device#37

Open
takaeda wants to merge 3 commits into
Frieve-A:mainfrom
takaeda:optimize/reduce-idle-power-on-sleep
Open

Reduce idle power in sleep mode: pause analyzer redraws and release the output device#37
takaeda wants to merge 3 commits into
Frieve-A:mainfrom
takaeda:optimize/reduce-idle-power-on-sleep

Conversation

@takaeda
Copy link
Copy Markdown
Contributor

@takaeda takaeda commented May 28, 2026

Reference implementation for #36. The symptom, diagnosis and reasoning live in the issue; this PR is the patch.

Two independent idle-power drains while the worklet is asleep, kept as two commits so each is reviewable / revertible on its own:

  1. Pause analyzer redraws - propagate the worklet's sleepModeChanged to plugins via PluginBase._setSleepMode(), extending the existing startAnimation() guard (from Reduce main-thread CPU during GUI interaction (especially on low-power devices) #33) with the sleep state. ~37% -> ~2% main-thread CPU on a Pi 5.

  2. Release the output device - suspend() the AudioContext on sleep so the OS output stream (and the downstream DAC/Amp) is released. Auto-wake is preserved by a device-free MediaStreamTrackProcessor watcher over a clone of the input track (new js/audio/input-activity-watcher.js); user activity and window visibility also wake via AudioManager.wakeFromSleep(). The context manager's onstatechange auto-resume is gated so the deliberate suspend isn't undone. No-op on macOS (stays clear of the HDMI/CoreAudio recovery path there); also falls back to the status quo when MediaStreamTrackProcessor is unavailable or the input isn't a live MediaStream.

Verified on a Raspberry Pi 5 (input = virtual mic, output = USB DAC): the DAC node goes from RUNNING to suspended in pipewire while asleep and resumes on returning audio or UI activity; idle main-thread CPU drops as above.

Happy to split this into two PRs if you'd prefer to take them separately.

takaeda and others added 2 commits May 28, 2026 18:06
The worklet already enters a sleep mode after 60s of silence + no
user activity (see audioLevelMonitoring in audio-processor.js): it
stops running plugin DSP and just passes input to output. The main
thread however kept analyzer plugins' per-frame redraw loops
running, so a fully-idle EffeTune still consumed visible main-thread
CPU on low-power hardware.

Propagate the sleep state to every plugin via _setSleepMode() so the
analyzer redraw loops can pause too. Each startAnimation() guard
already short-circuits when (this.enabled && this._sectionEnabled)
is false; this extends the same guard to also short-circuit when
this._sleepMode is true.

Editing while in sleep is safe: EventManager already forwards any
mousedown / mousemove / wheel / click / key / touch event to the
worklet as 'userActivity', which wakes the worklet immediately and
emits 'sleepModeChanged: false' back, so the redraw loops resume
before the first interaction frame the user sees.
EffeTune's worklet already enters sleep mode after 60s of input/output
silence + no user activity, but the AudioContext stays running, which
keeps the OS output stream (and any downstream DAC/Amp) fully awake -
wasteful on an always-on low-power host (Raspberry Pi 5 + external DAC).

Suspend the AudioContext on sleep so the output device is released and
the DAC/Amp can enter its own standby. The catch is that the suspended
worklet can no longer detect input returning (its process() stops), which
is what normally auto-wakes sleep. To keep auto-wake-on-audio, watch a
clone of the live input track with a MediaStreamTrackProcessor that opens
no output device (new InputActivityWatcher); on signal - or on any user
activity / window-visibility change - resume the context and nudge the
worklet awake.

Integration details:
- AudioContextManager gains setIntentionalSuspend(); its onstatechange
  auto-resume (added for macOS device-change recovery) skips the resume
  while the suspend is deliberate, so it doesn't immediately undo us.
- EventManager.handleUserActivity and app.js visibilitychange route
  through AudioManager.wakeFromSleep when sleep-suspended, since the frozen
  worklet can't act on a userActivity message.
- _doReset tears down the watcher and clears the intentional-suspend flag
  before rebuilding the graph, so a reset during sleep can't leave the new
  context unable to auto-resume.

Intentionally a no-op on macOS: that platform's carefully-tuned HDMI /
CoreAudio recovery treats AudioContext suspend/close transitions as
failure signals, so a deliberate suspend would collide with it for little
gain. If MediaStreamTrackProcessor is unavailable or the input isn't a
live MediaStream (e.g. file playback), we skip suspending and keep the
status-quo wake-on-input.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant