#!/usr/bin/env python3
"""Audio design diagrams."""
import math
BOX = ('fill="var(--surface)" stroke="var(--rule-strong)" stroke-width="1.4" rx="3"')
# Front fill: Meyer M1D, five cabinets on the curved apron (feet; x from centre, z upstage of plaster).
# Geometry is read off the seating plan, scaled from the 50 ft opening: the apron is a 50 ft chord, 15.2 ft deep,
# so the apron edge is 8.2 ft from row 1. Each cabinet lies on the apron deck and is aimed along its radius from
# the arc centre.
APRON_R = 28.2 # apron arc radius
APRON_CZ = 13.0 # apron arc centre, upstage of plaster
SEAT_CZ = 20.7 # centre of the seat fan (where the aisle lines meet)
FILL_X = (-21.0, -12.0, 0.0, 12.0, 21.0)
ROW1, ROW_PITCH = 23.4, 3.18 # row 1 distance from plaster, row spacing
M1D_W, M1D_H, M1D_D = 23.04 / 12, 7.12 / 12, 8.50 / 12 # feet, from the datasheet
DECK = 3.5 # apron deck height above house floor
def flow_svg():
p = []
A = p.append
A('')
cap = ('Signal flow. Channels sum into five audio Groups — which is where the '
'bus compressors live, because a Group is a bus the signal actually passes through — and '
'each Group is driven by one DCA, which is a control link rather than an audio '
'path. All five Groups feed LR, so LR is the sum of five faders. The scene’s level map '
'decides who is open, glided over one second; the operator only ever decides how loud.')
return ('\n \n
\n ' + ''.join(p) +
f'\n
\n {cap}\n ')
def _fill_cabs():
"""(x, z, aim) per cabinet: z in feet upstage of plaster (negative = downstage), aim in degrees out."""
out = []
for x in FILL_X:
out.append((x, APRON_CZ - math.sqrt(APRON_R ** 2 - x ** 2), math.degrees(math.asin(x / APRON_R))))
return out
def _fill_level(x, z, cabs):
"""Relative level (dB) of the five fills at a seat: -6 dB at +-50 deg off axis, inverse distance."""
p = 0.0
for cx, cz, aim in cabs:
dx, dz = x - cx, cz - z
th = math.degrees(math.atan2(dx, dz)) - aim
off = -6 * (th / 50.0) ** 2 if abs(th) <= 90 else -40
r = max(math.hypot(dx, dz), 3)
p += 10 ** ((off - 20 * math.log10(r / 16)) / 10)
return 10 * math.log10(p)
def _seats(rows=range(1, 6)):
"""Seat grid for the first five rows: a fan about SEAT_CZ, aisles at 17-22 deg."""
out = []
r1 = SEAT_CZ + ROW1
for n in rows:
r = r1 + (n - 1) * ROW_PITCH
for deg in range(-50, 51):
if 17 < abs(deg) < 22:
continue
a = math.radians(deg)
out.append((n, deg, r * math.sin(a), SEAT_CZ - r * math.cos(a)))
return out
def fill_plan_svg():
"""Plan of the apron, the five M1Ds and the first five rows, seats coloured by predicted level."""
S, X0 = 7.3, 360
px = lambda x: X0 + x * S
pz = lambda z: 40 + (4.0 - z) * S
cabs = _fill_cabs()
seats = _seats()
lv = [_fill_level(x, z, cabs) for _, _, x, z in seats]
med = sorted(lv)[len(lv) // 2]
H = pz(-38) + 16
p = []
A = p.append
A(f'')
cap = ('Front fill plan. Five M1Ds lie on the apron deck and follow its arc, each aimed along its radius '
'from the arc centre. Dashed wedges are the 100° horizontal coverage at its −6 dB '
'edges. Seats in rows 1 to 5 are green within 3 dB of the '
'median, orange more than 3 dB hot and '
'violet more than 3 dB low. The model sums the five '
'cabinets as power and ignores the vertical pattern, so treat it as a layout check and run the '
'final aim in MAPP.')
return ('\n \n
\n ' + ''.join(p) +
f'\n
\n {cap}\n ')
def coverage_svg():
"""Section: array, fill and surround coverage with delay callouts."""
PY = 5.4
FLOOR = 258.0
X0 = -95.0
sx = lambda z: 22.0 + (z - X0) * 6.0
sy = lambda y: FLOOR - y * PY
p = []
A = p.append
A('')
cap = ('Coverage and time alignment, one value per system and two for the front fill. The centre three front fills run 20 ms and the '
'outer two about 15 ms, so the image stays on the actor rather than at the lip; the subs 7.75 ms to sit with the arrays. '
'The surround is a different problem — sitting behind the audience, no single value is '
'right everywhere — so it is tuned to 22 ms for the middle of the house, kept low, '
'and never carries dialogue.')
return ('\n \n
\n ' + ''.join(p) +
f'\n
\n {cap}\n ')
# ---------------------------------------------------------------------------------------------
# Front fill: level map, section and projector-clearance figures
# ---------------------------------------------------------------------------------------------
EAR = 3.5 # seated ear height above the house floor
FOH_LENS_Z, FOH_LENS_Y = -75.0, DECK + 6.5 # FOH projector lens: 75 ft out, 6.5 ft above the deck
def _fill_levels():
"""Per-seat level relative to the median seat, for the first five rows."""
cabs = _fill_cabs()
seats = _seats()
lv = [_fill_level(x, z, cabs) for _, _, x, z in seats]
med = sorted(lv)[len(lv) // 2]
return seats, [v - med for v in lv]
def fill_stats():
"""Share of centre-block and side-block seats within 3 dB of the median, worst and hottest seat."""
seats, d = _fill_levels()
pick = lambda sel: [v for (n, deg, x, z), v in zip(seats, d) if sel(abs(deg))]
share = lambda vs: 100 * sum(abs(v) <= 3 for v in vs) / len(vs)
return dict(centre=share(pick(lambda a: a <= 17)), side=share(pick(lambda a: a >= 22)),
worst=min(d), hot=max(d))
def _foh_ray(z_at, z_target):
"""Height above the house floor, at z_at, of the FOH ray that lands on the deck at z_target."""
return FOH_LENS_Y + (DECK - FOH_LENS_Y) * ((z_at - FOH_LENS_Z) / (z_target - FOH_LENS_Z))
def _fig(svg_parts, cap):
return ('\n \n
\n ' + ''.join(svg_parts) +
f'\n
\n {cap}\n ')
def fill_level_svg():
"""Predicted level along rows 1, 3 and 5 by seat angle."""
seats, d = _fill_levels()
W, H, L, R, T, B = 720, 250, 54, 24, 18, 44
cx = lambda a: L + (a + 50) / 100 * (W - L - R)
cy = lambda db: T + (8 - db) / 18 * (H - T - B)
p = []
A = p.append
A(f'')
cap = ('Predicted level along row 1 (blue), row 3 '
'(green) and row 5 (violet), '
'relative to the median seat. The model sums the five cabinets as power and ignores the vertical pattern '
'and comb filtering. The percentages carry about ±5 points of noise, so run the final aim in MAPP.')
return _fig(p, cap)
def fill_section_svg():
"""Centre-line section through FF-3: vertical wedge over rows 1 to 5 and the lowest FOH rays."""
zi = _fill_cabs()[2][1]
sz, z_left, ys = 14.0, -42, 20.0
sx = lambda z: 20 + (z - z_left) * sz
sy = lambda y: 232 - y * ys
row_z = lambda n: -(ROW1 + ROW_PITCH * (n - 1))
p = []
A = p.append
A('')
cap = ('Centre-line section through FF-3. The blue wedge is the 10° vertical minimum for one cabinet, aimed '
'at seated ears; it covers rows 1 to 5 on a flat floor. Orange and violet are the lowest rays from the '
'FOH projectors, 75′ out. Vertical scale is exaggerated about 1.5×.')
return _fig(p, cap)
def fill_clearance_svg():
"""Detail at the outer cabinets: top edge against the lowest FOH rays, on the deck and raised."""
zo = _fill_cabs()[0][1]
zs, ys, zl, zr, yb = 161.0, 125.0, zo - 2.0, zo + 2.0, 3.3
ix = lambda z: 56 + (z - zl) * zs
iy = lambda y: 215 - (y - yb) * ys
rl = _foh_ray(zo, 6) - DECK
rb = _foh_ray(zo, 0) - DECK
p = []
A = p.append
A('')
cap = (f'Detail at FF-1 and FF-5, four feet across and vertically exaggerated. On the deck the cabinet top clears '
f'the ray to LEG-*1 by {rl-M1D_H:.2f}′. Raised to the 4.5′ centre an earlier plot showed, it would '
f'cut {0.7+M1D_H-rl:.2f}′ into the beam and shadow the foot of the panel. The three inner cabinets are '
'clear of every projected surface.')
return _fig(p, cap)