Skip to content

Commit

Permalink
Merge pull request #268 from CPJKU/develop
Browse files Browse the repository at this point in the history
Release 1.3.0 merge to main
  • Loading branch information
manoskary authored Jun 10, 2023
2 parents 9834601 + ef54c5a commit 2f20289
Show file tree
Hide file tree
Showing 44 changed files with 7,608 additions and 457 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/partitura_unittests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ jobs:
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install .
- name: Install Optional dependencies
run: |
pip install music21==8.3.0 Pillow==9.5.0 musescore==0.0.1
pip install miditok==2.0.6 tokenizers==0.13.3
- name: Run Tests
run: |
pip install coverage
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -148,4 +148,7 @@ static
*~

# vscode
.vscode
.vscode

# phdocs
phdocs.txt
43 changes: 43 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,49 @@
Release Notes
=============


New Version 1.3.0 (Released on 2023-06-09)
------------------------------------------

This PR addresses release 1.3.0, it includes several bug fixes, code cleaning, documentation, and new functionality.


New Features
------------

- Enhanced Performance features in the same fashion as the note features;
- Fixed-size option for Note features. Use: `
- Create a score from a note array functionality. Call `partitura.musicanalysis.scorify(note_array)`;

New Optional Features
---------------------

- _If music21 is installed_ : Import music21 to Partitura by calling `partitura.load_music21(m21_score)`
- _If MidiTok is installed_ : Export Partitura Score to Tokens by calling `partitura.utils.music.tokenize(score_data, tokenizer)`

Bug Fixes
----------

- Fixed bug: #264
- Fixed bug: #251
- Fixed bug: #207
- Fixed bug: #162
- Fixed bug: #261
- Fixed bug: #262
- Fixed Issue: #256
- Addressed Issue: #133
- Fixed bug: #257
- Fixed bug: #248
- Fixed bug: #223

Other Changes
-------------

- Minor Changes to the Documentation
- Addition of Docs link to the Github header
- Upgraded python version requirements to Python>= 3.7


Version 1.2.2 (Released on 2023-10-05)
--------------------------------------

Expand Down
4 changes: 2 additions & 2 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
# built documents.
#
# The short X.Y version.
version = "1.2.2" # pkg_resources.get_distribution("partitura").version
version = "1.3.0" # pkg_resources.get_distribution("partitura").version
# The full version, including alpha/beta/rc tags.
release = "1.2.2"
release = "1.3.0"

# # The full version, including alpha/beta/rc tags
# release = pkg_resources.get_distribution("partitura").version
Expand Down
10 changes: 10 additions & 0 deletions docs/source/introduction.rst
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ alignments, that are not handled by music21.
.. `partitura` intends to provide a convenient way to work with symbolic
.. musical data in the context of problems such as musical expression modeling, or music generation. Although it is not the main aim of the package to provide music analysis tools, the package does offer functionality for pitch spelling, voice assignment and key estimation.
Additional Resources
====================
For a more hands on tutorial of `partitura` for providing an introduction to symbolic music processing for a broad MIR audience, with a particular focus on showing how to extract relevant MIR features from symbolic musical formats in a fast, intuitive, and scalable way.

Please visit:
`https://cpjku.github.io/partitura_tutorial/ <https://cpjku.github.io/partitura_tutorial/>`_


Credits
=======

Expand All @@ -165,6 +173,8 @@ If you find Partitura useful, we would appreciate if you could cite us!
}




Acknowledgments
---------------

Expand Down
2 changes: 2 additions & 0 deletions partitura/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from .io.exportmusicxml import save_musicxml
from .io.importmei import load_mei
from .io.importkern import load_kern
from .io.importmusic21 import load_music21
from .io.importmidi import load_score_midi, load_performance_midi, midi_to_notearray
from .io.exportmidi import save_score_midi, save_performance_midi
from .io.importmatch import load_match
Expand All @@ -26,6 +27,7 @@
from . import musicanalysis
from .musicanalysis import make_note_features, compute_note_array, full_note_array


# define a version variable
__version__ = pkg_resources.get_distribution("partitura").version

Expand Down
1 change: 1 addition & 0 deletions partitura/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from .importkern import load_kern
from .importparangonada import load_parangonada_csv
from .exportparangonada import save_parangonada_csv
from .importmusic21 import load_music21

from partitura.utils.misc import (
deprecated_alias,
Expand Down
5 changes: 4 additions & 1 deletion partitura/io/exportmatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@

from partitura.utils.music import (
seconds_to_midi_ticks,
get_time_maps_from_alignment,
)

from partitura.utils.misc import (
Expand All @@ -54,6 +53,10 @@
deprecated_parameter,
)

from partitura.musicanalysis.performance_codec import (
get_time_maps_from_alignment
)

__all__ = ["save_match"]


Expand Down
81 changes: 73 additions & 8 deletions partitura/io/exportmidi.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ def save_score_midi(
part_voice_assign_mode: int = 0,
velocity: int = 64,
anacrusis_behavior: str = "shift",
minimum_ppq: int = 0,
) -> Optional[MidiFile]:
"""Write data from Part objects to a MIDI file
Expand Down Expand Up @@ -276,11 +277,20 @@ def save_score_midi(
The default mode is 0.
velocity : int, optional
Default velocity for all MIDI notes. Defaults to 64.
anacrusis_behavior : {"shift", "pad_bar"}, optional
anacrusis_behavior : {"shift", "pad_bar", "time_sig_change"}, optional
Strategy to deal with anacrusis. If "shift", all
time points are shifted by the anacrusis (i.e., the first
note starts at 0). If "pad_bar", the "incomplete" bar of
the anacrusis is padded with silence. Defaults to 'shift'.
If "time_sig_change", the time signature is changed to match
the duration of the measure. This also ensure the beat and
downbeats position are coherent in case of incomplete measures
later in the score.
minimum_ppq : int, optional
Minimum ppq to use for the MIDI file. If the ppq of the score is less,
it will be doubled until it is above the threshold. This is useful
because some libraries like miditok require a certain minimum ppq to
work properly.
Returns
-------
Expand All @@ -302,6 +312,10 @@ def save_score_midi(
f" or a list of `Part` instances but is {type(score_data)}"
)
ppq = get_ppq(parts)
# double it until it is above the minimum level.
# Doubling instead of setting it ensure that the common divisors stay the same.
while ppq < minimum_ppq:
ppq = ppq*2

events = defaultdict(lambda: defaultdict(list))
meta_events = defaultdict(lambda: defaultdict(list))
Expand All @@ -316,7 +330,7 @@ def save_score_midi(
ftp = 0
# Deal with anacrusis
if first_time_point < 0:
if anacrusis_behavior == "shift":
if anacrusis_behavior == "shift" or anacrusis_behavior == "time_sig_change":
ftp = first_time_point
elif anacrusis_behavior == "pad_bar":
time_signatures = []
Expand Down Expand Up @@ -346,12 +360,63 @@ def to_ppq(t):
"set_tempo", tempo=tp.microseconds_per_quarter
)

for ts in part.iter_all(score.TimeSignature):
meta_events[part][to_ppq(ts.start.t)].append(
MetaMessage(
"time_signature", numerator=ts.beats, denominator=ts.beat_type
)
)
if anacrusis_behavior == "time_sig_change":
# Change time signature to match the duration of the measure
# This ensure the beat and downbeats position are coherent
# in case of incomplete measures later in the score.
all_ts = list(part.iter_all(score.TimeSignature))
ts_changing_time = [ts.start.t for ts in all_ts]
for measure in part.iter_all(score.Measure):
m_duration_beat = part.beat_map(measure.end.t) - part.beat_map(measure.start.t)
m_ts = part.time_signature_map(measure.start.t)
if m_duration_beat != m_ts[0]:
# add ts change
# TODO: add support for changing the beat type if number of beats is not integer
meta_events[part][to_ppq(measure.start.t)].append(
MetaMessage(
"time_signature", numerator=int(m_duration_beat), denominator=int(m_ts[1])
)
)
ts_changing_time.append(measure.start.t) # keep track of changing the ts
# now go back to original ts if there is no ts change after this measure
if not any([ts_t > measure.start.t for ts_t in ts_changing_time]):
meta_events[part][to_ppq(measure.end.t)].append(
MetaMessage(
"time_signature", numerator=int(m_ts[0]), denominator=int(m_ts[1])
)
)
# filter out the multiple ts changes at the same time
# this happens when multiple measure in a row have wrong duration
for t in meta_events[part].keys():
if len(meta_events[part][t]) == 2:
meta_events[part][t] = meta_events[part][t][1:]

# now add the normal time signature change
for ts in part.iter_all(score.TimeSignature):
if ts.start.t in ts_changing_time:
#don't add if something is already added at this time to cover the case of a ts change when the first measure is shorter/longer
pass
else:
meta_events[part][to_ppq(ts.start.t)].append(
MetaMessage(
"time_signature", numerator=ts.beats, denominator=ts.beat_type
)
)
else: # just add the time signature that are explicit in partitura
for i, ts in enumerate(part.iter_all(score.TimeSignature)):
if anacrusis_behavior == "pad_bar" and i == 0:
# shift the first time signature to 0 so MIDI players can pick up the correct measure position
meta_events[part][0].append(
MetaMessage(
"time_signature", numerator=ts.beats, denominator=ts.beat_type
)
)
else: #follow the position in the partitura part
meta_events[part][to_ppq(ts.start.t)].append(
MetaMessage(
"time_signature", numerator=ts.beats, denominator=ts.beat_type
)
)

for ks in part.iter_all(score.KeySignature):
meta_events[part][to_ppq(ks.start.t)].append(
Expand Down
21 changes: 19 additions & 2 deletions partitura/io/importmatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -584,14 +584,14 @@ def part_from_matchfile(
onset_divs = onset_in_divs[ni]
assert onset_divs >= 0
assert np.isclose(onset_divs, onset_in_divs[ni], atol=divs * 0.01)

is_tied = False
articulations = set()
if "staccato" in note.ScoreAttributesList or "stac" in note.ScoreAttributesList:
articulations.add("staccato")
if "accent" in note.ScoreAttributesList:
articulations.add("accent")
if "leftOutTied" in note.ScoreAttributesList:
continue
is_tied = True

# dictionary with keyword args with which the Note
# (or GraceNote) will be instantiated
Expand Down Expand Up @@ -677,6 +677,23 @@ def part_from_matchfile(

part.add(part_note, onset_divs, offset_divs)

# Check if the note is tied and if so, add the tie information
if is_tied:
found = False
# iterate over all notes in the Timeline that end at the starting point.
for el in part_note.start.iter_ending(score.Note):
if isinstance(el, score.Note):
condition = el.step == note_attributes["step"] and el.octave == note_attributes["octave"] and el.alter == note_attributes["alter"]
if condition:
el.tie_next = part_note
part_note.tie_prev = el
found = True
break
if not found:
warnings.warn(
"Tie information found, but no previous note found to tie to for note {}.".format(part_note.id)
)

# add time signatures
for (ts_beat_time, ts_bar, tsg) in ts:
ts_beats = tsg.numerator
Expand Down
4 changes: 2 additions & 2 deletions partitura/io/importmei.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
@deprecated_alias(mei_path="filename")
def load_mei(filename: PathLike) -> score.Score:
"""
Loads a Mei score from path and returns a list of Partitura.Part
Loads a Mei score from path and returns a partitura Score object.
Parameters
----------
Expand Down Expand Up @@ -1037,7 +1037,7 @@ def _tie_notes(self, section_el, part_list):
# remove the # in first position
start_id = start_id[1:]
end_id = end_id[1:]
# set tie prev and tie next in partira note objects
# set tie prev and tie next in partitura note objects
all_notes_dict[start_id].tie_next = all_notes_dict[end_id]
all_notes_dict[end_id].tie_prev = all_notes_dict[start_id]

Expand Down
1 change: 1 addition & 0 deletions partitura/io/importmidi.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ def load_score_midi(
if len(ch_notes) > 0:
notes_by_track_ch[(track_nr, ch)] = ch_notes


tr_ch_keys = sorted(notes_by_track_ch.keys())
group_part_voice_keys, part_names, group_names = assign_group_part_voice(
part_voice_assign_mode, tr_ch_keys, track_names_by_track
Expand Down
Loading

0 comments on commit 2f20289

Please sign in to comment.