OSDN Git Service

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