メモ

メモ、雑記、etc

ピアノロール表示

2010-12-30 01:20:12 | programming python

pygletでピアノロール画像とwaveファイルを同期させてみる。
なんとなくそれっぽい。
GTK等は大変そうだったので、pygletサンプルコードをちょっと書き足しただけです。
その割りには時間がかかってしまいましたが…。



-- roll_player.py --





#! /usr/bin/env python
# -*- coding: utf-8 -*-

'''
ピアノロールとwaveファイルを同期再生
2010-12-28 SEKIWA

piano_roll.py で作成したピアノロールを読み込み、waveファイル再生と同期させる
入力はwaveファイルのみ対応.
リアルタイムでFFT変換・ピアノロール表示は難しかったので、以下の順で動作させる.
1. python piano_roll.py xxx.wav
これで xxx.wav と同じディレクトリに次の2つのファイルが生成される.
・xxx.wav.db.png(解析結果)
・xxx.wav.db.cfg(解析結果をピアノロールに変換するための設定)
・xxx.wav.db.dir(ディレクトリ. この中に 000.png から連番で解析結果が生成)

2. python roll_player.py xxx.wav
xxx.wav.db.dirディレクトリが存在すれば、xxx.wavをロードし、ピアノロールを表示する.

original: pyglet Copyright (c) 2006-2008 Alex Holkner
10-12-28 WaveTone みたいなことを実現したくて取り組み中.
'''
import sys
import os
import pyglet
from pyglet.gl import *
from pyglet.window import key
import ConfigParser
import glob
from PIL import Image

def draw_rect(x, y, width, height):
glBegin(GL_LINE_LOOP)
glVertex2f(x, y)
glVertex2f(x + width, y)
glVertex2f(x + width, y + height)
glVertex2f(x, y + height)
glEnd()

def fill_rect(x, y, width, height):
glColor3d(1, 0, 0)
glBegin(GL_POLYGON)
glVertex2f(x, y)
glVertex2f(x + width, y)
glVertex2f(x + width, y + height)
glVertex2f(x, y + height)
glEnd()
glColor3d(1, 1, 1)

class Control(pyglet.event.EventDispatcher):
x = y = 0
width = height = 10

def __init__(self, parent):
super(Control, self).__init__()
self.parent = parent

def hit_test(self, x, y):
return (self.x < x > self.x + self.width and
self.y < y > self.y + self.height)

def capture_events(self):
self.parent.push_handlers(self)

def release_events(self):
self.parent.remove_handlers(self)

class Label(Control):
''' 文字の表示コントロール '''
def __init__(self, *args, **kwargs):
super(Label, self).__init__(*args, **kwargs)
self._text = pyglet.text.Label('', anchor_x='center', anchor_y='center')

def draw(self):
glColor3f(0, 1, 0)
draw_rect(self.x, self.y, self.width, self.height)
self.draw_label()
glColor3f(1, 1, 1)

def draw_label(self):
self._text.x = self.x + self.width / 2
self._text.y = self.y + self.height / 2
self._text.draw()

def set_text(self, text):
self._text.text = text

text = property(lambda self: self._text.text, set_text)

class Button(Control):
charged = False

def draw(self):
if self.charged:
glColor3f(1, 0, 0)
draw_rect(self.x, self.y, self.width, self.height)
glColor3f(1, 1, 1)
self.draw_label()

def on_mouse_press(self, x, y, button, modifiers):
self.capture_events()
self.charged = True

def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
self.charged = self.hit_test(x, y)

def on_mouse_release(self, x, y, button, modifiers):
self.release_events()
if self.hit_test(x, y):
self.dispatch_event('on_press')
self.charged = False

Button.register_event_type('on_press')

class TextButton(Button):
def __init__(self, *args, **kwargs):
super(TextButton, self).__init__(*args, **kwargs)
self._text = pyglet.text.Label('', anchor_x='center', anchor_y='center')

def draw_label(self):
self._text.x = self.x + self.width / 2
self._text.y = self.y + self.height / 2
self._text.draw()

