spessasynth_core 3.26.34 → 3.26.35

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spessasynth_core",
3
- "version": "3.26.34",
3
+ "version": "3.26.35",
4
4
  "description": "MIDI and SoundFont2/DLS library with no compromises",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -20,11 +20,10 @@ export const sampleTypes = {
20
20
  };
21
21
 
22
22
  /**
23
- * @typedef {function} EncodeVorbisFunction
24
- * @param channelAudioData {Float32Array[]}
23
+ * @typedef {function} SampleEncodingFunction
24
+ * @async
25
+ * @param audioData {Float32Array}
25
26
  * @param sampleRate {number}
26
- * @param channels {number}
27
- * @param quality {number} -0.1 to 1
28
27
  * @returns {Uint8Array}
29
28
  */
30
29
 
@@ -106,7 +105,7 @@ export class BasicSample
106
105
  * Indicates if the data was overriden, so it cannot be copied back unchanged
107
106
  * @type {boolean}
108
107
  */
109
- dataOverriden = false;
108
+ dataOverriden = true;
110
109
 
111
110
  /**
112
111
  * The basic representation of a sample
@@ -163,7 +162,7 @@ export class BasicSample
163
162
  * @return {Uint8Array} either s16 or vorbis data
164
163
  * @virtual
165
164
  */
166
- getRawData(allowVorbis = true)
165
+ getRawData(allowVorbis)
167
166
  {
168
167
  if (this.isCompressed && allowVorbis && !this.dataOverriden)
169
168
  {
@@ -190,10 +189,9 @@ export class BasicSample
190
189
  }
191
190
 
192
191
  /**
193
- * @param quality {number}
194
- * @param encodeVorbis {EncodeVorbisFunction}
192
+ * @param encodeVorbis {SampleEncodingFunction}
195
193
  */
196
- compressSample(quality, encodeVorbis)
194
+ async compressSample(encodeVorbis)
197
195
  {
198
196
  // no need to compress
199
197
  if (this.isCompressed)
@@ -210,12 +208,12 @@ export class BasicSample
210
208
  this.resampleData(RESAMPLE_RATE);
211
209
  audioData = this.getAudioData();
212
210
  }
213
- const compressed = encodeVorbis([audioData], 1, this.sampleRate, quality);
211
+ const compressed = await encodeVorbis(audioData, this.sampleRate);
214
212
  this.setCompressedData(compressed);
215
213
  }
216
214
  catch (e)
