Mentat

Version: 1.6.0 (09/10/2023)
License: GNU/GPL v3 (© 2022 Jean-Emmanuel Doucet)

Overview

Mentat is a HUB / Conductor for OSC / MIDI capable softwares. It aims to centralize all controls in one place, manage their state and create routings.

Mentat is a module for python 3 and requires writing code to work. If you’re looking for a fully-featured software with a user interface, try Chataigne instead.

Mentat has been designed to coordinate the live setup of the rap-in-opposition band Plagiat (see the project’s source repository).

Requirements

python3 python3-pyalsa python3-pyinotify

Install

git clone https://github.com/jean-emmanuel/mentat/
cd mentat
python3 setup.py install

Usage

The typical use case for Mentat is a conductor for controlling a set of softwares during a live performance.

The Engine object is the main object, it manages the OSC / MIDI backends, the modules and the routes. It also holds a tempo, a cycle length (measure) and a time reference that’s used to create timed scenes and sequences in a musical way (using beats instead of seconds).

Module objects are interfaces between the controlled softwares and the engine. The Module class should be subclassed to create dedicated module classes for different softwares.

A set of controllable parameters can be defined for each module, each parameter being an alias for an OSC / MIDI value in the controlled software.

Controlled parameters should only modified using the module’s set() and animate() methods in order to guarantee that the state of the modules reflects the actual state of the softwares. This removes the need for feedback from said softwares and allows us to trust Mentat as the source of truth during the performance.

All messages received by the engine that are coming from a software associated with a module are first passed to that module’s route() method.

Route objects represent the different parts of the performance (eg tracks / songs in a musical show). The Engine has one active route at a time and will pass all incoming messages to its route() method.

Each track should be a dedicated class derived from the Route class. The route() method definition will allow writing the actual routing that should occur during that track.

Generic control API

In order to ensure state consistency, parameters should always be controlled by mentat. A generic command is exposed to control modules using osc messages, it allows calling any method owned by the engine and its modules:

/engine_name/module_name/submodule_name/method_name <*arguments>

Examples

/engine_name/set_route route_name
/engine_name/module_name/set parameter_name 1.0
/engine_name/module_name/submodule_name/animate parameter_name 1.0 10.0 1.0

MIDI

Mentat treats MIDI messages as OSC messages. Modules with protocol set to 'midi' will send and receive messages formatted as follows:

/note_on <int: channel> <int: note> <int: velocity>
/note_off <int: channel> <int: note>
/control_change <int: channel> <int: control> <int: value>
/program_change <int: channel> <int: program>
/pitch_bend  <int: channel> <int: pitch>
/sysex <*int: values>

Engine

Main object. Singleton that must be instanciated before any Module or Route object. The global engine instance is always accessible via Engine.INSTANCE.

The engine is also a Module instance and can use the methods of this class.

Instance properties

Events

Engine()

Engine(name, port, folder, debug=False, tcp_port=None, unix_port=None)

Engine constructor.

Parameters

Engine.start()

start()

Start engine. This is usually the last statement in the script as it starts the processing loop that keeps the engine running.

Engine.stop()

stop()

Stop engine. This is called automatically when the process is terminated or when the engine restarts.

Engine.restart()

restart()

Stop the engine and restart once the process is terminated.

Engine.autorestart()

autorestart()

Enable engine autorestart. This watches the main python script and all imported modules that are located in the same directoty. Whenever a file is modified, the engine is restarted.

Engine.add_module()

add_module(module)

Add a module. This method will create midi ports if the module’s protocol is ‘midi’.

Parameters

Engine.send()

send(protocol, port, address, *args)

Send OSC / MIDI message.

Parameters

Engine.add_route()

add_route(route)

Add a route.

Parameters

Engine.set_route()

set_route(name)

Set active route.

Parameters

Engine.route()

route(protocol, port, address, args)

Unified route for osc and midi messages, called when the engine receives a midi or osc message. Messages are processed this way :

  1. If the message is sent from a port that matches a module’s, the message is passed to that module’s route method. If it returns False, further processing is prevented.

  2. If the address matches a command as per the generic control API, further processing is prevented.

  3. If a route is active, the message is passed to that route’s route method.

Parameters

Engine.set_tempo()

set_tempo(bpm)

Set engine tempo.

Parameters

Engine.set_cycle_length()

set_cycle_length(quarter_notes)

Set engine cycle (measure) length in quarter notes.

Parameters

Engine.set_time_signature()

set_time_signature(signature)

Set engine cycle (measure) length from a musical time signature.

Parameters

Engine.start_cycle()

start_cycle()

Set current time as cycle start. Affects Route.wait_next_cycle() method.

Engine.fastforward()

fastforward(amount, mode=‘beats’)

/!\ Experimental /!\

Increment current time by a number of beats or seconds. All parameter animations and wait() calls will be affected.

Parameters


Module

Interface between a software / hardware and the engine.

Instance properties

Events

Module()

