Skip to content

Commit

Permalink
Finished base implementation of MIDI Meta Messages
Browse files Browse the repository at this point in the history
  • Loading branch information
MicroTransactionsMatterToo committed Sep 24, 2017
1 parent 939c49a commit edf5d8c
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 42 deletions.
10 changes: 8 additions & 2 deletions midisnake/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
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"""
33 changes: 24 additions & 9 deletions midisnake/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))



Expand Down
192 changes: 175 additions & 17 deletions midisnake/meta_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
[
Expand All @@ -40,7 +41,6 @@
) # type: Union[Callable, NamedTuple]



class MetaTextEvent:
variant_number = None # type: int
variant_name = None # type: str
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand All @@ -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

Expand All @@ -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")

Expand All @@ -169,15 +268,15 @@ 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


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

Expand All @@ -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))
Expand Down Expand Up @@ -238,26 +337,85 @@ 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]:
length_bytes = bytearray(data.read(1))
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]
1 change: 0 additions & 1 deletion midisnake/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit edf5d8c

Please sign in to comment.