Skip to content

Input and Output

Input

The main function for input is readscore.read_score described below. It calls upon various file readers to read Standard MIDI Files and Music XML files. Much of the work is done by various subsystems including Music21, Partitura, and pretty_midi. Use read_score to get the recommended implementation automatically.

readscore

Functions for music data input.

Classes

Functions

set_preferred_midi_reader

set_preferred_midi_reader(reader: str = _default_midi_reader) -> str

Set a (new) preferred MIDI reader.

Returns the previous reader preference. The current preference is stored in amads.io.reader.preferred_midi_reader.

Parameters:

  • reader (str, default: _default_midi_reader ) –

    The name of the preferred MIDI reader; "music21" or "pretty_midi". Defaults to "pretty_midi".

Returns:

  • str

    The previous name of the preferred MIDI reader.

Raises:

  • ValueError

    If an invalid reader is provided.

Source code in amads/io/readscore.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
def set_preferred_midi_reader(reader: str = _default_midi_reader) -> str:
    """
    Set a (new) preferred MIDI reader.

    Returns the previous reader preference. The current preference is stored
    in `amads.io.reader.preferred_midi_reader`.

    Parameters
    ----------
    reader : str, optional
        The name of the preferred MIDI reader; "music21" or "pretty_midi".
        Defaults to "pretty_midi".

    Returns
    -------
    str
        The previous name of the preferred MIDI reader.

    Raises
    ------
    ValueError
        If an invalid reader is provided.
    """
    global preferred_midi_reader
    allowed = ["music21", "pretty_midi"]
    if reader not in allowed_subsystems["midi"]:
        raise ValueError(f"Invalid MIDI reader. Must be one of {allowed}")

    previous = preferred_midi_reader
    preferred_midi_reader = reader
    return previous

set_preferred_xml_reader

set_preferred_xml_reader(reader: str = _default_xml_reader) -> str

Set a (new) preferred XML reader.

Returns the previous reader preference. The current preference is stored in amads.io.reader.preferred_xml_reader.

Parameters:

  • reader (str, default: _default_xml_reader ) –

    The name of the preferred XML reader. Can be "music21" or "partitura". Defaults to "music21".

Returns:

  • str

    The previous name of the preferred XML reader.

Raises:

  • ValueError

    If an invalid reader is provided.

Source code in amads/io/readscore.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
def set_preferred_xml_reader(reader: str = _default_xml_reader) -> str:
    """
    Set a (new) preferred XML reader.

    Returns the previous reader preference. The current preference is stored
    in `amads.io.reader.preferred_xml_reader`.

    Parameters
    ----------
    reader : str, optional
        The name of the preferred XML reader. Can be "music21" or "partitura".
        Defaults to "music21".

    Returns
    -------
    str
        The previous name of the preferred XML reader.

    Raises
    ------
    ValueError
        If an invalid reader is provided.
    """
    global preferred_xml_reader
    allowed = allowed_subsystems["musicxml"]
    if reader not in allowed:
        raise ValueError(f"Invalid XML reader. Must be one of {allowed}")

    previous = preferred_xml_reader
    preferred_xml_reader = reader
    return previous

set_preferred_kern_reader

set_preferred_kern_reader(reader: str = _default_kern_reader) -> str

Set a (new) preferred Kern reader.

Returns the previous reader preference. The current preference is stored in amads.io.reader.preferred_kern_reader.

Parameters:

  • reader (str, default: _default_kern_reader ) –

    The name of the preferred Kern reader. Can be "music21" or "partitura". Defaults to "music21".

Returns:

  • str

    The previous name of the preferred Kern reader.

Raises:

  • ValueError

    If an invalid reader is provided.

Source code in amads/io/readscore.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
def set_preferred_kern_reader(reader: str = _default_kern_reader) -> str:
    """
    Set a (new) preferred Kern reader.

    Returns the previous reader preference. The current preference is stored
    in `amads.io.reader.preferred_kern_reader`.

    Parameters
    ----------
    reader : str, optional
        The name of the preferred Kern reader. Can be "music21" or "partitura".
        Defaults to "music21".

    Returns
    -------
    str
        The previous name of the preferred Kern reader.

    Raises
    ------
    ValueError
        If an invalid reader is provided.
    """
    global preferred_kern_reader
    allowed = allowed_subsystems["kern"]
    if reader not in allowed:
        raise ValueError(f"Invalid Kern reader. Must be one of {allowed}")

    previous = preferred_kern_reader
    preferred_kern_reader = reader
    return previous

set_preferred_mei_reader

set_preferred_mei_reader(reader: str = _default_mei_reader) -> str

Set a (new) preferred MEI reader.

Returns the previous reader preference. The current preference is stored in amads.io.reader.preferred_mei_reader.

Parameters:

  • reader (str, default: _default_mei_reader ) –

    The name of the preferred MEI reader. Can be "music21" or "partitura". Defaults to "music21".

Returns:

  • str

    The previous name of the preferred MEI reader.

Raises:

  • ValueError

    If an invalid reader is provided.

Source code in amads/io/readscore.py
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
def set_preferred_mei_reader(reader: str = _default_mei_reader) -> str:
    """
    Set a (new) preferred MEI reader.

    Returns the previous reader preference. The current preference is stored
    in `amads.io.reader.preferred_mei_reader`.

    Parameters
    ----------
    reader : str, optional
        The name of the preferred MEI reader. Can be "music21" or "partitura".
        Defaults to "music21".

    Returns
    -------
    str
        The previous name of the preferred MEI reader.

    Raises
    ------
    ValueError
        If an invalid reader is provided.
    """
    global preferred_mei_reader
    allowed = allowed_subsystems["mei"]
    if reader not in allowed:
        raise ValueError(f"Invalid MEI reader. Must be one of {allowed}")

    previous = preferred_mei_reader
    preferred_mei_reader = reader
    return previous

set_reader_warning_level

set_reader_warning_level(level: str) -> str

Set the warning level for readscore functions.

The translation from music data files to AMADS is not always well-defined and may involve intermediate representations using Music21, Partitura or others. Usually, warnings are produced when there is possible data loss or ambiguity, but these can be more annoying than informative. The warning level can be controlled using this function, which applies to all file formats.

The current warning level is stored in amads.io.reader.reader_warning_level.

Parameters:

  • level (str) –

    The warning level to set. Options are "none", "low", "default", "high".

    • "none" will suppress all warnings during read_score() and also suppresses notice of reader subsystem and file name.
    • "low" will print one notice if there are any warnings.
    • "default" will obey environment settings to control warnings.
    • "high" will print all warnings during read_score(), overriding environment settings.

Returns:

  • str

    Previous warning level.

Raises:

  • ValueError

    If an invalid warning level is provided.

Source code in amads/io/readscore.py
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
def set_reader_warning_level(level: str) -> str:
    """
    Set the warning level for `readscore` functions.

    The translation from music data files to AMADS is not always well-defined
    and may involve intermediate representations using Music21, Partitura or
    others. Usually, warnings are produced when there is possible data loss or
    ambiguity, but these can be more annoying than informative. The warning
    level can be controlled using this function, which applies to all file
    formats.

    The current warning level is stored in
    `amads.io.reader.reader_warning_level`.

    Parameters
    ----------
    level : str
        The warning level to set.
        Options are "none", "low", "default", "high".

        - "none" will suppress all warnings during `read_score()`
          and also suppresses notice of reader subsystem and file name.
        - "low" will print one notice if there are any warnings.
        - "default" will obey environment settings to control warnings.
        - "high" will print all warnings during `read_score()`, overriding
            environment settings.

    Returns
    -------
    str
        Previous warning level.

    Raises
    ------
    ValueError
        If an invalid warning level is provided.
    """
    global reader_warning_level
    allowed = ["none", "low", "default", "high"]
    if level not in allowed:
        raise ValueError(f"Invalid warning level. Must be one of {allowed}")

    previous = reader_warning_level
    reader_warning_level = level
    return previous

_check_for_subsystem

_check_for_subsystem(
    format: str,
) -> tuple[
    Optional[Callable[[str, str, bool, bool, bool, bool], Score]],
    Optional[str],
]

Check if the preferred reader is available.

We support: music21 for midi and xml, partitura for xml, and PrettyMIDI for midi.

Partitura has basic MIDI import functionality, but is unsupported here because when it reads in a score it has no MIDI velocity and when it reads in a performance it has no tempo track, key signature, etc.

Parameters:

  • format (str) –

    The type of file to read: 'midi', 'musicxml', 'kern', or 'mei'.

Returns:

  • tuple[Optional[Callable], Optional[str]]

    The import function if available, None otherwise.

