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()