mirror of
https://codeberg.org/armin/noiz.git
synced 2026-09-01 14:00:54 +02:00
use "flatter" EQ curve (non-linear TTF); use 24 analyzer bands, zoom in
logo when loading page, add new window functions, sharpen edges of analyzer bands
This commit is contained in:
parent
7c3de9fc7b
commit
852b8f76a1
1 changed files with 105 additions and 37 deletions
142
index.html
142
index.html
|
|
@ -17,12 +17,10 @@
|
|||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #111; color: #e0e0e0; height: 100vh; overflow: hidden; display: flex; flex-direction: column; }
|
||||
.btn { background: linear-gradient(to bottom, #3a3a3a, #282828); border: 1px solid #484848; color: #e0e0e0; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 12px; transition: background .1s, transform .1s, box-shadow .1s; white-space: nowrap; box-shadow: 0 3px 0 #181818, 0 4px 6px rgba(0,0,0,0.4); transform: translateY(0); }
|
||||
.btn:hover { background: linear-gradient(to bottom, #404040, #2e2e2e); transform: translateY(-1px); box-shadow: 0 4px 0 #181818, 0 5px 8px rgba(0,0,0,0.4); }
|
||||
.btn:active { background: linear-gradient(to bottom, #282828, #3a3a3a); transform: translateY(2px); box-shadow: 0 1px 0 #181818, 0 2px 3px rgba(0,0,0,0.4); }
|
||||
.btn { background: linear-gradient(to bottom, #3a3a3a, #282828); border: 1px solid #484848; color: #e0e0e0; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 12px; white-space: nowrap; box-shadow: 0 3px 0 #181818, 0 4px 6px rgba(0,0,0,0.4); }
|
||||
.btn:hover { background: linear-gradient(to bottom, #404040, #2e2e2e); }
|
||||
.btn-primary { background: linear-gradient(to bottom, #2a7dff, #185dd5); border-color: #1a6df5; color: #fff; box-shadow: 0 3px 0 #0f4ab0, 0 4px 6px rgba(0,0,0,0.4); }
|
||||
.btn-primary:hover { background: linear-gradient(to bottom, #3a8dff, #1a6de5); box-shadow: 0 4px 0 #0f4ab0, 0 5px 8px rgba(0,0,0,0.4); }
|
||||
.btn-primary:active { background: linear-gradient(to bottom, #185dd5, #2a7dff); box-shadow: 0 1px 0 #0f4ab0, 0 2px 3px rgba(0,0,0,0.4); }
|
||||
.btn-primary:hover { background: linear-gradient(to bottom, #3a8dff, #1a6de5); }
|
||||
.btn-sm { padding: 4px 10px; font-size: 11px; }
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.brand { font-size: 15px; font-weight: 700; color: #999; letter-spacing: 1px; user-select: none; }
|
||||
|
|
@ -51,12 +49,10 @@ canvas { width: 100%; height: 100px; border-radius: 6px; display: block; }
|
|||
#timeRemaining { color: #555; }
|
||||
#vuMeter { width: 100%; height: 20px; border-radius: 2px; display: block; }
|
||||
.controls { display: flex; align-items: center; gap: 16px; align-self: flex-end; }
|
||||
.controls button { background: linear-gradient(to bottom, #3a3a3a, #222); border: none; color: #ccc; cursor: pointer; padding: 6px; font-size: 14px; border-radius: 50%; transition: all .1s; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 0 #1a1a1a, 0 3px 4px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.06); transform: translateY(0); }
|
||||
.controls button:hover { color: #fff; background: linear-gradient(to bottom, #444, #2a2a2a); transform: translateY(-1px); box-shadow: 0 3px 0 #1a1a1a, 0 4px 6px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.08); }
|
||||
.controls button:active { background: linear-gradient(to bottom, #222, #3a3a3a); transform: translateY(1px); box-shadow: 0 1px 0 #1a1a1a, 0 1px 2px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.03); }
|
||||
.controls button { background: linear-gradient(to bottom, #3a3a3a, #222); border: none; color: #ccc; cursor: pointer; padding: 6px; font-size: 14px; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 0 #1a1a1a, 0 3px 4px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.06); }
|
||||
.controls button:hover { color: #fff; background: linear-gradient(to bottom, #444, #2a2a2a); }
|
||||
.controls .play-btn { width: 44px; height: 44px; background: linear-gradient(to bottom, #2a7dff, #185dd5); color: #fff; font-size: 16px; border: 1px solid #1a6df5; box-shadow: 0 3px 0 #0f4ab0, 0 4px 6px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.15); }
|
||||
.controls .play-btn:hover { background: linear-gradient(to bottom, #3a8dff, #2a6de5); transform: translateY(-1px); box-shadow: 0 4px 0 #0f4ab0, 0 5px 8px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.2); }
|
||||
.controls .play-btn:active { background: linear-gradient(to bottom, #185dd5, #2a7dff); transform: translateY(2px); box-shadow: 0 1px 0 #0f4ab0, 0 2px 3px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.08); }
|
||||
.controls .play-btn:hover { background: linear-gradient(to bottom, #3a8dff, #2a6de5); }
|
||||
.volume-row { display: flex; align-items: center; gap: 8px; margin-top: 12px; width: 100%; max-width: 200px; align-self: flex-end; }
|
||||
.volume-row input[type="range"] { flex: 1; height: 3px; -webkit-appearance: none; appearance: none; background: #333; border-radius: 2px; outline: none; }
|
||||
.volume-row input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: #1a6df5; cursor: pointer; }
|
||||
|
|
@ -70,7 +66,7 @@ canvas { width: 100%; height: 100px; border-radius: 6px; display: block; }
|
|||
.playlist-toolbar { display: flex; align-items: center; gap: 8px; padding: 8px 24px; border-bottom: 1px solid #222; flex-shrink: 0; flex-wrap: wrap; }
|
||||
.playlist-toolbar .spacer { flex: 1; }
|
||||
.playlist-toolbar select { background: linear-gradient(to bottom, #3a3a3a, #282828); border: 1px solid #484848; color: #e0e0e0; padding: 4px 8px; border-radius: 4px; font-size: 11px; cursor: pointer; outline: none; box-shadow: 0 2px 0 #181818, 0 3px 4px rgba(0,0,0,0.3); transition: transform .1s, box-shadow .1s; }
|
||||
.playlist-toolbar select:active { background: linear-gradient(to bottom, #282828, #3a3a3a); transform: translateY(1px); box-shadow: 0 1px 0 #181818, 0 1px 2px rgba(0,0,0,0.3); }
|
||||
|
||||
.playlist-header { display: grid; grid-template-columns: 36px minmax(0,1fr) minmax(0,1fr) minmax(0,1fr) 60px 60px; gap: 8px; padding: 8px 24px; font-size: 10px; text-transform: uppercase; letter-spacing: .6px; color: #555; border-bottom: 1px solid #222; flex-shrink: 0; background: linear-gradient(to bottom, #1e1e1e, #141414); }
|
||||
.playlist-body { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 2px 0; position: relative; }
|
||||
#scanning { position: absolute; inset: 0; display: none; align-items: center; justify-content: center; background: rgba(0,0,0,0.75); z-index: 10; font-size: 14px; color: #ccc; flex-direction: column; gap: 10px; }
|
||||
|
|
@ -175,8 +171,14 @@ footer .footer-track { flex: 1; min-width: 0; white-space: nowrap; overflow: hid
|
|||
<span style="font-size:11px;color:#555;margin-left:4px" id="trackCount">0</span>
|
||||
<span style="font-size:11px;color:#555;margin-left:8px">Window:</span>
|
||||
<select id="windowSelect">
|
||||
<option value="hanning">Hanning</option>
|
||||
<option value="none">None</option>
|
||||
<option value="hanning" selected>Hanning</option>
|
||||
<option value="hamming">Hamming</option>
|
||||
<option value="blackman">Blackman-Harris</option>
|
||||
<option value="bartlett">Bartlett</option>
|
||||
<option value="gaussian">Gaussian</option>
|
||||
<option value="kaiser">Kaiser</option>
|
||||
<option value="flatTop">Flat Top</option>
|
||||
</select>
|
||||
<span style="font-size:11px;color:#555">Decay:</span>
|
||||
<select id="decaySelect">
|
||||
|
|
@ -300,14 +302,54 @@ let smoothedData = [];
|
|||
let vuPeakL = 0, vuPeakR = 0;
|
||||
let vuSmoothL = 0, vuSmoothR = 0;
|
||||
|
||||
function bessi0(x) {
|
||||
let s = 1, t = 1;
|
||||
for (let k = 1; k <= 20; k++) {
|
||||
t *= (x / (2 * k)) ** 2;
|
||||
s += t;
|
||||
if (t < 1e-12) break;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function applyWindow(data, type) {
|
||||
const n = data.length;
|
||||
let kaiserI0;
|
||||
if (type === 'kaiser') kaiserI0 = bessi0(3);
|
||||
for (let i = 0; i < n; i++) {
|
||||
let w;
|
||||
if (type === 'hanning') {
|
||||
w = 5.5 * (1 - Math.cos(0.2 * Math.PI * i / (n - 1)));
|
||||
} else {
|
||||
w = 0.35875 - 0.48829 * Math.cos(0.15 * Math.PI * i / n) + 5.14128 * Math.cos(4 * Math.PI * i / n) - 0.01168 * Math.cos(6 * Math.PI * i / n);
|
||||
switch (type) {
|
||||
case 'none':
|
||||
w = 1;
|
||||
break;
|
||||
case 'hanning':
|
||||
w = 0.5 * (1 - Math.cos(2 * Math.PI * i / (n - 1)));
|
||||
break;
|
||||
case 'hamming':
|
||||
w = 0.54 - 0.46 * Math.cos(2 * Math.PI * i / (n - 1));
|
||||
break;
|
||||
case 'blackman':
|
||||
w = 0.35875 - 0.48829 * Math.cos(2 * Math.PI * i / n) + 0.14128 * Math.cos(4 * Math.PI * i / n) - 0.01168 * Math.cos(6 * Math.PI * i / n);
|
||||
break;
|
||||
case 'bartlett':
|
||||
w = 1 - Math.abs(2 * i / (n - 1) - 1);
|
||||
break;
|
||||
case 'gaussian': {
|
||||
const z = 3 * (2 * i / (n - 1) - 1);
|
||||
w = Math.exp(-0.5 * z * z);
|
||||
break;
|
||||
}
|
||||
case 'kaiser': {
|
||||
const iz = 2 * i / (n - 1) - 1;
|
||||
const arg = 3 * Math.sqrt(Math.max(0, 1 - iz * iz));
|
||||
w = bessi0(arg) / kaiserI0;
|
||||
break;
|
||||
}
|
||||
case 'flatTop':
|
||||
w = 0.21557895 - 0.41663158 * Math.cos(2 * Math.PI * i / n) + 0.277263158 * Math.cos(4 * Math.PI * i / n) - 0.083578947 * Math.cos(6 * Math.PI * i / n) + 0.006947368 * Math.cos(8 * Math.PI * i / n);
|
||||
break;
|
||||
default:
|
||||
w = 1;
|
||||
}
|
||||
data[i] *= w;
|
||||
}
|
||||
|
|
@ -346,7 +388,7 @@ function fft(re, im) {
|
|||
}
|
||||
}
|
||||
|
||||
const SCALE_FACTOR = { hanning: 0.2, blackman: 0.3 };
|
||||
const SCALE_EXP = { none: 1, hanning: 0.5, hamming: 0.48, blackman: 0.4, bartlett: 0.55, gaussian: 0.43, kaiser: 0.33, flatTop: 0.3 };
|
||||
|
||||
function drawSpectrum() {
|
||||
if (!analyser) { animId = requestAnimationFrame(drawSpectrum); return; }
|
||||
|
|
@ -363,47 +405,73 @@ function drawSpectrum() {
|
|||
fft(re, im);
|
||||
const binCount = fftSize / 2;
|
||||
const mags = new Float32Array(binCount);
|
||||
const sf = SCALE_FACTOR[windowType] || 4;
|
||||
for (let i = 1; i < binCount; i++) {
|
||||
mags[i] = Math.min(1, Math.sqrt(re[i] * re[i] + im[i] * im[i]) * sf);
|
||||
const freqWeight = 0.72 + 0.35 * (i / binCount);
|
||||
mags[i] = Math.min(1, mags[i] * freqWeight);
|
||||
let mag = Math.sqrt(re[i] * re[i] + im[i] * im[i]);
|
||||
let dB = 20 * Math.log10(Math.max(mag / 64, 1e-3));
|
||||
dB += 10 * (i / binCount);
|
||||
let r = Math.min(1, Math.max(0, (dB + 50) / 50));
|
||||
const exp = SCALE_EXP[windowType] || 0.5;
|
||||
mags[i] = Math.min(1, Math.pow(r, exp) * 0.85);
|
||||
}
|
||||
if (peakData.length !== binCount) {
|
||||
peakData = new Float32Array(binCount);
|
||||
smoothedData = new Float32Array(binCount);
|
||||
const sampleRate = audioCtx.sampleRate;
|
||||
const maxBin = Math.min(binCount, Math.floor(19000 * fftSize / sampleRate));
|
||||
const NUM_BARS = 24;
|
||||
const barMags = new Float32Array(NUM_BARS);
|
||||
for (let b = 0; b < NUM_BARS; b++) {
|
||||
const start = 1 + Math.floor(b * (maxBin - 1) / NUM_BARS);
|
||||
const end = 1 + Math.floor((b + 1) * (maxBin - 1) / NUM_BARS);
|
||||
let sum = 0;
|
||||
for (let j = start; j < end; j++) sum += mags[j];
|
||||
barMags[b] = sum / (end - start);
|
||||
}
|
||||
const w = canvas.width, h = canvas.height;
|
||||
if (peakData.length !== NUM_BARS) {
|
||||
peakData = new Float32Array(NUM_BARS);
|
||||
smoothedData = new Float32Array(NUM_BARS);
|
||||
}
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const w = rect.width;
|
||||
const h = rect.height;
|
||||
const bufW = Math.round(w * dpr);
|
||||
const bufH = Math.round(h * dpr);
|
||||
if (canvas.width !== bufW || canvas.height !== bufH) {
|
||||
canvas.width = bufW;
|
||||
canvas.height = bufH;
|
||||
}
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.25)';
|
||||
const barCount = binCount;
|
||||
const barW = w / barCount * 0.75;
|
||||
const gap = w / barCount * 0.25;
|
||||
const grad = ctx.createLinearGradient(0, 0, 0, h);
|
||||
grad.addColorStop(0, '#4a9dff');
|
||||
grad.addColorStop(0.4, '#1a6df5');
|
||||
grad.addColorStop(1, '#0a1a3e');
|
||||
const smoothFactor = 0.96;
|
||||
for (let i = 1; i < barCount; i++) {
|
||||
const raw = mags[i];
|
||||
const barW = Math.max(1, Math.floor(w / NUM_BARS * 0.75));
|
||||
const gap = Math.max(1, Math.floor(w / NUM_BARS * 0.25));
|
||||
const totalBar = barW + gap;
|
||||
const offset = Math.floor((w - NUM_BARS * totalBar + gap) / 2);
|
||||
for (let i = 0; i < NUM_BARS; i++) {
|
||||
const raw = barMags[i];
|
||||
if (raw > smoothedData[i]) {
|
||||
smoothedData[i] = raw;
|
||||
} else {
|
||||
smoothedData[i] = smoothedData[i] * smoothFactor + raw * (1 - smoothFactor);
|
||||
}
|
||||
const val = smoothedData[i];
|
||||
const barH = val * h;
|
||||
const x = i * (barW + gap) + gap / 2;
|
||||
const barH = Math.round(val * h);
|
||||
const x = offset + i * totalBar;
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(x, h - barH, barW, barH);
|
||||
if (val * h > peakData[i]) {
|
||||
peakData[i] = val * h;
|
||||
if (barH > peakData[i]) {
|
||||
peakData[i] = barH;
|
||||
} else {
|
||||
peakData[i] *= peakDecay;
|
||||
}
|
||||
}
|
||||
ctx.fillStyle = '#ff2200';
|
||||
for (let i = 0; i < NUM_BARS; i++) {
|
||||
const x = offset + i * totalBar;
|
||||
if (peakData[i] > 1) {
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.6)';
|
||||
ctx.fillRect(x, h - peakData[i], barW, 2);
|
||||
ctx.fillRect(x, h - Math.round(peakData[i]), barW, 4);
|
||||
}
|
||||
}
|
||||
const vuCanvas = document.getElementById('vuMeter');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue