Acouvero
Download
TECHNICAL DOC · v0.5.0

Room acoustics,
full-stack — from
measurement to plan.

Acouvero is a room-acoustics analysis and treatment-planning tool for iPhone / iPad. This document is for readers interested in the acoustics engineering or the client implementation — covering the core algorithms, geometry system, measurement methods, recommendation engine and engineering notes.

Octave bands
6bands
Room shapes
4geometries
Reflections
1–3orders
§ 01

Overview

Input: the user enters room size / shape / finish materials, speaker and listening-position locations — or the room geometry is measured with the AR tape measure (no LiDAR needed) or captured via a LiDAR (RoomPlan) scan.

Output
  • Early-reflection point localization
  • RT60 (measured + simulated) plus EDT / C50 / C80 / D50 (derived from the simulated RIR)
  • Room-mode frequency list
  • Panel layout plan (position, count, product SKU)
  • 3D visualization + reflection-energy heatmap
  • Installation-manual-grade PDF export
  • On-device ESS sweep / clap measurement (built-in or USB measurement mic)
  • USB measurement-mic calibration & SPL reference, real-time spectrum (RTA)
  • Frequency-response overlays & multi-position averaging, REW interop, PEQ / subwoofer alignment, AI assistant

Rectangular, L-, U-shaped and arbitrary simple-polygon rooms are supported; first-order reflections and area / containment math use the true polygon; 2nd/3rd-order reflections and room modes approximate via the bounding box (see §9).

§ 02

Acoustic model

PLAIN VERSION These concepts each have a plain-language explainer: what is RT60, room modes & standing waves, and the image-source method.

2.1 Sabine RT60

The classic Sabine formula:

SABINE · RT60RT60=0.161⋅V∑iSiαi RT_{60} = \frac{0.161 \cdot V}{\sum_i S_i \alpha_i}
VV
Room volume (m³); for polygons, Shoelace area × height, minus obstacles such as columns / beams.
∑iSiαi\sum_i S_i \alpha_i
Surface area × absorption coefficient per face. Coefficients use 6 octave bands (125 / 250 / 500 / 1k / 2k / 4k Hz).
0.1610.161
= 24·ln(10)/c, with c = 343 m/s (20 °C).

The app ships 4 finish presets (sparse hard-surfaced / typical hard finish / soft furnishings / already treated), each with a per-band absorption curve; the headline single-value RT60 estimate and the panel budget use a mid-high-band average coefficient, while the 6-band Sabine curve drives the per-band display. The target RT60 is also a 6-band curve (not a single value), matching standards like EBU Tech 3276 / THX that allow a longer low-frequency tail.

2.2 Image Source Method (ISM)

First reflections use mirroring: reflect the source across a wall; the intersection of the “mirror source → listener” segment with the wall is the reflection point.

  • Rectangular room: mirror across all 6 planes (4 walls + floor + ceiling) — closed-form.
  • Non-rectangular (PolygonReflectionCalculator): mirror each wall segment from RoomPolygon.walls() and validate the in-segment parameter s ∈ [0, 1] (floor / ceiling reflections additionally require the reflection XY to lie inside the polygon); first-order paths also get a 2D occlusion check — any source→point→listener leg crossing another wall segment is discarded.

Multi-order reflections (orders 1–3) are implemented by MultiOrderReflectionCalculator following pyroomacoustics' recursive approach:

  1. Enumerate all wall sequences [s₁, …, sₙ], adjacent walls distinct
  2. Image-source iteration: S₀ = source, Sₖ = mirror(Sₖ₋₁, sₖ)
  3. Back-trace from the listener: line Sₙ → listener meets sₙ at Pₙ; line Sₙ₋₁ → Pₙ meets sₙ₋₁ at Pₙ₋₁; recurse back
  4. Each intersection must lie within the wall's valid range and in-segment t ∈ (0, 1)
APPROXIMATION In non-rectangular rooms, multi-order uses the bounding-box approximation (“ghost reflections” near concave corners). The analysis layer discards any path whose reflection point is > 0.3 m from every real wall segment; only the remaining valid paths feed flutter / slapback detection.

2.3 Path energy

Broadband energy as a function of reflection count and distance attenuation:

PATH ENERGYErel=(1−αˉ)N⋅ddirect2dtotal2 E_{rel} = (1 - \bar{\alpha})^N \cdot \frac{d_{direct}^2}{d_{total}^2}
  • α is clamped to [0, 0.99] so α = 1 can't produce log(0) or pow(negative, N) = NaN that poisons the whole path
  • Distance attenuation follows spherical spreading 1/r²
  • Conversion to dB uses 10·log₁₀ (energy domain)

Per-band energy is computed separately for the 6 octave bands (each using the per-band coefficient from finishProfile.absorptionByBand).

2.4 RT60 estimation (measured)

ImpulseResponseEstimator:

  • Matched filtering: the recorded sweep is inverse-filtered (ESS / Farina) into a single impulse; processing gain ≈ 10·log₁₀(T·B) — about +41 dB effective SNR for the 3 s, 180 Hz–4 kHz phone preset
  • rmsEnvelope: sliding-window RMS envelope
  • Schroeder backward integration: E_back(t) = ∫ₜ^∞ s²(τ) dτ, normalized with 10·log₁₀ to give the EDC curve
  • Slope linear regression: prefer T30 [−5, −35 dB] by SNR, falling back to T20 [−5, −25] or T10 [−5, −15]
  • RT₆₀ = −60 / slope

Per-band RT60: on the matched-filtered samples, band-pass at the ISO octave centers selected according to the actual sweep range (Q = √2), then run Schroeder per band.

2.5 RIR simulation

SimulatedRIRGenerator turns multi-order reflection paths into discrete impulse events written to an 8 kHz × 0.5 s PCM buffer:

SimulatedRIRGeneratorpseudocode
for path in reflectionPaths:
    t   = path.totalDistance / speedOfSound
    amp = 10^(path.relativeEnergyDB / 20)
    impulse[round(t * sampleRate)] += amp

Add the direct-sound impulse, then run Schroeder backward integration to extract EDT / RT60 / C50 / C80 / D50.

Before / after comparison Each reflection path is attenuated by the panels accumulated on the walls it touches, producing a “treated” RIR. The UI A/B-toggles to show the EDT / C50 improvement, and AVAudio convolves a dry signal to audibly compare before and after.

2.6 Room modes

Modal frequencies for a rectangular room:

ROOM MODESfnx,ny,nz=c2(nxLx)2+(nyLy)2+(nzLz)2 f_{n_x, n_y, n_z} = \frac{c}{2} \sqrt{\left(\frac{n_x}{L_x}\right)^2 + \left(\frac{n_y}{L_y}\right)^2 + \left(\frac{n_z}{L_z}\right)^2}

Classified by how many of nₓ, n_y, n_z are non-zero: 1 = axial, 2 = tangential, 3 = oblique.

Enumeration covers n ≤ 4 per axis up to 300 Hz. Non-rectangular rooms estimate modes from the bounding box (exact modes need FEM); this is stated explicitly in the UI.

§ 03

Geometry system

3.1 Supported shapes

  • Rectangle — width × length × height
  • L-shape — remove an nw × nl notch at a chosen corner
  • U-shape — remove an nw × nl notch at the middle of a chosen wall
  • Custom — any simple polygon (≥ 3, ≤ 24 vertices)

All non-rectangular shapes become a CCW vertex list via RoomPolygon.from(shape:, bboxWidth:, bboxLength:). First-order reflections, area / perimeter and containment math use these vertices directly; an invalid custom polygon (< 3 / > 24 vertices, self-intersecting, near-zero area) silently degrades to the bounding rectangle.

3.2 Polygon geometry utilities

  • Area — Shoelace formula
  • Perimeter — sum of edge lengths
  • contains(x, y) — ray-casting with a boundary epsilon fallback (avoids false negatives at corners)
  • clamped(point) — rectangles use the fast [0, w] × [0, l] path; non-rectangles use the true [minₓ, maxₓ] × [min_y, max_y] of polygon.boundingBox, test polygon.contains, and binary-search toward the bbox center if outside
  • walls() — splits the polygon into wall segments classified by inward normal (auto-binned to leftWall / rightWall / frontWall / backWall), so the rectangular panel recommender can be reused
  • PolygonValidator.isSimple — validates a non-self-intersecting simple polygon

3.3 RoomPlan scanning

RoomCaptureSession (an iOS 16+ API; the app gates scanning at iOS 17) is used, and CapturedRoomMapper maps the scan into a ScannedRoom (wallSegments + openings + furniture). Coordinate mapping:

ARKit (x, y, z) → y is vertical App (x, y, z) → z is vertical Wall endpoints (z=0 floor) App.x = ARKit.x App.y = ARKit.z App.z = 0 Opening / furniture center  App.x = ARKit.x App.y = ARKit.z App.z = ARKit.y

Scans are classified by wall count: exactly 4 wall segments are treated as a rectangle outright; anything else goes through RectangularRoomSimplifier.polygonShapeFromScan, chaining wallSegments into polygon vertices. RoomPlan endpoints err by 3–8 cm typically; a 12-segment loop can accumulate 5 cm, so the closing epsilon is 12 cm. If the chain doesn't close, it falls back to the bounding box and asks the user to rescan.

3.4 AR tape measure

On devices without LiDAR, ARTapeMeasureView (a RealityKit ARView with world tracking) takes over: aim the crosshair at the wall and tap twice to record start / end points, stepping through width → length → height, then confirm to produce a rectangular room — with camera-permission guidance and tracking-failure hints.

3.5 Free-vertex editor

FreeVertexEditorView lets the user drag vertices on a SwiftUI Canvas:

  • Long-press to delete (when vertex count > 3, so ≥ 3 remain)
  • Each drag frame is validated with isSimple; a self-intersecting new position simply isn't applied (the vertex stays put). Tapping empty space inserts a vertex on the nearest edge; there's also a per-vertex numeric editor and a reset-to-bounding-rectangle button
  • 0.05 m grid snapping
  • A max(0.5, bbox × 0.2) m margin outside the bbox lets users drag vertices beyond the current bbox (the parent view auto-grows room.width / length)
§ 04

Listening zones & multi-listener

