inst39.py - ポケット・ミクで楽器音を鳴らす。 #大人の科学ポケミク

D
Python 2.7.6 + Pygame 1.9.1 用

u'''inst39.py - ポケット・ミクを楽器に

ミクさんの代わりに楽器が鳴るよ。
「A I U E O」ボタンで楽器変更。
「SHIFT」で音を伸ばす。
「VIBRATO」でビブラート。
'''
import pygame
import pygame.midi
from pygame.locals import *

WIDTH = 600  # 画面の大きさ
HEIGHT = WIDTH // 2
COLOR = 0, 255, 200  # 文字色
BG_COLOR = 100, 0, 50  # 背景色
FONT_SIZE = HEIGHT // 5
FPS = 60

MODURATION = 64  # モジュレーションの値

# ポケミクのボタン
BUTTON_A = 0x01
BUTTON_I = 0x02
BUTTON_U = 0x04
BUTTON_E = 0x08
BUTTON_O = 0x10
BUTTON_VIBRATO = 0x20
BUTTOU_SHIFT = 0x40

POKEMIKU_NOTE = 78  # ポケミクから送出されるMIDIノートナンバー

GM = { # ノートNo.:(名前, 音域下限, 音域上限)
    1:('Acoustic Grand Piano', 21, 108),
    2:('Bright Acoustic Piano', 21, 108),
    3:('Electric Grand Piano', 21, 108),
    4:('Honky-tonk Piano', 21, 108),
    5:('Electric Piano1', 28, 103),
    6:('Electric Piano 2', 28, 103),
    7:('Harpsichord', 41, 89),
    8:('Clavi', 36, 96),
    9:('Celesta', 60, 108),
    10:('Glockenspiel', 72, 108),
    11:('Music Box', 60, 84),
    12:('Vibraphone', 53, 89),
    13:('Marimba', 48, 84),
    14:('Xylophone', 65, 96),
    15:('Tubular Bells', 60, 77),
    16:('Dulcimer', 60, 84),
    17:('Drawbar Organ', 36, 96),
    18:('Percussive Organ', 36, 96),
    19:('Rock Organ', 36, 96),
    20:('Church Organ', 21, 108),
    21:('Reed Organ', 36, 96),
    22:('Accordion', 53, 89),
    23:('Harmonica', 60, 84),
    24:('Tango Accordion', 53, 89),
    25:('Acoustic Guitar (nylon)', 40, 84),
    26:('Acoustic Guitar (steel)', 40, 84),
    27:('Electric Guitar(jazz)', 40, 86),
    28:('Electric Guitar (clean)', 40, 86),
    29:('Electric Guitar (muted)', 40, 86),
    30:('Overdriven Guitar', 40, 86),
    31:('Distortion Guitar', 40, 86),
    32:('Guitar Harmonics', 40, 86),
    33:('Acoustic Bass', 28, 55),
    34:('Electric Bass(finger)', 28, 55),
    35:('Electric Bass(pick)', 28, 55),
    36:('Fretless Bass', 28, 55),
    37:('Slap Bass 1', 28, 55),
    38:('Slap Bass 2', 28, 55),
    39:('Synth Bass 1', 28, 55),
    40:('Synth Bass 2', 28, 55),
    41:('Violin', 55, 96),
    42:('Viola', 48, 84),
    43:('Cello', 36, 72),
    44:('Contrabass', 28, 55),
    45:('Tremolo Strings', 28, 96),
    46:('Pizzicato Strings', 28, 96),
    47:('Orchestral Harp', 23, 103),
    48:('Timpani', 36, 57),
    49:('String Ensembles 1', 28, 96),
    50:('String Ensembles 2', 28, 96),
    51:('SynthStrings 1', 36, 96),
    52:('SynthStrings 2', 36, 96),
    53:('Choir Aahs', 48, 79),
    54:('Voice Oohs', 48, 79),
    55:('Synth Voice', 48, 84),
    56:('Orchestra Hit', 48, 72),
    57:('Trumpet', 58, 94),
    58:('Trombone', 34, 75),
    59:('Tuba', 29, 55),
    60:('Muted trumpet', 58, 82),
    61:('French Horn', 41, 77),
    62:('Brass Section', 36, 96),
    63:('Synth Brass 1', 36, 96),
    64:('Synth Brass 2', 36, 96),
    65:('Soprano Sax', 54, 87),
    66:('Alto Sax', 49, 80),
    67:('Tenor Sax', 42, 75),
    68:('Baritone Sax', 37, 68),
    69:('Oboe', 58, 91),
    70:('English Horn', 52, 81),
    71:('Bassoon', 34, 72),
    72:('Clarinet', 50, 91),
    73:('Piccolo', 74, 108),
    74:('Flute', 60, 96),
    75:('Recorder', 60, 96),
    76:('Pan Flute', 60, 96),
    77:('Blown Bottle', 60, 96),
    78:('Shakuhachi', 55, 84),
    79:('Whistle', 60, 96),
    80:('Ocarina', 60, 84),
    81:('Lead 1(square)', 21, 108),
    82:('Lead 2(sawtooth)', 21, 108),
    83:('Lead 3(calliope)', 36, 96),
    84:('Lead 4(chiff)', 36, 96),
    85:('Lead 5(charang)', 36, 96),
    86:('Lead 6(voice)', 36, 96),
    87:('Lead 7(fifths)', 36, 96),
    88:('Lead 8(bass + lead)', 21, 108),
    89:('Pad 1(new age)', 36, 96),
    90:('Pad 2(warm)', 36, 96),
    91:('Pad 3(polysynth)', 36, 96),
    92:('Pad 4(choir)', 36, 96),
    93:('Pad 5(bowed)', 36, 96),
    94:('Pad 6(metallic)', 36, 96),
    95:('Pad 7(halo)', 36, 96),
    96:('Pad 8(sweep)', 36, 96),
    97:('FX 1(rain)', 36, 96),
    98:('FX 2(soundtrack)', 36, 96),
    99:('FX 3(crystal)', 36, 96),
    100:('FX 4(atmosphere)', 36, 96),
    101:('FX 5(brightness)', 36, 96),
    102:('FX 6(goblins)', 36, 96),
    103:('FX 7(echoes)', 36, 96),
    104:('FX 8(sci-fi)', 36, 96),
    105:('Sitar', 48, 77),
    106:('Banjo', 48, 84),
    107:('Shamisen', 50, 79),
    108:('Koto', 55, 84),
    109:('Kalimba', 48, 79),
    110:('Bag pipe', 36, 77),
    111:('Fiddle', 55, 96),
    112:('Shanai', 48, 72),
    113:('Tinkle Bell', 72, 84),
    114:('Agogo', 60, 72),
    115:('Steel Drums', 52, 76),
    116:('Woodblock', 0, 127),
    117:('Taiko Drum', 0, 127),
    118:('Melodic Tom', 0, 127),
    119:('Synth Drum', 0, 127),
    120:('Reverse Cymbal', 0, 127),
    121:('Guitar Fret Noise', 0, 127),
    122:('Breath Noise', 0, 127),
    123:('Seashore', 0, 127),
    124:('Bird Tweet', 0, 127),
    125:('Telephone Ring', 0, 127),
    126:('Helicopter', 0, 127),
    127:('Applause', 0, 127),
    128:('Gunshot', 0, 127)
    }