Module(name, protocol=None, port=None, parent=None)

Base Module constructor.

Parameters

Module.add_submodule()

add_submodule(*modules)

Add a submodule.

Submodule’s protocol and port can be omitted, in which case they will be inherited from their parent.

A submodule can send messages but it will not receive messages through its route method.

The submodule’s parent instance must be provided in its constructor function (parent argument).

Parameters

Module.set_aliases()

set_aliases(aliases)

Set aliases for submodules. Aliases can be used in place of the submodule_name argument in some methods.

Parameters

Module.add_parameter()

add_parameter(name, address, types, static_args=[], default=None)

Add parameter to module.

Parameters

Module.remove_parameter()

remove_parameter(name)

Remove parameter from module.

Parameters

Module.get()

get(parameter_name)
get(submodule_name, param_name)

Get value of parameter

Parameters

Return

List of values

Module.set()

set(parameter_name, args, force_send=False, preserve_animation=False)
set(submodule_name, parameter_name,
args, force_send=False, preserve_animation=False)

Set value of parameter.

The engine will apply the new value only at the end of current processing cycle and send a message if the new value differs from the one that was previously sent.

When in a scene, subsequent calls to set() are not guaranteed to be executed within the same processing cycle. (see lock())

Parameters

Module.reset()

reset(parameter_name=None)
reset(submodule_name, parameter_name=None)

Reset parameter to its default values.

Parameters

Module.animate()

animate(parameter_name, start, end, duration, mode=‘beats’, easing=‘linear’, loop=False)
animate(submodule_name, parameter_name, start, end, duration, mode=‘beats’, easing=‘linear’, loop=False)

Animate parameter.

Parameters

Module.stop_animate()

stop_animate(parameter_name)
stop_animate(submodule_name, param_name)

Stop parameter animation.

Parameters

Module.add_mapping()

add_mapping(self, src, dest, transform, inverse=None)

Add a value mapping between two or more parameters owned by the module or one of its submodules. Whenever a value change occurs in one of the source parameters, transform will be called and its result will be dispatched to the destination parameters.

Parameters

Module.add_meta_parameter()

add_meta_parameter(name, parameters, getter, setter)

Add a special parameter whose value depends on the state of one or several parameters owned by the module or its submodules.

Parameters

Module.add_alias_parameter()

add_alias_parameter(name, parameter)

Add a special parameter that just mirrors another parameter owned by the module or its submodules. Under the hood, this creates a parameter and a 1:1 mapping between them.

Parameters

Module.get_state()

get_state()

Get state of all parameters and submodules’ parameters.

Parameters

Return

List of lists that can be fed to set()

Module.set_state()

set_state(state)

Set state of any number of parameters and submodules’ parameters.

Parameters

Module.send_state()

send_state()

Send current state of all parameters and submodules’ parameters.

Module.save()

save(name, omit_defaults=False)

Save current state (including submodules) to a JSON file.

Parameters

Module.load()

load(name, force_send=False)

Load state from memory or from file if not preloaded already. The file must be valid a JSON file containing one list of lists as returned by get_state(). Comments may be added manually by inserting string items in the main list:

[
    "This is a comment",
    ["parameter_a", 1.0],
    ["parameter_b", 2.0],
    "etc"
]

Parameters

Module.route()

route(address, args)

Route messages received by the engine on the module’s port. Does nothing by default, method should be overriden in subclasses. Not called on submodules.

Parameters

Return

False if the message should not be passed to the engine’s active route after being processed by the module.

Module.send()

send(address, *args)

Send message to the module’s port.

Parameters

Module.start_scene()

start_scene(name, scene, *args, **kwargs)

Start scene in a thread. If a scene with the same name is already running, it will be stopped. Scenes should be implemented as methods of the object or lambda functions and can call self.wait() and self.play_sequence() to create timed sequences or loops. Different objects may call a scene with the same name simultaneously.

Parameters

Module.restart_scene()

restart_scene(name)

Restart a scene that’s already running. Does nothing if the scene is not running.

Parameters

Module.stop_scene()

stop_scene(name)

Stop scene thread.

Parameters

Module.wait()

wait(duration, mode=‘beats’)

Wait for given amount of time. Can only be called in scenes. Subsequent calls to wait() in a scene do not drift with time and can be safely used to create beat sequences. The engine’s tempo must be set for the beats mode to work.

# Example
beat_1()
self.wait(1, 'b') # will wait 1 beat minus beat_1's exec time
beat_2()
self.wait(1, 'b') # will wait 1 beat minus beat_1 and beat_2's exec time

Parameters

Module.wait_next_cycle()

wait_next_cycle()

Wait until next cycle begins. The engine’s tempo and cycle_length must be set and the engine’s start_cycle() method must be called at the beginning of a cycle for this to work.

Module.lock()

lock()

Returns the engine’s main loop lock. Can only be called in scenes. While held, the engine’s main loop will be paused (no message will be processed, parameter changes won’t be applied).

