Creating custom device GUIs for MSoundFactory and MXXX

MeldaProduction has built an engine which lets you change the appearance of your devices using various images for the background, knobs etc. This is the way that most plugins are designed, because it is quite easy to do, however it is rather difficult to maintain—adding more controls for example may become a tedious task, since you may need to move the existing controls as well, change a background etc. You are likely to edit the entire visual in Photoshop for example, then export images of the background and of each control. After that you just need to write a simple XML text file containing the XY coordinates of each control and some other properties.

This document will serve as a description of the current possibilities and it may change from time to time, when new features are added.

Basics

Let's assume that you want to create custom graphics for the “Superman Piano” device; this device will be stored (on Windows) in

C:\Users\<Account_name>\AppData\Roaming\MeldaProduction\MSoundFactoryInstruments\Superman Audio\Superman Piano.mInstrument

First of all you need to create a folder with the same name, just no extension and a dot (.) at the start, which usually marks hidden folders. In our case the folder name is this:

C:\Users\<Account_name>\AppData\Roaming\MeldaProduction\MSoundFactoryInstruments\Superman Audio\.Superman Piano

Then put all your graphics files into that folder. You can use PNG and JPG files. To minimize compatibility issues following these rules for file names is recommended:

  • All filenames should start with a letter.
  • Only letters and numbers are allowed, no whitespaces, special characters etc.
  • Names are case sensitive, so if you use Test.png and name it "test" in the definition, it will not work, because t is not T.
  • Make the names as short as possible, for performance reasons.

So for example KnobDryWet.png is great, but Knob-123Dry-Wet!&=.png not so much. If you create multiple devices which share the images, you can change the name of the directory via the XML file that we will be creating below, so these devices can share the resources.

Finally you need to create the definition XML file, which we will discuss below. In our case it would be:

C:\Users\<Account_name>\AppData\Roaming\MeldaProduction\MSoundFactoryInstruments\Superman Audio\Superman Piano.xml

When MSoundFactory or MXXX loads a device, it checks whether there's a XML file with the same name and if there is, it loads the XML file and does whatever is necessary with the information in it.

Screen Resolution and GUI Size

Display screens

The display resolutions for modern screens vary a lot, from low resolutions used on MacBooks (2560x1600, the HDPI version of 1280x800) to high resolution laptops (3840x2160, the HDPI version of 1920x1080) and of course gigantic desktop displays having 5K resolution (5120x2880) or more at the time you are reading this.

And, according to https://en.wikipedia.org/wiki/Display_resolution#Common_display_resolutions and https://gs.statcounter.com/screen-resolution-stats/desktop/worldwide the two most popular desktop screen resolutions are 1366 x 768 and 1920 x 1080.

In any case, good GUI design needs to address the lowest resolutions used, otherwise your device will not be displayed completely on the screen. You can certainly change the width as well, but then you can expect the GUI to be resized.

GUI Dimensions

Every device has a background image, which defines the total size of the GUI.

It is important to understand that the same GUI needs to work on both normal and HDPI/retina displays (which are basically 2x larger in both height and width). All images that you provide for your device are assumed to be designed for HDPI displays. For MSoundFactory devices the width of the background image should be 1400 pixels. The height is up to you, but the taller it is, the more the chance that it won't fit on the screen. For non-HDPI displays the images are halved in size, hence the default width for MSoundFactory devices is 700 pixels on non-HDPI screens. The engine actually lets the user manually resize the devices, so the images may be stretched to any size.

The coordinates and dimensions of all components in the GUI definition (described below) should be divisible by 2. That helps avoiding controls being placed incorrectly when rendered from HDPI to normal displays.

When designing the graphics it is essential to be aware that the change of resolution may occur. It is essential that you test your graphics and make sure you won't lose important details – for example it may be tempting to create very fine grained text and sharp lines in your GUI, however there's a danger that it may not be readable after lowering the image size. As an example, lines with a width of 1 pixel could disappear.

You may provide different graphics for lower resolutions and avoid this problem. The downside is that you will need to maintain multiple editions of your graphics, the size deployed on the user computers will be bigger and more files will need to be loaded to memory, which means higher memory consumption and longer loading times. If you still want to do that, simply add "2" postfixed to all 2x downsized image files. For example, if there is background.jpg (1400x1000), you may also add background2.jpg (700x500) for non-HDPI displays (or if a user reduces the GUI size intentionally). Please note that you don't need to create downsized versions for all images, just those you really need.

Definition XML file

The word XML may make you feel scared, but it's really just a text tile. Here's an example:

<device>
	<GUI>
		<tab name="Generator" background="piano">
			<control target="1" pos="100,100" image="knob"/>
			<control target="3" pos="100,250" image="knob"/>
		</tab>
	</GUI>
</device>

Let’s work through that example.

XML is an easy to edit text format, which is also pretty fast to parse, so it is ideal for our cause. It consists of a set of so-called "tags", each tag looks like this:

<tagname attribute1="value" attribute2="value" ...>
	subtags
</tagname>

If there are no subtags, you can simplify it like this:

<tagname attribute1="value" attribute2="value" .../>

Notice the terminating "/" symbol. That's all that is needed there!

Our XML file has a global tag, named "device", which just needs to be there. Inside it there's a "GUI" tag, which marks the section describing the GUI of the device. And inside that one you create a custom GUI for each tab in your device. Each tab has a name, the same one as you used in the actual MSoundFactory /MXXXdevice and the background image name (without the file extension). Finally each "control" tag describes one control, such as a knob. In our case there are 2 identical knobs, both associated with the image knob.png (or jpg). The first one is associated with multiparameter 1 and painted at position 100,100 from the left top corner (0,0). The second one is associated with multiparameter 3 and painted at position 100,250. The control dimensions are determined from the image. Simple.

Each control tag can look like this (the attributes surrounded by [ ] are optional, and the ... indicates that there may be further optional attributes):

<control
	target="target multiparameter"
	pos="position, ideally divisible by 2"
	[type="type of the control, see below, defaults to knob/button/switch" ]
	[image="name of the image file without extension" ]
	[imagedisabled="name of the image file for disabled state" ]
	[height="height, ideally divisible by 2" ]
	[count="number of states for switch/button" ]
	...
/>

Please note that if type is not specified, then the type defaults to knob/button/switch.

Comments will be useful when you come back to your design after a few weeks. A comment can be added by starting it with <!-- and ending it with -->. (without the full stop)

Please remember that the controls are displayed in the order in which they appear in the XML file.

Control types

Knobs and basic controls...

Knobs and other basic controls are represented by an image file, which usually contains multiple images of the same control. For instance, if this is a knob, it is not a single image of a knob, but a (very) tall (vertical) image containing several knob images (“frames”) stacked vertically on top of each other. The top one represents the appearance for the minimum parameter value, and the bottom one represents the appearance for the maximum value. This is a traditional way for texturing knobs and other controls using knobman for example. Here's an example.

It is important to choose a good number of frames – each one represents a different "rotation" of the knob. We recommend 40 to 80 frames. Too low would make the knob hard to control, jumping from step to step. Too high a number of frames would require more memory and loading time. Image size should not exceed a value of 16,384, since that's the typical maximum texture size for most GPUs. If your knob requires a middle value (e.g. gain from -12dB to +12dB with a middle value 0dB), the number of frames should also be "odd", so that there's a dedicated frame for the middle value. So for example 40 frames may not be ideal and you should consider using 41 frames. Or, given that knobs often range from 0% to 100%, 51 could be the right number. As an example, 51 frames of a knob that is 160 pixels high would be 8160 pixels high.

Controls such as buttons or switches (for some particular number of values) use the same approach, except that the number of frames is defined by the number of states. For example, a button has 2 states: 0 = not pushed, 1 = pushed. A switch between 4 values (implemented using a multiparameter in banks mode for example) has 4 frames. So defining graphics for these controls is actually the same as for knobs! The top frame represents the “not-pushed” state for a button or the first state for a switch and the bottom frame represents the “pushed” state for a button or the last state for a switch. You should specify the number of states using the count attribute though, e.g.:

<control target="1" pos="100,100" image="switch" count="4"/>

By default the engine assumes that the image of each frame is square, which is usually the case for knobs. So if each knob is 32x32, the system can obviously deduce the height of it just from the width of the image. For other controls (or if your knob is not square) you should specify the optional height attribute:

<control target="1" pos="100,100" image="notsquareknob" height="112"/>

Additionally specifying blend="1" makes the engine not only paint one image from the set of images, but interpolate between 2 of them, providing finer granularity. This isn't really useful for knobs, where interpolating between 2 different images only produces some sort of blurred output, but may be useful for example for "led" indicators changing colours, images of "tubes" being heated differently etc. For example:

<control target="1" pos="100,100" image="tube" blend="1"/>

Multiple controls may use the same image. This is advantageous because the engine will load the image only once and share it between all controls that use it. So for example if you have a knob with some units around it, you should draw the units on the background image, not on the knob image. This also makes the knob smaller and provides better compression due to higher predictability of pixels. But, make sure that the knob is in the right place; if you subsequently move it you will need to edit the background image too. Tip: it will be valuable to use an image editor that support layers to make this easier&mdadh;with the “real” background on one layer and the units characters on another.

From the user point of view the knobs are handled in exactly the same way as MeldaProduction knobs: vertical dragging changes the value, right clicking selects the default value, ctrl key is used for fine tuning, shift + click or double click provides a text edit.

Arc and Rotated knobs

Classic knobs created using a stack of image frames are a generalized solution, though quite resource demanding - they require a huge image to be loaded and used, hence bigger executables and slower loading. Additionally the number of frames is limited, so the knob is "jumping" from one frame to the next. To make the visual response smoother, the only solution is to increase the number of frames, hence again increasing the resource requirements.

For 2D flat design it is often possible to paint the knobs in a different way—by painting only an arc of the image or rotating a single image. You can also combine all 3 of these methods. The normal image would be the static background, and an arc (usually some kind of partial circle indicating the current value) would be painted over it, and finally an actual knob would be painted as a rotated image. The arc also lets you highlight the value's origin. For example:

<control
	target="1" pos="32,240"
	image="background" imagearc="arc"
	imagerotated="theknob"
	baseangle="30"
/>

Base angle defines the angle of the 0% value from the bottom centre in degrees. By default 40 degrees is used. A variant of this approach is to use a static background, and a simple bar as the imagerotated. Think of a clock face – 95% is static, only the hand(s) move.

Using knob controls as horizontal / vertical sliders

Sliders present another way for controlling the value and in fact these can work in exactly the same way as knobs, just displaying a different control. While you can use the traditional way and make a big image of many slider frames in various positions, there's an alternative approach similar to the use of arcs and rotated knobs.

You would provide a background image and a highlight image. Background would always be painted fully, but the highlight would be painted only partly—the engine automatically cuts the left/right or top/bottom parts from it depending on current value and orientation (determined by the image dimensions). For example:

<control
	target="1" pos="32,240"
	image="background" imagehighlight="hl"
/>

For a horizontal slider, the imagehighlight is cut from the right-hand side and for a vertical slider the imagehighlight is cut from the top.

Using knob controls as meters

A knob always needs a target, typically a Multiparameter (“MP”). However if the MP's mode is Meter, it won't really be possible to control the knob and instead the knob will display the current value of the MP. While having a knob rotating on its own would be a bit obscure, you need to remember that it may actually not be a knob image, but any kind of visualisation – led indicators, tubes, some sort of animation... All these controls work the same way.

In this case you need to set the MP in your plugin settings in some way. One possibility is to attach it to a metering parameter – these are available for each of MSoundFactory's (or MXXX's) modules and usually there are input and output level, gain reduction and level follower values.

