์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐ŸŽต Web Audio API๋ฅผ ํ™œ์šฉํ•œ ๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ฐ˜ ์Œํ–ฅ ์ฒ˜๋ฆฌ ์™„์ „ ์ •๋ณต
JavaScript / ํ”„๋กœ๊ทธ๋žจ๊ฐœ๋ฐœ

๐ŸŽต Web Audio API๋ฅผ ํ™œ์šฉํ•œ ๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ฐ˜ ์Œํ–ฅ ์ฒ˜๋ฆฌ ์™„์ „ ์ •๋ณต

ํ”Œ๋Ÿฌ๊ทธ์ธ ์—†์ด, ์„ค์น˜ ์—†์ด, ์˜ค์ง JavaScript๋งŒ์œผ๋กœ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์Œ์•…์„ ๋งŒ๋“ค๊ณ  ๋ถ„์„ํ•˜๊ณ  ์กฐ์ž‘ํ•˜๋Š” ๋งˆ๋ฒ• ๊ฐ™์€ ์„ธ๊ณ„๋กœ ๋– ๋‚˜๋ณด์ž! ๐Ÿš€

๐ŸŽง ๋“ค์–ด๊ฐ€๋ฉฐ โ€” ๋ธŒ๋ผ์šฐ์ €๊ฐ€ DAW๊ฐ€ ๋˜๋Š” ์„ธ์ƒ

์˜ˆ์ „์—๋Š” ์Œ์•… ์ž‘์—…์„ ํ•˜๋ ค๋ฉด Pro Tools, Ableton Live, Logic Pro ๊ฐ™์€ ์ „๋ฌธ DAW(Digital Audio Workstation)๋ฅผ ์„ค์น˜ํ•ด์•ผ ํ–ˆ์–ด.
๊ทผ๋ฐ ์ง€๊ธˆ์€? ํฌ๋กฌ ๋ธŒ๋ผ์šฐ์ € ํ•˜๋‚˜๋งŒ ์žˆ์œผ๋ฉด ๋ผ. ์ง„์งœ์•ผ. ๐Ÿ˜ฒ

Web Audio API๋Š” W3C์—์„œ ํ‘œ์ค€ํ™”ํ•œ ๊ณ ์ˆ˜์ค€ JavaScript API๋กœ, ๋ธŒ๋ผ์šฐ์ € ์•ˆ์—์„œ ์˜ค๋””์˜ค๋ฅผ ์ƒ์„ฑยท์ฒ˜๋ฆฌยท๋ถ„์„ยท์ถœ๋ ฅํ•˜๋Š” ๋ชจ๋“  ์ž‘์—…์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ด์ค˜.
๋‹จ์ˆœํžˆ mp3 ํŒŒ์ผ ์žฌ์ƒํ•˜๋Š” ์ˆ˜์ค€์ด ์•„๋‹ˆ๋ผ, ์ดํ€„๋ผ์ด์ €, ๋ฆฌ๋ฒ„๋ธŒ, ๋”œ๋ ˆ์ด, ์ปดํ”„๋ ˆ์„œ, ์‹ ๋””์‚ฌ์ด์ €, ์ŠคํŽ™ํŠธ๋Ÿผ ๋ถ„์„๊ธฐ๊นŒ์ง€ ์ „๋ถ€ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด.

์ด ๊ธ€์—์„œ๋Š” Web Audio API์˜ ํ•ต์‹ฌ ๊ฐœ๋…๋ถ€ํ„ฐ ์‹ค์ „ ์ฝ”๋“œ๊นŒ์ง€ ์นœ๊ตฌํ•œํ…Œ ์„ค๋ช…ํ•˜๋“ฏ ์ญ‰ ํ’€์–ด๋ณผ๊ฒŒ. ์ปคํ”ผ ํ•œ ์ž” ๋“ค๊ณ  ํŽธํ•˜๊ฒŒ ์ฝ์–ด๋ด โ˜•

