레이블이 mid인 게시물을 표시합니다. 모든 게시물 표시
레이블이 mid인 게시물을 표시합니다. 모든 게시물 표시

2022년 9월 17일 토요일

python으로 mid file 분석, MTrk

 

지난번에는 MThd설명을 하였으며 MTrk에 가장 중요한 이벤트가 들어 있습니다.

MTrk는 그 구조가 상당히 복잡합니다.

time delta + 동작 코드 : 형태가 가장 기본 입니다.

MTrk + time delta + 동작 코드 + time delta + 동작 코드 + time delta + 동작 코드 + ....
위와 같은 형태가 반복됩니다.

일단 아래 그림을 살펴보도록 하겠습니다.


time delta

여기에서 time delta는 동작 코드를 얼마나 기다렸다가 동작을 시키는지에 대한 대기 시간입니다.

앞서서 MThd 설명에서 아래와 같은 time division이 있었습니다.
time division : 이 값은 4분 음표(4분 음표를 1박자라고 표현)를 여기 에서는 몇개의 틱으로 나타내는지를 나타내는 숫자 입니다.

(여기 값이 480이라고 한다면 나중에 음표를 연주할때 480틱이라는 시간 동안 연주를 하라는 명령이 내려오면 1박자 동안 연주하면 됩니다. )

time division이 480이고 time delta가 240이 들어있다고 한다면 0.5박자 후에 어떤 이벤트가 벌어진다는 얘기가 됩니다. 0.5박자라고 표현한것은 실제 tempo(템포)에 따라서 시간 개념이 다르기 때문입니다.

또한 time delta는 7비트만 이용하고 최상위 bit가 1인 경우 (127 보다 커지는 경우) 다음 byte를 더 읽도록 됩니다. 즉 time delta 의 데이터 크기가 가변적이라 읽을때 특별한 처리가 필요합니다.

아래와 같은 함수를 이용합니다. 리턴되는 값은 실제 읽은 data와 몇 바이트를 사용했는지 정보를 같이 리턴합니다.

def read_variable_int(self, byte_data, pos):
delta = 0
i = 0
while True:
byte = np.uint8(byte_data[i+pos])
delta = (delta << 7) | (byte & 0x7f)
if byte < 0x80:
return delta, i+1
i = i + 1


event type(status_byte)

event 라고 불리기도 하고 message로 불리기도 합니다. 여기에 1byte에 오는 형태의 값에 따라 여러가지 형태를 가지고 뒤쪽에 데이터의 길이도 달라지게 됩니다.

mido 라는 python 프로젝트의 아래 코드를 참고 해서 가져온 SPECS 부분을 참고했습니다. mido/messages/specs.py

SPECS = [
# https://www.recordingblogs.com/wiki/midi-voice-messages
# MIDI voice messages
_defmsg(0x80, 'note_off', ('channel', 'note', 'velocity'), 3),
_defmsg(0x90, 'note_on', ('channel', 'note', 'velocity'), 3),
_defmsg(0xa0, 'polytouch', ('channel', 'note', 'value'), 3),
_defmsg(0xb0, 'control_change', ('channel', 'control', 'value'), 3),
_defmsg(0xc0, 'program_change', ('channel', 'program',), 2),
_defmsg(0xd0, 'aftertouch', ('channel', 'value',), 2),
_defmsg(0xe0, 'pitchwheel', ('channel', 'pitch',), 3),

# https://www.recordingblogs.com/wiki/midi-system-common-messages
# System common messages.
# 0xf4 and 0xf5 are undefined.
_defmsg(0xf0, 'sysex', ('data',), float('inf')),
_defmsg(0xf1, 'quarter_frame', ('frame_type', 'frame_value'), 2),
_defmsg(0xf2, 'songpos', ('pos',), 3),
_defmsg(0xf3, 'song_select', ('song',), 2),
_defmsg(0xf6, 'tune_request', (), 1),

# https://www.recordingblogs.com/wiki/midi-system-realtime-messages
# System real time messages.
# 0xf9 and 0xfd are undefined.
_defmsg(0xf8, 'clock', (), 1),
_defmsg(0xfa, 'start', (), 1),
_defmsg(0xfb, 'continue', (), 1),
_defmsg(0xfc, 'stop', (), 1),
_defmsg(0xfe, 'active_sensing', (), 1),

# https://www.recordingblogs.com/wiki/midi-meta-messages
# MIDI meta messages
_defmsg(0xff, 'reset', (), 1),
]

이 list에서 마지막 숫자는 앞서 말한 동작 코드의 길이입니다.

메세지의 형태에 따라서 전체 길이가 달라집니다. 여기에서는 테스트 목적과 분석하는 목적이므로 note_on, note_off 만 처리할 예정입니다. 해당은 음표를 연주하거나 중지하는 가장 기본이 되는 부분이라 이 부분만 알면 연주하는 것에 문제가 없습니다.


meta messages

가장 이해가 쉬운 meta messages 부터 설명 합니다.

status_byte 값이 0xFF가 오면 뒤쪽에 command code와 데이터가 옵니다. 물론 command에 따라 데이터가 없을 수도 있습니다.

META_EVENT_SPEC = [
_meta_defmsg(0x00, "Sequence number"),
_meta_defmsg(0x01, "Text"),
_meta_defmsg(0x02, "Copyright notice"),
_meta_defmsg(0x03, "Track name"),
_meta_defmsg(0x04, "Instrument name"),
_meta_defmsg(0x05, "Lyrics"),
_meta_defmsg(0x06, "Marker"),
_meta_defmsg(0x07, "Cue point"),
_meta_defmsg(0x20, "Channel prefix"),
_meta_defmsg(0x2F, "End of track"),
_meta_defmsg(0x51, "Set tempo"),
_meta_defmsg(0x54, "SMPTE offset"),
_meta_defmsg(0x58, "Time signature"),
_meta_defmsg(0x59, "Key signature"),
_meta_defmsg(0x7F, "Sequencer specific")
]

command 코드는 META_EVENT_SPEC 에 정리되어있으며 해당 값도 mido 소스에서 가져온 내용입니다.

데이터 길이를 명시 하지 않은 이유는 meta message의 경우 다행히 뒤쪽 길이 정보가 곧장 붙어 있어서 그렇습니다.

코드 구현은 아래와 같습니다. 

여기에서 body와 pos가 있는데 body는 전체 np로 mid파일을 메모리로 모두 올린 데이터 입니다. 그중에서 다음 읽을 위치를 결정하는 변수를 pos로 관리합니다. 읽고 나면 다음을 읽기 위해 pos 변수를 읽은 만큼 증가 시켜서 관리를 합니다.

if status_byte == 0xFF:
# https://www.recordingblogs.com/wiki/midi-meta-messages
# Meta messages
command = np.uint8(body[pos])
if command == 0x2F:
print("End of track")
return
pos = pos + 1
data_delta, delta_size = self.read_variable_int(body, pos)
pos = pos + delta_size
data = body[pos:pos+data_delta]
pos = pos + data_delta
find = False
for spec_data in META_EVENT_SPEC:
if command == spec_data['Metatype']:
print(f"Meta message {spec_data['Message']} FF {command:02x} {data} {data_delta:02x} {delta_size:02x}")
find = True
break
if find == False:
print(f"error metatype:{command:02x}" )
#exit(-1)

mido 소스에 보면 몇개 처리를 안하는 부분도 존재합니다.

여기에서도 동일하게 처리 하지 않았습니다.

elif status_byte == 0xF0 or status_byte == 0xF7:
# 일단 처리 안함
data_delta, delta_size = self.read_variable_int(body, pos)
pos = pos + delta_size
data = body[pos:pos + data_delta]
pos = pos + data_delta
print(f"skip messages {data_delta:02x} {data} {delta_size:02x}")


midi messages

Midi message의 경우 하위 byte가 ?로 된 부분이 존재합니다.

0x8? ~ 0xE? : Midi messages

이런 부분인데, 하위 4bit에 채널 정보가 들어갑니다. 그리고 조건 비교시에도 상위 4bit만 midi message인지 비교를 합니다. ? 로 표시된 부분은 비교하지 않습니다.

note_on, note_off 처리하는 곳을 살펴보면 채널(ch)과 음정보(nn) 그리고 세기정보(vv)를 이용해서 음을 생성 하거나 끄도록 합니다.

if status_byte <= 0xEF and spec_data['status_byte'] == (status_byte & 0xF0):
# status_byte <= 0xEF 작은 조건은 Midi messages 가 됩니다.
# Midi messages
if spec_data['type'] == 'note_on' or spec_data['type'] == 'note_off' :
ch = status_byte & 0x0F
nn = body[pos]
vv = body[pos+1]
print(f"{spec_data['type']} {ch} {self.get_note_number_str(nn)} {vv}")
else:
print(f"{status_byte:02x} {spec_data['type']} {spec_data['length']}")
pos_len = spec_data['length'] - 1
find = True
break


System messages

여기에서는 단순 파일 분석이므로 system message부분도 처리해 주지 않는다

elif spec_data['status_byte'] == status_byte:
# status_byte > 0xEF 이면 System messages 가 된다.
# System messages
print(f"{status_byte:02x} {spec_data['type']} {spec_data['length']}")
pos_len = spec_data['length'] - 1
find = True
break



전체 소스

지금까지 구현된 전체 소스는 아래와 같습니다.

import numpy as np

# https://github.com/mido/mido

def _defmsg(status_byte, type_, value_names, length):
    return {
        'status_byte': status_byte,
        'type': type_,
        'value_names': value_names,
        'attribute_names': set(value_names) | {'type', 'time'},
        'length': length,
    }

# from modo mido/messages/specs.py
SPECS = [
    # https://www.recordingblogs.com/wiki/midi-voice-messages
    # MIDI voice messages
    _defmsg(0x80, 'note_off', ('channel', 'note', 'velocity'), 3),
    _defmsg(0x90, 'note_on', ('channel', 'note', 'velocity'), 3),
    _defmsg(0xa0, 'polytouch', ('channel', 'note', 'value'), 3),
    _defmsg(0xb0, 'control_change', ('channel', 'control', 'value'), 3),
    _defmsg(0xc0, 'program_change', ('channel', 'program',), 2),
    _defmsg(0xd0, 'aftertouch', ('channel', 'value',), 2),
    _defmsg(0xe0, 'pitchwheel', ('channel', 'pitch',), 3),

    # https://www.recordingblogs.com/wiki/midi-system-common-messages
    # System common messages.
    # 0xf4 and 0xf5 are undefined.
    _defmsg(0xf0, 'sysex', ('data',), float('inf')),
    _defmsg(0xf1, 'quarter_frame', ('frame_type', 'frame_value'), 2),
    _defmsg(0xf2, 'songpos', ('pos',), 3),
    _defmsg(0xf3, 'song_select', ('song',), 2),
    _defmsg(0xf6, 'tune_request', (), 1),

    # https://www.recordingblogs.com/wiki/midi-system-realtime-messages
    # System real time messages.
    # 0xf9 and 0xfd are undefined.
    _defmsg(0xf8, 'clock', (), 1),
    _defmsg(0xfa, 'start', (), 1),
    _defmsg(0xfb, 'continue', (), 1),
    _defmsg(0xfc, 'stop', (), 1),
    _defmsg(0xfe, 'active_sensing', (), 1),

    # https://www.recordingblogs.com/wiki/midi-meta-messages
    # MIDI meta messages
    _defmsg(0xff, 'reset', (), 1),
]

def _meta_defmsg(Meta_type,Message):
    return {
        'Message': Message,
        'Metatype': Meta_type,
    }

META_EVENT_SPEC = [
    _meta_defmsg(0x00, "Sequence number"),
    _meta_defmsg(0x01, "Text"),
    _meta_defmsg(0x02, "Copyright notice"),
    _meta_defmsg(0x03, "Track name"),
    _meta_defmsg(0x04, "Instrument name"),
    _meta_defmsg(0x05, "Lyrics"),
    _meta_defmsg(0x06, "Marker"),
    _meta_defmsg(0x07, "Cue point"),
    _meta_defmsg(0x20, "Channel prefix"),
    _meta_defmsg(0x2F, "End of track"),
    _meta_defmsg(0x51, "Set tempo"),
    _meta_defmsg(0x54, "SMPTE offset"),
    _meta_defmsg(0x58, "Time signature"),
    _meta_defmsg(0x59, "Key signature"),
    _meta_defmsg(0x7F, "Sequencer specific")
]

class mid_file():

    # https://faydoc.tripod.com/formats/mid.htm
    # https://github.com/mido/mido/blob/main/mido/midifiles/midifiles.py
    def __init__(self):
        self.ticks_per_beat = None
        self.trackcount = None
        self.type = None
        self.note_str = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']

    def get_note_number_str(self, nnumber):
        octave = int(nnumber / len(self.note_str))
        note_ = self.note_str[nnumber % len(self.note_str)]
        return note_+str(octave)

    def read_variable_int(self, byte_data, pos):
        delta = 0
        i = 0
        while True:
            byte = np.uint8(byte_data[i+pos])
            delta = (delta << 7) | (byte & 0x7f)
            if byte < 0x80:
                return delta, i+1
            i = i + 1

    def process_chunk(self, type, length, body):
        if type == 'MThd':
            ''' 
            * type (2)
              0:single-track
              1:multiple tracks, synchronous
              2:multiple tracks, asynchronous
            * trackcount (2)
            * ticks_per_beat (2)
              is the number of delta-time ticks per quarter note.
              4분 노트(음표)당 델타-타임 틱들의 갯수
              https://www.recordingblogs.com/wiki/header-chunk-of-a-midi-file
              16비트의 상위 비트가 0이면 "박자당 틱"(또는 "4분음표당 펄스(틱수)")입니다. 
              상위 비트가 1이면 시간 분할은 "초당 프레임 수"입니다.
            '''
            self.type = int.from_bytes(body[0:2].tobytes(), 'big')
            self.trackcount = int.from_bytes(body[2:4].tobytes(), 'big')
            self.ticks_per_beat = int.from_bytes(body[4:6].tobytes(), 'big')
            print(self.type, self.trackcount, self.ticks_per_beat)
        elif type == 'MTrk':
            # time delta + event type(status_byte) + data
            # time delta :
            #    - 가변길이로 127 보다 추가 byte 사용 : 7bit로 구성된 길이 사용(big endian)
            # event type (status_byte):
            #    - 0x00 ~ 0x7F : Running Status
            #         - same status_byte
            #    - 0x8? ~ 0xE? : Midi messages
            #         - SPECS
            #    - 0xF0, 0xF7 : 일단 처리 안함
            #         - data 가변 길이(7bit) + data
            #    - 0xF0 ~ 0xFE : System messages
            #         - SPECS
            #    - 0xFF : Meta messages (see, read_meta_message in mido )
            #         - command + data 가변 길이(7bit) + data
            pos = 0
            last_status_byte = None
            while True:
                time_delta, delta_size = self.read_variable_int(body, pos)
                pos = pos + delta_size
                print(f"delta {pos} {time_delta}(0x{time_delta:02x}) {delta_size}")

                status_byte = np.uint8(body[pos])
                pos = pos + 1

                if status_byte <= 0x7F:
                    status_byte = last_status_byte
                    pos = pos - 1

                if status_byte == 0xFF:
                    # https://www.recordingblogs.com/wiki/midi-meta-messages
                    # Meta messages
                    command = np.uint8(body[pos])
                    if command == 0x2F:
                        print("End of track")
                        return
                    pos = pos + 1
                    data_delta, delta_size = self.read_variable_int(body, pos)
                    pos = pos + delta_size
                    data = body[pos:pos+data_delta]
                    pos = pos + data_delta
                    find = False
                    for spec_data in META_EVENT_SPEC:
                        if command == spec_data['Metatype']:
                            print(f"Meta message {spec_data['Message']} FF {command:02x} {data} {data_delta:02x} {delta_size:02x}")
                            find = True
                            break
                    if find == False:
                        print(f"error metatype:{command:02x}" )
                        #exit(-1)

                elif status_byte == 0xF0 or status_byte == 0xF7:
                    # 일단 처리 안함
                    data_delta, delta_size = self.read_variable_int(body, pos)
                    pos = pos + delta_size
                    data = body[pos:pos + data_delta]
                    pos = pos + data_delta
                    print(f"skip messages {data_delta:02x} {data} {delta_size:02x}")
                else:
                    pos_len = 1
                    find = False
                    for spec_data in SPECS:
                        if status_byte <= 0xEF and spec_data['status_byte'] == (status_byte & 0xF0):
                            # status_byte <= 0xEF 작은 조건은 Midi messages 가 됩니다.
                            # Midi messages
                            if spec_data['type'] == 'note_on' or spec_data['type'] == 'note_off' :
                                ch = status_byte & 0x0F
                                nn = body[pos]
                                vv = body[pos+1]
                                print(f"{spec_data['type']} {status_byte:02x} {ch} {self.get_note_number_str(nn)} {vv}")
                            else:
                                print(f"{status_byte:02x} {spec_data['type']} {spec_data['length']}")
                            pos_len = spec_data['length'] - 1
                            find = True
                            break
                        elif spec_data['status_byte'] == status_byte:
                            # status_byte > 0xEF 이면 System messages 가 된다.
                            # System messages
                            print(f"{status_byte:02x} {spec_data['type']} {spec_data['length']}")
                            pos_len = spec_data['length'] - 1
                            find = True
                            break
                    if find == False:
                        print(f"error status_byte:{status_byte}" )
                        exit(-1)
                    pos = pos + pos_len
                last_status_byte = status_byte
                if pos >= length:
                    return

    def read(self, filename):
        with open(filename) as f:
            rectype = np.dtype(np.uint8)
            bdata = np.fromfile(f, dtype=rectype)
        # MID 파일은 chunk의 연속으로 되어 있습니다.
        # Chunk Type(4), chunk body length (4), body(chunk body length)
        read_pos = 0
        while True:
            chunk_type = bdata[read_pos:read_pos+4].tobytes()
            chunk_type = chunk_type.decode('utf-8')
            chunk_length = int.from_bytes(bdata[read_pos+4:read_pos+8].tobytes(), 'big')

            print(chunk_type, chunk_length)

            self.process_chunk(chunk_type, chunk_length, bdata[read_pos+8:read_pos+8+chunk_length])

            read_pos = read_pos + chunk_length + 8
            if len(bdata) <= read_pos:
                break


if __name__ == "__main__":
    print(SPECS)
    mid = mid_file()
    mid.read("../00_data/tests_for-elise.mid")


그리고 이것을 엘리제를 위하여 mid 파일로 실행한 결과 입니다.

[{'status_byte': 128, 'type': 'note_off', 'value_names': ('channel', 'note', 'velocity'), 'attribute_names': {'type', 'time', 'channel', 'note', 'velocity'}, 'length': 3}, {'status_byte': 144, 'type': 'note_on', 'value_names': ('channel', 'note', 'velocity'), 'attribute_names': {'type', 'time', 'channel', 'note', 'velocity'}, 'length': 3}, {'status_byte': 160, 'type': 'polytouch', 'value_names': ('channel', 'note', 'value'), 'attribute_names': {'type', 'time', 'channel', 'note', 'value'}, 'length': 3}, {'status_byte': 176, 'type': 'control_change', 'value_names': ('channel', 'control', 'value'), 'attribute_names': {'type', 'time', 'channel', 'control', 'value'}, 'length': 3}, {'status_byte': 192, 'type': 'program_change', 'value_names': ('channel', 'program'), 'attribute_names': {'channel', 'type', 'program', 'time'}, 'length': 2}, {'status_byte': 208, 'type': 'aftertouch', 'value_names': ('channel', 'value'), 'attribute_names': {'channel', 'type', 'value', 'time'}, 'length': 2}, {'status_byte': 224, 'type': 'pitchwheel', 'value_names': ('channel', 'pitch'), 'attribute_names': {'channel', 'type', 'time', 'pitch'}, 'length': 3}, {'status_byte': 240, 'type': 'sysex', 'value_names': ('data',), 'attribute_names': {'type', 'time', 'data'}, 'length': inf}, {'status_byte': 241, 'type': 'quarter_frame', 'value_names': ('frame_type', 'frame_value'), 'attribute_names': {'type', 'frame_value', 'frame_type', 'time'}, 'length': 2}, {'status_byte': 242, 'type': 'songpos', 'value_names': ('pos',), 'attribute_names': {'type', 'time', 'pos'}, 'length': 3}, {'status_byte': 243, 'type': 'song_select', 'value_names': ('song',), 'attribute_names': {'song', 'type', 'time'}, 'length': 2}, {'status_byte': 246, 'type': 'tune_request', 'value_names': (), 'attribute_names': {'type', 'time'}, 'length': 1}, {'status_byte': 248, 'type': 'clock', 'value_names': (), 'attribute_names': {'type', 'time'}, 'length': 1}, {'status_byte': 250, 'type': 'start', 'value_names': (), 'attribute_names': {'type', 'time'}, 'length': 1}, {'status_byte': 251, 'type': 'continue', 'value_names': (), 'attribute_names': {'type', 'time'}, 'length': 1}, {'status_byte': 252, 'type': 'stop', 'value_names': (), 'attribute_names': {'type', 'time'}, 'length': 1}, {'status_byte': 254, 'type': 'active_sensing', 'value_names': (), 'attribute_names': {'type', 'time'}, 'length': 1}, {'status_byte': 255, 'type': 'reset', 'value_names': (), 'attribute_names': {'type', 'time'}, 'length': 1}]
MThd 6
1 2 480
MTrk 3909
delta 1 0(0x00) 1
Meta message Time signature FF 58 [ 1  3 24  8] 04 01
delta 9 0(0x00) 1
Meta message Key signature FF 59 [0 0] 02 01
delta 15 0(0x00) 1
Meta message Set tempo FF 51 [ 12 183  53] 03 01
delta 22 0(0x00) 1
b0 control_change 3
delta 26 0(0x00) 1
c0 program_change 2
delta 29 0(0x00) 1
b0 control_change 3
delta 33 0(0x00) 1
b0 control_change 3
delta 36 0(0x00) 1
b0 control_change 3
delta 39 0(0x00) 1
b0 control_change 3
delta 42 0(0x00) 1
error metatype:21
delta 47 0(0x00) 1
note_on 90 0 E6 33
delta 51 113(0x71) 1
note_on 90 0 E6 0
delta 54 7(0x07) 1
note_on 90 0 D#6 33
delta 57 113(0x71) 1
note_on 90 0 D#6 0
delta 60 7(0x07) 1

양이 많아서 뒷부분을 생략하였습니다.

앞부분 약간 해석 해보면 다음과 같습니다.

소리를 만들어내는 note_on 만 주목해서 보도록 합니다.

 delta를 누적 해보면 첫번째 detla가 0에서 E6의 소리를 내고 113 delta뒤에 E6을 0으로 소리를 끄도록 되어 있습니다.

즉 note_on -> note_off로 제어하는 방식이 아니라 note_on 볼륨을 줄여서 소리를 끄는 방식을 사용하고 있습니다.


2022년 9월 12일 월요일

python으로 mid file 분석, MThd

 

MThd 분석

앞에서 mid file은 chunk의 연속이라고 하였습니다. 그중에 MThd라고 불리는 chunk가 있는데 헤더를 저장하고 있는 chunk입니다.

내용은 간단하지만 그중에 ticks_per_beat (또는 time division)이라고 불리는 개념이 있어서 설명하려고 합니다.


Header chunk (of a MIDI file)

내용은 아래와 같습니다.

DescriptionLength in bytesStarts at byteValue
chunk ID40x00The character string "MThd"
size40x046
format type20x080, 1, or 2
number of tracks20x0A1 - 65,535
time division20x0CVarious as described below

여기서 생소한 트랙이라는 개념이 나오는데 트랙이라는 건 다른 연주자의 악보(또는 다른 악기의 악보)라고 생각 하면 됩니다. 

format type : 0,1,2인데 0은 트랙이 하나일때 1은 트랙이 여러개 이면서 동기적일때, 2는 트랙이 여러개이나 비동기적으로 play가 될때 사용합니다. 보통은 1이라고 생각하면 됩니다.

number of tracks : 전체 트랙의 숫자 입니다.

time division : 이 값은 4분 음표(4분 음표를 1박자라고 표현)를 여기 에서는 몇개의 틱으로 나타내는지를 나타내는 숫자 입니다.

(여기 값이 480이라고 한다면 나중에 음표를 연주할때 480틱이라는 시간 동안 연주를 하라는 명령이 내려오면 1박자 동안 연주하면 됩니다. )


MidFile 클래스

기존 소스를 변형하여 chunk 단위를 분석할 수 있는 process_chunk() 함수를 만들었습니다.

코드를 약간 정리하였고 주석을 넣었습니다.


소스

import numpy as np


class MidFile:

    # https://faydoc.tripod.com/formats/mid.htm
    # https://github.com/mido/mido/blob/main/mido/midifiles/midifiles.py
    def __init__(self):
        self.ticks_per_beat = None
        self.trackcount = None
        self.type = None

    def process_chunk(self, type, length, body):
        if type == 'MThd':
            ''' 
            * type (2)
              0:single-track
              1:multiple tracks, synchronous
              2:multiple tracks, asynchronous
            * trackcount (2)
            * ticks_per_beat (2)
              is the number of delta-time ticks per quarter note.
              4분 노트(음표)당 델타-타임 틱들의 갯수
              https://www.recordingblogs.com/wiki/header-chunk-of-a-midi-file
              16비트의 상위 비트가 0이면 "박자당 틱"(또는 "4분음표당 펄스(틱수)")입니다. 
              상위 비트가 1이면 시간 분할은 "초당 프레임 수"입니다.
              - BPM = 1분당 bit수 
                120BPM은 1분에 120Bit로 표현 = 4분 음표(1박)가 1분에 120번 나올 수 있음(1번 나오는데 0.5초)
                ticks_per_beat = 480 이면, time delta가 480인 경우 4분 음표로 표현됨  
            '''
            self.type = int.from_bytes(body[0:2].tobytes(), 'big')
            self.trackcount = int.from_bytes(body[2:4].tobytes(), 'big')
            self.ticks_per_beat = int.from_bytes(body[4:6].tobytes(), 'big')
            print(self.type, self.trackcount, self.ticks_per_beat)

    def read(self, filename):
        with open(filename) as f:
            rectype = np.dtype(np.uint8)
            bdata = np.fromfile(f, dtype=rectype)
        # MID 파일은 chunk의 연속으로 되어 있습니다.
        # Chunk Type(4), chunk body length (4), body(chunk body length)
        read_pos = 0
        while True:
            chunk_type = bdata[read_pos:read_pos + 4].tobytes()
            chunk_type = chunk_type.decode('utf-8')
            chunk_length = int.from_bytes(bdata[read_pos + 4:read_pos + 8].tobytes(), 'big')

            print(chunk_type, chunk_length)

            self.process_chunk(chunk_type, chunk_length, bdata[read_pos + 8:read_pos + 8 + chunk_length])

            read_pos = read_pos + chunk_length + 8
            if len(bdata) <= read_pos:
                break


if __name__ == "__main__":
    mid = MidFile()
    mid.read("../00_data/tests_for-elise.mid")


실행결과

MThd 6
1 2 480
MTrk 3909
MTrk 3034




https://signal.vercel.app 이라는 site에 tests_for-elise.mid mid 파일을 올려서 분석해본 내용입니다.
헤더 분석으로 통하여 ticks_per_beat (또는 time division) 값이 480이라는 숫치를 알았는데요
120BPM으로 연주를 하면 (120BPM은 1분에 120bit이니 120bit/60sec = 2 bit/sec 여기서 1bit라는 것은 쿵쿵(120bpm이므로 음악에서 0.5초당 쿵쿵.. )거리는 것을 의미하게 됩니다. 1bit / 0.5sec 가 됩니다. 

1박=1bit=480 이 엘리제를 위하여라는 mid파일에서 설정한 값입니다. 480tick 동안 특정음을 낸다면 그건 한박자 짜리(4분 음표)가 된다는 의미가 됩니다.











python으로 mid file 분석, chunk

0. mid

mid 파일은 일반적인 음악을 재생하기 위한 format입니다. 

보통 음악이라고 하면 mp3나 ogg를 생각 할 수도 있는데 해당 종류의 음악 파일은 wav->mp3, ogg로 변환되어 있는 소리의 시간 샘플링이 파일이 됩니다. 압축 파일을 어떠한 형태로 하느냐에 따라 여러가지 포맷으로 나뉘게 되며 기본적으로는 wave 파일이 기본이 됩니다.

mid 파일의 기본은 피아노 건반의 이벤트를 기록한 파일이라고 생각하면 됩니다. 따라서 wave파일 형태 보다는 용량의 크기가 굉장히 작습니다. 좀 더 쉽게 말하자면 '도레미파솔라시도' 같은 음표를 저장하고 있는 형태라고 생각하면 됩니다.

갑자기 mid 파일을 분석을 시작했냐고 한다면, Machine Learning 작곡 쪽에 관심이 생겨서 살펴보다 보니 이 부분의 정리가 필요하다고 판단이 되었습니다.


mid file spec은 로그인 해야지만 볼 수 있어서 여기에서 사용한 주된 코드는 아래 링크로부터 참고 하였습니다.

https://github.com/mido/mido : python의 mid파일을 읽어오고 재생하는 패키지

http://www.ccarh.org/courses/253/assignment/midifile/ : mid 샘플 분석 자료

https://faydoc.tripod.com/formats/mid.htm : mid 파일 구조


1. chunk 의 이해

많은 종류의 file format에서 일반적으로 사용하는 chunk라는 개념이 있습니다. chunk는 PNG, IFF, MP3 및 AVI와 같은 많은 멀티미디어 파일 형식에서 사용되는 정보의 조각이며, chunk를 사용하는 이유 중의 하나라면 format의 확장성 및 길이의 가변 처리 때문에 많이 사용합니다.

mid 파일도 chunk를 사용하는데 아래와 같은 format을 가집니다.

MThd, MTrk String을 가지는 대표적인 chunk가 있습니다. 더 많은 chunk가 있을 듯 하지만 full spec을 보지 못한 상태이고 많은 곳에서 두개의 트랙만을 이용해서 설명하고 있어서 여기에서도 두개의 chunk분석을 할 예정입니다.


2. chunk 분석

파일을 읽는건 여기에서는 numpy로 한번에 fromfile로 읽었습니다. 보통 mid파일이 크지 않기 때문에 큰 문제는 없습니다. 

numpy로 읽은 데이터는 slice가 가능하기 때문에 실제 코드에서는 다음과 같은 형태를 사용하였습니다. bdata[read_pos:read_pos+4]

그리고 read_pos는 현재 읽는 위치를 의미합니다. 해당 값을 증가시켜 가면서 처리를 해주면 됩니다.

길이를 읽을 때는 4 byte 데이터가 big 엔디언으로 되어있어서 int.from_bytes를 이용해서 변환해서 읽어 주었습니다.

여기에서 사용한 샘플 mid는 엘리제를 위하여 mid 파일입니다.


전체 소스

import numpy as np

filename = "../00_data/tests_for-elise.mid"

with open(filename) as f:
    rectype = np.dtype(np.byte)
    bdata = np.fromfile(f, dtype=rectype)

print(bdata)

# MID 파일은 chunk의 연속으로 되어 있습니다.
# Chunk Type(4), chunk body length (4), body(chunk body length)
read_pos = 0
while True:
    chunk_type = bdata[read_pos:read_pos+4].tobytes()
    chunk_type = chunk_type.decode('utf-8')
    chunk_length = int.from_bytes(bdata[read_pos+4:read_pos+8].tobytes(), 'big')

    print(chunk_type, chunk_length)

    read_pos = read_pos + chunk_length + 8
    if len(bdata) <= read_pos:
        break


실행결과

[ 77  84 104 ...  -1  47   0]
MThd 6
MTrk 3909
MTrk 3034


관련 소스

sourcecode/music/01_mid at main · donarts/sourcecode · GitHub


다음에는 MThd, MTrk chunk를 분석해보도록 하겠습니다.


MThd : https://swlock.blogspot.com/2022/09/python-mid-file-mthd.html

MTrk : https://swlock.blogspot.com/2022/09/python-mid-file-mtrk.html