Loop through array at certain speed in Javascript - javascript

I'm new here so spare me please.
I'm working on a little hobby project of mine where an application gathers telemetry which is recorded and saved at 60Hz (so 60 index in an array for each second). This is always the case.
I then want to 'play through' this array at the regular speed as if you're playing a video. But instead of seeing a video I want to show the current data from each index that you're at on the frontend.
The structure of the array is not made yet, but I assume I will just store and load a JSON file somewhere in the database. This then should be played through in my frontend. I've used Angular (6) to build that, so it would be great if it could be mixed with Angular (to keep track of the progress of which index you are now, and bind the values of the current index to the frontend).
Would it be easy to just use 0-1-2-etc for indexx, or maybe something like timestamps?
Any suggestions are welcome. The most important thing here is to keep it quick so you don't need a strong rig to play this.
Thanks in advance!

You need to use setInterval function in which you will iterate over the array according to your frequency. So if your frequency is 60hz that means you want to iterate to next element in array after every 1000 / 60 milliseconds
var data = [1, 2, 3, 4, 5]
var currentIndex = 0;
var interval = 1000 / 60
var id = setInterval(function() {
// do your thing
if(currentIndex == (data.length-1)) {
clearInterval(id)
} else {
currentIndex++
}
}, interval)
This is not a particularly iterating over array but rather doing some action after interval of time and then moving to next item and when you are done with array this will clear the interval. Perhaps linked list will be more helpful than array here

You can make yourself a simple runner for such things. It's basically a game engine and you need a game loop :)
Here's a naive example. Skipping most of error checking and validation here for brewity.
class TelemetryPlayer {
constructor(items, render, interval) {
// setup local data.
this.interval = interval;
this.items = items;
this.render = render;
this.startLoop();
}
// Loop simply initializes before the first run, and then runs the loop.
// Other places do the actual work.
startLoop() {
this._renderInProgress = false;
this._currentIndex = 0;
// save the interval reference if you wanna pause/reset later on.
this._interval = setInterval(this.doWork.bind(this), this.interval);
}
// here we perform the actual render.
doWork() {
if (this._renderInProgress) {
// previous render has not completed yet.
console.log('skip');
return;
}
console.log('Tick');
this._renderInProgress = true;
const item = this.items[this._currentIndex];
console.log('Tick');
// now, call your renderer, and update stuff when complete.
this.render(item)
.then(() => {
// Or this can be a callback or similar.
this._renderInProgress = false;
// Ready next item. Do not go out of array bounds.
this._currentIndex++;
if (this._currentIndex === this.items.length) {
this._currentIndex = 0;
}
});
}
// You can then add fun things like skip, pause, reset etc.
skip(item) {
if (item < 0 || item > this.items.length) {
return;
}
// pause first
this.pause();
this._currentIndex = item;
this.unpause();
}
//
reset() {
this.skip(0);
}
//
pause() {
this._interval = clearInterval(this._interval);
}
unpause() {
if (!this._interval) {
this._interval = setInterval(this.doWork.bind(this), this.interval);
}
}
// you can even add items later
addItem(item) {
this.items.push(item);
}
// or replace them.
replaceItem(item, index) {
this.items[index] = item;
// show the new item right away.
this.skip(index);
}
// or add an item to be played just once.
playOnce(item) {
this.pause();
this.render(item);
this.unpause();
}
}
Now here's an example usage. You can copy the code (both class above and the code block bellow) and paste it into console right here on StackOverflow to see it in work. You likely wanna do other things, but you'll get the gist.
let items = [ 100, 200, 300, 50, 100, 200, 300, 250 ];
const timeline = document.createElement('ul');
// maybe better do this by adding class and external stylesheet
timeline.setAttribute('style', 'padding: 15px; border: 1px solid gray; list-style-type: none; height: 500px; width: 100%; position: absolute; top: 0;overflow-y: scroll;')
document.body.appendChild(timeline);
let render = function(item) {
return new Promise(function (resolve) {
// run something (in) expensive with item now.
const li = document.createElement('li');
// again, better do this with class.
li.setAttribute('style', `display: inline-block; width: 25px; margin: 5px; background-color: #c1c1c1; height: ${item}px;`);
timeline.appendChild(li);
li.scrollIntoView();
// return when done.
resolve();
});
}
const player = new TelemetryPlayer(items, render, 1500);
// now you can do things like speed up etc.
// now you can do things like speed up etc.
function speedUp() {
// speedup 3 seconds after "playback" starts.
return new Promise((resolve) => setTimeout(() => {
player.pause();
player.interval = 600;
player.unpause();
resolve();
}, 3000));
}
function playOnce() {
// add item once, but not in the array we loop around in
return new Promise((resolve) => setTimeout(() => {
player.playOnce(1000);
resolve();
}, 3000));
}
// or add a few items that will be repeated.
function addItems() {
// add a super very high item 3 seconds after function call.
return new Promise((resolve) => setTimeout(() => {
player.pause();
player.addItem(400);
player.addItem(430);
player.addItem(420);
player.unpause();
// now rewind to this block. I am off by one, likely.
player.skipTo(player.items.length - 3);
resolve();
}, 5000))
}
speedUp()
.then(playOnce)
.then(addItems);

Related

Javascript: Infinite loop in webworker [duplicate]

I want to sort an array, using Web Workers. But this array might receive new values over time, while the worker is still performing the sort function.
So my question is, how can I "stop" the sorting computation on the worker after receiving the new item, so it can perform the sort on the array with that item, while still keeping the sorting that was already made?
Example:
let worker = new Worker('worker.js');
let list = [10,1,5,2,14,3];
worker.postMessage({ list });
setInterval(() => worker.postMessage({ num: SOME_RANDOM_NUM, list }), 100);
worker.onmessage = event => {
list = event.data.list;
}
So lets say that, I've passed 50, the worker made some progress in the sorting before that and now I have something like this:
[1, 2, 3, 10, 5, 14, 50]. Which means the sorting stopped at index 3. So I pass this new array back to the worker, so it can continue the sorting from position 3.
How can I accomplish that, since there is no way to pause/resume a web worker?
Even though the Worker works on an other thread than the one of your main page, and can thus run continuously without blocking the UI, it still runs on a single thread.
This means that until your sort algorithm has finished, the Worker will delay the execution of the message event handler; it is as blocked as would be the main thread.
Even if you made use of an other Worker from inside this worker, the problem would be the same.
The only solution would be to use a kind of generator function as the sorter, and to yield it every now and then so that the events can get executed.
But doing this will drastically slow down your sorting algorithm.
To make it better, you could try to hook to each Event Loop, thanks to a MessageChannel object: you talk in one port and receive the message in the next Event loop. If you talk again to the other port, then you have your own hook to each Event loop.
Now, the best would be to run a good batch in every of these Event loop, but for demo, I'll call only one instance of our generator function (that I borrowed from this Q/A)
const worker = new Worker(getWorkerURL());
worker.onmessage = draw;
onclick = e => worker.postMessage(0x0000FF/0xFFFFFF); // add a red pixel
// every frame we request the current state from Worker
function requestFrame() {
worker.postMessage('gimme a frame');
requestAnimationFrame(requestFrame);
}
requestFrame();
// drawing part
const ctx = canvas.getContext('2d');
const img = ctx.createImageData(50, 50);
const data = new Uint32Array(img.data.buffer);
ctx.imageSmoothingEnabled = false;
function draw(evt) {
// converts 0&1 to black and white pixels
const list = evt.data;
list.forEach((bool, i) =>
data[i] = (bool * 0xFFFFFF) + 0xFF000000
);
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.putImageData(img,0,0);
// draw bigger
ctx.scale(5,5);
ctx.drawImage(canvas, 0,0);
}
function getWorkerURL() {
const script = document.querySelector('[type="worker-script"]');
const blob = new Blob([script.textContent]);
return URL.createObjectURL(blob);
}
body{
background: ivory;
}
<script type="worker-script">
// our list
const list = Array.from({length: 2500}).map(_=>+(Math.random()>.5));
// our sorter generator
let sorter = bubbleSort(list);
let done = false;
/* inner messaging channel */
const msg_channel = new MessageChannel();
// Hook to every Event loop
msg_channel.port2.onmessage = e => {
// procede next step in sorting algo
// could be a few thousands in a loop
const state = sorter.next();
// while running
if(!state.done) {
msg_channel.port1.postMessage('');
done = false;
}
else {
done = true;
}
}
msg_channel.port1.postMessage("");
/* outer messaging channel (from main) */
self.onmessage = e => {
if(e.data === "gimme a frame") {
self.postMessage(list);
}
else {
list.push(e.data);
if(done) { // restart the sorter
sorter = bubbleSort(list);
msg_channel.port1.postMessage('');
}
}
};
function* bubbleSort(a) { // * is magic
var swapped;
do {
swapped = false;
for (var i = 0; i < a.length - 1; i++) {
if (a[i] > a[i + 1]) {
var temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
swapped = true;
yield swapped; // pause here
}
}
} while (swapped);
}
</script>
<pre> click to add red pixels</pre>
<canvas id="canvas" width="250" height="250"></canvas>
Note that the same can be achieved with an async function, which may be more practical in some cases:
const worker = new Worker(getWorkerURL());
worker.onmessage = draw;
onclick = e => worker.postMessage(0x0000FF/0xFFFFFF); // add a red pixel
// every frame we request the current state from Worker
function requestFrame() {
worker.postMessage('gimme a frame');
requestAnimationFrame(requestFrame);
}
requestFrame();
// drawing part
const ctx = canvas.getContext('2d');
const img = ctx.createImageData(50, 50);
const data = new Uint32Array(img.data.buffer);
ctx.imageSmoothingEnabled = false;
function draw(evt) {
// converts 0&1 to black and white pixels
const list = evt.data;
list.forEach((bool, i) =>
data[i] = (bool * 0xFFFFFF) + 0xFF000000
);
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.putImageData(img,0,0);
// draw bigger
ctx.scale(5,5);
ctx.drawImage(canvas, 0,0);
}
function getWorkerURL() {
const script = document.querySelector('[type="worker-script"]');
const blob = new Blob([script.textContent]);
return URL.createObjectURL(blob);
}
body{
background: ivory;
}
<script type="worker-script">
// our list
const list = Array.from({length: 2500}).map(_=>+(Math.random()>.5));
// our sorter generator
let done = false;
/* outer messaging channel (from main) */
self.onmessage = e => {
if(e.data === "gimme a frame") {
self.postMessage(list);
}
else {
list.push(e.data);
if(done) { // restart the sorter
bubbleSort(list);
}
}
};
async function bubbleSort(a) { // async is magic
var swapped;
do {
swapped = false;
for (var i = 0; i < a.length - 1; i++) {
if (a[i] > a[i + 1]) {
const temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
swapped = true;
}
if( i % 50 === 0 ) { // by batches of 50?
await waitNextTask(); // pause here
}
}
} while (swapped);
done = true;
}
function waitNextTask() {
return new Promise( (resolve) => {
const channel = waitNextTask.channel ||= new MessageChannel();
channel.port1.addEventListener("message", (evt) => resolve(), { once: true });
channel.port2.postMessage("");
channel.port1.start();
});
}
bubbleSort(list);
</script>
<pre> click to add red pixels</pre>
<canvas id="canvas" width="250" height="250"></canvas>
There are two decent options.
Option 1: Worker.terminate()
The first is just to kill your existing web worker and start a new one. For that you can use Worker.terminate().
The terminate() method of the Worker interface immediately terminates the Worker. This does not offer the worker an opportunity to finish its operations; it is simply stopped at once.
The only downsides of this approach are:
You lose all worker state. If you had to copy a load of data into it for the request you have to do it all again.
It involves thread creation and destruction, which isn't as slow as most people think but if you terminate web workers a lot it might cause issues.
If neither of those are an issue it is probably the easiest option.
In my case I have lots of state. My worker is rendering part of an image, and when the user pans to a different area I want it to stop what it is doing and start rendering the new area. But the data needed to render the image is pretty huge.
In your case you have the state of your (presumably huge) list that you don't want to use.
Option 2: Yielding
The second option is basically to do cooperative multitasking. You run your computation as normal, but every now and then you pause (yield) and say "should I stop?", like this (this is for some nonsense calculation, not sorting).
let requestId = 0;
onmessage = event => {
++requestId;
sortAndSendData(requestId, event.data);
}
function sortAndSendData(thisRequestId, data) {
let isSorted = false;
let total = 0;
while (data !== 0) {
// Do a little bit of computation.
total += data;
--data;
// Check if we are still the current request ID.
if (thisRequestId !== requestId) {
// Data was changed. Cancel this sort.
return;
}
}
postMessage(total);
}
This won't work though because sortAndSendData() runs to completion and blocks the web worker's event loop. We need some way to yield just before thisRequestId !== requestId. Unfortunately Javascript doesn't quite have a yield method. It does have async/await so we might try this:
let requestId = 0;
onmessage = event => {
console.log("Got event", event);
++requestId;
sortAndSendData(requestId, event.data);
}
async function sortAndSendData(thisRequestId, data) {
let isSorted = false;
let total = 0;
while (data !== 0) {
// Do a little bit of computation.
total += data;
--data;
await Promise.resolve();
// Check if we are still the current request ID.
if (thisRequestId !== requestId) {
console.log("Cancelled!");
// Data was changed. Cancel this sort.
return;
}
}
postMessage(total);
}
Unfortunately it doesn't work. I think it's because async/await executes things eagerly using "microtasks", which get executed before pending "macrotasks" (our web worker message) if possible.
We need to force our await to become a macrotask, which you can do using setTimeout(0):
let requestId = 0;
onmessage = event => {
console.log("Got event", event);
++requestId;
sortAndSendData(requestId, event.data);
}
function yieldToMacrotasks() {
return new Promise((resolve) => setTimeout(resolve));
}
async function sortAndSendData(thisRequestId, data) {
let isSorted = false;
let total = 0;
while (data !== 0) {
// Do a little bit of computation.
total += data;
--data;
await yieldToMacrotasks();
// Check if we are still the current request ID.
if (thisRequestId !== requestId) {
console.log("Cancelled!");
// Data was changed. Cancel this sort.
return;
}
}
postMessage(total);
}
This works! However it is extremely slow. await yieldToMacrotasks() takes approximately 4 ms on my machine with Chrome! This is because browsers set a minimum timeout on setTimeout(0) of something like 1 or 4 ms (the actual minimum seems to be complicated).
Fortunately another user pointed me to a quicker way. Basically sending a message on another MessageChannel also yields to the event loop, but isn't subject to the minimum delay like setTimeout(0) is. This code works and each loop only takes ~0.04 ms which should be fine.
let currentTask = {
cancelled: false,
}
onmessage = event => {
currentTask.cancelled = true;
currentTask = {
cancelled: false,
};
performComputation(currentTask, event.data);
}
async function performComputation(task, data) {
let total = 0;
let promiseResolver;
const channel = new MessageChannel();
channel.port2.onmessage = event => {
promiseResolver();
};
while (data !== 0) {
// Do a little bit of computation.
total += data;
--data;
// Yield to the event loop.
const promise = new Promise(resolve => {
promiseResolver = resolve;
});
channel.port1.postMessage(null);
await promise;
// Check if this task has been superceded by another one.
if (task.cancelled) {
return;
}
}
// Return the result.
postMessage(total);
}
I'm not totally happy about it - it relies on postMessage() events being processed in FIFO order, which I doubt is guaranteed. I suspect you could rewrite the code to make it work even if that isn't true.
You can do it with some trick – with the help of setTimeout function interrupting. For example it is not possible without an addition thread to execute 2 functions parallel, but with setTimeout function interrupting trick we can do it like follows:
Example of parallel execution of functions
var count_0 = 0,
count_1 = 0;
function func_0()
{
if(count_0 < 3)
setTimeout(func_0, 0);//the same: setTimeout(func_0);
console.log('count_0 = '+count_0);
count_0++
}
function func_1()
{
if(count_1 < 3)
setTimeout(func_1, 0);
console.log('count_1 = '+count_1)
count_1++
}
func_0();
func_1();
You will get this output:
count_0 = 0
count_1 = 0
count_0 = 1
count_1 = 1
count_0 = 2
count_1 = 2
count_0 = 3
count_1 = 3
Why is it possible? Because the setTimeout function needs some time to be executed. And this time is even enought for the execution of some part from your following code.
Solution for you
For this case you have to write your own array sort function (or you can also use the following function from me) because we can not interrupt the native sort function. And in this your own function you have to use this setTimeout function interrupting trick. And you can receive your message event notification.
In the following example I have the interrupting in the half length of my array, and you can change it if you want.
Example with custom sort function interrupting
var numbers = [4, 2, 1, 3, 5];
// this is my bubble sort function with interruption
/**
* Sorting an array. You will get the same, but sorted array.
* #param {array[]} arr – array to sort
* #param {number} dir – if dir = -1 you will get an array like [5,4,3,2,1]
* and if dir = 1 in opposite direction like [1,2,3,4,5]
* #param {number} passCount – it is used only for setTimeout interrupting trick.
*/
function sortNumbersWithInterruption(arr, dir, passCount)
{
var passes = passCount || arr.length,
halfOfArrayLength = (arr.length / 2) | 0; // for ex. 2.5 | 0 = 2
// Why we need while loop: some values are on
// the end of array and we have to change their
// positions until they move to the first place of array.
while(passes--)
{
if(!passCount && passes == halfOfArrayLength)
{
// if you want you can also not write the following line for full break of sorting
setTimeout(function(){sortNumbersWithInterruption(arr, dir, passes)}, 0);
/*
You can do here all what you want. Place 1
*/
break
}
for(var i = 0; i < arr.length - 1; i++)
{
var a = arr[i],
b = arr[i+1];
if((a - b) * dir > 0)
{
arr[i] = b;
arr[i+1] = a;
}
}
console.log('array is: ' + arr.join());
}
if(passCount)
console.log('END sring is: ' + arr.join());
}
sortNumbersWithInterruption(numbers, -1); //without passCount parameter
/*
You can do here all what you want. Place 2
*/
console.log('The execution is here now!');
You will get this output:
array is: 4,2,3,5,1
array is: 4,3,5,2,1
The execution is here now!
array is: 4,5,3,2,1
array is: 5,4,3,2,1
END sring is: 5,4,3,2,1
You can do it with insertion sort (kind of).
Here is the idea:
Start your worker with an internal empty array (empty array is sorted obviously)
Your worker receives only elements not the entire array
Your worker insert any received element right in correct position into the array
Every n seconds, the worker raises a message with the current array if it has changed after the last event. (If you prefer, you can send the array on every insertion, but is more efficient to buffer somehow)
Eventually, you get the entire array, if any item is added, you will receive the updated array to.
NOTE: Because your array is always sorted, you can insert in correct position using binary search. This is very efficient.
I think the case comes down to careful management of postMessage calls and amount of data passed to be processed at a time. Was dealing with problem of this kind - think about not sending all new data into the function at once but rather creating your own queue and when small enough portion of the task has been acomplished by webworker thread send a message back to the main thread and decide to send the next portion, wait or quit.
In Your case, e.g. one time You get 9000 new items, next 100k - maybe create a queue/buffer that adds next 10k new elements each time webworker is done processing last data change.
const someWorker = new Worker('abc.js');
var processingLock = false;
var queue = [];
function newDataAction(arr = null) {
if (arr != null) {
queue = queue.concat(arr);
}
if (!processingLock) {
processingLock = true;
var data = [];
for (let i = 0; i < 10000 && queue.length > 0; i++) {
data.push(queue.pop());
}
worker.postMessage(data);
}
}
someWorker.addEventListener('message', function(e) {
if (e.data == 'finished-last-task') {
processingLock = false;
if (queue.length > 0) {
newDataAction();
}
}
});
Worked through many sorting algorithms and I don't see how sending new data into an sorting algorithm with partially sorted array makes much difference in terms of compuation time from sorting them both sequentially and performing a merge.