Web Audio API ์‹ ํ˜ธ ํ๋ฆ„๋„ ์†Œ์Šค ๋…ธ๋“œ AudioBufferSource ์ดํŽ™ํŠธ ๋…ธ๋“œ BiquadFilter ๋ถ„์„ ๋…ธ๋“œ AnalyserNode ๊ฒŒ์ธ ๋…ธ๋“œ GainNode ์ถœ๋ ฅ ๋…ธ๋“œ destination AudioContext โ€” ๋ชจ๋“  ๋…ธ๋“œ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์ค‘์•™ ์ปจํŠธ๋กค๋Ÿฌ ์†Œ์Šค โ†’ ์ดํŽ™ํŠธ โ†’ ๋ถ„์„ โ†’ ๊ฒŒ์ธ โ†’ ์Šคํ”ผ์ปค(destination) ๊ฐ ๋…ธ๋“œ๋Š” connect() ๋ฉ”์„œ๋“œ๋กœ ์—ฐ๊ฒฐ๋˜๋ฉฐ, ๊ทธ๋ž˜ํ”„ ํ˜•ํƒœ๋กœ ์‹ ํ˜ธ๊ฐ€ ํ๋ฆ„ ๋…ธ๋“œ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ์ถ”๊ฐ€ยท์ œ๊ฑฐยท์—ฐ๊ฒฐํ•˜์—ฌ ์‹ค์‹œ๊ฐ„ ์Œํ–ฅ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ

๐Ÿ—๏ธ Web Audio API์˜ ํ•ต์‹ฌ ๊ตฌ์กฐ โ€” AudioContext

Web Audio API์˜ ๋ชจ๋“  ๊ฒƒ์€ AudioContext์—์„œ ์‹œ์ž‘ํ•ด.
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 });
๐Ÿ’ก ์ค‘์š”ํ•œ ํฌ์ธํŠธ! 2018๋…„ ์ดํ›„ ๋Œ€๋ถ€๋ถ„์˜ ๋ธŒ๋ผ์šฐ์ €๋Š” ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜(ํด๋ฆญ, ํƒญ ๋“ฑ) ์—†์ด๋Š” AudioContext๋ฅผ ์ž๋™ ์‹œ์ž‘ํ•˜์ง€ ์•Š์•„.
์ด๊ฑด ์ž๋™ ์žฌ์ƒ ์ •์ฑ…(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();

}

});
AudioContext์˜ ์ฃผ์š” ์†์„ฑ
์†์„ฑ ์„ค๋ช… ์˜ˆ์‹œ ๊ฐ’
sampleRate ์ดˆ๋‹น ์ƒ˜ํ”Œ ์ˆ˜ (Hz) 44100, 48000
currentTime ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ ํ›„ ๊ฒฝ๊ณผ ์‹œ๊ฐ„ (์ดˆ) 3.142...
state ํ˜„์žฌ ์ƒํƒœ suspended / running / closed
destination ์ตœ์ข… ์ถœ๋ ฅ ๋…ธ๋“œ (์Šคํ”ผ์ปค) AudioDestinationNode
listener 3D ๊ณต๊ฐ„์Œํ–ฅ ๋ฆฌ์Šค๋„ˆ AudioListener

๐Ÿ”Š ์˜ค๋””์˜ค ๋…ธ๋“œ โ€” ๋ ˆ๊ณ  ๋ธ”๋ก์ฒ˜๋Ÿผ ์กฐ๋ฆฝํ•˜๋Š” ์Œํ–ฅ ์ฒ˜๋ฆฌ

Web Audio API์˜ ํ•ต์‹ฌ ์ฒ ํ•™์€ "๋…ธ๋“œ ๊ทธ๋ž˜ํ”„(Node Graph)"์•ผ.
๊ฐ๊ฐ์˜ ์˜ค๋””์˜ค ์ฒ˜๋ฆฌ ๋‹จ๊ณ„๋ฅผ "๋…ธ๋“œ"๋ผ๋Š” ๋ธ”๋ก์œผ๋กœ ๋งŒ๋“ค๊ณ , ์ด ๋ธ”๋ก๋“ค์„ ๋ ˆ๊ณ ์ฒ˜๋Ÿผ ์—ฐ๊ฒฐํ•ด์„œ ์›ํ•˜๋Š” ์Œํ–ฅ ์ฒ˜๋ฆฌ ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์„ฑํ•˜๋Š” ๊ฑฐ์•ผ.

๋…ธ๋“œ์˜ ์ข…๋ฅ˜๋Š” ํฌ๊ฒŒ ์„ธ ๊ฐ€์ง€๋กœ ๋‚˜๋‰˜์–ด:

1
์†Œ์Šค ๋…ธ๋“œ (Source Nodes) โ€” ์†Œ๋ฆฌ๋ฅผ ๋งŒ๋“ค์–ด๋‚ด๋Š” ๋…ธ๋“œ. ์˜ค์‹ค๋ ˆ์ดํ„ฐ, ์˜ค๋””์˜ค ๋ฒ„ํผ, ๋ฏธ๋””์–ด ์ŠคํŠธ๋ฆผ ๋“ฑ
2
์ฒ˜๋ฆฌ ๋…ธ๋“œ (Processing Nodes) โ€” ์†Œ๋ฆฌ๋ฅผ ๋ณ€ํ˜•ํ•˜๋Š” ๋…ธ๋“œ. ํ•„ํ„ฐ, ๊ฒŒ์ธ, ๋”œ๋ ˆ์ด, ์ปดํ”„๋ ˆ์„œ, ๋ฆฌ๋ฒ„๋ธŒ ๋“ฑ
3
๋ชฉ์ ์ง€ ๋…ธ๋“œ (Destination Node) โ€” ์ตœ์ข… ์ถœ๋ ฅ. ์Šคํ”ผ์ปค(audioCtx.destination) ๋˜๋Š” MediaStreamDestination
๐ŸŽน OscillatorNode โ€” ์†Œ๋ฆฌ์˜ ๊ธฐ๋ณธ ํŒŒํ˜• ์ƒ์„ฑ

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);
๐ŸŽต ์Œ๊ณ„์™€ ์ฃผํŒŒ์ˆ˜ ๊ด€๊ณ„ A4 = 440Hz, B4 = 493.88Hz, C5 = 523.25Hz
๋ฐ˜์Œ ์˜ฌ๋ผ๊ฐˆ ๋•Œ๋งˆ๋‹ค ์ฃผํŒŒ์ˆ˜์— 2^(1/12) โ‰ˆ 1.0595๋ฅผ ๊ณฑํ•˜๋ฉด ๋ผ.
์˜ˆ: A4(440) ร— 1.0595 = A#4(466.16Hz)
๐Ÿ“ AudioBufferSourceNode โ€” ์˜ค๋””์˜ค ํŒŒ์ผ ์žฌ์ƒ

์‹ค์ œ ์˜ค๋””์˜ค ํŒŒ์ผ(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๋Š” ํ•œ ๋ฒˆ๋งŒ ์žฌ์ƒํ•  ์ˆ˜ ์žˆ์–ด.
์žฌ์ƒ์ด ๋๋‚˜๋ฉด ๋‹ค์‹œ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๊ณ , ์ƒˆ๋กœ์šด AudioBufferSourceNode๋ฅผ ๋งŒ๋“ค์–ด์•ผ ํ•ด.
์ด๊ฑด ์„ค๊ณ„ ์˜๋„์•ผ โ€” ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ๊ณผ ํƒ€์ด๋ฐ ์ •ํ™•์„ฑ์„ ์œ„ํ•œ ์„ ํƒ์ด๊ฑฐ๋“ .

๐ŸŽ›๏ธ ์ฒ˜๋ฆฌ ๋…ธ๋“œ๋“ค โ€” ์†Œ๋ฆฌ๋ฅผ ์š”๋ฆฌํ•˜๋Š” ๋„๊ตฌ๋“ค
๐Ÿ”‰ GainNode โ€” ๋ณผ๋ฅจ ์กฐ์ ˆ์˜ ๊ธฐ๋ณธ

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 โ€” ์ดํ€„๋ผ์ด์ €์˜ ํ•ต์‹ฌ

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 โ€” ๋”œ๋ ˆ์ด ์ดํŽ™ํŠธ

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 โ€” ๋ฆฌ๋ฒ„๋ธŒ ํšจ๊ณผ

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);
๐Ÿ”ง DynamicsCompressorNode โ€” ์ปดํ”„๋ ˆ์„œ

์ปดํ”„๋ ˆ์„œ๋Š” ์˜ค๋””์˜ค ์‹ ํ˜ธ์˜ ๋‹ค์ด๋‚˜๋ฏน ๋ ˆ์ธ์ง€๋ฅผ ์ค„์—ฌ์ฃผ๋Š” ์ดํŽ™ํŠธ์•ผ.
๋„ˆ๋ฌด ํฐ ์†Œ๋ฆฌ๋Š” ์ค„์ด๊ณ , ์ „์ฒด์ ์ธ ๋ณผ๋ฅจ์„ ๊ท ์ผํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์ค˜.
๋งˆ์Šคํ„ฐ๋ง ์ฒด์ธ์—์„œ ํ•„์ˆ˜์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ๋„๊ตฌ์•ผ.


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 โ€” ์†Œ๋ฆฌ๋ฅผ ๋ˆˆ์œผ๋กœ ๋ณด๋‹ค

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;

}
๐ŸŽจ Canvas๋ฅผ ์ด์šฉํ•œ ์˜ค๋””์˜ค ๋น„์ฃผ์–ผ๋ผ์ด์ € ๊ตฌํ˜„

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();
์˜ค๋””์˜ค ๋น„์ฃผ์–ผ๋ผ์ด์ € โ€” ์ฃผํŒŒ์ˆ˜ ์ŠคํŽ™ํŠธ๋Ÿผ ๋ถ„์„ 20Hz 200Hz 1kHz 5kHz 20kHz AnalyserNode.getByteFrequencyData() ๋กœ ์‹ค์‹œ๊ฐ„ ์ฃผํŒŒ์ˆ˜ ๋ฐ์ดํ„ฐ ํš๋“ FFT(๊ณ ์† ํ‘ธ๋ฆฌ์— ๋ณ€ํ™˜)๋ฅผ ํ†ตํ•ด ์‹œ๊ฐ„ ๋„๋ฉ”์ธ ์‹ ํ˜ธ๋ฅผ ์ฃผํŒŒ์ˆ˜ ๋„๋ฉ”์ธ์œผ๋กœ ๋ณ€ํ™˜ ์ €์Œ(์™ผ์ชฝ) โ†’ ์ค‘์Œ(๊ฐ€์šด๋ฐ) โ†’ ๊ณ ์Œ(์˜ค๋ฅธ์ชฝ) ์ˆœ์œผ๋กœ ์ฃผํŒŒ์ˆ˜ ๋ฐฐ์—ด

๐ŸŽค MediaStream API์™€ ์—ฐ๋™ โ€” ๋งˆ์ดํฌ ์ž…๋ ฅ ์ฒ˜๋ฆฌ

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)
๐ŸŽต ์Œ์ • ๊ฐ์ง€ (Pitch 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;

}

๐ŸŽผ AudioParam โ€” ์ •๋ฐ€ํ•œ ์ž๋™ํ™” ์ œ์–ด

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 ์—”๋ฒจ๋กœํ”„ ๊ตฌํ˜„

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);

});

๐Ÿ”ฌ AudioWorklet โ€” ์ปค์Šคํ…€ ์˜ค๋””์˜ค ์ฒ˜๋ฆฌ์˜ ๋ํŒ์™•

Web Audio API์—๋Š” AudioWorklet์ด๋ผ๋Š” ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์ด ์žˆ์–ด.
๊ธฐ์กด์˜ ScriptProcessorNode(deprecated)๋ฅผ ๋Œ€์ฒดํ•˜๋Š” ๊ฒƒ์œผ๋กœ, ์˜ค๋””์˜ค ์ฒ˜๋ฆฌ ์Šค๋ ˆ๋“œ์—์„œ ์ง์ ‘ JavaScript ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜.

์ผ๋ฐ˜ JavaScript๋Š” ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰๋˜๋Š”๋ฐ, ์˜ค๋””์˜ค ์ฒ˜๋ฆฌ๋Š” ๋ณ„๋„์˜ ์˜ค๋””์˜ค ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰๋ผ.
AudioWorklet์„ ์‚ฌ์šฉํ•˜๋ฉด ์ด ์˜ค๋””์˜ค ์Šค๋ ˆ๋“œ์—์„œ ์ง์ ‘ ์ปค์Šคํ…€ DSP(Digital Signal Processing) ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์–ด์„œ,
๋ ˆ์ดํ„ด์‹œ ์—†์ด ์•ˆ์ •์ ์ธ ์‹ค์‹œ๊ฐ„ ์˜ค๋””์˜ค ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•ด.

AudioWorklet ๊ตฌํ˜„ ๋ฐฉ๋ฒ•

AudioWorklet์€ ๋‘ ํŒŒ์ผ๋กœ ๊ตฌ์„ฑ๋ผ:

1
Worklet ํ”„๋กœ์„ธ์„œ ํŒŒ์ผ (๋ณ„๋„ JS ํŒŒ์ผ) โ€” ์˜ค๋””์˜ค ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰๋˜๋Š” ์ฒ˜๋ฆฌ ์ฝ”๋“œ
2
๋ฉ”์ธ ์Šคํฌ๋ฆฝํŠธ โ€” Worklet์„ ๋กœ๋“œํ•˜๊ณ  AudioWorkletNode๋ฅผ ์ƒ์„ฑํ•˜๋Š” ์ฝ”๋“œ

// === 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);
๐Ÿš€ AudioWorklet์œผ๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ๋“ค - ์ปค์Šคํ…€ ์‹ ๋””์‚ฌ์ด์ € ์•Œ๊ณ ๋ฆฌ์ฆ˜ (FM ํ•ฉ์„ฑ, ์›จ์ด๋ธŒํ…Œ์ด๋ธ” ํ•ฉ์„ฑ ๋“ฑ)
- ์‹ค์‹œ๊ฐ„ ๋…ธ์ด์ฆˆ ์ œ๊ฑฐ ์•Œ๊ณ ๋ฆฌ์ฆ˜
- ์ปค์Šคํ…€ ๋ฆฌ๋ฒ„๋ธŒ/๋”œ๋ ˆ์ด ์•Œ๊ณ ๋ฆฌ์ฆ˜
- ๋น„ํŠธ ํฌ๋Ÿฌ์…”, ๋ง ๋ชจ๋“ˆ๋ ˆ์ดํ„ฐ ๋“ฑ ํŠน์ˆ˜ ์ดํŽ™ํŠธ
- ์‹ค์‹œ๊ฐ„ ์˜ค๋””์˜ค ๋ถ„์„ ๋ฐ ํŠน์ง• ์ถ”์ถœ

๐ŸŒ Web Audio API ํ™œ์šฉ ์‚ฌ๋ก€์™€ ์‹ค์ œ ํ”„๋กœ์ ํŠธ๋“ค

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);
๐ŸŽต DAW(Digital Audio Workstation) ๊ตฌํ˜„

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;
๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ ์ตœ์ ํ™”
๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ์—์„œ์˜ Web Audio API ์ฃผ์˜์‚ฌํ•ญ - iOS Safari๋Š” ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜ ์—†์ด AudioContext ์‹œ์ž‘ ๋ถˆ๊ฐ€
- ๋ชจ๋ฐ”์ผ์—์„œ๋Š” 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๋ฅผ ๋” ์‰ฝ๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋„์™€์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค๋„ ์žˆ์–ด.
์ฒ˜์Œ๋ถ€ํ„ฐ ๋ชจ๋“  ๊ฑธ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๊ธฐ ์–ด๋ ต๋‹ค๋ฉด ์ด๋Ÿฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์„ ํ™œ์šฉํ•ด๋ด!

๐ŸŽต Tone.js

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();
๐Ÿ“Š Meyda.js

์‹ค์‹œ๊ฐ„ ์˜ค๋””์˜ค ํŠน์ง• ์ถ”์ถœ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ.
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();
๐ŸŽธ Howler.js

๊ฒŒ์ž„์ด๋‚˜ ์•ฑ์—์„œ ์˜ค๋””์˜ค๋ฅผ ์‰ฝ๊ฒŒ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ.
Web Audio API์™€ HTML5 Audio๋ฅผ ์ž๋™์œผ๋กœ ์„ ํƒํ•ด์„œ ์ตœ์ ์˜ ๋ฐฉ๋ฒ•์œผ๋กœ ์˜ค๋””์˜ค๋ฅผ ์žฌ์ƒํ•ด์ค˜.

๐Ÿ’ก ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ ํƒ ๊ฐ€์ด๋“œ - ์Œ์•… ์ œ์ž‘/์‹ ๋””์‚ฌ์ด์ € โ†’ Tone.js
- ์˜ค๋””์˜ค ๋ถ„์„/๋จธ์‹ ๋Ÿฌ๋‹ โ†’ Meyda.js
- ๊ฒŒ์ž„/์•ฑ ์˜ค๋””์˜ค ๊ด€๋ฆฌ โ†’ Howler.js
- ์ €์ˆ˜์ค€ ์ปค์Šคํ…€ ์ฒ˜๋ฆฌ โ†’ Web Audio API ์ง์ ‘ ์‚ฌ์šฉ

