spessasynth_lib
Version:
MIDI and SoundFont2/DLS library with no compromises
743 lines (696 loc) • 33.2 kB
JavaScript
import { arrayToHexString, consoleColors } from "../../../utils/other.js";
import { SpessaSynthInfo, SpessaSynthWarn } from "../../../utils/loggin.js";
import { midiControllers } from "../../../midi_parser/midi_message.js";
import { ALL_CHANNELS_OR_DIFFERENT_ACTION } from "../message_protocol/worklet_message.js";
import { isSystemXG } from "../../../utils/xg_hacks.js";
/**
* KeyNum: tuning
* @typedef {MTSNoteTuning[]} MTSProgramTuning
*/
/**
* @typedef {Object} MTSNoteTuning
* @property {number} midiNote - the base midi note to use, -1 means no change
* @property {number} centTuning - additional tuning
*/
/**
* Calculates freqency for MIDI Tuning Standard
* @param byte1 {number}
* @param byte2 {number}
* @param byte3 {number}
* @return {{midiNote: number, centTuning: number|null}}
*/
function getTuning(byte1, byte2, byte3)
{
const midiNote = byte1;
const fraction = (byte2 << 7) | byte3; // Combine byte2 and byte3 into a 14-bit number
// no change
if (byte1 === 0x7F && byte2 === 0x7F && byte3 === 0x7F)
{
return { midiNote: -1, centTuning: null };
}
// calculate cent tuning
return { midiNote: midiNote, centTuning: fraction * 0.0061 };
}
/**
* The text types for the synth display
* @enum {number}
*/
export const SynthDisplayType = {
SoundCanvasText: 0,
XGText: 1,
SoundCanvasDotDisplay: 2
};
/**
* Executes a system exclusive
* @param messageData {number[]|IndexedByteArray} - the message data without f0
* @param channelOffset {number}
* @this {SpessaSynthProcessor}
*/
export function systemExclusive(messageData, channelOffset = 0)
{
const type = messageData[0];
if (this.deviceID !== ALL_CHANNELS_OR_DIFFERENT_ACTION && messageData[1] !== 0x7F)
{
if (this.deviceID !== messageData[1])
{
// not our device ID
return;
}
}
switch (type)
{
default:
SpessaSynthWarn(
`%cUnrecognized SysEx: %c${arrayToHexString(messageData)}`,
consoleColors.warn,
consoleColors.unrecognized
);
break;
// non realtime
case 0x7E:
case 0x7F:
switch (messageData[2])
{
case 0x04:
let cents;
// device control
switch (messageData[3])
{
case 0x01:
// main volume
const vol = messageData[5] << 7 | messageData[4];
this.setMIDIVolume(vol / 16384);
SpessaSynthInfo(
`%cMaster Volume. Volume: %c${vol}`,
consoleColors.info,
consoleColors.value
);
break;
case 0x02:
// main balance
// midi spec page 62
const balance = messageData[5] << 7 | messageData[4];
const pan = (balance - 8192) / 8192;
this.setMasterPan(pan);
SpessaSynthInfo(
`%cMaster Pan. Pan: %c${pan}`,
consoleColors.info,
consoleColors.value
);
break;
case 0x03:
// fine-tuning
const tuningValue = ((messageData[5] << 7) | messageData[6]) - 8192;
cents = Math.floor(tuningValue / 81.92); // [-100;+99] cents range
this.setMasterTuning(cents);
SpessaSynthInfo(
`%cMaster Fine Tuning. Cents: %c${cents}`,
consoleColors.info,
consoleColors.value
);
break;
case 0x04:
// coarse tuning
// lsb is ignored
const semitones = messageData[5] - 64;
cents = semitones * 100;
this.setMasterTuning(cents);
SpessaSynthInfo(
`%cMaster Coarse Tuning. Cents: %c${cents}`,
consoleColors.info,
consoleColors.value
);
break;
default:
SpessaSynthWarn(
`%cUnrecognized MIDI Device Control Real-time message: %c${arrayToHexString(messageData)}`,
consoleColors.warn,
consoleColors.unrecognized
);
}
break;
case 0x09:
// gm system related
if (messageData[3] === 0x01)
{
SpessaSynthInfo("%cGM1 system on", consoleColors.info);
this.setSystem("gm");
}
else if (messageData[3] === 0x03)
{
SpessaSynthInfo("%cGM2 system on", consoleColors.info);
this.setSystem("gm2");
}
else
{
SpessaSynthInfo("%cGM system off, defaulting to GS", consoleColors.info);
this.setSystem("gs");
}
break;
// MIDI Tuning standard
// https://midi.org/midi-tuning-updated-specification
case 0x08:
switch (messageData[3])
{
// single note change
// single note change bank
case 0x02:
case 0x07:
let currentMessageIndex = 4;
if (messageData[3] === 0x07)
{
// skip the bank
currentMessageIndex++;
}
// get program and number of changes
const tuningProgram = messageData[currentMessageIndex++];
const numberOfChanges = messageData[currentMessageIndex++];
for (let i = 0; i < numberOfChanges; i++)
{
// set the given tuning to the program
this.tunings[tuningProgram][messageData[currentMessageIndex++]] = getTuning(
messageData[currentMessageIndex++],
messageData[currentMessageIndex++],
messageData[currentMessageIndex++]
);
}
SpessaSynthInfo(
`%cSingle Note Tuning. Program: %c${tuningProgram}%c Keys affected: %c${numberOfChanges}`,
consoleColors.info,
consoleColors.recognized,
consoleColors.info,
consoleColors.recognized
);
break;
// octave tuning (1 byte)
// and octave tuning (2 bytes)
case 0x09:
case 0x08:
// get tuning:
const newOctaveTuning = new Int8Array(12);
// start from bit 7
if (messageData[3] === 0x08)
{
// 1 byte tuning: 0 is -64 cents, 64 is 0, 127 is +63
for (let i = 0; i < 12; i++)
{
newOctaveTuning[i] = messageData[7 + i] - 64;
}
}
else
{
// 2 byte tuning. Like fine tune: 0 is -100 cents, 8192 is 0 cents, 16,383 is +100 cents
for (let i = 0; i < 24; i += 2)
{
const tuning = ((messageData[7 + i] << 7) | messageData[8 + i]) - 8192;
newOctaveTuning[i / 2] = Math.floor(tuning / 81.92); // map to [-100;+99] cents
}
}
// apply to channels (ordered from 0)
// bit 1: 14 and 15
if ((messageData[4] & 1) === 1)
{
this.workletProcessorChannels[14 + channelOffset].setOctaveTuning(newOctaveTuning);
}
if (((messageData[4] >> 1) & 1) === 1)
{
this.workletProcessorChannels[15 + channelOffset].setOctaveTuning(newOctaveTuning);
}
// bit 2: channels 7 to 13
for (let i = 0; i < 7; i++)
{
const bit = (messageData[5] >> i) & 1;
if (bit === 1)
{
this.workletProcessorChannels[7 + i + channelOffset].setOctaveTuning(newOctaveTuning);
}
}
// bit 3: channels 0 to 16
for (let i = 0; i < 7; i++)
{
const bit = (messageData[6] >> i) & 1;
if (bit === 1)
{
this.workletProcessorChannels[i + channelOffset].setOctaveTuning(newOctaveTuning);
}
}
SpessaSynthInfo(
`%cMIDI Octave Scale ${
messageData[3] === 0x08 ? "(1 byte)" : "(2 bytes)"
} tuning via Tuning: %c${newOctaveTuning.join(" ")}`,
consoleColors.info,
consoleColors.value
);
break;
default:
SpessaSynthWarn(
`%cUnrecognized MIDI Tuning standard message: %c${arrayToHexString(messageData)}`,
consoleColors.warn,
consoleColors.unrecognized
);
break;
}
break;
default:
SpessaSynthWarn(
`%cUnrecognized MIDI Realtime/non realtime message: %c${arrayToHexString(messageData)}`,
consoleColors.warn,
consoleColors.unrecognized
);
}
break;
// this is a roland sysex
// http://www.bandtrax.com.au/sysex.htm
// https://cdn.roland.com/assets/media/pdf/AT-20R_30R_MI.pdf
case 0x41:
function notRecognized()
{
// this is some other GS sysex...
SpessaSynthWarn(
`%cUnrecognized Roland %cGS %cSysEx: %c${arrayToHexString(messageData)}`,
consoleColors.warn,
consoleColors.recognized,
consoleColors.warn,
consoleColors.unrecognized
);
}
if (messageData[2] === 0x42 && messageData[3] === 0x12)
{
// this is a GS sysex
// messageData[5] and [6] is the system parameter, messageData[7] is the value
const messageValue = messageData[7];
if (messageData[6] === 0x7F)
{
// GS mode set
if (messageValue === 0x00)
{
// this is a GS reset
SpessaSynthInfo("%cGS Reset received!", consoleColors.info);
this.resetAllControllers(false);
this.setSystem("gs");
}
else if (messageValue === 0x7F)
{
// GS mode off
SpessaSynthInfo("%cGS system off, switching to GM2", consoleColors.info);
this.resetAllControllers(false);
this.setSystem("gm2");
}
return;
}
else if (messageData[4] === 0x40)
{
// this is a system parameter
if ((messageData[5] & 0x10) > 0)
{
// this is an individual part (channel) parameter
// determine the channel 0 means channel 10 (default), 1 means 1 etc.
const channel = [9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14, 15][messageData[5] & 0x0F] + channelOffset;
// for example, 0x1A means A = 11, which corresponds to channel 12 (counting from 1)
const channelObject = this.workletProcessorChannels[channel];
switch (messageData[6])
{
default:
// this is some other GS sysex...
notRecognized();
break;
case 0x15:
// this is the Use for Drum Part sysex (multiple drums)
const isDrums = messageValue > 0 && messageData[5] >> 4; // if set to other than 0, is a drum channel
channelObject.setDrums(isDrums);
SpessaSynthInfo(
`%cChannel %c${channel}%c ${isDrums ?
"is now a drum channel"
:
"now isn't a drum channel"
}%c via: %c${arrayToHexString(messageData)}`,
consoleColors.info,
consoleColors.value,
consoleColors.recognized,
consoleColors.info,
consoleColors.value
);
return;
case 0x16:
// this is the pitch key shift sysex
const keyShift = messageValue - 64;
channelObject.transposeChannel(keyShift);
SpessaSynthInfo(
`%cChannel %c${channel}%c pitch shift. Semitones %c${keyShift}%c, with %c${arrayToHexString(
messageData)}`,
consoleColors.info,
consoleColors.recognized,
consoleColors.info,
consoleColors.value,
consoleColors.info,
consoleColors.value
);
return;
// pan position
case 0x1C:
// 0 is random
let panpot = messageValue;
if (panpot === 0)
{
channelObject.randomPan = true;
SpessaSynthInfo(
`%cRandom pan is set to %cON%c for %c${channel}`,
consoleColors.info,
consoleColors.recognized,
consoleColors.info,
consoleColors.value
);
}
else
{
channelObject.randomPan = false;
channelObject.controllerChange(midiControllers.pan, panpot);
}
break;
// chorus send
case 0x21:
channelObject.controllerChange(midiControllers.chorusDepth, messageValue);
break;
// reverb send
case 0x22:
channelObject.controllerChange(midiControllers.reverbDepth, messageValue);
break;
case 0x40:
case 0x41:
case 0x42:
case 0x43:
case 0x44:
case 0x45:
case 0x46:
case 0x47:
case 0x48:
case 0x49:
case 0x4A:
case 0x4B:
// scale tuning: up to 12 bytes
const tuningBytes = messageData.length - 9; // data starts at 7, minus checksum and f7
// read em bytes
const newTuning = new Int8Array(12);
for (let i = 0; i < tuningBytes; i++)
{
newTuning[i] = messageData[i + 7] - 64;
}
channelObject.setOctaveTuning(newTuning);
const cents = messageValue - 64;
SpessaSynthInfo(
`%cChannel %c${channel}%c octave scale tuning. Cents %c${newTuning.join(
" ")}%c, with %c${arrayToHexString(messageData)}`,
consoleColors.info,
consoleColors.recognized,
consoleColors.info,
consoleColors.value,
consoleColors.info,
consoleColors.value
);
channelObject.setTuning(cents);
break;
}
return;
}
else
// this is a global system parameter
if (messageData[5] === 0x00 && messageData[6] === 0x06)
{
// roland master pan
SpessaSynthInfo(
`%cRoland GS Master Pan set to: %c${messageValue}%c with: %c${arrayToHexString(
messageData)}`,
consoleColors.info,
consoleColors.value,
consoleColors.info,
consoleColors.value
);
this.setMasterPan((messageValue - 64) / 64);
return;
}
else if (messageData[5] === 0x00 && messageData[6] === 0x05)
{
// roland master key shift (transpose)
const transpose = messageValue - 64;
SpessaSynthInfo(
`%cRoland GS Master Key-Shift set to: %c${transpose}%c with: %c${arrayToHexString(
messageData)}`,
consoleColors.info,
consoleColors.value,
consoleColors.info,
consoleColors.value
);
this.setMasterTuning(transpose * 100);
return;
}
else if (messageData[5] === 0x00 && messageData[6] === 0x04)
{
// roland GS master volume
SpessaSynthInfo(
`%cRoland GS Master Volume set to: %c${messageValue}%c with: %c${arrayToHexString(
messageData)}`,
consoleColors.info,
consoleColors.value,
consoleColors.info,
consoleColors.value
);
this.setMIDIVolume(messageValue / 127);
return;
}
}
// this is some other GS sysex...
notRecognized();
return;
}
else if (messageData[2] === 0x45 && messageData[3] === 0x12)
{
// 0x45: GS Display Data, 0x12: DT1 (Device Transmit)
// check for embedded copyright
// (roland SC display sysex) http://www.bandtrax.com.au/sysex.htm
if (
messageData[4] === 0x10 && // Sound Canvas Display
messageData[6] === 0x00 // Data follows
)
{
if (messageData[5] === 0x00) // Display letters
{
// get the text
// and header ends with (checksum) F7
const text = new Uint8Array(messageData.slice(7, messageData.length - 2));
this.callEvent(
"synthdisplay",
{
displayData: text,
displayType: SynthDisplayType.SoundCanvasText
}
);
}
else if (messageData[5] === 0x01) // Matrix display
{
// get the data
// and header ends with (checksum) F7
const dotMatrixData = new Uint8Array(messageData.slice(7, messageData.length - 3));
this.callEvent(
"synthdisplay",
{
displayData: dotMatrixData,
displayType: SynthDisplayType.SoundCanvasDotDisplay
}
);
SpessaSynthInfo(
`%cRoland SC Display Dot Matrix via: %c${arrayToHexString(
messageData)}`,
consoleColors.info,
consoleColors.value
);
}
else
{
// this is some other GS sysex...
notRecognized();
}
}
}
else if (messageData[2] === 0x16 && messageData[3] === 0x12 && messageData[4] === 0x10)
{
// this is a roland master volume message
this.setMIDIVolume(messageData[7] / 100);
SpessaSynthInfo(
`%cRoland Master Volume control set to: %c${messageData[7]}%c via: %c${arrayToHexString(
messageData)}`,
consoleColors.info,
consoleColors.value,
consoleColors.info,
consoleColors.value
);
return;
}
else
{
// this is something else...
SpessaSynthWarn(
`%cUnrecognized Roland SysEx: %c${arrayToHexString(messageData)}`,
consoleColors.warn,
consoleColors.unrecognized
);
return;
}
break;
// yamaha
// http://www.studio4all.de/htmle/main91.html
case 0x43:
// XG sysex
if (messageData[2] === 0x4C)
{
// XG system parameter
if (messageData[3] === 0x00 && messageData[4] === 0x00)
{
switch (messageData[5])
{
// master volume
case 0x04:
const vol = messageData[6];
this.setMIDIVolume(vol / 127);
SpessaSynthInfo(
`%cXG master volume. Volume: %c${vol}`,
consoleColors.info,
consoleColors.recognized
);
break;
// master transpose
case 0x06:
const transpose = messageData[6] - 64;
this.transposeAllChannels(transpose);
SpessaSynthInfo(
`%cXG master transpose. Volume: %c${transpose}`,
consoleColors.info,
consoleColors.recognized
);
break;
// XG on
case 0x7E:
SpessaSynthInfo("%cXG system on", consoleColors.info);
this.resetAllControllers(false);
this.setSystem("xg");
break;
}
}
else
// XG part parameter
if (messageData[3] === 0x08)
{
if (!isSystemXG(this.system))
{
return;
}
const channel = messageData[4] + channelOffset;
if (channel >= this.workletProcessorChannels.length)
{
// invalid channel
return;
}
const channelObject = this.workletProcessorChannels[channel];
const value = messageData[6];
switch (messageData[5])
{
// bank-select MSB
case 0x01:
channelObject.controllerChange(midiControllers.bankSelect, value);
break;
// bank-select LSB
case 0x02:
channelObject.controllerChange(midiControllers.lsbForControl0BankSelect, value);
break;
// program change
case 0x03:
channelObject.programChange(value);
break;
// note shift
case 0x08:
if (channelObject.drumChannel)
{
return;
}
const semitones = value - 64;
channelObject.channelTransposeKeyShift = semitones;
break;
// volume
case 0x0B:
channelObject.controllerChange(midiControllers.mainVolume, value);
break;
// pan position
case 0x0E:
let pan = value;
if (pan === 0)
{
// 0 means random
channelObject.randomPan = true;
SpessaSynthInfo(
`%cRandom pan is set to %cON%c for %c${channel}`,
consoleColors.info,
consoleColors.recognized,
consoleColors.info,
consoleColors.value
);
}
else
{
channelObject.controllerChange(midiControllers.pan, pan);
}
break;
// reverb
case 0x13:
channelObject.controllerChange(midiControllers.reverbDepth, value);
break;
// chorus
case 0x12:
channelObject.controllerChange(midiControllers.chorusDepth, value);
break;
default:
SpessaSynthWarn(
`%cUnrecognized Yamaha XG Part Setup: %c${messageData[5].toString(16)
.toUpperCase()}`,
consoleColors.warn,
consoleColors.unrecognized
);
}
}
else if (
messageData[3] === 0x06 && // XG System parameter
messageData[4] === 0x00 // System Byte
)
{
// displayed letters (remove F7 at the end)
// include byte 5 as it seems to be line information (useful)
const textData = new Uint8Array(messageData.slice(5, messageData.length - 1));
this.callEvent(
"synthdisplay",
{
displayData: textData,
displayType: SynthDisplayType.XGText
}
);
}
else if (isSystemXG(this.system))
{
SpessaSynthWarn(
`%cUnrecognized Yamaha XG SysEx: %c${arrayToHexString(messageData)}`,
consoleColors.warn,
consoleColors.unrecognized
);
}
}
else
{
if (isSystemXG(this.system))
{
SpessaSynthWarn(
`%cUnrecognized Yamaha SysEx: %c${arrayToHexString(messageData)}`,
consoleColors.warn,
consoleColors.unrecognized
);
}
}
break;
}
}