まっそのせかい

やった事のメモとか

Prometheusによるサーバ監視について少し紹介する

この記事はCCS Advent Calendar 2020の7日目の記事です。
昨日の記事: おすすめ家電(?) 2020 - 稲枝の押入れ

お疲れ様です。
社畜の階段を登り続けているCCSOBのまっそうめんです。
労働に追われているせいで大したネタが無く低みの見物をしていたのですが、入れろと煽られたカレンダーが空いていたので参戦しようと思います。
ネタについてはかろうじて書けそうなサーバの監視をテーマにし、Prometheusというツールについて少し紹介しようと思います。
ゲーム制作とかだとこういう話はあまり無いと思うので「そういう概念とかツールがあるんだなぁ~」みたいな気持ちで見ていただければ幸いです。
一応補足ですが、基本的にウェブ関係の監視についての話です。
それしか知らないので…すいません…(無知)
また、紹介メインで設定ファイルなどの項目など細かい説明はあまりしない予定です。
実際に試す場合は公式のチュートリアルや他の記事を参照してみて下さい。

なぜ監視が必要か社畜の刃 無限出社編

上司「弊社の(人)柱になれ、まっそうめん!」
ワイ「ウィッス!(カチャカチャカチャ、ッターン!!!)」
※ フィクションです

唐突ですがみなさん、プログラムを書いてバグらせた経験はありますでしょうか?
ゲーム制作とかでありがちな奴だと配列外参照して強制終了したりとか、ああいう感じの奴です。
趣味の範囲であれば特に大した問題はないと思いますが、バグったり落ちたりすると困るプログラムや処理になると話が変わってきます。
お察しかもしれませんが、主に労働関係ですね
日々利用されるアプリケーションやAPIのサーバ、内部のバッチ処理など、この現代社会において止まると困るものは多岐に渡ります。
例えばTwitterが落ちたらTwitter民が発狂して暴動が起きますよね、そういう事です。
開発・運用する側の人間として、そもそもバグらないように作り込むというのは当然あるのですが、エンジニアも決して完璧ではないのでAPIに処理を追加した時にバグらせてしまったり、突然サーバ自体が落ちるなど不慮の事故に見舞われる事もあります。
正常に処理が行われてない場合、内容だったり程度によりますが下記のような問題が発生してしまいます。

  • 利用者の不満が溜まる
  • 金銭的な損失
  • 信用の低下
  • サービス終了
  • 上司に詰められる
  • クビを切られる
  • 鬱病

そこで被害を抑えるため、サーバの生存や処理が正常に動作している事を見張り、問題があった場合すぐに対応するという事がエンジニアに求められます。
しかし、基本的にエンジニアは金儲け良いサービス提供のため、新規機能の追加や既出のバグ修正に追われていて、とてもじゃないですがそんな見張り役なんて普段やってられません。
なんなら新規機能の追加すら締切に間に合わないが(?)
なので、いわゆる監視ツールを導入する事でサーバやプログラムの処理状態を計測し、問題があると判断された場合に通知が来るように設定します。 こうする事で普段から見張らずとも異常時に気付き対応する事ができます。
開発に加え見張りまで人力でやろうとなると、人柱まっしぐらな気がします。

監視処理導入の必要性について述べたので、以下では監視ツールの1つであるPrometheusについて紹介しようと思います。

Prometheus紹介全集中 監視の呼吸

ワイ「あ^~心がぴょんぴょんするんじゃ^~(朝5時のアラート通知で起床)」
※ ノンフィクションです

Prometheusというのは、監視ツールの一種です。
実際には各種ExporterやAlertManagerというソフトウェアと組み合わせる事でサーバのリソース(メモリやディスクなど)などの監視や異常時の通知を実現します。
全体像の図は公式サイトで見る事ができます。
(初見だと何が何やら…って感じになりません…?)

Exporter壱ノ型 メトリクス出力

監視対象のサーバからメトリクスを集め、/metricsなどのエンドポイントとして出力します。
いろいろなExporterがあり、ものによって収集するメトリクスが変わってきます。
例えばnode_exporterの場合、監視対象のサーバに配置し実行するとリソース(メモリやディスク使用量など)を出力させる事ができます。

どんな感じに出力されるかをお見せするため、手元のラズパイにダウンロードして試してみましょう。
去年のアドカレ記事で露出狂という名前にされた彼です。)
公式のdownloadページから対応するバイナリをダウンロードし、解凍後実行します。

$ ./node_exporter
(実行時のログいろいろ)

9100ポートの/metricsにアクセスするとラズパイのメモリ使用量などのメトリクスが出力されているのが確認できます。

$ curl localhost:9100/metrics
# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 0.000119685
go_gc_duration_seconds{quantile="0.25"} 0.000119685
go_gc_duration_seconds{quantile="0.5"} 0.000265944
go_gc_duration_seconds{quantile="0.75"} 0.000507354
go_gc_duration_seconds{quantile="1"} 0.000507354
(以下省略)

Exporterは他にもたくさんあります。
また、基本的に特定のフォーマットに沿ってテキストで出力しているだけなので、自作しようと思えばできるようです。

Prometheus弐ノ型 メトリクス収集 / 弐ノ型・改 アラート判定

監視ツールの本体です。
Exporterが出力したエンドポイントに定期的にアクセスし、メトリクスを収集・保存します。

普段使いのPCに導入し、ラズパイのnode_exporterが出力したメトリクスを収集してみましょう。
node_exporterと同様に公式のdownloadページから対応するバイナリをダウンロードし解凍します。
設定ファイル(prometheus.yml)にどのエンドポイントからメトリクスを取得するかを記載する箇所(targets)があるので、そこに先ほどのnode_exporterのものを入力し実行します。
デフォルトだと9090ポートでPrometheusが動作するので、ブラウザでアクセスします。
下記のようなシンプルなUIがPrometheusのUIになります。

f:id:massoumen:20201206171236p:plain

PrometheusではPromQLと呼ばれる独自クエリでメトリクスの表示や後述するアラート条件の設定をします。
例えば下記のようなクエリを投げるとnode_exporterで取得したnode_memory_MemAvailable_bytesというメトリクス(空きメモリ)の値を1024で3回割った値を出してくれます。
(単にギガバイト単位で出力したいなと思っただけで、あまり深い意味は無いですが…)

node_memory_MemAvailable_bytes / 1024 / 1024 / 1024

f:id:massoumen:20201206171249p:plain

また、時系列データとして保存されているので、グラフとして値の変化を見る事もできます。

f:id:massoumen:20201206172823p:plain

メモリに負荷をかけるコマンドを打った後にグラフを出力すると爆速で空きメモリの数値が減っていくのが分かります。
大事なサーバでいきなりこんな現象が起きたら発狂不可避ですね。