Source code in amads/io/readscore.py
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
def _check_for_subsystem(
    format: str,
) -> tuple[
    Optional[Callable[[str, str, bool, bool, bool, bool], Score]], Optional[str]
]:
    """
    Check if the preferred reader is available.

    We support:
    `music21` for midi and xml,
    `partitura` for xml,
    and
    `PrettyMIDI` for midi.

    Partitura has basic MIDI import functionality, but is unsupported here
    because when it reads in a score it has no MIDI velocity
    and when it reads in a performance it has no tempo track, key signature, etc.

    Parameters
    ----------
    format : str
        The type of file to read: 'midi', 'musicxml', 'kern', or 'mei'.

    Returns
    -------
    tuple[Optional[Callable], Optional[str]]
        The import function if available, None otherwise.
    """
    preferred_reader = {
        "midi": preferred_midi_reader,
        "musicxml": preferred_xml_reader,
        "kern": preferred_kern_reader,
        "mei": preferred_mei_reader,
    }.get(format)

    if not preferred_reader:
        return None, preferred_reader

    try:
        if (
            preferred_reader not in _subsystem_map
            or preferred_reader not in allowed_subsystems[format]
        ):
            raise ValueError(
                f"Preferred reader '{preferred_reader}' not supported for "
                f"{format} import."
            )

        module_name, func_name = _subsystem_map[preferred_reader]
        module = __import__(module_name, fromlist=[func_name])
        return getattr(module, func_name), preferred_reader
    except Exception as e:
        print(f"Error importing {preferred_reader} for {format} files: {e}")
    return None, preferred_reader

_import_score

_import_score(
    filename: str | Path,
    format: str,
    flatten: bool = False,
    collapse: bool = False,
    show: bool = False,
    group_by_instrument: bool = True,
) -> Score

Import a score file

Author: Roger B. Dannenberg

Source code in amads/io/readscore.py
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
def _import_score(
    filename: str | Path,
    format: str,
    flatten: bool = False,
    collapse: bool = False,
    show: bool = False,
    group_by_instrument: bool = True,
) -> Score:
    """Import a score file

    <small>**Author**: Roger B. Dannenberg</small>
    """
    global _last_used_reader
    import_fn, preferred_reader = _check_for_subsystem(format)
    if import_fn is not None:
        _last_used_reader = import_fn
        if reader_warning_level != "none":
            print(
                f"Reading {str(filename)} using {format} reader "
                f"file={import_fn.__name__}."
            )
        return import_fn(
            str(filename), format, flatten, collapse, show, group_by_instrument
        )
    elif preferred_reader:
        raise Exception(
            f"Could not find an import function for file {str(filename)}, "
            f"format {format}. Preferred subsystem is {preferred_reader}"
        )
    else:
        raise Exception(
            "Could not identify a preferred subsystem or file format to "
            f"read file {str(filename)}, format {format}."
        )

read_score

read_score(
    filename: str | Path,
    flatten: bool = False,
    collapse: bool = False,
    show: bool = False,
    format: Optional[str] = None,
    group_by_instrument: bool = True,
) -> Score

Read a file with the given format, 'musicxml', 'midi', 'kern', 'mei'.

If format is None (default), the format is based on the filename extension, which can be 'musicxml', 'mid', 'midi', 'smf', 'kern', or 'mei'. (Valid extensions are in amads.io.readscore.valid_score_extensions.)

Author: Roger B. Dannenberg

Parameters:

  • filename (str | Path) –

    The path (relative or absolute) to the music file. Can also be an URL.

  • flatten (bool, default: False ) –

    The returned score will be flat (Score, Parts, Notes).

  • collapse (bool, default: False ) –

    If collapse and flatten, the parts will be merged into one.

  • show (bool, default: False ) –

    Print a text representation of the data.

  • format (Optional[str], default: None ) –

    One from among limited standard options (e.g., 'musicxml', 'midi', 'kern', 'mei')

  • group_by_instrument (bool, default: True ) –

    If True (default), when the underlying reader (e.g. for "pretty_midi", "music21" or "partitura") reads Parts with the same instrument, their content will be grouped into a single part. This means that if flatten, then parts with the same instrument will be merged into a single part. If flatten is False, then the staffs of parts with the same instrument will be grouped within a single part. If group_by_instrument is False, the parts read in by the underlying reader will be preserved as separate parts. group_by_instrument is True by default so that when reading Piano scores with separate treble and bass staffs, the resulting AMADS Score will generally have a single Piano part with two staffs. A score for Piano and Violin will generally have two parts, one for Piano and one for Violin, as opposed to three parts (Piano-Treble, Piano-Bass, Violin). On the other hand, a score for two Violins might be represented a one part with two staffs by default, but setting group_by_instrument to False will more likely keep the two Violin parts separate. Unfortunately, exact behavior depends on the underlying reader, MIDI track names, and/or MusicXML structure and naming.

Returns:

  • Score

    The imported score

Raises:

  • ValueError

    If the format is unknown or not implemented.

Note on Incomplete First Measure

In Music21, the first measure may be a partial measure containing an anacrusis (“pickup”). This is somewhat ambiguous and does not translate well to MIDI which is less expressive than MusicXML.

Therefore, if the first measure read with Music21 is not a full measure, a rest is inserted and the remainder is shifted to form a full measure according to its time signature. Remaining measures are shifted in time accordingly and Score, Part and Staff durations are adjusted accordingly.

General MIDI Import Notes

Each Standard MIDI File track corresponds to a Staff when creating a full AMADS Score. Everything is combined into one part when flatten and collapse are specified.

AMADS assumes that instruments (midi program numbers) are fixed for each Staff (or Part in flat scores), and MIDI channels are not represented. The use of program change messages within a track to change the program are ignored, but may generate warnings.

In general, AMADS instrument name corresponds to the MIDI track name, and MIDI program numbers are stored as "midi_program" in the info attribute of the Staff or Part corresponding to the track.

MIDI files do not have a Part/Staff structure, but you can write multiple tracks with the same name. Both the "music21" and "pretty_midi" readers will group tracks with matching names as Staffs in a single Part. This may result in an unexpected Part/Staff hierarchy if tracks are not named or if tracks are named something like "Piano-Treble" and "Piano-Bass", which would produce two Parts as different instruments as opposed to one Part with two Staffs.

Unless flatten or collapse, the MIDI file time signature information will be used to form Measures with Staffs, and Notes will be broken where they cross measure boundaries and then tied. The default time signature is 4/4.

Pretty MIDI Import Notes

If there is no program change in a file, the "pretty_midi" reader will use 0, and 0 will be stored as "midi_program" in the Part or Staff's info (see get and set).

If there is no track name, the Part.instrument is derived from the track program number (defaults to zero).

If the MIDI file track name is "Unknown", the Part.instrument is set to None. This is because when the "pretty_midi" writer writes a part where Part.instrument is None, the name "Unknown" is used instead. Therefore, the reader will recreate the AMADS Part where Part.instrument is None.

Pretty MIDI will not insert any KeySignature unless key signature meta-events are found.

Music21 MIDI Import Notes

Music21 may infer a Clef and KeySignature even though MIDI does not even have a meta-event for clefs, and even if the MIDI file has no key signature meta-event.

