@elemaudio/offline-renderer
The official package for rendering Elementary applications offline, whether in Node.js or in the browser. Often this is used for file-based processing (reading files, processing them, and writing them to disk), though the actual encoding/decoding to and from file is not handled here, just the audio processing.
Installation
npm install --save @elemaudio/offline-renderer
Example
import {el} from '@elemaudio/core';
import OfflineRenderer from '@elemaudio/offline-renderer';
(async function main() {
let core = new OfflineRenderer();
await core.initialize({
numInputChannels: 0,
numOutputChannels: 1,
sampleRate: 44100,
});
// Our sample data for processing.
//
// We are not expecting any inputs, hence the empty array, but we are expecting
// an output buffer to write into.
let inps = [];
let outs = [new Float32Array(44100 * 10)]; // 10 seconds of output data
// Render our processing graph
core.render(el.cycle(440));
// Pushing samples through the graph. After this call, the buffer in `outs` will
// have the desired output data which can then be saved to file if you like.
core.process(inps, outs);
})();
Usage
import Offline from '@elemaudio/offline-renderer';
Constructor
let core = new OfflineRenderer();
No arguments provided; you can construct multiple OfflineRenderers and use them however you like, even to generate buffers that will be loaded into other OfflineRenderers.
initialize
core.initialize(options: Object): Promise<void>
Initializes the Elementary offline runtime. In most other Elementary applications you'll rely on listening
for the "load" event to fire after initialization. With the OfflineRenderer you can be sure that after initialize
resolves, the runtime is ready to render.
The options object expects the following properties:
numInputChannels: number
– default 0numOutputChannels: number
– default 2sampleRate: number
– default 44100blockSize: number
– default 512virtualFileSystem: Object<string, Array<number>|Float32Array>
– default{}
Note the difference here in the initialize
API between the offline-renderer
and the web-renderer
: when initializing
the web-renderer
we must use the processorOptions
field to propagate the virtualFileSystem
argument due to the nature of
Web Audio Worklets. Here, for the offline-renderer
, we have no such constraint, thus we can pass the virtualFileSystem
as a
top-level entry on the initialization object.
render
core.render(...args: Array<NodeRepr_t | number>) : Promise<RenderStats>;
Performs the reconciliation process for rendering your desired audio graph. This method expects one argument
for each available output channel. That is, if you want to render a stereo graph, you will invoke this method
with two arguments: core.render(leftOut, rightOut)
.
The RenderStats
object returned by this call provides some insight into what happened during the reconciliation
process: how many new nodes and edges were added to the graph, how long it took, etc.
createRef
core.createRef(kind: string, props: Object<string, any>, children: Array<ElemNode>): [NodeRepr_t, (props) => Promise<void>]
Creates a pair of [node, propertySetter]. The node can be used like a regular ElemNode in your graph construction, and the propertySetter can be used thereafter to set the node's properties without incurring a full graph render. Note that you must mount the node (i.e. pass it, in some graph layout, through the Renderer's render method) before invoking the property setter will work.
The arguments to createRef
are the same as when dealing with the el.*
standard library nodes, except that here we name the desired node in the first string argument. The props argument is as usual, and any child nodes of created ref node should be passed as an array in the third children
argument.
// The following expressions create equivalent nodes, but the latter provides the ref
// with the property setter function
el.svf({mode: 'lowpass'}, 800, 1, inputNode);
createRef('svf', {mode: 'lowpass'}, [el.const({value: 800}), el.const({value: 1}), inputNode]);
See Using Refs.
updateVirtualFileSystem
core.updateVirtualFileSystem(Object<string, Array | Float32Array>);
Use this method to dynamically update the buffers available in the virtual file system after initialization. See the Virtual File System section below for more details.
Note: overwriting existing entries is not supported. This method should be used only to add new entries to the virtual file system. If you need to clear old, unused entries, see pruneVirtualFileSystem
below.
listVirtualFileSystem
core.listVirtualFileSystem(): Array<string>;
Lists the entries in the virtual file system by name.
pruneVirtualFileSystem
core.pruneVirtualFileSystem(): void;
Removes unused entries from the virtual file system.
setCurrentTime
core.setCurrentTime(t): void;
Sets the current engine time to t
, given in samples. This immediately changes the output of any el.time()
node in the graph.
setCurrentTimeMs
core.setCurrentTime(t): void;
Sets the current engine time to t
, given in milliseconds. This immediately changes the output of any el.time()
node in the graph.
reset
core.reset(): void;
Resets internal nodes and buffers back to their initial state.
Events
Each OfflineRenderer
instance is itself an event emitter with an API matching that of the Node.js Event Emitter (opens in a new tab)
class.
The renderer will emit events from underlying audio processing graph for nodes such as el.meter
, el.snapshot
, etc. See
the reference documentation for each such node for details.
Virtual File System
When running offline, either in a web browser or in Node.js, the Elementary runtime has no access to your file system or network itself.
Therefore, when writing graphs which rely on sample data (such as with el.sample
, el.table
, or el.convolve
),
you must first load the sample data into the runtime using the virtual file system.
If you know your sample data ahead of time, you can load the virtual file system at initialization time using the
virtualFileSystem
property as follows.
await core.initialize(ctx, {
numInputChannels: 0,
numOutputChannels: 2,
virtualFileSystem: {
'/your/virtual/file.wav': (new Float32Array(512)).map(() => Math.random()),
}
});
After configuring the core processor this way, you may use el.sample
or any other node which
reads from file by referencing the corresponding virtual file path that you provided:
core.render(el.sample({path: '/your/virtual/path.wav'}, el.train(1)))
If you need to dynamically update the virtual file system after initialization, you may do so
using the updateVirtualFileSystem
method.
await core.initialize({
numInputChannels: 0,
numOutputChannels: 1,
});
let inps = [];
let outs = [new Float32Array(44100 * 10)]; // 10 seconds of output data
// Render our processing graph
core.render(el.cycle(440));
// Pushing samples through the graph. After this call, the buffer in `outs` will
// have the desired output data which can then be saved to file if you like.
core.process(inps, outs);
// Perhaps now we have some new sample data that needs to be loaded, maybe in response
// to an HTTP request (though admittedly it might be both easier and better to just make
// a new OfflineRenderer if you're doing something like HTTP request handling!)
core.updateVirtualFileSystem({
'/some/new/arbitrary/fileName.wav': myNewSampleData,
});
// In this example, after performing the update, we can now `render()` a new graph which references
// our new file data.
core.render(el.sample({path: '/some/new/arbitrary/fileName.wav'}, el.train(1)))
// Process again; this will overwrite what's already in `out`
core.process(inps, outs);
Note: Each virtual file system entry maps to a single channel of audio data. To load multi-channel sample data into the virtual file system, you should enumerate each channel as a differently named virtual file path.
For more information, see Virtual File System.