REF / Music API / Async

Ascene . Agraph . Aengine
Anode . Modulator . Envelope
Songbook . Song . TimeKeeper
Voices . SampleMgr . StreamMgr
Loader . Async . Plot
Random . Rhythm . Util
MIDI . OSC . HID . Piano
Music Theory with Tonal

right-click to navigate to page sections


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's coroutines and JavaScript's async functions and Promises. It's also loosely related to operating system threads.

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.

await

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());

yield

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.

Async Applications

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.

Audio Engine Control

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.

Anode Control

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 Event Handling

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");

Async I/O

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));
}

Async.Run()

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;
    }
} 

Async Generator Functions

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;
}

JavaScript Async API

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.

JavaScript FiberMgr API

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

Lua Async API

To make the module available to your scripts:

local async = require("async")

Functions

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.

async.Promise data access

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"

async.Promise control

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

Example 1 - resolving files

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) 

Example 2 - promise chaining in lua

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)

Implementation details

home .. topics .. interface .. reference .. examples .. tipjar