spessasynth_lib 3.12.3 → 3.13.1

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/@types/index.d.ts CHANGED
@@ -5,6 +5,7 @@ import { VOICE_CAP } from './synthetizer/synthetizer.js';
5
5
  import { SoundFont2 } from './soundfont/soundfont.js';
6
6
  import { trimSoundfont } from './soundfont/write/soundfont_trimmer.js';
7
7
  import { modulatorSources } from './soundfont/read/modulators.js';
8
+ import { encodeVorbis } from "./utils/encode_vorbis.js";
8
9
  import { MIDI } from './midi_parser/midi_loader.js';
9
10
  import { IndexedByteArray } from './utils/indexed_array.js';
10
11
  import { writeMIDIFile } from './midi_parser/midi_writer.js';
@@ -31,4 +32,4 @@ import { readBytesAsUintBigEndian } from './utils/byte_functions/big_endian.js';
31
32
  import { NON_CC_INDEX_OFFSET } from './synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js';
32
33
  import { ALL_CHANNELS_OR_DIFFERENT_ACTION } from './synthetizer/worklet_system/message_protocol/worklet_message.js';
33
34
  import { WORKLET_URL_ABSOLUTE } from './synthetizer/worklet_url.js';
34
- export { Sequencer, Synthetizer, DEFAULT_PERCUSSION, VOICE_CAP, SoundFont2, trimSoundfont, modulatorSources, OggVorbisEncoder, MIDI, IndexedByteArray, writeMIDIFile, writeRMIDI, applySnapshotToMIDI, modifyMIDI, audioBufferToWav, SpessaSynthLogging, SpessaSynthGroup, SpessaSynthTable, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn, SpessaSynthGroupCollapsed, midiControllers, messageTypes, MIDIDeviceHandler, WebMidiLinkHandler, arrayToHexString, consoleColors, formatTitle, formatTime, readBytesAsUintBigEndian, NON_CC_INDEX_OFFSET, ALL_CHANNELS_OR_DIFFERENT_ACTION, WORKLET_URL_ABSOLUTE };
35
+ export { Sequencer, Synthetizer, DEFAULT_PERCUSSION, VOICE_CAP, SoundFont2, trimSoundfont, modulatorSources, encodeVorbis, MIDI, IndexedByteArray, writeMIDIFile, writeRMIDI, applySnapshotToMIDI, modifyMIDI, audioBufferToWav, SpessaSynthLogging, SpessaSynthGroup, SpessaSynthTable, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn, SpessaSynthGroupCollapsed, midiControllers, messageTypes, MIDIDeviceHandler, WebMidiLinkHandler, arrayToHexString, consoleColors, formatTitle, formatTime, readBytesAsUintBigEndian, NON_CC_INDEX_OFFSET, ALL_CHANNELS_OR_DIFFERENT_ACTION, WORKLET_URL_ABSOLUTE };
@@ -1,28 +1,27 @@
1
- /**
2
- * @typedef {Object} WaveMetadata
3
- * @property {string} title - the song's title
4
- * @property {string} album - the song's album
5
- * @property {string} genre - the song's genre
6
- */
7
1
  /**
8
2
  *
9
3
  * @param audioBuffer {AudioBuffer}
10
4
  * @param normalizeAudio {boolean} find the max sample point and set it to 1, and scale others with it
11
5
  * @param channelOffset {number} channel offset and channel offset + 1 get saved
6
+ * @param metadata {WaveMetadata}
12
7
  * @returns {Blob}
13
8
  */
14
- export function audioBufferToWav(audioBuffer: AudioBuffer, normalizeAudio?: boolean, channelOffset?: number): Blob;
9
+ export function audioBufferToWav(audioBuffer: AudioBuffer, normalizeAudio?: boolean, channelOffset?: number, metadata?: WaveMetadata): Blob;
15
10
  export type WaveMetadata = {
16
11
  /**
17
12
  * - the song's title
18
13
  */
19
- title: string;
14
+ title: string | undefined;
15
+ /**
16
+ * - the song's artist
17
+ */
18
+ artist: string | undefined;
20
19
  /**
21
20
  * - the song's album
22
21
  */
23
- album: string;
22
+ album: string | undefined;
24
23
  /**
25
24
  * - the song's genre
26
25
  */
27
- genre: string;
26
+ genre: string | undefined;
28
27
  };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @typedef {function} EncodeVorbisFunction
