Blog of Luri a Pupile of Koo

I love the azure earth planet

ミニマルサウンドの機械学習による再編......への前奏曲

Sorry. This Page is Japanese Only.

この記事は創作+機械学習 Advent Calendar 2021の17日目です。

adventar.org

貴方はサンタさんに今年何をお願いしま[す|した]か?

皆さま、こんにちわ。こんばんわ。そしてはじめまして!かえるのクーの孫弟子の「井戸中 」(いとなか るり)です。まずは自己紹介します。

キャラ設定:

気のマッド(泥)サイエンテストである「かえるのクー」の助手である「井戸中 聖」(いとなか セイ:ワシミミズク)の門下生です。キャラクター割り当ては「鳥」です。

f:id:luriAPupileOfKoo:20211202142822j:plain  f:id:luriAPupileOfKoo:20211210190610j:plain
芽守のイメージ

おもに自然言語系の意味ネットワークに興味があります。本家クーの自由研究ブログ担当としては、おたまじゃくし♩さん(人類の場合は小中学生、場合によっては高校生くらいまで)の教育係/伝道者をしています。

今回はサウンド/音声部門としてエントリーしました。

かえるのクー」の登場を期待された方には申し訳ございません。「だれか(なにかカレンダーみたいのに)出てみて!」と失踪先から通信がはいり、一言残してまた消失しました。今、時期的には彼/彼女は既に冬眠にはいっていると思われます(なので連絡・応答が不可能)。今回記事の文責は「井戸中 」にございます。よろしくお願いします。なお、プログラムは「井戸中 芽守」(いとなか めもる):とんがり帽子妖精キャラ(実は異星人):音楽担当にも手伝わせってもらいました。

お題:サウンドの再編についてです

12月にはいってから参加を決め、途中路線変更もあり実質1週間程度で無理やり準備した内容となります(言い訳ともいいます)。「創作」とまではいかず、ちょっとした解析&再編となりました。

使用する技術/スキルについて

・自己符号化器のしくみをウェーブレット解析ツール(中間1層!の浅層学習済モデルに相当します)として使用しました(周波数解析&テンポ解析)。

・高いレベルの「根性」スキルを必要としました。

音のウェーブレット解析はお勧めです!

今回、「ウェーブレット解析」を使用します。ある瞬間(微小区間)の「音」(単音である必要はありません)を切り出して解析をします。ウェーブレットとは、例えば↓こんな形の波形です。波の両端がゼロであるところがポイントです。

f:id:luriAPupileOfKoo:20211209184137p:plain

使用ウェーブレットの基本形状は任意(sin波でないほうが効率的なケースもありますが、今回はsin波を使用しています)で、元になるものを「マザーウェーブレット」(お母さん波形)と呼びます。

ウェーブレット解析はイメージ的に言えば、上記のようなマザーウェーブレット波形を「ものさし」にしてどの程度「測定対象」と「似ているか」を測って離散的にサンプリングしていくようなものです。ものさし自体をいろいろな長さに伸縮して計測します。波なので位相もとても重要です。(日中の長さが同じでも春と秋は別物ですよね)位相のほうが周波数より重要な局面は多くあります。ウェーブレット解析では必然的に位相も計測することになります。

音/波形の解析といえば「フーリエ解析」と思われる方も多いかもしれませんが、局所的な解析や(音の)解像度の調整は5割増しで「ウェーブレット解析」の方が行いやすいので、お勧めです。(元の波形の窓処理をしなくてよいなどの利点)過去の本家ページでの実験例です。ウェーブレット解析(分解:エンコード)したあと、ウェーブレット群から元の音を復元(合成:デコード)している例です。「音楽」の方やオーディオマニアの方でないと違いに気づけないくらいだと思います。

また、ウェーブレット計算を使えば、「ほぼ」完璧に特定周波数をフィルタリングできます。(余談ですが下記リンクのNG例が「アニメにでてきたAIが学習し損ねた音」のようで気に入っています)

このように、ある瞬間前後、または指定した区間での (sin波)周波数 の解析ができます。(あらゆる音は離散ウェーブレットの(有限)組み合わせであると考えた計算をします)数式/理論はわかりにくいですが、配列/計算的には、特定区間の「波形・信号列」切り出しと「ウェーブレット基底の拡大・縮小波」を内積(ドット計算)すればいいだけでなので超お手軽です!(ただし1か所(基準時点)ごと複数伸長のウェーブレットで計測し、それを全体へスウィープしていくので、計算量は非常に多い弱点もあります。でも今やnumpy(CPU全コア/スレッド並列計算)やcupy/CUDA(GPU並列計算)が弱点を補います!)

周期性が内在するものであれば、なんでも解析可能です!!

("_ウェーブレット魔力万能です_")

動機:

今回はサウンド(音質etc)を改変/創作補助するレベルで考えていましたが、対象が「声」ではないのでミキシング・イコライジングエフェクター処理とそんなに変わらず、面白くならなさそうでした。音列「パターン」を組み替えることでサウンドを改変すれば面白いかもしれないと考えました。

息切れ:

ところが、最初のリズム検出で思いのほか時間がかかってしまい、この範囲でまとめました。(おもしろくなるところまで、行けませんでした)

サブジェクト(今回の解析対象サンプル):

サンプルの入力ミニマル音(単純音の繰り返し)はコンピューターが作成した「感じ」のランダムさで、(天然が)適当に手弾きしたものです。あえて「音圧」でのリズム検知をしにくいようなBeat感のすくない音列にしています。また、リズム周期性も検出しにくいように「ポリリズム」(複合リズム/変拍子)を意識しました。意図せずテンポ検出困難なくらい、リズム音痴にもなっています。(精一杯機械的に弾いたつもりでしたが、もっさり感満載で引きました)

わざと!リズム/テンポ検出しにくくしたのは、既存のDTM(デスクトップミュージック)ソフトや既存ツールで簡単に「リズム解析」できるものでは、面白味に欠けると思った次第です。

処方:

(Step1)入力サンプルのミニマル音をウェーブレットで解析して、「周波数構成(倍音構成)」×「時系列」を取得します。


周波数(音程)の検出グラフ*1

      f:id:luriAPupileOfKoo:20211211134925p:plain


12音階ちょうどのウェーブレットで解析しているので、上記のような感じになります。

よく見る周波数解析グラフとくらべて、「音程」に特化しており、切れ味のよいピアノロール的*2なヒートマップ図になっているのがわかります。*3

音を鳴らしながらグラフのいちばん低い音といちばん高い音に注目すれば、今どのあたりかわかると思います。縦軸はMidiノート番号互換の音高(周波数)です。

(Step2)そのままではさすがにリズム検知しにくいので、周波数強度を12音階で集計して、音階としての「強度」を得ます。(倍音も音階として加算することになります)*4

(Step3)リズムは「周期」なので、各音階の強度(変化)に対して、周波数1Hz~6Hz(♩=60~360 BPM)のウェーブレットでさらに周期性を解析します。「周波数信号強度自体の周期性」ではなく、「強度の立ち上がり」部分の周期性を解析しているのがポイントです。

Librosaなどを使えばもっとシンプルにできる部分がありますが、今回はベタコーディングしました。末尾に貼ります。実験プログラムなので、つぎはぎですがご容赦ください。

以下は今回入力の解析例です。それなりにテンポ解析できていると思います。継続性のある強度が大きい(青から白色)部分が検出テンポになります。


リズム/テンポの検出グラフ

f:id:luriAPupileOfKoo:20211210231536p:plain


縦軸 tp は 0が1Hz(♩=60)、 24が2Hz(♩=120)、48が4Hz(♩=240)... 実テンポ:=60 * (2**(tp/24))です。縦軸の「1」 が 2の24乗根になります。(内部データをそのままグラフにしたので、軸変換/換算していません)

横軸は計算上の1ブロック単位(1ブロック=約0.17秒:8192サンプル)での経過時間です。今回のサンプル例での横軸全体は、48000Hzサンプリング、27.107秒(1301136サンプル/およそ159ブロック)です。

赤点線(手書き)でテンポ検出の近傍最大を説明用につなぎました。最初はいい感じですが、中盤以降ぐだぐだです。

参考文献の図表(以下はテンポ検出で参考にした図表です)

f:id:luriAPupileOfKoo:20211210000059p:plain

kurene様の解説・サンプルがわかりやすいです。

(Step4)平均のテンポを導出し、それをもとにクリック音を入れてみます。

今回は正確なクリック音を入れることが目標ではなく、それなりにリズム検出できていることを確認したいので、そのまま平均テンポでクリック音を重ねてみます。(クリック音は単一ウェーブレットそのものにします)

クリックタイミングはすこしずれていますが、概ねリズムはとれている感じがします。

 

(Step5)元演奏がかなりゆらいでいるため、(4)で導出した平均リズムへ、ウェーブレットエンコードレベルで「無理やり」タイミング補正してみます。平均リズムの1/4へとプログラムで疑似クオンタイズ(ずれている音を最寄りのタイミングに補正すること)をしています。処理の組み合わせ的には少しだけ(実音ではなく、エンコードレベルで処理している点について)独自性があるかもです。(「疑似」なのは本来やるべきこと:音持続のグループ化などを端折って、部分的に調整しているためです)

音質、強弱や処理・判定不能箇所は触っていないので、ミニマル的に「悪い意味で」下手なグルーブ感がのこりました。

とはいえ、(前半はわかりにくいですが、特に後半は)かなり改善されていると思います。

副作用:

前後の信号強度がつながらなくなるような無理やりなクオンタイズをしていますので、調整した境界で音程が不安定になるアーティファクトノイズ/歪が発生します。

リズム感はよくなりましたが、これではちょっと使えません。。。工夫すれば周波数も安定する方法はあると思うので検討します。

お詫びと経過観察:

予想していたほどにはうまくできませんでした。また、音の組み換えはできませんでした。ごめんなさい。お気づきのように、ここまでで「機械学習」は登場していません。(機械学習の「部品」ともいえる(ウェーブレット)符号化器*5は登場しました)音そのものを学習対象として考える場合はどうしても、解析(前処理/初段処理)が必要となります。機械学習までたどり着けませんでしたが、「前処理」も機械学習の守備範囲としてご容赦くださいせ。

