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

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

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

Сообщение 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) Столичный горожанин
 
Сообщения: 224
Зарегистрирован: 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) Столичный горожанин
 
Сообщения: 224
Зарегистрирован: 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) Столичный горожанин
 
Сообщения: 224
Зарегистрирован: 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) Столичный горожанин
 
Сообщения: 224
Зарегистрирован: 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) Столичный горожанин
 
Сообщения: 224
Зарегистрирован: 02 янв 2011, 20:48
Откуда: город Бийск
Любимая часть Кирандии: Кирандия 3 и 4
Любимые персонажи Кирандии: Дарм, Зантия
Почему Вы любите Легенду о Кирандии?: За простоту, удобство интерфейса и лёгкую проходимость.

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

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

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

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

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

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

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

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

Почему Python, Reflector? Это язык, на котором написаны скрипты Blender, он известен мне лучше всего, и его намного легче отлаживать, чем C++ (я так и не научился работать с CDB), и компилять не нужно. Плюс куча доступных библиотек.
Аватара пользователя
bckpkol
(3) Столичный горожанин
 
Сообщения: 224
Зарегистрирован: 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/U3yGXdix3HNU7j
Добавлено: исправил ошибку при чтении meta и sysex событий длиннее 127. Я не знал, что длина измеряется в concat_7bit единицах. Ещё убрал старый workaround. Ждите конвертер.
Добавлено: забыл сказать, что теперь есть ключ "-file=audio.flac".
Добавлено: вернул проверку на win32.
Последний раз редактировалось bckpkol 27 апр 2017, 09:14, всего редактировалось 4 раз(а).
Аватара пользователя
bckpkol
(3) Столичный горожанин
 
Сообщения: 224
Зарегистрирован: 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) Столичный горожанин
 
Сообщения: 224
Зарегистрирован: 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 или пользуйтесь предыдущей версией.
Добавлено: можно поставить MSYS2. Там Python с GCC.
Последний раз редактировалось bckpkol 27 апр 2017, 09:16, всего редактировалось 2 раз(а).
Аватара пользователя
bckpkol
(3) Столичный горожанин
 
Сообщения: 224
Зарегистрирован: 02 янв 2011, 20:48
Откуда: город Бийск
Любимая часть Кирандии: Кирандия 3 и 4
Любимые персонажи Кирандии: Дарм, Зантия
Почему Вы любите Легенду о Кирандии?: За простоту, удобство интерфейса и лёгкую проходимость.

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

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

Код: Выделить всё
import sys,itertools,struct,math
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''
    for i in range(math.ceil(num.bit_length()/7)):
        n=(num>>(i*7))&127
        if i:
            n|=128
        ret=bytes([n])+ret
    if ret==b'':
        ret=b'\x00'
    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 and bank!=0:
                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 and bank!=0:
                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.numTracks=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',len(midi)) ) )
    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.
Добавлено: так как дорожки midi типа 1 сливаются в одну, исправил numTracks на 1, если тип - 1.
Добавлено: заметил, что конвертер флудит Bank-сообщениями. Исправлено.
Добавлено: выложил исправление бага в writemidtype2.py.
Добавлено: выложил исправление бага в writemidtype2.py, того же самого. Предыдущее исправление игнорировало пустые байты.
Добавлено: выложил исправление бага в writemidtype2.py, того же самого. Предыдущее исправление игнорировало нулевые таймеры.
Последний раз редактировалось bckpkol 07 май 2017, 12:09, всего редактировалось 7 раз(а).
Аватара пользователя
bckpkol
(3) Столичный горожанин
 
Сообщения: 224
Зарегистрирован: 02 янв 2011, 20:48
Откуда: город Бийск
Любимая часть Кирандии: Кирандия 3 и 4
Любимые персонажи Кирандии: Дарм, Зантия
Почему Вы любите Легенду о Кирандии?: За простоту, удобство интерфейса и лёгкую проходимость.

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

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

Сделал новую утилиту. Она делает mid файлы xmi-совместимыми, после обработки их можно преобразовывать в xmi. Фактически утилита удаляет tempo команды и приводит TPQN файла к значению 60.
Код: Выделить всё
import math,sys,struct
from collections import namedtuple,deque
from operator import attrgetter
from midplay import MidiFile,MetaTempo,MetaEoT
from writemidtype2 import prepare_notation_compression,get_raw_delay
from math import floor,ceil
def reduce_ticks(ticks,divisor=math.e):
    catched=set()
    result=[]
    for tick in ticks:
        mintick,maxtick=floor(tick/divisor),ceil(tick/divisor)+1
        isCatched=False
        for catch in range(mintick,maxtick):
            if catch not in catched:
                catched.update({catch})
                result.append(catch)
                isCatched=True
                break
        if not isCatched:
            catched.update({maxtick-1})
            result.append(maxtick-1)
    return dict(zip(ticks,result))
def scale_ticks(ticks,oldrate,newrate):
    remain=oldrate/newrate
    count=max(0,int(math.log(remain)))
    remain/=math.exp(count)
    mapping=dict(zip(ticks,ticks))
    for i in range(count):
        remap=reduce_ticks(mapping.values())
        for key in list(mapping.keys()):
            mapping[key]=remap[mapping[key]]
    remap=reduce_ticks(mapping.values(),remain)
    for key in list(mapping.keys()):
        mapping[key]=remap[mapping[key]]
    return mapping
def split_notation_by_tempo(notation):
    withtempos=[]
    tempo=500000
    withtempo=deque()
    starttick=0
    for msg in notation:
        if isinstance(msg,MetaTempo):
            withtempo.append(MetaEoT(msg.starttick-starttick))
            withtempos.append((tempo,withtempo))
            tempo=msg.tempo
            starttick=msg.starttick
            withtempo=deque()
        else:
            withtempo.append(msg._replace(**{'starttick':msg.starttick-starttick}))
    withtempos.append((tempo,withtempo))
    return withtempos
def compress_notation_with_scaling(notation,TPQN,xmi=False,newrate=60):
    rescalednotation=deque()
    starttick=0
    for tempo,subtempo in split_notation_by_tempo(notation):
        ticks=scale_ticks([msg.starttick for msg in subtempo],round(TPQN*(500000/tempo)),newrate)
        for msg in subtempo:
            if not isinstance(msg,MetaEoT):
                rescalednotation.append(msg._replace(**{'starttick':starttick+ticks[msg.starttick]}))
        starttick+=ticks[msg.starttick]
    data=b''
    starttick=0
    delay=0
    for msg in sorted(prepare_notation_compression(rescalednotation,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 and len(sys.argv)!=4:
        sys.exit('usage: py trc.py [C:\path]filename.mid [C:\path]filename.mid [60]')
    if len(sys.argv)==4:
        newrate=int(sys.argv[3])
    else:
        newrate=60
    midi=MidiFile(sys.argv[1])
    raw=b'MThd\x00\x00\x00\x06'+struct.pack('>H',2)+struct.pack('>H',len(midi))+struct.pack('>h',newrate)
    for track in midi:
        rawTrack=compress_notation_with_scaling(track,midi.TPQN,newrate=newrate)
        raw+=b'MTrk'+struct.pack('>I',len(rawTrack))+rawTrack
    f=open(sys.argv[2],'wb')
    f.write(raw)
    f.close()

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

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

Сообщение bckpkol » 26 апр 2017, 18:03

Код: Выделить всё
import sys,struct,glob
from midplay import MidiFile
from writemidtype2 import compress_notation
if __name__=='__main__':
    if len(sys.argv)!=3:
        sys.exit('usage: py midjoin.py [C:\path]filename.* [C:\path]filename.mid')
    raw=b''
    count=0
    for filename in sorted(glob.iglob(sys.argv[1])):
        midi=MidiFile(filename)
        for track in midi:
            rawTrack=compress_notation(track)
            raw+=b'MTrk'+struct.pack('>I',len(rawTrack))+rawTrack
            count+=1
    raw=b'MThd\x00\x00\x00\x06'+struct.pack('>H',2)+struct.pack('>H',count)+struct.pack('>h',midi.TPQN)+raw
    f=open(sys.argv[2],'wb')
    f.write(raw)
    f.close()

Собирает кучу midi файлов в один. Внимание:TicksPerQuarterNote берётся от последнего файла. Чтобы объединить файлы с разным TPQN, нужно сначала сделать каждый файл xmi-совместимым.
Добавлено: вот эта программа делает файлы xmi-совместимыми автоматически:
Код: Выделить всё
import sys,struct,glob
from midplay import MidiFile
from trc import compress_notation_with_scaling
if __name__=='__main__':
    if len(sys.argv)!=3:
        sys.exit('usage: py midjoinsc.py [C:\path]filename.* [C:\path]filename.mid')
    raw=b''
    count=0
    for filename in sorted(glob.iglob(sys.argv[1])):
        midi=MidiFile(filename)
        for track in midi:
            rawTrack=compress_notation_with_scaling(track,midi.TPQN,newrate=60)
            raw+=b'MTrk'+struct.pack('>I',len(rawTrack))+rawTrack
            count+=1
    raw=b'MThd\x00\x00\x00\x06'+struct.pack('>H',2)+struct.pack('>H',count)+struct.pack('>h',60)+raw
    f=open(sys.argv[2],'wb')
    f.write(raw)
    f.close()
Аватара пользователя
bckpkol
(3) Столичный горожанин
 
Сообщения: 224
Зарегистрирован: 02 янв 2011, 20:48
Откуда: город Бийск
Любимая часть Кирандии: Кирандия 3 и 4
Любимые персонажи Кирандии: Дарм, Зантия
Почему Вы любите Легенду о Кирандии?: За простоту, удобство интерфейса и лёгкую проходимость.

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

Сообщение LEGO-кирандиец » 04 май 2017, 21:25

Очень стоящая, на мой взгляд, попытка. Вот только Кайрацвет мне не по душе. Всё-тки перевод Каткова ближе :) .
Лего и Кирандия - то, что делает мою жизнь интересной и весёлой.
Неплохо было бы, если эти два интереса слились. :!:
LEGO-кирандиец
(1) Пират с острова Котов
 
Сообщения: 68
Зарегистрирован: 20 авг 2016, 12:36
Откуда: Ангарск
Любимая часть Кирандии: Первая и третья
Любимые персонажи Кирандии: Малколм, Брэндон, Дарм
Почему Вы любите Легенду о Кирандии?: Потому что это игра детства, вызывающие приятные воспоминания и красивые образы. В основном, мои мысли совпадают с мыслями других "жителей" форума.

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

Сообщение bckpkol » 05 май 2017, 01:00

Код: Выделить всё
def vgaPal(bin,part=False):
    return [tuple((ch<<2|ch>>4 for ch in col[1]))+((bool(col[0])|part)*255,) for col in enumerate(zip(*[iter(bin)]*3))]
if __name__=='__main__':
    import sys
    with open(sys.argv[1],'rb') as f:
        print(vgaPal(f))

Код: Выделить всё
import sys,os,struct,png
from pal import vgaPal
if len(sys.argv)!=3 and len(sys.argv)!=4:
    sys.exit('usage: py cps2png.py [C:\path\]filename.cps [C:\path\]filename.png [[C:\path\]filename.col]')
with open(sys.argv[1],'rb') as f:
    fileSize=struct.unpack('<H',f.read(2))[0]
    compressionType=struct.unpack('<H',f.read(2))[0]
    uncompressedSize=struct.unpack('<I',f.read(4))[0]
    paletteSize=struct.unpack('<H',f.read(2))[0]
    if uncompressedSize!=64000:
        sys.exit('invalid image')
    if compressionType==0 or compressionType==4:
        fileSize+=2
    if paletteSize==768:
        pal=vgaPal(f.read(768))
    else:
        f.read(paletteSize)
        if len(sys.argv)==4:
            with open(sys.argv[3],'rb') as palfile:
                pal=vgaPal(palfile.read())
            with open(r'PALETTE.COL','rb') as palfile:#Reflector's code start
                pal[249:255]=vgaPal(palfile.read())[249:255]
            pal[255]=(45,46,50)
            datfile='.'.join(sys.argv[1].split('.')[:-1]+['DAT'])
            if os.path.isfile(datfile):
                with open(datfile,'rb') as palfile:
                    palfile.seek(23)
                    pal[228:248]=vgaPal(palfile.read(60),True)#Reflector's code end
        else:
            pal=[(ch&240,(ch&15)<<4,0) for ch in range(256)]
    data=f.read()
    datapos=0
    destpos=0
    uncompressed=[0]*uncompressedSize
    if compressionType==4:
        while datapos<len(data):
            if data[datapos]&128:
                if data[datapos]&64:
                    if data[datapos]==255:
                        count=(data[datapos+2]<<8)|data[datapos+1]
                        offset=(data[datapos+4]<<8)|data[datapos+3]
                        for i in range(count):
                            uncompressed[destpos+i]=uncompressed[offset+i]
                        datapos+=5
                        destpos+=count
                    elif data[datapos]==254:
                        count=(data[datapos+2]<<8)|data[datapos+1]
                        color=data[datapos+3]
                        uncompressed[destpos:destpos+count]=[color]*count
                        datapos+=4
                        destpos+=count
                    else:
                        count=(data[datapos]&63)+3
                        offset=(data[datapos+2]<<8)|data[datapos+1]
                        for i in range(count):
                            uncompressed[destpos+i]=uncompressed[offset+i]
                        datapos+=3
                        destpos+=count
                else:
                    count=data[datapos]&63
                    if count==0:
                        break
                    datapos+=1
                    uncompressed[destpos:destpos+count]=data[datapos:datapos+count]
                    datapos+=count
                    destpos+=count
            else:
                count=((data[datapos]&112)>>4)+3
                relpos=((data[datapos]&15)<<8)|data[datapos+1]
                for i in range(count):
                    uncompressed[destpos+i]=uncompressed[destpos+i-relpos]
                datapos+=2
                destpos+=count
#print(fileSize,compressionType,uncompressedSize,paletteSize,pal,uncompressed)
with open(sys.argv[2],'wb') as f:
    w = png.Writer(320, 200, palette=pal, bitdepth=8)
    w.write(f, zip(*[iter(uncompressed)]*320))

Yes! Cps to png. Ждите обратный конвертер и редактор трекерной музыки.
Добавлено: одну строчку поправил, со встроенной палитрой не работало. Надо было не указатель на файл давать, а 768 байт из файла.
Последний раз редактировалось bckpkol 11 май 2017, 14:45, всего редактировалось 10 раз(а).
Аватара пользователя
bckpkol
(3) Столичный горожанин
 
Сообщения: 224
Зарегистрирован: 02 янв 2011, 20:48
Откуда: город Бийск
Любимая часть Кирандии: Кирандия 3 и 4
Любимые персонажи Кирандии: Дарм, Зантия
Почему Вы любите Легенду о Кирандии?: За простоту, удобство интерфейса и лёгкую проходимость.

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

Сообщение Reflector » 05 май 2017, 04:04

bckpkol писал(а):Yes! Cps to png. Ждите обратный конвертер и редактор трекерной музыки.

Для k1 это не будет нормально работать, там палитра собирается из 3-х частей: глобальной палитры(вроде palette.col), палитры для конкретного cps и еще кусок берется из .dat :)
Reflector
(2) Житель Милтонии
 
