Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ version: 2
build:
os: ubuntu-22.04
tools:
python: "3.11"
python: "3.13"

# Build documentation in the "docs/" directory with Sphinx
sphinx:
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ https://tinyurl.com/m21colab (runs music21 v7)
And to install, see:
https://www.music21.org/music21docs/usersGuide/usersGuide_01_installing.html

`Music21` runs on Python 3.10+. (Use version 4 on Python 2 or Py3.4, version 5
on Py3.5, version 6 on Py3.6, version 7 on Py3.7, version 8 on Py3.8/Py3.9)
`Music21` runs on Python 3.11+. (Use music21 version 4 on Python 2 or Py3.4, v5
on Py3.5, v6 on Py3.6, v7 on Py3.7, v8 on Py3.8/Py3.9, and v9 on Py3.10)

Released under the BSD (3-clause) license. See LICENSE.
Externally provided software (including the MIT-licensed Lilypond/MusicXML test Suite) and
Expand Down
2 changes: 1 addition & 1 deletion documentation/source/installing/installLinux.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ or terminal and enter the following command-line argument (where "$" is the prom

it should display something like:

Python 3.10.7
Python 3.13.2

if so, you're okay. If not, upgrade your version of Python. This is
often a problem on some AWS configuations, Google Colab, etc.
Expand Down
2 changes: 1 addition & 1 deletion documentation/source/installing/installMac.rst
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ it should display in Terminal something like the following:
.. image:: images/macScreenPythonVersion.*
:width: 650

If it says 3.8 or higher (or possibly a number like 3.10.7), you're okay.
If it says 3.11 or higher (or possibly a number like 3.11.7), you're okay.
If it says 2.7 or 3.4 or something,
go to https://www.python.org/downloads/
and download a newer version. Multiple versions of Python can exist
Expand Down
4 changes: 2 additions & 2 deletions documentation/source/installing/installWindows.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ is written in and in which you will write your own programs that
use `music21`.

Windows users should download and install Python version
3.10 or higher.
3.11 or higher.

To get Python for Windows, go to https://www.python.org/downloads/
and click on the "Windows installer" link. It is probably the
Expand All @@ -37,7 +37,7 @@ typing in "IDLE" or (on Windows Vista and newer) typing
in "IDLE" in the Search Programs list.

The first lines of text displayed will include a version number.
Make sure it begins with 3.10 or higher.
Make sure it begins with 3.11 or higher.

If your version is too old, download a newer version as above.

Expand Down
10 changes: 5 additions & 5 deletions music21/common/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@
# ------------------------------------------------------------------------------
from __future__ import annotations

from enum import Enum, EnumMeta, IntEnum
from enum import Enum, EnumType, IntEnum
import re

# When Python 3.11 is minimum, import EnumType instead of EnumMeta

# when Python 3.12 is minimum, will not need StrEnumMeta at all -- contains will work.

class StrEnumMeta(EnumMeta):
class StrEnumMeta(EnumType):
def __contains__(cls, item):
if isinstance(item, str):
if item in cls.__members__.values():
Expand All @@ -30,9 +28,11 @@ def __contains__(cls, item):
return False


class ContainsMeta(EnumMeta):
class ContainsMeta(EnumType):
'''
This is a backport of the Python 3.12 `EnumType` class's contains method.

This will be removed when Python 3.12 is the minimum version for music21.
'''
def __contains__(cls, item):
try:
Expand Down
2 changes: 1 addition & 1 deletion music21/converter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ def status(self) -> tuple[pathlib.Path, bool, pathlib.Path|None]:
>>> pickFilter = converter.PickleFilter(fp)
>>> #_DOCS_SHOW pickFilter.status()
(PosixPath('/Users/Cuthbert/Desktop/musicFile.mxl'), True,
PosixPath('/tmp/music21/m21-7.0.0-py3.9-18b8c5a5f07826bd67ea0f20462f0b8d.p.gz'))
PosixPath('/tmp/music21/m21-10.0.0-py3.13-18b8c5a5f07826bd67ea0f20462f0b8d.p.gz'))
'''
fpScratch = environLocal.getRootTempDir()
m21Format = common.findFormatFile(self.fp)
Expand Down
4 changes: 2 additions & 2 deletions music21/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ def __str__(self):
msg = []
if self.partId is not None:
msg.append(f'{self.partId}: ')
if self.partName is not None:
if self.partName is not None and self.partName != self.instrumentName:
msg.append(f'{self.partName}: ')
if self.instrumentName is not None:
msg.append(self.instrumentName)
Expand Down Expand Up @@ -1867,7 +1867,7 @@ def deduplicate(s: stream.Stream, inPlace: bool = False) -> stream.Stream:
[<music21.instrument.Flute 'Flute'>, <music21.instrument.Flute 'Flute'>]
>>> s2 = instrument.deduplicate(s2, inPlace=True)
>>> list(p1.getInstruments())
[<music21.instrument.Piccolo 'Piccolo: Piccolo'>]
[<music21.instrument.Piccolo 'Piccolo'>]
>>> list(p2.getInstruments())
[<music21.instrument.Flute 'Flute'>]
'''
Expand Down
8 changes: 4 additions & 4 deletions music21/mei/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1561,9 +1561,9 @@ def scoreDefFromElement(elem, slurBundle=None):
>>> len(result)
5
>>> result['1']
{'instrument': <music21.instrument.Clarinet '1: Clarinet: Clarinet'>}
{'instrument': <music21.instrument.Clarinet '1: Clarinet'>}
>>> result['3']
{'instrument': <music21.instrument.Violin '3: Violin: Violin'>}
{'instrument': <music21.instrument.Violin '3: Violin'>}
>>> result['all-part objects']
[<music21.meter.TimeSignature 3/4>]
>>> result['whole-score objects']
Expand Down Expand Up @@ -1727,7 +1727,7 @@ def staffDefFromElement(elem, slurBundle=None):
>>> len(result)
1
>>> result
{'instrument': <music21.instrument.Clarinet '1: Clarinet: Clarinet'>}
{'instrument': <music21.instrument.Clarinet '1: Clarinet'>}
>>> result['instrument'].partId
'1'
>>> result['instrument'].partName
Expand All @@ -1747,7 +1747,7 @@ def staffDefFromElement(elem, slurBundle=None):
>>> len(result)
3
>>> result['instrument']
<music21.instrument.Tuba '2: Tuba: Tuba'>
<music21.instrument.Tuba '2: Tuba'>
>>> result['clef']
<music21.clef.BassClef>
>>> result['key']
Expand Down
5 changes: 1 addition & 4 deletions music21/metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1719,11 +1719,8 @@ def bestTitle(self) -> str|None:

>>> md.bestTitle = 'Bonaparte'
Traceback (most recent call last):
AttributeError: ...'bestTitle'...
AttributeError: property 'bestTitle' of 'Metadata' object has no setter
'''
# TODO: once Py3.11 is the minimum, change doctest output to:
# AttributeError: property 'bestTitle' of 'Metadata' object has no setter

searchId = (
'title',
'popularTitle',
Expand Down
7 changes: 0 additions & 7 deletions music21/meter/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1439,13 +1439,6 @@ def _getFlatList(self):
mtList += obj._getFlatList()
return mtList

@property
def flat(self):
'''
deprecated. Call .flatten() instead. To be removed in v11.
'''
return self.flatten()

def flatten(self) -> MeterSequence:
'''
Return a new MeterSequence composed of the flattened representation.
Expand Down
33 changes: 26 additions & 7 deletions music21/midi/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ def _reprInternal(self) -> str:
if self.time != 0:
r += f't={self.time!r}, '
r += f'track={trackIndex}'
if self.type in ChannelVoiceMessages or self.type in ChannelModeMessages:
if self.isChannelEvent():
r += f', channel={self.channel!r}'
if self.type in (ChannelVoiceMessages.NOTE_ON, ChannelVoiceMessages.NOTE_OFF):
attrList = ['pitch', 'velocity']
Expand Down Expand Up @@ -626,6 +626,18 @@ def data(self, value: int|str|bytes|bool|None):
value = bytes([int(value)])
self.parameter1 = value

