网络音频时钟:让频率听起来正确
Web Audio Clock: getting frequency to sound right
尝试在 JS 中创建一个单一的振荡器,具有平滑、无咔哒声的频率音调,可以快速打开和关闭。 [Chris Wallace][1] 讨论的网络音频时序。 [Aqilah Misuary][2] 可以找到定时调度程序。此示例根据频率生成听起来正确的音调。但是,会出现明显的咔哒声。 [Stack Overflow][3] 上的帖子已解决点击问题。
第一个代码片段基于 Code Pen 版本(参见 link 2),将频率更改为 432 Hz。
第二个代码片段:
从这些资源中改编 JS(参见 links 2 和 3),添加斜坡,使用 432 Hz 频率,我可以点击停止,但我的频率声音现在变钝了,不再有正确的 432 Hz频响。我怀疑我做错了什么,没有正确使用时间或设置。我已经尝试了各种时间和设置,甚至尝试了“初始”而不是“指数”斜坡,但是 none 已经解决了这个问题:点击消失但频率声音现在变钝了,没有保留基于选择的频率,如果不使用斜坡,就会听到,留下咔哒声。我一定是做错了什么?
有什么方法可以在不减弱声音的情况下解决点击问题?
[1]: https://www.html5rocks.com/en/tutorials/audio/scheduling/
[2]: https://codepen.io/aqilahmisuary/pen/ONEKVM
[3]:
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Untitled Document</title>
</head>
<body>
<p><button name="button" id="startBtn">Start</button>
<button name="button" id="stopBtn">Stop</button></p>
<p>Audio Context Current Time:</p>
<p><span id="clock"></span></p>
<p>nextNotetime:</p>
<p><span id="nextNote"></span></p>
<style>
span, p, button {
font-family: 'Courier New', Courier, 'Lucida Sans Typewriter', 'Lucida Typewriter', monospace;
font-size: 25px;
font-weight: 1000;
line-height: 26.4px;
text-align: center;
}
span {
font-weight: 200;
}
</style>
<script>
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var audioContext = new AudioContext();
var nextNotetime = audioContext.currentTime;
var clock = document.getElementById("clock");
var nextNote = document.getElementById("nextNote");
var startBtn = document.getElementById("startBtn");
var stopBtn = document.getElementById("stopBtn");
var timerID;
setInterval(function(){ clock.innerHTML = audioContext.currentTime; }, 100);
function playSound(time) {
var osc = audioContext.createOscillator();
osc.connect(audioContext.destination);
osc.frequency.value = 432;
osc.start(time);
osc.stop(time + 0.1);
};
function scheduler() {
while(nextNotetime < audioContext.currentTime + 0.1) {
nextNotetime += 0.5;
nextNote.innerHTML = nextNotetime;
playSound(nextNotetime);
}
timerID = window.setTimeout(scheduler, 50.0);
}
startBtn.addEventListener('click', function() {
scheduler();
}, false);
stopBtn.addEventListener('click', function() {
clearTimeout(timerID);
}, false);
if(audioContext.state === 'suspended'){
audioContext.resume();
};
</script>
</body>
</html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Untitled Document</title>
</head>
<body>
<p><button name="button" id="startBtn">Start</button>
<button name="button" id="stopBtn">Stop</button></p>
<p>Audio Context Current Time:</p>
<p><span id="clock"></span></p>
<p>nextNotetime:</p>
<p><span id="nextNote"></span></p>
<style>
span, p, button {
font-family: 'Courier New', Courier, 'Lucida Sans Typewriter', 'Lucida Typewriter', monospace;
font-size: 25px;
font-weight: 1000;
line-height: 26.4px;
text-align: center;
}
span {
font-weight: 200;
}
</style>
<script>
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var audioContext = new AudioContext();
var nextNotetime = audioContext.currentTime;
var clock = document.getElementById("clock");
var nextNote = document.getElementById("nextNote");
var startBtn = document.getElementById("startBtn");
var stopBtn = document.getElementById("stopBtn");
var timerID;
var gainNode = audioContext.createGain();
var osc;
var rampDuration = 0.3;
gainNode.connect(audioContext.destination);
setInterval(function(){ clock.innerHTML = audioContext.currentTime; }, 100);
function playSound(time) {
osc = audioContext.createOscillator();
osc.connect(gainNode);
osc.frequency.value = 432;
osc.type = "sine";
gainNode.gain.setValueAtTime(1, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(0.0001, audioContext.currentTime + rampDuration);
gainNode.gain.linearRampToValueAtTime(1, audioContext.currentTime + rampDuration);
osc.start(time);
osc.stop(time + 0.01);
};
function scheduler() {
while(nextNotetime < audioContext.currentTime + 0.1) {
nextNotetime += 0.5;
nextNote.innerHTML = nextNotetime;
playSound(nextNotetime);
}
timerID = window.setTimeout(scheduler, 50.0);
}
startBtn.addEventListener('click', function() {
scheduler();
}, false);
stopBtn.addEventListener('click', function() {
clearTimeout(timerID);
}, false);
if(audioContext.state === 'suspended'){
audioContext.resume();
};
</script>
</body>
</html>
根据评论,我们本质上是在处理一个需要 精确 计时的短重复循环,因此 an AudioBufferSourceNode 是我们选择的武器:
It's especially useful for playing back audio which has particularly stringent timing accuracy requirements, such as for sounds that must match a specific rhythm
不幸的是,这也意味着我们需要亲自动手并编写一些 DSP 代码来合成该缓冲区,但老实说并没有那么糟糕(特别是因为我们只能处理浮点数,而不是原始 PCM 缓冲区...).
为了避免咔嗒声(振荡器在中间阶段被切断,就像过去一样),我们利用了正弦波总是从零开始的事实,我们渲染了一个声音循环,所以所有我们需要做的是确保浪潮的结束不会突然停止。我们通过稍微调整音调的长度来做到这一点,以确保最后一个可听到的样本非常接近于零。
这里的例子有几个按钮来演示不同的参数。您可以将它们连接到 UI 个组件中。
var audioContext = new AudioContext();
var gainNode = audioContext.createGain();
gainNode.connect(audioContext.destination);
var playerNode = null; // Initialized later.
function createLoop(audioContext, toneFrequency, toneDuration, loopDuration) {
const arrayBuffer = audioContext.createBuffer(
1,
audioContext.sampleRate * loopDuration,
audioContext.sampleRate,
);
const channel = arrayBuffer.getChannelData(0); // mono, only deal with single channel
const toneDurationInSamples = Math.floor(audioContext.sampleRate * toneDuration);
const phasePerSample = (Math.PI * 2 * toneFrequency) / audioContext.sampleRate;
let audible = true;
for (let i = 0; i < arrayBuffer.length; i++) {
if (audible) {
let value = Math.sin(phasePerSample * i);
channel[i] = value;
// We might slightly overshoot the tone's requested duration
// but we need to wait for the oscillation to be near zero
// to avoid an audible click (when the signal "snaps" from an arbitrary
// value to zero).
if (i >= toneDurationInSamples && Math.abs(value) < 0.02) {
audible = false;
}
} else {
channel[i] = 0; // Silence
}
}
return arrayBuffer;
}
function go(hz, length) {
halt(); // Remove the old player node. We couldn't modify the buffer anyway.
playerNode = audioContext.createBufferSource();
playerNode.loop = true;
playerNode.connect(gainNode);
const buf = createLoop(audioContext, hz, length / 5, length);
playerNode.buffer = buf;
playerNode.start();
audioContext.resume();
}
function halt() {
if (playerNode) {
playerNode.stop();
playerNode.disconnect();
playerNode = null;
}
}
function handleVolumeChange(volume) {
gainNode.gain.setValueAtTime(volume, audioContext.currentTime);
}
Since an Audio Context can't start with interaction, you'll need to hit a button...
<br />
<button onclick="go(432, 0.5)">Go at 432 hz</button>
<button onclick="go(880, 0.3)">Go faster and at 880 hz</button>
<button onclick="go(1250, 0.1)">Go really fast and high</button>
<button onclick="halt()">Stop going</button>
<br />
Volume: <input type="range" min="0" max="1" value="1" step="0.01" onInput="handleVolumeChange(event.target.valueAsNumber)">
尝试在 JS 中创建一个单一的振荡器,具有平滑、无咔哒声的频率音调,可以快速打开和关闭。 [Chris Wallace][1] 讨论的网络音频时序。 [Aqilah Misuary][2] 可以找到定时调度程序。此示例根据频率生成听起来正确的音调。但是,会出现明显的咔哒声。 [Stack Overflow][3] 上的帖子已解决点击问题。
第一个代码片段基于 Code Pen 版本(参见 link 2),将频率更改为 432 Hz。
第二个代码片段: 从这些资源中改编 JS(参见 links 2 和 3),添加斜坡,使用 432 Hz 频率,我可以点击停止,但我的频率声音现在变钝了,不再有正确的 432 Hz频响。我怀疑我做错了什么,没有正确使用时间或设置。我已经尝试了各种时间和设置,甚至尝试了“初始”而不是“指数”斜坡,但是 none 已经解决了这个问题:点击消失但频率声音现在变钝了,没有保留基于选择的频率,如果不使用斜坡,就会听到,留下咔哒声。我一定是做错了什么?
有什么方法可以在不减弱声音的情况下解决点击问题?
[1]: https://www.html5rocks.com/en/tutorials/audio/scheduling/
[2]: https://codepen.io/aqilahmisuary/pen/ONEKVM
[3]:
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Untitled Document</title>
</head>
<body>
<p><button name="button" id="startBtn">Start</button>
<button name="button" id="stopBtn">Stop</button></p>
<p>Audio Context Current Time:</p>
<p><span id="clock"></span></p>
<p>nextNotetime:</p>
<p><span id="nextNote"></span></p>
<style>
span, p, button {
font-family: 'Courier New', Courier, 'Lucida Sans Typewriter', 'Lucida Typewriter', monospace;
font-size: 25px;
font-weight: 1000;
line-height: 26.4px;
text-align: center;
}
span {
font-weight: 200;
}
</style>
<script>
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var audioContext = new AudioContext();
var nextNotetime = audioContext.currentTime;
var clock = document.getElementById("clock");
var nextNote = document.getElementById("nextNote");
var startBtn = document.getElementById("startBtn");
var stopBtn = document.getElementById("stopBtn");
var timerID;
setInterval(function(){ clock.innerHTML = audioContext.currentTime; }, 100);
function playSound(time) {
var osc = audioContext.createOscillator();
osc.connect(audioContext.destination);
osc.frequency.value = 432;
osc.start(time);
osc.stop(time + 0.1);
};
function scheduler() {
while(nextNotetime < audioContext.currentTime + 0.1) {
nextNotetime += 0.5;
nextNote.innerHTML = nextNotetime;
playSound(nextNotetime);
}
timerID = window.setTimeout(scheduler, 50.0);
}
startBtn.addEventListener('click', function() {
scheduler();
}, false);
stopBtn.addEventListener('click', function() {
clearTimeout(timerID);
}, false);
if(audioContext.state === 'suspended'){
audioContext.resume();
};
</script>
</body>
</html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Untitled Document</title>
</head>
<body>
<p><button name="button" id="startBtn">Start</button>
<button name="button" id="stopBtn">Stop</button></p>
<p>Audio Context Current Time:</p>
<p><span id="clock"></span></p>
<p>nextNotetime:</p>
<p><span id="nextNote"></span></p>
<style>
span, p, button {
font-family: 'Courier New', Courier, 'Lucida Sans Typewriter', 'Lucida Typewriter', monospace;
font-size: 25px;
font-weight: 1000;
line-height: 26.4px;
text-align: center;
}
span {
font-weight: 200;
}
</style>
<script>
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var audioContext = new AudioContext();
var nextNotetime = audioContext.currentTime;
var clock = document.getElementById("clock");
var nextNote = document.getElementById("nextNote");
var startBtn = document.getElementById("startBtn");
var stopBtn = document.getElementById("stopBtn");
var timerID;
var gainNode = audioContext.createGain();
var osc;
var rampDuration = 0.3;
gainNode.connect(audioContext.destination);
setInterval(function(){ clock.innerHTML = audioContext.currentTime; }, 100);
function playSound(time) {
osc = audioContext.createOscillator();
osc.connect(gainNode);
osc.frequency.value = 432;
osc.type = "sine";
gainNode.gain.setValueAtTime(1, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(0.0001, audioContext.currentTime + rampDuration);
gainNode.gain.linearRampToValueAtTime(1, audioContext.currentTime + rampDuration);
osc.start(time);
osc.stop(time + 0.01);
};
function scheduler() {
while(nextNotetime < audioContext.currentTime + 0.1) {
nextNotetime += 0.5;
nextNote.innerHTML = nextNotetime;
playSound(nextNotetime);
}
timerID = window.setTimeout(scheduler, 50.0);
}
startBtn.addEventListener('click', function() {
scheduler();
}, false);
stopBtn.addEventListener('click', function() {
clearTimeout(timerID);
}, false);
if(audioContext.state === 'suspended'){
audioContext.resume();
};
</script>
</body>
</html>
根据评论,我们本质上是在处理一个需要 精确 计时的短重复循环,因此 an AudioBufferSourceNode 是我们选择的武器:
It's especially useful for playing back audio which has particularly stringent timing accuracy requirements, such as for sounds that must match a specific rhythm
不幸的是,这也意味着我们需要亲自动手并编写一些 DSP 代码来合成该缓冲区,但老实说并没有那么糟糕(特别是因为我们只能处理浮点数,而不是原始 PCM 缓冲区...).
为了避免咔嗒声(振荡器在中间阶段被切断,就像过去一样),我们利用了正弦波总是从零开始的事实,我们渲染了一个声音循环,所以所有我们需要做的是确保浪潮的结束不会突然停止。我们通过稍微调整音调的长度来做到这一点,以确保最后一个可听到的样本非常接近于零。
这里的例子有几个按钮来演示不同的参数。您可以将它们连接到 UI 个组件中。
var audioContext = new AudioContext();
var gainNode = audioContext.createGain();
gainNode.connect(audioContext.destination);
var playerNode = null; // Initialized later.
function createLoop(audioContext, toneFrequency, toneDuration, loopDuration) {
const arrayBuffer = audioContext.createBuffer(
1,
audioContext.sampleRate * loopDuration,
audioContext.sampleRate,
);
const channel = arrayBuffer.getChannelData(0); // mono, only deal with single channel
const toneDurationInSamples = Math.floor(audioContext.sampleRate * toneDuration);
const phasePerSample = (Math.PI * 2 * toneFrequency) / audioContext.sampleRate;
let audible = true;
for (let i = 0; i < arrayBuffer.length; i++) {
if (audible) {
let value = Math.sin(phasePerSample * i);
channel[i] = value;
// We might slightly overshoot the tone's requested duration
// but we need to wait for the oscillation to be near zero
// to avoid an audible click (when the signal "snaps" from an arbitrary
// value to zero).
if (i >= toneDurationInSamples && Math.abs(value) < 0.02) {
audible = false;
}
} else {
channel[i] = 0; // Silence
}
}
return arrayBuffer;
}
function go(hz, length) {
halt(); // Remove the old player node. We couldn't modify the buffer anyway.
playerNode = audioContext.createBufferSource();
playerNode.loop = true;
playerNode.connect(gainNode);
const buf = createLoop(audioContext, hz, length / 5, length);
playerNode.buffer = buf;
playerNode.start();
audioContext.resume();
}
function halt() {
if (playerNode) {
playerNode.stop();
playerNode.disconnect();
playerNode = null;
}
}
function handleVolumeChange(volume) {
gainNode.gain.setValueAtTime(volume, audioContext.currentTime);
}
Since an Audio Context can't start with interaction, you'll need to hit a button...
<br />
<button onclick="go(432, 0.5)">Go at 432 hz</button>
<button onclick="go(880, 0.3)">Go faster and at 880 hz</button>
<button onclick="go(1250, 0.1)">Go really fast and high</button>
<button onclick="halt()">Stop going</button>
<br />
Volume: <input type="range" min="0" max="1" value="1" step="0.01" onInput="handleVolumeChange(event.target.valueAsNumber)">