summaryrefslogtreecommitdiff
path: root/libs/music/player.d/main_static.js
diff options
context:
space:
mode:
Diffstat (limited to 'libs/music/player.d/main_static.js')
-rw-r--r--libs/music/player.d/main_static.js526
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);