diff --git a/src/constants.ts b/src/constants.ts index 6cee348..919a299 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,3 @@ export const configFileName = "8tlr-router.config.yaml"; -export const sketchSwitchControlChangeNumber = 119; - export const frontendUpdateInterval = 100; diff --git a/src/frontend/updateSketch.ts b/src/frontend/updateSketch.ts index fb5a6e4..4dbb4e0 100644 --- a/src/frontend/updateSketch.ts +++ b/src/frontend/updateSketch.ts @@ -1,7 +1,7 @@ export function updateSketch(uiUpdate: UiUpdate) { const { type, track, sketch } = uiUpdate; - if (type !== "sketch") { + if (type !== "pgm") { return; } diff --git a/src/global.d.ts b/src/global.d.ts index c01c765..9f7e2d3 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -4,7 +4,7 @@ declare const MAIN_WINDOW_VITE_DEV_SERVER_URL: string; declare const MAIN_WINDOW_VITE_NAME: string; type TrackName = "fhyd" | "tang" | "duri" | "poml" | "tiff" | "coco" | "plum" | "flam"; -type MidiMessageType = "note-on" | "note-off" | "cc" | "at" | "pb" | "sketch"; +type MidiMessageType = "note-on" | "note-off" | "cc" | "at" | "pb" | "pgm"; type Sketch = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8; interface UiUpdate { diff --git a/src/midi/createMidiMessageRouter.test.ts b/src/midi/createMidiMessageRouter.test.ts index 925ff90..7ffdc5a 100644 --- a/src/midi/createMidiMessageRouter.test.ts +++ b/src/midi/createMidiMessageRouter.test.ts @@ -1,6 +1,5 @@ import type { MidiMessage, Output } from "@julusian/midi"; import { createMidiMessageRouter, loggers } from "./createMidiMessageRouter"; -import { sketchSwitchControlChangeNumber } from "../constants"; const outputs: Output[] = new Array(4).fill(null).map(() => ({ sendMessage: vi.fn(), @@ -17,8 +16,8 @@ const outputs: Output[] = new Array(4).fill(null).map(() => ({ const NOTE_ON_CH1_C2_V100: MidiMessage = [0x90, 0x30, 0x64]; const NOTE_ON_CH9_C2_V100: MidiMessage = [0x98, 0x30, 0x64]; -const SKETCH_SWITCH_2: MidiMessage = [0xb0, sketchSwitchControlChangeNumber, 0x10]; -const SKETCH_SWITCH_3: MidiMessage = [0xb0, sketchSwitchControlChangeNumber, 0x20]; +const PROGRAM_CHANGE_2: MidiMessage = [0xc0, 0x01]; +const PROGRAM_CHANGE_3: MidiMessage = [0xc0, 0x02]; vi.mock("debug", () => ({ default: () => vi.fn() })); @@ -30,7 +29,7 @@ describe("The router function created by createMidiMessageRouter", () => { midiMessageRouter = createMidiMessageRouter({ outputs }); }); - describe("when no sketch switch MIDI message has been received previously", () => { + describe("when no program change MIDI message has been received previously", () => { describe("and it receives a MIDI message", () => { beforeEach(() => { result = midiMessageRouter(0, NOTE_ON_CH1_C2_V100); @@ -73,21 +72,20 @@ describe("The router function created by createMidiMessageRouter", () => { }); }); - describe("when it receives a sketch switch control change for sketch 2", () => { + describe("when it receives a program change for sketch 2", () => { beforeEach(() => { - result = midiMessageRouter(0, SKETCH_SWITCH_2); + result = midiMessageRouter(0, PROGRAM_CHANGE_2); }); - it(`Logs a debug message that indicates that MIDI messages arriving on channel 1 - will now be routed to channel 9 on the same output port`, () => { - expect(loggers.sketch).toHaveBeenCalledWith(" [SK] ch: 1 | skt: 2 | val: 16"); + it(`Logs a debug message that indicates that a program change message + with value 1 was received (pgm is zero-based, so 1 is sketch 2)`, () => { + expect(loggers.pgm).toHaveBeenCalledWith(" [PG] ch: 1 | | val: 1"); }); it("returns the correct result", () => { expect(result).toMatchInlineSnapshot(` { "outputMidiMessage": [ - 184, - 119, - 16, + 200, + 1, ], "outputPortIndex": 0, } @@ -121,19 +119,18 @@ describe("The router function created by createMidiMessageRouter", () => { describe("when it receives a sketch switch control change for sketch 3", () => { beforeEach(() => { - result = midiMessageRouter(0, SKETCH_SWITCH_3); + result = midiMessageRouter(0, PROGRAM_CHANGE_3); }); - it(`Logs a debug message that indicates that MIDI messages arriving on channel 1 - will now be routed to channel 1 on output port 2`, () => { - expect(loggers.sketch).toHaveBeenCalledWith(" [SK] ch: 1 | skt: 3 | val: 32"); + it(`Logs a debug message that indicates that a program change message + with value 1 was received (pgm is zero-based, so 2 is sketch 3)`, () => { + expect(loggers.pgm).toHaveBeenCalledWith(" [PG] ch: 1 | | val: 2"); }); it("returns the correct result", () => { expect(result).toMatchInlineSnapshot(` { "outputMidiMessage": [ - 176, - 119, - 32, + 192, + 2, ], "outputPortIndex": 1, } diff --git a/src/midi/createMidiMessageRouter.ts b/src/midi/createMidiMessageRouter.ts index fd41dd5..bb4ed92 100644 --- a/src/midi/createMidiMessageRouter.ts +++ b/src/midi/createMidiMessageRouter.ts @@ -1,9 +1,8 @@ import type { MidiMessage, Output } from "@julusian/midi"; import { getMidiChannel } from "./getMidiChannel"; import createDebug from "debug"; -import { isSketchSwitch } from "./isSketchSwitch"; +import { isProgramChange } from "./isProgramChange"; import { formatMidiMessage } from "../utils"; -import { getSketchIndex } from "./getSketchIndex"; import { getMidiMessageType } from "./getMidiMessageType"; export const loggers = { @@ -12,7 +11,7 @@ export const loggers = { cc: createDebug("8tlr-router:midi:router:cc"), at: createDebug("8tlr-router:midi:router:at"), pb: createDebug("8tlr-router:midi:router:pb"), - sketch: createDebug("8tlr-router:midi:router:sketch"), + pgm: createDebug("8tlr-router:midi:router:pgm"), other: createDebug("8tlr-router:midi:router:other"), }; @@ -22,7 +21,7 @@ const leftPaddings = { cc: 6, at: 6, pb: 6, - sketch: 2, + pgm: 5, other: 3, }; @@ -42,17 +41,39 @@ export function createMidiMessageRouter({ outputs }: Args): MidiMessageRouter { return null; } - const isSketchSwitchMessage = isSketchSwitch(inputMidiMessage); - if (isSketchSwitchMessage) { - const sketchIndex = getSketchIndex(inputMidiMessage); + const isProgramChangeMessage = isProgramChange(inputMidiMessage); + if (isProgramChangeMessage) { + const sketchIndex = inputMidiMessage[1]; + + /* + * 0 => 0 + * 1 => 0 + * 2 => 1 + * 3 => 1 + * 4 => 2 + * 5 => 2 + * 6 => 3 + * 7 => 3 + */ selectedOutputIndices[inputChannel] = Math.floor(sketchIndex / 2); + + /* + * 0 => false + * 1 => true + * 2 => false + * 3 => true + * 4 => false + * 5 => true + * 6 => false + * 7 => true + */ shiftChannel[inputChannel] = sketchIndex % 2 !== 0; } if (shiftChannel[inputChannel]) { outputMidiMessage[0] += 8; } const outputPortIndex = selectedOutputIndices[inputChannel]; - if (!isSketchSwitchMessage) { + if (!isProgramChangeMessage) { const midiMessageType = getMidiMessageType(inputMidiMessage); const logger = midiMessageType === null ? loggers.other : loggers[midiMessageType]; const leftPadding = midiMessageType === null ? leftPaddings.other : leftPaddings[midiMessageType]; @@ -61,7 +82,7 @@ export function createMidiMessageRouter({ outputs }: Args): MidiMessageRouter { `${" ".repeat(leftPadding)}${formatMidiMessage(inputMidiMessage, "pretty")} >>> ${formatMidiMessage(outputMidiMessage, "pretty")} | port: ${outputPortIndex + 1}`, ); } else { - loggers.sketch(` ${formatMidiMessage(inputMidiMessage, "pretty")}`); + loggers.pgm(` ${formatMidiMessage(inputMidiMessage, "pretty")}`); } return { diff --git a/src/midi/getMidiMessageType.ts b/src/midi/getMidiMessageType.ts index 1b666f7..aef14b2 100644 --- a/src/midi/getMidiMessageType.ts +++ b/src/midi/getMidiMessageType.ts @@ -1,30 +1,30 @@ import type { MidiMessage } from "@julusian/midi"; -import { isSketchSwitch } from "./isSketchSwitch"; -import { isNoteOn } from "./isNoteOn"; -import { isNoteOff } from "./isNoteOff"; -import { isControlChange } from "./isControlChange"; import { isAfterTouch } from "./isAfterTouch"; +import { isControlChange } from "./isControlChange"; +import { isNoteOff } from "./isNoteOff"; +import { isNoteOn } from "./isNoteOn"; import { isPitchBend } from "./isPitchBend"; +import { isProgramChange } from "./isProgramChange"; export function getMidiMessageType(midiMessage: MidiMessage): MidiMessageType | null { - if (isSketchSwitch(midiMessage)) { - return "sketch"; + if (isAfterTouch(midiMessage)) { + return "at"; } - if (isNoteOn(midiMessage)) { - return "note-on"; + if (isControlChange(midiMessage)) { + return "cc"; } if (isNoteOff(midiMessage)) { return "note-off"; } - if (isControlChange(midiMessage)) { - return "cc"; - } - if (isAfterTouch(midiMessage)) { - return "at"; + if (isNoteOn(midiMessage)) { + return "note-on"; } if (isPitchBend(midiMessage)) { - return "sketch"; + return "pb"; + } + if (isProgramChange(midiMessage)) { + return "pgm"; } return null; } diff --git a/src/midi/getMidiMessageTypeName.ts b/src/midi/getMidiMessageTypeName.ts index 1aae71d..b7b3f5f 100644 --- a/src/midi/getMidiMessageTypeName.ts +++ b/src/midi/getMidiMessageTypeName.ts @@ -1,30 +1,30 @@ import type { MidiMessage } from "@julusian/midi"; -import { isSketchSwitch } from "./isSketchSwitch"; -import { isNoteOn } from "./isNoteOn"; -import { isNoteOff } from "./isNoteOff"; -import { isControlChange } from "./isControlChange"; import { isAfterTouch } from "./isAfterTouch"; +import { isControlChange } from "./isControlChange"; +import { isNoteOff } from "./isNoteOff"; +import { isNoteOn } from "./isNoteOn"; import { isPitchBend } from "./isPitchBend"; +import { isProgramChange } from "./isProgramChange"; export function getMidiMessageTypeName(midiMessage: MidiMessage, verbose = false) { - if (isSketchSwitch(midiMessage)) { - return verbose ? "sketch switch" : "SK"; + if (isAfterTouch(midiMessage)) { + return verbose ? "aftertouch" : "AT"; } - if (isNoteOn(midiMessage)) { - return verbose ? "note on" : "NO"; + if (isControlChange(midiMessage)) { + return verbose ? "control change" : "CC"; } if (isNoteOff(midiMessage)) { return verbose ? "note off" : "NF"; } - if (isControlChange(midiMessage)) { - return verbose ? "control change" : "CC"; - } - if (isAfterTouch(midiMessage)) { - return verbose ? "aftertouch" : "AT"; + if (isNoteOn(midiMessage)) { + return verbose ? "note on" : "NO"; } if (isPitchBend(midiMessage)) { return verbose ? "pitch bend" : "PB"; } + if (isProgramChange(midiMessage)) { + return verbose ? "program change" : "PG"; + } return null; } diff --git a/src/midi/getSketchIndex.test.ts b/src/midi/getSketchIndex.test.ts deleted file mode 100644 index 19b8125..0000000 --- a/src/midi/getSketchIndex.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { getSketchIndex } from "./getSketchIndex"; - -describe("The getSketchIndex function", () => { - describe.each` - midiMessage | value - ${[0xb0, 0x77, 0]} | ${0} - ${[0xb0, 0x77, 15]} | ${0} - ${[0xb0, 0x77, 16]} | ${1} - ${[0xb0, 0x77, 127]} | ${7} - `("when it receives MIDI message $midiMessage", ({ midiMessage, value }) => { - it(`returns ${value}`, () => { - expect(getSketchIndex(midiMessage)).toBe(value); - }); - }); -}); diff --git a/src/midi/getSketchIndex.ts b/src/midi/getSketchIndex.ts deleted file mode 100644 index b8630c0..0000000 --- a/src/midi/getSketchIndex.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { MidiMessage } from "@julusian/midi"; - -export function getSketchIndex(midiMessage: MidiMessage) { - const [, , value] = midiMessage; - return Math.floor(value / 16); -} diff --git a/src/midi/index.ts b/src/midi/index.ts index 4173dbb..37f07ab 100644 --- a/src/midi/index.ts +++ b/src/midi/index.ts @@ -2,16 +2,15 @@ export { createExitHandler } from "./createExitHandler"; export { createMidiMessageHandler } from "./createMidiMessageHandler"; export { createMidiMessageRouter } from "./createMidiMessageRouter"; export { getMidiChannel } from "./getMidiChannel"; -export { getPortIndex } from "./getPortIndex"; -export { getSketchIndex } from "./getSketchIndex"; export { getMidiMessageType } from "./getMidiMessageType"; export { getMidiMessageTypeName } from "./getMidiMessageTypeName"; +export { getPitchBendValue } from "./getPitchBendValue"; +export { getPortIndex } from "./getPortIndex"; export { initPort } from "./initPort"; -export { isControlChange } from "./isControlChange"; -export { isSketchSwitch } from "./isSketchSwitch"; -export { sendAllNotesOff } from "./sendAllNotesOff"; export { isAfterTouch } from "./isAfterTouch"; -export { isNoteOn } from "./isNoteOn"; +export { isControlChange } from "./isControlChange"; export { isNoteOff } from "./isNoteOff"; +export { isNoteOn } from "./isNoteOn"; export { isPitchBend } from "./isPitchBend"; -export { getPitchBendValue } from "./getPitchBendValue"; +export { isProgramChange } from "./isProgramChange"; +export { sendAllNotesOff } from "./sendAllNotesOff"; diff --git a/src/midi/isControlChange.test.ts b/src/midi/isControlChange.test.ts index 4628dcd..0693402 100644 --- a/src/midi/isControlChange.test.ts +++ b/src/midi/isControlChange.test.ts @@ -4,7 +4,7 @@ describe("The isControlChange function", () => { describe.each` midiMessage | value ${[0xb0, 0x01, 0x64]} | ${true} - ${[0xb4, 0x77, 0x24]} | ${false} + ${[0xb4, 0x77, 0x24]} | ${true} ${[0x90, 0x3c, 0x40]} | ${false} `("when it receives MIDI message $midiMessage", ({ midiMessage, value }) => { it(`returns ${value}`, () => { diff --git a/src/midi/isControlChange.ts b/src/midi/isControlChange.ts index 7d0768b..74684d3 100644 --- a/src/midi/isControlChange.ts +++ b/src/midi/isControlChange.ts @@ -1,7 +1,6 @@ import type { MidiMessage } from "@julusian/midi"; -import { sketchSwitchControlChangeNumber } from "../constants"; export function isControlChange(message: MidiMessage) { - const [statusByte, controlNumber] = message; - return (statusByte & 0xf0) === 0xb0 && controlNumber !== sketchSwitchControlChangeNumber; + const [statusByte] = message; + return (statusByte & 0xf0) === 0xb0; } diff --git a/src/midi/isProgramChange.ts b/src/midi/isProgramChange.ts new file mode 100644 index 0000000..dc5f297 --- /dev/null +++ b/src/midi/isProgramChange.ts @@ -0,0 +1,6 @@ +import type { MidiMessage } from "@julusian/midi"; + +export function isProgramChange(message: MidiMessage) { + const [statusByte] = message; + return (statusByte & 0xf0) === 0xc0; +} diff --git a/src/midi/isSketchSwitch.ts b/src/midi/isSketchSwitch.ts deleted file mode 100644 index 845aea3..0000000 --- a/src/midi/isSketchSwitch.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { MidiMessage } from "@julusian/midi"; -import { sketchSwitchControlChangeNumber } from "../constants"; - -export function isSketchSwitch(message: MidiMessage) { - const [statusByte, controlNumber] = message; - return (statusByte & 0xf0) === 0xb0 && controlNumber === sketchSwitchControlChangeNumber; -} diff --git a/src/ui/createUiUpdater.ts b/src/ui/createUiUpdater.ts index e3c1d7b..a1c6fc7 100644 --- a/src/ui/createUiUpdater.ts +++ b/src/ui/createUiUpdater.ts @@ -1,12 +1,12 @@ import type { BrowserWindow } from "electron"; import type { MidiMessage } from "@julusian/midi"; import { getTrackName } from "./getTrackName"; -import { getType } from "./getType"; +import { getMidiMessageType } from "../midi"; import { getSketch } from "./getSketch"; export function createUiUpdater(browserWindow: BrowserWindow) { return (midiMessage: MidiMessage, portIndex: number) => { - const type = getType(midiMessage); + const type = getMidiMessageType(midiMessage); if (!type) { // we only update the UI for note, control change, aftertouch (poly and channel), // pitch bend and sketch change MIDI messages, everything else is irrelevant diff --git a/src/ui/getType.ts b/src/ui/getType.ts deleted file mode 100644 index 7369755..0000000 --- a/src/ui/getType.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { MidiMessage } from "@julusian/midi"; -import { isNoteOn, isNoteOff, isSketchSwitch, isControlChange, isAfterTouch, isPitchBend } from "../midi"; - -export function getType(midiMessage: MidiMessage): MidiMessageType | null { - if (isNoteOn(midiMessage)) { - return "note-on"; - } - if (isNoteOff(midiMessage)) { - return "note-off"; - } - if (isControlChange(midiMessage)) { - return "cc"; - } - if (isAfterTouch(midiMessage)) { - return "at"; - } - if (isPitchBend(midiMessage)) { - return "pb"; - } - if (isSketchSwitch(midiMessage)) { - return "sketch"; - } - return null; -} diff --git a/src/utils/formatMidiMessage.test.ts b/src/utils/formatMidiMessage.test.ts index d3dcd12..c6c9d3d 100644 --- a/src/utils/formatMidiMessage.test.ts +++ b/src/utils/formatMidiMessage.test.ts @@ -21,9 +21,9 @@ describe("The formatMidiMessage function", () => { ${"pitch bend (min value)"} | ${[0xe0, 0x00, 0x00]} | ${"hex"} | ${"E0 00 00"} ${"pitch bend (min value)"} | ${[0xe0, 0x00, 0x00]} | ${"number"} | ${"224 0 0"} ${"pitch bend (min value)"} | ${[0xe0, 0x00, 0x00]} | ${"pretty"} | ${"[PB] ch: 1 | | val: -8192"} - ${"sketch switch"} | ${[0xb0, 0x77, 0x03]} | ${"hex"} | ${"B0 77 03"} - ${"sketch switch"} | ${[0xb0, 0x77, 0x03]} | ${"number"} | ${"176 119 3"} - ${"sketch switch"} | ${[0xb0, 0x77, 0x30]} | ${"pretty"} | ${"[SK] ch: 1 | skt: 4 | val: 48"} + ${"program change"} | ${[0xc0, 0x03]} | ${"hex"} | ${"C0 03 __"} + ${"program change"} | ${[0xc0, 0x03]} | ${"number"} | ${"192 3 ___"} + ${"program change"} | ${[0xc0, 0x03]} | ${"pretty"} | ${"[PG] ch: 1 | | val: 3"} `("when it receives $messageDescription and format $format", ({ midiMessage, format, expected }) => { it(`returns ${expected}`, () => { expect(formatMidiMessage(midiMessage, format)).toBe(expected); diff --git a/src/utils/formatMidiMessage.ts b/src/utils/formatMidiMessage.ts index cb74175..5b91bca 100644 --- a/src/utils/formatMidiMessage.ts +++ b/src/utils/formatMidiMessage.ts @@ -1,6 +1,6 @@ import type { MidiMessage } from "@julusian/midi"; import { formatHex } from "./formatHex"; -import { getMidiChannel, getMidiMessageTypeName, getPitchBendValue, getSketchIndex } from "../midi"; +import { getMidiChannel, getMidiMessageTypeName, getPitchBendValue } from "../midi"; import { getNoteName } from "./getNoteName"; type MidiMessageFormat = "hex" | "number" | "pretty"; @@ -25,8 +25,8 @@ export function formatMidiMessage(midiMessage: MidiMessage, format: MidiMessageF return `[${midiMessageTypeName}] ch: ${String(midiChannel + 1).padStart(2, " ")} | | val: ${String(midiMessage[1]).padStart(5, " ")}`; case "PB": return `[${midiMessageTypeName}] ch: ${String(midiChannel + 1).padStart(2, " ")} | | val: ${String(getPitchBendValue(midiMessage)).padStart(5, " ")}`; - case "SK": - return `[${midiMessageTypeName}] ch: ${String(midiChannel + 1).padStart(2, " ")} | skt: ${String(getSketchIndex(midiMessage) + 1).padStart(4, " ")} | val: ${String(midiMessage[2]).padStart(5, " ")}`; + case "PG": + return `[${midiMessageTypeName}] ch: ${String(midiChannel + 1).padStart(2, " ")} | | val: ${String(midiMessage[1]).padStart(5, " ")}`; default: return `[??] ch: ${String(midiChannel + 1).padStart(2, " ")} | ${formatMidiMessage(midiMessage, "hex")} | `; }