spessasynth_lib 3.20.35 → 3.20.40

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.
@@ -49,7 +49,9 @@ import { getWorkletVoices } from './worklet_utilities/worklet_voice.js'
49
49
  * purpose: manages the synthesizer (and worklet sequencer) from the AudioWorkletGlobalScope and renders the audio data
50
50
  */
51
51
 
52
- export const MIN_NOTE_LENGTH = 0.07; // if the note is released faster than that, it forced to last that long
52
+ // if the note is released faster than that, it forced to last that long
53
+ // this is used mostly for drum channels, where a lot of midis like to send instant note off after a note on
54
+ export const MIN_NOTE_LENGTH = 0.03;
53
55
 
54
56
  export const SYNTHESIZER_GAIN = 1.0;
55
57
 
@@ -84,7 +86,7 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
84
86
  * Interpolation type used
85
87
  * @type {interpolationTypes}
86
88
  */
87
- this.interpolationType = interpolationTypes.linear;
89
+ this.interpolationType = interpolationTypes.fourthOrder;
88
90
 
89
91
  /**
90
92
  * @type {function}
@@ -1,6 +1,6 @@
1
1
  import { consoleColors } from '../../../utils/other.js'
2
2
  import { midiControllers } from '../../../midi_parser/midi_message.js'
3
- import { dataEntryStates } from '../worklet_utilities/worklet_processor_channel.js'
3
+ import { channelConfiguration, dataEntryStates } from '../worklet_utilities/worklet_processor_channel.js'
4
4
  import { computeModulators } from '../worklet_utilities/worklet_modulator.js'
5
5
  import { SpessaSynthInfo, SpessaSynthWarn } from '../../../utils/loggin.js'
6
6
  import { SYNTHESIZER_GAIN } from '../main_processor.js'
@@ -8,7 +8,7 @@ import { DEFAULT_PERCUSSION } from '../../synthetizer.js'
8
8
 
9
9
  /**
10
10
  * @param channel {number}
11
- * @param controllerNumber {midiControllers}
11
+ * @param controllerNumber {number}
12
12
  * @param controllerValue {number}
13
13
  * @param force {boolean}
14
14
  * @this {SpessaSynthProcessor}
@@ -24,6 +24,19 @@ export function controllerChange(channel, controllerNumber, controllerValue, for
24
24
  SpessaSynthWarn(`Trying to access channel ${channel} which does not exist... ignoring!`);
25
25
  return;
26
26
  }
27
+ if(controllerNumber > 127)
28
+ {
29
+ // channel configuration. force must be set to true
30
+ if(!force) return;
31
+ switch (controllerNumber)
32
+ {
33
+ default:
34
+ return;
35
+
36
+ case channelConfiguration.velocityOverride:
37
+ channelObject.velocityOverride = controllerValue;
38
+ }
39
+ }
27
40
  // lsb controller values: append them as the lower nibble of the 14 bit value
28
41
  // excluding bank select and data entry as it's handled separately
29
42
  if(
@@ -43,6 +43,12 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen
43
43
  sentMidiNote = this.tunings[program]?.[midiNote].midiNote;
44
44
  }
45
45
 
46
+ // velocity override
47
+ if(channelObject.velocityOverride > 0)
48
+ {
49
+ velocity = channelObject.velocityOverride;
50
+ }
51
+
46
52
  // get voices
47
53
  const voices = this.getWorkletVoices(
48
54
  channel,
@@ -59,6 +65,7 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen
59
65
  const exclusive = voice.generators[generatorTypes.exclusiveClass];
60
66
  if(exclusive !== 0)
61
67
  {
68
+ // kill all voices with the same exclusive class
62
69
  channelVoices.forEach(v => {
63
70
  if(v.generators[generatorTypes.exclusiveClass] === exclusive)
64
71
  {
@@ -72,7 +79,33 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen
72
79
  }
73
80
  // compute all modulators
74
81
  computeModulators(voice, channelObject.midiControllers);
75
- WorkletVolumeEnvelope.intialize(voice);
82
+ // modulate sample offsets (these are not real time)
83
+ const cursorStartOffset = voice.modulatedGenerators[generatorTypes.startAddrsOffset] + voice.modulatedGenerators[generatorTypes.startAddrsCoarseOffset] * 32768;
84
+ const endOffset = voice.modulatedGenerators[generatorTypes.endAddrOffset] + voice.modulatedGenerators[generatorTypes.endAddrsCoarseOffset] * 32768;
85
+ const loopStartOffset = voice.modulatedGenerators[generatorTypes.startloopAddrsOffset] + voice.modulatedGenerators[generatorTypes.startloopAddrsCoarseOffset] * 32768;
86
+ const loopEndOffset = voice.modulatedGenerators[generatorTypes.endloopAddrsOffset] + voice.modulatedGenerators[generatorTypes.endloopAddrsCoarseOffset] * 32768;
87
+ const sm = voice.sample;
88
+ // apply them
89
+ const clamp = num => Math.max(0, Math.min(sm.sampleData.length - 1, num));
90
+ sm.cursor = clamp( sm.cursor + cursorStartOffset);
91
+ sm.end = clamp(sm.end + endOffset);
92
+ sm.loopStart = clamp(sm.loopStart + loopStartOffset);
93
+ sm.loopEnd = clamp(sm.loopEnd + loopEndOffset);
94
+ // swap loops if needed
95
+ if(sm.loopEnd < sm.loopStart)
96
+ {
97
+ const temp = sm.loopStart;
98
+ sm.loopStart = sm.loopEnd;
99
+ sm.loopEnd = temp;
100
+ }
101
+ if (sm.loopEnd - sm.loopStart < 1)
102
+ {
103
+ sm.loopingMode = 0;
104
+ sm.isLooping = false;
105
+ }
106
+ // set the current attenuation to target,
107
+ // as it's interpolated (we don't want 0 attenuation for even a split second)
108
+ voice.volumeEnvelope.attenuation = voice.volumeEnvelope.attenuationTarget;
76
109
  // set initial pan to avoid split second changing from middle to the correct value
77
110
  voice.currentPan = ((Math.max(-500, Math.min(500, voice.modulatedGenerators[generatorTypes.pan] )) + 500) / 1000) // 0 to 1
78
111
  });
@@ -4,6 +4,7 @@ import { getLFOValue } from '../worklet_utilities/lfo.js'
4
4
  import { customControllers } from '../worklet_utilities/worklet_processor_channel.js'
5
5
  import { WorkletModulationEnvelope } from '../worklet_utilities/modulation_envelope.js'
6
6
  import {
7
+ getSampleCubic,
7
8
  getSampleLinear,
8
9
  getSampleNearest,
9
10
  interpolationTypes,
@@ -15,7 +16,7 @@ import { WorkletVolumeEnvelope } from '../worklet_utilities/volume_envelope.js'
15
16
 
16
17
 
17
18
  const HALF_PI = Math.PI / 2;
18
- export const PAN_SMOOTHING_FACTOR = 0.01;
19
+ export const PAN_SMOOTHING_FACTOR = 0.05;
19
20
  /**
20
21
  * Renders a voice to the stereo output buffer
21
22
  * @param channel {WorkletProcessorChannel} the voice's channel
@@ -40,10 +41,14 @@ export function renderVoice(
40
41
  // if not in release, check if the release time is
41
42
  if (currentTime >= voice.releaseStartTime)
42
43
  {
43
-
44
+ // release the voice here
44
45
  voice.isInRelease = true;
45
46
  WorkletVolumeEnvelope.startRelease(voice);
46
47
  WorkletModulationEnvelope.startRelease(voice);
48
+ if(voice.sample.loopingMode === 3)
49
+ {
50
+ voice.sample.isLooping = false;
51
+ }
47
52
  }
48
53
  }
49
54
 
@@ -163,6 +168,9 @@ export function renderVoice(
163
168
  case interpolationTypes.nearestNeighbor:
164
169
  getSampleNearest(voice, bufferOut);
165
170
  break;
171
+
172
+ case interpolationTypes.fourthOrder:
173
+ getSampleCubic(voice, bufferOut);
166
174
  }
167
175
 
168
176
  // lowpass filter
@@ -264,8 +272,4 @@ export function releaseVoice(voice)
264
272
  {
265
273
  voice.releaseStartTime = voice.startTime + MIN_NOTE_LENGTH;
266
274
  }
267
- if(voice.sample.loopingMode === 3)
268
- {
269
- voice.sample.isLooping = false;
270
- }
271
275
  }
@@ -140,10 +140,7 @@ export class WorkletLowpassFilter
140
140
  filter.cutoffHz = absCentsToHz(filter.cutoffCents);
141
141
 
142
142
  // fix cutoff on low frequencies (fluid_iir_filter.c line 392)
143
- if(filter.cutoffHz > 0.45 * sampleRate)
144
- {
145
- filter.cutoffHz = 0.45 * sampleRate;
146
- }
143
+ filter.cutoffHz = Math.min(filter.cutoffHz, 0.45 * sampleRate);
147
144
 
148
145
  // adjust the filterQ (fluid_iir_filter.c line 204)
149
146
  const qDb = (filter.reasonanceCb / 10) - 3.01;
@@ -10,7 +10,8 @@ export const VOLUME_ENVELOPE_SMOOTHING_FACTOR = 0.001;
10
10
 
11
11
  const DB_SILENCE = 100;
12
12
  const PERCEIVED_DB_SILENCE = 90;
13
- const PERCEIVED_GAIN_SILENCE = 0.005;
13
+ // around 96 dB of attenuation
14
+ const PERCEIVED_GAIN_SILENCE = 0.000015; // can't go lower than that (see #50)
14
15
 
15
16
  /**
16
17
  * VOL ENV STATES:
@@ -147,22 +148,11 @@ export class WorkletVolumeEnvelope
147
148
  WorkletVolumeEnvelope.recalculate(voice);
148
149
  }
149
150
 
150
- /**
151
- * Initializes a volume envelope
152
- * @param voice {WorkletVoice}
153
- */
154
- static intialize(voice)
155
- {
156
- WorkletVolumeEnvelope.recalculate(voice, true);
157
- voice.volumeEnvelope.attenuation = voice.volumeEnvelope.attenuationTarget;
158
- }
159
-
160
151
  /**
161
152
  * Recalculates the envelope
162
153
  * @param voice {WorkletVoice} the voice this envelope belongs to
163
- * @param setupInterpolated {boolean} if we should initialize the interpolated values (attenuation and sustain)
164
154
  */
