クーの自由研究

マスターの かえるのクーは8年の任期を終え消失しました。クーⅡ世の中の人絶賛募集中です。(特にオリオン座&プレイアデス方向の方は優遇します)助手がメイド喫茶から生還し、リハビリを兼ねて復帰しています。

アルゴリズム作曲の紹介

流行りの逆へいくのがこのブログの流儀ですが

昨年末Advent Calender へ参加するという蛮行をしてしまった、かえるのクーの助手の弟子の「井戸中 」(いとなか )です。

f:id:luriAPupileOfKoo:20220110003048p:plainf:id:luriAPupileOfKoo:20220110003518p:plain

師匠(セイ)が「ネタがない!」とのことなので、何かつなぎに書きます。

サウンド方面は「音声変換」や「音声合成」がとても人気があるみたい

参加したAdvent Calender (創作+機械学習)の主催をされた方々のdiscordコミュニティを覗いてみました。そこでは「音声変換・合成」に興味がある方がとても多いので少し驚きました。このブログのISSUEにも「音声変換」はありますが、かなり優先度は低いです。流行りには乗りませんが、これだけ多くの方々がいろいろやっておられるのはそれだけ面白いのだと思います。

zoomやいろいろなコミュニティに参加すると、画像や実音声でやりとりことがしばしばあります。画像はアバター映像でどうにでもなりますが、音声のほうが「生音声」なのは恥ずかしいものがあります。

音声モーフィングや音声学習まではいきませんが、昨年だれかがやっていた、「音声エフェクト」系の延長で音声の改変をしてみてもおもしろいかもしれないと思いました。

さてここからが本題です。

アルゴリズム作曲とは

機械学習とはまったく別系統ですが、古くから「一定のルールに従い作曲を行う」試みは行われてきました。

古くから有名な方法として、以下がよく紹介されます。

「モーツアルトのダイスメヌエット」として紹介されることもある「Musical Game K.516f」です。

ある程度まとまったものを準備しておき、サイコロを振って音列作成をしていく方法です。

また、コンピュータが真空管の時代から「コンピュータに作曲させる」ことに情熱を燃やす人々は多くいたようです。有名なのが「イリアック組曲(1959)」です。

お世辞にも「いい曲」とはいえませんが、それなりな気はします。

古い時代での有名なアルゴリズム作曲系の音楽家に「ヤニス・クセナキス」先生がいらっしゃいます。ペンタブレットがはじめてできた時代ですが、それで絵を書くように作曲する方法が有名です。数学的な様々な規則性を取り入れた作曲法を試行しました。

ジョン・ケージ先生も有名ですが、アルゴリズム的な視点からはクセナキス先生の方が、より数学的手法である印象があります。以降はいろいろな先生がいろいろな理解困難な手法でアルゴリズム作曲に挑まれております。

説明にまったくなっていませんので、Wikipediaを参照ください。

作例

さて、Advent Calendar では苦し紛れに(本当はこんなのがやりたかったという例で)アルゴリズム作曲の実例を貼りました。以下の感じです。

 

この例での音列生成アルゴリズムは以下のようなものです。

(1)任意の音程と周期を人が選び(コーディング)、その周期毎に音を再生する。

(2)音は最大2音までとし、同時に音が発生される場合は下の音を優先する。

(3)音が発生しない場合は次の処理に進んで音が選択されるまで繰り返す(無音は発生させない)

(4)所定の回数処理したら、全体の音程をシフトさせる(ずっと同じだとつまらなかったため)

(5)音程がとても高くなれば、低い音程にシフトさせる

(6)所定の回数処理する(音を鳴らす)と終了させます。終了は音が急に途切れるのが気持ちわるかったので、使用した音を複数回ならして終了しています。

その他

・音は左と右の2音とする。1音だけ発生する場合は右と左で同じ音を出す。

・素数周期で音を出す要素をいれると、多様性が増す。

・周期が短いものが多い場合は、単純な音列になりがち。

 

プログラム的には「アルゴリズム」よりも、元にしているのが単音1音のWaveファイルであり、プログラムでリサンプリングして「サンプラー」的な機能で音階をつくっているところがポイントです。(すべて元の1音波形を伸縮して音を出しています。)

元の1音はこんな感じです。

 

今年もネタ不足に悩みそうですが、来月にはみんな急に暇になる予定(クビになるわけではないようです。)のようなので徐々に何か書くことでしょう。

ではまた!

新サムライマック燻製風マヨソースは「絶品」というかこれは「発明」です!

ソースはこんな感じです。ほぼサンプラープログラム部です。

リアルタイムで音を鳴らすところがミソです。


#! /usr/bin/python
# -*- coding: utf-8 -*-
import time
import pyaudio
import wave

# Input / Output
CHUNK = 1024
FORMAT = pyaudio.paInt16
CHANNELS = 2
RATE = 48000
MAX_SIG = 32000

# *** Envelope Control ***
ATTACK = 0 #SAMPLE TIME
DECAY = 0 #SAMPLE TIME
SUSTAIN_LEVEL = 1.0 #LEVEL
SUSTAIN_TIME = 5000 #SAMPLE TIME
RELEASE = 3000 #SAMPLE TIME
DECAY_POINT = ATTACK + DECAY
SUSTAIN_POINT = ATTACK + DECAY + SUSTAIN_TIME
END_POINT = ATTACK + DECAY + SUSTAIN_TIME + RELEASE

# *** Sampler ***
BASE_SAMPLE_FREQ = 440.0
TARGET_SAMPLE_FREQ = []
TARGET_TONE = [24, 40, 31, 46, 48, 52, 38, 41]
TARGET_LOOP_TIMING = [4, 5, 6, 9, 7, 17, 29, 37]
MASTER_VOL = 1.0
MODULATE = 160