๐ŸŽ“ ์‹ค์ „ ํŒ๊ณผ ๋””๋ฒ„๊น… ๋ฐฉ๋ฒ•
๐Ÿ” Chrome DevTools๋กœ ์˜ค๋””์˜ค ๋””๋ฒ„๊น…

Chrome ๋ธŒ๋ผ์šฐ์ €์˜ DevTools์—๋Š” Web Audio ์ „์šฉ ํŒจ๋„์ด ์žˆ์–ด!
chrome://inspect/#webaudio์— ์ ‘์†ํ•˜๊ฑฐ๋‚˜, DevTools โ†’ More tools โ†’ Web Audio๋ฅผ ์„ ํƒํ•˜๋ฉด ๋ผ.
ํ˜„์žฌ AudioContext์˜ ๋…ธ๋“œ ๊ทธ๋ž˜ํ”„๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ์–ด์„œ ๋””๋ฒ„๊น…์ด ํ›จ์”ฌ ์‰ฌ์›Œ์ ธ.

๐ŸŽฏ ์ž์ฃผ ํ•˜๋Š” ์‹ค์ˆ˜๋“ค
โš ๏ธ ์‹ค์ˆ˜ 1: AudioContext ์—ฌ๋Ÿฌ ๊ฐœ ์ƒ์„ฑ AudioContext๋Š” ํ•˜๋‚˜๋งŒ ์ƒ์„ฑํ•ด์„œ ์žฌ์‚ฌ์šฉํ•ด์•ผ ํ•ด. ์—ฌ๋Ÿฌ ๊ฐœ ๋งŒ๋“ค๋ฉด ๋ฆฌ์†Œ์Šค ๋‚ญ๋น„์•ผ.
์‹ฑ๊ธ€ํ†ค ํŒจํ„ด์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒŒ ์ข‹์•„.
โš ๏ธ ์‹ค์ˆ˜ 2: 0์œผ๋กœ exponentialRamp ์„ค์ • exponentialRampToValueAtTime์˜ ๋ชฉํ‘œ๊ฐ’์€ 0์ด ๋  ์ˆ˜ ์—†์–ด (์ˆ˜ํ•™์ ์œผ๋กœ ์ง€์ˆ˜ ํ•จ์ˆ˜๋Š” 0์— ๋„๋‹ฌ ๋ถˆ๊ฐ€).
0 ๋Œ€์‹  0.001 ๊ฐ™์€ ๋งค์šฐ ์ž‘์€ ๊ฐ’์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ด.
โš ๏ธ ์‹ค์ˆ˜ 3: ์—ฐ๊ฒฐ ํ•ด์ œ ์—†์ด ๋…ธ๋“œ ๋ฒ„๋ฆฌ๊ธฐ ๋…ธ๋“œ๋ฅผ ๋” ์ด์ƒ ์‚ฌ์šฉํ•˜์ง€ ์•Š์„ ๋•Œ๋Š” ๋ฐ˜๋“œ์‹œ disconnect()๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•ด.
๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์–ด.
โš ๏ธ ์‹ค์ˆ˜ 4: ํด๋ฆฌํ•‘ ๋ฌด์‹œ ์—ฌ๋Ÿฌ ์†Œ์Šค๋ฅผ ๋ฏน์Šคํ•  ๋•Œ ํ•ฉ์‚ฐ ๊ฒŒ์ธ์ด 1.0์„ ์ดˆ๊ณผํ•˜๋ฉด ํด๋ฆฌํ•‘(์ฐŒ๊ทธ๋Ÿฌ์ง)์ด ๋ฐœ์ƒํ•ด.
๋งˆ์Šคํ„ฐ ๊ฒŒ์ธ์„ ๋‚ฎ์ถ”๊ฑฐ๋‚˜ 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์˜ ๋ฏธ๋ž˜ โ€” ์•ž์œผ๋กœ ๋ญ๊ฐ€ ๋” ์˜ฌ๊นŒ?

Web Audio API๋Š” ๊ณ„์† ๋ฐœ์ „ํ•˜๊ณ  ์žˆ์–ด. ํ˜„์žฌ W3C์—์„œ ๋…ผ์˜ ์ค‘์ธ ๊ธฐ๋Šฅ๋“ค์„ ์‚ดํŽด๋ณด์ž.

๐Ÿ”ฎ Web MIDI API์™€์˜ ์—ฐ๋™

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);

}

};

});

}
๐Ÿค– Web Audio API + ๋จธ์‹ ๋Ÿฌ๋‹

