yuyu2midi.py - ゆゆシーケンサーの「ゆゆシケ・コード」を歌詞付きMIDIファイルにする

の「ゆゆシケ・コード」を VOCALOID2/3 Editor でインポートできる歌詞付きMIDIファイルにする Python スクリプトです。

Python 2.6 で使用してください。(Python 3.x は不可)

使い方

  1. Python をインストール(やり方は適当にぐぐって)
  2. 下のソースを BOM付き utf-8 で「vsqx2midi.py」という名前で保存 (メモ帳で文字コードutf-8」にして保存すればOK)
  3. そのファイルを右クリックして「Edit with IDLE」を選択
  4. 「ゆゆシケ・コード」を「hoge.txt」という名前のテキストファイルとして保存
  5. IDLE で [F5] を押して Python スクリプトを実行
  6. hoge.mid」という名前で MIDI ができるので VOCALOID2/3 Editor でインポート
  7. 各トラック毎に「歌詞→発音記号変換」をする ※重要
u'''yuyu2midi - ゆゆシーケンサの「ゆゆシケ・コード」を歌詞付きMIDIファイルにする
'''

__author__ = 'kadotanimitsuru'
__version__='1.0.0'

import struct

FILENAME = 'hoge'  # ファイル名
MIDIBASE = 58  # 基準になるMIDIノートナンバー

# No.,音階・コード,構成音,ゆゆシケ・コード
SOLO = u'''0,休符,-,0,ん
1,B,B,1,し
2,C,C,2,ど
3,C#,C#,E,ど
4,D,D,3,れ
5,D#,D#,F,れ
6,E,E,4,み
7,F,F,5,ふぁ
8,F#,F#,G,ふぁ
9,G,G,6,そ
10,G#,G#,H,そ
11,A,A,7,ら
12,A#,A#,I,ら
13,B↑,B↑,8,し
14,C↑,C↑,9,ど
15,C#↑,C#↑,J,ど
16,D↑,D↑,A,れ
17,D#↑,D#↑,K,れ
18,E↑,E↑,B,み
19,F↑,F↑,C,ふぁ
20,F#↑,F#↑,L,ふぁ
21,G↑,G↑,D,そ
22,G#↑,G#↑,M,そ'''

Solo = {}
for i in SOLO.split('\n'):
    noteno, scale, chord, yuyu, name = i.split(',')
    Solo[yuyu] = (int(noteno), scale, chord, name)

CHORUS = u'''0,休符,-,0
1,Bm-5,B・D・F,1
2,C,C・E・G,2
3,Dm,D・F・A,3
4,Em,E・G・B↑,4
5,F,F・A・C↑,5
6,G,G・B↑・D↑,6
7,Am,A・C↑・E↑,7
8,Bm-5↑,B↑・D↑・F↑,8
9,C↑,C↑・E↑・G↑,9
10,Bm,B・D・F#,A
11,Cm,C・Eb・G,B
12,D,D・F#・A,C
13,E,E・G#・B↑,D
14,Fm,F・Ab・C↑,E
15,Gm,G・Bb↑・D↑,F
16,A,A・C#↑・E↑,G
17,Bm↑,B↑・D↑・F#↑,H
18,Cm↑,C↑・Eb↑・G↑,I
19,B,B・D#・F#,J
20,B↑,B・D#・F#,K
21,C#,C#・F・G#,L
22,D#m,D#・F#・A#,M
23,F#,F#・A#・C#↑,N
24,G#,G#・C↑・D#↑,O
25,A#m,A#・C#↑・F↑,P
26,C#↑,C#↑・F↑・G#↑,Q
27,C#m,C#・E・G#,R
28,D#,D#・G・A#,S
29,F#m,F#・A・C#↑,T
30,G#m,G#・B↑・D#↑,U
31,A#,A#・D↑・F↑,V
32,C#m↑,C#↑・E↑・G#↑,W'''

Chorus = {}
for i in CHORUS.split('\n'):
    no, scale, chord, yuyu = i.split(',')
    if yuyu == '0':
        Chorus[yuyu] = [0, 0, 0]
        continue
    notes = []
    for n in chord.split(u'・'):
        note = 0
        if n[-1] == u'↑':
            note += 12
            n = n[0:-1]
        note += ' BC D EF G A'.find(n[0])
        n = n[1:]
        if n:
            if n == '#':
                note += 1
            elif n == 'b':
                note -= 1
            else:
                raise ValueError(n)
        notes.append(note)
    Chorus[yuyu] = notes


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 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 trackname(name):
    s = name.encode('shift-jis', 'replace')
    return '\xff\x03' + chr(len(s)) + s

def makemaintrack(yuyudata, ch):
    ch %= 16  # 16トラックを超えたら 0 に戻る
    notelen = 480
    midi = [delta(0) + trackname(u'main')]
    tick = 0
    for i in yuyudata:
        noteno, scale, chord, name = Solo[i]
        if noteno:
            midi.append(
                delta(tick) +
                lyrics(name) +
                delta(0) +
                noteon(ch, MIDIBASE + noteno, 64) +
                delta(notelen) +
                noteoff(ch, MIDIBASE + noteno))
            tick = 0
        else:
            tick += notelen
    data = ''.join(midi) + delta(0) + chrs([0xff, 0x2f, 0])
    return 'MTrk' + i4(len(data)) + data

def makechorustrack(notes, ch):
    ch %= 16  # 16トラックを超えたら 0 に戻る
    notelen = 480
    midi = [delta(0) + trackname(u'chorus')]
    tick = 0
    for n in notes:
        if n:
            midi.append(
                delta(tick) +
                lyrics(u'ら') +
                delta(0) +
                noteon(ch, MIDIBASE + n, 64) +
                delta(notelen) +
                noteoff(ch, MIDIBASE + n))
            tick = 0
        else:
            tick += notelen
    data = ''.join(midi) + delta(0) + chrs([0xff, 0x2f, 0])
    return 'MTrk' + i4(len(data)) + data

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

def makemidifile(yuyudata):
    if yuyudata.startswith('YuyuSeqPlusCode'):
        data = yuyudata[len('YuyuSeqPlusCode'):]
    elif yuyudata.startswith('YuyuSeqCode'):
        data = yuyudata[len('YuyuSeqCode'):]
    else:
        ValueError(yuyudata)
    main, chorus = data.split('AngelCode')
    chorus, tempo = chorus.split('Tempo')
    print 'Tempo:', tempo
    tracks = [makeconductor()]
    tracks.append(makemaintrack(main, 0))
    c1 = []
    c2 = []
    c3 = []
    for i in chorus:
        a, b, c = Chorus[i]
        c1.append(a)
        c2.append(b)
        c3.append(c)
    tracks.append(makechorustrack(c1, 1))
    tracks.append(makechorustrack(c2, 1))
    tracks.append(makechorustrack(c3, 1))
    header = 'MThd' + i4(6) + i2(1) + i2(len(tracks)) + i2(480)
    return header + ''.join(tracks)

f = open(FILENAME + '.txt')
text = f.read().strip()
f.close()
midi = makemidifile(unicode(text))
f = open(FILENAME + '.mid', 'wb')
f.write(midi)
f.close()

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

テンポには対応していません*1ので、自前で指定してください。

UTAUでも読み込めますが、テンポ情報が無いMIDIを演奏するとゼロ除算エラーが出るようですので、UTAUでは先にテンポを指定してから演奏してください。

*1:VOCALOID Editor がテンポの読み込みに対応していないっぽい