commit 34981d292fa22f4e80bb62cb60c381eac37c9a49 Author: chuckdaniels Date: Mon Sep 5 04:12:41 2022 -0500 technically 1.0 diff --git a/alt.html b/alt.html new file mode 100644 index 0000000..20e9a21 --- /dev/null +++ b/alt.html @@ -0,0 +1,32 @@ + + + wormTuner + + + + + + + + + + +
+ +
+
+
+
+
+
+ +
+
+
+

Click on a station to tune in!

+
+ +
+
+ + diff --git a/audiovis.js b/audiovis.js new file mode 100644 index 0000000..01b7b29 --- /dev/null +++ b/audiovis.js @@ -0,0 +1,166 @@ +/* wormTune Audio Visualizer */ + +var modes = ["None", "Bars","Scope","Spectrogram","Laser Rain"]; +var drawRequest = 0; +var renderInverval; + +function createVisualizer(player, context) { + var audioSrc = context.createMediaElementSource(player); + analyser = context.createAnalyser(); + + var canvas = document.querySelector("canvas.vis"); + + audioSrc.connect(analyser); + analyser.connect(context.destination); + + setVisualizer(canvas, analyser, 0); + + return analyser; +}; + +function setVisualizer(canvas, analyser, mode = 0) { + if (!analyser) return; + clearInterval(renderInverval); + var ctx = canvas.getContext("2d",{antialias: false,alpha: false}); + + var WIDTH = canvas.width; + var HEIGHT = canvas.height; + + // Set the "defaults" + analyser.smoothingTimeConstant = 0.8; + analyser.fftSize = 2048; + + if (mode != -1) canvas.height = 96; + + window.cancelAnimationFrame(drawRequest); + if (mode == -1) { // VIDEO MODE + canvas.height = canvas.width / 4 * 3; + renderInverval = window.setInterval(() => { + ctx.drawImage(audioPlayer, 0, 0, WIDTH, HEIGHT); + }, 1000 / 24); + } else if (mode == 0 || mode > modes.length - 1) { + function draw() { + drawRequest = window.requestAnimationFrame(draw); + + ctx.clearRect(0,0,WIDTH,HEIGHT); + + ctx.font = "16px Pixio"; + ctx.fillStyle = "#FFF"; + ctx.textAlign = "center"; + ctx.fillText("Click here to cycle visualizations.", WIDTH/2,HEIGHT/2); + } + draw(); + } else if (mode == 1) { // BAR + analyser.fftSize = 256; + var bufferLength = analyser.frequencyBinCount; + var dataArray = new Uint8Array(bufferLength) + + var barWidth = (WIDTH / bufferLength) * 2.5; + var barHeight; + + function draw() { + ctx.clearRect(0, 0, WIDTH, HEIGHT); + drawRequest = window.requestAnimationFrame(draw); + + analyser.getByteFrequencyData(dataArray); + ctx.fillStyle = "#FFF"; + + var x = 0; + + ctx.beginPath(); + for (var i = 0; i < bufferLength; i++) { + barHeight = (dataArray[i] / 256) * HEIGHT; + ctx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight); + x += barWidth + 1; + } + ctx.closePath(); + } + draw(); + } else if (mode == 2) { // SCOPE + analyser.smoothingTimeConstant = 0; + var dataArray = new Uint8Array(analyser.fftSize) + + function draw() { + ctx.clearRect(0, 0, WIDTH, HEIGHT); + drawRequest = window.requestAnimationFrame(draw); + + analyser.getByteTimeDomainData(dataArray); + const step = WIDTH / dataArray.length; + + ctx.strokeStyle = "#0F0"; + + ctx.beginPath(); + for (let i = 0; i < dataArray.length; i += 2) { + const percent = dataArray[i] / 256; + const x = i * step; + const y = HEIGHT * percent; + ctx.lineTo(x, y); + } + ctx.stroke(); + } + draw(); + } else if (mode == 3) { // SPECTROGRAM + analyser.smoothingTimeConstant = 0; + + var tempCanvas = document.createElement("canvas"), + tempCtx = tempCanvas.getContext("2d"); + tempCanvas.width = WIDTH; + tempCanvas.height = HEIGHT; + + analyser.fftSize = Math.pow(2, Math.ceil(Math.log(WIDTH)/Math.log(2))); + + var bufferLength = analyser.frequencyBinCount; + var dataArray = new Uint8Array(bufferLength); + + var start = new Date(); + //ctx.clearRect(0,0,WIDTH,HEIGHT); + function draw() { + drawRequest = window.requestAnimationFrame(draw); + + analyser.getByteFrequencyData(dataArray); + var barHeight = WIDTH/analyser.fftSize; + + var now = new Date(); + if (now < new Date(start.getTime() + 20)) { return; } + start = now; + + tempCtx.drawImage(ctx.canvas, 0, 0, WIDTH, HEIGHT); + + for (var i = 0; i < dataArray.length; i++) { + var value = dataArray[i]; + ctx.fillStyle = 'rgb('+((value > 190) ? 255 : value)+', '+ ((value > 220) ? 255 : value-100) +', 0)'; + ctx.fillRect(WIDTH - 1, HEIGHT - i*barHeight, 1, barHeight); + } + ctx.translate(-1, 0); + ctx.drawImage(tempCanvas, 0, 0, WIDTH, HEIGHT, 0, 0, WIDTH, HEIGHT); + ctx.setTransform(1, 0, 0, 1, 0, 0); + } + draw(); + } else if (mode == 4) { // RUNWAY + analyser.smoothingTimeConstant = 0.2; + + var bufferLength = analyser.frequencyBinCount; + var dataArray = new Uint8Array(bufferLength); + + //ctx.clearRect(0,0,WIDTH,HEIGHT); + function draw() { + drawRequest = window.requestAnimationFrame(draw); + + analyser.getByteFrequencyData(dataArray); + + ctx.clearRect(0,0,WIDTH,HEIGHT); + for (var i = 0; i < dataArray.length; i++) { + if (dataArray[i] == 0) continue; + ctx.fillStyle = 'rgb('+dataArray[i]/4+','+dataArray[i]/2+','+dataArray[i]+')'; + ctx.fillRect((WIDTH / 2)+i, 0, 1, HEIGHT); + ctx.fillRect((WIDTH / 2)-(i+1), 0, 1, HEIGHT); + } + // You are probably wondering how I got here, huh? Questioning my very sanity. Or maybe you completely understand, and as a web-developer, very well know the experience of bending the knees for Chrome in order to get something working that already works everywhere else. Since I've explained it, I think regardless, you understand now. + ctx.font = "16px Pixio"; + ctx.fillStyle = "#4080FF0A"; + ctx.textAlign = "center"; + ctx.fillText("0x4655434B204348524F4D45",WIDTH/2,HEIGHT/2); + } + draw(); + } +} diff --git a/banner.png b/banner.png new file mode 100644 index 0000000..5bb052a Binary files /dev/null and b/banner.png differ diff --git a/dir.png b/dir.png new file mode 100644 index 0000000..6d3e8bd Binary files /dev/null and b/dir.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..68b15a1 --- /dev/null +++ b/index.html @@ -0,0 +1,40 @@ + + + wormTuner + + + + + + + + + + + +
+ +
+
+
+
+
+
+ + Test + +
+ +
+ +
+
+
+
+

Click on a station to tune in! If you are having trouble, try the Alternate player.

+
+ +
+
+ + diff --git a/radio.js b/radio.js new file mode 100644 index 0000000..d89002f --- /dev/null +++ b/radio.js @@ -0,0 +1,310 @@ +/* == wormTuner == */ +/* The Icecast Radio JS Tuner */ + +const nameJs = "wormTuner"; +const version = "0.3.1"; + +const statusJson = "/status-json.xsl"; + +// 'mimetype': 'icon-path' +const mimeIcons = { +// 'audio/aac':'/mime/aac.png', +}; + +const options = { + 'json-timer': 2500, // Time (in ms) for each stream update + 'visualizers': true, // whether to support visualizers + 'video-support': true, // whether to support video streams + 'character-overflow': 38, // character count needed to put in marquee. + 'replace-url': false, // fellow lazies please stand up + 'url-replacement': ['',''], + 'crossorigin': true +}; + +var ajax = new XMLHttpRequest(); +var sources = []; + +var isFancy = true; +var castContainer; +var currentStation = ""; + +var audioPlayer = document.createElement('video'); +var audioContext; +var audioAnalyser; +var visualizer; +var visMode = 0; + +var videoMode = false; + +// Timers +var volTimer; +var visTimer; + +function updateJSON() { + ajax.open('GET', statusJson, true); + ajax.send(); +} + +function processListenURL(url) { + if (!options['replace-url'] || !url) return url; + // Add Date.now() to prevent browser caching. Cache-Control isn't reliable IME + return url.replace(options['url-replacement'][0], options['url-replacement'][1])+"?"+Date.now(); +} + +function setSource(source) { + if (!source) return; + currentStation = source.listenurl.substr(23); + window.location.hash = "#"+currentStation; + + if (options['video-support'] && source.server_type.substr(0, source.server_type.indexOf('/')) == 'video') { + visMode = -1; + } else if (visMode < 0) visMode = 0; + + audioPlayer.src = processListenURL(source.listenurl); + audioPlayer.type = source.server_type; + + if (isFancy) { + if (!audioContext || !audioAnalyser) { + audioContext = new AudioContext(); + audioAnalyser = createVisualizer(audioPlayer, audioContext); + } + setVisualizer(visualizer, audioAnalyser, visMode); + } else audioPlayer = document.querySelector("video"); + + audioPlayer.play(); + document.body.scrollTop = 0; + updateJSON(); +} + +function handleVolume(event) { + event.preventDefault(); + var volMeter = document.querySelector('.vol'); + volMeter.classList.remove('hidden'); + var volInc = 0.05; + + if (event.deltaY < 0) { + if (audioPlayer.volume+volInc > 1) audioPlayer.volume=1; + else audioPlayer.volume+=volInc; + } + else if (event.deltaY > 0) { + if (audioPlayer.volume-volInc < 0) audioPlayer.volume=0; + else audioPlayer.volume-=volInc; + } + + volMeter.innerText = "VOL ["+'='.repeat(((audioPlayer.volume / 1) * 20)).padEnd(20, ' ') + "] "+(Math.round(audioPlayer.volume * 100)+'').padStart(3, ' ')+"%"; + + window.clearTimeout(volTimer) + volTimer = setTimeout(function(){ + volMeter.classList.add('hidden'); + }, 5000); +} + +function switchMode() { + if (videoMode) return; + if (!isFancy) return; + if (visMode == modes.length - 1) // - 1 if we count empty. + visMode = 0; + else visMode++; + + var visCounter = document.querySelector('.vismode'); + visCounter.classList.remove('hidden'); + + visCounter.innerText = modes[visMode]+' '+visMode+'/'+(modes.length - 1); + + setVisualizer(visualizer, audioAnalyser, visMode); + + window.clearTimeout(visTimer) + visTimer = setTimeout(function(){ + visCounter.classList.add('hidden'); + }, 5000); + + audioPlayer.play(); +} + +function newEntry(source, i) { + var entryContainer = document.createElement('div'); + entryContainer.classList.add('station'); + + entryContainer.name = source.listenurl.substr(23); + + var titlebar = document.createElement('div'), + infoBar = document.createElement('div'), + descBox = document.createElement('div'); + + titlebar.classList.add('titlebar'); + infoBar.classList.add('infobar'); + descBox.classList.add('descbox'); + + var titleTxt = document.createElement('b'), + listenerBox = document.createElement('div'); + + titleTxt.innerText = source.server_name; + titlebar.appendChild(titleTxt); + + var stationLineHeight = 24; + var grower = document.createElement('div'); + grower.style.flexGrow = "1"; + titlebar.appendChild(grower); + + var listenerImg = document.createElement('img'), + listenerTxt = document.createElement('a'); + + + listenerImg.src = "users.png"; + listenerImg.width = listenerImg.height = stationLineHeight; + listenerTxt.innerText = source.listeners; + + listenerBox.title = source.listeners+" listener"+(source.listeners != 1 ? 's' : '')+" (peak "+source.listener_peak+")"; + listenerBox.appendChild(listenerImg); + listenerBox.appendChild(listenerTxt); + titlebar.appendChild(listenerBox); + + entryContainer.appendChild(titlebar); + + entryContainer.appendChild(document.createElement('hr')); + + var bitrateTxt = document.createElement('a'), + urlTxt = document.createElement('a'), + urlImg = document.createElement('img'), + dirTxt = document.createElement('a'), + dirImg = document.createElement('img'); + + bitrateTxt.classList.add('bitrate'); + listenerBox.classList.add('listeners'); + + if (mimeIcons[source.server_type]) { + var formatImg = document.createElement('img'); + formatImg.classList.add('format'); + formatImg.height = stationLineHeight; + formatImg.alt = formatImg.title = source.server_type.substr(source.server_type.indexOf('/') + 1); + formatImg.src = mimeIcons[source.server_type]; + infoBar.appendChild(formatImg); + } else { + var formatTxt = document.createElement('a'); + formatTxt.classList.add('format'); + formatTxt.innerText = formatTxt.title = source.server_type.substr(source.server_type.indexOf('/') + 1).toUpperCase(); + infoBar.appendChild(formatTxt); + } + + var btr = source.bitrate; + if (!source.bitrate && source.audio_bitrate) btr = source.audio_bitrate / 1000; + else if (!source.audio_bitrate && !source.bitrate) btr = "???"; + bitrateTxt.innerHTML = btr + "kbps"; + infoBar.appendChild(bitrateTxt); + + if (source.server_url) { + urlTxt.href = source.server_url; + urlTxt.target = "_blank"; + urlImg.src = "url.png"; + urlImg.height = stationLineHeight; + urlTxt.appendChild(urlImg); + infoBar.appendChild(urlTxt); + } + + dirTxt.href = processListenURL(source.listenurl); + dirTxt.target = "_blank"; + dirImg.src = "dir.png"; + dirImg.height = stationLineHeight; + dirTxt.appendChild(dirImg); + infoBar.appendChild(dirTxt); + + entryContainer.appendChild(infoBar); + + var descTxt = document.createElement('p'); + descTxt.innerHTML = source.server_description + " (Genre: "+source.genre+")"; + descBox.appendChild(descTxt); + + entryContainer.appendChild(descBox); + + if (source.listenurl.substr(23) == currentStation) entryContainer.classList.add("selected"); + entryContainer.onclick = function() { + setSource(sources[i]); + this.classList.add("selected"); + } + + return entryContainer; +} + +function setMaybeOverflow(elem, txt) { + if (!txt) return; + var limit = options['character-overflow']; + if (elem.innerText == txt) return; + if (txt.length > limit) + elem.innerHTML = ""+txt+""; + else if (txt.length <= limit) + elem.innerHTML = txt; + +} + +function updateStreamInfo(source) { + var stationName = document.querySelector(".textainer .station"), + title = document.querySelector(".textainer .track"), + genre = document.querySelector(".textainer .genre"); + if (source) { + setMaybeOverflow(stationName,source.server_name); + if (source.artist) + setMaybeOverflow(title,source.artist+" - "+source.title); + else + setMaybeOverflow(title,source.title); + setMaybeOverflow(genre,source.genre); + } else stationName.innerText = title.innerText = genre.innerText = ""; +} + +ajax.onload = function() { + // Clear Container for new stuff. + while(castContainer.childElementCount > 0) { + castContainer.removeChild(castContainer.lastChild); + } + + if (ajax.status == 200) { // OK + if (ajax.response.icestats.source) { + if (ajax.response.icestats.source.length > 0) sources = ajax.response.icestats.source; + else sources = [ ajax.response.icestats.source ]; + } else sources = []; + var index = -1; + for (let i = 0; i < sources.length; i++) { + castContainer.appendChild(newEntry(sources[i], i)); + if (sources[i].listenurl.substr(23) == currentStation) index = i; + } + if (index != -1) updateStreamInfo(sources[index]); + if (audioPlayer.paused && currentStation.length > 0) + setSource(sources[index]); + } +} + +window.addEventListener('DOMContentLoaded', (event) => { + castContainer = document.querySelector(".broadcasts"); + ajax.responseType = 'json'; + updateJSON(); + + visualizer = document.querySelector('canvas.vis'); + isFancy = isFancy && (visualizer != null); + + // Detect Visualizer and initialize events + if (isFancy) { + visualizer.addEventListener('click', (event) => { + switchMode(); + }); + visualizer.addEventListener('wheel', handleVolume); + } + + // #my-stream.ogg -> listenurl: /my-stream.ogg + if (window.location.hash.length > 2) currentStation = window.location.hash.substr(1); + + // Handle name and versioning + var _nameElem = document.querySelector('.about .name'); + if (_nameElem) _nameElem.innerText = nameJs; + var _versElem = document.querySelector('.about .vers'); + if (_versElem) _versElem.innerText = 'v'+version; + + setInterval(function() { + updateJSON(); + }, options['json-timer']); + + if (options['crossorigin']) audioPlayer.setAttribute('crossorigin','anonymous'); + audioPlayer.onended = function () { + currentStation = ""; + console.log("Stream over"); + updateStreamInfo(null); + }; +}); diff --git a/style.css b/style.css new file mode 100644 index 0000000..c616d95 --- /dev/null +++ b/style.css @@ -0,0 +1,129 @@ +:root { + --background: #b3a99e; + --primary: #f3dca9; + --border: #f2e5c8; + + --station-primary: #d0d0d0; + --station-border: #a4a4a4; + + --station-selected: #d0dadf; + --station-selected-border: #0088ff; + + --about-color: #b9a77f; + --about-indent: #000000AA; +} + +body { + background: var(--background); + background-repeat: repeat; +} + +canvas.vis { + border: 2px inset var(--border); + background: black; + margin: 4px; +} + +.player { + background: var(--primary); + border: 2px outset var(--border); + width: 384px; +} + +.textainer { + display: flex; + flex-direction:column; + line-height: 24px; + text-align: left; + flex-grow: 1; + padding: 2px; + overflow: hidden; + text-align: center; +} +.textainer .station { font-weight: bold; } +.textainer .genre { font-style: italic; display: none; } +.textainer > div { white-space: nowrap; } +.textainer > div.overflow { + -moz-animation: scroll-left 2s linear infinite; + -webkit-animation: scroll-left 2s linear infinite; + animation: scroll-left 20s linear infinite; +} + +.section { + display: flex; + flex-direction: column; +} + +.broadcasts .station { + background: var(--station-primary); + color: black; + border: 1px outset var(--station-border); + padding: 4px; + margin: 16px; + text-align: left; + cursor: pointer; + width: 360px; + display: inline-block; + vertical-align:top; +} + +.broadcasts .station.selected { + outline: 2px solid var(--station-selected-border); + background: var(--station-selected); +} + +.broadcasts .station hr { + margin: 2px 0px; + border: 1px solid #a4a4a4; + width: 100%; +} + +.titlebar { + display: flex; +} + +.infobar { + display: flex; + gap: 8px; +} + +.broadcasts .station .listeners img { + margin-right: 2px; +} + +.list { + flex-grow: 1; +} + +.vol, +.vismode { + pointer-events: none; + position: relative; + height: 0; + text-shadow: 0px 0px 5px black; + color: white; +} + +.vol { + top: -32px; + white-space: pre; +} + +.vismode { + top: 8px; + text-align: right; + right: 16px; +} + +.vol.hidden, +.vismode.hidden { + display: none; +} + +.about { + display: flex; + margin: -8px 0px 0px 0px; + text-shadow: -1px -1px 0px var(--about-indent); + padding: 4px 8px; +} +.about, .about a, .about a:link, .about:visited {color: var(--about-color);text-decoration: none;} diff --git a/url.png b/url.png new file mode 100644 index 0000000..bfc1b9d Binary files /dev/null and b/url.png differ diff --git a/users.png b/users.png new file mode 100644 index 0000000..71a4833 Binary files /dev/null and b/users.png differ