Skip to content

Notification sounds register app in macOS Now Playing / Media Control Center #439

@joshhbk

Description

@joshhbk

When notification sounds are enabled, playing a sound causes the app to appear in the macOS Now Playing widget (Control Center) as if it were media playback, complete with play/pause controls:

Image

This can also cause AirPods to switch focus from one device to another which can be annoying if trying to listen to music on one device and work on another.

It happens because src/utils/notificationSounds.ts uses new Audio(url) / audio.play(), which macOS treats as media content regardless of duration. A way to fix this is to switch from HTMLAudioElement to the Web Audio API - it shouldn't register with the OS media session.

I've tested a fix for this locally that appears to work as expected:

src/utils/notificationSounds.ts

import type { DebugEntry } from "../types";

type DebugLogger = (entry: DebugEntry) => void;

type SoundLabel = "success" | "error" | "test";

let audioContext: AudioContext | null = null;

function getAudioContext(): AudioContext {
  if (!audioContext || audioContext.state === "closed") {
    audioContext = new AudioContext();
  }
  return audioContext;
}

export function playNotificationSound(
  url: string,
  label: SoundLabel,
  onDebug?: DebugLogger,
) {
  try {
    const ctx = getAudioContext();

    if (ctx.state === "suspended") {
      void ctx.resume();
    }

    fetch(url)
      .then((response) => response.arrayBuffer())
      .then((arrayBuffer) => ctx.decodeAudioData(arrayBuffer))
      .then((audioBuffer) => {
        const source = ctx.createBufferSource();
        const gainNode = ctx.createGain();
        gainNode.gain.value = 0.05;
        source.buffer = audioBuffer;
        source.connect(gainNode);
        gainNode.connect(ctx.destination);
        source.start();
      })
      .catch((error) => {
        onDebug?.({
          id: `${Date.now()}-audio-${label}-play-error`,
          timestamp: Date.now(),
          source: "error",
          label: `audio/${label} play error`,
          payload: error instanceof Error ? error.message : String(error),
        });
      });
  } catch (error) {
    onDebug?.({
      id: `${Date.now()}-audio-${label}-init-error`,
      timestamp: Date.now(),
      source: "error",
      label: `audio/${label} init error`,
      payload: error instanceof Error ? error.message : String(error),
    });
  }
}

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