5.7 CROSSO的採点機
CROSSO風な採点機が完成した。
音程
瞬間瞬間で、歌唱した数値音階と、その時出すべき数値音階を渡して評価する。
ぴったりじゃなくても部分点が入るようにしている。
タイミング
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で反応テクニックを表示させるのが一番苦労した。
もちろん、こんな採点機はゴミも同然なので、今からオリジナルの採点機を作る。間に合うのかな