Created
June 16, 2021 17:40
-
-
Save fserb/adb21b207a2eef890f8abd565fe9e1e5 to your computer and use it in GitHub Desktop.
Karplus-Strong with Jaffe-Smith
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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