diff --git a/lith/scripts/gm-bake.mjs b/lith/scripts/gm-bake.mjs index 533ecf160..06fee3cff 100755 --- a/lith/scripts/gm-bake.mjs +++ b/lith/scripts/gm-bake.mjs @@ -178,8 +178,10 @@ if (!DRY_RUN) { } // ─── SoundFont resolution ─────────────────────────────────────────────────── +// Christian Collins's official GitHub mirror — stable raw download, unlike +// the schristiancollins.com page (now an SPA whose download URLs flux). const GENERALUSER_URL = - "https://schristiancollins.com/soundfonts/GeneralUser_GS_1.471.zip"; + "https://raw.githubusercontent.com/mrbumpy409/GeneralUser-GS/main/GeneralUser-GS.sf2"; const GENERALUSER_NAME = "GeneralUser GS v1.471"; const GENERALUSER_LICENSE = `${GENERALUSER_NAME} by S. Christian Collins (https://schristiancollins.com/generaluser.php) @@ -218,10 +220,11 @@ function resolveSoundFont() { return { path: cachedSf2, name: GENERALUSER_NAME }; } log(`downloading ${GENERALUSER_NAME} → ${cachedSf2}`); - const zip = join(CACHE_DIR, "GeneralUser_GS.zip"); + const isDirectSf2 = /\.sf2$/i.test(GENERALUSER_URL); + const downloadPath = isDirectSf2 ? cachedSf2 : join(CACHE_DIR, "GeneralUser_GS.zip"); const curl = spawnSync( "curl", - ["-fsSL", "-o", zip, GENERALUSER_URL], + ["-fsSL", "-o", downloadPath, GENERALUSER_URL], { stdio: "inherit" }, ); if (curl.status !== 0) { @@ -230,15 +233,18 @@ function resolveSoundFont() { ` Upstream page: https://schristiancollins.com/generaluser.php`, ); } - // Try both unzip and bsdtar; locate the .sf2 inside. + if (isDirectSf2) { + log(`downloaded: ${cachedSf2}`); + return { path: cachedSf2, name: GENERALUSER_NAME }; + } + // Archive case: unzip and locate the .sf2 inside. const extractDir = join(CACHE_DIR, "extract"); rmSync(extractDir, { recursive: true, force: true }); mkdirSync(extractDir, { recursive: true }); - const unzip = spawnSync("unzip", ["-q", "-o", zip, "-d", extractDir], { + const unzip = spawnSync("unzip", ["-q", "-o", downloadPath, "-d", extractDir], { stdio: "inherit", }); if (unzip.status !== 0) die("unzip failed; install `unzip` or extract manually"); - // Find the SF2 file recursively. const found = execFileSync("find", [extractDir, "-name", "*.sf2"]) .toString() .trim() diff --git a/system/public/aesthetic.computer/lib/gm.mjs b/system/public/aesthetic.computer/lib/gm.mjs index c7764b5eb..c50172a80 100644 --- a/system/public/aesthetic.computer/lib/gm.mjs +++ b/system/public/aesthetic.computer/lib/gm.mjs @@ -20,7 +20,22 @@ // runtime. It does not modify disk.mjs, midi.mjs, or any disk file; the // program-change wiring will be hooked up elsewhere. -const ASSET_BASE = "https://assets.aesthetic.computer/gm"; +// Local dev (`npm run site`) serves system/public/assets/ at /assets/, so +// the bake output dropped into system/public/assets/gm/ is reachable +// without uploading to the CDN. On prod we hit the Spaces-backed CDN. +function resolveAssetBase() { + if (typeof window === "undefined") return "https://assets.aesthetic.computer/gm"; + const host = window.location?.hostname || ""; + const isLocal = + host === "localhost" || + host === "127.0.0.1" || + host === "" || + host.endsWith(".local") || + host.endsWith(".github.dev") || + host.endsWith(".gitpod.io"); + return isLocal ? "/assets/gm" : "https://assets.aesthetic.computer/gm"; +} +const ASSET_BASE = resolveAssetBase(); const MANIFEST_URL = `${ASSET_BASE}/manifest.json`; // ─── AudioContext access ────────────────────────────────────────────── @@ -191,6 +206,13 @@ function noteUrl(dir, midi) { return `${dir}/${midiToNoteName(midi)}.mp3`; } +// Drum samples are filed by raw MIDI number (e.g. 35 = Acoustic Bass Drum) +// because drum kits aren't pitched — scientific-pitch names ("B1") would be +// semantically wrong. Matches the bake script's output convention. +function drumNoteUrl(dir, midi) { + return `${dir}/${midi}.mp3`; +} + // Pick the closest rendered MIDI note to a target. function nearestRenderedNote(noteList, target) { if (!noteList || noteList.length === 0) return null; @@ -409,7 +431,7 @@ export async function loadDrumKit(kitId = 0, audioContext) { function getSample(midi) { if (samples.has(midi)) return samples.get(midi); - const promise = fetchAndDecode(noteUrl(dir, midi), ctx); + const promise = fetchAndDecode(drumNoteUrl(dir, midi), ctx); samples.set(midi, promise); return promise; }