GM_CATEGORY = '''
Piano
Chromatic Percussion
Organ
Guitar
Bass
Strings
Ensemble
Brass
Reed
Pipe
Synth Lead
Synth Pad
Synth Effects
Ethnic
Percussive
Sound effects
'''.strip().split('\n')

def set_lyric(output, lyric):
    output.write_sys_ex(
        0, [0xF0, 0x43, 0x79, 0x09, 0x11, 0x0A, 0x00, lyric, 0xF7])

def set_pitch_bend_sensitivity(output, ch, value):
    output.write_short(0xB0 | ch, 101, 0)
    output.write_short(0xB0 | ch, 100, 0)
    output.write_short(0xB0 | ch, 6, 16)

def put_text(surface, font, text, dest, color=COLOR):
    x, y = dest
    for t in text.split('\n'):
        s = font.render(t, True, color)
        surface.blit(s, (x, y))
        y += font.get_linesize()

def set_inst(output, inst, ch=1):
    output.set_instrument(inst, ch)
    name, lower, upper = GM[inst + 1]
    average = (lower + upper) / 2.0
    note_shift = int((average - POKEMIKU_NOTE) / 12) * 12  # ポケミクとの音程差
    return note_shift
    
    
def main():
    pygame.init()
    pygame.midi.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption('inst39.py')
    font = pygame.font.SysFont(pygame.font.get_default_font(), FONT_SIZE)
    for i in range(pygame.midi.get_count()):
        interf, name, input, output, opened = pygame.midi.get_device_info(i)
        if output and name == 'NSX-39 ':
            midiout = pygame.midi.Output(i)
        if input and name == 'NSX-39 ':
            midiin = pygame.midi.Input(i)
    clock = pygame.time.Clock()
    clock.tick(FPS)
    inst = 0  # プログラム番号初期値(-1されている)
    note_shift = set_inst(midiout, inst)
    set_pitch_bend_sensitivity(midiout, 1, 16)
    midiout.write_short(0xB0, 7, 0)  # ミクさんの音量を0に
    last_note = None  # 最後に送出したノート番号
    moduration = 0  # モジュレーション ON/OFF
    sustain = False  # 次のノートオンまで音を続けるか
    while True:
        sysex = False
        for e in pygame.event.get():
            if (e.type is QUIT) or (e.type is KEYDOWN and e.key is K_ESCAPE):
                if last_note is not None:
                    midiout.note_off(last_note, 64, 1)
                midiout.write_short(0xB0, 7, 64)  # ミクさんの音量を64に
                return
        if midiin.poll():
            for e in midiin.read(1000):
                (status,data1,data2,data3), timestamp = e
                if sysex and status == 0x11 and data1 == 0x20:
                    if data3 & BUTTON_A:
                        inst = (inst - 8) & 0x7f
                        note_shift = set_inst(midiout, inst)
                    if data3 & BUTTON_I:
                        inst = (inst - 1) & 0x7f
                        note_shift = set_inst(midiout, inst)
                    if data3 & BUTTON_U:
                        inst = (inst + 1) & 0x7f
                        note_shift = set_inst(midiout, inst)
                    if data3 & BUTTON_E:
                        inst = (inst + 8) & 0x7f
                        note_shift = set_inst(midiout, inst)
                    if data3 & BUTTON_O:
                        inst = (inst + 32) & 0x7f
                        note_shift = set_inst(midiout, inst)
                    if data3 & BUTTOU_SHIFT:
                        sustain = True
                        midiout.write_short(0xB1, 64, 127)
                    if not (data3 & BUTTOU_SHIFT) and sustain:
                        sustain = False
                        midiout.write_short(0xB1, 64, 0)
                    if data3 & BUTTON_VIBRATO:
                        moduration = MODURATION
                        midiout.write_short(0xB1, 1, moduration)
                    if not (data3 & BUTTON_VIBRATO):
                        moduration = 0
                        midiout.write_short(0xB1, 1, moduration)
                if status  == 0xF0:  # SysEx start
                    sysex = True
                if status == 0xF7:  # SysEx end
                    sysex = False
                if (status & 0xF0) == 0x90:  # Note On
                    if last_note is not None:
                        midiout.note_off(last_note, data2, 1)
                    last_note = data1 + note_shift
                    midiout.note_on(last_note, data2, 1)
                if (status & 0xF0) == 0x80:  # Note Off
                    if (not sustain) and (last_note is not None):
                        midiout.note_off(last_note, data2, 1)
                        last_note = None
                if (status & 0xF0) == 0xE0:  # ピッチベンド
                    midiout.write_short(0xE1, data1, data2)
        screen.fill(BG_COLOR)
        text = u'''%s:
 %3d: %s

       %s

%s
''' % (
            GM_CATEGORY[inst // 8],
            inst + 1, GM[inst + 1][0],
            ('sustain', 'SUSTAIN')[sustain],
            ('vibrato', 'VIBRATO')[bool(moduration)])
        put_text(screen, font, text, (10, 10))
        clock.tick(FPS)
        pygame.display.flip()

if  __name__ == '__main__':
    try:
        main()
    finally:
        pygame.quit()

# Public Domain. 好きに流用してください。

なんかサスティンの効き方がおかしい気がする。NSX-39 のせいなのかプログラムがマズイのか?