AcousticConfiguration.listeners supports 1..N listening positions (with 0 listeners a default primary is synthesized with a warning; with 0 speakers the §5.2 use-case templates take over). Analysis runs the image-source method once per listener and merges reflection points into one array (the primary listener's points first). The panel recommender weights each wall by its reflection-point count (cap = max(6, speakerCount × listenerCount)); multiple seats sharing a wall naturally push it toward the budget ceiling.

UI visuals
  • In 3D, secondary listener nodes use opacity 0.45 + scale 0.75
  • In the top view (RoomPlanView), primary reflection points are full-color r=5, secondary 0.45-opacity r=3
  • The reflection list (RecommendationView) shows only primary points; secondary seats get a footnote “+ N reflections already counted in the plan”

Multi-order reflections / RIR / heatmap are still computed once for the primary listener (per-listener O(speakers × surfaces³) is too heavy, and RIR is a single-point metric).

§ 05

Recommendation engine

PanelPlacementRecommender outputs [PanelRecommendation], each with: position (left / right / ceiling / front / back wall / corner), count, priority (essential / strong / recommended / optional), effectivenessMultiplier, targetReflectionPointId.

5.1 Proportional allocation (largest remainder)

The old greedy add() starved walls: left wall consumed the budget → right wall got 0; under tight budgets the ceiling / front / back were starved too. The new version uses the largest-remainder method:

  • Compute each wall's raw desired (reflection-point count + room depth + use case)
  • floor(raw × budget / total) as the initial allocation
  • walls with raw &gt; 0 get at least 1 panel; only when the budget is smaller than the number of walls with demand are surfaces zeroed, lowest-priority first
  • if the sum &gt; budget, trim from the smallest fractional remainder up
  • left/right symmetry pass: if raw[0] == raw[1], force alloc[0] == alloc[1]
  • redistribute any leftover by largest fraction

5.2 Use-case templates

With no speakers placed, use-case-specific templates apply:

  • Meeting room: ceiling 35% + back wall 20% + each side wall 12%
  • Podcast: directly in front of the mic 25% + each side 18% + ceiling 20%
  • Studio: symmetric four-wall damping + up to 4 corner bass traps within the budget (the true budget bypass only happens on the dense low-frequency-mode path of §5.3)
  • General listening: symmetric baseline + a prompt to add speakers

5.3 Physical must-haves bypass the budget

Corner bass traps are forced to 4 when 80–150 Hz modes are dense, bypassing the totalPanel cap. Rationale: corners have the highest sound pressure and bass traps are ~1.5× more efficient there than on walls — a physical necessity, not a budget item.

5.4 Near-listener weighting

A post-pass tags wall recommendations within 1.5 m of a listener as .nearListener, sets effectivenessMultiplier ×1.15, and notes in the title / reason that this is a “close-range absorption-sensitive zone”.

§ 06

Measurement

PLAIN VERSION These three topics each have a plain-language deep dive: sine-sweep measurement, the clap test, and choosing a measurement mic.

6.1 ESS sweep

ESS logarithmic sweep (Farina method): four presets plus custom, with 3 / 6 / 12 s durations:

  • Phone quick test: 180 Hz–4 kHz on the built-in output, 75 Hz–4 kHz on external output (the only preset that allows the built-in speaker)
  • Low frequency / subwoofer: 15–300 Hz (external output required)
  • Full range: 20 Hz–20 kHz (external output required)
  • Custom: any start/end within 10 Hz–20 kHz spanning at least one octave (external output required)
  • Inverse filter (makeInverseSweep) = time reversal + a +6 dB/octave amplitude envelope (rising with frequency, to counter the ESS pink spectrum)
  • Recording via AVAudioRecorder to lossless Linear PCM (16-bit mono .caf, ~96 dB dynamic range — perceptual codecs don't belong in impulse measurement; the sample rate follows the audio session — 44.1 kHz on the built-in mic, the native 48 kHz on USB measurement mics)
  • Read back → matched filter → analyzeImpulse

The playback signal, inverse sweep and analysis share one configuration, with the top frequency capped by the actual sample rate; the low-frequency / full-range / custom presets refuse to start without an external output. RT60 octave bands are selected according to the actual sweep range; a preset’s frequency range is the stimulus configuration — the trustworthy range still depends on the speaker, microphone and measurement quality.

Sample-rate alignment The sweep WAV, the inverse sweep and the recording are all generated at the audio session's actual sample rate (makeInverseSweep(sampleRate:) is parameterized) — matched filtering is always in effect, at 44.1 kHz on the built-in mic and 48 kHz on USB measurement mics alike.

6.2 Clap impulse

No playback — the user claps hard once within a 1 s window; 4.5 s are recorded in total (~3.5 s of tail). It goes straight to analyzeImpulse (no matched filter needed; a clap is already a narrow impulse). This covers the 125 Hz octave band, which the phone preset (from 180 Hz) misses.

6.3 Measured vs simulated

MeasuredVsSimulatedCard pulls the project's latest measured RT60 and shows the deviation against the current geometry's simulated RT60:

< 15%model and measurement agree
15–30%some divergence (possibly unmodeled furniture / materials)
> 30%large divergence — recheck geometry / materials

6.4 Ambient noise & calibration

AmbientNoiseEstimator records 3 s of silence (trimming 100 ms at each end) for the room's background noise: broadband average / peak dBFS plus a 5-level quietness rating (dBFS is relative-only until an SPL calibration, after which dB SPL is shown). Near-field latency calibration reuses the same ESS sweep: hold the phone within 30 cm of the speaker, take the recording-start-to-impulse-peak delay (including a deliberate 250 ms pre-roll), ideally 2–3 runs — a spread of the last 3 readings under 5 ms rates high confidence, under 20 ms medium. The latency is bound to the current input + output route and auto-invalidates when either changes; far-field measurements show the propagation delay with it subtracted.

6.5 Time reference & measurement quality

  • Acoustic time reference: a chirp precedes the sweep as a time anchor, re-detected on every measurement; on detection failure the app falls back and explicitly marks phase confidence — no claim of absolute cross-measurement alignment.
  • Clock drift: estimates the clock difference between the playback and recording paths and corrects it when conditions allow.
  • Electrical loopback reference: multi-input USB interfaces can use a second channel as a loopback reference; when conditions aren’t met the app falls back to the acoustic reference. The Mac playback-helper path does not use the local electrical loopback.
  • Quality fields: each record stores the available SNR, noise floor, clipping status, IR time offset, actual input and sample rate; anything undeterminable is left empty, never fabricated.
  • Clipping handling: a sweep with detected clipping is invalidated with a prompt to re-measure; clap records are flagged and surfaced.
  • Simulated vs measured cross-check: the room’s latest measurement is compared side by side with the geometric-simulation RT60 to gauge model fit (thresholds in 6.3).

6.6 USB measurement mics & output routing

The measurement page’s input picker lists the built-in mic and class-compliant USB inputs (e.g. miniDSP UMIK-1 / UMIK-2); a routeChangeNotification listener falls back to the built-in mic when the external one vanishes. Recording and sweep sample rates follow the session (UMIK-1 = 48 kHz). REW / UMIK-style .txt frequency calibration files import directly (parsing Sens Factor and serial-number metadata, interpolated in the log-frequency domain; the 90° file is recommended for room measurement). Calibration is bound to the input port UID — switch to a mismatched input and the previous calibration is not applied.

One-time SPL reference calibration: fit a sound-level calibrator (default 94 dB @ 1 kHz) over the mic, the app samples 3 s of level to derive the dBFS → SPL offset; dB SPL is shown only when the input matches and an SPL calibration exists — otherwise readings stay in dBFS. Measurement-time calibration snapshot: new sweep / clap records store the calibration curve, name, serial number, Sens Factor, SPL offset and input UID in effect at the time, so replacing, clearing or rebinding the calibration later never changes those records’ readings; the measurement detail, the post-window frequency-response preview, frequency-response text export and the PDF all use the same saved snapshot. The calibration itself is persisted per input port UID to ~/Library/Application Support/mic_calibration.json.

Playback goes through the system route picker (an AVRoutePickerView wrapper, placed on the sweep / near-field calibration cards); the session allows AirPlay / Bluetooth A2DP / HFP so test tones can play through external speakers, and falls back to the speaker when none is connected. The recording input is always explicitly pinned to the user’s choice (USB mic or built-in) so a Bluetooth device’s mic can’t steal the input mid-playback and contaminate the measurement. Mac playback helper: discovers AcouveroMacSpeaker on the LAN via Bonjour, transfers the sweep WAV to the Mac first and then sends the play command, while the iPhone records — for Mac output paths that can’t host the measurement session’s audio directly (this path does not use the local electrical loopback).

6.7 REW & file interoperability

  • Frequency-response text import / export: tolerant of common delimiters, comment lines, scientific notation and decimal formats, with the frequency axis validated on import; files without a phase column yield no phase data.
  • Calibration-aware export: exports dB SPL when a valid SPL calibration snapshot exists, dBFS otherwise — the header labels the unit honestly.
  • IR audio import: WAV / AIFF impulse responses are supported; multi-channel files require an explicit channel choice.
  • Offline sweep analysis: export the stimulus file for external playback, then after importing the recording you must state the start/end frequencies and duration before deconvolution — sweep parameters are never guessed.
  • Audio share export: available IRs or raw recordings can be shared out as WAV.
  • Imported results can be archived and flow into the applicable charts, overlays and analysis; whether they may join a measurement group depends on the data type and confidence scope. REW .mdat project files are not parsed at present.
§ 07

Measurement charts & analysis

The measurement detail page shows charts according to the data each record actually holds — missing curves are never fabricated; charts support dark/light mode, Dynamic Type and accessibility summaries.

  • RT60: octave-band bar chart with target line and status colors.
  • Schroeder decay: EDC curve with the T20 fit interval.
  • Frequency response: log frequency axis, octave smoothing and cursor readout; shown in dB SPL while the calibration snapshot is valid.
  • Impulse response: waveform with time-zero and peak markers; amplitude is relative to the peak.
  • Phase: wrapped / unwrapped views, polarity inversion, time-zero nudging and a reference-confidence hint.
  • Group delay: group delay over the valid band, with optional minimum-phase / excess-phase analysis.
  • IR windowing: peak or t=0 reference, independent left/right widths, rectangular / Hann / Tukey / Blackman; original and windowed frequency responses preview side by side — windowing and phase nudging only affect the preview, never the stored measurement.
  • Spectrogram: time-frequency decay of the IR, with adjustable time span and dynamic range.
  • Waterfall & RT60 Decay: low-frequency decay visualization with adjustable window length / time / dynamic range; fits of insufficient quality are not shown.
  • Real-time spectrum (RTA): 1/1–1/24 octave display, A / C / Z weighting, Instant / Fast / Slow response, peak hold & clear, overall level, peak level and dominant frequency.
  • RTA uses the calibration of the matching input and is labeled dBFS until an SPL calibration exists; it is mutually exclusive with formal measurements (entering releases the measurement page’s level monitor, closing restores it), and stale callbacks never overwrite fresh state after a stop or input switch.
Data gating · DATA GATING Phase, group delay and windowing require a qualified sweep and IR data; when the time reference is insufficient the UI explicitly calls it relative phase — no claim of reliable absolute cross-measurement alignment. RTA has no long-term SPL logger yet.
§ 08

History, multi-position averaging & overlays

  • V2 archive: measurements store metadata, the available raw recording / IR sidecar and analysis data; legacy records stay read-only compatible, and old records without raw data are not back-filled.
  • History management: browse and delete measurements per room, open details, or pick records for further analysis.
  • Before / after comparison: pick any two records to compare overall and per-band RT60, overlaid frequency responses and differences over the valid bands.
  • 2–6 frequency-response overlays: 1/3–1/48 oct smoothing, per-curve show/hide, range presets such as full / low frequency; levels can stay unaligned, be mid-band zeroed or aligned to the first curve, with offsets shown in the legend; source data is untouched, and records that can’t be overlaid say why.
  • Measurement groups: records carry position labels, weights and a before / after stage; build a group from history, join one, or measure a new position straight into a group.
  • Multi-position averaging: dB arithmetic mean, RMS power mean, and — given a reliable time reference and complex spectra — vector complex averaging; shows the average curve, member curves and the per-frequency min/max spatial spread.
  • Group comparison: compares two groups’ average responses, spatial spreads and mean RT60, concluding on spatial consistency.
Boundaries · BOUNDARIES Group averaging and comparison currently use relative dB; vector averaging across sample rates isn’t supported yet. Overlays currently use range presets — no gesture zoom, phase / waterfall / decay overlays, or PDF export of overlay charts yet.
§ 09

PEQ & subwoofer alignment

PLAIN VERSION These two topics each have a plain-language deep dive: parametric EQ and subwoofer alignment.

9.1 PEQ target matching

  • Parameterized target curve: target level, low-frequency rise, high-frequency tilt and the matching band.
  • Filter constraints: filter count, maximum boost / cut and Q range.
  • Prediction & rationale: measurement, target, filter responses and the predicted result share one chart, with improvement metrics and a per-filter recommendation rationale.
  • Deep-notch exclusion: deep notches caused by acoustic cancellation are excluded automatically, avoiding huge boosts thrown at a null.
  • Export: Generic text, Equalizer APO, miniDSP coefficients and CamillaDSP YAML — computation and settings export only; nothing is written to devices directly.

There is no manual filter editor, target preset library or EQ result archive yet.

9.2 Two-way & multi-sub alignment

  • Two-way alignment: pick one reference-speaker and one adjustable-subwoofer measurement, predict the complex summation, and solve delay, gain and polarity; the summed curve is shown before and after alignment.
  • Multi-sub, multi-seat: 1–4 subwoofers with independent measurements selected per seat × source; optimize for all seats, constrain by the worst seat, or explicitly optimize the main seat only.
  • Results & sharing: per-sub settings, per-seat improvement, average and worst-case metrics, with settings shareable as text.
  • Data gating: records missing a sweep configuration, a reliable time reference or an IR list their reason for unavailability instead of being silently excluded.

Multi-seat results are currently a numeric table with no per-seat summed-curve overlay; alignment is a prediction that must be verified by re-measuring after applying the settings; mixing sources across sample rates is still to be validated.

§ 10

AI acoustic assistant

The measurement-history toolbar offers a chat entry that uses the user’s own Anthropic API key (stored in the on-device Keychain); the assistant sends the conversation and tool data needed for each request to Anthropic, and API costs are borne by the user’s own account.

  • Chat interface: streaming replies, tool-execution status, stop & clear, and usage / cost estimates.
  • Measurement data access: list records, summaries and partial frequency responses, and compare measurements.
  • Computation tools: invokes exactly the same PEQ recommendation and subwoofer-alignment algorithms as the manual UI.
  • Room & layout evaluation: lists rooms, reads speaker / listener positions and mode data, and predicts — on a copy of the project — the effect of moving existing equipment or changing the finish absorption profile; saved projects are never rewritten.
  • Measurement only after confirmation: the AI proposes the purpose and candidate rooms and starts a sweep only after the user confirms; declining or stopping cancels the request.
  • Shared measurement settings: AI and manual measurement reuse the same workspace, inheriting input, Mac playback helper, sweep range / duration, delay and loopback settings; the chat page shows live level / progress with cancel, and afterwards restores the original room and measurement-group context.
  • Session restore: one current session is saved and restorable, with the tool execution interrupted last time marked on restore; clearing the session wipes history and usage together.
Boundaries · BOUNDARIES There is no multi-session list or on-device model fallback yet; the layout tool doesn’t support adding/removing equipment, specified panel changes or prediction from measured calibration. The real API-key path and the on-device audio loop are still pending acceptance.
§ 11

Visualization & output

11.1 3D view

Room3DController renders with SceneKit: walls / floor / ceiling / speaker nodes (thin cabinet + accent tick + channel pill) / listener (FE glyph + drop stem) / obstacles (translucent SCNBox) / recommended panels (per-wall panel mockups) / multi-order reflection path segments / reflection-energy heatmap overlay.

DESIGN · TECHNICAL DRAWING The look is a “technical-drawing style” (hairline edges, constant lighting, no PBR), deliberately distinct from photorealistic 3D.

11.2 Reflection-energy heatmap

ReflectionEnergyMapGenerator accumulates each multi-order path's energy onto the touched wall's uCells × vCells grid (default 12×6). One energy map per wall, overlaid in 3D as an energy color layer (blue → cyan → green → yellow → red).

EnergyHeatmapRenderer turns the grid into a UIImage: block-style cells at 16 px each (no interpolation between cells, keeping grid edges legible; on-wall smoothing comes from SceneKit's bilinear texture filtering) + a custom 5-stop ramp (blue → cyan → green → yellow → red) + alpha mapped linearly from 0.20 to 0.85 by the energy's position on a fixed −30…0 dB scale (−∞ fully transparent).

11.3 RIR convolution preview

RIRPreviewPlayer uses AVAudioEngine + AVAudioPlayerNode to convolve a procedurally synthesized dry signal (plucked string / clap burst / vowel-like speech — no bundled samples) with the simulated RIR and play it, so users hear the difference “untreated vs treated”.

Time-domain convolution RIRConvolver.convolve(dry:rir:) is direct O(N × M) (N = dry samples, M = RIR samples), peak-normalized to 0.9 to avoid clipping.

11.4 PDF installation manual

ReportExporter renders an A4 PDF (595 × 842 pt) via UIGraphicsPDFRenderer:

  • Room overview + 3D snapshot
  • 6-band RT60 bar chart: Sabine estimate vs target, with measured values overlaid when measurement data exists
  • Reflection-point table + mode table + risk summary
  • Recommended-panel table (the shopping-list BOM is shown in-app, not in the PDF)
  • When measurement data exists, RT60 / EDC / frequency-response / spectrogram / waterfall / phase charts and a multi-position averaging summary are added; frequency-response units use the calibration snapshot saved at measurement time (honestly labeled dB SPL or dBFS)
  • Geist font + PingFangSC fallback (CoreText cascadeList, so CJK doesn't render as □□□)

11.5 Project persistence

ProjectStore (an actor) writes ~/Library/Application Support/Acouvero/projects.json: [RoomProject], each holding the full Room + speakers + listeners + recommendations + panelCountOverrides. On first launch, data migrates automatically from the legacy FindEcho/ directory (FindEcho is the pre-rename internal codename, deliberately kept in code symbols).

MeasurementStore (an actor) writes ~/Library/Application Support/Acouvero/measurements.json: [MeasurementResult], linked to a project by roomId.

Both use element-level decode recovery (a Throwable<T> wrapper) — one corrupt record won't fail the whole list.

MicCalibrationStore writes ~/Library/Application Support/mic_calibration.json (directory root, no subdirectory): the REW calibration curve + SPL offset, bound to the input port UID — switching inputs requires a fresh import / calibration.

§ 12

Engineering notes

12.1 SwiftUI + @Observable

The ViewModel (FindEchoViewModel) uses Swift 5.9's @Observable macro — state fields are observed directly, no Combine. AcousticAnalysis results are cached by an Equatable comparison of AcousticConfiguration:

FindEchoViewModel.analysisswift
var analysis: AcousticAnalysis {
    let cfg = configuration
    if let cached = cachedAnalysis, cachedConfiguration == cfg {
        return cached
    }
    let result = analysisService.analyze(cfg)
    cachedConfiguration = cfg
    cachedAnalysis = result
    return result
}

12.2 Actor isolation

ProjectStore and MeasurementStore are actors, serializing IO / async logic naturally; RoomPlanService is a @MainActor class. UI callers use async / await, so the main thread is never blocked by disk IO.

12.3 AVAudio lifecycle

AudioSessionManager centralizes the session category (.playback for RIR preview, .playAndRecord for measurement). Interruption protection covers both paths: on the measurement side an InterruptionMonitor inside AudioMeasurementService listens for AVAudioSession.interruptionNotification — a call / Siri during a sweep, clap or ambient-noise run discards the result with an error; on the preview side RIRPreviewPlayer listens too, auto-stopping the player node and clearing isPlaying. An audio-route change mid-measurement also voids the run.

12.4 Task cancellation

Convolution playback is a background task; rapid button taps trigger pendingPlayTask?.cancel() plus a Task.isCancelled double-check to prevent out-of-order MainActor writes.

12.5 Test coverage

AllShapesAndUseCasesTests uses the Swift Testing framework to run the full matrix — every shape × every use case (single listener; speakers are a stereo pair or a single center per use case) — asserting:

  • analysis.reflectionPaths.count >= 1
  • analysis.warnings includes the approximation note for non-rectangular rooms
  • the recommended panel total is non-negative (totalPanelCount ≥ 0; no budget-match assertion)
  • self-intersecting custom polygons fall back safely (no crash, degrade to a rectangle)
  • vertices &lt; 3 and other edge-case fallbacks

12.6 Platform & localization

Deployment target iOS 17.0, iPhone + iPad; zero third-party dependencies (no SPM / CocoaPods packages) — system frameworks only (SwiftUI / SceneKit / ARKit / RoomPlan / AVFoundation / Accelerate). Localization uses an .xcstrings catalog: Chinese as the source language plus English / Français / Deutsch / Español / Português (Brasil) — 6 languages total, switchable in-app or following the system.

§ 13

Known limitations

  1. Multi-order in non-rectangular rooms uses the bounding box — 2nd/3rd-order flutter / slapback risk near concave corners may be off. First-order points themselves use the true polygon.
  2. Room modes in non-rectangular rooms are estimated from the bounding box — exact modes need FEM.
  3. iPhone built-in mic measurement — uncalibrated; non-flat frequency response / sensitivity. Confidence is always marked .low, used as a cross-reference against the geometric estimate. With a USB measurement mic (miniDSP UMIK-1 / UMIK-2) and its REW calibration file loaded, per-band levels are corrected by the calibration curve; a one-time 94 dB SPL reference calibration then shows ambient and measurement levels as absolute dB SPL (dBFS only until then).
  4. Single-source ISM assumption — ignores cabinet diffraction, horn directivity and other real speaker traits.
  5. No bass-trap modeling — sub-80 Hz problems fall outside the Sabine model and need dedicated low-frequency treatment.
  6. AR scan accuracy — RoomPlan endpoints err 3-8 cm typically; a 12-segment loop can accumulate 5+ cm. Wall thickness isn't modeled.
  7. Analysis-scope boundaries — vector complex averaging across sample rates isn’t supported yet; multi-seat alignment results are currently a numeric table with no per-seat summed-curve overlay; frequency-response overlays don’t enter the PDF report yet.
  8. Professional tools not yet implemented — no long-term SPL logger, STI speech intelligibility or distortion (THD) measurement; impedance / Thiele-Small and a REST API are also absent.
  9. Pending on-device acceptance — the AI assistant’s real API path (streaming with a real key / confirmed measurement / recording archival) and the full UMIK measurement-mic chain on real hardware (including UMIK-2 at 96 kHz) are still to be accepted; simulator passes don’t prove the on-device audio loop.
§ 14

References

  1. Sabine, W. C. (1922). Collected Papers on Acoustics. Harvard University Press.
  2. Allen, J. B., & Berkley, D. A. (1979). Image method for efficiently simulating small-room acoustics. JASA, 65(4), 943–950.
  3. Schroeder, M. R. (1965). New method of measuring reverberation time. JASA, 37(3), 409–412.
  4. Farina, A. (2000). Simultaneous measurement of impulse response and distortion with a swept-sine technique. Audio Engineering Society Convention 108.
  5. Scheibler, R., Bezzam, E., & Dokmanić, I. (2018). Pyroomacoustics: A Python package for audio room simulation and array processing algorithms. ICASSP 2018.
  6. EBU Tech 3276. Listening conditions for the assessment of sound programme material.
  7. ITU-R BS.1116. Methods for the subjective assessment of small impairments in audio systems.
  8. BB93 — Acoustic design of schools (UK).
  9. THX Pro Cinema Certification requirements.
Want the plain-language version?

“Why your room rewrites the music you hear”

Read the Deep Read