Skip to content

Commit

Permalink
various; see CHANGES.md
Browse files Browse the repository at this point in the history
  • Loading branch information
mgrachten committed Nov 8, 2019
1 parent 09e1ad9 commit 472a1bf
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 62 deletions.
21 changes: 21 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
Release Notes
=============

Version 0.3.4 (Released on 2019-11-08)
--------------------------------------

API changes:

* Rename `out_fmt` kwarg to `fmt` in `show`
* Add `dpi` kwarg to `show`
* Rename `show` function to `render`

New features:

* Save rendered scores to image file using `render`
* Add `wedge` attribute to DynamicLoudnessMarkings to differentiate them
from textual directions

Bug fixes:

* Do not crash when calling time_signature_map on empty Part instances
* Fix bug in repeat unfolding


Version 0.3.3 (Released on 2019-11-04)
--------------------------------------

Expand Down
4 changes: 2 additions & 2 deletions partitura/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from .io.importmidi import load_score_midi, load_performance_midi
from .io.exportmidi import save_score_midi, save_performance_midi
from .io.importmatch import load_match
from .display import show
from .display import render
from . import musicanalysis


Expand All @@ -26,4 +26,4 @@
__all__ = ['load_musicxml', 'save_musicxml', 'musicxml_to_notearray',
'load_score_midi', 'save_score_midi', 'load_via_musescore',
'load_performance_midi', 'save_performance_midi',
'show', 'EXAMPLE_MUSICXML']
'render', 'EXAMPLE_MUSICXML']
50 changes: 28 additions & 22 deletions partitura/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
from tempfile import NamedTemporaryFile, TemporaryFile

from partitura import save_musicxml
from partitura.io.musescore import show_musescore
from partitura.io.musescore import render_musescore

LOGGER = logging.getLogger(__name__)

__all__ = ['show']
__all__ = ['render']

# def ly_install_msg():
# """Issue a platform specific installation suggestion for lilypond
Expand All @@ -37,9 +37,11 @@
# 'http://lilypond.org/')
# return s

