Is it possible to achieve low latency audio playback using HTML5? I'm currently using AudioContext API. However, I am getting latency of about 4 seconds. Which is way to much for my use case.
if (!window.audioContextInstance) {
window.audioContextInstance = new webkitAudioContext();
}
var context = window.audioContextInstance;
context.sampleRate = 48000;
var buffers = [];
var src = new Float32Array();
var srcIdx = 0;
var bufferSize = 2048;
var sourceNode = context.createScriptProcessor(bufferSize, 1, 2);
sourceNode.onaudioprocess = function(evt) {
var c0 = evt.outputBuffer.getChannelData(0);
var c1 = evt.outputBuffer.getChannelData(1);
var sample = 0;
while(sample < bufferSize) {
if (srcIdx >= src.length) {
if (!buffers.length) {
console.log("Warning: Audio Buffer Underflow")
return;
}
src = buffers.shift();
srcIdx = 0;
}
while(sample < bufferSize && srcIdx < src.length) {
c0[sample] = src[srcIdx++];
c1[sample] = src[srcIdx++];
sample++;
}
}
};
scope.$on('frame', function (event, frame) {
while (buffers.length > 1) {
buffers.shift();
}
buffers.push(new Float32Array(frame.data));
if (buffers.length > 0) {
sourceNode.connect(context.destination);
}
});
}
You may be interested in riffwave.js which appears to have much lower than 4s latency.
Related
I would like my audio2 file to play when audio1.currentTime is 3 seconds, but I'm not able to make it work. I'm a newbie in javascript, what am I missing?. This is my current javascript code:
function initAudioPlayer(){
var audio1, audio2, ext, agent;
ext = ".mp3";
agent = navigator.userAgent.toLocaleLowerCase();
if(agent.indexOf('firefox') != -1 || agent.indexOf('opera') != -1) { ext = ".ogg";}
//Audio Objects: audio1 and audio2
audio1 = new Audio();
audio1.src = "folder/Audio1"+ext;
audio1.loop = false;
audio1.play();
audio2 = new Audio();
audio2.src = "folder/Audio2"+ext;
audio2.loop = false;
audio2.play();
//Function that reproduces the second audio file at second 3 of the first audio file
function audio2(){
if(audio1.currentTime == 3) {audio2.play();}
};
}
window.addEventListener("load", initAudioPlayer);
You Must use Audio Api and fetch buffers of your files.
Then you must plus each byte and copy to another new buffer.
this code can help you:
let idnex=0;
samples.forEach(buufer => {
if (index === 0) {
tempBuf = buufer;
} else {
tempBuf = this.appendBuffer(tempBuf, buufer);
}
index++;
});
and by thi method you can append two buffer:
private appendBuffer(buffer1, buffer2) {
const numberOfChannels = Math.min(buffer1.numberOfChannels, buffer2.numberOfChannels);
const tmp = this.audioContextService.createBuffer(Math.max(buffer1.numberOfChannels, buffer2.numberOfChannels),
Math.max(buffer1.length, buffer2.length), buffer1.sampleRate);
for (let i = 0; i < numberOfChannels; i++) {
const channel = tmp.getChannelData(i);
let finallArray = [];
let d = [];
const chanelTemp = buffer1.getChannelData(i);
if (buffer2.numberOfChannels <= i) {
finallArray = chanelTemp;
} else {
const c = buffer2.getChannelData(i);
if (chanelTemp.length > c.length) {
finallArray = chanelTemp;
d = c;
} else {
finallArray = c;
d = chanelTemp;
}
for (let j = 0; j < d.length; j++) {
finallArray[j] += d[j] / 2;
}
}
channel.set(finallArray, i);
}
you can see my demo here
Also You Can See this Answer
If you truly want this to be accurate in time, you can't use Audio() - that's the HTML5 <audio> element, which is not sample-accurate. Javascript is also not accurate enough in event delivery to use a callback to do this (to be fair, neither are most general-purpose native OS APIs). You need to schedule the playback in advance, which is where Web Audio (https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API) comes in. You need to load both samples, decode them into AudioBuffers, and then schedule playing back each of them with an AudioBufferSourceNode.
I have an html5 video element and I need to apply different processing realtime on the video's output audio. On desktop I made it work with the WebAudio API. The Api is seemingly present on iOS also. I am able to inspect the created objects, but it doesn't modify the video's output signal.
Here's my example code:
$(function () {
window.AudioContext = window.AudioContext||window.webkitAudioContext;
var audioContext = new AudioContext();
var bufferSize = 1024;
var selectedChannel = 0;
var effect = (function() {
var node = audioContext.createScriptProcessor(bufferSize, 2, 2);
node.addEventListener('audioprocess', function(e) {
var input = e.inputBuffer.getChannelData(selectedChannel);
var outputL = e.outputBuffer.getChannelData(0);
var outputR = e.outputBuffer.getChannelData(1);
for (var i = 0; i < bufferSize; i++) {
outputL[i] = selectedChannel==0? input[i] : 0.0;
outputR[i] = selectedChannel==1? input[i] : 0.0;
}
});
return node;
})();
var streamAttached = false;
function attachStream(video) {
if (streamAttached) {
return;
}
var source = audioContext.createMediaElementSource(video);
source.connect(effect);
effect.connect(audioContext.destination);
streamAttached = true;
}
function iOS_video_touch_start() {
var video = $('#vid')[0];
video.play();
attachStream(video);
}
var needtouch = false;
$('#vid').on('play', function () {
attachStream(this);
}).on('loadedmetadata', function () {
this.play();
this.volume=1.0;
if (this && this.paused) {
if (needtouch == false) {
needtouch = true;
this.addEventListener("touchstart", iOS_video_touch_start, true);
}
}
});
window.panToRight = function(){
selectedChannel = 1;
};
window.panToLeft = function(){
selectedChannel = 0;
};
});
You can also check it on CP:
http://codepen.io/anon/pen/pgeJQG
With the buttons you are able to toggle between the left and the right channels. On desktop browsers (Chrome, Firefox, Safari tested) it works fine.
I have also tried the older createJavaScriptNode() instead of createScriptProcessor(). I have also tried it with an alternative effect chain, which was looking like this:
var audioContext = new (window.AudioContext||window.webkitAudioContext)();
audioContext.createGain = audioContext.createGain||audioContext.createGainNode;
var gainL = audioContext.createGain();
var gainR = audioContext.createGain();
gainL.gain.value = 1;
gainR.gain.value = 1;
var merger = audioContext.createChannelMerger(2);
var splitter = audioContext.createChannelSplitter(2);
//Connect to source
source = audioContext.createMediaElementSource(video);
//Connect the source to the splitter
source.connect(splitter, 0, 0);
//Connect splitter' outputs to each Gain Nodes
splitter.connect(gainL, 0);
splitter.connect(gainR, 1);
//Connect Left and Right Nodes to the Merger Node inputs
//Assuming stereo as initial status
gainL.connect(merger, 0, 0);
gainL.connect(merger, 0, 1);
//Connect Merger output to context destination
merger.connect(audioContext.destination, 0, 0);
As you probably noticed this code was using the built in nodes only. But no luck.
So my questions are: Is this even possible on mobile? If it is, than what am I missing? If it is not, than any possible workaround? Thanks
With Chrome on Android, MediaElementSource is not currently routed to WebAudio. This is a known issue and is planned to be fixed eventually.
var actions = function(){return {whichPattern:"pattern1"}};
var audio = new Audio();
var _play = function(){
var l = pattern[actions().whichPattern].instrument.length,
times_played =0;
var p = setInterval(function(){
var x = pattern[actions().whichPattern][times_played];
for(var i=0; i<x.length;i++){
var toBePlayed = pattern[actions().whichPattern][times_played][i],
url= "All Drums sounds/"+toBePlayed+".wav";
audio.src = url;
audio.play();
console.log(audio);
times_played++;
}
}, pattern_config[actions().whichPattern].delay);
if(times_played === l){
clearInterval(p);
};
var pp=document.getElementById("play_pause");
pp.style.background="url('graphics/pause.png') no-repeat";
pp.style.backgroundSize ="30px 28px"
audio.source='All Drums sounds/'+pattern['pattern1'][1][0]+'.wav';
audio.play();
};
var pattern_config = {
pattern1:{
WP_slotsCounter:0,
instrument:["Kick_02", "F_T_03", "Rude_cymbal_02"],
delay:10
},
};
var pattern={
pattern1: [], // [['asas', 'sf', 'asd'], ['svv','dgh','sdgh']]
}
The above code is very simple. The first function return an object ...
well the main motive of mine is like :
assuming that pattern.pattern1 = [['asas', 'sf', 'asd'], ['svv','dgh','sdgh']] so i what that the all the strings in the pattern['pattern1'][0] should play at same instant and then with a delay of patter_config[actions().whichPattern].delay that sound files with that names of pattern['pattern1'][1] should play together.
so to achieve this i made the above function _play()
well the problem is that it is not playing the music files and not giving any errors too so that i can identify where is that problem.
pattern["pattern1"] is an array of strings and does not have any 'instrument' property
maybe you were trying to use pattern_config["pattern1"]
var actions = function () {
return {
whichPattern: "pattern1"
}
};
var audio = new Audio();
var _play = function () {
var l = pattern_config[actions().whichPattern].instrument.length,
times_played = 0;
var p = setInterval(function () {
var x = pattern_config[actions().whichPattern][times_played];
for (var i = 0; i < x.length; i++) {
var toBePlayed = pattern_config[actions().whichPattern][times_played][i],
url = "All Drums sounds/" + toBePlayed + ".wav";
audio.src = url;
audio.play();
console.log(audio);
times_played++;
}
}, pattern_config[actions().whichPattern].delay);
if (times_played === l) {
clearInterval(p);
};
var pp = document.getElementById("play_pause");
pp.style.background = "url('graphics/pause.png') no-repeat";
pp.style.backgroundSize = "30px 28px"
audio.source = 'All Drums sounds/' + pattern['pattern1'][1][0] + '.wav';
audio.play();
};
var pattern_config = {
pattern1: {
WP_slotsCounter: 0,
instrument: ["Kick_02", "F_T_03", "Rude_cymbal_02"],
delay: 10
},
};
var pattern = {
pattern1: [['asas', 'sf', 'asd'], ['svv','dgh','sdgh']],
}
I have code that loads the variables and turns them into numbers, I can do math with these, and can display them on page, but i have functions, that worked before the loading was added, that use these variables to automatically add 1 to the variables. I have a loop that runs this automatic addition. however the loop does not seem to be working after the addition of the loading.
if(typeof(Storage)!=="undefined") {
var hotdogs = parseInt(localStorage.getItem('hotdogs'), 10);
var bread = parseInt(localStorage.getItem('bread'), 10);
var hotdog = parseInt(localStorage.getItem('hotdog'), 10);
var sauce = parseInt(localStorage.getItem('sauce'), 10);
var bakeries = parseInt(localStorage.getItem('bakeries'), 10);
var butchers = parseInt(localStorage.getItem('butchers'), 10);
var sauceries = parseInt(localStorage.getItem('sauceries'), 10);
}
else {
var hotdogs = 0;
var bread = 0;
var hotdog = 0;
var sauce = 0;
var bakeries = 0;
var butchers = 0;
var sauceries = 0;
}
function buyBakery(){
var bakeryCost = Math.floor(20 * Math.pow(1.05,bakeries));
if(hotdogs >= bakeryCost){
bakeries = bakeries + 1;
hotdogs = hotdogs - bakeryCost;
document.getElementById('bakeries').innerHTML = bakeries;
document.getElementById('hotdogs').innerHTML = hotdogs;
};
var nextCostBakery = Math.floor(20 * Math.pow(1.05,bakeries));
document.getElementById('bakerycost').innerHTML = nextCostBakery;
};
(function loop(timer) {
setTimeout(function() {
breadClick(bakeries);
hotdogClick(butchers);
sauceClick(sauceries);
customers(custom);
loop(1000);
}, timer)
})(1000)
For starters you don't appear to be calling the buyBakery() method within your loop.
Secondly, even though 'Storage' is defined the item you are looking for may not be on it.in that case it'll return null instead and parseInt() will convert into NaN(Not a Number)
also make sure the other functions you are calling within the loop are exist and not throwing any errors.use the web console for that.
you might wanna consider saving values back on localStorage (if you are not already doing that) so you can get it later. I've added function to do that on my example.
Try some thing like this.
function loadFromLS(item) {
var ret = 0;
if (typeof (Storage) === 'undefined') {
return ret;
}
if (null === (ret = localStorage.getItem(item))) {
ret = 0;
}
return parseInt(ret, 10);
}
function saveToLS(item, val) {
if (typeof (Storage) === 'undefined') {
return;
}
localStorage[item] = val;
}
var hotdogs = loadFromLS('hotdogs');
var bread = loadFromLS('bread');
var hotdog = loadFromLS('hotdog');
var sauce = loadFromLS('sauce');
var bakeries = loadFromLS('bakeries');
var butchers = loadFromLS('butchers');
var sauceries = loadFromLS('sauceries');
function buyBakery() {
var bakeryCost = Math.floor(20 * Math.pow(1.05, bakeries));
if (hotdogs <= bakeryCost) {
bakeries = bakeries + 1;
hotdogs = hotdogs - bakeryCost;
document.getElementById('bakeries') .innerHTML = bakeries;
document.getElementById('hotdogs').innerHTML = hotdogs;
};
var nextCostBakery = Math.floor(20 * Math.pow(1.05, bakeries));
document.getElementById('bakerycost') .innerHTML = nextCostBakery;
};
(function loop(timer) {
setTimeout(function () {
breadClick(bakeries);
hotdogClick(butchers);
sauceClick(sauceries);
customers(custom);
loop(1000);
}, timer)
}) (1000)
I am trying to desteghide an image to reveal the secret message.
I have got it working, but only by browsing the image which contains the message.
If you take a look at the screenshot:
To make this work I need to save the image (the arsenal badge). Then use the browse to upload the image again to make this work. I want to skip the browse stage and make automatically by grabbing the image straight from the div.
Any ideas?
here is my code (its quite messy sorry)
window.onload = function() {
// add action to the file input
var input = document.getElementById('file');
input.addEventListener('change', importImage);
};
var maxMessageSize = 1000;
$("#desteg").click(function()
{
decode();
});
var importImage = function(e) {
var reader = new FileReader();
reader.onload = function(event) {
// set the preview
document.getElementById('preview').style.display = 'block';
document.getElementById('preview').src = event.target.result;
// wipe all the fields clean
document.getElementById('pass').value = '';
document.getElementById('ContentArea').innerHTML = '';
// read the data into the canvas element
var img = new Image();
img.onload = function() {
var ctx = document.getElementById('canvas').getContext('2d');
ctx.canvas.width = img.width;
ctx.canvas.height = img.height;
ctx.drawImage(img, 0, 0);
decode();
};
img.src = event.target.result;
};
reader.readAsDataURL(e.target.files[0]);
};
// encode the image and save it
// decode the image and display the contents if there is anything
var decode = function() {
var password = document.getElementById('pass').value;
var passwordFail = 'Password is incorrect or there is nothing here.';
// decode the message with the supplied password
var ctx = document.getElementById('canvas').getContext('2d');
var imgData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
var message = decodeMessage(imgData.data, sjcl.hash.sha256.hash(password));
// try to parse the JSON
var obj = null;
try {
obj = JSON.parse(message);
} catch (e) {
// display the "choose" view
if (password.length > 0) {
alert(passwordFail);
}
}
// display the "reveal" view
if (obj) {
// decrypt if necessary
if (obj.ct) {
try {
obj.text = sjcl.decrypt(password, message);
} catch (e) {
alert(passwordFail);
}
}
// escape special characters
var escChars = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
'\'': ''',
'/': '/',
'\n': '<br/>'
};
var escHtml = function(string) {
return String(string).replace(/[&<>"'\/\n]/g, function (c) {
return escChars[c];
});
};
document.getElementById('ContentArea').innerHTML = escHtml(obj.text);
}
};
// returns a 1 or 0 for the bit in 'location'
var getBit = function(number, location) {
return ((number >> location) & 1);
};
// sets the bit in 'location' to 'bit' (either a 1 or 0)
var setBit = function(number, location, bit) {
return (number & ~(1 << location)) | (bit << location);
};
// returns an array of 1s and 0s for a 2-byte number
var getBitsFromNumber = function(number) {
var bits = [];
for (var i = 0; i < 16; i++) {
bits.push(getBit(number, i));
}
return bits;
};
// returns the next 2-byte number
var getNumberFromBits = function(bytes, history, hash) {
var number = 0, pos = 0;
while (pos < 16) {
var loc = getNextLocation(history, hash, bytes.length);
var bit = getBit(bytes[loc], 0);
number = setBit(number, pos, bit);
pos++;
}
return number;
};
// returns an array of 1s and 0s for the string 'message'
var getMessageBits = function(message) {
var messageBits = [];
for (var i = 0; i < message.length; i++) {
var code = message.charCodeAt(i);
messageBits = messageBits.concat(getBitsFromNumber(code));
}
return messageBits;
};
// gets the next location to store a bit
var getNextLocation = function(history, hash, total) {
var pos = history.length;
var loc = Math.abs(hash[pos % hash.length] * (pos + 1)) % total;
while (true) {
if (loc >= total) {
loc = 0;
} else if (history.indexOf(loc) >= 0) {
loc++;
} else if ((loc + 1) % 4 === 0) {
loc++;
} else {
history.push(loc);
return loc;
}
}
};
// encodes the supplied 'message' into the CanvasPixelArray 'colors'
// returns the message encoded in the CanvasPixelArray 'colors'
var decodeMessage = function(colors, hash) {
// this will store the color values we've already read from
var history = [];
// get the message size
var messageSize = getNumberFromBits(colors, history, hash);
// exit early if the message is too big for the image
if ((messageSize + 1) * 16 > colors.length * 0.75) {
return '';
}
// exit early if the message is above an artificial limit
if (messageSize === 0 || messageSize > maxMessageSize) {
return '';
}
// put each character into an array
var message = [];
for (var i = 0; i < messageSize; i++) {
var code = getNumberFromBits(colors, history, hash);
message.push(String.fromCharCode(code));
}
// the characters should parse into valid JSON
return message.join('');
};