AudioMixSettings.onInboundContextChange property

Called when the set of streams being mixed changes: a pin’s audio has arrived and is about to join the mix, a pin has left it, or a pin’s stream has changed.

The mixer is stopped while this runs. Nothing from the new set of streams has been mixed yet, and nothing will be until the returned promise settles - so keep it short. An AudioMixNode.updateConfig() made before it settles is applied on exactly the first frame of the new set: a pin muted here as it joins is never heard, and a pin dropped from sources here as it leaves is dropped on the frame it leaves on. (If several are made, the last one is used.) Audio that arrived under the previous set, including the last of a pin that has been removed, is mixed with the previous gains.

The mixer stops only when this callback is given. Without one, a change in the set of streams goes straight through, and an updateConfig is applied when it arrives.

This is exact within this node. Another node given the same pin at the same moment (a second mixer, a StreamSwitchSmoothNode) stops at its own first frame of that pin, which will be close but is not guaranteed to be the same frame.

Lining a change up across several nodes - a "take" that cuts the picture in a StreamSwitchSmoothNode and brings the same source up in one or more mixers:

  • Best: subscribe the source to every node ahead of time - not the active source in the switch, muted in the mixers - and make the take with plain calls issued together (switchSource, updateConfig). With nothing joining at that moment no node has to stop, and the changes land within about a frame of each other. - If the source can only be subscribed at the moment of the take: subscribe it to all the nodes in the same tick, and have each node make its own change in its own onInboundContextChange. Each then lands on that node’s first frame of the source; the nodes agree to within a frame or two. - Do not make one node’s change from another node’s callback. The other node is not stopped for it, and the change lands wherever that node has got to. - Do not make the callbacks wait for each other. Where each node stops is settled when the subscription reaches it, not when its callback returns, so waiting brings them no closer; it only keeps every node stopped for as long as the slowest, and for ever if one of them is never called (a source with no audio, say). - A node stops its whole output while a source that is behind the others joins it, whichever way the take is made, for as long as that source is behind. If that pause matters, hold the sources that are always present back by more than the live ones lag (a NorskTransform.jitterBuffer() on them, at the cost of that much delay), so that a joining source is never the one being waited for. Or bound the wait in the mixer with AudioMixSettings.maxQueueMs, at the cost of not hearing a source while it is further behind than that. - Where picture and sound have to change on the very same frame, take the sound from the switch itself, which keeps the two together; use separate mixers for feeds where a frame or two is acceptable.

Signature:

onInboundContextChange?: (allStreams: Map<Pins, StreamMetadata[]>) => Promise<void>;

Example

// "guest" arrives at the moment it goes on air. Each node is told what to
// do with it when IT first sees it.
const onAir = new Set<string>(["host"]);
const gains = (pins: Iterable<string>) =>
  [...pins].map((pin) => ({ pin, channelGains: [onAir.has(pin) ? 0 : null] }));

const picture = await norsk.processor.control.streamSwitchSmooth({
  // ...
  onInboundContextChange: async (streams) => {
    if (onAir.has("guest") && streams.get("guest")?.length === 2) void picture.switchSource("guest");
  },
});
const programme = await norsk.processor.transform.audioMix({
  // ...
  sources: gains(["host"]),
  onInboundContextChange: async (streams) => {
    await programme.updateConfig({ sources: gains(streams.keys()) });
  },
});

onAir.add("guest");
picture.subscribeToPins(picturePins);     // now including "guest"
programme.subscribeToPins(programmePins); // likewise, in the same tick