20200110のUnityに関する記事は8件です。

現職のスマホゲームアプリ開発者から聞いたビルド作業について

聞いたことメモ

以下、Tech Academyのメンターの方から聞いたこと。

  • ビルド作業はプログラマー以外にはハードルが高いため、自動化していることが多い。
  • ビルドマシンを用意している。
  • JenkinsなどのCIツールがあって、GitHubからブランチを取得してビルド専用マシン上でビルドする。
  • Jenkinsのコマンドを作ってUnityと接続している。
  • ビルド環境をメンテナンスする専任担当がいる。
  • デイリービルドしているが、Unitテストはやっていない(というよりスマホゲーム系はUnitテストまでやっている会社は少なく、リリースしてバグだらけというのがはびこっているのが実態←!)
  • Jenkinsはほとんどの現場である。
  • データベースのデータについてExcelファイル→CSV→バイナリへと変換するということもやってる
  • 今の現場だと開発ブランチと一個区切ったステージングブランチがある感じ
  • デイリービルドは開発ブランチに対してかけていて、ビルド時にエラーがあればすぐ通知が来る。
  • デバッグチームが存在し、ビルドが通った成果物の動作確認もしている。問題があれば連絡があり、当日中に取るような感じ。
  • とはいえ、そもそもpush前に実装者が動作確認はすべきなので、そのような事態を招いた人は反省すべき

まとめ

  • ビルドはJenkins使っている現場が多い
  • ただしUnitテストは実装する余力がなく、そもそも実装してもやっぱこうした方がいいなという修正も入るためやっていない現場が多数。結果、リリースしてバグだらけというのがはびこっているのがスマホゲーム業界(ひぃーーーーー!!!)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityでコードの速度比較をする時のベストプラクティスを模索してみた

速度比較をしたい!

UnityでC#のコードの最適化をしていて、「この書き方とこの書き方ではどっちが速いんや!」ってなることがよくあるので、速度比較をする時のベストプラクティスを模索してみました。

その1,検証用のコードはTest Runnerで書く

ちょっとテストコードを書きたいときに、空のゲームオブジェクト作ってスクリプトをアタッチして終わったら消すのって地味にめんどくさいです。
Test Runnerを使えば、上記の手順を踏むことなくコードを書いてすぐに実行できるので、爆速でトライ&エラーが出来ます。

詳しい使い方はこちらの記事が分かりやすかったです↓
Unity使いは全員Unity Test Runnerを使え!爆速のトライ&エラー環境だぞ!

その2,時間を計るだけならStopwatchクラス

時間を計りたいだけならStopwatchクラスが簡単で便利そうです。
現実のストップウォッチのような使い方で時間を計ることが出来ます。

参考↓
より高い精度で時間を計測する

その3,時間とGC Allocも図りたければPlofiler

時間やらGC Allowやらその他諸々を計測したい場合は、UnityのPlofilerを使いましょう。
速度比較に限らず、最適化には必須の機能なので絶対に使い方を覚えておいた方がいいです。

詳しい使い方はこちらの記事が分かりやすかったです↓
【Unity】CPUプロファイラでパフォーマンスを改善する 前編

その4,ILを確認する(上級者向け)

C#で書いたコードはコンパイルした際にILというものに変換され、そこから更に機械語に変換されて実行されます。
この変換の際にコンパイラがコードを良い感じに最適化してくれるので、実はC#のコードだけを見ても実際にどういう処理が行われるのかはよく分かりません。

例えば、変数の宣言をループ外に書くか、ループ内に書くかの違いがある以下の2つのコードですが、ILを確認してみると全く同一の処理であることが分かります。

ループ外で宣言する場合
public class C {
    public void M() {  
        int sum = 0;
        int count = 0;
        int num;
        while(count < 100){
            count++;
            num = count;   
            sum += num;
        }
    }
}
ループ内で宣言する場合
public class C {
    public void M() {  
        int sum = 0;
        int count = 0;
        while(count < 100){
            count++;
            int num = count;   
            sum += num;
        }
    }
}

ILの確認にはSharpLabというWebサービスが便利です。試しに上のコードをコピペしてみましょう。
しかしSharpLabでは、標準ライブラリしか使えないようなのでUnity固有のライブラリを使う場合は、スクリプトをdll化し、逆アセンブルする必要がありそうです。

