spessasynth_lib 3.11.2 → 3.12.2

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.
@@ -1,16 +1,33 @@
1
1
  /**
2
- *
2
+ * @typedef {Object} RMIDMetadata
3
+ * @property {string|undefined} name - the name of the file
4
+ * @property {string|undefined} engineer - the engineer who worked on the file
5
+ * @property {string|undefined} artist - the artist
6
+ * @property {string|undefined} album - the album
7
+ * @property {string|undefined} genre - the genre of the song
8
+ * @property {ArrayBuffer|undefined} picture - the image for the file (album cover)
9
+ * @property {string|undefined} comment - the coment of the file
10
+ * @property {string|undefined} creationDate - the creation date of the file
11
+ * @property {string|undefined} copyright - the copyright of the file
12
+ */
13
+ /**
14
+ * Writes an RMIDI file
3
15
  * @param soundfontBinary {Uint8Array}
4
16
  * @param mid {MIDI}
5
17
  * @param soundfont {SoundFont2}
6
18
  * @param bankOffset {number} the bank offset for RMIDI
7
19
  * @param encoding {string} the encoding of the RMIDI info chunk
20
+ * @param metadata {RMIDMetadata} the metadata of the file. Optional. If provided, the encoding is forced to utf-8/
8
21
  * @returns {IndexedByteArray}
9
22
  */
10
- export function writeRMIDI(soundfontBinary: Uint8Array, mid: MIDI, soundfont: SoundFont2, bankOffset?: number, encoding?: string): IndexedByteArray;
23
+ export function writeRMIDI(soundfontBinary: Uint8Array, mid: MIDI, soundfont: SoundFont2, bankOffset?: number, encoding?: string, metadata?: RMIDMetadata): IndexedByteArray;
11
24
  export type RMIDINFOChunks = string;