Сообщения: 163
Зарегистрирован: 11 сен 2010, 16:44
Любимая часть Кирандии: 2
Любимые персонажи Кирандии: Занция
Почему Вы любите Легенду о Кирандии?: Ностальгия...

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

Сообщение bckpkol » 05 май 2017, 05:42

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

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

Сообщение Reflector » 05 май 2017, 11:55

bckpkol писал(а):Я слышал об этом. Спецификацией формата dat не поделишься?

Нет никакой спецификации, но алгоритм для k1 следующий:
Если есть внешняя палитра, то грузим ее, если нет, то грузим origpal.col. Дальше у меня идет примерно такой код:
Код: Выделить всё
    CopyPalette(Properties.Resources.kyra1_palette, 0x2e8/*srcOffset*/, 0x2e8/*dstOffset*/, 21); // копируем 21 байт из Palette.col
    palette[255].R = ColorScale(0x2d);
    palette[255].G = ColorScale(0x2e);
    palette[255].B = ColorScale(0x32);
    // ищем в pak любой .dat
    using (var fdat = new BinaryReader(File.OpenRead(datFile)))
    {
        fdat.BaseStream.Position = 0x17;
        LoadPalette(fdat, 0x2ac, 60);  // копируем из .dat 60 байт в позицию 0x2ac
    }

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

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