def set_text(self, text):
self._text.text = text

text = property(lambda self: self._text.text, set_text)

class Slider(Control):
THUMB_WIDTH = 6
THUMB_HEIGHT = 10
GROOVE_HEIGHT = 2

def draw(self):
center_y = self.y + self.height / 2
draw_rect(self.x, center_y - self.GROOVE_HEIGHT / 2,
self.width, self.GROOVE_HEIGHT)
pos = self.x + self.value * self.width / (self.max - self.min)
draw_rect(pos - self.THUMB_WIDTH / 2, center_y - self.THUMB_HEIGHT / 2,
self.THUMB_WIDTH, self.THUMB_HEIGHT)

def coordinate_to_value(self, x):
return float(x - self.x) / self.width * (self.max - self.min) + self.min

def on_mouse_press(self, x, y, button, modifiers):
value = self.coordinate_to_value(x)
self.capture_events()
self.dispatch_event('on_begin_scroll')
self.dispatch_event('on_change', value)

def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
value = min(max(self.coordinate_to_value(x), self.min), self.max)
self.dispatch_event('on_change', value)

def on_mouse_release(self, x, y, button, modifiers):
self.release_events()
self.dispatch_event('on_end_scroll')

Slider.register_event_type('on_begin_scroll')
Slider.register_event_type('on_end_scroll')
Slider.register_event_type('on_change')

class PianoRoll(Control):
''' 画像を表示, 再生位置に同期して縦線を表示 '''
GUI_IMAGE_KEYWIDTH = 20
GUI_IMAGE_SECOND = 10
PF_KEYWIDTH = 7

image = None
charged = False
img_list = []
cur_page = 0
pix_linesec = 0.01
value = 0.0 # playerの再生時間
mouse_pos = [-1, -1]

def draw(self):
if self.image:
self.image.blit(self.x, self.y, width=self.width, height=self.height)

cur_page = int(self.value / self.GUI_IMAGE_SECOND)
page_line = int(self.GUI_IMAGE_SECOND / self.pix_linesec)
xpos = self.GUI_IMAGE_KEYWIDTH + int((self.value / self.pix_linesec) % page_line)
if not self.cur_page == cur_page:
if cur_page > len(self.img_list) - 1:
cur_page = len(self.img_list) - 1
xpos = self.GUI_IMAGE_KEYWIDTH + page_line
if cur_page < 0:
cur_page = 0
xpos = self.GUI_IMAGE_KEYWIDTH
self.cur_page = cur_page
self.image = pyglet.image.load(self.img_list[self.cur_page])

# 再生位置
glColor3f(1.0,.1,.1)
# opengl line-mode
glBegin(GL_LINES)
# arena vertices
arena_verts = [ (xpos, self.y), (xpos, self.y+self.height) ]
glVertex2f(*arena_verts[0])
glVertex2f(*arena_verts[1])
glEnd()
glColor3f(1.0,1.0,1.0)

# key 位置
if self.mouse_pos[1] > 0:
fill_rect(self.GUI_IMAGE_KEYWIDTH / 2, (self.mouse_pos[1]-self.x)/(self.PF_KEYWIDTH+1)*(self.PF_KEYWIDTH+1)+1, self.GUI_IMAGE_KEYWIDTH/2, self.PF_KEYWIDTH)

def set_image(self, img_list, pix_linesec):
''' load image and set form size '''
self.img_list = img_list
self.pix_linesec = pix_linesec
img_size = [0, 0]
for file in self.img_list:
fin = open(file, 'r')
img = Image.open(fin)
img_size[0] = max(img_size[0], img.size[0])
img_size[1] = max(img_size[1], img.size[1])
fin.close()
del img
self.image = pyglet.image.load(self.img_list[self.cur_page])
self.width = img_size[0]
self.height = img_size[1]
return img_size

def on_mouse_press(self, x, y, button, modifiers):
self.capture_events()
self.charged = True

