クーの自由研究

大きな海は知らないけれど、空(宇宙)の深層を学習します。

計算機音楽の自由研究(準備:その2.8)~機械学習用サウンドデータベースをつくる(結合編)

はじめに

今年は春のけだるさ病*1にはかからなかったようです。熱中できることを持つことは素晴らしいと感じました。

(準備:その2.6)で、一連の音の作成(たとえばあるVSTi音源のMIDIノート21~108の音)までは、設定して実行すれば簡単に行えるようになりました。

f:id:np2LKoo:20170519231630p:plain

パソコンの処理能力が低いので音を出すと(録音すると)よくノイズが乗ることがあります。外部ノイズではなく、明らかにCPU側能力不足のノイズです。そのため、もっと多数の音を一度に作成することはぜず、一旦作成単位のwavファイル(30音~100音くらい)ごとに、ノイズが乗っていないかチェックします。そのあとで、まとめて1つのDBに結合することを考えます。(6月末まで反省キャンペーン中です。内容を順次リファインします。)

機械学習用ファイル作成プログラムの要件

・複数の所定のフォルダに格納されたwavファイルを結合する。
・同じ条件(サンプリングレートやビット数など)の録音でないとチェックしてエラーとする。
所定のフォルダは数字5桁(00001,00002...)などに決め、その中にフォルダと同名のwavファイル(名前は任意、拡張子は.wav)を置く。(わざわざ00001などのフォルダをつくるのは、その中に複数の候補や作成補助用の(DBには直接関係しない)ファイルをもたせたいからです。)

以下の実装はまた今度しますが、定義だけしておきます。
・準備:その2.7で確認したタグのフォーマットを参考にコメント(ICMT)欄に音(0.68秒のサンプル単位)に関する基本情報を書く。(内容は別途きめる)
・ソース(ISRC)欄に音に関する情報を書く(音源の単位)
・DBに関するサマリ情報をプロダクト(アルバム)欄(IPRD)に書く
・いろいろな情報を乗せても、wavファイル単独で音が聞けるようにする。
・データベースが大きくなると、音の頭出しが難しくなるので、情報を指定すると特定の音を簡単に再生できるしくみを考える。(これは結合用プログラムとは別に考える)

結合用のプログラムを作成してみます。

準備:その2.7のプログラムを元にもうすこし汎用化して結合機能をもたせてみます。

汎用化すると逆に「読みにくい」ソースとなってしまいました。*2小出しで本当に申し訳ないですが、今回は結合する部分だけです。作成中ながら貼っちゃいます。結合する部分と出力する部分だけならあわせて10~20行程度なのですが、内容確認用の情報表示を汎用的にできるように作成したので長くなってしまいました。Python標準的な命名規約には全く従っていません。わかりやすいソースが書けるように勉強中です。

waveファイルのフォーマットについては前にも貼りましたが、こちらを参照してください。実験用でテストも十分行っていないので、間違いがあっらこっそり張り替えます。


import numpy as np
import os.path
import re

STRCODE = 'utf-8'
'''
=====================================================================================
Waveファイルに関するフォーマット情報を地道に定義してみます。
とりあえず、今回はwavファイルを結合するところまでやります。(クラスは未完成です)
=====================================================================================
'''