$ /dev/null < $(yes)

f:id:massoumen:20201206172955p:plain

また、設定ファイルに監視ルールを記載する事で、条件を満たした際に後述するAlertManagerにアラートを飛ばす事ができます。
例えば下記のような設定をするとディスクの空き容量が25%以下になった場合にアラートを出す事ができます。

groups:
  - name: example
    rules:
    - alert: HighDiskUsage
      expr: node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} < 0.25
      for: 5m
      labels:
        severity: page
      annotations:
        summary: High disk usage

設定ファイルを編集した後Prometheusを再起動し、Alertsを選択すると監視ルールと状態の確認ができます。

f:id:massoumen:20201206182904p:plain

ラズパイ上に無駄にデカいファイルを生成し、ディスク容量を消費してみましょう。

$ fallocate -l 10G 10G.txt

しばらく待つと監視ルールが赤くなりました。無事死亡です。

f:id:massoumen:20201206183721p:plain

この状態になるとAlertManagerにアラートを飛ばすようになります。
配置した無駄にデカいファイルを削除すると正常値と判定されるようになり、静かになります。

AlertManager参ノ型 アラート通知

Prometheusから送られてきたアラートを受け取り、実際に通知処理を行います。
通知先にはメールやSlack、PagerDutyなど様々なものが対応しています。
補足ですが、PagerDutyというのは設定したルールに応じてアラートの集約や管理、電話通知などを行ってくれるサービスの事です。
察して下さい。

また、送られてきたアラートのラベルを参照し、アラートAはチームXに通知、アラートBはチームYに通知…という風にアラートを振り分けたりする事もできます。
AlertManagerも他と同様に公式のdownloadページからバイナリをダウンロードできます。
ここではメールを送信するように設定してみます。
AlertManagerの設定ファイルにメールサーバやパスワードを記述し、Prometheusの設定ファイルにアラートの送り先(AlertManaerのURL)を記述します。
設定が完了したら両方とも起動します。
再度ラズパイ上に無駄にデカいファイルを生成しアラートを発生させると、下記のようなメールが来ます。

f:id:massoumen:20201206223121p:plain

終わりに隙自語

どうしたって 消せないバグと 止まらないアラート

監視処理の一例としてPrometheusと関連ソフトウェアについて紹介しましたが、いかがでしたか?
メトリクスの収集やアラート判定、通知の設定を行う事で、自分たちの管理しているサーバやプログラムに異常があった際にすぐに気付き、対応する事ができるようになります。
とは言え、なんでもかんでも監視してアラートを設定すれば良いというものでも無いので、本当に重要な処理だけ監視するようにしたり、重要度を設定し高いものは電話、低いものは適当なチャンネルにSlack通知するだけといったような調整も考えていかないといけません。

4時にアラートの電話で叩き起こされた後7時にもう一度アラートの電話で叩き起こされた日がありました。結局眠すぎてその日午前休を取って寝ようとしたのですが、その後10時にまたアラートの電話で叩き起こされたので、本気で仕事辞めようかと思いました。

また、監視やアラートの話とは少しズレますが、少しでもわかりやすく、管理しやすいプログラムを書けるよう日々努力しましょう。(自戒)
クソコードを書いては先輩に咎められる毎日…
書いて終わりな趣味のコードなら良いですが、長く付き合うコードの場合は後で誰かが苦しむ事になるので…
自分とは限らないですよ。
察して下さい。

Processingで絵を描く

この記事はCCS Advent Calendar 2019の7日目の記事です。
昨日の記事:C++講座++ - yuma1338’s blog

お疲れ様です。CCSOBのまっそうめんです。
今年もCCSのアドベントカレンダーが行われるという事で、盛り上げるべく記事を書きます…!

衝動

突然ですが、何か絵を描きたくなりました。
この衝動をどうにかするため、まずProcessingをダウンロードして開きます。

f:id:massoumen:20191207001710p:plain

開いたらそこにコードを書きます。

int pointNum = 100000;
int scale = 100;
float a = 1.1;
float b = 1.3;
float c = 1.8;
float d = 1.3;
int colorR = 70;
int colorB = 75;
int colorG = 150;

void setup() {
  size(800, 800);
  background(0, 0, 0);
  translate(width / 2, height / 2);
  drawCliffordAttractor();
}

void drawCliffordAttractor() {
  blendMode(ADD);
  stroke(colorR, colorB, colorG);
  float x = 0;
  float y = 0;
  for(int i = 0; i < pointNum; i++) {
    point(scale * x, scale * y);
    float newX = sin(a * y) + c * cos(a * x);
    float newY = sin(b * x) + d * cos(b * y);
    x = newX;
    y = newY;
  }
}

実行ボタンを押すと別の画面が出てきます。

f:id:massoumen:20191206235455p:plain

絵が描けました。

Processingとジェネラティブアート

ProcessingというJavaベースのプログラミング言語があります。
線や図形などの描画機能が中心で、デザインやアート系のプログラミングが手軽にできるという特徴があります。
JavaScript版のp5.jsというライブラリもあるようです)

プログラムから絵を生成するので、パラメータなどの要素に乱数を使ったりする事で同じプログラムから様々な絵を作れたりもします。
(このような作品をジェネラティブアートと呼んだりするようです)

例えば先ほどのプログラムの場合、パラメータa, b, c, dcolorR, colorG, colorBに固定値ではなく乱数を与える事で様々な絵を生成できます。

float a = random(1, 2);
float b = random(1, 2);
float c = random(1, 2);
float d = random(1, 2);
int colorR = int(random(0, 150));
int colorB = int(random(0, 150));
int colorG = int(random(0, 150));

f:id:massoumen:20191207010519p:plain f:id:massoumen:20191207010526p:plain f:id:massoumen:20191207010905p:plain

Twitter上で#processingや#generativeartといったタグ検索をするといろんな作品が上がっているので、見てみると楽しいかもしれません。

描いてみた

以下、労働で破壊された心を癒すべくいろいろ描いてみた奴です。

import java.util.ArrayList;

int step = 5;
float scale = 300f;

void setup() {
  size(800, 800);
  background(255, 255, 255);
  translate(width / 2, height / 2);
  drawKochSnowflake(step, scale);
}

