Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send and receive note and pedal events to/from connected devices via WebMIDI #112

Merged
merged 35 commits into from
Jan 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
679a145
Allow for multiple concurrent notifications
simonwiles Jan 14, 2022
dcb8164
Add some vertical space
simonwiles Jan 14, 2022
39e4b6a
Enable WebMIDI if available, keep track of note and pedal state
broadwell Oct 2, 2021
5923acf
Pianolatron component handles MIDI status notifications
broadwell Oct 3, 2021
12e3510
Handle MIDI input & output devices being (dis)connected while the app…
broadwell Oct 3, 2021
ae66c4d
Move MIDI in/out functionality to WebMidi Svelte component
broadwell Oct 4, 2021
12f7f63
Do not send simultaneous pedal events to external MIDI devices.
broadwell Oct 4, 2021
aff5b1c
Don't clear notifications on first roll load.
broadwell Oct 4, 2021
b3ce94b
Move external midi msg details out of SamplePlayer to WebMidi
broadwell Oct 4, 2021
cf7fcf7
Simplify pedal loop-avoidance logic
broadwell Oct 4, 2021
915b4e5
Move WebMidi to be direct child of SamplePlayer
broadwell Oct 12, 2021
40a2b10
Add persistent config option to enable/disable Web MIDI functionality.
broadwell Oct 14, 2021
986c503
<SamplePlayer/> shouldn't generate and <WebMidi/> shouldn't send out-…
broadwell Oct 14, 2021
c3eb54e
App should respond to external soft pedal
simonwiles Oct 15, 2021
9ca6f69
App should respond to external sustain pedal
simonwiles Oct 16, 2021
24d5f25
Don't modify velocity to represent softness when sending note events …
simonwiles Oct 15, 2021
4ea5fe5
Revert "Add persistent config option to enable/disable Web MIDI funct…
simonwiles Oct 14, 2021
1de25d0
Fix linting errors and typos
simonwiles Oct 9, 2021
9ea4845
Remove unneeded exports
simonwiles Oct 14, 2021
f9a8512
Remove checks for `$userSettings.midiMessageSeen`
simonwiles Oct 14, 2021
e31eaa6
Simple "Enable/Disable WebMIDI" button
simonwiles Oct 14, 2021
73a6e8a
Handle `$activeNotes` in `startNote` and `stopNote` functions
simonwiles Oct 15, 2021
3465406
Refactoring
simonwiles Oct 15, 2021
fb47ff1
Reorganize `<WebMidi/>` component, use stores, drop notifications
simonwiles Jan 14, 2022
511400a
Drop enable/disable button for WebMIDI
simonwiles Jan 14, 2022
8330419
Add a MIDI settings panel
simonwiles Jan 14, 2022
39c480d
Default `useWebMidi` to on if the browser supports it, off otherwise
simonwiles Jan 14, 2022
92ad48c
Drop unused import
simonwiles Jan 14, 2022
666e087
Avoid destruction/re-mounting of components on roll-change
simonwiles Jan 18, 2022
a35c450
Default WebMIDI functionality to off
simonwiles Jan 20, 2022
8b635d1
Fix MIDI connection event handling
simonwiles Jan 24, 2022
57c1273
Remove unused CSS selector
simonwiles Jan 24, 2022
954bc82
Fade headings in/out
simonwiles Jan 25, 2022
c3872be
Prevent feedback loops from external MIDI pedal devices
broadwell Jan 20, 2022
cf6a22b
Styling update
simonwiles Jan 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 15 additions & 13 deletions src/Pianolatron.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@

import catalog from "./config/catalog.json";