Сообщение bckpkol » 05 май 2017, 14:31

Reflector писал(а):
bckpkol писал(а):Я слышал об этом. Спецификацией формата dat не поделишься?

Нет никакой спецификации, но алгоритм для k1 следующий:

Это распаковка. Да, я заметил, что в dat есть что-то похожее на палитру. А упаковка? Какой формат у заголовка?
И размеры у dat файлов разные. Нет, там нечто большее, чем кусок палитры.
Кстати, что это за метка 53 4b?
Reflector писал(а):Если есть внешняя палитра, то грузим ее, если нет, то грузим origpal.col. Дальше у меня идет примерно такой код:
Код: Выделить всё
    CopyPalette(Properties.Resources.kyra1_palette, 0x2e8/*srcOffset*/, 0x2e8/*dstOffset*/, 21); // копируем 21 байт из Palette.col
    palette[255].R = ColorScale(0x2d);
    palette[255].G = ColorScale(0x2e);
    palette[255].B = ColorScale(0x32);
    // ищем в pak любой .dat
    using (var fdat = new BinaryReader(File.OpenRead(datFile)))
    {
        fdat.BaseStream.Position = 0x17;
        LoadPalette(fdat, 0x2ac, 60);  // копируем из .dat 60 байт в позицию 0x2ac
    }

Внешняя палитра? Уверен, что не внутренняя, запакованная в cps? Если нет, что понимается под внешней палитрой?
Странно... Взять одну палитру, скопировать из другой, затем странные магические числа 0x17 и 60.
Да, еще в k2, если палитра запакована, то вторая половина берется из Palette.col.

Тут хотелось бы спросить подробнее. Так как VGA использует 6 бит на канал, 18 на цвет, палитра должна быть 576 байт.
Мне непонятно, зачем нужно брать вторую половину, разве палитра изначально не целая?
ColorScale - это то же самое, что и <<2?
Вопрос не в тему. Я единственный, кому кажется, что исходники scummvm нечитаемы?
Что я понял - scummvm - это рендерер, который динамически загружает ресурсы игры. Отдельного файла на каждый тип ресурса нет, всё распихано непонятно как.
Аватара пользователя
bckpkol
(3) Столичный горожанин
 
Сообщения: 224
Зарегистрирован: 02 янв 2011, 20:48
Откуда: город Бийск
Любимая часть Кирандии: Кирандия 3 и 4
Любимые персонажи Кирандии: Дарм, Зантия
Почему Вы любите Легенду о Кирандии?: За простоту, удобство интерфейса и лёгкую проходимость.

След.

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

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

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

cron