参考文献:音声に関する機械学習ずんだもんのだいたい3分機械技術講座なのだ。の説明がわかりやすいです!「ずんだもん」ちゃんかわいい!!「つくよみちゃん」ちゃん声が素敵すぎる!!


★経過観察

(2021/12/18)エンコードレベルで転調を試みましたが、デコードすると位相が衝突して「音の」奈落へ落ちました。音にも「不気味の谷」や「狂気の大穴」はいたるところあるんです。ナナチ助けて~!

(2021/12/19)エンコード上で位相情報の圧縮(というより特定位相への寄せ)にチャレンジするも、見事敗北。どうにか現世にとどまる音なので貼り付けます(PGコードは省略:聞かないほうがよい音です。完全に失敗すると、もっとナイトメアーな音になります)

(2021/12/20)経過観察終了:路線変更で一旦、アルゴリズム創作方面にいってみます。もともとは↓こんな感じ(下のサウンドアルゴリズム生成したものです)の音列を機械学習で生成し、好みに応じて進化改善していきたかったのです。(意味があるか「でたらめ」か、わからないくらいの乱雑さを目標としました)

別ページで解説するかもしれません。突貫で全体がとても長くなりすみませんでした。

経過観察は以上です。よいクリスマスを!


ミニマル音楽に興味を持たれた方は:(本編とは無関係)

覚悟がある方は『ここ』💊をクリックしてください。赤い錠剤を飲むとクリックすると後戻りできません。(音楽人生が大きくかわる危険性があります:youtubeにアップされたとても有名なミニマル音楽です。ミニマル音楽は、構造的にとても機械学習と相性がよいように思います)お耳なおしになるかと思います。

すでにどっぷりはまっている方、4拍子を忘れて久しい方には息抜きに『こちら』をどうぞ。(アイデアと感性が秀逸です。ジャズに抵抗がない方なら聴けるかもしれません。)

え?!師匠(セイ)がミニマルやるならどうしても『これ』を貼ってほしいというので。(本意ではないです。特にボカロ好きの方は聞いてはいけません)

ほうら、だんだんすべての音をミニマルにしたくなってきませんか。きましたか。きましたね。きましたよ。ほら、きました!キタ━(゚∀゚)━!  失礼きました。

今後発表されるであろう「既成の音楽理論を超越した自動作曲」*6を深く味わうためには、人間/生物側の感性も磨いておく必要があります。そして機械学習/創作に触発された感性はさらなる深淵/天空へ。。。人類がいままで聞いたことがない気持ちのよいサウンドをめざしてがんばりましょう!

考察と妄想:

唐突ですが機械学習汎化の観点で、ディープラーニングの各層は多次元ウェーブレット群を「学習」する装置である、とみなすことがでます(私見)。あらゆるものは「周期性があるもの」の多次元的な複合物/現象ととらえれば、ディープラーニングの非常に深い階層がいろいろな周期に(たまたま)フィットして特徴抽出をうまくできることは感覚的に分かります。でも個人的にはうまくいくからといって無駄に深すぎるのは、「あまりにも非エコ」である気がしてなりません。

大脳は(ループ構造が一部あるといえ)6層程度の「驚くほど遅い」ニューロンネットワークユニット群で、高度で多様な汎用(触覚も味もにおいも音も映像も言語/文章も運動制御系への指令も抽象概念も第六感も)複合処理ができているので、現在の技術はまだ遠く自然の摂理に及んでいないと痛感します。

「音+生物」の観点でいえば、音程解析部(周波数検出部)は生物の蝸牛器官(内耳)に相当し、テンポ検出部は大脳の聴覚野(の一部)に相当します。聴覚野は音に関するさまざまなパターン検出を(ホンモノの「ニューロン」ネットワークで)行えます。プログラムでは音程解析部以降をGANなどのしくみに繋いでゆらぎを与えれば、いろいろな音列の(疑似的?または人の「それ」と本質的に変わらない?)創成ができる「はず」です。。。

f:id:luriAPupileOfKoo:20211217140021p:plain

実験プログラムの当初コンセプトは、『(人または学習による音列の)草案・創成(の断片)を「人と機械学習」の相互評価/試行ループにより、進化計算的に「よりよいもの」にしていく』*7という大風呂敷でしたが、圧倒的にスキル&時間不足で企画倒れました。あとはサンタさんに完成をお願いします。

明日は mikio | ミキオ 様の予定です。よろしくお願いします。

 

蛇足:今回作成した実験プログラム(クラス部分は過去プログラムを流用)

実験用のプログラムで、学習フレームワーク/Librosa(Pythonの汎用音楽/サウンド系処理ライブラリ)などを使わず「ベタ」コーディングしているので無駄に長いです。ご了承ください。時間がなくてパラメータやロジックが汎用的になっておらず、つぎはぎなので読む価値はあまりないと思います。改善は課題とさせてください。


#!/usr/bin/env python
# -*- coding: utf-8 -*-
import math