165
- static recalculate(voice, setupInterpolated = false)
155
+ static recalculate(voice)
166
156
  {
167
157
  const env = voice.volumeEnvelope;
168
158
  const timecentsToSamples = tc =>
@@ -172,10 +162,6 @@ export class WorkletVolumeEnvelope
172
162
  // calculate absolute times (they can change so we have to recalculate every time
173
163
  env.attenuationTarget = Math.max(0, Math.min(voice.modulatedGenerators[generatorTypes.initialAttenuation], 1440)) / 10; // divide by ten to get decibels
174
164
  env.sustainDbRelative = Math.min(DB_SILENCE, voice.modulatedGenerators[generatorTypes.sustainVolEnv] / 10);
175
- if(setupInterpolated)
176
- {
177
- env.attenuation = env.attenuationTarget;
178
- }
179
165
  const sustainDb = Math.min(DB_SILENCE, env.sustainDbRelative);
180
166
 
181
167
  // calculate durations
@@ -57,11 +57,6 @@ export function getSampleLinear(voice, outputBuffer)
57
57
  }
58
58
  else
59
59
  {
60
- // check and correct end errors
61
- if(sample.end >= sampleData.length)
62
- {
63
- sample.end = sampleData.length - 1;
64
- }
65
60
  for (let i = 0; i < outputBuffer.length; i++)
66
61
  {
67
62
 
@@ -124,11 +119,6 @@ export function getSampleNearest(voice, outputBuffer)
124
119
  }
125
120
  else
126
121
  {
127
- // check and correct end errors
128
- if(sample.end >= sampleData.length)
129
- {
130
- sample.end = sampleData.length - 1;
131
- }
132
122
  for (let i = 0; i < outputBuffer.length; i++)
133
123
  {
134
124
 
@@ -148,4 +138,98 @@ export function getSampleNearest(voice, outputBuffer)
148
138
  }
149
139
  }
150
140
  sample.cursor = cur;
141
+ }
142
+
143
+
144
+
145
+ /**
146
+ * Fills the output buffer with raw sample data using cubic interpolation
147
+ * @param voice {WorkletVoice} the voice we're working on
148
+ * @param outputBuffer {Float32Array} the output buffer to write to
149
+ */
150
+ export function getSampleCubic(voice, outputBuffer)
151
+ {
152
+ const sample = voice.sample;
153
+ let cur = sample.cursor;
154
+ const sampleData = sample.sampleData;
155
+
156
+ if(sample.isLooping)
157
+ {
158
+ const loopLength = sample.loopEnd - sample.loopStart;
159
+ for (let i = 0; i < outputBuffer.length; i++)
160
+ {
161
+ // check for loop
162
+ while(cur >= sample.loopEnd)
163
+ {
164
+ cur -= loopLength;
165
+ }
166
+
167
+ // math comes from
168
+ // https://stackoverflow.com/questions/1125666/how-do-you-do-bicubic-or-other-non-linear-interpolation-of-re-sampled-audio-da
169
+
170
+ // grab the 4 points
171
+ const y0 = ~~cur; // point before the cursor. twice bitwise not is just a faster Math.floor
172
+ let y1 = y0 + 1; // point after the cursor
173
+ let y2 = y1 + 1; // point 1 after the cursor
174
+ let y3 = y2 + 1; // point 2 after the cursor
175
+ const t = cur - y0; // distance from y0 to cursor
176
+ // y0 is not handled here
177
+ // as it's math.floor of cur which is handled above
178
+ if(y1 >= sample.loopEnd) y1 -= loopLength;
179
+ if(y2 >= sample.loopEnd) y2 -= loopLength;
180
+ if(y3 >= sample.loopEnd) y3 -= loopLength;
181
+
182
+ // grab the samples
183
+ const x0 = sampleData[y0];
184
+ const x1 = sampleData[y1];
185
+ const x2 = sampleData[y2];
186
+ const x3 = sampleData[y3];
187
+
188
+ // interpolate
189
+ // const c0 = x1
190
+ const c1 = 0.5 * (x2 - x0);
191
+ const c2 = x0 - (2.5 * x1) + (2 * x2) - (0.5 * x3);
192
+ const c3 = (0.5 * (x3 - x0)) + (1.5 * (x1 - x2));
193
+ outputBuffer[i] = (((((c3 * t) + c2) * t) + c1) * t) + x1;
194
+
195
+
196
+ cur += sample.playbackStep * voice.currentTuningCalculated;
197
+ }
198
+ }
199
+ else
200
+ {
201
+ for (let i = 0; i < outputBuffer.length; i++)
202
+ {
203
+
204
+ // math comes from
205
+ // https://stackoverflow.com/questions/1125666/how-do-you-do-bicubic-or-other-non-linear-interpolation-of-re-sampled-audio-da
206
+
207
+ // grab the 4 points
208
+ const y0 = ~~cur; // point before the cursor. twice bitwise not is just a faster Math.floor
209
+ let y1 = y0 + 1; // point after the cursor
210
+ let y2 = y1 + 1; // point 1 after the cursor
211
+ let y3 = y2 + 1; // point 2 after the cursor
212
+ const t = cur - y0; // distance from y0 to cursor
213
+
214
+ // flag as finished if needed
215
+ if(y1 >= sample.end ||
216
+ y2 >= sample.end ||
217
+ y3 >= sample.end) {voice.finished = true; return;}
218
+
219
+ // grab the samples
220
+ const x0 = sampleData[y0];
221
+ const x1 = sampleData[y1];
222
+ const x2 = sampleData[y2];
223
+ const x3 = sampleData[y3];
224
+
225
+ // interpolate
226
+ const c1 = 0.5 * (x2 - x0);
227
+ const c2 = x0 - (2.5 * x1) + (2 * x2) - (0.5 * x3);
228
+ const c3 = (0.5 * (x3 - x0)) + (1.5 * (x1 - x2));
229
+ outputBuffer[i] = (((((c3 * t) + c2) * t) + c1) * t) + x1;
230
+
231
+ cur += sample.playbackStep * voice.currentTuningCalculated;
232
+ }
233
+ }
234
+ voice.sample.cursor = cur;
151
235
  }
