From edf5d8c8e9e44eb5c6571c79719722b2b175c71e Mon Sep 17 00:00:00 2001 From: Ennis Massey Date: Mon, 25 Sep 2017 10:49:17 +1300 Subject: [PATCH] Finished base implementation of MIDI Meta Messages --- midisnake/errors.py | 10 +- midisnake/events.py | 33 +++++-- midisnake/meta_events.py | 192 +++++++++++++++++++++++++++++++++++---- midisnake/parser.py | 1 - midisnake/structure.py | 13 ++- tests/test_events.py | 18 ++-- 6 files changed, 225 insertions(+), 42 deletions(-) diff --git a/midisnake/errors.py b/midisnake/errors.py index a2376c7..ab6c7a2 100644 --- a/midisnake/errors.py +++ b/midisnake/errors.py @@ -20,6 +20,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -class LengthError(Exception): +class EventLengthError(Exception): """Raised when data is the wrong length""" - pass \ No newline at end of file + pass + +class EventNullLengthError(Exception): + """Raised when a length is zero when it should never be so""" + +class EventTextError(Exception): + """Raised when an event contains unparseable text""" \ No newline at end of file diff --git a/midisnake/events.py b/midisnake/events.py index a5aa75d..d7d13fb 100644 --- a/midisnake/events.py +++ b/midisnake/events.py @@ -307,17 +307,32 @@ def _process(self, data: int): class MetaFactory: - pass + def __new__(cls, midi_file: Union[FileIO, BufferedReader]) -> Union[MetaEventType, None]: + # If the event is a Sequencer Specific one, ignore it and consume the associated bytes + meta_variant_bytes = midi_file.read(1) + meta_variant = int.from_bytes(meta_variant_bytes, 'big') + if meta_variant == 0x7F: + length_of_event = VariableLengthValue(midi_file) + _ = midi_file.read(length_of_event) + return None + variant_function = meta_events[meta_variant].function + variant_output = variant_function(midi_file) + variant_obj_type = meta_events[meta_variant].object_type -event_info = data.read(16) # type: bytes -event_array = bytearray(event_info) -self.variant_number = event_array[1] -# Attempt to call matching entry in meta_events -try: - meta_data = meta_events[self.variant_number](data) -except KeyError: - raise ValueError("Invalid or unsupported MIDI meta event. Event code was {0:x}".format(self.variant_number)) + return variant_obj_type.__new__(variant_obj_type, variant_output) + + + + +# event_info = data.read(16) # type: bytes +# event_array = bytearray(event_info) +# self.variant_number = event_array[1] +# # Attempt to call matching entry in meta_events +# try: +# meta_data = meta_events[self.variant_number](data) +# except KeyError: +# raise ValueError("Invalid or unsupported MIDI meta event. Event code was {0:x}".format(self.variant_number)) diff --git a/midisnake/meta_events.py b/midisnake/meta_events.py index d003969..67876db 100644 --- a/midisnake/meta_events.py +++ b/midisnake/meta_events.py @@ -28,6 +28,7 @@ from typing import Union, Tuple, NamedTuple, Callable, Any from midisnake.structure import VariableLengthValue +from midisnake.errors import EventLengthError, EventNullLengthError, EventTextError SMPTE_Format = NamedTuple("SMPTE_Format", [ @@ -40,7 +41,6 @@ ) # type: Union[Callable, NamedTuple] - class MetaTextEvent: variant_number = None # type: int variant_name = None # type: str @@ -62,16 +62,115 @@ def __init__(self, event_info: bytes, variant: int, data: Tuple[int, str, bytear self.text = data[1] +class MetaSequenceNumber: + sequence_number = None # type: int + + length = None # type: int + raw_content = None # type: bytearray + + def __init__(self, data: Tuple[int, int, bytearray]): + self.length, self.sequence_number, self.raw_content = data + + +class MetaKeySignature: + signature_index = None # type: int + signature_name = None # type: str + + major_minor = None # type: bool + + length = None # type: int + raw_content = None # type: bytearray + + def __init__(self, data: Tuple[int, Tuple[int, int], bytearray]): + self.raw_content = data[2] + self.length = data[0] + + signature_names = ["Cb", "Fb", "Db", "Ab", "Eb", "Bb", "F", "C", "G", "D", "A", "E", "B", "F#", "C#"] + + if data[1][0] not in range(-7, 8): + raise ValueError("Invalid Key Signature value") + self.signature_index = data[1][0] + self.signature_name = signature_names[self.signature_index + 7] + + +class MetaTimeSignature: + numerator = None # type: int + denominator = None # type: int + + clocks_per_tick = None # type: int + tsnotes_per_qnote = None # type: int + + length = None # type: int + raw_content = None # type: bytearray + + def __init__(self, data: Tuple[int, Tuple[int, int, int, int], bytearray]): + self.raw_content = data[2] + self.length = data[0] + self.numerator = data[1][0] + self.denominator = data[1][1] + self.clocks_per_tick = data[1][2] + self.tsnotes_per_qnote = data[1][3] + @property + def parsed_signature(self) -> str: + actual_denominator = 2 ** self.denominator + return "{}/{}".format(self.numerator, actual_denominator) + + +class MetaSMPTEOffset: + hours = None # type: int + minutes = None # type: int + seconds = None # type: int + fps = None # type: int + fractional_frames = None # type: int + + length = None # type: int + raw_content = None # type: bytearray + + def __init__(self, data: Tuple[int, Tuple[int, int, int, int, int], bytearray]): + self.hours, self.minutes, self.seconds, self.fps, self.ff = data[1] + + self.length = data[0] + self.raw_content = data[1] + + +class MetaSetTempo: + tpqm = None # type: int + + length = None # type: int + raw_content = None # type: bytearray + + def __init__(self, data: Tuple[int, int, bytearray]): + self.length, self.tpqm, self.raw_content = data + + def get_tempo(self): + return self.tpqm / 60000000.0 + + +class MetaChannelPrefix: + prefix = None # type: int + + length = None # type: int + raw_content = None # type: bytearray + + def __init__(self, data: Tuple[int, int, bytearray]): + self.length, self.prefix, self.raw_content = data + + +class EndOfTrack: + length = None # type: int + + def __init__(self, data: Tuple[int, None, None]): + self.length = data[0] def sequence_number(data: Union[FileIO, BufferedReader]) -> Tuple[int, int, bytearray]: length_bytes = bytearray(data.read(4)) length = int.from_bytes(length_bytes, "big") if length != 2: - raise ValueError("Sequence Number length was incorrect. It should be 2, but it was {}".format(length)) + raise EventLengthError("Sequence Number length was incorrect. It should be 2, but it was {}".format(length)) sequence_num_raw = bytearray(data.read(2)) sequence_num = int.from_bytes(sequence_num_raw, "big") return length, sequence_num, sequence_num_raw @@ -83,7 +182,7 @@ def text_event(data: Union[FileIO, BufferedReader]) -> Tuple[int, str, bytearray try: text = raw_data.decode("ASCII") except UnicodeDecodeError as exc: - raise ValueError("Unparsable text in text event") from exc + raise EventTextError("Unparsable text in text event") from exc return length, text, raw_data @@ -94,7 +193,7 @@ def copyright_notice(data: Union[FileIO, BufferedReader]) -> Tuple[int, str, byt try: text = raw_data.decode("ASCII") except UnicodeDecodeError as exc: - raise ValueError("Unparsable text in copyright notice") from exc + raise EventTextError("Unparsable text in copyright notice") from exc return length, text, raw_data @@ -105,7 +204,7 @@ def chunk_name(data: Union[FileIO, BufferedReader]) -> Tuple[int, str, bytearray try: text = raw_data.decode("ASCII") except UnicodeDecodeError as exc: - raise ValueError("Unparsable text in track/sequence name") from exc + raise EventTextError("Unparsable text in track/sequence name") from exc return length, text, raw_data @@ -116,7 +215,7 @@ def instrument_name(data: Union[FileIO, BufferedReader]) -> Tuple[int, str, byte try: text = raw_data.decode("ASCII") except UnicodeDecodeError as exc: - raise ValueError("Unparsable text in instrument name") from exc + raise EventTextError("Unparsable text in instrument name") from exc return length, text, raw_data @@ -127,7 +226,7 @@ def lyric(data: Union[FileIO, BufferedReader]) -> Tuple[int, str, bytearray]: try: text = raw_data.decode("ASCII") except UnicodeDecodeError as exc: - raise ValueError("Unparseable text in lyric text") from exc + raise EventTextError("Unparseable text in lyric text") from exc return length, text, raw_data @@ -138,7 +237,7 @@ def marker(data: Union[FileIO, BufferedReader]) -> Tuple[int, str, bytearray]: try: text = raw_data.decode("ASCII") except UnicodeDecodeError as exc: - raise ValueError("Unparseable text in marker text") from exc + raise EventTextError("Unparseable text in marker text") from exc return length, text, raw_data @@ -149,7 +248,7 @@ def cue_point(data: Union[FileIO, BufferedReader]) -> Tuple[int, str, bytearray] try: text = raw_data.decode("ASCII") except UnicodeDecodeError as exc: - raise ValueError("Unparseable text in Cue Point text") from exc + raise EventTextError("Unparseable text in Cue Point text") from exc return length, text, raw_data @@ -158,7 +257,7 @@ def channel_prefix(data: Union[FileIO, BufferedReader]) -> Tuple[int, int, bytea length_bytes = data.read(4) length = int.from_bytes(length_bytes, "big") if length != 0x01: - raise ValueError("Channel Prefix length invalid. It should be 1, but it's {}".format(length)) + raise EventLengthError("Channel Prefix length invalid. It should be 1, but it's {}".format(length)) prefix_raw = bytearray(data.read(1)) prefix = int.from_bytes(prefix_raw, "big") @@ -169,7 +268,7 @@ def end_of_track(data: Union[FileIO, BufferedReader]) -> Tuple[int, None, None]: length_bytes = data.read(4) length = int.from_bytes(length_bytes, "big") if length != 0: - raise ValueError("End of Track event with non-zero length") + raise EventLengthError("End of Track event with non-zero length") return length, None, None @@ -177,7 +276,7 @@ def set_tempo(data: Union[FileIO, BufferedReader]) -> Tuple[int, int, bytearray] length_bytes = data.read(4) length = int.from_bytes(length_bytes, "big") if length != 3: - raise ValueError("Set Tempo event with length other than 3. Given length was {}".format(length)) + raise EventLengthError("Set Tempo event with length other than 3. Given length was {}".format(length)) raw_data = bytearray(data.read(3)) tpqm = int.from_bytes(raw_data, "big") @@ -188,7 +287,7 @@ def smpte_offset(data: Union[FileIO, BufferedReader]) -> Tuple[int, Tuple[int, i length_bytes = data.read(4) length = int.from_bytes(length_bytes, "big") if length != 0x05: - raise ValueError("SMPTE Offset length is not 5. Given value was {}".format(length)) + raise EventLengthError("SMPTE Offset length is not 5. Given value was {}".format(length)) # Process Hours hour_data = bytearray(data.read(8)) @@ -238,15 +337,15 @@ def time_signature(data: Union[FileIO, BufferedReader]) -> Tuple[int, Tuple[int, length = int.from_bytes(length_bytes, "big") if length != 0x04: - raise ValueError("Time Signature event has invalid length. Should be 4, value was {}".format(length)) + raise EventLengthError("Time Signature event has invalid length. Should be 4, value was {}".format(length)) data_bytes = bytearray(data.read(4)) # type: bytearray - nominator = data_bytes[0] # type: int + numerator = data_bytes[0] # type: int denominator = data_bytes[1] # type: int clock_num = data_bytes[2] ts_number = data_bytes[3] - return length, (nominator, denominator, clock_num, ts_number), data_bytes + return length, (numerator, denominator, clock_num, ts_number), data_bytes def key_signature(data: Union[FileIO, BufferedReader]) -> Tuple[int, Tuple[int, int], bytearray]: @@ -254,10 +353,69 @@ def key_signature(data: Union[FileIO, BufferedReader]) -> Tuple[int, Tuple[int, length = int.from_bytes(length_bytes, "big") if length != 0x02: - raise ValueError("Key Signature event has invalid length. Should be 2, value was {}".format(length)) + raise EventLengthError("Key Signature event has invalid length. Should be 2, value was {}".format(length)) data_bytes = bytearray(data.read(2)) signature_index = data_bytes[0] minor_major = data_bytes[1] return length, (signature_index, minor_major), data_bytes + + +meta_events = { + 0x00: { + "function": sequence_number, + "object_type": MetaSequenceNumber + }, + 0x01: { + "function": text_event, + "object_type": MetaTextEvent + }, + 0x02: { + "function": copyright_notice, + "object_type": MetaTextEvent, + }, + 0x03: { + "function": chunk_name, + "object_type": MetaTextEvent + }, + 0x04: { + "function": instrument_name, + "object_type": MetaTextEvent + }, + 0x05: { + "function": lyric, + "object_type": MetaTextEvent + }, + 0x06: { + "function": marker, + "object_type": MetaTextEvent + }, + 0x07: { + "function": cue_point, + "object_type": MetaTextEvent + }, + 0x2F: { + "function": channel_prefix, + "object_type": MetaChannelPrefix + }, + 0x51: { + "function": set_tempo, + "object_type": MetaSetTempo + }, + 0x54: { + "function": smpte_offset, + "object_type": MetaSMPTEOffset + }, + 0x58: { + "function": time_signature, + "object_type": MetaTimeSignature + }, + 0x59: { + "function": key_signature, + "object_type": MetaKeySignature + } +} + +MetaEventType = Union[MetaTextEvent, MetaSequenceNumber, MetaTimeSignature, MetaKeySignature, MetaSMPTEOffset, + MetaSetTempo, MetaChannelPrefix] diff --git a/midisnake/parser.py b/midisnake/parser.py index b014f0b..557d3e1 100644 --- a/midisnake/parser.py +++ b/midisnake/parser.py @@ -39,7 +39,6 @@ class Parser: def __init__(self, midi_file: BufferedReader) -> None: self.midi_file = midi_file - self.header = Header(self.midi_file) def _read_track(self): diff --git a/midisnake/structure.py b/midisnake/structure.py index 6ef44a3..40dff7d 100644 --- a/midisnake/structure.py +++ b/midisnake/structure.py @@ -21,9 +21,9 @@ # SOFTWARE. from abc import ABCMeta, abstractmethod from io import BufferedReader, FileIO -from typing import List, Union, Dict +from typing import List, Union, Dict, Any -from midisnake.errors import LengthError +from midisnake.errors import EventLengthError __all__ = ["Header", "Event"] @@ -90,7 +90,7 @@ def __init__(self, data: int) -> None: if len(hex(data)[2:]) != 6: err_msg = "Length of given data is incorrect. The length is {} and it should be 6".format( len(hex(data)[2:])) - raise LengthError(err_msg) + raise EventLengthError(err_msg) if self.valid(data): self._process(data) else: @@ -140,7 +140,11 @@ class Track: track_number = None # type: int length = None # type: int events = None # type: List[Event] - meta_data = None # type: Dict[str, ] + meta_data = { + "seq_number": None, + "copyright": None, + "chunk_name": None + } # type: Dict[str, Any] def __init__(self, data: Union[FileIO, BufferedReader]) -> None: chunk_name = data.read(4) @@ -153,6 +157,7 @@ def _parse(self, data: Union[FileIO, BufferedReader]): delta_time = VariableLengthValue(data) + class VariableLengthValue: """Parses and stores a MIDI variable length value diff --git a/tests/test_events.py b/tests/test_events.py index ddf3560..ab23903 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -24,7 +24,7 @@ from unittest.mock import MagicMock, call from midisnake.events import NoteOff, NoteOn, PolyphonicAftertouch, PitchBend, get_note_name, _decode_leftright -from midisnake.errors import LengthError +from midisnake.errors import EventLengthError logger = logging.getLogger(__name__) @@ -60,14 +60,14 @@ def test_constructor(self): # --- Exception Testing --- # logger.info("Starting NoteOn constructor exception tests") # Test Length Exceptions - with self.assertRaises(LengthError, + with self.assertRaises(EventLengthError, msg="NoteOn did not raise LengthError when given value 0x123001929391923919" ) as exc: NoteOn(0x123001929391923919) logger.exception(exc) - with self.assertRaises(LengthError, + with self.assertRaises(EventLengthError, msg="NoteOn did not raise LengthError when given value 0x1" ) as exc: NoteOn(0x1) @@ -176,13 +176,13 @@ def test_constructor(self): # --- Exception Testing --- # logger.info("Starting NoteOn constructor exception tests") # Test Length Exceptions - with self.assertRaises(LengthError, + with self.assertRaises(EventLengthError, msg="NoteOff did not raise LengthError when given value 0x123001929391923919" ) as exc: NoteOff(0x123001929391923919) logger.exception(exc) - with self.assertRaises(LengthError, + with self.assertRaises(EventLengthError, msg="NoteOff did not raise LengthError when given value 0x1" ) as exc: NoteOff(0x1) @@ -298,13 +298,13 @@ def test_constructor(self): # --- Exception Testing --- # logger.info("Starting PolyphonicAftertouch constructor exception tests") # Test Length Exceptions - with self.assertRaises(LengthError, + with self.assertRaises(EventLengthError, msg="PolyphonicAftertouch did not raise LengthError when given value 0x123001929391923919" ) as exc: PolyphonicAftertouch(0x123001929391923919) logger.exception(exc) - with self.assertRaises(LengthError, + with self.assertRaises(EventLengthError, msg="PolyphonicAftertouch did not raise LengthError when given value 0x1" ) as exc: PolyphonicAftertouch(0x1) @@ -419,13 +419,13 @@ def test_constructor(self): # --- Exception Testing --- # logger.info("Starting PitchBend constructor exception tests") # Test Length Exceptions - with self.assertRaises(LengthError, + with self.assertRaises(EventLengthError, msg="PitchBend did not raise LengthError when given value 0x123001929391923919" ) as exc: PitchBend(0x123001929391923919) logger.exception(exc) - with self.assertRaises(LengthError, + with self.assertRaises(EventLengthError, msg="PitchBend did not raise LengthError when given value 0x1" ) as exc: PitchBend(0x1)