3
+ * @param channelAudioData {Float32Array[]}
4
+ * @param sampleRate {number}
5
+ * @param channels {number}
6
+ * @param quality {number} -0.1 to 1
7
+ * @returns {Uint8Array}
8
+ */
9
+ export function encodeVorbis(channelAudioData: Float32Array[], channels: number, sampleRate: number, quality: number): Uint8Array;
10
+ export type EncodeVorbisFunction = Function;
package/index.js CHANGED
@@ -26,8 +26,8 @@ import { NON_CC_INDEX_OFFSET } from './synthetizer/worklet_system/worklet_utilit
26
26
  import { modulatorSources } from './soundfont/read/modulators.js';
27
27
  import { ALL_CHANNELS_OR_DIFFERENT_ACTION } from './synthetizer/worklet_system/message_protocol/worklet_message.js';
28
28
  import { trimSoundfont } from './soundfont/write/soundfont_trimmer.js';
29
- import { OggVorbisEncoder } from './externals/libvorbis/OggVorbisEncoder.min.js';
30
29
  import { WORKLET_URL_ABSOLUTE } from './synthetizer/worklet_url.js'
30
+ import { encodeVorbis} from "./utils/encode_vorbis.js";
31
31
 
32
32
  // Export modules
33
33
  export {
@@ -41,7 +41,7 @@ export {
41
41
  SoundFont2,
42
42
  trimSoundfont,
43
43
  modulatorSources,
44
- OggVorbisEncoder,
44
+ encodeVorbis,
45
45
 
46
46
  // MIDI
47
47
  MIDI,
@@ -61,6 +61,7 @@ export function writeRMIDI(soundfontBinary, mid, soundfont, bankOffset = 0, enco
61
61
  consoleColors.info,
62
62
  consoleColors.value);
63
63
  SpessaSynthInfo("metadata", metadata);
64
+ SpessaSynthInfo("Initial bank offset", mid.bankOffset);
64
65
  // add offset to bank. See wiki About-RMIDI
65
66
  // also fix presets that don't exists since midiplayer6 doesn't seem to default to 0 when nonextistent...
66
67
  let system = "gm";
@@ -69,202 +70,252 @@ export function writeRMIDI(soundfontBinary, mid, soundfont, bankOffset = 0, enco
69
70
  * @type {{tNum: number, e: MidiMessage}[]}
70
71
  */
71
72
  let unwantedSystems = [];
72
- const channelsAmount = 16 + mid.midiPortChannelOffsets.reduce((max, cur) => cur > max ? cur: max);
73
- const channelHasBankSelects = Array(channelsAmount).fill(false);
74
- mid.tracks.forEach((t, trackNum) => {
75
- /**
76
- * @type {boolean[]}
77
- */
78
- let hasBankSelects = Array(16).fill(true);
79
- mid.usedChannelsOnTrack[trackNum].forEach(c => {
80
- // fill with true, only the channels on this track are set to false
81
- // (so we won't add banks for channels the track isn't refering to)
82
- hasBankSelects[c] = false;
83
- });
84
- /**
85
- * @type {MidiMessage[]}
86
- */
87
- let lastBankChanges = [];
88
- /**
89
- * @type {boolean[]}
90
- */
91
- let drums = Array(16).fill(false);
92
- drums[DEFAULT_PERCUSSION] = true;
93
- let programs = Array(16).fill(0);
94
- const portOffset = mid.midiPortChannelOffsets[mid.midiPorts[trackNum]];
95
- t.forEach(e => {
96
- const status = e.messageStatusByte & 0xF0;
97
- if(
98
- status !== messageTypes.controllerChange &&
99
- status !== messageTypes.programChange &&
100
- status !== messageTypes.systemExclusive
101
- )
73
+ /**
74
+ * indexes for tracks
75
+ * @type {number[]}
76
+ */
77
+ const eventIndexes = Array(mid.tracks.length).fill(0);
78
+ let remainingTracks = mid.tracks.length;
79
+ function findFirstEventIndex()
80
+ {
81
+ let index = 0;
82
+ let ticks = Infinity;
83
+ mid.tracks.forEach((track, i) => {
84
+ if(eventIndexes[i] >= track.length)
102
85
  {
103
86
  return;
104
87
  }
105
-
106
- if(status === messageTypes.systemExclusive)
88
+ if(track[eventIndexes[i]].ticks < ticks)
107
89
  {
108
- // check for drum sysex
109
- if(
110
- e.messageData[0] !== 0x41 || // roland
111
- e.messageData[2] !== 0x42 || // GS
112
- e.messageData[3] !== 0x12 || // GS
113
- e.messageData[4] !== 0x40 || // system parameter
114
- (e.messageData[5] & 0x10 ) === 0 || // part parameter
115
- e.messageData[6] !== 0x15 // drum part
116
- )
117
- {
118
- // check for XG
119
- if(
120
- e.messageData[0] === 0x43 && // yamaha
121
- e.messageData[2] === 0x4C && // sXG ON
122
- e.messageData[5] === 0x7E &&
123
- e.messageData[6] === 0x00
124
- )
125
- {
126
- system = "xg";
127
- }
128
- else
129
- if(
130
- e.messageData[0] === 0x41 // roland
131
- && e.messageData[2] === 0x42 // GS
132
- && e.messageData[6] === 0x7F // Mode set
133
- )
134
- {
135
- system = "gs";
136
- }
137
- else
138
- if(
139
- e.messageData[0] === 0x7E // non realtime
140
- && e.messageData[2] === 0x09 // gm system
141
- )
142
- {
143
- system = "gm";
144
- unwantedSystems.push({
145
- tNum: trackNum,
146
- e: e
147
- });
148
- }
149
- return;
150
- }
151
- const sysexChannel = [9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15][e.messageData[5] & 0x0F];
152
- drums[sysexChannel] = !!(e.messageData[7] > 0 && e.messageData[5] >> 4);
153
- return;
90
+ index = i;
91
+ ticks = track[eventIndexes[i]].ticks;
154
92
  }
93
+ });
94
+ return index;
95
+ }
96
+ // it copies midiPorts everywhere else, but here 0 works so DO NOT CHANGE!!!!!!!
97
+ const ports = Array(mid.tracks.length).fill(0);
98
+ const channelsAmount = 16 + mid.midiPortChannelOffsets.reduce((max, cur) => cur > max ? cur: max);
99
+ /**
100
+ * @type {{
101
+ * program: number,
102
+ * drums: boolean,
103
+ * lastBank: MidiMessage,
104
+ * hasBankSelect: boolean
105
+ * }[]}
106
+ */
107
+ const channelsInfo = [];
108
+ for (let i = 0; i < channelsAmount; i++)
109
+ {
110
+ channelsInfo.push({
111
+ program: 0,
112
+ drums: i % 16 === DEFAULT_PERCUSSION, // drums appear on 9 every 16 channels,
113
+ lastBank: undefined,
114
+ hasBankSelect: false,
115
+ });
116
+ }
117
+ while(remainingTracks > 0)
118
+ {
119
+ let trackNum = findFirstEventIndex();
120
+ const track = mid.tracks[trackNum];
121
+ if(eventIndexes[trackNum] >= track.length)
122
+ {
123
+ remainingTracks--;
124
+ continue;
125
+ }
126
+ const e = track[eventIndexes[trackNum]];
127
+ eventIndexes[trackNum]++;
128
+
129
+ let portOffset = mid.midiPortChannelOffsets[ports[trackNum]];
130
+ if(e.messageStatusByte === messageTypes.midiPort)
131
+ {
132
+ ports[trackNum] = e.messageData[0];
133
+ continue;
134
+ }
135
+ const status = e.messageStatusByte & 0xF0;
136
+ if(
137
+ status !== messageTypes.controllerChange &&
138
+ status !== messageTypes.programChange &&
139
+ status !== messageTypes.systemExclusive
140
+ )
141
+ {
142
+ continue
143
+ }
155
144
 
156
- const chNum = e.messageStatusByte & 0xF;
157
- if(status === messageTypes.programChange)
145
+ if(status === messageTypes.systemExclusive)
146
+ {
147
+ // check for drum sysex
148
+ if(
149
+ e.messageData[0] !== 0x41 || // roland
150
+ e.messageData[2] !== 0x42 || // GS
151
+ e.messageData[3] !== 0x12 || // GS
152
+ e.messageData[4] !== 0x40 || // system parameter
153
+ (e.messageData[5] & 0x10 ) === 0 || // part parameter
154
+ e.messageData[6] !== 0x15 // drum part
155
+ )
158
156
  {
159
- // check if the preset for this program exists
160
- if(drums[chNum])
157
+ // check for XG
158
+ if(
159
+ e.messageData[0] === 0x43 && // yamaha
160
+ e.messageData[2] === 0x4C && // sXG ON
161
+ e.messageData[5] === 0x7E &&
162
+ e.messageData[6] === 0x00
163
+ )
161
164
  {
162
- if(soundfont.presets.findIndex(p => p.program === e.messageData[0] && p.bank === 128) === -1)
163
- {
164
- // doesn't exist. pick any preset that has the 128 bank.
165
- e.messageData[0] = soundfont.presets.find(p => p.bank === 128)?.program || 0;
166
- }
165
+ system = "xg";
167
166
  }
168
167
  else
168
+ if(
169
+ e.messageData[0] === 0x41 // roland
170
+ && e.messageData[2] === 0x42 // GS
171
+ && e.messageData[6] === 0x7F // Mode set
172
+ )
169
173
  {
170
- if (soundfont.presets.findIndex(p => p.program === e.messageData[0] && p.bank !== 128) === -1)
171
- {
172
- // doesn't exist. pick any preset that does not have the 128 bank.
173
- e.messageData[0] = soundfont.presets.find(p => p.bank !== 128)?.program || 0;
174
- }
175
- }
176
- programs[e.messageStatusByte & 0xf] = e.messageData[0];
177
- // check if this preset exists for program and bank
178
- const realBank = lastBankChanges[chNum]?.messageData[1] - mid.bankOffset; // make sure to take the previous bank offset into account
179
- const bank = drums[chNum] ? 128 : realBank;
180
- if(lastBankChanges[chNum] === undefined)
181
- {
182
- return;
174
+ system = "gs";
183
175
  }
184
- if(system === "xg" && drums[chNum])
176
+ else
177
+ if(
178
+ e.messageData[0] === 0x7E // non realtime
179
+ && e.messageData[2] === 0x09 // gm system
180
+ )
185
181
  {
186
- // drums override: set bank to 127
187
- lastBankChanges[chNum].messageData[1] = 127;
188
- return;
182
+ system = "gm";
183
+ unwantedSystems.push({
184
+ tNum: trackNum,
185
+ e: e
186
+ });
189
187
  }
188
+ continue;
189
+ }
190
+ const sysexChannel = [9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15][e.messageData[5] & 0x0F] + portOffset;
191
+ channelsInfo[sysexChannel].drums = !!(e.messageData[7] > 0 && e.messageData[5] >> 4);
192
+ continue;
193
+ }
190
194
 
191
- if(soundfont.presets.findIndex(p => p.bank === bank && p.program === e.messageData[0]) === -1)
195
+ // program change
196
+ const chNum = (e.messageStatusByte & 0xF) + portOffset;
197
+ const channel = channelsInfo[chNum];
198
+ if(status === messageTypes.programChange)
199
+ {
200
+ // check if the preset for this program exists
201
+ if(channel.drums)
202
+ {
203
+ if(soundfont.presets.findIndex(p => p.program === e.messageData[0] && p.bank === 128) === -1)
192
204
  {
193
- // no preset with this bank. find this program with any bank
194
- const targetPreset = (soundfont.presets.find(p => p.program === e.messageData[0])?.bank + bankOffset) || bankOffset;
195
- lastBankChanges[chNum].messageData[1] = targetPreset;
196
- SpessaSynthInfo(`%cNo preset %c${bank}:${e.messageData[0]}%c. Changing bank to ${targetPreset}.`,
197
- consoleColors.info,
198
- consoleColors.recognized,
199
- consoleColors.info);
205
+ // doesn't exist. pick any preset that has the 128 bank.
206
+ e.messageData[0] = soundfont.presets.find(p => p.bank === 128)?.program || 0;
200
207
  }
201
- else
208
+ }
209
+ else
210
+ {
211
+ if (soundfont.presets.findIndex(p => p.program === e.messageData[0] && p.bank !== 128) === -1)
202
212
  {
203
- // there is a preset with this bank. add offset
204
- lastBankChanges[chNum].messageData[1] = realBank + bankOffset;
205
- SpessaSynthInfo(`%cPreset %c${bank}:${e.messageData[0]}%c exists. Changing bank to ${lastBankChanges[chNum].messageData[1]}.`,
206
- consoleColors.info,
207
- consoleColors.recognized,
208
- consoleColors.info);
213
+ // doesn't exist. pick any preset that does not have the 128 bank.
214
+ e.messageData[0] = soundfont.presets.find(p => p.bank !== 128)?.program || 0;
209
215
  }
210
- return;
211
216
  }
212
- if(e.messageData[0] !== midiControllers.bankSelect)
217
+ channel.program = e.messageData[0];
218
+ // check if this preset exists for program and bank
219
+ const realBank = Math.max(0,channel.lastBank?.messageData[1] - mid.bankOffset); // make sure to take the previous bank offset into account
220
+ const bank = channel.drums ? 128 : realBank;
221
+ if(channel.lastBank === undefined)
213
222
  {
214
- return;
223
+ continue;
215
224
  }
216
- // bank select
217
- hasBankSelects[chNum] = true;
218
- channelHasBankSelects[chNum + portOffset] = true;
219
- if(system === "xg")
225
+ if(system === "xg" && channel.drums)
220
226
  {
221
- // check for xg drums
222
- drums[chNum] = e.messageData[1] === 120 || e.messageData[1] === 126 || e.messageData[1] === 127;
227
+ // drums override: set bank to 127
228
+ channelsInfo[chNum].lastBank.messageData[1] = 127;
223
229
  }
224
- lastBankChanges[chNum] = e;
225
- });
226
- // add all bank selects that are missing for this track
227
- hasBankSelects.forEach((has, ch) => {
228
- if(has === true)
230
+
231
+ if(soundfont.presets.findIndex(p => p.bank === bank && p.program === e.messageData[0]) === -1)
229
232
  {
230
- return;
233
+ // no preset with this bank. find this program with any bank
234
+ const targetBank = (soundfont.presets.find(p => p.program === e.messageData[0])?.bank + bankOffset) || bankOffset;
235
+ channel.lastBank.messageData[1] = targetBank;
236
+ SpessaSynthInfo(`%cNo preset %c${bank}:${e.messageData[0]}%c. Changing bank to ${targetBank}.`,
237
+ consoleColors.info,
238
+ consoleColors.recognized,
239
+ consoleColors.info);
231
240
  }
232
- // if this channel has bank selects but not in this track specifically, ignore too
233
- if(channelHasBankSelects[ch + portOffset] === true)
241
+ else
234
242
  {
235
- return;
243
+ // there is a preset with this bank. add offset. For drums add the normal offset.
244
+ const newBank = (bank === 128 ? 0 : realBank) + bankOffset;
245
+ channel.lastBank.messageData[1] = newBank;
246
+ SpessaSynthInfo(`%cPreset %c${bank}:${e.messageData[0]}%c exists. Changing bank to ${newBank}.`,
247
+ consoleColors.info,
248
+ consoleColors.recognized,
249
+ consoleColors.info);
236
250
  }
237
- // find first program change (for the given channel)
238
- const status = messageTypes.programChange | ch;
239
- let indexToAdd = t.findIndex(e => e.messageStatusByte === status);
240
- if(indexToAdd === -1)
251
+ continue;
252
+ }
253
+ // we only care about bank select
254
+ if(e.messageData[0] !== midiControllers.bankSelect)
255
+ {
256
+ continue;
257
+ }
258
+ // bank select
259
+ channel.hasBankSelect = true;
260
+ if(system === "xg")
261
+ {
262
+ // check for xg drums
263
+ channel.drums = e.messageData[1] === 120 || e.messageData[1] === 126 || e.messageData[1] === 127;
264
+ }
265
+ channel.lastBank = e;
266
+ }
267
+
268
+ // add missing bank selects
269
+ // add all bank selects that are missing for this track
270
+ channelsInfo.forEach((has, ch) => {
271
+ if(has.hasBankSelect === true)
272
+ {
273
+ return;
274
+ }
275
+ // find first program change (for the given channel)
276
+ const midiChannel = ch % 16;
277
+ const status = messageTypes.programChange | midiChannel;
278
+ // find track with this channel being used
279
+ const portOffset = Math.floor(ch / 16) * 16;
280
+ const port = mid.midiPortChannelOffsets.indexOf(portOffset);
281
+ const track = mid.tracks.find((t, tNum) => mid.midiPorts[tNum] === port && mid.usedChannelsOnTrack[tNum].has(midiChannel));
282
+ if(track === undefined)
283
+ {
284
+ // this channel is not used at all
285
+ return;
286
+ }
287
+ let indexToAdd = track.findIndex(e => e.messageStatusByte === status);
288
+ if(indexToAdd === -1)
289
+ {
290
+ // no program change...
291
+ // add programs if they are missing from the track (need them to activate bank 1 for the embedded sfont)
292
+ const programIndex = track.findIndex(e => (e.messageStatusByte > 0x80 && e.messageStatusByte < 0xF0) && (e.messageStatusByte & 0xF) === midiChannel);
293
+ if(programIndex === -1)
241
294
  {
242
- // no program change...
243
- // add programs if they are missing from the track (need them to activate bank 1 for the embedded sfont)
244
- const programIndex = t.findIndex(e => (e.messageStatusByte > 0x80 && e.messageStatusByte < 0xF0) && (e.messageStatusByte & 0xF) === ch);
245
- if(programIndex === -1)
246
- {
247
- // no voices??? skip
248
- return;
249
- }
250
- const programTicks = t[programIndex].ticks;
251
- const targetProgram = soundfont.getPreset(0, 0).program;
252
- t.splice(programIndex, 0, new MidiMessage(
253
- programTicks,
254
- messageTypes.programChange | ch,
255
- new IndexedByteArray([targetProgram])
256
- ));
257
- indexToAdd = programIndex;
295
+ // no voices??? skip
296
+ return;
258
297
  }
259
- const ticks = t[indexToAdd].ticks;
260
- const targetBank = (soundfont.getPreset(0, programs[ch])?.bank + bankOffset) || bankOffset;
261
- t.splice(indexToAdd,0, new MidiMessage(
262
- ticks,
263
- messageTypes.controllerChange | ch,
264
- new IndexedByteArray([midiControllers.bankSelect, targetBank])
298
+ const programTicks = track[programIndex].ticks;
299
+ const targetProgram = soundfont.getPreset(0, 0).program;
300
+ track.splice(programIndex, 0, new MidiMessage(
301
+ programTicks,
302
+ messageTypes.programChange | midiChannel,
303
+ new IndexedByteArray([targetProgram])
265
304
  ));
266
- });
305
+ indexToAdd = programIndex;
306
+ }
307
+ SpessaSynthInfo(`%cAdding bank select for %c${ch}`,
308
+ consoleColors.info,
309
+ consoleColors.recognized)
310
+ const ticks = track[indexToAdd].ticks;
311
+ const targetBank = (soundfont.getPreset(0, has.program)?.bank + bankOffset) || bankOffset;
312
+ track.splice(indexToAdd,0, new MidiMessage(
313
+ ticks,
314
+ messageTypes.controllerChange | midiChannel,
315
+ new IndexedByteArray([midiControllers.bankSelect, targetBank])
316
+ ));
267
317
  });
318
+
268
319
  // make sure to put xg if gm
269
320
  if(system !== "gs" && system !== "xg")
270
321
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spessasynth_lib",
3
- "version": "3.12.3",
3
+ "version": "3.13.1",
4
4
  "description": "No compromise MIDI and SoundFont2 Synthesizer library",
5
5
  "browser": "index.js",
6
6
  "types": "@types/index.d.ts",