Broadcaster
The Observer Pattern
is a very common software pattern and is used throughout most of the codebase in JUCE and HISE. It allows you to register objects that will be notified when anything changes and is super helpful for organising bigger projects.
Definition
In HiseScript you can implement the pattern manually by having an array that stores functions like this:
// Internal:
var registeredFunctions = [];
var currentValue = 0;
// register a function with a single argument
function addListener(lf)
{
registeredFunctions.push(lf);
// We call it once at registration, so it's up to date...
lf(currentValue);
}
function sendMessage(value)
{
// We only call the listener if the value has actually changed
if(value != currentValue)
{
currentValue = value;
for(f in registeredFunctions)
f(value);
}
}
// Usage:
addListener(function(newValue)
{
Console.print(newValue);
});
sendMessage(90);
Use cases of the Observer pattern
This pattern is mostly useful in GUI logic coding, eg. page-handling or displaying any information at multiple places (eg. which user preset is loaded). The advantage of this is that you can implement localised tasks at very narrow scope which are then automatically executed when the global state changes. Let's take a look at the previously mentioned example: page handling. Without the observer pattern, you would normally do it like this:
const var buttons = [Content.getComponent("Page1Button"),
Content.getComponent("Page2Button"),
Content.getComponent("Page3Button")];
const var pages = [Content.getComponent("Page1"),
Content.getComponent("Page2"),
Content.getComponent("Page3")];
inline function onPageButton(button, value)
{
local idx = buttons.indexOf(button);
for(p in pages)
p.set("visible", pages.indexOf(p) == idx);
}
for(b in buttons)
b.setControlCallback(onPageButton);
which is super slick and concise in this minimal example, but it quickly gets messy in real world projects, where you want additional tasks being performed when the page changes, eg:
- changing colours of other elements
- updating some UI elements
- hiding popup panels from that page that would otherwise leak into the other page
All these functionality has to be tucked into the poor little button callback which now gets super convoluted. With the observer pattern, the example looks like this:
// here is the observer code from the definition above...
var registeredFunctions = [];
var currentValue = [];
function addListener(f) { ... }
function sendMessage(value) { ... };
const var buttons = [Content.getComponent("Page1Button"),
Content.getComponent("Page2Button"),
Content.getComponent("Page3Button")];
const var pages = [Content.getComponent("Page1"),
Content.getComponent("Page2"),
Content.getComponent("Page3")];
inline function onPageButton(button, value)
{
local idx = buttons.indexOf(button);
sendMessage(idx);
}
for(b in buttons)
b.setControlCallback(onPageButton);
addListener(function(idx)
{
for(p in pages)
p.set("visible", pages.indexOf(p) == idx);
});
On first sight it looks more verbose than the previous example (and if it's as simple as that then it's a good example of overengineering), but this solution scales much better in real world projects, because now you can attach new listeners whenever you implement something at the very location you want:
// ... deep in some other namespace
// Now if you want to perform more tasks, you can leave the button callback alone and
// add it at the very scope you're working on.
addListener(function(idx)
{
local colours = [Colours.red, Colours.blue, Colours.green];
doSomethingWithColours(colours[idx]);
})
However, with the stock HiseScript solutions there are a few disadvantages:
- too much boilerplate. You have to write these logic functions (addListener, sendMessage) for every observer you need and keep the function array and the currentValue variable around
- hard to debug, easy to lose track of what calls what and what reacts to it.
- no support for async callback handling (would be possible to implement using a timer, but then it will be even more boilerplate)
- easy to mess up the argument amount of the registered functions
The Broadcaster object tries to address these issues by giving you (basically) the same functionality as shown in the script above, but with a clean interface, async callback support.
In order to use it, call Engine.createBroadcaster with the default values and then use one of its methods to implement the observer pattern:
// If you pass in a JSON object into the constructor, you can
// access the properties later using the standard dot operator.
const var bc = Engine.createBroadcaster({"myProperty": 12});
bc.addListener("testFunction", function(index)
{
Console.print(index);
});
// Access the property as if the broadcaster would be a generic JSON object:
Console.print(bc.myProperty);
// Setting the property sends a (synchronous) message to the listeners
bc.myProperty = 90;
// This line does the same as the one above...
bc.sendMessage([90], false);
However, calling sendMessage()
or assigning properties manually from your code is just one of the ways that this class can be used: you can also register it to any callback or even attach it to internal event sources that weren't accessible before.
Compatibility with callback slots
In HiseScript, there are many callback slots that can be registered with a function (or inline function), eg:
However instead of a reference to a function, you can also pass in a Broadcaster
object, and it will then call its listeners (asynchronously) everytime the callback happens.
Be aware that in this case the parameter amount defined by the argument in Engine.createBroadcaster()
must match the expected argument amount.
Attachable Event Sources
Another feature that vastly increases the usefulness of this object is the ability to register it to internal event types that were not accessible in HISE before:
Value changes
On the first look this doesn't sound particularly interesting because the value callback was already accessible through setControlCallback()
, however if you're using processorId
/ parameterId
properties it will not fire so this gives you the chance to add additional, "non-exclusive" callbacks for UI things
Property changes
This is super helpful if you want to react on changing properties (eg. the visible
flag) of certain components.
See: attachToComponentProperties()
Mouse events
This gives you the ability to attach custom mouse callbacks to ANY component using the same interface as ScriptPanel.setMouseCallback() This will not override the default mouse behaviour but rather give you the option to customize the user interface and eg. show certain things while a slider is being dragged.
See: attachToComponentMouseEvents()
Metadata
There is another concept of the broadcaster system which is tightly coupled with the visualisation of the broadcaster connections and this is the Metadata . Almost all methods which will generate an item on the BroadcasterMap (so the broadcaster itself but also all listeners and event sources) have a metadata parameter in their function signature that needs to be populated with a description of what this item is doing. This enforces self-documenting code on the coding level, which is fine but also heavily increases the usability of the broadcaster map which will help tremendously at keeping the overview over large projects.
A metadata object can be a simple string that only contains a description of the item, but for more information you can use a JSON object containing these properties:
Property | Type | Description |
id
|
String | a (unique) String that is used as name for the item. This doesn't need to be a variable name so you can use any human-readable title here. If you don't supply a JSON object but a simple string, this string will be used as id
(making this the only non-optional property in this list). |
comment
|
String | A markdown formatted string which will be shown on the broadcaster map. There is one little magic trick applied here and that is that if you comment the function call or object definition with a /**
comment (instead of the default /*
one), it will parse the comment from the code and write it into the metadata as comment
property. |
colour
|
int | A colour value that is used for the item drawing. You can supply a colour value using the 0xAARRGGBB
notation, or just enter -1
, then it will create a random colour from the ID hash, which is a quick way to colour an item. |
tags
|
Array of strings | You can attach tags to any item and then filter the broadcaster map to only display the items you want. This helps navigating around big projects. |
priority
|
int | This property is only valid when used with a listener item and defines the order of how the listeners are called. By default they are called by the order of the addListener
call, but if that is not what you want, you can shuffle around the listeners by supplying a priority (higher priority values means that the items are moved up the list and the default value is 0
). |
args
|
Array of strings | This is only valid for broadcaster definitions and contains a list of strings describing the arguments of this broadcaster. |
For an example usage take a look at the various API calls.
Class methods
addComponentPropertyListener
Adds a listener that sets the properties of the given components when the broadcaster receives a message.
Broadcaster.addComponentPropertyListener(var object, var propertyList, var metadata, var optionalFunction)
This function will change component properties (like visible
, enabled
, itemColour2
etc.) when the broadcaster sends a message. It basically is the same as adding a function call with addListener()
and changing the properties inside this call, however there are a few advantages over this approach:
- the broadcaster map will display the actual property in a meaningful way (eg. colours are rendered as colours and not as
124A438990
). - the amount of code you need to write is much less and if you only want to forward a property to other components you don't need to write any function at all
- the ability of customizing the value you'll send through a custom function allows a very concise function definition.
Content.addKnob("Knob1", 0, 0);
Content.addKnob("Knob2", 0, 50);
Content.addKnob("Knob3", 0, 100);
Content.addKnob("Knob4", 0, 150);
/** Create a broadcaster. We need 3 arguments to attach it to component properties. */
const var pb = Engine.createBroadcaster({
"id": "Property Syncer",
"colour": -1,
"args": ["component", "property", "value"]
});
/** Attach it to react on changes of the `x` property of `Knob1`. */
pb.attachToComponentProperties("Knob1", "x", "X-Position Watcher");
/** This function just syncs the `x` property by returning the value but you could calculate any custom value if you need to. */
inline function updateFunction(indexInList, component, property, value)
{
Console.print(trace({
"indexInList": indexInList,
"component": component.get("id"),
"property": property,
"value": value
}));
// something to play around with...
//return value * (indexInList + 2);
return value ;
};
/** You could also just pass in this instead of the updateFunction, then it will use the "default" behaviour. */
const var defaultUpdateFunction = 0;
/* Add a listener that changes properties when the broadcaster sends out a message. */
pb.addComponentPropertyListener(["Knob2", "Knob3", "Knob4"], // The array of knobs that should be synced
"x", // the properties that you want to sync
"update X position", updateFunction);
/* this will do the same thing but not as elegant. */
pb.addListener([Content.getComponent("Knob2"),
Content.getComponent("Knob3"),
Content.getComponent("Knob4")],
"update X position manually",
function(component, property, value)
{
//for(c in this)
// c.set(property, value);
});
This is the visualisation of the code above. You can see that the context awareness of the first listener item yields much more information to be displayed which gives you a quick way to ensure the correct functionality:
This will add a target to the broadcaster that will change component properties when the broadcaster receives a message. It can be used for synchronising properties, changing multiple properties of a list of components with all the benefits of the broadcaster system. The function expects these arguments:
Argument | Type | Description |
object | Single value or list of strings (component IDs) or script references | the target components which properties are supposed to be changed. |
propertyList | Single value or list of property IDs | the target properties that are supposed to be changed. |
metadata | String or JSON object | a metadata object that contains some information for the broadcaster map. |
optionalFunction | Callable object | An optional function that determines the value that should be sent to each component property (see below). If this argument is not a function, the broadcaster needs to have three properties (component, property, value) and will just send out the incoming value to the targets (which is an easy way of synchronizing properties. |
The optional function
If you supply a function as last argument, it will be called for every target component and property to figure out which value to send. The function signature needs to have all parameters of the broadcaster and a integer index at the first position that will contain the index of the component in the list that was passed in.
const var bc = Engine.createBroadcaster({
"id": "MyBroadcaster",
"args": { "firstArg": undefined, "secondArg": undefined, "thirdArg": undefined }
});
// This function needs to have an index parameter and then as much parameters as
// the broadcaster is using (in our case three).
// It will then be called for each property and component with the knobIndex argument
// containing the index of the component to change. The function's return value will
// be sent as property.
inline function setKnobColours(knobIndex, a1, a2, a3)
{
if(knobIndex == 0)
{
return calculateTheColourForTheFirstKnob();
}
if(knobIndex == 1)
{
return calculateTheColourForTheSecondKnob();
}
// ...
}
bc.addComponentPropertyListener(["Knob1", "Knob2", "Knob3"], // targets
["itemColour", "itemColour2"], // properties
{ "id": "set both itemColours"}), // metadata
setKnobColours); // optionalFunction
Be aware that the value returned by the function will be sent to all properties but if you want to send different values to different properties, you can call this function again with another function for each property.
addComponentRefreshListener
Adds a listener that will cause a refresh message (eg. repaint(), changed()) to be send out to the given components.
Broadcaster.addComponentRefreshListener(var componentIds, String refreshType, var metadata)
This function adds a listener to the broadcaster that will send a refresh message of the given type to all components defined by componentId
parameter. The refreshType
parameter defines the type and must be one of the following strings:
repaint
: sends a repaint message and will also cause any ScriptPanel to run its paint routinechanged
: causes the control callback to fire with the last value againupdateValueFromProcessorConnection
: if the control is connected to a processor attribute usingprocessorId
andparameterId
, it will update the value of the control to reflect the module's parameter stateloseFocus
: if the component is currently focused, it will make it lose its focusresetToDefault
: will cause the control to be resetted to itsdefaultValue
(just like double clicking on it)
This function is more or less equivalent to something like
bc.addListener(componentList, "repaint components", function(index)
{
for(c in this)
c.sendRepaintMessage();
});
but is less to type, a bit faster (because it doesn't have to evaluate the script function) and more versatile. And you get a nice visualisation in the Broadcaster map that blinks everytime the refresh messages are sent.
Example
This example will send a message when you click a button and causes a list of Panels to repaint themselves.
const var button = Content.addButton("Button1", 0, 0);
const var PanelArray = [Content.addPanel("Panel4", 0, 50),
Content.addPanel("Panel3", 100, 50),
Content.addPanel("Panel2", 200, 50),
Content.addPanel("Panel1", 300, 50)];
for(p in PanelArray)
{
p.setPaintRoutine(function(g)
{
g.setColour(Colours.withAlpha(Colours.white, Math.random()));
g.fillRect(this.getLocalBounds(0));
});
}
const var bc = Engine.createBroadcaster({
"id": "RepaintBroadcaster",
"colour": -1,
"args": ["index"]
});
bc.addComponentRefreshListener(PanelArray, "repaint", "Repaint Panels");
inline function onButton(component, value)
{
// just send out any value to trigger the broadcaster
bc.index = Math.random();
}
button.setControlCallback(onButton);
addComponentValueListener
Adds a listener that sets the value of the given components when the broadcaster receives a message.
Broadcaster.addComponentValueListener(var object, var metadata, var optionalFunction)
This call sets the value (and causes changed()
to be called) whenever a broadcaster message is sent. The syntax is pretty similiar to the addComponentPropertyListener
function (without the property
argument, which isn't required obviously).
addDelayedListener
Adds a listener that will be executed with a delay.
Broadcaster.addDelayedListener(int delayInMilliSeconds, var obj, var metadata, var function)
This function is very similar to the normal addListener function, but you can supply a millisecond value that will be used to delay the function.
Be aware that the execution of this function is not queued, so whenever you send a new message in the interval, it will just reset the timer and discard the pending function call.
addListener
Adds a listener that is notified when a message is send. The object can be either a JSON object, a script object or a simple string.
Broadcaster.addListener(var object, var metadata, var function)
This registers a listener to the broadcaster which will be notified whenever the broadcaster's state changes.
The function expects three parameters. The second parameter needs to be either a function (or inline function) with the exact same amount of parameters as the broadcaster's default value amount (defined by the constructor).
The first parameter can be one of three things:
- a string (for simple identification)
- an JSON object
- a script object
the second parameter is a metadata parameter that is used to display the target and set other properties (eg. priority)
This will be used in order to identifiy the listener (so if you want to remove it, you need to use the same value).
As an additional feature, it will be also accessible using this
in the function callback:
const var b = Engine.createBroadcaster({
"id": "My Broadcaster",
"args": ["index", "isTrue"]
});
b.addListener({"id": "MY_ID"}, "some description about the target",
function(index, isTrue)
{
Console.print(this.id); // "MY_ID"
});
b.addListener("funky_time", "some other target",
function(index, isTrue)
{
Console.print(this); // "funky_time";
});
const var Knob = Content.addKnob("Knob1", 0, 0);
/** Here we are using a JSON object instead of a metadata string
to set the priority. Note how the listener is at the top of the list
despite being added as last target.
*/
b.addListener(
Knob,
{
"id": "Print the knob value",
"colour": 0xFF3388AA,
"priority": 10
},
function(index, isTrue)
{
Console.print(this.getValue());
});
Note that the function will be called (synchronously) when you register it so that the listener is updated to the current value.
addModuleParameterSyncer
Adds a listener that will sync module parameters from the attached module parameter source. Edit on GitHub
Broadcaster.addModuleParameterSyncer(String moduleId, var parameterIndex, var metadata)
attachToComplexData
Registers this broadcaster to be notified when a complex data object changes.
Broadcaster.attachToComplexData(String dataTypeAndEvent, var moduleIds, var dataIndexes, var optionalMetadata)
If you want the broadcaster to be notified whenever an event occurs with a complex data type (SliderPacks, Tables or AudioFiles), you can use this method to attach the broadcaster to one or more data objects.
In order to attach a broadcaster to a complex data object using this method, it needs to have exactly 3 arguments defined in its args
metadata property.
The first arguments dataTypeAndEvent
is a String that describes the event type and datatype you want to listen to. The syntax for the string is DataType.EventType
with the following options for the DataType
part:
SliderPack
Table
AudioFile
and the following options for the EventType
part:
Display
: changes to the "displayed index": the ruler in the table / playback position in the audio file / the last active slider in the slider packContent
: changes to the content: adding / removing table points, editing slider values, loading new audio files / changing the playback range
The moduleIds
argument is either a String or an Array of Strings with the processor IDs that are holding the data types.
The dataIndexes
argument is either an integer index (zero based) or an array of zero based integers for each data type.
This means that the amount of data types that you attach the broadcaster to is defined by
NumberOfModules * NumberOfIndexes
.
optionalMetadata
is a metadata object used by the broadcaster map.
// You need three arguments
const var bc = Engine.createBroadcaster({
"id": "Complex Data Listener",
"colour": -1,
"args": ["processorId", "dataIndex", "value"]
});
bc.attachToComplexData("Table.Display",
["LFO Modulator1", "LFO Modulator2"],
0,
"Connect to 2 LFO table rulers");
bc.attachToComplexData("SliderPack.Content",
"Arpeggiator1",
[0, 1, 2],
"Connect to changes for every slider pack of an arp");
bc.attachToComplexData("Table.Content",
["Table Envelope1", "Table Envelope2"],
[0, 1],
"Connect to table edits for every table
(attack & release) for two table envelopes");
Once you've attached a broadcaster to a complex data object, it will call the registered listeners once the event happens. The three arguments will contain these values:
processor
: the processor ID as a string that holds the complex data object that caused the eventindex
: the index within the processor (!= the registered index!) of the object that caused the eventvalue
: depending on the event type, either the display value as normalised double number (0...1) or a string representation of the data (eg. Base64 representation of the table data).
attachToComponentMouseEvents
Registers this broadcaster to be notified for mouse events for the given components.
Broadcaster.attachToComponentMouseEvents(var componentIds, var callbackLevel, var optionalMetadata)
This registers the broadcaster to a list of components that will send out a message whenever the mouse callback is being triggered.
componentList
can be either a single string (component name), reference to the component (Content.getComponent(name)
or an array of those values.callbackLevel
must be one of the strings known from theScriptPanel
propertyallowCallbacks
.
After you've defined this method, all functions must have the prototype
function mouseCallback(component, event)
{
}
where component
is a reference to the actual script component object that triggered the mouse callback and event
is a JSON object that is identical to the one you know and love from ScriptPanel.setMouseCallback()
.
attachToComponentProperties
Registers this broadcaster to be called when one of the properties of the given components change.
Broadcaster.attachToComponentProperties(var componentIds, var propertyIds, var optionalMetadata)
Calling this function will attach the broadcaster to a list of components and properties and will notify its listeners everytime that one of the properties change.
componentList
can be either a single string (component name), reference to the component (Content.getComponent(name)
or an array of those values.propertyIds
must be a list of property ids (or a single string if you only want to listen to a single property). Be aware that every component you pass into the first argument needs to have this property, which prevents you from registering eg. a ScriptPanel to aviewPositionX
property).
After you've defined this method, all functions must have the prototype
function propertyCallback(component, id, value)
{
}
where component
is a reference to the actual script component object that triggered the mouse callback, id
is the property string and value
the updated value.
attachToComponentValue
Registers this broadcaster to be called when the value of the given components change.
Broadcaster.attachToComponentValue(var componentIds, var optionalMetadata)
Calling this function will attach the broadcaster to a list of components and properties and will notify its listeners everytime that one of the properties change.
componentList
can be either a single string (component name), reference to the component (Content.getComponent(name)
or an array of those values.
After you've defined this method, all functions must have the prototype
function valueCallback(component, value)
{
}
where component
is a reference to the actual script component object that triggered the mouse callback, and value
the updated value.
Note how the function signature is identical to the function parameters you can pass into setControlCallback()
which makes it super easy to migrate from a normal control callback to a broadcaster based system.
attachToComponentVisibility
Registers this broadcaster to be called when the visibility of one of the components (or one of its parent component) changes.
Broadcaster.attachToComponentVisibility(var componentIds, var optionalMetadata)
This function is similar to attachToComponentProperties
used with the visible
property, but with the additional feature that it also takes the parent's visibility into account. So if you want to react on something being actually shown
on the interface it might be a more stable solution if you have a deep component hierarchy.
attachToContextMenu
Registers this broadcaster to be notified when a context menu item from the given components was selected.
Broadcaster.attachToContextMenu(var componentIds, var stateFunction, var itemList, var optionalMetadata, var useLeftClick)
This function can be used to attach the broadcaster to any component and show a context menu when the user clicks on it with the right button (or double-tap on the trackpad / Ctrl+Click on macOS). It expects these 4 parameters:
- a single component or list of components (either a String with the component ID or existing script references)
- An array with Strings containing the popup menu items with a pseudo markdown syntax and the
{DYNAMIC}
wildcard to create dynamic item texts - A state function that expects two arguments (
type
,index
) and allows changing the active state / disable items or use dynamic text values. The first argument is always one of three strings (text
,enabled
oractive
and indicates which state it wants to know). Be aware that this function is called synchronously on the UI thread for every item so keep it simple. - Optional metadata (either JSON or String).
This function does not override any existing behaviour for right clicks, so you might want to eg. disable MIDI learn for components that you attach to this broadcaster.
After you've attached the broadcaster to the context menu of a component, right clicking will create a popup menu (using the component's LookAndFeel Customization ) and then send a message to its registered listeners with the clicked component as argument and the selected index (zero based).
Example
This example snippet registers a context menu to a button and allows setting the value and toggling the width of the button using two popup menu items.
// We'll attach the context menu to this button
const var Button1 = Content.addButton("Button1", 0, 0);
Button1.set("enableMidiLearn", false); // we don't want this to popup too...
/** Let's define a broadcaster with two arguments. */
const var bc = Engine.createBroadcaster({
"id": "ContextMenu Broadcaster",
"args": ["component", "selectedIndex"]
});
/** This defines a few items using a markdown-like syntax. */
const var POPUP_MENU_ITEMS = [
"**Set Value / Properties**", // A header
"Value is active", // the first item
"Set to {DYNAMIC}", // the second item with a dynamic text
"___", // a horizontal separator
"~~This is always off~~" // an item that is always disabled
];
inline function popupStateFunction(type, index)
{
// The this object of this function will always
// point to the component that was clicked (in our
// case it's always Button1).
Console.assertEqual(this, Button1);
local getEnableState = type == "enabled";
local getTextValue = type == "text";
local getActiveState = type == "active";
if(getEnableState) // We don't want to disable any item
return true; // so let's return true...
if(getTextValue)
{
// This function is only called with items
// that specify the `{DYNAMIC}` wildcard
// in this case the second item with the index 1
Console.assertEqual(value, 1);
// Now we can return whatever text we want to show
// and this is evaluated each time before the popup
// is shown
return this.get("width") > 150 ? "small" : "wide";
}
if(getActiveState)
{
// Now we can decide whether the popup menu item
// should be displayed as active or not
// the first item checks whether the button is active
if(index == 0)
return this.getValue();
// the second item checks whether its wide or not
if(index == 1)
return this.get("width") > 150;
}
};
/** Now we can use the item list and the state function to attach the broadcaster
to the context menu of the button (you can attach it to multiple components by
passing in a list. */
bc.attachToContextMenu("Button1", popupStateFunction, POPUP_MENU_ITEMS, "Context Menu");
/** This callback will be executed whenever a popup menu item was selected. */
bc.addListener(Button1, "Menu callback", function(component, index)
{
// the this object will point to the component
Console.assertEqual(component, this);
if(index == 0)
{
this.setValue(!component.getValue());
this.changed();
}
if(index == 1)
{
this.set("width", component.get("width") > 150 ? 100 : 200);
}
});
attachToEqEvents
Registers this broadcaster to be notified about changes to the EQ (adding / removing / selecting filter bands).
Broadcaster.attachToEqEvents(var moduleIds, var eventTypes, var optionalMetadata)
The parametric EQ
in HISE has a dynamic amount of EQ bands which can be addressed using the math formula
attributeIndex = attributeType + bandIndex * bandOffset
Adding & Removing bands however cannot be queried by the standard HISE parameter system, but you can use this attachment type to be notified whenever a Band is added / removed.
moduleIds
must be either a single string with the EQ ID or a list of strings for multiple EQseventType
must be one or multiple strings from this selection:["BandAdded", "BandRemoved", "BandSelected", "FFTEnabled"]
Once a broadcaster is attached to EQ events, it will fire its callbacks with three parameters:
function(eventType, value)
{
// eventType is one of the strings that define the event
// value is a context dependent value (eg. the band index at selection)...
}
attachToInterfaceSize
Registers the broadcaster to be notified when the interface size changes.
Broadcaster.attachToInterfaceSize(var optionalMetadata)
This will call registered functions when you change the interface size using Content.setHeight()
or Content.setWidth()
(and once with the initial value from Content.makeFrontInterface()
).
Note that this will not be triggered when the scale factor of the UI changes.
attachToModuleParameter
Registers this broadcaster to be notified when a module parameter changes. Edit on GitHub
Broadcaster.attachToModuleParameter(var moduleIds, var parameterIds, var optionalMetadata)
attachToNonRealtimeChange
Attaches this broadcaster to receive realtime / nonrealtime render change events. Edit on GitHub
Broadcaster.attachToNonRealtimeChange(var optionalMetadata)
attachToOtherBroadcaster
Attaches this broadcaster to another broadcaster(s) to forward the messages. Edit on GitHub
Broadcaster.attachToOtherBroadcaster(var otherBroadcaster, var argTransformFunction, bool async, var optionalMetadata)
attachToProcessingSpecs
Attaches this broadcaster to changes of the audio processing specs (samplerate / buffer size). Edit on GitHub
Broadcaster.attachToProcessingSpecs(var optionalMetadata)
attachToRadioGroup
Registers this broadcaster to be notified when a button of a radio group is clicked.
Broadcaster.attachToRadioGroup(int radioGroupIndex, var optionalMetadata)
If you want the broadcaster to listen to a list of buttons that are grouped into a exclusive radio group, you can use this method. The radioGroupIndex
must be the integer value that you've assigned as radioGroup
property to the buttons.
It will automatically scan all components and find the ones that are using this group index and then send out a message to all listeners when one of the buttons is clicked.
In order for this to work, the Broadcaster needs to have a single argument defined as args
property which will contain the index of the clicked button.
const var bc = Engine.createBroadcaster({
"id": "My Radio Watcher",
"colour": -1,
"args": ["buttonIndex"]
});
Be aware that this index is using the same order as the component list shows.
Also this attached mode is the only mode that is using a "bidirectional" communication. This means that if you send a broadcaster message using sendMessage()
or the assignment operator, it will also change the currently active button in the radio group.
Example: Page Handling
One of the most practical use cases for this function is the page handling logic which will show and hide panels when you click on the corresponding buttons
Please note how the entire logic and functionality is represented in the broadcaster map from the state of the buttons to the visible
property of the panels.
const var bc = Engine.createBroadcaster({
"id": "My Page Handler",
"colour": -1,
"comment": "This broadcaster will handle the page logic",
"args": ["pageIndex"]
});
// Just a dummy function that
inline function addRadioButton(i)
{
local b = Content.addButton("RadioButton " + (i+1), 0, i * 30);
b.set("radioGroup", 90);
b.set("saveInPreset", false);
local p = Content.addPanel("Page" + (i+1), 150 + i* 100, 0);
p.set("visible", false);
return p;
}
// This array will hold 4 panels
const var Pages = [];
// Create 4 radio buttons and 4 panels
for(i = 0; i < 4; i++)
Pages.push(addRadioButton(i));
bc.attachToRadioGroup(90, "Button Group");
const var PageList = []; //
bc.addComponentPropertyListener(Pages, "visible", "Show Panels", function(indexInList, buttonIndex)
{
return indexInList == buttonIndex;
});
// Show the first page
bc.pageIndex = 0;
attachToRoutingMatrix
Attaches this broadcaster to a routing matrix and listens for changes.
Broadcaster.attachToRoutingMatrix(var moduleIds, var optionalMetadata)
This function attaches the broadcaster to a routing matrix of one or more processors to be notified whenever the routing changes (so either the channel configuration or the amount of channels). In order to use this function, the broadcaster must have two arguments, the first will be the processor ID and the second one a Routingmatrix
object that you can query in your callback.
Be aware that you must not call any functions in a listener callback that itself causes the routing matrix to change or you will end up with an infinite loop!
const var bc = Engine.createBroadcaster({
"id": "router",
"args": ["id", "matrix"]
});
bc.attachToRoutingMatrix("Sine Wave Generator1", "script matrix");
bc.addListener("", "dudel", function(id, matrix)
{
// this just prints out where the sine wave generator is mapped
Console.print(trace(matrix.getDestinationChannelForSource([0, 1])));
});
attachToSampleMap
Attaches the broadcaster to events of a samplemap (loading, changing, adding samples).
Broadcaster.attachToSampleMap(var samplerIds, var eventTypes, var optionalMetadata)
This will register the broadcaster to be notified whenever a sample map is changed. There are three event types that can be selected. A broadcaster that is supposed to be attached to sample maps needs 3 arguments and will fire callbacks with these arguments:
eventType
- the event type string (see below)samplerId
- the ID of the sampler that caused the eventdata
- a event-specific data argument (see below)
const var b = Engine.createBroadcaster({
id: "sampleListener",
args: ["eventType", "samplerId", "data"]
});
b.attachToSampleMap("Sampler1", "SampleMapChanged", "");
b.addListener("", "funky", function(eventType, samplerId, data)
{
Console.print(data);
});
Note that using a broadcaster for listening to sample map changes is the best practice going forward and replaces the usage of the ScriptPanel.setLoadingCallback() function for this task.
These are the different event types:
Type | Description | data argument |
SampleMapLoaded
|
Whenever a sample map is loaded (or cleared). | the reference string as it goes into Sampler.loadSampleMap() |
SamplesAddedOrRemoved
|
Whenever a sample was added to (or removed from) the current samplemap | the current number of samples. |
SampleChanged
|
Whenever a sample property has changed | A JSON object with the sample information (see below). |
The eventTypes
argument will expect either the Type
string or an array with multiple type strings from the table above with all event types that the broadcaster should listen to.
The samplerIds
argument should be either a String of the sampler ID (or an array of strings for every sampler)
that you want to listen to.
Sample changes
If you want to listen to sample property changes (like changing the sample start or the low velocity), the values that you should pass into the eventTypes
argument is not the "SamplesChanged"
string, but one of the constants of the Sampler
API object.
In this mode, the data
argument will be a JSON object with these properties:
- a
sound
property that holds a reference to a Sample object. - an
id
property that holds the magic number of the property (query this against the Sampler constants). - a
value
property that will contain the value of the property change.
b.attachToSampleMap("Sampler1", [ Sampler.LoKey, Sampler.HiKey ], "");
b.addListener("", "funky", function(eventType, samplerId, data)
{
if(data.id == Sampler.LoKey)
{
Console.print("Changed low key to " + data.value);
}
if(data.id == Sampler.HiKey)
{
Console.print("Changed high key to " + data.value);
}
});
callWithDelay
Calls a function after a short period of time. This is exclusive, so if you pass in a new function while another is pending, the first will be replaced. Edit on GitHub
Broadcaster.callWithDelay(int delayInMilliseconds, var argArray, var function)
isBypassed
Checks if the broadcaster is bypassed. Edit on GitHub
Broadcaster.isBypassed()
refreshContextMenuState
If this broadcaster is attached to a context menu, calling this method will update the states for the menu items. Edit on GitHub
Broadcaster.refreshContextMenuState()
removeAllListeners
Removes all listeners. Edit on GitHub
Broadcaster.removeAllListeners()
removeAllSources
Removes all sources. Edit on GitHub
Broadcaster.removeAllSources()
removeListener
Removes the listener that was assigned with the given object.
Broadcaster.removeListener(var idFromMetadata)
This removes the listener from the list so that it will not be notified anymore. The parameter must be the exact same thing you've used in the addListener() function, so if you're using a JSON object, you need to use the exact same object (and not a clone with the same properties).
If you just want to temporarily deactivate a listener, you can do so by pressing the bypass button in the Broadcaster Controller. This is helpful for debugging
removeSource
Removes the source with the given metadata. Edit on GitHub
Broadcaster.removeSource(var metadata)
resendLastMessage
Resends the current state. Edit on GitHub
Broadcaster.resendLastMessage(var isSync)
reset
Resets the state.
Broadcaster.reset()
This resets the value to the default values passed into the constructor and sends a message to all listeners.
sendAsyncMessage
Sends an asynchronous message to all listeners. the length of args must match the default value list. Edit on GitHub
Broadcaster.sendAsyncMessage(var args)
sendMessage
deprecated function (use sendSyncMessage / sendAsyncMessage instead).
Broadcaster.sendMessage(var args, bool isSync)
This sends a message to all registered (and enabled) listeners. The first argument must be either
- An array, if the listener functions have multiple parameters. Then it will distribute the array elements to the function parameters.
- A single value if the registered functions and the default values have only one parameter.
The second parameter will control whether the message is being sent out synchronously or if it should be deferred and called a little bit later. This might be helpful if you want to coallascate calls to sendMessage so that it doesn't hammer the queue.
Be aware that it only sends the message to the listeners if any of the values is different than before.
Note: this function is deprecated and replaced by two other functions, sendAsyncMessage()
and sendSyncMessage()
. The rationale behind this is that the boolean parameter wasn't clear enough to indicate whether a message is being sent out synchronously or not (true
means sync or async?). So using this message will write an error message to the console but the functionality keeps working so you can migrate it more easily.
sendMessageWithDelay
Sends a message to all listeners with a delay.
Broadcaster.sendMessageWithDelay(var args, int delayInMilliseconds)
This will send out a message after a certain delay so it might come in handy in scenarios where you previously had to drag a timer around.
Be aware that if you call this method while a callback is pending, it will override the value to be send out and restart the timer, so the first message might get lost.
sendSyncMessage
Sends a synchronous message to all listeners (same as the dot assignment operator). the length of args must match the default value list. Edit on GitHub
Broadcaster.sendSyncMessage(var args)
setBypassed
Deactivates the broadcaster so that it will not send messages. If sendMessageIfEnabled is true, it will send the last value when unbypassed. Edit on GitHub
Broadcaster.setBypassed(bool shouldBeBypassed, bool sendMessageIfEnabled, bool async)
setEnableQueue
If this is enabled, the broadcaster will keep an internal queue of all messages and will guarantee to send them all.
Broadcaster.setEnableQueue(bool shouldUseQueue)
When the broadcaster is used asynchronously, it will always just send a message for the latest state, so
bc.sendMessage(0, false);
bc.sendMessage(1, false);
in this example the message with the value 0
will never reach its listeners. This is the default value in order to avoid unnecessary calls to the listeners, however there are a few occasions where you need to guarantee that every
message gets sent to the listeners. By enabling the queue, it will keep a list of all pending messages and sends out every value to its listeners.
If you attach certain event sources to a broadcaster, they will automatically switch to queue mode (eg. complex data events).
You can check the state of this value by the icon on the broadcaster map. If this icon appears:
the broadcaster is running in queued mode.
setForceSynchronousExecution
Forces every message to be sent synchronously.
Broadcaster.setForceSynchronousExecution(bool shouldExecuteSynchronously)
This function will enforce synchronous execution of its callback independently of which function is called.
setRealtimeMode
Guarantees that the synchronous execution of the listener callbacks can be called from the audio thread. Edit on GitHub
Broadcaster.setRealtimeMode(bool enableRealTimeMode)
setReplaceThisReference
This will control whether the this
reference for the listener function will be replaced with the object passed into addListener
. Edit on GitHub
Broadcaster.setReplaceThisReference(bool shouldReplaceThisReference)
setSendMessageForUndefinedArgs
Forces the broadcaster to also send a message when a parameter is undefined.
Broadcaster.setSendMessageForUndefinedArgs(bool shouldSendWhenUndefined)
By default, the broadcaster will not send a message when one or more arguments are undefined. This prevents wrong initialisation calls and script errors when the arguments are passed into function calls.
If you want to disable that function and also send messages for undefined arguments, call this function to change that behaviour (but in that case make sure to check isDefined()
before passing the parameters into function calls to avoid script errors).
If you create a broadcaster, all the arguments will have an undefined
state until you send the first message.