void drawKochSnowflake(int step, float scale) {
  ArrayList<PVector> points = new ArrayList<PVector>();
  points.add(new PVector(sqrt(3) / 2, 0.5));
  points.add(new PVector(-sqrt(3) / 2, 0.5));
  points.add(new PVector(0, -1));
  points.add(new PVector(sqrt(3) / 2, 0.5));
  
  for(int i = 0; i < step; i++) {
    ArrayList<PVector> newPoints = new ArrayList<PVector>();
    newPoints.add(points.get(0));
    
    for(int j = 0; j < points.size() - 1; j++) {
      PVector triPoint1 = PVector.sub(points.get(j + 1), points.get(j)).div(3).add(points.get(j));
      PVector triPoint3 = PVector.sub(points.get(j), points.get(j + 1)).div(3).add(points.get(j + 1));
      PVector triPoint2 = PVector.sub(triPoint1, triPoint3).rotate(radians(60)).add(triPoint3);
      newPoints.add(triPoint1);
      newPoints.add(triPoint2);
      newPoints.add(triPoint3);
      newPoints.add(points.get(j + 1));
    }
    
    points = newPoints;
  }
  
  fill(170, 255, 255);
  beginShape();
  
  for(int i = 0; i < points.size() - 1; i++) {
    vertex(scale * points.get(i).x,
           scale * points.get(i).y);
    vertex(scale * points.get(i + 1).x,
           scale * points.get(i + 1).y);
  }
  
  endShape();
}

f:id:massoumen:20191206235307p:plain

動くものも描けます。

float STEP = 2 * PI * 0.01;
int loopNum = 4000;
int iter = 0;

void setup() {
  size(600, 600);
}

void draw() {
  background(255, 255, 255);
  drawSpiral(rad(STEP * iter));
  iter++;
}

void drawSpiral(float scalar) {
  translate(width / 2, height / 2);
  
  for(int i = 0; i < loopNum; i++) {
    float theta = STEP * i;
    line(scalar * rad(theta) * cos(theta),
         scalar * rad(theta) * sin(theta),
         scalar * rad(theta + STEP) * cos(theta + STEP),
         scalar * rad(theta + STEP) * sin(theta + STEP));
  }
}

float rad(float t) {
  float r = pow(1.05, t);
  
  return r;
}

f:id:massoumen:20191207023405g:plain

目が回る~~~

2Dだけでなく、3Dの描画もできます。

void setup() {
  size(800, 800, P3D);
  background(255, 255, 255);
  drawMoebiusLoop();
}

void drawMoebiusLoop() {
  int divRNum = 10;
  int divTNum = 100;
  float stepR = 2.0f / divRNum;
  float stepT = PI / divTNum;
  float scale = 100;
  
  translate(width / 2, height / 2);
  rotateX(radians(45));
  rotateZ(radians(45));
  colorMode(HSB);
  beginShape(QUAD);
  
  for(int i = 0; i < divRNum; i++) {
    for(int j = 0; j < divTNum; j++) {
      float r1 = stepR * i - 1;
      float r2 = stepR * (i + 1) - 1;
      float t1 = stepT * j;
      float t2 = stepT * (j + 1);

      fill(255.0 / divRNum * i, 100, 255);
      vertex(scale * cos(2 * t1) * (r1 * cos(t1) + 2),
             scale * sin(2 * t1) * (r1 * cos(t1) + 2),
             scale * r1 * sin(t1));
      vertex(scale * cos(2 * t2) * (r1 * cos(t2) + 2),
             scale * sin(2 * t2) * (r1 * cos(t2) + 2),
             scale * r1 * sin(t2));
      vertex(scale * cos(2 * t2) * (r2 * cos(t2) + 2),
             scale * sin(2 * t2) * (r2 * cos(t2) + 2),
             scale * r2 * sin(t2));
      vertex(scale * cos(2 * t1) * (r2 * cos(t1) + 2),
             scale * sin(2 * t1) * (r2 * cos(t1) + 2),
             scale * r2 * sin(t1));
    }
  }
  
  endShape();
}

f:id:massoumen:20191207015541p:plain

void setup() {
  size(800, 800, P3D);
  background(255, 255, 255);
  translate(450, 800);
  rotateX(radians(45));
  rotateZ(radians(45));
  rotateX(radians(45));
  rotateY(radians(-45));
  drawDoubleHelix(0, 330);
  drawDoubleHelix(PI, 220);
}

void drawDoubleHelix(float shift, float colorH) {
  float theta = 6 * PI;
  float scale = 250;
  float h = 1000;
  int divNum = 60;
  float stepT = theta / divNum;
  float stepH = h / divNum;
 
  stroke(0,0,0);
  noStroke();
  colorMode(HSB, 360, 100, 100, 100);

  for(int i = 0; i < divNum; i++) {
    rotateZ(shift);
    translate(scale * rad(stepT * i) * cos(stepT * i),
              scale * rad(stepT * i) * sin(stepT * i),
              stepH * i);
    rotateZ(stepT * i);
    fill(colorH, 80.0 * (divNum - i) / divNum, 100, 50);
    box(80, 40, 10);
    rotateZ(-stepT * i);
    translate(-scale * rad(stepT * i) * cos(stepT * i),
              -scale * rad(stepT * i) * sin(stepT * i),
              -stepH * i);
    rotateZ(-shift);
  }
}

float rad(float t) {
  float r = pow(1.01, t);
  
  return r;
}

f:id:massoumen:20191207020821p:plain

以上、「Processingで絵を描く」でした。
明日はこまつなさんの記事です。

これまで作ったものを振り返る -いわゆるポエム-

この記事はCCS †裏† Advent Calendar 2018の2日目の記事です。
昨日の飯テロ(実行犯:kakira)

CCS4年のまっそうめんです。
気付けばもう4年生で、もうすぐ卒業して消えゆく存在です。
卒業論文が提出できればの話
これまで自分は何をしてきたのか、何を学び、何を得る事ができたのだろうか。
年を取ったせいか、ふとそんな風に考える事があります。

そんな訳で、今回は「これまで作ったもの」をなんとなく振り返って懐かしさに浸ろうかなと思います。
少し前に某先輩が過去に作ったゲームを紹介してるのを見てやりたくなっただけ
CCSの作品集に出したり大学祭の展示に出したものを中心に、思い出せる範囲で書き連ねます。

以下、基本的に作品紹介という名のポエムで有用な情報の類は無い気がするので、興味の無い人はそっ閉じ。
むしろ興味ある人がいるのか…?

nightmare

  • 制作時期:1年夏
  • 使用言語:C

f:id:massoumen:20181114023234p:plain

大学に入ってプログラミングを始めて最初に作った記念すべき作品。
コンソール上で動くプログラムで、C言語講座発表会に出しました。
このゲーム実は必勝法があって、逆にそれ以外では勝てない(COMが絶対勝つ)ようになっています。
(暇な人は考えてみて下さい)

FullFrontal

  • 制作時期:1年秋
  • 使用言語:C(DXライブラリ)

f:id:massoumen:20181117233431p:plain

