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