initial commit
This commit is contained in:
30
.gitignore
vendored
Normal file
30
.gitignore
vendored
Normal 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
4
Launch EP Sample Tool.command
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/zsh -l
|
||||
|
||||
cd "${0:A:h}"
|
||||
exec node launcher.mjs
|
||||
37
README.md
Normal file
37
README.md
Normal 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
107
launcher.mjs
Normal 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
9
package.json
Normal 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
34
scripts/verify.mjs
Normal 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.
BIN
web/apps/ep-sample-tool/assets/bg_x2-Blvo_klN.png
Normal file
BIN
web/apps/ep-sample-tool/assets/bg_x2-Blvo_klN.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
BIN
web/apps/ep-sample-tool/assets/favicon-CV1VhAGr.ico
Normal file
BIN
web/apps/ep-sample-tool/assets/favicon-CV1VhAGr.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
108
web/apps/ep-sample-tool/assets/index-bjKAcMKp.js
Normal file
108
web/apps/ep-sample-tool/assets/index-bjKAcMKp.js
Normal file
File diff suppressed because one or more lines are too long
1
web/apps/ep-sample-tool/assets/index-qny0Yi1N.css
Normal file
1
web/apps/ep-sample-tool/assets/index-qny0Yi1N.css
Normal file
File diff suppressed because one or more lines are too long
BIN
web/apps/ep-sample-tool/assets/libsamplerate-C34FEted.wasm
Normal file
BIN
web/apps/ep-sample-tool/assets/libsamplerate-C34FEted.wasm
Normal file
Binary file not shown.
BIN
web/apps/ep-sample-tool/assets/libsndfile---U-zQfO.wasm
Normal file
BIN
web/apps/ep-sample-tool/assets/libsndfile---U-zQfO.wasm
Normal file
Binary file not shown.
BIN
web/apps/ep-sample-tool/assets/libtag-C1F3EvPd.wasm
Normal file
BIN
web/apps/ep-sample-tool/assets/libtag-C1F3EvPd.wasm
Normal file
Binary file not shown.
BIN
web/apps/ep-sample-tool/assets/libtag_c-DP8IdmaO.wasm
Normal file
BIN
web/apps/ep-sample-tool/assets/libtag_c-DP8IdmaO.wasm
Normal file
Binary file not shown.
BIN
web/apps/ep-sample-tool/assets/resample-BWif2rvd.wasm
Normal file
BIN
web/apps/ep-sample-tool/assets/resample-BWif2rvd.wasm
Normal file
Binary file not shown.
31
web/apps/ep-sample-tool/index.html
Normal file
31
web/apps/ep-sample-tool/index.html
Normal 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>
|
||||
Reference in New Issue
Block a user