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) -> 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) –

    The name of the preferred MIDI reader; "music21" or "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
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
def set_preferred_midi_reader(reader: str) -> 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
        The name of the preferred MIDI reader; "music21" or "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) -> 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) –

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

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
 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 set_preferred_xml_reader(reader: str) -> 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
        The name of the preferred XML reader. Can be "music21" or "partitura".

    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) -> 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) –

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

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
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
def set_preferred_kern_reader(reader: str) -> 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
        The name of the preferred Kern reader. Can be "music21" or "partitura".

    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) -> 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) –

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

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
140
141
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
def set_preferred_mei_reader(reader: str) -> 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
        The name of the preferred MEI reader. Can be "music21" or "partitura".

    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
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
206
207
208
209
210
211
212
213
214
215
216
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
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
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,
    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
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
def _import_score(
    filename: str,
    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 {filename} using {format} reader "
                f"file={import_fn.__name__}."
            )
        return import_fn(
            filename, format, flatten, collapse, show, group_by_instrument
        )
    else:
        raise Exception(
            "Could not find a MusicXML import function. "
            f"Preferred subsystem is {preferred_reader}"
        )

read_score

read_score(
    filename: str,
    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) –

    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
306
307
308
309
310
311
312
313
314
315
316
317
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
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
def read_score(
    filename: str,
    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
        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 filename.startswith("http") or "://" in filename:
        with tempfile.NamedTemporaryFile(
            suffix=pathlib.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:
        ext = pathlib.Path(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"
            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.

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
481
482
483
484
485
486
487
488
489
490
491
492
def last_used_reader() -> Optional[str]:
    """Return the name of the last used reader function.

    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

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) -> 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) –

    The name of the preferred MIDI writer. Can be "music21" or "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
 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
def set_preferred_midi_writer(writer: str) -> 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
        The name of the preferred MIDI writer. Can be "music21" or "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) -> 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) –

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

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
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
def set_preferred_xml_writer(writer: str) -> 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
        The name of the preferred XML writer. Can be "music21" or "partitura".

    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) -> 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) –

    The name of the preferred Kern writer. Can be "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
140
141
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
def set_preferred_kern_writer(writer: str) -> 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
        The name of the preferred Kern writer. Can be "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) -> 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) –

    The name of the preferred MEI writer. Can be "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
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
def set_preferred_mei_writer(writer: str) -> 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
        The name of the preferred MEI writer. Can be "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) -> 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) –

    The name of the preferred PDF writer. Can be "music21-lilypond", "music21-xml-lilypond", or "partitura-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
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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
def set_preferred_pdf_writer(writer: str) -> 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
        The name of the preferred PDF writer. Can be "music21-lilypond",
        "music21-xml-lilypond", or "partitura-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
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
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], 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', or 'pdf'.

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
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
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
def _check_for_subsystem(
    format: str,
) -> tuple[
    Optional[Callable[[Score, Optional[str], 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', or 'pdf'.

    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,
    }.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

_export_score

_export_score(
    score: Score,
    filename: Optional[str],
    format: str,
    show: bool = False,
    display: bool = False,
) -> None

Use Partitura or music21 to export a MusicXML file.

Author: Roger B. Dannenberg

Source code in amads/io/writescore.py
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
def _export_score(
    score: Score,
    filename: Optional[str],
    format: str,
    show: bool = False,
    display: 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, display)
    else:
        raise Exception(
            f"Could not find an export function for format {format}. "
            "Preferred subsystem is " + str(subsystem)
        )

write_score

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

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 (Optional[str]) –

    the path (relative or absolute) to the music file. Optional only when display is True, but for display, you should call display_score instead.

  • 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.

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
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
def write_score(
    score: Score,
    filename: str,
    show: bool = False,
    format: Optional[str] = None,
) -> None:
    """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 : Optional[str]
        the path (relative or absolute) to the music file. Optional only
        when display is True, but for display, you should call `display_score`
        instead.
    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`.

    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`.

    """
    _write_or_display_score(score, filename, show, format, False)

_write_or_display_score

_write_or_display_score(
    score: Score,
    filename: Optional[str],
    show: bool = False,
    format: Optional[str] = None,
    display: bool = False,
) -> None

Write or display a Score.

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').

If display is True, the goal is to display the file, so the filename is optional, and a temporary file will be used if needed.

display is suppressed by setting AMADS_NO_OPEN=1 in the environment, which is used in testing with pytest so the user does not have to close a bunch of windows opened by demos that are run as part of testing.

Author: Roger B. Dannenberg

Parameters:

  • score (Score) –

    the score to write

  • filename (Optional[str]) –

    the path (relative or absolute) to the music file. Optional only when display is True, but for display, you should call display_score instead.

  • 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.

  • display (bool, default: False ) –

    If True and format is 'pdf', the created PDF file is displayed.

Raises:

  • ValueError

    if format is unknown

Source code in amads/io/writescore.py
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
498
499
500
501
502
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
def _write_or_display_score(
    score: Score,
    filename: Optional[str],
    show: bool = False,
    format: Optional[str] = None,
    display: bool = False,
) -> None:
    """Write or display a Score.

    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').

    If display is True, the goal is to display the file, so the
    filename is optional, and a temporary file will be used if needed.

    display is suppressed by setting AMADS_NO_OPEN=1 in the environment,
    which is used in testing with pytest so the user does not have to close
    a bunch of windows opened by demos that are run as part of testing.

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

    Parameters
    ----------
    score : Score
        the score to write
    filename : Optional[str]
        the path (relative or absolute) to the music file. Optional only
        when display is True, but for display, you should call `display_score`
        instead.
    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`.
    display : bool
        If True and format is 'pdf', the created PDF file is displayed.

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

    if not display and not filename:
        raise ValueError("filename must be provided if display is False")
    if format is None and not filename:
        raise ValueError("format must be provided if filename is not provided")

    # Type checking complains about filename being possibly None, but we
    # check for that above, so we can ignore it here.
    if format is None and filename:
        ext = pathlib.Path(filename).suffix  # type: ignore
        format = _suffix_to_format.get(ext)
        if not format:
            raise ValueError(
                f"Unsupported file extension: {ext}. "
                f"Valid extensions: {valid_score_extensions}"
            )
    elif format not in _suffix_to_format.values():
        raise ValueError(f"Unknown or unspecified format: {format}")

    with warnings.catch_warnings(record=True) as w:
        warnings.simplefilter(
            "ignore" if writer_warning_level == "none" else "always"
        )

        # here, we know format is valid, so ignore type checking:
        _export_score(score, filename, format, show, display)  # 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="")

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, or you can writea musicxml file and open it with MuseScore.

You will need to install either LilyPond or MuseScore, and either Music21 or Partitura to display a score.

displayscore

functions for score display

Classes

Functions

set_preferred_display_method

set_preferred_display_method(method: str) -> 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" or "pianoroll". Note that if the method is "pdf", then io.writescore.preferred_pdf_writer is used to create a PDF to display.

Returns:

  • str

    The previous name of the preferred method.

Raises:

  • ValueError

    If an invalid method is provided.

Source code in amads/io/displayscore.py
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
def set_preferred_display_method(method: str) -> 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" or
        "pianoroll". Note that if the method is "pdf", then
        `io.writescore.preferred_pdf_writer` is used to create a PDF to display.

    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", "pianoroll"]:
        preferred_display_method = method
    else:
        raise ValueError(
            "Invalid method. Choose 'pdf', 'musescore', or 'pianoroll'."
        )
    return previous_display_method

display_score

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

Display a score.

Author: Roger B. Dannenberg

Parameters:

  • score (Score) –

    the score to write

  • show (bool, default: False ) –

    show text representation of converted score for debugging

Source code in amads/io/displayscore.py
 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
def display_score(score: Score, show: bool = False) -> None:
    """Display a score.

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

    Parameters
    ----------
    score : Score
        the score to write
    show : bool
        show text representation of converted score for debugging
    """
    if preferred_display_method == "pdf":
        _write_or_display_score(score, None, show, "pdf", display=True)
    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":
        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"MuseScore display suppressed during tests; wrote {xml_path}"
            )
            return

        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."
        )

Piano Roll Display

pianoroll

Ports pianoroll Function

Original Doc: https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=6e06906ca1ba0bf0ac8f2cb1a929f3be95eeadfa#page=82

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.

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
 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
 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
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.

    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

Low-Level Input Functions

pretty_midi_import

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

Use PrettyMIDI to import a MIDI file and convert it to a Score.

Author: Roger B. Dannenberg

Parameters:

  • filename (Union(str, PosixPath)) –

    The path to the MIDI file to import.

  • format (str) –

    The format of the MIDI file. Must be "midi".

  • flatten (bool, default: False ) –

    If True, create a flat score where notes are direct children of parts. Defaults to collapse, which defaults to False.

  • collapse (bool, default: False ) –

    If True, merge all parts into a single part. Implies flatten=True. Defaults to False.

  • show (bool, default: False ) –

    If True, print the PrettyMIDI score structure for debugging. Defaults to False.

  • group_by_instrument (bool, default: True ) –

    If True, group parts by instrument name into staffs. Defaults to True. See read_midi() for more details.

Returns:

  • Score

    The converted Score object containing the imported MIDI data.

Examples:

>>> from amads.io.pm_midi_import import pretty_midi_import
>>> from amads.music import example
>>> score = pretty_midi_import(                     example.fullpath("midi/sarabande.mid"), "midi",                    flatten=True)  # show=True to see PrettyMIDI data
Source code in amads/io/pm_midi_import.py
207
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
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
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
371
372
def pretty_midi_import(
    filename: str,
    format: str,
    flatten: bool = False,
    collapse: bool = False,
    show: bool = False,
    group_by_instrument: bool = True,
) -> Score:
    """
    Use PrettyMIDI to import a MIDI file and convert it to a Score.

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

    Parameters
    ----------
    filename : Union(str, PosixPath)
        The path to the MIDI file to import.
    format : str
        The format of the MIDI file. Must be "midi".
    flatten : bool, optional
        If True, create a flat score where notes are direct children of
        parts. Defaults to collapse, which defaults to False.
    collapse : bool, optional
        If True, merge all parts into a single part. Implies flatten=True.
        Defaults to False.
    show : bool, optional
        If True, print the PrettyMIDI score structure for debugging.
        Defaults to False.
    group_by_instrument : bool, optional
        If True, group parts by instrument name into staffs. Defaults to True.
        See read_midi() for more details.

    Returns
    -------
    Score
        The converted Score object containing the imported MIDI data.

    Examples
    --------
    >>> from amads.io.pm_midi_import import pretty_midi_import
    >>> from amads.music import example
    >>> score = pretty_midi_import( \
                    example.fullpath("midi/sarabande.mid"), "midi",\
                    flatten=True)  # show=True to see PrettyMIDI data
    """
    flatten = flatten or collapse  # collapse implies flatten

    # Load the MIDI file using PrettyMidi
    filename = str(filename)
    pmscore = PrettyMIDI(filename)
    if show:
        from amads.io.pm_show import pretty_midi_show

        pretty_midi_show(pmscore, filename)

    # Create an empty Score object
    time_map = _time_map_from_tick_scales(
        pmscore._tick_scales, pmscore.resolution
    )
    score = Score(time_map=time_map)
    score.convert_to_seconds()  # convert to seconds for PrettyMIDI

    # Iterate over instruments of the PrettyMIDI score and build parts and notes
    # Then if collapse, merge and sort the notes
    # Then if not flatten, remove each part content, and staff and measures,
    # and move notes into measures, creating ties where they cross

    # This helps orgainize parts by instrument name. Parts with the same
    # instrument name are grouped together as staffs in a single Part.
    instrument_groups: dict[str, list[Part]] = {}

    # Iterate over instruments of the PrettyMIDI score and build parts and notes
    duration = 0
    no_name_id = 1
    for ins in pmscore.instruments:
        name = ins.name
        if not ins.name:
            name = program_to_instrument_name(ins.program)
        if name == "Unknown":  # AMADS uses "Unknown" to represent None.
            # Of course, if a user really names an instrument "Unknown",
            # that particular name will not be stored in the Part.
            name = None

        part = Part(parent=score, onset=0.0, instrument=name)

        # now that we've put the name in the Part, change None to a unique name
        # so that we can make the part a named instrument group. If we are not
        # grouping by instrument, we want each part in its own group, so we
        # give each part a unique name in that case too.
        if name is None or not group_by_instrument:
            name = f"_None~@_{no_name_id}"  # something unlikely to be a name
            no_name_id += 1
        group = instrument_groups.get(name, [])
        group.append(part)
        instrument_groups[name] = group

        for note in ins.notes:
            # Create a Note object and associate it with the Part
            Note(
                parent=part,
                onset=note.start,
                duration=note.get_duration(),
                pitch=Pitch(note.pitch),
                dynamic=note.velocity,
            )
            duration = max(duration, note.end)
        assert not has_staff(
            part
        ), "Part should not have Staffs before flattening"

    score.duration = duration
    for part in score.content:
        part.duration = duration  # all parts get same max duration
        assert not has_staff(part), "Part should not have Staffs"

    # Then if collapse, merge and sort the notes
    if collapse:
        score = score.flatten(collapse=True)

    score.convert_to_quarters()  # we want to return with quarters as time unit

    # TODO: remove this block
    for groups in instrument_groups.values():
        for part in groups:
            assert not has_staff(part), "Part should not have Staffs 2"

    # Then if not flatten, remove each part content, and staff and measures,
    # and move notes into measures, creating ties where they cross. This maybe
    # does some extra work if not grouping by instrument, because we already
    # have the right set of parts, but the real work here is creating the staffs
    # and measures, and moving notes into measures with ties, so we have
    # eliminated the small extra work of copying parts when not grouping.
    if not flatten:
        score.content.clear()  # remove parts from score
        for group in instrument_groups.values():
            new_part = group[0].insert_emptycopy_into(score)
            new_part = cast(Part, new_part)
            new_part.duration = score.duration
            for old_part in group:
                assert not has_staff(
                    old_part
                ), "Part should not have Staffs before creating staffs"
                old_part = cast(Part, old_part)
                notes = old_part.content
                # now notes have part as parent, but parent does not have notes
                staff = Staff(
                    parent=new_part, onset=0.0, duration=new_part.duration
                )
                # in principle we could do this once for the first staff and
                # then copy the created staff with measures for any other
                # staff, but then we would have to save off the notes and
                # write another loop to insert each note list to a corresponding
                # staff. Besides, _create_measures might even be faster than
                # calling deepcopy on a Staff to copy the measures.
                _create_measures(staff, score.time_map, pmscore)
                notes = cast(
                    list[Note], notes
                )  # tell type checker notes is list of Note
                _add_notes_to_measures(
                    notes,
                    cast(list[Measure], staff.content),
                    pmscore.resolution,
                )
                old_part.content.clear()  # clean up

    return score

music21_import

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

Use music21 to import a file and convert it to a Score.

Parameters:

  • filename (str) –

    The path to the music file.

  • format (str) –

    The file format: 'musicxml', 'kern', 'mei'

  • flatten (bool, default: False ) –

    If True, flatten the score structure.

  • collapse (bool, default: False ) –

    If True and flatten is true, also collapse parts.

  • show (bool, default: False ) –

    If True, print the music21 score structure for debugging.

  • group_by_instrument (bool, default: True ) –

    If True, group parts by instrument name into staffs. Defaults to True. See music21_to_score() for more details.

Returns:

  • Score

    The converted AMADS Score object.

Source code in amads/io/m21_import.py
 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
def music21_import(
    filename: str,
    format: str,
    flatten: bool = False,
    collapse: bool = False,
    show: bool = False,
    group_by_instrument: bool = True,
) -> Score:
    """
    Use music21 to import a file and convert it to a Score.

    Parameters
    ----------
    filename : str
        The path to the music file.
    format: str
        The file format: 'musicxml', 'kern', 'mei'
    flatten : bool, optional
        If True, flatten the score structure.
    collapse : bool, optional
        If True and flatten is true, also collapse parts.
    show : bool, optional
        If True, print the music21 score structure for debugging.
    group_by_instrument : bool, optional
        If True, group parts by instrument name into staffs. Defaults to True.
        See music21_to_score() for more details.

    Returns
    -------
    Score
        The converted AMADS Score object.
    """
    # Load the file using music21
    if format == "kern":
        format = "humdrum"  # music21 uses "humdrum" for kern files
    # other formats are the same for both AMADS and music21
    qp = format != "midi"  # quantize non-MIDI files until we discover this is
    # a bad idea. MIDI files may have expressive timing, so we never quantize.
    m21score = converter.parse(
        filename, format=format, forceSource=True, quantizePost=qp
    )

    # m21score can be an Opus, but this is checked in music21_to_score, so we
    # can ignore the type error here:
    score = music21_to_score(
        m21score,  # type: ignore
        flatten,
        collapse,
        show,  # type: ignore
        filename,
        group_by_instrument=group_by_instrument,
    )
    return score

partitura_import

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

Use Partitura to import a MusicXML file.

Parameters:

  • filename (Path | str) –

    The path (relative or absolute) to the music file.

  • format (str) –

    The format of the file: 'musicxml', 'mei', 'kern'

  • flatten (bool = False, default: False ) –

    Returns a flat score (see amads.core.basics.Part.flatten)

  • collapse (bool = False, default: False ) –

    If flatten is True, all Notes are merged (collapsed) into a single Part in the resulting Score.

  • show (bool = False, default: False ) –

    Print a text representation of the data.

  • group_by_instrument (bool = True, default: True ) –

    This parameter is ignored by Partitura, which automatically produces parts with multiple staffs. The Partitura grouping is respected, and group_by_instrument is ignored.

Returns:

  • Score

    The imported Score

  • <small>**Author**: Roger B. Dannenberg</small>
Source code in amads/io/pt_import.py
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
615
616
617
618
619
620
621
622
623
624
625
626
627
def partitura_import(
    filename: Path | str,
    format: str,
    flatten: bool = False,
    collapse: bool = False,
    show: bool = False,
    group_by_instrument: bool = True,
) -> Score:
    """Use Partitura to import a MusicXML file.

    Parameters
    ----------
    filename : Path | str
        The path (relative or absolute) to the music file.
    format: str
        The format of the file: 'musicxml', 'mei', 'kern'
    flatten : bool = False
        Returns a flat score (see `amads.core.basics.Part.flatten`)
    collapse : bool = False
        If `flatten` is True, all Notes are merged (collapsed) into
        a single Part in the resulting Score.
    show : bool = False
        Print a text representation of the data.
    group_by_instrument : bool = True
        This parameter is ignored by Partitura, which automatically
        produces parts with multiple staffs. The Partitura grouping
        is respected, and `group_by_instrument` is ignored.

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

    <small>**Author**: Roger B. Dannenberg</small>
    """
    # Partitura.load_score claims to accept a file-like object, but
    # it has been problematic for us in the past, so we use a string filename.
    filename = str(filename)

    # maybe load_score would handle all file types, but since we know the
    # format and might want to use some format-dependent parameters, handle
    # each case separately:
    ptscore = None
    if format == "musicxml":
        ptscore = pt.load_musicxml(filename)
    elif format == "mei":
        ptscore = pt.load_mei(filename)
    elif format == "kern":
        ptscore = pt.load_kern(filename)
    assert ptscore is not None, (
        "Partitura failed to load score from " f"{filename}"
    )
    if show:
        print(f"Partitura score structure from {filename}:")
        for ptpart in ptscore:
            print(ptpart.pretty())
    score = Score()
    for ptpart in ptscore.parts:
        partitura_convert_part(ptpart, score)
    score.inherit_duration()
    if flatten or collapse:
        score = score.flatten(collapse=collapse)
    return score

Low-Level Output Functions

pretty_midi_export

pretty_midi_export(
    score: Score,
    filename: str,
    format: str,
    show: bool = False,
    display: bool = False,
) -> None

Export a Score as a standard MIDI file using PrettyMIDI library.

Parameters:

  • score (Score) –

    The Score object to export.

  • filename (str) –

    The path to the output MIDI file.

  • format (str) –

    The export format, should be "midi" for this function.

  • show (bool, default: False ) –

    Print a text representation of the data.

  • display (bool, default: False ) –

    Display the MIDI file (always False for MIDI export, included for consistency with other export functions).

Source code in amads/io/pm_midi_export.py
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
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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
def pretty_midi_export(
    score: Score,
    filename: str,
    format: str,
    show: bool = False,
    display: bool = False,
) -> None:
    """
    Export a Score as a standard MIDI file using PrettyMIDI library.

    Parameters
    ----------
    score : Score
        The Score object to export.
    filename : str
        The path to the output MIDI file.
    format : str
        The export format, should be "midi" for this function.
    show : bool
        Print a text representation of the data.
    display : bool
        Display the MIDI file (always False for MIDI export, included for
        consistency with other export functions).
    """
    global tied_to_notes  # helps to merge tied notes
    tied_to_notes = {}

    score.convert_to_seconds()
    score.merge_tied_notes()

    # 600 gives 1 ms resolution at 100 bpm
    pmscore = pm.PrettyMIDI(resolution=600)

    # Create tempo changes from TimeMap
    tm = score.time_map
    if tm is not None:
        tick_scales = []
        for i in range(len(tm.changes)):
            bpm = tm.get_tempo_at(i)
            resolution = pmscore.resolution
            # form breakpoints with units of (ticks, seconds/tick)
            tick_scale = 60.0 / (bpm * resolution)
            tick_scales.append(
                (int(tm.changes[i].quarter * resolution), tick_scale)
            )
        pmscore._tick_scales = tick_scales

    # We want to write every Part or Staff as a separate PrettyMIDI Instrument
    # Gather the EventGroups to write. Note that the reader may reconstruct
    # The Part/Staff hierarchy if all Parts have different instruments and
    # read_score is invoked with the default group_by_instrument=True.
    evgroups: list[Part | Staff] = []
    for part in score.find_all(Part):  # type: ignore
        # search Part for any Notes at the top level
        part = cast(Part, part)
        if any(isinstance(ev, Note) for ev in part.content):
            evgroups.append(part)
        # search for Staffs within Part
        for staff in part.find_all(Staff):
            staff = cast(Staff, staff)
            evgroups.append(staff)
    key_signatures = []  # Collect key signatures for pmscore

    # Create instruments and add notes
    for evgroup in evgroups:
        # determine a name for the pm.Instrument
        if isinstance(evgroup, Part):
            part: Part = evgroup
        else:
            part = evgroup.part  # type: ignore
        name = part.instrument
        program = part.get("midi_program")
        if name is not None and program is None:
            try:
                program = pm.instrument_name_to_program(name)
            except Exception:
                program = None
        if program is None:
            program = 0  # Acoustic Grand Piano as default
        instrument = pm.Instrument(
            program, name=name if name is not None else "Unknown"
        )

        add_eventgroup_to_instrument(evgroup, instrument, key_signatures)
        pmscore.instruments.append(instrument)

    # Create time signature changes
    for ts in score.time_signatures:
        # Convert onset (in quarters) to seconds using time_map
        time_in_seconds = score.time_map.quarter_to_time(ts.time)
        pm_ts = pm.TimeSignature(
            numerator=int(ts.upper),
            denominator=int(ts.lower),
            time=time_in_seconds,
        )
        pmscore.time_signature_changes.append(pm_ts)

    # Add key signatures to pmscore
    pmscore.key_signature_changes = key_signatures

    # Force pretty_midi to use custom _tick_scales before writing:
    _ = pmscore.get_end_time()

    if show:
        from amads.io.pm_show import pretty_midi_show

        pretty_midi_show(pmscore, filename)

    # Write to MIDI file
    pmscore.write(filename)
    tied_to_notes = None  # clear global variable after use

music21_export

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

Save a Score to a file in musicxml format using music21.

Author: Roger B. Dannenberg

Parameters:

  • score (Score) –

    The Score to export.

  • filename (Path | str) –

    The name or path of the file to save the MusicXML data.

  • format (str) –

    The format to export. Must be "musicxml", "midi", or "kern".

  • show (bool, default: False ) –

    If True, print the music21 score structure for debugging.

  • display (bool, default: False ) –

    If True, open the generated PDF in the default viewer. Only relevant for PDF export (see music21_xml_lilypond_export).

Source code in amads/io/m21_export.py
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
def music21_export(
    score: Score,
    filename: Path | str,
    format: str,
    show: bool = False,
    display: bool = False,
) -> None:
    """Save a Score to a file in musicxml format using music21.

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

    Parameters
    ----------
    score : Score
        The Score to export.
    filename : Path | str
        The name or path of the file to save the MusicXML data.
    format : str
        The format to export. Must be "musicxml", "midi", or "kern".
    show : bool, optional
        If True, print the music21 score structure for debugging.
    display: bool, optional
        If True, open the generated PDF in the default viewer.
        Only relevant for PDF export (see `music21_xml_lilypond_export`).

    """
    m21score = score_to_music21(score, show, filename)

    if format == "musicxml":
        # Export as text and fix part id's
        exporter = musicxml.m21ToXml.GeneralObjectExporter(m21score)
        musicxml_bytes = exporter.parse()
        musicxml_str = musicxml_bytes.decode("utf-8")
        fixed_xml = fix_part_ids(musicxml_str)

        # Finally, write the file
        with open(filename, "w") as f:
            f.write(fixed_xml)
    elif format == "midi":
        m21score.write("midi", filename)
    elif format == "kern":
        m21score.write("kern", filename)

partitura_export

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

Save a Score to a file in musicxml format using Partitura.

Author: Roger B. Dannenberg

Parameters:

  • score (Score) –

    The Score to export.

  • filename (Path | str) –

    The name or path of the file to save the MusicXML data.

  • format (str) –

    The format of the file to save. Must be "musicxml" for this function.

  • show (bool, default: False ) –

    If True, print the partitura score structure for debugging.

  • display (bool, default: False ) –

    If True, open the generated PDF in the default viewer. Only relevant for PDF export (see music21_xml_lilypond_export).

Source code in amads/io/pt_export.py
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
def partitura_export(
    score: Score,
    filename: Path | str,
    format: str,
    show: bool = False,
    display: bool = False,
) -> None:
    """Save a Score to a file in musicxml format using Partitura.

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

    Parameters
    ----------
    score : Score
        The Score to export.
    filename : Path | str
        The name or path of the file to save the MusicXML data.
    format : str
        The format of the file to save. Must be "musicxml" for this function.
    show : bool, optional
        If True, print the partitura score structure for debugging.
    display : bool, optional
        If True, open the generated PDF in the default viewer.
        Only relevant for PDF export (see `music21_xml_lilypond_export`).
    """
    ptscore = score_to_partitura(score, filename, show)
    assert (
        format == "musicxml"
    ), f"Unsupported format for partitura export: {format}"
    save_musicxml(ptscore, filename)

music21_pdf_export

music21_pdf_export(
    score: Score,
    filename: str,
    show: bool = False,
    lilypond: bool = False,
    display: bool = False,
) -> None

Save a Score to a file in PDF format using music21 and LilyPond.

There are three main modes of operation, controlled by the lilypond and display flags:

  • To save a PDF file without opening it, set lilypond=False and provide a filename ending in .pdf.
  • To save a LilyPond file without converting to PDF, set lilypond=True and provide a filename ending in .ly.
  • To save and display a PDF file, set display=True and optionally provide a filename ending in .pdf.

Temporary files are created as needed for intermediate LilyPond and PDF output.

Author: Roger B. Dannenberg

Parameters:

  • score (Score) –

    The Score to export.

  • filename (str) –

    The name of the file to save the LilyPond or PDF data. Must be provided if display is False, and must end if .ly if lilypond is True, or .pdf if lilypond is False. If display is True, filename is optional and names the .pdf file to be written. In all cases, a LilyPond (.ly) file is written, either to filename or to a temporary directory when display is True or lilypond is False.

  • show (bool, default: False ) –

    If True, print the music21 score structure for debugging.

  • lilypond (bool, default: False ) –

    If True, also save the intermediate LilyPond file rather than PDF.

  • display (bool, default: False ) –

    If True, open the generated PDF in the default viewer.

Raises:

  • ValueError

    If filename is not provided when display is False, or if filename extension is not the expected .pdf or .ly.

Source code in amads/io/m21_pdf_export.py
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
def music21_pdf_export(
    score: Score,
    filename: str,
    show: bool = False,
    lilypond: bool = False,
    display: bool = False,
) -> None:
    """Save a Score to a file in PDF format using music21 and LilyPond.

    There are three main modes of operation, controlled by the `lilypond`
    and `display` flags:

    - To save a PDF file without opening it, set `lilypond=False` and provide a
      `filename` ending in `.pdf`.
    - To save a LilyPond file without converting to PDF, set `lilypond=True`
      and provide a `filename` ending in `.ly`.
    - To save and display a PDF file, set `display=True` and *optionally*
      provide a `filename` ending in `.pdf`.

    Temporary files are created as needed for intermediate LilyPond and PDF
    output.

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

    Parameters
    ----------
    score : Score
        The Score to export.
    filename : str
        The name of the file to save the LilyPond or PDF data. Must be
        provided if `display` is False, and must end if `.ly` if `lilypond`
        is True, or `.pdf` if `lilypond` is False. If `display` is True,
        filename is optional and names the .pdf file to be written. In all
        cases, a LilyPond (.ly) file is written, either to `filename` or to
        a temporary directory when `display` is True or `lilypond` is False.
    show : bool, optional
        If True, print the music21 score structure for debugging.
    lilypond : bool, optional
        If True, also save the intermediate LilyPond file rather than PDF.
    display : bool, optional
        If True, open the generated PDF in the default viewer.

    Raises
    ------
    ValueError
        If `filename` is not provided when `display` is False, or if
        `filename` extension is not the expected `.pdf` or `.ly`.
    """
    ly_path: Path | None = None
    pdf_path: Path | None = None
    ly_path, pdf_path, _ = lilypond_path_help(
        filename, lilypond, display, False
    )

    m21score = score_to_music21(score, show, filename)
    _write_lilypond_file(m21score, ly_path)
    if lilypond:
        return

    lilypond_to_pdf(ly_path, pdf_path, display)  # type: ignore

music21_xml_pdf_export

music21_xml_pdf_export(
    score: Score,
    filename: str,
    show: bool = False,
    lilypond: bool = False,
    display: bool = False,
) -> None

Write Score as PDF file using music21, musicxml2ly and LilyPond.

There are three main modes of operation, controlled by the lilypond and display flags:

  • To save a PDF file without opening it, set lilypond=False and provide a filename ending in .pdf.
  • To save a LilyPond file without converting to PDF, set lilypond=True and provide a filename ending in .ly.
  • To save and display a PDF file, set display=True and optionally provide a filename ending in .pdf.

Temporary files are created as needed for intermediate XML, LilyPond, and PDF output.

Author: Roger B. Dannenberg

Parameters:

  • score (Score) –

    The Score to export.

  • filename (str) –

    The name of the file to save the LilyPond or PDF data. Must be provided if display is False, and must end if .ly if lilypond is True, or .pdf if lilypond is False. If display is True, filename is optional and names the .pdf file to be written. In all cases, a LilyPond (.ly) file is written, either to filename or to a temporary directory when display is True or lilypond is False.

  • show (bool, default: False ) –

    If True, print the music21 score structure for debugging.

  • lilypond (bool, default: False ) –

    If True, also save the intermediate LilyPond file rather than PDF.

  • display (bool, default: False ) –

    If True, open the generated PDF in the default viewer.

Raises:

  • ValueError

    If filename is not provided when display is False, or if filename extension is not the expected .pdf or .ly, or if lilypond2ly or lilypond executables are not found or fail.

Source code in amads/io/m21_pdf_export.py
 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
def music21_xml_pdf_export(
    score: Score,
    filename: str,
    show: bool = False,
    lilypond: bool = False,
    display: bool = False,
) -> None:
    """Write Score as PDF file using music21, musicxml2ly and LilyPond.

    There are three main modes of operation, controlled by the `lilypond`
    and `display` flags:

    - To save a PDF file without opening it, set `lilypond=False` and provide a
      `filename` ending in `.pdf`.
    - To save a LilyPond file without converting to PDF, set `lilypond=True`
      and provide a `filename` ending in `.ly`.
    - To save and display a PDF file, set `display=True` and *optionally*
      provide a `filename` ending in `.pdf`.

    Temporary files are created as needed for intermediate XML, LilyPond,
    and PDF output.

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

    Parameters
    ----------
    score : Score
        The Score to export.
    filename : str
        The name of the file to save the LilyPond or PDF data. Must be
        provided if `display` is False, and must end if `.ly` if `lilypond`
        is True, or `.pdf` if `lilypond` is False. If `display` is True,
        filename is optional and names the .pdf file to be written. In all
        cases, a LilyPond (.ly) file is written, either to `filename` or to
        a temporary directory when `display` is True or `lilypond` is False.
    show : bool, optional
        If True, print the music21 score structure for debugging.
    lilypond : bool, optional
        If True, also save the intermediate LilyPond file rather than PDF.
    display : bool, optional
        If True, open the generated PDF in the default viewer.

    Raises
    ------
    ValueError
        If `filename` is not provided when `display` is False, or if
        `filename` extension is not the expected `.pdf` or `.ly`, or if
        lilypond2ly or lilypond executables are not found or fail.
    """
    ly_path: Path
    pdf_path: Path | None
    xml_path: Path | None
    ly_path, pdf_path, xml_path = lilypond_path_help(
        filename, lilypond, display, True
    )
    # xml_path is not None here
    music21_export(score, xml_path, "musicxml", show, display)  # type: ignore
    musicxml_to_lilypond(xml_path, ly_path)  # type: ignore (xml_path != None)
    if lilypond:
        return
    # pdf_path is not None here because lilypond is False
    lilypond_to_pdf(ly_path, pdf_path, display)  # type: ignore

partitura_xml_pdf_export

partitura_xml_pdf_export(
    score: Score,
    filename: Path | str,
    show: bool = False,
    lilypond: bool = False,
    display: bool = False,
) -> None

Save a Score to a file in PDF format using Partitura and LilyPond.

There are three main modes of operation, controlled by the lilypond and display flags:

  • To save a PDF file without opening it, set lilypond=False and provide a filename ending in .pdf.
  • To save a LilyPond file without converting to PDF, set lilypond=True and provide a filename ending in .ly.
  • To save and display a PDF file, set display=True and optionally provide a filename ending in .pdf.

Temporary files are created as needed for intermediate XML, LilyPond, and PDF output.

Author: Roger B. Dannenberg

Parameters:

  • score (Score) –

    The Score to export.

  • filename (Path | str) –

    The name or path of the file to save the LilyPond or PDF data. Must be provided if display is False, and must end if .ly if lilypond is True, or .pdf if lilypond is False. If display is True, filename is optional and names the .pdf file to be written. In all cases, a LilyPond (.ly) file is written, either to filename or to a temporary directory when display is True or lilypond is False.

  • show (bool, default: False ) –

    If True, print the partitura score structure for debugging.

  • lilypond (bool, default: False ) –

    If True, also save the intermediate LilyPond file rather than PDF.

  • display (bool, default: False ) –

    If True, open the generated PDF in the default viewer.

Raises:

  • ValueError

    If filename is not provided when display is False, or if filename extension is not the expected .pdf or .ly.

Source code in amads/io/pt_pdf_export.py
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
69
70
71
72
73
74
75
def partitura_xml_pdf_export(
    score: Score,
    filename: Path | str,
    show: bool = False,
    lilypond: bool = False,
    display: bool = False,
) -> None:
    """Save a Score to a file in PDF format using Partitura and LilyPond.

    There are three main modes of operation, controlled by the `lilypond`
    and `display` flags:

    - To save a PDF file without opening it, set `lilypond=False` and provide a
      `filename` ending in `.pdf`.
    - To save a LilyPond file without converting to PDF, set `lilypond=True`
      and provide a `filename` ending in `.ly`.
    - To save and display a PDF file, set `display=True` and *optionally*
      provide a `filename` ending in `.pdf`.

    Temporary files are created as needed for intermediate XML, LilyPond,
    and PDF output.

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

    Parameters
    ----------
    score : Score
        The Score to export.
    filename : Path | str
        The name or path of the file to save the LilyPond or PDF data. Must be
        provided if `display` is False, and must end if `.ly` if `lilypond`
        is True, or `.pdf` if `lilypond` is False. If `display` is True,
        filename is optional and names the .pdf file to be written. In all
        cases, a LilyPond (.ly) file is written, either to `filename` or to
        a temporary directory when `display` is True or `lilypond` is False.
    show : bool, optional
        If True, print the partitura score structure for debugging.
    lilypond : bool, optional
        If True, also save the intermediate LilyPond file rather than PDF.
    display : bool, optional
        If True, open the generated PDF in the default viewer.

    Raises
    ------
    ValueError
        If `filename` is not provided when `display` is False, or if
        `filename` extension is not the expected `.pdf` or `.ly`.
    """
    ly_path: Path
    pdf_path: Path | None
    xml_path: Path | None
    ly_path, pdf_path, xml_path = lilypond_path_help(
        filename, lilypond, display, True
    )

    # xml_path is not None here because lilypond_path_help had xml=True
    partitura_export(score, xml_path, "musicxml", show)  # type: ignore
    musicxml_to_lilypond(xml_path, ly_path)  # type: ignore
    if lilypond:
        return
    # pdf_path is not None here because lilypond is False
    lilypond_to_pdf(ly_path, pdf_path, display)  # type: ignore

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")