Skip to content

Commit

Permalink
release(0.1.0): Added MPE support (#18)
Browse files Browse the repository at this point in the history
* MPE implementation: initial steps

* feat(MPE support): added MPE support

* feat(MPE support): added changelog

* feat(MPE support): updating active note to have more getters
  • Loading branch information
kulak-at authored May 19, 2023
1 parent 678627a commit ee6633e
Show file tree
Hide file tree
Showing 21 changed files with 1,507 additions and 788 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [0.1.0] 2023-05-19
- Support for MPE. You can now instantiate `MPEMidivalInput` or `MPEMidivalOutput`
- Added support for Registered Parameters. See [MIDI Documentation, Table 3a](https://www.midi.org/specifications-old/item/table-3-control-change-messages-data-bytes-2)
- Dependencies were updated to the latest versions including TypeScript 5.0
- Breaking: `onPitchBend` callback changed it's parameter type. Instead of number it recieved object with value and channel. See [Migration guide](./MIGRATION.md) for more information.

## [0.0.17] 2022-06-15
- Fixed `onInputConnected` / `onInputDisconnected` / `onOutputConnected` / `onOutputDisconnected`
- Simplified code
Expand Down
5 changes: 5 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

This is migration guide explaining breaking changes between versions:

## [0.1.0]

### Breaking changes
- MIDIValInput.onPitchBend now sends information about channel. To do that the signature of the callback changed from Callback<[number]> to Callback<[PitchBend]> where PitchBend is an interface containing channel and value fields.

## [0.0.15]

### Method signature changes
Expand Down
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ module.exports = {
},
verbose: true,
collectCoverage: true,
coverageReporters: ["text-summary", "html"],
coverageReporters: ["json", "text-summary", "html"],
};
21 changes: 11 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
{
"name": "@midival/core",
"version": "0.0.17",
"version": "0.1.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"devDependencies": {
"@types/jest": "^28.1.1",
"@types/webmidi": "^2.0.6",
"gh-pages": "^4.0.0",
"jest": "^28.1.1",
"jest-environment-jsdom": "^28.1.1",
"prettier": "^2.7.0",
"ts-jest": "^28.0.5",
"typedoc": "^0.22.17",
"typescript": "^4.7.3"
"@types/jest": "^29.5.1",
"@types/node": "^20.2.1",
"@types/webmidi": "^2.0.7",
"gh-pages": "^5.0.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"prettier": "^2.8.8",
"ts-jest": "^29.1.0",
"typedoc": "^0.24.7",
"typescript": "^5.0.4"
},
"scripts": {
"compile": "tsc",
Expand Down
159 changes: 153 additions & 6 deletions src/MIDIValInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,25 @@ import {
toNoteMessage,
toProgramMessage,
} from "./types/messages";
import {
MIDIRegisteredParameters,
toRegisteredParameterKey,
} from "./utils/midiRegisteredParameters";

export interface PitchBendMessage {
channel: number;
value: number;
}

export interface RegisteredParameterData {
channel: number;
parameter: keyof typeof MIDIRegisteredParameters;
msb: number;
lsb: number;
}

interface EventDefinitions {
pitchBend: [number];
pitchBend: [PitchBendMessage];
sysex: [Uint8Array];
channelPressure: [MidiMessage];
noteOn: [NoteMessage];
Expand All @@ -40,6 +56,8 @@ interface EventDefinitions {
clockStart: [];
clockStop: [];
clockContinue: [];

registeredParameterData: [RegisteredParameterData];
}

const TEMPO_SAMPLES_LIMIT = 20;
Expand All @@ -64,6 +82,8 @@ export class MIDIValInput {
private tempoSamples: number[];
private options: MIDIValInputOptions;

private rpn: [number, number] = [-1, -1];

constructor(
input: IMIDIInput,
options: MIDIValInputOptions = DefaultOptions
Expand Down Expand Up @@ -165,10 +185,16 @@ export class MIDIValInput {
this.omnibus.trigger("polyKeyPressure", midiMessage);
break;
case MidiCommand.PitchBend:
this.omnibus.trigger(
"pitchBend",
splitValueIntoFraction([midiMessage.data1, midiMessage.data2])
);
this.omnibus.trigger("pitchBend", {
channel: midiMessage.channel,
value: splitValueIntoFraction([
midiMessage.data1,
midiMessage.data2,
]),
});
break;
case MidiCommand.ChannelPressure:
this.omnibus.trigger("channelPressure", midiMessage);
break;
default:
// TODO: Unknown message.
Expand All @@ -194,6 +220,41 @@ export class MIDIValInput {
this.onClockContinue(resetSamples);
this.onClockStart(resetSamples);
}

// RPM
this.onControlChange(
MidiControlChange.RegisteredParameterNumberMSB,
(message) => {
this.rpn = [message.data2, this.rpn[1]];
}
);

this.onControlChange(
MidiControlChange.RegisteredParameterNumberLSB,
(message) => {
this.rpn = [this.rpn[0], message.data2];
}
);

this.onControlChange(MidiControlChange.DataEntryMSB, (message) => {
const key = toRegisteredParameterKey(this.rpn);
this.omnibus.trigger("registeredParameterData", {
channel: message.channel,
parameter: key,
msb: message.data2,
lsb: null,
});
});

this.onControlChange(MidiControlChange.DataEntryLSB, (message) => {
const key = toRegisteredParameterKey(this.rpn);
this.omnibus.trigger("registeredParameterData", {
channel: message.channel,
parameter: key,
msb: null,
lsb: message.data2,
});
});
}

private isClockCommand(e: WebMidi.MIDIMessageEvent): boolean {
Expand Down Expand Up @@ -297,7 +358,9 @@ export class MIDIValInput {
* @param callback Callback that gets called on every pitch bend message. It gets value of the bend in the range of -1.0 to 1.0 using 16-bit precision (if supported by sending device).
* @returns Unregister callback.
*/
onPitchBend(callback: CallbackType<[number]>): UnregisterCallback {
onPitchBend(
callback: CallbackType<EventDefinitions["pitchBend"]>
): UnregisterCallback {
return this.omnibus.on("pitchBend", callback);
}

Expand Down Expand Up @@ -459,6 +522,12 @@ export class MIDIValInput {
);
}

onChannelPressure(
callback: CallbackType<EventDefinitions["channelPressure"]>
) {
return this.omnibus.on("channelPressure", callback);
}

onOmniModeOff(callback: CallbackType<[MidiMessage]>): UnregisterCallback {
return this.onBusKeyValue(
"controlChange",
Expand Down Expand Up @@ -510,4 +579,82 @@ export class MIDIValInput {
onClockContinue(callback: CallbackType<[]>): UnregisterCallback {
return this.omnibus.on("clockContinue", callback);
}

// RPN
onMpeConfiguration(
callback: CallbackType<EventDefinitions["registeredParameterData"]>
) {
return this.onBusKeyValue(
"registeredParameterData",
"parameter",
"MPE_CONFIGURATION_MESSAGE",
callback
);
}

onPitchBendSensitivity(
callback: CallbackType<EventDefinitions["registeredParameterData"]>
) {
return this.onBusKeyValue(
"registeredParameterData",
"parameter",
"PITCH_BEND_SENSITIVITY",
callback
);
}

onChannelFineTuning(
callback: CallbackType<EventDefinitions["registeredParameterData"]>
) {
return this.onBusKeyValue(
"registeredParameterData",
"parameter",
"CHANNEL_FINE_TUNING",
callback
);
}

onChannelCoarseTuning(
callback: CallbackType<EventDefinitions["registeredParameterData"]>
) {
return this.onBusKeyValue(
"registeredParameterData",
"parameter",
"CHANNEL_COARSE_TUNING",
callback
);
}

onTuningProgramChange(
callback: CallbackType<EventDefinitions["registeredParameterData"]>
) {
return this.onBusKeyValue(
"registeredParameterData",
"parameter",
"TUNING_PROGRAM_CHANGE",
callback
);
}

onTuningBankChange(
callback: CallbackType<EventDefinitions["registeredParameterData"]>
) {
return this.onBusKeyValue(
"registeredParameterData",
"parameter",
"TUNING_BANK_SELECT",
callback
);
}

onModulationDepthChange(
callback: CallbackType<EventDefinitions["registeredParameterData"]>
) {
return this.onBusKeyValue(
"registeredParameterData",
"parameter",
"MODULATION_DEPTH_CHANGE",
callback
);
}
}
78 changes: 78 additions & 0 deletions src/MIDIValOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { IMIDIAccess } from "./wrappers/access/IMIDIAccess";
import { fractionToPitchBendAsUints } from "./utils/pitchBend";
import { MidiCommand } from "./utils/midiCommands";
import { MidiControlChange } from "./utils/midiControlChanges";
import { MIDIRegisteredParameters } from "./utils/midiRegisteredParameters";

const delay = (n: number) => new Promise((resolve) => setInterval(resolve, n));

export class MIDIValOutput {
private midiOutput: IMIDIOutput;
Expand Down Expand Up @@ -192,4 +195,79 @@ export class MIDIValOutput {
sendClockPulse(): void {
return this.send([MidiCommand.Clock.Pulse]);
}

// RPN
sendRPNSelection([msb, lsb]: readonly [number, number], channel?: number) {
this.sendControlChange(
MidiControlChange.RegisteredParameterNumberMSB,
msb,
channel
);
this.sendControlChange(
MidiControlChange.RegisteredParameterNumberLSB,
lsb,
channel
);
}

sendRPDataMSB(data: number, channel?: number) {
this.sendControlChange(MidiControlChange.DataEntryMSB, data, channel);
}

sendRPDataLSB(data: number, channel?: number) {
this.sendControlChange(MidiControlChange.DataEntryLSB, data, channel);
}

incrementRPData(incrementValue: number, channel?: number) {
this.sendControlChange(
MidiControlChange.DataIncrement,
incrementValue,
channel
);
}

decrementRPData(decrementValue: number, channel?: number) {
this.sendControlChange(
MidiControlChange.DataDecrement,
decrementValue,
channel
);
}

sendRPNNull() {}

async initializeMPE(
lowerChannelSize: number,
upperChannelSize: number,
messageDelayMs: number = 100
) {
this.sendRPNSelection(
MIDIRegisteredParameters.MPE_CONFIGURATION_MESSAGE,
1
);
await delay(messageDelayMs);
this.sendRPDataMSB(lowerChannelSize, 1);
await delay(messageDelayMs);
this.sendRPDataMSB(upperChannelSize, 16);
await delay(messageDelayMs);
this.sendRPNNull();
await delay(messageDelayMs);
}

async setPitchBendSensitivity(
semitones: number,
cents: number,
channel?: number,
messageDelayMs: number = 100
) {
// FIXME: probably calculate it here?
this.sendRPNSelection(
MIDIRegisteredParameters.PITCH_BEND_SENSITIVITY,
channel
);
await delay(messageDelayMs);
this.sendRPDataMSB(semitones, channel);
await delay(messageDelayMs);
this.sendRPNNull();
}
}
2 changes: 1 addition & 1 deletion src/mock/requestMIDIAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,5 @@ export default (options: requestMIDIAccessFactoryOptions) =>
inputs: makeInputsMap(options),
outputs: makeOutputsMap(options),
onstatechange: null,
} as WebMidi.MIDIAccess;
} as unknown as WebMidi.MIDIAccess;
};
Loading

0 comments on commit ee6633e

Please # to comment.