From caf59c6c70036db1f2a0c7e391e8d37cba07a086 Mon Sep 17 00:00:00 2001 From: chuckdaniels Date: Mon, 12 Dec 2022 02:11:19 -0600 Subject: [PATCH] v0.4.0 --- .gitmodules | 3 + embed.html | 188 ++++++++ favicon.png | Bin 0 -> 2393 bytes heart.png | Bin 0 -> 5229 bytes icecast-metadata-js | 1 + index.html | 113 +++-- play.png | Bin 0 -> 5125 bytes radio.css | 304 ++++++++++++ radio.js | 1123 ++++++++++++++++++++++++++++++++----------- stop.png | Bin 0 -> 4860 bytes vis.png | Bin 0 -> 5452 bytes vol.png | Bin 0 -> 4890 bytes 12 files changed, 1412 insertions(+), 320 deletions(-) create mode 100644 .gitmodules create mode 100644 embed.html create mode 100644 favicon.png create mode 100644 heart.png create mode 160000 icecast-metadata-js create mode 100644 play.png create mode 100644 radio.css create mode 100644 stop.png create mode 100644 vis.png create mode 100644 vol.png diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c30cfb1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "icecast-metadata-js"] + path = icecast-metadata-js + url = https://github.com/eshaz/icecast-metadata-js.git diff --git a/embed.html b/embed.html new file mode 100644 index 0000000..6ff8657 --- /dev/null +++ b/embed.html @@ -0,0 +1,188 @@ + + + wormTuner Embed + + + + + + + + + + + + +
wormTuner
+ + + + +
+ +
+ + \ No newline at end of file diff --git a/favicon.png b/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c93f85bf617b63cbf0246d288d196ca3874ad2d1 GIT binary patch literal 2393 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 literal 0 HcmV?d00001 diff --git a/icecast-metadata-js b/icecast-metadata-js new file mode 160000 index 0000000..818cf3f --- /dev/null +++ b/icecast-metadata-js @@ -0,0 +1 @@ +Subproject commit 818cf3fe5cafeb46c4ec91dc15ea9105c5838bc8 diff --git a/index.html b/index.html index ecea275..ecdc46b 100644 --- a/index.html +++ b/index.html @@ -1,40 +1,75 @@ + - - wormTuner - - - - - - - - - - - -
- -
-
-
-
-
-
- - Test - -
- -
- -
-
-
-
-

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

