- Код: Выделить всё
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)
Выкладываю ещё не готовый код. Буду рад, если кто-то закончит раньше меня.
Добавлено: код готов, но всё ещё бета.
Добавлено:

Сконвертировал 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, того же самого. Предыдущее исправление игнорировало нулевые таймеры.