From c1cae612df2b0411d76fd80365b51f67a7adc5ee Mon Sep 17 00:00:00 2001 From: Sourav Das Date: Wed, 16 Sep 2026 12:06:08 +0530 Subject: [PATCH 1/4] fix: start illustrations after initial loader has finished --- docs/illustration-loading.md | 37 ++++++ src/lib/initialLoad.ts | 4 +- test/first-load-animation-browser.test.mjs | 90 +++++++++++++++ test/initial-load.test.mjs | 124 +++++++++++++++++++++ 4 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 test/first-load-animation-browser.test.mjs create mode 100644 test/initial-load.test.mjs diff --git a/docs/illustration-loading.md b/docs/illustration-loading.md index 8995c40..4420858 100644 --- a/docs/illustration-loading.md +++ b/docs/illustration-loading.md @@ -56,6 +56,43 @@ their bandwidth without fixing the scene change. barrel. Text/legal pages still exclude NAM/DAW/GSAP code. Standalone screenshots in articles, guides and nonanimated cards continue to use responsive images. +## Initial-load lifecycle correction — 16 September 2026 + +The initial HTML loader (`#openstudio-instant-loader`) emits +`openstudio:intro-hidden` once and records completion in +`window.__openstudioIntroHidden`. Later route loaders reuse its artwork and +`data-openstudio-loader` attribute, but have their own entrance/exit lifecycle. +They do not emit the initial-intro event. + +The startup scheduler previously waited for that event whenever *any* loader was +present. If the initial loader timed out before the first route's code arrived, +or a visitor navigated to an uncached route, the illustration could mount while +a route loader was still exiting. The initial event had already happened, so the +GSAP request was never scheduled. Waiting longer did not help; a reload could +avoid the timing window. + +`scheduleAfterInitialLoad` now checks the recorded completion state and the +specific initial loader. An exiting route loader cannot restart the initial +wait. The existing idle delay, input scheduling and cancellation remain intact; +GSAP still loads only for eligible visible illustrations. No loader artwork, +layout, animation sequence or screenshot fallback changes are part of this fix. + +`test/initial-load.test.mjs` covers both readiness orders, absent initial loaders, +idle/input scheduling and cancellation. `test/first-load-animation-browser.test.mjs` +holds the production Home chunk past the real initial-loader timeout and tests +uncached navigation at 768 and 1440 px. It verifies that the scene's clock advances +without a refresh or further interaction; a `playing` attribute alone would also +pass for the intentionally static reduced-motion frame. Reduced motion is checked +separately for a stable clock and no animation-engine download. The slow-entry +and normal-motion navigation regressions failed against the pre-fix build. + +After this fix, the production build (including strict TypeScript), zero-warning +lint and all **147 tests** pass. The unchanged core performance matrix passes +**10 of 10 cases**; it does not include the separate NAM Rack network limitation +recorded below. Generated CSS hashes match the pre-fix build. Local reproduction +and check logs are retained in ignored `output/review/first-load-*` files and +`output/review/production-reproduction.log`. + ## Performance tradeoff A still image can give a cheap early paint while complex rendering code loads; diff --git a/src/lib/initialLoad.ts b/src/lib/initialLoad.ts index d28184d..b02d332 100644 --- a/src/lib/initialLoad.ts +++ b/src/lib/initialLoad.ts @@ -71,7 +71,9 @@ export const scheduleAfterInitialLoad = ( } function waitForIntro() { - if (document.querySelector("[data-openstudio-loader]")) { + // Route loaders share the artwork but never emit the one-time intro event. + // They can still be exiting when a late first route or an uncached route mounts. + if (!window.__openstudioIntroHidden && document.getElementById("openstudio-instant-loader")) { window.addEventListener("openstudio:intro-hidden", scheduleIdle, { once: true }); return; } diff --git a/test/first-load-animation-browser.test.mjs b/test/first-load-animation-browser.test.mjs new file mode 100644 index 0000000..f9278f8 --- /dev/null +++ b/test/first-load-animation-browser.test.mjs @@ -0,0 +1,90 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { test } from "node:test"; +import { chromium } from "playwright"; +import { preview } from "vite"; + +const manifest = JSON.parse(readFileSync(new URL("../dist/.vite/manifest.json", import.meta.url), "utf8")); +const homePath = "/" + manifest["src/pages/HomePage.tsx"].file; +const animationPath = "/" + manifest["node_modules/gsap/index.js"].file; +const clockSelector = '.sp-home-session [title="Click to toggle between time and beats"]'; + +test("illustrations start after slow first loads and uncached navigation", { timeout: 120_000 }, async (t) => { + const server = await preview({ logLevel: "error", preview: { host: "127.0.0.1", port: 0, strictPort: true } }); + let browser; + try { + browser = await chromium.launch(); + const base = server.resolvedUrls.local[0]; + const cases = [ + { name: "first route arriving after the initial loader timeout", width: 1440, slowEntry: true }, + { name: "uncached desktop navigation", width: 1440 }, + { name: "uncached tablet navigation", width: 768 }, + { name: "uncached navigation with reduced motion", width: 1440, reducedMotion: true }, + ]; + for (const entry of cases) await t.test(entry.name, async () => { + const context = await browser.newContext({ + viewport: { width: entry.width, height: 1000 }, + reducedMotion: entry.reducedMotion ? "reduce" : "no-preference", + }); + let release; + const gate = new Promise((resolve) => { release = resolve; }); + try { + await context.route("https://**/*", (route) => route.abort()); + await context.route("**" + homePath, async (route) => { + await gate; + await route.continue().catch(() => undefined); + }); + await context.addInitScript(() => localStorage.setItem( + "openstudio.analytics-consent.v1", JSON.stringify({ choice: "rejected", time: Date.now() }), + )); + const page = await context.newPage(); + const errors = []; + const animationRequests = []; + page.on("pageerror", (error) => errors.push(error.message)); + page.on("request", (request) => { + if (new URL(request.url()).pathname === animationPath) animationRequests.push(request.url()); + }); + if (entry.slowEntry) { + await page.goto(base, { waitUntil: "domcontentloaded" }); + // Exercise the real timeout, without dispatching a synthetic ready event. + await page.waitForFunction(() => window.__openstudioIntroHidden); + assert.notEqual(await page.evaluate(() => window.__openstudioAppReady), true); + } else { + // Legal pages finish the intro immediately. Home has not been fetched. + await page.goto(base + "privacy"); + await page.waitForFunction(() => window.__openstudioAppReady && window.__openstudioIntroHidden); + await page.locator('header a[href="/"]').click(); + } + await page.locator('[data-openstudio-loader]:not(#openstudio-instant-loader)').waitFor({ state: "visible" }); + release(); + await page.locator("[data-openstudio-loader]").waitFor({ state: "detached" }); + await page.locator(clockSelector).waitFor({ state: "visible" }); + if (entry.reducedMotion) { + const before = await page.locator(clockSelector).innerText(); + await page.waitForTimeout(3200); + assert.equal(await page.locator(clockSelector).innerText(), before); + assert.deepEqual(animationRequests, [], "reduced motion does not load the animation engine"); + } else { + // A static reduced-motion frame also says "playing", so check the clock + // actually advances without a reload, scroll, click or other wake-up. + await page.waitForFunction(() => document.querySelector(".sp-home-session .daw-session")?.dataset.transport === "playing", null, { timeout: 8000 }); + const before = await page.locator(clockSelector).innerText(); + await page.waitForFunction(({ selector, before }) => document.querySelector(selector)?.textContent !== before, + { selector: clockSelector, before }, { timeout: 3000 }); + assert.equal(animationRequests.length, 1); + await context.unroute("**" + homePath); + await page.reload(); + await page.waitForFunction(() => window.__openstudioAppReady && window.__openstudioIntroHidden); + await page.waitForFunction(() => document.querySelector(".sp-home-session .daw-session")?.dataset.transport === "playing"); + } + assert.deepEqual(errors, []); + } finally { + release(); + await context.close(); + } + }); + } finally { + await browser?.close(); + await new Promise((resolve, reject) => server.httpServer.close((error) => error ? reject(error) : resolve())); + } +}); diff --git a/test/initial-load.test.mjs b/test/initial-load.test.mjs new file mode 100644 index 0000000..ec1d668 --- /dev/null +++ b/test/initial-load.test.mjs @@ -0,0 +1,124 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { test } from "node:test"; +import { runInNewContext } from "node:vm"; +import ts from "typescript"; + +const source = ts.transpileModule( + readFileSync(new URL("../src/lib/initialLoad.ts", import.meta.url), "utf8"), + { compilerOptions: { module: ts.ModuleKind.CommonJS } }, +).outputText; + +function harness({ ready = true, introHidden = false, initialLoader = true, routeLoader = false } = {}) { + let nextId = 0; + const timers = new Map(); + const idleCallbacks = new Map(); + const window = Object.assign(new EventTarget(), { + __openstudioAppReady: ready, + __openstudioIntroHidden: introHidden, + setTimeout(callback, delay) { + const id = ++nextId; + timers.set(id, { callback, delay }); + return id; + }, + clearTimeout: (id) => timers.delete(id), + requestIdleCallback(callback, options) { + const id = ++nextId; + idleCallbacks.set(id, { callback, timeout: options.timeout }); + return id; + }, + cancelIdleCallback: (id) => idleCallbacks.delete(id), + }); + const exports = {}; + runInNewContext(source, { + exports, + window, + document: { + getElementById: (id) => id === "openstudio-instant-loader" && initialLoader ? {} : null, + querySelector: () => initialLoader || routeLoader ? {} : null, + }, + }); + return { + window, timers, idleCallbacks, + schedule: exports.scheduleAfterInitialLoad, + emit: (name) => window.dispatchEvent(new Event(name)), + flush(queue) { + for (const [id, { callback }] of [...queue]) { + queue.delete(id); + callback(); + } + }, + }; +} + +test("initial work waits for app readiness and the initial intro, then runs once at idle", () => { + const h = harness({ ready: false }); + let calls = 0; + h.schedule(() => calls++, { delay: 400, timeout: 2000 }); + assert.equal(h.timers.size, 0); + h.emit("openstudio:app-ready"); + assert.equal(h.timers.size, 0); + h.emit("openstudio:intro-hidden"); + assert.equal([...h.timers.values()][0].delay, 400); + h.flush(h.timers); + assert.equal(calls, 0); + assert.equal([...h.idleCallbacks.values()][0].timeout, 2000); + h.flush(h.idleCallbacks); + h.emit("pointerdown"); + assert.equal(calls, 1); +}); + +for (const ready of [true, false]) { + test(`a route loader cannot strand work after the initial intro (app ready: ${ready})`, () => { + const h = harness({ ready, introHidden: true, initialLoader: false, routeLoader: true }); + let calls = 0; + h.schedule(() => calls++); + if (!ready) h.emit("openstudio:app-ready"); + h.flush(h.timers); + h.flush(h.idleCallbacks); + assert.equal(calls, 1, "no second intro-hidden event is needed"); + }); +} + +test("a page without the initial loader does not wait for an intro event", () => { + const h = harness({ initialLoader: false, routeLoader: true }); + let calls = 0; + h.schedule(() => calls++); + h.emit("keydown"); + h.flush(h.timers); + h.flush(h.idleCallbacks); + assert.equal(calls, 1); +}); + +test("cancellation removes pending readiness, input, timer and idle callbacks", () => { + for (const phase of ["app", "intro", "delay", "idle"]) { + const h = harness({ ready: false }); + let calls = 0; + const cancel = h.schedule(() => calls++); + if (phase !== "app") h.emit("openstudio:app-ready"); + if (phase === "delay" || phase === "idle") h.emit("openstudio:intro-hidden"); + if (phase === "idle") h.flush(h.timers); + cancel(); + h.emit("openstudio:app-ready"); + h.emit("openstudio:intro-hidden"); + h.emit("pointerdown"); + h.flush(h.timers); + h.flush(h.idleCallbacks); + assert.equal(calls, 0, phase); + assert.equal(h.timers.size + h.idleCallbacks.size, 0, phase); + } +}); + +test("input-disabled work keeps the timer fallback when idle callbacks are unavailable", () => { + const h = harness({ initialLoader: false }); + delete h.window.requestIdleCallback; + let calls = 0; + h.schedule(() => calls++, { runOnInput: false, timeout: 2000 }); + h.emit("pointerdown"); + assert.equal(calls, 0); + h.flush(h.timers); + assert.equal([...h.timers.values()][0].delay, 1400); + assert.equal(calls, 0); + h.flush(h.timers); + assert.equal(calls, 1); +}); From 4c65dd84de2bd882a02c96bd793e6ef62bcfa3f6 Mon Sep 17 00:00:00 2001 From: Sourav Das Date: Wed, 16 Sep 2026 12:48:22 +0530 Subject: [PATCH 2/4] fix: make system requirements readable on phones --- docs/visual-regression-correction.md | 24 +++++++++++ src/pages/DownloadPage.tsx | 28 ++++++++++++- test/redesign-routes-browser.test.mjs | 57 +++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/docs/visual-regression-correction.md b/docs/visual-regression-correction.md index c68d7b8..0c71b7f 100644 --- a/docs/visual-regression-correction.md +++ b/docs/visual-regression-correction.md @@ -1,5 +1,29 @@ # Visual regression correction — 16 September 2026 +## Follow-up: mobile download requirements + +The download page's 640 px minimum-width comparison grid gave the sticky label +column about 227 px, even inside a 280–350 px phone card. That left very little +room for the values and covered text as the visitor scrolled sideways. + +Below 640 px, the requirements now use a definition list: each component has +full-width Minimum and Recommended values. All four components and eight values +come from the same `systemRequirementMatrix` as the larger-screen comparison. +CSS selects the layout, so it also works before JavaScript and when the viewport +changes. There is no extra request or screen-size JavaScript. The phone section +is intentionally taller because every value is readable without horizontal +scrolling. The side-by-side comparison remains at 640 px and wider. + +Visual captures cover 320, 390, 640, 768, 900, 901 and 1440 px. The comparison +grid's columns and styling match the approved `97a3f2e` design; the retained +current copy is unchanged. Before/after captures at 768, 901 and 1440 px are +pixel-identical; 640 and 900 px retain the same dimensions with six and one +different pixels respectively. Phone captures were inspected for text wrapping +and spacing. Evidence is in ignored `output/playwright/requirements-*.png`. +`test/redesign-routes-browser.test.mjs` checks all eight values for clipping at +phone widths, the layout switch at 640 px, both sides of the navigation breakpoint, +and the JavaScript-disabled phone page. + ## What happened The branding/cleanup work replaced suprabho's two-piece SVG loader with a single diff --git a/src/pages/DownloadPage.tsx b/src/pages/DownloadPage.tsx index 0739c93..68934ab 100644 --- a/src/pages/DownloadPage.tsx +++ b/src/pages/DownloadPage.tsx @@ -181,9 +181,33 @@ const DownloadPage = () => { {/* System requirements */}
-
System requirements
+

