@kidlib/web-audio 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +11 -6
- package/dist/index.js +26 -29
- package/dist/processors/processors.js +2 -20
- package/package.json +24 -24
package/dist/index.d.ts
CHANGED
|
@@ -559,6 +559,8 @@ export declare type KeymapKey = "piano" | "major" | "minor" | "pentatonic" | "ch
|
|
|
559
559
|
|
|
560
560
|
export declare const keymaps: Record<KeymapKey, KeyMap>;
|
|
561
561
|
|
|
562
|
+
declare type KnownNodeType = NativeAudioNode | BaseNodeType | InstrumentType | VoiceType | ContainerType | CustomFxType;
|
|
563
|
+
|
|
562
564
|
declare class LFO {
|
|
563
565
|
#private;
|
|
564
566
|
constructor(context: AudioContext);
|
|
@@ -667,7 +669,7 @@ declare class MacroParam {
|
|
|
667
669
|
protected sendMessage(type: string, data: any): void;
|
|
668
670
|
dispose(): void;
|
|
669
671
|
connect(target: AudioParam, nodeType: NodeType, scaleFactor?: number): this;
|
|
670
|
-
disconnect(
|
|
672
|
+
disconnect(_target?: AudioParam): void;
|
|
671
673
|
}
|
|
672
674
|
|
|
673
675
|
declare interface Message {
|
|
@@ -682,7 +684,7 @@ declare type NativeAudioNode = "AudioWorkletNode" | "GainNode" | "BiquadFilterNo
|
|
|
682
684
|
|
|
683
685
|
declare type NodeID_2 = string;
|
|
684
686
|
|
|
685
|
-
declare type NodeType =
|
|
687
|
+
declare type NodeType = KnownNodeType | (string & Record<never, never>);
|
|
686
688
|
|
|
687
689
|
declare type NormalizeOptions = {
|
|
688
690
|
from: [number, number];
|
|
@@ -815,7 +817,7 @@ declare const ROOT_NOTES: {
|
|
|
815
817
|
};
|
|
816
818
|
|
|
817
819
|
export declare interface SampleLoader {
|
|
818
|
-
loadSample(
|
|
820
|
+
loadSample(buffer: AudioBuffer | ArrayBuffer): Promise<AudioBuffer | null>;
|
|
819
821
|
}
|
|
820
822
|
|
|
821
823
|
export declare class SamplePlayer implements ILibInstrumentNode {
|
|
@@ -916,7 +918,7 @@ export declare class SamplePlayer implements ILibInstrumentNode {
|
|
|
916
918
|
setLoopPoint(loopPoint: "start" | "end", loopStartSeconds: number, loopEndSeconds: number, rampDuration?: number): this;
|
|
917
919
|
scrollLoopPoints(loopStart: number, loopEnd: number): this;
|
|
918
920
|
setParam(name: string, value: number): this;
|
|
919
|
-
applyParams(params:
|
|
921
|
+
applyParams(params: SamplerParams): this;
|
|
920
922
|
/** PARAM GETTERS */
|
|
921
923
|
getAudioParam(name: string): AudioParam | null;
|
|
922
924
|
getStartPoint(): number;
|
|
@@ -988,7 +990,10 @@ export declare interface SamplerParamDescriptor {
|
|
|
988
990
|
|
|
989
991
|
export declare type SamplerParamKey = keyof typeof samplerParams;
|
|
990
992
|
|
|
991
|
-
|
|
993
|
+
/** @deprecated Renamed to {@link SamplerParams}. Removed in the next minor. */
|
|
994
|
+
export declare type SamplerParamPatch = SamplerParams;
|
|
995
|
+
|
|
996
|
+
export declare type SamplerParams = Partial<SamplerParamValues>;
|
|
992
997
|
|
|
993
998
|
export declare const samplerParams: Record<"distortion" | "delayTime" | "feedback" | "feedbackDecay" | "drive" | "loopEnd" | "loopStart" | "volume" | "dryWet" | "glide" | "tempo" | "lowpassFilter" | "highpassFilter" | "feedbackPitch" | "feedbackLpf" | "clipping" | "amMod" | "reverbSend" | "reverbSize" | "delaySend" | "delayFeedback" | "gainLFORate" | "gainLFODepth" | "pitchLFORate" | "pitchLFODepth" | "trimStart" | "trimEnd" | "loopDuration" | "loopRampDuration" | "loopDurationDrift" | "keytrackLoop", SamplerParamDescriptor>;
|
|
994
999
|
|
|
@@ -1229,7 +1234,7 @@ declare class ValueSnapper {
|
|
|
1229
1234
|
setScale(rootNote: keyof typeof ROOT_NOTES, scalePattern: readonly number[] | number[], tuningOffset: number | undefined, // in semitones
|
|
1230
1235
|
lowestOctave: number | undefined, highestOctave: number | undefined, normalize: NormalizeOptions | false, snapToZeroCrossings?: number[] | false): number[];
|
|
1231
1236
|
setRootNote(rootNote: keyof typeof ROOT_NOTES): void;
|
|
1232
|
-
setAllowedPeriods(periods: number[], normalize: NormalizeOptions | false,
|
|
1237
|
+
setAllowedPeriods(periods: number[], normalize: NormalizeOptions | false, _snapToZeroCrossings?: number[] | false, _direction?: "left" | "right" | "any"): number[];
|
|
1233
1238
|
snapToValue(target: number, allowedValues?: number[], tolerance?: number, preferDirection?: "left" | "right" | "any"): number;
|
|
1234
1239
|
snapToMusicalPeriod(targetPeriod: number, allowedPeriods?: number[]): number;
|
|
1235
1240
|
setAllowedValues(values: number[], normalize: NormalizeOptions | false): number[];
|
package/dist/index.js
CHANGED
|
@@ -246,7 +246,7 @@ function m(e, t, n = {}) {
|
|
|
246
246
|
seed: n.seed
|
|
247
247
|
});
|
|
248
248
|
case "custom-function": return ie(e, n.waveFunction || ((e) => Math.sin(e)), { harmonics: n.harmonics });
|
|
249
|
-
default: throw Error(
|
|
249
|
+
default: throw Error("Invalid waveform type");
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
function h(e, t = {}) {
|
|
@@ -786,12 +786,13 @@ function Pe(e, t) {
|
|
|
786
786
|
return r.min + a * (r.max - r.min);
|
|
787
787
|
}
|
|
788
788
|
function Fe(e, t) {
|
|
789
|
-
let { inputRange: n, outputRange: r, blend: i = 1,
|
|
790
|
-
(e > n.max || e < n.min) && console.warn("
|
|
791
|
-
let
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
789
|
+
let { inputRange: n, outputRange: r, blend: i = 1, curve: a = "linear" } = t;
|
|
790
|
+
(e > n.max || e < n.min) && console.warn("interpolateLinearToGeometric: Value outside of input range, will be clamped"), r.min <= 0 && console.warn("interpolateLinearToGeometric: Output min must be > 0 for geometric interpolation");
|
|
791
|
+
let o = (Math.max(n.min, Math.min(e, n.max)) - n.min) / (n.max - n.min), s = Math.max(0, Math.min(i, 1)), c = typeof a == "number" ? a : a === "smooth" ? 2 : a === "steep" ? 3 : a === "gentle" ? 1.5 : 1;
|
|
792
|
+
if (c !== 1 && (o **= 1 / c), o === 0) return r.min;
|
|
793
|
+
if (o === 1) return r.max;
|
|
794
|
+
let l = r.min + o * (r.max - r.min), u = Math.log(r.min), d = Math.exp(u + o * (Math.log(r.max) - u));
|
|
795
|
+
return (1 - s) * l + s * d;
|
|
795
796
|
}
|
|
796
797
|
//#endregion
|
|
797
798
|
//#region src/utils/code/set-utils.ts
|
|
@@ -1035,15 +1036,12 @@ async function Je(e, t, n = {}) {
|
|
|
1035
1036
|
}
|
|
1036
1037
|
}
|
|
1037
1038
|
if (o?.enabled && (f = c(e, f, o.maxAmplitudePeak)), s?.enabled) {
|
|
1038
|
-
let t =
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
makeupGain: s.makeupGain ?? 1
|
|
1045
|
-
} : t.suggestedSettings, f = Ge(e, f, r.threshold, r.ratio, r.makeupGain);
|
|
1046
|
-
}
|
|
1039
|
+
let t = s.threshold !== void 0 || s.ratio !== void 0 || s.makeupGain !== void 0 ? {
|
|
1040
|
+
threshold: s.threshold ?? .5,
|
|
1041
|
+
ratio: s.ratio ?? 2,
|
|
1042
|
+
makeupGain: s.makeupGain ?? 1
|
|
1043
|
+
} : Ke(f).suggestedSettings;
|
|
1044
|
+
t && (f = Ge(e, f, t.threshold, t.ratio, t.makeupGain));
|
|
1047
1045
|
}
|
|
1048
1046
|
if (d?.detectPitch || d?.autotune || i && "auto" in i && i.auto) {
|
|
1049
1047
|
let e = await Xe(f), t = d?.targetMidiNote || 60, n = Ze(e.midiFloat, t);
|
|
@@ -1992,7 +1990,7 @@ var nt = class {
|
|
|
1992
1990
|
return this.#t.setAllowedPeriods(e, t, n);
|
|
1993
1991
|
}
|
|
1994
1992
|
setScale(e) {
|
|
1995
|
-
let { rootNote: t, scale: n, tuningOffset: r
|
|
1993
|
+
let { rootNote: t, scale: n, tuningOffset: r, lowestOctave: i, highestOctave: a } = e, o = Array.isArray(n) ? n : xe[n];
|
|
1996
1994
|
return this.#t.setScale(t, o, r, i, a, e.normalize, e.snapToZeroCrossings);
|
|
1997
1995
|
}
|
|
1998
1996
|
setValue(e, t) {
|
|
@@ -2738,8 +2736,8 @@ var ht = class {
|
|
|
2738
2736
|
this.#s = t, this.#e = !0;
|
|
2739
2737
|
}
|
|
2740
2738
|
trigger(e, t = {}) {
|
|
2741
|
-
let { secondsFromNow: n = 0, cents: r = 0,
|
|
2742
|
-
return this.setPitch(e, r,
|
|
2739
|
+
let { secondsFromNow: n = 0, cents: r = 0, glideTime: i = 0, triggerDecay: a = !0 } = t, o = this.now + n;
|
|
2740
|
+
return this.setPitch(e, r, o, i), a && this.#_(), this;
|
|
2743
2741
|
}
|
|
2744
2742
|
#g = 0;
|
|
2745
2743
|
setAmountMacro(e) {
|
|
@@ -3391,7 +3389,7 @@ var Z = {
|
|
|
3391
3389
|
this.#p = e;
|
|
3392
3390
|
}
|
|
3393
3391
|
trigger(e) {
|
|
3394
|
-
let { midiNote: t
|
|
3392
|
+
let { midiNote: t, velocity: n, secondsFromNow: r = 0 } = e, i = this.now + r;
|
|
3395
3393
|
if (this.#c === Z.PLAYING || this.#c === Z.RELEASING) return console.log(`had to stop a playing voice, midinote: ${t}`), this.stop(i), null;
|
|
3396
3394
|
this.#c = Z.PLAYING, this.#d = i, this.#u = t;
|
|
3397
3395
|
let a = (e.glide?.glideTime ?? this.#p) / 8, o = 1, s = 1;
|
|
@@ -3622,7 +3620,7 @@ var Z = {
|
|
|
3622
3620
|
}
|
|
3623
3621
|
};
|
|
3624
3622
|
connect(e, t, n) {
|
|
3625
|
-
return e instanceof Y ? this.out.connect(e.input, t) : e instanceof AudioParam ? this.out.connect(e, t) : e instanceof AudioNode ? this.out.connect(e, t, n) : console.warn(
|
|
3623
|
+
return e instanceof Y ? this.out.connect(e.input, t) : e instanceof AudioParam ? this.out.connect(e, t) : e instanceof AudioNode ? this.out.connect(e, t, n) : console.warn("SampleVoice: Unsupported destination", e), e;
|
|
3626
3624
|
}
|
|
3627
3625
|
disconnect(e = "main", t) {
|
|
3628
3626
|
return e === "alt" ? (console.warn("SampleVoice has no \"alt\" output to disconnect"), this) : (t ? (t instanceof AudioNode || t instanceof AudioParam) && this.out.disconnect(t) : this.out.disconnect(), this);
|
|
@@ -3815,7 +3813,6 @@ var Z = {
|
|
|
3815
3813
|
max: 1
|
|
3816
3814
|
},
|
|
3817
3815
|
blend: 1,
|
|
3818
|
-
logBase: "dB",
|
|
3819
3816
|
curve: "linear"
|
|
3820
3817
|
});
|
|
3821
3818
|
return this.setParam("loopDurationDriftAmount", t, this.now), this;
|
|
@@ -4169,7 +4166,7 @@ var Ct = class e {
|
|
|
4169
4166
|
return this.#t;
|
|
4170
4167
|
}
|
|
4171
4168
|
#R() {
|
|
4172
|
-
return this.voicePool.onMessage("sample:loaded", (
|
|
4169
|
+
return this.voicePool.onMessage("sample:loaded", () => {
|
|
4173
4170
|
this.#r = !0;
|
|
4174
4171
|
}), this.voicePool.onMessage("voice-pool:initialized", () => {
|
|
4175
4172
|
this.sendUpstreamMessage("sample-player:initialized", {});
|
|
@@ -4197,20 +4194,20 @@ var Ct = class e {
|
|
|
4197
4194
|
switch (e) {
|
|
4198
4195
|
case "loopStart": return this.#_.audioParam;
|
|
4199
4196
|
case "loopEnd": return this.#v.audioParam;
|
|
4200
|
-
default: throw Error(
|
|
4197
|
+
default: throw Error("Unknown macro parameter");
|
|
4201
4198
|
}
|
|
4202
4199
|
}
|
|
4203
4200
|
getMacro(e) {
|
|
4204
4201
|
switch (e) {
|
|
4205
4202
|
case "loopStart": return this.#_;
|
|
4206
4203
|
case "loopEnd": return this.#v;
|
|
4207
|
-
default: throw Error(
|
|
4204
|
+
default: throw Error("Unknown macro parameter");
|
|
4208
4205
|
}
|
|
4209
4206
|
}
|
|
4210
4207
|
#z() {
|
|
4211
|
-
return this.voicePool.allVoices.forEach((e
|
|
4212
|
-
let
|
|
4213
|
-
|
|
4208
|
+
return this.voicePool.allVoices.forEach((e) => {
|
|
4209
|
+
let t = e.getParam("loopStart"), n = e.getParam("loopEnd");
|
|
4210
|
+
t ? this.#_.addTarget(t, "loopStart") : console.error("loopStart param is null!"), n ? this.#v.addTarget(n, "loopEnd") : console.error("loopEnd param is null!");
|
|
4214
4211
|
}), this;
|
|
4215
4212
|
}
|
|
4216
4213
|
#B() {
|
|
@@ -4683,7 +4680,7 @@ var Ct = class e {
|
|
|
4683
4680
|
console.error(`Error disposing Sampler ${this.nodeId}:`, e);
|
|
4684
4681
|
}
|
|
4685
4682
|
}
|
|
4686
|
-
}, wt = "//#region src/utils/search/findClosest.ts\n/**\n* Generic binary search that finds the closest element using a custom comparison function\n* @param sortedArray - Array sorted according to the compareValue function\n* @param target - Target value to search for\n* @param getValue - Function to extract comparison value from array elements (defaults to identity for number arrays)\n* @param getDistance - Optional function to calculate distance (defaults to absolute difference)\n* @returns The array index of the element which value is closest to the target value\n*/\nfunction findClosestIdx(sortedArray, target, direction = \"any\", getValue = (x) => x, getDistance = (a, b) => Math.abs(a - b)) {\n if (sortedArray.length === 0) throw new Error(\"Array cannot be empty\");\n if (sortedArray.length === 1) return 0;\n const targetValue = target;\n const firstValue = getValue(sortedArray[0]);\n const lastValue = getValue(sortedArray[sortedArray.length - 1]);\n if (targetValue <= firstValue) return 0;\n if (targetValue >= lastValue) return sortedArray.length - 1;\n let left = 0;\n let right = sortedArray.length - 1;\n while (left < right - 1) {\n const mid = Math.floor((left + right) / 2);\n const midValue = getValue(sortedArray[mid]);\n if (midValue === targetValue) return mid;\n else if (midValue < targetValue) left = mid;\n else right = mid;\n }\n if (direction === \"left\") return left;\n if (direction === \"right\") return right;\n return getDistance(getValue(sortedArray[left]), targetValue) <= getDistance(getValue(sortedArray[right]), targetValue) ? left : right;\n}\n/**\n* Generic binary search that finds the closest element using a custom comparison function\n* @param sortedArray - Array sorted according to the compareValue function\n* @param target - Target value to search for\n* @param getValue - Function to extract comparison value from array elements (defaults to identity for number arrays)\n* @param getDistance - Optional function to calculate distance (defaults to absolute difference)\n* @returns The array element which value is closest to the target value\n*/\nfunction findClosest(sortedArray, target, direction = \"any\", getValue = (x) => x, getDistance = (a, b) => Math.abs(a - b)) {\n return sortedArray[findClosestIdx(sortedArray, target, direction, getValue, getDistance)];\n}\nvar SAMPLE_PLAYER_WORKLET_AUDIOPARAM_DESCRIPTORS = Object.values({\n masterGain: {\n name: \"masterGain\",\n defaultValue: 1,\n minValue: 0,\n maxValue: 2,\n automationRate: \"k-rate\"\n },\n envGain: {\n name: \"envGain\",\n defaultValue: 0,\n minValue: 0,\n maxValue: 1,\n automationRate: \"a-rate\"\n },\n velocity: {\n name: \"velocity\",\n defaultValue: 100,\n minValue: 0,\n maxValue: 127,\n automationRate: \"k-rate\"\n },\n pan: {\n name: \"pan\",\n defaultValue: 0,\n minValue: -1,\n maxValue: 1,\n automationRate: \"k-rate\"\n },\n playbackRate: {\n name: \"playbackRate\",\n defaultValue: 1,\n minValue: .1,\n maxValue: 24,\n automationRate: \"a-rate\"\n },\n loopStart: {\n name: \"loopStart\",\n defaultValue: 0,\n minValue: 0,\n maxValue: 99999,\n automationRate: \"k-rate\"\n },\n loopEnd: {\n name: \"loopEnd\",\n defaultValue: 99999,\n minValue: 0,\n maxValue: 99999,\n automationRate: \"k-rate\"\n },\n startPoint: {\n name: \"startPoint\",\n defaultValue: 0,\n minValue: 0,\n maxValue: 9999,\n automationRate: \"k-rate\"\n },\n endPoint: {\n name: \"endPoint\",\n defaultValue: 9999,\n minValue: 0,\n maxValue: 9999,\n automationRate: \"k-rate\"\n },\n playbackPosition: {\n name: \"playbackPosition\",\n defaultValue: 0,\n minValue: 0,\n maxValue: 99999,\n automationRate: \"k-rate\"\n },\n loopDurationDriftAmount: {\n name: \"loopDurationDriftAmount\",\n defaultValue: 0,\n minValue: 0,\n maxValue: 1,\n automationRate: \"k-rate\"\n },\n maxLoopCount: {\n name: \"maxLoopCount\",\n defaultValue: 999999,\n minValue: 1,\n maxValue: 999999,\n automationRate: \"k-rate\"\n },\n tempo: {\n name: \"tempo\",\n defaultValue: 120,\n minValue: 20,\n maxValue: 300,\n automationRate: \"k-rate\"\n }\n});\n//#endregion\n//#region src/worklets/processors/play/sample-player-processor.js\nvar SamplePlayerProcessor = class extends AudioWorkletProcessor {\n static get parameterDescriptors() {\n return SAMPLE_PLAYER_WORKLET_AUDIOPARAM_DESCRIPTORS;\n }\n constructor() {\n super();\n this.layers = [];\n this.layerGain = 1;\n this.minZeroCrossing = 0;\n this.maxZeroCrossing = 0;\n this.usePlaybackPosition = false;\n this.enableLoopSmoothing = true;\n this.enableAdaptiveDrift = true;\n this.enableAmplitudeCompensation = true;\n this.syncLoopToTempo = false;\n this.keytrackLoopAmount = 0;\n this.durationPreservation = {\n enabled: false,\n maxDriftSamples: Math.floor(sampleRate * .04),\n timelinePosition: 0,\n resetPending: false\n };\n this.PITCH_PRESERVATION_THRESHOLD = Math.floor(sampleRate * .061);\n this.AMPLITUDE_COMPENSATION_THRESHOLD = Math.floor(sampleRate / 16.35);\n this.port.onmessage = this.#handleMessage.bind(this);\n this.#resetState();\n this.port.postMessage({ type: \"initialized\" });\n }\n /** Authority layer. All range and duration math reads through this. */\n get buffer() {\n return this.layers[0] ?? null;\n }\n #handleMessage(event) {\n const { type, value, buffer, layers, timestamp, durationSeconds, zeroCrossings, allowedPeriods, playbackDirection } = event.data;\n switch (type) {\n case \"voice:reset\":\n this.#resetState();\n this.port.postMessage({ type: \"voice:reset\" });\n break;\n case \"voice:setBuffer\":\n case \"voice:setLayers\":\n this.#resetState();\n this.zeroCrossings = [];\n this.minZeroCrossing = 0;\n this.maxZeroCrossing = 0;\n this.layers = (layers ?? (buffer ? [buffer] : [])).filter(Boolean);\n this.layerGain = this.layers.length ? 1 / this.layers.length : 1;\n this.port.postMessage({\n type: \"voice:loaded\",\n durationSeconds,\n time: currentTime\n });\n break;\n case \"voice:setZeroCrossings\":\n this.zeroCrossings = (zeroCrossings || []).map((timeSec) => timeSec * sampleRate);\n if (this.zeroCrossings.length > 0) {\n this.minZeroCrossing = this.zeroCrossings[0];\n this.maxZeroCrossing = this.zeroCrossings[this.zeroCrossings.length - 1];\n }\n break;\n case \"voice:start\":\n this.isReleasing = false;\n this.isPlaying = true;\n this.loopCount = 0;\n this.playbackPosition = 0;\n this.port.postMessage({\n type: \"voice:started\",\n time: timestamp || currentTime\n });\n break;\n case \"voice:release\":\n this.isReleasing = true;\n this.port.postMessage({\n type: \"voice:releasing\",\n time: currentTime\n });\n break;\n case \"voice:stop\":\n this.#stop();\n break;\n case \"setLoopEnabled\":\n this.loopEnabled = value;\n this.port.postMessage({\n type: \"loop:enabled\",\n enabled: value\n });\n break;\n case \"setPanDriftEnabled\":\n this.panDriftEnabled = value;\n break;\n case \"voice:setPlaybackDirection\": {\n const reverse = playbackDirection === \"reverse\";\n if (reverse !== this.reversePlayback && this.playbackPosition > 0) this.playbackPosition += reverse ? 1 : -1;\n this.reversePlayback = reverse;\n this.port.postMessage({\n type: \"voice:playbackDirectionChange\",\n playbackDirection\n });\n break;\n }\n case \"voice:usePlaybackPosition\":\n this.usePlaybackPosition = value;\n break;\n case \"syncLoopToTempo\":\n this.syncLoopToTempo = value;\n this.port.postMessage({\n type: \"loop:syncToTempo\",\n enabled: value\n });\n break;\n case \"setKeytrackLoopAmount\":\n this.keytrackLoopAmount = Math.max(0, Math.min(1, value));\n break;\n case \"setPreserveDuration\":\n this.durationPreservation.enabled = Boolean(value);\n this.#resetDurationPreservation(this.playbackPosition);\n }\n }\n #resetState() {\n this.isPlaying = false;\n this.isReleasing = false;\n this.loopEnabled = false;\n this.velocitySensitivity = 1;\n this.reversePlayback = false;\n this.playbackPosition = 0;\n this.debugCounter = 0;\n this.loopCount = 0;\n this.applyClickCompensation = false;\n this.loopClickCompensation = 0;\n this.driftUpdateCounter = 0;\n this.currentLoopDrift = 0;\n this.currentPanDrift = 0;\n this.panDriftEnabled = true;\n this.nextDriftGenerated = false;\n this.loopAmplitudeGain = 1;\n this.lastAnalyzedLoopStart = -1;\n this.lastAnalyzedLoopEnd = -1;\n this.#resetDurationPreservation();\n }\n #stop() {\n this.isPlaying = false;\n this.isReleasing = false;\n this.playbackPosition = 0;\n this.port.postMessage({ type: \"voice:stopped\" });\n }\n #smoothLoopWrap(lastLoopSample, newFirstSample) {\n const discontinuity = lastLoopSample - newFirstSample;\n if (this.enableLoopSmoothing && Math.abs(discontinuity) > .01) {\n this.loopClickCompensation = discontinuity * .5;\n this.compensationDecay = .9;\n this.applyClickCompensation = true;\n }\n }\n #clamp = (value, min, max) => Math.max(min, Math.min(max, value));\n #clampZeroCrossing = (value) => this.#clamp(value, this.minZeroCrossing, this.maxZeroCrossing);\n #findNearestZeroCrossing(position, direction = \"any\", maxDistance = null) {\n if (!this.zeroCrossings || this.zeroCrossings.length === 0) return position;\n const closestValue = findClosest(this.zeroCrossings, position, direction);\n if (maxDistance !== null && Math.abs(closestValue - position) > maxDistance) return position;\n return closestValue;\n }\n /**\n * Convert normalized position (0-1) to sample index\n * @param {number} normalizedPosition - Position as 0-1 value\n * @returns {number} - Sample index\n */\n #normalizedToSamples(normalizedPosition) {\n if (!this.buffer || !this.buffer[0]) return 0;\n return normalizedPosition * this.buffer[0].length;\n }\n /**\n * Convert sample index to normalized position (0-1)\n * @param {number} sampleIndex - Sample index\n * @returns {number} - Normalized position 0-1\n */\n #samplesToNormalized(sampleIndex) {\n if (!this.buffer || !this.buffer[0]) return 0;\n return sampleIndex / this.buffer[0].length;\n }\n /**\n * Convert MIDI velocity (0-127) to gain multiplier (0-1)\n * @param {number} midiVelocity - MIDI velocity 0-127\n * @returns {number} - Gain multiplier 0-1\n */\n #midiVelocityToGain(midiVelocity) {\n return Math.max(0, Math.min(1, midiVelocity / 127));\n }\n /**\n * Get buffer duration in seconds\n * @returns {number} - Buffer duration in seconds\n */\n #getBufferDurationSeconds() {\n return (this.buffer?.[0]?.length || 0) / sampleRate;\n }\n /**\n * Calculate musical note durations in samples for given tempo\n * @param {number} tempo - BPM\n * @returns {Object} - Musical note durations in samples\n */\n #getMusicalNoteDurations(tempo) {\n const beatsPerSecond = tempo / 60;\n const samplesPerBeat = sampleRate / beatsPerSecond;\n return {\n whole: samplesPerBeat * 4,\n half: samplesPerBeat * 2,\n quarter: samplesPerBeat,\n eighth: samplesPerBeat / 2,\n sixteenth: samplesPerBeat / 4,\n thirtySecond: samplesPerBeat / 8,\n quarterTriplet: samplesPerBeat * 2 / 3,\n eighthTriplet: samplesPerBeat / 2 * 2 / 3,\n sixteenthTriplet: samplesPerBeat / 4 * 2 / 3\n };\n }\n /**\n * Quantize loop duration to nearest musical interval (skips if below the smallest quantize option)\n * @param {number} loopDurationSamples - Current loop duration in samples\n * @param {number} tempo - Current tempo in BPM\n * @param {number} playbackRate - Current playback rate\n * @returns {number} - Quantized loop duration in samples\n */\n #quantizeLoopDuration(loopDurationSamples, tempo, playbackRate) {\n if (!this.syncLoopToTempo) return loopDurationSamples;\n const noteDurations = this.#getMusicalNoteDurations(tempo);\n const effectiveDuration = loopDurationSamples / Math.abs(playbackRate);\n if (effectiveDuration < noteDurations.thirtySecond) return loopDurationSamples;\n const intervals = Object.values(noteDurations);\n let closestInterval = intervals[0];\n let smallestDiff = Math.abs(effectiveDuration - closestInterval);\n for (const interval of intervals) {\n const diff = Math.abs(effectiveDuration - interval);\n if (diff < smallestDiff) {\n smallestDiff = diff;\n closestInterval = interval;\n }\n }\n return Math.floor(closestInterval * Math.abs(playbackRate));\n }\n /**\n * Extract and convert all position parameters from seconds to samples\n * @param {Object} parameters - AudioWorkletProcessor parameters\n * @returns {Object} - Converted parameters in samples\n */\n #extractPositionParams(parameters) {\n return {\n startPointSamples: Math.floor(parameters.startPoint[0] * sampleRate),\n endPointSamples: Math.floor(parameters.endPoint[0] * sampleRate),\n loopStartSamples: Math.floor(parameters.loopStart[0] * sampleRate),\n loopEndSamples: Math.floor(parameters.loopEnd[0] * sampleRate)\n };\n }\n /**\n * Calculate effective playback range in samples\n * @param {Object} params - Position parameters from #extractPositionParams\n * @returns {Object} - Effective start and end positions\n */\n #calculatePlaybackRange(params) {\n const bufferLength = this.buffer?.[0]?.length || 0;\n const start = Math.max(0, params.startPointSamples);\n const end = params.endPointSamples > start ? Math.min(bufferLength, params.endPointSamples) : bufferLength;\n const snappedStart = this.#findNearestZeroCrossing(start, \"right\");\n const snappedEnd = this.#findNearestZeroCrossing(end, \"left\");\n return {\n startSamples: snappedStart,\n endSamples: snappedEnd,\n durationSamples: snappedEnd - snappedStart\n };\n }\n /**\n * Calculate effective loop range in samples with optional drift\n * @param {Object} params - Position parameters from #extractPositionParams\n * @param {Object} playbackRange - Range from #calculatePlaybackRange\n * @param {number} driftAmount - Loop duration drift amount (0-1)\n * @param {number} tempo - Current tempo in BPM\n * @param {number} playbackRate - Current playback rate\n * @returns {Object} - Effective loop start and end positions with drift applied\n */\n #calculateLoopRange(params, playbackRange, driftAmount = 0, tempo = 120, playbackRate = 1) {\n const lpStart = params.loopStartSamples;\n const lpEnd = params.loopEndSamples;\n let calcLoopStart = lpStart < lpEnd && lpStart >= 0 ? lpStart : playbackRange.startSamples;\n let calcLoopEnd = lpEnd > lpStart && lpEnd <= playbackRange.endSamples ? lpEnd : playbackRange.endSamples;\n let baseDuration = calcLoopEnd - calcLoopStart;\n if (this.syncLoopToTempo) {\n const quantizedDuration = this.#quantizeLoopDuration(baseDuration, tempo, playbackRate);\n calcLoopEnd = calcLoopStart + quantizedDuration;\n calcLoopEnd = Math.min(calcLoopEnd, playbackRange.endSamples);\n }\n if (baseDuration > this.PITCH_PRESERVATION_THRESHOLD && this.keytrackLoopAmount > 0 && !this.syncLoopToTempo) {\n const scale = 1 + this.keytrackLoopAmount * (Math.abs(playbackRate) - 1);\n baseDuration = Math.max(1, Math.floor(baseDuration * scale));\n calcLoopEnd = calcLoopStart + baseDuration;\n }\n baseDuration = calcLoopEnd - calcLoopStart;\n if (baseDuration > this.PITCH_PRESERVATION_THRESHOLD) calcLoopStart = this.#findNearestZeroCrossing(calcLoopStart, \"right\");\n if (driftAmount > 0 && this.loopEnabled) {\n if (!this.nextDriftGenerated || this.loopCount === 0) {\n const updateInterval = baseDuration <= this.PITCH_PRESERVATION_THRESHOLD ? Math.max(1, Math.floor(this.PITCH_PRESERVATION_THRESHOLD / baseDuration)) : 1;\n if (this.driftUpdateCounter % updateInterval === 0) {\n this.currentLoopDrift = this.#generateLoopDrift(driftAmount, baseDuration);\n if (this.panDriftEnabled && driftAmount > 0 && this.loopCount > 0) {\n const panDriftAmountScalar = 1e-4;\n this.currentPanDrift = this.currentLoopDrift * panDriftAmountScalar;\n } else this.currentPanDrift = 0;\n }\n this.driftUpdateCounter++;\n this.nextDriftGenerated = true;\n }\n const driftedLoopEnd = calcLoopEnd + this.currentLoopDrift;\n const minLoopDuration = Math.max(1, Math.floor(baseDuration * .1));\n const maxLoopEnd = Math.max(playbackRange.endSamples, calcLoopEnd);\n calcLoopEnd = Math.max(calcLoopStart + minLoopDuration, Math.min(maxLoopEnd, driftedLoopEnd));\n } else this.currentPanDrift = 0;\n if (baseDuration > this.PITCH_PRESERVATION_THRESHOLD && calcLoopEnd <= playbackRange.endSamples) calcLoopEnd = Math.max(calcLoopStart + 1, this.#findNearestZeroCrossing(calcLoopEnd, \"left\"));\n const loopDuration = calcLoopEnd - calcLoopStart;\n return {\n loopStartSamples: calcLoopStart,\n loopEndSamples: calcLoopEnd,\n loopDurationSamples: loopDuration\n };\n }\n #getSafeParam(paramArray, index, isConstant) {\n return isConstant ? paramArray[0] : paramArray[Math.min(index, paramArray.length - 1)];\n }\n #getConstantFlags(parameters) {\n this.constantFlags ??= {\n envGain: true,\n playbackRate: true\n };\n this.constantFlags.envGain = parameters.envGain.length === 1;\n this.constantFlags.playbackRate = parameters.playbackRate.length === 1;\n return this.constantFlags;\n }\n #resetDurationPreservation(position = 0) {\n this.durationPreservation.timelinePosition = position;\n this.durationPreservation.resetPending = false;\n }\n #isDurationPreservationActive(loopRange) {\n return this.durationPreservation.enabled && Boolean(this.zeroCrossings?.length) && (!this.loopEnabled || loopRange.loopDurationSamples > this.PITCH_PRESERVATION_THRESHOLD);\n }\n #prepareDurationPreservingSample(playbackRate, loopRange) {\n const state = this.durationPreservation;\n if (!this.#isDurationPreservationActive(loopRange)) return null;\n if (Math.abs(this.playbackPosition - state.timelinePosition) > state.maxDriftSamples) state.resetPending = true;\n if (!state.resetPending) return null;\n const direction = playbackRate < 0 ? \"left\" : \"right\";\n const outgoingZero = this.#findNearestZeroCrossing(this.playbackPosition, direction);\n if (Math.abs(outgoingZero - this.playbackPosition) > Math.abs(playbackRate)) return null;\n this.playbackPosition = outgoingZero;\n state.resetPending = false;\n return this.#findNearestZeroCrossing(state.timelinePosition, \"any\", state.maxDriftSamples);\n }\n #advanceDurationPreservingPlayback(playbackRate, resetTarget, loopRange, canWrapLoop) {\n const state = this.durationPreservation;\n this.playbackPosition = resetTarget === null ? this.playbackPosition + playbackRate : resetTarget;\n if (this.#isDurationPreservationActive(loopRange)) {\n state.timelinePosition += playbackRate < 0 ? -1 : 1;\n if (canWrapLoop && playbackRate >= 0 && state.timelinePosition >= loopRange.loopEndSamples) state.timelinePosition = loopRange.loopStartSamples;\n else if (canWrapLoop && playbackRate < 0 && state.timelinePosition <= loopRange.loopStartSamples) state.timelinePosition = loopRange.loopEndSamples - 1;\n } else this.#resetDurationPreservation(this.playbackPosition);\n }\n /**\n * Generate a new drift amount for the current loop iteration\n * @param {number} driftAmount - Maximum drift amount (0-1)\n * @param {number} baseDuration - Base loop duration in samples\n * @returns {number} - Drift amount in samples\n */\n #generateLoopDrift(driftAmount, baseDuration) {\n if (driftAmount <= 0) return 0;\n const randomFactor = (Math.random() - .5) * 2;\n let effectiveDriftAmount = driftAmount;\n if (this.enableAdaptiveDrift) {\n const shortThreshold = 1024;\n const longThreshold = 8192;\n if (baseDuration < shortThreshold) effectiveDriftAmount *= .1;\n else if (baseDuration < longThreshold) {\n const scaleFactor = .1 + .9 * (baseDuration - shortThreshold) / 7168;\n effectiveDriftAmount *= scaleFactor;\n }\n }\n const maxDriftSamples = effectiveDriftAmount * baseDuration;\n return Math.floor(randomFactor * maxDriftSamples);\n }\n /**\n * Analyze loop amplitude and calculate makeup gain for short loops\n * @param {number} loopStart - Loop start position in samples\n * @param {number} loopEnd - Loop end position in samples\n * @returns {number} - Makeup gain multiplier (1.0 = no change)\n */\n #analyzeLoopAmplitude(loopStart, loopEnd) {\n if (!this.enableAmplitudeCompensation || !this.buffer || !this.buffer[0]) return 1;\n if (loopEnd - loopStart >= this.AMPLITUDE_COMPENSATION_THRESHOLD) return 1;\n if (loopStart === this.lastAnalyzedLoopStart && loopEnd === this.lastAnalyzedLoopEnd) return this.loopAmplitudeGain;\n let sumSquares = 0;\n let sampleCount = 0;\n const channel = this.buffer[0];\n const startIndex = Math.floor(loopStart);\n const endIndex = Math.floor(loopEnd);\n for (let i = startIndex; i < endIndex && i < channel.length; i++) {\n const sample = channel[i];\n sumSquares += sample * sample;\n sampleCount++;\n }\n if (sampleCount === 0) return 1;\n const rmsAmplitude = Math.sqrt(sumSquares / sampleCount);\n const targetAmplitude = .3;\n let makeupGain = 1;\n if (rmsAmplitude < targetAmplitude) {\n makeupGain = targetAmplitude / Math.max(rmsAmplitude, .001);\n makeupGain = Math.min(2, makeupGain);\n }\n this.lastAnalyzedLoopStart = loopStart;\n this.lastAnalyzedLoopEnd = loopEnd;\n this.loopAmplitudeGain = makeupGain;\n return makeupGain;\n }\n process(inputs, outputs, parameters) {\n const output = outputs[0];\n this.debugCounter++;\n if (!output || !this.isPlaying || !this.buffer?.[0]?.length) return true;\n const masterGain = parameters.masterGain[0];\n const positionParams = this.#extractPositionParams(parameters);\n const playbackRange = this.#calculatePlaybackRange(positionParams);\n const effectivePlaybackRate = parameters.playbackRate[0];\n const tempo = parameters.tempo[0];\n const loopRange = this.#calculateLoopRange(positionParams, playbackRange, parameters.loopDurationDriftAmount[0], tempo, effectivePlaybackRate);\n const amplitudeGain = this.#analyzeLoopAmplitude(loopRange.loopStartSamples, loopRange.loopEndSamples);\n const velocityGain = this.#midiVelocityToGain(parameters.velocity[0]) * this.velocitySensitivity;\n const basePan = parameters.pan[0];\n const effectivePan = this.panDriftEnabled ? Math.max(-1, Math.min(1, basePan + this.currentPanDrift)) : basePan;\n let outputChannels;\n if (output instanceof Float32Array) outputChannels = [output];\n else if (Array.isArray(output) && output.every((ch) => ch instanceof Float32Array)) outputChannels = output;\n else {\n console.error(\"Unexpected output structure:\", {\n outputType: typeof output,\n isArray: Array.isArray(output),\n constructor: output?.constructor?.name,\n length: output?.length\n });\n return true;\n }\n const numChannels = outputChannels.length;\n const isConstant = this.#getConstantFlags(parameters);\n const silencePadTail = loopRange.loopEndSamples > playbackRange.endSamples;\n const TAIL_FADE_SAMPLES = 64;\n if (this.playbackPosition === 0) {\n this.playbackPosition = this.reversePlayback ? playbackRange.endSamples - 1 : playbackRange.startSamples;\n this.#resetDurationPreservation(this.playbackPosition);\n }\n for (let sample = 0; sample < outputChannels[0].length; sample++) {\n const envelopeGain = this.#getSafeParam(parameters.envGain, sample, isConstant.envGain);\n const baseRate = this.#getSafeParam(parameters.playbackRate, sample, isConstant.playbackRate);\n const playbackStep = this.reversePlayback ? -Math.abs(baseRate) : Math.abs(baseRate);\n const canWrapLoop = this.loopEnabled && this.loopCount < parameters.maxLoopCount[0];\n if (canWrapLoop) {\n if (!this.reversePlayback && this.playbackPosition >= loopRange.loopEndSamples) {\n this.#smoothLoopWrap(silencePadTail ? 0 : this.buffer[0][Math.floor(this.playbackPosition - 1)] || 0, this.buffer[0][Math.floor(loopRange.loopStartSamples)] || 0);\n this.playbackPosition = loopRange.loopStartSamples;\n this.loopCount++;\n this.nextDriftGenerated = false;\n } else if (this.reversePlayback && this.playbackPosition <= loopRange.loopStartSamples) {\n this.#smoothLoopWrap(this.buffer[0][Math.floor(loopRange.loopStartSamples)] || 0, silencePadTail ? 0 : this.buffer[0][Math.floor(loopRange.loopEndSamples) - 1] || 0);\n this.playbackPosition = loopRange.loopEndSamples;\n this.loopCount++;\n this.nextDriftGenerated = false;\n }\n }\n const durationResetTarget = this.#prepareDurationPreservingSample(playbackStep, loopRange);\n const shouldStopForward = !this.reversePlayback && (this.#isDurationPreservationActive(loopRange) ? this.durationPreservation.timelinePosition : this.playbackPosition) >= playbackRange.endSamples;\n const shouldStopReverse = this.reversePlayback && (this.#isDurationPreservationActive(loopRange) ? this.durationPreservation.timelinePosition : this.playbackPosition) <= playbackRange.startSamples;\n const isWithinLoop = this.loopEnabled && this.playbackPosition >= loopRange.loopStartSamples && this.playbackPosition <= loopRange.loopEndSamples;\n if ((shouldStopForward || shouldStopReverse) && !(this.loopEnabled && isWithinLoop)) {\n this.#stop();\n return true;\n }\n let tailGain = 1;\n if (silencePadTail) {\n const distToEnd = playbackRange.endSamples - this.playbackPosition;\n if (distToEnd < TAIL_FADE_SAMPLES) tailGain = Math.max(0, distToEnd / TAIL_FADE_SAMPLES);\n }\n const currentPosition = Math.floor(this.playbackPosition);\n const positionOffset = this.playbackPosition - currentPosition;\n let nextPosition, interpWeight;\n if (this.reversePlayback) {\n nextPosition = Math.max(currentPosition - 1, playbackRange.startSamples);\n interpWeight = 1 - positionOffset;\n } else {\n nextPosition = Math.min(currentPosition + 1, playbackRange.endSamples - 1);\n interpWeight = positionOffset;\n }\n for (let channel = 0; channel < numChannels; channel++) {\n if (!outputChannels[channel]) {\n console.warn(`Output channel ${channel} does not exist. Available channels:`, outputChannels.length);\n continue;\n }\n let interpolatedSample = 0;\n for (let l = 0; l < this.layers.length; l++) {\n const layer = this.layers[l];\n const layerChannel = layer[Math.min(channel, layer.length - 1)];\n const currentSample = layerChannel[currentPosition] || 0;\n const nextSample = layerChannel[nextPosition] || 0;\n interpolatedSample += (currentSample + interpWeight * (nextSample - currentSample)) * this.layerGain;\n }\n if (this.applyClickCompensation) {\n interpolatedSample += this.loopClickCompensation;\n if (this.compensationDecay) {\n this.loopClickCompensation *= this.compensationDecay;\n if (Math.abs(this.loopClickCompensation) < .001) this.applyClickCompensation = false;\n } else this.applyClickCompensation = false;\n }\n const finalSample = interpolatedSample * velocityGain * envelopeGain * masterGain * amplitudeGain * tailGain;\n let panAdjustedSample = finalSample;\n if (outputChannels.length === 2) {\n if (channel === 0) panAdjustedSample = finalSample * (1 - Math.max(0, effectivePan));\n else if (channel === 1) panAdjustedSample = finalSample * (1 - Math.max(0, -effectivePan));\n }\n outputChannels[channel][sample] = Math.max(-1, Math.min(1, isFinite(panAdjustedSample) ? panAdjustedSample : 0));\n }\n this.#advanceDurationPreservingPlayback(playbackStep, durationResetTarget, loopRange, canWrapLoop);\n }\n if (this.usePlaybackPosition) {\n const normalizedPosition = this.#samplesToNormalized(this.playbackPosition);\n this.port.postMessage({\n type: \"voice:position\",\n position: normalizedPosition\n });\n }\n return true;\n }\n};\nregisterProcessor(\"sample-player-processor\", SamplePlayerProcessor);\n//#endregion\n//#region src/worklets/processors/noise/random-noise-processor.js\nvar RandomNoiseProcessor = class extends AudioWorkletProcessor {\n constructor() {\n super();\n this.previousNoise = 0;\n this.previousFiltered = 0;\n this.hpfHz = 150;\n this.alpha = this.hpfHz / (this.hpfHz + sampleRate / (2 * Math.PI));\n this.port.onmessage = (event) => {\n if (event.data.type === \"setHpfHz\") {\n this.hpfHz = event.data.value;\n this.alpha = this.calculateAlpha(this.hpfHz);\n }\n };\n this.port.postMessage({ type: \"initialized\" });\n }\n calculateAlpha(frequency) {\n return frequency / (frequency + sampleRate / (2 * Math.PI));\n }\n process(inputs, outputs, parameters) {\n outputs[0].forEach((channel) => {\n for (let i = 0; i < channel.length; i++) {\n const noise = Math.random() * 2 - 1;\n const filtered = this.alpha * (noise - this.previousNoise) + this.previousFiltered;\n this.previousNoise = noise;\n this.previousFiltered = filtered;\n channel[i] = filtered;\n }\n });\n return true;\n }\n};\nregisterProcessor(\"random-noise-processor\", RandomNoiseProcessor);\n//#endregion\n//#region src/worklets/shared/utils/compress-utils.ts\nvar cheapSoftClipSingleSample = (sample, max = .9) => {\n const a = Math.abs(sample);\n if (a <= max) return sample;\n const x = a / max;\n const compressed = x / (1 + x);\n return Math.sign(sample) * max * compressed;\n};\n/**\n* Basic attenuation compressor for single sample\n* Note: No validation since optimized for real time use\n*/\nvar compressSingleSample = (input, threshold = .75, ratio = 4, limiter = {\n enabled: true,\n type: \"soft\",\n outputRange: {\n min: -1,\n max: 1\n }\n}) => {\n const { min, max } = limiter.outputRange;\n let x = input;\n if (Math.abs(x) > threshold) x = Math.sign(x) * (threshold + (Math.abs(x) - threshold) / ratio);\n if (limiter.enabled) {\n if (limiter.type === \"soft\") x = cheapSoftClipSingleSample(x, Math.abs(max));\n else if (limiter.type === \"hard\") x = Math.max(min, Math.min(max, x));\n }\n return x;\n};\n//#endregion\n//#region src/worklets/processors/delay/DelayBuffer.js\nvar DelayBuffer = class {\n constructor(maxDelaySamples) {\n this.buffer = new Float32Array(maxDelaySamples);\n this.writePtr = 0;\n this.readPtr = 0;\n }\n write(sample) {\n this.buffer[this.writePtr] = sample;\n }\n read() {\n return this.buffer[this.readPtr];\n }\n updatePointers(delaySamples) {\n this.writePtr = (this.writePtr + 1) % this.buffer.length;\n this.readPtr = (this.writePtr - delaySamples + this.buffer.length) % this.buffer.length;\n }\n};\n//#endregion\n//#region src/worklets/processors/delay/FeedbackDelay.js\nvar AUTO_GAIN_THRESHOLD = .8;\nvar SAFETY_GAIN_COMPENSATION = .2;\nvar FeedbackDelay = class {\n constructor(sampleRate) {\n this.sampleRate = sampleRate;\n this.buffers = [];\n this.initialized = false;\n this.autoGainEnabled = false;\n this.gainCompensation = SAFETY_GAIN_COMPENSATION;\n this.lowpassStates = [];\n this.highpassStates = [];\n this.highpassInputStates = [];\n }\n initializeBuffers(channelCount) {\n this.buffers = [];\n this.lowpassStates = [];\n this.highpassStates = [];\n this.highpassInputStates = [];\n const maxSamples = Math.floor(this.sampleRate * 2);\n for (let c = 0; c < channelCount; c++) {\n this.buffers[c] = new DelayBuffer(maxSamples);\n this.lowpassStates[c] = 0;\n this.highpassStates[c] = 0;\n this.highpassInputStates[c] = 0;\n }\n this.initialized = true;\n }\n /** Simple one-pole lowpass filter */\n lowpass(input, cutoffFreq, channelIndex) {\n if (cutoffFreq >= this.sampleRate * .4) return input;\n const omega = 2 * Math.PI * cutoffFreq / this.sampleRate;\n const alpha = Math.max(0, Math.min(.99, Math.sin(omega) / (Math.sin(omega) + Math.cos(omega))));\n this.lowpassStates[channelIndex] = alpha * input + (1 - alpha) * this.lowpassStates[channelIndex];\n return this.lowpassStates[channelIndex];\n }\n /** Simple one-pole highpass filter */\n highpass(input, cutoffFreq, channelIndex) {\n if (cutoffFreq < 5) return input;\n const omega = 2 * Math.PI * cutoffFreq / this.sampleRate;\n const alpha = Math.max(0, Math.min(.99, Math.sin(omega) / (Math.sin(omega) + Math.cos(omega))));\n const lowpassOutput = alpha * input + (1 - alpha) * this.highpassStates[channelIndex];\n const highpassOutput = input - lowpassOutput;\n this.highpassStates[channelIndex] = lowpassOutput;\n return highpassOutput;\n }\n process(inputSample, channelIndex, feedbackAmount, delayTime, lowpassFreq = 1e4, highpassFreq = 100) {\n if (!this.initialized) return inputSample;\n const buffer = this.buffers[channelIndex] || this.buffers[0];\n const delaySamples = Math.floor(this.sampleRate * delayTime);\n const delayedSample = buffer.read();\n let filteredDelay = this.highpass(delayedSample, highpassFreq, channelIndex);\n filteredDelay = this.lowpass(filteredDelay, lowpassFreq, channelIndex);\n const feedbackSample = feedbackAmount * filteredDelay + inputSample;\n let outputSample = feedbackSample;\n const compressedFeedback = compressSingleSample(feedbackSample, .5, 4, {\n enabled: true,\n outputRange: {\n min: -.99,\n max: .99\n },\n type: \"soft\"\n });\n if (this.autoGainEnabled && feedbackAmount > AUTO_GAIN_THRESHOLD) outputSample = compressedFeedback * (1 - (feedbackAmount - AUTO_GAIN_THRESHOLD) * this.gainCompensation);\n return {\n outputSample,\n feedbackSample: compressedFeedback,\n delaySamples\n };\n }\n updateBuffer(channelIndex, sample, delaySamples) {\n const buffer = this.buffers[channelIndex] || this.buffers[0];\n buffer.write(sample);\n buffer.updatePointers(delaySamples);\n }\n setAutoGain(enabled, compensation = SAFETY_GAIN_COMPENSATION) {\n this.autoGainEnabled = enabled;\n this.gainCompensation = compensation;\n }\n};\n//#endregion\n//#region src/worklets/processors/delay/feedback-delay-processor.js\nregisterProcessor(\"feedback-delay-processor\", class extends AudioWorkletProcessor {\n static get parameterDescriptors() {\n return [\n {\n name: \"feedbackAmount\",\n defaultValue: .5,\n minValue: 0,\n maxValue: 1,\n automationRate: \"k-rate\"\n },\n {\n name: \"delayTime\",\n defaultValue: .5,\n minValue: .00012656238799684143,\n maxValue: 2,\n automationRate: \"k-rate\"\n },\n {\n name: \"decay\",\n defaultValue: 1,\n minValue: 0,\n maxValue: 1,\n automationRate: \"k-rate\"\n },\n {\n name: \"lowpass\",\n defaultValue: 1e4,\n minValue: 100,\n maxValue: 16e3,\n automationRate: \"k-rate\"\n }\n ];\n }\n constructor() {\n super();\n this.feedbackDelay = new FeedbackDelay(sampleRate);\n this.decayStartTime = null;\n this.decayActive = false;\n this.baseFeedbackAmount = .5;\n this.setupMessageHandling();\n this.port.postMessage({ type: \"initialized\" });\n }\n setupMessageHandling() {\n this.port.onmessage = (event) => {\n switch (event.data.type) {\n case \"setAutoGain\":\n this.feedbackDelay.setAutoGain(event.data.enabled, event.data.amount);\n break;\n case \"triggerDecay\":\n this.decayStartTime = currentTime;\n this.decayActive = true;\n this.baseFeedbackAmount = event.data.baseFeedbackAmount || .5;\n break;\n case \"stopDecay\":\n this.decayActive = false;\n this.decayStartTime = null;\n }\n };\n }\n process(inputs, outputs, parameters) {\n const input = inputs[0];\n const output = outputs[0];\n if (!input || !output) return true;\n if (!this.feedbackDelay.initialized || this.feedbackDelay.buffers.length !== input.length) this.feedbackDelay.initializeBuffers(input.length);\n const baseFeedbackAmount = parameters.feedbackAmount[0];\n const delayTime = parameters.delayTime[0];\n const decay = parameters.decay[0];\n const lowpassFreq = parameters.lowpass[0];\n const channelCount = Math.min(input.length, output.length);\n const frameCount = output[0].length;\n for (let i = 0; i < frameCount; ++i) {\n let effectiveFeedbackAmount = baseFeedbackAmount;\n if (this.decayActive && this.decayStartTime !== null) {\n const elapsedTime = currentTime - this.decayStartTime + i / sampleRate;\n const delayCompensation = Math.min(100, .5 / delayTime);\n const timeConstant = Math.pow(decay, 5) * 1e3 * delayCompensation + .5;\n effectiveFeedbackAmount = baseFeedbackAmount * Math.exp(-elapsedTime / timeConstant);\n if (effectiveFeedbackAmount < .01) {\n this.decayActive = false;\n effectiveFeedbackAmount = 0;\n }\n }\n for (let c = 0; c < channelCount; c++) {\n const processed = this.feedbackDelay.process(input[c][i], c, effectiveFeedbackAmount, delayTime, lowpassFreq);\n output[c][i] = processed.outputSample;\n this.feedbackDelay.updateBuffer(c, processed.feedbackSample, processed.delaySamples);\n }\n }\n return true;\n }\n});\n//#endregion\n//#region src/worklets/processors/delay/delay-processor.js\nvar DEFAULT_DELAY_CONFIG = {\n CHARACTER: [\"filtered\"],\n SMOOTHING_FACTOR: {\n slowest: 1e-4,\n slow: 25e-5,\n medium: 35e-5,\n fast: 5e-4,\n veryFast: .001,\n superFast: .1,\n none: 1\n }\n};\nvar DEFAULT_CHARACTER_CONFIG = {\n bitCrushed: {\n bits: 11,\n downsample: 3\n },\n filtered: {\n freq: 900,\n Q: .15\n }\n};\nregisterProcessor(\"delay-processor\", class extends AudioWorkletProcessor {\n static get parameterDescriptors() {\n return [{\n name: \"delayTime\",\n defaultValue: .5,\n minValue: .001,\n maxValue: 2,\n automationRate: \"k-rate\"\n }, {\n name: \"feedbackAmount\",\n defaultValue: 0,\n minValue: 0,\n maxValue: .99,\n automationRate: \"k-rate\"\n }];\n }\n constructor() {\n super();\n this.buffers = [];\n this.smoothedDelaySamples = [];\n this.smoothingFactor = DEFAULT_DELAY_CONFIG.SMOOTHING_FACTOR.slowest;\n this.characterModes = [...DEFAULT_DELAY_CONFIG.CHARACTER];\n this._bpState = [];\n this._bpFreq = DEFAULT_CHARACTER_CONFIG.filtered.freq;\n this._bpQ = DEFAULT_CHARACTER_CONFIG.filtered.Q;\n this._bpCoeffs = null;\n this._lastBpFreq = -1;\n this._lastBpQ = -1;\n this.lofiBits = DEFAULT_CHARACTER_CONFIG[\"bitCrushed\"].bits;\n this.lofiDownsample = DEFAULT_CHARACTER_CONFIG[\"bitCrushed\"].downsample;\n this._lofiSampleHold = [];\n this._lofiSampleCount = [];\n this.initialized = false;\n this.port.onmessage = (event) => {\n if (event.data && event.data.type === \"setCharacter\" && Array.isArray(event.data.modes)) this.characterModes = [...event.data.modes];\n if (event.data && event.data.type === \"setBandpassFreq\" && typeof event.data.hz === \"number\") this.setBandpassFreq(event.data.hz);\n if (event.data && event.data.type === \"trigger\") {}\n };\n this.port.postMessage({ type: \"initialized\" });\n }\n setBandpassFreq(hz) {\n this._bpFreq = hz;\n this._lastBpFreq = -1;\n }\n _updateBandpassCoeffs() {\n if (this._lastBpFreq === this._bpFreq && this._lastBpQ === this._bpQ) return;\n const bpFreq = this._bpFreq;\n const bpQ = this._bpQ;\n const omega = 2 * Math.PI * bpFreq / sampleRate;\n const alpha = Math.sin(omega) / (2 * bpQ);\n const cosw = Math.cos(omega);\n const b0 = alpha;\n const b1 = 0;\n const b2 = -alpha;\n const a0 = 1 + alpha;\n const a1 = -2 * cosw;\n const a2 = 1 - alpha;\n this._bpCoeffs = {\n b0: b0 / a0,\n b1: b1 / a0,\n b2: b2 / a0,\n a1: a1 / a0,\n a2: a2 / a0\n };\n this._lastBpFreq = bpFreq;\n this._lastBpQ = bpQ;\n }\n initializeBuffers(channelCount) {\n const maxSamples = Math.floor(sampleRate * 2);\n this.buffers = [];\n this.smoothedDelaySamples = [];\n this._lofiSampleHold = [];\n this._lofiSampleCount = [];\n for (let c = 0; c < channelCount; c++) {\n this.buffers[c] = new DelayBuffer(maxSamples);\n this.smoothedDelaySamples[c] = Math.floor(sampleRate * .5);\n this._lofiSampleHold[c] = 0;\n this._lofiSampleCount[c] = 0;\n }\n this.initialized = true;\n }\n _processLoFi(delayed, c) {\n if (this._lofiSampleCount[c] % this.lofiDownsample === 0) {\n const levels = Math.pow(2, this.lofiBits);\n delayed = Math.round(delayed * levels) / levels;\n this._lofiSampleHold[c] = delayed;\n } else delayed = this._lofiSampleHold[c];\n this._lofiSampleCount[c]++;\n return delayed;\n }\n _processBandpass(delayed, c) {\n if (!this._bpState) this._bpState = [];\n if (!this._bpState[c]) this._bpState[c] = {\n x1: 0,\n x2: 0,\n y1: 0,\n y2: 0\n };\n this._updateBandpassCoeffs();\n if (!this._bpCoeffs) return delayed;\n const { b0, b1, b2, a1, a2 } = this._bpCoeffs;\n const s = this._bpState[c];\n const y = b0 * delayed + b1 * s.x1 + b2 * s.x2 - a1 * s.y1 - a2 * s.y2;\n s.x2 = s.x1;\n s.x1 = delayed;\n s.y2 = s.y1;\n s.y1 = y;\n return y;\n }\n process(inputs, outputs, parameters) {\n const input = inputs[0];\n const output = outputs[0];\n if (!input || !output || input.length === 0 || output.length === 0) return true;\n if (!input[0] || !output[0] || input[0].length === 0 || output[0].length === 0) return true;\n if (!this.initialized || this.buffers.length !== input.length) this.initializeBuffers(input.length);\n const delayTime = parameters.delayTime[0];\n const feedbackAmount = parameters.feedbackAmount[0];\n const targetDelaySamples = sampleRate * delayTime;\n const channelCount = Math.min(input.length, output.length);\n const frameCount = output[0].length;\n const smoothing = this.smoothingFactor;\n for (let i = 0; i < frameCount; ++i) for (let c = 0; c < channelCount; c++) {\n const buf = this.buffers[c];\n if (!buf) continue;\n this.smoothedDelaySamples[c] += (targetDelaySamples - this.smoothedDelaySamples[c]) * smoothing;\n const smoothedDelay = this.smoothedDelaySamples[c];\n const intDelay = Math.floor(smoothedDelay);\n const frac = smoothedDelay - intDelay;\n const readPtrA = (buf.writePtr - intDelay + buf.buffer.length) % buf.buffer.length;\n const readPtrB = (readPtrA - 1 + buf.buffer.length) % buf.buffer.length;\n const sampleA = buf.buffer[readPtrA];\n const sampleB = buf.buffer[readPtrB];\n let delayed = sampleA * (1 - frac) + sampleB * frac;\n for (const mode of this.characterModes) if (mode === \"bitCrushed\") delayed = this._processLoFi(delayed, c);\n else if (mode === \"filtered\") delayed = this._processBandpass(delayed, c);\n output[c][i] = compressSingleSample(delayed, .75, 4, {\n enabled: true,\n type: \"soft\",\n outputRange: {\n min: -.9,\n max: .9\n }\n });\n const inputSample = input[c] && input[c][i] !== void 0 ? input[c][i] : 0;\n buf.write(inputSample + delayed * feedbackAmount);\n buf.updatePointers(intDelay);\n }\n return true;\n }\n});\n//#endregion\n//#region src/worklets/processors/reverb/dattorro-reverb-processor.js\nvar DattorroReverb = class extends AudioWorkletProcessor {\n static get parameterDescriptors() {\n return [\n [\n \"preDelay\",\n 0,\n 0,\n sampleRate - 1,\n \"k-rate\"\n ],\n [\n \"bandwidth\",\n .9999,\n 0,\n 1,\n \"k-rate\"\n ],\n [\n \"inputDiffusion1\",\n .75,\n 0,\n 1,\n \"k-rate\"\n ],\n [\n \"inputDiffusion2\",\n .625,\n 0,\n 1,\n \"k-rate\"\n ],\n [\n \"decay\",\n .5,\n 0,\n 1,\n \"k-rate\"\n ],\n [\n \"decayDiffusion1\",\n .7,\n 0,\n .999999,\n \"k-rate\"\n ],\n [\n \"decayDiffusion2\",\n .5,\n 0,\n .999999,\n \"k-rate\"\n ],\n [\n \"damping\",\n .005,\n 0,\n 1,\n \"k-rate\"\n ],\n [\n \"excursionRate\",\n .5,\n 0,\n 2,\n \"k-rate\"\n ],\n [\n \"excursionDepth\",\n .7,\n 0,\n 2,\n \"k-rate\"\n ],\n [\n \"wet\",\n .3,\n 0,\n 1,\n \"k-rate\"\n ],\n [\n \"dry\",\n .6,\n 0,\n 1,\n \"k-rate\"\n ]\n ].map((x) => /* @__PURE__ */ new Object({\n name: x[0],\n defaultValue: x[1],\n minValue: x[2],\n maxValue: x[3],\n automationRate: x[4]\n }));\n }\n constructor(options) {\n super(options);\n this._Delays = [];\n this._pDLength = sampleRate + (128 - sampleRate % 128);\n this._preDelay = new Float32Array(this._pDLength);\n this._pDWrite = 0;\n this._lp1 = 0;\n this._lp2 = 0;\n this._lp3 = 0;\n this._excPhase = 0;\n const SHORT_DELAY_SCALE = .5;\n [\n .004771345,\n .003595309,\n .012734787,\n .009307483,\n .022579886,\n .149625349,\n .060481839,\n .1249958,\n .030509727,\n .141695508,\n .089244313,\n .106280031\n ].map((x) => x * SHORT_DELAY_SCALE).forEach((x) => this.makeDelay(x));\n this._taps = Int16Array.from([\n .008937872,\n .099929438,\n .064278754,\n .067067639,\n .066866033,\n .006283391,\n .035818689,\n .011861161,\n .121870905,\n .041262054,\n .08981553,\n .070931756,\n .011256342,\n .004065724\n ], (x) => Math.round(x * sampleRate));\n this.port.postMessage({ type: \"initialized\" });\n }\n makeDelay(length) {\n let len = Math.round(length * sampleRate);\n let nextPow2 = 2 ** Math.ceil(Math.log2(len));\n this._Delays.push([\n new Float32Array(nextPow2),\n len - 1,\n 0,\n nextPow2 - 1\n ]);\n }\n writeDelay(index, data) {\n return this._Delays[index][0][this._Delays[index][1]] = data;\n }\n readDelay(index) {\n return this._Delays[index][0][this._Delays[index][2]];\n }\n readDelayAt(index, i) {\n let d = this._Delays[index];\n return d[0][d[2] + i & d[3]];\n }\n readDelayCAt(index, i) {\n let d = this._Delays[index], frac = i - ~~i, int = ~~i + d[2] - 1, mask = d[3];\n let x0 = d[0][int++ & mask], x1 = d[0][int++ & mask], x2 = d[0][int++ & mask], x3 = d[0][int & mask];\n let a = (3 * (x1 - x2) - x0 + x3) / 2, b = 2 * x2 + x0 - (5 * x1 + x3) / 2, c = (x2 - x0) / 2;\n return ((a * frac + b) * frac + c) * frac + x1;\n }\n process(inputs, outputs, parameters) {\n const TWO_PI = 6.283185307179586;\n const TWO_PI_DETUNE = 6.284702653297906;\n const pd = ~~parameters.preDelay[0], bw = parameters.bandwidth[0], fi = parameters.inputDiffusion1[0], si = parameters.inputDiffusion2[0], dc = parameters.decay[0], ft = parameters.decayDiffusion1[0], st = parameters.decayDiffusion2[0], dp = 1 - parameters.damping[0], ex = parameters.excursionRate[0] / sampleRate, ed = parameters.excursionDepth[0] * sampleRate / 1e3, we = parameters.wet[0] * .6, dr = parameters.dry[0];\n if (inputs[0].length == 2) for (let i = 127; i >= 0; i--) {\n this._preDelay[this._pDWrite + i] = (inputs[0][0][i] + inputs[0][1][i]) * .5;\n outputs[0][0][i] = inputs[0][0][i] * dr;\n outputs[0][1][i] = inputs[0][1][i] * dr;\n }\n else if (inputs[0].length > 0) {\n this._preDelay.set(inputs[0][0], this._pDWrite);\n for (let i = 127; i >= 0; i--) outputs[0][0][i] = outputs[0][1][i] = inputs[0][0][i] * dr;\n } else this._preDelay.set(/* @__PURE__ */ new Float32Array(128), this._pDWrite);\n let i = 0;\n while (i < 128) {\n let lo = 0, ro = 0;\n this._lp1 += bw * (this._preDelay[(this._pDLength + this._pDWrite - pd + i) % this._pDLength] - this._lp1);\n let pre = this.writeDelay(0, this._lp1 - fi * this.readDelay(0));\n pre = this.writeDelay(1, fi * (pre - this.readDelay(1)) + this.readDelay(0));\n pre = this.writeDelay(2, fi * pre + this.readDelay(1) - si * this.readDelay(2));\n pre = this.writeDelay(3, si * (pre - this.readDelay(3)) + this.readDelay(2));\n let split = si * pre + this.readDelay(3);\n let exc = ed * (1 + Math.cos(this._excPhase * TWO_PI));\n let exc2 = ed * (1 + Math.sin(this._excPhase * TWO_PI_DETUNE));\n let temp = this.writeDelay(4, split + dc * this.readDelay(11) + ft * this.readDelayCAt(4, exc));\n this.writeDelay(5, this.readDelayCAt(4, exc) - ft * temp);\n this._lp2 += dp * (this.readDelay(5) - this._lp2);\n temp = this.writeDelay(6, dc * this._lp2 - st * this.readDelay(6));\n this.writeDelay(7, this.readDelay(6) + st * temp);\n temp = this.writeDelay(8, split + dc * this.readDelay(7) + ft * this.readDelayCAt(8, exc2));\n this.writeDelay(9, this.readDelayCAt(8, exc2) - ft * temp);\n this._lp3 += dp * (this.readDelay(9) - this._lp3);\n temp = this.writeDelay(10, dc * this._lp3 - st * this.readDelay(10));\n this.writeDelay(11, this.readDelay(10) + st * temp);\n lo = this.readDelayAt(9, this._taps[0]) + this.readDelayAt(9, this._taps[1]) - this.readDelayAt(10, this._taps[2]) + this.readDelayAt(11, this._taps[3]) - this.readDelayAt(5, this._taps[4]) - this.readDelayAt(6, this._taps[5]) - this.readDelayAt(7, this._taps[6]);\n ro = this.readDelayAt(5, this._taps[7]) + this.readDelayAt(5, this._taps[8]) - this.readDelayAt(6, this._taps[9]) + this.readDelayAt(7, this._taps[10]) - this.readDelayAt(9, this._taps[11]) - this.readDelayAt(10, this._taps[12]) - this.readDelayAt(11, this._taps[13]);\n outputs[0][0][i] += lo * we;\n outputs[0][1][i] += ro * we;\n this._excPhase += ex;\n if (this._excPhase >= 1) this._excPhase -= 1;\n i++;\n const delays = this._Delays;\n for (let j = 0; j < delays.length; j++) {\n const d = delays[j];\n d[1] = d[1] + 1 & d[3];\n d[2] = d[2] + 1 & d[3];\n }\n }\n this._pDWrite = (this._pDWrite + 128) % this._pDLength;\n return true;\n }\n};\nregisterProcessor(\"dattorro-reverb-processor\", DattorroReverb);\n//#endregion\n//#region src/worklets/processors/distortion/distortion-processor.js\nvar Distortion = class {\n constructor() {\n this.limitingMode = \"hard-clipping\";\n }\n applyDrive(sample, driveAmount) {\n if (driveAmount <= 0) return sample;\n return sample * (1 + driveAmount * 3);\n }\n applyClipping(sample, clippingAmount, clipThreshold) {\n if (clippingAmount <= 0) return sample;\n let clippedSample;\n switch (this.limitingMode) {\n case \"soft-clipping\":\n clippedSample = clipThreshold * Math.tanh(sample / clipThreshold);\n break;\n case \"hard-clipping\":\n clippedSample = Math.max(-clipThreshold, Math.min(clipThreshold, sample));\n break;\n default: clippedSample = sample;\n }\n if (clipThreshold < .08) {\n const makeupGain = Math.min(2, Math.pow(.1 / clipThreshold, .5));\n clippedSample *= makeupGain;\n }\n return sample * (1 - clippingAmount) + clippedSample * clippingAmount;\n }\n setLimitingMode(mode) {\n this.limitingMode = mode;\n }\n};\nregisterProcessor(\"distortion-processor\", class extends AudioWorkletProcessor {\n static get parameterDescriptors() {\n return [\n {\n name: \"distortionDrive\",\n defaultValue: 0,\n minValue: 0,\n maxValue: 1,\n automationRate: \"a-rate\"\n },\n {\n name: \"clippingAmount\",\n defaultValue: 0,\n minValue: 0,\n maxValue: 1,\n automationRate: \"a-rate\"\n },\n {\n name: \"clippingThreshold\",\n defaultValue: .5,\n minValue: 0,\n maxValue: 1,\n automationRate: \"k-rate\"\n }\n ];\n }\n constructor() {\n super();\n this.distortion = new Distortion();\n this.setupMessageHandling();\n this.port.postMessage({ type: \"initialized\" });\n }\n setupMessageHandling() {\n this.port.onmessage = (event) => {\n switch (event.data.type) {\n case \"setLimitingMode\":\n this.distortion.setLimitingMode(event.data.mode);\n break;\n default: console.warn(\"distortion-processor: Unsupported message\");\n }\n };\n }\n process(inputs, outputs, parameters) {\n const input = inputs[0];\n const output = outputs[0];\n if (!input || !output) return true;\n const clipThreshold = parameters.clippingThreshold[0];\n for (let i = 0; i < output[0].length; ++i) {\n const distortionDrive = parameters.distortionDrive[Math.min(i, parameters.distortionDrive.length - 1)];\n const clippingAmount = parameters.clippingAmount[Math.min(i, parameters.clippingAmount.length - 1)];\n for (let c = 0; c < Math.min(input.length, output.length); c++) {\n let sample = input[c][i];\n sample = this.distortion.applyDrive(sample, distortionDrive);\n sample = this.distortion.applyClipping(sample, clippingAmount, clipThreshold);\n output[c][i] = Math.max(-.999, Math.min(.999, sample));\n }\n }\n return true;\n }\n});\n//#endregion\n//#region src/worklets/processors/follower/envelope-follower-processor.js\nregisterProcessor(\"envelope-follower-processor\", class extends AudioWorkletProcessor {\n static get parameterDescriptors() {\n return [\n {\n name: \"inputGain\",\n defaultValue: 1,\n minValue: 0,\n maxValue: 10,\n automationRate: \"k-rate\"\n },\n {\n name: \"outputGain\",\n defaultValue: 1,\n minValue: 0,\n maxValue: 10,\n automationRate: \"k-rate\"\n },\n {\n name: \"attack\",\n defaultValue: .003,\n minValue: .001,\n maxValue: 1,\n automationRate: \"k-rate\"\n },\n {\n name: \"release\",\n defaultValue: .05,\n minValue: .001,\n maxValue: 5,\n automationRate: \"k-rate\"\n }\n ];\n }\n constructor() {\n super();\n this.envelope = 0;\n this.gateThreshold = .005;\n this.debugCounter = 0;\n this.port.postMessage({ type: \"initialized\" });\n }\n process(inputs, outputs, parameters) {\n const input = inputs[0];\n const output = outputs[0];\n const channel = inputs[0][0];\n if (!input || !output || !channel || input.length === 0 || output.length === 0 || channel.length === 0) return true;\n const inChannel = input[0];\n if (!inChannel || inChannel.length === 0) return true;\n const attack = parameters.attack[0];\n const release = parameters.release[0];\n const inputGain = parameters.inputGain[0];\n const outputGain = parameters.outputGain[0];\n const attackCoeff = Math.exp(-1 / (attack * sampleRate));\n const releaseCoeff = Math.exp(-1 / (release * sampleRate));\n for (let sample = 0; sample < output[0].length; sample++) {\n const inputLevel = Math.abs((input[0][sample] || 0) * inputGain);\n if (inputLevel > 1e-6) {\n if (inputLevel > this.envelope) this.envelope = inputLevel + (this.envelope - inputLevel) * attackCoeff;\n else this.envelope = inputLevel + (this.envelope - inputLevel) * releaseCoeff;\n } else this.envelope *= releaseCoeff;\n if (this.envelope < this.gateThreshold) this.envelope = 0;\n const finalOutput = this.envelope * outputGain;\n for (let channel = 0; channel < output.length; channel++) output[channel][sample] = finalOutput;\n }\n return true;\n }\n});\n//#endregion\n", Tt = !1;
|
|
4683
|
+
}, wt = "//#region src/utils/search/findClosest.ts\n/**\n* Generic binary search that finds the closest element using a custom comparison function\n* @param sortedArray - Array sorted according to the compareValue function\n* @param target - Target value to search for\n* @param getValue - Function to extract comparison value from array elements (defaults to identity for number arrays)\n* @param getDistance - Optional function to calculate distance (defaults to absolute difference)\n* @returns The array index of the element which value is closest to the target value\n*/\nfunction findClosestIdx(sortedArray, target, direction = \"any\", getValue = (x) => x, getDistance = (a, b) => Math.abs(a - b)) {\n if (sortedArray.length === 0) throw new Error(\"Array cannot be empty\");\n if (sortedArray.length === 1) return 0;\n const targetValue = target;\n const firstValue = getValue(sortedArray[0]);\n const lastValue = getValue(sortedArray[sortedArray.length - 1]);\n if (targetValue <= firstValue) return 0;\n if (targetValue >= lastValue) return sortedArray.length - 1;\n let left = 0;\n let right = sortedArray.length - 1;\n while (left < right - 1) {\n const mid = Math.floor((left + right) / 2);\n const midValue = getValue(sortedArray[mid]);\n if (midValue === targetValue) return mid;\n else if (midValue < targetValue) left = mid;\n else right = mid;\n }\n if (direction === \"left\") return left;\n if (direction === \"right\") return right;\n return getDistance(getValue(sortedArray[left]), targetValue) <= getDistance(getValue(sortedArray[right]), targetValue) ? left : right;\n}\n/**\n* Generic binary search that finds the closest element using a custom comparison function\n* @param sortedArray - Array sorted according to the compareValue function\n* @param target - Target value to search for\n* @param getValue - Function to extract comparison value from array elements (defaults to identity for number arrays)\n* @param getDistance - Optional function to calculate distance (defaults to absolute difference)\n* @returns The array element which value is closest to the target value\n*/\nfunction findClosest(sortedArray, target, direction = \"any\", getValue = (x) => x, getDistance = (a, b) => Math.abs(a - b)) {\n return sortedArray[findClosestIdx(sortedArray, target, direction, getValue, getDistance)];\n}\nvar SAMPLE_PLAYER_WORKLET_AUDIOPARAM_DESCRIPTORS = Object.values({\n masterGain: {\n name: \"masterGain\",\n defaultValue: 1,\n minValue: 0,\n maxValue: 2,\n automationRate: \"k-rate\"\n },\n envGain: {\n name: \"envGain\",\n defaultValue: 0,\n minValue: 0,\n maxValue: 1,\n automationRate: \"a-rate\"\n },\n velocity: {\n name: \"velocity\",\n defaultValue: 100,\n minValue: 0,\n maxValue: 127,\n automationRate: \"k-rate\"\n },\n pan: {\n name: \"pan\",\n defaultValue: 0,\n minValue: -1,\n maxValue: 1,\n automationRate: \"k-rate\"\n },\n playbackRate: {\n name: \"playbackRate\",\n defaultValue: 1,\n minValue: .1,\n maxValue: 24,\n automationRate: \"a-rate\"\n },\n loopStart: {\n name: \"loopStart\",\n defaultValue: 0,\n minValue: 0,\n maxValue: 99999,\n automationRate: \"k-rate\"\n },\n loopEnd: {\n name: \"loopEnd\",\n defaultValue: 99999,\n minValue: 0,\n maxValue: 99999,\n automationRate: \"k-rate\"\n },\n startPoint: {\n name: \"startPoint\",\n defaultValue: 0,\n minValue: 0,\n maxValue: 9999,\n automationRate: \"k-rate\"\n },\n endPoint: {\n name: \"endPoint\",\n defaultValue: 9999,\n minValue: 0,\n maxValue: 9999,\n automationRate: \"k-rate\"\n },\n playbackPosition: {\n name: \"playbackPosition\",\n defaultValue: 0,\n minValue: 0,\n maxValue: 99999,\n automationRate: \"k-rate\"\n },\n loopDurationDriftAmount: {\n name: \"loopDurationDriftAmount\",\n defaultValue: 0,\n minValue: 0,\n maxValue: 1,\n automationRate: \"k-rate\"\n },\n maxLoopCount: {\n name: \"maxLoopCount\",\n defaultValue: 999999,\n minValue: 1,\n maxValue: 999999,\n automationRate: \"k-rate\"\n },\n tempo: {\n name: \"tempo\",\n defaultValue: 120,\n minValue: 20,\n maxValue: 300,\n automationRate: \"k-rate\"\n }\n});\n//#endregion\n//#region src/worklets/processors/play/sample-player-processor.js\nvar SamplePlayerProcessor = class extends AudioWorkletProcessor {\n static get parameterDescriptors() {\n return SAMPLE_PLAYER_WORKLET_AUDIOPARAM_DESCRIPTORS;\n }\n constructor() {\n super();\n this.layers = [];\n this.layerGain = 1;\n this.minZeroCrossing = 0;\n this.maxZeroCrossing = 0;\n this.usePlaybackPosition = false;\n this.enableLoopSmoothing = true;\n this.enableAdaptiveDrift = true;\n this.enableAmplitudeCompensation = true;\n this.syncLoopToTempo = false;\n this.keytrackLoopAmount = 0;\n this.durationPreservation = {\n enabled: false,\n maxDriftSamples: Math.floor(sampleRate * .04),\n timelinePosition: 0,\n resetPending: false\n };\n this.PITCH_PRESERVATION_THRESHOLD = Math.floor(sampleRate * .061);\n this.AMPLITUDE_COMPENSATION_THRESHOLD = Math.floor(sampleRate / 16.35);\n this.port.onmessage = this.#handleMessage.bind(this);\n this.#resetState();\n this.port.postMessage({ type: \"initialized\" });\n }\n /** Authority layer. All range and duration math reads through this. */\n get buffer() {\n return this.layers[0] ?? null;\n }\n #handleMessage(event) {\n const { type, value, buffer, layers, timestamp, durationSeconds, zeroCrossings, playbackDirection } = event.data;\n switch (type) {\n case \"voice:reset\":\n this.#resetState();\n this.port.postMessage({ type: \"voice:reset\" });\n break;\n case \"voice:setBuffer\":\n case \"voice:setLayers\":\n this.#resetState();\n this.zeroCrossings = [];\n this.minZeroCrossing = 0;\n this.maxZeroCrossing = 0;\n this.layers = (layers ?? (buffer ? [buffer] : [])).filter(Boolean);\n this.layerGain = this.layers.length ? 1 / this.layers.length : 1;\n this.port.postMessage({\n type: \"voice:loaded\",\n durationSeconds,\n time: currentTime\n });\n break;\n case \"voice:setZeroCrossings\":\n this.zeroCrossings = (zeroCrossings || []).map((timeSec) => timeSec * sampleRate);\n if (this.zeroCrossings.length > 0) {\n this.minZeroCrossing = this.zeroCrossings[0];\n this.maxZeroCrossing = this.zeroCrossings[this.zeroCrossings.length - 1];\n }\n break;\n case \"voice:start\":\n this.isReleasing = false;\n this.isPlaying = true;\n this.loopCount = 0;\n this.playbackPosition = 0;\n this.port.postMessage({\n type: \"voice:started\",\n time: timestamp || currentTime\n });\n break;\n case \"voice:release\":\n this.isReleasing = true;\n this.port.postMessage({\n type: \"voice:releasing\",\n time: currentTime\n });\n break;\n case \"voice:stop\":\n this.#stop();\n break;\n case \"setLoopEnabled\":\n this.loopEnabled = value;\n this.port.postMessage({\n type: \"loop:enabled\",\n enabled: value\n });\n break;\n case \"setPanDriftEnabled\":\n this.panDriftEnabled = value;\n break;\n case \"voice:setPlaybackDirection\": {\n const reverse = playbackDirection === \"reverse\";\n if (reverse !== this.reversePlayback && this.playbackPosition > 0) this.playbackPosition += reverse ? 1 : -1;\n this.reversePlayback = reverse;\n this.port.postMessage({\n type: \"voice:playbackDirectionChange\",\n playbackDirection\n });\n break;\n }\n case \"voice:usePlaybackPosition\":\n this.usePlaybackPosition = value;\n break;\n case \"syncLoopToTempo\":\n this.syncLoopToTempo = value;\n this.port.postMessage({\n type: \"loop:syncToTempo\",\n enabled: value\n });\n break;\n case \"setKeytrackLoopAmount\":\n this.keytrackLoopAmount = Math.max(0, Math.min(1, value));\n break;\n case \"setPreserveDuration\":\n this.durationPreservation.enabled = Boolean(value);\n this.#resetDurationPreservation(this.playbackPosition);\n }\n }\n #resetState() {\n this.isPlaying = false;\n this.isReleasing = false;\n this.loopEnabled = false;\n this.velocitySensitivity = 1;\n this.reversePlayback = false;\n this.playbackPosition = 0;\n this.debugCounter = 0;\n this.loopCount = 0;\n this.applyClickCompensation = false;\n this.loopClickCompensation = 0;\n this.driftUpdateCounter = 0;\n this.currentLoopDrift = 0;\n this.currentPanDrift = 0;\n this.panDriftEnabled = true;\n this.nextDriftGenerated = false;\n this.loopAmplitudeGain = 1;\n this.lastAnalyzedLoopStart = -1;\n this.lastAnalyzedLoopEnd = -1;\n this.#resetDurationPreservation();\n }\n #stop() {\n this.isPlaying = false;\n this.isReleasing = false;\n this.playbackPosition = 0;\n this.port.postMessage({ type: \"voice:stopped\" });\n }\n #smoothLoopWrap(lastLoopSample, newFirstSample) {\n const discontinuity = lastLoopSample - newFirstSample;\n if (this.enableLoopSmoothing && Math.abs(discontinuity) > .01) {\n this.loopClickCompensation = discontinuity * .5;\n this.compensationDecay = .9;\n this.applyClickCompensation = true;\n }\n }\n #findNearestZeroCrossing(position, direction = \"any\", maxDistance = null) {\n if (!this.zeroCrossings || this.zeroCrossings.length === 0) return position;\n const closestValue = findClosest(this.zeroCrossings, position, direction);\n if (maxDistance !== null && Math.abs(closestValue - position) > maxDistance) return position;\n return closestValue;\n }\n /**\n * Convert sample index to normalized position (0-1)\n * @param {number} sampleIndex - Sample index\n * @returns {number} - Normalized position 0-1\n */\n #samplesToNormalized(sampleIndex) {\n if (!this.buffer || !this.buffer[0]) return 0;\n return sampleIndex / this.buffer[0].length;\n }\n /**\n * Convert MIDI velocity (0-127) to gain multiplier (0-1)\n * @param {number} midiVelocity - MIDI velocity 0-127\n * @returns {number} - Gain multiplier 0-1\n */\n #midiVelocityToGain(midiVelocity) {\n return Math.max(0, Math.min(1, midiVelocity / 127));\n }\n /**\n * Calculate musical note durations in samples for given tempo\n * @param {number} tempo - BPM\n * @returns {Object} - Musical note durations in samples\n */\n #getMusicalNoteDurations(tempo) {\n const beatsPerSecond = tempo / 60;\n const samplesPerBeat = sampleRate / beatsPerSecond;\n return {\n whole: samplesPerBeat * 4,\n half: samplesPerBeat * 2,\n quarter: samplesPerBeat,\n eighth: samplesPerBeat / 2,\n sixteenth: samplesPerBeat / 4,\n thirtySecond: samplesPerBeat / 8,\n quarterTriplet: samplesPerBeat * 2 / 3,\n eighthTriplet: samplesPerBeat / 2 * 2 / 3,\n sixteenthTriplet: samplesPerBeat / 4 * 2 / 3\n };\n }\n /**\n * Quantize loop duration to nearest musical interval (skips if below the smallest quantize option)\n * @param {number} loopDurationSamples - Current loop duration in samples\n * @param {number} tempo - Current tempo in BPM\n * @param {number} playbackRate - Current playback rate\n * @returns {number} - Quantized loop duration in samples\n */\n #quantizeLoopDuration(loopDurationSamples, tempo, playbackRate) {\n if (!this.syncLoopToTempo) return loopDurationSamples;\n const noteDurations = this.#getMusicalNoteDurations(tempo);\n const effectiveDuration = loopDurationSamples / Math.abs(playbackRate);\n if (effectiveDuration < noteDurations.thirtySecond) return loopDurationSamples;\n const intervals = Object.values(noteDurations);\n let closestInterval = intervals[0];\n let smallestDiff = Math.abs(effectiveDuration - closestInterval);\n for (const interval of intervals) {\n const diff = Math.abs(effectiveDuration - interval);\n if (diff < smallestDiff) {\n smallestDiff = diff;\n closestInterval = interval;\n }\n }\n return Math.floor(closestInterval * Math.abs(playbackRate));\n }\n /**\n * Extract and convert all position parameters from seconds to samples\n * @param {Object} parameters - AudioWorkletProcessor parameters\n * @returns {Object} - Converted parameters in samples\n */\n #extractPositionParams(parameters) {\n return {\n startPointSamples: Math.floor(parameters.startPoint[0] * sampleRate),\n endPointSamples: Math.floor(parameters.endPoint[0] * sampleRate),\n loopStartSamples: Math.floor(parameters.loopStart[0] * sampleRate),\n loopEndSamples: Math.floor(parameters.loopEnd[0] * sampleRate)\n };\n }\n /**\n * Calculate effective playback range in samples\n * @param {Object} params - Position parameters from #extractPositionParams\n * @returns {Object} - Effective start and end positions\n */\n #calculatePlaybackRange(params) {\n const bufferLength = this.buffer?.[0]?.length || 0;\n const start = Math.max(0, params.startPointSamples);\n const end = params.endPointSamples > start ? Math.min(bufferLength, params.endPointSamples) : bufferLength;\n const snappedStart = this.#findNearestZeroCrossing(start, \"right\");\n const snappedEnd = this.#findNearestZeroCrossing(end, \"left\");\n return {\n startSamples: snappedStart,\n endSamples: snappedEnd,\n durationSamples: snappedEnd - snappedStart\n };\n }\n /**\n * Calculate effective loop range in samples with optional drift\n * @param {Object} params - Position parameters from #extractPositionParams\n * @param {Object} playbackRange - Range from #calculatePlaybackRange\n * @param {number} driftAmount - Loop duration drift amount (0-1)\n * @param {number} tempo - Current tempo in BPM\n * @param {number} playbackRate - Current playback rate\n * @returns {Object} - Effective loop start and end positions with drift applied\n */\n #calculateLoopRange(params, playbackRange, driftAmount = 0, tempo = 120, playbackRate = 1) {\n const lpStart = params.loopStartSamples;\n const lpEnd = params.loopEndSamples;\n let calcLoopStart = lpStart < lpEnd && lpStart >= 0 ? lpStart : playbackRange.startSamples;\n let calcLoopEnd = lpEnd > lpStart && lpEnd <= playbackRange.endSamples ? lpEnd : playbackRange.endSamples;\n let baseDuration = calcLoopEnd - calcLoopStart;\n if (this.syncLoopToTempo) {\n const quantizedDuration = this.#quantizeLoopDuration(baseDuration, tempo, playbackRate);\n calcLoopEnd = calcLoopStart + quantizedDuration;\n calcLoopEnd = Math.min(calcLoopEnd, playbackRange.endSamples);\n }\n if (baseDuration > this.PITCH_PRESERVATION_THRESHOLD && this.keytrackLoopAmount > 0 && !this.syncLoopToTempo) {\n const scale = 1 + this.keytrackLoopAmount * (Math.abs(playbackRate) - 1);\n baseDuration = Math.max(1, Math.floor(baseDuration * scale));\n calcLoopEnd = calcLoopStart + baseDuration;\n }\n baseDuration = calcLoopEnd - calcLoopStart;\n if (baseDuration > this.PITCH_PRESERVATION_THRESHOLD) calcLoopStart = this.#findNearestZeroCrossing(calcLoopStart, \"right\");\n if (driftAmount > 0 && this.loopEnabled) {\n if (!this.nextDriftGenerated || this.loopCount === 0) {\n const updateInterval = baseDuration <= this.PITCH_PRESERVATION_THRESHOLD ? Math.max(1, Math.floor(this.PITCH_PRESERVATION_THRESHOLD / baseDuration)) : 1;\n if (this.driftUpdateCounter % updateInterval === 0) {\n this.currentLoopDrift = this.#generateLoopDrift(driftAmount, baseDuration);\n if (this.panDriftEnabled && driftAmount > 0 && this.loopCount > 0) {\n const panDriftAmountScalar = 1e-4;\n this.currentPanDrift = this.currentLoopDrift * panDriftAmountScalar;\n } else this.currentPanDrift = 0;\n }\n this.driftUpdateCounter++;\n this.nextDriftGenerated = true;\n }\n const driftedLoopEnd = calcLoopEnd + this.currentLoopDrift;\n const minLoopDuration = Math.max(1, Math.floor(baseDuration * .1));\n const maxLoopEnd = Math.max(playbackRange.endSamples, calcLoopEnd);\n calcLoopEnd = Math.max(calcLoopStart + minLoopDuration, Math.min(maxLoopEnd, driftedLoopEnd));\n } else this.currentPanDrift = 0;\n if (baseDuration > this.PITCH_PRESERVATION_THRESHOLD && calcLoopEnd <= playbackRange.endSamples) calcLoopEnd = Math.max(calcLoopStart + 1, this.#findNearestZeroCrossing(calcLoopEnd, \"left\"));\n const loopDuration = calcLoopEnd - calcLoopStart;\n return {\n loopStartSamples: calcLoopStart,\n loopEndSamples: calcLoopEnd,\n loopDurationSamples: loopDuration\n };\n }\n #getSafeParam(paramArray, index, isConstant) {\n return isConstant ? paramArray[0] : paramArray[Math.min(index, paramArray.length - 1)];\n }\n #getConstantFlags(parameters) {\n this.constantFlags ??= {\n envGain: true,\n playbackRate: true\n };\n this.constantFlags.envGain = parameters.envGain.length === 1;\n this.constantFlags.playbackRate = parameters.playbackRate.length === 1;\n return this.constantFlags;\n }\n #resetDurationPreservation(position = 0) {\n this.durationPreservation.timelinePosition = position;\n this.durationPreservation.resetPending = false;\n }\n #isDurationPreservationActive(loopRange) {\n return this.durationPreservation.enabled && Boolean(this.zeroCrossings?.length) && (!this.loopEnabled || loopRange.loopDurationSamples > this.PITCH_PRESERVATION_THRESHOLD);\n }\n #prepareDurationPreservingSample(playbackRate, loopRange) {\n const state = this.durationPreservation;\n if (!this.#isDurationPreservationActive(loopRange)) return null;\n if (Math.abs(this.playbackPosition - state.timelinePosition) > state.maxDriftSamples) state.resetPending = true;\n if (!state.resetPending) return null;\n const direction = playbackRate < 0 ? \"left\" : \"right\";\n const outgoingZero = this.#findNearestZeroCrossing(this.playbackPosition, direction);\n if (Math.abs(outgoingZero - this.playbackPosition) > Math.abs(playbackRate)) return null;\n this.playbackPosition = outgoingZero;\n state.resetPending = false;\n return this.#findNearestZeroCrossing(state.timelinePosition, \"any\", state.maxDriftSamples);\n }\n #advanceDurationPreservingPlayback(playbackRate, resetTarget, loopRange, canWrapLoop) {\n const state = this.durationPreservation;\n this.playbackPosition = resetTarget === null ? this.playbackPosition + playbackRate : resetTarget;\n if (this.#isDurationPreservationActive(loopRange)) {\n state.timelinePosition += playbackRate < 0 ? -1 : 1;\n if (canWrapLoop && playbackRate >= 0 && state.timelinePosition >= loopRange.loopEndSamples) state.timelinePosition = loopRange.loopStartSamples;\n else if (canWrapLoop && playbackRate < 0 && state.timelinePosition <= loopRange.loopStartSamples) state.timelinePosition = loopRange.loopEndSamples - 1;\n } else this.#resetDurationPreservation(this.playbackPosition);\n }\n /**\n * Generate a new drift amount for the current loop iteration\n * @param {number} driftAmount - Maximum drift amount (0-1)\n * @param {number} baseDuration - Base loop duration in samples\n * @returns {number} - Drift amount in samples\n */\n #generateLoopDrift(driftAmount, baseDuration) {\n if (driftAmount <= 0) return 0;\n const randomFactor = (Math.random() - .5) * 2;\n let effectiveDriftAmount = driftAmount;\n if (this.enableAdaptiveDrift) {\n const shortThreshold = 1024;\n const longThreshold = 8192;\n if (baseDuration < shortThreshold) effectiveDriftAmount *= .1;\n else if (baseDuration < longThreshold) {\n const scaleFactor = .1 + .9 * (baseDuration - shortThreshold) / 7168;\n effectiveDriftAmount *= scaleFactor;\n }\n }\n const maxDriftSamples = effectiveDriftAmount * baseDuration;\n return Math.floor(randomFactor * maxDriftSamples);\n }\n /**\n * Analyze loop amplitude and calculate makeup gain for short loops\n * @param {number} loopStart - Loop start position in samples\n * @param {number} loopEnd - Loop end position in samples\n * @returns {number} - Makeup gain multiplier (1.0 = no change)\n */\n #analyzeLoopAmplitude(loopStart, loopEnd) {\n if (!this.enableAmplitudeCompensation || !this.buffer || !this.buffer[0]) return 1;\n if (loopEnd - loopStart >= this.AMPLITUDE_COMPENSATION_THRESHOLD) return 1;\n if (loopStart === this.lastAnalyzedLoopStart && loopEnd === this.lastAnalyzedLoopEnd) return this.loopAmplitudeGain;\n let sumSquares = 0;\n let sampleCount = 0;\n const channel = this.buffer[0];\n const startIndex = Math.floor(loopStart);\n const endIndex = Math.floor(loopEnd);\n for (let i = startIndex; i < endIndex && i < channel.length; i++) {\n const sample = channel[i];\n sumSquares += sample * sample;\n sampleCount++;\n }\n if (sampleCount === 0) return 1;\n const rmsAmplitude = Math.sqrt(sumSquares / sampleCount);\n const targetAmplitude = .3;\n let makeupGain = 1;\n if (rmsAmplitude < targetAmplitude) {\n makeupGain = targetAmplitude / Math.max(rmsAmplitude, .001);\n makeupGain = Math.min(2, makeupGain);\n }\n this.lastAnalyzedLoopStart = loopStart;\n this.lastAnalyzedLoopEnd = loopEnd;\n this.loopAmplitudeGain = makeupGain;\n return makeupGain;\n }\n process(inputs, outputs, parameters) {\n const output = outputs[0];\n this.debugCounter++;\n if (!output || !this.isPlaying || !this.buffer?.[0]?.length) return true;\n const masterGain = parameters.masterGain[0];\n const positionParams = this.#extractPositionParams(parameters);\n const playbackRange = this.#calculatePlaybackRange(positionParams);\n const effectivePlaybackRate = parameters.playbackRate[0];\n const tempo = parameters.tempo[0];\n const loopRange = this.#calculateLoopRange(positionParams, playbackRange, parameters.loopDurationDriftAmount[0], tempo, effectivePlaybackRate);\n const amplitudeGain = this.#analyzeLoopAmplitude(loopRange.loopStartSamples, loopRange.loopEndSamples);\n const velocityGain = this.#midiVelocityToGain(parameters.velocity[0]) * this.velocitySensitivity;\n const basePan = parameters.pan[0];\n const effectivePan = this.panDriftEnabled ? Math.max(-1, Math.min(1, basePan + this.currentPanDrift)) : basePan;\n let outputChannels;\n if (output instanceof Float32Array) outputChannels = [output];\n else if (Array.isArray(output) && output.every((ch) => ch instanceof Float32Array)) outputChannels = output;\n else {\n console.error(\"Unexpected output structure:\", {\n outputType: typeof output,\n isArray: Array.isArray(output),\n constructor: output?.constructor?.name,\n length: output?.length\n });\n return true;\n }\n const numChannels = outputChannels.length;\n const isConstant = this.#getConstantFlags(parameters);\n const silencePadTail = loopRange.loopEndSamples > playbackRange.endSamples;\n const TAIL_FADE_SAMPLES = 64;\n if (this.playbackPosition === 0) {\n this.playbackPosition = this.reversePlayback ? playbackRange.endSamples - 1 : playbackRange.startSamples;\n this.#resetDurationPreservation(this.playbackPosition);\n }\n for (let sample = 0; sample < outputChannels[0].length; sample++) {\n const envelopeGain = this.#getSafeParam(parameters.envGain, sample, isConstant.envGain);\n const baseRate = this.#getSafeParam(parameters.playbackRate, sample, isConstant.playbackRate);\n const playbackStep = this.reversePlayback ? -Math.abs(baseRate) : Math.abs(baseRate);\n const canWrapLoop = this.loopEnabled && this.loopCount < parameters.maxLoopCount[0];\n if (canWrapLoop) {\n if (!this.reversePlayback && this.playbackPosition >= loopRange.loopEndSamples) {\n this.#smoothLoopWrap(silencePadTail ? 0 : this.buffer[0][Math.floor(this.playbackPosition - 1)] || 0, this.buffer[0][Math.floor(loopRange.loopStartSamples)] || 0);\n this.playbackPosition = loopRange.loopStartSamples;\n this.loopCount++;\n this.nextDriftGenerated = false;\n } else if (this.reversePlayback && this.playbackPosition <= loopRange.loopStartSamples) {\n this.#smoothLoopWrap(this.buffer[0][Math.floor(loopRange.loopStartSamples)] || 0, silencePadTail ? 0 : this.buffer[0][Math.floor(loopRange.loopEndSamples) - 1] || 0);\n this.playbackPosition = loopRange.loopEndSamples;\n this.loopCount++;\n this.nextDriftGenerated = false;\n }\n }\n const durationResetTarget = this.#prepareDurationPreservingSample(playbackStep, loopRange);\n const shouldStopForward = !this.reversePlayback && (this.#isDurationPreservationActive(loopRange) ? this.durationPreservation.timelinePosition : this.playbackPosition) >= playbackRange.endSamples;\n const shouldStopReverse = this.reversePlayback && (this.#isDurationPreservationActive(loopRange) ? this.durationPreservation.timelinePosition : this.playbackPosition) <= playbackRange.startSamples;\n const isWithinLoop = this.loopEnabled && this.playbackPosition >= loopRange.loopStartSamples && this.playbackPosition <= loopRange.loopEndSamples;\n if ((shouldStopForward || shouldStopReverse) && !(this.loopEnabled && isWithinLoop)) {\n this.#stop();\n return true;\n }\n let tailGain = 1;\n if (silencePadTail) {\n const distToEnd = playbackRange.endSamples - this.playbackPosition;\n if (distToEnd < TAIL_FADE_SAMPLES) tailGain = Math.max(0, distToEnd / TAIL_FADE_SAMPLES);\n }\n const currentPosition = Math.floor(this.playbackPosition);\n const positionOffset = this.playbackPosition - currentPosition;\n let nextPosition, interpWeight;\n if (this.reversePlayback) {\n nextPosition = Math.max(currentPosition - 1, playbackRange.startSamples);\n interpWeight = 1 - positionOffset;\n } else {\n nextPosition = Math.min(currentPosition + 1, playbackRange.endSamples - 1);\n interpWeight = positionOffset;\n }\n for (let channel = 0; channel < numChannels; channel++) {\n if (!outputChannels[channel]) {\n console.warn(`Output channel ${channel} does not exist. Available channels:`, outputChannels.length);\n continue;\n }\n let interpolatedSample = 0;\n for (let l = 0; l < this.layers.length; l++) {\n const layer = this.layers[l];\n const layerChannel = layer[Math.min(channel, layer.length - 1)];\n const currentSample = layerChannel[currentPosition] || 0;\n const nextSample = layerChannel[nextPosition] || 0;\n interpolatedSample += (currentSample + interpWeight * (nextSample - currentSample)) * this.layerGain;\n }\n if (this.applyClickCompensation) {\n interpolatedSample += this.loopClickCompensation;\n if (this.compensationDecay) {\n this.loopClickCompensation *= this.compensationDecay;\n if (Math.abs(this.loopClickCompensation) < .001) this.applyClickCompensation = false;\n } else this.applyClickCompensation = false;\n }\n const finalSample = interpolatedSample * velocityGain * envelopeGain * masterGain * amplitudeGain * tailGain;\n let panAdjustedSample = finalSample;\n if (outputChannels.length === 2) {\n if (channel === 0) panAdjustedSample = finalSample * (1 - Math.max(0, effectivePan));\n else if (channel === 1) panAdjustedSample = finalSample * (1 - Math.max(0, -effectivePan));\n }\n outputChannels[channel][sample] = Math.max(-1, Math.min(1, isFinite(panAdjustedSample) ? panAdjustedSample : 0));\n }\n this.#advanceDurationPreservingPlayback(playbackStep, durationResetTarget, loopRange, canWrapLoop);\n }\n if (this.usePlaybackPosition) {\n const normalizedPosition = this.#samplesToNormalized(this.playbackPosition);\n this.port.postMessage({\n type: \"voice:position\",\n position: normalizedPosition\n });\n }\n return true;\n }\n};\nregisterProcessor(\"sample-player-processor\", SamplePlayerProcessor);\n//#endregion\n//#region src/worklets/processors/noise/random-noise-processor.js\nvar RandomNoiseProcessor = class extends AudioWorkletProcessor {\n constructor() {\n super();\n this.previousNoise = 0;\n this.previousFiltered = 0;\n this.hpfHz = 150;\n this.alpha = this.hpfHz / (this.hpfHz + sampleRate / (2 * Math.PI));\n this.port.onmessage = (event) => {\n if (event.data.type === \"setHpfHz\") {\n this.hpfHz = event.data.value;\n this.alpha = this.calculateAlpha(this.hpfHz);\n }\n };\n this.port.postMessage({ type: \"initialized\" });\n }\n calculateAlpha(frequency) {\n return frequency / (frequency + sampleRate / (2 * Math.PI));\n }\n process(inputs, outputs, _parameters) {\n outputs[0].forEach((channel) => {\n for (let i = 0; i < channel.length; i++) {\n const noise = Math.random() * 2 - 1;\n const filtered = this.alpha * (noise - this.previousNoise) + this.previousFiltered;\n this.previousNoise = noise;\n this.previousFiltered = filtered;\n channel[i] = filtered;\n }\n });\n return true;\n }\n};\nregisterProcessor(\"random-noise-processor\", RandomNoiseProcessor);\n//#endregion\n//#region src/worklets/shared/utils/compress-utils.ts\nvar cheapSoftClipSingleSample = (sample, max = .9) => {\n const a = Math.abs(sample);\n if (a <= max) return sample;\n const x = a / max;\n const compressed = x / (1 + x);\n return Math.sign(sample) * max * compressed;\n};\n/**\n* Basic attenuation compressor for single sample\n* Note: No validation since optimized for real time use\n*/\nvar compressSingleSample = (input, threshold = .75, ratio = 4, limiter = {\n enabled: true,\n type: \"soft\",\n outputRange: {\n min: -1,\n max: 1\n }\n}) => {\n const { min, max } = limiter.outputRange;\n let x = input;\n if (Math.abs(x) > threshold) x = Math.sign(x) * (threshold + (Math.abs(x) - threshold) / ratio);\n if (limiter.enabled) {\n if (limiter.type === \"soft\") x = cheapSoftClipSingleSample(x, Math.abs(max));\n else if (limiter.type === \"hard\") x = Math.max(min, Math.min(max, x));\n }\n return x;\n};\n//#endregion\n//#region src/worklets/processors/delay/DelayBuffer.js\nvar DelayBuffer = class {\n constructor(maxDelaySamples) {\n this.buffer = new Float32Array(maxDelaySamples);\n this.writePtr = 0;\n this.readPtr = 0;\n }\n write(sample) {\n this.buffer[this.writePtr] = sample;\n }\n read() {\n return this.buffer[this.readPtr];\n }\n updatePointers(delaySamples) {\n this.writePtr = (this.writePtr + 1) % this.buffer.length;\n this.readPtr = (this.writePtr - delaySamples + this.buffer.length) % this.buffer.length;\n }\n};\n//#endregion\n//#region src/worklets/processors/delay/FeedbackDelay.js\nvar AUTO_GAIN_THRESHOLD = .8;\nvar SAFETY_GAIN_COMPENSATION = .2;\nvar FeedbackDelay = class {\n constructor(sampleRate) {\n this.sampleRate = sampleRate;\n this.buffers = [];\n this.initialized = false;\n this.autoGainEnabled = false;\n this.gainCompensation = SAFETY_GAIN_COMPENSATION;\n this.lowpassStates = [];\n this.highpassStates = [];\n this.highpassInputStates = [];\n }\n initializeBuffers(channelCount) {\n this.buffers = [];\n this.lowpassStates = [];\n this.highpassStates = [];\n this.highpassInputStates = [];\n const maxSamples = Math.floor(this.sampleRate * 2);\n for (let c = 0; c < channelCount; c++) {\n this.buffers[c] = new DelayBuffer(maxSamples);\n this.lowpassStates[c] = 0;\n this.highpassStates[c] = 0;\n this.highpassInputStates[c] = 0;\n }\n this.initialized = true;\n }\n /** Simple one-pole lowpass filter */\n lowpass(input, cutoffFreq, channelIndex) {\n if (cutoffFreq >= this.sampleRate * .4) return input;\n const omega = 2 * Math.PI * cutoffFreq / this.sampleRate;\n const alpha = Math.max(0, Math.min(.99, Math.sin(omega) / (Math.sin(omega) + Math.cos(omega))));\n this.lowpassStates[channelIndex] = alpha * input + (1 - alpha) * this.lowpassStates[channelIndex];\n return this.lowpassStates[channelIndex];\n }\n /** Simple one-pole highpass filter */\n highpass(input, cutoffFreq, channelIndex) {\n if (cutoffFreq < 5) return input;\n const omega = 2 * Math.PI * cutoffFreq / this.sampleRate;\n const alpha = Math.max(0, Math.min(.99, Math.sin(omega) / (Math.sin(omega) + Math.cos(omega))));\n const lowpassOutput = alpha * input + (1 - alpha) * this.highpassStates[channelIndex];\n const highpassOutput = input - lowpassOutput;\n this.highpassStates[channelIndex] = lowpassOutput;\n return highpassOutput;\n }\n process(inputSample, channelIndex, feedbackAmount, delayTime, lowpassFreq = 1e4, highpassFreq = 100) {\n if (!this.initialized) return inputSample;\n const buffer = this.buffers[channelIndex] || this.buffers[0];\n const delaySamples = Math.floor(this.sampleRate * delayTime);\n const delayedSample = buffer.read();\n let filteredDelay = this.highpass(delayedSample, highpassFreq, channelIndex);\n filteredDelay = this.lowpass(filteredDelay, lowpassFreq, channelIndex);\n const feedbackSample = feedbackAmount * filteredDelay + inputSample;\n let outputSample = feedbackSample;\n const compressedFeedback = compressSingleSample(feedbackSample, .5, 4, {\n enabled: true,\n outputRange: {\n min: -.99,\n max: .99\n },\n type: \"soft\"\n });\n if (this.autoGainEnabled && feedbackAmount > AUTO_GAIN_THRESHOLD) outputSample = compressedFeedback * (1 - (feedbackAmount - AUTO_GAIN_THRESHOLD) * this.gainCompensation);\n return {\n outputSample,\n feedbackSample: compressedFeedback,\n delaySamples\n };\n }\n updateBuffer(channelIndex, sample, delaySamples) {\n const buffer = this.buffers[channelIndex] || this.buffers[0];\n buffer.write(sample);\n buffer.updatePointers(delaySamples);\n }\n setAutoGain(enabled, compensation = SAFETY_GAIN_COMPENSATION) {\n this.autoGainEnabled = enabled;\n this.gainCompensation = compensation;\n }\n};\n//#endregion\n//#region src/worklets/processors/delay/feedback-delay-processor.js\nregisterProcessor(\"feedback-delay-processor\", class extends AudioWorkletProcessor {\n static get parameterDescriptors() {\n return [\n {\n name: \"feedbackAmount\",\n defaultValue: .5,\n minValue: 0,\n maxValue: 1,\n automationRate: \"k-rate\"\n },\n {\n name: \"delayTime\",\n defaultValue: .5,\n minValue: .00012656238799684143,\n maxValue: 2,\n automationRate: \"k-rate\"\n },\n {\n name: \"decay\",\n defaultValue: 1,\n minValue: 0,\n maxValue: 1,\n automationRate: \"k-rate\"\n },\n {\n name: \"lowpass\",\n defaultValue: 1e4,\n minValue: 100,\n maxValue: 16e3,\n automationRate: \"k-rate\"\n }\n ];\n }\n constructor() {\n super();\n this.feedbackDelay = new FeedbackDelay(sampleRate);\n this.decayStartTime = null;\n this.decayActive = false;\n this.baseFeedbackAmount = .5;\n this.setupMessageHandling();\n this.port.postMessage({ type: \"initialized\" });\n }\n setupMessageHandling() {\n this.port.onmessage = (event) => {\n switch (event.data.type) {\n case \"setAutoGain\":\n this.feedbackDelay.setAutoGain(event.data.enabled, event.data.amount);\n break;\n case \"triggerDecay\":\n this.decayStartTime = currentTime;\n this.decayActive = true;\n this.baseFeedbackAmount = event.data.baseFeedbackAmount || .5;\n break;\n case \"stopDecay\":\n this.decayActive = false;\n this.decayStartTime = null;\n }\n };\n }\n process(inputs, outputs, parameters) {\n const input = inputs[0];\n const output = outputs[0];\n if (!input || !output) return true;\n if (!this.feedbackDelay.initialized || this.feedbackDelay.buffers.length !== input.length) this.feedbackDelay.initializeBuffers(input.length);\n const baseFeedbackAmount = parameters.feedbackAmount[0];\n const delayTime = parameters.delayTime[0];\n const decay = parameters.decay[0];\n const lowpassFreq = parameters.lowpass[0];\n const channelCount = Math.min(input.length, output.length);\n const frameCount = output[0].length;\n for (let i = 0; i < frameCount; ++i) {\n let effectiveFeedbackAmount = baseFeedbackAmount;\n if (this.decayActive && this.decayStartTime !== null) {\n const elapsedTime = currentTime - this.decayStartTime + i / sampleRate;\n const delayCompensation = Math.min(100, .5 / delayTime);\n const timeConstant = Math.pow(decay, 5) * 1e3 * delayCompensation + .5;\n effectiveFeedbackAmount = baseFeedbackAmount * Math.exp(-elapsedTime / timeConstant);\n if (effectiveFeedbackAmount < .01) {\n this.decayActive = false;\n effectiveFeedbackAmount = 0;\n }\n }\n for (let c = 0; c < channelCount; c++) {\n const processed = this.feedbackDelay.process(input[c][i], c, effectiveFeedbackAmount, delayTime, lowpassFreq);\n output[c][i] = processed.outputSample;\n this.feedbackDelay.updateBuffer(c, processed.feedbackSample, processed.delaySamples);\n }\n }\n return true;\n }\n});\n//#endregion\n//#region src/worklets/processors/delay/delay-processor.js\nvar DEFAULT_DELAY_CONFIG = {\n CHARACTER: [\"filtered\"],\n SMOOTHING_FACTOR: {\n slowest: 1e-4,\n slow: 25e-5,\n medium: 35e-5,\n fast: 5e-4,\n veryFast: .001,\n superFast: .1,\n none: 1\n }\n};\nvar DEFAULT_CHARACTER_CONFIG = {\n bitCrushed: {\n bits: 11,\n downsample: 3\n },\n filtered: {\n freq: 900,\n Q: .15\n }\n};\nregisterProcessor(\"delay-processor\", class extends AudioWorkletProcessor {\n static get parameterDescriptors() {\n return [{\n name: \"delayTime\",\n defaultValue: .5,\n minValue: .001,\n maxValue: 2,\n automationRate: \"k-rate\"\n }, {\n name: \"feedbackAmount\",\n defaultValue: 0,\n minValue: 0,\n maxValue: .99,\n automationRate: \"k-rate\"\n }];\n }\n constructor() {\n super();\n this.buffers = [];\n this.smoothedDelaySamples = [];\n this.smoothingFactor = DEFAULT_DELAY_CONFIG.SMOOTHING_FACTOR.slowest;\n this.characterModes = [...DEFAULT_DELAY_CONFIG.CHARACTER];\n this._bpState = [];\n this._bpFreq = DEFAULT_CHARACTER_CONFIG.filtered.freq;\n this._bpQ = DEFAULT_CHARACTER_CONFIG.filtered.Q;\n this._bpCoeffs = null;\n this._lastBpFreq = -1;\n this._lastBpQ = -1;\n this.lofiBits = DEFAULT_CHARACTER_CONFIG[\"bitCrushed\"].bits;\n this.lofiDownsample = DEFAULT_CHARACTER_CONFIG[\"bitCrushed\"].downsample;\n this._lofiSampleHold = [];\n this._lofiSampleCount = [];\n this.initialized = false;\n this.port.onmessage = (event) => {\n if (event.data && event.data.type === \"setCharacter\" && Array.isArray(event.data.modes)) this.characterModes = [...event.data.modes];\n if (event.data && event.data.type === \"setBandpassFreq\" && typeof event.data.hz === \"number\") this.setBandpassFreq(event.data.hz);\n if (event.data && event.data.type === \"trigger\") {}\n };\n this.port.postMessage({ type: \"initialized\" });\n }\n setBandpassFreq(hz) {\n this._bpFreq = hz;\n this._lastBpFreq = -1;\n }\n _updateBandpassCoeffs() {\n if (this._lastBpFreq === this._bpFreq && this._lastBpQ === this._bpQ) return;\n const bpFreq = this._bpFreq;\n const bpQ = this._bpQ;\n const omega = 2 * Math.PI * bpFreq / sampleRate;\n const alpha = Math.sin(omega) / (2 * bpQ);\n const cosw = Math.cos(omega);\n const b0 = alpha;\n const b1 = 0;\n const b2 = -alpha;\n const a0 = 1 + alpha;\n const a1 = -2 * cosw;\n const a2 = 1 - alpha;\n this._bpCoeffs = {\n b0: b0 / a0,\n b1: b1 / a0,\n b2: b2 / a0,\n a1: a1 / a0,\n a2: a2 / a0\n };\n this._lastBpFreq = bpFreq;\n this._lastBpQ = bpQ;\n }\n initializeBuffers(channelCount) {\n const maxSamples = Math.floor(sampleRate * 2);\n this.buffers = [];\n this.smoothedDelaySamples = [];\n this._lofiSampleHold = [];\n this._lofiSampleCount = [];\n for (let c = 0; c < channelCount; c++) {\n this.buffers[c] = new DelayBuffer(maxSamples);\n this.smoothedDelaySamples[c] = Math.floor(sampleRate * .5);\n this._lofiSampleHold[c] = 0;\n this._lofiSampleCount[c] = 0;\n }\n this.initialized = true;\n }\n _processLoFi(delayed, c) {\n if (this._lofiSampleCount[c] % this.lofiDownsample === 0) {\n const levels = Math.pow(2, this.lofiBits);\n delayed = Math.round(delayed * levels) / levels;\n this._lofiSampleHold[c] = delayed;\n } else delayed = this._lofiSampleHold[c];\n this._lofiSampleCount[c]++;\n return delayed;\n }\n _processBandpass(delayed, c) {\n if (!this._bpState) this._bpState = [];\n if (!this._bpState[c]) this._bpState[c] = {\n x1: 0,\n x2: 0,\n y1: 0,\n y2: 0\n };\n this._updateBandpassCoeffs();\n if (!this._bpCoeffs) return delayed;\n const { b0, b1, b2, a1, a2 } = this._bpCoeffs;\n const s = this._bpState[c];\n const y = b0 * delayed + b1 * s.x1 + b2 * s.x2 - a1 * s.y1 - a2 * s.y2;\n s.x2 = s.x1;\n s.x1 = delayed;\n s.y2 = s.y1;\n s.y1 = y;\n return y;\n }\n process(inputs, outputs, parameters) {\n const input = inputs[0];\n const output = outputs[0];\n if (!input || !output || input.length === 0 || output.length === 0) return true;\n if (!input[0] || !output[0] || input[0].length === 0 || output[0].length === 0) return true;\n if (!this.initialized || this.buffers.length !== input.length) this.initializeBuffers(input.length);\n const delayTime = parameters.delayTime[0];\n const feedbackAmount = parameters.feedbackAmount[0];\n const targetDelaySamples = sampleRate * delayTime;\n const channelCount = Math.min(input.length, output.length);\n const frameCount = output[0].length;\n const smoothing = this.smoothingFactor;\n for (let i = 0; i < frameCount; ++i) for (let c = 0; c < channelCount; c++) {\n const buf = this.buffers[c];\n if (!buf) continue;\n this.smoothedDelaySamples[c] += (targetDelaySamples - this.smoothedDelaySamples[c]) * smoothing;\n const smoothedDelay = this.smoothedDelaySamples[c];\n const intDelay = Math.floor(smoothedDelay);\n const frac = smoothedDelay - intDelay;\n const readPtrA = (buf.writePtr - intDelay + buf.buffer.length) % buf.buffer.length;\n const readPtrB = (readPtrA - 1 + buf.buffer.length) % buf.buffer.length;\n const sampleA = buf.buffer[readPtrA];\n const sampleB = buf.buffer[readPtrB];\n let delayed = sampleA * (1 - frac) + sampleB * frac;\n for (const mode of this.characterModes) if (mode === \"bitCrushed\") delayed = this._processLoFi(delayed, c);\n else if (mode === \"filtered\") delayed = this._processBandpass(delayed, c);\n output[c][i] = compressSingleSample(delayed, .75, 4, {\n enabled: true,\n type: \"soft\",\n outputRange: {\n min: -.9,\n max: .9\n }\n });\n const inputSample = input[c] && input[c][i] !== void 0 ? input[c][i] : 0;\n buf.write(inputSample + delayed * feedbackAmount);\n buf.updatePointers(intDelay);\n }\n return true;\n }\n});\n//#endregion\n//#region src/worklets/processors/reverb/dattorro-reverb-processor.js\nvar DattorroReverb = class extends AudioWorkletProcessor {\n static get parameterDescriptors() {\n return [\n [\n \"preDelay\",\n 0,\n 0,\n sampleRate - 1,\n \"k-rate\"\n ],\n [\n \"bandwidth\",\n .9999,\n 0,\n 1,\n \"k-rate\"\n ],\n [\n \"inputDiffusion1\",\n .75,\n 0,\n 1,\n \"k-rate\"\n ],\n [\n \"inputDiffusion2\",\n .625,\n 0,\n 1,\n \"k-rate\"\n ],\n [\n \"decay\",\n .5,\n 0,\n 1,\n \"k-rate\"\n ],\n [\n \"decayDiffusion1\",\n .7,\n 0,\n .999999,\n \"k-rate\"\n ],\n [\n \"decayDiffusion2\",\n .5,\n 0,\n .999999,\n \"k-rate\"\n ],\n [\n \"damping\",\n .005,\n 0,\n 1,\n \"k-rate\"\n ],\n [\n \"excursionRate\",\n .5,\n 0,\n 2,\n \"k-rate\"\n ],\n [\n \"excursionDepth\",\n .7,\n 0,\n 2,\n \"k-rate\"\n ],\n [\n \"wet\",\n .3,\n 0,\n 1,\n \"k-rate\"\n ],\n [\n \"dry\",\n .6,\n 0,\n 1,\n \"k-rate\"\n ]\n ].map((x) => /* @__PURE__ */ new Object({\n name: x[0],\n defaultValue: x[1],\n minValue: x[2],\n maxValue: x[3],\n automationRate: x[4]\n }));\n }\n constructor(options) {\n super(options);\n this._Delays = [];\n this._pDLength = sampleRate + (128 - sampleRate % 128);\n this._preDelay = new Float32Array(this._pDLength);\n this._pDWrite = 0;\n this._lp1 = 0;\n this._lp2 = 0;\n this._lp3 = 0;\n this._excPhase = 0;\n const SHORT_DELAY_SCALE = .5;\n [\n .004771345,\n .003595309,\n .012734787,\n .009307483,\n .022579886,\n .149625349,\n .060481839,\n .1249958,\n .030509727,\n .141695508,\n .089244313,\n .106280031\n ].map((x) => x * SHORT_DELAY_SCALE).forEach((x) => this.makeDelay(x));\n this._taps = Int16Array.from([\n .008937872,\n .099929438,\n .064278754,\n .067067639,\n .066866033,\n .006283391,\n .035818689,\n .011861161,\n .121870905,\n .041262054,\n .08981553,\n .070931756,\n .011256342,\n .004065724\n ], (x) => Math.round(x * sampleRate));\n this.port.postMessage({ type: \"initialized\" });\n }\n makeDelay(length) {\n let len = Math.round(length * sampleRate);\n let nextPow2 = 2 ** Math.ceil(Math.log2(len));\n this._Delays.push([\n new Float32Array(nextPow2),\n len - 1,\n 0,\n nextPow2 - 1\n ]);\n }\n writeDelay(index, data) {\n return this._Delays[index][0][this._Delays[index][1]] = data;\n }\n readDelay(index) {\n return this._Delays[index][0][this._Delays[index][2]];\n }\n readDelayAt(index, i) {\n let d = this._Delays[index];\n return d[0][d[2] + i & d[3]];\n }\n readDelayCAt(index, i) {\n let d = this._Delays[index], frac = i - ~~i, int = ~~i + d[2] - 1, mask = d[3];\n let x0 = d[0][int++ & mask], x1 = d[0][int++ & mask], x2 = d[0][int++ & mask], x3 = d[0][int & mask];\n let a = (3 * (x1 - x2) - x0 + x3) / 2, b = 2 * x2 + x0 - (5 * x1 + x3) / 2, c = (x2 - x0) / 2;\n return ((a * frac + b) * frac + c) * frac + x1;\n }\n process(inputs, outputs, parameters) {\n const TWO_PI = 6.283185307179586;\n const TWO_PI_DETUNE = 6.284702653297906;\n const pd = ~~parameters.preDelay[0], bw = parameters.bandwidth[0], fi = parameters.inputDiffusion1[0], si = parameters.inputDiffusion2[0], dc = parameters.decay[0], ft = parameters.decayDiffusion1[0], st = parameters.decayDiffusion2[0], dp = 1 - parameters.damping[0], ex = parameters.excursionRate[0] / sampleRate, ed = parameters.excursionDepth[0] * sampleRate / 1e3, we = parameters.wet[0] * .6, dr = parameters.dry[0];\n if (inputs[0].length == 2) for (let i = 127; i >= 0; i--) {\n this._preDelay[this._pDWrite + i] = (inputs[0][0][i] + inputs[0][1][i]) * .5;\n outputs[0][0][i] = inputs[0][0][i] * dr;\n outputs[0][1][i] = inputs[0][1][i] * dr;\n }\n else if (inputs[0].length > 0) {\n this._preDelay.set(inputs[0][0], this._pDWrite);\n for (let i = 127; i >= 0; i--) outputs[0][0][i] = outputs[0][1][i] = inputs[0][0][i] * dr;\n } else this._preDelay.set(/* @__PURE__ */ new Float32Array(128), this._pDWrite);\n let i = 0;\n while (i < 128) {\n let lo = 0, ro = 0;\n this._lp1 += bw * (this._preDelay[(this._pDLength + this._pDWrite - pd + i) % this._pDLength] - this._lp1);\n let pre = this.writeDelay(0, this._lp1 - fi * this.readDelay(0));\n pre = this.writeDelay(1, fi * (pre - this.readDelay(1)) + this.readDelay(0));\n pre = this.writeDelay(2, fi * pre + this.readDelay(1) - si * this.readDelay(2));\n pre = this.writeDelay(3, si * (pre - this.readDelay(3)) + this.readDelay(2));\n let split = si * pre + this.readDelay(3);\n let exc = ed * (1 + Math.cos(this._excPhase * TWO_PI));\n let exc2 = ed * (1 + Math.sin(this._excPhase * TWO_PI_DETUNE));\n let temp = this.writeDelay(4, split + dc * this.readDelay(11) + ft * this.readDelayCAt(4, exc));\n this.writeDelay(5, this.readDelayCAt(4, exc) - ft * temp);\n this._lp2 += dp * (this.readDelay(5) - this._lp2);\n temp = this.writeDelay(6, dc * this._lp2 - st * this.readDelay(6));\n this.writeDelay(7, this.readDelay(6) + st * temp);\n temp = this.writeDelay(8, split + dc * this.readDelay(7) + ft * this.readDelayCAt(8, exc2));\n this.writeDelay(9, this.readDelayCAt(8, exc2) - ft * temp);\n this._lp3 += dp * (this.readDelay(9) - this._lp3);\n temp = this.writeDelay(10, dc * this._lp3 - st * this.readDelay(10));\n this.writeDelay(11, this.readDelay(10) + st * temp);\n lo = this.readDelayAt(9, this._taps[0]) + this.readDelayAt(9, this._taps[1]) - this.readDelayAt(10, this._taps[2]) + this.readDelayAt(11, this._taps[3]) - this.readDelayAt(5, this._taps[4]) - this.readDelayAt(6, this._taps[5]) - this.readDelayAt(7, this._taps[6]);\n ro = this.readDelayAt(5, this._taps[7]) + this.readDelayAt(5, this._taps[8]) - this.readDelayAt(6, this._taps[9]) + this.readDelayAt(7, this._taps[10]) - this.readDelayAt(9, this._taps[11]) - this.readDelayAt(10, this._taps[12]) - this.readDelayAt(11, this._taps[13]);\n outputs[0][0][i] += lo * we;\n outputs[0][1][i] += ro * we;\n this._excPhase += ex;\n if (this._excPhase >= 1) this._excPhase -= 1;\n i++;\n const delays = this._Delays;\n for (let j = 0; j < delays.length; j++) {\n const d = delays[j];\n d[1] = d[1] + 1 & d[3];\n d[2] = d[2] + 1 & d[3];\n }\n }\n this._pDWrite = (this._pDWrite + 128) % this._pDLength;\n return true;\n }\n};\nregisterProcessor(\"dattorro-reverb-processor\", DattorroReverb);\n//#endregion\n//#region src/worklets/processors/distortion/distortion-processor.js\nvar Distortion = class {\n constructor() {\n this.limitingMode = \"hard-clipping\";\n }\n applyDrive(sample, driveAmount) {\n if (driveAmount <= 0) return sample;\n return sample * (1 + driveAmount * 3);\n }\n applyClipping(sample, clippingAmount, clipThreshold) {\n if (clippingAmount <= 0) return sample;\n let clippedSample;\n switch (this.limitingMode) {\n case \"soft-clipping\":\n clippedSample = clipThreshold * Math.tanh(sample / clipThreshold);\n break;\n case \"hard-clipping\":\n clippedSample = Math.max(-clipThreshold, Math.min(clipThreshold, sample));\n break;\n default: clippedSample = sample;\n }\n if (clipThreshold < .08) {\n const makeupGain = Math.min(2, Math.pow(.1 / clipThreshold, .5));\n clippedSample *= makeupGain;\n }\n return sample * (1 - clippingAmount) + clippedSample * clippingAmount;\n }\n setLimitingMode(mode) {\n this.limitingMode = mode;\n }\n};\nregisterProcessor(\"distortion-processor\", class extends AudioWorkletProcessor {\n static get parameterDescriptors() {\n return [\n {\n name: \"distortionDrive\",\n defaultValue: 0,\n minValue: 0,\n maxValue: 1,\n automationRate: \"a-rate\"\n },\n {\n name: \"clippingAmount\",\n defaultValue: 0,\n minValue: 0,\n maxValue: 1,\n automationRate: \"a-rate\"\n },\n {\n name: \"clippingThreshold\",\n defaultValue: .5,\n minValue: 0,\n maxValue: 1,\n automationRate: \"k-rate\"\n }\n ];\n }\n constructor() {\n super();\n this.distortion = new Distortion();\n this.setupMessageHandling();\n this.port.postMessage({ type: \"initialized\" });\n }\n setupMessageHandling() {\n this.port.onmessage = (event) => {\n switch (event.data.type) {\n case \"setLimitingMode\":\n this.distortion.setLimitingMode(event.data.mode);\n break;\n default: console.warn(\"distortion-processor: Unsupported message\");\n }\n };\n }\n process(inputs, outputs, parameters) {\n const input = inputs[0];\n const output = outputs[0];\n if (!input || !output) return true;\n const clipThreshold = parameters.clippingThreshold[0];\n for (let i = 0; i < output[0].length; ++i) {\n const distortionDrive = parameters.distortionDrive[Math.min(i, parameters.distortionDrive.length - 1)];\n const clippingAmount = parameters.clippingAmount[Math.min(i, parameters.clippingAmount.length - 1)];\n for (let c = 0; c < Math.min(input.length, output.length); c++) {\n let sample = input[c][i];\n sample = this.distortion.applyDrive(sample, distortionDrive);\n sample = this.distortion.applyClipping(sample, clippingAmount, clipThreshold);\n output[c][i] = Math.max(-.999, Math.min(.999, sample));\n }\n }\n return true;\n }\n});\n//#endregion\n//#region src/worklets/processors/follower/envelope-follower-processor.js\nregisterProcessor(\"envelope-follower-processor\", class extends AudioWorkletProcessor {\n static get parameterDescriptors() {\n return [\n {\n name: \"inputGain\",\n defaultValue: 1,\n minValue: 0,\n maxValue: 10,\n automationRate: \"k-rate\"\n },\n {\n name: \"outputGain\",\n defaultValue: 1,\n minValue: 0,\n maxValue: 10,\n automationRate: \"k-rate\"\n },\n {\n name: \"attack\",\n defaultValue: .003,\n minValue: .001,\n maxValue: 1,\n automationRate: \"k-rate\"\n },\n {\n name: \"release\",\n defaultValue: .05,\n minValue: .001,\n maxValue: 5,\n automationRate: \"k-rate\"\n }\n ];\n }\n constructor() {\n super();\n this.envelope = 0;\n this.gateThreshold = .005;\n this.debugCounter = 0;\n this.port.postMessage({ type: \"initialized\" });\n }\n process(inputs, outputs, parameters) {\n const input = inputs[0];\n const output = outputs[0];\n const channel = inputs[0][0];\n if (!input || !output || !channel || input.length === 0 || output.length === 0 || channel.length === 0) return true;\n const inChannel = input[0];\n if (!inChannel || inChannel.length === 0) return true;\n const attack = parameters.attack[0];\n const release = parameters.release[0];\n const inputGain = parameters.inputGain[0];\n const outputGain = parameters.outputGain[0];\n const attackCoeff = Math.exp(-1 / (attack * sampleRate));\n const releaseCoeff = Math.exp(-1 / (release * sampleRate));\n for (let sample = 0; sample < output[0].length; sample++) {\n const inputLevel = Math.abs((input[0][sample] || 0) * inputGain);\n if (inputLevel > 1e-6) {\n if (inputLevel > this.envelope) this.envelope = inputLevel + (this.envelope - inputLevel) * attackCoeff;\n else this.envelope = inputLevel + (this.envelope - inputLevel) * releaseCoeff;\n } else this.envelope *= releaseCoeff;\n if (this.envelope < this.gateThreshold) this.envelope = 0;\n const finalOutput = this.envelope * outputGain;\n for (let channel = 0; channel < output.length; channel++) output[channel][sample] = finalOutput;\n }\n return true;\n }\n});\n//#endregion\n", Tt = !1;
|
|
4687
4684
|
async function Et(e) {
|
|
4688
4685
|
if (Tt) return console.info("AudioWorklet processors already initialized, skipping"), {
|
|
4689
4686
|
success: !0,
|
|
@@ -167,7 +167,7 @@ var SamplePlayerProcessor = class extends AudioWorkletProcessor {
|
|
|
167
167
|
return this.layers[0] ?? null;
|
|
168
168
|
}
|
|
169
169
|
#handleMessage(event) {
|
|
170
|
-
const { type, value, buffer, layers, timestamp, durationSeconds, zeroCrossings,
|
|
170
|
+
const { type, value, buffer, layers, timestamp, durationSeconds, zeroCrossings, playbackDirection } = event.data;
|
|
171
171
|
switch (type) {
|
|
172
172
|
case "voice:reset":
|
|
173
173
|
this.#resetState();
|
|
@@ -287,8 +287,6 @@ var SamplePlayerProcessor = class extends AudioWorkletProcessor {
|
|
|
287
287
|
this.applyClickCompensation = true;
|
|
288
288
|
}
|
|
289
289
|
}
|
|
290
|
-
#clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
291
|
-
#clampZeroCrossing = (value) => this.#clamp(value, this.minZeroCrossing, this.maxZeroCrossing);
|
|
292
290
|
#findNearestZeroCrossing(position, direction = "any", maxDistance = null) {
|
|
293
291
|
if (!this.zeroCrossings || this.zeroCrossings.length === 0) return position;
|
|
294
292
|
const closestValue = findClosest(this.zeroCrossings, position, direction);
|
|
@@ -296,15 +294,6 @@ var SamplePlayerProcessor = class extends AudioWorkletProcessor {
|
|
|
296
294
|
return closestValue;
|
|
297
295
|
}
|
|
298
296
|
/**
|
|
299
|
-
* Convert normalized position (0-1) to sample index
|
|
300
|
-
* @param {number} normalizedPosition - Position as 0-1 value
|
|
301
|
-
* @returns {number} - Sample index
|
|
302
|
-
*/
|
|
303
|
-
#normalizedToSamples(normalizedPosition) {
|
|
304
|
-
if (!this.buffer || !this.buffer[0]) return 0;
|
|
305
|
-
return normalizedPosition * this.buffer[0].length;
|
|
306
|
-
}
|
|
307
|
-
/**
|
|
308
297
|
* Convert sample index to normalized position (0-1)
|
|
309
298
|
* @param {number} sampleIndex - Sample index
|
|
310
299
|
* @returns {number} - Normalized position 0-1
|
|
@@ -322,13 +311,6 @@ var SamplePlayerProcessor = class extends AudioWorkletProcessor {
|
|
|
322
311
|
return Math.max(0, Math.min(1, midiVelocity / 127));
|
|
323
312
|
}
|
|
324
313
|
/**
|
|
325
|
-
* Get buffer duration in seconds
|
|
326
|
-
* @returns {number} - Buffer duration in seconds
|
|
327
|
-
*/
|
|
328
|
-
#getBufferDurationSeconds() {
|
|
329
|
-
return (this.buffer?.[0]?.length || 0) / sampleRate;
|
|
330
|
-
}
|
|
331
|
-
/**
|
|
332
314
|
* Calculate musical note durations in samples for given tempo
|
|
333
315
|
* @param {number} tempo - BPM
|
|
334
316
|
* @returns {Object} - Musical note durations in samples
|
|
@@ -686,7 +668,7 @@ var RandomNoiseProcessor = class extends AudioWorkletProcessor {
|
|
|
686
668
|
calculateAlpha(frequency) {
|
|
687
669
|
return frequency / (frequency + sampleRate / (2 * Math.PI));
|
|
688
670
|
}
|
|
689
|
-
process(inputs, outputs,
|
|
671
|
+
process(inputs, outputs, _parameters) {
|
|
690
672
|
outputs[0].forEach((channel) => {
|
|
691
673
|
for (let i = 0; i < channel.length; i++) {
|
|
692
674
|
const noise = Math.random() * 2 - 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kidlib/web-audio",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "High-level Web Audio primitives for musical instruments and tools.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -29,38 +29,38 @@
|
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
|
-
"scripts": {
|
|
33
|
-
"build:worklets": "vp node build-processors.js",
|
|
34
|
-
"build": "vp run clean && vp build",
|
|
35
|
-
"prepack": "vp run build",
|
|
36
|
-
"watch:processors": "vp node watch-processors.js",
|
|
37
|
-
"watch": "concurrently \"vp test watch\" \"vp build --watch\" \"vp run watch:processors\"",
|
|
38
|
-
"clean": "rimraf dist",
|
|
39
|
-
"test": "vp test run",
|
|
40
|
-
"test:browser": "vp test run --config vitest.browser.config.ts",
|
|
41
|
-
"test:coverage": "vp test run --coverage",
|
|
42
|
-
"test:watch": "vp test",
|
|
43
|
-
"test:all": "vp run test && vp run test:browser",
|
|
44
|
-
"prepare": "vp config"
|
|
45
|
-
},
|
|
46
32
|
"dependencies": {
|
|
47
|
-
"webmidi": "^3.1.
|
|
33
|
+
"webmidi": "^3.1.16"
|
|
48
34
|
},
|
|
49
35
|
"devDependencies": {
|
|
36
|
+
"@changesets/cli": "3.0.1",
|
|
50
37
|
"@types/audioworklet": "^0.0.72",
|
|
51
38
|
"@types/node": "^24.13.3",
|
|
52
|
-
"@
|
|
53
|
-
"@vitest/
|
|
54
|
-
"@vitest/coverage-v8": "catalog:",
|
|
39
|
+
"@vitest/browser-playwright": "4.1.10",
|
|
40
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
55
41
|
"concurrently": "^9.1.2",
|
|
56
42
|
"jsdom": "^26.0.0",
|
|
57
43
|
"playwright": "^1.53.2",
|
|
58
44
|
"rimraf": "^5.0.1",
|
|
59
45
|
"typescript": "5.8.3",
|
|
60
|
-
"vite": "
|
|
46
|
+
"vite": "npm:@voidzero-dev/vite-plus-core@0.2.9",
|
|
61
47
|
"vite-plugin-dts": "^4.5.3",
|
|
62
|
-
"vite-plus": "
|
|
63
|
-
"vitest": "
|
|
48
|
+
"vite-plus": "0.2.9",
|
|
49
|
+
"vitest": "4.1.10"
|
|
64
50
|
},
|
|
65
|
-
"
|
|
66
|
-
|
|
51
|
+
"scripts": {
|
|
52
|
+
"changeset": "vp exec changeset",
|
|
53
|
+
"bump": "vp exec changeset version",
|
|
54
|
+
"release": "vp check && vp run test:all && vp exec changeset publish",
|
|
55
|
+
"build:worklets": "node build-processors.js",
|
|
56
|
+
"build": "vp run clean && vp build",
|
|
57
|
+
"watch:processors": "node watch-processors.js",
|
|
58
|
+
"watch": "concurrently \"vp test watch\" \"vp build --watch\" \"vp run watch:processors\"",
|
|
59
|
+
"clean": "rimraf dist",
|
|
60
|
+
"test": "vp test run",
|
|
61
|
+
"test:browser": "vp test run --config vitest.browser.config.ts",
|
|
62
|
+
"test:coverage": "vp test run --coverage",
|
|
63
|
+
"test:watch": "vp test",
|
|
64
|
+
"test:all": "vp run test && vp run test:browser"
|
|
65
|
+
}
|
|
66
|
+
}
|