Another useful option is to control the MP manually using a modulator. You can follow the input level. Or even use LFO/random modulation which may be useful to create some cool animations.

Meters are often painted as LED strips. In that case it would be quite inefficient to create the image with all possible states separately as we used with knobs. Instead you can simply provide one single image with all LEDs on and the engine can paint just part of the image. Disabled LEDs would be part of the background image. Then you specify the number of LEDs using attribute count="N" and enable the feature using attribute autofillery="1", or, if the image is not equally spaced (e.g. due to 3D projection or varying LED sizes) include attribute fillery="y0,y1,y2...", where yn defines number of pixels to paint for each state.

<control
	target="1" pos="100,100"
	image="ledstrip"
	count="32" autofillery="1"
/>

The image ledstrip has 32 lit LEDs. Depending on the value of multiparameter 1, 1 to 32 of those LEDs are displayed (and, of course, each LED can be a different colour or intensity).

Static knob

There's one more case in which the knobs, not really being knobs, is useful—for some sort of background images. Imagine a module which would switch between multiple compressors and you want the background to change depending on which compressor has been selected. There will be some MP which is selecting the compressor, so you can attach the same MP to another knob, which will be just the background image. If there are say 3 compressors, then this special knob would have 3 image frames. And to prevent it from actually working like a knob, hence letting user click on it and change the MPs value, specify the attribute static="1".

<control
	target="1" pos="100,100"
	image="CompressorBackgrounds"
	height="320" static="1"
/>

Of course, this control would need to be in the XML file before the actual compressor knob controls.

Switch

Switches are painted the same way as knobs. Knobs are controlled by vertical dragging, which would be quite cumbersome with switches between say 3 values. So there's a different control type called switch—by default it selects the next value on every click (right mouse button selects the previous value). Alternatively you can specify attribute clickswitch="0", in which case the image would be cut into equally sized regions for each value and clicking will select the value on which the user clicked. By default this is assumed to be a horizontal set of "radio buttons", to use vertical set just specify vertical="1".

You should use attribute count="..." to set the number of values. By default it is 2, which is useful for buttons, representing on/off.

<control
	target="2" type="switch"
	pos="100,100" image="SidechainEnableButton"
	count="2"
/>

<control
	target="1" type="switch"
	pos="100,100" image="CompressorSwitch"
	count="3" clickswitch="0"
/>

Additionally you may create a clickmap, if the switch isn't simply horizontal or vertical, by specifying a set of points. For example:

<control
	target="1" type="switch"
	pos="100,16" size="150,50"
	image="CompressorSwitch"
	count="3"
>
	<item value="0" pos="0,0" size="50,50"/>
	<item value="1" pos="50,0" size="30,50"/>
	<item value="2" pos="80,0" size="70,50"/>
</control>

This example will display a 3-way switch with frame widths of 50, 30 and 70 pixels respectively.

Locks

Parameter locks work in the same way as switches and their purpose is to lock current values when loading presets. Useful for dry/wet in reverbs for example. The control has a lock type and the image is assumed to have 2 states (the top one is “unlocked”).

<control
	target="1" type="lock"
	pos="100,10" image="DryWetLock"
/>

Slider

Sliders mimic the classic faders from mixing consoles. Again you need to specify the image attribute, which selects the image of the fader handle. The background is assumed to be incorporated into the plugin background image. Again, we recommend using an image editor that supports layers, so that you can move the slider backgrounds around more easily. The slider is painted simply by rendering the handle image at a position depending on the current value. Sliders can be horizontal or vertical (default). For vertical sliders you need to specify the height, which controls the area in which the handle is moving. Width is assumed to be the same as the width of the handle. For example:

<control
	type="slider" target="21" pos="10,10"
	height="224" image="handle"
/>

For horizontal sliders it's the other way around—height is assumed to be the same as handle height, and you need to specify the width. Like this:

<control
	type="slider" target="21" pos="10,10"
	width="224" vertical="0" image="handle"
/>

