いろいろ

エンジニアっぽい事と音楽っぽい事を主に書く予定です。

MIDIキーボードとProcessing

概要

MIDIキーボードの入力に合わせて図形を描画するおもちゃ?を作ってみました。

youtu.be

※ 2020.7.4 ソースコード微修正。

※結構なCPUリソースを使う贅沢なシステムになってしまった。ProcessingとDAWを同時に立ち上げているので当然といえば当然か。MacBookのFANが唸りを上げているし、筐体も熱々。もし万が一試される方がいらっしゃったら自己責任でお願いします。 改善の余地はいろいろありそうですが、今回はいったんこれで。。。

※最初はWindowsのデスクトップPCでやろうと思ったのですが、ProcessingからDAWへのMIDI信号の送り方がわからず早々に断念しました。

環境

動機

「0歳の娘と自治体の乳幼児施設に行ったらピアノ絵本で楽しそうに遊んで(叩いて?)いた」という話を妻から聞く

やっぱり視覚と聴覚と触覚と複数の感覚に訴えるものが良いみたいだな

そういえば以前Generative Art / Processingの本読んだことがあったけど、なんか作れないだろうか

www.bnn.co.jp

そういえば「ごぶごぶ ごぼごぼ」という、丸い形がたくさん出てくる絵本にとても興味を示していたなあ

www.fukuinkan.co.jp

!!!


作り方

1.Processingをインストールする。

公式サイトからダウンロードしてインストールする。

https://processing.org/


2. MidiBusを導入する。

processingを起動する。

"ツール" > "ツールを追加" > "Libraries"

"MidiBus"で検索 > Install


3.コーディング

本書中の図版のほとんどは、とりあげているトピックと関連した私のジェネラティブな作品 の図ですが、それ以外のものは、息抜きとしてランダムに添えられています。これらのイメー ジのソースコードの多くは、http://abandonedart.orgと出版社のWebサイトhttp://www.manning.com/GenerativeArt からダウンロードできます。これらもまたクリエイティブ・コモ ンズ・ライセンスのもとに公開されているので、あなたがこれら作品を利用し、分析し、変更し、 ごちゃまぜにしたり、壊したりすることも大歓迎です。

-[普及版]ジェネラティブ・アートProcessingによる実践ガイド:「本書について」より

http://abandonedart.orgはリンク切れのようです。(2020.6現在)


再利用可との事なので6章の例題のコードを改変して作りました。ありがたや~


MidiBusの部分についてはこちらを参考にさせていただきました。

ProcessingでMIDIコントローラーを使う | Music Theory Workshop Japan


という事で書いたのがこちら↓

import themidibus.*;

MidiBus myBus;

int _num =3;
ArrayList<Circle> _circleArr = new ArrayList<Circle>();

void setup(){
  size(1440, 800);
  frameRate(30);
  background(255);
  smooth();
  MidiBus.list();
  myBus = new MidiBus(this,"Keystation Mini 32","PROCESSING_TO_DAW");
}

void draw(){
  background(255);
  int circArrSize = _circleArr.size();
  for(int i = 0; i< circArrSize; i++){
    Circle thisCirc = _circleArr.get(i);
    if(!thisCirc.updateMe()){
      _circleArr.remove(i);
      i--;
      circArrSize--;
    }
  }
}

void drawCircles(int pitch, int velocity){
  for(int i=0;i< _num; i++){
    Circle thisCirc = new Circle(pitch, velocity);
    thisCirc.drawMe();
    _circleArr.add(thisCirc);
    //println(_circleArr.size());
  }
}

void noteOn(int channel, int pitch, int velocity) {
  // Receive a noteOn
  //println();
  //println("Note On:");
  //println("--------");
  //println("Channel:"+channel);
  //println("Pitch:"+pitch);
  //println("Velocity:"+velocity);

  int pitchOneOct = pitch % 12;

  drawCircles(pitchOneOct, velocity);
}

//=============================== objects

class Circle {
  float x, y;
  float radius;
  color linecol, fillcol;
  float alph;
  float xmove, ymove;
  int r,g,b;

  Circle(int pitch, int velocity){
    x = (width-50)/11 * pitch + 25;
    y = height;
    radius = random(100) + 10;
    r = pitch * 255 / 12;
    g = (12 - pitch) * 255 / 12;
    b = ((pitch * 255)/12 + 128) % 255;
    fillcol = color(r,g,b);
    alph = (velocity-1)*2;
    xmove = random(2) - 1;
    ymove = -random(10);
  }

  void drawMe(){
    noStroke();
    fill(fillcol,alph);
    ellipse(x, y, radius*2, radius*2);
  }

  boolean updateMe(){
    x += xmove;
    y += ymove;
    if(x > (width+radius)){x=0-radius;}
    if(x < (0-radius)){x = width + radius;}
    if(y < (0-radius)){return false;}
    if(y > (height+radius)){return false;}

    drawMe();
    return true;
  }
}
setup

MidiBusのインスタンス生成時にMIDIINとOUTを指定します。 直前の関数のMidiBus.list();でコンソールに使用可能なmidiバイスが表示されるので、 一度実行して表示された中から選択するようにします。

私の環境の場合は以下のように設定しました。 MIDI IN: "Keystation Mini 32" →使用するmidiキーボード MIDI OUT: "PROCESSING_TO_DAW" →macmidi設定が必要なので事項で説明します。

