Skip to content

Instantly share code, notes, and snippets.

@fserb
Created June 16, 2021 17:40
Show Gist options
  • Save fserb/adb21b207a2eef890f8abd565fe9e1e5 to your computer and use it in GitHub Desktop.
Save fserb/adb21b207a2eef890f8abd565fe9e1e5 to your computer and use it in GitHub Desktop.
Karplus-Strong with Jaffe-Smith
import {TAU, sampleRate} from "./utils.js";
/*
http://www.music.mcgill.ca/~gary/courses/papers/Karplus-Strong-CMJ-1983.pdf
http://musicweb.ucsd.edu/~trsmyth/papers/KSExtensions.pdf
Simple explanation: http://amid.fish/karplus-strong
+---+ b +---+
x / 0 ------->| B +-+-->| D +--> y
^ +---+ | +---+
|c v
| +---+ a+---+
+--+ C |<-+ A |
+---+ +---+
where
B = Z^-p
D = (1 - R) / (1 - RZ^-1)
A = (1 - S) + SZ^-1
C = (C + Z^-1) / (1 + CZ^-1)
A <- low pass filter of Karplus-Strong (the original one)
B <- wavetable
C <- pitch correction due to quantization on Math.floor(N)
D <- dynamics
(notice that the "block C" contains the constant C)
c is the recorded signal on the wavetable.
b is the time delayed value
y is the final result
where:
b = cz^-p
a = Ab
c = Ca
y = Dc
There's also a multiplier F that represents energy loss and used for drums.
*/
export class KarplusStrong {
constructor(params) {
this.rho = params.rho ?? 0.99;
this.amp = params.amp ?? 1;
this.b = params.b ?? 1;
this.L = params.L ?? 10000; // dynamic level (Hertz)
this.saveFreq = -1;
this.C = 0;
this.R = 0;
this.cur = 0;
this.yn1 = 0;
this.bn1 = 0;
this.an1 = 0;
this.cn1 = 0;
this.wavetable = new Float32Array(0);
}
play() {
return new KarplusStrong(this);
}
_update(freq) {
const SR = sampleRate;
const Ts = 1 / SR;
const wTs = TAU * freq * Ts;
const P1 = SR / freq;
// this should provide a slightly better tuning than just using
// const N = P1;
const s = this.S = 82.407 / (2 * freq);
const Pa = s * Math.sin(wTs) / (wTs * (1 - s) + s * wTs * Math.cos(wTs));
const N = Math.floor(P1 - Pa);
const Pc = P1 - N - Pa;
this.C = Math.sin(wTs * (1 - Pc) / 2) / Math.sin(wTs * (1 + Pc) / 2);
const fm = Math.sqrt(20 * SR / 2);
const Rl = Math.exp(-Math.PI * this.L * Ts);
const Gl = (1 - Rl) / Math.sqrt(
Rl * Rl * (Math.sin(TAU * fm * Ts) ** 2) +
(1 - Rl * Math.cos(TAU * fm * Ts)) ** 2);
const Ra = (1 - Gl * Gl * Math.cos(wTs)) / (1 - Gl * Gl);
const Rb = 2 * Gl * Math.sin(wTs / 2) *
Math.sqrt(1 - Gl * Gl * (Math.cos(wTs / 2) ** 2)) /
(1 - Gl * Gl);
const R1 = Ra + Rb;
const R2 = Ra - Rb;
this.R = R1 <= 1 ? R1 : R2;
const old = this.wavetable;
this.wavetable = new Float32Array(N);
// if this is the initial pluck, pluck it.
if (old.length == 0) {
const w2Ts = TAU * this.L / SR;
const x = 1 - Math.cos(w2Ts);
const a = -x + Math.sqrt(x * x + 2 * x);
const mm = Math.pow(2, (SR / 2 - this.L) / (SR / 2)) / 2;
let last = 0;
for (let i = 0; i < N; ++i) {
const v = (Math.random() * 2 - 1) * this.amp * mm;
const n = v * a + (1 - a) * last;
this.wavetable[i] = n;
last = n;
}
} else {
// if we are changing frequencies, we scale the wavetable values
// so we can hear the string vibrating the same way it was before in
// the now shorter array.
const step = old.length / N;
// we also reset the position of cur. This is not strictly necessary,
// but avoid us having to calculate what is the new cur of the array.
let cur = this.cur;
for (let i = 0; i < N; ++i) {
const x = Math.floor(cur);
const y = (x + 1) % old.length;
const d = cur - x;
this.wavetable[i] = old[x] * (1 - d) + old[y] * d;
cur = (cur + step) % old.length;
}
this.cur = 0;
}
}
step(freq) {
if (freq != this.saveFreq) {
this._update(freq);
this.saveFreq = freq;
}
const sign = Math.random() < this.b ? 1 : -1;
const F = this.rho * sign;
const bn = this.wavetable[this.cur];
const an = (1 - this.S) * bn + this.S * this.bn1;
const cn = F * (this.C * an + this.an1 - this.C * this.cn1);
const yn = (1 - this.R) * cn + this.R * this.yn1;
this.yn1 = yn;
this.bn1 = bn;
this.an1 = an;
this.cn1 = cn;
this.wavetable[this.cur] = cn;
this.cur = (this.cur + 1) % this.wavetable.length;
return yn;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment