Documentation

Assets and bundles

Asset-required effects

CrosstalkCancellation, FIRCrossover, FiveBandFIRPEQ, GroupDelayEQ, GroupDelayPEQ, 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.

Crosstalk Cancellation requires four prepared filter channels in trueStereo topology (LL, LR, RL, RR) at the processing sample rate and exactly two selected processing channels. Automatic topology is accepted for this four-channel asset.

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 six 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 (
    "CrosstalkCancellation",
    "FIRCrossover",
    "FiveBandFIRPEQ",
    "GroupDelayEQ",
    "GroupDelayPEQ",
    "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,
            }
        elif effect_type == "CrosstalkCancellation":
            asset = et.AssetData(
                np.stack((coefficients[0], coefficients[1], coefficients[1], coefficients[0])),
                SAMPLE_RATE,
                topology="trueStereo",
            )
            parameters = {"latencyMode": "0", "filterDelaySamples": 0}
        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',
  'GroupDelayPEQ',
  'RoomEQ',
  'IRReverb',
  'CrosstalkCancellation'
]);

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 }
        ]
      })
    : effect.type === 'CrosstalkCancellation'
      ? encodeEta1({
          channels: [coefficients[0], coefficients[1], coefficients[1], coefficients[0]],
          sampleRate,
          topology: 'trueStereo'
        })
      : 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 six 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);
  }
}