A browser extension that lets you shift a YouTube video's pitch up or down (±12 semitones, one full octave) without changing the tempo, so you can sing along to karaoke videos in a key that suits your voice.
Works on desktop Firefox, Firefox for Android, and Chrome.
A content script routes the YouTube <video> element's audio through the Web
Audio API:
video → MediaElementAudioSourceNode → SoundTouchNode (pitch shift) → GainNode → speakers
AudioWorklet (no deprecated ScriptProcessorNode).GainNode mirrors YouTube's own volume/mute controls, because
MediaElementAudioSourceNode does not apply them itself.storage.local.ON badge while it's enabled.npm install
npm run build # both targets
npm run build:firefox # dist/firefox only
npm run build:chrome # dist/chrome only
Each target gets its own folder — dist/firefox/ and dist/chrome/ —
containing manifest.json, the bundled content.js and background.js,
panel.css, icons, and the web-accessible soundtouch-processor.js.
All of src/ is shared. The two targets diverge in exactly two places, both
handled by build.js:
src/manifest.json is the Firefox manifest and is shipped
verbatim. The Chrome one is derived from it, so the version and description
can't drift: the gecko-only keys and action.default_area are dropped,
background.scripts becomes background.service_worker, the icons are
repointed at PNGs, and minimum_chrome_version is added.action.setIcon(),
so the build rasterizes src/icons/*.svg to PNGs at 16/32/48/128 with
sharp. Firefox keeps using the SVGs. background.js receives the right icon
paths through an esbuild define.Both targets share one API shim — globalThis.browser ?? globalThis.chrome —
rather than a polyfill, since every API used here (storage.local, runtime,
tabs, action) has been promise-based in Chrome since version 88. browser
is checked first because Firefox also defines a callback-style chrome alias.
npm run run
web-ext run launches a temporary Firefox profile with the extension loaded.
Click the toolbar button to show the panel, then open a karaoke video and use
− / + or the slider, and confirm the key changes while the tempo stays the
same. Then verify:
createMediaElementSource).npm run build:chrome
Then open chrome://extensions, enable Developer mode, click Load
unpacked, and select dist/chrome/. Pin the toolbar button, then run through
the same checks as the Firefox list above. Two things worth confirming
specifically on Chrome:
chrome.runtime.getURL(); if the extension API were unavailable the worklet
would fail to load and the audio would play unshifted.ON badge are still correct after Chrome has idled the
service worker (it restarts on demand and re-derives that state).Release Firefox for Android only installs add-ons listed on addons.mozilla.org. Two options:
about:debugging over USB.Change browser_specific_settings.gecko.id in src/manifest.json to a
unique ID you control (the placeholder karaoke-pitch@example.com is not
submittable). The manifest already declares "gecko_android": {} (required
to list the add-on for Firefox for Android) and
data_collection_permissions (this extension collects nothing).
Run npm run lint and fix any warnings.
Set your AMO API credentials as environment variables (create them at https://addons.mozilla.org/developers/addon/api/key/):
export WEB_EXT_API_KEY=user:123456:789
export WEB_EXT_API_SECRET=your-secret
Then sign and submit:
npm run sign
The listing metadata (summary, category, license) comes from
amo-metadata.json. web-ext sign uploads the package to AMO; you can also
upload the dist/firefox/ ZIP manually at https://addons.mozilla.org/developers/.
npm run build:chrome
npm run package:chrome
This writes web-ext-artifacts/karaoke_pitch-<version>-chrome.zip with
manifest.json at the archive root, ready to upload at the
Developer Dashboard. Unlike
AMO, this step is manual and needs a one-time $5 developer registration fee.
The listing also asks for justifications, which are worth having ready:
storage permission: remembers the chosen pitch and the panel's
visibility, per video, on the local device.youtube.com host access: the extension only functions on YouTube watch,
Shorts, embed, and live pages, where the <video> element it processes lives.storage.local.MPL-2.0. Third-party notices are in THIRD_PARTY_NOTICES.txt.
18 commits
JavaScript
93.7%
CSS
6.3%
A browser extension that lets you shift a YouTube video's pitch up or down (±12 semitones, one full octave) without changing the tempo, so you can sing along to karaoke videos in a key that suits your voice.
Works on desktop Firefox, Firefox for Android, and Chrome.
A content script routes the YouTube <video> element's audio through the Web
Audio API:
video → MediaElementAudioSourceNode → SoundTouchNode (pitch shift) → GainNode → speakers
AudioWorklet (no deprecated ScriptProcessorNode).GainNode mirrors YouTube's own volume/mute controls, because
MediaElementAudioSourceNode does not apply them itself.storage.local.ON badge while it's enabled.npm install
npm run build # both targets
npm run build:firefox # dist/firefox only
npm run build:chrome # dist/chrome only
Each target gets its own folder — dist/firefox/ and dist/chrome/ —
containing manifest.json, the bundled content.js and background.js,
panel.css, icons, and the web-accessible soundtouch-processor.js.
All of src/ is shared. The two targets diverge in exactly two places, both
handled by build.js:
src/manifest.json is the Firefox manifest and is shipped
verbatim. The Chrome one is derived from it, so the version and description
can't drift: the gecko-only keys and action.default_area are dropped,
background.scripts becomes background.service_worker, the icons are
repointed at PNGs, and minimum_chrome_version is added.action.setIcon(),
so the build rasterizes src/icons/*.svg to PNGs at 16/32/48/128 with
sharp. Firefox keeps using the SVGs. background.js receives the right icon
paths through an esbuild define.Both targets share one API shim — globalThis.browser ?? globalThis.chrome —
rather than a polyfill, since every API used here (storage.local, runtime,
tabs, action) has been promise-based in Chrome since version 88. browser
is checked first because Firefox also defines a callback-style chrome alias.
npm run run
web-ext run launches a temporary Firefox profile with the extension loaded.
Click the toolbar button to show the panel, then open a karaoke video and use
− / + or the slider, and confirm the key changes while the tempo stays the
same. Then verify:
createMediaElementSource).npm run build:chrome
Then open chrome://extensions, enable Developer mode, click Load
unpacked, and select dist/chrome/. Pin the toolbar button, then run through
the same checks as the Firefox list above. Two things worth confirming
specifically on Chrome:
chrome.runtime.getURL(); if the extension API were unavailable the worklet
would fail to load and the audio would play unshifted.ON badge are still correct after Chrome has idled the
service worker (it restarts on demand and re-derives that state).Release Firefox for Android only installs add-ons listed on addons.mozilla.org. Two options:
about:debugging over USB.Change browser_specific_settings.gecko.id in src/manifest.json to a
unique ID you control (the placeholder karaoke-pitch@example.com is not
submittable). The manifest already declares "gecko_android": {} (required
to list the add-on for Firefox for Android) and
data_collection_permissions (this extension collects nothing).
Run npm run lint and fix any warnings.
Set your AMO API credentials as environment variables (create them at https://addons.mozilla.org/developers/addon/api/key/):
export WEB_EXT_API_KEY=user:123456:789
export WEB_EXT_API_SECRET=your-secret
Then sign and submit:
npm run sign
The listing metadata (summary, category, license) comes from
amo-metadata.json. web-ext sign uploads the package to AMO; you can also
upload the dist/firefox/ ZIP manually at https://addons.mozilla.org/developers/.
npm run build:chrome
npm run package:chrome
This writes web-ext-artifacts/karaoke_pitch-<version>-chrome.zip with
manifest.json at the archive root, ready to upload at the
Developer Dashboard. Unlike
AMO, this step is manual and needs a one-time $5 developer registration fee.
The listing also asks for justifications, which are worth having ready:
storage permission: remembers the chosen pitch and the panel's
visibility, per video, on the local device.youtube.com host access: the extension only functions on YouTube watch,
Shorts, embed, and live pages, where the <video> element it processes lives.storage.local.MPL-2.0. Third-party notices are in THIRD_PARTY_NOTICES.txt.
18 commits
JavaScript
93.7%
CSS
6.3%