全てはここから始まった…(?)
ゲーム制作に挑戦するにあたり、まずは小規模のものを作る事が大事らしいです。
それを遵守した結果、友達がこちらを見ている間スペースキーを押して服を着るゲームが生まれました。(?)
見られてない間は全裸です。これ以降サークルでは全裸ゲーの人という扱いになった気がします。
大学祭で展示したら遊びに来たょぅι゛ょがこのゲームをプレイしてる光景を見てしまい、死にたくなりました。
(大学祭に展示するゲームの内容はよく吟味した方がいいと思いました)

エンシェントジャガイモ

  • 制作時期:1年冬
  • 使用言語:C++(DXライブラリ)

f:id:massoumen:20181117233452p:plain

C++の練習がてらシューティングゲームの制作にチャレンジしたもの。
std::vectorがやたら便利だったり、画像の読み込みミスってメモリがやばいことになったり、追尾弾の処理って面倒なんだなぁ…って思ったりいろいろあったけど、その辺も含めプログラム作るの楽しいなぁとか思いながらやっていた気がする。
ゲームを作るにあたり、素材の確保に困っていた時にエンシェントジャガイモっていう謎のスレを見つけてしまい、「そうだ、じゃがいもの画像を使えばいいんだ」って謎の思考の結果ビジュアルがこうなった。

あおてんじょう

  • 制作時期:2年春
  • 使用言語:C++(DXライブラリ)

f:id:massoumen:20181114222003p:plain

前回のシューティングのビジュアルが完全にネタなので、今度はまともなビジュアルにしようと思い引き続きシューティングゲームを制作。
プログラムは前作のものを流用し気になる箇所を直ししたりシステムを少し変えただけなので、全体像がどことなく前作と同じ。
1枚画像を用意して表示するだけだと敵がスィーと平行移動する(伝われ)何とも言えない感じになるので、2枚用意してちょっと動きを足すなどしてそれっぽさを出そうとしていた。
敵1号(名前はまだ無い)が地味にお気に入り。

f:id:massoumen:20181114220928g:plain

新歓ランチャー

  • 制作時期:2年春
  • 使用言語:C#(DXライブラリ)

f:id:massoumen:20181115011821p:plain

新歓の展示用に作った。
最初C++で作ろうとしたんだけどなんか上手くいかなくて途方に暮れていた。
先輩がC#で作れるって教えてくれて、それでなんとか新歓までに間に合ったもの。
唐突に作ってって仕事丸投げされて、作り方とかの情報とか一切無くて結構頭を抱えていたのは今となっては良い思い出(?)である。
ランチャーの作り方とかデータとか、情報の類は頑張って後輩に託そうと思った瞬間だった。
(この後C++で作った奴は以前のアドカレで一部記事にしたりした)
制作そのものは結構楽しいので、なんか作りたいけど題材がない人とかおすすめかもしれない。

FullFrontalRevolution -全裸再臨-

  • 制作時期:2年夏
  • 使用言語:C++(Siv3D)

f:id:massoumen:20181117152340p:plain

全裸ゲー第2弾です。(爆)
今回は友達に見つからないように全裸で踊ります。
コンボが繋がるとフィーバーして友達も脱ぎます。(爆)

f:id:massoumen:20181117152400p:plain

綺麗なシーン遷移や図形の当たり判定取得などライブラリの力を存分に活用していたり、キャラ絵を描いてもらったおかげかやたらハイクオリティ(当社比)なバカゲーになってしまった。
あとゲーム内には組み込まなかったけど送られてきたキャラ絵の中にKBTITって名前のファイルが混ざってて爆笑していた。

なぜかこれまで作ったものの中でトップクラスに気に入っている。
使っていたデスクトップPCが逝ったので開発データが消滅してて、ちょっと悲しんでいる。
(完成品は別の所にもたまたま保存していて生存)

Sara Washer

  • 制作時期:2年夏
  • 使用言語:C++(Siv3D)

f:id:massoumen:20181117154533p:plain

コミケ作品集の締め切り直前で「もう1本なんか作ろうぜ!」っていう例の勢いでプログラムを担当し、1日だか2日くらいで極力バグを出さないように勢いでコードを書きまくった気がする謎ゲー。
基本的には流れてくる皿を洗い拭き取るという労働の基本を学ぶゲームなのだが、処理が追い付かないと判断するや皿を粉砕して証拠を隠滅する狂気のムーブを行えるのが特徴。
ポエム書いてて思ったけどこいつろくなもん作ってねぇな

大学祭ランチャー

  • 制作時期:2年秋
  • 使用言語:C++(Siv3D)

f:id:massoumen:20181117175513p:plain

大学祭が秋にあるので、安直だけど読書の秋という事で本を題材にしたデザインで作ったランチャー。
ページをめくるアニメーションを作ろうと思ったけど、手書きでそれを表現する画力はないのであれこれ考えた結果、何故かBlenderという3DCGソフトを使って本とページめくりのアニメーションを作ろうという結論に至った。(?)

f:id:massoumen:20181117174959p:plain

制作期間は1ヶ月無いくらいで、その内2,3週間はBlenderでの本とアニメーション制作に費やされている。(プログラムは約3日)
Blenderを使った事がなかったので手探り感あって楽しかったけど結構疲れた。(操作とか慣れなかったな…)

旅するナイト

  • 制作時期:2年秋
  • 使用言語:C++(Siv3D)

f:id:massoumen:20181117180306p:plain

夏休みは競技プログラミングに時間を注ぎ、後期が始まったらランチャーを作っていたのでゲームを作る時間が無い中、何か軽く作りたくなったので短時間でサクッと作ったミニゲーム
元ネタはKnight's Tour(騎士の巡歴)と呼ばれるパズルです。
昔に比べサクッと作れる度合いの高まりを感じ、プログラミング始めた頃に比べだいぶ手馴れてきたんじゃないかとか思ったり思わなかったり。

朧火

  • 制作時期:2年冬
  • 使用言語:C++(Siv3D)

f:id:massoumen:20181126215415p:plain