System requirements

-
+
+ {systemRequirementMatrix.map((row) => ( +
+
{row.component}
+
+
+ {([ + ["Minimum", row.minimum], + ["Recommended", row.recommended], + ] as const).map(([label, value]) => ( +
+
+ {label} +
+
+ {value} +
+
+ ))} +
+
+
+ ))} +
+
{["Minimum", "Recommended"].map((heading, index) => (
{ + const context = await browser.newContext({ viewport: { width, height: 1000 }, reducedMotion: "reduce" }); + try { + await context.route("https://**/*", (route) => route.abort()); + const page = await context.newPage(); + await page.goto(base + "download"); + await page.waitForFunction(() => window.__openstudioAppReady && window.__openstudioIntroHidden); + await page.evaluate(() => document.fonts.ready); + const card = page.locator("#requirements .sp-card"); + const details = card.locator(":scope > dl"); + const comparison = card.locator(":scope > div"); + if (width < 640) { + assert.equal(await comparison.isVisible(), false); + const rows = details.locator(":scope > div"); + assert.deepEqual(await rows.locator(":scope > dt").allTextContents(), ["Processor", "Memory", "Storage", "OS / Audio"]); + for (const row of await rows.all()) { + assert.deepEqual(await row.locator("dd dt").allTextContents(), ["Minimum", "Recommended"]); + const values = row.locator("dd dd"); + assert.equal(await values.count(), 2); + for (const value of await values.all()) { + assert.equal(await value.isVisible(), true); + assert.ok((await value.innerText()).trim().length > 0); + const fits = await value.evaluate((element) => { + const rect = element.getBoundingClientRect(); + return rect.left >= 0 && rect.right <= innerWidth + && element.scrollWidth <= element.clientWidth + 1 + && element.scrollHeight <= element.clientHeight + 1; + }); + assert.equal(fits, true, await value.innerText()); + } + } + assert.equal(await card.evaluate((element) => element.scrollWidth > element.clientWidth), false); + } else { + assert.equal(await details.isVisible(), false); + assert.equal(await comparison.isVisible(), true); + assert.equal(await comparison.evaluate((element) => getComputedStyle(element).gridTemplateColumns.split(" ").length), 3); + } + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false); + } finally { + await context.close(); + } + }); + } + await t.test("mobile requirements are readable without JavaScript", async () => { + const context = await browser.newContext({ javaScriptEnabled: false, viewport: { width: 390, height: 900 } }); + try { + const page = await context.newPage(); + await page.goto(base + "download"); + const values = page.locator("#requirements dl dd dd"); + assert.equal(await values.count(), 8); + for (const value of await values.all()) assert.equal(await value.isVisible(), true); + assert.equal(await page.locator("#requirements .sp-card").evaluate((element) => element.scrollWidth > element.clientWidth), false); + } finally { + await context.close(); + } + }); await t.test("legacy aliases preserve their destinations and unknown routes return 404", async () => { const context = await browser.newContext({ reducedMotion: "reduce" }); try { From 5bd0de39f0019ca23a294a091f79279d844b2d85 Mon Sep 17 00:00:00 2001 From: Sourav Das Date: Wed, 16 Sep 2026 15:34:20 +0530 Subject: [PATCH 3/4] fix: restore illustration playback on phones and reloads --- AGENTS.md | 2 + README.md | 2 +- docs/illustration-loading.md | 66 ++++++++++- src/features/daw-preview/LiveSession.tsx | 4 +- src/features/daw-preview/stage/StageFrame.tsx | 3 - .../daw-preview/stage/stageScheduler.ts | 11 +- .../daw-preview/stage/useStageTimeline.ts | 16 ++- .../daw-preview/stages/ArrangementStage.tsx | 4 +- .../daw-preview/stages/MixerStage.tsx | 4 +- .../daw-preview/stages/NamChainStage.tsx | 4 +- .../daw-preview/stages/NamRackStage.tsx | 5 +- .../daw-preview/stages/PianoRollStage.tsx | 4 +- .../daw-preview/stages/PitchEditorStage.tsx | 4 +- .../daw-preview/stages/PluginWindowStage.tsx | 4 +- .../daw-preview/stages/RenderDialogStage.tsx | 4 +- .../daw-preview/stages/namRackScript.ts | 2 +- test/first-load-animation-browser.test.mjs | 29 ++++- test/mobile-illustrations-browser.test.mjs | 104 ++++++++++++++++++ test/stage-scheduler.test.mjs | 40 +++++++ test/still-frame-browser.test.mjs | 2 +- 20 files changed, 277 insertions(+), 37 deletions(-) create mode 100644 test/mobile-illustrations-browser.test.mjs create mode 100644 test/stage-scheduler.test.mjs diff --git a/AGENTS.md b/AGENTS.md index 2b331a5..5d651d0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,6 +58,8 @@ by installed OpenStudio apps. Start with [README.md](README.md) and The image GraphQL service has been retired. - Keep decorative controls out of keyboard navigation and preserve reduced-motion alternatives, menu focus behavior and readable content before JavaScript. + Small viewport size must not disable illustration playback. Verify that phones + actually animate on first visit, while offscreen and hidden-tab timelines pause. ## Content and branding diff --git a/README.md b/README.md index 69157e1..519fb72 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ Run `npm run build` before `npm test` in a clean checkout; tests consume the gen - Browser tests cover all canonical routes at 390, 768 and 1440 px, legacy redirects/404s, navigation and keyboard behavior, privacy consent, lazy-load recovery and GitHub release label/link consistency. - SEO browser tests visit every sitemap page with JavaScript disabled and enabled, compare the head metadata and structured data, validate social-image dimensions, and check metadata cleanup during navigation and 404 recovery. - Focused browser regressions cover normal-motion two-piece loading, AI card/table layout, current-page mobile-menu activation, and Features/NAM image selection at standard and high-density resolutions. Build tests check upgrade guidance before JavaScript; contract tests reject malformed repository snapshots and verify retry behavior. -- Illustration tests delay or fail the animation-engine request, verify that the same scene elements and dimensions remain, and cover prerendered artwork with JavaScript disabled. Ordinary screenshot/thumbnail delivery tests remain separate. +- Illustration tests delay or fail the animation-engine request, verify that the same scene elements and dimensions remain, and cover prerendered artwork with JavaScript disabled. Phone tests check real playback on fresh visits, delayed loads and uncached navigation, plus offscreen pausing, reduced motion and every NAM tile. Ordinary screenshot/thumbnail delivery tests remain separate. - The suite also includes unit, source-contract and build tests. A reported total is not an E2E-only count. Desktop app tests live in the separate app repository. - Browsers currently run in Chromium. The eight-width visual comparison recorded in the audit is a manual review artifact, not an automated screenshot-regression suite. Firefox/WebKit coverage and CI screenshot baselines are follow-up improvements. - Loading performance is an explicit `npm run verify:perf` check; the current CI workflow does not run that matrix automatically. diff --git a/docs/illustration-loading.md b/docs/illustration-loading.md index 4420858..0a5973a 100644 --- a/docs/illustration-loading.md +++ b/docs/illustration-loading.md @@ -30,8 +30,9 @@ their bandwidth without fixing the scene change. screenshot, bitmap crossfade or second scene to replace it. - `useStageTimeline` loads GSAP after initial loading, only when the visible stage has a playback slot. It animates the same DOM elements. Off-screen, - hidden-tab and scheduler-limited timelines pause; reduced-motion and small - stages retain their authored static state. + hidden-tab and scheduler-limited timelines pause; reduced motion and explicit + playback disabling retain their authored static state. Visible illustrations + animate at phone, tablet and desktop sizes. - `LiveStage` reserves its aspect ratio and uses `content-visibility: auto` so the browser can skip layout and painting of distant off-screen scenes. This does not substitute another scene or remove the actual renderer's DOM. It is @@ -93,6 +94,67 @@ recorded below. Generated CSS hashes match the pre-fix build. Local reproduction and check logs are retained in ignored `output/review/first-load-*` files and `output/review/production-reproduction.log`. +## Phone playback correction — 16 September 2026 + +The initial-load event fix above did not address a separate size gate. Every +illustration disabled its timeline below 60% of its design width (45% for the +compact NAM chain). Home scales to approximately 41%, 52% and 58% at 320, 390 and +430 px, respectively. These phones therefore never requested GSAP, even with +normal motion enabled. Waiting or refreshing could not remove the size gate. + +Removed that gate from Home and all eight stage renderers. Viewport size now +controls only layout. The shared driver still honors reduced motion, explicit +disabling, document visibility and its two-active-stage limit. Small NAM tiles +can share a viewport; the most eligible two play, and scrolling changes their +eligibility. Other tiles hold their frame instead of all consuming animation +work simultaneously. + +The all-tile test also exposed fractional clipping in the NAM grid: amp/cab +reported a 0.99865 intersection ratio while the following EQ/post row reported +1. The latter kept taking both slots even after scrolling amp/cab into view. +The scheduler now treats at least 99% visibility as fully visible, retaining +the priority order and two-stage cap. A regression using the measured fractions +fails against the previous scheduler and passes with this correction. + +Reload stress checks found another intermittent stall: the driver was settled, +the document visible and the intro complete, but every stage still had a zero +intersection ratio. Visibility observation now starts after the initial-loading +gate, and returning to a visible tab requests a fresh observation. This avoids +depending on measurements made while the route was hidden. Browser coverage +simulates missed loading-time visibility notifications and verifies that reload +still starts the clock without a scroll, tap or another refresh. + +The original artwork, sizing, loop choreography and two-piece loader are intact. +Phones now use the existing animation rest frame and start playback instead of +remaining on the reduced-motion sample. No screenshot placeholders were added. +All four generated CSS bundle hashes match the preceding build. + +The earlier rest-frame test only required Home playback at widths of at least +768 px. It now includes 390 px. First-load coverage adds delayed phone entry, +uncached phone navigation and reduced-motion phone navigation; the two +normal-motion regressions failed before the size-gate removal. The dedicated +mobile browser test checks fresh 320/390/430 px contexts with touch/mobile +emulation, actual clock advancement without input, offscreen pause/resume, +all eight illustration types and the rack tour plus all six smaller rack tiles. +Tile checks scroll the relevant row below the sticky header so it can receive +a playback slot. Canvas repainting alone is not treated as proof of a running +scene timeline. + +This is Chromium phone emulation, not physical Android/iOS or Safari testing. +Local evidence is under ignored `output/review/mobile-animation-fix/` and +`output/playwright/phone-*` files. The AI guide's accompanying INT8 correction +is sourced separately in [the music-model review](music-models-blog-review.md). + +The final production build (including strict TypeScript), zero-warning lint and +all **181 tests** pass. Earlier failed runs are retained: they exposed the phone +cutoff, fractional NAM visibility and intermittent zero-intersection reload. +The controlled visibility regression fails before the observer lifecycle change +and passes afterward; the final full suite includes that scenario. +The unchanged core performance matrix passes **10/10** cases. A separate mobile +NAM Rack check measures 3.12 s reveal-adjusted LCP, zero layout shift, 376 ms of +long tasks and 664.6 KiB encoded. Its 48 requests still exceed the generic limit +of 35; no request budget was raised. These are local throttled measurements. + ## Performance tradeoff A still image can give a cheap early paint while complex rendering code loads; diff --git a/src/features/daw-preview/LiveSession.tsx b/src/features/daw-preview/LiveSession.tsx index cbe1c3f..ffba294 100644 --- a/src/features/daw-preview/LiveSession.tsx +++ b/src/features/daw-preview/LiveSession.tsx @@ -3,7 +3,7 @@ import { ArrangementLanes, arrangementHeight } from "./ArrangementLanes"; import { BigClockLite } from "./BigClockLite"; import { MixerPanelLite } from "./MixerPanelLite"; import { LOOP_RANGE, SESSION_LENGTH, TEMPO, TIME_SIGNATURE, TRACKS } from "./sessionScript"; -import { MIN_ANIMATED_SCALE, StageFrame, useStageFit } from "./stage/StageFrame"; +import { StageFrame, useStageFit } from "./stage/StageFrame"; import { TransportLite } from "./TransportLite"; import { useSessionTimeline } from "./useSessionTimeline"; @@ -27,7 +27,7 @@ interface LiveSessionProps { const LiveSession = ({ className, paused = false }: LiveSessionProps) => { const outerRef = useRef(null); const { width: stageWidth, scale } = useStageFit(outerRef, MIN_STAGE_WIDTH); - const state = useSessionTimeline({ scope: outerRef, enabled: !paused && scale >= MIN_ANIMATED_SCALE }); + const state = useSessionTimeline({ scope: outerRef, enabled: !paused }); const lanes = TRACKS.map((track, index) => ({ name: track.name, diff --git a/src/features/daw-preview/stage/StageFrame.tsx b/src/features/daw-preview/stage/StageFrame.tsx index 0512ce9..af4d16a 100644 --- a/src/features/daw-preview/stage/StageFrame.tsx +++ b/src/features/daw-preview/stage/StageFrame.tsx @@ -5,9 +5,6 @@ import { StaticRenderContext } from "@/lib/staticRender"; const useBrowserLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect; -/** Below this scale the 7–9 px labels stop being legible; show the static frame. */ -export const MIN_ANIMATED_SCALE = 0.6; - /** * Fits a fixed design width to the element's current width, in both * directions: a column wider than the design scales the stage up so it fills diff --git a/src/features/daw-preview/stage/stageScheduler.ts b/src/features/daw-preview/stage/stageScheduler.ts index f7548e4..6be53f5 100644 --- a/src/features/daw-preview/stage/stageScheduler.ts +++ b/src/features/daw-preview/stage/stageScheduler.ts @@ -1,13 +1,13 @@ /** * Caps how many live stages animate at once. A features grid can have four * stages in view; letting them all tween and commit at 30 fps is wasteful, so - * only the most visible (then highest priority) `MAX_PLAYING` run and the rest + * only the highest-priority visible stages (then most visible) run and the rest * hold their last frame. */ export interface StageEntry { /** Visible fraction reported by the stage's IntersectionObserver. */ ratio: number; - /** Ties on ratio go to the higher priority (hero/carousel pass 1). */ + /** Heroes/carousels pass 1 to rank ahead of other visible stages. */ priority: number; /** Called whenever the stage's allowance changes. */ onAllowed: (allowed: boolean) => void; @@ -46,8 +46,11 @@ export const registerStage = (entry: StageEntry) => { }; export const updateStageRatio = (entry: StageEntry, ratio: number) => { - if (entry.ratio === ratio) return; - entry.ratio = ratio; + // A subpixel container clip can report 0.9986 for an otherwise fully visible + // phone tile. Treat the top 1% as fully visible so later rows cannot starve it. + const visibleRatio = ratio >= 0.99 ? 1 : ratio; + if (entry.ratio === visibleRatio) return; + entry.ratio = visibleRatio; rebalance(); }; diff --git a/src/features/daw-preview/stage/useStageTimeline.ts b/src/features/daw-preview/stage/useStageTimeline.ts index 33d3b29..e1c46cf 100644 --- a/src/features/daw-preview/stage/useStageTimeline.ts +++ b/src/features/daw-preview/stage/useStageTimeline.ts @@ -30,7 +30,7 @@ export interface StageTimelineSpec { export interface StageTimelineOptions { /** Element whose visibility gates playback. */ scope: RefObject; - /** False on stages too small to read; renders the static frame instead. */ + /** Explicitly disables playback and renders the static frame instead. */ enabled?: boolean; /** Delay before the first beat, so the reveal has landed. */ startDelay?: number; @@ -135,12 +135,13 @@ export const useStageTimeline = ( { threshold: [0, 0.05, 0.25, 0.5, 0.75, 1] }, ) : undefined; - observer?.observe(element); - // No observer: assume visible so the stage still runs. - if (!observer) updateStageRatio(entry, 1); - const onVisibility = () => { pageVisible = document.visibilityState !== "hidden"; + if (pageVisible && settled && observer) { + // Ask for fresh geometry when returning from a hidden/background page. + observer.unobserve(element); + observer.observe(element); + } syncPlayback(); }; document.addEventListener("visibilitychange", onVisibility); @@ -148,6 +149,11 @@ export const useStageTimeline = ( const cancelSchedule = scheduleAfterInitialLoad( () => { settled = true; + // Subscribing while the route/intro is hidden can leave a stale zero + // intersection until another viewport change. Sample the revealed page. + observer?.observe(element); + // No observer: assume visible so the stage still runs. + if (!observer) updateStageRatio(entry, 1); syncPlayback(); }, { delay: 400, timeout: 2000 }, diff --git a/src/features/daw-preview/stages/ArrangementStage.tsx b/src/features/daw-preview/stages/ArrangementStage.tsx index ff970f1..2478428 100644 --- a/src/features/daw-preview/stages/ArrangementStage.tsx +++ b/src/features/daw-preview/stages/ArrangementStage.tsx @@ -2,7 +2,7 @@ import { useRef } from "react"; import { ArrangementLanes, arrangementHeight } from "../ArrangementLanes"; import { BigClockLite } from "../BigClockLite"; import { rowMetrics } from "../clipArt"; -import { MIN_ANIMATED_SCALE, StageFrame, useStageScale } from "../stage/StageFrame"; +import { StageFrame, useStageScale } from "../stage/StageFrame"; import type { StageProps } from "../stage/LiveStage"; import { useStageTimeline } from "../stage/useStageTimeline"; import { TRACK_HEADER_WIDTH } from "../TrackHeaderLite"; @@ -24,7 +24,7 @@ const ArrangementStage = ({ variant = "default", priority, className }: StagePro const outerRef = useRef(null); const scale = useStageScale(outerRef, STAGE_WIDTH); const kind = (variant in SPECS ? variant : "default") as ArrangementVariant; - const state = useStageTimeline(SPECS[kind], { scope: outerRef, enabled: scale >= MIN_ANIMATED_SCALE, priority, startDelay: 0.6 }); + const state = useStageTimeline(SPECS[kind], { scope: outerRef, priority, startDelay: 0.6 }); const capacity = laneCapacity(kind); const laneHeight = Math.floor((STAGE_HEIGHT - TRANSPORT_HEIGHT - 30) / capacity); diff --git a/src/features/daw-preview/stages/MixerStage.tsx b/src/features/daw-preview/stages/MixerStage.tsx index f3afbc8..8a96b89 100644 --- a/src/features/daw-preview/stages/MixerStage.tsx +++ b/src/features/daw-preview/stages/MixerStage.tsx @@ -1,6 +1,6 @@ import { useRef } from "react"; import { MixerPanelLite } from "../MixerPanelLite"; -import { MIN_ANIMATED_SCALE, StageFrame, useStageScale } from "../stage/StageFrame"; +import { StageFrame, useStageScale } from "../stage/StageFrame"; import type { StageProps } from "../stage/LiveStage"; import { useStageTimeline } from "../stage/useStageTimeline"; import { TransportLite } from "../TransportLite"; @@ -13,7 +13,7 @@ const TRANSPORT_HEIGHT = 40; const MixerStage = ({ priority, className }: StageProps) => { const outerRef = useRef(null); const scale = useStageScale(outerRef, STAGE_WIDTH); - const state = useStageTimeline(SPEC, { scope: outerRef, enabled: scale >= MIN_ANIMATED_SCALE, priority, startDelay: 0.6 }); + const state = useStageTimeline(SPEC, { scope: outerRef, priority, startDelay: 0.6 }); return ( > = { const NamChainStage = ({ priority, className }: StageProps) => { const outerRef = useRef(null); const scale = useStageScale(outerRef, STAGE_WIDTH); - const state = useStageTimeline(SPEC, { scope: outerRef, enabled: scale >= MIN_ANIMATED_SCALE * 0.75, priority, startDelay: 0.6, fps: 20 }); + const state = useStageTimeline(SPEC, { scope: outerRef, priority, startDelay: 0.6, fps: 20 }); const fixedPre: NAMSignalChainRouteModule[] = [ { diff --git a/src/features/daw-preview/stages/NamRackStage.tsx b/src/features/daw-preview/stages/NamRackStage.tsx index 2d381cb..ada8e91 100644 --- a/src/features/daw-preview/stages/NamRackStage.tsx +++ b/src/features/daw-preview/stages/NamRackStage.tsx @@ -1,5 +1,5 @@ import { useMemo, useRef } from "react"; -import { MIN_ANIMATED_SCALE, StageFrame, useStageScale } from "../stage/StageFrame"; +import { StageFrame, useStageScale } from "../stage/StageFrame"; import type { StageProps } from "../stage/LiveStage"; import { useStageTimeline } from "../stage/useStageTimeline"; import { NAMRackDesignPort } from "../vendor/NAMRackDesignPort"; @@ -31,7 +31,7 @@ const LABELS = { /** * `variant` is "
[+tuner][+tour]", e.g. "amp+tour" for the page hero - * or "cab" for a tile. Tiles below the legibility scale show a static frame. + * or "cab" for a tile. Visible tiles animate at every viewport size. */ const NamRackStage = ({ variant, priority, className }: StageProps) => { const outerRef = useRef(null); @@ -40,7 +40,6 @@ const NamRackStage = ({ variant, priority, className }: StageProps) => { const spec = useMemo(() => specFor(parsed), [parsed]); const state = useStageTimeline(spec, { scope: outerRef, - enabled: scale >= MIN_ANIMATED_SCALE, priority, startDelay: 0.8, // The port is a large tree; 20 commits a second is plenty for knobs and meters. diff --git a/src/features/daw-preview/stages/PianoRollStage.tsx b/src/features/daw-preview/stages/PianoRollStage.tsx index 00b8636..edb893e 100644 --- a/src/features/daw-preview/stages/PianoRollStage.tsx +++ b/src/features/daw-preview/stages/PianoRollStage.tsx @@ -2,7 +2,7 @@ import { useRef } from "react"; import { Eraser, MousePointer2, Pencil } from "lucide-react"; import { cn } from "@/lib/utils"; import { DawButton } from "../DawButton"; -import { MIN_ANIMATED_SCALE, StageFrame, useStageScale } from "../stage/StageFrame"; +import { StageFrame, useStageScale } from "../stage/StageFrame"; import type { StageProps } from "../stage/LiveStage"; import { useStageTimeline } from "../stage/useStageTimeline"; import { TransportLite } from "../TransportLite"; @@ -32,7 +32,7 @@ const rowY = (row: number) => GRID_HEIGHT - (row + 1) * ROW_HEIGHT; const PianoRollStage = ({ priority, className }: StageProps) => { const outerRef = useRef(null); const scale = useStageScale(outerRef, STAGE_WIDTH); - const state = useStageTimeline(SPEC, { scope: outerRef, enabled: scale >= MIN_ANIMATED_SCALE, priority, startDelay: 0.6 }); + const state = useStageTimeline(SPEC, { scope: outerRef, priority, startDelay: 0.6 }); const playheadX = state.beat * BEAT_WIDTH; return ( diff --git a/src/features/daw-preview/stages/PitchEditorStage.tsx b/src/features/daw-preview/stages/PitchEditorStage.tsx index 58033b5..9499ae0 100644 --- a/src/features/daw-preview/stages/PitchEditorStage.tsx +++ b/src/features/daw-preview/stages/PitchEditorStage.tsx @@ -2,7 +2,7 @@ import { useRef } from "react"; import { AudioWaveform, MoveVertical, Waves } from "lucide-react"; import { cn } from "@/lib/utils"; import { DawButton } from "../DawButton"; -import { MIN_ANIMATED_SCALE, StageFrame, useStageScale } from "../stage/StageFrame"; +import { StageFrame, useStageScale } from "../stage/StageFrame"; import type { StageProps } from "../stage/LiveStage"; import { useStageTimeline } from "../stage/useStageTimeline"; import { TransportLite } from "../TransportLite"; @@ -28,7 +28,7 @@ const rowCenterY = (row: number) => GRID_HEIGHT - (row + 0.5) * ROW_HEIGHT; const PitchEditorStage = ({ priority, className }: StageProps) => { const outerRef = useRef(null); const scale = useStageScale(outerRef, STAGE_WIDTH); - const state = useStageTimeline(SPEC, { scope: outerRef, enabled: scale >= MIN_ANIMATED_SCALE, priority, startDelay: 0.6, fps: 24 }); + const state = useStageTimeline(SPEC, { scope: outerRef, priority, startDelay: 0.6, fps: 24 }); const playheadX = KEYS_WIDTH + state.beat * BEAT_WIDTH; return ( diff --git a/src/features/daw-preview/stages/PluginWindowStage.tsx b/src/features/daw-preview/stages/PluginWindowStage.tsx index a8a60b3..d809f0a 100644 --- a/src/features/daw-preview/stages/PluginWindowStage.tsx +++ b/src/features/daw-preview/stages/PluginWindowStage.tsx @@ -2,7 +2,7 @@ import { useMemo, useRef } from "react"; import { ChevronDown, Minus, Power, Square, X } from "lucide-react"; import { cn } from "@/lib/utils"; import { DawButton } from "../DawButton"; -import { MIN_ANIMATED_SCALE, StageFrame, useStageScale } from "../stage/StageFrame"; +import { StageFrame, useStageScale } from "../stage/StageFrame"; import type { StageProps } from "../stage/LiveStage"; import { useStageTimeline } from "../stage/useStageTimeline"; import { EQGraph } from "../vendor/ParametricGraph/EQGraph"; @@ -36,7 +36,7 @@ const formatFreq = (freq: number) => (freq >= 1000 ? `${(freq / 1000).toFixed(fr const PluginWindowStage = ({ priority, className }: StageProps) => { const outerRef = useRef(null); const scale = useStageScale(outerRef, STAGE_WIDTH); - const state = useStageTimeline(SPEC, { scope: outerRef, enabled: scale >= MIN_ANIMATED_SCALE, priority, startDelay: 0.6, fps: 24 }); + const state = useStageTimeline(SPEC, { scope: outerRef, priority, startDelay: 0.6, fps: 24 }); const sliders = useMemo(() => toSliders(state.bands, state.bypass), [state.bands, state.bypass]); const active = state.bands.filter((band) => band.enabled); diff --git a/src/features/daw-preview/stages/RenderDialogStage.tsx b/src/features/daw-preview/stages/RenderDialogStage.tsx index 2f7b74c..9de8bc2 100644 --- a/src/features/daw-preview/stages/RenderDialogStage.tsx +++ b/src/features/daw-preview/stages/RenderDialogStage.tsx @@ -3,7 +3,7 @@ import { Check, ChevronDown, X } from "lucide-react"; import { cn } from "@/lib/utils"; import { ArrangementLanes } from "../ArrangementLanes"; import { DawButton } from "../DawButton"; -import { MIN_ANIMATED_SCALE, StageFrame, useStageScale } from "../stage/StageFrame"; +import { StageFrame, useStageScale } from "../stage/StageFrame"; import type { StageProps } from "../stage/LiveStage"; import { useStageTimeline } from "../stage/useStageTimeline"; import { TransportLite } from "../TransportLite"; @@ -112,7 +112,7 @@ const Dialog = ({ state }: { state: RenderDialogState }) => ( const RenderDialogStage = ({ priority, className }: StageProps) => { const outerRef = useRef(null); const scale = useStageScale(outerRef, STAGE_WIDTH); - const state = useStageTimeline(SPEC, { scope: outerRef, enabled: scale >= MIN_ANIMATED_SCALE, priority, startDelay: 0.5, fps: 24 }); + const state = useStageTimeline(SPEC, { scope: outerRef, priority, startDelay: 0.5, fps: 24 }); return ( { +test("illustrations start after slow first loads and uncached navigation", { timeout: 180_000 }, async (t) => { const server = await preview({ logLevel: "error", preview: { host: "127.0.0.1", port: 0, strictPort: true } }); let browser; try { @@ -17,13 +17,20 @@ test("illustrations start after slow first loads and uncached navigation", { tim const base = server.resolvedUrls.local[0]; const cases = [ { name: "first route arriving after the initial loader timeout", width: 1440, slowEntry: true }, + { name: "phone first route arriving after the initial loader timeout", width: 390, mobile: true, slowEntry: true }, { name: "uncached desktop navigation", width: 1440 }, { name: "uncached tablet navigation", width: 768 }, + { name: "uncached phone navigation", width: 320, mobile: true }, + { name: "phone reload with missed visibility notifications during the intro", width: 390, mobile: true, lateVisibility: true }, { name: "uncached navigation with reduced motion", width: 1440, reducedMotion: true }, + { name: "uncached phone navigation with reduced motion", width: 390, mobile: true, reducedMotion: true }, ]; for (const entry of cases) await t.test(entry.name, async () => { const context = await browser.newContext({ viewport: { width: entry.width, height: 1000 }, + isMobile: entry.mobile ?? false, + hasTouch: entry.mobile ?? false, + deviceScaleFactor: entry.mobile ? 2 : 1, reducedMotion: entry.reducedMotion ? "reduce" : "no-preference", }); let release; @@ -37,6 +44,26 @@ test("illustrations start after slow first loads and uncached navigation", { tim await context.addInitScript(() => localStorage.setItem( "openstudio.analytics-consent.v1", JSON.stringify({ choice: "rejected", time: Date.now() }), )); + if (entry.lateVisibility) await context.addInitScript(() => { + const NativeObserver = window.IntersectionObserver; + // Reproduce a loading-time visibility subscription that never delivers + // a usable sample. A fresh subscription after the intro can see the page. + window.IntersectionObserver = class extends NativeObserver { + constructor(callback, options) { + const readyTargets = new WeakSet(); + super((entries, observer) => callback(entries.filter((entry) => readyTargets.has(entry.target)), observer), options); + this.readyTargets = readyTargets; + } + observe(target) { + if (!target.classList.contains("daw-session") || window.__openstudioIntroHidden) this.readyTargets.add(target); + super.observe(target); + } + unobserve(target) { + this.readyTargets.delete(target); + super.unobserve(target); + } + }; + }); const page = await context.newPage(); const errors = []; const animationRequests = []; diff --git a/test/mobile-illustrations-browser.test.mjs b/test/mobile-illustrations-browser.test.mjs new file mode 100644 index 0000000..495eb3d --- /dev/null +++ b/test/mobile-illustrations-browser.test.mjs @@ -0,0 +1,104 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { test } from "node:test"; +import { chromium } from "playwright"; +import { preview } from "vite"; + +const manifest = JSON.parse(readFileSync(new URL("../dist/.vite/manifest.json", import.meta.url), "utf8")); +const animationPath = "/" + manifest["node_modules/gsap/index.js"].file; +const clockSelector = '.sp-home-session [title="Click to toggle between time and beats"]'; + +// Check React-controlled artwork, not canvas pixels: meters can repaint even +// when the scene's GSAP timeline never starts. Two separate changes exclude a +// one-time measurement or switch from the prerendered frame. +async function expectPlayback(page, scene) { + await scene.scrollIntoViewIfNeeded(); + // NAM tiles can all fit in a tall phone viewport, while only two are allowed + // to play at once. Move earlier rows offscreen so this scene gets a slot. + await scene.evaluate((element) => { + const top = element.closest(".sp-live-stage").getBoundingClientRect().top; + const header = document.querySelector("header").getBoundingClientRect().height; + window.scrollBy({ top: top - header - 16, behavior: "instant" }); + }); + await scene.waitFor({ state: "visible" }); + const element = await scene.elementHandle(); + for (let sample = 0; sample < 2; sample += 1) { + const before = await scene.innerHTML(); + await page.waitForFunction(({ element, before }) => element.innerHTML !== before, + { element, before }, { timeout: 8000 }); + } +} + +test("phone illustrations animate at every size and pause outside the viewport", { timeout: 180_000 }, async (t) => { + const server = await preview({ logLevel: "error", preview: { host: "127.0.0.1", port: 0, strictPort: true } }); + let browser; + try { + browser = await chromium.launch(); + for (const width of [320, 390, 430]) await t.test(`${width}px fresh phone visit`, async (phoneTest) => { + const context = await browser.newContext({ + viewport: { width, height: 844 }, isMobile: true, hasTouch: true, + deviceScaleFactor: 2, reducedMotion: "no-preference", + }); + try { + await context.route("https://**/*", (route) => route.abort()); + await context.addInitScript(() => localStorage.setItem( + "openstudio.analytics-consent.v1", JSON.stringify({ choice: "rejected", time: Date.now() }), + )); + const page = await context.newPage(); + const errors = []; + const engineRequests = []; + page.on("pageerror", (error) => errors.push(error.message)); + page.on("request", (request) => { + if (new URL(request.url()).pathname === animationPath) engineRequests.push(request.url()); + }); + await page.goto(server.resolvedUrls.local[0]); + await page.waitForFunction(() => window.__openstudioAppReady && window.__openstudioIntroHidden); + const clock = page.locator(clockSelector); + const before = await clock.innerText(); + // No tap, scroll or refresh is allowed to wake up initial playback. + await page.waitForFunction(({ selector, before }) => document.querySelector(selector)?.textContent !== before, + { selector: clockSelector, before }, { timeout: 8000 }); + assert.equal(engineRequests.length, 1); + + if (width === 390) { + await page.locator("footer").scrollIntoViewIfNeeded(); + await page.waitForTimeout(500); + const paused = await clock.innerText(); + await page.waitForTimeout(900); + assert.equal(await clock.innerText(), paused, "offscreen phone hero pauses"); + await clock.scrollIntoViewIfNeeded(); + await page.waitForFunction(({ selector, paused }) => document.querySelector(selector)?.textContent !== paused, + { selector: clockSelector, paused }, { timeout: 3000 }); + + await page.getByRole("tab", { name: "NAM Rack" }).click(); + await expectPlayback(page, page.locator('[data-stage="nam-chain"] .daw-session__stage')); + await page.getByRole("tab", { name: "Local AI" }).click(); + await expectPlayback(page, page.locator('.sp-showcase [data-stage="arrangement"] .daw-session__stage')); + + await page.goto(server.resolvedUrls.local[0] + "features"); + await page.waitForFunction(() => window.__openstudioAppReady && window.__openstudioIntroHidden); + for (const stage of ["arrangement", "piano-roll", "mixer", "plugin-window", "pitch-editor", "render-dialog"]) { + await phoneTest.test(`${stage} plays on a phone`, async () => { + await expectPlayback(page, page.locator(`[data-stage="${stage}"] .daw-session__stage`)); + }); + } + + await page.goto(server.resolvedUrls.local[0] + "nam-rack"); + await page.waitForFunction(() => window.__openstudioAppReady && window.__openstudioIntroHidden); + const racks = page.locator('[data-stage="nam-rack"] .daw-session__stage'); + assert.ok(await racks.count() > 1, "cover the rack tour and its smaller tiles"); + for (const [index, scene] of (await racks.all()).entries()) { + await phoneTest.test(`NAM rack scene ${index + 1} plays on a phone`, async () => expectPlayback(page, scene)); + } + } + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false); + assert.deepEqual(errors, []); + } finally { + await context.close(); + } + }); + } finally { + await browser?.close(); + await new Promise((resolve, reject) => server.httpServer.close((error) => error ? reject(error) : resolve())); + } +}); diff --git a/test/stage-scheduler.test.mjs b/test/stage-scheduler.test.mjs new file mode 100644 index 0000000..77d2664 --- /dev/null +++ b/test/stage-scheduler.test.mjs @@ -0,0 +1,40 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { test } from "node:test"; +import { runInNewContext } from "node:vm"; +import ts from "typescript"; + +const source = ts.transpileModule( + readFileSync(new URL("../src/features/daw-preview/stage/stageScheduler.ts", import.meta.url), "utf8") + .replace("import.meta.env.DEV", "false"), + { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }, +).outputText; + +test("fractional clipping cannot starve the current NAM row of its two playback slots", () => { + const scheduler = {}; + runInNewContext(source, { exports: scheduler }); + // Production phone measurements: amp/cab are effectively fully visible, + // but a subpixel clip gives them 0.99865 while the following row gets 1. + const entries = [0.37, 0.37, 0.998655915, 0.998655915, 1, 1].map((ratio) => ({ + ratio: 0, priority: 0, onAllowed() {}, measuredRatio: ratio, + })); + const unregister = entries.map(scheduler.registerStage); + entries.forEach((entry) => scheduler.updateStageRatio(entry, entry.measuredRatio)); + assert.deepEqual(entries.map(scheduler.isStageAllowed), [false, false, true, true, false, false]); + scheduler.updateStageRatio(entries[2], 0.38); + scheduler.updateStageRatio(entries[3], 0.38); + assert.deepEqual(entries.map(scheduler.isStageAllowed), [false, false, false, false, true, true]); + unregister.forEach((remove) => remove()); + assert.ok(entries.every((entry) => !scheduler.isStageAllowed(entry))); +}); + +test("visible heroes retain priority; offscreen heroes free their playback slot", () => { + const scheduler = {}; + runInNewContext(source, { exports: scheduler }); + const entries = [0, 0, 1].map((priority) => ({ ratio: 0, priority, onAllowed() {} })); + entries.forEach(scheduler.registerStage); + entries.forEach((entry) => scheduler.updateStageRatio(entry, 1)); + assert.deepEqual(entries.map(scheduler.isStageAllowed), [true, false, true]); + scheduler.updateStageRatio(entries[2], 0); + assert.deepEqual(entries.map(scheduler.isStageAllowed), [true, true, false]); +}); diff --git a/test/still-frame-browser.test.mjs b/test/still-frame-browser.test.mjs index 330db62..1f30c3e 100644 --- a/test/still-frame-browser.test.mjs +++ b/test/still-frame-browser.test.mjs @@ -26,7 +26,7 @@ test("illustrations keep the same rendered frame while the animation engine load "openstudio.analytics-consent.v1", JSON.stringify({ choice: "rejected", time: Date.now() }), )); const page = await context.newPage(); - const animationRequest = path === "" && width >= 768 + const animationRequest = path === "" ? page.waitForRequest((request) => new URL(request.url()).pathname === animationPath) : undefined; const errors = []; From 795f0039ca5ff4286a3d2af97969b05483ed7efc Mon Sep 17 00:00:00 2001 From: Sourav Das Date: Wed, 16 Sep 2026 15:34:20 +0530 Subject: [PATCH 4/4] docs: verify Original and INT8 model setup against the app --- ...nimax-stable-audio-diffusers-openstudio.md | 8 ++++--- docs/music-models-blog-review.md | 10 +++++++++ ...nimax-stable-audio-diffusers-openstudio.ts | 2 +- src/data/generatedBlogIndex.ts | 2 +- src/features/docs/content/ai-runtime-setup.ts | 22 ++++++++++++++++++- 5 files changed, 38 insertions(+), 6 deletions(-) diff --git a/blogs/2026-09-16-minimax-stable-audio-diffusers-openstudio.md b/blogs/2026-09-16-minimax-stable-audio-diffusers-openstudio.md index 46a9e0c..7f5f68b 100644 --- a/blogs/2026-09-16-minimax-stable-audio-diffusers-openstudio.md +++ b/blogs/2026-09-16-minimax-stable-audio-diffusers-openstudio.md @@ -66,11 +66,13 @@ Downloads reuse completed cached files when you retry. A failed or cancelled set ## What about a smaller, quantized MiniMax? -**This release does not offer a supported quantized MiniMax download or a general-purpose INT8, INT4 or GGUF option.** There is experimental INT8 work in the app repository, including checks for a locally qualified configuration. That is not a low-VRAM model we can tell everyone to install. +**Update, 16 September 2026:** development builds containing [app commit `681fec8`](https://github.com/sdevil7th/OpenStudio/commit/681fec8) now offer **Original** and **INT8** model versions for all three models. In **AI Tools Setup**, **Download and Prepare INT8** reuses installed original weights or downloads the official originals, then saves and verifies a separate quantized copy. This is a local preparation step, so the first download is not smaller. Prepared OpenStudio INT8 folders can also be imported. Older installed releases may not have these controls. -The available memory-saving approach is **offloading**. OpenStudio can keep components on the GPU when there is room, or move components and language-model layers between system RAM and the GPU. This reduces how much must fit in VRAM at once. Transfers take time, and the model still needs substantial system RAM. +INT8 currently requires an **NVIDIA CUDA GPU**. It reduces the precision of MiniMax's language-model weights, or the diffusion-transformer weights in ACE-Step and Stable Audio; the other audio components keep their normal inference precision. The model picker remembers the selected version on the AI track, and Original and INT8 have separate installation status. There is no general-purpose INT4 or GGUF selector. -Quantization would change how the weights are represented to make them smaller. Offloading changes where they live. A machine with less VRAM can benefit from offloading and still run out of system memory, especially with a long song request. +This makes MiniMax more practical on the configuration we checked, but it does not turn it into a tiny model. The app's [qualification report](https://github.com/sdevil7th/OpenStudio/blob/681fec8/docs/ai-quantization-2026-09-16.md) records real generation with an RTX 4080, 16 GB VRAM and 32 GB system RAM. Long songs can still take many minutes, and MiniMax INT8 uses about 15 GiB of temporary disk cache for inactive stages. Those checks do not establish support for every smaller GPU or prove that quantized output sounds identical. + +Quantization makes weights smaller; **offloading** controls where components live while they are needed. Moving data between storage, system RAM and the GPU takes time. Lower memory use is useful, but it does not guarantee faster generation. Other GPU backends and CPU-only systems should use Original, where the model's hardware checks allow it. The [model-version setup guide](/docs/ai-runtime-setup#model-versions) covers the controls and installation details. Use the app's **Hardware check** for the request you intend to run, and refresh it after closing other applications. It is an estimate, not a guarantee that every stage will fit. We are not claiming universal support for a particular small GPU. Hugging Face's [memory optimization guide](https://huggingface.co/docs/diffusers/optimization/memory) is a useful explanation of the underlying tradeoffs. diff --git a/docs/music-models-blog-review.md b/docs/music-models-blog-review.md index e83b270..0daaab5 100644 --- a/docs/music-models-blog-review.md +++ b/docs/music-models-blog-review.md @@ -25,6 +25,16 @@ authors. It makes no new speed benchmark or subjective audio-quality claim. ## Source checks +**INT8 follow-up — 16 September 2026:** the quantization paragraph below records +the original review against `7f59cff`. App commit `681fec8` subsequently added +explicit Original/INT8 selection and separate prepared installations for all three +generation models. The article and AI runtime guide now reflect that development +behavior. Verified the selector, setup modal, AI-track persistence tests, +`ai_model_variants.py`, `prepare_diffusers_audio.py`, generation preflight and the +committed quantization report. The copy retains NVIDIA CUDA restrictions, original +download size, MiniMax's disk-cache cost and the limits of the RTX 4080 evidence. +This website review did not rerun native GPU generation or qualify other hardware. + Desktop base: `7f59cff92c4a5f70704ba5985e7da373900d6162`. The AI source files listed below were committed in that checkout. Its user manual and automation implementation also have concurrent working-tree edits; those were not treated diff --git a/src/data/generatedBlogContent/2026-09-16-minimax-stable-audio-diffusers-openstudio.ts b/src/data/generatedBlogContent/2026-09-16-minimax-stable-audio-diffusers-openstudio.ts index 0d97416..de37e2a 100644 --- a/src/data/generatedBlogContent/2026-09-16-minimax-stable-audio-diffusers-openstudio.ts +++ b/src/data/generatedBlogContent/2026-09-16-minimax-stable-audio-diffusers-openstudio.ts @@ -1,4 +1,4 @@ // Generated by scripts/generate-blog-content.mjs from trusted local Markdown. -const articleHtml = "

You have a verse, a guitar part and a fairly good idea of where the chorus should go. Hearing a few possible directions can be more useful than staring at the empty bars after it. That is the kind of session we had in mind while bringing two more generation models into OpenStudio.

\n

Alongside the rebranding release, MiniMax Music 3 and Stable Audio 3 Medium join ACE-Step 1.5 XL Turbo in the app. You can work from a description, bring your own lyrics, lay out song sections, or ask for a new version of an existing clip. The available tools depend on the model you choose. Results come back into the project as audio you can edit and mix.

\n

All three integrations use Hugging Face Diffusers. The models run locally after setup, and the large downloads are optional. You can keep using the DAW without installing any of them.

\n

Which model should I reach for?

\n

We wanted the model picker to answer a practical question: what are you trying to do with this piece of music?

\n
Model in OpenStudioStart something newWork with an existing clip
ACE-Step 1.5 XL TurboText to Music; Lyrics + StyleCreate Variation; Inpaint Selection; Continue Clip
MiniMax Music 3Lyrics + Style; Song SectionsUse ACE-Step or Stable Audio for these tools
Stable Audio 3 MediumText to AudioCreate Variation; Inpaint Selection; Continue Clip
\n

ACE-Step: a musical sketch, with room to revise

\n

ACE-Step remains useful when you want to describe a musical direction and start trying arrangements. Its controls include lyrics, BPM, duration, time signature, language, key and scale, a seed, and diffusion settings.

\n

An idea such as “a restrained rock verse with dry drums, a moving bass line and a much wider chorus” gives you something to react to. Once there is audio in the session, the clip tools let you explore a variation or work on a smaller passage. You can also feed those tools a recording you made yourself.

\n

We use the ACE-Step 1.5 XL Turbo Diffusers checkpoint. If you followed the earlier integration, this builds on the move to Diffusers that was already part of OpenStudio.

\n

MiniMax Music 3: give the song some shape

\n

MiniMax is the addition for working from lyrics and a more developed song description. Lyrics + Style accepts your words with section tags such as [verse] and [chorus] on their own lines. Song Sections provides separate verse, chorus and optional bridge fields, plus vocal direction and arrangement notes.

\n

That makes it easier to express an idea like a close, quiet vocal in the verse, harmonies in the chorus, and drums entering after the first section. The lyrics and the production direction have their own places in the form.

\n

OpenStudio exposes a Maximum length of up to five minutes. It is a ceiling: the model can finish the song earlier. Longer requests also ask more of your hardware. Tempo, instrumentation and section instructions guide the generation; they are not exact arrangement commands. MiniMax's model card describes the same distinction.

\n

Stable Audio 3 Medium: describe a sound, then work with it

\n

Stable Audio adds Text to Audio, with a sound description, duration, seed and steps. You can describe an instrumental passage, a texture or a sound effect, then bring the result into an arrangement alongside your recordings.

\n

It also supports all three source-audio tools. That is useful when you already like the character of a clip and want to explore what comes next, or replace a passage without generating an entirely new idea from text.

\n

OpenStudio uses the distilled Medium model. Its default is eight steps, with guidance fixed for this workflow. More steps are not automatically an improvement, and the app does not expose negative-prompt, CFG or LoRA controls for this integration. The Stable Audio 3 Diffusers documentation explains why the distilled checkpoint needs different settings from the base model.

\n

Three useful moves once you have a clip

\n

Right-click an audio clip and open AI Generation to use ACE-Step or Stable Audio:

\n
    \n
  • Create Variation uses the clip as context for a related version. ACE-Step's Source Preservation and Stable Audio's Variation Amount have different meanings, so check the selected model's controls before turning either one up.
  • \n
  • Inpaint Selection generates a replacement for the time selection that overlaps the clip. Make the selection first. The result includes the surrounding clip, so you can compare the new passage in context.
  • \n
  • Continue Clip generates a tail from the source audio. Its Tail Length control describes the extension you are asking for.
  • \n
\n

Variation and inpainting create an aligned result on a new track, keeping the original available. A continuation tail is placed at the source clip's end, on the same track if there is room; if it would overlap another clip, it gets a new track. You can audition the result, cut the useful part, process it through your effects, and keep arranging.

\n

A generated song is still audio. These models do not automatically turn it into separate vocal, drum and instrument tracks. OpenStudio's optional stem-separation tool is a separate step if you want to take a result apart.

\n

Getting the models onto your machine

\n

Open AI Tools Setup and choose the model you want. ACE-Step has guided installation. For MiniMax and Stable Audio, review the model terms and choose Download and Set Up. OpenStudio downloads the required files from Hugging Face, prepares the runtime and checks the model before making it active.

\n

Stable Audio's original checkpoint needs conversion into Diffusers format. Setup handles that conversion, but it still needs time and extra disk space. Access to the Stable Audio model on Hugging Face must be approved, including the applicable Stability AI and Gemma terms. Use a read token from that account, or an existing Hugging Face login. Accepting the checkbox in OpenStudio does not grant access on Hugging Face.

\n

MiniMax's public download does not require a token. A token entered into OpenStudio is used for that setup operation and is not saved by the app. Local model import remains available if you already have the files.

\n

Downloads reuse completed cached files when you retry. A failed or cancelled setup leaves the previous installed model in place. Once setup is complete, generation uses the local model; your prompts and source audio are not uploaded to Hugging Face for inference. The AI setup guide covers the steps and troubleshooting in more detail.

\n

What about a smaller, quantized MiniMax?

\n

This release does not offer a supported quantized MiniMax download or a general-purpose INT8, INT4 or GGUF option. There is experimental INT8 work in the app repository, including checks for a locally qualified configuration. That is not a low-VRAM model we can tell everyone to install.

\n

The available memory-saving approach is offloading. OpenStudio can keep components on the GPU when there is room, or move components and language-model layers between system RAM and the GPU. This reduces how much must fit in VRAM at once. Transfers take time, and the model still needs substantial system RAM.

\n

Quantization would change how the weights are represented to make them smaller. Offloading changes where they live. A machine with less VRAM can benefit from offloading and still run out of system memory, especially with a long song request.

\n

Use the app's Hardware check for the request you intend to run, and refresh it after closing other applications. It is an estimate, not a guarantee that every stage will fit. We are not claiming universal support for a particular small GPU. Hugging Face's memory optimization guide is a useful explanation of the underlying tradeoffs.

\n

The parts that took engineering work

\n

Getting a model to return a WAV is an early milestone. Making it behave inside a session means dealing with setup failures, memory pressure, cancellation and the exact bit of audio the musician selected.

\n

Three pipelines need different treatment

\n

Diffusers gives us implementations we can inspect and build around, but the models still have different structures. MiniMax combines sequential language-model generation with a diffusion stage and audio decoding. Its song length affects the growing generation cache as well as the final audio. Stable Audio has its own text encoder, scheduler and SAME audio autoencoder. ACE-Step retains its own managed runtime.

\n

We had to make the app's controls, progress reporting and worker lifecycle follow those differences. MiniMax audio-frame progress and a denoising-step percentage describe particular stages. Loading and decoding may need an indeterminate indicator. A percentage invented from elapsed time would tell the musician very little about what was actually happening. The MiniMax Diffusers pipeline documentation shows how its stages fit together.

\n

Long audio exposed a decoder bottleneck

\n

One concrete problem appeared after Stable Audio had finished generating its compressed audio representation. In our long-form test, the decoder could become the part that failed to finish within the test window.

\n

The pinned SAME implementation built large square attention masks inside blocks that only needed nearby context. We added bounded, overlapping processing windows for those blocks, with enough surrounding context to retain the unaffected center of each window. Preserving absolute positions and handling the edges correctly mattered as much as reducing memory use.

\n

The regression checks cover window boundaries and numerical agreement against the original path. Those checks establish a narrower engineering result; listening is still necessary to judge a generated take. The relevant upstream implementation is Diffusers' SAME autoencoder.

\n

Installation and recovery are part of using the instrument

\n

Stable Audio's conversion also needed a fix for a Windows checkpoint-reader crash. MiniMax's download needed to select its Diffusers components without pulling down duplicate legacy weights. A partly prepared model must not replace a working installation.

\n

During generation, memory recovery must preserve the request. The bounded out-of-memory retry for ACE-Step and Stable Audio keeps the user's settings rather than quietly shortening the job. Idle model caches are released after about two minutes, and active jobs survive closing and reopening the generation panel while the app remains running. Cancellation is explicit.

\n

These details are less visible than the first successful song, but they matter on the second attempt, the interrupted download and the session where another application is already using half the GPU.

\n

Thank you, Hugging Face and the Diffusers team

\n

A special thank-you to Hugging Face and the Diffusers maintainers and contributors. The Hub's model distribution, the readable pipeline implementations, the documentation and the ongoing work on memory management made these integrations possible. Being able to trace a problem through the actual implementation has been especially valuable while bringing longer audio generation into a desktop app.

\n

Thank you as well to the ACE-Step, MiniMax and Stability AI teams for the models and their technical work. OpenStudio builds on that effort, and we want the people using these tools to know where it comes from.

\n

Try a short idea first. Give it a clear direction, listen to what came back, and keep the part that makes you want to carry on. The useful result might be a whole song, a better transition, or just a few seconds that suggest what to record next.

"; +const articleHtml = "

You have a verse, a guitar part and a fairly good idea of where the chorus should go. Hearing a few possible directions can be more useful than staring at the empty bars after it. That is the kind of session we had in mind while bringing two more generation models into OpenStudio.

\n

Alongside the rebranding release, MiniMax Music 3 and Stable Audio 3 Medium join ACE-Step 1.5 XL Turbo in the app. You can work from a description, bring your own lyrics, lay out song sections, or ask for a new version of an existing clip. The available tools depend on the model you choose. Results come back into the project as audio you can edit and mix.

\n

All three integrations use Hugging Face Diffusers. The models run locally after setup, and the large downloads are optional. You can keep using the DAW without installing any of them.

\n

Which model should I reach for?

\n

We wanted the model picker to answer a practical question: what are you trying to do with this piece of music?

\n
Model in OpenStudioStart something newWork with an existing clip
ACE-Step 1.5 XL TurboText to Music; Lyrics + StyleCreate Variation; Inpaint Selection; Continue Clip
MiniMax Music 3Lyrics + Style; Song SectionsUse ACE-Step or Stable Audio for these tools
Stable Audio 3 MediumText to AudioCreate Variation; Inpaint Selection; Continue Clip
\n

ACE-Step: a musical sketch, with room to revise

\n

ACE-Step remains useful when you want to describe a musical direction and start trying arrangements. Its controls include lyrics, BPM, duration, time signature, language, key and scale, a seed, and diffusion settings.

\n

An idea such as “a restrained rock verse with dry drums, a moving bass line and a much wider chorus” gives you something to react to. Once there is audio in the session, the clip tools let you explore a variation or work on a smaller passage. You can also feed those tools a recording you made yourself.

\n

We use the ACE-Step 1.5 XL Turbo Diffusers checkpoint. If you followed the earlier integration, this builds on the move to Diffusers that was already part of OpenStudio.

\n

MiniMax Music 3: give the song some shape

\n

MiniMax is the addition for working from lyrics and a more developed song description. Lyrics + Style accepts your words with section tags such as [verse] and [chorus] on their own lines. Song Sections provides separate verse, chorus and optional bridge fields, plus vocal direction and arrangement notes.

\n

That makes it easier to express an idea like a close, quiet vocal in the verse, harmonies in the chorus, and drums entering after the first section. The lyrics and the production direction have their own places in the form.

\n

OpenStudio exposes a Maximum length of up to five minutes. It is a ceiling: the model can finish the song earlier. Longer requests also ask more of your hardware. Tempo, instrumentation and section instructions guide the generation; they are not exact arrangement commands. MiniMax's model card describes the same distinction.

\n

Stable Audio 3 Medium: describe a sound, then work with it

\n

Stable Audio adds Text to Audio, with a sound description, duration, seed and steps. You can describe an instrumental passage, a texture or a sound effect, then bring the result into an arrangement alongside your recordings.

\n

It also supports all three source-audio tools. That is useful when you already like the character of a clip and want to explore what comes next, or replace a passage without generating an entirely new idea from text.

\n

OpenStudio uses the distilled Medium model. Its default is eight steps, with guidance fixed for this workflow. More steps are not automatically an improvement, and the app does not expose negative-prompt, CFG or LoRA controls for this integration. The Stable Audio 3 Diffusers documentation explains why the distilled checkpoint needs different settings from the base model.

\n

Three useful moves once you have a clip

\n

Right-click an audio clip and open AI Generation to use ACE-Step or Stable Audio:

\n
    \n
  • Create Variation uses the clip as context for a related version. ACE-Step's Source Preservation and Stable Audio's Variation Amount have different meanings, so check the selected model's controls before turning either one up.
  • \n
  • Inpaint Selection generates a replacement for the time selection that overlaps the clip. Make the selection first. The result includes the surrounding clip, so you can compare the new passage in context.
  • \n
  • Continue Clip generates a tail from the source audio. Its Tail Length control describes the extension you are asking for.
  • \n
\n

Variation and inpainting create an aligned result on a new track, keeping the original available. A continuation tail is placed at the source clip's end, on the same track if there is room; if it would overlap another clip, it gets a new track. You can audition the result, cut the useful part, process it through your effects, and keep arranging.

\n

A generated song is still audio. These models do not automatically turn it into separate vocal, drum and instrument tracks. OpenStudio's optional stem-separation tool is a separate step if you want to take a result apart.

\n

Getting the models onto your machine

\n

Open AI Tools Setup and choose the model you want. ACE-Step has guided installation. For MiniMax and Stable Audio, review the model terms and choose Download and Set Up. OpenStudio downloads the required files from Hugging Face, prepares the runtime and checks the model before making it active.

\n

Stable Audio's original checkpoint needs conversion into Diffusers format. Setup handles that conversion, but it still needs time and extra disk space. Access to the Stable Audio model on Hugging Face must be approved, including the applicable Stability AI and Gemma terms. Use a read token from that account, or an existing Hugging Face login. Accepting the checkbox in OpenStudio does not grant access on Hugging Face.

\n

MiniMax's public download does not require a token. A token entered into OpenStudio is used for that setup operation and is not saved by the app. Local model import remains available if you already have the files.

\n

Downloads reuse completed cached files when you retry. A failed or cancelled setup leaves the previous installed model in place. Once setup is complete, generation uses the local model; your prompts and source audio are not uploaded to Hugging Face for inference. The AI setup guide covers the steps and troubleshooting in more detail.

\n

What about a smaller, quantized MiniMax?

\n

Update, 16 September 2026: development builds containing app commit 681fec8 now offer Original and INT8 model versions for all three models. In AI Tools Setup, Download and Prepare INT8 reuses installed original weights or downloads the official originals, then saves and verifies a separate quantized copy. This is a local preparation step, so the first download is not smaller. Prepared OpenStudio INT8 folders can also be imported. Older installed releases may not have these controls.

\n

INT8 currently requires an NVIDIA CUDA GPU. It reduces the precision of MiniMax's language-model weights, or the diffusion-transformer weights in ACE-Step and Stable Audio; the other audio components keep their normal inference precision. The model picker remembers the selected version on the AI track, and Original and INT8 have separate installation status. There is no general-purpose INT4 or GGUF selector.

\n

This makes MiniMax more practical on the configuration we checked, but it does not turn it into a tiny model. The app's qualification report records real generation with an RTX 4080, 16 GB VRAM and 32 GB system RAM. Long songs can still take many minutes, and MiniMax INT8 uses about 15 GiB of temporary disk cache for inactive stages. Those checks do not establish support for every smaller GPU or prove that quantized output sounds identical.

\n

Quantization makes weights smaller; offloading controls where components live while they are needed. Moving data between storage, system RAM and the GPU takes time. Lower memory use is useful, but it does not guarantee faster generation. Other GPU backends and CPU-only systems should use Original, where the model's hardware checks allow it. The model-version setup guide covers the controls and installation details.

\n

Use the app's Hardware check for the request you intend to run, and refresh it after closing other applications. It is an estimate, not a guarantee that every stage will fit. We are not claiming universal support for a particular small GPU. Hugging Face's memory optimization guide is a useful explanation of the underlying tradeoffs.

\n

The parts that took engineering work

\n

Getting a model to return a WAV is an early milestone. Making it behave inside a session means dealing with setup failures, memory pressure, cancellation and the exact bit of audio the musician selected.

\n

Three pipelines need different treatment

\n

Diffusers gives us implementations we can inspect and build around, but the models still have different structures. MiniMax combines sequential language-model generation with a diffusion stage and audio decoding. Its song length affects the growing generation cache as well as the final audio. Stable Audio has its own text encoder, scheduler and SAME audio autoencoder. ACE-Step retains its own managed runtime.

\n

We had to make the app's controls, progress reporting and worker lifecycle follow those differences. MiniMax audio-frame progress and a denoising-step percentage describe particular stages. Loading and decoding may need an indeterminate indicator. A percentage invented from elapsed time would tell the musician very little about what was actually happening. The MiniMax Diffusers pipeline documentation shows how its stages fit together.

\n

Long audio exposed a decoder bottleneck

\n

One concrete problem appeared after Stable Audio had finished generating its compressed audio representation. In our long-form test, the decoder could become the part that failed to finish within the test window.

\n

The pinned SAME implementation built large square attention masks inside blocks that only needed nearby context. We added bounded, overlapping processing windows for those blocks, with enough surrounding context to retain the unaffected center of each window. Preserving absolute positions and handling the edges correctly mattered as much as reducing memory use.

\n

The regression checks cover window boundaries and numerical agreement against the original path. Those checks establish a narrower engineering result; listening is still necessary to judge a generated take. The relevant upstream implementation is Diffusers' SAME autoencoder.

\n

Installation and recovery are part of using the instrument

\n

Stable Audio's conversion also needed a fix for a Windows checkpoint-reader crash. MiniMax's download needed to select its Diffusers components without pulling down duplicate legacy weights. A partly prepared model must not replace a working installation.

\n

During generation, memory recovery must preserve the request. The bounded out-of-memory retry for ACE-Step and Stable Audio keeps the user's settings rather than quietly shortening the job. Idle model caches are released after about two minutes, and active jobs survive closing and reopening the generation panel while the app remains running. Cancellation is explicit.

\n

These details are less visible than the first successful song, but they matter on the second attempt, the interrupted download and the session where another application is already using half the GPU.

\n

Thank you, Hugging Face and the Diffusers team

\n

A special thank-you to Hugging Face and the Diffusers maintainers and contributors. The Hub's model distribution, the readable pipeline implementations, the documentation and the ongoing work on memory management made these integrations possible. Being able to trace a problem through the actual implementation has been especially valuable while bringing longer audio generation into a desktop app.

\n

Thank you as well to the ACE-Step, MiniMax and Stability AI teams for the models and their technical work. OpenStudio builds on that effort, and we want the people using these tools to know where it comes from.

\n

Try a short idea first. Give it a clear direction, listen to what came back, and keep the part that makes you want to carry on. The useful result might be a whole song, a better transition, or just a few seconds that suggest what to record next.

"; export default articleHtml; diff --git a/src/data/generatedBlogIndex.ts b/src/data/generatedBlogIndex.ts index 40bd5ef..731a6c8 100644 --- a/src/data/generatedBlogIndex.ts +++ b/src/data/generatedBlogIndex.ts @@ -2,4 +2,4 @@ // Keep article bodies out of this file so the blog index never downloads every post. export const generatedBlogPostSeoOverrides = {"minimax-stable-audio-diffusers-openstudio":{"author":"OpenStudio team","dateModified":"2026-09-16","seoTitle":"MiniMax Music 3 & Stable Audio 3 in OpenStudio | Blog","seoDescription":"Explore MiniMax Music 3, Stable Audio 3 Medium and ACE-Step in OpenStudio: local Diffusers generation, clip editing, guided setup and memory tradeoffs.","imageAlt":"A sunlit home studio with a MIDI keyboard, headphones and an open songwriting notebook on a wooden desk.","keywords":["OpenStudio AI music","MiniMax Music 3","Stable Audio 3 Medium","ACE-Step 1.5 XL Turbo","Diffusers audio generation","local music generation","AI audio inpainting"]},"building-openstudio-nam-rack":{"author":"OpenStudio engineering team","dateModified":"2026-08-31","imageSlug":"building-openstudio-nam-rack-free-guitar-rig","seoTitle":"Building a Free NAM Guitar Rig Inside OpenStudio | OpenStudio Blog","seoDescription":"How OpenStudio’s free NAM guitar rig brings A1/A2 captures, native pedals, cabinet IRs, TONE3000 access, presets, and project recall into one open-source DAW.","imageAlt":"OpenStudio NAM Rack amp, pedals, post effects, and TONE3000 capture browser arranged in a music studio.","imageFit":"contain","keywords":["free guitar amp simulator","free guitar rig","open-source amp simulator","NAM A2 player","Neural Amp Modeler DAW","AmpliTube alternative","Guitar Rig alternative","Neural DSP alternative","free amp capture software","TONE3000 integration"]},"build-guitar-tones-with-openstudio-nam-rack":{"author":"OpenStudio team","dateModified":"2026-08-29","imageSlug":"build-guitar-tones-with-openstudio-nam-rack-v2","seoTitle":"How to Build Clean and High-Gain Tones with OpenStudio NAM Rack","seoDescription":"A practical guitarist's guide to OpenStudio NAM Rack, with exact clean and high-gain preset settings plus tone advice from Rabea Massaad, Nolly Getgood, and Misha Mansoor.","imageAlt":"OpenStudio NAM Rack framed by blue clean-tone and red high-gain waveform treatments.","imageFit":"contain","keywords":["OpenStudio NAM Rack tutorial","NAM guitar tone","Neural Amp Modeler preset","clean guitar tone settings","high gain guitar tone settings","5150 NAM tone","free guitar amp simulator","Precision Drive settings","guitar cabinet IR","TONE3000"]}} as const; -export const generatedBlogPosts = [{"author":"OpenStudio team","slug":"minimax-stable-audio-diffusers-openstudio","title":"MiniMax, Stable Audio and ACE-Step: three ways into a song","dek":"MiniMax Music 3 and Stable Audio 3 Medium join ACE-Step in OpenStudio, with local generation, guided setup and more ways to work with the audio already in your session.","summary":"MiniMax Music 3 and Stable Audio 3 Medium join ACE-Step in OpenStudio, with local generation, guided setup and more ways to work with the audio already in your session.","sourcePath":"blogs/2026-09-16-minimax-stable-audio-diffusers-openstudio.md","filename":"2026-09-16-minimax-stable-audio-diffusers-openstudio.md","wordCount":1871,"readTimeMinutes":9,"image":"/assets/blogs/minimax-stable-audio-diffusers-openstudio.webp?v=c1943e68e130","imageAlt":"A sunlit home studio with a MIDI keyboard, headphones and an open songwriting notebook on a wooden desk.","keywords":["OpenStudio AI music","MiniMax Music 3","Stable Audio 3 Medium","ACE-Step 1.5 XL Turbo","Diffusers audio generation","local music generation","AI audio inpainting"],"seoDescription":"Explore MiniMax Music 3, Stable Audio 3 Medium and ACE-Step in OpenStudio: local Diffusers generation, clip editing, guided setup and memory tradeoffs.","seoTitle":"MiniMax Music 3 & Stable Audio 3 in OpenStudio | Blog","date":"2026-09-16","dateLabel":"September 16, 2026","dateModified":"2026-09-16","dateModifiedLabel":"September 16, 2026"},{"author":"OpenStudio team","slug":"build-guitar-tones-with-openstudio-nam-rack","title":"Build Better Guitar Tones with OpenStudio's NAM Rack","dek":"A guitarist-first guide to building tones in OpenStudio, with a repeatable workflow, exact settings from two real presets, and practical ideas from Rabea Massaad, Adam “Nolly” Getgood, and Misha Mansoor.","summary":"A guitarist-first guide to building tones in OpenStudio, with a repeatable workflow, exact settings from two real presets, and practical ideas from Rabea Massaad, Adam “Nolly” Getgood,...","sourcePath":"blogs/2026-08-28-build-guitar-tones-with-openstudio-nam-rack.md","filename":"2026-08-28-build-guitar-tones-with-openstudio-nam-rack.md","wordCount":2825,"readTimeMinutes":13,"image":"/assets/blogs/build-guitar-tones-with-openstudio-nam-rack-v2.webp?v=e7b476333487","imageAlt":"OpenStudio NAM Rack framed by blue clean-tone and red high-gain waveform treatments.","imageFit":"contain","keywords":["OpenStudio NAM Rack tutorial","NAM guitar tone","Neural Amp Modeler preset","clean guitar tone settings","high gain guitar tone settings","5150 NAM tone","free guitar amp simulator","Precision Drive settings","guitar cabinet IR","TONE3000"],"seoDescription":"A practical guitarist's guide to OpenStudio NAM Rack, with exact clean and high-gain preset settings plus tone advice from Rabea Massaad, Nolly Getgood, and Misha Mansoor.","seoTitle":"How to Build Clean and High-Gain Tones with OpenStudio NAM Rack","date":"2026-08-28","dateLabel":"August 28, 2026","dateModified":"2026-08-29","dateModifiedLabel":"August 29, 2026"},{"author":"OpenStudio engineering team","slug":"building-openstudio-nam-rack","title":"Building a Free NAM Guitar Rig Inside OpenStudio","dek":"OpenStudio now includes a redesigned, complete NAM guitar rig inside the DAW: A1 and A2 captures, native pre-effects, optional Pedal NAM, cabinet shaping, Cabinet Space, studio effects, TONE3000 access, tuning,...","summary":"OpenStudio now includes a redesigned, complete NAM guitar rig inside the DAW: A1 and A2 captures, native pre-effects, optional Pedal NAM, cabinet shaping, Cabinet Space, studio effects,...","sourcePath":"blogs/2026-07-26-building-openstudio-nam-rack.md","filename":"2026-07-26-building-openstudio-nam-rack.md","wordCount":1987,"readTimeMinutes":9,"image":"/assets/blogs/building-openstudio-nam-rack-free-guitar-rig.webp?v=8f6e918d26dd","imageAlt":"OpenStudio NAM Rack amp, pedals, post effects, and TONE3000 capture browser arranged in a music studio.","imageFit":"contain","keywords":["free guitar amp simulator","free guitar rig","open-source amp simulator","NAM A2 player","Neural Amp Modeler DAW","AmpliTube alternative","Guitar Rig alternative","Neural DSP alternative","free amp capture software","TONE3000 integration"],"seoDescription":"How OpenStudio’s free NAM guitar rig brings A1/A2 captures, native pedals, cabinet IRs, TONE3000 access, presets, and project recall into one open-source DAW.","seoTitle":"Building a Free NAM Guitar Rig Inside OpenStudio | OpenStudio Blog","date":"2026-07-26","dateLabel":"July 26, 2026","dateModified":"2026-08-31","dateModifiedLabel":"August 31, 2026"},{"author":"OpenStudio engineering team","slug":"ace-step-diffusers-almost-3x-faster","title":"ACE-Step in OpenStudio Is Almost 3x Faster Now","dek":"ACE-Step generation in OpenStudio now runs almost 3x faster by moving onto Hugging Face Diffusers, using the native ACE-Step pipeline, and treating AI music generation like something that belongs inside the DAW...","summary":"ACE-Step generation in OpenStudio now runs almost 3x faster by moving onto Hugging Face Diffusers, using the native ACE-Step pipeline, and treating AI music generation like something...","sourcePath":"blogs/2026-06-04-ace-step-diffusers-almost-3x-faster.md","filename":"2026-06-04-ace-step-diffusers-almost-3x-faster.md","wordCount":1896,"readTimeMinutes":9,"image":"/assets/blogs/ace-step-diffusers-almost-3x-faster.webp?v=9bb2edb7ffce","imageAlt":"ACE-Step in OpenStudio Is Almost 3x Faster Now social share image","seoDescription":"ACE-Step generation in OpenStudio now runs almost 3x faster by moving onto Hugging Face Diffusers, using the native ACE-Step pipeline, and treating AI music...","seoTitle":"ACE-Step in OpenStudio Is Almost 3x Faster Now | OpenStudio Blog","date":"2026-06-04","dateLabel":"June 4, 2026","dateModified":"2026-06-04","dateModifiedLabel":"June 4, 2026"},{"author":"OpenStudio engineering team","slug":"building-the-midi-editor-harness","title":"The Harness That Rewarded Me With a Real MIDI Editor","dek":"The MIDI editor did not become trustworthy when the feature list was complete. It became trustworthy when a harness could use it like an impatient musician and prove every edit survived undo, redo, playback, save,...","summary":"The MIDI editor did not become trustworthy when the feature list was complete. It became trustworthy when a harness could use it like an impatient musician and prove every edit survived...","sourcePath":"blogs/2026-05-15-building-the-midi-editor-harness.md","filename":"2026-05-15-building-the-midi-editor-harness.md","wordCount":1555,"readTimeMinutes":7,"image":"/assets/blogs/building-the-midi-editor-harness.webp?v=d9aebc44146b","imageAlt":"The Harness That Rewarded Me With a Real MIDI Editor social share image","seoDescription":"The MIDI editor did not become trustworthy when the feature list was complete. It became trustworthy when a harness could use it like an impatient musician...","seoTitle":"The Harness That Rewarded Me With a Real MIDI Editor","date":"2026-05-15","dateLabel":"May 15, 2026","dateModified":"2026-05-15","dateModifiedLabel":"May 15, 2026"},{"author":"OpenStudio engineering team","slug":"ara2-hosting-challenges-blog","title":"The Pain of Hosting ARA2 Plugins in a Custom DAW","dek":"ARA2 hosting looked like a protocol integration problem. It turned into renderer warmup, channel negotiation, transport timing, keyboard routing, and a long lesson in testing the exact workflow that breaks.","summary":"ARA2 hosting looked like a protocol integration problem. It turned into renderer warmup, channel negotiation, transport timing, keyboard routing, and a long lesson in testing the exact...","sourcePath":"blogs/ara2-hosting-challenges-blog.md","filename":"ara2-hosting-challenges-blog.md","wordCount":1718,"readTimeMinutes":8,"image":"/assets/blogs/ara2-hosting-challenges-blog.webp?v=0de1365f6f18","imageAlt":"The Pain of Hosting ARA2 Plugins in a Custom DAW social share image","seoDescription":"ARA2 hosting looked like a protocol integration problem. It turned into renderer warmup, channel negotiation, transport timing, keyboard routing, and a long...","seoTitle":"The Pain of Hosting ARA2 Plugins in a Custom DAW"},{"author":"OpenStudio engineering team","slug":"ace-step-integration-challenges","title":"What It Took to Bring ACE-Step Music Generation Into OpenStudio","dek":"ACE-Step looked like a simple music-generation feature from the outside. The real work was matching the reference runtime closely enough that OpenStudio could generate coherent songs inside the DAW.","summary":"ACE-Step looked like a simple music-generation feature from the outside. The real work was matching the reference runtime closely enough that OpenStudio could generate coherent songs...","sourcePath":"blogs/ace-step-integration-challenges.md","filename":"ace-step-integration-challenges.md","wordCount":1854,"readTimeMinutes":9,"image":"/assets/blogs/ace-step-integration-challenges.webp?v=e8abb27b9f02","imageAlt":"What It Took to Bring ACE-Step Music Generation Into OpenStudio social share image","seoDescription":"ACE-Step looked like a simple music-generation feature from the outside. The real work was matching the reference runtime closely enough that OpenStudio...","seoTitle":"What It Took to Bring ACE-Step Music Generation Into OpenStudio"}] as const; +export const generatedBlogPosts = [{"author":"OpenStudio team","slug":"minimax-stable-audio-diffusers-openstudio","title":"MiniMax, Stable Audio and ACE-Step: three ways into a song","dek":"MiniMax Music 3 and Stable Audio 3 Medium join ACE-Step in OpenStudio, with local generation, guided setup and more ways to work with the audio already in your session.","summary":"MiniMax Music 3 and Stable Audio 3 Medium join ACE-Step in OpenStudio, with local generation, guided setup and more ways to work with the audio already in your session.","sourcePath":"blogs/2026-09-16-minimax-stable-audio-diffusers-openstudio.md","filename":"2026-09-16-minimax-stable-audio-diffusers-openstudio.md","wordCount":2010,"readTimeMinutes":9,"image":"/assets/blogs/minimax-stable-audio-diffusers-openstudio.webp?v=c1943e68e130","imageAlt":"A sunlit home studio with a MIDI keyboard, headphones and an open songwriting notebook on a wooden desk.","keywords":["OpenStudio AI music","MiniMax Music 3","Stable Audio 3 Medium","ACE-Step 1.5 XL Turbo","Diffusers audio generation","local music generation","AI audio inpainting"],"seoDescription":"Explore MiniMax Music 3, Stable Audio 3 Medium and ACE-Step in OpenStudio: local Diffusers generation, clip editing, guided setup and memory tradeoffs.","seoTitle":"MiniMax Music 3 & Stable Audio 3 in OpenStudio | Blog","date":"2026-09-16","dateLabel":"September 16, 2026","dateModified":"2026-09-16","dateModifiedLabel":"September 16, 2026"},{"author":"OpenStudio team","slug":"build-guitar-tones-with-openstudio-nam-rack","title":"Build Better Guitar Tones with OpenStudio's NAM Rack","dek":"A guitarist-first guide to building tones in OpenStudio, with a repeatable workflow, exact settings from two real presets, and practical ideas from Rabea Massaad, Adam “Nolly” Getgood, and Misha Mansoor.","summary":"A guitarist-first guide to building tones in OpenStudio, with a repeatable workflow, exact settings from two real presets, and practical ideas from Rabea Massaad, Adam “Nolly” Getgood,...","sourcePath":"blogs/2026-08-28-build-guitar-tones-with-openstudio-nam-rack.md","filename":"2026-08-28-build-guitar-tones-with-openstudio-nam-rack.md","wordCount":2825,"readTimeMinutes":13,"image":"/assets/blogs/build-guitar-tones-with-openstudio-nam-rack-v2.webp?v=e7b476333487","imageAlt":"OpenStudio NAM Rack framed by blue clean-tone and red high-gain waveform treatments.","imageFit":"contain","keywords":["OpenStudio NAM Rack tutorial","NAM guitar tone","Neural Amp Modeler preset","clean guitar tone settings","high gain guitar tone settings","5150 NAM tone","free guitar amp simulator","Precision Drive settings","guitar cabinet IR","TONE3000"],"seoDescription":"A practical guitarist's guide to OpenStudio NAM Rack, with exact clean and high-gain preset settings plus tone advice from Rabea Massaad, Nolly Getgood, and Misha Mansoor.","seoTitle":"How to Build Clean and High-Gain Tones with OpenStudio NAM Rack","date":"2026-08-28","dateLabel":"August 28, 2026","dateModified":"2026-08-29","dateModifiedLabel":"August 29, 2026"},{"author":"OpenStudio engineering team","slug":"building-openstudio-nam-rack","title":"Building a Free NAM Guitar Rig Inside OpenStudio","dek":"OpenStudio now includes a redesigned, complete NAM guitar rig inside the DAW: A1 and A2 captures, native pre-effects, optional Pedal NAM, cabinet shaping, Cabinet Space, studio effects, TONE3000 access, tuning,...","summary":"OpenStudio now includes a redesigned, complete NAM guitar rig inside the DAW: A1 and A2 captures, native pre-effects, optional Pedal NAM, cabinet shaping, Cabinet Space, studio effects,...","sourcePath":"blogs/2026-07-26-building-openstudio-nam-rack.md","filename":"2026-07-26-building-openstudio-nam-rack.md","wordCount":1987,"readTimeMinutes":9,"image":"/assets/blogs/building-openstudio-nam-rack-free-guitar-rig.webp?v=8f6e918d26dd","imageAlt":"OpenStudio NAM Rack amp, pedals, post effects, and TONE3000 capture browser arranged in a music studio.","imageFit":"contain","keywords":["free guitar amp simulator","free guitar rig","open-source amp simulator","NAM A2 player","Neural Amp Modeler DAW","AmpliTube alternative","Guitar Rig alternative","Neural DSP alternative","free amp capture software","TONE3000 integration"],"seoDescription":"How OpenStudio’s free NAM guitar rig brings A1/A2 captures, native pedals, cabinet IRs, TONE3000 access, presets, and project recall into one open-source DAW.","seoTitle":"Building a Free NAM Guitar Rig Inside OpenStudio | OpenStudio Blog","date":"2026-07-26","dateLabel":"July 26, 2026","dateModified":"2026-08-31","dateModifiedLabel":"August 31, 2026"},{"author":"OpenStudio engineering team","slug":"ace-step-diffusers-almost-3x-faster","title":"ACE-Step in OpenStudio Is Almost 3x Faster Now","dek":"ACE-Step generation in OpenStudio now runs almost 3x faster by moving onto Hugging Face Diffusers, using the native ACE-Step pipeline, and treating AI music generation like something that belongs inside the DAW...","summary":"ACE-Step generation in OpenStudio now runs almost 3x faster by moving onto Hugging Face Diffusers, using the native ACE-Step pipeline, and treating AI music generation like something...","sourcePath":"blogs/2026-06-04-ace-step-diffusers-almost-3x-faster.md","filename":"2026-06-04-ace-step-diffusers-almost-3x-faster.md","wordCount":1896,"readTimeMinutes":9,"image":"/assets/blogs/ace-step-diffusers-almost-3x-faster.webp?v=9bb2edb7ffce","imageAlt":"ACE-Step in OpenStudio Is Almost 3x Faster Now social share image","seoDescription":"ACE-Step generation in OpenStudio now runs almost 3x faster by moving onto Hugging Face Diffusers, using the native ACE-Step pipeline, and treating AI music...","seoTitle":"ACE-Step in OpenStudio Is Almost 3x Faster Now | OpenStudio Blog","date":"2026-06-04","dateLabel":"June 4, 2026","dateModified":"2026-06-04","dateModifiedLabel":"June 4, 2026"},{"author":"OpenStudio engineering team","slug":"building-the-midi-editor-harness","title":"The Harness That Rewarded Me With a Real MIDI Editor","dek":"The MIDI editor did not become trustworthy when the feature list was complete. It became trustworthy when a harness could use it like an impatient musician and prove every edit survived undo, redo, playback, save,...","summary":"The MIDI editor did not become trustworthy when the feature list was complete. It became trustworthy when a harness could use it like an impatient musician and prove every edit survived...","sourcePath":"blogs/2026-05-15-building-the-midi-editor-harness.md","filename":"2026-05-15-building-the-midi-editor-harness.md","wordCount":1555,"readTimeMinutes":7,"image":"/assets/blogs/building-the-midi-editor-harness.webp?v=d9aebc44146b","imageAlt":"The Harness That Rewarded Me With a Real MIDI Editor social share image","seoDescription":"The MIDI editor did not become trustworthy when the feature list was complete. It became trustworthy when a harness could use it like an impatient musician...","seoTitle":"The Harness That Rewarded Me With a Real MIDI Editor","date":"2026-05-15","dateLabel":"May 15, 2026","dateModified":"2026-05-15","dateModifiedLabel":"May 15, 2026"},{"author":"OpenStudio engineering team","slug":"ara2-hosting-challenges-blog","title":"The Pain of Hosting ARA2 Plugins in a Custom DAW","dek":"ARA2 hosting looked like a protocol integration problem. It turned into renderer warmup, channel negotiation, transport timing, keyboard routing, and a long lesson in testing the exact workflow that breaks.","summary":"ARA2 hosting looked like a protocol integration problem. It turned into renderer warmup, channel negotiation, transport timing, keyboard routing, and a long lesson in testing the exact...","sourcePath":"blogs/ara2-hosting-challenges-blog.md","filename":"ara2-hosting-challenges-blog.md","wordCount":1718,"readTimeMinutes":8,"image":"/assets/blogs/ara2-hosting-challenges-blog.webp?v=0de1365f6f18","imageAlt":"The Pain of Hosting ARA2 Plugins in a Custom DAW social share image","seoDescription":"ARA2 hosting looked like a protocol integration problem. It turned into renderer warmup, channel negotiation, transport timing, keyboard routing, and a long...","seoTitle":"The Pain of Hosting ARA2 Plugins in a Custom DAW"},{"author":"OpenStudio engineering team","slug":"ace-step-integration-challenges","title":"What It Took to Bring ACE-Step Music Generation Into OpenStudio","dek":"ACE-Step looked like a simple music-generation feature from the outside. The real work was matching the reference runtime closely enough that OpenStudio could generate coherent songs inside the DAW.","summary":"ACE-Step looked like a simple music-generation feature from the outside. The real work was matching the reference runtime closely enough that OpenStudio could generate coherent songs...","sourcePath":"blogs/ace-step-integration-challenges.md","filename":"ace-step-integration-challenges.md","wordCount":1854,"readTimeMinutes":9,"image":"/assets/blogs/ace-step-integration-challenges.webp?v=e8abb27b9f02","imageAlt":"What It Took to Bring ACE-Step Music Generation Into OpenStudio social share image","seoDescription":"ACE-Step looked like a simple music-generation feature from the outside. The real work was matching the reference runtime closely enough that OpenStudio...","seoTitle":"What It Took to Bring ACE-Step Music Generation Into OpenStudio"}] as const; diff --git a/src/features/docs/content/ai-runtime-setup.ts b/src/features/docs/content/ai-runtime-setup.ts index 9ba25e2..79662ad 100644 --- a/src/features/docs/content/ai-runtime-setup.ts +++ b/src/features/docs/content/ai-runtime-setup.ts @@ -5,13 +5,33 @@ import type { DocContent } from "../types"; const doc: DocContent = { updated: "2026-09-16", - appReference: { commit: "7f59cff", channel: "development" }, + appReference: { commit: "681fec8", channel: "development" }, blocks: [ { type: "p", text: `Generation, stem separation, and the larger analysis models are optional in OpenStudio. They are never bundled with the base download and never block launch. This page covers what the **AI Tools** install actually puts on your disk, what each platform can run, and how to start each workflow. The rules it follows are the upstream [runtime dependency contract](${REPO.runtimeContractDoc}) and manual section 18. Shortcuts shown are the OpenStudio default keyboard profile.`, }, + { type: "h2", id: "model-versions", text: "Original and INT8 model versions" }, + { + type: "p", + text: "In development builds containing app commit `681fec8`, choose a model, then **Original** or **INT8** in **Model version**. A suggestion above the selector explains when lower memory use may help. **AI Tools Setup** lists separate Original and INT8 installation status for ACE-Step, Stable Audio 3 Medium, and MiniMax Music 3. These controls may not be present in an older installed release.", + }, + { + type: "p", + text: "INT8 currently requires an **NVIDIA CUDA GPU**. **Download and Prepare INT8** reuses installed original weights or downloads the official originals, then prepares and verifies a separate INT8 copy. The first download is not smaller. Prepared OpenStudio INT8 folders can also be imported. Choose the installed version in the generation dialog; the AI track remembers it and supports undo/redo. Original and INT8 can coexist, and the prepared INT8 version can run offline without keeping the Original version installed.", + }, + { + type: "p", + text: "ACE-Step and Stable Audio quantize the diffusion transformer; MiniMax quantizes its language model. Other audio components retain their normal inference precision. Less memory use does not guarantee faster rendering or identical sound. Other GPU backends and CPU-only systems should select Original, subject to that model's hardware requirements.", + }, + { + type: "callout", + tone: "note", + label: "MiniMax INT8 still needs a capable machine", + text: "The app's [local qualification report](https://github.com/sdevil7th/OpenStudio/blob/681fec8/docs/ai-quantization-2026-09-16.md) records generation on an RTX 4080 with 16 GB VRAM and 32 GB system RAM. It does not establish support for every smaller GPU or equivalent audio quality. MiniMax can take minutes per song; its INT8 path also uses about 15 GiB of temporary disk cache for inactive stages and takes longer on first load. Run the app's **Hardware check** for your selected model version and request.", + }, + { type: "h2", id: "what-is-optional", text: "What is optional, and why" }, { type: "p",