From 6694998985e98326ca4ca0a0724cfd4901be2430 Mon Sep 17 00:00:00 2001 From: exci <76759714+icxes@users.noreply.github.com> Date: Tue, 3 Feb 2026 00:45:17 +0200 Subject: [PATCH] audioplayer: add skip-silence feature (#1265) * add skip silence option --- copyparty/web/browser.css | 6 +- copyparty/web/browser.js | 164 +++++++++++++++++++++++++++++++++++++- 2 files changed, 167 insertions(+), 3 deletions(-) diff --git a/copyparty/web/browser.css b/copyparty/web/browser.css index aa2e1fba..e2e9173a 100644 --- a/copyparty/web/browser.css +++ b/copyparty/web/browser.css @@ -1456,7 +1456,8 @@ html.dz input { width: calc(100% - 16.2em); } input.drc_v, -input.eq_gain { +input.eq_gain, +input.ssconf_v { width: 3em; text-align: center; margin: 0 .6em; @@ -1466,7 +1467,8 @@ input.eq_gain { border-collapse: collapse; } #audio_drc td, -#audio_eq td { +#audio_eq td, +#audio_ss td { text-align: center; } #audio_eq a.eq_step { diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 70c62356..eb6d2a65 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -290,6 +290,7 @@ if (1) "ml_tint": "tint", "ml_eq": "audio equalizer", "ml_drc": "dynamic range compressor", + "ml_ss": "skip silence", "mt_loop": "loop/repeat one song\">🔁", "mt_one": "stop after one song\">1️⃣", @@ -326,7 +327,13 @@ if (1) "mt_xowa": "there are bugs in iOS preventing background playback using this format; please use caf or mp3 instead", "mt_tint": "background level (0-100) on the seekbar$Nto make buffering less distracting", "mt_eq": "enables the equalizer and gain control;$N$Nboost <code>0</code> = standard 100% volume (unmodified)$N$Nwidth <code>1  </code> = standard stereo (unmodified)$Nwidth <code>0.5</code> = 50% left-right crossfeed$Nwidth <code>0  </code> = mono$N$Nboost <code>-0.8</code> & width <code>10</code> = vocal removal :^)$N$Nenabling the equalizer makes gapless albums fully gapless, so leave it on with all the values at zero (except width = 1) if you care about that", - "mt_drc": "enables the dynamic range compressor (volume flattener / brickwaller); will also enable EQ to balance the spaghetti, so set all EQ fields except for 'width' to 0 if you don't want it$N$Nlowers the volume of audio above THRESHOLD dB; for every RATIO dB past THRESHOLD there is 1 dB of output, so default values of tresh -24 and ratio 12 means it should never get louder than -22 dB and it is safe to increase the equalizer boost to 0.8, or even 1.8 with ATK 0 and a huge RLS like 90 (only works in firefox; RLS is max 1 in other browsers)$N$N(see wikipedia, they explain it much better)", + "mt_drc": "enables the dynamic range compressor (volume flattener / brickwaller); will also enable EQ to balance the spaghetti, so set all EQ fields except for 'width' to 0 if you don't want it$N$Nlowers the volume of audio above THRESHOLD dB; for every RATIO dB past THRESHOLD there is 1 dB of output, so default values of 'tresh' -24 and 'ratio' 12 means it should never get louder than -22 dB and it is safe to increase the equalizer boost to 0.8, or even 1.8 with ATK 0 and a huge RLS like 90 (only works in firefox; RLS is max 1 in other browsers)$N$N(see wikipedia, they explain it much better)", + "mt_ss": "enables skip silence; multiplies playback speed by <code>sspeed</code>$Nat the start/end of audio tracks when volume is under <code>vthresh</code>$Nand the track is within 0 to <code>sthresh</code>% of the start$Nor <code>100-ethresh</code> to 100% of the end of the track", + "mt_ssvt": "skip silence volume threshold (0-255)", + "mt_ssts": "skip silence active threshold (% of track, start)", + "mt_sste": "skip silence active threshold (% of track, end)", + "mt_ssrt": "skip silence volume/speed ramp up/down time", + "mt_sssm": "skip silence playback speed multiplier", "mb_play": "play", "mm_hashplay": "play this audio file?", @@ -1313,6 +1320,7 @@ var mpl = (function () { '

' + L.ml_drc + '

' + '

' + L.ml_eq + '

' + + '

' + L.ml_ss + '

' + ''); var r = { @@ -2583,6 +2591,8 @@ var mpui = (function () { // occasionally draw buffered regions if (nth % 5 == 0) pbar.drawbuf(); + + if (!IE && afilt.ssen) skipSilence(); } // preload next song @@ -2690,11 +2700,15 @@ var afilt = (function () { var r = { "eqen": false, "drcen": false, + "ssen": false, "bands": [31.25, 62.5, 125, 250, 500, 1000, 2000, 4000, 8000, 16000], "gains": [4, 3, 2, 1, 0, 0, 1, 2, 3, 4], "drcv": [-24, 30, 12, 0.01, 0.25], "drch": ['tresh', 'knee', 'ratio', 'atk', 'rls'], "drck": ['threshold', 'knee', 'ratio', 'attack', 'release'], + "sscl": ['vthresh', "sthresh", "ethresh", 'sspeed', 'rspeed'], + "sstt": [L.mt_ssvt, L.mt_ssts, L.mt_sste, L.mt_ssrt, L.mt_sssm], + "sscv": [1, 5, 5, 5.0, 0.2], "drcn": null, "filters": [], "filterskip": [], @@ -2769,6 +2783,7 @@ var afilt = (function () { r.gains = gains; r.drcv = jread('au_drcv', r.drcv); + r.sscv = jread('au_sscv', r.sscv); } catch (ex) { } @@ -2998,6 +3013,28 @@ var afilt = (function () { clmod(this, 'err', err); } + function adj_ss() { + var err = false; + try { + var n = this.getAttribute('k'), + ov = r.sscv[n], + vs = this.value, + v = parseFloat(vs); + if (!isNum(v) || v + '' != vs) + throw new Error('inval v'); + + if (v == ov) + return; + + r.sscv[n] = v; + jwrite('au_sscv', r.sscv); + } + catch (ex) { + err = true; + } + clmod(this, 'err', err); + } + function eq_mod(e) { ev(e); adj_band(this, 0); @@ -3057,6 +3094,19 @@ var afilt = (function () { html += h2.join('\n') + ''; ebi('audio_drc').innerHTML = html; + h2 = []; + html = ['
']; + + for (var a = 0; a < r.sscl.length; a++) { + html.push(''); + h2.push(''); + } + + html = html.join('\n') + ''; + html += h2.join('\n') + '
', + '' + L.enable + '' + r.sscl[a] + '
'; + ebi('audio_ss').innerHTML = html; + var stp = QSA('a.eq_step'); for (var a = 0, aa = stp.length; a < aa; a++) stp[a].onclick = eq_step; @@ -3070,8 +3120,13 @@ var afilt = (function () { for (var a = 0; a < txt.length; a++) txt[a].oninput = txt[a].onkeydown = adj_drc; + txt = QSA('input.ssconf_v'); + for (var a = 0; a < txt.length; a++) + txt[a].oninput = txt[a].onkeydown = adj_ss; + bcfg_bind(r, 'eqen', 'au_eq', false, r.apply); bcfg_bind(r, 'drcen', 'au_drc', false, r.apply); + bcfg_bind(r, 'ssen', 'au_ss', false, r.apply); r.draw(); return r; @@ -9929,6 +9984,113 @@ function reload_browser() { dsel_init(); })(); +var ssint = null; + +function skipSilence() { + var ae = mp.au; + var ssconf = afilt.sscv; + + var config = { + vthresh: ssconf[0], + sthresh: ssconf[1], + etresh: ssconf[2], + sspeed: ssconf[3], + rspeed: ssconf[4], + loopInterval: 25 + }; + + if (!ae._ssa) { + var ctx = new AudioContext(); + + var asrc = ctx.createMediaElementSource(ae); + var analyser = ctx.createAnalyser(); + var gnod = ctx.createGain(); + + analyser.fftSize = 256; + asrc.connect(analyser); + analyser.connect(gnod); + gnod.connect(ctx.destination); + + ae._analyser = analyser; + ae._gnod = gnod; + ae._actx = ctx; + ae._ssa = true; + + ae.addEventListener('play', function() { + if (ctx.state === 'suspended') ctx.resume(); + startLoop(); + }); + + ae.addEventListener('pause', stopLoop); + ae.addEventListener('ended', stopLoop); + + ae.addEventListener('durationchange', function() { + if (ae._gnod) ae._gnod.gain.value = 1.0; + ae.playbackRate = 1.0; + }); + + if (!ae.paused && !ae.ended) { + startLoop(); + } + } + + function startLoop() { + if (!ssint) { + ssint = setInterval(detectSilence, config.loopInterval); + } + } + + function stopLoop() { + if (ssint) { + clearInterval(ssint); + ssint = null; + } + if(ae._gnod) ae._gnod.gain.value = 1.0; + ae.playbackRate = 1.0; + } + + function detectSilence() { + var duration = ae.duration || 0; + + var slimit = duration * (config.sthresh / 100); + var elimit = duration * (1 - (config.etresh / 100)); + var in_limits = ae.currentTime < slimit || ae.currentTime > elimit; + + var tspeed = 1.0; + var tvol = 1.0; + var is_silent = false; + + if (in_limits) { + var analyser = ae._analyser; + var da = new Uint8Array(analyser.frequencyBinCount); + analyser.getByteFrequencyData(da); + + var maxvol = 0; + for (var i = 0; i < da.length; i++) { + if (da[i] > maxvol) maxvol = da[i]; + } + + if (maxvol < config.vthresh) { + tspeed = config.sspeed; + tvol = 0.0; + is_silent = true; + } + } + + if (is_silent) { + if (Math.abs(ae.playbackRate - tspeed) > 0.01) { + ae.playbackRate += (tspeed - ae.playbackRate) * config.rspeed; + } + if (Math.abs(ae._gnod.gain.value - tvol) > 0.01) { + ae._gnod.gain.value += (tvol - ae._gnod.gain.value) * config.rspeed; + } + } else { + ae.playbackRate = 1.0; + ae._gnod.gain.value = 1.0; + } + } +} + treectl.hydrate(); J_BRW = 2;