Spaces:
Running on Zero
Running on Zero
Download assets/wave.js from fffiloni/ZONOS3-Mini: direct link, hf CLI and curl.
- Browser
- Download file 4.89 kB
-
https://huggingface.co/spaces/fffiloni/ZONOS3-Mini/resolve/main/assets/wave.js
- Command line
-
hf download hf://spaces/fffiloni/ZONOS3-Mini/assets/wave.js
-
curl -L -o wave.js https://huggingface.co/spaces/fffiloni/ZONOS3-Mini/resolve/main/assets/wave.js
4.89 kB
| // An audio-reactive illustration, not an oscilloscope or a PCM preview. | |
| // Energy comes from the playing audio; shape and continuous phase are visual. | |
| function voiceEnergy(values) { | |
| if (!values.length) return 0; | |
| let sum=0, squares=0; | |
| for (const value of values) { const v=Number.isFinite(value)?value:0; sum+=v; squares+=v*v; } | |
| const rms=Math.sqrt(Math.max(0,squares/values.length-(sum/values.length)**2)); | |
| // Soft display gain makes quiet speech visible; silence still stays flat. | |
| return Math.sqrt(1-Math.exp(-36*Math.max(0,rms-.0025))); | |
| } | |
| class WaveMotion { | |
| constructor() { this.level=0; this.phase=0; } | |
| step(target,seconds) { | |
| const dt=Math.min(.05,Math.max(0,seconds)); | |
| target=Math.min(1,Math.max(0,Number.isFinite(target)?target:0)); | |
| // A quick attack follows syllables; a longer release rounds their decay. | |
| // Time-based attack/release behaves alike at 30, 60 and 120 Hz. | |
| const tau=target > this.level ? .045 : .16; | |
| this.level+=(target-this.level)*(1-Math.exp(-dt/tau)); | |
| this.phase=(this.phase+dt*(4.2+3.2*this.level))%(Math.PI*20); | |
| if(target===0 && this.level<.001)this.level=0; | |
| return this; | |
| } | |
| } | |
| function wavePath(level,phase=0) { | |
| level=Math.min(1,Math.max(0,Number.isFinite(level)?level:0)); | |
| if(level<.001)return 'M0 70 L1000 70'; | |
| function y(t) { | |
| const edge=Math.min(1,t/.16,(1-t)/.16); | |
| const fade=edge*edge*edge*(edge*(edge*6-15)+10); | |
| const broad=.6+.4*(.5+.5*Math.sin(Math.PI*2*1.5*t-phase*.7)); | |
| const bend=1.1*Math.sin(Math.PI*2*.8*t-phase*.4); | |
| const carrier=(Math.sin(Math.PI*2*5.2*t+phase+bend)+.18*Math.sin(Math.PI*2*8.6*t-phase*.6+.7))/1.18; | |
| return 70-64*level*fade*broad*carrier; | |
| } | |
| const derivative=t=>t<=0||t>=1?0:(y(t+.0001)-y(t-.0001))/.0002; | |
| // Cubics follow the analytic curve, without saturation or flattened peaks. | |
| const count=128, width=960; | |
| let d='M0 70 L20 70'; | |
| for(let i=1;i<=count;i++) { | |
| const a=(i-1)/count,b=i/count,h=(b-a)/3; | |
| d+=` C${(20+width*(a+h)).toFixed(2)} ${(y(a)+derivative(a)*h).toFixed(2)} ${ | |
| (20+width*(b-h)).toFixed(2)} ${(y(b)-derivative(b)*h).toFixed(2)} ${ | |
| (20+width*b).toFixed(2)} ${y(b).toFixed(2)}`; | |
| } | |
| return d+' L1000 70'; | |
| } | |
| // Playback follows the actual PCM texture. The activity curve above stays intact. | |
| class PlaybackWave { | |
| constructor() { | |
| this.level=0; | |
| this.shape=new Float32Array(192); | |
| this.bins=new Float32Array(256); | |
| this.filtered=new Float32Array(256); | |
| } | |
| reset(){this.level=0;this.shape.fill(0);} | |
| step(values,seconds){ | |
| const dt=Math.min(.05,Math.max(0,seconds)); | |
| const target=values?.length?voiceEnergy(values):0; | |
| this.level+=(target-this.level)*(1-Math.exp(-dt/(target > this.level ? .025 : .12))); | |
| if(target===0&&this.level<.001)this.reset(); | |
| if(!values?.length||target===0)return this; | |
| let mean=0; | |
| for(const value of values)mean+=Number.isFinite(value)?value:0; | |
| mean/=values.length; | |
| // Average adjacent PCM samples, then remove only the finest display jitter. | |
| for(let i=0;i<256;i++){ | |
| const a=Math.floor(i*values.length/256),b=Math.max(a+1,Math.floor((i+1)*values.length/256)); | |
| let sum=0; | |
| for(let j=a;j<b;j++)sum+=(Number.isFinite(values[j])?values[j]:0)-mean; | |
| this.bins[i]=sum/(b-a); | |
| } | |
| for(let i=0;i<256;i++)this.filtered[i]=(this.bins[Math.max(0,i-1)]+2*this.bins[i]+this.bins[Math.min(255,i+1)])/4; | |
| // A rising zero crossing reduces whole-line phase flips between frames. | |
| let start=0,best=0; | |
| for(let i=1;i<64;i++){ | |
| const slope=this.filtered[i]-this.filtered[i-1]; | |
| if(this.filtered[i-1]<=0&&this.filtered[i]>0&&slope>best){best=slope;start=i;} | |
| } | |
| const magnitudes=Array.from(this.filtered.slice(start,start+192),Math.abs).sort((a,b)=>a-b); | |
| const scale=Math.max(.012,magnitudes[182]*1.25); | |
| const mix=1-Math.exp(-dt/.028); | |
| for(let i=0;i<192;i++){ | |
| const sample=Math.tanh(this.filtered[start+i]/scale); | |
| this.shape[i]+=(sample-this.shape[i])*mix; | |
| } | |
| return this; | |
| } | |
| path(){ | |
| if(this.level<.001)return 'M0 70 L1000 70'; | |
| const count=this.shape.length; | |
| const ys=Array.from(this.shape,(value,i)=>{ | |
| const t=i/(count-1),edge=Math.min(1,t/.12,(1-t)/.12); | |
| const fade=edge*edge*edge*(edge*(edge*6-15)+10); | |
| return 70-64*this.level*value*fade; | |
| }); | |
| // Monotone cubic interpolation keeps small ripples without pointed corners. | |
| const slope=ys.slice(1).map((y,i)=>y-ys[i]); | |
| const tangent=ys.map((_,i)=>i===0||i===count-1?0: | |
| slope[i-1]*slope[i]<=0?0:2*slope[i-1]*slope[i]/(slope[i-1]+slope[i])); | |
| const dx=960/(count-1);let d='M0 70 L20 70'; | |
| for(let i=1;i<count;i++){ | |
| const x=20+dx*(i-1); | |
| d+=` C${(x+dx/3).toFixed(2)} ${(ys[i-1]+tangent[i-1]/3).toFixed(2)} ${(x+2*dx/3).toFixed(2)} ${(ys[i]-tangent[i]/3).toFixed(2)} ${(x+dx).toFixed(2)} ${ys[i].toFixed(2)}`; | |
| } | |
| return d+' L1000 70'; | |
| } | |
| } | |