Component demo · Test and measurement
Octave Bands
index.htm as it is written with the smart-industrial package
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>Octave Bands Overview Demo</title>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.default.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-elements.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial.css" />
<link rel="stylesheet" type="text/css" href="node_modules/smart-industrial/source/styles/smart.industrial-dark.css" />
<link rel="stylesheet" type="text/css" href="styles.css" />
</head>
<body class="viewport" theme="industrial">
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.OctaveBands displays the level in each octave or third-octave band as a bar, with the frequency-weighted overall level and a limit curve the bands are checked against. It is the display of a sound level meter and of a vibration survey.</h2>
<p>
Bands follow IEC 61260 and the A and C weightings follow IEC 61672, so a reading here matches the meter on the bench. Blocks of samples are pushed and analysed with the FFT of <code>Smart.DSP</code>, the power of the bins inside each band summed; an application with a real filter bank assigns the levels directly. Fast or slow averaging settles the bars the way a meter's time weighting does, and peak hold keeps the highest level of each band.
</p>
</section>
<section>
<h2>A compressor room, in dB SPL</h2>
<p>
A microphone at the operator position, 48 kS/s, calibrated in pascal, with the A weighting on and the noise limit of the site's hearing conservation plan as the dashed curve. The compressor's 2x line frequency hum at 100 Hz and its blade passing tone at 1.25 kHz stand out; open the door to the compressor and the bands above the limit turn red. The overall level is the sum of the bands on energy.
</p>
<smart-octave-bands id="room" label="Compressor room, position 3" resolution="third" sample-rate="48000" weighting="A" reference="0.00002" unit="Pa" frequency-min="25" frequency-max="16000" level-min="20" level-max="110"></smart-octave-bands>
<div class="demo-actions">
<button type="button" id="door">Open the door</button>
<button type="button" id="resetPeaks">Reset the peaks</button>
<label>Weighting <select id="weighting">
<option value="A" selected>A</option>
<option value="C">C</option>
<option value="Z">Z (none)</option>
</select></label>
<label>Averaging <select id="averaging">
<option value="fast" selected>fast</option>
<option value="slow">slow</option>
<option value="none">none</option>
</select></label>
<label><input type="checkbox" id="octave" /> Whole octaves</label>
<span id="roomLog" class="demo-log">levelsChange and bandClick are listed here.</span>
</div>
</section>
<section>
<h2>Levels from a filter bank</h2>
<p>
A vibration survey of a gearbox in whole octaves, from a hand-held analyser that reports the band levels itself: the levels are assigned, not computed. The limit is per band, from the machine's acceptance specification.
</p>
<smart-octave-bands id="gearbox" label="Gearbox GB-4, housing, vertical" resolution="octave" frequency-min="31.5" frequency-max="8000" unit="g" averaging="none" level-min="-40" level-max="20"></smart-octave-bands>
<p class="hint">The overall level in the header is a status line, so a screen reader hears it change. Every band's level is available as text under the plot, and the plot's label names the overall level and the highest band.</p>
</section>
</div>
<script type="module" src="node_modules/smart-industrial/source/modules/smart.octavebands.js"></script>
<script type="module" src="index.js"></script>
<script>
/* Shown once every element has rendered, so nothing moves on the way in (styles/demos.css). */
addEventListener('load', function () {
var all = Array.prototype.slice.call(document.querySelectorAll('*')),
ready = function () { document.body.classList.add('ready'); };
Promise.all(all.map(function (e) { return e.localName.indexOf('smart-') === 0 ? customElements.whenDefined(e.localName) : null; })).then(function () {
return Promise.all(all.filter(function (e) { return typeof e.whenRendered === 'function' && !e.isRendered; }).map(function (e) { return e.whenRendered(); }));
}).then(function () { requestAnimationFrame(function () { requestAnimationFrame(ready); }); }, ready);
});
</script>
</body>
</html>
index.js
/// <reference path="../../../source/typescript/smart.elements.d.ts" />
/* The room is a generated signal in pascal with known tones, so the bands can be checked
against what was put in. The seam is push(block): a microphone's driver pushes the
blocks it reads and nothing else on the page changes. */
function gaussian() {
return Math.sqrt(-2 * Math.log(Math.random() || 1e-12)) * Math.cos(2 * Math.PI * Math.random());
}
window.onload = function () {
/* ── The compressor room ───────────────────────────────────────────── */
const room = document.getElementById('room'),
roomLog = document.getElementById('roomLog'),
door = document.getElementById('door'),
resetPeaks = document.getElementById('resetPeaks'),
weighting = document.getElementById('weighting'),
averaging = document.getElementById('averaging'),
octave = document.getElementById('octave');
const RATE = 48000, BLOCK = 4096;
let doorOpen = false, phaseHum = 0, phaseBlade = 0;
/* Hearing conservation limit: an NR-style curve, in dB SPL per band centre. */
room.limit = [
{ frequency: 31.5, level: 96 }, { frequency: 63, level: 88 }, { frequency: 125, level: 83 },
{ frequency: 250, level: 79 }, { frequency: 500, level: 76 }, { frequency: 1000, level: 74 },
{ frequency: 2000, level: 72 }, { frequency: 4000, level: 71 }, { frequency: 8000, level: 70 },
{ frequency: 16000, level: 69 }
];
/* Pink-ish broadband noise from a handful of low-passed noise sources, a 100 Hz hum
with harmonics, a 1250 Hz blade tone, and, with the door open, everything 12 dB
louder plus a broadband hiss from the intake. */
let state = [0, 0, 0, 0];
function block() {
const samples = new Float64Array(BLOCK),
gain = doorOpen ? 4 : 1,
hiss = doorOpen ? 0.06 : 0;
for (let i = 0; i < BLOCK; i++) {
const white = gaussian();
state[0] = 0.997 * state[0] + white * 0.05;
state[1] = 0.98 * state[1] + white * 0.1;
state[2] = 0.9 * state[2] + white * 0.15;
state[3] = 0.6 * state[3] + white * 0.2;
phaseHum += 2 * Math.PI * 100 / RATE;
phaseBlade += 2 * Math.PI * 1250 / RATE;
samples[i] = gain * (
0.9 * (state[0] + state[1] + state[2] + state[3]) * 0.12 +
0.55 * Math.sin(phaseHum) + 0.18 * Math.sin(2 * phaseHum) + 0.07 * Math.sin(3 * phaseHum) +
0.28 * Math.sin(phaseBlade) + 0.05 * Math.sin(2 * phaseBlade)
) + hiss * white;
}
return samples;
}
setInterval(function () {
room.push(block());
}, 90);
door.addEventListener('click', function () {
doorOpen = !doorOpen;
door.textContent = doorOpen ? 'Close the door' : 'Open the door';
});
resetPeaks.addEventListener('click', function () {
room.resetPeaks();
});
weighting.addEventListener('change', function () {
room.weighting = weighting.value;
});
averaging.addEventListener('change', function () {
room.averaging = averaging.value;
});
octave.addEventListener('change', function () {
room.resolution = octave.checked ? 'octave' : 'third';
});
let lastLog = 0;
room.addEventListener('levelsChange', function (event) {
if (Date.now() - lastLog > 1000) {
lastLog = Date.now();
roomLog.textContent = 'levelsChange: overall ' + event.detail.overall.toFixed(1) + ' dB SPL over ' + event.detail.levels.length + ' bands';
}
});
room.addEventListener('bandClick', function (event) {
roomLog.textContent = 'bandClick: ' + event.detail.label + ' Hz, ' + event.detail.level.toFixed(1) + ' dB SPL, peak ' + event.detail.peak.toFixed(1);
lastLog = Date.now() + 3000;
});
/* ── The gearbox survey ────────────────────────────────────────────── */
const gearbox = document.getElementById('gearbox');
/* Levels per band in dB re 1 g, 31.5 Hz to 8 kHz, and the acceptance limit per band. */
gearbox.levels = [-28.4, -21.7, -14.2, -9.8, -6.1, -2.5, 4.8, 1.2, -8.9];
gearbox.limit = [-20, -15, -10, -6, -3, 0, 2, 2, -4];
gearbox.peakHold = false;
};
styles.css
smart-octave-bands {
--smart-octave-height: 360px;
}
#gearbox {
--smart-octave-height: 300px;
max-width: 760px;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
The same demo in Angular, generated from the HTML demo. The frameworks guide has the project setup.
angular/app.component.html
<div class="angular-root" theme="industrial">
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.OctaveBands displays the level in each octave or third-octave band as a bar, with the frequency-weighted overall level and a limit curve the bands are checked against. It is the display of a sound level meter and of a vibration survey.</h2>
<p>
Bands follow IEC 61260 and the A and C weightings follow IEC 61672, so a reading here matches the meter on the bench. Blocks of samples are pushed and analysed with the FFT of <code>Smart.DSP</code>, the power of the bins inside each band summed; an application with a real filter bank assigns the levels directly. Fast or slow averaging settles the bars the way a meter's time weighting does, and peak hold keeps the highest level of each band.
</p>
</section>
<section>
<h2>A compressor room, in dB SPL</h2>
<p>
A microphone at the operator position, 48 kS/s, calibrated in pascal, with the A weighting on and the noise limit of the site's hearing conservation plan as the dashed curve. The compressor's 2x line frequency hum at 100 Hz and its blade passing tone at 1.25 kHz stand out; open the door to the compressor and the bands above the limit turn red. The overall level is the sum of the bands on energy.
</p>
<smart-octave-bands id="room" label="Compressor room, position 3" resolution="third" [sampleRate]="48000" weighting="A" [reference]="0.00002" unit="Pa" [frequencyMin]="25" [frequencyMax]="16000" [levelMin]="20" [levelMax]="110"></smart-octave-bands>
<div class="demo-actions">
<button type="button" id="door">Open the door</button>
<button type="button" id="resetPeaks">Reset the peaks</button>
<label>Weighting <select id="weighting">
<option value="A" selected>A</option>
<option value="C">C</option>
<option value="Z">Z (none)</option>
</select></label>
<label>Averaging <select id="averaging">
<option value="fast" selected>fast</option>
<option value="slow">slow</option>
<option value="none">none</option>
</select></label>
<label><input type="checkbox" id="octave" /> Whole octaves</label>
<span id="roomLog" class="demo-log">levelsChange and bandClick are listed here.</span>
</div>
</section>
<section>
<h2>Levels from a filter bank</h2>
<p>
A vibration survey of a gearbox in whole octaves, from a hand-held analyser that reports the band levels itself: the levels are assigned, not computed. The limit is per band, from the machine's acceptance specification.
</p>
<smart-octave-bands id="gearbox" label="Gearbox GB-4, housing, vertical" resolution="octave" [frequencyMin]="31.5" [frequencyMax]="8000" unit="g" averaging="none" [levelMin]="-40" [levelMax]="20"></smart-octave-bands>
<p class="hint">The overall level in the header is a status line, so a screen reader hears it change. Every band's level is available as text under the plot, and the plot's label names the overall level and the highest band.</p>
</section>
</div>
</div>
angular/app.component.ts
import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { OctaveBandsModule } from 'smart-industrial/angular/octavebands';
import { runDemo } from './demo';
/**
* Smart.UI Industrial - OctaveBands in Angular.
*
* The template is the HTML demo's markup: the elements are the wrappers, so what is
* written on them is checked against their inputs. demo.ts drives them the way the
* plant's data would, once every element in the view has rendered.
*/
@Component({
selector: 'app-root',
standalone: true,
imports: [OctaveBandsModule],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent implements AfterViewInit, OnDestroy {
private unmounted = false;
ngAfterViewInit(): void {
const root = document.querySelector('.angular-root') as HTMLElement,
pending: Promise<void>[] = [];
root.querySelectorAll('*').forEach((element: any) => {
if (typeof element.whenRendered === 'function' && !element.isRendered) {
pending.push(element.whenRendered());
}
});
Promise.all(pending).then(() => {
if (!this.unmounted) {
runDemo();
}
});
}
ngOnDestroy(): void {
this.unmounted = true;
}
}
angular/demo.ts
// @ts-nocheck
/* The HTML demo's script, as it was written: page code that feeds the elements, not a typed
API. The types are on the wrappers in app.component.ts. */
/* The room is a generated signal in pascal with known tones, so the bands can be checked
against what was put in. The seam is push(block): a microphone's driver pushes the
blocks it reads and nothing else on the page changes. */
function gaussian() {
return Math.sqrt(-2 * Math.log(Math.random() || 1e-12)) * Math.cos(2 * Math.PI * Math.random());
}
export function runDemo(): void {
/* ── The compressor room ───────────────────────────────────────────── */
const room = document.getElementById('room'),
roomLog = document.getElementById('roomLog'),
door = document.getElementById('door'),
resetPeaks = document.getElementById('resetPeaks'),
weighting = document.getElementById('weighting'),
averaging = document.getElementById('averaging'),
octave = document.getElementById('octave');
const RATE = 48000, BLOCK = 4096;
let doorOpen = false, phaseHum = 0, phaseBlade = 0;
/* Hearing conservation limit: an NR-style curve, in dB SPL per band centre. */
room.limit = [
{ frequency: 31.5, level: 96 }, { frequency: 63, level: 88 }, { frequency: 125, level: 83 },
{ frequency: 250, level: 79 }, { frequency: 500, level: 76 }, { frequency: 1000, level: 74 },
{ frequency: 2000, level: 72 }, { frequency: 4000, level: 71 }, { frequency: 8000, level: 70 },
{ frequency: 16000, level: 69 }
];
/* Pink-ish broadband noise from a handful of low-passed noise sources, a 100 Hz hum
with harmonics, a 1250 Hz blade tone, and, with the door open, everything 12 dB
louder plus a broadband hiss from the intake. */
let state = [0, 0, 0, 0];
function block() {
const samples = new Float64Array(BLOCK),
gain = doorOpen ? 4 : 1,
hiss = doorOpen ? 0.06 : 0;
for (let i = 0; i < BLOCK; i++) {
const white = gaussian();
state[0] = 0.997 * state[0] + white * 0.05;
state[1] = 0.98 * state[1] + white * 0.1;
state[2] = 0.9 * state[2] + white * 0.15;
state[3] = 0.6 * state[3] + white * 0.2;
phaseHum += 2 * Math.PI * 100 / RATE;
phaseBlade += 2 * Math.PI * 1250 / RATE;
samples[i] = gain * (
0.9 * (state[0] + state[1] + state[2] + state[3]) * 0.12 +
0.55 * Math.sin(phaseHum) + 0.18 * Math.sin(2 * phaseHum) + 0.07 * Math.sin(3 * phaseHum) +
0.28 * Math.sin(phaseBlade) + 0.05 * Math.sin(2 * phaseBlade)
) + hiss * white;
}
return samples;
}
setInterval(function () {
room.push(block());
}, 90);
door.addEventListener('click', function () {
doorOpen = !doorOpen;
door.textContent = doorOpen ? 'Close the door' : 'Open the door';
});
resetPeaks.addEventListener('click', function () {
room.resetPeaks();
});
weighting.addEventListener('change', function () {
room.weighting = weighting.value;
});
averaging.addEventListener('change', function () {
room.averaging = averaging.value;
});
octave.addEventListener('change', function () {
room.resolution = octave.checked ? 'octave' : 'third';
});
let lastLog = 0;
room.addEventListener('levelsChange', function (event) {
if (Date.now() - lastLog > 1000) {
lastLog = Date.now();
roomLog.textContent = 'levelsChange: overall ' + event.detail.overall.toFixed(1) + ' dB SPL over ' + event.detail.levels.length + ' bands';
}
});
room.addEventListener('bandClick', function (event) {
roomLog.textContent = 'bandClick: ' + event.detail.label + ' Hz, ' + event.detail.level.toFixed(1) + ' dB SPL, peak ' + event.detail.peak.toFixed(1);
lastLog = Date.now() + 3000;
});
/* ── The gearbox survey ────────────────────────────────────────────── */
const gearbox = document.getElementById('gearbox');
/* Levels per band in dB re 1 g, 31.5 Hz to 8 kHz, and the acceptance limit per band. */
gearbox.levels = [-28.4, -21.7, -14.2, -9.8, -6.1, -2.5, 4.8, 1.2, -8.9];
gearbox.limit = [-20, -15, -10, -6, -3, 0, 2, 2, -4];
gearbox.peakHold = false;
}
angular/app.component.css
smart-octave-bands {
--smart-octave-height: 360px;
}
#gearbox {
--smart-octave-height: 300px;
max-width: 760px;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
The same demo in React, generated from the HTML demo. The frameworks guide has the project setup.
react/App.jsx
import 'smart-industrial/source/styles/smart.default.css';
import 'smart-industrial/source/styles/smart.industrial-elements.css';
import 'smart-industrial/source/styles/smart.industrial.css';
import 'smart-industrial/source/styles/smart.industrial-dark.css';
import './App.css';
import React from 'react';
import { OctaveBands } from 'smart-industrial/react/octavebands';
/* The demo logic is the HTML demo's: it feeds the elements the way a plant's data would. */
/* The room is a generated signal in pascal with known tones, so the bands can be checked
against what was put in. The seam is push(block): a microphone's driver pushes the
blocks it reads and nothing else on the page changes. */
function gaussian() {
return Math.sqrt(-2 * Math.log(Math.random() || 1e-12)) * Math.cos(2 * Math.PI * Math.random());
}
const App = () => {
const root = React.useRef(null);
React.useEffect(() => {
/* The elements render after React has mounted them, and the demo's logic - written
for a page's load event - reads their parts, so it waits for every element in the
tree to have rendered first. */
const pending = Array.from(root.current.querySelectorAll('*'))
.filter((element) => typeof element.whenRendered === 'function' && !element.isRendered)
.map((element) => element.whenRendered());
let unmounted = false;
Promise.all(pending).then(() => {
if (unmounted) {
return;
}
/* ── The compressor room ───────────────────────────────────────────── */
const room = document.getElementById('room'),
roomLog = document.getElementById('roomLog'),
door = document.getElementById('door'),
resetPeaks = document.getElementById('resetPeaks'),
weighting = document.getElementById('weighting'),
averaging = document.getElementById('averaging'),
octave = document.getElementById('octave');
const RATE = 48000, BLOCK = 4096;
let doorOpen = false, phaseHum = 0, phaseBlade = 0;
/* Hearing conservation limit: an NR-style curve, in dB SPL per band centre. */
room.limit = [
{ frequency: 31.5, level: 96 }, { frequency: 63, level: 88 }, { frequency: 125, level: 83 },
{ frequency: 250, level: 79 }, { frequency: 500, level: 76 }, { frequency: 1000, level: 74 },
{ frequency: 2000, level: 72 }, { frequency: 4000, level: 71 }, { frequency: 8000, level: 70 },
{ frequency: 16000, level: 69 }
];
/* Pink-ish broadband noise from a handful of low-passed noise sources, a 100 Hz hum
with harmonics, a 1250 Hz blade tone, and, with the door open, everything 12 dB
louder plus a broadband hiss from the intake. */
let state = [0, 0, 0, 0];
function block() {
const samples = new Float64Array(BLOCK),
gain = doorOpen ? 4 : 1,
hiss = doorOpen ? 0.06 : 0;
for (let i = 0; i < BLOCK; i++) {
const white = gaussian();
state[0] = 0.997 * state[0] + white * 0.05;
state[1] = 0.98 * state[1] + white * 0.1;
state[2] = 0.9 * state[2] + white * 0.15;
state[3] = 0.6 * state[3] + white * 0.2;
phaseHum += 2 * Math.PI * 100 / RATE;
phaseBlade += 2 * Math.PI * 1250 / RATE;
samples[i] = gain * (
0.9 * (state[0] + state[1] + state[2] + state[3]) * 0.12 +
0.55 * Math.sin(phaseHum) + 0.18 * Math.sin(2 * phaseHum) + 0.07 * Math.sin(3 * phaseHum) +
0.28 * Math.sin(phaseBlade) + 0.05 * Math.sin(2 * phaseBlade)
) + hiss * white;
}
return samples;
}
setInterval(function () {
room.push(block());
}, 90);
door.addEventListener('click', function () {
doorOpen = !doorOpen;
door.textContent = doorOpen ? 'Close the door' : 'Open the door';
});
resetPeaks.addEventListener('click', function () {
room.resetPeaks();
});
weighting.addEventListener('change', function () {
room.weighting = weighting.value;
});
averaging.addEventListener('change', function () {
room.averaging = averaging.value;
});
octave.addEventListener('change', function () {
room.resolution = octave.checked ? 'octave' : 'third';
});
let lastLog = 0;
room.addEventListener('levelsChange', function (event) {
if (Date.now() - lastLog > 1000) {
lastLog = Date.now();
roomLog.textContent = 'levelsChange: overall ' + event.detail.overall.toFixed(1) + ' dB SPL over ' + event.detail.levels.length + ' bands';
}
});
room.addEventListener('bandClick', function (event) {
roomLog.textContent = 'bandClick: ' + event.detail.label + ' Hz, ' + event.detail.level.toFixed(1) + ' dB SPL, peak ' + event.detail.peak.toFixed(1);
lastLog = Date.now() + 3000;
});
/* ── The gearbox survey ────────────────────────────────────────────── */
const gearbox = document.getElementById('gearbox');
/* Levels per band in dB re 1 g, 31.5 Hz to 8 kHz, and the acceptance limit per band. */
gearbox.levels = [-28.4, -21.7, -14.2, -9.8, -6.1, -2.5, 4.8, 1.2, -8.9];
gearbox.limit = [-20, -15, -10, -6, -3, 0, 2, 2, -4];
gearbox.peakHold = false;
});
return () => {
unmounted = true;
};
}, []);
return (
<div className="react-root" ref={root} theme="industrial">
<div className="smart-demo-container">
<section id="overview">
<h2>Smart.OctaveBands displays the level in each octave or third-octave band as a bar, with the frequency-weighted overall level and a limit curve the bands are checked against. It is the display of a sound level meter and of a vibration survey.</h2>
<p>Bands follow IEC 61260 and the A and C weightings follow IEC 61672, so a reading here matches the meter on the bench. Blocks of samples are pushed and analysed with the FFT of <code>Smart.DSP</code>, the power of the bins inside each band summed; an application with a real filter bank assigns the levels directly. Fast or slow averaging settles the bars the way a meter's time weighting does, and peak hold keeps the highest level of each band.</p>
</section>
<section>
<h2>A compressor room, in dB SPL</h2>
<p>A microphone at the operator position, 48 kS/s, calibrated in pascal, with the A weighting on and the noise limit of the site's hearing conservation plan as the dashed curve. The compressor's 2x line frequency hum at 100 Hz and its blade passing tone at 1.25 kHz stand out; open the door to the compressor and the bands above the limit turn red. The overall level is the sum of the bands on energy.</p>
<OctaveBands id="room" label="Compressor room, position 3" resolution="third" sampleRate={48000} weighting="A" reference={0.00002} unit="Pa" frequencyMin={25} frequencyMax={16000} levelMin={20} levelMax={110}></OctaveBands>
<div className="demo-actions">
<button type="button" id="door">Open the door</button>
<button type="button" id="resetPeaks">Reset the peaks</button>
<label>
Weighting
<select id="weighting" defaultValue="A"><option value="A">A</option> <option value="C">C</option> <option value="Z">Z (none)</option></select>
</label>
<label>
Averaging
<select id="averaging" defaultValue="fast"><option value="fast">fast</option> <option value="slow">slow</option> <option value="none">none</option></select>
</label>
<label><input type="checkbox" id="octave" /> Whole octaves</label>
<span id="roomLog" className="demo-log">levelsChange and bandClick are listed here.</span>
</div>
</section>
<section>
<h2>Levels from a filter bank</h2>
<p>A vibration survey of a gearbox in whole octaves, from a hand-held analyser that reports the band levels itself: the levels are assigned, not computed. The limit is per band, from the machine's acceptance specification.</p>
<OctaveBands id="gearbox" label="Gearbox GB-4, housing, vertical" resolution="octave" frequencyMin={31.5} frequencyMax={8000} unit="g" averaging="none" levelMin={-40} levelMax={20}></OctaveBands>
<p className="hint">The overall level in the header is a status line, so a screen reader hears it change. Every band's level is available as text under the plot, and the plot's label names the overall level and the highest band.</p>
</section>
</div>
</div>
);
};
export default App;
react/App.css
smart-octave-bands {
--smart-octave-height: 360px;
}
#gearbox {
--smart-octave-height: 300px;
max-width: 760px;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
The same demo in Vue, generated from the HTML demo. The frameworks guide has the project setup.
vue/App.vue
<template>
<div class="vue-root" theme="industrial" v-pre>
<div class="smart-demo-container">
<section id="overview">
<h2>Smart.OctaveBands displays the level in each octave or third-octave band as a bar, with the frequency-weighted overall level and a limit curve the bands are checked against. It is the display of a sound level meter and of a vibration survey.</h2>
<p>
Bands follow IEC 61260 and the A and C weightings follow IEC 61672, so a reading here matches the meter on the bench. Blocks of samples are pushed and analysed with the FFT of <code>Smart.DSP</code>, the power of the bins inside each band summed; an application with a real filter bank assigns the levels directly. Fast or slow averaging settles the bars the way a meter's time weighting does, and peak hold keeps the highest level of each band.
</p>
</section>
<section>
<h2>A compressor room, in dB SPL</h2>
<p>
A microphone at the operator position, 48 kS/s, calibrated in pascal, with the A weighting on and the noise limit of the site's hearing conservation plan as the dashed curve. The compressor's 2x line frequency hum at 100 Hz and its blade passing tone at 1.25 kHz stand out; open the door to the compressor and the bands above the limit turn red. The overall level is the sum of the bands on energy.
</p>
<smart-octave-bands id="room" label="Compressor room, position 3" resolution="third" sample-rate="48000" weighting="A" reference="0.00002" unit="Pa" frequency-min="25" frequency-max="16000" level-min="20" level-max="110"></smart-octave-bands>
<div class="demo-actions">
<button type="button" id="door">Open the door</button>
<button type="button" id="resetPeaks">Reset the peaks</button>
<label>Weighting <select id="weighting">
<option value="A" selected>A</option>
<option value="C">C</option>
<option value="Z">Z (none)</option>
</select></label>
<label>Averaging <select id="averaging">
<option value="fast" selected>fast</option>
<option value="slow">slow</option>
<option value="none">none</option>
</select></label>
<label><input type="checkbox" id="octave" /> Whole octaves</label>
<span id="roomLog" class="demo-log">levelsChange and bandClick are listed here.</span>
</div>
</section>
<section>
<h2>Levels from a filter bank</h2>
<p>
A vibration survey of a gearbox in whole octaves, from a hand-held analyser that reports the band levels itself: the levels are assigned, not computed. The limit is per band, from the machine's acceptance specification.
</p>
<smart-octave-bands id="gearbox" label="Gearbox GB-4, housing, vertical" resolution="octave" frequency-min="31.5" frequency-max="8000" unit="g" averaging="none" level-min="-40" level-max="20"></smart-octave-bands>
<p class="hint">The overall level in the header is a status line, so a screen reader hears it change. Every band's level is available as text under the plot, and the plot's label names the overall level and the highest band.</p>
</section>
</div>
</div>
</template>
<script>
import { onMounted } from "vue";
import "smart-industrial/source/styles/smart.default.css";
import "smart-industrial/source/styles/smart.industrial-elements.css";
import "smart-industrial/source/styles/smart.industrial.css";
import "smart-industrial/source/styles/smart.industrial-dark.css";
import "smart-industrial/source/modules/smart.octavebands.js";
/* The room is a generated signal in pascal with known tones, so the bands can be checked
against what was put in. The seam is push(block): a microphone's driver pushes the
blocks it reads and nothing else on the page changes. */
function gaussian() {
return Math.sqrt(-2 * Math.log(Math.random() || 1e-12)) * Math.cos(2 * Math.PI * Math.random());
}
export default {
name: "app",
setup() {
onMounted(() => {
/* The elements render after Vue has mounted them, and the demo's logic - written for
a page's load event - reads their parts, so it waits for every element in the tree
to have rendered first. */
const pending = Array.from(document.querySelector(".vue-root").querySelectorAll("*"))
.filter((element) => typeof element.whenRendered === "function" && !element.isRendered)
.map((element) => element.whenRendered());
Promise.all(pending).then(() => {
/* ── The compressor room ───────────────────────────────────────────── */
const room = document.getElementById('room'),
roomLog = document.getElementById('roomLog'),
door = document.getElementById('door'),
resetPeaks = document.getElementById('resetPeaks'),
weighting = document.getElementById('weighting'),
averaging = document.getElementById('averaging'),
octave = document.getElementById('octave');
const RATE = 48000, BLOCK = 4096;
let doorOpen = false, phaseHum = 0, phaseBlade = 0;
/* Hearing conservation limit: an NR-style curve, in dB SPL per band centre. */
room.limit = [
{ frequency: 31.5, level: 96 }, { frequency: 63, level: 88 }, { frequency: 125, level: 83 },
{ frequency: 250, level: 79 }, { frequency: 500, level: 76 }, { frequency: 1000, level: 74 },
{ frequency: 2000, level: 72 }, { frequency: 4000, level: 71 }, { frequency: 8000, level: 70 },
{ frequency: 16000, level: 69 }
];
/* Pink-ish broadband noise from a handful of low-passed noise sources, a 100 Hz hum
with harmonics, a 1250 Hz blade tone, and, with the door open, everything 12 dB
louder plus a broadband hiss from the intake. */
let state = [0, 0, 0, 0];
function block() {
const samples = new Float64Array(BLOCK),
gain = doorOpen ? 4 : 1,
hiss = doorOpen ? 0.06 : 0;
for (let i = 0; i < BLOCK; i++) {
const white = gaussian();
state[0] = 0.997 * state[0] + white * 0.05;
state[1] = 0.98 * state[1] + white * 0.1;
state[2] = 0.9 * state[2] + white * 0.15;
state[3] = 0.6 * state[3] + white * 0.2;
phaseHum += 2 * Math.PI * 100 / RATE;
phaseBlade += 2 * Math.PI * 1250 / RATE;
samples[i] = gain * (
0.9 * (state[0] + state[1] + state[2] + state[3]) * 0.12 +
0.55 * Math.sin(phaseHum) + 0.18 * Math.sin(2 * phaseHum) + 0.07 * Math.sin(3 * phaseHum) +
0.28 * Math.sin(phaseBlade) + 0.05 * Math.sin(2 * phaseBlade)
) + hiss * white;
}
return samples;
}
setInterval(function () {
room.push(block());
}, 90);
door.addEventListener('click', function () {
doorOpen = !doorOpen;
door.textContent = doorOpen ? 'Close the door' : 'Open the door';
});
resetPeaks.addEventListener('click', function () {
room.resetPeaks();
});
weighting.addEventListener('change', function () {
room.weighting = weighting.value;
});
averaging.addEventListener('change', function () {
room.averaging = averaging.value;
});
octave.addEventListener('change', function () {
room.resolution = octave.checked ? 'octave' : 'third';
});
let lastLog = 0;
room.addEventListener('levelsChange', function (event) {
if (Date.now() - lastLog > 1000) {
lastLog = Date.now();
roomLog.textContent = 'levelsChange: overall ' + event.detail.overall.toFixed(1) + ' dB SPL over ' + event.detail.levels.length + ' bands';
}
});
room.addEventListener('bandClick', function (event) {
roomLog.textContent = 'bandClick: ' + event.detail.label + ' Hz, ' + event.detail.level.toFixed(1) + ' dB SPL, peak ' + event.detail.peak.toFixed(1);
lastLog = Date.now() + 3000;
});
/* ── The gearbox survey ────────────────────────────────────────────── */
const gearbox = document.getElementById('gearbox');
/* Levels per band in dB re 1 g, 31.5 Hz to 8 kHz, and the acceptance limit per band. */
gearbox.levels = [-28.4, -21.7, -14.2, -9.8, -6.1, -2.5, 4.8, 1.2, -8.9];
gearbox.limit = [-20, -15, -10, -6, -3, 0, 2, 2, -4];
gearbox.peakHold = false;
});
});
}
};
</script>
<style>
smart-octave-bands {
--smart-octave-height: 360px;
}
#gearbox {
--smart-octave-height: 300px;
max-width: 760px;
}
/* ── Demo chrome, shared by the Industrial demos ─────────────────────────
Headings, prose, buttons and notes are the same on every demo page. This block
is appended by scripts and comes last, so it wins over the rules above. */
.smart-demo-container {
padding: 4px 0 32px;
}
.smart-demo-container section {
margin-bottom: 36px;
}
.smart-demo-container section h2 {
margin: 0 0 8px;
font-size: 17px;
font-weight: 600;
line-height: 1.35;
color: var(--smart-text-color);
}
.smart-demo-container #overview h2 {
margin-bottom: 10px;
font-size: 20px;
max-width: 78ch;
}
.smart-demo-container section > p {
margin: 0 0 14px;
max-width: 84ch;
font-size: 14px;
line-height: 1.55;
color: var(--smart-text-secondary-color);
}
.smart-demo-container code {
padding: 1px 5px;
border-radius: 2px;
background: var(--smart-ui-state-hover);
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
}
.smart-demo-container .demo-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 10px;
margin-top: 12px;
}
.smart-demo-container .demo-actions label {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--smart-text-color);
}
.smart-demo-container .demo-actions button,
.smart-demo-container .demo-button {
min-height: 30px;
padding: 0 12px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-industrial-panel-background, var(--smart-surface));
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
font-weight: 500;
letter-spacing: 0;
text-transform: none;
cursor: pointer;
}
.smart-demo-container .demo-actions button:hover,
.smart-demo-container .demo-button:hover {
border-color: var(--smart-ui-state-border-hover);
background: var(--smart-ui-state-hover);
color: var(--smart-ui-state-color-hover);
}
.smart-demo-container .demo-actions button.demo-button-primary,
.smart-demo-container .demo-button-primary {
border-color: var(--smart-primary);
background: var(--smart-primary);
color: var(--smart-primary-color, #fff);
}
.smart-demo-container .demo-actions button[disabled],
.smart-demo-container .demo-button[disabled] {
opacity: 0.5;
cursor: default;
}
.smart-demo-container .demo-actions select,
.smart-demo-container .demo-actions input[type="text"],
.smart-demo-container .demo-actions input[type="number"] {
min-height: 30px;
padding: 2px 6px;
border: 1px solid var(--smart-border);
border-radius: 3px;
background: var(--smart-background);
color: var(--smart-text-color);
font: inherit;
font-size: 13px;
}
.smart-demo-container .hint,
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
margin: 10px 0 0;
font-size: 12px;
line-height: 1.5;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .demo-log,
.smart-demo-container .demo-counts {
font-family: Consolas, "Courier New", monospace;
}
.smart-demo-container .log {
list-style: none;
margin: 10px 0 0;
padding: 0;
font-size: 12px;
font-variant-numeric: tabular-nums;
color: var(--smart-text-secondary-color);
opacity: 1;
}
.smart-demo-container .log li {
padding: 2px 0;
}
</style>
What the demo shows
- A compressor room, in dB SPL
- Levels from a filter bank
On the operator screens
Also in test and measurement
Bode PlotDigital WaveformIntensity GraphPolar PlotResource PickerWaveform Graph