let firstLoad = true;
let appReady = false;
let appWaiting = true;
let mididataReady;
Expand Down Expand Up @@ -167,7 +168,7 @@
const resetApp = () => {
rollViewer?.$destroy();
mididataReady = false;
clearNotification();
if (!firstLoad) clearNotification();
appReady = false;
pausePlayback();
resetPlayback();
Expand Down Expand Up @@ -214,6 +215,7 @@
$rollPedalingOnOff = $isReproducingRoll;
appReady = true;
appWaiting = false;
firstLoad = false;
previousRoll = currentRoll;
const params = new URLSearchParams(window.location.search);
if (params.has("druid") && params.get("druid") !== currentRoll.druid) {
Expand Down Expand Up @@ -285,25 +287,25 @@
{/if}
{/if}
</FlexCollapsible>
{#if appReady}
<div id="roll">
<div id="roll">
{#if appReady}
<RollViewer
bind:this={rollViewer}
imageUrl={currentRoll.image_url}
{holeData}
{holesByTickInterval}
{skipToTick}
/>
{#if $userSettings.showKeyboard && $userSettings.overlayKeyboard}
<div id="keyboard-overlay" transition:fade>
<Keyboard keyCount="88" {startNote} {stopNote} />
</div>
{/if}
</div>
<FlexCollapsible id="right-sidebar" width="20vw" position="left">
<TabbedPanel {playPauseApp} {stopApp} {skipToPercentage} />
</FlexCollapsible>
{/if}
{/if}
{#if $userSettings.showKeyboard && $userSettings.overlayKeyboard}
<div id="keyboard-overlay" transition:fade>
<Keyboard keyCount="88" {startNote} {stopNote} />
</div>
{/if}
</div>
<FlexCollapsible id="right-sidebar" width="20vw" position="left">
<TabbedPanel {playPauseApp} {stopApp} {skipToPercentage} />
</FlexCollapsible>
</div>
{#if $userSettings.showKeyboard && !$userSettings.overlayKeyboard}
<div id="keyboard-container" transition:slide>
Expand Down
2 changes: 1 addition & 1 deletion src/components/Keyboard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@
let mouseDown = false;
let playing = new Set();
const stopPlaying = () => {
playing.forEach(stopNote);
playing.forEach((midiNumber) => stopNote(midiNumber));
playing = new Set();
};
</script>
Expand Down
103 changes: 103 additions & 0 deletions src/components/MidiSettings.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<style lang="scss">
.setting {
margin: 1em 0;
}

.list-header {
font-family: $primary-typeface;
font-size: 0.9em;
margin-top: 1em;
text-transform: uppercase;
color: rgba(black, 0.6);
display: inline-block;
width: 100%;

&::first-letter {
font-size: 1.3em;
}
}

ul {
margin: 0.5em 0 0 1em;
padding: 0;

list-style-type: none;
}

li {
padding: 0.25em 0;
}
</style>

<script>
import { fade } from "svelte/transition";
import {
midiInputs,
midiOutputs,
userSettings,
sustainOnOff,
softOnOff,
sustainFromExternalMidi,
softFromExternalMidi,
} from "../stores";

const resetPedals = () => {
$sustainOnOff = false;
$softOnOff = false;
};

/* eslint-disable no-unused-expressions, no-sequences */
$: $sustainFromExternalMidi, resetPedals();
$: $softFromExternalMidi, resetPedals();
</script>

<section>
{#if navigator.requestMIDIAccess}
<fieldset>
<legend>MIDI in/out available</legend>

<p>
Connect a digital piano or other MIDI device to send/receive keyboard
and pedal events.
</p>

<div class="setting">
Enable WebMIDI:
<input type="checkbox" bind:checked={$userSettings.useWebMidi} />
</div>

{#if $userSettings.useWebMidi}
<div class="setting" transition:fade>
Sustain from External MIDI:
<input type="checkbox" bind:checked={$sustainFromExternalMidi} />
</div>
<div class="setting" transition:fade>
Soft from External MIDI:
<input type="checkbox" bind:checked={$softFromExternalMidi} />
</div>

<p class="list-header" transition:fade>Connected Inputs:</p>
<ul>
{#each $midiInputs as input}
<li transition:fade>{input.name} {input.manufacturer}</li>
{/each}
</ul>

<p class="list-header" transition:fade>Connected Outputs:</p>
<ul>
{#each $midiOutputs as output}
<li transition:fade>{output.name} {output.manufacturer}</li>
{/each}
</ul>
{/if}
</fieldset>
{:else}
<fieldset>
<legend>MIDI in/out not available</legend>
<p>
This browser does not support connecting to a digital piano or other
MIDI device.
</p>
</fieldset>
{/if}
</section>
79 changes: 65 additions & 14 deletions src/components/SamplePlayer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
tempoCoefficient,
playExpressionsOnOff,
rollPedalingOnOff,
sustainFromExternalMidi,
softFromExternalMidi,
useMidiTempoEventsOnOff,
activeNotes,
currentTick,
Expand All @@ -24,7 +26,11 @@
velocityCurveLow,
velocityCurveMid,
velocityCurveHigh,
userSettings,
} from "../stores";
import WebMidi from "./WebMidi.svelte";

let webMidi;

let tempoMap;
let pedalingMap;
Expand Down Expand Up @@ -72,6 +78,27 @@
return tempo;
};

const toggleSustain = (onOff, fromMidi) => {
if (onOff) {
piano.pedalDown();
} else {
piano.pedalUp();
}
if (fromMidi && $sustainFromExternalMidi) {
$sustainOnOff = onOff;
} else if (!fromMidi && !$sustainFromExternalMidi) {
webMidi?.sendMidiMsg("CONTROLLER", "SUSTAIN", onOff);
}
};

const toggleSoft = (onOff, fromMidi) => {
if (fromMidi && $softFromExternalMidi) {
$softOnOff = onOff;
} else if (!fromMidi && !$softFromExternalMidi) {
webMidi?.sendMidiMsg("CONTROLLER", "SOFT", onOff);
}
};

const setPlayerStateAtTick = (tick = $currentTick) => {
if (midiSamplePlayer.tracks[0])
midiSamplePlayer.tracks[0].enabled = $useMidiTempoEventsOnOff;
Expand Down Expand Up @@ -147,7 +174,8 @@
loadSampleVelocities();
};

const startNote = (noteNumber, velocity) => {
const startNote = (noteNumber, velocity, fromMidi) => {
activeNotes.add(noteNumber);
let baseVelocity =
(($playExpressionsOnOff && velocity) || DEFAULT_NOTE_VELOCITY) / 100;
[$velocityCurveLow, $velocityCurveMid, $velocityCurveHigh].forEach(
Expand All @@ -164,28 +192,42 @@
}
},
);
const modifiedVelocity =
// Note: SOFT_PEDAL_RATIO is only applied when calling piano.keyDown() as
// @tonejs/piano has so built-in soft pedaling and so we emulate in
// software. For WebMIDI outputs we send soft pedal controller
// events and note velocities that are not modified for softness.
const modifiedVelocity = Math.min(
baseVelocity *
(($softOnOff && SOFT_PEDAL_RATIO) || 1) *
(($accentOnOff && ACCENT_BUMP) || 1) *
$volumeCoefficient *
(noteNumber < HALF_BOUNDARY
? $bassVolumeCoefficient
: $trebleVolumeCoefficient);
(($accentOnOff && ACCENT_BUMP) || 1) *
$volumeCoefficient *
(noteNumber < HALF_BOUNDARY
? $bassVolumeCoefficient
: $trebleVolumeCoefficient),
1,
);
if (modifiedVelocity) {
piano.keyDown({
midi: noteNumber,
velocity: Math.min(modifiedVelocity, 1),
velocity: modifiedVelocity * (($softOnOff && SOFT_PEDAL_RATIO) || 1),
});
}
if (!fromMidi) {
webMidi?.sendMidiMsg("NOTE_ON", noteNumber, modifiedVelocity);
}
};

const stopNote = (noteNumber) => piano.keyUp({ midi: noteNumber });
const stopNote = (noteNumber, fromMidi) => {
activeNotes.delete(noteNumber);
piano.keyUp({ midi: noteNumber });
if (!fromMidi) {
webMidi?.sendMidiMsg("NOTE_OFF", noteNumber, 0);
}
};

const stopAllNotes = () => {
piano.pedalUp();
$activeNotes.forEach((midiNumber) => stopNote(midiNumber));
if ($sustainOnOff) piano.pedalDown();
$activeNotes.forEach(stopNote);
};

const resetPlayback = () => {
Expand Down Expand Up @@ -303,10 +345,8 @@
if (name === "Note on") {
if (velocity === 0) {
stopNote(noteNumber);
activeNotes.delete(noteNumber);
} else {
startNote(noteNumber, velocity);
activeNotes.add(noteNumber);
}
} else if (name === "Controller Change" && $rollPedalingOnOff) {
if (number === SUSTAIN_PEDAL) {
Expand All @@ -323,7 +363,8 @@
midiSamplePlayer.on("endOfFile", pausePlayback);

/* eslint-disable no-unused-expressions, no-sequences */
$: $sustainOnOff ? piano.pedalDown() : piano.pedalUp();
$: toggleSustain($sustainOnOff);
$: toggleSoft($softOnOff);
$: $tempoCoefficient, updatePlayer();
$: $useMidiTempoEventsOnOff, updatePlayer();
$: $rollPedalingOnOff, updatePlayer();
Expand All @@ -342,3 +383,13 @@
resetPlayback,
};
</script>

{#if $userSettings.useWebMidi}
<WebMidi
bind:this={webMidi}
{startNote}
{stopNote}
{toggleSustain}
{toggleSoft}
/>
{/if}
2 changes: 2 additions & 0 deletions src/components/TabbedPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import BasicSettings from "./BasicSettings.svelte";
import AdvancedSettings from "./AdvancedSettings.svelte";
import AudioSettings from "./AudioSettings.svelte";
import MidiSettings from "./MidiSettings.svelte";

export let playPauseApp;
export let skipToPercentage;
Expand All @@ -59,6 +60,7 @@
},
settings: { component: AdvancedSettings, icon: "cog" },
audio: { component: AudioSettings, icon: "piano" },
midi: { component: MidiSettings, icon: "midi" },
};

let selectedPanel = "controls";
Expand Down