プログラミング (Python) で作曲してみた

# はじめに

本日（3月31日）は、バッハの誕生日！
ということで、Megenta の Polyphony RNN を使えば、バッハ風の曲を誰でもつくることができます。なので、Pythonでヴァンパイアという曲を学習させて、クラシック調（バッハ風）のオリジナル曲に挑戦してみました。

# プログラム

Google Colab で触れるようにしていますので、良ければご活用ください
https://colab.research.google.com/github/TakayukiNJ/vanpireBach/blob/main/vanpireBach.ipynb

YouTubeでの解説：https://youtu.be/bnilVY-H5xE
※ 2021年4月1日18時に公開

## GANSynth で音色を変えてみる 

まずは使用するライブラリのインストール

```python
!apt-get update -qq && apt-get install -qq libfluidsynth1 fluid-soundfont-gm build-essential libasound2-dev libjack-dev
!pip install -qU pyfluidsynth pretty_midi
!pip install -qU magenta
```

次に、ライブラリの導入のプログラム
```python
import numpy as np
import matplotlib.pyplot as plt
from google.colab import files
import tensorflow.compat.v1 as tf
import librosa
import magenta.music as mm
from magenta.models.gansynth.lib import flags as lib_flags
from magenta.models.gansynth.lib import generate_util as gu
from magenta.models.gansynth.lib import model as lib_model
from magenta.models.gansynth.lib import util
from note_seq.notebook_utils import colab_play as play
import note_seq
```

次に、曲生成の設定（４つの関数）
```python
BATCH_SIZE = 16  # 一度に扱うデータ数
SR = 16000  # サンプリングレート

def load_midi(midi_path, min_pitch=36, max_pitch=84):
  # 音声を処理する関数
  """Load midi as a notesequence."""
  midi_path = util.expand_path(midi_path)
  ns = note_seq.midi_file_to_sequence_proto(midi_path)
  pitches = np.array([n.pitch for n in ns.notes])
  velocities = np.array([n.velocity for n in ns.notes])
  start_times = np.array([n.start_time for n in ns.notes])
  end_times = np.array([n.end_time for n in ns.notes])
  valid = np.logical_and(pitches >= min_pitch, pitches <= max_pitch)
  notes = {'pitches': pitches[valid],
           'velocities': velocities[valid],
           'start_times': start_times[valid],
           'end_times': end_times[valid]}
  return ns, notes

def get_envelope(t_note_length, t_attack=0.010, t_release=0.3, sr=16000):
  """Create an attack sustain release amplitude envelope."""
  t_note_length = min(t_note_length, 3.0)
  i_attack = int(sr * t_attack)
  i_sustain = int(sr * t_note_length)
  i_release = int(sr * t_release)
  i_tot = i_sustain + i_release  # attack envelope doesn't add to sound length
  envelope = np.ones(i_tot)
  # Linear attack
  envelope[:i_attack] = np.linspace(0.0, 1.0, i_attack)
  # Linear release
  envelope[i_sustain:i_tot] = np.linspace(1.0, 0.0, i_release)
  return envelope

def combine_notes(audio_notes, start_times, end_times, velocities, sr=16000):
  """Combine audio from multiple notes into a single audio clip.

  Args:
    audio_notes: Array of audio [n_notes, audio_samples].
    start_times: Array of note starts in seconds [n_notes].
    end_times: Array of note ends in seconds [n_notes].
    sr: Integer, sample rate.

  Returns:
    audio_clip: Array of combined audio clip [audio_samples]
  """
  n_notes = len(audio_notes)
  clip_length = end_times.max() + 3.0
  audio_clip = np.zeros(int(clip_length) * sr)

  for t_start, t_end, vel, i in zip(start_times, end_times, velocities, range(n_notes)):
    # Generate an amplitude envelope
    t_note_length = t_end - t_start
    envelope = get_envelope(t_note_length)
    length = len(envelope)
    audio_note = audio_notes[i, :length] * envelope
    # Normalize
    audio_note /= audio_note.max()
    audio_note *= (vel / 127.0)
    # Add to clip buffer
    clip_start = int(t_start * sr)
    clip_end = clip_start + length
    audio_clip[clip_start:clip_end] += audio_note

  # Normalize
  audio_clip /= audio_clip.max()
  audio_clip /= 2.0
  return audio_clip

def specplot(audio_clip):
  # Plotting tools
  p_min = np.min(36)
  p_max = np.max(84)
  f_min = librosa.midi_to_hz(p_min)
  f_max = 2 * librosa.midi_to_hz(p_max)
  octaves = int(np.ceil(np.log2(f_max) - np.log2(f_min)))
  bins_per_octave = 36
  n_bins = int(bins_per_octave * octaves)
  C = librosa.cqt(audio_clip, sr=SR, hop_length=2048, fmin=f_min, n_bins=n_bins, bins_per_octave=bins_per_octave)
  power = 10 * np.log10(np.abs(C)**2 + 1e-6)
  plt.matshow(power[::-1, 2:-2], aspect='auto', cmap=plt.cm.magma)
  plt.yticks([])
  plt.xticks([])
```

次に、学習済みモデルを読み込み
```python
tf.disable_v2_behavior()  # tensorflow2で1.xのコードを動かす
tf.reset_default_graph()  # tensorflowのグラフをリセット

model_dir = "gs://magentadata/models/gansynth/acoustic_only"
flags = lib_flags.Flags({
    "batch_size_schedule": [BATCH_SIZE],
    "tfds_data_dir": "gs://tfds-data/datasets",
})
model = lib_model.Model.load_from_path(model_dir, flags)
```

次に、ファイルの読み込み（Google Colab を使用していることを想定）
```python
midi_path = "/content/Vampire_Melo_164BPM.mid"
ns, notes = load_midi(midi_path)

note_seq.plot_sequence(ns)
note_seq.play_sequence(ns, synth=note_seq.fluidsynth) 
```

次に、音色を変更
```python
seconds_per_instrument = 5  # 楽器が切り替わる間隔
z_instruments, t_instruments = gu.get_random_instruments(  # 潜在変数とその時間
    # 潜在変数がランダムにゆっくりと変化
    model,
    notes["end_times"][-1],
    secs_per_instrument=seconds_per_instrument)

z_notes = gu.get_z_notes(notes["start_times"], z_instruments, t_instruments) # 各noteの潜在変数を取得

audio_notes = model.generate_samples_from_z(z_notes, notes["pitches"]) # 各ノートの音声を生成

audio = combine_notes(
    # 1つの音声にまとめる
    audio_notes,
    notes["start_times"],
    notes["end_times"],
    notes["velocities"]
    )

specplot(audio)  # スペクトログラムの表示
play(audio, sample_rate=SR)
```

### 完成サンプル（3分）

<iframe width="560" height="315" src="https://www.youtube.com/embed/cjNFTLG4EUQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

音声をwavデータに変換してダウンロード
```python
file_name = "atashiVanpire.wav"
gu.save_wav(audio, file_name)
files.download(file_name)
```

## Polyphony RNN で作曲してみる 

変数 ns の中身が良い感じに使えそうなのを確認
```python
print(ns)
```

次に、Polyphony RNN を使ってバッハ風の曲を作成
```python
from magenta.models.performance_rnn import performance_sequence_generator
from magenta.models.shared import sequence_generator_bundle

note_seq.notebook_utils.download_bundle("performance_with_dynamics.mag", "/models/")  # Bundle（.magファイル）をダウンロード
bundle = sequence_generator_bundle.read_bundle_file("/models/performance_with_dynamics.mag")  # Bundleの読み込み
generator_map = performance_sequence_generator.get_generator_map()
performance_rnn = generator_map["performance_with_dynamics"](checkpoint=None, bundle=bundle)  # 生成器の設定
performance_rnn.initialize()  # 初期化
```

さいごに、オリジナル曲を作成
```python
from note_seq.protobuf import generator_pb2

total_time = 360 # 曲の長さ（秒）
temperature = 1 # 曲の「ランダム度合い」を決める定数

base_end_time = max(note.end_time for note in ns.notes)  #ベース曲の終了時刻

generator_options = generator_pb2.GeneratorOptions()  # 生成器のオプション
generator_options.args["temperature"].float_value = temperature  # ランダム度合い
generator_options.generate_sections.add(
    start_time=base_end_time,  # 作曲開始時刻
    end_time=total_time)  # 作曲終了時刻

gen_seq = performance_rnn.generate(ns, generator_options) # 曲の生成

note_seq.plot_sequence(gen_seq)  # NoteSequenceの可視化
note_seq.play_sequence(gen_seq, synth=note_seq.fluidsynth)  # NoteSequenceの再生
```

MIDIデータとしてダウンロード
```python
from google.colab import files
note_seq.sequence_proto_to_midi_file(gen_seq, "atashiBach.mid") # MIDIデータに変換し保存
files.download("atashiBach.mid") # ダウンロード
```

### 完成サンプル（3分）

<iframe width="560" height="315" src="https://www.youtube.com/embed/Bzk6Et16KHE" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>


# 参考文献まとめ
◆ Magenta (GoogleのAI) のサンプルコード
https://colab.research.google.com/notebooks/magenta/hello_magenta/hello_magenta.ipynb

◆ GANSynth (音色を変えるAI) https://colab.research.google.com/notebooks/magenta/gansynth/gansynth_demo.ipynb

◆ 音色を変更したサンプル音源
https://youtu.be/cjNFTLG4EUQ

◆ Polyphony RNN (バッハ風の曲を作成できるAI) https://github.com/magenta/magenta/tree/main/magenta/models/polyphony_rnn

◆ 参考にしたGitHub
https://github.com/yukinaga/ai_music

◆ 実際に作曲してみたサンプル音源
https://youtu.be/Bzk6Et16KHE

◆ ヴァンパイアのMIDIファイルのダウンロード先
https://otoiro.co.jp/special/

◆ ヴァンパイア本家様のYouTube
https://www.youtube.com/watch?v=e1xCOsgWG0M