メモ

メモ、雑記、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()






最新の画像もっと見る

コメントを投稿