2 using System.Collections.Generic;
3 using System.Diagnostics;
7 using System.Text.RegularExpressions;
11 public class スコア : IDisposable
16 /// 指定されたコマンド名が対象文字列内で使用されている場合に、パラメータ部分の文字列を返す。
19 /// .dtx や box.def 等で使用されている "#<コマンド名>[:]<パラメータ>[;コメント]" 形式の文字列(対象文字列)について、
20 /// 指定されたコマンドを使用する行であるかどうかを判別し、使用する行であるなら、そのパラメータ部分の文字列を引数に格納し、true を返す。
21 /// 対象文字列のコマンド名が指定したコマンド名と異なる場合には、パラメータ文字列に null を格納して false を返す。
22 /// コマンド名は正しくてもパラメータが存在しない場合には、空文字列("") を格納して true を返す。
24 /// <param name="対象文字列">調べる対象の文字列。(例: "#TITLE: 曲名 ;コメント")</param>
25 /// <param name="コマンド名">調べるコマンドの名前(例:"TITLE")。#は不要、大文字小文字は区別されない。</param>
26 /// <returns>パラメータ文字列の取得に成功したら true、異なるコマンドだったなら false。</returns>
27 public static bool コマンドのパラメータ文字列部分を返す( string 対象文字列, string コマンド名, out string パラメータ文字列 )
29 // コメント部分を除去し、両端をトリムする。なお、全角空白はトリムしない。
30 対象文字列 = 対象文字列.Split( ';' )[ 0 ].Trim( ' ', '\t' );
32 string 正規表現パターン = $@"^\s*#{コマンド名}(:|\s)+(.*)\s*$"; // \s は空白文字。
33 var m = Regex.Match( 対象文字列, 正規表現パターン, RegexOptions.IgnoreCase );
35 if( m.Success && ( 3 <= m.Groups.Count ) )
37 パラメータ文字列 = m.Groups[ 2 ].Value;
49 public const double db初期BPM = 120.0;
50 public const double db初期小節解像度 = 480.0;
51 public const double dbBPM初期値固定での1小節4拍の時間ms = ( 60.0 * 1000 ) / ( スコア.db初期BPM / 4.0 );
52 public const double dbBPM初期値固定での1小節4拍の時間sec = 60.0 / ( スコア.db初期BPM / 4.0 );
55 /// 1ms あたりの設計ピクセル数 [dpx] 。
58 /// BPM 150 のとき、1小節が 234 dpx になるように調整。
59 /// → 60秒で150拍のとき、1小節(4拍)が 234 dpx。
60 /// → 60秒の間に、150[拍]÷4[拍]=37.5[小節]。
61 /// → 60秒の間に、37.5[小節]×234[dpx/小節]= 8775[dpx]。
62 /// → 1ms の間に、8775[dpx]÷60000[ms]=0.14625[dpx/ms]。割り切れて良かった。
64 public const double 基準譜面速度dpxms = 0.14625 * 2.25; // "* 2.25" は「x1.0はもう少し速くてもいいんではないか?」という感覚的な調整分。
67 /// 1秒あたりの設計ピクセル数 [dpx] 。
69 public const double db基準譜面速度dpxsec = 基準譜面速度dpxms * 1000.0;
71 public static readonly Dictionary<レーン種別, List<チップ種別>> dicSSTFレーンチップ対応表 = new Dictionary<レーン種別, List<チップ種別>>() {
74 { レーン種別.Bass, new List<チップ種別>() { チップ種別.Bass } },
75 { レーン種別.BPM, new List<チップ種別>() { チップ種別.BPM } },
76 { レーン種別.China, new List<チップ種別>() { チップ種別.China } },
77 { レーン種別.HiHat, new List<チップ種別>() { チップ種別.HiHat_Close, チップ種別.HiHat_Foot, チップ種別.HiHat_HalfOpen, チップ種別.HiHat_Open } },
78 { レーン種別.LeftCrash, new List<チップ種別>() { チップ種別.LeftCrash } },
79 { レーン種別.Ride, new List<チップ種別>() { チップ種別.Ride, チップ種別.Ride_Cup } },
80 { レーン種別.RightCrash, new List<チップ種別>() { チップ種別.RightCrash } },
81 { レーン種別.Snare, new List<チップ種別>() { チップ種別.Snare, チップ種別.Snare_ClosedRim, チップ種別.Snare_Ghost, チップ種別.Snare_OpenRim } },
82 { レーン種別.Song, new List<チップ種別>() { チップ種別.背景動画 } },
83 { レーン種別.Splash, new List<チップ種別>() { チップ種別.Splash } },
84 { レーン種別.Tom1, new List<チップ種別>() { チップ種別.Tom1, チップ種別.Tom1_Rim } },
85 { レーン種別.Tom2, new List<チップ種別>() { チップ種別.Tom2, チップ種別.Tom2_Rim } },
86 { レーン種別.Tom3, new List<チップ種別>() { チップ種別.Tom3, チップ種別.Tom3_Rim } },
92 public static readonly List<string> listデフォルト拡張子 = new List<string>() {
93 ".avi", ".flv", ".mp4", ".wmv", ".mpg", ".mpeg"
96 // プロパティ;読み込み時または編集時に設定される
98 public string 背景動画ファイル名 = "";
102 public string str曲名 = "(no title)";
103 public string str説明文 = "";
105 public CHeader Header = new CHeader();
107 public List<チップ> listチップ
112 public List<double> list小節長倍率
123 foreach( チップ chip in this.listチップ )
125 if( chip.小節番号 > n最大小節番号 )
132 public Dictionary<int, string> dicメモ = new Dictionary<int, string>();
138 this.listチップ = new List<チップ>();
139 this.list小節長倍率 = new List<double>();
141 public スコア( string str曲データファイル名, bool 左ライド = false, bool 左チャイナ = false, bool 左スプラッシュ = true )
144 this.t曲データファイルを読み込む( str曲データファイル名, 左ライド, 左チャイナ, 左スプラッシュ );
146 public void Dispose()
151 /// 指定された曲データファイルを読み込む。
154 /// 失敗すれば何らかの例外を発出する。
156 public void t曲データファイルを読み込む( string str曲データファイル名, bool 左ライド = false, bool 左チャイナ = false, bool 左スプラッシュ = true )
160 this.list小節長倍率 = new List<double>();
161 this.dicメモ = new Dictionary<int, string>();
164 #region " 曲データファイルを読み込む。"
166 var sr = new StreamReader( str曲データファイル名, Encoding.UTF8 );
172 //double db現在の小節長倍率 = 1.0;
173 チップ種別 e現在のチップ = チップ種別.Unknown;
175 while( !sr.EndOfStream )
179 string str行 = sr.ReadLine();
183 #region " 改行は ';' に、TABは空白文字にそれぞれ変換し、先頭末尾の空白を削除する。"
185 str行 = str行.Replace( Environment.NewLine, ";" );
186 str行 = str行.Replace( '\t', ' ' );
190 #region " 行中の '#' 以降はコメントとして除外する。また、コメントだけの行はスキップする。"
193 for( n区切り位置 = 0; n区切り位置 < str行.Length; n区切り位置++ )
195 if( str行[ n区切り位置 ] == '#' )
199 continue; // コメントだけの行はスキップ。
200 if( n区切り位置 < str行.Length )
202 str行 = str行.Substring( 0, n区切り位置 - 1 );
207 #region " 空行ならこの行はスキップする。"
209 if( string.IsNullOrEmpty( str行 ) )
216 #region " ヘッダコマンドの処理を行う。"
218 if( str行.StartsWith( "Title", StringComparison.OrdinalIgnoreCase ) )
220 #region " Title コマンド "
222 string[] items = str行.Split( '=' );
224 if( items.Length != 2 )
226 FDK.Log.ERROR( $"Title の書式が不正です。スキップします。[{n行番号}行目]" );
230 this.Header.str曲名 = items[ 1 ].Trim();
236 if( str行.StartsWith( "Description", StringComparison.OrdinalIgnoreCase ) )
238 #region " Description コマンド "
240 string[] items = str行.Split( '=' );
242 if( items.Length != 2 )
244 FDK.Log.ERROR( $"Description の書式が不正です。スキップします。[{n行番号}行目]" );
248 // 2文字のリテラル "\n" は改行に復号。
249 this.Header.str説明文 = items[ 1 ].Trim().Replace( @"\n", Environment.NewLine );
260 #region " メモ(小節単位)の処理を行う。"
262 if( str行.StartsWith( "PartMemo", StringComparison.OrdinalIgnoreCase ) )
264 #region " '=' 以前を除去する。"
266 int n等号位置 = str行.IndexOf( '=' );
269 FDK.Log.ERROR( $"PartMemo の書式が不正です。スキップします。[{n行番号}]行目]" );
272 str行 = str行.Substring( n等号位置 + 1 ).Trim();
273 if( string.IsNullOrEmpty( str行 ) )
275 FDK.Log.ERROR( $"PartMemo の書式が不正です。スキップします。[{n行番号}]行目]" );
280 #region " カンマ位置を取得する。"
282 int nカンマ位置 = str行.IndexOf( ',' );
285 FDK.Log.ERROR( $"PartMemo の書式が不正です。スキップします。[{n行番号}]行目]" );
290 #region " 小節番号を取得する。"
292 string str小節番号 = str行.Substring( 0, nカンマ位置 );
294 if( !int.TryParse( str小節番号, out n小節番号 ) || n小節番号 < 0 )
296 FDK.Log.ERROR( $"PartMemo の小節番号が不正です。スキップします。[{n行番号}]行目]" );
303 string strメモ = str行.Substring( nカンマ位置 + 1 );
305 // 2文字のリテラル文字列 "\n" は改行に復号。
306 strメモ = strメモ.Replace( @"\n", Environment.NewLine );
309 #region " メモが空文字列でないなら dicメモ に登録すると同時に、チップとしても追加する。"
311 if( !string.IsNullOrEmpty( strメモ ) )
313 this.dicメモ.Add( n小節番号, strメモ );
331 // 上記行頭コマンド以外は、チップ記述行だと見なす。
333 #region " チップ記述コマンドの処理を行う。"
337 string[] tokens = str行.Split( new char[] { ';', ':' } );
340 foreach( string token in tokens )
344 #region " トークンを区切り文字 '=' で strコマンド と strパラメータ に分割し、それぞれの先頭末尾の空白を削除する。"
346 string[] items = token.Split( '=' );
348 if( items.Length != 2 )
350 if( token.Trim().Length == 0 ) // 空文字列(行末など)は不正じゃない。
353 FDK.Log.ERROR( $"コマンドとパラメータの記述書式が不正です。このコマンドをスキップします。[{n行番号}行目]" );
357 string strコマンド = items[ 0 ].Trim();
358 string strパラメータ = items[ 1 ].Trim();
364 if( strコマンド.Equals( "Part", StringComparison.OrdinalIgnoreCase ) )
366 #region " Part(小節番号指定)コマンド "
369 #region " 小節番号を取得・設定。"
371 string str小節番号 = this.t指定された文字列の先頭から数字文字列を取り出す( ref strパラメータ );
372 if( string.IsNullOrEmpty( str小節番号 ) )
374 FDK.Log.ERROR( $"Part(小節番号)コマンドに小節番号の記述がありません。このコマンドをスキップします。[{n行番号}行目]" );
379 if( !int.TryParse( str小節番号, out n小節番号 ) )
381 FDK.Log.ERROR( $"Part(小節番号)コマンドの小節番号が不正です。このコマンドをスキップします。[{n行番号}行目]" );
386 FDK.Log.ERROR( $"Part(小節番号)コマンドの小節番号が負数です。このコマンドをスキップします。[{n行番号}行目]" );
393 #region " Part 属性があれば取得する。"
396 while( strパラメータ.Length > 0 )
400 char c属性ID = char.ToLower( strパラメータ[ 0 ] );
407 #region " 小節長倍率(>0) → list小節長倍率 "
410 strパラメータ = strパラメータ.Substring( 1 ).Trim();
412 string str小節長倍率 = this.t指定された文字列の先頭から数字文字列を取り出す( ref strパラメータ );
413 strパラメータ = strパラメータ.Trim();
415 if( string.IsNullOrEmpty( str小節長倍率 ) )
417 FDK.Log.ERROR( $"Part(小節番号)コマンドに小節長倍率の記述がありません。この属性をスキップします。[{n行番号}行目]" );
421 double db小節長倍率 = 1.0;
423 if( !double.TryParse( str小節長倍率, out db小節長倍率 ) )
425 FDK.Log.ERROR( $"Part(小節番号)コマンドの小節長倍率が不正です。この属性をスキップします。[{n行番号}行目]" );
431 FDK.Log.ERROR( $"Part(小節番号)コマンドの小節長倍率が 0.0 または負数です。この属性をスキップします。[{n行番号}行目]" );
436 // 小節長倍率辞書に追加 or 上書き更新。
438 this.小節長倍率を設定する( n現在の小節番号, db小節長倍率 );
455 if( strコマンド.Equals( "Lane", StringComparison.OrdinalIgnoreCase ) )
457 #region " Lane(レーン指定)コマンド(チップ種別の仮決め)"
459 if( strパラメータ.Equals( "LeftCrash", StringComparison.OrdinalIgnoreCase ) )
460 e現在のチップ = チップ種別.LeftCrash;
462 else if( strパラメータ.Equals( "Ride", StringComparison.OrdinalIgnoreCase ) )
463 e現在のチップ = チップ種別.Ride;
465 else if( strパラメータ.Equals( "China", StringComparison.OrdinalIgnoreCase ) )
466 e現在のチップ = チップ種別.China;
468 else if( strパラメータ.Equals( "Splash", StringComparison.OrdinalIgnoreCase ) )
469 e現在のチップ = チップ種別.Splash;
471 else if( strパラメータ.Equals( "HiHat", StringComparison.OrdinalIgnoreCase ) )
472 e現在のチップ = チップ種別.HiHat_Close;
474 else if( strパラメータ.Equals( "Snare", StringComparison.OrdinalIgnoreCase ) )
475 e現在のチップ = チップ種別.Snare;
477 else if( strパラメータ.Equals( "Bass", StringComparison.OrdinalIgnoreCase ) )
478 e現在のチップ = チップ種別.Bass;
480 else if( strパラメータ.Equals( "Tom1", StringComparison.OrdinalIgnoreCase ) )
481 e現在のチップ = チップ種別.Tom1;
483 else if( strパラメータ.Equals( "Tom2", StringComparison.OrdinalIgnoreCase ) )
484 e現在のチップ = チップ種別.Tom2;
486 else if( strパラメータ.Equals( "Tom3", StringComparison.OrdinalIgnoreCase ) )
487 e現在のチップ = チップ種別.Tom3;
489 else if( strパラメータ.Equals( "RightCrash", StringComparison.OrdinalIgnoreCase ) )
490 e現在のチップ = チップ種別.RightCrash;
492 else if( strパラメータ.Equals( "BPM", StringComparison.OrdinalIgnoreCase ) )
495 else if( strパラメータ.Equals( "Song", StringComparison.OrdinalIgnoreCase ) )
496 e現在のチップ = チップ種別.背景動画;
498 FDK.Log.ERROR( $"Lane(レーン指定)コマンドのパラメータ記述 '{strパラメータ}' が不正です。このコマンドをスキップします。[{n行番号}行目]" );
504 if( strコマンド.Equals( "Resolution", StringComparison.OrdinalIgnoreCase ) )
506 #region " Resolution(小節解像度指定)コマンド "
509 if( !int.TryParse( strパラメータ, out n解像度 ) )
511 FDK.Log.ERROR( $"Resolution(小節解像度指定)コマンドの解像度が不正です。このコマンドをスキップします。[{n行番号}行目]" );
516 FDK.Log.ERROR( $"Resolution(小節解像度指定)コマンドの解像度は 1 以上でなければなりません。このコマンドをスキップします。[{n行番号}行目]" );
525 if( strコマンド.Equals( "Chips", StringComparison.OrdinalIgnoreCase ) )
527 #region " Chips(チップ指定)コマンド "
530 // パラメータを区切り文字 ',' でチップトークンに分割。
531 string[] chipTokens = strパラメータ.Split( ',' );
534 for( int i = 0; i < chipTokens.Length; i++ )
536 chipTokens[ i ].Trim();
540 if( chipTokens[ i ].Length == 0 )
546 var chip = new チップ() {
552 if( chip.チップ種別 == チップ種別.China ) chip.チップ内文字列 = "C N";
553 if( chip.チップ種別 == チップ種別.Splash ) chip.チップ内文字列 = "S P";
555 if( ( e現在のチップ == チップ種別.BPM ) ||
556 ( e現在のチップ == チップ種別.背景動画 ) ||
557 ( e現在のチップ == チップ種別.小節メモ ) ||
558 ( e現在のチップ == チップ種別.Unknown ) )
565 #region " チップ位置を取得する。"
569 string str位置番号 = this.t指定された文字列の先頭から数字文字列を取り出す( ref chipTokens[ i ] );
570 chipTokens[ i ].Trim();
574 if( string.IsNullOrEmpty( str位置番号 ) )
576 FDK.Log.ERROR( $"チップの位置指定の記述がありません。このチップをスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
583 if( !int.TryParse( str位置番号, out nチップ位置 ) )
585 FDK.Log.ERROR( $"チップの位置指定の記述が不正です。このチップをスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
592 if( ( nチップ位置 < 0 ) || ( nチップ位置 >= n現在の小節解像度 ) )
594 FDK.Log.ERROR( $"チップの位置が負数であるか解像度(Resolution)以上の値になっています。このチップをスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
603 #region " 共通属性・レーン別属性があれば取得する。"
605 while( chipTokens[ i ].Length > 0 )
608 char c属性ID = char.ToLower( chipTokens[ i ][ 0 ] );
615 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
617 string str音量 = this.t指定された文字列の先頭から数字文字列を取り出す( ref chipTokens[ i ] );
618 chipTokens[ i ].Trim();
624 if( string.IsNullOrEmpty( str音量 ) )
626 FDK.Log.ERROR( $"チップの音量指定の記述がありません。この属性をスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
633 if( !int.TryParse( str音量, out nチップ音量 ) )
635 FDK.Log.ERROR( $"チップの音量指定の記述が不正です。この属性をスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
642 if( nチップ音量 < 1 || nチップ音量 > チップ.最大音量 )
644 FDK.Log.ERROR( $"チップの音量が適正範囲(1~{チップ.最大音量})を超えています。このチップをスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
662 #region " case LeftCymbal "
664 case チップ種別.LeftCrash:
668 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
669 FDK.Log.ERROR( $"未対応の属性「{c属性ID}」が指定されています。この属性をスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
677 #region " case Ride "
686 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
687 chip.チップ種別 = チップ種別.Ride_Cup;
695 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
696 FDK.Log.ERROR( $"未対応の属性「{c属性ID}」が指定されています。この属性をスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
704 #region " case China "
710 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
711 FDK.Log.ERROR( $"未対応の属性「{c属性ID}」が指定されています。この属性をスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
719 #region " case Splash "
725 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
726 FDK.Log.ERROR( $"未対応の属性「{c属性ID}」が指定されています。この属性をスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
734 #region " case HiHat "
736 case チップ種別.HiHat_Close:
737 case チップ種別.HiHat_HalfOpen:
738 case チップ種別.HiHat_Open:
739 case チップ種別.HiHat_Foot:
743 #region " HiHat.オープン "
745 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
746 chip.チップ種別 = チップ種別.HiHat_Open;
750 else if( c属性ID == 'h' )
752 #region " HiHat.ハーフオープン "
754 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
755 chip.チップ種別 = チップ種別.HiHat_HalfOpen;
759 else if( c属性ID == 'c' )
761 #region " HiHat.クローズ "
763 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
764 chip.チップ種別 = チップ種別.HiHat_Close;
768 else if( c属性ID == 'f' )
770 #region " HiHat.フットスプラッシュ "
772 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
773 chip.チップ種別 = チップ種別.HiHat_Foot;
781 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
782 FDK.Log.ERROR( $"未対応の属性「{c属性ID}」が指定されています。この属性をスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
790 #region " case Snare "
793 case チップ種別.Snare_ClosedRim:
794 case チップ種別.Snare_OpenRim:
795 case チップ種別.Snare_Ghost:
799 #region " Snare.オープンリム "
801 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
802 chip.チップ種別 = チップ種別.Snare_OpenRim;
806 else if( c属性ID == 'c' )
808 #region " Snare.クローズドリム "
810 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
811 chip.チップ種別 = チップ種別.Snare_ClosedRim;
815 else if( c属性ID == 'g' )
817 #region " Snare.ゴースト "
819 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
820 chip.チップ種別 = チップ種別.Snare_Ghost;
828 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
829 FDK.Log.ERROR( $"未対応の属性「{c属性ID}」が指定されています。この属性をスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
837 #region " case Bass "
843 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
844 FDK.Log.ERROR( $"未対応の属性「{c属性ID}」が指定されています。この属性をスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
852 #region " case Tom1 "
861 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
862 chip.チップ種別 = チップ種別.Tom1_Rim;
870 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
871 FDK.Log.ERROR( $"未対応の属性「{c属性ID}」が指定されています。この属性をスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
879 #region " case Tom2 "
888 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
889 chip.チップ種別 = チップ種別.Tom2_Rim;
897 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
898 FDK.Log.ERROR( $"未対応の属性「{c属性ID}」が指定されています。この属性をスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
906 #region " case Tom3 "
915 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
916 chip.チップ種別 = チップ種別.Tom3_Rim;
924 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
925 FDK.Log.ERROR( $"未対応の属性「{c属性ID}」が指定されています。この属性をスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
933 #region " case RightCymbal "
935 case チップ種別.RightCrash:
939 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
940 FDK.Log.ERROR( $"未対応の属性「{c属性ID}」が指定されています。この属性をスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
957 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
959 string strBPM = this.t指定された文字列の先頭から数字文字列を取り出す( ref chipTokens[ i ] );
960 chipTokens[ i ].Trim();
962 if( string.IsNullOrEmpty( strBPM ) )
964 FDK.Log.ERROR( $"BPM数値の記述がありません。この属性をスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
970 if( !double.TryParse( strBPM, out dbBPM ) || dbBPM <= 0.0 )
972 FDK.Log.ERROR( $"BPM数値の記述が不正です。この属性をスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
977 chip.チップ内文字列 = dbBPM.ToString( "###.##" );
985 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
986 FDK.Log.ERROR( $"未対応の属性「{c属性ID}」が指定されています。この属性をスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
994 #region " case Song "
1000 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1001 FDK.Log.ERROR( $"未対応の属性「{c属性ID}」が指定されています。この属性をスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
1013 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1014 FDK.Log.ERROR( $"未対応の属性「{c属性ID}」が指定されています。この属性をスキップします。[{n行番号}行目; {i + 1}個目のチップ]" );
1022 this.listチップ.Add( chip );
1030 FDK.Log.ERROR( $"不正なコマンド「{strコマンド}」が存在します。[{n行番号}行目]" );
1042 #region " 拍線の追加。小節線を先に追加すると小節が1つ増えるので、先に拍線から追加する。"
1045 // 「this.n最大小節番号」はチップ数に依存して変化するので、for 文には組み込まないこと。
1046 int n最大小節番号 = this.最大小節番号;
1048 for( int i = 0; i <= n最大小節番号; i++ )
1050 double db小節長倍率 = this.小節長倍率を取得する( i );
1051 for( int n = 1; n * 0.25 < db小節長倍率; n++ )
1057 小節内位置 = (int) ( ( n * 0.25 ) * 100 ),
1058 小節解像度 = (int) ( db小節長倍率 * 100 ),
1066 n最大小節番号 = this.最大小節番号;
1068 for( int i = 0; i <= n最大小節番号 + 1; i++ )
1081 this.listチップ.Sort();
1083 #region " 全チップの発声/描画時刻と譜面内位置を計算する。"
1086 // 1. BPMチップを無視し(初期BPMで固定)、dic小節長倍率, Cチップ.n小節解像度, Cチップ.n小節内位置 から両者を計算する。
1087 // 以下、listチップが小節番号順にソートされているという前提で。
1089 double dbチップが存在する小節の先頭時刻ms = 0.0;
1092 foreach( チップ chip in this.listチップ )
1094 #region " チップの小節番号が現在の小節番号よりも大きい場合、チップが存在する小節に至るまで、「dbチップが存在する小節の先頭時刻ms」を更新する。"
1096 while( n現在の小節の番号 < chip.小節番号 )
1098 double db現在の小節の小節長倍率 = this.小節長倍率を取得する( n現在の小節の番号 );
1099 dbチップが存在する小節の先頭時刻ms += dbBPM初期値固定での1小節4拍の時間ms * db現在の小節の小節長倍率;
1101 n現在の小節の番号++; // n現在の小節番号 が chip.n小節番号 に追いつくまでループする。
1105 #region " チップの発声/描画時刻を求める。"
1107 double dbチップが存在する小節の小節長倍率 = this.小節長倍率を取得する( n現在の小節の番号 );
1111 (long) ( dbチップが存在する小節の先頭時刻ms + ( dbBPM初期値固定での1小節4拍の時間ms * dbチップが存在する小節の小節長倍率 * chip.小節内位置 ) / chip.小節解像度 );
1116 // 2. BPMチップを考慮しながら調整する。(譜面内位置grid はBPMの影響を受けないので無視)
1118 double db現在のBPM = スコア.db初期BPM;
1119 int nチップ数 = this.listチップ.Count;
1120 for( int i = 0; i < nチップ数; i++ )
1123 var BPMチップ = this.listチップ[ i ];
1124 if( BPMチップ.チップ種別 != チップ種別.BPM )
1127 // BPMチップより後続の全チップの n発声/描画時刻ms を、新旧BPMの比率(加速率)で修正する。
1128 double db加速率 = BPMチップ.BPM / db現在のBPM; // BPMチップ.dbBPM > 0.0 であることは読み込み時に保証済み。
1129 for( int j = i + 1; j < nチップ数; j++ )
1131 this.listチップ[ j ].発声時刻ms =
1132 this.listチップ[ j ].描画時刻ms =
1133 (long) ( BPMチップ.発声時刻ms + ( ( this.listチップ[ j ].発声時刻ms - BPMチップ.発声時刻ms ) / db加速率 ) );
1136 db現在のBPM = BPMチップ.BPM;
1141 public void t曲データファイルを読み込む_ヘッダだけ( string str曲データファイル名 )
1143 this.list小節長倍率 = new List<double>();
1145 #region " 曲データファイルを読み込む。"
1147 var sr = new StreamReader( str曲データファイル名, Encoding.UTF8 );
1152 //int n現在の小節解像度 = 384;
1153 //double db現在の小節長倍率 = 1.0;
1154 //チップ種別 e現在のチップ = チップ種別.Unknown;
1156 while( !sr.EndOfStream )
1162 string str行 = sr.ReadLine();
1166 #region " 文字列に前処理を行う。"
1169 // 改行は ';' に、TABは空白文字にそれぞれ変換し、先頭末尾の空白を削除する。
1171 str行 = str行.Replace( Environment.NewLine, ";" );
1172 str行 = str行.Replace( '\t', ' ' );
1176 // 行中の '#' 以降はコメントとして除外する。また、コメントだけの行はスキップする。
1180 for( n区切り位置 = 0; n区切り位置 < str行.Length; n区切り位置++ )
1182 if( str行[ n区切り位置 ] == '#' )
1187 continue; // コメントだけの行はスキップ。
1189 if( n区切り位置 < str行.Length )
1191 str行 = str行.Substring( 0, n区切り位置 - 1 );
1198 if( string.IsNullOrEmpty( str行 ) )
1206 #region " ヘッダコマンドの処理を行う。"
1209 if( str行.StartsWith( "Title", StringComparison.OrdinalIgnoreCase ) )
1211 #region " Title コマンド "
1213 string[] items = str行.Split( '=' );
1215 if( items.Length != 2 )
1217 FDK.Log.ERROR( $"Title の書式が不正です。スキップします。[{n行番号}行目]" );
1221 this.Header.str曲名 = items[ 1 ].Trim();
1227 if( str行.StartsWith( "Description", StringComparison.OrdinalIgnoreCase ) )
1229 #region " Description コマンド "
1231 string[] items = str行.Split( '=' );
1233 if( items.Length != 2 )
1235 FDK.Log.ERROR( $"Description の書式が不正です。スキップします。[{n行番号}行目]" );
1239 // 2文字のリテラル "\n" は改行に復号。
1240 this.Header.str説明文 = items[ 1 ].Trim().Replace( @"\n", Environment.NewLine );
1260 /// 現在の Cスコア の内容をデータファイル(*.sstf)に書き出す。
1263 /// 小節線、拍線、Unknown チップは出力しない。
1264 /// 失敗すれば何らかの例外を発出する。
1266 public void t曲データファイルを書き出す( string str曲データファイル名, string strヘッダ行 )
1268 var sw = new StreamWriter( str曲データファイル名, false, Encoding.UTF8 );
1273 sw.WriteLine( $"{strヘッダ行}" ); // strヘッダ行に"{...}"が入ってても大丈夫なようにstring.Format()で囲む。
1278 sw.WriteLine( "Title={0}", ( string.IsNullOrEmpty( this.Header.str曲名 ) ) ? "(no title)" : this.Header.str曲名 );
1279 if( !string.IsNullOrEmpty( this.Header.str説明文 ) )
1281 // 改行コードは、2文字のリテラル "\n" に置換。
1282 sw.WriteLine( "Description=" + this.Header.str説明文.Replace( Environment.NewLine, @"\n" ) );
1288 #region " 全チップの最終小節番号を取得する。"
1291 foreach( var cc in this.listチップ )
1293 if( cc.小節番号 > n最終小節番号 )
1299 for( int n小節番号 = 0; n小節番号 <= n最終小節番号; n小節番号++ )
1301 #region " dicレーン別チップリストの初期化。"
1303 Dictionary<レーン種別, List<チップ>> dicレーン別チップリスト = new Dictionary<レーン種別, List<チップ>>();
1305 foreach( var lane in Enum.GetValues( typeof( レーン種別 ) ) )
1306 dicレーン別チップリスト[ (レーン種別) lane ] = new List<チップ>();
1309 #region " dicレーン別チップリストの構築; n小節番号 の小節に存在するチップのみをレーン別に振り分けて格納する。"
1311 foreach( var cc in this.listチップ )
1313 if( cc.小節番号 > n小節番号 )
1314 break; // listチップは昇順に並んでいるので、これ以上検索しても無駄。
1316 if( cc.チップ種別 == チップ種別.小節線 ||
1317 cc.チップ種別 == チップ種別.拍線 ||
1318 cc.チップ種別 == チップ種別.小節メモ ||
1319 cc.チップ種別 == チップ種別.Unknown )
1321 continue; // これらは出力しないので無視。
1324 if( cc.小節番号 == n小節番号 )
1326 var lane = レーン種別.Bass; // 対応するレーンがなかったら Bass でも返しておく。
1328 foreach( var kvp in dicSSTFレーンチップ対応表 )
1330 if( kvp.Value.Contains( cc.チップ種別 ) )
1337 dicレーン別チップリスト[ lane ].Add( cc );
1343 #region " Part行 出力。"
1345 sw.Write( "Part = {0}", n小節番号.ToString() );
1347 if( this.list小節長倍率[ n小節番号 ] != 1.0 )
1348 sw.Write( "s" + this.list小節長倍率[ n小節番号 ].ToString() );
1350 sw.WriteLine( ";" );
1353 #region " Lane, Resolution, Chip 行 出力。"
1355 foreach( var laneObj in Enum.GetValues( typeof( レーン種別 ) ) )
1357 var lane = (レーン種別) laneObj;
1359 if( dicレーン別チップリスト[ lane ].Count > 0 )
1361 sw.Write( "Lane={0}; ", lane.ToString() );
1363 #region " 新しい解像度を求める。"
1366 foreach( var cc in dicレーン別チップリスト[ lane ] )
1367 n新しい解像度 = FDK.Utilities.最小公倍数を返す( n新しい解像度, cc.小節解像度 );
1370 #region " dicレーン別チップリスト[ lane ] 要素の n小節解像度 と n小節内位置 を n新しい解像度 に合わせて修正する。 "
1372 foreach( var cc in dicレーン別チップリスト[ lane ] )
1374 int n倍率 = n新しい解像度 / cc.小節解像度; // n新しい解像度は n小節解像度 の最小公倍数なので常に割り切れる
1382 sw.Write( "Resolution = {0}; ", n新しい解像度 );
1383 sw.Write( "Chips = " );
1385 for( int i = 0; i < dicレーン別チップリスト[ lane ].Count; i++ )
1387 チップ cc = dicレーン別チップリスト[ lane ][ i ];
1390 sw.Write( cc.小節内位置.ToString() );
1394 #region " (1) 共通属性 "
1396 if( cc.音量 < チップ.最大音量 )
1397 sw.Write( "v" + cc.音量.ToString() );
1400 #region " (2) 専用属性 "
1404 case チップ種別.Ride_Cup:
1408 case チップ種別.HiHat_Open:
1412 case チップ種別.HiHat_HalfOpen:
1416 case チップ種別.HiHat_Foot:
1420 case チップ種別.Snare_OpenRim:
1424 case チップ種別.Snare_ClosedRim:
1428 case チップ種別.Snare_Ghost:
1432 case チップ種別.Tom1_Rim:
1436 case チップ種別.Tom2_Rim:
1440 case チップ種別.Tom3_Rim:
1445 sw.Write( "b" + cc.BPM.ToString() );
1451 // 区切り文字 または 終端文字 を出力
1452 sw.Write( ( i == dicレーン別チップリスト[ lane ].Count - 1 ) ? ";" : "," );
1455 sw.WriteLine( "" ); // 改行
1461 sw.WriteLine( "" ); // 次の Part 前に1行開ける
1466 #region " dicメモ を小節番号で昇順にソートし直した dic昇順メモ を作成する。"
1468 var dic昇順メモ = new Dictionary<int, string>();
1469 int n最大小節番号 = this.最大小節番号;
1471 for( int i = 0; i <= n最大小節番号; i++ )
1473 if( this.dicメモ.ContainsKey( i ) )
1474 dic昇順メモ.Add( i, this.dicメモ[ i ] );
1478 #region " dic昇順メモを出力する。"
1480 foreach( var kvp in dic昇順メモ )
1482 int n小節番号 = kvp.Key;
1484 // 改行コードは、2文字のリ照られる "\n" に置換。
1485 string strメモ = kvp.Value.Replace( Environment.NewLine, @"\n" );
1487 sw.WriteLine( "PartMemo={0},{1}", n小節番号, strメモ );
1499 /// 指定された Config.Speed を考慮し、指定された時間[ms]の間に流れるピクセル数[dpx]を算出して返す。</para>
1502 public int n指定された時間msに対応する符号付きピクセル数を返す( double speed, long n指定時間ms )
1504 return (int) ( n指定時間ms * スコア.基準譜面速度dpxms * speed );
1507 /// 指定された Config.Speed を考慮し、指定された時間[秒]の間に流れるピクセル数[dpx]を算出して返す。
1509 public double n指定された時間secに対応する符号付きピクセル数を返す( double speed, double 指定時間sec )
1511 return ( 指定時間sec * スコア.db基準譜面速度dpxsec * speed );
1513 public double 小節長倍率を取得する( int n小節番号 )
1515 // list小節長倍率 が短ければ増設する。
1516 if( n小節番号 >= this.list小節長倍率.Count )
1518 int n不足数 = n小節番号 - this.list小節長倍率.Count + 1;
1519 for( int i = 0; i < n不足数; i++ )
1520 this.list小節長倍率.Add( 1.0 );
1524 return this.list小節長倍率[ n小節番号 ];
1526 public void 小節長倍率を設定する( int n小節番号, double db倍率 )
1528 // list小節長倍率 が短ければ増設する。
1529 if( n小節番号 >= this.list小節長倍率.Count )
1531 int n不足数 = n小節番号 - this.list小節長倍率.Count + 1;
1532 for( int i = 0; i < n不足数; i++ )
1533 this.list小節長倍率.Add( 1.0 );
1536 // 小節番号に対応付けて倍率を登録する。
1537 this.list小節長倍率[ n小節番号 ] = db倍率;
1541 /// 取出文字列の先頭にある数字(小数点も有効)の連続した部分を取り出して、戻り値として返す。
1542 /// また、取出文字列から取り出した数字文字列部分を除去した文字列を再度格納する。
1544 protected string t指定された文字列の先頭から数字文字列を取り出す( ref string str取出文字列 )
1547 while( ( n桁数 < str取出文字列.Length ) && ( char.IsDigit( str取出文字列[ n桁数 ] ) || str取出文字列[ n桁数 ] == '.' ) )
1553 string str数字文字列 = str取出文字列.Substring( 0, n桁数 );
1554 str取出文字列 = ( n桁数 == str取出文字列.Length ) ? "" : str取出文字列.Substring( n桁数 );