def isChannelEvent(self) -> bool:
'''
returns bool if the channel matters for this event

>>> me = midi.MidiEvent(type=midi.ChannelVoiceMessages.NOTE_ON, channel=4)
>>> me.isChannelEvent()
True
>>> dt = midi.DeltaTime()
>>> dt.isChannelEvent()
False
'''
return self.type in ChannelVoiceMessages or self.type in ChannelModeMessages

def setPitchBend(self, cents: int|float, bendRange=2) -> None:
'''
Expand Down Expand Up @@ -1220,7 +1232,7 @@ class DeltaTime(MidiEvent):

Time values are in integers, representing ticks.

The `channel` attribute, inherited from MidiEvent is not used and set to None
The `channel` attribute, inherited from MidiEvent is not used and set to 1
unless overridden (it does not do anything if set and is not written out to MIDI).

>>> mt = midi.MidiTrack(1)
Expand Down Expand Up @@ -1248,6 +1260,12 @@ def _reprInternal(self) -> str:
rep = '(empty) ' + rep
return rep

def isChannelEvent(self) -> bool:
'''
Save processing time -- is always False
'''
return False

def readUntilLowByte(self, oldBytes: bytes) -> tuple[int, bytes]:
r'''
Read a byte-string until hitting a character below 0x80
Expand Down Expand Up @@ -1607,22 +1625,23 @@ def setChannel(self, value: int) -> None:

def getChannels(self) -> list[int]:
'''
Get all channels (excluding None) used in this Track (sorted)
Get all channels used in this Track (sorted) excepting events like DeltaTime
or end events where the channel does not matter

>>> mt = midi.MidiTrack(index=2)
>>> noteOn = midi.MidiEvent(type=midi.ChannelVoiceMessages.NOTE_ON, channel=14)
>>> noteOn2 = midi.MidiEvent(type=midi.ChannelVoiceMessages.NOTE_ON, channel=5)
>>> dt = midi.DeltaTime(1000, channel=2) # channel does not matter
>>> noteOn3 = midi.MidiEvent(type=midi.ChannelVoiceMessages.NOTE_ON, channel=14)
>>> noteOn4 = midi.MidiEvent(type=midi.ChannelVoiceMessages.PROGRAM_CHANGE, channel=None)

>>> mt.events = [noteOn, noteOn2, noteOn3, noteOn4]
>>> noteOn4 = midi.MidiEvent(type=midi.ChannelVoiceMessages.PROGRAM_CHANGE, channel=5)

>>> mt.events = [noteOn, noteOn2, dt, noteOn3, noteOn4]
>>> mt.getChannels()
[5, 14]
'''
post: set[int] = set()
for e in self.events:
if e.channel is not None:
if e.isChannelEvent():
post.add(e.channel)
return sorted(post)

Expand Down
10 changes: 5 additions & 5 deletions music21/midi/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
conductorStream,
getMetaEvents,
midiAsciiStringToBinaryString,
midiEventsToInstrument,
midiEventToInstrument,
midiEventsToNote,
midiFileToStream,
noteToMidiEvents,
Expand Down Expand Up @@ -1391,12 +1391,12 @@ def testNullTerminatedInstrumentName(self):
'''
event = MidiEvent()
event.data = bytes('Piccolo\x00', 'utf-8')
i = midiEventsToInstrument(event)
i = midiEventToInstrument(event)
self.assertIsInstance(i, instrument.Piccolo)

# test that nothing was broken.
event.data = bytes('Flute', 'utf-8')
i = midiEventsToInstrument(event)
i = midiEventToInstrument(event)
self.assertIsInstance(i, instrument.Flute)

def testLousyInstrumentData(self):
Expand All @@ -1406,7 +1406,7 @@ def testLousyInstrumentData(self):
event = MidiEvent()
event.data = bytes(name, 'utf-8')
event.type = MetaEvents.INSTRUMENT_NAME
i = midiEventsToInstrument(event)
i = midiEventToInstrument(event)
self.assertIsNone(i.instrumentName)

# lousy program change
Expand All @@ -1416,7 +1416,7 @@ def testLousyInstrumentData(self):
event.channel = 10
event.type = ChannelVoiceMessages.PROGRAM_CHANGE

i = midiEventsToInstrument(event)
i = midiEventToInstrument(event)
self.assertIsInstance(i, instrument.UnpitchedPercussion)

def testConductorStream(self):
Expand Down
Loading