Skip to content

Commit

Permalink
feat: Low-Pass Filter (BiquadFilterNode)
Browse files Browse the repository at this point in the history
  • Loading branch information
Korilakkuma committed Nov 14, 2024
1 parent 430b374 commit ce52ca4
Show file tree
Hide file tree
Showing 2 changed files with 202 additions and 0 deletions.
171 changes: 171 additions & 0 deletions docs/docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));

Expand Down Expand Up @@ -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');
31 changes: 31 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7837,6 +7837,37 @@ <h5>BiquadFilterNode の定義式</h5>
</p>
<p>IIR フィルタに関してはあとのセクションで解説します.</p>
</article>
<section id="section-effectors-filter-biquad-filter-node-lowpass">
<h5>Low-Pass Filter</h5>
<p>
<b>Low-Pass Filter</b> (<b>低域通過フィルタ</b>) とは, <b>カットオフ周波数</b> (<span class="inline-math">$\mathrm{f}_{\mathrm{computed}}$</span>) 付近までの周波数成分を通過させ, それより大きい周波数成分を遮断するフィルタです. すでに解説しましたが, サンプリング定理のために, A/D 変換や
D/A 変換で使われたり, あとのエフェクターのワウで使われたり, エフェクト音のトーンを設定したりするなど,
おそらく最も使用頻度の高いフィルタになります (おそらくその理由で, デフォルト値になっていると思われます).
</p>
<p>
Low-Pass Filter における, <b><code>Q</code></b> プロパティ (<b>クオリティファクタ</b>と呼ばれることもあります) は,
カットオフ周波数付近の急峻を変化させます. 正の値にすると, 急峻が増幅して, カットオフ周波数付近の周波数成分を増幅させます (これは,
ワウの実装において重要になる点です). 負の値を設定すると, カットオフ周波数付近の周波数成分を減衰させるフィルタ特性になります.
</p>
<p>
Low-Pass Filter においては, <b><code>gain</code></b> プロパティは無効で, フィルタ特性に影響を与えることはありません.
</p>
<figure>
<div class="app-headline">
<label for="range-filter-lowpass-frequency">frequency</label>
<input type="range" id="range-filter-lowpass-frequency" value="350" min="1" max="8000" step="1" />
<span id="print-filter-lowpass-frequency">350 Hz</span>
<label for="range-filter-lowpass-detune">detune</label>
<input type="range" id="range-filter-lowpass-detune" value="0" min="-1200" max="1200" step="1" />
<span id="print-filter-lowpass-detune">0 cent</span>
<label for="range-filter-lowpass-Q">Q</label>
<input type="range" id="range-filter-lowpass-Q" value="1" min="-20" max="20" step="1" />
<span id="print-filter-lowpass-Q">1 dB</span>
</div>
<svg id="svg-figure-filter-response-lowpass" width="600" height="300" />
<figcaption>Low-Pass Filter のフィルタ特性</figcaption>
</figure>
</section>
</section>
</section>
</section>
Expand Down

0 comments on commit ce52ca4

Please sign in to comment.