spessasynth_lib 3.24.4 → 3.24.6
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/README.md +14 -4
- package/midi_parser/midi_loader.js +51 -28
- package/package.json +1 -1
- package/sequencer/sequencer.js +3 -3
- package/synthetizer/synthetizer.js +8 -6
- package/synthetizer/worklet_processor.min.js +9 -9
- package/synthetizer/worklet_system/main_processor.js +9 -7
- package/synthetizer/worklet_system/worklet_methods/note_on.js +2 -1
package/README.md
CHANGED
|
@@ -43,12 +43,21 @@ document.getElementById("button").onclick = async () =>
|
|
|
43
43
|
- **Excellent SoundFont support:**
|
|
44
44
|
- **Full Generator Support**
|
|
45
45
|
- **Full Modulator Support:** *First (to my knowledge) JavaScript SoundFont synth with that feature!*
|
|
46
|
-
- **GeneralUserGS
|
|
46
|
+
- **GeneralUserGS Compatible:** *[See more here!](https://github.com/mrbumpy409/GeneralUser-GS/blob/main/documentation/README.md)*
|
|
47
47
|
- **SoundFont3 Support:** Play compressed SoundFonts!
|
|
48
48
|
- **Experimental SF2Pack Support:** Play soundfonts compressed with BASSMIDI! (*Note: only works with vorbis compression*)
|
|
49
49
|
- **Can load very large SoundFonts:** up to 4GB! *Note: Only Firefox handles this well; Chromium has a hard-coded memory limit*
|
|
50
|
+
- **Great DLS Support:**
|
|
51
|
+
- **DLS Level 1 Support**
|
|
52
|
+
- **DLS Level 2 Support**
|
|
53
|
+
- **Mobile DLS Support**
|
|
54
|
+
- **Correct articulator support:** *Converts articulators to both modulators and generators!*
|
|
55
|
+
- **Tested and working with gm.dls!**
|
|
56
|
+
- **Correct volume:** *Properly translated to SoundFont volume!*
|
|
57
|
+
- **A-Law encoding support**
|
|
58
|
+
- **Both unsigned 8-bit and signed 16-bit sample support (24-bit theoretically supported as well!)**
|
|
59
|
+
- **Detects special articulator combinations:** *Such as vibratoLfoToPitch*
|
|
50
60
|
- **Soundfont manager:** Stack multiple soundfonts!
|
|
51
|
-
- **DLS Level 1 and 2 Support:** *works with gm.dls!*
|
|
52
61
|
- **Reverb and chorus support:** [customizable!](https://github.com/spessasus/SpessaSynth/wiki/Synthetizer-Class#effects-configuration-object)
|
|
53
62
|
- **Export audio files** using [OfflineAudioContext](https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext)
|
|
54
63
|
- **[Custom modulators for additional controllers](https://github.com/spessasus/SpessaSynth/wiki/Modulator-Class#default-modulators):** *Why not?*
|
|
@@ -106,15 +115,16 @@ document.getElementById("button").onclick = async () =>
|
|
|
106
115
|
- **Variable compression quality:** You choose between file size and quality!
|
|
107
116
|
- **Compression preserving:** Avoid decompressing and recompressing uncompressed samples for minimal quality loss!
|
|
108
117
|
|
|
109
|
-
#### Read and write DLS Level
|
|
118
|
+
#### Read and write DLS Level One or Two files
|
|
110
119
|
- Read DLS (DownLoadable Sounds) files as SF2 files!
|
|
111
120
|
- **Works like a normal soundfont:** *Saving it as sf2 is still [just one function!](https://github.com/spessasus/SpessaSynth/wiki/SoundFont2-Class#write)*
|
|
112
121
|
- Converts articulators to both **modulators** and **generators**!
|
|
113
122
|
- Works with both unsigned 8-bit samples and signed 16-bit samples!
|
|
123
|
+
- A-Law encoding support
|
|
114
124
|
- **Covers special generator cases:** *such as modLfoToPitch*!
|
|
115
125
|
- **Correct volume:** *looking at you, Viena and gm.sf2!*
|
|
116
126
|
- Support built right into the synthesizer!
|
|
117
|
-
- **Convert SF2 to DLS:** [with limitations](https://github.com/spessasus/SpessaSynth/wiki/DLS-Conversion-Problem)
|
|
127
|
+
- **Convert SF2 to DLS:** [with limitations](https://github.com/spessasus/SpessaSynth/wiki/DLS-Conversion-Problem)
|
|
118
128
|
|
|
119
129
|
### Export MIDI as WAV
|
|
120
130
|
- Save the MIDI file as WAV audio!
|
|
@@ -12,7 +12,9 @@ import { BasicMIDI, MIDIticksToSeconds } from "./basic_midi.js";
|
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* midi_loader.js
|
|
15
|
-
* purpose:
|
|
15
|
+
* purpose:
|
|
16
|
+
* parses a midi file for the seqyencer,
|
|
17
|
+
* including things like marker or CC 2/4 loop detection, copyright detection, etc.
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
20
|
/**
|
|
@@ -65,7 +67,7 @@ class MIDI extends BasicMIDI
|
|
|
65
67
|
SpessaSynthGroupEnd();
|
|
66
68
|
throw new SyntaxError(`Invalid RMIDI Chunk header! Expected "data", got "${rmid}"`);
|
|
67
69
|
}
|
|
68
|
-
// this is
|
|
70
|
+
// this is a rmid, load the midi into an array for parsing
|
|
69
71
|
fileByteArray = riff.chunkData;
|
|
70
72
|
|
|
71
73
|
// keep loading chunks until we get sfbk
|
|
@@ -87,7 +89,7 @@ class MIDI extends BasicMIDI
|
|
|
87
89
|
}
|
|
88
90
|
if (type === "dls ")
|
|
89
91
|
{
|
|
90
|
-
//
|
|
92
|
+
// Assume bank offset of 0 by default. If we find any bank selects, then the offset is 1.
|
|
91
93
|
DLSRMID = true;
|
|
92
94
|
}
|
|
93
95
|
}
|
|
@@ -145,7 +147,13 @@ class MIDI extends BasicMIDI
|
|
|
145
147
|
|
|
146
148
|
if (DLSRMID)
|
|
147
149
|
{
|
|
148
|
-
//
|
|
150
|
+
// Assume bank offset of 0 by default. If we find any bank selects, then the offset is 1.
|
|
151
|
+
this.bankOffset = 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// if no embedded bank, assume 0
|
|
155
|
+
if (this.embeddedSoundFont === undefined)
|
|
156
|
+
{
|
|
149
157
|
this.bankOffset = 0;
|
|
150
158
|
}
|
|
151
159
|
}
|
|
@@ -198,8 +206,8 @@ class MIDI extends BasicMIDI
|
|
|
198
206
|
let loopEnd = null;
|
|
199
207
|
|
|
200
208
|
/**
|
|
201
|
-
* For karaoke files, text events starting with @T are considered titles
|
|
202
|
-
* usually the first one is the title, and the latter
|
|
209
|
+
* For karaoke files, text events starting with @T are considered titles,
|
|
210
|
+
* usually the first one is the title, and the latter is things such as "sequenced by" etc.
|
|
203
211
|
* @type {boolean}
|
|
204
212
|
*/
|
|
205
213
|
let karaokeHasTitle = false;
|
|
@@ -207,7 +215,7 @@ class MIDI extends BasicMIDI
|
|
|
207
215
|
this.lastVoiceEventTick = 0;
|
|
208
216
|
|
|
209
217
|
/**
|
|
210
|
-
* Midi port numbers for each tracks
|
|
218
|
+
* Midi port numbers for each one of the tracks
|
|
211
219
|
* @type {number[]}
|
|
212
220
|
*/
|
|
213
221
|
this.midiPorts = [];
|
|
@@ -274,16 +282,19 @@ class MIDI extends BasicMIDI
|
|
|
274
282
|
{
|
|
275
283
|
statusByte = runningByte;
|
|
276
284
|
}
|
|
277
|
-
else if (!runningByte && statusByteCheck < 0x80)
|
|
278
|
-
{
|
|
279
|
-
// if we don't have a running byte and the status byte isn't valid, it's an error.
|
|
280
|
-
SpessaSynthGroupEnd();
|
|
281
|
-
throw new SyntaxError(`Unexpected byte with no running byte. (${statusByteCheck})`);
|
|
282
|
-
}
|
|
283
285
|
else
|
|
284
|
-
{
|
|
285
|
-
|
|
286
|
-
|
|
286
|
+
{ // noinspection PointlessBooleanExpressionJS
|
|
287
|
+
if (!runningByte && statusByteCheck < 0x80)
|
|
288
|
+
{
|
|
289
|
+
// if we don't have a running byte and the status byte isn't valid, it's an error.
|
|
290
|
+
SpessaSynthGroupEnd();
|
|
291
|
+
throw new SyntaxError(`Unexpected byte with no running byte. (${statusByteCheck})`);
|
|
292
|
+
}
|
|
293
|
+
else
|
|
294
|
+
{
|
|
295
|
+
// if the status byte is valid, use that
|
|
296
|
+
statusByte = trackChunk.data[trackChunk.data.currentIndex++];
|
|
297
|
+
}
|
|
287
298
|
}
|
|
288
299
|
const statusByteChannel = getChannel(statusByte);
|
|
289
300
|
|
|
@@ -310,7 +321,7 @@ class MIDI extends BasicMIDI
|
|
|
310
321
|
|
|
311
322
|
default:
|
|
312
323
|
// voice message
|
|
313
|
-
//
|
|
324
|
+
// gets the midi message length
|
|
314
325
|
if (totalTicks > this.lastVoiceEventTick)
|
|
315
326
|
{
|
|
316
327
|
this.lastVoiceEventTick = totalTicks;
|
|
@@ -344,7 +355,7 @@ class MIDI extends BasicMIDI
|
|
|
344
355
|
switch (statusByteChannel)
|
|
345
356
|
{
|
|
346
357
|
case -2:
|
|
347
|
-
// since this is a meta
|
|
358
|
+
// since this is a meta-message
|
|
348
359
|
const eventText = readBytesAsString(eventData, eventData.length);
|
|
349
360
|
switch (statusByte)
|
|
350
361
|
{
|
|
@@ -404,7 +415,7 @@ class MIDI extends BasicMIDI
|
|
|
404
415
|
// note here: .kar files sometimes just use...
|
|
405
416
|
// lyrics instead of text because why not (of course)
|
|
406
417
|
// perform the same check for @KMIDI KARAOKE FILE
|
|
407
|
-
if (eventText.trim()
|
|
418
|
+
if (eventText.trim().startsWith("@KMIDI KARAOKE FILE"))
|
|
408
419
|
{
|
|
409
420
|
this.isKaraokeFile = true;
|
|
410
421
|
SpessaSynthInfo("%cKaraoke MIDI detected!", consoleColors.recognized);
|
|
@@ -431,7 +442,7 @@ class MIDI extends BasicMIDI
|
|
|
431
442
|
// it has a text event at the start of the file
|
|
432
443
|
// "@KMIDI KARAOKE FILE"
|
|
433
444
|
const checkedText = eventText.trim();
|
|
434
|
-
if (checkedText
|
|
445
|
+
if (checkedText.startsWith("@KMIDI KARAOKE FILE"))
|
|
435
446
|
{
|
|
436
447
|
this.isKaraokeFile = true;
|
|
437
448
|
|
|
@@ -440,7 +451,8 @@ class MIDI extends BasicMIDI
|
|
|
440
451
|
else if (this.isKaraokeFile)
|
|
441
452
|
{
|
|
442
453
|
// check for @T (title)
|
|
443
|
-
// or @A because it is a title too sometimes
|
|
454
|
+
// or @A because it is a title too sometimes?
|
|
455
|
+
// IDK it's strange
|
|
444
456
|
if (checkedText.startsWith("@T") || checkedText.startsWith("@A"))
|
|
445
457
|
{
|
|
446
458
|
if (!karaokeHasTitle)
|
|
@@ -477,6 +489,11 @@ class MIDI extends BasicMIDI
|
|
|
477
489
|
// since this is a voice message
|
|
478
490
|
// check for loop (CC 2/4)
|
|
479
491
|
trackHasVoiceMessages = true;
|
|
492
|
+
// voice messages are 7-bit always
|
|
493
|
+
for (let j = 0; j < eventData.length; j++)
|
|
494
|
+
{
|
|
495
|
+
eventData[j] = Math.min(127, eventData[j]);
|
|
496
|
+
}
|
|
480
497
|
if ((statusByte & 0xF0) === messageTypes.controllerChange)
|
|
481
498
|
{
|
|
482
499
|
switch (eventData[0])
|
|
@@ -494,7 +511,9 @@ class MIDI extends BasicMIDI
|
|
|
494
511
|
}
|
|
495
512
|
else
|
|
496
513
|
{
|
|
497
|
-
// this controller has occured more than once
|
|
514
|
+
// this controller has occured more than once;
|
|
515
|
+
// this means
|
|
516
|
+
// that it doesn't indicate the loop
|
|
498
517
|
loopEnd = 0;
|
|
499
518
|
}
|
|
500
519
|
break;
|
|
@@ -516,7 +535,7 @@ class MIDI extends BasicMIDI
|
|
|
516
535
|
this.tracks.push(track);
|
|
517
536
|
this.usedChannelsOnTrack.push(usedChannels);
|
|
518
537
|
|
|
519
|
-
//
|
|
538
|
+
// If the track has no voice messages, its "track name" event (if it has any)
|
|
520
539
|
// is some metadata. Add it to copyright
|
|
521
540
|
if (!trackHasVoiceMessages)
|
|
522
541
|
{
|
|
@@ -602,9 +621,13 @@ class MIDI extends BasicMIDI
|
|
|
602
621
|
|
|
603
622
|
// fix midi ports:
|
|
604
623
|
// midi tracks without ports will have a value of -1
|
|
605
|
-
// if all ports have a value of -1, set it to 0,
|
|
606
|
-
//
|
|
607
|
-
//
|
|
624
|
+
// if all ports have a value of -1, set it to 0,
|
|
625
|
+
// otherwise take the first midi port and replace all -1 with it,
|
|
626
|
+
// why would we do this?
|
|
627
|
+
// some midis (for some reason) specify all channels to port 1 or else,
|
|
628
|
+
// but leave the conductor track with no port pref.
|
|
629
|
+
// this spessasynth to reserve the first 16 channels for the conductor track
|
|
630
|
+
// (which doesn't play anything) and use the additional 16 for the actual ports.
|
|
608
631
|
let defaultPort = 0;
|
|
609
632
|
for (let port of this.midiPorts)
|
|
610
633
|
{
|
|
@@ -615,7 +638,7 @@ class MIDI extends BasicMIDI
|
|
|
615
638
|
}
|
|
616
639
|
}
|
|
617
640
|
this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultPort : port);
|
|
618
|
-
// add
|
|
641
|
+
// add fake port if empty
|
|
619
642
|
if (this.midiPortChannelOffsets.length === 0)
|
|
620
643
|
{
|
|
621
644
|
this.midiPortChannelOffsets = [0];
|
|
@@ -723,7 +746,7 @@ class MIDI extends BasicMIDI
|
|
|
723
746
|
{
|
|
724
747
|
this.lyrics = this.lyrics.map(lyric =>
|
|
725
748
|
{
|
|
726
|
-
//
|
|
749
|
+
// One exception: hyphens at the end. Don't add a space to them
|
|
727
750
|
if (lyric[lyric.length - 1] === 45)
|
|
728
751
|
{
|
|
729
752
|
return lyric;
|
package/package.json
CHANGED
package/sequencer/sequencer.js
CHANGED
|
@@ -149,12 +149,12 @@ export class Sequencer
|
|
|
149
149
|
* @type {boolean}
|
|
150
150
|
* @private
|
|
151
151
|
*/
|
|
152
|
-
this._skipToFirstNoteOn = options?.skipToFirstNoteOn
|
|
152
|
+
this._skipToFirstNoteOn = options?.skipToFirstNoteOn ?? true;
|
|
153
153
|
/**
|
|
154
154
|
* @type {boolean}
|
|
155
155
|
* @private
|
|
156
156
|
*/
|
|
157
|
-
this._preservePlaybackState = options?.preservePlaybackState
|
|
157
|
+
this._preservePlaybackState = options?.preservePlaybackState ?? false;
|
|
158
158
|
|
|
159
159
|
if (this._skipToFirstNoteOn === false)
|
|
160
160
|
{
|
|
@@ -167,7 +167,7 @@ export class Sequencer
|
|
|
167
167
|
this._sendMessage(WorkletSequencerMessageType.setPreservePlaybackState, true);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
this.loadNewSongList(midiBinaries, options?.autoPlay
|
|
170
|
+
this.loadNewSongList(midiBinaries, options?.autoPlay ?? true);
|
|
171
171
|
|
|
172
172
|
window.addEventListener("beforeunload", this.resetMIDIOut.bind(this));
|
|
173
173
|
}
|
|
@@ -109,7 +109,7 @@ export class Synthetizer
|
|
|
109
109
|
this._voicesAmount = 0;
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
* For Black MIDI's - forces release time to
|
|
112
|
+
* For Black MIDI's - forces release time to 50 ms
|
|
113
113
|
* @type {boolean}
|
|
114
114
|
*/
|
|
115
115
|
this._highPerformanceMode = false;
|
|
@@ -268,7 +268,7 @@ export class Synthetizer
|
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
/**
|
|
271
|
-
* For Black MIDI's - forces release time to
|
|
271
|
+
* For Black MIDI's - forces release time to 50 ms
|
|
272
272
|
* @param {boolean} value
|
|
273
273
|
*/
|
|
274
274
|
set highPerformanceMode(value)
|
|
@@ -325,8 +325,8 @@ export class Synthetizer
|
|
|
325
325
|
|
|
326
326
|
/**
|
|
327
327
|
* Sets the interpolation type for the synthesizer:
|
|
328
|
-
* 0 - linear
|
|
329
|
-
* 1 - nearest neighbor
|
|
328
|
+
* 0. - linear
|
|
329
|
+
* 1. - nearest neighbor
|
|
330
330
|
* @param type {interpolationTypes}
|
|
331
331
|
*/
|
|
332
332
|
setInterpolationType(type)
|
|
@@ -663,7 +663,7 @@ export class Synthetizer
|
|
|
663
663
|
|
|
664
664
|
/**
|
|
665
665
|
* Sets the master stereo panning
|
|
666
|
-
* @param pan {number} -1 to 1, the pan (-1 is left, 0 is midde, 1 is right)
|
|
666
|
+
* @param pan {number} (-1 to 1), the pan (-1 is left, 0 is midde, 1 is right)
|
|
667
667
|
*/
|
|
668
668
|
setMasterPan(pan)
|
|
669
669
|
{
|
|
@@ -691,7 +691,8 @@ export class Synthetizer
|
|
|
691
691
|
* Changes the patch for a given channel
|
|
692
692
|
* @param channel {number} usually 0-15: the channel to change
|
|
693
693
|
* @param programNumber {number} 0-127 the MIDI patch number
|
|
694
|
-
* @param userChange {boolean} indicates if
|
|
694
|
+
* @param userChange {boolean} indicates if user has called the program change.
|
|
695
|
+
* defaults to false
|
|
695
696
|
*/
|
|
696
697
|
programChange(channel, programNumber, userChange = false)
|
|
697
698
|
{
|
|
@@ -912,6 +913,7 @@ export class Synthetizer
|
|
|
912
913
|
for (let i = 0; i < this.channelsAmount; i++)
|
|
913
914
|
{
|
|
914
915
|
this.controllerChange(i, midiControllers.reverbDepth, 127);
|
|
916
|
+
this.lockController(i, midiControllers.reverbDepth, true);
|
|
915
917
|
}
|
|
916
918
|
return "That's the spirit!";
|
|
917
919
|
}
|