Skip to content

Commit

Permalink
Merge pull request #33 from Cycling74/fde/midi_keyboard
Browse files Browse the repository at this point in the history
new MidiKeyboard
  • Loading branch information
fde31 authored Dec 16, 2021
2 parents cf5d9ab + f3ffbff commit f30a520
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 122 deletions.
7 changes: 6 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@
"rules": {
// disable the rule for all files
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off"
"@typescript-eslint/no-explicit-any": "off",
"new-cap": [
"error", {
"capIsNewExceptionPattern": "^Immu*"
}
]
},
"root": true
}
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
"osc": "^2.4.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-piano": "^3.1.3",
"react-redux": "^7.2.4",
"redux": "^4.1.0",
"redux-thunk": "^2.3.0",
Expand Down
226 changes: 199 additions & 27 deletions src/components/PianoKeyboard.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,231 @@
import { memo, useCallback } from "react";
import { MidiNumbers } from "react-piano";
import { DimensionsProvider } from "../contexts/dimension";
import ResponsivePiano from "./ResponsivePiano";
import { triggerRemoteMidiNoteEvent } from "../actions/device";
import { FunctionComponent, memo, PointerEvent, useCallback, useEffect, useRef, useState } from "react";
import { Set as ImmuSet } from "immutable";
import { useAppDispatch } from "../hooks/useAppDispatch";
import { triggerRemoteMidiNoteEvent } from "../actions/device";
import styled from "styled-components";
import { clamp } from "../lib/util";

const MIDIWrapper = styled.div`
display: flex;
justify-content: center;
user-select: none;
touch-action: none;
`;


const KeyWidth = 30;
const OctaveWidth = KeyWidth * 7;
const BaseOctave = 4;

interface NoteElementProps {
index: number;
isActive: boolean;
isWhiteKey: boolean;
}

const NoteElement = styled.div.attrs<NoteElementProps>(({
index,
isWhiteKey
}) => ({
style: {
height: isWhiteKey ? "100%" : "60%",
left: isWhiteKey ? index * KeyWidth : index * KeyWidth + 0.5 * KeyWidth,
zIndex: isWhiteKey ? 2 : 3
}
}))<NoteElementProps>`
background-color: ${({ isActive, isWhiteKey, theme }) => isActive ? theme.colors.primary : isWhiteKey ? "#fff" : "#333" };
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
border-color: gray;
border-style: solid;
border-width: 1px;
position: absolute;
top: 0;
width: ${KeyWidth}px;
`;

const Note: FunctionComponent<{
index: number;
isActive: boolean;
isWhiteKey: boolean;
note: number;
onNoteOn: (n: number) => any;
onNoteOff: (n: number) => any;
}> = memo(({
index,
isActive,
isWhiteKey,
note,
onNoteOff,
onNoteOn
}) => {

const onPointerDown = (e: PointerEvent<HTMLDivElement>) => {
if (isActive) return;
onNoteOn(note);
};

const onPointerEnter = (e: PointerEvent<HTMLDivElement>) => {
if (e.pointerType === "mouse" && !e.buttons) return;
onNoteOn(note);
};

const onPointerLeave = (e: PointerEvent<HTMLDivElement>) => {
if (!isActive) return;
if (e.pointerType === "mouse" && !e.buttons) return;
onNoteOff(note);
};

const onPointerUp = (e: PointerEvent<HTMLDivElement>) => {
if (!isActive) return;
onNoteOff(note);
};

return (
<NoteElement
className={ isActive ? "active" : "" }
index={ index }
isActive={ isActive }
isWhiteKey={ isWhiteKey }
onPointerEnter={ onPointerEnter }
onPointerDown={ onPointerDown}
onPointerLeave={ onPointerLeave }
onPointerCancel={ onPointerUp }
onPointerUp={ onPointerUp }
/>
);
});

Note.displayName = "Note";