Source code in amads/io/readscore.py
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
def read_score(
    filename: str | Path,
    flatten: bool = False,
    collapse: bool = False,
    show: bool = False,
    format: Optional[str] = None,
    group_by_instrument: bool = True,
) -> Score:
    """Read a file with the given format, 'musicxml', 'midi', 'kern', 'mei'.

    If format is None (default), the format is based on the filename
    extension, which can be 'musicxml', 'mid', 'midi', 'smf', 'kern',
    or 'mei'. (Valid extensions are in
    `amads.io.readscore.valid_score_extensions`.)

    <small>**Author**: Roger B. Dannenberg</small>

    Parameters
    ----------
    filename : str | Path
        The path (relative or absolute) to the music file.
        Can also be an URL.
    flatten : bool
        The returned score will be flat (Score, Parts, Notes).
    collapse: bool
        If collapse and flatten, the parts will be merged into one.
    show : bool
        Print a text representation of the data.
    format: string
        One from among limited standard options (e.g.,
        `'musicxml'`, `'midi'`, `'kern'`, `'mei'`)
    group_by_instrument : bool
        If True (default), when the underlying reader (e.g. for "pretty_midi",
        "music21" or "partitura") reads Parts with the same instrument, their
        content will be grouped into a single part. This means that if
        `flatten`, then parts with the same instrument will be merged into a
        single part. If `flatten` is False, then the staffs of parts with the
        same instrument will be grouped within a single part.
        If `group_by_instrument` is False, the parts read in by the underlying
        reader will be preserved as separate parts. `group_by_instrument` is
        True by default so that when reading Piano scores with separate treble
        and bass staffs, the resulting AMADS Score will generally have a single
        Piano part with two staffs. A score for Piano and Violin will generally
        have two parts, one for Piano and one for Violin, as opposed to three
        parts (Piano-Treble, Piano-Bass, Violin). On the other hand, a score for
        two Violins might be represented a one part with two staffs by default,
        but setting `group_by_instrument` to False will more likely keep the
        two Violin parts separate. Unfortunately, exact behavior depends on the
        underlying reader, MIDI track names, and/or MusicXML structure and
        naming.

    Returns
    -------
    Score
        The imported score

    Raises
    ------
    ValueError
        If the format is unknown or not implemented.

    Note on Incomplete First Measure
    --------------------------------
    In Music21, the first measure may be a partial measure containing
    an anacrusis (“pickup”). This is somewhat ambiguous and does not
    translate well to MIDI which is less expressive than MusicXML.

    Therefore, if the first measure read with Music21 is not a full
    measure, a rest is inserted and the remainder is shifted to
    form a full measure according to its time signature. Remaining
    measures are shifted in time accordingly and Score, Part and
    Staff durations are adjusted accordingly.

    General MIDI Import Notes
    -------------------------
    Each Standard MIDI File track corresponds to a Staff when
    creating a full AMADS Score. Everything is combined into one
    part when `flatten` and `collapse` are specified.

    AMADS assumes that instruments (midi program numbers) are fixed
    for each Staff (or Part in flat scores), and MIDI channels are
    not represented. The use of program change messages within a
    track to change the program are ignored, but may generate warnings.

    In general, AMADS instrument name corresponds to the MIDI track
    name, and MIDI program numbers are stored as `"midi_program"`
    in the `info` attribute of the Staff or Part corresponding to
    the track.

    MIDI files do not have a Part/Staff structure, but you can
    write multiple tracks with the same name. Both the `"music21"`
    and `"pretty_midi"` readers will group tracks with matching
    names as Staffs in a single Part. This may result in an
    unexpected Part/Staff hierarchy if tracks are not named or
    if tracks are named something like "Piano-Treble" and
    "Piano-Bass", which would produce two Parts as different
    instruments as opposed to one Part with two Staffs.

    Unless `flatten` or `collapse`, the MIDI file time signature
    information will be used to form Measures with Staffs, and
    Notes will be broken where they cross measure boundaries and
    then tied.  The default time signature is 4/4.

    Pretty MIDI Import Notes
    ------------------------
    If there is no program change in a file, the `"pretty_midi"`
    reader will use 0, and 0 will be stored as `"midi_program"`
    in the Part or Staff's `info` (see
    [get][amads.core.basics.Event.get] and
    [set][amads.core.basics.Event.set]).

    If there is no track name, the `Part.instrument` is derived
    from the track program number (defaults to zero).

    If the MIDI file track name is `"Unknown"`, the `Part.instrument`
    is set to None. This is because when the `"pretty_midi"` writer
    writes a part where `Part.instrument is None`, the name `"Unknown"`
    is used instead. Therefore, the reader will recreate the AMADS
    Part where `Part.instrument is None`.

    Pretty MIDI will not insert any KeySignature unless key signature
    meta-events are found.

    Music21 MIDI Import Notes
    -------------------------
    Music21 may infer a Clef and KeySignature even though MIDI
    does not even have a meta-event for clefs, and even if the
    MIDI file has no key signature meta-event.
    """
    if isinstance(filename, str) and (
        filename.startswith("http") or "://" in filename
    ):
        with tempfile.NamedTemporaryFile(
            suffix=Path(filename).suffix or ".tmp", delete=False
        ) as tmp_file:
            urllib.request.urlretrieve(filename, tmp_file.name)
            filename = tmp_file.name

    if format is None:
        filename = Path(filename)
        ext = filename.suffix.lower()
        if ext not in [".pdf", ".ly"]:  # these are write-only extensions
            format = _suffix_to_format.get(ext)
        if not format:
            raise ValueError(
                f"Unsupported file extension: {ext}. "
                f"Valid extensions: {valid_score_extensions}"
            )

    # File format handling
    with warnings.catch_warnings(record=True) as w:
        warnings.simplefilter(
            "ignore" if reader_warning_level == "none" else "always"
        )

        score = _import_score(
            filename, format, flatten, collapse, show, group_by_instrument
        )

        # Warning handling
        if reader_warning_level == "low":
            if len(w) > 0:
                print(
                    f"Warning: {len(w)} warnings were generated in "
                    f"read_score({filename}).\n"
                    "  Use amads.io.readscore.set_reader_warning_level() "
                    "for more details."
                )
        else:  # "none", "default", or "high", but w is empty if "none"
            for warning in w:
                print(
                    f"{warning.filename}:{warning.lineno}: "
                    f"{warning.category.__name__}: {warning.message}"
                )

        return score

last_used_reader

last_used_reader() -> Optional[str]

Return the name of the last used reader function.

The reader function is an internal function called by read_score and based on the format, file name, and preferences in effect.

Returns:

  • Optional[str]

    The name of the actual function used in the last call to read_score, or None if no reader has been used yet.

Source code in amads/io/readscore.py
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
def last_used_reader() -> Optional[str]:
    """Return the name of the last used reader function.

    The reader function is an internal function called by `read_score`
    and based on the format, file name, and preferences in effect.

    Returns
    -------
    Optional[str]
        The name of the actual function used in the last call to `read_score`,
        or None if no reader has been used yet.
    """
    if _last_used_reader is not None:
        return _last_used_reader.__name__
    return None

_expand_first_measure

_expand_first_measure(staff: Staff) -> float

expand first measure to a full measure if necessary

Source code in amads/io/readscore.py
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
def _expand_first_measure(staff: Staff) -> float:
    """expand first measure to a full measure if necessary"""
    # what is the maximum offset of the first measure?
    shift = 0
    if len(staff.content) > 0:
        m1: Measure = cast(Measure, staff.content[0])
        m1_duration = m1.time_signature().duration
        max_offset = 0
        for elem in m1.content:
            max_offset = max(max_offset, elem.offset)
        if max_offset < m1_duration - 0.001:  # need to insert rest
            shift = m1_duration - max_offset
            for elem in m1.content:
                if (
                    isinstance(elem, Note)
                    or isinstance(elem, Rest)
                    or isinstance(elem, Chord)
                ):
                    elem.time_shift(shift)
            # insert Rest, Since m1 is first, m1.onset == 0
            _ = Rest(m1, m1.onset, duration=shift)
            m1.offset = m1_duration
            # now, first measure ending may have shifted, so adjust
            # remainder of the part
            if len(staff.content) > 1 and shift > 0.001:
                for m in staff.content[1:]:
                    # score.time_signatures has not been shifted yet, so
                    # look up the time signature for the meausure duration
                    # before shifting. We set the measure duration because
                    # partitura will give measure durations shorter than the
                    # time signature duration if the measure is not full
                    m.duration = m.time_signature().duration
                    m.time_shift(shift)
            staff.offset = staff.content[-1].offset
            # update Part offset:
            part = staff.parent
            part.offset = max(part.offset, staff.offset)
            # update Score offset:
            score = part.parent
            score.offset = max(score.duration, staff.offset)
    return shift

_finish_import

_finish_import(
    score: Score, flatten: bool, collapse: bool, shift: float
) -> Score

Apply some final manipulations common to m21 and pt import

Source code in amads/io/readscore.py
563
564
565
566
567
568
569
570
571
572
573
574
def _finish_import(
    score: Score, flatten: bool, collapse: bool, shift: float
) -> Score:
    """Apply some final manipulations common to m21 and pt import"""
    if shift > 0.001:
        # parts are shifted but not measures and time signatures.
        # shared_shift is in beats
        score.time_map._time_shift(shift)
        score._timesignatures_shift(shift)
    if flatten or collapse:
        score = score.flatten(collapse=collapse)
    return score

Output

Similar to input functions, you should use writescore.write_score described below to write an AMADS Score to a file.

writescore

functions for file output

Classes

Functions

set_preferred_midi_writer

set_preferred_midi_writer(writer: str = _default_midi_writer) -> str

Set a (new) preferred MIDI writer.

Returns the previous writer preference. The current preference is stored in amads.io.writer.preferred_midi_writer.

Parameters:

  • writer (str, default: _default_midi_writer ) –

    The name of the preferred MIDI writer. Can be "music21" or "pretty_midi". Defaults to "pretty_midi".

Returns:

  • str

    The previous name of the preferred MIDI writer.

Raises:

  • ValueError

    If an invalid writer is provided.

