import reapy
from reapy import reascript_api as RPR
from reapy.core import ReapyObject, ReapyObjectList
from reapy.errors import InvalidObjectError, UndefinedEnvelopeError
[docs]class Track(ReapyObject):
"""
REAPER Track.
Parameters
----------
id : str or int
If str, can either be a ReaScript ID (usually looking like
``"(MediaTrack*)0x00000000110A1AD0"``), or a track name. In
that case, ``project`` must be specified.
If int, the index of the track. In that case, ``project`` must
be specified.
project : Project
Parent project of the track. Only necessary to retrieve a
track from its name or index.
Examples
--------
In most cases, accessing tracks is better done directly from
the parent Project:
>>> project = reapy.Project()
>>> project.tracks[0]
Track("(MediaTrack*)0x00000000110A1AD0")
>>> project.tracks["PIANO"] # This is actually the same track
Track("(MediaTrack*)0x00000000110A1AD0")
But the same track can also directly be instantiated with:
>>> reapy.Track(0, project)
Track("(MediaTrack*)0x00000000110A1AD0")
or
>>> reapy.Track("PIANO", project)
Track("(MediaTrack*)0x00000000110A1AD0")
"""
def __init__(self, id, project=None):
self._project = None
if isinstance(id, int): # id is a track index
id = RPR.GetTrack(project.id, id)
if id.endswith("0x0000000000000000"):
raise IndexError('Track index out of range')
self._project = project
elif isinstance(id, str) and not id.startswith("(MediaTrack*)"):
# id is a track name
name = id
id = project._get_track_by_name(name).id
if id.endswith("0x0000000000000000"):
raise KeyError(name)
self._project = project
# id is now a real ReaScript ID
self.id = id
@property
def _args(self):
return self.id,
@classmethod
def _get_id_from_pointer(cls, pointer):
return '(MediaTrack*)0x{0:0{1}X}'.format(int(pointer), 16)
@reapy.inside_reaper()
def _get_project(self):
"""
Return parent project of track.
Should only be used internally; one should directly access
Track.project instead of calling this method.
"""
for project in reapy.get_projects():
if self.id in [t.id for t in project.tracks]:
return project
[docs] def add_audio_accessor(self):
"""
Create audio accessor and return it.
Returns
-------
audio_accessor : AudioAccessor
Audio accessor on track.
"""
audio_accessor_id = RPR.CreateTrackAudioAccessor(self.id)
audio_accessor = reapy.AudioAccessor(audio_accessor_id)
return audio_accessor
[docs] def add_fx(self, name, input_fx=False, even_if_exists=True):
"""
Add FX to track and return it.
Parameters
----------
name : str
FX name.
input_fx : bool, optional
Whether the FX should be an input (aka recording) FX or a
standard FX (default=False). Note that if the track is the
master track, input_fx=True will create a monitoring FX.
even_if_exists : bool, optional
Whether the FX should be added even if there already is an
instance of the same FX on the track (default=True).
Returns
-------
fx : FX
New FX on track (or previously existing instance of FX if
even_if_exists=False).
Raises
------
ValueError
If there is no FX with the specified name.
"""
index = RPR.TrackFX_AddByName(
self.id, name, input_fx, 1 - 2 * even_if_exists
)
if index == -1:
raise ValueError("Can't find FX named {}".format(name))
fx = reapy.FX(self, index)
return fx
[docs] @reapy.inside_reaper()
def add_item(self, start=0, end=None, length=0):
"""
Create new item on track and return it.
Parameters
----------
start : float, optional
New item start in seconds (default=0).
end : float, optional
New item end in seconds (default None). If None, `length`
is used instead.
length : float, optional
New item length in seconds (default 0).
Returns
-------
item : Item
New item on track.
"""
if end is None:
end = start + length
item = reapy.Item(RPR.AddMediaItemToTrack(self.id))
item.position = start
item.length = end - start
return item
[docs] def add_midi_item(self, start=0, end=1, quantize=False):
"""
Add empty MIDI item to track and return it.
Parameters
----------
start : float, optional
New item start in seconds (or beats if `quantize`=True).
end : float, optional
New item end in seconds (or beats if `quantize`=True).
quantize : bool, optional
Whether to count time in beats (True) or seconds (False,
default).
"""
item_id = RPR.CreateNewMIDIItemInProj(self.id, start, end, quantize)[0]
item = reapy.Item(item_id)
return item
[docs] def add_send(self, destination=None):
"""
Add send to track and return it.
Parameters
----------
destination : Track or None
Send destination (default=None). If None, destination is
set to hardware output.
Returns
-------
send : Send
New send on track.
"""
if isinstance(destination, Track):
destination = destination.id
send_id = RPR.CreateTrackSend(self.id, destination)
type = "hardware" if destination is None else "send"
send = reapy.Send(self, send_id, type=type)
return send
@property
def automation_mode(self):
"""
Track automation mode.
One of the following values:
"latch"
"latch preview"
"read"
"touch"
"trim/read"
"write"
:type: str
"""
modes = "trim/read", "read", "touch", "write", "latch", "latch preview"
automation_mode = modes[RPR.GetTrackAutomationMode(self.id)]
return automation_mode
@automation_mode.setter
def automation_mode(self, mode):
"""
Set track automation mode.
Parameters
-------
mode : str
One of the following values:
"latch"
"latch preview"
"read"
"touch"
"trim/read"
"write"
"""
modes = "trim/read", "read", "touch", "write", "latch", "latch preview"
RPR.SetTrackAutomationMode(self.id, modes.index(mode))
@property
def color(self):
"""
Track color in RGB format.
:type: tuple of int
"""
native_color = RPR.GetTrackColor(self.id)
r, g, b = reapy.rgb_from_native(native_color)
return r, g, b
@color.setter
def color(self, color):
"""
Set track color to `color`
Parameters
----------
color : tuple
Triplet of integers between 0 and 255 corresponding to RGB
values.
"""
native_color = reapy.rgb_to_native(color)
RPR.SetTrackColor(self.id, native_color)
[docs] def delete(self):
"""
Delete track.
"""
RPR.DeleteTrack(self.id)
@property
def depth(self):
"""
Track depth.
:type: int
"""
depth = RPR.GetTrackDepth(self.id)
return depth
@property
def envelopes(self):
"""
List of envelopes on track.
:type: EnvelopeList
"""
return reapy.EnvelopeList(self)
@property
def fxs(self):
"""
List of FXs on track.
:type: FXList
"""
fxs = reapy.FXList(self)
return fxs
[docs] def get_info_string(self, param_name):
return RPR.GetSetMediaTrackInfo_String(self.id, param_name, "", False)[3]
[docs] def get_info_value(self, param_name):
value = RPR.GetMediaTrackInfo_Value(self.id, param_name)
return value
@property
def GUID(self):
"""
Track's GUID.
16-byte GUID, can query or update.
If using a _String() function, GUID is a string {xyz-...}.
:type: str
"""
return RPR.GetTrackGUID(self.id)
@GUID.setter
def GUID(self, guid_string):
self.set_info_string("GUID", guid_string)
@reapy.inside_reaper()
@property
def has_valid_id(self):
"""
Whether ReaScript ID is still valid.
For instance, if track has been deleted, ID will not be valid
anymore.
:type: bool
"""
pointer, name = self._get_pointer_and_name()
if self._project is None:
return any(
RPR.ValidatePtr2(p.id, pointer, name)
for p in reapy.get_projects()
)
return bool(RPR.ValidatePtr2(self.project.id, pointer, name))
@property
def icon(self):
"""
Track icon.
Full filename, or relative to resource_path/data/track_icons.
:type: str
"""
return self.get_info_string("P_ICON")
@icon.setter
def icon(self, filename):
self.set_info_string("P_ICON", filename)
@property
def index(self):
"""Track index in GUI (0-based).
Will be ``None`` for master track.
:type: int or None
Raises
------
InvalidObjectError
When track does not exist in REAPER.
"""
index = int(self.get_info_value('IP_TRACKNUMBER')) - 1
if index >= 0:
return index
if index == -1:
raise InvalidObjectError(self)
@property
def instrument(self):
"""
First instrument FX on track if it exists, else None.
:type: FX or None
"""
fx_index = RPR.TrackFX_GetInstrument(self.id)
instrument = None if fx_index == -1 else reapy.FX(self, fx_index)
return instrument
@reapy.inside_reaper()
@property
def items(self):
"""
List of items on track.
:type: list of Item
"""
n_items = RPR.CountTrackMediaItems(self.id)
item_ids = [
RPR.GetTrackMediaItem(self.id, i) for i in range(n_items)
]
return list(map(reapy.Item, item_ids))
@property
def is_muted(self):
"""
Whether track is muted.
Can be manually set to change track state.
"""
return bool(self.get_info_value("B_MUTE"))
@is_muted.setter
def is_muted(self, muted):
if muted:
self.mute()
else:
self.unmute()
@property
def is_selected(self):
"""
Whether track is selected.
:type: bool
"""
is_selected = bool(RPR.IsTrackSelected(self.id))
return is_selected
@is_selected.setter
def is_selected(self, selected):
"""
Select or unselect track.
Parameters
----------
selected : bool
Whether to select or unselect track.
"""
if selected:
self.select()
else:
self.unselect()
@property
def is_solo(self):
"""
Whether track is solo.
Can be manually set to change track state.
"""
return bool(self.get_info_value("I_SOLO"))
@is_solo.setter
def is_solo(self, solo):
if solo:
self.solo()
else:
self.unsolo()
[docs] def make_only_selected_track(self):
"""
Make track the only selected track in parent project.
"""
RPR.SetOnlyTrackSelected(self.id)
[docs] def midi_hash(self, notes_only=False):
"""Get hash of MIDI-data to compare with later.
Parameters
----------
notes_only : bool, (False by default)
count only notes if True
Returns
-------
str
"""
return RPR.MIDI_GetTrackHash(self.id, notes_only, 'hash', 1024**2)[3]
@property
def midi_note_names(self):
with reapy.inside_reaper():
names = [
RPR.GetTrackMIDINoteName(self.id, i, 0) for i in range(128)
]
return names
[docs] @reapy.inside_reaper()
def mute(self):
"""Mute track (do nothing if track is already muted)."""
if not self.is_muted:
self.toggle_mute()
@property
def n_envelopes(self):
"""
Number of envelopes on track.
:type: int
"""
n_envelopes = RPR.CountTrackEnvelopes(self.id)
return n_envelopes
@property
def n_fxs(self):
"""
Number of FXs on track.
:type: int
"""
n_fxs = RPR.TrackFX_GetCount(self.id)
return n_fxs
@property
def n_hardware_sends(self):
"""
Number of hardware sends on track.
:type: int
"""
n_hardware_sends = RPR.GetTrackNumSends(self.id, 1)
return n_hardware_sends
@property
def n_items(self):
"""
Number of items on track.
:type: int
"""
n_items = RPR.CountTrackMediaItems(self.id)
return n_items
@property
def n_receives(self):
n_receives = RPR.GetTrackNumSends(self.id, -1)
return n_receives
@property
def n_sends(self):
n_sends = RPR.GetTrackNumSends(self.id, 0)
return n_sends
@property
def name(self):
"""
Track name.
Name is "MASTER" for master track, "Track N" if track has no
name.
:type: str
Track name .
"""
_, _, name, _ = RPR.GetTrackName(self.id, "", 2048)
return name
@name.setter
def name(self, track_name):
self.set_info_string("P_NAME", track_name)
@property
def parent_track(self):
"""
Parent track, or None if track has none.
:type: Track or NoneType
"""
parent = Track(RPR.GetParentTrack(self.id))
if not parent._is_defined:
parent = None
return parent
@property
def project(self):
"""
Track parent project.
:type: Project
"""
if self._project is None:
self._project = self._get_project()
return self._project
@reapy.inside_reaper()
@property
def receives(self):
return [
reapy.Send(self, i, type="receive") for i in range(self.n_receives)
]
[docs] def select(self):
"""
Select track.
"""
RPR.SetTrackSelected(self.id, True)
@reapy.inside_reaper()
@property
def sends(self):
return [
reapy.Send(self, i, type="send") for i in range(self.n_sends)
]
[docs] def set_info_string(self, param_name, param_string):
RPR.GetSetMediaTrackInfo_String(
self.id, param_name, param_string, True)
[docs] def set_info_value(self, param_name, param_value):
RPR.SetMediaTrackInfo_Value(self.id, param_name, param_value)
[docs] @reapy.inside_reaper()
def solo(self):
"""Solo track (do nothing if track is already solo)."""
if not self.is_solo:
self.toggle_solo()
[docs] @reapy.inside_reaper()
def toggle_mute(self):
"""Toggle mute on track."""
selected_tracks = self.project.selected_tracks
self.make_only_selected_track()
self.project.perform_action(40280)
self.project.selected_tracks = selected_tracks
[docs] @reapy.inside_reaper()
def toggle_solo(self):
"""Toggle solo on track."""
selected_tracks = self.project.selected_tracks
self.make_only_selected_track()
self.project.perform_action(7)
self.project.selected_tracks = selected_tracks
[docs] @reapy.inside_reaper()
def unmute(self):
"""Unmute track (do nothing if track is not muted)."""
if self.is_muted:
self.toggle_mute()
[docs] def unselect(self):
"""
Unselect track.
"""
RPR.SetTrackSelected(self.id, False)
[docs] @reapy.inside_reaper()
def unsolo(self):
"""Unsolo track (do nothing if track is not solo)."""
if self.is_solo:
self.toggle_solo()
@property
def visible_fx(self):
"""
Visible FX in FX chain if any, else None.
:type: FX or NoneType
"""
with reapy.inside_reaper():
return self.fxs[RPR.TrackFX_GetChainVisible(self.id)]
[docs]class TrackList(ReapyObjectList):
"""
Container for a project's track list.
Examples
--------
>>> tracks = project.tracks
>>> len(tracks)
4
>>> tracks[0].name
'Kick'
>>> for track in tracks:
... print(track.name)
...
'Kick'
'Snare'
'Hi-hat'
'Cymbal"
"""
def __init__(self, parent):
"""
Create track list.
Parameters
----------
parent : Project
Parent project.
"""
self.parent = parent
@reapy.inside_reaper()
def __delitem__(self, key):
tracks = self[key] if isinstance(key, slice) else [self[key]]
for track in tracks:
track.delete()
def __getitem__(self, key):
if isinstance(key, slice):
return self._get_items_from_slice(key)
return Track(key, self.parent)
def __iter__(self):
tracks = self[:] # Only cost one distant call
for track in tracks:
yield track
def __len__(self):
return self.parent.n_tracks
@property
def _args(self):
return self.parent,
@reapy.inside_reaper()
def _get_items_from_slice(self, slice):
indices = range(*slice.indices(len(self)))
return [self[i] for i in indices]