著者:石塚 美伶
今回は、音楽の「耳コピ」を支援する音源可視化ツールをPythonで制作する方法について紹介します。制作するツールは、WAVファイルを読み取って音を判別し、その音をピアノの鍵盤で示すGIFアニメーションを生成します。
シェルスクリプトマガジン Vol.69は以下のリンク先でご購入できます。![]()
![]()
図4 「main.py」ファイルに記述する内容(その1)
import scipy.io.wavfile # WAVファイルを読み込むために使用
from scipy import signal # 極大値を求めるために使用
import numpy as np # データの整形や高速フーリエ変換に使用
import pandas as pd # ピアノの音階辞書を作成するために使用
import matplotlib.pyplot as plt # 図の作成に使用
import matplotlib.animation as anm # アニメーション作成に使用
import matplotlib.patches as pat # 長方形を描画するために使用
import time # 時間を表示するために使用
図5 「main.py」ファイルに記述する内容(その2)
wav_filename = "./source.wav"
# 音声ファイルの読み込み
rate, data = scipy.io.wavfile.read(wav_filename)
print("サンプリング周波数:", rate)
print("データ数:", len(data))
if data.ndim < 2: # 配列の次元数
print("モノラル")
else:
print("ステレオ")
data = np.ravel(data)[::2] # 連結して偶数要素を抽出する
#(振幅)の配列を作成 (「-1」~「1」の範囲に正規化)
data = data / 32768
# データを0.1秒ごとに分割
split_datas = np.array_split(data, int(len(data) / (rate * 0.1)))
図6 「main.py」ファイルに記述する内容(その3)
count = 0
ex_freqency = [] # 抽出したデータを格納するために用意
for short_data in split_datas:
ex_freqency.append([]) # データを格納するために空リストを追加
# フーリエ変換により周波数成分と振幅を取得
fft_short_data = np.abs(np.fft.fft(short_data))
freqList = np.fft.fftfreq(short_data.shape[0], d=1.0/rate)
maxid = signal.argrelmax(fft_short_data, order=2) # 極大値を求める
for i in maxid[0]:
if fft_short_data[i] > 10 and 25 < freqList[i] < 4200:
ex_freqency[count].append(freqList[i]) # 周波数を格納
count += 1
図8 「main.py」ファイルに記述する内容(その4)
piano_dic = pd.read_csv("./piano_dict.csv", encoding="utf-8")
print(piano_dic)
black_keys = piano_dic[piano_dic["scaleNameEn"].str.contains("#")].index
print(black_keys)
count = 0
keys = [] # 含まれる周波数の行
for row in ex_freqency:
keys.append([]) # 各フレームの周波数を格納するために空リストを追加
for i in row:
# 差が最小の音階
key = piano_dic.loc[abs(piano_dic.frequency - i).idxmin(), "keyNumber"]
if (key in keys[count]) == False:
keys[count].append(key) # 重複してなければ音階を追加
count += 1
print(keys)
図9 「main.py」ファイルに記述する内容(その5)
fig, ax = plt.subplots(figsize = (10, 2))
# 各フレームの描画
def update(i, fig_title, data_list, ax):
if i != 0:
plt.cla() # 現在描写されているグラフを消去
ax.axis("off") # 軸と目盛りを削除
ax.set_xlim(0, 52.1)
ax.set_ylim(-0.5, 2.5)
skip = False
white_count = 0
plt.title(fig_title + time.strftime("%M:%S", time.gmtime(i * 0.1)))
for j in range(0, 88):
if skip == True:
skip = False # フラグを降ろす
continue # 既に描画した白鍵をスキップする
if j in black_keys:
# 黒鍵の右側の白鍵を描画
color = "white"
if j + 1 in data_list[i]:
color = "red" # 音が鳴っていれば色を赤にする
# 長方形を作成
rec = pat.Rectangle(xy = (white_count, 0), \
width = 1, height = 1.5, \
fc = color, ec = "black")
ax.add_patch(rec) # Axesに長方形を追加
skip = True # スキップフラグを立てる
# 白鍵の上に黒鍵を描画
color = "gray"
x, y = white_count - 0.3, 0.5
w, h = 0.6, 1
else:
# 白鍵を描画
color = "white"
x, y = white_count, 0
w, h = 1, 1.5
if j in data_list[i]:
color = "red" # 音が鳴っていれば色を赤にする
# 長方形を作成
rec = pat.Rectangle(xy = (x, y), width = w, \
height = h, fc = color, ec = "black")
ax.add_patch(rec) # Axesに長方形を追加
white_count += 1 # 白鍵の数をカウント
# アニメーションを生成
ani = anm.FuncAnimation(fig, update, fargs=("Mimicopy ", keys, ax), \
interval=100, frames=len(keys))
# GIFファイルとして保存
ani.save("Sample.gif", writer="pillow")