spessasynth_lib 3.20.11 → 3.20.15
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/midi_parser/midi_editor.js +1 -1
- package/package.json +1 -1
- package/sequencer/worklet_sequencer/song_control.js +1 -5
- package/soundfont/read_sf2/generators.js +1 -1
- package/synthetizer/worklet_processor.min.js +10 -10
- package/synthetizer/worklet_system/main_processor.js +18 -12
- package/synthetizer/worklet_system/worklet_methods/note_on.js +2 -10
- package/synthetizer/worklet_system/worklet_methods/program_control.js +15 -41
- package/synthetizer/worklet_system/worklet_methods/voice_control.js +4 -4
- package/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +156 -112
- package/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +7 -17
- package/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +306 -158
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// noinspection JSUnresolvedReference
|
|
2
|
+
|
|
1
3
|
import { DEFAULT_PERCUSSION, DEFAULT_SYNTH_MODE, VOICE_CAP } from '../synthetizer.js'
|
|
2
4
|
import { WorkletSequencer } from '../../sequencer/worklet_sequencer/worklet_sequencer.js'
|
|
3
5
|
import { SpessaSynthInfo } from '../../utils/loggin.js'
|
|
@@ -32,13 +34,14 @@ import {
|
|
|
32
34
|
clearSoundFont,
|
|
33
35
|
getPreset,
|
|
34
36
|
programChange,
|
|
35
|
-
reloadSoundFont,
|
|
36
|
-
setDrums,
|
|
37
|
+
reloadSoundFont, sendPresetList,
|
|
38
|
+
setDrums, setEmbeddedSoundFont,
|
|
37
39
|
setPreset,
|
|
38
40
|
} from './worklet_methods/program_control.js'
|
|
39
41
|
import { applySynthesizerSnapshot, sendSynthesizerSnapshot } from './worklet_methods/snapshot.js'
|
|
40
42
|
import { WorkletSoundfontManager } from './worklet_methods/worklet_soundfont_manager/worklet_soundfont_manager.js'
|
|
41
43
|
import { interpolationTypes } from './worklet_utilities/wavetable_oscillator.js'
|
|
44
|
+
import { getWorkletVoices } from './worklet_utilities/worklet_voice.js'
|
|
42
45
|
|
|
43
46
|
|
|
44
47
|
/**
|
|
@@ -50,6 +53,7 @@ export const MIN_NOTE_LENGTH = 0.07; // if the note is released faster than that
|
|
|
50
53
|
|
|
51
54
|
export const SYNTHESIZER_GAIN = 1.0;
|
|
52
55
|
|
|
56
|
+
|
|
53
57
|
class SpessaSynthProcessor extends AudioWorkletProcessor
|
|
54
58
|
{
|
|
55
59
|
/**
|
|
@@ -169,10 +173,6 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
|
|
|
169
173
|
this.defaultPreset = this.getPreset(0, 0);
|
|
170
174
|
this.drumPreset = this.getPreset(128, 0);
|
|
171
175
|
|
|
172
|
-
/**
|
|
173
|
-
* @type {Float32Array[]}
|
|
174
|
-
*/
|
|
175
|
-
this.workletDumpedSamplesList = [];
|
|
176
176
|
/**
|
|
177
177
|
* contains all the channels with their voices on the processor size
|
|
178
178
|
* @type {WorkletProcessorChannel[]}
|
|
@@ -186,9 +186,6 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
|
|
|
186
186
|
this.workletProcessorChannels[DEFAULT_PERCUSSION].preset = this.drumPreset;
|
|
187
187
|
this.workletProcessorChannels[DEFAULT_PERCUSSION].drumChannel = true;
|
|
188
188
|
|
|
189
|
-
// in seconds, time between two samples (very, very short)
|
|
190
|
-
this.sampleTime = 1 / sampleRate;
|
|
191
|
-
|
|
192
189
|
// these smoothing factors were tested on 44100Hz, adjust them here
|
|
193
190
|
this.volumeEnvelopeSmoothingFactor = VOLUME_ENVELOPE_SMOOTHING_FACTOR * (sampleRate / 44100);
|
|
194
191
|
this.panSmoothingFactor = PAN_SMOOTHING_FACTOR * (sampleRate / 44100);
|
|
@@ -204,14 +201,21 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
|
|
|
204
201
|
|
|
205
202
|
this.totalVoicesAmount = 0;
|
|
206
203
|
|
|
204
|
+
/**
|
|
205
|
+
* The snapshot that synth was restored from
|
|
206
|
+
* @type {SynthesizerSnapshot|undefined}
|
|
207
|
+
* @private
|
|
208
|
+
*/
|
|
209
|
+
this._snapshot = options.processorOptions?.startRenderingData?.snapshot;
|
|
210
|
+
|
|
207
211
|
this.port.onmessage = e => this.handleMessage(e.data);
|
|
208
212
|
|
|
209
213
|
// if sent, start rendering
|
|
210
214
|
if(options.processorOptions.startRenderingData)
|
|
211
215
|
{
|
|
212
|
-
if (
|
|
216
|
+
if (this._snapshot !== undefined)
|
|
213
217
|
{
|
|
214
|
-
this.applySynthesizerSnapshot(
|
|
218
|
+
this.applySynthesizerSnapshot(this._snapshot);
|
|
215
219
|
this.resetAllControllers();
|
|
216
220
|
}
|
|
217
221
|
|
|
@@ -271,6 +275,7 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
|
|
|
271
275
|
});
|
|
272
276
|
}
|
|
273
277
|
|
|
278
|
+
// noinspection JSUnusedGlobalSymbols
|
|
274
279
|
/**
|
|
275
280
|
* Syntesizes the voice to buffers
|
|
276
281
|
* @param inputs {Float32Array[][]} required by WebAudioAPI
|
|
@@ -355,6 +360,7 @@ class SpessaSynthProcessor extends AudioWorkletProcessor
|
|
|
355
360
|
SpessaSynthProcessor.prototype.renderVoice = renderVoice;
|
|
356
361
|
SpessaSynthProcessor.prototype.releaseVoice = releaseVoice;
|
|
357
362
|
SpessaSynthProcessor.prototype.voiceKilling = voiceKilling;
|
|
363
|
+
SpessaSynthProcessor.prototype.getWorkletVoices = getWorkletVoices;
|
|
358
364
|
|
|
359
365
|
// message port related
|
|
360
366
|
SpessaSynthProcessor.prototype.handleMessage = handleMessage;
|
|
@@ -411,7 +417,7 @@ SpessaSynthProcessor.prototype.setPreset = setPreset;
|
|
|
411
417
|
SpessaSynthProcessor.prototype.setDrums = setDrums;
|
|
412
418
|
SpessaSynthProcessor.prototype.reloadSoundFont = reloadSoundFont;
|
|
413
419
|
SpessaSynthProcessor.prototype.clearSoundFont = clearSoundFont;
|
|
414
|
-
SpessaSynthProcessor.prototype.
|
|
420
|
+
SpessaSynthProcessor.prototype.setEmbeddedSoundFont = setEmbeddedSoundFont;
|
|
415
421
|
SpessaSynthProcessor.prototype.sendPresetList = sendPresetList;
|
|
416
422
|
|
|
417
423
|
// snapshot related
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getWorkletVoices } from '../worklet_utilities/worklet_voice.js'
|
|
2
1
|
import { generatorTypes } from '../../../soundfont/read_sf2/generators.js'
|
|
3
2
|
import { computeModulators } from '../worklet_utilities/worklet_modulator.js'
|
|
4
3
|
import { WorkletVolumeEnvelope } from '../worklet_utilities/volume_envelope.js'
|
|
@@ -45,19 +44,12 @@ export function noteOn(channel, midiNote, velocity, enableDebugging = false, sen
|
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
// get voices
|
|
48
|
-
const voices = getWorkletVoices(
|
|
47
|
+
const voices = this.getWorkletVoices(
|
|
49
48
|
channel,
|
|
50
49
|
sentMidiNote,
|
|
51
50
|
velocity,
|
|
52
|
-
channelObject
|
|
51
|
+
channelObject,
|
|
53
52
|
startTime,
|
|
54
|
-
sampleRate,
|
|
55
|
-
data => this.sampleDump(
|
|
56
|
-
data.channel,
|
|
57
|
-
data.sampleID,
|
|
58
|
-
data.sampleData
|
|
59
|
-
),
|
|
60
|
-
channelObject.cachedVoices,
|
|
61
53
|
enableDebugging
|
|
62
54
|
);
|
|
63
55
|
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { midiControllers } from '../../../midi_parser/midi_message.js'
|
|
2
|
-
import { clearSamplesList } from '../worklet_utilities/worklet_voice.js'
|
|
3
|
-
import { generatorTypes } from '../../../soundfont/read_sf2/generators.js'
|
|
4
2
|
import { returnMessageType } from '../message_protocol/worklet_message.js'
|
|
5
3
|
import { SpessaSynthInfo, SpessaSynthWarn } from '../../../utils/loggin.js'
|
|
6
4
|
import { consoleColors } from '../../../utils/other.js'
|
|
@@ -183,13 +181,10 @@ export function sendPresetList()
|
|
|
183
181
|
export function clearSoundFont(sendPresets = true, clearOverride = true)
|
|
184
182
|
{
|
|
185
183
|
this.stopAllChannels(true);
|
|
186
|
-
clearSamplesList();
|
|
187
184
|
if(clearOverride)
|
|
188
185
|
{
|
|
189
186
|
delete this.overrideSoundfont;
|
|
190
187
|
}
|
|
191
|
-
delete this.workletDumpedSamplesList;
|
|
192
|
-
this.workletDumpedSamplesList = [];
|
|
193
188
|
this.defaultPreset = this.getPreset(0, 0);
|
|
194
189
|
this.drumPreset = this.getPreset(128, 0);
|
|
195
190
|
|
|
@@ -253,44 +248,23 @@ export function reloadSoundFont(buffer, isOverride = false)
|
|
|
253
248
|
}
|
|
254
249
|
|
|
255
250
|
/**
|
|
256
|
-
*
|
|
257
|
-
* @param
|
|
258
|
-
* @param
|
|
259
|
-
* @param sampleData {Float32Array}
|
|
251
|
+
* Sets the embedded (RMI soundfont)
|
|
252
|
+
* @param font {ArrayBuffer}
|
|
253
|
+
* @param offset {number}
|
|
260
254
|
* @this {SpessaSynthProcessor}
|
|
261
255
|
*/
|
|
262
|
-
export function
|
|
256
|
+
export function setEmbeddedSoundFont(font, offset)
|
|
263
257
|
{
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
//
|
|
268
|
-
this.
|
|
269
|
-
if(v.sample.sampleID !== sampleID)
|
|
270
|
-
{
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
v.sample.end = sampleData.length - 1 + v.generators[generatorTypes.endAddrOffset] + (v.generators[generatorTypes.endAddrsCoarseOffset] * 32768);
|
|
274
|
-
// calculate for how long the sample has been playing and move the cursor there
|
|
275
|
-
v.sample.cursor = (v.sample.playbackStep * sampleRate) * (currentTime - v.startTime);
|
|
276
|
-
if(v.sample.loopingMode === 0) // no loop
|
|
277
|
-
{
|
|
278
|
-
if (v.sample.cursor >= v.sample.end)
|
|
279
|
-
{
|
|
280
|
-
v.finished = true;
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
else
|
|
285
|
-
{
|
|
286
|
-
// go through modulo (adjust cursor if the sample has looped
|
|
287
|
-
if(v.sample.cursor > v.sample.loopEnd)
|
|
288
|
-
{
|
|
289
|
-
v.sample.cursor = v.sample.cursor % (v.sample.loopEnd - v.sample.loopStart) + v.sample.loopStart - 1;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
// set start time to current!
|
|
293
|
-
v.startTime = currentTime;
|
|
294
|
-
})
|
|
258
|
+
// set offet
|
|
259
|
+
this.soundfontBankOffset = offset;
|
|
260
|
+
this.reloadSoundFont(font, true);
|
|
261
|
+
// preload all samples
|
|
262
|
+
this.overrideSoundfont.samples.forEach(s => s.getAudioData());
|
|
295
263
|
|
|
264
|
+
// apply snapshot again if applicable
|
|
265
|
+
if(this._snapshot !== undefined)
|
|
266
|
+
{
|
|
267
|
+
this.applySynthesizerSnapshot(this._snapshot);
|
|
268
|
+
this.resetAllControllers();
|
|
269
|
+
}
|
|
296
270
|
}
|
|
@@ -5,7 +5,7 @@ import { customControllers } from '../worklet_utilities/worklet_processor_channe
|
|
|
5
5
|
import { WorkletModulationEnvelope } from '../worklet_utilities/modulation_envelope.js'
|
|
6
6
|
import { getSampleLinear, getSampleNearest, interpolationTypes } from '../worklet_utilities/wavetable_oscillator.js'
|
|
7
7
|
import { panVoice } from '../worklet_utilities/stereo_panner.js'
|
|
8
|
-
import {
|
|
8
|
+
import { WorkletLowpassFilter } from '../worklet_utilities/lowpass_filter.js'
|
|
9
9
|
import { MIN_NOTE_LENGTH } from '../main_processor.js'
|
|
10
10
|
import { WorkletVolumeEnvelope } from '../worklet_utilities/volume_envelope.js'
|
|
11
11
|
|
|
@@ -150,15 +150,15 @@ export function renderVoice(
|
|
|
150
150
|
// wavetable oscillator
|
|
151
151
|
if(this.interpolationType === interpolationTypes.linear)
|
|
152
152
|
{
|
|
153
|
-
getSampleLinear(voice,
|
|
153
|
+
getSampleLinear(voice, bufferOut);
|
|
154
154
|
}
|
|
155
155
|
else
|
|
156
156
|
{
|
|
157
|
-
getSampleNearest(voice,
|
|
157
|
+
getSampleNearest(voice, bufferOut);
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
// lowpass filter
|
|
161
|
-
|
|
161
|
+
WorkletLowpassFilter.apply(voice, bufferOut, lowpassCents);
|
|
162
162
|
|
|
163
163
|
// volenv
|
|
164
164
|
WorkletVolumeEnvelope.apply(voice, bufferOut, modLfoCentibels, this.volumeEnvelopeSmoothingFactor);
|
|
@@ -9,122 +9,166 @@ import { absCentsToHz, decibelAttenuationToGain } from './unit_converter.js'
|
|
|
9
9
|
* Shoutout to them!
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @typedef {Object} WorkletLowpassFilter
|
|
15
|
-
* @property {number} a0 - filter coefficient 1
|
|
16
|
-
* @property {number} a1 - filter coefficient 2
|
|
17
|
-
* @property {number} a2 - filter coefficient 3
|
|
18
|
-
* @property {number} a3 - filter coefficient 4
|
|
19
|
-
* @property {number} a4 - filter coefficient 5
|
|
20
|
-
* @property {number} x1 - input history 1
|
|
21
|
-
* @property {number} x2 - input history 2
|
|
22
|
-
* @property {number} y1 - output history 1
|
|
23
|
-
* @property {number} y2 - output history 2
|
|
24
|
-
* @property {number} reasonanceCb - reasonance in centibels
|
|
25
|
-
* @property {number} reasonanceGain - resonance gain
|
|
26
|
-
* @property {number} cutoffCents - cutoff frequency in cents
|
|
27
|
-
* @property {number} cutoffHz - cutoff frequency in Hz
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* @type {WorkletLowpassFilter}
|
|
32
|
-
*/
|
|
33
|
-
export const DEFAULT_WORKLET_LOWPASS_FILTER = {
|
|
34
|
-
a0: 0,
|
|
35
|
-
a1: 0,
|
|
36
|
-
a2: 0,
|
|
37
|
-
a3: 0,
|
|
38
|
-
a4: 0,
|
|
39
|
-
|
|
40
|
-
x1: 0,
|
|
41
|
-
x2: 0,
|
|
42
|
-
y1: 0,
|
|
43
|
-
y2: 0,
|
|
44
|
-
|
|
45
|
-
reasonanceCb: 0,
|
|
46
|
-
reasonanceGain: 1,
|
|
47
|
-
cutoffCents: 13500,
|
|
48
|
-
cutoffHz: 20000
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Applies a low-pass filter to the given buffer
|
|
53
|
-
* @param voice {WorkletVoice} the voice we're working on
|
|
54
|
-
* @param outputBuffer {Float32Array} the buffer to apply the filter to
|
|
55
|
-
* @param cutoffCents {number} cutoff frequency in cents
|
|
56
|
-
*/
|
|
57
|
-
export function applyLowpassFilter(voice, outputBuffer, cutoffCents)
|
|
12
|
+
export class WorkletLowpassFilter
|
|
58
13
|
{
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Filter coefficient 1
|
|
16
|
+
* @type {number}
|
|
17
|
+
*/
|
|
18
|
+
a0 = 0;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Filter coefficient 2
|
|
22
|
+
* @type {number}
|
|
23
|
+
*/
|
|
24
|
+
a1 = 0;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Filter coefficient 3
|
|
28
|
+
* @type {number}
|
|
29
|
+
*/
|
|
30
|
+
a2 = 0;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Filter coefficient 4
|
|
34
|
+
* @type {number}
|
|
35
|
+
*/
|
|
36
|
+
a3 = 0;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Filter coefficient 5
|
|
40
|
+
* @type {number}
|
|
41
|
+
*/
|
|
42
|
+
a4 = 0;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Input history 1
|
|
46
|
+
* @type {number}
|
|
47
|
+
*/
|
|
48
|
+
x1 = 0;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Input history 2
|
|
52
|
+
* @type {number}
|
|
53
|
+
*/
|
|
54
|
+
x2 = 0;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Output history 1
|
|
58
|
+
* @type {number}
|
|
59
|
+
*/
|
|
60
|
+
y1 = 0;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Output history 2
|
|
64
|
+
* @type {number}
|
|
65
|
+
*/
|
|
66
|
+
y2 = 0;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resonance in centibels
|
|
70
|
+
* @type {number}
|
|
71
|
+
*/
|
|
72
|
+
reasonanceCb = 0;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resonance gain
|
|
76
|
+
* @type {number}
|
|
77
|
+
*/
|
|
78
|
+
reasonanceGain = 1;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Cutoff frequency in cents
|
|
82
|
+
* @type {number}
|
|
83
|
+
*/
|
|
84
|
+
cutoffCents = 13500;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Cutoff frequency in Hz
|
|
88
|
+
* @type {number}
|
|
89
|
+
*/
|
|
90
|
+
cutoffHz = 20000;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Applies a low-pass filter to the given buffer
|
|
94
|
+
* @param voice {WorkletVoice} the voice we're working on
|
|
95
|
+
* @param outputBuffer {Float32Array} the buffer to apply the filter to
|
|
96
|
+
* @param cutoffCents {number} cutoff frequency in cents
|
|
97
|
+
*/
|
|
98
|
+
static apply(voice, outputBuffer, cutoffCents)
|
|
66
99
|
{
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
100
|
+
if(cutoffCents > 13499)
|
|
101
|
+
{
|
|
102
|
+
return; // filter is open
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// check if the frequency has changed. if so, calculate new coefficients
|
|
106
|
+
if(voice.filter.cutoffCents !== cutoffCents || voice.filter.reasonanceCb !== voice.modulatedGenerators[generatorTypes.initialFilterQ])
|
|
107
|
+
{
|
|
108
|
+
voice.filter.cutoffCents = cutoffCents;
|
|
109
|
+
voice.filter.reasonanceCb = voice.modulatedGenerators[generatorTypes.initialFilterQ];
|
|
110
|
+
WorkletLowpassFilter.calculateCoefficients(voice);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// filter the input
|
|
114
|
+
for (let i = 0; i < outputBuffer.length; i++)
|
|
115
|
+
{
|
|
116
|
+
let input = outputBuffer[i];
|
|
117
|
+
let filtered = voice.filter.a0 * input
|
|
118
|
+
+ voice.filter.a1 * voice.filter.x1
|
|
119
|
+
+ voice.filter.a2 * voice.filter.x2
|
|
120
|
+
- voice.filter.a3 * voice.filter.y1
|
|
121
|
+
- voice.filter.a4 * voice.filter.y2;
|
|
122
|
+
|
|
123
|
+
// set buffer
|
|
124
|
+
voice.filter.x2 = voice.filter.x1;
|
|
125
|
+
voice.filter.x1 = input;
|
|
126
|
+
voice.filter.y2 = voice.filter.y1;
|
|
127
|
+
voice.filter.y1 = filtered;
|
|
128
|
+
|
|
129
|
+
outputBuffer[i] = filtered;
|
|
130
|
+
}
|
|
70
131
|
}
|
|
71
132
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
+ voice.filter.a1 * voice.filter.x1
|
|
77
|
-
+ voice.filter.a2 * voice.filter.x2
|
|
78
|
-
- voice.filter.a3 * voice.filter.y1
|
|
79
|
-
- voice.filter.a4 * voice.filter.y2;
|
|
80
|
-
|
|
81
|
-
// set buffer
|
|
82
|
-
voice.filter.x2 = voice.filter.x1;
|
|
83
|
-
voice.filter.x1 = input;
|
|
84
|
-
voice.filter.y2 = voice.filter.y1;
|
|
85
|
-
voice.filter.y1 = filtered;
|
|
86
|
-
|
|
87
|
-
outputBuffer[i] = filtered;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* @param voice {WorkletVoice}
|
|
93
|
-
*/
|
|
94
|
-
function calculateCoefficients(voice)
|
|
95
|
-
{
|
|
96
|
-
voice.filter.cutoffHz = absCentsToHz(voice.filter.cutoffCents);
|
|
97
|
-
|
|
98
|
-
// fix cutoff on low frequencies (fluid_iir_filter.c line 392)
|
|
99
|
-
if(voice.filter.cutoffHz > 0.45 * sampleRate)
|
|
133
|
+
/**
|
|
134
|
+
* @param voice {WorkletVoice}
|
|
135
|
+
*/
|
|
136
|
+
static calculateCoefficients(voice)
|
|
100
137
|
{
|
|
101
|
-
voice.filter.cutoffHz =
|
|
138
|
+
voice.filter.cutoffHz = absCentsToHz(voice.filter.cutoffCents);
|
|
139
|
+
|
|
140
|
+
// fix cutoff on low frequencies (fluid_iir_filter.c line 392)
|
|
141
|
+
if(voice.filter.cutoffHz > 0.45 * sampleRate)
|
|
142
|
+
{
|
|
143
|
+
voice.filter.cutoffHz = 0.45 * sampleRate;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// adjust the filterQ (fluid_iir_filter.c line 204)
|
|
147
|
+
const qDb = (voice.filter.reasonanceCb / 10) - 3.01;
|
|
148
|
+
voice.filter.reasonanceGain = decibelAttenuationToGain(-1 * qDb); // -1 because it's attenuation and we don't want attenuation
|
|
149
|
+
|
|
150
|
+
// reduce the gain by the Q factor (fluid_iir_filter.c line 250)
|
|
151
|
+
const qGain = 1 / Math.sqrt(voice.filter.reasonanceGain);
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
// code is ported from https://github.com/sinshu/meltysynth/ to work with js.
|
|
155
|
+
// I'm too dumb to understand the math behind this...
|
|
156
|
+
let w = 2 * Math.PI * voice.filter.cutoffHz / sampleRate; // we're in the audioworkletglobalscope so we can use sampleRate
|
|
157
|
+
let cosw = Math.cos(w);
|
|
158
|
+
let alpha = Math.sin(w) / (2 * voice.filter.reasonanceGain);
|
|
159
|
+
|
|
160
|
+
let b1 = (1 - cosw) * qGain;
|
|
161
|
+
let b0 = b1 / 2;
|
|
162
|
+
let b2 = b0;
|
|
163
|
+
let a0 = 1 + alpha;
|
|
164
|
+
let a1 = -2 * cosw;
|
|
165
|
+
let a2 = 1 - alpha;
|
|
166
|
+
|
|
167
|
+
// set coefficients
|
|
168
|
+
voice.filter.a0 = b0 / a0;
|
|
169
|
+
voice.filter.a1 = b1 / a0;
|
|
170
|
+
voice.filter.a2 = b2 / a0;
|
|
171
|
+
voice.filter.a3 = a1 / a0;
|
|
172
|
+
voice.filter.a4 = a2 / a0;
|
|
102
173
|
}
|
|
103
|
-
|
|
104
|
-
// adjust the filterQ (fluid_iir_filter.c line 204)
|
|
105
|
-
const qDb = (voice.filter.reasonanceCb / 10) - 3.01;
|
|
106
|
-
voice.filter.reasonanceGain = decibelAttenuationToGain(-1 * qDb); // -1 because it's attenuation and we don't want attenuation
|
|
107
|
-
|
|
108
|
-
// reduce the gain by the Q factor (fluid_iir_filter.c line 250)
|
|
109
|
-
const qGain = 1 / Math.sqrt(voice.filter.reasonanceGain);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// code is ported from https://github.com/sinshu/meltysynth/ to work with js. I'm too dumb to understand the math behind this...
|
|
113
|
-
let w = 2 * Math.PI * voice.filter.cutoffHz / sampleRate; // we're in the audioworkletglobalscope so we can use sampleRate
|
|
114
|
-
let cosw = Math.cos(w);
|
|
115
|
-
let alpha = Math.sin(w) / (2 * voice.filter.reasonanceGain);
|
|
116
|
-
|
|
117
|
-
let b1 = (1 - cosw) * qGain;
|
|
118
|
-
let b0 = b1 / 2;
|
|
119
|
-
let b2 = b0;
|
|
120
|
-
let a0 = 1 + alpha;
|
|
121
|
-
let a1 = -2 * cosw;
|
|
122
|
-
let a2 = 1 - alpha;
|
|
123
|
-
|
|
124
|
-
// set coefficients
|
|
125
|
-
voice.filter.a0 = b0 / a0;
|
|
126
|
-
voice.filter.a1 = b1 / a0;
|
|
127
|
-
voice.filter.a2 = b2 / a0;
|
|
128
|
-
voice.filter.a3 = a1 / a0;
|
|
129
|
-
voice.filter.a4 = a2 / a0;
|
|
130
174
|
}
|
|
@@ -16,17 +16,17 @@ export const interpolationTypes = {
|
|
|
16
16
|
/**
|
|
17
17
|
* Fills the output buffer with raw sample data using linear interpolation
|
|
18
18
|
* @param voice {WorkletVoice} the voice we're working on
|
|
19
|
-
* @param sampleData {Float32Array} the sample data to write with
|
|
20
19
|
* @param outputBuffer {Float32Array} the output buffer to write to
|
|
21
20
|
*/
|
|
22
|
-
export function getSampleLinear(voice,
|
|
21
|
+
export function getSampleLinear(voice, outputBuffer)
|
|
23
22
|
{
|
|
24
23
|
let cur = voice.sample.cursor;
|
|
25
24
|
const loop = (voice.sample.loopingMode === 1) || (voice.sample.loopingMode === 3 && !voice.isInRelease);
|
|
26
|
-
const
|
|
25
|
+
const sampleData = voice.sample.sampleData;
|
|
27
26
|
|
|
28
27
|
if(loop)
|
|
29
28
|
{
|
|
29
|
+
const loopLength = voice.sample.loopEnd - voice.sample.loopStart;
|
|
30
30
|
for (let i = 0; i < outputBuffer.length; i++)
|
|
31
31
|
{
|
|
32
32
|
// check for loop
|
|
@@ -44,11 +44,6 @@ export function getSampleLinear(voice, sampleData, outputBuffer)
|
|
|
44
44
|
ceil -= loopLength;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
// nearest neighbor (uncomment to use)
|
|
48
|
-
// outputBuffer[i] = sampleData[ceil];
|
|
49
|
-
// cur += voice.sample.playbackStep * voice.currentTuningCalculated;
|
|
50
|
-
// continue;
|
|
51
|
-
|
|
52
47
|
const fraction = cur - floor;
|
|
53
48
|
|
|
54
49
|
// grab the samples and interpolate
|
|
@@ -73,7 +68,8 @@ export function getSampleLinear(voice, sampleData, outputBuffer)
|
|
|
73
68
|
{
|
|
74
69
|
voice.sample.end = sampleData.length - 1;
|
|
75
70
|
}
|
|
76
|
-
for (let i = 0; i < outputBuffer.length; i++)
|
|
71
|
+
for (let i = 0; i < outputBuffer.length; i++)
|
|
72
|
+
{
|
|
77
73
|
|
|
78
74
|
// linear interpolation
|
|
79
75
|
const floor = ~~cur;
|
|
@@ -86,11 +82,6 @@ export function getSampleLinear(voice, sampleData, outputBuffer)
|
|
|
86
82
|
return;
|
|
87
83
|
}
|
|
88
84
|
|
|
89
|
-
// nearest neighbor (uncomment to use)
|
|
90
|
-
// outputBuffer[i] = sampleData[ceil];
|
|
91
|
-
// cur += voice.sample.playbackStep * voice.currentTuningCalculated;
|
|
92
|
-
// continue;
|
|
93
|
-
|
|
94
85
|
const fraction = cur - floor;
|
|
95
86
|
|
|
96
87
|
// grab the samples and interpolate
|
|
@@ -107,15 +98,14 @@ export function getSampleLinear(voice, sampleData, outputBuffer)
|
|
|
107
98
|
/**
|
|
108
99
|
* Fills the output buffer with raw sample data using no interpolation (nearest neighbor)
|
|
109
100
|
* @param voice {WorkletVoice} the voice we're working on
|
|
110
|
-
* @param sampleData {Float32Array} the sample data to write with
|
|
111
101
|
* @param outputBuffer {Float32Array} the output buffer to write to
|
|
112
102
|
*/
|
|
113
|
-
export function getSampleNearest(voice,
|
|
103
|
+
export function getSampleNearest(voice, outputBuffer)
|
|
114
104
|
{
|
|
115
105
|
let cur = voice.sample.cursor;
|
|
116
106
|
const loop = (voice.sample.loopingMode === 1) || (voice.sample.loopingMode === 3 && !voice.isInRelease);
|
|
117
107
|
const loopLength = voice.sample.loopEnd - voice.sample.loopStart;
|
|
118
|
-
|
|
108
|
+
const sampleData = voice.sample.sampleData;
|
|
119
109
|
if(loop)
|
|
120
110
|
{
|
|
121
111
|
for (let i = 0; i < outputBuffer.length; i++)
|