"""Defines class Project."""
import pickle
import codecs
import os
import reapy
from reapy import reascript_api as RPR
from reapy.core import ReapyObject
from reapy.errors import RedoError, UndoError
[docs]class Project(ReapyObject):
"""REAPER project."""
def __init__(self, id=None, index=-1):
"""
Build project either by ID or index.
Parameters
----------
id : None, str or int, optional
Project identifier.
When None (default), `index is used instead.
An integer is interpreted as the project index in GUI.
A string starting with '(ReaProject*)0x' is interpreted
as a ReaScript identifier.
Otherwise, `id` is the project name. In that case, the .rpp
extension is optional.
index : int, optional
Project index in GUI (default=-1, corresponds to current
project).
"""
if isinstance(id, int):
id, index = None, id
if id is None:
id = RPR.EnumProjects(index, None, 0)[0]
if not id.startswith('(ReaProject*)0x'):
id = Project._from_name(id).id
self.id = id
self._filename = None
def __eq__(self, other):
if hasattr(other, 'id'):
return self.id == other.id
return False
@property
def _args(self):
return self.id,
@staticmethod
def _from_name(name):
"""Return project with corresponding name.
Parameters
----------
name : str
Project file name. Including the extension ('.rpp')
is optional.
Returns
-------
Project
Raises
------
NameError
If no project with the corresponding name is open.
"""
if not name.lower().endswith('.rpp'):
name += '.rpp'
with reapy.inside_reaper():
for project in reapy.get_projects():
project_name = project.name[:-4] + '.rpp'
if project_name == name:
return project
raise NameError('"{}" is not currently open.'.format(name))
@reapy.inside_reaper()
def _get_track_by_name(self, name):
"""Return first track with matching name."""
for track in self.tracks:
if track.name == name:
return track
raise KeyError(name)
[docs] def add_marker(self, position, name="", color=0):
"""
Create new marker and return its index.
Parameters
----------
position : float
Marker position in seconds.
name : str, optional
Marker name.
color : int or tuple of int, optional
Marker color. Integers correspond to REAPER native colors.
Tuple must be RGB triplets of integers between 0 and 255.
Returns
-------
marker : reapy.Marker
New marker.
Notes
-----
If a marker with the same position and name already exists, no
new marker will be created, and the existing marker index will
be returned.
"""
if isinstance(color, tuple):
color = reapy.rgb_to_native(color) | 0x1000000
marker_id = RPR.AddProjectMarker2(
self.id, False, position, 0, name, -1, color
)
marker = reapy.Marker(self, marker_id)
return marker
[docs] def add_region(self, start, end, name="", color=0):
"""
Create new region and return its index.
Parameters
----------
start : float
Region start in seconds.
end : float
Region end in seconds.
name : str, optional
Region name.
color : int or tuple of int, optional
Region color. Integers correspond to REAPER native colors.
Tuple must be RGB triplets of integers between 0 and 255.
Returns
-------
region : reapy.Region
New region.
"""
if isinstance(color, tuple):
color = reapy.rgb_to_native(color) | 0x1000000
region_id = RPR.AddProjectMarker2(
self.id, True, start, end, name, -1, color
)
region = reapy.Region(self, region_id)
return region
[docs] @reapy.inside_reaper()
def add_track(self, index=0, name=""):
"""
Add track at a specified index.
Parameters
----------
index : int
Index at which to insert track.
name : str, optional
Name of created track.
Returns
-------
track : Track
New track.
"""
n_tracks = self.n_tracks
index = max(-n_tracks, min(index, n_tracks))
if index < 0:
index = index % n_tracks
with self.make_current_project():
RPR.InsertTrackAtIndex(index, True)
track = self.tracks[index]
track.name = name
return track
@property
def any_track_solo(self):
"""
Test whether any track is soloed in project.
Returns
-------
any_track_solo : bool
Whether any track is soloed in project.
"""
any_track_solo = bool(RPR.AnyTrackSolo(self.id))
return any_track_solo
[docs] def beats_to_time(self, beats):
"""
Convert beats to time in seconds.
Parameters
----------
beats : float
Time in beats
Returns
-------
time : float
Converted time in seconds.
See also
--------
Project.time_to_beats
"""
time = RPR.TimeMap2_QNToTime(self.id, beats)
return time
[docs] def begin_undo_block(self):
"""
Start a new undo block.
"""
RPR.Undo_BeginBlock2(self.id)
@property
def bpi(self):
"""
Return project BPI (numerator of time signature).
Returns
-------
bpi : float
Numerator of time signature.
"""
return self.time_signature[1]
@reapy.inside_reaper()
@property
def bpm(self):
"""
Project BPM (beats per minute).
:type: float
"""
return self.time_signature[0]
@bpm.setter
def bpm(self, bpm):
"""
Set project BPM (beats per minute).
Parameters
----------
bpm : float
Tempo in beats per minute.
"""
RPR.SetCurrentBPM(self.id, bpm, True)
@property
def buffer_position(self):
"""
Position of next audio block being processed in seconds.
:type: float
See also
--------
Project.play_position
Latency-compensated actual-what-you-hear position.
"""
return RPR.GetPlayPosition2Ex(self.id)
[docs] @reapy.inside_reaper()
def bypass_fx_on_all_tracks(self, bypass=True):
"""
Bypass or un-bypass FX on all tracks.
Parameters
----------
bypass : bool
Whether to bypass or un-bypass FX.
"""
with self.make_current_project():
RPR.BypassFxAllTracks(bypass)
@property
def can_redo(self):
"""
Whether redo is possible.
:type: bool
"""
try:
RPR.Undo_CanRedo2(self.id)
can_redo = True
except AttributeError: # Bug in ReaScript API
can_redo = False
return can_redo
@property
def can_undo(self):
"""
Whether undo is possible.
:type: bool
"""
try:
RPR.Undo_CanUndo2(self.id)
can_undo = True
except AttributeError: # Bug in ReaScript API
can_undo = False
return can_undo
[docs] def close(self):
"""Close project and its correspondig tab."""
self._filename = os.path.join(self.path, self.name)
with self.make_current_project():
reapy.perform_action(40860)
@property
def cursor_position(self):
"""
Edit cursor position in seconds.
:type: float
"""
position = RPR.GetCursorPositionEx(self.id)
return position
@cursor_position.setter
def cursor_position(self, position):
"""
Set edit cursor position.
Parameters
----------
position : float
New edit cursor position in seconds.
"""
RPR.SetEditCurPos(position, True, True)
[docs] @reapy.inside_reaper()
def disarm_rec_on_all_tracks(self):
"""
Disarm record on all tracks.
"""
with self.make_current_project():
RPR.ClearAllRecArmed()
[docs] def end_undo_block(self, description=""):
"""
End undo block.
Parameters
----------
description : str
Undo block description.
"""
RPR.Undo_EndBlock2(self.id, description, 0)
@reapy.inside_reaper()
@property
def focused_fx(self):
"""
FX that has focus if any, else None.
:type: FX or NoneType
"""
if not self.is_current_project:
return
res = RPR.GetFocusedFX(0, 0, 0)
if not res[0]:
return
if res[1] == 0:
track = self.master_track
else:
track = self.tracks[res[1] - 1]
if res[0] == 1: # Track FX
return track.fxs[res[3]]
# Take FX
item = track.items[res[2]]
take = item.takes[res[3] // 2**16]
return take.fxs[res[3] % 2**16]
[docs] def get_info_string(self, param_name: str) -> str:
"""
Parameters
----------
param_name : str
MARKER_GUID:X : get the GUID (unique ID) of the marker or region
with index X, where X is the index passed to
EnumProjectMarkers, not necessarily the displayed number
RECORD_PATH :
recording directory -- may be blank or a relative path,
to get the effective path see GetProjectPathEx()
RENDER_FILE : render directory
RENDER_PATTERN : render file name (may contain wildcards)
RENDER_FORMAT : base64-encoded sink configuration
(see project files, etc). Callers can also pass a simple
4-byte string (non-base64-encoded), e.g. "evaw" or "l3pm",
to use default settings for that sink type.
RENDER_FORMAT2 : base64-encoded secondary sink configuration.
Callers can also pass a simple 4-byte string (non-base64-encoded),
e.g. "evaw" or "l3pm", to use default settings for
that sink type, or "" to disable secondary render.
Formats available on this machine:
"wave" "aiff" "iso " "ddp " "flac" "mp3l" "oggv" "OggS"
"FFMP" "GIF " "LCF " "wvpk"
"""
_, _, _, result, _ = RPR.GetSetProjectInfo_String(
self.id, param_name, 'valuestrNeedBig', False
)
return result
[docs] def get_info_value(self, param_name: str) -> float:
"""
Parameters
----------
param_name : str
RENDER_SETTINGS : &(1|2)=0:master mix, &1=stems+master mix,
&2=stems only, &4=multichannel tracks to multichannel files,
&8=use render matrix, &16=tracks with only mono media
to mono files, &32=selected media items,
&64=selected media items via master
RENDER_BOUNDSFLAG : 0=custom time bounds, 1=entire project,
2=time selection, 3=all project regions,
4=selected media items, 5=selected project regions
RENDER_CHANNELS : number of channels in rendered file
RENDER_SRATE : sample rate of rendered file
(or 0 for project sample rate)
RENDER_STARTPOS : render start time when RENDER_BOUNDSFLAG=0
RENDER_ENDPOS : render end time when RENDER_BOUNDSFLAG=0
RENDER_TAILFLAG : apply render tail setting when rendering:
&1=custom time bounds, &2=entire project, &4=time selection,
&8=all project regions, &16=selected media items,
&32=selected project regions
RENDER_TAILMS : tail length in ms to render
(only used if RENDER_BOUNDSFLAG and RENDER_TAILFLAG are set)
RENDER_ADDTOPROJ : 1=add rendered files to project
RENDER_DITHER : &1=dither, &2=noise shaping, &4=dither stems,
&8=noise shaping on stems
PROJECT_SRATE : samplerate (ignored unless PROJECT_SRATE_USE set)
PROJECT_SRATE_USE : set to 1 if project samplerate is used
"""
return RPR.GetSetProjectInfo(self.id, param_name, 0, False)
[docs] def get_play_rate(self, position):
"""
Return project play rate at a given position.
Parameters
----------
position : float
Position in seconds.
Returns
-------
play_rate : float
Play rate at the given position.
See also
--------
Project.play_rate
Project play rate at the current position.
"""
play_rate = RPR.Master_GetPlayRateAtTime(position, self.id)
return play_rate
[docs] def get_selected_item(self, index):
"""
Return index-th selected item.
Parameters
----------
index : int
Item index.
Returns
-------
item : Item
index-th selected item.
"""
item_id = RPR.GetSelectedMediaItem(self.id, index)
item = reapy.Item(item_id)
return item
[docs] def get_selected_track(self, index):
"""
Return index-th selected track.
Parameters
----------
index : int
Track index.
Returns
-------
track : Track
index-th selected track.
"""
track_id = RPR.GetSelectedTrack(self.id, index)
track = reapy.Track(track_id)
return track
[docs] def get_ext_state(self, section, key, pickled=False):
"""
Return external state of project.
Parameters
----------
section : str
key : str
pickled: bool
Whether data was pickled or not.
Returns
-------
value : str
If key or section does not exist an empty string is returned.
"""
value = RPR.GetProjExtState(self.id, section, key, "", 2**31 - 1)[4]
if value and pickled:
value = pickle.loads(codecs.decode(value.encode(), "base64"))
return value
[docs] def glue_items(self, within_time_selection=False):
"""
Glue items (action shortcut).
Parameters
----------
within_time_selection : bool
If True, glue items within time selection.
"""
action_id = 41588 if within_time_selection else 40362
self.perform_action(action_id)
@property
def has_valid_id(self):
"""
Whether ReaScript ID is still valid.
For instance, if project has been closed, ID will not be valid
anymore.
:type: bool
"""
return bool(RPR.ValidatePtr(*self._get_pointer_and_name()))
@property
def is_dirty(self):
"""
Whether project is dirty (i.e. needing save).
:type: bool
"""
is_dirty = RPR.IsProjectDirty(self.id)
return is_dirty
@property
def is_current_project(self):
"""
Whether project is current project.
:type: bool
"""
is_current = self == Project()
return is_current
@property
def is_paused(self):
"""
Return whether project is paused.
:type: bool
"""
return bool(RPR.GetPlayStateEx(self.id) & 2)
@property
def is_playing(self):
"""
Return whether project is playing.
:type: bool
"""
return bool(RPR.GetPlayStateEx(self.id) & 1)
@property
def is_recording(self):
"""
Return whether project is recording.
:type: bool
"""
return bool(RPR.GetPlayStateEx(self.id) & 4)
@reapy.inside_reaper()
@property
def is_stopped(self):
"""
Return whether project is stopped.
:type: bool
"""
return not self.is_playing and not self.is_paused
@reapy.inside_reaper()
@property
def items(self):
"""
List of items in project.
:type: list of Item
"""
n_items = self.n_items
item_ids = [RPR.GetMediaItem(self.id, i) for i in range(n_items)]
return list(map(reapy.Item, item_ids))
@property
def length(self):
"""
Project length in seconds.
:type: float
"""
length = RPR.GetProjectLength(self.id)
return length
@reapy.inside_reaper()
@property
def last_touched_fx(self):
"""
Last touched FX and corresponding parameter index.
:type: FX, int or NoneType, NoneType
Notes
-----
Only Track FX are detected by this property. If last touched
FX is a Take FX, this property is ``(None, None)``.
Examples
--------
>>> fx, index = project.last_touched_fx
>>> fx.name
'VSTi: ReaSamplOmatic5000 (Cockos)'
>>> fx.params[index].name
"Volume"
"""
if not self.is_current_project:
fx, index = None, None
else:
res = RPR.GetLastTouchedFX(0, 0, 0)
if not res[0]:
fx, index = None, None
else:
if res[1]:
track = self.tracks[res[1] - 1]
else:
track = self.master_track
fx, index = track.fxs[res[2]], res[3]
return fx, index
[docs] def make_current_project(self):
"""
Set project as current project.
Can also be used as a context manager to temporarily set
the project as current project and then go back to the original
situation.
Examples
--------
>>> p1 = reapy.Project() # current project
>>> p2 = reapy.Project(1) # other project
>>> p2.make_current_project() # now p2 is current project
>>> with p1.make_current_project():
... do_something() # current project is temporarily p1
>>> # and p2 is current project again
"""
return _MakeCurrentProject(self)
[docs] def mark_dirty(self):
"""
Mark project as dirty (i.e. needing save).
"""
RPR.MarkProjectDirty(self.id)
@reapy.inside_reaper()
@property
def markers(self):
"""
List of project markers.
:type: list of reapy.Marker
"""
ids = [
RPR.EnumProjectMarkers2(self.id, i, 0, 0, 0, 0, 0)
for i in range(self.n_regions + self.n_markers)
]
return [reapy.Marker(self, i[0]) for i in ids if not i[3]]
@property
def master_track(self):
"""
Project master track.
:type: Track
"""
track_id = RPR.GetMasterTrack(self.id)
master_track = reapy.Track(track_id)
return master_track
[docs] @reapy.inside_reaper()
def mute_all_tracks(self, mute=True):
"""
Mute or unmute all tracks.
Parameters
----------
mute : bool, optional
Whether to mute or unmute all tracks (default=True).
See also
--------
Project.unmute_all_tracks
"""
with self.make_current_project():
RPR.MuteAllTracks(mute)
@property
def n_items(self):
"""
Number of items in project.
:type: int
"""
n_items = RPR.CountMediaItems(self.id)
return n_items
@property
def n_markers(self):
"""
Number of markers in project.
:type: int
"""
n_markers = RPR.CountProjectMarkers(self.id, 0, 0)[2]
return n_markers
@property
def n_regions(self):
"""
Number of regions in project.
:type: int
"""
n_regions = RPR.CountProjectMarkers(self.id, 0, 0)[3]
return n_regions
@property
def n_selected_items(self):
"""
Number of selected media items.
:type: int
"""
n_items = RPR.CountSelectedMediaItems(self.id)
return n_items
@property
def n_selected_tracks(self):
"""
Number of selected tracks in project (excluding master).
:type: int
"""
n_tracks = RPR.CountSelectedTracks2(self.id, False)
return n_tracks
@property
def n_tempo_markers(self):
"""
Number of tempo/time signature markers in project.
:type: int
"""
n_tempo_markers = RPR.CountTempoTimeSigMarkers(self.id)
return n_tempo_markers
@property
def n_tracks(self):
"""
Number of tracks in project.
:type: int
"""
n_tracks = RPR.CountTracks(self.id)
return n_tracks
@property
def name(self):
"""
Project name.
:type: str
"""
_, name, _ = RPR.GetProjectName(self.id, "", 2048)
return name
[docs] def open(self, in_new_tab=False):
"""
Open project, if it was closed by Project.close.
Parameters
----------
in_new_tab : bool, optional
whether should be opened in new tab
Raises
------
RuntimeError
If hasn't been closed by Project.close yet
"""
if self._filename is None:
raise RuntimeError("project hasn't been closed")
self.id = reapy.open_project(self._filename, in_new_tab).id
[docs] def pause(self):
"""
Hit pause button.
"""
RPR.OnPauseButtonEx(self.id)
@property
def path(self):
"""
Project path.
:type: str
"""
_, path, _ = RPR.GetProjectPathEx(self.id, "", 2048)
return path
[docs] def play(self):
"""
Hit play button.
"""
RPR.OnPlayButtonEx(self.id)
@property
def play_position(self):
"""
Latency-compensated actual-what-you-hear position in seconds.
:type: float
See also
--------
Project.buffer_position
Position of next audio block being processed.
"""
return RPR.GetPlayPositionEx(self.id)
@property
def play_rate(self):
"""
Project play rate at the cursor position.
:type: float
See also
--------
Project.get_play_rate
Return project play rate at a specified time.
"""
play_rate = RPR.Master_GetPlayRate(self.id)
return play_rate
[docs] @reapy.inside_reaper()
def record(self):
"""Hit record button."""
with self.make_current_project():
reapy.perform_action(1013)
[docs] def redo(self):
"""
Redo last action.
Raises
------
RedoError
If impossible to redo.
"""
success = RPR.Undo_DoRedo2(self.id)
if not success:
raise RedoError
@reapy.inside_reaper()
@property
def regions(self):
"""
List of project regions.
:type: list of reapy.Region
"""
ids = [
RPR.EnumProjectMarkers2(self.id, i, 0, 0, 0, 0, 0)
for i in range(self.n_regions + self.n_markers)
]
return [reapy.Region(self, i[0]) for i in ids if i[3]]
[docs] def save(self, force_save_as=False):
"""
Save project.
Parameters
----------
force_save_as : bool
Force using "Save as" instead of "Save".
"""
RPR.Main_SaveProject(self.id, force_save_as)
[docs] def select(self, start, end=None, length=None):
if end is None:
message = "Either `end` or `length` must be specified."
assert length is not None, message
end = start + length
self.time_selection = start, end
[docs] def select_all_items(self, selected=True):
"""
Select or unselect all items, depending on `selected`.
Parameters
----------
selected : bool
Whether to select or unselect items.
"""
RPR.SelectAllMediaItems(self.id, selected)
[docs] def select_all_tracks(self):
"""Select all tracks."""
self.perform_action(40296)
@property
def selected_envelope(self):
"""
Project selected envelope.
:type: reapy.Envelope or None
"""
envelope_id = RPR.GetSelectedTrackEnvelope(self.id)
envelope = None if envelope_id == 0 else reapy.Envelope(envelope_id)
return envelope
@reapy.inside_reaper()
@property
def selected_items(self):
"""
List of all selected items.
:type: list of Item
See also
--------
Project.get_selected_item
Return a specific selected item.
"""
return [
reapy.Item(RPR.GetSelectedMediaItem(self.id, i))
for i in range(self.n_selected_items)
]
@reapy.inside_reaper()
@property
def selected_tracks(self):
"""
List of selected tracks (excluding master).
:type: list of Track
"""
return [
reapy.Track(RPR.GetSelectedTrack(self.id, i))
for i in range(self.n_selected_tracks)
]
@selected_tracks.setter
def selected_tracks(self, tracks):
self.unselect_all_tracks()
for track in tracks:
track.select()
[docs] def set_info_string(self, param_name, param_string):
"""
Parameters
----------
param_name : str
MARKER_GUID:X : get the GUID (unique ID) of the marker or region
with index X, where X is the index passed to
EnumProjectMarkers, not necessarily the displayed number
RECORD_PATH :
recording directory -- may be blank or a relative path,
to get the effective path see GetProjectPathEx()
RENDER_FILE : render directory
RENDER_PATTERN : render file name (may contain wildcards)
RENDER_FORMAT : base64-encoded sink configuration
(see project files, etc). Callers can also pass a simple
4-byte string (non-base64-encoded), e.g. "evaw" or "l3pm",
to use default settings for that sink type.
RENDER_FORMAT2 : base64-encoded secondary sink configuration.
Callers can also pass a simple 4-byte string (non-base64-encoded),
e.g. "evaw" or "l3pm", to use default settings for
that sink type, or "" to disable secondary render.
Formats available on this machine:
"wave" "aiff" "iso " "ddp " "flac" "mp3l" "oggv" "OggS"
"FFMP" "GIF " "LCF " "wvpk"
param_string : str
"""
RPR.GetSetProjectInfo_String(
self.id, param_name, param_string, True
)
[docs] def set_info_value(self, param_name, param_value):
"""
Parameters
----------
param_name : str
RENDER_SETTINGS : &(1|2)=0:master mix, &1=stems+master mix,
&2=stems only, &4=multichannel tracks to multichannel files,
&8=use render matrix, &16=tracks with only mono media
to mono files, &32=selected media items,
&64=selected media items via master
RENDER_BOUNDSFLAG : 0=custom time bounds, 1=entire project,
2=time selection, 3=all project regions,
4=selected media items, 5=selected project regions
RENDER_CHANNELS : number of channels in rendered file
RENDER_SRATE : sample rate of rendered file
(or 0 for project sample rate)
RENDER_STARTPOS : render start time when RENDER_BOUNDSFLAG=0
RENDER_ENDPOS : render end time when RENDER_BOUNDSFLAG=0
RENDER_TAILFLAG : apply render tail setting when rendering:
&1=custom time bounds, &2=entire project, &4=time selection,
&8=all project regions, &16=selected media items,
&32=selected project regions
RENDER_TAILMS : tail length in ms to render
(only used if RENDER_BOUNDSFLAG and RENDER_TAILFLAG are set)
RENDER_ADDTOPROJ : 1=add rendered files to project
RENDER_DITHER : &1=dither, &2=noise shaping, &4=dither stems,
&8=noise shaping on stems
PROJECT_SRATE : samplerate (ignored unless PROJECT_SRATE_USE set)
PROJECT_SRATE_USE : set to 1 if project samplerate is used
param_value : float
"""
RPR.GetSetProjectInfo(self.id, param_name, param_value, True)
[docs] def set_ext_state(self, section, key, value, pickled=False):
"""
Set external state of project.
Parameters
----------
section : str
key : str
value : Union[Any, str]
State value. Will be dumped to str using either `pickle` if
`pickled` is `True` or `json`. Length of the dumped value
must not be over 2**31 - 2.
pickled : bool, optional
Data will be pickled with the last version if True.
If you using mypy as type checker, typing_extensions.Literal[True]
has to be used for `pickled`.
Raises
------
ValueError
If dumped `value` has length over 2**31 - 2.
"""
if pickled:
value = pickle.dumps(value)
value = codecs.encode(value, "base64").decode()
if len(value) > 2**31 - 2:
message = (
"Dumped value length is {:,d}. It must not be over "
"2**31 - 2."
)
raise ValueError(message.format(len(value)))
RPR.SetProjExtState(self.id, section, key, value)
[docs] @reapy.inside_reaper()
def solo_all_tracks(self):
"""
Solo all tracks in project.
See also
--------
Project.unsolo_all_tracks
"""
with self.make_current_project():
RPR.SoloAllTracks(1)
[docs] def stop(self):
"""
Hit stop button.
"""
RPR.OnStopButtonEx(self.id)
@reapy.inside_reaper()
@property
def time_selection(self):
"""
Project time selection.
time_selection : reapy.TimeSelection
Can be set and deleted as follows:
>>> project = reapy.Project()
>>> project.time_selection = 3, 8 # seconds
>>> del project.time_selection
"""
time_selection = reapy.TimeSelection(self)
return time_selection
@time_selection.setter
def time_selection(self, selection):
"""
Set time selection bounds.
Parameters
----------
selection : (float, float)
Start and end of new time selection in seconds.
"""
self.time_selection._set_start_end(*selection)
@time_selection.deleter
def time_selection(self):
"""
Delete current time selection.
"""
self.time_selection._set_start_end(0, 0)
@property
def time_signature(self):
"""
Project time signature.
This does not reflect tempo envelopes but is purely what is set in the
project settings.
bpm : float
Project BPM (beats per minute)
bpi : float
Project BPI (numerator of time signature)
"""
_, bpm, bpi = RPR.GetProjectTimeSignature2(self.id, 0, 0)
return bpm, bpi
[docs] def time_to_beats(self, time):
"""
Convert time in seconds to beats.
Parameters
----------
time : float
Time in seconds.
Returns
-------
beats : float
Time in beats.
See also
--------
Projecr.beats_to_time
"""
beats = RPR.TimeMap2_timeToQN(self.id, time)
return beats
@property
def tracks(self):
"""
List of project tracks.
:type: TrackList
"""
return reapy.TrackList(self)
[docs] def undo(self):
"""
Undo last action.
Raises
------
UndoError
If impossible to undo.
"""
success = RPR.Undo_DoUndo2(self.id)
if not success:
raise UndoError
[docs] def unmute_all_tracks(self):
"""
Unmute all tracks.
"""
self.mute_all_tracks(mute=False)
[docs] def unselect_all_tracks(self):
"""Unselect all tracks."""
self.perform_action(40297)
[docs] @reapy.inside_reaper()
def unsolo_all_tracks(self):
"""
Unsolo all tracks in project.
See also
--------
Project.solo_all_tracks
"""
with self.make_current_project():
RPR.SoloAllTracks(0)
class _MakeCurrentProject:
"""Context manager used by Project.make_current_project."""
def __init__(self, project):
self.current_project = self._make_current_project(project)
@staticmethod
@reapy.inside_reaper()
def _make_current_project(project):
"""Set current project and return the previous current project."""
current_project = reapy.Project()
RPR.SelectProjectInstance(project.id)
return current_project
def __enter__(self):
pass
def __exit__(self, exc_type, exc_val, exc_tb):
# Test for valid ID in case project has been closed since __enter__
if self.current_project.has_valid_id:
self.current_project.make_current_project()