WebHID Demo: Elgato Stream Deck Daft Punk Soundboard

Sparked by Pete LePage’s work on talking to a Elgato Stream Deck device from within the browser, I wanted to play with WebHID myself. First thing that came to my mind was to create a DrumPad.

What first started out as a simple/classic DrumPad …

… soon led to creating a Soundboard which uses samples from Daft Punk’s “Harder, Better, Faster, Stronger”.


Screenshot of the Soundboard I built.

Before linking to the final version of the Daft Punk Soundboard (which has evolved quite a bit when compared to the screenshot above), let’s take a look at how it works.

~

Building the Drumpad/Soundboard and responding to clicks

All audio samples are defined as a small object on an array, and consist of three properties:

  1. A label
  2. A link to an audio fragment
  3. A Keyboard keyCode to respond to

ℹ️ In the final version I added some extra features such as the ability to use an image instead of a label and to customize the action when the button is being pressed, but these are not the focus here.

Each fragment is rendered as a <button> element and a (non-visible) <audio> element. The <audio> element its id is set to the keyCode.

<button data-keycode="${keyCode}">
    <span>${label}</span>
    <audio id="${keyCode}" src=${url} preload="auto"></audio>
</button>

Upon pressing a button, its linked <audio> element (fetched using the button’s data-keycode attribute value, instead of relying on DOM traversal) is selected and a play action is triggered on the fragment.

const playSound = (keyCode) => {
	const $el = document.getElementById(keyCode);
	
	if (!$el) return;

	$el.currentTime = 0;
	$el.play();
}

~

Responding to Keyboard Key Presses

To capture key presses a listener on the keydown event of the document is added. Using the pressed key’s code the correct button is selected and a click on it is triggered.

document.addEventListener('keydown', (e) => {
	const $button = document.querySelector(`button[data-keycode="${e.code}"]`);
	if ($button) $button.click();
});

~

Attaching the Stream Deck

☝️ Do note that connecting a Stream Deck is entirely optional: using a Stream Deck is considered to be an enhancement.

The Stream Deck code itself was borrowed from Pete’s Google Meet Stream Deck Chrome Plug-in, and launched using similar logic. If a Stream Deck device is found and connected, it is attached to the DrumPad instance.

const go = async () => {
	const drumPad = new DrumPad(config, document.querySelector("#app"));
	await drumPad.init();

	if (navigator.hid) {
		const streamDeck = new StreamDeck();

		// Connect to previously connected device
		await streamDeck.connect();

		// A previously connected device was found
		if (streamDeck.isConnected) {
			drumPad.attachStreamDeck(streamDeck);
		}

		// No Previously connected device was found
		else {
			// Add button to connect new device
			const elem = document.createElement("button");
			elem.innerText = "Connect Stream Deck";
			elem.addEventListener("click", async () => {
				elem.remove();
				await streamDeck.connect(true);
				drumPad.attachStreamDeck(streamDeck);
			});
			document.body.appendChild(elem);
		}
	}
}
go();

💡 As not all browsers support top-level await, we wrap the whole logic in a async function

~

Responding to Stream Deck button presses

To also respond to button presses on the Stream Deck, a map that maps a Stream Deck button ID (0, 1, 2, …) to a certain keyCode is built.

const buttonIdToKeyCodeMap = {
  0: "KeyQ",
  1: "KeyW",
  2: "KeyE",
  …
}

Upon pressing a Stream Deck button it will — using the buttonIdToKeyCodeMap — fetch the corresponding HTML button and trigger a click on it, similar to how the keyboard key presses work.

This is set up in the call to drumPad.attachStreamDeck(streamDeck); (see above) and looks like this:

streamDeck.addEventListener('keydown', (e) => {
	const keyCode = buttonIdToKeyCodeMap[e.detail.buttonId] ?? '';
	const $button = document.querySelector(`button[data-keycode="${keyCode}"]`);
	if ($button) $button.click();
});

In that same attachStreamDeck method the buttons on the Stream Deck are also drawn.

~

Stretching it a bit more …

The switch to the Daft Punk board didn’t sit 100% well me with me though: there are 16 samples to use, but the Stream Deck “only” has 15 buttons available …

But then it hit me: what if I paginated the samples, and allowed you to switch between two sets of 8 samples each? In that idea the 1st row would be filled with buttons to switch between different sample sets, and the 2nd+3rd row would respond to that.

With that refactor being worked on, I also took the time to update the UI to closely reflect the layout of the Stream Deck device.

In the end, it ended up like this:

Here’s a video of it, so see how it works and behaves, including with a connected Stream Deck:

~

During lunch today I polished the code a bit further and pushed everything online. The Source Code can be found in GitHub, and the app is deployed on Netlify.

Elgato Stream Deck Daft Punk Soundboard Demo →
Elgato Stream Deck Daft Punk Soundboard Source Code →

👨‍🔬 The demo website is registered for the WebHID Origin Trial, and therefore WebHID should be enabled by default. If you however don’t see a connect button, go to chrome://flags/ and manually enable ”Experimental Web Platform Features”.

~

Did this help you out? Like what you see?
Thank me with a coffee.

I don\'t do this for profit but a small one-time donation would surely put a smile on my face. Thanks!

BuymeaCoffee (€3)

To stay in the loop you can follow @bramus or follow @bramusblog on Twitter.

Published by Bramus!

Bramus is a frontend web developer from Belgium, working as a Chrome Developer Relations Engineer at Google. From the moment he discovered view-source at the age of 14 (way back in 1997), he fell in love with the web and has been tinkering with it ever since (more …)

Unless noted otherwise, the contents of this post are licensed under the Creative Commons Attribution 4.0 License and code samples are licensed under the MIT License

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.