Skip to content

Commit

Permalink
Keyboard UI work
Browse files Browse the repository at this point in the history
  • Loading branch information
jonikorpi committed Jan 23, 2025
1 parent 1a9eb35 commit a4fa78c
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 66 deletions.
220 changes: 184 additions & 36 deletions example/Keyboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,47 +8,37 @@ import { Magic } from "./magic";
export const Keyboard = new Magic(
(
state: {
instrument: null | ReturnType<typeof createInstrument>;
instrumentName: keyof typeof allInstrumentPresets;
velocity: number;
duration: number;
vibratoAmount: number;
vibratoFrequency: number;
} = {
instrument: null,
instrumentName: ((localStorage.getItem("instrumentName") ?? "none") in allInstrumentPresets
? localStorage.getItem("instrumentName")
: "piano") as keyof typeof allInstrumentPresets,
velocity: +(localStorage.getItem("velocity") ?? 0.5),
duration: +(localStorage.getItem("duration") ?? 0.2),
duration: +(localStorage.getItem("duration") ?? 0.5),
vibratoAmount: +(localStorage.getItem("vibratoAmount") ?? 0.0),
vibratoFrequency: +(localStorage.getItem("vibratoFrequency") ?? 5.0),
},
message?: {
instrumentName?: keyof typeof allInstrumentPresets;
velocity?: number;
duration?: number;
vibratoAmount?: number;
vibratoFrequency?: number;
},
) => {
const { audioContext, connectInstrument } = AudioSystem.get();

if (!state.instrument) {
state.instrument =
state.instrument ||
createInstrument(allInstrumentPresets[state.instrumentName], audioContext);
connectInstrument(state.instrument);
}

if (message) {
if (message.instrumentName) {
if (state.instrument) destroyInstrument(state.instrument);

state.instrumentName = message.instrumentName;
state.instrument = createInstrument(
allInstrumentPresets[state.instrumentName],
audioContext,
);
connectInstrument(state.instrument);
}

if (message.velocity !== undefined) state.velocity = message.velocity;
if (message.duration !== undefined) state.duration = message.duration;
if (message.vibratoAmount !== undefined) state.vibratoAmount = message.vibratoAmount;
if (message.vibratoFrequency !== undefined) state.vibratoFrequency = message.vibratoFrequency;
}

const keys = Keys.get();
Expand All @@ -58,6 +48,8 @@ export const Keyboard = new Magic(
const instrumentName = data.get("instrumentName") as keyof typeof allInstrumentPresets;
const velocity = data.get("velocity") as string;
const duration = data.get("duration") as string;
const vibratoAmount = data.get("vibratoAmount") as string;
const vibratoFrequency = data.get("vibratoFrequency") as string;

const current = Keyboard.get();

Expand All @@ -67,16 +59,22 @@ export const Keyboard = new Magic(
instrumentName,
});
}

if (+velocity !== current.velocity) {
localStorage.setItem("velocity", velocity);
Keyboard.update({ velocity: +velocity });
}

if (+duration !== current.duration) {
localStorage.setItem("duration", duration);
Keyboard.update({ duration: +duration });
}
if (+vibratoAmount !== current.vibratoAmount) {
localStorage.setItem("vibratoAmount", vibratoAmount);
Keyboard.update({ vibratoAmount: +vibratoAmount });
}
if (+vibratoFrequency !== current.vibratoFrequency) {
localStorage.setItem("vibratoFrequency", vibratoFrequency);
Keyboard.update({ vibratoFrequency: +vibratoFrequency });
}
};