複数人で組んでローグライクゲームを作ろうと思ったが、残念ながら制作が止まってしまったもの。
チーム制作ってめっちゃ難しいんだなって思いました。(小並感)
幽霊とか妖怪とか、その手の類が出るタイプのゲームになる予定でした。
僕はプログラム担当だったんですが、ターン制でプレイヤーの動作に合わせて敵の移動方向や攻撃を決定したり、ローグライク特有の視界設定やマップ描画など、これまで作ってきたものとは段違いにプログラムの錬成が難しくてつらぽよだった(白目
敵の移動処理のために幅優先探索を用いたりと、競技プログラミングで学んだ知識が役に立って感動するなどしていた。
ローグライク系のゲーム、機会があれば再度挑戦したいと思ったり思わなかったり。

Fantasia

  • 制作時期:3年夏
  • 使用言語:C#(Unity)

f:id:massoumen:20181118001635p:plain

3Dのゲーム制作に挑戦したくてUnityの本読んでいろいろググりながら作ったアクションゲーム。
一部のエフェクトはEffekseerというエフェクト制作ツールを用いて作成してみた。
DXライブラリやSiv3Dでのゲーム制作とは結構勝手が違って慣れるまで時間がかかったけど、結構新鮮で楽しかった。
あとユニティちゃんがかわいい

この世の終わりみたいなゲーム

  • 制作時期:3年夏
  • 使用言語:C++(Siv3D)

f:id:massoumen:20181118004713j:plain

危なさがMAXなバカゲー
ゲームシステム自体は全裸ゲーに近く、ウェイにバレないように酒を捨てるゲームという内容となっている。
オタクたちが自宅で録音した「ウェーイwwwww」や某インスタの音声に似た何か(加工済み)や当時Twitterでよく見かけたバジリスクタイム風のアニメーションなど、改めて見るといろいろ酷い。

卯月識別器

  • 制作時期:3年冬
  • 使用言語:Python(Keras)

どうも流行っているらしい(?)DeepLearningをやってみたくて、画像を入力したらそれが卯月かどうかを判定するプログラムの作成に挑戦した。
データを集めるためにアニメを再履修し、卯月を中心とする各キャラクターの顔が映ったタイミングでスクショを撮る苦痛極まりない作業が必要で、推しにかける情熱が試されているなと。
年末、弟が大量に友達を連れ込んでリビングで盛り上がってる中、僕は画面に向かって黙々と改良をしていた。
黒魔術的パラメータ調整(手動でのごり押し)、精度向上の祈りを捧げるなど数々の行いによりそれっぽい(?)結果になってとりあえず満足した。

声質変換(toみくにゃんボイス)

  • 制作時期:3年冬
  • 使用言語:Python(Keras)

f:id:massoumen:20181122175611p:plain

卯月識別器で画像を扱ったので、次は音声かなと軽い気持ちで手を出して絶望した奴。
今回用いた手法では同じ内容を喋っているデータが必要なんですが、これが何を意味するかというとみくにゃんと同じセリフを喋って録音する必要があるんですよね。
自分の事をみくにゃんだと勘違いしている精神異常者

音声処理自体もよく分からんし、かなりキツいチャレンジだなと始めて少し経って気付いてしまった。
結局クオリティも微妙で、DeepLearningが全てを解決してくれる訳じゃないんだなと思ったり思わなかったり。
(いろいろなモデルが次から次へと提案されているので、その辺試したらもう少しマシな結果になるのかもしれないが…)

卯月識別器と声質変換以外にも、画像のスタイル変換だったりGANを用いたキャラの生成や変換を試したりもした。
個人的にはスタイル変換が一番面白かった気がする。

f:id:massoumen:20181122153633p:plain

Secret Code

  • 制作時期:4年夏
  • 使用言語:C++(Siv3D)

f:id:massoumen:20181122183121p:plain

最後はまともなゲームで締めようと思い、コツコツ作っていたタワーディフェンスゲーム。
本当は夏コミの作品集に出す予定だったのだが、研究とシージに明け暮れていたせいで間に合わなかったので大学祭の展示で出す事に。
大学祭では割と好評だったらしく、作ったかいがあったなぁと。(ウレチイ・・・ウレチイ・・・)
ただ、ゲームの性質上1プレイの時間が長くなってしまい混雑に貢献していたらしく、現場の人たちへの申し訳なさで心の中で土下座してた。
少し手直しして冬コミの作品集にも出す予定。

ポエム書くために過去作品引っ張り出したりいろいろ思い出したりしてたら懐かしさやら何やらで予想以上に死んでしまった…
明日の記事の担当は…あれ、誰もいない…
とっちーさんがシージの記事を書いたようです。

yoooomaruuuu.hatenablog.com

CycleGANで推しキャラを相互に変換してみた

はじめに

カードキャプターさくらクリアカード編が終わってしまい絶望したので、GANを使ってさくらちゃんの生成を試みようと思いました。
GANにはいろいろなものが提案されていますが、調査をしているとCycleGAN(論文)という面白そうなGANがある事を知りました。CycleGANでは2種類の画像A,Bを互いに変換する事ができるそうです。そこで今回はCycleGANを使って推しキャラであるさくらちゃん(カードキャプターさくら)と島村卯月(アイドルマスターシンデレラガールズ)の2名を相互に変換する事を試みます。
単にさくらちゃんを生成するだけなら他のGANでも良いと思われますが、単純にCycleGANを試してみたいというのもあり、何より推し2名を相互に変換するという罪深さ(?)に魅入られました。
(両陣営からボコボコにされそう)

今回試した結果、以下のような画像が得られました。

  • 卯月→さくら

f:id:massoumen:20180701191305p:plainf:id:massoumen:20180701191319p:plain

  • さくら→卯月

f:id:massoumen:20180701191645p:plainf:id:massoumen:20180701191648p:plain

良さげなものを載せましたが、全部ここまで綺麗という訳ではないですね…
実際は細部が崩れてたり、見るのがつらいものも多々…(

CycleGANとは

基本的なGANではGenerator(生成器)とDiscriminator(判別器)を1つずつ用意しますが、CycleGANでは2種類のデータA,Bに対しGeneratorとDiscriminatorを2つずつ用意します。 (ここではAがさくらちゃんの顔画像、Bが卯月の顔画像となります)

各GeneratorとDiscriminatorは以下の処理を行います。

  • G_{A→B}:Aを入力しBを生成
  • G_{B→A}:Bを入力しAを生成
  • D_A:本物のAとG_{B→A}で生成されたA(偽物)を分類
  • D_B:本物のBとG_{A→B}で生成されたB(偽物)を分類

CycleGANでは各{G,D}に対し基本的なGANで用いるAdversarial Loss({L_{GAN}})だけでなく、Cycle Consistency Loss({L_{cyc}})という損失を用いているのが大きな特徴になります。
L_{cyc}ではG_{A→B}にAを入力して得られた出力BをG_{B→A}に入れ、最終的に得られた出力A'が元の入力Aとどのくらい近いか(画素値の差)を求めます。Bについても同様です。
L_{cyc}を用いる事で各Generatorに元の入力に戻しやすいような変換をGeneratorに行わせる事ができます。ここではキャラの顔画像を用いるので、顔の向きなどを保った変換を期待できます。

従来提案されているような画像の変換(pix2pixなど)では変換前と変換後のペアを用意して学習する必要があったのが、L_{cyc}を導入する事によりペアである必要がなくなるというのがCycleGANの大きな利点です。今回行うようなキャラクターの変換をpix2pixなどで行う場合、さくらちゃんと卯月の画像のペアが同じ位置や向きをしている必要がありデータを用意する事が難しいです。一方CycleGANでは画像のペアを用意する必要が無く、単にさくらちゃんと卯月の画像がある程度確保できれば学習する事ができます。

L_{cyc}ではA→B→Aと変換した時に元に戻る事ができれば良いので、必ずしも同じ顔の向きで変換が行われる保証は無いと思われます。(直感的には同じ向きや表情で変換する方が戻しやすいとは思いますが…)
この辺りが少し怪しくはありますが、少なくとも論文の画像や今回試した結果を見る感じでは割と上手くいってるので良いという事にします。

今回、コードはここを参考にしました。
Generatorではストライド2の畳み込みでサイズを小さくした後Residual Block(入力を最後に足す事で残差を学習する奴)を用い、最後に転置畳み込みでサイズを元に戻し出力します。
また、Discriminatorでは通常のCNNを利用します。
(論文だとPatchGANを利用していると書いてあった気がしますが、どうなんでしょうね…)

顔画像に関しては以前卯月識別器を作った時のように、アニメをキャプチャしたものから顔部分を切り取って利用しました。
枚数は卯月、さくらちゃん共に約700枚くらいです。

おわりに

変換結果をざっと見た感じ、ある程度は変換できていますがどれも高品質とは言い難かったです。
モデルの改良とかパラメータ調整とかしたらいい感じになるんですかね…
(学習に時間がかかるのであまりパラメータ変化による出力の変化などを見れてません)
とは言うものの、機械学習での変換には普通1対1対応の画像を用意する必要があるので、その必要が無いというのはすごいなと思いました。
CycleGANを多対多にしたStarGANという変換モデルも提案されているそうなので、そのあたりも試してみたいですね。

CycleGANで推しの変換テスト

CycleGANで推し2名(さくらちゃん、島村卯月)の変換を試したのでメモ。
とりあえずサイズは64*64でテスト。
(サイズが小さいので参考コードのDとGの一部層を削除)
うまくいくと以下のような結果になった。

  • 卯月→さくら

f:id:massoumen:20180624025755p:plainf:id:massoumen:20180624025820p:plain

  • さくら→卯月

f:id:massoumen:20180624030021j:plainf:id:massoumen:20180624030024j:plain

微妙な結果のものもあるが、それでも1対1対応でないものからそれらしいものを作れるのはすごいなぁと。
パラメータの調整とかモデルの改良とかしたらさらに綺麗なものができるのだろうか。

Webアプリケーション制作

サークルの先輩がWebアプリケーション制作の資料をアップしていたので、試しにやってみました。
利用する言語はPythonで、最近機械学習とかで遊ぶのに使っていたのでちょうど良いですね。

qiita.com

一通り読んで、試しになんか作ってみようという事で、せっかくなので以前作った卯月識別器をブラウザ上で試せるようなものを作ってみました。

\デデーン/

f:id:massoumen:20180329215512p:plain

はい。見た目の調節とかほぼ何もしていないので雑さが目立ちますね。
いくつかサンプルとして画像を見る事ができるようなってます。
(無駄に)データベース使って画像のパスを管理してみたり。

f:id:massoumen:20180329215724p:plain

手持ちの画像を与えて識別させる事もできます。こっちは識別のために裏でCNNによる計算処理が行われています。

f:id:massoumen:20180329215934p:plain

GPUのせいなのか何なのか良く分かりませんがたまにPythonが死にます。
問題だらけのWebアプリですね。

f:id:massoumen:20180329220150j:plain

ブラウザ上であれこれポチポチするとちゃんと反応するの、なかなか面白いですね。
せっかくなので試しに公開とかしてみたいんですが、どうすれば公開できるんでしょう。
何も分かっていません。あとできればお金かけたくない

声質変換に挑戦しました

はじめに

機械学習を用いて以前キャラクターの識別について遊んだので、次は画像以外で何か遊んでみたいなーって思い、「そうだ、声質変換に挑戦しよう」と思い立ったので挑戦しました。
地獄を見る事になるとは思いもよらなかったのであった…

声質変換とは、ある人の声を他の人の声に変換する事を言います。
これが上手くいった場合自分の声を好きな人の声にする事ができます。 夢が広がりまくりですね。 現実は非情であった

ちょっと調べてみた感じ声質変換にはいくつか種類があり、特定の人の声から特定の人へ変換する一対一変換、任意の声から特定の人の声へ変換する多対一変換、その逆の一対多、任意の声から任意の声へ変換する多対多があるようです。今回は一番シンプルそうな一対一変換に挑戦します。

今回の目標

さて、変換する声は当然自分の声になる訳ですが、誰の声に変換するかをまずは決めます。
突然ですが私、みくにゃんが大好きなんですよ。

f:id:massoumen:20180319205921p:plain

みくにゃんというのはアイドルマスターシンデレラガールズに登場する猫系アイドルで、アニメにも登場しています。
猫系アイドルなのに魚が嫌いというすさまじい性質の持ち主だったりします。あとおっぱい

今回は機械学習を用いて自分の声をみくにゃんの声に変換するプログラムを作成するのを目標にしようと思います。

結果

先に結果を載せようと思います。まずは「アイドルマスターシンデレラガールズ」と喋ったものの変換結果です。

目標(みくにゃんの声)

変換前(僕の声)

変換結果

みくにゃんに近づいてるとは思いますが、何とも言えない微妙感ですね。機械感も強いです。
激甘評価をするならまぁまぁみくにゃんです。(自分に甘い)
とは言うものの、このデータの組は機械学習の際に用いたものなので、イイ感じに変換できて当然と言えば当然です。(イイ感じ…?)
本当に大事なのは学習データに含まれていないものがどう変換されるかという所だと思うので、その結果も載せようと思います。

1つ目は「みくはお魚が大好きだにゃ」と喋ったものの変換結果です。単語単位では学習データに存在すると思いますが、1文としては存在していません。
みくにゃんは魚が苦手です。

変換結果

すごい力の抜けた感じや機械感がしますが、一応音声としては聞き取れる…?それぞれの単語は学習データに含まれているせいか、そこまで酷い結果には感じません。
(学習データすら結果が微妙じゃないかと言えばその通りなのですが…)

次は「島村卯月、頑張ります」と喋ったものの変換結果です。先ほどのものとは異なり単語単位ですら殆ど存在しないと思います。
(みくにゃんが「島村卯月」と喋っているデータを用いた記憶は無いです)

変換結果

思ったより酷くはないし音声としては(多分…?)認識できますが、それでもやはり先ほど同様すごい力の抜けた感じや機械感はします。

さて、いくつか結果を載せたところで微妙感が漂いまくりな訳ですが、私も微妙だと思います。
いろいろ試行錯誤した結果、出力が音声だと認識できるくらいにはなりましたが、どうしても微妙感が拭えません。
正直これでも結構マシな結果で、音声とは言えないような謎の出力を多数観測したりしています。
最近ニコニコ動画に投稿された結月ゆかりの声になってみた人のものの方が遥かにクオリティが高いと思います。

www.nicovideo.jp

僕ももっとクオリティアップを図りたいところですが、だいぶ力尽きてきたので一段落と言った感じでこの記事を書いています。

手順の概要

今回は以下の手順で声質変換を行いました。

  1. 同じ内容を喋っているデータを作成
  2. メルケプストラムの抽出
  3. 機械学習を用いてメルケプストラム変換用のパラメータを推定
  4. 推定結果を用いてメルケプストラムを変換し音声を得る

残念ながらそれぞれの細かい処理について、完璧に把握してるとは言えず雰囲気でやってたりします。
Pythonと各種ライブラリに任せまくっています。

データの作成

今回の手法ではまず自分の声とみくにゃんの声データについて、同じ内容を喋っているもの(パラレルデータ)が必要になります。
みくにゃんのデータはセリフ集とかから持ってくるので大した問題ではありません。
問題は自分の声データです。みくにゃんと同じセリフを喋って録音する必要があります。

なので録音します。(迫真

これが…ほんとですね…気が狂いそうになるといいますかね、もうあれですよ。二度とやりたくないです。
オタクがひたすらみくにゃんのセリフを喋ってる光景、相当キツいです。
自分の事をみくにゃんだと勘違いしている精神異常者

声優ってすごいなって改めて思いました。

メルケプストラムの抽出

なんとか録音を終えたら音声データから特徴量としてメルケプストラムを抽出します。
メルケプストラム以外にも特徴量がいろいろあったりしますが、どれが良いんでしょうね。(正直良く分かりません
メルケプストラムを抽出するにあたり以下の記事の内容を参考にしました。

aidiary.hatenablog.com

wavファイルはあらかじめ16kHz・モノラルにしておきます。
メルケプストラムの抽出は上のサイトのものをそのまま利用させていただきました。

自分の声とみくにゃんの声データから得られたメルケプストラムの内、1次元目は以下のようになっています。

f:id:massoumen:20180320031829p:plain

同じ内容(アイドルマスターシンデレラガールズ)を喋っているのですが、かなり値が異なっています。
また、今後の処理のために時間方向のデータ数を揃えておきます。以下の記事の内容を参考にしました。

aidiary.hatenablog.com

時間方向に揃えると以下のようになりました。
割と合ってる箇所とそうでない箇所が見られます。

f:id:massoumen:20180320031840p:plain

機械学習

データを用意したらモデルを構築し学習を行います。ライブラリはKerasを利用しました。そこまで複雑でないモデルならシンプルに書けて好きです。
画像を扱った時はCNNを用いましたが、今回は時系列データという事でRNN(Bidirectional LSTM)を用いてみたいと思います。
自分のメルケプストラムの時刻tからt+aの値を入力として与え、みくにゃんのメルケプストラムの時刻tの値を出力するように学習します。
また、メルケプストラムは最初の次元にパワーの値が入り26次元のベクトルになっているそうですが、それを無視した方が良さげな結果になったので、今回の場合は入力が(a, 25)の行列、出力が25次元のベクトルになります。
また、時刻0からa、1からa+1、2からa+2、…とスライドさせながら入出力の組を作成します。入力データについて、スライドさせていくと終わりの部分が足りなくなるので、足りない部分は0で埋めるようにしています。
また、各次元ごとに平均0、分散1になるように正規化を行いました。平均を引いて標準偏差で割るアレです。
学習用のソースコードは以下のようになりました。

from keras.models import Sequential
from keras.layers import CuDNNLSTM, Dense, Bidirectional
from sklearn.model_selection import train_test_split
import numpy as np
import glob
import gc
import tensorflow as tf
from keras.backend import tensorflow_backend

config = tf.ConfigProto(gpu_options=tf.GPUOptions(allow_growth=True))
session = tf.Session(config=config)
tensorflow_backend.set_session(session)

max_t = 40
input_dim = 25 # 入力の次元
hidden_dim = 100 # 隠れ層の次元
output_dim = 25 # 出力の次元

# 1フレームずつずらしながら(data_num, max_t, input_dim)のデータを生成
def read_input(mcep_dir, mean, std):
    data = []
    mcep_list = glob.glob(mcep_dir + "/*")

    for mcep_path in mcep_list:
        print(mcep_path)

        mcep = np.loadtxt(mcep_path)[:, 1:] # パワーを無視

        mcep = (mcep - mean) / std

        mcep = np.r_[mcep, np.zeros((max_t - 1, input_dim))]

        # ずらしながらデータを追加(max_tに満たない場合は0を付け足して追加)
        if mcep.shape[0] >= max_t:
            for i in range(mcep.shape[0] - max_t + 1):
                data.append(mcep[i:i + max_t, :])
        else:
            zero = np.zeros((max_t - mcep.shape[0], mcep.shape[1]))
            mcep = np.r_[mcep, zero]

            data.append(mcep)

    return np.array(data).astype("float32")

# 時刻t,t+max_tのデータから時刻tの変換結果を推定する用のデータを生成
def read_teacher(mcep_dir, mean, std):
    data = []
    mcep_list = glob.glob(mcep_dir + "/*")

    for mcep_path in mcep_list:
        print(mcep_path)

        mcep = np.loadtxt(mcep_path)[:, 1:] # パワーを無視

        mcep = (mcep - mean) / std

        data.extend(mcep)

    return np.array(data).astype("float32")

def read_mcep(mcep_dir):
    data = []
    mcep_list = glob.glob(mcep_dir + "/*")

    for mcep_path in mcep_list:
        print(mcep_path)

        mcep = np.loadtxt(mcep_path)[:, 1:] # パワーを無視
        data.extend(mcep)

    return np.array(data)

# LSTMの構築(入力:(data_num, max_t, input_dim), 出力:(data_num, output_dim))
model = Sequential()
model.add(Bidirectional(CuDNNLSTM(hidden_dim, return_sequences=True), input_shape=(max_t, input_dim)))
model.add(Bidirectional(CuDNNLSTM(hidden_dim, return_sequences=True)))
model.add(Bidirectional(CuDNNLSTM(hidden_dim, return_sequences=True)))
model.add(Bidirectional(CuDNNLSTM(hidden_dim)))
model.add(Dense(output_dim))

model.compile(optimizer="adam", loss="mae")

# 全部のデータをまとめ、各次元ごとに平均と標準偏差を求める
x_temp = read_mcep("./aligned_mcep/me")
t_temp = read_mcep("./aligned_mcep/miku")
y = []
y.extend(x_temp)
y.extend(t_temp)
y = np.array(y)
mean = np.mean(y, axis=0)
std = np.std(y, axis=0)
del x_temp
del t_temp
gc.collect()

# 平均と標準偏差を保存
np.savetxt("lstm_mean.txt", mean, fmt="%0.6f")
np.savetxt("lstm_std.txt", std, fmt="%0.6f")

# 入力データと教師データの読み込み
x = read_input("./aligned_mcep/me", mean, std)
t = read_teacher("./aligned_mcep/miku", mean, std)

# 学習用データとテストデータに分離
x_train, x_test, t_train, t_test = train_test_split(x, t, test_size=0.3)

# 学習
history = model.fit(x_train, t_train, epochs=50, batch_size=256, validation_data=(x_test, t_test))

# モデルを保存
model_json_str = model.to_json()
open("lstm_model.json", "w").write(model_json_str)

# 重みを保存
model.save_weights("lstm.h5")

あとは実際に実行し、学習が終わるのを待ちます。

実際に変換

学習が終わったらそれを用いて実際に変換を行います。
流れとしては以下のようになります。

  1. 変換したい音声からメルケプストラムを抽出
  2. 学習したRNNを利用してメルケプストラムを変換
  3. 変換したメルケプストラムから音声合成

新たに書いたソースコードは以下の2つです。
RNNを利用してメルケプストラムを変換する部分とメルケプストラムから音声合成を行う部分です。

import os
import sys
import numpy as np
from keras.layers import Input, Dense
from keras.optimizers import Adam
from keras.models import Model, model_from_json
import tensorflow as tf
from keras.backend import tensorflow_backend

config = tf.ConfigProto(gpu_options=tf.GPUOptions(allow_growth=True))
session = tf.Session(config=config)
tensorflow_backend.set_session(session)

if len(sys.argv) != 3:
    print("usage: python convert_mcep.py <mcep_path> <converted_mcep_path>")
    exit()

mcep_path = sys.argv[1]
converted_mcep_path = sys.argv[2]

input_dim = 25 # 入力の次元
hidden_dim = 100 # 隠れ層の次元
output_dim = 25 # 出力の次元

max_t = 40
mcep = np.loadtxt(mcep_path)
power = mcep[:, 0]
mc = mcep[:, 1:]
mean = np.loadtxt("lstm_mean.txt")
std = np.loadtxt("lstm_std.txt")

mc = (mc - mean) / std # 正規化
mc = np.r_[mc, np.zeros((max_t - 1, input_dim))]

# 変換用のLSTMを読み込む
lstm = model_from_json(open("lstm_model.json").read())
lstm.load_weights("lstm.h5")

# LSTM入力用に整形
data = []

for i in range(mc.shape[0] - max_t + 1):
    data.append(mc[i:i + max_t, :])

data = np.array(data).astype("float32")

converted_mcep = lstm.predict(data) # LSTMで変換

converted_mcep = converted_mcep * std + mean # 正規化したものを戻す
converted_mcep = np.c_[power, converted_mcep]

np.savetxt(converted_mcep_path, converted_mcep, fmt="%0.6f")
import os
import sys
import subprocess
import struct
import numpy as np
from scipy.stats import multivariate_normal
from sklearn.externals import joblib

# メルケプストラム次数
# 実際はパワー項を追加して26次元ベクトルになる
m = 25

def extract_mcep(wav_file, mcep_file, ascii=False):
    cmd = "bcut +s -s 22 %s | x2x +sf | frame -l 400 -p 80 | window -l 400 -L 512 | mcep -l 512 -m %d -a 0.42 | x2x +fa%d > %s" % (wav_file, m, m + 1, mcep_file)
    subprocess.call(cmd, shell=True)

def extract_pitch(wav_file, pitch_file):
    cmd = "bcut +s -s 22 %s | x2x +sf | pitch -a 1 -s 16 -p 80 > %s" % (wav_file, pitch_file)
    subprocess.call(cmd, shell=True)

def synthesis(pitch_file, mcep_file, wav_file):
    cmd = "excite -p 80 %s | mlsadf -m %d -a 0.42 -p 80 %s | clip -y -32000 32000 | x2x +fs > temp.raw" % (pitch_file, m, mcep_file)
    print(cmd)
    subprocess.call(cmd, shell=True)

    cmd = "sox -e signed-integer -c 1 -b 16 -r 16000 temp.raw %s" % (wav_file)
    print(cmd)
    subprocess.call(cmd, shell=True)

    os.remove("temp.raw")

if len(sys.argv) != 4:
    print("usage: python convert_voice.py <wav_path> <converted_mcep_path> <converted_wav_path>")
    exit()

source_wav_file = sys.argv[1]
converted_wav_file = sys.argv[3]

print("extract pitch ...")
source_pitch_file = "source.pitch"
extract_pitch(source_wav_file, source_pitch_file)

source_mcep = np.loadtxt(sys.argv[2]) # 変換後のmcepを読み込む
source_mcep_file = "temp.mcep"
fp = open(source_mcep_file, "wb")

for t in range(len(source_mcep)):
    x_t = source_mcep[t]
    fp.write(struct.pack('f' * (m + 1), * x_t))

fp.close()

# 変換元のピッチと変換したメルケプストラムから再合成
print("synthesis ...")
synthesis(source_pitch_file, source_mcep_file, converted_wav_file)

# 一時ファイルを削除
os.remove(source_mcep_file)
os.remove(source_pitch_file)

変換前と変換後、みくにゃんのメルケプストラムの1次元目は以下のようになっています。
青色が変換前、オレンジが変換後、緑がみくにゃんのメルケプストラムです。
ある程度みくにゃんのものに近づいている感じはあります。(時間は揃えています)

f:id:massoumen:20180320035830p:plain

おわりに

以上が今回の声質変換の結果と大まかな流れです。
音声処理、超難しいですね…(決して画像処理が簡単という訳ではないと思いますが…)
クオリティを上げるためには音声処理や機械学習についてもっといろいろ学ぶ必要がありそうです。
今回、GANを用いたものにも挑戦してみたかったんですが、理解が及ばず今のところ試していません…
(ソースコードの複雑さが段違いである…無念)

今後も勉強しつつ何かネタを見つけてチャレンジしたり、これまで作ったものの改良をしたりしていきたいですね。