Source code for reapy.core.project.project

"""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 perform_action(self, action_id): """ Perform action with ID `action_id` in the main Actions section. Parameters ---------- action_id : int Action ID in the main Actions section. """ RPR.Main_OnCommandEx(action_id, 0, self.id)
[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()