Skip to content

Commit

Permalink
release 0.0.16: MIDI Clock messages.
Browse files Browse the repository at this point in the history
  • Loading branch information
kulak-at committed Mar 26, 2022
1 parent 39ef5e2 commit f863431
Show file tree
Hide file tree
Showing 12 changed files with 333 additions and 110 deletions.
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,17 @@ You can listen to all notes off
input.onAllNotesOff(() => { });
```

### MIDI Clock
You can listen to MIDI Clock messages:
- `onClockStart`
- `onClockStop`
- `onClockContinue`
- `onClockPulse` - sent 24 times every quarternote.

### To Be Added
The following features are planned to be added to MIDI Input:
- Omni Mode On
- Omni Mode Off
- Mode Mode On
- Poly On
- Better support for sysex
- Better documentation

### Disconnect

Expand Down
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
{
"name": "@midival/core",
"version": "0.0.15",
"version": "0.0.16",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "MIT",
"devDependencies": {
"@types/jest": "^27.4.0",
"@types/jest": "^27.4.1",
"@types/webmidi": "^2.0.6",
"gh-pages": "^3.2.3",
"jest": "^27.5.1",
"prettier": "^2.5.1",
"ts-jest": "^27.1.3",
"typedoc": "^0.22.11",
"typescript": "^4.5.5"
"prettier": "^2.6.1",
"ts-jest": "^27.1.4",
"typedoc": "^0.22.13",
"typescript": "^4.6.3"
},
"scripts": {
"compile": "tsc",
Expand All @@ -23,6 +23,6 @@
"deploy": "gh-pages -d docs"
},
"dependencies": {
"@hypersphere/omnibus": "^0.0.2"
"@hypersphere/omnibus": "^0.0.6"
}
}
161 changes: 117 additions & 44 deletions src/MIDIValInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,10 @@ import {IMIDIAccess} from "./wrappers/access/IMIDIAccess";
import { splitValueIntoFraction } from "./utils/pitchBend";
import { MidiCommand } from "./utils/midiCommands";
import { MidiControlChange } from "./utils/midiControlChanges";
import { ticksToBPM } from "./utils/clock";
import { MIDIValConfigurationError, MIDIValError } from "./errors";
import { ControlChangeMessage, NoteMessage, ProgramChangeMessage, toControlChangeMessage, toNoteMessage, toProgramMessage } from "./types/messages";

interface NoteMessage extends MidiMessage {
note: number;
velocity: number;
}

const toNoteMessage = (m: MidiMessage): NoteMessage => ({
...m,
note: m.data1,
velocity: m.data2,
});

interface ControlChangeMessage extends MidiMessage {
control: number,
value: number,
}

const toControlChangeMessage = (m: MidiMessage): ControlChangeMessage => ({
...m,
control: m.data1,
value: m.data2,
});

interface ProgramMessage extends MidiMessage {
program: number,
value: number,
}

const toProgramMessage = (m: MidiMessage): ProgramMessage => ({
...m,
program: m.data1,
value: m.data2,
});

interface EventDefinitions {
'pitchBend': [number],
Expand All @@ -51,49 +22,91 @@ interface EventDefinitions {
'noteOn': [NoteMessage],
'noteOff': [NoteMessage],
'controlChange': [ControlChangeMessage],
'programChange': [ProgramMessage],
'programChange': [ProgramChangeMessage],
'polyKeyPressure': [MidiMessage],

'clockPulse': [],
'clockStart': [],
'clockStop': [],
'clockContinue': [],
}

const TEMPO_SAMPLES_LIMIT = 20;

/**
* MIDIVal Input Configuration Options
*/
export interface MIDIValInputOptions {
computeClockTempo: boolean,
}

const DefaultOptions: MIDIValInputOptions = {
computeClockTempo: false,
};

export class MIDIValInput {
private unregisterInput: UnregisterCallback;
private omnibus: Omnibus<EventDefinitions>;

private midiInput: IMIDIInput;

constructor(input: IMIDIInput) {
private tempoSamples: number[];
private options: MIDIValInputOptions

constructor(input: IMIDIInput, options: MIDIValInputOptions = DefaultOptions) {
this.omnibus = new Omnibus<EventDefinitions>();
this.tempoSamples = [];
this.registerInput(input);
this.options = options;
}

/**
* Returns new MIDIValInput object based on the interface id.
* @param interfaceId id of the interface from the MIDIAcces object.
* @throws MIDIValError when interface id is not found.
* @returns Promise resolving to MIDIValInput.
*/
static async fromInterfaceId(interfaceId: string): Promise<MIDIValInput> {
static async fromInterfaceId(interfaceId: string, options?: MIDIValInputOptions): Promise<MIDIValInput> {
const midiAccess = await this.getMidiAccess();
const input = midiAccess.inputs.find(({ id }) => id === interfaceId);
if (!input) {
throw new Error(`${interfaceId} not found`);
throw new MIDIValError(`${interfaceId} not found`);
}
return new MIDIValInput(input);
return new MIDIValInput(input, options);
}

static async fromInterfaceName(interfaceName: string): Promise<MIDIValInput> {
/**
* Finds first interface matching the name
* @param interfaceName interface Name
* @param options input configuration options
* @throws MIDIValError when no interface with that name is found
* @returns MIDIValInput object
*/
static async fromInterfaceName(interfaceName: string, options?: MIDIValInputOptions): Promise<MIDIValInput> {
const midiAccess = await this.getMidiAccess();
const input = midiAccess.inputs.find(({ name }) => name === interfaceName);
if (!input) {
throw new Error(`${interfaceName} not found`);
throw new MIDIValError(`${interfaceName} not found`);
}
return new MIDIValInput(input);
return new MIDIValInput(input, options);
}

private static async getMidiAccess(): Promise<IMIDIAccess> {
const midiAccess: IMIDIAccess = await MIDIVal.connect();
return midiAccess;
}

/**
* Current MIDI Clock tempo
* @throws MIDIValConfigurationError when computeClockTempo is not on.
* @returns current tempo in BPM.
*/
public get tempo(): number {
if (!this.options.computeClockTempo) {
throw new MIDIValConfigurationError("To use MIDIValInput.tempo you need to enable computeClockTempo option.");
}
return ticksToBPM(this.tempoSamples);
}

private async registerInput(input: IMIDIInput): Promise<void> {
this.midiInput = input;
this.unregisterInput = await input.onMessage(
Expand All @@ -103,6 +116,9 @@ export class MIDIValInput {
this.omnibus.trigger('sysex', e.data);
return;
}
if (this.isClockCommand(e)) {
return;
}
const midiMessage = toMidiMessage(e.data);
switch (midiMessage.command) {
case MidiCommand.NoteOn:
Expand All @@ -125,14 +141,55 @@ export class MIDIValInput {
break;
default:
// TODO: Unknown message.
console.log('unknown msg', midiMessage);
break;
}
}
);

if (this.options.computeClockTempo) {
this.onClockPulse(() => {
// compute time
this.tempoSamples.push(performance.now());
if (this.tempoSamples.length > TEMPO_SAMPLES_LIMIT) {
this.tempoSamples.shift();
}
});

const resetSamples = () => {
this.tempoSamples = [];
};

this.onClockContinue(resetSamples);
this.onClockStart(resetSamples);
}
}

private isClockCommand(e: WebMidi.MIDIMessageEvent): boolean {
switch (e.data[0]) {
case MidiCommand.Clock.Pulse:
this.omnibus.trigger('clockPulse');
return true;
case MidiCommand.Clock.Start:
this.omnibus.trigger('clockStart');
return true;
case MidiCommand.Clock.Continue:
this.omnibus.trigger('clockContinue');
return true;
case MidiCommand.Clock.Stop:
this.omnibus.trigger('clockStop');
return true;
default:
return false;
}
}

private onBusKeyValue<K extends keyof EventDefinitions>(event: K, key: keyof EventDefinitions[K][0], value: EventDefinitions[K][0][keyof EventDefinitions[K][0]], callback: (obj: EventDefinitions[K][0]) => void) {
return this.omnibus.on(event, (obj) => {
return this.omnibus.on(event, (...args) => {
if (!args.length) {
return;
}
const obj: EventDefinitions[K][0] = args[0];
// FIXME: how to do it so we have multiple args?
if (obj[key] === value) {
callback(obj);
Expand Down Expand Up @@ -237,7 +294,7 @@ export class MIDIValInput {
* @param callback Callback to be called
* @returns Unregister function.
*/
onAllProgramChange(callback: CallbackType<[ProgramMessage]>): UnregisterCallback {
onAllProgramChange(callback: CallbackType<[ProgramChangeMessage]>): UnregisterCallback {
return this.omnibus.on('programChange', callback);
}

Expand All @@ -247,7 +304,7 @@ export class MIDIValInput {
* @param callback Callback to be called
* @returns Unregister function
*/
onProgramChange(program: number, callback: CallbackType<[ProgramMessage]>): UnregisterCallback {
onProgramChange(program: number, callback: CallbackType<[ProgramChangeMessage]>): UnregisterCallback {
return this.onBusKeyValue('programChange', 'program', program, callback);
}

Expand Down Expand Up @@ -332,4 +389,20 @@ export class MIDIValInput {
onPolyModeOn(callback: CallbackType<[MidiMessage]>): UnregisterCallback {
return this.onBusKeyValue('controlChange', 'control', MidiControlChange.PolyModeOn, callback);
}

onClockPulse(callback: CallbackType<[]>): UnregisterCallback {
return this.omnibus.on('clockPulse', callback);
}

onClockStart(callback: CallbackType<[]>): UnregisterCallback {
return this.omnibus.on('clockStart', callback);
}

onClockStop(callback: CallbackType<[]>): UnregisterCallback {
return this.omnibus.on('clockStop', callback);
}

onClockContinue(callback: CallbackType<[]>): UnregisterCallback {
return this.omnibus.on('clockContinue', callback);
}
}
24 changes: 24 additions & 0 deletions src/MIDIValOutput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,28 @@ export class MIDIValOutput {
0,
]);
}

sendClockStart(): void {
return this.send([
MidiCommand.Clock.Start,
]);
}

sendClockStop(): void {
return this.send([
MidiCommand.Clock.Stop,
])
}

sendClockContinue(): void {
return this.send([
MidiCommand.Clock.Continue,
]);
}

sendClockPulse(): void {
return this.send([
MidiCommand.Clock.Pulse,
]);
}
}
13 changes: 13 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export class MIDIValError extends Error {
constructor(message: string) {
super(message);
}

get name() {
return this.constructor.name;
}
}

export class MIDIValConfigurationError extends MIDIValError {

}
9 changes: 8 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {MIDIVal} from "./MIDIval";
import {MIDIValInput} from "./MIDIValInput";
import {MIDIValInput, MIDIValInputOptions} from "./MIDIValInput";
import {MIDIValOutput} from "./MIDIValOutput";
import {IMIDIInput} from "./wrappers/inputs/IMIDIInput";
import {IMIDIOutput} from "./wrappers/outputs/IMIDIOutput";
import {IMIDIAccess} from "./wrappers/access/IMIDIAccess";
import { ControlChangeMessage, NoteMessage, ProgramChangeMessage } from "./types/messages";
import { MidiMessage } from "./utils/MIDIMessageConvert";
import { CallbackType, UnregisterCallback } from "@hypersphere/omnibus";

export {
Expand All @@ -15,4 +17,9 @@ export {
IMIDIAccess,
CallbackType as Callback,
UnregisterCallback,
MIDIValInputOptions,
ControlChangeMessage,
NoteMessage,
ProgramChangeMessage,
MidiMessage,
};
Loading

0 comments on commit f863431

Please # to comment.