Source code in amads/io/writescore.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
def set_preferred_midi_writer(writer: str = _default_midi_writer) -> str:
    """Set a (new) preferred MIDI writer.

    Returns the previous writer preference. The current preference is stored
    in `amads.io.writer.preferred_midi_writer`.

    Parameters
    ----------
    writer : str, optional
        The name of the preferred MIDI writer. Can be "music21" or "pretty_midi".
        Defaults to "pretty_midi".

    Returns
    -------
    str
        The previous name of the preferred MIDI writer.

    Raises
    ------
    ValueError
        If an invalid writer is provided.

    """
    global preferred_midi_writer
    previous_writer = preferred_midi_writer
    if writer in ["music21", "partitura", "pretty_midi"]:
        preferred_midi_writer = writer
    else:
        raise ValueError(
            "Invalid MIDI writer. Choose 'music21', 'partitura', or "
            "'pretty_midi'."
        )
    return previous_writer

set_preferred_xml_writer

set_preferred_xml_writer(writer: str = _default_xml_writer) -> str

Set a (new) preferred XML writer.

Returns the previous writer preference. The current preference is stored in amads.io.writer.preferred_xml_writer.

Parameters:

  • writer (str, default: _default_xml_writer ) –

    The name of the preferred XML writer. Can be "music21" or "partitura". Defaults to "music21".

Returns:

  • str

    The previous name of the preferred XML writer.

Raises:

  • ValueError

    If an invalid writer is provided.

Source code in amads/io/writescore.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
def set_preferred_xml_writer(writer: str = _default_xml_writer) -> str:
    """
    Set a (new) preferred XML writer.

    Returns the previous writer preference. The current preference is stored
    in `amads.io.writer.preferred_xml_writer`.

    Parameters
    ----------
    writer : str, optional
        The name of the preferred XML writer. Can be "music21" or "partitura".
        Defaults to "music21".

    Returns
    -------
    str
        The previous name of the preferred XML writer.

    Raises
    ------
    ValueError
        If an invalid writer is provided.
    """
    global preferred_xml_writer
    previous_writer = preferred_xml_writer
    if writer in allowed_subsystems["musicxml"]:
        preferred_xml_writer = writer
    else:
        raise ValueError("Invalid XML writer. Choose 'music21' or 'partitura'.")
    return previous_writer

set_preferred_kern_writer

set_preferred_kern_writer(writer: str = _default_kern_writer) -> str

Set a (new) preferred Kern writer.

Returns the previous writer preference. The current preference is stored in amads.io.writer.preferred_kern_writer.

Parameters:

  • writer (str, default: _default_kern_writer ) –

    The name of the preferred Kern writer. Can be "music21". Defaults to "music21".

Returns:

  • str

    The previous name of the preferred Kern writer.

Raises:

  • ValueError

    If an invalid writer is provided.

Source code in amads/io/writescore.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
def set_preferred_kern_writer(writer: str = _default_kern_writer) -> str:
    """
    Set a (new) preferred Kern writer.

    Returns the previous writer preference. The current preference is stored
    in `amads.io.writer.preferred_kern_writer`.

    Parameters
    ----------
    writer : str, optional
        The name of the preferred Kern writer. Can be "music21".
        Defaults to "music21".

    Returns
    -------
    str
        The previous name of the preferred Kern writer.

    Raises
    ------
    ValueError
        If an invalid writer is provided.
    """
    global preferred_kern_writer
    previous_writer = preferred_kern_writer
    if writer in allowed_subsystems["kern"]:
        preferred_kern_writer = writer
    else:
        raise ValueError("Invalid Kern writer. Choose 'music21'.")
    return previous_writer

set_preferred_mei_writer

set_preferred_mei_writer(writer: str = _default_mei_writer) -> str

Set a (new) preferred MEI writer.

Returns the previous writer preference. The current preference is stored in amads.io.writer.preferred_mei_writer.

Parameters:

  • writer (str, default: _default_mei_writer ) –

    The name of the preferred MEI writer. Can be "music21". Defaults to "music21".

Returns:

  • str

    The previous name of the preferred MEI writer.

Raises:

  • ValueError

    If an invalid writer is provided.

Source code in amads/io/writescore.py
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
def set_preferred_mei_writer(writer: str = _default_mei_writer) -> str:
    """
    Set a (new) preferred MEI writer.

    Returns the previous writer preference. The current preference is stored
    in `amads.io.writer.preferred_mei_writer`.

    Parameters
    ----------
    writer : str, optional
        The name of the preferred MEI writer. Can be "music21".
        Defaults to "music21".

    Returns
    -------
    str
        The previous name of the preferred MEI writer.

    Raises
    ------
    ValueError
        If an invalid writer is provided.
    """
    global preferred_mei_writer
    previous_writer = preferred_mei_writer
    if writer in allowed_subsystems["mei"]:
        preferred_mei_writer = writer
    else:
        raise ValueError("Invalid MEI writer. Choose 'music21'.")
    return previous_writer

set_preferred_pdf_writer

set_preferred_pdf_writer(writer: str = _default_pdf_writer) -> str

Set a (new) preferred PDF writer.

Returns the previous writer preference. The current preference is stored in amads.io.writescore.preferred_pdf_writer. Preferences are: - "music21-lilypond" - use music21 to create a LilyPond file, then use LilyPond to create a PDF. - "music21-xml-lilypond" - use music21 to create a MusicXML file, then run the program musicxml2ly to convert XML to LilyPond, then run LilyPond to create a PDF. - "partitura-xml-lilypond" - use partitura to create a MusicXML file, then run the program musicxml2ly to convert XML to LilyPond, then run LilyPond to create a PDF.

Parameters:

  • writer (str, default: _default_pdf_writer ) –

    The name of the preferred PDF writer. Can be "music21-lilypond", "music21-xml-lilypond", or "partitura-xml-lilypond". Defaults to "music21-xml-lilypond".

Returns:

  • str

    The previous name of the preferred PDF writer.

Raises:

  • ValueError

    If an invalid writer is provided.

Source code in amads/io/writescore.py
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def set_preferred_pdf_writer(writer: str = _default_pdf_writer) -> str:
    """
    Set a (new) preferred PDF writer.

    Returns the previous writer preference. The current preference is stored
    in `amads.io.writescore.preferred_pdf_writer`. Preferences are:
    - "music21-lilypond" - use music21 to create a LilyPond file, then use
      LilyPond to create a PDF.
    - "music21-xml-lilypond" - use music21 to create a MusicXML file, then run
      the program musicxml2ly to convert XML to LilyPond, then run LilyPond to
      create a PDF.
    - "partitura-xml-lilypond" - use partitura to create a MusicXML file, then
      run the program musicxml2ly to convert XML to LilyPond, then run LilyPond
      to create a PDF.


    Parameters
    ----------
    writer : str, optional
        The name of the preferred PDF writer. Can be "music21-lilypond",
        "music21-xml-lilypond", or "partitura-xml-lilypond".
        Defaults to "music21-xml-lilypond".

    Returns
    -------
    str
        The previous name of the preferred PDF writer.

    Raises
    ------
    ValueError
        If an invalid writer is provided.
    """
    global preferred_pdf_writer
    previous_writer = preferred_pdf_writer
    if writer in allowed_subsystems["pdf"]:
        preferred_pdf_writer = writer
    else:
        raise ValueError(
            "Invalid PDF writer. Choose " f"{allowed_subsystems['pdf']}."
        )
    return previous_writer

set_writer_warning_level

set_writer_warning_level(level: str) -> str

Set the warning level for writescore functions.

The translation from AMADS to music data files is not always well-defined and may involve intermediate representations using Music21, Partitura or others. Usually, warnings are produced when there is possible data loss or ambiguity, but these can be more annoying than informative. The warning level can be controlled using this function, which applies to all file formats.

The current warning level is stored in amads.io.writer.writer_warning_level.

Parameters:

  • level (str) –

    The warning level to set. Can be "none", "low", "default", "high".

    • "none" - will suppress all warnings during write_score().
    • "low" - will show print one notice if there are any warnings.
    • "default" - will obey environment settings to control warnings.
    • "high" - will print all warnings during write_score(), overriding environment settings.

Returns:

  • str

    Previous warning level.

Raises:

  • ValueError

    If an invalid warning level is provided.

Source code in amads/io/writescore.py
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
def set_writer_warning_level(level: str) -> str:
    """
    Set the warning level for writescore functions.

    The translation from AMADS to music data files is not always well-defined
    and may involve intermediate representations using Music21, Partitura or
    others. Usually, warnings are produced when there is possible data loss or
    ambiguity, but these can be more annoying than informative. The warning
    level can be controlled using this function, which applies to all file
    formats.

    The current warning level is stored in
    `amads.io.writer.writer_warning_level`.

    Parameters
    ----------
    level : str
        The warning level to set. Can be "none", "low", "default", "high".

        - "none" - will suppress all warnings during write_score().
        - "low" - will show print one notice if there are any warnings.
        - "default" - will obey environment settings to control warnings.
        - "high" - will print all warnings during write_score(), overriding
            environment settings.

    Returns
    -------
    str
        Previous warning level.

    Raises
    -------
    ValueError
        If an invalid warning level is provided.
    """
    global writer_warning_level
    previous_level = writer_warning_level
    if level in ["none", "low", "default", "high"]:
        writer_warning_level = level
    else:
        raise ValueError(
            "Invalid warning level. Choose 'none', 'low', 'default', or 'high'."
        )
    return previous_level

