Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions web/scripts/deck-pack-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*
* The PERFORMANCE decoration runs on every download of a deck that has one,
* and it rewrites the archive — so a mistake there breaks the pack itself,
* not just the performance. It must add both forms, leave the modules alone,
* not just the performance. It must add the table, leave the modules alone,
* and refuse to fail loudly: a broken attachment may cost its own file and
* nothing else.
*/
Expand Down Expand Up @@ -146,13 +146,10 @@ const names = Object.keys(decorated).sort();

check("the modules survive untouched", names.includes("wave_saw.pfm"), true);
check("so does the running order", names.includes("catalog.txt"), true);
check("the editable source is added", names.includes("performance.json"), true);
check("the packed table is added, named from the id", names.includes("sunset_set.pfs"), true);
check(
"performance.json is stored verbatim",
new TextDecoder().decode(decorated["performance.json"]),
performanceJson,
);
// The Director opens and saves .pfs, so the table IS the document — a JSON
// beside it would be a file nothing opens.
check("no JSON rides along", names.includes("performance.json"), false);

// The .pfs is what the panel plays, so it has to be a table, not bytes that
// merely exist — decode it back and check the timeline survived the trip.
Expand Down
6 changes: 3 additions & 3 deletions web/src/app/api/community/decks/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,9 @@ async function handlePatch(request: Request, context: { params: Promise<{ id: st

// Optional attached performance (Director timeline). null detaches; a
// string is validated against the same limits the device's .pfs player
// enforces, and stored in canonical form so the pack's performance.json
// is stable across re-serializations. Serve-time zip decoration means no
// rebuild is queued either way.
// enforces, and stored canonically so the table the pack ships is stable
// across re-serializations. Serve-time zip decoration means no rebuild is
// queued either way.
let performanceJson: string | null | undefined;
if (raw.performanceJson !== undefined) {
if (raw.performanceJson === null || raw.performanceJson === "") {
Expand Down
18 changes: 9 additions & 9 deletions web/src/app/community/d/[id]/DeckDetailClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ export default function DeckDetailClient({
<button
type="button"
className={styles.btn}
title="Attach a Director performance JSON — it ships inside the pack as performance.json + .pfs"
title="Attach a Director performance — the .pfs table ships inside this deck's pack and installs with it"
onClick={() => setPerfOpen(true)}
>
{deck.performanceJson ? "Performance…" : "Attach performance"}
Expand Down Expand Up @@ -545,11 +545,10 @@ export default function DeckDetailClient({
);
}

// A Director performance rides the deck's pack: performance.json (editable
// source) + the packed .pfs the device's Sequences page plays. Author it in
// the Director PWA (Save JSON), paste or drop the file here. The server
// re-validates against the device's limits, so a save that succeeds is one
// the panel will actually load.
// A Director performance rides the deck's pack as the .pfs table — the file
// the Director opens and saves, and the file the panel's Sequences page
// plays. Author it there, drop it here. The server re-validates against the
// device's limits, so a save that succeeds is one the panel will load.
function PerformanceModal({
deckId,
initialJson,
Expand Down Expand Up @@ -610,15 +609,16 @@ function PerformanceModal({
</div>
<div className={styles.modalBody}>
<label className={styles.field}>
<span className={styles.fieldLabel}>Load a .pfs or .json</span>
<span className={styles.fieldLabel}>Load a .pfs table</span>
<input
type="file"
accept=".pfs,.json,application/json,application/octet-stream"
onChange={(event) => pickFile(event.target.files?.[0])}
/>
<span className={styles.fieldHint}>
Either half of a Director save — a <code>.pfs</code> table is unpacked back into
the editable timeline below.
Unpacked into the timeline below so you can see what you are attaching. It
ships in this deck&apos;s pack and installs with it. Older <code>.json</code>{" "}
exports still load.
</span>
</label>
<label className={styles.field}>
Expand Down
6 changes: 3 additions & 3 deletions web/src/app/community/p/[id]/PatternDetailClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -906,16 +906,16 @@ function PerformancesSection({
href={communityApiUrl(
`/api/community/performances/${recording.id}?format=pfs`,
)}
title="Show table for the panel — drop it on the device's Sequences page"
title="The performance itself — open it in the Director to keep working on the ride, or drop it on the panel's Sequences page to play it"
>
.pfs
</a>
<a
className={`${styles.btnSmall} ${styles.perfLink}`}
href={communityApiUrl(`/api/community/performances/${recording.id}`)}
title="The editable Director JSON — open it to keep working on the ride"
title="The same cues as JSON — for reading or scripting, not for the Director"
>
.json
raw
</a>
{isOwner && !recording.byAuthor && !pattern.hasAuthorPerformance && (
<button
Expand Down
18 changes: 10 additions & 8 deletions web/src/components/community/AddPerformanceModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ export default function AddPerformanceModal({
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);

// Either half of a Director save: the .json it edits, or the .pfs it sends
// to a panel. A packed table is decoded back to the editable form here, so
// what gets published is always the source and the .pfs is regenerated.
// The Director saves .pfs and the panel plays it, so that is what people
// have. It is decoded here into the canonical form the site stores, which
// is also what regenerates the table on the way back out. JSON still loads
// because Directors from before the format settled saved it.
const pickFile = (file: File | undefined) => {
if (!file) return;
setError(null);
Expand Down Expand Up @@ -94,20 +95,21 @@ export default function AddPerformanceModal({
<p className={styles.formNote}>
A performance is a timed ride through this pattern&apos;s knobs — cues of absolute
values (0..1000) the panel replays exactly. Record it in the Director tool and
load either half of its save: the <code>.pfs</code> table you send to a panel, or
the <code>.json</code> it edits. It goes live immediately, credited to you; the
pattern&apos;s author can pin or out-rank it with their own recording.
load the <code>.pfs</code> it saves; that same file is what a panel plays. It
goes live immediately, credited to you; the pattern&apos;s author can pin or
out-rank it with their own recording.
</p>

<label className={styles.field}>
<span className={styles.fieldLabel}>Load a .pfs or .json</span>
<span className={styles.fieldLabel}>Load a .pfs table</span>
<input
type="file"
accept=".pfs,.json,application/json,application/octet-stream"
onChange={(event) => pickFile(event.target.files?.[0])}
/>
<span className={styles.fieldHint}>
A <code>.pfs</code> is unpacked back into the editable timeline below.
Unpacked into the timeline below so you can see what you are publishing.
Older <code>.json</code> exports still load.
</span>
</label>

Expand Down
11 changes: 7 additions & 4 deletions web/src/lib/community/deckZip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,13 @@ export async function invalidateDeckZip(deckId: string): Promise<void> {
}

/**
* Add a deck's attached performance to its pack, in the two forms the two
* audiences need: `performance.json` so the recording can be re-opened in the
* Director, and the encoded `.pfs` so the panel can play it.
* Add a deck's attached performance to its pack, as the `.pfs` table.
*
* One file, because upstream made `.pfs` the whole document: the Director
* opens and saves it, and the panel plays it. An earlier cut also shipped a
* `performance.json` beside it as "the editable source" — that was true when
* the Director edited JSON and stopped being true when it did not, and a file
* nothing opens is just weight in someone's download.
*
* Done at serve time rather than baked into the build artifact, so attaching
* or editing a performance updates downloads immediately and never queues a
Expand All @@ -172,7 +176,6 @@ export function decoratePackWithPerformance(
try {
const perf = normalizePerformance(JSON.parse(performanceJson));
const entries = unzipSync(pack);
entries["performance.json"] = new TextEncoder().encode(performanceJson);
entries[pfsFilename(perf)] = encodePfst(perf);
// Fixed timestamp, same reasoning as make_pack.py: identical inputs must
// produce identical bytes across requests.
Expand Down
37 changes: 22 additions & 15 deletions web/src/lib/community/performance.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
// Performance JSON (proposal §7.1) + the packed PFST v1 show table.
//
// A deck may carry one performance: a timed cue list (pattern / four
// absolute params 0..1000 / banner) authored in the Director PWA. The JSON
// is the editable source and travels in the pack for re-editing; the device
// itself plays only the packed little-endian .pfs table — it has no JSON
// parser on purpose. This module is the server-side twin of the Director's
// performance.js + show-table.js (Simone Majocchi's performance-director
// branch): same normalization, same byte layout, so a .pfs encoded here is
// indistinguishable from one saved out of the Director.
// A performance is a timed cue list (pattern / four absolute params 0..1000 /
// banner) authored in the Director PWA. **The `.pfs` table is the document**:
// the Director opens and saves it, the panel plays it, and it is what the site
// hands anyone who wants to edit or install one. It carries everything a
// recording is, sparse param patches included, because the cue flags record
// which channels each cue set.
//
// The JSON here is the site's own canonical form — what the database stores,
// what summaries and validation read, and what regenerates the table on the
// way out. It is not a second document to keep in step with the first; it has
// no life outside this server. (It was briefly the editable source, back when
// the Director edited JSON.)
//
// Byte layout matches the Director's show-table.js exactly — verified against
// all four of its demo tables in both directions — so a table encoded here is
// indistinguishable from one it saved.
//
// Layout (device: firmware/patternflow/src/core_show.h):
// header 76 bytes
Expand Down Expand Up @@ -234,13 +242,12 @@ export function validatePerformance(
/**
* Read a PFST table back into a performance.
*
* The Director saves both halves — the JSON it edits and the .pfs it sends to
* a panel — and people reach for whichever is in front of them, so publishing
* accepts either. The table is close to lossless for what a recording IS: the
* cue flags record exactly which param channels a cue set, so a sparse patch
* survives the round trip. What it does not carry is show-management dressing
* (utcStart, channel, patternsZip and its hash, the required list); those come
* back empty, which is what they are for a recording published here anyway.
* This is the import path, since `.pfs` is what the Director saves and what
* people therefore have. It is lossless for what a recording IS: the cue flags
* record exactly which param channels a cue set, so even a sparse patch
* survives. What it does not carry is show-management dressing (utcStart,
* channel, patternsZip and its hash, the required list); those come back
* empty, which is what they are for a recording published here anyway.
*
* Round-tripped in performance-smoke against the Director's own saves:
* decode → encode reproduces the original bytes.
Expand Down
10 changes: 5 additions & 5 deletions web/src/lib/community/performanceFile.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// Reading a performance out of whatever file the person has.
//
// The Director saves two halves of the same recording — the JSON it edits and
// the .pfs table it sends to a panel — and people reach for whichever is in
// front of them. Both land here and leave as canonical JSON, which is the one
// shape the API stores: it is the editable form, and the .pfs is regenerated
// from it on download, so nothing is lost by normalising on the way in.
// The Director saves `.pfs`, so that is what people have; `.json` still loads
// because Directors from before the format settled saved that instead. Either
// leaves here as the canonical JSON the API stores, and the table is
// regenerated from it on the way out — nothing is lost by normalising on the
// way in, since the table carries everything a recording is.

import { decodePfst, normalizePerformance, serializePerformance } from "./performance";

Expand Down
Loading