UNPKG

zusound

Version:

Sound feedback middleware for Zustand state management

101 lines (86 loc) 21.1 kB
var $=Object.defineProperty;var N=(t,e,n)=>e in t?$(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n;var l=(t,e,n)=>N(t,typeof e!="symbol"?e+"":e,n);import{shallow as Z}from"zustand/shallow";var G={detailed:!1,trackAdded:!0,trackRemoved:!0};function A(t,e,n=G){if(t===e)return{};if(typeof t!="object"||t===null||typeof e!="object"||e===null)return n.detailed?{value:e,type:t===void 0?"add":"change"}:e;let r=new Set([...Object.keys(t),...Object.keys(e)]),i={};for(let o of r){let s=t[o],a=e[o],u=null;!(o in t)&&o in e&&n.trackAdded?u="add":o in t&&!(o in e)&&n.trackRemoved?u="remove":Z(s,a)||(u="change"),u!==null&&(n.detailed?i[o]={value:a,previousValue:u!=="add"?s:void 0,type:u}:i[o]=a)}return i}function M(t,e){return A(t,e,{detailed:!1})}function q(t,e){return A(t,e,{detailed:!0})}var z=M;var H=(t,e,n,r,i)=>({diff:i(t,e),timestampStart:n,duration:r-n}),W=(t,e)=>{try{e(t)}catch(n){console.error("Error in trace middleware 'onTrace' callback:",n)}},j=(t,e,n)=>{let r=t.setState,{getState:i}=t;return(s,a,u)=>{let m=Date.now(),p=i();r(s,a,u);let T=i(),f=Date.now();if(p===T)return;let c=H(p,T,m,f,e);W(c,n)}},I=(t,e={})=>(n,r,i)=>{let{onTrace:o=()=>{},diffFn:s=z}=e,a=j(i,s,o);return i.setState=a,t(a,r,i)};var L=I;var S=()=>!!(typeof process<"u"&&process.env||(typeof globalThis<"u"?globalThis:{})?.import?.meta?.env?.PROD===!0);var v={BASE_FREQUENCY:110,MIN_DURATION_MS:50,DEFAULT_MAGNITUDE:{CHANGE:.5,REMOVE:.3},SCALE:[1,1.122,1.335,1.498,1.682],STAGGER_DELAY_MS:50};var b=class b{constructor(){l(this,"audioContext",null);l(this,"isAutoplayBlocked",!1);l(this,"hasUserInteracted",!1);if(typeof window<"u"){let e=()=>{this.hasUserInteracted=!0,this.audioContext?.state==="suspended"&&this.tryResumeAudioContext(),window.removeEventListener("click",e,{capture:!0}),window.removeEventListener("keydown",e,{capture:!0}),window.removeEventListener("touchstart",e,{capture:!0})};window.addEventListener("click",e,{capture:!0}),window.addEventListener("keydown",e,{capture:!0}),window.addEventListener("touchstart",e,{capture:!0})}}static getInstance(){return b.instance===null&&(b.instance=new b),b.instance}getContext(){if(this.audioContext===null||this.audioContext.state==="closed"){this.isAutoplayBlocked=!1;try{let e=this.hasUserInteracted?{}:{latencyHint:"interactive"};this.audioContext=new AudioContext(e),this.audioContext.state==="suspended"&&console.warn("AudioContext created in suspended state. Autoplay likely restricted.")}catch(e){this.audioContext=null;let n=e instanceof Error?e.message:String(e);throw console.error("Failed to create AudioContext:",n),new Error(`Web Audio API is not supported or could not be initialized: ${n}`)}}return this.audioContext}isAudioBlocked(){return this.isAutoplayBlocked||!!this.audioContext&&this.audioContext.state==="suspended"}async tryResumeAudioContext(){if(!this.audioContext)try{this.getContext()}catch{return console.error("Cannot resume: AudioContext failed to initialize."),{resumed:!1,blocked:!0}}if(!this.audioContext)return console.error("Cannot resume: AudioContext is null unexpectedly."),{resumed:!1,blocked:!0};if(this.audioContext.state==="closed")return console.warn("Cannot resume: AudioContext is closed."),{resumed:!1,blocked:!1};if(this.audioContext.state==="running")return this.isAutoplayBlocked=!1,{resumed:!0,blocked:!1};if(this.audioContext.state==="suspended"){console.log("Attempting to resume suspended AudioContext...");try{if(!this.audioContext)throw new Error("AudioContext became null before resume");let e=this.audioContext.resume(),n=new Promise((r,i)=>setTimeout(()=>i(new Error("AudioContext resume timed out after 1000ms")),1e3));if(await Promise.race([e,n]),!this.audioContext)throw new Error("AudioContext became null after resume");return console.log(`AudioContext resumed successfully, state: ${this.audioContext.state}`),this.isAutoplayBlocked=!1,{resumed:!0,blocked:!1}}catch(e){let n=e instanceof Error?e.message:String(e);return console.warn(`AudioContext resume failed: ${n}. Autoplay likely blocked.`),this.isAutoplayBlocked=!0,{resumed:!1,blocked:!0}}}return console.warn(`Cannot resume: AudioContext is in an unexpected state: ${this.audioContext?.state}`),{resumed:!1,blocked:this.isAudioBlocked()}}async cleanup(){if(this.audioContext&&this.audioContext.state!=="closed")try{await this.audioContext.close(),console.log("AudioContext closed successfully.")}catch(e){console.warn("Error closing audio context:",e instanceof Error?e.message:String(e))}finally{this.audioContext=null}else this.audioContext=null;this.isAutoplayBlocked=!1}};l(b,"instance",null);var w=b,k=t=>{let e=0;for(let n=0;n<t.length;n++)e=(e<<5)-e+t.charCodeAt(n),e|=0;return Math.abs(e)};function Y(t,e){if(!t||Object.keys(t).length===0)return[];let r=((i,o="")=>Object.keys(i).reduce((s,a)=>{let u=o.length?`${o}.`:"";return typeof i[a]=="object"&&i[a]!==null&&Array.isArray(i[a]),s[u+a]=i[a],s},{}))(t);return Object.entries(r).map(([i,o])=>{let s=o==null?"remove":"change",a=k(i),u=Math.floor(a/100)%3,m=a%v.SCALE.length,p=v.BASE_FREQUENCY*Math.pow(2,u)*v.SCALE[m],T=i.split(".").length-1;p*=1+T*.05;let f=0,c=typeof o;s==="change"&&(c==="number"?f=Math.min(Math.log1p(Math.abs(o))*50,600):c==="string"?f=Math.min(Math.log1p(o.length)*25,300):c==="boolean"&&(f=o?25:-25));let h="sine";s==="remove"?h="triangle":c==="number"?h="sine":c==="string"?h="square":c==="boolean"?h="sawtooth":h="triangle";let B=s==="remove"?v.DEFAULT_MAGNITUDE.REMOVE:v.DEFAULT_MAGNITUDE.CHANGE;return{id:i,type:h,valueType:s,frequency:p,magnitude:B,duration:Math.max(e,v.MIN_DURATION_MS),detune:f}})}async function R(t){try{let e=w.getInstance(),n=e.getContext();if(typeof window<"u"){let i=new CustomEvent("zusound",{detail:{chunk:t}});window.dispatchEvent(i)}let r=!1;if(n.state==="running")r=!0;else if(n.state==="suspended"){console.log(`Audio context suspended before playing chunk ${t.id}. Attempting resume...`);let{resumed:i}=await e.tryResumeAudioContext();if(i)r=!0;else return console.warn(`Audio playback skipped for chunk ${t.id} - context still suspended.`),!1}else return console.warn(`Audio context in unexpected state (${n.state}) for chunk ${t.id}. Cannot play.`),!1;if(r&&n.state==="running"){let i=n.currentTime,o=t.duration/1e3,s=n.createOscillator();s.type=t.type,s.frequency.setValueAtTime(t.frequency,i),s.detune.setValueAtTime(t.detune,i);let a=n.createGain();a.gain.setValueAtTime(0,i),s.connect(a),a.connect(n.destination);let u=Math.min(.01,o*.2),m=Math.min(.08,o*.3),p=Math.max(0,o-u-m);return o>.02?(a.gain.exponentialRampToValueAtTime(t.magnitude,i+u),p>.001&&a.gain.setValueAtTime(t.magnitude,i+u+p),a.gain.exponentialRampToValueAtTime(.001,i+o)):(a.gain.linearRampToValueAtTime(t.magnitude,i+o*.5),a.gain.linearRampToValueAtTime(0,i+o)),s.start(i),s.stop(i+o),await new Promise(T=>{let f=!1,c=setTimeout(()=>{if(!f){console.warn(`Oscillator onended for chunk ${t.id} timed out. Force cleaning up.`);try{s.disconnect(),a.disconnect()}catch{}f=!0,T()}},o*1e3+150);s.onended=()=>{if(!f){clearTimeout(c);try{s.disconnect(),a.disconnect()}catch{}f=!0,T()}}}),!0}return!1}catch(e){let n=e instanceof Error?e.message:String(e);return console.error(`Failed to prepare audio for chunk ${t.id}:`,n),!1}}function D(t,e,n=!1){try{let r=Y(t,e);if(r.length===0)return;r.forEach((i,o)=>{setTimeout(()=>{R(i).catch(s=>{console.error(`Error during scheduled playback for chunk ${i.id}:`,s)})},o*v.STAGGER_DELAY_MS)})}catch(r){console.error("Sonification setup failed:",r instanceof Error?r.message:String(r))}}function _(t){if(t instanceof CustomEvent&&t.type==="zusound:trace"&&t.detail?.traceData){let n=t.detail.traceData,{diff:r,duration:i}=n;r&&typeof r=="object"&&D(r,i)}}function P(){typeof window<"u"&&(window.removeEventListener("zusound:trace",_),window.addEventListener("zusound:trace",_))}function xe(){typeof window<"u"&&window.removeEventListener("zusound:trace",_)}function K(t){if(typeof window<"u"){let e=new CustomEvent("zusound:trace",{detail:{traceData:t}});window.dispatchEvent(e)}}var X=(t,e={})=>{let{enabled:n=!S(),logDiffs:r=!1,allowInProduction:i=!1,onTrace:o,diffFn:s,initSonification:a=!0,...u}=e,m=S();if(!n||m&&!i)return t;typeof window<"u"&&a&&setTimeout(()=>{try{P()}catch(c){console.error("Error initializing sonification:",c)}},0);let f={...u,diffFn:s,onTrace:c=>{if(K(c),o)try{o(c)}catch(h){console.error("Error in user-provided 'onTrace' callback:",h)}}};if(r){typeof window<"u"&&!window.__zusound_logger__&&(window.__zusound_logger__=[]);let c=f.onTrace;f.onTrace=h=>{typeof window<"u"&&window.__zusound_logger__&&window.__zusound_logger__.push({...h}),c(h)}}return L(t,f)};var E=class{constructor(e,n){l(this,"canvas");l(this,"gl",null);l(this,"program",null);l(this,"uniforms",{});l(this,"vertexBuffer",null);l(this,"vertexShaderSource",` attribute vec2 a_position; varying vec2 v_texCoord; void main() { gl_Position = vec4(a_position, 0.0, 1.0); v_texCoord = a_position * 0.5 + 0.5; // Map position to texture coordinates } `);l(this,"fragmentShaderSource",` precision mediump float; varying vec2 v_texCoord; uniform int u_eventCount; uniform float u_eventProgress[${10}]; uniform float u_eventFrequency[${10}]; uniform float u_eventMagnitude[${10}]; uniform float u_eventDetune[${10}]; uniform int u_eventType[${10}]; // 0=sine, 1=square, 2=sawtooth, 3=triangle // Function to get color based on waveform type vec3 getTypeColor(int type) { if (type == 0) return vec3(0.2, 0.6, 1.0); // sine - blue if (type == 1) return vec3(1.0, 0.5, 0.2); // square - orange if (type == 2) return vec3(0.2, 1.0, 0.5); // sawtooth - green return vec3(1.0, 0.3, 0.8); // triangle - pink } // Function to simulate wave shape based on type float getWaveShape(int type, float t) { t = fract(t); // Use fractional part for repeating pattern if (type == 0) return 0.5 + 0.5 * sin(t * 6.28318); // Sine wave if (type == 1) return t < 0.5 ? 0.0 : 1.0; // Square wave if (type == 2) return t; // Sawtooth wave return t < 0.5 ? t * 2.0 : 2.0 - t * 2.0; // Triangle wave } void main() { vec2 p = (v_texCoord * 2.0 - 1.0); // Map texture coords to -1 to 1 range vec3 color = vec3(0.1, 0.1, 0.15); // Base background color float alpha = 1.0; // Base alpha // Add subtle background glow towards the center float bgGlow = 1.0 - length(p); color += vec3(0.05, 0.05, 0.1) * max(0.0, bgGlow); // Loop through active events and layer their effects for (int i = 0; i < ${10}; i++) { if (i >= u_eventCount) break; // Stop if we've processed all active events if (u_eventProgress[i] >= 1.0) continue; // Skip completed events // Get event properties from uniforms float progress = u_eventProgress[i]; float magnitude = u_eventMagnitude[i]; float frequency = u_eventFrequency[i] / 440.0; // Normalize frequency (relative to A4) float detune = u_eventDetune[i] / 1200.0; // Convert cents to octave fraction float scaledFreq = frequency * pow(2.0, detune); // Apply detune int type = u_eventType[i]; // Calculate visibility based on progress (fade in/out) float fadeIn = smoothstep(0.0, 0.1, progress); float fadeOut = 1.0 - smoothstep(0.7, 1.0, progress); float visibility = fadeIn * fadeOut; // Calculate effect based on distance from center and wave shape float dist = length(p); float ringWidth = 0.05 * magnitude; // Ring width depends on magnitude float ringSize = 0.3 + 0.7 * (1.0 - progress); // Ring expands outwards as progress decreases // Create a smooth ring shape float ring = smoothstep(ringSize - ringWidth, ringSize, dist) * smoothstep(ringSize + ringWidth, ringSize, dist); // Add ripples based on wave shape and frequency float ripples = getWaveShape(type, dist * 10.0 * scaledFreq); // Combine ring and ripples, apply visibility and magnitude vec3 eventColor = getTypeColor(type); float eventEffect = (ring * 0.8 + ripples * 0.2) * visibility * magnitude; // Add event's color contribution color += eventColor * eventEffect; } gl_FragColor = vec4(color, alpha); } `);this.canvas=document.createElement("canvas"),this.canvas.width=e,this.canvas.height=n,this.canvas.style.display="block",this.canvas.style.borderRadius="50%"}getCanvasElement(){return this.canvas}initialize(){if(this.gl)return!0;if(typeof window>"u")return!1;try{if(this.gl=this.canvas.getContext("webgl",{alpha:!0,antialias:!0}),!this.gl)return console.error("WebGL not supported or context creation failed."),!1;let e=this.compileShader(this.gl.VERTEX_SHADER,this.vertexShaderSource),n=this.compileShader(this.gl.FRAGMENT_SHADER,this.fragmentShaderSource);if(!e||!n)return!1;if(this.program=this.gl.createProgram(),!this.program)return console.error("Failed to create WebGL program."),!1;if(this.gl.attachShader(this.program,e),this.gl.attachShader(this.program,n),this.gl.linkProgram(this.program),!this.gl.getProgramParameter(this.program,this.gl.LINK_STATUS))return console.error("Could not link WebGL program:",this.gl.getProgramInfoLog(this.program)),this.gl.deleteProgram(this.program),this.program=null,!1;this.gl.useProgram(this.program);let r=[-1,-1,1,-1,-1,1,1,1];this.vertexBuffer=this.gl.createBuffer(),this.gl.bindBuffer(this.gl.ARRAY_BUFFER,this.vertexBuffer),this.gl.bufferData(this.gl.ARRAY_BUFFER,new Float32Array(r),this.gl.STATIC_DRAW);let i=this.gl.getAttribLocation(this.program,"a_position");this.gl.enableVertexAttribArray(i),this.gl.vertexAttribPointer(i,2,this.gl.FLOAT,!1,0,0),this.uniforms.u_eventCount=this.gl.getUniformLocation(this.program,"u_eventCount");for(let o=0;o<10;o++)this.uniforms[`u_eventProgress[${o}]`]=this.gl.getUniformLocation(this.program,`u_eventProgress[${o}]`),this.uniforms[`u_eventFrequency[${o}]`]=this.gl.getUniformLocation(this.program,`u_eventFrequency[${o}]`),this.uniforms[`u_eventMagnitude[${o}]`]=this.gl.getUniformLocation(this.program,`u_eventMagnitude[${o}]`),this.uniforms[`u_eventDetune[${o}]`]=this.gl.getUniformLocation(this.program,`u_eventDetune[${o}]`),this.uniforms[`u_eventType[${o}]`]=this.gl.getUniformLocation(this.program,`u_eventType[${o}]`);return!0}catch(e){return console.error("Failed to initialize WebGL shader manager:",e),this.cleanupPartialInit(),!1}}compileShader(e,n){if(!this.gl)return null;let r=this.gl.createShader(e);return r?(this.gl.shaderSource(r,n),this.gl.compileShader(r),this.gl.getShaderParameter(r,this.gl.COMPILE_STATUS)?r:(console.error(`Shader compile error (${e===this.gl.VERTEX_SHADER?"Vertex":"Fragment"}):`,this.gl.getShaderInfoLog(r)),this.gl.deleteShader(r),null)):(console.error("Failed to create shader object."),null)}render(e){if(!this.gl||!this.program)return;this.gl.clearColor(0,0,0,0),this.gl.clear(this.gl.COLOR_BUFFER_BIT),this.gl.useProgram(this.program),this.gl.bindBuffer(this.gl.ARRAY_BUFFER,this.vertexBuffer);let n=this.gl.getAttribLocation(this.program,"a_position");this.gl.enableVertexAttribArray(n),this.gl.vertexAttribPointer(n,2,this.gl.FLOAT,!1,0,0),this.updateUniforms(e),this.gl.drawArrays(this.gl.TRIANGLE_STRIP,0,4)}updateUniforms(e){if(!this.gl||!this.program)return;this.gl.uniform1i(this.uniforms.u_eventCount,e.length);let n={sine:0,square:1,sawtooth:2,triangle:3};for(let r=0;r<10;r++){let i=this.uniforms[`u_eventProgress[${r}]`],o=this.uniforms[`u_eventFrequency[${r}]`],s=this.uniforms[`u_eventMagnitude[${r}]`],a=this.uniforms[`u_eventDetune[${r}]`],u=this.uniforms[`u_eventType[${r}]`];if(r<e.length){let m=e[r],p=m.chunk;this.gl.uniform1f(i,m.getProgress()),this.gl.uniform1f(o,p.frequency),this.gl.uniform1f(s,p.magnitude),this.gl.uniform1f(a,p.detune),this.gl.uniform1i(u,n[p.type]??3)}else this.gl.uniform1f(i,1)}}cleanup(){if(this.stopRenderLoop(),this.gl){this.program&&(this.vertexBuffer&&this.gl.deleteBuffer(this.vertexBuffer),this.gl.deleteProgram(this.program));let e=this.gl.getExtension("WEBGL_lose_context");e&&e.loseContext()}this.cleanupPartialInit()}cleanupPartialInit(){this.gl=null,this.program=null,this.vertexBuffer=null,this.uniforms={}}startRenderLoop(){}stopRenderLoop(){}};var y=class y{constructor(){l(this,"shaderManager",null);l(this,"isInitialized",!1);l(this,"eventListenerAttached",!1);l(this,"isMounted",!1);l(this,"animationFrameId",null);l(this,"events",[]);l(this,"handleSonificationEvent",e=>{this.isZusoundEvent(e)&&e.detail?.chunk&&this.addEvent(e.detail.chunk)})}static getInstance(){return y.instance||(y.instance=new y,y.instance.initialize(),y.instance.attachEventListener()),y.instance}initialize(){if(this.isInitialized)return!0;if(typeof window>"u")return console.warn("Visualizer cannot initialize outside of a browser environment."),!1;try{return this.shaderManager=new E(64,64),this.isInitialized=this.shaderManager.initialize(),this.isInitialized||(console.error("Visualizer Shader Manager failed to initialize."),this.shaderManager=null),this.isInitialized}catch(e){return console.error("Error during visualizer initialization:",e),this.shaderManager=null,this.isInitialized=!1,!1}}getCanvasElement(){return!this.isInitialized&&!this.initialize()?null:this.shaderManager?.getCanvasElement()??null}notifyMounted(){if(!this.isInitialized){console.warn("Visualizer cannot be mounted before initialization.");return}this.isMounted=!0,this.startRenderLoop()}notifyUnmounted(){this.isMounted=!1,this.stopRenderLoop()}addEvent(e){if(!this.isInitialized&&!this.initialize()){console.warn("Visualizer not initialized, cannot add event.");return}let n=performance.now(),r={chunk:e,startTime:n,getProgress:()=>Math.min(1,(performance.now()-r.startTime)/1e3)};this.events.push(r),this.events.length>10&&this.events.shift(),this.isMounted&&this.startRenderLoop()}startRenderLoop(){if(this.animationFrameId!==null||!this.isMounted||!this.isInitialized)return;let e=()=>{if(!this.isMounted||!this.isInitialized||!this.shaderManager){this.animationFrameId=null;return}this.events=this.events.filter(n=>n.getProgress()<1),this.shaderManager.render(this.events),this.isMounted&&this.events.length>0?this.animationFrameId=requestAnimationFrame(e):this.animationFrameId=null};this.animationFrameId=requestAnimationFrame(e)}stopRenderLoop(){this.animationFrameId!==null&&(cancelAnimationFrame(this.animationFrameId),this.animationFrameId=null)}isZusoundEvent(e){return e instanceof CustomEvent&&e.type==="zusound"&&"detail"in e}attachEventListener(){this.eventListenerAttached||typeof window>"u"||(window.addEventListener("zusound",this.handleSonificationEvent),this.eventListenerAttached=!0)}detachEventListener(){!this.eventListenerAttached||typeof window>"u"||(window.removeEventListener("zusound",this.handleSonificationEvent),this.eventListenerAttached=!1)}cleanup(){this.stopRenderLoop(),this.detachEventListener(),this.shaderManager?.cleanup(),this.isInitialized=!1,this.isMounted=!1,this.shaderManager=null,this.events=[]}};l(y,"instance",null);var x=y;var V=null,J=null,ee=!1,d=null,C=!1;function O(){if(C||typeof document>"u")return;let t=x.getInstance(),e=t.getCanvasElement();if(!e){console.error("Visualizer canvas unavailable. Cannot show persistent UI.");return}ee&&V&&V.contains(e)&&(t.notifyUnmounted(),J?.removeChild(e)),d=document.createElement("div"),d.style.cssText=` position: fixed; top: 20px; right: 20px; background: rgba(30, 30, 40, 0.8); border-radius: 50%; /* Keep the round shape */ z-index: 9999; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); width: ${e.width}px; height: ${e.height}px; transition: transform 0.3s ease, box-shadow 0.3s ease, opacity 0.3s ease; cursor: pointer; opacity: 0.8; transform: scale(1); `,d.addEventListener("mouseenter",()=>{d&&(d.style.transform="scale(1.1)",d.style.boxShadow="0 6px 16px rgba(0, 0, 0, 0.4)",d.style.opacity="1")}),d.addEventListener("mouseleave",()=>{d&&(d.style.transform="scale(1)",d.style.boxShadow="0 4px 12px rgba(0, 0, 0, 0.3)",d.style.opacity="0.8")}),d.appendChild(e),document.body.appendChild(d),t.notifyMounted(),C=!0}function U(){if(!C||!d)return;let t=x.getInstance(),e=t.getCanvasElement();e&&d.contains(e)&&t.notifyUnmounted(),d.parentNode&&d.parentNode.removeChild(d),d=null,C=!1}function te(){x.getInstance()}function ne(t){if(typeof window<"u"){let e=new CustomEvent("zusound",{detail:{chunk:t}});window.dispatchEvent(e)}else console.warn("Cannot visualizeSonicChunk outside of a browser environment.")}export{v as AUDIO_CONFIG,w as AudioContextManager,q as calculateDetailedDiff,z as calculateDiff,A as calculateDiffBase,M as calculateSimpleDiff,te as ensureVisualizerReady,U as hidePersistentVisualizer,P as initSonificationListener,R as playSonicChunk,xe as removeSonificationListener,O as showPersistentVisualizer,D as sonifyChanges,ne as visualizeSonicChunk,X as zusound}; //# sourceMappingURL=index.es.js.map