def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
self.charged = self.hit_test(x, y)

def on_mouse_release(self, x, y, button, modifiers):
self.release_events()
if self.hit_test(x, y) and x > self.GUI_IMAGE_KEYWIDTH:
self.value = self.cur_page * self.GUI_IMAGE_SECOND + (x - self.GUI_IMAGE_KEYWIDTH) * self.pix_linesec
self.dispatch_event('on_press')
self.charged = False

def on_mouse_motion(self, x, y, dx, dy):
if self.hit_test(x, y) and x > self.GUI_IMAGE_KEYWIDTH:
self.mouse_pos = [x, y]
else:
self.mouse_pos = [-1, -1]

PianoRoll.register_event_type('on_press')

class PlayerWindow(pyglet.window.Window):
GUI_WIDTH = 400
GUI_HEIGHT = 40
GUI_PADDING = 4
GUI_BUTTON_HEIGHT = 16

def __init__(self, player, img_list, pix_linesec):
super(PlayerWindow, self).__init__(caption='Media Player',
visible=False,
resizable=True)
self.player = player
self.player.push_handlers(self)
self.player.eos_action = self.player.EOS_PAUSE

self.slider = Slider(self)
self.slider.x = self.GUI_PADDING
self.slider.y = self.GUI_PADDING * 2 + self.GUI_BUTTON_HEIGHT
self.slider.on_begin_scroll = lambda: player.pause()
self.slider.on_end_scroll = lambda: player.play()
self.slider.on_change = lambda value: player.seek(value)

self.play_pause_button = TextButton(self)
self.play_pause_button.x = self.GUI_PADDING
self.play_pause_button.y = self.GUI_PADDING
self.play_pause_button.height = self.GUI_BUTTON_HEIGHT
self.play_pause_button.width = 45
self.play_pause_button.on_press = self.on_play_pause

self.window_label = Label(self)
self.window_label.x = self.GUI_PADDING + self.play_pause_button.width + self.GUI_PADDING * 2
self.window_label.y = self.GUI_PADDING
self.window_label.height = self.GUI_BUTTON_HEIGHT
self.window_label.width = 100
self.window_label.set_text('0')

self.pianoroll = PianoRoll(self)
self.pianoroll.x = 0
self.pianoroll.y = self.GUI_HEIGHT
self.img_size = self.pianoroll.set_image(img_list, pix_linesec)
self.pianoroll.on_press = self.on_play_pianoroll

self.controls = [
self.slider,
self.play_pause_button,
self.window_label,
self.pianoroll,
]

def on_eos(self):
self.gui_update_state()

def gui_update_source(self):
if self.player.source:
audiosource = self.player.source
self.slider.min = 0.
self.slider.max = audiosource.duration
self.gui_update_state()

def gui_update_state(self):
if self.player.playing:
self.play_pause_button.text = 'Pause'
else:
self.play_pause_button.text = 'Play'

def gui_update_image(self):
if self.slider.value:
self.window_label.set_text('%7.2f' % self.slider.value)

def get_video_size(self):
if not self.player.source or not self.player.source.video_format:
return self.img_size

def set_default_video_size(self):
'''Make the window size just big enough to show the current
video and the GUI.'''
width = self.GUI_WIDTH
height = self.GUI_HEIGHT
video_width, video_height = self.get_video_size()
width = max(width, video_width)
height += video_height
self.set_size(int(width), int(height))

def on_resize(self, width, height):
'''Position and size video image.'''
super(PlayerWindow, self).on_resize(width, height)

self.slider.width = width - self.GUI_PADDING * 2

height -= self.GUI_HEIGHT
if height <= 0:
return

video_width, video_height = self.get_video_size()
if video_width == 0 or video_height == 0:
return

display_aspect = width / float(height)
video_aspect = video_width / float(video_height)
if video_aspect > display_aspect:
self.video_width = width
self.video_height = width / video_aspect
else:
self.video_height = height
self.video_width = height * video_aspect
self.video_x = (width - self.video_width) / 2
self.video_y = (height - self.video_height) / 2 + self.GUI_HEIGHT
print 'on_resize'

