Enhanced Audio Speed Controller with Time Info, Speed Highlight, Font Size Control, and Toggle

Adds time information (duration, currentTime, etc.), adjusts for playback speed, highlights the active speed button, toggle for hiding/showing the control panel, and font size control.

  1. // ==UserScript==
  2. // @name Enhanced Audio Speed Controller with Time Info, Speed Highlight, Font Size Control, and Toggle
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.3
  5. // @description Adds time information (duration, currentTime, etc.), adjusts for playback speed, highlights the active speed button, toggle for hiding/showing the control panel, and font size control.
  6. // @author You
  7. // @match *://*/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function () {
  13. 'use strict';
  14.  
  15. let startTime = Date.now(); // Track the real-time start
  16. let isPanelVisible = true; // Track panel visibility
  17. let currentFontSize = 7; // Initial font size in pt
  18.  
  19. const odIcon = '🕰️';
  20. const adIcon = '⏰';
  21. const ctIcon = '⌚';
  22. const pcIcon = '➗';
  23. const trIcon = '⏳';
  24. const wcIcon = '🕛';
  25.  
  26. // Helper function to convert fractional minutes into hh:mm:ss format
  27. function convertToTimeFormat(minutes) {
  28. const totalSeconds = Math.floor(minutes * 60); // Convert minutes to seconds
  29. const hours = Math.floor(totalSeconds / 3600); // Calculate full hours
  30. const remainingSeconds = totalSeconds % 3600; // Remaining seconds after hours
  31. const mins = Math.floor(remainingSeconds / 60); // Full minutes
  32. const secs = remainingSeconds % 60; // Remaining seconds
  33.  
  34. const formattedTime =
  35. (hours > 9 ? hours : '0' + hours) + ':' +
  36. (mins > 9 ? mins : '0' + mins) + ':' +
  37. (secs > 9 ? secs : '0' + secs);
  38.  
  39. return formattedTime;
  40. }
  41.  
  42. // Function to calculate and update time stats
  43. function updateTimeStats() {
  44. const audioElement = document.querySelector('audio');
  45. if (audioElement && audioElement.duration && !isNaN(audioElement.duration)) {
  46. const duration = audioElement.duration;
  47. const adjustedDuration = audioElement.duration / audioElement.playbackRate / 60;
  48. const currentTime = audioElement.currentTime;
  49. const playbackRate = audioElement.playbackRate;
  50. const currentTimeDisp = currentTime / 60 / playbackRate;
  51. const percentComplete = (currentTime / duration) * 100;
  52.  
  53. const timeRemaining = (adjustedDuration - currentTimeDisp);
  54. const elapsedWallClockTime = (Date.now() - startTime) / 1000 / 60;
  55.  
  56. document.getElementById('original-duration').innerHTML = `${odIcon}<br>${convertToTimeFormat(duration / 60)}`;
  57. document.getElementById('adjusted-duration').innerHTML = `${adIcon}<br>${convertToTimeFormat(adjustedDuration)}`;
  58. document.getElementById('current-time').innerHTML = `${ctIcon}<br>${convertToTimeFormat(currentTimeDisp)}`;
  59. document.getElementById('percent-complete').innerHTML = `<span class="rotate">${pcIcon}</span><br>${percentComplete.toFixed(2)}%`;
  60. document.getElementById('time-remaining').innerHTML = `${trIcon}<br>${convertToTimeFormat(timeRemaining)}`;
  61. document.getElementById('elapsed-wall-clock').innerHTML = `${wcIcon}<br>${convertToTimeFormat(elapsedWallClockTime)}`;
  62. }
  63. }
  64.  
  65. // Function to create the control panel with buttons and time information
  66. function createControlPanel() {
  67. if (document.getElementById('audio-speed-control')) return;
  68.  
  69. const controlDiv = document.createElement('div');
  70. controlDiv.id = 'audio-speed-control';
  71. controlDiv.style.position = 'fixed';
  72. controlDiv.style.top = '20%';
  73. controlDiv.style.right = '0';
  74. controlDiv.style.background = 'rgba(0, 0, 0, 0.05)';
  75. controlDiv.style.padding = '5px';
  76. controlDiv.style.borderRadius = '5px';
  77. controlDiv.style.zIndex = '999999';
  78. controlDiv.style.display = 'flex';
  79. controlDiv.style.flexDirection = 'column';
  80. controlDiv.style.fontSize = `${currentFontSize}pt`;
  81. controlDiv.style.transition = 'transform 0.5s ease';
  82.  
  83. const timeStats = document.createElement('div');
  84. timeStats.style.marginBottom = '4px';
  85. timeStats.style.fontSize = '6pt';
  86. timeStats.style.color = 'black';
  87. timeStats.style.fontWeight = "bold";
  88. timeStats.style.textAlign = 'center';
  89.  
  90. const timeStatsData = [
  91. { id: 'original-duration', label: odIcon },
  92. { id: 'adjusted-duration', label: adIcon },
  93. { id: 'current-time', label: ctIcon },
  94. { id: 'percent-complete', label: pcIcon },
  95. { id: 'time-remaining', label: trIcon },
  96. { id: 'elapsed-wall-clock', label: wcIcon }
  97. ];
  98.  
  99. timeStatsData.forEach(stat => {
  100. const statDiv = document.createElement('div');
  101. statDiv.id = stat.id;
  102. statDiv.innerHTML = stat.label + "<br>--:--:--";
  103. timeStats.appendChild(statDiv);
  104. });
  105.  
  106. controlDiv.appendChild(timeStats);
  107.  
  108. // Create PLAY button
  109. const playButton = document.createElement('button');
  110. playButton.innerText = '▶'; // Label with play symbol
  111. playButton.style.padding = '3px 5px';
  112. playButton.style.marginBottom = '2px';
  113. playButton.style.backgroundColor = '#bada55';
  114. playButton.style.border = 'none';
  115. playButton.style.borderRadius = '3px';
  116. playButton.style.cursor = 'pointer';
  117. playButton.style.fontSize = '8px';
  118. playButton.style.fontWeight = 'bold';
  119. playButton.style.color = '#222';
  120. playButton.style.width = '36px';
  121.  
  122. playButton.addEventListener('click', function () {
  123. const audioElement = document.querySelector('audio');
  124. if (audioElement) {
  125. let attempts = 25;
  126. audioElement.play(); // Initial attempt
  127.  
  128. const playInterval = setInterval(() => {
  129. if (attempts > 0) {
  130. audioElement.play(); // Force play again
  131. attempts--;
  132. } else {
  133. clearInterval(playInterval);
  134. }
  135. }, 125);
  136. }
  137. });
  138.  
  139. controlDiv.appendChild(playButton);
  140.  
  141. // Create PAUSE button
  142. const pauseButton = document.createElement('button');
  143. pauseButton.innerText = '||'; // Label with pause symbol
  144. pauseButton.style.padding = '3px 5px';
  145. pauseButton.style.marginBottom = '2px';
  146. pauseButton.style.backgroundColor = '#bada55';
  147. pauseButton.style.border = 'none';
  148. pauseButton.style.borderRadius = '3px';
  149. pauseButton.style.cursor = 'pointer';
  150. pauseButton.style.fontSize = '8px';
  151. pauseButton.style.fontWeight = 'bold';
  152. pauseButton.style.color = '#222';
  153. pauseButton.style.width = '36px';
  154.  
  155. pauseButton.addEventListener('click', function () {
  156. const audioElement = document.querySelector('audio');
  157. if (audioElement) {
  158. let attempts = 25;
  159. audioElement.pause(); // Initial attempt
  160.  
  161. const pauseInterval = setInterval(() => {
  162. if (attempts > 0) {
  163. audioElement.pause(); // Force pause again
  164. attempts--;
  165. } else {
  166. clearInterval(pauseInterval);
  167. }
  168. }, 125);
  169. }
  170. });
  171.  
  172. controlDiv.appendChild(pauseButton);
  173.  
  174. /*
  175. // Ceate PAUoE button
  176. cst pauseButton = document.createElement('button');
  177. pauseButton.innerText = '||'; // Label with pause symbol
  178. pauseButton.style.padding = '3px 5px';
  179. pauseButton.style.marginBottom = '2px';
  180. pauseButton.style.backgroundColor = '#bada55';
  181. pauseButton.style.border = 'none';
  182. pauseButton.style.borderRadius = '3px';
  183. pauseButton.style.cursor = 'pointer';
  184. pauseButton.style.fontSize = '8px';
  185. = 'bold';
  186. = '#222';
  187. ');
  188. if (audioElement) {
  189. const targetState = audioElement.paused; // Determine the desired state
  190.  
  191. function tryTogglePlayPause() {
  192. if (targetState) {
  193. audioElement.play().catch(error => {
  194. console.error("Error playing audio:", error);
  195. setTimeout(tryTogglePlayPause, 100);
  196. });
  197. pauseButton.innerText = '||';
  198. } else {
  199. audioElement.pause();
  200. pauseButton.innerText = '▶';
  201. }
  202. }
  203.  
  204. tryTogglePlayPause(); // Initial attempt
  205. if (targetState) {
  206. // If the target state is playing, schedule retries
  207. setTimeout(tryTogglePlayPause, 100);
  208. }
  209. }
  210. });
  211.  
  212.  
  213. // pauseButton.addEventListener('click', function () {
  214. // const audioElement = document.querySelector('audio');
  215. // if (audioElement) {
  216. // if (audioElement.paused) {
  217. // audioElement.play();
  218. // pauseButton.innerText = '||';
  219. // } else {
  220. // audioElement.pause();
  221. // pauseButton.innerText = '▶';
  222. // }
  223. // }
  224. // });
  225.  
  226. // controlDiv.appendChild(pauseButton);
  227. */
  228.  
  229. const speeds = [1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 3.25, 3.5, 3.75, 4.0, 4.5, 5.0];
  230. let activeButton = null;
  231.  
  232. speeds.forEach(speed => {
  233. const button = document.createElement('button');
  234. button.innerText = speed.toFixed(2);
  235. button.style.padding = '3px 5px';
  236. button.style.marginBottom = '2px';
  237. button.style.backgroundColor = '#bada55';
  238. button.style.border = 'none';
  239. button.style.borderRadius = '3px';
  240. button.style.cursor = 'pointer';
  241. button.style.fontSize = '8px';
  242. button.style.fontWeight = 'bold';
  243. button.style.color = '#222';
  244. button.style.width = '36px';
  245.  
  246. function setActiveButton() {
  247. if (activeButton) {
  248. activeButton.style.backgroundColor = '#bada55';
  249. activeButton.style.color = '#222';
  250. activeButton.style.fontWeight = 'normal';
  251. }
  252. button.style.backgroundColor = '#006400';
  253. button.style.color = '#fff';
  254. button.style.fontWeight = 'bold';
  255. activeButton = button;
  256. }
  257.  
  258. button.addEventListener('click', function () {
  259. const audioElement = document.querySelector('audio');
  260. if (audioElement) {
  261. audioElement.playbackRate = speed;
  262. setActiveButton();
  263. }
  264. });
  265.  
  266. controlDiv.appendChild(button);
  267. });
  268.  
  269. const toggleButton = document.createElement('button');
  270. toggleButton.innerText = '◀';
  271. toggleButton.style.position = 'absolute';
  272. toggleButton.style.top = '5px';
  273. toggleButton.style.left = '-15px';
  274. toggleButton.style.backgroundColor = '#bada55';
  275. toggleButton.style.border = 'none';
  276. toggleButton.style.borderRadius = '20%';
  277. toggleButton.style.cursor = 'pointer';
  278. toggleButton.style.padding = '2px';
  279. toggleButton.style.zIndex = '1000';
  280.  
  281. toggleButton.addEventListener('click', () => {
  282. if (isPanelVisible) {
  283. controlDiv.style.transform = 'translateX(100%)';
  284. toggleButton.innerText = '▶';
  285. } else {
  286. controlDiv.style.transform = 'translateX(0)';
  287. toggleButton.innerText = '◀';
  288. }
  289. isPanelVisible = !isPanelVisible;
  290. });
  291.  
  292. controlDiv.appendChild(toggleButton);
  293.  
  294. // Font size control
  295. const fontSizeControl = document.createElement('div');
  296. fontSizeControl.style.marginTop = '4px';
  297.  
  298. const fontIncreaseButton = document.createElement('button');
  299. fontIncreaseButton.innerText = '⬆';
  300. fontIncreaseButton.style.margin = '2px';
  301. fontIncreaseButton.style.padding = '3px';
  302. fontIncreaseButton.style.cursor = 'pointer';
  303.  
  304. fontIncreaseButton.addEventListener('click', () => {
  305. currentFontSize += 2;
  306. console.log("Size: " + currentFontSize);
  307. controlDiv.style.fontSize = `${currentFontSize}pt`;
  308. });
  309.  
  310. const fontDecreaseButton = document.createElement('button');
  311. fontDecreaseButton.innerText = '⬇';
  312. fontDecreaseButton.style.margin = '2px';
  313. fontDecreaseButton.style.padding = '3px';
  314. fontDecreaseButton.style.cursor = 'pointer';
  315.  
  316. fontDecreaseButton.addEventListener('click', () => {
  317. currentFontSize = Math.max(4, currentFontSize - 2); // Prevent font size from going too small
  318. controlDiv.style.fontSize = `${currentFontSize}pt`;
  319. });
  320.  
  321. fontSizeControl.appendChild(fontIncreaseButton);
  322. fontSizeControl.appendChild(fontDecreaseButton);
  323.  
  324. controlDiv.appendChild(fontSizeControl);
  325.  
  326. document.body.appendChild(controlDiv);
  327. }
  328.  
  329. const style = document.createElement('style');
  330. style.innerHTML = `
  331. .rotate {
  332. display: inline-block;
  333. transform: rotate(45deg); /* Rotates emoji */
  334. }
  335. `;
  336. document.head.appendChild(style);
  337.  
  338. // Update the time stats periodically
  339. setInterval(updateTimeStats, 1000); // Update every second
  340.  
  341. // Wait for the document to fully load and ensure audio element exists
  342. const observer = new MutationObserver((mutations, observer) => {
  343. const audioElement = document.querySelector('audio');
  344. if (audioElement) {
  345. console.log("Chapter duration:");
  346. console.log(audioElement.duration / 60);
  347. audioElement.addEventListener('loadedmetadata', () => {
  348. audioElement.playbackRate = 2.0; // Set default playback speed to 2.0
  349. createControlPanel();
  350. updateTimeStats();
  351. });
  352. observer.disconnect(); // Stop observing once the audio is found
  353. }
  354. });
  355.  
  356. // Start observing the document for changes
  357. observer.observe(document, {
  358. childList: true,
  359. subtree: true
  360. });
  361.  
  362. })();