mirror of
https://codeberg.org/armin/noiz.git
synced 2026-09-01 14:00:54 +02:00
remove blank linez ;-)
This commit is contained in:
parent
98adc59450
commit
4cd4649c0a
1 changed files with 0 additions and 78 deletions
78
index.html
78
index.html
|
|
@ -5,16 +5,6 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>noiz://</title>
|
||||
<style>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #161821; color: #c6c8d1; height: 100vh; overflow: hidden; display: flex; flex-direction: column; }
|
||||
.btn { background: linear-gradient(to bottom, #272c42, #1e2132); border: 1px solid #6b7089; color: #c6c8d1; padding: 6px 14px; border-radius: 6px; cursor: pointer; font-size: 12px; white-space: nowrap; box-shadow: 0 3px 0 #161821, 0 4px 6px rgba(0,0,0,0.4); }
|
||||
|
|
@ -71,7 +61,6 @@ canvas { width: 100%; height: 100px; border-radius: 6px; display: block; }
|
|||
.playlist-toolbar .spacer { flex: 1; }
|
||||
.playlist-toolbar select { background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath d='M2 3l3 3 3-3' stroke='%23c6c8d1' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") right 6px center / 8px no-repeat, linear-gradient(to bottom, #272c42, #1e2132); border: 1px solid #6b7089; color: #c6c8d1; padding: 4px 20px 4px 8px; border-radius: 4px; font-size: 11px; cursor: pointer; outline: none; box-shadow: 0 2px 0 #161821, 0 3px 4px rgba(0,0,0,0.3); transition: transform .1s, box-shadow .1s; -webkit-appearance: none; appearance: none; }
|
||||
.playlist-toolbar select:hover { background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath d='M2 3l3 3 3-3' stroke='%23c6c8d1' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") right 6px center / 8px no-repeat, linear-gradient(to bottom, #2e3348, #272c42); }
|
||||
|
||||
.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: #6b7089; border-bottom: 1px solid #272c42; flex-shrink: 0; background: linear-gradient(to bottom, #1e2132, #161821); }
|
||||
.playlist-body { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 2px 0; position: relative; background: #101219; }
|
||||
#scanning { position: absolute; inset: 0; display: none; align-items: center; justify-content: center; background: rgba(22,24,33,0.85); z-index: 10; font-size: 14px; color: #c6c8d1; flex-direction: column; gap: 10px; }
|
||||
|
|
@ -122,14 +111,11 @@ footer .footer-track { flex: 1; min-width: 0; white-space: nowrap; overflow: hid
|
|||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="noiz-overlay">
|
||||
<img src="noizmainlogo.png" alt="noiz://">
|
||||
</div>
|
||||
|
||||
<div id="error"></div>
|
||||
<input type="file" id="folderPicker" webkitdirectory multiple style="display:none">
|
||||
|
||||
<div class="content">
|
||||
<div class="pinned" id="pinned">
|
||||
<div class="player" id="player">
|
||||
|
|
@ -250,13 +236,11 @@ footer .footer-track { flex: 1; min-width: 0; white-space: nowrap; overflow: hid
|
|||
<button class="btn" id="deleteBtn">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer id="footer">
|
||||
<span class="footer-track" id="footerTrack">No track</span>
|
||||
<span id="footerTime">0:00 / 0:00</span>
|
||||
<span class="brand">noiz<span>://</span></span>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const audioElA = new Audio();
|
||||
const audioElB = new Audio();
|
||||
|
|
@ -274,7 +258,6 @@ let crossfadeTimer = null;
|
|||
let isCrossfading = false;
|
||||
let repeatMode = 'off';
|
||||
let shuffleOn = false;
|
||||
|
||||
let audioCtx = null;
|
||||
let analyser = null;
|
||||
let analyserL = null;
|
||||
|
|
@ -285,8 +268,6 @@ let sortAsc = true;
|
|||
let windowType = 'hanning';
|
||||
let peakDecay = 0.985;
|
||||
let barDecay = 0.96;
|
||||
|
||||
|
||||
function initAudioCtx() {
|
||||
if (audioCtx) return;
|
||||
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
|
|
@ -318,16 +299,13 @@ function initAudioCtx() {
|
|||
analyser.connect(audioCtx.destination);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const canvas = document.getElementById('spectrum');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const bigTime = document.getElementById('bigTime');
|
||||
|
||||
let peakData = [];
|
||||
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++) {
|
||||
|
|
@ -337,7 +315,6 @@ function bessi0(x) {
|
|||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function applyWindow(data, type) {
|
||||
const n = data.length;
|
||||
let kaiserI0;
|
||||
|
|
@ -380,7 +357,6 @@ function applyWindow(data, type) {
|
|||
data[i] *= w;
|
||||
}
|
||||
}
|
||||
|
||||
function fft(re, im) {
|
||||
const n = re.length;
|
||||
for (let i = 1, j = 0; i < n; i++) {
|
||||
|
|
@ -413,9 +389,7 @@ function fft(re, im) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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; }
|
||||
const fftSize = analyser.fftSize;
|
||||
|
|
@ -559,16 +533,13 @@ function drawSpectrum() {
|
|||
}
|
||||
animId = requestAnimationFrame(drawSpectrum);
|
||||
}
|
||||
|
||||
drawSpectrum();
|
||||
|
||||
let tracks = [];
|
||||
let currentIndex = -1;
|
||||
let selectedIndices = new Set();
|
||||
let anchorTrack = null;
|
||||
let dragIndices = null;
|
||||
let dirHandle = null;
|
||||
|
||||
const addFolderBtn = document.getElementById('addFolderBtn');
|
||||
const addFilesBtn = document.getElementById('addFilesBtn');
|
||||
const folderPicker = document.getElementById('folderPicker');
|
||||
|
|
@ -603,9 +574,7 @@ const windowSelect = document.getElementById('windowSelect');
|
|||
const decaySelect = document.getElementById('decaySelect');
|
||||
const barDecaySelect = document.getElementById('barDecaySelect');
|
||||
const crossfadeSelect = document.getElementById('crossfadeSelect');
|
||||
|
||||
const AUDIO_EXTS = new Set(['.mp3','.wav','.ogg','.flac','.m4a','.aac','.wma','.aiff','.alac','.opus']);
|
||||
|
||||
const GENRES = [
|
||||
'Blues','Classic Rock','Country','Dance','Disco','Funk','Grunge','Hip-Hop','Jazz','Metal',
|
||||
'New Age','Oldies','Other','Pop','R&B','Rap','Reggae','Rock','Techno','Industrial',
|
||||
|
|
@ -627,20 +596,17 @@ const GENRES = [
|
|||
'Christian Gangsta Rap','Heavy Metal','Black Metal','Crossover','Contemporary Christian',
|
||||
'Christian Rock','Merengue','Salsa','Thrash Metal','Anime','JPop','Synthpop'
|
||||
];
|
||||
|
||||
function fmtTime(s) {
|
||||
if (!isFinite(s) || s < 0) return '0:00';
|
||||
const m = Math.floor(s / 60);
|
||||
const sec = Math.floor(s % 60);
|
||||
return `${m}:${sec.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
errorEl.textContent = msg;
|
||||
errorEl.style.display = 'block';
|
||||
setTimeout(() => { errorEl.style.display = 'none'; }, 5000);
|
||||
}
|
||||
|
||||
async function* walkDir(dirHandle, path = '') {
|
||||
for await (const entry of dirHandle.values()) {
|
||||
const fullPath = path ? `${path}/${entry.name}` : entry.name;
|
||||
|
|
@ -651,13 +617,11 @@ async function* walkDir(dirHandle, path = '') {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isAudioFile(name) {
|
||||
const dot = name.lastIndexOf('.');
|
||||
if (dot === -1) return false;
|
||||
return AUDIO_EXTS.has(name.slice(dot).toLowerCase());
|
||||
}
|
||||
|
||||
function readBytes(file, start, length) {
|
||||
const blob = file.slice(start, start + length);
|
||||
return new Promise((resolve) => {
|
||||
|
|
@ -666,7 +630,6 @@ function readBytes(file, start, length) {
|
|||
r.readAsArrayBuffer(blob);
|
||||
});
|
||||
}
|
||||
|
||||
function decodeText(arr, offset, len, enc) {
|
||||
const end = Math.min(offset + len, arr.length);
|
||||
if (end <= offset) return '';
|
||||
|
|
@ -687,7 +650,6 @@ function decodeText(arr, offset, len, enc) {
|
|||
return new TextDecoder('utf-8').decode(slice).replace(/\0/g, '').trim();
|
||||
} catch { return ''; }
|
||||
}
|
||||
|
||||
function parseSyncsafeInt(bytes, off, len) {
|
||||
let val = 0;
|
||||
for (let i = 0; i < len; i++) {
|
||||
|
|
@ -695,7 +657,6 @@ function parseSyncsafeInt(bytes, off, len) {
|
|||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
function parseGenre(text) {
|
||||
if (!text) return '';
|
||||
const m = text.match(/\((\d+)\)/);
|
||||
|
|
@ -703,7 +664,6 @@ function parseGenre(text) {
|
|||
if (!isNaN(text) && text !== '') return GENRES[parseInt(text)] || text;
|
||||
return text;
|
||||
}
|
||||
|
||||
async function readId3v2(file) {
|
||||
try {
|
||||
const hdr = await readBytes(file, 0, 10);
|
||||
|
|
@ -744,7 +704,6 @@ async function readId3v2(file) {
|
|||
return tags;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
async function readId3v1(file) {
|
||||
try {
|
||||
if (file.size < 128) return null;
|
||||
|
|
@ -765,7 +724,6 @@ async function readId3v1(file) {
|
|||
return tags;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
async function readTags(file) {
|
||||
let tags = await readId3v2(file);
|
||||
if (tags && (tags.title || tags.artist)) return tags;
|
||||
|
|
@ -781,7 +739,6 @@ async function readTags(file) {
|
|||
}
|
||||
return tags || {};
|
||||
}
|
||||
|
||||
const MP3_BITRATES = [
|
||||
[0,32,64,96,128,160,192,224,256,288,320,352,384,416,448,0],
|
||||
[0,32,48,56,64,80,96,112,128,160,192,224,256,320,384,0],
|
||||
|
|
@ -789,22 +746,17 @@ const MP3_BITRATES = [
|
|||
[0,32,48,56,64,80,96,112,128,144,160,176,192,224,256,0],
|
||||
[0,8,16,24,32,40,48,56,64,80,96,112,128,144,160,0]
|
||||
];
|
||||
|
||||
const MP3_SAMPLERATES = [44100, 48000, 32000, 0, 22050, 24000, 16000, 0, 11025, 12000, 8000, 0];
|
||||
|
||||
const MP3_CHANNELS = ['Stereo', 'Joint Stereo', 'Dual Channel', 'Mono'];
|
||||
|
||||
function isMp3Sync(b) {
|
||||
return b[0] === 0xFF && (b[1] & 0xE0) === 0xE0 && ((b[1] >> 3) & 3) !== 1 && ((b[1] >> 1) & 3) !== 0;
|
||||
}
|
||||
|
||||
function findMp3Header(data, off) {
|
||||
for (let i = off; i < data.length - 4; i++) {
|
||||
if (isMp3Sync(data.slice(i, i + 4))) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
async function readAudioInfo(file) {
|
||||
const info = {};
|
||||
const ext = file.name.toLowerCase().slice(file.name.lastIndexOf('.'));
|
||||
|
|
@ -860,7 +812,6 @@ async function readAudioInfo(file) {
|
|||
} catch {}
|
||||
return info;
|
||||
}
|
||||
|
||||
async function processFiles(files, paths) {
|
||||
scanning.style.display = 'flex';
|
||||
const total = files.length;
|
||||
|
|
@ -895,7 +846,6 @@ async function processFiles(files, paths) {
|
|||
setTimeout(() => { scanning.style.display = 'none'; }, 800);
|
||||
return found;
|
||||
}
|
||||
|
||||
async function addFolder() {
|
||||
if ('showDirectoryPicker' in window) {
|
||||
try {
|
||||
|
|
@ -924,7 +874,6 @@ async function addFolder() {
|
|||
}
|
||||
folderPicker.click();
|
||||
}
|
||||
|
||||
folderPicker.addEventListener('change', async (e) => {
|
||||
const fileList = e.target.files;
|
||||
if (!fileList || fileList.length === 0) return;
|
||||
|
|
@ -934,7 +883,6 @@ folderPicker.addEventListener('change', async (e) => {
|
|||
addTracks(processed);
|
||||
folderPicker.value = '';
|
||||
});
|
||||
|
||||
async function addFiles() {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
|
|
@ -950,7 +898,6 @@ async function addFiles() {
|
|||
});
|
||||
input.click();
|
||||
}
|
||||
|
||||
function addTracks(newTracks) {
|
||||
if (newTracks.length === 0) {
|
||||
showError('No audio files found.');
|
||||
|
|
@ -959,7 +906,6 @@ function addTracks(newTracks) {
|
|||
tracks = tracks.concat(newTracks);
|
||||
renderTracks();
|
||||
}
|
||||
|
||||
function isolateTracks() {
|
||||
tracks.forEach((t, i) => {
|
||||
if (i !== currentIndex) URL.revokeObjectURL(t.url);
|
||||
|
|
@ -975,7 +921,6 @@ function isolateTracks() {
|
|||
}
|
||||
renderTracks();
|
||||
}
|
||||
|
||||
function deleteTrack() {
|
||||
if (selectedIndices.size === 0) return;
|
||||
if (selectedIndices.size === tracks.length) {
|
||||
|
|
@ -1006,7 +951,6 @@ function deleteTrack() {
|
|||
renderTracks();
|
||||
}
|
||||
}
|
||||
|
||||
function getSortFn(field) {
|
||||
switch (field) {
|
||||
case 'idx': return (a, b) => tracks.indexOf(a) - tracks.indexOf(b);
|
||||
|
|
@ -1018,7 +962,6 @@ function getSortFn(field) {
|
|||
default: return (a, b) => tracks.indexOf(a) - tracks.indexOf(b);
|
||||
}
|
||||
}
|
||||
|
||||
function renderTracks() {
|
||||
const sorted = [...tracks].sort(getSortFn(sortField));
|
||||
if (!sortAsc) sorted.reverse();
|
||||
|
|
@ -1120,13 +1063,11 @@ function renderTracks() {
|
|||
canvas.style.display = hasT ? 'block' : 'none';
|
||||
progressRow.style.display = hasT ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
const d = document.createElement('div');
|
||||
d.textContent = s;
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function playTrack(i) {
|
||||
if (i < 0 || i >= tracks.length) return;
|
||||
const prevIdx = currentIndex;
|
||||
|
|
@ -1173,7 +1114,6 @@ function playTrack(i) {
|
|||
renderTracks();
|
||||
}
|
||||
}
|
||||
|
||||
function updateUI() {
|
||||
const t = tracks[currentIndex];
|
||||
if (!t) return;
|
||||
|
|
@ -1191,7 +1131,6 @@ function updateUI() {
|
|||
else if (ai.channels) parts.push(ai.channels === 1 ? 'Mono' : 'Stereo');
|
||||
audioInfo.textContent = parts.join(' \u00B7 ');
|
||||
}
|
||||
|
||||
function onLoadedmetadata(e) {
|
||||
if (e.target !== audio) return;
|
||||
const t = tracks[currentIndex];
|
||||
|
|
@ -1245,7 +1184,6 @@ audioElA.addEventListener('play', onPlay);
|
|||
audioElB.addEventListener('play', onPlay);
|
||||
audioElA.addEventListener('pause', onPause);
|
||||
audioElB.addEventListener('pause', onPause);
|
||||
|
||||
playBtn.addEventListener('click', () => {
|
||||
if (tracks.length === 0) return;
|
||||
const selArr = [...selectedIndices];
|
||||
|
|
@ -1262,10 +1200,8 @@ playBtn.addEventListener('click', () => {
|
|||
audio.pause();
|
||||
}
|
||||
});
|
||||
|
||||
prevBtn.addEventListener('click', prevTrack);
|
||||
nextBtn.addEventListener('click', nextTrack);
|
||||
|
||||
function prevTrack() {
|
||||
if (tracks.length === 0) return;
|
||||
if (audio.currentTime > 3) { audio.currentTime = 0; return; }
|
||||
|
|
@ -1277,7 +1213,6 @@ function prevTrack() {
|
|||
playTrack(currentIndex <= 0 ? tracks.length - 1 : currentIndex - 1);
|
||||
}
|
||||
}
|
||||
|
||||
function nextTrack() {
|
||||
if (tracks.length === 0) return;
|
||||
if (shuffleOn) {
|
||||
|
|
@ -1290,31 +1225,26 @@ function nextTrack() {
|
|||
playTrack(currentIndex >= tracks.length - 1 ? 0 : currentIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
progressBar.addEventListener('click', (e) => {
|
||||
if (!audio.duration) return;
|
||||
const rect = progressBar.getBoundingClientRect();
|
||||
audio.currentTime = ((e.clientX - rect.left) / rect.width) * audio.duration;
|
||||
});
|
||||
|
||||
volumeSlider.addEventListener('input', () => {
|
||||
const v = parseFloat(volumeSlider.value);
|
||||
audioElA.volume = v;
|
||||
audioElB.volume = v;
|
||||
volDisplay.textContent = Math.round(v * 100);
|
||||
});
|
||||
|
||||
addFolderBtn.addEventListener('click', addFolder);
|
||||
addFilesBtn.addEventListener('click', addFiles);
|
||||
isolateBtn.addEventListener('click', isolateTracks);
|
||||
deleteBtn.addEventListener('click', deleteTrack);
|
||||
|
||||
sortSelect.addEventListener('change', () => {
|
||||
if (sortSelect.value === sortField) { sortAsc = !sortAsc; }
|
||||
else { sortField = sortSelect.value; sortAsc = true; }
|
||||
renderTracks();
|
||||
});
|
||||
|
||||
windowSelect.addEventListener('change', () => { windowType = windowSelect.value; });
|
||||
decaySelect.addEventListener('change', () => { peakDecay = parseFloat(decaySelect.value); });
|
||||
barDecaySelect.addEventListener('change', () => { barDecay = parseFloat(barDecaySelect.value); });
|
||||
|
|
@ -1323,11 +1253,9 @@ crossfadeDuration = parseFloat(crossfadeSelect.value);
|
|||
repeatSelect.addEventListener('change', () => { repeatMode = repeatSelect.value; });
|
||||
shuffleSelect.addEventListener('change', () => { shuffleOn = shuffleSelect.value === 'on'; });
|
||||
volDisplay.textContent = Math.round(parseFloat(volumeSlider.value) * 100);
|
||||
|
||||
let dragCounter = 0;
|
||||
let isInternalDrag = false;
|
||||
const content = document.querySelector('.content');
|
||||
|
||||
content.addEventListener('dragenter', (e) => {
|
||||
if (isInternalDrag) return;
|
||||
e.preventDefault();
|
||||
|
|
@ -1335,19 +1263,16 @@ content.addEventListener('dragenter', (e) => {
|
|||
content.style.outline = '2px dashed #84a0c6';
|
||||
content.style.outlineOffset = '-12px';
|
||||
});
|
||||
|
||||
content.addEventListener('dragleave', (e) => {
|
||||
if (isInternalDrag) return;
|
||||
e.preventDefault();
|
||||
dragCounter--;
|
||||
if (dragCounter <= 0) { dragCounter = 0; content.style.outline = 'none'; }
|
||||
});
|
||||
|
||||
content.addEventListener('dragover', (e) => {
|
||||
if (isInternalDrag) { e.preventDefault(); return; }
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
content.addEventListener('drop', async (e) => {
|
||||
if (isInternalDrag) return;
|
||||
e.preventDefault();
|
||||
|
|
@ -1386,7 +1311,6 @@ content.addEventListener('drop', async (e) => {
|
|||
const processed = await processFiles(files, paths);
|
||||
addTracks(processed);
|
||||
});
|
||||
|
||||
async function collectFiles(entry, files, paths, path, onFile) {
|
||||
if (entry.isFile) {
|
||||
const file = await new Promise((resolve) => entry.file(resolve));
|
||||
|
|
@ -1409,14 +1333,12 @@ async function collectFiles(entry, files, paths, path, onFile) {
|
|||
await readAll();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;
|
||||
if (e.code === 'Space') { e.preventDefault(); playBtn.click(); }
|
||||
if (e.code === 'ArrowRight') nextBtn.click();
|
||||
if (e.code === 'ArrowLeft') prevBtn.click();
|
||||
});
|
||||
|
||||
window.addEventListener('focus', () => {
|
||||
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue