Controlling asynchronous behavior in your code is a subtle topic but may be needed for complex soundscapes.
Note that we present multiple examples in this project.
You can use the Code Editor's Context Menu to launch multiple scripts to run in different asynchronous fibers within the same or different sandboxes. If you need script-control asynchronous behavior, read on, oh fearless one.
Loosely speaking, in order to make multiple things happen at the same time we need asynchronous control. The MusicAPI leverages JavaScript promises and Lua coroutines to provide these capabilities. Since these are standard language features you can find a wealth of generic online resources to learn more.
First we note that the await
keyword "just works".
await.js
let scene = await Ascene.BeginFiber(this);
let sec = 1 + Math.random();
let dur = scene.Seconds(sec);
console.log("begin " + new Date());
for(let i=0;i<10;i++)
{
await scene.Wait(dur); // <----- wait quietly (blocking but async)
console.log(`tick ${i} ${sec.toFixed(3)}`);
// yield makes this Fiber cancellable.
// It's optional, but usually a good idea.
yield;
}
console.log("end " + new Date());
Here's an example of two independent launches of await.js
.
In this example we develop an async function
, myfunc. Each time
through the loop, we employ Async.Run
to launch our function as a
separate fiber. It's job is to modify the value of Vital's Oscillator 1 Pan
parameter with timing that is independent of the notes we produce.
The key behavior to notice is that both fibers can block without impacting the other.
asyncrun.js
let scene = await Ascene.BeginFiber(this);
let inst = scene.NewAnode("Vital"); // request creation of Vital synth node
let out = scene.GetDAC();
scene.Chain(inst, out);
await scene.Sync(); // after this completes our nodes are "live"
inst.Show(); // show the interface so we can see the parameter change
let loop = 0;
let notedur = scene.Seconds(.25);
let myfunc = async () => // we must be async in order to use 'await'
{
let l = loop;
let pans = [0, .5, 1];
for(let j=0;j<3;j++)
{
inst.SetParam("Oscillator 1 Pan", pans[j%3]);
await scene.Wait(notedur*2); // <--- block in another fiber
}
console.log("done pan " + l);
};
let asap = 0;
for(loop=0;loop<10;loop++)
{
Async.Run(myfunc); // <--- Launch myfunc on each loop
for(let key=40;key<60;key+=2)
{
inst.NoteOn(key, .9/*velocity*/, asap);
await scene.Wait(notedur); // <--- block in main fiber
inst.NoteOff(key, 1, asap);
yield;
}
}
generator.js
/* This is a async generator; it can be asynchronously iterated.
*/
let scene = await Ascene.BeginFiber(this);
let inst = scene.NewAnode("Hz.Syntho"); // request creation of Vital synth node
let out = scene.GetDAC();
scene.Chain(inst, out);
await scene.Sync(); // after this completes our nodes are "live"
let loop = 0;
let notedur = scene.Seconds(.25);
async function *doloop(startNote=0)
{
console.log(`doloop ${startNote} begin`);
let noteDur = scene.Seconds(.1);
let restDur = scene.Seconds(.10);
let note = startNote || 40 + Math.floor(12*Math.random());
for(let i=0;i<30;i++)
{
// console.log("note " + i);
inst.NoteOn(note, .7);
await scene.Wait(noteDur); // <-- we block here
inst.NoteOff(note);
await scene.Wait(restDur); // <-- we block here too
yield; // <<--- -yield control back to caller
note = 32 + (note+1) % 30 ;
}
console.log(`doloop ${startNote} end`);
}
// interate our async function
for await (const val of doloop(40))
{
yield; // here we yield to system, this updates AudioStatus/Fibers
}
// We must pass an iterator to AddFiber.
// Fibers provide more feedback than Async.Run.
// They can also be canceld.
console.log("Let's try FiberMgr");
FiberMgr.AddFiber(doloop(50), {id: 10, name: "subfiber0"});
FiberMgr.AddFiber(doloop(52), {id: 11, name: "subfiber1"});
// another iteration over our local async function.
for await (const val of doloop(60))
{
yield; // here we yield to system, this updates AudioStatus/Fibers
}
console.log("generate.js done.");
Produces this output: