summaryrefslogblamecommitdiff
path: root/libs/music/player.d/main_static.js
blob: bc11c0a8d4c278e4010714de39ae6c906f45a848 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
                       
                
                                                               







                         




                                         
                                                                                                             

                                                                 



         
                              


                                                               
                                     




                                              




                                                                                                                                   



                              
                                                                                                                             
     

 
























                                                                                       
          




                   
                                                                                                                                                                                                                                                              







                                                                                


                                         


                                                                                                                      

                               






                                      










                                                                                                   
                                                    



                                                                       


                                                                                                                  
                                                             

                                                                                                            
                          
                                 

































                                                                            


      















                       
                  































                                                                                  


                                                                         


                                                 





                                           


                                                                                               




                                                                                                                    
                                                           

                                     
                                                

                                                                                                       
                                                           
          
                                               


                                                                                                     










                                                                                                    
          

                                                      

















































                                                                                         
                                                  


                        






                                                                                                                       



                                   


                                                                                                                












                                                            
                                          
















                                                                       
                     



                                                                                   




















                                                                                                 
                                                                 

                                                                                                                







                                                                                                             
                                                                                                                                                                             






















                                                                                     
                                 


















                                                                               
                                                                                        






























                                                                                                                        
      
































                                                                                     


         






                                                                                                                          

                                                                             




                                                      
                               


                                                                        
                                                                  
      









                                                                       


                 
              
                          






                    


                           
                             
                           
                          
                      
                            

                                    



                              































                                                                                                                                                                       


























                                                                                                   






                                                                              


                                                                                                      
                                 

                                             





                                         
                                                          






                                                                                                                     
                 
                                                                                                         
                     













                                                                                                              
                     



                                                                                                                                          
                                                          

                                                                                   
                 
 

                                                                                                                                                                                                
 













                                                                                                                                                                
                                                   























                                                                                                                   
                                                                                               











                                                                                                                                                                       









                                                                             
















                                                     

                       
         



                                                                                                                                         


                                                                   


       





                                                                                                                                                                   


                                        
                      

                         
                                  











                                                                                                                
                                                          





                                                                 


                                           
                                                           
         
                                                                                               
      


                                 
                                           
                                                       


                                                        
            

































                                                                                                                      
             












                                                                                                                           







                                                                                                                        
                 
                                                                                                                                               

                                                                                                                                
                     

                                                                                                                                                  
                                                                                                         

                     

                                                                                     

                                       
                                                                 






                                                                                                     
























                                                                                                                                                 
                                                                                                                 
                 


                                                           
                             
         

                                           
                                                                                    
                                                  


                                                                                                                                         
     



               
                                                          
                




                                                                                    
                          
                
                                                      
                                                                                                     







                                                                                      
                 






                                                                                           
                 
             
                                                                     
         
                                     
      


                                                                                       



                                                             
                                          



























                                                                                               
 
                                                    
//Chris Xiong 2015-2025
//License: Expat
//WARNING: This file contains profanity (thrown as exceptions).

let NSInk=null;
let NSAudio=null;
let NSVisualization=null;
let NSPlayer=null;
let NSUI=null;

const sh={
    elem:function(e)
    {return document.getElementById(e);},
    newelem:function(e)
    {return document.createElement(e);},
    getcookie:function(key)
    {return document.cookie.replace(new RegExp('(?:(?:^|.*;\\s*)'+key+'\\s*\\=\\s*([^;]*).*$)|^.*$'),'$1');},
    setcookie:function(key,value)
    {return document.cookie=`${key}=${value};max-age=31536000`;},
};

class Ink
{
    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;
        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);
    }
}

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;
    }
}

NSPlayer={
    plistname:null,
    tracks:null,
    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'},'m4a':{'mime':'audio/aac','disp':'aac 192kbps'}},
    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();
        let rarr=r.split('\n');
        let tarr=[];
        for(let i=1;i<rarr.length;++i)
        {
            let t=rarr[i].trim();
            if(!t.length)continue;
            let titem={};
            titem.title=t;
            titem.ord=i-1;
            tarr.push(titem);
        }
        return ({plistname:pln,playlist:tarr,plistord:ord});
    },
    play:function(id)
    {
        if(!this.plistname)return;
        window.history.replaceState('','The Stupid Online Player',
            `#${encodeURIComponent(this.plistname)}/${encodeURIComponent(this.tracks[id].title)}`);
        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);
        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);
        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();
        return NSUI.audio.play();
    },
    next:function()
    {
        this.current=(this.current+1)%this.tracks.length;
        if(this.current==0&&this.shuffle)
            NSUI.switch_playlist(this.plistname);
        this.play(this.current);
    },
    prev:function()
    {
        this.current=(this.current-1+this.tracks.length)%this.tracks.length;
        this.play(this.current);
    },
    switch_playlist:function(plistname,plist)
    {
        this.plistname=plistname;
        this.tracks=plist;
    },
    sort_playlist:function(shuffle,plist)
    {
        const tarr=plist?plist:this.tracks;
        if(!tarr)return;
        if(shuffle)
        for(let i=0;i<tarr.length;++i)
            tarr[i].sord=Math.random()*tarr.length;
        tarr.sort(
            (a,b)=>{
                const ra=shuffle?a.sord:a.ord;
                const rb=shuffle?b.sord:b.ord;
                return ra<rb?-1:1;
            }
        );
        if(plist)plist=tarr;else this.tracks=tarr;
    }
};