_check_for_subsystem

_check_for_subsystem(
    format: str,
) -> tuple[
    Optional[
        Callable[
            [Score, Optional[str | Path], Optional[str], bool, bool],
            None,
        ]
    ],
    Optional[str],
]

Check if the preferred subsystem is available.

Parameters:

  • format (str) –

    The format of the file to write, either 'midi', 'musicxml', 'pdf', or 'lilypond'.

Returns:

  • tuple[Optional[Callable], Optional[str]]

    The export function if available, None otherwise, and the name of the subsystem used.

Source code in amads/io/writescore.py
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
def _check_for_subsystem(
    format: str,
) -> tuple[
    Optional[
        Callable[[Score, Optional[str | Path], Optional[str], bool, bool], None]
    ],
    Optional[str],
]:
    """Check if the preferred subsystem is available.

    Parameters
    ----------
    format : str
        The format of the file to write, either 'midi', 'musicxml', 'pdf',
        or 'lilypond'.

    Returns
    -------
    tuple[Optional[Callable], Optional[str]]
        The export function if available, None otherwise, and the name of
        the subsystem used.
    """
    preferred_writer = {
        "midi": preferred_midi_writer,
        "musicxml": preferred_xml_writer,
        "kern": preferred_midi_writer,
        "mei": preferred_midi_writer,
        "pdf": preferred_pdf_writer,
        "lilypond": preferred_pdf_writer,
    }.get(format)

    if not preferred_writer:
        return None, None

    try:
        if (
            preferred_writer not in _subsystem_map
            or preferred_writer not in allowed_subsystems[format]
        ):
            raise ValueError(
                f"Preferred writer '{preferred_writer}' not supported for "
                f"{format} export."
            )

        module_name, func_name = _subsystem_map[preferred_writer]
        module = __import__(module_name, fromlist=[func_name])
        # note that the type signature of func_name has an extra `display``
        # when format is "pdf", so if you call it with a `display` argument,
        # type checking will complain:
        return getattr(module, func_name), preferred_writer
    except Exception as e:
        print(f"Error importing {preferred_writer} for {format} files: {e}")
    return None, preferred_writer

_path_help

_path_help(
    path: Optional[Path | str],
    ext: str | list[str],
    extra_ext: Optional[str] = None,
    is_temp: bool = False,
) -> Path | tuple[Path, Path]

Construct a path for writescore functions.

If path is NULL, construct a name for a new temp file ending in ext. If path is given, it must end in ext or one of the listed extensions.