This can be used when multiple parameter changes must happen within a single processing cycle and prevent sending unnecessary intermediate values (example 1).

Examples

# example 1
with self.lock():
    # reset all parameters
    module_a.reset()
    # set a parameter, but if its value
    # was the same before being reset
    # no message will be sent
    module_a.set('some_param', 1)

Module.run()

run(callback, *args, **kwargs)

Run callback in the mainthread. Can only be called in scenes.

Parameters

Module.play_sequence()

play_sequence(sequence, loop=True)

Play a sequence of actions scheduled on arbitrary beats. Can only be called in scenes.

Parameters

Example

# single-bar sequence
play_sequence({
    'signature': '4/4',
    # beat 1
    1: lambda: guitar.set('mute', 1),
    # beat 3
    3: lambda: [guitar.set('mute', 0), guitar.set('disto', 1)],
    # "and" of beat 4
    4.5: lambda: guitar.set('disto', 0),
})

# multi-bar sequence
play_sequence([
    {   # bar 1
        'signature': '4/4',
        # beat 1
        1: lambda: voice.set('autotune', 1),
    },
    {}, # bar 2 (empty)
    {   # bar 3 (empty, but in 3/4)
        'signature': '3/4',
    },
    {   # bar 4 (in 3/4 too)
        # beat 1
        1: lambda: foo.set('autotune', 0),
        # beat 3
        3: lambda: foo.set('autotune', 1),
    }
])

Module.add_event_callback()

add_event_callback(event, callback)

Bind a callback function to an event. See Module and Engine for existing events.

Parameters

Module.dispatch_event()

dispatch_event(event, *args)

Dispatch event to bound callback functions. Unless the callback returns False, the event will be passed to the parent module until it reaches the engine module.

Parameters


Route

Routing object that processes messages received by the engine when active.

Instance properties

Route()

Route(name)

Route object constructor.

Parameters

Route.activate()

activate()

Called when the engine switches to this route.

Route.deactivate()

deactivate()

Called when the engine switches to another route.

Route.route()

route(protocol, port, address, args)

Process messages received by the engine.

Parameters

Route.start_scene()

start_scene(name, scene, *args, **kwargs)

Start scene in a thread. If a scene with the same name is already running, it will be stopped. Scenes should be implemented as methods of the object or lambda functions and can call self.wait() and self.play_sequence() to create timed sequences or loops. Different objects may call a scene with the same name simultaneously.

Parameters

Route.restart_scene()

restart_scene(name)

Restart a scene that’s already running. Does nothing if the scene is not running.

Parameters

Route.stop_scene()

stop_scene(name)

Stop scene thread.

Parameters

Route.wait()

wait(duration, mode=‘beats’)

Wait for given amount of time. Can only be called in scenes. Subsequent calls to wait() in a scene do not drift with time and can be safely used to create beat sequences. The engine’s tempo must be set for the beats mode to work.

# Example
beat_1()
self.wait(1, 'b') # will wait 1 beat minus beat_1's exec time
beat_2()
self.wait(1, 'b') # will wait 1 beat minus beat_1 and beat_2's exec time

Parameters

Route.wait_next_cycle()

wait_next_cycle()

Wait until next cycle begins. The engine’s tempo and cycle_length must be set and the engine’s start_cycle() method must be called at the beginning of a cycle for this to work.

Route.lock()

lock()

Returns the engine’s main loop lock. Can only be called in scenes. While held, the engine’s main loop will be paused (no message will be processed, parameter changes won’t be applied).

This can be used when multiple parameter changes must happen within a single processing cycle and prevent sending unnecessary intermediate values (example 1).

Examples

# example 1
with self.lock():
    # reset all parameters
    module_a.reset()
    # set a parameter, but if its value
    # was the same before being reset
    # no message will be sent
    module_a.set('some_param', 1)

Route.run()

run(callback, *args, **kwargs)

Run callback in the mainthread. Can only be called in scenes.

Parameters

Route.play_sequence()

play_sequence(sequence, loop=True)

Play a sequence of actions scheduled on arbitrary beats. Can only be called in scenes.

Parameters

Example

# single-bar sequence
play_sequence({
    'signature': '4/4',
    # beat 1
    1: lambda: guitar.set('mute', 1),
    # beat 3
    3: lambda: [guitar.set('mute', 0), guitar.set('disto', 1)],
    # "and" of beat 4
    4.5: lambda: guitar.set('disto', 0),
})

# multi-bar sequence
play_sequence([
    {   # bar 1
        'signature': '4/4',
        # beat 1
        1: lambda: voice.set('autotune', 1),
    },
    {}, # bar 2 (empty)
    {   # bar 3 (empty, but in 3/4)
        'signature': '3/4',
    },
    {   # bar 4 (in 3/4 too)
        # beat 1
        1: lambda: foo.set('autotune', 0),
        # beat 3
        3: lambda: foo.set('autotune', 1),
    }
])