spessasynth_lib 3.24.10 → 3.24.12
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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { messageTypes, midiControllers, MidiMessage } from "./midi_message.js";
|
|
2
2
|
import { IndexedByteArray } from "../utils/indexed_array.js";
|
|
3
|
-
import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo
|
|
3
|
+
import { SpessaSynthGroupCollapsed, SpessaSynthGroupEnd, SpessaSynthInfo } from "../utils/loggin.js";
|
|
4
4
|
import { consoleColors } from "../utils/other.js";
|
|
5
5
|
import { DEFAULT_PERCUSSION } from "../synthetizer/synthetizer.js";
|
|
6
6
|
|
|
@@ -20,16 +20,23 @@ export function getGsOn(ticks)
|
|
|
20
20
|
0x10, // Device ID (defaults to 16 on roland)
|
|
21
21
|
0x42, // GS
|
|
22
22
|
0x12, // Command ID (DT1) (whatever that means...)
|
|
23
|
-
0x40, // System parameter
|
|
24
|
-
0x00, // Global parameter
|
|
25
|
-
0x7F, // GS Change
|
|
26
|
-
0x00, // turn on
|
|
23
|
+
0x40, // System parameter - Address
|
|
24
|
+
0x00, // Global parameter - Address
|
|
25
|
+
0x7F, // GS Change - Address
|
|
26
|
+
0x00, // turn on - Data
|
|
27
27
|
0x41, // checksum
|
|
28
28
|
0xF7 // end of exclusive
|
|
29
29
|
])
|
|
30
30
|
);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* @param channel {number}
|
|
35
|
+
* @param cc {number}
|
|
36
|
+
* @param value {number}
|
|
37
|
+
* @param ticks {number}
|
|
38
|
+
* @returns {MidiMessage}
|
|
39
|
+
*/
|
|
33
40
|
function getControllerChange(channel, cc, value, ticks)
|
|
34
41
|
{
|
|
35
42
|
return new MidiMessage(
|
|
@@ -56,7 +63,7 @@ function getDrumChange(channel, ticks)
|
|
|
56
63
|
0x40, // System parameter }
|
|
57
64
|
chanAddress, // Channel parameter } Address
|
|
58
65
|
0x15, // Drum change }
|
|
59
|
-
0x01 // Is Drums
|
|
66
|
+
0x01 // Is Drums } Data
|
|
60
67
|
];
|
|
61
68
|
// calculate checksum
|
|
62
69
|
// https://cdn.roland.com/assets/media/pdf/F-20_MIDI_Imple_e01_W.pdf section 4
|
|
@@ -98,6 +105,7 @@ function getDrumChange(channel, ticks)
|
|
|
98
105
|
* which will be used to fine-tune the pitch in cents using RPN.
|
|
99
106
|
*/
|
|
100
107
|
|
|
108
|
+
|
|
101
109
|
/**
|
|
102
110
|
* Allows easy editing of the file by removing channels, changing programs,
|
|
103
111
|
* changing controllers and transposing channels. Note that this modifies the MIDI in-place.
|
|
@@ -117,494 +125,402 @@ export function modifyMIDI(
|
|
|
117
125
|
)
|
|
118
126
|
{
|
|
119
127
|
SpessaSynthGroupCollapsed("%cApplying changes to the MIDI file...", consoleColors.info);
|
|
128
|
+
|
|
120
129
|
/**
|
|
121
|
-
* @
|
|
122
|
-
* @param port {number}
|
|
130
|
+
* @type {Set<number>}
|
|
123
131
|
*/
|
|
124
|
-
const
|
|
132
|
+
const channelsToChangeProgram = new Set();
|
|
133
|
+
desiredProgramChanges.forEach(c =>
|
|
125
134
|
{
|
|
126
|
-
|
|
127
|
-
{
|
|
128
|
-
if (midi.midiPorts[trackNum] !== port)
|
|
129
|
-
{
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
for (let i = track.length - 1; i >= 0; i--) // iterate in reverse to not mess up indexes
|
|
133
|
-
{
|
|
134
|
-
if (track[i].messageStatusByte >= 0x80 && track[i].messageStatusByte < 0xF0) // do not clear sysexes
|
|
135
|
-
{
|
|
136
|
-
if ((track[i].messageStatusByte & 0xF) === channel)
|
|
137
|
-
{
|
|
138
|
-
track.splice(i, 1);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
});
|
|
143
|
-
};
|
|
144
|
-
desiredChannelsToClear.forEach(c =>
|
|
145
|
-
{
|
|
146
|
-
const channel = c % 16;
|
|
147
|
-
const offset = c - channel;
|
|
148
|
-
const port = midi.midiPortChannelOffsets.findIndex(o => o === offset);
|
|
149
|
-
clearChannelMessages(channel, port);
|
|
150
|
-
SpessaSynthInfo(
|
|
151
|
-
`%cRemoving channel %c${c}%c!`,
|
|
152
|
-
consoleColors.info,
|
|
153
|
-
consoleColors.recognized,
|
|
154
|
-
consoleColors.info
|
|
155
|
-
);
|
|
135
|
+
channelsToChangeProgram.add(c.channel);
|
|
156
136
|
});
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
// go through all events one by one
|
|
140
|
+
let system = "gs";
|
|
157
141
|
let addedGs = false;
|
|
158
|
-
let midiSystem = "gs";
|
|
159
142
|
/**
|
|
160
|
-
*
|
|
161
|
-
* @type {
|
|
162
|
-
* track: number,
|
|
163
|
-
* message: MidiMessage,
|
|
164
|
-
* channel: number
|
|
165
|
-
* }[]}
|
|
166
|
-
*/
|
|
167
|
-
const ccChanges = [];
|
|
168
|
-
/**
|
|
169
|
-
* @type {{
|
|
170
|
-
* track: number,
|
|
171
|
-
* message: MidiMessage,
|
|
172
|
-
* channel: number
|
|
173
|
-
* }[]}
|
|
143
|
+
* indexes for tracks
|
|
144
|
+
* @type {number[]}
|
|
174
145
|
*/
|
|
175
|
-
const
|
|
176
|
-
midi.tracks.
|
|
146
|
+
const eventIndexes = Array(midi.tracks.length).fill(0);
|
|
147
|
+
let remainingTracks = midi.tracks.length;
|
|
148
|
+
|
|
149
|
+
function findFirstEventIndex()
|
|
177
150
|
{
|
|
178
|
-
|
|
151
|
+
let index = 0;
|
|
152
|
+
let ticks = Infinity;
|
|
153
|
+
midi.tracks.forEach((track, i) =>
|
|
179
154
|
{
|
|
180
|
-
|
|
181
|
-
if (status === messageTypes.controllerChange)
|
|
155
|
+
if (eventIndexes[i] >= track.length)
|
|
182
156
|
{
|
|
183
|
-
|
|
184
|
-
track: trackNum,
|
|
185
|
-
message: message,
|
|
186
|
-
channel: message.messageStatusByte & 0xF
|
|
187
|
-
});
|
|
157
|
+
return;
|
|
188
158
|
}
|
|
189
|
-
|
|
159
|
+
if (track[eventIndexes[i]].ticks < ticks)
|
|
190
160
|
{
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
message: message,
|
|
194
|
-
channel: message.messageStatusByte & 0xF
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
else if (message.messageStatusByte === messageTypes.systemExclusive)
|
|
198
|
-
{
|
|
199
|
-
// check for xg
|
|
200
|
-
if (
|
|
201
|
-
message.messageData[0] === 0x43 && // Yamaha
|
|
202
|
-
message.messageData[2] === 0x4C && // XG ON
|
|
203
|
-
message.messageData[5] === 0x7E &&
|
|
204
|
-
message.messageData[6] === 0x00
|
|
205
|
-
)
|
|
206
|
-
{
|
|
207
|
-
SpessaSynthInfo("%cXG system on detected", consoleColors.info);
|
|
208
|
-
midiSystem = "xg";
|
|
209
|
-
addedGs = true; // flag as true so gs won't get added
|
|
210
|
-
}
|
|
211
|
-
else
|
|
212
|
-
// check for xg program change
|
|
213
|
-
if (
|
|
214
|
-
message.messageData[0] === 0x43 // yamaha
|
|
215
|
-
&& message.messageData[2] === 0x4C // XG
|
|
216
|
-
&& message.messageData[3] === 0x08 // part parameter
|
|
217
|
-
&& message.messageData[5] === 0x03 // program change
|
|
218
|
-
)
|
|
219
|
-
{
|
|
220
|
-
programChanges.push({
|
|
221
|
-
track: trackNum,
|
|
222
|
-
message: message,
|
|
223
|
-
channel: message.messageData[4]
|
|
224
|
-
});
|
|
225
|
-
}
|
|
161
|
+
index = i;
|
|
162
|
+
ticks = track[eventIndexes[i]].ticks;
|
|
226
163
|
}
|
|
227
164
|
});
|
|
228
|
-
|
|
165
|
+
return index;
|
|
166
|
+
}
|
|
229
167
|
|
|
168
|
+
// it copies midiPorts everywhere else, but here 0 works so DO NOT CHANGE!
|
|
230
169
|
/**
|
|
231
|
-
*
|
|
232
|
-
* @
|
|
233
|
-
* @param searchForNoteOn {boolean} search for note on if true, any voice otherwise.
|
|
234
|
-
* first note on is needed because multi port midis like to reference other ports before playing to the port we want.
|
|
235
|
-
* First voice otherwise, because MP6 doesn't like program changes after cc changes in embedded midis
|
|
236
|
-
* @return {{index: number, track: number}[]}
|
|
170
|
+
* midi port number for the corresponding track
|
|
171
|
+
* @type {number[]}
|
|
237
172
|
*/
|
|
238
|
-
const
|
|
239
|
-
{
|
|
240
|
-
return midi.tracks
|
|
241
|
-
.reduce((noteOns, track, trackNum) =>
|
|
242
|
-
{
|
|
243
|
-
if (midi.usedChannelsOnTrack[trackNum].has(chan) && midi.midiPorts[trackNum] === port)
|
|
244
|
-
{
|
|
245
|
-
let eventIndex;
|
|
246
|
-
if (searchForNoteOn)
|
|
247
|
-
{
|
|
248
|
-
eventIndex = track.findIndex(event =>
|
|
249
|
-
// event is a noteon
|
|
250
|
-
(event.messageStatusByte & 0xF0) === messageTypes.noteOn);
|
|
251
|
-
}
|
|
252
|
-
else
|
|
253
|
-
{
|
|
254
|
-
eventIndex = track.findIndex(event =>
|
|
255
|
-
// event is a voice event
|
|
256
|
-
(event.messageStatusByte > 0x80 && event.messageStatusByte < 0xF0) &&
|
|
257
|
-
// event has the channel we want
|
|
258
|
-
(event.messageStatusByte & 0xF) === chan &&
|
|
259
|
-
// event is not one of the controller changes that reset things
|
|
260
|
-
!(
|
|
261
|
-
(event.messageStatusByte & 0xF0 === messageTypes.controllerChange) &&
|
|
262
|
-
(
|
|
263
|
-
event.messageData[0] === midiControllers.resetAllControllers ||
|
|
264
|
-
event.messageData[0] === midiControllers.allNotesOff ||
|
|
265
|
-
event.messageData[0] === midiControllers.allSoundOff
|
|
266
|
-
)
|
|
267
|
-
)
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
if (eventIndex !== -1)
|
|
271
|
-
{
|
|
272
|
-
noteOns.push({
|
|
273
|
-
index: eventIndex,
|
|
274
|
-
track: trackNum
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return noteOns;
|
|
279
|
-
}, []);
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
|
|
173
|
+
const midiPorts = midi.midiPorts.slice();
|
|
283
174
|
/**
|
|
284
|
-
*
|
|
285
|
-
* @
|
|
286
|
-
* @param cc {number}
|
|
175
|
+
* midi port: channel offset
|
|
176
|
+
* @type {Object<number, number>}
|
|
287
177
|
*/
|
|
288
|
-
const
|
|
178
|
+
const midiPortChannelOffsets = {};
|
|
179
|
+
let midiPortChannelOffset = 0;
|
|
180
|
+
|
|
181
|
+
function assignMIDIPort(trackNum, port)
|
|
289
182
|
{
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
&& m.message.messageData[0] === cc
|
|
293
|
-
&& midi.midiPorts[m.track] === port);
|
|
294
|
-
// delete
|
|
295
|
-
for (let i = 0; i < thisCcChanges.length; i++)
|
|
183
|
+
// do not assign ports to empty tracks
|
|
184
|
+
if (midi.usedChannelsOnTrack[trackNum].size === 0)
|
|
296
185
|
{
|
|
297
|
-
|
|
298
|
-
const e = thisCcChanges[i];
|
|
299
|
-
midi.tracks[e.track].splice(midi.tracks[e.track].indexOf(e.message), 1);
|
|
300
|
-
ccChanges.splice(ccChanges.indexOf(e), 1);
|
|
186
|
+
return;
|
|
301
187
|
}
|
|
302
188
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
{
|
|
306
|
-
const channel = desiredChange.channel;
|
|
307
|
-
const midiChannel = channel % 16;
|
|
308
|
-
const offset = channel - midiChannel;
|
|
309
|
-
const port = midi.midiPortChannelOffsets.findIndex(o => o === offset);
|
|
310
|
-
const targetValue = desiredChange.controllerValue;
|
|
311
|
-
const ccNumber = desiredChange.controllerNumber;
|
|
312
|
-
// the controller is locked. Clear all controllers
|
|
313
|
-
clearControllers(midiChannel, port, ccNumber);
|
|
314
|
-
// since we've removed all ccs, we need to add the first one.
|
|
315
|
-
SpessaSynthInfo(
|
|
316
|
-
`%cNo controller %c${ccNumber}%c on channel %c${channel}%c found. Adding it!`,
|
|
317
|
-
consoleColors.info,
|
|
318
|
-
consoleColors.unrecognized,
|
|
319
|
-
consoleColors.info,
|
|
320
|
-
consoleColors.value,
|
|
321
|
-
consoleColors.info
|
|
322
|
-
);
|
|
323
|
-
/**
|
|
324
|
-
* @type {{index: number, track: number}[]}
|
|
325
|
-
*/
|
|
326
|
-
const firstNoteOnForTrack = getFirstVoiceForChannel(midiChannel, port, true);
|
|
327
|
-
if (firstNoteOnForTrack.length === 0)
|
|
189
|
+
// assign new 16 channels if the port is not occupied yet
|
|
190
|
+
if (midiPortChannelOffset === 0)
|
|
328
191
|
{
|
|
329
|
-
|
|
330
|
-
|
|
192
|
+
midiPortChannelOffset += 16;
|
|
193
|
+
midiPortChannelOffsets[port] = 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (midiPortChannelOffsets[port] === undefined)
|
|
197
|
+
{
|
|
198
|
+
midiPortChannelOffsets[port] = midiPortChannelOffset;
|
|
199
|
+
midiPortChannelOffset += 16;
|
|
331
200
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
201
|
+
|
|
202
|
+
midiPorts[trackNum] = port;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// assign port offsets
|
|
206
|
+
midi.midiPorts.forEach((port, trackIndex) =>
|
|
207
|
+
{
|
|
208
|
+
assignMIDIPort(trackIndex, port);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const channelsAmount = midiPortChannelOffset;
|
|
212
|
+
/**
|
|
213
|
+
* Tracks if the channel already had its first note on
|
|
214
|
+
* @type {boolean[]}
|
|
215
|
+
*/
|
|
216
|
+
const isFirstNoteOn = Array(channelsAmount).fill(true);
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* MIDI key transpose
|
|
220
|
+
* @type {number[]}
|
|
221
|
+
*/
|
|
222
|
+
const coarseTranspose = Array(channelsAmount).fill(0);
|
|
223
|
+
/**
|
|
224
|
+
* RPN fine transpose
|
|
225
|
+
* @type {number[]}
|
|
226
|
+
*/
|
|
227
|
+
const fineTranspose = Array(channelsAmount).fill(0);
|
|
228
|
+
desiredChannelsToTranspose.forEach(transpose =>
|
|
229
|
+
{
|
|
230
|
+
const coarse = Math.trunc(transpose.keyShift);
|
|
231
|
+
const fine = transpose.keyShift - coarse;
|
|
232
|
+
coarseTranspose[transpose.channel] = coarse;
|
|
233
|
+
fineTranspose[transpose.channel] = fine;
|
|
342
234
|
});
|
|
343
235
|
|
|
344
|
-
|
|
236
|
+
while (remainingTracks > 0)
|
|
345
237
|
{
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
238
|
+
let trackNum = findFirstEventIndex();
|
|
239
|
+
const track = midi.tracks[trackNum];
|
|
240
|
+
if (eventIndexes[trackNum] >= track.length)
|
|
241
|
+
{
|
|
242
|
+
remainingTracks--;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const index = eventIndexes[trackNum]++;
|
|
246
|
+
const e = track[index];
|
|
351
247
|
|
|
352
|
-
|
|
353
|
-
|
|
248
|
+
const deleteThisEvent = () =>
|
|
249
|
+
{
|
|
250
|
+
track.splice(index, 1);
|
|
251
|
+
eventIndexes[trackNum]--;
|
|
252
|
+
};
|
|
354
253
|
|
|
254
|
+
/**
|
|
255
|
+
* @param e {MidiMessage}
|
|
256
|
+
* @param offset{number}
|
|
257
|
+
*/
|
|
258
|
+
const addEventBefore = (e, offset = 0) =>
|
|
259
|
+
{
|
|
260
|
+
track.splice(index + offset, 0, e);
|
|
261
|
+
eventIndexes[trackNum]++;
|
|
262
|
+
};
|
|
355
263
|
|
|
356
|
-
// clear bank selects
|
|
357
|
-
clearControllers(midiChannel, port, midiControllers.bankSelect);
|
|
358
|
-
clearControllers(midiChannel, port, midiControllers.lsbForControl0BankSelect);
|
|
359
264
|
|
|
360
|
-
|
|
361
|
-
if (
|
|
265
|
+
let portOffset = midiPortChannelOffsets[midiPorts[trackNum]] || 0;
|
|
266
|
+
if (e.messageStatusByte === messageTypes.midiPort)
|
|
362
267
|
{
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
268
|
+
assignMIDIPort(trackNum, e.messageData[0]);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
// don't clear meta
|
|
272
|
+
if (e.messageStatusByte <= messageTypes.sequenceSpecific && e.messageStatusByte >= messageTypes.sequenceNumber)
|
|
273
|
+
{
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const status = e.messageStatusByte & 0xF0;
|
|
277
|
+
const midiChannel = e.messageStatusByte & 0xF;
|
|
278
|
+
const channel = midiChannel + portOffset;
|
|
279
|
+
// clear channel?
|
|
280
|
+
if (desiredChannelsToClear.indexOf(channel) !== -1)
|
|
281
|
+
{
|
|
282
|
+
deleteThisEvent();
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
switch (status)
|
|
286
|
+
{
|
|
287
|
+
case messageTypes.noteOn:
|
|
288
|
+
// is it first?
|
|
289
|
+
if (isFirstNoteOn[channel])
|
|
368
290
|
{
|
|
369
|
-
|
|
370
|
-
|
|
291
|
+
isFirstNoteOn[channel] = false;
|
|
292
|
+
// all right, so this is the first note on
|
|
293
|
+
// first: controllers
|
|
294
|
+
// because FSMP does not like program changes after cc changes in embedded midis
|
|
295
|
+
// and since we use splice,
|
|
296
|
+
// controllers get added first, then programs before them
|
|
297
|
+
// now add controllers
|
|
298
|
+
desiredControllerChanges.filter(c => c.channel === channel).forEach(change =>
|
|
371
299
|
{
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
300
|
+
const ccChange = getControllerChange(
|
|
301
|
+
midiChannel,
|
|
302
|
+
change.controllerNumber,
|
|
303
|
+
change.controllerValue,
|
|
304
|
+
e.ticks
|
|
305
|
+
);
|
|
306
|
+
addEventBefore(ccChange);
|
|
307
|
+
});
|
|
308
|
+
const fineTune = fineTranspose[channel];
|
|
309
|
+
|
|
310
|
+
if (fineTune !== 0)
|
|
311
|
+
{
|
|
312
|
+
// add rpn
|
|
313
|
+
// 64 is the center, 96 = 50 cents up
|
|
314
|
+
const centsCoarse = (fineTune * 64) + 64;
|
|
315
|
+
const rpnCoarse = getControllerChange(midiChannel, midiControllers.RPNMsb, 0, e.ticks);
|
|
316
|
+
const rpnFine = getControllerChange(midiChannel, midiControllers.RPNLsb, 1, e.ticks);
|
|
317
|
+
const dataEntryCoarse = getControllerChange(
|
|
318
|
+
channel,
|
|
319
|
+
midiControllers.dataEntryMsb,
|
|
320
|
+
centsCoarse,
|
|
321
|
+
e.ticks
|
|
322
|
+
);
|
|
323
|
+
const dataEntryFine = getControllerChange(
|
|
324
|
+
midiChannel,
|
|
325
|
+
midiControllers.lsbForControl6DataEntry,
|
|
326
|
+
0,
|
|
327
|
+
e.ticks
|
|
328
|
+
);
|
|
329
|
+
addEventBefore(dataEntryFine);
|
|
330
|
+
addEventBefore(dataEntryCoarse);
|
|
331
|
+
addEventBefore(rpnFine);
|
|
332
|
+
addEventBefore(rpnCoarse);
|
|
333
|
+
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (channelsToChangeProgram.has(channel))
|
|
337
|
+
{
|
|
338
|
+
const change = desiredProgramChanges.find(c => c.channel === channel);
|
|
339
|
+
let desiredBank = change.bank;
|
|
340
|
+
const desiredProgram = change.program;
|
|
341
|
+
SpessaSynthInfo(
|
|
342
|
+
`%cSetting %c${change.channel}%c to %c${desiredBank}:${desiredProgram}%c. Track num: %c${trackNum}`,
|
|
343
|
+
consoleColors.info,
|
|
344
|
+
consoleColors.recognized,
|
|
345
|
+
consoleColors.info,
|
|
346
|
+
consoleColors.recognized,
|
|
347
|
+
consoleColors.info,
|
|
348
|
+
consoleColors.recognized
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
// note: this is in reverse.
|
|
352
|
+
// the output event order is: drums -> lsb -> msb -> program change
|
|
353
|
+
|
|
354
|
+
// add program change
|
|
355
|
+
const programChange = new MidiMessage(
|
|
356
|
+
e.ticks,
|
|
357
|
+
messageTypes.programChange | midiChannel,
|
|
358
|
+
new IndexedByteArray([
|
|
359
|
+
desiredProgram
|
|
360
|
+
])
|
|
361
|
+
);
|
|
362
|
+
addEventBefore(programChange);
|
|
363
|
+
|
|
364
|
+
// on xg, add lsb
|
|
365
|
+
if (!change.isDrum && system === "xg")
|
|
377
366
|
{
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
367
|
+
const bankChangeLSB = getControllerChange(
|
|
368
|
+
midiChannel,
|
|
369
|
+
midiControllers.lsbForControl0BankSelect,
|
|
370
|
+
desiredBank,
|
|
371
|
+
e.ticks
|
|
383
372
|
);
|
|
384
|
-
|
|
373
|
+
addEventBefore(bankChangeLSB);
|
|
385
374
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
375
|
+
|
|
376
|
+
// add the bank MSB
|
|
377
|
+
const bankChange = getControllerChange(
|
|
378
|
+
midiChannel,
|
|
379
|
+
midiControllers.bankSelect,
|
|
380
|
+
desiredBank,
|
|
381
|
+
e.ticks
|
|
382
|
+
);
|
|
383
|
+
addEventBefore(bankChange);
|
|
384
|
+
|
|
385
|
+
// is drums?
|
|
386
|
+
// if so, adjust
|
|
387
|
+
// do not add gs drum change on the drum channel
|
|
388
|
+
if (change.isDrum)
|
|
390
389
|
{
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
390
|
+
if (system === "gs" && midiChannel !== DEFAULT_PERCUSSION)
|
|
391
|
+
{
|
|
392
|
+
SpessaSynthInfo(
|
|
393
|
+
`%cAdding GS Drum change on track %c${trackNum}`,
|
|
394
|
+
consoleColors.recognized,
|
|
395
|
+
consoleColors.value
|
|
396
|
+
);
|
|
397
|
+
addEventBefore(getDrumChange(midiChannel, e.ticks));
|
|
398
|
+
}
|
|
399
|
+
else if (system === "xg")
|
|
400
|
+
{
|
|
401
|
+
SpessaSynthInfo(
|
|
402
|
+
`%cAdding XG Drum change on track %c${trackNum}`,
|
|
403
|
+
consoleColors.recognized,
|
|
404
|
+
consoleColors.value
|
|
405
|
+
);
|
|
406
|
+
// the system is xg. drums are on msb bank 127.
|
|
407
|
+
desiredBank = 127;
|
|
408
|
+
}
|
|
399
409
|
}
|
|
400
410
|
}
|
|
401
411
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
412
|
+
// transpose key (for zero it won't change anyway)
|
|
413
|
+
e.messageData[0] += coarseTranspose[channel];
|
|
414
|
+
break;
|
|
415
|
+
|
|
416
|
+
case messageTypes.noteOff:
|
|
417
|
+
e.messageData[0] += coarseTranspose[channel];
|
|
418
|
+
break;
|
|
419
|
+
|
|
420
|
+
case messageTypes.programChange:
|
|
421
|
+
// do we delete it?
|
|
422
|
+
if (channelsToChangeProgram.has(channel))
|
|
409
423
|
{
|
|
410
|
-
|
|
424
|
+
// this channel has program change. BEGONE!
|
|
425
|
+
deleteThisEvent();
|
|
426
|
+
continue;
|
|
411
427
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const firstVoice = firstVoiceForTrack.reduce((first, current) =>
|
|
438
|
-
midi.tracks[current.track][current.index].ticks < midi.tracks[first.track][first.index].ticks ? current : first);
|
|
439
|
-
// get the index and ticks
|
|
440
|
-
let firstIndex = firstVoice.index;
|
|
441
|
-
const ticks = midi.tracks[firstVoice.track][firstVoice.index].ticks;
|
|
442
|
-
|
|
443
|
-
// add drums if needed
|
|
444
|
-
if (change.isDrum)
|
|
445
|
-
{
|
|
446
|
-
// do not add gs drum change on drum channel
|
|
447
|
-
if (midiSystem === "gs" && midiChannel !== DEFAULT_PERCUSSION)
|
|
448
|
-
{
|
|
449
|
-
SpessaSynthInfo(
|
|
450
|
-
`%cAdding GS Drum change on track %c${firstVoice.track}`,
|
|
451
|
-
consoleColors.recognized,
|
|
452
|
-
consoleColors.value
|
|
453
|
-
);
|
|
454
|
-
midi.tracks[firstVoice.track].splice(
|
|
455
|
-
firstIndex,
|
|
456
|
-
0,
|
|
457
|
-
getDrumChange(change.channel, ticks)
|
|
458
|
-
);
|
|
459
|
-
firstIndex++;
|
|
460
|
-
}
|
|
461
|
-
else if (midiSystem === "xg")
|
|
462
|
-
{
|
|
463
|
-
SpessaSynthInfo(
|
|
464
|
-
`%cAdding XG Drum change on track %c${firstVoice.track}`,
|
|
465
|
-
consoleColors.recognized,
|
|
466
|
-
consoleColors.value
|
|
467
|
-
);
|
|
468
|
-
// system is xg. drums are on msb bank 127.
|
|
469
|
-
desiredBank = 127;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
SpessaSynthInfo(
|
|
474
|
-
`%cSetting %c${change.channel}%c to %c${desiredBank}:${desiredProgram}%c. Track num: %c${firstVoice.track}`,
|
|
475
|
-
consoleColors.info,
|
|
476
|
-
consoleColors.recognized,
|
|
477
|
-
consoleColors.info,
|
|
478
|
-
consoleColors.recognized,
|
|
479
|
-
consoleColors.info,
|
|
480
|
-
consoleColors.recognized
|
|
481
|
-
);
|
|
482
|
-
|
|
483
|
-
// add bank
|
|
484
|
-
const bankChange = getControllerChange(
|
|
485
|
-
midiChannel,
|
|
486
|
-
midiControllers.bankSelect,
|
|
487
|
-
desiredBank,
|
|
488
|
-
ticks
|
|
489
|
-
);
|
|
490
|
-
midi.tracks[firstVoice.track].splice(firstIndex, 0, bankChange);
|
|
491
|
-
firstIndex++;
|
|
492
|
-
|
|
493
|
-
// on xg, add lsb
|
|
494
|
-
if (!change.isDrum && midiSystem === "xg")
|
|
495
|
-
{
|
|
496
|
-
const bankChangeLSB = getControllerChange(
|
|
497
|
-
midiChannel,
|
|
498
|
-
midiControllers.lsbForControl0BankSelect,
|
|
499
|
-
desiredBank,
|
|
500
|
-
ticks
|
|
501
|
-
);
|
|
502
|
-
midi.tracks[firstVoice.track].splice(firstIndex, 0, bankChangeLSB);
|
|
503
|
-
firstIndex++;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// add program change
|
|
507
|
-
const programChange = new MidiMessage(
|
|
508
|
-
ticks,
|
|
509
|
-
messageTypes.programChange | midiChannel,
|
|
510
|
-
new IndexedByteArray([
|
|
511
|
-
desiredProgram
|
|
512
|
-
])
|
|
513
|
-
);
|
|
514
|
-
midi.tracks[firstVoice.track].splice(firstIndex, 0, programChange);
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
// transpose channels
|
|
518
|
-
for (const transpose of desiredChannelsToTranspose)
|
|
519
|
-
{
|
|
520
|
-
const midiChannel = transpose.channel % 16;
|
|
521
|
-
const port = Math.floor(transpose.channel / 16);
|
|
522
|
-
const keyShift = Math.trunc(transpose.keyShift);
|
|
523
|
-
const fineTune = transpose.keyShift - keyShift;
|
|
524
|
-
SpessaSynthInfo(
|
|
525
|
-
`%cTransposing channel %c${transpose.channel}%c by %c${transpose.keyShift}%c semitones`,
|
|
526
|
-
consoleColors.info,
|
|
527
|
-
consoleColors.recognized,
|
|
528
|
-
consoleColors.info,
|
|
529
|
-
consoleColors.value,
|
|
530
|
-
consoleColors.info
|
|
531
|
-
);
|
|
532
|
-
if (keyShift !== 0)
|
|
533
|
-
{
|
|
534
|
-
midi.tracks.forEach((track, trackNum) =>
|
|
535
|
-
{
|
|
428
|
+
break;
|
|
429
|
+
|
|
430
|
+
case messageTypes.controllerChange:
|
|
431
|
+
const ccNum = e.messageData[0];
|
|
432
|
+
const changes = desiredControllerChanges.find(c => c.channel === channel && ccNum === c.controllerNumber);
|
|
433
|
+
if (changes !== undefined)
|
|
434
|
+
{
|
|
435
|
+
// this controller is locked, BEGONE CHANGE!
|
|
436
|
+
deleteThisEvent();
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
// bank maybe?
|
|
440
|
+
if (ccNum === midiControllers.bankSelect || ccNum === midiControllers.lsbForControl0BankSelect)
|
|
441
|
+
{
|
|
442
|
+
if (channelsToChangeProgram.has(channel))
|
|
443
|
+
{
|
|
444
|
+
// BEGONE!
|
|
445
|
+
deleteThisEvent();
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
|
|
451
|
+
case messageTypes.systemExclusive:
|
|
452
|
+
// check for xg on
|
|
536
453
|
if (
|
|
537
|
-
|
|
538
|
-
|
|
454
|
+
e.messageData[0] === 0x43 && // Yamaha
|
|
455
|
+
e.messageData[2] === 0x4C && // XG ON
|
|
456
|
+
e.messageData[5] === 0x7E &&
|
|
457
|
+
e.messageData[6] === 0x00
|
|
539
458
|
)
|
|
540
459
|
{
|
|
541
|
-
|
|
460
|
+
SpessaSynthInfo("%cXG system on detected", consoleColors.info);
|
|
461
|
+
system = "xg";
|
|
462
|
+
addedGs = true; // flag as true so gs won't get added
|
|
542
463
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
464
|
+
else
|
|
465
|
+
// check for xg program change
|
|
466
|
+
if (
|
|
467
|
+
e.messageData[0] === 0x43 // yamaha
|
|
468
|
+
&& e.messageData[2] === 0x4C // XG
|
|
469
|
+
&& e.messageData[3] === 0x08 // part parameter
|
|
470
|
+
&& e.messageData[5] === 0x03 // program change
|
|
471
|
+
)
|
|
547
472
|
{
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
event.messageStatusByte !== offStatus &&
|
|
551
|
-
event.messageStatusByte !== polyStatus
|
|
552
|
-
)
|
|
473
|
+
// do we delete it?
|
|
474
|
+
if (channelsToChangeProgram.has(e.messageData[4] + portOffset))
|
|
553
475
|
{
|
|
554
|
-
|
|
476
|
+
// this channel has program change. BEGONE!
|
|
477
|
+
deleteThisEvent();
|
|
555
478
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
479
|
+
}
|
|
480
|
+
else
|
|
481
|
+
// check for GS on
|
|
482
|
+
if (
|
|
483
|
+
e.messageData[0] === 0x41 // roland
|
|
484
|
+
&& e.messageData[2] === 0x42 // GS
|
|
485
|
+
&& e.messageData[6] === 0x7F // Mode set
|
|
486
|
+
)
|
|
487
|
+
{
|
|
488
|
+
// that's a GS on, we're done here
|
|
489
|
+
addedGs = true;
|
|
490
|
+
SpessaSynthInfo(
|
|
491
|
+
"%cGS on detected!",
|
|
492
|
+
consoleColors.recognized
|
|
562
493
|
);
|
|
563
|
-
|
|
564
|
-
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
else
|
|
497
|
+
// check for GM/2 on
|
|
498
|
+
if (
|
|
499
|
+
e.messageData[0] === 0x7E // non realtime
|
|
500
|
+
&& e.messageData[2] === 0x09 // gm system
|
|
501
|
+
)
|
|
502
|
+
{
|
|
503
|
+
// that's a GM/2 system change, remove it!
|
|
504
|
+
SpessaSynthInfo(
|
|
505
|
+
"%cGM/2 on detected, removing!",
|
|
506
|
+
consoleColors.info
|
|
507
|
+
);
|
|
508
|
+
deleteThisEvent();
|
|
509
|
+
addedGs = false;
|
|
510
|
+
}
|
|
565
511
|
}
|
|
566
|
-
|
|
567
|
-
|
|
512
|
+
}
|
|
513
|
+
// check for gs
|
|
514
|
+
if (!addedGs)
|
|
515
|
+
{
|
|
516
|
+
// gs is not on, add it on the first track at index 0 (or 1 if track name is first)
|
|
517
|
+
let index = 0;
|
|
518
|
+
if (midi.tracks[0][0].messageStatusByte === messageTypes.trackName)
|
|
568
519
|
{
|
|
569
|
-
|
|
570
|
-
const track = midi.tracks.find((t, tNum) => midi.usedChannelsOnTrack[tNum].has(transpose.channel));
|
|
571
|
-
if (track === undefined)
|
|
572
|
-
{
|
|
573
|
-
SpessaSynthWarn(`Channel ${transpose.channel} unused but transpose requested???`);
|
|
574
|
-
continue;
|
|
575
|
-
}
|
|
576
|
-
// find first noteon for this channel
|
|
577
|
-
const noteOn = messageTypes.noteOn | (transpose.channel % 16);
|
|
578
|
-
const noteIndex = track.findIndex(n => n.messageStatusByte === noteOn);
|
|
579
|
-
if (noteIndex === -1)
|
|
580
|
-
{
|
|
581
|
-
SpessaSynthWarn(`No notes on channel ${transpose.channel} but transpose requested???`);
|
|
582
|
-
continue;
|
|
583
|
-
}
|
|
584
|
-
const ticks = track[noteIndex].ticks;
|
|
585
|
-
// add rpn
|
|
586
|
-
// 64 is the center, 96 = 50 cents up
|
|
587
|
-
const centsCoarse = (fineTune * 64) + 64;
|
|
588
|
-
const ccChange = messageTypes.controllerChange | (transpose.channel % 16);
|
|
589
|
-
const rpnCoarse = new MidiMessage(ticks, ccChange, new IndexedByteArray([midiControllers.RPNMsb, 0]));
|
|
590
|
-
const rpnFine = new MidiMessage(ticks, ccChange, new IndexedByteArray([midiControllers.RPNLsb, 1]));
|
|
591
|
-
const deCoarse = new MidiMessage(
|
|
592
|
-
ticks,
|
|
593
|
-
ccChange,
|
|
594
|
-
new IndexedByteArray([midiControllers.dataEntryMsb, centsCoarse])
|
|
595
|
-
);
|
|
596
|
-
const deFine = new MidiMessage(
|
|
597
|
-
ticks,
|
|
598
|
-
ccChange,
|
|
599
|
-
new IndexedByteArray([midiControllers.lsbForControl6DataEntry, 0])
|
|
600
|
-
);
|
|
601
|
-
// add in reverse
|
|
602
|
-
track.splice(noteIndex, 0, deFine);
|
|
603
|
-
track.splice(noteIndex, 0, deCoarse);
|
|
604
|
-
track.splice(noteIndex, 0, rpnFine);
|
|
605
|
-
track.splice(noteIndex, 0, rpnCoarse);
|
|
606
|
-
|
|
520
|
+
index++;
|
|
607
521
|
}
|
|
522
|
+
midi.tracks[0].splice(index, 0, getGsOn(0));
|
|
523
|
+
SpessaSynthInfo("%cGS on not detected. Adding it.", consoleColors.info);
|
|
608
524
|
}
|
|
609
525
|
SpessaSynthGroupEnd();
|
|
610
526
|
}
|