TensorFlow.js์™€ Web Audio API๋ฅผ ๊ฒฐํ•ฉํ•˜๋ฉด ๋ธŒ๋ผ์šฐ์ €์—์„œ ์‹ค์‹œ๊ฐ„ ์Œ์•… ๋ถ„๋ฅ˜, ์•…๊ธฐ ์ธ์‹, ๊ฐ์ • ๋ถ„์„ ๋“ฑ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์–ด.
Google์˜ Magenta.js ํ”„๋กœ์ ํŠธ๊ฐ€ ๋Œ€ํ‘œ์ ์ธ ์˜ˆ์•ผ โ€” ๋ธŒ๋ผ์šฐ์ €์—์„œ AI๊ฐ€ ์Œ์•…์„ ์ƒ์„ฑํ•˜๊ณ  ์—ฐ์ฃผํ•˜๋Š” ๊ฑฐ์•ผ!

์žฌ๋Šฅ๋„ท์—์„œ ์Œ์•… ๊ด€๋ จ ์žฌ๋Šฅ์„ ๊ฐ€๋ฅด์น˜๋Š” ๋ถ„๋“ค์ด๋ผ๋ฉด, ์ด๋Ÿฐ ๊ธฐ์ˆ ์„ ํ™œ์šฉํ•ด์„œ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ๋ ˆ์Šจ ๋„๊ตฌ๋ฅผ ๋งŒ๋“ค์–ด๋ณผ ์ˆ˜๋„ ์žˆ๊ฒ ์ง€? ๐Ÿ˜Š

๐ŸŽฏ Web Audio API + TensorFlow.js ํ™œ์šฉ ์‚ฌ๋ก€ - ์‹ค์‹œ๊ฐ„ ์•…๊ธฐ ์ธ์‹ (ํ”ผ์•„๋…ธ, ๊ธฐํƒ€, ๋“œ๋Ÿผ ๋“ฑ)
- ์Œ์„ฑ ๊ฐ์ • ๋ถ„์„
- ์ž๋™ ์Œ์•… ์ƒ์„ฑ (AI ์ž‘๊ณก)
- ์Œ์•… ์žฅ๋ฅด ๋ถ„๋ฅ˜
- ๋ฐ•์ž/๋ฆฌ๋“ฌ ์ž๋™ ๊ฐ์ง€

๐Ÿ“š ๋งˆ๋ฌด๋ฆฌ โ€” ์ •๋ฆฌํ•˜๋ฉฐ

์ž, ์—ฌ๊ธฐ๊นŒ์ง€ Web Audio API์˜ ํ•ต์‹ฌ ๊ฐœ๋…๋ถ€ํ„ฐ ์‹ค์ „ ํ™œ์šฉ๊นŒ์ง€ ์ญ‰ ์‚ดํŽด๋ดค์–ด!
์ •๋ง ๊ธด ์—ฌ์ •์ด์—ˆ๋Š”๋ฐ ์ž˜ ๋”ฐ๋ผ์™€์คฌ์–ด? ๐Ÿ˜„

Web Audio API๋Š” ์ฒ˜์Œ์—” ๋ณต์žกํ•ด ๋ณด์ด์ง€๋งŒ, ํ•ต์‹ฌ ๊ฐœ๋…์ธ ๋…ธ๋“œ ๊ทธ๋ž˜ํ”„๋ฅผ ์ดํ•ดํ•˜๋ฉด ๋‚˜๋จธ์ง€๋Š” ๋ ˆ๊ณ  ๋ธ”๋ก ์กฐ๋ฆฝํ•˜๋“ฏ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ด์–ด์ ธ.

ํ•ต์‹ฌ์„ ๋‹ค์‹œ ํ•œ๋ฒˆ ์ •๋ฆฌํ•ด๋ณผ๊ฒŒ:

1
AudioContext๊ฐ€ ๋ชจ๋“  ๊ฒƒ์˜ ์‹œ์ž‘. ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜ ํ›„์— ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜ resumeํ•ด์•ผ ํ•ด.
2
๋…ธ๋“œ ๊ทธ๋ž˜ํ”„๋กœ ์†Œ๋ฆฌ๋ฅผ ์ฒ˜๋ฆฌ. ์†Œ์Šค โ†’ ์ฒ˜๋ฆฌ โ†’ ์ถœ๋ ฅ ์ˆœ์œผ๋กœ connect()๋กœ ์—ฐ๊ฒฐ.
3
AudioParam์œผ๋กœ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์‹œ๊ฐ„์— ๋”ฐ๋ผ ์ •๋ฐ€ํ•˜๊ฒŒ ์ œ์–ด. ADSR ์—”๋ฒจ๋กœํ”„ ๊ตฌํ˜„์˜ ํ•ต์‹ฌ.
4
AnalyserNode๋กœ ์˜ค๋””์˜ค๋ฅผ ๋ถ„์„ํ•˜๊ณ  Canvas๋กœ ์‹œ๊ฐํ™”. ๋น„์ฃผ์–ผ๋ผ์ด์ €์˜ ํ•ต์‹ฌ.
5
AudioWorklet์œผ๋กœ ์ปค์Šคํ…€ DSP ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์˜ค๋””์˜ค ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰. ๊ณ ๊ธ‰ ์ฒ˜๋ฆฌ์˜ ๋ํŒ์™•.
6
๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ๋ฅผ ์žŠ์ง€ ๋งˆ. disconnect(), close(), null ์ฒ˜๋ฆฌ๋กœ ๊น”๋”ํ•˜๊ฒŒ ์ •๋ฆฌ.
๐ŸŽต Web Audio API๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋Š” ๊ฒƒ๋“ค ์‹ ๋””์‚ฌ์ด์ € ยท ์ดํ€„๋ผ์ด์ € ยท ๋ฆฌ๋ฒ„๋ธŒ ยท ๋”œ๋ ˆ์ด ยท ์ปดํ”„๋ ˆ์„œ ยท ๋น„์ฃผ์–ผ๋ผ์ด์ € ยท ํŠœ๋„ˆ ยท ๋ฉ”ํŠธ๋กœ๋†ˆ ยท DAW ยท ๊ฒŒ์ž„ ์˜ค๋””์˜ค ยท ๋ณด์ด์Šค ์ดํŽ™ํ„ฐ ยท ์Œ์„ฑ ๋ถ„์„ ยท 3D ๊ณต๊ฐ„ ์Œํ–ฅ ยท AI ์Œ์•… ์ƒ์„ฑ...

๋ธŒ๋ผ์šฐ์ € ํ•˜๋‚˜๋กœ ์ด ๋ชจ๋“  ๊ฒŒ ๊ฐ€๋Šฅํ•ด. ์ •๋ง ๋Œ€๋‹จํ•˜์ง€ ์•Š์•„? ๐Ÿš€

JavaScript์™€ Web Audio API์˜ ์กฐํ•ฉ์€ ์Œ์•…๊ณผ ๊ธฐ์ˆ ์˜ ๊ฒฝ๊ณ„๋ฅผ ํ—ˆ๋ฌผ๊ณ  ์žˆ์–ด.
์ด ๊ธ€์„ ์ฝ๊ณ  ๋‚˜์„œ ๋ญ”๊ฐ€ ๋งŒ๋“ค์–ด๋ณด๊ณ  ์‹ถ์€ ์•„์ด๋””์–ด๊ฐ€ ์ƒ๊ฒผ๋‹ค๋ฉด, ๊ทธ๊ฒŒ ๋ฐ”๋กœ ์‹œ์ž‘์ด์•ผ!
์ž‘์€ ์˜ค์‹ค๋ ˆ์ดํ„ฐ ํ•˜๋‚˜๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ, ์ ์  ๋ณต์žกํ•œ ์‹ ์Šค๋ฅผ ๋งŒ๋“ค์–ด๊ฐ€๋‹ค ๋ณด๋ฉด ์–ด๋А์ƒˆ ๋ธŒ๋ผ์šฐ์ € DAW๋ฅผ ๋งŒ๋“ค๊ณ  ์žˆ๋Š” ์ž์‹ ์„ ๋ฐœ๊ฒฌํ•˜๊ฒŒ ๋  ๊ฑฐ์•ผ ๐Ÿ˜„

์ฝ”๋”ฉ ์ฆ๊ฒ๊ฒŒ ํ•˜๊ณ , ์ข‹์€ ์Œ์•… ๋งŽ์ด ๋งŒ๋“ค์–ด๋ด! ๐ŸŽถ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0
๋Œ“๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค.

์•„์ง ๋Œ“๊ธ€์ด ์—†์Šต๋‹ˆ๋‹ค.