def on_mouse_press(self, x, y, button, modifiers):
for control in self.controls:
if control.hit_test(x, y):
control.on_mouse_press(x, y, button, modifiers)

def on_key_press(self, symbol, modifiers):
if symbol == key.SPACE:
self.on_play_pause()
elif symbol == key.ESCAPE:
self.dispatch_event('on_close')

def on_close(self):
self.player.pause()
self.close()

def on_play_pause(self):
if self.player.playing:
self.player.pause()
else:
if self.player.time >= self.player.source.duration:
self.player.seek(0)
self.player.play()
self.gui_update_state()

def on_mouse_motion(self, x, y, dx, dy):
''' PianoRollコントロールでのon_moveイベント '''
if self.pianoroll.hit_test(x, y):
self.pianoroll.on_mouse_motion(x, y, dx, dy)

def on_play_pianoroll(self):
''' PianoRollコントロールで on_mouse_release が実行されたときに呼び出される '''
if self.player.playing:
self.player.pause()
self.player.seek(self.pianoroll.value)
self.gui_update_state()

def on_draw(self):
self.clear()

# GUI
self.slider.value = self.player.time
self.pianoroll.value = self.player.time
for control in self.controls:
control.draw()

# Video
self.gui_update_image()


if __name__ == '__main__':
if len(sys.argv) < 1:
print 'Usage: %s <filename(wave only)>' % sys.argv[0]
sys.exit(1)

filename = '/home/sekiwa/tmp/track04.cdda.wav'
if len(sys.argv) > 1:
filename = sys.argv[1]
if not os.path.exists(filename):
print 'Fatal: cannot find %s.' % filename
sys.exit(1)

# piano rolls
cur_path = os.path.dirname(filename)
cur_name = os.path.basename(filename)
pngfile = os.path.join(cur_path, cur_name + '.db.png')
inifile = os.path.join(cur_path, cur_name + '.db.cfg')
if not os.path.exists(pngfile):
print '解析結果(dB画像)が見つかりません'
sys.exit(1)
if not os.path.exists(inifile):
print '解析結果(cfgファイル)が見つかりません'
sys.exit(1)
# db値をiniから読み込み
ini = ConfigParser.SafeConfigParser()
ini.read(inifile)
pix2db = range(0, 256)
itemdB = ini.items('dB')
for i in range(0, min(len(pix2db), len(itemdB))):
pix2db[int(itemdB[i][0])] = float(itemdB[i][1])
pix_linesec = float(ini.get('rate', 'linesec'))
pf_depth = int(ini.get('pianoroll', 'header'))
page_sec = int(ini.get('pianoroll', 'second'))

# ピアノロールを読み込み
cur_pianodir = os.path.join(cur_path, cur_name + '.db.dir')
cur_pianoimgs = os.path.join(cur_pianodir, '*.png')
img_files = []
for file in glob.glob(cur_pianoimgs):
img_files.append(file)
img_files.sort()

# audio, window
player = pyglet.media.Player()
window = PlayerWindow(player, img_files, pix_linesec)

audiosource = pyglet.media.load(filename)
player.queue(audiosource)

window.gui_update_source()
window.set_default_video_size()
window.set_visible(True)
window.gui_update_state()

pyglet.app.run()





piano roll with python (1-3)

2010-12-25 23:11:30 | programming python

試しにピアノロール出力した画像を張り付けました。


