背景
通过开发学习 Web Aduio API 的基本特性。
运行 Codelab 需要的工具
鸣谢
安装 Google Chrome
访问这里,点击“下载 Chrome”然后安装。
安装 Chrome Dev Editor
请用 Chrome 打开 这个 URL,然后安装 Chrome Dev Editor 。
folder_open设置应用使用的目录。
启动 Chrome Dev Editor
在 Chrome 地址栏中输入 chrome://apps
, 然后点击 Chrome Dev Editor 启动。
点击左侧面板下方的 add_circle 按钮,在弹出窗口的 [CHOOSE FOLDER] 区域指定工作目录。
※ 第一次启动 Chrome Deve Editor, 会出现一个窗口让你填写 "Project Name" 和 "Script type"。
这里我们按下面内容填写
Project Name: test_webaudioapi
Project type: JavaScript web app
使用振荡器节点(Oscillator Node)播放声音。
复制黏贴下面代码到 index.html 中。
<button onclick="Play()">Play</button>
<script type="text/javascript">
var audioctx = new AudioContext();
var osc = audioctx.createOscillator();
osc.connect(audioctx.destination);
const Play = () => {
osc.start(0);
}
</script>
点击 Chrome Dev Editor 左上方的运行按钮 play_arrow 启动应用。
点击浏览器窗口中 按钮,能够听到声音就对了。这段代码没考虑停止声音的事情,需要刷新页面才能停止。
通过如下过程创建节点图(Node Graph)。
- 创建一个 AudioContext
new AudioContext();
- 创建一个振荡器
audioctx.createOscillator();
- 连接到 destination 完成节点图
osc.connect(audioctx.destination);
- 通过点击按钮启动振荡器。
振荡器是 Web Audio API 的基本部分之一,这些基本部分称为“节点”(Node)。默认有许多这种类型的基本节点可用。基本上使用 Web Audio API 创建应用程序的一种方法就是彼此连接这些节点,并创建节点图。
节点图的终点被称为目的节点 destination (destination node)。目的节点在 AudioContext 构造的时候会被自动创建。连接到目的节点后,节点图就可以播放声音了。
另外,我们创建的这个节点图就像下面这样。
(接下来,我们不再解释如何启动应用程序了。)
振荡器可以创建声音,但声音有点单调。下面我们尝试用其他振荡器来调节振荡器的旋律。
删掉上一步我们在 index.html 中添加的代码, 并添加下面的代码到 index.html 中。
<table>
<tr><td>LFO Freq : </td><td><input type="text" size="10" id="lfofreq" value="5"/></td></tr>
<tr><td>Depth : </td><td><input type="text" size="10" id="depth" value="10"/></td></tr>
<tr><td>VCO Freq : </td><td><input type="text" size="10" id="vcofreq" value="440"/></td></tr>
</table>
<br/>
<button onclick="Setup()">Set</button><br/>
<script type="text/javascript">
var audioctx = new AudioContext();
var play = 0;
var vco = audioctx.createOscillator();
var lfo = audioctx.createOscillator();
var depth = audioctx.createGain();
vco.connect(audioctx.destination);
lfo.connect(depth);
depth.connect(vco.frequency); // <== connect to frequency parameter of vco
const Setup = () => {
if(play == 0) {
play = 1;
vco.start(0);
lfo.start(0);
}
vco.frequency.value = parseFloat(document.getElementById("vcofreq").value);
lfo.frequency.value = parseFloat(document.getElementById("lfofreq").value);
depth.gain.value = parseFloat(document.getElementById("depth").value);
}
</script>
完成之后应用就可以正常工作了。点击 按钮播放声音。文本框中的参数影响声音的节奏,修改这些参数再重新点击 按钮。
- VCO Freq = 440
- LFO Freq = 5
- Depth = 10
名词解释:
- VCO : [Voltage-Controlled Oscillator] 由电压控制振荡器频率的振荡器的振荡器。
- LFO : [Low Frequency Oscillator Oscillator] 低频振荡器。
基本思想是用一个振荡器控制发出声音的频率,用另一个振荡器控制声音的节奏。节奏的宽度称为 depth 。
关于这一步中的节点图:
- 创建 AudioContext
new AudioContext();
- 创建发声振荡器
audioctx.createOscillator();
- 创建控制发声振荡器频率的振荡器
audioctx.createOscillator();
- 创建控制 depth 参数的节点
audioCtx.createGain();
- 连接到 audioctx.destination 完成节点视图
vco.connect(audioctx.destination);
- 连接到节奏 depth
lfo.connect(depth);
- 连接到发声振荡器
depth.connect(vco.frequency);
- 通过点击按钮启动和更新振荡器参数
本步骤所创建的节点图。
下载 snare.wav , 然后把它拖到 Chrome Dev Editor 左侧面板对应的项目中(这里是"test_webaudioapi")"
删除上一步添加到 index.html 中的代码, 然后添加下的代码到 index.html 中。
<button onclick="Play()" id="playsound" disabled>Play</button>
<script type="text/javascript">
var audioctx = new AudioContext();
var buffer = null;
const Play = () => {
var src = audioctx.createBufferSource();
src.buffer = buffer;
src.connect(audioctx.destination);
src.start(0);
}
const LoadSample = (ctx, url) => {
fetch(url).then( response => {
return response.arrayBuffer();
}).then( arrayBuffer => {
ctx.decodeAudioData(arrayBuffer, (b) => {buffer=b;}, () => {});
document.querySelector("button#playsound").removeAttribute("disabled");
});
}
LoadSample(audioctx, "./snare.wav");
</script>
点击 按钮能听到小鼓的声音就表示应用创建成功了。
过程如下:
- 通过
fetch()
从服务端获取文件
- 复制声音数据到
audioctx.createBufferSource();
所创建的 buffer 中
- 连接到 audioctx.destination 完成节点图
src.connect(audioctx.destination);
- 通过点击按钮播放 buffer 中的声音
本步骤所创建的节点图。
本步骤我们要用一个持续时间较长的音频文件。
所以请下载 loop.wav 文件,并把它拖到 Chrome Dev Editor 左侧面板的项目中("test_webaudioapi") 。
接下来删除前一步 index.html 中的代码,然后先把如下代码添加到 index.html 中。
<button id="playsound" disabled>Play</button><br/>
<table>
<tr><td>Bypass :</td><td><input id="bypass" type="checkbox"/></td></tr>
<tr><td>Time : </td><td><input type="text" size="8" id="time" value="0.25"/></td></tr>
<tr><td>Feedback : </td><td><input type="text" size="8" id="feedback" value="0.4"/></td></tr>
<tr><td>Mix : </td><td><input type="text" size="8" id="mix" value="0.4"/></td></tr>
</table>
这一步,我们要创建的目标节点图如下。
解释一下新出现的名词:
- 干(Dry) : 原始输入的声音。
- 湿(Wet) : 应用到原始输入中的音频效果的声音。
- 音效(Audio Effect) : 通过某种方式创建不同的声音。例如,延时是把输入信号记录到音频存储介质中,然后过段时间再播放出来的一种音效;混响是一个声音产生之后声音持续性的一种音效。
这些节点的角色
- 增益(干)/Gain(Dry) 和 增益(湿)/Gain(Wet) : 控制原始声音和音效声音的比率。
- 延时 : 一段时间之后回放声音。
- 增益反馈/Gain(Feedback) :反馈延时效果音到原始音源中。
<script type="text/javascript">
var audioctx = new AudioContext();
var buffer = null;
var src = null;
var input = audioctx.createGain();
var delay = audioctx.createDelay();
var wetgain = audioctx.createGain();
var drygain = audioctx.createGain();
var feedback = audioctx.createGain();
input.connect(delay);
input.connect(drygain);
delay.connect(wetgain);
delay.connect(feedback);
feedback.connect(delay);
wetgain.connect(audioctx.destination);
drygain.connect(audioctx.destination);
const LoadSample = (ctx, url) => {
fetch(url).then( response => {
return response.arrayBuffer();
}).then( arrayBuffer => {
ctx.decodeAudioData(arrayBuffer, (b) => {buffer=b;}, () => {});
document.querySelector("button#playsound").removeAttribute("disabled");
});
}
LoadSample(audioctx, "./loop.wav");
</script>
节点图解释:
- 连接输入到 Delay 和 Gain(Dry)
- 连接输出从 Delay 到 Gain(Wet)
- 连接输出从 Delay 到 Gain(Feedback)
- 连接输出从 Gain(Feedback) 到 Delay
最后编写按钮的事件处理函数。
之前的事件处理函数我们是写在 HTML 里的,现在开始我们写在 JavaScript 里。
<script type="text/javascript">
const Setup = () => {
var bypass = document.getElementById("bypass").checked;
delay.delayTime.value = parseFloat(document.getElementById("time").value);
feedback.gain.value = parseFloat(document.getElementById("feedback").value);
var mix = parseFloat(document.getElementById("mix").value);
if(bypass) mix = 0;
wetgain.gain.value = mix;
drygain.gain.value = 1 - mix;
}
document.querySelector("button#playsound").addEventListener("click", (event) => {
var label;
if(event.target.innerHTML=="Stop") {
src.stop(0);
label="Start";
} else {
src = audioctx.createBufferSource();
src.buffer = buffer;
src.loop = true;
src.connect(input);
src.start(0);
label="Stop";
}
event.target.innerHTML=label;
});
document.querySelector("input#bypass").addEventListener("change", Setup);
document.querySelector("input#time").addEventListener("change", Setup);
document.querySelector("input#feedback").addEventListener("change", Setup);
document.querySelector("input#mix").addEventListener("change", Setup);
Setup();
</script>
添加完上面三段代码后,运行应用程序。点击 按钮播放声音,修改参数、勾选或取消勾选 ByPass 复选框改变 depth 以及开启或关闭延时效果。
和添加延迟节点的方法一样,添加下面的节点可以获得对应的音效:
- PannerNode : 分配声音到每一个扬声器
- BiquadFilterNode : 过滤声音 ( 例如: 低通, 高通, 等)
- GainNode : 控制振幅 (控制音量)
Analyser node 提供了声音可视化的功能。使用这个节点可视化声音非常简单,让我们开始吧。
我们用上一步用过的 loop.wav 文件来分析声音。
如果你还没有下载这个文件,请从 这里 下载,并把它拖放到 Chrome Dev Editor 左侧面板的项目中。
接下来删除在上一步添加到 index.html 中的代码,然后把下面的 HTML 代码先添加到 index.html 中。
<button id="playsound" disabled>Play</button><br/>
<table>
<tr><td>Frequency/TimeDomain : </td><td><select id="mode" ><option>Frequency</option><option>TimeDomain</option></select></td></tr>
<tr><td>SmoothingTimeConstant : </td><td><input type="text" id="smoothing" value="0.9"/></td></tr>
<tr><td>MinDecibels : </td><td><input type="text" id="min"/></td></tr>
<tr><td>MaxDecibels : </td><td><input type="text" id="max"/></td></tr>
</table>
<br/><br/>
<canvas id="graph" width="512" height="256"></canvas>
因为可视化输入源不需要添加任何音效,所以节点图如下。
在 BufferSource 后面添加 Analyser 节点。
从 Analyser 节点获取数据,添加画图过程如下:
<script type="text/javascript">
var audioctx = new AudioContext();
var buffer = null;
var src = null;
const LoadSample = (ctx, url) => {
fetch(url).then( response => {
return response.arrayBuffer();
}).then( arrayBuffer => {
ctx.decodeAudioData(arrayBuffer, (b) => {buffer=b;}, () => {});
document.querySelector("button#playsound").removeAttribute("disabled");
});
}
LoadSample(audioctx, "./loop.wav");
var mode = 0;
var timerId;
var analyser = audioctx.createAnalyser();
analyser.fftSize = 1024;
document.getElementById("min").value = analyser.minDecibels;
document.getElementById("max").value = analyser.maxDecibels;
var ctx = document.getElementById("graph").getContext("2d");
const DrawGraph = () => {
ctx.fillStyle = "rgba(34, 34, 34, 1.0)";
ctx.fillRect(0, 0, 512, 256);
ctx.strokeStyle="rgba(255, 255, 255, 1)";
var data = new Uint8Array(512);
if(mode == 0) analyser.getByteFrequencyData(data); //Spectrum Data
else analyser.getByteTimeDomainData(data); //Waveform Data
if(mode!=0) ctx.beginPath();
for(var i = 0; i < 256; ++i) {
if(mode==0) {
ctx.fillStyle = "rgba(204, 204, 204, 0.8)";
ctx.fillRect(i*2, 256 - data[i], 1, data[i]);
} else {
ctx.lineTo(i*2, 256 - data[i]);
}
}
if(mode!=0) {
ctx.stroke();
}
requestAnimationFrame(DrawGraph);
}
timerId=requestAnimationFrame(DrawGraph);
const Setup = () => {
mode = document.getElementById("mode").selectedIndex;
analyser.minDecibels = parseFloat(document.getElementById("min").value);
analyser.maxDecibels = parseFloat(document.getElementById("max").value);
analyser.smoothingTimeConstant = parseFloat(document.getElementById("smoothing").value);
}
Setup();
</script>
DrawGraph()
函数负责画图,当窗口中的参数更新时也用这个函数更新图形。解释如下:
- 创建 analyser
createAnalyser();
- 指定 FFT 数据大小为 fftSize
- 运行动画循环
requestAnimationFrame();
,在每一次迭代中从 Analyser 节点获取数据,并在 canvas 上更新图形实现数值的可视化。
最后添加点击按钮的事件处理函数。
<script type="text/javascript">
document.querySelector("button#playsound").addEventListener("click", (event) => {
var label;
if(event.target.innerHTML=="Stop") {
src.stop(0);
cancelAnimationFrame(timerId);
label="Start";
} else {
src = audioctx.createBufferSource();
src.buffer = buffer;
src.loop = true;
src.connect(audioctx.destination);
src.connect(analyser);
src.start(0);
label="Stop";
}
event.target.innerHTML=label;
});
document.querySelector("select#mode").addEventListener("change", Setup);
document.querySelector("input#smoothing").addEventListener("change", Setup);
document.querySelector("input#min").addEventListener("change", Setup);
document.querySelector("input#max").addEventListener("change", Setup);
</script>
添加这三段代码之后运行应用程序。点击 按钮播放歌曲,图形会显示在 canvas 中。修改 Frequency/TimeDomain 参数会更新图形。
※ 在本页面中,点击下面的图形区域会改变它的显示类型(Frequency/TimeDomain) 。
这一步中,我们要可视化来自麦克的实时输入数据。
这里我们要添加新代码到上一步的代码中,所以请不要删除旧代码。
添加下面的 HTML 到 index.html 中,添加的位置不重要。
<button id="startmic">Start Mic</button><br/>
接着添加下面的 JavaScript 代码。和前面一样,放置的位置不重要。
<script type="text/javascript">
var getUserMedia = navigator.getUserMedia ? 'getUserMedia' :
navigator.webkitGetUserMedia ? 'webkitGetUserMedia' :
navigator.mozGetUserMedia ? 'mozGetUserMedia' :
navigator.msGetUserMedia ? 'msGetUserMedia' :
undefined;
var astream, micsrc;
var conditions={audio:true, video:false};
const Mic = () => {
navigator[getUserMedia](
conditions,
(stream) => {
astream=stream;
micsrc=audioctx.createMediaStreamSource(stream);
micsrc.connect(audioctx.destination);
micsrc.connect(analyser);
},
(e) => { console.error(e); }
);
}
// event handler
document.querySelector("button#startmic").addEventListener("click", (event) => {
var label;
if(event.target.innerHTML=="Start Mic") {
Mic();
label="Stop Mic";
} else {
(astream.getAudioTracks())[0].stop();
label="Start Mic";
}
event.target.innerHTML=label;
});
</script>
由于可视化麦克输入不需要添加任何音效,所以节点图如下。
算法如下:
- 获取麦克输入
getUserMedia();
- 从 stream 中获得音频数据流
audioctx.createMediaStreamSource(stream)
- 连接音频数据流到 analyser
micsrc.connect(analyser);
- 连接音频数据流到 audioctx.destination 完成节点图
micsrc.connect(audioctx.destination);
- 点击按钮开始可视化麦克输入
点击 按钮开始执行应用程序。修改参数(如 Frequency/TimeDomain) 更新图形样式。
※ 在这里,点击图形区域就会改变样式。
通过卷积(convoluting) 音频输入/文件和脉冲响应音频文件源添加音效。
听上去很难,但是 Web Audio API 默认提供了 ConvolverNode 使得卷积非常容易。
从这里下载脉冲响应文件 s1_r1_bd.wav,然后把它拖到 Chrome Dev Editor 左侧面板的项目中。
※ 这个文件(s1_r1_bd.wav) 仅在 这里 提供,如果是非商业用途可以免费使用。
删除之前添加到 index.html 里的代码,然后添加下面新的 HTML 到 index.html 中(位置不重要)。
<button id="playsound" disabled>Start</button><br/>
ReverbLevel : <input type="range" id="revlevel" min="0" max="100" value="50"/>
<span id="revdisp">50</span>
接着添加 JavaScript 代码
<script type="text/javascript">
var audioctx = new AudioContext();
var files = [
"loop.wav",
"s1_r1_bd.wav",
];
var source = null;
var convolver = audioctx.createConvolver();
var revlevel = audioctx.createGain();
revlevel.gain.value=0.5;
convolver.connect(revlevel);
revlevel.connect(audioctx.destination);
var buffers = [];
var loadidx = 0;
const LoadSample = (ctx, idx) => {
fetch(files[idx]).then( response => {
return response.arrayBuffer();
}).then( arrayBuffer => {
ctx.decodeAudioData(arrayBuffer, (b) => {buffers[idx]=b;}, () => {});
if(files.length==buffers.length) {
document.querySelector("button#playsound").removeAttribute("disabled");
}
});
}
for(var i=0; i<files.length; i++) {
LoadSample(audioctx, i);
}
</script>
Convolver Node 与输入源和 destination 交互。这样节点图像这样:
最后,添加点击按钮的事件处理函数。
<script type="text/javascript">
document.querySelector("button#playsound").addEventListener("click", (event) => {
var label;
if(event.target.innerHTML=="Stop") {
src.stop(0);
label="Start";
} else {
src = audioctx.createBufferSource();
src.buffer = buffers[0];
convolver.buffer = buffers[1];
src.loop = true;
src.connect(audioctx.destination);
src.connect(convolver);
src.start(0);
label="Stop";
}
event.target.innerHTML=label;
});
document.querySelector("input#revlevel").addEventListener("change", (event) => {
var lev=event.target.value;
revlevel.gain.value=parseInt(lev)*0.01;
document.querySelector("#revdisp").innerHTML=lev;
});
</script>
点击 按钮应用程序就开始播放音频文件的声音。使用下面的滑块可以改变音效的等级。