-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild-session.ts
More file actions
136 lines (119 loc) · 4.34 KB
/
Copy pathbuild-session.ts
File metadata and controls
136 lines (119 loc) · 4.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/**
* Generates server/osc/session.json — the Open Stage Control UI.
*
* Output is wrapped in O-S-C's session envelope ({ session: <root>, version, type }).
* Speaker markers are derived from CONFIG.speakers; source slots from SOURCES below.
* Run with `npm run build:session`.
*/
import { copyFileSync, writeFileSync } from "node:fs";
import { CONFIG } from "../config";
import { button, disabledButton, fader, image, speaker, xy, type Root, type Session, type Widget } from "./widgets";
const STAGE = { top: 70, left: 40, size: 520 };
const COL0 = 600;
const COL1 = 720;
const BTN_W = 110;
const ROW_H = 80;
/** Open Stage Control version this session targets (silences the load-time warning). */
const OSC_VERSION = "1.30.3";
/** Floorplan image — copied next to session.json so O-S-C can serve it by relative path. */
const FLOORPLAN_SRC = "src/floorplan.png";
const FLOORPLAN_NAME = "floorplan.png";
/**
* Source-select slots shown on the control surface (UI only — Max owns the real source
* config). `disabled` slots render as greyed, non-interactive placeholders.
*/
type SourceSlot = { i: number; label: string; disabled?: boolean };
const SOURCES: SourceSlot[] = [
{ i: 1, label: "Sonos" },
{ i: 2, label: "Source 2", disabled: true },
];
/** Speaker marker geometry on the stage pad. */
const SPK_SIZE = 38;
const SPK_RADIUS = 232; // distance from pad centre to a marker centre (px)
/** Place speaker discs around the pad from their SPAT5 azimuths (deg, 0 = front/up, CCW). */
function speakerMarkers(): Widget[] {
const cx = STAGE.left + STAGE.size / 2;
const cy = STAGE.top + STAGE.size / 2;
return CONFIG.speakers.map((azDeg, i) => {
const az = (azDeg * Math.PI) / 180;
// Pad convention (see module xyToAzDist): x = -sin(az), y = cos(az); screen y grows down.
const x = -Math.sin(az);
const y = Math.cos(az);
return speaker({
id: `spk${i + 1}`,
left: Math.round(cx + x * SPK_RADIUS - SPK_SIZE / 2),
top: Math.round(cy - y * SPK_RADIUS - SPK_SIZE / 2),
size: SPK_SIZE,
text: String(i + 1),
});
});
}
function buildSession(): Root {
const widgets: Widget[] = [];
// Floorplan, drawn behind the stage pad.
widgets.push(
image(
{ id: "floorplan", top: STAGE.top, left: STAGE.left, width: STAGE.size, height: STAGE.size },
FLOORPLAN_NAME,
),
);
// The stage: drag to move the selected source. Transparent so the floorplan shows through.
widgets.push({
...xy({
id: "stage",
top: STAGE.top,
left: STAGE.left,
width: STAGE.size,
height: STAGE.size,
label: "STAGE (drag = move selected source)",
address: "/stage",
}),
css: "background: transparent;",
});
// Speaker positions, drawn on top of the floorplan.
widgets.push(...speakerMarkers());
// Source-select buttons in a 2-column grid. The actual source/renderer setup lives in
// Max; this is just the control surface. For now we expose Sonos (source 1) plus a single
// greyed-out placeholder where future sources will go.
SOURCES.forEach((s, idx) => {
const row = Math.floor(idx / 2);
const col = idx % 2;
const top = STAGE.top + row * ROW_H;
const left = col === 0 ? COL0 : COL1;
if (s.disabled) {
widgets.push(
disabledButton({ id: `s${s.i}`, top, left, width: BTN_W, height: 70, text: s.label }),
);
} else {
widgets.push(
button({ id: `s${s.i}`, top, left, width: BTN_W, height: 70, label: s.label, address: `/select/${s.i}` }),
);
}
});
// Level fader for the selected source = SPAT5 "presence" (0..120, default 90).
widgets.push(
fader(
{ id: "presence", top: STAGE.top, left: 980, width: 100, height: STAGE.size, label: "Level (presence)", address: "/presence" },
0,
120,
90,
),
);
return {
type: "root",
id: "root",
width: 1120,
height: STAGE.top + STAGE.size + 30,
widgets,
};
}
// O-S-C (v1.30) expects the root nested under `content`, with the version alongside.
const session: Session = {
version: OSC_VERSION,
content: buildSession(),
};
const outDir = "server/osc";
const out = `${outDir}/session.json`;
copyFileSync(FLOORPLAN_SRC, `${outDir}/${FLOORPLAN_NAME}`);
writeFileSync(out, JSON.stringify(session, null, 2) + "\n");
console.log(`wrote ${out} (${session.content.widgets.length} widgets, ${SOURCES.length} source slots)`);