spessasynth_lib 3.16.4 → 3.17.0
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 +5 -4
- package/@types/midi_parser/basic_midi.d.ts +125 -0
- package/@types/midi_parser/midi_builder.d.ts +69 -0
- package/@types/midi_parser/midi_data.d.ts +2 -2
- package/@types/midi_parser/midi_editor.d.ts +4 -4
- package/@types/midi_parser/midi_loader.d.ts +3 -100
- package/@types/midi_parser/midi_writer.d.ts +2 -2
- package/@types/midi_parser/rmidi_writer.d.ts +4 -3
- package/@types/midi_parser/used_keys_loaded.d.ts +2 -2
- package/@types/sequencer/sequencer.d.ts +1 -1
- package/@types/soundfont/basic_soundfont/basic_soundfont.d.ts +5 -4
- package/@types/soundfont/basic_soundfont/write_sf2/soundfont_trimmer.d.ts +2 -2
- package/@types/soundfont/dls/dls_preset.d.ts +11 -0
- package/@types/soundfont/dls/dls_soundfont.d.ts +24 -0
- package/@types/soundfont/dls/read_instrument.d.ts +5 -0
- package/@types/soundfont/dls/read_instrument_list.d.ts +5 -0
- package/@types/soundfont/load_soundfont.d.ts +6 -0
- package/@types/soundfont/soundfont.d.ts +2 -1
- package/@types/synthetizer/synthetizer.d.ts +2 -2
- package/@types/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.d.ts +2 -2
- package/@types/utils/byte_functions/little_endian.d.ts +1 -1
- package/README.md +17 -15
- package/index.js +6 -4
- package/midi_parser/basic_midi.js +146 -0
- package/midi_parser/midi_builder.js +281 -0
- package/midi_parser/midi_data.js +1 -1
- package/midi_parser/midi_editor.js +2 -2
- package/midi_parser/midi_loader.js +17 -53
- package/midi_parser/midi_writer.js +1 -1
- package/midi_parser/rmidi_writer.js +227 -246
- package/midi_parser/used_keys_loaded.js +1 -1
- package/package.json +1 -1
- package/sequencer/sequencer.js +1 -1
- package/sequencer/worklet_sequencer/song_control.js +3 -3
- package/sequencer/worklet_sequencer/worklet_sequencer.js +1 -1
- package/soundfont/basic_soundfont/basic_soundfont.js +25 -10
- package/soundfont/basic_soundfont/riff_chunk.js +2 -2
- package/soundfont/basic_soundfont/write_sf2/soundfont_trimmer.js +1 -1
- package/soundfont/dls/dls_preset.js +25 -0
- package/soundfont/dls/dls_soundfont.js +93 -0
- package/soundfont/dls/read_instrument.js +22 -0
- package/soundfont/dls/read_instrument_list.js +17 -0
- package/soundfont/load_soundfont.js +21 -0
- package/soundfont/read_sf2/instruments.js +2 -2
- package/soundfont/read_sf2/modulators.js +5 -5
- package/soundfont/read_sf2/presets.js +7 -7
- package/soundfont/read_sf2/samples.js +8 -8
- package/soundfont/read_sf2/zones.js +5 -5
- package/soundfont/soundfont.js +8 -3
- package/synthetizer/synthetizer.js +1 -1
- package/synthetizer/worklet_processor.min.js +7 -6
- package/synthetizer/worklet_system/main_processor.js +1 -2
- package/synthetizer/worklet_system/worklet_methods/program_control.js +12 -20
- package/synthetizer/worklet_system/worklet_methods/worklet_soundfont_manager/worklet_soundfont_manager.js +5 -5
- package/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +1 -1
- package/utils/byte_functions/little_endian.js +1 -1
- /package/@types/{midi_handler → external_midi}/midi_handler.d.ts +0 -0
- /package/@types/{midi_handler → external_midi}/web_midi_link.d.ts +0 -0
- /package/{midi_handler → external_midi}/README.md +0 -0
- /package/{midi_handler → external_midi}/midi_handler.js +0 -0
- /package/{midi_handler → external_midi}/web_midi_link.js +0 -0
package/README.md
CHANGED
|
@@ -36,24 +36,24 @@ document.getElementById("button").onclick = async () => {
|
|
|
36
36
|
### Powerful SoundFont Synthesizer
|
|
37
37
|
- Suitable for both **real-time** and **offline** synthesis
|
|
38
38
|
- **Excellent SoundFont support:**
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
- **Generator Support**
|
|
40
|
+
- **Modulator Support:** *First (to my knowledge) JavaScript SoundFont synth with that feature!*
|
|
41
|
+
- **SoundFont3 Support:** Play compressed SoundFonts!
|
|
42
|
+
- **Experimental SF2Pack Support:** Play soundfonts compressed with BASSMIDI! (*Note: only works with vorbis compression*)
|
|
43
|
+
- **Can load very large SoundFonts:** up to 4GB! *Note: Only Firefox handles this well; Chromium has a hard-coded memory limit*
|
|
44
|
+
- **Soundfont manager:** Stack multiple soundfonts!
|
|
44
45
|
- **Reverb and chorus support:** [customizable!](https://github.com/spessasus/SpessaSynth/wiki/Synthetizer-Class#effects-configuration-object)
|
|
45
46
|
- **Export audio files** using [OfflineAudioContext](https://developer.mozilla.org/en-US/docs/Web/API/OfflineAudioContext)
|
|
46
47
|
- **[Custom modulators for additional controllers](https://github.com/spessasus/SpessaSynth/wiki/Modulator-Class#default-modulators):** Why not?
|
|
47
48
|
- **Written using AudioWorklets:**
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
- Runs in a **separate thread** for maximum performance
|
|
50
|
+
- Supported by all modern browsers
|
|
50
51
|
- **Unlimited channel count:** Your CPU is the limit!
|
|
51
52
|
- **Excellent MIDI Standards Support:**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
- Supports some [**Roland GS** and **Yamaha XG** system exclusives](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-system-exclusives)
|
|
53
|
+
- **MIDI Controller Support:** Default supported controllers [here](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-controllers)
|
|
54
|
+
- **MIDI Tuning Standard Support:** [more info here](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#midi-tuning-standard)
|
|
55
|
+
- [Full **RPN** and limited **NRPN** support](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-registered-parameters)
|
|
56
|
+
- Supports some [**Roland GS** and **Yamaha XG** system exclusives](https://github.com/spessasus/SpessaSynth/wiki/MIDI-Implementation#supported-system-exclusives)
|
|
57
57
|
|
|
58
58
|
- **High-performance mode:** Play Rush E! _note: may kill your browser ;)_
|
|
59
59
|
|
|
@@ -76,8 +76,9 @@ document.getElementById("button").onclick = async () => {
|
|
|
76
76
|
- **Easy MIDI editing:** Use [helper functions](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#modifymidi) to modify the song to your needs!
|
|
77
77
|
- **Loop detection:** Automatically detects loops in MIDIs (e.g., from _Touhou Project_)
|
|
78
78
|
- **First note detection:** Skip unnecessary silence at the start by jumping to the first note!
|
|
79
|
+
- **[Write MIDI files from scratch](https://github.com/spessasus/SpessaSynth/wiki/Creating-MIDI-Files.md)**
|
|
79
80
|
- **Easy saving:** Save with just [one function!](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#writemidifile)
|
|
80
|
-
|
|
81
|
+
|
|
81
82
|
#### Read and write [RMID files with embedded SF2 soundfonts](https://github.com/spessasus/sf2-rmidi-specification#readme)
|
|
82
83
|
- **[Level 4](https://github.com/spessasus/sf2-rmidi-specification#level-4) compliance:** Reads and writes *everything!*
|
|
83
84
|
- **Compression and trimming support:** Reduce a MIDI file with a 1GB soundfont to **as small as 5MB**!
|
|
@@ -85,16 +86,17 @@ document.getElementById("button").onclick = async () => {
|
|
|
85
86
|
- **Metadata support:** Add title, artist, album name and cover and more! And of course read them too! *(In any encoding!)*
|
|
86
87
|
- **Compatible with [Falcosoft Midi Player 6!](https://falcosoft.hu/softwares.html#midiplayer)**
|
|
87
88
|
- **Easy saving:** [As simple as saving a MIDI file!](https://github.com/spessasus/SpessaSynth/wiki/Writing-MIDI-Files#writermidi)
|
|
88
|
-
|
|
89
|
+
|
|
89
90
|
#### Read and write SoundFont2 files
|
|
90
91
|
- **Easy info access:** Just an [object of strings!](https://github.com/spessasus/SpessaSynth/wiki/SoundFont2-Class#soundfontinfo)
|
|
91
92
|
- **Smart trimming:** Trim the SoundFont to only include samples used in the MIDI *(down to key and velocity!)*
|
|
92
93
|
- **sf3 conversion:** Compress SoundFont2 files to SoundFont3 with variable quality!
|
|
93
94
|
- **Easy saving:** Also just [one function!](https://github.com/spessasus/SpessaSynth/wiki/SoundFont2-Class#write)
|
|
94
|
-
|
|
95
|
+
|
|
95
96
|
#### Read and write SoundFont3 files
|
|
96
97
|
- Same features as SoundFont2 but with now with **Ogg Vorbis compression!**
|
|
97
98
|
- **Variable compression quality:** You choose between file size and quality!
|
|
98
99
|
- **Compression preserving:** Avoid decompressing and recompressing uncompressed samples for minimal quality loss!
|
|
100
|
+
|
|
99
101
|
## License
|
|
100
102
|
MIT License, except for the stbvorbis_sync.js in the `externals` folder which is licensed under the Apache-2.0 license.
|
package/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Import modules
|
|
2
|
-
import {
|
|
2
|
+
import { loadSoundFont } from "./soundfont/load_soundfont.js";
|
|
3
3
|
import { MIDI } from './midi_parser/midi_loader.js';
|
|
4
|
+
import { MIDIBuilder } from "./midi_parser/midi_builder.js";
|
|
4
5
|
import { Synthetizer, VOICE_CAP, DEFAULT_PERCUSSION } from './synthetizer/synthetizer.js';
|
|
5
6
|
import { Sequencer } from './sequencer/sequencer.js';
|
|
6
7
|
import { IndexedByteArray } from './utils/indexed_array.js';
|
|
@@ -18,8 +19,8 @@ import {
|
|
|
18
19
|
SpessaSynthGroup
|
|
19
20
|
} from './utils/loggin.js';
|
|
20
21
|
import { midiControllers, messageTypes } from './midi_parser/midi_message.js';
|
|
21
|
-
import { MIDIDeviceHandler
|
|
22
|
-
import { WebMidiLinkHandler
|
|
22
|
+
import { MIDIDeviceHandler} from "./external_midi/midi_handler.js";
|
|
23
|
+
import { WebMidiLinkHandler} from "./external_midi/web_midi_link.js";
|
|
23
24
|
import { formatTime, formatTitle, consoleColors, arrayToHexString } from './utils/other.js';
|
|
24
25
|
import { readBytesAsUintBigEndian } from './utils/byte_functions/big_endian.js';
|
|
25
26
|
import { NON_CC_INDEX_OFFSET } from './synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js';
|
|
@@ -38,13 +39,14 @@ export {
|
|
|
38
39
|
VOICE_CAP,
|
|
39
40
|
|
|
40
41
|
// SoundFont
|
|
41
|
-
|
|
42
|
+
loadSoundFont,
|
|
42
43
|
trimSoundfont,
|
|
43
44
|
modulatorSources,
|
|
44
45
|
encodeVorbis,
|
|
45
46
|
|
|
46
47
|
// MIDI
|
|
47
48
|
MIDI,
|
|
49
|
+
MIDIBuilder,
|
|
48
50
|
IndexedByteArray,
|
|
49
51
|
writeMIDIFile,
|
|
50
52
|
writeRMIDI,
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export class BasicMIDI
|
|
2
|
+
{
|
|
3
|
+
constructor()
|
|
4
|
+
{
|
|
5
|
+
/**
|
|
6
|
+
* The time division of the sequence
|
|
7
|
+
* @type {number}
|
|
8
|
+
*/
|
|
9
|
+
this.timeDivision = 0;
|
|
10
|
+
/**
|
|
11
|
+
* The duration of the sequence, in seconds
|
|
12
|
+
* @type {number}
|
|
13
|
+
*/
|
|
14
|
+
this.duration = 0;
|
|
15
|
+
/**
|
|
16
|
+
* The tempo changes in the sequence, ordered from last to first
|
|
17
|
+
* @type {{ticks: number, tempo: number}[]}
|
|
18
|
+
*/
|
|
19
|
+
this.tempoChanges = [{ticks: 0, tempo: 120}];
|
|
20
|
+
/**
|
|
21
|
+
* Contains the copyright strings
|
|
22
|
+
* @type {string}
|
|
23
|
+
*/
|
|
24
|
+
this.copyright = "";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The amount of tracks in the sequence
|
|
28
|
+
* @type {number}
|
|
29
|
+
*/
|
|
30
|
+
this.tracksAmount = 0;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The lyrics of the sequence as binary chunks
|
|
34
|
+
* @type {Uint8Array[]}
|
|
35
|
+
*/
|
|
36
|
+
this.lyrics = [];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* First note on of the MIDI file
|
|
40
|
+
* @type {number}
|
|
41
|
+
*/
|
|
42
|
+
this.firstNoteOn = 0;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The MIDI's key range
|
|
46
|
+
* @type {{min: number, max: number}}
|
|
47
|
+
*/
|
|
48
|
+
this.keyRange = { min: 0, max: 127 };
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The last voice (note on, off, cc change etc.) event tick
|
|
52
|
+
* @type {number}
|
|
53
|
+
*/
|
|
54
|
+
this.lastVoiceEventTick = 0;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Midi port numbers for each track
|
|
58
|
+
* @type {number[]}
|
|
59
|
+
*/
|
|
60
|
+
this.midiPorts = [0];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Channel offsets for each port, using the SpessaSynth method
|
|
64
|
+
* @type {number[]}
|
|
65
|
+
*/
|
|
66
|
+
this.midiPortChannelOffsets = [0];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* All channels that each track uses
|
|
70
|
+
* @type {Set<number>[]}
|
|
71
|
+
*/
|
|
72
|
+
this.usedChannelsOnTrack = [];
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The loop points (in ticks) of the sequence
|
|
76
|
+
* @type {{start: number, end: number}}
|
|
77
|
+
*/
|
|
78
|
+
this.loop = { start: 0, end: 0 };
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The sequence's name
|
|
82
|
+
* @type {string}
|
|
83
|
+
*/
|
|
84
|
+
this.midiName = "";
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The file name of the sequence, if provided in the MIDI class
|
|
88
|
+
* @type {string}
|
|
89
|
+
*/
|
|
90
|
+
this.fileName = "";
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The raw, encoded MIDI name.
|
|
94
|
+
* @type {Uint8Array}
|
|
95
|
+
*/
|
|
96
|
+
this.rawMidiName = undefined;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The MIDI's embedded soundfont
|
|
100
|
+
* @type {ArrayBuffer|undefined}
|
|
101
|
+
*/
|
|
102
|
+
this.embeddedSoundFont = undefined;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The MIDI file's format
|
|
106
|
+
* @type {number}
|
|
107
|
+
*/
|
|
108
|
+
this.format = 0;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The RMID Info data if RMID, otherwise undefined
|
|
112
|
+
* @type {Object<string, IndexedByteArray>}
|
|
113
|
+
*/
|
|
114
|
+
this.RMIDInfo = {};
|
|
115
|
+
/**
|
|
116
|
+
* The bank offset for RMIDI
|
|
117
|
+
* @type {number}
|
|
118
|
+
*/
|
|
119
|
+
this.bankOffset = 0;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The actual track data of the MIDI file
|
|
123
|
+
* @type {MidiMessage[][]}
|
|
124
|
+
*/
|
|
125
|
+
this.tracks = [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Coverts ticks to time in seconds
|
|
130
|
+
* @param ticks {number}
|
|
131
|
+
* @returns {number}
|
|
132
|
+
* @protected
|
|
133
|
+
*/
|
|
134
|
+
_ticksToSeconds(ticks)
|
|
135
|
+
{
|
|
136
|
+
if (ticks <= 0) {
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// find the last tempo change that has occured
|
|
141
|
+
let tempo = this.tempoChanges.find(v => v.ticks < ticks);
|
|
142
|
+
|
|
143
|
+
let timeSinceLastTempo = ticks - tempo.ticks;
|
|
144
|
+
return this._ticksToSeconds(ticks - timeSinceLastTempo) + (timeSinceLastTempo * 60) / (tempo.tempo * this.timeDivision);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { BasicMIDI } from './basic_midi.js'
|
|
2
|
+
import { messageTypes, MidiMessage } from './midi_message.js'
|
|
3
|
+
import { IndexedByteArray } from '../utils/indexed_array.js'
|
|
4
|
+
import { readBytesAsUintBigEndian } from '../utils/byte_functions/big_endian.js'
|
|
5
|
+
import { SpessaSynthWarn } from '../utils/loggin.js'
|
|
6
|
+
|
|
7
|
+
export class MIDIBuilder extends BasicMIDI
|
|
8
|
+
{
|
|
9
|
+
/**
|
|
10
|
+
* @param name {string} The MIDI's name
|
|
11
|
+
* @param timeDivision {number} the file's time division
|
|
12
|
+
* @param initialTempo {number} the file's initial tempo
|
|
13
|
+
*/
|
|
14
|
+
constructor(name, timeDivision = 480, initialTempo = 120)
|
|
15
|
+
{
|
|
16
|
+
super();
|
|
17
|
+
this.timeDivision = timeDivision;
|
|
18
|
+
this.midiName = name;
|
|
19
|
+
this.encoder = new TextEncoder();
|
|
20
|
+
this.rawMidiName = this.encoder.encode(name);
|
|
21
|
+
|
|
22
|
+
// create the first track with the file name
|
|
23
|
+
this.addNewTrack(name);
|
|
24
|
+
this.addSetTempo(0, initialTempo);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Updates all internal values
|
|
29
|
+
*/
|
|
30
|
+
flush()
|
|
31
|
+
{
|
|
32
|
+
|
|
33
|
+
// find first note on
|
|
34
|
+
const firstNoteOns = [];
|
|
35
|
+
for(const t of this.tracks)
|
|
36
|
+
{
|
|
37
|
+
// sost the track by ticks
|
|
38
|
+
t.sort((e1, e2) => e1.ticks - e2.ticks);
|
|
39
|
+
const firstNoteOn = t.find(e => (e.messageStatusByte & 0xF0) === messageTypes.noteOn);
|
|
40
|
+
if(firstNoteOn)
|
|
41
|
+
{
|
|
42
|
+
firstNoteOns.push(firstNoteOn.ticks);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
this.firstNoteOn = Math.min(...firstNoteOns);
|
|
46
|
+
|
|
47
|
+
// find tempo changes
|
|
48
|
+
// and used channels on tracks
|
|
49
|
+
// and midi ports
|
|
50
|
+
// and last voice event tick
|
|
51
|
+
// and loop
|
|
52
|
+
this.lastVoiceEventTick = 0
|
|
53
|
+
this.tempoChanges = [{ticks: 0, tempo: 120}];
|
|
54
|
+
this.midiPorts = [];
|
|
55
|
+
this.midiPortChannelOffsets = [];
|
|
56
|
+
let portOffset = 0;
|
|
57
|
+
/**
|
|
58
|
+
* @type {Set<number>[]}
|
|
59
|
+
*/
|
|
60
|
+
this.usedChannelsOnTrack = this.tracks.map(() => new Set());
|
|
61
|
+
this.tracks.forEach((t, trackNum) => {
|
|
62
|
+
this.midiPorts.push(-1);
|
|
63
|
+
t.forEach(e => {
|
|
64
|
+
// last voice event tick
|
|
65
|
+
if(e.messageStatusByte >= 0x80 && e.messageStatusByte < 0xF0)
|
|
66
|
+
{
|
|
67
|
+
if(e.ticks > this.lastVoiceEventTick)
|
|
68
|
+
{
|
|
69
|
+
this.lastVoiceEventTick = e.ticks;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// tempo, used channels, port
|
|
74
|
+
if(e.messageStatusByte === messageTypes.setTempo)
|
|
75
|
+
{
|
|
76
|
+
this.tempoChanges.push({
|
|
77
|
+
ticks: e.ticks,
|
|
78
|
+
tempo: 60000000 / readBytesAsUintBigEndian(e.messageData, 3)
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
else
|
|
82
|
+
if((e.messageStatusByte & 0xF0) === messageTypes.noteOn)
|
|
83
|
+
{
|
|
84
|
+
this.usedChannelsOnTrack[trackNum].add(e.messageData[0]);
|
|
85
|
+
}
|
|
86
|
+
else
|
|
87
|
+
if(e.messageStatusByte === messageTypes.midiPort)
|
|
88
|
+
{
|
|
89
|
+
const port = e.messageData[0];
|
|
90
|
+
this.midiPorts[trackNum] = port;
|
|
91
|
+
if(this.midiPortChannelOffsets[port] === undefined)
|
|
92
|
+
{
|
|
93
|
+
this.midiPortChannelOffsets[port] = portOffset;
|
|
94
|
+
portOffset += 16;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
this.loop = {start: this.firstNoteOn, end: this.lastVoiceEventTick};
|
|
101
|
+
|
|
102
|
+
// reverse tempo and compute duration
|
|
103
|
+
this.tempoChanges.reverse();
|
|
104
|
+
this.duration = this._ticksToSeconds(this.lastVoiceEventTick);
|
|
105
|
+
|
|
106
|
+
// fix midi ports:
|
|
107
|
+
// midi tracks without ports will have a value of -1
|
|
108
|
+
// if all ports have a value of -1, set it to 0, otherwise take the first midi port and replace all -1 with it
|
|
109
|
+
// why do this? some midis (for some reason) specify all channels to port 1 or else, but leave the conductor track with no port pref.
|
|
110
|
+
// this spessasynth to reserve the first 16 channels for the conductor track (which doesn't play anything) and use additional 16 for the actual ports.
|
|
111
|
+
let defaultP = 0;
|
|
112
|
+
for(let port of this.midiPorts)
|
|
113
|
+
{
|
|
114
|
+
if(port !== -1)
|
|
115
|
+
{
|
|
116
|
+
defaultP = port;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
this.midiPorts = this.midiPorts.map(port => port === -1 ? defaultP : port);
|
|
121
|
+
// add dummy port if empty
|
|
122
|
+
if(this.midiPortChannelOffsets.length === 0)
|
|
123
|
+
{
|
|
124
|
+
this.midiPortChannelOffsets = [0];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Adds a new "set tempo" message
|
|
130
|
+
* @param ticks {number} the tick number of the event
|
|
131
|
+
* @param tempo {number} the tempo in beats per minute (BPM)
|
|
132
|
+
*/
|
|
133
|
+
addSetTempo(ticks, tempo)
|
|
134
|
+
{
|
|
135
|
+
const array = new IndexedByteArray(3);
|
|
136
|
+
|
|
137
|
+
tempo = 60000000 / tempo;
|
|
138
|
+
|
|
139
|
+
// Extract each byte in big-endian order
|
|
140
|
+
array[0] = (tempo >> 16) & 0xFF;
|
|
141
|
+
array[1] = (tempo >> 8) & 0xFF;
|
|
142
|
+
array[2] = tempo & 0xFF;
|
|
143
|
+
|
|
144
|
+
this.addEvent(ticks, 0, messageTypes.setTempo, array);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Adds a new MIDI track
|
|
149
|
+
* @param name {string} the new track's name
|
|
150
|
+
* @param port {number} the new track's port
|
|
151
|
+
*/
|
|
152
|
+
addNewTrack(name, port = 0)
|
|
153
|
+
{
|
|
154
|
+
this.tracksAmount++;
|
|
155
|
+
if(this.tracksAmount > 1)
|
|
156
|
+
{
|
|
157
|
+
this.format = 1;
|
|
158
|
+
}
|
|
159
|
+
this.tracks.push([]);
|
|
160
|
+
this.tracks[this.tracksAmount - 1].push(
|
|
161
|
+
new MidiMessage(0, messageTypes.endOfTrack, new IndexedByteArray(0))
|
|
162
|
+
);
|
|
163
|
+
this.addEvent(0, this.tracksAmount - 1, messageTypes.trackName, this.encoder.encode(name));
|
|
164
|
+
this.addEvent(0, this.tracksAmount - 1, messageTypes.midiPort, [port]);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Adds a new MIDI Event
|
|
169
|
+
* @param ticks {number} the tick time of the event
|
|
170
|
+
* @param track {number} the track number to use
|
|
171
|
+
* @param event {number} the MIDI event number
|
|
172
|
+
* @param eventData {Uint8Array|Iterable<number>} the raw event data
|
|
173
|
+
*/
|
|
174
|
+
addEvent(ticks, track, event, eventData)
|
|
175
|
+
{
|
|
176
|
+
if(!this.tracks[track])
|
|
177
|
+
{
|
|
178
|
+
throw new Error(`Track ${track} does not exist. Add it via addTrack method.`);
|
|
179
|
+
}
|
|
180
|
+
if(event === messageTypes.endOfTrack)
|
|
181
|
+
{
|
|
182
|
+
SpessaSynthWarn("The EndOfTrack is added automatically. Ignoring!");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
// remove end of track
|
|
186
|
+
this.tracks[track].pop();
|
|
187
|
+
this.tracks[track].push(new MidiMessage(
|
|
188
|
+
ticks,
|
|
189
|
+
event,
|
|
190
|
+
new IndexedByteArray(eventData)
|
|
191
|
+
));
|
|
192
|
+
// add end of track
|
|
193
|
+
this.tracks[track].push(new MidiMessage(
|
|
194
|
+
ticks,
|
|
195
|
+
messageTypes.endOfTrack,
|
|
196
|
+
new IndexedByteArray(0)
|
|
197
|
+
));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Adds a new Note On event
|
|
202
|
+
* @param ticks {number} the tick time of the event
|
|
203
|
+
* @param track {number} the track number to use
|
|
204
|
+
* @param channel {number} the channel to use
|
|
205
|
+
* @param midiNote {number} the midi note of the keypress
|
|
206
|
+
* @param velocity {number} the velocity of the keypress
|
|
207
|
+
*/
|
|
208
|
+
addNoteOn(ticks, track, channel, midiNote, velocity)
|
|
209
|
+
{
|
|
210
|
+
channel %= 16;
|
|
211
|
+
midiNote %= 128;
|
|
212
|
+
velocity %= 128;
|
|
213
|
+
this.addEvent(
|
|
214
|
+
ticks,
|
|
215
|
+
track,
|
|
216
|
+
messageTypes.noteOn | channel,
|
|
217
|
+
[midiNote, velocity]
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Adds a new Note Off event
|
|
223
|
+
* @param ticks {number} the tick time of the event
|
|
224
|
+
* @param track {number} the track number to use
|
|
225
|
+
* @param channel {number} the channel to use
|
|
226
|
+
* @param midiNote {number} the midi note of the key release
|
|
227
|
+
*/
|
|
228
|
+
addNoteOff(ticks, track, channel, midiNote)
|
|
229
|
+
{
|
|
230
|
+
channel %= 16;
|
|
231
|
+
midiNote %= 128;
|
|
232
|
+
this.addEvent(
|
|
233
|
+
ticks,
|
|
234
|
+
track,
|
|
235
|
+
messageTypes.noteOff | channel,
|
|
236
|
+
[midiNote, 64]
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Adds a new Controller Change event
|
|
242
|
+
* @param ticks {number} the tick time of the event
|
|
243
|
+
* @param track {number} the track number to use
|
|
244
|
+
* @param channel {number} the channel to use
|
|
245
|
+
* @param controllerNumber {number} the MIDI CC to use
|
|
246
|
+
* @param controllerValue {number} the new CC value
|
|
247
|
+
*/
|
|
248
|
+
addControllerChange(ticks, track, channel, controllerNumber, controllerValue)
|
|
249
|
+
{
|
|
250
|
+
channel %= 16;
|
|
251
|
+
controllerNumber %= 128;
|
|
252
|
+
controllerValue %= 128;
|
|
253
|
+
this.addEvent(
|
|
254
|
+
ticks,
|
|
255
|
+
track,
|
|
256
|
+
messageTypes.controllerChange | channel,
|
|
257
|
+
[controllerNumber, controllerValue]
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Adds a new Pitch Wheel event
|
|
263
|
+
* @param ticks {number} the tick time of the event
|
|
264
|
+
* @param track {number} the track to use
|
|
265
|
+
* @param channel {number} the channel to use
|
|
266
|
+
* @param MSB {number} SECOND byte of the MIDI pitchWheel message
|
|
267
|
+
* @param LSB {number} FIRST byte of the MIDI pitchWheel message
|
|
268
|
+
*/
|
|
269
|
+
addPitchWheel(ticks, track, channel, MSB, LSB)
|
|
270
|
+
{
|
|
271
|
+
channel %= 16;
|
|
272
|
+
MSB %= 128;
|
|
273
|
+
LSB %= 128;
|
|
274
|
+
this.addEvent(
|
|
275
|
+
ticks,
|
|
276
|
+
track,
|
|
277
|
+
messageTypes.pitchBend | channel,
|
|
278
|
+
[LSB, MSB]
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
package/midi_parser/midi_data.js
CHANGED
|
@@ -75,7 +75,7 @@ function getDrumChange(channel, ticks)
|
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
77
|
* Allows easy editing of the file
|
|
78
|
-
* @param midi {
|
|
78
|
+
* @param midi {BasicMIDI}
|
|
79
79
|
* @param desiredProgramChanges {{
|
|
80
80
|
* channel: number,
|
|
81
81
|
* program: number,
|
|
@@ -520,7 +520,7 @@ export function modifyMIDI(
|
|
|
520
520
|
|
|
521
521
|
/**
|
|
522
522
|
* Modifies the sequence according to the locked presets and controllers in the given snapshot
|
|
523
|
-
* @param midi {
|
|
523
|
+
* @param midi {BasicMIDI}
|
|
524
524
|
* @param snapshot {SynthesizerSnapshot}
|
|
525
525
|
*/
|
|
526
526
|
export function applySnapshotToMIDI(midi, snapshot)
|
|
@@ -6,56 +6,31 @@ import { readRIFFChunk } from '../soundfont/basic_soundfont/riff_chunk.js'
|
|
|
6
6
|
import { readVariableLengthQuantity } from '../utils/byte_functions/variable_length_quantity.js'
|
|
7
7
|
import { readBytesAsUintBigEndian } from '../utils/byte_functions/big_endian.js'
|
|
8
8
|
import { readBytesAsString } from '../utils/byte_functions/string.js'
|
|
9
|
-
import {
|
|
9
|
+
import { readLittleEndian } from '../utils/byte_functions/little_endian.js'
|
|
10
10
|
import { RMIDINFOChunks } from './rmidi_writer.js'
|
|
11
|
+
import { BasicMIDI } from './basic_midi.js'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* midi_loader.js
|
|
14
15
|
* purpose: parses a midi file for the seqyencer, including things like marker or CC 2/4 loop detection, copyright detection etc.
|
|
15
16
|
*/
|
|
16
|
-
class MIDI
|
|
17
|
+
class MIDI extends BasicMIDI
|
|
18
|
+
{
|
|
17
19
|
/**
|
|
18
20
|
* Parses a given midi file
|
|
19
21
|
* @param arrayBuffer {ArrayBuffer}
|
|
20
22
|
* @param fileName {string} optional, replaces the decoded title if empty
|
|
21
23
|
*/
|
|
22
|
-
constructor(arrayBuffer, fileName="")
|
|
24
|
+
constructor(arrayBuffer, fileName="")
|
|
25
|
+
{
|
|
26
|
+
super();
|
|
23
27
|
SpessaSynthGroupCollapsed(`%cParsing MIDI File...`, consoleColors.info);
|
|
24
28
|
const binaryData = new IndexedByteArray(arrayBuffer);
|
|
25
29
|
let fileByteArray;
|
|
26
30
|
|
|
27
31
|
// check for rmid
|
|
28
|
-
/**
|
|
29
|
-
* If the RMI file has an embedded sf2 in it, it will appeear here, otherwise undefined
|
|
30
|
-
* @type {ArrayBuffer}
|
|
31
|
-
*/
|
|
32
|
-
this.embeddedSoundFont = undefined;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* The RMID Info data if RMID, otherwise undefined
|
|
36
|
-
* @type {Object<string, IndexedByteArray>}
|
|
37
|
-
*/
|
|
38
|
-
this.RMIDInfo = undefined;
|
|
39
|
-
/**
|
|
40
|
-
* The bank offset for RMIDI
|
|
41
|
-
* @type {number}
|
|
42
|
-
*/
|
|
43
|
-
this.bankOffset = 0;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Contains the copyright strings
|
|
47
|
-
* @type {string}
|
|
48
|
-
*/
|
|
49
|
-
this.copyright = "";
|
|
50
32
|
let copyrightDetected = false;
|
|
51
33
|
|
|
52
|
-
/**
|
|
53
|
-
* The MIDI name
|
|
54
|
-
* @type {string}
|
|
55
|
-
*/
|
|
56
|
-
this.midiName = "";
|
|
57
|
-
|
|
58
|
-
this.rawMidiName = new Uint8Array(0);
|
|
59
34
|
let nameDetected = false;
|
|
60
35
|
|
|
61
36
|
const initialString = readBytesAsString(binaryData, 4);
|
|
@@ -117,10 +92,19 @@ class MIDI{
|
|
|
117
92
|
this.midiName = readBytesAsString(this.rawMidiName, this.rawMidiName.length, undefined, false);
|
|
118
93
|
nameDetected = true;
|
|
119
94
|
}
|
|
95
|
+
// these can be used interchangeably
|
|
96
|
+
if(this.RMIDInfo['IALB'] && !this.RMIDInfo['IPRD'])
|
|
97
|
+
{
|
|
98
|
+
this.RMIDInfo['IPRD'] = this.RMIDInfo['IALB'];
|
|
99
|
+
}
|
|
100
|
+
if(this.RMIDInfo['PRD'] && !this.RMIDInfo['IALB'])
|
|
101
|
+
{
|
|
102
|
+
this.RMIDInfo['IALB'] = this.RMIDInfo['IPRD'];
|
|
103
|
+
}
|
|
120
104
|
this.bankOffset = 1; // defaults to 1
|
|
121
105
|
if(this.RMIDInfo[RMIDINFOChunks.bankOffset])
|
|
122
106
|
{
|
|
123
|
-
this.bankOffset =
|
|
107
|
+
this.bankOffset = readLittleEndian(this.RMIDInfo[RMIDINFOChunks.bankOffset], 2);
|
|
124
108
|
}
|
|
125
109
|
}
|
|
126
110
|
}
|
|
@@ -555,25 +539,5 @@ class MIDI{
|
|
|
555
539
|
fileByteArray.currentIndex += chunk.size;
|
|
556
540
|
return chunk;
|
|
557
541
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
/**
|
|
561
|
-
* Coverts ticks to time in seconds
|
|
562
|
-
* @param ticks {number}
|
|
563
|
-
* @returns {number}
|
|
564
|
-
* @private
|
|
565
|
-
*/
|
|
566
|
-
_ticksToSeconds(ticks)
|
|
567
|
-
{
|
|
568
|
-
if (ticks <= 0) {
|
|
569
|
-
return 0;
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// find the last tempo change that has occured
|
|
573
|
-
let tempo = this.tempoChanges.find(v => v.ticks < ticks);
|
|
574
|
-
|
|
575
|
-
let timeSinceLastTempo = ticks - tempo.ticks;
|
|
576
|
-
return this._ticksToSeconds(ticks - timeSinceLastTempo) + (timeSinceLastTempo * 60) / (tempo.tempo * this.timeDivision);
|
|
577
|
-
}
|
|
578
542
|
}
|
|
579
543
|
export { MIDI }
|
|
@@ -4,7 +4,7 @@ import { writeBytesAsUintBigEndian } from '../utils/byte_functions/big_endian.js
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Exports the midi as a .mid file
|
|
7
|
-
* @param midi {
|
|
7
|
+
* @param midi {BasicMIDI}
|
|
8
8
|
* @returns {Uint8Array} the binary .mid file data
|
|
9
9
|
*/
|
|
10
10
|
export function writeMIDIFile(midi)
|