diff --git a/package.json b/package.json index 9827b524..af2a9dca 100644 --- a/package.json +++ b/package.json @@ -111,7 +111,6 @@ "electron-unhandled": "^4.0.1", "electron-updater": "^4.6.3", "filenamify": "^4.3.0", - "hark": "^1.2.3", "html-to-text": "^8.2.1", "md5": "^2.3.0", "mpris-service": "^2.1.2", diff --git a/plugins/audio-compressor/front.js b/plugins/audio-compressor/front.js index 129073e3..1281cec5 100644 --- a/plugins/audio-compressor/front.js +++ b/plugins/audio-compressor/front.js @@ -1,5 +1,5 @@ -const applyCompressor = () => { - const audioContext = new AudioContext(); +const applyCompressor = (e) => { + const audioContext = e.detail.audioContext; const compressor = audioContext.createDynamicsCompressor(); compressor.threshold.value = -50; @@ -8,10 +8,11 @@ const applyCompressor = () => { compressor.attack.value = 0; compressor.release.value = 0.25; - const source = audioContext.createMediaElementSource(document.querySelector("video")); - - source.connect(compressor); + e.detail.audioSource.connect(compressor); compressor.connect(audioContext.destination); }; -module.exports = () => document.addEventListener('apiLoaded', applyCompressor, { once: true, passive: true }); +module.exports = () => + document.addEventListener("audioCanPlay", applyCompressor, { + passive: true, + }); diff --git a/plugins/skip-silences/front.js b/plugins/skip-silences/front.js index 17a534c9..69a6e919 100644 --- a/plugins/skip-silences/front.js +++ b/plugins/skip-silences/front.js @@ -1,53 +1,112 @@ -const hark = require("hark/hark.bundle.js"); - module.exports = (options) => { let isSilent = false; let hasAudioStarted = false; - document.addEventListener("apiLoaded", () => { - const video = document.querySelector("video"); - const speechEvents = hark(video, { - threshold: -100, // dB (-100 = absolute silence, 0 = loudest) - interval: 2, // ms - }); - const skipSilence = () => { - if (options.onlySkipBeginning && hasAudioStarted) { - return; - } + const smoothing = 0.1; + const threshold = -100; // dB (-100 = absolute silence, 0 = loudest) + const interval = 2; // ms + const history = 10; + const speakingHistory = Array(history).fill(0); - if (isSilent && !video.paused) { - video.currentTime += 0.2; // in s - } - }; + document.addEventListener( + "audioCanPlay", + (e) => { + const video = document.querySelector("video"); + const audioContext = e.detail.audioContext; + const sourceNode = e.detail.audioSource; - speechEvents.on("speaking", function () { - isSilent = false; - hasAudioStarted = true; - }); + // Use an audio analyser similar to Hark + // https://github.com/otalk/hark/blob/master/hark.bundle.js + const analyser = audioContext.createAnalyser(); + analyser.fftSize = 512; + analyser.smoothingTimeConstant = smoothing; + const fftBins = new Float32Array(analyser.frequencyBinCount); - speechEvents.on("stopped_speaking", function () { - if ( - !( - video.paused || - video.seeking || - video.ended || - video.muted || - video.volume === 0 - ) - ) { - isSilent = true; + sourceNode.connect(analyser); + analyser.connect(audioContext.destination); + + const looper = () => { + setTimeout(() => { + const currentVolume = getMaxVolume(analyser, fftBins); + + let history = 0; + if (currentVolume > threshold && isSilent) { + // trigger quickly, short history + for ( + let i = speakingHistory.length - 3; + i < speakingHistory.length; + i++ + ) { + history += speakingHistory[i]; + } + if (history >= 2) { + // Not silent + isSilent = false; + hasAudioStarted = true; + } + } else if (currentVolume < threshold && !isSilent) { + for (let i = 0; i < speakingHistory.length; i++) { + history += speakingHistory[i]; + } + if (history == 0) { + // Silent + if ( + !( + video.paused || + video.seeking || + video.ended || + video.muted || + video.volume === 0 + ) + ) { + isSilent = true; + skipSilence(); + } + } + } + speakingHistory.shift(); + speakingHistory.push(0 + (currentVolume > threshold)); + + looper(); + }, interval); + }; + looper(); + + const skipSilence = () => { + if (options.onlySkipBeginning && hasAudioStarted) { + return; + } + + if (isSilent && !video.paused) { + video.currentTime += 0.2; // in s + } + }; + + video.addEventListener("play", function () { + hasAudioStarted = false; skipSilence(); - } - }); + }); - video.addEventListener("play", function () { - hasAudioStarted = false; - skipSilence(); - }); - - video.addEventListener("seeked", function () { - hasAudioStarted = false; - skipSilence(); - }); - }); + video.addEventListener("seeked", function () { + hasAudioStarted = false; + skipSilence(); + }); + }, + { + passive: true, + } + ); }; + +function getMaxVolume(analyser, fftBins) { + var maxVolume = -Infinity; + analyser.getFloatFrequencyData(fftBins); + + for (var i = 4, ii = fftBins.length; i < ii; i++) { + if (fftBins[i] > maxVolume && fftBins[i] < 0) { + maxVolume = fftBins[i]; + } + } + + return maxVolume; +} diff --git a/preload.js b/preload.js index 2b9122f1..ecfb8e17 100644 --- a/preload.js +++ b/preload.js @@ -90,6 +90,32 @@ function listenForApiLoad() { } function onApiLoaded() { + const video = document.querySelector("video"); + const audioContext = new AudioContext(); + const audioSource = audioContext.createMediaElementSource(video); + + video.addEventListener( + "loadstart", + () => { + // Emit "audioCanPlay" for each video + video.addEventListener( + "canplaythrough", + () => { + document.dispatchEvent( + new CustomEvent("audioCanPlay", { + detail: { + audioContext: audioContext, + audioSource: audioSource, + }, + }) + ); + }, + { once: true } + ); + }, + { passive: true } + ); + document.dispatchEvent(new CustomEvent('apiLoaded', { detail: api })); // Remove upgrade button diff --git a/yarn.lock b/yarn.lock index c5a07fde..b7505584 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3527,13 +3527,6 @@ hard-rejection@^2.1.0: resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== -hark@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/hark/-/hark-1.2.3.tgz#959981400f561be5580ecd4321a9f55b16bacbd0" - integrity sha512-u68vz9SCa38ESiFJSDjqK8XbXqWzyot7Cj6Y2b6jk2NJ+II3MY2dIrLMg/kjtIAun4Y1DHF/20hfx4rq1G5GMg== - dependencies: - wildemitter "^1.2.0" - has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" @@ -6639,11 +6632,6 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" -wildemitter@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/wildemitter/-/wildemitter-1.2.1.tgz#9da3b5ca498e4378628d1783145493c70a10b774" - integrity sha512-UMmSUoIQSir+XbBpTxOTS53uJ8s/lVhADCkEbhfRjUGFDPme/XGOb0sBWLx5sTz7Wx/2+TlAw1eK9O5lw5PiEw== - word-wrap@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"