.keyboardContainer {
height: 15rem;
width: 80%;
const OctaveElement = styled.div`
height: 150px;
position: relative;
user-select: none;
width: ${OctaveWidth}px;
&:not(:last-child) {
border-right: none;
}
.keyboardContainer > div {
> div {
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
`;

const Octave: FunctionComponent<{
octave: number;
activeNotes: ImmuSet<number>;
onNoteOn: (note: number) => any;
onNoteOff: (note: number) => any;
}> = memo(({
octave,
activeNotes,
onNoteOn,
onNoteOff
}) => {

const start = 12 * octave;
const whiteNotes: JSX.Element[] = [];
const blackNotes: JSX.Element[] = [];

for (let i = 0, key = start; i < 7; i++) {

@media screen and (max-width: 35.5em) {
padding-top: 5rem;
.keyboardContainer {
height: 10rem;
width: 100%;
// create a white key for every entry
whiteNotes.push(<Note
key={ key }
index={ i }
note={ key }
isActive={ activeNotes.has(key) }
isWhiteKey={ true }
onNoteOn={ onNoteOn }
onNoteOff={ onNoteOff }
/>);
key++;

// create black key?!
if (i !== 2 && i !== 6) {
blackNotes.push(
<Note
key={ key }
index={ i }
note={ key }
isActive={ activeNotes.has(key) }
isWhiteKey={ false }
onNoteOn={ onNoteOn }
onNoteOff={ onNoteOff }
/>
);
key++;
}
}
`;

const noteRange = {
first: MidiNumbers.fromNote("c3"),
last: MidiNumbers.fromNote("f4")
};
return (
<OctaveElement>
<div>
{ blackNotes }
{ whiteNotes }
</div>
</OctaveElement>
);
});

const PianoKeyboard = memo(function WrappedPianoKeyboard() {
Octave.displayName = "OctaveName";

export const PianoKeyboard: FunctionComponent<{}> = memo(() => {

const dispatch = useAppDispatch();
const containerRef = useRef<HTMLDivElement>();
const [activeNotes, setActiveNotes] = useState(ImmuSet<number>());
const [noOfOctaves, setNoOfOctaves] = useState(4);

const onNoteOn = useCallback((p: number) => {
dispatch(triggerRemoteMidiNoteEvent(p, true));
setActiveNotes((notes: ImmuSet<number>) => notes.add(p));
}, [dispatch]);

const onNoteOff = useCallback((p: number) => {
dispatch(triggerRemoteMidiNoteEvent(p, false));
setActiveNotes((notes: ImmuSet<number>) => notes.delete(p));
}, [dispatch]);

return (
<MIDIWrapper>
<div className="keyboardContainer">
<DimensionsProvider>
<ResponsivePiano noteRange={noteRange} onNoteOn={onNoteOn} onNoteOff={onNoteOff} />
</DimensionsProvider>
</div>
useEffect(() => {
const onResize = (ev?: UIEvent) => {
if (!containerRef.current) return;
const { width } = containerRef.current.getBoundingClientRect();
setNoOfOctaves(clamp(Math.floor(width / OctaveWidth), 1, 6));
};

window.addEventListener("resize", onResize);
onResize();

return () => window.removeEventListener("resize", onResize);
}, []);

const octs: JSX.Element[] = [];

for (let i = 0; i < noOfOctaves; i++) {
octs.push(<Octave
key={ i }
octave={ BaseOctave + i }
activeNotes={ activeNotes }
onNoteOn={ onNoteOn }
onNoteOff={ onNoteOff }
/>);
}

return (
<MIDIWrapper ref={ containerRef } >
{ octs }
</MIDIWrapper>
);
});

PianoKeyboard.displayName = "PianoKeyboard";

export default PianoKeyboard;
27 changes: 0 additions & 27 deletions src/components/ResponsivePiano.tsx

This file was deleted.

33 changes: 0 additions & 33 deletions src/contexts/dimension.tsx

This file was deleted.

7 changes: 0 additions & 7 deletions src/hooks/useDimensions.ts

This file was deleted.

8 changes: 8 additions & 0 deletions src/lib/util.ts
Original file line number Diff line number Diff line change
@@ -1 +1,9 @@
export const sleep = (t: number): Promise<void> => new Promise(resolve => setTimeout(resolve, t));

export const clamp = (num: number, min: number, max: number): number => {
return Math.min(Math.max(num, min), max);
};

export const scale = (x: number, inLow: number, inHigh: number, outLow: number, outHigh: number): number => {
return (x - inLow) * (outHigh - outLow) / (inHigh - inLow) + outLow;
};
Loading

0 comments on commit f30a520

Please sign in to comment.