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.
Files changed (55) hide show
  1. package/.idea/inspectionProfiles/Project_Default.xml +10 -0
  2. package/.idea/jsLibraryMappings.xml +6 -0
  3. package/.idea/modules.xml +8 -0
  4. package/.idea/spessasynth_core.iml +12 -0
  5. package/.idea/vcs.xml +6 -0
  6. package/README.md +376 -0
  7. package/index.js +7 -0
  8. package/package.json +34 -0
  9. package/spessasynth_core/midi_parser/README.md +3 -0
  10. package/spessasynth_core/midi_parser/midi_loader.js +381 -0
  11. package/spessasynth_core/midi_parser/midi_message.js +231 -0
  12. package/spessasynth_core/sequencer/sequencer.js +192 -0
  13. package/spessasynth_core/sequencer/worklet_sequencer/play.js +221 -0
  14. package/spessasynth_core/sequencer/worklet_sequencer/process_event.js +138 -0
  15. package/spessasynth_core/sequencer/worklet_sequencer/process_tick.js +85 -0
  16. package/spessasynth_core/sequencer/worklet_sequencer/song_control.js +90 -0
  17. package/spessasynth_core/soundfont/README.md +4 -0
  18. package/spessasynth_core/soundfont/chunk/generators.js +205 -0
  19. package/spessasynth_core/soundfont/chunk/instruments.js +60 -0
  20. package/spessasynth_core/soundfont/chunk/modulators.js +232 -0
  21. package/spessasynth_core/soundfont/chunk/presets.js +264 -0
  22. package/spessasynth_core/soundfont/chunk/riff_chunk.js +46 -0
  23. package/spessasynth_core/soundfont/chunk/samples.js +250 -0
  24. package/spessasynth_core/soundfont/chunk/zones.js +264 -0
  25. package/spessasynth_core/soundfont/soundfont_parser.js +301 -0
  26. package/spessasynth_core/synthetizer/README.md +6 -0
  27. package/spessasynth_core/synthetizer/synthesizer.js +303 -0
  28. package/spessasynth_core/synthetizer/worklet_system/README.md +3 -0
  29. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/controller_control.js +285 -0
  30. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/data_entry.js +280 -0
  31. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_off.js +102 -0
  32. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_on.js +75 -0
  33. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/program_control.js +140 -0
  34. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/system_exclusive.js +265 -0
  35. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/tuning_control.js +105 -0
  36. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/vibrato_control.js +29 -0
  37. package/spessasynth_core/synthetizer/worklet_system/worklet_methods/voice_control.js +186 -0
  38. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/lfo.js +23 -0
  39. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +95 -0
  40. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js +73 -0
  41. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/modulator_curves.js +86 -0
  42. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/stereo_panner.js +76 -0
  43. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/unit_converter.js +66 -0
  44. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +194 -0
  45. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +83 -0
  46. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +173 -0
  47. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +105 -0
  48. package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +313 -0
  49. package/spessasynth_core/utils/README.md +4 -0
  50. package/spessasynth_core/utils/buffer_to_wav.js +70 -0
  51. package/spessasynth_core/utils/byte_functions.js +141 -0
  52. package/spessasynth_core/utils/loggin.js +79 -0
  53. package/spessasynth_core/utils/other.js +49 -0
  54. package/spessasynth_core/utils/shiftable_array.js +26 -0
  55. 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
+ }