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,173 @@
1
+ import { modulatorSources } from '../../../soundfont/chunk/modulators.js'
2
+ import { getModulatorCurveValue, MOD_PRECOMPUTED_LENGTH } from './modulator_curves.js'
3
+ import { NON_CC_INDEX_OFFSET } from './worklet_processor_channel.js'
4
+
5
+ /**
6
+ * worklet_modulator.js
7
+ * purpose: precomputes all curve types and computes modulators
8
+ */
9
+
10
+ /**
11
+ * Computes a given modulator
12
+ * @param controllerTable {Int16Array} all midi controllers as 14bit values + the non controller indexes, starting at 128
13
+ * @param modulator {Modulator} the modulator to compute
14
+ * @param midiNote {number} the midiNote of the voice belonging to the modulator
15
+ * @param velocity {number} the velocity of the voice belonging to the modulator
16
+ * @returns {number} the computed value
17
+ */
18
+ export function computeWorkletModulator(controllerTable, modulator, midiNote, velocity)
19
+ {
20
+ if(modulator.transformAmount === 0)
21
+ {
22
+ return 0;
23
+ }
24
+ // mapped to 0-16384
25
+ let rawSourceValue = 0;
26
+ if(modulator.sourceUsesCC)
27
+ {
28
+ rawSourceValue = controllerTable[modulator.sourceIndex];
29
+ }
30
+ else
31
+ {
32
+ const index = modulator.sourceIndex + NON_CC_INDEX_OFFSET;
33
+ switch (modulator.sourceIndex)
34
+ {
35
+ case modulatorSources.noController:
36
+ return 0;// fluid_mod.c line 374 (0 times secondary times amount is still zero)
37
+
38
+ case modulatorSources.noteOnKeyNum:
39
+ rawSourceValue = midiNote << 7;
40
+ break;
41
+
42
+ case modulatorSources.noteOnVelocity:
43
+ case modulatorSources.polyPressure:
44
+ rawSourceValue = velocity << 7;
45
+ break;
46
+
47
+ default:
48
+ rawSourceValue = controllerTable[index]; // use the 7 bit value
49
+ break;
50
+ }
51
+
52
+ }
53
+
54
+ const sourceValue = transforms[modulator.sourceCurveType][modulator.sourcePolarity][modulator.sourceDirection][rawSourceValue];
55
+
56
+ // mapped to 0-127
57
+ let rawSecondSrcValue;
58
+ if(modulator.secSrcUsesCC)
59
+ {
60
+ rawSecondSrcValue = controllerTable[modulator.secSrcIndex];
61
+ }
62
+ else
63
+ {
64
+ const index = modulator.secSrcIndex + NON_CC_INDEX_OFFSET;
65
+ switch (modulator.secSrcIndex)
66
+ {
67
+ case modulatorSources.noController:
68
+ rawSecondSrcValue = 16383;// fluid_mod.c line 376
69
+ break;
70
+
71
+ case modulatorSources.noteOnKeyNum:
72
+ rawSecondSrcValue = midiNote << 7;
73
+ break;
74
+
75
+ case modulatorSources.noteOnVelocity:
76
+ case modulatorSources.polyPressure:
77
+ rawSecondSrcValue = velocity << 7;
78
+ break;
79
+
80
+ default:
81
+ rawSecondSrcValue = controllerTable[index];
82
+ }
83
+
84
+ }
85
+ const secondSrcValue = transforms[modulator.secSrcCurveType][modulator.secSrcPolarity][modulator.secSrcDirection][rawSecondSrcValue];
86
+
87
+
88
+ // compute the modulator
89
+ const computedValue = sourceValue * secondSrcValue * modulator.transformAmount;
90
+
91
+ if(modulator.transformType === 2)
92
+ {
93
+ // abs value
94
+ return Math.abs(computedValue);
95
+ }
96
+ return computedValue;
97
+ }
98
+
99
+ /**
100
+ * Computes all modulators of a given voice
101
+ * @param voice {WorkletVoice} the voice to compute modulators for
102
+ * @param controllerTable {Int16Array} all midi controllers as 14bit values + the non controller indexes, starting at 128
103
+ */
104
+ export function computeModulators(voice, controllerTable)
105
+ {
106
+ voice.modulatedGenerators.set(voice.generators);
107
+ voice.modulators.forEach(mod => {
108
+ voice.modulatedGenerators[mod.modulatorDestination] += computeWorkletModulator(controllerTable, mod, voice.midiNote, voice.velocity);
109
+ });
110
+ }
111
+
112
+ /**
113
+ * as follows: transforms[curveType][polarity][direction] is an array
114
+ * @type {Float32Array[][][]}
115
+ */
116
+ const transforms = [];
117
+
118
+ for(let curve = 0; curve < 4; curve++)
119
+ {
120
+ transforms[curve] =
121
+ [
122
+ [
123
+ new Float32Array(MOD_PRECOMPUTED_LENGTH),
124
+ new Float32Array(MOD_PRECOMPUTED_LENGTH)
125
+ ],
126
+ [
127
+ new Float32Array(MOD_PRECOMPUTED_LENGTH),
128
+ new Float32Array(MOD_PRECOMPUTED_LENGTH)
129
+ ]
130
+ ];
131
+ for (let i = 0; i < MOD_PRECOMPUTED_LENGTH; i++) {
132
+
133
+ // polarity 0 dir 0
134
+ transforms[curve][0][0][i] = getModulatorCurveValue(
135
+ 0,
136
+ curve,
137
+ i / MOD_PRECOMPUTED_LENGTH,
138
+ 0);
139
+ if (isNaN(transforms[curve][0][0][i])) {
140
+ transforms[curve][0][0][i] = 1;
141
+ }
142
+
143
+ // polarity 1 dir 0
144
+ transforms[curve][1][0][i] = getModulatorCurveValue(
145
+ 0,
146
+ curve,
147
+ i / MOD_PRECOMPUTED_LENGTH,
148
+ 1);
149
+ if (isNaN(transforms[curve][1][0][i])) {
150
+ transforms[curve][1][0][i] = 1;
151
+ }
152
+
153
+ // polarity 0 dir 1
154
+ transforms[curve][0][1][i] = getModulatorCurveValue(
155
+ 1,
156
+ curve,
157
+ i / MOD_PRECOMPUTED_LENGTH,
158
+ 0);
159
+ if (isNaN(transforms[curve][0][1][i])) {
160
+ transforms[curve][0][1][i] = 1;
161
+ }
162
+
163
+ // polarity 1 dir 1
164
+ transforms[curve][1][1][i] = getModulatorCurveValue(
165
+ 1,
166
+ curve,
167
+ i / MOD_PRECOMPUTED_LENGTH,
168
+ 1);
169
+ if (isNaN(transforms[curve][1][1][i])) {
170
+ transforms[curve][1][1][i] = 1;
171
+ }
172
+ }
173
+ }
@@ -0,0 +1,105 @@
1
+ import { midiControllers } from '../../../midi_parser/midi_message.js'
2
+ import { modulatorSources } from '../../../soundfont/chunk/modulators.js'
3
+ /**
4
+ * @typedef {Object} WorkletProcessorChannel
5
+ * @property {Int16Array} midiControllers - array of MIDI controller values
6
+ * @property {boolean[]} lockedControllers - array indicating if a controller is locked
7
+ * @property {Float32Array} customControllers - array of custom (not sf2) control values such as RPN pitch tuning, transpose, modulation depth, etc.
8
+ * @property {boolean} holdPedal - indicates whether the hold pedal is active
9
+ * @property {boolean} drumChannel - indicates whether the channel is a drum channel
10
+ * @property {dataEntryStates} dataEntryState - the current state of the data entry
11
+ * @property {number} NRPCoarse - the current coarse value of the Non-Registered Parameter
12
+ * @property {number} NRPFine - the current fine value of the Non-Registered Parameter
13
+ * @property {number} RPValue - the current value of the Registered Parameter
14
+ *
15
+ * @property {Preset} preset - the channel's preset
16
+ * @property {boolean} lockPreset - indicates whether the program on the channel is locked
17
+ *
18
+ * @property {boolean} lockVibrato - indicates whether the custom vibrato is locked
19
+ * @property {Object} channelVibrato - vibrato settings for the channel
20
+ * @property {number} channelVibrato.depth - depth of the vibrato effect (cents)
21
+ * @property {number} channelVibrato.delay - delay before the vibrato effect starts (seconds)
22
+ * @property {number} channelVibrato.rate - rate of the vibrato oscillation (Hz)
23
+
24
+ * @property {boolean} isMuted - indicates whether the channel is muted
25
+ * @property {WorkletVoice[]} voices - array of voices currently active on the channel
26
+ * @property {WorkletVoice[]} sustainedVoices - array of voices that are sustained on the channel
27
+ * @property {WorkletVoice[][][]} cachedVoices - first is midi note, second is velocity. output is an array of WorkletVoices
28
+ */
29
+
30
+ /**
31
+ * @this {Synthesizer}
32
+ */
33
+ export function addNewChannel()
34
+ {
35
+ /**
36
+ * @type {WorkletProcessorChannel}
37
+ */
38
+ const channel = {
39
+ midiControllers: new Int16Array(CONTROLLER_TABLE_SIZE),
40
+ lockedControllers: Array(CONTROLLER_TABLE_SIZE).fill(false),
41
+ customControllers: new Float32Array(CUSTOM_CONTROLLER_TABLE_SIZE),
42
+
43
+ NRPCoarse: 0,
44
+ NRPFine: 0,
45
+ RPValue: 0,
46
+ dataEntryState: dataEntryStates.Idle,
47
+
48
+ voices: [],
49
+ sustainedVoices: [],
50
+ cachedVoices: [],
51
+ preset: this.defaultPreset,
52
+
53
+ channelVibrato: {delay: 0, depth: 0, rate: 0},
54
+ lockVibrato: false,
55
+ holdPedal: false,
56
+ isMuted: false,
57
+ drumChannel: false,
58
+ lockPreset: false,
59
+
60
+ }
61
+ for (let i = 0; i < 128; i++) {
62
+ channel.cachedVoices.push([]);
63
+ }
64
+ this.workletProcessorChannels.push(channel);
65
+ this.resetControllers(this.workletProcessorChannels.length - 1);
66
+ }
67
+
68
+ export const NON_CC_INDEX_OFFSET = 128;
69
+ export const CONTROLLER_TABLE_SIZE = 147;
70
+ // an array with preset default values so we can quickly use set() to reset the controllers
71
+ export const resetArray = new Int16Array(CONTROLLER_TABLE_SIZE);
72
+ // default values (the array is 14 bit so shift the 7 bit values by 7 bits)
73
+ resetArray[midiControllers.mainVolume] = 100 << 7;
74
+ resetArray[midiControllers.expressionController] = 127 << 7;
75
+ resetArray[midiControllers.pan] = 64 << 7;
76
+ resetArray[midiControllers.releaseTime] = 64 << 7;
77
+ resetArray[midiControllers.brightness] = 64 << 7;
78
+ resetArray[midiControllers.effects1Depth] = 40 << 7;
79
+ resetArray[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel] = 8192;
80
+ resetArray[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheelRange] = 2 << 7;
81
+ resetArray[NON_CC_INDEX_OFFSET + modulatorSources.channelPressure] = 127 << 7;
82
+
83
+ /**
84
+ * @enum {number}
85
+ */
86
+ export const dataEntryStates = {
87
+ Idle: 0,
88
+ RPCoarse: 1,
89
+ RPFine: 2,
90
+ NRPCoarse: 3,
91
+ NRPFine: 4,
92
+ DataCoarse: 5,
93
+ DataFine: 6
94
+ };
95
+
96
+
97
+ export const customControllers = {
98
+ channelTuning: 0, // cents
99
+ channelTranspose: 1, // cents
100
+ modulationMultiplier: 2, // cents
101
+ masterTuning: 3, // cents
102
+ }
103
+ export const CUSTOM_CONTROLLER_TABLE_SIZE = Object.keys(customControllers).length;
104
+ export const customResetArray = new Float32Array(CUSTOM_CONTROLLER_TABLE_SIZE);
105
+ customResetArray[customControllers.modulationMultiplier] = 1;
@@ -0,0 +1,313 @@
1
+ /**
2
+ * worklet_voice.js
3
+ * purpose: prepares workletvoices from sample and generator data and manages sample dumping
4
+ * note: sample dumping means sending it over to the AudioWorkletGlobalScope
5
+ */
6
+
7
+ /**
8
+ * @typedef {Object} WorkletSample
9
+ * @property {number} sampleID - ID of the sample
10
+ * @property {number} playbackStep - current playback step (rate)
11
+ * @property {number} cursor - current position in the sample
12
+ * @property {number} rootKey - root key of the sample
13
+ * @property {number} loopStart - start position of the loop
14
+ * @property {number} loopEnd - end position of the loop
15
+ * @property {number} end - end position of the sample
16
+ * @property {0|1|2} loopingMode - looping mode of the sample
17
+ */
18
+
19
+ /**
20
+ * @typedef {Object} WorkletLowpassFilter
21
+ * @property {number} a0 - filter coefficient 1
22
+ * @property {number} a1 - filter coefficient 2
23
+ * @property {number} a2 - filter coefficient 3
24
+ * @property {number} a3 - filter coefficient 4
25
+ * @property {number} a4 - filter coefficient 5
26
+ * @property {number} x1 - input history 1
27
+ * @property {number} x2 - input history 2
28
+ * @property {number} y1 - output history 1
29
+ * @property {number} y2 - output history 2
30
+ * @property {number} reasonanceCb - reasonance in centibels
31
+ * @property {number} reasonanceGain - resonance gain
32
+ * @property {number} cutoffCents - cutoff frequency in cents
33
+ * @property {number} cutoffHz - cutoff frequency in Hz
34
+ */
35
+
36
+ /**
37
+ * @typedef {Object} WorkletVoice
38
+ * @property {WorkletSample} sample - sample ID for voice.
39
+ * @property {WorkletLowpassFilter} filter - lowpass filter applied to the voice
40
+ * @property {Int16Array} generators - the unmodulated (constant) generators of the voice
41
+ * @property {Modulator[]} modulators - the voice's modulators
42
+ * @property {Int16Array} modulatedGenerators - the generators modulated by the modulators
43
+ *
44
+ * @property {boolean} finished - indicates if the voice has finished
45
+ * @property {boolean} isInRelease - indicates if the voice is in the release phase
46
+ *
47
+ * @property {number} channelNumber - MIDI channel number
48
+ * @property {number} velocity - velocity of the note
49
+ * @property {number} midiNote - MIDI note number
50
+ * @property {number} targetKey - target key for the note
51
+ *
52
+ * @property {number} currentAttenuationDb - current attenuation in dB (used for calculating start of the release phase)
53
+ * @property {0|1|2|3|4} volumeEnvelopeState - state of the volume envelope.
54
+ * @property {number} currentModEnvValue - current value of the modulation envelope
55
+ *
56
+ * @property {number} startTime - start time of the voice
57
+ * @property {number} releaseStartTime - start time of the release phase
58
+ * @property {number} releaseStartModEnv - modenv value at the start of the release phase
59
+ *
60
+ * @property {number} currentTuningCents - current tuning adjustment in cents
61
+ * @property {number} currentTuningCalculated - calculated tuning adjustment
62
+ * @property {number} currentPan - from 0 to 1
63
+ */
64
+
65
+ import { addAndClampGenerator, generatorTypes } from '../../../soundfont/chunk/generators.js'
66
+ import { SpessaSynthTable } from '../../../utils/loggin.js'
67
+
68
+
69
+ /**
70
+ * the sampleID is the index
71
+ * @type {boolean[]}
72
+ */
73
+ let globalDumpedSamplesList = [];
74
+
75
+ export function clearSamplesList()
76
+ {
77
+ globalDumpedSamplesList = [];
78
+ }
79
+
80
+ function /**
81
+ * This is how the logic works: since sf3 is compressed, we rely on an async decoder.
82
+ * So, if the sample isn't loaded yet:
83
+ * send the workletVoice (generators, modulators, etc) and the WorkletSample(sampleID + end offset + loop)
84
+ * once the voice is done, then we dump it.
85
+ *
86
+ * on the WorkletScope side:
87
+ * skip the voice if sampleID isn't valid
88
+ * once we receive a sample dump, adjust all voice endOffsets (loop is already correct in sf3)
89
+ * now the voice starts playing, yay!
90
+ * @param channel {number} channel hint for the processor to recalculate cursor positions
91
+ * @param sample {Sample}
92
+ * @param id {number}
93
+ * @param sampleDumpCallback {function({channel: number, sampleID: number, sampleData: Float32Array})}
94
+ */
95
+ dumpSample(channel, sample, id, sampleDumpCallback)
96
+ {
97
+ // flag as defined, so it's currently being dumped
98
+ globalDumpedSamplesList[id] = false;
99
+
100
+ // load the data
101
+ sampleDumpCallback({
102
+ channel: channel,
103
+ sampleID: id,
104
+ sampleData: sample.getAudioData()
105
+ });
106
+ globalDumpedSamplesList[id] = true;
107
+ }
108
+
109
+ /**
110
+ * Deep clone function for the WorkletVoice object and its nested structures.
111
+ * This function handles Int16Array, objects, arrays, and primitives.
112
+ * It does not handle circular references.
113
+ * @param {WorkletVoice} obj - The object to clone.
114
+ * @returns {WorkletVoice} - Cloned object.
115
+ */
116
+ function deepClone(obj) {
117
+ if (obj === null || typeof obj !== 'object') {
118
+ return obj;
119
+ }
120
+
121
+ // Handle Int16Array separately
122
+ if (obj instanceof Int16Array) {
123
+ return new Int16Array(obj);
124
+ }
125
+
126
+ // Handle objects and arrays
127
+ const clonedObj = Array.isArray(obj) ? [] : {};
128
+ for (let key in obj) {
129
+ if (obj.hasOwnProperty(key)) {
130
+ if (typeof obj[key] === 'object' && obj[key] !== null) {
131
+ clonedObj[key] = deepClone(obj[key]); // Recursively clone nested objects
132
+ } else if (obj[key] instanceof Int16Array) {
133
+ clonedObj[key] = new Int16Array(obj[key]); // Clone Int16Array
134
+ } else {
135
+ clonedObj[key] = obj[key]; // Copy primitives
136
+ }
137
+ }
138
+ }
139
+ return clonedObj;
140
+ }
141
+
142
+
143
+ /**
144
+ * @param channel {number} a hint for the processor to recalculate sample cursors when sample dumping
145
+ * @param midiNote {number}
146
+ * @param velocity {number}
147
+ * @param preset {Preset}
148
+ * @param currentTime {number}
149
+ * @param sampleRate {number}
150
+ * @param sampleDumpCallback {function({channel: number, sampleID: number, sampleData: Float32Array})}
151
+ * @param cachedVoices {WorkletVoice[][][]} first is midi note, second is velocity. output is an array of WorkletVoices
152
+ * @param debug {boolean}
153
+ * @returns {WorkletVoice[]}
154
+ */
155
+ export function getWorkletVoices(channel,
156
+ midiNote,
157
+ velocity,
158
+ preset,
159
+ currentTime,
160
+ sampleRate,
161
+ sampleDumpCallback,
162
+ cachedVoices,
163
+ debug=false)
164
+ {
165
+ /**
166
+ * @type {WorkletVoice[]}
167
+ */
168
+ let workletVoices;
169
+
170
+ const cached = cachedVoices[midiNote][velocity];
171
+ if(cached)
172
+ {
173
+ workletVoices = cached.map(deepClone);
174
+ workletVoices.forEach(v => {
175
+ v.startTime = currentTime;
176
+ });
177
+ }
178
+ else
179
+ {
180
+ let canCache = true;
181
+ /**
182
+ * @returns {WorkletVoice}
183
+ */
184
+ workletVoices = preset.getSamplesAndGenerators(midiNote, velocity).map(sampleAndGenerators => {
185
+ // dump the sample if haven't already
186
+ if (globalDumpedSamplesList[sampleAndGenerators.sampleID] !== true) {
187
+ // if the sample is currently being loaded, don't dump again (undefined means not loaded, false means is being loaded)
188
+ if(globalDumpedSamplesList[sampleAndGenerators.sampleID] === undefined) {
189
+ dumpSample(channel, sampleAndGenerators.sample, sampleAndGenerators.sampleID, sampleDumpCallback);
190
+ }
191
+
192
+ // can't cache the voice as the end in workletSample maybe is incorrect (the sample is still loading)
193
+ if(globalDumpedSamplesList[sampleAndGenerators.sampleID] !== true)
194
+ {
195
+ canCache = false;
196
+ }
197
+ }
198
+
199
+ // create the generator list
200
+ const generators = new Int16Array(60);
201
+ // apply and sum the gens
202
+ for (let i = 0; i < 60; i++) {
203
+ generators[i] = addAndClampGenerator(i, sampleAndGenerators.presetGenerators, sampleAndGenerators.instrumentGenerators);
204
+ }
205
+
206
+ // !! EMU initial attenuation correction, multiply initial attenuation by 0.4
207
+ generators[generatorTypes.initialAttenuation] = Math.floor(generators[generatorTypes.initialAttenuation] * 0.4);
208
+
209
+ // key override
210
+ let rootKey = sampleAndGenerators.sample.samplePitch;
211
+ if (generators[generatorTypes.overridingRootKey] > -1) {
212
+ rootKey = generators[generatorTypes.overridingRootKey];
213
+ }
214
+
215
+ let targetKey = midiNote;
216
+ if (generators[generatorTypes.keyNum] > -1) {
217
+ targetKey = generators[generatorTypes.keyNum];
218
+ }
219
+
220
+ // determine looping mode now. if the loop is too small, disable
221
+ const loopStart = (sampleAndGenerators.sample.sampleLoopStartIndex / 2) + (generators[generatorTypes.startloopAddrsOffset] + (generators[generatorTypes.startloopAddrsCoarseOffset] * 32768));
222
+ const loopEnd = (sampleAndGenerators.sample.sampleLoopEndIndex / 2) + (generators[generatorTypes.endloopAddrsOffset] + (generators[generatorTypes.endloopAddrsCoarseOffset] * 32768));
223
+ let loopingMode = generators[generatorTypes.sampleModes];
224
+ if (loopEnd - loopStart < 1) {
225
+ loopingMode = 0;
226
+ }
227
+
228
+ // determine end
229
+ /**
230
+ * create the worklet sample
231
+ * @type {WorkletSample}
232
+ */
233
+ const workletSample = {
234
+ sampleID: sampleAndGenerators.sampleID,
235
+ playbackStep: (sampleAndGenerators.sample.sampleRate / sampleRate) * Math.pow(2, sampleAndGenerators.sample.samplePitchCorrection / 1200),// cent tuning
236
+ cursor: generators[generatorTypes.startAddrsOffset] + (generators[generatorTypes.startAddrsCoarseOffset] * 32768),
237
+ rootKey: rootKey,
238
+ loopStart: loopStart,
239
+ loopEnd: loopEnd,
240
+ end: Math.floor( sampleAndGenerators.sample.sampleData.length) - 1 + (generators[generatorTypes.endAddrOffset] + (generators[generatorTypes.endAddrsCoarseOffset] * 32768)),
241
+ loopingMode: loopingMode
242
+ };
243
+
244
+ // velocity override
245
+ if (generators[generatorTypes.velocity] > -1) {
246
+ velocity = generators[generatorTypes.velocity];
247
+ }
248
+
249
+ if(debug)
250
+ {
251
+ SpessaSynthTable([{
252
+ Sample: sampleAndGenerators.sample.sampleName,
253
+ Generators: generators,
254
+ Modulators: sampleAndGenerators.modulators.map(m => m.debugString()),
255
+ Velocity: velocity,
256
+ TargetKey: targetKey,
257
+ MidiNote: midiNote,
258
+ WorkletSample: workletSample
259
+ }]);
260
+ }
261
+
262
+ return {
263
+ filter: {
264
+ a0: 0,
265
+ a1: 0,
266
+ a2: 0,
267
+ a3: 0,
268
+ a4: 0,
269
+
270
+ x1: 0,
271
+ x2: 0,
272
+ y1: 0,
273
+ y2: 0,
274
+ reasonanceCb: 0,
275
+ reasonanceGain: 1,
276
+ cutoffCents: 13500,
277
+ cutoffHz: 20000
278
+ },
279
+ // generators and modulators
280
+ generators: generators,
281
+ modulators: sampleAndGenerators.modulators,
282
+ modulatedGenerators: new Int16Array(60),
283
+
284
+ // sample and playback data
285
+ sample: workletSample,
286
+ velocity: velocity,
287
+ midiNote: midiNote,
288
+ channelNumber: channel,
289
+ startTime: currentTime,
290
+ targetKey: targetKey,
291
+ currentTuningCalculated: 1,
292
+ currentTuningCents: 0,
293
+ releaseStartTime: Infinity,
294
+
295
+ // envelope data
296
+ finished: false,
297
+ isInRelease: false,
298
+ currentAttenuationDb: 100,
299
+ currentModEnvValue: 0,
300
+ releaseStartModEnv: 1,
301
+ volumeEnvelopeState: 0,
302
+ currentPan: 0.5
303
+ };
304
+
305
+ });
306
+ // cache the voice
307
+ if(canCache) {
308
+ // clone it so the system won't mess with it!
309
+ cachedVoices[midiNote][velocity] = workletVoices.map(deepClone);
310
+ }
311
+ }
312
+ return workletVoices;
313
+ }
@@ -0,0 +1,4 @@
1
+ ## This is the utility folder.
2
+ There are various utilites here used by the SpessaSynth library.
3
+
4
+ ### Note that the stbvorbis_sync.js is licensed under Apache-2.0.
@@ -0,0 +1,70 @@
1
+ /**
2
+ *
3
+ * @param sampleRate {number}
4
+ * @param left {Float32Array}
5
+ * @param right {Float32Array}
6
+ * @returns {ArrayBufferLike}
7
+ */
8
+ export function rawDataToWav(sampleRate, left, right)
9
+ {
10
+
11
+ const length = left.length;
12
+
13
+ const bytesPerSample = 2; // 16-bit PCM
14
+
15
+ // Prepare the header
16
+ const headerSize = 44;
17
+ const dataSize = length * 2 * bytesPerSample; // 2 channels, 16-bit per channel
18
+ const fileSize = headerSize + dataSize - 8; // total file size minus the first 8 bytes
19
+ const header = new Uint8Array(headerSize);
20
+
21
+ // 'RIFF'
22
+ header.set([82, 73, 70, 70], 0);
23
+ // file length
24
+ header.set(new Uint8Array([fileSize & 0xff, (fileSize >> 8) & 0xff, (fileSize >> 16) & 0xff, (fileSize >> 24) & 0xff]), 4);
25
+ // 'WAVE'
26
+ header.set([87, 65, 86, 69], 8);
27
+ // 'fmt '
28
+ header.set([102, 109, 116, 32], 12);
29
+ // fmt chunk length
30
+ header.set([16, 0, 0, 0], 16); // 16 for PCM
31
+ // audio format (PCM)
32
+ header.set([1, 0], 20);
33
+ // number of channels (2)
34
+ header.set([2, 0], 22);
35
+ // sample rate
36
+ header.set(new Uint8Array([sampleRate & 0xff, (sampleRate >> 8) & 0xff, (sampleRate >> 16) & 0xff, (sampleRate >> 24) & 0xff]), 24);
37
+ // byte rate (sample rate * block align)
38
+ const byteRate = sampleRate * 2 * bytesPerSample; // 2 channels, 16-bit per channel
39
+ header.set(new Uint8Array([byteRate & 0xff, (byteRate >> 8) & 0xff, (byteRate >> 16) & 0xff, (byteRate >> 24) & 0xff]), 28);
40
+ // block align (channels * bytes per sample)
41
+ header.set([4, 0], 32); // 2 channels * 16-bit per channel / 8
42
+ // bits per sample
43
+ header.set([16, 0], 34); // 16-bit
44
+
45
+ // data chunk identifier 'data'
46
+ header.set([100, 97, 116, 97], 36);
47
+ // data chunk length
48
+ header.set(new Uint8Array([dataSize & 0xff, (dataSize >> 8) & 0xff, (dataSize >> 16) & 0xff, (dataSize >> 24) & 0xff]), 40);
49
+
50
+ const wavData = new Uint8Array(headerSize + dataSize);
51
+ wavData.set(header, 0);
52
+
53
+ // Interleave audio data (combine channels)
54
+ let offset = headerSize;
55
+ for (let i = 0; i < length; i++)
56
+ {
57
+ // interleave both channels
58
+ const sample1 = Math.max(-1, Math.min(1, left[i])) * 0x7FFF;
59
+ const sample2 = Math.max(-1, Math.min(1, right[i])) * 0x7FFF;
60
+
61
+ // convert to 16-bit
62
+ wavData[offset++] = sample1 & 0xff;
63
+ wavData[offset++] = (sample1 >> 8) & 0xff;
64
+ wavData[offset++] = sample2 & 0xff;
65
+ wavData[offset++] = (sample2 >> 8) & 0xff;
66
+ }
67
+
68
+
69
+ return wavData.buffer;
70
+ }