-
- -
-
- - + + wormTuner + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ + +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ + +
+ +
+
+ + + \ No newline at end of file diff --git a/play.png b/play.png new file mode 100644 index 0000000000000000000000000000000000000000..ee71f0ef89def797a73a3e12e2a5c1602ba793d0 GIT binary patch literal 5125 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 literal 0 HcmV?d00001 diff --git a/radio.css b/radio.css new file mode 100644 index 0000000..a2639d0 --- /dev/null +++ b/radio.css @@ -0,0 +1,304 @@ +:root { + --background: #b3a99e; + + --primary: #0079b5; + --primary-border: #009ae6; + + --secondary: #f3dca9; + --secondary-border: #f2e5c8; + + --button: #DFDFDF; + --button-active: gray; + + --monitor: #0F0F0F; + --monitor-text: #F0F0F0; + --monitor-border: gray; + + --link-color: unset; +} + +*:link, *:visited { + color: var(--link-color); +} + +html { + height: 100%; +} + +body { + background: var(--background); + background-repeat: repeat; + height: 100%; + margin: 0; +} + +.container, .player-container { + background: var(--primary); + border: 2px outset var(--primary-border); + padding: 4px; +} +.container { + min-width: 320px; + min-height: 280px; + display: flex; + gap: 4px; + margin: auto; + max-width: 976px; +} +.player-container { + position: relative; + border-bottom: none; + width: 320px; + height: 78px; + margin-bottom: -2px; + z-index: 1; +} +.footer { + min-width: 320px; + max-width: 976px; + text-align: right; + margin: auto; +} + +player, visualizer { + display: flex; + flex-direction: column; + background: var(--secondary); + border: 2px outset var(--secondary-border); + flex-shrink: 0; + padding: 4px; +} + +.banner { + display: flex; +} + +player { + width: 308px; height: 68px; +} + +visualizer { + margin-bottom: 4px; +} + +.vis-container { + width: 320px; + max-width: 320px; +} + +button, fakebutton, slider { + border: 2px outset var(--button); + background: var(--button); +} + +slider input { + background: lightgray; + -webkit-appearance: none; + appearance: none; + height: 4px; + margin: 6 2; + width: 96px; +} + +slider input::-moz-range-thumb, +slider input::-webkit-slider-thumb { + background: gray; + -webkit-appearance: none; + appearance: none; + width: 8px; + height: 16px; + border: none; + border-radius: 0; +} + +select { + border: none; + background: none; +} + +button:active, +button.active, +fakebutton:active, +fakebutton.active { + border: 2px inset var(--button-active); + background: var(--button-active); +} + +fakebutton { display: flex; } +fakebutton img { margin: 2px; } + +controls { + display: flex; +} + +controls input { flex-grow: 1; } + +tablist { + display: flex; + flex-shrink: 0; +} + +metadata, canvas { + color: var(--monitor-text); + background: black; + border: 2px inset var(--secondary-border); +} + +metadata { + padding: 4px; + display: flex; + flex-direction: column; +} +metadata station { font-weight: bold; } + +panel { + display: flex; + flex-direction: column; + flex: 1; +} + +stations { + display: flex; + flex-direction: column; + overflow-y: auto; + border: 2px inset var(--primary-border); + margin: 2px; + flex-grow: 1; +} + +favorites, history, stats { + background: var(--monitor); + color: var(--monitor-text); + border: 2px inset var(--monitor-border); + flex-grow: 1; + display: flex; + flex-direction: column; + padding: 4px; + overflow-y: scroll; + min-height: 0; +} + +tabbox { + display: none; + flex-direction: column; + flex-grow: 1; +} +tabbox.active { display: flex; } + +station { + display: flex; + flex-direction: column; + cursor: pointer; + user-select: none; + padding: 4px; + margin: 2px; + border: 2px inset var(--button-active); + background: var(--button-active); +} + +station.active { + background: var(--button); + border: 2px outset var(--button); + color: black; +} + +station station-name { font-weight: bold; } +station track-meta { + font-size: 12px; +} + +history track-container { + display: flex; + flex-direction: column; + padding: 2px; +} + +history track-container track-station { font-weight: bold; } + +history track-container:nth-of-type(1) { + background-color: var(--monitor-text); + color: var(--monitor); +} + +canvas.vis, canvas.metadata { border-bottom: none; } + +.button-row { display: flex; } + +.fav-button img { + filter: contrast(0%); +} + +.fav-button.fav img { + filter: none; +} + +favorites .station-header, +history .station-header { + text-align: center; + margin: 4px 0; + text-decoration: underline; + margin-top: 12px; +} + +favorites .track, +history .track { + display: flex; + border-bottom: 1px dashed var(--monitor-text); + margin: 2px 0; +} + +favorites .track button, +history .track button { + background: none; + border: none; + cursor: pointer; + flex-shrink: 0; + flex-grow: 0; + margin: 2px; +} + +favorites .track a, +history .track a { + flex-grow: 1; +} + +@media only screen and (orientation: portrait) { + .container, .footer { + flex-direction: column; + width: 320px; + } + + .banner { flex-direction: column-reverse; } + .banner img, .player-container { + margin: auto; + } + .banner > img { flex-shrink: 0; } + .player-container { margin-bottom: -2px; } + + panel { + max-height: 480px; + } + + favorites, history { + height: 240px; + } +} + +@media only screen and (orientation: landscape) { + .container, .footer { + width: calc(100% - 64px); + height: calc(100% - 212px); + } + + .banner { + margin: auto; + min-width: 320px; + width: calc(100% - 52px); + max-width: 988px; + } + .banner > img { margin-top: 2px; } + + .player-container { + margin-top: 48px; + } +} \ No newline at end of file diff --git a/radio.js b/radio.js index d89002f..7bf3d94 100644 --- a/radio.js +++ b/radio.js @@ -1,310 +1,871 @@ -/* == 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 tuner_settings = { + tuner_name: "wormTuner", + icecast_status: "/stream/status-json.xsl", + update_interval: 0 // in milliseconds (ms). 0 means no update }; -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 tuner_mem = { + xhr: null }; -var ajax = new XMLHttpRequest(); -var sources = []; +// VISUALIZER VARS START +var visualizer = { + canvas: null, + mode: 1, + drawRequest: 0, + renderInterval: null, + startTime: new Date() +}; +const vis_modes = [ + "None", + "Bars", + "Scope", + "Multi-Scope", + "Spectrogram", + "Hyperdrive", + "Nixie Bars", + "Wire Meters", + "VU Bars", + "Stereo Diff" +]; +// VISUALIZER VARS END -var isFancy = true; -var castContainer; -var currentStation = ""; +var tuner = { + player: null, + playing: true, + volume: 100, + canvas: { + element: null, + drawRequest: 0 + }, + html_elements: { + title: null, + station: null + }, + analysers: null, + context: null, + splitter: null, + merger: null, + history_container: null, + station_container: null, + favorites_container: null +}; -var audioPlayer = document.createElement('video'); -var audioContext; -var audioAnalyser; -var visualizer; -var visMode = 0; +var station = null; -var videoMode = false; +var track_favorites = { +}; -// Timers -var volTimer; -var visTimer; +var track_history = [ +]; -function updateJSON() { - ajax.open('GET', statusJson, true); - ajax.send(); +// Updates tuner_mem.xhr +function updateXHR() { + tuner_mem.xhr.open('GET', tuner_settings.icecast_status, true); + tuner_mem.xhr.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(); +// Fixes listen_url to return a proper proxied address. +function fixURL(url) { + return url.replace('http://example.com:8000/', '/stream/'); } -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 startStation(new_station) { + station = new_station; + updateMetadata(null); // Avoid HUGE ROCK STATION playing Seasame Street theme text. + if (tuner.player != null) tuner.player.stop(); + // Start player using IcecastMetadataPlayer for the metadata. + // Use ?+date for avoiding problems with cache. + tuner.player = new IcecastMetadataPlayer(new_station.listen_url+"?"+Date.now(), { + onMetadata: (metadata) => {updateMetadata(metadata.StreamTitle);}, + metadataTypes: ["icy"] + }); + + // Start new context with analysers + // Check if channels is only 1. If not, we assume stereo. + tuner.context = new AudioContext(); + tuner.analysers = createVisualizer(tuner.player.audioElement, tuner.context, new_station.channels != 1); + setVisualizer(tuner.analysers, visualizer.mode); + + // Properly set volume value. + tuner.player.audioElement.volume = tuner.volume/100; + play(); } -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 searchStations(query) { + var stations = document.querySelectorAll("stations > station"); + for(let f = 0; f < stations.length; f++) { + var station = stations[f]; + var name = station.querySelector(':scope > station-name').innerText; + var description = station.querySelector(':scope > description').innerText; + var meta = station.querySelector(':scope > track-meta').innerText; + + if (name.toLowerCase().includes(query.toLowerCase()) || description.toLowerCase().includes(query.toLowerCase()) || meta.toLowerCase().includes(query.toLowerCase())) + station.style.display = ''; + else + station.style.display = 'none'; + } } -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 play() { + tuner.player.play(); + document.querySelector('.play').classList.add('active'); } -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 stop() { + tuner.player.stop(); + document.querySelector('.play').classList.remove('active'); + station = null; + updateMetadata(''); } -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 updateMetadata(track) { + var line_data = [ + { + 'index': 0, + 'timer': new Date() + } + ]; + var load_str = [ // Fuck off. + '==-'.padStart(0, ' '), + '--==-'.padStart(4, ' '), + '--==-'.padStart(8, ' '), + '--==-'.padStart(12, ' '), + '--==-'.padStart(16, ' '), + '--==-'.padStart(20, ' '), + '--==-'.padStart(24, ' '), + '--==-'.padStart(28, ' '), + '--==-'.padStart(32, ' '), + '-=='.padStart(36, ' '), + '-==--'.padStart(32, ' '), + '-==--'.padStart(28, ' '), + '-==--'.padStart(24, ' '), + '-==--'.padStart(20, ' '), + '-==--'.padStart(16, ' '), + '-==--'.padStart(12, ' '), + '-==--'.padStart(8, ' '), + '-==--'.padStart(4, ' ') + ]; + var max_chars = 36; + function marqueeify(line, x, y, e) { + if (!line_data[e]) line_data[e] = { 'index': 0,'timer': new Date() }; + if (line != null && line.length >= max_chars) { + var txt = line.concat(' ').concat(line); + if (line_data[e].index - 4 > line.length) {line_data[e].index = 0;} + ctx.fillText(txt.substr(line_data[e].index,max_chars), x,y); + var now = new Date(); + if (now >= new Date(line_data[e].timer.getTime() + 250)) { + line_data[e].timer = now; + line_data[e].index++; + } + } else if (line == null || line.length == 0) { + if (line_data[e].index >= load_str.length) line_data[e].index = 0; + ctx.fillText(load_str[line_data[e].index], x, y); + var now = new Date(); + if (now >= new Date(line_data[e].timer.getTime() + 50)) { + line_data[e].timer = now; + line_data[e].index++; + } + } else ctx.fillText(line,x,y); + } + + if (track != null) { + addHistory(station, track); + updateFavStatus(isFavorite(station.listen_url, track)); + } + + station.title = track; + if (tuner.canvas.element) { + var ctx = tuner.canvas.element.getContext("2d",{antialias: false,alpha: false}); + + var WIDTH = tuner.canvas.element.width; + var HEIGHT = tuner.canvas.element.height; + + window.cancelAnimationFrame(tuner.canvas.drawRequest); + function drawMeta() { + tuner.canvas.drawRequest = window.requestAnimationFrame(drawMeta); + + ctx.clearRect(0,0,WIDTH,HEIGHT); + if (station != null) { + let quality_text = station.bitrate ? station.bitrate+'kbps' : station.quality; + + ctx.font = "16px hack,monospace"; + ctx.fillStyle = "#FFF"; + ctx.textAlign = "justify"; + + ctx.font = "bold 16px hack,monospace"; + marqueeify(station.name,6,20,0); + ctx.font = "16px hack,monospace"; + marqueeify(track,8,36,1); + } + } + drawMeta(); + } } -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 = ""; +function setTab(i) { + var tabbuttons = document.querySelectorAll('tablist button'); + var tabboxes = document.querySelectorAll('tabbox'); + for (let t = 0; t < tabboxes.length; t++) { + var tabbox = tabboxes[t]; + var tabbutton = tabbuttons[t]; + if (t != i) { + tabbox.classList.remove('active'); + tabbutton.classList.remove('active'); + } else { + tabbox.classList.add('active'); + tabbutton.classList.add('active'); + } + } } -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]); - } +function addHistory(station, track = null) { + if (station == null) return; + var track = track??station.title; + + if (track_history.length > 0 && (track_history[0].station_id == station.listen_url && track_history[0].title == track)) return; + + console.log(station.name.concat(' > ').concat(track)); + var trackContainer = document.createElement('div'), + favButton = document.createElement('button'), + favImg = document.createElement('img'), + trackTitle = document.createElement('a'); + + let station_url = station.listen_url; + let station_track = track; + + favButton.classList.add('fav-button'); + + favButton.onclick = function() { + let status = favoriteTrack(station_url, station_track); + updateFavStatus(status) + favButton.classList.toggle('fav', status); + }; + favImg.src = 'heart.png'; + if (isFavorite(station_url, station_track)) favButton.classList.add('fav'); + + trackTitle.innerText = track; + + favButton.appendChild(favImg); + + trackContainer.appendChild(trackTitle); + trackContainer.appendChild(favButton); + trackContainer.classList.add('track'); + + if (track_history[0] == null || track_history[0].station_id != station.listen_url) { + tuner.history_container.insertBefore(trackContainer, tuner.history_container.childNodes[0]); + + var statCont = document.createElement('div'), + statText = document.createElement('b'); + + statText.innerText = station.name; + statCont.appendChild(statText); + statCont.classList.add('station-header'); + + tuner.history_container.insertBefore(statCont, tuner.history_container.childNodes[0]); + } else { + tuner.history_container.insertBefore(trackContainer, tuner.history_container.childNodes[1]); + } + + var history_entry = { + station_id: station.listen_url, + title: track + }; + + track_history.unshift(history_entry); +} + +function isFavorite(track_station, title) { + return track_favorites[track_station] != null && track_favorites[track_station].includes(title); +} + +function favoriteCurrentTrack() { + updateFavStatus(favoriteTrack(station.listen_url, station.title)); +} + +function favoriteTrack(track_station, track) { + if (track_favorites[track_station] == null) + track_favorites[track_station] = []; + + var result = false; + if (track_favorites[track_station].includes(track)) { + track_favorites[track_station].splice(track_favorites[track_station].indexOf(track), 1); + } else { + track_favorites[track_station].unshift(track); + result = true; + } + const d = new Date(); + d.setTime(d.getTime() + (99983090*24*60*60*1000)); + let expires = "expires=" + d.toUTCString(); + document.cookie = "favorites="+JSON.stringify(track_favorites)+";"+expires+";SameSite=None;secure=1;"; + + populateFavorites(); + return result; +} + +function updateFavStatus(isFav = false) { + var favButton = document.querySelector('button.fav-button'); + if (isFav) { + favButton.classList.add('fav'); + } else { + favButton.classList.remove('fav'); + } +} + +function exportFavorites() { + var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(track_favorites, null, 2)); + var downloadAnchorNode = document.createElement('a'); + downloadAnchorNode.setAttribute("href", dataStr); + downloadAnchorNode.setAttribute("download", "worlio_station_favorites.json"); + document.body.appendChild(downloadAnchorNode); + downloadAnchorNode.click(); + downloadAnchorNode.remove(); +} + +function populateFavorites() { + while (tuner.favorites_container.hasChildNodes()) { + tuner.favorites_container.lastElementChild.remove(); + } + for (var key in track_favorites) { + var statCont = document.createElement('div'), + statText = document.createElement('b'); + + statText.innerText = key; + statCont.appendChild(statText); + statCont.classList.add('station-header'); + + tuner.favorites_container.append(statCont); + + var value = track_favorites[key]; + for (let t = 0; t < value.length; t++) { + var track = value[t]; + + var trackContainer = document.createElement('div'), + favButton = document.createElement('button'), + favImg = document.createElement('img'), + trackTitle = document.createElement('a'); + + let station_url = key; + let station_track = track; + + favButton.classList.add('fav-button'); + + favButton.onclick = function() { + let status = favoriteTrack(station_url, station_track); + updateFavStatus(status) + favButton.classList.toggle('fav', status); + }; + favImg.src = 'heart.png'; + if (isFavorite(station_url, station_track)) favButton.classList.add('fav'); + + trackTitle.innerText = track; + + favButton.appendChild(favImg); + + trackContainer.appendChild(trackTitle); + trackContainer.appendChild(favButton); + trackContainer.classList.add('track'); + + tuner.favorites_container.append(trackContainer); + } + } +} + +function searchFavorites(query) { + var tracks = document.querySelectorAll("favorites > .track"); + for(let f = 0; f < tracks.length; f++) { + var track = tracks[f]; + var title = track.querySelector(':scope > a'); + + if (!title.innerText.toLowerCase().includes(query.toLowerCase())) + track.style.display = 'none'; + else + track.style.display = ''; + } +} + +function volChange(e) { + tuner.player.audioElement.volume=e.value/100; + e.parentNode.title = e.value+'%'; + tuner.volume = e.value; +} + +function setVisMode(i) { + if (i > vis_modes.length) visualizer.mode = 1; + else visualizer.mode = i; + setVisualizer(tuner.analysers, visualizer.mode); +} + +function createVisualizer(player, context, stereo = true) { + var audioSrc = context.createMediaElementSource(player); + if (stereo) { + tuner.splitter = context.createChannelSplitter(2); + audioSrc.connect(tuner.splitter); + tuner.merger = context.createChannelMerger(2); + + var analysers = { + left: tuner.context.createAnalyser(), + right: tuner.context.createAnalyser() + }; + + tuner.splitter.connect(analysers.left, 0); + tuner.splitter.connect(analysers.right, 1); + + analysers.left.connect(tuner.merger, 0, 0); + analysers.right.connect(tuner.merger, 0, 1); + + tuner.merger.connect(context.destination); + } else { + var analy = context.createAnalyser(); + audioSrc.connect(analy); + analy.connect(context.destination); + var analysers = { + left: analy, + right: analy + }; + } + setVisualizer(analysers, 0); + + return analysers; +}; + +function setVisualizer(analysers, mode = 0) { + var canvas = visualizer.canvas; + if (!analysers) return; + clearInterval(visualizer.renderInverval); + var ctx = canvas.getContext("2d",{antialias: false,alpha: false}); + + var HEIGHT; + var WIDTH = canvas.width; + + // Set the "defaults" + analysers.left.smoothingTimeConstant = 0.8; + analysers.left.fftSize = 2048; + + analysers.right.smoothingTimeConstant = 0.8; + analysers.right.fftSize = 2048; + + var analyser = analysers.right; + + ctx.clearRect(0, 0, WIDTH, HEIGHT); + window.cancelAnimationFrame(visualizer.drawRequest); + if (mode == 0 || mode > vis_modes.length) { + HEIGHT = canvas.height = 96; + function drawVis() { + ctx.clearRect(0, 0, WIDTH, HEIGHT); + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + } + drawVis(); + } else if (mode == 1) { // BAR + HEIGHT = canvas.height = 96; + analysers.right.fftSize = analysers.left.fftSize = 256; + var bufferLength = analyser.frequencyBinCount; + var dataArrayL = new Uint8Array(bufferLength) + var dataArrayR = new Uint8Array(bufferLength) + + function drawVis() { + ctx.clearRect(0, 0, WIDTH, HEIGHT); + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + + analysers.left.getByteFrequencyData(dataArrayL); + analysers.right.getByteFrequencyData(dataArrayR); + ctx.fillStyle = "#FFF"; + + var x = 0; + + ctx.beginPath(); + for (var i = 0; i < bufferLength; i++) { + var value = (dataArrayL[i] + dataArrayR[i]) / 2; + barHeight = (value / 256) * HEIGHT; + ctx.fillRect(x, HEIGHT - barHeight, WIDTH / bufferLength, barHeight); + x += (WIDTH / bufferLength) + 1; + } + ctx.closePath(); + } + drawVis(); + } else if (mode == 2) { // SCOPE + HEIGHT = canvas.height = 96; + analysers.left.smoothingTimeConstant = analysers.right.smoothingTimeConstant = 0; + var dataArrayL = new Uint8Array(analysers.left.fftSize) + var dataArrayR = new Uint8Array(analysers.right.fftSize) + + function drawVis() { + ctx.clearRect(0, 0, WIDTH, HEIGHT); + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + + analysers.left.getByteTimeDomainData(dataArrayL); + analysers.right.getByteTimeDomainData(dataArrayR); + + const step = WIDTH / dataArrayL.length; + + ctx.strokeStyle = "#0F0"; + + ctx.beginPath(); + for (let i = 0; i < dataArrayL.length; i += 2) { + var percent = (dataArrayL[i] + dataArrayR[i]) / 512; + ctx.lineTo(i * step, HEIGHT * percent); + } + ctx.stroke(); + } + drawVis(); + } else if (mode == 3) { // MULTI SCOPE + HEIGHT = canvas.height = 96; + analysers.left.smoothingTimeConstant = analysers.right.smoothingTimeConstant = 0; + var dataArrayL = new Uint8Array(analysers.left.fftSize) + var dataArrayR = new Uint8Array(analysers.right.fftSize) + + function drawVis() { + ctx.clearRect(0, 0, WIDTH, HEIGHT); + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + + analysers.left.getByteTimeDomainData(dataArrayL); + analysers.right.getByteTimeDomainData(dataArrayR); + + const step = WIDTH / dataArrayL.length; + + ctx.strokeStyle = "#0F0"; + + ctx.beginPath(); + for (let i = 0; i < dataArrayL.length; i += 2) { + var percentL = dataArrayL[i] / 512; + ctx.lineTo(i * step, HEIGHT * percentL); + } + ctx.stroke(); + ctx.beginPath(); + for (let i = 0; i < dataArrayR.length; i += 2) { + var percentR = dataArrayR[i] / 512; + ctx.lineTo(i * step, (HEIGHT * percentR) + HEIGHT/2); + } + ctx.stroke(); + } + drawVis(); + } else if (mode == 4) { // SPECTROGRAM + HEIGHT = canvas.height = 128; + analysers.left.smoothingTimeConstant = analysers.right.smoothingTimeConstant = 0; + + var tempCanvas = document.createElement("canvas"), + tempCtx = tempCanvas.getContext("2d"); + tempCanvas.width = WIDTH; + tempCanvas.height = HEIGHT; + + analysers.left.fftSize = analysers.right.fftSize = Math.pow(2, Math.ceil(Math.log(WIDTH)/Math.log(2))); + + var bufferLength = analyser.frequencyBinCount; + var dataArrayL = new Uint8Array(bufferLength); + var dataArrayR = new Uint8Array(bufferLength); + + var startTime = new Date(); + function drawVis() { + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + + analysers.left.getByteFrequencyData(dataArrayL); + analysers.right.getByteFrequencyData(dataArrayR); + var barHeight = WIDTH/analysers.left.fftSize; + + var now = new Date(); + if (now < new Date(startTime.getTime() + 20)) { return; } + startTime = now; + + tempCtx.drawImage(ctx.canvas, 0, 0, WIDTH, HEIGHT); + + for (var i = 0; i < bufferLength; i++) { + var value = (dataArrayL[i]/2)+(dataArrayR[i]/2); + 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); + } + drawVis(); + } else if (mode == 5) { // LASER RAIN + HEIGHT = canvas.height = 64; + analysers.left.smoothingTimeConstant = analysers.right.smoothingTimeConstant = 0; + var bufferLength = analysers.left.frequencyBinCount; + var dataArrayL = new Uint8Array(bufferLength); + var dataArrayR = new Uint8Array(bufferLength); + + //ctx.clearRect(0,0,WIDTH,HEIGHT); + function drawVis() { + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + + analysers.left.getByteFrequencyData(dataArrayL); + analysers.right.getByteFrequencyData(dataArrayR); + + ctx.clearRect(0,0,WIDTH,HEIGHT); + for (var i = 0; i < bufferLength; i++) { + if (dataArrayL[i] != 0) { + ctx.fillStyle = 'rgb('+dataArrayL[i]/4+','+dataArrayL[i]/2+','+dataArrayL[i]+')'; + ctx.fillRect((WIDTH / 2)-(i+1), 0, 1, HEIGHT); + } + + if (dataArrayR[i] != 0) { + ctx.fillStyle = 'rgb('+dataArrayR[i]/4+','+dataArrayR[i]/2+','+dataArrayR[i]+')'; + ctx.fillRect((WIDTH / 2)+i, 0, 1, HEIGHT); + } + } + } + drawVis(); + } else if (mode == 6) { // HEATBARS + HEIGHT = canvas.height = 96; + analysers.left.fftSize = 256; + analysers.right.fftSize = 256; + var bufferLength = analyser.frequencyBinCount; + var dataArray = new Uint8Array(bufferLength); + var dataArrayL = new Uint8Array(bufferLength); + var dataArrayR = new Uint8Array(bufferLength); + + function drawVis() { + ctx.clearRect(0, 0, WIDTH, HEIGHT); + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + + analyser.getByteFrequencyData(dataArray); + analysers.left.getByteFrequencyData(dataArrayL); + analysers.right.getByteFrequencyData(dataArrayR); + + for (let i = 0; i < bufferLength; i++) { + ctx.beginPath(); + ctx.fillStyle = 'rgb(255,'+dataArray[i]+',0)'; + ctx.ellipse((i*3), ((HEIGHT)-(dataArrayL[i]-dataArrayR[i])/4)/2, 1, dataArray[i] / 5, 0, 0, 2 * Math.PI); + ctx.fill(); + ctx.closePath(); + } + } + drawVis(); + } else if (mode == 7) { // VU METERS + HEIGHT = canvas.height = 96; + analysers.left.fftSize = 256; + analysers.right.fftSize = 256; + var bufferLength = analyser.frequencyBinCount; + var dataArrayL = new Uint8Array(bufferLength); + var dataArrayR = new Uint8Array(bufferLength); + + function drawVis() { + ctx.clearRect(0, 0, WIDTH, HEIGHT); + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + + analysers.left.getByteFrequencyData(dataArrayL); + analysers.right.getByteFrequencyData(dataArrayR); + + var dataL = dataArrayL.reduce(function(a,b){ return a+b; }); + var dataR = dataArrayR.reduce(function(a,b){ return a+b; }); + + function drawChannel(data, x) { + ctx.beginPath(); + ctx.arc(x, HEIGHT, HEIGHT-24, 1*Math.PI, 0, false); + ctx.strokeStyle = "white"; + ctx.stroke(); + ctx.closePath(); + + ctx.beginPath(); + + ctx.arc(x, HEIGHT, HEIGHT-32, 1.3*Math.PI, (1.3*Math.PI)+(data/12000), false); + ctx.strokeStyle = "transparent"; + ctx.stroke(); + + ctx.lineTo(x, HEIGHT); + ctx.strokeStyle = "red"; + ctx.stroke(); + ctx.closePath(); + + } + + drawChannel(dataL, (WIDTH/4)); + drawChannel(dataR, (WIDTH/4)*3); + } + drawVis(); + } else if (mode == 8) { // VU BARS + tuner.analysers.left.smoothingTimeConstant = tuner.analysers.right.smoothingTimeConstant = 0.2; + HEIGHT = canvas.height = 32; + analysers.left.fftSize = 256; + analysers.right.fftSize = 256; + var dataArrayL = new Uint8Array(analysers.left.frequencyBinCount); + var dataArrayR = new Uint8Array(analysers.right.frequencyBinCount); + + function drawVis() { + ctx.clearRect(0, 0, WIDTH, HEIGHT); + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + + analysers.left.getByteFrequencyData(dataArrayL); + analysers.right.getByteFrequencyData(dataArrayR); + + var dataL = dataArrayL.reduce(function(a,b){ return a+b; }); + var dataR = dataArrayR.reduce(function(a,b){ return a+b; }); + + ctx.beginPath(); + for (let l = 0; l < Math.min(Math.max(Math.round(dataL / 1270), 0), 31); l++) { + if (l > 16) ctx.fillStyle = "rgb(255,0,0)"; + else if (l > 10) ctx.fillStyle = "rgb(255,255,0)"; + else ctx.fillStyle = "rgb(0,255,0)"; + ctx.fillRect(2+(l*14), (HEIGHT/2)-6, 12, 4); + } + ctx.closePath(); + + ctx.beginPath(); + for (let r = 0; r < Math.min(Math.max(Math.round(dataR / 1270), 0), 31); r++) { + if (r > 16) ctx.fillStyle = "rgb(255,0,0)"; + else if (r > 10) ctx.fillStyle = "rgb(255,255,0)"; + else ctx.fillStyle = "rgb(0,255,0)"; + ctx.fillRect(2+(r*14), (HEIGHT/2)+2, 12, 4); + } + ctx.closePath(); + } + drawVis(); + } else if (mode == 9) { // STEREO DIFFERENCE + HEIGHT = canvas.height = 128; + analysers.left.smoothingTimeConstant = analysers.right.smoothingTimeConstant = 0; + + var tempCanvas = document.createElement("canvas"), + tempCtx = tempCanvas.getContext("2d"); + tempCanvas.width = WIDTH; + tempCanvas.height = HEIGHT; + + analysers.left.fftSize = analysers.right.fftSize = Math.pow(2, Math.ceil(Math.log(WIDTH)/Math.log(2))); + + var bufferLength = analyser.frequencyBinCount; + var dataArrayL = new Uint8Array(bufferLength); + var dataArrayR = new Uint8Array(bufferLength); + + var startTime = new Date(); + function drawVis() { + visualizer.drawRequest = window.requestAnimationFrame(drawVis); + + analysers.left.getByteFrequencyData(dataArrayL); + analysers.right.getByteFrequencyData(dataArrayR); + var barHeight = WIDTH/analysers.left.fftSize; + + var now = new Date(); + if (now < new Date(startTime.getTime() + 20)) { return; } + startTime = now; + + tempCtx.drawImage(ctx.canvas, 0, 0, WIDTH, HEIGHT); + + for (var i = 0; i < bufferLength; i++) { + var value = dataArrayL[i] - dataArrayR[i]; + // LEFT + if (value > 0) ctx.fillStyle = 'rgb('+value+', 0, 0)'; + // RIGHT + else if (value < 0) ctx.fillStyle = 'rgb(0, 0, '+((value*-1)*4)+')'; + else ctx.fillStyle = 'rgb(0, 0, 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); + } + drawVis(); + } } 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); - }; + visualizer.canvas = document.querySelector('canvas.vis'); + + tuner.canvas.element = document.querySelector('canvas.metadata'); + tuner.history_container = document.querySelector('history'); + tuner.station_container = document.querySelector('stations'); + tuner.favorites_container = document.querySelector('favorites'); + + var fav_cookie = getCookie('favorites'); + if (fav_cookie != null && fav_cookie.length > 0) track_favorites = JSON.parse(getCookie('favorites')); + populateFavorites(); + + var visModeChanger = document.querySelector('select[name="visModes"]'); + for (let v = 0; v < vis_modes.length; v++) { + var option = document.createElement('option'); + option.innerText = vis_modes[v]; + option.value = v; + visModeChanger.appendChild(option); + } + visModeChanger.selectedIndex = visualizer.mode; + + tuner_mem.xhr = new XMLHttpRequest(); + tuner_mem.xhr.responseType = 'json'; + tuner_mem.xhr.onload = function() { + var xhr = tuner_mem.xhr; + + while (tuner.station_container.hasChildNodes()) { + tuner.station_container.lastElementChild.remove(); + } + + if (xhr.response) { + var stations = xhr.response.icestats.source; + for(let s = 0; s < stations.length; s++) { + let st = stations[s]; + + var stationContainer = document.createElement('station'), + stationName = document.createElement('station-name'), + stationURLs = document.createElement('station-urls'), + stationDesc = document.createElement('description'), + stationMeta = document.createElement('track-meta'); + + stationName.innerText = st.server_name; + stationDesc.innerText = st.server_description; + + let urls = []; + urls.push('Direct'); + if (st.server_url) urls.push('Site'); + stationURLs.innerHTML = urls.join(' - '); + + let meta = []; + meta.push(st.server_type.substr(st.server_type.indexOf('/')+1).toUpperCase()); + if (st.bitrate) meta.push(st.bitrate+"kbps"); + else if (st.quality) meta.push(st.quality); + else meta.push('???kbps'); + if (st.channels) meta.push(st.channels+'ch'); + else meta.push('?ch'); + if (st.samplerate) meta.push((st.samplerate / 1000)+'hz'); + else meta.push('????hz'); + meta.push(st.genre); + meta.push(st.listeners+" Listening"); + stationMeta.innerText = meta.join(' - '); + + stationContainer.appendChild(stationName); + stationContainer.appendChild(stationURLs); + stationContainer.appendChild(stationDesc); + stationContainer.appendChild(stationMeta); + + let nstat = { + name: st.server_name, + listen_url: fixURL(st.listenurl), + channels: st.channels, + bitrate: st.bitrate, + quality: st.quality, + listeners: st.listeners, + genre: st.genre, + samplerate: st.samplerate, + description: st.server_description, + type: st.server_type, + site_url: st.server_url + }; + + if (station != null && fixURL(st.listenurl) == station.listen_url) { + stationContainer.classList.add('active'); + } + + stationContainer.addEventListener('click', (e) => { + startStation(nstat); + var stationboxes = document.querySelectorAll('stations station'); + for (let s = 0; s < stationboxes.length; s++) { + stationboxes[s].classList.remove('active'); + } + e.currentTarget.classList.add('active'); + }); + + tuner.station_container.appendChild(stationContainer); + } + } + } + + updateXHR(); }); + +function getCookie(cName) { + const name = cName + "="; + const cDecoded = decodeURIComponent(document.cookie); //to be careful + const cArr = cDecoded .split('; '); + let res; + cArr.forEach(val => { + if (val.indexOf(name) === 0) res = val.substring(name.length); + }) + return res; +} \ No newline at end of file diff --git a/stop.png b/stop.png new file mode 100644 index 0000000000000000000000000000000000000000..848cf6d53e5d3e0e0dab49c8fd460d835d7f0464 GIT binary patch literal 4860 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 literal 0 HcmV?d00001 diff --git a/vis.png b/vis.png new file mode 100644 index 0000000000000000000000000000000000000000..043f760de4f44d5c106f29e779284aea9e099854 GIT binary patch literal 5452 zcmeHLdsGuw8V{g=fDe=kBI-1(>#LJwChwSl$SYAE5rQCS$z)~{N65oufZ!Vhe6b3x zOCMI0ty;Aei-;8kYy}@E*dlsVM6{yVt`)UD>MGQ|35eM3IeR?a?qB9)l9_wI?|1L_ z`+ncOxmlx(3Los|>&9R(1}o$tG2nZc?Q(DepW|hECWGPh(IQm>9fM^+CbLmXrIHYx zX(A!gN@*DkYs;PZS?ir9cy!j~htq1#UQ;)oO)OZPxKwi7c++cdUa7nJ$@d$I_lzyRdjlKoS;haQu(NH`qe|+h zf8^vOY^&IHdv=G%qRXqVcTFz&JbK8Sln?WZn;N~73u7DORV{g$e|0{w*^xi_9JK}C z?zTEBV|ISS`Lc+TtZfZ(VQ5a%uJRd0dFRx-xT^|$T}FOvctFS3FR|2ksy;T>(;<$;YwmS ztjzae$3?dkIpnyO`hQ)p!eNV?IUb7f+Zf`yEo-ae5AS3NInxlclY9ts(2cEaHgB$&%sY^L+5NzHmpvd)sDZPNIBISY|bCo%R0i zkDI?ZR+k+*bBDHQ4{;{DeX8~xb>I`rBvYcg^4e|3+!#R{;hxlbcE=|DCH;2h(gi!q zcI-aCpmh7f_wyW$yQuP_q)Sxz0aM)BPZ?nDf)v%!8JDBlb1tMM3lisdU{v zzwQYIhsu#BgG#27*K&d~)^?02Gwa@3KE2Dg`znF%I zD;|w1TP!?zq_*X>?(tsZoeRdo4^r;X(`%OQtqyvYa-r$@@{lffk4J6uYM*y@8daX^ zU8k0XvltAA94a_isR#~!QKXXjI_WJJ_ZbUMDmTC51z+D8H*tEd za?1XHs?9%o^C)@DT)%-AvR95<7w6)wn>S|ez@U3hbxrk(rqCH($uaXi+rL>D*mh-c zYqe8Xv(z9x7}jr zjoI~#Ez_i@=~`wRRNS_oKJ%;&n_c8}-Ew`h!ZYJnbxXOa1ouH5rYSjh z%&g@Nls}gJ%RP9Yp8V)GFTC(5vk02LB409j=h5l; zv;XEbVpL1?q9^|AlmGHTM5&vPc%*WrTmAi&&%e2AXuN)I+a|9Wj@9e~-H7wUJC1wu z^vlOxPQN|CRq7OLwZhyM&>C`)!5A7q zfkiVRGD3nI^(>4qYDktSj^_2 zY!roo1#HPQ(3llASp01eJs2UR1vgVBnlc(78z!bPrqMDc6ZAtb=BGDBM)tuQEWIoM zKG;^w#70;gwqDQf?_r@s(*a2Dgnrk?kVG*dj0<=GLL@?A4UZ7OC@K_+2$bXlgMJXvW(ueh zOYJ`^8x&!K;_^|A5aGZWM<9lIVgU|oggg|+P!UhdCow(=#A?$3flI=SW<3VdN$D{i z$u=2u_6{52k|3o*#za}1mo3UvjMf4N88d=1q*-5fs3<)dOJg=R5rIG;0t2C39x6nH z{Fg?vNwWng(T0g|SX|WJV@rz!1Otf0Y&r!1_I419B-l)1w9%|G8dGIV+ms-iXJ2WP}gmGgV+I`wR*zn}zG$a@PYMWvl3bJn z;|NG1k1No^Vv@^&#R9E}k81=7%GcUi=tH*{wR8q%CWCZ=ERVG*Ix!lV%6XauN$AW6-u zlm3GtA_55yy!rpX5OJ=U(DDfcMiE>DLKJC00U{(!YQf5Yh&2eo`He>YXox+s|4kO9 zY}?BJLbp=(e<-`hpx;&jvftANwji*hvHP~PUReOu{h6=ciuyB$fS@;nycWN2=z2rf zYccRz#&6X1hOXCQ;I)k3sOx`5m)pxjF=+t5Wi!B$H#hxs9XKvKD7dbtRz!xnbh!C? z^QRcZ7Ms&3Iy8X}HtKDsE{6RuKoU>}MbC$9hk^L4A>P1fv?3%(<$3{EtJU%qC3Vij jPUU_&SiOI6mX8nPxtE72{dqh}qe4ojtF!+N3r^cF literal 0 HcmV?d00001 diff --git a/vol.png b/vol.png new file mode 100644 index 0000000000000000000000000000000000000000..92edd89d524c605d59d3fc0e095455de89ad26da GIT binary patch literal 4890 zcmeHKc~leU79SK*c;Hs+%9CkCWHBpANMZs)!V)!r5D>&|GBbfFSx5#5*rHakEIy&& zf{I0pJ}Z@4lv+`!@?=v{uqq-hASf;%%2A&@LFqRE70>HA?|9DZe`Zc5Gxz)5-*@lt zyWh=BYM{Tbjnz~u2!d>+ev%;YJC^*8vH)khXZ;8S4W}f^!-yax9@go#N>wxt6A3yT z#*Hc^1R0wiZ;#08bg^XHa@29wIl^<8McAwaMHF&{~&FRK~Wq0qCl#jLaDcP3%Yu^T2 zLsQ3*-SvfQrX>nPYM;0H6h|osdM0G0EZcC{5q}f* zXiI+NS+lgpi4zx2C`&7>@ZBRBl~Nq-%W*&3m%A&&_N$n$A|uB6SMj%cEbMj6y!Ohd ze4hN}_HyxsXTN=MI1EjyT7FKd6~DAe=zAJjcYAsrU7uZDdGXS$nT;vEY)VPZ z>fo254c3X(wXKQH`Qf!@Assojq4mOs#=u|^H@A5BGcW6^_2Xj;*bkPV`5VW6mKQyF zN5qP7uaW!yrQYhb^B)$wCOmBYA?w&mM@x^QdVR#UlRnc|r|jNy&KxaH>$E%ebZI5C zJ)DyE=B`yt_aQ z*U3qTlha#X6ppxGY;@Wm$}9YNNjH(~RXEa4zfYX;W#b)ROJB8P-nN~`ZXQZ-DU9p- zJ?8Z4u7D}TGLyk*xr`bv}d8&3uGboFqb z4?X(b*j1zc-aqV2-pQOw${75?Y6vn*R*A)dQnC2$ZUK8F_p5J3erM*|RxS@t4{{zI zm)V-RR}h}*Oz~fwOp%ZJ#p}n>4%?GFvp3pTRxZ?0c|Ap8vAURstK}hM%{}JY?#`XC zbYDgP{^W_w>9#N1Zk1Kvnlm>_o}+xRY4}dfu7{XcZF^|t?M3F}JfWD{np#%7z0-)g z?$|MQ%bzHO=X-DF8VH0t-_F;lwY&F zEa9iEJi8flpU~eV7!(lmgReAtX-#WX+4qewEFbD(TwL< z%|AQmCGGUu3zg&pHFA7=qwf$AgG1}4S^V`8p8~dVt_+}8&JJUN2s(KnB+th+E_wFp@4Dt zZG38-O!kgmV;EEc=s`CkIy#fapsUsNp&kapI}VTx2K1*M208c_pa0l@wkH*yiY5=n`KbX>2Dhqt)K}ry*QtM1!KfvQBJhekzGVSox@vt3axs0%)u<3?q3SqiFx%TrL@ zk)SvmHBn)xz(=cBBVajIY9tb;>ok$30a9>*XP{I>VbK`xErHPpp#%;h$^w-p*7$xv zu2SQ{1VXCGdOn&hMO{Up~XxjssPj{<+Hu1|7(lmZ_G z{#0H6H@U3dzfR#A@DvmeUY0ui{N{ldEepkbUkTJt9(&3TWCM$(&M({mL9`j<*UY%z zBMulx5>lD>$abr#79#{zE{9-XnnHMo5n`>Hd@6@b4~sYk$E%1an0#1Vv~Khy0P>JZ zJmtLe=!(gmEgoj>o)`LyqWS{TluM@GiFTmm+6>>+csH!(g`N8(Yh?K3y*Y|WjMmh} hJ|||{7ue&kAZY!8&&?D~F9m=Kl6w0~e)3xJ^&i(AK