技術的なやつ

技術的なやつ

5.7 CROSSO的採点機

  1. Unityを用いる
    1. C#について少し復習する
    2. Unityの2D描画について簡単に学ぶ
  2. Unityの音声制御
    1. MIDIを再生させる手段を見つける
    2. 音声のマイク入力手段を見つける
  3. 入力音声の解析
    1. UnityのFFTライブラリを触ってみる
    2. FFTされたデータから音程を確定させるアルゴリズムを考える
  4. MIDIの解析
    1. 第1トラックの楽譜を解析する
    2. MIDIの再生箇所を解析する手段を見つける
  5. 採点のための解析
    1. 全体の点数計算アルゴリズムを考える
    2. ビブラート判定アルゴリズムを考える
    3. しゃくり判定アルゴリズムを考える
    4. タイミング判定アルゴリズムを考える
    5. なめらかさ判定アルゴリズムを考える

f:id:ibako31:20140303005448j:plain

CROSSO風な採点機が完成した。

配点

CROSSOなので、こんな感じ。

  • 音程…90
    • ただし、線形配点ではない
  • タイミング…2
  • なめらかさ…1
  • ビブラート…7
  • しゃくり…3

音程

瞬間瞬間で、歌唱した数値音階と、その時出すべき数値音階を渡して評価する。
ぴったりじゃなくても部分点が入るようにしている。

タイミング

1つのノートが終わるごとに評価する。
4分音符以下の長さの音程バーのどこかで、正しい音程を発していればOK。
音程バーにはそれぞれタグが付いており、タグの数値はx分音符を意味する。たとえば、四分音符なら4、全音符なら1、タイで2小節にまたがる全音符・全音符なら0.5。

なめらかさ

2つのノートが終わるごとに評価する(事実的に1つのノートが終わるごとに評価する)。
2つのノートの間で何らかの音を発していればOK。階段上に音が続く部分では特に大きく評価する。

ビブラート

1つのノートが終わるごとに評価する。
そのノート中に波が2つ以上連続して存在すればOK。
sin的な波と-sin的な波の2つを想定する。評価ポイントは、sin(x)でいうとx=0,PI/2,PI,3PI/2,2PI。横幅および縦幅に制限を設け、それを満たしていればOKとする。

しゃくり

1つのノートが終わるごとに評価する。
ノート中に、「本来の音程より低い音程をm秒連続で発し、その後正しい音程をn秒保った」場合OK。

