The Async module takes its name from asynchronous (non-sequential, background) processing. Async constructs allow you to control potentially long-running tasks in a way that doesn't prevent other tasks from running. There are many pieces of jargon to capture aspects of asynchronous programming and the jargon differs between programming languages.
In Hz we have settled on the term
Fiber
to represent an independent execution context. This term overlaps with Lua'scoroutines
and JavaScript'sasync functions
andPromises
. It's also loosely related to operating systemthreads
.
Hz's Music API relies heavily on the underlying asynchronous support in both JavaScript and Lua. This includes support for asynchronous processing as well as asynchronous I/O to both local and remote files. In the case of remote I/O, we cache downloaded files into your local filesystem where they can be made available to plugins or other programming scripts.
The asynchronous capabilities rely on the promises
design-pattern. You
can read more at these sites: promises for JavaScript
and promises for Lua.
In short, the purpose of Promises is to coordinate behavior associated with one or more potentially long-running tasks in a way that allows other activites or processing to continue normally.
You don't need to understand the nuances of promises to get most of their
benefits. The most common async methods in the Music API (JS) are
Ascene.BeginFiber() and scene.Wait()
:
// javascript 'await'
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); // stall
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());
Typically you simply request one or more operations, then wait on news of the results. Above, the program execution "stalls" for a random period during its 10 iterations. The async behavior means that the whole of Hz doesn't stall, just this portion of this program. You can launch this program 10 times with different wait periods and they can all merrily stall and tick.
Usually the built-in async behavior of the Music API coupled with JavaScript's
await
keyword gets the job done. If you need to achieve more complex synchronization feats in your projects, a deeper understanding of promises will be helpful.
It's interesting to compare the previous result with that produced by a simple promise-based loop, below. The programs actually produce significantly different behavior. The first example serializes the ticks while this one can be said to schedule them for later. Performing it produces nearly simultaneous "begin" and "end" messages followed by a randomized collection of ticks. We'll leave it as an exercise to further explore the difference between these examples. Then you'll be armed with the knowledge to select the approach that best fits the needs of your projects.
// promise.then
let scene = await Ascene.BeginFiber(this);
console.log("begin " + new Date());
for(let i=0;i<10;i++)
{
let sec = 1 + Math.random();
let dur = scene.Seconds(sec);
scene.Wait(dur) // no stall
.then(() =>
{
console.log(`tick ${i} ${sec.toFixed(3)}`);
yield;
});
}
console.log("end " + new Date());
One more aspect to async control needs highlighting. Promises go a long
way toward expressing how to control and combine your own program behaviors,
but to make your own code async-controllable it must yield
to a greater
power, in our case the Hz runtime. This is where JavaScript generators
and Lua's coroutines
enter the picture and this explains our inclusion
of the JavaScript yield
in our examples above. Note that Lua's coroutine
implementation doesn't require an explicit await
. This behavior
is implied in any function that internally invokes yield
.
In JavaScript, yield
can only be invoked from within generator functions
so this implies that the execution context of your program is a generator
function. All of this is automatic. You don't need to be aware of this fact
unless you wish to author your own yielding functions. Again, in Lua coroutines
no await
is needed and yield
is mostly implicit in programs. On the
flip-side, Lua doesn't support promises as a first-class language feature
but some Music API functions and its runtime environment provide them.
We started by stating that Hz's Music API relies heavily on the underlying asynchronous support in both JavaScript and Lua. Here we focus on several different areas where asynchronous behavior is important.
We've already seen an example of Audio Engine Control. Ensuring that the audio engine is running is as simple as this:
let scene = await Ascene.BeginFiber(this);
BeginFiber
requests Hz to initialize the audio system and otherwise
prepare for program execution. This operation can take several milliseconds
and our program can't operate correctly until conditions allow.
The await
keyword expresses this and stalls until that time.
Making sounds requires audio nodes organized into an audio graph and
connected to your system's audio output. Instantiating audio nodes
is another operation that takes a variable amount of time and we
thus express moments of synchronization and timing through
scene and anode methods like Sync()
and Wait()
.
// play 10 notes
let scene = await Ascene.BeginFiber(this);
let inst = scene.NewAnode("Vital"); // request creation of Vital synth node
let out = scene.GetDAC();
let notedur = scene.Seconds(.25);
let asap = 0;
scene.Chain(inst, out); // describe our tiny audio graph
await scene.Sync(); // after this completes our nodes are "live"
for(let key=40;key<60;key+=2)
{
inst.NoteOn(key, .9, asap);
await scene.Wait(notedur); // <----
inst.NoteOff(key, 1, asap);
}
MIDI devices produce events at arbitrary points in time. Async comes to the rescue in this example. See MIDI API for more.
let scene = await Ascene.BeginFiber(this); // <-- async wait
let inst = scene.NewAnode("Surge XT");
let out = scene.GetDAC()
scene.Chain(inst, out);
await scene.Sync();
let midiInput = new MidiIn(0/*port*/);
let ok = await midiInput.Open(); // <--- async wait for open to succeed
while(true)
{
let data = await midiInput.Recv(); // <-- async trigger on each midi event
midiInput.Perform(inst, data); // parse data and trigger anode methods.
let msg = yield; // watch for cancellation request
if(msg == "_cancel_") break;
}
console.log("Done");
Loading files from your local drive and especially the internet is time-consuming and we need async behavior to ensure our scripts don't lock-up Hz. Currently the Loader API can asynchronously read and return files from your current workspace and also cache internet files there. See Loader API for more.
// no need for audio engine in this example
// json is a very convenient format for preset files.
let json = await FetchWSFile("my/file.json", "json");
if(json)
{
// here obj contents depend upon the file
console.log(JSON.stringify(json, null, 2));
}
Your scripts can create additional "execution fibers" using Async.Run()
.
Here we see 10 loops of a run of notes. Each time the run starts
we run another function (myfunc
) that independently loops invoking
anode.SetParam
repeatedly.
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 and watch osc1 pan update
let loop = 0;
let notedur = scene.Seconds(.25);
let myfunc = async () => // must be async 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);
for(let key=40;key<60;key+=2)
{
inst.NoteOn(key, .9/*velocity*/, asap);
await scene.Wait(notedur); // <--- block
inst.NoteOff(key, 1, asap);
yield;
}
}
You scripts can asynchronously run your own or other Music API
functions. Here's a local async generator
function and the
magic JavaScript syntax to iterate over its results. Because
our function is async, it can invoke await inside. And because
it's a generator function (indicated by *
doloop) it can yield
control to its caller.
async function *doloop(constantNote=0)
{
let noteDur = scene.Seconds(.1);
let restDur = scene.Seconds(.10);
let note = constantNote || 40 + Math.floor(12*Math.random());
for(let i=0;i<10;i++)
{
// console.log("note " + i);
inst.NoteOn(note, .9);
await scene.Wait(noteDur);
inst.NoteOff(note, .9);
await scene.Wait(restDur);
yield;
if(constantNote != note)
note++;
}
}
// interate our async function
for await (const val of doloop())
{
yield;
}
console.log("Let's try FiberMgr");
// We must pass an iterator to AddFiber.
// Fibers provide more feedback than Async.Run.
// They can also be canceld.
FiberMgr.AddFiber(doloop(), {id: 10, name: "subfiber"});
// another interation over our local async function.
for await (const val of doloop())
{
yield; // here we yield to system, this updates AudioStatus/Fibers
}
console.log("generate.js done.");
The Songbook runtime provides one such function. Here's snippet from a larger example.
for await (const v of song.Perform(scene, anodeMap))
{
yield;
}
Function | Description |
---|---|
Async.Run(fn) |
Run fn in a new unmonitored subfiber. |
Async.Subscribe(key, nn) * |
Run fn when events associated with key occur. Returns a subid to use to Unsubscribe. |
Async.Unsubscribe(key, subid) * |
Unsubscribe from events associated with key |
Async.Request(key, ...) * |
Make a one-off request associated with key and optional args. Returns a Promise. |
*
mostly used by MusicAPI components.
Function | Description |
---|---|
FiberMgr.AddFiber(fn, {name, id}) |
Create a new monitored subfiber with a unique id. fn must be an async generator function . |
FiberMgr.CancelFiber(id) |
Cancel the identified subfiber |
a |
To make the module available to your scripts:
local async = require("async")
Function | Description |
---|---|
async.Validate(fileref [1]) |
returns a Promise for referring to local file. |
async.ValidateURL(urlbase, relpath, cachedir [1], cachebase) |
returns a Promise for reading a remote file. This method only downloads the remote file (via http GET ). To read it, chain it with Read() |
async.Read(fileref [1]) |
returns a Promise for reading a local file. |
async.Wait({..promises..}) [2] |
blocks until either all inputs resolved or fail. Returns a list of fulfilled results. |
async.New() [3] |
returns a newly minted promise that can be fulfilled by miscellaneous application state transitions. |
async.Ping() |
returns a Promise that will resolve presently. This can be useful to present a uniform interface to potentially cacheable operations. |
1
IO param supports prefix ${WS}
, ${HOME}
, ${ENVVAR}
2
method only available within a coroutine context
3
method available for a static set of application events
IO param | Description |
---|---|
subdir | optional param to guide validation, names a subdir within resource searchpaths |
urlbase | http or https URL to a repository of files |
relpath | a relative path below urlbase used to both produce the remote URL as well the local file reference. |
cachedir | a cache directory on the local filesystem. Can be a fully qualfied path or a special prefix. |
cachebase | a cache directory below cachedir helps to key your cache well-organized. |
These methods are available on a promise after its creation. In common+simple usage (ie either via async.Wait or promise:then) you can access results via the starred (*) methods below.
Method | Description |
---|---|
p:getid() |
returns the unique integer id for this promise. |
p:getstate() |
returns one of "pending", "fulfilled", "rejected" |
p:next(callback) |
invokes callback when resolved, not needed if using async.Wait on one or more Promises |
p:getpath() * |
returns the pathname to resolved file. |
p:getdata() * |
returns the data read by Read(), may return nil |
*
valid only when getstate()
returns "fulfilled"
Use these methods when advanced command and control is required.
Method | Description |
---|---|
p:then(cb) |
schedules cb (function accepting promise result) |
p:catch(cb) |
schedules error handling cb (function accepting reject reason) |
p:resolve(value) |
signals the fulfillment of promise p |
p:reject(reason) |
signals the error for promise p |
Validate the presence on the local filesystem of two files and resolve them to fully qualified names if present.
local async = require("async")
function doit()
local requests = {
async.Validate("${WS}/samples/file1.wav"),
async.Validate("${WS}/samples/file2.aiff")
}
async.Wait(requests) -- <-- wait for results
Log.notice("sample files resolved to ")
for _,v in ipairs(requests) do
Log.notice(string.format(" %s %s", v.getpath(), v.getstate()))
end
end
-- async.Wait is valid only in coroutine contexts
App.performCoroutine("validate", doit)
This example shows how the results of once async operation (ValidateURL) can be chained with another (Read).
Log.info("async/testdata begin");
local base = "https://raw.githubusercontent.com/tidalcycles/Dirt-Samples/master"
local cacheNS = "github/tidalcycles/Dirt-Samples" -- namespace in cache
local cachedir = "${WS}/_samplecache"
local wavfile = "808bd/BD0000.WAV";
-- step 1: make sure the remote file is cached locally
async.ValidateURL(base, wavfile, cachedir, cacheNS)
:next(function(p)
-- step 2: asynchronously read the contents of results of step 1.
Log.info("validated, now read "..p:getpath())
return async.Read(p:getpath())
end)
:next(function(p)
-- step 3, do something with the data
local data = p:getdata()
if data then
Log.info(string.format("data received, size %d bytes", #data))
Log.info("async/testdata end");
else
Log.warning("no data available")
end
end)
:catch(function(reason)
-- we arrive here if either step 1, 2 or 3 fail
Log.error("async/testdata error: "..reason)
end)
Read
is read in binary mode and stored in a lua string
,
see section 21.2.2 of PIL for
more details on manipulating binary data in lua.