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,557 @@
|
|
|
1
|
+
import { messageTypes, midiControllers, MidiMessage } from './midi_message.js'
|
|
2
|
+
import { IndexedByteArray } from '../utils/indexed_array.js'
|
|
3
|
+
import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo, SpessaSynthWarn } from '../utils/loggin.js'
|
|
4
|
+
import { consoleColors } from '../utils/other.js'
|
|
5
|
+
import { DEFAULT_PERCUSSION } from '../synthetizer/synthetizer.js'
|
|
6
|
+
import { customControllers } from '../synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param ticks {number}
|
|
10
|
+
* @returns {MidiMessage}
|
|
11
|
+
*/
|
|
12
|
+
export function getGsOn(ticks)
|
|
13
|
+
{
|
|
14
|
+
return new MidiMessage(
|
|
15
|
+
ticks,
|
|
16
|
+
messageTypes.systemExclusive,
|
|
17
|
+
new IndexedByteArray([
|
|
18
|
+
0x41, // Roland
|
|
19
|
+
0x10, // Device ID (defaults to 16 on roland)
|
|
20
|
+
0x42, // GS
|
|
21
|
+
0x12, // Command ID (DT1) (whatever that means...)
|
|
22
|
+
0x40, // System parameter }
|
|
23
|
+
0x00, // Global parameter } Address
|
|
24
|
+
0x7F, // GS Change }
|
|
25
|
+
0x00, // turn on } Data
|
|
26
|
+
0x41, // checksum
|
|
27
|
+
0xF7, // end of exclusive
|
|
28
|
+
])
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getControllerChange(channel, cc, value, ticks)
|
|
33
|
+
{
|
|
34
|
+
return new MidiMessage(
|
|
35
|
+
ticks,
|
|
36
|
+
messageTypes.controllerChange | (channel % 16),
|
|
37
|
+
new IndexedByteArray([cc, value])
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param channel {number}
|
|
43
|
+
* @param ticks {number}
|
|
44
|
+
* @returns {MidiMessage}
|
|
45
|
+
*/
|
|
46
|
+
function getDrumChange(channel, ticks)
|
|
47
|
+
{
|
|
48
|
+
const chanAddress = 0x10 | [1, 2, 3, 4, 5, 6, 7, 8, 0, 9, 10, 11, 12, 13, 14, 15][channel % 16];
|
|
49
|
+
// excluding manufacturerID DeviceID and ModelID (and F7)
|
|
50
|
+
const sysexData = [
|
|
51
|
+
0x41, // Roland
|
|
52
|
+
0x10, // Device ID (defaults to 16 on roland)
|
|
53
|
+
0x42, // GS
|
|
54
|
+
0x12, // Command ID (DT1) (whatever that means...)
|
|
55
|
+
0x40, // System parameter }
|
|
56
|
+
chanAddress, // Channel parameter } Address
|
|
57
|
+
0x15, // Drum change }
|
|
58
|
+
0x01, // Is Drums } Data
|
|
59
|
+
]
|
|
60
|
+
// calculate checksum
|
|
61
|
+
// https://cdn.roland.com/assets/media/pdf/F-20_MIDI_Imple_e01_W.pdf section 4
|
|
62
|
+
const sum = 0x40 + chanAddress + 0x15 + 0x01;
|
|
63
|
+
const checksum = 128 - (sum % 128);
|
|
64
|
+
// add system exclusive to enable drums
|
|
65
|
+
return new MidiMessage(
|
|
66
|
+
ticks,
|
|
67
|
+
messageTypes.systemExclusive,
|
|
68
|
+
new IndexedByteArray([
|
|
69
|
+
...sysexData,
|
|
70
|
+
checksum,
|
|
71
|
+
0xF7
|
|
72
|
+
])
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Allows easy editing of the file
|
|
78
|
+
* @param midi {MIDI}
|
|
79
|
+
* @param desiredProgramChanges {{
|
|
80
|
+
* channel: number,
|
|
81
|
+
* program: number,
|
|
82
|
+
* bank: number,
|
|
83
|
+
* isDrum: boolean
|
|
84
|
+
* }[]} the programs to set on given channels. Note that the channel may be more than 16, function will adjust midi ports automatically
|
|
85
|
+
* @param desiredControllerChanges {{
|
|
86
|
+
* channel: number,
|
|
87
|
+
* controllerNumber: number,
|
|
88
|
+
* controllerValue: number,
|
|
89
|
+
* }[]} the controllers to set on given channels. Note that the channel may be more than 16, function will adjust midi ports automatically
|
|
90
|
+
* @param desiredChannelsToClear {number[]} the channels to remove from the sequence. Note that the channel may be more than 16, function will adjust midi ports automatically
|
|
91
|
+
* @param desiredChannelsToTranspose {{
|
|
92
|
+
* channel: number,
|
|
93
|
+
* keyShift: number
|
|
94
|
+
* }[]} the channels to transpose. if keyShift is float, rpn fine tuning will be applied as well. Note that the channel may be more than 16, function will adjust midi ports automatically
|
|
95
|
+
*/
|
|
96
|
+
export function modifyMIDI(
|
|
97
|
+
midi,
|
|
98
|
+
desiredProgramChanges = [],
|
|
99
|
+
desiredControllerChanges = [],
|
|
100
|
+
desiredChannelsToClear = [],
|
|
101
|
+
desiredChannelsToTranspose = []
|
|
102
|
+
)
|
|
103
|
+
{
|
|
104
|
+
SpessaSynthGroupCollapsed("%cApplying changes to the MIDI file...", consoleColors.info);
|
|
105
|
+
/**
|
|
106
|
+
* @param channel {number}
|
|
107
|
+
* @param port {number}
|
|
108
|
+
*/
|
|
109
|
+
const clearChannelMessages = (channel, port) => {
|
|
110
|
+
midi.tracks.forEach((track, trackNum) => {
|
|
111
|
+
if(midi.midiPorts[trackNum] !== port)
|
|
112
|
+
{
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
for(let i = track.length - 1; i >= 0; i--) // iterate in reverse to not mess up indexes
|
|
116
|
+
{
|
|
117
|
+
if(track[i].messageStatusByte >= 0x80 && track[i].messageStatusByte < 0xF0) // do not clear sysexes
|
|
118
|
+
{
|
|
119
|
+
if((track[i].messageStatusByte & 0xF) === channel)
|
|
120
|
+
{
|
|
121
|
+
track.splice(i, 1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
desiredChannelsToClear.forEach(c => {
|
|
128
|
+
const port = Math.floor(c / 16);
|
|
129
|
+
const channel = c % 16;
|
|
130
|
+
clearChannelMessages(channel, port);
|
|
131
|
+
SpessaSynthInfo(`%cRemoving channel %c${c}%c!`,
|
|
132
|
+
consoleColors.info,
|
|
133
|
+
consoleColors.recognized,
|
|
134
|
+
consoleColors.info);
|
|
135
|
+
});
|
|
136
|
+
let addedGs = false;
|
|
137
|
+
let midiSystem = "gs";
|
|
138
|
+
/**
|
|
139
|
+
* find all controller changes in the file
|
|
140
|
+
* @type {{
|
|
141
|
+
* track: number,
|
|
142
|
+
* message: MidiMessage,
|
|
143
|
+
* channel: number
|
|
144
|
+
* }[]}
|
|
145
|
+
*/
|
|
146
|
+
const ccChanges = [];
|
|
147
|
+
/**
|
|
148
|
+
* @type {{
|
|
149
|
+
* track: number,
|
|
150
|
+
* message: MidiMessage,
|
|
151
|
+
* channel: number
|
|
152
|
+
* }[]}
|
|
153
|
+
*/
|
|
154
|
+
const programChanges = [];
|
|
155
|
+
midi.tracks.forEach((track, trackNum) => {
|
|
156
|
+
track.forEach(message => {
|
|
157
|
+
const status = message.messageStatusByte & 0xF0;
|
|
158
|
+
if(status === messageTypes.controllerChange)
|
|
159
|
+
{
|
|
160
|
+
ccChanges.push({
|
|
161
|
+
track: trackNum,
|
|
162
|
+
message: message,
|
|
163
|
+
channel: message.messageStatusByte & 0xF
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
else if(status === messageTypes.programChange)
|
|
167
|
+
{
|
|
168
|
+
programChanges.push({
|
|
169
|
+
track: trackNum,
|
|
170
|
+
message: message,
|
|
171
|
+
channel: message.messageStatusByte & 0xF
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
else if(message.messageStatusByte === messageTypes.systemExclusive)
|
|
175
|
+
{
|
|
176
|
+
// check for xg
|
|
177
|
+
if(
|
|
178
|
+
message.messageData[0] === 0x43 && // Yamaha
|
|
179
|
+
message.messageData[2] === 0x4C && // XG ON
|
|
180
|
+
message.messageData[5] === 0x7E &&
|
|
181
|
+
message.messageData[6] === 0x00
|
|
182
|
+
)
|
|
183
|
+
{
|
|
184
|
+
SpessaSynthInfo("%cXG system on detected", consoleColors.info);
|
|
185
|
+
midiSystem = "xg";
|
|
186
|
+
addedGs = true; // flag as true so gs won't get added
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const getFirstVoiceForChannel = (chan, port) => {
|
|
193
|
+
return midi.tracks
|
|
194
|
+
.reduce((noteOns, track, trackNum) => {
|
|
195
|
+
if(midi.usedChannelsOnTrack[trackNum].has(chan) && midi.midiPorts[trackNum] === port)
|
|
196
|
+
{
|
|
197
|
+
const eventIndex = track.findIndex(event =>
|
|
198
|
+
// event is a voice event
|
|
199
|
+
(event.messageStatusByte > 0x80 && event.messageStatusByte < 0xF0) &&
|
|
200
|
+
// event has the channel we want
|
|
201
|
+
(event.messageStatusByte & 0xF) === chan &&
|
|
202
|
+
// event is not a controller change which resets all controllers or kills all sounds
|
|
203
|
+
(
|
|
204
|
+
(event.messageStatusByte & 0xF0) === messageTypes.controllerChange &&
|
|
205
|
+
event.messageData[0] !== midiControllers.resetAllControllers &&
|
|
206
|
+
event.messageData[0] !== midiControllers.allNotesOff &&
|
|
207
|
+
event.messageData[0] !== midiControllers.allSoundOff
|
|
208
|
+
)
|
|
209
|
+
);
|
|
210
|
+
if(eventIndex !== -1)
|
|
211
|
+
{
|
|
212
|
+
noteOns.push({
|
|
213
|
+
index: eventIndex,
|
|
214
|
+
track: trackNum
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return noteOns;
|
|
219
|
+
}, []);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param channel {number}
|
|
225
|
+
* @param port {number}
|
|
226
|
+
* @param cc {number}
|
|
227
|
+
*/
|
|
228
|
+
const clearControllers = (channel, port, cc,) => {
|
|
229
|
+
const thisCcChanges = ccChanges.filter(m =>
|
|
230
|
+
m.channel === channel
|
|
231
|
+
&& m.message.messageData[0] === cc
|
|
232
|
+
&& midi.midiPorts[m.track] === port);
|
|
233
|
+
// delete
|
|
234
|
+
for(let i = 0; i < thisCcChanges.length; i++)
|
|
235
|
+
{
|
|
236
|
+
// remove
|
|
237
|
+
const e = thisCcChanges[i];
|
|
238
|
+
midi.tracks[e.track].splice(midi.tracks[e.track].indexOf(e.message), 1);
|
|
239
|
+
ccChanges.splice(ccChanges.indexOf(e), 1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
}
|
|
243
|
+
desiredControllerChanges.forEach(desiredChange => {
|
|
244
|
+
const channel = desiredChange.channel;
|
|
245
|
+
const midiChannel = channel % 16;
|
|
246
|
+
const port = Math.floor(channel / 16);
|
|
247
|
+
const targetValue = desiredChange.controllerValue;
|
|
248
|
+
const ccNumber = desiredChange.controllerNumber;
|
|
249
|
+
// the controller is locked. Clear all controllers
|
|
250
|
+
clearControllers(midiChannel, port, ccNumber);
|
|
251
|
+
// since we've removed all ccs, we need to add the first one.
|
|
252
|
+
SpessaSynthInfo(`%cNo controller %c${ccNumber}%c on channel %c${channel}%c found. Adding it!`,
|
|
253
|
+
consoleColors.info,
|
|
254
|
+
consoleColors.unrecognized,
|
|
255
|
+
consoleColors.info,
|
|
256
|
+
consoleColors.value,
|
|
257
|
+
consoleColors.info
|
|
258
|
+
);
|
|
259
|
+
/**
|
|
260
|
+
* @type {{index: number, track: number}[]}
|
|
261
|
+
*/
|
|
262
|
+
const firstNoteOnForTrack = getFirstVoiceForChannel(midiChannel, port);
|
|
263
|
+
if(firstNoteOnForTrack.length === 0)
|
|
264
|
+
{
|
|
265
|
+
SpessaSynthWarn("Program change but no notes... ignoring!");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const firstNoteOn = firstNoteOnForTrack.reduce((first, current) =>
|
|
269
|
+
midi.tracks[current.track][current.index].ticks < midi.tracks[first.track][first.index] ? current : first);
|
|
270
|
+
// prepend with controller change
|
|
271
|
+
const ccChange = getControllerChange(midiChannel, ccNumber, targetValue, midi.tracks[firstNoteOn.track][firstNoteOn.index].ticks);
|
|
272
|
+
midi.tracks[firstNoteOn.track].splice(firstNoteOn.index, 0, ccChange);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
desiredProgramChanges.forEach(change => {
|
|
276
|
+
const midiChannel = change.channel % 16;
|
|
277
|
+
const port = Math.floor(change.channel / 16);
|
|
278
|
+
let desiredBank = change.isDrum ? 0 : change.bank;
|
|
279
|
+
const desiredProgram = change.program;
|
|
280
|
+
|
|
281
|
+
// get the program changes that are relevant for this channel (and port)
|
|
282
|
+
const thisProgramChanges = programChanges.filter(c => midi.midiPorts[c.track] === port && c.channel === midiChannel);
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
// clear bank selects
|
|
286
|
+
clearControllers(midiChannel, port, midiControllers.bankSelect);
|
|
287
|
+
clearControllers(midiChannel, port, midiControllers.lsbForControl0BankSelect);
|
|
288
|
+
|
|
289
|
+
// if drums or the program uses bank select, flag as gs
|
|
290
|
+
if((change.isDrum || desiredBank > 0) && !addedGs)
|
|
291
|
+
{
|
|
292
|
+
// make sure that GS is on
|
|
293
|
+
// GS on: F0 41 10 42 12 40 00 7F 00 41 F7
|
|
294
|
+
midi.tracks.forEach(track => {
|
|
295
|
+
for(let eventIndex = 0; eventIndex < track.length; eventIndex++)
|
|
296
|
+
{
|
|
297
|
+
const event = track[eventIndex];
|
|
298
|
+
if(event.messageStatusByte === messageTypes.systemExclusive)
|
|
299
|
+
{
|
|
300
|
+
if(
|
|
301
|
+
event.messageData[0] === 0x41 // roland
|
|
302
|
+
&& event.messageData[2] === 0x42 // GS
|
|
303
|
+
&& event.messageData[6] === 0x7F // Mode set
|
|
304
|
+
)
|
|
305
|
+
{
|
|
306
|
+
// thats a GS on, we're done here
|
|
307
|
+
addedGs = true;
|
|
308
|
+
SpessaSynthInfo("%cGS on detected!", consoleColors.recognized);
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
else if(
|
|
312
|
+
event.messageData[0] === 0x7E // non realtime
|
|
313
|
+
&& event.messageData[2] === 0x09 // gm system
|
|
314
|
+
)
|
|
315
|
+
{
|
|
316
|
+
// thats a GM/2 system change, remove it!
|
|
317
|
+
SpessaSynthInfo("%cGM/2 on detected, removing!", consoleColors.info);
|
|
318
|
+
track.splice(eventIndex, 1);
|
|
319
|
+
// adjust program and bank changes
|
|
320
|
+
eventIndex--;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
});
|
|
326
|
+
if(!addedGs)
|
|
327
|
+
{
|
|
328
|
+
// gs is not on, add it on the first track at index 0 (or 1 if track name is first)
|
|
329
|
+
let index = 0;
|
|
330
|
+
if(midi.tracks[0][0].messageStatusByte === messageTypes.trackName)
|
|
331
|
+
index++;
|
|
332
|
+
midi.tracks[0].splice(index, 0, getGsOn(0));
|
|
333
|
+
SpessaSynthInfo("%cGS on not detected. Adding it.", consoleColors.info);
|
|
334
|
+
addedGs = true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// remove all program changes
|
|
339
|
+
for(const change of thisProgramChanges)
|
|
340
|
+
{
|
|
341
|
+
midi.tracks[change.track].splice(midi.tracks[change.track].indexOf(change.message), 1);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Find the first voice message
|
|
345
|
+
* @type {{index: number, track: number}[]}
|
|
346
|
+
*/
|
|
347
|
+
const firstVoiceForTrack = getFirstVoiceForChannel(midiChannel, port);
|
|
348
|
+
if(firstVoiceForTrack.length === 0)
|
|
349
|
+
{
|
|
350
|
+
SpessaSynthWarn("Program change but no notes... ignoring!");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
// get the first voice overall
|
|
354
|
+
const firstVoice = firstVoiceForTrack.reduce((first, current) =>
|
|
355
|
+
midi.tracks[current.track][current.index].ticks < midi.tracks[first.track][first.index] ? current : first);
|
|
356
|
+
// get the index and ticks
|
|
357
|
+
let firstIndex = firstVoice.index;
|
|
358
|
+
const ticks = midi.tracks[firstVoice.track][firstVoice.index].ticks;
|
|
359
|
+
|
|
360
|
+
// add drums if needed
|
|
361
|
+
if(change.isDrum)
|
|
362
|
+
{
|
|
363
|
+
// do not add gs drum change on drum channel
|
|
364
|
+
if(midiSystem === "gs" && midiChannel !== DEFAULT_PERCUSSION)
|
|
365
|
+
{
|
|
366
|
+
SpessaSynthInfo(`%cAdding GS Drum change on track %c${firstVoice.track}`,
|
|
367
|
+
consoleColors.recognized,
|
|
368
|
+
consoleColors.value
|
|
369
|
+
);
|
|
370
|
+
midi.tracks[firstVoice.track].splice(firstIndex, 0, getDrumChange(change.channel, ticks));
|
|
371
|
+
firstIndex++;
|
|
372
|
+
}
|
|
373
|
+
else if(midiSystem === "xg")
|
|
374
|
+
{
|
|
375
|
+
SpessaSynthInfo(`%cAdding XG Drum change on track %c${firstVoice.track}`,
|
|
376
|
+
consoleColors.recognized,
|
|
377
|
+
consoleColors.value
|
|
378
|
+
);
|
|
379
|
+
// system is xg. drums are on msb bank 127.
|
|
380
|
+
desiredBank = 127;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
SpessaSynthInfo(`%cSetting %c${change.channel}%c to %c${desiredBank}:${desiredProgram}`,
|
|
385
|
+
consoleColors.info,
|
|
386
|
+
consoleColors.recognized,
|
|
387
|
+
consoleColors.info,
|
|
388
|
+
consoleColors.recognized);
|
|
389
|
+
|
|
390
|
+
// add bank
|
|
391
|
+
const bankChange = getControllerChange(midiChannel, midiControllers.bankSelect, desiredBank, ticks);
|
|
392
|
+
midi.tracks[firstVoice.track].splice(firstIndex, 0, bankChange);
|
|
393
|
+
firstIndex++;
|
|
394
|
+
|
|
395
|
+
// add program change
|
|
396
|
+
const programChange = new MidiMessage(
|
|
397
|
+
ticks,
|
|
398
|
+
messageTypes.programChange | midiChannel,
|
|
399
|
+
new IndexedByteArray([
|
|
400
|
+
desiredProgram
|
|
401
|
+
])
|
|
402
|
+
);
|
|
403
|
+
midi.tracks[firstVoice.track].splice(firstIndex, 0, programChange);
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// transpose channels
|
|
409
|
+
for(const transpose of desiredChannelsToTranspose)
|
|
410
|
+
{
|
|
411
|
+
const midiChannel = transpose.channel % 16;
|
|
412
|
+
const port = Math.floor(transpose.channel / 16);
|
|
413
|
+
const keyShift = Math.trunc(transpose.keyShift);
|
|
414
|
+
const fineTune = transpose.keyShift - keyShift;
|
|
415
|
+
SpessaSynthInfo(`%cTransposing channel %c${transpose.channel}%c by %c${transpose.keyShift}%c semitones`,
|
|
416
|
+
consoleColors.info,
|
|
417
|
+
consoleColors.recognized,
|
|
418
|
+
consoleColors.info,
|
|
419
|
+
consoleColors.value,
|
|
420
|
+
consoleColors.info);
|
|
421
|
+
if(keyShift !== 0)
|
|
422
|
+
{
|
|
423
|
+
midi.tracks.forEach((track, trackNum) => {
|
|
424
|
+
if (
|
|
425
|
+
midi.midiPorts[trackNum] !== port ||
|
|
426
|
+
!midi.usedChannelsOnTrack[trackNum].has(midiChannel)
|
|
427
|
+
) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const onStatus = messageTypes.noteOn | midiChannel;
|
|
431
|
+
const offStatus = messageTypes.noteOff | midiChannel;
|
|
432
|
+
const polyStatus = messageTypes.polyPressure | midiChannel;
|
|
433
|
+
track.forEach(event => {
|
|
434
|
+
if (
|
|
435
|
+
event.messageStatusByte !== onStatus &&
|
|
436
|
+
event.messageStatusByte !== offStatus &&
|
|
437
|
+
event.messageStatusByte !== polyStatus
|
|
438
|
+
) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
event.messageData[0] = Math.max(0, Math.min(127, event.messageData[0] + keyShift));
|
|
442
|
+
})
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if(fineTune !== 0)
|
|
447
|
+
{
|
|
448
|
+
// find the first track that uses this channel
|
|
449
|
+
const track = midi.tracks.find((t, tNum) => midi.usedChannelsOnTrack[tNum].has(transpose.channel));
|
|
450
|
+
if(track === undefined)
|
|
451
|
+
{
|
|
452
|
+
SpessaSynthWarn(`Channel ${transpose.channel} unused but transpose requested???`);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
// find first noteon for this channel
|
|
456
|
+
const noteOn = messageTypes.noteOn | (transpose.channel % 16);
|
|
457
|
+
const noteIndex = track.findIndex(n => n.messageStatusByte === noteOn);
|
|
458
|
+
if(noteIndex === -1)
|
|
459
|
+
{
|
|
460
|
+
SpessaSynthWarn(`No notes on channel ${transpose.channel} but transpose requested???`);
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
const ticks = track[noteIndex].ticks;
|
|
464
|
+
// add rpn
|
|
465
|
+
// 64 is the center, 96 = 50 cents up
|
|
466
|
+
const centsCoarse = (fineTune * 64) + 64;
|
|
467
|
+
const ccChange = messageTypes.controllerChange | (transpose.channel % 16);
|
|
468
|
+
const rpnCoarse = new MidiMessage(ticks, ccChange, new IndexedByteArray([midiControllers.RPNMsb, 0]));
|
|
469
|
+
const rpnFine = new MidiMessage(ticks, ccChange, new IndexedByteArray([midiControllers.RPNLsb, 1]));
|
|
470
|
+
const deCoarse = new MidiMessage(ticks, ccChange, new IndexedByteArray([midiControllers.dataEntryMsb, centsCoarse]));
|
|
471
|
+
const deFine = new MidiMessage(ticks, ccChange, new IndexedByteArray([midiControllers.lsbForControl6DataEntry, 0]));
|
|
472
|
+
// add in reverse
|
|
473
|
+
track.splice(noteIndex, 0, deFine);
|
|
474
|
+
track.splice(noteIndex, 0, deCoarse);
|
|
475
|
+
track.splice(noteIndex, 0, rpnFine);
|
|
476
|
+
track.splice(noteIndex, 0, rpnCoarse);
|
|
477
|
+
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
SpessaSynthGroupEnd();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Modifies the sequence according to the locked presets and controllers in the given snapshot
|
|
485
|
+
* @param midi {MIDI}
|
|
486
|
+
* @param snapshot {SynthesizerSnapshot}
|
|
487
|
+
*/
|
|
488
|
+
export function applySnapshotToMIDI(midi, snapshot)
|
|
489
|
+
{
|
|
490
|
+
/**
|
|
491
|
+
* @type {{
|
|
492
|
+
* channel: number,
|
|
493
|
+
* keyShift: number
|
|
494
|
+
* }[]}
|
|
495
|
+
*/
|
|
496
|
+
const channelsToTranspose = [];
|
|
497
|
+
/**
|
|
498
|
+
* @type {number[]}
|
|
499
|
+
*/
|
|
500
|
+
const channelsToClear = [];
|
|
501
|
+
/**
|
|
502
|
+
* @type {{
|
|
503
|
+
* channel: number,
|
|
504
|
+
* program: number,
|
|
505
|
+
* bank: number,
|
|
506
|
+
* isDrum: boolean
|
|
507
|
+
* }[]}
|
|
508
|
+
*/
|
|
509
|
+
const programChanges = [];
|
|
510
|
+
/**
|
|
511
|
+
*
|
|
512
|
+
* @type {{
|
|
513
|
+
* channel: number,
|
|
514
|
+
* controllerNumber: number,
|
|
515
|
+
* controllerValue: number
|
|
516
|
+
* }[]}
|
|
517
|
+
*/
|
|
518
|
+
const controllerChanges = [];
|
|
519
|
+
snapshot.channelSnapshots.forEach((channel, channelNumber) => {
|
|
520
|
+
if(channel.isMuted)
|
|
521
|
+
{
|
|
522
|
+
channelsToClear.push(channelNumber);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
const transposeFloat = channel.channelTransposeKeyShift + channel.customControllers[customControllers.channelTransposeFine] / 100;
|
|
526
|
+
if(transposeFloat !== 0)
|
|
527
|
+
{
|
|
528
|
+
channelsToTranspose.push({
|
|
529
|
+
channel: channelNumber,
|
|
530
|
+
keyShift: transposeFloat,
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
if(channel.lockPreset)
|
|
534
|
+
{
|
|
535
|
+
programChanges.push({
|
|
536
|
+
channel: channelNumber,
|
|
537
|
+
program: channel.program,
|
|
538
|
+
bank: channel.bank,
|
|
539
|
+
isDrum: channel.drumChannel
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
// check for locked controllers and change them appropriately
|
|
543
|
+
channel.lockedControllers.forEach((l, ccNumber) => {
|
|
544
|
+
if(!l || ccNumber > 127 || ccNumber === midiControllers.bankSelect)
|
|
545
|
+
{
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const targetValue = channel.midiControllers[ccNumber] >> 7; // channel controllers are stored as 14 bit values
|
|
549
|
+
controllerChanges.push({
|
|
550
|
+
channel: channelNumber,
|
|
551
|
+
controllerNumber: ccNumber,
|
|
552
|
+
controllerValue: targetValue
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
modifyMIDI(midi, programChanges, controllerChanges, channelsToClear, channelsToTranspose);
|
|
557
|
+
}
|