| 1 | // js/worklets/spectral-freeze.js |
| 2 | |
| 3 | class SpectralFreezeProcessor extends AudioWorkletProcessor { |
| 4 | constructor() { |
| 5 | super(); |
| 6 | this.frozenBuffer = null; |
| 7 | this.bufferIndex = 0; |
| 8 | this.fadeIn = 0; |
| 9 | this.fadeOut = 0; |
| 10 | this.isTransitioning = false; |
| 11 | |
| 12 | // Buffer size for spectral capture |
| 13 | this.bufferSize = 2048; |
| 14 | this.captureBuffer = new Float32Array(this.bufferSize); |
| 15 | this.captureIndex = 0; |
| 16 | |
| 17 | // Crossfade duration in samples |
| 18 | this.fadeLength = 128; |
| 19 | } |
| 20 | |
| 21 | static get parameterDescriptors() { |
| 22 | return [{ |
| 23 | name: 'freeze', |
| 24 | defaultValue: 0, |
| 25 | minValue: 0, |
| 26 | maxValue: 1, |
| 27 | automationRate: 'k-rate' |
| 28 | }]; |
| 29 | } |
| 30 | |
| 31 | process(inputs, outputs, parameters) { |
| 32 | const input = inputs[0]; |
| 33 | const output = outputs[0]; |
| 34 | |
| 35 | if (!input || !input[0]) { |
| 36 | return true; |
| 37 | } |
| 38 | |
| 39 | const freeze = parameters.freeze[0] || parameters.freeze; |
| 40 | const shouldFreeze = freeze > 0.5; |
| 41 | |
| 42 | for (let channel = 0; channel < output.length; channel++) { |
| 43 | const inputChannel = input[channel]; |
| 44 | const outputChannel = output[channel]; |
| 45 | |
| 46 | for (let i = 0; i < outputChannel.length; i++) { |
| 47 | const inputSample = inputChannel ? inputChannel[i] : 0; |
| 48 | |
| 49 | // Capture input into buffer |
| 50 | if (!shouldFreeze) { |
| 51 | this.captureBuffer[this.captureIndex] = inputSample; |
| 52 | this.captureIndex = (this.captureIndex + 1) % this.bufferSize; |
| 53 | } |
| 54 | |
| 55 | // Handle freeze state |
| 56 | if (shouldFreeze && !this.frozenBuffer) { |
| 57 | // Start freezing - copy capture buffer |
| 58 | this.frozenBuffer = new Float32Array(this.captureBuffer); |
| 59 | this.bufferIndex = 0; |
| 60 | this.isTransitioning = true; |
| 61 | this.fadeIn = 0; |
| 62 | } else if (!shouldFreeze && this.frozenBuffer) { |
| 63 | // Stop freezing |
| 64 | this.isTransitioning = true; |
| 65 | this.fadeOut = 0; |
| 66 | } |
| 67 | |
| 68 | // Generate output |
| 69 | if (this.frozenBuffer && shouldFreeze) { |
| 70 | // Output frozen spectrum with slight variation |
| 71 | const frozenSample = this.frozenBuffer[this.bufferIndex]; |
| 72 | const variation = 1 + (Math.random() - 0.5) * 0.02; // ±1% variation |
| 73 | let outputSample = frozenSample * variation; |
| 74 | |
| 75 | // Apply fade in |
| 76 | if (this.isTransitioning && this.fadeIn < this.fadeLength) { |
| 77 | const fadeGain = this.fadeIn / this.fadeLength; |
| 78 | outputSample = inputSample * (1 - fadeGain) + outputSample * fadeGain; |
| 79 | this.fadeIn++; |
| 80 | if (this.fadeIn >= this.fadeLength) { |
| 81 | this.isTransitioning = false; |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | outputChannel[i] = outputSample; |
| 86 | this.bufferIndex = (this.bufferIndex + 1) % this.bufferSize; |
| 87 | } else if (this.frozenBuffer && !shouldFreeze) { |
| 88 | // Fade out frozen buffer |
| 89 | const frozenSample = this.frozenBuffer[this.bufferIndex]; |
| 90 | const fadeGain = 1 - (this.fadeOut / this.fadeLength); |
| 91 | |
| 92 | outputChannel[i] = inputSample * (1 - fadeGain) + frozenSample * fadeGain; |
| 93 | |
| 94 | this.bufferIndex = (this.bufferIndex + 1) % this.bufferSize; |
| 95 | this.fadeOut++; |
| 96 | |
| 97 | if (this.fadeOut >= this.fadeLength) { |
| 98 | this.frozenBuffer = null; |
| 99 | this.isTransitioning = false; |
| 100 | } |
| 101 | } else { |
| 102 | // Pass through |
| 103 | outputChannel[i] = inputSample; |
| 104 | } |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | return true; |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | registerProcessor('spectral-freeze-processor', SpectralFreezeProcessor); |