noteOn

MIDIキーボードからNoteOnでnoteOn関数が実行されます。 pitch(音程)とvelocity(強さ)を drawCircles関数に渡して描画してもらいます。 pitchは12で割って、どのオクターブでも共通にします。

drawCircles

1回のnoteOnに対して_numの数だけ丸を生成します。 pitchによって丸の発生場所と色が変わり、velocityによって丸の透過度が変化するようにしました。

この辺はいろいろ自由にできそうですね。


4. ソフトシンセの準備

今回Processingからは音は出しません。それなりに良い音を出したかったので、手っ取り早くDAWのソフトウェアシンセを使うことにしました。

まず、processingからのMIDI OUT をDAWに入力するようにします。 こちらが参考になりました。 Sending MIDI information to my DAW - Electronics (Arduino, etc.) - Processing Foundation

  1. Ran MidiBus.list() and confirmed that the standard default output devices [0] Gervill and [1] Real Time Sequencer were showing up in the console
  2. Went to audio midi setup on my mac -> Window -> show MIDI studio
  3. Clicked in to the IAC driver -> ticked box for “Device is online”
  4. Renamed the port to Processing to DAW
  5. Ran MidiBus.list() again in Processing, and saw the new option Processing to DAW in the console

今回はmacMIDI studio で"PROCESSING_TO_DAW"という名前に設定しています。

次にDAWを開いて好きなソフトウェアシンセを立ち上げます。GarageBandMIDI入力は自動で認識してくれるみたいですね。


5. 実行

Processingに戻ってコードを実行します。キーボードを弾くと冒頭の動画のような動作をします。


感想

仕事だとハードウェアかOSより下のレイヤを扱っているので、自分で書いたコードで絵が動くと楽しい。

娘も何だかよくはわからないだろうけど、画面見ながらバシバシキーボード叩いてくれているので良しとする。

CPU負荷軽くしたい…

余力があれば色んなバリエーション作ってみようかな...


参考

[普及版]ジェネラティブ・アート | 株式会社ビー・エヌ・エヌ新社

ProcessingでMIDIコントローラーを使う | Music Theory Workshop Japan

Sending MIDI information to my DAW - Electronics (Arduino, etc.) - Processing Foundation

Python/LibROSA でギターのサウンド解析(2/2)

前回からの続きです。

  1. ギターの音を録音 (前回)

  2. サウンドスペクトログラムを表示 (前回)

  3. 周波数スペクトルをアニメーション表示

nasb.hatenablog.com

3 Matplotlibで周波数スペクトルをアニメーション表示

グラフ描画ライブラリのMatplotlibを使って周波数スペクトルをアニメーション表示してみました。 前回librosa.stftした結果を使います。


sftfまでのソースコード

import numpy as np
import matplotlib.pyplot as plt
import librosa
import librosa.display
from matplotlib.animation import PillowWriter
from matplotlib.animation import FuncAnimation
import glob
%matplotlib inline

files = glob.glob('./*.wav')
"""
'.\\1_tele_front.wav',
'.\\2_tele_center.wav',
'.\\3_tele_rear.wav',
'.\\4_semiag_front.wav',
'.\\5_semiag_center.wav',
'.\\6_semiag_rear.wav'
"""

dbs = []
n_fft = 1024*4     #フレーム長 (サンプル数)
win_length = n_fft     #窓関数長 (サンプル数)
hop_length = win_length // 4     #ホップ長 (サンプル数)
sr = 44100     #サンプリングレート [Hz]
duration=20     #期間 [sec]

for file in files:
    y, sr = librosa.load(file, sr=sr, duration=duration)
    D = np.abs(librosa.stft(y, n_fft=n_fft, win_length=win_length, hop_length=hop_length))
    db = librosa.amplitude_to_db(D, ref=np.max)
    dbs.append(db)


周波数スペクトルを1フレームずつ描画してパラパラ漫画にしています。