def show(part, out_fmt='png'):
"""Show a rendering of one or more parts or partgroups using the
desktop default application.
def render(part, fmt='png', dpi=90, out_fn=None):
"""Create a rendering of one or more parts or partgroups.
The function can save the rendered image to a file (when `out_fn`
is specified), or shown in the default image viewer application.
Rendering is first attempted through musecore, and if that fails
through lilypond. If that also fails the function returns without
Expand All @@ -49,33 +51,37 @@ def show(part, out_fmt='png'):
----------
part : :class:`partitura.score.Part` or :class:`partitura.score.PartGroup` or list of these
The score content to be displayed
out_fmt : {'png', 'pdf'}, optional
fmt : {'png', 'pdf'}, optional
The image format of the rendered material
out_fn : str or None, optional
The path of the image output file. If None, the rendering will
be displayed in a viewer.
"""

img_fn = show_musescore(part, out_fmt)
img_fn = render_musescore(part, fmt, out_fn, dpi)

if img_fn is None or not os.path.exists(img_fn):
img_fn = show_lilypond(part, out_fmt)
img_fn = render_lilypond(part, fmt, out_fn)
if img_fn is None or not os.path.exists(img_fn):
return None

# NOTE: the temporary image file will not be deleted.
if platform.system() == 'Linux':
subprocess.call(['xdg-open', img_fn])
elif platform.system() == 'Darwin':
subprocess.call(['open', img_fn])
elif platform.system() == 'Windows':
os.startfile(img_fn)


def show_lilypond(part, out_fmt='png'):
if out_fmt not in ('png', 'pdf'):
if not out_fn:
# NOTE: the temporary image file will not be deleted.
if platform.system() == 'Linux':
subprocess.call(['xdg-open', img_fn])
elif platform.system() == 'Darwin':
subprocess.call(['open', img_fn])
elif platform.system() == 'Windows':
os.startfile(img_fn)


def render_lilypond(part, fmt='png', out_fn=None):
if fmt not in ('png', 'pdf'):
print('warning: unsupported output format')
return

prvw_sfx = '.preview.{}'.format(out_fmt)
prvw_sfx = '.preview.{}'.format(fmt)

with TemporaryFile() as xml_fh, \
NamedTemporaryFile(suffix=prvw_sfx, delete=False) as img_fh:
Expand All @@ -102,7 +108,7 @@ def show_lilypond(part, out_fmt='png'):

# convert lilypond format (read from pipe of ps1) to image, and save to
# temporary filename
cmd2 = ['lilypond', '--{}'.format(out_fmt),
cmd2 = ['lilypond', '--{}'.format(fmt),
'-dno-print-pages', '-dpreview',
'-o{}'.format(img_stem), '-']
try:
Expand Down
50 changes: 37 additions & 13 deletions partitura/io/musescore.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,43 +92,50 @@ def load_via_musescore(fn):
return load_musicxml(xml_fh.name)


def show_musescore(part, out_fmt, dpi=90):
def render_musescore(part, fmt, out_fn=None, dpi=90):
"""Render a part using musescore.
Parameters
----------
part : Part
Part to be rendered
out_fmt : {'png', 'pdf'}
fmt : {'png', 'pdf'}
Output image format
out_fn : str or None, optional
The path of the image output file, if not specified, the
rendering will be saved to a temporary filename. Defaults to
None.
dpi : int, optional
Image resolution. This option is ignored when `out_fmt` is
Image resolution. This option is ignored when `fmt` is
'pdf'. Defaults to 90.
"""
mscore_exec = find_musescore3()

if not mscore_exec:

return None

if out_fmt not in ('png', 'pdf'):
if fmt not in ('png', 'pdf'):

LOGGER.warning('warning: unsupported output format')
return None

with NamedTemporaryFile(suffix='.musicxml') as xml_fh, \
NamedTemporaryFile(suffix='.{}'.format(out_fmt), delete=False) as img_fh:
NamedTemporaryFile(suffix='.{}'.format(fmt)) as img_fh:

save_musicxml(part, xml_fh)
cmd = [mscore_exec, '-T', '10', '-r', '{}'.format(dpi), '-o', img_fh.name, xml_fh.name]
save_musicxml(part, xml_fh.name)

cmd = [mscore_exec, '-T', '10', '-r', '{}'.format(dpi), '-o', img_fh.name, xml_fh.name]
try:

ps = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
# ps = subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
ps = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

if ps.returncode != 0:
LOGGER.error('Command {} failed with code {}'
.format(cmd, ps.returncode))
LOGGER.error('Command {} failed with code {}; stdout: {}; stderr: {}'
.format(cmd, ps.returncode, ps.stdout.decode('UTF-8'),
ps.stderr.decode('UTF-8')))
return None

except FileNotFoundError as f:
Expand All @@ -137,7 +144,24 @@ def show_musescore(part, out_fmt, dpi=90):
.format(' '.join(cmd), f))
return None

LOGGER.error('Command {} returned with code {}; stdout: {}; stderr: {}'
.format(cmd, ps.returncode, ps.stdout.decode('UTF-8'),
ps.stderr.decode('UTF-8')))

name, ext = os.path.splitext(img_fh.name)
return '{}-1{}'.format(name, ext)
if fmt == 'png':
img_fn = '{}-1{}'.format(name, ext)
else:
img_fn = '{}{}'.format(name, ext)

# print(img_fn, os.path.exists(img_fn))
if os.path.exists(img_fn):
if out_fn:
shutil.copy(img_fn, out_fn)
return out_fn
else:
return img_fn
else:
return None


70 changes: 48 additions & 22 deletions partitura/score.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,23 @@ def time_signature_map(self):
tss = np.array([(ts.start.t, ts.beats, ts.beat_type)
for ts in self.iter_all(TimeSignature)])
if len(tss) == 0:
# warn assumption
tss = np.array([(self.first_point.t, 4, 4),
(self.last_point.t, 4, 4)])
# default time sig
beats, beat_type = 4, 4
LOGGER.warning('No time signatures found, assuming {}/{}'
.format(beats, beat_type))
if self.first_point is None:
t0, tN = 0, 0
else:
t0 = self.first_point.t
tN = self.last_point.t
tss = np.array([(t0, beats, beat_type),
(tN, beats, beat_type)])
elif tss[0, 0] > self.first_point.t:
tss = np.vstack(((self.first_point.t, tss[0, 1], tss[0, 2]),
tss))
tss = np.vstack((tss,
(self.last_point.t, tss[-1, 1], tss[-1, 2])))

return interp1d(tss[:, 0], tss[:, 1:], axis=0, kind='previous')
return interp1d(tss[:, 0], tss[:, 1:], axis=0, kind='previous',
bounds_error=False, fill_value='extrapolate')

def _time_interpolator(self, quarter=False, inv=False):

Expand Down Expand Up @@ -1524,7 +1531,7 @@ def __init__(self, text, raw_text=None, staff=None):

def __str__(self):
if self.raw_text is not None:
return '{} "{}" ("{}")'.format(type(self).__name__, self.text, self.raw_text)
return '{} "{}" raw_text="{}"'.format(type(self).__name__, self.text, self.raw_text)
else:
return '{} "{}"'.format(type(self).__name__, self.text)

Expand All @@ -1546,6 +1553,12 @@ class DynamicLoudnessDirection(DynamicDirection, LoudnessDirection):
def __init__(self, *args, wedge=False, **kwargs):
super().__init__(*args, **kwargs)
self.wedge = wedge
def __str__(self):
if self.wedge:
return '{} wedge'.format(super().__str__())
else:
return super().__init__()

class DynamicTempoDirection(DynamicDirection, TempoDirection): pass

class IncreasingLoudnessDirection(DynamicLoudnessDirection): pass
Expand Down Expand Up @@ -2180,24 +2193,27 @@ def make_score_variants(part):
sv.add_segment(rep_start, ending1.start)

ending2 = next(rep_end.iter_starting(Ending), None)

if ending2:
# add the first occurrence of the repeat
sv.add_segment(ending2.start, ending2.end)

# new_sv includes the 1/2 ending repeat, which means:
# 1. from repeat start to repeat end (which includes ending 1)
new_sv.add_segment(rep_start, rep_end)
# 2. from repeat start to ending 1 start
new_sv.add_segment(rep_start, ending1.start)
# 3. ending 2 start to ending 2 end
new_sv.add_segment(ending2.start, ending2.end)

# new score time will be the score time
t_end = ending2.end

else:
# ending 1 without ending 2, should not happen normally
pass

# new_sv includes the 1/2 ending repeat, which means:
# 1. from repeat start to repeat end (which includes ending 1)
new_sv.add_segment(rep_start, rep_end)
# 2. from repeat start to ending 1 start
new_sv.add_segment(rep_start, ending1.start)
# 3. ending 2 start to ending 2 end
new_sv.add_segment(ending2.start, ending2.end)

# update the score time
t_score = ending2.end

LOGGER.warning('ending 1 without ending 2')
# new score time will be the score time
t_end = ending1.end
else:
# add the first occurrence of the repeat
sv.add_segment(rep_start, rep_end)
Expand All @@ -2207,12 +2223,13 @@ def make_score_variants(part):
new_sv.add_segment(rep_start, rep_end)

# update the score time
t_score = rep_end
t_end = rep_end

# add both score variants
new_svs.append(sv)
new_svs.append(new_sv)

t_score = t_end

svs = new_svs

# are we at the end of the piece already?
Expand Down Expand Up @@ -2240,12 +2257,21 @@ def add_measures(part):
Part instance
"""