def create_one_sample(a_sample_no, a_index, a_sample_list_left, a_sample_list_right,
base_sample_freq, target_l_sample_freq, target_r_sample_freq):
now_sample_no = a_index * CHUNK + a_sample_no
if now_sample_no < ATTACK: # --- ATTACK ---
vol = 1.0 - (ATTACK - now_sample_no) / ATTACK
elif now_sample_no < DECAY_POINT: # --- DECAY ---
vol = 1.0 - (1.0 - SUSTAIN_LEVEL) * (now_sample_no - ATTACK) / DECAY
elif now_sample_no < SUSTAIN_POINT: # --- SUSTAIN ---
vol = SUSTAIN_LEVEL
elif now_sample_no <= END_POINT: # --- RELEASE ---
vol = SUSTAIN_LEVEL * (END_POINT - now_sample_no) / RELEASE
else:
vol = 0.0
if vol < 0:
vol = 0.0 #誤差のための安全装置
elif vol > 1.0:
vol = 1.0 #誤差のための安全装置

#ReSampling Wave
target_sample_seq = 1.0 * a_index * CHUNK + a_sample_no
ref_l_sample_seq = target_sample_seq * target_l_sample_freq / base_sample_freq
ref_l_sample_seq_int = int(ref_l_sample_seq)
ref_l_sample_seq_adj = ref_l_sample_seq - ref_l_sample_seq_int

sample_left_1s = a_sample_list_left[ref_l_sample_seq_int]
sample_left_1e = a_sample_list_left[ref_l_sample_seq_int + 1]

sample_left = 1.0 * sample_left_1s + 1.0 * (sample_left_1e - sample_left_1s) * ref_l_sample_seq_adj
float_data_l = sample_left * vol * MASTER_VOL
byte_data_l = int(float_data_l).to_bytes(2, 'little', signed=True)

ref_r_sample_seq = target_sample_seq * target_r_sample_freq / base_sample_freq
ref_r_sample_seq_int = int(ref_r_sample_seq)
ref_r_sample_seq_adj = ref_r_sample_seq - ref_r_sample_seq_int

sample_right_1s = a_sample_list_right[ref_r_sample_seq_int]
sample_right_1e = a_sample_list_right[ref_r_sample_seq_int + 1]

sample_right = 1.0 * sample_right_1s + 1.0 * (sample_right_1e - sample_right_1s) * ref_r_sample_seq_adj
float_data_r = sample_right * vol * MASTER_VOL
byte_data_r = int(float_data_r).to_bytes(2, 'little', signed=True)

return byte_data_l + byte_data_r

def create_data(a_chunk, a_index, a_sample_list_left, a_sample_list_right, l_freq, r_frq):
rtn_sample = b''
for sample_no in range(a_chunk):
one_sample = create_one_sample(sample_no, a_index, a_sample_list_left,
a_sample_list_right, BASE_SAMPLE_FREQ, l_freq, r_frq)
rtn_sample += one_sample
return rtn_sample

def play(a_pa, a_sample_list_left, a_sample_list_right):
global TARGET_TONE
stream = a_pa.open(format=FORMAT,
channels=CHANNELS,
rate=RATE,
output=True)
for note in range(MODULATE * 8):
l_flg , r_flg = False, False
l_frq, r_frq = 0, 0
if (note % MODULATE) == 0:
TARGET_SAMPLE_FREQ = []
for ii in range(len(TARGET_TONE)):
TARGET_SAMPLE_FREQ.append(440 * 2**((TARGET_TONE[ii] - 45)/12))
TARGET_TONE[ii] += 2
if note == (MODULATE * 2):
for ii in range(len(TARGET_TONE)):
TARGET_SAMPLE_FREQ.append(440 * 2**((TARGET_TONE[ii] - 45)/12))
TARGET_TONE[ii] -= 12
for i in range(len(TARGET_LOOP_TIMING)):
if note % TARGET_LOOP_TIMING[i] == 0 and l_flg == False:
l_frq = TARGET_SAMPLE_FREQ[i]
l_flg = True
elif note % TARGET_LOOP_TIMING[i] == 0 and r_flg == False:
r_frq = TARGET_SAMPLE_FREQ[i]
r_flg = True
break
if l_flg == False:
continue
if l_flg == True and r_flg == False:
r_frq = l_frq
for index in range(int(END_POINT/CHUNK) ):
wave_data = create_data(CHUNK, index, a_sample_list_left, a_sample_list_right, l_frq, r_frq)
stream.write(wave_data)
for note in range(len(TARGET_SAMPLE_FREQ)-1, -1, -1):
l_frq = TARGET_SAMPLE_FREQ[note]
r_frq = TARGET_SAMPLE_FREQ[note]
for i in range(4):
for index in range(int(END_POINT/CHUNK) ):
wave_data = create_data(CHUNK, index, a_sample_list_left, a_sample_list_right, l_frq, r_frq)
stream.write(wave_data)
for i in range(12):
for index in range(int(END_POINT / CHUNK)):
wave_data = create_data(CHUNK, index, a_sample_list_left, a_sample_list_right, l_frq, r_frq)
stream.write(wave_data)
stream.stop_stream()
stream.close()

if __name__ == '__main__':
pa = pyaudio.PyAudio()
sample = wave.open('EP45.wav', 'rb')
sample_list_left, sample_list_right = [], []
for i in range(sample.getnframes()):
sound_frame = sample.readframes(1) #2チャンネルなら左右分の情報が一度に取得される!!!
sample_list_left.append(int.from_bytes(sound_frame[0:2], 'little', signed=True))
sample_list_right.append(int.from_bytes(sound_frame[2:4], 'little', signed=True))
play(pa, sample_list_left, sample_list_right)
time.sleep(1)
pa.terminate()