Trying in JS to create a single oscillator, with a smooth, non-clicking frequency tone that rapidly turns on and off. Timing for web audio discussed by [Chris Wallace][1]. A timing scheduler can be found by [Aqilah Misuary][2]. This example produces a correct-sounding tone based on the frequency. However, a noticeable click is present. Fixing clicks has been addressed by a posting on [Stack Overflow][3].
First code snippet is based on the Code Pen version (see link 2), changing frequency to 432 Hz.
Second code snippet:
Adapted JS (see links 2 and 3) from these resources, adding ramping, using the 432 Hz frequency,I can get the clicking to stop, but my frequency sound is now blunted, no longer has the correct 432 Hz frequency sound. I suspect that I am doing something wrong, not using the timing or settings correctly. I've tried various timings and settings and even "initial" instead of "exponential" ramping, but none have fixed the issue: clicking gone but frequency sound is now blunted, doesn't retain the correct sound based on the frequency chosen and that would be heard if ramping is not used, leaving the clicking. I must be doing something wrong?
Any way to fix the clicking without blunting the sound?
[1]: https://www.html5rocks.com/en/tutorials/audio/scheduling/
[2]: https://codepen.io/aqilahmisuary/pen/ONEKVM
[3]: Web audio "Click" sound even when using exponentialRampToValueAtTime
<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>
Based on the comments, we're essentially dealing with a short repetitive loop that needs exact timing, so an AudioBufferSourceNode is our weapon of choice:
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
Unfortunately that also means we'll need to get our hands dirty and write some DSP code to synthesize that buffer, but it's honestly not that bad (especially since we can just work in floating point numbers, not raw PCM buffers...).
To avoid clicks (where the oscillator is cut off mid-phase, as it were), we take advantage of the facts that a sine wave always starts at zero and we render a single loop of the sound, so all we need to do is make sure the end of the wave does not stop abruptly. We do that by slightly adjusting the length of the tone to make sure the last audible sample is very near to zero.
The example here has a couple of buttons to demonstrate different parameters. You could hook those up into UI components.
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)">
Related
This seems to be an issue after I upgraded my iPod Touch to iOS 15 (15.0.1).
When running the example below, it works fine on the first load, playing the sound as many times as I want. However, if I lock the screen on my iPod Touch, then return after a couple minutes, the sound no longer plays. To troubleshoot I set up a state change listener on the AudioContext instance and verified that Safari sets the state back to running after it was set to interrupted when the screen was locked. Yet, the sound does not play.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>audio-test</title>
</head>
<body>
<script>
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioContext = new AudioContext();
// Create AudioBuffer and fill with two seconds of white noise.
const channels = 2;
const frameCount = audioContext.sampleRate * 2.0;
const audioBuffer = audioContext.createBuffer(channels, frameCount, audioContext.sampleRate);
for (var channel = 0; channel < channels; channel++) {
var nowBuffering = audioBuffer.getChannelData(channel);
for (var i = 0; i < frameCount; i++) {
nowBuffering[i] = Math.random() * 2 - 1;
}
}
const button = document.createElement('button');
button.textContent = 'Play Audio';
document.body.append(button);
button.addEventListener('click', () => {
const currentSourceNode = new AudioBufferSourceNode(audioContext, {
buffer: audioBuffer,
});
currentSourceNode.connect(audioContext.destination);
currentSourceNode.start();
});
</script>
</body>
</html>
Strangely, if I add an audio element to the HTML that points to some mp3 file, which isn't even referenced in the code at all, then locking the screen for a while then returning to the page no longer affects the audio playback.
Updated code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>audio-test</title>
</head>
<body>
<script>
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioContext = new AudioContext();
// Create AudioBuffer and fill with two seconds of white noise.
const channels = 2;
const frameCount = audioContext.sampleRate * 2.0;
const audioBuffer = audioContext.createBuffer(channels, frameCount, audioContext.sampleRate);
for (var channel = 0; channel < channels; channel++) {
var nowBuffering = audioBuffer.getChannelData(channel);
for (var i = 0; i < frameCount; i++) {
nowBuffering[i] = Math.random() * 2 - 1;
}
}
const button = document.createElement('button');
button.textContent = 'Play Audio';
document.body.append(button);
button.addEventListener('click', () => {
const currentSourceNode = new AudioBufferSourceNode(audioContext, {
buffer: audioBuffer,
});
currentSourceNode.connect(audioContext.destination);
currentSourceNode.start();
});
</script>
<audio src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/858/outfoxing.mp3" crossorigin="anonymous"></audio>
</body>
</html>
Screen lock does not allow scripts to run on IOS.
My workaround is to run 2 audio player elements.
One audioElementWithSound and, another with an empty-10s.mp3 file.
Clicking play starts both elements.
I then have an event listner listening to 'ended' on the empty.mp3, re-starting the empty.mp3 if the withSound is not done playing yet.
It would look something like this :
let audioElement();
let stayAwakeAudio();
function playSound(){
audioElement.src = "/assets/audioClip.mp3"
audioElement.play;
stayAwake()
}
function stayAwake() {
console.log("I'm awake");
stayAwakeAudio.src = "/assets/empty10sSoundFile.mp3";
stayAwakeAudio.play();
}
stayAwakeAudio.addEventListener('ended', function () {
if (audioElement.play) {
console.log('Audio still playing')
stayAwake();
}
else {
console.log('No audio playing, letting screen time out');
}
}, false);
the code is short and it cant be because of outdated systems as this laptop was bought 4 months ago.
Every time I click on the index.html file it takes 50 seconds to load and then I get the error. I am not sure what else to do as the Microsoft Help Team was useless. I am not sure if there are any errors in the code as I can't run it. Any help is appreciated.
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<title>A canvas</title>
</head>
<style type="text/css">
canvas {
border: 1px solid black;
}
</style>
<body>
<canvas id="canvas" height="1000px" width="1000px"></canvas>
<script>
var canvas = document.getElementById('canvas');
var c = canvas.getContext('2d');
canvas.addEventListener('mousedown', onDown, false);
var fireWall = false;
var score = 0;
while (score != 10){
c.font = "30px Comic Sans MS"
c.fillText(score, 500, 200)
c.font = "30px Comic Sans MS"
c.fillText("Click the Screen 10 Times to Win the Hardest Game Ever!!!", 50,300);
function onDown(){
console.log('working')
c.score++;
}
if(score === 10){
fireWall = true;
break;
}
}
if(fireWall === true){
c.fill(255, 153, 0);
c.noStroke();
c.ellipse(mouseX-90, mouseY-30, 75, 25);
c.ellipse(mouseX-90, mouseY-80, 85, 35);
c.rect(mouseX-103, mouseY-80, 25,45);
c.rect(mouseX-132, mouseY-155, 85,75);
c.textSize(50);
c.fill(255,0,0);
c.text("YOU WON", mouseX-205, mouseY-170);
}
</script>
</body>
</html>
com/jywPv.png
const c = document.getElementById('canvas').getContext('2d');
const game = { // You could use an Object literal to store stuff
fireWall: false,
score: 0,
mouseX: 0,
mouseY: 0,
};
const draw = () => { // Create a function to handle drawing
// Don't forget to clear the canvas before painting
c.clearRect(0, 0, c.canvas.width, c.canvas.height);
c.font = "30px Comic Sans MS";
if (game.fireWall) {
c.fillText("YOU WON", game.mouseX-80, game.mouseY-5);
} else {
c.fillText(game.score, 500, 200); // Retrieve the score from game Object
c.fillText("Click the Screen 10 Times to Win the Hardest Game Ever!!!", 50, 300);
}
}
const onDown = (ev) => { // Don't forget the ev Event!
if (game.fireWall) return; // Prevent additional clicks if firewall reached
const bcr = c.canvas.getBoundingClientRect();
game.mouseX = ev.clientX - bcr.x;
game.mouseY = ev.clientY - bcr.y;
game.score++;
if (game.score >= 10) {
game.fireWall = true;
}
draw();
}
draw(); // First draw!
c.canvas.addEventListener('mousedown', onDown); // And draw on click
canvas {
border: 1px solid black;
}
<canvas id="canvas" height="1000px" width="1000px"></canvas>
First I just want to say thank you to everyone that has been helping me on this site. It's really helping my confidence with javascript.
I am building an audio/visual app that should play a different sound every time the user clicks. Every time the user clicks, the animation restarts as well and so on. My issue is that I plan on inserting 5-10 sounds that I have designed myself and I would like them to loop with every click. Meaning click once, soun1 plays. Click again, sound 2 plays. So in my mind, I should create a for loop with an array of the sounds. Simple idea, but I have no idea how to do this in vanilla js let alone p5. I'm working with p5 because I might want to add audio effects later on. Anyway, this is what I have tried below. I can play a single sound just fine, and feel like I am on the right track, but I keep getting errors like .play() is not defined.
I know the syntax below is wayyyy off in the loop area. I just started throwing everything at the wall.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script src="resources\p5.js"></script>
<script src="resources\p5.dom.js"></script>
<script src="resources\p5.sound.js"></script>
<script type="text/javascript" src="js\app.js"></script>
<link rel="stylesheet" href="css\style.css">
<title>Breathe</title>
</head>
<body>
</body>
</html>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
let outerDiam = 0;
let cnv;
let px;
let py;
let bgColor;
let sound1;
let sound2;
let sound3;
let sound4;
let sound5;
let allSounds;
function centerCanvas() {
let x = (windowWidth - width) / 2;
let y = (windowHeight - height) / 2;
cnv.position(x, y);
}
function setup() {
sound1 = loadSound('https://www.dl.dropboxusercontent.com/s/hkg7jnhfwic842j/bubbles.mp3?dl=0', loaded);
sound2 = loadSound('https://www.dl.dropboxusercontent.com/s/9el41r25exizbwl/clay.mp3?dl=0', loaded);
sound3 = loadSound('https://www.dl.dropboxusercontent.com/s/8o5rgfknx0do8ps/confetti.mp3?dl=0', loaded);
sound4 = loadSound('https://www.dl.dropboxusercontent.com/s/g5auzxd6lkk522a/corona.mp3?dl=0', loaded);
sound5 = loadSound('https://www.dl.dropboxusercontent.com/s/pc73ig27wmmnc4l/dotted-spiral.mp3?dl=0', loaded);
cnv = createCanvas(windowWidth, windowHeight);
cnv.style('display', 'block');
centerCanvas();
bgColor = random(150, 255);
}
function loaded() {
console.log('song is loaded');
}
function windowResized() {
centerCanvas();
}
function draw() {
background(bgColor);
for (let i = 0; i < 5; i++){
let diam = outerDiam - 30 * i;
if (diam > 0) {
let fade = map(diam, 0, width, 0, 255);
stroke(fade);
noFill();
ellipse(px, py, diam);
}
}
outerDiam = outerDiam + 2;
}
function mousePressed() {
outerDiam = 0;
px = random(width);
py = random(height);
bgColor = random (150, 255);
// if (!sound1.isPlaying()) {
// sound1.play();
// sound1.play();
// } else {
// sound1.pause();
// }
allSounds = [sound1, sound2, sound3, sound4, sound5];
let newSound = [];
for (let i = 0; i < allSounds.length; i++) {
allSounds[i].push(newSound);
allSounds[i].play();
}
}
Not a P5 expert, but you can do this using plain Javascript, using the Audio class:
var audio = new Audio("path/to/audio");
audio.loop = true; //loop
audio.play(); //play
Then, whenever you want to change track:
audio.pause(); //stop playing old track
audio.currentTime = 0; //rewind
audio.src = "path/to/new_track" //change track
audio.play() //play
I'm having an issue with ScriptProcessorNode and lag. Even when doing an incredibly simple process that just takes the sample and does x %= 0.35. Why does this simple ScriptProcessorNode (included below) have so much latency?
The MDN says nothing about latency: https://developer.mozilla.org/en-US/docs/Web/API/ScriptProcessorNode
Also, I know that ScriptProcessorNode is to be deprecated soon, but audioWorklets aren't yet implemented, so this is the best there is.
var baseFreq = 220;
var audioCtx = new(window.AudioContext || window.webkitAudioContext)();
var osc = audioCtx.createOscillator();
osc.type = 'sine';
osc.frequency.value = baseFreq;
osc.start();
var intervals = [1, 1.125, 1.25, 1.5, 1.6666666, 1.875, 2, 4]
var index = 0;
function newTone() {
index++;
index %= intervals.length;
osc.frequency.value = intervals[index] * baseFreq;
}
var modulus = audioCtx.createScriptProcessor();
modulus.onaudioprocess = function(audioProcessingEvent) {
var inputBuffer = audioProcessingEvent.inputBuffer;
var outputBuffer = audioProcessingEvent.outputBuffer;
for (var channel = 0; channel < outputBuffer.numberOfChannels; channel++) {
var inputData = inputBuffer.getChannelData(channel);
var outputData = outputBuffer.getChannelData(channel);
for (var sample = 0; sample < inputBuffer.length; sample++) {
outputData[sample] = inputData[sample] % moduAmount;
}
}
}
var moduLow = 0.35;
var moduHigh = 2;
var moduAmount = moduLow;
function turnModulusOnOff() {
if (moduAmount == moduLow)
moduAmount = moduHigh;
else
moduAmount = moduLow;
}
var gain = audioCtx.createGain();
gain.gain.value = 0.05;
gain.connect(audioCtx.destination);
modulus.connect(gain)
osc.connect(modulus);
osc.connect(gain);
document.body.addEventListener("keydown", newTone);
html,
body {
height: 100%;
width: 100%;
font-family: arial;
}
<button onclick="newTone()">Next Tone</button>
<button onclick="turnModulusOnOff()">Mute/Unmute Modulus</button>
<br>
To test, click anywhere and then press any key to change the tone. Notice when Modulus is muted how responsive the tone change is. Notice the lag between the sine frequency change and the modulus effect.
<br>
<b>For Extra Fun: Go crazy on your keyboard</b>
I've been looking at the information here:
Is there a way get something like decibel levels from an audio file and transform that information into a json array?
But when I try to run the JSBin here:
<!DOCTYPE html>
<html>
<head>
<meta charset=utf-8 />
<title>Get Audio Data</title>
<link rel="stylesheet" type="text/css" href="index.css">
<script type="text/javascript" src="music.js"></script>
</head>
<body>
<div id=meter></div>
</body>
</html>
#meter {
width: 500px;
height: 15px;
margin: 2px 0;
background: green;
-webkit-transition;
width .05s;
}
function() {
var ctx = new webkitAudioContext(),
url = 'test.mp3',
audio = new Audio(url),
processor = ctx.createJavaScriptNode(2048, 1, 1),
meter = document.getElementById('meter'),
source;
audio.addEventListener('canplaythrough', function() {
source = ctx.createMediaElementSource(audio);
source.connect(processor);
source.connect(ctx.destination);
processor.connect(ctx.destination);
audio.play();
}, false);
processor.onaudioprocess = function(evt) {
var input = evt.inputBuffer.getChannelData(0),
len = input.length,
total = i = 0,
rms;
while(i<len)
total += Math.abs(input[i++]);
rms = Math.sqrt(total/len);
meter.style.width = (rms*100) + '%';
};
};
I'm not really sure what it's doing. Can anyone give me more info?
Your code does generate RMS output once you replace defunct API calls
old webkitAudioContext
new
try {
window.AudioContext = window.AudioContext ||
window.webkitAudioContext ||
window.mozAudioContext ||
window.oAudioContext ||
window.msAudioContext;
ctx = new AudioContext();
console.log("cool audio context established ... ctx ");
} catch (e) {
alert("Web Audio API is not supported by this browser\n ... http://caniuse.com/#feat=audio-api");
}
and
old createJavaScriptNode
new createScriptProcessor