OSDN Git Service

dbe77dfd820871f3dbf047d524b7806c109cad5b
[strokestylet/CsWin10Desktop3.git] / SSTFormat / スコア.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Diagnostics;
4 using System.IO;
5 using System.Linq;
6 using System.Text;
7 using System.Text.RegularExpressions;
8 using FDK;      // for string 拡張
9
10 namespace SSTFormat
11 {
12         public class スコア : IDisposable
13         {
14                 // ヘルパ
15
16                 /// <summary>
17                 /// 指定されたコマンド名が対象文字列内で使用されている場合に、パラメータ部分の文字列を返す。
18                 /// </summary>
19                 /// <remarks>
20                 /// .dtx や box.def 等で使用されている "#<コマンド名>[:]<パラメータ>[;コメント]" 形式の文字列(対象文字列)について、
21                 /// 指定されたコマンドを使用する行であるかどうかを判別し、使用する行であるなら、そのパラメータ部分の文字列を引数に格納し、true を返す。
22                 /// 対象文字列のコマンド名が指定したコマンド名と異なる場合には、パラメータ文字列に null を格納して false を返す。
23                 /// コマンド名は正しくてもパラメータが存在しない場合には、空文字列("") を格納して true を返す。
24                 /// </remarks>
25                 /// <param name="対象文字列">調べる対象の文字列。(例: "#TITLE: 曲名 ;コメント")</param>
26                 /// <param name="コマンド名">調べるコマンドの名前(例:"TITLE")。#は不要、大文字小文字は区別されない。</param>
27                 /// <returns>パラメータ文字列の取得に成功したら true、異なるコマンドだったなら false。</returns>
28                 public static bool コマンドのパラメータ文字列部分を返す( string 対象文字列, string コマンド名, out string パラメータ文字列 )
29                 {
30                         // コメント部分を除去し、両端をトリムする。なお、全角空白はトリムしない。
31                         対象文字列 = 対象文字列.Split( ';' )[ 0 ].Trim( ' ', '\t' );
32
33                         string 正規表現パターン = $@"^\s*#{コマンド名}(:|\s)+(.*)\s*$";  // \s は空白文字。
34                         var m = Regex.Match( 対象文字列, 正規表現パターン, RegexOptions.IgnoreCase );
35
36                         if( m.Success && ( 3 <= m.Groups.Count ) )
37                         {
38                                 パラメータ文字列 = m.Groups[ 2 ].Value;
39                                 return true;
40                         }
41                         else
42                         {
43                                 パラメータ文字列 = null;
44                                 return false;
45                         }
46                 }
47
48                 // 定数プロパティ
49
50                 public const double 初期BPM = 120.0;
51                 public const double 初期小節解像度 = 480.0;
52                 public const double BPM初期値固定での1小節4拍の時間ms = ( 60.0 * 1000 ) / ( スコア.初期BPM / 4.0 );
53                 public const double BPM初期値固定での1小節4拍の時間sec = 60.0 / ( スコア.初期BPM / 4.0 );
54
55                 /// <summary>
56                 /// 1ms あたりの設計ピクセル数 [dpx] 。
57                 /// </summary>
58                 /// <remarks>
59                 /// BPM 150 のとき、1小節が 234 dpx になるように調整。
60                 /// → 60秒で150拍のとき、1小節(4拍)が 234 dpx。
61                 /// → 60秒の間に、150[拍]÷4[拍]=37.5[小節]。
62                 /// → 60秒の間に、37.5[小節]×234[dpx/小節]= 8775[dpx]。
63                 /// → 1ms の間に、8775[dpx]÷60000[ms]=0.14625[dpx/ms]。割り切れて良かった。
64                 /// </remarks>
65                 public const double 基準譜面速度dpxms = 0.14625 * 2.25;  // "* 2.25" は「x1.0はもう少し速くてもいいんではないか?」という感覚的な調整分。
66
67                 /// <summary>
68                 /// 1秒あたりの設計ピクセル数 [dpx] 。
69                 /// </summary>
70                 public const double 基準譜面速度dpxsec = 基準譜面速度dpxms * 1000.0;
71
72                 public static readonly Dictionary<レーン種別, List<チップ種別>> dicSSTFレーンチップ対応表
73                         #region " *** "
74                         //-----------------
75                         = new Dictionary<レーン種別, List<チップ種別>>() {
76                                 { レーン種別.Bass, new List<チップ種別>() { チップ種別.Bass } },
77                                 { レーン種別.BPM, new List<チップ種別>() { チップ種別.BPM } },
78                                 { レーン種別.China, new List<チップ種別>() { チップ種別.China } },
79                                 { レーン種別.HiHat, new List<チップ種別>() { チップ種別.HiHat_Close, チップ種別.HiHat_Foot, チップ種別.HiHat_HalfOpen, チップ種別.HiHat_Open } },
80                                 { レーン種別.LeftCrash, new List<チップ種別>() { チップ種別.LeftCrash } },
81                                 { レーン種別.Ride, new List<チップ種別>() { チップ種別.Ride, チップ種別.Ride_Cup } },
82                                 { レーン種別.RightCrash, new List<チップ種別>() { チップ種別.RightCrash } },
83                                 { レーン種別.Snare, new List<チップ種別>() { チップ種別.Snare, チップ種別.Snare_ClosedRim, チップ種別.Snare_Ghost, チップ種別.Snare_OpenRim } },
84                                 { レーン種別.Song, new List<チップ種別>() { チップ種別.背景動画 } },
85                                 { レーン種別.Splash, new List<チップ種別>() { チップ種別.Splash } },
86                                 { レーン種別.Tom1, new List<チップ種別>() { チップ種別.Tom1, チップ種別.Tom1_Rim } },
87                                 { レーン種別.Tom2, new List<チップ種別>() { チップ種別.Tom2, チップ種別.Tom2_Rim } },
88                                 { レーン種別.Tom3, new List<チップ種別>() { チップ種別.Tom3, チップ種別.Tom3_Rim } },
89                         };
90                         //-----------------
91                         #endregion
92
93                 // 背景動画のデフォルト拡張子
94                 public static readonly List<string> 背景動画のデフォルト拡張子s = new List<string>() {
95                         ".avi", ".flv", ".mp4", ".wmv", ".mpg", ".mpeg"
96                 };
97
98                 // プロパティ;読み込み時または編集時に設定される
99
100                 /// <remarks>
101                 /// 背景動画ファイル名は、sstf ファイルには保存されず、必要時に sstf ファイルと同じフォルダを検索して取得する。
102                 /// </remarks>
103                 public string 背景動画ファイル名 = "";
104
105                 public class CHeader
106                 {
107                         public string 曲名 = "(no title)";
108                         public string 説明文 = "";
109                         public float サウンドデバイス遅延ms = 0f;
110                 }
111                 public CHeader Header = new CHeader();
112
113                 public List<チップ> チップリスト
114                 {
115                         get;
116                         protected set;
117                 }
118                 public List<double> 小節長倍率リスト
119                 {
120                         get;
121                         protected set;
122                 }
123                 public int 最大小節番号
124                 {
125                         get
126                         {
127                                 int n最大小節番号 = 0;
128
129                                 foreach( チップ chip in this.チップリスト )
130                                 {
131                                         if( chip.小節番号 > n最大小節番号 )
132                                                 n最大小節番号 = chip.小節番号;
133                                 }
134
135                                 return n最大小節番号;
136                         }
137                 }
138                 public Dictionary<int, string> dicメモ = new Dictionary<int, string>();
139
140                 // メソッド
141
142                 public スコア()
143                 {
144                         this.チップリスト = new List<チップ>();
145                         this.小節長倍率リスト = new List<double>();
146                 }
147                 public スコア( string 曲データファイル名, bool 左ライド = false, bool 左チャイナ = false, bool 左スプラッシュ = true ) : this()
148                 {
149                         this.曲データファイルを読み込む( 曲データファイル名, 左ライド, 左チャイナ, 左スプラッシュ );
150                 }
151                 public void Dispose()
152                 {
153                 }
154
155                 /// <summary>
156                 /// 指定された曲データファイルを読み込む。失敗すれば何らかの例外を発出する。
157                 /// </summary>
158                 public void 曲データファイルを読み込む( string 曲データファイル名, bool 左ライド = false, bool 左チャイナ = false, bool 左スプラッシュ = true )
159                 {
160                         #region " 初期化する。"
161                         //-----------------
162                         this.小節長倍率リスト = new List<double>();
163                         this.dicメモ = new Dictionary<int, string>();
164                         //-----------------
165                         #endregion
166                         #region " 背景動画ファイル名を更新する。"
167                         //----------------
168                         this.背景動画ファイル名 =
169                                 ( from file in Directory.GetFiles( Path.GetDirectoryName( 曲データファイル名 ) )
170                                   where SSTFormat.スコア.背景動画のデフォルト拡張子s.Any( 拡張子名 => ( Path.GetExtension( file ).ToLower() == 拡張子名 ) )
171                                   select file ).FirstOrDefault();
172                    //----------------
173                         #endregion
174                         #region " 曲データファイルを読み込む。"
175                         //-----------------
176                         var sr = new StreamReader( 曲データファイル名, Encoding.UTF8 );
177                         try
178                         {
179                                 int 行番号 = 0;
180                                 int 現在の小節番号 = 0;
181                                 int 現在の小節解像度 = 384;
182                                 チップ種別 e現在のチップ = チップ種別.Unknown;
183
184                                 while( false == sr.EndOfStream )
185                                 {
186                                         // 1行ずつ読み込む。
187                                         行番号++;
188                                         string 行 = sr.ReadLine();
189
190                                         // 前処理。
191
192                                         #region " 改行は ';' に、TABは空白文字にそれぞれ変換し、先頭末尾の空白を削除する。"
193                                         //-----------------
194                                         行 = 行.Replace( Environment.NewLine, ";" );
195                                         行 = 行.Replace( '\t', ' ' );
196                                         行 = 行.Trim();
197                                         //-----------------
198                                         #endregion
199                                         #region " 行中の '#' 以降はコメントとして除外する。また、コメントだけの行はスキップする。"
200                                         //-----------------
201                                         int 区切り位置 = 0;
202                                         for( 区切り位置 = 0; 区切り位置 < 行.Length; 区切り位置++ )
203                                         {
204                                                 if( 行[ 区切り位置 ] == '#' )
205                                                         break;
206                                         }
207                                         if( 0 == 区切り位置 )
208                                                 continue;       // コメントだけの行はスキップ。
209                                         if( 区切り位置 < 行.Length )
210                                         {
211                                                 行 = 行.Substring( 0, 区切り位置 - 1 );
212                                                 行 = 行.Trim();
213                                         }
214                                         //-----------------
215                                         #endregion
216                                         #region " 空行ならこの行はスキップする。"
217                                         //-----------------
218                                         if( 行.Nullまたは空である() )
219                                                 continue;
220                                         //-----------------
221                                         #endregion
222
223                                         // ヘッダコマンド処理。
224
225                                         #region " ヘッダコマンドの処理を行う。"
226                                         //-----------------
227                                         if( 行.StartsWith( "Title", StringComparison.OrdinalIgnoreCase ) )
228                                         {
229                                                 #region " Title コマンド "
230                                                 //-----------------
231                                                 string[] items = 行.Split( '=' );
232
233                                                 if( items.Length != 2 )
234                                                 {
235                                                         FDK.Log.ERROR( $"Title の書式が不正です。スキップします。[{行番号}行目]" );
236                                                         continue;
237                                                 }
238
239                                                 this.Header.曲名 = items[ 1 ].Trim();
240                                                 //-----------------
241                                                 #endregion
242
243                                                 continue;
244                                         }
245                                         if( 行.StartsWith( "Description", StringComparison.OrdinalIgnoreCase ) )
246                                         {
247                                                 #region " Description コマンド "
248                                                 //-----------------
249                                                 string[] items = 行.Split( '=' );
250
251                                                 if( items.Length != 2 )
252                                                 {
253                                                         FDK.Log.ERROR( $"Description の書式が不正です。スキップします。[{行番号}行目]" );
254                                                         continue;
255                                                 }
256
257                                                 // 2文字のリテラル "\n" は改行に復号。
258                                                 this.Header.説明文 = items[ 1 ].Trim().Replace( @"\n", Environment.NewLine );
259                                                 //-----------------
260                                                 #endregion
261
262                                                 continue;
263                                         }
264                                         if( 行.StartsWith( "SoundDevice.Delay", StringComparison.OrdinalIgnoreCase ) )
265                                         {
266                                                 #region " SoundDevice.Delay コマンド "
267                                                 //-----------------
268                                                 string[] items = 行.Split( '=' );
269
270                                                 if( items.Length != 2 )
271                                                 {
272                                                         FDK.Log.ERROR( $"SoundDevice.Delay の書式が不正です。スキップします。[{行番号}行目]" );
273                                                         continue;
274                                                 }
275
276                                                 // 2文字のリテラル "\n" は改行に復号。
277                                                 float value = 0f;
278                                                 if( float.TryParse( items[ 1 ].Trim().Replace( @"\n", Environment.NewLine ), out value ) )
279                                                         this.Header.サウンドデバイス遅延ms = value;
280                                                 //-----------------
281                                                 #endregion
282
283                                                 continue;
284                                         }
285                                         //-----------------
286                                         #endregion
287
288                                         // メモ(小節単位)処理。
289
290                                         #region " メモ(小節単位)の処理を行う。"
291                                         //-----------------
292                                         if( 行.StartsWith( "PartMemo", StringComparison.OrdinalIgnoreCase ) )
293                                         {
294                                                 #region " '=' 以前を除去する。"
295                                                 //-----------------
296                                                 int 等号位置 = 行.IndexOf( '=' );
297                                                 if( 0 >= 等号位置 )
298                                                 {
299                                                         FDK.Log.ERROR( $"PartMemo の書式が不正です。スキップします。[{行番号}]行目]" );
300                                                         continue;
301                                                 }
302                                                 行 = 行.Substring( 等号位置 + 1 ).Trim();
303                                                 if( 行.Nullまたは空である() )
304                                                 {
305                                                         FDK.Log.ERROR( $"PartMemo の書式が不正です。スキップします。[{行番号}]行目]" );
306                                                         continue;
307                                                 }
308                                                 //-----------------
309                                                 #endregion
310                                                 #region " カンマ位置を取得する。"
311                                                 //-----------------
312                                                 int カンマ位置 = 行.IndexOf( ',' );
313                                                 if( 0 >= カンマ位置 )
314                                                 {
315                                                         FDK.Log.ERROR( $"PartMemo の書式が不正です。スキップします。[{行番号}]行目]" );
316                                                         continue;
317                                                 }
318                                                 //-----------------
319                                                 #endregion
320                                                 #region " 小節番号を取得する。"
321                                                 //-----------------
322                                                 string 小説番号文字列 = 行.Substring( 0, カンマ位置 );
323                                                 int 小節番号;
324                                                 if( false == int.TryParse( 小説番号文字列, out 小節番号 ) || ( 0 > 小節番号 ) )
325                                                 {
326                                                         FDK.Log.ERROR( $"PartMemo の小節番号が不正です。スキップします。[{行番号}]行目]" );
327                                                         continue;
328                                                 }
329                                                 //-----------------
330                                                 #endregion
331                                                 #region " メモを取得する。"
332                                                 //-----------------
333                                                 string メモ = 行.Substring( カンマ位置 + 1 );
334
335                                                 // 2文字のリテラル文字列 "\n" は改行に復号。
336                                                 メモ = メモ.Replace( @"\n", Environment.NewLine );
337                                                 //-----------------
338                                                 #endregion
339                                                 #region " メモが空文字列でないなら dicメモ に登録すると同時に、チップとしても追加する。"
340                                                 //-----------------
341                                                 if( メモ.Nullでも空でもない() )
342                                                 {
343                                                         this.dicメモ.Add( 小節番号, メモ );
344
345                                                         this.チップリスト.Add(
346                                                                 new チップ() {
347                                                                         チップ種別 = チップ種別.小節メモ,
348                                                                         小節番号 = 小節番号,
349                                                                         小節内位置 = 0,
350                                                                         小節解像度 = 1,
351                                                                 } );
352                                                 }
353                                                 //-----------------
354                                                 #endregion
355
356                                                 continue;
357                                         }
358                                         //-----------------
359                                         #endregion
360
361                                         // 上記行頭コマンド以外は、チップ記述行だと見なす。
362
363                                         #region " チップ記述コマンドの処理を行う。"
364                                         //-----------------
365
366                                         // 行を区切り文字でトークンに分割。
367                                         string[] tokens = 行.Split( new char[] { ';', ':' } );
368
369                                         // すべてのトークンについて……
370                                         foreach( string token in tokens )
371                                         {
372                                                 // トークンを分割。
373
374                                                 #region " トークンを区切り文字 '=' で strコマンド と strパラメータ に分割し、それぞれの先頭末尾の空白を削除する。"
375                                                 //-----------------
376                                                 string[] items = token.Split( '=' );
377
378                                                 if( 2 != items.Length )
379                                                 {
380                                                         if( 0 == token.Trim().Length )  // 空文字列(行末など)は不正じゃない。
381                                                                 continue;
382
383                                                         FDK.Log.ERROR( $"コマンドとパラメータの記述書式が不正です。このコマンドをスキップします。[{行番号}行目]" );
384                                                         continue;
385                                                 }
386
387                                                 string コマンド = items[ 0 ].Trim();
388                                                 string パラメータ = items[ 1 ].Trim();
389                                                 //-----------------
390                                                 #endregion
391
392                                                 // コマンド別に処理。
393
394                                                 if( コマンド.Equals( "Part", StringComparison.OrdinalIgnoreCase ) )
395                                                 {
396                                                         #region " Part(小節番号指定)コマンド "
397                                                         //-----------------
398
399                                                         #region " 小節番号を取得・設定。"
400                                                         //-----------------
401                                                         string 小節番号文字列 = this.指定された文字列の先頭から数字文字列を取り出す( ref パラメータ );
402                                                         if( 小節番号文字列.Nullまたは空である() )
403                                                         {
404                                                                 FDK.Log.ERROR( $"Part(小節番号)コマンドに小節番号の記述がありません。このコマンドをスキップします。[{行番号}行目]" );
405                                                                 continue;
406                                                         }
407
408                                                         int 小節番号 = 0;
409                                                         if( false == int.TryParse( 小節番号文字列, out 小節番号 ) )
410                                                         {
411                                                                 FDK.Log.ERROR( $"Part(小節番号)コマンドの小節番号が不正です。このコマンドをスキップします。[{行番号}行目]" );
412                                                                 continue;
413                                                         }
414                                                         if( 0 > 小節番号 )
415                                                         {
416                                                                 FDK.Log.ERROR( $"Part(小節番号)コマンドの小節番号が負数です。このコマンドをスキップします。[{行番号}行目]" );
417                                                                 continue;
418                                                         }
419
420                                                         現在の小節番号 = 小節番号;
421                                                         //-----------------
422                                                         #endregion
423                                                         #region " Part 属性があれば取得する。"
424                                                         //-----------------
425                                                         while( 0 < パラメータ.Length )
426                                                         {
427                                                                 // 属性ID を取得。
428                                                                 char 属性ID = char.ToLower( パラメータ[ 0 ] );
429
430                                                                 // Part 属性があれば取得する。
431                                                                 if( 属性ID == 's' )
432                                                                 {
433                                                                         #region " 小節長倍率(>0) → list小節長倍率 "
434                                                                         //-----------------
435                                                                         パラメータ = パラメータ.Substring( 1 ).Trim();
436
437                                                                         string 小節長倍率文字列 = this.指定された文字列の先頭から数字文字列を取り出す( ref パラメータ );
438                                                                         if( 小節長倍率文字列.Nullまたは空である() )
439                                                                         {
440                                                                                 FDK.Log.ERROR( $"Part(小節番号)コマンドに小節長倍率の記述がありません。この属性をスキップします。[{行番号}行目]" );
441                                                                                 continue;
442                                                                         }
443                                                                         パラメータ = パラメータ.Trim();
444
445                                                                         double 小節長倍率 = 1.0;
446                                                                         if( false == double.TryParse( 小節長倍率文字列, out 小節長倍率 ) )
447                                                                         {
448                                                                                 FDK.Log.ERROR( $"Part(小節番号)コマンドの小節長倍率が不正です。この属性をスキップします。[{行番号}行目]" );
449                                                                                 continue;
450                                                                         }
451                                                                         if( 0.0 >= 小節長倍率 )
452                                                                         {
453                                                                                 FDK.Log.ERROR( $"Part(小節番号)コマンドの小節長倍率が 0.0 または負数です。この属性をスキップします。[{行番号}行目]" );
454                                                                                 continue;
455                                                                         }
456
457                                                                         // 小節長倍率辞書に追加 or 上書き更新。
458                                                                         this.小節長倍率を設定する( 現在の小節番号, 小節長倍率 );
459                                                                         //-----------------
460                                                                         #endregion
461
462                                                                         continue;
463                                                                 }
464                                                         }
465                                                         //-----------------
466                                                         #endregion
467
468                                                         //-----------------
469                                                         #endregion
470
471                                                         continue;
472                                                 }
473                                                 if( コマンド.Equals( "Lane", StringComparison.OrdinalIgnoreCase ) )
474                                                 {
475                                                         #region " Lane(レーン指定)コマンド(チップ種別の仮決め)"
476                                                         //-----------------
477                                                         if( パラメータ.Equals( "LeftCrash", StringComparison.OrdinalIgnoreCase ) )
478                                                                 e現在のチップ = チップ種別.LeftCrash;
479
480                                                         else if( パラメータ.Equals( "Ride", StringComparison.OrdinalIgnoreCase ) )
481                                                                 e現在のチップ = チップ種別.Ride;
482
483                                                         else if( パラメータ.Equals( "China", StringComparison.OrdinalIgnoreCase ) )
484                                                                 e現在のチップ = チップ種別.China;
485
486                                                         else if( パラメータ.Equals( "Splash", StringComparison.OrdinalIgnoreCase ) )
487                                                                 e現在のチップ = チップ種別.Splash;
488
489                                                         else if( パラメータ.Equals( "HiHat", StringComparison.OrdinalIgnoreCase ) )
490                                                                 e現在のチップ = チップ種別.HiHat_Close;
491
492                                                         else if( パラメータ.Equals( "Snare", StringComparison.OrdinalIgnoreCase ) )
493                                                                 e現在のチップ = チップ種別.Snare;
494
495                                                         else if( パラメータ.Equals( "Bass", StringComparison.OrdinalIgnoreCase ) )
496                                                                 e現在のチップ = チップ種別.Bass;
497
498                                                         else if( パラメータ.Equals( "Tom1", StringComparison.OrdinalIgnoreCase ) )
499                                                                 e現在のチップ = チップ種別.Tom1;
500
501                                                         else if( パラメータ.Equals( "Tom2", StringComparison.OrdinalIgnoreCase ) )
502                                                                 e現在のチップ = チップ種別.Tom2;
503
504                                                         else if( パラメータ.Equals( "Tom3", StringComparison.OrdinalIgnoreCase ) )
505                                                                 e現在のチップ = チップ種別.Tom3;
506
507                                                         else if( パラメータ.Equals( "RightCrash", StringComparison.OrdinalIgnoreCase ) )
508                                                                 e現在のチップ = チップ種別.RightCrash;
509
510                                                         else if( パラメータ.Equals( "BPM", StringComparison.OrdinalIgnoreCase ) )
511                                                                 e現在のチップ = チップ種別.BPM;
512
513                                                         else if( パラメータ.Equals( "Song", StringComparison.OrdinalIgnoreCase ) )
514                                                                 e現在のチップ = チップ種別.背景動画;
515                                                         else
516                                                                 FDK.Log.ERROR( $"Lane(レーン指定)コマンドのパラメータ記述 '{パラメータ}' が不正です。このコマンドをスキップします。[{行番号}行目]" );
517                                                         //-----------------
518                                                         #endregion
519
520                                                         continue;
521                                                 }
522                                                 if( コマンド.Equals( "Resolution", StringComparison.OrdinalIgnoreCase ) )
523                                                 {
524                                                         #region " Resolution(小節解像度指定)コマンド "
525                                                         //-----------------
526                                                         int 解像度 = 1;
527                                                         if( false == int.TryParse( パラメータ, out 解像度 ) )
528                                                         {
529                                                                 FDK.Log.ERROR( $"Resolution(小節解像度指定)コマンドの解像度が不正です。このコマンドをスキップします。[{行番号}行目]" );
530                                                                 continue;
531                                                         }
532                                                         if( 1 > 解像度 )
533                                                         {
534                                                                 FDK.Log.ERROR( $"Resolution(小節解像度指定)コマンドの解像度は 1 以上でなければなりません。このコマンドをスキップします。[{行番号}行目]" );
535                                                                 continue;
536                                                         }
537                                                         現在の小節解像度 = 解像度;
538                                                         //-----------------
539                                                         #endregion
540
541                                                         continue;
542                                                 }
543                                                 if( コマンド.Equals( "Chips", StringComparison.OrdinalIgnoreCase ) )
544                                                 {
545                                                         #region " Chips(チップ指定)コマンド "
546                                                         //-----------------
547
548                                                         // パラメータを区切り文字 ',' でチップトークンに分割。
549                                                         string[] chipTokens = パラメータ.Split( ',' );
550
551                                                         // すべてのチップトークンについて……
552                                                         for( int i = 0; i < chipTokens.Length; i++ )
553                                                         {
554                                                                 chipTokens[ i ].Trim();
555
556                                                                 #region " 空文字はスキップ。"
557                                                                 //-----------------
558                                                                 if( 0 == chipTokens[ i ].Length )
559                                                                         continue;
560                                                                 //-----------------
561                                                                 #endregion
562                                                                 #region " チップを生成する。"
563                                                                 //-----------------
564                                                                 var chip = new チップ() {
565                                                                         小節番号 = 現在の小節番号,
566                                                                         チップ種別 = e現在のチップ,
567                                                                         小節解像度 = 現在の小節解像度,
568                                                                         音量 = 4,
569                                                                 };
570                                                                 chip.可視 = chip.可視の初期値;
571                                                                 if( chip.チップ種別 == チップ種別.China ) chip.チップ内文字列 = "C N";
572                                                                 if( chip.チップ種別 == チップ種別.Splash ) chip.チップ内文字列 = "S P";
573                                                                 //-----------------
574                                                                 #endregion
575
576                                                                 #region " チップ位置を取得する。"
577                                                                 //-----------------
578                                                                 int チップ位置 = 0;
579                                                                 string 位置番号文字列 = this.指定された文字列の先頭から数字文字列を取り出す( ref chipTokens[ i ] );
580                                                                 chipTokens[ i ].Trim();
581
582                                                                 // 文法チェック。
583                                                                 if( 位置番号文字列.Nullまたは空である() )
584                                                                 {
585                                                                         FDK.Log.ERROR( $"チップの位置指定の記述がありません。このチップをスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
586                                                                         continue;
587                                                                 }
588
589                                                                 // 位置を取得。
590                                                                 if( false == int.TryParse( 位置番号文字列, out チップ位置 ) )
591                                                                 {
592                                                                         FDK.Log.ERROR( $"チップの位置指定の記述が不正です。このチップをスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
593                                                                         continue;
594                                                                 }
595
596                                                                 // 値域チェック。
597                                                                 if( ( 0 > チップ位置 ) || ( チップ位置 >= 現在の小節解像度 ) )
598                                                                 {
599                                                                         FDK.Log.ERROR( $"チップの位置が負数であるか解像度(Resolution)以上の値になっています。このチップをスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
600                                                                         continue;
601                                                                 }
602
603                                                                 chip.小節内位置 = チップ位置;
604                                                                 //-----------------
605                                                                 #endregion
606                                                                 #region " 共通属性・レーン別属性があれば取得する。"
607                                                                 //-----------------
608                                                                 while( chipTokens[ i ].Length > 0 )
609                                                                 {
610                                                                         // 属性ID を取得。
611                                                                         char 属性ID = char.ToLower( chipTokens[ i ][ 0 ] );
612
613                                                                         // 共通属性があれば取得。
614                                                                         if( 属性ID == 'v' )
615                                                                         {
616                                                                                 #region " 音量 "
617                                                                                 //-----------------
618                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
619                                                                                 string 音量文字列 = this.指定された文字列の先頭から数字文字列を取り出す( ref chipTokens[ i ] );
620                                                                                 chipTokens[ i ].Trim();
621
622                                                                                 int チップ音量 = 0;
623
624                                                                                 // 文法チェック。
625                                                                                 if( 音量文字列.Nullまたは空である() )
626                                                                                 {
627                                                                                         FDK.Log.ERROR( $"チップの音量指定の記述がありません。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
628                                                                                         continue;
629                                                                                 }
630                                                                                 
631                                                                                 // チップ音量の取得。
632                                                                                 if( false == int.TryParse( 音量文字列, out チップ音量 ) )
633                                                                                 {
634                                                                                         FDK.Log.ERROR( $"チップの音量指定の記述が不正です。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
635                                                                                         continue;
636                                                                                 }
637                                                                                 
638                                                                                 // 値域チェック。
639                                                                                 if( ( 1 > チップ音量 ) || ( チップ音量 > チップ.最大音量 ) )
640                                                                                 {
641                                                                                         FDK.Log.ERROR( $"チップの音量が適正範囲(1~{チップ.最大音量})を超えています。このチップをスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
642                                                                                         continue;
643                                                                                 }
644
645                                                                                 chip.音量 = チップ音量;
646                                                                                 //-----------------
647                                                                                 #endregion
648
649                                                                                 continue;
650                                                                         }
651
652                                                                         // レーン別属性があれば取得。
653                                                                         switch( e現在のチップ )
654                                                                         {
655                                                                                 #region " case LeftCymbal "
656                                                                                 //-----------------
657                                                                                 case チップ種別.LeftCrash:
658
659                                                                                         #region " 未知の属性 "
660                                                                                         //-----------------
661                                                                                         chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
662                                                                                         FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
663                                                                                         //-----------------
664                                                                                         #endregion
665
666                                                                                         continue;
667
668                                                                                 //-----------------
669                                                                                 #endregion
670                                                                                 #region " case Ride "
671                                                                                 //-----------------
672                                                                                 case チップ種別.Ride:
673                                                                                 case チップ種別.Ride_Cup:
674
675                                                                                         if( 属性ID == 'c' )
676                                                                                         {
677                                                                                                 #region " Ride.カップ "
678                                                                                                 //-----------------
679                                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
680                                                                                                 chip.チップ種別 = チップ種別.Ride_Cup;
681                                                                                                 //-----------------
682                                                                                                 #endregion
683                                                                                         }
684                                                                                         else
685                                                                                         {
686                                                                                                 #region " 未知の属性 "
687                                                                                                 //-----------------
688                                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
689                                                                                                 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
690                                                                                                 //-----------------
691                                                                                                 #endregion
692                                                                                         }
693                                                                                         continue;
694
695                                                                                 //-----------------
696                                                                                 #endregion
697                                                                                 #region " case China "
698                                                                                 //-----------------
699                                                                                 case チップ種別.China:
700
701                                                                                         #region " 未知の属性 "
702                                                                                         //-----------------
703                                                                                         chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
704                                                                                         FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
705                                                                                         //-----------------
706                                                                                         #endregion
707
708                                                                                         continue;
709
710                                                                                 //-----------------
711                                                                                 #endregion
712                                                                                 #region " case Splash "
713                                                                                 //-----------------
714                                                                                 case チップ種別.Splash:
715
716                                                                                         #region " 未知の属性 "
717                                                                                         //-----------------
718                                                                                         chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
719                                                                                         FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
720                                                                                         //-----------------
721                                                                                         #endregion
722
723                                                                                         continue;
724
725                                                                                 //-----------------
726                                                                                 #endregion
727                                                                                 #region " case HiHat "
728                                                                                 //-----------------
729                                                                                 case チップ種別.HiHat_Close:
730                                                                                 case チップ種別.HiHat_HalfOpen:
731                                                                                 case チップ種別.HiHat_Open:
732                                                                                 case チップ種別.HiHat_Foot:
733
734                                                                                         if( 属性ID == 'o' )
735                                                                                         {
736                                                                                                 #region " HiHat.オープン "
737                                                                                                 //-----------------
738                                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
739                                                                                                 chip.チップ種別 = チップ種別.HiHat_Open;
740                                                                                                 //-----------------
741                                                                                                 #endregion
742                                                                                         }
743                                                                                         else if( 属性ID == 'h' )
744                                                                                         {
745                                                                                                 #region " HiHat.ハーフオープン "
746                                                                                                 //-----------------
747                                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
748                                                                                                 chip.チップ種別 = チップ種別.HiHat_HalfOpen;
749                                                                                                 //-----------------
750                                                                                                 #endregion
751                                                                                         }
752                                                                                         else if( 属性ID == 'c' )
753                                                                                         {
754                                                                                                 #region " HiHat.クローズ "
755                                                                                                 //-----------------
756                                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
757                                                                                                 chip.チップ種別 = チップ種別.HiHat_Close;
758                                                                                                 //-----------------
759                                                                                                 #endregion
760                                                                                         }
761                                                                                         else if( 属性ID == 'f' )
762                                                                                         {
763                                                                                                 #region " HiHat.フットスプラッシュ "
764                                                                                                 //-----------------
765                                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
766                                                                                                 chip.チップ種別 = チップ種別.HiHat_Foot;
767                                                                                                 //-----------------
768                                                                                                 #endregion
769                                                                                         }
770                                                                                         else
771                                                                                         {
772                                                                                                 #region " 未知の属性 "
773                                                                                                 //-----------------
774                                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
775                                                                                                 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
776                                                                                                 //-----------------
777                                                                                                 #endregion
778                                                                                         }
779                                                                                         continue;
780
781                                                                                 //-----------------
782                                                                                 #endregion
783                                                                                 #region " case Snare "
784                                                                                 //-----------------
785                                                                                 case チップ種別.Snare:
786                                                                                 case チップ種別.Snare_ClosedRim:
787                                                                                 case チップ種別.Snare_OpenRim:
788                                                                                 case チップ種別.Snare_Ghost:
789
790                                                                                         if( 属性ID == 'o' )
791                                                                                         {
792                                                                                                 #region " Snare.オープンリム "
793                                                                                                 //-----------------
794                                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
795                                                                                                 chip.チップ種別 = チップ種別.Snare_OpenRim;
796                                                                                                 //-----------------
797                                                                                                 #endregion
798                                                                                         }
799                                                                                         else if( 属性ID == 'c' )
800                                                                                         {
801                                                                                                 #region " Snare.クローズドリム "
802                                                                                                 //-----------------
803                                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
804                                                                                                 chip.チップ種別 = チップ種別.Snare_ClosedRim;
805                                                                                                 //-----------------
806                                                                                                 #endregion
807                                                                                         }
808                                                                                         else if( 属性ID == 'g' )
809                                                                                         {
810                                                                                                 #region " Snare.ゴースト "
811                                                                                                 //-----------------
812                                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
813                                                                                                 chip.チップ種別 = チップ種別.Snare_Ghost;
814                                                                                                 //-----------------
815                                                                                                 #endregion
816                                                                                         }
817                                                                                         else
818                                                                                         {
819                                                                                                 #region " 未知の属性 "
820                                                                                                 //-----------------
821                                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
822                                                                                                 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
823                                                                                                 //-----------------
824                                                                                                 #endregion
825                                                                                         }
826                                                                                         continue;
827
828                                                                                 //-----------------
829                                                                                 #endregion
830                                                                                 #region " case Bass "
831                                                                                 //-----------------
832                                                                                 case チップ種別.Bass:
833
834                                                                                         #region " 未知の属性 "
835                                                                                         //-----------------
836                                                                                         chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
837                                                                                         FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
838                                                                                         //-----------------
839                                                                                         #endregion
840
841                                                                                         continue;
842
843                                                                                 //-----------------
844                                                                                 #endregion
845                                                                                 #region " case Tom1 "
846                                                                                 //-----------------
847                                                                                 case チップ種別.Tom1:
848                                                                                 case チップ種別.Tom1_Rim:
849
850                                                                                         if( 属性ID == 'r' )
851                                                                                         {
852                                                                                                 #region " Tom1.リム "
853                                                                                                 //-----------------
854                                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
855                                                                                                 chip.チップ種別 = チップ種別.Tom1_Rim;
856                                                                                                 //-----------------
857                                                                                                 #endregion
858                                                                                         }
859                                                                                         else
860                                                                                         {
861                                                                                                 #region " 未知の属性 "
862                                                                                                 //-----------------
863                                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
864                                                                                                 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
865                                                                                                 //-----------------
866                                                                                                 #endregion
867                                                                                         }
868                                                                                         continue;
869
870                                                                                 //-----------------
871                                                                                 #endregion
872                                                                                 #region " case Tom2 "
873                                                                                 //-----------------
874                                                                                 case チップ種別.Tom2:
875                                                                                 case チップ種別.Tom2_Rim:
876
877                                                                                         if( 属性ID == 'r' )
878                                                                                         {
879                                                                                                 #region " Tom2.リム "
880                                                                                                 //-----------------
881                                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
882                                                                                                 chip.チップ種別 = チップ種別.Tom2_Rim;
883                                                                                                 //-----------------
884                                                                                                 #endregion
885                                                                                         }
886                                                                                         else
887                                                                                         {
888                                                                                                 #region " 未知の属性 "
889                                                                                                 //-----------------
890                                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
891                                                                                                 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
892                                                                                                 //-----------------
893                                                                                                 #endregion
894                                                                                         }
895                                                                                         continue;
896
897                                                                                 //-----------------
898                                                                                 #endregion
899                                                                                 #region " case Tom3 "
900                                                                                 //-----------------
901                                                                                 case チップ種別.Tom3:
902                                                                                 case チップ種別.Tom3_Rim:
903
904                                                                                         if( 属性ID == 'r' )
905                                                                                         {
906                                                                                                 #region " Tom3.リム "
907                                                                                                 //-----------------
908                                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
909                                                                                                 chip.チップ種別 = チップ種別.Tom3_Rim;
910                                                                                                 //-----------------
911                                                                                                 #endregion
912                                                                                         }
913                                                                                         else
914                                                                                         {
915                                                                                                 #region " 未知の属性 "
916                                                                                                 //-----------------
917                                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
918                                                                                                 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
919                                                                                                 //-----------------
920                                                                                                 #endregion
921                                                                                         }
922                                                                                         continue;
923
924                                                                                 //-----------------
925                                                                                 #endregion
926                                                                                 #region " case RightCymbal "
927                                                                                 //-----------------
928                                                                                 case チップ種別.RightCrash:
929
930                                                                                         #region " 未知の属性 "
931                                                                                         //-----------------
932                                                                                         chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
933                                                                                         FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
934                                                                                         //-----------------
935                                                                                         #endregion
936
937                                                                                         continue;
938
939                                                                                 //-----------------
940                                                                                 #endregion
941                                                                                 #region " case BPM "
942                                                                                 //-----------------
943                                                                                 case チップ種別.BPM:
944
945                                                                                         if( 属性ID == 'b' )
946                                                                                         {
947                                                                                                 #region " BPM値 "
948                                                                                                 //-----------------
949
950                                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
951
952                                                                                                 string BPM文字列 = this.指定された文字列の先頭から数字文字列を取り出す( ref chipTokens[ i ] );
953                                                                                                 chipTokens[ i ].Trim();
954
955                                                                                                 if( BPM文字列.Nullまたは空である() )
956                                                                                                 {
957                                                                                                         FDK.Log.ERROR( $"BPM数値の記述がありません。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
958                                                                                                         continue;
959                                                                                                 }
960
961                                                                                                 double BPM = 0.0;
962
963                                                                                                 if( false == double.TryParse( BPM文字列, out BPM ) || ( 0.0 >= BPM ) )
964                                                                                                 {
965                                                                                                         FDK.Log.ERROR( $"BPM数値の記述が不正です。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
966                                                                                                         continue;
967                                                                                                 }
968
969                                                                                                 chip.BPM = BPM;
970                                                                                                 chip.チップ内文字列 = BPM.ToString( "###.##" );
971                                                                                                 //-----------------
972                                                                                                 #endregion
973                                                                                         }
974                                                                                         else
975                                                                                         {
976                                                                                                 #region " 未知の属性 "
977                                                                                                 //-----------------
978                                                                                                 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
979                                                                                                 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
980                                                                                                 //-----------------
981                                                                                                 #endregion
982                                                                                         }
983                                                                                         continue;
984
985                                                                                 //-----------------
986                                                                                 #endregion
987                                                                                 #region " case Song "
988                                                                                 //-----------------
989                                                                                 case チップ種別.背景動画:
990
991                                                                                         #region " 未知の属性 "
992                                                                                         //-----------------
993                                                                                         chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
994                                                                                         FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
995                                                                                         //-----------------
996                                                                                         #endregion
997
998                                                                                         continue;
999
1000                                                                                 //-----------------
1001                                                                                 #endregion
1002                                                                         }
1003
1004                                                                         #region " 未知の属性 "
1005                                                                         //-----------------
1006                                                                         chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1007                                                                         FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
1008                                                                         //-----------------
1009                                                                         #endregion
1010                                                                 }
1011                                                                 //-----------------
1012                                                                 #endregion
1013
1014                                                                 this.チップリスト.Add( chip );
1015                                                         }
1016                                                         //-----------------
1017                                                         #endregion
1018
1019                                                         continue;
1020                                                 }
1021
1022                                                 FDK.Log.ERROR( $"不正なコマンド「{コマンド}」が存在します。[{行番号}行目]" );
1023                                         }
1024                                         //-----------------
1025                                         #endregion
1026                                 }
1027                         }
1028                         finally
1029                         {
1030                                 sr.Close();
1031                         }
1032                         //-----------------
1033                         #endregion
1034                         #region " 拍線の追加。小節線を先に追加すると小節が1つ増えるので、先に拍線から追加する。"
1035                         //-----------------
1036                         int 最大小節番号 = this.最大小節番号;       // this.最大小節番号 プロパティはチップ数に依存して変化するので、for 文には組み込まないこと。
1037
1038                         for( int i = 0; i <= 最大小節番号; i++ )
1039                         {
1040                                 double 小節長倍率 = this.小節長倍率を取得する( i );
1041                                 for( int n = 1; n * 0.25 < 小節長倍率; n++ )
1042                                 {
1043                                         this.チップリスト.Add(
1044                                                 new チップ() {
1045                                                         小節番号 = i,
1046                                                         チップ種別 = チップ種別.拍線,
1047                                                         小節内位置 = (int) ( ( n * 0.25 ) * 100 ),
1048                                                         小節解像度 = (int) ( 小節長倍率 * 100 ),
1049                                                 } );
1050                                 }
1051                         }
1052                         //-----------------
1053                         #endregion
1054                         #region " 小節線の追加。"
1055                         //-----------------
1056                         最大小節番号 = this.最大小節番号;
1057
1058                         for( int i = 0; i <= 最大小節番号 + 1; i++ )
1059                         {
1060                                 this.チップリスト.Add(
1061                                         new チップ() {
1062                                                 小節番号 = i,
1063                                                 チップ種別 = チップ種別.小節線,
1064                                                 小節内位置 = 0,
1065                                                 小節解像度 = 1,
1066                                         } );
1067                         }
1068                         //-----------------
1069                         #endregion
1070                         #region " 小節の先頭 の追加。"
1071                         //----------------
1072                         最大小節番号 = this.最大小節番号;
1073
1074                         // 「小節の先頭」チップは、小節線と同じく、全小節の先頭位置に置かれる。
1075                         // 小節線には今後譜面作者によって位置をアレンジできる可能性を残したいが、
1076                         // ビュアーが小節の先頭位置を検索するためには、小節の先頭に置かれるチップが必要になる。
1077                         // よって、譜面作者の影響を受けない(ビュアー用の)チップを機械的に配置する。
1078
1079                         for( int i = 0; i <= 最大小節番号 + 1; i++ )
1080                         {
1081                                 this.チップリスト.Add(
1082                                         new チップ() {
1083                                                 小節番号 = i,
1084                                                 チップ種別 = チップ種別.小節の先頭,
1085                                                 小節内位置 = 0,
1086                                                 小節解像度 = 1,
1087                                         } );
1088                         }
1089                         //----------------
1090                         #endregion
1091
1092                         this.チップリスト.Sort();
1093
1094                         #region " 全チップの発声/描画時刻と譜面内位置を計算する。"
1095                         //-----------------
1096
1097                         // 1. BPMチップを無視し(初期BPMで固定)、dic小節長倍率, Cチップ.小節解像度, Cチップ.小節内位置 から両者を計算する。
1098                         //    以下、listチップが小節番号順にソートされているという前提で。
1099
1100                         double チップが存在する小節の先頭時刻ms = 0.0;
1101                         int 現在の小節の番号 = 0;
1102
1103                         foreach( チップ chip in this.チップリスト )
1104                         {
1105                                 #region " チップの小節番号が現在の小節番号よりも大きい場合、チップが存在する小節に至るまで、「dbチップが存在する小節の先頭時刻ms」を更新する。"
1106                                 //-----------------
1107                                 while( 現在の小節の番号 < chip.小節番号 )
1108                                 {
1109                                         double 現在の小節の小節長倍率 = this.小節長倍率を取得する( 現在の小節の番号 );
1110                                         チップが存在する小節の先頭時刻ms += BPM初期値固定での1小節4拍の時間ms * 現在の小節の小節長倍率;
1111
1112                                         現在の小節の番号++;    // 現在の小節番号 が chip.小節番号 に追いつくまでループする。
1113                                 }
1114                                 //-----------------
1115                                 #endregion
1116                                 #region " チップの発声/描画時刻を求める。"
1117                                 //-----------------
1118                                 double チップが存在する小節の小節長倍率 = this.小節長倍率を取得する( 現在の小節の番号 );
1119
1120                                 chip.発声時刻ms =
1121                                         chip.描画時刻ms =
1122                                                 (long) ( チップが存在する小節の先頭時刻ms + ( BPM初期値固定での1小節4拍の時間ms * チップが存在する小節の小節長倍率 * chip.小節内位置 ) / chip.小節解像度 );
1123                                 //-----------------
1124                                 #endregion
1125                         }
1126
1127                         // 2. BPMチップを考慮しながら調整する。(譜面内位置grid はBPMの影響を受けないので無視)
1128
1129                         double 現在のBPM = スコア.初期BPM;
1130                         int チップ数 = this.チップリスト.Count;
1131                         for( int i = 0; i < チップ数; i++ )
1132                         {
1133                                 // BPM チップ以外は無視。
1134                                 var BPMチップ = this.チップリスト[ i ];
1135                                 if( BPMチップ.チップ種別 != チップ種別.BPM )
1136                                         continue;
1137
1138                                 // BPMチップより後続の全チップの n発声/描画時刻ms を、新旧BPMの比率(加速率)で修正する。
1139                                 double 加速率 = BPMチップ.BPM / 現在のBPM; // BPMチップ.dbBPM > 0.0 であることは読み込み時に保証済み。
1140                                 for( int j = i + 1; j < チップ数; j++ )
1141                                 {
1142                                         this.チップリスト[ j ].発声時刻ms =
1143                                                 this.チップリスト[ j ].描画時刻ms =
1144                                                         (long) ( BPMチップ.発声時刻ms + ( ( this.チップリスト[ j ].発声時刻ms - BPMチップ.発声時刻ms ) / 加速率 ) );
1145                                 }
1146
1147                                 現在のBPM = BPMチップ.BPM;
1148                         }
1149                         //-----------------
1150                         #endregion
1151                 }
1152                 public void 曲データファイルを読み込む_ヘッダだけ( string 曲データファイル名 )
1153                 {
1154                         this.小節長倍率リスト = new List<double>();
1155
1156                         #region " 曲データファイルを読み込む。"
1157                         //-----------------
1158                         var sr = new StreamReader( 曲データファイル名, Encoding.UTF8 );
1159                         try
1160                         {
1161                                 int 行番号 = 0;
1162
1163                                 while( false == sr.EndOfStream )
1164                                 {
1165                                         // 1行ずつ読み込む。
1166
1167                                         行番号++;
1168                                         string 行 = sr.ReadLine();
1169
1170                                         // 前処理。
1171
1172                                         #region " 文字列に前処理を行う。"
1173                                         //-----------------
1174
1175                                         // 改行は ';' に、TABは空白文字にそれぞれ変換し、先頭末尾の空白を削除する。
1176                                         行 = 行.Replace( Environment.NewLine, ";" );
1177                                         行 = 行.Replace( '\t', ' ' );
1178                                         行 = 行.Trim();
1179
1180                                         // 行中の '#' 以降はコメントとして除外する。また、コメントだけの行はスキップする。
1181                                         int 区切り位置 = 0;
1182                                         for( 区切り位置 = 0; 区切り位置 < 行.Length; 区切り位置++ )
1183                                         {
1184                                                 if( 行[ 区切り位置 ] == '#' )
1185                                                         break;
1186                                         }
1187                                         if( 0 == 区切り位置 )
1188                                         {
1189                                                 continue;       // コメントだけの行はスキップ。
1190                                         }
1191                                         if( 区切り位置 < 行.Length )
1192                                         {
1193                                                 行 = 行.Substring( 0, 区切り位置 - 1 );
1194                                                 行 = 行.Trim();
1195                                         }
1196
1197                                         // 空行ならこの行はスキップする。
1198                                         if( 行.Nullまたは空である() )
1199                                                 continue;
1200                                         //-----------------
1201                                         #endregion
1202
1203                                         // ヘッダコマンド処理。
1204
1205                                         #region " ヘッダコマンドの処理を行う。"
1206                                         //-----------------
1207
1208                                         if( 行.StartsWith( "Title", StringComparison.OrdinalIgnoreCase ) )
1209                                         {
1210                                                 #region " Title コマンド "
1211                                                 //-----------------
1212                                                 string[] items = 行.Split( '=' );
1213
1214                                                 if( 2 != items.Length )
1215                                                 {
1216                                                         FDK.Log.ERROR( $"Title の書式が不正です。スキップします。[{行番号}行目]" );
1217                                                         continue;
1218                                                 }
1219
1220                                                 this.Header.曲名 = items[ 1 ].Trim();
1221                                                 //-----------------
1222                                                 #endregion
1223
1224                                                 continue;
1225                                         }
1226                                         if( 行.StartsWith( "Description", StringComparison.OrdinalIgnoreCase ) )
1227                                         {
1228                                                 #region " Description コマンド "
1229                                                 //-----------------
1230                                                 string[] items = 行.Split( '=' );
1231
1232                                                 if( 2 != items.Length )
1233                                                 {
1234                                                         FDK.Log.ERROR( $"Description の書式が不正です。スキップします。[{行番号}行目]" );
1235                                                         continue;
1236                                                 }
1237
1238                                                 // 2文字のリテラル "\n" は改行に復号。
1239                                                 this.Header.説明文 = items[ 1 ].Trim().Replace( @"\n", Environment.NewLine );
1240                                                 //-----------------
1241                                                 #endregion
1242
1243                                                 continue;
1244                                         }
1245                                         //-----------------
1246                                         #endregion
1247
1248                                         // 上記行頭コマンド以外は無視。
1249                                 }
1250                         }
1251                         finally
1252                         {
1253                                 sr.Close();
1254                         }
1255                         //-----------------
1256                         #endregion
1257                 }
1258                 /// <summary>
1259                 /// 現在の スコア の内容をデータファイル(*.sstf)に書き出す。
1260                 /// </summary>
1261                 /// <remarks>
1262                 /// 小節線、拍線、Unknown チップは出力しない。
1263                 /// 失敗すれば何らかの例外を発出する。
1264                 /// </remarks>
1265                 public void 曲データファイルを書き出す( string 曲データファイル名, string ヘッダ行 )
1266                 {
1267                         var sw = new StreamWriter( 曲データファイル名, false, Encoding.UTF8 );
1268                         try
1269                         {
1270                                 // ヘッダ行の出力
1271                                 sw.WriteLine( $"{ヘッダ行}" );    // strヘッダ行に"{...}"が入ってても大丈夫なようにstring.Format()で囲む。
1272                                 sw.WriteLine( "" );
1273
1274                                 // ヘッダコマンド行の出力
1275                                 sw.WriteLine( "Title={0}", ( string.IsNullOrEmpty( this.Header.曲名 ) ) ? "(no title)" : this.Header.曲名 );
1276                                 if( this.Header.説明文.Nullでも空でもない() )
1277                                 {
1278                                         // 改行コードは、2文字のリテラル "\n" に置換。
1279                                         sw.WriteLine( "Description=" + this.Header.説明文.Replace( Environment.NewLine, @"\n" ) );
1280                                 }
1281                                 sw.WriteLine( "SoundDevice.Delay={0}", this.Header.サウンドデバイス遅延ms );
1282                                 sw.WriteLine( "" );
1283
1284                                 // 全チップの出力
1285
1286                                 #region " 全チップの最終小節番号を取得する。"
1287                                 //-----------------
1288                                 int 最終小節番号 = 0;
1289                                 foreach( var cc in this.チップリスト )
1290                                 {
1291                                         if( cc.小節番号 > 最終小節番号 )
1292                                                 最終小節番号 = cc.小節番号;
1293                                 }
1294                                 //-----------------
1295                                 #endregion
1296
1297                                 for( int 小節番号 = 0; 小節番号 <= 最終小節番号; 小節番号++ )
1298                                 {
1299                                         #region " dicレーン別チップリストの初期化。"
1300                                         //-----------------
1301                                         var dicレーン別チップリスト = new Dictionary<レーン種別, List<チップ>>();
1302                                         foreach( var lane in Enum.GetValues( typeof( レーン種別 ) ) )
1303                                                 dicレーン別チップリスト[ (レーン種別) lane ] = new List<チップ>();
1304                                         //-----------------
1305                                         #endregion
1306                                         #region " dicレーン別チップリストの構築; 小節番号 の小節に存在するチップのみをレーン別に振り分けて格納する。"
1307                                         //-----------------
1308                                         foreach( var cc in this.チップリスト )
1309                                         {
1310                                                 if( cc.小節番号 > 小節番号 )
1311                                                         break;    // チップリストは昇順に並んでいるので、これ以上検索しても無駄。
1312
1313                                                 if( cc.チップ種別 == チップ種別.小節線 ||
1314                                                         cc.チップ種別 == チップ種別.拍線 ||
1315                                                         cc.チップ種別 == チップ種別.小節メモ ||
1316                                                         cc.チップ種別 == チップ種別.Unknown )
1317                                                 {
1318                                                         continue;   // これらは出力しないので無視。
1319                                                 }
1320
1321                                                 if( cc.小節番号 == 小節番号 )
1322                                                 {
1323                                                         var lane = レーン種別.Bass;   // 対応するレーンがなかったら Bass でも返しておく。
1324
1325                                                         foreach( var kvp in dicSSTFレーンチップ対応表 )
1326                                                         {
1327                                                                 if( kvp.Value.Contains( cc.チップ種別 ) )
1328                                                                 {
1329                                                                         lane = kvp.Key;
1330                                                                         break;
1331                                                                 }
1332                                                         }
1333
1334                                                         dicレーン別チップリスト[ lane ].Add( cc );
1335                                                 }
1336                                         }
1337                                         //-----------------
1338                                         #endregion
1339
1340                                         #region " Part行 出力。"
1341                                         //-----------------
1342                                         sw.Write( $"Part = {小節番号.ToString()}" );
1343
1344                                         if( this.小節長倍率リスト[ 小節番号 ] != 1.0 )
1345                                                 sw.Write( $"s{this.小節長倍率リスト[ 小節番号 ].ToString()}" );
1346
1347                                         sw.WriteLine( ";" );
1348                                         //-----------------
1349                                         #endregion
1350                                         #region " Lane, Resolution, Chip 行 出力。"
1351                                         //-----------------
1352                                         foreach( var laneObj in Enum.GetValues( typeof( レーン種別 ) ) )
1353                                         {
1354                                                 var lane = (レーン種別) laneObj;
1355
1356                                                 if( 0 < dicレーン別チップリスト[ lane ].Count )
1357                                                 {
1358                                                         sw.Write( $"Lane={lane.ToString()}; " );
1359
1360                                                         #region " 新しい解像度を求める。"
1361                                                         //-----------------
1362                                                         int 新しい解像度 = 1;
1363                                                         foreach( var cc in dicレーン別チップリスト[ lane ] )
1364                                                                 新しい解像度 = FDK.Utilities.最小公倍数を返す( 新しい解像度, cc.小節解像度 );
1365                                                         //-----------------
1366                                                         #endregion
1367                                                         #region " dicレーン別チップリスト[ lane ] 要素の n小節解像度 と n小節内位置 を n新しい解像度 に合わせて修正する。 "
1368                                                         //-----------------
1369                                                         foreach( var cc in dicレーン別チップリスト[ lane ] )
1370                                                         {
1371                                                                 int 倍率 = 新しい解像度 / cc.小節解像度;      // 新しい解像度 は 小節解像度 の最小公倍数なので常に割り切れる。
1372
1373                                                                 cc.小節解像度 *= 倍率;
1374                                                                 cc.小節内位置 *= 倍率;
1375                                                         }
1376                                                         //-----------------
1377                                                         #endregion
1378
1379                                                         sw.Write( $"Resolution = {新しい解像度}; " );
1380                                                         sw.Write( "Chips = " );
1381
1382                                                         for( int i = 0; i < dicレーン別チップリスト[ lane ].Count; i++ )
1383                                                         {
1384                                                                 チップ cc = dicレーン別チップリスト[ lane ][ i ];
1385
1386                                                                 // 位置を出力。
1387                                                                 sw.Write( cc.小節内位置.ToString() );
1388
1389                                                                 // 属性を出力(あれば)。
1390
1391                                                                 #region " (1) 共通属性 "
1392                                                                 //-----------------
1393                                                                 if( cc.音量 < チップ.最大音量 )
1394                                                                         sw.Write( $"v{cc.音量.ToString()}" );
1395                                                                 //-----------------
1396                                                                 #endregion
1397                                                                 #region " (2) 専用属性 "
1398                                                                 //-----------------
1399                                                                 switch( cc.チップ種別 )
1400                                                                 {
1401                                                                         case チップ種別.Ride_Cup:
1402                                                                                 sw.Write( 'c' );
1403                                                                                 break;
1404
1405                                                                         case チップ種別.HiHat_Open:
1406                                                                                 sw.Write( 'o' );
1407                                                                                 break;
1408
1409                                                                         case チップ種別.HiHat_HalfOpen:
1410                                                                                 sw.Write( 'h' );
1411                                                                                 break;
1412
1413                                                                         case チップ種別.HiHat_Foot:
1414                                                                                 sw.Write( 'f' );
1415                                                                                 break;
1416
1417                                                                         case チップ種別.Snare_OpenRim:
1418                                                                                 sw.Write( 'o' );
1419                                                                                 break;
1420
1421                                                                         case チップ種別.Snare_ClosedRim:
1422                                                                                 sw.Write( 'c' );
1423                                                                                 break;
1424
1425                                                                         case チップ種別.Snare_Ghost:
1426                                                                                 sw.Write( 'g' );
1427                                                                                 break;
1428
1429                                                                         case チップ種別.Tom1_Rim:
1430                                                                                 sw.Write( 'r' );
1431                                                                                 break;
1432
1433                                                                         case チップ種別.Tom2_Rim:
1434                                                                                 sw.Write( 'r' );
1435                                                                                 break;
1436
1437                                                                         case チップ種別.Tom3_Rim:
1438                                                                                 sw.Write( 'r' );
1439                                                                                 break;
1440
1441                                                                         case チップ種別.BPM:
1442                                                                                 sw.Write( $"b{cc.BPM.ToString()}" );
1443                                                                                 break;
1444                                                                 }
1445                                                                 //-----------------
1446                                                                 #endregion
1447
1448                                                                 // 区切り文字 または 終端文字 を出力
1449                                                                 sw.Write( ( i == dicレーン別チップリスト[ lane ].Count - 1 ) ? ";" : "," );
1450                                                         }
1451
1452                                                         sw.WriteLine( "" ); // 改行
1453                                                 }
1454                                         }
1455                                         //-----------------
1456                                         #endregion
1457
1458                                         sw.WriteLine( "" ); // 次の Part 前に1行あける。
1459                                 }
1460
1461                                 // メモ(小節単位)の出力
1462
1463                                 #region " dicメモ を小節番号で昇順にソートし直した dic昇順メモ を作成する。"
1464                                 //-----------------
1465                                 var dic昇順メモ = new Dictionary<int, string>();
1466                                 int 最大小節番号 = this.最大小節番号;
1467
1468                                 for( int i = 0; i <= 最大小節番号; i++ )
1469                                 {
1470                                         if( this.dicメモ.ContainsKey( i ) )
1471                                                 dic昇順メモ.Add( i, this.dicメモ[ i ] );
1472                                 }
1473                                 //-----------------
1474                                 #endregion
1475                                 #region " dic昇順メモを出力する。"
1476                                 //-----------------
1477                                 foreach( var kvp in dic昇順メモ )
1478                                 {
1479                                         int 小節番号 = kvp.Key;
1480
1481                                         // 改行コードは、2文字のリ照られる "\n" に置換。
1482                                         string メモ = kvp.Value.Replace( Environment.NewLine, @"\n" );
1483
1484                                         sw.WriteLine( $"PartMemo={小節番号},{メモ}" );
1485                                 }
1486
1487                                 sw.WriteLine( "" );
1488                                 //-----------------
1489                                 #endregion
1490                         }
1491                         finally
1492                         {
1493                                 sw.Close();
1494                         }
1495                 }
1496                 /// <summary>
1497                 /// 指定された Config.Speed を考慮し、指定された時間[ms]の間に流れるピクセル数[dpx]を算出して返す。</para>
1498                 /// </summary>
1499                 [Obsolete]
1500                 public int 指定された時間msに対応する符号付きピクセル数を返す( double speed, long 指定時間ms )
1501                 {
1502                         return (int) ( 指定時間ms * スコア.基準譜面速度dpxms * speed );
1503                 }
1504                 /// <summary>
1505                 /// 指定された Config.Speed を考慮し、指定された時間[秒]の間に流れるピクセル数[dpx]を算出して返す。
1506                 /// </summary>
1507                 public double 指定された時間secに対応する符号付きピクセル数を返す( double speed, double 指定時間sec )
1508                 {
1509                         return ( 指定時間sec * スコア.基準譜面速度dpxsec * speed );
1510                 }
1511                 public double 小節長倍率を取得する( int 小節番号 )
1512                 {
1513                         // 小節長倍率リスト が短ければ増設する。
1514                         if( 小節番号 >= this.小節長倍率リスト.Count )
1515                         {
1516                                 int 不足数 = 小節番号 - this.小節長倍率リスト.Count + 1;
1517                                 for( int i = 0; i < 不足数; i++ )
1518                                         this.小節長倍率リスト.Add( 1.0 );
1519                         }
1520
1521                         // 小節番号に対応する倍率を返す。
1522                         return this.小節長倍率リスト[ 小節番号 ];
1523                 }
1524                 public void 小節長倍率を設定する( int 小節番号, double 倍率 )
1525                 {
1526                         // 小節長倍率リスト が短ければ増設する。
1527                         if( 小節番号 >= this.小節長倍率リスト.Count )
1528                         {
1529                                 int 不足数 = 小節番号 - this.小節長倍率リスト.Count + 1;
1530                                 for( int i = 0; i < 不足数; i++ )
1531                                         this.小節長倍率リスト.Add( 1.0 );
1532                         }
1533
1534                         // 小節番号に対応付けて倍率を登録する。
1535                         this.小節長倍率リスト[ 小節番号 ] = 倍率;
1536                 }
1537
1538                 /// <summary>
1539                 /// 取出文字列の先頭にある数字(小数点も有効)の連続した部分を取り出して、戻り値として返す。
1540                 /// また、取出文字列から取り出した数字文字列部分を除去した文字列を再度格納する。
1541                 /// </summary>
1542                 protected string 指定された文字列の先頭から数字文字列を取り出す( ref string 取出文字列 )
1543                 {
1544                         int 桁数 = 0;
1545                         while( ( 桁数 < 取出文字列.Length ) && ( char.IsDigit( 取出文字列[ 桁数 ] ) || 取出文字列[ 桁数 ] == '.' ) )
1546                                 桁数++;
1547
1548                         if( 0 == 桁数 )
1549                                 return "";
1550
1551                         string 数字文字列 = 取出文字列.Substring( 0, 桁数 );
1552                         取出文字列 = ( 桁数 == 取出文字列.Length ) ? "" : 取出文字列.Substring( 桁数 );
1553
1554                         return 数字文字列;
1555                 }
1556         }
1557 }