diff --git a/src/i18n/resources/en.json b/src/i18n/resources/en.json index 4cf482aa3b..b080010cee 100644 --- a/src/i18n/resources/en.json +++ b/src/i18n/resources/en.json @@ -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", diff --git a/src/i18n/resources/fr.json b/src/i18n/resources/fr.json index 5d69ce916c..8e878e6618 100644 --- a/src/i18n/resources/fr.json +++ b/src/i18n/resources/fr.json @@ -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", diff --git a/src/plugins/pitch-shifter/index.ts b/src/plugins/pitch-shifter/index.ts new file mode 100644 index 0000000000..99b1d6d873 --- /dev/null +++ b/src/plugins/pitch-shifter/index.ts @@ -0,0 +1,48 @@ +import style from './style.css?inline'; +import { createPlugin } from '@/utils'; +import { onPlayerApiReady } from './renderer'; +import { t } from '@/i18n'; + + +/** + * 🎡 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). */ + enabled: boolean; + + /** Current pitch shift amount in semitones (-12 to +12). */ + semitones: number; +}; + +export default createPlugin({ + // 🧱 ─────────────── Plugin Metadata ─────────────── + name: () => t('plugins.pitch-shifter.name', 'Pitch Shifter'), + description: () => t('plugins.pitch-shifter.description'), + + /** Whether the app must restart when enabling/disabling the plugin. */ + restartNeeded: false, + + // βš™οΈ ─────────────── Default Configuration ─────────────── + config: { + enabled: false, // Plugin starts disabled by default + semitones: 0, // Neutral pitch (no shift) + } as PitchShifterPluginConfig, + + // 🎨 ─────────────── 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, + }, +}); diff --git a/src/plugins/pitch-shifter/renderer.tsx b/src/plugins/pitch-shifter/renderer.tsx new file mode 100644 index 0000000000..8f71b691db --- /dev/null +++ b/src/plugins/pitch-shifter/renderer.tsx @@ -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 +) => { + 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