spessasynth_lib 3.15.0 → 3.16.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 (32) hide show
  1. package/@types/midi_parser/used_keys_loaded.d.ts +4 -2
  2. package/@types/soundfont/read/presets.d.ts +1 -0
  3. package/@types/soundfont/soundfont.d.ts +12 -0
  4. package/@types/synthetizer/synth_soundfont_manager.d.ts +52 -0
  5. package/@types/synthetizer/synthetizer.d.ts +13 -1
  6. package/@types/synthetizer/worklet_system/message_protocol/worklet_message.d.ts +2 -2
  7. package/@types/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/sfman_message.d.ts +7 -0
  8. package/README.md +1 -0
  9. package/midi_parser/midi_loader.js +1 -1
  10. package/midi_parser/used_keys_loaded.js +1 -1
  11. package/package.json +1 -1
  12. package/sequencer/worklet_sequencer/song_control.js +2 -2
  13. package/soundfont/read/presets.js +2 -0
  14. package/soundfont/soundfont.js +25 -0
  15. package/synthetizer/synth_soundfont_manager.js +111 -0
  16. package/synthetizer/synthetizer.js +19 -12
  17. package/synthetizer/worklet_processor.min.js +6 -6
  18. package/synthetizer/worklet_system/main_processor.js +132 -9
  19. package/synthetizer/worklet_system/message_protocol/handle_message.js +6 -5
  20. package/synthetizer/worklet_system/message_protocol/message_sending.js +0 -13
  21. package/synthetizer/worklet_system/message_protocol/worklet_message.js +4 -3
  22. package/synthetizer/worklet_system/worklet_methods/note_off.js +5 -4
  23. package/synthetizer/worklet_system/worklet_methods/note_on.js +0 -1
  24. package/synthetizer/worklet_system/worklet_methods/program_control.js +11 -16
  25. package/synthetizer/worklet_system/worklet_methods/voice_control.js +20 -12
  26. package/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/sfman_message.js +9 -0
  27. package/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/worklet_soundfont_manager.js +231 -0
  28. package/synthetizer/worklet_system/worklet_processor.js +1 -1
  29. package/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +5 -5
  30. package/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +1 -3
  31. package/utils/buffer_to_wav.js +42 -38
  32. package/synthetizer/worklet_system/combine_class.js +0 -104