class VWave:
# 読み込んだwaveファイルの情報を格納するクラスです。
SAMPLING_COUNT = 32768
# 地道にwavフォーマット情報を定義していきます。
IFF_ALL = 0
IFF_ID, IFF_SIZE, IFF_FMT, IFF_SUBID, IFF_SUBSIZE = 1, 2, 3, 4, 5
IFF_WAVFOMT, IFF_CHANNEL, IFF_SAMPLING, IFF_BYTEPS, IFF_BLKSIZE = 6, 7, 8, 9, 10
IFF_BIT, IFF_DATAID, IFF_DATASIZE, IFF_DATA, IFF_LIST = 11, 12, 13, 14, 15
IFF_LISTID, IFF_LISTSIZE, IFF_INFOID = 16, 17, 18
TYPE_BYTE = "byte"
TYPE_STR = "string"
TYPE_LITTLE = "little"
DEF_METAID, DEF_TITLE, DEF_TYPE, DEF_START, DEF_BYTE, DEF_ENC = 0, 1, 2, 3, 4, 5
IFF_DEF = [
[IFF_ALL, "ALL DATA ", TYPE_BYTE, None, None, None ],
[IFF_ID, "ID ", TYPE_STR, 0, 4, STRCODE],
[IFF_SIZE, "Chunk Size", TYPE_LITTLE, 4, 4, None],
[IFF_FMT, "FormatName", TYPE_STR, 8, 4, STRCODE],
[IFF_SUBID, "Sub ID ", TYPE_STR, 12, 4, STRCODE],
[IFF_SUBSIZE, "Sub Size ", TYPE_LITTLE, 16, 4, None],
[IFF_WAVFOMT, "WavFomat ", TYPE_LITTLE, 20, 2, None],
[IFF_CHANNEL, "Channel ", TYPE_LITTLE, 22, 2, None],
[IFF_SAMPLING,"Sampling ", TYPE_LITTLE, 24, 4, None],
[IFF_BYTEPS, "BytePS ", TYPE_LITTLE, 28, 4, None],
[IFF_BLKSIZE, "BlockSize ", TYPE_LITTLE, 32, 2, None],
[IFF_BIT, "Bit ", TYPE_LITTLE, 34, 2, None],
[IFF_DATAID, "DataID ", TYPE_STR, 36, 4, STRCODE],
[IFF_DATASIZE,"DataSize ", TYPE_LITTLE, 40, 4, None],
[IFF_DATA, "IFF Data ", TYPE_BYTE, 44, None, None],
[IFF_LIST, "List ", TYPE_BYTE, 0, None, None ],
[IFF_LISTID, "List ID ", TYPE_STR, 0, 4, STRCODE ],
[IFF_LISTSIZE,"List Size ", TYPE_LITTLE, 4, 4, None],
[IFF_INFOID, "INFO ID ", TYPE_STR, 8, 4, STRCODE]
]
#読み込んだ情報を格納する配列です。最初はクラス変数を使っていましたが、多すぎてわかりにくくなったため、
#すべて配列で持つことにしました。
iff = []
iffSubChunkID = []
iffSubChunkSize = []
iffSubChunkData = []

def __init__(self, data):
self.iff =[b'', "", 0, "", "", 0, 0, 0, 0, 0, 0, 0, "", 0, b'', b'', "", 0, ""]
siff = self.iff
sALL = self.IFF_ALL
dMetaID, dTitle, dType, dStart, dByte, dEnc = self.DEF_METAID, self.DEF_TITLE, self.DEF_TYPE, self.DEF_START, self.DEF_BYTE, self.DEF_ENC
tByte, tStr, tLittle = self.TYPE_BYTE, self.TYPE_STR, self.TYPE_LITTLE
# 以降全体から特定部分へアクセスするためにバイト配列にして格納します。
siff[sALL] = np.frombuffer(data, dtype=self.IFF_DEF[sALL][dType])
#順次定義の内容に基づき、data内容を解釈して配列に格納します。
for i in range(14)[1:]:
iDEF = self.IFF_DEF[i]
if iDEF[dType] == tStr:
siff[i] = (b''.join(siff[sALL][iDEF[dStart]:iDEF[dStart] + iDEF[dByte]])).decode(iDEF[dEnc])
elif iDEF[dType] == tLittle:
siff[i] = int.from_bytes(siff[sALL][iDEF[dStart]:iDEF[dStart] + iDEF[dByte]], tLittle)

