I'm using the WebAudio API to crossfade between multiple sources. Fades are queued using setValueCurveAtTime(curve, time, duration). The WebAudio spec indicates that any subsequent call to setValueCurveAtTime with overlapping durations is not allowed. So I'm calling cancelScheduledValues(time) before queuing up new fades. Both Firefox v68 and Chrome v77 throw errors on the second setValueCurveAtTime call however.
The attached snippet contains the minimum amount of code to trigger the errors in either browser. Click on Start to trigger the error. Note that it doesn't play any audio as it's not needed for the error to be thrown. The select dropdown allow control of the time argument to both functions. In Chrome v77 time=0 doesn't trigger an error.
Any ideas on how to get this to work in both browsers would be much appreciated!
Update: As Raymond Toy pointed out cancelScheduledValues(t) appears to cancel automations which started at t or later (not simply active during t). By using cancelScheduledValues(Math.max(t - duration, 0)) the code now appears to work in Chrome. Firefox still fails with a Operation is not supported error however.
<!DOCTYPE html>
<html>
<body>
<button id="start">Start</button>
<select id="time">
<option value="0">time=0</option>
<option value="1">time=currentTime</option>
</select>
<pre id="log"></pre>
<script>
const select = document.querySelector('#time')
const log = document.querySelector('#log')
function start() {
const ctx = new AudioContext()
ctx.resume()
const gain = ctx.createGain()
gain.connect(ctx.destination)
// Fade in
gain.gain.setValueCurveAtTime(new Float32Array([0, 1]), 0, 1)
setTimeout(() => {
const time = select.options[select.selectedIndex].value === '0' ? 0 : ctx.currentTime
// Replace fade in with fade out
// THIS IS THE CALL THAT DOESN'T WORK =====
// Doesn't work in Firefox nor Chrome:
// gain.gain.cancelScheduledValues(time)
// Doesn't work in Firefox:
gain.gain.cancelScheduledValues(Math.max(time - 1 /* duration of previous fade */, 0))
try {
// ERROR IS THROWN HERE =================
gain.gain.setValueCurveAtTime(new Float32Array([0, 1]), time, 1)
} catch (error) {
log.prepend(error.message + '\n')
throw error
}
log.prepend('No error!\n')
}, 100)
}
document.querySelector('#start').addEventListener('click', start)
</script>
</body>
</html>
My reading of cancelScheduledValues shows that this is working as intended. The event time for a setValueCurveAtTime(curve, time, duration) is time. cancelScheduledValues(t2) removes all events whose event time is t2 or greater. In your test case, time = 0, and t2 is currentTime which is greater than 0. Thus, nothing is removed from the timeline. The second call to setValueCurveAtTime inserts a new event that does overlap the previous one. Hence, you get an error.
Having said that, I think this is kind of unexpected. This could be an error in the WebAudio spec.
In the end I ended up ditching setValueCurveAtTime() in favour of multiple setValueAtTime calls:
function fade(gainNode, fadeIn, startTime = 0) {
const duration = 1 // seconds
const delta = 1 / 100 // number of volume changes per second
const targetVolume = fadeIn ? 1 : 0
const currentTime = audioContext.currentTime
if (!startTime) { startTime = currentTime }
// We can only read current volume for current startTime, so if queueing a fade
// we'll have to assume it starts in the other end
const startingVolume = startTime && startTime !== currentTime
? 1 - targetVolume
: gainNode.gain.value
// Offset to start at when startingVolume isn't 0 or 1
let tOffset = equalPowerEasingInverse(startingVolume)
if (fadeIn) tOffset = 1 - tOffset
let t = 0 // time iterator [0..1]
try {
// Cancel any potentially overlapping fade automations
gainNode.gain.cancelScheduledValues(Math.max(startTime - duration, 0))
for (; t + tOffset <= 1; t += delta) {
// Queue volume change
gainNode.gain.setValueAtTime(
equalPowerEasing(t + tOffset, fadeIn),
startTime + t*duration
)
}
// Ensure final value is exact
gainNode.gain.setValueAtTime(targetVolume, startTime + t*duration)
} catch (error) {
if (/Failed to execute 'setValueCurveAtTime'|Operation is not supported/.test(error.message)) {
// Ignore Chrome + Firefox errors
} else {
throw error
}
}
}
function equalPowerEasing(t, invert = true) {
if (invert) t = 1 - t
return Math.cos(t * 0.5 * Math.PI)
}
function equalPowerEasingInverse(x) {
return Math.acos(x) / 0.5 / Math.PI
}
Related
I would like to get the amount of time the process took to fully complete. I have this code:
console.log(`Ran ${ran} equations in ${console.timeEnd()}`)
but I don't get my expected output, which is:
Ran X equations in 33.099ms // eg
instead I get
default: 33.099ms
Ran X Equations in undefined
Note that I didn't give my console.time() a label.
How can I achieve my expected output?
console.timeEnd doesn't return anything; it's part of the console, as you know. Because console.timeEnd counts in milliseconds with decimal places (unlike Date.now()), the closest thing you'll get is with performance.now(). We can create a custom time function:
class Time {
constructor() {
this.time = performance.now();
}
end() {
return (performance.now() - this.time).toFixed(3); // round number to lower decimal precision, like console.time()
}
}
const time = new Time();
console.time("Console measurement");
let dummyVar = 0;
for(let i = 0; i < 1000000; i++) {
// do some time consuming task
dummyVar += i;
}
console.log("Custom measurement:", time.end() + "ms");
console.timeEnd("Console measurement");
I intentionally returned a number and not a string (with "ms" at the end) so you could use it better. Obviously, if you want to represent the exact output of console.timeEnd(), simply append + "ms" to the expression in Time#end:
class Time {
constructor() {
this.time = performance.now();
}
end() {
return (performance.now() - this.time).toFixed(3) + "ms";
}
}
const time = new Time();
// dummy example
setTimeout(() => console.log(`Ran X equations in ${time.end()}`), Math.floor(Math.random() * 100));
console.timeEnd() returns undefined, not what it logs. To get very close to the desired output, label the timer with the descriptive string...
const ran = 4; // you'll need to know ran before starting the timer
const label = `Ran ${ran} equations in`;
console.time(label);
// do some time consuming stuff
alert('wait a sec, then press ok')
console.timeEnd(label);
I am playing with the Web Audio API. If I play the notes automatically and set the release right after attack, it sounds ok.
But when I use the piano, and set the release to wait until the key is released, (either a setTimeout for example), it produces a cracking sounds.
This demonstrates the issue very well.
Please fix this I need this.
let context = new AudioContext()
function nocrack() {
let r = play()
r(0.2)
}
function crack() {
let r = play()
setTimeout(() => {
r(0.2)
}, 200)
}
function play() {
let time = context.currentTime
let gain = context.createGain()
gain.gain.setValueAtTime(1, time)
gain.connect(context.destination)
let osc1 = new OscillatorNode(context, { type: 'triangle', detune: 15 })
let osc1_mix = new GainNode(context)
osc1.connect(osc1_mix)
osc1_mix.gain.setValueAtTime(0.5, time)
osc1_mix.connect(gain)
osc1.start(time)
return (r) => {
let now = context.currentTime
osc1_mix.gain.linearRampToValueAtTime(0, now + r)
osc1.stop(now + r)
}
}
<button onclick="nocrack()">
Play a Note
</button>
<button onclick="crack()">
Play Crackie
</button>
I think you can fix the problem by adding one line to your stop/fade-out function.
return (r) => {
const now = context.currentTime;
osc1_mix.gain.setValueAtTime(0.5, now);
osc1_mix.gain.linearRampToValueAtTime(0, now + r);
osc1.stop(now + r);
}
When scheduling a linear ramp with linearRampToValueAtTime() the starting point of that ramp is the last event preceding the ramp in the timeline. If you schedule the ramp directly it doesn't really matter. But if you do so after a while it results in a noticeable sudden volume drop heard as a click.
Let's say you start your sound at a currentTime of 10. And then you stop it 1 second later. That means you schedule a ramp which starts at a currentTime of 10 and ends at a currentTime of 11.2. But at this point currentTime is already at 11. That means the browser immediately lowers the volume to match the volume which it should have when applying about 83% of the ramp and continues from there.
I guess what you wanted to achieve was a fade-out of 0.2 seconds from the original volume starting at the time the sound gets stopped. This can be achieved by inserting a new event in the timeline right before starting the ramp.
I am trying to create a Countup counter Starting from 1 to 10000 and i do not want it to reset when user refreshes the page or cancels the page. The Counter should start from 1 for every user that visits the page and keep running in background till it gets to 10000 even if the page is closed.
I have written the page below which;
Starts from the specified number for every new visitor
Saves the progress and does not reset when page is refreshed, however
It does not keep counting when page is closed and starts from the last progress when user closes the tab and comes back later. My code is
function countUp() {
var countEl = document.querySelector('.counter');
var countBar = document.querySelector('.progress-bar');
var x = parseInt(localStorage.getItem('lastCount')) - 1 || 1;
var y = countEl.dataset.to;
var z = countBar.dataset.to;
function addNum() {
countEl.innerHTML = x;
x += 1;
if (x > y && x > z) {
clearInterval(timer);
}
localStorage.setItem('lastCount', x);
}
var timer = window.setInterval(addNum, 1000);
localStorage.setItem("addNum", counter);
toggleBtn.addEventListener('click', function(){
countUp();
toggleBtn.classList.add('hidden');
});
}
countUp();</script>
<body onload=countUp();>
<div class="counter" data-from="0" data-to="10000000"></div>
<div class="progress-bar" data-from="0" data-to="10000000"></div>
</body>
It's difficult to show an example on StackOverflow because it doesn't let you fiddle with localStorage but, it sounds like you want something like:
When a user visits the page check localStorage for a timestamp.
If timestamp exists, go to step 4
Timestamp doesn't exist so get the current timestamp and stash it in localStorage.
Get the current timestamp. Subtract the timestamp from before. If over 10,000, stop, you're done.
Display difference calculated in step 4.
Start a 1 second timer, when time is up, go to step 4.
Something along those lines should work even if they refresh the page and since you are calculating from the original timestamp it will "count" in the background even if the page is closed.
window.addEventListener("DOMContentLoaded", () => {
const start = localStorage.getItem("timestamp") || Date.now();
localStorage.setItem("timestamp", start);
function tick() {
const now = Date.now();
const seconds = Math.floor((now - start) / 1000);
const display = document.getElementById("display");
if (seconds > 10000) return display.innerHTML = "We're done";
display.innerHTML = seconds;
setTimeout(tick, 1000);
}
tick();
});
<div id="display"></div>
So, client-side code can't normally execute when a client-side javascript page is closed.
What you could do, however, is calculate where the timer should be then next time it is loaded.
For example, in your addNum() function, you could in addition to the last count, also store the current date (and time).
function addNum() {
countEl.innerHTML = x;
x += 1;
if (x > y && x > z) {
clearInterval(timer);
}
localStorage.setItem('lastCount', x);
localStorage.setItem('lastDate', new Date());
}
Then, when your code starts, you can retrieve lastDate, and then subtract the current Date() from it.
Then use that to add the difference to your counter.
function countUp() {
let storedCount = parseInt(localStorage.getItem('lastCount'));
let storedDate = Date.parse(localStorage.getItem('lastDate'));
let now = new Date()
let diffSeconds = (now.getTime() - storedDate.getTime()) / 1000;
let storedCount += diffSeconds;
var countEl = document.querySelector('.counter');
var countBar = document.querySelector('.progress-bar');
var x = storedCount - 1 || 1;
var y = countEl.dataset.to;
var z = countBar.dataset.to;
}
I'm sure there are some more changes required to make it work with your code, but the idea is to store the current time so that when the page is closed and reopened, you can 'adjust' the count to catch up to what it should be.
What you want here is not possible just from the client-side code, there is no way for 2 different machines to share that information at all.
Here's the thing though, you can do this with a backend where the database gets updated every time a new IP hits the server. Note with this approach, a user here is one system and not different browsers or sessions.
To update this real-time for someone who is already on the website, run a timer and call an API that specifically gives you the count. Therefore the page gets updated frequently. You can also do this with react-query as it comes with inbuilt functions to do all this.
i would like to measure the skipped time during video playback if a user skipped some.
Using video.currentTime
First it looks quite trivial
Listen to seeking and get the currentTime A
Listen to seekend and get the currentTime B
B - A = Seeked Time
When i do that in Chrome the result is 0.
Why is that? If you listen to timeupdate TU it gets quite obvious.
Simplified Sequence:
Press Play
TU: 1
TU: 2
TU: 3 // now i use the mouse to seek forward to 19
TU: 19
//chrome & ff fire pause in between, ie not
Seek Start: 19
TU: 19
Seek End: 19
TU: 19
//chrome & ff fire play, ie not
TU: 20
...
I know i can play dirty and save the currentTime somewhere but not in an reliable way ;-)
Using TimeRanges video.played
I can use TimeRanges to calculate the amount of Time which got seeked/skipped. But the Problem is: TimeRanges come in ordered and normalized form as list. So if the User jumps forth an back the Ranges become merged and ordered => not reliable for accurate tracking.
Is there an easier less complicated approch i just dont see?
How I solved it.
I used a RingBuffer (Size 10) from here https://stackoverflow.com/a/28038535/2588818 and push the Math.round(video.currentTime) with every timeupdate to it.
On seekend I go left (prev) in the RingBuffer and take the first datum which differs from the current time.
Works just fine enough to use it for tracking the skipped time.
var createRingBuffer = createRingBuffer || function(length) {
/* https://stackoverflow.com/a/4774081 */
var pointer = 0, buffer = [];
return {
...
push : function(item){
buffer[pointer] = item;
pointer = (pointer + 1) % length;
return item;
},
...
getFirstDifference: function() {
var last_value = buffer[pointer - 1],
prev_value;
for (var i = 1; i <= length; i++) {
prev_value = buffer[(pointer - i) % length]
//check for undefined or initialize the buffer beforehand
if(prev_value === undefined || last_value === prev_value) {
} else {
return prev_value;
}
}
return last_value;
},
print: function() {
console.log(JSON.stringify(buffer));
}
};
};
...
var seekTimes = createRingBuffer(10);
handleUpdateTime = function() {
seekTimes.push(Math.round(video.currentTime));
},
handleSeekEnd = function(e) {
seekTimes.print();
console.log("%s - %s", seekTimes.getFirstDifference(), Math.round(video.currentTime));
},
...
I'm working on a JavaScript game that involves throwing a snowball. I need the snowball to render as often as possible during its flight path. Chrome does all the calculations, including setting the style.left and style.top properties, but doesn't actually redraw the snowball until it reaches its destination. Opera doesn't have this problem.
A relevant point is that putting in an alert() after renderSnowball() fixes the problem, except using the alert() is an obvious issue.
Here's my code so far:
function throwSnowball()
{
var theta = parseFloat(angleField.value) * Math.PI/180 ;
var Vir = parseFloat(velocityField.value) ;
if (!isNaN(Vir) && !isNaN(theta) )
{
Vix = Math.cos(theta) * Vir * 50;
Viy = Math.sin(theta) * Vir * 50;
time = new Date() ;
var timeThrown = time.getTime() ;
while (snowballPosY > 0)
{
current = new Date() ;
var currentTime = current.getTime() ;
var timeElapsed = (currentTime - timeThrown)/5000 ;
snowballPosX += Vix * timeElapsed;
snowballPosY += Viy * timeElapsed;
Viy -= GRAVITY * timeElapsed ;
renderSnowball() ; //renderSnowball() sets the style.left
// and style.top properties to snowballPosX pixels
// and snowballPosY pixels respectively
timeThrown = currentTime ;
}
snowballPosX = 0 ;
snowballPosY = 50 ;
renderSnowball() ;
}
}
You're totally blocking the main thread. Have you tried using a setTimeout (even with a zero timeout) to allow other things to happen during your animation?
If you're willing to use experimental technology, requestAnimationFrame would be even better.
Edit: the setTimeout approach would look something like this (replacing the while loop):
var drawAndWait = function() {
if (snowballPosY > 0) {
// movement/drawing code here
setTimeout(drawAndWait, 20 /* milliseconds */);
} else {
// reset code that would normally go after your while loop
}
};
drawAndWait();
So each time the drawing finishes, it arranges for itself to be invoked again, if appropriate. Note that your throwSnowball function will return quickly; the throwing isn't actually done until later on. This takes awhile to get used to doing correctly; don't be too concerned if it's not intuitive at first.
Try getting out of the tight loop. Chrome may not want to redraw until your function exits. Try using setInterval or setTimeout to give Chrome a chance to repaint.