Mentat
Version: 1.6.1 (13/10/2024)
License: GNU/GPL v3 (© 2024
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
modules
: dict
containing modules added to
the engine with names as keys
routes
: dict
containing routes added to
the engine with names as keys
active_route
: active route object (None
by
default)
restarted
: True
if the engine was
restarted using autorestart()
logger
: python logger
tempo
: beats per minute
cycle_length
: quarter notes per cycle
port
: osc (udp) input port number
tcp_port
: osc (tcp) input port number
unix_port
: osc (unix) input socket path
Events
started
: emitted when the engine starts.
stopping
: emitted before the engine stops
stopped
: emitted when the engine is stopped
route_added
: emitted when a route is added to the
engine. Arguments:
route_changed
: emitted when the engine’s active route
changes. Arguments:
name
: active route instance
Engine()
Engine(name, port, folder, debug=False, tcp_port=None,
unix_port=None)
Engine constructor.
Parameters
name
: client name
port
: osc (udp) input port number
folder
: path to config folder where state files will be
saved to and loaded from
debug
: set to True to enable debug messages and i/o
statistics
tcp_port
: osc (tcp) input port number
unix_port
: osc (unix) input socket path
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
protocol
: ‘osc’, ‘osc.tcp’, ‘osc.unix’ or ‘midi’
port
: module name, port number (‘osc’ protocol only) or
unix socket path (‘osc.unix’ protocol only)
address
: osc address
args
: values or (typetag, value) tuples
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 :
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.
If the address matches a command as per the generic control API,
further processing is prevented.
If a route is active, the message is passed to that route’s route
method.
Parameters
protocol
: ‘osc’, ‘osc.tcp’ or ‘midi’
port
: name of module or port number if unknown
address
: osc address
args
: list of values
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
quarter_notes
: quarter notes per cycle (decimals
allowed)
Engine.set_time_signature()
set_time_signature(signature)
Set engine cycle (measure) length from a musical time signature.
Parameters
signature
: string
like ‘4/4’, ‘5/4’,
‘7/8’…
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
duration
: number of beats or seconds
mode
: ‘beats’ or ‘seconds’ (only the first letter
matters)
Module
Interface between a software / hardware and the engine.
Instance properties
engine
: Engine instance
logger
: python logger
name
: module name
parent_module
: parent module instance,
None
if the module is not a submodule
module_path
: list of module names, from topmost parent
(engine) to submodule
submodules
: dict
containing submodules
added to the module with names as keys
parameters
: dict
containing parameters
added to the module with names as keys
meta_parameters
: dict
containing meta
parameters added to the module with names as keys
Events
module_added
: emitted when a submodule is added to a
module. Arguments:
module
: instance of parent module
submodule
: instace of child module
parameter_added
: emitted when a parameter is added to a
module. Arguments:
module
: instance of module that emitted the event
name
: name of parameter
parameter_changed
: emitted when a module’s parameter
changes. Arguments:
module
: instance of module that emitted the event
name
: name of parameter
value
: value of parameter or list of values
Module()
Module(name, protocol=None, port=None, parent=None)
Base Module constructor.
Parameters
name
: module name
protocol
: ‘osc’, ‘osc.tcp’, ‘osc.unix’ or ‘midi’
port
: port used by the software / hardware to send and
receive messages
- port number if protocol is ‘osc’ or ‘osc.tcp’
- unix socket path if protocol is ‘osc.unix’
None
if protocol is ‘midi’ or if no port is needed
parent
: if the module is a submodule, this must be set
to the parent module’s instance
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
modules
: Module objects (one module per argument)
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
aliases
: {alias: name} dictionary
Module.add_parameter()
add_parameter(name, address, types, static_args=[],
default=None)
Add parameter to module.
Parameters
name
: name of parameter
address
: osc address of parameter (None
if
the parameter should not send any message)
types
: osc typetags string, one letter per value,
including static values (character ’*’ can be used for arguments that
should not be explicitely typed)
static_args
: list of static values before the ones that
can be modified
default
: value or list of values if the parameter has
multiple dynamic values
Module.remove_parameter()
remove_parameter(name)
Remove parameter from module.
Parameters
name
: name of parameter, ’*’ to delete all
parameters
Module.get()
get(parameter_name)
get(submodule_name,
param_name)
Get value of parameter
Parameters
parameter_name
: name of parameter
submodule_name
: name of submodule
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 trigger events related to the value change 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
parameter_name
: name of parameter
submodule_name
: name of submodule, with wildcard (’*‘)
and range (’[]’) support
*args
: value(s)
force_send
: send a message even if the parameter’s
value has not changed
preserve_animation
: by default, animations are
automatically stopped when set()
is called, set to
True
to prevent that
Module.reset()
reset(parameter_name=None)
reset(submodule_name,
parameter_name=None)
Reset parameter to its default values.
Parameters
submodule_name
: name of submodule, with wildcard (’*‘)
and range (’[]’) support
parameter_name
: name of parameter. If omitted, affects
all parameters including submodules’
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
parameter_name
: name of parameter
submodule_name
: name of submodule, with wildcard (’*‘)
and range (’[]’) support
start
: starting value(s), can be None to use current
value (only for single value parameters)
end
: ending value(s), can be None to use current value
(only for single value parameters)
duration
: animation duration
mode
: ‘seconds’ or ‘beats’
easing
: easing function name.
- available easings: linear, sine, quadratic, cubic, quartic, quintic,
exponential, random, elastic (sinc)
- easing name can be suffixed with
-mirror
(back and
forth animation)
- easing name can be suffixed with
-out
(inverted and
flipped easing) or -inout
(linear interpolation between
default and -out
). Example:
exponential-mirror-inout
.
loop
: if set to True
, the animation will
start over when duration
is reached (use mirror easing for
back-and-forth loop)
Module.stop_animate()
stop_animate(parameter_name)
stop_animate(submodule_name, param_name)
Stop parameter animation.
Parameters
parameter_name
: name of parameter, can be ’*’ to stop
all animations including submodules’.
submodule_name
: name of submodule
Module.add_mapping()
add_mapping(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
src
: source parameter(s), can be
string
if there’s only one source parameter owned by
the module itself
tuple
of string
if the source parameter is
owned by a submodule
(e.g. ('submodule_name', 'parameter_name')
)
list
containing either of the above if there are
multiple source parameters.
dest
: destination parameter(s), see
src
transform
: function that takes one argument per source
parameter and returns a value for the destination parameters or a list
if there are multiple destination parameters.
inverse
: same as transform
but for
updating source parameters when destination parameters update. If
transform
and inverse
are inconsistent
(e.g. transform(inverse(x)) != x
), mappings will not
trigger each others indefinetely (a mapping cannot run twice during a
cycle).
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
name
: name of meta parameter
parameters
: parameters involved in the meta parameter’s
definition, can be
string
if there’s only one source parameter owned by
the module itself
tuple
of string
if the source parameter is
owned by a submodule
(e.g. ('submodule_name', 'parameter_name')
)
list
containing either of the above if there are
multiple source parameters.
getter
: callback function that will be called with the
values of each parameters
as arguments whenever one these
parameters changes. Its return value will define the meta parameter’s
value.
setter
: callback function used to set the value of each
parameters
when set()
is called to change the
meta parameter’s value. The function’s signature must not use *args or
**kwargs arguments.
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
name
: name of alias parameter
parameter
: name of parameter to mirror, may a be tuple
if the parameter are owned by a submodule
(('submodule_name', 'parameter_name')
)
Module.get_state()
get_state()
Get state of all parameters and submodules’ parameters.
Parameters
omit_defaults
: set to True
to only
retreive parameters that differ from their default values.
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
state
: state object as returned by
get_state()
force_send
: see set()
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
name
: name of state save (without file extension)
omit_defaults
: set to True
to only save
parameters that differ from their default values.
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
name
: name of state save (without file extension)
force_send
: see set()
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
address
: osc address
args
: list of values
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
address
: osc address
*args
: values, or (typetag, value) tuples
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
name
: scene name
scene
: function or method
*args
: arguments for the scene function
**kwargs
: keyword arguments for the scene function
Module.restart_scene()
restart_scene(name)
Restart a scene that’s already running. Does nothing if the scene is
not running.
Parameters
name
: scene name, with wildcard (’*‘) and range (’[]’)
support
Module.stop_scene()
stop_scene(name)
Stop scene thread.
Parameters
name
: scene name, with wildcard support
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
duration
: amount of time to wait
mode
: ‘beats’ or ‘seconds’ (only the first letter
matters)
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
callback
: function or method
*args
: arguments for the scene function
**kwargs
: keyword arguments for the scene function
Module.play_sequence()
play_sequence(sequence, loop=True)
Play a sequence of actions scheduled on arbitrary beats. Can only be
called in scenes.
Parameters
sequence
:
dict
with beat numbers (1-indexed, quarter note based)
as keys and lambda functions as values
list
of dict
sequences (one sequence = one
bar)
- sequences may contain an extra ‘signature’ string to change the time
signature. If not provided in the first sequence, the signature takes
the engine’s cycle length value.
loop
: if False
, the sequence will play
only once, otherwise it will loop until the scene is stopped
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
event
: name of event
callback
: function or method. The callback’s signature
must match the event’s arguments.
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
event
: name of event
*args
: arguments for the callback function
Route
Routing object that processes messages received by the engine when
active.
Instance properties
engine
: Engine instance
logger
: python logger
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
protocol
: ‘osc’, ‘osc.tcp’ or ‘midi’
port
: name of module or port number if unknown
address
: osc address
args
: list of values
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
name
: scene name
scene
: function or method
*args
: arguments for the scene function
**kwargs
: keyword arguments for the scene function
Route.restart_scene()
restart_scene(name)
Restart a scene that’s already running. Does nothing if the scene is
not running.
Parameters
name
: scene name, with wildcard (’*‘) and range (’[]’)
support
Route.stop_scene()
stop_scene(name)
Stop scene thread.
Parameters
name
: scene name, with wildcard support
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
duration
: amount of time to wait
mode
: ‘beats’ or ‘seconds’ (only the first letter
matters)
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
callback
: function or method
*args
: arguments for the scene function
**kwargs
: keyword arguments for the scene function
Route.play_sequence()
play_sequence(sequence, loop=True)
Play a sequence of actions scheduled on arbitrary beats. Can only be
called in scenes.
Parameters
sequence
:
dict
with beat numbers (1-indexed, quarter note based)
as keys and lambda functions as values
list
of dict
sequences (one sequence = one
bar)
- sequences may contain an extra ‘signature’ string to change the time
signature. If not provided in the first sequence, the signature takes
the engine’s cycle length value.
loop
: if False
, the sequence will play
only once, otherwise it will loop until the scene is stopped
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),
}
])