<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[The Audio Stuff]]></title><description><![CDATA[The Audio Stuff]]></description><link>https://theaudiostuff.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 30 Oct 2026 08:32:43 GMT</lastBuildDate><atom:link href="https://theaudiostuff.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Room modes in JavaScript: finding every standing wave in a rectangular room]]></title><description><![CDATA[Compute every standing wave in a rectangular room in JavaScript: mode frequencies, Schroeder frequency, mode pile-ups and pressure at your seat.]]></description><link>https://theaudiostuff.hashnode.dev/room-modes-in-javascript-finding-every-standing-wave-in-a-rectangular-room</link><guid isPermaLink="true">https://theaudiostuff.hashnode.dev/room-modes-in-javascript-finding-every-standing-wave-in-a-rectangular-room</guid><category><![CDATA[JavaScript]]></category><category><![CDATA[audio]]></category><category><![CDATA[#Acoustics]]></category><dc:creator><![CDATA[The Audio Stuff]]></dc:creator><pubDate>Mon, 26 Oct 2026 12:50:03 GMT</pubDate><content:encoded><![CDATA[<p>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 <a href="https://theaudiostuff.com/tools/room-mode-calculator/">Room Mode Calculator</a> I built for The Audio Stuff, reduced to a few plain JavaScript functions.</p>
<h2>The formula</h2>
<p>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:</p>
<pre><code class="language-plaintext">f = (c / 2) · √( (p/L)² + (q/W)² + (r/H)² )
</code></pre>
<p>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.</p>
<p>Modes come in three families, depending on how many of p, q and r are non-zero:</p>
<ul>
<li><strong>Axial</strong> (one non-zero): a wave bouncing between two opposite surfaces. These are the strongest.</li>
<li><strong>Tangential</strong> (two non-zero): a wave travelling around four surfaces. A common rule of thumb puts them about 3 dB weaker than axial modes.</li>
<li><strong>Oblique</strong> (all three non-zero): a wave involving all six surfaces, about 6 dB weaker.</li>
</ul>
<h2>1. Enumerate the modes</h2>
<p>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).</p>
<pre><code class="language-js">function roomModes(L, W, H, fMax = 300, tempC = 20) {
  const c = 331.3 + 0.606 * tempC;            // speed of sound, m/s
  const max = d =&gt; Math.floor((2 * d * fMax) / c);
  const modes = [];

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

  return modes.sort((a, b) =&gt; a.f - b.f);
}
</code></pre>
<p>For a typical living room of 5.5 × 4.2 × 2.7 m, this gives 239 modes below 300 Hz. The lowest ones are:</p>
<table>
<thead>
<tr>
<th>Frequency</th>
<th>(p, q, r)</th>
<th>Type</th>
</tr>
</thead>
<tbody><tr>
<td>31.2 Hz</td>
<td>(1, 0, 0)</td>
<td>axial, length</td>
</tr>
<tr>
<td>40.9 Hz</td>
<td>(0, 1, 0)</td>
<td>axial, width</td>
</tr>
<tr>
<td>51.4 Hz</td>
<td>(1, 1, 0)</td>
<td>tangential</td>
</tr>
<tr>
<td>62.4 Hz</td>
<td>(2, 0, 0)</td>
<td>axial, length</td>
</tr>
<tr>
<td>63.6 Hz</td>
<td>(0, 0, 1)</td>
<td>axial, height</td>
</tr>
<tr>
<td>70.8 Hz</td>
<td>(1, 0, 1)</td>
<td>tangential</td>
</tr>
</tbody></table>
<h2>2. Find where the modes stop mattering</h2>
<p>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:</p>
<pre><code class="language-js">const schroeder = (rt60, volume) =&gt; 2000 * Math.sqrt(rt60 / volume);
</code></pre>
<p>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.</p>
<h2>3. Flag modes that pile up</h2>
<p>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.</p>
<pre><code class="language-js">function coincidences(modes, fLimit, tolerance = 0.03) {
  const strong = modes.filter(m =&gt; m.type !== "oblique" &amp;&amp; m.f &lt; fLimit);
  const out = [];
  for (let i = 0; i &lt; strong.length - 1; i++) {
    const a = strong[i], b = strong[i + 1];
    if ((b.f - a.f) / a.f &lt; tolerance) out.push([a, b]);
  }
  return out;
}
</code></pre>
<p>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.</p>
<h2>4. Pressure at the listening position</h2>
<p>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:</p>
<pre><code class="language-js">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);
}
</code></pre>
<p>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.</p>
<p>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.</p>
<h2>5. What this model leaves out</h2>
<p>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.</p>
<h2>Try it</h2>
<p>The finished <a href="https://theaudiostuff.com/tools/room-mode-calculator/">Room Mode Calculator</a> 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 <a href="https://theaudiostuff.com/tools/">theaudiostuff.com/tools/</a>.</p>
<p><em>Jakub Charkiewicz, The Audio Stuff</em></p>
]]></content:encoded></item></channel></rss>