-
Notifications
You must be signed in to change notification settings - Fork 1.9k
feat(plugins): add Slowed playback plugin #4400
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 2 commits
6823dd0
96e6470
8cd6297
874d992
c3ef41b
39fcec6
38f634b
2326edd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| import style from './style.css?inline'; | ||
| import { createPlugin } from '@/utils'; | ||
|
|
||
| export interface SlowedConfig { | ||
| enabled: boolean; | ||
| speed: number; | ||
| keepPitch: boolean; | ||
| } | ||
|
|
||
| const DEFAULT_CONFIG: SlowedConfig = { | ||
| enabled: false, | ||
| speed: 1.0, | ||
| keepPitch: false, | ||
| }; | ||
|
|
||
| let currentConfig = { ...DEFAULT_CONFIG }; | ||
|
|
||
| function applyConfig(cfg: SlowedConfig, video: HTMLVideoElement | null) { | ||
| if (!video) return; | ||
| currentConfig = cfg; | ||
| video.playbackRate = cfg.speed; | ||
| video.preservesPitch = cfg.keepPitch; | ||
| } | ||
|
|
||
| function getVideo(): HTMLVideoElement | null { | ||
| return document.querySelector<HTMLVideoElement>('video'); | ||
| } | ||
|
|
||
| const PRESETS: Record<string, Partial<SlowedConfig>> = { | ||
| slowed: { speed: 0.75, keepPitch: false }, | ||
| nightcore: { speed: 1.25, keepPitch: true }, | ||
| reset: { speed: 1.0, keepPitch: false }, | ||
| }; | ||
|
|
||
| function buildPanel( | ||
| cfg: SlowedConfig, | ||
| persist: (patch: Partial<SlowedConfig>) => void, | ||
| ): HTMLElement { | ||
| const panel = document.createElement('div'); | ||
| panel.id = 'sr-panel'; | ||
| panel.innerHTML = ` | ||
| <div class="sr-header" title="Clique para ocultar/mostrar"> | ||
| <span class="sr-logo">◈</span> | ||
| <span class="sr-title">SLOWED</span> | ||
| </div> | ||
| <div class="sr-body" id="sr-body"> | ||
| <div class="sr-presets"> | ||
| <button class="sr-btn" data-preset="slowed">Slowed</button> | ||
| <button class="sr-btn" data-preset="nightcore">Nightcore</button> | ||
| <button class="sr-btn sr-btn--danger" data-preset="reset">Reset</button> | ||
| </div> | ||
| <div class="sr-row"> | ||
| <div class="sr-label-row"> | ||
| <span class="sr-label">Speed</span> | ||
| <span class="sr-val" id="sr-speed-val">${cfg.speed.toFixed(2)}x</span> | ||
| </div> | ||
| <input class="sr-slider" id="sr-speed" type="range" min="0.5" max="1.5" step="0.01" value="${cfg.speed}" /> | ||
| </div> | ||
| <div class="sr-row sr-pitch-row"> | ||
| <span class="sr-label">Keep pitch</span> | ||
| <label class="sr-switch"> | ||
| <input type="checkbox" id="sr-pitch" ${cfg.keepPitch ? 'checked' : ''} /> | ||
| <span class="sr-thumb"></span> | ||
| </label> | ||
| </div> | ||
| <div class="sr-footer">Made by Kryz <3</div> | ||
| </div> | ||
| `; | ||
|
|
||
| const $ = <T extends HTMLElement>(sel: string) => panel.querySelector<T>(sel)!; | ||
| const speed = $<HTMLInputElement>('#sr-speed'); | ||
| const pitch = $<HTMLInputElement>('#sr-pitch'); | ||
| const body = $<HTMLDivElement>('#sr-body'); | ||
| const header = $<HTMLDivElement>('.sr-header'); | ||
|
ArjixWasTaken marked this conversation as resolved.
Outdated
|
||
|
|
||
| function updateSliderFill(input: HTMLInputElement) { | ||
| const min = parseFloat(input.min); | ||
| const max = parseFloat(input.max); | ||
| const val = parseFloat(input.value); | ||
| const pct = ((val - min) / (max - min)) * 100; | ||
| input.style.setProperty('--fill', `${pct}%`); | ||
| } | ||
|
|
||
| function syncSlider(input: HTMLInputElement, valEl: HTMLElement, format: (n: number) => string, key: keyof SlowedConfig, parse: (s: string) => number) { | ||
| input.addEventListener('input', () => { | ||
| const v = parse(input.value); | ||
| valEl.textContent = format(v); | ||
| (cfg as any)[key] = v; | ||
| currentConfig = cfg; | ||
| persist({ [key]: v } as any); | ||
| applyConfig(cfg, getVideo()); | ||
| updateSliderFill(input); | ||
| }); | ||
| updateSliderFill(input); | ||
| } | ||
|
|
||
| syncSlider(speed, $('#sr-speed-val'), (v) => v.toFixed(2) + 'x', 'speed', parseFloat); | ||
|
|
||
| pitch.addEventListener('change', () => { | ||
| cfg.keepPitch = pitch.checked; | ||
| currentConfig = cfg; | ||
| persist({ keepPitch: pitch.checked }); | ||
| applyConfig(cfg, getVideo()); | ||
| }); | ||
|
|
||
| panel.querySelectorAll<HTMLButtonElement>('[data-preset]').forEach((btn) => { | ||
| btn.addEventListener('click', () => { | ||
| const patch = PRESETS[btn.dataset.preset!]; | ||
| if (!patch) return; | ||
| Object.assign(cfg, patch); | ||
| currentConfig = cfg; | ||
| persist(patch); | ||
| if (patch.speed !== undefined) { | ||
| speed.value = String(patch.speed); | ||
| $('#sr-speed-val').textContent = patch.speed.toFixed(2) + 'x'; | ||
| updateSliderFill(speed); | ||
| } | ||
| if (patch.keepPitch !== undefined) { | ||
| pitch.checked = patch.keepPitch; | ||
| } | ||
| applyConfig(cfg, getVideo()); | ||
| }); | ||
| }); | ||
|
|
||
| let collapsed = false; | ||
| header.addEventListener('click', () => { | ||
| collapsed = !collapsed; | ||
| body.style.display = collapsed ? 'none' : ''; | ||
| }); | ||
|
|
||
| return panel; | ||
| } | ||
|
|
||
| let watchInterval: any = null; | ||
|
|
||
| export default createPlugin({ | ||
| name: () => 'SLOWED', | ||
| restartNeeded: false, | ||
| config: DEFAULT_CONFIG as any, | ||
| menu: async () => { return []; }, | ||
| renderer: { | ||
| start({ config, setConfig }) { | ||
| const styleSheet = document.createElement('style'); | ||
| styleSheet.textContent = style; | ||
| document.head.appendChild(styleSheet); | ||
|
ArjixWasTaken marked this conversation as resolved.
Outdated
|
||
| currentConfig = { ...DEFAULT_CONFIG, ...(config as any) }; | ||
| watchInterval = setInterval(() => { | ||
| const video = getVideo(); | ||
| if (!video) return; | ||
| if (!document.getElementById('sr-panel')) { | ||
| const panel = buildPanel(currentConfig, (patch) => setConfig(patch as any)); | ||
| document.body.appendChild(panel); | ||
| } | ||
| if (video.playbackRate !== currentConfig.speed) { | ||
| video.playbackRate = currentConfig.speed; | ||
| } | ||
| if (video.preservesPitch !== currentConfig.keepPitch) { | ||
| video.preservesPitch = currentConfig.keepPitch; | ||
| } | ||
| }, 300); | ||
| }, | ||
| onConfigChange(newConfig) { | ||
| currentConfig = newConfig as SlowedConfig; | ||
| applyConfig(currentConfig, getVideo()); | ||
| const panel = document.getElementById('sr-panel'); | ||
| if (!panel) return; | ||
| const setVal = (id: string, val: string) => { | ||
| const el = panel.querySelector<HTMLInputElement>(id); | ||
| if (el) { | ||
| el.value = val; | ||
| el.style.setProperty('--fill', `${((parseFloat(val) - parseFloat(el.min)) / (parseFloat(el.max) - parseFloat(el.min))) * 100}%`); | ||
| } | ||
| }; | ||
| setVal('#sr-speed', String((newConfig as any).speed)); | ||
| const pitch = panel.querySelector<HTMLInputElement>('#sr-pitch'); | ||
| if (pitch) pitch.checked = (newConfig as any).keepPitch; | ||
| const sv = panel.querySelector('#sr-speed-val'); | ||
| if (sv) sv.textContent = (newConfig as any).speed.toFixed(2) + 'x'; | ||
| }, | ||
| stop() { | ||
| if (watchInterval) clearInterval(watchInterval); | ||
| const video = getVideo(); | ||
| if (video) { | ||
| video.playbackRate = 1.0; | ||
| video.preservesPitch = true; | ||
| } | ||
| document.getElementById('sr-panel')?.remove(); | ||
| } | ||
| } | ||
| }); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is this file? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| # Slowed & Nightcore Plugin | ||
|
|
||
| Adds an interactive floating panel to easily apply "Slowed" or "Nightcore" effects to tracks. | ||
|
|
||
| ## Features | ||
| * **Playback Speed Control:** Precision slider from 0.5x to 1.5x. | ||
| * **Pitch Preservation:** Toggle to keep the original pitch or let it deepen/raise with the speed (true slowed/nightcore effect). | ||
| * **Presets:** Quick buttons for standard Slowed, Nightcore, and Reset. | ||
| * **Persistent Settings:** Preserves your chosen speed and pitch settings across track changes seamlessly. | ||
|
|
||
| ## Compatibility Note | ||
| This plugin operates strictly via the HTML5 `<video>` element properties (`playbackRate` and `preservesPitch`). It explicitly avoids the Web Audio API to guarantee 100% compatibility with other audio-hijacking plugins (like native Equalizers or Crossfade). | ||
|
Comment on lines
+11
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Soften the The implementation still polls and mutates shared 🤖 Prompt for AI Agents |
||
Uh oh!
There was an error while loading. Please reload this page.