mosfez-synth
Version:
A microtonal-aware synth engine library for web.
277 lines (271 loc) • 7.04 kB
JavaScript
function validateParamDefinition(name, paramDef) {
if (!isConstant(paramDef) && !isVariable(paramDef)) {
throw new Error(`param "${name}" must be a constant number or a string referring to a param name, but got ${paramDef}`);
}
return paramDef;
}
function isConstant(paramDef) {
return typeof paramDef === "number";
}
function isVariable(paramDef) {
return typeof paramDef === "string";
}
class DspNode {
type;
constructor(config) {
this.type = config.type;
}
static isFaustDspNode(DspNode2) {
return DspNode2.type === "faust";
}
static isPolyDspNode(DspNode2) {
return DspNode2.type === "poly";
}
static isFaustDspNodeSerialized(serialized) {
return serialized.type === "faust";
}
static isPolyDspNodeSerialized(serialized) {
return serialized.type === "poly";
}
serialize() {
throw new Error(".serialize() can only be called on subclasses");
}
}
class DspNodePoly extends DspNode {
input;
polyphony;
paramCacheSize;
release;
gate;
dependencies;
constructor(config) {
super({
type: "poly"
});
this.input = config.input;
this.polyphony = config.polyphony;
this.paramCacheSize = config.paramCacheSize;
this.release = validateParamDefinition("release", config.release);
this.gate = validateParamDefinition("gate", config.gate);
this.dependencies = config.dependencies;
}
serialize() {
const { polyphony, paramCacheSize, release, gate } = this;
const input = this.input.serialize();
return {
type: "poly",
input,
polyphony,
paramCacheSize,
release,
gate
};
}
}
function findOldestVoiceIndex(voices) {
if (voices.length === 0)
return -1;
const oldest = voices.reduce((prev, current) => {
const usePrev = !current || prev && prev.time < current.time;
return usePrev ? prev : current;
});
return oldest?.voice ?? -1;
}
class VoiceAllocator {
_time = 0;
_voices = [];
constructor(total) {
this._voices.length = total;
}
_startVoice(voice, id) {
const existing = this._voices[voice];
if (existing?.released !== void 0) {
clearTimeout(existing.released);
}
this._voices[voice] = {
id,
voice,
time: this._time++
};
return voice;
}
_stopVoice(voice) {
this._voices[voice] = void 0;
}
_findStarted(id) {
return this._voices.findIndex((v) => v && v.id === id && v.released === void 0);
}
_findReleased(id) {
return this._voices.findIndex((v) => v && v.id === id && v.released !== void 0);
}
get voices() {
return this._voices.map((voice) => voice?.id);
}
get(id) {
return this._voices.findIndex((v) => v && v.id === id);
}
start(id) {
const started = this._findStarted(id);
if (started !== -1) {
return [started, false];
}
const released = this._findReleased(id);
if (released !== -1) {
return [this._startVoice(released, id), false];
}
const stopped = this._voices.findIndex((v) => !v);
if (stopped !== -1) {
return [this._startVoice(stopped, id), true];
}
const releasedVoices = this._voices.filter((v) => v?.released !== void 0);
const oldestReleased = findOldestVoiceIndex(releasedVoices);
if (oldestReleased !== -1) {
return [this._startVoice(oldestReleased, id), true];
}
const oldestActive = findOldestVoiceIndex(this._voices);
if (oldestActive !== -1) {
return [this._startVoice(oldestActive, id), true];
}
throw new Error("unable to find oldest active voice");
}
stop(id) {
const started = this._findStarted(id);
if (started !== -1) {
this._stopVoice(started);
}
return [started, false];
}
release(id, ms) {
const started = this._findStarted(id);
const voiceObject = this._voices[started];
if (voiceObject) {
voiceObject.released = setTimeout(() => {
this._stopVoice(started);
}, ms);
}
return [started, false];
}
}
class VoiceController {
_polyphony;
_resolveGate;
_release = 2e3;
_paramCacheSize;
_allocator;
_allParams = {};
_perVoiceParamsStore = /* @__PURE__ */ new Map();
_recentlyAccessedIds = /* @__PURE__ */ new Set();
constructor(config) {
const { polyphony, paramCacheSize, resolveGate } = config;
this._polyphony = polyphony;
this._paramCacheSize = paramCacheSize;
this._resolveGate = resolveGate;
this._allocator = new VoiceAllocator(polyphony);
}
_accessId(id) {
const set = this._recentlyAccessedIds;
set.delete(id);
set.add(id);
while (set.size > this._paramCacheSize) {
const id2 = set.keys().next().value;
set.delete(id2);
this._deleteParamsForId(id2);
}
}
_getParamsForVoice(id) {
this._accessId(id);
const out = {};
this._perVoiceParamsStore.forEach((subMap, key) => {
const value = subMap.get(id);
if (value !== void 0) {
out[key] = value;
}
});
return out;
}
_addParamForVoice(paramKey, id, paramValue) {
const subMap = this._perVoiceParamsStore.get(paramKey) ?? /* @__PURE__ */ new Map();
subMap.set(id, paramValue);
this._perVoiceParamsStore.set(paramKey, subMap);
}
_addParamsForVoice(id, params) {
this._accessId(id);
for (const key in params) {
const value = params[key];
if (value !== void 0) {
this._addParamForVoice(key, id, value);
}
}
}
_deleteParamsForId(id) {
this._perVoiceParamsStore.forEach((subMap) => {
subMap.delete(id);
});
}
setRelease(release) {
this._release = release;
}
set({ voice, ...params }) {
if (voice === void 0) {
return this.setAll(params);
}
return this.setWithId(`${voice}`, params);
}
setWithId(id, params) {
const gate = this._resolveGate(params);
const { _allocator, _allParams } = this;
let index = -1;
if (gate !== void 0) {
[index] = gate > 0 ? _allocator.start(id) : _allocator.release(id, this._release);
} else {
index = _allocator.get(id);
}
if (index === -1)
return [];
const perVoiceParams = this._getParamsForVoice(id);
const mergedParams = {
..._allParams,
...perVoiceParams,
...params
};
this._addParamsForVoice(id, mergedParams);
return [
{
index,
params: mergedParams
}
];
}
setAll(params) {
this._allParams = {
...this._allParams,
...params
};
for (const paramName in params) {
this._perVoiceParamsStore.set(paramName, /* @__PURE__ */ new Map());
}
const out = [];
for (let i = 0; i < this._polyphony; i++) {
out.push({
index: i,
params
});
}
return out;
}
}
function poly(params) {
const { input, polyphony, paramCacheSize, release, gate } = params;
return new DspNodePoly({
input,
polyphony,
paramCacheSize,
release,
gate,
dependencies: {
VoiceController
}
});
}
export { poly };
//# sourceMappingURL=poly.mjs.map