Resources Numerical accuracy Calculation policy

Room reverberation time

Works out how long sound takes to die away in each room, per octave band, from the room's volume, its surfaces and the air.

How the precision is kept

The arithmetic is 64-bit throughout, on areas in m² and volumes in m³. The accuracy is set by the statistical formulas (Sabine and Eyring), by tabulated absorption coefficients of two decimals, and by constants written with three or four digits (0.161, 4.343, 8.686). Each room is treated as a box of its floor area and one height.

Open item. Replace the literal 4.343 in airM with 10 ÷ ln 10 and write 0.161 from its definition, 24 ln 10 ÷ c.

Calculations under this policy

11 calculations in 2 source files. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.

acoustics.js BimCraft 0.8.1 64-bit 5 calculations Reviewed

Tests tools/bimcraft-test/acoustics-test.mjs, tools/acoustics-test/test.mjs

Sabine constant

64-bit

The constant both reverberation formulas multiply the volume by.

C = 0.161 s/m

A three-digit literal. The comment gives its origin as 24 ln(10) ÷ c at 20 °C; with c = 343 m/s (the value acoustics-ir.js uses) that is 0.16111, so the literal is about 0.07 % lower.

  • acoustics.js
  • line 24
  • BimCraft 0.8.1
const C = 0.161;   // 24 ln(10) / c at 20 °C: the constant of both formulas

Sound absorbed by the air

64-bit

The attenuation of a pure tone in air, in dB per meter, at the room's temperature, humidity and pressure.

α = 8.686 f² [1.84e-11 (p_r ÷ p) √(T ÷ T₀) + (T ÷ T₀)^−2.5 (0.01275 e^(−2239.1 ÷ T) ÷ (f_rO + f² ÷ f_rO) + 0.1068 e^(−3352 ÷ T) ÷ (f_rN + f² ÷ f_rN))]

One 64-bit expression built from Math.pow, Math.exp and Math.sqrt. Its coefficients are decimal literals of three to five digits (8.686, 1.84e-11, 0.01275, 0.1068, 2239.1, 3352, 6.8346, 4.6151). A pressure of zero is not guarded: it is a divisor.

  • acoustics.js
  • lines 57 to 64
  • BimCraft 0.8.1
/** ISO 9613-1 pure-tone attenuation in dB per metre (temperature °C, relative humidity %, pressure kPa). */
export function airAbsorption(f, { T = 20, RH = 50, p = 101.325 } = {}) {
  const Tk = T + 273.15, T0 = 293.15, pr = 101.325, T01 = 273.16;
  const Csat = -6.8346 * Math.pow(T01 / Tk, 1.261) + 4.6151, psat = pr * Math.pow(10, Csat), h = RH * (psat / p);   // molar concentration of water vapour, %
  const frO = (p / pr) * (24 + 4.04e4 * h * (0.02 + h) / (0.391 + h));
  const frN = (p / pr) * Math.pow(Tk / T0, -0.5) * (9 + 280 * h * Math.exp(-4.170 * (Math.pow(Tk / T0, -1 / 3) - 1)));
  return 8.686 * f * f * (1.84e-11 * (pr / p) * Math.sqrt(Tk / T0) + Math.pow(Tk / T0, -2.5) * (0.01275 * Math.exp(-2239.1 / Tk) / (frO + f * f / frO) + 0.1068 * Math.exp(-3352 / Tk) / (frN + f * f / frN)));
}

Air absorption per meter

64-bit

Turns the dB figure into the energy absorption coefficient the formulas use.

m = α ÷ 4.343

4.343 stands for 10 log₁₀(e) = 4.3429448…. The literal is higher by about 1.3 parts in 100,000, so m is lower by the same share.

  • acoustics.js
  • lines 65 to 66
  • BimCraft 0.8.1
/** Energy absorption of the air in 1/m: the dB figure over 10 log10(e). */
export const airM = (f, env = {}) => airAbsorption(f, env) / 4.343;

Reverberation time per band

64-bit

The Sabine and Eyring times of a room in each of six octave bands, and the mid-frequency mean.

A = Σ Sᵢαᵢ + extra; ᾱ = min(0.999, A ÷ S); Sabine = C V ÷ (A + 4mV); Eyring = C V ÷ (−S ln(1 − ᾱ) + 4mV)

Areas and absorption are summed in list order in 64-bit. The answer T switches from Sabine to Eyring where ᾱ passes 0.2, so T has a step there. ln(1 − ᾱ) is taken with Math.log, not log1p; ᾱ is capped at 0.999 so the logarithm stays finite.

  • acoustics.js
  • lines 68 to 88
  • BimCraft 0.8.1