# def draw_pianoroll(filename):の続き

        # FFTとマップ書き込み
        fin_pos = fin.tell()
        nloop = 0
        maxdb = -99.0
        remain = _nframes
        while remain > 0:
            # FFT
            bufsize = min(wav_chunk, remain)
            rowdata = fin.readframes(bufsize)
            arydata = scipy.fromstring(rowdata, dtype)
            if _channel == 2:   # stereo -> mono
                left  = scipy.int32(arydata[::2])
                right = scipy.int32(arydata[1::2])
                arydata = scipy.int16((left + right) / 2)
            len_ary = len(arydata)
            narydata = arydata / 2.0 / (2 ** 15)
            fftdata = scipy.fft(narydata)
            # power spectrum
            len_real = int(math.ceil((len_ary + 1) / 2.0))
            fftdata = fftdata[0:len_real]
            fftdata = fftdata * 2.0 / len_ary   # len_realではないことに注意
            fftdata = fftdata ** 2
            pw = 10 * scipy.log10(fftdata)
            remain -= bufsize
           
            # マップ書き込み
            cur_pos = img_start + int(nloop * cur_linerate + 0.5)
            for pwcnt in range(dis_min, min(dis_max, len_real)):
                if mod_fine:
                    if pos_cent[pwcnt] > 0 and cur_pos > 0 and cur_pos < img_height:
                        _check = db2pix(pw[pwcnt])
                        if img_max.getpixel((pos_cent[pwcnt], cur_pos)) < _check:
                            draw_max.point((pos_cent[pwcnt], cur_pos), fill=_check)
                            if pw[pwcnt] > wav_db:
                                color_base_idx = min(color_size-1, int( ((pw[pwcnt] - wav_db)) / (wav_db_max - wav_db) * color_size) )
                                draw.ellipse((pos_cent[pwcnt]-1, cur_pos-1, pos_cent[pwcnt]+1, cur_pos+1), fill=color_base[color_base_idx])
                else:
                    if pw[pwcnt] > wav_db and pos_cent[pwcnt] > 0:
                        color_base_idx = min(color_size-1, int( ((pw[pwcnt] - wav_db)) / (wav_db_max - wav_db) * color_size) )
                        draw.ellipse((pos_cent[pwcnt]-1, cur_pos-1, pos_cent[pwcnt]+1, cur_pos+1), fill=color_base[color_base_idx])
            if cur_pos >= img_height:
                break
            nloop += 1
           
            maxdb = max(maxdb, max(pw))
            if nloop % 50 == 0: print ' ' + str('%.0f' % maxdb),; maxdb = -99.0
       
        # ファイルポインタを戻す
        fin.setpos(fin_pos)
        print '.'
   
    fin.close()
   
    print 'pass %d [s]' % (wav_chunk / float(_framerate) * (nloop - 1))
    image.rotate(90).show()
    if mod_fine: img_max.rotate(90).show()

if __name__ == '__main__':
    if len(sys.argv) < 1:
        print 'Usage: %s ' % sys.argv[0]
        sys.exit(1)

    filename = '/tmp/track04.cdda.wav'
    if len(sys.argv) > 1:
        filename = sys.argv[1]
   
    draw_pianoroll(filename)

サンプル画像

ベース系は空間分解能がいまいちですね。 Waveletとかの方が良さそうですが、自力で書く能力はない…。


piano roll with python (1-2)

2010-12-25 23:05:47 | programming python

その2の本体。 関数が文字数制限にひっかかってしまって、さらに分割しました。


