Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
[submodule "runtime/elem/third-party/signalsmith-stretch"]
path = runtime/elem/third-party/signalsmith-stretch
url = https://github.com/Signalsmith-Audio/signalsmith-stretch.git
[submodule "runtime/elem/third-party/choc"]
path = runtime/elem/third-party/choc
url = https://github.com/Tracktion/choc.git
87 changes: 54 additions & 33 deletions js/packages/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,35 @@
import {
renderWithDelegate,
} from './src/Reconciler.gen';
import { renderWithDelegate } from "./src/Reconciler.gen";

import { updateNodeProps } from './src/Hash';
import { updateNodeProps } from "./src/Hash";

import {
createNode,
isNode,
resolve,
unpack
} from './nodeUtils';
import { createNode, isNode, resolve, unpack } from "./nodeUtils";

import * as co from './lib/core';
import * as dy from './lib/dynamics';
import * as en from './lib/envelopes';
import * as fi from './lib/filters';
import * as ma from './lib/math';
import * as mc from './lib/mc';
import * as os from './lib/oscillators';
import * as si from './lib/signals';
import * as co from "./lib/core";
import * as dy from "./lib/dynamics";
import * as en from "./lib/envelopes";
import * as ev from "./lib/event-nodes";
import * as fi from "./lib/filters";
import * as ma from "./lib/math";
import * as mc from "./lib/mc";
import * as os from "./lib/oscillators";
import * as si from "./lib/signals";

export type { ElemNode, NodeRepr_t } from './nodeUtils';
export { default as EventEmitter } from './src/Events';
export type { ElemNode, NodeRepr_t } from "./nodeUtils";
export { default as EventEmitter } from "./src/Events";

const stdlib = {
...co,
...dy,
...en,
...ev,
...fi,
...ma,
...os,
...si,
mc,
// Aliases for reserved keyword conflicts
"const": co.constant,
"in": ma.identity,
const: co.constant,
in: ma.identity,
};

const InstructionTypes = {
Expand Down Expand Up @@ -80,7 +75,9 @@ class Delegate {
};
}

getNodeMap() { return this.nodeMap; }
getNodeMap() {
return this.nodeMap;
}

