FOH dimmer calibration
Rendered from the working notes FOH-CALIBRATION.md on 2026-10-11.
FOH spot pair calibration (2026-10-10, live rig, camera-measured)
Answer
- The two units have different dimmer curves. At the same DMX, FOH SPOT 1 (201, "FRONT WASH 1") is dimmer than FOH SPOT 2 (202) below about DMX 110 (43 %). It is brighter above that. At full, 201 at DMX 210 gives the same light as 202 at 255. At DMX 32, 201 needs 40 to match 202. Confidence: high (repeats agree within about 1 camera luma, several hundred frames).
- A constant trim cannot fix this. Trimming 201 to 0.82 would match the top and make the low end worse. The fix is a per-fixture curve.
- The desk is not the cause. Both units receive identical DMX on every channel except pan and tilt. They share one profile with no curve or mode channel. Confidence: high.
- The cause is at the fixtures. It is most likely a different dimmer-curve setting in the unit menus, or LED or driver ageing. The equivalence fits a power law with exponent 0.8, which is what two different dimmer gammas produce. Confidence: moderate (inference). Someone at the units can compare their menus.
- 202 also throws a stray field of light onto the downstage-left floor, outside its main pool. 201 throws none. The field moves with the head, and focus does not change it. This is an optical fault in 202 (lens, reflector or a light leak) and cannot be fixed from the desk. Confidence: high that it exists and comes from 202. Confidence on the exact part: low.
- The earlier key-balance figure ("201 is 18 to 33 % dimmer at 204") was confounded. The camera auto-exposes, and 202's stray field inflated its lit-pixel statistics. At the same mark, at DMX 255, 201's pool reads about 12 % brighter than 202's.
- The calibration is applied in the v2 desk. It is in
data/dimmer-gain.json with an engine change on branch two-screen (commits c780c0a and 0ae4085, local, not pushed). It is not on the live desk, which would need a restart. The live looks did not need a data edit: they run the pair at 0 % (42 scenes) or 40 % (5 scenes), and at 40 % the two units already match within 1 %.
How it was measured
- Camera auto-exposure. The room camera (NDI "LIVEROOM", 1387x780) auto-exposes. The unlit background drops 19 % whenever a pool comes on, and it has not recovered 3 s later. Frames taken at different times cannot be compared directly.
- Swap null test. 201 lit spot L while 202 lit spot R (frame X). Then they swapped spots (frame Y). Both units are on in both frames, so exposure, spill and background are the same. Spot geometry cancels through the swap. Equal light makes X and Y identical, whatever the camera's response.
- The reading. Delta = (X minus Y on spot L + Y minus X on spot R) / 2, in camera luma, over eroded floor masks that exclude other fixtures' gobo pools. Positive means 201 is brighter.
- Bracketing. At each level, 202 held DMX L and 201 took three values around the expected match. The zero crossing was fitted, with 2 repeats and 4 s settle per frame.
- Aim. Spots L and R were solved from a measured pan/tilt response per unit. Each unit's pool was confirmed alone, and the L pools coincide within 7 px. At the normal baseline aim both pools already land on the same centre-stage spot, with centroids 12 px apart.
- Rejected frames. People walked through the stage during the runs. Pairs with a scene change outside the masks, or with L and R disagreeing by more than 4 luma, were rejected and are listed in the job logs.
Matching curve (202 DMX = the 201 DMX that gives the same light)
| 202 DMX |
201 DMX |
ratio |
fit sd (DMX) |
| 32 |
40 |
1.25 |
0.5 |
| 64 |
76 at fit; verification chose 73 |
1.14 |
1.2 |
| 96 |
100 at fit; verification chose 98 |
1.02 |
1.6 |
| 128 |
124 |
0.97 |
1.7 |
| 160 |
153 |
0.96 |
7.8 |
| 192 |
175 |
0.91 |
7.1 |
| 224 |
197 |
0.88 |
6.7 |
| 255 |
210 |
0.82 |
3.7 |
The applied curve is {"201": [[0,0],[32,40],[48,58],[64,73],[96,98],[128,124],[160,153],[192,175],[224,197],[255,210]]}.
Verification (swap test, the exact DMX the calibrated engine sends)
| asked |
before: 201 / 202 |
before delta |
after: 201 / 202 |
after delta |
after, in DMX |
| 12 % (32) |
32 / 32 |
201 −52 % |
40 / 32 |
+3.3 % |
+0.5 |
| 19 % (48) |
48 / 48 |
201 −19 % |
58 / 48 |
−1.1 % |
−0.7 |
| 25 % (64) |
64 / 64 |
201 −18 % |
73 / 64 |
−2.0 % |
−2.2 |
| 40 % (102) |
102 / 102 |
−0.9 % |
103 / 102 |
−1.1 % |
−1.4 |
| 50 % (128) |
128 / 128 |
+3.1 % |
124 / 128 |
+1.0 % |
+0.9 |
| 75 % (191) |
191 / 191 |
201 +9.6 % |
174 / 191 |
+1.5 % |
+3.3 |
| 100 % (255) |
255 / 255 |
201 +18.5 % |
210 / 255 |
+0.6 % |
+1.8 |
- What the percentages mean. Each is the delta as a share of 202's added pool luma in camera units. The figures are approximate because camera luma is not linear in light. The DMX column is response-independent.
- Result. Every calibrated level is within 3.3 % on camera and within 3.3 DMX of a perfect match. Before calibration the gap ran from −52 % to +18.5 %.
- Independent check. Both units were on their normal centre-stage aim, each alone, sequentially, with the pool added over dark. The results are from
frames/pics:
- At 255: 201 52.5, 202 47.1 (+11.5 %). After, 201 at 210 gives 47.7 (+1.3 %).
- At 128: 34.0 vs 33.2 (+2.4 %). After, 124 gives 33.1 (−0.3 %).
- At 64: 21.9 vs 24.8 (−11.7 %). After, 76 gives 25.5 (+2.8 %). The final curve sends 73, slightly lower.
202's stray field
| frame |
added luma, downstage-left floor (rows 600+, cols 0-300) |
| 201 alone, spot L |
0.4 |
| 202 alone, spot L |
15.9 |
| 202 alone, spot R |
4.7 |
| 202 at focus 0 / 100 / 180 / 255 |
6.7 / 6.4 / 6.1 / 5.9 |
| 201 at focus 0 / 100 / 180 / 255 |
−0.6 / −0.6 / −0.7 / −0.7 |
See halo-sheet.jpg and focus-sheet.jpg. The next step is a look at 202's front lens and head in person.
What changed, where
| layer |
changed? |
detail |
| Live desk (PC :7070) |
no |
Programmer values only during the tests. Every run restored universe 1 identically, 512/512 (logs *-job.log). |
Live data (scenes.json, aerial.json) |
no |
The looks use the pair at 0 % or 40 %, and at 40 % they already match within 1 %. A data-only scale cannot express a curve, and it would not reach the hand fader on group 22. |
v2 engine (Dmx/Engine.cs, branch two-screen) |
yes, c780c0a |
dimmer-gain.json now also takes a list of [asked, sent] DMX pairs per fixture, interpolated linearly. A number is still a constant gain. It applies in Render() to every source (looks, faders, programmer), so the group 22 fader is calibrated too. Raw Attribute "Dimmer" At Decimal8 values and parks bypass it. Built with dotnet build -c Release (0 errors), and the curve was unit-checked. |
v2 data (data/dimmer-gain.json) |
yes, c780c0a + 0ae4085 |
201 curve as above. |
- Going live. The live desk picks this up only from a build containing
c780c0a, with dimmer-gain.json in its data folder, and that needs a desk restart. The deploy-pc lane owns that step.
- Headroom cost. The pair's maximum becomes 202's maximum, because 201 is capped at DMX 210.
Roll back
- v2. Delete
data/dimmer-gain.json, which leaves the engine as before for every fixture. Alternatively git revert 0ae4085 c780c0a on two-screen.
- Live desk. Nothing to undo.
Files
- Contact sheets.
contact-swap-null.jpg shows the before/after swap frames and their difference images. contact-before-after-centre.jpg shows the same mark at full and at 25 %. aim-sheet.jpg, poses-sheet.jpg, halo-sheet.jpg and focus-sheet.jpg cover aim and spill.
- Data.
curve-fit.json holds the fitted curve and verify-summary.json the verification. frames/<run>/ holds every frame with log.txt (the wire per state) and pre_u1.txt / post_u1.txt.
- Scripts.
scripts/fohrun.ps1 is the PC runner, scripts/mkspec.py builds the state lists, and scripts/fit.py and scripts/verify_an.py do the analysis.
- Transport. Each run is pushed to
%TEMP%\fohcal on the PC, which was deleted at the end. Jobs 1542, 1557, 1563 and up to 1601.
Restore proof
All ten later runs printed RESTORE IDENTICAL 512/512. The first one did not. The aim run left tilt at the test value, because the desk clears pan and tilt with separate At Default lines. It was fixed 30 s later in job 1544 and the wire read 121,214,56,227,…,126,147,56,227,… exactly as found. Universe 1 channels 381-408 at the end match the start: 121,214,56,227,0,0,0,0,0,0,0,6,0,0,126,147,56,227,0,0,0,0,0,0,0,6,0,0.