Ver3.5 ๐ต Web Audio API๋ฅผ ํ์ฉํ ๋ธ๋ผ์ฐ์ ๊ธฐ๋ฐ ์ํฅ ์ฒ๋ฆฌ ์์ ์ ๋ณต

๐ต Web Audio API๋ฅผ ํ์ฉํ ๋ธ๋ผ์ฐ์ ๊ธฐ๋ฐ ์ํฅ ์ฒ๋ฆฌ ์์ ์ ๋ณต
ํ๋ฌ๊ทธ์ธ ์์ด, ์ค์น ์์ด, ์ค์ง JavaScript๋ง์ผ๋ก ๋ธ๋ผ์ฐ์ ์์ ์์ ์ ๋ง๋ค๊ณ ๋ถ์ํ๊ณ ์กฐ์ํ๋ ๋ง๋ฒ ๊ฐ์ ์ธ๊ณ๋ก ๋ ๋๋ณด์! ๐
์์ ์๋ ์์
์์
์ ํ๋ ค๋ฉด Pro Tools, Ableton Live, Logic Pro ๊ฐ์ ์ ๋ฌธ DAW(Digital Audio Workstation)๋ฅผ ์ค์นํด์ผ ํ์ด.
๊ทผ๋ฐ ์ง๊ธ์? ํฌ๋กฌ ๋ธ๋ผ์ฐ์ ํ๋๋ง ์์ผ๋ฉด ๋ผ. ์ง์ง์ผ. ๐ฒ
Web Audio API๋ W3C์์ ํ์คํํ ๊ณ ์์ค JavaScript API๋ก, ๋ธ๋ผ์ฐ์ ์์์ ์ค๋์ค๋ฅผ ์์ฑยท์ฒ๋ฆฌยท๋ถ์ยท์ถ๋ ฅํ๋ ๋ชจ๋ ์์
์ ๊ฐ๋ฅํ๊ฒ ํด์ค.
๋จ์ํ mp3 ํ์ผ ์ฌ์ํ๋ ์์ค์ด ์๋๋ผ, ์ดํ๋ผ์ด์ , ๋ฆฌ๋ฒ๋ธ, ๋๋ ์ด, ์ปดํ๋ ์, ์ ๋์ฌ์ด์ , ์คํํธ๋ผ ๋ถ์๊ธฐ๊น์ง ์ ๋ถ ๊ตฌํํ ์ ์์ด.
์ด ๊ธ์์๋ Web Audio API์ ํต์ฌ ๊ฐ๋
๋ถํฐ ์ค์ ์ฝ๋๊น์ง ์น๊ตฌํํ
์ค๋ช
ํ๋ฏ ์ญ ํ์ด๋ณผ๊ฒ. ์ปคํผ ํ ์ ๋ค๊ณ ํธํ๊ฒ ์ฝ์ด๋ด โ
Web Audio API์ ๋ชจ๋ ๊ฒ์ AudioContext์์ ์์ํด.
AudioContext๋ ์ผ์ข
์ "์ํฅ ์์
์ค" ๊ฐ์ ๊ฑฐ์ผ. ๋ชจ๋ ๋
ธ๋๋ฅผ ๋ง๋ค๊ณ , ์ฐ๊ฒฐํ๊ณ , ๊ด๋ฆฌํ๋ ์ค์ ํ๋ธ ์ญํ ์ ํด.
AudioContext ์์ด๋ ์๋ฌด๊ฒ๋ ํ ์ ์์ด. ๋ง์น ์ ๊ธฐ ์์ด ๊ธฐํ ์ฐํ๋ฅผ ์ผค ์ ์๋ ๊ฒ์ฒ๋ผ ๐
// ๊ธฐ๋ณธ AudioContext ์์ฑ
const audioCtx = new AudioContext();
// Safari ํธํ์ฑ์ ์ํ ์ฒ๋ฆฌ (๊ตฌ๋ฒ์ Safari๋ webkitAudioContext ์ฌ์ฉ)
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioCtx = new AudioContext();
// ์ํ๋ ์ดํธ ์ง์ (๊ธฐ๋ณธ๊ฐ์ ๋ณดํต 44100Hz ๋๋ 48000Hz)
const audioCtx = new AudioContext({ sampleRate: 44100 });
์ด๊ฑด ์๋ ์ฌ์ ์ ์ฑ (Autoplay Policy) ๋๋ฌธ์ธ๋ฐ, ์ฌ์ฉ์๊ฐ ๋ฒํผ์ ํด๋ฆญํ๋ ๋ฑ์ ํ๋์ ํ ํ์ AudioContext๋ฅผ ์์ฑํ๊ฑฐ๋ resume()์ ํธ์ถํด์ผ ํด.
// ์ฌ์ฉ์ ํด๋ฆญ ํ AudioContext ์์ํ๋ ํจํด
let audioCtx;
document.getElementById('startBtn').addEventListener('click', () => {
if (!audioCtx) {
audioCtx = new AudioContext();
}
// suspended ์ํ๋ผ๋ฉด resume
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
});
| ์์ฑ | ์ค๋ช | ์์ ๊ฐ |
|---|---|---|
sampleRate |
์ด๋น ์ํ ์ (Hz) | 44100, 48000 |
currentTime |
์ปจํ ์คํธ ์์ฑ ํ ๊ฒฝ๊ณผ ์๊ฐ (์ด) | 3.142... |
state |
ํ์ฌ ์ํ | suspended / running / closed |
destination |
์ต์ข ์ถ๋ ฅ ๋ ธ๋ (์คํผ์ปค) | AudioDestinationNode |
listener |
3D ๊ณต๊ฐ์ํฅ ๋ฆฌ์ค๋ | AudioListener |
Web Audio API์ ํต์ฌ ์ฒ ํ์ "๋
ธ๋ ๊ทธ๋ํ(Node Graph)"์ผ.
๊ฐ๊ฐ์ ์ค๋์ค ์ฒ๋ฆฌ ๋จ๊ณ๋ฅผ "๋
ธ๋"๋ผ๋ ๋ธ๋ก์ผ๋ก ๋ง๋ค๊ณ , ์ด ๋ธ๋ก๋ค์ ๋ ๊ณ ์ฒ๋ผ ์ฐ๊ฒฐํด์ ์ํ๋ ์ํฅ ์ฒ๋ฆฌ ํ์ดํ๋ผ์ธ์ ๊ตฌ์ฑํ๋ ๊ฑฐ์ผ.
๋
ธ๋์ ์ข
๋ฅ๋ ํฌ๊ฒ ์ธ ๊ฐ์ง๋ก ๋๋์ด:
OscillatorNode๋ ์ํ์ ์ผ๋ก ์ ์๋ ํํ์ ์์ฑํ๋ ์์ค ๋
ธ๋์ผ.
์ ๋์ฌ์ด์ ์ ๊ธฐ๋ณธ ์ค์ ๊ธฐ๋ณธ์ด๋ผ๊ณ ํ ์ ์์ด. ์ฌ์ธํ, ์ฌ๊ฐํ, ์ผ๊ฐํ, ํฑ๋ํ๋ฅผ ์ง์ํด.
const audioCtx = new AudioContext();
// ์ค์ค๋ ์ดํฐ ์์ฑ
const oscillator = audioCtx.createOscillator();
// ํํ ํ์
์ค์ : 'sine' | 'square' | 'sawtooth' | 'triangle' | 'custom'
oscillator.type = 'sine';
// ์ฃผํ์ ์ค์ (Hz) โ 440Hz = ๋ผ(A4) ์
oscillator.frequency.setValueAtTime(440, audioCtx.currentTime);
// ์ถ๋ ฅ์ ์ฐ๊ฒฐ
oscillator.connect(audioCtx.destination);
// ์์ (0 = ์ฆ์ ์์)
oscillator.start(0);
// 2์ด ํ ์ ์ง
oscillator.stop(audioCtx.currentTime + 2);
๋ฐ์ ์ฌ๋ผ๊ฐ ๋๋ง๋ค ์ฃผํ์์ 2^(1/12) โ 1.0595๋ฅผ ๊ณฑํ๋ฉด ๋ผ.
์: A4(440) ร 1.0595 = A#4(466.16Hz)
์ค์ ์ค๋์ค ํ์ผ(mp3, wav, ogg ๋ฑ)์ ๋ถ๋ฌ์์ ์ฌ์ํ ๋ ์ฌ์ฉํ๋ ๋
ธ๋์ผ.
fetch API๋ก ํ์ผ์ ๊ฐ์ ธ์ค๊ณ , decodeAudioData๋ก ๋์ฝ๋ฉํ ํ ์ฌ์ํ๋ ํจํด์ ์จ.
async function loadAndPlayAudio(url) {
const audioCtx = new AudioContext();
// 1. ํ์ผ fetch
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
// 2. ์ค๋์ค ๋ฐ์ดํฐ ๋์ฝ๋ฉ
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
// 3. ์์ค ๋
ธ๋ ์์ฑ ๋ฐ ๋ฒํผ ์ฐ๊ฒฐ
const source = audioCtx.createBufferSource();
source.buffer = audioBuffer;
// 4. ๋ฃจํ ์ค์ (์ ํ)
source.loop = true;
// 5. ์ฌ์ ์๋ ์กฐ์ (1.0 = ์ ์, 2.0 = 2๋ฐฐ์)
source.playbackRate.value = 1.0;
// 6. ์ถ๋ ฅ ์ฐ๊ฒฐ ํ ์ฌ์
source.connect(audioCtx.destination);
source.start(0);
return source;
}
์ฌ์์ด ๋๋๋ฉด ๋ค์ ์ฌ์ฉํ ์ ์๊ณ , ์๋ก์ด AudioBufferSourceNode๋ฅผ ๋ง๋ค์ด์ผ ํด.
์ด๊ฑด ์ค๊ณ ์๋์ผ โ ๋ฉ๋ชจ๋ฆฌ ํจ์จ๊ณผ ํ์ด๋ฐ ์ ํ์ฑ์ ์ํ ์ ํ์ด๊ฑฐ๋ .
GainNode๋ ์ ํธ์ ์งํญ(๋ณผ๋ฅจ)์ ์กฐ์ ํ๋ ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ์ฒ๋ฆฌ ๋
ธ๋์ผ.
gain ๊ฐ์ด 1.0์ด๋ฉด ์๋ณธ ๊ทธ๋๋ก, 0.5๋ฉด ์ ๋ฐ ๋ณผ๋ฅจ, 0์ด๋ฉด ๋ฌด์, 2.0์ด๋ฉด 2๋ฐฐ ๋ณผ๋ฅจ์ด์ผ.
const gainNode = audioCtx.createGain();
// ์ฆ์ ๋ณผ๋ฅจ ์ค์
gainNode.gain.value = 0.8;
// ์๊ฐ์ ๋ฐ๋ฅธ ๋ณผ๋ฅจ ๋ณํ (ํ์ด๋ ์ธ ํจ๊ณผ)
gainNode.gain.setValueAtTime(0, audioCtx.currentTime);
gainNode.gain.linearRampToValueAtTime(1.0, audioCtx.currentTime + 2);
// ํ์ด๋ ์์ (์ง์์ ๊ฐ์ โ ๋ ์์ฐ์ค๋ฌ์ด ์๋ฆฌ)
gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 3);
// ์ฐ๊ฒฐ
source.connect(gainNode);
gainNode.connect(audioCtx.destination);
BiquadFilterNode๋ ํน์ ์ฃผํ์ ๋์ญ์ ๊ฐ์กฐํ๊ฑฐ๋ ๊ฐ์ ์ํค๋ ํํฐ์ผ.
์ดํ๋ผ์ด์ (EQ)๋ฅผ ๋ง๋ค ๋ ํต์ฌ์ด ๋๋ ๋
ธ๋์ด๊ณ , ๋ค์ํ ํํฐ ํ์
์ ์ง์ํด.
| ํํฐ ํ์ | ์ค๋ช | ์ฉ๋ |
|---|---|---|
lowpass |
์ค์ ์ฃผํ์ ์ดํ๋ง ํต๊ณผ | ๊ณ ์ ์ ๊ฑฐ, ๋จน๋จนํ ํจ๊ณผ |
highpass |
์ค์ ์ฃผํ์ ์ด์๋ง ํต๊ณผ | ์ ์ ์ ๊ฑฐ, ๋ผ๋์ค ํจ๊ณผ |
bandpass |
ํน์ ๋์ญ๋ง ํต๊ณผ | ํน์ ์์ญ ๊ฐ์กฐ |
lowshelf |
์ ์ ๋์ญ ๋ถ์คํธ/์ปท | ๋ฒ ์ด์ค ์กฐ์ |
highshelf |
๊ณ ์ ๋์ญ ๋ถ์คํธ/์ปท | ํธ๋ ๋ธ ์กฐ์ |
peaking |
ํน์ ์ฃผํ์ ๋ถ์คํธ/์ปท | ํ๋ผ๋ฉํธ๋ฆญ EQ |
notch |
ํน์ ์ฃผํ์ ์ ๊ฑฐ | ํ ๋ ธ์ด์ฆ ์ ๊ฑฐ |
allpass |
์์๋ง ๋ณ๊ฒฝ | ํ์ด์ ํจ๊ณผ |
// ๋ก์ฐํจ์ค ํํฐ (์ ์๋ง ํต๊ณผ)
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 1000; // 1kHz ์ดํ๋ง ํต๊ณผ
filter.Q.value = 1.0; // ๊ณต์ง (๋ ์นด๋ก์)
// ํผํน EQ (ํน์ ์ฃผํ์ ๋ถ์คํธ)
const peakFilter = audioCtx.createBiquadFilter();
peakFilter.type = 'peaking';
peakFilter.frequency.value = 2000; // 2kHz
peakFilter.gain.value = 6; // +6dB ๋ถ์คํธ
peakFilter.Q.value = 2.0;
// 3๋ฐด๋ EQ ๊ตฌ์ฑ
const lowEQ = audioCtx.createBiquadFilter();
lowEQ.type = 'lowshelf';
lowEQ.frequency.value = 200;
lowEQ.gain.value = 3; // ์ ์ +3dB
const midEQ = audioCtx.createBiquadFilter();
midEQ.type = 'peaking';
midEQ.frequency.value = 1000;
midEQ.gain.value = -2; // ์ค์ -2dB
const highEQ = audioCtx.createBiquadFilter();
highEQ.type = 'highshelf';
highEQ.frequency.value = 8000;
highEQ.gain.value = 4; // ๊ณ ์ +4dB
// ์ฒด์ธ ์ฐ๊ฒฐ
source.connect(lowEQ);
lowEQ.connect(midEQ);
midEQ.connect(highEQ);
highEQ.connect(audioCtx.destination);
DelayNode๋ ์ค๋์ค ์ ํธ๋ฅผ ์ง์ ํ ์๊ฐ๋งํผ ์ง์ฐ์์ผ์ ์ถ๋ ฅํด.
์์ฝ ํจ๊ณผ๋ ์ฌ๋ฉ๋ฐฑ ๋๋ ์ด ๊ฐ์ ์ดํํธ๋ฅผ ๋ง๋ค ๋ ์ฌ์ฉํด.
// ๋๋ ์ด ๋
ธ๋ ์์ฑ (์ต๋ ๋๋ ์ด ์๊ฐ ์ง์ )
const delay = audioCtx.createDelay(5.0); // ์ต๋ 5์ด
delay.delayTime.value = 0.3; // 0.3์ด ๋๋ ์ด
// ํผ๋๋ฐฑ ๋๋ ์ด (์์ฝ ํจ๊ณผ)
const feedbackGain = audioCtx.createGain();
feedbackGain.gain.value = 0.5; // 50% ํผ๋๋ฐฑ
// ์ฐ๊ฒฐ: source โ delay โ output
// โ_feedbackGain_โ (ํผ๋๋ฐฑ ๋ฃจํ)
source.connect(delay);
delay.connect(feedbackGain);
feedbackGain.connect(delay); // ํผ๋๋ฐฑ ๋ฃจํ
delay.connect(audioCtx.destination);
source.connect(audioCtx.destination); // ๋๋ผ์ด ์ ํธ๋ ์ถ๋ ฅ
ConvolverNode๋ ์ํ์ค ์๋ต(Impulse Response, IR)์ ์ด์ฉํ ์ปจ๋ณผ๋ฃจ์
๋ฆฌ๋ฒ๋ธ๋ฅผ ๊ตฌํํด.
์ค์ ๊ณต๊ฐ(์ฝ์ํธํ, ๋๊ตด, ์์ค ๋ฑ)์ IR ํ์ผ์ ์ฌ์ฉํ๋ฉด ๊ทธ ๊ณต๊ฐ์ ์๋ ๊ฒ ๊ฐ์ ๋ฆฌ๋ฒ๋ธ๋ฅผ ๋ง๋ค ์ ์์ด.
์ด๊ฒ ์ง์ง ์ ๊ธฐํ ๊ธฐ์ ์ธ๋ฐ, ์ค์ ๊ณต๊ฐ์์ ๋
น์ํ ์ํ์ค ์๋ต ๋ฐ์ดํฐ๋ฅผ ์ค๋์ค ์ ํธ์ ์ํ์ ์ผ๋ก ํฉ์ฑ(์ปจ๋ณผ๋ฃจ์
)ํด์ ๊ทธ ๊ณต๊ฐ์ ์ํฅ ํน์ฑ์ ์ฌํํ๋ ๊ฑฐ์ผ.
async function createReverb(audioCtx, irUrl) {
const convolver = audioCtx.createConvolver();
// IR ํ์ผ ๋ก๋
const response = await fetch(irUrl);
const arrayBuffer = await response.arrayBuffer();
convolver.buffer = await audioCtx.decodeAudioData(arrayBuffer);
return convolver;
}
// ๋๋ผ์ด/์ป ๋ฏน์ค (๋ฆฌ๋ฒ๋ธ ์ ์กฐ์ )
const dryGain = audioCtx.createGain();
const wetGain = audioCtx.createGain();
dryGain.gain.value = 0.7; // ์๋ณธ 70%
wetGain.gain.value = 0.3; // ๋ฆฌ๋ฒ๋ธ 30%
source.connect(dryGain);
source.connect(convolver);
convolver.connect(wetGain);
dryGain.connect(audioCtx.destination);
wetGain.connect(audioCtx.destination);
์ปดํ๋ ์๋ ์ค๋์ค ์ ํธ์ ๋ค์ด๋๋ฏน ๋ ์ธ์ง๋ฅผ ์ค์ฌ์ฃผ๋ ์ดํํธ์ผ.
๋๋ฌด ํฐ ์๋ฆฌ๋ ์ค์ด๊ณ , ์ ์ฒด์ ์ธ ๋ณผ๋ฅจ์ ๊ท ์ผํ๊ฒ ๋ง๋ค์ด์ค.
๋ง์คํฐ๋ง ์ฒด์ธ์์ ํ์์ ์ผ๋ก ์ฌ์ฉ๋๋ ๋๊ตฌ์ผ.
const compressor = audioCtx.createDynamicsCompressor();
// ์ค๋ ์๋: ์ด ๋ ๋ฒจ ์ด์์ ์ ํธ๋ฅผ ์์ถ (-24dB)
compressor.threshold.value = -24;
// ๋: ์์ถ์ด ์์๋๋ ๋ถ๋๋ฌ์ (30dB)
compressor.knee.value = 30;
// ๋ ์ด์ผ: ์์ถ ๋น์จ (12:1)
compressor.ratio.value = 12;
// ์ดํ: ์์ถ ์์๊น์ง ๊ฑธ๋ฆฌ๋ ์๊ฐ (0.003์ด)
compressor.attack.value = 0.003;
// ๋ฆด๋ฆฌ์ฆ: ์์ถ ํด์ ๊น์ง ๊ฑธ๋ฆฌ๋ ์๊ฐ (0.25์ด)
compressor.release.value = 0.25;
source.connect(compressor);
compressor.connect(audioCtx.destination);
AnalyserNode๋ ์ค๋์ค ์ ํธ๋ฅผ ๋ถ์ํด์ ์๊ฐํํ ์ ์๋ ๋ฐ์ดํฐ๋ฅผ ์ ๊ณตํด.
์ฃผํ์ ์คํํธ๋ผ ๋ถ์(FFT)์ด๋ ํํ(Waveform) ๋ฐ์ดํฐ๋ฅผ ์ค์๊ฐ์ผ๋ก ๊ฐ์ ธ์ฌ ์ ์์ด.
์ด๊ฑธ Canvas๋ WebGL๊ณผ ๊ฒฐํฉํ๋ฉด ๋ฉ์ง ์ค๋์ค ๋น์ฃผ์ผ๋ผ์ด์ ๋ฅผ ๋ง๋ค ์ ์์ด! ๐จ
const analyser = audioCtx.createAnalyser();
// FFT ํฌ๊ธฐ ์ค์ (2์ ๊ฑฐ๋ญ์ ๊ณฑ, 32~32768)
// ํด์๋ก ์ฃผํ์ ํด์๋ ๋์์ง์ง๋ง ์๊ฐ ํด์๋ ๋ฎ์์ง
analyser.fftSize = 2048;
// ์ค๋ฌด๋ฉ (0~1, ๋์์๋ก ๋ถ๋๋ฝ๊ฒ)
analyser.smoothingTimeConstant = 0.8;
// ์ต์/์ต๋ ๋ฐ์๋ฒจ ๋ฒ์
analyser.minDecibels = -90;
analyser.maxDecibels = -10;
// ์ฃผํ์ ๋ฐ์ดํฐ ๋ฐฐ์ด (frequencyBinCount = fftSize / 2)
const bufferLength = analyser.frequencyBinCount; // 1024
const dataArray = new Uint8Array(bufferLength);
// ์ฐ๊ฒฐ (๋ถ์ ๋
ธ๋๋ ์ ํธ๋ฅผ ํต๊ณผ์ํด)
source.connect(analyser);
analyser.connect(audioCtx.destination);
// ์ค์๊ฐ ์ฃผํ์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
function getFrequencyData() {
analyser.getByteFrequencyData(dataArray);
// dataArray์ 0~255 ๋ฒ์์ ์ฃผํ์ ๋ฐ์ดํฐ๊ฐ ๋ด๊น
return dataArray;
}
// ์ค์๊ฐ ํํ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
function getTimeDomainData() {
analyser.getByteTimeDomainData(dataArray);
return dataArray;
}
AnalyserNode์์ ๊ฐ์ ธ์จ ๋ฐ์ดํฐ๋ฅผ Canvas์ ๊ทธ๋ ค์ ์ค์๊ฐ ๋น์ฃผ์ผ๋ผ์ด์ ๋ฅผ ๋ง๋ค์ด๋ณด์!
const canvas = document.getElementById('visualizer');
const canvasCtx = canvas.getContext('2d');
function drawVisualizer() {
requestAnimationFrame(drawVisualizer);
// ์ฃผํ์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
analyser.getByteFrequencyData(dataArray);
// ๋ฐฐ๊ฒฝ ์ง์ฐ๊ธฐ
canvasCtx.fillStyle = '#1e2a35';
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
const barWidth = (canvas.width / bufferLength) * 2.5;
let barHeight;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
barHeight = dataArray[i] / 255 * canvas.height;
// ์ฃผํ์์ ๋ฐ๋ผ ์์ ๋ณํ
const hue = (i / bufferLength) * 240;
canvasCtx.fillStyle = `hsl(${hue}, 70%, 60%)`;
canvasCtx.fillRect(
x,
canvas.height - barHeight,
barWidth,
barHeight
);
x += barWidth + 1;
}
}
drawVisualizer();
Web Audio API๋ ๋ง์ดํฌ ์
๋ ฅ๋ ์ฒ๋ฆฌํ ์ ์์ด!
getUserMedia API๋ก ๋ง์ดํฌ ์คํธ๋ฆผ์ ๊ฐ์ ธ์์ Web Audio API์ ๋
ธ๋ ๊ทธ๋ํ์ ์ฐ๊ฒฐํ๋ฉด ๋ผ.
์ค์๊ฐ ์์ฑ ์ฒ๋ฆฌ, ๋ณด์ด์ค ์ดํํฐ, ์์ฑ ๋ถ์ ๋ฑ์ ๊ตฌํํ ์ ์์ด.
async function setupMicrophone() {
const audioCtx = new AudioContext();
// ๋ง์ดํฌ ๊ถํ ์์ฒญ ๋ฐ ์คํธ๋ฆผ ํ๋
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true, // ์์ฝ ์บ์ฌ๋ง
noiseSuppression: true, // ๋
ธ์ด์ฆ ์ต์
sampleRate: 44100
}
});
// ๋ง์ดํฌ ์คํธ๋ฆผ์ Web Audio API ์์ค๋ก ๋ณํ
const micSource = audioCtx.createMediaStreamSource(stream);
// ๋ถ์ ๋
ธ๋ ์ฐ๊ฒฐ
const analyser = audioCtx.createAnalyser();
micSource.connect(analyser);
// ์ดํํธ ์ถ๊ฐ (์: ํผ์น ์ํํฐ ํจ๊ณผ๋ฅผ ์ํ ๋๋ ์ด)
const gainNode = audioCtx.createGain();
analyser.connect(gainNode);
gainNode.connect(audioCtx.destination);
return { micSource, analyser, gainNode };
}
// ๋ง์ดํฌ ์ค์ง
function stopMicrophone(stream) {
stream.getTracks().forEach(track => track.stop());
}
- ์์ ๊ฐ์ง (ํผ์น ๋ํ ์ )
- ์๋ ๋ฏธํฐ (VU Meter)
- ๋ณด์ด์ค ์ดํํฐ (๋ก๋ด ๋ชฉ์๋ฆฌ, ์์ฝ ๋ฑ)
- ์์ฑ ํ๋ ๊ฐ์ง (VAD, Voice Activity Detection)
๋ง์ดํฌ ์
๋ ฅ์์ ํ์ฌ ์์ (์ฃผํ์)์ ๊ฐ์งํ๋ ๊ฑด ๊ฝค ๋ณต์กํ ์๊ณ ๋ฆฌ์ฆ์ด ํ์ํด.
๊ฐ์ฅ ๊ฐ๋จํ ๋ฐฉ๋ฒ์ ์๊ธฐ์๊ด(Autocorrelation) ์๊ณ ๋ฆฌ์ฆ์ ์ฌ์ฉํ๋ ๊ฑฐ์ผ.
// ์๊ธฐ์๊ด ๊ธฐ๋ฐ ํผ์น ๊ฐ์ง
function detectPitch(analyser, audioCtx) {
const bufferLength = analyser.fftSize;
const buffer = new Float32Array(bufferLength);
analyser.getFloatTimeDomainData(buffer);
// ์๊ธฐ์๊ด ๊ณ์ฐ
let r1 = 0, r2 = bufferLength - 1, threshold = 0.2;
// ์ ํธ ์์์ ์ฐพ๊ธฐ
for (let i = 0; i < bufferLength / 2; i++) {
if (Math.abs(buffer[i]) < threshold) { r1 = i; break; }
}
for (let i = 1; i < bufferLength / 2; i++) {
if (Math.abs(buffer[bufferLength - i]) < threshold) { r2 = bufferLength - i; break; }
}
const buf2 = buffer.slice(r1, r2);
const c = new Array(buf2.length).fill(0);
for (let i = 0; i < buf2.length; i++) {
for (let j = 0; j < buf2.length - i; j++) {
c[i] += buf2[j] * buf2[j + i];
}
}
// ํผํฌ ์ฐพ๊ธฐ
let d = 0;
while (c[d] > c[d + 1]) d++;
let maxVal = -1, maxPos = -1;
for (let i = d; i < buf2.length; i++) {
if (c[i] > maxVal) { maxVal = c[i]; maxPos = i; }
}
// ์ฃผํ์ ๊ณ์ฐ
const frequency = audioCtx.sampleRate / maxPos;
return frequency;
}
Web Audio API์ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ ์ค ํ๋๊ฐ ๋ฐ๋ก AudioParam์ด์ผ.
AudioParam์ ๋
ธ๋์ ํ๋ผ๋ฏธํฐ(์ฃผํ์, ๊ฒ์ธ, ๋๋ ์ด ์๊ฐ ๋ฑ)๋ฅผ ์๊ฐ์ ๋ฐ๋ผ ์ ๋ฐํ๊ฒ ์ ์ดํ ์ ์๊ฒ ํด์ค.
๋จ์ํ ๊ฐ์ ๋ฐ๊พธ๋ ๊ฒ ์๋๋ผ, ๋ฏธ๋์ ํน์ ์์ ์ ์ด๋ค ๊ฐ์ด ๋์ด์ผ ํ๋์ง ์ค์ผ์ค๋งํ ์ ์์ด.
์ด๊ฒ ๋ฐ๋ก Web Audio API๊ฐ ์์
์ ์์ ์ ํฉํ ์ด์ ์ผ โ ์ํ ๋จ์์ ์ ํํ ํ์ด๋ฐ ์ ์ด๊ฐ ๊ฐ๋ฅํ๊ฑฐ๋ !
| ๋ฉ์๋ | ์ค๋ช |
|---|---|
setValueAtTime(value, time) |
ํน์ ์๊ฐ์ ์ฆ์ ๊ฐ ๋ณ๊ฒฝ |
linearRampToValueAtTime(value, time) |
์ ํ์ ์ผ๋ก ๊ฐ ๋ณํ |
exponentialRampToValueAtTime(value, time) |
์ง์์ ์ผ๋ก ๊ฐ ๋ณํ (๋ ์์ฐ์ค๋ฌ์ด ์๋ฆฌ) |
setTargetAtTime(target, startTime, timeConstant) |
์ง์์ ์ผ๋ก ๋ชฉํ๊ฐ์ ์ ๊ทผ |
setValueCurveAtTime(values, startTime, duration) |
์ปค์คํ ์ปค๋ธ๋ก ๊ฐ ๋ณํ |
cancelScheduledValues(startTime) |
์์ฝ๋ ๋ณํ ์ทจ์ |
ADSR(Attack, Decay, Sustain, Release)์ ์ ๋์ฌ์ด์ ์์ ์๋ฆฌ์ ์๊ฐ์ ํํ๋ฅผ ์ ์ํ๋ ์๋ฒจ๋กํ์ผ.
AudioParam์ ์๋ํ ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋ฉด ์๋ฒฝํ ADSR ์๋ฒจ๋กํ๋ฅผ ๊ตฌํํ ์ ์์ด.
function createADSREnvelope(audioCtx, gainNode, options = {}) {
const {
attack = 0.01, // ์ดํ ์๊ฐ (์ด)
decay = 0.1, // ๋์ผ์ด ์๊ฐ (์ด)
sustain = 0.7, // ์์คํ
์ธ ๋ ๋ฒจ (0~1)
release = 0.3, // ๋ฆด๋ฆฌ์ฆ ์๊ฐ (์ด)
peak = 1.0 // ํผํฌ ๋ ๋ฒจ
} = options;
const now = audioCtx.currentTime;
const gain = gainNode.gain;
// ์ด๊ธฐ๊ฐ 0์ผ๋ก ์ค์
gain.setValueAtTime(0, now);
// Attack: 0 โ peak
gain.linearRampToValueAtTime(peak, now + attack);
// Decay: peak โ sustain
gain.exponentialRampToValueAtTime(sustain, now + attack + decay);
// Sustain: ์ ์ง (๋
ธํธ ์คํ ์๊น์ง)
// Release๋ ๋
ธํธ ์คํ ์ด๋ฒคํธ์์ ์ฒ๋ฆฌ
return {
noteOff: (releaseTime = audioCtx.currentTime) => {
gain.cancelScheduledValues(releaseTime);
gain.setValueAtTime(gain.value, releaseTime);
// Release: sustain โ 0
gain.exponentialRampToValueAtTime(0.001, releaseTime + release);
}
};
}
์ง๊ธ๊น์ง ๋ฐฐ์ด ๋ด์ฉ์ ์ข
ํฉํด์ ๊ฐ๋จํ ํด๋ฆฌํฌ๋ ์ ๋์ฌ์ด์ ๋ฅผ ๋ง๋ค์ด๋ณด์!
ํค๋ณด๋ ์
๋ ฅ์ผ๋ก ์์ ์ฐ์ฃผํ๊ณ , ADSR ์๋ฒจ๋กํ์ ํํฐ๋ฅผ ์ ์ฉํ๋ ๊ธฐ๋ณธ ์ ์ค์ผ.
class BrowserSynth {
constructor() {
this.audioCtx = null;
this.activeNotes = new Map();
this.masterGain = null;
this.filter = null;
this.compressor = null;
this.waveType = 'sawtooth';
}
init() {
this.audioCtx = new AudioContext();
// ๋ง์คํฐ ์ฒด์ธ ๊ตฌ์ฑ
this.filter = this.audioCtx.createBiquadFilter();
this.filter.type = 'lowpass';
this.filter.frequency.value = 8000;
this.filter.Q.value = 2.0;
this.masterGain = this.audioCtx.createGain();
this.masterGain.gain.value = 0.7;
this.compressor = this.audioCtx.createDynamicsCompressor();
this.compressor.threshold.value = -20;
this.compressor.ratio.value = 8;
// ์ฒด์ธ ์ฐ๊ฒฐ
this.filter.connect(this.masterGain);
this.masterGain.connect(this.compressor);
this.compressor.connect(this.audioCtx.destination);
}
noteOn(frequency) {
if (this.activeNotes.has(frequency)) return;
const osc = this.audioCtx.createOscillator();
const envGain = this.audioCtx.createGain();
osc.type = this.waveType;
osc.frequency.value = frequency;
// ADSR ์ ์ฉ
const now = this.audioCtx.currentTime;
envGain.gain.setValueAtTime(0, now);
envGain.gain.linearRampToValueAtTime(0.8, now + 0.01);
envGain.gain.exponentialRampToValueAtTime(0.5, now + 0.1);
osc.connect(envGain);
envGain.connect(this.filter);
osc.start();
this.activeNotes.set(frequency, { osc, envGain });
}
noteOff(frequency) {
const note = this.activeNotes.get(frequency);
if (!note) return;
const { osc, envGain } = note;
const now = this.audioCtx.currentTime;
// Release
envGain.gain.cancelScheduledValues(now);
envGain.gain.setValueAtTime(envGain.gain.value, now);
envGain.gain.exponentialRampToValueAtTime(0.001, now + 0.3);
osc.stop(now + 0.3);
this.activeNotes.delete(frequency);
}
}
// ์ฌ์ฉ ์์
const synth = new BrowserSynth();
document.addEventListener('click', () => {
synth.init();
});
// ํค๋ณด๋ ๋งคํ
const keyFreqMap = {
'a': 261.63, // C4
'w': 277.18, // C#4
's': 293.66, // D4
'e': 311.13, // D#4
'd': 329.63, // E4
'f': 349.23, // F4
't': 369.99, // F#4
'g': 392.00, // G4
'y': 415.30, // G#4
'h': 440.00, // A4
'u': 466.16, // A#4
'j': 493.88, // B4
'k': 523.25, // C5
};
document.addEventListener('keydown', (e) => {
if (e.repeat) return;
const freq = keyFreqMap[e.key];
if (freq) synth.noteOn(freq);
});
document.addEventListener('keyup', (e) => {
const freq = keyFreqMap[e.key];
if (freq) synth.noteOff(freq);
});
Web Audio API์๋ AudioWorklet์ด๋ผ๋ ๊ฐ๋ ฅํ ๊ธฐ๋ฅ์ด ์์ด.
๊ธฐ์กด์ ScriptProcessorNode(deprecated)๋ฅผ ๋์ฒดํ๋ ๊ฒ์ผ๋ก, ์ค๋์ค ์ฒ๋ฆฌ ์ค๋ ๋์์ ์ง์ JavaScript ์ฝ๋๋ฅผ ์คํํ ์ ์๊ฒ ํด์ค.
์ผ๋ฐ JavaScript๋ ๋ฉ์ธ ์ค๋ ๋์์ ์คํ๋๋๋ฐ, ์ค๋์ค ์ฒ๋ฆฌ๋ ๋ณ๋์ ์ค๋์ค ์ค๋ ๋์์ ์คํ๋ผ.
AudioWorklet์ ์ฌ์ฉํ๋ฉด ์ด ์ค๋์ค ์ค๋ ๋์์ ์ง์ ์ปค์คํ
DSP(Digital Signal Processing) ์ฝ๋๋ฅผ ์คํํ ์ ์์ด์,
๋ ์ดํด์ ์์ด ์์ ์ ์ธ ์ค์๊ฐ ์ค๋์ค ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํด.
AudioWorklet์ ๋ ํ์ผ๋ก ๊ตฌ์ฑ๋ผ:
// === my-processor.js (Worklet ํ๋ก์ธ์ ํ์ผ) ===
class MyProcessor extends AudioWorkletProcessor {
constructor(options) {
super();
this.gain = 1.0;
// ๋ฉ์ธ ์ค๋ ๋์์ ๋ฉ์์ง ์์
this.port.onmessage = (event) => {
if (event.data.type === 'setGain') {
this.gain = event.data.value;
}
};
}
// ํต์ฌ ๋ฉ์๋: 128 ์ํ์ฉ ์ฒ๋ฆฌ
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
// ๊ฐ ์ฑ๋ ์ฒ๋ฆฌ
for (let channel = 0; channel < output.length; channel++) {
const inputChannel = input[channel];
const outputChannel = output[channel];
// ์ํ๋ณ ์ฒ๋ฆฌ (์ฌ๊ธฐ์ DSP ์๊ณ ๋ฆฌ์ฆ ๊ตฌํ)
for (let i = 0; i < outputChannel.length; i++) {
// ์: ๊ฒ์ธ ์ ์ฉ
outputChannel[i] = inputChannel[i] * this.gain;
}
}
// true ๋ฐํ = ํ๋ก์ธ์ ๊ณ์ ์คํ
return true;
}
}
registerProcessor('my-processor', MyProcessor);
// === ๋ฉ์ธ ์คํฌ๋ฆฝํธ ===
const audioCtx = new AudioContext();
// Worklet ๋ชจ๋ ๋ก๋
await audioCtx.audioWorklet.addModule('my-processor.js');
// AudioWorkletNode ์์ฑ
const myNode = new AudioWorkletNode(audioCtx, 'my-processor');
// ๋ฉ์ธ ์ค๋ ๋์์ Worklet์ผ๋ก ๋ฉ์์ง ์ ์ก
myNode.port.postMessage({ type: 'setGain', value: 0.8 });
// ์ฐ๊ฒฐ
source.connect(myNode);
myNode.connect(audioCtx.destination);
- ์ค์๊ฐ ๋ ธ์ด์ฆ ์ ๊ฑฐ ์๊ณ ๋ฆฌ์ฆ
- ์ปค์คํ ๋ฆฌ๋ฒ๋ธ/๋๋ ์ด ์๊ณ ๋ฆฌ์ฆ
- ๋นํธ ํฌ๋ฌ์ , ๋ง ๋ชจ๋๋ ์ดํฐ ๋ฑ ํน์ ์ดํํธ
- ์ค์๊ฐ ์ค๋์ค ๋ถ์ ๋ฐ ํน์ง ์ถ์ถ
Web Audio API๋ ์ด๋ฏธ ๋ค์ํ ์ค์ ํ๋ก์ ํธ์์ ํ๋ฐํ๊ฒ ์ฌ์ฉ๋๊ณ ์์ด.
์ด๋ค ๊ฒ๋ค์ด ๊ฐ๋ฅํ์ง ์ดํด๋ณด์!
Three.js, Babylon.js ๊ฐ์ 3D ๊ฒ์ ์์ง๋ค์ Web Audio API๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๊ณต๊ฐ ์ํฅ์ ๊ตฌํํด.
PannerNode์ AudioListener๋ฅผ ์ฌ์ฉํ๋ฉด 3D ๊ณต๊ฐ์์ ์๋ฆฌ์ ์์น์ ๋ฐฉํฅ์ ์๋ฎฌ๋ ์ด์
ํ ์ ์์ด.
// 3D ๊ณต๊ฐ ์ํฅ ๊ตฌํ
const panner = audioCtx.createPanner();
// ํจ๋ ๋ชจ๋ธ ์ค์
panner.panningModel = 'HRTF'; // ๋จธ๋ฆฌ ์ ๋ฌ ํจ์ (๊ฐ์ฅ ํ์ค์ )
panner.distanceModel = 'inverse'; // ๊ฑฐ๋ฆฌ์ ๋ฐ๋ฅธ ๊ฐ์
// ์๋ฆฌ ์์น ์ค์ (x, y, z)
panner.positionX.value = 5; // ์ค๋ฅธ์ชฝ 5m
panner.positionY.value = 0; // ๊ฐ์ ๋์ด
panner.positionZ.value = -3; // ์์ชฝ 3m
// ๋ฆฌ์ค๋(์ฒญ์ทจ์) ์์น ์ค์
const listener = audioCtx.listener;
listener.positionX.value = 0;
listener.positionY.value = 0;
listener.positionZ.value = 0;
// ๋ฆฌ์ค๋ ๋ฐฉํฅ ์ค์
listener.forwardX.value = 0;
listener.forwardY.value = 0;
listener.forwardZ.value = -1;
source.connect(panner);
panner.connect(audioCtx.destination);
Soundtrap, BandLab ๊ฐ์ ๋ธ๋ผ์ฐ์ ๊ธฐ๋ฐ DAW๋ค์ด Web Audio API๋ฅผ ํต์ฌ์ผ๋ก ์ฌ์ฉํด.
๋ฉํฐํธ๋ ๋ ์ฝ๋ฉ, ๋ฏน์ฑ, ๋ง์คํฐ๋ง๊น์ง ๋ธ๋ผ์ฐ์ ์์ ๊ฐ๋ฅํด.
์์ ํ๋ จ, ๋ฆฌ๋ฌ ํ๋ จ, ์
๊ธฐ ์๋ฎฌ๋ ์ดํฐ ๋ฑ ์์
๊ต์ก ์ฑ์์๋ ๋ง์ด ์ฌ์ฉ๋ผ.
์ฌ๋ฅ๋ท ๊ฐ์ ์ฌ๋ฅ ๊ณต์ ํ๋ซํผ์์๋ ์์
๋ ์จ ๊ด๋ จ ์ธํฐ๋ํฐ๋ธ ์ฝํ
์ธ ๋ฅผ ๋ง๋ค ๋ ํ์ฉํ ์ ์์ด!
์คํํธ๋ผ ๋ถ์๊ธฐ, ํ๋, ๋ฉํธ๋ก๋, ์์ฑ ๋ถ์ ๋๊ตฌ ๋ฑ์ ์์ ๋ธ๋ผ์ฐ์ ์์ ๊ตฌํํ ์ ์์ด.
// ์ ํํ ๋ฉํธ๋ก๋ ๊ตฌํ (Web Audio API์ ํ์ด๋ฐ ์ ํ์ฑ ํ์ฉ)
class Metronome {
constructor(audioCtx) {
this.audioCtx = audioCtx;
this.bpm = 120;
this.isPlaying = false;
this.nextBeatTime = 0;
this.lookahead = 25; // ms (์ค์ผ์ค๋ง ๊ฐ๊ฒฉ)
this.scheduleAheadTime = 0.1; // ์ด (๋ฏธ๋ฆฌ ์ค์ผ์ค๋งํ ์๊ฐ)
}
scheduleBeat(time) {
// ํด๋ฆญ ์๋ฆฌ ์์ฑ
const osc = this.audioCtx.createOscillator();
const gain = this.audioCtx.createGain();
osc.frequency.value = 880;
gain.gain.setValueAtTime(1, time);
gain.gain.exponentialRampToValueAtTime(0.001, time + 0.05);
osc.connect(gain);
gain.connect(this.audioCtx.destination);
osc.start(time);
osc.stop(time + 0.05);
}
scheduler() {
while (this.nextBeatTime < this.audioCtx.currentTime + this.scheduleAheadTime) {
this.scheduleBeat(this.nextBeatTime);
this.nextBeatTime += 60.0 / this.bpm;
}
if (this.isPlaying) {
setTimeout(() => this.scheduler(), this.lookahead);
}
}
start() {
this.isPlaying = true;
this.nextBeatTime = this.audioCtx.currentTime;
this.scheduler();
}
stop() {
this.isPlaying = false;
}
}
Web Audio API๋ฅผ ์ฌ์ฉํ ๋ ๋ฉ๋ชจ๋ฆฌ ๊ด๋ฆฌ๋ ๋งค์ฐ ์ค์ํด.
๋
ธ๋๋ฅผ ์์ฑํ๊ณ ์ฐ๊ฒฐ์ ๋์ด๋ ์๋์ผ๋ก ๊ฐ๋น์ง ์ปฌ๋ ์
์ด ๋์ง ์๋ ๊ฒฝ์ฐ๊ฐ ์์ด.
// ๋
ธ๋ ์ ๋ฆฌ ๋ฐฉ๋ฒ
function cleanupNode(node) {
// ๋ชจ๋ ์ฐ๊ฒฐ ํด์
node.disconnect();
// ์์ค ๋
ธ๋๋ผ๋ฉด ์ ์ง
if (node.stop) {
try { node.stop(); } catch(e) {}
}
}
// AudioContext ๋ซ๊ธฐ (๋ ์ด์ ์ฌ์ฉํ์ง ์์ ๋)
audioCtx.close().then(() => {
console.log('AudioContext ๋ซํ');
});
// ํฐ AudioBuffer๋ ์ฌ์ฉ ํ null๋ก ์ค์
source.buffer = null;
- ๋ชจ๋ฐ์ผ์์๋ sampleRate๊ฐ ๋ค๋ฅผ ์ ์์ (๋ณดํต 44100Hz)
- ๋ฐฐํฐ๋ฆฌ ์๋ชจ๋ฅผ ๊ณ ๋ คํด ๋ถํ์ํ ๋ ธ๋๋ ์ฆ์ disconnect
- ๋ฐฑ๊ทธ๋ผ์ด๋ ํญ์์๋ AudioContext๊ฐ ์๋์ผ๋ก suspend๋ ์ ์์
- visibilitychange ์ด๋ฒคํธ๋ก ํญ ์ ํ ๊ฐ์ง ํ ์ ์ ํ ์ฒ๋ฆฌ
// ํญ ์ ํ ์ฒ๋ฆฌ
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// ํญ์ด ์จ๊ฒจ์ง๋ฉด AudioContext ์ผ์์ ์ง
audioCtx.suspend();
} else {
// ํญ์ด ๋ค์ ๋ณด์ด๋ฉด ์ฌ๊ฐ
audioCtx.resume();
}
});
// iOS Safari ์๋์ฌ์ ์ ์ฑ
๋์
document.addEventListener('touchstart', function initAudio() {
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
document.removeEventListener('touchstart', initAudio);
}, { once: true });
| ๋ธ๋ผ์ฐ์ | ์ง์ ๋ฒ์ | ํน์ด์ฌํญ |
|---|---|---|
| Chrome | 35+ | ๊ฐ์ฅ ์์ ํ ์ง์ |
| Firefox | 25+ | AudioWorklet ์ง์ 76+ |
| Safari | 14.1+ | webkitAudioContext ํ์ (๊ตฌ๋ฒ์ ) |
| Edge | 79+ | Chromium ๊ธฐ๋ฐ, ์์ ์ง์ |
| iOS Safari | 14.5+ | ์๋์ฌ์ ์ ์ฑ ์๊ฒฉ |
| Android Chrome | 35+ | ๋๋ถ๋ถ ์ง์ |
Web Audio API๋ฅผ ๋ ์ฝ๊ฒ ์ฌ์ฉํ ์ ์๊ฒ ๋์์ฃผ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค๋ ์์ด.
์ฒ์๋ถํฐ ๋ชจ๋ ๊ฑธ ์ง์ ๊ตฌํํ๊ธฐ ์ด๋ ต๋ค๋ฉด ์ด๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค์ ํ์ฉํด๋ด!
Web Audio API ์์ ๊ตฌ์ถ๋ ๊ฐ์ฅ ์ธ๊ธฐ ์๋ ์์
์ ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ.
์ ๋์ฌ์ด์ , ์ดํํธ, ์ํ์, ํธ๋์คํฌํธ ๋ฑ ์์
์ ์์ ํ์ํ ๋ชจ๋ ๊ฒ์ ์ ๊ณตํด.
// Tone.js ์์
import * as Tone from 'tone';
// ์ ๋์ฌ์ด์ ์์ฑ
const synth = new Tone.Synth({
oscillator: { type: 'sawtooth' },
envelope: {
attack: 0.01,
decay: 0.1,
sustain: 0.5,
release: 0.3
}
}).toDestination();
// ์ดํํธ ์ฒด์ธ
const reverb = new Tone.Reverb(2).toDestination();
const delay = new Tone.FeedbackDelay('8n', 0.5).connect(reverb);
synth.connect(delay);
// ์ํ์
const seq = new Tone.Sequence((time, note) => {
synth.triggerAttackRelease(note, '8n', time);
}, ['C4', 'E4', 'G4', 'B4'], '4n');
// ์ฌ์
await Tone.start();
seq.start();
Tone.Transport.start();
์ค์๊ฐ ์ค๋์ค ํน์ง ์ถ์ถ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ.
RMS, ์คํํธ๋ผ ์ผํธ๋ก์ด๋, MFCC, ํฌ๋ก๋ง ๋ฑ ๋ค์ํ ์ค๋์ค ํน์ง์ ์ถ์ถํ ์ ์์ด.
๋จธ์ ๋ฌ๋๊ณผ ๊ฒฐํฉํ๋ฉด ์์
์ฅ๋ฅด ๋ถ๋ฅ, ๊ฐ์ ๋ถ์ ๋ฑ๋ ๊ฐ๋ฅํด.
// Meyda.js ์์
const analyzer = Meyda.createMeydaAnalyzer({
audioContext: audioCtx,
source: source,
bufferSize: 512,
featureExtractors: ['rms', 'spectralCentroid', 'chroma'],
callback: (features) => {
console.log('RMS:', features.rms);
console.log('์คํํธ๋ผ ์ผํธ๋ก์ด๋:', features.spectralCentroid);
console.log('ํฌ๋ก๋ง:', features.chroma);
}
});
analyzer.start();
๊ฒ์์ด๋ ์ฑ์์ ์ค๋์ค๋ฅผ ์ฝ๊ฒ ๊ด๋ฆฌํ๊ธฐ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ผ.
Web Audio API์ HTML5 Audio๋ฅผ ์๋์ผ๋ก ์ ํํด์ ์ต์ ์ ๋ฐฉ๋ฒ์ผ๋ก ์ค๋์ค๋ฅผ ์ฌ์ํด์ค.
- ์ค๋์ค ๋ถ์/๋จธ์ ๋ฌ๋ โ Meyda.js
- ๊ฒ์/์ฑ ์ค๋์ค ๊ด๋ฆฌ โ Howler.js
- ์ ์์ค ์ปค์คํ ์ฒ๋ฆฌ โ Web Audio API ์ง์ ์ฌ์ฉ
Chrome ๋ธ๋ผ์ฐ์ ์ DevTools์๋ Web Audio ์ ์ฉ ํจ๋์ด ์์ด!
chrome://inspect/#webaudio์ ์ ์ํ๊ฑฐ๋, DevTools โ More tools โ Web Audio๋ฅผ ์ ํํ๋ฉด ๋ผ.
ํ์ฌ AudioContext์ ๋
ธ๋ ๊ทธ๋ํ๋ฅผ ์๊ฐ์ ์ผ๋ก ํ์ธํ ์ ์์ด์ ๋๋ฒ๊น
์ด ํจ์ฌ ์ฌ์์ ธ.
์ฑ๊ธํค ํจํด์ผ๋ก ๊ด๋ฆฌํ๋ ๊ฒ ์ข์.
0 ๋์ 0.001 ๊ฐ์ ๋งค์ฐ ์์ ๊ฐ์ ์ฌ์ฉํด์ผ ํด.
๊ทธ๋ ์ง ์์ผ๋ฉด ๋ฉ๋ชจ๋ฆฌ ๋์๊ฐ ๋ฐ์ํ ์ ์์ด.
๋ง์คํฐ ๊ฒ์ธ์ ๋ฎ์ถ๊ฑฐ๋ DynamicsCompressorNode๋ฅผ ์ฌ์ฉํด์ ํด๋ฆฌํ์ ๋ฐฉ์งํด์ผ ํด.
// 1. OfflineAudioContext๋ก ์คํ๋ผ์ธ ๋ ๋๋ง (์ค์๊ฐ ์ฌ์ ์์ด ์ฒ๋ฆฌ)
const offlineCtx = new OfflineAudioContext({
numberOfChannels: 2,
length: 44100 * 10, // 10์ด
sampleRate: 44100
});
// ๋
ธ๋ ๊ตฌ์ฑ...
const renderedBuffer = await offlineCtx.startRendering();
// renderedBuffer์ ์ฒ๋ฆฌ๋ ์ค๋์ค ๋ฐ์ดํฐ๊ฐ ๋ด๊น
// 2. ์์ฃผ ์ฌ์ฉํ๋ AudioBuffer๋ ์บ์ฑ
const audioBufferCache = new Map();
async function getCachedBuffer(url) {
if (audioBufferCache.has(url)) {
return audioBufferCache.get(url);
}
const buffer = await loadAudioBuffer(url);
audioBufferCache.set(url, buffer);
return buffer;
}
// 3. ๋ถํ์ํ AnalyserNode ๋นํ์ฑํ
// ๋น์ฃผ์ผ๋ผ์ด์ ๊ฐ ๋ณด์ด์ง ์์ ๋๋ requestAnimationFrame ์ทจ์
let animFrameId;
function startVisualization() {
function draw() {
animFrameId = requestAnimationFrame(draw);
// ๊ทธ๋ฆฌ๊ธฐ ์ฝ๋...
}
draw();
}
function stopVisualization() {
cancelAnimationFrame(animFrameId);
}
Web Audio API๋ ๊ณ์ ๋ฐ์ ํ๊ณ ์์ด. ํ์ฌ W3C์์ ๋ ผ์ ์ค์ธ ๊ธฐ๋ฅ๋ค์ ์ดํด๋ณด์.
Web MIDI API๋ฅผ ์ฌ์ฉํ๋ฉด ์ค์ MIDI ํค๋ณด๋๋ ์ปจํธ๋กค๋ฌ๋ฅผ ๋ธ๋ผ์ฐ์ ์ ์ฐ๊ฒฐํด์ Web Audio API์ ํจ๊ป ์ฌ์ฉํ ์ ์์ด.
์ด๋ฏธ Chrome์์๋ ์ง์ํ๊ณ ์์ด!
// Web MIDI API ์ฐ๋
async function setupMIDI() {
const midiAccess = await navigator.requestMIDIAccess();
midiAccess.inputs.forEach(input => {
input.onmidimessage = (event) => {
const [status, note, velocity] = event.data;
// Note On (144 = 0x90)
if (status === 144 && velocity > 0) {
const frequency = 440 * Math.pow(2, (note - 69) / 12);
synth.noteOn(frequency);
}
// Note Off (128 = 0x80 ๋๋ velocity 0)
if (status === 128 || (status === 144 && velocity === 0)) {
const frequency = 440 * Math.pow(2, (note - 69) / 12);
synth.noteOff(frequency);
}
};
});
}
TensorFlow.js์ Web Audio API๋ฅผ ๊ฒฐํฉํ๋ฉด ๋ธ๋ผ์ฐ์ ์์ ์ค์๊ฐ ์์
๋ถ๋ฅ, ์
๊ธฐ ์ธ์, ๊ฐ์ ๋ถ์ ๋ฑ์ ๊ตฌํํ ์ ์์ด.
Google์ Magenta.js ํ๋ก์ ํธ๊ฐ ๋ํ์ ์ธ ์์ผ โ ๋ธ๋ผ์ฐ์ ์์ AI๊ฐ ์์
์ ์์ฑํ๊ณ ์ฐ์ฃผํ๋ ๊ฑฐ์ผ!
์ฌ๋ฅ๋ท์์ ์์
๊ด๋ จ ์ฌ๋ฅ์ ๊ฐ๋ฅด์น๋ ๋ถ๋ค์ด๋ผ๋ฉด, ์ด๋ฐ ๊ธฐ์ ์ ํ์ฉํด์ ์ธํฐ๋ํฐ๋ธ ๋ ์จ ๋๊ตฌ๋ฅผ ๋ง๋ค์ด๋ณผ ์๋ ์๊ฒ ์ง? ๐
- ์์ฑ ๊ฐ์ ๋ถ์
- ์๋ ์์ ์์ฑ (AI ์๊ณก)
- ์์ ์ฅ๋ฅด ๋ถ๋ฅ
- ๋ฐ์/๋ฆฌ๋ฌ ์๋ ๊ฐ์ง
์, ์ฌ๊ธฐ๊น์ง Web Audio API์ ํต์ฌ ๊ฐ๋
๋ถํฐ ์ค์ ํ์ฉ๊น์ง ์ญ ์ดํด๋ดค์ด!
์ ๋ง ๊ธด ์ฌ์ ์ด์๋๋ฐ ์ ๋ฐ๋ผ์์คฌ์ด? ๐
Web Audio API๋ ์ฒ์์ ๋ณต์กํด ๋ณด์ด์ง๋ง, ํต์ฌ ๊ฐ๋
์ธ ๋
ธ๋ ๊ทธ๋ํ๋ฅผ ์ดํดํ๋ฉด ๋๋จธ์ง๋ ๋ ๊ณ ๋ธ๋ก ์กฐ๋ฆฝํ๋ฏ ์์ฐ์ค๋ฝ๊ฒ ์ด์ด์ ธ.
ํต์ฌ์ ๋ค์ ํ๋ฒ ์ ๋ฆฌํด๋ณผ๊ฒ:
๋ธ๋ผ์ฐ์ ํ๋๋ก ์ด ๋ชจ๋ ๊ฒ ๊ฐ๋ฅํด. ์ ๋ง ๋๋จํ์ง ์์? ๐
JavaScript์ Web Audio API์ ์กฐํฉ์ ์์
๊ณผ ๊ธฐ์ ์ ๊ฒฝ๊ณ๋ฅผ ํ๋ฌผ๊ณ ์์ด.
์ด ๊ธ์ ์ฝ๊ณ ๋์ ๋ญ๊ฐ ๋ง๋ค์ด๋ณด๊ณ ์ถ์ ์์ด๋์ด๊ฐ ์๊ฒผ๋ค๋ฉด, ๊ทธ๊ฒ ๋ฐ๋ก ์์์ด์ผ!
์์ ์ค์ค๋ ์ดํฐ ํ๋๋ถํฐ ์์ํด์, ์ ์ ๋ณต์กํ ์ ์ค๋ฅผ ๋ง๋ค์ด๊ฐ๋ค ๋ณด๋ฉด ์ด๋์ ๋ธ๋ผ์ฐ์ DAW๋ฅผ ๋ง๋ค๊ณ ์๋ ์์ ์ ๋ฐ๊ฒฌํ๊ฒ ๋ ๊ฑฐ์ผ ๐
์ฝ๋ฉ ์ฆ๊ฒ๊ฒ ํ๊ณ , ์ข์ ์์
๋ง์ด ๋ง๋ค์ด๋ด! ๐ถ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.