Continuous preset morphing for openFrameworks.
Place your presets as nodes. Drag a point between them. Get a weighted blend.
example-mapping: the point sits out among the null nodes ringing the edge.
The node weights still sum to 1.000, but the resolved targets total only 0.303
— the missing 0.697 is held by nodes bound to nothing. That shortfall is the
fade, and it is the reason silent nodes were invented.
ofxManifold::Manifold2D m;
auto a = m.addNode("calm", {0.2f, 0.8f});
auto b = m.addNode("bright", {0.8f, 0.8f});
auto c = m.addNode("heavy", {0.5f, 0.2f});
m.addTriangle(a, b, c);
ofxManifold::Evaluator ev(m);
auto e = ev.evaluate({0.45f, 0.6f}); // point -> weights
MyPreset blended = interpolate(e.weights, presets); // weights + valuesThe manifold never learns what a MyPreset is. It answers which nodes
influence this point, and by how much. Everything after that is interpretation.
A single continuous gesture can drive several independent things at once — synthesis parameters, rhythmic density, colour, lighting, speaker gains, OSC output — without any of them being hardcoded into the thing producing the gesture.
Spatial audio panning is the historically important application and the narrowest one. If you have four presets you like and want to move smoothly between them, that is the same problem.
This is an independent implementation of an idea with a long history. It carries no code from the work below.
Steve Ellison devised barycentric amplitude panning in 1986 for a sixteen-channel geodesic dome in Canberra. The dome's triangular arrangement suggested panning between triplets of loudspeakers, and barycentric coordinates turned out to be a cheap way to derive power-preserving gains from it. He arrived at the technique intuitively and learned only later that Möbius had introduced the mathematics in 1827.
The software became SpaceNodes, then SpaceMap, commercialized through LCS Audio and later Meyer Sound across four hardware generations. His account of it — SpaceMap: 20 Years of Audio Origami, Lighting & Sound America, April 2013 — is the reason several decisions here are what they are. Every feature in SpaceMap exists because a production broke without it, which makes the feature set evidence rather than one arrangement among many.
Zachary Seldess reimplemented the approach for Max/MSP and Pure Data as MIAP — Manifold-Interface Amplitude Panning, presented at the AES 137th Convention in 2014, and generalized it considerably in the process. His paper and externals are at zacharyseldess.com, and anyone working in Max or Pd should go there rather than here.
Three things in MIAP shaped this addon directly.
It generalized the approach past panning. MIAP interpolates between HRTFs, between reverb impulse responses, across VST parameter sets — the weights are treated as influence over whatever sits at each node, and speaker gain is one case among several. Seldess has said the non-panning use cases were a large part of why he committed to the project, alongside making the manifold approach known outside the small circle of theatrical sound designers working on very expensive systems. That generalization is the premise this addon starts from rather than something it arrived at.
The response curve is selectable — cosine, square root, linear — precisely because the weights might be driving plugin parameters or lighting-board faders rather than audio, where constant-power is meaningless or actively wrong.
Non-centricity. Unlike VBAP or Ambisonics, the map need not be a projection of physical space and need not assume a listener at the origin. Two nodes adjacent on the map may be the two most distant outputs in the room.
MIAP has since generalized further, from triangles to polysets of N ≥ 3 nodes using mean-value coordinates — a generalized barycentric scheme that reduces exactly to ordinary barycentric coordinates at N = 3.
Try it: Seldess's interactive demo lets you drag a source through a polyset and watch each node's influence resolve in real time. It is the fastest way to understand what a manifold does, and it shows the idea through speaker gains. ofxManifold isolates the step before that — the weights themselves — and leaves what they mean to whatever consumes them.
Scot Gresham-Lancaster's Tibetan Yantra map, described in Ellison's article, is the clearest demonstration that the geometry is genuinely independent of spatial meaning: intersections in a diagram assigned to eight real channels with every other intersection virtual, producing sheets and planes of sound collapsing into points rather than movement from A to B.
Not better, and not a wider scope — MIAP already generalized past panning. The differences are of host, packaging, and where the seams fall.
Different host. MIAP is a set of Max/MSP and Pd externals. This is a C++ library for openFrameworks, usable from any C++ project. If you are working in Max or Pd, MIAP is the mature tool and you should use it.
No audio at all. MIAP interpolates non-audio parameters, but it lives in an audio environment and panning is first-class in it. There is no audio code anywhere in this addon, and the square-root conversion to constant-power gains is one optional curve among several rather than the default path.
Blending arbitrary values is a first-class operation. interpolate() takes
weights and any C++ type supporting + and * float — a colour, a struct of
parameters, a glm::vec3 — so a blended value comes back in the consumer's own
type rather than as a list of gains to distribute.
The node taxonomy collapses to cardinality. MIAP has Speaker, Silent, Virtual and Derived node types. Here the kernel has no node type field at all: terminal, null and composite nodes are the same struct, differing only in whether the mapping layer resolves them to zero, one, or many targets. And a Derived node — which runs the routing backwards, receiving a sum rather than distributing one — is not a node here. It is an aggregator, a separate object outside the geometry, because a non-participating object inside the geometry forces every region operation to filter it out.
The kernel has no openFrameworks dependency and is proved independently.
src/core builds and tests with a compiler and make. Every layer is verified
against a second implementation written in Python from a different
formulation — Cramer's rule against signed sub-areas, edge-sign containment
against barycentric non-negativity, the standard library's JSON parser against
a hand-written one. Agreement is evidence rather than a tautology.
Triangles only, for now. MIAP's mean-value coordinates handle polygons of
any size; this handles triangles. Region is an interface so a second
coordinate kernel can be added without touching anything downstream, but that
is future work rather than a claim (see ROADMAP.md).
A manifold and a mapping are separate files. A manifold is portable; a mapping is installation-specific. That split is what lets a recorded path survive a change of venue: the map is rebuilt for the new rig and the cues are not touched.
cd openFrameworks/addons
git clone https://github.com/leeMeredith/ofxManifold.git
Then add ofxManifold in Project Generator. No dependencies beyond
openFrameworks itself. glm is vendored only so the kernel tests build without
an openFrameworks checkout.
example-basic — drag the point, watch the weights. Drag a node and
watch it refuse to move when it would turn a region inside out. Three fixtures:
a clean fan, a deliberately broken T-junction, two overlapping regions where the
answer depends on which side you entered from, and a 144-node grid for looking
at the renderer at density.
example-editor needs a second addon, ofxManifoldEditor, which lives
in this repository beside src/: copy it into your addons/ folder next to
ofxManifold. It is kept out of ofxManifold's own src/ on purpose, so projects
that want only the manifold never compile the editor.
example-editor — the first piece of the editor. Click empty space to
place a node on the active grid, drag a box or shift-click to select, drag the
selection to move it as one operation, press f to join the selection into a
region, delete to remove nodes, u to unjoin, x for an empty map, s and l
to save and load. Outputs: o makes one, b binds the selection to it or unbinds
it (a toggle), ⇧B to every output, q and w step a node's fill down and up by 5% (shift for 1%),
d makes a derived output. ⌘Z undoes and ⇧⌘Z redoes, any edit. ⌘S saves the map and its
outputs as a pair of files, ⇧⌘S under a new name, ⌘O opens either file of a
pair. Node shape
shows what a node does with its share -- circle for one output, triangle for
several, hollow square for none -- and its fill shows how much reaches an
output. Hold space to audition: dragging moves a listening point and never a
node, and the chart along the bottom shows every output, and silence, live,
while a halo on each node shows its share -- green where it reaches an output,
grey where it goes to silence. With auto-output on (the default, p to
toggle), each new node gets its own output; O removes the current one. Free, square, triangular and polar grids,
all rotatable; hold alt for a free-hand drag. Drag a corner of the quad
across its opposite edge and the move is refused, because the ring would cross
itself.
example-regions — a triangle, a hexagon, an L-shape and a star in one
map. Regions of three nodes use the barycentric solve; more use mean-value
coordinates. Drag into the L-shape's inner corner and a red line pulls
backwards toward a vertex you cannot see — a negative weight, which is MVC
working. Press c to clamp it: right for speakers, wrong for parameters.
example-smoothing — raw and smoothed weights traced against each other
over time. Cross an interior edge and the raw line does not jump: on a
conforming mesh the weights are already continuous there. Then add jitter, fire
a jump, leave the hull, or load the T-junction — those are the discontinuities
smoothing is for.
example-spread — one slider from pinpoint to wash. The node weights stay
at 1.000 the whole way; the resolved targets fall, because spread hands weight
to the null nodes too and they discard it. Nothing was told to fade.
example-blend — two manifolds stacked in one square, one point evaluated
against both, one crossfade combining them by target name. Hold the point still
and sweep the crossfade: every output changes while the point has not moved.
That is the third axis — two 2D maps combined downstream, which is how SpaceMap
reached three dimensions rather than with tetrahedra.
example-trajectory — the touring case. Record a path by dragging, play it
back, then swap the venue underneath it while it is still playing. The dot
traces the same shape; every node has moved, so the weights reorganize around a
gesture that has not changed. This is why the manifold lives in normalized
coordinates rather than pixels.
example-parameter-morphing — four presets at four nodes, each an ordinary
struct. Every field interpolates independently.
example-mapping — the node taxonomy made visible. A ring of null nodes
around the edge, a composite node feeding four outputs at once, two nodes
sharing one target, and aggregators running the routing backwards. Drag toward
a corner and watch the resolved target total fall while the node weights still
sum to one. That shortfall is the fade.
make test
Vectors are classed by where their authority comes from:
| Class | Authority | A failure means |
|---|---|---|
ANALYTIC |
the mathematics itself | the arithmetic is wrong |
CROSS |
an independent implementation | the two disagree |
SPEC |
a rule we invented | we are inconsistent with ourselves |
613 vectors across eleven suites, plus 124 mutation gates that introduce known faults and require each to be caught — because a suite that has never gone red is an assertion rather than a check. CI runs on Linux x86_64 and macOS arm64, which has twice caught floating-point differences invisible on one platform alone.
make bench measures evaluation and validation across triangulated grids from
16 to 10000 nodes. It gates nothing; it exists so scaling questions get answered
with evidence. See DECISIONS.md D-015.
DECISIONS.md records the reversals and what forced each one. ROADMAP.md
records what is next and what is deliberately parked.
src/core/ kernel, glm only, no ofMain.h
src/interpretation/ curves, spread, blend, interpolate, smoother
src/mapping/ node to target bindings, aggregators
src/io/ JSON manifold and mapping files
src/authoring/ grids for placing nodes: free, lattice, polar
src/ofx/ the ONLY place ofMain.h may appear
tests/ references, vectors, fixtures
libs/glm/ vendored, see libs/VENDORED.md
make headers enforces the boundaries: nothing outside src/ofx may include
ofMain.h, every kernel header must compile standalone as the first thing in a
translation unit, and every header must be reachable from ofxManifold.h.
To use the kernel without openFrameworks, include the headers under src/core
directly.
The manifold describes relationships, not meanings.
A point has a location. A node has an identity. A region defines interpolation. The evaluator produces a weight vector. Everything after that is interpretation.
MIT licensed. See LICENSE.