コード

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CROSSO
{
	class Evaluator
	{
        private int all_time;  // 歌唱しなければならない全時間(10ms)
        private int time;  // 歌唱時間(10ms)
        class Interval
        {
            private int interval_score;  // 音程点。満点なら1回の評価につき5点入る
            private int interval_num;  // 評価した音程の数
            
            // 初期化処理
            public Interval()
            {
                interval_score = 0;
                interval_num = 0;
            }

            // 音程を1回評価する
            // scale: 出した数値音階、best_scale: 出すべき数値音階
            public void Evaluate(float scale, float best_scale)
            {
                float dif = Math.Abs(scale - best_scale);
                interval_num += 1;
                if (scale == 0.0f)
                {
                    interval_score += 2;
                }
                else if (dif < 0.7f)
                {
                    interval_score += 5;
                }
                else if (dif < 1.0f)
                {
                    interval_score += 4;
                }
                else if (dif < 1.5f)
                {
                    interval_score += 3;
                }
                else if (dif < 2.0f)
                {
                    interval_score += 2;
                }
                else
                {
                    interval_score += 1;
                }
            }

            // 音程の評価値を返す
            public float GetScore()
            {
                if (interval_num == 0)
                {
                    return 0.0f;
                }
                else
                {
                    return 100.0f * (interval_score / 5.0f / interval_num);
                }
            }
        }


        class Timing
        {
            private int timing_score;  // 評価点。0か1か
            private int timing_num;  // 評価したタイミングの数
            
            // 初期化処理
            public Timing()
            {
                timing_score = 0;
                timing_num = 0;
            }

            // タイミングを1回評価する
            // scale: 出した数値音階、best_scale: 出すべき数値音階、start_index: 始まりのインデックス番号、index_size: その音符のインデックス長さ
            public void Evaluate(float[] scale, float best_scale, int start_index, int index_size)
            {
                timing_num += 1;
                for (int i = 0; i < index_size; i++)
                {
                    float dif = Math.Abs(scale[(start_index + i) % scale.Length] - best_scale);
                    if (scale[i] != 0.0f)
                    {
                        if (dif < 0.5f)
                        {
                            timing_score += 1;
                            break;
                        }
                    }
                }
            }

            // タイミングの評価値を返す
            public float GetScore()
            {
                if (timing_num == 0)
                {
                    return 0.0f;
                }
                else
                {
                    return 100.0f * (timing_score / 1.0f / timing_num);
                }
            }
        }


        class Smoothness
        {
            private int smoothness_score;  // 評価点。0~5
            private int smoothness_num;  // 評価したなめらかさの数
        
            // 初期化処理
            public Smoothness()
            {
                smoothness_score = 0;
                smoothness_num = 0;
            }

            // なめらかさを1回評価する(合わせて全音符以下の長さの、2つ分のノートを評価する)
            // scale: 出した数値音階、best_scale: 出すべき数値音階(2つ)、start_index: 始まりのインデックス番号、index_size: インデックス長さ、border_index: 境界のインデックス
            public void Evaluate(float[] scale, float[] best_scale, int start_index, int index_size, int border_index)
            {
                smoothness_num += 1;
                for (int i = 0; i < index_size; i++)
                {
                    int index = (start_index + i) % scale.Length;
                    if (index == border_index % scale.Length)
                    {
                        if (scale[index] != 0.0f)
                        {
                            float dif = Math.Abs(best_scale[0] - best_scale[1]);
                            if (dif == 0.0f) smoothness_score += 1;
                            if (dif <= 2.0f) smoothness_score += 5;
                            else if (dif <= 5.0f) smoothness_score += 3;
                            else smoothness_score += 1;
                        }
                        else
                        {
                            smoothness_score += 0;
                        }
                        break;
                    }
                }
            }

            // なめらかさの評価値を返す
            public float GetScore()
            {
                if (smoothness_num == 0)
                {
                    return 0.0f;
                }
                else
                {
                    return 100.0f * (smoothness_score / 3.0f / smoothness_num);
                }
            }
        }


        class Vibrato
        {
            private int vibrato_score;  // 評価点。感知できたノート数
            private int vibrato_num;  // 評価したノートの数
            private int time;  // 歌唱時間
            
            // 初期化処理
            public Vibrato()
            {
                vibrato_score = 0;
                vibrato_num = 0;
                time = 0;
            }

            // ビブラートを1回評価する
            // scale: 出した数値音階、start_index: 始まりのインデックス番号、index_size: その音符のインデックス長さ、time: 歌唱時間
            // 戻り値: 0-> 感知されなかった、1-> 感知された
            public int Evaluate(float[] scale, int start_index, int index_size, int time)
            {
                this.time = time;
                const int necessary_wave_num = 2;  // 感知に必要な波の数
                const int limit_time = 50;  // ビブラート感知の限界時間(*10ms)
                const float limit_height_min = 0.5f;  // ビブラート感知可能な限界最低縦幅(数値音階)
                vibrato_num += 1;
                int break_flag = 0;

                for (int i = 0; i < index_size; i++)
                {
                    int base_index = (start_index + i) % scale.Length;  // ビブラートの起点とするインデックス
                    float base_scale = scale[base_index];  // 起点の数値音階
                    // 発音されてない場合は無効
                    if (scale[base_index] == 0.0f) continue;

                    // 感知の段階。n:now, b:base_scale, h:limit_height_min
                    // mod 4 について。必要感知波数を変えられるように。 
                    // 0-> n >= b、 1-> n >= b+h、 2-> n <= b、 3-> n <= b-h
                    // 0-> n <= b、-1-> n <= b-h、-2-> n >= b、-3-> n <= b-h
                    int process = 0;

                    for (int j = 1; j < limit_time && i + j < index_size; j++)
                    {
                        int index = (base_index + j) % scale.Length;
                        float dif = scale[index] - base_scale;  // 起点の数値音階との差
                        int process_mod = Math.Abs(process) % 4;

                        // 発音されてない場合は無効
                        if (scale[index] == 0.0f) break;

                        // sin的な波
                        if (process >= 0)
                        {
                            if (process_mod == 0)
                            {
                                if (dif >= limit_height_min) process++;
                            }
                            else if (process_mod == 1)
                            {
                                if (dif <= 0) process++;
                            }
                            else if (process_mod == 2)
                            {
                                if (dif <= -limit_height_min) process++;
                            }
                            else
                            {
                                if (dif >= 0) process++;
                            }
                        }
                        // -sin的な波
                        else
                        {
                            if (process_mod == 0)
                            {
                                if (dif <= -limit_height_min) process--;
                            }
                            else if (process_mod == 1)
                            {
                                if (dif >= 0) process--;
                            }
                            else if (process_mod == 2)
                            {
                                if (dif >= limit_height_min) process--;
                            }
                            else
                            {
                                if (dif <= 0) process--;
                            }
                        }

                        // 波を2つ感知できていたらOK!
                        if (Math.Abs(process) == 4 * necessary_wave_num)
                        {
                            vibrato_score++;
                            break_flag = 1;
                            break;
                        }
                    }
                    if (break_flag == 1) break;
                }

                if (break_flag == 0) return 0;
                else return 1;
            }

            // ビブラートの評価値を返す
            public float GetScore()
            {
                if (time == 0)
                {
                    return 0.0f;
                }
                else
                {
                    return 100.0f * (3.0f * (float)vibrato_score / ((float)time / 100.0f));
                }
            }
        }


        class Singup
        {
            private int singup_score;  // 評価点。感知できたノート数
            private int singup_num;  // 評価したノートの数
            private int time;  // 歌唱時間
            
            // 初期化処理
            public Singup()
            {
                singup_score = 0;
                singup_num = 0;
                time = 0;
            }

            // しゃくりを1回評価する
            // scale: 出した数値音階、best_scale: 出すべき数値音階、start_index: 始まりのインデックス番号、index_size: その音符のインデックス長さ、time: 歌唱時間
            // 戻り値: 0-> 感知されなかった、1-> 感知された
            public int Evaluate(float[] scale, float best_scale, int start_index, int index_size, int time)
            {
                this.time = time;
                const int necessary_time = 5;  // しゃくり感知のために必要な時間(*10ms)
                const int limit_time = 30;  // しゃくり感知の限界時間(*10ms)
                const int necessary_stable_time = 5; // しゃくり感知のために必要な、正しい音程での安定時間(*10ms)
                singup_num += 1;

                int ok = 0;  // しゃくり感知したら1
                int process = -1;  // -1:音程が下になるまで待つ、0:音程のずり上がり中、それ以外:正しい音程での安定中(数値は正しい音程に達した時間)
                int start_time = 0;  // 低音を出し始めた時間

                for (int i = 0; i < index_size && i < limit_time; i++)
                {
                    int index = (start_index + i) % scale.Length;  // チェックするインデックス
                    if (process == -1 && scale[index] == 0.0f) break;  // 発音していない部分があれば終了
                    else if (best_scale - 0.5f <= scale[index] && scale[index] <= best_scale + 0.5f)  // 正しい音程
                    {
                        if (process == 0)
                        {
                            if (i - start_time >= necessary_time - 1)
                            {
                                process = i;
                            }
                            else if (i - start_time >= limit_time)
                            {
                                break;
                            }
                        }
                        else if (process > 0)
                        {
                            if (i - process >= necessary_stable_time)
                            {
                                singup_score++;
                                ok = 1;
                                break;
                            }
                        }    
                    }
                    else if (process > 0) break;  // 一旦正しい音程に達したのに、安定して正しい音程が出せなかった
                    else if (scale[index] < best_scale - 0.5f)  // 正しい音程より低い音を出している
                    {
                        process = 0;
                        start_time = i;
                    }
                    else break;
                }

                return ok;
            }

            // しゃくりの評価値を返す
            public float GetScore()
            {
                if (time == 0)
                {
                    return 0.0f;
                }
                else
                {
                    return 100.0f * (3.0f * (float)singup_score / ((float)time / 100.0f));
                }
            }
        }


        private Interval interval;
        private Timing timing;
        private Smoothness smoothness;
        private Vibrato vibrato;
        private Singup singup;


        // 最初に実行する
        public Evaluator(int all_time)
        {
            this.all_time = all_time;
            time = 0;
            interval = new Interval();
            timing = new Timing();
            smoothness = new Smoothness();
            vibrato = new Vibrato();
            singup = new Singup();
        }

        // 音程評価のために実行する。瞬間瞬間で回す
        // 歌唱時間の計算も行う
        // scale: 出した数値音階、best_scale: 出すべき数値音階
        public void EvaluateInterval(float scale, float best_scale)
        {
            interval.Evaluate(scale, best_scale);
            time += 1;
        }

        // タイミング評価のために実行する。1つのノートが終わったら実行する
        // scale: 出した数値音階の配列、best_scale: 出すべき数値音階、start_index: 出した数値音階の配列のスタートインデックス、index_size: 評価すべきインデックス数
        public void EvaluateTiming(float[] scale, float best_scale, int start_index, int index_size)
        {
            timing.Evaluate(scale, best_scale, start_index, index_size);
        }

        // なめらかさ評価のために実行する。2つのノートが終わったら実行する
        // scale: 出した数値音階の配列、best_scale: 出すべき数値音階(2つ)、start_index: 出した数値音階の配列のスタートインデックス、index_size: 評価すべきインデックス数、border_index: 境界インデックス
        public void EvaluateSmoothness(float[] scale, float[] best_scale, int start_index, int index_size, int border_index)
        {
            smoothness.Evaluate(scale, best_scale, start_index, index_size, border_index);
        }

        // ビブラート評価のために実行する。1つのノートが終わったら実行する
        // scale: 出した数値音階の配列、start_index: 出した数値音階の配列のスタートインデックス、index_size: 評価すべきインデックス数
        // 戻り値: 0-> 感知されなかった、1-> 感知された
        public int EvaluateVibrato(float[] scale, int start_index, int index_size)
        {
            return vibrato.Evaluate(scale, start_index, index_size, time);
        }

        // しゃくり評価のために実行する。1つのノートが終わったら実行する
        // scale: 出した数値音階の配列、best_scale: 出すべき数値音階、start_index: 出した数値音階の配列のスタートインデックス、index_size: 評価すべきインデックス数
        // 戻り値: 0-> 感知されなかった、1-> 感知された
        public int EvaluateSingup(float[] scale, float best_scale, int start_index, int index_size)
        {
            return singup.Evaluate(scale, best_scale, start_index, index_size, time);
        }

        // 評価値を得る
        // id -> 0: 総合, 1: 音程, 2: タイミング, 3: なめらかさ, 4: ビブラート, 5: しゃくり
        public float GetScore(int id)
        {
            if (id == 0)
            {
                float interval_score = interval.GetScore();
                float timing_score = 2.0f * timing.GetScore() / 100.0f;
                float smoothness_score = smoothness.GetScore();
                float vibrato_score = vibrato.GetScore();
                float singup_score = singup.GetScore();

                if (interval_score >= 95.0f) interval_score = 88.0f + 2.0f * (interval_score - 95.0f) / 5.0f;
                else if (interval_score >= 90.0f) interval_score = 87.0f + 1.0f * (interval_score - 90.0f) / 5.0f;
                else if (interval_score >= 85.0f) interval_score = 85.0f + 2.0f * (interval_score - 85.0f) / 5.0f;
                else if (interval_score >= 80.0f) interval_score = 83.0f + 2.0f * (interval_score - 80.0f) / 5.0f;
                else interval_score = 0.0f + 83.0f * (interval_score - 0.0f) / 80.0f;

                if (smoothness_score > 100.0f) smoothness_score = 1.0f;
                else smoothness_score = 1.0f * smoothness_score / 100.0f;

                if (vibrato_score > 100.0f) vibrato_score = 7.0f;
                else vibrato_score = 7.0f * vibrato_score / 100.0f;

                if (singup_score > 100.0f) singup_score = 3.0f;
                else singup_score = 3.0f * singup_score / 100.0f;

                return (interval_score + timing_score + smoothness_score + vibrato_score + singup_score);
            }
            else if (id == 1) return interval.GetScore();
            else if (id == 2) return timing.GetScore();
            else if (id == 3) return smoothness.GetScore();
            else if (id == 4) return vibrato.GetScore();
            else return singup.GetScore();
        }

        // デバッグ用
        public void Debug()
        {
            UnityEngine.Debug.Log("all_time->" + all_time + ", time->" + time);
        }
    }
}

感想

本来の黒よりテクニック評価が厳しいので、なかなかつらい採点になっている。
GUI.DrawTextureで反応テクニックを表示させるのが一番苦労した。
もちろん、こんな採点機はゴミも同然なので、今からオリジナルの採点機を作る。間に合うのかな