export function reverb(room, env = {}) {
  const e = { T: 20, RH: 50, p: 101.325, air: true, ...env }, V = Number(room.volume_m3), notes = [];
  const surfaces = (room.surfaces || []).map((s) => ({ ...s, alpha: s.alpha || MATERIALS[s.key]?.alpha })).filter((s) => s.area_m2 > 0);
  if (!(V > 0) || !surfaces.length) return { ok: false, reason: 'noroom', notes };
  for (const s of surfaces) if (!s.alpha) return { ok: false, reason: 'nomaterial', notes, key: s.key };
  const S = surfaces.reduce((a, s) => a + s.area_m2, 0);
  const bands = BANDS.map((f, i) => {
    const sa = surfaces.reduce((a, s) => a + s.area_m2 * s.alpha[i], 0) + (room.extra || []).reduce((a, x) => a + (x.a?.[i] || 0), 0);
    const alpha = Math.min(0.999, sa / S), air = e.air ? 4 * airM(f, e) * V : 0;
    return { f, A: sa, alpha, air, sabine: C * V / (sa + air), eyring: C * V / (-S * Math.log(1 - alpha) + air) };
  });
  for (const b of bands) b.T = b.alpha > 0.2 ? b.eyring : b.sabine;
  const mean = bands.reduce((a, b) => a + b.alpha, 0) / bands.length;
  const mid = { sabine: (bands[2].sabine + bands[3].sabine) / 2, eyring: (bands[2].eyring + bands[3].eyring) / 2, T: (bands[2].T + bands[3].T) / 2 };
  const dims = Math.cbrt(V);
  if (V < 30) notes.push({ key: 'ac.note.small', V: Math.round(V) });
  if (Number(room.aspect) > 3) notes.push({ key: 'ac.note.aspect', ratio: Math.round(room.aspect * 10) / 10 });
  if (bands.some((b) => b.alpha > 0.2)) notes.push({ key: 'ac.note.dead' });
  if (room.uneven) notes.push({ key: 'ac.note.uneven' });
  return { ok: true, volume: V, area: S, bands, mean, mid, meanFreePath: 4 * V / S, dims, notes };
}

Reverberation over the target

64-bit

The finding raised when a room's mid-frequency time is over the person's target, with the absorption that would meet it.

T = round(T_mid × 100) ÷ 100; need = round(max(0, V C ÷ target − (A₅₀₀ + A₁₀₀₀) ÷ 2))

The time is rounded to 0.01 s before it is compared with the target (plus 1e-9 s) and before it is stored in the finding. The extra absorption is rounded to a whole m² and comes from Sabine's formula without the air term.

  • acoustics.js
  • lines 90 to 105
  • BimCraft 0.8.1
/** Findings as shared Problems (rule 29): the mid-band reverberation time over the person's own target. */
export function rtProblems(rooms, { target = null, t = null } = {}) {
  if (!(target > 0)) return [];
  const out = [], say = (key, vars, en) => (t ? t(key, vars) : en);
  for (const r of rooms) {
    if (!r.res?.ok) continue;
    const T = Math.round(r.res.mid.T * 100) / 100;
    if (T > target + 1e-9) {
      const need = Math.round(Math.max(0, r.res.volume * C / target - (r.res.bands[2].A + r.res.bands[3].A) / 2));   // m² sabins more absorption for the target (Sabine)
      out.push({ code: 'acoustics.rt', severity: 'warning', msg: { key: 'ac.p.rt', vars: { room: r.name, t: T, target, a: need } },
        text: say('ac.p.rt', { room: r.name, t: T, target, a: need }, r.name + ': reverberation ' + T + ' s at mid frequencies, over your ' + target + ' s target. About ' + need + ' m² more absorption would bring it down.'),
        req: { op: '<=', value: target, unit: 's' }, found: { value: T, unit: 's' }, source: { kind: 'engine', name: 'bimcraft.acoustics' }, where: r.at ? { space: '2d', at: r.at } : undefined });
    }
  }
  return out;
}
acoustics-model.js BimCraft 0.8.1 64-bit 6 calculations Reviewed

Tests tools/bimcraft-test/acoustics-test.mjs

Distance from a point to a room outline

64-bit

How far an opening's middle is from the room's outline, to decide which room the opening belongs to.

