spessasynth_lib 0.0.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/.idea/modules.xml +8 -0
- package/.idea/spessasynth_lib.iml +12 -0
- package/.idea/vcs.xml +6 -0
- package/copy_version.sh +38 -0
- package/index.js +73 -0
- package/package/@types/externals/stbvorbis_sync/stbvorbis_sync.min.d.ts +1 -0
- package/package/@types/index.d.ts +34 -0
- package/package/@types/midi_handler/midi_handler.d.ts +39 -0
- package/package/@types/midi_handler/web_midi_link.d.ts +12 -0
- package/package/@types/midi_parser/midi_data.d.ts +95 -0
- package/package/@types/midi_parser/midi_editor.d.ts +45 -0
- package/package/@types/midi_parser/midi_loader.d.ts +100 -0
- package/package/@types/midi_parser/midi_message.d.ts +154 -0
- package/package/@types/midi_parser/midi_writer.d.ts +6 -0
- package/package/@types/midi_parser/rmidi_writer.d.ts +9 -0
- package/package/@types/midi_parser/used_keys_loaded.d.ts +7 -0
- package/package/@types/sequencer/sequencer.d.ts +180 -0
- package/package/@types/sequencer/worklet_sequencer/sequencer_message.d.ts +28 -0
- package/package/@types/soundfont/read/generators.d.ts +98 -0
- package/package/@types/soundfont/read/instruments.d.ts +50 -0
- package/package/@types/soundfont/read/modulators.d.ts +73 -0
- package/package/@types/soundfont/read/presets.d.ts +87 -0
- package/package/@types/soundfont/read/riff_chunk.d.ts +31 -0
- package/package/@types/soundfont/read/samples.d.ts +134 -0
- package/package/@types/soundfont/read/zones.d.ts +141 -0
- package/package/@types/soundfont/soundfont.d.ts +76 -0
- package/package/@types/soundfont/write/ibag.d.ts +6 -0
- package/package/@types/soundfont/write/igen.d.ts +6 -0
- package/package/@types/soundfont/write/imod.d.ts +6 -0
- package/package/@types/soundfont/write/inst.d.ts +6 -0
- package/package/@types/soundfont/write/pbag.d.ts +6 -0
- package/package/@types/soundfont/write/pgen.d.ts +6 -0
- package/package/@types/soundfont/write/phdr.d.ts +6 -0
- package/package/@types/soundfont/write/pmod.d.ts +6 -0
- package/package/@types/soundfont/write/sdta.d.ts +11 -0
- package/package/@types/soundfont/write/shdr.d.ts +8 -0
- package/package/@types/soundfont/write/soundfont_trimmer.d.ts +6 -0
- package/package/@types/soundfont/write/write.d.ts +21 -0
- package/package/@types/synthetizer/audio_effects/effects_config.d.ts +29 -0
- package/package/@types/synthetizer/audio_effects/fancy_chorus.d.ts +93 -0
- package/package/@types/synthetizer/audio_effects/reverb.d.ts +7 -0
- package/package/@types/synthetizer/synth_event_handler.d.ts +161 -0
- package/package/@types/synthetizer/synthetizer.d.ts +294 -0
- package/package/@types/synthetizer/worklet_system/message_protocol/worklet_message.d.ts +89 -0
- package/package/@types/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.d.ts +134 -0
- package/package/@types/synthetizer/worklet_url.d.ts +5 -0
- package/package/@types/utils/buffer_to_wav.d.ts +8 -0
- package/package/@types/utils/byte_functions/big_endian.d.ts +13 -0
- package/package/@types/utils/byte_functions/little_endian.d.ts +35 -0
- package/package/@types/utils/byte_functions/string.d.ts +22 -0
- package/package/@types/utils/byte_functions/variable_length_quantity.d.ts +12 -0
- package/package/@types/utils/indexed_array.d.ts +21 -0
- package/package/@types/utils/loggin.d.ts +26 -0
- package/package/@types/utils/other.d.ts +32 -0
- package/package/LICENSE +26 -0
- package/package/README.md +84 -0
- package/package/externals/NOTICE +9 -0
- package/package/externals/libvorbis/@types/OggVorbisEncoder.d.ts +34 -0
- package/package/externals/libvorbis/OggVorbisEncoder.min.js +1 -0
- package/package/externals/stbvorbis_sync/@types/stbvorbis_sync.d.ts +12 -0
- package/package/externals/stbvorbis_sync/LICENSE +202 -0
- package/package/externals/stbvorbis_sync/stbvorbis_sync.min.js +1 -0
- package/package/index.js +73 -0
- package/package/midi_handler/README.md +3 -0
- package/package/midi_handler/midi_handler.js +118 -0
- package/package/midi_handler/web_midi_link.js +41 -0
- package/package/midi_parser/README.md +3 -0
- package/package/midi_parser/midi_data.js +121 -0
- package/package/midi_parser/midi_editor.js +557 -0
- package/package/midi_parser/midi_loader.js +502 -0
- package/package/midi_parser/midi_message.js +234 -0
- package/package/midi_parser/midi_writer.js +95 -0
- package/package/midi_parser/rmidi_writer.js +271 -0
- package/package/midi_parser/used_keys_loaded.js +172 -0
- package/package/package.json +43 -0
- package/package/sequencer/README.md +23 -0
- package/package/sequencer/sequencer.js +439 -0
- package/package/sequencer/worklet_sequencer/events.js +92 -0
- package/package/sequencer/worklet_sequencer/play.js +309 -0
- package/package/sequencer/worklet_sequencer/process_event.js +167 -0
- package/package/sequencer/worklet_sequencer/process_tick.js +85 -0
- package/package/sequencer/worklet_sequencer/sequencer_message.js +39 -0
- package/package/sequencer/worklet_sequencer/song_control.js +193 -0
- package/package/sequencer/worklet_sequencer/worklet_sequencer.js +218 -0
- package/package/soundfont/README.md +8 -0
- package/package/soundfont/read/generators.js +212 -0
- package/package/soundfont/read/instruments.js +125 -0
- package/package/soundfont/read/modulators.js +249 -0
- package/package/soundfont/read/presets.js +300 -0
- package/package/soundfont/read/riff_chunk.js +81 -0
- package/package/soundfont/read/samples.js +398 -0
- package/package/soundfont/read/zones.js +310 -0
- package/package/soundfont/soundfont.js +357 -0
- package/package/soundfont/write/ibag.js +39 -0
- package/package/soundfont/write/igen.js +75 -0
- package/package/soundfont/write/imod.js +46 -0
- package/package/soundfont/write/inst.js +34 -0
- package/package/soundfont/write/pbag.js +39 -0
- package/package/soundfont/write/pgen.js +77 -0
- package/package/soundfont/write/phdr.js +42 -0
- package/package/soundfont/write/pmod.js +46 -0
- package/package/soundfont/write/sdta.js +72 -0
- package/package/soundfont/write/shdr.js +54 -0
- package/package/soundfont/write/soundfont_trimmer.js +169 -0
- package/package/soundfont/write/write.js +180 -0
- package/package/synthetizer/README.md +6 -0
- package/package/synthetizer/audio_effects/effects_config.js +21 -0
- package/package/synthetizer/audio_effects/fancy_chorus.js +120 -0
- package/package/synthetizer/audio_effects/impulse_response_2.flac +0 -0
- package/package/synthetizer/audio_effects/reverb.js +24 -0
- package/package/synthetizer/synth_event_handler.js +156 -0
- package/package/synthetizer/synthetizer.js +766 -0
- package/package/synthetizer/worklet_processor.min.js +13 -0
- package/package/synthetizer/worklet_system/README.md +6 -0
- package/package/synthetizer/worklet_system/main_processor.js +363 -0
- package/package/synthetizer/worklet_system/message_protocol/handle_message.js +197 -0
- package/package/synthetizer/worklet_system/message_protocol/message_sending.js +74 -0
- package/package/synthetizer/worklet_system/message_protocol/worklet_message.js +121 -0
- package/package/synthetizer/worklet_system/minify_processor.sh +4 -0
- package/package/synthetizer/worklet_system/worklet_methods/controller_control.js +230 -0
- package/package/synthetizer/worklet_system/worklet_methods/data_entry.js +277 -0
- package/package/synthetizer/worklet_system/worklet_methods/note_off.js +109 -0
- package/package/synthetizer/worklet_system/worklet_methods/note_on.js +91 -0
- package/package/synthetizer/worklet_system/worklet_methods/program_control.js +183 -0
- package/package/synthetizer/worklet_system/worklet_methods/reset_controllers.js +177 -0
- package/package/synthetizer/worklet_system/worklet_methods/snapshot.js +129 -0
- package/package/synthetizer/worklet_system/worklet_methods/system_exclusive.js +272 -0
- package/package/synthetizer/worklet_system/worklet_methods/tuning_control.js +195 -0
- package/package/synthetizer/worklet_system/worklet_methods/vibrato_control.js +29 -0
- package/package/synthetizer/worklet_system/worklet_methods/voice_control.js +233 -0
- package/package/synthetizer/worklet_system/worklet_processor.js +9 -0
- package/package/synthetizer/worklet_system/worklet_utilities/lfo.js +23 -0
- package/package/synthetizer/worklet_system/worklet_utilities/lowpass_filter.js +130 -0
- package/package/synthetizer/worklet_system/worklet_utilities/modulation_envelope.js +73 -0
- package/package/synthetizer/worklet_system/worklet_utilities/modulator_curves.js +86 -0
- package/package/synthetizer/worklet_system/worklet_utilities/stereo_panner.js +81 -0
- package/package/synthetizer/worklet_system/worklet_utilities/unit_converter.js +66 -0
- package/package/synthetizer/worklet_system/worklet_utilities/volume_envelope.js +265 -0
- package/package/synthetizer/worklet_system/worklet_utilities/wavetable_oscillator.js +83 -0
- package/package/synthetizer/worklet_system/worklet_utilities/worklet_modulator.js +234 -0
- package/package/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +116 -0
- package/package/synthetizer/worklet_system/worklet_utilities/worklet_voice.js +272 -0
- package/package/synthetizer/worklet_url.js +5 -0
- package/package/utils/README.md +4 -0
- package/package/utils/buffer_to_wav.js +101 -0
- package/package/utils/byte_functions/big_endian.js +28 -0
- package/package/utils/byte_functions/little_endian.js +74 -0
- package/package/utils/byte_functions/string.js +97 -0
- package/package/utils/byte_functions/variable_length_quantity.js +37 -0
- package/package/utils/encode_vorbis.js +30 -0
- package/package/utils/indexed_array.js +41 -0
- package/package/utils/loggin.js +79 -0
- package/package/utils/other.js +54 -0
- package/package.json +43 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from '../utils/loggin.js'
|
|
2
|
+
import { consoleColors } from '../utils/other.js'
|
|
3
|
+
import { DEFAULT_PERCUSSION } from '../synthetizer/synthetizer.js'
|
|
4
|
+
import { messageTypes, midiControllers } from './midi_message.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param mid {MIDI}
|
|
8
|
+
* @param soundfont {SoundFont2}
|
|
9
|
+
*/
|
|
10
|
+
export function getUsedProgramsAndKeys(mid, soundfont)
|
|
11
|
+
{
|
|
12
|
+
SpessaSynthGroupCollapsed("%cSearching for all used programs and keys...",
|
|
13
|
+
consoleColors.info);
|
|
14
|
+
// find every bank:program combo and every key:velocity for each. Make sure to care about ports and drums
|
|
15
|
+
const channelsAmount = 16 + Math.max.apply(undefined, mid.midiPorts) * 16;
|
|
16
|
+
/**
|
|
17
|
+
*
|
|
18
|
+
* @type {{program: number, bank: number, drums: boolean, string: string}[]}
|
|
19
|
+
*/
|
|
20
|
+
const channelPresets = [];
|
|
21
|
+
for (let i = 0; i < channelsAmount; i++) {
|
|
22
|
+
const bank = i % 16 === DEFAULT_PERCUSSION ? 128 : 0;
|
|
23
|
+
channelPresets.push({
|
|
24
|
+
program: 0,
|
|
25
|
+
bank: bank,
|
|
26
|
+
drums: i % 16 === DEFAULT_PERCUSSION, // drums appear on 9 every 16 channels,
|
|
27
|
+
string: `${bank}:0`
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function updateString(ch)
|
|
32
|
+
{
|
|
33
|
+
// check if this exists in the soundfont
|
|
34
|
+
let exists = soundfont.getPreset(ch.bank, ch.program);
|
|
35
|
+
if(exists.bank !== ch.bank && mid.embeddedSoundFont)
|
|
36
|
+
{
|
|
37
|
+
// maybe it doesn't exists becase RMIDI has a bank shift?
|
|
38
|
+
exists = soundfont.getPreset(ch.bank - 1, ch.program);
|
|
39
|
+
}
|
|
40
|
+
ch.bank = exists.bank;
|
|
41
|
+
ch.program = exists.program;
|
|
42
|
+
ch.string = ch.bank + ":" + ch.program;
|
|
43
|
+
if(!usedProgramsAndKeys[ch.string])
|
|
44
|
+
{
|
|
45
|
+
SpessaSynthInfo(`%cDetected a new preset: %c${ch.string}`,
|
|
46
|
+
consoleColors.info,
|
|
47
|
+
consoleColors.recognized);
|
|
48
|
+
usedProgramsAndKeys[ch.string] = new Set();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* find all programs used and key-velocity combos in them
|
|
53
|
+
* bank:program each has a set of midiNote-velocity
|
|
54
|
+
* @type {Object<string, Set<string>>}
|
|
55
|
+
*/
|
|
56
|
+
const usedProgramsAndKeys = {};
|
|
57
|
+
// check for xg
|
|
58
|
+
let system = "gs";
|
|
59
|
+
mid.tracks.forEach((t, trackNum) => {
|
|
60
|
+
const portOffset = mid.midiPorts[trackNum] * 16;
|
|
61
|
+
for(const event of t)
|
|
62
|
+
{
|
|
63
|
+
const status = event.messageStatusByte & 0xF0;
|
|
64
|
+
if(
|
|
65
|
+
status !== messageTypes.noteOn &&
|
|
66
|
+
status !== messageTypes.controllerChange &&
|
|
67
|
+
status !== messageTypes.programChange &&
|
|
68
|
+
status !== messageTypes.systemExclusive
|
|
69
|
+
)
|
|
70
|
+
{
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const channel = (event.messageStatusByte & 0xF) + portOffset;
|
|
74
|
+
let ch = channelPresets[channel];
|
|
75
|
+
switch(status)
|
|
76
|
+
{
|
|
77
|
+
case messageTypes.programChange:
|
|
78
|
+
ch.program = event.messageData[0];
|
|
79
|
+
updateString(ch);
|
|
80
|
+
break;
|
|
81
|
+
|
|
82
|
+
case messageTypes.controllerChange:
|
|
83
|
+
if(event.messageData[0] !== midiControllers.bankSelect)
|
|
84
|
+
{
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if(system === "gs" && ch.drums)
|
|
88
|
+
{
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const bank = event.messageData[1];
|
|
92
|
+
if(system === "xg")
|
|
93
|
+
{
|
|
94
|
+
const drumsBool = bank === 120 || bank === 126 || bank === 127;
|
|
95
|
+
if(drumsBool !== ch.drums)
|
|
96
|
+
{
|
|
97
|
+
// drum change is a program change
|
|
98
|
+
ch.drums = drumsBool;
|
|
99
|
+
ch.bank = ch.drums ? 128 : bank;
|
|
100
|
+
updateString(ch);
|
|
101
|
+
}
|
|
102
|
+
else
|
|
103
|
+
{
|
|
104
|
+
ch.bank = ch.drums ? 128 : bank;
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
channelPresets[channel].bank = bank;
|
|
109
|
+
// do not update the data, bank change doesnt change the preset
|
|
110
|
+
break;
|
|
111
|
+
|
|
112
|
+
case messageTypes.noteOn:
|
|
113
|
+
if(event.messageData[1] === 0)
|
|
114
|
+
{
|
|
115
|
+
// that's a note off
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if(!usedProgramsAndKeys[ch.string])
|
|
119
|
+
{
|
|
120
|
+
usedProgramsAndKeys[ch.string] = new Set();
|
|
121
|
+
}
|
|
122
|
+
usedProgramsAndKeys[ch.string].add(`${event.messageData[0]}-${event.messageData[1]}`);
|
|
123
|
+
break;
|
|
124
|
+
|
|
125
|
+
case messageTypes.systemExclusive:
|
|
126
|
+
// check for drum sysex
|
|
127
|
+
if(
|
|
128
|
+
event.messageData[0] !== 0x41 || // roland
|
|
129
|
+
event.messageData[2] !== 0x42 || // GS
|
|
130
|
+
event.messageData[3] !== 0x12 || // GS
|
|
131
|
+
event.messageData[4] !== 0x40 || // system parameter
|
|
132
|
+
(event.messageData[5] & 0x10 ) === 0 || // part parameter
|
|
133
|
+
event.messageData[6] !== 0x15 // drum pars
|
|
134
|
+
|
|
135
|
+
)
|
|
136
|
+
{
|
|
137
|
+
// check for XG
|
|
138
|
+
if(
|
|
139
|
+
event.messageData[0] === 0x43 && // yamaha
|
|
140
|
+
event.messageData[2] === 0x4C && // sXG ON
|
|
141
|
+
event.messageData[5] === 0x7E &&
|
|
142
|
+
event.messageData[6] === 0x00
|
|
143
|
+
)
|
|
144
|
+
{
|
|
145
|
+
system = "xg";
|
|
146
|
+
}
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const sysexChannel = [9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15][event.messageData[5] & 0x0F] + portOffset;
|
|
150
|
+
const isDrum = !!(event.messageData[7] > 0 && event.messageData[5] >> 4);
|
|
151
|
+
ch = channelPresets[sysexChannel];
|
|
152
|
+
ch.drums = isDrum;
|
|
153
|
+
ch.bank = isDrum ? 128 : 0;
|
|
154
|
+
updateString(ch);
|
|
155
|
+
break;
|
|
156
|
+
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
for(const key of Object.keys(usedProgramsAndKeys))
|
|
161
|
+
{
|
|
162
|
+
if(usedProgramsAndKeys[key].size === 0)
|
|
163
|
+
{
|
|
164
|
+
SpessaSynthInfo(`%cDetected change but no keys for %c${key}`,
|
|
165
|
+
consoleColors.info,
|
|
166
|
+
consoleColors.value)
|
|
167
|
+
delete usedProgramsAndKeys[key];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
SpessaSynthGroupEnd();
|
|
171
|
+
return usedProgramsAndKeys;
|
|
172
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "spessasynth_lib",
|
|
3
|
+
"version": "3.9.12",
|
|
4
|
+
"description": "No compromise SoundFont and MIDI library and player",
|
|
5
|
+
"browser": "index.js",
|
|
6
|
+
"types": "@types/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"directories": {
|
|
9
|
+
"lib": "lib"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/spessasus/SpessaSynth.git"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"soundfont",
|
|
20
|
+
"synthesizer",
|
|
21
|
+
"synth",
|
|
22
|
+
"sf2",
|
|
23
|
+
"sf3",
|
|
24
|
+
"midi",
|
|
25
|
+
"midi-player",
|
|
26
|
+
"web-audio-api",
|
|
27
|
+
"web-midi-api",
|
|
28
|
+
"player",
|
|
29
|
+
"soundfont2",
|
|
30
|
+
"soundfont3"
|
|
31
|
+
],
|
|
32
|
+
"author": {
|
|
33
|
+
"name": "spessasus",
|
|
34
|
+
"email": "spesekspesek@gmail.com",
|
|
35
|
+
"url": "https://github.com/spessasus"
|
|
36
|
+
},
|
|
37
|
+
"license": "(MIT AND Apache-2.0)",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/spessasus/SpessaSynth/issues",
|
|
40
|
+
"email": "spesekspesek@gmail.com"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/spessasus/SpessaSynth#readme"
|
|
43
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
## This is the sequencer's folder.
|
|
2
|
+
The code here is responsible for playing back the parsed MIDI sequence with the synthesizer.
|
|
3
|
+
|
|
4
|
+
### Message protocol:
|
|
5
|
+
#### Message structure
|
|
6
|
+
```js
|
|
7
|
+
const message = {
|
|
8
|
+
messageType: number, // WorkletSequencerMessageType
|
|
9
|
+
messageData: any // any
|
|
10
|
+
}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
#### To worklet
|
|
14
|
+
Sequencer uses `Synthetizer`'s `post` method to post a message with `messageData` set to `workletMessageType.sequencerSpecific`.
|
|
15
|
+
The `messageData` is set to the sequencer's message.
|
|
16
|
+
|
|
17
|
+
#### From worklet
|
|
18
|
+
`WorkletSequencer` uses `SpessaSynthProcessor`'s post to send a message with `messageData` set to `returnMessageType.sequencerSpecific`.
|
|
19
|
+
The `messageData` is set to the sequencer's return message.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Process tick
|
|
23
|
+
`processTick` is called every time the `process` method is called via `SpessaSynthProcessor.processTickCallback`.
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { MIDI } from '../midi_parser/midi_loader.js'
|
|
2
|
+
import { Synthetizer } from '../synthetizer/synthetizer.js'
|
|
3
|
+
import { messageTypes } from '../midi_parser/midi_message.js'
|
|
4
|
+
import { workletMessageType } from '../synthetizer/worklet_system/message_protocol/worklet_message.js'
|
|
5
|
+
import {
|
|
6
|
+
WorkletSequencerMessageType,
|
|
7
|
+
WorkletSequencerReturnMessageType,
|
|
8
|
+
} from './worklet_sequencer/sequencer_message.js'
|
|
9
|
+
import { SpessaSynthWarn } from '../utils/loggin.js'
|
|
10
|
+
import { DUMMY_MIDI_DATA, MidiData } from '../midi_parser/midi_data.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* sequencer.js
|
|
14
|
+
* purpose: plays back the midi file decoded by midi_loader.js, including support for multi-channel midis (adding channels when more than 1 midi port is detected)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef MidFile {Object}
|
|
19
|
+
* @property {ArrayBuffer} binary - the binary data of the file.
|
|
20
|
+
* @property {string|undefined} altName - the alternative name for the file
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {MIDI|MidFile} MIDIFile
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
export class Sequencer
|
|
28
|
+
{
|
|
29
|
+
/**
|
|
30
|
+
* Creates a new Midi sequencer for playing back MIDI files
|
|
31
|
+
* @param midiBinaries {MIDIFile[]} List of the buffers of the MIDI files
|
|
32
|
+
* @param synth {Synthetizer} synth to send events to
|
|
33
|
+
*/
|
|
34
|
+
constructor(midiBinaries, synth)
|
|
35
|
+
{
|
|
36
|
+
this.ignoreEvents = false;
|
|
37
|
+
this.synth = synth;
|
|
38
|
+
this.highResTimeOffset = 0;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Absolute playback startTime, bases on the synth's time
|
|
42
|
+
* @type {number}
|
|
43
|
+
*/
|
|
44
|
+
this.absoluteStartTime = this.synth.currentTime;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @type {function(MIDI)}
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
this._getMIDIResolve = undefined;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Controls the playback's rate
|
|
54
|
+
* @type {number}
|
|
55
|
+
*/
|
|
56
|
+
this._playbackRate = 1;
|
|
57
|
+
|
|
58
|
+
this.songIndex = 0;
|
|
59
|
+
|
|
60
|
+
this._loop = true;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Indicates whether the sequencer has finished playing a sequence
|
|
64
|
+
* @type {boolean}
|
|
65
|
+
*/
|
|
66
|
+
this.isFinished = false;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The current sequence's length, in seconds
|
|
70
|
+
* @type {number}
|
|
71
|
+
*/
|
|
72
|
+
this.duration = 0;
|
|
73
|
+
|
|
74
|
+
this.synth.sequencerCallbackFunction = this._handleMessage.bind(this);
|
|
75
|
+
|
|
76
|
+
this.loadNewSongList(midiBinaries);
|
|
77
|
+
|
|
78
|
+
window.addEventListener("beforeunload", this.resetMIDIOut.bind(this))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
resetMIDIOut()
|
|
82
|
+
{
|
|
83
|
+
if(!this.MIDIout)
|
|
84
|
+
{
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
for (let i = 0; i < 16; i++)
|
|
88
|
+
{
|
|
89
|
+
this.MIDIout.send([messageTypes.controllerChange | i, 120, 0]); // all notes off
|
|
90
|
+
this.MIDIout.send([messageTypes.controllerChange | i, 123, 0]); // all sound off
|
|
91
|
+
}
|
|
92
|
+
this.MIDIout.send([messageTypes.reset]); // reset
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
set loop(value)
|
|
96
|
+
{
|
|
97
|
+
this._sendMessage(WorkletSequencerMessageType.setLoop, value);
|
|
98
|
+
this._loop = value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get loop()
|
|
102
|
+
{
|
|
103
|
+
return this._loop;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param messageType {WorkletSequencerMessageType}
|
|
108
|
+
* @param messageData {any}
|
|
109
|
+
* @private
|
|
110
|
+
*/
|
|
111
|
+
_sendMessage(messageType, messageData = undefined)
|
|
112
|
+
{
|
|
113
|
+
this.synth.post({
|
|
114
|
+
channelNumber: -1,
|
|
115
|
+
messageType: workletMessageType.sequencerSpecific,
|
|
116
|
+
messageData: {
|
|
117
|
+
messageType: messageType,
|
|
118
|
+
messageData: messageData
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Executes when MIDI parsing has an error.
|
|
125
|
+
* @type {function(string)}
|
|
126
|
+
*/
|
|
127
|
+
onError;
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {WorkletSequencerReturnMessageType} messageType
|
|
131
|
+
* @param {any} messageData
|
|
132
|
+
* @private
|
|
133
|
+
*/
|
|
134
|
+
_handleMessage(messageType, messageData)
|
|
135
|
+
{
|
|
136
|
+
if(this.ignoreEvents)
|
|
137
|
+
{
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
switch (messageType)
|
|
141
|
+
{
|
|
142
|
+
default:
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
case WorkletSequencerReturnMessageType.midiEvent:
|
|
146
|
+
/**
|
|
147
|
+
* @type {number[]}
|
|
148
|
+
*/
|
|
149
|
+
let midiEventData = messageData;
|
|
150
|
+
if (this.MIDIout) {
|
|
151
|
+
if (midiEventData[0] >= 0x80) {
|
|
152
|
+
this.MIDIout.send(midiEventData);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
|
|
158
|
+
case WorkletSequencerReturnMessageType.songChange:
|
|
159
|
+
/**
|
|
160
|
+
* messageData is expected to be {MidiData}
|
|
161
|
+
* @type {MidiData}
|
|
162
|
+
*/
|
|
163
|
+
let songChangeData = messageData[0];
|
|
164
|
+
this.songIndex = messageData[1];
|
|
165
|
+
this.midiData = songChangeData;
|
|
166
|
+
this.absoluteStartTime = 0;
|
|
167
|
+
this.duration = this.midiData.duration;
|
|
168
|
+
Object.entries(this.onSongChange).forEach((callback) => callback[1](songChangeData));
|
|
169
|
+
this.unpause();
|
|
170
|
+
break;
|
|
171
|
+
|
|
172
|
+
case WorkletSequencerReturnMessageType.textEvent:
|
|
173
|
+
/**
|
|
174
|
+
* @type {[Uint8Array, number]}
|
|
175
|
+
*/
|
|
176
|
+
let textEventData = messageData;
|
|
177
|
+
if (this.onTextEvent) {
|
|
178
|
+
this.onTextEvent(textEventData[0], textEventData[1]);
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
|
|
182
|
+
case WorkletSequencerReturnMessageType.timeChange:
|
|
183
|
+
// message data is absolute time
|
|
184
|
+
const time = this.synth.currentTime - messageData;
|
|
185
|
+
Object.entries(this.onTimeChange).forEach((callback) => callback[1](time));
|
|
186
|
+
this.unpause();
|
|
187
|
+
this._recalculateStartTime(time);
|
|
188
|
+
break;
|
|
189
|
+
|
|
190
|
+
case WorkletSequencerReturnMessageType.pause:
|
|
191
|
+
this.pausedTime = this.currentTime;
|
|
192
|
+
this.isFinished = messageData;
|
|
193
|
+
break;
|
|
194
|
+
|
|
195
|
+
case WorkletSequencerReturnMessageType.midiError:
|
|
196
|
+
if(this.onError)
|
|
197
|
+
{
|
|
198
|
+
this.onError(messageData);
|
|
199
|
+
}
|
|
200
|
+
else
|
|
201
|
+
{
|
|
202
|
+
throw new Error(messageData);
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
|
|
206
|
+
case WorkletSequencerReturnMessageType.getMIDI:
|
|
207
|
+
if(this._getMIDIResolve)
|
|
208
|
+
{
|
|
209
|
+
this._getMIDIResolve(messageData);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* @param value {number}
|
|
216
|
+
*/
|
|
217
|
+
set playbackRate(value)
|
|
218
|
+
{
|
|
219
|
+
this._sendMessage(WorkletSequencerMessageType.setPlaybackRate, value);
|
|
220
|
+
this.highResTimeOffset *= (value / this._playbackRate);
|
|
221
|
+
this._playbackRate = value;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @returns {number}
|
|
226
|
+
*/
|
|
227
|
+
get playbackRate()
|
|
228
|
+
{
|
|
229
|
+
return this._playbackRate;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Adds a new event that gets called when the song changes
|
|
234
|
+
* @param callback {function(MidiData)}
|
|
235
|
+
* @param id {string} must be unique
|
|
236
|
+
*/
|
|
237
|
+
addOnSongChangeEvent(callback, id)
|
|
238
|
+
{
|
|
239
|
+
this.onSongChange[id] = callback;
|
|
240
|
+
callback(this.midiData);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Adds a new event that gets called when the time changes
|
|
245
|
+
* @param callback {function(number)} the new time, in seconds
|
|
246
|
+
* @param id {string} must be unique
|
|
247
|
+
*/
|
|
248
|
+
addOnTimeChangeEvent(callback, id)
|
|
249
|
+
{
|
|
250
|
+
this.onTimeChange[id] = callback;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @returns {Promise<MIDI>}
|
|
255
|
+
*/
|
|
256
|
+
async getMIDI()
|
|
257
|
+
{
|
|
258
|
+
return new Promise(resolve => {
|
|
259
|
+
this._getMIDIResolve = resolve;
|
|
260
|
+
this._sendMessage(WorkletSequencerMessageType.getMIDI, undefined);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* @param midiBuffers {MIDIFile[]}
|
|
266
|
+
*/
|
|
267
|
+
loadNewSongList(midiBuffers)
|
|
268
|
+
{
|
|
269
|
+
this.pause();
|
|
270
|
+
// add some dummy data
|
|
271
|
+
this.midiData = DUMMY_MIDI_DATA;
|
|
272
|
+
this.duration = 99999;
|
|
273
|
+
this._sendMessage(WorkletSequencerMessageType.loadNewSongList, midiBuffers);
|
|
274
|
+
this.songIndex = 0;
|
|
275
|
+
this.songsAmount = midiBuffers.length;
|
|
276
|
+
if(this.songsAmount > 1)
|
|
277
|
+
{
|
|
278
|
+
this.loop = false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
nextSong()
|
|
283
|
+
{
|
|
284
|
+
this._sendMessage(WorkletSequencerMessageType.changeSong, true);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
previousSong()
|
|
288
|
+
{
|
|
289
|
+
this._sendMessage(WorkletSequencerMessageType.changeSong, false);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* @returns {number} Current playback time, in seconds
|
|
294
|
+
*/
|
|
295
|
+
get currentTime()
|
|
296
|
+
{
|
|
297
|
+
// return the paused time if it's set to something other than undefined
|
|
298
|
+
if(this.pausedTime)
|
|
299
|
+
{
|
|
300
|
+
return this.pausedTime;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return (this.synth.currentTime - this.absoluteStartTime) * this._playbackRate;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* @param time
|
|
308
|
+
* @private
|
|
309
|
+
*/
|
|
310
|
+
_recalculateStartTime(time)
|
|
311
|
+
{
|
|
312
|
+
this.absoluteStartTime = this.synth.currentTime - time / this._playbackRate;
|
|
313
|
+
this.highResTimeOffset = (this.synth.currentTime - (performance.now() / 1000)) * this._playbackRate;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Use for visualization as it's not affected by the audioContext stutter
|
|
318
|
+
* @returns {number}
|
|
319
|
+
*/
|
|
320
|
+
get currentHighResolutionTime() {
|
|
321
|
+
if (this.pausedTime) {
|
|
322
|
+
return this.pausedTime;
|
|
323
|
+
}
|
|
324
|
+
const highResTimeOffset = this.highResTimeOffset;
|
|
325
|
+
const absoluteStartTime = this.absoluteStartTime;
|
|
326
|
+
|
|
327
|
+
// sync performance.now to current time
|
|
328
|
+
const performanceElapsedTime = ((performance.now() / 1000) - absoluteStartTime) * this._playbackRate;
|
|
329
|
+
|
|
330
|
+
let currentPerformanceTime = highResTimeOffset + performanceElapsedTime;
|
|
331
|
+
const currentAudioTime = this.currentTime;
|
|
332
|
+
|
|
333
|
+
const smoothingFactor = 0.01 * this._playbackRate;
|
|
334
|
+
|
|
335
|
+
// diff times smoothing factor
|
|
336
|
+
const timeDifference = currentAudioTime - currentPerformanceTime;
|
|
337
|
+
this.highResTimeOffset += timeDifference * smoothingFactor;
|
|
338
|
+
|
|
339
|
+
// return a smoothed performance time
|
|
340
|
+
currentPerformanceTime = this.highResTimeOffset + performanceElapsedTime;
|
|
341
|
+
return currentPerformanceTime;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
set currentTime(time)
|
|
346
|
+
{
|
|
347
|
+
this.unpause()
|
|
348
|
+
this._sendMessage(WorkletSequencerMessageType.setTime, time);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* @param output {MIDIOutput}
|
|
353
|
+
*/
|
|
354
|
+
connectMidiOutput(output)
|
|
355
|
+
{
|
|
356
|
+
this.resetMIDIOut();
|
|
357
|
+
this.MIDIout = output;
|
|
358
|
+
this._sendMessage(WorkletSequencerMessageType.changeMIDIMessageSending, output !== undefined);
|
|
359
|
+
this.currentTime -= 0.1;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Pauses the playback
|
|
364
|
+
*/
|
|
365
|
+
pause()
|
|
366
|
+
{
|
|
367
|
+
if(this.paused)
|
|
368
|
+
{
|
|
369
|
+
SpessaSynthWarn("Already paused");
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
this.pausedTime = this.currentTime;
|
|
373
|
+
this._sendMessage(WorkletSequencerMessageType.pause);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
unpause()
|
|
377
|
+
{
|
|
378
|
+
this.pausedTime = undefined;
|
|
379
|
+
this.isFinished = false;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* true if paused, false if playing or stopped
|
|
384
|
+
* @returns {boolean}
|
|
385
|
+
*/
|
|
386
|
+
get paused()
|
|
387
|
+
{
|
|
388
|
+
return this.pausedTime !== undefined;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Starts the playback
|
|
393
|
+
* @param resetTime {boolean} If true, time is set to 0s
|
|
394
|
+
*/
|
|
395
|
+
play(resetTime = false)
|
|
396
|
+
{
|
|
397
|
+
if(this.isFinished)
|
|
398
|
+
{
|
|
399
|
+
resetTime = true;
|
|
400
|
+
}
|
|
401
|
+
this._recalculateStartTime(this.pausedTime || 0);
|
|
402
|
+
this.unpause()
|
|
403
|
+
this._sendMessage(WorkletSequencerMessageType.play, resetTime);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Stops the playback
|
|
408
|
+
*/
|
|
409
|
+
stop()
|
|
410
|
+
{
|
|
411
|
+
this._sendMessage(WorkletSequencerMessageType.stop);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* The sequence's data, except for the track data.
|
|
416
|
+
* @type {MidiData}
|
|
417
|
+
*/
|
|
418
|
+
midiData;
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* @type {Object<string, function(MidiData)>}
|
|
422
|
+
* @private
|
|
423
|
+
*/
|
|
424
|
+
onSongChange = {};
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Fires on text event
|
|
428
|
+
* @param data {Uint8Array} the data text
|
|
429
|
+
* @param type {number} the status byte of the message (the meta status byte)
|
|
430
|
+
*/
|
|
431
|
+
onTextEvent;
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Fires when CurrentTime changes
|
|
435
|
+
* @type {Object<string, function(number)>} the time that was changed to
|
|
436
|
+
* @private
|
|
437
|
+
*/
|
|
438
|
+
onTimeChange = {};
|
|
439
|
+
}
|