Acouvero
Download
TECHNICAL DOC · v0.3.1

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 captured via LiDAR (RoomPlan).

Output
  • Early-reflection point localization
  • Full RT60 / EDT / C50 / C80 / D50 metrics (measured + simulated)
  • Room-mode frequency list
  • Panel layout plan (position, count, product SKU)
  • 3D visualization + reflection-energy heatmap
  • Installation-manual-grade PDF export
  • On-device measurement via the iPhone mic (sweep / clap)

Rectangular, L-, U-shaped and arbitrary simple-polygon rooms are supported; acoustic calculations use the true polygon, not its bounding box.

§ 02

Acoustic model

2.1 Sabine RT60

The classic Sabine formula:

SABINE · RT60 $$ RT_{60} = \frac{0.161 \cdot V}{\sum_i S_i \alpha_i} $$
$V$
Room volume (m³); for polygons, Shoelace area × height, minus obstacles such as columns / beams.
$\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.161$
= 24·ln(10)/c, with c = 343 m/s (20 °C).

The app ships 5 finish presets (empty / hard finish / soft furnishings / already treated / studio), each with a per-band absorption curve. 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(), validate the in-segment parameter s ∈ [0, 1], and require the reflection XY to lie inside the polygon.

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 ENERGY $$ 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 0.45 s sweep is inverse-filtered (ESS / Farina) into a single impulse, gaining ~+40 dB effective SNR
  • 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 ISO octave centers (250 / 500 / 1k / 2k / 4k) with 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 MODES $$ 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.

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:). Downstream acoustic math uses these vertices directly, not the bounding box.

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

On iOS 17+, RoomCaptureSession 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

Scanned non-rectangular rooms go 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 Free-vertex editor

FreeVertexEditorView lets the user drag vertices on a SwiftUI Canvas:

  • Long-press to delete (when vertex count ≥ 3)
  • Dragging records the last valid state in dragSnapshot; a self-intersecting new position silently rolls back
  • 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. 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 > 0 get at least 1 panel (no wall with demand is fully starved)
  • if the sum > 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 + forced 4 corner bass traps
  • 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

6.1 ESS sweep

Following Farina's method:

  • 180 Hz → 4 kHz, 0.45 s logarithmic sweep
  • Inverse filter (makeInverseSweep) = time reversal + 1/freq amplitude compensation
  • Recording via AVAudioRecorder to AAC m4a (44.1 kHz mono)
  • Read back → matched filter → analyzeImpulse
Sample-rate guard If the device's actual recording rate ≠ 44.1 kHz (USB-C audio interfaces / some Bluetooth configs), matched filtering is skipped and the UI warns that confidence is low, advising the user to disconnect external gear and re-measure.

6.2 Clap impulse

No playback — the user claps hard once and 3 s of tail is recorded. 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 sweep (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 a silent segment to compute background noise dBFS / spectrum, gauging whether measurement SNR is sufficient. DelayCalibrationService runs near-field playback + recording to determine the iPhone's own playback-to-record latency (typically 80–150 ms).

§ 07

Visualization & output

7.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.

7.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 a color-temperature layer (blue → red).

EnergyHeatmapRenderer turns the grid into a UIImage: bicubic upsampling + Magma colormap + alpha by absolute energy.

7.3 RIR convolution preview

RIRPreviewPlayer uses AVAudioEngine + AVAudioPlayerNode to convolve any dry signal (voice / drums / piano sample) 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.

7.4 PDF installation manual

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

  • Room overview + 3D snapshot
  • 6-band RT60 measured-vs-target bar chart
  • Reflection-point table + mode table + risk summary
  • Recommended-panel table + shopping-list BOM
  • Geist font + PingFangSC fallback (CoreText cascadeList, so CJK doesn't render as □□□)

7.5 Project persistence

ProjectStore (an actor) writes ~/Library/Application Support/AcouVero/projects.json: [RoomProject], each holding the full Room + speakers + listeners + recommendations + panelCountOverrides.

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.

§ 08

Engineering notes

8.1 SwiftUI + @Observable

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

AcouVeroViewModel.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
}

8.2 Actor isolation

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

8.3 AVAudio lifecycle

AudioSessionManager centralizes the session category (.playback for RIR preview, .playAndRecord for measurement). AVAudioSession.interruptionNotification listens for call / Siri interruptions and auto-stops the engine, clearing isPlaying.

8.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.

8.5 Test coverage

AllShapesAndUseCasesTests uses the Swift Testing framework to run the full matrix — every shape × every use case × various listener / speaker combinations — asserting:

  • analysis.reflectionPaths.count >= 1
  • analysis.warnings includes the approximation note for non-rectangular rooms
  • recommendation total matches the budget
  • the allocator guarantees sum ≤ budget and symmetry across budgets
  • polygon closure / vertices &lt; 3 and other edge cases
§ 09

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.
  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.
§ 10

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