Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Midi note measure #115

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
dist/*
docs/build/*
docs/make.bat
docs/Makefile
docs/Makefile
python_reapy.egg-info/
*.sublime-workspace
*.sublime-project
.vscode/*
test-reapy.py
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
# CHANGELOG

All notable changes to this project will be documented in this file.
## [Unreleased] - 2021-01-29

### Added
- `Project.beats_to_measure()` to get the measure of a beat.
- `Note.beat` to get the absolute beat of a note
- `Note.measure` to get the measure in which the note resides
- `NoteList.in_measure()` to get a list of Notes in a specified measure
would be used like so : `project.tracks[0].items[0].takes[0].notes.in_measure(1)`

## [Unreleased] - 2021-01-18

### Added
- `Project.import_media()` method to import a file.
- `reapy.tools.file_handler` tool containting a method (`validate_path()`) used to validate if a path is valid.
It checks if a the file or folder exists.
If it's a path to a file, it make sure that it's in a format supported by REAPER.
You can specify a specific extension or a list of extensions and it will raise if the choosen file does not match
- `Project.select_item()` method to select a specific item.
## [Unreleased] - 2020-12-30

### Fixed

- `reapy.configure_reaper() -> ... -> get_reaper_process_path()` not working on Windows when the user doesn't have valid privilege over the process

### Changed

- `get_reaper_process_path()` Now catches `psutil.AccessDenied` Exceptions. If no Reaper process is found, but a `psutil.AccessDenied` has been caught, a message will print asking to run this script using admin privilege.


## [0.9.0](https://github.com/RomeoDespres/reapy/releases/tag/0.9.0) - 2020-11-10
Expand Down
32 changes: 28 additions & 4 deletions reapy/config/resource_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,40 @@ def get_reaper_process_path():
When zero or more than one REAPER instances are currently
running.
"""
processes = []
"""
#BEFORE FIX -----
processes = [
p for p in psutil.process_iter(['name', 'exe'])
if os.path.splitext(p.info['name'])[0].lower() == 'reaper'
]
p for p in psutil.process_iter(['name', 'exe'])
if os.path.splitext(p.info['name'])[0].lower() == 'reaper'
]

p.info['name'] could return 'None' without knowing why the name could not be fetch.
Using p.name() will raise if the process name could not be fetched because of a privilege error
-----------------
"""
adminDenied = False
#for each running processes
for p in psutil.process_iter(['name', 'exe']):
try :
#get process name without extension
pName = os.path.splitext(p.name())[0].lower()
#save process if reaper is running
if pName == 'reaper' :
processes.append(p)
#catch if there's an 'Access denied' when trying to get infos on a process
except psutil.AccessDenied:
adminDenied = True

if not processes:
raise RuntimeError('No REAPER instance is currently running.')
errMsg = 'No REAPER instance is currently running.' if not adminDenied else 'No REAPER instance is currently running, try running this script with admin privileges'
raise RuntimeError(errMsg)

elif len(processes) > 1:
raise RuntimeError(
'More than one REAPER instance is currently running.'
)

return processes[0].info['exe']


Expand Down
50 changes: 48 additions & 2 deletions reapy/core/item/midi_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,16 @@ def infos(self):
res = list(RPR.MIDI_GetCC(
self.parent.id, self.index, 0, 0, 0, 0, 0, 0, 0
))[3:]
ppq = res[2]
res[0] = bool(res[0])
res[1] = bool(res[1])
res[2] = self.parent.ppq_to_time(res[2])
res[2] = self.parent.ppq_to_time(ppq)
res[-2] = res[-2], res[-1]
res.pop()
res.append(ppq)
keys = (
"selected", "muted", "position", "channel_message", "channel",
"messages"
"messages", "ppq"
)
return {k: r for k, r in zip(keys, res)}

Expand Down Expand Up @@ -376,7 +378,25 @@ def start(self):
a Note.
"""
return self.infos["start"]

@property
def beat(self):
"""
Beat of the note. (absolute)

:type: float
"""
return self.parent.project.time_to_beats(self.start)

@property
def measure(self):
"""
Measure of the note.

:type: int
"""
return self.parent.project.beats_to_measure(self.beat)

@property
def velocity(self):
"""
Expand All @@ -399,3 +419,29 @@ class NoteList(MIDIEventList):

_elements_class = Note
_n_elements = "n_notes"

def in_measure(self, measure):
"""
Returns a list of Note contained in the specified measure.

Parameters
----------
measure : int

Returns
-------
notes : List[Note]
Notes in the measure.
"""
notes = []
with reapy.inside_reaper():
measure = int(measure)
for n in self:
n_measure = n.measure
if n_measure < measure:
continue
elif n_measure == measure:
notes.insert(0,n)
else:
break
return notes
34 changes: 34 additions & 0 deletions reapy/core/item/midi_event.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,25 @@ class Note(MIDIEvent):
"""
...

@reapy.inside_reaper()
@property
def beat(self) -> float:
"""
Beat of the note. (absolute)

:type: float
"""
...

@reapy.inside_reaper()
@property
def measure(self) -> int:
"""
Measure of the note.

:type: int
"""
...
@property
def velocity(self) -> int:
"""
Expand All @@ -321,3 +340,18 @@ class NoteList(MIDIEventList[Note]):

_elements_class: ty.Type[Note]
_n_elements: str


def in_measure(self, measure:int) -> ty.List[Note]:
"""
Returns a list of Note contained in the specified measure.

Parameters
----------
measure : int

Returns
-------
notes : List[Note]
Notes in the measure.
"""
91 changes: 90 additions & 1 deletion reapy/core/project/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from reapy import reascript_api as RPR
from reapy.core import ReapyObject
from reapy.errors import RedoError, UndoError
from reapy.tools import file_handler


class Project(ReapyObject):
Expand Down Expand Up @@ -147,6 +148,55 @@ def add_region(self, start, end, name="", color=0):
)
region = reapy.Region(self, region_id)
return region

def import_media(self, filepath, addToSelectedTrack=False, setToCursorPosition=False):
"""
Imports a file and place the media on a track.

Parameters
----------
filepath : str
Filepath to the file to import (relative to the REAPER project)
addToSelectedTrack : bool, optional
Instead of creating a new track, the selected track will be used.
setToCursorPosition : bool, optional
When True, set the position of the imported Item to the cursor position
If False, the Item will be positionned at "00:00"
Returns
-------
item : Item
New imported item
"""
valid_filepath = None
try :
valid_filepath = file_handler.validate_path(filepath)
except FileNotFoundError as e:
print(f'File could not be found "{e}"')
return

insertMode = 1-int(addToSelectedTrack)

if addToSelectedTrack:
#if no track is selected
if not len(self.selected_tracks):
raise IndexError("No track is selected")
else:
#unselect all tracks to make sure the new track will not be nested in a selected track
self.master_track.make_only_selected_track()


#import media
RPR.InsertMedia(valid_filepath, insertMode)
selectedTrack = self.selected_tracks[0]
#even if the file doesn't exists, an item is created, but it's length is 0.0
item = selectedTrack.items[0]
#update position
if not setToCursorPosition:
item.position = 0.0

#force UI update
reapy.update_timeline()
return item

@reapy.inside_reaper()
def add_track(self, index=0, name=""):
Expand Down Expand Up @@ -946,6 +996,25 @@ def select_all_items(self, selected=True):
def select_all_tracks(self):
"""Select all tracks."""
self.perform_action(40296)

def select_item(self, item_obj, selected=True, makeUnique=False):
"""
Select or unselect an item, depending on `selected`.

Parameters
----------
item_obj : reapy.Item
The item to select
selected : bool [optional]
Whether to select or unselect the item.
makeUnique : bool [optional]
If False the Item will be added to the current selection, if True it will become the only selected item
"""
if makeUnique:
self.select_all_items(selected=False)
RPR.SetMediaItemSelected(item_obj.id, selected)
#update UI
reapy.update_timeline()

@property
def selected_envelope(self):
Expand Down Expand Up @@ -1177,10 +1246,30 @@ def time_to_beats(self, time):

See also
--------
Projecr.beats_to_time
Project.beats_to_time
"""
beats = RPR.TimeMap2_timeToQN(self.id, time)
return beats

def beats_to_measure(self, beats):
"""
Convert beats(QN time) to measure.

Parameters
----------
beats : float

Returns
-------
measure : int
measure of that beat.

See also
--------
Project.time_to_beats
"""
measure = int(RPR.TimeMap_QNToMeasures(self.id, beats, 0, 10000)[0])
return measure

@property
def tracks(self):
Expand Down
Loading