지난 글에서는 decibel을 구현하는 것까지 진행하였다면 이번에는 decibel을 dBA로 전환할 것이다. dB(decibel)과 dBA의 차이는 무엇일까?
dB(데시벨)은 소리의 크기(음압)를 나타내는 단위이며, dBA는 dB 단위에 인간의 청감 특성을 고려한 'A-가중치'를 적용한 것이다.
쉽게 풀어 말하자면, 측정된 각 주파수에 인간의 귀가 주파수별로 민감도가 다르다는 특성을 반영한 A-가중치(A-weighting)을 부여한다.
A-Weighting을 꼭 적용해야 할까?
프로젝트가 소음 측정이라면 반드시 적용해야 한다.
예를 들어 250Hz 주파수의 보정값은 -8.6dB의 보정이 필요한데
이를 적용하지 않는다면 사람이 실제로 느끼는 소리보다 약 2.7배 더 크게 측정되는 오류가 발생할 수 있다.
library로 적용하면 되지 않을까?
안타깝게도 JS로 구성된 A-Weighting library는 발견할 수 없었다.
github 검색시 A-weighting library가 존재하긴 하나 A-가중치 뿐만 아니라 B,C,D 가중치가 존재하기도 했고, 공식과 다르다는 느낌이 들어 직접 구현하는 것이 정확할 것이라는 판단이 들었다.
const startButton = document.getElementById('startButton');
const dbDisplay = document.getElementById('current-db');
let audioContext = null;
let analyser = null;
let dataArray = null;
startButton.addEventListener('click', (event) => {
event.preventDefault();
if (!audioContext) {
startMeasurement();
}
});
function startMeasurement() {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
const source = audioContext.createMediaStreamSource(stream);
analyser = audioContext.createAnalyser();
analyser.fftSize = 2048;
analyser.minDecibels = -100; // AnalyserNode의 기본 dB 범위로 설정
analyser.maxDecibels = -30;
dataArray = new Uint8Array(analyser.frequencyBinCount);
source.connect(analyser);
console.log("A-weighting 소음 측정을 시작합니다.");
updateDb();
})
.catch(err => {
console.error("마이크 접근에 실패했습니다:", err);
});
}
// A-weighting 계수 계산 함수
function getAWeightingForFrequency(f) {
const f2 = f * f;
const f4 = f2 * f2;
const a1 = 12200 * 12200 * f4;
const a2 = (f2 + 20.6 * 20.6) * Math.sqrt((f2 + 107.7 * 107.7) * (f2 + 737.9 * 737.9));
const a3 = f2 + 12200 * 12200;
const r_a = a1 / (a2 * a3);
const a_db = 20 * Math.log10(r_a) + 2.0;
return a_db;
}
function updateDb() {
analyser.getByteFrequencyData(dataArray);
const bufferLength = analyser.frequencyBinCount;
const sampleRate = audioContext.sampleRate;
let sumOfSquares = 0;
for (let i = 0; i < bufferLength; i++) {
const frequency = i * sampleRate / bufferLength;
const aWeightingDb = getAWeightingForFrequency(frequency);
const amplitude = dataArray[i];
// amplitude(0-255)를 analyser의 dB 범위로 변환
const amplitudeDb = (amplitude / 255) * (analyser.maxDecibels - analyser.minDecibels) + analyser.minDecibels;
// RMS 계산을 위해 선형 값으로 변환하고 A-weighting 보정값을 더함
const weightedAmplitudeDb = amplitudeDb + aWeightingDb;
const weightedAmplitudeLinear = Math.pow(10, weightedAmplitudeDb / 20);
sumOfSquares += weightedAmplitudeLinear * weightedAmplitudeLinear;
}
const rms = Math.sqrt(sumOfSquares / bufferLength);
let dbA = rms > 0 ? 20 * Math.log10(rms) : -100; // -100으로 설정하여 음수 값이 나오게 함
// 최종 캘리브레이션 적용
const calibrationOffset = 110; // 교정을 통해 얻은 상수를 사용하세요.
dbA = dbA + calibrationOffset;
// 최종 값이 120dB을 초과하지 않도록 제한
if (dbA > 120) {
dbA = 120;
}
// 최종 값이 0dB 미만일 경우 0으로 설정
if (dbA < 0) {
dbA = 0;
}
dbDisplay.textContent = dbA.toFixed(2);
requestAnimationFrame(updateDb);
}
위 코드에서 구현된 가중치는 국제 표준(IEC 61672)에 정의된 A-weighting 필터의 동일한 수학 공식을 기반으로 한다.
각 A-가중치 적용 절차는 다음과 같다.
for 루프에서 frequency = i * sampleRate / bufferLength; 코드를 통해 AnalyserNode의 각 데이터 인덱스가 어떤 주파수 대역을 나타내는지 정확하게 계산한다.
getAWeightingForFrequency(frequency) 함수를 호출하여 각 주파수에 맞는 A-weighting 보정값(aWeightingDb)을 얻는다.
amplitudeDb + aWeightingDb 이 부분에서 AnalyserNode가 측정한 원본 주파수별 음량(amplitudeDb)에 계산된 보정값을 더한다.
보정된 값들을 선형 값으로 다시 변환한 후, RMS 계산과 로그 변환을 통해 최종적인 dBA 값을 얻는다.
다만, 여기서 calibrationOffset이라는 교정값을 더했는데 그 이유는 본 프로젝트는 0~120dB로 사람에게 체감되는 수치로 나누었는데, 마이크의 상태와 오디오 환경에 따라 음의 dBA 수치가 나올 수 있어, 110이라는 임의의 값을 집어넣은 상태이다. 좀 더 교정을 위해서는 소음 측정계 혹은 핸드폰의 소음 측정기 어플들을 사용해 값을 측정하여 차이를 활용해 교정해 나가야 한다.