spessasynth_lib 3.24.7 → 3.24.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/sequencer/worklet_sequencer/events.js +4 -1
- package/sequencer/worklet_sequencer/process_tick.js +6 -3
- package/sequencer/worklet_sequencer/worklet_sequencer.js +5 -4
- package/synthetizer/synthetizer.js +2 -2
- package/synthetizer/worklet_processor.min.js +9 -9
- package/synthetizer/worklet_system/main_processor.js +11 -27
- package/synthetizer/worklet_system/message_protocol/README.md +13 -0
- package/synthetizer/worklet_system/message_protocol/handle_message.js +1 -0
- package/synthetizer/worklet_system/worklet_methods/note_on.js +1 -4
- package/synthetizer/worklet_system/worklet_methods/voice_control.js +0 -2
- package/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +72 -46
- package/synthetizer/worklet_system/worklet_utilities/stereo_panner.js +57 -31
- package/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +3 -3
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
// noinspection JSUnresolvedReference
|
|
2
|
-
|
|
3
1
|
import { DEFAULT_PERCUSSION, DEFAULT_SYNTH_MODE, VOICE_CAP } from "../synthetizer.js";
|
|
4
2
|
import { WorkletSequencer } from "../../sequencer/worklet_sequencer/worklet_sequencer.js";
|
|
5
3
|
import { SpessaSynthInfo } from "../../utils/loggin.js";
|
|
6
4
|
import { consoleColors } from "../../utils/other.js";
|
|
7
|
-
import {
|
|
5
|
+
import { releaseVoice, renderVoice, voiceKilling } from "./worklet_methods/voice_control.js";
|
|
8
6
|
import { ALL_CHANNELS_OR_DIFFERENT_ACTION, returnMessageType } from "./message_protocol/worklet_message.js";
|
|
9
7
|
import { stbvorbis } from "../../externals/stbvorbis_sync/stbvorbis_sync.min.js";
|
|
10
8
|
import { VOLUME_ENVELOPE_SMOOTHING_FACTOR } from "./worklet_utilities/volume_envelope.js";
|
|
@@ -51,7 +49,7 @@ import { WorkletSoundfontManager } from "./worklet_methods/worklet_soundfont_man
|
|
|
51
49
|
import { interpolationTypes } from "./worklet_utilities/wavetable_oscillator.js";
|
|
52
50
|
import { WorkletKeyModifierManager } from "./worklet_methods/worklet_key_modifier.js";
|
|
53
51
|
import { getWorkletVoices } from "./worklet_utilities/worklet_voice.js";
|
|
54
|
-
import { panVoice } from "./worklet_utilities/stereo_panner.js";
|
|
52
|
+
import { PAN_SMOOTHING_FACTOR, panVoice } from "./worklet_utilities/stereo_panner.js";
|
|
55
53
|
|
|
56
54
|
|
|
57
55
|
/**
|
|
@@ -68,6 +66,7 @@ export const MIN_EXCLUSIVE_LENGTH = 0.07;
|
|
|
68
66
|
export const SYNTHESIZER_GAIN = 1.0;
|
|
69
67
|
|
|
70
68
|
|
|
69
|
+
// noinspection JSUnresolvedReference
|
|
71
70
|
class SpessaSynthProcessor extends AudioWorkletProcessor
|
|
72
71
|
{
|
|
73
72
|
/**
|
|
@@ -106,11 +105,6 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
|
|
|
106
105
|
*/
|
|
107
106
|
this.interpolationType = interpolationTypes.fourthOrder;
|
|
108
107
|
|
|
109
|
-
/**
|
|
110
|
-
* @type {function}
|
|
111
|
-
*/
|
|
112
|
-
this.processTickCallback = undefined;
|
|
113
|
-
|
|
114
108
|
this.sequencer = new WorkletSequencer(this);
|
|
115
109
|
|
|
116
110
|
this.transposition = 0;
|
|
@@ -214,7 +208,7 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
|
|
|
214
208
|
this.workletProcessorChannels[DEFAULT_PERCUSSION].preset = this.drumPreset;
|
|
215
209
|
this.workletProcessorChannels[DEFAULT_PERCUSSION].drumChannel = true;
|
|
216
210
|
|
|
217
|
-
// these smoothing factors were tested on 44,100 Hz, adjust them here
|
|
211
|
+
// these smoothing factors were tested on 44,100 Hz, adjust them to target sample rate here
|
|
218
212
|
this.volumeEnvelopeSmoothingFactor = VOLUME_ENVELOPE_SMOOTHING_FACTOR * (44100 / sampleRate);
|
|
219
213
|
this.panSmoothingFactor = PAN_SMOOTHING_FACTOR * (44100 / sampleRate);
|
|
220
214
|
|
|
@@ -327,10 +321,8 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
|
|
|
327
321
|
{
|
|
328
322
|
return false;
|
|
329
323
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
this.processTickCallback();
|
|
333
|
-
}
|
|
324
|
+
// process the sequencer playback
|
|
325
|
+
this.sequencer.processTick();
|
|
334
326
|
|
|
335
327
|
// for every channel
|
|
336
328
|
let totalCurrentVoices = 0;
|
|
@@ -365,13 +357,8 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
|
|
|
365
357
|
chorusChannels = outputs[1];
|
|
366
358
|
}
|
|
367
359
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
// reset voices
|
|
371
|
-
channel.voices = [];
|
|
372
|
-
|
|
373
|
-
// for every voice
|
|
374
|
-
tempV.forEach(v =>
|
|
360
|
+
// for every voice, render it
|
|
361
|
+
channel.voices = channel.voices.filter(v =>
|
|
375
362
|
{
|
|
376
363
|
// render voice
|
|
377
364
|
this.renderVoice(
|
|
@@ -381,11 +368,8 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
|
|
|
381
368
|
reverbChannels,
|
|
382
369
|
chorusChannels
|
|
383
370
|
);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
// if not finished, add it back
|
|
387
|
-
channel.voices.push(v);
|
|
388
|
-
}
|
|
371
|
+
|
|
372
|
+
return !v.finished;
|
|
389
373
|
});
|
|
390
374
|
|
|
391
375
|
totalCurrentVoices += channel.voices.length;
|
|
@@ -433,7 +417,7 @@ SpessaSynthProcessor.prototype.handleMessage = handleMessage;
|
|
|
433
417
|
SpessaSynthProcessor.prototype.sendChannelProperties = sendChannelProperties;
|
|
434
418
|
SpessaSynthProcessor.prototype.callEvent = callEvent;
|
|
435
419
|
|
|
436
|
-
// system
|
|
420
|
+
// system-exclusive related
|
|
437
421
|
SpessaSynthProcessor.prototype.systemExclusive = systemExclusive;
|
|
438
422
|
|
|
439
423
|
// note messages related
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# About the message protocol
|
|
2
|
+
Since spessasynth runs in the audioWorklet thread, here is an explanation of how it works:
|
|
3
|
+
|
|
4
|
+
There's one processor per synthesizer, with a `MessagePort` for communication.
|
|
5
|
+
Each processor has a single `WorkletSequencer` instance that is idle by default.
|
|
6
|
+
|
|
7
|
+
The `Synthetizer`,
|
|
8
|
+
`Sequencer` and `SoundFontManager` classes are all interfaces
|
|
9
|
+
that do not do anything except sending the commands to te processor.
|
|
10
|
+
|
|
11
|
+
The synthesizer sends the commands (note on, off, etc.) directly to the processor where they are processed and executed.
|
|
12
|
+
|
|
13
|
+
The sequencer sends the commands through the connected synthesizer's messagePort, which then get processed as sequencer messages and routed
|
|
@@ -123,10 +123,7 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen
|
|
|
123
123
|
// as it's interpolated (we don't want 0 attenuation for even a split second)
|
|
124
124
|
voice.volumeEnvelope.attenuation = voice.volumeEnvelope.attenuationTargetGain;
|
|
125
125
|
// set initial pan to avoid split second changing from middle to the correct value
|
|
126
|
-
voice.currentPan =
|
|
127
|
-
-500,
|
|
128
|
-
Math.min(500, voice.modulatedGenerators[generatorTypes.pan])
|
|
129
|
-
) + 500) / 1000); // 0 to 1
|
|
126
|
+
voice.currentPan = Math.max(-500, Math.min(500, voice.modulatedGenerators[generatorTypes.pan])); // -500 to 500
|
|
130
127
|
});
|
|
131
128
|
|
|
132
129
|
this.totalVoicesAmount += voices.length;
|
|
@@ -13,8 +13,6 @@ import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js"
|
|
|
13
13
|
import { customControllers } from "../worklet_utilities/controller_tables.js";
|
|
14
14
|
import { WorkletLowpassFilter } from "../worklet_utilities/lowpass_filter.js";
|
|
15
15
|
|
|
16
|
-
export const PAN_SMOOTHING_FACTOR = 0.05;
|
|
17
|
-
|
|
18
16
|
/**
|
|
19
17
|
* Renders a voice to the stereo output buffer
|
|
20
18
|
* @param channel {WorkletProcessorChannel} the voice's channel
|
|
@@ -4,13 +4,29 @@ import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js"
|
|
|
4
4
|
/**
|
|
5
5
|
* lowpass_filter.js
|
|
6
6
|
* purpose: applies a low pass filter to a voice
|
|
7
|
-
* note to self:
|
|
8
|
-
*
|
|
7
|
+
* note to self: a lot of tricks and fixes come from fluidsynth.
|
|
8
|
+
* They are the real smart guys.
|
|
9
9
|
* Shoutout to them!
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} CachedCoefficient
|
|
14
|
+
* @property {number} a0 - Filter coefficient 1
|
|
15
|
+
* @property {number} a1 - Filter coefficient 2
|
|
16
|
+
* @property {number} a2 - Filter coefficient 3
|
|
17
|
+
* @property {number} a3 - Filter coefficient 4
|
|
18
|
+
* @property {number} a4 - Filter coefficient 5
|
|
19
|
+
*/
|
|
20
|
+
|
|
12
21
|
export class WorkletLowpassFilter
|
|
13
22
|
{
|
|
23
|
+
/**
|
|
24
|
+
* Cached coefficient calculations
|
|
25
|
+
* stored as cachedCoefficients[reasonanceCb][cutoffCents]
|
|
26
|
+
* @type {CachedCoefficient[][]}
|
|
27
|
+
* @private
|
|
28
|
+
*/
|
|
29
|
+
static cachedCoefficients = [];
|
|
14
30
|
/**
|
|
15
31
|
* Filter coefficient 1
|
|
16
32
|
* @type {number}
|
|
@@ -71,25 +87,13 @@ export class WorkletLowpassFilter
|
|
|
71
87
|
*/
|
|
72
88
|
reasonanceCb = 0;
|
|
73
89
|
|
|
74
|
-
/**
|
|
75
|
-
* Resonance gain
|
|
76
|
-
* @type {number}
|
|
77
|
-
*/
|
|
78
|
-
reasonanceGain = 1;
|
|
79
|
-
|
|
80
90
|
/**
|
|
81
91
|
* Cutoff frequency in cents
|
|
82
|
-
* Note: defaults to
|
|
92
|
+
* Note: defaults to 13,501 to cause a recalculation even at initial fc being 13,500
|
|
83
93
|
* @type {number}
|
|
84
94
|
*/
|
|
85
95
|
cutoffCents = 13501;
|
|
86
96
|
|
|
87
|
-
/**
|
|
88
|
-
* Cutoff frequency in Hz
|
|
89
|
-
* @type {number}
|
|
90
|
-
*/
|
|
91
|
-
cutoffHz = 20001;
|
|
92
|
-
|
|
93
97
|
/**
|
|
94
98
|
* Applies a low-pass filter to the given buffer
|
|
95
99
|
* @param voice {WorkletVoice} the voice we're working on
|
|
@@ -139,36 +143,58 @@ export class WorkletLowpassFilter
|
|
|
139
143
|
*/
|
|
140
144
|
static calculateCoefficients(filter)
|
|
141
145
|
{
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
146
|
+
const cutoffCents = ~~filter.cutoffCents; // Math.floor
|
|
147
|
+
const qCb = filter.reasonanceCb;
|
|
148
|
+
// check if these coefficients were already cached
|
|
149
|
+
if (WorkletLowpassFilter.cachedCoefficients?.[qCb]?.[cutoffCents] === undefined)
|
|
150
|
+
{
|
|
151
|
+
let cutoffHz = absCentsToHz(cutoffCents);
|
|
152
|
+
|
|
153
|
+
// fix cutoff on low frequencies (fluid_iir_filter.c line 392)
|
|
154
|
+
cutoffHz = Math.min(cutoffHz, 0.45 * sampleRate);
|
|
155
|
+
|
|
156
|
+
const qDb = qCb / 10;
|
|
157
|
+
// correct the filter gain, like fluid does
|
|
158
|
+
const reasonanceGain = decibelAttenuationToGain(-1 * (qDb - 3.01)); // -1 because it's attenuation, and we don't want attenuation
|
|
159
|
+
|
|
160
|
+
// reduce the gain by the Q factor (fluid_iir_filter.c line 250)
|
|
161
|
+
const qGain = 1 / Math.sqrt(decibelAttenuationToGain(-qDb));
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
// initial filtering code was ported from meltysynth created by sinshu.
|
|
165
|
+
let w = 2 * Math.PI * cutoffHz / sampleRate; // we're in the AudioWorkletGlobalScope so we can use sampleRate
|
|
166
|
+
let cosw = Math.cos(w);
|
|
167
|
+
let alpha = Math.sin(w) / (2 * reasonanceGain);
|
|
168
|
+
|
|
169
|
+
let b1 = (1 - cosw) * qGain;
|
|
170
|
+
let b0 = b1 / 2;
|
|
171
|
+
let b2 = b0;
|
|
172
|
+
let a0 = 1 + alpha;
|
|
173
|
+
let a1 = -2 * cosw;
|
|
174
|
+
let a2 = 1 - alpha;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* set coefficients
|
|
178
|
+
* @type {CachedCoefficient}
|
|
179
|
+
*/
|
|
180
|
+
const toCache = {};
|
|
181
|
+
toCache.a0 = b0 / a0;
|
|
182
|
+
toCache.a1 = b1 / a0;
|
|
183
|
+
toCache.a2 = b2 / a0;
|
|
184
|
+
toCache.a3 = a1 / a0;
|
|
185
|
+
toCache.a4 = a2 / a0;
|
|
186
|
+
|
|
187
|
+
if (WorkletLowpassFilter.cachedCoefficients[qCb] === undefined)
|
|
188
|
+
{
|
|
189
|
+
WorkletLowpassFilter.cachedCoefficients[qCb] = [];
|
|
190
|
+
}
|
|
191
|
+
WorkletLowpassFilter.cachedCoefficients[qCb][cutoffCents] = toCache;
|
|
192
|
+
}
|
|
193
|
+
const cached = WorkletLowpassFilter.cachedCoefficients[qCb][cutoffCents];
|
|
194
|
+
filter.a0 = cached.a0;
|
|
195
|
+
filter.a1 = cached.a1;
|
|
196
|
+
filter.a2 = cached.a2;
|
|
197
|
+
filter.a3 = cached.a3;
|
|
198
|
+
filter.a4 = cached.a4;
|
|
173
199
|
}
|
|
174
200
|
}
|
|
@@ -1,14 +1,32 @@
|
|
|
1
1
|
import { generatorTypes } from "../../../soundfont/basic_soundfont/generator.js";
|
|
2
2
|
|
|
3
|
-
export const WORKLET_SYSTEM_REVERB_DIVIDER = 4600;
|
|
4
|
-
export const WORKLET_SYSTEM_CHORUS_DIVIDER = 2000;
|
|
5
|
-
const HALF_PI = Math.PI / 2;
|
|
6
|
-
|
|
7
3
|
/**
|
|
8
4
|
* stereo_panner.js
|
|
9
5
|
* purpose: pans a given voice out to the stereo output and to the effects' outputs
|
|
10
6
|
*/
|
|
11
7
|
|
|
8
|
+
export const PAN_SMOOTHING_FACTOR = 0.1;
|
|
9
|
+
|
|
10
|
+
export const WORKLET_SYSTEM_REVERB_DIVIDER = 4600;
|
|
11
|
+
export const WORKLET_SYSTEM_CHORUS_DIVIDER = 2000;
|
|
12
|
+
const HALF_PI = Math.PI / 2;
|
|
13
|
+
|
|
14
|
+
const MIN_PAN = -500;
|
|
15
|
+
const MAX_PAN = 500;
|
|
16
|
+
const PAN_RESOLUTION = MAX_PAN - MIN_PAN;
|
|
17
|
+
|
|
18
|
+
// initialize pan lookup tables
|
|
19
|
+
const panTableLeft = new Float32Array(PAN_RESOLUTION + 1);
|
|
20
|
+
const panTableRight = new Float32Array(PAN_RESOLUTION + 1);
|
|
21
|
+
for (let pan = MIN_PAN; pan <= MAX_PAN; pan++)
|
|
22
|
+
{
|
|
23
|
+
// clamp to 0-1
|
|
24
|
+
const realPan = (pan - MIN_PAN) / PAN_RESOLUTION;
|
|
25
|
+
const tableIndex = pan - MIN_PAN;
|
|
26
|
+
panTableLeft[tableIndex] = Math.cos(HALF_PI * realPan);
|
|
27
|
+
panTableRight[tableIndex] = Math.sin(HALF_PI * realPan);
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
/**
|
|
13
31
|
* Pans the voice to the given output buffers
|
|
14
32
|
* @param voice {WorkletVoice} the voice to pan
|
|
@@ -29,43 +47,51 @@ export function panVoice(voice,
|
|
|
29
47
|
{
|
|
30
48
|
return;
|
|
31
49
|
}
|
|
32
|
-
|
|
33
|
-
|
|
50
|
+
// clamp -500 to 500
|
|
51
|
+
const pan = Math.max(-500, Math.min(500, voice.modulatedGenerators[generatorTypes.pan]));
|
|
52
|
+
// smooth out pan to prevent clicking
|
|
53
|
+
voice.currentPan += (pan - voice.currentPan) * this.panSmoothingFactor;
|
|
34
54
|
|
|
35
55
|
const gain = this.currentGain;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
56
|
+
const index = ~~(pan + 500);
|
|
57
|
+
// get voice's gain levels for each channel
|
|
58
|
+
const gainLeft = panTableLeft[index] * gain * this.panLeft;
|
|
59
|
+
const gainRight = panTableRight[index] * gain * this.panRight;
|
|
40
60
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (reverbLevel > 0 && !this.oneOutputMode)
|
|
61
|
+
// disable reverb and chorus in one output mode
|
|
62
|
+
if (!this.oneOutputMode)
|
|
45
63
|
{
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
64
|
+
// reverb is mono so we need to multiply by gain
|
|
65
|
+
const reverbLevel = this.reverbGain * voice.modulatedGenerators[generatorTypes.reverbEffectsSend] / WORKLET_SYSTEM_REVERB_DIVIDER * gain;
|
|
66
|
+
// chorus is stereo so we do not need to
|
|
67
|
+
const chorusLevel = this.chorusGain * voice.modulatedGenerators[generatorTypes.chorusEffectsSend] / WORKLET_SYSTEM_CHORUS_DIVIDER;
|
|
68
|
+
|
|
69
|
+
if (reverbLevel > 0)
|
|
49
70
|
{
|
|
50
|
-
reverbLeft
|
|
51
|
-
reverbRight
|
|
71
|
+
const reverbLeft = reverb[0];
|
|
72
|
+
const reverbRight = reverb[1];
|
|
73
|
+
for (let i = 0; i < inputBuffer.length; i++)
|
|
74
|
+
{
|
|
75
|
+
reverbLeft[i] += reverbLevel * inputBuffer[i];
|
|
76
|
+
reverbRight[i] += reverbLevel * inputBuffer[i];
|
|
77
|
+
}
|
|
52
78
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (chorusLevel > 0 && !this.oneOutputMode)
|
|
56
|
-
{
|
|
57
|
-
const chorusLeft = chorus[0];
|
|
58
|
-
const chorusRight = chorus[1];
|
|
59
|
-
const chorusLeftGain = gainLeft * chorusLevel;
|
|
60
|
-
const chorusRightGain = gainRight * chorusLevel;
|
|
61
|
-
for (let i = 0; i < inputBuffer.length; i++)
|
|
79
|
+
|
|
80
|
+
if (chorusLevel > 0)
|
|
62
81
|
{
|
|
63
|
-
chorusLeft
|
|
64
|
-
chorusRight
|
|
82
|
+
const chorusLeft = chorus[0];
|
|
83
|
+
const chorusRight = chorus[1];
|
|
84
|
+
const chorusLeftGain = gainLeft * chorusLevel;
|
|
85
|
+
const chorusRightGain = gainRight * chorusLevel;
|
|
86
|
+
for (let i = 0; i < inputBuffer.length; i++)
|
|
87
|
+
{
|
|
88
|
+
chorusLeft[i] += chorusLeftGain * inputBuffer[i];
|
|
89
|
+
chorusRight[i] += chorusRightGain * inputBuffer[i];
|
|
90
|
+
}
|
|
65
91
|
}
|
|
66
92
|
}
|
|
67
93
|
|
|
68
|
-
// mix
|
|
94
|
+
// mix down the audio data
|
|
69
95
|
if (gainLeft > 0)
|
|
70
96
|
{
|
|
71
97
|
for (let i = 0; i < inputBuffer.length; i++)
|
|
@@ -220,10 +220,10 @@ class WorkletVoice
|
|
|
220
220
|
currentTuningCalculated = 1;
|
|
221
221
|
|
|
222
222
|
/**
|
|
223
|
-
* From
|
|
223
|
+
* From -500 to 500.
|
|
224
224
|
* @param {number}
|
|
225
225
|
*/
|
|
226
|
-
currentPan = 0
|
|
226
|
+
currentPan = 0;
|
|
227
227
|
|
|
228
228
|
/**
|
|
229
229
|
* If MIDI Tuning Standard is already applied (at note-on time),
|
|
@@ -337,7 +337,7 @@ export function getWorkletVoices(channel,
|
|
|
337
337
|
// override patch
|
|
338
338
|
const overridePatch = this.keyModifierManager.hasOverridePatch(channel, midiNote);
|
|
339
339
|
|
|
340
|
-
//
|
|
340
|
+
// override patch is not cached
|
|
341
341
|
if (cached !== undefined && !overridePatch)
|
|
342
342
|
{
|
|
343
343
|
return cached.map(v => WorkletVoice.copy(v, currentTime));
|