0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonでの音声信号処理 (3) 読み込んだ情報を可視化する その2

Last updated at Posted at 2023-01-08

やりたいこと

ファイルから読み込んだ音声データを見やすい形で表示すること。

やってみる

グラフ描画部分を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()

実行結果
P03.png

見える部分のみを描画することで、処理負荷軽減を図っています。
なお、tkinterのスクロールイベントで、少し手を抜いています(矢印ボタン押下でスクロールさせると、グラフが一瞬、途切れます)。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?