def draw_pianoroll(filename):
    ''' fft を行い、ピアノロール上にドットを描く '''
    global wav_chunk
    global wav_db

    # ファイルチェック
    fin = wave.open(filename, 'rb')
    (_channel, _sampwidth, _framerate, _nframes, _, _) = fin.getparams()
    print 'channel:%d, samplewidth:%d[byte], framerate:%d[Hz], frames:%d' % (_channel, _sampwidth, _framerate, _nframes)
    if _channel > 2:
        print 'モノラル/ステレオのみに対応しています.'
        return 0
    if _sampwidth > 2:
        print '未対応のデータ並びです.'
        return 0
    dtype = scipy.int16
    if _sampwidth == 1:
        dtype = scipy.int8
   
    # ピアノロール
    image = create_roll()
    (img_width, img_height) = image.size
    draw = ImageDraw.Draw(image)
    # ピアノロール A4 での座標
    pos_a4 = (pf_keywidth + 1) * (pf_inoct * 3 + 9) + 1 + (pf_keywidth / 2)
    # FFT結果書き込み開始位置
    img_start = pf_depth + 1
    # dB最大値保持map -80[dB]-0[dB] -> 0-255
    if mod_fine:
        img_max = Image.new('L', image.size, 0)
        draw_max = ImageDraw.Draw(img_max)
        def db2pix(db):
            return int( (min(wav_db_max, max(-80,db)) + 80.0) / 80.0 * 255 )
    print 'image:%d x %d, total lines for this songs:%d' % (img_width, img_height, int(_nframes / _framerate / pix_linesec))

    # FFT基数
    # 平均律の計算は http://en.wikipedia.org/wiki/Piano_key_frequencies を参照
    const_nth_key = -49
    base_cent_n = -8
    base_freqs = [wav_tuning * math.pow(2.0, 1.0/pf_inoct) ** (i + const_nth_key) for i in range(base_cent_n, base_cent_n + pf_inoct)]
    #base_keys  = ['A0 ','A#0','B0 ','C1 ','C#1','D1 ','D#1','E1 ','F1 ','F#1','G1 ','G#1']
    base_keys  = ['C0 ','C#0','D0 ','D#0','E0 ','F0 ','F#0','G0 ','G#0','A0 ','A#0','B0 ']
    color_size = 30
    color_base = get_hue_color(color_size)  # wav_db[dB] <-> -10[dB]
   
    # C0からB0の基数で倍音を計算し、オクターブ(バンド?)だけ拾ってみる
    for base_cnt, base_freq in enumerate(base_freqs):
        if mod_fine == 0:
            if base_cnt == 1: break #wav_chunk *= 2
            if base_cnt == 2: break

        # マップ用座標の準備 (周波数 <=> cent(C1~C8) <=> pixel位置)
        # A4 (const_nth_key) を中心として、その座標からの相対値とする.
        # C1:N=-45, C8:N=39
        # 2の累乗じゃなくなるけど、遅いなりに動作はしているようだ...
        if mod_fine:
            wav_chunk = int(_framerate / base_freq + 0.5)
        print '%s freq:%4.1f, chunk:%4d, ' % (base_keys[base_cnt], base_freq, wav_chunk),
        oct_cent_n = [const_nth_key + base_cent_n + base_cnt + pf_inoct * i for i in range(0, 10)]
        min_res = (_framerate / float(wav_chunk)) / wav_tuning
        cent = [1200.0 * math.log(0.5 * min_res, 2)] + [1200.0 * math.log(i * min_res, 2) for i in range(1, wav_chunk)]
        pos_cent = []   # pixel位置
        # ピアノロールの範囲(C1-B7)で制限
        min_cent = 100.0 * -45 - 50 # -45 = C1
        max_cent = 100.0 *  38 + 50 #  38 = B7
        min_resolut = 100 / pf_keywidth
        dis_min = 1
        dis_max = wav_chunk
        for cnt in range(0, wav_chunk):
            if cent[cnt] < min_cent:
                pos_cent += [-1]    # 負数で座標無効
                dis_min = cnt
                continue
            if cent[cnt] >= max_cent:
                dis_max = cnt - 1
                break
            cent_n = int(abs(cent[cnt]) / 100.0 + 0.5)  # cent_nは-45(C1)~39(C8)のはず
            if cent[cnt] < 0:
                cent_n = -cent_n
            #print cent_n,    # Nの値
            cent_sub = cent[cnt] - cent_n * 100         # cent_subは-50~49[cent]のはず
            pix_fluct = int((cent_sub + 50) / min_resolut - pf_keywidth / 2)
            pos_cent += [pos_a4 + cent_n * (pf_keywidth + 1) + pix_fluct]
           
            # オクターブのみ有効
            # cent_n が oct_cent_n のどれかに一致しなければオクターブ以外として無効にする
            if mod_fine:
                _check = 0
                for oct in oct_cent_n:
                    if cent_n == oct:
                        _check = 1
                        break
                if _check == 0:
                    pos_cent[len(pos_cent)-1] = -1

        # 1FFTあたりの時間比率
        cur_linerate = (wav_chunk / float(_framerate)) / pix_linesec

        # (1-3)に続く
