initial commit

This commit is contained in:
2026-07-12 11:02:46 +02:00
commit a98a5985a7
18 changed files with 361 additions and 0 deletions

30
.gitignore vendored Normal file
View File

@@ -0,0 +1,30 @@
*.log
*.swp
*.tmp
.cache
.git
.mypy_cache
.next
.nuxt
.parcel-cache
.pytest_cache
.turbo
.venv
__pycache__
build
coverage
dist
dist-tauri
logs
node_modules
out
output
src-tauri/gen
src-tauri/target
target
temp
tmp
tmp*
venv
*.app
.DS_Store

4
Launch EP Sample Tool.command Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/zsh -l
cd "${0:A:h}"
exec node launcher.mjs

37
README.md Normal file
View File

@@ -0,0 +1,37 @@
# EP Sample Tool — offline Mac launcher
This folder contains a local snapshot of Teenage Engineering's EP Sample Tool for
personal offline use with an EP-133. The browser talks directly to the connected
device over USB MIDI SysEx; sample data is not sent through an internet service.
## Start it
Double-click **EP Sample Tool Offline.app**. The small wrapper stays open so the
local server remains available; quit it from the Dock when you are finished.
For a visible launcher log, double-click **Launch EP Sample Tool.command**, or run:
```sh
npm start
```
Keep the launcher window open while using the tool. Chrome will ask for permission
to use MIDI system-exclusive messages the first time. Connect and power on the
EP-133 before approving that prompt.
The launcher binds only to `127.0.0.1` on a random port. It requires Google Chrome,
Chromium, or Microsoft Edge because Safari/WKWebView does not currently expose the
Web MIDI API used by the EP tool.
## Included offline
- The current EP Sample Tool JavaScript and CSS
- Audio conversion/tagging WebAssembly modules
- The EP-133 factory-content pack used by factory restore
- The app font and images
The snapshot was taken from
`https://teenage.engineering/apps/ep-sample-tool/` on 2026-07-12. It is not an
official Teenage Engineering desktop application and will not update itself.
Run `npm run verify` to check that all expected local assets are present.

107
launcher.mjs Normal file
View File

@@ -0,0 +1,107 @@
import { createReadStream, statSync } from "node:fs";
import { createServer } from "node:http";
import { spawn } from "node:child_process";
import { dirname, extname, join, normalize } from "node:path";
import { fileURLToPath } from "node:url";
const projectRoot = dirname(fileURLToPath(import.meta.url));
const webRoot = join(projectRoot, "web");
const shouldOpenBrowser = !process.argv.includes("--no-open");
const contentTypes = {
".css": "text/css; charset=utf-8",
".html": "text/html; charset=utf-8",
".ico": "image/x-icon",
".js": "text/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".pak": "application/octet-stream",
".png": "image/png",
".wasm": "application/wasm",
".woff2": "font/woff2",
};
function localPath(url) {
const pathname = decodeURIComponent(new URL(url, "http://127.0.0.1").pathname);
const requested = pathname === "/" ? "/apps/ep-sample-tool/" : pathname;
const relative = normalize(requested).replace(/^([/\\])+/, "");
const candidate = join(webRoot, relative);
if (!candidate.startsWith(webRoot)) return null;
try {
return statSync(candidate).isDirectory() ? join(candidate, "index.html") : candidate;
} catch {
return candidate;
}
}
const server = createServer((request, response) => {
// The upstream app reports errors to /_api/err. Keep that diagnostic local.
if (request.url?.startsWith("/_api/err")) {
response.writeHead(204).end();
return;
}
const path = localPath(request.url ?? "/");
if (!path) {
response.writeHead(403).end("Forbidden");
return;
}
try {
const { size } = statSync(path);
response.writeHead(200, {
"Cache-Control": "no-store",
"Content-Length": size,
"Content-Type": contentTypes[extname(path)] ?? "application/octet-stream",
"Cross-Origin-Opener-Policy": "same-origin",
"X-Content-Type-Options": "nosniff",
});
createReadStream(path).pipe(response);
} catch {
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
response.end("Not found");
}
});
server.listen(0, "127.0.0.1", () => {
const address = server.address();
if (!address || typeof address === "string") throw new Error("Could not start local server");
const url = `http://127.0.0.1:${address.port}/apps/ep-sample-tool/`;
if (!shouldOpenBrowser) {
console.log(`READY ${url}`);
return;
}
const candidates = [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
];
const browser = candidates.find((candidate) => {
try {
return statSync(candidate).isFile();
} catch {
return false;
}
});
if (!browser) {
console.error("Google Chrome, Chromium, or Microsoft Edge is required for Web MIDI.");
console.error(`Open this URL in a Web MIDI browser: ${url}`);
return;
}
spawn(browser, [`--app=${url}`, "--new-window"], {
detached: true,
stdio: "ignore",
}).unref();
console.log("EP sample tool is running fully locally.");
console.log(`Local address: ${url}`);
console.log("Keep this window open while using the tool. Press Control-C to stop.");
});
for (const signal of ["SIGINT", "SIGTERM"]) {
process.on(signal, () => server.close(() => process.exit(0)));
}

9
package.json Normal file
View File

@@ -0,0 +1,9 @@
{
"name": "ep-133-sample-tool-offline-launcher",
"private": true,
"type": "module",
"scripts": {
"start": "node launcher.mjs",
"verify": "node scripts/verify.mjs"
}
}

34
scripts/verify.mjs Normal file
View File

@@ -0,0 +1,34 @@
import { createHash } from "node:crypto";
import { readFileSync, statSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const root = dirname(dirname(fileURLToPath(import.meta.url)));
const assets = join(root, "web", "apps", "ep-sample-tool", "assets");
const expected = [
"Technotype34_EP_series-Bold-CzOuNzV9.woff2",
"bg_x2-Blvo_klN.png",
"ep-133-factory-content-DRyE_DHC.pak",
"favicon-CV1VhAGr.ico",
"index-bjKAcMKp.js",
"index-qny0Yi1N.css",
"libsamplerate-C34FEted.wasm",
"libsndfile---U-zQfO.wasm",
"libtag-C1F3EvPd.wasm",
"libtag_c-DP8IdmaO.wasm",
"resample-BWif2rvd.wasm",
];
for (const name of expected) {
const path = join(assets, name);
const size = statSync(path).size;
const digest = createHash("sha256").update(readFileSync(path)).digest("hex").slice(0, 12);
console.log(`${digest} ${String(size).padStart(9)} ${name}`);
}
const bundle = readFileSync(join(assets, "index-bjKAcMKp.js"), "utf8");
for (const required of ["requestMIDIAccess", "ep-133-factory-content-DRyE_DHC.pak"]) {
if (!bundle.includes(required)) throw new Error(`App bundle is missing ${required}`);
}
console.log("Offline asset set is complete.");

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,31 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
<meta
name="description"
content="Local offline launcher for the Teenage Engineering EP sample tool"
/>
<title>ep sample tool (offline)</title>
<link
rel="icon"
type="image/x-icon"
href="/apps/ep-sample-tool/assets/favicon-CV1VhAGr.ico"
/>
<script
type="module"
src="/apps/ep-sample-tool/assets/index-bjKAcMKp.js"
></script>
<link
rel="stylesheet"
href="/apps/ep-sample-tool/assets/index-qny0Yi1N.css"
/>
</head>
<body>
<div id="root"></div>
</body>
</html>