Skip to content

Commit

Permalink
Added starting meta events
Browse files Browse the repository at this point in the history
  • Loading branch information
MicroTransactionsMatterToo committed Sep 21, 2017
1 parent 14bfea5 commit 939c49a
Show file tree
Hide file tree
Showing 5 changed files with 357 additions and 12 deletions.
4 changes: 2 additions & 2 deletions midisnake/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from midisnake.parser import *
from midisnake.structure import *
from midisnake.parser import Parser
from midisnake.structure import Event

__all__ = ["Parser", "Event"]
30 changes: 30 additions & 0 deletions midisnake/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from typing import Any, Dict

from midisnake.meta_events import *
from midisnake.structure import Event

__all__ = ["NoteOn", "NoteOff", "PolyphonicAftertouch", "PitchBend", "events"]

note_values = {
0: "C",
1: "C#",
Expand Down Expand Up @@ -49,6 +53,9 @@ def _decode_leftright(data: int) -> str:
return "left"
elif data == 64:
return "center"
else:
raise ValueError("Unable to decode left-right value")


midi_controls = {
0x00: {
Expand Down Expand Up @@ -135,6 +142,11 @@ def _decode_leftright(data: int) -> str:
}
}

meta_events = {
0: sequence_number,
1: text_event
} # type: Dict[int, Callable[[Union[BufferedReader, FileIO]], Tuple[Any, Any, Any]]]


def get_note_name(data: int) -> str:
"""Converts a MIDI note value to a note name.
Expand Down Expand Up @@ -292,3 +304,21 @@ def _process(self, data: int):
self.channel_number = data_array[0] & 0x0F
self.bend_amount = (data_array[2] << 7) + data_array[1]
self.raw_data = data


class MetaFactory:
pass


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



events = [NoteOn, NoteOff, PitchBend, PolyphonicAftertouch]
263 changes: 263 additions & 0 deletions midisnake/meta_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
# MIT License
#
# Copyright (c) 20/09/17 Ennis Massey
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

"""
Provides functions for parsing of MIDI meta events
"""

from io import BufferedReader, FileIO
from typing import Union, Tuple, NamedTuple, Callable, Any

from midisnake.structure import VariableLengthValue

SMPTE_Format = NamedTuple("SMPTE_Format",
[
('hours', int),
('minutes', int),
('seconds', int),
('fps', int),
('ff', int)
]
) # type: Union[Callable, NamedTuple]



class MetaTextEvent:
variant_number = None # type: int
variant_name = None # type: str

length = None # type: int

text = None # type: str

event_info = None # type: bytearray
raw_content = None # type: bytearray

def __init__(self, event_info: bytes, variant: int, data: Tuple[int, str, bytearray]) -> None:
self.event_info = bytearray(event_info)
self.variant_number = variant

self.length = data[0] + len(event_info)
self.raw_content = self.event_info + bytes(data[2])

self.text = data[1]







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))
sequence_num_raw = bytearray(data.read(2))
sequence_num = int.from_bytes(sequence_num_raw, "big")
return length, sequence_num, sequence_num_raw


def text_event(data: Union[FileIO, BufferedReader]) -> Tuple[int, str, bytearray]:
length = VariableLengthValue(data).value
raw_data = bytearray(data.read(length))
try:
text = raw_data.decode("ASCII")
except UnicodeDecodeError as exc:
raise ValueError("Unparsable text in text event") from exc

return length, text, raw_data


def copyright_notice(data: Union[FileIO, BufferedReader]) -> Tuple[int, str, bytearray]:
length = VariableLengthValue(data).value
raw_data = bytearray(data.read(length))
try:
text = raw_data.decode("ASCII")
except UnicodeDecodeError as exc:
raise ValueError("Unparsable text in copyright notice") from exc

return length, text, raw_data


def chunk_name(data: Union[FileIO, BufferedReader]) -> Tuple[int, str, bytearray]:
length = VariableLengthValue(data).value
raw_data = bytearray(data.read(length))
try:
text = raw_data.decode("ASCII")
except UnicodeDecodeError as exc:
raise ValueError("Unparsable text in track/sequence name") from exc

return length, text, raw_data


def instrument_name(data: Union[FileIO, BufferedReader]) -> Tuple[int, str, bytearray]:
length = VariableLengthValue(data).value
raw_data = bytearray(data.read(length))
try:
text = raw_data.decode("ASCII")
except UnicodeDecodeError as exc:
raise ValueError("Unparsable text in instrument name") from exc

return length, text, raw_data


def lyric(data: Union[FileIO, BufferedReader]) -> Tuple[int, str, bytearray]:
length = VariableLengthValue(data).value
raw_data = bytearray(data.read(length))
try:
text = raw_data.decode("ASCII")
except UnicodeDecodeError as exc:
raise ValueError("Unparseable text in lyric text") from exc

return length, text, raw_data


def marker(data: Union[FileIO, BufferedReader]) -> Tuple[int, str, bytearray]:
length = VariableLengthValue(data).value
raw_data = bytearray(data.read(length))
try:
text = raw_data.decode("ASCII")
except UnicodeDecodeError as exc:
raise ValueError("Unparseable text in marker text") from exc

return length, text, raw_data


def cue_point(data: Union[FileIO, BufferedReader]) -> Tuple[int, str, bytearray]:
length = VariableLengthValue(data).value
raw_data = bytearray(data.read(length))
try:
text = raw_data.decode("ASCII")
except UnicodeDecodeError as exc:
raise ValueError("Unparseable text in Cue Point text") from exc

return length, text, raw_data


def channel_prefix(data: Union[FileIO, BufferedReader]) -> Tuple[int, int, bytearray]:
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))
prefix_raw = bytearray(data.read(1))
prefix = int.from_bytes(prefix_raw, "big")

return length, prefix, prefix_raw


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")
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))
raw_data = bytearray(data.read(3))
tpqm = int.from_bytes(raw_data, "big")

return length, tpqm, raw_data


def smpte_offset(data: Union[FileIO, BufferedReader]) -> Tuple[int, Tuple[int, int, int, int, int], bytearray]:
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))

# Process Hours
hour_data = bytearray(data.read(8))
hour_bits = int.from_bytes(hour_data, 'big')
null_bit = hour_bits & 0b10000000

frame_crumb = (hour_bits & 0b01100000) >> 5
hours = hour_bits & 0b00011111

minute_data = bytearray(data.read(8))
minute_bits = int.from_bytes(minute_data, "big")
null_bit |= minute_bits & 0b11000000
minutes = minute_bits & 0b00111111

second_data = bytearray(data.read(8))
second_bits = int.from_bytes(second_data, "big")
null_bit |= second_bits & 0b11000000
seconds = second_bits & 0b00111111

frame_count_data = bytearray(data.read(8))
frame_count_bits = int.from_bytes(frame_count_data, "big")
null_bit = frame_count_bits & 0b11100000
frame_count = frame_count_bits & 0b00011111

fraction_data = bytearray(data.read(8))
fraction = int.from_bytes(fraction_data, "big")

raw_data = bytearray()
[raw_data.append(x) for x in [hour_bits, minute_bits, second_bits, frame_count_bits, fraction]]

smpte_data = SMPTE_Format(
hours=hours,
minutes=minutes,
seconds=seconds,
fps=frame_count,
ff=fraction
)

if null_bit != 0b0:
raise ValueError("Null bits were not 0 in SMPTE timecode")

return length, smpte_data, raw_data


def time_signature(data: Union[FileIO, BufferedReader]) -> Tuple[int, Tuple[int, int, int, int], bytearray]:
length_bytes = bytearray(data.read(1))
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))

data_bytes = bytearray(data.read(4)) # type: bytearray
nominator = 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


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

data_bytes = bytearray(data.read(2))
signature_index = data_bytes[0]
minor_major = data_bytes[1]

return length, (signature_index, minor_major), data_bytes
25 changes: 22 additions & 3 deletions midisnake/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,30 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from io import BufferedReader, TextIOWrapper
from typing import Union, Dict
from io import BufferedReader, FileIO
from typing import Union, Dict, List

from midisnake.structure import Track, Header, VariableLengthValue

__all__ = ["Parser"]


class Parser:
__slots__ = ["midi_file", "current_position", "current_chunk", "chunk_positions"]
midi_file = None # type: Union[BufferedReader, FileIO]

current_position = None # type: int
current_chunk = None # type: int
chunk_positions = [] # type: List[int]

header = None # type: Header
tracks = [] # type: List[Track]

def __init__(self, midi_file: BufferedReader) -> None:
self.midi_file = midi_file

self.header = Header(self.midi_file)

def _read_track(self):
self.chunk_positions.append(self.midi_file.tell())

new_track = Track(self.midi_file)
Loading

0 comments on commit 939c49a

Please sign in to comment.