spessasynth_core 3.27.5 → 3.27.7
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 +4 -3
- package/package.json +1 -1
- package/src/midi/basic_midi.js +23 -6
- package/src/midi/midi_loader.js +2 -5
- package/src/midi/midi_sequence.js +8 -2
- package/src/midi/midi_tools/midi_editor.js +1 -1
- package/src/midi/midi_tools/rmidi_writer.js +1 -1
- package/src/midi/midi_tools/used_keys_loaded.js +2 -2
- package/src/midi/xmf_loader.js +1 -1
- package/src/sequencer/sequencer_engine.js +2 -2
- package/src/sequencer/song_control.js +1 -1
- package/src/soundfont/basic_soundfont/basic_soundbank.js +2 -2
- package/src/soundfont/basic_soundfont/generator.js +4 -1
- package/src/soundfont/basic_soundfont/write_dls/combine_zones.js +1 -1
- package/src/soundfont/load_soundfont.js +1 -1
- package/src/soundfont/read_sf2/soundfont.js +1 -1
- package/src/synthetizer/audio_engine/engine_components/soundfont_manager.js +1 -1
- package/src/utils/byte_functions/string.js +16 -27
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ npm install --save spessasynth_core
|
|
|
66
66
|
- **Soundfont manager:** Stack multiple soundfonts!
|
|
67
67
|
- **Unlimited channel count:** Your CPU is the limit!
|
|
68
68
|
- **Excellent MIDI Standards Support:**
|
|
69
|
-
- **MIDI Controller Support:** Default supported controllers [here](https://github.com/spessasus/spessasynth_core/wiki/MIDI-Implementation#supported-controllers)
|
|
69
|
+
- **MIDI Controller Support:** Default supported controllers [here](https://github.com/spessasus/spessasynth_core/wiki/MIDI-Implementation#default-supported-controllers)
|
|
70
70
|
- **Portamento Support:** Glide the notes!
|
|
71
71
|
- **Sound Controllers:** Real-time filter and envelope control!
|
|
72
72
|
- **MIDI Tuning Standard Support:** [more info here](https://github.com/spessasus/spessasynth_core/wiki/MIDI-Implementation#midi-tuning-standard)
|
|
@@ -110,7 +110,7 @@ npm install --save spessasynth_core
|
|
|
110
110
|
- **Easy info access:** Just an [object of strings!](https://github.com/spessasus/spessasynth_core/wiki/SoundFont2-Class#soundfontinfo)
|
|
111
111
|
- **Smart trimming:** Trim the SoundFont to only include samples used in the MIDI *(down to key and velocity!)*
|
|
112
112
|
- **sf3 conversion:** Compress SoundFont2 files to SoundFont3 with variable quality!
|
|
113
|
-
- **Easy saving:** Also just [one function!](https://github.com/spessasus/spessasynth_core/wiki/
|
|
113
|
+
- **Easy saving:** Also just [one function!](https://github.com/spessasus/spessasynth_core/wiki/Sound-Bank-Parser#write)
|
|
114
114
|
|
|
115
115
|
#### Read and write SoundFont3 files
|
|
116
116
|
- Same features as SoundFont2 but with now with **Ogg Vorbis compression!**
|
|
@@ -119,7 +119,8 @@ npm install --save spessasynth_core
|
|
|
119
119
|
|
|
120
120
|
#### Read and write DLS Level One or Two files
|
|
121
121
|
- Read DLS (DownLoadable Sounds) files as SF2 files!
|
|
122
|
-
- **Works like a normal soundfont:** *Saving it as sf2 is still [just one function!](https://github.com/spessasus/spessasynth_core/wiki/
|
|
122
|
+
- **Works like a normal soundfont:** *Saving it as sf2 is still [just one function!](https://github.com/spessasus/spessasynth_core/wiki/Sound-Bank-Parser#write)*
|
|
123
|
+
- *That's right, saving as DLS is also [just one function!](https://github.com/spessasus/spessasynth_core/wiki/Sound-Bank-Parser#writedls)*
|
|
123
124
|
- Converts articulators to both **modulators** and **generators**!
|
|
124
125
|
- Works with both unsigned 8-bit samples and signed 16-bit samples!
|
|
125
126
|
- A-Law encoding support
|
package/package.json
CHANGED
package/src/midi/basic_midi.js
CHANGED
|
@@ -41,7 +41,7 @@ class BasicMIDI extends MIDISequenceData
|
|
|
41
41
|
isDLSRMIDI = false;
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* Copies a MIDI
|
|
44
|
+
* Copies a MIDI (tracks are shallowly copied!)
|
|
45
45
|
* @param mid {BasicMIDI}
|
|
46
46
|
* @returns {BasicMIDI}
|
|
47
47
|
*/
|
|
@@ -51,9 +51,27 @@ class BasicMIDI extends MIDISequenceData
|
|
|
51
51
|
m._copyFromSequence(mid);
|
|
52
52
|
|
|
53
53
|
m.isDLSRMIDI = mid.isDLSRMIDI;
|
|
54
|
-
m.embeddedSoundFont = mid
|
|
54
|
+
m.embeddedSoundFont = mid?.embeddedSoundFont ? mid.embeddedSoundFont : undefined; // Shallow copy
|
|
55
55
|
m.tracks = mid.tracks.map(track => [...track]); // Shallow copy of each track array
|
|
56
|
-
|
|
56
|
+
return m;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Copies a MIDI with deep copy
|
|
61
|
+
* @param mid {BasicMIDI}
|
|
62
|
+
* @returns {BasicMIDI}
|
|
63
|
+
*/
|
|
64
|
+
static copyFromDeep(mid)
|
|
65
|
+
{
|
|
66
|
+
const m = new BasicMIDI();
|
|
67
|
+
m._copyFromSequence(mid);
|
|
68
|
+
m.isDLSRMIDI = mid.isDLSRMIDI;
|
|
69
|
+
m.embeddedSoundFont = mid.embeddedSoundFont ? mid.embeddedSoundFont.slice(0) : undefined; // Deep copy
|
|
70
|
+
m.tracks = mid.tracks.map(track => track.map(event => new MIDIMessage(
|
|
71
|
+
event.ticks,
|
|
72
|
+
event.messageStatusByte,
|
|
73
|
+
event.messageData
|
|
74
|
+
))); // Deep copy
|
|
57
75
|
return m;
|
|
58
76
|
}
|
|
59
77
|
|
|
@@ -217,7 +235,6 @@ class BasicMIDI extends MIDISequenceData
|
|
|
217
235
|
copyrightComponents.push(readBytesAsString(
|
|
218
236
|
e.messageData,
|
|
219
237
|
e.messageData.length,
|
|
220
|
-
undefined,
|
|
221
238
|
false
|
|
222
239
|
));
|
|
223
240
|
e.messageData.currentIndex = 0;
|
|
@@ -466,7 +483,7 @@ class BasicMIDI extends MIDISequenceData
|
|
|
466
483
|
{
|
|
467
484
|
this.rawMidiName = name.messageData;
|
|
468
485
|
name.messageData.currentIndex = 0;
|
|
469
|
-
this.midiName = readBytesAsString(name.messageData, name.messageData.length,
|
|
486
|
+
this.midiName = readBytesAsString(name.messageData, name.messageData.length, false);
|
|
470
487
|
}
|
|
471
488
|
}
|
|
472
489
|
}
|
|
@@ -478,7 +495,7 @@ class BasicMIDI extends MIDISequenceData
|
|
|
478
495
|
{
|
|
479
496
|
this.rawMidiName = name.messageData;
|
|
480
497
|
name.messageData.currentIndex = 0;
|
|
481
|
-
this.midiName = readBytesAsString(name.messageData, name.messageData.length,
|
|
498
|
+
this.midiName = readBytesAsString(name.messageData, name.messageData.length, false);
|
|
482
499
|
}
|
|
483
500
|
}
|
|
484
501
|
}
|
package/src/midi/midi_loader.js
CHANGED
|
@@ -45,7 +45,7 @@ class MIDI extends BasicMIDI
|
|
|
45
45
|
// possibly an RMID file (https://github.com/spessasus/sf2-rmidi-specification#readme)
|
|
46
46
|
// skip size
|
|
47
47
|
binaryData.currentIndex += 8;
|
|
48
|
-
const rmid = readBytesAsString(binaryData, 4,
|
|
48
|
+
const rmid = readBytesAsString(binaryData, 4, false);
|
|
49
49
|
if (rmid !== "RMID")
|
|
50
50
|
{
|
|
51
51
|
SpessaSynthGroupEnd();
|
|
@@ -101,18 +101,15 @@ class MIDI extends BasicMIDI
|
|
|
101
101
|
this.copyright = readBytesAsString(
|
|
102
102
|
this.RMIDInfo["ICOP"],
|
|
103
103
|
this.RMIDInfo["ICOP"].length,
|
|
104
|
-
undefined,
|
|
105
104
|
false
|
|
106
105
|
).replaceAll("\n", " ");
|
|
107
106
|
}
|
|
108
107
|
if (this.RMIDInfo["INAM"])
|
|
109
108
|
{
|
|
110
109
|
this.rawMidiName = this.RMIDInfo[RMIDINFOChunks.name];
|
|
111
|
-
// noinspection JSCheckFunctionSignatures
|
|
112
110
|
this.midiName = readBytesAsString(
|
|
113
|
-
this.rawMidiName,
|
|
111
|
+
/** @type {IndexedByteArray}*/this.rawMidiName,
|
|
114
112
|
this.rawMidiName.length,
|
|
115
|
-
undefined,
|
|
116
113
|
false
|
|
117
114
|
).replaceAll("\n", " ");
|
|
118
115
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { IndexedByteArray } from "../utils/indexed_array.js";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* This is the base type for MIDI files. It contains all the "metadata" and information.
|
|
3
5
|
* It extends to:
|
|
@@ -133,7 +135,7 @@ class MIDISequenceData
|
|
|
133
135
|
* The RMID (Resource-Interchangeable MIDI) info data, if the file is RMID formatted.
|
|
134
136
|
* Otherwise, this field is undefined.
|
|
135
137
|
* Chunk type (e.g. "INAM"): Chunk data as a binary array.
|
|
136
|
-
* @type {
|
|
138
|
+
* @type {Record<string, IndexedByteArray>}
|
|
137
139
|
*/
|
|
138
140
|
RMIDInfo = {};
|
|
139
141
|
|
|
@@ -217,7 +219,11 @@ class MIDISequenceData
|
|
|
217
219
|
// copying objects
|
|
218
220
|
this.loop = { ...sequence.loop };
|
|
219
221
|
this.keyRange = { ...sequence.keyRange };
|
|
220
|
-
this.RMIDInfo = {
|
|
222
|
+
this.RMIDInfo = {};
|
|
223
|
+
for (const [key, value] of Object.entries(sequence.RMIDInfo))
|
|
224
|
+
{
|
|
225
|
+
this.RMIDInfo[key] = new IndexedByteArray(value);
|
|
226
|
+
}
|
|
221
227
|
}
|
|
222
228
|
}
|
|
223
229
|
|
|
@@ -180,7 +180,7 @@ export function modifyMIDI(
|
|
|
180
180
|
const midiPorts = midi.midiPorts.slice();
|
|
181
181
|
/**
|
|
182
182
|
* midi port: channel offset
|
|
183
|
-
* @type {
|
|
183
|
+
* @type {Record<number, number>}
|
|
184
184
|
*/
|
|
185
185
|
const midiPortChannelOffsets = {};
|
|
186
186
|
let midiPortChannelOffset = 0;
|
|
@@ -48,7 +48,7 @@ const DEFAULT_COPYRIGHT = "Created using SpessaSynth";
|
|
|
48
48
|
*/
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* Writes an RMIDI file
|
|
51
|
+
* Writes an RMIDI file. Note that this method modifies the MIDI file in-place.
|
|
52
52
|
* @this {BasicMIDI}
|
|
53
53
|
* @param soundfontBinary {Uint8Array}
|
|
54
54
|
* @param soundfont {BasicSoundBank}
|
|
@@ -10,7 +10,7 @@ import { SoundFontManager } from "../../synthetizer/audio_engine/engine_componen
|
|
|
10
10
|
* Gets the used programs and keys for this MIDI file with a given sound bank
|
|
11
11
|
* @this {BasicMIDI}
|
|
12
12
|
* @param soundfont {SoundFontManager|BasicSoundBank} - the sound bank
|
|
13
|
-
* @returns {
|
|
13
|
+
* @returns {Record<string, Set<string>>} Record<bank:program, Set<key-velocity>>
|
|
14
14
|
*/
|
|
15
15
|
export function getUsedProgramsAndKeys(soundfont)
|
|
16
16
|
{
|
|
@@ -81,7 +81,7 @@ export function getUsedProgramsAndKeys(soundfont)
|
|
|
81
81
|
/**
|
|
82
82
|
* find all programs used and key-velocity combos in them
|
|
83
83
|
* bank:program each has a set of midiNote-velocity
|
|
84
|
-
* @type {
|
|
84
|
+
* @type {Record<string, Set<string>>}
|
|
85
85
|
*/
|
|
86
86
|
const usedProgramsAndKeys = {};
|
|
87
87
|
|
package/src/midi/xmf_loader.js
CHANGED
|
@@ -111,8 +111,8 @@ class SpessaSynthSequencer
|
|
|
111
111
|
midiPortChannelOffset = 0;
|
|
112
112
|
/**
|
|
113
113
|
* stored as:
|
|
114
|
-
*
|
|
115
|
-
* @type {
|
|
114
|
+
* Record<midi port, channel offset>
|
|
115
|
+
* @type {Record<number, number>}
|
|
116
116
|
*/
|
|
117
117
|
midiPortChannelOffsets = {};
|
|
118
118
|
|
|
@@ -147,7 +147,7 @@ export function loadNewSongList(midiBuffers, autoPlay = true)
|
|
|
147
147
|
* parse the MIDIs (only the array buffers, MIDI is unchanged)
|
|
148
148
|
* @type {BasicMIDI[]}
|
|
149
149
|
*/
|
|
150
|
-
this.songs = midiBuffers
|
|
150
|
+
this.songs = midiBuffers;
|
|
151
151
|
if (this.songs.length < 1)
|
|
152
152
|
{
|
|
153
153
|
return;
|
|
@@ -32,7 +32,7 @@ class BasicSoundBank
|
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Soundfont's info stored as name: value. ifil and iver are stored as string representation of float (e.g., 2.1)
|
|
35
|
-
* @type {
|
|
35
|
+
* @type {Record<string, string|IndexedByteArray>}
|
|
36
36
|
*/
|
|
37
37
|
soundFontInfo = {};
|
|
38
38
|
|
|
@@ -77,7 +77,7 @@ class BasicSoundBank
|
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
79
|
* Creates a new basic soundfont template (or copies)
|
|
80
|
-
* @param data {undefined|{presets: BasicPreset[], info:
|
|
80
|
+
* @param data {undefined|{presets: BasicPreset[], info: Record<string, string>}}
|
|
81
81
|
*/
|
|
82
82
|
constructor(data = undefined)
|
|
83
83
|
{
|
|
@@ -69,5 +69,8 @@ export function addAndClampGenerator(generatorType, presetGens, instrumentGens)
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// limits are applied in the compute_modulator function
|
|
72
|
-
|
|
72
|
+
// clamp to prevent short from overflowing
|
|
73
|
+
// testcase: Sega Genesis soundfont (spessasynth/#169) adds 20,999 and the default 13,500 to initialFilterFc
|
|
74
|
+
// which is more than 32k
|
|
75
|
+
return Math.max(-32767, Math.min(32767, instruValue + presetValue));
|
|
73
76
|
}
|
|
@@ -234,7 +234,7 @@ export function combineZones(preset, globalize = true)
|
|
|
234
234
|
continue;
|
|
235
235
|
}
|
|
236
236
|
/**
|
|
237
|
-
* @type {
|
|
237
|
+
* @type {Record<string, number>}
|
|
238
238
|
*/
|
|
239
239
|
let occurencesForValues = {};
|
|
240
240
|
const defaultForChecked = generatorLimits[checkedType]?.def || 0;
|
|
@@ -12,7 +12,7 @@ export function loadSoundFont(buffer)
|
|
|
12
12
|
{
|
|
13
13
|
const check = buffer.slice(8, 12);
|
|
14
14
|
const a = new IndexedByteArray(check);
|
|
15
|
-
const id = readBytesAsString(a, 4,
|
|
15
|
+
const id = readBytesAsString(a, 4, false).toLowerCase();
|
|
16
16
|
if (id === "dls ")
|
|
17
17
|
{
|
|
18
18
|
return new DLSSoundFont(buffer);
|
|
@@ -101,7 +101,7 @@ export class SoundFont2 extends BasicSoundBank
|
|
|
101
101
|
break;
|
|
102
102
|
|
|
103
103
|
case "icmt":
|
|
104
|
-
text = readBytesAsString(chunk.chunkData, chunk.chunkData.length,
|
|
104
|
+
text = readBytesAsString(chunk.chunkData, chunk.chunkData.length, false);
|
|
105
105
|
this.soundFontInfo[chunk.header] = text;
|
|
106
106
|
break;
|
|
107
107
|
|
|
@@ -3,50 +3,39 @@ import { IndexedByteArray } from "../indexed_array.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* @param dataArray {IndexedByteArray}
|
|
5
5
|
* @param bytes {number}
|
|
6
|
-
* @param encoding {string} the textElement encoding
|
|
7
6
|
* @param trimEnd {boolean} if we should trim once we reach an invalid byte
|
|
8
7
|
* @returns {string}
|
|
9
8
|
*/
|
|
10
|
-
export function readBytesAsString(dataArray, bytes,
|
|
9
|
+
export function readBytesAsString(dataArray, bytes, trimEnd = true)
|
|
11
10
|
{
|
|
12
|
-
|
|
11
|
+
let finished = false;
|
|
12
|
+
let string = "";
|
|
13
|
+
for (let i = 0; i < bytes; i++)
|
|
13
14
|
{
|
|
14
|
-
let
|
|
15
|
-
|
|
16
|
-
for (let i = 0; i < bytes; i++)
|
|
15
|
+
let byte = dataArray[dataArray.currentIndex++];
|
|
16
|
+
if (finished)
|
|
17
17
|
{
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if ((byte < 32 || byte > 127) && byte !== 10) // 10 is "\n"
|
|
21
|
+
{
|
|
22
|
+
if (trimEnd)
|
|
20
23
|
{
|
|
24
|
+
finished = true;
|
|
21
25
|
continue;
|
|
22
26
|
}
|
|
23
|
-
|
|
27
|
+
else
|
|
24
28
|
{
|
|
25
|
-
if (
|
|
29
|
+
if (byte === 0)
|
|
26
30
|
{
|
|
27
31
|
finished = true;
|
|
28
32
|
continue;
|
|
29
33
|
}
|
|
30
|
-
else
|
|
31
|
-
{
|
|
32
|
-
if (byte === 0)
|
|
33
|
-
{
|
|
34
|
-
finished = true;
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
34
|
}
|
|
39
|
-
string += String.fromCharCode(byte);
|
|
40
35
|
}
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
else
|
|
44
|
-
{
|
|
45
|
-
let byteBuffer = dataArray.slice(dataArray.currentIndex, dataArray.currentIndex + bytes);
|
|
46
|
-
dataArray.currentIndex += bytes;
|
|
47
|
-
let decoder = new TextDecoder(encoding.replace(/[^\x20-\x7E]/g, ""));
|
|
48
|
-
return decoder.decode(byteBuffer.buffer);
|
|
36
|
+
string += String.fromCharCode(byte);
|
|
49
37
|
}
|
|
38
|
+
return string;
|
|
50
39
|
}
|
|
51
40
|
|
|
52
41
|
/**
|