import numpy as np
from scipy import signal
import audioop
import wave
import copy
import matplotlib.pyplot as plt # --- 説明用 ---
import seaborn as sb # --- 説明用ヒートマップ図 ---
"""
-------------------------------------------------
Advent Calendar 2021.12.17 Luri Itonaka:luriAPupileOfKoo & Memol Itonaka
Hazure Encoder V1.0 2021.12.17 Luri Itonaka:luriAPupileOfKoo
Korejanai Encoder V1.0 2017.09.22 Koo Wells
-------------------------------------------------
"""
IN_WAVE_FILE = ".\\ThemeOfAdventCalender17thDay.wav"
DECODE_FILE_1 = ".\\DecodeSound_addClick.wav"
DECODE_FILE_2 = ".\\DecodeSound_forceQuantize.wav"
BASE_SAMPLING = 48000 # 内部処理の基本サンプリングレート
BASE_WINDOW_SIZE = 2**15 # いちばん広いウェーブレット重みの幅
HALF_WINDOW_SIZE = int(BASE_WINDOW_SIZE / 2) # BASE_WINDOW_SIZE幅の半分
PHASE_SPLIT = 16 # 位相は16分割(22.5°)
A0_NOTE_NO = 21 # A0(27.5Hz) Midi Note No. ピアノの一番低い音
A4_NOTE_NO = 69 # A4(440Hz) Midi Note No.
CROMATIC = 12 # 平均律12半音
WAVELET_SHIFT = 4 # ウェーブレットを1/指定数 分づつスゥィープシフト


""" サウンドの読み込みと変換をします。------------------------------------------------------"""
class soundtool():
def __init__(self):
pass

# 音のファイルを読み、基本的な情報を獲得します。
def fetch_soundData(self, filename):
wave_read = wave.open(filename, 'r')
w_channel = wave_read.getnchannels()
w_rate = wave_read.getframerate()
w_framenumber = wave_read.getnframes()
w_frame = wave_read.readframes(w_framenumber)
wave_read.close()
return w_channel, w_rate, w_framenumber, w_frame

# ストリームをサンプリングレート48KHz モノラルの音の配列(16bit)に変換します。
def conv48KMonoArray(self,aframe, achannel, arate):
if achannel == 2:
converted = audioop.tomono(aframe, 2, 0.5, 0.5)
else:
converted = aframe
new_frames = audioop.ratecv(converted, 2, 1, arate, BASE_SAMPLING, None)
array = np.array(np.frombuffer(new_frames[0], dtype="int16"))
return array

# ストリームをWaveファイルとして出力します。
def write_soundData(self, filename, stream):
s_write = wave.open(filename, 'w')
s_write.setparams((1, 2, BASE_SAMPLING, 0, 'NONE', 'Uncompressed'))
s_write.writeframes(stream)
s_write.close()

""" ボクが欲しいのはコレジャナイ Koo エンコーダ --------------------------------------------"""
class korejanai():
k_activation_Sigmoid = 'Sigmoid'
k_activation_ReLU = 'ReLU'
k_activation_Tanh = 'Tanh'
k_activation_Liner = 'Liner'

def __init__(self):
# 活性化関数とウェーブレットの設定
self.func = self.factory_activate_func(self.k_activation_ReLU)
self.func2 = self.factory_activate_func(self.k_activation_Liner)
# 21:MidiNote A0 27.5Hz
self.w1 = [
self.wavelet_init(2**(0/2), A0_NOTE_NO, A0_NOTE_NO + CROMATIC*1 - 1, PHASE_SPLIT, 2**15),
self.wavelet_init(2**(1/2), A0_NOTE_NO + CROMATIC*1, A0_NOTE_NO + CROMATIC*2 - 1, PHASE_SPLIT, 2**14),
self.wavelet_init(2**(2/2), A0_NOTE_NO + CROMATIC*2, A0_NOTE_NO + CROMATIC*3 - 1, PHASE_SPLIT, 2**13),
self.wavelet_init(2**(3/2), A0_NOTE_NO + CROMATIC*3, A0_NOTE_NO + CROMATIC*4 - 1, PHASE_SPLIT, 2**12),
self.wavelet_init(2**(4/2), A0_NOTE_NO + CROMATIC*4, A0_NOTE_NO + CROMATIC*5 - 1, PHASE_SPLIT, 2**11),
self.wavelet_init(2**(5/2), A0_NOTE_NO + CROMATIC*5, A0_NOTE_NO + CROMATIC*6 - 1, PHASE_SPLIT, 2**10),
self.wavelet_init(2**(6/2), A0_NOTE_NO + CROMATIC*6, A0_NOTE_NO + CROMATIC*7 - 1, PHASE_SPLIT, 2**9),
self.wavelet_init(2**(7/2), A0_NOTE_NO + CROMATIC*7, A0_NOTE_NO + CROMATIC*8 - 1, PHASE_SPLIT, 2**8),
self.wavelet_init(2**(8/2), A0_NOTE_NO + CROMATIC*8, A0_NOTE_NO + CROMATIC*9 - 1, PHASE_SPLIT, 2**7),
self.wavelet_init(2**(9/2), A0_NOTE_NO + CROMATIC*9, A0_NOTE_NO + CROMATIC*10 - 1, PHASE_SPLIT, 2**6),
]

