From b77c62128eb6dc8ee3223f8f3fbad170dded9c88 Mon Sep 17 00:00:00 2001 From: TC Date: Mon, 9 Jan 2023 09:00:16 +0100 Subject: [PATCH] Implement visualizer plugin --- config/defaults.js | 66 +++++++++++++++++++ package.json | 4 ++ plugins/visualizer/back.js | 6 ++ plugins/visualizer/empty-player.css | 9 +++ plugins/visualizer/front.js | 61 +++++++++++++++++ plugins/visualizer/menu.js | 23 +++++++ plugins/visualizer/visualizers/butterchurn.js | 46 +++++++++++++ plugins/visualizer/visualizers/vudio.js | 33 ++++++++++ plugins/visualizer/visualizers/wave.js | 31 +++++++++ readme.md | 2 + yarn.lock | 64 +++++++++++++++++- 11 files changed, 344 insertions(+), 1 deletion(-) create mode 100644 plugins/visualizer/back.js create mode 100644 plugins/visualizer/empty-player.css create mode 100644 plugins/visualizer/front.js create mode 100644 plugins/visualizer/menu.js create mode 100644 plugins/visualizer/visualizers/butterchurn.js create mode 100644 plugins/visualizer/visualizers/vudio.js create mode 100644 plugins/visualizer/visualizers/wave.js diff --git a/config/defaults.js b/config/defaults.js index b62432c6..eaefcc24 100644 --- a/config/defaults.js +++ b/config/defaults.js @@ -94,6 +94,72 @@ const defaultConfig = { "skip-silences": { onlySkipBeginning: false, }, + visualizer: { + enabled: false, + type: "butterchurn", + // Config per visualizer + butterchurn: { + preset: "martin [shadow harlequins shape code] - fata morgana", + renderingFrequencyInMs: 500, + blendTimeInSeconds: 2.7, + }, + vudio: { + effect: "lighting", + accuracy: 128, + lighting: { + maxHeight: 160, + maxSize: 12, + lineWidth: 1, + color: "#49f3f7", + shadowBlur: 2, + shadowColor: "rgba(244,244,244,.5)", + fadeSide: true, + prettify: false, + horizontalAlign: "center", + verticalAlign: "middle", + dottify: true, + }, + }, + wave: { + animations: [ + { + type: "Cubes", + config: { + bottom: true, + count: 30, + cubeHeight: 5, + fillColor: { gradient: ["#FAD961", "#F76B1C"] }, + lineColor: "rgba(0,0,0,0)", + radius: 20, + }, + }, + { + type: "Cubes", + config: { + top: true, + count: 12, + cubeHeight: 5, + fillColor: { gradient: ["#FAD961", "#F76B1C"] }, + lineColor: "rgba(0,0,0,0)", + radius: 10, + }, + }, + { + type: "Circles", + config: { + lineColor: { + gradient: ["#FAD961", "#FAD961", "#F76B1C"], + rotate: 90, + }, + lineWidth: 4, + diameter: 20, + count: 10, + frequencyBand: "base", + }, + }, + ], + }, + }, }, }; diff --git a/package.json b/package.json index af2a9dca..9086d2e7 100644 --- a/package.json +++ b/package.json @@ -96,9 +96,12 @@ "@cliqz/adblocker-electron": "^1.25.1", "@ffmpeg/core": "^0.11.0", "@ffmpeg/ffmpeg": "^0.11.6", + "@foobar404/wave": "^2.0.4", "Simple-YouTube-Age-Restriction-Bypass": "https://gitpkg.now.sh/zerodytrash/Simple-YouTube-Age-Restriction-Bypass/dist?v2.5.4", "async-mutex": "^0.4.0", "browser-id3-writer": "^4.4.0", + "butterchurn": "^2.6.7", + "butterchurn-presets": "^2.4.7", "chokidar": "^3.5.3", "custom-electron-prompt": "^1.5.0", "custom-electron-titlebar": "^4.1.2", @@ -116,6 +119,7 @@ "mpris-service": "^2.1.2", "node-fetch": "^2.6.7", "node-notifier": "^10.0.1", + "vudio": "^2.1.1", "ytdl-core": "^4.11.1", "ytpl": "^2.3.0" }, diff --git a/plugins/visualizer/back.js b/plugins/visualizer/back.js new file mode 100644 index 00000000..b0f8c42b --- /dev/null +++ b/plugins/visualizer/back.js @@ -0,0 +1,6 @@ +const { injectCSS } = require("../utils"); +const path = require("path"); + +module.exports = (win, options) => { + injectCSS(win.webContents, path.join(__dirname, "empty-player.css")); +}; diff --git a/plugins/visualizer/empty-player.css b/plugins/visualizer/empty-player.css new file mode 100644 index 00000000..dc94788a --- /dev/null +++ b/plugins/visualizer/empty-player.css @@ -0,0 +1,9 @@ +#player { + margin: 0 !important; + background: black; +} + +#song-image, +#song-video { + display: none !important; +} diff --git a/plugins/visualizer/front.js b/plugins/visualizer/front.js new file mode 100644 index 00000000..47522422 --- /dev/null +++ b/plugins/visualizer/front.js @@ -0,0 +1,61 @@ +const defaultConfig = require("../../config/defaults"); + +module.exports = (options) => { + const optionsWithDefaults = { + ...defaultConfig.plugins.visualizer, + ...options, + }; + const VisualizerType = require(`./visualizers/${optionsWithDefaults.type}`); + + document.addEventListener( + "audioCanPlay", + (e) => { + const video = document.querySelector("video"); + const visualizerContainer = document.querySelector("#player"); + + let canvas = document.getElementById("visualizer"); + if (!canvas) { + canvas = document.createElement("canvas"); + canvas.id = "visualizer"; + canvas.style.position = "absolute"; + canvas.style.background = "black"; + visualizerContainer.append(canvas); + } + + const resizeCanvas = () => { + canvas.width = visualizerContainer.clientWidth; + canvas.height = visualizerContainer.clientHeight; + }; + resizeCanvas(); + + const gainNode = e.detail.audioContext.createGain(); + gainNode.gain.value = 1.25; + e.detail.audioSource.connect(gainNode); + + const visualizer = new VisualizerType( + e.detail.audioContext, + e.detail.audioSource, + visualizerContainer, + canvas, + gainNode, + video.captureStream(), + optionsWithDefaults[optionsWithDefaults.type] + ); + + const resizeVisualizer = (width, height) => { + resizeCanvas(); + visualizer.resize(width, height); + }; + resizeVisualizer(canvas.width, canvas.height); + const visualizerContainerObserver = new ResizeObserver((entries) => { + entries.forEach((entry) => { + resizeVisualizer(entry.contentRect.width, entry.contentRect.height); + }); + }); + visualizerContainerObserver.observe(visualizerContainer); + + visualizer.render(); + }, + { passive: true } + ); +}; diff --git a/plugins/visualizer/menu.js b/plugins/visualizer/menu.js new file mode 100644 index 00000000..181a71e7 --- /dev/null +++ b/plugins/visualizer/menu.js @@ -0,0 +1,23 @@ +const { readdirSync } = require("fs"); +const path = require("path"); + +const { setMenuOptions } = require("../../config/plugins"); + +const visualizerTypes = readdirSync(path.join(__dirname, "visualizers")).map( + (filename) => path.parse(filename).name +); + +module.exports = (win, options) => [ + { + label: "Type", + submenu: visualizerTypes.map((visualizerType) => ({ + label: visualizerType, + type: "radio", + checked: options.type === visualizerType, + click: () => { + options.type = visualizerType; + setMenuOptions("visualizer", options); + }, + })), + }, +]; diff --git a/plugins/visualizer/visualizers/butterchurn.js b/plugins/visualizer/visualizers/butterchurn.js new file mode 100644 index 00000000..1c30355d --- /dev/null +++ b/plugins/visualizer/visualizers/butterchurn.js @@ -0,0 +1,46 @@ +const butterchurn = require("butterchurn"); +const butterchurnPresets = require("butterchurn-presets"); + +const presets = butterchurnPresets.getPresets(); + +class ButterchurnVisualizer { + constructor( + audioContext, + audioSource, + visualizerContainer, + canvas, + audioNode, + stream, + options + ) { + this.visualizer = butterchurn.default.createVisualizer( + audioContext, + canvas, + { + width: canvas.width, + height: canvas.height, + } + ); + + const preset = presets[options.preset]; + this.visualizer.loadPreset(preset, options.blendTimeInSeconds); + + this.visualizer.connectAudio(audioNode); + + this.renderingFrequencyInMs = options.renderingFrequencyInMs; + } + + resize(width, height) { + this.visualizer.setRendererSize(width, height); + } + + render() { + const renderVisualizer = () => { + requestAnimationFrame(() => renderVisualizer()); + this.visualizer.render(); + }; + setTimeout(renderVisualizer(), this.renderingFrequencyInMs); + } +} + +module.exports = ButterchurnVisualizer; diff --git a/plugins/visualizer/visualizers/vudio.js b/plugins/visualizer/visualizers/vudio.js new file mode 100644 index 00000000..36cdf2a2 --- /dev/null +++ b/plugins/visualizer/visualizers/vudio.js @@ -0,0 +1,33 @@ +const Vudio = require("vudio/umd/vudio"); + +class VudioVisualizer { + constructor( + audioContext, + audioSource, + visualizerContainer, + canvas, + audioNode, + stream, + options + ) { + this.visualizer = new Vudio(stream, canvas, { + width: canvas.width, + height: canvas.height, + // Visualizer config + ...options, + }); + } + + resize(width, height) { + this.visualizer.setOption({ + width: width, + height: height, + }); + } + + render() { + this.visualizer.dance(); + } +} + +module.exports = VudioVisualizer; diff --git a/plugins/visualizer/visualizers/wave.js b/plugins/visualizer/visualizers/wave.js new file mode 100644 index 00000000..70db7f2a --- /dev/null +++ b/plugins/visualizer/visualizers/wave.js @@ -0,0 +1,31 @@ +const { Wave } = require("@foobar404/wave"); + +class WaveVisualizer { + constructor( + audioContext, + audioSource, + visualizerContainer, + canvas, + audioNode, + stream, + options + ) { + this.visualizer = new Wave( + { context: audioContext, source: audioSource }, + canvas + ); + options.animations.forEach((animation) => { + this.visualizer.addAnimation( + eval(`new this.visualizer.animations.${animation.type}( + ${JSON.stringify(animation.config)} + )`) + ); + }); + } + + resize(width, height) {} + + render() {} +} + +module.exports = WaveVisualizer; diff --git a/readme.md b/readme.md index ed490b31..03f2df33 100644 --- a/readme.md +++ b/readme.md @@ -108,6 +108,8 @@ winget install th-ch.YouTubeMusic - **Video Toggle**: Adds a [button](https://user-images.githubusercontent.com/28893833/173663950-63e6610e-a532-49b7-9afa-54cb57ddfc15.png) to switch between Video/Song mode. can also optionally remove the whole video tab +- **Visualizer**: Different music visualizers + --- - **Auto confirm when paused** (Always Enabled): disable the ["Continue Watching?"](https://user-images.githubusercontent.com/61631665/129977894-01c60740-7ec6-4bf0-9a2c-25da24491b0e.png) popup that pause music after a certain time diff --git a/yarn.lock b/yarn.lock index b7505584..7e583ea5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -202,6 +202,13 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.15.tgz#8e66775fb523599acb6a289e12929fa5ab0954d8" integrity sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ== +"@babel/runtime@^7.0.0": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd" + integrity sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ== + dependencies: + regenerator-runtime "^0.13.11" + "@babel/runtime@^7.7.2": version "7.12.13" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.13.tgz#0a21452352b02542db0ffb928ac2d3ca7cb6d66d" @@ -374,6 +381,11 @@ regenerator-runtime "^0.13.7" resolve-url "^0.2.1" +"@foobar404/wave@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@foobar404/wave/-/wave-2.0.4.tgz#c9bc54c41b18642c6a4587851e28b8f858af98b0" + integrity sha512-FEyg37hDvQtrQVlFxbit7ov5e487BjsR32bZfJ4oAb5i+NnlbGaNyy6iYBZ8ocVHo8fgug+SL+mFdDTzqjvPww== + "@humanwhocodes/config-array@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" @@ -1364,6 +1376,14 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59" integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== +babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + integrity sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g== + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -1557,6 +1577,23 @@ builtin-modules@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887" integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA== +butterchurn-presets@^2.4.7: + version "2.4.7" + resolved "https://registry.yarnpkg.com/butterchurn-presets/-/butterchurn-presets-2.4.7.tgz#41e4e37cd3af2aec124fa8062352816100956c29" + integrity sha512-4MdM8ripz/VfH1BCldrIKdAc/1ryJFBDvqlyow6Ivo1frwj0H3duzvSMFC7/wIjAjxb1QpwVHVqGqS9uAFKhpg== + dependencies: + babel-runtime "^6.26.0" + ecma-proposal-math-extensions "0.0.2" + lodash "^4.17.4" + +butterchurn@^2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/butterchurn/-/butterchurn-2.6.7.tgz#1ff0c1365731a4fb7ada7bb16ae1c6f09a110c12" + integrity sha512-BJiRA8L0L2+84uoG2SSfkp0kclBuN+vQKf217pK7pMlwEO2ZEg3MtO2/o+l8Qpr8Nbejg8tmL1ZHD1jmhiaaqg== + dependencies: + "@babel/runtime" "^7.0.0" + ecma-proposal-math-extensions "0.0.2" + cacheable-request@^6.0.0: version "6.1.0" resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" @@ -1892,6 +1929,11 @@ convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" +core-js@^2.4.0: + version "2.6.12" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" + integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== + core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -2266,6 +2308,11 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +ecma-proposal-math-extensions@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/ecma-proposal-math-extensions/-/ecma-proposal-math-extensions-0.0.2.tgz#a6a3d64819db70cee0d2e1976b6315d00e4714a0" + integrity sha512-80BnDp2Fn7RxXlEr5HHZblniY4aQ97MOAicdWWpSo0vkQiISSE9wLR4SqxKsu4gCtXFBIPPzy8JMhay4NWRg/Q== + ejs@^3.1.6: version "3.1.7" resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.7.tgz#c544d9c7f715783dd92f0bddcf73a59e6962d006" @@ -4525,7 +4572,7 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= -lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.3.0: +lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.3.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -5605,6 +5652,16 @@ redent@^4.0.0: indent-string "^5.0.0" strip-indent "^4.0.0" +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== + +regenerator-runtime@^0.13.11: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + regenerator-runtime@^0.13.3, regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7: version "0.13.7" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" @@ -6582,6 +6639,11 @@ verror@1.10.0, verror@^1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vudio@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/vudio/-/vudio-2.1.1.tgz#af256c4e1c8ae8bdbbba0e718f8106a30a44e96e" + integrity sha512-VkFQcFt/b/kpF5Eg5Sq+oXUo1Zp5aRFF4BSmIrOzau5o+5WMWwX9ae/EGJZstCyZFiCTU5iw1Y+u2BCGW6Y6Jw== + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"