• R/O
  • HTTP
  • SSH
  • HTTPS

CsWin10Desktop3: Commit

Visual C# 7.0, Windows10 Desktop App


Commit MetaInfo

Revisionf7b13cf50d1980515e344dcb67639fb5430084d1 (tree)
Time2017-10-07 20:03:22
Authorくまかみ工房 <kumakamikoubou@gmai...>
Commiterくまかみ工房

Log Message

SSTFormat を v3 に更新し忘れていたので更新。

Change Summary

Incremental Difference

--- a/SSTFormat/SSTFormat.csproj
+++ b/SSTFormat/SSTFormat.csproj
@@ -44,6 +44,9 @@
4444 <ItemGroup>
4545 <Reference Include="System" />
4646 <Reference Include="System.Core" />
47+ <Reference Include="System.ValueTuple, Version=4.0.2.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
48+ <HintPath>..\packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll</HintPath>
49+ </Reference>
4750 <Reference Include="System.Xml.Linq" />
4851 <Reference Include="System.Data.DataSetExtensions" />
4952 <Reference Include="Microsoft.CSharp" />
@@ -72,6 +75,9 @@
7275 <ItemGroup>
7376 <Content Include="仕様履歴.txt" />
7477 </ItemGroup>
78+ <ItemGroup>
79+ <None Include="packages.config" />
80+ </ItemGroup>
7581 <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
7682 <!-- To modify your build process, add your task inside one of the targets below and uncomment it.
7783 Other similar extension points exist, see Microsoft.Common.targets.
--- /dev/null
+++ b/SSTFormat/packages.config
@@ -0,0 +1,4 @@
1+<?xml version="1.0" encoding="utf-8"?>
2+<packages>
3+ <package id="System.ValueTuple" version="4.4.0" targetFramework="net452" />
4+</packages>
\ No newline at end of file
--- a/SSTFormat/v3/DTXReader.cs
+++ b/SSTFormat/v3/DTXReader.cs
@@ -225,7 +225,7 @@ namespace SSTFormat.v3
225225 if( "title" != コマンド.ToLower() )
226226 return false;
227227
228- スコア.Header.曲名 = パラメータ;
228+ スコア.曲名 = パラメータ;
229229
230230 return true;
231231 }
@@ -234,7 +234,7 @@ namespace SSTFormat.v3
234234 if( "comment" != コマンド.ToLower() )
235235 return false;
236236
237- スコア.Header.説明文 = パラメータ;
237+ スコア.説明文 = パラメータ;
238238
239239 return true;
240240 }
--- a/SSTFormat/v3/スコア.cs
+++ b/SSTFormat/v3/スコア.cs
@@ -4,56 +4,18 @@ using System.Diagnostics;
44 using System.IO;
55 using System.Linq;
66 using System.Text;
7-using System.Text.RegularExpressions;
87
98 namespace SSTFormat.v3
109 {
1110 public class スコア : IDisposable
1211 {
13- // ヘルパ
14-
1512 /// <summary>
16- /// 指定されたコマンド名が対象文字列内で使用されている場合に、パラメータ部分の文字列を返す。
13+ /// このソースで対応するバージョン。
1714 /// </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 );
15+ public Version SSTFVERSION { get; } = new Version( 3, 0, 0, 0 );
5616
17+ #region " 定数 "
18+ //----------------
5719 public const double 初期BPM = 120.0;
5820 public const double 初期小節解像度 = 480.0;
5921 public const double BPM初期値固定での1小節4拍の時間ms = ( 60.0 * 1000 ) / ( スコア.初期BPM / 4.0 );
@@ -73,7 +35,6 @@ namespace SSTFormat.v3
7335 /// 1秒あたりの設計ピクセル数 [dpx] 。
7436 /// </summary>
7537 public const double 基準譜面速度dpxsec = 基準譜面速度dpxms * 1000.0;
76-
7738 public static readonly Dictionary<レーン種別, List<チップ種別>> dicSSTFレーンチップ対応表
7839 #region " *** "
7940 //-----------------
@@ -94,45 +55,67 @@ namespace SSTFormat.v3
9455 };
9556 //-----------------
9657 #endregion
97-
98- // 背景動画のデフォルト拡張子
9958 public static readonly List<string> 背景動画のデフォルト拡張子s = new List<string>() {
10059 ".avi", ".flv", ".mp4", ".wmv", ".mpg", ".mpeg"
10160 };
61+ //----------------
62+ #endregion
10263
103- // プロパティ;読み込み時または編集時に設定される
64+ // ヘッダ
10465
66+ /// <summary>
67+ /// ファイルのSSTFバージョン。
68+ /// ファイルの先頭行に記載される。
69+ /// </summary>
70+ public Version SSTFバージョン
71+ {
72+ get;
73+ protected set;
74+ } = new Version( 1, 0, 0, 0 ); // ファイルにバージョンの指定がない場合は v1 とみなす。
75+ public string 曲名
76+ {
77+ get;
78+ set;
79+ } = "(no title)";
80+ public string 説明文
81+ {
82+ get;
83+ set;
84+ } = "";
85+ public float サウンドデバイス遅延ms
86+ {
87+ get;
88+ set;
89+ } = 0f;
90+ /// <summary>
91+ /// 0.00~9.99。
92+ /// </summary>
93+ public float 難易度
94+ {
95+ get;
96+ set;
97+ } = 0.0f;
10598 /// <remarks>
10699 /// 背景動画ファイル名は、sstf ファイルには保存されず、必要時に sstf ファイルと同じフォルダを検索して取得する。
107100 /// </remarks>
108101 public string 背景動画ファイル名 = "";
109102
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; }
103+ // その他プロパティ
135104
105+ public List<チップ> チップリスト
106+ {
107+ get;
108+ protected set;
109+ } = new List<チップ>();
110+ /// <summary>
111+ /// インデックス番号が小節番号を表すので、
112+ /// 小節 0 から最大小節まで、すべての小節の倍率がこのリストに含まれる。
113+ /// </summary>
114+ public List<double> 小節長倍率リスト
115+ {
116+ get;
117+ protected set;
118+ } = new List<double>();
136119 public int 最大小節番号
137120 {
138121 get
@@ -148,1442 +131,1325 @@ namespace SSTFormat.v3
148131 return 最大小節番号;
149132 }
150133 }
134+ public Dictionary<int, string> dicメモ
135+ {
136+ get;
137+ protected set;
138+ } = new Dictionary<int, string>();
151139
152- public Dictionary<int, string> dicメモ { get; protected set; } = new Dictionary<int, string>();
153-
154- // メソッド
155140
156141 public スコア()
157142 {
158- this.Header.SSTFバージョン = new Version( 2, 0, 0, 0 ); // このソースで対応するバージョン
159- this.チップリスト = new List<チップ>();
160- this.小節長倍率リスト = new List<double>();
143+ this._初期化する();
161144 }
162-
163- public スコア( string 曲データファイル名 ) : this()
145+ public スコア( string 曲データファイル名 )
146+ : this()
164147 {
165148 this.曲データファイルを読み込む( 曲データファイル名 );
166149 }
167-
168150 public void Dispose()
169151 {
170152 }
171-
172- /// <summary>
173- /// 指定された曲データファイルを読み込む。
153+
154+ /// <remarks>
174155 /// 失敗すれば何らかの例外を発出する。
175- /// </summary>
156+ /// </remarks>
176157 public void 曲データファイルを読み込む( string 曲データファイル名 )
177158 {
159+ var ファイルのSSTFバージョン = Version.CreateVersionFromFile( 曲データファイル名 );
160+
178161 // ファイルのSSTFバージョンによって処理分岐。
179- var version = Version.CreateVersionFromFile( 曲データファイル名 );
162+ if( SSTFVERSION.Major == ファイルのSSTFバージョン.Major )
163+ {
164+ this._v3曲データファイルを読み込む( 曲データファイル名 );
165+ }
166+ else
167+ {
168+ var v2score = new SSTFormat.v2.スコア( 曲データファイル名 );
169+ this._v2スコアからマイグレーションする( v2score );
170+ }
171+ }
172+
173+ public void 曲データファイルを読み込む_ヘッダだけ( string 曲データファイル名 )
174+ {
175+ var ファイルのSSTFバージョン = Version.CreateVersionFromFile( 曲データファイル名 );
180176
181- if( 2 == version.Major )
177+ // ファイルのSSTFバージョンによって処理分岐。
178+ if( SSTFVERSION.Major == ファイルのSSTFバージョン.Major )
179+ {
180+ this._v3曲データファイルを読み込む_ヘッダだけ( 曲データファイル名 );
181+ }
182+ else
182183 {
183- // (A) 同じバージョン。
184+ var v2score = new SSTFormat.v2.スコア();
185+ v2score.曲データファイルを読み込む_ヘッダだけ( 曲データファイル名 );
186+ this._v2スコアからマイグレーションする_ヘッダだけ( v2score );
187+ }
188+ }
189+ /// <summary>
190+ /// すでにスコアの構築が完了しているものとして、チップリストへの後処理(小節線・拍線の追加、発声時刻の計算など)のみ行う。
191+ /// </summary>
192+ public void 曲データファイルを読み込む_後処理だけ()
193+ {
194+ #region " 拍線の追加。小節線を先に追加すると小節が1つ増えるので、先に拍線から追加する。"
195+ //-----------------
196+ int 最大小節番号 = this.最大小節番号; // this.最大小節番号 プロパティはチップ数に依存して変化するので、for 文には組み込まないこと。
184197
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 ) )
198+ for( int i = 0; i <= 最大小節番号; i++ )
199+ {
200+ double 小節長倍率 = this.小節長倍率を取得する( i );
201+ for( int n = 1; n * 0.25 < 小節長倍率; n++ )
202202 {
203- int 行番号 = 0;
204- int 現在の小節番号 = 0;
205- int 現在の小節解像度 = 384;
206- チップ種別 e現在のチップ = チップ種別.Unknown;
203+ this.チップリスト.Add(
204+ new チップ() {
205+ 小節番号 = i,
206+ チップ種別 = チップ種別.拍線,
207+ 小節内位置 = (int) ( ( n * 0.25 ) * 100 ),
208+ 小節解像度 = (int) ( 小節長倍率 * 100 ),
209+ } );
210+ }
211+ }
212+ //-----------------
213+ #endregion
214+ #region " 小節線の追加。"
215+ //-----------------
216+ 最大小節番号 = this.最大小節番号;
207217
208- while( false == sr.EndOfStream )
209- {
210- // 1行ずつ読み込む。
211- 行番号++;
212- string 行 = this._行を読み込む( sr );
218+ for( int i = 0; i <= 最大小節番号 + 1; i++ )
219+ {
220+ this.チップリスト.Add(
221+ new チップ() {
222+ 小節番号 = i,
223+ チップ種別 = チップ種別.小節線,
224+ 小節内位置 = 0,
225+ 小節解像度 = 1,
226+ } );
227+ }
228+ //-----------------
229+ #endregion
230+ #region " 小節の先頭 の追加。"
231+ //----------------
232+ 最大小節番号 = this.最大小節番号;
213233
214- if( string.IsNullOrEmpty( 行 ) )
215- continue;
234+ // 「小節の先頭」チップは、小節線と同じく、全小節の先頭位置に置かれる。
235+ // 小節線には今後譜面作者によって位置をアレンジできる可能性を残したいが、
236+ // ビュアーが小節の先頭位置を検索するためには、小節の先頭に置かれるチップが必要になる。
237+ // よって、譜面作者の影響を受けない(ビュアー用の)チップを機械的に配置する。
216238
217- // ヘッダコマンド処理。
239+ for( int i = 0; i <= 最大小節番号; i++ )
240+ {
241+ this.チップリスト.Add(
242+ new チップ() {
243+ 小節番号 = i,
244+ チップ種別 = チップ種別.小節の先頭,
245+ 小節内位置 = 0,
246+ 小節解像度 = 1,
247+ } );
248+ }
249+ //----------------
250+ #endregion
218251
219- #region " ヘッダコマンドの処理を行う。"
220- //-----------------
221- if( 行.StartsWith( "Title", StringComparison.OrdinalIgnoreCase ) )
222- {
223- #region " Title コマンド "
224- //-----------------
225- string[] items = 行.Split( '=' );
252+ this.チップリスト.Sort();
226253
227- if( items.Length != 2 )
228- {
229- Trace.TraceError( $"Title の書式が不正です。スキップします。[{行番号}行目]" );
230- continue;
231- }
254+ #region " 全チップの発声/描画時刻と譜面内位置を計算する。"
255+ //-----------------
232256
233- this.Header.曲名 = items[ 1 ].Trim();
234- //-----------------
235- #endregion
257+ // 1. BPMチップを無視し(初期BPMで固定)、dic小節長倍率, Cチップ.小節解像度, Cチップ.小節内位置 から両者を計算する。
258+ // 以下、チップリストが小節番号順にソートされているという前提で。
236259
237- continue;
238- }
239- if( 行.StartsWith( "Description", StringComparison.OrdinalIgnoreCase ) )
240- {
241- #region " Description コマンド "
242- //-----------------
243- string[] items = 行.Split( '=' );
260+ double チップが存在する小節の先頭時刻ms = 0.0;
261+ int 現在の小節の番号 = 0;
244262
245- if( items.Length != 2 )
246- {
247- Trace.TraceError( $"Description の書式が不正です。スキップします。[{行番号}行目]" );
248- continue;
249- }
263+ foreach( チップ chip in this.チップリスト )
264+ {
265+ #region " チップの小節番号が現在の小節番号よりも大きい場合、チップが存在する小節に至るまで、「dbチップが存在する小節の先頭時刻ms」を更新する。"
266+ //-----------------
267+ while( 現在の小節の番号 < chip.小節番号 )
268+ {
269+ double 現在の小節の小節長倍率 = this.小節長倍率を取得する( 現在の小節の番号 );
270+ チップが存在する小節の先頭時刻ms += BPM初期値固定での1小節4拍の時間ms * 現在の小節の小節長倍率;
250271
251- // 2文字のリテラル "\n" は改行に復号。
252- this.Header.説明文 = items[ 1 ].Trim().Replace( @"\n", Environment.NewLine );
253- //-----------------
254- #endregion
272+ 現在の小節の番号++; // 現在の小節番号 が chip.小節番号 に追いつくまでループする。
273+ }
274+ //-----------------
275+ #endregion
276+ #region " チップの発声/描画時刻を求める。"
277+ //-----------------
278+ double チップが存在する小節の小節長倍率 = this.小節長倍率を取得する( 現在の小節の番号 );
255279
256- continue;
257- }
258- if( 行.StartsWith( "SoundDevice.Delay", StringComparison.OrdinalIgnoreCase ) )
259- {
260- #region " SoundDevice.Delay コマンド "
261- //-----------------
262- string[] items = 行.Split( '=' );
280+ chip.発声時刻ms =
281+ chip.描画時刻ms =
282+ (long) ( チップが存在する小節の先頭時刻ms + ( BPM初期値固定での1小節4拍の時間ms * チップが存在する小節の小節長倍率 * chip.小節内位置 ) / chip.小節解像度 );
283+ //-----------------
284+ #endregion
285+ }
263286
264- if( items.Length != 2 )
265- {
266- Trace.TraceError( $"SoundDevice.Delay の書式が不正です。スキップします。[{行番号}行目]" );
267- continue;
268- }
287+ // 2. BPMチップを考慮しながら調整する。(譜面内位置grid はBPMの影響を受けないので無視)
269288
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
289+ double 現在のBPM = スコア.初期BPM;
290+ int チップ数 = this.チップリスト.Count;
291+ for( int i = 0; i < チップ数; i++ )
292+ {
293+ // BPM チップ以外は無視。
294+ var BPMチップ = this.チップリスト[ i ];
295+ if( BPMチップ.チップ種別 != チップ種別.BPM )
296+ continue;
275297
276- continue;
277- }
278- //-----------------
279- #endregion
298+ // BPMチップより後続の全チップの n発声/描画時刻ms を、新旧BPMの比率(加速率)で修正する。
299+ double 加速率 = BPMチップ.BPM / 現在のBPM; // BPMチップ.dbBPM > 0.0 であることは読み込み時に保証済み。
300+ for( int j = i + 1; j < チップ数; j++ )
301+ {
302+ long 時刻ms = (long) ( BPMチップ.発声時刻ms + ( ( this.チップリスト[ j ].発声時刻ms - BPMチップ.発声時刻ms ) / 加速率 ) );
280303
281- // メモ(小節単位)処理。
304+ this.チップリスト[ j ].発声時刻ms = 時刻ms;
305+ this.チップリスト[ j ].描画時刻ms = 時刻ms;
306+ }
282307
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 );
308+ 現在のBPM = BPMチップ.BPM;
309+ }
310+ //-----------------
311+ #endregion
312+ }
326313
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
314+ /// <summary>
315+ /// 現在の スコア の内容をデータファイル(*.sstf)に書き出す。
316+ /// </summary>
317+ /// <remarks>
318+ /// 小節線、拍線、Unknown チップは出力しない。
319+ /// 失敗すれば何らかの例外を発出する。
320+ /// </remarks>
321+ public void 曲データファイルを書き出す( string 曲データファイル名, string ヘッダ行 )
322+ {
323+ using( var sw = new StreamWriter( 曲データファイル名, false, Encoding.UTF8 ) )
324+ {
325+ // SSTFバージョンの出力
326+ sw.WriteLine( $"# SSTFVersion {this.SSTFVERSION.ToString()}" );
347327
328+ // ヘッダ行の出力
329+ sw.WriteLine( $"{ヘッダ行}" ); // strヘッダ行に"{...}"が入ってても大丈夫なようにstring.Format()で囲む。
330+ sw.WriteLine( "" );
331+
332+ // ヘッダコマンド行の出力
333+ sw.WriteLine( "Title=" + ( ( string.IsNullOrEmpty( this.曲名 ) ) ? "(no title)" : this.曲名 ) );
334+ if( !string.IsNullOrEmpty( this.説明文 ) )
335+ {
336+ // 改行コードは、2文字のリテラル "\n" に置換。
337+ sw.WriteLine( "Description=" + this.説明文.Replace( Environment.NewLine, @"\n" ) );
338+ }
339+ sw.WriteLine( "SoundDevice.Delay={0}", this.サウンドデバイス遅延ms );
340+ sw.WriteLine( "" );
341+
342+ // 全チップの出力
343+
344+ #region " 全チップの最終小節番号を取得する。"
345+ //-----------------
346+ int 最終小節番号 = 0;
347+ foreach( var cc in this.チップリスト )
348+ {
349+ if( cc.小節番号 > 最終小節番号 )
350+ 最終小節番号 = cc.小節番号;
351+ }
352+ //-----------------
353+ #endregion
354+
355+ for( int 小節番号 = 0; 小節番号 <= 最終小節番号; 小節番号++ )
356+ {
357+ #region " dicレーン別チップリストの初期化。"
358+ //-----------------
359+ var dicレーン別チップリスト = new Dictionary<レーン種別, List<チップ>>();
360+
361+ foreach( レーン種別 laneType in Enum.GetValues( typeof( レーン種別 ) ) )
362+ dicレーン別チップリスト[ laneType ] = new List<チップ>();
363+ //-----------------
364+ #endregion
365+ #region " dicレーン別チップリストの構築; 小節番号 の小節に存在するチップのみをレーン別に振り分けて格納する。"
366+ //-----------------
367+ foreach( var cc in this.チップリスト )
368+ {
369+ #region " 出力しないチップ種別は無視。"
370+ //----------------
371+ if( cc.チップ種別 == チップ種別.小節線 ||
372+ cc.チップ種別 == チップ種別.拍線 ||
373+ cc.チップ種別 == チップ種別.小節メモ ||
374+ cc.チップ種別 == チップ種別.小節の先頭 ||
375+ cc.チップ種別 == チップ種別.Unknown )
376+ {
348377 continue;
349378 }
350- //-----------------
379+ //----------------
351380 #endregion
352381
353- // 上記行頭コマンド以外は、チップ記述行だと見なす。
382+ if( cc.小節番号 > 小節番号 )
383+ {
384+ // チップリストは昇順に並んでいるので、これ以上検索しても無駄。
385+ break;
386+ }
387+ else if( cc.小節番号 == 小節番号 )
388+ {
389+ var lane = レーン種別.Bass; // 対応するレーンがなかったら Bass でも返しておく。
354390
355- #region " チップ記述コマンドの処理を行う。"
356- //-----------------
391+ foreach( var kvp in dicSSTFレーンチップ対応表 )
392+ {
393+ if( kvp.Value.Contains( cc.チップ種別 ) )
394+ {
395+ lane = kvp.Key;
396+ break;
397+ }
398+ }
357399
358- // 行を区切り文字でトークンに分割。
359- string[] tokens = 行.Split( new char[] { ';', ':' } );
400+ dicレーン別チップリスト[ lane ].Add( cc );
401+ }
402+ }
403+ //-----------------
404+ #endregion
405+
406+ #region " Part行 出力。"
407+ //-----------------
408+ sw.Write( $"Part = {小節番号.ToString()}" );
409+
410+ if( this.小節長倍率リスト[ 小節番号 ] != 1.0 )
411+ sw.Write( $"s{this.小節長倍率リスト[ 小節番号 ].ToString()}" );
360412
361- // すべてのトークンについて……
362- foreach( string token in tokens )
413+ sw.WriteLine( ";" );
414+ //-----------------
415+ #endregion
416+ #region " Lane, Resolution, Chip 行 出力。"
417+ //-----------------
418+ foreach( レーン種別 laneType in Enum.GetValues( typeof( レーン種別 ) ) )
419+ {
420+ if( 0 < dicレーン別チップリスト[ laneType ].Count )
363421 {
364- // トークンを分割。
422+ sw.Write( $"Lane={laneType.ToString()}; " );
365423
366- #region " トークンを区切り文字 '=' で strコマンド と strパラメータ に分割し、それぞれの先頭末尾の空白を削除する。"
424+ #region " 新しい解像度を求める。"
367425 //-----------------
368- string[] items = token.Split( '=' );
369-
370- if( 2 != items.Length )
426+ int 新しい解像度 = 1;
427+ foreach( var cc in dicレーン別チップリスト[ laneType ] )
428+ 新しい解像度 = this._最小公倍数を返す( 新しい解像度, cc.小節解像度 );
429+ //-----------------
430+ #endregion
431+ #region " dicレーン別チップリスト[ lane ] 要素の 小節解像度 と 小節内位置 を 新しい解像度 に合わせて修正する。 "
432+ //-----------------
433+ foreach( var cc in dicレーン別チップリスト[ laneType ] )
371434 {
372- if( 0 == token.Trim().Length ) // 空文字列(行末など)は不正じゃない。
373- continue;
435+ int 倍率 = 新しい解像度 / cc.小節解像度; // 新しい解像度 は 小節解像度 の最小公倍数なので常に割り切れる。
374436
375- Trace.TraceError( $"コマンドとパラメータの記述書式が不正です。このコマンドをスキップします。[{行番号}行目]" );
376- continue;
437+ cc.小節解像度 *= 倍率;
438+ cc.小節内位置 *= 倍率;
377439 }
378-
379- string コマンド = items[ 0 ].Trim();
380- string パラメータ = items[ 1 ].Trim();
381440 //-----------------
382441 #endregion
383442
384- // コマンド別に処理。
443+ sw.Write( $"Resolution = {新しい解像度}; " );
444+ sw.Write( "Chips = " );
385445
386- if( コマンド.Equals( "Part", StringComparison.OrdinalIgnoreCase ) )
446+ for( int i = 0; i < dicレーン別チップリスト[ laneType ].Count; i++ )
387447 {
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();
448+ チップ cc = dicレーン別チップリスト[ laneType ][ i ];
436449
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- }
450+ // 位置を出力。
451+ sw.Write( cc.小節内位置.ToString() );
447452
448- // 小節長倍率辞書に追加 or 上書き更新。
449- this.小節長倍率を設定する( 現在の小節番号, 小節長倍率 );
450- //-----------------
451- #endregion
453+ // 属性を出力(あれば)。
452454
453- continue;
454- }
455- }
455+ #region " (1) 共通属性 "
456456 //-----------------
457- #endregion
458-
457+ if( cc.音量 < チップ.最大音量 )
458+ sw.Write( $"v{cc.音量.ToString()}" );
459459 //-----------------
460460 #endregion
461-
462- continue;
463- }
464- if( コマンド.Equals( "Lane", StringComparison.OrdinalIgnoreCase ) )
465- {
466- #region " Lane(レーン指定)コマンド(チップ種別の仮決め)"
461+ #region " (2) 専用属性 "
467462 //-----------------
468- if( パラメータ.Equals( "LeftCrash", StringComparison.OrdinalIgnoreCase ) )
469- e現在のチップ = チップ種別.LeftCrash;
470-
471- else if( パラメータ.Equals( "Ride", StringComparison.OrdinalIgnoreCase ) )
472- e現在のチップ = チップ種別.Ride;
463+ switch( cc.チップ種別 )
464+ {
465+ case チップ種別.Ride_Cup:
466+ sw.Write( 'c' );
467+ break;
473468
474- else if( パラメータ.Equals( "China", StringComparison.OrdinalIgnoreCase ) )
475- e現在のチップ = チップ種別.China;
469+ case チップ種別.HiHat_Open:
470+ sw.Write( 'o' );
471+ break;
476472
477- else if( パラメータ.Equals( "Splash", StringComparison.OrdinalIgnoreCase ) )
478- e現在のチップ = チップ種別.Splash;
473+ case チップ種別.HiHat_HalfOpen:
474+ sw.Write( 'h' );
475+ break;
479476
480- else if( パラメータ.Equals( "HiHat", StringComparison.OrdinalIgnoreCase ) )
481- e現在のチップ = チップ種別.HiHat_Close;
477+ case チップ種別.HiHat_Foot:
478+ sw.Write( 'f' );
479+ break;
482480
483- else if( パラメータ.Equals( "Snare", StringComparison.OrdinalIgnoreCase ) )
484- e現在のチップ = チップ種別.Snare;
481+ case チップ種別.Snare_OpenRim:
482+ sw.Write( 'o' );
483+ break;
485484
486- else if( パラメータ.Equals( "Bass", StringComparison.OrdinalIgnoreCase ) )
487- e現在のチップ = チップ種別.Bass;
485+ case チップ種別.Snare_ClosedRim:
486+ sw.Write( 'c' );
487+ break;
488488
489- else if( パラメータ.Equals( "Tom1", StringComparison.OrdinalIgnoreCase ) )
490- e現在のチップ = チップ種別.Tom1;
489+ case チップ種別.Snare_Ghost:
490+ sw.Write( 'g' );
491+ break;
491492
492- else if( パラメータ.Equals( "Tom2", StringComparison.OrdinalIgnoreCase ) )
493- e現在のチップ = チップ種別.Tom2;
493+ case チップ種別.Tom1_Rim:
494+ sw.Write( 'r' );
495+ break;
494496
495- else if( パラメータ.Equals( "Tom3", StringComparison.OrdinalIgnoreCase ) )
496- e現在のチップ = チップ種別.Tom3;
497+ case チップ種別.Tom2_Rim:
498+ sw.Write( 'r' );
499+ break;
497500
498- else if( パラメータ.Equals( "RightCrash", StringComparison.OrdinalIgnoreCase ) )
499- e現在のチップ = チップ種別.RightCrash;
501+ case チップ種別.Tom3_Rim:
502+ sw.Write( 'r' );
503+ break;
500504
501- else if( パラメータ.Equals( "BPM", StringComparison.OrdinalIgnoreCase ) )
502- e現在のチップ = チップ種別.BPM;
505+ case チップ種別.LeftCymbal_Mute:
506+ sw.Write( 'm' );
507+ break;
503508
504- else if( パラメータ.Equals( "Song", StringComparison.OrdinalIgnoreCase ) )
505- e現在のチップ = チップ種別.背景動画;
506- else
507- Trace.TraceError( $"Lane(レーン指定)コマンドのパラメータ記述 '{パラメータ}' が不正です。このコマンドをスキップします。[{行番号}行目]" );
508- //-----------------
509- #endregion
509+ case チップ種別.RightCymbal_Mute:
510+ sw.Write( 'm' );
511+ break;
510512
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;
513+ case チップ種別.BPM:
514+ sw.Write( $"b{cc.BPM.ToString()}" );
515+ break;
526516 }
527- 現在の小節解像度 = 解像度;
528517 //-----------------
529518 #endregion
530519
531- continue;
520+ // 区切り文字 または 終端文字 を出力
521+ sw.Write( ( i == dicレーン別チップリスト[ laneType ].Count - 1 ) ? ";" : "," );
532522 }
533- if( コマンド.Equals( "Chips", StringComparison.OrdinalIgnoreCase ) )
534- {
535- #region " Chips(チップ指定)コマンド "
536- //-----------------
537-
538- // パラメータを区切り文字 ',' でチップトークンに分割。
539- string[] chipTokens = パラメータ.Split( ',' );
540523
541- // すべてのチップトークンについて……
542- for( int i = 0; i < chipTokens.Length; i++ )
543- {
544- chipTokens[ i ].Trim();
524+ sw.WriteLine( "" ); // 改行
525+ }
526+ }
527+ //-----------------
528+ #endregion
545529
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
530+ sw.WriteLine( "" ); // 次の Part 前に1行あける。
531+ }
565532
566- #region " チップ位置を取得する。"
567- //-----------------
568- string 位置番号文字列 = this._指定された文字列の先頭から数字文字列を取り出す( ref chipTokens[ i ] );
569- chipTokens[ i ].Trim();
533+ // メモ(小節単位)の出力
570534
571- // 文法チェック。
572- if( string.IsNullOrEmpty( 位置番号文字列 ) )
573- {
574- Trace.TraceError( $"チップの位置指定の記述がありません。このチップをスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
575- continue;
576- }
535+ #region " dicメモ を小節番号で昇順に出力する。"
536+ //-----------------
537+ var dic昇順メモ = new Dictionary<int, string>();
538+ int 最大小節番号 = this.最大小節番号;
577539
578- // 位置を取得。
579- if( false == int.TryParse( 位置番号文字列, out int チップ位置 ) )
580- {
581- Trace.TraceError( $"チップの位置指定の記述が不正です。このチップをスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
582- continue;
583- }
540+ for( int i = 0; i <= 最大小節番号; i++ )
541+ {
542+ if( this.dicメモ.ContainsKey( i ) )
543+ dic昇順メモ.Add( i, this.dicメモ[ i ] );
544+ }
584545
585- // 値域チェック。
586- if( ( 0 > チップ位置 ) || ( チップ位置 >= 現在の小節解像度 ) )
587- {
588- Trace.TraceError( $"チップの位置が負数であるか解像度(Resolution)以上の値になっています。このチップをスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
589- continue;
590- }
546+ foreach( var kvp in dic昇順メモ )
547+ {
548+ int 小節番号 = kvp.Key;
591549
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 ] );
550+ // 改行コードは、2文字のリテラル "\n" に置換。
551+ string メモ = kvp.Value.Replace( Environment.NewLine, @"\n" );
601552
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();
553+ sw.WriteLine( $"PartMemo={小節番号},{メモ}" );
554+ }
610555
611- // 文法チェック。
612- if( string.IsNullOrEmpty( 音量文字列 ) )
613- {
614- Trace.TraceError( $"チップの音量指定の記述がありません。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
615- continue;
616- }
556+ sw.WriteLine( "" );
557+ //-----------------
558+ #endregion
617559
618- // チップ音量の取得。
619- if( false == int.TryParse( 音量文字列, out int チップ音量 ) )
620- {
621- Trace.TraceError( $"チップの音量指定の記述が不正です。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
622- continue;
623- }
560+ sw.Close();
561+ }
562+ }
624563
625- // 値域チェック。
626- if( ( 1 > チップ音量 ) || ( チップ音量 > チップ.最大音量 ) )
627- {
628- Trace.TraceError( $"チップの音量が適正範囲(1~{チップ.最大音量})を超えています。このチップをスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
629- continue;
630- }
564+ /// <summary>
565+ /// 指定された Config.Speed を考慮し、指定された時間[ms]の間に流れるピクセル数[dpx]を算出して返す。</para>
566+ /// </summary>
567+ [Obsolete( "指定時間がミリ秒単位ではなく秒単位であるメソッドを使用してください。" )]
568+ public int 指定された時間msに対応する符号付きピクセル数を返す( double speed, long 指定時間ms )
569+ {
570+ return (int) ( 指定時間ms * スコア.基準譜面速度dpxms * speed );
571+ }
572+ /// <summary>
573+ /// 指定された Config.Speed を考慮し、指定された時間[秒]の間に流れるピクセル数[dpx]を算出して返す。
574+ /// </summary>
575+ public double 指定された時間secに対応する符号付きピクセル数を返す( double speed, double 指定時間sec )
576+ {
577+ return ( 指定時間sec * スコア.基準譜面速度dpxsec * speed );
578+ }
631579
632- chip.音量 = チップ音量;
633- //-----------------
634- #endregion
580+ public double 小節長倍率を取得する( int 小節番号 )
581+ {
582+ // 小節長倍率リスト が短ければ増設する。
583+ if( 小節番号 >= this.小節長倍率リスト.Count )
584+ {
585+ int 不足数 = 小節番号 - this.小節長倍率リスト.Count + 1;
586+ for( int i = 0; i < 不足数; i++ )
587+ this.小節長倍率リスト.Add( 1.0 );
588+ }
635589
636- continue;
637- }
590+ // 小節番号に対応する倍率を返す。
591+ return this.小節長倍率リスト[ 小節番号 ];
592+ }
593+ public void 小節長倍率を設定する( int 小節番号, double 倍率 )
594+ {
595+ // 小節長倍率リスト が短ければ増設する。
596+ if( 小節番号 >= this.小節長倍率リスト.Count )
597+ {
598+ int 不足数 = 小節番号 - this.小節長倍率リスト.Count + 1;
599+ for( int i = 0; i < 不足数; i++ )
600+ this.小節長倍率リスト.Add( 1.0 );
601+ }
638602
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;
603+ // 小節番号に対応付けて倍率を登録する。
604+ this.小節長倍率リスト[ 小節番号 ] = 倍率;
605+ }
665606
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;
607+ private void _初期化する()
608+ {
609+ this.SSTFバージョン = SSTFVERSION;
610+ this.曲名 = "(no title)";
611+ this.説明文 = "";
612+ this.サウンドデバイス遅延ms = 0f;
613+ this.難易度 = 0f;
614+ this.背景動画ファイル名 = "";
615+ this.小節長倍率リスト = new List<double>();
616+ this.dicメモ = new Dictionary<int, string>();
617+ }
692618
693- //-----------------
694- #endregion
695- #region " case China "
696- //-----------------
697- case チップ種別.China:
619+ /// <summary>
620+ /// 指定された曲データファイルを、現行バージョンのSSTFormatとして読み込む。
621+ /// </summary>
622+ private void _v3曲データファイルを読み込む( string 曲データファイル名 )
623+ {
624+ this._初期化する();
698625
699- #region " 未知の属性 "
700- //-----------------
701- chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
702- Trace.TraceError( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
703- //-----------------
704- #endregion
626+ #region " 背景動画ファイル名を更新する。"
627+ //----------------
628+ this.背景動画ファイル名 =
629+ ( from file in Directory.GetFiles( Path.GetDirectoryName( 曲データファイル名 ) )
630+ where スコア.背景動画のデフォルト拡張子s.Any( 拡張子名 => ( Path.GetExtension( file ).ToLower() == 拡張子名 ) )
631+ select file ).FirstOrDefault();
632+ //----------------
633+ #endregion
705634
706- continue;
635+ using( var sr = new StreamReader( 曲データファイル名, Encoding.UTF8 ) )
636+ {
637+ var 現在の = new 解析用作業変数();
707638
708- //-----------------
709- #endregion
710- #region " case Splash "
711- //-----------------
712- case チップ種別.Splash:
639+ // 1行ずつ読み込む。
640+ while( false == sr.EndOfStream )
641+ {
642+ string 行 = this._行を読み込む( sr );
643+ 現在の.行番号++;
713644
714- #region " 未知の属性 "
715- //-----------------
716- chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
717- Trace.TraceError( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
718- //-----------------
719- #endregion
645+ if( string.IsNullOrEmpty( 行 ) )
646+ continue;
720647
721- continue;
648+ if( this._行をヘッダ行と想定して解析する( 行, 現在の ) )
649+ continue;
722650
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;
651+ if( this._行を小節メモ行として解析する( 行, 現在の ) )
652+ continue;
778653
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;
654+ if( this._行をチップ記述行として解析する( 行, 現在の ) )
655+ continue;
656+ }
657+ }
658+ }
825659
826- //-----------------
827- #endregion
828- #region " case Bass "
829- //-----------------
830- case チップ種別.Bass:
660+ /// <summary>
661+ /// 指定されたスコアから、現行バージョンに変換する。
662+ /// </summary>
663+ /// <param name="v2score">読み込み済みのスコア</param>
664+ private void _v2スコアからマイグレーションする( SSTFormat.v2.スコア v2score )
665+ {
666+ this._v2スコアからマイグレーションする_ヘッダだけ( v2score );
831667
832- #region " 未知の属性 "
833- //-----------------
834- chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
835- Trace.TraceError( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
836- //-----------------
837- #endregion
668+ this.チップリスト = new List<チップ>();
669+ foreach( var v2chip in v2score.チップリスト )
670+ this.チップリスト.Add( new チップ( v2chip ) );
838671
839- continue;
672+ this.小節長倍率リスト = new List<double>();
673+ foreach( var v2scale in v2score.小節長倍率リスト )
674+ this.小節長倍率リスト.Add( v2scale );
840675
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;
676+ this.dicメモ = new Dictionary<int, string>();
677+ foreach( var kvp in v2score.dicメモ )
678+ this.dicメモ.Add( kvp.Key, kvp.Value );
679+ }
867680
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;
681+ private void _v3曲データファイルを読み込む_ヘッダだけ( string 曲データファイル名 )
682+ {
683+ this._初期化する();
894684
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;
685+ #region " 背景動画ファイル名を更新する。"
686+ //----------------
687+ this.背景動画ファイル名 =
688+ ( from file in Directory.GetFiles( Path.GetDirectoryName( 曲データファイル名 ) )
689+ where スコア.背景動画のデフォルト拡張子s.Any( 拡張子名 => ( Path.GetExtension( file ).ToLower() == 拡張子名 ) )
690+ select file ).FirstOrDefault();
691+ //----------------
692+ #endregion
921693
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;
694+ using( var sr = new StreamReader( 曲データファイル名, Encoding.UTF8 ) )
695+ {
696+ var 現在の = new 解析用作業変数();
947697
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;
698+ // 1行ずつ読み込む。
699+ while( false == sr.EndOfStream )
700+ {
701+ string 行 = this._行を読み込む( sr );
702+ 現在の.行番号++;
991703
992- //-----------------
993- #endregion
994- #region " case Song "
995- //-----------------
996- case チップ種別.背景動画:
704+ if( string.IsNullOrEmpty( 行 ) )
705+ continue;
997706
998- #region " 未知の属性 "
999- //-----------------
1000- chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1001- Trace.TraceError( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
1002- //-----------------
1003- #endregion
707+ if( this._行をヘッダ行と想定して解析する( 行, 現在の ) )
708+ continue;
709+ }
710+ }
711+ }
1004712
1005- continue;
713+ private void _v2スコアからマイグレーションする_ヘッダだけ( SSTFormat.v2.スコア v2score )
714+ {
715+ this._初期化する();
716+
717+ this.SSTFバージョン = SSTFVERSION;
718+ this.曲名 = v2score.Header.曲名;
719+ this.説明文 = v2score.Header.説明文;
720+ this.サウンドデバイス遅延ms = v2score.Header.サウンドデバイス遅延ms;
721+ this.難易度 = 0.0f; // 新規
722+ this.背景動画ファイル名 = v2score.背景動画ファイル名;
723+ }
1006724
1007- //-----------------
1008- #endregion
1009- }
725+ private class 解析用作業変数
726+ {
727+ public int 行番号 = 0;
728+ public int 小節番号 = 0;
729+ public int 小節解像度 = 384;
730+ public チップ種別 チップ種別 = チップ種別.Unknown;
731+ }
1010732
1011- #region " 未知の属性 "
1012- //-----------------
1013- chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1014- Trace.TraceError( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{行番号}行目; {i + 1}個目のチップ]" );
1015- //-----------------
1016- #endregion
1017- }
1018- //-----------------
1019- #endregion
733+ /// <returns>
734+ /// 行をヘッダとして処理したなら true 、該当しないまたはエラーが発生したときは false を返す。
735+ /// </returns>
736+ private bool _行をヘッダ行と想定して解析する( string 行, 解析用作業変数 現在の )
737+ {
738+ 行 = 行.ToLower();
1020739
1021- this.チップリスト.Add( chip );
1022- }
1023- //-----------------
1024- #endregion
740+ if( 行.StartsWith( "title" ) )
741+ {
742+ #region " Title コマンド "
743+ //-----------------
744+ string[] items = 行.Split( '=' );
1025745
1026- continue;
1027- }
746+ if( 2 != items.Length )
747+ {
748+ Trace.TraceError( $"Title の書式が不正です。スキップします。[{現在の.行番号}行目]" );
749+ return false;
750+ }
1028751
1029- Trace.TraceError( $"不正なコマンド「{コマンド}」が存在します。[{行番号}行目]" );
1030- }
1031- //-----------------
1032- #endregion
1033- }
752+ this.曲名 = items[ 1 ].Trim();
753+
754+ return true;
755+ //-----------------
756+ #endregion
757+ }
758+ if( 行.StartsWith( "description" ) )
759+ {
760+ #region " Description コマンド "
761+ //-----------------
762+ string[] items = 行.Split( '=' );
763+
764+ if( items.Length != 2 )
765+ {
766+ Trace.TraceError( $"Description の書式が不正です。スキップします。[{現在の.行番号}行目]" );
767+ return false;
768+ }
769+
770+ // 2文字のリテラル "\n" は改行に復号。
771+ this.説明文 = items[ 1 ].Trim().Replace( @"\n", Environment.NewLine );
772+
773+ return true;
774+ //-----------------
775+ #endregion
776+ }
777+ if( 行.StartsWith( "sounddevice.delay" ) )
778+ {
779+ #region " SoundDevice.Delay コマンド "
780+ //-----------------
781+ string[] items = 行.Split( '=' );
1034782
1035- sr.Close();
783+ if( 2 != items.Length )
784+ {
785+ Trace.TraceError( $"SoundDevice.Delay の書式が不正です。スキップします。[{現在の.行番号}行目]" );
786+ return false;
1036787 }
788+
789+ // 2文字のリテラル "\n" は改行に復号。
790+ if( float.TryParse( items[ 1 ].Trim().Replace( @"\n", Environment.NewLine ), out float value ) )
791+ this.サウンドデバイス遅延ms = value;
792+
793+ return true;
1037794 //-----------------
1038795 #endregion
796+ }
797+
798+ return false;
799+ }
800+ /// <returns>
801+ /// 行を小節メモ行として処理したなら true 、該当しないまたはエラーが発生したときは false を返す。
802+ /// </returns>
803+ private bool _行を小節メモ行として解析する( string 行, 解析用作業変数 現在の )
804+ {
805+ if( !( 行.ToLower().StartsWith( "partmemo" ) ) )
806+ return false;
1039807
1040- this.曲データファイルを読み込む_後処理だけ();
808+ #region " '=' 以前を除去する。"
809+ //-----------------
810+ int 等号位置 = 行.IndexOf( '=' );
811+
812+ if( 0 >= 等号位置 )
813+ {
814+ Trace.TraceError( $"PartMemo の書式が不正です。スキップします。[{現在の.行番号}]行目]" );
815+ return false;
1041816 }
1042- else if( 2 > version.Major )
817+ 行 = 行.Substring( 等号位置 + 1 ).Trim();
818+ if( string.IsNullOrEmpty( 行 ) )
1043819 {
1044- // (B) 下位バージョン。
1045- var v1score = new SSTFormat.v1.スコア( 曲データファイル名 );
1046- this._SSTFv1スコアで初期化する( v1score );
820+ Trace.TraceError( $"PartMemo の書式が不正です。スキップします。[{現在の.行番号}]行目]" );
821+ return false;
1047822 }
1048- else
823+ //-----------------
824+ #endregion
825+ #region " カンマ位置を取得する。"
826+ //-----------------
827+ int カンマ位置 = 行.IndexOf( ',' );
828+
829+ if( 0 >= カンマ位置 )
1049830 {
1050- // (C) 上位バージョン。
1051- throw new ArgumentException( $"この曲データファイルのSSTFバージョン({version})には未対応です。" );
831+ Trace.TraceError( $"PartMemo の書式が不正です。スキップします。[{現在の.行番号}]行目]" );
832+ return false;
1052833 }
1053- }
834+ //-----------------
835+ #endregion
836+ #region " 小節番号を取得する。"
837+ //-----------------
838+ string 小説番号文字列 = 行.Substring( 0, カンマ位置 );
1054839
1055- public void 曲データファイルを読み込む_ヘッダだけ( string 曲データファイル名 )
1056- {
1057- // ファイルのSSTFバージョンによって処理分岐。
1058- var version = Version.CreateVersionFromFile( 曲データファイル名 );
840+ if( !( int.TryParse( 小説番号文字列, out int 小節番号 ) || ( 0 > 小節番号 ) ) )
841+ {
842+ Trace.TraceError( $"PartMemo の小節番号が不正です。スキップします。[{現在の.行番号}]行目]" );
843+ return false;
844+ }
845+ //-----------------
846+ #endregion
847+ #region " メモを取得する。"
848+ //-----------------
849+ string メモ = 行.Substring( カンマ位置 + 1 );
1059850
1060- if( 2 == version.Major )
851+ // 2文字のリテラル文字列 "\n" は改行に復号。
852+ メモ = メモ.Replace( @"\n", Environment.NewLine );
853+ //-----------------
854+ #endregion
855+ #region " メモが空文字列でないなら dicメモ に登録すると同時に、チップとしても追加する。"
856+ //-----------------
857+ if( !( string.IsNullOrEmpty( メモ ) ) )
1061858 {
1062- // (A) 同じバージョン。
859+ this.dicメモ.Add( 小節番号, メモ );
1063860
1064- this.小節長倍率リスト = new List<double>();
861+ this.チップリスト.Add(
862+ new チップ() {
863+ チップ種別 = チップ種別.小節メモ,
864+ 小節番号 = 小節番号,
865+ 小節内位置 = 0,
866+ 小節解像度 = 1,
867+ } );
868+ }
869+ //-----------------
870+ #endregion
1065871
1066- // 曲データファイルのヘッダ部を読み込む。
1067- using( var sr = new StreamReader( 曲データファイル名, Encoding.UTF8 ) )
1068- {
1069- int 行番号 = 0;
872+ return true;
873+ }
874+ /// <returns>
875+ /// 常に true を返す。
876+ /// </returns>
877+ private bool _行をチップ記述行として解析する( string 行, 解析用作業変数 現在の )
878+ {
879+ // 行を区切り文字でトークンに分割。
880+ string[] tokens = 行.Split( new char[] { ';', ':' } );
1070881
1071- while( false == sr.EndOfStream )
1072- {
1073- // 1行ずつ読み込む。
882+ // すべてのトークンについて……
883+ foreach( string token in tokens )
884+ {
885+ #region " トークンを区切り文字 '=' で コマンド と パラメータ に分割し、それぞれの先頭末尾の空白を削除する。"
886+ //-----------------
887+ string[] items = token.Split( '=' );
1074888
1075- 行番号++;
1076- string 行 = this._行を読み込む( sr );
889+ if( 2 != items.Length )
890+ {
891+ if( 0 == token.Trim().Length ) // 空文字列(行末など)は不正じゃない。
892+ continue;
1077893
1078- if( string.IsNullOrEmpty( 行 ) )
1079- continue;
894+ Trace.TraceError( $"コマンドとパラメータの記述書式が不正です。このコマンドをスキップします。[{現在の.行番号}行目]" );
895+ continue;
896+ }
1080897
1081- // ヘッダコマンド処理。
898+ string コマンド = items[ 0 ].Trim();
899+ string パラメータ = items[ 1 ].Trim();
900+ //-----------------
901+ #endregion
1082902
1083- #region " ヘッダコマンドの処理を行う。"
903+ switch( コマンド.ToLower() )
904+ {
905+ case "part":
906+ #region " Part(小節番号指定)コマンド "
1084907 //-----------------
1085- if( 1 == 行番号 && // 先頭行に限る。
1086- 行.StartsWith( "SSTFVersion", StringComparison.OrdinalIgnoreCase ) )
1087908 {
1088- #region " SSTFバージョン "
909+ #region " 小節番号を取得・設定。"
1089910 //----------------
1090- string[] items = 行.Split( ' ' ); // SPACE 1文字 で統一するので注意
1091-
1092- if( 2 != items.Length )
911+ string 小節番号文字列 = this._指定された文字列の先頭から数字文字列を取り出す( ref パラメータ );
912+ if( string.IsNullOrEmpty( 小節番号文字列 ) )
1093913 {
1094- Trace.TraceError( $"SSTFVersion の書式が不正です。スキップします(バージョンは1.0.0.0と見なされます)。[{行番号}行目]" );
914+ Trace.TraceError( $"Part(小節番号)コマンドに小節番号の記述がありません。このコマンドをスキップします。[{現在の.行番号}行目]" );
1095915 continue;
1096916 }
1097- try
917+ if( !( int.TryParse( 小節番号文字列, out int 小節番号 ) ) )
1098918 {
1099- this.Header.SSTFバージョン = new Version( items[ 1 ].Trim() ); // string から Version へ変換できる書式であること。(例: "1.2.3.4")
919+ Trace.TraceError( $"Part(小節番号)コマンドの小節番号が不正です。このコマンドをスキップします。[{現在の.行番号}行目]" );
920+ continue;
1100921 }
1101- catch
922+ if( 0 > 小節番号 )
1102923 {
1103- Trace.TraceError( $"SSTFVersion のバージョン書式が不正です。スキップします(バージョンは1.0.0.0と見なされます)。[{行番号}行目]" );
924+ Trace.TraceError( $"Part(小節番号)コマンドの小節番号が負数です。このコマンドをスキップします。[{現在の.行番号}行目]" );
1104925 continue;
1105926 }
927+ 現在の.小節番号 = 小節番号;
1106928 //----------------
1107929 #endregion
1108930
1109- continue;
1110- }
1111- if( 行.StartsWith( "Title", StringComparison.OrdinalIgnoreCase ) )
1112- {
1113- #region " Title コマンド "
1114- //-----------------
1115- string[] items = 行.Split( '=' );
931+ // Part の属性があれば取得する。
1116932
1117- if( 2 != items.Length )
933+ while( 0 < パラメータ.Length )
1118934 {
1119- Trace.TraceError( $"Title の書式が不正です。スキップします。[{行番号}行目]" );
1120- continue;
1121- }
935+ char 属性ID = char.ToLower( パラメータ[ 0 ] );
1122936
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( '=' );
937+ if( 属性ID == 's' )
938+ {
939+ #region " 小節長倍率(>0) → list小節長倍率 "
940+ //-----------------
941+ パラメータ = パラメータ.Substring( 1 ).Trim();
1134942
1135- if( 2 != items.Length )
1136- {
1137- Trace.TraceError( $"Description の書式が不正です。スキップします。[{行番号}行目]" );
1138- continue;
1139- }
943+ string 小節長倍率文字列 = this._指定された文字列の先頭から数字文字列を取り出す( ref パラメータ );
944+ if( string.IsNullOrEmpty( 小節長倍率文字列 ) )
945+ {
946+ Trace.TraceError( $"Part(小節番号)コマンドに小節長倍率の記述がありません。この属性をスキップします。[{現在の.行番号}行目]" );
947+ continue;
948+ }
949+ パラメータ = パラメータ.Trim();
1140950
1141- // 2文字のリテラル "\n" は改行に復号。
1142- this.Header.説明文 = items[ 1 ].Trim().Replace( @"\n", Environment.NewLine );
1143- //-----------------
1144- #endregion
951+ if( !( double.TryParse( 小節長倍率文字列, out double 小節長倍率 ) ) )
952+ {
953+ Trace.TraceError( $"Part(小節番号)コマンドの小節長倍率が不正です。この属性をスキップします。[{現在の.行番号}行目]" );
954+ continue;
955+ }
956+ if( 0.0 >= 小節長倍率 )
957+ {
958+ Trace.TraceError( $"Part(小節番号)コマンドの小節長倍率が 0.0 または負数です。この属性をスキップします。[{現在の.行番号}行目]" );
959+ continue;
960+ }
961+ // 小節長倍率辞書に追加 or 上書き更新。
962+ this.小節長倍率を設定する( 現在の.小節番号, 小節長倍率 );
1145963
1146- continue;
964+ continue;
965+ //-----------------
966+ #endregion
967+ }
968+ }
1147969 }
1148970 //-----------------
1149971 #endregion
972+ break;
1150973
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 )
974+ case "lane":
975+ #region " Lane(レーン指定)コマンド(チップ種別の仮決め)"
976+ //-----------------
1356977 {
1357- continue;
978+ switch( パラメータ.ToLower() )
979+ {
980+ case "leftcrash": 現在の.チップ種別 = チップ種別.LeftCrash; break;
981+ case "ride": 現在の.チップ種別 = チップ種別.Ride; break;
982+ case "china": 現在の.チップ種別 = チップ種別.China; break;
983+ case "splash": 現在の.チップ種別 = チップ種別.Splash; break;
984+ case "hihat": 現在の.チップ種別 = チップ種別.HiHat_Close; break;
985+ case "snare": 現在の.チップ種別 = チップ種別.Snare; break;
986+ case "bass": 現在の.チップ種別 = チップ種別.Bass; break;
987+ case "tom1": 現在の.チップ種別 = チップ種別.Tom1; break;
988+ case "tom2": 現在の.チップ種別 = チップ種別.Tom2; break;
989+ case "tom3": 現在の.チップ種別 = チップ種別.Tom3; break;
990+ case "rightcrash": 現在の.チップ種別 = チップ種別.RightCrash; break;
991+ case "bpm": 現在の.チップ種別 = チップ種別.BPM; break;
992+ case "song": 現在の.チップ種別 = チップ種別.背景動画; break;
993+ default:
994+ Trace.TraceError( $"Lane(レーン指定)コマンドのパラメータ記述 '{パラメータ}' が不正です。このコマンドをスキップします。[{現在の.行番号}行目]" );
995+ break;
996+ }
1358997 }
1359- //----------------
998+ //-----------------
1360999 #endregion
1000+ break;
13611001
1362- if( cc.小節番号 > 小節番号 )
1363- {
1364- // チップリストは昇順に並んでいるので、これ以上検索しても無駄。
1365- break;
1366- }
1367- else if( cc.小節番号 == 小節番号 )
1002+ case "resolution":
1003+ #region " Resolution(小節解像度指定)コマンド "
1004+ //-----------------
13681005 {
1369- var lane = レーン種別.Bass; // 対応するレーンがなかったら Bass でも返しておく。
1370-
1371- foreach( var kvp in dicSSTFレーンチップ対応表 )
1006+ if( !(int.TryParse( パラメータ, out int 解像度 ) ) )
13721007 {
1373- if( kvp.Value.Contains( cc.チップ種別 ) )
1374- {
1375- lane = kvp.Key;
1376- break;
1377- }
1008+ Trace.TraceError( $"Resolution(小節解像度指定)コマンドの解像度が不正です。このコマンドをスキップします。[{現在の.行番号}行目]" );
1009+ continue;
1010+ }
1011+ if( 1 > 解像度 )
1012+ {
1013+ Trace.TraceError( $"Resolution(小節解像度指定)コマンドの解像度は 1 以上でなければなりません。このコマンドをスキップします。[{現在の.行番号}行目]" );
1014+ continue;
13781015 }
13791016
1380- dicレーン別チップリスト[ lane ].Add( cc );
1017+ 現在の.小節解像度 = 解像度;
13811018 }
1382- }
1383- //-----------------
1384- #endregion
1385-
1386- #region " Part行 出力。"
1387- //-----------------
1388- sw.Write( $"Part = {小節番号.ToString()}" );
1019+ //-----------------
1020+ #endregion
1021+ break;
13891022
1390- if( this.小節長倍率リスト[ 小節番号 ] != 1.0 )
1391- sw.Write( $"s{this.小節長倍率リスト[ 小節番号 ].ToString()}" );
1023+ case "chips":
1024+ #region " Chips(チップ指定)コマンド "
1025+ //-----------------
1026+ // パラメータを区切り文字 ',' でチップトークンに分割。
1027+ string[] chipTokens = パラメータ.Split( ',' );
13921028
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 )
1029+ // すべてのチップトークンについて……
1030+ for( int i = 0; i < chipTokens.Length; i++ )
14011031 {
1402- sw.Write( $"Lane={laneType.ToString()}; " );
1032+ chipTokens[ i ].Trim();
14031033
1404- #region " 新しい解像度を求める。"
1034+ if( 0 == chipTokens[ i ].Length )
1035+ continue;
1036+
1037+ #region " チップを生成する。"
14051038 //-----------------
1406- int 新しい解像度 = 1;
1407- foreach( var cc in dicレーン別チップリスト[ laneType ] )
1408- 新しい解像度 = this._最小公倍数を返す( 新しい解像度, cc.小節解像度 );
1039+ var chip = new チップ() {
1040+ 小節番号 = 現在の.小節番号,
1041+ チップ種別 = 現在の.チップ種別,
1042+ 小節解像度 = 現在の.小節解像度,
1043+ 音量 = チップ.最大音量,
1044+ };
1045+ chip.可視 = chip.可視の初期値;
1046+ if( chip.チップ種別 == チップ種別.China ) chip.チップ内文字列 = "C N";
1047+ if( chip.チップ種別 == チップ種別.Splash ) chip.チップ内文字列 = "S P";
14091048 //-----------------
14101049 #endregion
1411- #region " dicレーン別チップリスト[ lane ] 要素の 小節解像度 と 小節内位置 を 新しい解像度 に合わせて修正する。 "
1050+ #region " チップ位置を取得する。"
14121051 //-----------------
1413- foreach( var cc in dicレーン別チップリスト[ laneType ] )
14141052 {
1415- int 倍率 = 新しい解像度 / cc.小節解像度; // 新しい解像度 は 小節解像度 の最小公倍数なので常に割り切れる。
1053+ string 位置番号文字列 = this._指定された文字列の先頭から数字文字列を取り出す( ref chipTokens[ i ] );
1054+ chipTokens[ i ].Trim();
14161055
1417- cc.小節解像度 *= 倍率;
1418- cc.小節内位置 *= 倍率;
1056+ // 文法チェック。
1057+ if( string.IsNullOrEmpty( 位置番号文字列 ) )
1058+ {
1059+ Trace.TraceError( $"チップの位置指定の記述がありません。このチップをスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1060+ continue;
1061+ }
1062+
1063+ // 位置を取得。
1064+ if( false == int.TryParse( 位置番号文字列, out int チップ位置 ) )
1065+ {
1066+ Trace.TraceError( $"チップの位置指定の記述が不正です。このチップをスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1067+ continue;
1068+ }
1069+
1070+ // 値域チェック。
1071+ if( ( 0 > チップ位置 ) || ( チップ位置 >= 現在の.小節解像度 ) )
1072+ {
1073+ Trace.TraceError( $"チップの位置が負数であるか解像度(Resolution)以上の値になっています。このチップをスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1074+ continue;
1075+ }
1076+
1077+ chip.小節内位置 = チップ位置;
14191078 }
14201079 //-----------------
14211080 #endregion
1081+ #region " 共通属性・レーン別属性があれば取得する。"
1082+ //-----------------
1083+ while( 0 < chipTokens[ i ].Length )
1084+ {
1085+ var 属性ID = char.ToLower( chipTokens[ i ][ 0 ] );
14221086
1423- sw.Write( $"Resolution = {新しい解像度}; " );
1424- sw.Write( "Chips = " );
1087+ // 共通属性
14251088
1426- for( int i = 0; i < dicレーン別チップリスト[ laneType ].Count; i++ )
1427- {
1428- チップ cc = dicレーン別チップリスト[ laneType ][ i ];
1089+ if( 'v' == 属性ID )
1090+ {
1091+ #region " 音量 "
1092+ //----------------
1093+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1094+ string 音量文字列 = this._指定された文字列の先頭から数字文字列を取り出す( ref chipTokens[ i ] );
1095+ chipTokens[ i ].Trim();
14291096
1430- // 位置を出力。
1431- sw.Write( cc.小節内位置.ToString() );
1097+ // 文法チェック。
1098+ if( string.IsNullOrEmpty( 音量文字列 ) )
1099+ {
1100+ Trace.TraceError( $"チップの音量指定の記述がありません。この属性をスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1101+ continue;
1102+ }
14321103
1433- // 属性を出力(あれば)。
1104+ // チップ音量の取得。
1105+ if( !( int.TryParse( 音量文字列, out int チップ音量 ) ) )
1106+ {
1107+ Trace.TraceError( $"チップの音量指定の記述が不正です。この属性をスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1108+ continue;
1109+ }
14341110
1435- #region " (1) 共通属性 "
1436- //-----------------
1437- if( cc.音量 < チップ.最大音量 )
1438- sw.Write( $"v{cc.音量.ToString()}" );
1439- //-----------------
1440- #endregion
1441- #region " (2) 専用属性 "
1442- //-----------------
1443- switch( cc.チップ種別 )
1111+ // 値域チェック。
1112+ if( ( 1 > チップ音量 ) || ( チップ音量 > チップ.最大音量 ) )
1113+ {
1114+ Trace.TraceError( $"チップの音量が適正範囲(1~{チップ.最大音量})を超えています。このチップをスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1115+ continue;
1116+ }
1117+
1118+ chip.音量 = チップ音量;
1119+ continue;
1120+ //----------------
1121+ #endregion
1122+ }
1123+
1124+ // レーン別属性
1125+
1126+ switch( 現在の.チップ種別 )
14441127 {
1128+ case チップ種別.LeftCrash:
1129+ if( 'm' == 属性ID )
1130+ {
1131+ #region " ミュート "
1132+ //----------------
1133+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1134+ chip.チップ種別 = チップ種別.LeftCymbal_Mute;
1135+ //----------------
1136+ #endregion
1137+ }
1138+ else
1139+ {
1140+ #region " 未知の属性 "
1141+ //-----------------
1142+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1143+ Trace.TraceError( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1144+ //-----------------
1145+ #endregion
1146+ }
1147+ continue;
1148+
1149+ case チップ種別.Ride:
14451150 case チップ種別.Ride_Cup:
1446- sw.Write( 'c' );
1447- break;
1151+ if( 'c' == 属性ID )
1152+ {
1153+ #region " Ride.カップ "
1154+ //-----------------
1155+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1156+ chip.チップ種別 = チップ種別.Ride_Cup;
1157+ //-----------------
1158+ #endregion
1159+ }
1160+ else
1161+ {
1162+ #region " 未知の属性 "
1163+ //-----------------
1164+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1165+ Trace.TraceError( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1166+ //-----------------
1167+ #endregion
1168+ }
1169+ continue;
14481170
1449- case チップ種別.HiHat_Open:
1450- sw.Write( 'o' );
1451- break;
1171+ case チップ種別.China:
1172+ {
1173+ #region " 未知の属性 "
1174+ //-----------------
1175+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1176+ Trace.TraceError( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1177+ //-----------------
1178+ #endregion
1179+ }
1180+ continue;
14521181
1453- case チップ種別.HiHat_HalfOpen:
1454- sw.Write( 'h' );
1455- break;
1182+ case チップ種別.Splash:
1183+ {
1184+ #region " 未知の属性 "
1185+ //-----------------
1186+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1187+ Trace.TraceError( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1188+ //-----------------
1189+ #endregion
1190+ }
1191+ continue;
14561192
1193+ case チップ種別.HiHat_Close:
1194+ case チップ種別.HiHat_HalfOpen:
1195+ case チップ種別.HiHat_Open:
14571196 case チップ種別.HiHat_Foot:
1458- sw.Write( 'f' );
1459- break;
1460-
1461- case チップ種別.Snare_OpenRim:
1462- sw.Write( 'o' );
1463- break;
1197+ if( 'o' == 属性ID )
1198+ {
1199+ #region " HiHat.オープン "
1200+ //-----------------
1201+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1202+ chip.チップ種別 = チップ種別.HiHat_Open;
1203+ //-----------------
1204+ #endregion
1205+ }
1206+ else if( 'h' == 属性ID )
1207+ {
1208+ #region " HiHat.ハーフオープン "
1209+ //-----------------
1210+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1211+ chip.チップ種別 = チップ種別.HiHat_HalfOpen;
1212+ //-----------------
1213+ #endregion
1214+ }
1215+ else if( 'c' == 属性ID )
1216+ {
1217+ #region " HiHat.クローズ "
1218+ //-----------------
1219+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1220+ chip.チップ種別 = チップ種別.HiHat_Close;
1221+ //-----------------
1222+ #endregion
1223+ }
1224+ else if( 'f' == 属性ID )
1225+ {
1226+ #region " HiHat.フットスプラッシュ "
1227+ //-----------------
1228+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1229+ chip.チップ種別 = チップ種別.HiHat_Foot;
1230+ //-----------------
1231+ #endregion
1232+ }
1233+ else
1234+ {
1235+ #region " 未知の属性 "
1236+ //-----------------
1237+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1238+ Trace.TraceError( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1239+ //-----------------
1240+ #endregion
1241+ }
1242+ continue;
14641243
1244+ case チップ種別.Snare:
14651245 case チップ種別.Snare_ClosedRim:
1466- sw.Write( 'c' );
1467- break;
1468-
1246+ case チップ種別.Snare_OpenRim:
14691247 case チップ種別.Snare_Ghost:
1470- sw.Write( 'g' );
1471- break;
1248+ if( 'o' == 属性ID )
1249+ {
1250+ #region " Snare.オープンリム "
1251+ //-----------------
1252+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1253+ chip.チップ種別 = チップ種別.Snare_OpenRim;
1254+ //-----------------
1255+ #endregion
1256+ }
1257+ else if( 'c' == 属性ID )
1258+ {
1259+ #region " Snare.クローズドリム "
1260+ //-----------------
1261+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1262+ chip.チップ種別 = チップ種別.Snare_ClosedRim;
1263+ //-----------------
1264+ #endregion
1265+ }
1266+ else if( 'g' == 属性ID )
1267+ {
1268+ #region " Snare.ゴースト "
1269+ //-----------------
1270+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1271+ chip.チップ種別 = チップ種別.Snare_Ghost;
1272+ //-----------------
1273+ #endregion
1274+ }
1275+ else
1276+ {
1277+ #region " 未知の属性 "
1278+ //-----------------
1279+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1280+ Trace.TraceError( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1281+ //-----------------
1282+ #endregion
1283+ }
1284+ continue;
1285+
1286+ case チップ種別.Bass:
1287+ {
1288+ #region " 未知の属性 "
1289+ //-----------------
1290+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1291+ Trace.TraceError( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1292+ //-----------------
1293+ #endregion
1294+ }
1295+ continue;
14721296
1297+ case チップ種別.Tom1:
14731298 case チップ種別.Tom1_Rim:
1474- sw.Write( 'r' );
1475- break;
1299+ if( 'r' == 属性ID )
1300+ {
1301+ #region " Tom1.リム "
1302+ //-----------------
1303+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1304+ chip.チップ種別 = チップ種別.Tom1_Rim;
1305+ //-----------------
1306+ #endregion
1307+ }
1308+ else
1309+ {
1310+ #region " 未知の属性 "
1311+ //-----------------
1312+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1313+ Trace.TraceError( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1314+ //-----------------
1315+ #endregion
1316+ }
1317+ continue;
14761318
1319+ case チップ種別.Tom2:
14771320 case チップ種別.Tom2_Rim:
1478- sw.Write( 'r' );
1479- break;
1321+ if( 'r' == 属性ID )
1322+ {
1323+ #region " Tom2.リム "
1324+ //-----------------
1325+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1326+ chip.チップ種別 = チップ種別.Tom2_Rim;
1327+ //-----------------
1328+ #endregion
1329+ }
1330+ else
1331+ {
1332+ #region " 未知の属性 "
1333+ //-----------------
1334+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1335+ Trace.TraceError( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1336+ //-----------------
1337+ #endregion
1338+ }
1339+ continue;
14801340
1341+ case チップ種別.Tom3:
14811342 case チップ種別.Tom3_Rim:
1482- sw.Write( 'r' );
1483- break;
1484-
1485- case チップ種別.LeftCymbal_Mute:
1486- sw.Write( 'm' );
1487- break;
1343+ if( 'r' == 属性ID )
1344+ {
1345+ #region " Tom3.リム "
1346+ //-----------------
1347+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1348+ chip.チップ種別 = チップ種別.Tom3_Rim;
1349+ //-----------------
1350+ #endregion
1351+ }
1352+ else
1353+ {
1354+ #region " 未知の属性 "
1355+ //-----------------
1356+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1357+ Trace.TraceError( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1358+ //-----------------
1359+ #endregion
1360+ }
1361+ continue;
14881362
1489- case チップ種別.RightCymbal_Mute:
1490- sw.Write( 'm' );
1491- break;
1363+ case チップ種別.RightCrash:
1364+ if( 'm' == 属性ID )
1365+ {
1366+ #region " ミュート "
1367+ //----------------
1368+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1369+ chip.チップ種別 = チップ種別.RightCymbal_Mute;
1370+ //----------------
1371+ #endregion
1372+ }
1373+ else
1374+ {
1375+ #region " 未知の属性 "
1376+ //-----------------
1377+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1378+ Trace.TraceError( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1379+ //-----------------
1380+ #endregion
1381+ }
1382+ continue;
14921383
14931384 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
1385+ if( 'b' == 属性ID )
1386+ {
1387+ #region " BPM値 "
1388+ //-----------------
1389+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
15091390
1510- sw.WriteLine( "" ); // 次の Part 前に1行あける。
1511- }
1391+ string BPM文字列 = this._指定された文字列の先頭から数字文字列を取り出す( ref chipTokens[ i ] );
1392+ chipTokens[ i ].Trim();
15121393
1513- // メモ(小節単位)の出力
1394+ if( string.IsNullOrEmpty( BPM文字列 ) )
1395+ {
1396+ Trace.TraceError( $"BPM数値の記述がありません。この属性をスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1397+ continue;
1398+ }
15141399
1515- #region " dicメモ を小節番号で昇順に出力する。"
1516- //-----------------
1517- var dic昇順メモ = new Dictionary<int, string>();
1518- int 最大小節番号 = this.最大小節番号;
1400+ if( false == double.TryParse( BPM文字列, out double BPM ) || ( 0.0 >= BPM ) )
1401+ {
1402+ Trace.TraceError( $"BPM数値の記述が不正です。この属性をスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1403+ continue;
1404+ }
15191405
1520- for( int i = 0; i <= 最大小節番号; i++ )
1521- {
1522- if( this.dicメモ.ContainsKey( i ) )
1523- dic昇順メモ.Add( i, this.dicメモ[ i ] );
1524- }
1406+ chip.BPM = BPM;
1407+ chip.チップ内文字列 = BPM.ToString( "###.##" );
1408+ //-----------------
1409+ #endregion
1410+ }
1411+ else
1412+ {
1413+ #region " 未知の属性 "
1414+ //-----------------
1415+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1416+ Trace.TraceError( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1417+ //-----------------
1418+ #endregion
1419+ }
1420+ continue;
15251421
1526- foreach( var kvp in dic昇順メモ )
1527- {
1528- int 小節番号 = kvp.Key;
1422+ case チップ種別.背景動画:
1423+ {
1424+ #region " 未知の属性 "
1425+ //-----------------
1426+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1427+ Trace.TraceError( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1428+ //-----------------
1429+ #endregion
1430+ }
1431+ continue;
1432+ }
15291433
1530- // 改行コードは、2文字のリテラル "\n" に置換。
1531- string メモ = kvp.Value.Replace( Environment.NewLine, @"\n" );
1434+ #region " 未知の属性 "
1435+ //-----------------
1436+ chipTokens[ i ] = chipTokens[ i ].Substring( 1 ).Trim();
1437+ Trace.TraceError( $"未対応の属性「{属性ID}」が指定されています。この属性をスキップします。[{現在の.行番号}行目; {i + 1}個目のチップ]" );
1438+ //-----------------
1439+ #endregion
1440+ }
1441+ //-----------------
1442+ #endregion
15321443
1533- sw.WriteLine( $"PartMemo={小節番号},{メモ}" );
1444+ this.チップリスト.Add( chip );
1445+ }
1446+ //-----------------
1447+ #endregion
1448+ break;
15341449 }
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 );
15831450 }
15841451
1585- // 小節番号に対応付けて倍率を登録する。
1586- this.小節長倍率リスト[ 小節番号 ] = 倍率;
1452+ return true;
15871453 }
15881454
15891455 /// <summary>
@@ -1639,37 +1505,10 @@ namespace SSTFormat.v3
16391505 return 行;
16401506 }
16411507
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-
16691508 private int _最大公約数を返す( int m, int n )
16701509 {
16711510 if( ( 0 >= m ) || ( 0 >= n ) )
1672- throw new ArgumentOutOfRangeException( "引数に0以下の数は指定できません。" );
1511+ throw new Exception( "引数に0以下の数は指定できません。" );
16731512
16741513 // ユーグリッドの互除法
16751514 int r;
@@ -1681,5 +1520,13 @@ namespace SSTFormat.v3
16811520
16821521 return n;
16831522 }
1523+ private int _最小公倍数を返す( int m, int n )
1524+ {
1525+ if( ( 0 >= m ) || ( 0 >= n ) )
1526+ throw new Exception( "引数に0以下の数は指定できません。" );
1527+
1528+ return ( m * n / this._最大公約数を返す( m, n ) );
1529+ }
16841530 }
1531+
16851532 }
--- a/SSTFormat/v3/チップ.cs
+++ b/SSTFormat/v3/チップ.cs
@@ -152,9 +152,9 @@ namespace SSTFormat.v3
152152 {
153153 this.CopyFrom( コピー元チップ );
154154 }
155- public チップ( SSTFormat.v1.チップ コピー元v1チップ )
155+ public チップ( SSTFormat.v2.チップ コピー元v2チップ )
156156 {
157- this.CopyFrom( コピー元v1チップ );
157+ this.CopyFrom( コピー元v2チップ );
158158 }
159159
160160 public void CopyFrom( チップ srcChip )
@@ -183,32 +183,6 @@ namespace SSTFormat.v3
183183 this.チップ内文字列 = srcChip.チップ内文字列;
184184 this.枠外レーン数 = srcChip.枠外レーン数;
185185 }
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- }
212186 public void CopyFrom( SSTFormat.v2.チップ srcChip )
213187 {
214188 // プロパティ(1)
@@ -219,7 +193,7 @@ namespace SSTFormat.v3
219193 this.小節解像度 = srcChip.小節解像度;
220194 this.描画時刻ms = srcChip.描画時刻ms;
221195 this.発声時刻ms = srcChip.発声時刻ms;
222- this.音量 = srcChip.音量 * 2; // [仕様変更] v1:1~4 → v2:1~8
196+ this.音量 = srcChip.音量 * 2;
223197 this.BPM = srcChip.BPM;
224198
225199 // プロパティ(2)
--- a/SSTFormat/仕様履歴.txt
+++ b/SSTFormat/仕様履歴.txt
@@ -3,6 +3,7 @@ v3
33 チップに、チップ種別のサブとしてチップサブIDを設置。
44 DTXから変換した場合、ここにオブジェクト値が格納される。
55 SSTFでは未使用。
6+ チップに、難易度(0~9.99)を追加。
67
78 v2
89 チップ音量を4段階(1~4)から8段階(1~8)に変更。
Show on old repository browser