@@ -0,0 +1,231 @@
1
+ import { SoundFont2 } from '../../../../soundfont/soundfont.js'
2
+ import { SpessaSynthWarn } from '../../../../utils/loggin.js'
3
+ import { WorkletSoundfontManagerMessageType } from './sfman_message.js'
4
+
5
+ /**
6
+ * @typedef {Object} SoundFontType
7
+ * @property {string} id - unique id for the soundfont
8
+ * @property {SoundFont2} soundfont - the soundfont itself
9
+ * @property {number} bankOffset - the soundfont's bank offset
10
+ */
11
+
12
+ export class WorkletSoundfontManager
13
+ {
14
+ /**
15
+ * Creates a new instance of worklet soundfont manager (worklet scope)
16
+ * @param initialSoundFontBuffer {ArrayBuffer} Array buffer of the soundfont. This soudfont always has the id "main"
17
+ * @param readyCallback {function} postReady() method from synth
18
+ */
19
+ constructor(initialSoundFontBuffer, readyCallback)
20
+ {
21
+ /**
22
+ * @type {Function}
23
+ */
24
+ this.ready = readyCallback;
25
+ this.totalSoundfontOffset = 0;
26
+ this.reloadManager(initialSoundFontBuffer);
27
+ }
28
+
29
+ _assingSampleOffsets()
30
+ {
31
+ let offset = 0;
32
+ this.soundfontList.forEach(s => {
33
+ s.soundfont.setSampleIDOffset(offset);
34
+ offset += s.soundfont.samples.length
35
+ });
36
+ this.totalSoundfontOffset = offset;
37
+ }
38
+
39
+ generatePresetList()
40
+ {
41
+ this._assingSampleOffsets();
42
+ /**
43
+ * <"bank-program", "presetName">
44
+ * @type {Object<string, string>}
45
+ */
46
+ const presetList = {};
47
+ // gather the presets in reverse and replace if necessary
48
+ for (let i = this.soundfontList.length - 1; i >= 0; i--)
49
+ {
50
+ const font = this.soundfontList[i];
51
+ for(const p of font.soundfont.presets)
52
+ {
53
+ const presetString = `${p.bank + font.bankOffset}-${p.program}`;
54
+ presetList[presetString] = p.presetName;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * @type {{bank: number, presetName: string, program: number}[]}
60
+ */
61
+ this.presetList = [];
62
+ for(const [string, name] of Object.entries(presetList))
63
+ {
64
+ const pb = string.split("-");
65
+ this.presetList.push({
66
+ presetName: name,
67
+ program: parseInt(pb[1]),
68
+ bank: parseInt(pb[0])
69
+ })
70
+ }
71
+ }
72
+
73
+ /**
74
+ * @param type {WorkletSoundfontManagerMessageType}
75
+ * @param data {any}
76
+ */
77
+ handleMessage(type, data)
78
+ {
79
+ switch (type)
80
+ {
81
+ case WorkletSoundfontManagerMessageType.addNewSoundFont:
82
+ this.addNewSoundFont(data[0], data[1], data[2]);
83
+ break;
84
+
85
+ case WorkletSoundfontManagerMessageType.reloadSoundFont:
86
+ this.reloadManager(data);
87
+ break;
88
+
89
+ case WorkletSoundfontManagerMessageType.deleteSoundFont:
90
+ this.deleteSoundFont(data);
91
+ break;
92
+
93
+ case WorkletSoundfontManagerMessageType.rearrangeSoundFonts:
94
+ this.rearrangeSoundFonts(data);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Get the final preset list
100
+ * @returns {{bank: number, presetName: string, program: number}[]}
101
+ */
102
+ getPresetList()
103
+ {
104
+ return this.presetList;
105
+ }
106
+
107
+ /**
108
+ * Clears all soundfonts and adds a new one
109
+ * @param soundFontArrayBuffer {ArrayBuffer}
110
+ */
111
+ reloadManager(soundFontArrayBuffer)
112
+ {
113
+ const font = new SoundFont2(soundFontArrayBuffer);
114
+ /**
115
+ * All the soundfonts, ordered from the most important to the least.
116
+ * @type {SoundFontType[]}
117
+ */
118
+ this.soundfontList = [];
119
+ this.soundfontList.push({
120
+ id: "main",
121
+ bankOffset: 0,
122
+ soundfont: font
123
+ });
124
+ this.generatePresetList();
125
+ this.ready();
126
+ }
127
+
128
+ deleteSoundFont(id)
129
+ {
130
+ if(this.soundfontList.length === 0)
131
+ {
132
+ SpessaSynthWarn("1 soundfont left. Aborting!");
133
+ return;
134
+ }
135
+ const index = this.soundfontList.findIndex(s => s.id === id);
136
+ if(index === -1)
137
+ {
138
+ SpessaSynthWarn(`No soundfont with id of "${id}" found. Aborting!`);
139
+ return;
140
+ }
141
+ delete this.soundfontList[index].soundfont.presets;
142
+ delete this.soundfontList[index].soundfont.instruments;
143
+ delete this.soundfontList[index].soundfont.samples;
144
+ this.soundfontList.splice(index, 1);
145
+ this.generatePresetList();
146
+ }
147
+
148
+ /**
149
+ * Adds a new soundfont buffer with a given ID
150
+ * @param buffer {ArrayBuffer}
151
+ * @param id {string}
152
+ * @param bankOffset {number}
153
+ */
154
+ addNewSoundFont(buffer, id, bankOffset)
155
+ {
156
+ if(this.soundfontList.find(s => s.id === id) !== undefined)
157
+ {
158
+ throw new Error("Cannot overwrite the existing soundfont. Use soundfontManager.delete(id) instead.");
159
+ }
160
+ this.soundfontList.push({
161
+ id: id,
162
+ soundfont: new SoundFont2(buffer),
163
+ bankOffset: bankOffset
164
+ });
165
+ this.generatePresetList();
166
+ this.ready();
167
+ }
168
+
169
+ /**
170
+ * Rearranges the soundfonts
171
+ * @param newList {string[]} the order of soundfonts, a list of strings, first overwrites second
172
+ */
173
+ rearrangeSoundFonts(newList)
174
+ {
175
+ this.soundfontList.sort((a, b) =>
176
+ newList.indexOf(a.id) - newList.indexOf(b.id)
177
+ );
178
+ this.generatePresetList();
179
+ }
180
+
181
+ /**
182
+ * Gets a given preset from the soundfont stack
183
+ * @param bankNumber {number}
184
+ * @param programNumber {number}
185
+ * @returns {Preset} the preset
186
+ */
187
+ getPreset(bankNumber, programNumber)
188
+ {
189
+ if(this.soundfontList.length < 1)
190
+ {
191
+ throw new Error("No soundfonts! This should never happen.");
192
+ }
193
+ for(const sf of this.soundfontList)
194
+ {
195
+ // check for the preset (with given offset)
196
+ const preset = sf.soundfont.getPresetNoFallback(bankNumber - sf.bankOffset, programNumber);
197
+ if(preset !== undefined)
198
+ {
199
+ return preset;
200
+ }
201
+ // if not found, advance to the next soundfont
202
+ }
203
+ // if none found, return the first correct preset found
204
+ if(bankNumber !== 128)
205
+ {
206
+ for(const sf of this.soundfontList)
207
+ {
208
+ const preset = sf.soundfont.presets.find(p => p.program === programNumber);
209
+ if(preset)
210
+ {
211
+ return preset;
212
+ }
213
+ }
214
+ // if nothing at all, use the first preset
215
+ return this.soundfontList[0].soundfont.presets[0];
216
+ }
217
+ else
218
+ {
219
+ for(const sf of this.soundfontList)
220
+ {
221
+ const preset = sf.soundfont.presets.find(p => p.bank === 128);
222
+ if(preset)
223
+ {
224
+ return preset;
225
+ }
226
+ }
227
+ // if nothing at all, use the first preset
228
+ return this.soundfontList[0].soundfont.presets[0];
229
+ }
230
+ }
231
+ }
@@ -1,6 +1,6 @@
1
1
  import { WORKLET_PROCESSOR_NAME } from '../synthetizer.js'
2
2
  import { consoleColors } from '../../utils/other.js'
3
- import { SpessaSynthProcessor } from './combine_class.js'
3
+ import { SpessaSynthProcessor } from './main_processor.js'
4
4
  import { SpessaSynthInfo } from '../../utils/loggin.js'
5
5
 
6
6
 
@@ -109,11 +109,11 @@ export const dataEntryStates = {
109
109
 
110
110
 
111
111
  export const customControllers = {
112
- channelTuning: 0, // cents, RPN for fine tuning
113
- channelTransposeFine: 1, // cents, only the decimal tuning, (e.g. transpose is 4.5, then shift by 4 keys + tune by 50 cents)
114
- modulationMultiplier: 2, // cents, set by moduldation depth RPN
115
- masterTuning: 3, // cents, set by system exclusive
116
- channelTuningSemitones: 4, // semitones, for RPN coarse tuning
112
+ channelTuning: 0, // cents, RPN for fine tuning
113
+ channelTransposeFine: 1, // cents, only the decimal tuning, (e.g. transpose is 4.5, then shift by 4 keys + tune by 50 cents)
114
+ modulationMultiplier: 2, // cents, set by moduldation depth RPN
115
+ masterTuning: 3, // cents, set by system exclusive
116
+ channelTuningSemitones: 4, // semitones, for RPN coarse tuning
117
117
  }
118
118
  export const CUSTOM_CONTROLLER_TABLE_SIZE = Object.keys(customControllers).length;
119
119
  export const customResetArray = new Float32Array(CUSTOM_CONTROLLER_TABLE_SIZE);
@@ -126,7 +126,6 @@ function deepClone(obj) {
126
126
  * @param sampleRate {number}
127
127
  * @param sampleDumpCallback {function({channel: number, sampleID: number, sampleData: Float32Array})}
128
128
  * @param cachedVoices {WorkletVoice[][][]} first is midi note, second is velocity. output is an array of WorkletVoices
129
- * @param sampleIDOffset {number}
130
129
  * @param debug {boolean}
131
130
  * @returns {WorkletVoice[]}
132
131
  */
@@ -138,7 +137,6 @@ export function getWorkletVoices(channel,
138
137
  sampleRate,
139
138
  sampleDumpCallback,
140
139
  cachedVoices,
141
- sampleIDOffset,
142
140
  debug=false)
143
141
  {
144
142
  /**
@@ -161,7 +159,7 @@ export function getWorkletVoices(channel,
161
159
  */
162
160
  workletVoices = preset.getSamplesAndGenerators(midiNote, velocity).reduce((voices, sampleAndGenerators) => {
163
161
  // dump the sample if haven't already
164
- const sampleID = sampleAndGenerators.sampleID + sampleIDOffset;
162
+ const sampleID = sampleAndGenerators.sampleID + preset.sampleIDOffset;
165
163
  if (globalDumpedSamplesList[sampleID] !== true)
166
164
  {
167
165
  dumpSample(channel, sampleAndGenerators.sample, sampleID, sampleDumpCallback);
@@ -6,7 +6,7 @@
6
6
  * @property {string|undefined} genre - the song's genre
7
7
  */
8
8
 
9
- import { combineArrays } from './indexed_array.js'
9
+ import { combineArrays, IndexedByteArray } from './indexed_array.js'
10
10
  import { getStringBytes } from './byte_functions/string.js'
11
11
  import { writeRIFFOddSize } from '../soundfont/read/riff_chunk.js'
12
12
 
@@ -20,17 +20,54 @@ import { writeRIFFOddSize } from '../soundfont/read/riff_chunk.js'
20
20
  */
21
21
  export function audioBufferToWav(audioBuffer, normalizeAudio = true, channelOffset = 0, metadata = {})
22
22
  {
23
- // this code currently doesn't add any metadata
24
23
  const channel1Data = audioBuffer.getChannelData(channelOffset);
25
24
  const channel2Data = audioBuffer.getChannelData(channelOffset + 1);
26
25
  const length = channel1Data.length;
27
26
 
28
27
  const bytesPerSample = 2; // 16-bit PCM
29
28
 
29
+ // prepare INFO chunk
30
+ let infoChunk = new IndexedByteArray(0);
31
+ const infoOn = Object.keys(metadata).length > 0;
32
+ // INFO chunk
33
+ if(infoOn)
34
+ {
35
+ const encoder = new TextEncoder();
36
+ const infoChunks = [
37
+ getStringBytes("INFO"),
38
+ writeRIFFOddSize("ICMT", encoder.encode("Created with SpessaSynth"))
39
+ ];
40
+ if(metadata.artist)
41
+ {
42
+ infoChunks.push(
43
+ writeRIFFOddSize("IART", encoder.encode(metadata.artist))
44
+ );
45
+ }
46
+ if(metadata.album)
47
+ {
48
+ infoChunks.push(
49
+ writeRIFFOddSize("IPRD", encoder.encode(metadata.album))
50
+ );
51
+ }
52
+ if(metadata.genre)
53
+ {
54
+ infoChunks.push(
55
+ writeRIFFOddSize("IGNR", encoder.encode(metadata.genre))
56
+ );
57
+ }
58
+ if(metadata.title)
59
+ {
60
+ infoChunks.push(
61
+ writeRIFFOddSize("INAM", encoder.encode(metadata.title))
62
+ );
63
+ }
64
+ infoChunk = writeRIFFOddSize("LIST", combineArrays(infoChunks));
65
+ }
66
+
30
67
  // Prepare the header
31
68
  const headerSize = 44;
32
69
  const dataSize = length * 2 * bytesPerSample; // 2 channels, 16-bit per channel
33
- const fileSize = headerSize + dataSize - 8; // total file size minus the first 8 bytes
70
+ const fileSize = headerSize + dataSize + infoChunk.length - 8; // total file size minus the first 8 bytes
34
71
  const header = new Uint8Array(headerSize);
35
72
 
36
73
  // 'RIFF'
@@ -65,46 +102,13 @@ export function audioBufferToWav(audioBuffer, normalizeAudio = true, channelOffs
65
102
 
66
103
  let wavData;
67
104
  let offset = headerSize;
68
- let infoChunk = undefined;
69
- // INFO chunk
70
- if(Object.keys(metadata).length > 0)
105
+ if(infoOn)
71
106
  {
72
- const encoder = new TextEncoder();
73
- const infoChunks = [
74
- getStringBytes("INFO"),
75
- writeRIFFOddSize("ICMT", encoder.encode("Created with SpessaSynth"))
76
- ];
77
- if(metadata.artist)
78
- {
79
- infoChunks.push(
80
- writeRIFFOddSize("IART", encoder.encode(metadata.artist))
81
- );
82
- }
83
- if(metadata.album)
84
- {
85
- infoChunks.push(
86
- writeRIFFOddSize("IPRD", encoder.encode(metadata.album))
87
- );
88
- }
89
- if(metadata.genre)
90
- {
91
- infoChunks.push(
92
- writeRIFFOddSize("IGNR", encoder.encode(metadata.genre))
93
- );
94
- }
95
- if(metadata.title)
96
- {
97
- infoChunks.push(
98
- writeRIFFOddSize("INAM", encoder.encode(metadata.title))
99
- );
100
- }
101
- infoChunk = writeRIFFOddSize("LIST", combineArrays(infoChunks));
102
107
  wavData = new Uint8Array(headerSize + dataSize + infoChunk.length);
103
108
  }
104
109
  else
105
110
  {
106
111
  wavData = new Uint8Array(headerSize + dataSize);
107
-
108
112
  }
109
113
  wavData.set(header, 0);
110
114
 
@@ -150,7 +154,7 @@ export function audioBufferToWav(audioBuffer, normalizeAudio = true, channelOffs
150
154
  wavData[offset++] = (sample2 >> 8) & 0xff;
151
155
  }
152
156
 
153
- if(infoChunk)
157
+ if(infoOn)
154
158
  {
155
159
  wavData.set(infoChunk, offset);
156
160
  }
@@ -1,104 +0,0 @@
1
- import { SpessaSynthProcessor } from './main_processor.js'
2
- import { releaseVoice, renderVoice, voiceKilling } from './worklet_methods/voice_control.js'
3
- import { handleMessage } from './message_protocol/handle_message.js'
4
- import { callEvent, post, sendChannelProperties } from './message_protocol/message_sending.js'
5
- import { systemExclusive } from './worklet_methods/system_exclusive.js'
6
- import { noteOn } from './worklet_methods/note_on.js'
7
- import { killNote, noteOff, stopAll, stopAllChannels } from './worklet_methods/note_off.js'
8
- import {
9
- channelPressure, pitchWheel,
10
- polyPressure, setChannelTuning, setChannelTuningSemitones, setMasterTuning, setModulationDepth, setOctaveTuning,
11
- transposeAllChannels,
12
- transposeChannel,
13
- } from './worklet_methods/tuning_control.js'
14
- import {
15
- controllerChange,
16
- muteChannel,
17
- setMasterGain,
18
- setMasterPan,
19
- setMIDIVolume,
20
- } from './worklet_methods/controller_control.js'
21
- import { disableAndLockVibrato, setVibrato } from './worklet_methods/vibrato_control.js'
22
- import { dataEntryCoarse, dataEntryFine } from './worklet_methods/data_entry.js'
23
- import { createWorkletChannel } from './worklet_utilities/worklet_processor_channel.js'
24
- import { resetAllControllers, resetControllers, resetParameters } from './worklet_methods/reset_controllers.js'
25
- import {
26
- clearSoundFont,
27
- getPreset,
28
- programChange,
29
- reloadSoundFont, sampleDump, sendPresetList,
30
- setDrums,
31
- setPreset,
32
- } from './worklet_methods/program_control.js'
33
- import { applySynthesizerSnapshot, sendSynthesizerSnapshot } from './worklet_methods/snapshot.js'
34
-
35
- // include other methods
36
- // voice related
37
- SpessaSynthProcessor.prototype.renderVoice = renderVoice;
38
- SpessaSynthProcessor.prototype.releaseVoice = releaseVoice;
39
- SpessaSynthProcessor.prototype.voiceKilling = voiceKilling;
40
-
41
- // message port related
42
- SpessaSynthProcessor.prototype.handleMessage = handleMessage;
43
- SpessaSynthProcessor.prototype.post = post;
44
- SpessaSynthProcessor.prototype.sendChannelProperties = sendChannelProperties;
45
- SpessaSynthProcessor.prototype.callEvent = callEvent;
46
-
47
- // system exlcusive related
48
- SpessaSynthProcessor.prototype.systemExclusive = systemExclusive;
49
-
50
- // note messages related
51
- SpessaSynthProcessor.prototype.noteOn = noteOn;
52
- SpessaSynthProcessor.prototype.noteOff = noteOff;
53
- SpessaSynthProcessor.prototype.polyPressure = polyPressure;
54
- SpessaSynthProcessor.prototype.killNote = killNote;
55
- SpessaSynthProcessor.prototype.stopAll = stopAll;
56
- SpessaSynthProcessor.prototype.stopAllChannels = stopAllChannels;
57
- SpessaSynthProcessor.prototype.muteChannel = muteChannel;
58
-
59
- // custom vibrato related
60
- SpessaSynthProcessor.prototype.setVibrato = setVibrato;
61
- SpessaSynthProcessor.prototype.disableAndLockVibrato = disableAndLockVibrato;
62
-
63
- // data entry related
64
- SpessaSynthProcessor.prototype.dataEntryCoarse = dataEntryCoarse;
65
- SpessaSynthProcessor.prototype.dataEntryFine = dataEntryFine;
66
-
67
- // channel related
68
- SpessaSynthProcessor.prototype.createWorkletChannel = createWorkletChannel;
69
- SpessaSynthProcessor.prototype.controllerChange = controllerChange;
70
- SpessaSynthProcessor.prototype.channelPressure = channelPressure;
71
- SpessaSynthProcessor.prototype.resetAllControllers = resetAllControllers;
72
- SpessaSynthProcessor.prototype.resetControllers = resetControllers;
73
- SpessaSynthProcessor.prototype.resetParameters = resetParameters;
74
-
75
- // master parameter related
76
- SpessaSynthProcessor.prototype.setMasterGain = setMasterGain;
77
- SpessaSynthProcessor.prototype.setMasterPan = setMasterPan;
78
- SpessaSynthProcessor.prototype.setMIDIVolume = setMIDIVolume;
79
-
80
- // tuning related
81
- SpessaSynthProcessor.prototype.transposeAllChannels = transposeAllChannels;
82
- SpessaSynthProcessor.prototype.transposeChannel = transposeChannel;
83
- SpessaSynthProcessor.prototype.setChannelTuning = setChannelTuning;
84
- SpessaSynthProcessor.prototype.setChannelTuningSemitones = setChannelTuningSemitones;
85
- SpessaSynthProcessor.prototype.setMasterTuning = setMasterTuning;
86
- SpessaSynthProcessor.prototype.setModulationDepth = setModulationDepth;
87
- SpessaSynthProcessor.prototype.pitchWheel = pitchWheel;
88
- SpessaSynthProcessor.prototype.setOctaveTuning = setOctaveTuning;
89
-
90
- // program related
91
- SpessaSynthProcessor.prototype.programChange = programChange;
92
- SpessaSynthProcessor.prototype.getPreset = getPreset;
93
- SpessaSynthProcessor.prototype.setPreset = setPreset;
94
- SpessaSynthProcessor.prototype.setDrums = setDrums;
95
- SpessaSynthProcessor.prototype.reloadSoundFont = reloadSoundFont;
96
- SpessaSynthProcessor.prototype.clearSoundFont = clearSoundFont;
97
- SpessaSynthProcessor.prototype.sampleDump = sampleDump;
98
- SpessaSynthProcessor.prototype.sendPresetList = sendPresetList;
99
-
100
- // snapshot related
101
- SpessaSynthProcessor.prototype.sendSynthesizerSnapshot = sendSynthesizerSnapshot;
102
- SpessaSynthProcessor.prototype.applySynthesizerSnapshot = applySynthesizerSnapshot;
103
-
104
- export {SpessaSynthProcessor}