@@ -1,4 +1,4 @@
1
- import { modulatorSources } from '../../../soundfont/read_sf2/modulators.js'
1
+ import { Modulator, modulatorSources } from '../../../soundfont/read_sf2/modulators.js'
2
2
  import { getModulatorCurveValue, MOD_PRECOMPUTED_LENGTH } from './modulator_curves.js'
3
3
  import { NON_CC_INDEX_OFFSET } from './worklet_processor_channel.js'
4
4
  import { generatorLimits, generatorTypes } from '../../../soundfont/read_sf2/generators.js'
@@ -21,6 +21,7 @@ export function computeWorkletModulator(controllerTable, modulator, voice)
21
21
  {
22
22
  if(modulator.transformAmount === 0)
23
23
  {
24
+ modulator.currentValue = 0;
24
25
  return 0;
25
26
  }
26
27
  // mapped to 0-16384
@@ -95,13 +96,15 @@ export function computeWorkletModulator(controllerTable, modulator, voice)
95
96
 
96
97
 
97
98
  // compute the modulator
98
- const computedValue = sourceValue * secondSrcValue * modulator.transformAmount;
99
+ let computedValue = sourceValue * secondSrcValue * modulator.transformAmount;
99
100
 
100
101
  if(modulator.transformType === 2)
101
102
  {
102
103
  // abs value
103
- return Math.abs(computedValue);
104
+ computedValue = Math.abs(computedValue);
104
105
  }
106
+
107
+ modulator.currentValue = computedValue;
105
108
  return computedValue;
106
109
  }