#Waveデータ部分を格納します。この部分が可変長なんです。
iDEF = self.IFF_DEF[self.IFF_DATA]
siff[self.IFF_DATA] = b''.join(siff[sALL][iDEF[dStart]:iDEF[dStart] + siff[self.IFF_DATASIZE]])
#タグの部分はIFF_LISTで示す配列に格納します。
siff[self.IFF_LIST] = siff[sALL][44 + siff[self.IFF_DATASIZE]:]

#タグの部分の範囲を解釈して配列「XXXiffSubChunk」に格納します。
if (siff[self.IFF_LIST].size != 0):
sList = siff[self.IFF_LIST]
iDEF = self.IFF_DEF[self.IFF_LISTID]
siff[self.IFF_LISTID] = (b''.join(sList[iDEF[dStart]:iDEF[dStart] + iDEF[dByte]])).decode(iDEF[dEnc])
iDEF = self.IFF_DEF[self.IFF_LISTSIZE]
siff[self.IFF_LISTSIZE] = int.from_bytes(sList[iDEF[dStart]:iDEF[dStart] + iDEF[dByte]], tLittle)
iDEF = self.IFF_DEF[self.IFF_INFOID]
siff[self.IFF_INFOID] = (b''.join(sList[iDEF[dStart]:iDEF[dStart] + iDEF[dByte]])).decode(iDEF[dEnc])
offset = 12
for i in range(999):
chunkID = (b''.join(sList[offset:offset + 4])).decode(STRCODE)
subChunkSize = int.from_bytes((b''.join(sList[offset + 4:offset + 8])), tLittle)
chunkData = (b''.join(sList[offset + 8:offset + 8 + subChunkSize]).decode('SJIS'))
offset += (8 + subChunkSize + (subChunkSize % 2))
self.iffSubChunkID.append(chunkID)
self.iffSubChunkSize.append(subChunkSize)
self.iffSubChunkData.append(chunkData)
if siff[self.IFF_LISTSIZE] <= offset + 12:
break