Detecting elm view not working in wordpress but in javascript

What i am trying to do
I am trying to make a number increase to a certain state when entering view. I am gonna be completely transparent with you. I have next to no website development knowledge, this is simply for a wordpress site where i couldnt find a plugin to do it for me + this gives more flexability. My knowledge simply lies elsewhere. So far i have created code that activates as soon as it enters a view, and it also counts up.
Da Problem
As you can see here "https://jsfiddle.net/yd81prgq/" this works already. I then tried putting the exact same code in wordpress on a element. Now i know that its the activatition part thats not working, since during testing i made it count up, but only as soon as the page loaded.
Code i am putting in wordpress
<h2 id="(id)">Count to ten when seen</h2>
<script>
var i = 0;
function increment() {
if (i<10) {
i++;
document.getElementById('(id)').innerHTML = i;
}
}
function startcount() {
setInterval('increment()', 50);
}
var observer = new IntersectionObserver(function(entries) {
if(entries[0].isIntersecting === true)
startcount();
}, { threshold: [1] });
observer.observe(document.querySelector("#(id)"));
</script>
Thanks in advance :)
You must to remove brackets from the id. Globaly, an id should have only letters, numbers, underscore and/or dash.
PS: java is not javascript ;)
var i = 0;
function increment() {
if (i<10) {
i++;
document.getElementById('id').innerHTML = i;
}
}
function startcount() {
setInterval('increment()', 50);
}
var observer = new IntersectionObserver(function(entries) {
if(entries[0].isIntersecting === true)
startcount();
}, { threshold: [1] });
observer.observe(document.querySelector("#id"));
<h2 id="id">Count to ten when seen</h2>
An alternative with less code for the same result:
const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay));
async function counter() {
for (let i = 1; i <= 10; i++) {
await sleep(50);
document.getElementById('id').innerHTML = i;
}
}
counter();
<h2 id="id">Count to ten when seen</h2>
I found the issue. This puts my amount of my fault posts up to 2/2... The problem was that since the code checks when a certain element enters view wordpress is apparently build in a way that makes it happen when it exits view. Probably to do with some visuals vs. code happening in the background of wordpress. I dont know, but this was the issue. Thanks for the help recieved
For future reference here is the working code:
<h2 id="id" class="class">Make this count to ten</h2>
<script>
const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay));
async function counter() {
for (let i = 1; i <= 10; i++) {
await sleep(50);
document.getElementById('id').innerHTML = i;
}
}
var observer = new IntersectionObserver(function(entries) {
if(entries[0].isIntersecting === true)
counter();
}, { threshold: [1] });
observer.observe(document.querySelector("#id"));
</script>

