やりたいこと
ファイルから読み込んだ音声データを見やすい形で表示すること。
やってみる
グラフ描画部分をtkinterで塩梅よく造ってみる。
Grp.py
# -*- coding: utf-8 -*-
import tkinter as tk
import numpy as np
import math
class Line:
def __init__(self):
self.width = 640 # 横サイズ
self.height = 200 # 縦サイズ
self.vals = np.array([]) # データ列
self.rate = 256 # サンプリングレート
self.amp = 1 # 表示時の幅
self.orderScale = 1 # 表示縮尺指定値
self.scale = 1 # 表示縮尺
self.cnv = None # キャンバス
self.items = [] # 表示部品
self.dispSt = 0 # 表示開始INDEX
self.dispEd = 0 # 表示終了INDEX
def setWidth(self, width):
self.width = width
def setHeight(self, height):
self.height = height
def setScale(self, scale):
self.orderScale = scale
self.scale = self.orderScale * self.width / (self.rate * 3)
# データ設定処理
def setData(self, vals, rate):
self.vals = vals
self.amp = math.fabs(max(
(np.floor(vals * 100000) / 100000).min(),
(np.floor(vals * 100000) / 100000).max(), key=abs)) + 1
self.rate = rate
self.scale = self.orderScale * self.width / (self.rate * 3)
# スクロール処理
def __eventScroll(self, event):
# スクロールバーの先頭と末尾位置を取得
v = self.bar.get()
self.dispSt = v[0]
self.dispEd = v[1]
# 画面を更新
self.__updateDisplay()
# [x]押下時処理
def __eventFin(self):
if self.rt is None:
return
self.rt.destroy()
self.rt = None
# 画面更新処理
def __updateDisplay(self):
if self.cnv is None:
return
# 描画済みのパーツを削除
for item in self.items:
self.cnv.delete(item)
self.items = []
# データ未設定時は何もしない
if len(self.vals) == 0:
return
# スクロール位置に対応した描画先頭位置、末尾位置を取得
dispStIdx = int(len(self.vals) * self.dispSt)
dispEdIdx = int(len(self.vals) * self.dispEd)
if dispStIdx >= dispEdIdx:
return
# 値の列を作成
vals = self.vals[dispStIdx:dispEdIdx] # Y軸方向
lbls = np.arange(len(vals)) + dispStIdx # X軸方向
# 描画縮尺を加味したピクセル値の列を作成し、X,Y,X,Y,...の順に並べ替える
conv_x = lbls * self.scale
conv_y = vals / self.amp * ((self.height - 20)/2) * (-1) + ((self.height - 20)/2)
data_s = list(np.array([conv_x, conv_y]).T.reshape(1, len(conv_x) + len(conv_y))[0])
line = self.cnv.create_line(data_s, fill="Blue", width=1)
self.items.append(line)
# X軸を生成
xaxe_x = [conv_x[0], conv_x[len(conv_x) - 1]]
xaxe_y = [(self.height - 20)/2, (self.height - 20)/2]
xaxe_s = list(np.array([xaxe_x, xaxe_y]).T.reshape(1, len(xaxe_x) + len(xaxe_y))[0])
yoko = self.cnv.create_line(xaxe_s, fill="Black", width=1)
self.items.append(yoko)
# 目盛りを生成
st = (dispStIdx // self.rate) * self.rate
ed = (dispEdIdx // self.rate + 1) * self.rate
for i in range(st, ed, self.rate // 10):
if i < dispStIdx or i > dispEdIdx:
continue
l = 5
xmem_x = list(np.array([i, i]) * self.scale)
xmem_y = [(self.height - 20)/2 - l, (self.height - 20)/2 + l]
xmem_s = list(np.array([xmem_x, xmem_y]).T.reshape(1, len(xmem_x) + len(xmem_y))[0])
xmem = self.cnv.create_line(xmem_s, fill="Black", width=1)
self.items.append(xmem)
# 表示
def display(self):
data_width = len(self.vals) * self.scale
if data_width < self.width:
data_width = self.width
rt = tk.Tk()
rt.geometry(str(self.width) + "x" + str(self.height))
cnv = tk.Canvas(rt, bg="white", width=self.width, height=self.height - 20)
cnv.place(x=0, y=0)
bar = tk.Scrollbar(rt, orient=tk.HORIZONTAL)
bar.pack(side=tk.BOTTOM, fill=tk.X)
bar.config(command=cnv.xview)
cnv.config(scrollregion=(0, 0, data_width, self.height))
cnv.config(xscrollcommand=bar.set)
self.dispSt = 0
self.dispEd = self.width / data_width
bar.bind("<B1-Motion>", self.__eventScroll)
bar.bind("<Button-1>", self.__eventScroll)
rt.protocol("WM_DELETE_WINDOW", self.__eventFin)
self.rt = rt
self.cnv = cnv
self.bar = bar
self.__updateDisplay()
while True:
if self.rt is None:
break
self.rt.update_idletasks()
self.rt.update()
class Bar:
def __init__(self):
# 画面サイズ
self.width = 640
self.height = 200
self.wova = 50 # 縦軸の幅(Width of vertical axis)
self.hoha = 70 # 横軸の高さ(Height of horizontal axis)
self.woov = 30 # Width of One Value
# データ、ラベル
self.vals = np.array([]) # データ列
self.lbls = []
self.vavl = []
self.maxv = 100
self.minv = -100
# graphical parts
self.rt = None
self.cnv_vw = None
self.cnv_lb = None
self.cnv_va = None
self.vw_items = []
self.lb_items = []
self.va_items = []
# Width of Viewarea
def __wovw(self):
return self.width - self.wova
# Height of Viewarea
def __hovw(self):
return self.height - self.hoha
# Height of one value
def __hoov(self):
return self.__hovw() / (self.maxv - self.minv)
def setMaxViewarea(self, v):
self.maxv = v
self.vavl = self.__make_vaxis(self.maxv, self.minv, 6)
def setMinViewarea(self, v):
self.minv = v
self.vavl = self.__make_vaxis(self.maxv, self.minv, 6)
def setMaxMinViewarea(self, maxv, minv):
self.setMaxViewarea(maxv)
self.setMinViewarea(minv)
def setMaxMinValueByDefault(self):
maxv = np.amax(self.vals)
minv = np.amin(self.vals)
rngv = (maxv - minv) / 10 if (maxv - minv) > 100 else 10
self.setMaxMinViewarea(maxv + rngv, minv - rngv)
# set data
def setData(self, vals):
self.setDataLabel(vals, list(range(1, len(vals)+1)))
return
# set data and label
def setDataLabel(self, vals, lbls):
self.vals = vals
if len(lbls) >= len(vals):
self.lbls = lbls[:len(vals)]
else:
last_value = lbls[-1]
self.lbls = lbls + list(range(last_value + 1, last_value + (len(vals) - len(lbls)) + 1))
self.setMaxMinValueByDefault()
self.setMinViewarea(-10)
return
def __eventFin(self):
if self.rt is None:
return
self.rt.destroy()
self.rt = None
def __eventScroll(self, event):
# スクロールバーの先頭と末尾位置を取得
v = self.bar.get()
self.dispSt = v[0]
self.dispEd = v[1]
# 画面を更新
self.__updateDisplay()
return
def __eventWheel(self, event):
#self.woov
if event.delta > 0:
# up
self.woov += 2
else:
# down
self.woov -= 2
if self.woov < 5:
self.woov = 6
if self.woov > 100:
self.woov = 100
self.__updateDisplay()
return
def __coor_x(self, x):
return x * self.woov + self.woov / 2
def __coor_y(self, y):
return self.__hovw() - (y - self.minv) * self.__hoov()
# 10の何乗オーダーの値か
# 91 -> 1, 1 -> 0, 0.15 -> -1, 0.05 -> -2
def __flen(self, v):
return math.floor(math.log10(v))
# 桁指定切り上げ
# 小数点以下 keta 桁数を保証して、その下の位で切り上げ
def __roundup(self, v, keta):
return math.ceil(v * math.pow(10, keta)) * math.pow(10, -1 * keta)
# lim以下最大数で割り切った場合の商
def __divmax(self, v, lim):
r = lim
while r >= 1:
q = math.floor(v / r)
if q * r == v:
return q
r -= 1
return 1
def __make_vaxis(self, maxv, minv, num):
rng = maxv - minv
ord = self.__flen(rng)
ordnum = (1 - ord) if (ord < 1) else 0
rng10 = rng * math.pow(10, ordnum)
rngup = self.__roundup(rng10, -1 * self.__flen(rng10))
r = self.__divmax(rngup, num)
res = []
st = int(math.floor((minv * math.pow(10, ordnum)) / r ) * r)
ed = int(maxv * math.pow(10, ordnum))
for idx in range(st, ed + 1, r):
if idx < minv * math.pow(10, ordnum):
continue
res.append(idx)
res = list(map(lambda e: e / math.pow(10, ordnum), res))
return res
def __updateVerticalAxis(self):
if self.rt is None or self.cnv_va is None:
return
for item in self.va_items:
self.cnv_va.delete(item)
self.va_items.remove(item)
if len(self.vavl) == 0:
return
for v in self.vavl:
px = self.wova - 5
py = self.__coor_y(v)
item = self.cnv_va.create_text(px, py, text='{:1}'.format(v), anchor="e", fill="black")
self.va_items.append(item)
def __updateDisplay(self):
if self.rt is None or self.cnv_vw is None or self.cnv_lb is None:
return
for item in self.vw_items:
self.cnv_vw.delete(item)
self.vw_items = []
for item in self.lb_items:
self.cnv_lb.delete(item)
self.lb_items = []
if len(self.vals) == 0:
return
data_width = len(self.vals) * self.woov
if data_width < self.__wovw():
data_width = self.__wovw()
self.cnv_vw.config(width=self.__wovw())
self.cnv_lb.config(width=self.__wovw())
self.cnv_vw.config(scrollregion=(0, 0, data_width, self.height))
self.cnv_lb.config(scrollregion=(0, 0, data_width, self.hoha))
dispStIdx = int(len(self.vals) * self.dispSt)
dispEdIdx = int(len(self.vals) * self.dispEd)
if dispStIdx >= dispEdIdx:
return
y_val = self.vals[dispStIdx:dispEdIdx]
l_val = self.lbls[dispStIdx:dispEdIdx]
x_val = np.arange(len(y_val)) + dispStIdx
for x, y, lb in zip(x_val, y_val, l_val):
px = self.__coor_x(x)
py_top = self.__coor_y(y)
py_btm = self.__coor_y(0)
item = self.cnv_lb.create_text(px, 5, text=str(lb), anchor="n", fill="black")
self.lb_items.append(item)
if py_top == py_btm:
item = self.cnv_vw.create_line(
px - (self.woov / 2 - 1), py_top,
px + (self.woov / 2 - 1), py_btm, fill='black')
else:
color = 'blue'
if py_top > py_btm:
color = 'red'
item = self.cnv_vw.create_rectangle(
px - (self.woov / 2 - 1), py_top,
px + (self.woov / 2 - 1), py_btm, fill=color)
self.vw_items.append(item)
return
def __sc(self, a, b, c=None):
if self.cnv_vw is None or self.cnv_lb is None:
return
if c is None:
self.cnv_vw.xview(a, b)
self.cnv_lb.xview(a, b)
else:
self.cnv_vw.xview(a, b, c)
self.cnv_lb.xview(a, b, c)
return
def display(self):
data_width = len(self.vals) * self.woov
if data_width < self.__wovw():
data_width = self.__wovw()
rt = tk.Tk()
rt.geometry(str(self.width) + "x" + str(self.height))
# view area
cnv_vw = tk.Canvas(rt, bg="white", width=self.__wovw(), height=self.__hovw())
cnv_vw.place(x=self.wova, y=0)
# label area
cnv_lb = tk.Canvas(rt, bg="white", width=self.__wovw(), height=self.hoha)
cnv_lb.place(x=self.wova, y=self.__hovw())
cnv_va = tk.Canvas(rt, bg="white", width=self.wova, height=self.__hovw())
cnv_va.place(x=0, y=0)
bar = tk.Scrollbar(rt, orient=tk.HORIZONTAL)
bar.pack(side=tk.BOTTOM, fill=tk.X)
bar.config(command=self.__sc)
cnv_vw.config(scrollregion=(0, 0, data_width, self.height))
cnv_lb.config(scrollregion=(0, 0, data_width, self.hoha))
cnv_vw.config(xscrollcommand=bar.set)
# set scrollbar value
self.dispSt = 0
self.dispEd = self.width / data_width
bar.bind("<B1-Motion>", self.__eventScroll)
bar.bind("<Button-1>", self.__eventScroll)
cnv_vw.bind("<MouseWheel>", self.__eventWheel)
cnv_lb.bind("<MouseWheel>", self.__eventWheel)
rt.protocol("WM_DELETE_WINDOW", self.__eventFin)
self.rt = rt
self.cnv_vw = cnv_vw
self.cnv_lb = cnv_lb
self.cnv_va = cnv_va
self.bar = bar
self.__updateDisplay()
self.__updateVerticalAxis()
while True:
if self.rt is None:
break
self.rt.update_idletasks()
self.rt.update()
p3.py
# -*- coding: utf-8 -*-
import sys
sys.dont_write_bytecode = True
import warnings
warnings.filterwarnings( 'ignore' )
import numpy as np
from pydub import AudioSegment
from Grp import Line
def main():
dt_raw = AudioSegment.from_mp3("data.mp3")
dt_arr = np.array(dt_raw.get_array_of_samples())
dt_ch1 = dt_arr[::2]
grp = Line()
grp.setData(dt_ch1, dt_raw.frame_rate)
grp.display()
if __name__ == '__main__':
main()
見える部分のみを描画することで、処理負荷軽減を図っています。
なお、tkinterのスクロールイベントで、少し手を抜いています(矢印ボタン押下でスクロールさせると、グラフが一瞬、途切れます)。