d = min over edges of |P − (A + t (B − A))|, t clamped to 0…1

64-bit, in plan millimeters. The projection parameter is clamped and a zero-length edge is guarded. Lengths use Math.hypot.

  • acoustics-model.js
  • lines 24 to 32
  • BimCraft 0.8.1
function ringDist(poly, x, y) {
  let d = Infinity;
  for (let i = 0; i < poly.length; i++) {
    const a = poly[i], b = poly[(i + 1) % poly.length], dx = b[0] - a[0], dy = b[1] - a[1], l2 = dx * dx + dy * dy;
    const t = l2 ? Math.max(0, Math.min(1, ((x - a[0]) * dx + (y - a[1]) * dy) / l2)) : 0;
    d = Math.min(d, Math.hypot(x - a[0] - dx * t, y - a[1] - dy * t));
  }
  return d;
}

Room perimeter

64-bit

The length of the room's outline, used for the wall area when the walls' finishes are not read from the model.

P = Σ |pᵢ₊₁ − pᵢ|

Edge lengths from Math.hypot, added in order in 64-bit, in millimeters.

  • acoustics-model.js
  • line 33
  • BimCraft 0.8.1
const perimeter = (p) => { let s = 0; for (let i = 0; i < p.length; i++) s += Math.hypot(p[(i + 1) % p.length][0] - p[i][0], p[(i + 1) % p.length][1] - p[i][1]); return s; };

Is a point inside a room

64-bit

Decides whether a ceiling, a floor or a piece of furniture lies in a room.

A crossing-number test in 64-bit with no tolerance: a point exactly on an edge can fall either way. The division by the edge's height is reached only when the edge crosses the point's level, so it is not zero.

  • acoustics-model.js
  • line 34
  • BimCraft 0.8.1
const inPoly = (poly, x, y) => { let c = false; for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) { const a = poly[i], b = poly[j]; if ((a[1] > y) !== (b[1] > y) && x < ((b[0] - a[0]) * (y - a[1])) / (b[1] - a[1]) + a[0]) c = !c; } return c; };

Room height and opening areas

64-bit

The height the room is given and the area of the doors and windows in its walls.

H = height ÷ 1000; opening area = (width ÷ 1000) × (height ÷ 1000)

Millimeters are divided by 1000 before the product, in 64-bit. The height is a ceiling's when one over 1500 mm is drawn in the room, else the lowest wall on the level, else the level height or 2700 mm. An opening counts for a room when its middle is within half the wall thickness + 60 mm of the outline; its position divides by max(1, wall length).

  • acoustics-model.js
  • lines 69 to 89
  • BimCraft 0.8.1
const hmm = ceil ? ceil.height : (mine.length ? Math.min(...mine.map((x) => wallHeight(x, levels))) : lv.height || 2700), H = hmm / 1000;
const mine2 = settings.rooms?.[r.id] || {};
// 0.8 EG.49 (bim-sim_PART_3): what the model says is on each surface; the person's pick for a room wins, then the model, then the default
const src = { floor: 'default', wall: 'default', ceiling: 'default' };
const set = { ...finish };
if (fromModel) {
  const fl = floors.find((f) => f.storey === lv.id && f.seed && inPoly(r.poly, f.seed[0], f.seed[1])) || floors.find((f) => f.storey === lv.id && f.mode === 'building');
  if (fl && !settings.defaultsOnly) { set.floor = fl.kind === 'concrete' ? 'concrete' : 'wood-floor'; src.floor = 'model'; }
  if (ceil) { set.ceiling = ceil.pattern ? 'ceiling-tile' : 'ceiling-board'; src.ceiling = 'model'; }
}
for (const k of ['floor', 'wall', 'ceiling']) if (mine2[k]) { set[k] = mine2[k]; src[k] = 'picked'; }
const floor = r.area_m2, per = perimeter(r.poly) / 1000;
let glass = 0, door = 0;
for (const o of ops) {
  const wall = byGuid.get(o.wallGuid);
  if (!wall || wall.storey !== lv.id) continue;
  const [ax, ay, bx, by] = [wall.a[0], wall.a[1], wall.b[0], wall.b[1]], t = (o.offset + o.width / 2) / Math.max(1, wall.len), x = ax + (bx - ax) * t, y = ay + (by - ay) * t;
  if (ringDist(r.poly, x, y) > wall.t / 2 + 60) continue;
  const a = (o.width / 1000) * (o.height / 1000);
  if (o.kind === 'window') glass += a; else door += a;
}