Parameters:

  • path (Optional[Path | str]) –

    The proposed name if any. If None, a temp file name is created.

  • ext (str | list[str]) –

    The extension required for path or list of extensions allowed for path.

  • extra_ext (Optional[str], default: None ) –

    Sometimes, we need an extra temp file path, e.g. an intermediate MusicXML path when the goal is writing a Lilypond file. If extra_ext is provided, a temp file with that extension is created and returned as the second element of a tuple. If path is None, both returned paths will share a temp directory. If you need more temp files, you can use .with_suffix(".ext2") to create unique paths from the 2nd return value, as long as ".ext2" is notextorextra_ext`.

  • is_temp (bool, default: False ) –

    If True, we can make temp file names by changing path suffix.

Returns:

  • Path | Tuple[Path, Path]

    Returns a single Path unless extra_ext is provided, in which case two Paths are returned.

Raises:

  • ValueError

    If a given filename does not end in ext or if is_temp and there is a conflict in extensions.

Source code in amads/io/writescore.py
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
def _path_help(
    path: Optional[Path | str],
    ext: str | list[str],
    extra_ext: Optional[str] = None,
    is_temp: bool = False,
) -> Path | tuple[Path, Path]:
    """
    Construct a path for writescore functions.

    If path is NULL, construct a name for a new temp file ending in ext.
    If path is given, it must end in ext or one of the listed extensions.

    Parameters
    ----------
    path: Optional[Path | str]
        The proposed name if any. If None, a temp file name is created.
    ext: str | list[str]
        The extension required for path or list of extensions allowed for path.
    extra_ext: Optional[str]
        Sometimes, we need an extra temp file path, e.g. an intermediate
        MusicXML path when the goal is writing a Lilypond file. If `extra_ext`
        is provided, a temp file with that extension is created and returned
        as the second element of a tuple. If path is None, both returned paths
        will share a temp directory. If you need more temp files, you can use
        `.with_suffix(".ext2") to create unique paths from the 2nd return
        value, as long as ".ext2" is not `ext` or `extra_ext`.
    is_temp: bool
        If True, we can make temp file names by changing path suffix.

    Returns
    -------
    Path | Tuple[Path, Path]
        Returns a single Path unless extra_ext is provided, in which case
        two Paths are returned.

    Raises
    ------
    ValueError
        If a given `filename` does not end in `ext` or if is_temp and
        there is a conflict in extensions.
    """
    temp_dir = None
    result = Path(path) if isinstance(path, str) else path

    if not result:
        # to avoid a race condition, create a (unique) directory for
        # the file
        if isinstance(ext, list):
            ext = ext[0]  # use the first in list as extension
        temp_dir = tempfile.mkdtemp(prefix="amads_")
        result = Path(temp_dir) / ("temp" + ext)
    elif isinstance(result, Path):
        if not isinstance(ext, list):
            ext = [ext]  # make it a list so we can check for membership
        if result.suffix not in ext:
            raise ValueError(
                f"filename {str(result)} was expected to" f" end with {ext}"
            )
    else:
        raise ValueError(f"path is not a Path or str or None: {repr(path)}.")

    if extra_ext:  # need an extra temp file with extension extra_ext:
        if not temp_dir:
            if is_temp:
                if result.suffix.lower() == extra_ext.lower():
                    raise ValueError(
                        f"filename {str(result)} is already using"
                        f" {extra_ext}, but is_temp implies that we can"
                        " make a new path by changing the extension."
                    )
                temp_dir = result.parent
            else:
                temp_dir = tempfile.mkdtemp(prefix="amads_")
        result = (result, Path(temp_dir) / ("temp" + extra_ext))

    return result

_export_score

_export_score(
    score: Score,
    filename: str | Path,
    format: str,
    show: bool = False,
    is_temp: bool = False,
) -> None

Use Partitura or music21 to export a MusicXML file.

Author: Roger B. Dannenberg

Source code in amads/io/writescore.py
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
def _export_score(
    score: Score,
    filename: str | Path,
    format: str,
    show: bool = False,
    is_temp: bool = False,
) -> None:
    """Use Partitura or music21 to export a MusicXML file.

    <small>**Author**: Roger B. Dannenberg</small>
    """
    global _last_used_writer

    export_fn, subsystem = _check_for_subsystem(format)
    if export_fn is not None:
        _last_used_writer = export_fn
        if writer_warning_level != "none":
            print(
                f"Exporting {filename} using {format} writer"
                f" {export_fn.__name__} from subsystem {subsystem}."
            )
        export_fn(score, filename, format, show, is_temp)
    else:
        raise Exception(
            f"Could not find an export function for format {format}. "
            "Preferred subsystem is " + str(subsystem)
        )

_update_format_with_filename

_update_format_with_filename(
    format: Optional[str], filename: Path
) -> str

Determine format from filename and check consistency

Source code in amads/io/writescore.py
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
def _update_format_with_filename(format: Optional[str], filename: Path) -> str:
    """Determine format from filename and check consistency"""

    if filename:
        ext = filename.suffix
        implied_format = _suffix_to_format.get(ext)
        if not implied_format:
            raise ValueError(
                f"Unsupported file extension: {ext}. "
                f"Valid extensions: {valid_score_extensions}"
            )
        if format and format != implied_format:
            raise ValueError(
                f"Filename extension {ext} conflicts with format {format}."
            )
        format = implied_format

    if format not in _suffix_to_format.values():
        raise ValueError(f"Unknown or unspecified format: {format}")

    return format  # type: ignore  (format is guaranteed to be a string here)

write_score

write_score(
    score: Score,
    filename: str | Path | None,
    show: bool = False,
    format: Optional[str] = None,
    is_temp: bool = False,
) -> Path

Write a file with the given format.

If format is None (default), the format is based on the filename extension, which can be one of writescore.valid_score_extensions ('xml', 'musicxml', 'mxl', 'mid', 'midi', 'smf', 'krn', 'kern', 'mei', 'pdf', or 'ly').

Author: Roger B. Dannenberg

Parameters:

  • score (Score) –

    the score to write

  • filename (str | Path | None) –

    the path (relative or absolute) to the music file. If None, a temp directory is created, a file is created in the directory, and the path of the written file is returned.

  • show (bool, default: False ) –

    print a text representation of the data

  • format (Optional[string], default: None ) –

    one of 'musicxml', 'midi', 'kern', 'mei', 'pdf', 'lilypond'. Defaults to the format implied by filename.

  • is_temp (bool, default: False ) –

    If true, then intermediate files needed to construct filename can be placed in the same directory and named by changing the extension because filename is in a unique temp directory created by _path_help. (This is merely an optimization to group temp files and avoid creating another temp directory when it is unnecessary.)

Returns:

  • Path

    The path to which the data was written.

Raises:

  • ValueError

    if format is unknown

Notes

AMADS assumes that instruments (midi program numbers) are fixed for each Staff (or Part in flat scores), and MIDI channels are not represented. This corresponds to some DAWs such as LogicPro, which represents channels but ignores them when tracks are synthesized in software by a single instrument. The MIDI program is stored as info (see get and set) under key "midi_program" on the Staff, or if there is no Staff or no "midi_program" on the Staff, under key "midi_program" on the Part.

Parts also have an instrument attribute, which is stored as the MIDI track name. (Therefore, if a Part has two Staffs, there will be two tracks with the same name.) If there is no MIDI program for the track, the 'pretty_midi' writer will use pretty_midi.instrument_name_to_program to determine a program number since a program number is required. (As opposed to Standard MIDI Files, which need not have any MIDI program message at all.) If pretty_midi.instrument_name_to_program fails, the program is set to 0 (“Acoustic Grand Piano”).

Partitura does not seem to support per-staff key signatures, so key signatures from AMADS are simply added to Partitura parts. When there are multiple staffs, there could be duplicate key signatures (to be tested).

Pretty MIDI also requires an instrument name. If the AMADS Part instrument attribute is None, then "Unknown" is used. The Pretty MIDI reader will convert "Unknown" back to None.

Source code in amads/io/writescore.py
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
def write_score(
    score: Score,
    filename: str | Path | None,
    show: bool = False,
    format: Optional[str] = None,
    is_temp: bool = False,
) -> Path:
    """Write a file with the given format.

    If format is None (default), the format is based on the filename
    extension, which can be one of `writescore.valid_score_extensions`
    ('xml', 'musicxml', 'mxl', 'mid', 'midi', 'smf', 'krn', 'kern',
    'mei', 'pdf', or 'ly').

    <small>**Author**: Roger B. Dannenberg</small>

    Parameters
    ----------
    score : Score
        the score to write
    filename : str | Path | None
        the path (relative or absolute) to the music file. If None, a temp
        directory is created, a file is created in the directory, and the
        path of the written file is returned.
    show : bool
        print a text representation of the data
    format : Optional[string]
        one of `'musicxml'`, `'midi'`, `'kern'`, `'mei'`, `'pdf'`, `'lilypond'`.
        Defaults to the format implied by `filename`.
    is_temp: bool
        If true, then intermediate files needed to construct filename can
        be placed in the same directory and named by changing the extension
        because filename is in a unique temp directory created by _path_help.
        (This is merely an optimization to group temp files and avoid creating
        another temp directory when it is unnecessary.)

    Returns
    -------
    Path
        The path to which the data was written.

    Raises
    ------
    ValueError
        if format is unknown

    Notes
    -----
    AMADS assumes that instruments (midi program numbers) are fixed
    for each Staff (or Part in flat scores), and MIDI channels are
    not represented. This corresponds to some DAWs such as LogicPro,
    which represents channels but ignores them when tracks are
    synthesized in software by a single instrument. The MIDI program
    is stored as info (see [get][amads.core.basics.Event.get] and
    [set][amads.core.basics.Event.set]) under key `"midi_program"`
    on the Staff, or if there is no Staff or no `"midi_program"` on
    the Staff, under key `"midi_program"` on the Part.

    Parts also have an `instrument` attribute, which is stored as
    the MIDI track name. (Therefore, if a Part has two Staffs, there
    will be two tracks with the same name.)  If there is no MIDI
    program for the track, the `'pretty_midi'` writer will use
    `pretty_midi.instrument_name_to_program` to determine a program
    number since a program number is required. (As opposed to Standard
    MIDI Files, which need not have any MIDI program message at all.)
    If `pretty_midi.instrument_name_to_program` fails, the program is
    set to 0 (“Acoustic Grand Piano”).

    Partitura does not seem to support per-staff key signatures,
    so key signatures from AMADS are simply added to Partitura
    parts. When there are multiple staffs, there could be
    duplicate key signatures (to be tested).

    Pretty MIDI also requires an instrument name. If the AMADS Part
    `instrument` attribute is `None`, then `"Unknown"` is used. The
    Pretty MIDI reader will convert `"Unknown"` back to `None`.

    """
    if not isinstance(filename, Path):
        if filename:
            filename = Path(filename)
        elif format:
            tmp_dir = mkdtemp("_amads")
            filename = Path(tmp_dir) / ("score" + _format_to_suffix[format])
    format = _update_format_with_filename(format, cast(Path, filename))

    with warnings.catch_warnings(record=True) as w:
        warnings.simplefilter(
            "ignore" if writer_warning_level == "none" else "always"
        )
        # format is guaranteed to be a Path here
        _export_score(score, filename, cast(str, format), show)  # type: ignore

        # Warning handling
        if writer_warning_level == "low":
            if len(w) > 0:
                print(
                    f"Warning: {len(w)} warnings were generated in"
                    f" write_score({filename}). Use"
                    " amads.io.writescore.set_writer_warning_level() for"
                    " more details."
                )
        else:  # "none", "default", or "high"
            for warning in w:
                formatted = warnings.formatwarning(
                    warning.message,
                    warning.category,
                    warning.filename,
                    warning.lineno,
                )
                print(formatted, end="")
    return cast(Path, filename)

last_used_writer

last_used_writer() -> Optional[str]

Return the name of the last used writer function.

The writer function is an internal function called by write_score and based on the format, file name, and preferences in effect.

Returns:

  • Optional[str]

    The name of the actual function used in the last call to write_score, or None if no writer has been used yet.

Source code in amads/io/writescore.py
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
def last_used_writer() -> Optional[str]:
    """Return the name of the last used writer function.

    The writer function is an internal function called by `write_score`
    and based on the format, file name, and preferences in effect.

    Returns
    -------
    Optional[str]
        The name of the actual function used in the last call to `write_score`,
        or None if no writer has been used yet.
    """
    if _last_used_writer is not None:
        return _last_used_writer.__name__
    return None

Display

Similar to output functions, you should use displayscore.display_score described below to display an AMADS Score. You can use Music21 to write directly to a LilyPond file and use LilyPond to render the file as a PDF, you can use Music21 or Partitura to write a musicxml file, convert that with musicxml2ly and render with LilyPond, you can write a musicxml file and open it with MuseScore, or you can write a musicxml file and embed it in an HTML file along with Open Sheet Music Display (OSDM) and open it in a browser.

You will need to install LilyPond and/or MuseScore to use them for music display. OSDM is installed automatically as part of AMADS.

displayscore

Functions for score display

Author: Roger B. Dannenberg

Classes

Functions

set_preferred_display_method

set_preferred_display_method(
    method: str = _default_display_method,
) -> str

Set a (new) preferred display method.

Returns the previous preference. The current preference is stored in amads.io.writer.preferred_display_method

Parameters:

  • method (str, default: _default_display_method ) –

    The name of the preferred method. Can be "pdf", "musescore", "OSMD" (Open Sheet Music Display) or "pianoroll". Note that if the method is "pdf", then io.writescore.preferred_pdf_writer is used to create a PDF to display. Defaults to "pdf".

Returns:

  • str

    The previous name of the preferred method.

Raises:

  • ValueError

    If an invalid method is provided.

Source code in amads/io/displayscore.py
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def set_preferred_display_method(method: str = _default_display_method) -> str:
    """Set a (new) preferred display method.

    Returns the previous preference. The current preference is stored
    in `amads.io.writer.preferred_display_method`

    Parameters
    ----------
    method : str
        The name of the preferred method. Can be "pdf", "musescore", "OSMD"
        (Open Sheet Music Display) or "pianoroll". Note that if the method
        is "pdf", then `io.writescore.preferred_pdf_writer` is used to create
        a PDF to display. Defaults to "pdf".

    Returns
    -------
    str
        The previous name of the preferred method.

    Raises
    ------
    ValueError
        If an invalid method is provided.
    """
    global preferred_display_method
    previous_display_method = preferred_display_method
    if method in ["pdf", "musescore", "OSMD", "pianoroll"]:
        preferred_display_method = method
    else:
        raise ValueError(
            "Invalid method. Choose 'pdf', 'musescore', 'OSMD', or 'pianoroll'."
        )
    return previous_display_method

_load_mxl

_load_mxl(path: str) -> str

Extract the MusicXML content from a compressed .mxl file.

An .mxl ZIP archive contains a META-INF/container.xml that identifies the root MusicXML file. Fall back to the first .xml/.musicxml entry if container.xml is absent.

Source code in amads/io/displayscore.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
def _load_mxl(path: str) -> str:
    """
    Extract the MusicXML content from a compressed .mxl file.

    An .mxl ZIP archive contains a META-INF/container.xml that identifies
    the root MusicXML file. Fall back to the first .xml/.musicxml entry
    if container.xml is absent.
    """
    with zipfile.ZipFile(path, "r") as zf:
        # Try the standard MXL manifest first
        if "META-INF/container.xml" in zf.namelist():
            container = zf.read("META-INF/container.xml").decode("utf-8")
            # The rootfile element's full-path attribute names the XML file
            import xml.etree.ElementTree as ET

            root = ET.fromstring(container)
            ns = {"mc": "urn:oasis:names:tc:opendocument:xmlns:container"}
            rootfile = root.find(".//mc:rootfile", ns)
            if rootfile is not None:
                xml_filename = rootfile.attrib["full-path"]
                return zf.read(xml_filename).decode("utf-8")

        # Fallback: first .xml or .musicxml entry that isn't the manifest
        for name in zf.namelist():
            if name.startswith("META-INF"):
                continue
            if name.endswith(".xml") or name.endswith(".musicxml"):
                return zf.read(name).decode("utf-8")

    raise ValueError(f"No MusicXML content found in {path}")

display_file

display_file(file: str) -> None

Display a score from a file path.

Parameters:

  • file (str) –

    the file (must be MIDI or MusicXML) to display.

Raises:

  • RunTimeError

    If file does not end with ".mid", ".midi", ".xml", ".musicxml" or ".mxl".

Source code in amads/io/displayscore.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
def display_file(file: str) -> None:
    """Display a score from a file path.

    Parameters
    ----------
    file : str
        the file (must be MIDI or MusicXML) to display.

    Raises
    ------
    RunTimeError
        If `file` does not end with ".mid", ".midi", ".xml", ".musicxml"
        or ".mxl".
    """
    if file.endswith(".mid") or file.endswith(".midi"):
        score = read_score(file)
        pianoroll(score)
    elif (
        file.endswith(".xml")
        or file.endswith(".musicxml")
        or file.endswith(".mxl")
    ):
        _display_musicxml_file(file)
    elif file.endswith(".pdf"):
        if suppress_external_open():
            print(f"PDF display suppressed during tests: {file}.")
        else:
            webbrowser.open(Path(file).resolve().as_uri())  # type: ignore
    else:
        raise RuntimeError(
            f"Unsupported file extension for display_file: {file}"
        )

display_score

display_score(score: Score, show: bool = False) -> None

Display a score.

AMADS supports several display methods. "pdf" (default) uses the preferred MusicXML writer (defaults to Music21) to write an XML file. Then, Lilypond (which must be installed) is run to create a pdf file. The pdf file is then opened using Python's webbrowser.open, which may in fact open the pdf with Preview on MacOS.

"pianoroll" will make a pianoroll display directly from the score and plot it, showing the plot.

"musescore" will make a MusicXML file using the preferred MusicXML writer and open the file with MuseScore, which must be installed.

"OSMD" will make a MusicXML file using the preferred MusicXML writer. It then constructs a web page consisting of the MusicXML file (as a Javascript text string) and OSMD (the Open Sheet Music Display library which runs in the browser to render the MusicXML). The constructed HTML file is opened in a browser.

Parameters:

  • score (Score) –

    the score to write

  • show (bool, default: False ) –

    show text representation of converted score for debugging.

Raises:

  • RunTimeError

    If the preferred_display_method is not "pianoroll", "musescore", or "OSMD".

Source code in amads/io/displayscore.py
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
def display_score(score: Score, show: bool = False) -> None:
    """Display a score.

    AMADS supports several display methods. "pdf" (default) uses the
    preferred MusicXML writer (defaults to Music21) to write an XML file.
    Then, Lilypond (which must be installed) is run to create a pdf file.
    The pdf file is then opened using Python's `webbrowser.open`, which
    may in fact open the pdf with Preview on MacOS.

    "pianoroll" will make a pianoroll display directly from the score
    and plot it, showing the plot.

    "musescore" will make a MusicXML file using the preferred MusicXML
    writer and open the file with MuseScore, which must be installed.

    "OSMD" will  make a MusicXML file using the preferred MusicXML
    writer. It then constructs a web page consisting of the MusicXML
    file (as a Javascript text string) and OSMD (the Open Sheet
    Music Display library which runs in the browser to render the
    MusicXML). The constructed HTML file is opened in a browser.

    Parameters
    ----------
    score : Score
        the score to write
    show : bool
        show text representation of converted score for debugging.

    Raises
    ------
    RunTimeError
        If the `preferred_display_method` is not "pianoroll", "musescore",
        or "OSMD".
    """
    if preferred_display_method == "pdf":
        # we need a temp directory for intermediate files:
        pdf_file = writescore._path_help(None, ".pdf")
        write_score(score, str(pdf_file), show, "pdf", True)
        display_file(str(pdf_file))
    elif preferred_display_method == "pianoroll":
        pianoroll(score)  # Note that 'show' for pianoroll invokes plt.show(),
        # which is different from display_score's 'show' argument, which prints
        # a text representation of the score upon conversion. However,
        # pianoroll does not *do* conversion, so our 'show' argument is not
        # relevant here. pianoroll's 'show' argument is set to True by default.
    elif (
        preferred_display_method == "musescore"
        or preferred_display_method == "OSMD"
    ):
        with tempfile.NamedTemporaryFile(
            prefix="amads_display_", suffix=".musicxml", delete=False
        ) as tmp_file:
            xml_path = tmp_file.name

        write_score(score, xml_path, show, "musicxml")

        if suppress_external_open():
            print(f"score display suppressed during tests; wrote {xml_path}")
            return
        _display_musicxml_file(xml_path)
    else:
        raise RuntimeError(
            f"Unsupported display method: {preferred_display_method}"
        )

_display_musicxml_file

_display_musicxml_file(xml_path: str) -> None

Use preferred_display_method to display a MusicXML file.

Parameters:

  • xml_path (str) –

Raises:

  • RunTimeError

    If MuseScore is needed but the executable was not found.

Source code in amads/io/displayscore.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
def _display_musicxml_file(xml_path: str) -> None:
    """
    Use `preferred_display_method` to display a MusicXML file.

    Parameters
    ----------
    xml_path (str): a path to the MusicXML or mxl (compressed) file.

    Raises
    ------
    RunTimeError
        If MuseScore is needed but the executable was not found.
    """
    if preferred_display_method == "musescore":
        musescore_exe = (
            shutil.which("musescore")
            or shutil.which("musescore4")
            or shutil.which("musescore3")
            or shutil.which("mscore")
        )
        if musescore_exe:
            subprocess.Popen([musescore_exe, xml_path])
            return

        if sys.platform == "darwin":
            for app_name in [
                "MuseScore Studio",
                "MuseScore 4",
                "MuseScore 3",
                "MuseScore",
            ]:
                result = subprocess.run(
                    ["open", "-a", app_name, xml_path],
                    capture_output=True,
                    check=False,
                    text=True,
                )
                if result.returncode == 0:
                    return

            subprocess.Popen(["open", xml_path])
            return

        raise RuntimeError(
            "Could not find MuseScore executable. Ensure MuseScore is "
            "installed and on PATH."
        )

    else:  # preferred_display_method == "OSMD"
        osmd_js = _get_osmd_js()
        path = os.path.abspath(xml_path)
        if zipfile.is_zipfile(path):
            musicxml_content = _load_mxl(path)
        else:
            with open(xml_path) as f:
                musicxml_content = f.read()
        html_content = f"""<!DOCTYPE html>
    <html lang="en">
