Link Search Menu Expand Document

Device setup window

Mapper device

The mapper device allows mapping the parameters between devices directly. It will operate permanently, even when the score is not running.

Like other devices such as Serial, HTTP and WebSockets, it is defined in Javascript within a QML script. The basic form is:

import Ossia 1.0 as Ossia

Ossia.Mapper
{
  function createTree() {
    return [
      {
        name: "node",
        children: [
          {
            name: "sensor"
          }
        ]
      }
    ]
  }
}

This does nothing interesting and only creates a tree with a single address: Mapper:/node/sensor

Mapping a node of the mapper to another

This is useful to give for instance an user-readable name to another parameter. For instance, to give a name to a specific MIDI CC:

import Ossia 1.0 as Ossia

Ossia.Mapper
{
  function createTree() {
    return [
      {
        name: "node",
        children: [
          {
            name: "sensor",
            bind: "MidiDevice:/1/control/45",
            type: Ossia.Type.Int
          }
        ]
      }
    ]
  }
}

Which gives:

Mapper example

The mapping is bidirectional:

  • When MidiDevice:/1/control/45 receives a message, it is written to Mapper:/node/sensor
  • When Mapper:/node/sensor receives a message, it is written to MidiDevice:/1/control/45

Custom mappings with Javascript expressions

If one wants to transform the value, for instance to rescale it, it is possible to use small JS snippets:

import Ossia 1.0 as Ossia

Ossia.Mapper
{
  function createTree() {
    return [
      {
        name: "node",
        children: [
          {
            name: "sensor",
            bind: "MidiDevice:/1/control/45",
            type: Ossia.Type.Float,
            
            // What happens when the bound parameter (MidiDevice:/1/control/45) is written to:
            //
            // When MidiDevice:/1/control/45 receives the value 64, 
            // Mapper:/node/sensor will get the value 64 / 127, roughly 0.5.
            read: function(orig, v) { return v.value / 127.; },

            // What happens when the mapper parameter (Mapper:/node/sensor) is written to:
            //
            // When Mapper:/node/sensor receives the value 0.5, 
            // MidiDevice:/1/control/45 will get the value 0.5 * 127, roughly 64.
            write: function(v) { return v.value * 127.; }
          }
        ]
      }
    ]
  }
}

This example will scale the 0-127 integer values of the MIDI CC to 0-1 floating point values for the Mapper.

Example:

Mapper example

Binding to multiple parameters

bind: can be an array.

import Ossia 1.0 as Ossia

Ossia.Mapper
{
  function createTree() {
    return [
      {
        name: "node",
        children: [
          {
            name: "sensor",
            bind: ["MidiDevice:/1/control/45", "MidiDevice:/1/control/55"],
            type: Ossia.Type.Float,
            
            // The first parameter, `orig` is the OSC address of the parameter which 
            // was changed: it will be either "/1/control/45" or "/1/control/55".
            // The second parameter is the value.
            read: function(orig, v) { return v.value / 127.; },

            // Here we now return an array of values, one for each address:
            // for instance, if a message "0.5" is sent to Mapper:/node/sensor from within score,
            // - MidiDevice:/1/control/45 will get 0.5 * 127
            // - MidiDevice:/1/control/55 will get 0
            write: function(v) { return [v.value * 127., 0]; }
          }
        ]
      }
    ]
  }
}

Writing to arbitrary parameters

Sometimes one may want to map an address to another only known at run-time, depending on a message. For instance, imagine a case where you want to send messages [channel, value] to control varying MIDI channels at run-time, e.g. sending the list message [12, 45, 127] to Mapper:/node/sensor should write the CC value 127 to the CC 45 on MIDI channel 12, e.g. at the address MidiDevice:/12/control/45.

This can be done by returning a list of address-value pairs from write:

[ { address: "foo:/bar", value: 123 }, etc... ]

In this case one must not set bind: or read: as they do not make sense:

Example:

import Ossia 1.0 as Ossia

Ossia.Mapper
{
  function createTree() {
    return [
      {
        name: "node",
        children: [
          {
            name: "sensor",
            type: Ossia.Type.List,
            
            // What happens when the mapper parameter (Mapper:/node/sensor) is written to
            write: (v) => {
              // If v is [12, 45, 127], this gives: 
              // MidiDevice:/12/control/45
              let addr = `MidiDevice:/${v.value[0].value}/control/${v.value[1].value}`
              return [ { address: addr, value : v.value[2].value } ];
            }
          }
        ]
      }
    ]
  }
}

Mapping and combining values from multiple addresses

To do this, one can simply add a custom member to the QML object. For instance, here we combine two distinct addresses which represent an XY coordinate, in a single parameter of type Vec2.

import Ossia 1.0 as Ossia

Ossia.Mapper
{
  // Our custom member which will contain the current value for the address.
  property var xy: [0.0, 0.0]

  function createTree() {
    return [
      {
        name: "node",
        children: [
          {
            name: "sensor",
            bind: ["Millumin:/millumin/layer/x/instance", "Millumin:/millumin/layer/y/instance"],
            type: Ossia.Type.Vec2f,
            
            read: function(orig, v) {
              // Assign to xy depending on the origin
              if(orig === "/millumin/layer/x/instance")
                xy[0] = v.value;
              if(orig === "/millumin/layer/y/instance")
                xy[1] = v.value;
              
              return xy; 
            },

            // Write to the correct addresses. "v.value" is a Vec2, so two floats directly
            write: (v) => { 
              return [v.value[0], v.value[1]]; 
            }
          }
        ]
      }
    ]
  }
}

Using the mapper device as a generator

The device provides an easy way to create generic generative devices with Javascript.

Here is a simple example which creates a device which gives the time. The interval: property is used to define at which granularity in milliseconds the parameters will be polled.

import Ossia 1.0 as Ossia

Ossia.Mapper
{
  function createTree() {
    return [
      {
        name: "hours",
        type: Ossia.Type.Int,
        interval: 1000, // The read function() will be called every 1000 millisecond (every second)
        read: function() {
          return new Date().getHours();
        }
      },
      {
        name: "minutes",
        type: Ossia.Type.Int,
        interval: 1000,
        read: function() {
          return new Date().getMinutes();
        }
      },
      {
        name: "seconds",
        type: Ossia.Type.Int,
        interval: 200,
        read: function() {
          return new Date().getSeconds();
        }
      }
    ];
  }
}