#!/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('') A('') cols = [ (14, 'CHANNELS', [ ('Ch 1–14', 'principals'), ('Ch 15–19', 'ens. male'), ('Ch 20–24', 'ens. female'), ('Ch 25–26', 'orchestra'), ('FX 1–7', 'returns')]), (186, 'GROUP + bus comp', [ ('Group 1', 'mono · comp'), ('Group 2', 'stereo · comp'), ('Group 3', 'stereo · comp'), ('Group 4', 'stereo · comp'), ('Group 5', 'stereo · no comp')]), (358, 'DCA — fixed', [ ('DCA 1', 'principals'), ('DCA 2', 'ensemble male'), ('DCA 3', 'ensemble female'), ('DCA 4', 'orchestra'), ('DCA 5', 'FX returns')]), (530, 'LR → OUT', [ ('LR', 'flown arrays'), ('Matrix 1–2', 'front fill, 2 zones'), ('Matrix 3–4', 'dressing / backstage'), ('St Matrix 1–2', 'video rec / stream'), ('Aux, St Aux', 'monitors, subs, surr')]), ] W, H, GAP = 158, 42, 14 for x, title, rows in cols: A(f'{title}') for i, (a_, b_) in enumerate(rows): if not a_: continue y = 46 + i * (H + GAP) A(f'') A(f'{a_}') A(f'{b_}') for x0 in (172, 344, 516): for i in range(5): y = 46 + i * (H + GAP) + H / 2 A(f'') 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'') A(f'') a0 = math.asin(25 / APRON_R) pts = [(APRON_R * math.sin(-a0 + 2 * a0 * i / 60), APRON_CZ - APRON_R * math.cos(-a0 + 2 * a0 * i / 60)) for i in range(61)] d = 'M' + ' L'.join(f'{px(x):.1f} {pz(z):.1f}' for x, z in pts) A(f'') A(f'APRON') A(f'' 'plaster line · 50′ opening') for x, z, aim in cabs: t0 = math.radians(aim) edge = [(x + 24 * math.sin(t0 + math.radians(o)), z - 24 * math.cos(t0 + math.radians(o))) for o in (-50, 50)] A(f'') tip = (x + 24 * math.sin(t0), z - 24 * math.cos(t0)) A(f'') for (n, deg, x, z), v in zip(seats, lv): dv = v - med col = 'var(--warn)' if dv > 3 else ('var(--aurora)' if dv < -3 else 'var(--good)') A(f'') for n in (1, 3, 5): r = SEAT_CZ + ROW1 + (n - 1) * ROW_PITCH A(f'row {n}') for i, (x, z, aim) in enumerate(cabs): A(f'') A(f'FF-{i+1}') A('') 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('') # rake / floor A(f'') A(f'') A(f'STAGE') # coverage wedges A(f'') zf = _fill_cabs()[2][1] # centre cabinet FF-3, centre-line section yf = DECK + M1D_H / 2 z5 = -(ROW1 + 4 * ROW_PITCH) dy = abs(z5 - zf) * math.tan(math.radians(5)) # 10 deg vertical minimum, single cabinet A(f'') A(f'') # boxes for z, y, col, lbl, sub in ((2.0, 25.5, 'var(--accent)', 'FLOWN ARRAY', 'L / R'), (_fill_cabs()[2][1], DECK + M1D_H / 2, 'var(--good)', 'FRONT FILL', '5 × M1D, two delays'), (-89.0, 33.5, 'var(--warn)', 'SURROUND', 'spot booth')): A(f'') A(f'{lbl}') A(f'{sub}') # sub position A(f'') A(f'SUBS · 7.75 ms') # delay callouts for z, txt, col in ((-26.0, '20 / 15 ms', 'var(--good)'), (-62.0, '22 ms', 'var(--warn)')): A(f'{txt}') # row markers for z, lbl in ((-ROW1, 'row 1'), (-(ROW1 + 11 * ROW_PITCH), 'row 12'), (-83, 'back')): A(f'') A(f'{lbl}') 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'') for db in range(8, -11, -3): A(f'') A(f'' f'{"median" if db == 0 else f"{db:+d} dB"}') for a0, a1 in ((17, 22), (-22, -17)): A(f'' f'aisle') for a in range(-50, 51, 10): A(f'' f'{a:+d}°' if a else f'' f'0°') A(f'' 'seat angle from the centre line, seen from the middle of the seat fan') for n, col in ((1, 'var(--accent)'), (3, 'var(--good)'), (5, 'var(--aurora)')): row = sorted((deg, v) for (nn, deg, x, z), v in zip(seats, d) if nn == n) for part in ([t for t in row if t[0] <= -22], [t for t in row if -17 <= t[0] <= 0], [t for t in row if 0 <= t[0] <= 17], [t for t in row if t[0] >= 22]): if len(part) > 1: A('') A('') 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('') A(f'') A(f'') A(f'APRON DECK +3.5′') y0 = DECK + M1D_H / 2 for n in range(1, 6): A(f'' f'r{n}') z5 = row_z(5) dy = abs(z5 - zi) * math.tan(math.radians(5)) A(f'') A(f'') A(f'') A(f'seated ear 3.5′') for zt, col in ((0, 'var(--warn)'), (6, 'var(--aurora)')): za = -38 A(f'') A(f'') A(f'FF-3 on the deck' f'apron apex, {abs(zi):.1f}′ downstage') 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('') A(f'') A(f'') for yy in (DECK + 0.5, DECK + 1.0, DECK + 1.5): A(f'' f'+{yy-DECK:.1f}′') for zt, col in ((0, 'var(--warn)'), (6, 'var(--aurora)')): A(f'') A(f'' f'ray to LEG-*1, {rl:.2f}′ at FF-1 / FF-5') A(f'' f'ray to portal side band, {rb:.2f}′') c1, c2 = zo - 0.9, zo + 0.9 A(f'') A(f'') A(f'on the deck' f'' f'top +{M1D_H:.2f}′ — clears by {rl-M1D_H:.2f}′') A(f'raised to a 4.5′ centre' f'' f'top +{0.7+M1D_H:.2f}′ — cuts the ray by {0.7+M1D_H-rl:.2f}′') 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)