Assets and bundles
Asset-required effects
FIRCrossover, FiveBandFIRPEQ, GroupDelayEQ, IRReverb, and RoomEQ
require assets.impulseResponse. FIR filter effects require prepared coefficients at
the processing rate; IR Reverb accepts supported convolution topologies. The resolver
returns Python AssetData or deterministic ETA1 bytes/{bytes, format} in
JavaScript. It never relies on repository fixtures.
The examples below run each effect with two distinct caller-owned IRs and require finite, non-zero, different output. FIR Crossover uses two coefficient channels, accepts stereo in processing lanes 0 and 1, and routes its two bands to four output lanes.
Bundle v1 combines Chain JSON with external ETA1 manifests. Python
Bundle.pack() writes a deterministic directory bundle, and
effetune bundle pack CHAIN DESTINATION --asset ID=FILE provides the same writer for
decoded IR audio files. Payloads are SHA-256 and size checked, with a 32 MiB cap. ETA1
is little-endian planar float32 with a 32-byte header, optional 12-byte matrix paths,
and explicit rate/channel/topology metadata. ZIP is not part of Bundle v1.
Programmatic and CLI bundle writing:
import json
from pathlib import Path
import numpy as np
import soundfile as sf
import effetune as et
chain = {
"version": 1,
"chain": [{
"id": "room",
"type": "IRReverb",
"parameters": {
"channelMode": "mono",
"latency": 0,
"convolutionRate": "full",
"wetLevel": 0,
"dryLevel": -96,
"preDelay": 0,
},
"assets": {"impulseResponse": "room-ir"},
}],
}
ir_samples = np.array([[0.75, 0.2]], dtype=np.float32)
ir = et.AssetData(ir_samples, 48_000, topology="automatic")
bundle = et.Bundle.pack("python-bundle", chain, {"room-ir": ir})
loaded = et.Bundle.load("python-bundle")
assert loaded.manifest == bundle.manifest
source = np.zeros((2, 1024), dtype=np.float32)
source[:, 0] = (0.5, -0.25)
output = et.Chain.from_bundle("python-bundle").process(
source,
sample_rate=48_000,
block_size=64,
)
assert np.isfinite(output).all()
assert np.max(np.abs(output)) > 1e-7
Path("room-chain.json").write_text(
json.dumps(chain, indent=2) + "\n",
encoding="utf-8",
)
sf.write("room-ir.wav", ir_samples.T, 48_000, subtype="FLOAT")
sf.write("input.wav", source.T, 48_000, subtype="FLOAT")
effetune bundle pack room-chain.json cli-bundle \
--asset room-ir=room-ir.wav
effetune render input.wav convolved.wav --preset cli-bundle --subtype FLOAT
--preset cli-bundle/bundle.json is equivalent to the Bundle-directory form above.
For WAV output, omitting --subtype keeps SoundFile’s PCM_16 default; use
--subtype FLOAT when the rendered samples must remain 32-bit floating point.
Python AssetData examples for all five types:
import numpy as np
import effetune as et
SAMPLE_RATE = 48_000
COEFFICIENTS = {
"a": (
np.array([0.75, 0.2], dtype=np.float32),
np.array([0.35, -0.1], dtype=np.float32),
),
"b": (
np.array([0.4, -0.3, 0.1], dtype=np.float32),
np.array([0.8, 0.1, -0.05], dtype=np.float32),
),
}
for effect_type in (
"FIRCrossover",
"FiveBandFIRPEQ",
"GroupDelayEQ",
"IRReverb",
"RoomEQ",
):
channels = 4 if effect_type == "FIRCrossover" else 2
source = np.zeros((channels, 4096), dtype=np.float32)
source[0, 0] = 0.5
source[1, 0] = -0.25
outputs = []
for ir_variant in ("a", "b"):
coefficients = COEFFICIENTS[ir_variant]
if effect_type == "FIRCrossover":
frames = max(len(channel) for channel in coefficients)
samples = np.zeros((2, frames), dtype=np.float32)
for index, channel in enumerate(coefficients):
samples[index, : len(channel)] = channel
asset = et.AssetData(
samples,
SAMPLE_RATE,
topology="matrix",
paths=(
et.ConvolutionPath(0, 0, 0),
et.ConvolutionPath(1, 1, 0),
et.ConvolutionPath(0, 2, 1),
et.ConvolutionPath(1, 3, 1),
),
input_count=2,
)
parameters = {
"latencyMode": "0",
"filterDelaySamples": 0,
"bandCount": 2,
}
else:
asset = et.AssetData(
coefficients[0][np.newaxis, :],
SAMPLE_RATE,
topology="mono",
)
parameters = (
{
"channelMode": "mono",
"latency": 0,
"convolutionRate": "full",
"wetLevel": 0,
"dryLevel": -96,
"preDelay": 0,
}
if effect_type == "IRReverb"
else {"latencyMode": "0", "filterDelaySamples": 0}
)
chain = et.Chain.from_preset(
{
"version": 1,
"chain": [{
"id": effect_type,
"type": effect_type,
"parameters": parameters,
"assets": {"impulseResponse": f"memory:{effect_type}"},
}],
},
asset_resolver=lambda _reference, resolved=asset: resolved,
)
output = chain(source, SAMPLE_RATE, seed=0, block_size=64)
assert output.shape == source.shape
assert np.isfinite(output).all()
assert np.max(np.abs(output)) > 1e-7
outputs.append(output)
assert np.max(np.abs(outputs[0] - outputs[1])) > 1e-5
Use the public encodeEta1() helper to encode deterministic JavaScript IR data:
import { encodeEta1 } from '@effetune/dsp';
export const ASSET_EFFECT_TYPES = Object.freeze([
'FIRCrossover',
'FiveBandFIRPEQ',
'GroupDelayEQ',
'RoomEQ',
'IRReverb'
]);
const COEFFICIENTS = Object.freeze({
a: Object.freeze([
Float32Array.of(0.75, 0.2),
Float32Array.of(0.35, -0.1)
]),
b: Object.freeze([
Float32Array.of(0.4, -0.3, 0.1),
Float32Array.of(0.8, 0.1, -0.05)
])
});
export function assetSetup(effect, sampleRate = 48000, irVariant = 'a') {
if (!effect.assets.length) {
return { channels: 2, references: undefined, assetResolver: undefined };
}
if (!ASSET_EFFECT_TYPES.includes(effect.type)) {
throw new Error(`No canonical asset fixture for ${effect.type}.`);
}
const coefficients = COEFFICIENTS[irVariant];
if (!coefficients) throw new Error(`Unknown IR variant: ${irVariant}.`);
const crossover = effect.type === 'FIRCrossover';
const bytes = crossover
? encodeEta1({
channels: coefficients,
sampleRate,
topology: 'matrix',
paths: [
{ inputSlot: 0, outputSlot: 0, irChannel: 0 },
{ inputSlot: 1, outputSlot: 1, irChannel: 0 },
{ inputSlot: 0, outputSlot: 2, irChannel: 1 },
{ inputSlot: 1, outputSlot: 3, irChannel: 1 }
]
})
: encodeEta1({
channels: [coefficients[0]],
sampleRate,
topology: 'mono'
});
const parameters = effect.type === 'IRReverb'
? {
channelMode: 'mono',
latency: 0,
convolutionRate: 'full',
wetLevel: 0,
dryLevel: -96,
preDelay: 0
}
: effect.type === 'FIRCrossover'
? { latencyMode: '0', filterDelaySamples: 0, bandCount: 2 }
: { latencyMode: '0', filterDelaySamples: 0 };
return {
channels: crossover ? 4 : 2,
parameters,
references: Object.fromEntries(effect.assets.map(asset => [
asset.name,
`memory:${effect.type}:${asset.name}`
])),
assetResolver: () => bytes
};
}
Then run the JavaScript resolver examples for all five types:
import assert from 'node:assert/strict';
import { createChain, getEffectCatalog } from '@effetune/dsp';
import { ASSET_EFFECT_TYPES, assetSetup } from './asset-fixtures.mjs';
const sampleRate = 48000;
const effects = getEffectCatalog().effects.filter(effect => effect.assets.length);
assert.deepEqual(effects.map(effect => effect.type), ASSET_EFFECT_TYPES);
for (const variant of ['baseline', 'simd']) {
for (const effect of effects) {
const inputChannels = effect.type === 'FIRCrossover' ? 4 : 2;
const input = Array.from({ length: inputChannels }, (_, channelIndex) => {
const channel = new Float32Array(4096);
if (channelIndex < 2) channel[0] = channelIndex === 0 ? 0.5 : -0.25;
return channel;
});
const outputs = [];
for (const irVariant of ['a', 'b']) {
const {
channels,
parameters,
references,
assetResolver
} = assetSetup(effect, sampleRate, irVariant);
assert.equal(channels, inputChannels);
const chain = await createChain({
version: 1,
chain: [{
id: effect.type,
type: effect.type,
parameters,
assets: references
}]
}, { variant, assetResolver });
const output = await chain.process(input, {
sampleRate,
seed: 0,
blockSize: 64
});
assert.equal(output.length, input.length);
assert.ok(output.every(channel =>
channel.length === input[0].length && channel.every(Number.isFinite)
));
assert.ok(output.some(channel =>
channel.some(sample => Math.abs(sample) > 1e-7)
));
outputs.push(output);
chain.close();
}
let maximumDifference = 0;
for (let channel = 0; channel < outputs[0].length; channel += 1) {
for (let frame = 0; frame < outputs[0][channel].length; frame += 1) {
maximumDifference = Math.max(
maximumDifference,
Math.abs(outputs[0][channel][frame] - outputs[1][channel][frame])
);
}
}
assert.ok(maximumDifference > 1e-5);
}
}