NSUI={
    playlist:null,
    audio:null,
    canvas:null,
    iplaypause:null,
    irepeat:null,
    ishuffle:null,
    lbnowplaying:null,
    playlists:null,
    ulplaylists:null,
    swplaylist:null,
    selectedplist:null,
    vissel:null,
    pbnext:null,
    pbprev:null,
    ctrlcontainer:null,
    am3u8:null,
    swformat:null,
    bplayrect:"0 -48px",
    bpauserect:"-24px -48px",
    brallrect:"-24px -24px",
    brnonrect:"-48px -24px",
    bronerect:"0 0",
    bsoffrect:"-24px 0",
    bshonrect:"-48px 0",
    plistshown:true,
    setup_ui:function()
    {
        window.devicePixelRatio=window.devicePixelRatio?window.devicePixelRatio:1;
        window.onresize=function()
        {
            if(window.innerWidth<768)
            setupevents();
            else unsetevents();
        };
        window.onresize();
        this.playlist=sh.elem('playlist');
        this.swplaylist=sh.elem('plistsw');
        this.audio=sh.elem('audio');
        this.canvas=sh.elem('cvs');
        this.iplaypause=sh.elem('imgplaypause');
        this.irepeat=sh.elem('imgrepeat');
        this.ishuffle=sh.elem('imgshuffle');
        this.lbnowplaying=sh.elem('nowplaying');
        this.ulplaylists=sh.elem('plists');
        this.vissel=sh.elem('visualizationsel');
        this.pbnext=sh.elem('pbnext');
        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);
            sh.setcookie('playervisualization',this.value);
            this.oldvalue=this.value;
        };
        sh.elem('shufflesw').onclick=function(){
            NSUI.shuffle_switch(NSPlayer.shuffle=1-NSPlayer.shuffle);
            NSUI.ishuffle.style.backgroundPosition=`${NSPlayer.shuffle?NSUI.bshonrect:NSUI.bsoffrect}`;
            sh.setcookie('playershuffle',NSPlayer.shuffle);
        };
        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}`;
            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);
        };
        sh.elem('tsliderbase').onclick=
        sh.elem('tsliderbase').onmousemove=function(e)
        {
            if(e.type=='click'||(e.type=='mousemove'&&e.buttons==1))
            {
                const sr=this.getBoundingClientRect();
                const p=(e.clientX-sr.left)/sr.width;
                NSUI.audio.currentTime=NSUI.audio.duration*p;
            }
        };
        const playpausef=function()
        {
            if(NSUI.audio.src=='')
            {
                if(NSUI.selectedplist)
                {
                    NSUI.switch_playlist(NSUI.selectedplist);
                    NSUI.switch_track(0);
                }
                return;
            }
            if(NSUI.audio.paused)
            {
                NSUI.audio.play();
                NSUI.iplaypause.style.backgroundPosition=`${NSUI.bpauserect}`;
            }
            else
            {
                NSUI.audio.pause();
                NSUI.iplaypause.style.backgroundPosition=`${NSUI.bplayrect}`;
            }
        };
        NSUI.iplaypause.onclick=playpausef;
        NSUI.pbprev.onclick=function()
        {
            if(NSUI.audio.currentTime>10)NSUI.audio.currentTime=0;
            else NSPlayer.prev();
        };
        NSUI.pbnext.onclick=NSPlayer.next.bind(NSPlayer);
        if(navigator.mediaSession)
        {
            navigator.mediaSession.setActionHandler('previoustrack',NSUI.pbprev.onclick);
            navigator.mediaSession.setActionHandler('nexttrack',NSUI.pbnext.onclick);
        }
        window.onkeydown=function(e)
        {
            if(e.key==' ')
            {
                playpausef();
                return false;
            }
            if(e.key=='c')
                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();
        let rarr=r.split('\n');
        let tarr=[];
        let cnt=0;
        for(let i=0;i<rarr.length;++i)
        if(rarr[i].trim().length)++cnt;
        const p=new Promise((a,b)=>{
                for(let i=0;i<rarr.length;++i)
                {
                    let t=rarr[i].trim();
                    if(!t.length)continue;
                    NSPlayer.load_playlist(t,i).then((rp)=>{
                            tarr[rp.plistord]=rp;
                            if(--cnt<=0)a(null);
                        },()=>{b(null);});
                }
            });
        await p;
        for(let i=0;i<tarr.length;++i)
        {
            const e=sh.newelem('li');
            const ea=sh.newelem('a');
            ea.innerHTML=tarr[i].plistname;
            ea.classList.add('listitem');
            ea.href='javascript:void(0);';
            ea.pid=i;
            ea.onclick=function(e){
                e.preventDefault();
                for(let i=0;i<NSUI.ulplaylists.childNodes.length;++i)
                {
                    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');
                    }
                    else{ta.classList.remove('highlighted');ta.classList.remove('active');}
                }
                NSUI.present_playlist.bind(NSUI,this.pid)();
                if(NSPlayer.current)
                NSUI.set_highlighted(NSPlayer.plistname,NSPlayer.tracks[NSPlayer.current].title);
            };
            e.appendChild(ea);
            NSUI.ulplaylists.appendChild(e);
        }
        moi.playlists=tarr;
    },
    present_playlist:function(id)
    {
        while(this.playlist.firstChild)
        this.playlist.removeChild(this.playlist.firstChild);
        const list=this.playlists[id].playlist;
        for(let i=0;i<list.length;++i)
        {
            const l=sh.newelem('li');
            const a=sh.newelem('a');
            a.innerHTML=list[i].title;
            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);
            this.playlist.appendChild(l);
        }
        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}&type=${NSPlayer.get_preferred_or_default_format()}`;
    },
    switch_playlist:function(pl,setactive)
    {
        let cid=0;
        for(;cid<this.playlists.length;++cid)
        if(this.playlists[cid].plistname==pl)break;
        if(cid>=this.playlists.length)return;
        if(setactive)
        {
            NSUI.ulplaylists.childNodes[cid].firstChild.classList.add('highlighted');
            NSUI.ulplaylists.childNodes[cid].firstChild.classList.add('active');
        }
        //VERY stupid design, but it _should_ work.
        NSPlayer.switch_playlist(pl,this.playlists[cid].playlist);
        NSPlayer.sort_playlist(NSPlayer.shuffle);
        this.playlists[cid].playlist=NSPlayer.tracks;
        this.present_playlist(cid);
        this.selectedplist=pl;
    },
    switch_track:function(id)
    {
        if(NSPlayer.plistname!=this.selectedplist)
            this.switch_playlist(this.selectedplist);
        return NSPlayer.play(id);
    },
    set_highlighted:function(pl,t)
    {
        if(pl!=this.selectedplist)return;
        let cid=0;
        for(;cid<this.playlists.length;++cid)
        if(this.playlists[cid].plistname==pl)break;
        const clist=this.playlists[cid].playlist;
        for(let i=0;i<clist.length;++i)
        if(clist[i].title==t)
        this.playlist.childNodes[i].firstChild.classList.add('highlighted');
        else
        this.playlist.childNodes[i].firstChild.classList.remove('highlighted');
    },
    shuffle_switch:function(shuffle)
    {
        for(let i=0;i<this.playlists.length;++i)
        {
            NSPlayer.sort_playlist(shuffle,this.playlists[i].playlist);
            if(this.playlists[i].plistname==this.selectedplist)this.present_playlist(i);
        }
        NSPlayer.sort_playlist(shuffle);
    },
    togglePlist:function()
    {
        NSUI.plistshown=!NSUI.plistshown;
        NSUI.swplaylist.innerHTML=`[${NSUI.plistshown?'Hide':'Show'} Playlist]`;
        NSUI.playlist.style.opacity=NSUI.plistshown?'1':'0';
        NSUI.playlist.style.pointerEvents=NSUI.plistshown?'auto':'none';
    },
    formatTime:function(t)
    {
        if(isNaN(t))return '-:--';
        let m=Math.floor(t/60),s=Math.round(t-Math.floor(t/60)*60);
        if(s<10)return `${m}:0${s}`;
        else if(s==60)return `${m+1}:00`;
        else return `${m}:${s}`;
    },
    bufferedUpdate:function()
    {
        let r=0;
        for(let i=0;i<NSUI.audio.buffered.length;++i)
        r=r<NSUI.audio.buffered.end(i)?NSUI.audio.buffered.end(i):r;
        document.getElementById('cbuff').style.width=r/NSUI.audio.duration*100+'%';
    },
    timeUpdate:function()
    {
        document.getElementById('timenow').innerHTML=NSUI.formatTime(NSUI.audio.currentTime);
        document.getElementById('timeleft').innerHTML=`-${NSUI.formatTime(NSUI.audio.duration-NSUI.audio.currentTime)}`;
        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.;},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={
    audioctx:null,
    srcnode:null,
    anlznode:null,
    audioInit:function()
    {
        window.AudioContext=window.AudioContext||window.webkitAudioContext||window.mozAudioContext||window.msAudioContext;
        if(!window.AudioContext)alert('This page requires Web Audio API to work...');
        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),
    spectgrmw:1200,
    spectrw:0.7,
    spectrm:0.08125,
    caps:[],
    cctx:null,
    spectgrmp:0,
    _61spbins:29,
    _61spbinw:5,
    _61spaccel:.3617274873,
    _61spdamp:0.1617174873,
    _61spvelcap:2*6.17274873,
    _61spgvel:.06617274873,
    _61spelas:.2617274873,
    _61spf:61.7274873,
    _61spelasth:.1617274873,
    _61spcaps:new Float32Array(29),
    _61spcapsv:new Float32Array(29),
    _61spft:performance.now(),
    frms:0,
    lastfpsupd:null,
    fps:0,
    init:function()
    {
        window.requestAnimationFrame=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.msRequestAnimationFrame;
        this.cctx=NSUI.canvas.getContext('2d');
        this.spectrw=NSUI.canvas.width/this.nbins;
        this.spectrm=this.spectrw/10;this.spectrw*=0.9;
        for(let i=0;i<this.nbins;++i)this.caps[i]=0;
    },
    update_spectrum:function()
    {
        const cctx=this.cctx,canvas=NSUI.canvas;
        cctx.clearRect(0,0,canvas.width,canvas.height);
        try{
            let freqdomv=new Uint8Array(NSAudio.anlznode.frequencyBinCount);
            NSAudio.anlznode.getByteFrequencyData(freqdomv);
            for(let i=0;i<this.nbins;++i)
            {
                cctx.fillStyle='hsl('+i*360.0/this.nbins+',100%,50%)';
                cctx.fillRect(i*(this.spectrw+this.spectrm),canvas.height-(canvas.height*freqdomv[i]/255.),this.spectrw,canvas.height*freqdomv[i]/255.);
                if(this.caps[i]<freqdomv[i])this.caps[i]=freqdomv[i];else if(this.caps[i]>0)--this.caps[i];
                cctx.fillStyle='hsl('+i*360.0/this.nbins+',100%,25%)';
                cctx.fillRect(i*(this.spectrw+this.spectrm),canvas.height-(canvas.height*this.caps[i]/255.)-1,this.spectrw,1);
            }
        }catch(e){
            for(let i=0;i<this.nbins;++i)
            {
                if(this.caps[i]>0)--this.caps[i];
                cctx.fillStyle='hsl('+i*360.0/this.nbins+',100%,25%)';
                cctx.fillRect(i*(this.spectrw+this.spectrm),canvas.height-this.caps[i]-1,this.spectrw,1);
            }
        }
    },
    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;
        cctx.clearRect(0,0,canvas.width,canvas.height);
        try{
            let freqdomv=new Float32Array(NSAudio.anlznode.frequencyBinCount);
            NSAudio.anlznode.getFloatFrequencyData(freqdomv);
            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)
            {
                if(ft>1000)
                {
                    this._61spcaps[i]=0;
                    this._61spcapsv[i]=0;
                    continue;
                }
                let vel=this._61spgvel*this._61spcapsv[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])
                {
                    if (!(this._61spcaps[i] < 1e-2 && this._61spcapsv[i] < 1e-2 && _61spfdomv[i] < 1e-2))
                    {
                        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;
                    }
                }
                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.max(this._61spcaps[i], _61spfdomv[i]);
                }

                cctx.fillStyle=`rgba(70,130,180,${0.36+0.64*_61spfdomv[i]})`;
                cctx.fillRect(i*(canvas.width/this._61spbins),canvas.height-(0.617274873*canvas.height*_61spfdomv[i]),canvas.width/this._61spbins*0.96,0.617274873*canvas.height*_61spfdomv[i]);

                cctx.fillStyle='rgb(110,139,161)';
                cctx.fillRect(i*(canvas.width/this._61spbins),canvas.height-(0.617274873*canvas.height*this._61spcaps[i])-4,canvas.width/this._61spbins*0.96,4);
            }
        }catch(e){}
    },
    update_scope:function()
    {
        const cctx=this.cctx,canvas=NSUI.canvas;
        cctx.clearRect(0,0,canvas.width,canvas.height);
        try{
            let timedomv=new Uint8Array(NSAudio.anlznode.frequencyBinCount);
            NSAudio.anlznode.getByteTimeDomainData(timedomv);
            cctx.lineWidth=window.devicePixelRatio;
            cctx.strokeStyle='#000';
            if(isDarkTheme)cctx.strokeStyle='#FFF';
            cctx.beginPath();
            for(let i=0,x=0;i<this.nbins;++i)
            {
                if(i==0)cctx.moveTo(x,timedomv[i]/128.*canvas.height/2);
                else cctx.lineTo(x,timedomv[i]/128.*canvas.height/2);
                x+=canvas.width*1./this.nbins;
            }
            cctx.stroke();
        }catch(e){
            cctx.beginPath();
            cctx.moveTo(0,canvas.height/2);
            cctx.lineTo(canvas.width,canvas.height/2);
            cctx.stroke();
        }
    },
    update_spectrogram:function()
    {
        const cctx=this.cctx,canvas=NSUI.canvas;
        try{
            let freqdomv=new Uint8Array(NSAudio.anlznode.frequencyBinCount);
            NSAudio.anlznode.getByteFrequencyData(freqdomv);
            cctx.clearRect(this.spectgrmp/this.spectgrmw*canvas.width,0,canvas.width/this.spectgrmw,canvas.height);
            for(let i=0;i<this.nbins;++i)
            {
                let color=(isDarkTheme?'rgba(255,255,255,':'rgba(0,0,0,')+freqdomv[i]/256.+')';
                cctx.fillStyle=color;
                cctx.fillRect(this.spectgrmp/this.spectgrmw*canvas.width,(this.nbins-i)/this.nbins*canvas.height,canvas.width/this.spectgrmw,canvas.height/this.nbins);
            }
            if(!NSUI.audio.paused)++this.spectgrmp;
            if(this.spectgrmp>=this.spectgrmw)this.spectgrmp=0;
        }catch(e){
            this.spectgrmp=0;
            cctx.clearRect(0,0,canvas.width,canvas.height);
        }
    },
    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':
                NSVisualization.update_spectrum();
            break;
            case 'spspectrum':
                NSVisualization.update_61_spectrum();
            break;
            case 'scope':
                NSVisualization.update_scope();
            break;
            case 'spectrogram':
                NSVisualization.update_spectrogram();
            break;
            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);
    }
};

NSInk={
    ic1:['#FF3333','#FF8800','#FFFF00','#CCFF00','#33CCFF','#0000FF','#9966FF'],
    ic2:['rgba(204,51,51,0.6)','rgba(187,85,0,0.6)','rgba(255,204,0,0.6)','rgba(153,204,0,0.6)','rgba(51,153,255,0.6)','rgba(0,0,102,0.6)','rgba(153,51,204,0.6)'],
    inks:[],
    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)
        {
            this.inkimg[i]=document.createElement('canvas');
            this.inkimg[i].width=this.inkimg[i].height=10*window.devicePixelRatio;
            const cctx=this.inkimg[i].getContext('2d');
            cctx.fillStyle=this.ic1[i];
            cctx.beginPath();
            cctx.arc(5*window.devicePixelRatio,5*window.devicePixelRatio,3*window.devicePixelRatio,0,2*Math.PI);
            cctx.fill();
            cctx.fillStyle=this.ic2[i];
            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)
    {
        for(let i=0;i<this.inks.length;++i)
        {
            if(!this.inks[i].active)
            {this.inks[i]=new Ink(_vx,_vy,_c1,_c2);return;}
        }
        if (this.inks.length < NSInk.max_droplets) this.inks.push(new Ink(_vx, _vy, _c1, _c2));
    },
    update:function()
    {
        const canvas=NSUI.canvas;
        const cctx=canvas.getContext('2d');
        cctx.clearRect(0,0,canvas.width,canvas.height);
        if (!NSAudio || !NSAudio.anlznode) return;
        const origblend = cctx.globalCompositeOperation;
        cctx.globalCompositeOperation = "screen";
        try{
            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)
            {
                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)
                {
                    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)
                    {
                        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);
                    }
                }
                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);
        }
        let aa=0;
        for(let i=0;i<this.inks.length;++i)
        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();
    NSUI.setup_ui();
    NSUI.load_playlists()
    .then(()=>{
        sh.elem('overlaytext').innerHTML+="Done!<br>Click or tap anywhere to start."
        sh.elem('overlay').onclick=function(){
            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('/');
                for(let i=0;i<p.length;++i)p[i]=decodeURIComponent(p[i]);
                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)
                {
                    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';
                }
            }
            else if(!initerr)sh.elem('overlay').style.display='none';
        }
        sh.elem('overlay').onclick();
    },
    ()=>{
        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
            sh.elem('mt').style.display='none';
    };
    switch(sh.getcookie('playershuffle'))
    {
        case '1':NSPlayer.shuffle=1;break;
        default:
        case '0':NSPlayer.shuffle=0;break;
    }
    switch(sh.getcookie('playerrepeat'))
    {
        case '1':
            NSPlayer.repeat=1;
            NSUI.audio.loop=true;
        break;
        default:
        case '0':
            NSPlayer.repeat=0;
            NSUI.audio.loop=false;
        break;
    }
    NSUI.ishuffle.style.backgroundPosition=`${NSPlayer.shuffle?NSUI.bshonrect:NSUI.bsoffrect}`;
    NSUI.irepeat.style.backgroundPosition=`${NSPlayer.repeat?NSUI.bronerect:NSUI.brallrect}`;
    if(sh.getcookie('playervisualization').length!==0)
    NSUI.vissel.value=sh.getcookie('playervisualization');
    requestAnimationFrame(NSVisualization.updateVisualization);
}
document.addEventListener("DOMContentLoaded", init);