やり方はこちらの記事が分かりやすかったです↓
UnityのスクリプトをDLL化する
C#で作られたプログラムをデコンパイルしてみよう

ILを確認することで、そのコードが速度比較に適したものかどうかを確認したり、なぜその計測結果になるのかを調べる助けになると思います。

まとめ

正確な速度比較をしようと思うとかなり高度な知識が必要になってしまいますが(ILとか機械語とかワカラン)、Stopwatchで時間を計るコードを書いてTest Runnerで実行するくらいなら簡単にできそうですね。

ご意見ご感想、それは違うよ!等ございましたらコメントを頂けますと幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

使用Unity的「rigidbody」「Physic Material」功能、制作一个拥有物理特性的弹球。

◆写在开头

这次要介绍的是,经常会用到的 「rigidbody」和「Physic Material」这两个功能。
关于「rigidbody」「Physic Material」的具体说明请参考官网说明。

这里做一个简单的总结性说明、
Object里追加「rigidbody」功能后,可自动运行一些简单的物理运算。
而「Physic Material」则是可以让Object本身拥有摩擦力 弹力等物理特性

(还有日文版哦)
PS:因为我的制作环境是日语环境,所以下面的软件截图,都是日文版的截图。
表示内容不同,但菜单的位置,作用等都是一样的,
根据说明和参考截图应该能找到相对应的功能的所在位置。

那么…

◆开发环境

macOS Mojave 版本 10.14.6
Unity 2018.4.12f1
Android SDK

◆制作顺序

  1. 新建一个Object
  2. 追加必要的物体
    1. 追加地面
    2. 追加需要的物体(小球)
  3. 让小球拥有物理特性
    1. rigidbody的追加
    2. Physic Material的做成
  4. 运行确认

1.新建一个Object

01.png
首先,启动Unity Hub,新建项目。
选择3D模式,文件名和保存地点随意。

02.png

启动后会进入这样的画面(根据Unity的设置不同,菜单颜色等会有微妙的变化请注意。)

2. 追加必要的物体

1. 追加地面

04.png
在「Hierarchy」中,右键点击「3D Object」→「Cube」,追加新的Object。
05.png

刚才追加啊的Cube是要作为地板的模型,所以为了让它看起来像块地板,调整一下尺寸吧。
选择「Cube」,在「Inspector」里选择「Transform」→「Position」调整「Scale」
(可自由调整)

这次的例子的调整数值是「Position」y=-3、「Scale」x=20 、y=0.5、z=10。
06.png

2. 追加需要的物体(小球)

07.png
和刚追加的地板方法同样,追加一个新的物体吧
右键点击「3D Object」→「Sphere」追加小球。
名字改为「ball」。
尺寸可自由调整。
08.png
需要的物体都追加完成后,点击Unity上方的「Play」按钮,观察游戏画面的情况,确认追加无误。

3.让小球拥有物理特性

1. rigidbody的追加

09.png
追加方法非常的简单,选择想要追加的Object(这次的例子,需要选择的是「ball」)
选择「Inspector」中的「Add Component」,下滑菜单里找到「Rigidbody」点击追加。

2. Physic Material的做成

10.png
右键点击「Project」,选择「Create」→「Physic Material」。
名字自由,这里设置成「ball Physic Material」
11.png
选择刚才追加的「ball Physic Material」,将「Bounciness」的数值设置为0.9。
12.png
点击「ball Physic Material」,拖入「ball」里的「Sphere Collider」的「Material」选项中。

4.运行确认

最后点击はUnity上方的「Play」按钮,确认小球是否正确运动吧。
t1njp-wisz7.gif

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityの「rigidbody」と「Physic Material」を使って、物理特性を持った跳ねるボールを作りましょう。

◆はじめに

今回はよく使われている「rigidbody」「Physic Material」機能の使い方紹介します。
いろんな場面で活用できる機能です。

「rigidbody」「Physic Material」に関する具体的な説明は、公式を参照してください。

簡単に説明しますと、
オブジェクトに「rigidbody」機能を追加すれば、簡単に物理演算を行ってくれます。
「Physic Material」は衝突するオブジェクトの摩擦や跳ね返り効果を調整できます。

では…

◆開発環境

macOS Mojave バージョン 10.14.6
Unity 2018.4.12f1
Android SDK