self.w2 = [
self.w1[0].T,
self.w1[1].T,
self.w1[2].T,
self.w1[3].T,
self.w1[4].T,
self.w1[5].T,
self.w1[6].T,
self.w1[7].T,
self.w1[8].T,
self.w1[9].T,
]
self.z = [
[],
[],
[],
[],
[], # これ以前はコメントアウトしないでください。
#[], # 高い周波数解析はとても時間がかかるので、実験では880Hz以上はコメントアウトします。(断崖絶壁的ローパスフイルタとして機能します)
#[],
#[],
#[],
#[],
]

# 活性化関数のファクトリ
def factory_activate_func(self, activate_func):
rtn_func = ActivateFunction()
if activate_func == self.k_activation_Sigmoid:
rtn_func = Sigmoid()
elif activate_func == self.k_activation_ReLU:
rtn_func = ReLU()
elif activate_func == self.k_activation_Tanh:
rtn_func = Tanh()
elif activate_func == self.k_activation_Liner:
rtn_func = Liner()
return rtn_func

# エンコード
def encode(self, x):
print('*** Encode now ***')
for i in range(len(self.z)):
print('range = %d (%d to %d Hz)' % (i, self.get_freq(i * CROMATIC + A0_NOTE_NO), self.get_freq((i + 1) * CROMATIC + A0_NOTE_NO)))
self.z[i] = []
w1 = self.w1[i]
ws1 = w1[0]
swin = x.size / ws1.size * WAVELET_SHIFT + 1
xwork = np.append(np.zeros(HALF_WINDOW_SIZE), x, axis=0)
xwork = np.append(xwork, np.zeros(HALF_WINDOW_SIZE), axis=0)
for s in range(int(swin)):
xwin = xwork[int(s * ws1.size / WAVELET_SHIFT):int(s * ws1.size / WAVELET_SHIFT) + ws1.size]
zwin = self.func.activate_func(np.dot(w1, xwin))
self.z[i].append(zwin)

return self.z

# デコード
def decode(self, z, acount):
print('*** Decode now ***')
x = [0] * (acount + BASE_WINDOW_SIZE + HALF_WINDOW_SIZE)
for i in range(len(self.z)):
print('range = %d (%d to %d Hz)' % (i, self.get_freq(i * CROMATIC + A0_NOTE_NO), self.get_freq((i + 1) * CROMATIC + A0_NOTE_NO)))
zwin = z[i]
w1 = self.w1[i]
ws1 = w1[0]
w2 = self.w2[i]
ws2 = w2[0]
swin = acount / ws1.size * WAVELET_SHIFT + 1
for s in range(int(swin)):
try:
xwk = self.func2.activate_func(np.dot(w2, zwin[s]))
except:
print('error 01')
print(s)
print(len(zwin))
try:
x[HALF_WINDOW_SIZE + int(s * ws1.size / WAVELET_SHIFT):HALF_WINDOW_SIZE + int(s * ws1.size / WAVELET_SHIFT) + ws1.size] += xwk
except:
print('error 02')
print(s)
print(ws1.size)
print(xwk.size)

return x[BASE_WINDOW_SIZE:acount + BASE_WINDOW_SIZE]

# ウェーブレットの初期設定
def wavelet_init(self, amp, astart_note, aend_note, aPHASE, asample_size):
swav = np.empty(((aend_note - astart_note + 1) * aPHASE, asample_size))
fs = BASE_SAMPLING
sec = asample_size / fs
freq = []
hann_window = signal.hann(asample_size) # ウェーブレットには「ハン窓」を使用します
for n in range(160)[astart_note:aend_note + 1]:
f = self.get_freq(n)
freq.append(f)
i = 0
for f in freq:
for PHASE in range(PHASE_SPLIT): # 指定された位相のウェーブレットを準備します。
s = amp * np.sin(2.0 * np.pi * (f * np.arange(fs * sec) / fs + (PHASE / PHASE_SPLIT)))
swav[i] = s * hann_window
i += 1
return swav

# 指定周波数取得(平均律
def get_freq(self, n):
note = n - A4_NOTE_NO
freq = 440.0 * (2 ** (note / CROMATIC))
return freq

""" ハズレ(Hyper瑠璃色)エンコーダ(音声サンプリング2エンコード専用) -----------------------"""
class hazure():
R_SPLIT = 24
F_SPLIT = 16
WAVELET_SHIFT = 4
HALF_WINDOW_SIZE = 256
def __init__(self, k): #コレジャナイエンコーダの機能を使用するため、引数に必要です
self.k = k
# 活性化関数とウェーブレットの設定
self.func = self.k.factory_activate_func(self.k.k_activation_ReLU)
self.func2 = self.k.factory_activate_func(self.k.k_activation_Liner)
self.w1 = [
self.r_wavelet_init(2**(0/2), 1), # (60 to 120 BPM)
self.r_wavelet_init(2**(1/2), 2), # (120 to 240 BPM)
self.r_wavelet_init(2**(2/2), 3), # (240 to 480 BPM)
self.r_wavelet_init(2**(3/2), 4), # (480 to 960 BPM)
]
self.z = [
[],
[],
[],
[],
]

