vsqx2midi121.py - VOCALOID3 の vsqx を歌詞入りMIDIに変換する(UTAU 対応)

前バージョン

では UTAU で読み込むと歌詞が一つ後ろにずれてしまう、という不具合があったので訂正しました。(使い方は前バージョンの記述参照)
ちなみに UTAU で読み込んだ時にはまず最初にテンポを設定してください。MIDI の仕様だとテンポの指定の無い時には 120 が指定されていると見なすはずなのですが、UTAU はそれが実装されていないようです。

# 2012-02-19 ver.1.2.1 UTAU 対応

import xml.dom.minidom
import struct

FILENAME = 'hoge'  # 対象のファイル名

def chrs(s):
    return ''.join([chr(x) for x in s])

def i2(v):
    if v > 0xffff:
        raise ValueError(v)
    return struct.pack('>H', v)

def i4(v):
    if v > 0xffffffff:
        raise ValueError(v)
    return struct.pack('>I', v)

def delta(v):
    if v < 0:
        raise ValueError(v)
    a = []
    while True:
        if v <= 0x7f:
            a.append(v)
            break
        else:
            a.append(v % 0x80)
            v //= 0x80
    a.reverse()
    return ''.join([chr(x + 0x80) for x in a[0:-1]] + [chr(a[-1])])

def notedata(n, tag):
    return n.getElementsByTagName(tag)[0].firstChild.data

def makenote(n):
    return dict([(x, notedata(n, x)) for x in
                 ('posTick', 'durTick', 'noteNum',
                  'velocity', 'lyric', 'phnms')])

def noteon(ch, nn, vel):
    return chr(0x90 + ch) + chr(nn) + chr(vel)

def noteoff(ch, nn):
    return chr(0x80 + ch) + chr(nn) + chr(0)

def lyrics(lyric):
    s = lyric.encode('shift-jis', 'replace')
    return '\xff\x05' + chr(len(s)) + s

def maketrack(n):
    track = []
    if n.nodeName == 'note':
        track.append(makenote(n))
    if n.childNodes:
        for i in n.childNodes:
            track += maketrack(i)
    return track

def makemiditrack(n, ch):
    ch %= 16  # 16トラックを超えたら 0 に戻る
    midi = []
    lyric = []
    tick = 0
    for p in n.getElementsByTagName('musicalPart'):
        postick = int(notedata(p, 'posTick'))
        for i in maketrack(p):
            t = postick + int(i['posTick'])
            notelen = int(i['durTick'])
            midi.append(
                delta(t - tick) +
                lyrics(i['lyric']) +
                delta(0) +
                noteon(ch, int(i['noteNum']), int(i['velocity'])) +
                delta(notelen) +
                noteoff(ch,  int(i['noteNum'])))
            lyric.append(i['lyric'])
            tick = t + notelen
    data = ''.join(midi) + delta(0) + chrs([0xff, 0x2f, 0])
    return 'MTrk' + i4(len(data)) + data, ' '.join(lyric)

def makeconductor():
    data = delta(0) + '\xff\x2f\x00'
    return 'MTrk' + i4(len(data)) + data

def makemidifile(filename):
    print 'loading:', filename
    d = xml.dom.minidom.parse(filename)                
    tracks = [makeconductor()]
    lyrics = []
    for n, i in enumerate(d.getElementsByTagName('vsTrack')):
        print 'track', n
        track, lyric = makemiditrack(i, n)
        tracks.append(track)
        lyrics.append(lyric)
    header = 'MThd' + i4(6) + i2(1) + i2(len(tracks)) + i2(480)
    return header + ''.join(tracks), '\n\n'.join(lyrics)

midi, lyric = makemidifile(FILENAME + '.vsqx')
f = open(FILENAME + '.mid', 'wb')
f.write(midi)
f.close()
f = open(FILENAME + '.txt', 'w')
f.write(lyric.encode('utf_8_sig'))
f.close()

## パブリックドメイン。好きに流用してください