spessasynth_core 1.0.0
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/.idea/inspectionProfiles/Project_Default.xml +10 -0
- package/.idea/jsLibraryMappings.xml +6 -0
- package/.idea/modules.xml +8 -0
- package/.idea/spessasynth_core.iml +12 -0
- package/.idea/vcs.xml +6 -0
- package/README.md +376 -0
- package/index.js +7 -0
- package/package.json +34 -0
- package/spessasynth_core/midi_parser/README.md +3 -0
- package/spessasynth_core/midi_parser/midi_loader.js +381 -0
- package/spessasynth_core/midi_parser/midi_message.js +231 -0
- package/spessasynth_core/sequencer/sequencer.js +192 -0
- package/spessasynth_core/sequencer/worklet_sequencer/play.js +221 -0
- package/spessasynth_core/sequencer/worklet_sequencer/process_event.js +138 -0
- package/spessasynth_core/sequencer/worklet_sequencer/process_tick.js +85 -0
- package/spessasynth_core/sequencer/worklet_sequencer/song_control.js +90 -0
- package/spessasynth_core/soundfont/README.md +4 -0
- package/spessasynth_core/soundfont/chunk/generators.js +205 -0
- package/spessasynth_core/soundfont/chunk/instruments.js +60 -0
- package/spessasynth_core/soundfont/chunk/modulators.js +232 -0
- package/spessasynth_core/soundfont/chunk/presets.js +264 -0
- package/spessasynth_core/soundfont/chunk/riff_chunk.js +46 -0
- package/spessasynth_core/soundfont/chunk/samples.js +250 -0
- package/spessasynth_core/soundfont/chunk/zones.js +264 -0
- package/spessasynth_core/soundfont/soundfont_parser.js +301 -0
- package/spessasynth_core/synthetizer/README.md +6 -0
- package/spessasynth_core/synthetizer/synthesizer.js +303 -0
- package/spessasynth_core/synthetizer/worklet_system/README.md +3 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/controller_control.js +285 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/data_entry.js +280 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_off.js +102 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_on.js +75 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/program_control.js +140 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/system_exclusive.js +265 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/tuning_control.js +105 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/vibrato_control.js +29 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/voice_control.js +186 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/lfo.js +23 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +95 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js +73 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/modulator_curves.js +86 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/stereo_panner.js +76 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/unit_converter.js +66 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +194 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +83 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +173 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +105 -0
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +313 -0
- package/spessasynth_core/utils/README.md +4 -0
- package/spessasynth_core/utils/buffer_to_wav.js +70 -0
- package/spessasynth_core/utils/byte_functions.js +141 -0
- package/spessasynth_core/utils/loggin.js +79 -0
- package/spessasynth_core/utils/other.js +49 -0
- package/spessasynth_core/utils/shiftable_array.js +26 -0
- package/spessasynth_core/utils/stbvorbis_sync.js +1877 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { generatorTypes } from '../../../soundfont/chunk/generators.js'
|
|
2
|
+
import { absCentsToHz, decibelAttenuationToGain } from './unit_converter.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* lowpass_filter.js
|
|
6
|
+
* purpose: applies a low pass filter to a voice
|
|
7
|
+
* note to self: most of this is code is just javascript version of the C code from fluidsynth,
|
|
8
|
+
* they are the real smart guys.
|
|
9
|
+
* Shoutout to them!
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Applies a low-pass filter to the given buffer
|
|
15
|
+
* @param voice {WorkletVoice} the voice we're working on
|
|
16
|
+
* @param outputBuffer {Float32Array} the buffer to apply the filter to
|
|
17
|
+
* @param cutoffCents {number} cutoff frequency in cents
|
|
18
|
+
* @param sampleRate {number} in hertz
|
|
19
|
+
* @this {Synthesizer}
|
|
20
|
+
*/
|
|
21
|
+
export function applyLowpassFilter(voice, outputBuffer, cutoffCents, sampleRate)
|
|
22
|
+
{
|
|
23
|
+
if(cutoffCents > 13499)
|
|
24
|
+
{
|
|
25
|
+
return; // filter is open
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// check if the frequency has changed. if so, calculate new coefficients
|
|
29
|
+
if(voice.filter.cutoffCents !== cutoffCents || voice.filter.reasonanceCb !== voice.modulatedGenerators[generatorTypes.initialFilterQ])
|
|
30
|
+
{
|
|
31
|
+
voice.filter.cutoffCents = cutoffCents;
|
|
32
|
+
voice.filter.reasonanceCb = voice.modulatedGenerators[generatorTypes.initialFilterQ];
|
|
33
|
+
calculateCoefficients(voice, sampleRate);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// filter the input
|
|
37
|
+
for (let i = 0; i < outputBuffer.length; i++) {
|
|
38
|
+
let input = outputBuffer[i];
|
|
39
|
+
let filtered = voice.filter.a0 * input
|
|
40
|
+
+ voice.filter.a1 * voice.filter.x1
|
|
41
|
+
+ voice.filter.a2 * voice.filter.x2
|
|
42
|
+
- voice.filter.a3 * voice.filter.y1
|
|
43
|
+
- voice.filter.a4 * voice.filter.y2;
|
|
44
|
+
|
|
45
|
+
// set buffer
|
|
46
|
+
voice.filter.x2 = voice.filter.x1;
|
|
47
|
+
voice.filter.x1 = input;
|
|
48
|
+
voice.filter.y2 = voice.filter.y1;
|
|
49
|
+
voice.filter.y1 = filtered;
|
|
50
|
+
|
|
51
|
+
outputBuffer[i] = filtered;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param voice {WorkletVoice}
|
|
57
|
+
* @param sampleRate {number}
|
|
58
|
+
*/
|
|
59
|
+
function calculateCoefficients(voice, sampleRate)
|
|
60
|
+
{
|
|
61
|
+
voice.filter.cutoffHz = absCentsToHz(voice.filter.cutoffCents);
|
|
62
|
+
|
|
63
|
+
// fix cutoff on low frequencies (fluid_iir_filter.c line 392)
|
|
64
|
+
if(voice.filter.cutoffHz > 0.45 * sampleRate)
|
|
65
|
+
{
|
|
66
|
+
voice.filter.cutoffHz = 0.45 * sampleRate;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// adjust the filterQ (fluid_iir_filter.c line 204)
|
|
70
|
+
const qDb = (voice.filter.reasonanceCb / 10) - 3.01;
|
|
71
|
+
voice.filter.reasonanceGain = decibelAttenuationToGain(-1 * qDb); // -1 because it's attenuation, and we don't want attenuation
|
|
72
|
+
|
|
73
|
+
// reduce the gain by the Q factor (fluid_iir_filter.c line 250)
|
|
74
|
+
const qGain = 1 / Math.sqrt(voice.filter.reasonanceGain);
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
// code is ported from https://github.com/sinshu/meltysynth/ to work with js. I'm too dumb to understand the math behind this...
|
|
78
|
+
let w = 2 * Math.PI * voice.filter.cutoffHz / sampleRate;
|
|
79
|
+
let cosw = Math.cos(w);
|
|
80
|
+
let alpha = Math.sin(w) / (2 * voice.filter.reasonanceGain);
|
|
81
|
+
|
|
82
|
+
let b1 = (1 - cosw) * qGain;
|
|
83
|
+
let b0 = b1 / 2;
|
|
84
|
+
let b2 = b0;
|
|
85
|
+
let a0 = 1 + alpha;
|
|
86
|
+
let a1 = -2 * cosw;
|
|
87
|
+
let a2 = 1 - alpha;
|
|
88
|
+
|
|
89
|
+
// set coefficients
|
|
90
|
+
voice.filter.a0 = b0 / a0;
|
|
91
|
+
voice.filter.a1 = b1 / a0;
|
|
92
|
+
voice.filter.a2 = b2 / a0;
|
|
93
|
+
voice.filter.a3 = a1 / a0;
|
|
94
|
+
voice.filter.a4 = a2 / a0;
|
|
95
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { timecentsToSeconds } from './unit_converter.js'
|
|
2
|
+
import { generatorTypes } from '../../../soundfont/chunk/generators.js'
|
|
3
|
+
import { getModulatorCurveValue } from './modulator_curves.js'
|
|
4
|
+
import { modulatorCurveTypes } from '../../../soundfont/chunk/modulators.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* modulation_envelope.js
|
|
8
|
+
* purpose: calculates the modulation envelope for the given voice
|
|
9
|
+
*/
|
|
10
|
+
const PEAK = 1;
|
|
11
|
+
|
|
12
|
+
// 1000 should be precise enough
|
|
13
|
+
const CONVEX_ATTACK = new Float32Array(1000);
|
|
14
|
+
for (let i = 0; i < CONVEX_ATTACK.length; i++) {
|
|
15
|
+
// this makes the db linear ( i think
|
|
16
|
+
CONVEX_ATTACK[i] = getModulatorCurveValue(0, modulatorCurveTypes.convex, i / 1000, 0);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Calculates the current modulation envelope value for the given time and voice
|
|
21
|
+
* @param voice {WorkletVoice} the voice we're working on
|
|
22
|
+
* @param currentTime {number} in seconds
|
|
23
|
+
* @returns {number} modenv value, from 0 to 1
|
|
24
|
+
*/
|
|
25
|
+
export function getModEnvValue(voice, currentTime)
|
|
26
|
+
{
|
|
27
|
+
// calculate env times
|
|
28
|
+
let attack = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.attackModEnv]);
|
|
29
|
+
let decay = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.decayModEnv] + ((60 - voice.midiNote) * voice.modulatedGenerators[generatorTypes.keyNumToModEnvDecay]));
|
|
30
|
+
let hold = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.holdModEnv] + ((60 - voice.midiNote) * voice.modulatedGenerators[generatorTypes.keyNumToModEnvHold]));
|
|
31
|
+
|
|
32
|
+
// calculate absolute times
|
|
33
|
+
if(voice.isInRelease && voice.releaseStartTime < currentTime)
|
|
34
|
+
{
|
|
35
|
+
let release = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.releaseModEnv]);
|
|
36
|
+
if(voice.modulatedGenerators[generatorTypes.releaseModEnv] < -7199)
|
|
37
|
+
{
|
|
38
|
+
// prevent lowpass bugs if release is instant
|
|
39
|
+
return voice.releaseStartModEnv;
|
|
40
|
+
}
|
|
41
|
+
return (1 - (currentTime - voice.releaseStartTime) / release) * voice.releaseStartModEnv;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let sustain = 1 - (voice.modulatedGenerators[generatorTypes.sustainModEnv] / 1000);
|
|
45
|
+
let delayEnd = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayModEnv]) + voice.startTime;
|
|
46
|
+
let attackEnd = attack + delayEnd;
|
|
47
|
+
let holdEnd = hold + attackEnd;
|
|
48
|
+
let decayEnd = decay + holdEnd;
|
|
49
|
+
|
|
50
|
+
let modEnvVal
|
|
51
|
+
if(currentTime < delayEnd)
|
|
52
|
+
{
|
|
53
|
+
modEnvVal = 0; // delay
|
|
54
|
+
}
|
|
55
|
+
else if(currentTime < attackEnd)
|
|
56
|
+
{
|
|
57
|
+
modEnvVal = CONVEX_ATTACK[~~((1 - (attackEnd - currentTime) / attack) * 1000)]; // convex attack
|
|
58
|
+
}
|
|
59
|
+
else if(currentTime < holdEnd)
|
|
60
|
+
{
|
|
61
|
+
modEnvVal = PEAK; // peak
|
|
62
|
+
}
|
|
63
|
+
else if(currentTime < decayEnd)
|
|
64
|
+
{
|
|
65
|
+
modEnvVal = (1 - (decayEnd - currentTime) / decay) * (sustain - PEAK) + PEAK; // decay
|
|
66
|
+
}
|
|
67
|
+
else
|
|
68
|
+
{
|
|
69
|
+
modEnvVal = sustain; // sustain
|
|
70
|
+
}
|
|
71
|
+
voice.currentModEnvValue = modEnvVal;
|
|
72
|
+
return modEnvVal;
|
|
73
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { modulatorCurveTypes } from '../../../soundfont/chunk/modulators.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* modulator_curves.js
|
|
5
|
+
* precomputes modulator concave and conves curves and calculates a curve value for a given polarity, direction and type
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// the length of the precomputed curve tables
|
|
9
|
+
export const MOD_PRECOMPUTED_LENGTH = 16384;
|
|
10
|
+
|
|
11
|
+
// Precalculate lookup tables for concave and convers
|
|
12
|
+
const concave = new Float32Array(MOD_PRECOMPUTED_LENGTH);
|
|
13
|
+
const convex = new Float32Array(MOD_PRECOMPUTED_LENGTH);
|
|
14
|
+
// the equation is taken from FluidSynth as it's the standard for soundFonts
|
|
15
|
+
// more precisely, this:
|
|
16
|
+
// https://github.com/FluidSynth/fluidsynth/blob/cb8da1e1e2c0a5cff2bab6a419755b598b793384/src/gentables/gen_conv.c#L55
|
|
17
|
+
concave[0] = 0;
|
|
18
|
+
concave[MOD_PRECOMPUTED_LENGTH - 1] = 1;
|
|
19
|
+
|
|
20
|
+
convex[0] = 0;
|
|
21
|
+
convex[MOD_PRECOMPUTED_LENGTH - 1] = 1;
|
|
22
|
+
for(let i = 1; i < MOD_PRECOMPUTED_LENGTH - 1; i++)
|
|
23
|
+
{
|
|
24
|
+
let x = (-200 * 2 / 960) * Math.log(i / (MOD_PRECOMPUTED_LENGTH - 1)) / Math.LN10;
|
|
25
|
+
convex[i] = 1 - x;
|
|
26
|
+
concave[MOD_PRECOMPUTED_LENGTH - 1 - i] = x;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Transforms a value with a given curve type
|
|
31
|
+
* @param polarity {number} 0 or 1
|
|
32
|
+
* @param direction {number} 0 or 1
|
|
33
|
+
* @param curveType {number} see modulatorCurveTypes in modulators.js
|
|
34
|
+
* @param value {number} the linear value, 0 to 1
|
|
35
|
+
* @returns {number} the transformed value, 0 to 1 or -1 to 1
|
|
36
|
+
*/
|
|
37
|
+
export function getModulatorCurveValue(direction, curveType, value, polarity) {
|
|
38
|
+
// inverse the value if needed
|
|
39
|
+
if(direction)
|
|
40
|
+
{
|
|
41
|
+
value = 1 - value
|
|
42
|
+
}
|
|
43
|
+
switch (curveType) {
|
|
44
|
+
case modulatorCurveTypes.linear:
|
|
45
|
+
if (polarity) {
|
|
46
|
+
// bipolar
|
|
47
|
+
return value * 2 - 1;
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
|
|
51
|
+
case modulatorCurveTypes.switch:
|
|
52
|
+
// switch
|
|
53
|
+
value = value > 0.5 ? 1 : 0;
|
|
54
|
+
if (polarity) {
|
|
55
|
+
// multiply
|
|
56
|
+
return value * 2 - 1;
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
|
|
60
|
+
case modulatorCurveTypes.concave:
|
|
61
|
+
// look up the value
|
|
62
|
+
if(polarity)
|
|
63
|
+
{
|
|
64
|
+
value = value * 2 - 1;
|
|
65
|
+
if(value < 0)
|
|
66
|
+
{
|
|
67
|
+
return 1 - concave[~~(value * -MOD_PRECOMPUTED_LENGTH)] - 1;
|
|
68
|
+
}
|
|
69
|
+
return concave[~~value * MOD_PRECOMPUTED_LENGTH];
|
|
70
|
+
}
|
|
71
|
+
return concave[~~(value * MOD_PRECOMPUTED_LENGTH)]
|
|
72
|
+
|
|
73
|
+
case modulatorCurveTypes.convex:
|
|
74
|
+
// look up the value
|
|
75
|
+
if(polarity)
|
|
76
|
+
{
|
|
77
|
+
value = value * 2 - 1;
|
|
78
|
+
if(value < 0)
|
|
79
|
+
{
|
|
80
|
+
return 1 - convex[~~(value * -MOD_PRECOMPUTED_LENGTH)] - 1;
|
|
81
|
+
}
|
|
82
|
+
return convex[~~(value * MOD_PRECOMPUTED_LENGTH)];
|
|
83
|
+
}
|
|
84
|
+
return convex[~~(value * MOD_PRECOMPUTED_LENGTH)];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export const WORKLET_SYSTEM_REVERB_DIVIDER = 200;
|
|
2
|
+
export const WORKLET_SYSTEM_CHORUS_DIVIDER = 500;
|
|
3
|
+
/**
|
|
4
|
+
* stereo_panner.js
|
|
5
|
+
* purpose: pans a given voice out to the stereo output and to the effects' outputs
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Pans the voice to the given output buffers
|
|
10
|
+
* @param gainLeft {number} the left channel gain
|
|
11
|
+
* @param gainRight {number} the right channel gain
|
|
12
|
+
* @param inputBuffer {Float32Array} the input buffer in mono
|
|
13
|
+
* @param output {Float32Array[]} stereo output buffer
|
|
14
|
+
* @param reverb {Float32Array[]} stereo reverb input
|
|
15
|
+
* @param reverbLevel {number} 0 to 1000, the level of reverb to send
|
|
16
|
+
* @param chorus {Float32Array[]} stereo chorus buttfer
|
|
17
|
+
* @param chorusLevel {number} 0 to 1000, the level of chorus to send
|
|
18
|
+
*/
|
|
19
|
+
export function panVoice(gainLeft,
|
|
20
|
+
gainRight,
|
|
21
|
+
inputBuffer,
|
|
22
|
+
output,
|
|
23
|
+
reverb,
|
|
24
|
+
reverbLevel,
|
|
25
|
+
chorus,
|
|
26
|
+
chorusLevel)
|
|
27
|
+
{
|
|
28
|
+
if(isNaN(inputBuffer[0]))
|
|
29
|
+
{
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if(reverbLevel > 0 && reverb !== undefined)
|
|
34
|
+
{
|
|
35
|
+
const reverbLeft = reverb[0];
|
|
36
|
+
const reverbRight = reverb[1];
|
|
37
|
+
// cap reverb
|
|
38
|
+
reverbLevel = Math.min(reverbLevel, 1000);
|
|
39
|
+
const reverbGain = reverbLevel / WORKLET_SYSTEM_REVERB_DIVIDER;
|
|
40
|
+
const reverbLeftGain = gainLeft * reverbGain;
|
|
41
|
+
const reverbRightGain = gainRight * reverbGain;
|
|
42
|
+
for (let i = 0; i < inputBuffer.length; i++) {
|
|
43
|
+
reverbLeft[i] += reverbLeftGain * inputBuffer[i];
|
|
44
|
+
reverbRight[i] += reverbRightGain * inputBuffer[i];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if(chorusLevel > 0 && chorus !== undefined)
|
|
49
|
+
{
|
|
50
|
+
const chorusLeft = chorus[0];
|
|
51
|
+
const chorusRight = chorus[1];
|
|
52
|
+
// cap chorus
|
|
53
|
+
chorusLevel = Math.min(chorusLevel, 1000);
|
|
54
|
+
const chorusGain = chorusLevel / WORKLET_SYSTEM_CHORUS_DIVIDER;
|
|
55
|
+
const chorusLeftGain = gainLeft * chorusGain;
|
|
56
|
+
const chorusRightGain = gainRight * chorusGain;
|
|
57
|
+
for (let i = 0; i < inputBuffer.length; i++) {
|
|
58
|
+
chorusLeft[i] += chorusLeftGain * inputBuffer[i];
|
|
59
|
+
chorusRight[i] += chorusRightGain * inputBuffer[i];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const leftChannel = output[0];
|
|
64
|
+
const rightChannel = output[1];
|
|
65
|
+
if(gainLeft > 0)
|
|
66
|
+
{
|
|
67
|
+
for (let i = 0; i < inputBuffer.length; i++) {
|
|
68
|
+
leftChannel[i] += gainLeft * inputBuffer[i];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if(gainRight > 0) {
|
|
72
|
+
for (let i = 0; i < inputBuffer.length; i++) {
|
|
73
|
+
rightChannel[i] += gainRight * inputBuffer[i];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* unit_converter.js
|
|
3
|
+
* purpose: converts soundfont units into more useable values with the use of lookup tables to improve performance
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
// timecent lookup table
|
|
8
|
+
const MIN_TIMECENT = -15000;
|
|
9
|
+
const MAX_TIMECENT = 15000;
|
|
10
|
+
const timecentLookupTable = new Float32Array(MAX_TIMECENT - MIN_TIMECENT + 1);
|
|
11
|
+
for (let i = 0; i < timecentLookupTable.length; i++) {
|
|
12
|
+
const timecents = MIN_TIMECENT + i;
|
|
13
|
+
timecentLookupTable[i] = Math.pow(2, timecents / 1200);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Converts timecents to seconds
|
|
18
|
+
* @param timecents {number} timecents
|
|
19
|
+
* @returns {number} seconds
|
|
20
|
+
*/
|
|
21
|
+
export function timecentsToSeconds(timecents)
|
|
22
|
+
{
|
|
23
|
+
return timecentLookupTable[timecents - MIN_TIMECENT];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// abs cent lookup table
|
|
27
|
+
const MIN_ABS_CENT = -20000; // freqVibLfo
|
|
28
|
+
const MAX_ABS_CENT = 16500; // filterFc
|
|
29
|
+
const absoluteCentLookupTable = new Float32Array(MAX_ABS_CENT - MIN_ABS_CENT + 1);
|
|
30
|
+
for (let i = 0; i < absoluteCentLookupTable.length; i++) {
|
|
31
|
+
const absoluteCents = MIN_ABS_CENT + i;
|
|
32
|
+
absoluteCentLookupTable[i] = 440 * Math.pow(2, (absoluteCents - 6900) / 1200);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Converts absolute cents to hertz
|
|
37
|
+
* @param cents {number} absolute cents
|
|
38
|
+
* @returns {number} hertz
|
|
39
|
+
*/
|
|
40
|
+
export function absCentsToHz(cents)
|
|
41
|
+
{
|
|
42
|
+
if(cents < MIN_ABS_CENT || cents > MAX_ABS_CENT)
|
|
43
|
+
{
|
|
44
|
+
return 440 * Math.pow(2, (cents - 6900) / 1200);
|
|
45
|
+
}
|
|
46
|
+
return absoluteCentLookupTable[~~(cents) - MIN_ABS_CENT];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// decibel lookup table (2 points of precision)
|
|
50
|
+
const MIN_DECIBELS = -1660;
|
|
51
|
+
const MAX_DECIBELS = 1600;
|
|
52
|
+
const decibelLookUpTable = new Float32Array((MAX_DECIBELS - MIN_DECIBELS) * 100 + 1);
|
|
53
|
+
for (let i = 0; i < decibelLookUpTable.length; i++) {
|
|
54
|
+
const decibels = (MIN_DECIBELS * 100 + i) / 100;
|
|
55
|
+
decibelLookUpTable[i] = Math.pow(10, -decibels / 20);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* convers decibel attenuation to gain
|
|
60
|
+
* @param decibels {number} the decibel attenuation
|
|
61
|
+
* @returns {number} gain
|
|
62
|
+
*/
|
|
63
|
+
export function decibelAttenuationToGain(decibels)
|
|
64
|
+
{
|
|
65
|
+
return decibelLookUpTable[Math.floor((decibels - MIN_DECIBELS) * 100)];
|
|
66
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { decibelAttenuationToGain, timecentsToSeconds } from './unit_converter.js'
|
|
2
|
+
import { generatorTypes } from '../../../soundfont/chunk/generators.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* volume_envelope.js
|
|
6
|
+
* purpose: applies a volume envelope for a given voice
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const DB_SILENCE = 100;
|
|
10
|
+
const GAIN_SILENCE = 0.005;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* VOL ENV STATES:
|
|
14
|
+
* 0 - delay
|
|
15
|
+
* 1 - attack
|
|
16
|
+
* 2 - hold/peak
|
|
17
|
+
* 3 - decay
|
|
18
|
+
* 4 - sustain
|
|
19
|
+
* release is indicated by isInRelease property
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Applies volume envelope gain to the given output buffer
|
|
24
|
+
* @param voice {WorkletVoice} the voice we're working on
|
|
25
|
+
* @param audioBuffer {Float32Array} the audio buffer to modify
|
|
26
|
+
* @param currentTime {number} the current audio time
|
|
27
|
+
* @param centibelOffset {number} the centibel offset of volume, for modLFOtoVolume
|
|
28
|
+
* @param sampleTime {number} single sample time in seconds, usually 1 / 44100 of a second
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
export function applyVolumeEnvelope(voice, audioBuffer, currentTime, centibelOffset, sampleTime)
|
|
32
|
+
{
|
|
33
|
+
// calculate values
|
|
34
|
+
let decibelOffset = centibelOffset / 10;
|
|
35
|
+
|
|
36
|
+
// calculate env times
|
|
37
|
+
let attack = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.attackVolEnv]);
|
|
38
|
+
let decay = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.decayVolEnv] + ((60 - voice.midiNote) * voice.modulatedGenerators[generatorTypes.keyNumToVolEnvDecay]));
|
|
39
|
+
|
|
40
|
+
// calculate absolute times
|
|
41
|
+
let attenuation = voice.modulatedGenerators[generatorTypes.initialAttenuation] / 10; // divide by ten to get decibelts
|
|
42
|
+
let release = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.releaseVolEnv]);
|
|
43
|
+
let sustain = attenuation + voice.modulatedGenerators[generatorTypes.sustainVolEnv] / 10;
|
|
44
|
+
let delayEnd = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.delayVolEnv]) + voice.startTime;
|
|
45
|
+
let attackEnd = attack + delayEnd;
|
|
46
|
+
let holdEnd = timecentsToSeconds(voice.modulatedGenerators[generatorTypes.holdVolEnv] + ((60 - voice.midiNote) * voice.modulatedGenerators[generatorTypes.keyNumToVolEnvHold])) + attackEnd;
|
|
47
|
+
let decayEnd = decay + holdEnd;
|
|
48
|
+
|
|
49
|
+
// check if voice is in release
|
|
50
|
+
if(voice.isInRelease)
|
|
51
|
+
{
|
|
52
|
+
// calculate the db attenuation at the time of release (not a constant because it can change (ex, volume set to 0, the sound should cut off)
|
|
53
|
+
let releaseStartDb;
|
|
54
|
+
switch (voice.volumeEnvelopeState)
|
|
55
|
+
{
|
|
56
|
+
case 0:
|
|
57
|
+
// no sound: fill with zero and skip!
|
|
58
|
+
for (let i = 0; i < audioBuffer.length; i++) {
|
|
59
|
+
audioBuffer[i] = 0;
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
|
|
63
|
+
case 1:
|
|
64
|
+
// attack phase
|
|
65
|
+
// attack is linear (in gain) so we need to do get db from that
|
|
66
|
+
let elapsed = 1 - ((attackEnd - voice.releaseStartTime) / attack);
|
|
67
|
+
// calculate the gain that the attack would have
|
|
68
|
+
let attackGain = elapsed * decibelAttenuationToGain(attenuation + decibelOffset);
|
|
69
|
+
|
|
70
|
+
// turn that into db
|
|
71
|
+
releaseStartDb = 20 * Math.log10(attackGain) * -1;
|
|
72
|
+
break;
|
|
73
|
+
|
|
74
|
+
case 2:
|
|
75
|
+
// hold
|
|
76
|
+
releaseStartDb = attenuation;
|
|
77
|
+
break;
|
|
78
|
+
|
|
79
|
+
case 3:
|
|
80
|
+
// decay
|
|
81
|
+
releaseStartDb = (1 - (decayEnd - voice.releaseStartTime) / decay) * (sustain - attenuation) + attenuation;
|
|
82
|
+
break;
|
|
83
|
+
|
|
84
|
+
case 4:
|
|
85
|
+
// sustain
|
|
86
|
+
releaseStartDb = sustain;
|
|
87
|
+
break;
|
|
88
|
+
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// uncommented because doesn't seem to be needed but just in case
|
|
92
|
+
// // if the voice is not released, but state set to true (due to min note length, simply use the release db)
|
|
93
|
+
// if(voice.releaseStartTime > currentTime)
|
|
94
|
+
// {
|
|
95
|
+
// const gain = decibelAttenuationToGain(releaseStartDb + decibelOffset);
|
|
96
|
+
// for (let i = 0; i < audioBuffer.length; i++) {
|
|
97
|
+
// audioBuffer[i] = gain * audioBuffer[i];
|
|
98
|
+
// }
|
|
99
|
+
// return;
|
|
100
|
+
// }
|
|
101
|
+
|
|
102
|
+
let elapsedRelease = currentTime - voice.releaseStartTime;
|
|
103
|
+
let dbDifference = DB_SILENCE - releaseStartDb;
|
|
104
|
+
let gain;
|
|
105
|
+
for (let i = 0; i < audioBuffer.length; i++) {
|
|
106
|
+
let db = (elapsedRelease / release) * dbDifference + releaseStartDb;
|
|
107
|
+
gain = decibelAttenuationToGain(db + decibelOffset);
|
|
108
|
+
audioBuffer[i] = gain * audioBuffer[i];
|
|
109
|
+
elapsedRelease += sampleTime;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if(gain <= GAIN_SILENCE)
|
|
113
|
+
{
|
|
114
|
+
voice.finished = true;
|
|
115
|
+
}
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
let currentFrameTime = currentTime;
|
|
119
|
+
let dbAttenuation;
|
|
120
|
+
for (let i = 0; i < audioBuffer.length; i++) {
|
|
121
|
+
switch(voice.volumeEnvelopeState)
|
|
122
|
+
{
|
|
123
|
+
case 0:
|
|
124
|
+
// delay phase, no sound is produced
|
|
125
|
+
if(currentFrameTime >= delayEnd)
|
|
126
|
+
{
|
|
127
|
+
voice.volumeEnvelopeState++;
|
|
128
|
+
}
|
|
129
|
+
else
|
|
130
|
+
{
|
|
131
|
+
dbAttenuation = DB_SILENCE;
|
|
132
|
+
audioBuffer[i] = 0;
|
|
133
|
+
|
|
134
|
+
// no need to go through the hassle of converting. Skip
|
|
135
|
+
currentFrameTime += sampleTime;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
// fallthrough
|
|
139
|
+
|
|
140
|
+
case 1:
|
|
141
|
+
// attack phase: ramp from 0 to attenuation
|
|
142
|
+
if(currentFrameTime >= attackEnd)
|
|
143
|
+
{
|
|
144
|
+
voice.volumeEnvelopeState++;
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
// Special case: linear gain ramp instead of linear db ramp
|
|
148
|
+
const elapsed = (attackEnd - currentFrameTime) / attack;
|
|
149
|
+
dbAttenuation = 10 * Math.log10((elapsed * (attenuation - DB_SILENCE) + DB_SILENCE) * -1);
|
|
150
|
+
audioBuffer[i] = audioBuffer[i] * (1 - elapsed) * decibelAttenuationToGain(attenuation + decibelOffset);
|
|
151
|
+
currentFrameTime += sampleTime;
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
}
|
|
155
|
+
// fallthrough
|
|
156
|
+
|
|
157
|
+
case 2:
|
|
158
|
+
// hold/peak phase: stay at attenuation
|
|
159
|
+
if(currentFrameTime >= holdEnd)
|
|
160
|
+
{
|
|
161
|
+
voice.volumeEnvelopeState++;
|
|
162
|
+
}
|
|
163
|
+
else
|
|
164
|
+
{
|
|
165
|
+
dbAttenuation = attenuation;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
// fallthrough
|
|
169
|
+
|
|
170
|
+
case 3:
|
|
171
|
+
// decay phase: linear ramp from attenuation to sustain
|
|
172
|
+
if(currentFrameTime >= decayEnd)
|
|
173
|
+
{
|
|
174
|
+
voice.volumeEnvelopeState++;
|
|
175
|
+
}
|
|
176
|
+
else
|
|
177
|
+
{
|
|
178
|
+
dbAttenuation = (1 - (decayEnd - currentFrameTime) / decay) * (sustain - attenuation) + attenuation;
|
|
179
|
+
break
|
|
180
|
+
}
|
|
181
|
+
// fallthrough
|
|
182
|
+
|
|
183
|
+
case 4:
|
|
184
|
+
// sustain phase: stay at sustain
|
|
185
|
+
dbAttenuation = sustain;
|
|
186
|
+
|
|
187
|
+
}
|
|
188
|
+
// apply gain and advance the time
|
|
189
|
+
const gain = decibelAttenuationToGain(dbAttenuation + decibelOffset);
|
|
190
|
+
audioBuffer[i] = audioBuffer[i] * gain;
|
|
191
|
+
currentFrameTime += sampleTime;
|
|
192
|
+
}
|
|
193
|
+
voice.currentAttenuationDb = dbAttenuation;
|
|
194
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wavetable_oscillator.js
|
|
3
|
+
* purpose: plays back raw audio data at an arbitrary playback rate
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Fills the output buffer with raw sample data
|
|
9
|
+
* @param voice {WorkletVoice} the voice we're working on
|
|
10
|
+
* @param sampleData {Float32Array} the sample data to write with
|
|
11
|
+
* @param outputBuffer {Float32Array} the output buffer to write to
|
|
12
|
+
*/
|
|
13
|
+
export function getOscillatorData(voice, sampleData, outputBuffer)
|
|
14
|
+
{
|
|
15
|
+
let cur = voice.sample.cursor;
|
|
16
|
+
const loop = (voice.sample.loopingMode === 1) || (voice.sample.loopingMode === 3 && !voice.isInRelease);
|
|
17
|
+
const loopLength = voice.sample.loopEnd - voice.sample.loopStart;
|
|
18
|
+
|
|
19
|
+
if(loop)
|
|
20
|
+
{
|
|
21
|
+
for (let i = 0; i < outputBuffer.length; i++) {
|
|
22
|
+
// check for loop
|
|
23
|
+
while(cur >= voice.sample.loopEnd) {
|
|
24
|
+
cur -= loopLength;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// grab the 2 nearest points
|
|
28
|
+
const floor = ~~cur;
|
|
29
|
+
let ceil = floor + 1;
|
|
30
|
+
|
|
31
|
+
while(ceil >= voice.sample.loopEnd) {
|
|
32
|
+
ceil -= loopLength;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const fraction = cur - floor;
|
|
36
|
+
|
|
37
|
+
// grab the samples and interpolate
|
|
38
|
+
const upper = sampleData[ceil];
|
|
39
|
+
const lower = sampleData[floor];
|
|
40
|
+
outputBuffer[i] = (lower + (upper - lower) * fraction);
|
|
41
|
+
|
|
42
|
+
// commented code because it's probably gonna come handy... (it did like 6 times already :/)
|
|
43
|
+
// if(isNaN(outputBuffer[i]))
|
|
44
|
+
// {
|
|
45
|
+
// console.error(voice, upper, lower, floor, ceil, cur)
|
|
46
|
+
// throw "NAN ALERT";
|
|
47
|
+
// }
|
|
48
|
+
|
|
49
|
+
cur += voice.sample.playbackStep * voice.currentTuningCalculated;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else
|
|
53
|
+
{
|
|
54
|
+
// check and correct end errors
|
|
55
|
+
if(voice.sample.end >= sampleData.length)
|
|
56
|
+
{
|
|
57
|
+
voice.sample.end = sampleData.length - 1;
|
|
58
|
+
}
|
|
59
|
+
for (let i = 0; i < outputBuffer.length; i++) {
|
|
60
|
+
|
|
61
|
+
// linear interpolation
|
|
62
|
+
const floor = ~~cur;
|
|
63
|
+
const ceil = floor + 1;
|
|
64
|
+
|
|
65
|
+
// flag the voice as finished if needed
|
|
66
|
+
if(ceil >= voice.sample.end)
|
|
67
|
+
{
|
|
68
|
+
voice.finished = true;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const fraction = cur - floor;
|
|
73
|
+
|
|
74
|
+
// grab the samples and interpolate
|
|
75
|
+
const upper = sampleData[ceil];
|
|
76
|
+
const lower = sampleData[floor];
|
|
77
|
+
outputBuffer[i] = (lower + (upper - lower) * fraction);
|
|
78
|
+
|
|
79
|
+
cur += voice.sample.playbackStep * voice.currentTuningCalculated;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
voice.sample.cursor = cur;
|
|
83
|
+
}
|