plt.rcParams["animation.embed_limit"] = 70.0  #出力するアニメーションのサイズ制限を変更[MB]
fig, ax = plt.subplots(3, 2, figsize=(12,12))
x = np.linspace(0,sr//2, n_fft//2+1)
x[0] = 1e-7 #logスケールで表示するために微調整

def init_plt():
    for i in range(6):
        if i < 3:
            m, n = i, 0
        else:
            m, n = i-3, 1
            
        ax[m,n].set_xscale("log")
        ax[m,n].set_ylim(-90,0)
        ax[m,n].set_xlim(10,1e4*5)
        ax[m,n].set_xlabel("Hz")
        ax[m,n].set_ylabel("dB")
        ax[m,n].grid(b=True)
        ax[m,n].set_title(files[i])


def plot(frame):
    time = 20 / total_frames * frame
    plt.subplots_adjust(wspace=0.2, hspace=0.3)
    plt.suptitle("Power Spectrum  time:{:.2f}sec".format(time))
    for i in range(6):
        y = dbs[i][:,frame]
        if i < 3:
            m, n = i, 0
        else:
            m, n = i-3, 1

        ax[m,n].lines = []
        ax[m,n].plot(x, y, linewidth=1, color='navy')

        
total_frames = dbs[0].shape[1]
f_interval = 2  
t_interval = f_interval * 20 * 1000 // total_frames   #実時間と大体合うように調整

anim = FuncAnimation(fig, plot, init_func=init_plt, frames=range(0,total_frames,f_interval), interval=t_interval)
#anim.save('./ps_animation.gif', writer="pillow")   #GIFにする場合はこっち
anim.save('ps_animation.mp4', writer='ffmpeg')

#jupyter notebook上で表示させたい場合は以下を記述
from IPython.display import HTML
HTML(anim.to_jshtml())


描画の仕方を変えてみます。

ギターの比較

ソースコード

fig, ax = plt.subplots(3, 1, figsize=(6,12))
x = np.linspace(0,sr//2, n_fft//2+1)
x[0] = 1e-7

titles = ["front","center","rear"]

def init_plt():
    for m in range(3):
        ax[m].set_xscale("log")
        ax[m].set_ylim(-90,0)
        ax[m].set_xlim(10,1e4*5)
        ax[m].set_xlabel("Hz")
        ax[m].set_ylabel("dB")
        ax[m].grid(b=True)
        ax[m].set_title(titles[m])


def plot(frame):
    time = 20 / total_frames * frame
    plt.subplots_adjust(wspace=0.2, hspace=0.3)
    plt.suptitle("Power Spectrum  time:{:.2f}sec".format(time))
    for i in range(3):
        y = dbs[i][:,frame]
        y2 = dbs[i+3][:,frame]

        ax[i].lines = []

        ax[i].plot(x, y, linewidth=1, color='navy', label="tele")
        ax[i].plot(x, y2, linewidth=1, color='r', label="semi")
        ax[i].legend()

   
total_frames = dbs[0].shape[1]
f_interval = 2
t_interval = f_interval * 20*1000 // total_frames


anim = FuncAnimation(fig, plot, init_func=init_plt, frames=range(0,total_frames,f_interval), interval=t_interval)
#anim.save('./ps_animation2.gif', writer="pillow")
anim.save('ps_animation2.mp4', writer='ffmpeg')

from IPython.display import HTML
HTML(anim.to_jshtml())

弦を弾いた直後、2k辺り以上の成分に大きな差があります。シングルコイルの方が一般的に巻き数が少ない分インダクタンスが少ないので、高周波の減衰が少ないんですね。この辺がシングルコイルの「カリッ」「パキッ」「シャリッ」感とハムバッカーの「暖かみのある」音の差になってるんでしょうかね。


ピックアップの比較

ソースコード

fig, ax = plt.subplots(1, 2, figsize=(12,4))
x = np.linspace(0,sr//2, n_fft//2+1)
x[0] = 1e-7

titles = ["tele","semi"]

def init_plt():
    for m in range(2):
        ax[m].set_xscale("log")
        ax[m].set_ylim(-90,0)
        ax[m].set_xlim(10,1e4*5)
        ax[m].set_xlabel("Hz")
        ax[m].set_ylabel("dB")
        ax[m].grid(b=True)
        ax[m].set_title(titles[m])


def plot(frame):
    time = 20 / total_frames * frame
    plt.subplots_adjust(wspace=0.2, hspace=0.3)
    plt.suptitle("Power Spectrum  time:{:.2f}sec".format(time))
    for i in range(2):
        y = dbs[i*3][:,frame]
        y2 = dbs[i*3+1][:,frame]
        y3 = dbs[i*3+2][:,frame]

        ax[i].lines = []

        ax[i].plot(x, y, linewidth=1, color='navy', label="front")
        ax[i].plot(x, y2, linewidth=1, color='r', label="center")
        ax[i].plot(x, y3, linewidth=1, color='g', label="rear")
        ax[i].legend()

        
total_frames = dbs[0].shape[1]
f_interval = 2
t_interval = f_interval * 20*1000 // total_frames

anim = FuncAnimation(fig, plot, init_func=init_plt, frames=range(0,total_frames,f_interval), interval=t_interval)
#anim.save('./ps_animation3.gif', writer="pillow")
anim.save('ps_animation3.mp4', writer='ffmpeg')

from IPython.display import HTML
HTML(anim.to_jshtml())

フロント>センター>リアの順で基音の音量に差があります。フロントの方が弦の真ん中に近いので基音を拾いやすいようです。 さらにリアは弾いた直後は基音よりも大きな倍音成分がありますね。


参考にさせていただいたページ

Matplotlib: Python plotting — Matplotlib 3.1.0 documentation

Librosa

早く知っておきたかったmatplotlibの基礎知識、あるいは見た目の調整が捗るArtistの話 - Qiita

matplotlibでアニメーション - Qiita

matplotlibでimagemagickを使わずにアニメGIFを保存する - Qiita

Python/LibROSA でギターのサウンド解析(1/2)

やったこと

  1. ギターの音を録音

  2. サウンドスペクトログラムを表示

  3. 周波数スペクトルをアニメーション表示(次回)

nasb.hatenablog.com

1 ギターの音を録音

全部で6種類

1: テレキャスター/フロントピックアップ(シングルコイル)

2: テレキャスター/センターポジション(シングルコイル)

3: テレキャスター/リアピックアップ(シングルコイル)

4: セミアコースティックギター/フロントピックアップ(ハムバッカー)

5: セミアコースティックギター/センターポジション(ハムバッカー)

6: セミアコースティックギター/リアピックアップ(ハムバッカー)

※両ギターともセンターポジションの場合は、フロントとリアのハーフトーンになります。

条件
  • 5弦開放弦 A (ラ) 110Hz 単音
  • Audio I/F直の音。アンプシミュレータやエフェクターは未使用。
  • ギターの TONE / VOLUME つまみは全て10
  • サンプリングレート:44.1kHz
  • モノラル
  • 録音時間:20秒
  • 録音後にノーマライズ処理
  • wav形式で保存



2 サウンドスペクトログラムを表示

Pythonの音響解析向けのパッケージlibrosaを使って、録音した音声を解析してスペクトログラムを表示させます。

環境
  • python 3.6.8
  • numpy 1.16.3
  • matplotlib 3.0.3
  • librosa 0.6.3


全体の流れ

f:id:Nasb:20190531184852j:plain


各種ライブラリのインポート
import numpy as np
import matplotlib.pyplot as plt
import librosa
import librosa.display
from matplotlib.animation import PillowWriter
from matplotlib.animation import FuncAnimation
import glob
%matplotlib inline


録音したファイルを読み込んでstft
files = glob.glob('./*.wav')
"""
'.\\1_tele_front.wav',
'.\\2_tele_center.wav',
'.\\3_tele_rear.wav',
'.\\4_semiag_front.wav',
'.\\5_semiag_center.wav',
'.\\6_semiag_rear.wav'
"""

dbs = []
n_fft = 1024*4     #フレーム長 (サンプル数)
win_length = n_fft     #窓関数長 (サンプル数)
hop_length = win_length // 4     #ホップ長 (サンプル数)
sr = 44100     #サンプリングレート [Hz]
duration=20     #期間 [sec]

for file in files:
    y, sr = librosa.load(file, sr=sr, duration=duration)
    D = np.abs(librosa.stft(y, n_fft=n_fft, win_length=win_length, hop_length=hop_length))
    db = librosa.amplitude_to_db(D, ref=np.max)
    dbs.append(db)
librosa.load

wavファイルを読み込んでオーディオデータのnumpy配列:y とサンプリングレート:sr を返します。


librosa.stft

短時間フーリエ変換(Short-Time Fourier Transform: STFT)の結果を返してくれます。結果は要素が複素数のnumpy.ndarrayです。振幅を取り出すために絶対値をとります。

周波数分解能は0 ~ sr/2 の範囲をn_fft/2+1 段階。最大周波数がサンプリングレートの半分なのはサンプリング定理の為です。

フレーム長と窓関数長は同じです。

窓関数はデフォルトでハニング(ハン)窓になっているようです。

ホップ長 は(win_length / 2)サンプルなので 隣のフレームと1/2ずつオーバーラップしていることになります。

STFTについては↓こちらが大変詳しく解説してくださっています。とても参考になりました。

Pythonでお手軽音声処理(5) フーリエ変換・スペクトログラム表示 | クリエイティヴなヴログ


librosa.amplitude_to_db

名前の通り振幅からdBに変換します。


6つのスペクトログラムを描画します

fig = plt.figure(figsize=(15, 15))
plt.subplots_adjust(wspace=0.2, hspace=0.3)
plt.suptitle("Power Spectrogram")
ax = []
pos = [1,3,5,2,4,6]

for i in range(6):
    ax.append(fig.add_subplot(3, 2, pos[i]))
    librosa.display.specshow(dbs[i], y_axis='log', x_axis='time', sr=sr, hop_length=hop_length)
    plt.title(files[i])
    cb = plt.colorbar()
    cb.set_label("dB")
    
plt.savefig("Power_Spectrogram.jpg")


librosa.display.specshow

stft結果を渡すとスペクトログラムを表示してくれます。

f:id:Nasb:20190529001631j:plain ギターとピックアップによって倍音成分の出方が違いますね

特に4kHz以上の成分がテレキャスには見えますが、セミアコにはほとんど見えません

また、フロントの方が基本波が比較的強くて、リアは弱そうです

とはいったものの、素人の私には違いが思ったほどわからないのでこの後周波数スペクトルをアニメーション表示して比較してみました。 記事が長くなってしまったので分けます。

次回に続く…

参考にさせていただいたページ

Matplotlib: Python plotting — Matplotlib 3.1.0 documentation

Librosa

Pythonでお手軽音声処理(5) フーリエ変換・スペクトログラム表示 | クリエイティヴなヴログ

信号処理と音楽 窓長とスペクトルの関係 (時間的に周期性の変化をもつサイン波)

活性化関数の導関数の導出

ここでは3つの関数(sigmoid, ReLU, tanh)のみ扱います。

sigmoid

 \displaystyle{
\begin{align}
y = \frac{1}{1+e^{-ax}} \quad (a > 0)
\end{align}
}


 \displaystyle{
\begin{align}
y' &= \left(\frac{1}{1+e^{-ax}}\right)' \\\
&= -\frac{(1+e^{-ax})'}{(1+e^{-ax})^2} \\\
&= -\frac{(-a)e^{-ax}}{(1+e^{-ax})^2} \\\
&= \frac{ae^{-ax}}{(1+e^{-ax})^2} \\\
&= a\cdot \frac{1}{(1+e^{-ax})}\cdot\frac{e^{-ax}}{(1+e^{-ax})} \\\
&= a\cdot \frac{1}{(1+e^{-ax})}\cdot \left\{{1-\frac{1}{(1+e^{-ax})}}\right\} \\\
&= ay(1-y)
\end{align}
}


ReLU

 \displaystyle{
\begin{align}
y = \max(0, x)
\end{align}
}


 \displaystyle{
\begin{align}
y' =   \begin{cases}
    1 & ( x \gt 0 ) \\
    0 & ( x \leq 0 )
  \end{cases}
\end{align}
}


正確にはx=0において微分不可能ですが、実用上は0もしくは1としているようですね。 ここでは下記のオライリー本に合わせてx=0のときは0としました。


参考:

実装ノート · tiny-dnn/tiny-dnn Wiki · GitHub

高卒でもわかる機械学習 (4) 誤差逆伝播法の前置き – 頭の中に思い浮かべた時には

O'Reilly Japan - ゼロから作るDeep Learning


tanh

 \displaystyle{
\begin{align}
y &= \tanh x = \frac{e^x-e^{-x}}{e^x+e^{-x}} = \frac{\sinh x}{\cosh x} 
\end{align}
}



\displaystyle{
\begin{align}
y'&= (\tanh x)' \\\
&= \left(\frac{\sinh x}{\cosh x}\right)' \\\
&= (\sinh x)'\frac{1}{\cosh x}+\sinh x \left(\frac{1}{\cosh x}\right)'\\\
&= \frac{(\sinh x)'\cosh x - \sinh x (\cosh x)' }{(\cosh x)^2}\\\
&= \frac{ (\cosh x)^2 - (\sinh x)' }{(\cosh x)^2} \\\
&= 1-(\tanh x)^2 \\\
&= 1-y^2
\end{align}}


補足

 \displaystyle{
\begin{align}
\sinh x&= \frac{e^x-e^{-x}}{2} \\\
\cosh x&= \frac{e^x+e^{-x}}{2} 
\end{align}
}


 \displaystyle{
\begin{align}
(\sinh x)'&= \frac{e^x-(-1)e^{-x}}{2}  =\frac{e^x+e^{-x}}{2} = \cosh x \\\
(\cosh x)'&= \frac{e^x+(-1)e^{-x}}{2} =  \frac{e^x-e^{-x}}{2}=\sinh x
\end{align}
}


ライプニッツ

 \displaystyle{
(f(x)g(x))' = f'(x) g(x) + f(x)g'(x) 
}


実装と描画

import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

#活性化関数
def sigmoid(x):
    return 1 / (1 + np.exp(-x))  #係数a = 1とした

def relu(x):
    return np.maximum(0, x)

def tanh(x):
    return np.tanh(x)


#活性化関数の導関数
def d_sigmoid(x):
    return sigmoid(x)**2 * (1 - sigmoid(x))

def d_relu(x):
    return np.where(x>0, 1, 0)
    
def d_tanh(x):
    return 1 - tanh(x)**2


#matplotlibで描画
fig = plt.figure(figsize=(7,10))
axes = []

for i in range(2):
    axes.append(fig.add_subplot(2, 1, i+1))

x = np.linspace(-6,6,1000)    

for i, ax in enumerate(axes):
    if i ==0:
        ax.plot(x, sigmoid(x), color="k", label="sigmoid")
        ax.plot(x, relu(x), color="b", label="ReLU")
        ax.plot(x, tanh(x), color="r", label="tanh")
        ax.set_title("Activation functions")
    else:
        ax.plot(x, d_sigmoid(x), "--", color="k", label="d_sigmoid")
        ax.plot(x, d_relu(x), "--", color="b", label="d_ReLU")
        ax.plot(x, d_tanh(x), "--", color="r",  label="d_tanh")
        ax.set_title("Derivatives of activation functions")
        
    ax.legend()
    ax.grid(True)
    ax.set_xlim([-6, 6])
    ax.set_ylim([-1.1, 1.1])
    
plt.show()


f:id:Nasb:20190303160742p:plain


Texとmatplotlibの練習がてらやってみました。

高校数学を思い出すのが大変だった・・・

Google Magenta についてちょっとだけ調べた

本記事は前記事のもう少しだけ詳しい説明です。

nasb.hatenablog.com

Magentaが音楽生成する仕組み

Magentaが音楽生成する仕組みについては以下がとても勉強になりました!ありがとうございます!

Tech Circle #23 Next Music Production by Google Magenta

人工知能に音楽はつくれるのか? 〜 Google Magentaプロジェクトに見る人工知能による作曲方法について 〜


私は以下のように理解しました。(もし間違っていましたらご指摘ください・・・)


RNN(recurrent neural network)を使ったクラス分類
  • データ:

midiを使用。midiは音そのもののデータではなく、演奏情報のデータ(音の高さ、長さ、等)なので機械学習で扱いやすい。 midiは音の波形をサンプリングした生データから作曲に必要な特徴量を抽出して前処理されたものともみなせるような気がします。


  • 学習:

過去~現在の音符データから、次の音を推論する。

モデルが推論した(出力した)メロディやリズムが、教師データの曲に近くなるように学習させる。


  • 推論(音楽生成):

頭出しの音を学習したモデルに入力して、以降のメロディーを推論して出力する。


今回の記事では前回使った以下2つのモデルについて書きます。

  • improv_rnn
  • drums_rnn

improv_rnn

コード進行と出だしの音を与えるとそれに合ったメロディを生成してくれます。元のモデルであるmelody_rnnの改善(inprove)版ということでしょうか。

melody_rnnは出だしの音のみでコード進行は指定しませんでした。

人間が聞いて心地よいと感じる音のならびにはルールが存在し、それが音楽理論として体系化されています。そのため人間は多くの場合音楽理論に則って作曲しています。(今までの常識・理論をあえて破ってみたりする事で革新的なものが生まれる事も多いとは思いますが…)

既知の音楽理論という特徴量は、ディープラーニングの学習によって獲得するよりも人間が与えたほうが効率的ということだと思います。

今回の場合はコード進行という音楽理論に基づく制約を与えると、その中で使える(使って不自然ではない)音が限られるので、より自然なメロディーが生成できるということかと思います。

configurations

improve_rnnでは3つの方式を選択できます。

  • Basic Improv

    melody_rnnのBasicと似ているが、入力したコードのエンコードに48次元のone-hot vectorを使用する。

    one-hot vector は該当するクラスのみ1でそれ以外が0となるベクトルです。 1オクターブ12音階(C ~ hiC) x 4種のコード(Major, Minor, Augment, Diminish)を表現できるそうなので48次元必要になります。

  • Attention Improv

    melody_rnnのAttentionと似ているが、入力したコードのエンコードに48次元のone-hot vectorを使用する。


BasicとAttentionは次の音符を推定するに当たって過去の音符をどこまで考慮するかが違うようです。

こちらでとても詳細に解説してくださっています。

Tech Circle #23 Next Music Production by Google Magenta


  • Chord Pitches Improv

    基本的にはbasic improv と同じようですが、入力したコードのエンコードに、上記の48one -hot vector は使いません。以下のように12次元のベクトル3つを連結(concatenation)したものを使うようです。

'''Returns the input vector for the given position in the chord progression.

Indices [0, 36]:

[0]: Whether or not this chord is "no chord".

[1, 12]: A one-hot encoding of the chord root pitch class.

[13, 24]: Whether or not each pitch class is present in the chord.

[25, 36]: A one-hot encoding of the chord bass pitch class.

chords_encoder_decoder.py


[0]:

コードが存在するかしないかを示しています。


[1, 12]:

コードルートピッチクラスのone-hot encode vector ピッチクラスのマッピングは以下の通りです

# Mapping from pitch class index to name.
_PITCH_CLASS_MAPPING = ['C', 'C#', 'D', 'Eb', 'E', 'F',
                        'F#', 'G', 'Ab', 'A', 'Bb', 'B']

chords_encoder_decoder.py

例えばルートが”D”(レ)なら

[0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0]


[13, 24]:

各ピッチクラスの有無を表すバイナリベクトル

例えばC7#9(ジミヘンコードですね!)の構成音は[ド、ミ、ソ、シ♭、レ#] なので

[1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0]


[25, 32]:

コードベースピッチクラスを表すone-hot encode vector

C/Eのようなスラッシュコードでない限り、通常はルート音と同じです。


drums_rnn

drums_rnnは同時に複数の音を鳴らすという点でメロディとは異なることから、言語モデリングを使用しているということです。

ドラムの音色を少数のクラスにマッピングして学習させています。


configurations

コンフィグは2つから選択できます。

  • One Drum

すべてのドラムを単一のクラスにマッピングする。ということは、単に音が鳴ったor鳴ってないかのみ判断していると思われます。

  • Drums_kit

9つのクラス(バスドラム、スネアドラム、クローズドハイハット、オープンハイハット、タム x3、クラッシュシンバル、ライドシンバル)にマッピングする。

以下の数字はmidiのドラムノート番号です。例えば同じスネアでも複数のクラスがあるので、それらをスネアクラスとしてまとめています。

DEFAULT_DRUM_TYPE_PITCHES = [
    # bass drum
    [36, 35],

    # snare drum
    [38, 27, 28, 31, 32, 33, 34, 37, 39, 40, 56, 65, 66, 75, 85],

    # closed hi-hat
    [42, 44, 54, 68, 69, 70, 71, 73, 78, 80],

    # open hi-hat
    [46, 67, 72, 74, 79, 81],

    # low tom
    [45, 29, 41, 61, 64, 84],

    # mid tom
    [48, 47, 60, 63, 77, 86, 87],

    # high tom
    [50, 30, 43, 62, 76, 83],

    # crash cymbal
    [49, 55, 57, 58],

    # ride cymbal
    [51, 52, 53, 59, 82]
]

drums_encoder_decoder.py


9つのクラスから同時に発音する1~9個の音色を選択する組み合わせは29 = 512 通りあるので512次元のone-hot-vectorが必要になります。

(最初単純に9次元で良いのではと勘違いしたのですが、one-hotなのですべての場合それぞれに1次元必要になるようです)


参考にさせて頂いたページ

Magenta

GitHub - tensorflow/magenta: Magenta: Music and Art Generation with Machine Intelligence

Tech Circle #23 Next Music Production by Google Magenta

人工知能に音楽はつくれるのか? 〜 Google Magentaプロジェクトに見る人工知能による作曲方法について 〜

Google Magenta に作曲してもらった


概要

Google機械学習プロジェクト "Magenta"を利用して、8小節だけの曲を作ってみました。

できたもの


やったこと

  • Magentaの improv_rnn でメロディと伴奏を生成

  • Magentaの drums_rnn でリズム(ドラムパート)を生成

  • DAWで編曲


※今回は実施した手順に絞って書きました。もう少し詳しい中身については別記事で書く予定です。。。

※今回は学習済みモデルを使用したので、自分で学習はさせていません。


Magentaとは

magenta.tensorflow.org


An open source research project exploring the role of machine learning as a tool in the creative process. Magenta

Magentaプロジェクトでは音楽や芸術創作の手助けとなるツールが色々と開発されていますが、機械学習が全部作るということではなく、あくまで人間創作活動を補助するツールということですね。

今回は作曲におけるクリエイティブな部分はほぼMagentaにやってもらいましたが…


magentaには色々なモデルがありますが、今回は以下の2つを使いました。

  • improv_rnn: メロディの生成
  • drums_rnn : リズム(ドラムパート)の生成


環境

Magenta

  • Ubuntu18.04
  • CUDA9.0
  • cuDNN 7.4.1 (for CUDA9.0)
  • Anaconda 2018.12
  • python3.6

DAW

  • Windows10
  • Studio One 3


詳細は過去記事をご覧ください。

機械学習用PCを自作して環境構築するまで(その1) - いろいろ


インストール

こちらに従って行います(2019.2時点)。今回はGPUを使えるようにしたいのでManual Installが必要です。ということで、先にAnacondaの環境を作っておきます。

使用環境がTensorflow with GPU の動作要件を満たしているか確認しろと書いてあるので、pythonのバージョンは3.6にしときます。

$ conda create -n magenta pip python=3.6
$ source activate magenta

インストールします。

$ pip install magenta-gpu


メロディの生成

Improv_rnnを使用します。

コード進行(+いくつかのパラメータ)を指定すると、それにあったメロディとコードバッキングを出力してくれます。

まず、学習済みモデルをダウンロードして適当なフォルダに置きます。

http://download.magenta.tensorflow.org/models/chord_pitches_improv.mag

Ubuntuのターミナルで以下のコマンドを実行するとmidiファイルが生成されます。

improv_rnn_generate \
--config='chord_pitches_improv' \
--bundle_file=~/magenta/improv_rnn/pre_trained/chord_pitches_improv.mag \
--output_dir=~/magenta/improv_rnn/outputs  \
--num_outputs=10 \
--primer_melody="[67, 65, 67, -2, -2 ,-2, -2, -2]" \
--backing_chords="Eb Eb Cm Cm Fm Fm Bb Bb Eb Bb/D Cm Abm Eb Eb" \
--steps_per_chord=8 \
--render_chords
  • --config: 3つ選択できるうちchord_pitches_improvを使いました
  • --bundle_file: ダウンロードした学習済みモデルのパスです
  • --output_dir: 生成したmidiファイルの出力先です
  • --num_outputs: 生成するmidiファイル数です
  • --primer_melody: 出だしのメロディーを指定します。数字は音の高さを表していて、midi規格で決まっています。ここは「ソファソー」です。
  • --backing_chords: コード進行を指定します。
  • --steps_per_chord=8: 記述したコード一つの長さを指定します。ここでは8step = 2拍分にしました。指定しないと 16step = 4拍になるようです。

ちなみに、コード進行と出だしのメロディはQueenBohemian Rhapsodyからです。 (映画とっても良かったですね!)


生成されたメロディ:  10個生成したうち5つ掲載します。

※実際に生成された順番とは異なります。


リズムの生成

drums_rnnを使用します。

いくつかのパラメータを指定すると、ドラムシーケンスを出力してくれます。

こちらも同様に学習済みモデルをダウンロードします。

http://download.magenta.tensorflow.org/models/drum_kit_rnn.mag

Ubuntuのターミナルで以下のコマンドを実行するとドラムシーケンスが入ったmidiファイルが生成されます。

drums_rnn_generate \
--config="drum_kit" \
--bundle_file=~/magenta/drums_rnn/pre_trained/drum_kit_rnn.mag \
--output_dir=~/magenta/drums_rnn/outputs \
--num_outputs=10 \
--num_steps=128 \
--primer_drums="[(36,57)]"
  • --config: 2つ選択できるうち'drum_kit'を使いました
  • --bundle_file: ダウンロードした学習済みモデルのパスです
  • --output_dir: 生成したmidiファイルの出力先です
  • --num_outputs: 生成するmidiファイル数です
  • --num_steps: 1stepあたり16分音符の長さとなっているようです。128step = 4/4拍子で8小節分生成します。
  • --primer_drums:最初の音だけ指定します。数字はドラムの音色を表していますが、これもメロディと同様midi規格で決まっています。ここでは1拍目の頭でバスドラムとクラッシュシンバルが鳴るようにしました


生成されたリズム: 10個生成したうち5つ掲載します。

※実際に生成された順番とは異なります。

1の4拍目裏のオープンハイハット、2のシャッフル、3のシンコペーション、5のバスドラドコドコ等、それぞれ雰囲気が違っていて面白いですね!


DAWで編曲

上記の1番同士をDAWでちょっとだけ編集して曲にしてみました。

  • ベースパートだけは0から作成しました。

  • Magentaが作った譜面はメロディの最後の2音を削除した以外変えていません。


参考にさせていただいたページ

Magenta

GitHub - tensorflow/magenta: Magenta: Music and Art Generation with Machine Intelligence

人工知能時代の音楽制作への招待 - Google Magenta 解説&amp;体験ハンズオン (自習編) - - Qiita

作曲できる人工知能「magenta」を使って自動作曲してみる(mac) │ おとわび和風音楽素材


機械学習用PCを自作して環境構築するまで(その3)

機械学習を勉強するという名目で久しぶりにPCを自作しました。その記録です。 ※2019/4/13 追記(python環境構築について)


全3回(予定)です。

その1:ハードウェア構成

その2:OSの設定

その3: ソフトウェア環境構築 (←今回 )

 jupyter notebook上でtensorflow-gpu、tensorflow-cpu only が使える環境をそれぞれ作りました。



Tensorflow-gpuが使える環境を構築

こちらのページに私がやりたかった事が全部書かれておりました。ありがたや〜

qiita.com

私がインストールしたバージョンは以下です

  • CUDA9.0
  • cuDNN 7.4.1 (for CUDA9.0)
  • Anaconda 2018.12
  • Tensorflow-gpu v1.12


= = 4/13 追記 = =

Anacondaを使ったpython環境構築方法については議論があるようです。

pyenvが必要かどうかフローチャート - Qiita

Pythonの環境管理ツール良し悪し - Zopfcode

Anaconda は Environment Isolation Tool (環境分離ツール) ではない - Qiita


色々考えた結果、直にAnacondaをインストールしています。

(最初に貼らせていただいたリンク先のやり方と同じだと思いますが公式のリンクも貼らせていただきます。)

Installation — Anaconda 2.0 documentation


理由は以下です

  • ソフトウェア開発者ではない

→自宅PCでデータサイエンス・機械学習の勉強が目的(ちなみに本業はハードウェア寄りの事をやっています)

  • 環境はなるべくシンプルにしたい

→不要なもの・必要かどうかわからないものはなるべく入れたくない


懸念としては

  • Anaconda がPATHに変更を加える事でシステムのコマンドを隠蔽してしまう

がありますが、上記目的で使用している分には今の所不便はないです。


= = = = = = = =


Jupyter notebook上で使えるようにする

Anacondaで構築した 'tensorflow'という名前のtensorflow-gpu環境をactivateします。

$ source activate tensorflow

Anaconda環境でjupyter notebookをインストールします。

(tensorflow) $ conda install notebook ipykernel

Jupyter notebook のkernelに'tensorflow'を追加します。

(tensorflow) $ ipython kernel install --user --name tensorflow


Jupyter notebook上で確認します。

f:id:Nasb:20190119183611p:plain

New ボタンを押すと"tensorflow"が追加されているのがわかります。 (このあとcpu-onlyの環境も作成したので、画像には"tensorflow_cpu" も表示されています。)


参考: Condaの仮想環境をJupyter Notebookから利用する | 有意に無意味な話


CPU-onlyのTensorflow環境も構築

GPUとの比較のためにCPU-onlyのTensorflow環境も構築しました。

$ conda create -n tensorflow_cpu pip python=3.6

作成した"tensorflow_cpu"をactivateします。

$ source activate tensorflow_cpu

"tensorflow_cpu"でもjupyter notebookをインストールします。

(tensorflow_cpu) $ conda install notebook ipykernel

Jupyter notebook のkernelに'tensorflow_cpu'を追加します。

(tensorflow_cpu) $ ipython kernel install --user --name tensorflow_cpu



Jupyter notebook上で確認します。 f:id:Nasb:20190119183639p:plain


動作確認

動作確認も兼ねて、Tensorflowのwebサイトに乗っているMNISTチュートリアルで試してみます。

gpu, cpu-onlyそれぞれで実行してみて学習時間を比較してみます。


ソースはこちら:

https://www.tensorflow.org/tutorials/



結果

GPU : 3s / Epoch

CPU-Only : 5s / Epoch


チュートリアルで非常に軽い計算だったためか、あんまりGPUの恩恵がわかりません。

今度はもっと計算量の多いやつで比較してみたいと思います。

ちなみに実行ログは以下です。

GPU
Epoch 1/5
60000/60000 [==============================] - 3s 57us/step - loss: 0.2039 - acc: 0.9404
Epoch 2/5
60000/60000 [==============================] - 3s 48us/step - loss: 0.0812 - acc: 0.9757
Epoch 3/5
60000/60000 [==============================] - 3s 48us/step - loss: 0.0537 - acc: 0.9831
Epoch 4/5
60000/60000 [==============================] - 3s 48us/step - loss: 0.0371 - acc: 0.9881
Epoch 5/5
60000/60000 [==============================] - 3s 49us/step - loss: 0.0270 - acc: 0.9913
10000/10000 [==============================] - 0s 22us/step

[0.08493565447720466, 0.977]


CPU-Only
Epoch 1/5
60000/60000 [==============================] - 5s 83us/step - loss: 0.2041 - acc: 0.9400
Epoch 2/5
60000/60000 [==============================] - 5s 81us/step - loss: 0.0793 - acc: 0.9763
Epoch 3/5
60000/60000 [==============================] - 5s 85us/step - loss: 0.0518 - acc: 0.9841
Epoch 4/5
60000/60000 [==============================] - 5s 83us/step - loss: 0.0382 - acc: 0.9876
Epoch 5/5
60000/60000 [==============================] - 5s 81us/step - loss: 0.0272 - acc: 0.9913
10000/10000 [==============================] - 0s 20us/step

[0.06549025830835745, 0.9816]



以上