// ==UserScript==
// @name Robomonkey.io YouTube Evil Pitch Shifter
// @description Adjust video pitch and speed independently on YouTube videos
// @version 1.1.5
// @match https://*.youtube.com/*
// @icon https://www.youtube.com/s/desktop/1a8d73a2/img/favicon_32x32.png
// @namespace https://greasyfork.org/users/1502537
// ==/UserScript==
(function() {
'use strict';
let audioContext = null;
let sourceNode = null;
let gainNode = null;
let scriptProcessor = null;
let currentPitchCents = 0; // pitch in cents (100 cents = 1 semitone)
let currentSpeed = 1.0;
let isProcessing = false;
// Advanced pitch shifter using PSOLA (Pitch Synchronous Overlap Add) approach
class PitchShifter {
constructor(sampleRate) {
this.sampleRate = sampleRate;
this.frameSize = 2048;
this.hopSize = this.frameSize / 4;
this.overlapFactor = 4;
this.inputBuffer = new Float32Array(this.frameSize);
this.outputBuffer = new Float32Array(this.frameSize);
this.grainBuffer = new Float32Array(this.frameSize * 2);
this.position = 0;
this.grainPosition = 0;
}
// High-quality pitch shift using granular synthesis with windowing
process(inputBuffer, pitchRatio) {
const inputLength = inputBuffer.length;
const output = new Float32Array(inputLength);
if (Math.abs(pitchRatio - 1.0) < 0.001) {
// No pitch change, direct copy
return new Float32Array(inputBuffer);
}
const grainSize = 1024;
const overlap = grainSize / 2;
for (let i = 0; i < inputLength; i++) {
const readPos = i / pitchRatio;
const baseIndex = Math.floor(readPos);
const fraction = readPos - baseIndex;
// Bounds checking
if (baseIndex >= 0 && baseIndex < inputLength - 1) {
// Cubic interpolation for better quality
const y0 = baseIndex > 0 ? inputBuffer[baseIndex - 1] : inputBuffer[baseIndex];
const y1 = inputBuffer[baseIndex];
const y2 = inputBuffer[baseIndex + 1];
const y3 = baseIndex < inputLength - 2 ? inputBuffer[baseIndex + 2] : inputBuffer[baseIndex + 1];
// Cubic interpolation
const c0 = y1;
const c1 = 0.5 * (y2 - y0);
const c2 = y0 - 2.5 * y1 + 2 * y2 - 0.5 * y3;
const c3 = 0.5 * (y3 - y0) + 1.5 * (y1 - y2);
output[i] = ((c3 * fraction + c2) * fraction + c1) * fraction + c0;
} else if (baseIndex >= 0 && baseIndex < inputLength) {
output[i] = inputBuffer[baseIndex];
} else {
output[i] = 0;
}
// Apply windowing to reduce artifacts
if (i < overlap) {
const fadeIn = i / overlap;
output[i] *= fadeIn;
} else if (i > inputLength - overlap) {
const fadeOut = (inputLength - i) / overlap;
output[i] *= fadeOut;
}
}
return output;
}
}
let pitchShifter = null;
// Debounce function to prevent excessive calls
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Convert cents to pitch ratio (professional audio formula)
function centsToPitchRatio(cents) {
return Math.pow(2, cents / 1200);
}
// Convert semitones to cents
function semitonesToCents(semitones) {
return semitones * 100;
}
// Convert cents to semitones for display
function centsToSemitones(cents) {
return cents / 100;
}
// Format pitch display (like Transpose does)
function formatPitchDisplay(cents) {
const semitones = Math.round(cents / 100 * 10) / 10; // Round to 1 decimal
const absSemitones = Math.abs(semitones);
const sign = cents > 0 ? '+' : cents < 0 ? '-' : '';
if (cents === 0) return '0';
if (cents % 100 === 0) {
// Whole semitones
return `${sign}${Math.abs(Math.round(semitones))}`;
} else {
// Include decimal for partial semitones
return `${sign}${absSemitones.toFixed(1)}`;
}
}
// Create pitch shifter using Web Audio API
async function createPitchShifter(video) {
try {
if (audioContext) {
audioContext.close();
}
audioContext = new (window.AudioContext || window.webkitAudioContext)();
pitchShifter = new PitchShifter(audioContext.sampleRate);
// Create source and destination nodes
sourceNode = audioContext.createMediaElementSource(video);
gainNode = audioContext.createGain();
// Create script processor for pitch shifting
const bufferSize = 4096;
scriptProcessor = audioContext.createScriptProcessor(bufferSize, 2, 2);
scriptProcessor.onaudioprocess = function(event) {
if (Math.abs(currentPitchCents) < 1) {
// No significant pitch shift, just pass through
for (let channel = 0; channel < event.outputBuffer.numberOfChannels; channel++) {
const inputData = event.inputBuffer.getChannelData(channel);
const outputData = event.outputBuffer.getChannelData(channel);
outputData.set(inputData);
}
} else {
// Apply pitch shift using cents
const pitchRatio = centsToPitchRatio(currentPitchCents);
for (let channel = 0; channel < event.outputBuffer.numberOfChannels; channel++) {
const inputData = event.inputBuffer.getChannelData(channel);
const outputData = event.outputBuffer.getChannelData(channel);
const shifted = pitchShifter.process(inputData, pitchRatio);
outputData.set(shifted);
}
}
};
// Connect the audio graph
sourceNode.connect(scriptProcessor);
scriptProcessor.connect(gainNode);
gainNode.connect(audioContext.destination);
console.log('Advanced pitch shifter initialized');
return true;
} catch (error) {
console.error('Failed to create pitch shifter:', error);
return false;
}
}
// Apply speed changes (separate from pitch)
function applySpeed(video, speed) {
if (!video) return;
try {
video.playbackRate = speed;
console.log(`Applied speed: ${speed}x`);
} catch (error) {
console.error('Error applying speed:', error);
}
}
// Create pitch control UI
async function createPitchControls() {
const video = document.querySelector('video.html5-main-video');
if (!video) return null;
// Load saved position and state
const savedPosition = await GM.getValue('pitchShifter_position', { top: 80, right: 20, left: null });
const savedMinimized = await GM.getValue('pitchShifter_minimized', false);
// Create container for pitch controls
const pitchContainer = document.createElement('div');
pitchContainer.id = 'pitch-shifter-controls';
// Apply saved position
let positionCSS = `
position: fixed;
z-index: 10000;
display: flex;
flex-direction: column;
gap: 10px;
padding: 0;
background: rgba(0, 0, 0, 0.9);
border-radius: 10px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 13px;
color: white;
user-select: none;
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
min-width: 280px;
cursor: move;
`;
// Set position based on saved data
if (savedPosition.left !== null) {
positionCSS += `left: ${savedPosition.left}px; top: ${savedPosition.top}px;`;
} else {
positionCSS += `right: ${savedPosition.right}px; top: ${savedPosition.top}px;`;
}
pitchContainer.style.cssText = positionCSS;
// Create draggable header
const header = document.createElement('div');
header.id = 'pitch-shifter-header';
header.style.cssText = `
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: rgba(0, 0, 0, 0.3);
border-radius: 10px 10px 0 0;
cursor: move;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
`;
// Title
const title = document.createElement('div');
title.textContent = '🎼 Pitch Shifter';
title.style.cssText = 'font-weight: 600; color: #fff; font-size: 14px; flex: 1;';
// Control buttons container
const headerControls = document.createElement('div');
headerControls.style.cssText = 'display: flex; gap: 8px; align-items: center;';
// Minimize/Maximize button
const minimizeButton = document.createElement('button');
minimizeButton.innerHTML = savedMinimized ? '+' : '−';
minimizeButton.style.cssText = `
background: rgba(255, 255, 255, 0.1);
color: white;
border: none;
width: 24px;
height: 24px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
`;
// Close button
const closeButton = document.createElement('button');
closeButton.innerHTML = '×';
closeButton.style.cssText = `
background: rgba(255, 0, 0, 0.3);
color: white;
border: none;
width: 24px;
height: 24px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
`;
// Content container
const content = document.createElement('div');
content.id = 'pitch-shifter-content';
content.style.cssText = `
padding: 16px;
display: flex;
flex-direction: column;
gap: 10px;
`;
let isMinimized = savedMinimized;
// Apply saved minimized state
if (isMinimized) {
content.style.display = 'none';
pitchContainer.style.minWidth = 'auto';
}
// Save position function
async function savePosition() {
const rect = pitchContainer.getBoundingClientRect();
const position = {
top: rect.top,
left: rect.left,
right: null
};
// If positioned near right edge, save as right-based position
if (rect.left > window.innerWidth / 2) {
position.right = window.innerWidth - rect.right;
position.left = null;
}
await GM.setValue('pitchShifter_position', position);
console.log('Position saved:', position);
}
// Save minimized state function
async function saveMinimizedState(minimized) {
await GM.setValue('pitchShifter_minimized', minimized);
console.log('Minimized state saved:', minimized);
}
// Minimize/Maximize functionality
minimizeButton.addEventListener('click', async (e) => {
e.stopPropagation();
isMinimized = !isMinimized;
if (isMinimized) {
content.style.display = 'none';
minimizeButton.innerHTML = '+';
pitchContainer.style.minWidth = 'auto';
pitchContainer.style.cursor = 'move';
} else {
content.style.display = 'flex';
minimizeButton.innerHTML = '−';
pitchContainer.style.minWidth = '280px';
pitchContainer.style.cursor = 'move';
}
await saveMinimizedState(isMinimized);
});
// Close functionality
closeButton.addEventListener('click', (e) => {
e.stopPropagation();
pitchContainer.remove();
});
// Hover effects for buttons
minimizeButton.addEventListener('mouseenter', () => {
minimizeButton.style.background = 'rgba(255, 255, 255, 0.2)';
});
minimizeButton.addEventListener('mouseleave', () => {
minimizeButton.style.background = 'rgba(255, 255, 255, 0.1)';
});
closeButton.addEventListener('mouseenter', () => {
closeButton.style.background = 'rgba(255, 0, 0, 0.5)';
});
closeButton.addEventListener('mouseleave', () => {
closeButton.style.background = 'rgba(255, 0, 0, 0.3)';
});
// Drag functionality
let isDragging = false;
let startX, startY, startLeft, startTop;
function startDrag(e) {
// Only start drag if clicking on header or container, not on controls
if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON') {
if (e.target !== minimizeButton && e.target !== closeButton) {
return;
}
}
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = pitchContainer.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
pitchContainer.style.transition = 'none';
document.body.style.userSelect = 'none';
// Add global event listeners for drag
document.addEventListener('mousemove', handleDrag);
document.addEventListener('mouseup', endDrag);
}
function handleDrag(e) {
if (!isDragging) return;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
let newLeft = startLeft + deltaX;
let newTop = startTop + deltaY;
// Keep within viewport bounds
const containerRect = pitchContainer.getBoundingClientRect();
const maxLeft = window.innerWidth - containerRect.width;
const maxTop = window.innerHeight - containerRect.height;
newLeft = Math.max(0, Math.min(newLeft, maxLeft));
newTop = Math.max(0, Math.min(newTop, maxTop));
pitchContainer.style.left = `${newLeft}px`;
pitchContainer.style.top = `${newTop}px`;
pitchContainer.style.right = 'auto';
}
async function endDrag() {
isDragging = false;
pitchContainer.style.transition = '';
document.body.style.userSelect = '';
// Remove global event listeners
document.removeEventListener('mousemove', handleDrag);
document.removeEventListener('mouseup', endDrag);
// Save new position
await savePosition();
}
// Add drag event listeners to header and container
header.addEventListener('mousedown', startDrag);
pitchContainer.addEventListener('mousedown', (e) => {
// Only allow dragging from empty areas or header
if (e.target === pitchContainer || e.target === header || e.target === title) {
startDrag(e);
}
});
// Assemble header
headerControls.appendChild(minimizeButton);
headerControls.appendChild(closeButton);
header.appendChild(title);
header.appendChild(headerControls);
// Fine pitch controls section
const finePitchSection = document.createElement('div');
finePitchSection.style.cssText = 'border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 12px;';
const finePitchLabel = document.createElement('div');
finePitchLabel.textContent = 'Fine Pitch (cents)';
finePitchLabel.style.cssText = 'font-weight: 500; margin-bottom: 8px; color: #ccc; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px;';
const finePitchRow = document.createElement('div');
finePitchRow.style.cssText = 'display: flex; align-items: center; gap: 10px;';
const finePitchSlider = document.createElement('input');
finePitchSlider.type = 'range';
finePitchSlider.min = '-1200'; // -12 semitones in cents
finePitchSlider.max = '1200'; // +12 semitones in cents
finePitchSlider.value = '0';
finePitchSlider.step = '1'; // 1 cent precision
finePitchSlider.style.cssText = `
flex: 1;
height: 6px;
background: linear-gradient(to right, #ff4444, #333, #44ff44);
outline: none;
border-radius: 3px;
cursor: pointer;
-webkit-appearance: none;
`;
const finePitchValue = document.createElement('span');
finePitchValue.textContent = '0';
finePitchValue.style.cssText = `
min-width: 45px;
font-weight: 600;
color: #4CAF50;
font-family: 'Courier New', monospace;
font-size: 13px;
text-align: center;
background: rgba(76, 175, 80, 0.1);
padding: 4px 6px;
border-radius: 4px;
`;
// Coarse pitch controls (semitones)
const coarsePitchRow = document.createElement('div');
coarsePitchRow.style.cssText = 'display: flex; align-items: center; gap: 10px; margin-top: 8px;';
const coarsePitchLabel = document.createElement('span');
coarsePitchLabel.textContent = 'Semitones:';
coarsePitchLabel.style.cssText = 'font-weight: 500; min-width: 65px; color: #ccc;';
const coarsePitchSlider = document.createElement('input');
coarsePitchSlider.type = 'range';
coarsePitchSlider.min = '-12';
coarsePitchSlider.max = '12';
coarsePitchSlider.value = '0';
coarsePitchSlider.step = '1';
coarsePitchSlider.style.cssText = `
flex: 1;
height: 6px;
background: #333;
outline: none;
border-radius: 3px;
cursor: pointer;
-webkit-appearance: none;
`;
const coarsePitchValue = document.createElement('span');
coarsePitchValue.textContent = '0';
coarsePitchValue.style.cssText = 'min-width: 25px; text-align: center; font-weight: 600; color: #fff;';
// Speed controls section
const speedSection = document.createElement('div');
speedSection.style.cssText = 'border-top: 1px solid rgba(255,255,255,0.1); padding-top: 12px;';
const speedLabel = document.createElement('div');
speedLabel.textContent = 'Playback Speed';
speedLabel.style.cssText = 'font-weight: 500; margin-bottom: 8px; color: #ccc; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px;';
const speedRow = document.createElement('div');
speedRow.style.cssText = 'display: flex; align-items: center; gap: 10px;';
const speedSlider = document.createElement('input');
speedSlider.type = 'range';
speedSlider.min = '0.25';
speedSlider.max = '2.0';
speedSlider.value = '1.0';
speedSlider.step = '0.01'; // Fine speed control
speedSlider.style.cssText = `
flex: 1;
height: 6px;
background: #333;
outline: none;
border-radius: 3px;
cursor: pointer;
-webkit-appearance: none;
`;
const speedValue = document.createElement('span');
speedValue.textContent = '1.00x';
speedValue.style.cssText = `
min-width: 50px;
text-align: center;
font-weight: 600;
color: #2196F3;
font-family: 'Courier New', monospace;
background: rgba(33, 150, 243, 0.1);
padding: 4px 6px;
border-radius: 4px;
`;
// Control buttons
const buttonRow = document.createElement('div');
buttonRow.style.cssText = 'display: flex; gap: 8px; margin-top: 12px;';
const resetButton = document.createElement('button');
resetButton.textContent = 'Reset All';
resetButton.style.cssText = `
flex: 1;
background: #ff4444;
color: white;
border: none;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: background 0.2s;
`;
const toggleButton = document.createElement('button');
toggleButton.textContent = 'Bypass';
toggleButton.style.cssText = `
flex: 1;
background: #666;
color: white;
border: none;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: background 0.2s;
`;
let isBypassed = false;
// Add custom slider styling
const style = document.createElement('style');
style.textContent = `
#pitch-shifter-controls input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #fff;
cursor: pointer;
border: 2px solid #333;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
#pitch-shifter-controls input[type="range"]::-moz-range-thumb {
width: 18px;
height: 18px;
border-radius: 50%;
background: #fff;
cursor: pointer;
border: 2px solid #333;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
#pitch-shifter-controls button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
#pitch-shifter-header button:hover {
transform: none;
}
`;
document.head.appendChild(style);
// Event handlers
const debouncedApplyPitch = debounce((cents) => {
if (!isBypassed) {
currentPitchCents = cents;
console.log(`Pitch changed to: ${cents} cents (${centsToSemitones(cents).toFixed(2)} semitones)`);
}
}, 30);
const debouncedApplySpeed = debounce((speed) => {
currentSpeed = speed;
applySpeed(video, speed);
}, 30);
// Fine pitch slider
finePitchSlider.addEventListener('input', (e) => {
const cents = parseInt(e.target.value);
finePitchValue.textContent = formatPitchDisplay(cents);
// Update coarse slider to match
const semitones = Math.round(cents / 100);
coarsePitchSlider.value = semitones.toString();
coarsePitchValue.textContent = semitones > 0 ? `+${semitones}` : semitones.toString();
debouncedApplyPitch(cents);
});
// Coarse pitch slider
coarsePitchSlider.addEventListener('input', (e) => {
const semitones = parseInt(e.target.value);
const cents = semitonesToCents(semitones);
// Update fine slider
finePitchSlider.value = cents.toString();
finePitchValue.textContent = formatPitchDisplay(cents);
coarsePitchValue.textContent = semitones > 0 ? `+${semitones}` : semitones.toString();
debouncedApplyPitch(cents);
});
// Speed slider
speedSlider.addEventListener('input', (e) => {
const speed = parseFloat(e.target.value);
speedValue.textContent = `${speed.toFixed(2)}x`;
debouncedApplySpeed(speed);
});
// Reset button
resetButton.addEventListener('click', () => {
currentPitchCents = 0;
currentSpeed = 1.0;
finePitchSlider.value = '0';
coarsePitchSlider.value = '0';
speedSlider.value = '1.0';
finePitchValue.textContent = '0';
coarsePitchValue.textContent = '0';
speedValue.textContent = '1.00x';
applySpeed(video, 1.0);
console.log('Reset pitch and speed');
});
// Toggle bypass
toggleButton.addEventListener('click', () => {
isBypassed = !isBypassed;
toggleButton.textContent = isBypassed ? 'Enable' : 'Bypass';
toggleButton.style.background = isBypassed ? '#4CAF50' : '#666';
if (isBypassed) {
currentPitchCents = 0; // Bypass pitch processing
} else {
currentPitchCents = parseInt(finePitchSlider.value);
}
console.log(`Pitch processing ${isBypassed ? 'bypassed' : 'enabled'}`);
});
// Assemble the controls
pitchContainer.appendChild(header);
pitchContainer.appendChild(content);
content.appendChild(finePitchSection);
finePitchSection.appendChild(finePitchLabel);
finePitchSection.appendChild(finePitchRow);
finePitchRow.appendChild(finePitchSlider);
finePitchRow.appendChild(finePitchValue);
// Coarse pitch
finePitchSection.appendChild(coarsePitchRow);
coarsePitchRow.appendChild(coarsePitchLabel);
coarsePitchRow.appendChild(coarsePitchSlider);
coarsePitchRow.appendChild(coarsePitchValue);
// Speed section
content.appendChild(speedSection);
speedSection.appendChild(speedLabel);
speedSection.appendChild(speedRow);
speedRow.appendChild(speedSlider);
speedRow.appendChild(speedValue);
// Buttons
content.appendChild(buttonRow);
buttonRow.appendChild(resetButton);
buttonRow.appendChild(toggleButton);
return pitchContainer;
}
// Insert pitch controls into page
async function insertPitchControls() {
// Remove existing controls if any
const existing = document.getElementById('pitch-shifter-controls');
if (existing) {
existing.remove();
}
const pitchControls = await createPitchControls();
if (!pitchControls) {
console.log('Failed to create pitch controls');
return false;
}
// Insert into document body
document.body.appendChild(pitchControls);
console.log('Advanced pitch controls inserted successfully');
return true;
}
// Initialize pitch shifter for the current video
async function initializePitchShifter() {
console.log('Initializing pitch shifter...');
const video = document.querySelector('video.html5-main-video');
if (!video) {
console.log('Video element not found, retrying in 1 second...');
setTimeout(initializePitchShifter, 1000);
return;
}
console.log('Video found, creating pitch shifter...');
// Initialize audio context
const success = await createPitchShifter(video);
if (!success) {
console.error('Failed to initialize audio context');
return;
}
// Insert controls
await insertPitchControls();
// Resume audio context on user interaction
const resumeAudio = async () => {
if (audioContext && audioContext.state === 'suspended') {
await audioContext.resume();
console.log('Audio context resumed');
}
};
video.addEventListener('play', resumeAudio);
document.addEventListener('click', resumeAudio, { once: true });
}
// Observe DOM changes to handle YouTube's dynamic content
function observePlayerChanges() {
const observer = new MutationObserver(debounce(() => {
const video = document.querySelector('video.html5-main-video');
const existingControls = document.getElementById('pitch-shifter-controls');
if (video && !existingControls) {
console.log('Video detected via observer, initializing pitch shifter...');
initializePitchShifter();
}
}, 500));
observer.observe(document.body, {
childList: true,
subtree: true
});
return observer;
}
// Handle page navigation (YouTube SPA)
function handleNavigation() {
let currentUrl = window.location.href;
const checkUrlChange = () => {
if (window.location.href !== currentUrl) {
currentUrl = window.location.href;
console.log('YouTube navigation detected');
// Reset current values
currentPitchCents = 0;
currentSpeed = 1.0;
// Clean up existing audio context
if (audioContext) {
audioContext.close();
audioContext = null;
}
// Wait for new video to load
setTimeout(() => {
initializePitchShifter();
}, 1000);
}
};
// Check for URL changes periodically
setInterval(checkUrlChange, 1000);
// Also listen for popstate events
window.addEventListener('popstate', () => {
setTimeout(checkUrlChange, 500);
});
}
// Main initialization
function init() {
console.log('YouTube Advanced Pitch Shifter initialized');
// Start immediately
initializePitchShifter();
// Set up observers and navigation handling
observePlayerChanges();
handleNavigation();
// Also try after DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(initializePitchShifter, 500);
});
}
}
// Start the extension
init();
})();