Wall area per finish

64-bit

The area of wall each finish covers in a room, less its doors and windows.

wall area = Σ edge length × H; net = max(0, gross − glass − doors); each finish × net ÷ gross

Edge lengths (Math.hypot ÷ 1000) times the height are summed per finish. Doors and windows are taken off by scaling every finish by net ÷ gross, not from the wall they sit in. The net area is clamped at zero.

  • acoustics-model.js
  • lines 91 to 103
  • BimCraft 0.8.1
const wallArea = new Map();
if (fromModel && !mine2.wall) {
  for (let i = 0; i < r.poly.length; i++) {
    const p0 = r.poly[i], p1 = r.poly[(i + 1) % r.poly.length], mx = (p0[0] + p1[0]) / 2, my = (p0[1] + p1[1]) / 2, len = Math.hypot(p1[0] - p0[0], p1[1] - p0[1]) / 1000;
    let best = null, bd = Infinity;
    for (const wl of mine) { const d = segDist(mx, my, wl.a, wl.b); if (d < bd && d <= wl.t / 2 + 80) { bd = d; best = wl; } }
    const key = (best && wallFinish(best)) || set.wall;
    wallArea.set(key, (wallArea.get(key) || 0) + len * H);
  }
  if ([...wallArea.keys()].some((k) => k !== finish.wall) || wallArea.size > 1) src.wall = 'model';
} else wallArea.set(set.wall, per * H);
const grossWalls = [...wallArea.values()].reduce((s2, v) => s2 + v, 0), netWalls = Math.max(0, grossWalls - glass - door), keep = grossWalls > 0 ? netWalls / grossWalls : 0;
const surfaces = [{ name: 'floor', key: set.floor, area_m2: floor }, { name: 'ceiling', key: set.ceiling, area_m2: floor }];

Room volume and the figures reported

64-bit

The volume passed to the formulas and the area, height and volume listed for each room.

V = floor area × H; aspect = longest ÷ max(shortest, 0.1)

The volume passed to the reverberation formulas is the unrounded product. The area, height and volume listed with the room are rounded with Math.round to 0.01 m², 0.01 m and 0.1 m³, and the room's center to whole millimeters.

  • acoustics-model.js
  • lines 118 to 124
  • BimCraft 0.8.1
const xs = r.poly.map((p) => p[0]), ys = r.poly.map((p) => p[1]), dims = [(Math.max(...xs) - Math.min(...xs)) / 1000, (Math.max(...ys) - Math.min(...ys)) / 1000, H].sort((a, b) => a - b);
const spec = { volume_m3: floor * H, surfaces, extra, aspect: dims[2] / Math.max(dims[0], 0.1), uneven: false };
const unknown = surfaces.find((s) => !MATERIALS[s.key]);
out.push({ id: r.id, name: r.name, level: r.level, levelName: r.levelName, area_m2: Math.round(floor * 100) / 100, height_m: Math.round(H * 100) / 100, volume_m3: Math.round(floor * H * 10) / 10,
  finishSource: src, people, at: (r.centroid || [(Math.min(...xs) + Math.max(...xs)) / 2, (Math.min(...ys) + Math.max(...ys)) / 2]).map(Math.round), poly: r.poly, set, spec, surfaces,
  res: unknown ? { ok: false, reason: 'nomaterial', key: unknown.key } : reverb(spec, settings.env || {}),
  box: { lx: (Math.max(...xs) - Math.min(...xs)) / 1000, ly: (Math.max(...ys) - Math.min(...ys)) / 1000, h: H } });

In detail

Method
Sabine T = 0.161 V ÷ (A + 4mV) and Eyring T = 0.161 V ÷ (−S ln(1 − ᾱ) + 4mV); the answer per band is Eyring when ᾱ is over 0.2, else Sabine. Air absorption m from the ISO 9613-1 formula, as the code's comments cite it.
Number types
64-bit numbers; plain arrays of six band values.
Tolerances
Mean absorption capped at 0.999. An opening belongs to a room when its middle is within half the wall thickness + 60 mm of the room outline; a room edge takes the finish of a wall within half its thickness + 80 mm. A finding compares the time rounded to 0.01 s with the target + 1e-9 s.
Limits
A diffuse sound field is assumed. Rooms are boxes of one height. The area, height and volume reported with a room are rounded to 0.01 m², 0.01 m and 0.1 m³; the calculation uses the unrounded values.

Approach: 64-bit, stated limit

The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.

Every policy that takes this approach