#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Generate a MID(Standard MIDI File) file from a CSV file.
Format of CSV file
<Format> = <Track Name>, <Bar Number>, <Tick Number>, <Event List>,
<Value List>
1. Track Name
Track Name.
- example: "Sequencer"
- example: "Main Melody"
- example: "Sub Melody"
2. Bar Number
Indicates the order of appearance of the measures.
- example: 0
- example: 1
3. Tick Number
Indicates the timing of event occurrence within a bar in terms of Tick
conversion. However, the length of a quarter note is 480 ticks.
- example: 0
- example: 480
- example: 960
4. Event List
Event.
- example: "Meta Event","Set Tempo"
- example: "Midi Event","Note On"
- example: "Midi Event","Control Change","Channel Volume"
5. Value List
Event Value.
- example: 500000
- example: 0,69,0x40,200
- example: 0,0x60
restrictions
#. Only "GM System Level 1".
#. Tempo changes and odd time signatures are not supported.
#. The division is fixed at 480.
"""
from dataclasses import InitVar, dataclass, field
from typing import ClassVar
from re import match
import logging as LSMF
import struct
import csv
# Logger
logger = LSMF.getLogger(__name__)
logger.setLevel(LSMF.DEBUG)
_logger_formatter = LSMF.Formatter("%(asctime)s %(levelname)s %(message)s")
# Logger StreamHandler
_logger_sh = LSMF.StreamHandler()
_logger_sh.setFormatter(_logger_formatter)
logger.addHandler(_logger_sh)
[ドキュメント]class SmfError(Exception):
"""Exceptions sent out from errcheck func."""
def __init__(self, *args: object) -> None:
super().__init__(*args)
logger.error(f"SMF: {args}")
@dataclass
class _SmfEvent:
"""_SmfEvent is the base class for event."""
#: str: "track" is a name that identifies a track.
track: str
#: InitVar[str]: bar number
bar: InitVar[str] = "0"
#: InitVar[str]: tick count in bar
tick: InitVar[str] = "0"
#: ClassVar[int]: division, ticks per quarter-note.
division: ClassVar[int] = 480
#: ClassVar[int]: beat
beat: ClassVar[int] = 4
#: ClassVar[int]: power of two, 2 represents a quarter-note, etc.
powers: ClassVar[int] = 2
event_time: int = field(default=0, init=False)
delta_time: int = field(default=0, init=False)
def set_delta_time(self, before_event_time: int) -> None:
"""set_delta_time _summary_
:param int before_event_time: _description_
"""
self.delta_time = self.event_time - before_event_time
@classmethod
def set_division(cls, division: int):
cls.division = division
@classmethod
def set_rhythm(cls, beat: int, powers: int):
cls.beat = beat
cls.powers = powers
def __post_init__(self, bar, tick) -> None:
self.event_time = int(bar) * self.beat * self.division * 4 / (2**self.powers)
self.event_time += int(tick)
self.event_time = int(self.event_time)
def __bytes__(self):
return _convert_to_variable_length_quantity(self.delta_time)
def __eq__(self, other) -> bool:
"""compare time: equal"""
if isinstance(other, _SmfEvent):
return self.event_time == other.event_time
else:
name: str = other.__class__.__name__
message: str = f"comparison between event and {name} is not supported"
raise SmfError(message)
def __lt__(self, other) -> bool:
"""compare time: less than"""
if isinstance(other, _SmfEvent):
return self.event_time < other.event_time
else:
name: str = other.__class__.__name__
message: str = f"comparison between event and {name} is not supported"
raise SmfError(message)
@classmethod
def name(cls) -> list[str]:
return []
@dataclass
class _MetaEvnet(_SmfEvent):
"""_MetaEvent is the base class of "Meta Event"."""
def __bytes__(self):
return super().__bytes__() + b"\xFF"
@classmethod
def name(cls) -> list[str]:
return super().name() + ["Meta Event"]
[ドキュメント]@dataclass
class TextEvent(_MetaEvnet):
"""TextEvent is class."""
#: str: text event
text: str = ""
def __bytes__(self):
byte_datas = b"\x01"
byte_datas += int(len(self.text)).to_bytes(1, "big")
byte_datas += bytearray(self.text, encoding="ascii")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["Text Event"]
[ドキュメント]@dataclass
class CopyrightNotice(_MetaEvnet):
"""CopyrightNotice is class."""
#: str: Copyright Notice
text: str = ""
def __bytes__(self):
byte_datas = b"\x02"
byte_datas += int(len(self.text)).to_bytes(1, "big")
byte_datas += bytearray(self.text, encoding="ascii")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["Copyright Notice"]
[ドキュメント]@dataclass
class SequencerTrackName(_MetaEvnet):
"""SequenceTrackName is class."""
#: str: Sequencer / Track Name
text: str = ""
def __bytes__(self):
byte_datas = b"\x03"
byte_datas += int(len(self.text)).to_bytes(1, "big")
byte_datas += bytearray(self.text, encoding="ascii")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["SequencerTrack Name"]
[ドキュメント]@dataclass
class InstrumentName(_MetaEvnet):
"""InstrumentName is class."""
#: str: Instrument Name
text: str = ""
def __bytes__(self):
byte_datas = b"\x04"
byte_datas += int(len(self.text)).to_bytes(1, "big")
byte_datas += bytearray(self.text, encoding="ascii")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls):
return super().name() + ["Instrument Name"]
[ドキュメント]@dataclass
class Lyric(_MetaEvnet):
"""Lyric is class."""
#: str: Lyric
text: str = ""
def __bytes__(self):
byte_datas = b"\x05"
byte_datas += int(len(self.text)).to_bytes(1, "big")
byte_datas += bytearray(self.text, encoding="ascii")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["Lyric"]
[ドキュメント]@dataclass
class EndOfTrack(_MetaEvnet):
"""end of track"""
def __bytes__(self):
return super().__bytes__() + b"\x2F\x00"
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["End Of Track"]
[ドキュメント]@dataclass
class SetTempo(_MetaEvnet):
"""Quarter note microsecond unit time."""
#: str: Quarter note microsecond unit time
value: str = "500000"
def __bytes__(self):
byte_datas = b"\x51"
byte_datas += b"\x03"
byte_datas += int(self.value).to_bytes(3, "big")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["Set Tempo"]
[ドキュメント]@dataclass
class TimeSignature(_MetaEvnet):
"""TimeSignature is expressed as four numbers."""
#: str: rhythm beat
nn: str = "4"
#: str: power to two, rhythm notevalue
dd: str = "2"
#: str:
cc: str = "24"
#: str:
bb: str = "8"
def __post_init__(self, bar, tick) -> None:
_SmfEvent.set_rhythm(int(self.nn), int(self.dd))
return super().__post_init__(bar, tick)
def __bytes__(self):
byte_datas = b"\x58"
byte_datas += b"\x04"
byte_datas += int(self.nn).to_bytes(1, "big")
byte_datas += int(self.dd).to_bytes(1, "big")
byte_datas += int(self.cc).to_bytes(1, "big")
byte_datas += int(self.bb).to_bytes(1, "big")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["Time Signature"]
[ドキュメント]@dataclass
class KeySignature(_MetaEvnet):
"""KeySignature is expressed as four numbers."""
#: str: 0 is C, +1 ~ +7 is #, -1 ~ -7 is b
sf: str = "0"
#: str: 0 is major, 1 is minor
mi: str = "0"
def __bytes__(self):
byte_datas = b"\x59"
byte_datas += b"\x02"
byte_datas += int(self.sf).to_bytes(1, "big", signed=True)
byte_datas += int(self.mi).to_bytes(1, "big")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["Key Signature"]
@dataclass
class _SystemExclusiveEvent(_SmfEvent):
"""_SystemExclusiveEvent is the base class of "Sysex Event"."""
def __bytes__(self):
return super().__bytes__() + b"\xF0"
@classmethod
def name(cls) -> list[str]:
return super().name() + ["Sysex Event"]
[ドキュメント]@dataclass
class GmSystemOn(_SystemExclusiveEvent):
"""GM System Level 1."""
def __bytes__(self):
return super().__bytes__() + b"\x05\x7E\x7F\x09\x01\xF7"
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["GM System On"]
@dataclass
class _MidiEvent(_SmfEvent):
"""_MidiEvent is the base class of "Midi Event"."""
def __bytes__(self):
return super().__bytes__()
@classmethod
def name(cls) -> list[str]:
return super().name() + ["Midi Event"]
[ドキュメント]@dataclass
class NoteOn(_MidiEvent):
"""NoteOn is "Note On"."""
#: str: channel
n: str = "0"
#: str: key number
kk: str = "60"
#: str: velocity
vv: str = "0x40"
#: str: duration
dr: str = "100"
def __bytes__(self):
byte_datas = (0x90 + int(self.n, 0)).to_bytes(1, "big")
byte_datas += int(self.kk, 0).to_bytes(1, "big")
byte_datas += int(self.vv, 0).to_bytes(1, "big")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["Note On"]
[ドキュメント]@dataclass
class NoteOffAfterOn(_MidiEvent):
"""NoteOffAfterOn is "Note off"."""
#: str: channel
n: str = "0"
#: str: key number
kk: str = "60"
#: str: velocity
vv: str = "0x20"
def __bytes__(self):
byte_datas = (0x80 + int(self.n, 0)).to_bytes(1, "big")
byte_datas += int(self.kk, 0).to_bytes(1, "big")
byte_datas += int(self.vv, 0).to_bytes(1, "big")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["Note Off"]
@dataclass
class _ControlChange(_MidiEvent):
"""_ControlChange is the base class of "Midi Event - Control Change"."""
#: str: channel
n: str = "0"
def __bytes__(self):
byte_datas = (0xB0 + int(self.n, 0)).to_bytes(1, "big")
return super().__bytes__() + byte_datas
@classmethod
def name(cls) -> list[str]:
return super().name() + ["Control Change"]
[ドキュメント]@dataclass
class ModulationWheel(_ControlChange):
"""ModulationWeel is "Modulation Wheel"."""
#: str: value
vv: str = "0"
def __bytes__(self):
byte_datas = (0x01).to_bytes(1, "big")
byte_datas += int(self.vv, 0).to_bytes(1, "big")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["Modulation Wheel"]
[ドキュメント]@dataclass
class ChannelVolume(_ControlChange):
"""ChannelVolume is "Channel Volume"."""
#: str: value
vv: str = "0x70"
def __bytes__(self):
byte_datas = (0x07).to_bytes(1, "big")
byte_datas += int(self.vv, 0).to_bytes(1, "big")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["Channel Volume"]
[ドキュメント]@dataclass
class Pan(_ControlChange):
"""Pan is "Pan"."""
#: str:
vv: str = "0x40"
def __bytes__(self):
byte_datas = (0x0A).to_bytes(1, "big")
byte_datas += int(self.vv, 0).to_bytes(1, "big")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["Pan"]
[ドキュメント]@dataclass
class Expression(_ControlChange):
"""Expresson is "Expression"."""
#: str: value
vv: str = "0x70"
def __bytes__(self):
byte_datas = (0x0B).to_bytes(1, "big")
byte_datas += int(self.vv, 0).to_bytes(1, "big")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["Expression"]
[ドキュメント]@dataclass
class SustainOn(_ControlChange):
"""SustaionOn is ***."""
def __bytes__(self):
byte_datas = (0x40).to_bytes(1, "big")
byte_datas += (0x40).to_bytes(1, "big")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["Sustain On"]
[ドキュメント]@dataclass
class SustainOff(_ControlChange):
"""SustaionOff is ***."""
def __bytes__(self):
byte_datas = (0x40).to_bytes(1, "big")
byte_datas += (0x00).to_bytes(1, "big")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["Sustain Off"]
@dataclass
class _ChannelMode(_MidiEvent):
"""_ChannelMode is base class of "Midi Event - Channel Mode"."""
#: str: channel
n: str = "0"
def __bytes__(self):
byte_datas = (0xB0 + int(self.n, 0)).to_bytes(1, "big")
return super().__bytes__() + byte_datas
@classmethod
def name(cls) -> list[str]:
return super().name() + ["Channel Mode"]
[ドキュメント]@dataclass
class ResetAllControllers(_ChannelMode):
"""ResetAllControllers is ***."""
def __bytes__(self):
byte_datas = (0x79).to_bytes(1, "big")
byte_datas += (0x00).to_bytes(1, "big")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["Reset All Controllers"]
[ドキュメント]@dataclass
class AllNotesOff(_ChannelMode):
"""AllNoteOff is "all notes off message"."""
def __bytes__(self):
byte_datas = (0x7B).to_bytes(1, "big")
byte_datas += (0x00).to_bytes(1, "big")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["All Notes Off"]
[ドキュメント]@dataclass
class ProgramChange(_MidiEvent):
"""ProgramChange is ***."""
#: str: channle
n: str = "0"
#: str: preset number
vv: str = "0"
def __bytes__(self):
byte_datas = (0xC0 + int(self.n, 0)).to_bytes(1, "big")
byte_datas += int(self.vv, 0).to_bytes(1, "big")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["Program Change"]
[ドキュメント]@dataclass
class ChannelPressure(_MidiEvent):
"""ChannelPressure is ***."""
#: str: channle
n: str = "0"
#: int: threshold
vv: str = "100"
def __bytes__(self):
byte_datas = (0xD0 + int(self.n, 0)).to_bytes(1, "big")
byte_datas += int(self.vv, 0).to_bytes(1, "big")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["Channel Pressuire"]
[ドキュメント]@dataclass
class PitchWheelChange(_MidiEvent):
"""PitchWheelChange is ***."""
#: str: channel
n: str = "0"
#: str: LSB, centre(non-effect) is 0x00
ll: str = "0"
#: str: MSB, centre(non-effect) is 0x40
mm: str = "0x40"
def __bytes__(self):
byte_datas = (0xE0 + int(self.n, 0)).to_bytes(1, "big")
byte_datas += int(self.ll, 0).to_bytes(1, "big")
byte_datas += int(self.mm, 0).to_bytes(1, "big")
return super().__bytes__() + byte_datas
[ドキュメント] @classmethod
def name(cls) -> list[str]:
return super().name() + ["Pitch Wheel Change"]
def _convert_to_variable_length_quantity(n: int) -> bytes:
"""Convert "n(delta time)" to a "variable length quantity".
:param int n: delta time
:return: variable length quantity
"""
def base_decimal(base: int, n: int) -> list[int]:
"""Convert to a base-decimal.
:param int base: base
:param int n: taget number
:return: Converted value stored in list[int]
"""
if n < base:
return [n]
else:
digit: list = base_decimal(base, n // base)
return digit + [n % base]
quantity: list[int] = base_decimal(2**7, n)
for i in range(len(quantity) - 1):
quantity[i] = quantity[i] + 0x80
return bytes(quantity)
[ドキュメント]def event_classes() -> set:
"""Return a set of event classes.
:return: event classes
"""
def sub_classes(cls):
return set(cls.__subclasses__()).union(
[s for c in cls.__subclasses__() for s in sub_classes(c)]
)
return sub_classes(_SmfEvent)
[ドキュメント]def event_data(params: list) -> object:
"""Convert CSV data list to event data object.
:param list params: csv data list
:return: event data object
"""
n_name: int
data_classes: set = event_classes()
for data_class in data_classes:
if not match("_", data_class.__name__):
if set(data_class.name()) <= set(params):
n_name = len(data_class.name())
return data_class(
params[0], params[1], params[2], *tuple(params[3 + n_name :])
)
return None
[ドキュメント]def generate(csvfile: str, midifile: str) -> None:
"""Generate a MID file from a CSV file.
:param str csvfile: CSV filename to be converted.
:param str midifile: MID filename to be created.
"""
csv_datas: list = []
with open(file=csvfile, mode="rt") as f:
for line in csv.reader(f, dialect="unix"):
csv_datas.append(line)
track_names: list = list(map(lambda x: x[0], csv_datas))
track_names = sorted(set(track_names), key=track_names.index)
header = HeaderChunk(format=1, ntrks=len(track_names), division=480)
tracks: list = []
for i in range(len(track_names)):
tracks.append([])
for line in csv_datas:
if line[0] == track_names[i]:
tracks[i].append(event_data(line))
for trk in tracks:
for ev in trk:
if isinstance(ev, NoteOn):
note_off = NoteOffAfterOn(track=ev.track, n=ev.n, kk=ev.kk)
note_off.event_time = ev.event_time + int(ev.dr)
trk.append(note_off)
trk.sort()
trk[0].set_delta_time = trk[0].event_time
for i in range(1, len(trk)):
trk[i].set_delta_time(trk[i - 1].event_time)
with open(file=midifile, mode="wb") as smf:
smf.write(bytes(header))
for trk in tracks:
smf.write(
struct.pack(
">" "4s" "L", b"MTrk", sum(list(map(len, (map(bytes, trk)))))
)
)
for ev in trk:
smf.write(bytes(ev))
if __name__ == "__main__":
print(__file__)