render(
Expand All @@ -85,10 +83,13 @@ export const Keyboard = new Magic(
${instrumentSelect(state.instrumentName)}
${velocityInput(state.velocity)}
${durationInput(state.duration)}
${vibratoAmountInput(state.vibratoAmount)}
${vibratoFrequencyInput(state.vibratoFrequency)}
</form>
${keys}
<p>Hold shift for 2x duration, alt for 0.5x duration, or shift+alt for 4x duration. Keyboard is mapped to octaves 2–5.</p>
`,
keyboardElement,
document.getElementById("keyboard") as HTMLElement,
);

return state;
Expand Down Expand Up @@ -117,11 +118,41 @@ const durationInput = (duration: number) => {
`;
};

const vibratoAmountInput = (vibratoAmount: number) => {
return html`
<div>
<label>
<span>Vibrato</span>
<input name="vibratoAmount" type="number" min="0.0" max="2" step="0.1" .value=${vibratoAmount}/>
</label>
</div>
`;
};

const vibratoFrequencyInput = (vibratoFrequency: number) => {
return html`
<div>
<label>
<span>Vibrato (hz)</span>
<input name="vibratoFrequency" type="number" min="0.0" max="20" step="1" .value=${vibratoFrequency}/>
</label>
</div>
`;
};

const instrumentSelect = (selected: string) => {
const options = [];
const groupArrays = new Map();

for (const name in allInstrumentPresets) {
options.push(html`<option value="${name}" ?selected=${selected === name}>${name}</option>`);
const { group } = allInstrumentPresets[name as keyof typeof allInstrumentPresets];
const groupArray = groupArrays.get(group) ?? groupArrays.set(group, []).get(group);
groupArray.push(html`<option value="${name}" ?selected=${selected === name}>${name}</option>`);
}

const options = [];

for (const [group, groupArray] of groupArrays) {
options.push(html`<optgroup label=${group}>${groupArray}</optgroup>`);
}

return html`
Expand All @@ -137,7 +168,7 @@ const instrumentSelect = (selected: string) => {
const Keys = new Magic(() => {
const keys = [];

for (let octave = 10; octave > 0; octave--) {
for (let octave = 1; octave < 11; octave++) {
const octaveKeys = [];

for (let note = 0; note < 12; note++) {
Expand All @@ -156,7 +187,8 @@ const Keys = new Magic(() => {
pointersDown.add(event.pointerId);

if (!target.dataset.midiNumber) return;
playNote(+(target.dataset.midiNumber ?? 0));
const durationMultiplier = getDurationMultiplier(event);
playNote(+(target.dataset.midiNumber ?? 0), durationMultiplier);
};

const pointerup = (event: PointerEvent) => {
Expand All @@ -172,7 +204,8 @@ const Keys = new Magic(() => {
if (!target) return;

if (!target.dataset.midiNumber || !pointersDown.has(event.pointerId)) return;
playNote(+(target.dataset.midiNumber ?? 0));
const durationMultiplier = getDurationMultiplier(event);
playNote(+(target.dataset.midiNumber ?? 0), durationMultiplier);
};

return html`
Expand All @@ -190,27 +223,142 @@ const Keys = new Magic(() => {
const key = (note: number, octave: number, isBlack = false) => {
const labels = "CCDDEFFGGAAB";
return html`<button class="${isBlack ? "black" : "white"}" type="button" data-midi-number="${note + octave * 12}">
${labels[note]}<sup>${octave - 1}</sup>
<span>${labels[note]}<sup>${octave - 1}</sup></span>
</button>`;
};

const keyboardElement = document.getElementById("keyboard") as HTMLElement;

const pointersDown = new Set();

const playNote = (midiNumber = 0) => {
const { instrument, velocity, duration } = Keyboard.get();
const { audioContext } = AudioSystem.get();
document.addEventListener("keydown", (event: KeyboardEvent) => {
const { code, repeat, shiftKey, altKey } = event;
if (repeat) return;

const higherOctave = 12 * 6;
const highOctave = 12 * 5;
const midOctave = 12 * 4;
const lowOctave = 12 * 3;

const notes = {
Digit1: 0 + higherOctave,
Digit2: 1 + higherOctave,
Digit3: 2 + higherOctave,
Digit4: 3 + higherOctave,
Digit5: 4 + higherOctave,
Digit6: 5 + higherOctave,
Digit7: 6 + higherOctave,
Digit8: 7 + higherOctave,
Digit9: 8 + higherOctave,
Digit0: 9 + higherOctave,
Minus: 10 + higherOctave,
Equal: 11 + higherOctave,

KeyQ: 0 + highOctave,
KeyW: 1 + highOctave,
KeyE: 2 + highOctave,
KeyR: 3 + highOctave,
KeyT: 4 + highOctave,
KeyY: 5 + highOctave,
KeyU: 6 + highOctave,
KeyI: 7 + highOctave,
KeyO: 8 + highOctave,
KeyP: 9 + highOctave,
BracketLeft: 10 + highOctave,
BracketRight: 11 + highOctave,

KeyA: 0 + midOctave,
KeyS: 1 + midOctave,
KeyD: 2 + midOctave,
KeyF: 3 + midOctave,
KeyG: 4 + midOctave,
KeyH: 5 + midOctave,
KeyJ: 6 + midOctave,
KeyK: 7 + midOctave,
KeyL: 8 + midOctave,
Semicolon: 9 + midOctave,
Quote: 10 + midOctave,
Backslash: 11 + midOctave,

Backquote: 0 + lowOctave,
KeyZ: 1 + lowOctave,
KeyX: 2 + lowOctave,
KeyC: 3 + lowOctave,
KeyV: 4 + lowOctave,
KeyB: 5 + lowOctave,
KeyN: 6 + lowOctave,
KeyM: 7 + lowOctave,
Comma: 8 + lowOctave,
Period: 9 + lowOctave,
Slash: 10 + lowOctave,
// ShiftRight: 11 + lowOctave,
};

const note = notes[code as keyof typeof notes];

if (note === undefined) return;

const durationMultiplier = getDurationMultiplier(event);
playNote(note, durationMultiplier);
});

const getDurationMultiplier = (event: KeyboardEvent | PointerEvent) => {
if (event.shiftKey && event.altKey) return 4.0;
if (event.shiftKey) return 2.0;
if (event.altKey) return 0.5;
};

const freeInstruments = new Set<ReturnType<typeof createInstrument>>();
const playingInstruments = new Set<ReturnType<typeof createInstrument>>();

const playNote = (midiNumber = 0, durationMultiplier = 1.0) => {
const { instrumentName, velocity, duration, vibratoAmount, vibratoFrequency } = Keyboard.get();
const { audioContext, connectInstrument } = AudioSystem.get();

if (audioContext.state !== "running") audioContext.resume();

console.log(midiToJustFrequency(midiNumber));
const preset = allInstrumentPresets[instrumentName];

// Save instruments that are done playing their notes
for (const instrument of playingInstruments) {
if (instrument.willPlayUntil < audioContext.currentTime) {
playingInstruments.delete(instrument);
freeInstruments.add(instrument);
console.log("save", instrumentName);
}
}

let instrument: ReturnType<typeof createInstrument>;

// Find an instrument, and cull any that that haven't been used for a while
for (const freeInstrument of freeInstruments) {
if (!instrument && freeInstrument.preset === preset) {
instrument = freeInstrument;
freeInstruments.delete(instrument);
continue;
}

if (freeInstrument.willPlayUntil < audioContext.currentTime - 5) {
freeInstruments.delete(freeInstrument);
destroyInstrument(freeInstrument);
console.log("destroy", instrumentName);
}
}

if (!instrument) {
instrument = createInstrument(allInstrumentPresets[instrumentName], audioContext);
connectInstrument(instrument);
console.log("create", instrumentName);
}

playingInstruments.add(instrument);

playInstrument(
instrument,
midiToJustFrequency(midiNumber),
audioContext.currentTime + 0.04,
duration,
audioContext.currentTime,
duration * durationMultiplier,
velocity,
0.5,
vibratoAmount,
vibratoFrequency,
);
};
Loading

0 comments on commit a4fa78c

Please sign in to comment.