# ウェーブレットの初期設定
def r_wavelet_init(self, amp, fr):
asample_size = int(2**10 / 2**fr )
swav = np.empty((self.R_SPLIT * self.F_SPLIT, asample_size))
fs = BASE_SAMPLING / 512
sec = asample_size / fs
freq = []
hann_window = signal.hann(asample_size)
i = 0
for f in range(self.R_SPLIT):
freq = 2 ** (f / self.R_SPLIT)
for PHASE in range(self.F_SPLIT):
s = amp * np.sin(2.0 * np.pi * (2**(fr - 1) * freq * np.arange(fs * sec) / fs + (PHASE / self.F_SPLIT)))
swav[i] = s * hann_window
#plt.plot(range(len(swav[i])), swav[i])
#plt.show
i += 1
return swav

# リズム用エンコード(今回解析専用)
def r_encode(self, x):
print('*** Rhythm Encode now ***')
for i in range(len(self.z)):
print('range = %d (%d to %d BPM)' % (i, 60 * 2**i, 60 * 2**(i+1)))
self.z[i] = []
w1 = self.w1[i]
ws1 = w1[0]
work = []
swin = x.size / ws1.size * self.WAVELET_SHIFT + 1
offset = self.HALF_WINDOW_SIZE
xwork = np.append(np.zeros(offset + (8 - 2**(i+3))), x, axis=0) #無音領域付加と立ち上がり部にしたことによる補正
xwork = np.append(xwork, np.zeros(offset - (8 - 2**(i+3))), axis=0)
for s in range(int(swin)):
xwin = xwork[int(s * ws1.size / self.WAVELET_SHIFT):int(s * ws1.size / self.WAVELET_SHIFT) + ws1.size]
zwin = self.func.activate_func(np.dot(w1, xwin))
work_base = []
for t in range(self.R_SPLIT):
wwin = zwin[t * self.F_SPLIT:(t + 1) * self.F_SPLIT]
work_base.append(sum(wwin))
work.append(work_base)
self.z[i].append(work)
return self.z

def broad_array(self, zz):
x_range = len(zz[3][0])
y_sub_range = len(zz[0][0][0])
y_range = y_sub_range*len(zz)

rtnVal = np.zeros((x_range, y_range))
for i in range(x_range):
for j in range(y_range):
rtnVal[i][j] = zz[int(j / self.R_SPLIT)][0][int(i / 2**(3 - int(j / self.R_SPLIT)))][int(j % self.R_SPLIT)]
return np.reshape(rtnVal, (x_range, y_range))


class ActivateFunction(object):
def __init__(self):
self.name = self.__class__.__name__

@classmethod
# @abstractmethod
def activate_func(cls, x):
raise NotImplementedError()

@classmethod
# @abstractmethod
# 微分関数は現在つかってませんが、そのうち実験で使うかもしれないので残しています。
def differential_func(cls, x):
raise NotImplementedError()

""" ---------------------------------------------------------------------------------------
Define the activate function
---------------------------------------------------------------------------------------"""
class Sigmoid(ActivateFunction):
@classmethod
def activate_func(cls, x):
return 1. / (1. + np.exp(-x))

@classmethod
def differential_func(cls, x):
return x * (1. - x)

# ---------------------------------------------------------------------------------------
class Tanh(ActivateFunction):
@classmethod
def activate_func(cls, x):
return np.tanh(x)

@classmethod
def differential_func(cls, x):
return (1. - (np.tanh(x) * np.tanh(x)))

# ----------------------------------------------------------------------------------------
class ReLU(ActivateFunction):
@classmethod
def activate_func(cls, x):
return x * (x > 0)

@classmethod
def differential_func(cls, x):
return 1. * (x > 0)

# ----------------------------------------------------------------------------------------
class Liner(ActivateFunction):
@classmethod
def activate_func(cls, x):
return x

@classmethod
def differential_func(cls, x):
return np.ones(x.shape)

# 今回専用のメソッド(クラス化するときに汎用性を考慮しましょう)---------------------------
def sum_sound_atack(zf):
wfsample = []
for i in range(len(zf)):
wffsample = []
if i >= 5: # 5以上になるまで待つのは平均と比較するためです
for j in range(12):
nowval = 0
avrval = 0
for k in range(16): # 位相ごとの強度を合計し、周波数ごとにまとめる
nowval += zf[i][j + k]
fzavrval = 0
for s in range(1, 5):
fzavrval += zf[i - s][j + k]
avrval += (fzavrval / 4) # 直前4点の平均をとる
addval = nowval - avrval # 直前平均との差をとる
if addval > 0:
wffsample.append(addval) # プラスであれば、音の立ち上がりとして加算
else:
wffsample.append(0) # マイナスであれば、音の減衰期としてゼロ
wfsample.append(wffsample)
else:
for j in range(12):
wffsample.append(0)
wfsample.append(wffsample)
return wfsample

# リズム検出用12音サマリ計算
def conv_croma(z):
# ウェーブレットサンプリング(エンコード)値を加算して、周波数ごとの信号強度を得る
wsample = []
for frange in range(len(z)):
wfsample = sum_sound_atack(z[frange])
wsample.append(wfsample)

# range = 4 (440 to 880 Hz)をもとにクロマチック半音12階にまとめる
# いろいろ組み合わせをかえるので、ベタで計算します
croma_all = []
for croma in range(12):
croma_level = []
for ws in range(len(wsample[4])): #低い周波数は無視しています
cws = wsample[4][ws][croma] + wsample[3][int(ws/2)][croma] + wsample[2][int(ws/4)][croma]
croma_level.append(cws)
croma_all.append(croma_level)
croma_total = []
for ws in range(len(croma_all[0])):
wsc = 0
for croma in range(12):
wsc += croma_all[croma][ws]
croma_total.append(wsc)
return croma_total

