Docs
Packages
@elemaudio/offline-renderer

@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 0
  • numOutputChannels: number – default 2
  • sampleRate: number – default 44100
  • blockSize: number – default 512
  • virtualFileSystem: 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.