◆手順

  1. 新しいプロジェクト作成
  2. 必要なObjectを追加
    1. 床を作成
    2. 物体(ボール)を追加
  3. ボールに物理特性を入れる
    1. rigidbody追加
    2. Physic Materialを作る
  4. 動作確認

1.新しいプロジェクト作成

01.png
まず、Unity Hubを起動して、 新規作成します。
テンプレートは3Dを選択します、プロジェクト名と保存先は自由です。

02.png
起動画面に入りました(画面はUnityの設定により違いがある可能性があります)

2. 必要なObjectを追加

1. 床を作成

04.png

「Hierarchy」にて、右クリックし「3D Object」→「Cube」をクリックしてObjectを追加します。
05.png
追加したCubeの見た目は床に見えるように調整します。
「Cube」を選択し、「Inspector」→「Transform」中の「Position」と「Scale」を調整します。
自由に調整して構いません。
ここでは、「Position」y=-3、「Scale」x=20 、y=0.5、z=10に調整しました。
06.png

2. 物体を追加

07.png
床と同じ方法で、新しい物体を追加します。
「3D Object」→「Sphere」クリックし、ボールを追加します。

名前は「ball」にします。
サイズは初期値のままでも大丈夫ですが、見やすいように若干上に移動しても構いません。
(結構高い所に置くと、ボールがカメラの範囲を超え、見えなくなる可能性ありますので注意してください)
08.png
追加完成後、Unity上の「Play」ボタンを押して、Game画面を確認します。

3.ボールに物理特性を入れる

プロパティの意味は、公式サイトがあるので、ここでは詳しい説明は割愛します。

1. rigidbody追加

09.png
追加方法はすごく簡単で、追加したいObjectを選択して(今回は先程追加した「ball」を選択)
「Inspector」中の「Add Component」をクリックし、「Rigidbody」を選択するだけです。

2. Physic Materialを作る

10.png
「Project」に右クリックし「Create」→「Physic Material」を選択し追加します。
名前はご自由で、ここでは「ball Physic Material」にします。
11.png
追加した「ball Physic Material」を選択し、「Bounciness」を0.9に設定します。
12.png
「ball Physic Material」は「ball」の「Sphere Collider」の「Material」に入れます。

4.動作確認

最後はUnity上部の「Play」ボタン押して、動作確認しましょう。
t1njp-wisz7.gif

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HoloLens 2 開発入門 ~ManipulationHandlerとAppBar~(Unity2018.4.2f1、MRTKv2.2)

前回の記事 HoloLens 2 開発入門 ~BoundingBoxとAppBar~(Unity2018.4.2f1、MRTKv2.2) では、Cubeの移動ができません。
MRTKのManipulationHandlerを用いれば、Cubeの移動・回転・スケールを変更することが可能です。
しかし、AppBarと併用するとAdjust中に回転やスケールを変更したいのに誤って移動してしまい困難だったため、Adjust中は移動しない設定にしてみました。他の案としては、手のメッシュを表示して、それぞれの操作時の挙動を変えるなどの工夫でもいいかと思います。

開発環境

  • HoloLens 2
  • MRTK v2.2.0
  • Unity 2018.4.2f1
  • Visual Studio 2019
  • Unity Hub 2.2.2

仕組み

AppBarのStateがAppBar.AppBarStateEnum.Manipulationの時にManipulationHandlerをfalse、それ以外の時にManipulationHandlerをtrueにします。

1.前回の記事に従ってプロジェクトを作成してください
2.CubeにManipulationHandlerをアタッチ、図のようにHost Transform/One Handed Only/Move Scale/Eventsのパラメータを設定します
3.ManipulationHandlerManager.csを作成(下に記述)、Cubeにアタッチします
4.Cubeの子オブジェクトのAppBarをManipulationHandlerManagerのAppBarにアタッチします
01.PNG

プログラム

ManipulationHandlerManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Microsoft.MixedReality.Toolkit.UI;

public class ManipulationHandlerManager : MonoBehaviour
{
    public AppBar appBar;
    private AppBar.AppBarStateEnum state;

    // Update is called once per frame
    void Update()
    {
        state = appBar.GetComponent<AppBar>().State;
        // Debug.Log(state);

        if (state == AppBar.AppBarStateEnum.Manipulation){
            this.GetComponent<ManipulationHandler>().enabled = false;
        } else {
            this.GetComponent<ManipulationHandler>().enabled = true;
        }
    }
}

デモ

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

