Playing a simple sound with web audio api - javascript

I've been trying to follow the steps in some tutorials for playback of a simple, encoded local wav or mp3 file with the web Audio API using a button. My code is the following (testAudioAPI.js):
window.AudioContext = window.AudioContext || window.webkitAudioContext;
var context = new AudioContext();
var myBuffer;
clickme = document.getElementById('clickme');
clickme.addEventListener('click',clickHandler);
var request = new XMLHttpRequest();
request.open('GET', 'WoodeBlock_SMan_B.wav', true);
request.responseType = 'arraybuffer';
// Decode asynchronously
request.onload = function() {
context.decodeAudioData(request.response, function(theBuffer) {
myBuffer = theBuffer;
}, onError);
}
request.send();
function playSound(buffer) {
var source = context.createBufferSource(), g = context.createGain();
source.buffer = buffer;
source.start(0);
g.gain.value = 0.5;
source.connect(g);
g.connect(context.destination);
}
function clickHandler(e) {
playSound(myBuffer);
}
And the HTML file would look like this:
<!doctype html>
<html>
<body>
<button id="clickme">Play</button>
<script src='testAudioAPI.js'></script>
</body>
</html>
However, no sound is achieved whatsoever. I've tried several snippets but I still can't figure it out. When I try to generate a sound by synthesizing it by creating an oscillator node, I do get sound, but not with buffers from local files. What would be the problem here? Thank you all.

minimalistic approach to modern ES6.
new AudioContext();
context.createBufferSource();
source.buffer = audioBuffer;
audioBuffer requests ArrayBuffer data by fetch, and then decodeAudioData decodes to AudioBuffer.
source.start()
<button id="start">playSound</button>
const audioPlay = async url => {
const context = new AudioContext();
const source = context.createBufferSource();
const audioBuffer = await fetch(url)
.then(res => res.arrayBuffer())
.then(ArrayBuffer => context.decodeAudioData(ArrayBuffer));
source.buffer = audioBuffer;
source.connect(context.destination);
source.start();
};
document.querySelector('#start').onclick = () => audioPlay('music/music.mp3');
stop play: source.stop();
The Web Audio API cannot be played automatically, you need to be triggered by an event.
Creating multiple AudioContext objects will cause an error, you should log out and then create them.
Failed to construct 'AudioContext': number of hardware contexts reached maximum
const audioPlay = (() => {
let context = null;
return async url => {
if (context) context.close();
context = new AudioContext();
const source = context.createBufferSource();
source.buffer = await fetch(url)
.then(res => res.arrayBuffer())
.then(arrayBuffer => context.decodeAudioData(arrayBuffer));
source.connect(context.destination);
source.start();
};
})();
document.querySelector('#start').onclick = () => audioPlay('music/music.mp3');

Local files, huh? Are you just grabbing them from the filesystem (e.g. "file://"), or do you have a local web server running?
From what I recall, serving directly from the filesystem violates CORS in most (all?) browsers – which will prevent your AJAX request from succeeding.
Maybe try serving the page with python -m SimpleHTTPServer or similar, and then access it at http://localhost:8000.
From what I can tell, all of your code looks fine.

You have a call to onError which isn't defined.
Apart from that it should be working, but not with Chrome because Chrome blocks all XMLHttpRequest to local files. But with Firefox for example it should work once you define (or get rid of) your call to onError.

You can create a URL reference to play a local file with two lines of code:
const sound = new URL('../assets/sound.mp3', import.meta.url)
new Audio(sound.href).play()
The href is only needed to satisfy Typescript, otherwise you can omit that suffix.

Related

Audio plays in all browsers but Safari desktop [duplicate]

