Skip to main content

Room modes in JavaScript: finding every standing wave in a rectangular room

Updated 6 min readMarkdown

Every rectangular room has a set of bass frequencies it amplifies and others it swallows. These are its standing waves, or room modes, and they explain why the bass booms in one seat and almost disappears in another. For a rectangular room the maths is a closed-form formula, which makes it a nice small programming exercise. This is the core of the Room Mode Calculator I built for The Audio Stuff, reduced to a few plain JavaScript functions.

The formula

For a rectangular room with length L, width W and height H (in metres), each mode is identified by three whole numbers (p, q, r), and its frequency is:

f = (c / 2) · √( (p/L)² + (q/W)² + (r/H)² )

Here c is the speed of sound, about 343 m/s at 20 °C. It rises by roughly 0.6 m/s for every degree Celsius, so every mode moves up by about 0.17% per degree.

Modes come in three families, depending on how many of p, q and r are non-zero:

  • Axial (one non-zero): a wave bouncing between two opposite surfaces. These are the strongest.
  • Tangential (two non-zero): a wave travelling around four surfaces. A common rule of thumb puts them about 3 dB weaker than axial modes.
  • Oblique (all three non-zero): a wave involving all six surfaces, about 6 dB weaker.

1. Enumerate the modes

Loop over p, q and r up to the largest index that can still fall below your frequency limit. Along the length, mode p has frequency p·c/2L, so the largest useful index is floor(2·L·fMax / c).

function roomModes(L, W, H, fMax = 300, tempC = 20) {
  const c = 331.3 + 0.606 * tempC;            // speed of sound, m/s
  const max = d => Math.floor((2 * d * fMax) / c);
  const modes = [];

  for (let p = 0; p <= max(L); p++)
    for (let q = 0; q <= max(W); q++)
      for (let r = 0; r <= max(H); r++) {
        if (p + q + r === 0) continue;
        const f = (c / 2) * Math.hypot(p / L, q / W, r / H);
        if (f > fMax) continue;
        const nonZero = (p > 0) + (q > 0) + (r > 0);
        const type = ["", "axial", "tangential", "oblique"][nonZero];
        modes.push({ f, p, q, r, type });
      }

  return modes.sort((a, b) => a.f - b.f);
}

For a typical living room of 5.5 × 4.2 × 2.7 m, this gives 239 modes below 300 Hz. The lowest ones are:

Frequency (p, q, r) Type
31.2 Hz (1, 0, 0) axial, length
40.9 Hz (0, 1, 0) axial, width
51.4 Hz (1, 1, 0) tangential
62.4 Hz (2, 0, 0) axial, length
63.6 Hz (0, 0, 1) axial, height
70.8 Hz (1, 0, 1) tangential

2. Find where the modes stop mattering

Mode density rises quickly with frequency. Above a certain point the modes overlap so densely that the room behaves statistically, and you hear reverberation rather than individual resonances. That point is the Schroeder frequency:

const schroeder = (rt60, volume) => 2000 * Math.sqrt(rt60 / volume);

The living room above has a volume of 62.4 m³. With a reverberation time of 0.5 s, its Schroeder frequency is about 179 Hz, so only the modes below that are worth reporting individually.

3. Flag modes that pile up

One mode on its own is a moderate bump. Two strong modes within a few percent of each other add up to an obvious boom. In the example room, the second length mode (62.4 Hz) and the first height mode (63.6 Hz) are less than 2% apart, which is a likely boom at about 63 Hz.

function coincidences(modes, fLimit, tolerance = 0.03) {
  const strong = modes.filter(m => m.type !== "oblique" && m.f < fLimit);
  const out = [];
  for (let i = 0; i < strong.length - 1; i++) {
    const a = strong[i], b = strong[i + 1];
    if ((b.f - a.f) / a.f < tolerance) out.push([a, b]);
  }
  return out;
}

A classic overall check is the Bonello criterion: count the modes in each third-octave band, and the count should never fall as frequency rises. A band with fewer modes than the band below it suggests a gap you will hear as uneven bass.

4. Pressure at the listening position

Each mode has a fixed pattern of loud spots (antinodes) and quiet spots (nodes). The relative pressure of a mode at a point (x, y, z) is the product of three cosines:

function pressureAt(m, L, W, H, x, y, z) {
  return Math.cos((m.p * Math.PI * x) / L) *
         Math.cos((m.q * Math.PI * y) / W) *
         Math.cos((m.r * Math.PI * z) / H);
}

A value near 0 means you sit in a null for that mode, and ±1 means you sit at a peak. Every mode has an antinode in the corners, which is why bass traps go there.

This explains a common mistake: sitting exactly halfway down the room. In the example room, the middle of the length is a perfect null for the 31.2 Hz mode (pressure 0) and a full peak for the 62.4 Hz mode (pressure −1). The often-quoted 38% position avoids the nulls of the first three length modes, which is why it is a popular starting point for the listening seat.

5. What this model leaves out

The formula assumes perfectly rigid walls and a perfect box. Real walls flex and absorb some bass, so measured peaks are lower and broader than the model predicts, and the frequencies can shift a little. L-shaped, round and vaulted rooms have no closed-form solution and need a numerical method, such as the finite-element solver the full calculator uses. As a guide to where your seat and your bass traps should go, though, the rectangular model is very useful.

Try it

The finished Room Mode Calculator is free and runs in the browser. It adds a 3D pressure map, a room-quality score, and playback of each mode as a sine tone. My other free audio tools, including an acoustic panel calculator and a speaker test, are at theaudiostuff.com/tools/.

Jakub Charkiewicz, The Audio Stuff

About the author

  • Independent audio review site from Poland, with free browser tools: ABX blind test, EQ ear training, tone generator, speaker test and audio calculators. https://theaudiostuff.com/

No comments yet. Be the first to comment.