def print(self):
#読み込んだwavファイルの内容をレポートします。
siff = self.iff
sALL = self.IFF_ALL
dMetaID, dTitle, dType, dStart, dByte, dEnc = self.DEF_METAID, self.DEF_TITLE, self.DEF_TYPE, self.DEF_START, self.DEF_BYTE, self.DEF_ENC
tByte, tStr, tLittle = self.TYPE_BYTE, self.TYPE_STR, self.TYPE_LITTLE
print('Toral size = ' + "{0:,d}".format(siff[sALL].size))
for i in range(19)[1:]:
if (i == 14):
if (siff[self.IFF_LIST].size != 0):
print("-------------------------------------------")
continue
else:
#このブロックは調整中です
break
iDEF = self.IFF_DEF[i]
if iDEF[dType] == tStr:
print(iDEF[dTitle] + '=' + siff[i])
elif iDEF[dType] == tLittle:
print(iDEF[dTitle] + '=' + "{0:,d}".format(siff[i]))
print("<<This wave file contains [ " + "{0:,d}".format(siff[self.IFF_DATASIZE] // (self.SAMPLING_COUNT * siff[self.IFF_BLKSIZE])) + "] sound>>")
print("-------------------------------------------")

def printSize(self):
siff = self.iff
print("Chunk Size=" + siff[self.IFF_SIZE])

def getList(self):
return self.iffSubChunkID, self.iffSubChunkSize, self.iffSubChunkData

def cat(self, data):
#結合用のメソッドです。元のソースとチャンネル数、サンプリングレート、ビット数が異なればエラーにします。
#エラーがあれば結合しません。
if (self.iff[self.IFF_CHANNEL] != data.iff[data.IFF_CHANNEL]):
print("Channel is different base=" + "0:d".format(self.iff[self.IFF_CHANNEL]) +
"source=" + "0:d".format(data.iff[self.IFF_CHANNEL]))
return -1
if (self.iff[self.IFF_SAMPLING] != data.iff[data.IFF_SAMPLING]):
print("Samplint is different base=" + "0:d".format(self.iff[self.IFF_SAMPLING]) +
":source=" + "0:d".format(data.iff[self.IFF_SAMPLING]))
return -1
if (self.iff[self.IFF_BIT] != data.iff[data.IFF_BIT]):
print("Bit is different base=" + "0:d".format(self.iff[self.IFF_BIT]) +
":source=" + "0:d".format(data.iff[self.IFF_BIT]))
return -1
#結合するサンプリング数がSAMPLING_COUNTの整数倍でないと、エラーとします。
if (data.iff[data.IFF_DATASIZE] % self.SAMPLING_COUNT != 0):
print("Sampling count is not base on :" + self.SAMPLING_COUNT )

#結合は単純に足しているだけです。
self.iff[self.IFF_DATA] += data.iff[data.IFF_DATA]
self.iff[self.IFF_DATASIZE] += data.iff[data.IFF_DATASIZE]
self.iff[self.IFF_SIZE] += data.iff[data.IFF_DATASIZE]


def getWave(self):
#Waveデータのフォーマットでバイナリで取り出すメソッドです。
dMetaID, dTitle, dType, dStart, dByte, dEnc = self.DEF_METAID, self.DEF_TITLE, self.DEF_TYPE, self.DEF_START, self.DEF_BYTE, self.DEF_ENC
work = b''
for i in range(14):
siffData = self.iff[i]
iDEF = self.IFF_DEF[i]
if (iDEF[dType] == self.TYPE_STR):
work = work + siffData.encode(iDEF[dEnc])
elif (iDEF[dType] == self.TYPE_LITTLE and iDEF[dByte] == 2 ):
work = work + siffData.to_bytes(2, 'little')
elif (iDEF[dType] == self.TYPE_LITTLE and iDEF[dByte] == 4):
work = work + siffData.to_bytes(4, 'little')
work = work + self.iff[self.IFF_DATA]
return work

#出力するDBの名前です。
CAT_WAVE_FILE='KooSoundDB.wav'
sCount = 0
#カレントディレクトリで、"00001"から"99999"(5桁)のフォルダがあれば名前を取得してソートします
output_path = "."
files = os.listdir(output_path)
list_dir = [f for f in files if os.path.isdir(os.path.join(output_path, f))]
sound_dir = [i for i in list_dir if re.search(r'[0-9][0-9][0-9][0-9][0-9]', i)]
sound_dir.sort()
print(sound_dir)
#フォルダ名と同じ番号のwavファイルがあるか調べます。
for dir_name in sound_dir:
fileName = dir_name + "\\" + dir_name + ".wav"
if os.path.isfile(fileName):
sCount += 1
print ("exist:" + fileName)
f = open(fileName, 'rb')
aData = f.read()
waveData = VWave(aData)
if (sCount == 1):
# 1個目は元データそのものです。
catWave = waveData
else:
# 2個目以降は結合していきます。
catWave.cat(waveData)
waveData.print()
#タグリストはget/setするので確認用です。setはまだ作ってません。
subChunkID, subChunkSize, subChunkData = waveData.getList()
for i in range(len(subChunkID)):
print(">" + subChunkID[i] + ":{0:4d}:".format(subChunkSize[i]) + subChunkData[i])
f.close()
print("===========================================")
print("Output file name:" + CAT_WAVE_FILE)
print("===========================================")
catWave.print()
#出力用のファイルをオープンし、結合演算済の内容をwavファイルに出力します。
fw = open(CAT_WAVE_FILE, 'wb')
fw.write(catWave.getWave())
fw.close()

wavフォーマットの定義を配列(リスト)で行いました。本当はPythonstruct(バイナリ構造の定義、解釈)を使いたかったのですが、可変長のbyteデータの扱いがうまくできなかったので、このようになりました。
RIFF形式のファイルであれば少し改造するだけでなんでも読めるようになるはずです。(作ったボクでもわかりにくいソースになっており反省です)

実行結果例です。(テスト用)

['02001', '02002', '04001', '07001']
exist:02001\02001.wav
Toral size = 5,767,212
ID        =RIFF
Chunk Size=5,767,204
FormatName=WAVE
Sub ID    =fmt 
Sub Size  =16
WavFomat  =1
Channel   =1
Sampling  =48,000
BytePS    =96,000
BlockSize =2
Bit       =16
DataID    =data
DataSize  =5,767,168
<<This wave file contains [ 88] sound>>
-------------------------------------------
exist:02002\02002.wav
...
===========================================
Output file name:KooSoundDB.wav
===========================================
Toral size = 5,767,212
ID        =RIFF
Chunk Size=14,614,564
FormatName=WAVE
Sub ID    =fmt 
Sub Size  =16
WavFomat  =1
Channel   =1
Sampling  =48,000
BytePS    =96,000
BlockSize =2
Bit       =16
DataID    =data
DataSize  =14,614,528
<<This wave file contains [ 223] sound>>
-------------------------------------------

あとは、機械学習用のタグ情報を付加させる機能をつければ、機械学習用サウンドDB作成プログラムはほぼ完成です。

お元気で(2.7)!こんにちは(3.6)!!

 余談ですが、世の中の流れに従って完全にPython 3.xにしています。昨年はじめたときは2.xでしか動かない太古のソースをたくさん参照していたので、おのずと2.xを多用していたのですが、今年は完全に3.xベースにします。今年作成のソースは2.xでは動作しません*3のであしからず。

f:id:np2LKoo:20170520124131p:plain

2.7おくえんのパガーニ ウアイラロードスター

f:id:np2LKoo:20170520123120p:plain

おいしい3.6牛乳

次回は、この「機械学習用サウンドDB」(だけどwavファイルそのもの)に、機械学習に必要なタグ情報を付加する方法を考えます。

プログラムが完成すれば、あとは「ねこ音あつめ」です!めざせ!10万音!!

 

プロモーションキャンペーン終了のお知らせ

プロモーション空しく全く再生されません。2週間くらいカウントアップなしです。15HITからいっこうにアップしません!そこで、プロモーションは今月末までの期間限定となりました。(ジョーンズ調査員より:地球人は「限定」に弱い。)

f:id:np2LKoo:20170601000756p:plain

月末までなら再生すると「小さな幸せ音ずれ訪れる」という特典付きです。(秘密結社「柔らか銀行」マーケット報告書より:日本人は「お得」と「特典」に弱い。

地域限定性をさりげなくアピールしSNSをよいスパイラルで巻き込むと相当の効果がある(大正製菓販売戦略部:(社外秘)一発勝負の列島分断飢餓作戦資料より)
よろしくお願いいたします!めざせ20HIT!!!

→カール便乗キャンペーンはあと1ポ及ばず、不成功におわりました。(19Hitでした)

ご視聴いただきありがとうございました。

*1:蛙界に存在する、生きることの意義に疑問を感じる青春期/思春期特有の病気。「蛙はなぜ生きるのか」をつきつめ、崇高な自己実現目的意識と自己潜在能力とのギャップに失望し、なにもヤルキが起こらなくなる、モチベーション低下スパイラルパターン。失望が大きいと食欲もなくなり死に至る病でもある。:おたまじゃくしから蛙になった年と翌年(感受性の強い蛙は毎年春期に)かかりやすいといわれている。

*2:余談ですが、クラスを使ったので「self.」がやまほどでてきました。あまりに多いところは、一旦別の変数におきかえることをしています。「self.」を1文字であらわす略記方法を採用してくれないかなぁ。。。1行の半分がself.である行が何行も続くと誰でもうんざりすると思います。慣れれば気にならないのかもしれませんが、初心者には辛いです。

*3:「今時、『機械学習するならPython2.xだよ!』なんて言っていると小学生に笑われる。」と、どなたかが書いていましたが、ドキッとしました