Моя попытка стать разработчиком Кайрэндии

Обсуждение технических вопросов (проблемы с запуском игры, баги, глюки, баглюки)

Моя попытка стать разработчиком Кайрэндии

Сообщение bckpkol » 19 апр 2017, 16:51

Код: Выделить всё
import os,sys,struct,re
if len(sys.argv)!=3:
  sys.exit('usage: py unpak.py [C:\path\]filename.pak [C:\path]folder')
stream=open(sys.argv[1],'rb')
pak=stream.read()
stream.close()
header=pak[:struct.unpack('<I',pak[:4])[0]]
regexp=re.compile(b'(.{4})([^\x00]*)\x00',re.S)
files=regexp.findall(header)
if not os.path.exists(sys.argv[2]):
    os.makedirs(sys.argv[2])
for file in range(len(files)-1):
    f=open(os.path.join(sys.argv[2],files[file][1].decode("utf-8")),"wb")
    f.write(pak[struct.unpack('<I',files[file][0])[0]:struct.unpack('<I',files[file+1][0])[0]])
    f.close()

Короче, ставим python 3, копипастим в файл, запускаем и радуемся :) Упаковщик на очереди.
Последний раз редактировалось bckpkol 19 апр 2017, 17:43, всего редактировалось 1 раз.
Аватара пользователя
bckpkol
(3) Столичный горожанин
 
Сообщения: 199
Зарегистрирован: 02 янв 2011, 20:48
Откуда: город Бийск
Любимая часть Кирандии: Кирандия 3 и 4
Любимые персонажи Кирандии: Дарм, Зантия
Почему Вы любите Легенду о Кирандии?: За простоту, удобство интерфейса и лёгкую проходимость.

Re: Моя попытка стать разработчиком Кайрэндии

Сообщение bckpkol » 19 апр 2017, 17:39

А вот и упаковщик.
Код: Выделить всё
import glob,sys,os,struct
if len(sys.argv)!=3:
  sys.exit('usage: py pak.py [C:\path\]*.* [C:\path\]filename.pak')
pak=dict()
for file in glob.glob(sys.argv[1]):
    f=open(file,'rb')
    pak[os.path.split(file)[-1].upper()]=f.read()
    f.close()
start=len(pak)*5+sum((len(filename) for filename in pak.keys()))+9
header=b''
body=b''
for file in pak.keys():
    body+=pak[file]
    header+=struct.pack('<I',start)+file.encode('utf-8')+b'\x00'
    start+=len(pak[file])
header+=struct.pack('<I',start)+b'\x00\x00\x00\x00\x00'
stream=open(sys.argv[2],'wb')
stream.write(header)
stream.write(body)
stream.close()
Последний раз редактировалось bckpkol 20 апр 2017, 09:20, всего редактировалось 2 раз(а).
Аватара пользователя
bckpkol
(3) Столичный горожанин
 
Сообщения: 199
Зарегистрирован: 02 янв 2011, 20:48
Откуда: город Бийск
Любимая часть Кирандии: Кирандия 3 и 4
Любимые персонажи Кирандии: Дарм, Зантия
Почему Вы любите Легенду о Кирандии?: За простоту, удобство интерфейса и лёгкую проходимость.

Re: Моя попытка стать разработчиком Кайрэндии

Сообщение bckpkol » 19 апр 2017, 21:43

Конвертер voc файлов из Кайрэндии 1 в wav. Ждите обратную конвертацию.
Код: Выделить всё
import os,sys,struct
if len(sys.argv)!=3:
    sys.exit('usage: py voc2wav.py [C:\path\]filename.voc [C:\path]filename.wav')
stream=open(sys.argv[1],'rb')
voc=stream.read()
stream.close()
start=struct.unpack('<H',voc[20:22])[0]
freq=21739
wave=b''
while start<len(voc):
    if voc[start]==0:
        break
    elif voc[start]==1:
        size=struct.unpack('<I',voc[start+1:start+4]+b'\x00')[0]
        start+=4
        freq=int(1000000/(256-voc[start]))
        if voc[start+1]!=0:
            sys.exit('unknown compression')
        wave+=voc[start+2:start+size]
        start+=size
    else:
        sys.exit('error')
if sys.argv[2]=="-":
    wav=sys.stdout.buffer
else:
    wav=open(sys.argv[2],'wb')
wav.write(b'RIFF')
wav.write(struct.pack('<I',36+len(wave)))
wav.write(b'WAVEfmt\x20\x10\x00\x00\x00\01\00\01\00')
wav.write(struct.pack('<I',freq))
wav.write(struct.pack('<I',freq))
wav.write(struct.pack('<H',1))
wav.write(struct.pack('<H',8))
wav.write(b'data')
wav.write(struct.pack('<I',len(wave)))
wav.write(wave)
if sys.argv[2]!="-":
    wav.close()

Отредактировал, чтобы правильно записывал длину файла.
Отредактировал ещё раз.
Добавляю: вот обратный конвертер. Работу не проверял уже проверил. Файлы перепаковываются нормально.
Код: Выделить всё
import os,sys,struct
if len(sys.argv)!=3:
    sys.exit('usage: py wav2voc.py [C:\path\]filename.wav [C:\path]filename.voc')
if sys.argv[1]=='-':
    stream=sys.stdin.buffer
else:
    stream=open(sys.argv[1],'rb')
if stream.read(4)!=b'RIFF':
    sys.exit('not valid wav format')
expect=struct.unpack('<I',stream.read(4))[0]
if stream.read(8)!=b'WAVEfmt\x20':
    sys.exit('failed')
chunksize=struct.unpack('<I',stream.read(4))[0]
chunk=stream.read(chunksize)
fmt=struct.unpack('<H',chunk[0:2])[0]
ch=struct.unpack('<H',chunk[2:4])[0]
freq=struct.unpack('<I',chunk[4:8])[0]
rate=struct.unpack('<I',chunk[8:12])[0]
ba=struct.unpack('<H',chunk[12:14])[0]
bps=struct.unpack('<H',chunk[14:16])[0]
if fmt!=1:
    sys.exit('not a PCM')
if ch!=1:
    sys.exit('not mono')
if bps!=8:
    sys.exit('16 bit yet not supported')
if rate!=freq*ba or ba!=ch*bps/8:
    sys.exit('file corrupted')
if stream.read(4)!=b'data':
    sys.exit('failed')
datasize=struct.unpack('<I',stream.read(4))[0]
expect-=20+chunksize
expect=min(expect,datasize)
data=stream.read(expect)
if sys.argv[2]=="-":
    voc=sys.stdout.buffer
else:
    voc=open(sys.argv[2],'wb')
voc.write(b'Creative Voice File\x1a\x1a\x00\x0a\x01\x29\x11')
chunkstart=0
while True:
    chunk=data[chunkstart:chunkstart+16777215]
    if len(chunk)==0:
        break
    voc.write(b'\x01'+struct.pack('<I',len(chunk)+2)[:3]+struct.pack('<B',round(256-(1000000/freq)))+b'\x00')
    voc.write(chunk)
    chunkstart+=16777215
voc.write(b'\x00')
if sys.argv[2]!="-":
    voc.close()
if sys.argv[1]!='-':
    stream.close()
Последний раз редактировалось bckpkol 22 апр 2017, 18:45, всего редактировалось 3 раз(а).
Аватара пользователя
bckpkol
(3) Столичный горожанин
 
Сообщения: 199
Зарегистрирован: 02 янв 2011, 20:48
Откуда: город Бийск
Любимая часть Кирандии: Кирандия 3 и 4
Любимые персонажи Кирандии: Дарм, Зантия
Почему Вы любите Легенду о Кирандии?: За простоту, удобство интерфейса и лёгкую проходимость.

Re: Моя попытка стать разработчиком Кайрэндии

Сообщение bckpkol » 20 апр 2017, 09:13

Альтернативный распаковщик для vrm, преобразует voc в wav.
Код: Выделить всё
import os,sys,struct,re
if len(sys.argv)!=3:
    sys.exit('usage: py unvrm.py [C:\path\]filename.vrm [C:\path]folder')
stream=open(sys.argv[1],'rb')
pak=stream.read()
stream.close()
header=pak[:struct.unpack('<I',pak[:4])[0]]
regexp=re.compile(b'(.{4})([^\x00]*)\x00',re.S)
files=regexp.findall(header)
if not os.path.exists(sys.argv[2]):
    os.makedirs(sys.argv[2])
for file in range(len(files)-1):
    if files[file][1].upper().endswith(b'.VOC'):
        wav=open(os.path.join(sys.argv[2],(files[file][1][:-3]+b'WAV').decode("utf-8")),"wb")
        voc=pak[struct.unpack('<I',files[file][0])[0]:struct.unpack('<I',files[file+1][0])[0]]
        start=struct.unpack('<H',voc[20:22])[0]
        freq=21739
        wave=b''
        while start<len(voc):
            if voc[start]==0:
                break
            elif voc[start]==1:
                size=struct.unpack('<I',voc[start+1:start+4]+b'\x00')[0]
                start+=4
                freq=int(1000000/(256-voc[start]))
                if voc[start+1]!=0:
                    sys.exit('unknown compression')
                wave+=voc[start+2:start+size]
                start+=size
            else:
                sys.exit('error')
        wav.write(b'RIFF')
        wav.write(struct.pack('<I',36+len(wave)))
        wav.write(b'WAVEfmt\x20\x10\x00\x00\x00\01\00\01\00')
        wav.write(struct.pack('<I',freq))
        wav.write(struct.pack('<I',freq))
        wav.write(struct.pack('<H',1))
        wav.write(struct.pack('<H',8))
        wav.write(b'data')
        wav.write(struct.pack('<I',len(wave)))
        wav.write(wave)
        wav.close()
    else:
        f=open(os.path.join(sys.argv[2],files[file][1].decode("utf-8")),"wb")
        f.write(pak[struct.unpack('<I',files[file][0])[0]:struct.unpack('<I',files[file+1][0])[0]])
        f.close()
Аватара пользователя
bckpkol
(3) Столичный горожанин
 
Сообщения: 199
Зарегистрирован: 02 янв 2011, 20:48
Откуда: город Бийск
Любимая часть Кирандии: Кирандия 3 и 4
Любимые персонажи Кирандии: Дарм, Зантия
Почему Вы любите Легенду о Кирандии?: За простоту, удобство интерфейса и лёгкую проходимость.

Re: Моя попытка стать разработчиком Кайрэндии

Сообщение bckpkol » 20 апр 2017, 09:47

Код: Выделить всё
import glob,sys,os,struct,io,audioop
if len(sys.argv)!=3:
  sys.exit('usage: py vrm.py [C:\path\]*.* [C:\path\]filename.vrm')
pak=dict()
for file in glob.glob(sys.argv[1]):
    f=open(file,'rb')
    pak[os.path.split(file)[-1].upper()]=f.read()
    f.close()
start=len(pak)*5+sum((len(filename) for filename in pak.keys()))+9
header=b''
body=b''
noise_called=False
def noise():
    global noise_called
    if noise_called:
       noise.called=False
       return 1
    else:
       noise.called=True
       return -1
for file in pak.keys():
    if file.endswith(".WAV"):
        stream=io.BytesIO(pak[file])
        if stream.read(4)!=b'RIFF':
            sys.exit('not valid wav format')
        expect=struct.unpack('<I',stream.read(4))[0]
        if stream.read(8)!=b'WAVEfmt\x20':
            sys.exit('failed')
        chunksize=struct.unpack('<I',stream.read(4))[0]
        chunk=stream.read(chunksize)
        fmt=struct.unpack('<H',chunk[0:2])[0]
        ch=struct.unpack('<H',chunk[2:4])[0]
        freq=struct.unpack('<I',chunk[4:8])[0]
        rate=struct.unpack('<I',chunk[8:12])[0]
        ba=struct.unpack('<H',chunk[12:14])[0]
        bps=struct.unpack('<H',chunk[14:16])[0]
        if fmt!=1:
            sys.exit('not a PCM')
        if ch!=1:
            sys.exit('not mono')
        if bps not in (8,16,24,32):
            sys.exit('bit not supported')
        if rate!=freq*ba or ba!=ch*bps/8:
            sys.exit('file corrupted')
        if stream.read(4)!=b'data':
            sys.exit('failed')
        datasize=struct.unpack('<I',stream.read(4))[0]
        expect-=20+chunksize
        expect=min(expect,datasize)
        if bps!=8:
            data=stream.read(divmod(expect,2)[0]*2)
            data=audioop.lin2lin(data,round(bps/8),1)
            data=audioop.bias(data, 1, 128)
        else:
            data=stream.read(expect)
        voc=b'Creative Voice File\x1a\x1a\x00\x0a\x01\x29\x11'
        chunkstart=0
        while True:
            chunk=data[chunkstart:chunkstart+16777215]
            if len(chunk)==0:
                break
            voc+=b'\x01'+struct.pack('<I',len(chunk)+2)[:3]+struct.pack('<B',round(256-(1000000/freq)))+b'\x00'
            voc+=chunk
            chunkstart+=16777215
        voc+=b'\x00'
        body+=voc
        header+=struct.pack('<I',start)+(file[:-3]+"VOC").encode('utf-8')+b'\x00'
        start+=len(voc)
    else:
        body+=pak[file]
        header+=struct.pack('<I',start)+file.encode('utf-8')+b'\x00'
        start+=len(pak[file])
header+=struct.pack('<I',start)+b'\x00\x00\x00\x00\x00'
stream=open(sys.argv[2],'wb')
stream.write(header)
stream.write(body)
stream.close()

Вот. Рабочий vrm упаковщик.
Добавлено: да, рабочий, но только для 8bit wav. Стандарт де-факто 16 бит не только не поддерживается, но даже не проверяется. Второе исправил, первое ждите.
Добавлено: полностью рабочий вариант, тестовый звуковой файл слышно в ScummVM и DOSBox. Правда, в последнем 48000Hz семпл малость трещит. Можно добавить Kaiser filter из resampy, или преобразовывать вручную. Первое удобней, но в конце выходит щелчок, который нужно обрезать.
Добавлено: работает лучше, чем думал. Один файл был float, и программа выдала not a pcm. А я-то думал, как проверять на float...
Последний раз редактировалось bckpkol 23 апр 2017, 14:04, всего редактировалось 5 раз(а).
Аватара пользователя
bckpkol
(3) Столичный горожанин
 
Сообщения: 199
Зарегистрирован: 02 янв 2011, 20:48
Откуда: город Бийск
Любимая часть Кирандии: Кирандия 3 и 4
Любимые персонажи Кирандии: Дарм, Зантия
Почему Вы любите Легенду о Кирандии?: За простоту, удобство интерфейса и лёгкую проходимость.

Re: Моя попытка стать разработчиком Кайрэндии

Сообщение bckpkol » 20 апр 2017, 11:00

Малость оффтопик:
Код: Выделить всё
Как бы выпилить тебя?
Ой-ой, может, эти зубы на пиле можно выправить.
Попытка не пытка.
Выправил.
Камни.
Куча камней.
Зачем я трогаю их?
Разве могут они помочь?
Брендон?
Что?
Чьи это слова?
Ты ли Брэндон, внук Каллака?
Да я.
Что ты?
Мы из Другого Царства.
Мы голос Земли.
Зачем бы Земле обращаться ко мне?
Мы гибнем, Брендон.
Волшебство злонамеренно нарушило равновесие природы.
Вы могли бы сами со злом бороться.
Зло есть только в Царстве Людей.
Земля зла не знала,..
...и потому, она беззащитна.
Но-- как зло проникло к вам?
Волшебный Кайрацвет, знак доверия между нашими царствами, разрушен.
Теперь, он больше нас не защитит.
Ты, Брендон, должен встать за наш род.
Что!?
Но я...
Ты наш избранник, Потомок Каллака.
Судьба твоя была известна ещё до рождения.
Знать бы мне, с чего мне начать.
Твоя вера знает, Брендон.
Готовься к дороге.
Но!
Как насчёт--
OUCH.WSA
Что случилось с моим дедом?
Что-нибудь там ёще?
Думал, там мои босоножки.
Яблоко!
Снова здорово.
Упало в вазу!
По моему, там уже есть что-то.
Дерево право.
Земля гибнет.
Много гнилушек видно отсюда.
Брин, Брин!
Сюда!
Ты меня не слышишь.
Я должен сам прийти.
Положить ли деда в постель?
Вряд ли можно его как-то сдвинуть.
Нельзя брать, деду нужен свет.
Книжный Клуб Мистиков.
Милая обложка, но гадкое чтиво.
Пила деда.
Не собираюсь таскаться с этим!
Вряд ли он может жевать.
RANDPA.WSA
Дед, ты меня слышишь?
Письмо.
Есть там что-нибудь?
Дом Брендона
Дед!
Что стало с тобой?!?
Если б магия могла вылечить деда...
Прости, дед...
Жаль, это не вышло.
Последний раз редактировалось bckpkol 23 апр 2017, 12:45, всего редактировалось 4 раз(а).
Аватара пользователя
bckpkol
(3) Столичный горожанин
 
Сообщения: 199
Зарегистрирован: 02 янв 2011, 20:48
Откуда: город Бийск
Любимая часть Кирандии: Кирандия 3 и 4
Любимые персонажи Кирандии: Дарм, Зантия
Почему Вы любите Легенду о Кирандии?: За простоту, удобство интерфейса и лёгкую проходимость.

Re: Моя попытка стать разработчиком Кайрэндии

Сообщение Reflector » 20 апр 2017, 20:30

Здорово, только почему python?
Reflector
(2) Житель Милтонии
 
Сообщения: 158
Зарегистрирован: 11 сен 2010, 16:44
Любимая часть Кирандии: 2
Любимые персонажи Кирандии: Занция
Почему Вы любите Легенду о Кирандии?: Ностальгия...

Re: Моя попытка стать разработчиком Кайрэндии

Сообщение bckpkol » 22 апр 2017, 16:31

Почему Python, Reflector? Это язык, на котором написаны скрипты Blender, он известен мне лучше всего, и его намного легче отлаживать, чем C++ (я так и не научился работать с CDB), и компилять не нужно. Плюс куча доступных библиотек.
Аватара пользователя
bckpkol
(3) Столичный горожанин
 
Сообщения: 199
Зарегистрирован: 02 янв 2011, 20:48
Откуда: город Бийск
Любимая часть Кирандии: Кирандия 3 и 4
Любимые персонажи Кирандии: Дарм, Зантия
Почему Вы любите Легенду о Кирандии?: За простоту, удобство интерфейса и лёгкую проходимость.

Re: Моя попытка стать разработчиком Кайрэндии

Сообщение bckpkol » 22 апр 2017, 16:49

Пакет batteries included для воспроизведения xmi и mid файлов. Программа - midplay.py, слегка подредактированная библиотека - fluidsynth.py.
Правда, midplay тоже можно использовать как библиотеку.
Использование - запускать с расположением программы как рабочей папкой, команда "python midplay.py TimGM6mb.sf2 jesu.mid 0" запустит трек 0 файла jesu.mid с банком TimGM6mb.sf2.
Если опустить трек, проигрывание будет последовательным.
Для тех, кто ещё не понял: "python midplay.py TimGM6mb.sf2 INTRO.XMI 2" запустит воспроизведение заставки Кайрандии.
https://yadi.sk/d/PGBgnIHF3HEm2u
Добавлено: исправил ошибку при чтении meta и sysex событий длиннее 127. Я не знал, что длина измеряется в concat_7bit единицах. Ещё убрал старый workaround. Ждите конвертер.
Добавлено: забыл сказать, что теперь есть ключ "-file=audio.flac".
Последний раз редактировалось bckpkol 24 апр 2017, 14:31, всего редактировалось 2 раз(а).
Аватара пользователя
bckpkol
(3) Столичный горожанин
 
Сообщения: 199
Зарегистрирован: 02 янв 2011, 20:48
Откуда: город Бийск
Любимая часть Кирандии: Кирандия 3 и 4
Любимые персонажи Кирандии: Дарм, Зантия
Почему Вы любите Легенду о Кирандии?: За простоту, удобство интерфейса и лёгкую проходимость.

Re: Моя попытка стать разработчиком Кайрэндии

Сообщение bckpkol » 22 апр 2017, 17:23

Заметил, что при чтении xmi файлов треки - генераторы, а mid - списки. Должны быть списки в любом случае, иначе прочитать их можно будет только один раз.
Пока что не исправил. Вот надпрограмма, считающая размер и количество треков:
Код: Выделить всё
import sys
from midplay import MidiFile
if len(sys.argv)!=2:
    sys.exit('usage: py middir.py [C:\path]filename.mid')
midi=MidiFile(sys.argv[1])
for num, track in enumerate(midi):
    print('Track:',num,'messages:',len(list(track)))

Можете заменить
Код: Выделить всё
                        self.append(xmi_decode(container))

на
Код: Выделить всё
                        self.append(list(xmi_decode(container)))

Добавлено: пока исправил всё, что нашёл.
Последний раз редактировалось bckpkol 24 апр 2017, 15:14, всего редактировалось 1 раз.
Аватара пользователя
bckpkol
(3) Столичный горожанин
 
Сообщения: 199
Зарегистрирован: 02 янв 2011, 20:48
Откуда: город Бийск
Любимая часть Кирандии: Кирандия 3 и 4
Любимые персонажи Кирандии: Дарм, Зантия
Почему Вы любите Легенду о Кирандии?: За простоту, удобство интерфейса и лёгкую проходимость.

Re: Моя попытка стать разработчиком Кайрэндии

Сообщение bckpkol » 23 апр 2017, 14:30

Код: Выделить всё
import glob,sys,os,struct,io,audioop,numpy,resampy
if len(sys.argv)!=3:
  sys.exit('usage: py vrm.py [C:\path\]*.* [C:\path\]filename.vrm')
pak=dict()
for file in glob.glob(sys.argv[1]):
    f=open(file,'rb')
    pak[os.path.split(file)[-1].upper()]=f.read()
    f.close()
start=len(pak)*5+sum((len(filename) for filename in pak.keys()))+9
header=b''
body=b''
noise_called=False
def noise():
    global noise_called
    if noise_called:
       noise.called=False
       return 1
    else:
       noise.called=True
       return -1
for file in pak.keys():
    if file.endswith(".WAV"):
        stream=io.BytesIO(pak[file])
        if stream.read(4)!=b'RIFF':
            sys.exit('not valid wav format')
        expect=struct.unpack('<I',stream.read(4))[0]
        if stream.read(8)!=b'WAVEfmt\x20':
            sys.exit('failed')
        chunksize=struct.unpack('<I',stream.read(4))[0]
        chunk=stream.read(chunksize)
        fmt=struct.unpack('<H',chunk[0:2])[0]
        ch=struct.unpack('<H',chunk[2:4])[0]
        freq=struct.unpack('<I',chunk[4:8])[0]
        rate=struct.unpack('<I',chunk[8:12])[0]
        ba=struct.unpack('<H',chunk[12:14])[0]
        bps=struct.unpack('<H',chunk[14:16])[0]
        if fmt!=1:
            sys.exit('not a PCM')
        if ch!=1:
            sys.exit('not mono')
        if bps not in (8,16,24,32):
            sys.exit('bit not supported')
        if rate!=freq*ba or ba!=ch*bps/8:
            sys.exit('file corrupted')
        if stream.read(4)!=b'data':
            sys.exit('failed')
        datasize=struct.unpack('<I',stream.read(4))[0]
        expect-=20+chunksize
        expect=min(expect,datasize)
        if bps!=8:
            data=stream.read(divmod(expect,2)[0]*2)
            data=audioop.lin2lin(data,round(bps/8),1)
            data=audioop.bias(data, 1, 128)
            expect=len(data)
        else:
            data=stream.read(expect)
        if freq!=21739:
            data=numpy.array([num/255.0 for num in data]+[num/255.0 for num in data[-1:]*freq],dtype=numpy.float)
            data=resampy.resample(data,freq,21739)
            data=bytes((min(int(num),255) for num in (data*255.9).astype(int)))[:int(expect*21739/freq)]
        voc=b'Creative Voice File\x1a\x1a\x00\x0a\x01\x29\x11'
        chunkstart=0
        while True:
            chunk=data[chunkstart:chunkstart+16777215]
            if len(chunk)==0:
                break
            voc+=b'\x01'+struct.pack('<I',len(chunk)+2)[:3]+struct.pack('<B',round(256-(1000000/21739)))+b'\x00'
            voc+=chunk
            chunkstart+=16777215
        voc+=b'\x00'
        body+=voc
        header+=struct.pack('<I',start)+(file[:-3]+"VOC").encode('utf-8')+b'\x00'
        start+=len(voc)
    else:
        body+=pak[file]
        header+=struct.pack('<I',start)+file.encode('utf-8')+b'\x00'
        start+=len(pak[file])
header+=struct.pack('<I',start)+b'\x00\x00\x00\x00\x00'
stream=open(sys.argv[2],'wb')
stream.write(header)
stream.write(body)
stream.close()

Версия с конвертером частоты.
Добавлено: на Шindoфs требует VC14. Поставьте linux или пользуйтесь предыдущей версией.
Последний раз редактировалось bckpkol 24 апр 2017, 16:13, всего редактировалось 1 раз.
Аватара пользователя
bckpkol
(3) Столичный горожанин
 
Сообщения: 199
Зарегистрирован: 02 янв 2011, 20:48
Откуда: город Бийск
Любимая часть Кирандии: Кирандия 3 и 4
Любимые персонажи Кирандии: Дарм, Зантия
Почему Вы любите Легенду о Кирандии?: За простоту, удобство интерфейса и лёгкую проходимость.

Re: Моя попытка стать разработчиком Кайрэндии

Сообщение bckpkol » 24 апр 2017, 14:58

Код: Выделить всё
import sys,itertools,struct
from collections import namedtuple,deque
from operator import attrgetter
from midplay import MidiFile
from midplay import Controller,Instrument,Bank
from midplay import ChannelLockUnlock,ChannelLockProtect
from midplay import Pressure,Transpose,SysEx
from midplay import MetaText,MetaEoT,MetaTempo,MetaVendor,Meta
from midplay import NoteOn,NoteOff,NotePressure
RawMsg=namedtuple('RawMsg', ['data','starttick'])
def get_timb(notation):
    timb=dict()
    instrument=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
    bank=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
    for msg in notation:
        if isinstance(msg,Instrument):
            instrument[msg.channel]=msg.instrument
            if msg.bank!=0:
                bank[msg.channel]=msg.bank
            if instrument[msg.channel] not in timb or timb.get(instrument[msg.channel],0)==0:
                timb.update({instrument[msg.channel]:bank[msg.channel]})
        elif isinstance(msg,Bank):
            bank[msg.channel]=msg.bank
            if timb.get(instrument[msg.channel],0)==0:
                timb.update({instrument[msg.channel]:bank[msg.channel]})
    return timb
def gen_concat_7bit(num):
    ret=b''
    while num>127:
        ret+=bytes([(num>>num.bit_length()//7*7)|128])
        num%=2**(num.bit_length()//7*7)
    ret+=bytes([num])
    return ret
def gen_sum_7bit(num):
    ret=b''
    while num>127:
        ret+=b'\x7f'
        num-=127
    ret+=bytes([num])
    if ret==b'\x00':
        return b''
    return ret
def get_raw_delay(num,xmi):
    if xmi:
        return gen_sum_7bit(num)
    return gen_concat_7bit(num)
def check_xmi_compat(midi):
    TPQN=midi.TPQN
    found=False
    for track in midi:
        for msg in track:
            if isinstance(msg,MetaTempo):
                found=True
                if round(TPQN*1000000/msg.tempo)==120:
                    return True
    if not found and TPQN==60:
        return True
    return False
def prepare_notation_compression(notation,xmi=False):
    onnotes=set()
    for msg in notation:
        if isinstance(msg,NoteOn):
            if xmi:
                onnotes.add(msg)
            else:
                msgtype=0x90|msg.channel
                note=msg.note
                velocity=msg.velocity
                yield RawMsg(bytes([msgtype,note,velocity]),msg.starttick)
        elif isinstance(msg,NoteOff):
            if xmi:
                for onnote in sorted(onnotes,key=attrgetter('starttick')):
                    msgtype=0x90|onnote.channel
                    note=onnote.note
                    velocity=onnote.velocity
                    if note!=msg.note or onnote.channel!=msg.channel:
                        continue
                    onnotes.remove(onnote)
                    yield RawMsg(bytes([msgtype,note,velocity])+gen_concat_7bit(msg.starttick-onnote.starttick),onnote.starttick)
                    break
            else:
                msgtype=0x80|msg.channel
                note=msg.note
                velocity=msg.velocity
                yield RawMsg(bytes([msgtype,note,velocity]),msg.starttick)
        elif isinstance(msg,NotePressure):
            msgtype=0xA0|msg.channel
            note=msg.note
            pressure=msg.pressure
            yield RawMsg(bytes([msgtype,pressure,note]),msg.starttick)
        elif isinstance(msg,Pressure):
            msgtype=0xD0|msg.channel
            pressure=msg.pressure
            yield RawMsg(bytes([msgtype,pressure]),msg.starttick)
        elif isinstance(msg,Transpose):
            msgtype=0xE0|msg.channel
            transpose=msg.transpose
            yield RawMsg(bytes([msgtype,transpose&127,transpose>>7]),msg.starttick)
        elif isinstance(msg,Controller):
            msgtype=0xB0|msg.channel
            controller=msg.controller
            value=msg.value
            yield RawMsg(bytes([msgtype,controller,value]),msg.starttick)
        elif isinstance(msg,Instrument):
            msgtype=0xC0|msg.channel
            instrument=msg.instrument
            bank=msg.bank
            yield RawMsg(bytes([msgtype,instrument]),msg.starttick)
            if not xmi:
                yield RawMsg(bytes([0xB0|msg.channel,114,bank]),msg.starttick)
        elif isinstance(msg,Bank):
            msgtype=0xB0|msg.channel
            controller=114
            value=msg.bank
            if not xmi:
                yield RawMsg(bytes([msgtype,controller,value]),msg.starttick)
        elif isinstance(msg,SysEx):
            msgtype=0xF0|msg.type
            data=msg.data
            yield RawMsg(bytes([msgtype])+gen_concat_7bit(len(data))+data,msg.starttick)
        elif isinstance(msg,MetaText):
            msgtype=0xFF
            metatype=msg.type
            data=msg.data
            yield RawMsg(bytes([msgtype,metatype])+gen_concat_7bit(len(data))+data,msg.starttick)
        elif isinstance(msg,MetaTempo):
            msgtype=0xFF
            metatype=0x51
            tempo=msg.tempo
            yield RawMsg(bytes([msgtype,metatype])+gen_concat_7bit(3)+struct.pack('>I',tempo)[1:],msg.starttick)
        elif isinstance(msg,MetaVendor):
            msgtype=0xFF
            metatype=0x7f
            vendor=msg.vendor
            yield RawMsg(bytes([msgtype,metatype])+gen_concat_7bit(len(vendor))+vendor,msg.starttick)
        elif isinstance(msg,Meta):
            msgtype=0xFF
            metatype=msg.type
            data=msg.data
            yield RawMsg(bytes([msgtype,metatype])+gen_concat_7bit(len(data))+data,msg.starttick)
        elif isinstance(msg,ChannelLockUnlock):
            msgtype=0xB0|msg.channel
            controller=110
            value=127
            yield RawMsg(bytes([msgtype,controller,value]),msg.starttick)
        elif isinstance(msg,ChannelLockProtect):
            msgtype=0xB0|msg.channel
            controller=111
            value=127
            yield RawMsg(bytes([msgtype,controller,value]),msg.starttick)
    yield RawMsg(bytes([0xFF,0x2F,0x00]),msg.starttick)
def compress_notation(notation,xmi=False):
    data=b''
    starttick=0
    delay=0
    for msg in sorted(prepare_notation_compression(notation,xmi),key=attrgetter('starttick')):
        delay,starttick=msg.starttick-starttick,msg.starttick
        data+=get_raw_delay(delay,xmi)+msg.data
    return data
if __name__=='__main__':
    if len(sys.argv)!=3:
        sys.exit('usage: py writemidtype2.py [C:\path]filename.mid [C:\path]filename.mid')
    midi=MidiFile(sys.argv[1])
#print("XMI compatible:",check_xmi_compat(midi))
#for num, track in enumerate(midi):
#    print(("Track: "+str(num)).rjust(80,"-"))
#    print(compress_notation(track))
#    print(compress_notation(track,True))
    raw=b'MThd\x00\x00\x00\x06'+struct.pack('>H',2)+struct.pack('>H',len(midi))+struct.pack('>h',midi.TPQN)
    for track in midi:
        rawTrack=compress_notation(track)
        raw+=b'MTrk'+struct.pack('>I',len(rawTrack))+rawTrack
#print(raw)
    f=open(sys.argv[2],'wb')
    f.write(raw)
    f.close()
#    for msg in prepare_notation_compression(track):
#        print(msg)

Выкладываю ещё не готовый код. Буду рад, если кто-то закончит раньше меня.
Добавлено: код готов, но всё ещё бета.
Добавлено: :lol: :lol: :lol: :lol: :lol: Сконвертировал xmi в mid этим кодом, foobar2000 с аддоном от kode54 нормально проигрывает
Добавлено: а что же с патчеными файлами от netsky? По моему, звучат неплохо. Но я бы предпочёл сделать нормальный звуковой банк специально для нужд игры, а не подстраиваться под GM.
Добавлено: ага! Мой конвертер работает со всеми файлами netsky, кроме INTRO.XMI! Вот вывод:
Код: Выделить всё
Traceback (most recent call last):
  File "/home/malapu/kyra/xmi/writemidtype2.py", line 157, in <module>
    midi=MidiFile(sys.argv[1])
  File "/home/malapu/kyra/xmi/midplay.py", line 210, in __init__
    elif container[0].fourCC==b'FORM:XDIR' and container[0].chunkID==b'INFO':
AttributeError: 'bytes' object has no attribute 'fourCC'

По размеру файлов явно видно, что netsky делал этот файл как-то по другому, и это единственный файл, размер которого совпадает с оригиналом.
А ещё - единственный файл с uppercase-именем.
Очевидно, формат у этого файла нестандартный. Видимо, netsky что-то напортачил, и мне придётся добавить поддержку его ошибки.
Добавлено: вот багфикс:
Код: Выделить всё
import struct,sys,time,fluidsynth,io
from collections import namedtuple,deque
from operator import attrgetter
XMIChunk=namedtuple('XMIChunk',['fourCC','chunkID','content'])
Controller=namedtuple('Controller', ['controller','value','channel','starttick'])
Instrument=namedtuple('Instrument', ['instrument','bank','channel','starttick'])
Bank=namedtuple('Bank', ['bank','channel','starttick'])
ChannelLockUnlock=namedtuple('ChannelLockUnlock', ['channel','starttick'])
ChannelLockProtect=namedtuple('ChannelLockProtect', ['channel','starttick'])
Pressure=namedtuple('Pressure', ['pressure','channel','starttick'])
Transpose=namedtuple('Transpose', ['transpose','channel','starttick'])
SysEx=namedtuple('SysEx', ['type','data','starttick'])
MetaText=namedtuple('MetaText', ['type','data','starttick'])
MetaEoT=namedtuple('MetaEoT', ['starttick'])
MetaTempo=namedtuple('MetaTempo', ['tempo','starttick'])
MetaVendor=namedtuple('MetaVendor', ['vendor','starttick'])
Meta=namedtuple('Meta', ['type','data','starttick'])
_NoteOn=namedtuple('NoteOn', ['note','velocity','channel','starttick','pretty','pressed'])
class NoteOn(_NoteOn):
    def __new__(self,note,velocity,channel,starttick=0):
        notes=("C","C#","D","Eb","E","F","F#","G","G#","A","Bb","B")
        o,n=divmod(note,12)
        return _NoteOn.__new__(self,note,velocity,channel,starttick,(notes[n]+str(o-1)).ljust(3).rjust(4),True)
_NoteOff=namedtuple('NoteOff', ['note','velocity','channel','starttick','pretty','pressed'])
class NoteOff(_NoteOff):
    def __new__(self,note,velocity,channel,starttick=0):
        notes=("C","C#","D","Eb","E","F","F#","G","G#","A","Bb","B")
        o,n=divmod(note,12)
        return _NoteOff.__new__(self,note,velocity,channel,starttick,(notes[n]+str(o-1)).ljust(3).rjust(4),False)
_NotePressure=namedtuple('NotePressure', ['note','pressure','channel','starttick','pretty'])
class NotePressure(_NotePressure):
    def __new__(self,note,pressure,channel,starttick=0):
        notes=("C","C#","D","Eb","E","F","F#","G","G#","A","Bb","B")
        o,n=divmod(note,12)
        return _NotePressure.__new__(self,note,pressure,channel,starttick,(notes[n]+str(o-1)).ljust(3).rjust(4))
def pairwise(iterable):
    a=iter(iterable)
    return zip(a,a)
def xmi_decode(container):
    timb=dict()
    for chunk in container:
        for subchunk in chunk:
            if subchunk.fourCC==b'FORM:XMID' and subchunk.chunkID==b'TIMB':
                timb=dict(pairwise(subchunk.content))
            elif subchunk.fourCC==b'FORM:XMID' and subchunk.chunkID==b'EVNT':
                return decompress_notation(subchunk.content,timb)
def stream_parse(stream,fourCC=b'ROOT'):
    while True:
        chunkID=stream.read(4)
        if len(chunkID)!=4:
            break
        while chunkID[0]==0:
            chunkID=chunkID[1:]+stream.read(1)
            if len(chunkID)!=4:
                break
        lenChunk=struct.unpack('>I',stream.read(4))[0]
        content=stream.read(lenChunk)
        yield list(unpack_chunk(chunkID,content,fourCC))
def unpack_chunk(chunkID,content,fourCC=b'ROOT'):
    if chunkID==b'FORM' or chunkID==b'CAT\x20' or chunkID==b'LIST':
        fourCC=chunkID+b':'+content[:4]
        vstream=io.BytesIO(content[4:])
        for chunks in stream_parse(vstream,fourCC):
            yield chunks
    else:
        yield XMIChunk(fourCC,chunkID,content)
def sum_7bit(notation,start):
    num=0
    byte=notation[start]
    while not byte&128:
        num+=byte&127
        start+=1
        byte=notation[start]
    return(num,start)
def concat_7bit(notation,start):
    num=0
    while True:
        byte=notation[start]
        start+=1
        num|=byte&127
        if byte&128:
            num<<=7
        else:
            break
    return(num,start)
def decompress_notation(notation,xmi=None):
    start=0
    starttick=0
    offnotes=set()
    while start<len(notation)-1:
        if xmi!=None:
            delay,start=sum_7bit(notation,start)
        else:
            delay,start=concat_7bit(notation,start)
        starttick+=delay
        for offnote in sorted(offnotes,key=attrgetter('starttick')):
            if offnote.starttick<=starttick:
                offnotes.remove(offnote)
                yield offnote
        msg=0
        byte=notation[start]
        if byte==255: #meta
            msgtype=notation[start+1]
            lenMeta,startMeta=concat_7bit(notation,start+2)
            if msgtype in range(1,16):
                msg=MetaText(msgtype,notation[startMeta:startMeta+lenMeta],starttick)
                start=startMeta+lenMeta
            elif msgtype==0x2f:
                msg=MetaEoT(starttick)
                start=startMeta+lenMeta
            elif msgtype==0x51:
                msg=MetaTempo(struct.unpack('>I',b'\x00'+notation[startMeta:startMeta+lenMeta])[0],starttick)
                start=startMeta+lenMeta
                if xmi!=None:
                    continue
            elif msgtype==0x7f:
                msg=MetaVendor(notation[startMeta:startMeta+lenMeta],starttick)
                start=startMeta+lenMeta
            else:
                msg=Meta(msgtype,notation[startMeta:startMeta+lenMeta],starttick)
                start=startMeta+lenMeta
        else:
            msgtype=byte&240
            channel=byte&15
            if msgtype==0x80:
                msg=NoteOff(notation[start+1],notation[start+2],channel,starttick)
                start+=3
            elif msgtype==0x90:
                note,vel=notation[start+1:start+3]
                msg=NoteOn(note,vel,channel,starttick)
                start+=3
                if xmi!=None:
                    duration,start=concat_7bit(notation,start)
                    offnotes.add(NoteOff(note,vel,channel,starttick+duration))
            elif msgtype==0xA0:
                msg=NotePressure(notation[start+2],notation[start+1],channel,starttick)
                start+=3
            elif msgtype==0xB0:
                if notation[start+1]==114:
                    msg=Bank(notation[start+2],channel,starttick)
                elif notation[start+1]==110 and notation[start+2]==127:
                    msg=ChannelLockUnlock(channel,starttick)
                elif notation[start+1]==111 and notation[start+2]==127:
                    msg=ChannelLockProtect(channel,starttick)
                else:
                    msg=Controller(notation[start+1],notation[start+2],channel,starttick)
                start+=3
            elif msgtype==0xC0:
                bank=0
                if xmi!=None:
                    bank=xmi[notation[start+1]]
                msg=Instrument(notation[start+1],bank,channel,starttick)
                start+=2
            elif msgtype==0xD0:
                msg=Pressure(notation[start+1],channel,starttick)
                start+=2
            elif msgtype==0xE0:
                msg=Transpose((notation[start+2]<<7)|notation[start+1],channel,starttick)
                start+=3
            elif msgtype==0xF0:
                lenSysEx,startSysEx=concat_7bit(notation,start+1)
                dataSysEx=b''
#                try:
#                    while notation[startSysEx]!=0xF7:
#                        dataSysEx+=notation[startSysEx:startSysEx+1]
#                        startSysEx+=1
#                    dataSysEx+=notation[startSysEx:startSysEx+1]
#                except IndexError:
                dataSysEx=notation[startSysEx:startSysEx+lenSysEx]
                msg=SysEx(channel,dataSysEx,starttick) #here channel is not a channel, but type
                start=startSysEx+lenSysEx
            else:
                raise ValueError('not valid msg: '+format(byte,'x'))
        yield msg
class MidiFile(list):
    def __init__(self,filename):
        list.__init__(self)
        stream=open(filename,'rb')
        self.fileType=stream.read(4)
        if self.fileType==b'MThd':
            if stream.read(4)!=b'\x00\x00\x00\x06':
                raise ValueError('unsupported format')
            self.midiType=struct.unpack('>H',stream.read(2))[0]
            self.numTracks=struct.unpack('>H',stream.read(2))[0]
            self.TPQN=struct.unpack('>h',stream.read(2))[0]
            self.tempo=500000
            tracks=deque()
            for track in range(self.numTracks):
                if stream.read(4)!=b'MTrk':
                    sys.exit('not a track')
                lenTrack=struct.unpack('>I',stream.read(4))[0]
                tracks.append(stream.read(lenTrack))
            stream.close()
            if self.midiType==1:
                self.append([])
            for notation in tracks:
                if self.midiType!=1:
                    self.append([])
                self[-1].extend(decompress_notation(notation))
            if self.midiType==1:
                self[-1].sort(key=attrgetter('starttick'))
        elif self.fileType==b'FORM':
            stream.seek(0)
            xmi=list(stream_parse(stream))
            stream.close()
            for root in xmi:
                for container in root:
                    if isinstance(container[0],list):
                        self.append(list(xmi_decode(container)))
                    elif isinstance(container[0],XMIChunk):
                        if container[0].fourCC==b'FORM:XDIR' and container[0].chunkID==b'INFO':
                            self.numTracks=struct.unpack('<H',container[0].content)[0]
                    else:
                        print("netsky's bug",container)
            self.midiType=2
            self.TPQN=60
            self.tempo=500000
#        elif self.fileType==b'#MID':
        else:
            raise ValueError('not a midi file')
    def play(self,sf,bank,tracknum=-1,filename=None):
        if self.TPQN<1:
            raise ValueError('absolute ticks not supported')
        fs=fluidsynth.Synth(samplerate=48000)
        if filename!=None:
            fs.start(driver=b'file',filename=filename)
        elif sys.platform=="win32":
            fs.start(driver=b'dsound')
        else:
            fs.start(driver=b'pulseaudio')
        sfid=fs.sfload(sf)
        for num,track in enumerate(self):
            if tracknum==-1 or tracknum==num:
                tempo=self.tempo
                starttick=0
                delay=0
                for msg in track:
                    delay,starttick=msg.starttick-starttick,msg.starttick
                    time.sleep(delay*tempo/self.TPQN/1000000)
                    if isinstance(msg,NoteOn):
                        fs.noteon(msg.channel,msg.note,msg.velocity)
                    elif isinstance(msg,NoteOff):
                        fs.noteoff(msg.channel,msg.note)
                    elif isinstance(msg,NotePressure):
                        print('pressure is unsupported')
                    elif isinstance(msg,Pressure):
                        print('pressure is unsupported')
                    elif isinstance(msg,Controller):
                        fs.cc(msg.channel,msg.controller,msg.value)
                    elif isinstance(msg,Instrument):
                        if self.fileType==b'FORM':
                            fs.program_select(msg.channel,sfid,msg.bank,msg.instrument)
                        else:
                            fs.program_select(msg.channel,sfid,bank,msg.instrument)
                    elif isinstance(msg,Bank):
                            fs.bank_select(msg.channel,msg.bank)
                    elif isinstance(msg,Transpose):
                        fs.pitch_bend(msg.channel,msg.transpose-8192)
                    elif isinstance(msg,MetaTempo):
                        tempo=msg.tempo
                fs.system_reset()
if __name__=='__main__':
    argv=list(sys.argv)
    filename=None
    for num,arg in enumerate(list(argv)):
        if arg.startswith('-file='):
            filename=arg[6:]
            del argv[num]
            break
    if len(argv)!=3 and len(argv)!=4 and len(argv)!=5:
        sys.exit('usage: py midplay.py [C:\path\]filename.sf2 [C:\path]filename.mid [track [bank]]')
    if len(argv)>4:
        bank=int(argv[4])
    else:
        bank=0
    if len(argv)>3:
        track=int(argv[3])
    else:
        track=-1
    midi=MidiFile(argv[2])
    midi.play(argv[1],bank,track,filename)

Добавлено: код вверху уже никакая не бета, а релиз-кандидат. Кроме того, выкладываю надпрограмму:
Код: Выделить всё
import sys,struct
from midplay import MidiFile
from writemidtype2 import compress_notation
if __name__=='__main__':
    if len(sys.argv)!=3:
        sys.exit('usage: py writemidtype0.py [C:\path]filename.mid [C:\path]filename')
    midi=MidiFile(sys.argv[1])
    for num,track in enumerate(midi):
        raw=b'MThd\x00\x00\x00\x06'+struct.pack('>H',0)+struct.pack('>H',1)+struct.pack('>h',midi.TPQN)
        rawTrack=compress_notation(track)
        raw+=b'MTrk'+struct.pack('>I',len(rawTrack))+rawTrack
        f=open(sys.argv[2]+'.'+str(num).rjust(3,'0')+'.mid','wb')
        f.write(raw)
        f.close()

Добавлено: оказалось, при xmi=True неправильно подсчитывалась задержка. Совместимость потребовала серьёзной модификации кода выше. Теперь код ниже работает.
Код: Выделить всё
import sys,struct,itertools
from midplay import MidiFile
from writemidtype2 import compress_notation,get_timb,check_xmi_compat
if __name__=='__main__':
    if len(sys.argv)!=3:
        sys.exit('usage: py writexmi.py [C:\path]filename.mid [C:\path]filename.xmi')
    midi=MidiFile(sys.argv[1])
#    print("XMI compatible:",check_xmi_compat(midi))
    if not check_xmi_compat(midi):
        sys.exit('not xmi compatible')
    data=b'FORM'+struct.pack('>I',14)+b'XDIR'+( b'INFO'+struct.pack('>I',2)+( struct.pack('<H',midi.numTracks) ) )
    form=b''
    for track in midi:
        timb=bytes(itertools.chain(*get_timb(track).items()))
        evnt=compress_notation(track,True)
        form+=b'FORM'+struct.pack('>I',20+len(timb)+len(evnt))+b'XMID'+b'TIMB'+struct.pack('>I',len(timb))+timb+b'EVNT'+struct.pack('>I',len(evnt))+evnt
    data+=b'CAT\x20'+struct.pack('>I',4+len(form))+b'XMID'+form
    with open(sys.argv[2],'wb') as xmi:
        xmi.write(data)
##    print(data)
#        print(bytes(itertools.chain(*get_timb(track).items())))
#        print(compress_notation(track,True))

Добавлено: ой, кажется, evnt чанк парсится неверно.
Добавлено: предыдущее сообщение - паранойя.
Добавлено: перепакованные файлы работают в DOSBox и ScummVM.
Аватара пользователя
bckpkol
(3) Столичный горожанин
 
Сообщения: 199
Зарегистрирован: 02 янв 2011, 20:48
Откуда: город Бийск
Любимая часть Кирандии: Кирандия 3 и 4
Любимые персонажи Кирандии: Дарм, Зантия
Почему Вы любите Легенду о Кирандии?: За простоту, удобство интерфейса и лёгкую проходимость.


Вернуться в Техничка

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 1

cron