createNode(hash, type) {
this.nodesAdded++;
Expand All @@ -89,12 +86,22 @@ class Delegate {

appendChild(parentHash, childHash, childOutputChannel) {
this.edgesAdded++;
this.batch.appendChild.push([InstructionTypes.APPEND_CHILD, parentHash, childHash, childOutputChannel]);
this.batch.appendChild.push([
InstructionTypes.APPEND_CHILD,
parentHash,
childHash,
childOutputChannel,
]);
}

setProperty(hash, key, value) {
this.propsWritten++;
this.batch.setProperty.push([InstructionTypes.SET_PROPERTY, hash, key, value]);
this.batch.setProperty.push([
InstructionTypes.SET_PROPERTY,
hash,
key,
value,
]);
}

activateRoots(roots) {
Expand All @@ -103,7 +110,8 @@ class Delegate {
// because it may be that we're asked to activate a subset of the current
// active roots, in which case we need the instruction to prompt the engine
// to deactivate the now excluded roots.
let alreadyActive = roots.length === this.currentActiveRoots.size &&
let alreadyActive =
roots.length === this.currentActiveRoots.size &&
roots.every((root) => this.currentActiveRoots.has(root));

if (!alreadyActive) {
Expand All @@ -129,7 +137,7 @@ class Delegate {

// A quick shim for platforms which do not support the `performance` global
function now() {
if (typeof performance === 'undefined') {
if (typeof performance === "undefined") {
return Date.now();
}

Expand Down Expand Up @@ -173,11 +181,13 @@ class Renderer {
// In other words, don't share refs between different renderer instances.
createRef(kind, props, children) {
let key = `__refKey:${this._nextRefId++}`;
let node = createNode(kind, Object.assign({key}, props), children);
let node = createNode(kind, Object.assign({ key }, props), children);

let setter = (newProps) => {
if (!this._delegate.nodeMap.has(node.hash)) {
throw new Error('Cannot update a ref that has not been mounted; make sure you render your node first')
throw new Error(
"Cannot update a ref that has not been mounted; make sure you render your node first",
);
}

const nodeMapCopy = this._delegate.nodeMap.get(node.hash);
Expand All @@ -196,14 +206,25 @@ class Renderer {
}

render(...args) {
return this.renderWithOptions({ rootFadeInMs: 20, rootFadeOutMs: 20 }, ...args);
return this.renderWithOptions(
{ rootFadeInMs: 20, rootFadeOutMs: 20 },
...args,
);
}

renderWithOptions(options: { rootFadeInMs: number, rootFadeOutMs: number }, ...args) {
renderWithOptions(
options: { rootFadeInMs: number; rootFadeOutMs: number },
...args
) {
const t0 = now();

this._delegate.clear();
renderWithDelegate(this._delegate as any, args.map(resolve), options.rootFadeInMs, options.rootFadeOutMs);
renderWithDelegate(
this._delegate as any,
args.map(resolve),
options.rootFadeInMs,
options.rootFadeOutMs,
);

const t1 = now();

Expand Down Expand Up @@ -238,5 +259,5 @@ export {
renderWithDelegate,
resolve,
stdlib,
unpack
unpack,
};
94 changes: 94 additions & 0 deletions js/packages/core/lib/event-nodes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import {
createNode,
resolve,
ElemNode,
NodeRepr_t,
unpack,
} from "../nodeUtils";

/**
* A simple identity function for realtime block events.
*
* Expects no props and no children, though it may accept children as
* a way to merge and propagate other event streams.

* @returns {NodeRepr_t}
*/
export function midinotein(...args: Array<ElemNode>): NodeRepr_t {
return createNode("midinotein", {}, args.map(resolve));
}

/**
* Polyphonic note allocation node with LRU voice allocation and stealing.
*
* This uses MPE style voice allocation, remapping the incoming note events
* onto channels 0-15, corresponding to (up to) 16 voices.
*
* @param {Object} props
* @param {string} [props.key] - An optional unique identifier for the node
* @param {number} [props.voices] - Number of voices to allocate (defaults to 16)
* @param {...ElemNode} children - Child nodes to process
* @returns {NodeRepr_t}
*/
export function midinoteallocate(
props: { key?: string; voices?: number },
...children: Array<ElemNode>
): NodeRepr_t {
return createNode("midinoteallocate", props, children.map(resolve));
}

/**
* Emits audio signals reflecting the frequency and velocity information
* of the incoming MIDI events.
*
* Unpacks the incoming MIDI note event stream into two separate outputs:
* - Channel 0: Frequency (Hz)
* - Channel 1: Velocity (normalized 0-1)
*
* @param {Object} props
* @param {string} [props.key] - An optional unique identifier for the node
* @param {number} [props.channel] - Filter incoming events, react only to those that match the channel number
* @param {...ElemNode} children - MIDI note event stream(s) to unpack
* @returns {Array<NodeRepr_t>} An array of two nodes: [frequency, velocity]
*/
export function midinoteunpack(
props: { key?: string; channel?: number },
...children: Array<ElemNode>
): [NodeRepr_t, NodeRepr_t] {
return unpack(
createNode("midinoteunpack", props, children.map(resolve)),
2,
) as [NodeRepr_t, NodeRepr_t];
}

/**
* Shifts MIDI note values by a specified amount.
*
* Transposes incoming MIDI note events by adding or subtracting a fixed offset
* to the note number. Useful for octave shifting or transposition effects.
*
* @param {Object} props
* @param {string} [props.key] - An optional unique identifier for the node
* @param {number} props.steps - Number of steps (semitones) to shift the MIDI note numbers
* @param {...ElemNode} children - MIDI note event stream(s) to shift
* @returns {NodeRepr_t}
*/
export function midinoteshift(
props: { key?: string; steps: number },
...children: Array<ElemNode>
): NodeRepr_t {
return createNode("midinoteshift", props, children.map(resolve));
}

/**
* Emits audio rate signals carrying the current value of the parameter
* identified by the given parameter index.
*
* @param {Object} props
* @param {string} [props.key] - An optional unique identifier for the node
* @param {number} props.index - Parameter index for which to follow events
* @returns {NodeRepr_t}
*/
export function param(props: { key?: string; index: number }): NodeRepr_t {
return createNode("param", props, []);
}
74 changes: 74 additions & 0 deletions js/packages/offline-renderer/__tests__/block-events.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import OfflineRenderer from "..";
import { el } from "@elemaudio/core";

const repeat = (n, x) => Array.from({ length: n }).fill(x);
const take = (x, n) => x.slice(0, n);
const round = (x) => [...x.map(Math.round)];

test("midi events", async function () {
let core = new OfflineRenderer();

await core.initialize({
numInputChannels: 0,
numOutputChannels: 2,
sampleRate: 44100,
blockSize: 128,
});

// Graph
core.render(...el.midinoteunpack({}));

// Ten blocks of data
let inps = [];
let outs = [new Float32Array(128), new Float32Array(128)];

// Get past the fade-in
for (let i = 0; i < 20; ++i) {
core.process(inps, outs);
}

// Now we push some events and study the outputs
core.pushMidiEvent(0, new Uint8Array([0x90, 60, 127]));
core.process(inps, outs);

expect(round(take(outs[0], 8))).toMatchObject(repeat(8, 262));
expect(round(take(outs[1], 8))).toMatchObject(repeat(8, 1));

// On the next block we should see that the note is still held
core.process(inps, outs);
expect(round(take(outs[0], 8))).toMatchObject(repeat(8, 262));
expect(round(take(outs[1], 8))).toMatchObject(repeat(8, 1));

// Now we'll push a note-off 4 samples into the next block
core.pushMidiEvent(4, new Uint8Array([0x80, 60, 0]));
core.process(inps, outs);
expect(round(take(outs[0], 8))).toMatchObject(repeat(8, 262));
expect(round(take(outs[1], 8))).toMatchObject([1, 1, 1, 1, 0, 0, 0, 0]);
});

test("param value events", async function () {
let core = new OfflineRenderer();

await core.initialize({
numInputChannels: 0,
numOutputChannels: 1,
sampleRate: 44100,
blockSize: 128,
});

// Graph
core.render(el.param({ index: 0 }));

// Data
let inps = [];
let outs = [new Float32Array(128)];

// Get past the fade-in
for (let i = 0; i < 20; ++i) {
core.process(inps, outs);
}

core.pushParamValueEvent(4, 0, 15.0);
core.process(inps, outs);
expect([...take(outs[0], 8)]).toMatchObject([0, 0, 0, 0, ...repeat(4, 15)]);
});
2 changes: 1 addition & 1 deletion js/packages/offline-renderer/elementary-wasm.cjs

Large diffs are not rendered by default.

Loading
Loading