DEV Community

Cover image for Your Laptop Speakers Pop When You Pause? It's Not a Bug, It's Power Saving
vadim albarov
vadim albarov

Posted on

Your Laptop Speakers Pop When You Pause? It's Not a Bug, It's Power Saving

My ASUS Zephyrus has great speakers and one annoying habit: a pop every time sound starts or stops. Pause a video: pop. Play again: pop. Click "skip 10 seconds" five times in a row on YouTube: pop-pop-pop-pop-pop.

This is not an ASUS problem, and not a problem of one model. Many modern laptops have it. It seems most common on laptops with good speakers, where the speakers are driven by power-saving "smart amps". I have only tested one laptop, so take that part as a guess.

This post shows how I found the cause and fixed it with a small PowerShell script. No admin rights, no driver hacks.

The hardware

The speakers are not driven by a classic onboard codec. Device Manager shows a SoundWire setup:

  • Cirrus Logic CS35L56 smart amps (left and right speakers)
  • Cirrus Logic CS42L43 codec (headphone jack, mics)
  • Intel Smart Sound Technology over SoundWire

Smart amps like the CS35L56 save power. When the audio line goes silent, they shut down. When sound comes back, they power up again. On this laptop, that power change is loud enough to hear as a pop.

Step 1: Find out what causes the pop

There are two common causes:

  1. The stream – the app opens or closes an audio stream, and the DSP mutes/unmutes.
  2. The power-down – the amp goes to sleep on silence and wakes up on sound.

To tell them apart, I wrote a test script (Test-SilentKeeper.ps1) that plays a looping file of pure digital silence while I use the laptop as normal.

The pop was still there. That was a useful clue: pure zeros keep the Windows audio stream open, but the amp still detects silence and goes to sleep. So the amp itself watches the signal level.

Step 2: What did not work

Most forum posts say "disable power saving for the audio device". I tried that in the registry for the three SoundWire amp devices:

HKLM\SYSTEM\CurrentControlSet\Control\Class\{4d36e96c-e325-11ce-bfc1-08002be10318}\<instance>\PowerSettings
  ConservationIdleTime = FF FF FF FF
  PerformanceIdleTime  = FF FF FF FF
  IdlePowerState       = 00 00 00 00
Enter fullscreen mode Exit fullscreen mode

After a reboot: no change. The amp's silence gate runs inside the amp and its firmware, not through Windows device power management. I reverted it.

Step 3: Keep the sound device busy... all the time

The next idea: keep the sound device busy with a signal so quiet that you can't hear it, but loud enough that the amp doesn't see "silence".

There are small tools that do exactly this. I used SoundKeeper (open source, MIT). Any similar tool that plays a steady, inaudible signal to the output device should work the same way. The important part is that it plays a real signal, not pure zeros, because the amp ignores zeros.

This is not an advertisement. I have no ties to SoundKeeper or its author. It is simply the tool I found first, and it worked.

With SoundKeeper, the command is:

SoundKeeper64.exe sine f10 a5
Enter fullscreen mode Exit fullscreen mode

That is a 10 Hz sine wave at 5% volume. Humans can't hear 10 Hz, and laptop speakers can't really play it anyway. But to the amp, it is "not silence", so it stays awake.

Result: no more pops.

The downside: the amps never sleep. That costs battery, and it feels wrong to keep the amps on all day just to avoid a pop when I pause a video.

Step 4: Keep the amp awake only when it matters

What I want:

  • When I'm watching or listening to something, keep the amp awake, so pause, seek and skip don't pop.
  • When nothing has played for a while, let the amp sleep.

So I wrote AmpPopGuard: a watchdog that

  1. reads the peak level of the default output device every 200 ms,
  2. starts the keeper tool (SoundKeeper in my case) when it hears real audio,
  3. stops it after 10 seconds with no real audio.

Rapid seeking now happens inside the 10-second window, so the amp never sleeps between clicks. After real silence, you get one pop on the next cold start, and that's it.

Reading the peak meter from PowerShell

Windows exposes the output level through the Core Audio API (IAudioMeterInformation). It's the same meter you see in the Volume Mixer. PowerShell can't call it directly, but Add-Type with a few lines of C# COM interop works fine:

[Guid("C02216F6-8C67-4B5B-9D00-D008E73E0064"),
 InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IAudioMeterInformation
{
    int GetPeakValue(out float pfPeak);
}
Enter fullscreen mode Exit fullscreen mode

Get the default render device from IMMDeviceEnumerator, Activate the meter, and call GetPeakValue – you get a float from 0.0 to 1.0.

The trap: the script hears itself

The first version had a bug. The keeper's own 10 Hz sine shows up on the same peak meter at about 0.01–0.02. With a single threshold of 0.01, the watchdog started the keeper, then "heard audio" (its own sine), and never stopped it.

The fix is two thresholds:

State Counts as real audio when peak >
Keeper off 0.01
Keeper on 0.05

While the keeper runs, only sound clearly louder than its own sine resets the 10-second timer.

The full script

# AmpPopGuard.ps1
# Prevents the Cirrus amp pop on rapid pause/seek WITHOUT keeping the amps
# awake all the time.
# Only one instance at a time
$script:mutex = New-Object System.Threading.Mutex($false, 'Global\AmpPopGuard')
if (-not $script:mutex.WaitOne(0)) { exit }

$KeeperExe     = Join-Path $PSScriptRoot 'SoundKeeper64.exe'   # script lives next to the exe
$KeeperArgs    = 'sine', 'f10', 'a5'   # 10 Hz sine @ 5% - inaudible, defeats the amp's silence gate
$LingerSeconds = 10      # keep amps awake this long after last real audio
# Two thresholds: the keeper's own 10 Hz sine reads ~0.01-0.02 on the meter,
# so while it runs, only levels above ThresholdOn count as real audio.
$ThresholdOff  = 0.01    # start trigger while keeper is off
$ThresholdOn   = 0.05    # sustain trigger while keeper is running
$PollMs        = 200

if (-not (Test-Path $KeeperExe)) {
    Write-Error "SoundKeeper not found at $KeeperExe"
    exit 1
}

# --- Core Audio peak meter interop -----------------------------------------
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;

[ComImport, Guid("BCDE0395-E52F-467C-8E3D-C4579291692E")]
class MMDeviceEnumeratorComObject { }

[Guid("A95664D2-9614-4F35-A746-DE8DB63617E6"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDeviceEnumerator
{
    int NotImpl1();
    int GetDefaultAudioEndpoint(int dataFlow, int role, out IMMDevice ppDevice);
}

[Guid("D666063F-1587-4E43-81F1-B948E807363F"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IMMDevice
{
    int Activate(ref Guid iid, int dwClsCtx, IntPtr pActivationParams,
        [MarshalAs(UnmanagedType.IUnknown)] out object ppInterface);
}

[Guid("C02216F6-8C67-4B5B-9D00-D008E73E0064"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IAudioMeterInformation
{
    int GetPeakValue(out float pfPeak);
}

public static class AudioMeter
{
    static IAudioMeterInformation meter;

    public static float GetPeak()
    {
        if (meter == null)
        {
            var en = (IMMDeviceEnumerator)new MMDeviceEnumeratorComObject();
            IMMDevice dev;
            en.GetDefaultAudioEndpoint(0, 1, out dev);   // eRender, eMultimedia
            var iid = typeof(IAudioMeterInformation).GUID;
            object o;
            dev.Activate(ref iid, 23, IntPtr.Zero, out o); // CLSCTX_ALL
            meter = (IAudioMeterInformation)o;
        }
        float peak;
        meter.GetPeakValue(out peak);
        return peak;
    }

    public static void Reset() { meter = null; }  // call after device change errors
}
'@

function Stop-Keeper {
    Get-Process -Name 'SoundKeeper64' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
}

# Clean slate: no stray keeper from a previous run
Stop-Keeper

# --- Watchdog loop ----------------------------------------------------------
$keeperOn = $false
$lastLoud = [DateTime]::MinValue

Write-Host "AmpPopGuard running (linger ${LingerSeconds}s, thresholds $ThresholdOff/$ThresholdOn). Keeper: $KeeperExe"

try {
    while ($true) {
        try {
            $peak = [AudioMeter]::GetPeak()
        } catch {
            # default device changed / audio engine restarted - reconnect
            [AudioMeter]::Reset()
            Start-Sleep -Milliseconds 1000
            continue
        }

        $now = [DateTime]::UtcNow
        $threshold = if ($keeperOn) { $ThresholdOn } else { $ThresholdOff }
        if ($peak -gt $threshold) {
            $lastLoud = $now
            if (-not $keeperOn) {
                Start-Process -FilePath $KeeperExe -ArgumentList $KeeperArgs -WindowStyle Hidden | Out-Null
                $keeperOn = $true
            }
        } elseif ($keeperOn -and ($now - $lastLoud).TotalSeconds -gt $LingerSeconds) {
            Stop-Keeper
            $keeperOn = $false
        }

        Start-Sleep -Milliseconds $PollMs
    }
} finally {
    Stop-Keeper
}
Enter fullscreen mode Exit fullscreen mode

Put SoundKeeper64.exe in the same folder as the script. Get it from the GitHub releases or the project page. I use v1.3.6. To use a different keeper tool, change $KeeperExe, $KeeperArgs and the process name in Stop-Keeper.

Start it at login, hidden

PowerShell's -WindowStyle Hidden still flashes a window for a moment. A small VBS launcher in the Startup folder avoids that. Save as
%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\AmpPopGuard.vbs:

Set sh = CreateObject("WScript.Shell")
sh.Run """C:\path\to\pwsh.exe"" -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File ""C:\path\to\AmpPopGuard.ps1""", 0, False
Enter fullscreen mode Exit fullscreen mode

To stop it:

Get-CimInstance Win32_Process -Filter "Name='pwsh.exe'" |
  ? CommandLine -match AmpPopGuard.ps1 | % { Stop-Process -Id $_.ProcessId }
Enter fullscreen mode Exit fullscreen mode

Trade-offs

  • One pop is left: the first sound after more than 10 s of silence still pops. That pop is part of how the amp works.
  • Cost: one COM call every 200 ms. CPU use is near zero.
  • Tuning: $LingerSeconds sets how long the amp stays awake. I use 10. The thresholds may need small changes on other hardware.
  • Device changes: if you switch outputs (headphones, Bluetooth), the meter call fails, and the script reconnects to the new default device.

Does this apply to your laptop?

Probably, if you have:

  • a pop or click when audio starts or stops,
  • that gets worse when you seek or skip quickly,
  • and "disable power saving" tweaks don't help.

Many modern laptops from different brands use smart amps (Cirrus Logic CS35Lxx, TI TAS, Realtek and others). These amps often have the same silence gate. Higher-end laptops with better speakers seem to use them more, but I am not sure how wide it goes. The script does not depend on the brand or the model. It only watches the Windows peak meter.

Have you seen this on other laptops? Tell me in the comments.

Top comments (0)