# 12音階粒度平滑化
def broad_array(z):
z_max = len(z) - 1
y_max = len(z[z_max])
rtnary = np.zeros((z_max * 12, y_max))
for i in range(y_max):
for j in range(z_max): # Octave
for k in range(12): # Chromatic
c_val = 0
for p in range(16): # Phase
c_val += z[j][int(i / (2**(z_max - j)))][k * 16 + p] / math.sqrt(2**(z_max - j))
rtnary[j * 12 + k][i] = c_val
return rtnary

# リズムが ♩ =120240とした場合の推定リズム
def report_rythm(zz):
target = zz[1][0]
t_len = len(target)
count, r_total = 0, 0,
r_list = []
for i in range(t_len):
s_max, r_val = 0, 0
for j in range(24):
if target[i][j] > s_max:
s_max = target[i][j]
r_val = j
if s_max > 0.5 * 10**10 and r_val != 0 and r_val != 23: # 振り切れている場合は正しく検出できていない可能性が高いので除外
# 今回実験なので、検出の閾値は測定後に適値を手動で設定しています。本来はここは2PASSにするなどして自動にすべきです。
rytm = 60 * 2**(1 + r_val/24)
print("point={:d}: {:.2f} BPM".format(i, rytm))
r_total += rytm
r_list.append((i, rytm))
count += 1
estimated = r_total / count
print("avarage Rythm = {:.2f} BPM".format(estimated))
return estimated, r_list

# WAVEフォーマットファイルの出力
def output_wave_file(x_hat, file_name):
amax = max(np.max(x_hat), -np.min(x_hat)) # 振幅の最大値を求める
decode = x_hat / amax * 32768 * 0.98 # 16bitでほどよく収まるように振幅を調整する
decode.astype('int16') # Wave出力用に16Bit化します
work = b''
out_stream = b''
amp = 0
print('*** convert Array to Stream Now ***') # WAVEファイルとして出力します。
for i in range(len(decode)):
amp = int(decode[i])
ampbyte = amp.to_bytes(2, byteorder='little', signed=True) # 地道に1サンプルづつ変換します。
work = b''.join([work, ampbyte])
if i % 10000 == 0: # 一旦ワークに入れるのはそのままjoinしていくと指数的に遅くなるからです。
if i % 100000 == 0:
print('stream:%d' % (i)) # 遅いので、経過をレポートします。
out_stream = b''.join([out_stream, work])
work = b''
out_stream = b''.join([out_stream, work]) # 最後の残りをフラッシュします。
print('stream:%d' % (i))
print('*** output Now ***')
st.write_soundData(file_name, out_stream) # ファイルに書き出します(形式は48KHzwavです)
print('DecodeFile = ' + file_name)

# クリック音の追加
def add_click(z, ave_rythm):
# 本来は全周波数みるべきだが、実験簡素化のため、440Hz から 880Hzでスタートタイミング候補を導出する
target = z[4]
start = 0
for i in range(len(target)):
s_level = sum(target[i])
if s_level > 0.5 * 10**8:
start = i
break
beat_sample_count = BASE_SAMPLING / (ave_rythm / 60)
for i in range(len(target)):
offset = beat_sample_count - int((start - 1) * 2**9 / beat_sample_count)
if i >= start:
b_sample = int(((i - 1) * 2**9 - offset) / beat_sample_count)
n_sample = int((i * 2**9 - offset) / beat_sample_count)
if n_sample > b_sample:
target[i][0] = 0.6 * 10 ** 9
return z

# 信号ピークを中央に寄せる(クオンタイズ相当)
def peak_shift(z, c_size, offset, qt_point, qt_width):
wk = z[qt_point-qt_width:qt_point+qt_width + 1]
comp_list = []
for i in range(len(wk)):
max_val = np.max(wk[i])
comp_list.append(max_val)
total_max_val = max(comp_list)
max_index = comp_list.index(total_max_val)
r_wk = wk
if max_index == 0 or max_index == 2*qt_width: # 明確なPeakといえないためシフトしない
pass
else: # Peakをセンターにシフトする
for i in range(qt_width * 2):
offset = max_index - qt_width
xi = i + offset
if xi < 0:
r_wk[i] = wk[0]
elif xi > 2 * qt_width:
r_wk[i] = wk[2*qt_width]
else:
r_wk[i] = wk[xi]
return r_wk

# 強制クオンタイズ
def force_quantize(z, ave_rythm):
# 本来は全周波数みるべきだが、実験簡素化のため、440Hz から 880Hzでスタートタイミング候補を導出する
target = sum_sound_atack(z[3])
start = 0
for i in range(len(target)):
s_level = sum(target[i])
if s_level > 0.5 * 10**8:
start = i
break
beat_sample_count = BASE_SAMPLING / (ave_rythm / 60)
offset = (start * 2 ** 9) % beat_sample_count
zf = z.copy()
for r in range (3, len(z)):
c_size = 2 ** (12 - r)
for i in range(len(target)):
qt_width = int(beat_sample_count / (c_size * 8)) + 1
if i > start * 2**(r-3):
b_sample = int(((i - 1) * c_size - offset) / (beat_sample_count * 4))
n_sample = int((i * c_size - offset) / (beat_sample_count * 4))
if n_sample > b_sample:
ps = peak_shift(z[r], c_size, offset, i, qt_width)
zf[r][i-qt_width:i+qt_width] = ps
pass
return zf