For 3D HW looking interfaces you may run into the problem that a slider isn't really going straight up and down but is rather following some diagonal line and even perspective may come into play. In that case you can specify pos and size attributes defining the entire region and then provide coordinates pos1, pos2 and pos3, which define the trajectory for the slider from pos1 (0%) through pos2 (50%) to pos3 (100%). You can also specify the attribute centre, which defines coordinates of the centre position in the slider handle image, which is useful to align the slider to the 3-point trajectory. For example:

<control
	type="slider" target="1" pos="10,10" size="120,538"
	pos0="128,714" pos1="136,487" pos2="144,270"
	center="46,60" image="handle"
/>

XYPad

XY-pad is a cool way to control 2 multiparameters at once, one is referenced as the target and the other as target2. The image of the XY pad is simply the "handle", which is painted into the appropriate position. As before, the background to the XY pad is assumed to be incorporated into the plugin background image. The pos and size need to be specified and define the area in which the handle can move. By default the XY pad assumes that the handle is the same size as its image (which limits its movement inside the XY pad), but if you for example include some sort of outer glow or something, you don't really want to limit the movement based on the edge of the glow, so you need to also add attribute handlesize. For example:

<control
	type="xypad" target="11" target2="12"
	size="384,384" pos="1056,288"
	image="handleimage" handlesize="2"
/>

Image

Image is simply an image overlay displayed over the current background with controls painted so far. It can be used to provide some additional 3D realistic feeling. It doesn't need the target attribute of course.

<control type="image" pos="100,100" image="FrontGlass"/>

Needle

A needle is quite common for painting oldschool hardware VU / level meters. This type of control doesn't have an image file at all. Instead it has attributes size specifying the size (which combined with pos results in the crop rectangle), center as the place where the needle is attached (usually out of the crop rectangle), min as the minimum value end point and max as the maximum value end point. The engine will calculate the remaining points automatically. It additionally needs color attribute as RGBA and linewidth.

<control
	type="needle" target="5" pos="550,200" size="200,100"
	center="650,400" min="560,280" max="740,280"
	color="00000030" linewidth="8"
/>

To paint a needle with a shadow you can simply use multiple needles.

Meter

Meter control provides the classic bar meter that we use in MeldaProduction plugins. It needs the attributes color1 and color2 specified as hexadecimal RGBA, size specifying its full size (width, height, the engine uses it also to detect whether the meter is horizontal or vertical) and linesize, which controls the size of each "led" indicator in the meter. It isn't so useful for vintage hardware (unless it is in some sort of display), but is definitely useful for more modern GUIs.

<control
	type="meter" target="6" pos="100,150" size="40,200"
	color1="00FF00FF" color2="FF0000FF"
	linesize="4"
/>

Units

Units is a plain text field painted using the specified font, which displays the text representation of value of the target multiparameter. It is useful for actual controls as well as meters. The text is centred by default, but you can change that using attribute fontlayout, currently supported values are left, right and center. An example (which shows the defaults for font selection):

<control
	type="units" target="6" pos="100,150" size="40,200"
	color="00FF00FF"
	fontname="roboto" fontheight="20" fontwidth="0"
	fontitalic="0" fontbold="0"	fontlayout="center"
/>

You can use any fonts installed on the system. We would however recommend using Roboto, which is installed with Melda plugins. If your font is not found on the system, Roboto will be used.

Menu

Menu is similar to the Switcher control in MeldaProduction plugins. It displays the text representation of the current value and clicking on it displays a menu with choices. It should be associated with a multiparameter in Banks mode; the bank names are used for the menu choices. It is a single frame image. Optionally it can have arrows on the side, which need to be part of its image and the size of both left and right arrows is represented by attribute arrowwidth. It can also be controlled using vertical arrows in which case there should be attribute arrowheight. You also need to specify font for the units the same way it is done for Units type.

<control
	target="1" type="menu" pos="100,16" image="Switch"
	color="00FF00FF"
	fontname="arial" fontheight="20" fontwidth="0"
	fontitalic="0" fontbold="0"	fontlayout="right"
/>