UNPKG

mosfez-synth

Version:

A microtonal-aware synth engine library for web.

277 lines (271 loc) 7.04 kB
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