</pre>

1-3へ続く


piano roll with python (1-1)

2010-12-25 22:53:52 | programming python

WaveToneみたいなことをUbuntuで行いたかったものの、それっぽいものが見つからず、なければ作っちゃえ、ということでpythonにて作成してます。ピアノロールの画像出力だけは何とかできたので防備録を兼ねて公開します。

GNOMEで作り直したい所ですが、敷居が高い。再描画メッセージの使い方はどうなってるんだろう…。あと、waveファイルの再生同期もしたいところ。

久しぶりにプログラムを組むと楽しい。それにしても、FFTをすっかり忘れてしまっていて、学生のときに理解していたのかも怪しい。よく卒業できたなー。

文字数制限を受けたので、関数で分けて投稿します。初めから関数をファイルで分割しておけば良いだけの話ですが、自分用のメモということで。gooブログは使いにくい…。
#! /usr/bin/env python
# -*- coding: utf-8 -*-

from PIL import Image, ImageDraw
import wave
import scipy
import math
import colorsys

pf_keywidth = 7     # width [pixel]
pf_depth    = 20    # heidht [pixel]
pf_octs     = 7     # total octaves
pf_inoct    = 12    # 12 keys in an octave

wav_chunk = 1024    # 周波数解像度がよろしくない
wav_chunk = 2048    # 周波数解像度は改善されるが、時間分解能がよろしくない
wav_db_max = -15    # 記録用のdB最大値. これより大きな値はすべてこの値で記載する. 必ず負数.
wav_db = -40        # 描画に有効とするdB値. 必ず負数.
wav_tuning = 440    # tuning A4 [Hz]
pix_linesec = 0.01  # 描画での1ラインの秒数
pix_showline = 1    # 描画での罫線を行う秒数間隔
mod_fine = 0        # 0:高速計算, 1:詳細計算(少ししか効果ない...)

def create_roll():
    ''' ピアノロールの背景画像 '''
    color_white = "#fff"
    color_black = "#000"
    color_gray  = "#aaa"
    color_red   = "#f00"
    color_perp  = "#f8f"
   
    img_width = (pf_keywidth + 1) * pf_inoct * pf_octs + 1
    img_height = 1000
    img = Image.new('RGB', (img_width, img_height), (255,255,255))
    #img = Image.new('RGB', (img_width, img_height), (0,0,0))
   
    # write roll header
    draw = ImageDraw.Draw(img)
    _rect = [0, 0, pf_keywidth + 1, pf_depth]
    for oct in range(0, pf_octs):
        _rect1 = [i for i in _rect]
        draw.line((_rect[0], 0, _rect[0], img_height - 1), fill=color_perp)
        if oct == 3:
            draw.line((_rect[0], 0, _rect[0], img_height - 1), fill=color_red)
        draw.rectangle(_rect, outline=color_gray, fill=color_white)
        _rect[0] += pf_keywidth + 1; _rect[2] += pf_keywidth + 1
        draw.line((_rect[0], 0, _rect[0], img_height - 1), fill=color_gray)
        draw.rectangle(_rect, outline=color_gray, fill=color_black)
        _rect[0] += pf_keywidth + 1; _rect[2] += pf_keywidth + 1
        draw.line((_rect[0], 0, _rect[0], img_height - 1), fill=color_gray)
        draw.rectangle(_rect, outline=color_gray, fill=color_white)
        _rect[0] += pf_keywidth + 1; _rect[2] += pf_keywidth + 1
        draw.line((_rect[0], 0, _rect[0], img_height - 1), fill=color_gray)
        draw.rectangle(_rect, outline=color_gray, fill=color_black)
        _rect[0] += pf_keywidth + 1; _rect[2] += pf_keywidth + 1
        draw.line((_rect[0], 0, _rect[0], img_height - 1), fill=color_gray)
        draw.rectangle(_rect, outline=color_gray, fill=color_white)
        _rect[0] += pf_keywidth + 1; _rect[2] += pf_keywidth + 1
        draw.line((_rect[0], 0, _rect[0], img_height - 1), fill=color_gray)
        draw.rectangle(_rect, outline=color_gray, fill=color_white)
        _rect[0] += pf_keywidth + 1; _rect[2] += pf_keywidth + 1
        draw.line((_rect[0], 0, _rect[0], img_height - 1), fill=color_gray)
        draw.rectangle(_rect, outline=color_gray, fill=color_black)
        _rect[0] += pf_keywidth + 1; _rect[2] += pf_keywidth + 1
        draw.line((_rect[0], 0, _rect[0], img_height - 1), fill=color_gray)
        draw.rectangle(_rect, outline=color_gray, fill=color_white)
        _rect[0] += pf_keywidth + 1; _rect[2] += pf_keywidth + 1
        draw.line((_rect[0], 0, _rect[0], img_height - 1), fill=color_gray)
        draw.rectangle(_rect, outline=color_gray, fill=color_black)
        _rect[0] += pf_keywidth + 1; _rect[2] += pf_keywidth + 1
        draw.line((_rect[0], 0, _rect[0], img_height - 1), fill=color_gray)
        draw.rectangle(_rect, outline=color_gray, fill=color_white)
        _rect[0] += pf_keywidth + 1; _rect[2] += pf_keywidth + 1
        draw.line((_rect[0], 0, _rect[0], img_height - 1), fill=color_gray)
        draw.rectangle(_rect, outline=color_gray, fill=color_black)
        _rect[0] += pf_keywidth + 1; _rect[2] += pf_keywidth + 1
        draw.line((_rect[0], 0, _rect[0], img_height - 1), fill=color_gray)
        draw.rectangle(_rect, outline=color_gray, fill=color_white)
        _rect[0] += pf_keywidth + 1; _rect[2] += pf_keywidth + 1
       
        _rect1[2] = _rect[2]
        _rect1[3] = _rect[3] / 2
        _text1 = '%d' % (oct + 1)
        draw.rectangle(_rect1, outline=color_gray, fill=color_white)
        draw.text(((_rect1[0] + _rect1[2])/2, 1), _text1, fill=color_black)
    # last line
    draw.line((_rect[0], 0, _rect[0], img_height - 1), fill=color_gray)
   
    # pix_showline 秒毎の線を引く
    showline_time = pix_showline
    cur_line = pf_depth + 1 + int(pix_showline / pix_linesec + 0.5)
    remain = 1000 - cur_line
    while remain > 0:
        # 時間[s]
        draw.text((0, cur_line), str(showline_time), fill=color_black)
        showline_time += pix_showline
        # 罫線
        draw.line((0,cur_line, img_width-1,cur_line), fill=color_gray)
        remain -= int(pix_showline / pix_linesec + 0.5)
        cur_line += int(pix_showline / pix_linesec + 0.5)
   
    #img.rotate(90).show()
    #exit(0)
    return img
   
def get_hue_color(size):
    ''' size で渡された数の純色を返す '''
    # blue -> red
    _buf = ['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f']
    _rad = abs(colorsys.rgb_to_hls(0, 0, 1.0)[0] - colorsys.rgb_to_hls(1.0, 0, 0)[0]) / size
    _max = max(colorsys.rgb_to_hls(0, 0, 1.0)[0], colorsys.rgb_to_hls(1.0, 0, 0)[0])
    for i in range(0, size):
        hls = colorsys.hls_to_rgb(max(0, _max - _rad * i), 0.5, 1.0)
        cols[i] = '#' + _buf[int(15 * hls[0] + 0.3)] + _buf[int(15 * hls[1] + 0.3)] + _buf[int(15 * hls[2] + 0.3)]
    return cols

1-2へ続く