OSDN Git Service

Merge branch 'develop'
[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 FDK.メディア;
7 using FDK;      // for SystemStringExtensions
8
9 namespace SST.ステージ.演奏
10 {
11         /// <remarks>
12         /// 入力:
13         ///  StrokeStyleT.演奏スコア(ビュアーモードなら null でも可)
14         ///  StrokeStyleT.ユーザ管理.現在選択されているユーザ(譜面スクロール速度の倍率)
15         ///  StrokeStyleT.Wasapiデバイス.AudioClock
16         ///  StrokeStyleT.ビュアーモードである
17         ///  StrokeStyleT.最後に取得したビュアーメッセージ
18         ///  
19         /// 出力:
20         ///  (A) クリアまたは失敗した場合
21         ///    StrokeStyleT.ユーザ管理.現在選択されているユーザ(譜面スクロール速度の倍率)(変更されていた場合)
22         ///    this.現在のフェーズ ← クリアor失敗
23         ///    
24         ///  (B) キャンセルされた場合
25         ///    StrokeStyleT.ユーザ管理.現在選択されているユーザ(譜面スクロール速度の倍率)(変更されていた場合)
26         ///    this.現在のフェーズ ← キャンセル
27         /// </remarks>
28         class 演奏ステージ : ステージ
29         {
30                 public ConcurrentDictionary<ヒット判定種別, int> ヒットした回数 { get; } = new ConcurrentDictionary<ヒット判定種別, int>();
31
32                 public enum フェーズ
33                 {
34                         初期状態,
35                         演奏中,
36                         クリアor失敗,
37                         キャンセル,
38                         ビュアーメッセージ待機中,
39                 }
40
41                 public FDK.同期.RWLock<フェーズ> 現在のフェーズ { get; } = new FDK.同期.RWLock<フェーズ>( フェーズ.初期状態 );
42
43                 public 演奏ステージ()
44                 {
45                         this.子リスト.Add( this._コンボ = new コンボ() );
46                         this.子リスト.Add( this._レーンフレーム = new レーンフレーム() );
47                         this.子リスト.Add( this._スクロール譜面 = new スクロール譜面() );
48                         this.子リスト.Add( this._ヒット判定文字列 = new ヒット判定文字列() );
49                         this.子リスト.Add( this._回転羽 = new 回転羽( 最大同時発火数: 32 ) );
50                         this.子リスト.Add( this._ドラムサウンド = new ドラムサウンド() );
51                         this.子リスト.Add( this._ドラムセット = new ドラムセット() );
52                         this.子リスト.Add( this._判定バー = new 画像( @"$(Static)\images\判定バー.png" ) );
53                         this.子リスト.Add( this._ステージ台 = new 画像( @"$(Static)\images\ステージ台.png" ) );
54                         this.子リスト.Add( this._FPS画像 = new 文字列画像() );
55
56                         // 子Activity の外部依存 Action の実体を定義する。
57                         this._スクロール譜面.ヒット判定文字列開始 = ( chipType, hitType ) => {
58                                 this._ヒット判定文字列.表示開始( chipType, hitType );
59                         };
60                         this._スクロール譜面.コンボリセット = () => {
61                                 this._コンボ.COMBO値.Value = 0;
62                         };
63                         this._スクロール譜面.コンボ加算 = () => {
64                                 this._コンボ.COMBO値.Value++;
65                         };
66                         this._スクロール譜面.ヒット判定数加算 = ( hitType ) => {
67                                 this.ヒットした回数[ hitType ]++;
68                         };
69                         this._スクロール譜面.背景動画の長さsec = () => {
70                                 return ( null != this._BGM ) ? this._BGM.長さsec : 0.0;
71                         };
72                         this._スクロール譜面.背景動画再生開始 = ( 開始位置sec ) => {
73                                 this._背景動画?.再生を開始する( 開始位置sec );
74                                 this._背景動画開始済み.Value = true;
75                                 if( null != this._BGM )
76                                         this._BGM?.Play( 開始位置sec );
77                                 this._BGM再生開始済み = true;
78                         };
79                         this._スクロール譜面.チップヒット = ( chip ) => {
80                                 this._回転羽.発火する( chip.チップ種別 );
81                                 if( this._Autoチップのドラム音を再生する )
82                                         this._ドラムサウンド.発声する( chip.チップ種別, chip.音量 * 0.25f );
83                         };
84                         this._スクロール譜面.ステージクリア = () => {
85                                 this.現在のフェーズ.Value = フェーズ.クリアor失敗;
86                         };
87                         this._スクロール譜面.リアルタイム演奏時刻sec = () => {
88                                 return this._現在の演奏時刻secを返す();
89                         };
90                         this._スクロール譜面.譜面スクロール速度の倍率 = () => {
91                                 return this._現在進行描画中の譜面スクロール速度の倍率.Value;
92                         };
93                         this._スクロール譜面.FPSをカウント = () => {
94                                 this._FPS.FPSをカウントしプロパティを更新する();
95                         };
96                 }
97
98                 protected override void On活性化( デバイスリソース dr )
99                 {
100                         FDK.Log.Info( "演奏ステージを開始します。" + ( StrokeStyleT.ビュアーモードである ? "(ViewerMode)" : "" ) );
101
102                         #region " 動画ファイルパスが有効なら、背景動画とBGMを初期化する。"
103                         //----------------
104                         if( ( null != StrokeStyleT.演奏スコア ) && ( StrokeStyleT.演奏スコア.背景動画ファイル名.Nullでも空でもない() ) )
105                         {
106                                 // 動画を子リストに追加。
107                                 this.子リスト.Add( this._背景動画 = new 動画( StrokeStyleT.演奏スコア.背景動画ファイル名, StrokeStyleT.Config.動画デコーダのキューサイズ ) );
108
109                                 // 動画から音声パートを抽出して BGM を作成。
110                                 if( ( null != this._デコード済みWaveSource ) && this._デコード済みWaveSource.Path.Equals( StrokeStyleT.演奏スコア.背景動画ファイル名 ) )
111                                 {
112                                         // (A) 前回生成したBGMとパスが同じなので、前回のデコード済み WaveSource をキャッシュとして再利用する。
113                                         FDK.Log.Info( "前回生成したサウンドデータを再利用します。" );
114                                 }
115                                 else
116                                 {
117                                         // (B) 初めての生成か、または前回生成したBGMとパスが違うので、新しくデコード済み WaveSource を生成する。
118                                         this._デコード済みWaveSource?.Dispose();
119                                         this._デコード済みWaveSource = new FDK.メディア.サウンド.WASAPI.DecodedWaveSource(
120                                                 StrokeStyleT.演奏スコア.背景動画ファイル名,
121                                                 StrokeStyleT.サウンドデバイス.WaveFormat );
122                                 }
123                                 this._BGM?.Dispose();
124                                 this._BGM = StrokeStyleT.サウンドデバイス.CreateSound( this._デコード済みWaveSource );
125                         }
126                         else
127                         {
128                                 this._背景動画 = null;
129                                 this._BGM = null;
130                                 //this._デコード済みWaveSource = null;    キャッシュは消さない。
131                         }
132                         //----------------
133                         #endregion
134
135                         this._演奏開始時刻sec = 0.0;
136                         this._現在進行描画中の譜面スクロール速度の倍率.Value = StrokeStyleT.ユーザ管理.現在選択されているユーザ.譜面スクロール速度の倍率;
137                         this._BGM再生開始済み = false;
138                         this._背景動画開始済み.Value = false;
139                         this._レーンフレーム.左端位置dpx = 400f;
140                         this._レーンフレーム.高さdpx = dr.設計画面サイズdpx.Height;
141
142                         this.ヒットした回数.Clear();
143                         foreach( var 判定 in typeof( ヒット判定種別 ).GetEnumValues() )
144                                 this.ヒットした回数[ (ヒット判定種別) 判定 ] = 0;
145
146                         #region " 最初のフェーズを設定する。"
147                         //----------------
148                         if( StrokeStyleT.ビュアーモードである )
149                         {
150                                 // 演奏スコアが設定済みなら演奏開始。それ以外ならメッセージ待機へ。
151                                 this.現在のフェーズ.Value = ( null != StrokeStyleT.演奏スコア ) ? フェーズ.演奏中 : フェーズ.ビュアーメッセージ待機中;
152                         }
153                         else
154                         {
155                                 // 演奏開始。
156                                 this.現在のフェーズ.Value = フェーズ.演奏中;
157                         }
158                         //----------------
159                         #endregion
160
161                         this._活性化した直後である = true;
162                 }
163
164                 protected override void On非活性化( デバイスリソース dr )
165                 {
166                         FDK.Log.Info( "演奏ステージを終了します。" );
167
168                         //this.BGMを解放する(); → ここではまだ解放しない。結果ステージに非活性化時に、外部から解放する。
169                         //FDK.Utilities.解放する( ref this._DecodedWaveSource );    → 演奏ステージインスタンスの破棄時に、外部から解放する。
170
171                         if( null != this._背景動画 )
172                                 this.子リスト.Remove( this._背景動画 ); // 子リストから削除
173                 }
174
175                 public override void 進行描画する( デバイスリソース dr )
176                 {
177                         // 進行描画。
178
179                         #region " 初めての進行描画。"
180                         //----------------
181                         if( this._活性化した直後である )
182                         {
183                                 this._活性化した直後である = false;
184                                 this._FPS = new FDK.カウンタ.FPS();
185
186                                 double 演奏開始位置の先頭からの時間sec = 0.0;
187
188                                 // ビュアーメッセージがある場合。
189                                 var msg = StrokeStyleT.最後に取得したビュアーメッセージ;
190                                 if( null != msg )
191                                 {
192                                         FDK.Log.Info( msg.ToString() );
193
194                                         演奏開始位置の先頭からの時間sec = this._スクロール譜面.演奏開始小節番号を設定しその時刻secを返す( msg.演奏開始小節番号 );
195                                         FDK.Log.Info( $"演奏開始の先頭からの時間sec: {演奏開始位置の先頭からの時間sec}" );
196
197                                         this._Autoチップのドラム音を再生する = msg.ドラムチップ発声;
198                                 }
199
200                                 // 演奏開始時刻sec の設定(1)
201                                 this._演奏開始時刻sec = StrokeStyleT.サウンドデバイス.GetDevicePosition() - 演奏開始位置の先頭からの時間sec;
202                                 FDK.Log.Info( $"演奏開始時刻sec(背景動画再生チェック前): {this._演奏開始時刻sec}" );
203
204                                 this._スクロール譜面.再生中の時刻なら動画とBGMを再生開始する( 演奏開始位置の先頭からの時間sec );
205
206                                 // 演奏開始時刻sec の設定(2) 動画とBGMが再生された場合の誤差を修正する。
207                                 this._演奏開始時刻sec = StrokeStyleT.サウンドデバイス.GetDevicePosition() - 演奏開始位置の先頭からの時間sec;
208                                 FDK.Log.Info( $"演奏開始時刻sec(背景動画再生チェック後): {this._演奏開始時刻sec}" );
209                         }
210                         //----------------
211                         #endregion
212
213                         double 演奏時刻sec = this._現在の演奏時刻secを返す();
214
215                         #region " 譜面スクロール速度が変化している場合の追い付き進行。"
216                         //----------------
217                         double 倍率 = this._現在進行描画中の譜面スクロール速度の倍率.Value;
218                         if( 倍率 < StrokeStyleT.ユーザ管理.現在選択されているユーザ.譜面スクロール速度の倍率 )
219                         {
220                                 // todo: 時間間隔に関係なく進行描画ごとに倍率を変えてしまっているので、あとからVPSに依存しないよう修正すること。
221                                 this._現在進行描画中の譜面スクロール速度の倍率.Value =
222                                         Math.Min( 倍率 + 0.05, StrokeStyleT.ユーザ管理.現在選択されているユーザ.譜面スクロール速度の倍率 );
223                         }
224                         else if( 倍率 > StrokeStyleT.ユーザ管理.現在選択されているユーザ.譜面スクロール速度の倍率 )
225                         {
226                                 // todo: 同上。
227                                 this._現在進行描画中の譜面スクロール速度の倍率.Value =
228                                         Math.Max( 倍率 - 0.05, StrokeStyleT.ユーザ管理.現在選択されているユーザ.譜面スクロール速度の倍率 );
229                         }
230                         //----------------
231                         #endregion
232
233                         #region " 背景動画とBGMの開始と進行描画。"
234                         //----------------
235                         if( this._背景動画開始済み.Value )
236                         {
237                                 float width = dr.設計画面サイズdpx.Width;
238                                 float height = dr.設計画面サイズdpx.Height;
239
240                                 // 背景動画チップがヒット済みなら、背景動画の進行描画を行う。
241                                 if( StrokeStyleT.ユーザ管理.現在選択されているユーザ.動画の縮小表示 )
242                                 {
243                                         this._背景動画?.進行描画する( dr, new SharpDX.RectangleF( 0f, 0f, width, height ), 0.2f );  // 全体
244
245                                         float 拡大縮小率 = 0.75f;
246                                         float 上移動dpx = 100.0f;
247
248                                         this._背景動画?.進行描画する( dr, new SharpDX.RectangleF(
249                                                 width * ( 1f - 拡大縮小率 ) / 2f,
250                                                 height * ( 1f - 拡大縮小率 ) / 2f - 上移動dpx,
251                                                 width * 拡大縮小率,
252                                                 height * 拡大縮小率 ) );
253                                 }
254                                 else
255                                 {
256                                         this._背景動画?.進行描画する( dr, new SharpDX.RectangleF( 0f, 0f, width, height ), 1.0f );
257                                 }
258
259                                 // 動画が重たいかもしれないので、演奏時刻をここで再度取得する。
260                                 演奏時刻sec = this._現在の演奏時刻secを返す();
261
262                                 // 背景動画が再生されているのにBGMがまだ再生されていないなら、再生を開始する。
263                                 if( false == this._BGM再生開始済み )
264                                 {
265                                         this._BGM?.Play();
266                                         this._BGM再生開始済み = true;
267                                 }
268                         }
269                         //----------------
270                         #endregion
271
272                         this._ステージ台.描画する( dr, 0.0f, 0.0f );
273                         this._レーンフレーム.進行描画する( dr );
274                         this._コンボ.進行描画する( dr );
275                         this._ヒット判定文字列.進行描画する( dr );
276                         this._スクロール譜面.小節線拍線を進行描画する( dr, 演奏時刻sec );
277                         this._判定バー.描画する( dr, 597f, 座標.判定バーの中央Y座標dpx - 43f );
278                         this._ドラムセット.進行描画する( dr );
279                         this._スクロール譜面.チップを進行描画する( dr, 演奏時刻sec );
280                         this._回転羽.進行描画する( dr );
281                         this._FPS.VPSをカウントする();
282                         this._FPS画像.表示文字列 = $"VPS: {this._FPS.現在のVPS.ToString()} / FPS: {this._FPS.現在のFPS.ToString()}";
283                         this._FPS画像.進行描画する( dr, 0f, 0f );
284
285                         // 入力。
286
287                         StrokeStyleT.すべての入力デバイスをポーリングする();
288
289                         // ESC 押下 → キャンセル
290                         if( StrokeStyleT.キーボード入力.キーが押された( SharpDX.DirectInput.Key.Escape ) &&
291                                 StrokeStyleT.ビュアーモードではない )  // ビュアーモードでは無効。
292                         {
293                                 this.BGMを停止する();
294                                 this.現在のフェーズ.Value = フェーズ.キャンセル;
295                         }
296                         // 上矢印押下 → 譜面スクロール速度の倍率を +0.5
297                         else if( StrokeStyleT.キーボード入力.キーが押された( SharpDX.DirectInput.Key.Up ) )
298                         {
299                                 StrokeStyleT.ユーザ管理.現在選択されているユーザ.譜面スクロール速度の倍率 =
300                                         Math.Min( StrokeStyleT.ユーザ管理.現在選択されているユーザ.譜面スクロール速度の倍率 + 0.5, 8.0 );    // 最大 8.0
301                         }
302                         // 下矢印押下 → 譜面スクロール速度の倍率を -0.5
303                         else if( StrokeStyleT.キーボード入力.キーが押された( SharpDX.DirectInput.Key.Down ) )
304                         {
305                                 StrokeStyleT.ユーザ管理.現在選択されているユーザ.譜面スクロール速度の倍率 =
306                                         Math.Max( StrokeStyleT.ユーザ管理.現在選択されているユーザ.譜面スクロール速度の倍率 - 0.5, 0.5 );    // 最小 0.5
307                         }
308                 }
309
310                 public void 演奏を停止する()
311                 {
312                         this._スクロール譜面?.演奏を停止する();
313
314                         this._背景動画開始済み.Value = false;
315
316                         this.BGMを停止する();
317
318                         this._コンボ.COMBO値.Value = 0;
319                 }
320
321                 public void BGMを停止する()
322                 {
323                         if( null != this._BGM )  // 背景動画がなければ BGM も null であり、それはエラーではない。
324                         {
325                                 this._BGM.Stop();
326                                 FDK.Utilities.解放する( ref this._BGM );
327                                 //FDK.Utilities.解放する( ref this._デコード済みWaveSource ); → ここでは解放しない。
328                         }
329                 }
330
331                 public void BGMのキャッシュを解放する()
332                 {
333                         this.BGMを停止する();
334
335                         FDK.Utilities.解放する( ref this._デコード済みWaveSource );
336                 }
337
338                 private bool _活性化した直後である = false;
339
340                 private double _演奏開始時刻sec = 0.0;
341
342                 private bool _Autoチップのドラム音を再生する = true;
343
344                 private readonly SST.ステージ.演奏.コンボ _コンボ;
345
346                 private readonly SST.ステージ.演奏.レーンフレーム _レーンフレーム;
347
348                 private readonly SST.ステージ.演奏.スクロール譜面 _スクロール譜面;
349
350                 private readonly SST.ステージ.演奏.ヒット判定文字列 _ヒット判定文字列;
351
352                 private readonly SST.ステージ.演奏.回転羽 _回転羽 = new 回転羽( 32 );
353
354                 private readonly SST.ステージ.演奏.ドラムサウンド _ドラムサウンド;
355
356                 private readonly SST.ステージ.ドラムセット _ドラムセット;
357
358                 private readonly FDK.メディア.画像 _判定バー;
359
360                 private readonly FDK.メディア.画像 _ステージ台;
361
362                 private readonly FDK.メディア.文字列画像 _FPS画像;
363
364                 private FDK.同期.RWLock<bool> _背景動画開始済み = new FDK.同期.RWLock<bool>( false );
365
366                 private bool _BGM再生開始済み = false;
367
368                 private FDK.同期.RWLock<double> _現在進行描画中の譜面スクロール速度の倍率 = new FDK.同期.RWLock<double>( 0.0 );
369
370                 /// <remarks>
371                 ///             停止と解放は、演奏ステージクラスの非活性化後に、外部から行われる。
372                 ///             <see cref="SST.ステージ.演奏.演奏ステージ.BGMを停止する"/>
373                 ///             <see cref="SST.ステージ.演奏.演奏ステージ.BGMのキャッシュを解放する"/>
374                 /// </remarks>
375                 private FDK.メディア.サウンド.WASAPI.Sound _BGM = null;
376                 
377                 /// <summary>
378                 ///             BGM の生成もとになるデコード済みサウンドデータ。
379                 ///     </summary>
380                 ///     <remarks>
381                 ///             活性化と非活性化に関係なく、常に最後にデコードしたデータを持つ。(キャッシュ)
382                 ///             演奏ステージインスタンスを破棄する際に、このインスタンスもDisposeすること。
383                 /// </remarks>
384                 private FDK.メディア.サウンド.WASAPI.DecodedWaveSource _デコード済みWaveSource = null;
385
386                 private FDK.メディア.動画 _背景動画 = null;
387
388                 private FDK.カウンタ.FPS _FPS = null;
389
390                 private double _現在の演奏時刻secを返す()
391                 {
392                         return StrokeStyleT.サウンドデバイス.GetDevicePosition() - this._演奏開始時刻sec;
393                 }
394         }
395 }