I've got an angular 5 application where I've set the click handler of a button to download an audio file and play it. I'm using this code to do so:
onPreviewPressed(media: Media): void {
const url = ".....";
this.httpClient.get(url, {responseType: 'blob'}).subscribe(x => {
const fileReader = new FileReader();
fileReader.onloadend = () => {
const context = new ((<any>window).AudioContext || (<any>window).webkitAudioContext)();
const source = context.createBufferSource();
context.decodeAudioData(fileReader.result, buffer => {
source.buffer = buffer;
source.connect(context.destination);
source.start(0);
}, y => {
console.info("Error: " + y);
});
};
fileReader.readAsArrayBuffer(x);
});
}
If I go to the page in Chrome and press the button the audio starts right up. If I do it in Safari nothing happens. I know Safari locked things down but this is in response to a button click, it's not an auto-play.
The audio is sent back from the server via a PHP script, and it's sending headers like this, in case it matters:
header("Content-Type: audio/mpeg");
header('Content-Transfer-Encoding: binary');
header('Content-Length: ' . filesize($_GET['file']));
header('Cache-Control: no-cache');
No, it is not "in response to a button click".
In response to this click event, you are starting an asynchronous task. By the time you call source.start(0), your event is long dead (or at least not anymore an "trusted user gesture". So they will indeed block this call.
To circumvent this, you could simply mark your context as allowed with silence. Then, when the data will be available, you'll be able to start it with no restriction:
function markContextAsAllowed(context) {
const gain = context.createGain();
gain.gain.value = 0; // silence
const osc = context.createOscillator();
osc.connect(gain);
gain.connect(context.destination);
osc.onended = e => gain.disconnect();
osc.start(0);
osc.stop(0.01);
}
onPreviewPressed(media: Media): void {
const url = ".....";
// declare in the event handler
const context = new(window.AudioContext || window.webkitAudioContext)();
const source = context.createBufferSource();
// allow context synchronously
markContextAsAllowed(context);
this.httpClient.get(url, {
responseType: 'blob'
}).subscribe(x => {
const fileReader = new FileReader();
fileReader.onloadend = () => {
context.decodeAudioData(fileReader.result, buffer => {
source.buffer = buffer;
source.connect(context.destination);
source.start(0);
}, y => {
console.info("Error: " + y);
});
};
fileReader.readAsArrayBuffer(x);
});
}
As a fiddle since Safari doesn't like over-protected StackSnippets®
Also, my angular knowledge is very limited, but if httpClient.get does support {responseType: 'arraybuffer'} option, you could get rid of this FileReader and avoid populating twice the memory with the same data.
Finally, if you are going to play this audio more than once, consider prefetching and pre-decoding it, you'll then be able to avoid the whole asynchronous mess.

Safari 14 (macOS). An аudio is truncated at the beginning when repeating [duplicate]

So it turns out, when you use javascript to trigger audio. In the latest version of safari, if you use obj.play() twice, the second time, the part of the audio is cut off (on mac at least). This problem does not occur in Chrome. Only on Safari.
Is anyone aware of a work around for this?
Play
<audio id="t" src="https://biblicaltext.com/audio/%e1%bc%a4.mp3"></audio>
<script>
function playWord(word) {
a = document.getElementById('t');
//a.currentTime = 0
a.play();
//a.src = a.src
}
</script>
https://jsfiddle.net/8fbt7rgc/1/
Redownloading the file using a.src = a.src works, but it is not ideal.
That sounds like a bug they should be made aware of.
For the time being, if you have access to the file you play in a same-origin way, you can use the Web Audio API and its AudioBufferSourceNode interface to play your media with high precision and less latency than through HTMLMediaElements:
(async () => {
const ctx = new (window.AudioContext || window.webkitAudioContext)();
// Using a different file because biblicaltext.com
// doesn't allow cross-origin requests
const data_buf = await fetch("https://dl.dropboxusercontent.com/s/agepbh2agnduknz/camera.mp3")
.then( resp => resp.arrayBuffer() );
const audio_buf = await ctx.decodeAudioData(data_buf);
document.querySelector("a").onclick = (evt) => {
evt.preventDefault();
const source = ctx.createBufferSource();
source.buffer = audio_buf;
source.connect(ctx.destination);
source.start(0);
}
})();
<!-- Promising decodeAudioData for old Safari https://github.com/mohayonao/promise-decode-audio-data/ [MIT] -->
<script src="https://cdn.rawgit.com/mohayonao/promise-decode-audio-data/eb4b1322/build/promise-decode-audio-data.min.js"></script>
Play

JavaScript: Use MediaRecorder to record streams from <video> but failed

I'm trying to record parts of the video from a tag, save it for later use. And I found this article: Recording a media element, which described a method by first calling stream = video.captureStream(), then use new MediaRecord(stream) to get a recorder.
I've tested on some demos, the MediaRecorder works fine if stream is from user's device (such as microphone). However, when it comes to media element, my FireFox browser throws an exception: MediaRecorder.start: The MediaStream's isolation properties disallow access from MediaRecorder.
So any idea on how to deal with it?
Browser: Firefox
The page (including the js file) is stored at local.
The src attribute of <video> tag could either be a file from local storage or a url from Internet.
Code snippets:
let chunks = [];
let getCaptureStream = function () {
let stream;
const fps = 0;
if (video.captureStream) {
console.log("use captureStream");
stream = video.captureStream(fps);
} else if (video.mozCaptureStream) {
console.log("use mozCaptureStream");
stream = video.mozCaptureStream(fps);
} else {
console.error('Stream capture is not supported');
stream = null;
}
return stream;
}
video.addEventListener('play', () => {
let stream = getCaptureStream();
const mediaRecorder = new MediaRecorder(stream);
mediaRecorder.onstop = function() {
const newVideo = document.createElement('video');
newVideo.setAttribute('controls', '');
newVideo.controls = true;
const blob = new Blob(chunks);
chunks = [];
const videoURL = window.URL.createObjectURL(blob, { 'type' : 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"' });
newVideo.src = videoURL;
document.body.appendChild(video);
}
mediaRecorder.ondataavailable = function(e) {
chunks.push(e.data);
}
stopButton.onclick = function() {
mediaRecorder.stop()
}
mediaRecorder.start(); // This is the line triggers exception.
});
I found the solution myself.
When I turned to Chrome, it shows that a CORS issue limits me from even playing original video. So I guess it's because the secure strategy that preventing MediaRecorder from accessing MediaStreams. Therefore, I deployed the local files to a local server with instruction on this page.
After that, the MediaRecorder started working. Hope this will help someone in need.
But still, the official document doesn't seem to mention much about isolation properties of media elements. So any idea or further explanation is welcomed.

AnalyserNode.getFloatFrequencyData always returns -Infinity

Alright, so I'm trying to determine the intensity (in dB) on samples of an audio file which is recorded by the user's browser.
I have been able to record it and play it through an HTML element.
But when I try to use this element as a source and connect it to an AnalyserNode, AnalyserNode.getFloatFrequencyData always returns an array full of -Infinity, getByteFrequencyData always returns zeroes, getByteTimeDomainData is full of 128.
Here's my code:
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var source;
var analyser = audioCtx.createAnalyser();
var bufferLength = analyser.frequencyBinCount;
var data = new Float32Array(bufferLength);
mediaRecorder.onstop = function(e) {
var blob = new Blob(chunks, { 'type' : 'audio/ogg; codecs=opus' });
chunks = [];
var audioURL = window.URL.createObjectURL(blob);
// audio is an HTML audio element
audio.src = audioURL;
audio.addEventListener("canplaythrough", function() {
source = audioCtx.createMediaElementSource(audio);
source.connect(analyser);
analyser.connect(audioCtx.destination);
analyser.getFloatFrequencyData(data);
console.log(data);
});
}
Any idea why the AnalyserNode behaves like the source is empty/mute? I also tried to put the stream as source while recording, with the same result.
I ran into the same issue, thanks to some of your code snippets, I made it work on my end (the code bellow is typescript and will not work in the browser at the moment of writing):
audioCtx.decodeAudioData(this.result as ArrayBuffer).then(function (buffer: AudioBuffer) {
soundSource = audioCtx.createBufferSource();
soundSource.buffer = buffer;
//soundSource.connect(audioCtx.destination); //I do not need to play the sound
soundSource.connect(analyser);
soundSource.start(0);
setInterval(() => {
calc(); //In here, I will get the analyzed data with analyser.getFloatFrequencyData
}, 300); //This can be changed to 0.
// The interval helps with making sure the buffer has the data
Some explanation (I'm still a beginner when it comes to the Web Audio API, so my explanation might be wrong or incomplete):
An analyzer needs to be able to analyze a specific part of your sound file. In this case I create a AudioBufferSoundNode that contains the buffer that I got from decoding the audio data. I feed the buffer to the source, which eventually will be able to be copied inside the Analyzer. However, without the interval callback, the buffer never seems to be ready and the analysed data contains -Inifinity (which I assume is the absence of any sound, as it has nothing to read) at every index of the array. Which is why the interval is there. It analyses the data every 300ms.
Hope this helps someone!
You need to fetch the audio file and decode the audio buffer.
The url to the audio source must also be on the same domain or have have the correct CORS headers as well (as mentioned by Anthony).
Note: Replace <FILE-URI> with the path to your file in the example below.
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();
var source;
var analyser = audioCtx.createAnalyser();
var button = document.querySelector('button');
var freqs;
var times;
button.addEventListener('click', (e) => {
fetch("<FILE-URI>", {
headers: new Headers({
"Content-Type" : "audio/mpeg"
})
}).then(function(response){
return response.arrayBuffer()
}).then((ab) => {
audioCtx.decodeAudioData(ab, (buffer) => {
source = audioCtx.createBufferSource();
source.connect(audioCtx.destination)
source.connect(analyser);
source.buffer = buffer;
source.start(0);
viewBufferData();
});
});
});
// Watch the changes in the audio buffer
function viewBufferData() {
setInterval(function(){
freqs = new Uint8Array(analyser.frequencyBinCount);
times = new Uint8Array(analyser.frequencyBinCount);
analyser.smoothingTimeConstant = 0.8;
analyser.fftSize = 2048;
analyser.getByteFrequencyData(freqs);
analyser.getByteTimeDomainData(times);
console.log(freqs)
console.log(times)
}, 1000)
}
If the source file from a different domain? That would fail in createMediaElementSource.

Play sound with delay (100ms)?

What is the best way to play sound with delay 50ms or 100ms?
Here is something, what i tried:
var beat = new Audio('/sound/BEAT.wav');
var time = 300;
playbeats();
function playbeats(){
beat.cloneNode().play();
setTimeout(playbeats, time);
}
This is working correctly but my goal is to play BEAT.wav after every 100ms. When I change "time" variable to 100, then it is so "laggy".
721ms is my BEAT.wav (that's why im using cloneNode())
What is alternatives to solve this?
You can use setInterval(), the arguments are the same.
setInterval(function() {
playbeats();
}, 100);
and your function playbeats function should be.
function playbeats(){
var tempBeat=beat.cloneNode();
tempBeat.play();
}
your whole program should be like this.
var beat = new Audio('/sound/BEAT.wav');
setInterval(function() {
playbeats();
}, 100);
function playbeats(){
var tempBeat=beat.cloneNode();
tempBeat.play();
}
You can use the Web Audio API but the code will be a bit different.If you want the Web Audio API's timing and loop capabillities you will need to load the file into a buffer first. It also requires that your code is run on a server. Here is an example:
var audioContext = new AudioContext();
var audioBuffer;
var getSound = new XMLHttpRequest();
getSound.open("get", "sound/BEAT.wav", true);
getSound.responseType = "arraybuffer";
getSound.onload = function() {
audioContext.decodeAudioData(getSound.response, function(buffer) {
audioBuffer = buffer;
});
};
getSound.send();
function playback() {
var playSound = audioContext.createBufferSource();
playSound.buffer = audioBuffer;
playSound.loop = true;
playSound.connect(audioContext.destination);
playSound.start(audioContext.currentTime, 0, 0.3);
}
window.addEventListener("mousedown", playback);
I would also recommend using the Web Audio API. From there, you can simply loop a buffer source node every 100ms or 50ms or whatever time you want.
To do this, as stated in other responses, you'll need to use an XMLHttpRequest to load the sound file via a server
// set up the Web Audio context
var audioCtx = new AudioContext();
// create a new buffer
// 2 channels, 4410 samples (100 ms at 44100 samples/sec), 44100 samples per sec
var buffer = audioCtx.createBuffer(2, 4410, 44100);
// load the sound file via an XMLHttpRequest from a server
var request = new XMLHttpRequest();
request.open('GET', '/sound/BEAT.wav', true);
request.responseType = 'arraybuffer';
request.onload = function () {
var audioData = request.response;
audioCtx.decodeAudioData(audioData, function (newBuffer) {
buffer = newBuffer;
});
}
request.send();
Now you can make a Buffer Source Node to loop the playback
// create the buffer source
var bufferSource = audioCtx.createBufferSource();
// set the buffer we want to use
bufferSource.buffer = buffer;
// set the buffer source node to loop
bufferSource.loop = true;
// specify the loop points in seconds (0.1s = 100ms)
// this is a little redundant since we already set our buffer to be 100ms
// so by default it would loop when the buffer comes to an end (at 100ms)
bufferSource.loopStart = 0;
bufferSource.loopEnd = 0.1;
// connect the buffer source to the Web Audio sound output
bufferSource.connect(audioCtx.destination);
// play!
bufferSource.start();
Note that if you stop the playback via bufferSource.stop(), you will not be able to start it again. You can only call start() once, so you'll need to create a new source node if you want to start playback again.
Note that because of the way the sound file is loaded via an XMLHttpRequest, if you try to test this on your machine without running a server, you'll get a cross-reference request error on most browsers. So the simplest way to get around this if you want to test this on your machine is to run a Python SimpleHTTPServer

Categories