Как написать Midi-файл с нуля с помощью python

#python #hex #midi

#python #hex #midi

Вопрос:

Я изучаю спецификацию Midi-файла, прямо сейчас я тестирую это, которое отлично работает, если его воспроизводит Timidity, но оно повреждено для Garage Band, OS X (вывод не воспроизводится) и Synthesia.

 head = '4d 54 68 64' 
chunklen = '00 00 00 06'
mformat = '00 01' 
ntracks = '00 02' 
tickdiv = '00 60'
trackid = '4d 54 72 6b'
eot = '00 ff 2f 00'

makeheader = lambda : " ".join([head,chunklen,mformat,ntracks,tickdiv])

def chunklencalc(notes):
    chlen = format(len(notes)*4, 'x')
    return " ".join([x for x in re.compile('(.{2})').split("00000000"[len(chlen):]   chlen) if x != ''])

maketrack = lambda notes : " ".join([trackid, chunklencalc(notes)]   notes   [eot])

makestandardquarter = lambda root : f"00 90 {root} 64 60 80 {root} 64"

def createMidi(filename,bytelist):
    with open(filename, 'wb') as f:
        for e in bytelist.split(" "):
            f.write(bytes.fromhex(e))


filename = 'firsttest.mid'
head = makeheader()
notes1 =[
    makestandardquarter('3c'),
    makestandardquarter('3c'),
    makestandardquarter('3c'),
    makestandardquarter('3c'),
    makestandardquarter('3c'),
    makestandardquarter('3c'),
    makestandardquarter('3c'),
    makestandardquarter('3c'),
]
notes2 =[
    makestandardquarter('40'),
    makestandardquarter('40'),
    makestandardquarter('40'),
    makestandardquarter('40'),
    makestandardquarter('40'),
    makestandardquarter('40'),
    makestandardquarter('40'),
    makestandardquarter('40'),
]
track1 = maketrack(notes1)
track2 = maketrack(notes2)

createMidi(filename, " ".join([head, track1,track2]))
 

Я ожидал серию четвертей в двух дорожках, получил только первые четыре только на одной дорожке.

Комментарии:

1. вы проверили, имеет ли файл правильную конечность? глядя на спецификацию, она должна быть маленькой.

2. Меня беспокоит вычисление chunklen и функции, которая создает заметку, потому что в предыдущем тесте я вручную рассчитал и жестко запрограммировал ноты, и это работает в каждой системе. Также кажется странным, что обрабатывается только половина нот. Наконец, я не знаком с такого рода низкоуровневыми материалами, любая помощь приветствуется.

3. Также в функции chunklen, я думаю, должно быть 4 для сегмента eot (4 байта), но робость не жалуется.

4. для работы с байтами я счел полезным использовать модуль struct python: docs.python.org/3/library/struct.html Помните, что вы можете определить двоичные строки следующим образом: b"x00"

Ответ №1:

После глубокого изучения hexdump и просмотра определенных длин блоков: первый блок объявляется 0x20 длиной (32) байта, начиная с позиции 0x17 (23) и заканчивается на 0x5b (91), что означает, что ваши вычисления длины блока отклонены на 34 байта.

 00000000  4d 54 68 64 00 00 00 06  00 01 00 02 00 60 4d 54  |MThd.........`MT|
00000010  72 6b 00 00 00 20 00 90  3c 64 60 80 3c 64 00 90  |rk... ..<d`.<d..|
00000020  3c 64 60 80 3c 64 00 90  3c 64 60 80 3c 64 00 90  |<d`.<d..<d`.<d..|
*
00000050  3c 64 60 80 3c 64 00 ff  2f 00 4d 54 72 6b 00 00  |<d`.<d../.MTrk..|
00000060  00 20 00 90 40 64 60 80  40 64 00 90 40 64 60 80  |. ..@d`.@d..@d`.|
00000070  40 64 00 90 40 64 60 80  40 64 00 90 40 64 60 80  |@d..@d`.@d..@d`.|
*
000000a0  40 64 00 ff 2f 00                                 |@d../.|
000000a6
 

Я написал свою собственную версию, используя struct:

 import struct

HEAD_ID = b"x4dx54x68x64"
TRACK_ID = b"x4dx54x72x6b"

class HeaderChunk:
    def __init__(self, format, ntrack, tickdiv):
        self.format = format
        self.ntrack = ntrack
        self.tickdiv = tickdiv

    def dump(self):
        payload = struct.pack(">HH2s", self.format, self.ntrack, self.tickdiv)
        header = HEAD_ID   struct.pack(">I", len(payload))
        return header   payload


class TrackChunk:
    """Represents a track"""
    def __init__(self):
        self.data = b""
    def quarter(self, note):
        self.data  = b"x00x90"   note   b"x64x60x80"   note   b"x64"

    def dump(self):
        header = TRACK_ID   struct.pack(">I", len(self.data))
        return header   self.data


header = HeaderChunk(1, 2, b"x00x60")

first_track = TrackChunk()
for _ in range(8):
    first_track.quarter(b"x3c")

second_track = TrackChunk()
for _ in range(8):
    second_track.quarter(b"x40")


with open("joac-example.mid", "wb") as output:
    output.write(header.dump())
    output.write(first_track.dump())
    output.write(second_track.dump())
 

Он правильно загружен на garage band