Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,7 @@
gap: 1rem;
}

/* Sections are flex rows: a full-width child forces whatever follows onto a
new line, which keeps the markup free of layout-only wrappers. */
/* Flex rows: a full-width child pushes what follows onto a new line. */
main > section {
display: flex;
flex-wrap: wrap;
Expand Down Expand Up @@ -262,11 +261,31 @@
background: var(--surface-raised);
}

input,
select {
input {
width: 100%;
}

/* Read-only cells, so they sit flat rather than looking like inputs. */
.parameter-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.parameter-type {
font-size: 0.8rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
}

.parameter-number {
font-family: var(--font-mono);
font-size: 0.85rem;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}

.parameter-enabled {
justify-self: center;
}
Expand Down
15 changes: 3 additions & 12 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,18 +82,9 @@ <h2>Parameters</h2>

<template id="parameter-row">
<li class="parameter">
<input class="parameter-name" data-field="name" type="text" />
<select class="parameter-type" data-field="type">
<option value="cc">CC</option>
<option value="nrpn">NRPN</option>
</select>
<input
class="parameter-number"
data-field="number"
type="number"
min="0"
max="16383"
/>
<span class="parameter-name" data-field="name"></span>
<span class="parameter-type" data-field="type"></span>
<span class="parameter-number" data-field="number"></span>
<input
class="parameter-min"
data-field="min"
Expand Down
6 changes: 1 addition & 5 deletions js/devices.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
/**
* Static hosts can't list a directory, so devices are discovered through the
* devices/generated/index.json manifest. It carries enough to build the picker, and a
* device file is only fetched once that device is selected.
*/
// Static hosts can't list a directory, so a manifest lists what's available.

const DEVICES_PATH = "devices/generated/";

Expand Down
14 changes: 5 additions & 9 deletions js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ const deviceSelect = document.querySelector("#device-select");
const randomiseButton = document.querySelector("#randomise");
const deviceStatus = document.querySelector("#device-status");

const NUMERIC_FIELDS = new Set(["number", "min", "max"]);
const NUMERIC_FIELDS = new Set(["min", "max"]);

let connected = false;
// Manifest entries: { file, name, manufacturer }. The device itself is only
// fetched on selection.
// Manifest entries; the device itself is fetched on selection.
let index = [];
let currentDevice = null;
// Bumped on every selection, so a slow fetch can't overwrite a newer one.
Expand Down Expand Up @@ -110,8 +109,7 @@ function renderManufacturerOptions() {
return;
}

// The manifest already arrives sorted by manufacturer, so first-seen order
// is alphabetical.
// The manifest arrives sorted, so first-seen order is alphabetical.
const manufacturers = [...new Set(index.map((entry) => entry.manufacturer))];

manufacturerSelect.replaceChildren(
Expand All @@ -136,8 +134,7 @@ function renderModelOptions(manufacturer) {
deviceSelect.replaceChildren(
...models.map((entry) => {
const option = document.createElement("option");
// Keyed by filename rather than position, so the value survives the
// list being rebuilt.
// Keyed by filename, so the value survives the list being rebuilt.
option.value = entry.file;
option.textContent = entry.name;
return option;
Expand Down Expand Up @@ -266,8 +263,7 @@ channelSelect.addEventListener("change", () => {

randomiseButton.addEventListener("click", randomise);

// Keep the parameter objects in step with the inputs, so Randomise uses what
// is on screen rather than what the device file shipped with.
// Keeps Randomise using what's on screen, not what the file shipped with.
onParameterEdit((index, field, value) => {
const parameter = currentDevice?.parameters[index];

Expand Down
6 changes: 1 addition & 5 deletions js/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,7 @@ export function ccMessage(channel, number, value) {
];
}

/**
* Selects the parameter on CC 99/98, sends the 14-bit value on CC 6/38, then
* nulls the parameter number on CC 101/100 so a later data-entry message can't
* land on it by accident.
*/
/** Select on 99/98, value on 6/38, then null 101/100 so nothing strays. */
export function nrpnMessages(channel, number, value) {
const target = clamp(value, NRPN_MAX);

Expand Down
15 changes: 9 additions & 6 deletions js/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ const parameterTemplate = document.querySelector("#parameter-row");
function createRow(parameter) {
const row = parameterTemplate.content.firstElementChild.cloneNode(true);

for (const input of row.querySelectorAll("[data-field]")) {
const value = parameter[input.dataset.field];

if (input.type === "checkbox") {
input.checked = value;
// Only the range and checkbox are inputs; the rest are read-only cells.
for (const cell of row.querySelectorAll("[data-field]")) {
const value = parameter[cell.dataset.field];

if (cell.type === "checkbox") {
cell.checked = value;
} else if (cell.tagName === "INPUT") {
cell.value = value;
} else {
input.value = value;
cell.textContent = value;
}
}

Expand Down
32 changes: 4 additions & 28 deletions scripts/import-devices.mjs
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
/**
* Converts the MIDI Guide CSV dataset into the device JSON the app loads.
*
* Source: https://github.com/pencilresearch/midi (CC BY-SA 4.0). One CSV per
* device becomes one JSON file in devices/generated/, plus the index the app
* discovers them through. Nothing here is committed - it is rebuilt on every
* deploy, and locally with `npm run devices`.
*
* Pass a path to an existing clone to convert that instead of fetching one.
*/
// Converts the MIDI Guide dataset (CC BY-SA 4.0) into the app's device JSON.

import { execFile } from "node:child_process";
import { mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
Expand All @@ -25,10 +16,7 @@ const OUT_DIR = join("devices", "generated");
const CC_MAX = 127;
const NRPN_MAX = 16383;

/**
* Minimal RFC 4180 reader. The dataset has quoted fields containing commas,
* escaped quotes and, in one file, newlines, so it can't be split on lines.
*/
/** Minimal RFC 4180 reader: quoted commas, quotes, embedded newlines. */
export function parseCsv(text) {
const rows = [];
let row = [];
Expand Down Expand Up @@ -97,16 +85,7 @@ const integer = (value, fallback) => {
/** A row pairing two CC numbers into one 14-bit value. */
export const isWideCC = (row) => Boolean(row.cc_msb && row.cc_lsb);

/**
* One row becomes at most one parameter. Rows documenting both a CC and an
* NRPN take the CC: it is a single message rather than a four-message
* sequence, and any device listing both accepts both.
*
* Rows carrying a cc_lsb are skipped. They pair two CC numbers into one
* 14-bit value, which the schema has no way to express, and importing the
* coarse half alone would mean reinterpreting a range the dataset documents
* inconsistently.
*/
/** One row becomes at most one parameter, preferring the CC over the NRPN. */
export function toParameter(row) {
const label = row.parameter_name;

Expand Down Expand Up @@ -164,10 +143,7 @@ export const slug = (value) =>
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");

/**
* Converts one CSV's text into a device, or null if nothing survives. Returns
* the count of rows dropped as unrepresentable so the run can report them.
*/
/** Converts one CSV into a device, with counts of what it couldn't import. */
export function toDevice(text) {
const rows = readRows(text);

Expand Down
12 changes: 3 additions & 9 deletions scripts/validate-devices.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
/**
* Validates the generated device files against devices/schema.json, and checks
* that the manifest and the directory agree. Run as part of `npm run lint`,
* and in the build before anything is deployed.
*/
// Validates the generated devices against devices/schema.json before deploy.

import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
Expand Down Expand Up @@ -60,17 +56,15 @@ for (const entry of manifest.filter((item) => present.includes(item.file))) {
continue;
}

// The manifest duplicates these two fields so the picker can be built
// without fetching every device, so they have to stay in step.
// The manifest duplicates these, so they have to stay in step.
if (
entry.name !== device.name ||
entry.manufacturer !== device.manufacturer
) {
errors.push(`${file}: index.json name/manufacturer is out of date`);
}

// Ranges are checked here rather than in the schema, which can't compare two
// sibling properties.
// The schema can't compare sibling properties, so ranges are checked here.
for (const [index, parameter] of device.parameters.entries()) {
if (parameter.min > parameter.max) {
errors.push(
Expand Down
Loading