I would like to create a decibel meter for the audio that is playing in a video element. The video element is playing a WebRTC stream.
At the moment WebRTC streams cannot be passed into a Web Audio Analyzer. (Although this might change soon … ) (see Web Audio API analyser node getByteFrequencyData returning blank array)
Is there currently another way to get decibel information from a remote mediastream?
Chrome 50 was released: As of the 13th of April 2016 using an Analyser Node with a MediaStreamAudioSourceNode works fine to get audio levels. The resulting audioLevels value can be animated or simply passed into a html meter element.
var _mediaStream = SOME_LOCAL_OR_RTP_MEDIASTREAM;
var _audioContext = new AudioContext();
var _audioAnalyser = [];
var _freqs = [];
var audioLevels = [0];
var _audioSource = _audioContext.createMediaStreamSource(_mediaStream);
var _audioGain1 = _audioContext.createGain();
var _audioChannelSplitter = _audioContext.createChannelSplitter(_audioSource.channelCount);
_audioSource.connect(_audioGain1);
_audioGain1.connect(_audioChannelSplitter);
_audioGain1.connect(_audioContext.destination);
for (let i = 0; i < _audioSource.channelCount; i++) {
_audioAnalyser[i] = _audioContext.createAnalyser();
_audioAnalyser[i].minDecibels = -100;
_audioAnalyser[i].maxDecibels = 0;
_audioAnalyser[i].smoothingTimeConstant = 0.8;
_audioAnalyser[i].fftSize = 32;
_freqs[i] = new Uint8Array(_audioAnalyser[i].frequencyBinCount);
_audioChannelSplitter.connect(_audioAnalyser[i], i, 0);
}
function calculateAudioLevels() {
setTimeout(() => {
for (let channelI = 0; channelI < _audioAnalyser.length; channelI++) {
_audioAnalyser[channelI].getByteFrequencyData(_freqs[channelI]);
let value = 0;
for (let freqBinI = 0; freqBinI < _audioAnalyser[channelI].frequencyBinCount; freqBinI++) {
value = Math.max(value, _freqs[channelI][freqBinI]);
}
audioLevels[channelI] = value / 256;
}
requestAnimationFrame(calculateAudioLevels.bind(this));
}, 1000 / 15); // Max 15fps — not more needed
}
This is a good example:
https://webrtc.github.io/samples/src/content/getusermedia/volume/
And this is the source code:
https://github.com/webrtc/samples/tree/gh-pages/src/content/getusermedia/volume
And this is a sample:
function recordAudio() {
try {
window.AudioContext = window.AudioContext || window.webkitAudioContext;
window.audioContext = new AudioContext();
const instantMeter = document.querySelector('#sound-meter');
const constraints = {'video': false, 'audio': true};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
window.stream = stream;
const soundMeter = window.soundMeter = new SoundMeter(window.audioContext);
soundMeter.connectToSource(stream, function(e) {
if (e) {
alert(e);
return;
}
setInterval(() => {
instantMeter.value = soundMeter.instant.toFixed(2);
}, 200);
});
$('#sound-meter').show();
$('#audio-icon').hide()
} catch(error) {
console.error('Error recording audio.', error);
}
}
Related
I am trying to create and play a playlist manifest using HLS. I am looping over all the segment files to add their names to the file/provide their url, but when I run my function, I get the error below, which means it's not properly receiving the urls:
[error] > 0 while loading data:application/x-mpegURL;base64,undefined
Here's my code:
segment_files = ["https://gib.s3.amazonaws.com/segment/first.ts", "https://gib.s3.amazonaws.com/segment/2nd.ts", "https://gib.s3.amazonaws.com/segment/first.ts"
function create_manifest() {
if (hls == undefined) {
player = document.createElement("video");
document.body.appendChild(player);
player.pause();
player.src = "";
for (let ii = 0; ii < segment_files.length; i++) {
var segment = segment_files[ii];
}
var manifestfile = btoa(
`#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXT-X-TARGETDURATION:11\n#EXTINF:10.000,\n${segment}\n#EXT-X-ENDLIST`
);
}
if (Hls.isSupported()) {
hls = new Hls({
debug: true,
enableWorker: true,
lowLatencyMode: true,
});
hls.attachMedia(player);
hls.loadSource("data:application/x-mpegURL;base64," + manifestfile);
player.muted = true;
player.play();
}
}
I would like to play multiple audio files one by one. I have read the same question, but I have found the links to the original answer not working and question unanswered.
Original answer
<audio id="mySound" controls><source src="http://www.hvalur.org/audio/uploaded_files/ds_umkomuleysi_0_0.mp3" type="audio/mpeg"></audio>
<audio id="mySound1" ><source src="http://www.hvalur.org/audio/uploaded_files/ds_ljosmyndavel_0_0.mp3" type="audio/mpeg"></audio>
I want the first sound started by the user action (pressing the play button) and then the following sound(s) playing one by one.
The usage of this example will be practising of understanding of the telephone numbers read by native speaker.
I would do something like that:
var mySound = document.getElementById("mySound");
var mySound1 = document.getElementById("mySound1");
var mySound2 = document.getElementById("mySound2");
var mySound3 = document.getElementById("mySound3");
var mySoundArray = [mySound, mySound1, mySound2, mySound3];
var mySoundArrayPos = 0;
var myButton = document.getElementById("myButton");
myButton.addEventListener("click", function(event) {
mySound.play();
mySoundArrayPos = 0;
startSoundTimer();
});
const startSoundTimer = () => {
var mySoundTimer = setInterval(() => {
if (mySoundArray[mySoundArrayPos].currentTime >= mySoundArray[mySoundArrayPos].duration) {
if (mySoundArrayPos < mySoundArray.length -1) {
mySoundArrayPos += 1;
mySoundArray[mySoundArrayPos].play();
} else {
clearInterval(mySoundTimer);
}
}
}, 10);
};
I don't know if the 10ms are a bit too fast, but I think, it's legit.
Javascript
=============== `playaudio(i) {
var thisme = this;
var audio = new Audio(thisme.model.audioFiles[i].audioUrl);
audio.play();
audio.addEventListener("ended", function () {
i = i + 1;
console.log(i);
if (i < thisme.model.audioFiles.length) {
audio.src = thisme.model.audioFiles[i].audioUrl;
audio.play();
}
}, false);
}
}`
HTML
====`<span class='chunkAudio' (click)="playaudio(0)">
<img class='playbtn' src="{{model.playerImageUrl}}" />
</span>`
Is there an easy way to repeat this code to create a volume controller that works for 4 different players simultaneously? I've managed to get it to work for one player at any given point by changing the myAudio(x) value but any more than that seems to confuse it.
// Global variables and controls
var vid = document.getElementById("myVideo");
var volumeControl = document.getElementById("vol-control");
var setVolume = function(){
vid.volume = this.value / 100;
};
var audio = document.getElementById("myAudio(1)");
var volumeControl2 = document.getElementById("vol-control2");
var setVolume2 = function(){
audio.volume = this.value / 100;
};
//event listeners
volumeControl.addEventListener('change',setVolume);
volumeControl.addEventListener('input',setVolume);
volumeControl2.addEventListener('change',setVolume2);
volumeControl2.addEventListener('input',setVolume2);
Delegate
HTML
<div id="audvid">
<video.../>
<audeo.../>
</div>
event listeners
document.getElementById("audvid").addEventListener("change", setVolume)
document.getElementById("audvid").addEventListener("input", setVolume)
change whatever is accessed
const setVolume = function(e) {
const tgt = e.target;
if (["AUDIO", "VIDEO"].includes(tgt.tagName)) {
tgt.volume = tgt.value / 100;
}
};
change any audio/video
const setVolume = function(e) {
const tgt = e.target;
[...document.querySelectorAll("AUDIO", "VIDEO")].forEach(ele => {
ele.volume = tgt.value / 100;
})
};
I am trying to get an "image URL field" convert to a base64 blob data, however the updated field console log data is showing as ZoneAwarePromise {__zone_symbol__state: null, __zone_symbol__value: Array(0)}
this.http.get(this.api).subscribe(data => {
this.mainContact = data;
for (var a = 0; a < this.mainContact.length; a++) {
for (var j = 0; j < this.mainContact[a].acf.project_gallery.length; j++) {
var mainUrl = 'https://cdnwebapp.azureedge.net'+this.mainContact[a].acf.project_gallery[j].url;
//Below updates the all field value to the converted blob
this.mainContact[a].acf.project_gallery[j].url = this.convertToDataURLviaCanvas(mainUrl, "image/jpeg", 1)
.then(base64Img => {
return base64Img;
// console.log(base64Img); *As Expected blob*
})
//console.log(this.mainContact[a].acf.project_gallery[j].url) * The update field data console log is showing as ZoneAwarePromise
}
}
});
convertToDataURLviaCanvas(url, outputFormat, osize){
return new Promise( (resolve, reject) => {
let img = new Image();
img.crossOrigin = 'Anonymous';
img.onload = function(){
let canvas = <HTMLCanvasElement> document.createElement('CANVAS'),
ctx = canvas.getContext('2d'),
dataURL;
canvas.height = 959;
canvas.width = 1365;
ctx.drawImage(img, 0, 0);
dataURL = canvas.toDataURL(outputFormat, osize);
//callback(dataURL);
canvas = null;
resolve(dataURL);
};
img.src = url;
});
}
I think you have to assign in on the .then.
Try:
this.convertToDataURLviaCanvas(mainUrl, "image/jpeg", 1)
.then(base64Img => {
this.mainContact[a].acf.project_gallery[j].url = base64img
console.log(base64Img);
console.log(this.mainContact[a].acf.project_gallery[j].url);
})
In order to try and get around the odd issue in having with CORS (here) I am attempting to reload any images loaded via canvas.loadFromJSON()
But, I am experiencing weird issues. Sometimes only one image is replaced, other times I get duplicates of one image.
Here is my code:
canvas.loadFromJSON(<?php echo json_encode($objects); ?>, function() {
var objArray = canvas.getObjects();
for (var i = 0; i < objArray.length; i++) {
canvas.setActiveObject(objArray[i]);
var activeObject = canvas.getActiveObject();
if(activeObject.type === 'image') {
fabric.util.loadImage(activeObject.src, function(img) {
var object = new fabric.Image(img);
object.hasControls = true;
object.lockUniScaling = true;
object.scaleX = activeObject.scaleX;
object.scaleY = activeObject.scaleY;
object.originX = activeObject.originX;
object.originY = activeObject.originY;
object.centeredRotation = true;
object.centeredScaling = true;
canvas.add(object);
}, null, {crossOrigin: 'Anonymous'});
canvas.remove(activeObject);
}
activeObject.setCoords();
}
canvas.deactivateAll();
canvas.renderAll();
canvas.calcOffset();
});
Any ideas why I'm getting these weird issues?
First glance at your code I don't see anything wrong... But I'm also thinking the code might be a bit inefficient? Is there a need to create a new image instance?
I believe you should be able to just set the crossOrigin property on the image object.
This code is untested, but I'd try something like this:
canvas.loadFromJSON(<?php echo json_encode($objects); ?>, function() {
var objArray = canvas.getObjects();
for (var i = 0; i < objArray.length; i++) {
canvas.setActiveObject(objArray[i]);
var activeObject = canvas.getActiveObject();
if(activeObject.type === 'image') {
activeObject.crossOrigin = 'Anonymous';
}
}
canvas.deactivateAll();
canvas.renderAll();
canvas.calcOffset();
});
I had the same problem and overcome it downloading again the image then reassign it to object._element once each fabric object was created using loadFromJSON.
export const getImage = url => {
return new Promise((resolve, reject) => {
let img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.setAttribute('crossOrigin', 'anonymous');
img.src = url;
});
}
canvas.loadFromJSON(json, canvas.renderAll.bind(canvas), async (o, object) => {
if (object.type === "image") {
let imagecore = await getImage(object.src);
object._element = imagecore;
}
});