A browser extension that forces stereo audio to mono, so single-channel ("broken") audio plays in both ears.
Some videos, podcasts, and livestreams ship with audio mixed to only one channel. If you have hearing loss in one ear, you listen with one earbud, or you just find single-sided audio uncomfortable, that content is partially or completely silent. This extension fixes it by routing every <video> and <audio> element on a page through a small Web Audio graph that sums L and R into a mono signal, then writes that signal to both output channels.
It runs only on the sites you choose, stores its settings locally, and sends nothing to any server.
| Browser | Supported | Install |
|---|---|---|
| Firefox | 115 and later | Add-ons site or temporary load |
| Chrome | Recent versions | Web Store or unpacked load |
| Opera | Chromium-based versions | Chrome Web Store via "Install Chrome Extensions" addon, or unpacked load |
| Edge, Brave, Vivaldi | Not officially tested, but should work | Unpacked load |
- Per-site override (force on, force off, or follow the global default).
- Global default for sites without an override.
- Adjustable sum gain (default 1.0 for full-volume accessibility playback, lower if proper stereo clips after summing).
- Smooth crossfade when toggling, so there are no clicks or pops.
- Catches dynamically inserted media (single-page apps, late-mounting players).
- Status indicator in the popup showing whether the extension is active and how many media elements it's handling on the current page.
- Zero data collection. Settings live in
chrome.storage.localand never leave the browser.
- Firefox
- Chrome coming soon!
- Opera coming soon!
- Download or clone this repository.
- Open
chrome://extensions(Opera:opera://extensions). - Turn on Developer mode in the top right.
- Click Load unpacked and select the project folder.
Tip
On Opera, the Chrome Web Store version of this extension can also be installed by first adding the official "Install Chrome Extensions" addon from the Opera addons site.
Temporary install (cleared when Firefox restarts, useful for testing):
- Open
about:debugging#/runtime/this-firefox. - Click Load Temporary Add-on.
- Select
manifest.jsonfrom the project folder.
Permanent install (requires a signed XPI):
- Use
web-extto package the folder. - Submit to addons.mozilla.org for signing, or self-host with an
update_url.
Click the toolbar icon. The popup shows three controls:
- This site. Sets the override for the current origin. Choose "Use default", "Force on", or "Force off".
- Default for all sites. A single switch that applies everywhere no override is set.
- Sum gain. A slider from 0.30 to 1.00.
Note
Default sum gain is 1.0, not the conventional 0.7. The reason: at 1.0, audio that exists only on one channel plays at full volume in both ears, which is the accessibility goal. Lower the gain only if you mostly listen to legitimately stereo content and want clipping protection.
If a tab was already open when you installed the extension, reload it once so the content script can attach to the existing media elements.
Audio graph diagram (click to expand)
splitter sumGain merger
+-------+ +--------+ +--------+
+-->| L |--->| |---0->| out L |
| | R |--->| L+R |---0->| out R |--+
| +-------+ +--------+ +--------+ |
| |
| v
+------------+ | +---------+
| <video> / |--+ +--->| monoOut |---+
| <audio> | | +---------+ |
+------------+----------------------------------------+ v
| +-------------+
| +--->| destination |
| | +-------------+
v |
+--------+
|bypassOut|
+--------+
Two parallel paths run from each media element:
- Mono path. A
ChannelSplitterseparates L and R, both feed into a singleGainNode(which sums them), and aChannelMergerwrites the summed signal to both output channels. - Bypass path. The original stereo source connects straight through.
A pair of gain nodes (monoOut and bypassOut) crossfade between the two paths over 50 ms when the user toggles the effect, so the transition is inaudible.
Why a parallel bypass path instead of disconnecting nodes?
Disconnecting and reconnecting Web Audio nodes on every toggle would cause clicks and risks race conditions with the audio thread. Keeping both paths permanently connected and crossfading their gains is cheap, glitch-free, and makes the toggle feel instant.
Why document_start and all_frames: true?
Many sites mount media elements before DOMContentLoaded, especially video players that use Media Source Extensions. Running at document_start and watching the DOM with a MutationObserver ensures the extension catches every element regardless of when it appears. all_frames: true covers embedded iframes, which is where most video players live.
mono-mixdown/
├── manifest.json Manifest V3, cross-browser
├── content.js Builds the audio graph, syncs with storage
├── popup.html Toolbar popup UI
├── popup.css Popup styling
├── popup.js Popup logic and content script ping
├── icon.png 128x128 icon
└── README.md This file
There is no background service worker. Settings flow through chrome.storage.local, and content scripts react directly to storage.onChanged.
| Category | Status |
|---|---|
| Personal data collected | None |
| Telemetry / analytics | None |
| Remote servers contacted | None |
| Data stored | Local settings only (chrome.storage.local) |
| Accounts required | None |
This is reflected in manifest.json via data_collection_permissions: { required: ["none"] }, which is the value Mozilla requires for extensions that don't collect or transmit personal data.
Important
A few hard limits exist that no browser extension can work around. These are listed here so users know what to expect, not as bugs.
- DRM-protected playback (Netflix, Spotify Web Player, Apple Music, and similar services). Audio is decoded inside a protected pipeline that browser extensions cannot reach. The effect will not apply.
- Cross-origin media without CORS headers. When a site loads audio or video from another domain without the right headers, the browser blocks Web Audio from processing it. Major platforms (YouTube, Twitch, Vimeo, most podcast players) are unaffected. For sites that hotlink raw media files, force the effect off for that site.
- Browser internal pages.
chrome://,about:, the Web Store, and the Mozilla Add-ons site cannot run extensions, by design. - Local files. On Chromium browsers,
file://access is off by default. To enable it, open the extension's details page and turn on "Allow access to file URLs".
The extension is plain JavaScript with no build step. To package it for distribution:
# Chromium (.zip)
zip -r mono-mixdown.zip . -x "*.git*" "*.DS_Store" "node_modules/*" "*.md"
# Firefox (.xpi via web-ext)
npm install -g web-ext
web-ext buildThe output ZIP/XPI can be uploaded to the Chrome Web Store, the Opera addons site, or addons.mozilla.org.
Issues and pull requests are welcome. A few areas where help would be useful:
- Testing on browsers that aren't in the support table.
- Reporting sites where the extension misbehaves (with the URL and any console errors).
- Translation of popup strings.
- Per-element controls for users who want different behavior on different videos within the same page.
MIT. See LICENSE for details.
Built on the Web Audio API, which makes this kind of routing about a hundred lines of code. Thanks to the spec authors and the browser teams that implement it consistently across vendors.