217
215
  {
218
- SpessaSynthWarn(`Failed to compress ${this.sampleName}. Leaving as uncompressed!`);
216
+ SpessaSynthWarn(`Failed to compress ${this.sampleName}. Leaving as uncompressed!`, e);
219
217
  delete this.compressedData;
220
218
  // flag as uncompressed
221
219
  this.isCompressed = false;
@@ -148,9 +148,9 @@ class BasicSoundBank
148
148
 
149
149
  /**
150
150
  * Creates a simple soundfont with one saw wave preset.
151
- * @returns {ArrayBufferLike}
151
+ * @returns {Promise<ArrayBufferLike>}
152
152
  */
153
- static getDummySoundfontFile()
153
+ static async getDummySoundfontFile()
154
154
  {
155
155
  const font = new BasicSoundBank();
156
156
  const sample = new BasicSample(
@@ -162,11 +162,12 @@ class BasicSoundBank
162
162
  0,
163
163
  127
164
164
  );
165
- sample.sampleData = new Float32Array(128);
165
+ const sampleData = new Float32Array(128);
166
166
  for (let i = 0; i < 128; i++)
167
167
  {
168
- sample.sampleData[i] = (i / 128) * 2 - 1;
168
+ sampleData[i] = (i / 128) * 2 - 1;
169
169
  }
170
+ sample.setAudioData(sampleData);
170
171
  font.addSamples(sample);
171
172
 
172
173
  const gZone = new BasicGlobalZone();
@@ -201,7 +202,8 @@ class BasicSoundBank
201
202
  font.soundFontInfo["isng"] = "E-mu 10K2";
202
203
  font.soundFontInfo["INAM"] = "Dummy";
203
204
  font.flush();
204
- return font.write().buffer;
205
+ const f = await font.write();
206
+ return f.buffer;
205
207
  }
206
208
 
207
209
  /**
@@ -252,7 +254,6 @@ class BasicSoundBank
252
254
  if (sample.isCompressed)
253
255
  {
254
256
  newSample.setCompressedData(sample.compressedData.slice());
255
- console.log(sample.compressedData.length);
256
257
  }
257
258
  else
258
259
  {
@@ -6,14 +6,30 @@ import { getStringBytes } from "../../../utils/byte_functions/string.js";
6
6
  import { writeWavePool } from "./wvpl.js";
7
7
  import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from "../../../utils/loggin.js";
8
8
  import { consoleColors } from "../../../utils/other.js";
9
+ import { fillWithDefaults } from "../../../utils/fill_with_defaults.js";
10
+
11
+ /**
12
+ * @typedef {Object} DLSWriteOptions
13
+ * @property {ProgressFunction|undefined} progressFunction - a function to show progress for writing large banks. It can be undefined.
14
+ */
15
+
16
+
17
+ /**
18
+ * @type {DLSWriteOptions}
19
+ */
20
+ const DEFAULT_DLS_OPTIONS = {
21
+ progressFunction: undefined
22
+ };
9
23
 
10
24
  /**
11
25
  * Write the soundfont as a .dls file. Experimental
12
26
  * @this {BasicSoundBank}
27
+ * @param {DLSWriteOptions|undefined} options - options for writing the file.
13
28
  * @returns {Uint8Array}
14
29
  */
15
- export function writeDLS()
30
+ export async function writeDLS(options = DEFAULT_DLS_OPTIONS)
16
31
  {
32
+ options = fillWithDefaults(options, DEFAULT_DLS_OPTIONS);
17
33
  SpessaSynthGroupCollapsed(
18
34
  "%cSaving DLS...",
19
35
  consoleColors.info
@@ -40,7 +56,7 @@ export function writeDLS()
40
56
  "%cWriting WAVE samples...",
41
57
  consoleColors.info
42
58
  );
43
- const wavepool = writeWavePool.apply(this);
59
+ const wavepool = await writeWavePool.call(this, options.progressFunction);
44
60
  const wvpl = wavepool.data;
45
61
  const ptblOffsets = wavepool.indexes;
46
62
  SpessaSynthInfo("%cSucceeded!", consoleColors.recognized);
@@ -3,22 +3,27 @@ import { writeRIFFChunkParts } from "../riff_chunk.js";
3
3
 
4
4
  /**
5
5
  * @this {BasicSoundBank}
6
- * @returns {{data: IndexedByteArray, indexes: number[] }}
6
+ * @param {ProgressFunction|undefined} progressFunction
7
+ * @returns {Promise<{data: IndexedByteArray, indexes: number[] }>}
7
8
  */
8
- export function writeWavePool()
9
+ export async function writeWavePool(progressFunction)
9
10
  {
10
11
  let currentIndex = 0;
11
12
  const offsets = [];
12
13
  /**
13
14
  * @type {IndexedByteArray[]}
14
15
  */
15
- const samples = this.samples.map(s =>
16
+ const samples = [];
17
+ let written = 0;
18
+ for (const s of this.samples)
16
19
  {
17
20
  const out = writeDLSSample(s);
21
+ await progressFunction?.(s.sampleName, written, this.samples.length);
18
22
  offsets.push(currentIndex);
19
23
  currentIndex += out.length;
20
- return out;
21
- });
24
+ samples.push(out);
25
+ written++;
26
+ }
22
27
  return {
23
28
  data: writeRIFFChunkParts(
24
29
  "wvpl",
@@ -1,8 +1,8 @@
1
- import { SpessaSynthInfo } from "../../../utils/loggin.js";
2
- import { consoleColors } from "../../../utils/other.js";
3
1
  import { IndexedByteArray } from "../../../utils/indexed_array.js";
4
2
  import { writeStringAsBytes } from "../../../utils/byte_functions/string.js";
5
3
  import { writeLittleEndian } from "../../../utils/byte_functions/little_endian.js";
4
+ import { SpessaSynthInfo } from "../../../utils/loggin.js";
5
+ import { consoleColors } from "../../../utils/other.js";
6
6
 
7
7
  /*
8
8
  Sdta structure:
@@ -26,25 +26,45 @@ const SDTA_TO_DATA_OFFSET =
26
26
  * @param smplStartOffsets {number[]}
27
27
  * @param smplEndOffsets {number[]}
28
28
  * @param compress {boolean}
29
- * @param quality {number}
30
- * @param vorbisFunc {EncodeVorbisFunction}
29
+ * @param decompress {boolean}
30
+ * @param vorbisFunc {SampleEncodingFunction}
31
+ * @param progressFunc {ProgressFunction|undefined}
31
32
  * @returns {Uint8Array}
32
33
  */
33
- export function getSDTA(smplStartOffsets, smplEndOffsets, compress, quality, vorbisFunc)
34
+ export async function getSDTA(smplStartOffsets,
35
+ smplEndOffsets,
36
+ compress,
37
+ decompress,
38
+ vorbisFunc,
39
+ progressFunc
40
+ )
34
41
  {
35
42
  // write smpl: write int16 data of each sample linearly
36
43
  // get size (calling getAudioData twice doesn't matter since it gets cached)
44
+ let writtenCount = 0;
37
45
  let smplChunkSize = 0;
38
- const sampleDatas = this.samples.map((s, i) =>
46
+ const sampleDatas = [];
47
+
48
+ // linear async is faster here as the writing function usually uses a single wasm instance
49
+ for (const s of this.samples)
39
50
  {
40
51
  if (compress)
41
52
  {
42
- s.compressSample(quality, vorbisFunc);
53
+ await s.compressSample(vorbisFunc);
54
+ }
55
+ if (decompress)
56
+ {
57
+ s.setAudioData(s.getAudioData());
43
58
  }
59
+
44
60
  // raw data: either copy s16le or encoded vorbis or encode manually if overridden
45
- const r = s.getRawData();
61
+ // use set timeout so the thread doesn't die
62
+ const r = s.getRawData(true);
63
+ writtenCount++;
64
+ progressFunc?.(s.sampleName, writtenCount, this.samples.length);
65
+
46
66
  SpessaSynthInfo(
47
- `%cEncoded sample %c${i}. ${s.sampleName}%c of %c${this.samples.length}%c. Compressed: %c${s.isCompressed}%c.`,
67
+ `%cEncoded sample %c${writtenCount}. ${s.sampleName}%c of %c${this.samples.length}%c. Compressed: %c${s.isCompressed}%c.`,
48
68
  consoleColors.info,
49
69
  consoleColors.recognized,
50
70
  consoleColors.info,
@@ -53,15 +73,16 @@ export function getSDTA(smplStartOffsets, smplEndOffsets, compress, quality, vor
53
73
  s.isCompressed ? consoleColors.recognized : consoleColors.unrecognized,
54
74
  consoleColors.info
55
75
  );
76
+
56
77
  /* 6.1 Sample Data Format in the smpl Sub-chunk
57
78
  Each sample is followed by a minimum of forty-six zero
58
79
  valued sample data points. These zero valued data points are necessary to guarantee that any reasonable upward pitch shift
59
80
  using any reasonable interpolator can loop on zero data at the end of the sound.
60
81
  This doesn't apply to sf3 tho
61
82
  */
62
- smplChunkSize += r.length + (s.isCompressed ? 0 : 92); // 92 = 46 sample data points
63
- return r;
64
- });
83
+ smplChunkSize += r.length + (s.isCompressed ? 0 : 92);
84
+ sampleDatas.push(r);
85
+ }
65
86
 
66
87
  if (smplChunkSize % 2 !== 0)
67
88
  {
@@ -16,16 +16,25 @@ import { writeLittleEndian, writeWord } from "../../../utils/byte_functions/litt
16
16
  import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from "../../../utils/loggin.js";
17
17
  import { MOD_BYTE_SIZE } from "../modulator.js";
18
18
  import { fillWithDefaults } from "../../../utils/fill_with_defaults.js";
19
+
20
+ /**
21
+ * @typedef {function} ProgressFunction
22
+ * @param {string} sampleName - the written sample name.
23
+ * @param {number} sampleIndex - the sample's index.
24
+ * @param {number} sampleCount - the total sample count for progress displaying.
25
+ */
26
+
19
27
  /**
20
28
  * @typedef {Object} SoundFont2WriteOptions
21
- * @property {boolean|undefined} compress - if the soundfont should be compressed with the Ogg Vorbis codec
22
- * @property {number|undefined} compressionQuality - the vorbis compression quality, from -0.1 to 1
23
- * @property {EncodeVorbisFunction|undefined} compressionFunction -
24
- * the encode vorbis function. Can be undefined if not compressed.
29
+ * @property {boolean|undefined} compress - if the soundfont should be compressed with a given function.
30
+ * @property {SampleEncodingFunction|undefined} compressionFunction -
31
+ * the encode vorbis function. It can be undefined if not compressed.
32
+ * @property {ProgressFunction|undefined} progressFunction - a function to show progress for writing large banks. It can be undefined.
25
33
  * @property {boolean|undefined} writeDefaultModulators - if the DMOD chunk should be written.
26
34
  * Recommended.
27
35
  * @property {boolean|undefined} writeExtendedLimits - if the xdta chunk should be written to allow virtually infinite parameters.
28
36
  * Recommended.
37
+ * @property {boolean|undefined} decompress - if an sf3 bank should be decompressed back to sf2. Not recommended.
29
38
  */
30
39
 
31
40
 
@@ -43,8 +52,10 @@ const DEFAULT_WRITE_OPTIONS = {
43
52
  compress: false,
44
53
  compressionQuality: 0.5,
45
54
  compressionFunction: undefined,
55
+ progressFunction: undefined,
46
56
  writeDefaultModulators: true,
47
- writeExtendedLimits: true
57
+ writeExtendedLimits: true,
58
+ decompress: false
48
59
  };
49
60
 
50
61
  /**
@@ -53,14 +64,18 @@ const DEFAULT_WRITE_OPTIONS = {
53
64
  * @param {SoundFont2WriteOptions} options
54
65
  * @returns {Uint8Array}
55
66
  */
56
- export function write(options = DEFAULT_WRITE_OPTIONS)
67
+ export async function write(options = DEFAULT_WRITE_OPTIONS)
57
68
  {
58
69
  options = fillWithDefaults(options, DEFAULT_WRITE_OPTIONS);
59
- if (options.compress)
70
+ if (options?.compress)
60
71
  {
61
- if (typeof options.compressionFunction !== "function")
72
+ if (typeof options?.compressionFunction !== "function")
62
73
  {
63
- throw new TypeError("No compression function supplied but compression enabled.");
74
+ throw new Error("No compression function supplied but compression enabled.");
75
+ }
76
+ if (options?.decompress)
77
+ {
78
+ throw new Error("Decompressed and compressed at the same time.");
64
79
  }
65
80
  }
66
81
  SpessaSynthGroupCollapsed(
@@ -88,6 +103,10 @@ export function write(options = DEFAULT_WRITE_OPTIONS)
88
103
  {
89
104
  this.soundFontInfo["ifil"] = "3.0"; // set version to 3
90
105
  }
106
+ if (options?.decompress)
107
+ {
108
+ this.soundFontInfo["ifil"] = "2.4"; // set version to 2.04
109
+ }
91
110
 
92
111
  if (options?.writeDefaultModulators)
93
112
  {
@@ -152,13 +171,14 @@ export function write(options = DEFAULT_WRITE_OPTIONS)
152
171
  // write sdta
153
172
  const smplStartOffsets = [];
154
173
  const smplEndOffsets = [];
155
- const sdtaChunk = getSDTA.call(
174
+ const sdtaChunk = await getSDTA.call(
156
175
  this,
157
176
  smplStartOffsets,
158
177
  smplEndOffsets,
159
- options?.compress,
160
- options?.compressionQuality ?? 0.5,
161
- options.compressionFunction
178
+ options.compress,
179
+ options.decompress,
180
+ options?.compressionFunction,
181
+ options?.progressFunction
162
182
  );
163
183
 
164
184
  SpessaSynthInfo(
@@ -175,6 +175,7 @@ export class DLSSample extends BasicSample
175
175
  loopEnd
176
176
  );
177
177
  this.sampleDbAttenuation = sampleDbAttenuation;
178
+ this.dataOverriden = false;
178
179
  /**
179
180
  * @type {IndexedByteArray}
180
181
  */
@@ -221,11 +222,11 @@ export class DLSSample extends BasicSample
221
222
  super.setAudioData(audioData);
222
223
  }
223
224
 
224
- getRawData(allowVorbis = true)
225
+ getRawData(allowVorbis)
225
226
  {
226
227
  if (this.dataOverriden || this.isCompressed)
227
228
  {
228
- return super.getRawData();
229
+ return super.getRawData(allowVorbis);
229
230
  }
230
231
  if (this.wFormatTag === W_FORMAT_TAG.PCM && this.bytesPerSample === 2)
231
232
  {
@@ -22,21 +22,10 @@ export class SoundFontSample extends BasicSample
22
22
  linkedSampleIndex;
23
23
 
24
24
  /**
25
- * The handle to the core sf2 file for dynamic sample reading
25
+ * The sliced sample from the smpl chunk
26
26
  * @type {Uint8Array}
27
27
  */
28
- sf2FileArrayHandle;
29
-
30
- /**
31
- * Start index of the sample in the file byte array
32
- * @type {number}
33
- */
34
- s16leStart = 0;
35
- /**
36
- * End index of the sample in the file byte array
37
- * @type {number}
38
- */
39
- s16leEnd = 0;
28
+ s16leData;
40
29
 
41
30
  /**
42
31
  * Creates a sample
@@ -83,6 +72,7 @@ export class SoundFontSample extends BasicSample
83
72
  sampleLoopStartIndex - (sampleStartIndex / 2),
84
73
  sampleLoopEndIndex - (sampleStartIndex / 2)
85
74
  );
75
+ this.dataOverriden = false;
86
76
  this.isCompressed = compressed;
87
77
  this.sampleName = sampleName;
88
78
  // in bytes
@@ -116,13 +106,15 @@ export class SoundFontSample extends BasicSample
116
106
  this.startByteOffset / 2,
117
107
  this.endByteOffset / 2
118
108
  );
109
+ this.dataOverriden = true;
119
110
  }
120
111
  else
121
112
  {
122
113
  // regular sf2 s16le
123
- this.s16leStart = smplStart + this.startByteOffset;
124
- this.s16leEnd = smplStart + this.endByteOffset;
125
- this.sf2FileArrayHandle = sampleDataArray;
114
+ this.s16leData = sampleDataArray.slice(
115
+ smplStart + this.startByteOffset,
116
+ smplStart + this.endByteOffset
117
+ );
126
118
  }
127
119
 
128
120
  }
@@ -189,7 +181,7 @@ export class SoundFontSample extends BasicSample
189
181
  // read the sample data
190
182
  let audioData = new Float32Array(byteLength / 2);
191
183
  let convertedSigned16 = new Int16Array(
192
- this.sf2FileArrayHandle.buffer.slice(this.s16leStart, this.s16leEnd)
184
+ this.s16leData.buffer
193
185
  );
194
186
 
195
187
  // convert to float
@@ -207,15 +199,15 @@ export class SoundFontSample extends BasicSample
207
199
  * @param allowVorbis
208
200
  * @returns {Uint8Array}
209
201
  */
210
- getRawData(allowVorbis = true)
202
+ getRawData(allowVorbis)
211
203
  {
212
204
  if (this.dataOverriden || this.compressedData)
213
205
  {
214
206
  // return vorbis or encode manually
215
- return super.getRawData();
207
+ return super.getRawData(allowVorbis);
216
208
  }
217
209
  // copy the smpl directly
218
- return this.sf2FileArrayHandle.slice(this.s16leStart, this.s16leEnd);
210
+ return this.s16leData;
219
211
  }
220
212
  }
221
213