Virtual File System (VFS)
Elementary uses the notion of a virtual file system to coordinate audio processing nodes
which have dependencies on large, static shared resources. A common example is the el.sample
node which manages playback of an audio sample, or el.table
which performs an interpolated
read from a lookup table. Using the virtual file system enables internal shared memory optimizations
and allows the audio processing nodes to remain lightweight.
While we refer to this feature as a virtual "file system," it's really just a flat storage object which maps from an arbitrary string name (key) to a particular resource (value).
Loading data into the VFS
The process for loading data into the virtual file system depends on how you're using Elementary, although the concept is the same everywhere: you add key-value entries to the virtual file system map, where the key is a "virtual file path" or simply a name of your choosing, and the value is the resource, and then you build your audio graph by referencing the associated name or virtual file path of the buffer you want to use.
If you're working strictly in JavaScript with one of the provided renderers, consult the documentation for your specific renderer on adding entries to the VFS.
If you're working with the C++ API, see elem::Runtime<FloatType>::addSharedResource
. For
your custom integrations, it may be worth establishing a dedicated message or FFI to allow your scripting environment to make calls to this native
method. This is exactly how the provided renderers offer their VFS APIs.
Reading data from the VFS
Again the details differ here depending on your use case. In most cases, however, you will simply read from the VFS by giving relevant nodes the right name or virtual file system path to perform the lookup themselves. For example, suppose that we've loaded a particular buffer into the map with the name "sample0":
// In the case of the web-renderer, for example we might use Web APIs to fetch and decode the
// audio buffer data, then `updateVirtualFileSystem` to load it in
let res = await fetch('https://ia800407.us.archive.org/9/items/999WavFiles/10.mp3');
let sampleBuffer = await ctx.decodeAudioData(await res.arrayBuffer());
core.updateVirtualFileSystem({
'sample0': [
sampleBuffer.getChannelData(0),
sampleBuffer.getChannelData(1),
],
});
or equivalently:
auto resource = std::make_unique<elem::AudioBufferResource>(audioChannelData, numChannels, numSamples);
auto result = runtime->addSharedResource("sample0", std::move(resource));
Now we should be ok to render an audio graph which references sample0
from one of the appropriate nodes:
// In our JavaScript environment somewhere
core.render(el.sample({path: 'sample0'}, 1, 1));
Inspecting
The listVirtualFileSystem()
method on the web and offline renderers, or equivalently the getSharedResourceMapKeys()
method on the elem::Runtime<FloatType>
interface,
will list by name the contents of shared map. This API intentionally does not share access to the underlying buffers to ensure immutability and thread safety.
Pruning
The pruneVirtualFileSystem()
method on the web and offline renderers, or equivalently the pruneSharedResourceMap()
method on the elem::Runtime<FloatType>
interface,
will remove contents from the shared map that are not actively being used by any of the processing graph.
With Custom Native Nodes
Lastly, if you find yourself needing a custom native node with a dependency on the virtual file system, you
can access the shared buffer map by overriding elem::GraphNode<FloatType>::setProperty
. The GraphNode API provides
two different setProperty
overloads, one of which carries a reference to Elementary's internal SharedResourceMap<FloatType>& resources
.
From there you'll be able to grab a pointer to the audio buffer you need by name.