Skip to content

Commit

Permalink
feat: simplify ks algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
danigb committed Sep 30, 2024
1 parent e5476fa commit 336f87c
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 149 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ This library wouldn't be possible with all the people writing books, blog posts
- [Cmajor](https://github.com/SoundStacks/cmajor)
- [VCVRack](https://github.com/VCVRack/Rack)
- [The Synthesis ToolKit](https://github.com/thestk/stk)
- [Surge synth](https://github.com/surge-synthesizer/surge)
- [Surge Rust](https://github.com/klebs6/surge-rs)
- https://github.com/jd-13/WE-Core
- https://github.com/mhetrick/nonlinearcircuits
- https://github.com/timowest/analogue
Expand Down
170 changes: 64 additions & 106 deletions packages/karplus-strong/src/dsp.ts
Original file line number Diff line number Diff line change
@@ -1,120 +1,78 @@
export function createKS(sampleRate: number, minFrequency: number) {
let active = false;
let startExciter = false;
const targetAmplitude = 0.001; // Amplitude decays to 0.1% of initial value
const maxDelayLineLength = Math.ceil(sampleRate / minFrequency) + 2; // Extra samples for interpolation

const excite = createExciter(1000);
const delayLine = new Float32Array(maxDelayLineLength);

// Delay line
const maxIndex = Math.floor(sampleRate / minFrequency + 0.5);
const delay = new InterpolatedDelayLine(maxIndex);
let delayInSamples = maxDelayLineLength - 2;
let writeIndex = 0;

// One pole filter for delay
let filterAlpha = 0.99;
let prevFilterOut = 0;
let isPlaying = false;
let prevTrigger = 0;

// Feedback
let lossFactor = 0.99;

// Param cache
let $freq = 0;
let $damping = 0;

return function (
return (
output: Float32Array,
trigger: number,
frequency: number,
damping: number
) {
if ($freq !== frequency) {
$freq = frequency;
delay.setDelayLength(sampleRate / frequency);
}
if ($damping !== damping) {
$damping = damping;
lossFactor = damping * 0.2 + 0.792;
filterAlpha = Math.max(0, Math.min(1, damping));
}

if (trigger === 1) {
if (!active) {
active = true;
startExciter = true;
decay: number
) => {
const outputLength = output.length;

const decayTimeInSamples = decay * sampleRate;
const filterCoefficient = Math.pow(targetAmplitude, 1 / decayTimeInSamples);

if (trigger > 0 && prevTrigger <= 0) {
delayInSamples = sampleRate / frequency;
delayInSamples = Math.min(
Math.max(delayInSamples, 1),
maxDelayLineLength - 2
);

for (let i = 0; i < maxDelayLineLength; i++) {
delayLine[i] = Math.random() * 2 - 1;
}
} else {
active = false;
writeIndex = 0;
isPlaying = true;
}

for (let i = 0; i < output.length; i++) {
const pluck = excite(startExciter);
startExciter = false;

//const prev = delay.read(delayLength - 1);
const curr = delay.read();
const filterOut =
curr * 1.0 * filterAlpha + prevFilterOut * (1 - filterAlpha);
prevFilterOut = filterOut;
output[i] = pluck * filterOut * lossFactor;
}
};
}

class InterpolatedDelayLine {
buffer: Float32Array;
writeIndex: number;
delayLength: number;
position: number; //

constructor(public readonly size: number) {
this.buffer = new Float32Array(size);
this.writeIndex = 0;
this.delayLength = 10;
this.position = size - this.delayLength;
}

setDelayLength(delayLength: number) {
this.delayLength = delayLength;
this.position = this.writeIndex - delayLength;
while (this.position < 0) this.position += this.size;
}

writeAndUpdateIndex(value: number) {
this.buffer[this.writeIndex] = value;
this.writeIndex++;
if (this.writeIndex >= this.size) this.writeIndex = 0;
this.position++;
if (this.position >= this.size) this.position -= this.size;
}

read() {
while (this.position < 0) this.position += this.size;
const curr = Math.floor(this.position);
let next = curr + 1;
while (next >= this.size) next -= this.size;
const frac = this.position - curr;
const currVal = this.buffer[curr];
const nextVal = this.buffer[next];
return currVal - frac + (nextVal - currVal);
}
}

function createExciter(durationInSamples: number) {
const window = new Float32Array(durationInSamples);

// Create a triangular envelope
for (let i = 0; i < durationInSamples; i++) {
window[i] = 1 - Math.abs(i / (durationInSamples / 2) - 1);
}

let index = window.length;

return function (start: boolean) {
if (start) index = 0;
if (index < window.length) {
index++;
const noise = Math.random() * 2 - 1;
return noise * window[index];
prevTrigger = trigger;

if (isPlaying) {
for (let i = 0; i < outputLength; i++) {
let readIndex = writeIndex - delayInSamples;
if (readIndex < 0) {
readIndex += maxDelayLineLength;
}

const readIndexInt = Math.floor(readIndex);
const frac = readIndex - readIndexInt;

// Wrap around the delay line
const index0 = readIndexInt % maxDelayLineLength;
const index1 = (readIndexInt + 1) % maxDelayLineLength;

// Linear interpolation between two samples
const sample0 = delayLine[index0];
const sample1 = delayLine[index1];
const currentSample = sample0 + frac * (sample1 - sample0);

const nextSample = filterCoefficient * currentSample;
delayLine[writeIndex] = nextSample;
output[i] = currentSample;

writeIndex = (writeIndex + 1) % maxDelayLineLength;

// Stop playing if the signal has decayed below a threshold
if (Math.abs(currentSample) < 1e-6) {
isPlaying = false;
for (let j = i + 1; j < outputLength; j++) {
output[j] = 0;
}
break;
}
}
} else {
return 0;
// Output silence when not playing
output.fill(0);
}
};
}
6 changes: 3 additions & 3 deletions packages/karplus-strong/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ export const registerKarplusStrongWorklet = createRegistrar(
export type KarplusStrongInputs = {
trigger: ParamInput;
frequency: ParamInput;
damping: ParamInput;
decay: ParamInput;
};

export type KarplusStrongWorkletNode = AudioWorkletNode & {
trigger: AudioParam;
frequency: AudioParam;
damping: AudioParam;
decay: AudioParam;
dispose(): void;
};

Expand All @@ -28,7 +28,7 @@ export const KarplusStrong = createWorkletConstructor<
KarplusStrongInputs
>({
processorName: "KsProcessor",
paramNames: ["trigger", "frequency", "damping"],
paramNames: ["trigger", "frequency", "decay"],
workletOptions: () => ({
numberOfInputs: 0,
numberOfOutputs: 1,
Expand Down
2 changes: 1 addition & 1 deletion packages/karplus-strong/src/processor.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const PROCESSOR = `"use strict";(()=>{function w(s,t){let i=!1,e=!1,r=F(1e3),o=Math.floor(s/t+.5),f=new c(o),a=.99,p=0,b=.99,d=0,x=0;return function(g,y,h,n){d!==h&&(d=h,f.setDelayLength(s/h)),x!==n&&(x=n,b=n*.2+.792,a=Math.max(0,Math.min(1,n))),y===1?i||(i=!0,e=!0):i=!1;for(let l=0;l<g.length;l++){let A=r(e);e=!1;let m=f.read()*1*a+p*(1-a);p=m,g[l]=A*m*b}}}var c=class{constructor(t){this.size=t;this.buffer=new Float32Array(t),this.writeIndex=0,this.delayLength=10,this.position=t-this.delayLength}buffer;writeIndex;delayLength;position;setDelayLength(t){for(this.delayLength=t,this.position=this.writeIndex-t;this.position<0;)this.position+=this.size}writeAndUpdateIndex(t){this.buffer[this.writeIndex]=t,this.writeIndex++,this.writeIndex>=this.size&&(this.writeIndex=0),this.position++,this.position>=this.size&&(this.position-=this.size)}read(){for(;this.position<0;)this.position+=this.size;let t=Math.floor(this.position),i=t+1;for(;i>=this.size;)i-=this.size;let e=this.position-t,r=this.buffer[t],o=this.buffer[i];return r-e+(o-r)}};function F(s){let t=new Float32Array(s);for(let e=0;e<s;e++)t[e]=1-Math.abs(e/(s/2)-1);let i=t.length;return function(e){return e&&(i=0),i<t.length?(i++,(Math.random()*2-1)*t[i]):0}}var u=class extends AudioWorkletProcessor{r;g;constructor(){super(),this.r=!0,this.g=w(sampleRate,20),this.port.onmessage=t=>{switch(t.data.type){case"DISPOSE":this.r=!1;break}}}process(t,i,e){let r=i[0][0];return this.g(r,e.trigger[0],e.frequency[0],e.damping[0]),this.r}static get parameterDescriptors(){return[["trigger",0,0,1],["frequency",440,20,2e4],["damping",.1,0,1]].map(([t,i,e,r])=>({name:t,defaultValue:i,minValue:e,maxValue:r,automationRate:"k-rate"}))}};registerProcessor("KsProcessor",u);})();`;
export const PROCESSOR = `"use strict";(()=>{function b(o,n){let e=Math.ceil(o/n)+2,t=new Float32Array(e),i=e-2,a=0,f=!1,h=0;return(c,y,S,A)=>{let g=c.length,I=A*o,M=Math.pow(.001,1/I);if(y>0&&h<=0){i=o/S,i=Math.min(Math.max(i,1),e-2);for(let r=0;r<e;r++)t[r]=Math.random()*2-1;a=0,f=!0}if(h=y,f)for(let r=0;r<g;r++){let l=a-i;l<0&&(l+=e);let m=Math.floor(l),k=l-m,w=m%e,F=(m+1)%e,x=t[w],L=t[F],u=x+k*(L-x),P=M*u;if(t[a]=P,c[r]=u,a=(a+1)%e,Math.abs(u)<1e-6){f=!1;for(let p=r+1;p<g;p++)c[p]=0;break}}else c.fill(0)}}var d=class extends AudioWorkletProcessor{r;g;constructor(){super(),this.r=!0,this.g=b(sampleRate,100),this.port.onmessage=n=>{switch(n.data.type){case"DISPOSE":this.r=!1;break}}}process(n,s,e){let t=s[0][0];return this.g(t,e.trigger[0],e.frequency[0],e.decay[0]),this.r}static get parameterDescriptors(){return[["trigger",0,0,1],["frequency",440,20,2e4],["decay",.1,.01,5]].map(([n,s,e,t])=>({name:n,defaultValue:s,minValue:e,maxValue:t,automationRate:"k-rate"}))}};registerProcessor("KsProcessor",d);})();`;
6 changes: 3 additions & 3 deletions packages/karplus-strong/src/worklet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class KsProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.r = true;
this.g = createKS(sampleRate, 20);
this.g = createKS(sampleRate, 100);
this.port.onmessage = (event) => {
switch (event.data.type) {
case "DISPOSE":
Expand All @@ -19,7 +19,7 @@ export class KsProcessor extends AudioWorkletProcessor {

process(inputs: Float32Array[][], outputs: Float32Array[][], params: any) {
const output = outputs[0][0];
this.g(output, params.trigger[0], params.frequency[0], params.damping[0]);
this.g(output, params.trigger[0], params.frequency[0], params.decay[0]);

return this.r;
}
Expand All @@ -28,7 +28,7 @@ export class KsProcessor extends AudioWorkletProcessor {
return [
["trigger", 0, 0, 1],
["frequency", 440, 20, 20000],
["damping", 0.1, 0, 1],
["decay", 0.1, 0.01, 5],
].map(([name, defaultValue, minValue, maxValue]) => ({
name,
defaultValue,
Expand Down
Loading

0 comments on commit 336f87c

Please sign in to comment.