import reapy
from reapy import reascript_api as RPR
from reapy.core import ReapyObject
[docs]class Take(ReapyObject):
_class_name = "Take"
def __init__(self, id):
self.id = id
def __eq__(self, other):
return isinstance(other, Take) and self.id == other.id
@property
def _args(self):
return self.id,
[docs] def add_audio_accessor(self):
"""
Create audio accessor and return it.
Returns
-------
audio_accessor : AudioAccessor
Audio accessor on take.
"""
audio_accessor_id = RPR.CreateTakeAudioAccessor(self.id)
audio_accessor = reapy.AudioAccessor(audio_accessor_id)
return audio_accessor
[docs] @reapy.inside_reaper()
def add_event(self, message, position, unit="seconds"):
"""
Add generic event to the take at position.
Note
----
⋅ No sort events during this call
⋅ Inserting notes within this function causes problems
(wrong note on and off timing), this is known REAPER bug.
Use `Take.add_note` instead.
Parameters
----------
message : Iterable[int]
Can be any message buffer, for example: (0xb0, 64, 127)
which is CC64 val127 on channel 1
position : float
position at take
unit : str, optional
"beats"|"ppq"|"seconds" (default are seconds)
See also
--------
Take.add_note
"""
ppqpos = self._resolve_midi_unit((position,), unit)[0]
bytestr = self._midi_to_bytestr(message)
RPR.MIDI_InsertEvt(
self.id, False, False, ppqpos, bytestr, len(bytestr)
)
[docs] def add_fx(self, name, even_if_exists=True):
"""
Add FX to track and return it.
Parameters
----------
name : str
FX name.
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 take (or previously existing instance of FX if
even_if_exists=False).
Raises
------
ValueError
If there is no FX with the specified name.
"""
index = RPR.TakeFX_AddByName(
self.id, name, 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_note(
self, start, end, pitch, velocity=100, channel=0, selected=False,
muted=False, unit="seconds", sort=True
):
"""
Add MIDI note to take.
Parameters
----------
start : float
Note start. Unit depends on ``unit``.
end : float
Note end. Unit depends on ``unit``.
pitch : int
Note pitch between 0 and 127.
velocity : int, optional
Note velocity between 0 and 127 (default=100).
channel : int, optional
MIDI channel between 0 and 15.
selected : bool, optional
Whether to select new note (default=False).
muted : bool
Whether to mute new note (default=False).
unit : {"seconds", "ppq", "beats"}, optional
Time unit for ``start`` and ``end`` (default="seconds").
``"ppq"`` refers to MIDI ticks.
sort : bool, optional
Whether to resort notes after creating new note
(default=True). If False, then the new note will be
``take.notes[-1]``. Otherwise it will be at its place in
the time-sorted list ``take.notes``. Set to False for
improved efficiency when adding several notes, then call
``Take.sort_events`` at the end.
See also
--------
Take.sort_events
"""
start, end = self._resolve_midi_unit((start, end), unit)
sort = bool(not sort)
args = (
self.id, selected, muted, start, end, channel, pitch, velocity,
sort
)
RPR.MIDI_InsertNote(*args)
[docs] @reapy.inside_reaper()
def add_sysex(self, message, position, unit="seconds", evt_type=-1):
"""
Add SysEx event to take.
Notes
-----
⋅ No sort events during this call
⋅ No need to add 0xf0 ... 0xf7 bytes (they will be doubled)
Parameters
----------
message : Iterable[int]
Can be any message buffer, for example: (0xb0, 64, 127)
which is CC64 val127 on channel 1
position : float
position at take
unit : str, optional
"beats"|"ppq"|"seconds" (default are seconds)
evt_type: int (default -1)
Allowable types are
⋅ -1:sysex (msg should not include bounding F0..F7),
⋅ 1-14:MIDI text event types,
⋅ 15=REAPER notation event.
"""
bytestr = self._midi_to_bytestr(message)
ppqpos = self._resolve_midi_unit((position,), unit)[0]
RPR.MIDI_InsertTextSysexEvt(
self.id, False, False, ppqpos, evt_type, bytestr, len(bytestr)
)
[docs] def beat_to_ppq(self, beat):
"""
Convert beat number (from project start) to MIDI ticks (of the take).
Parameters
----------
beat : float
Beat time to convert in beats.
Returns
-------
ppq : float
Converted time in MIDI ticks of current take.
See also
--------
Take.ppq_to_beat
Take.time_to_ppq
"""
ppq = RPR.MIDI_GetPPQPosFromProjQN(self.id, beat)
return ppq
@property
def cc_events(self):
"""
List of CC events on take.
:type: CCList
"""
return reapy.CCList(self)
@property
def envelopes(self):
return reapy.EnvelopeList(self)
@property
def fxs(self):
"""
FXs on take.
:type: FXList
"""
return reapy.FXList(self)
[docs] def get_info_value(self, param_name):
return RPR.GetMediaItemTakeInfo_Value(self.id, param_name)
@reapy.inside_reaper()
@property
def has_valid_id(self):
"""
Whether ReaScript ID is still valid.
For instance, if take has been deleted, ID will not be valid
anymore.
:type: bool
"""
try:
project_id = self.track.project.id
except (OSError, AttributeError):
return False
pointer, name = self._get_pointer_and_name()
return bool(RPR.ValidatePtr2(project_id, pointer, name))
@reapy.inside_reaper()
@property
def is_active(self):
"""
Whether take is active.
:type: bool
"""
return self == self.item.active_take
@property
def is_midi(self):
"""
Whether take contains MIDI or audio.
:type: bool
"""
return bool(RPR.TakeIsMIDI(self.id))
@property
def item(self):
"""
Parent item.
:type: Item
"""
return reapy.Item(RPR.GetMediaItemTake_Item(self.id))
@property
def guid(self):
"""
Used for communication within other scripts.
:type: str
"""
_, _, _, guid, _ = RPR.GetSetMediaItemTakeInfo_String(
self.id, 'GUID', 'stringNeedBig', False
)
return guid
[docs] def make_active_take(self):
"""
Make take active.
"""
RPR.SetActiveTake(self.id)
@property
def midi_events(self):
"""
Get all midi events as EventList.
Returns
-------
MIDIEventList
"""
return reapy.core.item.midi_event.MIDIEventList(self)
[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_GetHash(self.id, notes_only, 'hash', 1024**2)[3]
def _midi_to_bytestr(self, message):
return bytes(message).decode('latin-1')
@property
def n_cc(self):
"""
Number of MIDI CC events in take (always 0 for audio takes).
:type: int
"""
return RPR.MIDI_CountEvts(self.id, 0, 0, 0)[3]
@property
def n_envelopes(self):
"""
Number of envelopes on take.
:type: int
"""
return RPR.CountTakeEnvelopes(self.id)
@property
def n_fxs(self):
"""
Number of FXs on take.
:type: int
"""
return RPR.TakeFX_GetCount(self.id)
@property
def n_midi_events(self):
"""
Number of MIDI events in take.
:type: int
"""
return RPR.MIDI_CountEvts(self.id, 1, 1, 1)[0]
@property
def n_notes(self):
"""
Number of MIDI notes in take (always 0 for audio takes).
:type: int
"""
return RPR.MIDI_CountEvts(self.id, 0, 0, 0)[2]
@property
def n_text_sysex(self):
"""
Number of MIDI text/sysex events in take (0 for audio takes).
:type: int
"""
return RPR.MIDI_CountEvts(self.id, 0, 0, 0)[4]
@property
def name(self):
"""
Take name.
:type: str
"""
if self._is_defined:
return RPR.GetTakeName(self.id)
return ""
@property
def notes(self):
"""
List of MIDI notes on take.
Unless ``Take.add_note`` has been called with ``sort=False``,
notes are time-sorted.
:type: NoteList
"""
return reapy.NoteList(self)
[docs] def ppq_to_beat(self, ppq):
"""
Convert time in MIDI ticks (from take's start) to beats (from project's start).
Parameters
----------
ppq : float
Time to convert in MIDI ticks.
Returns
-------
beat : float
Converted time in beats.
See also
--------
Take.beat_to_ppq
Take.ppq_to_time
"""
beat = RPR.MIDI_GetProjQNFromPPQPos(self.id, ppq)
return beat
[docs] def ppq_to_time(self, ppq):
"""
Convert time in MIDI ticks to seconds.
Parameters
----------
ppq : float
Time to convert in MIDI ticks.
Returns
-------
time : float
Converted time in seconds.
See also
--------
Take.time_to_ppq
"""
time = RPR.MIDI_GetProjTimeFromPPQPos(self.id, ppq)
return time
@reapy.inside_reaper()
@property
def project(self):
"""
Take parent project.
:type: reapy.Project
"""
return self.item.project
@reapy.inside_reaper()
def _resolve_midi_unit(self, pos_tuple, unit="seconds"):
"""Get positions as ppq from tuple of positions of any length.
Parameters
----------
pos_tuple : Tuple[float]
tuple of position time in bets, ppq or seconds.
unit : str, optional
type of position inside tuple: seconds|beats|ppq
Returns
-------
Tuple[float]
the same tuple normalized to ppq
"""
if unit == "ppq":
return pos_tuple
item_start_seconds = self.item.position
def resolver(pos):
if unit == "beats":
take_start_beat = self.track.project.time_to_beats(
item_start_seconds
)
return self.beat_to_ppq(take_start_beat + pos)
if unit == "seconds":
return self.time_to_ppq(item_start_seconds + pos)
raise ValueError('unit param should be one of seconds|beats|ppq')
return [resolver(pos) for pos in pos_tuple]
[docs] def select_all_midi_events(self, select=True):
"""
Select or unselect all MIDI events.
Parameters
----------
select : bool
Whether to select or unselect events.
See also
--------
Take.unselect_all_midi_events
"""
RPR.MIDI_SelectAll(self.id, select)
[docs] def set_info_value(self, param_name, value):
return RPR.SetMediaItemTakeInfo_Value(self.id, param_name, value)
[docs] def sort_events(self):
"""
Sort MIDI events on take.
This is only needed if ``Take.add_note`` was called with
``sort=False``.
Examples
--------
The following example creates 100 MIDI notes on take in
reversed order, with ``sort=False`` for efficiency. Thus,
``take.notes`` is not time-sorted. ``take.sort_events`` is
called afterwards so that ``take.notes`` is time-sorted.
>>> for i in range(100):
... take.add_note(99 - i, 100 - i, pitch=0, sort=False)
...
>>> take.notes[0].start, take.notes[1].start
99.0, 98.0
>>> take.sort_events()
>>> take.notes[0].start, take.notes[1].start
0.0, 1.0
"""
RPR.MIDI_Sort(self.id)
@property
def source(self):
"""
Take source.
:type: Source
"""
return reapy.Source(RPR.GetMediaItemTake_Source(self.id))
@property
def start_offset(self):
"""
Start time of the take relative to start of source file.
:type: float
"""
return self.get_info_value("D_STARTOFFS")
[docs] def time_to_ppq(self, time):
"""
Convert time in seconds to MIDI ticks.
Parameters
----------
time : float
Time to convert in seconds.
Returns
-------
ppq : float
Converted time in MIDI ticks.
See also
--------
Take.ppq_to_time
"""
ppq = RPR.MIDI_GetPPQPosFromProjTime(self.id, time)
return ppq
@property
def track(self):
"""
Parent track of take.
:type: Track
"""
track_id = RPR.GetMediaItemTake_Track(self.id)
return reapy.Track(track_id)
[docs] def unselect_all_midi_events(self):
"""
Unselect all MIDI events.
See also
--------
Take.select_all_midi_events
"""
self.select_all_midi_events(select=False)
@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.TakeFX_GetChainVisible(self.id)]