Vue d'ensemble
Entrée : l'utilisateur saisit dimensions / forme / matériaux, positions des enceintes et du point d'écoute — ou la géométrie est capturée par LiDAR (RoomPlan).
- Localisation des premières réflexions
- Jeu complet RT60 / EDT / C50 / C80 / D50 (mesuré + simulé)
- Liste des fréquences modales
- Plan d'implantation des panneaux (position, nombre, SKU)
- Visualisation 3D + carte d'énergie des réflexions
- Export PDF de niveau manuel d'installation
- Mesure in situ via le micro de l'iPhone (balayage / claquement)
Pièces rectangulaires, en L, en U et tout polygone simple sont pris en charge ; les calculs utilisent le vrai polygone, pas sa boîte englobante.
Modèle acoustique
2.1 Sabine RT60
La formule de Sabine classique :
- $V$
- Volume (m³) ; pour les polygones, aire de Shoelace × hauteur, moins les obstacles (poteaux / poutres).
- $\sum_i S_i \alpha_i$
- Surface × coefficient d'absorption par face. Coefficients sur 6 bandes (125 / 250 / 500 / 1k / 2k / 4k Hz).
- $0.161$
- = 24·ln(10)/c, avec c = 343 m/s (20 °C).
L'app fournit 5 préréglages de finition (vide / dur / mobilier / déjà traité / studio), chacun avec une courbe d'absorption par bande. Le RT60 cible est aussi une courbe à 6 bandes, conforme aux normes EBU Tech 3276 / THX qui tolèrent une traîne plus longue dans le grave.
2.2 Méthode des sources images (ISM)
Les premières réflexions utilisent le miroir : on reflète la source par rapport au mur ; l'intersection du segment « source miroir → auditeur » avec le mur est le point de réflexion.
- Pièce rectangulaire : miroir sur les 6 plans (4 murs + sol + plafond) — solution analytique.
- Non rectangulaire (
PolygonReflectionCalculator) : miroir sur chaque segment deRoomPolygon.walls(), validation du paramètre s ∈ [0, 1] et point XY dans le polygone.
Les réflexions multi-ordres (1–3) sont implémentées par MultiOrderReflectionCalculator selon l'approche récursive de pyroomacoustics :
- Énumérer toutes les séquences de murs [s₁, …, sₙ], murs adjacents distincts
- Itération source-image : S₀ = source, Sₖ = mirror(Sₖ₋₁, sₖ)
- Tracé inverse depuis l'auditeur : Sₙ → auditeur coupe sₙ en Pₙ ; Sₙ₋₁ → Pₙ coupe sₙ₋₁ en Pₙ₋₁ ; et ainsi de suite
- Chaque intersection doit être dans la plage valide du mur et t ∈ (0, 1)
2.3 Énergie des trajets
Énergie large bande selon le nombre de réflexions et l'atténuation de distance :
- α est borné à [0, 0.99] pour éviter que α = 1 produise log(0) ou pow(négatif, N) = NaN
- L'atténuation suit la divergence sphérique 1/r²
- Conversion en dB via 10·log₁₀ (domaine énergétique)
L'énergie par bande est calculée pour les 6 bandes (coefficient de finishProfile.absorptionByBand).
2.4 Estimation RT60 (mesurée)
ImpulseResponseEstimator :
- Filtrage adapté : le balayage enregistré de 0,45 s est filtré en inverse (ESS / Farina) en une impulsion, gain SNR ~+40 dB
- rmsEnvelope : enveloppe RMS à fenêtre glissante
- Intégration inverse de Schroeder : E_back(t) = ∫ₜ^∞ s²(τ) dτ, normalisée en 10·log₁₀ pour la courbe EDC
- Régression linéaire de pente : T30 [−5, −35 dB] selon SNR, repli T20 [−5, −25] ou T10 [−5, −15]
- RT₆₀ = −60 / pente
RT60 par bande : sur les échantillons filtrés, passe-bande aux centres ISO (250 / 500 / 1k / 2k / 4k), Q = √2, puis Schroeder par bande.
2.5 Simulation de RIR
SimulatedRIRGenerator convertit les trajets multi-ordres en impulsions discrètes écrites dans un buffer PCM 8 kHz × 0,5 s :
for path in reflectionPaths:
t = path.totalDistance / speedOfSound
amp = 10^(path.relativeEnergyDB / 20)
impulse[round(t * sampleRate)] += amp
Ajouter l'impulsion du son direct, puis intégration inverse de Schroeder pour EDT / RT60 / C50 / C80 / D50.
2.6 Modes propres
Fréquences modales d'une pièce rectangulaire :
Classés selon le nombre de nₓ, n_y, n_z non nuls : 1 = axial, 2 = tangentiel, 3 = oblique.
Les pièces non rectangulaires estiment les modes via la boîte englobante (les modes exacts exigent un FEM) ; indiqué dans l'UI.
Système géométrique
3.1 Formes prises en charge
- Rectangle — largeur × longueur × hauteur
- Forme en L — retire une encoche nw × nl à un coin choisi
- Forme en U — retire une encoche nw × nl au milieu d'un mur
- Personnalisé — tout polygone simple (≥ 3, ≤ 24 sommets)
Toutes les formes non rectangulaires deviennent une liste de sommets CCW via RoomPolygon.from(shape:, bboxWidth:, bboxLength:). Les calculs utilisent ces sommets, pas la boîte englobante.
3.2 Outils de géométrie polygonale
- Aire — formule de Shoelace
- Périmètre — somme des longueurs d'arêtes
- contains(x, y) — lancer de rayon + epsilon de bord (évite les faux négatifs aux coins)
- clamped(point) — les rectangles utilisent [0, w] × [0, l] ; les autres utilisent le vrai [minₓ, maxₓ] × [min_y, max_y] de
polygon.boundingBox, testent polygon.contains, et dichotomie vers le centre si hors polygone - walls() — découpe le polygone en segments classés par normale intérieure (leftWall / rightWall / frontWall / backWall), réutilisant le recommandeur rectangulaire
- PolygonValidator.isSimple — valide un polygone simple non auto-sécant
3.3 Numérisation RoomPlan
Sur iOS 17+, RoomCaptureSession est utilisé et CapturedRoomMapper mappe le scan en ScannedRoom (wallSegments + openings + furniture). Mappage des coordonnées :
Les pièces non rectangulaires scannées passent par RectangularRoomSimplifier.polygonShapeFromScan, chaînant les wallSegments en sommets. Les extrémités RoomPlan ont une erreur de 3–8 cm ; une boucle de 12 segments peut cumuler 5 cm, d'où un epsilon de fermeture de 12 cm. Si la boucle ne ferme pas, repli sur la boîte englobante et invitation à rescanner.
3.4 Éditeur de sommets libres
FreeVertexEditorView permet de glisser les sommets sur un Canvas SwiftUI :
- Appui long pour supprimer (si sommets ≥ 3)
- Le glissement enregistre le dernier état valide dans
dragSnapshot; une position auto-sécante revient en arrière silencieusement - Accrochage à la grille de 0,05 m
- Une marge max(0,5, bbox × 0,2) m hors bbox permet de tirer les sommets au-delà de la bbox (la vue parente agrandit room.width / length)
Zones d'écoute & multi-auditeur
AcousticConfiguration.listeners gère 1..N positions d'écoute. L'analyse exécute la méthode des images par auditeur et fusionne les points dans un tableau (l'auditeur principal d'abord). Le recommandeur pondère chaque mur par son nombre de points (cap = max(6, speakerCount × listenerCount)) ; plusieurs sièges partageant un mur le poussent vers le plafond du budget.
- En 3D, les auditeurs secondaires : opacité 0,45 + échelle 0,75
- Vue de dessus (RoomPlanView) : points primaires pleine couleur r=5, secondaires opacité 0,45 r=3
- La liste (RecommendationView) n'affiche que les points primaires ; note de bas de page « + N réflexions déjà comptées »
Réflexions multi-ordres / RIR / carte sont calculées une fois pour l'auditeur principal (O(speakers × surfaces³) par auditeur est trop lourd, et le RIR est ponctuel).
Moteur de recommandation
PanelPlacementRecommender produit [PanelRecommendation], chacun avec : position (mur gauche / droit / plafond / avant / arrière / coin), nombre, priorité (essentiel / fort / recommandé / optionnel), effectivenessMultiplier, targetReflectionPointId.
5.1 Répartition proportionnelle (plus fort reste)
L'ancien add() glouton affamait des murs : le mur gauche vidait le budget → mur droit à 0 ; budget serré, plafond / avant / arrière affamés. La nouvelle version : méthode du plus fort reste :
- Calculer le « raw desired » par mur (nombre de points + profondeur + cas d'usage)
- floor(raw × budget / total) comme allocation initiale
- les murs avec raw > 0 reçoivent ≥ 1 panneau (aucun mur en demande n'est affamé)
- si somme > budget, retrancher en partant du plus petit reste fractionnaire
- passe de symétrie G/D : si raw[0] == raw[1], forcer alloc[0] == alloc[1]
- redistribuer le reliquat selon la plus grande fraction
5.2 Modèles par cas d'usage
Sans enceintes placées, des modèles spécifiques s'appliquent :
- Salle de réunion : plafond 35 % + mur arrière 20 % + chaque mur latéral 12 %
- Podcast : devant le micro 25 % + chaque côté 18 % + plafond 20 %
- Studio : amortissement symétrique 4 murs + 4 pièges à graves d'angle forcés
- Écoute générale : base symétrique + invitation à ajouter des enceintes
5.3 Les nécessités physiques contournent le budget
Les pièges à graves d'angle sont forcés à 4 quand les modes 80–150 Hz sont denses, contournant le plafond totalPanel. Raison : la pression est maximale aux angles et les pièges y sont ~1,5× plus efficaces — une nécessité physique.
5.4 Pondération proche auditeur
Une passe finale marque les recommandations à moins d'1,5 m d'un auditeur en .nearListener, effectivenessMultiplier ×1,15, et l'indique dans le titre / la raison.
Mesure
6.1 Balayage ESS
D'après la méthode de Farina :
- Balayage log 180 Hz → 4 kHz, 0,45 s
- Filtre inverse (
makeInverseSweep) = inversion temporelle + compensation 1/freq - Enregistrement via AVAudioRecorder en AAC m4a (44,1 kHz mono)
- Relecture → filtrage adapté → analyzeImpulse
6.2 Impulsion par claquement
Sans lecture — l'utilisateur claque fort une fois, 3 s de traîne enregistrées. Directement vers analyzeImpulse (pas de filtre adapté ; un claquement est déjà une impulsion étroite). Couvre la bande 125 Hz que le balayage (dès 180 Hz) rate.
6.3 Mesuré vs simulé
MeasuredVsSimulatedCard récupère le dernier RT60 mesuré et affiche l'écart avec le RT60 simulé :
6.4 Bruit ambiant & calibrage
AmbientNoiseEstimator enregistre un segment silencieux pour le bruit de fond dBFS / spectre, évaluant le SNR. DelayCalibrationService fait lecture + enregistrement en champ proche pour la latence lecture-enregistrement de l'iPhone (80–150 ms).
Visualisation & export
7.1 Vue 3D
Room3DController rend via SceneKit : murs / sol / plafond / enceintes (caisson fin + repère + pastille canal) / auditeur (glyphe FE + tige) / obstacles (SCNBox translucide) / panneaux recommandés / segments de trajets multi-ordres / superposition de carte d'énergie.
7.2 Carte d'énergie des réflexions
ReflectionEnergyMapGenerator accumule l'énergie de chaque trajet sur la grille uCells × vCells du mur (12×6 par défaut). Une carte par mur, superposée en 3D (bleu → rouge).
EnergyHeatmapRenderer convertit la grille en UIImage : suréchantillonnage bicubique + colormap Magma + alpha selon l'énergie absolue.
7.3 Aperçu par convolution RIR
RIRPreviewPlayer (AVAudioEngine + AVAudioPlayerNode) convolue tout signal sec (voix / batterie / piano) avec le RIR simulé pour l'écoute « non traité vs traité ».
La convolution temporelle RIRConvolver.convolve(dry:rir:) est directe O(N × M) (N = échantillons secs, M = RIR), normalisée à 0,9 pour éviter l'écrêtage.
7.4 Manuel d'installation PDF
ReportExporter rend un PDF A4 (595 × 842 pt) via UIGraphicsPDFRenderer :
- Aperçu de la pièce + capture 3D
- Histogramme RT60 6 bandes mesuré vs cible
- Tableau des points + tableau des modes + résumé des risques
- Tableau des panneaux + nomenclature (BOM)
- Police Geist + repli PingFangSC (CoreText cascadeList, pour éviter les □□□ en CJK)
7.5 Persistance des projets
ProjectStore (actor) écrit ~/Library/Application Support/AcouVero/projects.json : [RoomProject], chacun avec Room + enceintes + auditeurs + recommandations + panelCountOverrides.
MeasurementStore (actor) écrit measurements.json : [MeasurementResult], lié au projet par roomId.
Les deux ont une récupération de décodage par élément (wrapper Throwable<T>) — un enregistrement corrompu ne fait pas échouer toute la liste.
Notes d'ingénierie
8.1 SwiftUI + @Observable
Le ViewModel (AcouVeroViewModel) utilise la macro @Observable de Swift 5.9 — champs observés directement, sans Combine. Les résultats AcousticAnalysis sont mis en cache par comparaison Equatable de AcousticConfiguration :
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 Isolation par acteur
ProjectStore, MeasurementStore et RoomPlanService sont des actor, sérialisant l'IO / l'async. L'UI utilise async / await ; le thread principal n'est jamais bloqué par l'IO disque.
8.3 Cycle de vie AVAudio
AudioSessionManager centralise la catégorie de session (.playback pour l'aperçu RIR, .playAndRecord pour la mesure). AVAudioSession.interruptionNotification écoute appels / Siri et arrête le moteur, réinitialisant isPlaying.
8.4 Annulation de tâche
La lecture par convolution est une tâche de fond ; les appuis rapides déclenchent pendingPlayTask?.cancel() + double vérif Task.isCancelled pour éviter des écritures MainActor désordonnées.
8.5 Couverture de tests
AllShapesAndUseCasesTests (framework Swift Testing) exécute la matrice complète — chaque forme × cas d'usage × combinaisons auditeur / enceinte — en vérifiant :
analysis.reflectionPaths.count >= 1analysis.warningscontient la note d'approximation pour les pièces non rectangulaires- le total des recommandations correspond au budget
- l'allocateur garantit sum ≤ budget et la symétrie
- fermeture du polygone / sommets < 3 et autres cas limites
Limites connues
- Multi-ordre en pièce non rectangulaire = boîte englobante — le risque flutter / slapback d'ordre 2-3 près des angles rentrants peut dévier. Les points d'ordre 1 utilisent le vrai polygone.
- Modes en pièce non rectangulaire estimés via boîte englobante — modes exacts = FEM.
- Mesure au micro intégré de l'iPhone — non calibré ; réponse / sensibilité non plates. Fiabilité toujours
.low, en recoupement de l'estimation géométrique. - Hypothèse ISM source unique — ignore diffraction du caisson, directivité du pavillon, etc.
- Pas de modélisation des pièges à graves — les problèmes sous 80 Hz sortent du modèle de Sabine et exigent un traitement dédié.
- Précision du scan AR — extrémités RoomPlan à 3-8 cm près ; boucle de 12 segments peut cumuler 5+ cm. Épaisseur des murs non modélisée.
Références
- Sabine, W. C. (1922). Collected Papers on Acoustics. Harvard University Press.
- Allen, J. B., & Berkley, D. A. (1979). Image method for efficiently simulating small-room acoustics. JASA, 65(4), 943–950.
- Schroeder, M. R. (1965). New method of measuring reverberation time. JASA, 37(3), 409–412.
- Farina, A. (2000). Simultaneous measurement of impulse response and distortion with a swept-sine technique. Audio Engineering Society Convention 108.
- Scheibler, R., Bezzam, E., & Dokmanić, I. (2018). Pyroomacoustics: A Python package for audio room simulation and array processing algorithms. ICASSP 2018.
- EBU Tech 3276. Listening conditions for the assessment of sound programme material.
- ITU-R BS.1116. Methods for the subjective assessment of small impairments in audio systems.
- BB93 — Acoustic design of schools (UK).
- THX Pro Cinema Certification requirements.