Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/i18n/resources/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,10 @@
"button": "Picture-in-picture"
}
},
"pitch-shifter": {
"description": "Adds real-time pitch control slider for YouTube Music",
"name": "Pitch Shifter"
},
"playback-speed": {
"description": "Listen fast, listen slow! Adds a slider that controls song speed",
"name": "Playback Speed",
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/resources/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,10 @@
"button": "Image dans l'image"
}
},
"pitch-shifter": {
"description": "Changez la hauteur de la musique sans affecter la vitesse de lecture",
"name": "Changeur de hauteur tonale"
},
"playback-speed": {
"description": "Γ‰coutez vite, Γ©coutez lentementΒ ! Ajoute un curseur qui contrΓ΄le la vitesse de la chanson",
"name": "Vitesse de lecture",
Expand Down
48 changes: 48 additions & 0 deletions src/plugins/pitch-shifter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import style from './style.css?inline';
import { createPlugin } from '@/utils';
import { onPlayerApiReady } from './renderer';
import { t } from '@/i18n';


/**
Comment on lines +6 to +7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐢
Delete ⏎

Suggested change
/**
/**

* 🎡 Pitch Shifter Plugin (Tone.js + Solid.js Edition)
* Author: TheSakyo
*
* Provides real-time pitch shifting for YouTube Music using Tone.js,
* allowing users to raise or lower the key of a song dynamically.
*/
export type PitchShifterPluginConfig = {
/** Whether the plugin is enabled (active in the player). */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐢
Replace β†Ή with Β·Β·

Suggested change
/** Whether the plugin is enabled (active in the player). */
/** Whether the plugin is enabled (active in the player). */

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <stylistic/no-tabs> reported by reviewdog 🐢
Unexpected tab character.

enabled: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐢
Replace β†Ή with Β·Β·

Suggested change
enabled: boolean;
enabled: boolean;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <stylistic/no-tabs> reported by reviewdog 🐢
Unexpected tab character.


/** Current pitch shift amount in semitones (-12 to +12). */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐢
Replace β†Ή with Β·Β·

Suggested change
/** Current pitch shift amount in semitones (-12 to +12). */
/** Current pitch shift amount in semitones (-12 to +12). */

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <stylistic/no-tabs> reported by reviewdog 🐢
Unexpected tab character.

semitones: number;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐢
Replace β†Ή with Β·Β·

Suggested change
semitones: number;
semitones: number;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <stylistic/no-tabs> reported by reviewdog 🐢
Unexpected tab character.

};

export default createPlugin({
// 🧱 ─────────────── Plugin Metadata ───────────────
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐢
Replace β†Ή with Β·Β·

Suggested change
// 🧱 ─────────────── Plugin Metadata ───────────────
// 🧱 ─────────────── Plugin Metadata ───────────────

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <stylistic/no-tabs> reported by reviewdog 🐢
Unexpected tab character.

name: () => t('plugins.pitch-shifter.name', 'Pitch Shifter'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐢
Replace β†Ή with Β·Β·

Suggested change
name: () => t('plugins.pitch-shifter.name', 'Pitch Shifter'),
name: () => t('plugins.pitch-shifter.name', 'Pitch Shifter'),

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <stylistic/no-tabs> reported by reviewdog 🐢
Unexpected tab character.

description: () => t('plugins.pitch-shifter.description'),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐢
Replace β†Ή with Β·Β·

Suggested change
description: () => t('plugins.pitch-shifter.description'),
description: () => t('plugins.pitch-shifter.description'),

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <stylistic/no-tabs> reported by reviewdog 🐢
Unexpected tab character.


/** Whether the app must restart when enabling/disabling the plugin. */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐢
Replace β†Ή with Β·Β·

Suggested change
/** Whether the app must restart when enabling/disabling the plugin. */
/** Whether the app must restart when enabling/disabling the plugin. */

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <stylistic/no-tabs> reported by reviewdog 🐢
Unexpected tab character.

restartNeeded: false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐢
Replace β†Ή with Β·Β·

Suggested change
restartNeeded: false,
restartNeeded: false,

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <stylistic/no-tabs> reported by reviewdog 🐢
Unexpected tab character.


// βš™οΈ ─────────────── Default Configuration ───────────────
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐢
Replace β†Ή with Β·Β·

Suggested change
// βš™οΈ ─────────────── Default Configuration ───────────────
// βš™οΈ ─────────────── Default Configuration ───────────────

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <stylistic/no-tabs> reported by reviewdog 🐢
Unexpected tab character.

config: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐢
Replace β†Ή with Β·Β·

Suggested change
config: {
config: {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <stylistic/no-tabs> reported by reviewdog 🐢
Unexpected tab character.

enabled: false, // Plugin starts disabled by default
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐢
Replace β†Ήβ†Ή with Β·Β·Β·Β·

Suggested change
enabled: false, // Plugin starts disabled by default
enabled: false, // Plugin starts disabled by default

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <stylistic/no-tabs> reported by reviewdog 🐢
Unexpected tab character.

semitones: 0, // Neutral pitch (no shift)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐢
Replace β†Ήβ†Ήsemitones:Β·0,Β·Β· with Β·Β·Β·Β·semitones:Β·0,

Suggested change
semitones: 0, // Neutral pitch (no shift)
semitones: 0, // Neutral pitch (no shift)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <stylistic/no-tabs> reported by reviewdog 🐢
Unexpected tab character.

} as PitchShifterPluginConfig,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐢
Replace β†Ή with Β·Β·

Suggested change
} as PitchShifterPluginConfig,
} as PitchShifterPluginConfig,

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <stylistic/no-tabs> reported by reviewdog 🐢
Unexpected tab character.


// 🎨 ─────────────── Plugin Stylesheet ───────────────
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <prettier/prettier> reported by reviewdog 🐢
Replace β†Ή with Β·Β·

Suggested change
// 🎨 ─────────────── Plugin Stylesheet ───────────────
// 🎨 ─────────────── Plugin Stylesheet ───────────────

/** Inline CSS loaded into the YT Music renderer for consistent styling. */
stylesheets: [style],

// 🎧 ─────────────── Renderer Logic ───────────────
/**
* The renderer is triggered once the YouTube Music player API is available.
* It handles all DOM interactions, UI injection, and audio processing.
*/
renderer: {
onPlayerApiReady,
},
});
206 changes: 206 additions & 0 deletions src/plugins/pitch-shifter/renderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { createSignal, onCleanup, createEffect } from "solid-js";
import { render } from "solid-js/web";
import * as Tone from "tone";
import type { RendererContext } from "@/types/contexts";
import type { PitchShifterPluginConfig } from "./index";

/**
* 🎡 Pitch Shifter Plugin (Tone.js + Solid.js Edition)
* βœ… Real-time pitch updates
* βœ… Single slider instance
* βœ… Clean removal on disable
* βœ… Dynamic slider color (cool β†’ neutral β†’ warm)
* βœ… Glassmorphism-ready UI
* Author: TheSakyo
*/
export const onPlayerApiReady = async (
_,
{ getConfig, setConfig }: RendererContext<PitchShifterPluginConfig>
) => {
console.log("[pitch-shifter] Renderer (Solid) initialized βœ…");

const userConfig = await getConfig();
const [enabled, setEnabled] = createSignal(userConfig.enabled);
const [semitones, setSemitones] = createSignal(userConfig.semitones ?? 0);

let media: HTMLMediaElement | null = null;
let pitchShift: Tone.PitchShift | null = null;
let nativeSource: MediaStreamAudioSourceNode | null = null;
let mount: HTMLDivElement | null = null;

/** 🎧 Wait for <video> element */
const waitForMedia = (): Promise<HTMLMediaElement> =>
new Promise((resolve) => {
const check = () => {
const el =
document.querySelector("video") ||
document.querySelector("audio") ||
document.querySelector("ytmusic-player video");
if (el) resolve(el as HTMLMediaElement);
else setTimeout(check, 400);
};
check();
});

media = await waitForMedia();
console.log("[pitch-shifter] Media found 🎧", media);

await Tone.start();
const toneCtx = Tone.getContext();
const stream =
(media as any).captureStream?.() || (media as any).mozCaptureStream?.();
if (!stream) {
console.error("[pitch-shifter] ❌ captureStream() unavailable");
return;
}

/** 🎚️ Setup pitch shifting (only once) */
const setupPitchShift = () => {
if (pitchShift) return;
pitchShift = new Tone.PitchShift({
pitch: semitones(),
windowSize: 0.1,
}).toDestination();
nativeSource = toneCtx.createMediaStreamSource(stream);
Tone.connect(nativeSource, pitchShift);
media!.muted = true;
console.log("[pitch-shifter] Pitch processor active 🎢");
};

/** πŸ“΄ Teardown cleanly */
const teardownPitchShift = () => {
pitchShift?.dispose();
pitchShift = null;
nativeSource?.disconnect();
nativeSource = null;
media!.muted = false;
console.log("[pitch-shifter] Pitch processor stopped πŸ“΄");
};

/** 🎨 Solid component for slider UI */
const PitchUI = () => {
/** πŸ’‘ Utility: compute slider gradient based on pitch */
const getSliderGradient = (value: number) => {
// Map -12 β†’ 0, 0 β†’ 0.5, 12 β†’ 1
const normalized = (value + 12) / 24;
const cold = [77, 166, 255]; // blue
const neutral = [255, 77, 77]; // red
const warm = [255, 170, 51]; // orange

let color: number[];
if (value < 0) {
// blend blue β†’ red
const t = normalized * 2;
color = cold.map((c, i) => Math.round(c + (neutral[i] - c) * t));
} else {
// blend red β†’ orange
const t = (normalized - 0.5) * 2;
color = neutral.map((c, i) => Math.round(c + (warm[i] - c) * t));
}
return `linear-gradient(90deg, rgb(${color.join(",")}) 0%, #fff 100%)`;
};

/** 🎚️ Update slider color when pitch changes */
const updateSliderColor = (slider: HTMLInputElement, value: number) => {
slider.style.background = getSliderGradient(value);
};

return (
<div class="pitch-wrapper">
<input
type="range"
min="-12"
max="12"
step="1"
value={semitones()}
class="pitch-slider"
onInput={(e) => {
const slider = e.target as HTMLInputElement;
const v = parseInt(slider.value);
setSemitones(v);
setConfig({ semitones: v });
if (pitchShift) pitchShift.pitch = v;
updateSliderColor(slider, v);

const labelEl = document.querySelector(".pitch-label");
if (labelEl) {
labelEl.classList.add("active");
setTimeout(() => labelEl.classList.remove("active"), 200);
}
}}
ref={(el) => updateSliderColor(el, semitones())}
/>
<span class="pitch-label">
{semitones() >= 0 ? "+" : ""}
{semitones()} semitones
</span>
<button
class="pitch-reset"
title="Reset pitch"
onClick={() => {
setSemitones(0);
setConfig({ semitones: 0 });
if (pitchShift) pitchShift.pitch = 0;
const slider = document.querySelector(
".pitch-slider"
) as HTMLInputElement;
if (slider) updateSliderColor(slider, 0);

const labelEl = document.querySelector(".pitch-label");
if (labelEl) {
labelEl.classList.add("active");
setTimeout(() => labelEl.classList.remove("active"), 200);
}
}}
>
πŸ”„
</button>
</div>
);
};

/** 🧱 Mount UI (only once) */
const injectUI = () => {
const tabs = document.querySelector("tp-yt-paper-tabs.tab-header-container");
if (tabs && tabs.parentElement && !document.querySelector(".pitch-wrapper")) {
mount = document.createElement("div");
tabs.parentElement.insertBefore(mount, tabs);
render(() => <PitchUI />, mount);
console.log("[pitch-shifter] UI injected via Solid βœ…");
}
};

/** 🧹 Remove UI on disable */
const removeUI = () => {
const existing = document.querySelector(".pitch-wrapper");
if (existing) {
existing.remove();
mount = null;
console.log("[pitch-shifter] UI removed ❌");
}
};

/** πŸ” React to plugin state */
createEffect(() => {
if (enabled()) {
setupPitchShift();
injectUI();
} else {
teardownPitchShift();
removeUI();
}
});

/** ⏱️ Periodically sync config */
const interval = setInterval(async () => {
const conf = await getConfig();
if (conf.enabled !== enabled()) setEnabled(conf.enabled);
if (conf.semitones !== semitones()) setSemitones(conf.semitones);
}, 1000);

onCleanup(() => {
clearInterval(interval);
teardownPitchShift();
removeUI();
});
};
110 changes: 110 additions & 0 deletions src/plugins/pitch-shifter/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
.pitch-wrapper {
display: flex;
align-items: center;
justify-content: center;
gap: 14px;
margin-bottom: 6px;
user-select: none;
font-family: "Inter", "Segoe UI", sans-serif;
background: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(12px) saturate(180%);
border-radius: 12px;
padding: 8px 14px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
transition: background 0.3s ease, transform 0.2s ease;
}

.pitch-wrapper:hover {
background: rgba(255, 255, 255, 0.08);
transform: translateY(-1px);
}

/* 🎚️ Premium gradient slider */
.pitch-slider {
width: 160px;
height: 5px;
appearance: none;
border-radius: 3px;
background: linear-gradient(90deg, #ff4d4d 0%, #ff8080 100%);
outline: none;
cursor: pointer;
transition: filter 0.2s ease, transform 0.2s ease;
}

.pitch-slider:hover {
filter: brightness(1.15);
transform: scaleX(1.03);
}

/* Chrome / Edge thumb */
.pitch-slider::-webkit-slider-thumb {
appearance: none;
width: 16px;
height: 16px;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
border: 2px solid #ff4d4d;
transition: all 0.25s ease;
box-shadow: 0 0 6px rgba(255, 77, 77, 0.5);
}

.pitch-slider::-webkit-slider-thumb:hover {
background: #ff4d4d;
border-color: #fff;
box-shadow: 0 0 12px rgba(255, 77, 77, 0.8);
transform: scale(1.15);
}

/* Firefox thumb */
.pitch-slider::-moz-range-thumb {
width: 16px;
height: 16px;
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
border: 2px solid #ff4d4d;
transition: all 0.25s ease;
box-shadow: 0 0 6px rgba(255, 77, 77, 0.5);
}

.pitch-slider::-moz-range-thumb:hover {
background: #ff4d4d;
border-color: #fff;
box-shadow: 0 0 12px rgba(255, 77, 77, 0.8);
transform: scale(1.15);
}

/* 🎡 Animated label */
.pitch-label {
color: #fff;
font-size: 0.9rem;
min-width: 80px;
text-align: center;
letter-spacing: 0.4px;
text-shadow: 0 0 8px rgba(255, 255, 255, 0.1);
transition: transform 0.15s ease, opacity 0.15s ease;
}

.pitch-label.active {
transform: scale(1.25);
opacity: 0.7;
}

/* πŸ”„ Animated reset icon */
.pitch-reset {
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
font-size: 1.3rem;
color: #ff6666;
cursor: pointer;
transition: transform 0.4s ease, color 0.4s ease, filter 0.4s ease;
}

.pitch-reset:hover {
transform: rotate(360deg) scale(1.25);
color: #ffaaaa;
filter: drop-shadow(0 0 10px rgba(255, 77, 77, 0.7));
}
Loading