Pause and restart setTimeout in loop

I have put a delay in my loop by awaiting a function that returns a Promise with setTimeout(). I am able to start and reset the loop but would also like to be able to pause the loop somehow. After clicking the start button again, I would like to continue looping from the last looped element in the array.
$(document).ready(() => {
$('#btn-start').click(() => {
loopMoves();
});
$('#btn-pause').click(() => {
// ...
});
$('#btn-reset').click(() => {
clearTimeout(timer);
});
})
var moves = ['Nf3', 'd5', 'g3', 'g6', 'c4', 'dxc4'];
async function loopMoves() {
for(let i = 0; i < moves.length; i++){
console.log(moves[i]);
await delay(2000);
}
}
var timer;
function delay(ms) {
return new Promise((x) => {
timer = setTimeout(x, ms);
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button id="btn-start">start</button>
<button id="btn-pause">pause</button>
<button id="btn-reset">reset</button>
Instead of using for loop this could be achieved by using setInterval() and a generator function.
Please refer below snippet for the working example:
$(document).ready(() => {
let loop = loopMoves();
$('#btn-start').click(() => {
console.log("****STARTED*****");
loop("PLAY");
});
$('#btn-pause').click(() => {
console.log("****PAUSED*****");
loop("PAUSE");
});
$('#btn-reset').click(() => {
console.log("****RESET*****");
loop("RESET");
});
})
const moves = ['Nf3', 'd5', 'g3', 'g6', 'c4', 'dxc4'];
function loopMoves() {
let moves = generateMoves();
let intervalId;
function startInterval(){
intervalId = setInterval(()=>{
const move = moves.next();
if(move.done)
clearInterval(intervalId);
else
console.log(move.value);
}, 2000);
}
return (state) => {
switch(state){
case "PLAY": startInterval(); break;
case "PAUSE": clearInterval(intervalId); break;
case "RESET": moves = generateMoves();
clearInterval(intervalId); break;
}
}
}
function* generateMoves(){
for(let i = 0; i < moves.length; i++){
yield moves[i];
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button id="btn-start">start</button>
<button id="btn-pause">pause</button>
<button id="btn-reset">reset</button>
You might consider flipping things around into more of a time based "state machine".
When play is clicked, you start your timmer ("playing" state), at the end of the timmer you check the current state. If the state is still "playing" you grab the next move and display that and start the timmer again. If no interaction happens this repeats until all moves are done and the state goes to "stopped".
If someone clicks pause you go to "paused" state but leave the rest as is. When play is clicked again (going from "paused" to "playing") you just pick up where you were. When stop is clicked you reset everything. When play is clicked from the "stopped" state you set the move index to 0 and do the first move, start the timmer.
This does mean each update only happens every 2 seconds but that might be acceptable and it's a whole lot easier than the alternative of pausing the timmer etc.
const moves = ['Nf3', 'd5', 'g3', 'g6', 'c4', 'dxc4']; // These can be loaded however
let state = "stopped"; // The starting state
let currentMoveIndex = 0; // Used to track which move to grab for the next tick
$(document).ready(() => {
$('#btn-start').click(() => {
state = "playing";
tick();
});
$('#btn-pause').click(() => {
state = "paused";
});
$('#btn-reset').click(() => {
state = "stopped";
tick(); // Since we put the clean up inside the tick function we run it again to be sure it's executed in case it was paused
});
})
function tick() {
switch(state) {
case "playing":
if (currentMoveIndex + 1 === moves.length) {
break;
}
const move = moves[currentMoveIndex];
console.log(move); // Or whatever you wish to do with the move
currentMoveIndex += 1;
setTimeout(tick, 2000);
break;
case "paused":
// Do whatever you want based on entering the paused state,
// Maybe show some message saying "Paused"?
break;
case "stopped":
currentMoveIndex = 0;
break;
}
}
If you're using something like React or Angular you can wrap this into a Component/Controller to keep things together, or wrap it all into a Game function if you're using plain JavaScript
I have tried out these answers and both of them seem to work, thank you. I return to this question because I previously thought of something and would like to get some feedback on it. What if I would store the looped items in a new array and restart the loop from the position of the last stored item setting i = done.length (done being the array of looped items). When clicking the pause button clearTimeout() is called and when clicking the reset button, I clear out the done array after calling clearTimeout(), start still runs the function containing the loop.
I understand that I am not really pausing the timer but rather stopping it and restarting it from the last position in the array. Would this be considered 'bad practice' or could this also be an acceptable solution?
$(document).ready(() => {
$('#btn-start').click(() => {
loopMoves();
});
$('#btn-pause').click(() => {
clearTimeout(timer);
});
$('#btn-reset').click(() => {
clearTimeout(timer);
done = [];
});
})
var moves = ['Nf3', 'd5', 'g3', 'g6', 'c4', 'dxc4'];
var done = [];
async function loopMoves() {
for(let i = done.length; i < moves.length; i++){
console.log(moves[i]);
done.push(moves[i]);
await delay(2000);
}
}
var timer;
function delay(ms) {
return new Promise((x) => {
timer = setTimeout(x, ms);
});
}

How do I play audio files synchronously in JavaScript?

I am working on a program to convert text into morse code audio.
Say I type in sos. My program will turn this into the array [1, 1, 1, 0, 2, 2, 2, 0, 1, 1, 1]. Where s = dot dot dot (or 1,1,1), and o = dash dash dash (or 2,2,2). This part is quite easy.
Next, I have two sound files:
var dot = new Audio('dot.mp3');
var dash = new Audio('dash.mp3');
My goal is to have a function that will play dot.mp3 when it sees a 1, and dash.mp3 when it sees a 2, and pauses when it sees a 0.
The following sort of/ kind of/ sometimes works, but I think it's fundamentally flawed and I don't know how to fix it.
function playMorseArr(morseArr) {
for (let i = 0; i < morseArr.length; i++) {
setTimeout(function() {
if (morseArr[i] === 1) {
dot.play();
}
if (morseArr[i] === 2) {
dash.play();
}
}, 250*i);
}
}
The problem:
I can loop over the array, and play the sound files, but timing is a challenge. If I don't set the setTimeout() interval just right, if the last audio file is not done playing and the 250ms has elapsed, the next element in the array will be skipped. So dash.mp3 is longer than dot.mp3. If my timing is too short, I might hear [dot dot dot pause dash dash pause dot dot dot], or something to that effect.
The effect I want
I want the program to go like this (in pseudocode):
look at the ith array element
if 1 or 2, start playing sound file or else create a pause
wait for the sound file or pause to finish
increment i and go back to step 1
What I have thought of, but don't know how to implement
So the pickle is that I want the loop to proceed synchronously. I've used promises in situations where I had several functions that I wanted executed in a specific order, but how would I chain an unknown number of functions?
I also considered using custom events, but I have the same problem.
Do not use HTMLAudioElement for that kind of application.
The HTMLMediaElements are by nature asynchronous and everything from the play() method to the pause() one and going through the obvious resource fetching and the less obvious currentTime setting is asynchronous.
This means that for applications that need perfect timings (like a Morse-code reader), these elements are purely unreliable.
Instead, use the Web Audio API, and its AudioBufferSourceNodes objects, which you can control with µs precision.
First fetch all your resources as ArrayBuffers, then when needed generate and play AudioBufferSourceNodes from these ArrayBuffers.
You'll be able to start playing these synchronously, or to schedule them with higher precision than setTimeout will offer you (AudioContext uses its own clock).
Worried about memory impact of having several AudioBufferSourceNodes playing your samples? Don't be. The data is stored only once in memory, in the AudioBuffer. AudioBufferSourceNodes are just views over this data and take up no place.
// I use a lib for Morse encoding, didn't tested it too much though
// https://github.com/Syncthetic/MorseCode/
const morse = Object.create(MorseCode);
const ctx = new (window.AudioContext || window.webkitAudioContext)();
(async function initMorseData() {
// our AudioBuffers objects
const [short, long] = await fetchBuffers();
btn.onclick = e => {
let time = 0; // a simple time counter
const sequence = morse.encode(inp.value);
console.log(sequence); // dots and dashes
sequence.split('').forEach(type => {
if(type === ' ') { // space => 0.5s of silence
time += 0.5;
return;
}
// create an AudioBufferSourceNode
let source = ctx.createBufferSource();
// assign the correct AudioBuffer to it
source.buffer = type === '-' ? long : short;
// connect to our output audio
source.connect(ctx.destination);
// schedule it to start at the end of previous one
source.start(ctx.currentTime + time);
// increment our timer with our sample's duration
time += source.buffer.duration;
});
};
// ready to go
btn.disabled = false
})()
.catch(console.error);
function fetchBuffers() {
return Promise.all(
[
'https://dl.dropboxusercontent.com/s/1cdwpm3gca9mlo0/kick.mp3',
'https://dl.dropboxusercontent.com/s/h2j6vm17r07jf03/snare.mp3'
].map(url => fetch(url)
.then(r => r.arrayBuffer())
.then(buf => ctx.decodeAudioData(buf))
)
);
}
<script src="https://cdn.jsdelivr.net/gh/mohayonao/promise-decode-audio-data#eb4b1322113b08614634559bc12e6a8163b9cf0c/build/promise-decode-audio-data.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/Syncthetic/MorseCode#master/morsecode.js"></script>
<input type="text" id="inp" value="sos"><button id="btn" disabled>play</button>
Audios have an ended event that you can listen for, so you can await a Promise that resolves when that event fires:
const audios = [undefined, dot, dash];
async function playMorseArr(morseArr) {
for (let i = 0; i < morseArr.length; i++) {
const item = morseArr[i];
await new Promise((resolve) => {
if (item === 0) {
// insert desired number of milliseconds to pause here
setTimeout(resolve, 250);
} else {
audios[item].onended = resolve;
audios[item].play();
}
});
}
}
I will use a recursive approach that will listen on the audio ended event. So, every time the current playing audio stop, the method is called again to play the next one.
function playMorseArr(morseArr, idx)
{
// Finish condition.
if (idx >= morseArr.length)
return;
let next = function() {playMorseArr(morseArr, idx + 1)};
if (morseArr[idx] === 1) {
dot.onended = next;
dot.play();
}
else if (morseArr[idx] === 2) {
dash.onended = next;
dash.play();
}
else {
setTimeout(next, 250);
}
}
You can initialize the procedure calling playMorseArr() with the array and the start index:
playMorseArr([1, 1, 1, 0, 2, 2, 2, 0, 1, 1, 1], 0);
A test example (Using the dummy mp3 files from Kaiido's answer)
let [dot, dash] = [
new Audio('https://dl.dropboxusercontent.com/s/1cdwpm3gca9mlo0/kick.mp3'),
new Audio('https://dl.dropboxusercontent.com/s/h2j6vm17r07jf03/snare.mp3')
];
function playMorseArr(morseArr, idx)
{
// Finish condition.
if (idx >= morseArr.length)
return;
let next = function() {playMorseArr(morseArr, idx + 1)};
if (morseArr[idx] === 1) {
dot.onended = next;
dot.play();
}
else if (morseArr[idx] === 2) {
dash.onended = next;
dash.play();
}
else {
setTimeout(next, 250);
}
}
playMorseArr([1,1,1,0,2,2,2,0,1,1,1], 0);
async & await
Although they are used for asynchronous operations they can be used for synchronous tasks as well. You make a Promise for each function, wrap them in an async function, and then call them with await one at a time. The following is the documentation of the async function as a named function in the demo, the one in the actual demo is an arrow function but either way they are one in the same:
/**
* async function sequencer(seq, t)
*
* #param {Array} seq - An array of 0s, 1s, and 2s. Pause. Dot, and Dash respectively.
* #param {Number} t - Number representing the rate in ms.
*/
Plunker
Demo
Note: If the Stack Snippet doesn't work, review the Plunker
<!DOCTYPE html>
<html>
<head>
<style>
html,
body {
font: 400 16px/1.5 Consolas;
}
fieldset {
max-width: fit-content;
}
button {
font-size: 18px;
vertical-align: middle;
}
#time {
display: inline-block;
width: 6ch;
font: inherit;
vertical-align: middle;
text-align: center;
}
#morse {
display: inline-block;
width: 30ch;
margin-top: 0px;
font: inherit;
text-align: center;
}
[name=response] {
position: relative;
left: 9999px;
}
</style>
</head>
<body>
<form id='main' action='' method='post' target='response'>
<fieldset>
<legend>Morse Code</legend>
<label>Rate:
<input id='time' type='number' min='300' max='1000' pattern='[2-9][0-9]{2,3}' required value='350'>ms
</label>
<button type='submit'>
🔘➖
</button>
<br>
<label><small>0-Pause, 1-Dot, 2-Dash (no delimiters)</small></label>
<br>
<input id='morse' type='number' min='0' pattern='[012]+' required value='111000222000111'>
</fieldset>
</form>
<iframe name='response'></iframe>
<script>
const dot = new Audio(`https://od.lk/s/NzlfOTYzMDgzN18/dot.mp3`);
const dash = new Audio(`https://od.lk/s/NzlfOTYzMDgzNl8/dash.mp3`);
const sequencer = async(array, FW = 350) => {
const pause = () => {
return new Promise(resolve => {
setTimeout(() => resolve(dot.pause(), dash.pause()), FW);
});
}
const playDot = () => {
return new Promise(resolve => {
setTimeout(() => resolve(dot.play()), FW);
});
}
const playDash = () => {
return new Promise(resolve => {
setTimeout(() => resolve(dash.play()), FW + 100);
});
}
for (let seq of array) {
if (seq === 0) {
await pause();
}
if (seq === 1) {
await playDot();
}
if (seq === 2) {
await playDash();
}
}
}
const main = document.forms[0];
const ui = main.elements;
main.addEventListener('submit', e => {
let t = ui.time.valueAsNumber;
let m = ui.morse.value;
let seq = m.split('').map(num => Number(num));
sequencer(seq, t);
});
</script>
</body>
</html>

A pattern to queue notifications?

I created a library to pop up some toast notifications and I tried to put a limit on the maximum notifications on screen.
I managed to extract the idea into a plunker (don't mind the code, it is only to solve the issue).
I have a function to create those toasts:
function createToast() {
var body = $document.find('body').eq(0);
var toast = {};
toast.id = index++;
toast.el = angular.element('<div class="toast">Toast ' + toast.id + '</div>');
toast.el = $compile(toast.el)($scope);
if (maxOpened && toasts.length >= maxOpened) {
remove(toasts[0].id);
}
toasts.push(toast);
$animate.enter(toast.el, body).then(function() {
$timeout(function() {
remove(toast.id);
}, 3000);
});
}
Basically it creates a new object with an el and then animates it out on the body. Notice that if the maxOpened is reached it removes the first one.
function remove(id) {
var toast = findToast(id);
if (toast) {
$animate.leave(toast.el).then(function() {
var index = toasts.indexOf(toast);
toasts.splice(index, 1);
});
}
function findToast(toastId) {
for (var i = 0; i < toasts.length; i++) {
if (toasts[i].id === id) {
return toasts[i];
}
}
}
}
Find the toast, animate the leave and then delete it.
If I do a $interval on them, let's say 600ms it works.
Try here: http://plnkr.co/edit/lDnT57FPadCt5Ir5wHuK?p=preview
If you lower it to something like 100ms it starts to break, not only ignoring the max but also leaving some orphan toasts that won't get deleted.
So I am not sure what could be a good solution here. My best guess is to provide a queue so I start to drain it as soon as a toast get removed but so far, I didn't make it.
The probably simplest solution would be to add a deferred to each toast and only start to animate the toast when the limit is not or no longer reached.
You start by adding a deferred and resolve it immediately, if the limit is not reached yet or the limit can be ignored:
toast.slotOpen = $q.defer();
toasts.push(toast);
if (maxOpened && toasts.length <= maxOpened || !maxOpened) { // i guess 0 or a falsy value means to ignore the limit
toast.slotOpen.resolve();
}
You only start the animation, when a slot is open:
toast.slotOpen.promise.then(function() {
$animate.enter(toast.el, body).then(function() {
The last thing to do is to resolve the deferred when a new slot gets opened after an old toast has been removed:
$animate.leave(toast.el).then(function() {
var index = toasts.indexOf(toast);
toasts.splice(index, 1);
if (maxOpened && toasts.length >= maxOpened) {
toasts[maxOpened - 1].slotOpen.resolve();
}
I have adjusted your code and created a new Plunker.

Categories

Resources