<head>
<meta charset="UTF-8">
<title>OSMD Display</title>
</head>
<body>
<script>{osmd_js}</script>
<div id="osmd-container"></div>
<script>
    // Initialize OSMD with the MusicXML content
    const osmd = new opensheetmusicdisplay.OpenSheetMusicDisplay(
                            "osmd-container", {{ autoResize: true }});
    osmd.load({repr(musicxml_content)}).then(() => osmd.render());
</script>
</body>
</html>"""
        with tempfile.NamedTemporaryFile(
            prefix="amads_osmd_display_", suffix=".html", delete=False
        ) as tmp_html_file:
            tmp_html_file.write(html_content.encode("utf-8"))
            html_path = tmp_html_file.name

        if suppress_external_open():
            print(f"OSMD display suppressed during tests; wrote {html_path}")
            return

        if sys.platform == "darwin":
            subprocess.Popen(["open", html_path])
        elif sys.platform == "win32":
            os.startfile(html_path)
        else:
            subprocess.Popen(["xdg-open", html_path])

Piano Roll Display

pianoroll

Plot a score using a piano roll visualization.

Based on pianoroll in Matlab MIDI Toolbox. Original documentaion is here., page 83.

Classes

Functions

pianoroll

pianoroll(
    score: Score,
    title: str = "Piano Roll",
    y_label: str = "name",
    x_label: str = "quarter",
    color: str = "skyblue",
    accidental: str = "sharp",
    show: bool = True,
) -> Figure

Converts a Score to a piano roll display of a musical score.

Based on the pianoroll function in Matlab MIDItoolbox.

You can also create a piano roll display with set_preferred_display_method("pianoroll") and calling display_score().

Parameters:

  • score (Score) –

    The musical score to display

  • title (str, default: 'Piano Roll' ) –

    The title of the plot. Defaults to "Piano Roll".

  • y_label (str, default: 'name' ) –

    Determines whether the y-axis is labeled with note names or MIDI numbers. Valid Input: 'name' (default) or 'num'.

  • x_label (str, default: 'quarter' ) –

    Determines whether the x-axis is labeled with quarters or seconds. Valid input: 'quarter' (default) or 'sec'.

  • color (str, default: 'skyblue' ) –

    The color of the note rectangles. Defaults to 'skyblue'.

  • accidental (str, default: 'sharp' ) –

    Determines whether the y-axis is labeled with sharps or flats. Only useful if argument y_label is 'name'. Raises exception on inputs that's not 'sharp', 'flat', or 'both'. Defaults to 'sharp', which is what is done in miditoolbox. 'both' means use AMADS defaults which are C#, Eb, F#, G#, Bb.

  • show (bool, default: True ) –

    If True (default), the plot is displayed.

Returns:

  • Figure

    A matplotlib.figure.Figure of a pianoroll diagram.

Raises:

  • ValueError

    If there are invalid input arguments

Source code in amads/io/pianoroll.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
def pianoroll(
    score: Score,
    title: str = "Piano Roll",
    y_label: str = "name",
    x_label: str = "quarter",
    color: str = "skyblue",
    accidental: str = "sharp",
    show: bool = True,
) -> figure.Figure:
    """Converts a Score to a piano roll display of a musical score.

    Based on the pianoroll function in Matlab MIDItoolbox.

    You can also create a piano roll display with
    `set_preferred_display_method("pianoroll")` and calling `display_score()`.

    Parameters
    ----------
    score : Score
        The musical score to display
    title : str, optional
        The title of the plot. Defaults to "Piano Roll".
    y_label : str, optional
        Determines whether the y-axis is
        labeled with note names or MIDI numbers.
        Valid Input: 'name' (default) or 'num'.
    x_label : str, optional
        Determines whether the x-axis is labeled with quarters or
        seconds. Valid input: 'quarter' (default) or 'sec'.
    color : str, optional
        The color of the note rectangles. Defaults to 'skyblue'.
    accidental : str, optional
        Determines whether the y-axis is
        labeled with sharps or flats. Only useful if argument
        y_label is 'name'. Raises exception on inputs that's not
        'sharp', 'flat', or 'both'. Defaults to 'sharp', which is
        what is done in miditoolbox. 'both' means use AMADS defaults
        which are C#, Eb, F#, G#, Bb.
    show : bool, optional
        If True (default), the plot is displayed.

    Returns
    -------
    Figure
        A matplotlib.figure.Figure of a pianoroll diagram.

    Raises
    ------
    ValueError
        If there are invalid input arguments
    """

    # remove ties and make a sorted list of all notes:
    score = score.flatten(collapse=True)
    # Check for correct x_label input argument
    if x_label != "quarter" and x_label != "sec":
        raise ValueError("Invalid x_label type")

    # Check for correct accidental input argument
    if accidental != "sharp" and accidental != "flat" and accidental != "both":
        raise ValueError("Invalid accidental type")

    fig, ax = plt.subplots()

    min_note, max_note = 127.0, 0.0
    max_time = 1  # plot at least 1 second or beat
    # now score has one part that is all notes
    for note in cast(Part, next(score.find_all(Part))).content:
        note = cast(Note, note)
        onset_time = note.onset
        offset_time = note.offset
        pitch = note.key_num - 0.5  # to center note rectangle

        # Conditionally converts beat to sec
        if x_label == "sec" and score.units_are_quarters:
            onset_time = score.time_map.quarter_to_time(onset_time)
            offset_time = score.time_map.quarter_to_time(offset_time)
        elif x_label == "quarter" and score.units_are_seconds:
            onset_time = score.time_map.time_to_quarter(onset_time)
            offset_time = score.time_map.time_to_quarter(offset_time)
        # Stores min and max note for y_axis labeling
        if pitch < min_note:
            min_note = pitch
        if pitch > max_note:
            max_note = pitch

        # Stores max note start time + note duration for x_axis limit
        if offset_time > max_time:
            max_time = offset_time

        # Draws the note
        rect = patches.Rectangle(
            (onset_time, pitch),
            offset_time - onset_time,
            1,
            edgecolor="black",
            facecolor=color,
        )
        ax.add_patch(rect)

    # Determines correct axis labels
    if min_note == 127 and max_note == 0:  # "fake" better axes:
        min_note = 59
        max_note = 59

    midi_numbers = list(range(int(min_note), int(max_note + 2)))

    match y_label:
        case "num":
            notes = midi_numbers
            y_label = "MIDI Key (Pitch) Number"
        case "name":
            if accidental == "both":
                accidental = "default"  # for simplest_enharmonic
            notes = [
                Pitch(mn).simplest_enharmonic(accidental).name_with_octave
                for mn in midi_numbers
            ]
            y_label = "Pitch Name"
        case _:
            raise ValueError("Invalid y_label type")

    # Plots the graph
    ax.set_title(title)

    ax.set_xlabel("Quarters" if x_label == "quarter" else "Seconds")
    ax.set_ylabel(y_label)

    ax.set_yticks(midi_numbers)
    ax.set_yticklabels([str(note) for note in notes])

    ax.set_xlim(0, max_time)
    ax.set_ylim(min(midi_numbers), max(midi_numbers) + 1)

    ax.grid(True)

    if show:
        plt.show()

    return fig

Built-In Scores

example

Functions

fullpath

fullpath(example: str) -> str

Construct a full path name for an example file.

For example, fullpath("midi/sarabande.mid") returns a path to a readable file from this package. This uses importlib so that we can read files even from compressed packages (we hope).

Parameters:

  • example (str) –

    The relative path to the example file, starting from the "music" directory. For example, "midi/sarabande.mid" or "musicxml/ex2.xml".

Returns:

  • str

    The full path to the example file.

Raises:

  • FileNotFoundError

    If the example file is not found or is not readable.

Source code in amads/music/example.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
def fullpath(example: str) -> str:
    """Construct a full path name for an example file.

    For example, fullpath("midi/sarabande.mid") returns a path to a
    readable file from this package.  This uses importlib so that
    we can read files even from compressed packages (we hope).

    Parameters
    ----------
    example : str
        The relative path to the example file, starting from the "music"
        directory. For example, "midi/sarabande.mid" or "musicxml/ex2.xml".

    Returns
    -------
    str
        The full path to the example file.

    Raises
    ------
    FileNotFoundError
        If the example file is not found or is not readable.
    """

    def trim_path(full: str) -> str:
        """remove first part of path to construct valid parameter value"""
        first_part = "amads/music/"
        index = full.find(first_part)
        return full if index == -1 else full[index + len(first_part) :]

    path = str(resources.files("amads").joinpath("music/" + example))

    if os.path.isfile(path) and os.access(path, os.R_OK):
        return path

    print("In amads.example.fullpath(" + example + "):")
    print("    File was not found. Try one of these:")

    spec = util.find_spec("amads")
    if spec is None:
        print("Error: Package amads not found")
        raise FileNotFoundError("Package amads not found")
    if spec.submodule_search_locations is None:
        print("Error: Package amads has no submodule search locations")
        raise FileNotFoundError(
            "Package amads has no submodule search locations"
        )
    package_path = spec.submodule_search_locations[0]

    # Walk through the directory hierarchy
    for root, dirs, files in os.walk(package_path):
        for file in files:
            for ext in valid_score_extensions:
                if file.endswith(ext):
                    parameter_option = trim_path(os.path.join(root, file))
                    print(f'   "{parameter_option}"')
    raise FileNotFoundError("Example file not found")