// Radio Station Emulator // // Kaotisk Hund - 2024 // // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL v3.0 // // A simple co-mechanism to pretend playing a live radio as it would happen for // a radio station that mostly plays prerecoded shows. // // Client side implementation // Let's remind here the structures we are waiting for: // 1. Hash // 2. 0 // 3. audio/ogg file // 4. application/json file // // 1. Hash // Can be an SHA512sum or SHA256sum, we don't do checks, we only ask the hash // accompanied by what do we think it is. // // 2. 0 // When nothing exists on the radio station we are visiting. // // 3. audio/ogg file // An audio file to play. // // 4. application/json file // Could be one of the following: // - list // - show_info // var debugMode = true; const apiURL = "http://z.kaotisk-hund.com:8010/"; const version = "v0"; const listRequest = `${apiURL}${version}/list` const jsonRequest = `${apiURL}${version}/application/json/` const audioRequest = `${apiURL}${version}/audio/ogg/` const documentTitle = "Radio Station Emulator"; const separator = " :: "; var audioElement = document.querySelector('audio'); var placeholders = { currentTime: document.querySelector('.current-time'), listStarted: document.querySelector('.started-on'), currentShowHash : document.querySelector('.current-show-hash'), listHash : document.querySelector('.list-hash'), artist: document.querySelector('.artist'), title: document.querySelector('.title'), radioPlayer: document.querySelector('.radio-player'), youAreHere : document.querySelector('.you-are-here'), playProgress: document.querySelector('.play-progress'), startsOn: document.querySelector('.starts-on'), showDuration: document.querySelectorAll('.show-duration'), relativeTime: document.querySelector('.relative-time'), listeningAt: document.querySelector('.listening-at'), dlProgress: document.querySelector('.dl-progress'), minTimesPlayed: document.querySelector('.min-played'), maxTimesPlayed: document.querySelector('.max-played'), listDuration: document.querySelector('.list-duration'), previouslyPlayed: document.querySelector('.previously-played'), showInfo: document.querySelector('.show-info'), listInfo: document.querySelector('.list-info'), dt: document.querySelector('.d-t'), deltaTime: document.querySelector('.delta-time'), timeOfVisit: document.querySelector('.time-of-visit') } var values = { delta_time: 0, min_times_played: 0, maxTimesPlayed: 0, Dt: 0, now_on_sequence: 0, seconds_here: 0, current_time: 0 } function debugLog(message) { if ( debugMode ) console.log(message); } function setTitleSyncing(){ document.title = "🔃Syncing" + separator + documentTitle; } function setTitleMessage(message){ document.title = message + separator + documentTitle; } function updateComponentsAfterIncreaseSeconds() { placeholders.youAreHere.innerText = values.seconds_here; placeholders.playProgress.value = values.current_time + values.seconds_here; placeholders.relativeTime.innerText = values.current_time + values.seconds_here; placeholders.listeningAt.innerText = Math.floor(audioElement.currentTime); } function increaseSeconds() { values.seconds_here = values.seconds_here + 1; updateComponentsAfterIncreaseSeconds(); return values.seconds_here; } function getSecondsHere() { return values.seconds_here; } setInterval(increaseSeconds, 1000); function FetchJSON( url, callback, params ) { setTitleSyncing(); const request = new XMLHttpRequest(); request.addEventListener("load", ()=>{ var json = JSON.parse(request.response); if(request.status !== 404){ callback(json, params); } else { debugLog(`ERROR ${request.status} while loading ${url}`); } }); request.addEventListener("error", ()=>{ debugLog("An error occured. While loading: "+url+" for "+callback+"."); }); request.addEventListener("abort", ()=>{ debugLog("Request aborted: "+url+" for "+callback+"."); }); request.open("GET", url); request.send(); } function FetchAudio(url, callback) { const request = new XMLHttpRequest(); request.responseType = 'blob'; request.addEventListener("load", ()=>{ if(request.status === 200){ debugLog("Got it... trying!") placeholders.dlProgress.value = 100; audioElement.src = URL.createObjectURL(request.response); callback(); // audioElement.play(); debugLog("Tried... did it work?"); } else { debugLog(`ERROR ${request.status} while loading ${url}`); } }); request.addEventListener("progress", (event)=>{ if (event.lengthComputable) { debugLog(`Fetching: ${event.total}`); placeholders.dlProgress.value = (event.loaded/event.total)*100; } }); request.addEventListener("error", ()=>{ debugLog("An error occured. While loading: "+url); }); request.addEventListener("abort", ()=>{ debugLog("Request aborted: "+url); }); request.open("GET", url); request.send(); } function genericCallback(json, params) { debugLog('genericCallback'); debugLog(json); debugLog(params); } var calledLoadShowCallback = 0; function stopHereAndReflect() { return 0; } function updateComponentsAfterLoadShowCallback(json, listItem) { setTitleMessage ( "▶️ " + json.artist + " - " + json.title ); placeholders.currentShowHash.innerText = listItem.hash; placeholders.artist.innerText = json.artist; placeholders.title.innerText = json.title; placeholders.showDuration.forEach((element)=>{element.innerText = Math.floor(listItem.duration/1000)}); placeholders.startsOn.innerText = listItem.starts_on; placeholders.playProgress.max = Math.floor(listItem.duration/1000); placeholders.showInfo.innerText = JSON.stringify(json, null, 2); placeholders.currentTime.innerText = values.current_time; } function loadShowCallback(json, params) { const [ list, now_on_sequence, listItem, hash_of_list ] = params; debugLog('loadShowCallback'); debugLog(json); debugLog(listItem); //debugLog(params); audioElement.load(); FetchAudio(`${audioRequest}${json.hash}`, sync_radio); audioElement.type = json.mimetype; values.current_time = Math.floor((values.now_on_sequence/1000)); // - listItem.starts_on)/1000); updateComponentsAfterLoadShowCallback(json, listItem); audioElement.addEventListener('ended', function(){ values.current_time = 0; values.seconds_here = 0; placeholders.currentTime.value = 0; FetchJSON(`${listRequest}`, hashCallback, [ new Date().getTime() ]); }); } function preciseIncreaseVolume(value) { if ( value <= 1 ) audioElement.volume = value; } function fadeInAudio() { var timeSpan = 1000; var timeStep = 100; for ( var i = 0; i <= 90; i++ ) { timeSpan = timeSpan + timeStep; var newVolume = Math.sin(i*(Math.PI/180)); setTimeout( preciseIncreaseVolume(newVolume), timeSpan); } } function sync_radio() { var new_now = values.current_time + getSecondsHere(); debugLog("Trying to sync @ "+ values.current_time + " + " + getSecondsHere() + " = " + new_now); audioElement.currentTime = new_now; audioElement.play(); audioElement.muted = false; audioElement.volume = 0; fadeInAudio(); return new_now; } function syncOnDOMfromListCallback(now, hash_of_list, json) { placeholders.timeOfVisit.innerText = now; placeholders.listStarted.innerText = json.started_on; placeholders.listDuration.innerText = json.duration; placeholders.listHash.innerText = hash_of_list; placeholders.deltaTime.innerText = values.delta_time; placeholders.minTimesPlayed.innerText = values.min_times_played; placeholders.maxTimesPlayed.innerText = values.max_times_to_be_played; placeholders.dt.innerText = values.Dt; placeholders.currentTime.innerText = values.now_on_sequence; placeholders.listInfo.innerText = JSON.stringify(json, null, 2); } function infoShowCallback(json, params) { var [ hash ] = params; if ( document.querySelector(`#t-${hash}`) === null ) { var parent = document.querySelector(`#d-${hash}`); var tableElement = document.createElement('table'); tableElement.id = `t-${hash}`; var atr = document.createElement('tr'); var artistth = document.createElement('th'); artistth.innerText = "Artist"; var artistElement = document.createElement('td'); artistElement.innerText = `${json.artist}`; atr.appendChild(artistth); atr.appendChild(artistElement); tableElement.appendChild(atr); var ttr = document.createElement('tr'); var titleth = document.createElement('th'); titleth.innerText = "Title"; var titleElement = document.createElement('td'); titleElement.innerText = `${json.title}`; ttr.appendChild(titleth); ttr.appendChild(titleElement); tableElement.appendChild(ttr); parent.appendChild(tableElement); } } function appendPreviouslyPlayedShows(listItem){ if ( document.querySelector(`#s-${listItem.hash}`) === null ) { var tmp = document.createElement('div'); var tmpp = document.createElement('pre'); // tmp.style = 'flex-direction:row-reverse;'; tmp.style = 'flex-direction:row;align-items:center;'; tmp.id = `d-${listItem.hash}`; // tmpp.id = `s-${listItem.hash}`; // tmpp.innerText = JSON.stringify(listItem, null, 2); var tmpc = document.createElement('img'); tmpc.id = `i-${listItem.hash}`; // tmp.append(tmpp); tmp.append(tmpc); placeholders.previouslyPlayed.append(tmp); generateImage(listItem.hash, 'new'); FetchJSON(`${jsonRequest}${listItem.hash}`, infoShowCallback, [listItem.hash]); } } function listCallback(json, params) { debugLog('listCallback'); debugLog(json); debugLog(params); var [ now, hash_of_list ] = params; values.delta_time = now - json.started_on; values.min_times_played = Math.floor( values.delta_time / json.duration ); values.max_times_to_be_played = values.delta_time / json.duration; values.Dt = values.max_times_to_be_played - values.min_times_played; values.now_on_sequence = values.Dt * json.duration; syncOnDOMfromListCallback(now, hash_of_list, json); debugLog(`now_on_sequence: ${values.now_on_sequence}, Dt: ${values.Dt}`) debugLog(json.list.map((item, index)=>({index, value: item})).sort((a,b)=>{return b.value.index - a.value.index})); if ( json.list.length === 1 ) { FetchJSON(`${jsonRequest}${json.list[0].hash}`, loadShowCallback, [json, values.now_on_sequence, json.list[0], hash_of_list]); } else { for ( var i = 0; i < json.list.length - 1; i++) { if( i !== 0 ) appendPreviouslyPlayedShows(json.list[i-1]); debugLog("getting there " + i) debugLog(`${json.list[i].starts_on} < ${values.now_on_sequence} < ${json.list[i+1].starts_on}`); if ( json.list[i].starts_on < values.now_on_sequence && values.now_on_sequence < json.list[i+1].starts_on ) { values.now_on_sequence = values.now_on_sequence - json.list[i].starts_on; debugLog(`now_on_sequence (1updated): ${values.now_on_sequence}`); FetchJSON(`${jsonRequest}${json.list[i].hash}`, loadShowCallback, [json, values.now_on_sequence, json.list[i], hash_of_list]); generateImage(json.list[i].hash); debugLog('First!'); break; } else if ( values.now_on_sequence > json.list[i+1].starts_on && i === json.list.length - 2 ) { if( i !== 0 ) appendPreviouslyPlayedShows(json.list[i]); values.now_on_sequence = values.now_on_sequence - json.list[i+1].starts_on; FetchJSON(`${jsonRequest}${json.list[i+1].hash}`, loadShowCallback, [json, values.now_on_sequence, json.list[i+1], hash_of_list]); generateImage(json.list[i].hash); debugLog('Second!'); break; } else { debugLog(`We are here: ${i}`); debugLog(values.now_on_sequence); debugLog(json.list[i].starts_on); if (i > 0) debugLog(json.list[i-1].starts_on); debugLog('Nothing!'); } } } } function hashCallback(json, params) { var [ now ] = params; debugLog('hashCallback'); FetchJSON(`${jsonRequest}${json.latest_list}`, listCallback, [now, json.latest_list]); } FetchJSON(`${listRequest}`, hashCallback, [ new Date().getTime() ]); function generateImage(hash, what) { var pixels = []; var yi = 0; var xi = 0; for ( var y = 0; y < 256; ++y ){ pixels[y] = []; for ( var x = 0; x < 256; ++x ) { pixels[y][x] = '#000'; } } var z = 1; for ( var y = 0; y < 256; ++y ) { yi = y - 128; for ( var x = 0; x < 256; ++x ) { var pixel; xi = x - 128; z=Math.abs(Math.floor((yi*yi+xi*xi)/3)); //z=Math.abs(Math.floor((xi*yi)/1)); var r1 = hash[z] === undefined ?'0':hash[z]; var r2 = hash[z+1] === undefined ?'0':hash[z+1]; var g1 = hash[z+2] === undefined ?'0':hash[z+2]; var g2 = hash[z+3] === undefined ?'0':hash[z+3]; var b1 = hash[z+4] === undefined ?'0':hash[z+4]; var b2 = hash[z+5] === undefined ?'0':hash[z+5]; var t1 = hash[z+6] === undefined ?'0':hash[z+6]; var t2 = hash[z+7] === undefined ?'0':hash[z+7]; if (t2 === undefined) { pixel = "#000"; } else { // pixel = `#${r1}${r2}${g1}${g2}${b1}${b2}`; pixel = `#${r1}${r2}${g1}${g2}${b1}${b2}${t1}${t2}`; //pixel = `#${r1}${r1}${r1}${r1}${r1}${r1}${r1}${r1}`; //pixel = `#${r1}${r1}${r1}${r1}${r1}${r1}`; //pixel = `#${r1}${g1}${b1}`; } if ( 0 >= xi && 0 >= yi ) { // //z=Math.abs(Math.floor((yi*xi)/4)); // //if ( z > 120 ) z = z-120; // //pixel = `#${hash[z-113]}${hash[z-114]}${hash[z-115]}${hash[z-116]}${hash[z-117]}${hash[z-118]}${hash[z-119]}${hash[z-120]}`; // pixel = `#${hash[z]}${hash[z+1]}${hash[z+2]}${hash[z+3]}${hash[z+4]}${hash[z+5]}${hash[z+6]}${hash[z+7]}`; // //pixel = '#2a2' } else if ( 0 <= xi && 0 >= yi ) { //pixel = `#${hash[z]}${hash[z+1]}${hash[z+2]}${hash[z+3]}${hash[z+4]}${hash[z+5]}`; //${hash[z+6]}${hash[z+7]}`; // //z=Math.abs(Math.floor((yi*xi)/128)); // pixel = `#${hash[z]}${hash[z+1]}${hash[z+2]}${hash[z+3]}${hash[z+4]}${hash[z+5]}${hash[z+6]}${hash[z+7]}`; // // pixel = '#000'; // //pixel = '#22a'; } else if ( 0 >= xi && 0 <= yi ) { //pixel = `#${hash[z]}${hash[z+1]}${hash[z+2]}${hash[z+3]}${hash[z+4]}${hash[z+5]}`; //${hash[z+6]}${hash[z+7]}`; // z=Math.abs(Math.floor((yi*xi)/128)); // // pixel = '#000'; // pixel = '#a22'; } else if ( 0 <= xi && 0 <= yi ) { //pixel = `#${hash[z]}${hash[z+1]}${hash[z+2]}${hash[z+3]}${hash[z+4]}${hash[z+5]}`; //${hash[z+6]}${hash[z+7]}`; // z=Math.abs(Math.floor((yi*xi)/128)); // pixel = '#000'; } pixels[y][x] = pixel; } } createImage( pixels, hash, what ); } function createImage(pixels, hash, what) { //debugLog(pixels); canvas = document.createElement('canvas'); //canvas.width = pixels[0].length; //canvas.height = pixels.length; var output_x_y = 256; canvas.width = output_x_y; canvas.height = output_x_y; var context = canvas.getContext('2d'); var zoom_factor = 5; var zoom_size = Math.floor(output_x_y/zoom_factor); var radius = Math.floor(output_x_y/zoom_size); debugLog(`Radius: ${radius}`); for ( var y = 0; y < zoom_size; ++y ) { for ( var x = 0; x < zoom_size; ++x ) { var fy = (canvas.height-zoom_size)/2; var fx = (canvas.width-zoom_size)/2; var ax = Math.floor(x+fx+1); var ay = Math.floor(y+fy+1); //debugLog(`${ax} + ${ay}`); var pixel_to_be_enhanced = pixels[ay][ax]; var c = 0; var r = 0; for (var i = 0; i < radius; ++i) { for (var k = 0; k < radius; ++k) { var subpixel = pixel_to_be_enhanced; if ( i > radius - Math.floor(radius/2) ) { debugLog(i); } c = (x*radius)+k; r = (y*radius)+i; context.fillStyle = subpixel; context.fillRect(c, r, radius, radius); context.stroke(); //debugLog(`${x} -> ${c}, ${y} -> ${r}`); } } } } if ( what !== 'new' ) { document.querySelector('.generated').src = canvas.toDataURL('image/png'); } else { document.querySelector(`#i-${hash}`).src = canvas.toDataURL('image/png'); document.querySelector(`#i-${hash}`).width = 128; } } // @license-end