// === Debug helpers (safe) ===
const log = (...a)=>console.log('[SV]', ...a);
const warn = (...a)=>console.warn('[SV]', ...a);
import AudioMotionAnalyzer from 'https://cdn.skypack.dev/audiomotion-analyzer?min';
const audioEl = document.getElementById('audio');
const container = document.getElementById('container');
const playlist = document.getElementById('playlist');
// Üst ince bar kontrolleri
const settingsBar = document.getElementById('settingsBar');
const openBtn = document.getElementById('openSettings');
const closeBtn = document.getElementById('closeSettings');
const fsBtn = document.getElementById('fullscreen');
const gradientSelect = document.getElementById('gradientSelect');
const modeSelect = document.getElementById('modeSelect');
const scaleSelect = document.getElementById('scaleSelect');
// YENİ kontrol referansları
const fftSizeSelect = document.getElementById('fftSizeSelect');
const minFreqSelect = document.getElementById('minFreqSelect');
const maxFreqSelect = document.getElementById('maxFreqSelect');
const barSpaceSelect = document.getElementById('barSpaceSelect');
const mirrorSelect = document.getElementById('mirrorSelect');
const smoothingRange = document.getElementById('smoothingRange');
const smoothValSpan = document.getElementById('smoothVal');
let currentIndex = -1;
const loopStates = new WeakMap();
let hls = null;
let shakaPlayer = null;
// Auto-resume AudioContext on user interaction
try {
const resume = ()=>{ if (audioCtx && audioCtx.state !== 'running') audioCtx.resume().then(()=>log('AudioContext resumed')); };
['click','touchstart','keydown'].forEach(ev => document.addEventListener(ev, resume, { passive:true }));
audioEl.addEventListener('play', resume);
} catch(e){}
// Aç/Kapat (TOGGLE)
function openSettingsBar() { settingsBar.classList.add('open'); settingsBar.setAttribute('aria-hidden','false'); }
function closeSettingsBar() { settingsBar.classList.remove('open'); settingsBar.setAttribute('aria-hidden','true'); }
function toggleSettingsBar(){ settingsBar.classList.contains('open') ? closeSettingsBar() : openSettingsBar(); }
openBtn.addEventListener('click', toggleSettingsBar);
closeBtn.addEventListener('click', closeSettingsBar);
document.addEventListener('keydown', (e)=>{ if(e.key==='Escape') closeSettingsBar(); });
// Fullscreen
fsBtn.addEventListener('click', () => {
if (!document.fullscreenElement) container.requestFullscreen().catch(()=>{});
else document.exitFullscreen().catch(()=>{});
});
// Kaynak türleri
const isM3U8 = s => /\.m3u8(\?|$)/i.test(s);
const isAAC = s => /\.aac(\?|$)/i.test(s);
const isM4A = s => /\.m4a(\?|$)/i.test(s);
const isMP3 = s => /\.mp3(\?|$)/i.test(s);
const isOGG = s => /\.ogg(\?|$)/i.test(s);
const isWAV = s => /\.wav(\?|$)/i.test(s);
const isFLAC = s => /\.flac(\?|$)/i.test(s);
const isWEBM = s => /\.webm(\?|$)/i.test(s);
function guessMime(src){
if (isAAC(src)) return 'audio/aac';
if (isM4A(src)) return 'audio/mp4';
if (isMP3(src)) return 'audio/mpeg';
if (isOGG(src)) return 'audio/ogg';
if (isWAV(src)) return 'audio/wav';
if (isFLAC(src)) return 'audio/flac';
if (isWEBM(src)) return 'audio/webm';
return '';
}
function cleanupPlayers(){
if (hls) { try { hls.destroy(); } catch {} hls = null; }
if (shakaPlayer) { try { shakaPlayer.destroy(); } catch {} shakaPlayer = null; }
}
function setTypedSource(src, mime){
audioEl.pause();
cleanupPlayers();
audioEl.removeAttribute('src');
while (audioEl.firstChild) audioEl.removeChild(audioEl.firstChild);
const source = document.createElement('source');
source.src = src;
if (mime) source.type = mime;
audioEl.appendChild(source);
audioEl.load();
audioEl.play().catch(()=>{});
}
// Analyzer (başlangıç varsayılanları - sonra UI / kayıt ile override edilecek)
// Web Audio pipeline (single instance)
const audioCtx = new (window.AudioContext||window.webkitAudioContext)();
const srcNode = audioCtx.createMediaElementSource(audioEl);
srcNode.connect(audioCtx.destination);
const audioMotion = new AudioMotionAnalyzer(container, { source: srcNode,
mode: 6,
barSpace: .1,
showLeds: false,
mirror: 0,
reflexRatio: 0,
height: container.clientHeight,
fftSize: 8192,
minFreq: 20,
maxFreq: 22000,
smoothing: 0.5
});
/* ---------- 19 Gradient kaydı ---------- */
const gradients = {
'Apple ][': ['#001100','#005500','#00AA00','#55FF55','#AAFFAA'],
'Aurora': ['#00284d','#005f73','#0a9396','#94d2bd','#e9d8a6','#ee9b00','#ca6702'],
'Borealis': ['#0b0e2e','#1b3a4b','#1f7a8c','#88d498','#c3f0ca'],
'Candy': ['#ff006e','#fb5607','#ffbe0b','#ff006e'],
'Classic': ['#0f0','#ff0','#f80','#f00'],
'Cool': ['#001f3f','#0074D9','#7FDBFF','#39CCCC'],
'Dusk': ['#2e026d','#6a00f4','#b5179e','#ff006e','#ffbe0b'],
'Miami': ['#00d2d3','#54a0ff','#5f27cd','#ff6b6b','#feca57'],
'Orient': ['#2C3E50','#FD746C','#FF9068'],
'Outrun': ['#200122','#6f0000','#C33764','#1D2671','#12c2e9'],
'Pacific Dream':['#34e89e','#0f3443','#2c5364','#0b8793'],
'Prism': ['#ff0000','#ffa500','#ffff00','#00ff00','#00ffff','#0000ff','#8b00ff'],
'Prism (legacy)':['#ff0000','#ffff00','#00ff00','#00ffff','#0000ff'],
'Rainbow': ['#ff0000','#ff7f00','#ffff00','#00ff00','#0000ff','#4b0082','#8f00ff'],
'Rainbow (legacy)':['#ff0000','#ffff00','#00ff00','#0000ff','#ff00ff'],
'Shahabi': ['#e96443','#904e95','#1f1c2c'],
'Summer': ['#f6d365','#fda085','#fbc2eb','#a6c0fe'],
'Sunset': ['#0b486b','#f56217','#f7485e','#ff8c00'],
'Tie Dye': ['#ff595e','#ffca3a','#8ac926','#1982c4','#6a4c93']
};
for (const [name, stops] of Object.entries(gradients)) {
audioMotion.registerGradient(name, { colorStops: stops });
}
// ====== Ayarları uygula & persist ======
const LS_KEY = 'amSettingsV1';
const switchesWrap = settingsBar; // tüm switch inputları bar içinde
function applyOptions(opts) {
try { audioMotion.setOptions(opts); }
catch(e){ console.warn('setOptions error:', e); }
}
function readSwitches() {
const switches = {};
switchesWrap.querySelectorAll('input[type="checkbox"][data-key]').forEach(cb=>{
switches[cb.dataset.key] = cb.checked;
});
return switches;
}
function saveSettings() {
const data = {
gradient: gradientSelect.value,
mode: parseInt(modeSelect.value,10),
scale: scaleSelect.value,
// yeni numeric/select değerleri
fftSize: parseInt(fftSizeSelect.value,10),
minFreq: parseFloat(minFreqSelect.value),
maxFreq: parseFloat(maxFreqSelect.value),
barSpace: parseFloat(barSpaceSelect.value),
mirror: parseInt(mirrorSelect.value,10),
smoothing: parseFloat(smoothingRange.value),
switches: readSwitches()
};
localStorage.setItem(LS_KEY, JSON.stringify(data));
}
function loadSettings() {
try {
const raw = localStorage.getItem(LS_KEY);
if (!raw) return null;
return JSON.parse(raw);
} catch { return null; }
}
// İlk yükleme
(function initUI(){
const saved = loadSettings();
// Gradient
const defaultGradient = (saved && saved.gradient) || 'Tie Dye';
if ([...gradientSelect.options].some(o=>o.value===defaultGradient)) {
gradientSelect.value = defaultGradient;
}
audioMotion.gradient = gradientSelect.value;
// Mode
if (saved && Number.isFinite(saved.mode)) {
modeSelect.value = String(saved.mode);
applyOptions({ mode: saved.mode });
} else {
applyOptions({ mode: parseInt(modeSelect.value,10) });
}
// Scale
scaleSelect.value = (saved && saved.scale) || 'log';
applyOptions({ frequencyScale: scaleSelect.value });
// ==== Yeni numeric/select alanları ====
// fftSize
if (saved && saved.fftSize) fftSizeSelect.value = String(saved.fftSize);
applyOptions({ fftSize: parseInt(fftSizeSelect.value,10) });
// min/max freq (geçersiz birleşim seçildiyse kibarca düzelt)
if (saved && saved.minFreq) minFreqSelect.value = String(saved.minFreq);
if (saved && saved.maxFreq) maxFreqSelect.value = String(saved.maxFreq);
let minF = parseFloat(minFreqSelect.value), maxF = parseFloat(maxFreqSelect.value);
if (minF >= maxF) { minF = 20; maxF = 22000; minFreqSelect.value = '20'; maxFreqSelect.value = '22000'; }
applyOptions({ minFreq: minF, maxFreq: maxF });
// barSpace
if (saved && typeof saved.barSpace==='number') barSpaceSelect.value = String(saved.barSpace);
applyOptions({ barSpace: parseFloat(barSpaceSelect.value) });
// mirror (0/1)
if (saved && typeof saved.mirror!=='undefined') mirrorSelect.value = String(saved.mirror);
applyOptions({ mirror: parseInt(mirrorSelect.value,10) });
// smoothing
const sm = (saved && typeof saved.smoothing==='number') ? saved.smoothing : 0.5;
smoothingRange.value = String(sm);
smoothValSpan.textContent = Number(sm).toFixed(2);
applyOptions({ smoothing: parseFloat(smoothingRange.value) });
// Switches (kaldırılanlar DOM’da yok; varsa kayıtları yine okunur)
const defaults = {
// eski anahtarlar
showPeaks:false, showScaleX:false, showScaleY:false,
linearAmplitude:false, lumiBars:false, showLeds:false,
outlineBars:false, radial:false, roundBars:false,
ansiBands:false, showFPS:false, alphaBars:false,
// yeni anahtarlar
overlay:false, splitGradient:false, trueLeds:false,
fadePeaks:false, peakLine:false, ledBars:false
};
const sw = Object.assign({}, defaults, (saved && saved.switches) || {});
switchesWrap.querySelectorAll('input[type="checkbox"][data-key]').forEach(cb=>{
cb.checked = !!sw[cb.dataset.key];
});
applyOptions(sw);
// Events
gradientSelect.addEventListener('change', ()=>{ audioMotion.gradient = gradientSelect.value; saveSettings(); });
modeSelect.addEventListener('change', ()=>{ applyOptions({ mode: parseInt(modeSelect.value,10) }); saveSettings(); });
scaleSelect.addEventListener('change', ()=>{ applyOptions({ frequencyScale: scaleSelect.value }); saveSettings(); });
fftSizeSelect.addEventListener('change', ()=>{ applyOptions({ fftSize: parseInt(fftSizeSelect.value,10) }); saveSettings(); });
minFreqSelect.addEventListener('change', ()=>{
const minFreq = parseFloat(minFreqSelect.value);
let maxFreq = parseFloat(maxFreqSelect.value);
if (minFreq >= maxFreq) {
// min >= max ise, mantıklı bir üst değer seç
const options = [...maxFreqSelect.options].map(o=>parseFloat(o.value)).filter(v=>v>minFreq);
if (options.length) { maxFreq = options[0]; maxFreqSelect.value = String(maxFreq); }
}
applyOptions({ minFreq, maxFreq });
saveSettings();
});
maxFreqSelect.addEventListener('change', ()=>{
let minFreq = parseFloat(minFreqSelect.value);
const maxFreq = parseFloat(maxFreqSelect.value);
if (minFreq >= maxFreq) {
// max <= min ise, mantıklı bir alt değer seç
const options = [...minFreqSelect.options].map(o=>parseFloat(o.value)).filter(v=>v
{ applyOptions({ barSpace: parseFloat(barSpaceSelect.value) }); saveSettings(); });
mirrorSelect.addEventListener('change', ()=>{ applyOptions({ mirror: parseInt(mirrorSelect.value,10) }); saveSettings(); });
smoothingRange.addEventListener('input', ()=>{
smoothValSpan.textContent = Number(smoothingRange.value).toFixed(2);
applyOptions({ smoothing: parseFloat(smoothingRange.value) });
saveSettings();
});
switchesWrap.addEventListener('change', (e)=>{
if (e.target.matches('input[type="checkbox"][data-key]')) {
const key = e.target.dataset.key;
const val = e.target.checked;
applyOptions({ [key]: val });
saveSettings();
}
});
})();
// ------- player kaynak yükleme -------
async function loadSource(src) {
log('loadSource', src);
try { if (audioCtx && audioCtx.state !== 'running') audioCtx.resume(); } catch(e) {}
// .m3u8 ise: HLS.js -> (gerekirse) native HLS -> (gerekirse) Shaka
if (isM3U8(src)) {
cleanupPlayers();
audioEl.pause();
audioEl.removeAttribute('src');
while (audioEl.firstChild) audioEl.removeChild(audioEl.firstChild);
audioEl.load();
// 1) HLS.js
if (window.Hls && Hls.isSupported()) {
log('HLS.js supported, initializing');
try {
hls = new Hls({
enableWorker: true,
lowLatencyMode: true
});
hls.loadSource(src);
hls.attachMedia(audioEl);
hls.on(Hls.Events.MANIFEST_PARSED, () => audioEl.play().catch(() => {}));
// Fatal error olursa Shaka'ya düş
hls.on(Hls.Events.ERROR, (_e, data) => {
if (data && data.fatal) {
try { hls.destroy(); } catch {}
hls = null;
tryShaka(src);
}
});
return;
} catch(e){
try { hls && hls.destroy(); } catch {}
hls = null;
// HLS.js başarısız oldu → Shaka dene
await tryShaka(src);
return;
}
}
// 2) Safari/native HLS
if (audioEl.canPlayType('application/vnd.apple.mpegurl')) {
log('Using native HLS playback');
const source = document.createElement('source');
source.src = src;
source.type = 'application/vnd.apple.mpegurl';
audioEl.appendChild(source);
audioEl.load();
audioEl.play().catch(() => {});
return;
}
// 3) Shaka
await tryShaka(src);
return;
}
// Diğer bilinen ses tipleri
const mime = guessMime(src);
log('Direct type', mime||''); setTypedSource(src, mime);
}
async function tryShaka(src) {
log('Trying Shaka', src);
try {
if (!(window.shaka && shaka.Player && shaka.Player.isBrowserSupported())) {
console.warn('Shaka desteklenmiyor ya da yüklenemedi.');
return;
}
cleanupPlayers();
shakaPlayer = new shaka.Player(audioEl);
await shakaPlayer.load(src);
await audioEl.play().catch(()=>{});
console.log('Shaka Player ile oynatılıyor:', src);
} catch (err) {
console.warn('Shaka yüklenemedi:', err);
}
}
function playFromPlaylist(trackEl) {
if (!trackEl) return;
const btn = trackEl.querySelector('.play');
if (!btn) return;
const src = btn.dataset.src;
loadSource(src);
highlightActive(btn);
currentIndex = [...playlist.querySelectorAll('.track')].indexOf(trackEl);
}
function highlightActive(activeBtn) {
playlist.querySelectorAll('.play').forEach(b => {
b.classList.toggle('active', b === activeBtn);
});
}
playlist.addEventListener('click', e => {
if (e.target.closest('.play')) {
playFromPlaylist(e.target.closest('.track'));
}
if (e.target.closest('.remove')) {
const trackEl = e.target.closest('.track');
if (trackEl) {
if (trackEl.querySelector('.play').classList.contains('active')) {
audioEl.pause();
currentIndex = -1;
}
trackEl.remove();
}
}
if (e.target.closest('.loop')) {
const btn = e.target.closest('.loop');
const trackEl = e.target.closest('.track');
const state = !loopStates.get(trackEl);
loopStates.set(trackEl, state);
btn.classList.toggle('active', state);
}
});
document.getElementById('upload').addEventListener('change', e => {
const files = e.target.files;
if (!files.length) return;
let firstTrackEl = null;
for (const fileBlob of files) {
const url = URL.createObjectURL(fileBlob);
const name = fileBlob.name;
const trackEl = document.createElement('div');
trackEl.className = 'track';
trackEl.draggable = true;
trackEl.innerHTML = `
`;
playlist.insertBefore(trackEl, playlist.firstChild);
if (!firstTrackEl) firstTrackEl = trackEl;
}
if (firstTrackEl) playFromPlaylist(firstTrackEl);
});
audioEl.addEventListener('ended', () => {
const tracks = playlist.querySelectorAll('.track');
if (currentIndex >= 0) {
const currentTrack = tracks[currentIndex];
if (loopStates.get(currentTrack)) {
playFromPlaylist(currentTrack);
return;
}
if (currentIndex + 1 < tracks.length) {
playFromPlaylist(tracks[currentIndex + 1]);
} else if (tracks.length) {
playFromPlaylist(tracks[0]);
}
}
});
const defaultSrc = 'https://moondigitaledge.radyotvonline.net/worldhits/playlist.m3u8';
loadSource(defaultSrc);
let draggedEl = null;
playlist.addEventListener('dragstart', e => {
const tr = e.target.closest('.track');
if (!tr) return;
draggedEl = tr;
tr.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
});
playlist.addEventListener('dragover', e => {
e.preventDefault();
const target = e.target.closest('.track');
if (target && target !== draggedEl) {
const rect = target.getBoundingClientRect();
const next = (e.clientY - rect.top) / rect.height > 0.5;
playlist.insertBefore(draggedEl, next ? target.nextSibling : target);
}
});
playlist.addEventListener('dragend', () => {
if (draggedEl) draggedEl.classList.remove('dragging');
draggedEl = null;
});
// Canvas çift tık ile tam ekran
container.addEventListener('dblclick', () => {
if (document.fullscreenElement) document.exitFullscreen();
else container.requestFullscreen();
});
audioEl.addEventListener('error', () => {
const e = audioEl.error;
console.warn('Audio error', e && e.code);
});
// --- test stream input ---
document.getElementById('testStream').addEventListener('click', () => {
const url = document.getElementById('customStream').value.trim();
if (!url) return;
let label = url;
try {
const u = new URL(url);
label = u.hostname;
const pathName = u.pathname.split('/').pop();
if (pathName) label = pathName;
} catch {
const parts = url.split('/');
if (parts.length) label = parts.pop();
}
const trackEl = document.createElement('div');
trackEl.className = 'track';
trackEl.draggable = true;
trackEl.innerHTML = `
`;
playlist.insertBefore(trackEl, playlist.firstChild);
playFromPlaylist(trackEl);
document.getElementById('customStream').value = '';
});
// Reset Defaults button handler (affects ALL controls)
const resetBtn = document.getElementById('resetDefaults');
if (resetBtn) {
resetBtn.addEventListener('click', ()=>{
const defaultsMain = {
gradient: 'Classic',
mode: 6,
frequencyScale: 'log',
fftSize: 8192,
minFreq: 20,
maxFreq: 22000,
barSpace: 0.10,
mirror: 0,
smoothing: 0.40
};
const desiredSwitches = {
showPeaks: true,
ledBars: true
// others default to false
};
try {
// Build options object including all switches present in DOM
const switchesObj = {};
document.querySelectorAll('#settingsBar input[type="checkbox"][data-key]').forEach(cb=>{
const k = cb.dataset.key;
const val = !!desiredSwitches[k];
cb.checked = val;
switchesObj[k] = val;
});
const opts = Object.assign({}, defaultsMain, switchesObj);
// Apply options at once
try { audioMotion.setOptions(opts); } catch(e){ console.warn('setOptions error on reset:', e); }
// Sync selects/ranges UI
if (typeof gradientSelect !== 'undefined' && gradientSelect) gradientSelect.value = defaultsMain.gradient;
if (typeof modeSelect !== 'undefined' && modeSelect) modeSelect.value = String(defaultsMain.mode);
if (typeof scaleSelect !== 'undefined' && scaleSelect) scaleSelect.value = defaultsMain.frequencyScale;
if (typeof fftSizeSelect !== 'undefined' && fftSizeSelect) fftSizeSelect.value = String(defaultsMain.fftSize);
if (typeof minFreqSelect !== 'undefined' && minFreqSelect) minFreqSelect.value = String(defaultsMain.minFreq);
if (typeof maxFreqSelect !== 'undefined' && maxFreqSelect) maxFreqSelect.value = String(defaultsMain.maxFreq);
if (typeof barSpaceSelect !== 'undefined' && barSpaceSelect) barSpaceSelect.value = String(defaultsMain.barSpace);
if (typeof mirrorSelect !== 'undefined' && mirrorSelect) mirrorSelect.value = String(defaultsMain.mirror);
if (typeof smoothingRange !== 'undefined' && smoothingRange) smoothingRange.value = defaultsMain.smoothing;
if (typeof smoothValSpan !== 'undefined' && smoothValSpan) smoothValSpan.textContent = defaultsMain.smoothing.toFixed(2);
// Persist
try { if (typeof saveSettings === 'function') saveSettings(); } catch(e){}
if (window.__svDebug && window.__svDebug.log) __svDebug.log('Defaults restored');
} catch(e){ console.warn('Reset defaults error', e); }
});
}