レベル作成ツールを作ってみる

この記事は[デレステのノーツを再現してみた #2](https://qiita.com/Magiqych/items/f35790add3ebaf234b0c)の続編です。
## Gnuderella
![gnu3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423236/158440ce-3ef8-c5d7-7d6b-6c7dcd095d8a.png)

https://github.com/Magiqych/Gnuderella

## テクスチャ方式の限界
Master/Master+を嗜むPならわかるだろうが交差するノーツというものが存在する。
ゲームプレイをしているうちに理解してきたが例えば一番右の同じ箇所（生成レーン）からロングノーツが左の2レーンに降ってくる場合などがあり、やはり単純なビットマップデータではなくてノーツ生成座標とタップ領域座標をもつベクターデータで保持しないと再現できないことに気がついた。

## レベル作成ツールを作成しながらデレステの譜面のデータ構造を考える
![タイトルなし.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423236/6e269386-2db1-003a-a2e2-42bda09621a9.png)

スコア(譜面)を表現として

* 譜面はノーツグループの集合体である
ここでいうノーツグループとは連結フリックや紫のやつのことをいう、シングルノーツは１つしかノーツを含まないノーツグループとして表現する

* ノーツグループはノーツの集合体である
ノーツにはタップタイミング、生成レーン、タップレーンを保持させておく、Vector3は後でよしなにできるように準備してあるがレベル作成ツールでは使わない

書いていて思ったが、ノーツにはタイプも必要である(シングル、ロング、上下左右、むらさき)
あとは脳死でdoubleつかってるがなんか色々型キャスト面倒だしfloatでいいんじゃね説が私の中ではある。

```c# ScoreProject.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;

namespace ScoreEditor.Models
{
    /// <summary>
    /// 譜面を表すクラス
    /// </summary>
    internal class Score
    {
        /// <summary>
        /// 譜面はNotesGroupのリストで構成される
        /// </summary>
        public List<NotesGroup> NotesGroupList { get; set; }

        /// <summary>
        /// 正規化されたノーツ速度
        /// </summary>
        public double NormalizedNotesSpeed { get; set; }

        /// <summary>
        /// ノーツ速度
        /// </summary>
        public double NotesSpeed { get; set; }

        /// <summary>
        /// ノーツ速度比率
        /// </summary>
        public double NotesSpeedRatio { get; set; }

        /// <summary>
        /// レーン数
        /// </summary>
        public int LaneCount { get; set; }

        /// <summary>
        /// BPM
        /// </summary>
        public double StaticTempo { get; set; }

        /// <summary>
        /// Static Beats List
        /// </summary>
        public List<double> BeatsStatic { get; set; }

        /// <summary>
        /// Dynamic Beats List
        /// </summary>
        public List<double> BeatsDynamic { get; set; }

        /// <summary>
        /// 曲の長さ
        /// </summary> 
        public double Duration { get; set; }

        /// <summary>
        /// Score クラスの新しいインスタンスを初期化します。
        /// </summary>
        public Score(List<NotesGroup> NotesGroupList,
            double normalizedNotesSpeed,
            double notesSpeed)
        {
            NotesGroupList = new List<NotesGroup>();
            NotesGroupList.AddRange(NotesGroupList);
            NormalizedNotesSpeed = normalizedNotesSpeed;
            NotesSpeed = notesSpeed;
            NotesSpeedRatio = NotesSpeed / NormalizedNotesSpeed;
        }

        public Score(int laneCount,
            double normalizedNotesSpeed,
            double notesSpeed)
        {
            NotesGroupList = new List<NotesGroup>();
            LaneCount = laneCount;
            NormalizedNotesSpeed = normalizedNotesSpeed;
            NotesSpeed = notesSpeed;
            NotesSpeedRatio = NotesSpeed / NormalizedNotesSpeed;
        }

        /// <summary>
        /// Score クラスの新しいインスタンスを初期化します。
        /// </summary>
        /// <param name="normalizedNotesSpeed"></param>
        /// <param name="notesSpeed"></param>
        public Score(double normalizedNotesSpeed,
            double notesSpeed)
        {
            NotesGroupList = new List<NotesGroup>();
            NormalizedNotesSpeed = normalizedNotesSpeed;
            NotesSpeed = notesSpeed;
            NotesSpeedRatio = NotesSpeed / NormalizedNotesSpeed;
        }

        /// <summary>
        /// Score クラスの新しいインスタンスを初期化します。
        /// </summary>
        public Score()
        {
            NotesGroupList = new List<NotesGroup>();
            NormalizedNotesSpeed = Constants.NormalizedNotesSpeed;
            NotesSpeed = Constants.NormalizedNotesSpeed;
            NotesSpeedRatio = Constants.NormalizedNotesSpeed / Constants.NormalizedNotesSpeed;
        }
    }

    internal class NotesGroup
    {
        /// <summary>
        /// ノーツグループはNotesのリストで構成される
        /// </summary>
        public List<Notes> NotesList { get; set; }

        /// <summary>
        /// NotesGroup クラスの新しいインスタンスを初期化します。
        /// </summary>
        public NotesGroup()
        {
            NotesList = new List<Notes>();
        }

        public List<Notes> CalcNotesTiming(double notesSpeedRatio)
        {
            List<Notes> notesList = new List<Notes>();
            foreach (var notes in NotesList)
            {
                Notes newNotes = new Notes();
                notesList.Add(newNotes);
            }
            return notesList;
        }

    }

    internal class Notes
    {
        /// <summary>
        /// 描写フラグ
        /// </summary>
        public bool IsVisible { get; set; }
        /// <summary>
        /// 正規化されたタイミング変数
        /// </summary>
        public double NormalizedTiming { get; set; }
        /// <summary>
        /// タイミング変数
        /// </summary>
        public double Timing { get; set; }
        /// <summary>
        /// ノーツ生成レーンインデックス
        /// </summary>
        public int SpawnLaneIndex { get; set; }
        /// <summary>
        /// ノーツ到達レーンインデックス
        /// </summary>
        public int DestLaneIndex { get; set; }
        /// <summary>
        /// ノーツ生成座標
        /// </summary>
        public Vector3 SpawnPos { get; set; }
        /// <summary>
        /// ノーツ到達座標
        /// </summary>
        public Vector3 DestPos { get; set; }

    }
}
```


## BPM解析(動的BPMと静的BPM)

https://librosa.org/doc/latest/index.html

個人開発なのでなにもC#で完結する必要は一切ない、使えるものは使う、これが私のモットーである。
plt.savefigで保存した画像なのだが使用しないことになったのでこれも書いていて気がついたが不要だなjsonにデータを保存できれば良い。

```python musicPy.py
import json
import os
import sys
import librosa
import librosa.display
import numpy as np
import matplotlib.pyplot as plt

def process_music(ScoreProjectPath, ScorePath):
    # ScoreProjectファイルを読み込む
    ScoreProject = None
    Score = None
    with open(ScoreProjectPath, 'r', encoding='utf-8') as file:
        ScoreProject = json.load(file)
    with open(ScorePath, 'r', encoding='utf-8') as file:
        Score = json.load(file)

    # Estimate a static tempo
    music_path =  ScoreProject.get('MusicFilePath')
    y, sr = librosa.load(music_path)
    duration = librosa.get_duration(y=y, sr=sr)
    tempo, beats_static = librosa.beat.beat_track(y=y, sr=sr, units='time', trim=False)
    tempo_dynamic = librosa.feature.tempo(y=y, sr=sr, aggregate=None, start_bpm=tempo[0])
    tempo, beats_dynamic = librosa.beat.beat_track(y=y, sr=sr, units='time',bpm=tempo_dynamic, trim=False)
    Score['StaticTempo'] = tempo[0]
    Score['BeatsDynamic'] = beats_dynamic.tolist()
    Score['BeatsStatic'] = beats_static.tolist()
    Score['Duration'] = duration
    # Save Score
    with open(ScorePath, 'w', encoding='utf-8') as file:
        json.dump(Score, file, ensure_ascii=False, indent=4)

    # 画像を出力
    fig, ax = plt.subplots(figsize=(10, duration))  # 画像のサイズを設定
    fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
    ax.set_xlim([0, 1080])
    ax.set_ylim([0, duration])
    ax.axis('off')  # 軸を非表示にする
    for beat in beats_static:
        ax.axhline(y=beat, color='r')  # Y軸方向に平行な線を引く
    output_path = os.path.join(os.path.dirname(ScoreProjectPath), 'BeatsStatic.png')
    plt.savefig(output_path, bbox_inches='tight', pad_inches=0)

    fig, ax = plt.subplots(figsize=(10, duration))  # 画像のサイズを設定
    fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
    ax.set_xlim([0, 1080])
    ax.set_ylim([0, duration])
    ax.axis('off')  # 軸を非表示にする
    for beat in beats_dynamic:
        ax.axhline(y=beat, color='b')  # Y軸方向に平行な線を引く
    output_path = os.path.join(os.path.dirname(ScoreProjectPath), 'BeatsDynamic.png')
    plt.savefig(output_path, bbox_inches='tight', pad_inches=0)

def main():
    if len(sys.argv) != 2:
        print("Usage: python MusicPy.py <ScoreProjectRootPath>")
        sys.exit(1)
    ScoreProjectRootPath = sys.argv[1]
    ScoreProjectPath = os.path.join(ScoreProjectRootPath, 'ScoreProject.json')
    ScorePath = os.path.join(ScoreProjectRootPath, 'Score.json')
    process_music(ScoreProjectPath, ScorePath)

if __name__ == "__main__":
    main()
```

また寝たらなんか思いつくかもしれないので寝ることにする。