OSDN Git Service

背景動画のサウンド(BGM)について、発声開始時点での時刻補正を追加。
[strokestylet/CsWin10Desktop3.git] / StrokeStyleT / ステージ / 演奏 / 演奏ステージ.cs
1 using System;
2 using System.Collections.Concurrent;
3 using System.Collections.Generic;
4 using System.Diagnostics;
5 using System.Linq;
6 using SharpDX;
7 using SharpDX.DirectInput;
8 using FDK;
9 using FDK.メディア;
10 using FDK.メディア.サウンド.WASAPI;
11 using FDK.カウンタ;
12 using SSTFormat.v2;
13 using SST.入力;
14 using SST.設定;
15
16 using FDKUtilities = FDK.FDKUtilities;
17
18 namespace SST.ステージ.演奏
19 {
20         class 演奏ステージ : ステージ
21         {
22                 public enum フェーズ
23                 {
24                         演奏中,
25                         Failed,
26                         クリア時フェードアウト,
27                         クリア,
28                         キャンセル,
29                         ビュアーメッセージ待機,
30                 }
31
32                 public フェーズ 現在のフェーズ
33                 {
34                         get;
35                         set;
36                 }
37
38                 public const float ヒット判定バーの中央Y座標 = 869f + 43f;
39
40                 public const float レーンフレームの左端位置 = 619f;
41
42                 public ConcurrentDictionary<ヒットランク種別, int> ヒットランク別ヒット回数
43                 {
44                         get;
45                 } = new ConcurrentDictionary<ヒットランク種別, int>();
46
47
48                 public 演奏ステージ()
49                 {
50                         this.子リスト.Add( this._ステージ台 = new 画像( @"$(System)images\ステージ台.png" ) );
51                         this.子リスト.Add( this._レーンフレーム = new レーンフレーム() );
52                         this.子リスト.Add( this._ドラムセット = new ドラムセット() );
53                         this.子リスト.Add( this._ヒット判定バー = new 画像( @"$(System)images\判定バー.png" ) );
54                         this.子リスト.Add( this._チップ画像 = new 画像( @"$(System)images\Chips.png" ) );
55                         this.子リスト.Add( this._回転羽 = new 回転羽( 最大同時発火数: 32 ) );
56                         this.子リスト.Add( this._コンソールフォント = new コンソールフォント() );
57                         this.子リスト.Add( this._ドラムサウンド = new ドラムサウンド() );
58                         this.子リスト.Add( this._コンボ = new コンボ() );
59                         this.子リスト.Add( this._ヒットランク = new ヒットランク() );
60                         this.子リスト.Add( this._クリア時フェードアウト = new フェードアウト( 1.0f ) );
61                         this.子リスト.Add( this._白パネル = new 画像( @"$(System)images\パネル白64x64.png" ) );
62                         this.子リスト.Add( this._FPS = new FPS() );
63                 }
64
65                 protected override void On活性化( グラフィックデバイス gd )
66                 {
67                         using( Log.Block( FDKUtilities.現在のメソッド名 ) )
68                         {
69                                 this._活性化した直後である = true;
70                                 this._背景動画開始済み = false;
71                                 this._現在進行描画中の譜面スクロール速度の倍率 = App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率;
72                                 this._描画開始チップ番号 = -1;
73                                 this._背景動画 = null;
74                                 this._BGM = null;
75                                 //this._デコード済みWaveSource = null;    --> キャッシュなので消さない。
76                                 this._背景動画開始済み = false;
77                                 this._BGM再生開始済み = false;
78                                 this._チップ画像の矩形リスト = new 矩形リスト( @"$(System)images\Chips Rectangle List.xml" );      // デバイスリソースは持たないので、子Activityではない。
79
80                                 this.ヒットランク別ヒット回数.Clear();
81                                 foreach( ヒットランク種別 hitRankType in Enum.GetValues( typeof( ヒットランク種別 ) ) )
82                                         this.ヒットランク別ヒット回数[ hitRankType ] = 0;
83
84                                 #region " 背景動画とBGMを生成する。"
85                                 //----------------
86                                 if( ( null != App.演奏スコア ) && ( App.演奏スコア.背景動画ファイル名.Nullでも空でもない() ) )
87                                 {
88                                         Log.Info( "背景動画とBGMを読み込みます。" );
89
90                                         // 動画を子リストに追加。
91                                         this.子リスト.Add( this._背景動画 = new 動画( App.演奏スコア.背景動画ファイル名, App.システム設定.動画デコーダのキューサイズ ) );
92
93                                         // 動画から音声パートを抽出して BGM を作成。
94                                         if( ( null != this._デコード済みWaveSource ) && this._デコード済みWaveSource.Path.Equals( App.演奏スコア.背景動画ファイル名 ) )
95                                         {
96                                                 // (A) 前回生成したBGMとパスが同じなので、前回のデコード済み WaveSource をキャッシュとして再利用する。
97                                                 Log.Info( "前回生成したサウンドデータを再利用します。" );
98                                         }
99                                         else
100                                         {
101                                                 // (B) 初めての生成か、または前回生成したBGMとパスが違うので、新しくデコード済み WaveSource を生成する。
102                                                 this._デコード済みWaveSource?.Dispose();
103                                                 this._デコード済みWaveSource = new DecodedWaveSource( App.演奏スコア.背景動画ファイル名, App.サウンドデバイス.WaveFormat );
104                                         }
105
106                                         this._BGM?.Dispose();
107                                         this._BGM = App.サウンドデバイス.サウンドを生成する( this._デコード済みWaveSource );
108                                 }
109                                 else
110                                 {
111                                         Log.Info( "背景動画とBGMはありません。" );
112                                 }
113                                 //----------------
114                                 #endregion
115
116                                 #region " 最初のフェーズを設定する。"
117                                 //----------------
118                                 if( ( App.ビュアーモードではない ) ||
119                                         ( ( null != App.最後に取得したビュアーメッセージ ) &&
120                                           ( App.最後に取得したビュアーメッセージ.種別 == Viewer.ViewerMessage.指示種別.演奏開始 ) ) )
121                                 {
122                                         this.現在のフェーズ = フェーズ.演奏中;
123                                 }
124                                 else
125                                 {
126                                         this.現在のフェーズ = フェーズ.ビュアーメッセージ待機;
127                                 }
128                                 //----------------
129                                 #endregion
130                         }
131                 }
132
133                 protected override void On非活性化( グラフィックデバイス gd )
134                 {
135                         using( Log.Block( FDKUtilities.現在のメソッド名 ) )
136                         {
137                                 // 背景動画を生成した場合は子リストから削除。
138                                 if( null != this._背景動画 )
139                                         this.子リスト.Remove( this._背景動画 );
140
141                                 App.ユーザ管理.選択されているユーザ.保存する();
142
143                                 this._活性化した直後である = false;
144                         }
145                 }
146
147                 public override void 高速進行する()
148                 {
149                         Debug.Assert( this.活性化している );
150
151                         switch( this.現在のフェーズ )
152                         {
153                                 case フェーズ.演奏中:
154                                         #region " *** "
155                                         //----------------
156                                         {
157                                                 // 進行。
158
159                                                 #region " 初めての進行描画。"
160                                                 //----------------
161                                                 if( this._活性化した直後である )
162                                                 {
163                                                         this._活性化した直後である = false;
164                                                         this._描画開始チップ番号 = 0; // -1 → 0; 演奏開始。
165                                                         this._チップアニメ.開始する( 最初の値: 0, 最後の値: 48, 値をひとつ増加させるのにかける時間ms: 10 );
166
167                                                         #region " 演奏開始時刻を初期化し、動画とBGMの再生を開始する。"
168                                                         //----------------
169                                                         double 演奏開始位置の先頭からの時間sec = 0.0;
170
171                                                         // 演奏開始時刻の設定(1)
172                                                         App.サウンドタイマ.リセットする( 演奏開始位置の先頭からの時間sec );
173                                                         Log.Info( $"演奏開始時刻(背景動画再生チェック前): {App.サウンドタイマ.現在時刻sec} sec" );
174
175                                                         // 演奏開始時刻の設定(2) ビュアーメッセージがある場合、開始時刻を修正する。
176                                                         var msg = App.最後に取得したビュアーメッセージ;
177                                                         if( null != msg )
178                                                         {
179                                                                 // 演奏開始位置を設定。
180                                                                 演奏開始位置の先頭からの時間sec = this._演奏開始小節番号を設定しその時刻secを返す( msg.演奏を開始する小節番号 );
181                                                                 Log.Info( $"演奏開始の先頭からの時間: {演奏開始位置の先頭からの時間sec} sec" );
182
183                                                                 // その他のオプション。
184                                                                 App.システム設定.Autoチップのドラム音を再生する = msg.ドラムチップのヒット時に発声する;
185                                                         }
186
187                                                         // BGMと動画を(必要あれば)再生開始。
188                                                         this._再生中の時刻なら動画とBGMを再生開始する( 演奏開始位置の先頭からの時間sec );
189
190                                                         // 演奏開始時刻の設定(3) 動画とBGMが再生された場合、ここに到達するまでに多少の遅延が生じているので、ここで演奏開始時刻を再取得しておく。
191                                                         App.サウンドタイマ.リセットする( 演奏開始位置の先頭からの時間sec );
192                                                         Log.Info( $"演奏開始時刻(背景動画再生チェック後): {App.サウンドタイマ.現在時刻sec} sec" );
193                                                         //----------------
194                                                         #endregion
195                                                 }
196                                                 //----------------
197                                                 #endregion
198
199                                                 this._FPS.FPSをカウントしプロパティを更新する();
200
201                                                 #region " 背景動画が再生されているのにBGMがまだ再生されていないなら、すぐに再生を開始する。"
202                                                 //----------------
203                                                 if( this._背景動画開始済み && !( this._BGM再生開始済み ) )
204                                                 {
205                                                         this._BGM?.Play();
206                                                         this._BGM再生開始済み = true;
207                                                 }
208                                                 //----------------
209                                                 #endregion
210
211                                                 double 現在の演奏時刻sec = this._演奏開始からの経過時間secを返す();
212
213                                                 // 自動演奏
214
215                                                 #region " 自動ヒット処理。"
216                                                 //----------------
217                                                 this._描画範囲のチップに処理を適用する( 現在の演奏時刻sec, ( chip, index, ヒット判定バーと描画との時間sec, ヒット判定バーと発声との時間sec, ヒット判定バーとの距離 ) => {
218
219                                                         var オプション設定 = App.ユーザ管理.選択されているユーザ.オプション設定;
220                                                         var 対応表 = オプション設定.ドラムとチップと入力の対応表[ chip.チップ種別 ];
221                                                         var AutoPlay = オプション設定.AutoPlay[ 対応表.AutoPlay種別 ];
222
223                                                         bool チップはヒット済みである = chip.ヒット済みである;
224                                                         bool チップはMISSエリアに達している = ( ヒット判定バーと描画との時間sec > オプション設定.最大ヒット距離sec[ ヒットランク種別.POOR ] );
225                                                         bool チップは描画についてヒット判定バーを通過した = ( 0 <= ヒット判定バーと描画との時間sec );
226                                                         bool チップは発声についてヒット判定バーを通過した = ( 0 <= ヒット判定バーと発声との時間sec );
227
228                                                         if( チップはヒット済みである )
229                                                         {
230                                                                 // 何もしない。
231                                                                 return;
232                                                         }
233
234                                                         if( チップはMISSエリアに達している )
235                                                         {
236                                                                 // MISS判定。
237                                                                 if( AutoPlay && 対応表.AutoPlayON.MISS判定 )
238                                                                 {
239                                                                         this._チップのヒット処理を行う( chip, ヒットランク種別.MISS, 対応表.AutoPlayON.自動ヒット時処理, ヒット判定バーと発声との時間sec );
240                                                                         return;
241                                                                 }
242                                                                 else if( !AutoPlay && 対応表.AutoPlayOFF.MISS判定 )
243                                                                 {
244                                                                         this._チップのヒット処理を行う( chip, ヒットランク種別.MISS, 対応表.AutoPlayOFF.ユーザヒット時処理, ヒット判定バーと発声との時間sec );
245                                                                         return;
246                                                                 }
247                                                                 else
248                                                                 {
249                                                                         // 通過。
250                                                                 }
251                                                         }
252
253                                                         if( チップは発声についてヒット判定バーを通過した )
254                                                         {
255                                                                 // 自動ヒット判定。
256                                                                 if( ( AutoPlay && 対応表.AutoPlayON.自動ヒット && 対応表.AutoPlayON.自動ヒット時処理.再生 ) ||
257                                                                         ( !AutoPlay && 対応表.AutoPlayOFF.自動ヒット && 対応表.AutoPlayOFF.自動ヒット時処理.再生 ) )
258                                                                 {
259                                                                         this._チップの発声を行う( chip, ヒット判定バーと発声との時間sec );
260                                                                 }
261                                                         }
262
263                                                         if( チップは描画についてヒット判定バーを通過した )
264                                                         {
265                                                                 // 自動ヒット判定。
266                                                                 if( AutoPlay && 対応表.AutoPlayON.自動ヒット )
267                                                                 {
268                                                                         this._チップのヒット処理を行う( chip, ヒットランク種別.AUTO, 対応表.AutoPlayON.自動ヒット時処理, ヒット判定バーと発声との時間sec );
269                                                                         return;
270                                                                 }
271                                                                 else if( !AutoPlay && 対応表.AutoPlayOFF.自動ヒット )
272                                                                 {
273                                                                         this._チップのヒット処理を行う( chip, ヒットランク種別.AUTO, 対応表.AutoPlayOFF.自動ヒット時処理, ヒット判定バーと発声との時間sec );
274                                                                         return;
275                                                                 }
276                                                                 else
277                                                                 {
278                                                                         // 通過。
279                                                                 }
280                                                         }
281
282                                                 } );
283                                                 //----------------
284                                                 #endregion
285
286                                                 // 入力(1) 手動演奏
287
288                                                 App.入力管理.すべての入力デバイスをポーリングする( 入力履歴を記録する: false );
289
290                                                 #region " ユーザヒット処理。"
291                                                 //----------------
292                                                 {
293                                                         var オプション設定 = App.ユーザ管理.選択されているユーザ.オプション設定;
294
295                                                         #region " ヒットしてようがしてまいが起こすアクション(空打ち処理)。 "
296                                                         //----------------
297                                                         foreach( var 入力 in App.入力管理.ポーリング結果 )
298                                                         {
299                                                                 if( 入力.InputEvent.離された )
300                                                                         continue;   // 押下イベントじゃないなら無視。
301
302                                                                 this._ドラムセット.ヒットアニメ開始( 入力.Type, オプション設定.表示レーンの左右 );
303
304                                                                 var カラム = オプション設定.ドラムとチップと入力の対応表.対応表.Where( ( kvp ) => ( kvp.Value.ドラム入力種別 == 入力.Type ) );  // カラムは struct なので FirstOrDefault() は使わない。
305                                                                 if( 0 < カラム.Count() )
306                                                                         this._レーンフレーム.フラッシュ開始( カラム.First().Value.表示レーン種別 );
307                                                         }
308                                                         //----------------
309                                                         #endregion
310
311                                                         var 処理済み入力 = new List<ドラム入力イベント>(); // ヒット処理が終わった入力は、二重処理しないよう、この中に追加しておく。
312
313                                                         this._描画範囲のチップに処理を適用する( 現在の演奏時刻sec, ( chip, index, ヒット判定バーと描画との時間sec, ヒット判定バーと発声との時間sec, ヒット判定バーとの距離 ) => {
314
315                                                                 #region " ヒット判定 "
316                                                                 //----------------
317                                                                 if( chip.ヒット済みである )
318                                                                         return;
319
320                                                                 var チップの対応表 = オプション設定.ドラムとチップと入力の対応表[ chip.チップ種別 ];
321
322                                                                 if( オプション設定.AutoPlay[ チップの対応表.AutoPlay種別 ] )
323                                                                         return; // チップが AutoPlay ON である。
324
325                                                                 if( !( チップの対応表.AutoPlayOFF.ユーザヒット ) )
326                                                                         return; // このチップは、AutoPlay OFF 時でもユーザヒットの対象ではない。
327
328                                                                 double ヒット判定バーとの時間の絶対値sec = Math.Abs( ヒット判定バーと描画との時間sec );
329
330                                                                 bool チップはMISSエリアに達している = ( ヒット判定バーと描画との時間sec > オプション設定.最大ヒット距離sec[ ヒットランク種別.POOR ] );
331                                                                 if( !( ヒット判定バーと描画との時間sec >= -( オプション設定.最大ヒット距離sec[ ヒットランク種別.POOR ] ) && !チップはMISSエリアに達している ) )
332                                                                         return; // チップはヒット可能エリアにある。
333
334                                                                 var ヒット入力 = App.入力管理.ポーリング結果.FirstOrDefault( ( 入力 ) => {
335                                                                         #region " chip にヒットする入力があれば true を返す。"
336                                                                         //----------------
337                                                                         if( !( 入力.InputEvent.押された ) )
338                                                                                 return false;   // 押下入力じゃない。
339
340                                                                         if( 処理済み入力.Contains( 入力 ) )
341                                                                                 return false;   // すでに今回のターンで処理済み(=処理済み入力リストに追加済み)。
342
343                                                                         // chip がシンバルフリーの対象なら、chip に直接対応する入力の他にも、ヒット判定すべき入力がある。
344                                                                         if( チップの対応表.シンバルフリーの対象 && オプション設定.シンバルフリー )
345                                                                         {
346                                                                                 // この入力に対応するカラムのうち、
347                                                                                 var カラムs = オプション設定.ドラムとチップと入力の対応表.対応表.Where( ( kvp ) => ( kvp.Value.ドラム入力種別 == 入力.Type ) );
348                                                                                 foreach( var カラム in カラムs )
349                                                                                 {
350                                                                                         // シンバルフリーなチップがあるなら true。
351                                                                                         if( カラム.Value.シンバルフリーの対象 )
352                                                                                                 return true;
353                                                                                 }
354                                                                                 // まったくなかったら false。
355                                                                                 return false;
356                                                                         }
357
358                                                                         // chip に対応する入力なら true。
359                                                                         return ( チップの対応表.ドラム入力種別 == 入力.Type );
360                                                                         //----------------
361                                                                         #endregion
362                                                                 } );
363
364                                                                 if( null == ヒット入力 )
365                                                                         return;
366                                                                 //----------------
367                                                                 #endregion
368
369                                                                 処理済み入力.Add( ヒット入力 );
370
371                                                                 #region " ヒットランクを判定する。"
372                                                                 //----------------
373                                                                 var ヒットランク = ヒットランク種別.POOR;
374
375                                                                 if( ヒット判定バーとの時間の絶対値sec <= オプション設定.最大ヒット距離sec[ ヒットランク種別.PERFECT ] )
376                                                                 {
377                                                                         ヒットランク = ヒットランク種別.PERFECT;
378                                                                 }
379                                                                 else if( ヒット判定バーとの時間の絶対値sec <= オプション設定.最大ヒット距離sec[ ヒットランク種別.GREAT ] )
380                                                                 {
381                                                                         ヒットランク = ヒットランク種別.GREAT;
382                                                                 }
383                                                                 else if( ヒット判定バーとの時間の絶対値sec <= オプション設定.最大ヒット距離sec[ ヒットランク種別.GOOD ] )
384                                                                 {
385                                                                         ヒットランク = ヒットランク種別.GOOD;
386                                                                 }
387                                                                 //----------------
388                                                                 #endregion
389
390                                                                 this._チップのヒット処理を行う( chip, ヒットランク, チップの対応表.AutoPlayOFF.ユーザヒット時処理, ヒット判定バーと発声との時間sec );
391
392                                                         } );
393                                                 }
394                                                 //----------------
395                                                 #endregion
396
397                                                 // 入力(2) アプリ操作
398
399                                                 if( App.入力管理.キーボードデバイス.キーが押された( 0, Key.Escape ) )
400                                                 {
401                                                         #region " ESC → ステージキャンセル "
402                                                         //----------------
403                                                         if( App.ビュアーモードではない )
404                                                         {
405                                                                 this.BGMを停止する();
406                                                                 this.現在のフェーズ = フェーズ.キャンセル;
407                                                                 break;  // このタスクを終了。
408                                                         }
409                                                         else
410                                                         {
411                                                                 // ビュアーモード時のキャンセルは無効。
412                                                         }
413                                                         //----------------
414                                                         #endregion
415                                                 }
416                                                 if( App.入力管理.キーボードデバイス.キーが押された( 0, Key.Up ) )
417                                                 {
418                                                         #region " 上 → 譜面スクロールを加速 "
419                                                         //----------------
420                                                         const double 最大倍率 = 8.0;
421                                                         App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 =
422                                                                 Math.Min( App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 + 0.5, 最大倍率 );
423                                                         //----------------
424                                                         #endregion
425                                                 }
426                                                 if( App.入力管理.キーボードデバイス.キーが押された( 0, Key.Down ) )
427                                                 {
428                                                         #region " 下 → 譜面スクロールを減速 "
429                                                         //----------------
430                                                         const double 最小倍率 = 0.5;
431                                                         App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 =
432                                                                 Math.Max( App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 - 0.5, 最小倍率 );
433                                                         //----------------
434                                                         #endregion
435                                                 }
436                                                 foreach( var ev in App.入力管理.MIDI入力デバイス.入力イベントリスト.Where( ( ie ) => ( 255 == ie.Key ) ) )
437                                                 {
438                                                         #region " ハイハットの開閉 "
439                                                         //----------------
440                                                         this._ドラムセット.ハイハットのベロシティ = ev.Velocity;
441                                                         //----------------
442                                                         #endregion
443                                                 }
444                                         }
445                                         //----------------
446                                         #endregion
447                                         break;
448                         }
449                 }
450
451                 public override void 進行描画する( グラフィックデバイス gd )
452                 {
453                         Debug.Assert( this.活性化している );
454                         Debug.Assert( null != gd );
455
456                         switch( this.現在のフェーズ )
457                         {
458                                 case フェーズ.演奏中:
459                                         if( this._活性化した直後である )
460                                                 break;  // 進行処理がまだ行われていない。
461
462                                         #region " 譜面スクロール速度が変化している → 追い付き進行 "
463                                         //----------------
464                                         {
465                                                 double 倍率 = this._現在進行描画中の譜面スクロール速度の倍率;
466
467                                                 if( 倍率 < App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 )
468                                                 {
469                                                         if( 0 > this._スクロール倍率追い付き用_最後の値 )
470                                                         {
471                                                                 this._スクロール倍率追い付き用カウンタ = new LoopCounter( 0, 1000, 10 );    // 0→100; 全部で10×1000 = 10000ms = 10sec あれば十分だろう
472                                                                 this._スクロール倍率追い付き用_最後の値 = 0;
473                                                         }
474                                                         else
475                                                         {
476                                                                 while( this._スクロール倍率追い付き用_最後の値 < this._スクロール倍率追い付き用カウンタ.現在値 )
477                                                                 {
478                                                                         倍率 += 0.025;
479                                                                         this._スクロール倍率追い付き用_最後の値++;
480                                                                 }
481
482                                                                 this._現在進行描画中の譜面スクロール速度の倍率 =
483                                                                         Math.Min( 倍率, App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 );
484                                                         }
485                                                 }
486                                                 else if( 倍率 > App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 )
487                                                 {
488                                                         if( 0 > this._スクロール倍率追い付き用_最後の値 )
489                                                         {
490                                                                 this._スクロール倍率追い付き用カウンタ = new LoopCounter( 0, 1000, 10 );    // 0→100; 全部で10×1000 = 10000ms = 10sec あれば十分だろう
491                                                                 this._スクロール倍率追い付き用_最後の値 = 0;
492                                                         }
493                                                         else
494                                                         {
495                                                                 while( this._スクロール倍率追い付き用_最後の値 < this._スクロール倍率追い付き用カウンタ.現在値 )
496                                                                 {
497                                                                         倍率 -= 0.025;
498                                                                         this._スクロール倍率追い付き用_最後の値++;
499                                                                 }
500
501                                                                 this._現在進行描画中の譜面スクロール速度の倍率 =
502                                                                         Math.Max( 倍率, App.ユーザ管理.選択されているユーザ.オプション設定.譜面スクロール速度の倍率 );
503                                                         }
504                                                 }
505                                                 else
506                                                 {
507                                                         this._スクロール倍率追い付き用_最後の値 = -1;
508                                                         this._スクロール倍率追い付き用カウンタ = null;
509                                                 }
510                                         }
511                                         //----------------
512                                         #endregion
513
514                                         this._コンボ.進行描画する( gd );
515                                         this._回転羽.進行描画する( gd );
516
517                                         double 演奏時刻sec = this._演奏開始からの経過時間secを返す();
518
519                                         #region " 背景動画とBGMの進行描画。"
520                                         //----------------
521                                         if( this._背景動画開始済み )
522                                         {
523                                                 // 背景動画チップがヒット済みなら、背景動画の進行描画を行う。
524                                                 switch( App.ユーザ管理.選択されているユーザ.オプション設定.動画表示パターン種別 )
525                                                 {
526                                                         case 動画表示パターン種別.中央表示:
527                                                                 #region " *** "
528                                                                 //----------------
529                                                                 {
530                                                                         float w = gd.設計画面サイズ.Width;
531                                                                         float h = gd.設計画面サイズ.Height;
532
533                                                                         this._背景動画?.描画する( gd, new RectangleF( 0f, 0f, w, h ), 0.2f );  // 全体
534
535                                                                         float 拡大縮小率 = 0.75f;
536                                                                         float 上移動 = 100.0f;
537
538                                                                         // 進行描画せず、直前に取得したフレームをそのまま描画する。
539                                                                         this._背景動画?.前のフレームを描画する( gd, new RectangleF(
540                                                                                 w * ( 1f - 拡大縮小率 ) / 2f,
541                                                                                 h * ( 1f - 拡大縮小率 ) / 2f - 上移動,
542                                                                                 w * 拡大縮小率,
543                                                                                 h * 拡大縮小率 ) );
544                                                                 }
545                                                                 //----------------
546                                                                 #endregion
547                                                                 break;
548
549                                                         case 動画表示パターン種別.最大表示:
550                                                                 #region " *** "
551                                                                 //----------------
552                                                                 this._背景動画?.描画する( gd, new RectangleF( 0f, 0f, gd.設計画面サイズ.Width, gd.設計画面サイズ.Height ), 1.0f );
553                                                                 //----------------
554                                                                 #endregion
555                                                                 break;
556                                                 }
557
558                                                 // 動画が重たいかもしれないので、演奏時刻をここで更新しておく。   ---> 重たくても更新禁止!(譜面スクロールがガタつく原因になる)
559                                                 //演奏時刻sec = this._演奏開始からの経過時間secを返す();
560                                         }
561                                         //----------------
562                                         #endregion
563
564                                         this._ステージ台.描画する( gd, 0f, 0f );
565                                         this._レーンフレーム.進行描画する( gd, レーンフレームの左端位置 );
566                                         this._コンボ.進行描画する( gd );
567                                         this._ヒットランク.進行描画する( gd, レーンフレームの左端位置 );
568                                         this._小節線拍線を描画する( gd, 演奏時刻sec );
569                                         this._ヒット判定バー.描画する( gd, 597f, ヒット判定バーの中央Y座標 - 43f );
570                                         this._ドラムセット.進行描画する( gd );
571                                         this._チップを描画する( gd, 演奏時刻sec );
572                                         this._回転羽.進行描画する( gd );
573                                         this._FPS.VPSをカウントする();
574                                         this._FPS.描画する( gd, 0f, 0f );
575                                         break;
576
577                                 case フェーズ.クリア時フェードアウト:
578                                         if( this._クリア時フェードアウト.開始されていない )
579                                         {
580                                                 this._クリア時フェードアウト.開始する();
581                                         }
582                                         else
583                                         {
584                                                 if( this._クリア時フェードアウト.完了した )
585                                                 {
586                                                         this.現在のフェーズ = フェーズ.クリア;
587                                                 }
588                                         }
589                                         this._ステージ台.描画する( gd, 0f, 0f );
590                                         this._レーンフレーム.進行描画する( gd, レーンフレームの左端位置 );
591                                         this._ヒット判定バー.描画する( gd, 597f, ヒット判定バーの中央Y座標 - 43f );
592                                         this._ドラムセット.進行描画する( gd );
593                                         this._回転羽.進行描画する( gd );
594                                         this._クリア時フェードアウト.進行描画する( gd, this._白パネル );
595                                         this._FPS.VPSをカウントする();
596                                         this._FPS.描画する( gd, 0f, 0f );
597                                         break;
598
599                                 case フェーズ.Failed:
600                                 case フェーズ.クリア:
601                                 case フェーズ.キャンセル:
602                                 case フェーズ.ビュアーメッセージ待機:
603                                         break;
604                         }
605                 }
606
607                 public void 演奏を停止する()
608                 {
609                         using( Log.Block( FDKUtilities.現在のメソッド名 ) )
610                         {
611                                 this._描画開始チップ番号 = -1;   // 演奏停止
612
613                                 this.BGMを停止する();
614                                 this._背景動画開始済み = false;
615
616                                 this._コンボ.COMBO値 = 0;
617                         }
618                 }
619
620                 /// <remarks>
621                 ///             演奏クリア時には、次の結果ステージに入ってもBGMが鳴り続ける。
622                 ///             そのため、後からBGMだけを別個に停止するためのメソッドが必要になる。
623                 /// </remarks>
624                 public void BGMを停止する()
625                 {
626                         using( Log.Block( FDKUtilities.現在のメソッド名 ) )
627                         {
628                                 this._BGM?.Stop();
629                                 this._BGM?.Dispose();
630                                 this._BGM = null;
631
632                                 //this._デコード済みWaveSource?.Dispose();        --> ここではまだ解放しない。
633                                 //this._デコード済みWaveSource = null;
634                         }
635                 }
636
637                 public void BGMのキャッシュを解放する()
638                 {
639                         using( Log.Block( FDKUtilities.現在のメソッド名 ) )
640                         {
641                                 this.BGMを停止する();
642                                 FDKUtilities.解放する( ref this._デコード済みWaveSource );
643                         }
644                 }
645
646
647                 private double _現在進行描画中の譜面スクロール速度の倍率 = 1.0;
648
649                 /// <summary>
650                 ///             <see cref="スコア.チップリスト"/> のうち、描画を始めるチップのインデックス番号。
651                 ///             未演奏時・演奏終了時は -1 。
652                 /// </summary>
653                 /// <remarks>
654                 ///             演奏開始直後は 0 で始まり、対象番号のチップが描画範囲を流れ去るたびに +1 される。
655                 ///             このメンバの更新は、高頻度進行タスクではなく、進行描画メソッドで行う。(低精度で構わないので)
656                 /// </remarks>
657                 private int _描画開始チップ番号 = -1;
658
659                 private 動画 _背景動画 = null;
660
661                 /// <remarks>
662                 ///             停止と解放は、演奏ステージクラスの非活性化後に、外部から行われる。
663                 ///             <see cref="SST.ステージ.演奏.演奏ステージ.BGMを停止する"/>
664                 ///             <see cref="SST.ステージ.演奏.演奏ステージ.BGMのキャッシュを解放する"/>
665                 /// </remarks>
666                 private Sound _BGM = null;
667
668                 /// <summary>
669                 ///             BGM の生成もとになるデコード済みサウンドデータ。
670                 ///     </summary>
671                 ///     <remarks>
672                 ///             活性化と非活性化に関係なく、常に最後にデコードしたデータを持つ。(キャッシュ)
673                 ///             演奏ステージインスタンスを破棄する際に、このインスタンスもDisposeすること。
674                 /// </remarks>
675                 private DecodedWaveSource _デコード済みWaveSource = null;
676
677                 private bool _背景動画開始済み = false;
678
679                 private bool _BGM再生開始済み = false;
680
681                 private 画像 _ステージ台 = null;
682
683                 private レーンフレーム _レーンフレーム = null;
684
685                 private ドラムセット _ドラムセット = null;
686
687                 private 画像 _ヒット判定バー = null;
688
689                 private 画像 _チップ画像 = null;
690
691                 private 矩形リスト _チップ画像の矩形リスト = null;
692
693                 private LoopCounter _チップアニメ = new LoopCounter();
694
695                 private 回転羽 _回転羽 = null;
696
697                 private コンソールフォント _コンソールフォント = null;
698
699                 private ドラムサウンド _ドラムサウンド = null;
700
701                 private コンボ _コンボ = null;
702
703                 private ヒットランク _ヒットランク = null;
704
705                 private FPS _FPS = null;
706
707                 private LoopCounter _スクロール倍率追い付き用カウンタ = null;
708
709                 private int _スクロール倍率追い付き用_最後の値 = -1;
710
711                 private フェードアウト _クリア時フェードアウト = null;
712
713                 private 画像 _白パネル = null;
714
715                 private bool _活性化した直後である;
716
717
718                 // 譜面描画。
719
720                 /// <summary>
721                 ///             指定した小節の先頭を演奏開始位置として設定し、その位置(時刻)を返す。
722                 /// </summary>
723                 /// <returns>演奏開始位置sec。(0~)</returns>
724                 private double _演奏開始小節番号を設定しその時刻secを返す( int 演奏開始小節番号 )
725                 {
726                         var score = App.演奏スコア;
727
728                         if( null == score )
729                                 return 0.0;
730
731                         double 演奏開始時刻sec = 0.0;
732
733                         for( int i = 0; i < score.チップリスト.Count; i++ )
734                         {
735                                 if( score.チップリスト[ i ].小節番号 < 演奏開始小節番号 )
736                                 {
737                                         // 開始チップ以前のチップはヒット済みとする。
738                                         score.チップリスト[ i ].ヒット済みの状態にする();
739                                 }
740                                 else
741                                 {
742                                         // 開始チップを設定。
743                                         this._描画開始チップ番号 = i;
744
745                                         // 演奏開始時刻は、開始チップの発声時刻から少し早めの値に。
746                                         演奏開始時刻sec = score.チップリスト[ i ].発声時刻sec;
747                                         演奏開始時刻sec -= 0.5;
748
749                                         // 開始チップ以降のすべてのチップをヒット前の状態にする。
750                                         for( int j = i; j >= 0; j-- )
751                                         {
752                                                 if( score.チップリスト[ j ].ヒット済みである )
753                                                         score.チップリスト[ j ].ヒット前の状態にする();
754                                         }
755
756                                         break;
757                                 }
758                         }
759
760                         return 演奏開始時刻sec;
761                 }
762
763                 private void _再生中の時刻なら動画とBGMを再生開始する( double 時刻sec )
764                 {
765                         using( Log.Block( FDKUtilities.現在のメソッド名 ) )
766                         {
767                                 Log.Info( $"指定された時刻: {時刻sec} sec" );
768
769                                 var 背景動画チップ = App.演奏スコア.チップリスト.FirstOrDefault( ( chip ) => ( chip.チップ種別 == チップ種別.背景動画 ) );
770
771                                 if( null == 背景動画チップ )
772                                 {
773                                         Log.Info( "背景動画チップは存在しません。" );
774                                         return;
775                                 }
776
777                                 double 背景動画の長さsec = this._BGM?.長さsec ?? 0.0;        // 動画のサイズは、映像ではなく音声を優先する。
778
779                                 Log.Info( $"背景動画の発生時刻: {背景動画チップ.発声時刻sec} sec" );
780                                 Log.Info( $"背景動画の長さ: {背景動画の長さsec} sec" );
781
782                                 // 指定された時刻secは再生期間内?
783                                 if( ( 背景動画チップ.発声時刻sec <= 時刻sec ) && ( 時刻sec < ( 背景動画チップ.発声時刻sec + 背景動画の長さsec ) ) )
784                                 {
785                                         // 背景動画の再生を開始する。
786                                         double 再生開始時刻sec = ( 時刻sec - 背景動画チップ.発声時刻sec );
787                                         this._背景動画?.再生を開始する( 再生開始時刻sec );
788                                         this._背景動画開始済み = true;
789                                         this._BGM?.Play( 再生開始時刻sec - App.サウンドデバイス.遅延sec );
790                                         this._BGM再生開始済み = true;
791                                         Log.Info( $"背景動画の再生を開始しました。(再生開始時刻: {再生開始時刻sec} sec)" );
792                                 }
793                                 else
794                                 {
795                                         Log.Info( $"指定された時刻は背景動画の再生期間内ではないので、何もしません。" );
796                                 }
797                         }
798                 }
799
800                 private double _演奏開始からの経過時間secを返す()
801                 {
802                         return App.サウンドタイマ.現在時刻sec;
803                 }
804
805                 // 小節線・拍線 と チップ は描画階層(奥行き)が異なるので、別々のメソッドに分ける。
806
807                 private void _小節線拍線を描画する( グラフィックデバイス gd, double 現在の演奏時刻sec )
808                 {
809                         if( null == this._チップ画像 )
810                                 return;
811
812                         this._チップ画像.加算合成 = false;
813
814                         this._描画範囲のチップに処理を適用する( 現在の演奏時刻sec, ( chip, index, ヒット判定バーと描画との時間sec, ヒット判定バーと発声との時間sec, ヒット判定バーとの距離 ) => {
815
816                                 if( chip.チップ種別 == チップ種別.小節線 )
817                                 {
818                                         #region " 小節線 "
819                                         //----------------
820                                         float 左位置 = レーンフレームの左端位置 + レーンフレーム.レーンto横中央相対位置[ 表示レーン種別.LeftCrash ] - 1f;                // -1f はレーン線の幅の半分。
821                                         float 上位置 = (float)( ヒット判定バーの中央Y座標 + ヒット判定バーとの距離 - 1f );   // -1f は小節線の厚みの半分。
822                                         if( this._チップ画像の矩形リスト[ nameof( チップ種別.小節線 ) ] is RectangleF 画像範囲 )
823                                         {
824                                                 this._チップ画像.描画する( gd, 左位置, 上位置, 転送元矩形: 画像範囲, 描画先矩形を整数境界に合わせる: true );     // false にすると、
825                                                 this._チップ画像.描画する( gd, 左位置 + 画像範囲.Width, 上位置, 転送元矩形: 画像範囲, 描画先矩形を整数境界に合わせる: true );        // チカチカする。
826                                         }
827                                         //----------------
828                                         #endregion
829                                 }
830                                 else if( chip.チップ種別 == チップ種別.拍線 )
831                                 {
832                                         #region " 拍線 "
833                                         //----------------
834                                         float 左位置 = レーンフレームの左端位置 + レーンフレーム.レーンto横中央相対位置 [ 表示レーン種別.LeftCrash ] - 1f;        // -1f はレーン線の幅の半分。
835                                         float 上位置 = (float)( ヒット判定バーの中央Y座標 + ヒット判定バーとの距離 - 1f );   // -1f は拍線の厚みの半分。
836                                         if( this._チップ画像の矩形リスト[ nameof( チップ種別.拍線 ) ] is RectangleF 画像範囲 )
837                                         {
838                                                 this._チップ画像.描画する( gd, 左位置: 左位置, 上位置: 上位置, 転送元矩形: 画像範囲, 描画先矩形を整数境界に合わせる: true );       // false にすると、
839                                                 this._チップ画像.描画する( gd, 左位置: 左位置 + 画像範囲.Width, 上位置: 上位置, 転送元矩形: 画像範囲, 描画先矩形を整数境界に合わせる: true );  // チカチカする。
840                                         }
841                                         //----------------
842                                         #endregion
843                                 }
844
845                         } );
846                 }
847
848                 private void _チップを描画する( グラフィックデバイス gd, double 現在の演奏時刻sec )
849                 {
850                         if( null == this._チップ画像 )
851                                 return;
852
853                         this._チップ画像.加算合成 = false;
854
855                         this._描画範囲のチップに処理を適用する( 現在の演奏時刻sec, ( chip, index, ヒット判定バーと描画との時間sec, ヒット判定バーと発声との時間sec, ヒット判定バーとの距離 ) => {
856
857                                 float 縦中央位置 = (float)( ヒット判定バーの中央Y座標 + ヒット判定バーとの距離 );
858
859                                 #region " チップが描画開始チップであり、かつ、そのY座標が画面下端を超えたなら、描画開始チップ番号を更新する。"
860                                 //----------------
861                                 if( ( index == this._描画開始チップ番号 ) &&
862                                         ( gd.設計画面サイズ.Height + 40.0 < 縦中央位置 ) )   // +40 はチップが隠れるであろう適当なマージン。
863                                 {
864                                         this._描画開始チップ番号++;
865
866                                         if( App.演奏スコア.チップリスト.Count <= this._描画開始チップ番号 )
867                                         {
868                                                 this.現在のフェーズ = フェーズ.クリア時フェードアウト;
869                                                 this._描画開始チップ番号 = -1;    // 演奏完了。
870                                                 return;
871                                         }
872                                 }
873                                 //----------------
874                                 #endregion
875
876                                 if( chip.不可視 )
877                                         return;
878
879                                 #region " チップを個別に描画する。"
880                                 //----------------
881                                 float 音量0to1 = chip.音量 / (float) チップ.最大音量;
882
883                                 switch( chip.チップ種別 )
884                                 {
885                                         case チップ種別.LeftCrash:
886                                                 _単画チップを1つ描画する( 表示レーン種別.LeftCrash, this._チップ画像の矩形リスト[ nameof( チップ種別.LeftCrash ) ], 縦中央位置, 音量0to1 );
887                                                 break;
888
889                                         case チップ種別.HiHat_Close:
890                                                 _アニメチップを1つ描画する( 表示レーン種別.HiHat, this._チップ画像の矩形リスト[ nameof( チップ種別.HiHat_Close ) ], 縦中央位置, 音量0to1 );
891                                                 break;
892
893                                         case チップ種別.HiHat_HalfOpen:
894                                                 _アニメチップを1つ描画する( 表示レーン種別.HiHat, this._チップ画像の矩形リスト[ nameof( チップ種別.HiHat_Close ) ], 縦中央位置, 音量0to1 );
895                                                 _単画チップを1つ描画する( 表示レーン種別.Foot, this._チップ画像の矩形リスト[ nameof( チップ種別.HiHat_HalfOpen ) ], 縦中央位置, 1.0f );
896                                                 break;
897
898                                         case チップ種別.HiHat_Open:
899                                                 _アニメチップを1つ描画する( 表示レーン種別.HiHat, this._チップ画像の矩形リスト[ nameof( チップ種別.HiHat_Close ) ], 縦中央位置, 音量0to1 );
900                                                 _単画チップを1つ描画する( 表示レーン種別.Foot, this._チップ画像の矩形リスト[ nameof( チップ種別.HiHat_Open ) ], 縦中央位置, 1.0f );
901                                                 break;
902
903                                         case チップ種別.HiHat_Foot:
904                                                 _単画チップを1つ描画する( 表示レーン種別.Foot, this._チップ画像の矩形リスト[ nameof( チップ種別.HiHat_Foot ) ], 縦中央位置, 1.0f );
905                                                 break;
906
907                                         case チップ種別.Snare:
908                                                 _アニメチップを1つ描画する( 表示レーン種別.Snare, this._チップ画像の矩形リスト[ nameof( チップ種別.Snare ) ], 縦中央位置, 音量0to1 );
909                                                 break;
910
911                                         case チップ種別.Snare_ClosedRim:
912                                                 _単画チップを1つ描画する( 表示レーン種別.Snare, this._チップ画像の矩形リスト[ nameof( チップ種別.Snare_ClosedRim ) ], 縦中央位置, 1.0f );
913                                                 break;
914
915                                         case チップ種別.Snare_OpenRim:
916                                                 _単画チップを1つ描画する( 表示レーン種別.Snare, this._チップ画像の矩形リスト[ nameof( チップ種別.Snare_OpenRim ) ], 縦中央位置, 音量0to1 );
917                                                 // ↓ないほうがいいかも。
918                                                 //_単画チップを1つ描画する( 表示レーン種別.Snare, this._チップ画像の矩形リスト[ nameof( チップ種別.Snare ) ], 縦中央位置, 音量0to1 );
919                                                 break;
920
921                                         case チップ種別.Snare_Ghost:
922                                                 _単画チップを1つ描画する( 表示レーン種別.Snare, this._チップ画像の矩形リスト[ nameof( チップ種別.Snare_Ghost ) ], 縦中央位置, 1.0f );
923                                                 break;
924
925                                         case チップ種別.Bass:
926                                                 _アニメチップを1つ描画する( 表示レーン種別.Bass, this._チップ画像の矩形リスト[ nameof( チップ種別.Bass ) ], 縦中央位置, 音量0to1 );
927                                                 break;
928
929                                         case チップ種別.Tom1:
930                                                 _アニメチップを1つ描画する( 表示レーン種別.Tom1, this._チップ画像の矩形リスト[ nameof( チップ種別.Tom1 ) ], 縦中央位置, 音量0to1 );
931                                                 break;
932
933                                         case チップ種別.Tom1_Rim:
934                                                 _単画チップを1つ描画する( 表示レーン種別.Tom1, this._チップ画像の矩形リスト[ nameof( チップ種別.Tom1_Rim ) ], 縦中央位置, 1.0f );
935                                                 break;
936
937                                         case チップ種別.Tom2:
938                                                 _アニメチップを1つ描画する( 表示レーン種別.Tom2, this._チップ画像の矩形リスト[ nameof( チップ種別.Tom2 ) ], 縦中央位置, 音量0to1 );
939                                                 break;
940
941                                         case チップ種別.Tom2_Rim:
942                                                 _単画チップを1つ描画する( 表示レーン種別.Tom2, this._チップ画像の矩形リスト[ nameof( チップ種別.Tom2_Rim ) ], 縦中央位置, 1.0f );
943                                                 break;
944
945                                         case チップ種別.Tom3:
946                                                 _アニメチップを1つ描画する( 表示レーン種別.Tom3, this._チップ画像の矩形リスト[ nameof( チップ種別.Tom3 ) ], 縦中央位置, 音量0to1 );
947                                                 break;
948
949                                         case チップ種別.Tom3_Rim:
950                                                 _単画チップを1つ描画する( 表示レーン種別.Tom3, this._チップ画像の矩形リスト[ nameof( チップ種別.Tom3_Rim ) ], 縦中央位置, 1.0f );
951                                                 break;
952
953                                         case チップ種別.RightCrash:
954                                                 _単画チップを1つ描画する( 表示レーン種別.RightCrash, this._チップ画像の矩形リスト[ nameof( チップ種別.RightCrash ) ], 縦中央位置, 音量0to1 );
955                                                 break;
956
957                                         case チップ種別.China:
958                                                 if( App.ユーザ管理.選択されているユーザ.オプション設定.表示レーンの左右.Chinaは左 )
959                                                 {
960                                                         _単画チップを1つ描画する( 表示レーン種別.LeftCrash, this._チップ画像の矩形リスト[ "LeftChina" ], 縦中央位置, 音量0to1 );
961                                                 }
962                                                 else
963                                                 {
964                                                         _単画チップを1つ描画する( 表示レーン種別.RightCrash, this._チップ画像の矩形リスト[ "RightChina" ], 縦中央位置, 音量0to1 );
965                                                 }
966                                                 break;
967
968                                         case チップ種別.Ride:
969                                                 if( App.ユーザ管理.選択されているユーザ.オプション設定.表示レーンの左右.Rideは左 )
970                                                 {
971                                                         _単画チップを1つ描画する( 表示レーン種別.LeftCrash, this._チップ画像の矩形リスト[ "LeftRide" ], 縦中央位置, 音量0to1 );
972                                                 }
973                                                 else
974                                                 {
975                                                         _単画チップを1つ描画する( 表示レーン種別.RightCrash, this._チップ画像の矩形リスト[ "RightRide" ], 縦中央位置, 音量0to1 );
976                                                 }
977                                                 break;
978
979                                         case チップ種別.Ride_Cup:
980                                                 if( App.ユーザ管理.選択されているユーザ.オプション設定.表示レーンの左右.Rideは左 )
981                                                 {
982                                                         _単画チップを1つ描画する( 表示レーン種別.LeftCrash, this._チップ画像の矩形リスト[ "LeftRide_Cup" ], 縦中央位置, 音量0to1 );
983                                                 }
984                                                 else
985                                                 {
986                                                         _単画チップを1つ描画する( 表示レーン種別.RightCrash, this._チップ画像の矩形リスト[ "RightRide_Cup" ], 縦中央位置, 音量0to1 );
987                                                 }
988                                                 break;
989
990                                         case チップ種別.Splash:
991                                                 if( App.ユーザ管理.選択されているユーザ.オプション設定.表示レーンの左右.Splashは左 )
992                                                 {
993                                                         _単画チップを1つ描画する( 表示レーン種別.LeftCrash, this._チップ画像の矩形リスト[ "LeftSplash" ], 縦中央位置, 音量0to1 );
994                                                 }
995                                                 else
996                                                 {
997                                                         _単画チップを1つ描画する( 表示レーン種別.RightCrash, this._チップ画像の矩形リスト[ "RightSplash" ], 縦中央位置, 音量0to1 );
998                                                 }
999                                                 break;
1000
1001                                         case チップ種別.LeftCymbal_Mute:
1002                                                 _単画チップを1つ描画する( 表示レーン種別.LeftCrash, this._チップ画像の矩形リスト[ "LeftCymbal_Mute" ], 縦中央位置, 1.0f );
1003                                                 break;
1004
1005                                         case チップ種別.RightCymbal_Mute:
1006                                                 _単画チップを1つ描画する( 表示レーン種別.RightCrash, this._チップ画像の矩形リスト[ "RightCymbal_Mute" ], 縦中央位置, 1.0f );
1007                                                 break;
1008                                 }
1009                                 //----------------
1010                                 #endregion
1011
1012                         } );
1013
1014                         #region " ローカル関数 "
1015                         //----------------
1016                         void _単画チップを1つ描画する( 表示レーン種別 lane, RectangleF? 元矩形, float 上位置, float 音量0to1 )
1017                         {
1018                                 if( null == 元矩形 )
1019                                         return;
1020
1021                                 var 画像範囲 = (RectangleF) 元矩形;
1022
1023                                 this._チップ画像?.描画する(
1024                                         gd,
1025                                         左位置: レーンフレームの左端位置 + レーンフレーム.レーンto横中央相対位置[ lane ] - ( 画像範囲.Width / 2f ),
1026                                         上位置: 上位置 - ( ( 画像範囲.Height / 2f ) * 音量0to1 ),
1027                                         転送元矩形: 元矩形,
1028                                         Y方向拡大率: 音量0to1 );
1029                         }
1030                         void _アニメチップを1つ描画する( 表示レーン種別 lane, RectangleF? 画像範囲orNull, float Y, float 音量0to1 )
1031                         {
1032                                 if( null == 画像範囲orNull )
1033                                         return;
1034
1035                                 var 画像範囲 = (RectangleF) 画像範囲orNull;
1036
1037                                 float チップ1枚の高さ = 18f;
1038                                 画像範囲.Offset( 0f, this._チップアニメ.現在値 * 15f );   // 下端3pxは下のチップと共有する前提のデザインなので、18f-3f = 15f。
1039                                 画像範囲.Height = チップ1枚の高さ;
1040                                 float 左位置 = レーンフレームの左端位置 + レーンフレーム.レーンto横中央相対位置[ lane ] - ( 画像範囲.Width / 2f );
1041                                 float 上位置 = Y - ( チップ1枚の高さ / 2f ) * 音量0to1;
1042
1043                                 this._チップ画像?.描画する( gd, 左位置, 上位置, 転送元矩形: 画像範囲, Y方向拡大率: 音量0to1 );
1044                         }
1045                         //----------------
1046                         #endregion
1047                 }
1048
1049                 /// <summary>
1050                 ///             <see cref="_描画開始チップ番号"/> から画面上端にはみ出すまでの間の各チップに対して、指定された処理を適用する。
1051                 /// </summary>
1052                 /// <param name="適用する処理">引数は、順に、対象のチップ、チップ番号、ヒット判定バーとの時間sec、ヒット判定バーとの距離</param>
1053                 private void _描画範囲のチップに処理を適用する( double 現在の演奏時刻sec, Action<チップ, int, double, double, double> 適用する処理 )
1054                 {
1055                         var スコア = App.演奏スコア;
1056                         if( null == スコア )
1057                                 return;
1058
1059                         for( int i = this._描画開始チップ番号; ( 0 <= i ) && ( i < スコア.チップリスト.Count ); i++ )
1060                         {
1061                                 var チップ = スコア.チップリスト[ i ];
1062
1063                                 // ヒット判定バーとチップの間の、時間 と 距離 を算出。→ いずれも、負数ならバー未達、0でバー直上、正数でバー通過。
1064                                 double ヒット判定バーと描画との時間sec = 現在の演奏時刻sec - チップ.描画時刻sec;
1065                                 double ヒット判定バーと発声との時間sec = 現在の演奏時刻sec - チップ.発声時刻sec;
1066                                 double ヒット判定バーとの距離 = スコア.指定された時間secに対応する符号付きピクセル数を返す( this._現在進行描画中の譜面スクロール速度の倍率, ヒット判定バーと描画との時間sec );
1067
1068                                 // 終了判定。
1069                                 bool チップは画面上端より上に出ている = ( ( ヒット判定バーの中央Y座標 + ヒット判定バーとの距離 ) < -40.0 );   // -40 はチップが隠れるであろう適当なマージン。
1070                                 if( チップは画面上端より上に出ている )
1071                                         break;
1072
1073                                 // 処理実行。開始判定(描画開始チップ番号の更新)もこの中で。
1074                                 適用する処理( チップ, i, ヒット判定バーと描画との時間sec, ヒット判定バーと発声との時間sec, ヒット判定バーとの距離 );
1075                         }
1076                 }
1077
1078                 private void _チップのヒット処理を行う( チップ chip, ヒットランク種別 hitRankType, ドラムとチップと入力の対応表.Column.Columnヒット処理 ヒット処理表, double ヒット判定バーと発声との時間sec )
1079                 {
1080                         chip.ヒット済みである = true;
1081
1082                         if( ヒット処理表.再生 )
1083                         {
1084                                 #region " チップの発声を行う。"
1085                                 //----------------
1086                                 if( chip.発声されていない )
1087                                         this._チップの発声を行う( chip, ヒット判定バーと発声との時間sec );
1088                                 //----------------
1089                                 #endregion
1090                         }
1091                         if( ヒット処理表.判定 )
1092                         {
1093                                 #region " チップの判定処理を行う。"
1094                                 //----------------
1095                                 if( hitRankType != ヒットランク種別.MISS )
1096                                 {
1097                                         // (A) PERFECT~POOR
1098
1099                                         var 対応表 = App.ユーザ管理.選択されているユーザ.オプション設定.ドラムとチップと入力の対応表[ chip.チップ種別 ];
1100
1101                                         this._コンボ.COMBO値++;
1102
1103                                         this._回転羽.発火する(
1104                                                 new Vector2(
1105                                                         レーンフレームの左端位置 + レーンフレーム.レーンto横中央相対位置[ 対応表.表示レーン種別 ],
1106                                                         ヒット判定バーの中央Y座標 ) );
1107
1108                                         this._ドラムセット.ヒットアニメ開始( 対応表.ドラム入力種別, App.ユーザ管理.選択されているユーザ.オプション設定.表示レーンの左右 );
1109
1110                                         this._ヒットランク.表示開始( chip.チップ種別, hitRankType );
1111                                         this.ヒットランク別ヒット回数[ hitRankType ]++;
1112                                 }
1113                                 else
1114                                 {
1115                                         // (B) MISS
1116
1117                                         this._コンボ.COMBO値 = 0;
1118
1119                                         this._ヒットランク.表示開始( chip.チップ種別, ヒットランク種別.MISS );
1120                                         this.ヒットランク別ヒット回数[ hitRankType ]++;
1121                                 }
1122                                 //----------------
1123                                 #endregion
1124                         }
1125                         if( ヒット処理表.非表示 )
1126                         {
1127                                 #region " チップを非表示にする。"
1128                                 //----------------
1129                                 if( hitRankType != ヒットランク種別.MISS )
1130                                 {
1131                                         chip.可視 = false;        // PERFECT~POOR チップは非表示。
1132                                 }
1133                                 else
1134                                 {
1135                                         if( chip.可視 )
1136                                                 chip.可視 = false;    // MISSチップは最後まで表示し続ける。
1137                                 }
1138                                 //----------------
1139                                 #endregion
1140                         }
1141                 }
1142
1143                 private void _チップの発声を行う( チップ chip, double 再生開始位置sec )
1144                 {
1145                         if( chip.発声済みである )
1146                                 return;
1147
1148                         chip.発声済みである = true;
1149
1150                         if( chip.チップ種別 == チップ種別.背景動画 )
1151                         {
1152                                 App.サウンドタイマ.一時停止する();
1153
1154                                 // 背景動画の再生を開始する。
1155                                 this._背景動画?.再生を開始する();
1156                                 this._背景動画開始済み = true;
1157
1158                                 // BGMの再生を開始する。
1159                                 this._BGM?.Play( 再生開始位置sec );
1160                                 this._BGM再生開始済み = true;
1161
1162                                 App.サウンドタイマ.再開する();
1163                         }
1164                         else
1165                         {
1166                                 // BGM以外のサウンドについては、再生開始位置sec は反映せず、常に最初から再生する。
1167                                 if( App.システム設定.Autoチップのドラム音を再生する )
1168                                         this._ドラムサウンド.発声する( chip.チップ種別, ( chip.音量 / (float) チップ.最大音量 ) );
1169                         }
1170                 }
1171         }
1172 }