三日間で作成したゲームの技術的な話

ゲーム開発の技術的な話

はじめに

Qiita初投稿です。
昨年度末に二人で、三日間かけてUnityの2Dにてスマホゲームを開発しました。
作成したゲームは
横スクロールゲームで、プレイヤーが落ちないようにタップして浮かせつつ、横からくる障害物(ブロック)をかわしつつアイテムをとり、体力ゲージを意識しながらプレイするものです。

そのとき使った技術的な内容についてまとめてみます。
今回使用した技術・考え方としては以下の3つです。

  • シングルトン(GameManager)
  • ゲームマスタ(今回は操作説明シーンで使用)
  • ブロック自動生成

これらについてまとめていこうと思います。

シングルトン(GameManagerの作成)

シングルトンとは

今回Unityで作成したゲームで最も開発がしやすかった手法として、シングルトンという方法(考え方?)が挙げられます。
シングルトンとは
https://qiita.com/shoheiyokoyama/items/c16fd547a77773c0ccc1
にも記載されている通り、

  • 指定したクラスのインスタンスが1つしか存在しないことを保証する
  • インスタンスが1個しか存在しないことをプログラム上で表現したい

という利点があります。
作成したアプリが100%この手法を使えているかといわれると自信はないのですが、基本的にはこの考え方に従って開発を進めました。

Unityでシングルトンを利用する

Unityでシングルトンを実現するために、まずGameManagerを作成します。

GameManagerで変数を管理するため
ゲーム起動時にGameManagerでインスタンスを生成しておく必要があります。
ゲームを起動すると(基本的には)Startが最初に開かれるためStartSceneにGameManagerを設置します。

StartSceneに空のオブジェクトを配置し、GameManagerのスクリプトをアタッチします。

GameManagerの作り方

GameManagerの構成は以下の4つの項目で作りました。

  • Awake()でGameManagerのインスタンスを生成
  • 使用する変数系(スコアやタイマー、プレイヤーの状態など)を宣言
  • 変数を持ってくるためのgetter
  • GameManagerにある変数に値をセットするためのsetter

GameManagerが完成すれば、あとは開発を進めていくうちに必要な変数は増えていくので、基本全てGameManagerで宣言、getter・setterを追記していきます。
必要になればGameManagerのインスタンスを参照しgetterを呼ぶ、値を変えたければsetterで値をセットする、をするだけで変数を管理できます。

マスターデータの作成(操作説明用マスタの作成)

次は、操作説明に出力させる文言を管理する操作説明用マスターを作成しました。
開発当初はtxtファイルをResourcesフォルダからロードするだけでした。
しかし、txtファイルで読み込むと改行やページ番号の管理などが難しくなってしまったためにマスターを作ることにしました。(小規模のゲーム開発ならここまでしなくてもよかったかも)

マスターとは

マスターとは、文字列で管理されたデータ群のことです。(違っているかも)

例えば、RPGなどで出てくるPlayerについて考えてみます。
ある人がいて、その人は剣を持っていて、HPは300で、MPは600で、髪の毛は黒で、レベルは30で……
とたくさんのデータが詰まっていると思います。
このような登場人物がたくさん出てくると管理が大変になってきます。
これらをうまく管理させるために表を作成します。
上記の例で行くと、以下のような表が作れそうです。

Player

PlayerId HP Weapon Level
1 300 sord 30
2 500 - 50
3 100 stick 10

(長いので項目略)

このように、表で管理すると見やすく、管理がしやすくなります。
また、登場人物が増えても行を追加するだけで表現することができます。

マスターの作り方・処理の流れ

今回Unityで作成たマスターは次のような流れに沿って作成しました。

  1. Excelでシートを作成
  2. Excelに必要項目を記載
  3. Excelのシートにボタンを設置
  4. マクロを組み、JSONデータを出力させる
  5. Unity側でMaster.csを作成
  6. Master.csでJSONデータを読み込む
  7. JSONデータに従ってUnityが出力する

上記の流れを図にすると、以下のようになります。
Untitled Diagram.jpg
マスタを作成してしまえば、行を追加してJSONを出力させればUnity側ですぐ反映させることができます。
また、登場人物すべてのデータの管理が一括で行うことができます。

今回のゲーム製作では、Playerなどの管理ではなく、操作説明で表示する文字列をマスタとして作成し
表示しているページ数や、表示するときの画像のパスなどをマスタで管理するようにしました。
(画像のパスはうまく使えていませんが)