107
110
 
@@ -113,9 +116,13 @@ export function computeWorkletModulator(controllerTable, modulator, voice)
113
116
  * @param sourceIndex {number} enum for the source
114
117
  */
115
118
  export function computeModulators(voice, controllerTable, sourceUsesCC = -1, sourceIndex = 0) {
116
- const { modulators, generators, modulatedGenerators } = voice;
119
+ const modulators = voice.modulators;
120
+ const generators = voice.generators;
121
+ const modulatedGenerators = voice.modulatedGenerators;
117
122
 
118
123
  // Modulation envelope is cheap to recalculate
124
+ // why here and not at the bottom?
125
+ // I dunno, seems to work fine
119
126
  WorkletModulationEnvelope.recalculate(voice);
120
127
 
121
128
  if (sourceUsesCC === -1)
@@ -156,13 +163,14 @@ export function computeModulators(voice, controllerTable, sourceUsesCC = -1, sou
156
163
  {
157
164
  // Reset this destination
158
165
  modulatedGenerators[destination] = generators[destination];
159
- // Compute all modulators for this destination
166
+ // compute our modulator
167
+ computeWorkletModulator(controllerTable, mod, voice);
168
+ // sum the values of all modulators for this destination
160
169
  modulators.forEach(m => {
161
170
  if (m.modulatorDestination === destination)
162
171
  {
163
172
  const limits = generatorLimits[mod.modulatorDestination];
164
- const current = modulatedGenerators[mod.modulatorDestination];
165
- const newValue = current + computeWorkletModulator(controllerTable, m, voice);
173
+ const newValue = modulatedGenerators[mod.modulatorDestination] + m.currentValue;
166
174
  modulatedGenerators[mod.modulatorDestination] = Math.max(limits.min, Math.min(newValue, limits.max));
167
175
  }
168
176
  });
@@ -11,6 +11,7 @@ import { modulatorSources } from '../../../soundfont/read_sf2/modulators.js'
11
11
  * @property {Int16Array} keyCentTuning - tuning of individual keys in cents
12
12
  * @property {boolean} holdPedal - indicates whether the hold pedal is active
13
13
  * @property {boolean} drumChannel - indicates whether the channel is a drum channel
14
+ * @property {number} velocityOverride - overrides velocity if > 0 otherwise disabled
14
15
  *
15
16
  * @property {dataEntryStates} dataEntryState - the current state of the data entry
16
17
  * @property {number} NRPCoarse - the current coarse value of the Non-Registered Parameter
@@ -62,6 +63,8 @@ export function createWorkletChannel(sendEvent = false)
62
63
  channelOctaveTuning: new Int8Array(12),
63
64
  keyCentTuning: new Int16Array(128),
64
65
  channelVibrato: {delay: 0, depth: 0, rate: 0},
66
+ velocityOverride: 0,
67
+
65
68
  lockGSNRPNParams: false,
66
69
  holdPedal: false,
67
70
  isMuted: false,
@@ -92,6 +95,7 @@ resetArray[midiControllers.expressionController] = 127 << 7;
92
95
  resetArray[midiControllers.pan] = 64 << 7;
93
96
  resetArray[midiControllers.releaseTime] = 64 << 7;
94
97
  resetArray[midiControllers.brightness] = 64 << 7;
98
+ resetArray[midiControllers.timbreHarmonicContent] = 64 << 7;
95
99
  resetArray[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel] = 8192;
96
100
  resetArray[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange] = 2 << 7;
97
101
 
@@ -119,3 +123,11 @@ export const customControllers = {
119
123
  export const CUSTOM_CONTROLLER_TABLE_SIZE = Object.keys(customControllers).length;
120
124
  export const customResetArray = new Float32Array(CUSTOM_CONTROLLER_TABLE_SIZE);
121
125
  customResetArray[customControllers.modulationMultiplier] = 1;
126
+
127
+ /**
128
+ * This is a channel configuration enum, it is internally sent from Synthetizer via controller change
129
+ * @enum {number}
130
+ */
131
+ export const channelConfiguration = {
132
+ velocityOverride: 128, // overrides velocity for the given channel
133
+ }
@@ -3,6 +3,7 @@
3
3
  * purpose: prepares workletvoices from sample and generator data and manages sample dumping
4
4
  * note: sample dumping means sending it over to the AudioWorkletGlobalScope
5
5
  */
6
+ import { Modulator } from '../../../soundfont/read_sf2/modulators.js'
6
7
 
7
8
  class WorkletSample
8
9
  {
@@ -298,7 +299,7 @@ class WorkletVoice
298
299
  currentTime,
299
300
  voice.targetKey,
300
301
  voice.generators,
301
- voice.modulators.slice()
302
+ voice.modulators.map(m => Modulator.copy(m))
302
303
  );
303
304
  }
304
305
  }
@@ -329,7 +330,7 @@ export function getWorkletVoices(channel,
329
330
  const cached = channelObject.cachedVoices[midiNote][velocity];
330
331
  if(cached !== undefined)
331
332
  {
332
- workletVoices = cached.map(v => WorkletVoice.copy(v, currentTime));
333
+ return cached.map(v => WorkletVoice.copy(v, currentTime));
333
334
  }
334
335
  else
335
336
  {
@@ -368,8 +369,8 @@ export function getWorkletVoices(channel,
368
369
  }
369
370
 
370
371
  // determine looping mode now. if the loop is too small, disable
371
- let loopStart = (sampleAndGenerators.sample.sampleLoopStartIndex / 2) + (generators[generatorTypes.startloopAddrsOffset] + (generators[generatorTypes.startloopAddrsCoarseOffset] * 32768));
372
- let loopEnd = (sampleAndGenerators.sample.sampleLoopEndIndex / 2) + (generators[generatorTypes.endloopAddrsOffset] + (generators[generatorTypes.endloopAddrsCoarseOffset] * 32768));
372
+ let loopStart = (sampleAndGenerators.sample.sampleLoopStartIndex / 2);
373
+ let loopEnd = (sampleAndGenerators.sample.sampleLoopEndIndex / 2);
373
374
  let loopingMode = generators[generatorTypes.sampleModes];
374
375
  const sampleLength = sampleAndGenerators.sample.getAudioData().length;
375
376
  // clamp loop
@@ -381,17 +382,18 @@ export function getWorkletVoices(channel,
381
382
  loopingMode = 0;
382
383
  }
383
384
  /**
384
- * create the worklet sample and calculate offsets
385
+ * create the worklet sample
386
+ * offsets are calculated at note on time (to allow for modulation of them)
385
387
  * @type {WorkletSample}
386
388
  */
387
389
  const workletSample = new WorkletSample(
388
390
  sampleAndGenerators.sample.getAudioData(),
389
391
  (sampleAndGenerators.sample.sampleRate / sampleRate) * Math.pow(2, sampleAndGenerators.sample.samplePitchCorrection / 1200), // cent tuning
390
- generators[generatorTypes.startAddrsOffset] + (generators[generatorTypes.startAddrsCoarseOffset] * 32768),
392
+ 0,
391
393
  rootKey,
392
394
  loopStart,
393
395
  loopEnd,
394
- Math.floor( sampleAndGenerators.sample.sampleData.length) - 1 + (generators[generatorTypes.endAddrOffset] + (generators[generatorTypes.endAddrsCoarseOffset] * 32768)),
396
+ Math.floor( sampleAndGenerators.sample.sampleData.length) - 1,
395
397
  loopingMode
396
398
  )
397
399
  // velocity override
@@ -424,7 +426,7 @@ export function getWorkletVoices(channel,
424
426
  currentTime,
425
427
  targetKey,
426
428
  generators,
427
- sampleAndGenerators.modulators
429
+ sampleAndGenerators.modulators.map(m => Modulator.copy(m))
428
430
  )
429
431
  );
430
432
  return voices;