if __name__ == '__main__':
print('--------------------------------------------')
print('Advent Calender 2021 17th Day Experiment:luriAPupileOfKoo & Memol')
print('--------------------------------------------')
print(' / ̄ ̄ ̄¥  ')
print(' H*Azure| ハズレ(Hyper瑠璃) Encoder V1.0 ')
print(' ¥___/ 2021 luriAPupileOfKoo ')
print('--------------------------------------------')
print(' _( )_ ')
print(' |□□| コレジャナイ Encoder V1.0 ')
print(' | - | 2017 Koo Wells')
print('--------------------------------------------')
st = soundtool()
w_channel, w_rate, w_framenumber, w_frame = st.fetch_soundData(IN_WAVE_FILE)
print('*** Start ***')
print('SoundFile 1 = ' + IN_WAVE_FILE)
data48KMono = st.conv48KMonoArray(w_frame, w_channel, w_rate) # 48KHz モノラルの配列にする
k = korejanai() # コレジャナイエンコーダオブジェクトの取得
z = k.encode(data48KMono) # エンコードして活性化値を求める
exp_k = broad_array(z) # 周波数によって粒度が異なるため、平滑化する(説明グラフ用)
fig = plt.figure() # グラフ化
sb.heatmap(exp_k) # ヒートマップ
plt.ylim(24, exp_k.shape[0]) # 縦軸並びの調整
plt.show() # 一旦ここで止まる(グラフを閉じると続行)-----------------------
count = len(data48KMono) # データ長を求めておく
croma = conv_croma(z) # 12音の時系列強度の配列にしてみます。
h = hazure(k) # ハズレ(Hyper瑠璃)エンコーダオブジェクトの取得
zz = h.r_encode(np.array(croma)) # リズムのエンコード
zz_heat = h.broad_array(zz) # リズムによって(リズムの)サンプリング粒度が異なるため、平滑化する
fig = plt.figure() # グラフ化
sb.heatmap(zz_heat.T, cmap='ocean') # ヒートマップ
plt.ylim(0, zz_heat.shape[1]) # 縦軸並びの調整
plt.show() # 一旦ここで止まる(グラフを閉じると続行)-----------------------
est_rtm, r_list = report_rythm(zz) # リズムの平均を無理やり求めます(妥当性は無視)
addClick_z = add_click(copy.deepcopy(z), est_rtm) # 平均リズムでクリックを付加する
x_hat = k.decode(addClick_z, count) # 音をデコードする
output_wave_file(x_hat, DECODE_FILE_1) # WAVEファイルに出力する

f_quantize_z = force_quantize(copy.deepcopy(z), est_rtm) # 強制クオンタイズする
x_hat = k.decode(f_quantize_z, count) # 音をデコードする
output_wave_file(x_hat, DECODE_FILE_2) # WAVEファイルに出力する

print('*** Complete ! ***')

DiscordはLuriです

*1:seaborn/heatmapのカラーバーの位置調整は超絶難しいのでタイミングがわかりやすいように「切り貼り」して表示位置をかえています

*2:12/19補足:実際には、この図形をMIDIに変換しても半音~1音ずれていることが多々あります。ウェーブレット+異なる位相の連続で、実際の周波数と2~3番目に近いウェーブレットに最大エンコード値がくるケースは多分にあるため、このままでは正しく音符にできないケースがほとんどです。楽譜/MDI情報にするためには別の工夫が必要です。

*3:ウェーブレットの周波数は今回12音階/オクターブです。このように周波数が離散したウェーブレット群であっても、離散度合いが広すぎなければ、すべての周波数を連続的に解析(エンコード)、再現(デコード)できます。

*4:リズム検出はフィルタリングしてからそのまま音圧検出するものが多く、この周波数/音階にわけて検出する方法は(見かけないので)新規性があるかもしれません。そういえばウェーブレット解析に(Reluなどの)活性化関数を使うものも他では見かけません。活性化関数を使用するとフィルタリング性能が劇的に向上します。これは深層学習の「層」または自己符号化器と同じ理屈です。

*5:ウェーブレット符号化器は学習こそないものの、基本的しくみ・機能として「学習済な1層」とほぼ等価です。「ウェーブレット」そのものが(学習済の)自己符号化器の「重み」とみることもできます。

*6:自動作曲は、現代音楽的な理詰めアプローチではなく、「気持ちよさ・ここちよさ」を極限まで追求していく直観的・感性的なアプローチが好みです。:その上で感性の数値化/マッピングは必須です!

*7:創成の場合はこの「人・機械学習ループ」の中に人の好み・感性や判断が必須である感覚があります。「エンジニアリング」や理論からは逸脱しますが、プログラムや付随するデータもartということで。。。(苦しい)