ブロックの自動生成

今回作成したゲームは、
「横スクロールゲーム」で、Playerが落ちないようにタップして宙に浮かせつつ、
横から流れてくるブロックに触れたり、かわしたりを楽しむものです。
加えて、障害物とPlayerに属性を持たせ、それぞれの属性の相性によって体力ゲージの増減が決定するという要素もあります。

その時、障害物であるブロックを自動で、かつランダムで生成するアルゴリズムを考えたので以下にまとめてみます。

ブロックの生成

いきなりすべてのブロックを自動生成する方法を考えるのではなく、まずは1つのブロックをどのように生成するかを考えました。

まず、1×1の正方形のブロックを用意します。
testBlock.png
このブロックを縦方向にランダムで伸ばしてあげることで、「壁」を表現できると考えました。
例えば、3×1と5×1と2×1のブロックをそれぞれ横においてみましょう。
すると、以下の図のようになると思います。
testBlock2.png
今はわかりやすくするために、外枠の線を黒にしていますが、ブロックと同一色にすれば、いい感じのブロックができそうです。
あとは「高さをランダムにして、1つ作ったら生成する場所を1つ右にずらす」をほしい長さの分だけループさせれば1まとまりのブロックが生成できそうです。
ただし、高さ0を含めてしまうと、最悪の場合、すべて高さ0となってしまう可能性があるので、ランダム関数を1~MaxHeightの中からランダムで数値を出す、という処理にすれば今回の問題は回避できそうです。
これをコードにしてみると、以下のようになりました。

createBlock.cs
// MAX_LENGTHはブロック群の横幅
// MAX_HEIGHTは各ブロックの最大の高さ
for (int i = 0; i < MAX_LENGTH; i++)
{
    GameObject block = (GameObject)Resources.Load("Block");
    blockHeight = randam.Next(1, MAX_HEIGHT);
    block.transform.localScale = new Vector3(1, blockHeight, 1); // ランダムな高さを入れる
    Instantiate(block, new Vector2((float)i, 0.0f), Quaternion.identity); // 作ったブロックを配置
}

アイテムの配置

ただ単にブロックが流れてくるだけでは面白くない、属性の変更ができないという意見から、Playerの色を変えるためのアイテムや体力回復アイテムの設置方法を追加で考える必要が出てきました。

純粋にブロックの上に置けばいいですが、ここもランダムにしました。
以下がアイテム設置のコードです。

createBlock.cs
// MAX_LENGTHはブロック群の横幅
// MAX_HEIGHTは各ブロックの最大の高さ
// setItemPathには、アイテムが保存されているパスが格納されている
for (int i = 0; i < MAX_LENGTH; i++)
{
    GameObject block = (GameObject)Resources.Load("Block");
    blockHeight = randam.Next(1, MAX_HEIGHT);
    block.transform.localScale = new Vector3(1, blockHeight, 1); // ランダムな高さを入れる

    var setColorRandomNum = randam.Next(0, 10); // 出現アイテムをランダムで設定
    var setBlockAboveItem = (GameObject)Resources.Load(setItemPath[setColorRandomNum]);
    Instantiate(setBlockAboveItem , new Vector2((float)i, blockHeight + 1.0f), Quaternion.identity); // アイテムブロックの上に配置

    Instantiate(block, new Vector2((float)i, 0.0f), Quaternion.identity); // 作ったブロックを配置
}

ブロックを横にスクロール

ここまでできれば、あとはブロックを横に動かすだけです。
今回のゲームでは、右から左にブロックが流れるという仕様のため、時間とともに横に動かす処理を書きます。

ただし、生成したブロックを各1つずつうごかしているととてもコードが長くなってしまう&冗長です。
そのため、空のparentBlockというオブジェクトを作り、作った各ブロック、アイテムなどを子にしてしまいます。
そして、この親オブジェクトを動かすことで、全体を動かすことができます。
イメージとしては以下の図のような状態です。
testBlock3.png
これを実際にコードにすると、このような形になります。

createBlock.cs
// MAX_LENGTHはブロック群の横幅
// MAX_HEIGHTは各ブロックの最大の高さ
// setItemPathには、アイテムが保存されているパスが格納されている

var parentBlock = (GameObject)Resources.Load("parentBlock");
GameObject parentBlockObj = Instantiate(parentBlock, new Vector2(0.0f, 0.0f), Quaternion.identity) as GameObject;
for (int i = 0; i < MAX_LENGTH; i++)
{
    GameObject block = (GameObject)Resources.Load("Block");
    blockHeight = randam.Next(1, MAX_HEIGHT);
    block.transform.localScale = new Vector3(1, blockHeight, 1); // ランダムな高さを入れる

    var setColorRandomNum = randam.Next(0, 10); // 出現アイテムをランダムで設定
    var setBlockAboveItem = (GameObject)Resources.Load(setItemPath[setColorRandomNum]);
    Instantiate(setBlockAboveItem , new Vector2((float)i, blockHeight + 1.0f), Quaternion.identity); // アイテムブロックの上に配置

    var blockObj = Instantiate(block, new Vector2((float)i, 0.0f), Quaternion.identity) as GameObject;
    blockObj.transform.parent = parentBlockObj.transform; // 生成したブロックの親が誰かを教える
}

あとはこの親ブロックをTime.deltaTimeなどで右から左に動かす処理を書けばそれっぽい動きをしてくれました。

おわりに

今回まとめた内容はゲームを作るうえではとても基本的な内容だとは思います。
しかし、実際にプログラムを書き、それを自分の言葉でまとめることでより理解が深まったと思っています。
また、いろんな人に見てもらえる環境下で記事を書くことで自分の成長にもつながるかなと考えています。
三日間で成長できたと感じれたハッカソンでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

c# メンバ変数に一括 代入

Base.cs
class Document
{
    public string Name1 { get; set; }
    public string Name2 { get; set; }
    public string Name3 { get; set; }
    public string Name4 { get; set; }
    public string Name5 { get; set; }

