Skip to content

Commit

Permalink
Visualize MIDI mapping and active MIDI keys
Browse files Browse the repository at this point in the history
Add a missing index to the computed white indices

ref #375
  • Loading branch information
frostburn committed Dec 19, 2022
1 parent eff139b commit 408a01f
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 7 deletions.
4 changes: 1 addition & 3 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
NEWLINE_TEST,
NUMBER_OF_NOTES,
UNIX_NEWLINE,
WHITE_MODE_OFFSET,
} from "@/constants";
import { ScaleWorkshopOneData } from "@/scale-workshop-one";
import type { Input, Output } from "webmidi";
Expand Down Expand Up @@ -388,9 +389,6 @@ function sendNoteOn(frequency: number, rawAttack: number) {
return off;
}
// Offset such that default base MIDI note doesn't move
const WHITE_MODE_OFFSET = 69 - 40;
function midiNoteOn(index: number, rawAttack: number) {
let frequency = frequencies.value[index];
if (!midiVelocityOn.value) {
Expand Down
183 changes: 183 additions & 0 deletions src/components/MidiPiano.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<script setup lang="ts">
import { WHITE_MODE_OFFSET } from "@/constants";
import { computeWhiteIndices, midiNoteInfo } from "@/midi";
import { computed } from "vue";
const props = defineProps<{
baseMidiNote: number;
midiWhiteMode: "off" | "simple" | "blackAverage" | "keyColors";
keyColors: string[];
activeKeys: Set<number>;
}>();
// The svg container preserves aspect ratio so at least an octave around the base midi note is shown.
// View box is centered around 0,0 and has a logical width of 100 units (padding is added to contain strokes).
const KEY_SCALE = 10;
const BLACK_WIDTH = 0.6 * KEY_SCALE;
// Close enough.
const center = computed(() => {
const info = midiNoteInfo(props.baseMidiNote);
if (info.whiteNumber) {
return info.whiteNumber * KEY_SCALE;
}
return (info.sharpOf! + 0.5) * KEY_SCALE;
});
const whiteIndices = computed(() =>
computeWhiteIndices(props.baseMidiNote, props.keyColors)
);
function keyLabel(chromaticNumber: number) {
const info = midiNoteInfo(chromaticNumber);
if (props.midiWhiteMode === "off") {
return [(chromaticNumber - props.baseMidiNote).toString()];
} else if (props.midiWhiteMode === "simple") {
if (info.whiteNumber !== undefined) {
return [
(info.whiteNumber + WHITE_MODE_OFFSET - props.baseMidiNote).toString(),
];
}
} else if (props.midiWhiteMode === "blackAverage") {
const offset = WHITE_MODE_OFFSET - props.baseMidiNote;
if (info.whiteNumber === undefined) {
return [
(info.flatOf + offset).toString(),
"\u2295",
(info.sharpOf + offset).toString(),
];
} else {
return [(info.whiteNumber + offset).toString()];
}
} else {
const indices = whiteIndices.value;
if (indices.length) {
if (info.whiteNumber === undefined) {
// Use a black key if available
const index = indices[info.sharpOf] + 1;
// Eliminate duplicates
if (index === indices[info.sharpOf + 1]) {
return [];
} else {
return [(index - props.baseMidiNote).toString()];
}
} else {
return [(indices[info.whiteNumber] - props.baseMidiNote).toString()];
}
}
}
return [];
}
const whiteKeys = computed(() => {
const result = [];
for (let i = 0; i < 128; ++i) {
const info = midiNoteInfo(i);
if (info.whiteNumber !== undefined) {
const x = info.whiteNumber;
result.push({
x: x * KEY_SCALE - center.value,
label: keyLabel(i),
index: i,
});
}
}
return result;
});
const blackKeys = computed(() => {
const result = [];
for (let i = 0; i < 128; ++i) {
const info = midiNoteInfo(i);
const label = keyLabel(i);
if (info.sharpOf !== undefined) {
const x =
info.sharpOf * KEY_SCALE + KEY_SCALE - 0.5 * BLACK_WIDTH - center.value;
const left = midiNoteInfo(i - 2);
const right = midiNoteInfo(i + 2);
if (left.whiteNumber !== undefined) {
result.push({ x: x - 0.2 * BLACK_WIDTH, label, index: i });
} else if (right.whiteNumber !== undefined) {
result.push({ x: x + 0.2 * BLACK_WIDTH, label, index: i });
} else {
result.push({ x, label, index: i });
}
}
}
return result;
});
</script>

<template>
<svg width="100%" height="100%" viewBox="-51 -51 102 102">
<g v-for="key of whiteKeys" :key="key.index">
<rect
:class="{ white: true, active: activeKeys.has(key.index) }"
:x="key.x"
y="-50"
height="100"
:width="KEY_SCALE"
stroke="gray"
stroke-width="0.1"
/>
<text
y="20"
:font-size="0.5 * KEY_SCALE"
text-anchor="middle"
fill="black"
>
<tspan
v-for="(line, i) of key.label"
:key="i"
:x="key.x + KEY_SCALE * 0.5"
:dy="0.5 * KEY_SCALE"
>
{{ line }}
</tspan>
</text>
</g>
<g v-for="key of blackKeys" :key="key.index">
<rect
:class="{ black: true, active: activeKeys.has(key.index) }"
:x="key.x"
y="-50"
height="60"
:width="BLACK_WIDTH"
stroke="gray"
stroke-width="0.1"
/>
<text
y="-5"
:font-size="0.6 * BLACK_WIDTH"
text-anchor="middle"
fill="white"
>
<tspan
v-for="(line, i) of key.label"
:key="i"
:x="key.x + BLACK_WIDTH * 0.5"
:dy="0.6 * BLACK_WIDTH"
>
{{ line }}
</tspan>
</text>
</g>
</svg>
</template>

<style scoped>
svg rect.white {
fill: white;
}
svg rect.white.active {
fill: green;
}
svg rect.black {
fill: black;
}
svg rect.black.active {
fill: darkgreen;
}
</style>
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ export const KORG = {

// Browser interaction
export const LEFT_MOUSE_BTN = 0;

// Offset such that default base MIDI note doesn't move in "simple" white mode.
export const WHITE_MODE_OFFSET = 69 - 40;
2 changes: 1 addition & 1 deletion src/midi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ export function computeWhiteIndices(baseMidiNote: number, colors: string[]) {
info.whiteNumber === undefined ? info.sharpOf + 1 : info.whiteNumber;
let colorIndex = 0;
const result = [];
while (whiteIndex > 0 && index > -1024) {
while (whiteIndex >= 0 && index > -1024) {
if (colors[mmod(colorIndex--, colors.length)] !== "black") {
result[whiteIndex--] = index;
}
Expand Down
59 changes: 56 additions & 3 deletions src/views/MidiView.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
<script setup lang="ts">
import { computed, onMounted, reactive } from "vue";
import { Input, Output, WebMidi } from "webmidi";
import { computed, onMounted, onUnmounted, reactive, ref } from "vue";
import { Input, Output, WebMidi, type NoteMessageEvent } from "webmidi";
import MidiPiano from "@/components/MidiPiano.vue";
const props = defineProps<{
baseMidiNote: number;
midiInput: Input | null;
midiOutput: Output | null;
midiInputChannels: Set<number>;
midiOutputChannels: Set<number>;
midiVelocityOn: boolean;
midiWhiteMode: "off" | "simple" | "blackAverage" | "keyColors";
keyColors: string[];
}>();
const emit = defineEmits([
Expand All @@ -22,6 +25,10 @@ const emit = defineEmits([
const inputs = reactive<Input[]>([]);
const outputs = reactive<Output[]>([]);
const activeKeys = reactive<Set<number>>(new Set());
const stopActivations = ref<() => void>(() => {});
const midiVelocityOn = computed({
get: () => props.midiVelocityOn,
set: (newValue) => emit("update:midiVelocityOn", newValue),
Expand All @@ -31,12 +38,33 @@ const midiWhiteMode = computed({
set: (newValue) => emit("update:midiWhiteMode", newValue),
});
function activateKey(event: NoteMessageEvent) {
if (props.midiInputChannels.has(event.message.channel)) {
activeKeys.add(event.note.number);
}
}
function deactivateKey(event: NoteMessageEvent) {
if (props.midiInputChannels.has(event.message.channel)) {
activeKeys.delete(event.note.number);
}
}
function selectMidiInput(event: Event) {
const id = (event!.target as HTMLSelectElement).value;
stopActivations.value();
if (id === "no-midi-input") {
emit("update:midiInput", null);
stopActivations.value = () => {};
} else {
emit("update:midiInput", WebMidi.getInputById(id));
const input = WebMidi.getInputById(id);
emit("update:midiInput", input);
input.addListener("noteon", activateKey);
input.addListener("noteoff", deactivateKey);
stopActivations.value = () => {
input.removeListener("noteon", activateKey);
input.removeListener("noteoff", deactivateKey);
};
}
}
Expand Down Expand Up @@ -93,6 +121,19 @@ onMounted(async () => {
// XXX: Webmidi doesn't expose this state change correctly so we'll have to use a time out hack.
setTimeout(refreshMidi, 500);
};
if (props.midiInput !== null) {
const input = props.midiInput;
input.addListener("noteon", activateKey);
input.addListener("noteoff", deactivateKey);
stopActivations.value = () => {
input.removeListener("noteon", activateKey);
input.removeListener("noteoff", deactivateKey);
};
}
});
onUnmounted(() => {
stopActivations.value();
});
</script>

Expand Down Expand Up @@ -178,6 +219,14 @@ onMounted(async () => {
</span>
</div>
</div>
<div class="piano-container">
<MidiPiano
:baseMidiNote="baseMidiNote"
:midiWhiteMode="midiWhiteMode"
:keyColors="keyColors"
:activeKeys="activeKeys"
/>
</div>
</div>
<div class="column midi-controls">
<h2>MIDI Output</h2>
Expand Down Expand Up @@ -254,4 +303,8 @@ div.channels-wrapper span {
flex-flow: column;
text-align: center;
}
div.piano-container {
height: 50%;
}
</style>

0 comments on commit 408a01f

Please sign in to comment.