P2b: offline WAV render of the arrangement

This commit is contained in:
Emil
2026-11-01 02:19:25 +03:00
parent c85e3cc723
commit 0df07577a1
17 changed files with 345 additions and 10 deletions
+1
View File
@@ -14,6 +14,7 @@
"record-smoke": "node scripts/record-smoke.mjs",
"faces-smoke": "node scripts/faces-smoke.mjs",
"arrangement-smoke": "node scripts/arrangement-smoke.mjs",
"render-ui-smoke": "node scripts/render-ui-smoke.mjs",
"restored-smoke": "node scripts/restored-smoke.mjs"
},
"devDependencies": {
+30
View File
@@ -0,0 +1,30 @@
import { chromium } from 'playwright-core';
const chrome = process.env.CHROME_PATH ?? '/home/emil/.cache/ms-playwright/chromium-1248/chrome-linux64/chrome';
const browser = await chromium.launch({ executablePath: chrome, args: ['--no-sandbox'] });
const page = await browser.newPage({ viewport: { width: 1600, height: 1000 }, deviceScaleFactor: 1 });
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
page.on('console', (message) => { if (message.type() === 'error') errors.push(message.text()); });
await page.goto('http://127.0.0.1:5173/', { waitUntil: 'load' });
await page.waitForTimeout(2200);
await page.locator('.render-button').click();
await page.waitForFunction(
() => [...document.querySelectorAll('.wire-label')].some((el) => el.textContent?.trim() === 'render'),
null,
{ timeout: 30000 }
);
await page.waitForTimeout(500);
await page.screenshot({ path: '../preview-p2-render.png' });
const report = await page.evaluate(() => ({
wire: [...document.querySelectorAll('.wire-entry')].slice(-4).map((el) => el.textContent?.replace(/\s+/g, ' ').trim()),
title: document.querySelector('.render-button')?.getAttribute('title'),
button: document.querySelector('.render-button')?.textContent?.trim()
}));
console.log(JSON.stringify({ report, errors }, null, 2));
const failures = [];
if (!report.title?.startsWith('last render:')) failures.push('render path is not on the button');
if (!report.wire.some((line) => line?.includes('16.5 s'))) failures.push('render event is not on the wire');
if (errors.length) failures.push('console errors');
await browser.close();
if (failures.length) { console.error('RENDER UI SMOKE FAILED: ' + failures.join('; ')); process.exit(1); }
console.log('render UI smoke passed');
+21
View File
@@ -38,6 +38,8 @@
let selectedFace: number = $state(-1);
let selectedRow: number = $state(0);
let importing: boolean = $state(false);
let rendering: boolean = $state(false);
let lastRender: string | null = $state(null);
let bridge: Bridge | null = null;
let entryId = 0;
let snapshotLogged = false;
@@ -109,6 +111,9 @@
project.arrangement = event.sections;
project.active_section = event.active;
project.pattern = event.sections[event.active] ?? project.pattern;
} else if (event.kind === 'rendered') {
lastRender = event.path;
addEntry('recv', 'render', `${event.duration_sec.toFixed(1)} s · ${event.path}`);
}
}
@@ -216,6 +221,16 @@
void run({ type: 'copy_section', from, to });
}
async function renderProject() {
if (!bridge || rendering) return;
rendering = true;
try {
await run({ type: 'render' });
} finally {
rendering = false;
}
}
function undo() {
if (project?.can_undo) void run({ type: 'undo' });
}
@@ -322,6 +337,12 @@
<div class="history" aria-label="history">
<button class="history-button" onclick={undo} disabled={!project?.can_undo} aria-label="undo" title="undo · ctrl+z">undo</button>
<button class="history-button" onclick={redo} disabled={!project?.can_redo} aria-label="redo" title="redo · shift+ctrl+z">redo</button>
<button
class="history-button render-button"
onclick={renderProject}
disabled={rendering || status !== 'live'}
title={lastRender ? `last render: ${lastRender}` : 'render the arrangement to WAV'}
>{rendering ? 'rendering…' : 'render'}</button>
</div>
<div class="net" class:is-live={status === 'live'}>
<span class="dot" aria-hidden="true"></span>
+1
View File
@@ -37,6 +37,7 @@ export function commandLabel(command: Command): string {
case 'select_section': return `section ${String.fromCharCode(65 + command.index)}`;
case 'clear_section': return `clear section ${String.fromCharCode(65 + command.index)}`;
case 'copy_section': return `copy ${String.fromCharCode(65 + command.from)} → ${String.fromCharCode(65 + command.to)}`;
case 'render': return 'render wav';
case 'undo': return 'undo';
case 'redo': return 'redo';
case 'get_snapshot': return 'snapshot';
+3 -1
View File
@@ -84,7 +84,8 @@ export type Event =
| { kind: 'face_struck'; core: string; face: number; velocity: number }
| { kind: 'history'; can_undo: boolean; can_redo: boolean }
| { kind: 'core_faces'; core: CoreSummary }
| { kind: 'arrangement'; sections: Pattern[]; active: number };
| { kind: 'arrangement'; sections: Pattern[]; active: number }
| { kind: 'rendered'; path: string; duration_sec: number };
export type Command =
| { type: 'get_snapshot' }
@@ -100,6 +101,7 @@ export type Command =
| { type: 'select_section'; index: number }
| { type: 'clear_section'; index: number }
| { type: 'copy_section'; from: number; to: number }
| { type: 'render' }
| { type: 'undo' }
| { type: 'redo' };