    public int Age1 { get; set; }
    public int Age2 { get; set; }
    public int Age3 { get; set; }
    public int Age4 { get; set; }
    public int Age5 { get; set; }
}

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
InsertToDocument.cs
List&<Person> people = new List<Person>gt;
{
    new Person { Name = "Taro", Age = 31 },
    new Person { Name = "Hanako", Age = 33 },
    new Person { Name = "Hitoshi", Age = 17 },
    new Person { Name = "Yoshie", Age = 28 },
    new Person { Name = "Kenta", Age = 21 }
};
Reflection.cs
Document doc1 = new Document();
int index = 1;
foreach (var item in people)
{
    // Name[1-5]プロパティに動的にアクセスし、値を設定
    var nameProperty = typeof(Document).GetProperty("Name" + index.ToString());
    nameProperty.SetValue(doc1, item.Name);

    // Age[1-5]プロパティに動的にアクセスし、値を設定
    var ageProperty = typeof(Document).GetProperty("Age" + index.ToString());
    ageProperty.SetValue(doc1, item.Age);

    index++;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FFTでギターの演奏を正誤判定

概要

マイクで拾った演奏音をもとに、FFTで出力した周波数スペクトラムのピーク値の組み合わせから、演奏の正誤を判定する。

はじめに

ギターの基本的な演奏技術の一つに、複数の弦を同時に指で押さえて和音を奏でる『コード弾き』があるのは周知の事実である。本稿はそんなコード弾きを独自に練習する際に役立つと考える、コード弾き演奏音から正誤判定を行う手法を実現する。
具体的には、マイクで演奏した音を拾い、コンピュータで周波数成分を検出、検出結果をもとに正誤の判定を行う。
なお、本稿で想定するシステムユースケースは、コード弾きのお手本をこちらから提示し予め演奏されるであろう音が分かっているというシナリオであり、演奏者がランダムに演奏する音のコード認識をするというものではない。

処理の流れ

マイクで取得した音情報に対して次のような操作を行う。
16.png

1. フーリエ変換

フーリエ変換およびFFT(Fast Fourier Transform)についての詳細な説明は割愛するが、一言でいえば「周波数ごとの成分を数値的に見ることができるようにする変換」である。

今回開発フレームとして利用しているUnityには、AudioSource.GetSpectrumDataというFFTスクリプトが既に用意されている。AudioSourceとして使用するマイクを指定し、FFT結果の周波数スペクトラムデータを受け取る2のべき乗サイズの配列を用意しておく。
以下図は実際に『Cメジャー』と呼ばれるコードを弾いた際の周波数スペクトラムである。

図1.png

2. ピーク値の探索

フーリエ変換により算出された周波数スペクトラムにおいて、ピークが立っている周波数がすなわち演奏された音と想定できる。したがって、ピーク値を探索し、検出されたピーク値の組み合わせから演奏の正誤を判定することができる。

一般的な(6弦)ギターは、最低周波数が82Hz(6弦開放)のE2、最高周波数が1245Hz(1弦23f)のD#6であり、この範囲内でピークを探索する。また、音階間の幅はE2とF2の間の5Hzが最小で、音階が高くなるほど大きくなっていくため、ピークを探索する幅も5Hzごととする。

ピーク値としては5Hz区間で探索する最大値をそのまま採用するのではなく、最大値間の最小値も同時に探索し、算出された最大値と最小値の差が閾値以上の場合にのみピーク値として採用する。これにより、ピークが立っていない、すなわちなだらかな区間での最大値を外すことができる。

3. 周波数を音階に変換

一般によく知られるドレミファソラシド、ギターをはじめとする国際スタンダードCDEFGAB。これらの音階は、1オクターブを分割した一つ一つ(という認識)であり、440Hz / ラ / Aを基音に、1オクターブ高くなると周波数が2倍になると定義されている。
ギターにおける音階は12音階であり、1オクターブの間にC、C#、D、D#、E、F、F#、G、G#、A、A#、Bが存在する。前述した通り、ギターの最低音はE2の82Hzであり、E3の164Hz、その間82Hzを12分割しているということである。

話は長くなったが、本稿では便宜的にC2の65.5Hzを0、C3の131Hzを12とし、scale = 12.0f * Mathf.Log(hertz / 65.5f) / Mathf.Log(2.0f)で周波数を数値的な音階に変換し、四捨五入を行ったのち変換した数値をもとに音階とオクターブをそれぞれ算出する。

4. コードごとに正誤判定

あとは、変換した音階と演奏されるであろう音とを比較するだけである。

以下図は作例であるが、変換した音階を表示し、演奏されるであろう音と比較した結果に応じて、『人差指の位置が間違えています!』『GOOD!』などの判定を表示している。
17.JPG
18.JPG

コード例

Analyze.cs
// FFT分解能(2の累乗)
private static int FFT_RESOLUTION = 2048;
// 採用する最低周波域
private static int FREQUENCY_RANGE = 450;
// 極値を算出する幅
private static int EXTREMUM_RANGE = 5;
// 極小値-極大値の閾値
private static float EXTREMUM_THRESHOLD = 0.0005f;

// a ~ bでの最大値インデックスを返す
public static int GetMaxNo(int a, int b, float[] c)
{
    int maxNum = a;
    float max = c[a];
    for (int i = a; i < a + b && i < c.Length; i++)
    {
        if (c[i] > max)
        {
            max = c[i];
            maxNum = i;
        }
    }
    return maxNum;
}
// a ~ bでの最小値インデックスを返す
public static int GetMinNo(int a, int b, float[] c)
{
    int minNum = a;
    float min = c[a];
    for (int i = a; i < b; i++)
    {
        if (min > c[i])
        {
            min = c[i];
            minNum = i;
        }
    }
    return minNum;
}

// FFT,ピーク値の算出・リターン
public static float[] AnalyzeSound(AudioSource audio)
{
    // FFT
    float[] spectrum = new float[FFT_RESOLUTION];
    audio.GetSpectrumData(spectrum, 0, FFTWindow.BlackmanHarris);
    /*
     * 周波数スペクトル波形をSceneに描画【テスト用】
     *
     * for (int i = 1; i < spectrum.Length - 1; ++i)
     * {
     *     Debug.DrawLine(
     *             new Vector3(Mathf.Log(i - 1), Mathf.Log(spectrum[i - 1]), 3),
     *             new Vector3(Mathf.Log(i), Mathf.Log(spectrum[i]), 3),
     *             Color.yellow);
     * }
     */

    // 極大値インデックスの配列
    int[] maxes = new int[spectrum.Length];
    // 極小値インデックスの配列
    int[] mins = new int[spectrum.Length];
    // ピーク値インデックスの配列
    int[] peaks = new int[spectrum.Length];

    //極大値の探索
    int count = 0;
    for (int i = 0; i < spectrum.Length - EXTREMUM_RANGE; i++)
    {
        if (GetMaxNo(i, EXTREMUM_RANGE, spectrum) == GetMaxNo(i + 1, EXTREMUM_RANGE, spectrum))
        {
            int check = 0;
            for (int k = 1; k < EXTREMUM_RANGE; k++)
            {
                if (GetMaxNo(i, EXTREMUM_RANGE, spectrum) == GetMaxNo(i + k, EXTREMUM_RANGE, spectrum))
                {
                    check++;
                }
            }
            if (check == EXTREMUM_RANGE - 1)
            {
                maxes[count] = GetMaxNo(i, EXTREMUM_RANGE, spectrum);
                count++;
            }
        }   
    }

    //極小値の探索
    mins[0] = GetMinNo(0, maxes[0], spectrum);
    for (int i = 0; i < spectrum.Length; i++)
    {
        if (maxes[i + 1] == 0) break;
        mins[i + 1] = GetMinNo(maxes[i], maxes[i + 1], spectrum);
    }

    //差分の計算
    int peakscnt = 0;
    for (int i = 0; i < spectrum.Length; i++)
    {
        if (spectrum[maxes[i]] - spectrum[mins[i]] >= EXTREMUM_THRESHOLD)
        {
            peaks[peakscnt] = maxes[i];
            peakscnt++;
        }
    }

    // ピーク周波数を返す
    // ピーク周波数インデックスの配列
    float[] freqs = new float[peakscnt];
    // ピーク周波数
    float[] pitches = new float[peakscnt];
    // 各ピークの前後のスペクトルも考慮
    for (int i = 0; i < peakscnt; i++)
    {
        freqs[i] = peaks[i];
        if (peaks[i] > 0 && peaks[i] < spectrum.Length - 1)
        {
            float dL = spectrum[peaks[i] - 1] / spectrum[peaks[i]];
            float dR = spectrum[peaks[i] + 1] / spectrum[peaks[i]];

            freqs[i] += 0.5f * (dR * dR - dL * dL);
        }
        pitches[i] = freqs[i] * (AudioSettings.outputSampleRate / 2) / spectrum.Length;
        // 検出する周波数域を考慮
        if (pitches[i] >= FREQUENCY_RANGE) pitches[i] = 0;
    }
    return pitches;
}

// 周波数から音階に変換
public static string ConvertHertzToScale(float hertz)
{
    // 周波数を,C2を0,(中略),C3を12とする数値に変換
    float scale = 12.0f * Mathf.Log(hertz / 65.5f) / Mathf.Log(2.0f);
    // 四捨五入
    int s = (int)scale;
    if (scale - s >= 0.5) s += 1;

    int smod = s % 12; // 音階
    int soct = s / 12; // オクターブ

    string value; // 音階
    if (smod == 0) value = "C";
    else if (smod == 1) value = "C#";
    else if (smod == 2) value = "D";
    else if (smod == 3) value = "D#";
    else if (smod == 4) value = "E";
    else if (smod == 5) value = "F";
    else if (smod == 6) value = "F#";
    else if (smod == 7) value = "G";
    else if (smod == 8) value = "G#";
    else if (smod == 9) value = "A";
    else if (smod == 10) value = "A#";
    else if (smod == 11) value = "B";
    else value = "EXCEPTION";
    value += soct + 2;

    return value;
}

void Start () {
        // マイク入力
        Mic.clip = Microphone.Start(null, true, 999, 44100);
        while (!(Microphone.GetPosition(null) > 0)) { }
        Mic.Play();
}

void Update () {
        // FFT,ピーク値の算出・リターン
        float[] hertz = AnalyzeSound(Mic);
        // 各ピーク周波数に対して
        for (int i = 0; i < hertz.Length; i++)
        {
            if (hertz[i] == 0) break;
            // 周波数から音階に変換
            string scale = ConvertHertzToScale(hertz[i]);
            /* 演奏された音の周波数と音階を描画【テスト用】
             * Debug.Log(hertz[i] + "Hz, Scale:" + scale);
             */
        }
}

おわりに

今回は、FFTで出力した周波数スペクトラムのピーク値の組み合わせから、演奏の正誤を判定する手法を提示した。
正直なところ、判定性能は満足のいくものではない。マイクの設置する位置や指向性、性能に依存することが主な要因である。シンセサイザを噛ませたり、電気信号的に判定するといったハードウェアによる改善は比較的容易であるが、できればシステマチックに解決する手法を模索してみたい。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む