From ce52ca489b134f5a1da7566fd1f37d3ed545088b Mon Sep 17 00:00:00 2001 From: Tomohiro IKEDA Date: Thu, 14 Nov 2024 20:00:51 +0900 Subject: [PATCH] feat: Low-Pass Filter (BiquadFilterNode) --- docs/docs.js | 171 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/index.html | 31 +++++++++ 2 files changed, 202 insertions(+) diff --git a/docs/docs.js b/docs/docs.js index cb98c67..e5a8523 100644 --- a/docs/docs.js +++ b/docs/docs.js @@ -6923,6 +6923,175 @@ const animateAM = (svgTime, svgSpectrum) => { }); }; +const renderFrequencyResponse = (svg, type) => { + const innerWidth = Number(svg.getAttribute('width')) - padding * 2; + const innerHeight = Number(svg.getAttribute('height')) - padding * 2; + + const path = document.createElementNS(xmlns, 'path'); + + path.setAttribute('stroke', waveColor); + path.setAttribute('fill', 'none'); + path.setAttribute('stroke-width', lineWidth.toString(10)); + path.setAttribute('stroke-linecap', lineCap); + path.setAttribute('stroke-linejoin', lineJoin); + + svg.appendChild(path); + + const frequencies = new Float32Array(8000); + + const min = Math.log(10); + const max = Math.log(20000); + const diff = max - min; + + for (let i = 0, len = frequencies.length; i < len; i++) { + const ratio = i / (len - 1); + + frequencies[i] = Math.exp(diff * ratio + min); + } + + for (let i = 0; i < 10; i++) { + const x = i * (innerWidth / 9) + padding; + + const rect = document.createElementNS(xmlns, 'rect'); + + rect.setAttribute('x', x.toString(10)); + rect.setAttribute('y', padding.toString(10)); + rect.setAttribute('width', lineWidth.toString(10)); + rect.setAttribute('height', innerHeight.toString(10)); + rect.setAttribute('stroke', 'none'); + rect.setAttribute('fill', alphaBaseColor); + + svg.appendChild(rect); + + const text = document.createElementNS(xmlns, 'text'); + + text.textContent = `${Math.trunc(frequencies[i < 9 ? (i + 1) * 800 : 7999])} Hz`; + + text.setAttribute('x', x.toString(10)); + text.setAttribute('y', (padding + innerHeight + 16).toString(10)); + + text.setAttribute('text-anchor', 'middle'); + text.setAttribute('stroke', 'none'); + text.setAttribute('fill', baseColor); + text.setAttribute('font-size', '12px'); + + svg.appendChild(text); + } + + const dBs = [' 24', ' 18', ' 12', ' 6', ' 0', ' -6', '-12', '-18', '-24']; + + for (let i = 0; i < 9; i++) { + const y = i * (innerHeight / 8) + padding; + + const rect = document.createElementNS(xmlns, 'rect'); + + rect.setAttribute('x', padding.toString(10)); + rect.setAttribute('y', y.toString(10)); + rect.setAttribute('width', innerWidth.toString(10)); + rect.setAttribute('height', lineWidth.toString(10)); + rect.setAttribute('stroke', 'none'); + rect.setAttribute('fill', alphaBaseColor); + + svg.appendChild(rect); + + const text = document.createElementNS(xmlns, 'text'); + + text.textContent = `${dBs[i]} dB`; + + text.setAttribute('x', (padding - 20).toString(10)); + text.setAttribute('y', (y + 4).toString(10)); + + text.setAttribute('text-anchor', 'middle'); + text.setAttribute('stroke', 'none'); + text.setAttribute('fill', baseColor); + text.setAttribute('font-size', '12px'); + + svg.appendChild(text); + } + + const filter = new BiquadFilterNode(audiocontext, { type }); + + const render = () => { + const magResponses = new Float32Array(frequencies.length); + const phaseResponses = new Float32Array(frequencies.length); + + filter.getFrequencyResponse(frequencies, magResponses, phaseResponses); + + path.removeAttribute('d'); + + let d = ''; + + for (let i = 0, len = frequencies.length; i < len; i++) { + const f = frequencies[i]; + const x = (Math.log10(f / 20) / Math.log10(1000)) * innerWidth + padding - 3; + const dB = 20 * Math.log10(magResponses[i]); + const y = ((-1 * dB) / 48) * innerHeight + innerHeight / 2 + padding; + + if (x < padding) { + continue; + } + + if (y > padding + innerHeight) { + continue; + } + + if (d === '') { + d += `M${x} ${y} `; + } else { + d += `L${x} ${y} `; + } + } + + path.setAttribute('d', d); + }; + + if (!type) { + document.getElementById('select-filter-type').addEventListener('change', (event) => { + filter.type = event.currentTarget.value; + + render(); + }); + } + + document.getElementById(`range-filter-${type}-frequency`).addEventListener('input', (event) => { + filter.frequency.value = event.currentTarget.valueAsNumber; + + document.getElementById(`print-filter-${type}-frequency`).textContent = `${filter.frequency.value} Hz`; + + render(); + }); + + document.getElementById(`range-filter-${type}-detune`).addEventListener('input', (event) => { + filter.detune.value = event.currentTarget.valueAsNumber; + + document.getElementById(`print-filter-${type}-detune`).textContent = `${filter.detune.value} cent`; + + render(); + }); + + document.getElementById(`range-filter-${type}-Q`).addEventListener('input', (event) => { + filter.Q.value = event.currentTarget.valueAsNumber; + + if (type === 'lowpass' || type === 'highpass') { + document.getElementById(`print-filter-${type}-Q`).textContent = `${filter.Q.value} dB`; + } else { + document.getElementById(`print-filter-${type}-Q`).textContent = `${filter.Q.value}`; + } + + render(); + }); + + if (type === 'lowshelf' || type === 'highshelf' || type === 'peaking') { + document.getElementById(`range-filter-${type}-gain`).addEventListener('input', (event) => { + filter.gain.value = event.currentTarget.valueAsNumber; + + render(); + }); + } + + render(); +}; + createCoordinateRect(document.getElementById('svg-figure-sin-function')); createSinFunctionPath(document.getElementById('svg-figure-sin-function')); @@ -7019,3 +7188,5 @@ createNodeConnectionsForRingmodulator(document.getElementById('svg-figure-node-c ringmodulator(); animateAM(document.getElementById('svg-animation-amplitude-modulation-time'), document.getElementById('svg-animation-amplitude-modulation-spectrum')); + +renderFrequencyResponse(document.getElementById('svg-figure-filter-response-lowpass'), 'lowpass'); diff --git a/docs/index.html b/docs/index.html index 57d5d2f..1dedfdd 100644 --- a/docs/index.html +++ b/docs/index.html @@ -7837,6 +7837,37 @@
BiquadFilterNode の定義式

IIR フィルタに関してはあとのセクションで解説します.

+
+
Low-Pass Filter
+

+ Low-Pass Filter (低域通過フィルタ) とは, カットオフ周波数 ($\mathrm{f}_{\mathrm{computed}}$) 付近までの周波数成分を通過させ, それより大きい周波数成分を遮断するフィルタです. すでに解説しましたが, サンプリング定理のために, A/D 変換や + D/A 変換で使われたり, あとのエフェクターのワウで使われたり, エフェクト音のトーンを設定したりするなど, + おそらく最も使用頻度の高いフィルタになります (おそらくその理由で, デフォルト値になっていると思われます). +

+

+ Low-Pass Filter における, Q プロパティ (クオリティファクタと呼ばれることもあります) は, + カットオフ周波数付近の急峻を変化させます. 正の値にすると, 急峻が増幅して, カットオフ周波数付近の周波数成分を増幅させます (これは, + ワウの実装において重要になる点です). 負の値を設定すると, カットオフ周波数付近の周波数成分を減衰させるフィルタ特性になります. +

+

+ Low-Pass Filter においては, gain プロパティは無効で, フィルタ特性に影響を与えることはありません. +

+
+
+ + + 350 Hz + + + 0 cent + + + 1 dB +
+ +
Low-Pass Filter のフィルタ特性
+
+