2 using System.Collections.Generic;
3 using System.Diagnostics;
7 using System.Text.RegularExpressions;
8 using FDK; // for string 拡張
12 public class スコア : IDisposable
17 /// 指定されたコマンド名が対象文字列内で使用されている場合に、パラメータ部分の文字列を返す。
20 /// .dtx や box.def 等で使用されている "#<コマンド名>[:]<パラメータ>[;コメント]" 形式の文字列(対象文字列)について、
21 /// 指定されたコマンドを使用する行であるかどうかを判別し、使用する行であるなら、そのパラメータ部分の文字列を引数に格納し、true を返す。
22 /// 対象文字列のコマンド名が指定したコマンド名と異なる場合には、パラメータ文字列に null を格納して false を返す。
23 /// コマンド名は正しくてもパラメータが存在しない場合には、空文字列("") を格納して true を返す。
25 /// <param name="対象文字列">調べる対象の文字列。(例: "#TITLE: 曲名 ;コメント")</param>
26 /// <param name="コマンド名">調べるコマンドの名前(例:"TITLE")。#は不要、大文字小文字は区別されない。</param>
27 /// <returns>パラメータ文字列の取得に成功したら true、異なるコマンドだったなら false。</returns>
28 public static bool コマンドのパラメータ文字列部分を返す( string 対象文字列, string コマンド名, out string パラメータ文字列 )
30 // コメント部分を除去し、両端をトリムする。なお、全角空白はトリムしない。
31 対象文字列 = 対象文字列.Split( ';' )[ 0 ].Trim( ' ', '\t' );
33 string 正規表現パターン = $@"^\s*#{コマンド名}(:|\s)+(.*)\s*$"; // \s は空白文字。
34 var m = Regex.Match( 対象文字列, 正規表現パターン, RegexOptions.IgnoreCase );
36 if( m.Success && ( 3 <= m.Groups.Count ) )
38 パラメータ文字列 = m.Groups[ 2 ].Value;
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 );
56 /// 1ms あたりの設計ピクセル数 [dpx] 。
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]。割り切れて良かった。
65 public const double 基準譜面速度dpxms = 0.14625 * 2.25; // "* 2.25" は「x1.0はもう少し速くてもいいんではないか?」という感覚的な調整分。
68 /// 1秒あたりの設計ピクセル数 [dpx] 。
70 public const double 基準譜面速度dpxsec = 基準譜面速度dpxms * 1000.0;
72 public static readonly Dictionary<レーン種別, List<チップ種別>> dicSSTFレーンチップ対応表
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 } },
94 public static readonly List<string> 背景動画のデフォルト拡張子s = new List<string>() {
95 ".avi", ".flv", ".mp4", ".wmv", ".mpg", ".mpeg"
98 // プロパティ;読み込み時または編集時に設定される
101 /// 背景動画ファイル名は、sstf ファイルには保存されず、必要時に sstf ファイルと同じフォルダを検索して取得する。
103 public string 背景動画ファイル名 = "";
107 public string 曲名 = "(no title)";
108 public string 説明文 = "";
109 public float サウンドデバイス遅延ms = 0f;
111 public CHeader Header = new CHeader();
113 public List<チップ> チップリスト
118 public List<double> 小節長倍率リスト
129 foreach( チップ chip in this.チップリスト )
131 if( chip.小節番号 > n最大小節番号 )
138 public Dictionary<int, string> dicメモ = new Dictionary<int, string>();
144 this.チップリスト = new List<チップ>();
145 this.小節長倍率リスト = new List<double>();
147 public スコア( string 曲データファイル名, bool 左ライド = false, bool 左チャイナ = false, bool 左スプラッシュ = true ) : this()
149 this.曲データファイルを読み込む( 曲データファイル名, 左ライド, 左チャイナ, 左スプラッシュ );
151 public void Dispose()
156 /// 指定された曲データファイルを読み込む。失敗すれば何らかの例外を発出する。
158 public void 曲データファイルを読み込む( string 曲データファイル名, bool 左ライド = false, bool 左チャイナ = false, bool 左スプラッシュ = true )
162 this.小節長倍率リスト = new List<double>();
163 this.dicメモ = new Dictionary<int, string>();
166 #region " 背景動画ファイル名を更新する。"
169 ( from file in Directory.GetFiles( Path.GetDirectoryName( 曲データファイル名 ) )
170 where SSTFormat.スコア.背景動画のデフォルト拡張子s.Any( 拡張子名 => ( Path.GetExtension( file ).ToLower() == 拡張子名 ) )
171 select file ).FirstOrDefault();
174 #region " 曲データファイルを読み込む。"
176 var sr = new StreamReader( 曲データファイル名, Encoding.UTF8 );
182 チップ種別 e現在のチップ = チップ種別.Unknown;
184 while( false == sr.EndOfStream )
188 string 行 = sr.ReadLine();
192 #region " 改行は ';' に、TABは空白文字にそれぞれ変換し、先頭末尾の空白を削除する。"
194 行 = 行.Replace( Environment.NewLine, ";" );
195 行 = 行.Replace( '\t', ' ' );
199 #region " 行中の '#' 以降はコメントとして除外する。また、コメントだけの行はスキップする。"
202 for( 区切り位置 = 0; 区切り位置 < 行.Length; 区切り位置++ )
204 if( 行[ 区切り位置 ] == '#' )
208 continue; // コメントだけの行はスキップ。
209 if( 区切り位置 < 行.Length )
211 行 = 行.Substring( 0, 区切り位置 - 1 );
216 #region " 空行ならこの行はスキップする。"
218 if( 行.Nullまたは空である() )
225 #region " ヘッダコマンドの処理を行う。"
227 if( 行.StartsWith( "Title", StringComparison.OrdinalIgnoreCase ) )
229 #region " Title コマンド "
231 string[] items = 行.Split( '=' );
233 if( items.Length != 2 )
235 FDK.Log.ERROR( $"Title の書式が不正です。スキップします。[{行番号}行目]" );
239 this.Header.曲名 = items[ 1 ].Trim();
245 if( 行.StartsWith( "Description", StringComparison.OrdinalIgnoreCase ) )
247 #region " Description コマンド "
249 string[] items = 行.Split( '=' );
251 if( items.Length != 2 )
253 FDK.Log.ERROR( $"Description の書式が不正です。スキップします。[{行番号}行目]" );
257 // 2文字のリテラル "\n" は改行に復号。
258 this.Header.説明文 = items[ 1 ].Trim().Replace( @"\n", Environment.NewLine );
264 if( 行.StartsWith( "SoundDevice.Delay", StringComparison.OrdinalIgnoreCase ) )
266 #region " SoundDevice.Delay コマンド "
268 string[] items = 行.Split( '=' );
270 if( items.Length != 2 )
272 FDK.Log.ERROR( $"SoundDevice.Delay の書式が不正です。スキップします。[{行番号}行目]" );
276 // 2文字のリテラル "\n" は改行に復号。
278 if( float.TryParse( items[ 1 ].Trim().Replace( @"\n", Environment.NewLine ), out value ) )
279 this.Header.サウンドデバイス遅延ms = value;
290 #region " メモ(小節単位)の処理を行う。"
292 if( 行.StartsWith( "PartMemo", StringComparison.OrdinalIgnoreCase ) )
294 #region " '=' 以前を除去する。"
296 int 等号位置 = 行.IndexOf( '=' );
299 FDK.Log.ERROR( $"PartMemo の書式が不正です。スキップします。[{行番号}]行目]" );
302 行 = 行.Substring( 等号位置 + 1 ).Trim();
303 if( 行.Nullまたは空である() )
305 FDK.Log.ERROR( $"PartMemo の書式が不正です。スキップします。[{行番号}]行目]" );
310 #region " カンマ位置を取得する。"
312 int カンマ位置 = 行.IndexOf( ',' );
315 FDK.Log.ERROR( $"PartMemo の書式が不正です。スキップします。[{行番号}]行目]" );
320 #region " 小節番号を取得する。"
322 string 小説番号文字列 = 行.Substring( 0, カンマ位置 );
324 if( false == int.TryParse( 小説番号文字列, out 小節番号 ) || ( 0 > 小節番号 ) )
326 FDK.Log.ERROR( $"PartMemo の小節番号が不正です。スキップします。[{行番号}]行目]" );
333 string メモ = 行.Substring( カンマ位置 + 1 );
335 // 2文字のリテラル文字列 "\n" は改行に復号。
336 メモ = メモ.Replace( @"\n", Environment.NewLine );
339 #region " メモが空文字列でないなら dicメモ に登録すると同時に、チップとしても追加する。"
341 if( メモ.Nullでも空でもない() )
343 this.dicメモ.Add( 小節番号, メモ );
361 // 上記行頭コマンド以外は、チップ記述行だと見なす。
363 #region " チップ記述コマンドの処理を行う。"
367 string[] tokens = 行.Split( new char[] { ';', ':' } );
370 foreach( string token in tokens )
374 #region " トークンを区切り文字 '=' で strコマンド と strパラメータ に分割し、それぞれの先頭末尾の空白を削除する。"
376 string[] items = token.Split( '=' );
378 if( 2 != items.Length )
380 if( 0 == token.Trim().Length ) // 空文字列(行末など)は不正じゃない。
383 FDK.Log.ERROR( $"コマンドとパラメータの記述書式が不正です。このコマンドをスキップします。[{行番号}行目]" );
387 string コマンド = items[ 0 ].Trim();
388 string パラメータ = items[ 1 ].Trim();
394 if( コマンド.Equals( "Part", StringComparison.OrdinalIgnoreCase ) )
396 #region " Part(小節番号指定)コマンド "
399 #region " 小節番号を取得・設定。"
401 string 小節番号文字列 = this.指定された文字列の先頭から数字文字列を取り出す( ref パラメータ );
402 if( 小節番号文字列.Nullまたは空である() )
404 FDK.Log.ERROR( $"Part(小節番号)コマンドに小節番号の記述がありません。このコマンドをスキップします。[{行番号}行目]" );
409 if( false == int.TryParse( 小節番号文字列, out 小節番号 ) )
411 FDK.Log.ERROR( $"Part(小節番号)コマンドの小節番号が不正です。このコマンドをスキップします。[{行番号}行目]" );
416 FDK.Log.ERROR( $"Part(小節番号)コマンドの小節番号が負数です。このコマンドをスキップします。[{行番号}行目]" );
423 #region " Part 属性があれば取得する。"
425 while( 0 < パラメータ.Length )
428 char 属性ID = char.ToLower( パラメータ[ 0 ] );
433 #region " 小節長倍率(>0) → list小節長倍率 "
435 パラメータ = パラメータ.Substring( 1 ).Trim();
437 string 小節長倍率文字列 = this.指定された文字列の先頭から数字文字列を取り出す( ref パラメータ );
438 if( 小節長倍率文字列.Nullまたは空である() )
440 FDK.Log.ERROR( $"Part(小節番号)コマンドに小節長倍率の記述がありません。この属性をスキップします。[{行番号}行目]" );
443 パラメータ = パラメータ.Trim();
446 if( false == double.TryParse( 小節長倍率文字列, out 小節長倍率 ) )
448 FDK.Log.ERROR( $"Part(小節番号)コマンドの小節長倍率が不正です。この属性をスキップします。[{行番号}行目]" );
453 FDK.Log.ERROR( $"Part(小節番号)コマンドの小節長倍率が 0.0 または負数です。この属性をスキップします。[{行番号}行目]" );
457 // 小節長倍率辞書に追加 or 上書き更新。
458 this.小節長倍率を設定する( 現在の小節番号, 小節長倍率 );
473 if( コマンド.Equals( "Lane", StringComparison.OrdinalIgnoreCase ) )
475 #region " Lane(レーン指定)コマンド(チップ種別の仮決め)"
477 if( パラメータ.Equals( "LeftCrash", StringComparison.OrdinalIgnoreCase ) )
478 e現在のチップ = チップ種別.LeftCrash;
480 else if( パラメータ.Equals( "Ride", StringComparison.OrdinalIgnoreCase ) )
481 e現在のチップ = チップ種別.Ride;
483 else if( パラメータ.Equals( "China", StringComparison.OrdinalIgnoreCase ) )
484 e現在のチップ = チップ種別.China;
486 else if( パラメータ.Equals( "Splash", StringComparison.OrdinalIgnoreCase ) )
487 e現在のチップ = チップ種別.Splash;
489 else if( パラメータ.Equals( "HiHat", StringComparison.OrdinalIgnoreCase ) )
490 e現在のチップ = チップ種別.HiHat_Close;
492 else if( パラメータ.Equals( "Snare", StringComparison.OrdinalIgnoreCase ) )
493 e現在のチップ = チップ種別.Snare;
495 else if( パラメータ.Equals( "Bass", StringComparison.OrdinalIgnoreCase ) )
496 e現在のチップ = チップ種別.Bass;
498 else if( パラメータ.Equals( "Tom1", StringComparison.OrdinalIgnoreCase ) )
499 e現在のチップ = チップ種別.Tom1;
501 else if( パラメータ.Equals( "Tom2", StringComparison.OrdinalIgnoreCase ) )
502 e現在のチップ = チップ種別.Tom2;
504 else if( パラメータ.Equals( "Tom3", StringComparison.OrdinalIgnoreCase ) )
505 e現在のチップ = チップ種別.Tom3;
507 else if( パラメータ.Equals( "RightCrash", StringComparison.OrdinalIgnoreCase ) )
508 e現在のチップ = チップ種別.RightCrash;
510 else if( パラメータ.Equals( "BPM", StringComparison.OrdinalIgnoreCase ) )
513 else if( パラメータ.Equals( "Song", StringComparison.OrdinalIgnoreCase ) )
514 e現在のチップ = チップ種別.背景動画;
516 FDK.Log.ERROR( $"Lane(レーン指定)コマンドのパラメータ記述 '{パラメータ}' が不正です。このコマンドをスキップします。[{行番号}行目]" );
522 if( コマンド.Equals( "Resolution", StringComparison.OrdinalIgnoreCase ) )
524 #region " Resolution(小節解像度指定)コマンド "
527 if( false == int.TryParse( パラメータ, out 解像度 ) )
529 FDK.Log.ERROR( $"Resolution(小節解像度指定)コマンドの解像度が不正です。このコマンドをスキップします。[{行番号}行目]" );
534 FDK.Log.ERROR( $"Resolution(小節解像度指定)コマンドの解像度は 1 以上でなければなりません。このコマンドをスキップします。[{行番号}行目]" );
543 if( コマンド.Equals( "Chips", StringComparison.OrdinalIgnoreCase ) )
545 #region " Chips(チップ指定)コマンド "
548 // パラメータを区切り文字 ',' でチップトークンに分割。
549 string[] chipTokens = パラメータ.Split( ',' );
552 for( int i = 0; i < chipTokens.Length; i++ )
554 chipTokens[ i ].Trim();
558 if( 0 == chipTokens[ i ].Length )
564 var chip = new チップ() {
570 chip.可視 = chip.可視の初期値;
571 if( chip.チップ種別 == チップ種別.China ) chip.チップ内文字列 = "C N";
572 if( chip.チップ種別 == チップ種別.Splash ) chip.チップ内文字列 = "S P";
576 #region " チップ位置を取得する。"
579 string 位置番号文字列 = this.指定された文字列の先頭から数字文字列を取り出す( ref chipTokens[ i ] );
580 chipTokens[ i ].Trim();
583 if( 位置番号文字列.Nullまたは空である() )
585 FDK.Log.ERROR( $"チップの位置指定の記述がありません。このチップをスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
590 if( false == int.TryParse( 位置番号文字列, out チップ位置 ) )
592 FDK.Log.ERROR( $"チップの位置指定の記述が不正です。このチップをスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
597 if( ( 0 > チップ位置 ) || ( チップ位置 >= 現在の小節解像度 ) )
599 FDK.Log.ERROR( $"チップの位置が負数であるか解像度(Resolution)以上の値になっています。このチップをスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
606 #region " 共通属性・レーン別属性があれば取得する。"
608 while( chipTokens[ i ].Length > 0 )
611 char 属性ID = char.ToLower( chipTokens[ i ][ 0 ] );
618 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
619 string 音量文字列 = this.指定された文字列の先頭から数字文字列を取り出す( ref chipTokens[ i ] );
620 chipTokens[ i ].Trim();
625 if( 音量文字列.Nullまたは空である() )
627 FDK.Log.ERROR( $"チップの音量指定の記述がありません。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
632 if( false == int.TryParse( 音量文字列, out チップ音量 ) )
634 FDK.Log.ERROR( $"チップの音量指定の記述が不正です。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
639 if( ( 1 > チップ音量 ) || ( チップ音量 > チップ.最大音量 ) )
641 FDK.Log.ERROR( $"チップの音量が適正範囲(1~{チップ.最大音量})を超えています。このチップをスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
655 #region " case LeftCymbal "
657 case チップ種別.LeftCrash:
661 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
662 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
670 #region " case Ride "
679 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
680 chip.チップ種別 = チップ種別.Ride_Cup;
688 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
689 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
697 #region " case China "
703 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
704 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
712 #region " case Splash "
718 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
719 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
727 #region " case HiHat "
729 case チップ種別.HiHat_Close:
730 case チップ種別.HiHat_HalfOpen:
731 case チップ種別.HiHat_Open:
732 case チップ種別.HiHat_Foot:
736 #region " HiHat.オープン "
738 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
739 chip.チップ種別 = チップ種別.HiHat_Open;
743 else if( 属性ID == 'h' )
745 #region " HiHat.ハーフオープン "
747 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
748 chip.チップ種別 = チップ種別.HiHat_HalfOpen;
752 else if( 属性ID == 'c' )
754 #region " HiHat.クローズ "
756 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
757 chip.チップ種別 = チップ種別.HiHat_Close;
761 else if( 属性ID == 'f' )
763 #region " HiHat.フットスプラッシュ "
765 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
766 chip.チップ種別 = チップ種別.HiHat_Foot;
774 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
775 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
783 #region " case Snare "
786 case チップ種別.Snare_ClosedRim:
787 case チップ種別.Snare_OpenRim:
788 case チップ種別.Snare_Ghost:
792 #region " Snare.オープンリム "
794 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
795 chip.チップ種別 = チップ種別.Snare_OpenRim;
799 else if( 属性ID == 'c' )
801 #region " Snare.クローズドリム "
803 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
804 chip.チップ種別 = チップ種別.Snare_ClosedRim;
808 else if( 属性ID == 'g' )
810 #region " Snare.ゴースト "
812 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
813 chip.チップ種別 = チップ種別.Snare_Ghost;
821 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
822 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
830 #region " case Bass "
836 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
837 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
845 #region " case Tom1 "
854 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
855 chip.チップ種別 = チップ種別.Tom1_Rim;
863 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
864 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
872 #region " case Tom2 "
881 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
882 chip.チップ種別 = チップ種別.Tom2_Rim;
890 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
891 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
899 #region " case Tom3 "
908 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
909 chip.チップ種別 = チップ種別.Tom3_Rim;
917 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
918 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
926 #region " case RightCymbal "
928 case チップ種別.RightCrash:
932 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
933 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
950 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
952 string BPM文字列 = this.指定された文字列の先頭から数字文字列を取り出す( ref chipTokens[ i ] );
953 chipTokens[ i ].Trim();
955 if( BPM文字列.Nullまたは空である() )
957 FDK.Log.ERROR( $"BPM数値の記述がありません。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
963 if( false == double.TryParse( BPM文字列, out BPM ) || ( 0.0 >= BPM ) )
965 FDK.Log.ERROR( $"BPM数値の記述が不正です。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
970 chip.チップ内文字列 = BPM.ToString( "###.##" );
978 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
979 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
987 #region " case Song "
993 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
994 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
1006 chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1007 FDK.Log.ERROR( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
1014 this.チップリスト.Add( chip );
1022 FDK.Log.ERROR( $"不正なコマンド「{コマンド}」が存在します。[{行番号}行目]" );
1034 #region " 拍線の追加。小節線を先に追加すると小節が1つ増えるので、先に拍線から追加する。"
1036 int 最大小節番号 = this.最大小節番号; // this.最大小節番号 プロパティはチップ数に依存して変化するので、for 文には組み込まないこと。
1038 for( int i = 0; i <= 最大小節番号; i++ )
1040 double 小節長倍率 = this.小節長倍率を取得する( i );
1041 for( int n = 1; n * 0.25 < 小節長倍率; n++ )
1047 小節内位置 = (int) ( ( n * 0.25 ) * 100 ),
1048 小節解像度 = (int) ( 小節長倍率 * 100 ),
1056 最大小節番号 = this.最大小節番号;
1058 for( int i = 0; i <= 最大小節番号 + 1; i++ )
1070 #region " 小節の先頭 の追加。"
1072 最大小節番号 = this.最大小節番号;
1074 // 「小節の先頭」チップは、小節線と同じく、全小節の先頭位置に置かれる。
1075 // 小節線には今後譜面作者によって位置をアレンジできる可能性を残したいが、
1076 // ビュアーが小節の先頭位置を検索するためには、小節の先頭に置かれるチップが必要になる。
1077 // よって、譜面作者の影響を受けない(ビュアー用の)チップを機械的に配置する。
1079 for( int i = 0; i <= 最大小節番号 + 1; i++ )
1084 チップ種別 = チップ種別.小節の先頭,
1094 #region " 全チップの発声/描画時刻と譜面内位置を計算する。"
1097 // 1. BPMチップを無視し(初期BPMで固定)、dic小節長倍率, Cチップ.小節解像度, Cチップ.小節内位置 から両者を計算する。
1098 // 以下、listチップが小節番号順にソートされているという前提で。
1100 double チップが存在する小節の先頭時刻ms = 0.0;
1103 foreach( チップ chip in this.チップリスト )
1105 #region " チップの小節番号が現在の小節番号よりも大きい場合、チップが存在する小節に至るまで、「dbチップが存在する小節の先頭時刻ms」を更新する。"
1107 while( 現在の小節の番号 < chip.小節番号 )
1109 double 現在の小節の小節長倍率 = this.小節長倍率を取得する( 現在の小節の番号 );
1110 チップが存在する小節の先頭時刻ms += BPM初期値固定での1小節4拍の時間ms * 現在の小節の小節長倍率;
1112 現在の小節の番号++; // 現在の小節番号 が chip.小節番号 に追いつくまでループする。
1116 #region " チップの発声/描画時刻を求める。"
1118 double チップが存在する小節の小節長倍率 = this.小節長倍率を取得する( 現在の小節の番号 );
1122 (long) ( チップが存在する小節の先頭時刻ms + ( BPM初期値固定での1小節4拍の時間ms * チップが存在する小節の小節長倍率 * chip.小節内位置 ) / chip.小節解像度 );
1127 // 2. BPMチップを考慮しながら調整する。(譜面内位置grid はBPMの影響を受けないので無視)
1129 double 現在のBPM = スコア.初期BPM;
1130 int チップ数 = this.チップリスト.Count;
1131 for( int i = 0; i < チップ数; i++ )
1134 var BPMチップ = this.チップリスト[ i ];
1135 if( BPMチップ.チップ種別 != チップ種別.BPM )
1138 // BPMチップより後続の全チップの n発声/描画時刻ms を、新旧BPMの比率(加速率)で修正する。
1139 double 加速率 = BPMチップ.BPM / 現在のBPM; // BPMチップ.dbBPM > 0.0 であることは読み込み時に保証済み。
1140 for( int j = i + 1; j < チップ数; j++ )
1142 this.チップリスト[ j ].発声時刻ms =
1143 this.チップリスト[ j ].描画時刻ms =
1144 (long) ( BPMチップ.発声時刻ms + ( ( this.チップリスト[ j ].発声時刻ms - BPMチップ.発声時刻ms ) / 加速率 ) );
1147 現在のBPM = BPMチップ.BPM;
1152 public void 曲データファイルを読み込む_ヘッダだけ( string 曲データファイル名 )
1154 this.小節長倍率リスト = new List<double>();
1156 #region " 曲データファイルを読み込む。"
1158 var sr = new StreamReader( 曲データファイル名, Encoding.UTF8 );
1163 while( false == sr.EndOfStream )
1168 string 行 = sr.ReadLine();
1172 #region " 文字列に前処理を行う。"
1175 // 改行は ';' に、TABは空白文字にそれぞれ変換し、先頭末尾の空白を削除する。
1176 行 = 行.Replace( Environment.NewLine, ";" );
1177 行 = 行.Replace( '\t', ' ' );
1180 // 行中の '#' 以降はコメントとして除外する。また、コメントだけの行はスキップする。
1182 for( 区切り位置 = 0; 区切り位置 < 行.Length; 区切り位置++ )
1184 if( 行[ 区切り位置 ] == '#' )
1189 continue; // コメントだけの行はスキップ。
1191 if( 区切り位置 < 行.Length )
1193 行 = 行.Substring( 0, 区切り位置 - 1 );
1198 if( 行.Nullまたは空である() )
1205 #region " ヘッダコマンドの処理を行う。"
1208 if( 行.StartsWith( "Title", StringComparison.OrdinalIgnoreCase ) )
1210 #region " Title コマンド "
1212 string[] items = 行.Split( '=' );
1214 if( 2 != items.Length )
1216 FDK.Log.ERROR( $"Title の書式が不正です。スキップします。[{行番号}行目]" );
1220 this.Header.曲名 = items[ 1 ].Trim();
1226 if( 行.StartsWith( "Description", StringComparison.OrdinalIgnoreCase ) )
1228 #region " Description コマンド "
1230 string[] items = 行.Split( '=' );
1232 if( 2 != items.Length )
1234 FDK.Log.ERROR( $"Description の書式が不正です。スキップします。[{行番号}行目]" );
1238 // 2文字のリテラル "\n" は改行に復号。
1239 this.Header.説明文 = items[ 1 ].Trim().Replace( @"\n", Environment.NewLine );
1259 /// 現在の スコア の内容をデータファイル(*.sstf)に書き出す。
1262 /// 小節線、拍線、Unknown チップは出力しない。
1263 /// 失敗すれば何らかの例外を発出する。
1265 public void 曲データファイルを書き出す( string 曲データファイル名, string ヘッダ行 )
1267 var sw = new StreamWriter( 曲データファイル名, false, Encoding.UTF8 );
1271 sw.WriteLine( $"{ヘッダ行}" ); // strヘッダ行に"{...}"が入ってても大丈夫なようにstring.Format()で囲む。
1275 sw.WriteLine( "Title={0}", ( string.IsNullOrEmpty( this.Header.曲名 ) ) ? "(no title)" : this.Header.曲名 );
1276 if( this.Header.説明文.Nullでも空でもない() )
1278 // 改行コードは、2文字のリテラル "\n" に置換。
1279 sw.WriteLine( "Description=" + this.Header.説明文.Replace( Environment.NewLine, @"\n" ) );
1281 sw.WriteLine( "SoundDevice.Delay={0}", this.Header.サウンドデバイス遅延ms );
1286 #region " 全チップの最終小節番号を取得する。"
1289 foreach( var cc in this.チップリスト )
1291 if( cc.小節番号 > 最終小節番号 )
1297 for( int 小節番号 = 0; 小節番号 <= 最終小節番号; 小節番号++ )
1299 #region " dicレーン別チップリストの初期化。"
1301 var dicレーン別チップリスト = new Dictionary<レーン種別, List<チップ>>();
1302 foreach( var lane in Enum.GetValues( typeof( レーン種別 ) ) )
1303 dicレーン別チップリスト[ (レーン種別) lane ] = new List<チップ>();
1306 #region " dicレーン別チップリストの構築; 小節番号 の小節に存在するチップのみをレーン別に振り分けて格納する。"
1308 foreach( var cc in this.チップリスト )
1310 if( cc.小節番号 > 小節番号 )
1311 break; // チップリストは昇順に並んでいるので、これ以上検索しても無駄。
1313 if( cc.チップ種別 == チップ種別.小節線 ||
1314 cc.チップ種別 == チップ種別.拍線 ||
1315 cc.チップ種別 == チップ種別.小節メモ ||
1316 cc.チップ種別 == チップ種別.Unknown )
1318 continue; // これらは出力しないので無視。
1321 if( cc.小節番号 == 小節番号 )
1323 var lane = レーン種別.Bass; // 対応するレーンがなかったら Bass でも返しておく。
1325 foreach( var kvp in dicSSTFレーンチップ対応表 )
1327 if( kvp.Value.Contains( cc.チップ種別 ) )
1334 dicレーン別チップリスト[ lane ].Add( cc );
1340 #region " Part行 出力。"
1342 sw.Write( $"Part = {小節番号.ToString()}" );
1344 if( this.小節長倍率リスト[ 小節番号 ] != 1.0 )
1345 sw.Write( $"s{this.小節長倍率リスト[ 小節番号 ].ToString()}" );
1347 sw.WriteLine( ";" );
1350 #region " Lane, Resolution, Chip 行 出力。"
1352 foreach( var laneObj in Enum.GetValues( typeof( レーン種別 ) ) )
1354 var lane = (レーン種別) laneObj;
1356 if( 0 < dicレーン別チップリスト[ lane ].Count )
1358 sw.Write( $"Lane={lane.ToString()}; " );
1360 #region " 新しい解像度を求める。"
1363 foreach( var cc in dicレーン別チップリスト[ lane ] )
1364 新しい解像度 = FDK.Utilities.最小公倍数を返す( 新しい解像度, cc.小節解像度 );
1367 #region " dicレーン別チップリスト[ lane ] 要素の n小節解像度 と n小節内位置 を n新しい解像度 に合わせて修正する。 "
1369 foreach( var cc in dicレーン別チップリスト[ lane ] )
1371 int 倍率 = 新しい解像度 / cc.小節解像度; // 新しい解像度 は 小節解像度 の最小公倍数なので常に割り切れる。
1379 sw.Write( $"Resolution = {新しい解像度}; " );
1380 sw.Write( "Chips = " );
1382 for( int i = 0; i < dicレーン別チップリスト[ lane ].Count; i++ )
1384 チップ cc = dicレーン別チップリスト[ lane ][ i ];
1387 sw.Write( cc.小節内位置.ToString() );
1391 #region " (1) 共通属性 "
1393 if( cc.音量 < チップ.最大音量 )
1394 sw.Write( $"v{cc.音量.ToString()}" );
1397 #region " (2) 専用属性 "
1401 case チップ種別.Ride_Cup:
1405 case チップ種別.HiHat_Open:
1409 case チップ種別.HiHat_HalfOpen:
1413 case チップ種別.HiHat_Foot:
1417 case チップ種別.Snare_OpenRim:
1421 case チップ種別.Snare_ClosedRim:
1425 case チップ種別.Snare_Ghost:
1429 case チップ種別.Tom1_Rim:
1433 case チップ種別.Tom2_Rim:
1437 case チップ種別.Tom3_Rim:
1442 sw.Write( $"b{cc.BPM.ToString()}" );
1448 // 区切り文字 または 終端文字 を出力
1449 sw.Write( ( i == dicレーン別チップリスト[ lane ].Count - 1 ) ? ";" : "," );
1452 sw.WriteLine( "" ); // 改行
1458 sw.WriteLine( "" ); // 次の Part 前に1行あける。
1463 #region " dicメモ を小節番号で昇順にソートし直した dic昇順メモ を作成する。"
1465 var dic昇順メモ = new Dictionary<int, string>();
1466 int 最大小節番号 = this.最大小節番号;
1468 for( int i = 0; i <= 最大小節番号; i++ )
1470 if( this.dicメモ.ContainsKey( i ) )
1471 dic昇順メモ.Add( i, this.dicメモ[ i ] );
1475 #region " dic昇順メモを出力する。"
1477 foreach( var kvp in dic昇順メモ )
1481 // 改行コードは、2文字のリ照られる "\n" に置換。
1482 string メモ = kvp.Value.Replace( Environment.NewLine, @"\n" );
1484 sw.WriteLine( $"PartMemo={小節番号},{メモ}" );
1497 /// 指定された Config.Speed を考慮し、指定された時間[ms]の間に流れるピクセル数[dpx]を算出して返す。</para>
1500 public int 指定された時間msに対応する符号付きピクセル数を返す( double speed, long 指定時間ms )
1502 return (int) ( 指定時間ms * スコア.基準譜面速度dpxms * speed );
1505 /// 指定された Config.Speed を考慮し、指定された時間[秒]の間に流れるピクセル数[dpx]を算出して返す。
1507 public double 指定された時間secに対応する符号付きピクセル数を返す( double speed, double 指定時間sec )
1509 return ( 指定時間sec * スコア.基準譜面速度dpxsec * speed );
1511 public double 小節長倍率を取得する( int 小節番号 )
1513 // 小節長倍率リスト が短ければ増設する。
1514 if( 小節番号 >= this.小節長倍率リスト.Count )
1516 int 不足数 = 小節番号 - this.小節長倍率リスト.Count + 1;
1517 for( int i = 0; i < 不足数; i++ )
1518 this.小節長倍率リスト.Add( 1.0 );
1522 return this.小節長倍率リスト[ 小節番号 ];
1524 public void 小節長倍率を設定する( int 小節番号, double 倍率 )
1526 // 小節長倍率リスト が短ければ増設する。
1527 if( 小節番号 >= this.小節長倍率リスト.Count )
1529 int 不足数 = 小節番号 - this.小節長倍率リスト.Count + 1;
1530 for( int i = 0; i < 不足数; i++ )
1531 this.小節長倍率リスト.Add( 1.0 );
1534 // 小節番号に対応付けて倍率を登録する。
1535 this.小節長倍率リスト[ 小節番号 ] = 倍率;
1539 /// 取出文字列の先頭にある数字(小数点も有効)の連続した部分を取り出して、戻り値として返す。
1540 /// また、取出文字列から取り出した数字文字列部分を除去した文字列を再度格納する。
1542 protected string 指定された文字列の先頭から数字文字列を取り出す( ref string 取出文字列 )
1545 while( ( 桁数 < 取出文字列.Length ) && ( char.IsDigit( 取出文字列[ 桁数 ] ) || 取出文字列[ 桁数 ] == '.' ) )
1551 string 数字文字列 = 取出文字列.Substring( 0, 桁数 );
1552 取出文字列 = ( 桁数 == 取出文字列.Length ) ? "" : 取出文字列.Substring( 桁数 );