diff options
Diffstat (limited to 'libs/music/player.d/main_static.js')
-rw-r--r-- | libs/music/player.d/main_static.js | 526 |
1 files changed, 379 insertions, 147 deletions
diff --git a/libs/music/player.d/main_static.js b/libs/music/player.d/main_static.js index 1a48297..eec68e0 100644 --- a/libs/music/player.d/main_static.js +++ b/libs/music/player.d/main_static.js @@ -1,4 +1,4 @@ -//Chris Xiong 2015-2021 +//Chris Xiong 2015-2025 //License: Expat //WARNING: This file contains profanity (thrown as exceptions). @@ -15,29 +15,57 @@ const sh={ {return document.createElement(e);}, getcookie:function(key) {return document.cookie.replace(new RegExp('(?:(?:^|.*;\\s*)'+key+'\\s*\\=\\s*([^;]*).*$)|^.*$'),'$1');}, - useFLAC:function() - {return sh.elem('audio').canPlayType('audio/ogg')=='';} + setcookie:function(key,value) + {return document.cookie=`${key}=${value};max-age=31536000`;}, }; class Ink { - constructor(_vx,_vy,_c) + constructor(_vx,_vy,_c,_d) { this.x=NSUI.canvas.width/2;this.y=NSUI.canvas.height/2; this.vx=_vx;this.vy=_vy; + this.d = _d ? 1 - _d : 0.995; this.color=_c>6?6:_c;this.active=true; } update() { const canvas=NSUI.canvas; - this.x+=this.vx;this.y+=this.vy; - this.vx*=0.995;this.vy*=0.995; - if(this.x<-10||this.x>canvas.width+10||this.y<-10||this.y>canvas.height+10) + this.x += this.vx; this.y += this.vy; + if (this.vx > 5 && this.vy > 5) { + this.vx *= this.d; this.vy *= this.d; + } + if(this.x<-30||this.x>canvas.width+30||this.y<-30||this.y>canvas.height+30 || Number.isNaN(this.x) || Number.isNaN(this.y)) this.active=false; } draw(cctx) { - cctx.drawImage(NSInk.inkimg[this.color],this.x+5*window.devicePixelRatio,this.y+5*window.devicePixelRatio); + cctx.drawImage(NSInk.inkimg[this.color], this.x - 5 * window.devicePixelRatio, this.y - 5 * window.devicePixelRatio); + } +} + +class TrendTracker +{ + constructor(_window_size, _default_value) + { + this.window_size = _window_size; + this.data = new Array(_window_size).fill(_default_value); + this.meanx = (this.window_size - 1) / 2; + } + push(v) + { + this.data.shift(); + this.data.push(v); + this.update_slope(); + } + update_slope() + { + const meanx = this.meanx; + const meany = this.data.reduce((s, v) => s + v / this.window_size, 0); + this.slope = this.data.reduce((s, v, i) => s + (i - meanx) * (v - meany), 0) / + this.data.reduce((s, _, i) => s + (i - meanx) * (i - meanx), 0); + this.intercept = meany - this.slope * meanx; + this.meanv = meany; } } @@ -47,15 +75,23 @@ NSPlayer={ current:null, shuffle:0, repeat:0, + served_formats:{'vorbis':{'mime':'audio/ogg; codecs=vorbis','disp':'ogg 224 kbps','ext':'ogg'},'flac':{'mime':'audio/flac','disp':'flac'},'opus':{'mime':'audio/ogg; codecs=opus','disp':'opus 96 kbps'},'aac':{'mime':'audio/aac','disp':'aac 192kbps','ext':'m4a'}}, + get_preferred_or_default_format:function() + { + if (sh.getcookie('preferredformat') in this.served_formats) + return sh.getcookie('preferredformat'); + for (let fmt in this.served_formats) + if (sh.elem('audio').canPlayType(this.served_formats[fmt].mime)!='') + return fmt; + }, load_playlist:async function(pln,ord) { let r=null; - const resp=await fetch(new Request(`/libs/music/player.d/playlists/${pln}.playlist?${new Date().getTime()}`)); - if(!resp.ok)throw "shit"; - r=await resp.text(); + const resp=await fetch(new Request(`/libs/music/player.d/playlists/${pln}.playlist?${new Date().getTime()}`)); + if(!resp.ok)throw "shit"; + r=await resp.text(); let rarr=r.split('\n'); let tarr=[]; - const fmt=sh.useFLAC()?'flac':'ogg'; for(let i=1;i<rarr.length;++i) { let t=rarr[i].trim(); @@ -63,7 +99,6 @@ NSPlayer={ let titem={}; titem.title=t; titem.ord=i-1; - titem.src=`//filestorage.chrisoft.org/music/${fmt}/${t}.${fmt}`; tarr.push(titem); } return ({plistname:pln,playlist:tarr,plistord:ord}); @@ -76,16 +111,18 @@ NSPlayer={ NSUI.iplaypause.style.backgroundPosition=`${NSUI.bpauserect}`; this.current=id; NSUI.lbnowplaying.innerHTML="Now Playing: "; - const a=sh.newelem("a"); - a.innerHTML=this.tracks[id].title; - a.href=`javascript:NSUI.showNotes("${this.tracks[id].title}")`; - NSUI.lbnowplaying.appendChild(a); + const a=sh.newelem("a"); + a.innerHTML=this.tracks[id].title; + a.href=`javascript:NSUI.showNotes("${this.tracks[id].title}")`; + NSUI.lbnowplaying.appendChild(a); if(navigator.mediaSession) navigator.mediaSession.metadata=new MediaMetadata({title:this.tracks[id].title,album:this.plistname}); NSUI.set_highlighted(this.plistname,this.tracks[id].title); - NSUI.audio.src=this.tracks[id].src; + const fmt=NSPlayer.get_preferred_or_default_format(); + const ext=(NSPlayer.served_formats[fmt].ext !== undefined) ? NSPlayer.served_formats[fmt].ext : fmt; + NSUI.audio.src=`//filestorage.chrisoft.org/music/${fmt}/${this.tracks[id].title}.${ext}`; NSUI.audio.load(); - NSUI.audio.play(); + return NSUI.audio.play(); }, next:function() { @@ -139,6 +176,7 @@ NSUI={ pbprev:null, ctrlcontainer:null, am3u8:null, + swformat:null, bplayrect:"0 -48px", bpauserect:"-24px -48px", brallrect:"-24px -24px", @@ -171,28 +209,52 @@ NSUI={ this.pbprev=sh.elem('pbprev'); this.ctrlcontainer=sh.elem('ctrlcontainer'); this.am3u8=sh.elem('am3u8'); + this.swformat=sh.elem('formatsw'); + if(!(sh.getcookie('preferredformat') in NSPlayer.served_formats)) + sh.setcookie('preferredformat',''); + NSUI.resize_canvas(); + try { + const ro = new ResizeObserver(() => { + NSUI.resize_canvas(); + }); + ro.observe(sh.elem("content")); + } catch(e) { + console.error(e); + } + const fmt=NSPlayer.get_preferred_or_default_format(); + const cantplay=(NSUI.audio.canPlayType(NSPlayer.served_formats[fmt].mime)=='')?' !':''; + this.swformat.innerHTML=`[${NSPlayer.served_formats[fmt].disp}${cantplay}]`; NSUI.canvas.width=NSUI.canvas.clientWidth*window.devicePixelRatio; NSUI.canvas.height=NSUI.canvas.clientHeight*window.devicePixelRatio; NSUI.vissel.onchange=function(){ if(this.value!='none'&&this.oldvalue=='none')requestAnimationFrame(NSVisualization.updateVisualization); else NSUI.canvas.getContext('2d').clearRect(0,0,NSUI.canvas.width,NSUI.canvas.height); - document.cookie='playervisualization='+this.value; + sh.setcookie('playervisualization',this.value); this.oldvalue=this.value; }; - document.getElementById('shufflesw').onclick=function(){ + sh.elem('shufflesw').onclick=function(){ NSUI.shuffle_switch(NSPlayer.shuffle=1-NSPlayer.shuffle); NSUI.ishuffle.style.backgroundPosition=`${NSPlayer.shuffle?NSUI.bshonrect:NSUI.bsoffrect}`; - document.cookie=`playershuffle=${NSPlayer.shuffle}`; + sh.setcookie('playershuffle',NSPlayer.shuffle); }; - document.getElementById('repeatsw').onclick=function(){ + sh.elem('repeatsw').onclick=function(){ NSPlayer.repeat=1-NSPlayer.repeat; NSUI.audio.loop=NSPlayer.repeat?true:false; NSUI.irepeat.style.backgroundPosition=`${NSPlayer.repeat?NSUI.bronerect:NSUI.brallrect}`; - document.cookie=`playerrepeat=${NSPlayer.repeat}`; + sh.setcookie('playerrepeat',NSPlayer.repeat); + }; + sh.elem('formatsw').onclick=function(){ + const cfmt=NSPlayer.get_preferred_or_default_format(); + fmts=Object.keys(NSPlayer.served_formats); + const nfmt=fmts[(fmts.indexOf(cfmt)+1)%fmts.length]; + sh.setcookie('preferredformat',nfmt); + const cantplay=(NSUI.audio.canPlayType(NSPlayer.served_formats[nfmt].mime)=='')?' !':''; + NSUI.swformat.innerHTML=`[${NSPlayer.served_formats[nfmt].disp}${cantplay}]`; + if(NSUI.selectedplist) + NSUI.switch_playlist(NSUI.selectedplist); }; - document.getElementById('plistsw').onclick=NSUI.togglePlist; - document.getElementById('tsliderbase').onclick= - document.getElementById('tsliderbase').onmousemove=function(e) + sh.elem('tsliderbase').onclick= + sh.elem('tsliderbase').onmousemove=function(e) { if(e.type=='click'||(e.type=='mousemove'&&e.buttons==1)) { @@ -243,23 +305,24 @@ NSUI={ return false; } if(e.key=='c') - if(NSUI.audio.audioTracks.length==2) - { - const t=NSUI.audio.currentTime; - NSUI.audio.audioTracks[0].enabled^=1; - NSUI.audio.audioTracks[1].enabled^=1; - NSUI.audio.currentTime=t; - } + return NSAudio.switchAudioTrack(); return true; }; }, + resize_canvas: function() + { + sh.elem("cvsdiv").style.width = `calc(100% - 1em - ${window.getComputedStyle(sh.elem("content")).marginLeft})`; + NSUI.canvas.width = NSUI.canvas.clientWidth * window.devicePixelRatio; + NSUI.canvas.height = NSUI.canvas.clientHeight * window.devicePixelRatio; + NSInk.inkPrepare(); + }, load_playlists:async function() { const moi=this; let r=null; - const resp=await fetch(new Request(`/libs/music/player.d/playlists/playlists?${new Date().getTime()}`)); - if(!resp.ok)throw "shit"; - r=await resp.text(); + const resp=await fetch(new Request(`/libs/music/player.d/playlists/playlists?${new Date().getTime()}`)); + if(!resp.ok)throw "shit"; + r=await resp.text(); let rarr=r.split('\n'); let tarr=[]; let cnt=0; @@ -292,10 +355,10 @@ NSUI={ const ta=NSUI.ulplaylists.childNodes[i].firstChild; if(ta.pid==this.pid) { - if(ta.classList.contains('highlighted')) - NSUI.showNotes(this.innerHTML); - ta.classList.add('highlighted');ta.classList.add('active'); - } + if(ta.classList.contains('highlighted')) + NSUI.showNotes(this.innerHTML); + ta.classList.add('highlighted');ta.classList.add('active'); + } else{ta.classList.remove('highlighted');ta.classList.remove('active');} } NSUI.present_playlist.bind(NSUI,this.pid)(); @@ -317,7 +380,9 @@ NSUI={ const l=sh.newelem('li'); const a=sh.newelem('a'); a.innerHTML=list[i].title; - a.href=list[i].src; + const fmt=NSPlayer.get_preferred_or_default_format(); + const ext=(NSPlayer.served_formats[fmt].ext !== undefined) ? NSPlayer.served_formats[fmt].ext : fmt; + a.href=`//filestorage.chrisoft.org/music/${fmt}/${list[i].title}.${ext}`; a.ord=i; a.onclick=function(e){e.preventDefault();NSUI.switch_track.bind(NSUI,this.ord)();}; l.appendChild(a); @@ -326,7 +391,7 @@ NSUI={ const d=sh.newelem('div');d.style.height=`${NSUI.ctrlcontainer.getBoundingClientRect().height+16}px`; this.playlist.appendChild(d); this.selectedplist=this.playlists[id].plistname; - this.am3u8.href=`https://chrisoft.org/libs/music/player.d/cgi-bin/m3u8.cgi?plist=${this.playlists[id].plistname}`; + this.am3u8.href=`https://chrisoft.org/libs/music/player.d/cgi-bin/m3u8.cgi?plist=${this.playlists[id].plistname}&type=${NSPlayer.get_preferred_or_default_format()}`; }, switch_playlist:function(pl,setactive) { @@ -350,7 +415,7 @@ NSUI={ { if(NSPlayer.plistname!=this.selectedplist) this.switch_playlist(this.selectedplist); - NSPlayer.play(id); + return NSPlayer.play(id); }, set_highlighted:function(pl,t) { @@ -370,7 +435,7 @@ NSUI={ for(let i=0;i<this.playlists.length;++i) { NSPlayer.sort_playlist(shuffle,this.playlists[i].playlist); - if(i==this.selectedplist)this.present_playlist(i); + if(this.playlists[i].plistname==this.selectedplist)this.present_playlist(i); } NSPlayer.sort_playlist(shuffle); }, @@ -403,27 +468,39 @@ NSUI={ document.getElementById('ctime').style.width=NSUI.audio.currentTime/NSUI.audio.duration*100+'%'; NSUI.bufferedUpdate(); }, - showNotes:async function(title) - { - const nd=sh.elem("notes"); - const nt=sh.elem("ntext"); - nt.innerHTML="Loading..." - nd.style.display="block"; - setTimeout(()=>{nd.style.opacity=1.;}); - const url=`//filestorage.chrisoft.org/music/notes/${title}.note`; - try{ - const resp=await fetch(new Request(url)); - if(!resp.ok)throw "shit"; - r=await resp.text(); - nt.innerHTML=r; - }catch(e){nt.innerHTML="This particular track doesn't seem to have a note.";} - }, - hideNotes:function(title) - { - const nd=sh.elem("notes"); - nd.style.opacity=0.; - setTimeout(()=>{nd.style.display="none";},500); - } + showNotes:async function(title) + { + const nd=sh.elem("notes"); + const nt=sh.elem("ntext"); + nt.innerHTML="Loading..." + nd.style.display="block"; + setTimeout(()=>{nd.style.opacity=1.;},5); + const url=`//filestorage.chrisoft.org/music/notes/${title}.note`; + try{ + const resp=await fetch(new Request(url)); + if(!resp.ok)throw "shit"; + r=await resp.text(); + nt.innerHTML=r; + }catch(e){nt.innerHTML="This particular track doesn't seem to have a note.";} + }, + hideNotes:function(title) + { + const nd=sh.elem("notes"); + nd.style.opacity=0.; + setTimeout(()=>{nd.style.display="none";},500); + }, + showHelp:function() + { + const hd=sh.elem("helpoverlay"); + hd.style.display="block"; + setTimeout(()=>{hd.style.opacity=1.;},5); + }, + hideHelp:function() + { + const hd=sh.elem("helpoverlay"); + hd.style.opacity=0.; + setTimeout(()=>{hd.style.display="none";},500); + } }; NSAudio={ @@ -434,22 +511,34 @@ NSAudio={ { window.AudioContext=window.AudioContext||window.webkitAudioContext||window.mozAudioContext||window.msAudioContext; if(!window.AudioContext)alert('This page requires Web Audio API to work...'); - this.audioctx=new AudioContext; - this.anlznode=this.audioctx.createAnalyser(); + if(this.audioctx===null)this.audioctx=new AudioContext; + if(this.anlznode===null)this.anlznode=this.audioctx.createAnalyser(); this.anlznode.fftSize=2*NSVisualization.nbins; NSUI.audio.volume=1; }, connect:function() { + if(this.srcnode===null) this.srcnode=this.audioctx.createMediaElementSource(NSUI.audio); this.srcnode.connect(this.anlznode); this.anlznode.connect(this.audioctx.destination); + if(this.audioctx.state!="running")throw "resume required"; }, + switchAudioTrack:function() + { + const a = NSUI.audio; + if (a.audioTracks.length != 2) return false; + const t = a.currentTime; + const e = a.audioTracks[0].enabled; + [a.audioTracks[0].enabled, a.audioTracks[1].enabled] = [!e, e]; + a.currentTime = t; + return true; + } }; NSVisualization={ nbinsp:10, - nbins:Math.pow(2,10), + nbins:Math.pow(2, 10), spectgrmw:1200, spectrw:0.7, spectrm:0.08125, @@ -457,18 +546,20 @@ NSVisualization={ cctx:null, spectgrmp:0, _61spbins:29, - _61spfintv:Math.pow(1024,1./29), - _61spbinw:9, - _61spaccel:.617274873, - _61spdamp:Math.pow(.617274873,6.17274873), + _61spbinw:5, + _61spaccel:.3617274873, + _61spdamp:0.1617174873, _61spvelcap:2*6.17274873, - _61spgvel:.025, - _61spelas:.617274873, - _61spf:6.17274873*6.17274873, - _61spelasth:.017274873, + _61spgvel:.06617274873, + _61spelas:.2617274873, + _61spf:61.7274873, + _61spelasth:.1617274873, _61spcaps:new Float32Array(29), _61spcapsv:new Float32Array(29), - _61spft:new Date(), + _61spft:performance.now(), + frms:0, + lastfpsupd:null, + fps:0, init:function() { window.requestAnimationFrame=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.msRequestAnimationFrame; @@ -501,6 +592,33 @@ NSVisualization={ } } }, + distr_log_bins:function(data, nbins, shift) + { + let ret = new Float32Array(nbins); + let binfreq = 0; + const cctx = NSVisualization.cctx; + //cctx.fillStyle=window.getComputedStyle(document.body).getPropertyValue("--principal-fg"); + const cw = NSUI.canvas.width; + for (let i = 0; i < nbins; ++i) + { + const lbin = Math.floor(binfreq); + binfreq = Math.pow(2, (i + shift) * NSVisualization.nbinsp / (nbins - 1 + shift)) - 1; + const rbin = Math.max(Math.floor(binfreq), lbin + 1); + //cctx.fillText(lbin, i / nbins * cw + 8, 60); + //cctx.fillText(rbin, i / nbins * cw + 8, 90); + const clamp0 = (x) => Math.max(0, x); + ret[i] = Math.pow(data. + slice(lbin, rbin + 1). + reduce( + (s, c) => s + + clamp0((c - NSAudio.anlznode.minDecibels) / + (NSAudio.anlznode.maxDecibels - NSAudio.anlznode.minDecibels)) / + (rbin - lbin + 1) + , 0), 2);//iS tHiS dBv^2? + //cctx.fillText(ret[i].toFixed(2), i / nbins * cw + 8, 120); + } + return ret; + }, update_61_spectrum:function() { const cctx=this.cctx,canvas=NSUI.canvas; @@ -508,29 +626,12 @@ NSVisualization={ try{ let freqdomv=new Float32Array(NSAudio.anlznode.frequencyBinCount); NSAudio.anlznode.getFloatFrequencyData(freqdomv); - let binl=Math.pow(2,(this._61spbinw-1)*this.nbinsp/(this._61spbins-1+this._61spbinw))-1; - let _61spfdomv=new Float32Array(this._61spbins); - const ft=new Date()-this._61spft; - this._61spft=new Date(); + let _61spfdomv = NSVisualization.distr_log_bins(freqdomv, this._61spbins, this._61spbinw); + const ft=performance.now() - this._61spft; + this._61spft=performance.now(); const rft=ft/1000*24; for(let i=0;i<this._61spbins;++i) { - let binr=Math.pow(2,(i+this._61spbinw)*this.nbinsp/(this._61spbins-1+this._61spbinw))-1; - if(Math.floor(binr)<=Math.floor(binl))binr=binl+1; - let binli=Math.floor(binl); - let binri=Math.floor(binr); - const clamp=(x)=>Math.max(0,Math.min(1,x)); - _61spfdomv[i]=Math.pow(freqdomv. - slice(binli,binri+1). - reduce( - (s,c)=>s+ - clamp( - (c-NSAudio.anlznode.minDecibels) - /(NSAudio.anlznode.maxDecibels-NSAudio.anlznode.minDecibels) - )/(binri-binli+1) - ,0),2);//iS tHiS dBv^2? - binl=binr; - if(ft>1000) { this._61spcaps[i]=0; @@ -538,24 +639,38 @@ NSVisualization={ continue; } let vel=this._61spgvel*this._61spcapsv[i]; - if(this._61spcaps[i]-vel*rft<=_61spfdomv[i]) + //cctx.fillStyle = this._61spcapsv[i] > 0 ? "#f00" : "#0f0"; + //cctx.fillText(Math.abs(this._61spcapsv[i]).toFixed(1), i / this._61spbins * canvas.width + 8, 120); + if (Number.isNaN(this._61spcaps[i]) || !Number.isFinite(this._61spcaps[i])) { + this._61spcaps[i] = _61spfdomv[i]; + this._61spcapsv[i] = 0; + } + if (this._61spcaps[i] - (vel < 0.5 ? vel * rft : 0) <= _61spfdomv[i]) { - let elf=0; - if(vel>this._61spgvel*this._61spvelcap) + if (!(this._61spcaps[i] < 1e-2 && this._61spcapsv[i] < 1e-2 && _61spfdomv[i] < 1e-2)) { - vel=this._61spgvel*this._61spvelcap;//cap this so that Newton could rest in peace. - this._61spcapsv[i]=this._61spvelcap; + let elf=0; + if(vel>this._61spgvel*this._61spvelcap) + { + vel=this._61spgvel*this._61spvelcap;//cap this so that Newton could rest in peace. + this._61spcapsv[i]=this._61spvelcap; + } + if(vel>rft*this._61spelasth) + elf=this._61spcapsv[i]*this._61spelas; + this._61spcapsv[i]=(this._61spcaps[i]-vel*rft-_61spfdomv[i])*this._61spf-elf; + this._61spcaps[i]=_61spfdomv[i]; + } else + { + this._61spcaps[i] = _61spfdomv[i]; + this._61spcapsv[i] = 0; } - if(vel>rft*this._61spelasth) - elf=this._61spcapsv[i]*this._61spelas; - this._61spcapsv[i]=(this._61spcaps[i]-vel*rft-_61spfdomv[i])*this._61spf-elf; - this._61spcaps[i]=_61spfdomv[i]; } else { this._61spcapsv[i]+=rft*(this._61spaccel-this._61spdamp*Math.sign(this._61spcapsv[i])*Math.pow(this._61spcapsv[i],2)); vel=this._61spgvel*this._61spcapsv[i]; - this._61spcaps[i]-=Math.min(rft*vel,this._61spcaps[i]); + this._61spcaps[i] -= Math.min(rft * vel,this._61spcaps[i]); + this._61spcaps[i] = Math.max(this._61spcaps[i], _61spfdomv[i]); } cctx.fillStyle=`rgba(70,130,180,${0.36+0.64*_61spfdomv[i]})`; @@ -613,6 +728,16 @@ NSVisualization={ }, updateVisualization:function() { + const self=NSVisualization; + const cctx=self.cctx; + ++self.frms; + if(Date.now()-self.lastfpsupd>500) + { + if(self.lastfpsupd) + self.fps=1000*self.frms/(Date.now()-self.lastfpsupd),self.frms=0; + self.lastfpsupd=Date.now(); + } + const ts=Date.now(); switch(NSUI.vissel.value) { case 'spectrum': @@ -630,7 +755,13 @@ NSVisualization={ case 'inkfountain': NSInk.update(); break; + default: + return; } + cctx.fillStyle=window.getComputedStyle(document.body).getPropertyValue("--principal-fg"); + cctx.font="2em 'CMU Typewriter Text w', 'CMU Typewriter Text', 'TeX Gyre Cursor', 'FreeMono', 'Courier New', Courier, monospace"; + const fpst=`FPS: ${self.fps.toFixed(1)}, update time ${Date.now()-ts} ms`; + cctx.fillText(fpst,0,24); if(NSUI.vissel.value!='none') requestAnimationFrame(NSVisualization.updateVisualization); } @@ -643,9 +774,13 @@ NSInk={ inkimg:[], ifcaps:[], nbinsif:128, + debug_render:false, + erms_tracker:new TrendTracker(8, 0), + freq_tracker:[], + max_droplets:1536, inkPrepare:function() { - for(let i=0;i<7;++i) + for(let i = 0; i < 7; ++i) { this.inkimg[i]=document.createElement('canvas'); this.inkimg[i].width=this.inkimg[i].height=10*window.devicePixelRatio; @@ -658,76 +793,162 @@ NSInk={ cctx.beginPath(); cctx.arc(5*window.devicePixelRatio,5*window.devicePixelRatio,5*window.devicePixelRatio,0,2*Math.PI); cctx.fill(); + this.freq_tracker[i] = new TrendTracker(8, 0); } for(let i=0;i<NSVisualization.nbins;++i)this.ifcaps[i]=0; this.nbinsif=NSVisualization.nbins/8; }, createInk:function(_vx,_vy,_c1,_c2) { - let f=false; for(let i=0;i<this.inks.length;++i) { if(!this.inks[i].active) - {this.inks[i]=new Ink(_vx,_vy,_c1,_c2);f=true;break;} + {this.inks[i]=new Ink(_vx,_vy,_c1,_c2);return;} } - if(!f&&this.inks.length<512)this.inks.push(new Ink(_vx,_vy,_c1,_c2)); + if (this.inks.length < NSInk.max_droplets) this.inks.push(new Ink(_vx, _vy, _c1, _c2)); }, - frms:0,lastfpsupd:null,fps:0, update:function() { const canvas=NSUI.canvas; - const cctx=canvas.getContext('2d'),ts=Date.now(); + const cctx=canvas.getContext('2d'); cctx.clearRect(0,0,canvas.width,canvas.height); - ++this.frms;if(Date.now()-this.lastfpsupd>500) - { - if(this.lastfpsupd) - this.fps=1000*this.frms/(Date.now()-this.lastfpsupd),this.frms=0; - this.lastfpsupd=Date.now(); - } + if (!NSAudio || !NSAudio.anlznode) return; + const origblend = cctx.globalCompositeOperation; + cctx.globalCompositeOperation = "screen"; try{ - let freqarr=new Uint8Array(NSAudio.anlznode.frequencyBinCount); - NSAudio.anlznode.getByteFrequencyData(freqarr); - for(let i=0;i<this.nbinsif;++i) + let amplarr = new Float32Array(NSAudio.anlznode.frequencyBinCount); + let freqarr = new Float32Array(NSAudio.anlznode.frequencyBinCount); + NSAudio.anlznode.getFloatTimeDomainData(amplarr); + NSAudio.anlznode.getFloatFrequencyData(freqarr); + + const rms = 10 * Math.log10(amplarr.reduce((s, a) => s += a * a / NSAudio.anlznode.frequencyBinCount, 0)); + const erms = Math.pow(Math.pow(10, rms / 10), .5); + this.erms_tracker.push(erms); + + const evals = NSVisualization.distr_log_bins(freqarr, 8, 2); + evals[6] += evals[7]; + + const clamped_lerp = function(v, imin, imax, omin, omax) { + if (v <= imin) return omin; + if (v >= imax) return omax; + const f = (v - imin) / (imax - imin); + return f * (omax - omin) + omin; + }; + + let base_emission_modifier = 1; + if (this.erms_tracker.slope < -0.015) + base_emission_modifier = clamped_lerp(this.erms_tracker.slope, -0.03, -0.015, 0.3, 1); + else if (this.erms_tracker.slope > 0.01) + base_emission_modifier = clamped_lerp(this.erms_tracker.slope, 0.01, 0.1, 1, 5); + const base_emission_factor = Math.max(1, Math.pow(erms * 8, 2)); + const base_velocity_factor = clamped_lerp(erms, 0.05, 0.4, 12, 80); + const base_veldamp_factor = clamped_lerp(erms, 0.1, 0.4, 0.005, 0.001); + + const freq_emission_factors = []; + const freq_emission_modifiers = []; + const freq_velocity_shift = []; + const freq_velocity_variances = []; + const actual_emission = []; + for(let i = 0; i < 7; ++i) { - let r=0; - for(let j=0;j<8;++j)r+=freqarr[i*8+j]; - r/=8.; - if(r-this.ifcaps[i]>7) + this.freq_tracker[i].push(evals[i]); + let r = evals[i] * 255; + + freq_emission_modifiers[i] = 1; + if (this.freq_tracker[i].slope < -0.01) + freq_emission_modifiers[i] = Math.pow(clamped_lerp(this.freq_tracker[i].slope, -0.03, -0.01, 0, 1), 2); + else if (this.freq_tracker[i].slope > 0.005) + freq_emission_modifiers[i] = clamped_lerp(this.freq_tracker[i].slope, 0.005, 0.05, 1, 5); + freq_emission_factors[i] = Math.pow(evals[i] * (i > 1 ? (i > 4 ? 20 : 10) : 7), 2) / 10; + freq_velocity_shift[i] = Math.pow(clamped_lerp(this.freq_tracker[i].slope, 0.005, 0.03, 4, 8), 1.5); + freq_velocity_variances[i] = Math.min(base_velocity_factor * 0.5, Math.pow(evals[i] * 10, 0.5)); + + actual_emission[i] = 0; + const sustained1 = Math.abs(this.freq_tracker[i].slope) < 0.00125 && (evals[i] * (i > 4 ? 2 : 1)) > 0.5; + const sustained2 = Math.abs(this.freq_tracker[i].slope) < 0.00075 && (evals[i] * (i > 4 ? 2 : 1)) > 0.3; + let emit = false; + emit |= (r - this.ifcaps[i] > 5); + emit |= (sustained1 || sustained2); + if (sustained1) freq_emission_factors[i] /= 5; + if (sustained2) freq_emission_factors[i] /= 10; + if (emit) { - let color=Math.floor(i*8.0/this.nbinsif),rad=Math.random()*Math.PI*2; - let ndrops=(128-i)/128.*3+1;ndrops*=(r/128.);ndrops=Math.floor(ndrops);if(ndrops<1)ndrops=1; - for(let k=0;k<ndrops;++k) + actual_emission[i] = base_emission_factor * base_emission_modifier * freq_emission_factors[i] * freq_emission_modifiers[i]; + const emission = actual_emission[i] < 0.075 ? 0 : Math.min(Math.max(Math.floor(actual_emission[i]), 1), 24); + for(let k = 0; k < emission; ++k) { - this.createInk(((r-this.ifcaps[i]-7)/32*24+12)*Math.cos(rad),((r-this.ifcaps[i]-7)/32*24+12)*Math.sin(rad),color); - rad=Math.random()*Math.PI*2; + const rad = Math.random() * Math.PI * 2; + const vel = (base_velocity_factor + freq_velocity_shift[i]) * 0.25 + (Math.random() * 2 - 1) * freq_velocity_variances[i]; + this.createInk(vel * Math.cos(rad), vel * Math.sin(rad), i, base_veldamp_factor); + } + } + if (r > this.ifcaps[i]) this.ifcaps[i] = r; else this.ifcaps[i] -= 3; + } + for (let i = 0; i < 7; ++i) + { + const multi = [1, 1, 1.1, 1.25, 1.65, 2.5, 2.15]; + const v = Math.pow(Math.min(evals[i] * multi[i], 1), 2); + const r = 10 + 110 * v; + cctx.fillStyle = `${NSInk.ic2[i].slice(0, -4)}${0.3 + 0.5 * v})`; + cctx.beginPath(); + cctx.ellipse(NSUI.canvas.width / 2, NSUI.canvas.height / 2, r, r, 0, 0, 2 * Math.PI); + cctx.fill(); + } + if (this.debug_render) { + const debug_render_bars = function(data, n, max, bipolar, style) { + const cw = NSUI.canvas.width; + const ch = NSUI.canvas.height; + for (let i = 0; i < n; ++i) { + cctx.fillStyle = Array.isArray(style) ? style[i] : style; + const h = Math.min(Math.max(-data[i] / max, -1), 1); + cctx.fillRect(i * (cw / n), bipolar ? ch / 2 : ch, cw / n * 0.96, h * ch * 0.6); } } - if(r>this.ifcaps[i])this.ifcaps[i]=r;else this.ifcaps[i]-=2; + debug_render_bars(evals, 7, 1, false, NSInk.ic1); + const fsl = []; + for (let i = 0; i < 7; ++i) fsl.push(this.freq_tracker[i].slope); + debug_render_bars(fsl, 7, 0.1, true, NSInk.ic2); + cctx.fillStyle = "rgba(70,130,180)"; + cctx.fillRect(0, 0, erms * NSUI.canvas.width, 10); + cctx.fillRect(NSUI.canvas.width / 2, 10, 10 * this.erms_tracker.slope * NSUI.canvas.width / 2, 10); + cctx.fillStyle=window.getComputedStyle(document.body).getPropertyValue("--principal-fg"); + cctx.font="2em 'CMU Typewriter Text w', 'CMU Typewriter Text', 'TeX Gyre Cursor', 'FreeMono', 'Courier New', Courier, monospace"; + cctx.fillText(`${erms.toFixed(4).padStart(7)} ${this.erms_tracker.slope.toFixed(6).padStart(9)}`,0,90); + cctx.fillText(`${base_emission_factor.toFixed(3).padStart(6)} ${base_emission_modifier.toFixed(3).padStart(6)}`,0,120); + for (let i = 0; i < 7; ++i) { + cctx.fillStyle = NSInk.ic1[i]; + cctx.fillText(evals[i].toFixed(2), i / 7 * NSUI.canvas.width + 8, 180); + cctx.fillText(this.freq_tracker[i].slope.toFixed(4).padStart(7), i / 7 * NSUI.canvas.width + 8, 210); + cctx.fillText(actual_emission[i].toFixed(4).padStart(7), i / 7 * NSUI.canvas.width + 8, 240); + } } }catch(e){ cctx.clearRect(0,0,canvas.width,canvas.height); + console.error(e); } - const tu=Date.now(); let aa=0; for(let i=0;i<this.inks.length;++i) - if(this.inks[i].active){this.inks[i].update();this.inks[i].draw(cctx);++aa;} - cctx.fillStyle='#000'; - cctx.font="1em 'CMU Typewriter Text w', 'CMU Typewriter Text', 'TeX Gyre Cursor', 'FreeMono', 'Courier New', Courier, monospace"; - cctx.fillText('Active droplets '+aa+', '+this.fps.toFixed(2)+' FPS',0,10); - cctx.fillText('Update time '+(tu-ts)+'ms, render time '+(Date.now()-tu)+'ms',0,24); + if(this.inks[i].active){this.inks[i].draw(cctx);this.inks[i].update();++aa;} + cctx.globalCompositeOperation = origblend; + cctx.fillStyle=window.getComputedStyle(document.body).getPropertyValue("--principal-fg"); + cctx.font="2em 'CMU Typewriter Text w', 'CMU Typewriter Text', 'TeX Gyre Cursor', 'FreeMono', 'Courier New', Courier, monospace"; + cctx.fillText(`Active droplets ${aa}`,0,52); } }; function init() { if(!window.devicePixelRatio)window.devicePixelRatio=1; - loadTheme(); + loadTheme(); NSUI.setup_ui(); NSUI.load_playlists() .then(()=>{ sh.elem('overlaytext').innerHTML+="Done!<br>Click or tap anywhere to start." sh.elem('overlay').onclick=function(){ - NSAudio.audioInit();NSAudio.connect(); + let initerr=0; + try{ + NSAudio.audioInit();NSAudio.connect(); + }catch(e){initerr=1;console.log(e);if(NSAudio.audioctx!==null)NSAudio.audioctx.resume();} if(window.location.hash.length) { let p=window.location.hash.substr(1).split('/'); @@ -735,19 +956,29 @@ function init() NSUI.switch_playlist(p[0],true); let id=0; for(;id<NSPlayer.tracks.length&&NSPlayer.tracks[id].title!=p[1];++id); - if(id<NSPlayer.tracks.length)NSUI.switch_track(id); + if(id<NSPlayer.tracks.length) + { + const p=NSUI.switch_track(id); + if(p!=undefined) + p.then(_=>{sh.elem('overlay').style.display='none';}).catch(e=>{}); + else + //this browser is from an era before this autoplay policy mess, + //assume it succeeded + sh.elem('overlay').style.display='none'; + } } - sh.elem('overlay').style.display='none'; + else if(!initerr)sh.elem('overlay').style.display='none'; } - sh.elem('overlay').onclick(); + sh.elem('overlay').onclick(); }, - ()=>{ - sh.elem('overlaytext').innerHTML+="Failed...<br>Maybe try refreshing the page?" - }); + ()=>{ + sh.elem('overlaytext').innerHTML+="Failed...<br>Maybe try refreshing the page?" + }); NSVisualization.init();NSInk.inkPrepare(); NSUI.audio.ontimeupdate=NSUI.timeUpdate; NSUI.audio.onended=NSPlayer.next.bind(NSPlayer); NSUI.audio.onplay=NSUI.audio.ondurationchange=function(){ + if(!NSUI.audio.audioTracks)return; if(NSUI.audio.audioTracks.length==2) sh.elem('mt').style.display='inline'; else @@ -777,3 +1008,4 @@ function init() NSUI.vissel.value=sh.getcookie('playervisualization'); requestAnimationFrame(NSVisualization.updateVisualization); } +document.addEventListener("DOMContentLoaded", init); |