12
25
  export namespace RMIDINFOChunks {
13
26
  let name: string;
27
+ let album: string;
28
+ let artist: string;
29
+ let genre: string;
30
+ let picture: string;
14
31
  let copyright: string;
15
32
  let creationDate: string;
16
33
  let comment: string;
@@ -19,4 +36,42 @@ export namespace RMIDINFOChunks {
19
36
  let encoding: string;
20
37
  let bankOffset: string;
21
38
  }
39
+ export type RMIDMetadata = {
40
+ /**
41
+ * - the name of the file
42
+ */
43
+ name: string | undefined;
44
+ /**
45
+ * - the engineer who worked on the file
46
+ */
47
+ engineer: string | undefined;
48
+ /**
49
+ * - the artist
50
+ */
51
+ artist: string | undefined;
52
+ /**
53
+ * - the album
54
+ */
55
+ album: string | undefined;
56
+ /**
57
+ * - the genre of the song
58
+ */
59
+ genre: string | undefined;
60
+ /**
61
+ * - the image for the file (album cover)
62
+ */
63
+ picture: ArrayBuffer | undefined;
64
+ /**
65
+ * - the coment of the file
66
+ */
67
+ comment: string | undefined;
68
+ /**
69
+ * - the creation date of the file
70
+ */
71
+ creationDate: string | undefined;
72
+ /**
73
+ * - the copyright of the file
74
+ */
75
+ copyright: string | undefined;
76
+ };
22
77
  import { IndexedByteArray } from '../utils/indexed_array.js';
@@ -11,6 +11,12 @@ export function readRIFFChunk(dataArray: IndexedByteArray, readData?: boolean, f
11
11
  * @returns {IndexedByteArray}
12
12
  */
13
13
  export function writeRIFFChunk(chunk: RiffChunk, prepend?: IndexedByteArray): IndexedByteArray;
14
+ /**
15
+ * @param header {string}
16
+ * @param data {Uint8Array}
17
+ * @returns {IndexedByteArray}
18
+ */
19
+ export function writeRIFFOddSize(header: string, data: Uint8Array): IndexedByteArray;
14
20
  /**
15
21
  * riff_chunk.js
16
22
  * reads a riff read and stores it as a class
@@ -1,3 +1,9 @@
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
+ */
1
7
  /**
2
8
  *
3
9
  * @param audioBuffer {AudioBuffer}
@@ -6,3 +12,17 @@
6
12
  * @returns {Blob}
7
13
  */
8
14
  export function audioBufferToWav(audioBuffer: AudioBuffer, normalizeAudio?: boolean, channelOffset?: number): Blob;
15
+ export type WaveMetadata = {
16
+ /**
17
+ * - the song's title
18
+ */
19
+ title: string;
20
+ /**
21
+ * - the song's album
22
+ */
23
+ album: string;
24
+ /**
25
+ * - the song's genre
26
+ */
27
+ genre: string;
28
+ };
@@ -47,6 +47,7 @@ class MIDI{
47
47
  * @type {string}
48
48
  */
49
49
  this.copyright = "";
50
+ let copyrightDetected = false;
50
51
 
51
52
  /**
52
53
  * The MIDI name
@@ -55,6 +56,7 @@ class MIDI{
55
56
  this.midiName = "";
56
57
 
57
58
  this.rawMidiName = new Uint8Array(0);
59
+ let nameDetected = false;
58
60
 
59
61
  const initialString = readBytesAsString(binaryData, 4);
60
62
  binaryData.currentIndex -= 4;
@@ -106,12 +108,14 @@ class MIDI{
106
108
  }
107
109
  if(this.RMIDInfo['ICOP'])
108
110
  {
109
- this.copyright += readBytesAsString(this.RMIDInfo['ICOP'], this.RMIDInfo['ICOP'].length);
111
+ copyrightDetected = true;
112
+ this.copyright = readBytesAsString(this.RMIDInfo['ICOP'], this.RMIDInfo['ICOP'].length);
110
113
  }
111
114
  if(this.RMIDInfo['INAM'])
112
115
  {
113
116
  this.rawMidiName = this.RMIDInfo[RMIDINFOChunks.name];
114
- this.midiName = readBytesAsString(this.rawMidiName, this.rawMidiName.length);
117
+ this.midiName = readBytesAsString(this.rawMidiName, this.rawMidiName.length, undefined, false);
118
+ nameDetected = true;
115
119
  }
116
120
  this.bankOffset = 1; // defaults to 1
117
121
  if(this.RMIDInfo[RMIDINFOChunks.bankOffset])
@@ -346,7 +350,10 @@ class MIDI{
346
350
  break;
347
351
 
348
352
  case messageTypes.copyright:
349
- this.copyright += readBytesAsString(eventData, eventData.length) + "\n";
353
+ if(!copyrightDetected)
354
+ {
355
+ this.copyright += readBytesAsString(eventData, eventData.length, undefined, false) + "\n";
356
+ }
350
357
  break;
351
358
 
352
359
  case messageTypes.lyric:
@@ -432,8 +439,10 @@ class MIDI{
432
439
  loopStart = this.firstNoteOn;
433
440
  loopEnd = this.lastVoiceEventTick;
434
441
  }
435
- else {
436
- if (loopStart === null) {
442
+ else
443
+ {
444
+ if (loopStart === null)
445
+ {
437
446
  loopStart = this.firstNoteOn;
438
447
  }
439
448
 
@@ -470,7 +479,7 @@ class MIDI{
470
479
  this.loop = {start: loopStart, end: loopEnd};
471
480
 
472
481
  // midi name
473
- if(this.midiName.length < 1)
482
+ if(!nameDetected)
474
483
  {
475
484
  if (this.tracks.length > 1)
476
485
  {
@@ -479,7 +488,7 @@ class MIDI{
479
488
  this.tracks[0].find(
480
489
  message => message.messageStatusByte >= messageTypes.noteOn
481
490
  &&
482
- message.messageStatusByte < messageTypes.systemExclusive
491
+ message.messageStatusByte < messageTypes.polyPressure
483
492
  ) === undefined
484
493
  )
485
494
  {
@@ -1,6 +1,6 @@
1
1
  import { combineArrays, IndexedByteArray } from '../utils/indexed_array.js'
2
2
  import { writeMIDIFile } from './midi_writer.js'
3
- import { RiffChunk, writeRIFFChunk } from '../soundfont/read/riff_chunk.js'
3
+ import { writeRIFFOddSize } from '../soundfont/read/riff_chunk.js'
4
4
  import { getStringBytes } from '../utils/byte_functions/string.js'
5
5
  import { messageTypes, midiControllers, MidiMessage } from './midi_message.js'
6
6
  import { DEFAULT_PERCUSSION } from '../synthetizer/synthetizer.js'
@@ -8,14 +8,17 @@ import { getGsOn } from './midi_editor.js'
8
8
  import { SpessaSynthGroup, SpessaSynthGroupEnd, SpessaSynthInfo } from '../utils/loggin.js'
9
9
  import { consoleColors } from '../utils/other.js'
10
10
  import { writeLittleEndian } from '../utils/byte_functions/little_endian.js'
11
-
12
11
  /**
13
12
  * @enum {string}
14
13
  */
15
14
  export const RMIDINFOChunks = {
16
15
  name: "INAM",
16
+ album: "IPRD",
17
+ artist: "IART",
18
+ genre: "IGNR",
19
+ picture: "IPIC",
17
20
  copyright: "ICOP",
18
- creationDate: "ICRT",
21
+ creationDate: "ICRD",
19
22
  comment: "ICMT",
20
23
  engineer: "IENG",
21
24
  software: "ISFT",
@@ -23,16 +26,33 @@ export const RMIDINFOChunks = {
23
26
  bankOffset: "DBNK"
24
27
  }
25
28
 
29
+ const FORCED_ENCODING = "utf-8";
30
+ const DEFAULT_COPYRIGHT = "Created by SpessaSynth";
31
+
32
+ /**
33
+ * @typedef {Object} RMIDMetadata
34
+ * @property {string|undefined} name - the name of the file
35
+ * @property {string|undefined} engineer - the engineer who worked on the file
36
+ * @property {string|undefined} artist - the artist
37
+ * @property {string|undefined} album - the album
38
+ * @property {string|undefined} genre - the genre of the song
39
+ * @property {ArrayBuffer|undefined} picture - the image for the file (album cover)
40
+ * @property {string|undefined} comment - the coment of the file
41
+ * @property {string|undefined} creationDate - the creation date of the file
42
+ * @property {string|undefined} copyright - the copyright of the file
43
+ */
44
+
26
45
  /**
27
- *
46
+ * Writes an RMIDI file
28
47
  * @param soundfontBinary {Uint8Array}
29
48
  * @param mid {MIDI}
30
49
  * @param soundfont {SoundFont2}
31
50
  * @param bankOffset {number} the bank offset for RMIDI
32
51
  * @param encoding {string} the encoding of the RMIDI info chunk
52
+ * @param metadata {RMIDMetadata} the metadata of the file. Optional. If provided, the encoding is forced to utf-8/
33
53
  * @returns {IndexedByteArray}
34
54
  */
35
- export function writeRMIDI(soundfontBinary, mid, soundfont, bankOffset = 0, encoding = "Shift_JIS")
55
+ export function writeRMIDI(soundfontBinary, mid, soundfont, bankOffset = 0, encoding = "Shift_JIS", metadata = {})
36
56
  {
37
57
  SpessaSynthGroup("%cWriting the RMIDI File...", consoleColors.info);
38
58
  SpessaSynthInfo(`%cConfiguration: Bank offset: %c${bankOffset}%c, encoding: %c${encoding}`,
@@ -40,6 +60,7 @@ export function writeRMIDI(soundfontBinary, mid, soundfont, bankOffset = 0, enco
40
60
  consoleColors.value,
41
61
  consoleColors.info,
42
62
  consoleColors.value);
63
+ SpessaSynthInfo("metadata", metadata);
43
64
  // add offset to bank. See wiki About-RMIDI
44
65
  // also fix presets that don't exists since midiplayer6 doesn't seem to default to 0 when nonextistent...
45
66
  let system = "gm";
@@ -156,7 +177,7 @@ export function writeRMIDI(soundfontBinary, mid, soundfont, bankOffset = 0, enco
156
177
  // check if this preset exists for program and bank
157
178
  const realBank = lastBankChanges[chNum]?.messageData[1] - mid.bankOffset; // make sure to take the previous bank offset into account
158
179
  const bank = drums[chNum] ? 128 : realBank;
159
- if(bank === undefined)
180
+ if(lastBankChanges[chNum] === undefined)
160
181
  {
161
182
  return;
162
183
  }
@@ -259,81 +280,139 @@ export function writeRMIDI(soundfontBinary, mid, soundfont, bankOffset = 0, enco
259
280
  const newMid = new IndexedByteArray(writeMIDIFile(mid).buffer);
260
281
 
261
282
  // infodata for RMID
262
- const today = new Date().toLocaleString();
283
+ /**
284
+ * @type {Uint8Array[]}
285
+ */
286
+ const infoContent = [getStringBytes("INFO")];
287
+ const encoder = new TextEncoder();
288
+ // software
289
+ infoContent.push(
290
+ writeRIFFOddSize(RMIDINFOChunks.software, encoder.encode("SpessaSynth"))
291
+ );
292
+ // name
293
+ if(metadata.name !== undefined)
294
+ {
295
+
296
+ infoContent.push(
297
+ writeRIFFOddSize(RMIDINFOChunks.name, encoder.encode(metadata.name))
298
+ );
299
+ encoding = FORCED_ENCODING;
300
+ }
301
+ else
302
+ {
303
+ infoContent.push(
304
+ writeRIFFOddSize(RMIDINFOChunks.name, mid.rawMidiName)
305
+ );
306
+ }
307
+ // creation date
308
+ if(metadata.creationDate !== undefined)
309
+ {
310
+ encoding = FORCED_ENCODING;
311
+ infoContent.push(
312
+ writeRIFFOddSize(RMIDINFOChunks.creationDate, encoder.encode(metadata.creationDate))
313
+ );
314
+ }
315
+ else
316
+ {
317
+ const today = new Date().toLocaleString(undefined, {
318
+ weekday: "long",
319
+ year: 'numeric',
320
+ month: "long",
321
+ day: "numeric",
322
+ hour: "numeric",
323
+ minute: "numeric"
324
+ });
325
+ infoContent.push(
326
+ writeRIFFOddSize(RMIDINFOChunks.creationDate, getStringBytes(today))
327
+ );
328
+ }
329
+ // comment
330
+ if(metadata.comment !== undefined)
331
+ {
332
+ encoding = FORCED_ENCODING;
333
+ infoContent.push(
334
+ writeRIFFOddSize(RMIDINFOChunks.comment, encoder.encode(metadata.comment))
335
+ );
336
+ }
337
+ // engineer
338
+ if(metadata.engineer !== undefined)
339
+ {
340
+ infoContent.push(
341
+ writeRIFFOddSize(RMIDINFOChunks.engineer, encoder.encode(metadata.engineer))
342
+ )
343
+ }
344
+ // album
345
+ if(metadata.album !== undefined)
346
+ {
347
+ encoding = FORCED_ENCODING;
348
+ infoContent.push(
349
+ writeRIFFOddSize(RMIDINFOChunks.album, encoder.encode(metadata.album))
350
+ );
351
+ }
352
+ // artist
353
+ if(metadata.artist !== undefined)
354
+ {
355
+ encoding = FORCED_ENCODING;
356
+ infoContent.push(
357
+ writeRIFFOddSize(RMIDINFOChunks.artist, encoder.encode(metadata.artist))
358
+ );
359
+ }
360
+ // genre
361
+ if(metadata.genre !== undefined)
362
+ {
363
+ encoding = FORCED_ENCODING;
364
+ infoContent.push(
365
+ writeRIFFOddSize(RMIDINFOChunks.genre, encoder.encode(metadata.genre))
366
+ );
367
+ }
368
+ // picture
369
+ if(metadata.picture !== undefined)
370
+ {
371
+ infoContent.push(
372
+ writeRIFFOddSize(RMIDINFOChunks.picture, new Uint8Array(metadata.picture))
373
+ );
374
+ }
375
+ // copyright
376
+ if(metadata.copyright !== undefined)
377
+ {
378
+ encoding = FORCED_ENCODING;
379
+ infoContent.push(
380
+ writeRIFFOddSize(RMIDINFOChunks.copyright, encoder.encode(metadata.copyright))
381
+ );
382
+ }
383
+ else
384
+ {
385
+ // use midi copyright if possible
386
+ const copyright = mid.copyright.length > 0 ? mid.copyright : DEFAULT_COPYRIGHT;
387
+ infoContent.push(
388
+ writeRIFFOddSize(RMIDINFOChunks.copyright, getStringBytes(copyright))
389
+ );
390
+ }
391
+
392
+ // bank offset
263
393
  const DBNK = new IndexedByteArray(2);
264
394
  writeLittleEndian(DBNK, bankOffset, 2);
265
- const ICOP = mid.copyright.length > 0 ? getStringBytes(mid.copyright) : getStringBytes("Created by SpessaSynth");
266
- const infodata = combineArrays([
267
- // icop: copyright
268
- writeRIFFChunk(
269
- new RiffChunk(
270
- RMIDINFOChunks.copyright,
271
- ICOP.length,
272
- ICOP
273
- ),
274
- new IndexedByteArray([73, 78, 70, 79]) // "INFO"
275
- ),
276
- // inam: name
277
- writeRIFFChunk(
278
- new RiffChunk(
279
- RMIDINFOChunks.name,
280
- mid.rawMidiName.length,
281
- new IndexedByteArray(mid.rawMidiName.buffer)
282
- ),
283
- ),
284
- // icrd: creation date
285
- writeRIFFChunk(
286
- new RiffChunk(
287
- RMIDINFOChunks.creationDate,
288
- today.length,
289
- getStringBytes(today)
290
- )
291
- ),
292
- // isft: software used
293
- writeRIFFChunk(
294
- new RiffChunk(
295
- RMIDINFOChunks.software,
296
- 11,
297
- getStringBytes("SpessaSynth"),
298
- )
299
- ),
300
- // ienc: encoding for the info chunk
301
- writeRIFFChunk(
302
- new RiffChunk(
303
- RMIDINFOChunks.encoding,
304
- encoding.length,
305
- getStringBytes(encoding)
306
- )
307
- ),
308
- // dbnk: bank offset
309
- writeRIFFChunk(
310
- new RiffChunk(
311
- RMIDINFOChunks.bankOffset,
312
- 2,
313
- DBNK
314
- )
315
- )
316
- ]);
395
+ infoContent.push(writeRIFFOddSize(RMIDINFOChunks.bankOffset, DBNK));
396
+ // encoding
397
+ infoContent.push(writeRIFFOddSize(RMIDINFOChunks.encoding, getStringBytes(encoding)));
398
+ const infodata = combineArrays(infoContent);
317
399
 
318
400
  const rmiddata = combineArrays([
319
- new Uint8Array([82, 77, 73, 68]), // "RMID"
320
- writeRIFFChunk(new RiffChunk(
401
+ getStringBytes("RMID"),
402
+ writeRIFFOddSize(
321
403
  "data",
322
- newMid.length, // "data", size, midi binary
323
404
  newMid
324
- )),
325
- writeRIFFChunk(new RiffChunk(
405
+ ),
406
+ writeRIFFOddSize(
326
407
  "LIST",
327
- infodata.length,
328
408
  infodata
329
- )),
409
+ ),
330
410
  soundfontBinary
331
411
  ]);
332
412
  SpessaSynthInfo("%cFinished!", consoleColors.info)
333
413
  SpessaSynthGroupEnd();
334
- return writeRIFFChunk(new RiffChunk(
414
+ return writeRIFFOddSize(
335
415
  "RIFF",
336
- rmiddata.length,
337
416
  rmiddata
338
- ));
417
+ );
339
418
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spessasynth_lib",
3
- "version": "3.11.2",
3
+ "version": "3.12.2",
4
4
  "description": "No compromise MIDI and SoundFont2 Synthesizer library",
5
5
  "browser": "index.js",
6
6
  "types": "@types/index.d.ts",
@@ -86,4 +86,23 @@ export function writeRIFFChunk(chunk, prepend = undefined)
86
86
  // write data
87
87
  array.set(chunk.chunkData, array.currentIndex);
88
88
  return array;
89
+ }
90
+
91
+ /**
92
+ * @param header {string}
93
+ * @param data {Uint8Array}
94
+ * @returns {IndexedByteArray}
95
+ */
96
+ export function writeRIFFOddSize(header, data)
97
+ {
98
+ let finalSize = 8 + data.length;
99
+ if(finalSize % 2 !== 0)
100
+ {
101
+ finalSize++;
102
+ }
103
+ const outArray = new IndexedByteArray(finalSize);
104
+ writeStringAsBytes(outArray, header);
105
+ writeDword(outArray, data.length);
106
+ outArray.set(data, 8);
107
+ return outArray;
89
108
  }