timesigs = np.array([(ts.start.t, ts.beats)
for ts in part.iter_all(TimeSignature)],
dtype=np.int)

if len(timesigs) == 0:
LOGGER.warning('No time signatures found, not adding measures')
return

start = part.first_point.t
end = part.last_point.t

if start == end:
return

# make sure we cover time from the start of the timeline
if len(timesigs) == 0 or timesigs[0, 0] > start:
timesigs = np.vstack(([[start, 4]], timesigs))
Expand Down
7 changes: 4 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
EMAIL = '[email protected]'
AUTHOR = 'Maarten Grachten, Carlos Cancino-Chacón, and Thassilo Gadermaier'
REQUIRES_PYTHON = '>=3.5'
VERSION = '0.3.3'
VERSION = '0.3.4'

# What packages are required for this module to be executed?
REQUIRED = [
Expand All @@ -43,7 +43,7 @@
# Import the README and use it as the long-description.
# Note: this will only work if 'README.md' is present in your MANIFEST.in file!
try:
with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f:
with io.open(os.path.join(here, 'README.rst'), encoding='utf-8') as f:
long_description = '\n' + f.read()
except FileNotFoundError:
long_description = DESCRIPTION
Expand All @@ -63,7 +63,8 @@
version=about['__version__'],
description=DESCRIPTION,
long_description=long_description,
long_description_content_type='text/markdown',
# long_description_content_type='text/markdown',
long_description_content_type='text/x-rst',
keywords=KEYWORDS,
author=AUTHOR,
author_email=EMAIL,
Expand Down

0 comments on commit 472a1bf

Please sign in to comment.