• R/O
  • HTTP
  • SSH
  • HTTPS

CsWin10Desktop3: Commit

Visual C# 7.0, Windows10 Desktop App


Commit MetaInfo

Revisionf21cf3c66e11abe9a74b7c12032d90b4d253df54 (tree)
Time2017-09-23 15:19:01
Authorくまかみ工房 <kumakamikoubou@gmai...>
Commiterくまかみ工房

Log Message

SSTFormat を v3 に更新。

Change Summary

Incremental Difference

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