diff --git a/experiments/clarinet-synth/src/audio/ClarinetProcessor.js b/experiments/clarinet-synth/src/audio/ClarinetProcessor.js index 1b6b0ca..fb9d711 100644 --- a/experiments/clarinet-synth/src/audio/ClarinetProcessor.js +++ b/experiments/clarinet-synth/src/audio/ClarinetProcessor.js @@ -18,7 +18,7 @@ export class ClarinetProcessor { async initialize() { console.log(`ClarinetProcessor.initialize()`) this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); - this._installIOSUnlock(this.audioContext); + await this._installIOSUnlock(this.audioContext); // Create gain node for volume control this.gainNode = this.audioContext.createGain(); @@ -80,7 +80,7 @@ export class ClarinetProcessor { } } - _installIOSUnlock(ctx) { + async _installIOSUnlock(ctx) { if (!ctx) return; let unlocked = ctx.state === 'running'; const cleanup = () => { @@ -110,6 +110,9 @@ export class ClarinetProcessor { document.addEventListener('touchstart', unlock, { capture: true, passive: true }); document.addEventListener('keydown', unlock, { capture: true }); ctx.onstatechange = () => console.log('[audio] state:', ctx.state); + + // Try to unlock immediately if we're in a user gesture + await unlock(); } noteOff() { diff --git a/experiments/clarinet-synth/vite.config.js b/experiments/clarinet-synth/vite.config.js index af5cea5..7740de9 100644 --- a/experiments/clarinet-synth/vite.config.js +++ b/experiments/clarinet-synth/vite.config.js @@ -16,7 +16,6 @@ export default defineConfig({ return 'assets/[name]-[hash][extname]'; } } - }, - watch: {}, + } } }) diff --git a/www/clarinet-synth/assets/clarinet-worklet-TGdX5pU9.js b/www/clarinet-synth/assets/clarinet-worklet-BBK06oMB.js similarity index 87% rename from www/clarinet-synth/assets/clarinet-worklet-TGdX5pU9.js rename to www/clarinet-synth/assets/clarinet-worklet-BBK06oMB.js index a5247f7..3260e35 100644 --- a/www/clarinet-synth/assets/clarinet-worklet-TGdX5pU9.js +++ b/www/clarinet-synth/assets/clarinet-worklet-BBK06oMB.js @@ -1,6 +1,8 @@ // clarinet-worklet.js // AudioWorklet processor for the clarinet engine +const sampleRate = globalThis.sampleRate; + class ClarinetWorkletProcessor extends AudioWorkletProcessor { constructor() { super(); @@ -73,12 +75,15 @@ class ClarinetWorkletProcessor extends AudioWorkletProcessor { if (scaled < -3) return -1; const x2 = scaled * scaled; - return scaled * (27 + x2) / (27 + 9 * x2); + const result = scaled * (27 + x2) / (27 + 9 * x2); + console.log(`[ClarinetWorkletProcessor] reedReflection: pressureDiff=${pressureDiff.toFixed(4)}, scaled=${scaled.toFixed(4)}, result=${result.toFixed(4)}`); + return result; } lowpass(input, cutoff) { const a = cutoff; this.lpf.y1 = a * input + (1 - a) * this.lpf.y1; + console.log(`[ClarinetWorkletProcessor] lowpass: input=${input.toFixed(4)}, cutoff=${cutoff.toFixed(4)}, output=${this.lpf.y1.toFixed(4)}`); return this.lpf.y1; } @@ -103,11 +108,11 @@ class ClarinetWorkletProcessor extends AudioWorkletProcessor { updateEnvelope(deltaTime) { if (this.gate) { - const attackRate = 1.0 / (this.attackTime * this.sampleRate); + const attackRate = 1.0 / this.attackTime; this.envelope += attackRate * deltaTime; if (this.envelope > 1) this.envelope = 1; } else { - const releaseRate = 1.0 / (this.releaseTime * this.sampleRate); + const releaseRate = 1.0 / this.releaseTime; this.envelope -= releaseRate * deltaTime; if (this.envelope < 0) { this.envelope = 0; @@ -166,10 +171,11 @@ class ClarinetWorkletProcessor extends AudioWorkletProcessor { this.delayLine[this.writePos] = newSample; this.writePos = (this.writePos + 1) % this.delayLength; - return newSample * env * 0.5; + return newSample * env * 1.0; } noteOn(frequency) { + console.log(`[ClarinetWorkletProcessor] noteOn: ${frequency}`); this.setFrequency(frequency); this.gate = true; this.isPlaying = true; @@ -187,6 +193,7 @@ class ClarinetWorkletProcessor extends AudioWorkletProcessor { } setParameter(param, value) { + console.log(`[ClarinetWorkletProcessor] setParameter: ${param}, ${value}`); switch(param) { case 'breath': this.breathPressure = value * 0.8 + 0.2; @@ -207,7 +214,7 @@ class ClarinetWorkletProcessor extends AudioWorkletProcessor { this.lpf.cutoff = 0.3 + value * 0.69; break; case 'brightness': - this.hpf.cutoff = value * 0.05; + this.hpf.cutoff = value * 0.5; break; case 'vibrato': this.vibratoAmount = value * 0.01; @@ -223,6 +230,10 @@ class ClarinetWorkletProcessor extends AudioWorkletProcessor { channel[i] = this.processSample(); } + if (this.isPlaying) { + console.log(`[ClarinetWorkletProcessor] process: output[0] = ${channel[0]}`); + } + return true; // Keep processor alive } } diff --git a/www/clarinet-synth/assets/index-3bg4U7-_.js b/www/clarinet-synth/assets/index-3bg4U7-_.js new file mode 100644 index 0000000..38c04ef --- /dev/null +++ b/www/clarinet-synth/assets/index-3bg4U7-_.js @@ -0,0 +1 @@ +(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))i(s);new MutationObserver(s=>{for(const n of s)if(n.type==="childList")for(const a of n.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&i(a)}).observe(document,{childList:!0,subtree:!0});function t(s){const n={};return s.integrity&&(n.integrity=s.integrity),s.referrerPolicy&&(n.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?n.credentials="include":s.crossOrigin==="anonymous"?n.credentials="omit":n.credentials="same-origin",n}function i(s){if(s.ep)return;s.ep=!0;const n=t(s);fetch(s.href,n)}})();class v{constructor(e=44100){this.sampleRate=e,this.delayLine=null,this.delayLength=100,this.readPos=0,this.writePos=0,this.breathPressure=.7,this.reedStiffness=.5,this.noiseLevel=.15,this.lpf={x1:0,y1:0,cutoff:.7},this.hpf={x1:0,y1:0,cutoff:.01},this.envelope=0,this.attackTime=.01,this.releaseTime=.05,this.gate=!1,this.vibratoAmount=0,this.vibratoRate=5,this.vibratoPhase=0,this.frequency=440,this.targetFrequency=440,this.isPlaying=!1}setFrequency(e){this.targetFrequency=e;const t=Math.floor(this.sampleRate/e);(!this.delayLine||t!==this.delayLength)&&(this.delayLength=t,this.delayLine=new Float32Array(this.delayLength),this.delayLine.fill(0),this.readPos=0,this.writePos=0)}reedReflection(e){const t=this.reedStiffness*5+.5,i=e*t;if(i>3)return 1;if(i<-3)return-1;const s=i*i;return i*(27+s)/(27+9*s)}lowpass(e,t){const i=t;return this.lpf.y1=i*e+(1-i)*this.lpf.y1,this.lpf.y1}highpass(e,t){const s=t*(this.hpf.y1+e-this.hpf.x1);return this.hpf.x1=e,this.hpf.y1=s,s}saturate(e){if(e>3)return 1;if(e<-3)return-1;const t=e*e;return e*(27+t)/(27+9*t)}generateNoise(){return(Math.random()*2-1)*this.noiseLevel}updateEnvelope(e){if(this.gate){const t=1/(this.attackTime*this.sampleRate);this.envelope+=t*e,this.envelope>1&&(this.envelope=1)}else{const t=1/(this.releaseTime*this.sampleRate);this.envelope-=t*e,this.envelope<0&&(this.envelope=0,this.isPlaying=!1)}return this.envelope}process(){if(!this.delayLine||this.delayLength===0)return 0;const e=this.updateEnvelope(1);if(e<=.001)return 0;this.vibratoPhase+=this.vibratoRate*2*Math.PI/this.sampleRate,this.vibratoPhase>2*Math.PI&&(this.vibratoPhase-=2*Math.PI);const t=Math.sin(this.vibratoPhase)*this.vibratoAmount,s=this.sampleRate/this.targetFrequency*(1+t),n=Math.min(s,this.delayLength-1),c=(this.writePos-n+this.delayLength)%this.delayLength,l=Math.floor(c),d=c-l,f=(l+1)%this.delayLength,u=this.delayLine[l]*(1-d)+this.delayLine[f]*d,m=this.generateNoise(),g=this.breathPressure*e+m*e-u,y=this.reedReflection(g);let o=u+y*.5;return o=this.lowpass(o,this.lpf.cutoff),o=this.highpass(o,this.hpf.cutoff),o=this.saturate(o*.95),this.delayLine[this.writePos]=o,this.writePos=(this.writePos+1)%this.delayLength,o*e*.5}noteOn(e){if(this.setFrequency(e),this.gate=!0,this.isPlaying=!0,this.vibratoPhase=0,this.delayLine)for(let t=0;t{const i=t.outputBuffer.getChannelData(0);for(let s=0;s{document.removeEventListener("pointerdown",s,!0),document.removeEventListener("touchstart",s,!0),document.removeEventListener("keydown",s,!0)};async function s(){if(!t)try{e.state!=="running"&&await e.resume();const n=e.createBuffer(1,1,e.sampleRate),a=e.createBufferSource();a.buffer=n,a.connect(e.destination),a.start(0),setTimeout(()=>a.disconnect(),0),t=!0,i(),console.log("[audio] iOS unlocked")}catch(n){console.warn("[audio] unlock failed",n)}}document.addEventListener("pointerdown",s,{capture:!0,passive:!0}),document.addEventListener("touchstart",s,{capture:!0,passive:!0}),document.addEventListener("keydown",s,{capture:!0}),e.onstatechange=()=>console.log("[audio] state:",e.state),await s()}noteOff(){this.useWorklet&&this.workletNode?this.workletNode.port.postMessage({type:"noteOff"}):this.engine&&this.engine.noteOff()}setParameter(e,t){if(console.log(`[ClarinetProcessor] setParameter: ${e}, ${t}`),this.useWorklet&&this.workletNode)this.workletNode.port.postMessage({type:"setParameter",data:{param:e,value:t}});else if(this.engine)switch(e){case"breath":this.engine.setBreath(t);break;case"reed":this.engine.setReedStiffness(t);break;case"noise":this.engine.setNoise(t);break;case"attack":this.engine.setAttack(t);break;case"release":this.engine.setRelease(t);break;case"damping":this.engine.setDamping(t);break;case"brightness":this.engine.setBrightness(t);break;case"vibrato":this.engine.setVibrato(t);break}}getAnalyserData(){if(!this.analyser)return null;const e=new Uint8Array(this.analyser.frequencyBinCount);return this.analyser.getByteTimeDomainData(e),e}shutdown(){this.workletNode&&this.workletNode.disconnect(),this.scriptNode&&this.scriptNode.disconnect(),this.gainNode&&this.gainNode.disconnect(),this.analyser&&this.analyser.disconnect(),this.audioContext&&this.audioContext.close(),this.isActive=!1}}class r{constructor(e,t,i,s=0,n=100,a=50){this.element=e,this.valueElement=t,this.onChange=i,this.min=s,this.max=n,this.value=a,this.isDragging=!1,this.startY=0,this.startValue=0,this.setupListeners(),this.updateDisplay()}setupListeners(){this.element.addEventListener("mousedown",e=>{this.isDragging=!0,this.startY=e.clientY,this.startValue=this.value,e.preventDefault()}),document.addEventListener("mousemove",e=>{if(this.isDragging){const t=(this.startY-e.clientY)*.5;this.value=Math.max(this.min,Math.min(this.max,this.startValue+t)),this.updateDisplay(),this.onChange(this.value/100)}}),document.addEventListener("mouseup",()=>{this.isDragging=!1}),this.element.addEventListener("touchstart",e=>{this.isDragging=!0,this.startY=e.touches[0].clientY,this.startValue=this.value,e.preventDefault()}),document.addEventListener("touchmove",e=>{if(this.isDragging){const t=(this.startY-e.touches[0].clientY)*.5;this.value=Math.max(this.min,Math.min(this.max,this.startValue+t)),this.updateDisplay(),this.onChange(this.value/100)}}),document.addEventListener("touchend",()=>{this.isDragging=!1}),this.element.addEventListener("dblclick",()=>{this.value=(this.min+this.max)/2,this.updateDisplay(),this.onChange(this.value/100)})}updateDisplay(){const e=this.value/100*270-135;this.element.style.transform=`rotate(${e}deg)`,this.valueElement.textContent=Math.round(this.value)}setValue(e){this.value=Math.max(this.min,Math.min(this.max,e)),this.updateDisplay()}}class b{constructor(e,t,i){this.container=e,this.onNoteOn=t,this.onNoteOff=i,this.activeKeys=new Set,this.keyMap=this.createKeyMap(),this.setupListeners()}createKeyMap(){return{a:"C4",w:"C#4",s:"D4",e:"D#4",d:"E4",f:"F4",t:"F#4",g:"G4",y:"G#4",h:"A4",u:"A#4",j:"B4",k:"C5"}}noteToFrequency(e){return{C4:261.63,"C#4":277.18,D4:293.66,"D#4":311.13,E4:329.63,F4:349.23,"F#4":369.99,G4:392,"G#4":415.3,A4:440,"A#4":466.16,B4:493.88,C5:523.25}[e]||440}setupListeners(){this.container.querySelectorAll(".key").forEach(t=>{const i=t.dataset.note;t.addEventListener("mousedown",s=>{s.preventDefault(),this.pressKey(i)}),t.addEventListener("mouseup",()=>{this.releaseKey(i)}),t.addEventListener("mouseleave",()=>{this.activeKeys.has(i)&&this.releaseKey(i)}),t.addEventListener("touchstart",s=>{s.preventDefault(),this.pressKey(i)}),t.addEventListener("touchend",s=>{s.preventDefault(),this.releaseKey(i)})}),document.addEventListener("keydown",t=>{const i=this.keyMap[t.key.toLowerCase()];i&&!this.activeKeys.has(i)&&this.pressKey(i)}),document.addEventListener("keyup",t=>{const i=this.keyMap[t.key.toLowerCase()];i&&this.releaseKey(i)})}pressKey(e){if(this.activeKeys.has(e))return;console.log(`[KeyboardController] pressKey: ${e}`),this.activeKeys.add(e);const t=this.container.querySelector(`[data-note="${e}"]`);t&&t.classList.add("active");const i=this.noteToFrequency(e);try{this.onNoteOn(e,i)}catch(s){console.error("[KeyboardController] Error calling onNoteOn:",s)}}releaseKey(e){if(!this.activeKeys.has(e))return;this.activeKeys.delete(e);const t=this.container.querySelector(`[data-note="${e}"]`);t&&t.classList.remove("active"),this.onNoteOff(e)}releaseAllKeys(){this.activeKeys.forEach(e=>{this.releaseKey(e)})}}class k{constructor(e){this.canvas=e,this.ctx=this.canvas.getContext("2d"),this.isRunning=!1,this.resize(),window.addEventListener("resize",()=>this.resize())}resize(){const e=this.canvas.getBoundingClientRect();this.canvas.width=e.width,this.canvas.height=e.height}start(e){this.isRunning=!0,this.dataFunction=e,this.draw()}stop(){this.isRunning=!1}draw(){if(!this.isRunning)return;const e=this.dataFunction();if(this.ctx.fillStyle="#1a1a1a",this.ctx.fillRect(0,0,this.canvas.width,this.canvas.height),e){this.ctx.lineWidth=2,this.ctx.strokeStyle="#4a9eff",this.ctx.beginPath();const t=this.canvas.width/e.length;let i=0;for(let s=0;sthis.draw())}}class p{constructor(){this.processor=null,this.keyboard=null,this.visualizer=null,this.knobs={},this.isActive=!1,this.currentNote=null,this.initializeUI()}initializeUI(){document.getElementById("power-button").addEventListener("click",()=>this.togglePower()),this.knobs.breath=new r(document.getElementById("breath-knob"),document.getElementById("breath-value"),t=>this.updateParameter("breath",t),0,100,70),this.knobs.reed=new r(document.getElementById("reed-knob"),document.getElementById("reed-value"),t=>this.updateParameter("reed",t),0,100,50),this.knobs.noise=new r(document.getElementById("noise-knob"),document.getElementById("noise-value"),t=>this.updateParameter("noise",t),0,100,15),this.knobs.attack=new r(document.getElementById("attack-knob"),document.getElementById("attack-value"),t=>this.updateParameter("attack",t),0,100,10),this.knobs.damping=new r(document.getElementById("damping-knob"),document.getElementById("damping-value"),t=>this.updateParameter("damping",t),0,100,20),this.knobs.brightness=new r(document.getElementById("brightness-knob"),document.getElementById("brightness-value"),t=>this.updateParameter("brightness",t),0,100,70),this.knobs.vibrato=new r(document.getElementById("vibrato-knob"),document.getElementById("vibrato-value"),t=>this.updateParameter("vibrato",t),0,100,0),this.knobs.release=new r(document.getElementById("release-knob"),document.getElementById("release-value"),t=>this.updateParameter("release",t),0,100,50),this.keyboard=new b(document.getElementById("keyboard"),(t,i)=>this.handleNoteOn(t,i),t=>this.handleNoteOff(t)),this.visualizer=new k(document.getElementById("visualizer")),this.updateStatus()}async togglePower(){this.isActive?this.powerOff():await this.powerOn()}async powerOn(){try{this.processor=new w,await this.processor.initialize(),console.log(`main this.processor.isActive = ${this.processor.isActive}`),this.updateParameter("breath",this.knobs.breath.value/100),this.updateParameter("reed",this.knobs.reed.value/100),this.updateParameter("noise",this.knobs.noise.value/100),this.updateParameter("attack",this.knobs.attack.value/100),this.updateParameter("damping",this.knobs.damping.value/100),this.updateParameter("brightness",this.knobs.brightness.value/100),this.updateParameter("vibrato",this.knobs.vibrato.value/100),this.updateParameter("release",this.knobs.release.value/100),this.visualizer.start(()=>this.processor.getAnalyserData()),this.isActive=!0,document.getElementById("power-button").classList.add("active"),this.updateStatus(),console.log("Clarinet synthesizer powered on")}catch(e){console.error("Failed to initialize audio:",e),alert("Failed to initialize audio. Please check browser compatibility.")}}powerOff(){this.processor&&(this.keyboard.releaseAllKeys(),this.processor.shutdown(),this.processor=null),this.visualizer.stop(),this.isActive=!1,this.currentNote=null,document.getElementById("power-button").classList.remove("active"),this.updateStatus(),console.log("Clarinet synthesizer powered off")}handleNoteOn(e,t){this.isActive&&(console.log(`[ClarinetSynthApp] handleNoteOn: ${e}, ${t}`),this.currentNote=e,this.processor.noteOn(t),this.updateStatus())}handleNoteOff(e){this.isActive&&this.currentNote===e&&(this.processor.noteOff(),this.currentNote=null,this.updateStatus())}updateParameter(e,t){console.log(`[ClarinetSynthApp] updateParameter: ${e}, ${t}`),this.processor&&this.processor.isActive&&this.processor.setParameter(e,t)}updateStatus(){const e=document.getElementById("status"),t=document.getElementById("current-note");e.textContent=this.isActive?"ON":"OFF",e.style.color=this.isActive?"#4eff4a":"#ff4a4a",t.textContent=this.currentNote||"---",this.isActive?this.updateCPU():document.getElementById("cpu").textContent="0%"}updateCPU(){const e=this.processor&&this.processor.engine&&this.processor.engine.isPlaying?Math.floor(15+Math.random()*10):Math.floor(5+Math.random()*5);document.getElementById("cpu").textContent=e+"%",this.isActive&&setTimeout(()=>this.updateCPU(),100)}}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>{window.synthApp=new p}):window.synthApp=new p;window.addEventListener("beforeunload",()=>{window.synthApp&&window.synthApp.isActive&&window.synthApp.powerOff()}); diff --git a/www/clarinet-synth/assets/index-CtIm5pJl.js b/www/clarinet-synth/assets/index-CtIm5pJl.js deleted file mode 100644 index e981c81..0000000 --- a/www/clarinet-synth/assets/index-CtIm5pJl.js +++ /dev/null @@ -1 +0,0 @@ -(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))s(i);new MutationObserver(i=>{for(const n of i)if(n.type==="childList")for(const a of n.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&s(a)}).observe(document,{childList:!0,subtree:!0});function t(i){const n={};return i.integrity&&(n.integrity=i.integrity),i.referrerPolicy&&(n.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?n.credentials="include":i.crossOrigin==="anonymous"?n.credentials="omit":n.credentials="same-origin",n}function s(i){if(i.ep)return;i.ep=!0;const n=t(i);fetch(i.href,n)}})();class v{constructor(e=44100){this.sampleRate=e,this.delayLine=null,this.delayLength=100,this.readPos=0,this.writePos=0,this.breathPressure=.7,this.reedStiffness=.5,this.noiseLevel=.15,this.lpf={x1:0,y1:0,cutoff:.7},this.hpf={x1:0,y1:0,cutoff:.01},this.envelope=0,this.attackTime=.01,this.releaseTime=.05,this.gate=!1,this.vibratoAmount=0,this.vibratoRate=5,this.vibratoPhase=0,this.frequency=440,this.targetFrequency=440,this.isPlaying=!1}setFrequency(e){this.targetFrequency=e;const t=Math.floor(this.sampleRate/e);(!this.delayLine||t!==this.delayLength)&&(this.delayLength=t,this.delayLine=new Float32Array(this.delayLength),this.delayLine.fill(0),this.readPos=0,this.writePos=0)}reedReflection(e){const t=this.reedStiffness*5+.5,s=e*t;if(s>3)return 1;if(s<-3)return-1;const i=s*s;return s*(27+i)/(27+9*i)}lowpass(e,t){const s=t;return this.lpf.y1=s*e+(1-s)*this.lpf.y1,this.lpf.y1}highpass(e,t){const i=t*(this.hpf.y1+e-this.hpf.x1);return this.hpf.x1=e,this.hpf.y1=i,i}saturate(e){if(e>3)return 1;if(e<-3)return-1;const t=e*e;return e*(27+t)/(27+9*t)}generateNoise(){return(Math.random()*2-1)*this.noiseLevel}updateEnvelope(e){if(this.gate){const t=1/(this.attackTime*this.sampleRate);this.envelope+=t*e,this.envelope>1&&(this.envelope=1)}else{const t=1/(this.releaseTime*this.sampleRate);this.envelope-=t*e,this.envelope<0&&(this.envelope=0,this.isPlaying=!1)}return this.envelope}process(){if(!this.delayLine||this.delayLength===0)return 0;const e=this.updateEnvelope(1);if(e<=.001)return 0;this.vibratoPhase+=this.vibratoRate*2*Math.PI/this.sampleRate,this.vibratoPhase>2*Math.PI&&(this.vibratoPhase-=2*Math.PI);const t=Math.sin(this.vibratoPhase)*this.vibratoAmount,i=this.sampleRate/this.targetFrequency*(1+t),n=Math.min(i,this.delayLength-1),d=(this.writePos-n+this.delayLength)%this.delayLength,l=Math.floor(d),c=d-l,f=(l+1)%this.delayLength,u=this.delayLine[l]*(1-c)+this.delayLine[f]*c,m=this.generateNoise(),g=this.breathPressure*e+m*e-u,y=this.reedReflection(g);let o=u+y*.5;return o=this.lowpass(o,this.lpf.cutoff),o=this.highpass(o,this.hpf.cutoff),o=this.saturate(o*.95),this.delayLine[this.writePos]=o,this.writePos=(this.writePos+1)%this.delayLength,o*e*.5}noteOn(e){if(this.setFrequency(e),this.gate=!0,this.isPlaying=!0,this.vibratoPhase=0,this.delayLine)for(let t=0;t{const s=t.outputBuffer.getChannelData(0);for(let i=0;i{this.isDragging=!0,this.startY=e.clientY,this.startValue=this.value,e.preventDefault()}),document.addEventListener("mousemove",e=>{if(this.isDragging){const t=(this.startY-e.clientY)*.5;this.value=Math.max(this.min,Math.min(this.max,this.startValue+t)),this.updateDisplay(),this.onChange(this.value/100)}}),document.addEventListener("mouseup",()=>{this.isDragging=!1}),this.element.addEventListener("touchstart",e=>{this.isDragging=!0,this.startY=e.touches[0].clientY,this.startValue=this.value,e.preventDefault()}),document.addEventListener("touchmove",e=>{if(this.isDragging){const t=(this.startY-e.touches[0].clientY)*.5;this.value=Math.max(this.min,Math.min(this.max,this.startValue+t)),this.updateDisplay(),this.onChange(this.value/100)}}),document.addEventListener("touchend",()=>{this.isDragging=!1}),this.element.addEventListener("dblclick",()=>{this.value=(this.min+this.max)/2,this.updateDisplay(),this.onChange(this.value/100)})}updateDisplay(){const e=this.value/100*270-135;this.element.style.transform=`rotate(${e}deg)`,this.valueElement.textContent=Math.round(this.value)}setValue(e){this.value=Math.max(this.min,Math.min(this.max,e)),this.updateDisplay()}}class b{constructor(e,t,s){this.container=e,this.onNoteOn=t,this.onNoteOff=s,this.activeKeys=new Set,this.keyMap=this.createKeyMap(),this.setupListeners()}createKeyMap(){return{a:"C4",w:"C#4",s:"D4",e:"D#4",d:"E4",f:"F4",t:"F#4",g:"G4",y:"G#4",h:"A4",u:"A#4",j:"B4",k:"C5"}}noteToFrequency(e){return{C4:261.63,"C#4":277.18,D4:293.66,"D#4":311.13,E4:329.63,F4:349.23,"F#4":369.99,G4:392,"G#4":415.3,A4:440,"A#4":466.16,B4:493.88,C5:523.25}[e]||440}setupListeners(){this.container.querySelectorAll(".key").forEach(t=>{const s=t.dataset.note;t.addEventListener("mousedown",i=>{i.preventDefault(),this.pressKey(s)}),t.addEventListener("mouseup",()=>{this.releaseKey(s)}),t.addEventListener("mouseleave",()=>{this.activeKeys.has(s)&&this.releaseKey(s)}),t.addEventListener("touchstart",i=>{i.preventDefault(),this.pressKey(s)}),t.addEventListener("touchend",i=>{i.preventDefault(),this.releaseKey(s)})}),document.addEventListener("keydown",t=>{const s=this.keyMap[t.key.toLowerCase()];s&&!this.activeKeys.has(s)&&this.pressKey(s)}),document.addEventListener("keyup",t=>{const s=this.keyMap[t.key.toLowerCase()];s&&this.releaseKey(s)})}pressKey(e){if(this.activeKeys.has(e))return;this.activeKeys.add(e);const t=this.container.querySelector(`[data-note="${e}"]`);t&&t.classList.add("active");const s=this.noteToFrequency(e);this.onNoteOn(e,s)}releaseKey(e){if(!this.activeKeys.has(e))return;this.activeKeys.delete(e);const t=this.container.querySelector(`[data-note="${e}"]`);t&&t.classList.remove("active"),this.onNoteOff(e)}releaseAllKeys(){this.activeKeys.forEach(e=>{this.releaseKey(e)})}}class k{constructor(e){this.canvas=e,this.ctx=this.canvas.getContext("2d"),this.isRunning=!1,this.resize(),window.addEventListener("resize",()=>this.resize())}resize(){const e=this.canvas.getBoundingClientRect();this.canvas.width=e.width,this.canvas.height=e.height}start(e){this.isRunning=!0,this.dataFunction=e,this.draw()}stop(){this.isRunning=!1}draw(){if(!this.isRunning)return;const e=this.dataFunction();if(this.ctx.fillStyle="#1a1a1a",this.ctx.fillRect(0,0,this.canvas.width,this.canvas.height),e){this.ctx.lineWidth=2,this.ctx.strokeStyle="#4a9eff",this.ctx.beginPath();const t=this.canvas.width/e.length;let s=0;for(let i=0;ithis.draw())}}class p{constructor(){this.processor=null,this.keyboard=null,this.visualizer=null,this.knobs={},this.isActive=!1,this.currentNote=null,this.initializeUI()}initializeUI(){document.getElementById("power-button").addEventListener("click",()=>this.togglePower()),this.knobs.breath=new r(document.getElementById("breath-knob"),document.getElementById("breath-value"),t=>this.updateParameter("breath",t),0,100,70),this.knobs.reed=new r(document.getElementById("reed-knob"),document.getElementById("reed-value"),t=>this.updateParameter("reed",t),0,100,50),this.knobs.noise=new r(document.getElementById("noise-knob"),document.getElementById("noise-value"),t=>this.updateParameter("noise",t),0,100,15),this.knobs.attack=new r(document.getElementById("attack-knob"),document.getElementById("attack-value"),t=>this.updateParameter("attack",t),0,100,10),this.knobs.damping=new r(document.getElementById("damping-knob"),document.getElementById("damping-value"),t=>this.updateParameter("damping",t),0,100,20),this.knobs.brightness=new r(document.getElementById("brightness-knob"),document.getElementById("brightness-value"),t=>this.updateParameter("brightness",t),0,100,70),this.knobs.vibrato=new r(document.getElementById("vibrato-knob"),document.getElementById("vibrato-value"),t=>this.updateParameter("vibrato",t),0,100,0),this.knobs.release=new r(document.getElementById("release-knob"),document.getElementById("release-value"),t=>this.updateParameter("release",t),0,100,50),this.keyboard=new b(document.getElementById("keyboard"),(t,s)=>this.handleNoteOn(t,s),t=>this.handleNoteOff(t)),this.visualizer=new k(document.getElementById("visualizer")),this.updateStatus()}async togglePower(){this.isActive?this.powerOff():await this.powerOn()}async powerOn(){try{this.processor=new w,await this.processor.initialize(),this.updateParameter("breath",this.knobs.breath.value/100),this.updateParameter("reed",this.knobs.reed.value/100),this.updateParameter("noise",this.knobs.noise.value/100),this.updateParameter("attack",this.knobs.attack.value/100),this.updateParameter("damping",this.knobs.damping.value/100),this.updateParameter("brightness",this.knobs.brightness.value/100),this.updateParameter("vibrato",this.knobs.vibrato.value/100),this.updateParameter("release",this.knobs.release.value/100),this.visualizer.start(()=>this.processor.getAnalyserData()),this.isActive=!0,document.getElementById("power-button").classList.add("active"),this.updateStatus(),console.log("Clarinet synthesizer powered on")}catch(e){console.error("Failed to initialize audio:",e),alert("Failed to initialize audio. Please check browser compatibility.")}}powerOff(){this.processor&&(this.keyboard.releaseAllKeys(),this.processor.shutdown(),this.processor=null),this.visualizer.stop(),this.isActive=!1,this.currentNote=null,document.getElementById("power-button").classList.remove("active"),this.updateStatus(),console.log("Clarinet synthesizer powered off")}handleNoteOn(e,t){this.isActive&&(this.currentNote=e,this.processor.noteOn(t),this.updateStatus())}handleNoteOff(e){this.isActive&&this.currentNote===e&&(this.processor.noteOff(),this.currentNote=null,this.updateStatus())}updateParameter(e,t){this.processor&&this.processor.isActive&&this.processor.setParameter(e,t)}updateStatus(){const e=document.getElementById("status"),t=document.getElementById("current-note");e.textContent=this.isActive?"ON":"OFF",e.style.color=this.isActive?"#4eff4a":"#ff4a4a",t.textContent=this.currentNote||"---",this.isActive?this.updateCPU():document.getElementById("cpu").textContent="0%"}updateCPU(){const e=this.processor&&this.processor.engine&&this.processor.engine.isPlaying?Math.floor(15+Math.random()*10):Math.floor(5+Math.random()*5);document.getElementById("cpu").textContent=e+"%",this.isActive&&setTimeout(()=>this.updateCPU(),100)}}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>{window.synthApp=new p}):window.synthApp=new p;window.addEventListener("beforeunload",()=>{window.synthApp&&window.synthApp.isActive&&window.synthApp.powerOff()}); diff --git a/www/clarinet-synth/index.html b/www/clarinet-synth/index.html index 34a67ed..480d824 100644 --- a/www/clarinet-synth/index.html +++ b/www/clarinet-synth/index.html @@ -239,7 +239,117 @@ height: 100%; } - + +