diff --git a/.gitmodules b/.gitmodules index 630b550732b..ffd25d161ed 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,6 +7,3 @@ url=https://github.com/MrHelmut/NVorbis.git [submodule "ThirdParty/SDL_GameControllerDB"] path = ThirdParty/SDL_GameControllerDB url = https://github.com/gabomdq/SDL_GameControllerDB.git -[submodule "MonoGame.Framework"] - path = MonoGame.Framework - url = https://github.com/wihu/MonoGame.Framework.git diff --git a/MonoGame.Framework b/MonoGame.Framework deleted file mode 160000 index 73e51e2de7a..00000000000 --- a/MonoGame.Framework +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 73e51e2de7a8211bf10e86f3df3c14d826873884 diff --git a/MonoGame.Framework/Android/AndroidCompatibility.cs b/MonoGame.Framework/Android/AndroidCompatibility.cs new file mode 100644 index 00000000000..28d9a537ca1 --- /dev/null +++ b/MonoGame.Framework/Android/AndroidCompatibility.cs @@ -0,0 +1,110 @@ +using System; +using System.Linq; +using Android.App; +using Android.Content.Res; +using Android.OS; +using Android.Views; + +namespace Microsoft.Xna.Framework +{ + /// + /// Properties that change from how XNA works by default + /// + public static class AndroidCompatibility + { + /// + /// Because the Kindle Fire devices default orientation is fliped by 180 degrees from all the other android devices + /// on the market we need to do some special processing to make sure that LandscapeLeft is the correct way round. + /// This list contains all the Build.Model strings of the effected devices, it should be added to if and when + /// more devices exhibit the same issues. + /// + private static readonly string[] Kindles = new[] { "KFTT", "KFJWI", "KFJWA", "KFSOWI", "KFTHWA", "KFTHWI", "KFAPWA", "KFAPWI" }; + + public static bool FlipLandscape { get; private set; } + public static Lazy NaturalOrientation { get; private set; } + + static AndroidCompatibility() + { + FlipLandscape = Kindles.Contains(Build.Model); + NaturalOrientation = new Lazy(GetDeviceNaturalOrientation); + } + + private static Orientation GetDeviceNaturalOrientation() + { + var orientation = Game.Activity.Resources.Configuration.Orientation; + SurfaceOrientation rotation = Game.Activity.WindowManager.DefaultDisplay.Rotation; + + if (((rotation == SurfaceOrientation.Rotation0 || rotation == SurfaceOrientation.Rotation180) && + orientation == Orientation.Landscape) + || ((rotation == SurfaceOrientation.Rotation90 || rotation == SurfaceOrientation.Rotation270) && + orientation == Orientation.Portrait)) + { + return Orientation.Landscape; + } + else + { + return Orientation.Portrait; + } + } + + internal static DisplayOrientation GetAbsoluteOrientation(int orientation) + { + // Orientation is reported by the device in degrees compared to the natural orientation + // Some tablets have a natural landscape orientation, which we need to account for + if (NaturalOrientation.Value == Orientation.Landscape) + orientation += 270; + + // Round orientation into one of 4 positions, either 0, 90, 180, 270. + int ort = ((orientation + 45) / 90 * 90) % 360; + + // Surprisingly 90 degree is landscape right, except on Kindle devices + var disporientation = DisplayOrientation.Unknown; + switch (ort) + { + case 90: disporientation = FlipLandscape ? DisplayOrientation.LandscapeLeft : DisplayOrientation.LandscapeRight; + break; + case 270: disporientation = FlipLandscape ? DisplayOrientation.LandscapeRight : DisplayOrientation.LandscapeLeft; + break; + case 0: disporientation = DisplayOrientation.Portrait; + break; + case 180: disporientation = DisplayOrientation.PortraitDown; + break; + default: + disporientation = DisplayOrientation.LandscapeLeft; + break; + } + + return disporientation; + } + + /// + /// Get the absolute orientation of the device, accounting for platform differences. + /// + /// + public static DisplayOrientation GetAbsoluteOrientation() + { + var orientation = Game.Activity.WindowManager.DefaultDisplay.Rotation; + + // Landscape degrees (provided by the OrientationListener) are swapped by default + // Since we use the code used by OrientationListener, we have to swap manually + int degrees; + switch (orientation) + { + case SurfaceOrientation.Rotation90: + degrees = 270; + break; + case SurfaceOrientation.Rotation180: + degrees = 180; + break; + case SurfaceOrientation.Rotation270: + degrees = 90; + break; + default: + degrees = 0; + break; + } + + return GetAbsoluteOrientation(degrees); + } + } +} diff --git a/MonoGame.Framework/Android/AndroidGameActivity.cs b/MonoGame.Framework/Android/AndroidGameActivity.cs new file mode 100644 index 00000000000..8a13e04c44f --- /dev/null +++ b/MonoGame.Framework/Android/AndroidGameActivity.cs @@ -0,0 +1,110 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using Android.App; +using Android.Content; +using Android.OS; +using Android.Views; + +namespace Microsoft.Xna.Framework +{ + [CLSCompliant(false)] + public class AndroidGameActivity : Activity + { + internal Game Game { private get; set; } + + private ScreenReceiver screenReceiver; + private OrientationListener _orientationListener; + + public bool AutoPauseAndResumeMediaPlayer = true; + public bool RenderOnUIThread = true; + + /// + /// OnCreate called when the activity is launched from cold or after the app + /// has been killed due to a higher priority app needing the memory + /// + /// + /// Saved instance state. + /// + protected override void OnCreate (Bundle savedInstanceState) + { + RequestWindowFeature(WindowFeatures.NoTitle); + base.OnCreate(savedInstanceState); + + IntentFilter filter = new IntentFilter(); + filter.AddAction(Intent.ActionScreenOff); + filter.AddAction(Intent.ActionScreenOn); + filter.AddAction(Intent.ActionUserPresent); + + screenReceiver = new ScreenReceiver(); + RegisterReceiver(screenReceiver, filter); + + _orientationListener = new OrientationListener(this); + + Game.Activity = this; + } + + public static event EventHandler Paused; + + public override void OnConfigurationChanged (Android.Content.Res.Configuration newConfig) + { + // we need to refresh the viewport here. + base.OnConfigurationChanged (newConfig); + } + + protected override void OnPause() + { + base.OnPause(); + EventHelpers.Raise(this, Paused, EventArgs.Empty); + + if (_orientationListener.CanDetectOrientation()) + _orientationListener.Disable(); + } + + public static event EventHandler Resumed; + protected override void OnResume() + { + base.OnResume(); + EventHelpers.Raise(this, Resumed, EventArgs.Empty); + + if (Game != null) + { + var deviceManager = (IGraphicsDeviceManager)Game.Services.GetService(typeof(IGraphicsDeviceManager)); + if (deviceManager == null) + return; + ((GraphicsDeviceManager)deviceManager).ForceSetFullScreen(); + ((AndroidGameWindow)Game.Window).GameView.RequestFocus(); + if (_orientationListener.CanDetectOrientation()) + _orientationListener.Enable(); + } + } + + protected override void OnDestroy () + { + UnregisterReceiver(screenReceiver); + ScreenReceiver.ScreenLocked = false; + _orientationListener = null; + if (Game != null) + Game.Dispose(); + Game = null; + base.OnDestroy (); + } + } + + [CLSCompliant(false)] + public static class ActivityExtensions + { + public static ActivityAttribute GetActivityAttribute(this AndroidGameActivity obj) + { + var attr = obj.GetType().GetCustomAttributes(typeof(ActivityAttribute), true); + if (attr != null) + { + return ((ActivityAttribute)attr[0]); + } + return null; + } + } + +} diff --git a/MonoGame.Framework/Android/AndroidGamePlatform.cs b/MonoGame.Framework/Android/AndroidGamePlatform.cs new file mode 100644 index 00000000000..2f078154cf0 --- /dev/null +++ b/MonoGame.Framework/Android/AndroidGamePlatform.cs @@ -0,0 +1,175 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using Android.Views; +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Media; + +namespace Microsoft.Xna.Framework +{ + class AndroidGamePlatform : GamePlatform + { + public AndroidGamePlatform(Game game) + : base(game) + { + System.Diagnostics.Debug.Assert(Game.Activity != null, "Must set Game.Activity before creating the Game instance"); + Game.Activity.Game = Game; + AndroidGameActivity.Paused += Activity_Paused; + AndroidGameActivity.Resumed += Activity_Resumed; + + _gameWindow = new AndroidGameWindow(Game.Activity, game); + Window = _gameWindow; + + MediaLibrary.Context = Game.Activity; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + AndroidGameActivity.Paused -= Activity_Paused; + AndroidGameActivity.Resumed -= Activity_Resumed; + } + base.Dispose(disposing); + } + + private bool _initialized; + public static bool IsPlayingVdeo { get; set; } + private AndroidGameWindow _gameWindow; + + public override void Exit() + { + Game.Activity.MoveTaskToBack(true); + } + + public override void RunLoop() + { + throw new NotSupportedException("The Android platform does not support synchronous run loops"); + } + + public override void StartRunLoop() + { + _gameWindow.GameView.Resume(); + } + + public override bool BeforeUpdate(GameTime gameTime) + { + if (!_initialized) + { + Game.DoInitialize(); + _initialized = true; + } + + return true; + } + + public override bool BeforeDraw(GameTime gameTime) + { + PrimaryThreadLoader.DoLoads(); + return !IsPlayingVdeo; + } + + public override void BeforeInitialize() + { + var currentOrientation = AndroidCompatibility.GetAbsoluteOrientation(); + + switch (Game.Activity.Resources.Configuration.Orientation) + { + case Android.Content.Res.Orientation.Portrait: + this._gameWindow.SetOrientation(currentOrientation == DisplayOrientation.PortraitDown ? DisplayOrientation.PortraitDown : DisplayOrientation.Portrait, false); + break; + default: + this._gameWindow.SetOrientation(currentOrientation == DisplayOrientation.LandscapeRight ? DisplayOrientation.LandscapeRight : DisplayOrientation.LandscapeLeft, false); + break; + } + base.BeforeInitialize(); + _gameWindow.GameView.TouchEnabled = true; + } + + public override bool BeforeRun() + { + + // Run it as fast as we can to allow for more response on threaded GPU resource creation + _gameWindow.GameView.Run(); + + return false; + } + + public override void EnterFullScreen() + { + } + + public override void ExitFullScreen() + { + } + + public override void BeginScreenDeviceChange(bool willBeFullScreen) + { + } + + public override void EndScreenDeviceChange(string screenDeviceName, int clientWidth, int clientHeight) + { + // Force the Viewport to be correctly set + Game.graphicsDeviceManager.ResetClientBounds(); + } + + // EnterForeground + void Activity_Resumed(object sender, EventArgs e) + { + if (!IsActive) + { + IsActive = true; + _gameWindow.GameView.Resume(); + if (_MediaPlayer_PrevState == MediaState.Playing && Game.Activity.AutoPauseAndResumeMediaPlayer) + MediaPlayer.Resume(); + if (!_gameWindow.GameView.IsFocused) + _gameWindow.GameView.RequestFocus(); + } + } + + MediaState _MediaPlayer_PrevState = MediaState.Stopped; + // EnterBackground + void Activity_Paused(object sender, EventArgs e) + { + if (IsActive) + { + IsActive = false; + _MediaPlayer_PrevState = MediaPlayer.State; + _gameWindow.GameView.Pause(); + _gameWindow.GameView.ClearFocus(); + if (Game.Activity.AutoPauseAndResumeMediaPlayer) + MediaPlayer.Pause(); + } + } + + public override GameRunBehavior DefaultRunBehavior + { + get { return GameRunBehavior.Asynchronous; } + } + + public override void Log(string Message) + { +#if LOGGING + Android.Util.Log.Debug("MonoGameDebug", Message); +#endif + } + + public override void Present() + { + try + { + var device = Game.GraphicsDevice; + if (device != null) + device.Present(); + + _gameWindow.GameView.SwapBuffers(); + } + catch (Exception ex) + { + Android.Util.Log.Error("Error in swap buffers", ex.ToString()); + } + } + } +} diff --git a/MonoGame.Framework/Android/AndroidGameWindow.cs b/MonoGame.Framework/Android/AndroidGameWindow.cs new file mode 100644 index 00000000000..751ffcf73c4 --- /dev/null +++ b/MonoGame.Framework/Android/AndroidGameWindow.cs @@ -0,0 +1,333 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using Android.Content; +using Android.Content.PM; +using Android.OS; +using Android.Views; +using Microsoft.Xna.Framework.Input.Touch; +using MonoGame.OpenGL; + +namespace Microsoft.Xna.Framework +{ + [CLSCompliant(false)] + public class AndroidGameWindow : GameWindow, IDisposable + { + internal MonoGameAndroidGameView GameView { get; private set; } + internal IResumeManager Resumer; + + private readonly Game _game; + private Rectangle _clientBounds; + private DisplayOrientation _supportedOrientations = DisplayOrientation.Default; + private DisplayOrientation _currentOrientation; + + public override IntPtr Handle { get { return IntPtr.Zero; } } + + + public void SetResumer(IResumeManager resumer) + { + Resumer = resumer; + } + + public AndroidGameWindow(AndroidGameActivity activity, Game game) + { + _game = game; + + Point size; + if (Build.VERSION.SdkInt < BuildVersionCodes.JellyBean) + { + size.X = activity.Resources.DisplayMetrics.WidthPixels; + size.Y = activity.Resources.DisplayMetrics.HeightPixels; + } + else + { + Android.Graphics.Point p = new Android.Graphics.Point(); + activity.WindowManager.DefaultDisplay.GetRealSize(p); + size.X = p.X; + size.Y = p.Y; + } + + Initialize(activity, size); + + game.Services.AddService(typeof(View), GameView); + } + + private void Initialize(Context context, Point size) + { + _clientBounds = new Rectangle(0, 0, size.X, size.Y); + + GameView = new MonoGameAndroidGameView(context, this, _game); + GameView.RenderOnUIThread = Game.Activity.RenderOnUIThread; + GameView.RenderFrame += OnRenderFrame; + GameView.UpdateFrame += OnUpdateFrame; + + GameView.RequestFocus(); + GameView.FocusableInTouchMode = true; + } + + #region AndroidGameView Methods + + private void OnRenderFrame(object sender, MonoGameAndroidGameView.FrameEventArgs frameEventArgs) + { + GameView.MakeCurrent(); + + Threading.Run(); + } + + private void OnUpdateFrame(object sender, MonoGameAndroidGameView.FrameEventArgs frameEventArgs) + { + GameView.MakeCurrent(); + + Threading.Run(); + + if (_game != null) + { + if (!GameView.IsResuming && _game.Platform.IsActive && !ScreenReceiver.ScreenLocked) //Only call draw if an update has occured + { + _game.Tick(); + } + else if (_game.GraphicsDevice != null) + { + _game.GraphicsDevice.Clear(Color.Black); + if (GameView.IsResuming && Resumer != null) + { + Resumer.Draw(); + } + _game.Platform.Present(); + } + } + } + + #endregion + + + protected internal override void SetSupportedOrientations(DisplayOrientation orientations) + { + _supportedOrientations = orientations; + } + + /// + /// In Xna, setting SupportedOrientations = DisplayOrientation.Default (which is the default value) + /// has the effect of setting SupportedOrientations to landscape only or portrait only, based on the + /// aspect ratio of PreferredBackBufferWidth / PreferredBackBufferHeight + /// + /// + internal DisplayOrientation GetEffectiveSupportedOrientations() + { + if (_supportedOrientations == DisplayOrientation.Default) + { + var deviceManager = (_game.Services.GetService(typeof(IGraphicsDeviceManager)) as GraphicsDeviceManager); + if (deviceManager == null) + return DisplayOrientation.LandscapeLeft | DisplayOrientation.LandscapeRight; + + if (deviceManager.PreferredBackBufferWidth > deviceManager.PreferredBackBufferHeight) + { + return DisplayOrientation.LandscapeLeft | DisplayOrientation.LandscapeRight; + } + else + { + return DisplayOrientation.Portrait | DisplayOrientation.PortraitDown; + } + } + else + { + return _supportedOrientations; + } + } + + /// + /// Updates the screen orientation. Filters out requests for unsupported orientations. + /// + internal void SetOrientation(DisplayOrientation newOrientation, bool applyGraphicsChanges) + { + DisplayOrientation supported = GetEffectiveSupportedOrientations(); + + // If the new orientation is not supported, force a supported orientation + if ((supported & newOrientation) == 0) + { + if ((supported & DisplayOrientation.LandscapeLeft) != 0) + newOrientation = DisplayOrientation.LandscapeLeft; + else if ((supported & DisplayOrientation.LandscapeRight) != 0) + newOrientation = DisplayOrientation.LandscapeRight; + else if ((supported & DisplayOrientation.Portrait) != 0) + newOrientation = DisplayOrientation.Portrait; + else if ((supported & DisplayOrientation.PortraitDown) != 0) + newOrientation = DisplayOrientation.PortraitDown; + } + + DisplayOrientation oldOrientation = CurrentOrientation; + + SetDisplayOrientation(newOrientation); + TouchPanel.DisplayOrientation = newOrientation; + + if (applyGraphicsChanges && oldOrientation != CurrentOrientation && _game.graphicsDeviceManager != null) + _game.graphicsDeviceManager.ApplyChanges(); + } + + public override string ScreenDeviceName + { + get + { + throw new NotImplementedException(); + } + } + + + public override Rectangle ClientBounds + { + get + { + return _clientBounds; + } + } + + internal void ChangeClientBounds(Rectangle bounds) + { + if (bounds != _clientBounds) + { + _clientBounds = bounds; + OnClientSizeChanged(); + } + } + + public override bool AllowUserResizing + { + get + { + return false; + } + set + { + // Do nothing; Ignore rather than raising an exception + } + } + + // A copy of ScreenOrientation from Android 2.3 + // This allows us to continue to support 2.2 whilst + // utilising the 2.3 improved orientation support. + enum ScreenOrientationAll + { + Unspecified = -1, + Landscape = 0, + Portrait = 1, + User = 2, + Behind = 3, + Sensor = 4, + Nosensor = 5, + SensorLandscape = 6, + SensorPortrait = 7, + ReverseLandscape = 8, + ReversePortrait = 9, + FullSensor = 10, + } + + public override DisplayOrientation CurrentOrientation + { + get + { + return _currentOrientation; + } + } + + + private void SetDisplayOrientation(DisplayOrientation value) + { + if (value != _currentOrientation) + { + DisplayOrientation supported = GetEffectiveSupportedOrientations(); + ScreenOrientation requestedOrientation = ScreenOrientation.Unspecified; + bool wasPortrait = _currentOrientation == DisplayOrientation.Portrait || _currentOrientation == DisplayOrientation.PortraitDown; + bool requestPortrait = false; + + bool didOrientationChange = false; + // Android 2.3 and above support reverse orientations + int sdkVer = (int)Android.OS.Build.VERSION.SdkInt; + if (sdkVer >= 10) + { + // Check if the requested orientation is supported. Default means all are supported. + if ((supported & value) != 0) + { + didOrientationChange = true; + _currentOrientation = value; + switch (value) + { + case DisplayOrientation.LandscapeLeft: + requestedOrientation = (ScreenOrientation)ScreenOrientationAll.Landscape; + requestPortrait = false; + break; + case DisplayOrientation.LandscapeRight: + requestedOrientation = (ScreenOrientation)ScreenOrientationAll.ReverseLandscape; + requestPortrait = false; + break; + case DisplayOrientation.Portrait: + requestedOrientation = (ScreenOrientation)ScreenOrientationAll.Portrait; + requestPortrait = true; + break; + case DisplayOrientation.PortraitDown: + requestedOrientation = (ScreenOrientation)ScreenOrientationAll.ReversePortrait; + requestPortrait = true; + break; + } + } + } + else + { + // Check if the requested orientation is either of the landscape orientations and any landscape orientation is supported. + if ((value == DisplayOrientation.LandscapeLeft || value == DisplayOrientation.LandscapeRight) && + ((supported & (DisplayOrientation.LandscapeLeft | DisplayOrientation.LandscapeRight)) != 0)) + { + didOrientationChange = true; + _currentOrientation = DisplayOrientation.LandscapeLeft; + requestedOrientation = ScreenOrientation.Landscape; + requestPortrait = false; + } + // Check if the requested orientation is either of the portrain orientations and any portrait orientation is supported. + else if ((value == DisplayOrientation.Portrait || value == DisplayOrientation.PortraitDown) && + ((supported & (DisplayOrientation.Portrait | DisplayOrientation.PortraitDown)) != 0)) + { + didOrientationChange = true; + _currentOrientation = DisplayOrientation.Portrait; + requestedOrientation = ScreenOrientation.Portrait; + requestPortrait = true; + } + } + + if (didOrientationChange) + { + // Android doesn't fire Released events for existing touches + // so we need to clear them out. + if (wasPortrait != requestPortrait) + { + TouchPanelState.ReleaseAllTouches(); + } + + OnOrientationChanged(); + } + } + } + + + public void Dispose() + { + if (GameView != null) + { + GameView.Dispose(); + GameView = null; + } + } + + public override void BeginScreenDeviceChange(bool willBeFullScreen) + { + } + + public override void EndScreenDeviceChange(string screenDeviceName, int clientWidth, int clientHeight) + { + } + + protected override void SetTitle(string title) + { + } + } +} diff --git a/MonoGame.Framework/Android/Devices/Sensors/Accelerometer.cs b/MonoGame.Framework/Android/Devices/Sensors/Accelerometer.cs new file mode 100644 index 00000000000..740043e53a9 --- /dev/null +++ b/MonoGame.Framework/Android/Devices/Sensors/Accelerometer.cs @@ -0,0 +1,208 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using Android.Content; +using Android.Hardware; +using Microsoft.Xna.Framework; + +namespace Microsoft.Devices.Sensors +{ + /// + /// Provides Android applications access to the device’s accelerometer sensor. + /// + public sealed class Accelerometer : SensorBase + { + static readonly int MaxSensorCount = 10; + static SensorManager sensorManager; + static Sensor sensor; + SensorListener listener; + SensorState state; + bool started = false; + static int instanceCount; + + /// + /// Gets or sets whether the device on which the application is running supports the accelerometer sensor. + /// + public static bool IsSupported + { + get + { + if (sensorManager == null) + Initialize(); + return sensor != null; + } + } + + /// + /// Gets the current state of the accelerometer. The value is a member of the SensorState enumeration. + /// + public SensorState State + { + get + { + if (IsDisposed) + throw new ObjectDisposedException(GetType().Name); + if (sensorManager == null) + { + Initialize(); + state = sensor != null ? SensorState.Initializing : SensorState.NotSupported; + } + return state; + } + } + + /// + /// Creates a new instance of the Accelerometer object. + /// + public Accelerometer() + { + if (instanceCount >= MaxSensorCount) + throw new SensorFailedException("The limit of 10 simultaneous instances of the Accelerometer class per application has been exceeded."); + ++instanceCount; + + state = sensor != null ? SensorState.Initializing : SensorState.NotSupported; + listener = new SensorListener(); + } + + /// + /// Initializes the platform resources required for the accelerometer sensor. + /// + static void Initialize() + { + sensorManager = (SensorManager)Game.Activity.GetSystemService(Context.SensorService); + sensor = sensorManager.GetDefaultSensor(SensorType.Accelerometer); + } + + void ActivityPaused(object sender, EventArgs eventArgs) + { + sensorManager.UnregisterListener(listener, sensor); + } + + void ActivityResumed(object sender, EventArgs eventArgs) + { + sensorManager.RegisterListener(listener, sensor, SensorDelay.Game); + } + + /// + /// Starts data acquisition from the accelerometer. + /// + public override void Start() + { + if (IsDisposed) + throw new ObjectDisposedException(GetType().Name); + if (sensorManager == null) + Initialize(); + if (started == false) + { + if (sensorManager != null && sensor != null) + { + listener.accelerometer = this; + sensorManager.RegisterListener(listener, sensor, SensorDelay.Game); + // So the system can pause and resume the sensor when the activity is paused + AndroidGameActivity.Paused += ActivityPaused; + AndroidGameActivity.Resumed += ActivityResumed; + } + else + { + throw new AccelerometerFailedException("Failed to start accelerometer data acquisition. No default sensor found.", -1); + } + started = true; + state = SensorState.Ready; + return; + } + else + { + throw new AccelerometerFailedException("Failed to start accelerometer data acquisition. Data acquisition already started.", -1); + } + } + + /// + /// Stops data acquisition from the accelerometer. + /// + public override void Stop() + { + if (IsDisposed) + throw new ObjectDisposedException(GetType().Name); + if (started) + { + if (sensorManager != null && sensor != null) + { + AndroidGameActivity.Paused -= ActivityPaused; + AndroidGameActivity.Resumed -= ActivityResumed; + sensorManager.UnregisterListener(listener, sensor); + listener.accelerometer = null; + } + } + started = false; + state = SensorState.Disabled; + } + + protected override void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + if (started) + Stop(); + --instanceCount; + if (instanceCount == 0) + { + sensor = null; + sensorManager = null; + } + } + } + base.Dispose(disposing); + } + + class SensorListener : Java.Lang.Object, ISensorEventListener + { + internal Accelerometer accelerometer; + + public void OnAccuracyChanged(Sensor sensor, SensorStatus accuracy) + { + //do nothing + } + + public void OnSensorChanged(SensorEvent e) + { + try + { + if (e != null && e.Sensor.Type == SensorType.Accelerometer && accelerometer != null) + { + var values = e.Values; + try + { + AccelerometerReading reading = new AccelerometerReading(); + accelerometer.IsDataValid = (values != null && values.Count == 3); + if (accelerometer.IsDataValid) + { + const float gravity = Android.Hardware.SensorManager.GravityEarth; + reading.Acceleration = new Vector3(values[0], values[1], values[2]) / gravity; + reading.Timestamp = DateTime.UtcNow; + } + accelerometer.CurrentValue = reading; + } + finally + { + IDisposable d = values as IDisposable; + if (d != null) + d.Dispose(); + } + } + } + catch (NullReferenceException) + { + //Occassionally an NullReferenceException is thrown when accessing e.Values?? + // mono : Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object + // mono : at Android.Runtime.JNIEnv.GetObjectField (IntPtr jobject, IntPtr jfieldID) [0x00000] in :0 + // mono : at Android.Hardware.SensorEvent.get_Values () [0x00000] in :0 + } + } + } + } +} + diff --git a/MonoGame.Framework/Android/Devices/Sensors/Compass.cs b/MonoGame.Framework/Android/Devices/Sensors/Compass.cs new file mode 100644 index 00000000000..8846603e96b --- /dev/null +++ b/MonoGame.Framework/Android/Devices/Sensors/Compass.cs @@ -0,0 +1,228 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using Android.Content; +using Android.Hardware; +using Microsoft.Xna.Framework; + +namespace Microsoft.Devices.Sensors +{ + /// + /// Provides Android applications access to the device’s compass sensor. + /// + public sealed class Compass : SensorBase + { + static readonly int MaxSensorCount = 10; + static SensorManager sensorManager; + static Sensor sensorMagneticField; + static Sensor sensorAccelerometer; + SensorListener listener; + SensorState state; + bool started = false; + static int instanceCount; + + /// + /// Gets whether the device on which the application is running supports the compass sensor. + /// + public static bool IsSupported + { + get + { + if (sensorManager == null) + Initialize(); + return sensorMagneticField != null; + } + } + + /// + /// Gets the current state of the compass. The value is a member of the SensorState enumeration. + /// + public SensorState State + { + get + { + if (IsDisposed) + throw new ObjectDisposedException(GetType().Name); + if (sensorManager == null) + Initialize(); + return state; + } + } + + /// + /// Creates a new instance of the Compass object. + /// + public Compass() + { + if (instanceCount >= MaxSensorCount) + throw new SensorFailedException("The limit of 10 simultaneous instances of the Compass class per application has been exceeded."); + ++instanceCount; + + state = sensorMagneticField != null ? SensorState.Initializing : SensorState.NotSupported; + listener = new SensorListener(); + } + + /// + /// Initializes the platform resources required for the compass sensor. + /// + static void Initialize() + { + sensorManager = (SensorManager)Game.Activity.GetSystemService(Context.SensorService); + sensorMagneticField = sensorManager.GetDefaultSensor(SensorType.MagneticField); + sensorAccelerometer = sensorManager.GetDefaultSensor(SensorType.Accelerometer); + } + + void ActivityPaused(object sender, EventArgs eventArgs) + { + sensorManager.UnregisterListener(listener, sensorMagneticField); + sensorManager.UnregisterListener(listener, sensorAccelerometer); + } + + void ActivityResumed(object sender, EventArgs eventArgs) + { + sensorManager.RegisterListener(listener, sensorAccelerometer, SensorDelay.Game); + sensorManager.RegisterListener(listener, sensorMagneticField, SensorDelay.Game); + } + + /// + /// Starts data acquisition from the compass. + /// + public override void Start() + { + if (IsDisposed) + throw new ObjectDisposedException(GetType().Name); + if (sensorManager == null) + Initialize(); + if (started == false) + { + if (sensorManager != null && sensorMagneticField != null && sensorAccelerometer != null) + { + listener.compass = this; + sensorManager.RegisterListener(listener, sensorMagneticField, SensorDelay.Game); + sensorManager.RegisterListener(listener, sensorAccelerometer, SensorDelay.Game); + } + else + { + throw new SensorFailedException("Failed to start compass data acquisition. No default sensor found."); + } + started = true; + state = SensorState.Ready; + return; + } + else + { + throw new SensorFailedException("Failed to start compass data acquisition. Data acquisition already started."); + } + } + + /// + /// Stops data acquisition from the accelerometer. + /// + public override void Stop() + { + if (IsDisposed) + throw new ObjectDisposedException(GetType().Name); + if (started) + { + if (sensorManager != null && sensorMagneticField != null && sensorAccelerometer != null) + { + sensorManager.UnregisterListener(listener, sensorAccelerometer); + sensorManager.UnregisterListener(listener, sensorMagneticField); + listener.compass = null; + } + } + started = false; + state = SensorState.Disabled; + } + + protected override void Dispose(bool disposing) + { + if (!IsDisposed) + { + if (disposing) + { + if (started) + Stop(); + --instanceCount; + if (instanceCount == 0) + { + sensorAccelerometer = null; + sensorMagneticField = null; + sensorManager = null; + } + } + } + base.Dispose(disposing); + } + + class SensorListener : Java.Lang.Object, ISensorEventListener + { + internal Compass compass; + float[] valuesAccelerometer; + float[] valuesMagenticField; + float[] matrixR; + float[] matrixI; + float[] matrixValues; + + public SensorListener() + { + valuesAccelerometer = new float[3]; + valuesMagenticField = new float[3]; + matrixR = new float[9]; + matrixI = new float[9]; + matrixValues = new float[3]; + } + + public void OnAccuracyChanged(Sensor sensor, SensorStatus accuracy) + { + //do nothing + } + + public void OnSensorChanged(SensorEvent e) + { + try + { + switch (e.Sensor.Type) + { + case SensorType.Accelerometer: + valuesAccelerometer[0] = e.Values[0]; + valuesAccelerometer[1] = e.Values[1]; + valuesAccelerometer[2] = e.Values[2]; + break; + + case SensorType.MagneticField: + valuesMagenticField[0] = e.Values[0]; + valuesMagenticField[1] = e.Values[1]; + valuesMagenticField[2] = e.Values[2]; + break; + } + + compass.IsDataValid = SensorManager.GetRotationMatrix(matrixR, matrixI, valuesAccelerometer, valuesMagenticField); + if (compass.IsDataValid) + { + SensorManager.GetOrientation(matrixR, matrixValues); + CompassReading reading = new CompassReading(); + reading.MagneticHeading = matrixValues[0]; + Vector3 magnetometer = new Vector3(valuesMagenticField[0], valuesMagenticField[1], valuesMagenticField[2]); + reading.MagnetometerReading = magnetometer; + // We need the magnetic declination from true north to calculate the true heading from the magnetic heading. + // On Android, this is available through Android.Hardware.GeomagneticField, but this requires your geo position. + reading.TrueHeading = reading.MagneticHeading; + reading.Timestamp = DateTime.UtcNow; + compass.CurrentValue = reading; + } + } + catch (NullReferenceException) + { + //Occassionally an NullReferenceException is thrown when accessing e.Values?? + // mono : Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object + // mono : at Android.Runtime.JNIEnv.GetObjectField (IntPtr jobject, IntPtr jfieldID) [0x00000] in :0 + // mono : at Android.Hardware.SensorEvent.get_Values () [0x00000] in :0 + } + } + } + } +} + diff --git a/MonoGame.Framework/Android/GamerServices/Guide.cs b/MonoGame.Framework/Android/GamerServices/Guide.cs new file mode 100644 index 00000000000..2ecb9983ac3 --- /dev/null +++ b/MonoGame.Framework/Android/GamerServices/Guide.cs @@ -0,0 +1,402 @@ +#region License +/* +Microsoft Public License (Ms-PL) +MonoGame - Copyright © 2009 The MonoGame Team + +All rights reserved. + +This license governs use of the accompanying software. If you use the software, you accept this license. If you do not +accept the license, do not use the software. + +1. Definitions +The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under +U.S. copyright law. + +A "contribution" is the original software, or any additions or changes to the software. +A "contributor" is any person that distributes its contribution under this license. +"Licensed patents" are a contributor's patent claims that read directly on its contribution. + +2. Grant of Rights +(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. +(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. + +3. Conditions and Limitations +(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. +(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, +your patent license from such contributor to the software ends automatically. +(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution +notices that are present in the software. +(D) If you distribute any portion of the software in source code form, you may do so only under this license by including +a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object +code form, you may only do so under a license that complies with this license. +(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees +or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent +permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular +purpose and non-infringement. +*/ +#endregion License + +#region Using clause +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Runtime.Remoting.Messaging; +using Android.App; +using Android.Content; +using Android.Views; +using Android.Widget; +using Microsoft.Xna.Framework.Net; + + +#endregion Using clause + +namespace Microsoft.Xna.Framework.GamerServices +{ + public static class Guide + { + private static bool isScreenSaverEnabled; + private static bool isTrialMode; + private static bool isVisible; + private static bool simulateTrialMode; + + internal static void Initialise(Game game) + { + MonoGameGamerServicesHelper.Initialise(game); + } + + delegate string ShowKeyboardInputDelegate( + PlayerIndex player, + string title, + string description, + string defaultText, + bool usePasswordMode); + + public static string ShowKeyboardInput( + PlayerIndex player, + string title, + string description, + string defaultText, + bool usePasswordMode) + { + string result = null; + EventWaitHandle waitHandle = new EventWaitHandle(false, EventResetMode.AutoReset); + + IsVisible = true; + + Game.Activity.RunOnUiThread(() => + { + var alert = new AlertDialog.Builder(Game.Activity); + + alert.SetTitle(title); + alert.SetMessage(description); + + var input = new EditText(Game.Activity) { Text = defaultText }; + if (defaultText != null) + { + input.SetSelection(defaultText.Length); + } + if (usePasswordMode) + { + input.InputType = Android.Text.InputTypes.ClassText | Android.Text.InputTypes.TextVariationPassword; + } + alert.SetView(input); + + alert.SetPositiveButton("Ok", (dialog, whichButton) => + { + result = input.Text; + IsVisible = false; + waitHandle.Set(); + }); + + alert.SetNegativeButton("Cancel", (dialog, whichButton) => + { + result = null; + IsVisible = false; + waitHandle.Set(); + }); + alert.SetCancelable(false); + alert.Show(); + + }); + waitHandle.WaitOne(); + IsVisible = false; + + return result; + } + + public static IAsyncResult BeginShowKeyboardInput ( + PlayerIndex player, + string title, + string description, + string defaultText, + AsyncCallback callback, + Object state) + { + return BeginShowKeyboardInput(player, title, description, defaultText, callback, state, false ); + } + + public static IAsyncResult BeginShowKeyboardInput ( + PlayerIndex player, + string title, + string description, + string defaultText, + AsyncCallback callback, + Object state, + bool usePasswordMode) + { + if (IsVisible) + throw new GuideAlreadyVisibleException("The function cannot be completed at this time: the Guide UI is already active. Wait until Guide.IsVisible is false before issuing this call."); + + IsVisible = true; + + ShowKeyboardInputDelegate ski = ShowKeyboardInput; + + return ski.BeginInvoke(player, title, description, defaultText, usePasswordMode, callback, ski); + } + + public static string EndShowKeyboardInput (IAsyncResult result) + { + try + { + return (result.AsyncState as ShowKeyboardInputDelegate).EndInvoke(result); + } + finally + { + IsVisible = false; + } + } + + delegate Nullable ShowMessageBoxDelegate( string title, + string text, + IEnumerable buttons, + int focusButton, + MessageBoxIcon icon); + + public static Nullable ShowMessageBox( string title, + string text, + IEnumerable buttons, + int focusButton, + MessageBoxIcon icon) + { + Nullable result = null; + + IsVisible = true; + EventWaitHandle waitHandle = new EventWaitHandle(false, EventResetMode.AutoReset); + + Game.Activity.RunOnUiThread(() => { + AlertDialog.Builder alert = new AlertDialog.Builder(Game.Activity); + + alert.SetTitle(title); + alert.SetMessage(text); + + alert.SetPositiveButton(buttons.ElementAt(0), (dialog, whichButton) => + { + result = 0; + IsVisible = false; + waitHandle.Set(); + }); + + if (buttons.Count() == 2) + alert.SetNegativeButton(buttons.ElementAt(1), (dialog, whichButton) => + { + result = 1; + IsVisible = false; + waitHandle.Set(); + }); + alert.SetCancelable(false); + + alert.Show(); + }); + waitHandle.WaitOne(); + IsVisible = false; + + return result; + } + + public static IAsyncResult BeginShowMessageBox( + PlayerIndex player, + string title, + string text, + IEnumerable buttons, + int focusButton, + MessageBoxIcon icon, + AsyncCallback callback, + Object state + ) + { + if (IsVisible) + throw new GuideAlreadyVisibleException("The function cannot be completed at this time: the Guide UI is already active. Wait until Guide.IsVisible is false before issuing this call."); + + IsVisible = true; + + ShowMessageBoxDelegate smb = ShowMessageBox; + + return smb.BeginInvoke(title, text, buttons, focusButton, icon, callback, smb); + } + + public static IAsyncResult BeginShowMessageBox ( + string title, + string text, + IEnumerable buttons, + int focusButton, + MessageBoxIcon icon, + AsyncCallback callback, + Object state + ) + { + return BeginShowMessageBox(PlayerIndex.One, title, text, buttons, focusButton, icon, callback, state); + } + + public static Nullable EndShowMessageBox (IAsyncResult result) + { + try + { + return (result.AsyncState as ShowMessageBoxDelegate).EndInvoke(result); + } + finally + { + IsVisible = false; + } + } + + + public static void ShowMarketplace (PlayerIndex player ) + { + string packageName = Game.Activity.PackageName; + try + { + Intent intent = new Intent(Intent.ActionView); + intent.SetData(Android.Net.Uri.Parse("market://details?id=" + packageName)); + intent.SetFlags(ActivityFlags.NewTask); + Game.Activity.StartActivity(intent); + } + catch (ActivityNotFoundException) + { + Intent intent = new Intent(Intent.ActionView); + intent.SetData(Android.Net.Uri.Parse("http://play.google.com/store/apps/details?id=" + packageName)); + intent.SetFlags(ActivityFlags.NewTask); + Game.Activity.StartActivity(intent); + } + } + + public static void Show () + { + ShowSignIn(1, false); + } + + public static void ShowSignIn (int paneCount, bool onlineOnly) + { + if ( paneCount != 1 ) + { + new ArgumentException("paneCount Can only be 1 on iPhone"); + return; + } + + MonoGameGamerServicesHelper.ShowSigninSheet(); + + if (GamerServicesComponent.LocalNetworkGamer == null) + { + GamerServicesComponent.LocalNetworkGamer = new LocalNetworkGamer(); + } + else + { + GamerServicesComponent.LocalNetworkGamer.SignedInGamer.BeginAuthentication(null, null); + } + } + + public static void ShowLeaderboard() + { + if ( ( Gamer.SignedInGamers.Count > 0 ) && ( Gamer.SignedInGamers[0].IsSignedInToLive ) ) + { + + } + } + + public static void ShowAchievements() + { + if ( ( Gamer.SignedInGamers.Count > 0 ) && ( Gamer.SignedInGamers[0].IsSignedInToLive ) ) + { + + } + } + + public static void ShowPeerPicker() + { + if ( ( Gamer.SignedInGamers.Count > 0 ) && ( Gamer.SignedInGamers[0].IsSignedInToLive ) ) + { + + } + } + + + public static void ShowMatchMaker() + { + if ( ( Gamer.SignedInGamers.Count > 0 ) && ( Gamer.SignedInGamers[0].IsSignedInToLive ) ) + { + + } + } + + #region Properties + public static bool IsScreenSaverEnabled + { + get + { + return isScreenSaverEnabled; + } + set + { + isScreenSaverEnabled = value; + } + } + + public static bool IsTrialMode + { + get + { + return isTrialMode; + } + set + { + isTrialMode = value; + } + } + + public static bool IsVisible + { + get + { + return isVisible; + } + internal set + { + isVisible = value; + } + } + + public static bool SimulateTrialMode + { + get + { + return simulateTrialMode; + } + set + { + simulateTrialMode = value; + } + } + + [CLSCompliant(false)] + public static AndroidGameWindow Window + { + get; + set; + } + #endregion + + } +} diff --git a/MonoGame.Framework/Android/GamerServices/MonoGameGamerServicesHelper.cs b/MonoGame.Framework/Android/GamerServices/MonoGameGamerServicesHelper.cs new file mode 100644 index 00000000000..6fc33816d3b --- /dev/null +++ b/MonoGame.Framework/Android/GamerServices/MonoGameGamerServicesHelper.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Diagnostics; +using Microsoft.Xna.Framework.Graphics; + +namespace Microsoft.Xna.Framework.GamerServices +{ + internal class MonoGameGamerServicesHelper + { + private static MonoLiveGuide guide = null; + + + + public static void ShowSigninSheet() + { + guide.Enabled = true; + guide.Visible = true; + Guide.IsVisible = true; + } + + internal static void Initialise(Game game) + { + if (guide == null) + { + guide = new MonoLiveGuide(game); + game.Components.Add(guide); + } + }} + + internal class MonoLiveGuide : DrawableGameComponent + { + SpriteBatch spriteBatch; + Texture2D signInProgress; + Color alphaColor = new Color(128, 128, 128, 0); + byte startalpha = 0; + + public MonoLiveGuide(Game game) + : base(game) + { + this.Enabled = false; + this.Visible = false; + //Guide.IsVisible = false; + this.DrawOrder = Int32.MaxValue; + } + + public override void Initialize() + { + base.Initialize(); + } + + Texture2D Circle(GraphicsDevice graphics, int radius) + { + int aDiameter = radius * 2; + Vector2 aCenter = new Vector2(radius, radius); + + Texture2D aCircle = new Texture2D(graphics, aDiameter, aDiameter, false, SurfaceFormat.Color); + Color[] aColors = new Color[aDiameter * aDiameter]; + + for (int i = 0; i < aColors.Length; i++) + { + int x = (i + 1) % aDiameter; + int y = (i + 1) / aDiameter; + + Vector2 aDistance = new Vector2(Math.Abs(aCenter.X - x), Math.Abs(aCenter.Y - y)); + + + if (Math.Sqrt((aDistance.X * aDistance.X) + (aDistance.Y * aDistance.Y)) > radius) + { + aColors[i] = Color.Transparent; + } + else + { + aColors[i] = Color.White; + } + } + + aCircle.SetData(aColors); + + return aCircle; + } + + protected override void LoadContent() + { + spriteBatch = new SpriteBatch(this.Game.GraphicsDevice); + + signInProgress = Circle(this.Game.GraphicsDevice, 10); + + base.LoadContent(); + } + + protected override void UnloadContent() + { + base.UnloadContent(); + } + + public override void Draw(GameTime gameTime) + { + spriteBatch.Begin();//SpriteSortMode.Immediate, BlendState.AlphaBlend); + + Vector2 center = new Vector2(this.Game.GraphicsDevice.Viewport.Width / 2, this.Game.GraphicsDevice.Viewport.Height - 100); + Vector2 loc = Vector2.Zero; + alphaColor.A = startalpha; + for (int i = 0; i < 12; i++) + { + float angle = (float)(i / 12.0 * Math.PI * 2); + loc = new Vector2(center.X + (float)Math.Cos(angle) * 50, center.Y + (float)Math.Sin(angle) * 50); + spriteBatch.Draw(signInProgress, loc, alphaColor); + alphaColor.A += 255 / 12; + if (alphaColor.A > 255) alphaColor.A = 0; + } + spriteBatch.End(); + base.Draw(gameTime); + } + + TimeSpan gt = TimeSpan.Zero; + TimeSpan last = TimeSpan.Zero; + int delay = 2; + + public override void Update(GameTime gameTime) + { + if (gt == TimeSpan.Zero) gt = last = gameTime.TotalGameTime; + + if ((gameTime.TotalGameTime - last).Milliseconds > 100) + { + last = gameTime.TotalGameTime; + startalpha += 255 / 12; + } + + if ((gameTime.TotalGameTime - gt).TotalSeconds > delay) // close after 10 seconds + { + + string name = "androiduser"; + try + { + Android.Accounts.AccountManager mgr = (Android.Accounts.AccountManager)Android.App.Application.Context.GetSystemService(Android.App.Activity.AccountService); + if (mgr != null) + { + var accounts = mgr.GetAccounts(); + if (accounts != null && accounts.Length > 0) + { + name = accounts[0].Name; + if (name.Contains("@")) + { + // its an email + name = name.Substring(0, name.IndexOf("@")); + } + } + } + } + catch + { + } + + SignedInGamer sig = new SignedInGamer(); + sig.DisplayName = name; + sig.Gamertag = name; + sig.IsSignedInToLive = false; + + Gamer.SignedInGamers.Add(sig); + + this.Visible = false; + this.Enabled = false; + //Guide.IsVisible = false; + gt = TimeSpan.Zero; + } + base.Update(gameTime); + } + + } +} + + diff --git a/MonoGame.Framework/Android/GamerServices/SignedInGamer.cs b/MonoGame.Framework/Android/GamerServices/SignedInGamer.cs new file mode 100644 index 00000000000..0f459af2342 --- /dev/null +++ b/MonoGame.Framework/Android/GamerServices/SignedInGamer.cs @@ -0,0 +1,341 @@ +#region License +/* +Microsoft Public License (Ms-PL) +MonoGame - Copyright © 2009 The MonoGame Team + +All rights reserved. + +This license governs use of the accompanying software. If you use the software, you accept this license. If you do not +accept the license, do not use the software. + +1. Definitions +The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under +U.S. copyright law. + +A "contribution" is the original software, or any additions or changes to the software. +A "contributor" is any person that distributes its contribution under this license. +"Licensed patents" are a contributor's patent claims that read directly on its contribution. + +2. Grant of Rights +(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. +(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. + +3. Conditions and Limitations +(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. +(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, +your patent license from such contributor to the software ends automatically. +(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution +notices that are present in the software. +(D) If you distribute any portion of the software in source code form, you may do so only under this license by including +a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object +code form, you may only do so under a license that complies with this license. +(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees +or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent +permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular +purpose and non-infringement. +*/ +#endregion License + +#region Statement +using System; + +using Android.Net; + +#endregion Statement + + +namespace Microsoft.Xna.Framework.GamerServices +{ + public class SignedInGamer : Gamer + { + // TODO private GKLocalPlayer lp; + + private AchievementCollection gamerAchievements; + private FriendCollection friendCollection; + private bool isSignedInToLive = true; + + delegate void AuthenticationDelegate(); + + public IAsyncResult BeginAuthentication(AsyncCallback callback, Object asyncState) + { + // Go off authenticate + AuthenticationDelegate ad = DoAuthentication; + + return ad.BeginInvoke(callback, ad); + } + + public void EndAuthentication( IAsyncResult result ) + { + AuthenticationDelegate ad = (AuthenticationDelegate)result.AsyncState; + + ad.EndInvoke(result); + } + + private void DoAuthentication() + { + try + { + + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + } + + public SignedInGamer() + { + var result = BeginAuthentication(null, null); + EndAuthentication( result ); + } + + private void AuthenticationCompletedCallback( IAsyncResult result ) + { + EndAuthentication(result); + } + + #region Methods + public FriendCollection GetFriends() + { + if(IsSignedInToLive) + { + if ( friendCollection == null ) + { + friendCollection = new FriendCollection(); + } + } + + return friendCollection; + } + + public bool IsFriend (Gamer gamer) + { + if ( gamer == null ) + throw new ArgumentNullException(); + + if ( gamer.IsDisposed ) + throw new ObjectDisposedException(gamer.ToString()); + + bool found = false; + foreach(FriendGamer f in friendCollection) + { + if ( f.Gamertag == gamer.Gamertag ) + { + found = true; + } + } + return found; + + } + + delegate AchievementCollection GetAchievementsDelegate(); + + public IAsyncResult BeginGetAchievements( AsyncCallback callback, Object asyncState) + { + // Go off and grab achievements + GetAchievementsDelegate gad = GetAchievements; + + return gad.BeginInvoke(callback, gad); + } + + private void GetAchievementCompletedCallback( IAsyncResult result ) + { + // get the delegate that was used to call that method + GetAchievementsDelegate gad = (GetAchievementsDelegate)result.AsyncState; + + // get the return value from that method call + gamerAchievements = gad.EndInvoke(result); + } + + public AchievementCollection EndGetAchievements( IAsyncResult result ) + { + GetAchievementsDelegate gad = (GetAchievementsDelegate)result.AsyncState; + + gamerAchievements = gad.EndInvoke(result); + + return gamerAchievements; + } + + public AchievementCollection GetAchievements() + { + if ( IsSignedInToLive ) + { + if (gamerAchievements == null) + { + gamerAchievements = new AchievementCollection(); + } + } + return gamerAchievements; + } + + delegate void AwardAchievementDelegate(string achievementId, double percentageComplete); + + public IAsyncResult BeginAwardAchievement(string achievementId, AsyncCallback callback, Object state) + { + return BeginAwardAchievement(achievementId, 100.0, callback, state); + } + + public IAsyncResult BeginAwardAchievement( + string achievementId, + double percentageComplete, + AsyncCallback callback, + Object state + ) + { + // Go off and award the achievement + AwardAchievementDelegate aad = DoAwardAchievement; + + return aad.BeginInvoke(achievementId, percentageComplete, callback, aad); + } + + public void EndAwardAchievement(IAsyncResult result) + { + AwardAchievementDelegate aad = (AwardAchievementDelegate)result.AsyncState; + + aad.EndInvoke(result); + } + + private void AwardAchievementCompletedCallback( IAsyncResult result ) + { + EndAwardAchievement(result); + } + + public void AwardAchievement( string achievementId ) + { + AwardAchievement(achievementId, 100.0f); + } + + public void DoAwardAchievement( string achievementId, double percentageComplete ) + { + + } + + public void AwardAchievement( string achievementId, double percentageComplete ) + { + if (IsSignedInToLive) + { + BeginAwardAchievement( achievementId, percentageComplete, AwardAchievementCompletedCallback, null ); + } + } + + public void UpdateScore( string aCategory, long aScore ) + { + if (IsSignedInToLive) + { + + } + } + + public void ResetAchievements() + { + if (IsSignedInToLive) + { + + } + } + + #endregion + + #region Properties + public GameDefaults GameDefaults + { + get + { + throw new NotSupportedException(); + } + } + + public bool IsGuest + { + get + { + throw new NotSupportedException(); + } + } + + public bool IsSignedInToLive + { + get + { + return isSignedInToLive; + } + internal set{ + isSignedInToLive = value; + } + } + + public int PartySize + { + get + { + throw new NotSupportedException(); + } + set + { + throw new NotSupportedException(); + } + } + + public PlayerIndex PlayerIndex + { + get + { + return PlayerIndex.One; + } + } + + public GamerPresence Presence + { + get + { + throw new NotSupportedException(); + } + } + + GamerPrivileges _privileges = new GamerPrivileges(); + public GamerPrivileges Privileges + { + get + { + return _privileges; + } + } + #endregion + + + protected virtual void OnSignedIn(SignedInEventArgs e) + { + EventHelpers.Raise(this, SignedIn, e); + } + + protected virtual void OnSignedOut(SignedOutEventArgs e) + { + EventHelpers.Raise(this, SignedOut, e); + } + + + #region Events + public static event EventHandler SignedIn; + + public static event EventHandler SignedOut; + #endregion + } + + public class SignedInEventArgs : EventArgs + { + public SignedInEventArgs ( SignedInGamer gamer ) + { + + } + } + + public class SignedOutEventArgs : EventArgs + { + public SignedOutEventArgs (SignedInGamer gamer ) + { + + } + } +} diff --git a/MonoGame.Framework/Android/IResumeManager.cs b/MonoGame.Framework/Android/IResumeManager.cs new file mode 100644 index 00000000000..af421133686 --- /dev/null +++ b/MonoGame.Framework/Android/IResumeManager.cs @@ -0,0 +1,63 @@ +#region License +/* +Microsoft Public License (Ms-PL) +MonoGame - Copyright © 2012 The MonoGame Team + +All rights reserved. + +This license governs use of the accompanying software. If you use the software, you accept this license. If you do not +accept the license, do not use the software. + +1. Definitions +The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under +U.S. copyright law. + +A "contribution" is the original software, or any additions or changes to the software. +A "contributor" is any person that distributes its contribution under this license. +"Licensed patents" are a contributor's patent claims that read directly on its contribution. + +2. Grant of Rights +(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. +(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. + +3. Conditions and Limitations +(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. +(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, +your patent license from such contributor to the software ends automatically. +(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution +notices that are present in the software. +(D) If you distribute any portion of the software in source code form, you may do so only under this license by including +a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object +code form, you may only do so under a license that complies with this license. +(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees +or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent +permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular +purpose and non-infringement. +*/ +#endregion License + +using System; + +namespace Microsoft.Xna.Framework +{ + /// + /// Interface for a class that handles resuming after a device lost event. + /// In particular, this allows the game to draw something to the screen whilst + /// graphics content is reloaded - a potentially lengthy operation. + /// + public interface IResumeManager + { + /// + /// Called at the start of the resume process. Textures should always be reloaded here. + /// If using a ContentManager, it should be disposed and recreated. + /// + void LoadContent(); + + /// + /// Called whilst the game is resuming. Draw something to the screen here. + /// + void Draw(); + } +} \ No newline at end of file diff --git a/MonoGame.Framework/Android/Input/Keyboard.cs b/MonoGame.Framework/Android/Input/Keyboard.cs new file mode 100644 index 00000000000..72ee5dcd283 --- /dev/null +++ b/MonoGame.Framework/Android/Input/Keyboard.cs @@ -0,0 +1,194 @@ +// #region License +// /* +// Microsoft Public License (Ms-PL) +// MonoGame - Copyright © 2009 The MonoGame Team +// +// All rights reserved. +// +// This license governs use of the accompanying software. If you use the software, you accept this license. If you do not +// accept the license, do not use the software. +// +// 1. Definitions +// The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under +// U.S. copyright law. +// +// A "contribution" is the original software, or any additions or changes to the software. +// A "contributor" is any person that distributes its contribution under this license. +// "Licensed patents" are a contributor's patent claims that read directly on its contribution. +// +// 2. Grant of Rights +// (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +// each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. +// (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +// each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. +// +// 3. Conditions and Limitations +// (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. +// (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, +// your patent license from such contributor to the software ends automatically. +// (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution +// notices that are present in the software. +// (D) If you distribute any portion of the software in source code form, you may do so only under this license by including +// a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object +// code form, you may only do so under a license that complies with this license. +// (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees +// or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent +// permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular +// purpose and non-infringement. +// */ +// #endregion License +// + +using System; +using System.Collections.Generic; +using System.Linq; +using Android.Views; + +namespace Microsoft.Xna.Framework.Input +{ + public static class Keyboard + { + private static List keys = new List(); + + private static readonly IDictionary KeyMap = LoadKeyMap(); + + internal static bool KeyDown(Keycode keyCode) + { + Keys key; + if (KeyMap.TryGetValue(keyCode, out key) && key != Keys.None) + { + if (!keys.Contains(key)) + keys.Add(key); + return true; + } + return false; + } + + internal static bool KeyUp(Keycode keyCode) + { + Keys key; + if (KeyMap.TryGetValue(keyCode, out key) && key != Keys.None) + { + if (keys.Contains(key)) + keys.Remove(key); + return true; + } + return false; + } + + private static IDictionary LoadKeyMap() + { + // create a map for every Keycode and default it to none so that every possible key is mapped + var maps = Enum.GetValues(typeof (Keycode)) + .Cast() + .ToDictionary(key => key, key => Keys.None); + + // then update it with the actual mappings + maps[Keycode.DpadLeft] = Keys.Left; + maps[Keycode.DpadRight] = Keys.Right; + maps[Keycode.DpadUp] = Keys.Up; + maps[Keycode.DpadDown] = Keys.Down; + maps[Keycode.DpadCenter] = Keys.Enter; + maps[Keycode.Num0] = Keys.D0; + maps[Keycode.Num1] = Keys.D1; + maps[Keycode.Num2] = Keys.D2; + maps[Keycode.Num3] = Keys.D3; + maps[Keycode.Num4] = Keys.D4; + maps[Keycode.Num5] = Keys.D5; + maps[Keycode.Num6] = Keys.D6; + maps[Keycode.Num7] = Keys.D7; + maps[Keycode.Num8] = Keys.D8; + maps[Keycode.Num9] = Keys.D9; + maps[Keycode.A] = Keys.A; + maps[Keycode.B] = Keys.B; + maps[Keycode.C] = Keys.C; + maps[Keycode.D] = Keys.D; + maps[Keycode.E] = Keys.E; + maps[Keycode.F] = Keys.F; + maps[Keycode.G] = Keys.G; + maps[Keycode.H] = Keys.H; + maps[Keycode.I] = Keys.I; + maps[Keycode.J] = Keys.J; + maps[Keycode.K] = Keys.K; + maps[Keycode.L] = Keys.L; + maps[Keycode.M] = Keys.M; + maps[Keycode.N] = Keys.N; + maps[Keycode.O] = Keys.O; + maps[Keycode.P] = Keys.P; + maps[Keycode.Q] = Keys.Q; + maps[Keycode.R] = Keys.R; + maps[Keycode.S] = Keys.S; + maps[Keycode.T] = Keys.T; + maps[Keycode.U] = Keys.U; + maps[Keycode.V] = Keys.V; + maps[Keycode.W] = Keys.W; + maps[Keycode.X] = Keys.X; + maps[Keycode.Y] = Keys.Y; + maps[Keycode.Z] = Keys.Z; + maps[Keycode.Space] = Keys.Space; + maps[Keycode.Escape] = Keys.Escape; + maps[Keycode.Back] = Keys.Back; + maps[Keycode.Home] = Keys.Home; + maps[Keycode.Enter] = Keys.Enter; + maps[Keycode.Period] = Keys.OemPeriod; + maps[Keycode.Comma] = Keys.OemComma; + maps[Keycode.Menu] = Keys.Help; + maps[Keycode.Search] = Keys.BrowserSearch; + maps[Keycode.VolumeUp] = Keys.VolumeUp; + maps[Keycode.VolumeDown] = Keys.VolumeDown; + maps[Keycode.MediaPause] = Keys.Pause; + maps[Keycode.MediaPlayPause] = Keys.MediaPlayPause; + maps[Keycode.MediaStop] = Keys.MediaStop; + maps[Keycode.MediaNext] = Keys.MediaNextTrack; + maps[Keycode.MediaPrevious] = Keys.MediaPreviousTrack; + maps[Keycode.Mute] = Keys.VolumeMute; + maps[Keycode.AltLeft] = Keys.LeftAlt; + maps[Keycode.AltRight] = Keys.RightAlt; + maps[Keycode.ShiftLeft] = Keys.LeftShift; + maps[Keycode.ShiftRight] = Keys.RightShift; + maps[Keycode.Tab] = Keys.Tab; + maps[Keycode.Del] = Keys.Delete; + maps[Keycode.Minus] = Keys.OemMinus; + maps[Keycode.LeftBracket] = Keys.OemOpenBrackets; + maps[Keycode.RightBracket] = Keys.OemCloseBrackets; + maps[Keycode.Backslash] = Keys.OemBackslash; + maps[Keycode.Semicolon] = Keys.OemSemicolon; + maps[Keycode.PageUp] = Keys.PageUp; + maps[Keycode.PageDown] = Keys.PageDown; + maps[Keycode.CtrlLeft] = Keys.LeftControl; + maps[Keycode.CtrlRight] = Keys.RightControl; + maps[Keycode.CapsLock] = Keys.CapsLock; + maps[Keycode.ScrollLock] = Keys.Scroll; + maps[Keycode.NumLock] = Keys.NumLock; + maps[Keycode.Insert] = Keys.Insert; + maps[Keycode.F1] = Keys.F1; + maps[Keycode.F2] = Keys.F2; + maps[Keycode.F3] = Keys.F3; + maps[Keycode.F4] = Keys.F4; + maps[Keycode.F5] = Keys.F5; + maps[Keycode.F6] = Keys.F6; + maps[Keycode.F7] = Keys.F7; + maps[Keycode.F8] = Keys.F8; + maps[Keycode.F9] = Keys.F9; + maps[Keycode.F10] = Keys.F10; + maps[Keycode.F11] = Keys.F11; + maps[Keycode.F12] = Keys.F12; + maps[Keycode.NumpadDivide] = Keys.Divide; + maps[Keycode.NumpadMultiply] = Keys.Multiply; + maps[Keycode.NumpadSubtract] = Keys.Subtract; + maps[Keycode.NumpadAdd] = Keys.Add; + + return maps; + } + + public static KeyboardState GetState() + { + return new KeyboardState(keys); + } + + public static KeyboardState GetState(PlayerIndex playerIndex) + { + return new KeyboardState(keys); + } + } +} diff --git a/MonoGame.Framework/Android/Input/Touch/AndroidTouchEventManager.cs b/MonoGame.Framework/Android/Input/Touch/AndroidTouchEventManager.cs new file mode 100644 index 00000000000..9aacc004a1e --- /dev/null +++ b/MonoGame.Framework/Android/Input/Touch/AndroidTouchEventManager.cs @@ -0,0 +1,80 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using Android.Views; + +namespace Microsoft.Xna.Framework.Input.Touch +{ + /// + /// Manages touch events for Android. Maps new presses to new touch Ids as per Xna WP7 incrementing touch Id behaviour. + /// This is required as Android reports touch IDs of 0 to 5, which leads to incorrect handling of touch events. + /// Motivation and discussion: http://monogame.codeplex.com/discussions/382252 + /// + class AndroidTouchEventManager + { + readonly AndroidGameWindow _gameWindow; + + public bool Enabled { get; set; } + + public AndroidTouchEventManager(AndroidGameWindow androidGameWindow) + { + _gameWindow = androidGameWindow; + } + + public void OnTouchEvent(MotionEvent e) + { + if (!Enabled) + return; + + Vector2 position = Vector2.Zero; + position.X = e.GetX(e.ActionIndex); + position.Y = e.GetY(e.ActionIndex); + UpdateTouchPosition(ref position); + int id = e.GetPointerId(e.ActionIndex); + switch (e.ActionMasked) + { + // DOWN + case MotionEventActions.Down: + case MotionEventActions.PointerDown: + TouchPanel.AddEvent(id, TouchLocationState.Pressed, position); + break; + // UP + case MotionEventActions.Up: + case MotionEventActions.PointerUp: + TouchPanel.AddEvent(id, TouchLocationState.Released, position); + break; + // MOVE + case MotionEventActions.Move: + for (int i = 0; i < e.PointerCount; i++) + { + id = e.GetPointerId(i); + position.X = e.GetX(i); + position.Y = e.GetY(i); + UpdateTouchPosition(ref position); + TouchPanel.AddEvent(id, TouchLocationState.Moved, position); + } + break; + + // CANCEL, OUTSIDE + case MotionEventActions.Cancel: + case MotionEventActions.Outside: + for (int i = 0; i < e.PointerCount; i++) + { + id = e.GetPointerId(i); + TouchPanel.AddEvent(id, TouchLocationState.Released, position); + } + break; + } + } + + void UpdateTouchPosition(ref Vector2 position) + { + Rectangle clientBounds = _gameWindow.ClientBounds; + + //Fix for ClientBounds + position.X -= clientBounds.X; + position.Y -= clientBounds.Y; + } + } +} \ No newline at end of file diff --git a/MonoGame.Framework/Android/MonoGameAndroidGameView.cs b/MonoGame.Framework/Android/MonoGameAndroidGameView.cs new file mode 100644 index 00000000000..17272088898 --- /dev/null +++ b/MonoGame.Framework/Android/MonoGameAndroidGameView.cs @@ -0,0 +1,1297 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Android.Content; +using Android.Media; +using Android.Runtime; +using Android.Util; +using Android.Views; +using Javax.Microedition.Khronos.Egl; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Input; +using Microsoft.Xna.Framework.Input.Touch; + +namespace Microsoft.Xna.Framework +{ + [CLSCompliant(false)] + public class MonoGameAndroidGameView : SurfaceView, ISurfaceHolderCallback, View.IOnTouchListener + { + // What is the state of the app, for tracking surface recreation inside this class. + // This acts as a replacement for the all-out monitor wait approach which caused code to be quite fragile. + enum InternalState + { + Pausing_UIThread, // set by android UI thread and the game thread process it and transitions into 'Paused' state + Resuming_UIThread, // set by android UI thread and the game thread process it and transitions into 'Running' state + Exiting, // set either by game or android UI thread and the game thread process it and transitions into 'Exited' state + + Paused_GameThread, // set by game thread after processing 'Pausing' state + Running_GameThread, // set by game thread after processing 'Resuming' state + Exited_GameThread, // set by game thread after processing 'Exiting' state + + ForceRecreateSurface, // also used to create the surface the 1st time or when screen orientation changes + } + + bool disposed = false; + ISurfaceHolder mHolder; + System.Drawing.Size size; + + ManualResetEvent _waitForPausedStateProcessed = new ManualResetEvent(false); + ManualResetEvent _waitForResumedStateProcessed = new ManualResetEvent(false); + ManualResetEvent _waitForExitedStateProcessed = new ManualResetEvent(false); + + AutoResetEvent _waitForMainGameLoop = new AutoResetEvent(false); + AutoResetEvent _workerThreadUIRenderingWait = new AutoResetEvent(false); + + object _lockObject = new object(); + + volatile InternalState _internalState = InternalState.Exited_GameThread; + + bool androidSurfaceAvailable = false; + + bool glSurfaceAvailable; + bool glContextAvailable; + bool lostglContext; + System.Diagnostics.Stopwatch stopWatch; + double tick = 0; + + bool loaded = false; + + Task renderTask; + CancellationTokenSource cts = null; + private readonly AndroidTouchEventManager _touchManager; + private readonly AndroidGameWindow _gameWindow; + private readonly Game _game; + + // Events that are triggered on the game thread + public static event EventHandler OnPauseGameThread; + public static event EventHandler OnResumeGameThread; + + public bool TouchEnabled + { + get { return _touchManager.Enabled; } + set + { + _touchManager.Enabled = value; + SetOnTouchListener(value ? this : null); + } + } + + public bool IsResuming { get; private set; } + + public MonoGameAndroidGameView(Context context, AndroidGameWindow gameWindow, Game game) + : base(context) + { + _gameWindow = gameWindow; + _game = game; + _touchManager = new AndroidTouchEventManager(gameWindow); + Init(); + } + + private void Init() + { + // default + mHolder = Holder; + // Add callback to get the SurfaceCreated etc events + mHolder.AddCallback(this); + mHolder.SetType(SurfaceType.Gpu); + } + + public void SurfaceChanged(ISurfaceHolder holder, global::Android.Graphics.Format format, int width, int height) + { + // Set flag to recreate gl surface or rendering can be bad on orienation change or if app + // is closed in one orientation and re-opened in another. + lock (_lockObject) + { + // can only be triggered when main loop is running, is unsafe to overwrite other states + if (_internalState == InternalState.Running_GameThread) + { + _internalState = InternalState.ForceRecreateSurface; + } + + } + } + + public void SurfaceCreated(ISurfaceHolder holder) + { + lock (_lockObject) + { + androidSurfaceAvailable = true; + } + } + + public void SurfaceDestroyed(ISurfaceHolder holder) + { + lock (_lockObject) + { + androidSurfaceAvailable = false; + } + } + + public bool OnTouch(View v, MotionEvent e) + { + _touchManager.OnTouchEvent(e); + return true; + } + + public virtual void SwapBuffers() + { + EnsureUndisposed(); + if (!egl.EglSwapBuffers(eglDisplay, eglSurface)) + { + if (egl.EglGetError() == 0) + { + if (lostglContext) + System.Diagnostics.Debug.WriteLine("Lost EGL context" + GetErrorAsString()); + lostglContext = true; + } + } + + } + + public virtual void MakeCurrent() + { + EnsureUndisposed(); + if (!egl.EglMakeCurrent(eglDisplay, eglSurface, + eglSurface, eglContext)) + { + System.Diagnostics.Debug.WriteLine("Error Make Current" + GetErrorAsString()); + } + + } + + public virtual void ClearCurrent() + { + EnsureUndisposed(); + if (!egl.EglMakeCurrent(eglDisplay, EGL10.EglNoSurface, + EGL10.EglNoSurface, EGL10.EglNoContext)) + { + System.Diagnostics.Debug.WriteLine("Error Clearing Current" + GetErrorAsString()); + } + } + + double updates; + + public bool LogFPS { get; set; } + public bool RenderOnUIThread { get; set; } + + public virtual void Run() + { + Run(0.0); + } + + public virtual void Run(double updatesPerSecond) + { + cts = new CancellationTokenSource(); + if (LogFPS) + { + targetFps = currentFps = 0; + avgFps = 1; + } + updates = 1000 / updatesPerSecond; + + //var syncContext = new SynchronizationContext (); + var syncContext = SynchronizationContext.Current; + + // We always start a new task, regardless if we render on UI thread or not. + renderTask = Task.Factory.StartNew(() => + { + WorkerThreadFrameDispatcher(syncContext); + + }, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default) + .ContinueWith((t) => + { + OnStopped(EventArgs.Empty); + }); + } + + public virtual void Pause() + { + EnsureUndisposed(); + + // if triggered in quick succession and blocked by graphics device creation, + // pause can be triggered twice, without resume in between on some phones. + if (_internalState != InternalState.Running_GameThread) + { + return; + } + + // this guarantees that resume finished processing, since we cannot wait inside resume because we deadlock as surface wouldn't get created + if (RenderOnUIThread == false) + { + _waitForResumedStateProcessed.WaitOne(); + } + + _waitForMainGameLoop.Reset(); // in case it was enabled + + // happens if pause is called immediately after resume so that the surfaceCreated callback was not called yet. + bool isAndroidSurfaceAvalible = false; // use local because the wait below must be outside lock + lock (_lockObject) + { + isAndroidSurfaceAvalible = androidSurfaceAvailable; + if (!isAndroidSurfaceAvalible) + { + _internalState = InternalState.Paused_GameThread; // prepare for next game loop iteration + } + } + + lock (_lockObject) + { + // processing the pausing state only if the surface was created already + if (androidSurfaceAvailable) + { + _waitForPausedStateProcessed.Reset(); + _internalState = InternalState.Pausing_UIThread; + } + } + + if (RenderOnUIThread == false) + { + _waitForPausedStateProcessed.WaitOne(); + } + } + + public virtual void Resume() + { + EnsureUndisposed(); + + lock (_lockObject) + { + _waitForResumedStateProcessed.Reset(); + _internalState = InternalState.Resuming_UIThread; + } + + _waitForMainGameLoop.Set(); + + try + { + if (!IsFocused) + RequestFocus(); + } + catch { } + + // do not wait for state transition here since surface creation must be triggered first + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + Stop(); + } + base.Dispose(disposing); + } + + public void Stop() + { + EnsureUndisposed(); + if (cts != null) + { + lock (_lockObject) + { + _internalState = InternalState.Exiting; + } + + cts.Cancel(); + + if (RenderOnUIThread == false) + { + _waitForExitedStateProcessed.Reset(); + } + + } + } + + FrameEventArgs renderEventArgs = new FrameEventArgs(); + + protected void WorkerThreadFrameDispatcher(SynchronizationContext uiThreadSyncContext) + { + Threading.ResetThread(Thread.CurrentThread.ManagedThreadId); + try + { + stopWatch = System.Diagnostics.Stopwatch.StartNew(); + tick = 0; + prevUpdateTime = DateTime.Now; + + while (!cts.IsCancellationRequested) + { + // either use UI thread to render one frame or this worker thread + bool pauseThread = false; + if (RenderOnUIThread) + { + uiThreadSyncContext.Send((s) => + { + pauseThread = RunIteration(cts.Token); + }, null); + } + else + { + pauseThread = RunIteration(cts.Token); + } + + + if (pauseThread) + { + _waitForPausedStateProcessed.Set(); + _waitForMainGameLoop.WaitOne(); // pause this thread + } + } + } + catch (Exception ex) + { + Log.Error("AndroidGameView", ex.ToString()); + } + finally + { + bool c = cts.IsCancellationRequested; + + cts = null; + + if (glSurfaceAvailable) + DestroyGLSurface(); + + if (glContextAvailable) + { + DestroyGLContext(); + ContextLostInternal(); + } + + lock (_lockObject) + { + _internalState = InternalState.Exited_GameThread; + } + } + + } + + DateTime prevUpdateTime; + DateTime prevRenderTime; + DateTime curUpdateTime; + DateTime curRenderTime; + FrameEventArgs updateEventArgs = new FrameEventArgs(); + + void processStateDefault() + { + Log.Error("AndroidGameView", "Default case for switch on InternalState in main game loop, exiting"); + + lock (_lockObject) + { + _internalState = InternalState.Exited_GameThread; + } + } + + void processStateRunning(CancellationToken token) + { + // do not run game if surface is not avalible + lock (_lockObject) + { + if (!androidSurfaceAvailable) + { + return; + } + } + + + // check if app wants to exit + if (token.IsCancellationRequested) + { + // change state to exit and skip game loop + lock (_lockObject) + { + _internalState = InternalState.Exiting; + } + + return; + } + + try + { + UpdateAndRenderFrame(); + } + catch (MonoGameGLException ex) + { + Log.Error("AndroidGameView", "GL Exception occured during RunIteration {0}", ex.Message); + } + + if (updates > 0) + { + var t = updates - (stopWatch.Elapsed.TotalMilliseconds - tick); + if (t > 0) + { + if (LogFPS) + { + Log.Verbose("AndroidGameView", "took {0:F2}ms, should take {1:F2}ms, sleeping for {2:F2}", stopWatch.Elapsed.TotalMilliseconds - tick, updates, t); + } + + } + } + + } + + void processStatePausing() + { + if (glSurfaceAvailable) + { + // Surface we are using needs to go away + DestroyGLSurface(); + + if (loaded) + OnUnload(EventArgs.Empty); + } + + // trigger callbacks, must pause openAL device here + OnPauseGameThread(this, EventArgs.Empty); + + // go to next state + lock (_lockObject) + { + _internalState = InternalState.Paused_GameThread; + } + } + + void processStateResuming() + { + bool isSurfaceAvalible = false; + lock (_lockObject) + { + isSurfaceAvalible = androidSurfaceAvailable; + } + + // must sleep outside lock! + if (!RenderOnUIThread && !isSurfaceAvalible) + { + Thread.Sleep(50); // sleep so UI thread easier acquires lock + return; + } + + // this can happen if pause is triggered immediately after resume so that SurfaceCreated callback doesn't get called yet, + // in this case we skip the resume process and pause sets a new state. + lock (_lockObject) + { + if (!androidSurfaceAvailable) + return; + + // create surface if context is avalible + if (glContextAvailable && !lostglContext) + { + try + { + CreateGLSurface(); + } + catch (Exception ex) + { + // We failed to create the surface for some reason + Log.Verbose("AndroidGameView", ex.ToString()); + } + } + + // create context if not avalible + if ((!glContextAvailable || lostglContext)) + { + // Start or Restart due to context loss + bool contextLost = false; + if (lostglContext || glContextAvailable) + { + // we actually lost the context + // so we need to free up our existing + // objects and re-create one. + DestroyGLContext(); + contextLost = true; + + ContextLostInternal(); + } + + CreateGLContext(); + CreateGLSurface(); + + if (!loaded && glContextAvailable) + OnLoad(EventArgs.Empty); + + if (contextLost && glContextAvailable) + { + // we lost the gl context, we need to let the programmer + // know so they can re-create textures etc. + ContextSetInternal(); + } + + } + else if (glSurfaceAvailable) // finish state if surface created, may take a frame or two until the android UI thread callbacks fire + { + // trigger callbacks, must resume openAL device here + OnResumeGameThread(this, EventArgs.Empty); + + // go to next state + _internalState = InternalState.Running_GameThread; + } + } + } + + void processStateExiting() + { + // go to next state + lock (_lockObject) + { + _internalState = InternalState.Exited_GameThread; + } + } + + void processStateForceSurfaceRecreation() + { + // needed at app start + lock (_lockObject) + { + if (!androidSurfaceAvailable || !glContextAvailable) + { + return; + } + } + + DestroyGLSurface(); + CreateGLSurface(); + + // go to next state + lock (_lockObject) + { + _internalState = InternalState.Running_GameThread; + } + } + + // Return true to trigger worker thread pause + bool RunIteration(CancellationToken token) + { + // set main game thread global ID + Threading.ResetThread(Thread.CurrentThread.ManagedThreadId); + + InternalState currentState = InternalState.Exited_GameThread; + + lock (_lockObject) + { + currentState = _internalState; + } + + switch (currentState) + { + // exit states + case InternalState.Exiting: // when ui thread wants to exit + processStateExiting(); + break; + + case InternalState.Exited_GameThread: // when game thread processed exiting event + lock (_lockObject) + { + _waitForExitedStateProcessed.Set(); + cts.Cancel(); + } + break; + + // pause states + case InternalState.Pausing_UIThread: // when ui thread wants to pause + processStatePausing(); + break; + + case InternalState.Paused_GameThread: // when game thread processed pausing event + + // this must be processed outside of this loop, in the new task thread! + return true; // trigger pause of worker thread + + // other states + case InternalState.Resuming_UIThread: // when ui thread wants to resume + processStateResuming(); + + // pause must wait for resume in case pause/resume is called in very quick succession + lock (_lockObject) + { + _waitForResumedStateProcessed.Set(); + } + break; + + case InternalState.Running_GameThread: // when we are running game + processStateRunning(token); + + break; + + case InternalState.ForceRecreateSurface: + processStateForceSurfaceRecreation(); + break; + + // default case, error + default: + processStateDefault(); + cts.Cancel(); + break; + } + + return false; + } + + void UpdateFrameInternal(FrameEventArgs e) + { + OnUpdateFrame(e); + if (UpdateFrame != null) + { + UpdateFrame(this, e); + } + + } + + protected virtual void OnUpdateFrame(FrameEventArgs e) + { + + } + + // this method is called on the main thread + void UpdateAndRenderFrame() + { + curUpdateTime = DateTime.Now; + if (prevUpdateTime.Ticks != 0) + { + var t = (curUpdateTime - prevUpdateTime).TotalMilliseconds; + updateEventArgs.Time = t < 0 ? 0 : t; + } + + try + { + UpdateFrameInternal(updateEventArgs); + } + catch (Content.ContentLoadException ex) + { + if (RenderOnUIThread) + throw ex; + else + { + Game.Activity.RunOnUiThread (() => + { + throw ex; + }); + } + } + + prevUpdateTime = curUpdateTime; + + curRenderTime = DateTime.Now; + if (prevRenderTime.Ticks == 0) + { + var t = (curRenderTime - prevRenderTime).TotalMilliseconds; + renderEventArgs.Time = t < 0 ? 0 : t; + } + + RenderFrameInternal(renderEventArgs); + + prevRenderTime = curRenderTime; + } + + void RenderFrameInternal(FrameEventArgs e) + { + if (LogFPS) + { + Mark(); + } + + OnRenderFrame(e); + + if (RenderFrame != null) + RenderFrame(this, e); + } + + protected virtual void OnRenderFrame(FrameEventArgs e) + { + + } + + int frames = 0; + double prev = 0; + double avgFps = 0; + double currentFps = 0; + double targetFps = 0; + + void Mark() + { + double cur = stopWatch.Elapsed.TotalMilliseconds; + if (cur < 2000) + { + return; + } + frames++; + + if (cur - prev >= 995) + { + avgFps = 0.8 * avgFps + 0.2 * frames; + + Log.Verbose("AndroidGameView", "frames {0} elapsed {1}ms {2:F2} fps", + frames, + cur - prev, + avgFps); + + frames = 0; + prev = cur; + } + } + + protected void EnsureUndisposed() + { + if (disposed) + throw new ObjectDisposedException(""); + } + + protected void DestroyGLContext() + { + if (eglContext != null) + { + if (!egl.EglDestroyContext(eglDisplay, eglContext)) + throw new Exception("Could not destroy EGL context" + GetErrorAsString()); + eglContext = null; + } + if (eglDisplay != null) + { + if (!egl.EglTerminate(eglDisplay)) + throw new Exception("Could not terminate EGL connection" + GetErrorAsString()); + eglDisplay = null; + } + + glContextAvailable = false; + } + + protected void DestroyGLSurface() + { + if (!(eglSurface == null || eglSurface == EGL10.EglNoSurface)) + { + if (!egl.EglMakeCurrent(eglDisplay, EGL10.EglNoSurface, + EGL10.EglNoSurface, EGL10.EglNoContext)) + { + Log.Verbose("AndroidGameView", "Could not unbind EGL surface" + GetErrorAsString()); + } + + if (!egl.EglDestroySurface(eglDisplay, eglSurface)) + { + Log.Verbose("AndroidGameView", "Could not destroy EGL surface" + GetErrorAsString()); + } + } + eglSurface = null; + glSurfaceAvailable = false; + + } + + internal struct SurfaceConfig + { + public int Red; + public int Green; + public int Blue; + public int Alpha; + public int Depth; + public int Stencil; + + public int[] ToConfigAttribs() + { + List attribs = new List(); + if (Red != 0) + { + attribs.Add(EGL11.EglRedSize); + attribs.Add(Red); + } + if (Green != 0) + { + attribs.Add(EGL11.EglGreenSize); + attribs.Add(Green); + } + if (Blue != 0) + { + attribs.Add(EGL11.EglBlueSize); + attribs.Add(Blue); + } + if (Alpha != 0) + { + attribs.Add(EGL11.EglAlphaSize); + attribs.Add(Alpha); + } + if (Depth != 0) + { + attribs.Add(EGL11.EglDepthSize); + attribs.Add(Depth); + } + if (Stencil != 0) + { + attribs.Add(EGL11.EglStencilSize); + attribs.Add(Stencil); + } + attribs.Add(EGL11.EglRenderableType); + attribs.Add(4); + attribs.Add(EGL11.EglNone); + + return attribs.ToArray(); + } + + static int GetAttribute(EGLConfig config, IEGL10 egl, EGLDisplay eglDisplay,int attribute) + { + int[] data = new int[1]; + egl.EglGetConfigAttrib(eglDisplay, config, EGL11.EglRedSize, data); + return data[0]; + } + + public static SurfaceConfig FromEGLConfig (EGLConfig config, IEGL10 egl, EGLDisplay eglDisplay) + { + return new SurfaceConfig() + { + Red = GetAttribute(config, egl, eglDisplay, EGL11.EglRedSize), + Green = GetAttribute(config, egl, eglDisplay, EGL11.EglGreenSize), + Blue = GetAttribute(config, egl, eglDisplay, EGL11.EglBlueSize), + Alpha = GetAttribute(config, egl, eglDisplay, EGL11.EglAlphaSize), + Depth = GetAttribute(config, egl, eglDisplay, EGL11.EglDepthSize), + Stencil = GetAttribute(config, egl, eglDisplay, EGL11.EglStencilSize), + }; + } + + public override string ToString() + { + return string.Format("Red:{0} Green:{1} Blue:{2} Alpha:{3} Depth:{4} Stencil:{5}", Red, Green, Blue, Alpha, Depth, Stencil); + } + } + + protected void CreateGLContext() + { + lostglContext = false; + + egl = EGLContext.EGL.JavaCast(); + + eglDisplay = egl.EglGetDisplay(EGL10.EglDefaultDisplay); + if (eglDisplay == EGL10.EglNoDisplay) + throw new Exception("Could not get EGL display" + GetErrorAsString()); + + int[] version = new int[2]; + if (!egl.EglInitialize(eglDisplay, version)) + throw new Exception("Could not initialize EGL display" + GetErrorAsString()); + + int depth = 0; + int stencil = 0; + switch (_game.graphicsDeviceManager.PreferredDepthStencilFormat) + { + case DepthFormat.Depth16: + depth = 16; + break; + case DepthFormat.Depth24: + depth = 24; + break; + case DepthFormat.Depth24Stencil8: + depth = 24; + stencil = 8; + break; + case DepthFormat.None: + break; + } + + List configs = new List(); + if (depth > 0) + { + configs.Add(new SurfaceConfig() { Red = 8, Green = 8, Blue = 8, Alpha = 8, Depth = depth, Stencil = stencil }); + configs.Add(new SurfaceConfig() { Red = 5, Green = 6, Blue = 5, Depth = depth, Stencil = stencil }); + configs.Add(new SurfaceConfig() { Depth = depth, Stencil = stencil }); + if (depth > 16) + { + configs.Add(new SurfaceConfig() { Red = 8, Green = 8, Blue = 8, Alpha = 8, Depth = 16 }); + configs.Add(new SurfaceConfig() { Red = 5, Green = 6, Blue = 5, Depth = 16 }); + configs.Add(new SurfaceConfig() { Depth = 16 }); + } + configs.Add(new SurfaceConfig() { Red = 8, Green = 8, Blue = 8, Alpha = 8 }); + configs.Add(new SurfaceConfig() { Red = 5, Green = 6, Blue = 5 }); + } + else + { + configs.Add(new SurfaceConfig() { Red = 8, Green = 8, Blue = 8, Alpha = 8 }); + configs.Add(new SurfaceConfig() { Red = 5, Green = 6, Blue = 5 }); + } + configs.Add(new SurfaceConfig() { Red = 4, Green = 4, Blue = 4 }); + int[] numConfigs = new int[1]; + EGLConfig[] results = new EGLConfig[1]; + + if (!egl.EglGetConfigs(eglDisplay, null, 0, numConfigs)) { + throw new Exception("Could not get config count. " + GetErrorAsString()); + } + + EGLConfig[] cfgs = new EGLConfig[numConfigs[0]]; + egl.EglGetConfigs(eglDisplay, cfgs, numConfigs[0], numConfigs); + Log.Verbose("AndroidGameView", "Device Supports"); + foreach (var c in cfgs) { + Log.Verbose("AndroidGameView", string.Format(" {0}", SurfaceConfig.FromEGLConfig(c, egl, eglDisplay))); + } + + bool found = false; + numConfigs[0] = 0; + foreach (var config in configs) + { + Log.Verbose("AndroidGameView", string.Format("Checking Config : {0}", config)); + found = egl.EglChooseConfig(eglDisplay, config.ToConfigAttribs(), results, 1, numConfigs); + Log.Verbose("AndroidGameView", "EglChooseConfig returned {0} and {1}", found, numConfigs[0]); + if (!found || numConfigs[0] <= 0) + { + Log.Verbose("AndroidGameView", "Config not supported"); + continue; + } + Log.Verbose("AndroidGameView", string.Format("Selected Config : {0}", config)); + break; + } + + if (!found || numConfigs[0] <= 0) + throw new Exception("No valid EGL configs found" + GetErrorAsString()); + var createdVersion = new MonoGame.OpenGL.GLESVersion(); + foreach (var v in MonoGame.OpenGL.GLESVersion.GetSupportedGLESVersions ()) { + Log.Verbose("AndroidGameView", "Creating GLES {0} Context", v); + eglContext = egl.EglCreateContext(eglDisplay, results[0], EGL10.EglNoContext, v.GetAttributes()); + if (eglContext == null || eglContext == EGL10.EglNoContext) + { + Log.Verbose("AndroidGameView", string.Format("GLES {0} Not Supported. {1}", v, GetErrorAsString())); + eglContext = EGL10.EglNoContext; + continue; + } + createdVersion = v; + break; + } + if (eglContext == null || eglContext == EGL10.EglNoContext) + { + eglContext = null; + throw new Exception("Could not create EGL context" + GetErrorAsString()); + } + Log.Verbose("AndroidGameView", "Created GLES {0} Context", createdVersion); + eglConfig = results[0]; + glContextAvailable = true; + } + + private string GetErrorAsString() + { + switch (egl.EglGetError()) + { + case EGL10.EglSuccess: + return "Success"; + + case EGL10.EglNotInitialized: + return "Not Initialized"; + + case EGL10.EglBadAccess: + return "Bad Access"; + case EGL10.EglBadAlloc: + return "Bad Allocation"; + case EGL10.EglBadAttribute: + return "Bad Attribute"; + case EGL10.EglBadConfig: + return "Bad Config"; + case EGL10.EglBadContext: + return "Bad Context"; + case EGL10.EglBadCurrentSurface: + return "Bad Current Surface"; + case EGL10.EglBadDisplay: + return "Bad Display"; + case EGL10.EglBadMatch: + return "Bad Match"; + case EGL10.EglBadNativePixmap: + return "Bad Native Pixmap"; + case EGL10.EglBadNativeWindow: + return "Bad Native Window"; + case EGL10.EglBadParameter: + return "Bad Parameter"; + case EGL10.EglBadSurface: + return "Bad Surface"; + + default: + return "Unknown Error"; + } + } + + protected void CreateGLSurface() + { + if (!glSurfaceAvailable) + { + try + { + // If there is an existing surface, destroy the old one + DestroyGLSurface(); + + eglSurface = egl.EglCreateWindowSurface(eglDisplay, eglConfig, (Java.Lang.Object)this.Holder, null); + if (eglSurface == null || eglSurface == EGL10.EglNoSurface) + throw new Exception("Could not create EGL window surface" + GetErrorAsString()); + + if (!egl.EglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) + throw new Exception("Could not make EGL current" + GetErrorAsString()); + + glSurfaceAvailable = true; + + // Must set viewport after creation, the viewport has correct values in it already as we call it, but + // the surface is created after the correct viewport is already applied so we must do it again. + if (_game.GraphicsDevice != null) + _game.graphicsDeviceManager.ResetClientBounds(); + + if (MonoGame.OpenGL.GL.GetError == null) + MonoGame.OpenGL.GL.LoadEntryPoints(); + } + catch (Exception ex) + { + Log.Error("AndroidGameView", ex.ToString()); + glSurfaceAvailable = false; + } + } + } + + protected EGLSurface CreatePBufferSurface(EGLConfig config, int[] attribList) + { + IEGL10 egl = EGLContext.EGL.JavaCast(); + EGLSurface result = egl.EglCreatePbufferSurface(eglDisplay, config, attribList); + if (result == null || result == EGL10.EglNoSurface) + throw new Exception("EglCreatePBufferSurface"); + return result; + } + + protected void ContextSetInternal() + { + if (lostglContext) + { + if (_game.GraphicsDevice != null) + { + _game.GraphicsDevice.Initialize(); + + IsResuming = true; + if (_gameWindow.Resumer != null) + { + _gameWindow.Resumer.LoadContent(); + } + + // Reload textures on a different thread so the resumer can be drawn + System.Threading.Thread bgThread = new System.Threading.Thread( + o => + { + Android.Util.Log.Debug("MonoGame", "Begin reloading graphics content"); + Microsoft.Xna.Framework.Content.ContentManager.ReloadGraphicsContent(); + Android.Util.Log.Debug("MonoGame", "End reloading graphics content"); + + // DeviceReset events + _game.graphicsDeviceManager.OnDeviceReset(EventArgs.Empty); + _game.GraphicsDevice.OnDeviceReset(); + + IsResuming = false; + }); + + bgThread.Start(); + } + } + OnContextSet(EventArgs.Empty); + } + + protected void ContextLostInternal() + { + OnContextLost(EventArgs.Empty); + _game.graphicsDeviceManager.OnDeviceResetting(EventArgs.Empty); + if (_game.GraphicsDevice != null) + _game.GraphicsDevice.OnDeviceResetting(); + } + + protected virtual void OnContextLost(EventArgs eventArgs) + { + + } + + protected virtual void OnContextSet(EventArgs eventArgs) + { + + } + + protected virtual void OnUnload(EventArgs eventArgs) + { + + } + + protected virtual void OnLoad(EventArgs eventArgs) + { + + } + + protected virtual void OnStopped(EventArgs eventArgs) + { + + } + + #region Key and Motion + + public override bool OnKeyDown(Keycode keyCode, KeyEvent e) + { + bool handled = false; + if (GamePad.OnKeyDown(keyCode, e)) + return true; + + handled = Keyboard.KeyDown(keyCode); +#if !OUYA + // we need to handle the Back key here because it doesnt work any other way + if (keyCode == Keycode.Back) + { + GamePad.Back = true; + handled = true; + } +#endif + if (keyCode == Keycode.VolumeUp) + { + AudioManager audioManager = (AudioManager)Context.GetSystemService(Context.AudioService); + audioManager.AdjustStreamVolume(Stream.Music, Adjust.Raise, VolumeNotificationFlags.ShowUi); + return true; + } + + if (keyCode == Keycode.VolumeDown) + { + AudioManager audioManager = (AudioManager)Context.GetSystemService(Context.AudioService); + audioManager.AdjustStreamVolume(Stream.Music, Adjust.Lower, VolumeNotificationFlags.ShowUi); + return true; + } + + return handled; + } + + public override bool OnKeyUp(Keycode keyCode, KeyEvent e) + { + if (GamePad.OnKeyUp(keyCode, e)) + return true; + return Keyboard.KeyUp(keyCode); + } + + public override bool OnGenericMotionEvent(MotionEvent e) + { + if (GamePad.OnGenericMotionEvent(e)) + return true; + + return base.OnGenericMotionEvent(e); + } + + #endregion + + #region Properties + + private IEGL10 egl; + private EGLDisplay eglDisplay; + private EGLConfig eglConfig; + private EGLContext eglContext; + private EGLSurface eglSurface; + + /// The visibility of the window. Always returns true. + /// + /// The instance has been disposed + public virtual bool Visible + { + get + { + EnsureUndisposed(); + return true; + } + set + { + EnsureUndisposed(); + } + } + + /// The size of the current view. + /// A which is the size of the current view. + /// The instance has been disposed + public virtual System.Drawing.Size Size + { + get + { + EnsureUndisposed(); + return size; + } + set + { + EnsureUndisposed(); + if (size != value) + { + size = value; + OnResize(EventArgs.Empty); + } + } + } + + private void OnResize(EventArgs eventArgs) + { + + } + + #endregion + + public event FrameEvent RenderFrame; + public event FrameEvent UpdateFrame; + + public delegate void FrameEvent(object sender, FrameEventArgs e); + + public class FrameEventArgs : EventArgs + { + double elapsed; + + /// + /// Constructs a new FrameEventArgs instance. + /// + public FrameEventArgs() + { + } + + /// + /// Constructs a new FrameEventArgs instance. + /// + /// The amount of time that has elapsed since the previous event, in seconds. + public FrameEventArgs(double elapsed) + { + Time = elapsed; + } + + /// + /// Gets a that indicates how many seconds of time elapsed since the previous event. + /// + public double Time + { + get { return elapsed; } + internal set + { + if (value < 0) + throw new ArgumentOutOfRangeException(); + elapsed = value; + } + } + } + + public BackgroundContext CreateBackgroundContext() + { + return new BackgroundContext(this); + } + + public class BackgroundContext + { + + EGLContext eglContext; + MonoGameAndroidGameView view; + EGLSurface surface; + + public BackgroundContext(MonoGameAndroidGameView view) + { + this.view = view; + foreach (var v in MonoGame.OpenGL.GLESVersion.GetSupportedGLESVersions()) + { + eglContext = view.egl.EglCreateContext(view.eglDisplay, view.eglConfig, EGL10.EglNoContext, v.GetAttributes()); + if (eglContext == null || eglContext == EGL10.EglNoContext) + { + continue; + } + break; + } + if (eglContext == null || eglContext == EGL10.EglNoContext) + { + eglContext = null; + throw new Exception("Could not create EGL context" + view.GetErrorAsString()); + } + int[] pbufferAttribList = new int[] { EGL10.EglWidth, 64, EGL10.EglHeight, 64, EGL10.EglNone }; + surface = view.CreatePBufferSurface(view.eglConfig, pbufferAttribList); + if (surface == EGL10.EglNoSurface) + throw new Exception("Could not create Pbuffer Surface" + view.GetErrorAsString()); + } + + public void MakeCurrent() + { + view.ClearCurrent(); + view.egl.EglMakeCurrent(view.eglDisplay, surface, surface, eglContext); + } + } + } +} diff --git a/MonoGame.Framework/Android/OrientationListener.cs b/MonoGame.Framework/Android/OrientationListener.cs new file mode 100644 index 00000000000..e465e555a18 --- /dev/null +++ b/MonoGame.Framework/Android/OrientationListener.cs @@ -0,0 +1,55 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using Android.App; +using Android.Content; +using Android.Hardware; +using Android.Views; +using Android.Provider; + +namespace Microsoft.Xna.Framework +{ + internal class OrientationListener : OrientationEventListener + { + /// + /// Constructor. SensorDelay.Ui is passed to the base class as this orientation listener + /// is just used for flipping the screen orientation, therefore high frequency data is not required. + /// + public OrientationListener(Context context) + : base(context, SensorDelay.Ui) + { + } + + public override void OnOrientationChanged(int orientation) + { + if (orientation == OrientationEventListener.OrientationUnknown) + return; + + // Avoid changing orientation whilst the screen is locked + if (ScreenReceiver.ScreenLocked) + return; + + // Check if screen orientation is locked by user: if it's locked, do not change orientation. + try + { + if (Settings.System.GetInt(Application.Context.ContentResolver, "accelerometer_rotation") == 0) + return; + } + catch (Settings.SettingNotFoundException) + { + // Do nothing (or log warning?). In case android API or Xamarin do not support this Android system property. + } + + var disporientation = AndroidCompatibility.GetAbsoluteOrientation(orientation); + + // Only auto-rotate if target orientation is supported and not current + AndroidGameWindow gameWindow = (AndroidGameWindow)Game.Instance.Window; + if ((gameWindow.GetEffectiveSupportedOrientations() & disporientation) != 0 && + disporientation != gameWindow.CurrentOrientation) + { + gameWindow.SetOrientation(disporientation, true); + } + } + } +} \ No newline at end of file diff --git a/MonoGame.Framework/Android/ResumeManager.cs b/MonoGame.Framework/Android/ResumeManager.cs new file mode 100644 index 00000000000..7fb8d9d4bb0 --- /dev/null +++ b/MonoGame.Framework/Android/ResumeManager.cs @@ -0,0 +1,112 @@ +#region License +/* +Microsoft Public License (Ms-PL) +MonoGame - Copyright © 2012 The MonoGame Team + +All rights reserved. + +This license governs use of the accompanying software. If you use the software, you accept this license. If you do not +accept the license, do not use the software. + +1. Definitions +The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under +U.S. copyright law. + +A "contribution" is the original software, or any additions or changes to the software. +A "contributor" is any person that distributes its contribution under this license. +"Licensed patents" are a contributor's patent claims that read directly on its contribution. + +2. Grant of Rights +(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. +(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. + +3. Conditions and Limitations +(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. +(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, +your patent license from such contributor to the software ends automatically. +(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution +notices that are present in the software. +(D) If you distribute any portion of the software in source code form, you may do so only under this license by including +a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object +code form, you may only do so under a license that complies with this license. +(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees +or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent +permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular +purpose and non-infringement. +*/ +#endregion License + +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; + +namespace Microsoft.Xna.Framework +{ + /// + /// A default implementation of IResumeManager. Loads a user specified + /// image file (eg png) and draws it the middle of the screen. + /// + /// Example usage in Game.Initialise(): + /// + /// #if ANDROID + /// this.Window.SetResumer(new ResumeManager(this.Services, + /// spriteBatch, + /// "UI/ResumingTexture", + /// 1.0f, 0.01f)); + /// #endif + /// + public class ResumeManager : IResumeManager + { + ContentManager content; + GraphicsDevice device; + SpriteBatch spriteBatch; + string resumeTextureName; + Texture2D resumeTexture; + float rotation; + float scale; + float rotateSpeed; + + public ResumeManager(IServiceProvider services, + SpriteBatch spriteBatch, + string resumeTextureName, + float scale, + float rotateSpeed) + { + this.content = new ContentManager(services, "Content"); + this.device = ((IGraphicsDeviceService)services.GetService(typeof(IGraphicsDeviceService))).GraphicsDevice; + this.spriteBatch = spriteBatch; + this.resumeTextureName = resumeTextureName; + this.scale = scale; + this.rotateSpeed = rotateSpeed; + } + + public virtual void LoadContent() + { + content.Unload(); + resumeTexture = content.Load(resumeTextureName); + } + + public virtual void Draw() + { + rotation += rotateSpeed; + + int sw = device.PresentationParameters.BackBufferWidth; + int sh = device.PresentationParameters.BackBufferHeight; + int tw = resumeTexture.Width; + int th = resumeTexture.Height; + + // Draw the resume texture in the middle of the screen and make it spin + spriteBatch.Begin(); + spriteBatch.Draw(resumeTexture, + new Vector2(sw / 2, sh / 2), + null, Color.White, rotation, + new Vector2(tw / 2, th / 2), + scale, SpriteEffects.None, 0.0f); + + spriteBatch.End(); + } + } +} \ No newline at end of file diff --git a/MonoGame.Framework/Android/ScreenReciever.cs b/MonoGame.Framework/Android/ScreenReciever.cs new file mode 100644 index 00000000000..24832b8742b --- /dev/null +++ b/MonoGame.Framework/Android/ScreenReciever.cs @@ -0,0 +1,51 @@ +using System; +using Android.Content; +using Microsoft.Xna.Framework.Media; +using Android.App; + +namespace Microsoft.Xna.Framework +{ + internal class ScreenReceiver : BroadcastReceiver + { + public static bool ScreenLocked; + + public override void OnReceive(Context context, Intent intent) + { + Android.Util.Log.Info("MonoGame", intent.Action.ToString()); + if(intent.Action == Intent.ActionScreenOff) + { + OnLocked(); + } + else if(intent.Action == Intent.ActionScreenOn) + { + // If the user turns the screen on just after it has automatically turned off, + // the keyguard will not have had time to activate and the ActionUserPreset intent + // will not be broadcast. We need to check if the lock is currently active + // and if not re-enable the game related functions. + // http://stackoverflow.com/questions/4260794/how-to-tell-if-device-is-sleeping + KeyguardManager keyguard = (KeyguardManager)context.GetSystemService(Context.KeyguardService); + if (!keyguard.InKeyguardRestrictedInputMode()) + OnUnlocked(); + } + else if(intent.Action == Intent.ActionUserPresent) + { + // This intent is broadcast when the user unlocks the phone + OnUnlocked(); + } + } + + private void OnLocked() + { + ScreenReceiver.ScreenLocked = true; + MediaPlayer.IsMuted = true; + } + + private void OnUnlocked() + { + ScreenReceiver.ScreenLocked = false; + MediaPlayer.IsMuted = false; + ((AndroidGameWindow)Game.Instance.Window).GameView.Resume(); + } + } +} + diff --git a/MonoGame.Framework/Audio/AudioChannels.cs b/MonoGame.Framework/Audio/AudioChannels.cs new file mode 100644 index 00000000000..cb85d44e10a --- /dev/null +++ b/MonoGame.Framework/Audio/AudioChannels.cs @@ -0,0 +1,20 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Audio +{ + /// + /// Represents how many channels are used in the audio data. + /// + public enum AudioChannels + { + /// Single channel. + Mono = 1, + /// Two channels. + Stereo = 2 + } +} + diff --git a/MonoGame.Framework/Audio/AudioEmitter.cs b/MonoGame.Framework/Audio/AudioEmitter.cs new file mode 100644 index 00000000000..70a79ed2065 --- /dev/null +++ b/MonoGame.Framework/Audio/AudioEmitter.cs @@ -0,0 +1,87 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Xna.Framework.Audio +{ + /// + /// Represents a 3D audio emitter. Used to simulate 3D audio effects. + /// + public class AudioEmitter + { + /// Initializes a new AudioEmitter instance. + public AudioEmitter () + { + _dopplerScale = 1.0f; + Forward = Vector3.Forward; + Position = Vector3.Zero; + Up = Vector3.Up; + Velocity = Vector3.Zero; + } + + private float _dopplerScale; + + /// Gets or sets a scale applied to the Doppler effect between the AudioEmitter and an AudioListener. + /// + /// Defaults to 1.0 + /// A value of 1.0 leaves the Doppler effect unmodified. + /// + public float DopplerScale + { + get + { + return _dopplerScale; + } + + set + { + if (value < 0.0f) + throw new ArgumentOutOfRangeException("AudioEmitter.DopplerScale must be greater than or equal to 0.0f"); + + _dopplerScale = value; + } + } + + /// Gets or sets the emitter's forward vector. + /// + /// Defaults to Vector3.Forward. (new Vector3(0, 0, -1)) + /// Used with AudioListener.Velocity to calculate Doppler values. + /// The Forward and Up values must be orthonormal. + /// + public Vector3 Forward { + get; + set; + } + + /// Gets or sets the position of this emitter. + public Vector3 Position { + get; + set; + } + + /// Gets or sets the emitter's Up vector. + /// + /// Defaults to Vector3.Up. (new Vector3(0, -1, 1)). + /// The Up and Forward vectors must be orthonormal. + /// + public Vector3 Up { + get; + set; + } + + /// Gets or sets the emitter's velocity vector. + /// + /// Defaults to Vector3.Zero. + /// This value is only used when calculating Doppler values. + /// + public Vector3 Velocity { + get; + set; + } + + } +} diff --git a/MonoGame.Framework/Audio/AudioListener.cs b/MonoGame.Framework/Audio/AudioListener.cs new file mode 100644 index 00000000000..efd1ee3adeb --- /dev/null +++ b/MonoGame.Framework/Audio/AudioListener.cs @@ -0,0 +1,69 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Xna.Framework.Audio +{ + /// + /// Represents a 3D audio listener. Used when simulating 3D Audio. + /// + public class AudioListener + { + public AudioListener () + { + Forward = Vector3.Forward; + Position = Vector3.Zero; + Up = Vector3.Up; + Velocity = Vector3.Zero; + } + + /// Gets or sets the listener's forward vector. + /// + /// Defaults to Vector3.Forward. (new Vector3(0, 0, -1)) + /// Used with AudioListener.Velocity and AudioEmitter.Velocity to calculate Doppler values. + /// The Forward and Up vectors must be orthonormal. + /// + public Vector3 Forward { + get; + set; + } + + /// Gets or sets the listener's position. + /// + /// Defaults to Vector3.Zero. + /// + public Vector3 Position { + get; + set; + } + + /// + /// Gets or sets the listener's up vector.. + /// + /// + /// Defaults to Vector3.Up (New Vector3(0, -1, 0)). + /// Used with AudioListener.Velocity and AudioEmitter.Velocity to calculate Doppler values. + /// The values of the Forward and Up vectors must be orthonormal. + /// + public Vector3 Up { + get; + set; + } + + /// Gets or sets the listener's velocity vector. + /// + /// Defaults to Vector3.Zero. + /// Scaled by DopplerScale to calculate the Doppler effect value applied to a Cue. + /// This value is only used to calculate Doppler values. + /// + public Vector3 Velocity { + get; + set; + } + } +} + diff --git a/MonoGame.Framework/Audio/AudioLoader.cs b/MonoGame.Framework/Audio/AudioLoader.cs new file mode 100644 index 00000000000..4b16d9f6095 --- /dev/null +++ b/MonoGame.Framework/Audio/AudioLoader.cs @@ -0,0 +1,648 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.IO; +using MonoGame.OpenAL; + +namespace Microsoft.Xna.Framework.Audio +{ + internal static class AudioLoader + { + internal const int FormatPcm = 1; + internal const int FormatMsAdpcm = 2; + internal const int FormatIeee = 3; + internal const int FormatIma4 = 17; + + public static ALFormat GetSoundFormat(int format, int channels, int bits) + { + switch (format) + { + case FormatPcm: + // PCM + switch (channels) + { + case 1: return bits == 8 ? ALFormat.Mono8 : ALFormat.Mono16; + case 2: return bits == 8 ? ALFormat.Stereo8 : ALFormat.Stereo16; + default: throw new NotSupportedException("The specified channel count is not supported."); + } + case FormatMsAdpcm: + // Microsoft ADPCM + switch (channels) + { + case 1: return ALFormat.MonoMSAdpcm; + case 2: return ALFormat.StereoMSAdpcm; + default: throw new NotSupportedException("The specified channel count is not supported."); + } + case FormatIeee: + // IEEE Float + switch (channels) + { + case 1: return ALFormat.MonoFloat32; + case 2: return ALFormat.StereoFloat32; + default: throw new NotSupportedException("The specified channel count is not supported."); + } + case FormatIma4: + // IMA4 ADPCM + switch (channels) + { + case 1: return ALFormat.MonoIma4; + case 2: return ALFormat.StereoIma4; + default: throw new NotSupportedException("The specified channel count is not supported."); + } + default: + throw new NotSupportedException("The specified sound format (" + format.ToString() + ") is not supported."); + } + } + + // Converts block alignment in bytes to sample alignment, primarily for compressed formats + // Calculation of sample alignment from http://kcat.strangesoft.net/openal-extensions/SOFT_block_alignment.txt + public static int SampleAlignment(ALFormat format, int blockAlignment) + { + switch (format) + { + case ALFormat.MonoIma4: + return (blockAlignment - 4) / 4 * 8 + 1; + case ALFormat.StereoIma4: + return (blockAlignment / 2 - 4) / 4 * 8 + 1; + case ALFormat.MonoMSAdpcm: + return (blockAlignment - 7) * 2 + 2; + case ALFormat.StereoMSAdpcm: + return (blockAlignment / 2 - 7) * 2 + 2; + } + return 0; + } + + /// + /// Load a WAV file from stream. + /// + /// The stream positioned at the start of the WAV file. + /// Gets the OpenAL format enumeration value. + /// Gets the frequency or sample rate. + /// Gets the number of channels. + /// Gets the block alignment, important for compressed sounds. + /// Gets the number of bits per sample. + /// Gets the number of samples per block. + /// Gets the total number of samples. + /// The byte buffer containing the waveform data or compressed blocks. + public static byte[] Load(Stream stream, out ALFormat format, out int frequency, out int channels, out int blockAlignment, out int bitsPerSample, out int samplesPerBlock, out int sampleCount) + { + byte[] audioData = null; + + using (BinaryReader reader = new BinaryReader(stream)) + { + // for now we'll only support wave files + audioData = LoadWave(reader, out format, out frequency, out channels, out blockAlignment, out bitsPerSample, out samplesPerBlock, out sampleCount); + } + + return audioData; + } + + private static byte[] LoadWave(BinaryReader reader, out ALFormat format, out int frequency, out int channels, out int blockAlignment, out int bitsPerSample, out int samplesPerBlock, out int sampleCount) + { + byte[] audioData = null; + + //header + string signature = new string(reader.ReadChars(4)); + if (signature != "RIFF") + throw new ArgumentException("Specified stream is not a wave file."); + reader.ReadInt32(); // riff_chunk_size + + string wformat = new string(reader.ReadChars(4)); + if (wformat != "WAVE") + throw new ArgumentException("Specified stream is not a wave file."); + + int audioFormat = 0; + channels = 0; + bitsPerSample = 0; + format = ALFormat.Mono16; + frequency = 0; + blockAlignment = 0; + samplesPerBlock = 0; + sampleCount = 0; + + // WAVE header + while (audioData == null) + { + string chunkType = new string(reader.ReadChars(4)); + int chunkSize = reader.ReadInt32(); + switch (chunkType) + { + case "fmt ": + { + audioFormat = reader.ReadInt16(); // 2 + channels = reader.ReadInt16(); // 4 + frequency = reader.ReadInt32(); // 8 + int byteRate = reader.ReadInt32(); // 12 + blockAlignment = (int)reader.ReadInt16(); // 14 + bitsPerSample = reader.ReadInt16(); // 16 + + // Read extra data if present + if (chunkSize > 16) + { + int extraDataSize = reader.ReadInt16(); + if (audioFormat == FormatIma4) + { + samplesPerBlock = reader.ReadInt16(); + extraDataSize -= 2; + } + if (extraDataSize > 0) + { + if (reader.BaseStream.CanSeek) + reader.BaseStream.Seek(extraDataSize, SeekOrigin.Current); + else + { + for (int i = 0; i < extraDataSize; ++i) + reader.ReadByte(); + } + } + } + } + break; + case "fact": + if (audioFormat == FormatIma4) + { + sampleCount = reader.ReadInt32() * channels; + chunkSize -= 4; + } + // Skip any remaining chunk data + if (chunkSize > 0) + { + if (reader.BaseStream.CanSeek) + reader.BaseStream.Seek(chunkSize, SeekOrigin.Current); + else + { + for (int i = 0; i < chunkSize; ++i) + reader.ReadByte(); + } + } + break; + case "data": + audioData = reader.ReadBytes(chunkSize); + break; + default: + // Skip this chunk + if (reader.BaseStream.CanSeek) + reader.BaseStream.Seek(chunkSize, SeekOrigin.Current); + else + { + for (int i = 0; i < chunkSize; ++i) + reader.ReadByte(); + } + break; + } + } + + // Calculate fields we didn't read from the file + format = GetSoundFormat(audioFormat, channels, bitsPerSample); + + if (samplesPerBlock == 0) + { + samplesPerBlock = SampleAlignment(format, blockAlignment); + } + + if (sampleCount == 0) + { + switch (audioFormat) + { + case FormatIma4: + case FormatMsAdpcm: + sampleCount = ((audioData.Length / blockAlignment) * samplesPerBlock) + SampleAlignment(format, audioData.Length % blockAlignment); + break; + case FormatPcm: + case FormatIeee: + sampleCount = audioData.Length / ((channels * bitsPerSample) / 8); + break; + default: + throw new InvalidDataException("Unhandled WAV format " + format.ToString()); + } + } + + return audioData; + } + + // Convert buffer containing 24-bit signed PCM wav data to a 16-bit signed PCM buffer + internal static unsafe byte[] Convert24To16(byte[] data, int offset, int count) + { + if ((offset + count > data.Length) || ((count % 3) != 0)) + throw new ArgumentException("Invalid 24-bit PCM data received"); + // Sample count includes both channels if stereo + var sampleCount = count / 3; + var outData = new byte[sampleCount * sizeof(short)]; + fixed (byte* src = &data[offset]) + { + fixed (byte* dst = &outData[0]) + { + var srcIndex = 0; + var dstIndex = 0; + for (int i = 0; i < sampleCount; ++i) + { + // Drop the least significant byte from the 24-bit sample to get the 16-bit sample + dst[dstIndex] = src[srcIndex + 1]; + dst[dstIndex + 1] = src[srcIndex + 2]; + dstIndex += 2; + srcIndex += 3; + } + } + } + return outData; + } + + // Convert buffer containing IEEE 32-bit float wav data to a 16-bit signed PCM buffer + internal static unsafe byte[] ConvertFloatTo16(byte[] data, int offset, int count) + { + if ((offset + count > data.Length) || ((count % 4) != 0)) + throw new ArgumentException("Invalid 32-bit float PCM data received"); + // Sample count includes both channels if stereo + var sampleCount = count / 4; + var outData = new byte[sampleCount * sizeof(short)]; + fixed (byte* src = &data[offset]) + { + float* f = (float*)src; + fixed (byte* dst = &outData[0]) + { + byte* d = dst; + for (int i = 0; i < sampleCount; ++i) + { + short s = (short)(*f * 32767.0f); + *d++ = (byte)(s & 0xff); + *d++ = (byte)(s >> 8); + ++f; + } + } + } + return outData; + } + + #region IMA4 decoding + + // Step table + static int[] stepTable = new int[] + { + 7, 8, 9, 10, 11, 12, 13, 14, + 16, 17, 19, 21, 23, 25, 28, 31, + 34, 37, 41, 45, 50, 55, 60, 66, + 73, 80, 88, 97, 107, 118, 130, 143, + 157, 173, 190, 209, 230, 253, 279, 307, + 337, 371, 408, 449, 494, 544, 598, 658, + 724, 796, 876, 963, 1060, 1166, 1282, 1411, + 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, + 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, + 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, + 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, + 32767 + }; + + // Step index tables + static int[] indexTable = new int[] + { + // ADPCM data size is 4 + -1, -1, -1, -1, 2, 4, 6, 8, + -1, -1, -1, -1, 2, 4, 6, 8 + }; + + struct ImaState + { + public int predictor; + public int stepIndex; + } + + static int AdpcmImaWavExpandNibble(ref ImaState channel, int nibble) + { + int diff = stepTable[channel.stepIndex] >> 3; + if ((nibble & 0x04) != 0) + diff += stepTable[channel.stepIndex]; + if ((nibble & 0x02) != 0) + diff += stepTable[channel.stepIndex] >> 1; + if ((nibble & 0x01) != 0) + diff += stepTable[channel.stepIndex] >> 2; + if ((nibble & 0x08) != 0) + channel.predictor -= diff; + else + channel.predictor += diff; + + if (channel.predictor < -32768) + channel.predictor = -32768; + else if (channel.predictor > 32767) + channel.predictor = 32767; + + channel.stepIndex += indexTable[nibble]; + + if (channel.stepIndex < 0) + channel.stepIndex = 0; + else if (channel.stepIndex > 88) + channel.stepIndex = 88; + + return channel.predictor; + } + + // Convert buffer containing IMA/ADPCM wav data to a 16-bit signed PCM buffer + internal static byte[] ConvertIma4ToPcm(byte[] buffer, int offset, int count, int channels, int blockAlignment) + { + ImaState channel0 = new ImaState(); + ImaState channel1 = new ImaState(); + + int sampleCountFullBlock = ((blockAlignment / channels) - 4) / 4 * 8 + 1; + int sampleCountLastBlock = 0; + if ((count % blockAlignment) > 0) + sampleCountLastBlock = (((count % blockAlignment) / channels) - 4) / 4 * 8 + 1; + int sampleCount = ((count / blockAlignment) * sampleCountFullBlock) + sampleCountLastBlock; + var samples = new byte[sampleCount * sizeof(short) * channels]; + int sampleOffset = 0; + + while (count > 0) + { + int blockSize = blockAlignment; + if (count < blockSize) + blockSize = count; + count -= blockAlignment; + + channel0.predictor = buffer[offset++]; + channel0.predictor |= buffer[offset++] << 8; + if ((channel0.predictor & 0x8000) != 0) + channel0.predictor -= 0x10000; + channel0.stepIndex = buffer[offset++]; + if (channel0.stepIndex > 88) + channel0.stepIndex = 88; + offset++; + int index = sampleOffset * 2; + samples[index] = (byte)channel0.predictor; + samples[index + 1] = (byte)(channel0.predictor >> 8); + ++sampleOffset; + + if (channels == 2) + { + channel1.predictor = buffer[offset++]; + channel1.predictor |= buffer[offset++] << 8; + if ((channel1.predictor & 0x8000) != 0) + channel1.predictor -= 0x10000; + channel1.stepIndex = buffer[offset++]; + if (channel1.stepIndex > 88) + channel1.stepIndex = 88; + offset++; + index = sampleOffset * 2; + samples[index] = (byte)channel1.predictor; + samples[index + 1] = (byte)(channel1.predictor >> 8); + ++sampleOffset; + } + + if (channels == 2) + { + for (int nibbles = 2 * (blockSize - 8); nibbles > 0; nibbles -= 16) + { + for (int i = 0; i < 4; i++) + { + index = (sampleOffset + i * 4) * 2; + int sample = AdpcmImaWavExpandNibble(ref channel0, buffer[offset + i] & 0x0f); + samples[index] = (byte)sample; + samples[index + 1] = (byte)(sample >> 8); + + index = (sampleOffset + i * 4 + 2) * 2; + sample = AdpcmImaWavExpandNibble(ref channel0, buffer[offset + i] >> 4); + samples[index] = (byte)sample; + samples[index + 1] = (byte)(sample >> 8); + } + offset += 4; + + for (int i = 0; i < 4; i++) + { + index = (sampleOffset + i * 4 + 1) * 2; + int sample = AdpcmImaWavExpandNibble(ref channel1, buffer[offset + i] & 0x0f); + samples[index] = (byte)sample; + samples[index + 1] = (byte)(sample >> 8); + + index = (sampleOffset + i * 4 + 3) * 2; + sample = AdpcmImaWavExpandNibble(ref channel1, buffer[offset + i] >> 4); + samples[index] = (byte)sample; + samples[index + 1] = (byte)(sample >> 8); + } + offset += 4; + sampleOffset += 16; + } + } + else + { + for (int nibbles = 2 * (blockSize - 4); nibbles > 0; nibbles -= 2) + { + index = (sampleOffset * 2); + int b = buffer[offset]; + int sample = AdpcmImaWavExpandNibble(ref channel0, b & 0x0f); + samples[index] = (byte)sample; + samples[index + 1] = (byte)(sample >> 8); + index += 2; + sample = AdpcmImaWavExpandNibble(ref channel0, b >> 4); + samples[index] = (byte)sample; + samples[index + 1] = (byte)(sample >> 8); + + sampleOffset += 2; + ++offset; + } + } + } + + return samples; + } + + #endregion + + #region MS-ADPCM decoding + + static int[] adaptationTable = new int[] + { + 230, 230, 230, 230, 307, 409, 512, 614, + 768, 614, 512, 409, 307, 230, 230, 230 + }; + + static int[] adaptationCoeff1 = new int[] + { + 256, 512, 0, 192, 240, 460, 392 + }; + + static int[] adaptationCoeff2 = new int[] + { + 0, -256, 0, 64, 0, -208, -232 + }; + + struct MsAdpcmState + { + public int delta; + public int sample1; + public int sample2; + public int coeff1; + public int coeff2; + } + + static int AdpcmMsExpandNibble(ref MsAdpcmState channel, int nibble) + { + int nibbleSign = nibble - (((nibble & 0x08) != 0) ? 0x10 : 0); + int predictor = ((channel.sample1 * channel.coeff1) + (channel.sample2 * channel.coeff2)) / 256 + (nibbleSign * channel.delta); + + if (predictor < -32768) + predictor = -32768; + else if (predictor > 32767) + predictor = 32767; + + channel.sample2 = channel.sample1; + channel.sample1 = predictor; + + channel.delta = (adaptationTable[nibble] * channel.delta) / 256; + if (channel.delta < 16) + channel.delta = 16; + + return predictor; + } + + // Convert buffer containing MS-ADPCM wav data to a 16-bit signed PCM buffer + internal static byte[] ConvertMsAdpcmToPcm(byte[] buffer, int offset, int count, int channels, int blockAlignment) + { + MsAdpcmState channel0 = new MsAdpcmState(); + MsAdpcmState channel1 = new MsAdpcmState(); + int blockPredictor; + + int sampleCountFullBlock = ((blockAlignment / channels) - 7) * 2 + 2; + int sampleCountLastBlock = 0; + if ((count % blockAlignment) > 0) + sampleCountLastBlock = (((count % blockAlignment) / channels) - 7) * 2 + 2; + int sampleCount = ((count / blockAlignment) * sampleCountFullBlock) + sampleCountLastBlock; + var samples = new byte[sampleCount * sizeof(short) * channels]; + int sampleOffset = 0; + + bool stereo = channels == 2; + + while (count > 0) + { + int blockSize = blockAlignment; + if (count < blockSize) + blockSize = count; + count -= blockAlignment; + + int totalSamples = ((blockSize / channels) - 7) * 2 + 2; + if (totalSamples < 2) + break; + + int offsetStart = offset; + blockPredictor = buffer[offset]; + ++offset; + if (blockPredictor > 6) + blockPredictor = 6; + channel0.coeff1 = adaptationCoeff1[blockPredictor]; + channel0.coeff2 = adaptationCoeff2[blockPredictor]; + if (stereo) + { + blockPredictor = buffer[offset]; + ++offset; + if (blockPredictor > 6) + blockPredictor = 6; + channel1.coeff1 = adaptationCoeff1[blockPredictor]; + channel1.coeff2 = adaptationCoeff2[blockPredictor]; + } + + channel0.delta = buffer[offset]; + channel0.delta |= buffer[offset + 1] << 8; + if ((channel0.delta & 0x8000) != 0) + channel0.delta -= 0x10000; + offset += 2; + if (stereo) + { + channel1.delta = buffer[offset]; + channel1.delta |= buffer[offset + 1] << 8; + if ((channel1.delta & 0x8000) != 0) + channel1.delta -= 0x10000; + offset += 2; + } + + channel0.sample1 = buffer[offset]; + channel0.sample1 |= buffer[offset + 1] << 8; + if ((channel0.sample1 & 0x8000) != 0) + channel0.sample1 -= 0x10000; + offset += 2; + if (stereo) + { + channel1.sample1 = buffer[offset]; + channel1.sample1 |= buffer[offset + 1] << 8; + if ((channel1.sample1 & 0x8000) != 0) + channel1.sample1 -= 0x10000; + offset += 2; + } + + channel0.sample2 = buffer[offset]; + channel0.sample2 |= buffer[offset + 1] << 8; + if ((channel0.sample2 & 0x8000) != 0) + channel0.sample2 -= 0x10000; + offset += 2; + if (stereo) + { + channel1.sample2 = buffer[offset]; + channel1.sample2 |= buffer[offset + 1] << 8; + if ((channel1.sample2 & 0x8000) != 0) + channel1.sample2 -= 0x10000; + offset += 2; + } + + if (stereo) + { + samples[sampleOffset] = (byte)channel0.sample2; + samples[sampleOffset + 1] = (byte)(channel0.sample2 >> 8); + samples[sampleOffset + 2] = (byte)channel1.sample2; + samples[sampleOffset + 3] = (byte)(channel1.sample2 >> 8); + samples[sampleOffset + 4] = (byte)channel0.sample1; + samples[sampleOffset + 5] = (byte)(channel0.sample1 >> 8); + samples[sampleOffset + 6] = (byte)channel1.sample1; + samples[sampleOffset + 7] = (byte)(channel1.sample1 >> 8); + sampleOffset += 8; + } + else + { + samples[sampleOffset] = (byte)channel0.sample2; + samples[sampleOffset + 1] = (byte)(channel0.sample2 >> 8); + samples[sampleOffset + 2] = (byte)channel0.sample1; + samples[sampleOffset + 3] = (byte)(channel0.sample1 >> 8); + sampleOffset += 4; + } + + blockSize -= (offset - offsetStart); + if (stereo) + { + for (int i = 0; i < blockSize; ++i) + { + int nibbles = buffer[offset]; + + int sample = AdpcmMsExpandNibble(ref channel0, nibbles >> 4); + samples[sampleOffset] = (byte)sample; + samples[sampleOffset + 1] = (byte)(sample >> 8); + + sample = AdpcmMsExpandNibble(ref channel1, nibbles & 0x0f); + samples[sampleOffset + 2] = (byte)sample; + samples[sampleOffset + 3] = (byte)(sample >> 8); + + sampleOffset += 4; + ++offset; + } + } + else + { + for (int i = 0; i < blockSize; ++i) + { + int nibbles = buffer[offset]; + + int sample = AdpcmMsExpandNibble(ref channel0, nibbles >> 4); + samples[sampleOffset] = (byte)sample; + samples[sampleOffset + 1] = (byte)(sample >> 8); + + sample = AdpcmMsExpandNibble(ref channel0, nibbles & 0x0f); + samples[sampleOffset + 2] = (byte)sample; + samples[sampleOffset + 3] = (byte)(sample >> 8); + + sampleOffset += 4; + ++offset; + } + } + } + + return samples; + } + + #endregion + } +} diff --git a/MonoGame.Framework/Audio/AudioUtil.cs b/MonoGame.Framework/Audio/AudioUtil.cs new file mode 100644 index 00000000000..0672a828aea --- /dev/null +++ b/MonoGame.Framework/Audio/AudioUtil.cs @@ -0,0 +1,43 @@ +using System; +using System.IO; + +namespace Microsoft.Xna.Framework.Audio +{ + internal static class AudioUtil + { + /// + /// Takes WAV data and appends a header to it. + /// + internal static byte[] FormatWavData(byte[] buffer, int sampleRate, int channels) + { + //buffer should contain 16-bit PCM wave data + short bitsPerSample = 16; + + using (var mStream = new MemoryStream(44+buffer.Length)) + using (var writer = new BinaryWriter(mStream)) + { + writer.Write("RIFF".ToCharArray()); //chunk id + writer.Write((int)(36 + buffer.Length)); //chunk size + writer.Write("WAVE".ToCharArray()); //RIFF type + + writer.Write("fmt ".ToCharArray()); //chunk id + writer.Write((int)16); //format header size + writer.Write((short)1); //format (PCM) + writer.Write((short)channels); + writer.Write((int)sampleRate); + short blockAlign = (short)((bitsPerSample / 8) * (int)channels); + writer.Write((int)(sampleRate * blockAlign)); //byte rate + writer.Write((short)blockAlign); + writer.Write((short)bitsPerSample); + + writer.Write("data".ToCharArray()); //chunk id + writer.Write((int)buffer.Length); //data size + + writer.Write(buffer); + + return mStream.ToArray(); + } + } + } +} + diff --git a/MonoGame.Framework/Audio/DynamicSoundEffectInstance.OpenAL.cs b/MonoGame.Framework/Audio/DynamicSoundEffectInstance.OpenAL.cs new file mode 100644 index 00000000000..9d6dcc5d1e6 --- /dev/null +++ b/MonoGame.Framework/Audio/DynamicSoundEffectInstance.OpenAL.cs @@ -0,0 +1,154 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using MonoGame.OpenAL; + +namespace Microsoft.Xna.Framework.Audio +{ + public sealed partial class DynamicSoundEffectInstance : SoundEffectInstance + { + private Queue _queuedBuffers; + private ALFormat _format; + + private void PlatformCreate() + { + _format = _channels == AudioChannels.Mono ? ALFormat.Mono16 : ALFormat.Stereo16; + InitializeSound(); + + SourceId = controller.ReserveSource(); + HasSourceId = true; + + _queuedBuffers = new Queue(); + } + + private int PlatformGetPendingBufferCount() + { + return _queuedBuffers.Count; + } + + private void PlatformPlay() + { + AL.GetError(); + + // Ensure that the source is not looped (due to source recycling) + AL.Source(SourceId, ALSourceb.Looping, false); + ALHelper.CheckError("Failed to set source loop state."); + + AL.SourcePlay(SourceId); + ALHelper.CheckError("Failed to play the source."); + } + + private void PlatformPause() + { + AL.GetError(); + AL.SourcePause(SourceId); + ALHelper.CheckError("Failed to pause the source."); + } + + private void PlatformResume() + { + AL.GetError(); + AL.SourcePlay(SourceId); + ALHelper.CheckError("Failed to play the source."); + } + + private void PlatformStop() + { + AL.GetError(); + AL.SourceStop(SourceId); + ALHelper.CheckError("Failed to stop the source."); + + // Remove all queued buffers + AL.Source(SourceId, ALSourcei.Buffer, 0); + while (_queuedBuffers.Count > 0) + { + var buffer = _queuedBuffers.Dequeue(); + buffer.Dispose(); + } + } + + private void PlatformSubmitBuffer(byte[] buffer, int offset, int count) + { + // Get a buffer + OALSoundBuffer oalBuffer = new OALSoundBuffer(); + + // Bind the data + if (offset == 0) + { + oalBuffer.BindDataBuffer(buffer, _format, count, _sampleRate); + } + else + { + // BindDataBuffer does not support offset + var offsetBuffer = new byte[count]; + Array.Copy(buffer, offset, offsetBuffer, 0, count); + oalBuffer.BindDataBuffer(offsetBuffer, _format, count, _sampleRate); + } + + // Queue the buffer + AL.SourceQueueBuffer(SourceId, oalBuffer.OpenALDataBuffer); + ALHelper.CheckError(); + _queuedBuffers.Enqueue(oalBuffer); + + // If the source has run out of buffers, restart it + var sourceState = AL.GetSourceState(SourceId); + if (_state == SoundState.Playing && sourceState == ALSourceState.Stopped) + { + AL.SourcePlay(SourceId); + ALHelper.CheckError("Failed to resume source playback."); + } + } + + private void PlatformDispose(bool disposing) + { + // Stop the source and bind null buffer so that it can be recycled + AL.GetError(); + if (AL.IsSource(SourceId)) + { + AL.SourceStop(SourceId); + AL.Source(SourceId, ALSourcei.Buffer, 0); + ALHelper.CheckError("Failed to stop the source."); + controller.RecycleSource(SourceId); + } + + if (disposing) + { + while (_queuedBuffers.Count > 0) + { + var buffer = _queuedBuffers.Dequeue(); + buffer.Dispose(); + } + + DynamicSoundEffectInstanceManager.RemoveInstance(this); + } + } + + private void PlatformUpdateQueue() + { + // Get the completed buffers + AL.GetError(); + int numBuffers; + AL.GetSource(SourceId, ALGetSourcei.BuffersProcessed, out numBuffers); + ALHelper.CheckError("Failed to get processed buffer count."); + + // Unqueue them + if (numBuffers > 0) + { + AL.SourceUnqueueBuffers(SourceId, numBuffers); + ALHelper.CheckError("Failed to unqueue buffers."); + for (int i = 0; i < numBuffers; i++) + { + var buffer = _queuedBuffers.Dequeue(); + buffer.Dispose(); + } + } + + // Raise the event for each removed buffer, if needed + for (int i = 0; i < numBuffers; i++) + CheckBufferCount(); + } + } +} diff --git a/MonoGame.Framework/Audio/DynamicSoundEffectInstance.Web.cs b/MonoGame.Framework/Audio/DynamicSoundEffectInstance.Web.cs new file mode 100644 index 00000000000..f72119f068b --- /dev/null +++ b/MonoGame.Framework/Audio/DynamicSoundEffectInstance.Web.cs @@ -0,0 +1,48 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Audio +{ + public sealed partial class DynamicSoundEffectInstance : SoundEffectInstance + { + private void PlatformCreate() + { + } + + private int PlatformGetPendingBufferCount() + { + return 0; + } + + private void PlatformPlay() + { + } + + private void PlatformPause() + { + } + + private void PlatformResume() + { + } + + private void PlatformStop() + { + } + + private void PlatformSubmitBuffer(byte[] buffer, int offset, int count) + { + } + + private void PlatformDispose(bool disposing) + { + } + + private void PlatformUpdateQueue() + { + } + } +} diff --git a/MonoGame.Framework/Audio/DynamicSoundEffectInstance.XAudio.cs b/MonoGame.Framework/Audio/DynamicSoundEffectInstance.XAudio.cs new file mode 100644 index 00000000000..abf12226156 --- /dev/null +++ b/MonoGame.Framework/Audio/DynamicSoundEffectInstance.XAudio.cs @@ -0,0 +1,113 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using MonoGame.Utilities; +using SharpDX; +using SharpDX.Multimedia; +using SharpDX.XAudio2; + +namespace Microsoft.Xna.Framework.Audio +{ + public sealed partial class DynamicSoundEffectInstance : SoundEffectInstance + { + private Queue _queuedBuffers; + private Queue _pooledBuffers; + private static ByteBufferPool _bufferPool = new ByteBufferPool(); + + private void PlatformCreate() + { + _format = new WaveFormat(_sampleRate, (int)_channels); + _voice = new SourceVoice(SoundEffect.Device, _format, true); + _voice.BufferEnd += OnBufferEnd; + _queuedBuffers = new Queue(); + _pooledBuffers = new Queue(); + } + + private int PlatformGetPendingBufferCount() + { + return _queuedBuffers.Count; + } + + private void PlatformPlay() + { + _voice.Start(); + } + + private void PlatformPause() + { + _voice.Stop(); + } + + private void PlatformResume() + { + _voice.Start(); + } + + private void PlatformStop() + { + _voice.Stop(); + + // Dequeue all the submitted buffers + _voice.FlushSourceBuffers(); + + while (_queuedBuffers.Count > 0) + { + var buffer = _queuedBuffers.Dequeue(); + buffer.Stream.Dispose(); + _bufferPool.Return(_pooledBuffers.Dequeue()); + } + } + + private void PlatformSubmitBuffer(byte[] buffer, int offset, int count) + { + // we need to copy so datastream does not pin the buffer that the user might modify later + byte[] pooledBuffer; + pooledBuffer = _bufferPool.Get(count); + _pooledBuffers.Enqueue(pooledBuffer); + Buffer.BlockCopy(buffer, offset, pooledBuffer, 0, count); + + var stream = DataStream.Create(pooledBuffer, true, false, 0, true); + var audioBuffer = new AudioBuffer(stream); + audioBuffer.AudioBytes = count; + + _voice.SubmitSourceBuffer(audioBuffer, null); + _queuedBuffers.Enqueue(audioBuffer); + } + + private void PlatformUpdateQueue() + { + // The XAudio implementation utilizes callbacks, so no work here. + } + + private void PlatformDispose(bool disposing) + { + if (disposing) + { + while (_queuedBuffers.Count > 0) + { + var buffer = _queuedBuffers.Dequeue(); + buffer.Stream.Dispose(); + _bufferPool.Return(_pooledBuffers.Dequeue()); + } + } + // _voice is disposed by SoundEffectInstance.PlatformDispose + } + + private void OnBufferEnd(IntPtr obj) + { + // Release the buffer + if (_queuedBuffers.Count > 0) + { + var buffer = _queuedBuffers.Dequeue(); + buffer.Stream.Dispose(); + _bufferPool.Return(_pooledBuffers.Dequeue()); + } + + CheckBufferCount(); + } + + } +} diff --git a/MonoGame.Framework/Audio/DynamicSoundEffectInstance.cs b/MonoGame.Framework/Audio/DynamicSoundEffectInstance.cs new file mode 100644 index 00000000000..3899732722c --- /dev/null +++ b/MonoGame.Framework/Audio/DynamicSoundEffectInstance.cs @@ -0,0 +1,312 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Xna.Framework.Audio +{ + /// + /// A for which the audio buffer is provided by the game at run time. + /// + public sealed partial class DynamicSoundEffectInstance : SoundEffectInstance + { + #region Public Properties + + /// + /// This value has no effect on DynamicSoundEffectInstance. + /// It may not be set. + /// + public override bool IsLooped + { + get + { + return false; + } + + set + { + AssertNotDisposed(); + if (value == true) + throw new InvalidOperationException("IsLooped cannot be set true. Submit looped audio data to implement looping."); + } + } + + public override SoundState State + { + get + { + AssertNotDisposed(); + return _state; + } + } + + /// + /// Returns the number of audio buffers queued for playback. + /// + public int PendingBufferCount + { + get + { + AssertNotDisposed(); + return PlatformGetPendingBufferCount(); + } + } + + /// + /// The event that occurs when the number of queued audio buffers is less than or equal to 2. + /// + /// + /// This event may occur when is called or during playback when a buffer is completed. + /// + public event EventHandler BufferNeeded; + + #endregion + + private const int TargetPendingBufferCount = 3; + private int _buffersNeeded; + private int _sampleRate; + private AudioChannels _channels; + private SoundState _state; + + #region Public Constructor + + /// Sample rate, in Hertz (Hz). + /// Number of channels (mono or stereo). + public DynamicSoundEffectInstance(int sampleRate, AudioChannels channels) + { + SoundEffect.Initialize(); + if (SoundEffect._systemState != SoundEffect.SoundSystemState.Initialized) + throw new NoAudioHardwareException("Audio has failed to initialize. Call SoundEffect.Initialize() before sound operation to get more specific errors."); + + if ((sampleRate < 8000) || (sampleRate > 48000)) + throw new ArgumentOutOfRangeException("sampleRate"); + if ((channels != AudioChannels.Mono) && (channels != AudioChannels.Stereo)) + throw new ArgumentOutOfRangeException("channels"); + + _sampleRate = sampleRate; + _channels = channels; + _state = SoundState.Stopped; + PlatformCreate(); + + // This instance is added to the pool so that its volume reflects master volume changes + // and it contributes to the playing instances limit, but the source/voice is not owned by the pool. + _isPooled = false; + _isDynamic = true; + } + + #endregion + + #region Public Functions + + /// + /// Returns the duration of an audio buffer of the specified size, based on the settings of this instance. + /// + /// Size of the buffer, in bytes. + /// The playback length of the buffer. + public TimeSpan GetSampleDuration(int sizeInBytes) + { + AssertNotDisposed(); + return SoundEffect.GetSampleDuration(sizeInBytes, _sampleRate, _channels); + } + + /// + /// Returns the size, in bytes, of a buffer of the specified duration, based on the settings of this instance. + /// + /// The playback length of the buffer. + /// The data size of the buffer, in bytes. + public int GetSampleSizeInBytes(TimeSpan duration) + { + AssertNotDisposed(); + return SoundEffect.GetSampleSizeInBytes(duration, _sampleRate, _channels); + } + + /// + /// Plays or resumes the DynamicSoundEffectInstance. + /// + public override void Play() + { + AssertNotDisposed(); + + if (_state != SoundState.Playing) + { + // Ensure that the volume reflects master volume, which is done by the setter. + Volume = Volume; + + // Add the instance to the pool + if (!SoundEffectInstancePool.SoundsAvailable) + throw new InstancePlayLimitException(); + SoundEffectInstancePool.Remove(this); + + PlatformPlay(); + _state = SoundState.Playing; + + CheckBufferCount(); + DynamicSoundEffectInstanceManager.AddInstance(this); + } + } + + /// + /// Pauses playback of the DynamicSoundEffectInstance. + /// + public override void Pause() + { + AssertNotDisposed(); + PlatformPause(); + _state = SoundState.Paused; + } + + /// + /// Resumes playback of the DynamicSoundEffectInstance. + /// + public override void Resume() + { + AssertNotDisposed(); + + if (_state != SoundState.Playing) + { + Volume = Volume; + + // Add the instance to the pool + if (!SoundEffectInstancePool.SoundsAvailable) + throw new InstancePlayLimitException(); + SoundEffectInstancePool.Remove(this); + } + + PlatformResume(); + _state = SoundState.Playing; + } + + /// + /// Immediately stops playing the DynamicSoundEffectInstance. + /// + /// + /// Calling this also releases all queued buffers. + /// + public override void Stop() + { + Stop(true); + } + + /// + /// Stops playing the DynamicSoundEffectInstance. + /// If the parameter is false, this call has no effect. + /// + /// + /// Calling this also releases all queued buffers. + /// + /// When set to false, this call has no effect. + public override void Stop(bool immediate) + { + AssertNotDisposed(); + + if (immediate) + { + DynamicSoundEffectInstanceManager.RemoveInstance(this); + + PlatformStop(); + _state = SoundState.Stopped; + + SoundEffectInstancePool.Add(this); + } + } + + /// + /// Queues an audio buffer for playback. + /// + /// + /// The buffer length must conform to alignment requirements for the audio format. + /// + /// The buffer containing PCM audio data. + public void SubmitBuffer(byte[] buffer) + { + AssertNotDisposed(); + + if (buffer.Length == 0) + throw new ArgumentException("Buffer may not be empty."); + + // Ensure that the buffer length matches alignment. + // The data must be 16-bit, so the length is a multiple of 2 (mono) or 4 (stereo). + var sampleSize = 2 * (int)_channels; + if (buffer.Length % sampleSize != 0) + throw new ArgumentException("Buffer length does not match format alignment."); + + SubmitBuffer(buffer, 0, buffer.Length); + } + + /// + /// Queues an audio buffer for playback. + /// + /// + /// The buffer length must conform to alignment requirements for the audio format. + /// + /// The buffer containing PCM audio data. + /// The starting position of audio data. + /// The amount of bytes to use. + public void SubmitBuffer(byte[] buffer, int offset, int count) + { + AssertNotDisposed(); + + if ((buffer == null) || (buffer.Length == 0)) + throw new ArgumentException("Buffer may not be null or empty."); + if (count <= 0) + throw new ArgumentException("Number of bytes must be greater than zero."); + if ((offset + count) > buffer.Length) + throw new ArgumentException("Buffer is shorter than the specified number of bytes from the offset."); + + // Ensure that the buffer length and start position match alignment. + var sampleSize = 2 * (int)_channels; + if (count % sampleSize != 0) + throw new ArgumentException("Number of bytes does not match format alignment."); + if (offset % sampleSize != 0) + throw new ArgumentException("Offset into the buffer does not match format alignment."); + + PlatformSubmitBuffer(buffer, offset, count); + } + + #endregion + + #region Nonpublic Functions + + private void AssertNotDisposed() + { + if (IsDisposed) + throw new ObjectDisposedException(null); + } + + protected override void Dispose(bool disposing) + { + PlatformDispose(disposing); + base.Dispose(disposing); + } + + private void CheckBufferCount() + { + if ((PendingBufferCount < TargetPendingBufferCount) && (_state == SoundState.Playing)) + _buffersNeeded++; + } + + internal void UpdateQueue() + { + // Update the buffers + PlatformUpdateQueue(); + + // Raise the event + var bufferNeededHandler = BufferNeeded; + + if (bufferNeededHandler != null) + { + var eventCount = (_buffersNeeded < 3) ? _buffersNeeded : 3; + for (var i = 0; i < eventCount; i++) + { + bufferNeededHandler(this, EventArgs.Empty); + } + } + + _buffersNeeded = 0; + } + + #endregion + } +} diff --git a/MonoGame.Framework/Audio/DynamicSoundEffectInstanceManager.cs b/MonoGame.Framework/Audio/DynamicSoundEffectInstanceManager.cs new file mode 100644 index 00000000000..89d8f38371b --- /dev/null +++ b/MonoGame.Framework/Audio/DynamicSoundEffectInstanceManager.cs @@ -0,0 +1,64 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Xna.Framework.Audio +{ + /// + /// Handles the buffer events of all DynamicSoundEffectInstance instances. + /// + internal static class DynamicSoundEffectInstanceManager + { + private static readonly List _playingInstances; + + static DynamicSoundEffectInstanceManager() + { + _playingInstances = new List(); + } + + public static void AddInstance(DynamicSoundEffectInstance instance) + { + var weakRef = new WeakReference(instance); + _playingInstances.Add(weakRef); + } + + public static void RemoveInstance(DynamicSoundEffectInstance instance) + { + for (int i = _playingInstances.Count - 1; i >= 0; i--) + { + if (_playingInstances[i].Target == instance) + { + _playingInstances.RemoveAt(i); + return; + } + } + } + + /// + /// Updates buffer queues of the currently playing instances. + /// + /// + /// XNA posts events always on the main thread. + /// + public static void UpdatePlayingInstances() + { + for (int i = _playingInstances.Count - 1; i >= 0; i--) + { + var target = _playingInstances[i].Target as DynamicSoundEffectInstance; + if (target != null) + { + if (!target.IsDisposed) + target.UpdateQueue(); + } + else + { + // The instance has been disposed. + _playingInstances.RemoveAt(i); + } + } + } + } +} diff --git a/MonoGame.Framework/Audio/InstancePlayLimitException.cs b/MonoGame.Framework/Audio/InstancePlayLimitException.cs new file mode 100644 index 00000000000..06b42d27380 --- /dev/null +++ b/MonoGame.Framework/Audio/InstancePlayLimitException.cs @@ -0,0 +1,26 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; + +namespace Microsoft.Xna.Framework.Audio +{ + /// + /// The exception thrown when the system attempts to play more SoundEffectInstances than allotted. + /// + /// + /// Most platforms have a hard limit on how many sounds can be played simultaneously. This exception is thrown when that limit is exceeded. + /// + [DataContract] +#if WINDOWS_UAP + public sealed class InstancePlayLimitException : Exception +#else + public sealed class InstancePlayLimitException : ExternalException +#endif + { + } +} + diff --git a/MonoGame.Framework/Audio/Microphone.Default.cs b/MonoGame.Framework/Audio/Microphone.Default.cs new file mode 100644 index 00000000000..38d1907f843 --- /dev/null +++ b/MonoGame.Framework/Audio/Microphone.Default.cs @@ -0,0 +1,35 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Xna.Framework.Audio +{ + /// + /// Provides microphones capture features. + /// + public sealed partial class Microphone + { + internal void PlatformStart() + { + throw new NotImplementedException(); + } + + internal void PlatformStop() + { + throw new NotImplementedException(); + } + + internal void Update() + { + throw new NotImplementedException(); + } + + internal int PlatformGetData(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + } +} diff --git a/MonoGame.Framework/Audio/Microphone.OpenAL.cs b/MonoGame.Framework/Audio/Microphone.OpenAL.cs new file mode 100644 index 00000000000..5e1b21c7011 --- /dev/null +++ b/MonoGame.Framework/Audio/Microphone.OpenAL.cs @@ -0,0 +1,165 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using MonoGame.Utilities; + +#if OPENAL +using MonoGame.OpenAL; +#if IOS || MONOMAC +using AudioToolbox; +using AudioUnit; +using AVFoundation; +#endif +#endif + +namespace Microsoft.Xna.Framework.Audio +{ + /// + /// Provides microphones capture features. + /// + public sealed partial class Microphone + { + private IntPtr _captureDevice = IntPtr.Zero; + + internal void CheckALCError(string operation) + { + AlcError error = Alc.GetErrorForDevice(_captureDevice); + + if (error == AlcError.NoError) + return; + + string errorFmt = "OpenAL Error: {0}"; + + throw new NoMicrophoneConnectedException(String.Format("{0} - {1}", + operation, + string.Format(errorFmt, error))); + } + + internal static void PopulateCaptureDevices() + { + // clear microphones + if (_allMicrophones != null) + _allMicrophones.Clear(); + else + _allMicrophones = new List(); + + _default = null; + + // default device + string defaultDevice = Alc.GetString(IntPtr.Zero, AlcGetString.CaptureDefaultDeviceSpecifier); + +#if true //DESKTOPGL + // enumerating capture devices + IntPtr deviceList = Alc.alcGetString(IntPtr.Zero, (int)AlcGetString.CaptureDeviceSpecifier); + + // Marshal native UTF-8 character array to .NET string + // The native string is a null-char separated list of known capture device specifiers ending with an empty string + + while (true) + { + var deviceIdentifier = InteropHelpers.Utf8ToString(deviceList); + + if (string.IsNullOrEmpty(deviceIdentifier)) + break; + + var microphone = new Microphone(deviceIdentifier); + _allMicrophones.Add(microphone); + if (deviceIdentifier == defaultDevice) + _default = microphone; + + // increase the offset, add one extra for the terminator + deviceList += deviceIdentifier.Length + 1; + } +#else + // Xamarin platforms don't provide an handle to alGetString that allow to marshal string arrays + // so we're basically only adding the default microphone + Microphone microphone = new Microphone(defaultDevice); + _allMicrophones.Add(microphone); + _default = microphone; +#endif + } + + internal void PlatformStart() + { + if (_state == MicrophoneState.Started) + return; + + _captureDevice = Alc.CaptureOpenDevice( + Name, + (uint)_sampleRate, + ALFormat.Mono16, + GetSampleSizeInBytes(_bufferDuration)); + + CheckALCError("Failed to open capture device."); + + if (_captureDevice != IntPtr.Zero) + { + Alc.CaptureStart(_captureDevice); + CheckALCError("Failed to start capture."); + + _state = MicrophoneState.Started; + } + else + { + throw new NoMicrophoneConnectedException("Failed to open capture device."); + } + } + + internal void PlatformStop() + { + if (_state == MicrophoneState.Started) + { + Alc.CaptureStop(_captureDevice); + CheckALCError("Failed to stop capture."); + Alc.CaptureCloseDevice(_captureDevice); + CheckALCError("Failed to close capture device."); + _captureDevice = IntPtr.Zero; + } + _state = MicrophoneState.Stopped; + } + + internal int GetQueuedSampleCount() + { + if (_state == MicrophoneState.Stopped || BufferReady == null) + return 0; + + int[] values = new int[1]; + Alc.GetInteger(_captureDevice, AlcGetInteger.CaptureSamples, 1, values); + + CheckALCError("Failed to query capture samples."); + + return values[0]; + } + + internal void Update() + { + if (GetQueuedSampleCount() > 0) + { + BufferReady.Invoke(this, EventArgs.Empty); + } + } + + internal int PlatformGetData(byte[] buffer, int offset, int count) + { + int sampleCount = GetQueuedSampleCount(); + sampleCount = Math.Min(count / 2, sampleCount); // 16bit adjust + + if (sampleCount > 0) + { + GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned); + Alc.CaptureSamples(_captureDevice, handle.AddrOfPinnedObject() + offset, sampleCount); + handle.Free(); + + CheckALCError("Failed to capture samples."); + + return sampleCount * 2; // 16bit adjust + } + + return 0; + } + } +} diff --git a/MonoGame.Framework/Audio/Microphone.cs b/MonoGame.Framework/Audio/Microphone.cs new file mode 100644 index 00000000000..0e31926ffea --- /dev/null +++ b/MonoGame.Framework/Audio/Microphone.cs @@ -0,0 +1,232 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Microsoft.Xna.Framework.Audio +{ + /// + /// Microphone state. + /// + public enum MicrophoneState + { + Started, + Stopped + } + + /// + /// Provides microphones capture features. + /// + public sealed partial class Microphone + { + #region Internal Constructors + + internal Microphone() + { + + } + + internal Microphone(string name) + { + Name = name; + } + + #endregion + + #region Public Fields + + /// + /// Returns the friendly name of the microphone. + /// + public readonly string Name; + + #endregion + + #region Public Properties + + private TimeSpan _bufferDuration = TimeSpan.FromMilliseconds(1000.0); + + /// + /// Gets or sets the capture buffer duration. This value must be greater than 100 milliseconds, lower than 1000 milliseconds, and must be 10 milliseconds aligned (BufferDuration % 10 == 10). + /// + public TimeSpan BufferDuration + { + get { return _bufferDuration; } + set + { + if (value.TotalMilliseconds < 100 || value.TotalMilliseconds > 1000) + throw new ArgumentOutOfRangeException("Buffer duration must be a value between 100 and 1000 milliseconds."); + if (value.TotalMilliseconds % 10 != 0) + throw new ArgumentOutOfRangeException("Buffer duration must be 10ms aligned (BufferDuration % 10 == 0)"); + _bufferDuration = value; + } + } + + // always true on mobile, this can't be queried on any platform (it was most probably only set to true if the headset was plugged in an XInput controller) +#if IOS || ANDROID + private const bool _isHeadset = true; +#else + private const bool _isHeadset = false; +#endif + /// + /// Determines if the microphone is a wired headset. + /// Note: XNA could know if a headset microphone was plugged in an Xbox 360 controller but MonoGame can't. + /// Hence, this is always true on mobile platforms, and always false otherwise. + /// + public bool IsHeadset + { + get { return _isHeadset; } + } + + private int _sampleRate = 44100; // XNA default is 44100, don't know if it supports any other rates + + /// + /// Returns the sample rate of the captured audio. + /// Note: default value is 44100hz + /// + public int SampleRate + { + get { return _sampleRate; } + } + + private MicrophoneState _state = MicrophoneState.Stopped; + + /// + /// Returns the state of the Microphone. + /// + public MicrophoneState State + { + get { return _state; } + } + + #endregion + + #region Static Members + + private static List _allMicrophones = null; + + /// + /// Returns all compatible microphones. + /// + public static ReadOnlyCollection All + { + get + { + SoundEffect.Initialize(); + if (_allMicrophones == null) + _allMicrophones = new List(); + return new ReadOnlyCollection(_allMicrophones); + } + } + + private static Microphone _default = null; + + /// + /// Returns the default microphone. + /// + public static Microphone Default + { + get { SoundEffect.Initialize(); return _default; } + } + + #endregion + + #region Public Methods + + /// + /// Returns the duration based on the size of the buffer (assuming 16-bit PCM data). + /// + /// Size, in bytes + /// TimeSpan of the duration. + public TimeSpan GetSampleDuration(int sizeInBytes) + { + // this should be 10ms aligned + // this assumes 16bit mono data + return SoundEffect.GetSampleDuration(sizeInBytes, _sampleRate, AudioChannels.Mono); + } + + /// + /// Returns the size, in bytes, of the array required to hold the specified duration of 16-bit PCM data. + /// + /// TimeSpan of the duration of the sample. + /// Size, in bytes, of the buffer. + public int GetSampleSizeInBytes(TimeSpan duration) + { + // this should be 10ms aligned + // this assumes 16bit mono data + return SoundEffect.GetSampleSizeInBytes(duration, _sampleRate, AudioChannels.Mono); + } + + /// + /// Starts microphone capture. + /// + public void Start() + { + PlatformStart(); + } + + /// + /// Stops microphone capture. + /// + public void Stop() + { + PlatformStop(); + } + + /// + /// Gets the latest available data from the microphone. + /// + /// Buffer, in bytes, of the captured data (16-bit PCM). + /// The buffer size, in bytes, of the captured data. + public int GetData(byte[] buffer) + { + return GetData(buffer, 0, buffer.Length); + } + + /// + /// Gets the latest available data from the microphone. + /// + /// Buffer, in bytes, of the captured data (16-bit PCM). + /// Byte offset. + /// Amount, in bytes. + /// The buffer size, in bytes, of the captured data. + public int GetData(byte[] buffer, int offset, int count) + { + return PlatformGetData(buffer, offset, count); + } + + #endregion + + #region Public Events + + /// + /// Event fired when the audio data are available. + /// + public event EventHandler BufferReady; + + #endregion + + #region Static Methods + + internal static void UpdateMicrophones() + { + // querying all running microphones for new samples available + if (_allMicrophones != null) + for (int i = 0; i < _allMicrophones.Count; i++) + _allMicrophones[i].Update(); + } + + internal static void StopMicrophones() + { + // stopping all running microphones before shutting down audio devices + if (_allMicrophones != null) + for (int i = 0; i < _allMicrophones.Count; i++) + _allMicrophones[i].Stop(); + } + + #endregion + } +} diff --git a/MonoGame.Framework/Audio/NoAudioHardwareException.cs b/MonoGame.Framework/Audio/NoAudioHardwareException.cs new file mode 100644 index 00000000000..665122dbaef --- /dev/null +++ b/MonoGame.Framework/Audio/NoAudioHardwareException.cs @@ -0,0 +1,36 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Runtime.InteropServices; +using System.Runtime.Serialization; + +namespace Microsoft.Xna.Framework.Audio +{ + + /// + /// The exception thrown when no audio hardware is present, or driver issues are detected. + /// + [DataContract] +#if WINDOWS_UAP + public sealed class NoAudioHardwareException : Exception +#else + public sealed class NoAudioHardwareException : ExternalException +#endif + { + /// A message describing the error. + public NoAudioHardwareException(string msg) + : base(msg) + { + } + + /// A message describing the error. + /// The exception that is the underlying cause of the current exception. If not null, the current exception is raised in a try/catch block that handled the innerException. + public NoAudioHardwareException(string msg, Exception innerException) + : base(msg, innerException) + { + } + } +} + diff --git a/MonoGame.Framework/Audio/NoMicrophoneConnectedException.cs b/MonoGame.Framework/Audio/NoMicrophoneConnectedException.cs new file mode 100644 index 00000000000..5dac7079e83 --- /dev/null +++ b/MonoGame.Framework/Audio/NoMicrophoneConnectedException.cs @@ -0,0 +1,31 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Runtime.Serialization; + +namespace Microsoft.Xna.Framework.Audio +{ + + /// + /// The exception thrown when no audio hardware is present, or driver issues are detected. + /// + [DataContract] + public sealed class NoMicrophoneConnectedException : Exception + { + /// A message describing the error. + public NoMicrophoneConnectedException(string msg) + : base(msg) + { + } + + /// A message describing the error. + /// The exception that is the underlying cause of the current exception. If not null, the current exception is raised in a try/catch block that handled the innerException. + public NoMicrophoneConnectedException(string msg, Exception innerException) + : base(msg, innerException) + { + } + } +} + diff --git a/MonoGame.Framework/Audio/OALSoundBuffer.cs b/MonoGame.Framework/Audio/OALSoundBuffer.cs new file mode 100644 index 00000000000..dd7b8992bac --- /dev/null +++ b/MonoGame.Framework/Audio/OALSoundBuffer.cs @@ -0,0 +1,99 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using MonoGame.OpenAL; + +namespace Microsoft.Xna.Framework.Audio +{ + internal class OALSoundBuffer : IDisposable + { + int openALDataBuffer; + ALFormat openALFormat; + int dataSize; + bool _isDisposed; + + public OALSoundBuffer() + { + AL.GenBuffers(1, out openALDataBuffer); + ALHelper.CheckError("Failed to generate OpenAL data buffer."); + } + + ~OALSoundBuffer() + { + Dispose(false); + } + + public int OpenALDataBuffer + { + get + { + return openALDataBuffer; + } + } + + public double Duration + { + get; + set; + } + + public void BindDataBuffer(byte[] dataBuffer, ALFormat format, int size, int sampleRate, int sampleAlignment = 0) + { + if ((format == ALFormat.MonoMSAdpcm || format == ALFormat.StereoMSAdpcm) && !OpenALSoundController.Instance.SupportsAdpcm) + throw new InvalidOperationException("MS-ADPCM is not supported by this OpenAL driver"); + if ((format == ALFormat.MonoIma4 || format == ALFormat.StereoIma4) && !OpenALSoundController.Instance.SupportsIma4) + throw new InvalidOperationException("IMA/ADPCM is not supported by this OpenAL driver"); + + openALFormat = format; + dataSize = size; + int unpackedSize = 0; + + if (sampleAlignment > 0) + { + AL.Bufferi(openALDataBuffer, ALBufferi.UnpackBlockAlignmentSoft, sampleAlignment); + ALHelper.CheckError("Failed to fill buffer."); + } + + AL.BufferData(openALDataBuffer, openALFormat, dataBuffer, size, sampleRate); + ALHelper.CheckError("Failed to fill buffer."); + + int bits, channels; + Duration = -1; + AL.GetBuffer(openALDataBuffer, ALGetBufferi.Bits, out bits); + ALHelper.CheckError("Failed to get buffer bits"); + AL.GetBuffer(openALDataBuffer, ALGetBufferi.Channels, out channels); + ALHelper.CheckError("Failed to get buffer channels"); + AL.GetBuffer(openALDataBuffer, ALGetBufferi.Size, out unpackedSize); + ALHelper.CheckError("Failed to get buffer size"); + Duration = (float)(unpackedSize / ((bits / 8) * channels)) / (float)sampleRate; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { + // Clean up managed objects + } + // Release unmanaged resources + if (AL.IsBuffer(openALDataBuffer)) + { + ALHelper.CheckError("Failed to fetch buffer state."); + AL.DeleteBuffers(1, ref openALDataBuffer); + ALHelper.CheckError("Failed to delete buffer."); + } + + _isDisposed = true; + } + } + } +} diff --git a/MonoGame.Framework/Audio/OggStream.cs b/MonoGame.Framework/Audio/OggStream.cs new file mode 100644 index 00000000000..5f6064ce4be --- /dev/null +++ b/MonoGame.Framework/Audio/OggStream.cs @@ -0,0 +1,529 @@ +// This code originated from: +// +// http://theinstructionlimit.com/ogg-streaming-using-opentk-and-nvorbis +// https://github.com/renaudbedard/nvorbis/ +// +// It was released to the public domain by the author (Renaud Bedard). +// No other license is intended or required. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using NVorbis; +using MonoGame.OpenAL; + +namespace Microsoft.Xna.Framework.Audio +{ + internal class OggStream : IDisposable + { + const int DefaultBufferCount = 3; + + internal readonly object stopMutex = new object(); + internal readonly object prepareMutex = new object(); + + internal readonly int alSourceId; + internal readonly int[] alBufferIds; + + readonly int alFilterId; + readonly string oggFileName; + + internal VorbisReader Reader { get; private set; } + internal bool Ready { get; private set; } + internal bool Preparing { get; private set; } + + public Action FinishedAction { get; private set; } + public int BufferCount { get; private set; } + + public OggStream(string filename, Action finishedAction = null, int bufferCount = DefaultBufferCount) + { + oggFileName = filename; + FinishedAction = finishedAction; + BufferCount = bufferCount; + + alBufferIds = AL.GenBuffers(bufferCount); + ALHelper.CheckError("Failed to generate buffers."); + alSourceId = OpenALSoundController.Instance.ReserveSource(); + + if (OggStreamer.Instance.XRam.IsInitialized) + { + OggStreamer.Instance.XRam.SetBufferMode(BufferCount, ref alBufferIds[0], XRamExtension.XRamStorage.Hardware); + ALHelper.CheckError("Failed to activate Xram."); + } + + Volume = 1; + + if (OggStreamer.Instance.Efx.IsInitialized) + { + alFilterId = OggStreamer.Instance.Efx.GenFilter(); + ALHelper.CheckError("Failed to generate Efx filter."); + OggStreamer.Instance.Efx.Filter(alFilterId, EfxFilteri.FilterType, (int)EfxFilterType.Lowpass); + ALHelper.CheckError("Failed to set Efx filter type."); + OggStreamer.Instance.Efx.Filter(alFilterId, EfxFilterf.LowpassGain, 1); + ALHelper.CheckError("Failed to set Efx filter value."); + LowPassHFGain = 1; + } + } + + public void Prepare() + { + if (Preparing) return; + + var state = AL.GetSourceState(alSourceId); + ALHelper.CheckError("Failed to get source state."); + + lock (stopMutex) + { + switch (state) + { + case ALSourceState.Playing: + case ALSourceState.Paused: + return; + + case ALSourceState.Stopped: + lock (prepareMutex) + { + Close(); + Empty(); + } + break; + } + + if (!Ready) + { + lock (prepareMutex) + { + Preparing = true; + Open(precache: true); + } + } + } + } + + public void Play() + { + var state = AL.GetSourceState(alSourceId); + ALHelper.CheckError("Failed to get source state."); + + switch (state) + { + case ALSourceState.Playing: return; + case ALSourceState.Paused: + Resume(); + return; + } + + Prepare(); + + AL.SourcePlay(alSourceId); + ALHelper.CheckError("Failed to play source."); + + Preparing = false; + + OggStreamer.Instance.AddStream(this); + } + + public void Pause() + { + var state = AL.GetSourceState(alSourceId); + ALHelper.CheckError("Failed to get source state."); + if (state != ALSourceState.Playing) + return; + + OggStreamer.Instance.RemoveStream(this); + AL.SourcePause(alSourceId); + ALHelper.CheckError("Failed to pause source."); + } + + public void Resume() + { + var state = AL.GetSourceState(alSourceId); + ALHelper.CheckError("Failed to get source state."); + if (state != ALSourceState.Paused) + return; + + OggStreamer.Instance.AddStream(this); + AL.SourcePlay(alSourceId); + ALHelper.CheckError("Failed to play source."); + } + + public void Stop() + { + var state = AL.GetSourceState(alSourceId); + ALHelper.CheckError("Failed to get source state."); + if (state == ALSourceState.Playing || state == ALSourceState.Paused) + StopPlayback(); + + lock (stopMutex) + { + OggStreamer.Instance.RemoveStream(this); + + lock (prepareMutex) + { + if (state != ALSourceState.Initial) + Empty(); // force the queued buffers to be unqueued to avoid issues on Mac + } + } + AL.Source(alSourceId, ALSourcei.Buffer, 0); + ALHelper.CheckError("Failed to free source from buffers."); + } + + public void SeekToPosition(TimeSpan pos) + { + Reader.DecodedTime = pos; + AL.SourceStop(alSourceId); + ALHelper.CheckError("Failed to stop source."); + } + + public TimeSpan GetPosition() + { + if (Reader == null) + return TimeSpan.Zero; + + return Reader.DecodedTime; + } + + public TimeSpan GetLength() + { + return Reader.TotalTime; + } + + float lowPassHfGain; + public float LowPassHFGain + { + get { return lowPassHfGain; } + set + { + if (OggStreamer.Instance.Efx.IsInitialized) + { + OggStreamer.Instance.Efx.Filter(alFilterId, EfxFilterf.LowpassGainHF, lowPassHfGain = value); + ALHelper.CheckError("Failed to set Efx filter."); + OggStreamer.Instance.Efx.BindFilterToSource(alSourceId, alFilterId); + ALHelper.CheckError("Failed to bind Efx filter to source."); + } + } + } + + float volume; + public float Volume + { + get { return volume; } + set + { + AL.Source(alSourceId, ALSourcef.Gain, volume = value); + ALHelper.CheckError("Failed to set volume."); + } + } + + public bool IsLooped { get; set; } + + public void Dispose() + { + var state = AL.GetSourceState(alSourceId); + ALHelper.CheckError("Failed to get the source state."); + if (state == ALSourceState.Playing || state == ALSourceState.Paused) + StopPlayback(); + + lock (prepareMutex) + { + OggStreamer.Instance.RemoveStream(this); + + if (state != ALSourceState.Initial) + Empty(); + + Close(); + } + + AL.Source(alSourceId, ALSourcei.Buffer, 0); + ALHelper.CheckError("Failed to free source from buffers."); + OpenALSoundController.Instance.RecycleSource(alSourceId); + AL.DeleteBuffers(alBufferIds); + ALHelper.CheckError("Failed to delete buffer."); + if (OggStreamer.Instance.Efx.IsInitialized) + { + OggStreamer.Instance.Efx.DeleteFilter(alFilterId); + ALHelper.CheckError("Failed to delete EFX filter."); + } + + + } + + void StopPlayback() + { + AL.SourceStop(alSourceId); + ALHelper.CheckError("Failed to stop source."); + } + + void Empty() + { + int queued; + AL.GetSource(alSourceId, ALGetSourcei.BuffersQueued, out queued); + ALHelper.CheckError("Failed to fetch queued buffers."); + if (queued > 0) + { + try + { + AL.SourceUnqueueBuffers(alSourceId, queued); + ALHelper.CheckError("Failed to unqueue buffers (first attempt)."); + } + catch (InvalidOperationException) + { + // This is a bug in the OpenAL implementation + // Salvage what we can + int processed; + AL.GetSource(alSourceId, ALGetSourcei.BuffersProcessed, out processed); + ALHelper.CheckError("Failed to fetch processed buffers."); + var salvaged = new int[processed]; + if (processed > 0) + { + AL.SourceUnqueueBuffers(alSourceId, processed, out salvaged); + ALHelper.CheckError("Failed to unqueue buffers (second attempt)."); + } + + // Try turning it off again? + AL.SourceStop(alSourceId); + ALHelper.CheckError("Failed to stop source."); + + Empty(); + } + } + } + + internal void Open(bool precache = false) + { + Reader = new VorbisReader(oggFileName); + + if (precache) + { + // Fill first buffer synchronously + OggStreamer.Instance.FillBuffer(this, alBufferIds[0]); + AL.SourceQueueBuffer(alSourceId, alBufferIds[0]); + ALHelper.CheckError("Failed to queue buffer."); + } + + Ready = true; + } + + internal void Close() + { + if (Reader != null) + { + Reader.Dispose(); + Reader = null; + } + Ready = false; + } + } + + internal class OggStreamer : IDisposable + { + public readonly XRamExtension XRam = new XRamExtension(); + public readonly EffectsExtension Efx = OpenALSoundController.Efx; + + const float DefaultUpdateRate = 10; + const int DefaultBufferSize = 44100; + + static readonly object singletonMutex = new object(); + + readonly object iterationMutex = new object(); + readonly object readMutex = new object(); + + readonly float[] readSampleBuffer; + readonly short[] castBuffer; + + readonly HashSet streams = new HashSet(); + readonly List threadLocalStreams = new List(); + + readonly Thread underlyingThread; + volatile bool cancelled; + + bool pendingFinish; + + public float UpdateRate { get; private set; } + public int BufferSize { get; private set; } + + static OggStreamer instance; + public static OggStreamer Instance + { + get + { + lock (singletonMutex) + { + if (instance == null) + throw new InvalidOperationException("No instance running"); + return instance; + } + } + private set { lock (singletonMutex) instance = value; } + } + + public OggStreamer(int bufferSize = DefaultBufferSize, float updateRate = DefaultUpdateRate) + { + UpdateRate = updateRate; + BufferSize = bufferSize; + pendingFinish = false; + + lock (singletonMutex) + { + if (instance != null) + throw new InvalidOperationException("Already running"); + + Instance = this; + underlyingThread = new Thread(EnsureBuffersFilled) { Priority = ThreadPriority.Lowest }; + underlyingThread.Start(); + } + + readSampleBuffer = new float[bufferSize]; + castBuffer = new short[bufferSize]; + } + + public void Dispose() + { + lock (singletonMutex) + { + Debug.Assert(Instance == this, "Two instances running, somehow...?"); + + cancelled = true; + lock (iterationMutex) + streams.Clear(); + + Instance = null; + } + } + + internal bool AddStream(OggStream stream) + { + lock (iterationMutex) + return streams.Add(stream); + } + + internal bool RemoveStream(OggStream stream) + { + lock (iterationMutex) + return streams.Remove(stream); + } + + public bool FillBuffer(OggStream stream, int bufferId) + { + int readSamples; + lock (readMutex) + { + readSamples = stream.Reader.ReadSamples(readSampleBuffer, 0, BufferSize); + CastBuffer(readSampleBuffer, castBuffer, readSamples); + } + AL.BufferData(bufferId, stream.Reader.Channels == 1 ? ALFormat.Mono16 : ALFormat.Stereo16, castBuffer, + readSamples * sizeof(short), stream.Reader.SampleRate); + ALHelper.CheckError("Failed to fill buffer, readSamples = {0}, SampleRate = {1}, buffer.Length = {2}.", readSamples, stream.Reader.SampleRate, castBuffer.Length); + + + return readSamples != BufferSize; + } + static void CastBuffer(float[] inBuffer, short[] outBuffer, int length) + { + for (int i = 0; i < length; i++) + { + var temp = (int)(32767f * inBuffer[i]); + if (temp > short.MaxValue) temp = short.MaxValue; + else if (temp < short.MinValue) temp = short.MinValue; + outBuffer[i] = (short)temp; + } + } + + void EnsureBuffersFilled() + { + while (!cancelled) + { + Thread.Sleep((int) (1000 / ((UpdateRate <= 0) ? 1 : UpdateRate))); + if (cancelled) break; + + threadLocalStreams.Clear(); + lock (iterationMutex) threadLocalStreams.AddRange(streams); + + foreach (var stream in threadLocalStreams) + { + lock (stream.prepareMutex) + { + lock (iterationMutex) + if (!streams.Contains(stream)) + continue; + + bool finished = false; + + int queued; + AL.GetSource(stream.alSourceId, ALGetSourcei.BuffersQueued, out queued); + ALHelper.CheckError("Failed to fetch queued buffers."); + int processed; + AL.GetSource(stream.alSourceId, ALGetSourcei.BuffersProcessed, out processed); + ALHelper.CheckError("Failed to fetch processed buffers."); + + if (processed == 0 && queued == stream.BufferCount) continue; + + int[] tempBuffers; + if (processed > 0) + { + tempBuffers = AL.SourceUnqueueBuffers(stream.alSourceId, processed); + ALHelper.CheckError("Failed to unqueue buffers."); + } + else + tempBuffers = stream.alBufferIds.Skip(queued).ToArray(); + + int bufferFilled = 0; + for (int i = 0; i < tempBuffers.Length && !pendingFinish; i++) + { + finished |= FillBuffer(stream, tempBuffers[i]); + bufferFilled++; + + if (finished) + { + if (stream.IsLooped) + { + stream.Close(); + stream.Open(); + } + else + { + pendingFinish = true; + } + } + } + + if (pendingFinish && queued == 0) + { + pendingFinish = false; + lock (iterationMutex) + streams.Remove(stream); + if (stream.FinishedAction != null) + stream.FinishedAction.Invoke(); + } + else if (!finished && bufferFilled > 0) // queue only successfully filled buffers + { + AL.SourceQueueBuffers(stream.alSourceId, bufferFilled, tempBuffers); + ALHelper.CheckError("Failed to queue buffers."); + } + else if (!stream.IsLooped) + continue; + } + + lock (stream.stopMutex) + { + if (stream.Preparing) continue; + + lock (iterationMutex) + if (!streams.Contains(stream)) + continue; + + var state = AL.GetSourceState(stream.alSourceId); + ALHelper.CheckError("Failed to get source state."); + if (state == ALSourceState.Stopped) + { + AL.SourcePlay(stream.alSourceId); + ALHelper.CheckError("Failed to play."); + } + } + } + } + } + } +} diff --git a/MonoGame.Framework/Audio/OpenAL.cs b/MonoGame.Framework/Audio/OpenAL.cs new file mode 100644 index 00000000000..9a8eb4202ed --- /dev/null +++ b/MonoGame.Framework/Audio/OpenAL.cs @@ -0,0 +1,887 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Xna.Framework.Audio; +using MonoGame.Utilities; +using System.IO; + +namespace MonoGame.OpenAL +{ + internal enum ALFormat + { + Mono8 = 0x1100, + Mono16 = 0x1101, + Stereo8 = 0x1102, + Stereo16 = 0x1103, + MonoIma4 = 0x1300, + StereoIma4 = 0x1301, + MonoMSAdpcm = 0x1302, + StereoMSAdpcm = 0x1303, + MonoFloat32 = 0x10010, + StereoFloat32 = 0x10011, + } + + internal enum ALError + { + NoError = 0, + InvalidName = 0xA001, + InvalidEnum = 0xA002, + InvalidValue = 0xA003, + InvalidOperation = 0xA004, + OutOfMemory = 0xA005, + } + + internal enum ALGetString + { + Extensions = 0xB004, + } + + internal enum ALBufferi + { + UnpackBlockAlignmentSoft = 0x200C, + LoopSoftPointsExt = 0x2015, + } + + internal enum ALGetBufferi + { + Bits = 0x2002, + Channels = 0x2003, + Size = 0x2004, + } + + internal enum ALSourceb + { + Looping = 0x1007, + } + + internal enum ALSourcei + { + SourceRelative = 0x202, + Buffer = 0x1009, + EfxDirectFilter = 0x20005, + EfxAuxilarySendFilter = 0x20006, + } + + internal enum ALSourcef + { + Pitch = 0x1003, + Gain = 0x100A, + ReferenceDistance = 0x1020 + } + + internal enum ALGetSourcei + { + SampleOffset = 0x1025, + SourceState = 0x1010, + BuffersQueued = 0x1015, + BuffersProcessed = 0x1016, + } + + internal enum ALSourceState + { + Initial = 0x1011, + Playing = 0x1012, + Paused = 0x1013, + Stopped = 0x1014, + } + + internal enum ALListener3f + { + Position = 0x1004, + } + + internal enum ALSource3f + { + Position = 0x1004, + Velocity = 0x1006, + } + + internal enum ALDistanceModel + { + None = 0, + InverseDistanceClamped = 0xD002, + } + + internal enum AlcError + { + NoError = 0, + } + + internal enum AlcGetString + { + CaptureDeviceSpecifier = 0x0310, + CaptureDefaultDeviceSpecifier = 0x0311, + Extensions = 0x1006, + } + + internal enum AlcGetInteger + { + CaptureSamples = 0x0312, + } + + internal enum EfxFilteri + { + FilterType = 0x8001, + } + + internal enum EfxFilterf + { + LowpassGain = 0x0001, + LowpassGainHF = 0x0002, + HighpassGain = 0x0001, + HighpassGainLF = 0x0002, + BandpassGain = 0x0001, + BandpassGainLF = 0x0002, + BandpassGainHF = 0x0003, + } + + internal enum EfxFilterType + { + None = 0x0000, + Lowpass = 0x0001, + Highpass = 0x0002, + Bandpass = 0x0003, + } + + internal enum EfxEffecti + { + EffectType = 0x8001, + SlotEffect = 0x0001, + } + + internal enum EfxEffectSlotf + { + EffectSlotGain = 0x0002, + } + + internal enum EfxEffectf + { + EaxReverbDensity = 0x0001, + EaxReverbDiffusion = 0x0002, + EaxReverbGain = 0x0003, + EaxReverbGainHF = 0x0004, + EaxReverbGainLF = 0x0005, + DecayTime = 0x0006, + DecayHighFrequencyRatio = 0x0007, + DecayLowFrequencyRation = 0x0008, + EaxReverbReflectionsGain = 0x0009, + EaxReverbReflectionsDelay = 0x000A, + ReflectionsPain = 0x000B, + LateReverbGain = 0x000C, + LateReverbDelay = 0x000D, + LateRevertPain = 0x000E, + EchoTime = 0x000F, + EchoDepth = 0x0010, + ModulationTime = 0x0011, + ModulationDepth = 0x0012, + AirAbsorbsionHighFrequency = 0x0013, + EaxReverbHFReference = 0x0014, + EaxReverbLFReference = 0x0015, + RoomRolloffFactor = 0x0016, + DecayHighFrequencyLimit = 0x0017, + } + + internal enum EfxEffectType + { + Reverb = 0x8000, + } + + internal class AL + { + public static IntPtr NativeLibrary = GetNativeLibrary(); + + private static IntPtr GetNativeLibrary() + { + var ret = IntPtr.Zero; + +#if DESKTOPGL + // Load bundled library + var assemblyLocation = Path.GetDirectoryName(typeof(AL).Assembly.Location); + if (CurrentPlatform.OS == OS.Windows && Environment.Is64BitProcess) + ret = FuncLoader.LoadLibrary(Path.Combine(assemblyLocation, "x64/soft_oal.dll")); + else if (CurrentPlatform.OS == OS.Windows && !Environment.Is64BitProcess) + ret = FuncLoader.LoadLibrary(Path.Combine(assemblyLocation, "x86/soft_oal.dll")); + else if (CurrentPlatform.OS == OS.Linux && Environment.Is64BitProcess) + ret = FuncLoader.LoadLibrary(Path.Combine(assemblyLocation, "x64/libopenal.so.1")); + else if (CurrentPlatform.OS == OS.Linux && !Environment.Is64BitProcess) + ret = FuncLoader.LoadLibrary(Path.Combine(assemblyLocation, "x86/libopenal.so.1")); + else if (CurrentPlatform.OS == OS.MacOSX) + ret = FuncLoader.LoadLibrary(Path.Combine(assemblyLocation, "libopenal.1.dylib")); + + // Load system library + if (ret == IntPtr.Zero) + { + if (CurrentPlatform.OS == OS.Windows) + ret = FuncLoader.LoadLibrary("soft_oal.dll"); + else if (CurrentPlatform.OS == OS.Linux) + ret = FuncLoader.LoadLibrary("libopenal.so.1"); + else + ret = FuncLoader.LoadLibrary("libopenal.1.dylib"); + } +#elif ANDROID + ret = FuncLoader.LoadLibrary("libopenal32.so"); + + if (ret == IntPtr.Zero) + { + var appFilesDir = Environment.GetFolderPath(Environment.SpecialFolder.Personal); + var appDir = Path.GetDirectoryName(appFilesDir); + var lib = Path.Combine(appDir, "lib", "libopenal32.so"); + + ret = FuncLoader.LoadLibrary(lib); + } +#else + ret = FuncLoader.LoadLibrary("/System/Library/Frameworks/OpenAL.framework/OpenAL"); +#endif + + return ret; + } + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_alenable(int cap); + internal static d_alenable Enable = FuncLoader.LoadFunction(NativeLibrary, "alEnable"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_albufferdata(uint bid, int format, IntPtr data, int size, int freq); + internal static d_albufferdata alBufferData = FuncLoader.LoadFunction(NativeLibrary, "alBufferData"); + + internal static void BufferData(int bid, ALFormat format, byte[] data, int size, int freq) + { + var handle = GCHandle.Alloc(data, GCHandleType.Pinned); + alBufferData((uint)bid, (int)format, handle.AddrOfPinnedObject(), size, freq); + handle.Free(); + } + + internal static void BufferData(int bid, ALFormat format, short[] data, int size, int freq) + { + var handle = GCHandle.Alloc(data, GCHandleType.Pinned); + alBufferData((uint)bid, (int)format, handle.AddrOfPinnedObject(), size, freq); + handle.Free(); + } + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal unsafe delegate void d_aldeletebuffers(int n, int* buffers); + internal static d_aldeletebuffers alDeleteBuffers = FuncLoader.LoadFunction(NativeLibrary, "alDeleteBuffers"); + + internal static void DeleteBuffers(int[] buffers) + { + DeleteBuffers(buffers.Length, ref buffers[0]); + } + + internal unsafe static void DeleteBuffers(int n, ref int buffers) + { + fixed (int* pbuffers = &buffers) + { + alDeleteBuffers(n, pbuffers); + } + } + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_albufferi(int buffer, ALBufferi param, int value); + internal static d_albufferi Bufferi = FuncLoader.LoadFunction(NativeLibrary, "alBufferi"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_algetbufferi(int bid, ALGetBufferi param, out int value); + internal static d_algetbufferi GetBufferi = FuncLoader.LoadFunction(NativeLibrary, "alGetBufferi"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_albufferiv(int bid, ALBufferi param, int[] values); + internal static d_albufferiv Bufferiv = FuncLoader.LoadFunction(NativeLibrary, "alBufferiv"); + + internal static void GetBuffer(int bid, ALGetBufferi param, out int value) + { + GetBufferi(bid, param, out value); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal unsafe delegate void d_algenbuffers(int count, int* buffers); + internal static d_algenbuffers alGenBuffers = FuncLoader.LoadFunction(NativeLibrary, "alGenBuffers"); + + internal unsafe static void GenBuffers(int count, out int[] buffers) + { + buffers = new int[count]; + fixed (int* ptr = &buffers[0]) + { + alGenBuffers(count, ptr); + } + } + + internal static void GenBuffers(int count, out int buffer) + { + int[] ret; + GenBuffers(count, out ret); + buffer = ret[0]; + } + + internal static int[] GenBuffers(int count) + { + int[] ret; + GenBuffers(count, out ret); + return ret; + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_algensources(int n, uint[] sources); + internal static d_algensources alGenSources = FuncLoader.LoadFunction(NativeLibrary, "alGenSources"); + + + internal static void GenSources(int[] sources) + { + uint[] temp = new uint[sources.Length]; + alGenSources(temp.Length, temp); + for (int i = 0; i < temp.Length; i++) + { + sources[i] = (int)temp[i]; + } + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate ALError d_algeterror(); + internal static d_algeterror GetError = FuncLoader.LoadFunction(NativeLibrary, "alGetError"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate bool d_alisbuffer(uint buffer); + internal static d_alisbuffer alIsBuffer = FuncLoader.LoadFunction(NativeLibrary, "alIsBuffer"); + + internal static bool IsBuffer(int buffer) + { + return alIsBuffer((uint)buffer); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_alsourcepause(uint source); + internal static d_alsourcepause alSourcePause = FuncLoader.LoadFunction(NativeLibrary, "alSourcePause"); + + internal static void SourcePause(int source) + { + alSourcePause((uint)source); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_alsourceplay(uint source); + internal static d_alsourceplay alSourcePlay = FuncLoader.LoadFunction(NativeLibrary, "alSourcePlay"); + + internal static void SourcePlay(int source) + { + alSourcePlay((uint)source); + } + + internal static string GetErrorString(ALError errorCode) + { + return errorCode.ToString(); + } + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate bool d_alissource(int source); + internal static d_alissource IsSource = FuncLoader.LoadFunction(NativeLibrary, "alIsSource"); + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_aldeletesources(int n, ref int sources); + internal static d_aldeletesources alDeleteSources = FuncLoader.LoadFunction(NativeLibrary, "alDeleteSources"); + + internal static void DeleteSource(int source) + { + alDeleteSources(1, ref source); + } + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_alsourcestop(int sourceId); + internal static d_alsourcestop SourceStop = FuncLoader.LoadFunction(NativeLibrary, "alSourceStop"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_alsourcei(int sourceId, int i, int a); + internal static d_alsourcei alSourcei = FuncLoader.LoadFunction(NativeLibrary, "alSourcei"); + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_alsource3i(int sourceId, ALSourcei i, int a, int b, int c); + internal static d_alsource3i alSource3i = FuncLoader.LoadFunction(NativeLibrary, "alSource3i"); + + internal static void Source(int sourceId, ALSourcei i, int a) + { + alSourcei(sourceId, (int)i, a); + } + + internal static void Source(int sourceId, ALSourceb i, bool a) + { + alSourcei(sourceId, (int)i, a ? 1 : 0); + } + + internal static void Source(int sourceId, ALSource3f i, float x, float y, float z) + { + alSource3f(sourceId, i, x, y, z); + } + + internal static void Source(int sourceId, ALSourcef i, float dist) + { + alSourcef(sourceId, i, dist); + } + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_alsourcef(int sourceId, ALSourcef i, float a); + internal static d_alsourcef alSourcef = FuncLoader.LoadFunction(NativeLibrary, "alSourcef"); + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_alsource3f(int sourceId, ALSource3f i, float x, float y, float z); + internal static d_alsource3f alSource3f = FuncLoader.LoadFunction(NativeLibrary, "alSource3f"); + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_algetsourcei(int sourceId, ALGetSourcei i, out int state); + internal static d_algetsourcei GetSource = FuncLoader.LoadFunction(NativeLibrary, "alGetSourcei"); + + internal static ALSourceState GetSourceState(int sourceId) + { + int state; + GetSource(sourceId, ALGetSourcei.SourceState, out state); + return (ALSourceState)state; + } + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_algetlistener3f(ALListener3f param, out float value1, out float value2, out float value3); + internal static d_algetlistener3f GetListener = FuncLoader.LoadFunction(NativeLibrary, "alGetListener3f"); + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_aldistancemodel(ALDistanceModel model); + internal static d_aldistancemodel DistanceModel = FuncLoader.LoadFunction(NativeLibrary, "alDistanceModel"); + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_aldopplerfactor(float value); + internal static d_aldopplerfactor DopplerFactor = FuncLoader.LoadFunction(NativeLibrary, "alDopplerFactor"); + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal unsafe delegate void d_alsourcequeuebuffers(int sourceId, int numEntries, int* buffers); + internal static d_alsourcequeuebuffers alSourceQueueBuffers = FuncLoader.LoadFunction(NativeLibrary, "alSourceQueueBuffers"); + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal unsafe delegate void d_alsourceunqueuebuffers(int sourceId, int numEntries, int* salvaged); + internal static d_alsourceunqueuebuffers alSourceUnqueueBuffers = FuncLoader.LoadFunction(NativeLibrary, "alSourceUnqueueBuffers"); + + [CLSCompliant(false)] + internal static unsafe void SourceQueueBuffers(int sourceId, int numEntries, int[] buffers) + { + fixed (int* ptr = &buffers[0]) + { + AL.alSourceQueueBuffers(sourceId, numEntries, ptr); + } + } + + internal unsafe static void SourceQueueBuffer(int sourceId, int buffer) + { + AL.alSourceQueueBuffers(sourceId, 1, &buffer); + } + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_alsourceunqueuebuffers2(int sid, int numEntries, out int[] bids); + internal static d_alsourceunqueuebuffers2 alSourceUnqueueBuffers2 = FuncLoader.LoadFunction(NativeLibrary, "alSourceUnqueueBuffers"); + + internal static unsafe int[] SourceUnqueueBuffers(int sourceId, int numEntries) + { + if (numEntries <= 0) + { + throw new ArgumentOutOfRangeException("numEntries", "Must be greater than zero."); + } + int[] array = new int[numEntries]; + fixed (int* ptr = &array[0]) + { + alSourceUnqueueBuffers(sourceId, numEntries, ptr); + } + return array; + } + + internal static void SourceUnqueueBuffers(int sid, int numENtries, out int[] bids) + { + alSourceUnqueueBuffers2(sid, numENtries, out bids); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int d_algetenumvalue(string enumName); + internal static d_algetenumvalue alGetEnumValue = FuncLoader.LoadFunction(NativeLibrary, "alGetEnumValue"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate bool d_alisextensionpresent(string extensionName); + internal static d_alisextensionpresent IsExtensionPresent = FuncLoader.LoadFunction(NativeLibrary, "alIsExtensionPresent"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate IntPtr d_algetprocaddress(string functionName); + internal static d_algetprocaddress alGetProcAddress = FuncLoader.LoadFunction(NativeLibrary, "alGetProcAddress"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate IntPtr d_algetstring(int p); + private static d_algetstring alGetString = FuncLoader.LoadFunction(NativeLibrary, "alGetString"); + + internal static string GetString(int p) + { + return Marshal.PtrToStringAnsi(alGetString(p)); + } + + internal static string Get(ALGetString p) + { + return GetString((int)p); + } + } + + internal partial class Alc + { + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate IntPtr d_alccreatecontext(IntPtr device, int[] attributes); + internal static d_alccreatecontext CreateContext = FuncLoader.LoadFunction(AL.NativeLibrary, "alcCreateContext"); + + internal static AlcError GetError() + { + return GetErrorForDevice(IntPtr.Zero); + } + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate AlcError d_alcgeterror(IntPtr device); + internal static d_alcgeterror GetErrorForDevice = FuncLoader.LoadFunction(AL.NativeLibrary, "alcGetError"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_alcgetintegerv(IntPtr device, int param, int size, int[] values); + internal static d_alcgetintegerv alcGetIntegerv = FuncLoader.LoadFunction(AL.NativeLibrary, "alcGetIntegerv"); + + internal static void GetInteger(IntPtr device, AlcGetInteger param, int size, int[] values) + { + alcGetIntegerv(device, (int)param, size, values); + } + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate IntPtr d_alcgetcurrentcontext(); + internal static d_alcgetcurrentcontext GetCurrentContext = FuncLoader.LoadFunction(AL.NativeLibrary, "alcGetCurrentContext"); + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_alcmakecontextcurrent(IntPtr context); + internal static d_alcmakecontextcurrent MakeContextCurrent = FuncLoader.LoadFunction(AL.NativeLibrary, "alcMakeContextCurrent"); + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_alcdestroycontext(IntPtr context); + internal static d_alcdestroycontext DestroyContext = FuncLoader.LoadFunction(AL.NativeLibrary, "alcDestroyContext"); + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_alcclosedevice(IntPtr device); + internal static d_alcclosedevice CloseDevice = FuncLoader.LoadFunction(AL.NativeLibrary, "alcCloseDevice"); + + [CLSCompliant(false)] + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate IntPtr d_alcopendevice(string device); + internal static d_alcopendevice OpenDevice = FuncLoader.LoadFunction(AL.NativeLibrary, "alcOpenDevice"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate IntPtr d_alccaptureopendevice(string device, uint sampleRate, int format, int sampleSize); + internal static d_alccaptureopendevice alcCaptureOpenDevice = FuncLoader.LoadFunction(AL.NativeLibrary, "alcCaptureOpenDevice"); + + internal static IntPtr CaptureOpenDevice(string device, uint sampleRate, ALFormat format, int sampleSize) + { + return alcCaptureOpenDevice(device, sampleRate, (int)format, sampleSize); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate IntPtr d_alccapturestart(IntPtr device); + internal static d_alccapturestart CaptureStart = FuncLoader.LoadFunction(AL.NativeLibrary, "alcCaptureStart"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_alccapturesamples(IntPtr device, IntPtr buffer, int samples); + internal static d_alccapturesamples CaptureSamples = FuncLoader.LoadFunction(AL.NativeLibrary, "alcCaptureSamples"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate IntPtr d_alccapturestop(IntPtr device); + internal static d_alccapturestop CaptureStop = FuncLoader.LoadFunction(AL.NativeLibrary, "alcCaptureStop"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate IntPtr d_alccaptureclosedevice(IntPtr device); + internal static d_alccaptureclosedevice CaptureCloseDevice = FuncLoader.LoadFunction(AL.NativeLibrary, "alcCaptureCloseDevice"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate bool d_alcisextensionpresent(IntPtr device, string extensionName); + internal static d_alcisextensionpresent IsExtensionPresent = FuncLoader.LoadFunction(AL.NativeLibrary, "alcIsExtensionPresent"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate IntPtr d_alcgetstring(IntPtr device, int p); + internal static d_alcgetstring alcGetString = FuncLoader.LoadFunction(AL.NativeLibrary, "alcGetString"); + + internal static string GetString(IntPtr device, int p) + { + return Marshal.PtrToStringAnsi(alcGetString(device, p)); + } + + internal static string GetString(IntPtr device, AlcGetString p) + { + return GetString(device, (int)p); + } + +#if IOS + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_alcsuspendcontext(IntPtr context); + internal static d_alcsuspendcontext SuspendContext = FuncLoader.LoadFunction(AL.NativeLibrary, "alcSuspendContext"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_alcprocesscontext(IntPtr context); + internal static d_alcprocesscontext ProcessContext = FuncLoader.LoadFunction(AL.NativeLibrary, "alcProcessContext"); +#endif + +#if ANDROID + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_alcdevicepausesoft(IntPtr device); + internal static d_alcdevicepausesoft DevicePause = FuncLoader.LoadFunction(AL.NativeLibrary, "alcDevicePauseSOFT"); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void d_alcdeviceresumesoft(IntPtr device); + internal static d_alcdeviceresumesoft DeviceResume = FuncLoader.LoadFunction(AL.NativeLibrary, "alcDeviceResumeSOFT"); +#endif + } + + internal class XRamExtension + { + internal enum XRamStorage + { + Automatic, + Hardware, + Accessible + } + + private int RamSize; + private int RamFree; + private int StorageAuto; + private int StorageHardware; + private int StorageAccessible; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate bool SetBufferModeDelegate(int n, ref int buffers, int value); + + private SetBufferModeDelegate setBufferMode; + + internal XRamExtension() + { + IsInitialized = false; + if (!AL.IsExtensionPresent("EAX-RAM")) + { + return; + } + RamSize = AL.alGetEnumValue("AL_EAX_RAM_SIZE"); + RamFree = AL.alGetEnumValue("AL_EAX_RAM_FREE"); + StorageAuto = AL.alGetEnumValue("AL_STORAGE_AUTOMATIC"); + StorageHardware = AL.alGetEnumValue("AL_STORAGE_HARDWARE"); + StorageAccessible = AL.alGetEnumValue("AL_STORAGE_ACCESSIBLE"); + if (RamSize == 0 || RamFree == 0 || StorageAuto == 0 || StorageHardware == 0 || StorageAccessible == 0) + { + return; + } + try + { + setBufferMode = (XRamExtension.SetBufferModeDelegate)Marshal.GetDelegateForFunctionPointer(AL.alGetProcAddress("EAXSetBufferMode"), typeof(XRamExtension.SetBufferModeDelegate)); + } + catch (Exception) + { + return; + } + IsInitialized = true; + } + + internal bool IsInitialized { get; private set; } + + internal bool SetBufferMode(int i, ref int id, XRamStorage storage) + { + if (storage == XRamExtension.XRamStorage.Accessible) + { + return setBufferMode(i, ref id, StorageAccessible); + } + if (storage != XRamExtension.XRamStorage.Hardware) + { + return setBufferMode(i, ref id, StorageAuto); + } + return setBufferMode(i, ref id, StorageHardware); + } + } + + [CLSCompliant(false)] + internal class EffectsExtension + { + /* Effect API */ + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void alGenEffectsDelegate(int n, out uint effect); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void alDeleteEffectsDelegate(int n, ref int effect); + //[UnmanagedFunctionPointer (CallingConvention.Cdecl)] + //private delegate bool alIsEffectDelegate (uint effect); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void alEffectfDelegate(uint effect, EfxEffectf param, float value); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void alEffectiDelegate(uint effect, EfxEffecti param, int value); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void alGenAuxiliaryEffectSlotsDelegate(int n, out uint effectslots); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void alDeleteAuxiliaryEffectSlotsDelegate(int n, ref int effectslots); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void alAuxiliaryEffectSlotiDelegate(uint slot, EfxEffecti type, uint effect); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void alAuxiliaryEffectSlotfDelegate(uint slot, EfxEffectSlotf param, float value); + + /* Filter API */ + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private unsafe delegate void alGenFiltersDelegate(int n, [Out] uint* filters); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void alFilteriDelegate(uint fid, EfxFilteri param, int value); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void alFilterfDelegate(uint fid, EfxFilterf param, float value); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private unsafe delegate void alDeleteFiltersDelegate(int n, [In] uint* filters); + + + private alGenEffectsDelegate alGenEffects; + private alDeleteEffectsDelegate alDeleteEffects; + //private alIsEffectDelegate alIsEffect; + private alEffectfDelegate alEffectf; + private alEffectiDelegate alEffecti; + private alGenAuxiliaryEffectSlotsDelegate alGenAuxiliaryEffectSlots; + private alDeleteAuxiliaryEffectSlotsDelegate alDeleteAuxiliaryEffectSlots; + private alAuxiliaryEffectSlotiDelegate alAuxiliaryEffectSloti; + private alAuxiliaryEffectSlotfDelegate alAuxiliaryEffectSlotf; + private alGenFiltersDelegate alGenFilters; + private alFilteriDelegate alFilteri; + private alFilterfDelegate alFilterf; + private alDeleteFiltersDelegate alDeleteFilters; + + internal static IntPtr device; + static EffectsExtension _instance; + internal static EffectsExtension Instance + { + get + { + if (_instance == null) + _instance = new EffectsExtension(); + return _instance; + } + } + + internal EffectsExtension() + { + IsInitialized = false; + if (!Alc.IsExtensionPresent(device, "ALC_EXT_EFX")) + { + return; + } + + alGenEffects = (alGenEffectsDelegate)Marshal.GetDelegateForFunctionPointer(AL.alGetProcAddress("alGenEffects"), typeof(alGenEffectsDelegate)); + alDeleteEffects = (alDeleteEffectsDelegate)Marshal.GetDelegateForFunctionPointer(AL.alGetProcAddress("alDeleteEffects"), typeof(alDeleteEffectsDelegate)); + alEffectf = (alEffectfDelegate)Marshal.GetDelegateForFunctionPointer(AL.alGetProcAddress("alEffectf"), typeof(alEffectfDelegate)); + alEffecti = (alEffectiDelegate)Marshal.GetDelegateForFunctionPointer(AL.alGetProcAddress("alEffecti"), typeof(alEffectiDelegate)); + alGenAuxiliaryEffectSlots = (alGenAuxiliaryEffectSlotsDelegate)Marshal.GetDelegateForFunctionPointer(AL.alGetProcAddress("alGenAuxiliaryEffectSlots"), typeof(alGenAuxiliaryEffectSlotsDelegate)); + alDeleteAuxiliaryEffectSlots = (alDeleteAuxiliaryEffectSlotsDelegate)Marshal.GetDelegateForFunctionPointer(AL.alGetProcAddress("alDeleteAuxiliaryEffectSlots"), typeof(alDeleteAuxiliaryEffectSlotsDelegate)); + alAuxiliaryEffectSloti = (alAuxiliaryEffectSlotiDelegate)Marshal.GetDelegateForFunctionPointer(AL.alGetProcAddress("alAuxiliaryEffectSloti"), typeof(alAuxiliaryEffectSlotiDelegate)); + alAuxiliaryEffectSlotf = (alAuxiliaryEffectSlotfDelegate)Marshal.GetDelegateForFunctionPointer(AL.alGetProcAddress("alAuxiliaryEffectSlotf"), typeof(alAuxiliaryEffectSlotfDelegate)); + + alGenFilters = (alGenFiltersDelegate)Marshal.GetDelegateForFunctionPointer(AL.alGetProcAddress("alGenFilters"), typeof(alGenFiltersDelegate)); + alFilteri = (alFilteriDelegate)Marshal.GetDelegateForFunctionPointer(AL.alGetProcAddress("alFilteri"), typeof(alFilteriDelegate)); + alFilterf = (alFilterfDelegate)Marshal.GetDelegateForFunctionPointer(AL.alGetProcAddress("alFilterf"), typeof(alFilterfDelegate)); + alDeleteFilters = (alDeleteFiltersDelegate)Marshal.GetDelegateForFunctionPointer(AL.alGetProcAddress("alDeleteFilters"), typeof(alDeleteFiltersDelegate)); + + IsInitialized = true; + } + + internal bool IsInitialized { get; private set; } + + /* + +alEffecti (effect, EfxEffecti.FilterType, (int)EfxEffectType.Reverb); + ALHelper.CheckError ("Failed to set Filter Type."); + + */ + + internal void GenAuxiliaryEffectSlots(int count, out uint slot) + { + this.alGenAuxiliaryEffectSlots(count, out slot); + ALHelper.CheckError("Failed to Genereate Aux slot"); + } + + internal void GenEffect(out uint effect) + { + this.alGenEffects(1, out effect); + ALHelper.CheckError("Failed to Generate Effect."); + } + + internal void DeleteAuxiliaryEffectSlot(int slot) + { + alDeleteAuxiliaryEffectSlots(1, ref slot); + } + + internal void DeleteEffect(int effect) + { + alDeleteEffects(1, ref effect); + } + + internal void BindEffectToAuxiliarySlot(uint slot, uint effect) + { + alAuxiliaryEffectSloti(slot, EfxEffecti.SlotEffect, effect); + ALHelper.CheckError("Failed to bind Effect"); + } + + internal void AuxiliaryEffectSlot(uint slot, EfxEffectSlotf param, float value) + { + alAuxiliaryEffectSlotf(slot, param, value); + ALHelper.CheckError("Failes to set " + param + " " + value); + } + + internal void BindSourceToAuxiliarySlot(int SourceId, int slot, int slotnumber, int filter) + { + AL.alSource3i(SourceId, ALSourcei.EfxAuxilarySendFilter, slot, slotnumber, filter); + } + + internal void Effect(uint effect, EfxEffectf param, float value) + { + alEffectf(effect, param, value); + ALHelper.CheckError("Failed to set " + param + " " + value); + } + + internal void Effect(uint effect, EfxEffecti param, int value) + { + alEffecti(effect, param, value); + ALHelper.CheckError("Failed to set " + param + " " + value); + } + + internal unsafe int GenFilter() + { + uint filter = 0; + this.alGenFilters(1, &filter); + return (int)filter; + } + internal void Filter(int sourceId, EfxFilteri filter, int EfxFilterType) + { + this.alFilteri((uint)sourceId, filter, EfxFilterType); + } + internal void Filter(int sourceId, EfxFilterf filter, float EfxFilterType) + { + this.alFilterf((uint)sourceId, filter, EfxFilterType); + } + internal void BindFilterToSource(int sourceId, int filterId) + { + AL.Source(sourceId, ALSourcei.EfxDirectFilter, filterId); + } + internal unsafe void DeleteFilter(int filterId) + { + alDeleteFilters(1, (uint*)&filterId); + } + } +} diff --git a/MonoGame.Framework/Audio/OpenALSoundController.cs b/MonoGame.Framework/Audio/OpenALSoundController.cs new file mode 100644 index 00000000000..c404cb2bf58 --- /dev/null +++ b/MonoGame.Framework/Audio/OpenALSoundController.cs @@ -0,0 +1,469 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using System.Runtime.InteropServices; +using MonoGame.Utilities; +using MonoGame.OpenAL; +using MonoGame.OpenGL; + +#if ANDROID +using System.Globalization; +using Android.Content.PM; +using Android.Content; +using Android.Media; +#endif + +#if IOS +using AudioToolbox; +using AVFoundation; +#endif + +namespace Microsoft.Xna.Framework.Audio +{ + internal static class ALHelper + { + [System.Diagnostics.Conditional("DEBUG")] + [System.Diagnostics.DebuggerHidden] + internal static void CheckError(string message = "", params object[] args) + { + ALError error; + if ((error = AL.GetError()) != ALError.NoError) + { + if (args != null && args.Length > 0) + message = String.Format(message, args); + + throw new InvalidOperationException(message + " (Reason: " + AL.GetErrorString(error) + ")"); + } + } + + public static bool IsStereoFormat(ALFormat format) + { + return (format == ALFormat.Stereo8 + || format == ALFormat.Stereo16 + || format == ALFormat.StereoFloat32 + || format == ALFormat.StereoIma4 + || format == ALFormat.StereoMSAdpcm); + } + } + + internal static class AlcHelper + { + [System.Diagnostics.Conditional("DEBUG")] + [System.Diagnostics.DebuggerHidden] + internal static void CheckError(string message = "", params object[] args) + { + AlcError error; + if ((error = Alc.GetError()) != AlcError.NoError) + { + if (args != null && args.Length > 0) + message = String.Format(message, args); + + throw new InvalidOperationException(message + " (Reason: " + error.ToString() + ")"); + } + } + } + + internal sealed class OpenALSoundController : IDisposable + { + private static OpenALSoundController _instance = null; + private static EffectsExtension _efx = null; + private IntPtr _device; + private IntPtr _context; + IntPtr NullContext = IntPtr.Zero; + private int[] allSourcesArray; +#if DESKTOPGL || ANGLE + + // MacOS & Linux shares a limit of 256. + internal const int MAX_NUMBER_OF_SOURCES = 256; + +#elif IOS + + // Reference: http://stackoverflow.com/questions/3894044/maximum-number-of-openal-sound-buffers-on-iphone + internal const int MAX_NUMBER_OF_SOURCES = 32; + +#elif ANDROID + + // Set to the same as OpenAL on iOS + internal const int MAX_NUMBER_OF_SOURCES = 32; + +#endif +#if ANDROID + private const int DEFAULT_FREQUENCY = 48000; + private const int DEFAULT_UPDATE_SIZE = 512; + private const int DEFAULT_UPDATE_BUFFER_COUNT = 2; +#elif DESKTOPGL + private static OggStreamer _oggstreamer; +#endif + private List availableSourcesCollection; + private List inUseSourcesCollection; + bool _isDisposed; + public bool SupportsIma4 { get; private set; } + public bool SupportsAdpcm { get; private set; } + public bool SupportsEfx { get; private set; } + public bool SupportsIeee { get; private set; } + + /// + /// Sets up the hardware resources used by the controller. + /// + private OpenALSoundController() + { + if (AL.NativeLibrary == IntPtr.Zero) + throw new DllNotFoundException("Couldn't initialize OpenAL because the native binaries couldn't be found."); + + if (!OpenSoundController()) + { + throw new NoAudioHardwareException("OpenAL device could not be initialized, see console output for details."); + } + + if (Alc.IsExtensionPresent(_device, "ALC_EXT_CAPTURE")) + Microphone.PopulateCaptureDevices(); + + // We have hardware here and it is ready + + allSourcesArray = new int[MAX_NUMBER_OF_SOURCES]; + AL.GenSources(allSourcesArray); + ALHelper.CheckError("Failed to generate sources."); + Filter = 0; + if (Efx.IsInitialized) + { + Filter = Efx.GenFilter(); + } + availableSourcesCollection = new List(allSourcesArray); + inUseSourcesCollection = new List(); + } + + ~OpenALSoundController() + { + Dispose(false); + } + + /// + /// Open the sound device, sets up an audio context, and makes the new context + /// the current context. Note that this method will stop the playback of + /// music that was running prior to the game start. If any error occurs, then + /// the state of the controller is reset. + /// + /// True if the sound controller was setup, and false if not. + private bool OpenSoundController() + { + try + { + _device = Alc.OpenDevice(string.Empty); + EffectsExtension.device = _device; + } + catch (Exception ex) + { + throw new NoAudioHardwareException("OpenAL device could not be initialized.", ex); + } + + AlcHelper.CheckError("Could not open OpenAL device"); + + if (_device != IntPtr.Zero) + { +#if ANDROID + // Attach activity event handlers so we can pause and resume all playing sounds + MonoGameAndroidGameView.OnPauseGameThread += Activity_Paused; + MonoGameAndroidGameView.OnResumeGameThread += Activity_Resumed; + + // Query the device for the ideal frequency and update buffer size so + // we can get the low latency sound path. + + /* + The recommended sequence is: + + Check for feature "android.hardware.audio.low_latency" using code such as this: + import android.content.pm.PackageManager; + ... + PackageManager pm = getContext().getPackageManager(); + boolean claimsFeature = pm.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY); + Check for API level 17 or higher, to confirm use of android.media.AudioManager.getProperty(). + Get the native or optimal output sample rate and buffer size for this device's primary output stream, using code such as this: + import android.media.AudioManager; + ... + AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + String sampleRate = am.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)); + String framesPerBuffer = am.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER)); + Note that sampleRate and framesPerBuffer are Strings. First check for null and then convert to int using Integer.parseInt(). + Now use OpenSL ES to create an AudioPlayer with PCM buffer queue data locator. + + See http://stackoverflow.com/questions/14842803/low-latency-audio-playback-on-android + */ + + int frequency = DEFAULT_FREQUENCY; + int updateSize = DEFAULT_UPDATE_SIZE; + int updateBuffers = DEFAULT_UPDATE_BUFFER_COUNT; + if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.JellyBeanMr1) + { + Android.Util.Log.Debug("OAL", Game.Activity.PackageManager.HasSystemFeature(PackageManager.FeatureAudioLowLatency) ? "Supports low latency audio playback." : "Does not support low latency audio playback."); + + var audioManager = Game.Activity.GetSystemService(Context.AudioService) as AudioManager; + if (audioManager != null) + { + var result = audioManager.GetProperty(AudioManager.PropertyOutputSampleRate); + if (!string.IsNullOrEmpty(result)) + frequency = int.Parse(result, CultureInfo.InvariantCulture); + result = audioManager.GetProperty(AudioManager.PropertyOutputFramesPerBuffer); + if (!string.IsNullOrEmpty(result)) + updateSize = int.Parse(result, CultureInfo.InvariantCulture); + } + + // If 4.4 or higher, then we don't need to double buffer on the application side. + // See http://stackoverflow.com/a/15006327 + if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.Kitkat) + { + updateBuffers = 1; + } + } + else + { + Android.Util.Log.Debug("OAL", "Android 4.2 or higher required for low latency audio playback."); + } + Android.Util.Log.Debug("OAL", "Using sample rate " + frequency + "Hz and " + updateBuffers + " buffers of " + updateSize + " frames."); + + // These are missing and non-standard ALC constants + const int AlcFrequency = 0x1007; + const int AlcUpdateSize = 0x1014; + const int AlcUpdateBuffers = 0x1015; + + int[] attribute = new[] + { + AlcFrequency, frequency, + AlcUpdateSize, updateSize, + AlcUpdateBuffers, updateBuffers, + 0 + }; +#elif IOS + AVAudioSession.SharedInstance().Init(); + + // NOTE: Do not override AVAudioSessionCategory set by the game developer: + // see https://github.com/MonoGame/MonoGame/issues/6595 + + EventHandler handler = delegate(object sender, AVAudioSessionInterruptionEventArgs e) { + switch (e.InterruptionType) + { + case AVAudioSessionInterruptionType.Began: + AVAudioSession.SharedInstance().SetActive(false); + Alc.MakeContextCurrent(IntPtr.Zero); + Alc.SuspendContext(_context); + break; + case AVAudioSessionInterruptionType.Ended: + AVAudioSession.SharedInstance().SetActive(true); + Alc.MakeContextCurrent(_context); + Alc.ProcessContext(_context); + break; + } + }; + + AVAudioSession.Notifications.ObserveInterruption(handler); + + // Activate the instance or else the interruption handler will not be called. + AVAudioSession.SharedInstance().SetActive(true); + + int[] attribute = new int[0]; +#else + int[] attribute = new int[0]; +#endif + + _context = Alc.CreateContext(_device, attribute); +#if DESKTOPGL + _oggstreamer = new OggStreamer(); +#endif + + AlcHelper.CheckError("Could not create OpenAL context"); + + if (_context != NullContext) + { + Alc.MakeContextCurrent(_context); + AlcHelper.CheckError("Could not make OpenAL context current"); + SupportsIma4 = AL.IsExtensionPresent("AL_EXT_IMA4"); + SupportsAdpcm = AL.IsExtensionPresent("AL_SOFT_MSADPCM"); + SupportsEfx = AL.IsExtensionPresent("AL_EXT_EFX"); + SupportsIeee = AL.IsExtensionPresent("AL_EXT_float32"); + return true; + } + } + return false; + } + + public static void EnsureInitialized() + { + if (_instance == null) + { + try + { + _instance = new OpenALSoundController(); + } + catch (DllNotFoundException) + { + throw; + } + catch (NoAudioHardwareException) + { + throw; + } + catch (Exception ex) + { + throw (new NoAudioHardwareException("Failed to init OpenALSoundController", ex)); + } + } + } + + + public static OpenALSoundController Instance + { + get + { + if (_instance == null) + throw new NoAudioHardwareException("OpenAL context has failed to initialize. Call SoundEffect.Initialize() before sound operation to get more specific errors."); + return _instance; + } + } + + public static EffectsExtension Efx + { + get + { + if (_efx == null) + _efx = new EffectsExtension(); + return _efx; + } + } + + public int Filter + { + get; private set; + } + + public static void DestroyInstance() + { + if (_instance != null) + { + _instance.Dispose(); + _instance = null; + } + } + + /// + /// Destroys the AL context and closes the device, when they exist. + /// + private void CleanUpOpenAL() + { + Alc.MakeContextCurrent(NullContext); + + if (_context != NullContext) + { + Alc.DestroyContext (_context); + _context = NullContext; + } + if (_device != IntPtr.Zero) + { + Alc.CloseDevice (_device); + _device = IntPtr.Zero; + } + } + + /// + /// Dispose of the OpenALSoundCOntroller. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Dispose of the OpenALSoundCOntroller. + /// + /// If true, the managed resources are to be disposed. + void Dispose(bool disposing) + { + if (!_isDisposed) + { + if (disposing) + { +#if DESKTOPGL + if(_oggstreamer != null) + _oggstreamer.Dispose(); +#endif + for (int i = 0; i < allSourcesArray.Length; i++) + { + AL.DeleteSource(allSourcesArray[i]); + ALHelper.CheckError("Failed to delete source."); + } + + if (Filter != 0 && Efx.IsInitialized) + Efx.DeleteFilter(Filter); + + Microphone.StopMicrophones(); + CleanUpOpenAL(); + } + _isDisposed = true; + } + } + + /// + /// Reserves a sound buffer and return its identifier. If there are no available sources + /// or the controller was not able to setup the hardware then an + /// is thrown. + /// + /// The source number of the reserved sound buffer. + public int ReserveSource() + { + int sourceNumber; + + lock (availableSourcesCollection) + { + if (availableSourcesCollection.Count == 0) + { + throw new InstancePlayLimitException(); + } + + sourceNumber = availableSourcesCollection.Last(); + inUseSourcesCollection.Add(sourceNumber); + availableSourcesCollection.Remove(sourceNumber); + } + + return sourceNumber; + } + + public void RecycleSource(int sourceId) + { + lock (availableSourcesCollection) + { + inUseSourcesCollection.Remove(sourceId); + availableSourcesCollection.Add(sourceId); + } + } + + public void FreeSource(SoundEffectInstance inst) + { + RecycleSource(inst.SourceId); + inst.SourceId = 0; + inst.HasSourceId = false; + inst.SoundState = SoundState.Stopped; + } + + public double SourceCurrentPosition (int sourceId) + { + int pos; + AL.GetSource (sourceId, ALGetSourcei.SampleOffset, out pos); + ALHelper.CheckError("Failed to set source offset."); + return pos; + } + +#if ANDROID + void Activity_Paused(object sender, EventArgs e) + { + // Pause all currently playing sounds by pausing the mixer + Alc.DevicePause(_device); + } + + void Activity_Resumed(object sender, EventArgs e) + { + // Resume all sounds that were playing when the activity was paused + Alc.DeviceResume(_device); + } +#endif + } +} diff --git a/MonoGame.Framework/Audio/SoundEffect.OpenAL.cs b/MonoGame.Framework/Audio/SoundEffect.OpenAL.cs new file mode 100644 index 00000000000..429ca15f28d --- /dev/null +++ b/MonoGame.Framework/Audio/SoundEffect.OpenAL.cs @@ -0,0 +1,254 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.IO; + +#if OPENAL +using MonoGame.OpenAL; +#endif +#if IOS +using AudioToolbox; +using AudioUnit; +#endif + +namespace Microsoft.Xna.Framework.Audio +{ + public sealed partial class SoundEffect : IDisposable + { + internal const int MAX_PLAYING_INSTANCES = OpenALSoundController.MAX_NUMBER_OF_SOURCES; + internal static uint ReverbSlot = 0; + internal static uint ReverbEffect = 0; + + internal OALSoundBuffer SoundBuffer; + + #region Public Constructors + + private void PlatformLoadAudioStream(Stream stream, out TimeSpan duration) + { + byte[] buffer; + + ALFormat format; + int freq; + int channels; + int blockAlignment; + int bitsPerSample; + int samplesPerBlock; + int sampleCount; + buffer = AudioLoader.Load(stream, out format, out freq, out channels, out blockAlignment, out bitsPerSample, out samplesPerBlock, out sampleCount); + + duration = TimeSpan.FromSeconds((float)sampleCount / (float)freq); + + PlatformInitializeBuffer(buffer, buffer.Length, format, channels, freq, blockAlignment, bitsPerSample, 0, 0); + } + + private void PlatformInitializePcm(byte[] buffer, int offset, int count, int sampleBits, int sampleRate, AudioChannels channels, int loopStart, int loopLength) + { + if (sampleBits == 24) + { + // Convert 24-bit signed PCM to 16-bit signed PCM + buffer = AudioLoader.Convert24To16(buffer, offset, count); + offset = 0; + count = buffer.Length; + sampleBits = 16; + } + + var format = AudioLoader.GetSoundFormat(AudioLoader.FormatPcm, (int)channels, sampleBits); + + // bind buffer + SoundBuffer = new OALSoundBuffer(); + SoundBuffer.BindDataBuffer(buffer, format, count, sampleRate); + } + + private void PlatformInitializeIeeeFloat(byte[] buffer, int offset, int count, int sampleRate, AudioChannels channels, int loopStart, int loopLength) + { + if (!OpenALSoundController.Instance.SupportsIeee) + { + // If 32-bit IEEE float is not supported, convert to 16-bit signed PCM + buffer = AudioLoader.ConvertFloatTo16(buffer, offset, count); + PlatformInitializePcm(buffer, 0, buffer.Length, 16, sampleRate, channels, loopStart, loopLength); + return; + } + + var format = AudioLoader.GetSoundFormat(AudioLoader.FormatIeee, (int)channels, 32); + + // bind buffer + SoundBuffer = new OALSoundBuffer(); + SoundBuffer.BindDataBuffer(buffer, format, count, sampleRate); + } + + private void PlatformInitializeAdpcm(byte[] buffer, int offset, int count, int sampleRate, AudioChannels channels, int blockAlignment, int loopStart, int loopLength) + { + if (!OpenALSoundController.Instance.SupportsAdpcm) + { + // If MS-ADPCM is not supported, convert to 16-bit signed PCM + buffer = AudioLoader.ConvertMsAdpcmToPcm(buffer, offset, count, (int)channels, blockAlignment); + PlatformInitializePcm(buffer, 0, buffer.Length, 16, sampleRate, channels, loopStart, loopLength); + return; + } + + var format = AudioLoader.GetSoundFormat(AudioLoader.FormatMsAdpcm, (int)channels, 0); + int sampleAlignment = AudioLoader.SampleAlignment(format, blockAlignment); + + // bind buffer + SoundBuffer = new OALSoundBuffer(); + // Buffer length must be aligned with the block alignment + int alignedCount = count - (count % blockAlignment); + SoundBuffer.BindDataBuffer(buffer, format, alignedCount, sampleRate, sampleAlignment); + } + + private void PlatformInitializeIma4(byte[] buffer, int offset, int count, int sampleRate, AudioChannels channels, int blockAlignment, int loopStart, int loopLength) + { + if (!OpenALSoundController.Instance.SupportsIma4) + { + // If IMA/ADPCM is not supported, convert to 16-bit signed PCM + buffer = AudioLoader.ConvertIma4ToPcm(buffer, offset, count, (int)channels, blockAlignment); + PlatformInitializePcm(buffer, 0, buffer.Length, 16, sampleRate, channels, loopStart, loopLength); + return; + } + + var format = AudioLoader.GetSoundFormat(AudioLoader.FormatIma4, (int)channels, 0); + int sampleAlignment = AudioLoader.SampleAlignment(format, blockAlignment); + + // bind buffer + SoundBuffer = new OALSoundBuffer(); + SoundBuffer.BindDataBuffer(buffer, format, count, sampleRate, sampleAlignment); + } + + private void PlatformInitializeFormat(byte[] header, byte[] buffer, int bufferSize, int loopStart, int loopLength) + { + var wavFormat = BitConverter.ToInt16(header, 0); + var channels = BitConverter.ToInt16(header, 2); + var sampleRate = BitConverter.ToInt32(header, 4); + var blockAlignment = BitConverter.ToInt16(header, 12); + var bitsPerSample = BitConverter.ToInt16(header, 14); + + var format = AudioLoader.GetSoundFormat(wavFormat, channels, bitsPerSample); + PlatformInitializeBuffer(buffer, bufferSize, format, channels, sampleRate, blockAlignment, bitsPerSample, loopStart, loopLength); + } + + private void PlatformInitializeBuffer(byte[] buffer, int bufferSize, ALFormat format, int channels, int sampleRate, int blockAlignment, int bitsPerSample, int loopStart, int loopLength) + { + switch (format) + { + case ALFormat.Mono8: + case ALFormat.Mono16: + case ALFormat.Stereo8: + case ALFormat.Stereo16: + PlatformInitializePcm(buffer, 0, bufferSize, bitsPerSample, sampleRate, (AudioChannels)channels, loopStart, loopLength); + break; + case ALFormat.MonoMSAdpcm: + case ALFormat.StereoMSAdpcm: + PlatformInitializeAdpcm(buffer, 0, bufferSize, sampleRate, (AudioChannels)channels, blockAlignment, loopStart, loopLength); + break; + case ALFormat.MonoFloat32: + case ALFormat.StereoFloat32: + PlatformInitializeIeeeFloat(buffer, 0, bufferSize, sampleRate, (AudioChannels)channels, loopStart, loopLength); + break; + case ALFormat.MonoIma4: + case ALFormat.StereoIma4: + PlatformInitializeIma4(buffer, 0, bufferSize, sampleRate, (AudioChannels)channels, blockAlignment, loopStart, loopLength); + break; + default: + throw new NotSupportedException("Unsupported wave format!"); + } + } + + private void PlatformInitializeXact(MiniFormatTag codec, byte[] buffer, int channels, int sampleRate, int blockAlignment, int loopStart, int loopLength, out TimeSpan duration) + { + if (codec == MiniFormatTag.Adpcm) + { + PlatformInitializeAdpcm(buffer, 0, buffer.Length, sampleRate, (AudioChannels)channels, (blockAlignment + 16) * channels, loopStart, loopLength); + duration = TimeSpan.FromSeconds(SoundBuffer.Duration); + return; + } + + throw new NotSupportedException("Unsupported sound format!"); + } + + #endregion + + #region Additional SoundEffect/SoundEffectInstance Creation Methods + + private void PlatformSetupInstance(SoundEffectInstance inst) + { + inst.InitializeSound(); + } + + #endregion + + internal static void PlatformSetReverbSettings(ReverbSettings reverbSettings) + { + if (!OpenALSoundController.Efx.IsInitialized) + return; + + if (ReverbEffect != 0) + return; + + var efx = OpenALSoundController.Efx; + efx.GenAuxiliaryEffectSlots (1, out ReverbSlot); + efx.GenEffect (out ReverbEffect); + efx.Effect (ReverbEffect, EfxEffecti.EffectType, (int)EfxEffectType.Reverb); + efx.Effect (ReverbEffect, EfxEffectf.EaxReverbReflectionsDelay, reverbSettings.ReflectionsDelayMs / 1000.0f); + efx.Effect (ReverbEffect, EfxEffectf.LateReverbDelay, reverbSettings.ReverbDelayMs / 1000.0f); + // map these from range 0-15 to 0-1 + efx.Effect (ReverbEffect, EfxEffectf.EaxReverbDiffusion, reverbSettings.EarlyDiffusion / 15f); + efx.Effect (ReverbEffect, EfxEffectf.EaxReverbDiffusion, reverbSettings.LateDiffusion / 15f); + efx.Effect (ReverbEffect, EfxEffectf.EaxReverbGainLF, Math.Min (XactHelpers.ParseVolumeFromDecibels (reverbSettings.LowEqGain - 8f), 1.0f)); + efx.Effect (ReverbEffect, EfxEffectf.EaxReverbLFReference, (reverbSettings.LowEqCutoff * 50f) + 50f); + efx.Effect (ReverbEffect, EfxEffectf.EaxReverbGainHF, XactHelpers.ParseVolumeFromDecibels (reverbSettings.HighEqGain - 8f)); + efx.Effect (ReverbEffect, EfxEffectf.EaxReverbHFReference, (reverbSettings.HighEqCutoff * 500f) + 1000f); + // According to Xamarin docs EaxReverbReflectionsGain Unit: Linear gain Range [0.0f .. 3.16f] Default: 0.05f + efx.Effect (ReverbEffect, EfxEffectf.EaxReverbReflectionsGain, Math.Min (XactHelpers.ParseVolumeFromDecibels (reverbSettings.ReflectionsGainDb), 3.16f)); + efx.Effect (ReverbEffect, EfxEffectf.EaxReverbGain, Math.Min (XactHelpers.ParseVolumeFromDecibels (reverbSettings.ReverbGainDb), 1.0f)); + // map these from 0-100 down to 0-1 + efx.Effect (ReverbEffect, EfxEffectf.EaxReverbDensity, reverbSettings.DensityPct / 100f); + efx.AuxiliaryEffectSlot (ReverbSlot, EfxEffectSlotf.EffectSlotGain, reverbSettings.WetDryMixPct / 200f); + + // Dont know what to do with these EFX has no mapping for them. Just ignore for now + // we can enable them as we go. + //efx.SetEffectParam (ReverbEffect, EfxEffectf.PositionLeft, reverbSettings.PositionLeft); + //efx.SetEffectParam (ReverbEffect, EfxEffectf.PositionRight, reverbSettings.PositionRight); + //efx.SetEffectParam (ReverbEffect, EfxEffectf.PositionLeftMatrix, reverbSettings.PositionLeftMatrix); + //efx.SetEffectParam (ReverbEffect, EfxEffectf.PositionRightMatrix, reverbSettings.PositionRightMatrix); + //efx.SetEffectParam (ReverbEffect, EfxEffectf.LowFrequencyReference, reverbSettings.RearDelayMs); + //efx.SetEffectParam (ReverbEffect, EfxEffectf.LowFrequencyReference, reverbSettings.RoomFilterFrequencyHz); + //efx.SetEffectParam (ReverbEffect, EfxEffectf.LowFrequencyReference, reverbSettings.RoomFilterMainDb); + //efx.SetEffectParam (ReverbEffect, EfxEffectf.LowFrequencyReference, reverbSettings.RoomFilterHighFrequencyDb); + //efx.SetEffectParam (ReverbEffect, EfxEffectf.LowFrequencyReference, reverbSettings.DecayTimeSec); + //efx.SetEffectParam (ReverbEffect, EfxEffectf.LowFrequencyReference, reverbSettings.RoomSizeFeet); + + efx.BindEffectToAuxiliarySlot (ReverbSlot, ReverbEffect); + } + +#region IDisposable Members + + private void PlatformDispose(bool disposing) + { + if (SoundBuffer != null) + { + SoundBuffer.Dispose(); + SoundBuffer = null; + } + } + +#endregion + + internal static void PlatformInitialize() + { + OpenALSoundController.EnsureInitialized(); + } + + internal static void PlatformShutdown() + { + if (ReverbEffect != 0) + { + OpenALSoundController.Efx.DeleteAuxiliaryEffectSlot((int)ReverbSlot); + OpenALSoundController.Efx.DeleteEffect((int)ReverbEffect); + } + OpenALSoundController.DestroyInstance(); + } + } +} + diff --git a/MonoGame.Framework/Audio/SoundEffect.Web.cs b/MonoGame.Framework/Audio/SoundEffect.Web.cs new file mode 100644 index 00000000000..bb1e6c46a44 --- /dev/null +++ b/MonoGame.Framework/Audio/SoundEffect.Web.cs @@ -0,0 +1,59 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using System.IO; + +using Microsoft.Xna; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Audio; + +namespace Microsoft.Xna.Framework.Audio +{ + public sealed partial class SoundEffect : IDisposable + { + // This platform is only limited by memory. + internal const int MAX_PLAYING_INSTANCES = int.MaxValue; + + private void PlatformLoadAudioStream(Stream s, out TimeSpan duration) + { + duration = TimeSpan.Zero; + } + + private void PlatformInitializePcm(byte[] buffer, int offset, int count, int sampleBits, int sampleRate, AudioChannels channels, int loopStart, int loopLength) + { + } + + private void PlatformInitializeFormat(byte[] header, byte[] buffer, int bufferSize, int loopStart, int loopLength) + { + } + + private void PlatformInitializeXact(MiniFormatTag codec, byte[] buffer, int channels, int sampleRate, int blockAlignment, int loopStart, int loopLength, out TimeSpan duration) + { + throw new NotSupportedException("Unsupported sound format!"); + } + + private void PlatformSetupInstance(SoundEffectInstance instance) + { + } + + private void PlatformDispose(bool disposing) + { + } + + internal static void PlatformSetReverbSettings(ReverbSettings reverbSettings) + { + } + + internal static void PlatformInitialize() + { + } + + internal static void PlatformShutdown() + { + } + } +} + diff --git a/MonoGame.Framework/Audio/SoundEffect.XAudio.cs b/MonoGame.Framework/Audio/SoundEffect.XAudio.cs new file mode 100644 index 00000000000..18c57fc90c5 --- /dev/null +++ b/MonoGame.Framework/Audio/SoundEffect.XAudio.cs @@ -0,0 +1,384 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.IO; + +using SharpDX; +using SharpDX.XAudio2; +using SharpDX.Multimedia; +using SharpDX.X3DAudio; + +namespace Microsoft.Xna.Framework.Audio +{ + partial class SoundEffect + { + // These platforms are only limited by memory. + internal const int MAX_PLAYING_INSTANCES = int.MaxValue; + + #region Static Fields & Properties + + internal static XAudio2 Device { get; private set; } + internal static MasteringVoice MasterVoice { get; private set; } + + private static X3DAudio _device3D; + private static bool _device3DDirty = true; + private static Speakers _speakers = Speakers.Stereo; + + // XNA does not expose this, but it exists in X3DAudio. + [CLSCompliant(false)] + public static Speakers Speakers + { + get { return _speakers; } + + set + { + if (_speakers == value) + return; + + _speakers = value; + _device3DDirty = true; + } + } + + internal static X3DAudio Device3D + { + get + { + if (_device3DDirty) + { + _device3DDirty = false; + _device3D = new X3DAudio(_speakers); + } + + return _device3D; + } + } + + + private static SubmixVoice _reverbVoice; + + internal static SubmixVoice ReverbVoice + { + get + { + if (_reverbVoice == null) + { + var details = MasterVoice.VoiceDetails; + _reverbVoice = new SubmixVoice(Device, details.InputChannelCount, details.InputSampleRate); + + var reverb = new SharpDX.XAudio2.Fx.Reverb(Device); + var desc = new EffectDescriptor(reverb); + desc.InitialState = true; + desc.OutputChannelCount = details.InputChannelCount; + _reverbVoice.SetEffectChain(desc); + } + + return _reverbVoice; + } + } + + #endregion + + internal DataStream _dataStream; + internal AudioBuffer _buffer; + internal AudioBuffer _loopedBuffer; + internal WaveFormat _format; + + #region Initialization + + /// + /// Initializes XAudio. + /// + internal static void PlatformInitialize() + { + try + { + if (Device == null) + { +#if !WINDOWS_UAP && DEBUG + try + { + //Fails if the XAudio2 SDK is not installed + Device = new XAudio2(XAudio2Flags.DebugEngine, ProcessorSpecifier.DefaultProcessor); + Device.StartEngine(); + } + catch +#endif + { + Device = new XAudio2(XAudio2Flags.None, ProcessorSpecifier.DefaultProcessor); + Device.StartEngine(); + } + } + + // Just use the default device. +#if WINDOWS_UAP + string deviceId = null; +#else + const int deviceId = 0; +#endif + + if (MasterVoice == null) + { + // Let windows autodetect number of channels and sample rate. + MasterVoice = new MasteringVoice(Device, XAudio2.DefaultChannels, XAudio2.DefaultSampleRate); + } + + // The autodetected value of MasterVoice.ChannelMask corresponds to the speaker layout. +#if WINDOWS_UAP + Speakers = (Speakers)MasterVoice.ChannelMask; +#else + Speakers = Device.Version == XAudio2Version.Version27 ? + Device.GetDeviceDetails(deviceId).OutputFormat.ChannelMask: + (Speakers) MasterVoice.ChannelMask; +#endif + } + catch + { + // Release the device and null it as + // we have no audio support. + if (Device != null) + { + Device.Dispose(); + Device = null; + } + + MasterVoice = null; + } + } + + private static DataStream ToDataStream(int offset, byte[] buffer, int length) + { + // NOTE: We make a copy here because old versions of + // DataStream.Create didn't work correctly for offsets. + var data = new byte[length - offset]; + Buffer.BlockCopy(buffer, offset, data, 0, length - offset); + + return DataStream.Create(data, true, false); + } + + private void PlatformInitializePcm(byte[] buffer, int offset, int count, int sampleBits, int sampleRate, AudioChannels channels, int loopStart, int loopLength) + { + CreateBuffers( new WaveFormat(sampleRate, sampleBits, (int)channels), + ToDataStream(offset, buffer, count), + loopStart, + loopLength); + } + + private void PlatformInitializeFormat(byte[] header, byte[] buffer, int bufferSize, int loopStart, int loopLength) + { + var format = BitConverter.ToInt16(header, 0); + var channels = BitConverter.ToInt16(header, 2); + var sampleRate = BitConverter.ToInt32(header, 4); + var blockAlignment = BitConverter.ToInt16(header, 12); + var sampleBits = BitConverter.ToInt16(header, 14); + + WaveFormat waveFormat; + if (format == 1) + waveFormat = new WaveFormat(sampleRate, sampleBits, channels); + else if (format == 2) + waveFormat = new WaveFormatAdpcm(sampleRate, channels, blockAlignment); + else if (format == 3) + waveFormat = WaveFormat.CreateIeeeFloatWaveFormat(sampleRate, channels); + else + throw new NotSupportedException("Unsupported wave format!"); + + CreateBuffers( waveFormat, + ToDataStream(0, buffer, bufferSize), + loopStart, + loopLength); + } + + private void PlatformInitializeXact(MiniFormatTag codec, byte[] buffer, int channels, int sampleRate, int blockAlignment, int loopStart, int loopLength, out TimeSpan duration) + { + if (codec == MiniFormatTag.Adpcm) + { + duration = TimeSpan.FromSeconds((float)loopLength / sampleRate); + + CreateBuffers( new WaveFormatAdpcm(sampleRate, channels, blockAlignment), + ToDataStream(0, buffer, buffer.Length), + loopStart, + loopLength); + + return; + } + + throw new NotSupportedException("Unsupported sound format!"); + } + + private void PlatformLoadAudioStream(Stream stream, out TimeSpan duration) + { + SoundStream soundStream = null; + try + { + soundStream = new SoundStream(stream); + } + catch (InvalidOperationException ex) + { + throw new ArgumentException("Ensure that the specified stream contains valid PCM or IEEE Float wave data.", ex); + } + + var dataStream = soundStream.ToDataStream(); + int sampleCount = 0; + switch (soundStream.Format.Encoding) + { + case WaveFormatEncoding.Adpcm: + { + var samplesPerBlock = (soundStream.Format.BlockAlign / soundStream.Format.Channels - 7) * 2 + 2; + sampleCount = ((int)dataStream.Length / soundStream.Format.BlockAlign) * samplesPerBlock; + } + break; + case WaveFormatEncoding.Pcm: + case WaveFormatEncoding.IeeeFloat: + sampleCount = (int)(dataStream.Length / ((soundStream.Format.Channels * soundStream.Format.BitsPerSample) / 8)); + break; + default: + throw new ArgumentException("Ensure that the specified stream contains valid PCM, MS-ADPCM or IEEE Float wave data."); + } + + CreateBuffers(soundStream.Format, dataStream, 0, sampleCount); + + duration = TimeSpan.FromSeconds((float)sampleCount / (float)soundStream.Format.SampleRate); + } + + private void CreateBuffers(WaveFormat format, DataStream dataStream, int loopStart, int loopLength) + { + _format = format; + _dataStream = dataStream; + + _buffer = new AudioBuffer + { + Stream = _dataStream, + AudioBytes = (int)_dataStream.Length, + Flags = BufferFlags.EndOfStream, + PlayBegin = loopStart, + PlayLength = loopLength, + Context = new IntPtr(42), + }; + + _loopedBuffer = new AudioBuffer + { + Stream = _dataStream, + AudioBytes = (int)_dataStream.Length, + Flags = BufferFlags.EndOfStream, + LoopBegin = loopStart, + LoopLength = loopLength, + LoopCount = AudioBuffer.LoopInfinite, + Context = new IntPtr(42), + }; + } + + private void PlatformSetupInstance(SoundEffectInstance inst) + { + // If the instance came from the pool then it could + // already have a valid voice assigned. + var voice = inst._voice; + + if (voice != null) + { + // TODO: This really shouldn't be here. Instead we should fix the + // SoundEffectInstancePool to internally to look for a compatible + // instance or return a new instance without a voice. + // + // For now we do the same test that the pool should be doing here. + + if (!ReferenceEquals(inst._format, _format)) + { + if (inst._format.Encoding != _format.Encoding || + inst._format.Channels != _format.Channels || + inst._format.SampleRate != _format.SampleRate || + inst._format.BitsPerSample != _format.BitsPerSample) + { + voice.DestroyVoice(); + voice.Dispose(); + voice = null; + } + } + } + + if (voice == null && Device != null) + { + voice = new SourceVoice(Device, _format, VoiceFlags.UseFilter, XAudio2.MaximumFrequencyRatio); + inst._voice = voice; + inst.UpdateOutputMatrix(); // Ensure the output matrix is set for this new voice + } + + inst._format = _format; + } + + #endregion + + internal static void PlatformSetReverbSettings(ReverbSettings reverbSettings) + { + // All parameters related to sampling rate or time are relative to a 48kHz + // voice and must be scaled for use with other sampling rates. + var timeScale = 48000.0f / ReverbVoice.VoiceDetails.InputSampleRate; + + var settings = new SharpDX.XAudio2.Fx.ReverbParameters + { + ReflectionsGain = reverbSettings.ReflectionsGainDb, + ReverbGain = reverbSettings.ReverbGainDb, + DecayTime = reverbSettings.DecayTimeSec, + ReflectionsDelay = (byte)(reverbSettings.ReflectionsDelayMs * timeScale), + ReverbDelay = (byte)(reverbSettings.ReverbDelayMs * timeScale), + RearDelay = (byte)(reverbSettings.RearDelayMs * timeScale), + RoomSize = reverbSettings.RoomSizeFeet, + Density = reverbSettings.DensityPct, + LowEQGain = (byte)reverbSettings.LowEqGain, + LowEQCutoff = (byte)reverbSettings.LowEqCutoff, + HighEQGain = (byte)reverbSettings.HighEqGain, + HighEQCutoff = (byte)reverbSettings.HighEqCutoff, + PositionLeft = (byte)reverbSettings.PositionLeft, + PositionRight = (byte)reverbSettings.PositionRight, + PositionMatrixLeft = (byte)reverbSettings.PositionLeftMatrix, + PositionMatrixRight = (byte)reverbSettings.PositionRightMatrix, + EarlyDiffusion = (byte)reverbSettings.EarlyDiffusion, + LateDiffusion = (byte)reverbSettings.LateDiffusion, + RoomFilterMain = reverbSettings.RoomFilterMainDb, + RoomFilterFreq = reverbSettings.RoomFilterFrequencyHz * timeScale, + RoomFilterHF = reverbSettings.RoomFilterHighFrequencyDb, + WetDryMix = reverbSettings.WetDryMixPct + }; + + ReverbVoice.SetEffectParameters(0, settings); + } + + private void PlatformDispose(bool disposing) + { + if (disposing) + { + if (_dataStream != null) + _dataStream.Dispose(); + } + _dataStream = null; + } + + internal static void PlatformShutdown() + { + if (_reverbVoice != null) + { + _reverbVoice.DestroyVoice(); + _reverbVoice.Dispose(); + _reverbVoice = null; + } + + if (MasterVoice != null) + { + MasterVoice.Dispose(); + MasterVoice = null; + } + + if (Device != null) + { + Device.StopEngine(); + Device.Dispose(); + Device = null; + } + + _device3DDirty = true; + _speakers = Speakers.Stereo; + } + } +} + diff --git a/MonoGame.Framework/Audio/SoundEffect.cs b/MonoGame.Framework/Audio/SoundEffect.cs new file mode 100644 index 00000000000..b5c3ebd1d36 --- /dev/null +++ b/MonoGame.Framework/Audio/SoundEffect.cs @@ -0,0 +1,517 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.IO; + +namespace Microsoft.Xna.Framework.Audio +{ + /// Represents a loaded sound resource. + /// + /// A SoundEffect represents the buffer used to hold audio data and metadata. SoundEffectInstances are used to play from SoundEffects. Multiple SoundEffectInstance objects can be created and played from the same SoundEffect object. + /// The only limit on the number of loaded SoundEffects is restricted by available memory. When a SoundEffect is disposed, all SoundEffectInstances created from it will become invalid. + /// SoundEffect.Play() can be used for 'fire and forget' sounds. If advanced playback controls like volume or pitch is required, use SoundEffect.CreateInstance(). + /// + public sealed partial class SoundEffect : IDisposable + { + #region Internal Audio Data + + private string _name = string.Empty; + + private bool _isDisposed = false; + private readonly TimeSpan _duration; + + #endregion + + #region Internal Constructors + + // Only used from SoundEffect.FromStream. + private SoundEffect(Stream stream) + { + Initialize(); + if (_systemState != SoundSystemState.Initialized) + throw new NoAudioHardwareException("Audio has failed to initialize. Call SoundEffect.Initialize() before sound operation to get more specific errors."); + + /* + The Stream object must point to the head of a valid PCM wave file. Also, this wave file must be in the RIFF bitstream format. + The audio format has the following restrictions: + Must be a PCM wave file + Can only be mono or stereo + Must be 8 or 16 bit + Sample rate must be between 8,000 Hz and 48,000 Hz + */ + + PlatformLoadAudioStream(stream, out _duration); + } + + // Only used from SoundEffectReader. + internal SoundEffect(byte[] header, byte[] buffer, int bufferSize, int durationMs, int loopStart, int loopLength) + { + Initialize(); + if (_systemState != SoundSystemState.Initialized) + throw new NoAudioHardwareException("Audio has failed to initialize. Call SoundEffect.Initialize() before sound operation to get more specific errors."); + + _duration = TimeSpan.FromMilliseconds(durationMs); + + // Peek at the format... handle regular PCM data. + var format = BitConverter.ToInt16(header, 0); + if (format == 1) + { + var channels = BitConverter.ToInt16(header, 2); + var sampleRate = BitConverter.ToInt32(header, 4); + var bitsPerSample = BitConverter.ToInt16(header, 14); + PlatformInitializePcm(buffer, 0, bufferSize, bitsPerSample, sampleRate, (AudioChannels)channels, loopStart, loopLength); + return; + } + + // Everything else is platform specific. + PlatformInitializeFormat(header, buffer, bufferSize, loopStart, loopLength); + } + + // Only used from XACT WaveBank. + internal SoundEffect(MiniFormatTag codec, byte[] buffer, int channels, int sampleRate, int blockAlignment, int loopStart, int loopLength) + { + Initialize(); + if (_systemState != SoundSystemState.Initialized) + throw new NoAudioHardwareException("Audio has failed to initialize. Call SoundEffect.Initialize() before sound operation to get more specific errors."); + + // Handle the common case... the rest is platform specific. + if (codec == MiniFormatTag.Pcm) + { + _duration = TimeSpan.FromSeconds((float)buffer.Length / (sampleRate * blockAlignment)); + PlatformInitializePcm(buffer, 0, buffer.Length, 16, sampleRate, (AudioChannels)channels, loopStart, loopLength); + return; + } + + PlatformInitializeXact(codec, buffer, channels, sampleRate, blockAlignment, loopStart, loopLength, out _duration); + } + + #endregion + + #region Audio System Initialization + + internal enum SoundSystemState + { + NotInitialized, + Initialized, + FailedToInitialized + } + + internal static SoundSystemState _systemState = SoundSystemState.NotInitialized; + + /// + /// Initializes the sound system for SoundEffect support. + /// This method is automatically called when a SoundEffect is loaded, a DynamicSoundEffectInstance is created, or Microphone.All is queried. + /// You can however call this method manually (preferably in, or before the Game constructor) to catch any Exception that may occur during the sound system initialization (and act accordingly). + /// + public static void Initialize() + { + if (_systemState != SoundSystemState.NotInitialized) + return; + + try + { + PlatformInitialize(); + _systemState = SoundSystemState.Initialized; + } + catch (Exception) + { + _systemState = SoundSystemState.FailedToInitialized; + throw; + } + } + + #endregion + + #region Public Constructors + + /// + /// Create a sound effect. + /// + /// The buffer with the sound data. + /// The sound data sample rate in hertz. + /// The number of channels in the sound data. + /// This only supports uncompressed 16bit PCM wav data. + public SoundEffect(byte[] buffer, int sampleRate, AudioChannels channels) + : this(buffer, 0, buffer != null ? buffer.Length : 0, sampleRate, channels, 0, 0) + { + } + + /// + /// Create a sound effect. + /// + /// The buffer with the sound data. + /// The offset to the start of the sound data in bytes. + /// The length of the sound data in bytes. + /// The sound data sample rate in hertz. + /// The number of channels in the sound data. + /// The position where the sound should begin looping in samples. + /// The duration of the sound data loop in samples. + /// This only supports uncompressed 16bit PCM wav data. + public SoundEffect(byte[] buffer, int offset, int count, int sampleRate, AudioChannels channels, int loopStart, int loopLength) + { + Initialize(); + if (_systemState != SoundSystemState.Initialized) + throw new NoAudioHardwareException("Audio has failed to initialize. Call SoundEffect.Initialize() before sound operation to get more specific errors."); + + if (sampleRate < 8000 || sampleRate > 48000) + throw new ArgumentOutOfRangeException("sampleRate"); + if ((int)channels != 1 && (int)channels != 2) + throw new ArgumentOutOfRangeException("channels"); + + if (buffer == null || buffer.Length == 0) + throw new ArgumentException("Ensure that the buffer length is non-zero.", "buffer"); + + var blockAlign = (int)channels * 2; + if ((buffer.Length % blockAlign) != 0) + throw new ArgumentException("Ensure that the buffer meets the block alignment requirements for the number of channels.", "buffer"); + + if (count <= 0) + throw new ArgumentException("Ensure that the count is greater than zero.", "count"); + if ((count % blockAlign) != 0) + throw new ArgumentException("Ensure that the count meets the block alignment requirements for the number of channels.", "count"); + + if (offset < 0) + throw new ArgumentException("The offset cannot be negative.", "offset"); + if (((ulong)count + (ulong)offset) > (ulong)buffer.Length) + throw new ArgumentException("Ensure that the offset+count region lines within the buffer.", "offset"); + + var totalSamples = buffer.Length / blockAlign; + + if (loopStart < 0) + throw new ArgumentException("The loopStart cannot be negative.", "loopStart"); + if (loopStart > totalSamples) + throw new ArgumentException("The loopStart cannot be greater than the total number of samples.", "loopStart"); + + if (loopLength == 0) + loopLength = totalSamples - loopStart; + + if (loopLength < 0) + throw new ArgumentException("The loopLength cannot be negative.", "loopLength"); + if (((ulong)loopStart + (ulong)loopLength) > (ulong)totalSamples) + throw new ArgumentException("Ensure that the loopStart+loopLength region lies within the sample range.", "loopLength"); + + _duration = GetSampleDuration(count, sampleRate, channels); + + PlatformInitializePcm(buffer, offset, count, 16, sampleRate, channels, loopStart, loopLength); + } + + #endregion + + #region Finalizer + + /// + /// Releases unmanaged resources and performs other cleanup operations before the + /// is reclaimed by garbage collection. + /// + ~SoundEffect() + { + Dispose(false); + } + + #endregion + + #region Additional SoundEffect/SoundEffectInstance Creation Methods + + /// + /// Creates a new SoundEffectInstance for this SoundEffect. + /// + /// A new SoundEffectInstance for this SoundEffect. + /// Creating a SoundEffectInstance before calling SoundEffectInstance.Play() allows you to access advanced playback features, such as volume, pitch, and 3D positioning. + public SoundEffectInstance CreateInstance() + { + var inst = new SoundEffectInstance(); + PlatformSetupInstance(inst); + + inst._isPooled = false; + inst._effect = this; + + return inst; + } + + /// + /// Creates a new SoundEffect object based on the specified data stream. + /// + /// A stream containing the wave data. + /// A new SoundEffect object. + /// The stream must point to the head of a valid wave file in the RIFF bitstream format. The formats supported are: + /// + /// + /// 8-bit unsigned PCM + /// 16-bit signed PCM + /// 24-bit signed PCM + /// 32-bit IEEE float PCM + /// MS-ADPCM 4-bit compressed + /// IMA/ADPCM (IMA4) 4-bit compressed + /// + /// + /// + public static SoundEffect FromStream(Stream stream) + { + if (stream == null) + throw new ArgumentNullException("stream"); + + return new SoundEffect(stream); + } + + /// + /// Returns the duration for 16-bit PCM audio. + /// + /// The length of the audio data in bytes. + /// Sample rate, in Hertz (Hz). Must be between 8000 Hz and 48000 Hz + /// Number of channels in the audio data. + /// The duration of the audio data. + public static TimeSpan GetSampleDuration(int sizeInBytes, int sampleRate, AudioChannels channels) + { + if (sizeInBytes < 0) + throw new ArgumentException("Buffer size cannot be negative.", "sizeInBytes"); + if (sampleRate < 8000 || sampleRate > 48000) + throw new ArgumentOutOfRangeException("sampleRate"); + + var numChannels = (int)channels; + if (numChannels != 1 && numChannels != 2) + throw new ArgumentOutOfRangeException("channels"); + + if (sizeInBytes == 0) + return TimeSpan.Zero; + + // Reference + // http://tinyurl.com/hq9slfy + + var dur = sizeInBytes / (sampleRate * numChannels * 16f / 8f); + + var duration = TimeSpan.FromSeconds(dur); + + return duration; + } + + /// + /// Returns the data size in bytes for 16bit PCM audio. + /// + /// The total duration of the audio data. + /// Sample rate, in Hertz (Hz), of audio data. Must be between 8,000 and 48,000 Hz. + /// Number of channels in the audio data. + /// The size in bytes of a single sample of audio data. + public static int GetSampleSizeInBytes(TimeSpan duration, int sampleRate, AudioChannels channels) + { + if (duration < TimeSpan.Zero || duration > TimeSpan.FromMilliseconds(0x7FFFFFF)) + throw new ArgumentOutOfRangeException("duration"); + if (sampleRate < 8000 || sampleRate > 48000) + throw new ArgumentOutOfRangeException("sampleRate"); + + var numChannels = (int)channels; + if (numChannels != 1 && numChannels != 2) + throw new ArgumentOutOfRangeException("channels"); + + // Reference + // http://tinyurl.com/hq9slfy + + var sizeInBytes = duration.TotalSeconds * (sampleRate * numChannels * 16f / 8f); + + return (int)sizeInBytes; + } + + #endregion + + #region Play + + /// Gets an internal SoundEffectInstance and plays it. + /// True if a SoundEffectInstance was successfully played, false if not. + /// + /// Play returns false if more SoundEffectInstances are currently playing then the platform allows. + /// To loop a sound or apply 3D effects, call SoundEffect.CreateInstance() and SoundEffectInstance.Play() instead. + /// SoundEffectInstances used by SoundEffect.Play() are pooled internally. + /// + public bool Play() + { + var inst = GetPooledInstance(false); + if (inst == null) + return false; + + inst.Play(); + + return true; + } + + /// Gets an internal SoundEffectInstance and plays it with the specified volume, pitch, and panning. + /// True if a SoundEffectInstance was successfully created and played, false if not. + /// Volume, ranging from 0.0 (silence) to 1.0 (full volume). Volume during playback is scaled by SoundEffect.MasterVolume. + /// Pitch adjustment, ranging from -1.0 (down an octave) to 0.0 (no change) to 1.0 (up an octave). + /// Panning, ranging from -1.0 (left speaker) to 0.0 (centered), 1.0 (right speaker). + /// + /// Play returns false if more SoundEffectInstances are currently playing then the platform allows. + /// To apply looping or simulate 3D audio, call SoundEffect.CreateInstance() and SoundEffectInstance.Play() instead. + /// SoundEffectInstances used by SoundEffect.Play() are pooled internally. + /// + public bool Play(float volume, float pitch, float pan) + { + var inst = GetPooledInstance(false); + if (inst == null) + return false; + + inst.Volume = volume; + inst.Pitch = pitch; + inst.Pan = pan; + + inst.Play(); + + return true; + } + + /// + /// Returns a sound effect instance from the pool or null if none are available. + /// + internal SoundEffectInstance GetPooledInstance(bool forXAct) + { + if (!SoundEffectInstancePool.SoundsAvailable) + return null; + + var inst = SoundEffectInstancePool.GetInstance(forXAct); + inst._effect = this; + PlatformSetupInstance(inst); + + return inst; + } + + #endregion + + #region Public Properties + + /// Gets the duration of the SoundEffect. + public TimeSpan Duration { get { return _duration; } } + + /// Gets or sets the asset name of the SoundEffect. + public string Name + { + get { return _name; } + set { _name = value; } + } + + #endregion + + #region Static Members + + static float _masterVolume = 1.0f; + /// + /// Gets or sets the master volume scale applied to all SoundEffectInstances. + /// + /// + /// Each SoundEffectInstance has its own Volume property that is independent to SoundEffect.MasterVolume. During playback SoundEffectInstance.Volume is multiplied by SoundEffect.MasterVolume. + /// This property is used to adjust the volume on all current and newly created SoundEffectInstances. The volume of an individual SoundEffectInstance can be adjusted on its own. + /// + public static float MasterVolume + { + get { return _masterVolume; } + set + { + if (value < 0.0f || value > 1.0f) + throw new ArgumentOutOfRangeException(); + + if (_masterVolume == value) + return; + + _masterVolume = value; + SoundEffectInstancePool.UpdateMasterVolume(); + } + } + + static float _distanceScale = 1.0f; + /// + /// Gets or sets the scale of distance calculations. + /// + /// + /// DistanceScale defaults to 1.0 and must be greater than 0.0. + /// Higher values reduce the rate of falloff between the sound and listener. + /// + public static float DistanceScale + { + get { return _distanceScale; } + set + { + if (value <= 0f) + throw new ArgumentOutOfRangeException ("value of DistanceScale"); + + _distanceScale = value; + } + } + + static float _dopplerScale = 1f; + /// + /// Gets or sets the scale of Doppler calculations applied to sounds. + /// + /// + /// DopplerScale defaults to 1.0 and must be greater or equal to 0.0 + /// Affects the relative velocity of emitters and listeners. + /// Higher values more dramatically shift the pitch for the given relative velocity of the emitter and listener. + /// + public static float DopplerScale + { + get { return _dopplerScale; } + set + { + // As per documenation it does not look like the value can be less than 0 + // although the documentation does not say it throws an error we will anyway + // just so it is like the DistanceScale + if (value < 0.0f) + throw new ArgumentOutOfRangeException ("value of DopplerScale"); + + _dopplerScale = value; + } + } + + static float speedOfSound = 343.5f; + /// Returns the speed of sound used when calculating the Doppler effect.. + /// + /// Defaults to 343.5. Value is measured in meters per second. + /// Has no effect on distance attenuation. + /// + public static float SpeedOfSound + { + get { return speedOfSound; } + set + { + if (value <= 0.0f) + throw new ArgumentOutOfRangeException(); + + speedOfSound = value; + } + } + + #endregion + + #region IDisposable Members + + /// Indicates whether the object is disposed. + public bool IsDisposed { get { return _isDisposed; } } + + /// Releases the resources held by this . + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases the resources held by this . + /// + /// If set to true, Dispose was called explicitly. + /// If the disposing parameter is true, the Dispose method was called explicitly. This + /// means that managed objects referenced by this instance should be disposed or released as + /// required. If the disposing parameter is false, Dispose was called by the finalizer and + /// no managed objects should be touched because we do not know if they are still valid or + /// not at that time. Unmanaged resources should always be released. + void Dispose(bool disposing) + { + if (!_isDisposed) + { + SoundEffectInstancePool.StopPooledInstances(this); + PlatformDispose(disposing); + _isDisposed = true; + } + } + + #endregion + + } +} diff --git a/MonoGame.Framework/Audio/SoundEffectInstance.OpenAL.cs b/MonoGame.Framework/Audio/SoundEffectInstance.OpenAL.cs new file mode 100644 index 00000000000..929bc409b03 --- /dev/null +++ b/MonoGame.Framework/Audio/SoundEffectInstance.OpenAL.cs @@ -0,0 +1,356 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using MonoGame.OpenAL; + +namespace Microsoft.Xna.Framework.Audio +{ + public partial class SoundEffectInstance : IDisposable + { + internal SoundState SoundState = SoundState.Stopped; + private bool _looped = false; + private float _alVolume = 1f; + + internal int SourceId; + private float reverb = 0f; + bool applyFilter = false; + EfxFilterType filterType; + float filterQ; + float frequency; + int pauseCount; + + internal OpenALSoundController controller; + + internal bool HasSourceId = false; + +#region Initialization + + /// + /// Creates a standalone SoundEffectInstance from given wavedata. + /// + internal void PlatformInitialize(byte[] buffer, int sampleRate, int channels) + { + InitializeSound(); + } + + /// + /// Gets the OpenAL sound controller, constructs the sound buffer, and sets up the event delegates for + /// the reserved and recycled events. + /// + internal void InitializeSound() + { + controller = OpenALSoundController.Instance; + } + +#endregion // Initialization + + /// + /// Converts the XNA [-1, 1] pitch range to OpenAL pitch (0, INF) or Android SoundPool playback rate [0.5, 2]. + /// The pitch of the sound in the Microsoft XNA range. + /// + private static float XnaPitchToAlPitch(float xnaPitch) + { + return (float)Math.Pow(2, xnaPitch); + } + + private void PlatformApply3D(AudioListener listener, AudioEmitter emitter) + { + if (!HasSourceId) + return; + // get AL's listener position + float x, y, z; + AL.GetListener(ALListener3f.Position, out x, out y, out z); + ALHelper.CheckError("Failed to get source position."); + + // get the emitter offset from origin + Vector3 posOffset = emitter.Position - listener.Position; + // set up orientation matrix + Matrix orientation = Matrix.CreateWorld(Vector3.Zero, listener.Forward, listener.Up); + // set up our final position and velocity according to orientation of listener + Vector3 finalPos = new Vector3(x + posOffset.X, y + posOffset.Y, z + posOffset.Z); + finalPos = Vector3.Transform(finalPos, orientation); + Vector3 finalVel = emitter.Velocity; + finalVel = Vector3.Transform(finalVel, orientation); + + // set the position based on relative positon + AL.Source(SourceId, ALSource3f.Position, finalPos.X, finalPos.Y, finalPos.Z); + ALHelper.CheckError("Failed to set source position."); + AL.Source(SourceId, ALSource3f.Velocity, finalVel.X, finalVel.Y, finalVel.Z); + ALHelper.CheckError("Failed to set source velocity."); + + AL.Source(SourceId, ALSourcef.ReferenceDistance, SoundEffect.DistanceScale); + ALHelper.CheckError("Failed to set source distance scale."); + AL.DopplerFactor(SoundEffect.DopplerScale); + ALHelper.CheckError("Failed to set Doppler scale."); + } + + private void PlatformPause() + { + if (!HasSourceId || SoundState != SoundState.Playing) + return; + + if (pauseCount == 0) + { + AL.SourcePause(SourceId); + ALHelper.CheckError("Failed to pause source."); + } + ++pauseCount; + SoundState = SoundState.Paused; + } + + private void PlatformPlay() + { + SourceId = 0; + HasSourceId = false; + SourceId = controller.ReserveSource(); + HasSourceId = true; + + int bufferId = _effect.SoundBuffer.OpenALDataBuffer; + AL.Source(SourceId, ALSourcei.Buffer, bufferId); + ALHelper.CheckError("Failed to bind buffer to source."); + + // Send the position, gain, looping, pitch, and distance model to the OpenAL driver. + if (!HasSourceId) + return; + + AL.Source(SourceId, ALSourcei.SourceRelative, 1); + ALHelper.CheckError("Failed set source relative."); + // Distance Model + AL.DistanceModel (ALDistanceModel.InverseDistanceClamped); + ALHelper.CheckError("Failed set source distance."); + // Pan + AL.Source (SourceId, ALSource3f.Position, _pan, 0f, 0f); + ALHelper.CheckError("Failed to set source pan."); + // Velocity + AL.Source (SourceId, ALSource3f.Velocity, 0f, 0f, 0f); + ALHelper.CheckError("Failed to set source pan."); + // Volume + AL.Source(SourceId, ALSourcef.Gain, _alVolume); + ALHelper.CheckError("Failed to set source volume."); + // Looping + AL.Source (SourceId, ALSourceb.Looping, IsLooped); + ALHelper.CheckError("Failed to set source loop state."); + // Pitch + AL.Source (SourceId, ALSourcef.Pitch, XnaPitchToAlPitch(_pitch)); + ALHelper.CheckError("Failed to set source pitch."); + + ApplyReverb (); + ApplyFilter (); + + AL.SourcePlay(SourceId); + ALHelper.CheckError("Failed to play source."); + + SoundState = SoundState.Playing; + } + + private void PlatformResume() + { + if (!HasSourceId) + { + Play(); + return; + } + + if (SoundState == SoundState.Paused) + { + --pauseCount; + if (pauseCount == 0) + { + AL.SourcePlay(SourceId); + ALHelper.CheckError("Failed to play source."); + } + } + SoundState = SoundState.Playing; + } + + private void PlatformStop(bool immediate) + { + if (HasSourceId) + { + AL.SourceStop(SourceId); + ALHelper.CheckError("Failed to stop source."); + + // Reset the SendFilter to 0 if we are NOT using reverb since + // sources are recycled + if (OpenALSoundController.Instance.SupportsEfx) + { + OpenALSoundController.Efx.BindSourceToAuxiliarySlot(SourceId, 0, 0, 0); + ALHelper.CheckError("Failed to unset reverb."); + AL.Source(SourceId, ALSourcei.EfxDirectFilter, 0); + ALHelper.CheckError("Failed to unset filter."); + } + AL.Source(SourceId, ALSourcei.Buffer, 0); + ALHelper.CheckError("Failed to free source from buffer."); + + controller.FreeSource(this); + } + SoundState = SoundState.Stopped; + } + + private void PlatformSetIsLooped(bool value) + { + _looped = value; + + if (HasSourceId) + { + AL.Source(SourceId, ALSourceb.Looping, _looped); + ALHelper.CheckError("Failed to set source loop state."); + } + } + + private bool PlatformGetIsLooped() + { + return _looped; + } + + private void PlatformSetPan(float value) + { + if (HasSourceId) + { + AL.Source(SourceId, ALSource3f.Position, value, 0.0f, 0.1f); + ALHelper.CheckError("Failed to set source pan."); + } + } + + private void PlatformSetPitch(float value) + { + if (HasSourceId) + { + AL.Source(SourceId, ALSourcef.Pitch, XnaPitchToAlPitch(value)); + ALHelper.CheckError("Failed to set source pitch."); + } + } + + private SoundState PlatformGetState() + { + if (!HasSourceId) + return SoundState.Stopped; + + var alState = AL.GetSourceState(SourceId); + ALHelper.CheckError("Failed to get source state."); + + switch (alState) + { + case ALSourceState.Initial: + case ALSourceState.Stopped: + SoundState = SoundState.Stopped; + break; + + case ALSourceState.Paused: + SoundState = SoundState.Paused; + break; + + case ALSourceState.Playing: + SoundState = SoundState.Playing; + break; + } + + return SoundState; + } + + private void PlatformSetVolume(float value) + { + _alVolume = value; + + if (HasSourceId) + { + AL.Source(SourceId, ALSourcef.Gain, _alVolume); + ALHelper.CheckError("Failed to set source volume."); + } + } + + internal void PlatformSetReverbMix(float mix) + { + if (!OpenALSoundController.Efx.IsInitialized) + return; + reverb = mix; + if (State == SoundState.Playing) + { + ApplyReverb(); + reverb = 0f; + } + } + + void ApplyReverb() + { + if (reverb > 0f && SoundEffect.ReverbSlot != 0) + { + OpenALSoundController.Efx.BindSourceToAuxiliarySlot(SourceId, (int)SoundEffect.ReverbSlot, 0, 0); + ALHelper.CheckError("Failed to set reverb."); + } + } + + void ApplyFilter() + { + if (applyFilter && controller.Filter > 0) + { + var freq = frequency / 20000f; + var lf = 1.0f - freq; + var efx = OpenALSoundController.Efx; + efx.Filter(controller.Filter, EfxFilteri.FilterType, (int)filterType); + ALHelper.CheckError("Failed to set filter."); + switch (filterType) + { + case EfxFilterType.Lowpass: + efx.Filter(controller.Filter, EfxFilterf.LowpassGainHF, freq); + ALHelper.CheckError("Failed to set LowpassGainHF."); + break; + case EfxFilterType.Highpass: + efx.Filter(controller.Filter, EfxFilterf.HighpassGainLF, freq); + ALHelper.CheckError("Failed to set HighpassGainLF."); + break; + case EfxFilterType.Bandpass: + efx.Filter(controller.Filter, EfxFilterf.BandpassGainHF, freq); + ALHelper.CheckError("Failed to set BandpassGainHF."); + efx.Filter(controller.Filter, EfxFilterf.BandpassGainLF, lf); + ALHelper.CheckError("Failed to set BandpassGainLF."); + break; + } + AL.Source(SourceId, ALSourcei.EfxDirectFilter, controller.Filter); + ALHelper.CheckError("Failed to set DirectFilter."); + } + } + + internal void PlatformSetFilter(FilterMode mode, float filterQ, float frequency) + { + if (!OpenALSoundController.Efx.IsInitialized) + return; + + applyFilter = true; + switch (mode) + { + case FilterMode.BandPass: + filterType = EfxFilterType.Bandpass; + break; + case FilterMode.LowPass: + filterType = EfxFilterType.Lowpass; + break; + case FilterMode.HighPass: + filterType = EfxFilterType.Highpass; + break; + } + this.filterQ = filterQ; + this.frequency = frequency; + if (State == SoundState.Playing) + { + ApplyFilter(); + applyFilter = false; + } + } + + internal void PlatformClearFilter() + { + if (!OpenALSoundController.Efx.IsInitialized) + return; + + applyFilter = false; + } + + private void PlatformDispose(bool disposing) + { + + } + } +} diff --git a/MonoGame.Framework/Audio/SoundEffectInstance.Web.cs b/MonoGame.Framework/Audio/SoundEffectInstance.Web.cs new file mode 100644 index 00000000000..83981ee4331 --- /dev/null +++ b/MonoGame.Framework/Audio/SoundEffectInstance.Web.cs @@ -0,0 +1,78 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.IO; + +namespace Microsoft.Xna.Framework.Audio +{ + public partial class SoundEffectInstance : IDisposable + { + internal void PlatformInitialize(byte[] buffer, int sampleRate, int channels) + { + } + + private void PlatformApply3D(AudioListener listener, AudioEmitter emitter) + { + } + + private void PlatformPause() + { + } + + private void PlatformPlay() + { + } + + private void PlatformResume() + { + } + + private void PlatformStop(bool immediate) + { + } + + private void PlatformSetIsLooped(bool value) + { + } + + private bool PlatformGetIsLooped() + { + return false; + } + + private void PlatformSetPan(float value) + { + } + + private void PlatformSetPitch(float value) + { + } + + private SoundState PlatformGetState() + { + return SoundState.Stopped; + } + + private void PlatformSetVolume(float value) + { + } + + internal void PlatformSetReverbMix(float mix) + { + } + + internal void PlatformSetFilter(FilterMode mode, float filterQ, float frequency) + { + } + + internal void PlatformClearFilter() + { + } + + private void PlatformDispose(bool disposing) + { + } + } +} diff --git a/MonoGame.Framework/Audio/SoundEffectInstance.XAudio.cs b/MonoGame.Framework/Audio/SoundEffectInstance.XAudio.cs new file mode 100644 index 00000000000..93eea625c98 --- /dev/null +++ b/MonoGame.Framework/Audio/SoundEffectInstance.XAudio.cs @@ -0,0 +1,388 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using SharpDX.XAudio2; +using SharpDX.X3DAudio; +using SharpDX.Multimedia; +using SharpDX.Mathematics.Interop; + +namespace Microsoft.Xna.Framework.Audio +{ + public partial class SoundEffectInstance : IDisposable + { + private static float[] _defaultChannelAzimuths = new float[] { 0f, 0f }; + + internal SourceVoice _voice; + internal WaveFormat _format; + + private SharpDX.XAudio2.Fx.Reverb _reverb; + + private static readonly float[] _outputMatrix = new float[16]; + + private float _reverbMix; + + private bool _paused; + private bool _loop; + + private void PlatformInitialize(byte[] buffer, int sampleRate, int channels) + { + throw new NotImplementedException(); + } + + private void PlatformApply3D(AudioListener listener, AudioEmitter emitter) + { + // If we have no voice then nothing to do. + if (_voice == null || SoundEffect.MasterVoice == null) + return; + + // Convert from XNA Emitter to a SharpDX Emitter + var e = ToDXEmitter(emitter); + e.CurveDistanceScaler = SoundEffect.DistanceScale; + e.DopplerScaler = SoundEffect.DopplerScale; + e.ChannelCount = _effect._format.Channels; + + //stereo channel + if (e.ChannelCount > 1) + { + e.ChannelRadius = 0; + e.ChannelAzimuths = _defaultChannelAzimuths; + } + + // Convert from XNA Listener to a SharpDX Listener + var l = ToDXListener(listener); + + // Number of channels in the sound being played. + // Not actually sure if XNA supported 3D attenuation of sterio sounds, but X3DAudio does. + var srcChannelCount = _effect._format.Channels; + + // Number of output channels. + var dstChannelCount = SoundEffect.MasterVoice.VoiceDetails.InputChannelCount; + + // XNA supports distance attenuation and doppler. + var dpsSettings = SoundEffect.Device3D.Calculate(l, e, CalculateFlags.Matrix | CalculateFlags.Doppler, srcChannelCount, dstChannelCount); + + // Apply Volume settings (from distance attenuation) ... + _voice.SetOutputMatrix(SoundEffect.MasterVoice, srcChannelCount, dstChannelCount, dpsSettings.MatrixCoefficients, 0); + + // Apply Pitch settings (from doppler) ... + _voice.SetFrequencyRatio(dpsSettings.DopplerFactor); + } + + private Emitter _dxEmitter; + private Listener _dxListener; + + private Emitter ToDXEmitter(AudioEmitter emitter) + { + // Pulling out Vector properties for efficiency. + var pos = emitter.Position; + var vel = emitter.Velocity; + var forward = emitter.Forward; + var up = emitter.Up; + + // From MSDN: + // X3DAudio uses a left-handed Cartesian coordinate system, + // with values on the x-axis increasing from left to right, on the y-axis from bottom to top, + // and on the z-axis from near to far. + // Azimuths are measured clockwise from a given reference direction. + // + // From MSDN: + // The XNA Framework uses a right-handed coordinate system, + // with the positive z-axis pointing toward the observer when the positive x-axis is pointing to the right, + // and the positive y-axis is pointing up. + // + // Programmer Notes: + // According to this description the z-axis (forward vector) is inverted between these two coordinate systems. + // Therefore, we need to negate the z component of any position/velocity values, and negate any forward vectors. + + forward *= -1.0f; + pos.Z *= -1.0f; + vel.Z *= -1.0f; + + if (_dxEmitter == null) + _dxEmitter = new Emitter(); + + _dxEmitter.Position = new RawVector3(pos.X, pos.Y, pos.Z); + _dxEmitter.Velocity = new RawVector3(vel.X, vel.Y, vel.Z); + _dxEmitter.OrientFront = new RawVector3(forward.X, forward.Y, forward.Z); + _dxEmitter.OrientTop = new RawVector3(up.X, up.Y, up.Z); + _dxEmitter.DopplerScaler = emitter.DopplerScale; + return _dxEmitter; + } + + private Listener ToDXListener(AudioListener listener) + { + // Pulling out Vector properties for efficiency. + var pos = listener.Position; + var vel = listener.Velocity; + var forward = listener.Forward; + var up = listener.Up; + + // From MSDN: + // X3DAudio uses a left-handed Cartesian coordinate system, + // with values on the x-axis increasing from left to right, on the y-axis from bottom to top, + // and on the z-axis from near to far. + // Azimuths are measured clockwise from a given reference direction. + // + // From MSDN: + // The XNA Framework uses a right-handed coordinate system, + // with the positive z-axis pointing toward the observer when the positive x-axis is pointing to the right, + // and the positive y-axis is pointing up. + // + // Programmer Notes: + // According to this description the z-axis (forward vector) is inverted between these two coordinate systems. + // Therefore, we need to negate the z component of any position/velocity values, and negate any forward vectors. + + forward *= -1.0f; + pos.Z *= -1.0f; + vel.Z *= -1.0f; + + if (_dxListener == null) + _dxListener = new Listener(); + + _dxListener.Position = new RawVector3 { X = pos.X, Y = pos.Y, Z = pos.Z }; + _dxListener.Velocity = new RawVector3 { X = vel.X, Y = vel.Y, Z = vel.Z }; + _dxListener.OrientFront = new RawVector3 { X = forward.X, Y = forward.Y, Z = forward.Z }; + _dxListener.OrientTop = new RawVector3 { X = up.X, Y = up.Y, Z = up.Z }; + return _dxListener; + } + + private void PlatformPause() + { + if (_voice != null && SoundEffect.MasterVoice != null) + _voice.Stop(); + _paused = true; + } + + private void PlatformPlay() + { + if (_voice != null && SoundEffect.MasterVoice != null) + { + // Choose the correct buffer depending on if we are looped. + var buffer = _loop ? _effect._loopedBuffer : _effect._buffer; + + if (_voice.State.BuffersQueued > 0) + { + _voice.Stop(); + _voice.FlushSourceBuffers(); + } + + _voice.SubmitSourceBuffer(buffer, null); + _voice.Start(); + } + + _paused = false; + } + + private void PlatformResume() + { + if (_voice != null && SoundEffect.MasterVoice != null) + { + // Restart the sound if (and only if) it stopped playing + if (!_loop) + { + if (_voice.State.BuffersQueued == 0) + { + _voice.Stop(); + _voice.FlushSourceBuffers(); + _voice.SubmitSourceBuffer(_effect._buffer, null); + } + } + _voice.Start(); + } + _paused = false; + } + + private void PlatformStop(bool immediate) + { + if (_voice != null && SoundEffect.MasterVoice != null) + { + if (immediate) + { + _voice.Stop(0); + _voice.FlushSourceBuffers(); + } + else + _voice.Stop((int)PlayFlags.Tails); + } + + _paused = false; + } + + private void PlatformSetIsLooped(bool value) + { + _loop = value; + } + + private bool PlatformGetIsLooped() + { + return _loop; + } + + private void PlatformSetPan(float value) + { + // According to XNA documentation: + // "Panning, ranging from -1.0f (full left) to 1.0f (full right). 0.0f is centered." + _pan = MathHelper.Clamp(value, -1.0f, 1.0f); + + // If we have no voice then nothing more to do. + if (_voice == null || SoundEffect.MasterVoice == null) + return; + + UpdateOutputMatrix(); + } + + internal void UpdateOutputMatrix() + { + var srcChannelCount = _voice.VoiceDetails.InputChannelCount; + var dstChannelCount = SoundEffect.MasterVoice.VoiceDetails.InputChannelCount; + + // Set the pan on the correct channels based on the reverb mix. + if (!(_reverbMix > 0.0f)) + _voice.SetOutputMatrix(srcChannelCount, dstChannelCount, CalculateOutputMatrix(_pan, 1.0f, srcChannelCount)); + else + { + _voice.SetOutputMatrix(SoundEffect.ReverbVoice, srcChannelCount, dstChannelCount, CalculateOutputMatrix(_pan, _reverbMix, srcChannelCount)); + _voice.SetOutputMatrix(SoundEffect.MasterVoice, srcChannelCount, dstChannelCount, CalculateOutputMatrix(_pan, 1.0f - Math.Min(_reverbMix, 1.0f), srcChannelCount)); + } + } + + internal static float[] CalculateOutputMatrix(float pan, float scale, int inputChannels) + { + // XNA only ever outputs to the front left/right speakers (channels 0 and 1) + // Assumes there are at least 2 speaker channels to output to + + // Clear all the channels. + var outputMatrix = _outputMatrix; + Array.Clear(outputMatrix, 0, outputMatrix.Length); + + if (inputChannels == 1) // Mono source + { + // Left/Right output levels: + // Pan -1.0: L = 1.0, R = 0.0 + // Pan 0.0: L = 1.0, R = 1.0 + // Pan +1.0: L = 0.0, R = 1.0 + outputMatrix[0] = (pan > 0f) ? ((1f - pan) * scale) : scale; // Front-left output + outputMatrix[1] = (pan < 0f) ? ((1f + pan) * scale) : scale; // Front-right output + } + else if (inputChannels == 2) // Stereo source + { + // Left/Right input (Li/Ri) mix for Left/Right outputs (Lo/Ro): + // Pan -1.0: Lo = 0.5Li + 0.5Ri, Ro = 0.0Li + 0.0Ri + // Pan 0.0: Lo = 1.0Li + 0.0Ri, Ro = 0.0Li + 1.0Ri + // Pan +1.0: Lo = 0.0Li + 0.0Ri, Ro = 0.5Li + 0.5Ri + if (pan <= 0f) + { + outputMatrix[0] = (1f + pan * 0.5f) * scale; // Front-left output, Left input + outputMatrix[1] = (-pan * 0.5f) * scale; // Front-left output, Right input + outputMatrix[2] = 0f; // Front-right output, Left input + outputMatrix[3] = (1f + pan) * scale; // Front-right output, Right input + } + else + { + outputMatrix[0] = (1f - pan) * scale; // Front-left output, Left input + outputMatrix[1] = 0f; // Front-left output, Right input + outputMatrix[2] = (pan * 0.5f) * scale; // Front-right output, Left input + outputMatrix[3] = (1f - pan * 0.5f) * scale; // Front-right output, Right input + } + } + + return outputMatrix; + } + + private void PlatformSetPitch(float value) + { + _pitch = value; + + if (_voice == null || SoundEffect.MasterVoice == null) + return; + + // NOTE: This is copy of what XAudio2.SemitonesToFrequencyRatio() does + // which avoids the native call and is actually more accurate. + var pitch = (float)Math.Pow(2.0, value); + _voice.SetFrequencyRatio(pitch); + } + + private SoundState PlatformGetState() + { + // If no voice or no buffers queued the sound is stopped. + if (_voice == null || SoundEffect.MasterVoice == null || _voice.State.BuffersQueued == 0) + return SoundState.Stopped; + + // Because XAudio2 does not actually provide if a SourceVoice is Started / Stopped + // we have to save the "paused" state ourself. + if (_paused) + return SoundState.Paused; + + return SoundState.Playing; + } + + private void PlatformSetVolume(float value) + { + if (_voice != null && SoundEffect.MasterVoice != null) + _voice.SetVolume(value, XAudio2.CommitNow); + } + + internal void PlatformSetReverbMix(float mix) + { + // At least for XACT we can't go over 2x the volume on the mix. + _reverbMix = MathHelper.Clamp(mix, 0, 2); + + // If we have no voice then nothing more to do. + if (_voice == null || SoundEffect.MasterVoice == null) + return; + + if (!(_reverbMix > 0.0f)) + _voice.SetOutputVoices(new VoiceSendDescriptor(SoundEffect.MasterVoice)); + else + { + _voice.SetOutputVoices( new VoiceSendDescriptor(SoundEffect.ReverbVoice), + new VoiceSendDescriptor(SoundEffect.MasterVoice)); + } + + UpdateOutputMatrix(); + } + + internal void PlatformSetFilter(FilterMode mode, float filterQ, float frequency) + { + if (_voice == null || SoundEffect.MasterVoice == null) + return; + + var filter = new FilterParameters + { + Frequency = XAudio2.CutoffFrequencyToRadians(frequency, _voice.VoiceDetails.InputSampleRate), + OneOverQ = 1.0f / filterQ, + Type = (FilterType)mode + }; + _voice.SetFilterParameters(filter); + } + + internal void PlatformClearFilter() + { + if (_voice == null || SoundEffect.MasterVoice == null) + return; + + var filter = new FilterParameters { Frequency = 1.0f, OneOverQ = 1.0f, Type = FilterType.LowPassFilter }; + _voice.SetFilterParameters(filter); + } + + private void PlatformDispose(bool disposing) + { + if (disposing) + { + if (_reverb != null) + _reverb.Dispose(); + + if (_voice != null && SoundEffect.MasterVoice != null) + { + _voice.DestroyVoice(); + _voice.Dispose(); + } + } + _voice = null; + _effect = null; + _reverb = null; + } + } +} diff --git a/MonoGame.Framework/Audio/SoundEffectInstance.cs b/MonoGame.Framework/Audio/SoundEffectInstance.cs new file mode 100644 index 00000000000..14e3092fd84 --- /dev/null +++ b/MonoGame.Framework/Audio/SoundEffectInstance.cs @@ -0,0 +1,213 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Audio +{ + /// Represents a single instance of a playing, paused, or stopped sound. + /// + /// SoundEffectInstances are created through SoundEffect.CreateInstance() and used internally by SoundEffect.Play() + /// + public partial class SoundEffectInstance : IDisposable + { + private bool _isDisposed = false; + internal bool _isPooled = true; + internal bool _isXAct; + internal bool _isDynamic; + internal SoundEffect _effect; + private float _pan; + private float _volume; + private float _pitch; + + /// Enables or Disables whether the SoundEffectInstance should repeat after playback. + /// This value has no effect on an already playing sound. + public virtual bool IsLooped + { + get { return PlatformGetIsLooped(); } + set { PlatformSetIsLooped(value); } + } + + /// Gets or sets the pan, or speaker balance.. + /// Pan value ranging from -1.0 (left speaker) to 0.0 (centered), 1.0 (right speaker). Values outside of this range will throw an exception. + public float Pan + { + get { return _pan; } + set + { + if (value < -1.0f || value > 1.0f) + throw new ArgumentOutOfRangeException(); + + _pan = value; + PlatformSetPan(value); + } + } + + /// Gets or sets the pitch adjustment. + /// Pitch adjustment, ranging from -1.0 (down an octave) to 0.0 (no change) to 1.0 (up an octave). Values outside of this range will throw an Exception. + public float Pitch + { + get { return _pitch; } + set + { + // XAct sounds effects don't have pitch limits + if (!_isXAct && (value < -1.0f || value > 1.0f)) + throw new ArgumentOutOfRangeException(); + + _pitch = value; + PlatformSetPitch(value); + } + } + + /// Gets or sets the volume of the SoundEffectInstance. + /// Volume, ranging from 0.0 (silence) to 1.0 (full volume). Volume during playback is scaled by SoundEffect.MasterVolume. + /// + /// This is the volume relative to SoundEffect.MasterVolume. Before playback, this Volume property is multiplied by SoundEffect.MasterVolume when determining the final mix volume. + /// + public float Volume + { + get { return _volume; } + set + { + // XAct sound effects don't have volume limits. + if (!_isXAct && (value < 0.0f || value > 1.0f)) + throw new ArgumentOutOfRangeException(); + + _volume = value; + + // XAct sound effects are not tied to the SoundEffect master volume. + if (_isXAct) + PlatformSetVolume(value); + else + PlatformSetVolume(value * SoundEffect.MasterVolume); + } + } + + /// Gets the SoundEffectInstance's current playback state. + public virtual SoundState State { get { return PlatformGetState(); } } + + /// Indicates whether the object is disposed. + public bool IsDisposed { get { return _isDisposed; } } + + internal SoundEffectInstance() + { + _pan = 0.0f; + _volume = 1.0f; + _pitch = 0.0f; + } + + internal SoundEffectInstance(byte[] buffer, int sampleRate, int channels) + : this() + { + PlatformInitialize(buffer, sampleRate, channels); + } + + /// + /// Releases unmanaged resources and performs other cleanup operations before the + /// is reclaimed by garbage collection. + /// + ~SoundEffectInstance() + { + Dispose(false); + } + + /// Applies 3D positioning to the SoundEffectInstance using a single listener. + /// Data about the listener. + /// Data about the source of emission. + public void Apply3D(AudioListener listener, AudioEmitter emitter) + { + PlatformApply3D(listener, emitter); + } + + /// Applies 3D positioning to the SoundEffectInstance using multiple listeners. + /// Data about each listener. + /// Data about the source of emission. + public void Apply3D(AudioListener[] listeners, AudioEmitter emitter) + { + foreach (var l in listeners) + PlatformApply3D(l, emitter); + } + + /// Pauses playback of a SoundEffectInstance. + /// Paused instances can be resumed with SoundEffectInstance.Play() or SoundEffectInstance.Resume(). + public virtual void Pause() + { + PlatformPause(); + } + + /// Plays or resumes a SoundEffectInstance. + /// Throws an exception if more sounds are playing than the platform allows. + public virtual void Play() + { + if (_isDisposed) + throw new ObjectDisposedException("SoundEffectInstance"); + + if (State == SoundState.Playing) + return; + + // We don't need to check if we're at the instance play limit + // if we're resuming from a paused state. + if (State != SoundState.Paused) + { + if (!SoundEffectInstancePool.SoundsAvailable) + throw new InstancePlayLimitException(); + + SoundEffectInstancePool.Remove(this); + } + + // For non-XAct sounds we need to be sure the latest + // master volume level is applied before playback. + if (!_isXAct) + PlatformSetVolume(_volume * SoundEffect.MasterVolume); + + PlatformPlay(); + } + + /// Resumes playback for a SoundEffectInstance. + /// Only has effect on a SoundEffectInstance in a paused state. + public virtual void Resume() + { + PlatformResume(); + } + + /// Immediately stops playing a SoundEffectInstance. + public virtual void Stop() + { + PlatformStop(true); + } + + /// Stops playing a SoundEffectInstance, either immediately or as authored. + /// Determined whether the sound stops immediately, or after playing its release phase and/or transitions. + /// Stopping a sound with the immediate argument set to false will allow it to play any release phases, such as fade, before coming to a stop. + public virtual void Stop(bool immediate) + { + PlatformStop(immediate); + } + + /// Releases the resources held by this . + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases the resources held by this . + /// + /// If set to true, Dispose was called explicitly. + /// If the disposing parameter is true, the Dispose method was called explicitly. This + /// means that managed objects referenced by this instance should be disposed or released as + /// required. If the disposing parameter is false, Dispose was called by the finalizer and + /// no managed objects should be touched because we do not know if they are still valid or + /// not at that time. Unmanaged resources should always be released. + protected virtual void Dispose(bool disposing) + { + if (!_isDisposed) + { + PlatformDispose(disposing); + _isDisposed = true; + } + } + } +} diff --git a/MonoGame.Framework/Audio/SoundEffectInstancePool.cs b/MonoGame.Framework/Audio/SoundEffectInstancePool.cs new file mode 100644 index 00000000000..a5e65dd4e12 --- /dev/null +++ b/MonoGame.Framework/Audio/SoundEffectInstancePool.cs @@ -0,0 +1,194 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System.Collections.Generic; + +namespace Microsoft.Xna.Framework.Audio +{ + internal static class SoundEffectInstancePool + { + private static readonly List _playingInstances; + private static readonly List _pooledInstances; + + private static readonly object _locker; + + static SoundEffectInstancePool() + { + _locker = new object(); + + // Reduce garbage generation by allocating enough capacity for + // the maximum playing instances or at least some reasonable value. + var maxInstances = SoundEffect.MAX_PLAYING_INSTANCES < 1024 ? SoundEffect.MAX_PLAYING_INSTANCES : 1024; + _playingInstances = new List(maxInstances); + _pooledInstances = new List(maxInstances); + } + + /// + /// Gets a value indicating whether the platform has capacity for more sounds to be played at this time. + /// + /// true if more sounds can be played; otherwise, false. + internal static bool SoundsAvailable + { + get + { + lock(_locker) + return _playingInstances.Count < SoundEffect.MAX_PLAYING_INSTANCES; + } + } + + /// + /// Add the specified instance to the pool if it is a pooled instance and removes it from the + /// list of playing instances. + /// + /// The SoundEffectInstance + internal static void Add(SoundEffectInstance inst) + { + lock (_locker) { + + if (inst._isPooled) + { + _pooledInstances.Add(inst); + inst._effect = null; + } + + _playingInstances.Remove(inst); + + } // lock(_locker) + } + + /// + /// Adds the SoundEffectInstance to the list of playing instances. + /// + /// The SoundEffectInstance to add to the playing list. + internal static void Remove(SoundEffectInstance inst) + { + lock (_locker) + _playingInstances.Add(inst); + } + + /// + /// Returns a pooled SoundEffectInstance if one is available, or allocates a new + /// SoundEffectInstance if the pool is empty. + /// + /// The SoundEffectInstance. + internal static SoundEffectInstance GetInstance(bool forXAct) + { + lock (_locker) { + + SoundEffectInstance inst = null; + var count = _pooledInstances.Count; + if (count > 0) + { + // Grab the item at the end of the list so the remove doesn't copy all + // the list items down one slot. + inst = _pooledInstances[count - 1]; + _pooledInstances.RemoveAt(count - 1); + + // Reset used instance to the "default" state. + inst._isPooled = true; + inst._isXAct = forXAct; + inst.Volume = 1.0f; + inst.Pan = 0.0f; + inst.Pitch = 0.0f; + inst.IsLooped = false; + inst.PlatformSetReverbMix(0); + inst.PlatformClearFilter(); + } + else + { + inst = new SoundEffectInstance(); + inst._isPooled = true; + inst._isXAct = forXAct; + } + + return inst; + + } // lock (_locker) + } + + /// + /// Iterates the list of playing instances, returning them to the pool if they + /// have stopped playing. + /// + internal static void Update() + { + lock (_locker) { + + SoundEffectInstance inst = null; + + // Cleanup instances which have finished playing. + for (var x = 0; x < _playingInstances.Count;) + { + inst = _playingInstances[x]; + + // Don't consume XACT instances... XACT will + // clear this flag when it is done with the wave. + if (inst._isXAct) + { + x++; + continue; + } + + if (inst.IsDisposed || inst.State == SoundState.Stopped || (inst._effect == null && !inst._isDynamic)) + { +#if OPENAL + if (!inst.IsDisposed) + inst.Stop(true); // force stopping it to free its AL source +#endif + Add(inst); + continue; + } + + x++; + } + + } // lock (_locker) + } + + /// + /// Iterates the list of playing instances, stop them and return them to the pool if they are instances of the given SoundEffect. + /// + /// The SoundEffect + internal static void StopPooledInstances(SoundEffect effect) + { + lock (_locker) { + + SoundEffectInstance inst = null; + + for (var x = 0; x < _playingInstances.Count;) + { + inst = _playingInstances[x]; + if (inst._effect == effect) + { + inst.Stop(true); // stop immediatly + Add(inst); + continue; + } + + x++; + } + + } // lock (_locker) + } + + internal static void UpdateMasterVolume() + { + lock (_locker) { + + foreach (var inst in _playingInstances) + { + // XAct sounds are not controlled by the SoundEffect + // master volume, so we can skip them completely. + if (inst._isXAct) + continue; + + // Re-applying the volume to itself will update + // the sound with the current master volume. + inst.Volume = inst.Volume; + } + } + + } // lock (_locker) + } +} diff --git a/MonoGame.Framework/Audio/SoundState.cs b/MonoGame.Framework/Audio/SoundState.cs new file mode 100644 index 00000000000..8604a820f47 --- /dev/null +++ b/MonoGame.Framework/Audio/SoundState.cs @@ -0,0 +1,56 @@ +#region License +/* +Microsoft Public License (Ms-PL) +MonoGame - Copyright © 2009 The MonoGame Team + +All rights reserved. + +This license governs use of the accompanying software. If you use the software, you accept this license. If you do not +accept the license, do not use the software. + +1. Definitions +The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under +U.S. copyright law. + +A "contribution" is the original software, or any additions or changes to the software. +A "contributor" is any person that distributes its contribution under this license. +"Licensed patents" are a contributor's patent claims that read directly on its contribution. + +2. Grant of Rights +(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. +(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. + +3. Conditions and Limitations +(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. +(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, +your patent license from such contributor to the software ends automatically. +(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution +notices that are present in the software. +(D) If you distribute any portion of the software in source code form, you may do so only under this license by including +a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object +code form, you may only do so under a license that complies with this license. +(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees +or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent +permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular +purpose and non-infringement. +*/ +#endregion License + +using System; + +namespace Microsoft.Xna.Framework.Audio +{ + /// Described the playback state of a SoundEffectInstance. + public enum SoundState + { + /// The SoundEffectInstance is currently playing. + Playing, + /// The SoundEffectInstance is currently paused. + Paused, + /// The SoundEffectInstance is currently stopped. + Stopped + } +} + diff --git a/MonoGame.Framework/Audio/Xact/AudioCategory.cs b/MonoGame.Framework/Audio/Xact/AudioCategory.cs new file mode 100644 index 00000000000..f00deb4b15f --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/AudioCategory.cs @@ -0,0 +1,210 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Diagnostics; +using System.IO; +using System.Collections.Generic; + +namespace Microsoft.Xna.Framework.Audio +{ + /// + /// Provides functionality for manipulating multiple sounds at a time. + /// + public struct AudioCategory : IEquatable + { + readonly string _name; + readonly AudioEngine _engine; + readonly List _sounds; + + // This is a bit gross, but we use an array here + // instead of a field since AudioCategory is a struct + // This allows us to save _volume when the user + // holds onto a reference of AudioCategory, or when a cue + // is created/loaded after the volume's already been set. + internal float[] _volume; + + internal bool isBackgroundMusic; + internal bool isPublic; + + internal bool instanceLimit; + internal int maxInstances; + internal MaxInstanceBehavior InstanceBehavior; + + internal CrossfadeType fadeType; + internal float fadeIn; + internal float fadeOut; + + + internal AudioCategory (AudioEngine audioengine, string name, BinaryReader reader) + { + Debug.Assert(audioengine != null); + Debug.Assert(!string.IsNullOrEmpty(name)); + + _sounds = new List(); + _name = name; + _engine = audioengine; + + maxInstances = reader.ReadByte (); + instanceLimit = maxInstances != 0xff; + + fadeIn = (reader.ReadUInt16 () / 1000f); + fadeOut = (reader.ReadUInt16 () / 1000f); + + byte instanceFlags = reader.ReadByte (); + fadeType = (CrossfadeType)(instanceFlags & 0x7); + InstanceBehavior = (MaxInstanceBehavior)(instanceFlags >> 3); + + reader.ReadUInt16 (); //unkn + + var volume = XactHelpers.ParseVolumeFromDecibels(reader.ReadByte()); + _volume = new float[1] { volume }; + + byte visibilityFlags = reader.ReadByte (); + isBackgroundMusic = (visibilityFlags & 0x1) != 0; + isPublic = (visibilityFlags & 0x2) != 0; + } + + internal void AddSound(XactSound sound) + { + _sounds.Add(sound); + } + + internal int GetPlayingInstanceCount() + { + var sum = 0; + for (var i = 0; i < _sounds.Count; i++) + { + if (_sounds[i].Playing) + sum++; + } + return sum; + } + + internal XactSound GetOldestInstance() + { + for (var i = 0; i < _sounds.Count; i++) + { + if (_sounds[i].Playing) + return _sounds[i]; + } + return null; + } + + /// + /// Gets the category's friendly name. + /// + public string Name { get { return _name; } } + + /// + /// Pauses all associated sounds. + /// + public void Pause () + { + foreach (var sound in _sounds) + sound.Pause(); + } + + /// + /// Resumes all associated paused sounds. + /// + public void Resume () + { + foreach (var sound in _sounds) + sound.Resume(); + } + + /// + /// Stops all associated sounds. + /// + public void Stop(AudioStopOptions options) + { + foreach (var sound in _sounds) + sound.Stop(options); + } + + public void SetVolume(float volume) + { + if (volume < 0) + throw new ArgumentException("The volume must be positive."); + + // Updating all the sounds in a category can be + // very expensive... so avoid it if we can. + if (_volume[0] == volume) + return; + + _volume[0] = volume; + + foreach (var sound in _sounds) + sound.UpdateCategoryVolume(volume); + } + + /// + /// Determines whether two AudioCategory instances are equal. + /// + /// First AudioCategory instance to compare. + /// Second AudioCategory instance to compare. + /// true if the objects are equal or false if they aren't. + public static bool operator ==(AudioCategory first, AudioCategory second) + { + return first._engine == second._engine && first._name.Equals(second._name, StringComparison.Ordinal); + } + + /// + /// Determines whether two AudioCategory instances are not equal. + /// + /// First AudioCategory instance to compare. + /// Second AudioCategory instance to compare. + /// true if the objects are not equal or false if they are. + public static bool operator !=(AudioCategory first, AudioCategory second) + { + return first._engine != second._engine || !first._name.Equals(second._name, StringComparison.Ordinal); + } + + /// + /// Determines whether two AudioCategory instances are equal. + /// + /// AudioCategory to compare with this instance. + /// true if the objects are equal or false if they aren't + public bool Equals(AudioCategory other) + { + return _engine == other._engine && _name.Equals(other._name, StringComparison.Ordinal); + } + + /// + /// Determines whether two AudioCategory instances are equal. + /// + /// Object to compare with this instance. + /// true if the objects are equal or false if they aren't. + public override bool Equals(object obj) + { + if (obj is AudioCategory) + { + var other = (AudioCategory)obj; + return _engine == other._engine && _name.Equals(other._name, StringComparison.Ordinal); + } + + return false; + } + + /// + /// Gets the hash code for this instance. + /// + /// Hash code for this object. + public override int GetHashCode() + { + return _name.GetHashCode() ^ _engine.GetHashCode(); + } + + /// + /// Returns the name of this AudioCategory + /// + /// Friendly name of the AudioCategory + public override string ToString() + { + return _name; + } + } +} + diff --git a/MonoGame.Framework/Audio/Xact/AudioEngine.cs b/MonoGame.Framework/Audio/Xact/AudioEngine.cs new file mode 100644 index 00000000000..fff74780136 --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/AudioEngine.cs @@ -0,0 +1,404 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Diagnostics; +using System.IO; +using System.Collections.Generic; + +namespace Microsoft.Xna.Framework.Audio +{ + /// + /// Class used to create and manipulate code audio objects. + /// + public class AudioEngine : IDisposable + { + private readonly AudioCategory[] _categories; + private readonly Dictionary _categoryLookup = new Dictionary(); + + private readonly RpcVariable[] _variables; + private readonly Dictionary _variableLookup = new Dictionary(); + + private readonly RpcVariable[] _cueVariables; + + private readonly Stopwatch _stopwatch; + private TimeSpan _lastUpdateTime; + + private readonly ReverbSettings _reverbSettings; + private readonly RpcCurve[] _reverbCurves; + + internal List ActiveCues = new List(); + + internal AudioCategory[] Categories { get { return _categories; } } + + internal Dictionary Wavebanks = new Dictionary(); + + internal readonly RpcCurve[] RpcCurves; + + internal readonly object UpdateLock = new object(); + + internal RpcVariable[] CreateCueVariables() + { + var clone = new RpcVariable[_cueVariables.Length]; + Array.Copy(_cueVariables, clone, _cueVariables.Length); + return clone; + } + + /// + /// The current content version. + /// + public const int ContentVersion = 39; + + /// Path to a XACT settings file. + public AudioEngine(string settingsFile) + : this(settingsFile, TimeSpan.Zero, "") + { + } + + internal static Stream OpenStream(string filePath, bool useMemoryStream = false) + { + var stream = TitleContainer.OpenStream(filePath); + + // Read the asset into memory in one go. This results in a ~50% reduction + // in load times on Android due to slow Android asset streams. +#if ANDROID + useMemoryStream = true; +#endif + + if (useMemoryStream) + { + var memStream = new MemoryStream(); + stream.CopyTo(memStream); + memStream.Seek(0, SeekOrigin.Begin); + stream.Dispose(); + stream = memStream; + } + + return stream; + } + + /// Path to a XACT settings file. + /// Determines how many milliseconds the engine will look ahead when determing when to transition to another sound. + /// A string that specifies the audio renderer to use. + /// For the best results, use a lookAheadTime of 250 milliseconds or greater. + public AudioEngine(string settingsFile, TimeSpan lookAheadTime, string rendererId) + { + if (string.IsNullOrEmpty(settingsFile)) + throw new ArgumentNullException("settingsFile"); + + // Read the xact settings file + // Credits to alisci01 for initial format documentation + using (var stream = OpenStream(settingsFile)) + using (var reader = new BinaryReader(stream)) + { + uint magic = reader.ReadUInt32 (); + if (magic != 0x46534758) //'XGFS' + throw new ArgumentException ("XGS format not recognized"); + + reader.ReadUInt16 (); // toolVersion + uint formatVersion = reader.ReadUInt16(); + if (formatVersion != 42) + Debug.WriteLine("Warning: XGS format " + formatVersion + " not supported!"); + + reader.ReadUInt16 (); // crc + reader.ReadUInt32 (); // lastModifiedLow + reader.ReadUInt32 (); // lastModifiedHigh + reader.ReadByte (); //unkn, 0x03. Platform? + + uint numCats = reader.ReadUInt16 (); + uint numVars = reader.ReadUInt16 (); + + reader.ReadUInt16 (); //unkn, 0x16 + reader.ReadUInt16 (); //unkn, 0x16 + + uint numRpc = reader.ReadUInt16 (); + uint numDspPresets = reader.ReadUInt16 (); + uint numDspParams = reader.ReadUInt16 (); + + uint catsOffset = reader.ReadUInt32 (); + uint varsOffset = reader.ReadUInt32 (); + + reader.ReadUInt32 (); //unknown, leads to a short with value of 1? + reader.ReadUInt32 (); // catNameIndexOffset + reader.ReadUInt32 (); //unknown, two shorts of values 2 and 3? + reader.ReadUInt32 (); // varNameIndexOffset + + uint catNamesOffset = reader.ReadUInt32 (); + uint varNamesOffset = reader.ReadUInt32 (); + uint rpcOffset = reader.ReadUInt32 (); + reader.ReadUInt32(); // dspPresetsOffset + uint dspParamsOffset = reader.ReadUInt32 (); + + reader.BaseStream.Seek (catNamesOffset, SeekOrigin.Begin); + string[] categoryNames = ReadNullTerminatedStrings(numCats, reader); + + _categories = new AudioCategory[numCats]; + reader.BaseStream.Seek (catsOffset, SeekOrigin.Begin); + for (int i=0; i(); + var cueVariables = new List(); + var globalVariables = new List(); + reader.BaseStream.Seek (varsOffset, SeekOrigin.Begin); + for (var i=0; i < numVars; i++) + { + var v = new RpcVariable(); + v.Name = varNames[i]; + v.Flags = reader.ReadByte(); + v.InitValue = reader.ReadSingle(); + v.MinValue = reader.ReadSingle(); + v.MaxValue = reader.ReadSingle(); + v.Value = v.InitValue; + + variables.Add(v); + if (!v.IsGlobal) + cueVariables.Add(v); + else + { + globalVariables.Add(v); + _variableLookup.Add(v.Name, globalVariables.Count - 1); + } + } + _cueVariables = cueVariables.ToArray(); + _variables = globalVariables.ToArray(); + + var reverbCurves = new List(); + RpcCurves = new RpcCurve[numRpc]; + if (numRpc > 0) + { + reader.BaseStream.Seek(rpcOffset, SeekOrigin.Begin); + for (var i=0; i < numRpc; i++) + { + var curve = new RpcCurve(); + curve.FileOffset = (uint)reader.BaseStream.Position; + + var variable = variables[ reader.ReadUInt16() ]; + if (variable.IsGlobal) + { + curve.IsGlobal = true; + curve.Variable = globalVariables.FindIndex(e => e.Name == variable.Name); + } + else + { + curve.IsGlobal = false; + curve.Variable = cueVariables.FindIndex(e => e.Name == variable.Name); + } + + var pointCount = (int)reader.ReadByte(); + curve.Parameter = (RpcParameter)reader.ReadUInt16(); + + curve.Points = new RpcPoint[pointCount]; + for (var j=0; j < pointCount; j++) + { + curve.Points[j].Position = reader.ReadSingle(); + curve.Points[j].Value = reader.ReadSingle(); + curve.Points[j].Type = (RpcPointType)reader.ReadByte(); + } + + // If the parameter is greater than the max then this is a DSP + // parameter which is for reverb. + var dspParameter = curve.Parameter - RpcParameter.NumParameters; + if (dspParameter >= 0 && variable.IsGlobal) + reverbCurves.Add(curve); + + RpcCurves[i] = curve; + } + } + _reverbCurves = reverbCurves.ToArray(); + + if (numDspPresets > 0) + { + // Note: It seemed like MS designed this to support multiple + // DSP effects, but in practice XACT only has one... Microsoft Reverb. + // + // So because of this we know exactly how many presets and + // parameters we should have. + if (numDspPresets != 1) + throw new Exception("Unexpected number of DSP presets!"); + if (numDspParams != 22) + throw new Exception("Unexpected number of DSP parameters!"); + + reader.BaseStream.Seek(dspParamsOffset, SeekOrigin.Begin); + _reverbSettings = new ReverbSettings(reader); + } + } + + _stopwatch = new Stopwatch(); + _stopwatch.Start(); + } + + internal int GetRpcIndex(uint fileOffset) + { + for (var i = 0; i < RpcCurves.Length; i++) + { + if (RpcCurves[i].FileOffset == fileOffset) + return i; + } + + return -1; + } + + private static string[] ReadNullTerminatedStrings(uint count, BinaryReader reader) + { + var ret = new string[count]; + + for (var i=0; i < count; i++) + { + var s = new List(); + while (reader.PeekChar() != 0) + s.Add(reader.ReadChar()); + + reader.ReadChar(); + ret[i] = new string(s.ToArray()); + } + + return ret; + } + + /// + /// Performs periodic work required by the audio engine. + /// + /// Must be called at least once per frame. + public void Update() + { + var cur = _stopwatch.Elapsed; + var elapsed = cur - _lastUpdateTime; + _lastUpdateTime = cur; + var dt = (float)elapsed.TotalSeconds; + + lock (UpdateLock) + { + for (var x = 0; x < ActiveCues.Count; ) + { + var cue = ActiveCues[x]; + + cue.Update(dt); + + if (cue.IsStopped || cue.IsDisposed) + { + ActiveCues.Remove(cue); + continue; + } + + x++; + } + } + + // The only global curves we can process seem to be + // specifically for the reverb DSP effect. + if (_reverbSettings != null) + { + for (var i = 0; i < _reverbCurves.Length; i++) + { + var curve = _reverbCurves[i]; + var result = curve.Evaluate(_variables[curve.Variable].Value); + var parameter = curve.Parameter - RpcParameter.NumParameters; + _reverbSettings[parameter] = result; + } + + SoundEffect.PlatformSetReverbSettings(_reverbSettings); + } + } + + /// Returns an audio category by name. + /// Friendly name of the category to get. + /// The AudioCategory with a matching name. Throws an exception if not found. + public AudioCategory GetCategory(string name) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentNullException("name"); + + int i; + if (!_categoryLookup.TryGetValue(name, out i)) + throw new InvalidOperationException("This resource could not be created."); + + return _categories[i]; + } + + /// Gets the value of a global variable. + /// Friendly name of the variable. + /// float value of the queried variable. + /// A global variable has global scope. It can be accessed by all code within a project. + public float GetGlobalVariable(string name) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentNullException("name"); + + int i; + if (!_variableLookup.TryGetValue(name, out i) || !_variables[i].IsPublic) + throw new IndexOutOfRangeException("The specified variable index is invalid."); + + lock (UpdateLock) + return _variables[i].Value; + } + + internal float GetGlobalVariable(int index) + { + lock (UpdateLock) + return _variables[index].Value; + } + + /// Sets the value of a global variable. + /// Friendly name of the variable. + /// Value of the global variable. + public void SetGlobalVariable(string name, float value) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentNullException("name"); + + int i; + if (!_variableLookup.TryGetValue(name, out i) || !_variables[i].IsPublic) + throw new IndexOutOfRangeException("The specified variable index is invalid."); + + lock (UpdateLock) + _variables[i].SetValue(value); + } + + /// + /// This event is triggered when the AudioEngine is disposed. + /// + public event EventHandler Disposing; + + /// + /// Is true if the AudioEngine has been disposed. + /// + public bool IsDisposed { get; private set; } + + /// + /// Disposes the AudioEngine. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~AudioEngine() + { + Dispose(false); + } + + private void Dispose(bool disposing) + { + if (IsDisposed) + return; + + IsDisposed = true; + + // TODO: Should we be forcing any active + // audio cues to stop here? + + if (disposing) + EventHelpers.Raise(this, Disposing, EventArgs.Empty); + } + } +} + diff --git a/MonoGame.Framework/Audio/Xact/AudioStopOptions.cs b/MonoGame.Framework/Audio/Xact/AudioStopOptions.cs new file mode 100644 index 00000000000..704c7b51076 --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/AudioStopOptions.cs @@ -0,0 +1,18 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Audio +{ + /// Controls how Cue objects should cease playback when told to stop. + public enum AudioStopOptions + { + /// Stop normally, playing any pending release phases or transitions. + AsAuthored, + /// Immediately stops the cue, ignoring any pending release phases or transitions. + Immediate + } +} + diff --git a/MonoGame.Framework/Audio/Xact/ClipEvent.cs b/MonoGame.Framework/Audio/Xact/ClipEvent.cs new file mode 100644 index 00000000000..c2aace826aa --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/ClipEvent.cs @@ -0,0 +1,36 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.IO; + +namespace Microsoft.Xna.Framework.Audio +{ + abstract class ClipEvent + { + protected XactClip _clip; + + protected ClipEvent(XactClip clip, float timeStamp, float randomOffset) + { + _clip = clip; + TimeStamp = timeStamp; + RandomOffset = randomOffset; + } + + public float RandomOffset { get; private set; } + + public float TimeStamp { get; private set; } + + public abstract void Play(); + public abstract void Stop(); + public abstract void Pause(); + public abstract void Resume(); + public abstract void SetFade(float fadeInDuration, float fadeOutDuration); + public abstract void SetTrackVolume(float volume); + public abstract void SetTrackPan(float pan); + public abstract void SetState(float volume, float pitch, float reverbMix, float? filterFrequency, float? filterQFactor); + public abstract bool Update(float dt); + } +} + diff --git a/MonoGame.Framework/Audio/Xact/CrossfadeType.cs b/MonoGame.Framework/Audio/Xact/CrossfadeType.cs new file mode 100644 index 00000000000..c1594e39579 --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/CrossfadeType.cs @@ -0,0 +1,13 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +namespace Microsoft.Xna.Framework.Audio +{ + enum CrossfadeType + { + Linear, + Logarithmic, + EqualPower, + } +} \ No newline at end of file diff --git a/MonoGame.Framework/Audio/Xact/Cue.cs b/MonoGame.Framework/Audio/Xact/Cue.cs new file mode 100644 index 00000000000..fe0e42dca64 --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/Cue.cs @@ -0,0 +1,378 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Audio +{ + /// Manages the playback of a sound or set of sounds. + /// + /// Cues are comprised of one or more sounds. + /// Cues also define specific properties such as pitch or volume. + /// Cues are referenced through SoundBank objects. + /// + public class Cue : IDisposable + { + private readonly AudioEngine _engine; + private readonly string _name; + private readonly XactSound[] _sounds; + private readonly float[] _probs; + + private readonly RpcVariable[] _variables; + + private XactSound _curSound; + + private bool _applied3D; + private bool _played; + + /// Indicates whether or not the cue is currently paused. + /// IsPlaying and IsPaused both return true if a cue is paused while playing. + public bool IsPaused + { + get + { + if (_curSound != null) + return _curSound.IsPaused; + + return false; + } + } + + /// Indicates whether or not the cue is currently playing. + /// IsPlaying and IsPaused both return true if a cue is paused while playing. + public bool IsPlaying + { + get + { + if (_curSound != null) + return _curSound.Playing; + + return false; + } + } + + /// Indicates whether or not the cue is currently stopped. + public bool IsStopped + { + get + { + if (_curSound != null) + return _curSound.Stopped; + + return !IsDisposed && !IsPrepared; + } + } + + public bool IsStopping + { + get + { + // TODO: Implement me! + return false; + } + } + + public bool IsPreparing + { + get { return false; } + } + + public bool IsPrepared { get; internal set; } + + public bool IsCreated { get; internal set; } + + /// Gets the friendly name of the cue. + /// The friendly name is a value set from the designer. + public string Name + { + get { return _name; } + } + + internal Cue(AudioEngine engine, string cuename, XactSound sound) + { + _engine = engine; + _name = cuename; + _sounds = new XactSound[1]; + _sounds[0] = sound; + _probs = new float[1]; + _probs[0] = 1.0f; + _variables = engine.CreateCueVariables(); + } + + internal Cue(AudioEngine engine, string cuename, XactSound[] sounds, float[] probs) + { + _engine = engine; + _name = cuename; + _sounds = sounds; + _probs = probs; + _variables = engine.CreateCueVariables(); + } + + internal void Prepare() + { + IsDisposed = false; + IsCreated = false; + IsPrepared = true; + _curSound = null; + } + + /// Pauses playback. + public void Pause() + { + lock (_engine.UpdateLock) + { + if (_curSound != null) + _curSound.Pause(); + } + } + + /// Requests playback of a prepared or preparing Cue. + /// Calling Play when the Cue already is playing can result in an InvalidOperationException. + public void Play() + { + lock (_engine.UpdateLock) + { + if (!_engine.ActiveCues.Contains(this)) + _engine.ActiveCues.Add(this); + + //TODO: Probabilities + var index = XactHelpers.Random.Next(_sounds.Length); + _curSound = _sounds[index]; + + var volume = UpdateRpcCurves(); + + _curSound.Play(volume, _engine); + } + + _played = true; + IsPrepared = false; + } + + /// Resumes playback of a paused Cue. + public void Resume() + { + lock (_engine.UpdateLock) + { + if (_curSound != null) + _curSound.Resume(); + } + } + + /// Stops playback of a Cue. + /// Specifies if the sound should play any pending release phases or transitions before stopping. + public void Stop(AudioStopOptions options) + { + lock (_engine.UpdateLock) + { + _engine.ActiveCues.Remove(this); + + if (_curSound != null) + _curSound.Stop(options); + } + + IsPrepared = false; + } + + private int FindVariable(string name) + { + // Do a simple linear search... which is fast + // for as little variables as most cues have. + for (var i = 0; i < _variables.Length; i++) + { + if (_variables[i].Name == name) + return i; + } + + return -1; + } + + /// + /// Sets the value of a cue-instance variable based on its friendly name. + /// + /// Friendly name of the variable to set. + /// Value to assign to the variable. + /// The friendly name is a value set from the designer. + public void SetVariable(string name, float value) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentNullException("name"); + + var i = FindVariable(name); + if (i == -1 || !_variables[i].IsPublic) + throw new IndexOutOfRangeException("The specified variable index is invalid."); + + _variables[i].SetValue(value); + } + + /// Gets a cue-instance variable value based on its friendly name. + /// Friendly name of the variable. + /// Value of the variable. + /// + /// Cue-instance variables are useful when multiple instantiations of a single cue (and its associated sounds) are required (for example, a "car" cue where there may be more than one car at any given time). While a global variable allows multiple audio elements to be controlled in unison, a cue instance variable grants discrete control of each instance of a cue, even for each copy of the same cue. + /// The friendly name is a value set from the designer. + /// + public float GetVariable(string name) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentNullException("name"); + + var i = FindVariable(name); + if (i == -1 || !_variables[i].IsPublic) + throw new IndexOutOfRangeException("The specified variable index is invalid."); + + return _variables[i].Value; + } + + /// Updates the simulated 3D Audio settings calculated between an AudioEmitter and AudioListener. + /// The listener to calculate. + /// The emitter to calculate. + /// + /// This must be called before Play(). + /// Calling this method automatically converts the sound to monoaural and sets the speaker mix for any sound played by this cue to a value calculated with the listener's and emitter's positions. Any stereo information in the sound will be discarded. + /// + public void Apply3D(AudioListener listener, AudioEmitter emitter) + { + if (listener == null) + throw new ArgumentNullException("listener"); + if (emitter == null) + throw new ArgumentNullException("emitter"); + + if (_played && !_applied3D) + throw new InvalidOperationException("You must call Apply3D on a Cue before calling Play to be able to call Apply3D after calling Play."); + + var direction = listener.Position - emitter.Position; + + lock (_engine.UpdateLock) + { + // Set the distance for falloff. + var distance = direction.Length(); + var i = FindVariable("Distance"); + _variables[i].SetValue(distance); + + // Calculate the orientation. + if (distance > 0.0f) + direction /= distance; + var right = Vector3.Cross(listener.Up, listener.Forward); + var slope = Vector3.Dot(direction, listener.Forward); + var angle = MathHelper.ToDegrees((float)Math.Acos(slope)); + var j = FindVariable("OrientationAngle"); + _variables[j].SetValue(angle); + if (_curSound != null) + _curSound.SetCuePan(Vector3.Dot(direction, right)); + + // Calculate doppler effect. + var relativeVelocity = emitter.Velocity - listener.Velocity; + relativeVelocity *= emitter.DopplerScale; + } + + _applied3D = true; + } + + internal void Update(float dt) + { + if (_curSound == null) + return; + + _curSound.Update(dt); + + UpdateRpcCurves(); + } + + private float UpdateRpcCurves() + { + var volume = 1.0f; + + // Evaluate the runtime parameter controls. + var rpcCurves = _curSound.RpcCurves; + if (rpcCurves.Length > 0) + { + var pitch = 0.0f; + var reverbMix = 1.0f; + float? filterFrequency = null; + float? filterQFactor = null; + + for (var i = 0; i < rpcCurves.Length; i++) + { + var rpcCurve = _engine.RpcCurves[rpcCurves[i]]; + + // Some curves are driven by global variables and others by cue instance variables. + float value; + if (rpcCurve.IsGlobal) + value = rpcCurve.Evaluate(_engine.GetGlobalVariable(rpcCurve.Variable)); + else + value = rpcCurve.Evaluate(_variables[rpcCurve.Variable].Value); + + // Process the final curve value based on the parameter type it is. + switch (rpcCurve.Parameter) + { + case RpcParameter.Volume: + volume *= XactHelpers.ParseVolumeFromDecibels(value / 100.0f); + break; + + case RpcParameter.Pitch: + pitch += value / 1000.0f; + break; + + case RpcParameter.ReverbSend: + reverbMix *= XactHelpers.ParseVolumeFromDecibels(value / 100.0f); + break; + + case RpcParameter.FilterFrequency: + filterFrequency = value; + break; + + case RpcParameter.FilterQFactor: + filterQFactor = value; + break; + + default: + throw new ArgumentOutOfRangeException("rpcCurve.Parameter"); + } + } + + pitch = MathHelper.Clamp(pitch, -1.0f, 1.0f); + if (volume < 0.0f) + volume = 0.0f; + + _curSound.UpdateState(_engine, volume, pitch, reverbMix, filterFrequency, filterQFactor); + } + + return volume; + } + + /// + /// This event is triggered when the Cue is disposed. + /// + public event EventHandler Disposing; + + /// + /// Is true if the Cue has been disposed. + /// + public bool IsDisposed { get; internal set; } + + /// + /// Disposes the Cue. + /// + public void Dispose() + { + Dispose(true); + } + + private void Dispose(bool disposing) + { + if (IsDisposed) + return; + + IsDisposed = true; + + if (disposing) + { + IsCreated = false; + IsPrepared = false; + EventHelpers.Raise(this, Disposing, EventArgs.Empty); + } + } + } +} + diff --git a/MonoGame.Framework/Audio/Xact/DspParameter.cs b/MonoGame.Framework/Audio/Xact/DspParameter.cs new file mode 100644 index 00000000000..4ecf9cefe0a --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/DspParameter.cs @@ -0,0 +1,47 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System.IO; + +namespace Microsoft.Xna.Framework.Audio +{ + struct DspParameter + { + public float Value; + public readonly float MinValue; + public readonly float MaxValue; + + public DspParameter(BinaryReader reader) + { + // This is 1 if the type is byte sized and 0 for + // floats... not sure if we should use this info. + reader.ReadByte(); + + // The value and the min/max range for limiting the + // results from the RPC curve when animated. + Value = reader.ReadSingle(); + MinValue = reader.ReadSingle(); + MaxValue = reader.ReadSingle(); + + // Looks to always be zero... maybe some padding + // for future expansion that never occured? + reader.ReadUInt16(); + } + + public void SetValue(float value) + { + if (value < MinValue) + Value = MinValue; + else if (value > MaxValue) + Value = MaxValue; + else + Value = value; + } + + public override string ToString() + { + return "Value:" + Value + " MinValue:" + MinValue + " MaxValue:" + MaxValue; + } + } +} \ No newline at end of file diff --git a/MonoGame.Framework/Audio/Xact/FilterMode.cs b/MonoGame.Framework/Audio/Xact/FilterMode.cs new file mode 100644 index 00000000000..e2970c2c8b5 --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/FilterMode.cs @@ -0,0 +1,13 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +namespace Microsoft.Xna.Framework.Audio +{ + enum FilterMode + { + LowPass = 0, + BandPass = 1, + HighPass = 2, + } +} \ No newline at end of file diff --git a/MonoGame.Framework/Audio/Xact/MaxInstanceBehavior.cs b/MonoGame.Framework/Audio/Xact/MaxInstanceBehavior.cs new file mode 100644 index 00000000000..6bc9e61a1c4 --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/MaxInstanceBehavior.cs @@ -0,0 +1,15 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +namespace Microsoft.Xna.Framework.Audio +{ + enum MaxInstanceBehavior + { + FailToPlay, + Queue, + ReplaceOldest, + ReplaceQuietest, + ReplaceLowestPriority, + } +} \ No newline at end of file diff --git a/MonoGame.Framework/Audio/Xact/MiniFormatTag.cs b/MonoGame.Framework/Audio/Xact/MiniFormatTag.cs new file mode 100644 index 00000000000..ee94888329a --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/MiniFormatTag.cs @@ -0,0 +1,17 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +namespace Microsoft.Xna.Framework.Audio +{ + enum MiniFormatTag + { + Pcm = 0x0, + Xma = 0x1, + Adpcm = 0x2, + Wma = 0x3, + + // We allow XMA to be reused for a platform specific format. + PlatformSpecific = Xma, + } +} \ No newline at end of file diff --git a/MonoGame.Framework/Audio/Xact/PlayWaveEvent.cs b/MonoGame.Framework/Audio/Xact/PlayWaveEvent.cs new file mode 100644 index 00000000000..1192057a9e4 --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/PlayWaveEvent.cs @@ -0,0 +1,303 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.IO; + +namespace Microsoft.Xna.Framework.Audio +{ + enum VariationType + { + Ordered, + OrderedFromRandom, + Random, + RandomNoImmediateRepeats, + Shuffle + }; + + class PlayWaveEvent : ClipEvent + { + private readonly SoundBank _soundBank; + + private readonly VariationType _variation; + + private readonly int _loopCount; + + private readonly bool _newWaveOnLoop; + + private readonly int[] _tracks; + private readonly int[] _waveBanks; + + private readonly byte[] _weights; + private readonly int _totalWeights; + + private float _trackVolume; + private float _trackPitch; + private float _trackFilterFrequency; + private float _trackFilterQFactor; + + private float _clipVolume; + private float _clipPitch; + private float _clipReverbMix; + + private readonly Vector4? _filterVar; + private readonly Vector2? _volumeVar; + private readonly Vector2? _pitchVar; + + private int _wavIndex; + private int _loopIndex; + + private SoundEffectInstance _wav; + private bool _streaming; + + public PlayWaveEvent( XactClip clip, float timeStamp, float randomOffset, SoundBank soundBank, + int[] waveBanks, int[] tracks, byte[] weights, int totalWeights, + VariationType variation, Vector2? volumeVar, Vector2? pitchVar, Vector4? filterVar, + int loopCount, bool newWaveOnLoop) + : base(clip, timeStamp, randomOffset) + { + _soundBank = soundBank; + _waveBanks = waveBanks; + _tracks = tracks; + _weights = weights; + _totalWeights = totalWeights; + _volumeVar = volumeVar; + _pitchVar = pitchVar; + _filterVar = filterVar; + _wavIndex = -1; + _loopIndex = 0; + + _trackVolume = 1.0f; + _trackPitch = 0; + _trackFilterFrequency = 0; + _trackFilterQFactor = 0; + + _clipVolume = 1.0f; + _clipPitch = 0; + _clipReverbMix = 0; + + _variation = variation; + _loopCount = loopCount; + _newWaveOnLoop = newWaveOnLoop; + } + + public override void Play() + { + if (_wav != null) + { + if (_wav.State != SoundState.Stopped) + _wav.Stop(); + if (_streaming) + _wav.Dispose(); + else + _wav._isXAct = false; + _wav = null; + } + + Play(true); + } + + private void Play(bool pickNewWav) + { + var trackCount = _tracks.Length; + + // Do we need to pick a new wav to play first? + if (pickNewWav) + { + switch (_variation) + { + case VariationType.Ordered: + _wavIndex = (_wavIndex + 1) % trackCount; + break; + + case VariationType.OrderedFromRandom: + _wavIndex = (_wavIndex + 1) % trackCount; + break; + + case VariationType.Random: + if (_weights == null || trackCount == 1) + _wavIndex = XactHelpers.Random.Next() % trackCount; + else + { + var sum = XactHelpers.Random.Next(_totalWeights); + for (var i=0; i < trackCount; i++) + { + sum -= _weights[i]; + if (sum <= 0) + { + _wavIndex = i; + break; + } + } + } + break; + + case VariationType.RandomNoImmediateRepeats: + { + if (_weights == null || trackCount == 1) + _wavIndex = XactHelpers.Random.Next() % trackCount; + else + { + var last = _wavIndex; + var sum = XactHelpers.Random.Next(_totalWeights); + for (var i=0; i < trackCount; i++) + { + sum -= _weights[i]; + if (sum <= 0) + { + _wavIndex = i; + break; + } + } + + if (_wavIndex == last) + _wavIndex = (_wavIndex + 1) % trackCount; + } + break; + } + + case VariationType.Shuffle: + // TODO: Need some sort of deck implementation. + _wavIndex = XactHelpers.Random.Next() % trackCount; + break; + }; + } + + _wav = _soundBank.GetSoundEffectInstance(_waveBanks[_wavIndex], _tracks[_wavIndex], out _streaming); + if (_wav == null) + { + // We couldn't create a sound effect instance, most likely + // because we've reached the sound pool limits. + return; + } + + // Do all the randoms before we play. + if (_volumeVar.HasValue) + _trackVolume = _volumeVar.Value.X + ((float)XactHelpers.Random.NextDouble() * _volumeVar.Value.Y); + if (_pitchVar.HasValue) + _trackPitch = _pitchVar.Value.X + ((float)XactHelpers.Random.NextDouble() * _pitchVar.Value.Y); + if (_clip.FilterEnabled) + { + if (_filterVar.HasValue) + { + _trackFilterFrequency = _filterVar.Value.X + ((float)XactHelpers.Random.NextDouble() * _filterVar.Value.Y); + _trackFilterQFactor = _filterVar.Value.Z + ((float)XactHelpers.Random.NextDouble() * _filterVar.Value.W); + } + else + { + _trackFilterFrequency = _clip.FilterFrequency; + _trackFilterQFactor = _clip.FilterQ; + } + } + + // This is a shortcut for infinite looping of a single track. + _wav.IsLooped = _loopCount == 255 && trackCount == 1; + + // Update all the wave states then play. + UpdateState(); + _wav.Play(); + } + + public override void Stop() + { + if (_wav != null) + { + _wav.Stop(); + if (_streaming) + _wav.Dispose(); + else + _wav._isXAct = false; + _wav = null; + } + _loopIndex = 0; + } + + public override void Pause() + { + if (_wav != null) + _wav.Pause(); + } + + public override void Resume() + { + if (_wav != null && _wav.State == SoundState.Paused) + _wav.Resume(); + } + + public override void SetTrackVolume(float volume) + { + _clipVolume = volume; + if (_wav != null) + _wav.Volume = _trackVolume * _clipVolume; + } + + public override void SetTrackPan(float pan) + { + if (_wav != null) + _wav.Pan = pan; + } + + public override void SetState(float volume, float pitch, float reverbMix, float? filterFrequency, float? filterQFactor) + { + _clipVolume = volume; + _clipPitch = pitch; + _clipReverbMix = reverbMix; + + // The RPC filter overrides the randomized track filter. + if (filterFrequency.HasValue) + _trackFilterFrequency = filterFrequency.Value; + if (filterQFactor.HasValue) + _trackFilterQFactor = filterQFactor.Value; + + if (_wav != null) + UpdateState(); + } + + private void UpdateState() + { + _wav.Volume = _trackVolume * _clipVolume; + _wav.Pitch = _trackPitch + _clipPitch; + + if (_clip.UseReverb) + _wav.PlatformSetReverbMix(_clipReverbMix); + if (_clip.FilterEnabled) + _wav.PlatformSetFilter(_clip.FilterMode, _trackFilterQFactor, _trackFilterFrequency); + } + + public override void SetFade(float fadeInDuration, float fadeOutDuration) + { + // TODO + } + + public override bool Update(float dt) + { + if (_wav != null && _wav.State == SoundState.Stopped) + { + // If we're not looping or reached our loop + // limit then we can stop. + if (_loopCount == 0 || _loopIndex >= _loopCount) + { + if (_streaming) + _wav.Dispose(); + else + _wav._isXAct = false; + _wav = null; + _loopIndex = 0; + } + else + { + // Increment the loop count if it isn't infinite. + if (_loopCount != 255) + ++_loopIndex; + + // Play the next track. + Play(_newWaveOnLoop); + } + } + + return _wav != null; + } + } +} + diff --git a/MonoGame.Framework/Audio/Xact/ReverbSettings.cs b/MonoGame.Framework/Audio/Xact/ReverbSettings.cs new file mode 100644 index 00000000000..6c414f5f596 --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/ReverbSettings.cs @@ -0,0 +1,69 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.IO; + +namespace Microsoft.Xna.Framework.Audio +{ + class ReverbSettings + { + private readonly DspParameter[] _parameters = new DspParameter[22]; + + public ReverbSettings(BinaryReader reader) + { + _parameters[0] = new DspParameter(reader); // ReflectionsDelayMs + _parameters[1] = new DspParameter(reader); // ReverbDelayMs + _parameters[2] = new DspParameter(reader); // PositionLeft + _parameters[3] = new DspParameter(reader); // PositionRight + _parameters[4] = new DspParameter(reader); // PositionLeftMatrix + _parameters[5] = new DspParameter(reader); // PositionRightMatrix + _parameters[6] = new DspParameter(reader); // EarlyDiffusion + _parameters[7] = new DspParameter(reader); // LateDiffusion + _parameters[8] = new DspParameter(reader); // LowEqGain + _parameters[9] = new DspParameter(reader); // LowEqCutoff + _parameters[10] = new DspParameter(reader); // HighEqGain + _parameters[11] = new DspParameter(reader); // HighEqCutoff + _parameters[12] = new DspParameter(reader); // RearDelayMs + _parameters[13] = new DspParameter(reader); // RoomFilterFrequencyHz + _parameters[14] = new DspParameter(reader); // RoomFilterMainDb + _parameters[15] = new DspParameter(reader); // RoomFilterHighFrequencyDb + _parameters[16] = new DspParameter(reader); // ReflectionsGainDb + _parameters[17] = new DspParameter(reader); // ReverbGainDb + _parameters[18] = new DspParameter(reader); // DecayTimeSec + _parameters[19] = new DspParameter(reader); // DensityPct + _parameters[20] = new DspParameter(reader); // RoomSizeFeet + _parameters[21] = new DspParameter(reader); // WetDryMixPct + } + + public float this[int index] + { + get { return _parameters[index].Value; } + set { _parameters[index].SetValue(value); } + } + + public float ReflectionsDelayMs { get { return _parameters[0].Value; } } + public float ReverbDelayMs { get { return _parameters[1].Value; } } + public float PositionLeft { get { return _parameters[2].Value; } } + public float PositionRight { get { return _parameters[3].Value; } } + public float PositionLeftMatrix { get { return _parameters[4].Value; } } + public float PositionRightMatrix { get { return _parameters[5].Value; } } + public float EarlyDiffusion { get { return _parameters[6].Value; } } + public float LateDiffusion { get { return _parameters[7].Value; } } + public float LowEqGain { get { return _parameters[8].Value; } } + public float LowEqCutoff { get { return _parameters[9].Value; } } + public float HighEqGain { get { return _parameters[10].Value; } } + public float HighEqCutoff { get { return _parameters[11].Value; } } + public float RearDelayMs { get { return _parameters[12].Value; } } + public float RoomFilterFrequencyHz { get { return _parameters[13].Value; } } + public float RoomFilterMainDb { get { return _parameters[14].Value; } } + public float RoomFilterHighFrequencyDb { get { return _parameters[15].Value; } } + public float ReflectionsGainDb { get { return _parameters[16].Value; } } + public float ReverbGainDb { get { return _parameters[17].Value; } } + public float DecayTimeSec { get { return _parameters[18].Value; } } + public float DensityPct { get { return _parameters[19].Value; } } + public float RoomSizeFeet { get { return _parameters[20].Value; } } + public float WetDryMixPct { get { return _parameters[21].Value; } } + } +} \ No newline at end of file diff --git a/MonoGame.Framework/Audio/Xact/RpcCurve.cs b/MonoGame.Framework/Audio/Xact/RpcCurve.cs new file mode 100644 index 00000000000..bba1861d8bf --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/RpcCurve.cs @@ -0,0 +1,47 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +namespace Microsoft.Xna.Framework.Audio +{ + struct RpcCurve + { + public uint FileOffset; + public int Variable; + public bool IsGlobal; + public RpcParameter Parameter; + public RpcPoint[] Points; + + public float Evaluate(float position) + { + // TODO: We need to implement the different RpcPointTypes. + + var first = Points[0]; + if (position <= first.Position) + return first.Value; + + var second = Points[Points.Length - 1]; + if (position >= second.Position) + return second.Value; + + for (var i = 1; i < Points.Length; ++i) + { + second = Points[i]; + if (second.Position >= position) + break; + + first = second; + } + + switch (first.Type) + { + default: + case RpcPointType.Linear: + { + var t = (position - first.Position) / (second.Position - first.Position); + return first.Value + ((second.Value - first.Value) * t); + } + } + } + } +} \ No newline at end of file diff --git a/MonoGame.Framework/Audio/Xact/RpcParameter.cs b/MonoGame.Framework/Audio/Xact/RpcParameter.cs new file mode 100644 index 00000000000..20b3d34d2fd --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/RpcParameter.cs @@ -0,0 +1,16 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +namespace Microsoft.Xna.Framework.Audio +{ + enum RpcParameter + { + Volume, + Pitch, + ReverbSend, + FilterFrequency, + FilterQFactor, + NumParameters, + } +} \ No newline at end of file diff --git a/MonoGame.Framework/Audio/Xact/RpcPoint.cs b/MonoGame.Framework/Audio/Xact/RpcPoint.cs new file mode 100644 index 00000000000..68be3816de9 --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/RpcPoint.cs @@ -0,0 +1,13 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +namespace Microsoft.Xna.Framework.Audio +{ + struct RpcPoint + { + public RpcPointType Type; + public float Position; + public float Value; + } +} \ No newline at end of file diff --git a/MonoGame.Framework/Audio/Xact/RpcPointType.cs b/MonoGame.Framework/Audio/Xact/RpcPointType.cs new file mode 100644 index 00000000000..f70e1e949a2 --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/RpcPointType.cs @@ -0,0 +1,14 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +namespace Microsoft.Xna.Framework.Audio +{ + enum RpcPointType + { + Linear, + Fast, + Slow, + SinCos + } +} \ No newline at end of file diff --git a/MonoGame.Framework/Audio/Xact/RpcVariable.cs b/MonoGame.Framework/Audio/Xact/RpcVariable.cs new file mode 100644 index 00000000000..39b76400f58 --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/RpcVariable.cs @@ -0,0 +1,46 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +namespace Microsoft.Xna.Framework.Audio +{ + struct RpcVariable + { + public string Name; + public float Value; + public byte Flags; + public float InitValue; + public float MaxValue; + public float MinValue; + + public bool IsPublic + { + get { return (Flags & 0x1) != 0; } + } + + public bool IsReadOnly + { + get { return (Flags & 0x2) != 0; } + } + + public bool IsGlobal + { + get { return (Flags & 0x4) == 0; } + } + + public bool IsReserved + { + get { return (Flags & 0x8) != 0; } + } + + public void SetValue(float value) + { + if (value < MinValue) + Value = MinValue; + else if (value > MaxValue) + Value = MaxValue; + else + Value = value; + } + } +} \ No newline at end of file diff --git a/MonoGame.Framework/Audio/Xact/SoundBank.cs b/MonoGame.Framework/Audio/Xact/SoundBank.cs new file mode 100644 index 00000000000..afd2048aac5 --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/SoundBank.cs @@ -0,0 +1,355 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.IO; +using System.Diagnostics; +using System.Collections.Generic; +using MonoGame.Utilities; + +namespace Microsoft.Xna.Framework.Audio +{ + /// Represents a collection of Cues. + public class SoundBank : IDisposable + { + readonly AudioEngine _audioengine; + readonly string[] _waveBankNames; + readonly WaveBank[] _waveBanks; + + readonly float [] defaultProbability = new float [1] { 1.0f }; + readonly Dictionary _sounds = new Dictionary(); + readonly Dictionary _probabilities = new Dictionary (); + + /// + /// Is true if the SoundBank has any live Cues in use. + /// + public bool IsInUse { get; private set; } + + /// AudioEngine that will be associated with this sound bank. + /// Path to a .xsb SoundBank file. + public SoundBank(AudioEngine audioEngine, string fileName) + { + if (audioEngine == null) + throw new ArgumentNullException("audioEngine"); + if (string.IsNullOrEmpty(fileName)) + throw new ArgumentNullException("fileName"); + + _audioengine = audioEngine; + + using (var stream = AudioEngine.OpenStream(fileName, true)) + using (var reader = new BinaryReader(stream)) + { + // Thanks to Liandril for "xactxtract" for some of the offsets. + + uint magic = reader.ReadUInt32(); + if (magic != 0x4B424453) //"SDBK" + throw new Exception ("Bad soundbank format"); + + reader.ReadUInt16(); // toolVersion + + uint formatVersion = reader.ReadUInt16(); + if (formatVersion != 43) + Debug.WriteLine("Warning: SoundBank format {0} not supported.", formatVersion); + + reader.ReadUInt16(); // crc, TODO: Verify crc (FCS16) + + reader.ReadUInt32(); // lastModifiedLow + reader.ReadUInt32(); // lastModifiedHigh + reader.ReadByte(); // platform ??? + + uint numSimpleCues = reader.ReadUInt16(); + uint numComplexCues = reader.ReadUInt16(); + reader.ReadUInt16(); //unkn + reader.ReadUInt16(); // numTotalCues + uint numWaveBanks = reader.ReadByte(); + reader.ReadUInt16(); // numSounds + uint cueNameTableLen = reader.ReadUInt16(); + reader.ReadUInt16(); //unkn + + uint simpleCuesOffset = reader.ReadUInt32(); + uint complexCuesOffset = reader.ReadUInt32(); //unkn + uint cueNamesOffset = reader.ReadUInt32(); + reader.ReadUInt32(); //unkn + reader.ReadUInt32(); // variationTablesOffset + reader.ReadUInt32(); //unkn + uint waveBankNameTableOffset = reader.ReadUInt32(); + reader.ReadUInt32(); // cueNameHashTableOffset + reader.ReadUInt32(); // cueNameHashValsOffset + reader.ReadUInt32(); // soundsOffset + + //name = System.Text.Encoding.UTF8.GetString(soundbankreader.ReadBytes(64),0,64).Replace("\0",""); + + //parse wave bank name table + stream.Seek(waveBankNameTableOffset, SeekOrigin.Begin); + _waveBanks = new WaveBank[numWaveBanks]; + _waveBankNames = new string[numWaveBanks]; + for (int i=0; i 0) + { + stream.Seek(simpleCuesOffset, SeekOrigin.Begin); + for (int i = 0; i < numSimpleCues; i++) + { + reader.ReadByte(); // flags + uint soundOffset = reader.ReadUInt32(); + + var oldPosition = stream.Position; + stream.Seek(soundOffset, SeekOrigin.Begin); + XactSound sound = new XactSound(audioEngine, this, reader); + stream.Seek(oldPosition, SeekOrigin.Begin); + + _sounds.Add(cueNames [i], new XactSound [] { sound } ); + _probabilities.Add (cueNames [i], defaultProbability); + } + } + + // Complex cues + if (numComplexCues > 0) + { + stream.Seek(complexCuesOffset, SeekOrigin.Begin); + for (int i = 0; i < numComplexCues; i++) + { + byte flags = reader.ReadByte(); + if (((flags >> 2) & 1) != 0) + { + uint soundOffset = reader.ReadUInt32(); + reader.ReadUInt32(); //unkn + + var oldPosition = stream.Position; + stream.Seek(soundOffset, SeekOrigin.Begin); + XactSound sound = new XactSound(audioEngine, this, reader); + stream.Seek(oldPosition, SeekOrigin.Begin); + + _sounds.Add (cueNames [numSimpleCues + i], new XactSound [] { sound }); + _probabilities.Add (cueNames [numSimpleCues + i], defaultProbability); + } + else + { + uint variationTableOffset = reader.ReadUInt32(); + reader.ReadUInt32(); // transitionTableOffset + + //parse variation table + long savepos = stream.Position; + stream.Seek(variationTableOffset, SeekOrigin.Begin); + + uint numEntries = reader.ReadUInt16(); + uint variationflags = reader.ReadUInt16(); + reader.ReadByte(); + reader.ReadUInt16(); + reader.ReadByte(); + + XactSound[] cueSounds = new XactSound[numEntries]; + float[] probs = new float[numEntries]; + + uint tableType = (variationflags >> 3) & 0x7; + for (int j = 0; j < numEntries; j++) + { + switch (tableType) + { + case 0: //Wave + { + int trackIndex = reader.ReadUInt16(); + int waveBankIndex = reader.ReadByte(); + reader.ReadByte(); // weightMin + reader.ReadByte(); // weightMax + + cueSounds[j] = new XactSound(this, waveBankIndex, trackIndex); + break; + } + case 1: + { + uint soundOffset = reader.ReadUInt32(); + reader.ReadByte(); // weightMin + reader.ReadByte(); // weightMax + + var oldPosition = stream.Position; + stream.Seek(soundOffset, SeekOrigin.Begin); + cueSounds[j] = new XactSound(audioEngine, this, reader); + stream.Seek(oldPosition, SeekOrigin.Begin); + break; + } + case 3: + { + uint soundOffset = reader.ReadUInt32(); + reader.ReadSingle(); // weightMin + reader.ReadSingle(); // weightMax + reader.ReadUInt32(); // flags + + var oldPosition = stream.Position; + stream.Seek(soundOffset, SeekOrigin.Begin); + cueSounds[j] = new XactSound(audioEngine, this, reader); + stream.Seek(oldPosition, SeekOrigin.Begin); + break; + } + case 4: //CompactWave + { + int trackIndex = reader.ReadUInt16(); + int waveBankIndex = reader.ReadByte(); + cueSounds[j] = new XactSound(this, waveBankIndex, trackIndex); + break; + } + default: + throw new NotSupportedException(); + } + } + + stream.Seek(savepos, SeekOrigin.Begin); + + _sounds.Add (cueNames [numSimpleCues + i], cueSounds); + _probabilities.Add (cueNames [numSimpleCues + i], probs); + } + + // Instance limiting + reader.ReadByte(); //instanceLimit + reader.ReadUInt16(); //fadeInSec, divide by 1000.0f + reader.ReadUInt16(); //fadeOutSec, divide by 1000.0f + reader.ReadByte(); //instanceFlags + } + } + } + } + + internal SoundEffectInstance GetSoundEffectInstance(int waveBankIndex, int trackIndex, out bool streaming) + { + var waveBank = _waveBanks[waveBankIndex]; + + // If the wave bank has not been resolved then do so now. + if (waveBank == null) + { + var name = _waveBankNames[waveBankIndex]; + if (!_audioengine.Wavebanks.TryGetValue(name, out waveBank)) + throw new Exception("The wave bank '" + name + "' was not found!"); + _waveBanks[waveBankIndex] = waveBank; + } + + return waveBank.GetSoundEffectInstance(trackIndex, out streaming); + } + + /// + /// Returns a pooled Cue object. + /// + /// Friendly name of the cue to get. + /// a unique Cue object from a pool. + /// + /// Cue instances are unique, even when sharing the same name. This allows multiple instances to simultaneously play. + /// + public Cue GetCue(string name) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentNullException("name"); + + XactSound[] sounds; + if (!_sounds.TryGetValue(name, out sounds)) + throw new ArgumentException(); + + float [] probs; + if (!_probabilities.TryGetValue (name, out probs)) + throw new ArgumentException (); + + IsInUse = true; + + var cue = new Cue (_audioengine, name, sounds, probs); + cue.Prepare(); + return cue; + } + + /// + /// Plays a cue. + /// + /// Name of the cue to play. + public void PlayCue(string name) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentNullException("name"); + + XactSound[] sounds; + if (!_sounds.TryGetValue(name, out sounds)) + throw new ArgumentException(); + + float [] probs; + if (!_probabilities.TryGetValue (name, out probs)) + throw new ArgumentException (); + + IsInUse = true; + var cue = new Cue (_audioengine, name, sounds, probs); + cue.Prepare(); + cue.Play(); + } + + /// + /// Plays a cue with static 3D positional information. + /// + /// + /// Commonly used for short lived effects. To dynamically change the 3D + /// positional information on a cue over time use and . + /// The name of the cue to play. + /// The listener state. + /// The cue emitter state. + public void PlayCue(string name, AudioListener listener, AudioEmitter emitter) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentNullException("name"); + + XactSound[] sounds; + if (!_sounds.TryGetValue(name, out sounds)) + throw new InvalidOperationException(); + + float [] probs; + if (!_probabilities.TryGetValue (name, out probs)) + throw new ArgumentException (); + + IsInUse = true; + + var cue = new Cue (_audioengine, name, sounds, probs); + cue.Prepare(); + cue.Apply3D(listener, emitter); + cue.Play(); + } + + /// + /// This event is triggered when the SoundBank is disposed. + /// + public event EventHandler Disposing; + + /// + /// Is true if the SoundBank has been disposed. + /// + public bool IsDisposed { get; private set; } + + /// + /// Disposes the SoundBank. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~SoundBank() + { + Dispose(false); + } + + private void Dispose(bool disposing) + { + if (IsDisposed) + return; + + IsDisposed = true; + + if (disposing) + { + IsInUse = false; + EventHelpers.Raise(this, Disposing, EventArgs.Empty); + } + } + } +} + diff --git a/MonoGame.Framework/Audio/Xact/VolumeEvent.cs b/MonoGame.Framework/Audio/Xact/VolumeEvent.cs new file mode 100644 index 00000000000..49adbf9dc29 --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/VolumeEvent.cs @@ -0,0 +1,60 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.IO; + +namespace Microsoft.Xna.Framework.Audio +{ + class VolumeEvent : ClipEvent + { + private readonly float _volume; + + public VolumeEvent(XactClip clip, float timeStamp, float randomOffset, float volume) + : base(clip, timeStamp, randomOffset) + { + _volume = volume; + } + + public override void Play() + { + _clip.SetVolume(_volume); + } + + public override void Stop() + { + } + + public override void Pause() + { + } + + public override void Resume() + { + } + + public override void SetTrackVolume(float volume) + { + } + + public override void SetTrackPan(float pan) + { + } + + public override void SetState(float volume, float pitch, float reverbMix, float? filterFrequency, float? filterQFactor) + { + } + + public override bool Update(float dt) + { + return false; + } + + public override void SetFade(float fadeInDuration, float fadeOutDuration) + { + } + + } +} + diff --git a/MonoGame.Framework/Audio/Xact/WaveBank.Default.cs b/MonoGame.Framework/Audio/Xact/WaveBank.Default.cs new file mode 100644 index 00000000000..a495a94190e --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/WaveBank.Default.cs @@ -0,0 +1,17 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Audio +{ + partial class WaveBank + { + private SoundEffectInstance PlatformCreateStream(StreamInfo stream) + { + throw new NotImplementedException("XACT streaming is not implemented on this platform."); + } + } +} + diff --git a/MonoGame.Framework/Audio/Xact/WaveBank.cs b/MonoGame.Framework/Audio/Xact/WaveBank.cs new file mode 100644 index 00000000000..165e9a7ec12 --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/WaveBank.cs @@ -0,0 +1,420 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.IO; + +namespace Microsoft.Xna.Framework.Audio +{ + /// Represents a collection of wave files. + public partial class WaveBank : IDisposable + { + private readonly SoundEffect[] _sounds; + private readonly StreamInfo[] _streams; + private readonly string _bankName; + private readonly string _waveBankFileName; + private readonly bool _streaming; + private readonly int _offset; + private readonly int _packetSize; + + private readonly int _version; + private readonly int _playRegionOffset; + + struct Segment + { + public int Offset; + public int Length; + } + + struct WaveBankHeader + { + public int Version; + public Segment[] Segments; + } + + struct WaveBankData + { + public int Flags; // Bank flags + public int EntryCount; // Number of entries in the bank + public string BankName; // Bank friendly name + public int EntryMetaDataElementSize; // Size of each entry meta-data element, in bytes + public int EntryNameElementSize; // Size of each entry name element, in bytes + public int Alignment; // Entry alignment, in bytes + public int CompactFormat; // Format data for compact bank + public int BuildTime; // Build timestamp + } + + struct StreamInfo + { + public int Format; + public int FileOffset; + public int FileLength; + public int LoopStart; + public int LoopLength; + } + + private const int Flag_EntryNames = 0x00010000; // Bank includes entry names + private const int Flag_Compact = 0x00020000; // Bank uses compact format + private const int Flag_SyncDisabled = 0x00040000; // Bank is disabled for audition sync + private const int Flag_SeekTables = 0x00080000; // Bank includes seek tables. + private const int Flag_Mask = 0x000F0000; + + /// + /// + public bool IsInUse { get; private set; } + + /// + /// + public bool IsPrepared { get; private set; } + + /// Instance of the AudioEngine to associate this wave bank with. + /// Path to the .xwb file to load. + /// This constructor immediately loads all wave data into memory at once. + public WaveBank(AudioEngine audioEngine, string nonStreamingWaveBankFilename) + : this(audioEngine, nonStreamingWaveBankFilename, false, 0, 0) + { + } + + private WaveBank(AudioEngine audioEngine, string waveBankFilename, bool streaming, int offset, int packetsize) + { + if (audioEngine == null) + throw new ArgumentNullException("audioEngine"); + if (string.IsNullOrEmpty(waveBankFilename)) + throw new ArgumentNullException("nonStreamingWaveBankFilename"); + + // Is this a streaming wavebank? + if (streaming) + { + if (offset != 0) + throw new ArgumentException("We only support a zero offset in streaming banks.", "offset"); + if (packetsize < 2) + throw new ArgumentException("The packet size must be greater than 2.", "packetsize"); + + _streaming = true; + _offset = offset; + _packetSize = packetsize; + } + + //XWB PARSING + //Adapted from MonoXNA + //Originally adaped from Luigi Auriemma's unxwb + + WaveBankHeader wavebankheader; + WaveBankData wavebankdata; + + wavebankdata.EntryNameElementSize = 0; + wavebankdata.CompactFormat = 0; + wavebankdata.Alignment = 0; + wavebankdata.BuildTime = 0; + + int wavebank_offset = 0; + + _waveBankFileName = waveBankFilename; + + BinaryReader reader = new BinaryReader(AudioEngine.OpenStream(waveBankFilename)); + + reader.ReadBytes(4); + + _version = wavebankheader.Version = reader.ReadInt32(); + + int last_segment = 4; + //if (wavebankheader.Version == 1) goto WAVEBANKDATA; + if (wavebankheader.Version <= 3) last_segment = 3; + if (wavebankheader.Version >= 42) reader.ReadInt32(); // skip HeaderVersion + + wavebankheader.Segments = new Segment[5]; + + for (int i = 0; i <= last_segment; i++) + { + wavebankheader.Segments[i].Offset = reader.ReadInt32(); + wavebankheader.Segments[i].Length = reader.ReadInt32(); + } + + reader.BaseStream.Seek(wavebankheader.Segments[0].Offset, SeekOrigin.Begin); + + //WAVEBANKDATA: + + wavebankdata.Flags = reader.ReadInt32(); + wavebankdata.EntryCount = reader.ReadInt32(); + + if ((wavebankheader.Version == 2) || (wavebankheader.Version == 3)) + { + wavebankdata.BankName = System.Text.Encoding.UTF8.GetString(reader.ReadBytes(16),0,16).Replace("\0", ""); + } + else + { + wavebankdata.BankName = System.Text.Encoding.UTF8.GetString(reader.ReadBytes(64),0,64).Replace("\0", ""); + } + + _bankName = wavebankdata.BankName; + + if (wavebankheader.Version == 1) + { + //wavebank_offset = (int)ftell(fd) - file_offset; + wavebankdata.EntryMetaDataElementSize = 20; + } + else + { + wavebankdata.EntryMetaDataElementSize = reader.ReadInt32(); + wavebankdata.EntryNameElementSize = reader.ReadInt32(); + wavebankdata.Alignment = reader.ReadInt32(); + wavebank_offset = wavebankheader.Segments[1].Offset; //METADATASEGMENT + } + + if ((wavebankdata.Flags & Flag_Compact) != 0) + { + reader.ReadInt32(); // compact_format + } + + _playRegionOffset = wavebankheader.Segments[last_segment].Offset; + if (_playRegionOffset == 0) + { + _playRegionOffset = + wavebank_offset + + (wavebankdata.EntryCount * wavebankdata.EntryMetaDataElementSize); + } + + int segidx_entry_name = 2; + if (wavebankheader.Version >= 42) segidx_entry_name = 3; + + if ((wavebankheader.Segments[segidx_entry_name].Offset != 0) && + (wavebankheader.Segments[segidx_entry_name].Length != 0)) + { + if (wavebankdata.EntryNameElementSize == -1) wavebankdata.EntryNameElementSize = 0; + byte[] entry_name = new byte[wavebankdata.EntryNameElementSize + 1]; + entry_name[wavebankdata.EntryNameElementSize] = 0; + } + + _sounds = new SoundEffect[wavebankdata.EntryCount]; + _streams = new StreamInfo[wavebankdata.EntryCount]; + + reader.BaseStream.Seek(wavebank_offset, SeekOrigin.Begin); + + // The compact format requires us to load stuff differently. + var isCompactFormat = (wavebankdata.Flags & Flag_Compact) != 0; + if (isCompactFormat) + { + // Load the sound data offset table from disk. + for (var i = 0; i < wavebankdata.EntryCount; i++) + { + var len = reader.ReadInt32(); + _streams[i].Format = wavebankdata.CompactFormat; + _streams[i].FileOffset = (len & ((1 << 21) - 1))*wavebankdata.Alignment; + } + + // Now figure out the sound data lengths. + for (var i = 0; i < wavebankdata.EntryCount; i++) + { + int nextOffset; + if (i == (wavebankdata.EntryCount - 1)) + nextOffset = wavebankheader.Segments[last_segment].Length; + else + nextOffset = _streams[i + 1].FileOffset; + + // The next and current offsets used to calculate the length. + _streams[i].FileLength = nextOffset - _streams[i].FileOffset; + } + } + else + { + for (var i = 0; i < wavebankdata.EntryCount; i++) + { + var info = new StreamInfo(); + if (wavebankheader.Version == 1) + { + info.Format = reader.ReadInt32(); + info.FileOffset = reader.ReadInt32(); + info.FileLength = reader.ReadInt32(); + info.LoopStart = reader.ReadInt32(); + info.LoopLength = reader.ReadInt32(); + } + else + { + var flagsAndDuration = reader.ReadInt32(); // Unused + + if (wavebankdata.EntryMetaDataElementSize >= 8) + info.Format = reader.ReadInt32(); + if (wavebankdata.EntryMetaDataElementSize >= 12) + info.FileOffset = reader.ReadInt32(); + if (wavebankdata.EntryMetaDataElementSize >= 16) + info.FileLength = reader.ReadInt32(); + if (wavebankdata.EntryMetaDataElementSize >= 20) + info.LoopStart = reader.ReadInt32(); + if (wavebankdata.EntryMetaDataElementSize >= 24) + info.LoopLength = reader.ReadInt32(); + } + + // TODO: What is this doing? + if (wavebankdata.EntryMetaDataElementSize < 24) + { + if (info.FileLength != 0) + info.FileLength = wavebankheader.Segments[last_segment].Length; + } + + _streams[i] = info; + } + } + + // If this isn't a streaming wavebank then load all the sounds now. + if (!_streaming) + { + for (var i = 0; i < _streams.Length; i++) + { + var info = _streams[i]; + + // Read the data. + reader.BaseStream.Seek(info.FileOffset + _playRegionOffset, SeekOrigin.Begin); + var audiodata = reader.ReadBytes(info.FileLength); + + // Decode the format information. + MiniFormatTag codec; + int channels, rate, alignment; + DecodeFormat(info.Format, out codec, out channels, out rate, out alignment); + + // Call the special constuctor on SoundEffect to sort it out. + _sounds[i] = new SoundEffect(codec, audiodata, channels, rate, alignment, info.LoopStart, info.LoopLength); + } + + _streams = null; + } + + audioEngine.Wavebanks[_bankName] = this; + + IsPrepared = true; + } + + private void DecodeFormat(int format, out MiniFormatTag codec, out int channels, out int rate, out int alignment) + { + if (_version == 1) + { + // I'm not 100% sure if the following is correct + // version 1: + // 1 00000000 000101011000100010 0 001 0 + // | | | | | | + // | | | | | wFormatTag + // | | | | nChannels + // | | | ??? + // | | nSamplesPerSec + // | wBlockAlign + // wBitsPerSample + + codec = (MiniFormatTag)((format) & ((1 << 1) - 1)); + channels = (format >> (1)) & ((1 << 3) - 1); + rate = (format >> (1 + 3 + 1)) & ((1 << 18) - 1); + alignment = (format >> (1 + 3 + 1 + 18)) & ((1 << 8) - 1); + //bits = (format >> (1 + 3 + 1 + 18 + 8)) & ((1 << 1) - 1); + + /*} else if(wavebankheader.dwVersion == 23) { // I'm not 100% sure if the following is correct + // version 23: + // 1000000000 001011101110000000 001 1 + // | | | | | + // | | | | ??? + // | | | nChannels? + // | | nSamplesPerSec + // | ??? + // !!!UNKNOWN FORMAT!!! + + //codec = -1; + //chans = (wavebankentry.Format >> 1) & ((1 << 3) - 1); + //rate = (wavebankentry.Format >> 4) & ((1 << 18) - 1); + //bits = (wavebankentry.Format >> 31) & ((1 << 1) - 1); + codec = (wavebankentry.Format ) & ((1 << 1) - 1); + chans = (wavebankentry.Format >> (1) ) & ((1 << 3) - 1); + rate = (wavebankentry.Format >> (1 + 3) ) & ((1 << 18) - 1); + align = (wavebankentry.Format >> (1 + 3 + 18) ) & ((1 << 9) - 1); + bits = (wavebankentry.Format >> (1 + 3 + 18 + 9)) & ((1 << 1) - 1); */ + + } + else + { + // 0 00000000 000111110100000000 010 01 + // | | | | | + // | | | | wFormatTag + // | | | nChannels + // | | nSamplesPerSec + // | wBlockAlign + // wBitsPerSample + + codec = (MiniFormatTag)((format) & ((1 << 2) - 1)); + channels = (format >> (2)) & ((1 << 3) - 1); + rate = (format >> (2 + 3)) & ((1 << 18) - 1); + alignment = (format >> (2 + 3 + 18)) & ((1 << 8) - 1); + //bits = (info.Format >> (2 + 3 + 18 + 8)) & ((1 << 1) - 1); + } + } + + /// Instance of the AudioEngine to associate this wave bank with. + /// Path to the .xwb to stream from. + /// DVD sector-aligned offset within the wave bank data file. + /// Stream packet size, in sectors, to use for each stream. The minimum value is 2. + /// + /// This constructor streams wave data as needed. + /// Note that packetsize is in sectors, which is 2048 bytes. + /// AudioEngine.Update() must be called at least once before using data from a streaming wave bank. + /// + public WaveBank(AudioEngine audioEngine, string streamingWaveBankFilename, int offset, short packetsize) + : this(audioEngine, streamingWaveBankFilename, true, offset, packetsize) + { + } + + internal SoundEffectInstance GetSoundEffectInstance(int trackIndex, out bool streaming) + { + if (_streaming) + { + streaming = true; + var stream = _streams[trackIndex]; + return PlatformCreateStream(stream); + } + else + { + streaming = false; + var sound = _sounds[trackIndex]; + return sound.GetPooledInstance(true); + } + } + + /// + /// This event is triggered when the WaveBank is disposed. + /// + public event EventHandler Disposing; + + /// + /// Is true if the WaveBank has been disposed. + /// + public bool IsDisposed { get; private set; } + + /// + /// Disposes the WaveBank. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~WaveBank() + { + Dispose(false); + } + + private void Dispose(bool disposing) + { + if (IsDisposed) + return; + + IsDisposed = true; + + if (disposing) + { + foreach (var s in _sounds) + s.Dispose(); + + IsPrepared = false; + IsInUse = false; + EventHelpers.Raise(this, Disposing, EventArgs.Empty); + } + } + } +} + diff --git a/MonoGame.Framework/Audio/Xact/XactClip.cs b/MonoGame.Framework/Audio/Xact/XactClip.cs new file mode 100644 index 00000000000..fb8ffe5fbcb --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/XactClip.cs @@ -0,0 +1,495 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.IO; + +namespace Microsoft.Xna.Framework.Audio +{ + class XactClip + { + private readonly float _defaultVolume; + private float _volumeScale; + private float _volume; + + private readonly ClipEvent[] _events; + private float _time; + private int _nextEvent; + + internal readonly bool FilterEnabled; + internal readonly FilterMode FilterMode; + internal readonly float FilterQ; + internal readonly ushort FilterFrequency; + + internal readonly bool UseReverb; + + public XactClip (SoundBank soundBank, BinaryReader clipReader, bool useReverb) + { +#pragma warning disable 0219 + State = SoundState.Stopped; + + UseReverb = useReverb; + + var volumeDb = XactHelpers.ParseDecibels(clipReader.ReadByte()); + _defaultVolume = XactHelpers.ParseVolumeFromDecibels(volumeDb); + var clipOffset = clipReader.ReadUInt32(); + + // Read the filter info. + var filterQAndFlags = clipReader.ReadUInt16(); + FilterEnabled = (filterQAndFlags & 1) == 1; + FilterMode = (FilterMode)((filterQAndFlags >> 1) & 3); + FilterQ = (filterQAndFlags >> 3) * 0.01f; + FilterFrequency = clipReader.ReadUInt16(); + + var oldPosition = clipReader.BaseStream.Position; + clipReader.BaseStream.Seek(clipOffset, SeekOrigin.Begin); + + var numEvents = clipReader.ReadByte(); + _events = new ClipEvent[numEvents]; + + for (var i=0; i> 5) & 0xFFFF) * 0.001f; + var unknown = eventInfo >> 21; + + switch (eventId) { + case 0: + // Stop Event + throw new NotImplementedException("Stop event"); + + case 1: + { + // Unknown! + clipReader.ReadByte(); + + // Event flags + var eventFlags = clipReader.ReadByte(); + var playRelease = (eventFlags & 0x01) == 0x01; + var panEnabled = (eventFlags & 0x02) == 0x02; + var useCenterSpeaker = (eventFlags & 0x04) == 0x04; + + int trackIndex = clipReader.ReadUInt16(); + int waveBankIndex = clipReader.ReadByte(); + var loopCount = clipReader.ReadByte(); + var panAngle = clipReader.ReadUInt16() / 100.0f; + var panArc = clipReader.ReadUInt16() / 100.0f; + + _events[i] = new PlayWaveEvent( + this, + timeStamp, + randomOffset, + soundBank, + new[] { waveBankIndex }, + new[] { trackIndex }, + null, + 0, + VariationType.Ordered, + null, + null, + null, + loopCount, + false); + + break; + } + + case 3: + { + // Unknown! + clipReader.ReadByte(); + + // Event flags + var eventFlags = clipReader.ReadByte(); + var playRelease = (eventFlags & 0x01) == 0x01; + var panEnabled = (eventFlags & 0x02) == 0x02; + var useCenterSpeaker = (eventFlags & 0x04) == 0x04; + + var loopCount = clipReader.ReadByte(); + var panAngle = clipReader.ReadUInt16() / 100.0f; + var panArc = clipReader.ReadUInt16() / 100.0f; + + // The number of tracks for the variations. + var numTracks = clipReader.ReadUInt16(); + + // Not sure what most of this is. + var moreFlags = clipReader.ReadByte(); + var newWaveOnLoop = (moreFlags & 0x40) == 0x40; + + // The variation playlist type seems to be + // stored in the bottom 4bits only. + var variationType = (VariationType)(moreFlags & 0x0F); + + // Unknown! + clipReader.ReadBytes(5); + + // Read in the variation playlist. + var waveBanks = new int[numTracks]; + var tracks = new int[numTracks]; + var weights = new byte[numTracks]; + var totalWeights = 0; + for (var j = 0; j < numTracks; j++) + { + tracks[j] = clipReader.ReadUInt16(); + waveBanks[j] = clipReader.ReadByte(); + var minWeight = clipReader.ReadByte(); + var maxWeight = clipReader.ReadByte(); + weights[j] = (byte)(maxWeight - minWeight); + totalWeights += weights[j]; + } + + _events[i] = new PlayWaveEvent( + this, + timeStamp, + randomOffset, + soundBank, + waveBanks, + tracks, + weights, + totalWeights, + variationType, + null, + null, + null, + loopCount, + newWaveOnLoop); + + break; + } + + case 4: + { + // Unknown! + clipReader.ReadByte(); + + // Event flags + var eventFlags = clipReader.ReadByte(); + var playRelease = (eventFlags & 0x01) == 0x01; + var panEnabled = (eventFlags & 0x02) == 0x02; + var useCenterSpeaker = (eventFlags & 0x04) == 0x04; + + int trackIndex = clipReader.ReadUInt16(); + int waveBankIndex = clipReader.ReadByte(); + var loopCount = clipReader.ReadByte(); + var panAngle = clipReader.ReadUInt16() / 100.0f; + var panArc = clipReader.ReadUInt16() / 100.0f; + + // Pitch variation range + var minPitch = clipReader.ReadInt16() / 1000.0f; + var maxPitch = clipReader.ReadInt16() / 1000.0f; + + // Volume variation range + var minVolume = XactHelpers.ParseVolumeFromDecibels(clipReader.ReadByte()); + var maxVolume = XactHelpers.ParseVolumeFromDecibels(clipReader.ReadByte()); + + // Filter variation + var minFrequency = clipReader.ReadSingle(); + var maxFrequency = clipReader.ReadSingle(); + var minQ = clipReader.ReadSingle(); + var maxQ = clipReader.ReadSingle(); + + // Unknown! + clipReader.ReadByte(); + + var variationFlags = clipReader.ReadByte(); + + // Enable pitch variation + Vector2? pitchVar = null; + if ((variationFlags & 0x10) == 0x10) + pitchVar = new Vector2(minPitch, maxPitch - minPitch); + + // Enable volume variation + Vector2? volumeVar = null; + if ((variationFlags & 0x20) == 0x20) + volumeVar = new Vector2(minVolume, maxVolume - minVolume); + + // Enable filter variation + Vector4? filterVar = null; + if ((variationFlags & 0x40) == 0x40) + filterVar = new Vector4(minFrequency, maxFrequency - minFrequency, minQ, maxQ - minQ); + + _events[i] = new PlayWaveEvent( + this, + timeStamp, + randomOffset, + soundBank, + new[] { waveBankIndex }, + new[] { trackIndex }, + null, + 0, + VariationType.Ordered, + volumeVar, + pitchVar, + filterVar, + loopCount, + false); + + break; + } + + case 6: + { + // Unknown! + clipReader.ReadByte(); + + // Event flags + var eventFlags = clipReader.ReadByte(); + var playRelease = (eventFlags & 0x01) == 0x01; + var panEnabled = (eventFlags & 0x02) == 0x02; + var useCenterSpeaker = (eventFlags & 0x04) == 0x04; + + var loopCount = clipReader.ReadByte(); + var panAngle = clipReader.ReadUInt16() / 100.0f; + var panArc = clipReader.ReadUInt16() / 100.0f; + + // Pitch variation range + var minPitch = clipReader.ReadInt16() / 1000.0f; + var maxPitch = clipReader.ReadInt16() / 1000.0f; + + // Volume variation range + var minVolume = XactHelpers.ParseVolumeFromDecibels(clipReader.ReadByte()); + var maxVolume = XactHelpers.ParseVolumeFromDecibels(clipReader.ReadByte()); + + // Filter variation range + var minFrequency = clipReader.ReadSingle(); + var maxFrequency = clipReader.ReadSingle(); + var minQ = clipReader.ReadSingle(); + var maxQ = clipReader.ReadSingle(); + + // Unknown! + clipReader.ReadByte(); + + // TODO: Still has unknown bits! + var variationFlags = clipReader.ReadByte(); + + // Enable pitch variation + Vector2? pitchVar = null; + if ((variationFlags & 0x10) == 0x10) + pitchVar = new Vector2(minPitch, maxPitch - minPitch); + + // Enable volume variation + Vector2? volumeVar = null; + if ((variationFlags & 0x20) == 0x20) + volumeVar = new Vector2(minVolume, maxVolume - minVolume); + + // Enable filter variation + Vector4? filterVar = null; + if ((variationFlags & 0x40) == 0x40) + filterVar = new Vector4(minFrequency, maxFrequency - minFrequency, minQ, maxQ - minQ); + + // The number of tracks for the variations. + var numTracks = clipReader.ReadUInt16(); + + // Not sure what most of this is. + var moreFlags = clipReader.ReadByte(); + var newWaveOnLoop = (moreFlags & 0x40) == 0x40; + + // The variation playlist type seems to be + // stored in the bottom 4bits only. + var variationType = (VariationType)(moreFlags & 0x0F); + + // Unknown! + clipReader.ReadBytes(5); + + // Read in the variation playlist. + var waveBanks = new int[numTracks]; + var tracks = new int[numTracks]; + var weights = new byte[numTracks]; + var totalWeights = 0; + for (var j = 0; j < numTracks; j++) + { + tracks[j] = clipReader.ReadUInt16(); + waveBanks[j] = clipReader.ReadByte(); + var minWeight = clipReader.ReadByte(); + var maxWeight = clipReader.ReadByte(); + weights[j] = (byte)(maxWeight - minWeight); + totalWeights += weights[j]; + } + + _events[i] = new PlayWaveEvent( + this, + timeStamp, + randomOffset, + soundBank, + waveBanks, + tracks, + weights, + totalWeights, + variationType, + volumeVar, + pitchVar, + filterVar, + loopCount, + newWaveOnLoop); + + break; + } + + case 7: + // Pitch Event + throw new NotImplementedException("Pitch event"); + + case 8: + { + // Unknown! + clipReader.ReadBytes(2); + + // Event flags + var eventFlags = clipReader.ReadByte(); + var isAdd = (eventFlags & 0x01) == 0x01; + + // The replacement or additive volume. + var decibles = clipReader.ReadSingle() / 100.0f; + var volume = XactHelpers.ParseVolumeFromDecibels(decibles + (isAdd ? volumeDb : 0)); + + // Unknown! + clipReader.ReadBytes(9); + + _events[i] = new VolumeEvent( this, + timeStamp, + randomOffset, + volume); + break; + } + + case 17: + // Volume Repeat Event + throw new NotImplementedException("Volume repeat event"); + + case 9: + // Marker Event + throw new NotImplementedException("Marker event"); + + default: + throw new NotSupportedException("Unknown event " + eventId); + } + } + + clipReader.BaseStream.Seek (oldPosition, SeekOrigin.Begin); +#pragma warning restore 0219 + } + + internal void Update(float dt) + { + if (State != SoundState.Playing) + return; + + _time += dt; + + // Play the next event. + while (_nextEvent < _events.Length) + { + var evt = _events[_nextEvent]; + if (_time < evt.TimeStamp) + break; + + evt.Play(); + ++_nextEvent; + } + + // Update all the active events. + var isPlaying = _nextEvent < _events.Length; + for (var i = 0; i < _nextEvent; i++) + { + var evt = _events[i]; + isPlaying |= evt.Update(dt); + } + + // Update the state. + if (!isPlaying) + State = SoundState.Stopped; + } + + internal void SetFade(float fadeInDuration, float fadeOutDuration) + { + foreach (var evt in _events) + { + if (evt is PlayWaveEvent) + evt.SetFade(fadeInDuration, fadeOutDuration); + } + } + + internal void UpdateState(float volume, float pitch, float reverbMix, float? filterFrequency, float? filterQFactor) + { + _volumeScale = volume; + var trackVolume = _volume * _volumeScale; + + foreach (var evt in _events) + evt.SetState(trackVolume, pitch, reverbMix, filterFrequency, filterQFactor); + } + + public void Play() + { + _time = 0.0f; + _nextEvent = 0; + SetVolume(_defaultVolume); + State = SoundState.Playing; + Update(0); + } + + public void Resume() + { + foreach (var evt in _events) + evt.Resume(); + + State = SoundState.Playing; + } + + public void Stop() + { + foreach (var evt in _events) + evt.Stop(); + + State = SoundState.Stopped; + } + + public void Pause() + { + foreach (var evt in _events) + evt.Pause(); + + State = SoundState.Paused; + } + + public SoundState State { get; private set; } + + /// + /// Set the combined volume scale from the parent objects. + /// + /// The volume scale. + public void SetVolumeScale(float volume) + { + _volumeScale = volume; + UpdateVolumes(); + } + + /// + /// Set the volume for the clip. + /// + /// The volume level. + public void SetVolume(float volume) + { + _volume = volume; + UpdateVolumes(); + } + + private void UpdateVolumes() + { + var volume = _volume * _volumeScale; + foreach (var evt in _events) + evt.SetTrackVolume(volume); + } + + public void SetPan(float pan) + { + foreach (var evt in _events) + evt.SetTrackPan(pan); + } + } +} + diff --git a/MonoGame.Framework/Audio/Xact/XactHelpers.cs b/MonoGame.Framework/Audio/Xact/XactHelpers.cs new file mode 100644 index 00000000000..0ce35af0b21 --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/XactHelpers.cs @@ -0,0 +1,60 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Audio +{ + static class XactHelpers + { + static internal readonly Random Random = new Random(); + + public static float ParseDecibels(byte decibles) + { + //lazy 4-param fitting: + //0xff 6.0 + //0xca 2.0 + //0xbf 1.0 + //0xb4 0.0 + //0x8f -4.0 + //0x5a -12.0 + //0x14 -38.0 + //0x00 -96.0 + const double a = -96.0; + const double b = 0.432254984608615; + const double c = 80.1748600297963; + const double d = 67.7385212334047; + var dB = (float)(((a - d) / (1 + (Math.Pow(decibles / c, b)))) + d); + + return dB; + } + + public static float ParseVolumeFromDecibels(byte decibles) + { + //lazy 4-param fitting: + //0xff 6.0 + //0xca 2.0 + //0xbf 1.0 + //0xb4 0.0 + //0x8f -4.0 + //0x5a -12.0 + //0x14 -38.0 + //0x00 -96.0 + const double a = -96.0; + const double b = 0.432254984608615; + const double c = 80.1748600297963; + const double d = 67.7385212334047; + var dB = (float)(((a - d) / (1 + (Math.Pow(decibles / c, b)))) + d); + + return ParseVolumeFromDecibels(dB); + } + + public static float ParseVolumeFromDecibels(float decibles) + { + // Convert from decibles to linear volume. + return (float)Math.Pow(10.0, decibles / 20.0); + } + } +} + diff --git a/MonoGame.Framework/Audio/Xact/XactSound.cs b/MonoGame.Framework/Audio/Xact/XactSound.cs new file mode 100644 index 00000000000..e461f47da22 --- /dev/null +++ b/MonoGame.Framework/Audio/Xact/XactSound.cs @@ -0,0 +1,392 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.IO; + +namespace Microsoft.Xna.Framework.Audio +{ + class XactSound + { + private readonly bool _complexSound; + private readonly XactClip[] _soundClips; + private readonly int _waveBankIndex; + private readonly int _trackIndex; + private readonly float _volume; + private readonly float _pitch; + private readonly uint _categoryID; + private readonly SoundBank _soundBank; + private readonly bool _useReverb; + + private SoundEffectInstance _wave; + private bool _streaming; + + private float _cueVolume = 1; + private float _cuePitch = 0; + private float _cueReverbMix = 0; + private float? _cueFilterFrequency; + private float? _cueFilterQFactor; + + internal readonly int[] RpcCurves; + + public XactSound(SoundBank soundBank, int waveBankIndex, int trackIndex) + { + _complexSound = false; + + _soundBank = soundBank; + _waveBankIndex = waveBankIndex; + _trackIndex = trackIndex; + RpcCurves = new int[0]; + } + + public XactSound(AudioEngine engine, SoundBank soundBank, BinaryReader soundReader) + { + _soundBank = soundBank; + + var flags = soundReader.ReadByte(); + _complexSound = (flags & 0x1) != 0; + var hasRPCs = (flags & 0x0E) != 0; + var hasDSPs = (flags & 0x10) != 0; + + _categoryID = soundReader.ReadUInt16(); + _volume = XactHelpers.ParseVolumeFromDecibels(soundReader.ReadByte()); + _pitch = soundReader.ReadInt16() / 1000.0f; + soundReader.ReadByte(); //priority + soundReader.ReadUInt16(); // filter stuff? + + var numClips = 0; + if (_complexSound) + numClips = soundReader.ReadByte(); + else + { + _trackIndex = soundReader.ReadUInt16(); + _waveBankIndex = soundReader.ReadByte(); + } + + if (!hasRPCs) + RpcCurves = new int[0]; + else + { + var current = soundReader.BaseStream.Position; + + // This doesn't seem to be used... might have been there + // to allow for some future file format expansion. + var dataLength = soundReader.ReadUInt16(); + + var numPresets = soundReader.ReadByte(); + RpcCurves = new int[numPresets]; + for (var i = 0; i < numPresets; i++) + RpcCurves[i] = engine.GetRpcIndex(soundReader.ReadUInt32()); + + // Just in case seek to the right spot. + soundReader.BaseStream.Seek(current + dataLength, SeekOrigin.Begin); + } + + if (!hasDSPs) + _useReverb = false; + else + { + // The file format for this seems to follow the pattern for + // the RPC curves above, but in this case XACT only supports + // a single effect... Microsoft Reverb... so just set it. + _useReverb = true; + soundReader.BaseStream.Seek(7, SeekOrigin.Current); + } + + if (_complexSound) + { + _soundClips = new XactClip[numClips]; + for (int i = 0; i < numClips; i++) + _soundClips[i] = new XactClip(soundBank, soundReader, _useReverb); + } + + var category = engine.Categories[_categoryID]; + category.AddSound(this); + } + + internal void SetFade(float fadeInTime, float fadeOutTime) + { + if (fadeInTime == 0.0f && + fadeOutTime == 0.0f ) + return; + + if (_complexSound) + { + foreach (var sound in _soundClips) + sound.SetFade(fadeInTime, fadeOutTime); + } + else + { + // TODO: + } + } + + public void Play(float volume, AudioEngine engine) + { + _cueVolume = volume; + var category = engine.Categories[_categoryID]; + + var curInstances = category.GetPlayingInstanceCount(); + if (curInstances >= category.maxInstances) + { + var prevSound = category.GetOldestInstance(); + + if (prevSound != null) + { + prevSound.SetFade(0.0f, category.fadeOut); + prevSound.Stop(AudioStopOptions.Immediate); + SetFade(category.fadeIn, 0.0f); + } + } + + float finalVolume = _volume * _cueVolume * category._volume[0]; + float finalPitch = _pitch + _cuePitch; + float finalMix = _useReverb ? _cueReverbMix : 0.0f; + + if (_complexSound) + { + foreach (XactClip clip in _soundClips) + { + clip.UpdateState(finalVolume, finalPitch, finalMix, _cueFilterFrequency, _cueFilterQFactor); + clip.Play(); + } + } + else + { + if (_wave != null) + { + if (_streaming) + _wave.Dispose(); + else + _wave._isXAct = false; + _wave = null; + } + + _wave = _soundBank.GetSoundEffectInstance(_waveBankIndex, _trackIndex, out _streaming); + + if (_wave == null) + { + // We couldn't create a sound effect instance, most likely + // because we've reached the sound pool limits. + return; + } + + _wave.Pitch = finalPitch; + _wave.Volume = finalVolume; + _wave.PlatformSetReverbMix(finalMix); + _wave.Play(); + } + } + + internal void Update(float dt) + { + if (_complexSound) + { + foreach (var sound in _soundClips) + sound.Update(dt); + } + else + { + if (_wave != null && _wave.State == SoundState.Stopped) + { + if (_streaming) + _wave.Dispose(); + else + _wave._isXAct = false; + _wave = null; + } + } + } + + internal void StopAll(AudioStopOptions options) + { + if (_complexSound) + { + foreach (XactClip clip in _soundClips) + clip.Stop(); + } + else + { + if (_wave != null) + { + _wave.Stop(); + if (_streaming) + _wave.Dispose(); + else + _wave._isXAct = false; + _wave = null; + } + } + } + + public void Stop(AudioStopOptions options) + { + if (_complexSound) + { + foreach (var sound in _soundClips) + sound.Stop(); + } + else + { + if (_wave != null) + { + _wave.Stop(); + if (_streaming) + _wave.Dispose(); + else + _wave._isXAct = false; + _wave = null; + } + } + } + + public void Pause() + { + if (_complexSound) + { + foreach (var sound in _soundClips) + { + if (sound.State == SoundState.Playing) + sound.Pause(); + } + } + else + { + if (_wave != null && _wave.State == SoundState.Playing) + _wave.Pause(); + } + } + + public void Resume() + { + if (_complexSound) + { + foreach (var sound in _soundClips) + { + if (sound.State == SoundState.Paused) + sound.Resume(); + } + } + else + { + if (_wave != null && _wave.State == SoundState.Paused) + _wave.Resume(); + } + } + + internal void UpdateCategoryVolume(float categoryVolume) + { + // The different volumes modulate each other. + var volume = _volume * _cueVolume * categoryVolume; + + if (_complexSound) + { + foreach (var clip in _soundClips) + clip.SetVolumeScale(volume); + } + else + { + if (_wave != null) + _wave.Volume = volume; + } + } + + internal void UpdateState(AudioEngine engine, float volume, float pitch, float reverbMix, float? filterFrequency, float? filterQFactor) + { + _cueVolume = volume; + var finalVolume = _volume * _cueVolume * engine.Categories[_categoryID]._volume[0]; + + _cueReverbMix = reverbMix; + _cueFilterFrequency = filterFrequency; + _cueFilterQFactor = filterQFactor; + + _cuePitch = pitch; + var finalPitch = _pitch + _cuePitch; + + if (_complexSound) + { + foreach (var clip in _soundClips) + clip.UpdateState(finalVolume, finalPitch, _useReverb ? _cueReverbMix : 0.0f, _cueFilterFrequency, _cueFilterQFactor); + } + else if (_wave != null) + { + _wave.PlatformSetReverbMix(_useReverb ? _cueReverbMix : 0.0f); + _wave.Pitch = finalPitch; + _wave.Volume = finalVolume; + } + } + + internal void SetCuePan(float pan) + { + if (_complexSound) + { + foreach (var clip in _soundClips) + clip.SetPan(pan); + } + else + { + if (_wave != null) + _wave.Pan = pan; + } + } + + public bool Playing + { + get + { + if (_complexSound) + { + foreach (var clip in _soundClips) + if (clip.State == SoundState.Playing) + return true; + + return false; + } + + return _wave != null && _wave.State == SoundState.Playing; + } + } + + public bool Stopped + { + get + { + if (_complexSound) + { + var notStopped = false; + + // All clips must be stopped for the sound to be stopped. + foreach (var clip in _soundClips) + { + if (clip.State != SoundState.Stopped) + notStopped = true; + } + + return !notStopped; + } + + // We null the wave when it it stopped. + return _wave == null; + } + } + + public bool IsPaused + { + get + { + if (_complexSound) + { + foreach (var clip in _soundClips) + if (clip.State == SoundState.Paused) + return true; + + return false; + } + + return _wave != null && _wave.State == SoundState.Paused; + } + } + } +} + diff --git a/MonoGame.Framework/BoundingBox.cs b/MonoGame.Framework/BoundingBox.cs new file mode 100644 index 00000000000..8e79fb8a04e --- /dev/null +++ b/MonoGame.Framework/BoundingBox.cs @@ -0,0 +1,538 @@ +// MIT License - Copyright (C) The Mono.Xna Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Serialization; + +namespace Microsoft.Xna.Framework +{ + [DataContract] + [DebuggerDisplay("{DebugDisplayString,nq}")] + public struct BoundingBox : IEquatable + { + + #region Public Fields + + [DataMember] + public Vector3 Min; + + [DataMember] + public Vector3 Max; + + public const int CornerCount = 8; + + #endregion Public Fields + + + #region Public Constructors + + public BoundingBox(Vector3 min, Vector3 max) + { + this.Min = min; + this.Max = max; + } + + #endregion Public Constructors + + + #region Public Methods + + public ContainmentType Contains(BoundingBox box) + { + //test if all corner is in the same side of a face by just checking min and max + if (box.Max.X < Min.X + || box.Min.X > Max.X + || box.Max.Y < Min.Y + || box.Min.Y > Max.Y + || box.Max.Z < Min.Z + || box.Min.Z > Max.Z) + return ContainmentType.Disjoint; + + + if (box.Min.X >= Min.X + && box.Max.X <= Max.X + && box.Min.Y >= Min.Y + && box.Max.Y <= Max.Y + && box.Min.Z >= Min.Z + && box.Max.Z <= Max.Z) + return ContainmentType.Contains; + + return ContainmentType.Intersects; + } + + public void Contains(ref BoundingBox box, out ContainmentType result) + { + result = Contains(box); + } + + public ContainmentType Contains(BoundingFrustum frustum) + { + //TODO: bad done here need a fix. + //Because question is not frustum contain box but reverse and this is not the same + int i; + ContainmentType contained; + Vector3[] corners = frustum.GetCorners(); + + // First we check if frustum is in box + for (i = 0; i < corners.Length; i++) + { + this.Contains(ref corners[i], out contained); + if (contained == ContainmentType.Disjoint) + break; + } + + if (i == corners.Length) // This means we checked all the corners and they were all contain or instersect + return ContainmentType.Contains; + + if (i != 0) // if i is not equal to zero, we can fastpath and say that this box intersects + return ContainmentType.Intersects; + + + // If we get here, it means the first (and only) point we checked was actually contained in the frustum. + // So we assume that all other points will also be contained. If one of the points is disjoint, we can + // exit immediately saying that the result is Intersects + i++; + for (; i < corners.Length; i++) + { + this.Contains(ref corners[i], out contained); + if (contained != ContainmentType.Contains) + return ContainmentType.Intersects; + + } + + // If we get here, then we know all the points were actually contained, therefore result is Contains + return ContainmentType.Contains; + } + + public ContainmentType Contains(BoundingSphere sphere) + { + if (sphere.Center.X - Min.X >= sphere.Radius + && sphere.Center.Y - Min.Y >= sphere.Radius + && sphere.Center.Z - Min.Z >= sphere.Radius + && Max.X - sphere.Center.X >= sphere.Radius + && Max.Y - sphere.Center.Y >= sphere.Radius + && Max.Z - sphere.Center.Z >= sphere.Radius) + return ContainmentType.Contains; + + double dmin = 0; + + double e = sphere.Center.X - Min.X; + if (e < 0) + { + if (e < -sphere.Radius) + { + return ContainmentType.Disjoint; + } + dmin += e * e; + } + else + { + e = sphere.Center.X - Max.X; + if (e > 0) + { + if (e > sphere.Radius) + { + return ContainmentType.Disjoint; + } + dmin += e * e; + } + } + + e = sphere.Center.Y - Min.Y; + if (e < 0) + { + if (e < -sphere.Radius) + { + return ContainmentType.Disjoint; + } + dmin += e * e; + } + else + { + e = sphere.Center.Y - Max.Y; + if (e > 0) + { + if (e > sphere.Radius) + { + return ContainmentType.Disjoint; + } + dmin += e * e; + } + } + + e = sphere.Center.Z - Min.Z; + if (e < 0) + { + if (e < -sphere.Radius) + { + return ContainmentType.Disjoint; + } + dmin += e * e; + } + else + { + e = sphere.Center.Z - Max.Z; + if (e > 0) + { + if (e > sphere.Radius) + { + return ContainmentType.Disjoint; + } + dmin += e * e; + } + } + + if (dmin <= sphere.Radius * sphere.Radius) + return ContainmentType.Intersects; + + return ContainmentType.Disjoint; + } + + public void Contains(ref BoundingSphere sphere, out ContainmentType result) + { + result = this.Contains(sphere); + } + + public ContainmentType Contains(Vector3 point) + { + ContainmentType result; + this.Contains(ref point, out result); + return result; + } + + public void Contains(ref Vector3 point, out ContainmentType result) + { + //first we get if point is out of box + if (point.X < this.Min.X + || point.X > this.Max.X + || point.Y < this.Min.Y + || point.Y > this.Max.Y + || point.Z < this.Min.Z + || point.Z > this.Max.Z) + { + result = ContainmentType.Disjoint; + } + else + { + result = ContainmentType.Contains; + } + } + + private static readonly Vector3 MaxVector3 = new Vector3(float.MaxValue); + private static readonly Vector3 MinVector3 = new Vector3(float.MinValue); + + /// + /// Create a bounding box from the given list of points. + /// + /// The list of Vector3 instances defining the point cloud to bound + /// A bounding box that encapsulates the given point cloud. + /// Thrown if the given list has no points. + public static BoundingBox CreateFromPoints(IEnumerable points) + { + if (points == null) + throw new ArgumentNullException(); + + var empty = true; + var minVec = MaxVector3; + var maxVec = MinVector3; + foreach (var ptVector in points) + { + minVec.X = (minVec.X < ptVector.X) ? minVec.X : ptVector.X; + minVec.Y = (minVec.Y < ptVector.Y) ? minVec.Y : ptVector.Y; + minVec.Z = (minVec.Z < ptVector.Z) ? minVec.Z : ptVector.Z; + + maxVec.X = (maxVec.X > ptVector.X) ? maxVec.X : ptVector.X; + maxVec.Y = (maxVec.Y > ptVector.Y) ? maxVec.Y : ptVector.Y; + maxVec.Z = (maxVec.Z > ptVector.Z) ? maxVec.Z : ptVector.Z; + + empty = false; + } + if (empty) + throw new ArgumentException(); + + return new BoundingBox(minVec, maxVec); + } + + public static BoundingBox CreateFromSphere(BoundingSphere sphere) + { + BoundingBox result; + CreateFromSphere(ref sphere, out result); + return result; + } + + public static void CreateFromSphere(ref BoundingSphere sphere, out BoundingBox result) + { + var corner = new Vector3(sphere.Radius); + result.Min = sphere.Center - corner; + result.Max = sphere.Center + corner; + } + + public static BoundingBox CreateMerged(BoundingBox original, BoundingBox additional) + { + BoundingBox result; + CreateMerged(ref original, ref additional, out result); + return result; + } + + public static void CreateMerged(ref BoundingBox original, ref BoundingBox additional, out BoundingBox result) + { + result.Min.X = Math.Min(original.Min.X, additional.Min.X); + result.Min.Y = Math.Min(original.Min.Y, additional.Min.Y); + result.Min.Z = Math.Min(original.Min.Z, additional.Min.Z); + result.Max.X = Math.Max(original.Max.X, additional.Max.X); + result.Max.Y = Math.Max(original.Max.Y, additional.Max.Y); + result.Max.Z = Math.Max(original.Max.Z, additional.Max.Z); + } + + public bool Equals(BoundingBox other) + { + return (this.Min == other.Min) && (this.Max == other.Max); + } + + public override bool Equals(object obj) + { + return (obj is BoundingBox) ? this.Equals((BoundingBox)obj) : false; + } + + public Vector3[] GetCorners() + { + return new Vector3[] { + new Vector3(this.Min.X, this.Max.Y, this.Max.Z), + new Vector3(this.Max.X, this.Max.Y, this.Max.Z), + new Vector3(this.Max.X, this.Min.Y, this.Max.Z), + new Vector3(this.Min.X, this.Min.Y, this.Max.Z), + new Vector3(this.Min.X, this.Max.Y, this.Min.Z), + new Vector3(this.Max.X, this.Max.Y, this.Min.Z), + new Vector3(this.Max.X, this.Min.Y, this.Min.Z), + new Vector3(this.Min.X, this.Min.Y, this.Min.Z) + }; + } + + public void GetCorners(Vector3[] corners) + { + if (corners == null) + { + throw new ArgumentNullException("corners"); + } + if (corners.Length < 8) + { + throw new ArgumentOutOfRangeException("corners", "Not Enought Corners"); + } + corners[0].X = this.Min.X; + corners[0].Y = this.Max.Y; + corners[0].Z = this.Max.Z; + corners[1].X = this.Max.X; + corners[1].Y = this.Max.Y; + corners[1].Z = this.Max.Z; + corners[2].X = this.Max.X; + corners[2].Y = this.Min.Y; + corners[2].Z = this.Max.Z; + corners[3].X = this.Min.X; + corners[3].Y = this.Min.Y; + corners[3].Z = this.Max.Z; + corners[4].X = this.Min.X; + corners[4].Y = this.Max.Y; + corners[4].Z = this.Min.Z; + corners[5].X = this.Max.X; + corners[5].Y = this.Max.Y; + corners[5].Z = this.Min.Z; + corners[6].X = this.Max.X; + corners[6].Y = this.Min.Y; + corners[6].Z = this.Min.Z; + corners[7].X = this.Min.X; + corners[7].Y = this.Min.Y; + corners[7].Z = this.Min.Z; + } + + public override int GetHashCode() + { + return this.Min.GetHashCode() + this.Max.GetHashCode(); + } + + public bool Intersects(BoundingBox box) + { + bool result; + Intersects(ref box, out result); + return result; + } + + public void Intersects(ref BoundingBox box, out bool result) + { + if ((this.Max.X >= box.Min.X) && (this.Min.X <= box.Max.X)) + { + if ((this.Max.Y < box.Min.Y) || (this.Min.Y > box.Max.Y)) + { + result = false; + return; + } + + result = (this.Max.Z >= box.Min.Z) && (this.Min.Z <= box.Max.Z); + return; + } + + result = false; + return; + } + + public bool Intersects(BoundingFrustum frustum) + { + return frustum.Intersects(this); + } + + public bool Intersects(BoundingSphere sphere) + { + if (sphere.Center.X - Min.X > sphere.Radius + && sphere.Center.Y - Min.Y > sphere.Radius + && sphere.Center.Z - Min.Z > sphere.Radius + && Max.X - sphere.Center.X > sphere.Radius + && Max.Y - sphere.Center.Y > sphere.Radius + && Max.Z - sphere.Center.Z > sphere.Radius) + return true; + + double dmin = 0; + + if (sphere.Center.X - Min.X <= sphere.Radius) + dmin += (sphere.Center.X - Min.X) * (sphere.Center.X - Min.X); + else if (Max.X - sphere.Center.X <= sphere.Radius) + dmin += (sphere.Center.X - Max.X) * (sphere.Center.X - Max.X); + + if (sphere.Center.Y - Min.Y <= sphere.Radius) + dmin += (sphere.Center.Y - Min.Y) * (sphere.Center.Y - Min.Y); + else if (Max.Y - sphere.Center.Y <= sphere.Radius) + dmin += (sphere.Center.Y - Max.Y) * (sphere.Center.Y - Max.Y); + + if (sphere.Center.Z - Min.Z <= sphere.Radius) + dmin += (sphere.Center.Z - Min.Z) * (sphere.Center.Z - Min.Z); + else if (Max.Z - sphere.Center.Z <= sphere.Radius) + dmin += (sphere.Center.Z - Max.Z) * (sphere.Center.Z - Max.Z); + + if (dmin <= sphere.Radius * sphere.Radius) + return true; + + return false; + } + + public void Intersects(ref BoundingSphere sphere, out bool result) + { + result = Intersects(sphere); + } + + public PlaneIntersectionType Intersects(Plane plane) + { + PlaneIntersectionType result; + Intersects(ref plane, out result); + return result; + } + + public void Intersects(ref Plane plane, out PlaneIntersectionType result) + { + // See http://zach.in.tu-clausthal.de/teaching/cg_literatur/lighthouse3d_view_frustum_culling/index.html + + Vector3 positiveVertex; + Vector3 negativeVertex; + + if (plane.Normal.X >= 0) + { + positiveVertex.X = Max.X; + negativeVertex.X = Min.X; + } + else + { + positiveVertex.X = Min.X; + negativeVertex.X = Max.X; + } + + if (plane.Normal.Y >= 0) + { + positiveVertex.Y = Max.Y; + negativeVertex.Y = Min.Y; + } + else + { + positiveVertex.Y = Min.Y; + negativeVertex.Y = Max.Y; + } + + if (plane.Normal.Z >= 0) + { + positiveVertex.Z = Max.Z; + negativeVertex.Z = Min.Z; + } + else + { + positiveVertex.Z = Min.Z; + negativeVertex.Z = Max.Z; + } + + // Inline Vector3.Dot(plane.Normal, negativeVertex) + plane.D; + var distance = plane.Normal.X * negativeVertex.X + plane.Normal.Y * negativeVertex.Y + plane.Normal.Z * negativeVertex.Z + plane.D; + if (distance > 0) + { + result = PlaneIntersectionType.Front; + return; + } + + // Inline Vector3.Dot(plane.Normal, positiveVertex) + plane.D; + distance = plane.Normal.X * positiveVertex.X + plane.Normal.Y * positiveVertex.Y + plane.Normal.Z * positiveVertex.Z + plane.D; + if (distance < 0) + { + result = PlaneIntersectionType.Back; + return; + } + + result = PlaneIntersectionType.Intersecting; + } + + public Nullable Intersects(Ray ray) + { + return ray.Intersects(this); + } + + public void Intersects(ref Ray ray, out Nullable result) + { + result = Intersects(ray); + } + + public static bool operator ==(BoundingBox a, BoundingBox b) + { + return a.Equals(b); + } + + public static bool operator !=(BoundingBox a, BoundingBox b) + { + return !a.Equals(b); + } + + internal string DebugDisplayString + { + get + { + return string.Concat( + "Min( ", this.Min.DebugDisplayString, " ) \r\n", + "Max( ",this.Max.DebugDisplayString, " )" + ); + } + } + + public override string ToString() + { + return "{{Min:" + this.Min.ToString() + " Max:" + this.Max.ToString() + "}}"; + } + + /// + /// Deconstruction method for . + /// + /// + /// + public void Deconstruct(out Vector3 min, out Vector3 max) + { + min = Min; + max = Max; + } + + #endregion Public Methods + } +} diff --git a/MonoGame.Framework/BoundingFrustum.cs b/MonoGame.Framework/BoundingFrustum.cs new file mode 100644 index 00000000000..b8211661bda --- /dev/null +++ b/MonoGame.Framework/BoundingFrustum.cs @@ -0,0 +1,578 @@ +// MIT License - Copyright (C) The Mono.Xna Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Diagnostics; + +namespace Microsoft.Xna.Framework +{ + /// + /// Defines a viewing frustum for intersection operations. + /// + [DebuggerDisplay("{DebugDisplayString,nq}")] + public class BoundingFrustum : IEquatable + { + #region Private Fields + + private Matrix _matrix; + private readonly Vector3[] _corners = new Vector3[CornerCount]; + private readonly Plane[] _planes = new Plane[PlaneCount]; + + #endregion + + #region Public Fields + + /// + /// The number of planes in the frustum. + /// + public const int PlaneCount = 6; + + /// + /// The number of corner points in the frustum. + /// + public const int CornerCount = 8; + + #endregion + + #region Properties + + /// + /// Gets or sets the of the frustum. + /// + public Matrix Matrix + { + get { return this._matrix; } + set + { + this._matrix = value; + this.CreatePlanes(); // FIXME: The odds are the planes will be used a lot more often than the matrix + this.CreateCorners(); // is updated, so this should help performance. I hope ;) + } + } + + /// + /// Gets the near plane of the frustum. + /// + public Plane Near + { + get { return this._planes[0]; } + } + + /// + /// Gets the far plane of the frustum. + /// + public Plane Far + { + get { return this._planes[1]; } + } + + /// + /// Gets the left plane of the frustum. + /// + public Plane Left + { + get { return this._planes[2]; } + } + + /// + /// Gets the right plane of the frustum. + /// + public Plane Right + { + get { return this._planes[3]; } + } + + /// + /// Gets the top plane of the frustum. + /// + public Plane Top + { + get { return this._planes[4]; } + } + + /// + /// Gets the bottom plane of the frustum. + /// + public Plane Bottom + { + get { return this._planes[5]; } + } + + #endregion + + #region Internal Properties + + internal string DebugDisplayString + { + get + { + return string.Concat( + "Near( ", this._planes[0].DebugDisplayString, " ) \r\n", + "Far( ", this._planes[1].DebugDisplayString, " ) \r\n", + "Left( ", this._planes[2].DebugDisplayString, " ) \r\n", + "Right( ", this._planes[3].DebugDisplayString, " ) \r\n", + "Top( ", this._planes[4].DebugDisplayString, " ) \r\n", + "Bottom( ", this._planes[5].DebugDisplayString, " ) " + ); + } + } + + #endregion + + #region Constructors + + /// + /// Constructs the frustum by extracting the view planes from a matrix. + /// + /// Combined matrix which usually is (View * Projection). + public BoundingFrustum(Matrix value) + { + this._matrix = value; + this.CreatePlanes(); + this.CreateCorners(); + } + + #endregion + + #region Operators + + /// + /// Compares whether two instances are equal. + /// + /// instance on the left of the equal sign. + /// instance on the right of the equal sign. + /// true if the instances are equal; false otherwise. + public static bool operator ==(BoundingFrustum a, BoundingFrustum b) + { + if (Equals(a, null)) + return (Equals(b, null)); + + if (Equals(b, null)) + return (Equals(a, null)); + + return a._matrix == (b._matrix); + } + + /// + /// Compares whether two instances are not equal. + /// + /// instance on the left of the not equal sign. + /// instance on the right of the not equal sign. + /// true if the instances are not equal; false otherwise. + public static bool operator !=(BoundingFrustum a, BoundingFrustum b) + { + return !(a == b); + } + + #endregion + + #region Public Methods + + #region Contains + + /// + /// Containment test between this and specified . + /// + /// A for testing. + /// Result of testing for containment between this and specified . + public ContainmentType Contains(BoundingBox box) + { + var result = default(ContainmentType); + this.Contains(ref box, out result); + return result; + } + + /// + /// Containment test between this and specified . + /// + /// A for testing. + /// Result of testing for containment between this and specified as an output parameter. + public void Contains(ref BoundingBox box, out ContainmentType result) + { + var intersects = false; + for (var i = 0; i < PlaneCount; ++i) + { + var planeIntersectionType = default(PlaneIntersectionType); + box.Intersects(ref this._planes[i], out planeIntersectionType); + switch (planeIntersectionType) + { + case PlaneIntersectionType.Front: + result = ContainmentType.Disjoint; + return; + case PlaneIntersectionType.Intersecting: + intersects = true; + break; + } + } + result = intersects ? ContainmentType.Intersects : ContainmentType.Contains; + } + + /// + /// Containment test between this and specified . + /// + /// A for testing. + /// Result of testing for containment between this and specified . + public ContainmentType Contains(BoundingFrustum frustum) + { + if (this == frustum) // We check to see if the two frustums are equal + return ContainmentType.Contains;// If they are, there's no need to go any further. + + var intersects = false; + for (var i = 0; i < PlaneCount; ++i) + { + PlaneIntersectionType planeIntersectionType; + frustum.Intersects(ref _planes[i], out planeIntersectionType); + switch (planeIntersectionType) + { + case PlaneIntersectionType.Front: + return ContainmentType.Disjoint; + case PlaneIntersectionType.Intersecting: + intersects = true; + break; + } + } + return intersects ? ContainmentType.Intersects : ContainmentType.Contains; + } + + /// + /// Containment test between this and specified . + /// + /// A for testing. + /// Result of testing for containment between this and specified . + public ContainmentType Contains(BoundingSphere sphere) + { + var result = default(ContainmentType); + this.Contains(ref sphere, out result); + return result; + } + + /// + /// Containment test between this and specified . + /// + /// A for testing. + /// Result of testing for containment between this and specified as an output parameter. + public void Contains(ref BoundingSphere sphere, out ContainmentType result) + { + var intersects = false; + for (var i = 0; i < PlaneCount; ++i) + { + var planeIntersectionType = default(PlaneIntersectionType); + + // TODO: we might want to inline this for performance reasons + sphere.Intersects(ref this._planes[i], out planeIntersectionType); + switch (planeIntersectionType) + { + case PlaneIntersectionType.Front: + result = ContainmentType.Disjoint; + return; + case PlaneIntersectionType.Intersecting: + intersects = true; + break; + } + } + result = intersects ? ContainmentType.Intersects : ContainmentType.Contains; + } + + /// + /// Containment test between this and specified . + /// + /// A for testing. + /// Result of testing for containment between this and specified . + public ContainmentType Contains(Vector3 point) + { + var result = default(ContainmentType); + this.Contains(ref point, out result); + return result; + } + + /// + /// Containment test between this and specified . + /// + /// A for testing. + /// Result of testing for containment between this and specified as an output parameter. + public void Contains(ref Vector3 point, out ContainmentType result) + { + for (var i = 0; i < PlaneCount; ++i) + { + // TODO: we might want to inline this for performance reasons + if (PlaneHelper.ClassifyPoint(ref point, ref this._planes[i]) > 0) + { + result = ContainmentType.Disjoint; + return; + } + } + result = ContainmentType.Contains; + } + + #endregion + + /// + /// Compares whether current instance is equal to specified . + /// + /// The to compare. + /// true if the instances are equal; false otherwise. + public bool Equals(BoundingFrustum other) + { + return (this == other); + } + + /// + /// Compares whether current instance is equal to specified . + /// + /// The to compare. + /// true if the instances are equal; false otherwise. + public override bool Equals(object obj) + { + return (obj is BoundingFrustum) && this == ((BoundingFrustum)obj); + } + + /// + /// Returns a copy of internal corners array. + /// + /// The array of corners. + public Vector3[] GetCorners() + { + return (Vector3[])this._corners.Clone(); + } + + /// + /// Returns a copy of internal corners array. + /// + /// The array which values will be replaced to corner values of this instance. It must have size of . + public void GetCorners(Vector3[] corners) + { + if (corners == null) throw new ArgumentNullException("corners"); + if (corners.Length < CornerCount) throw new ArgumentOutOfRangeException("corners"); + + this._corners.CopyTo(corners, 0); + } + + /// + /// Gets the hash code of this . + /// + /// Hash code of this . + public override int GetHashCode() + { + return this._matrix.GetHashCode(); + } + + /// + /// Gets whether or not a specified intersects with this . + /// + /// A for intersection test. + /// true if specified intersects with this ; false otherwise. + public bool Intersects(BoundingBox box) + { + var result = false; + this.Intersects(ref box, out result); + return result; + } + + /// + /// Gets whether or not a specified intersects with this . + /// + /// A for intersection test. + /// true if specified intersects with this ; false otherwise as an output parameter. + public void Intersects(ref BoundingBox box, out bool result) + { + var containment = default(ContainmentType); + this.Contains(ref box, out containment); + result = containment != ContainmentType.Disjoint; + } + + /// + /// Gets whether or not a specified intersects with this . + /// + /// An other for intersection test. + /// true if other intersects with this ; false otherwise. + public bool Intersects(BoundingFrustum frustum) + { + return Contains(frustum) != ContainmentType.Disjoint; + } + + /// + /// Gets whether or not a specified intersects with this . + /// + /// A for intersection test. + /// true if specified intersects with this ; false otherwise. + public bool Intersects(BoundingSphere sphere) + { + var result = default(bool); + this.Intersects(ref sphere, out result); + return result; + } + + /// + /// Gets whether or not a specified intersects with this . + /// + /// A for intersection test. + /// true if specified intersects with this ; false otherwise as an output parameter. + public void Intersects(ref BoundingSphere sphere, out bool result) + { + var containment = default(ContainmentType); + this.Contains(ref sphere, out containment); + result = containment != ContainmentType.Disjoint; + } + + /// + /// Gets type of intersection between specified and this . + /// + /// A for intersection test. + /// A plane intersection type. + public PlaneIntersectionType Intersects(Plane plane) + { + PlaneIntersectionType result; + Intersects(ref plane, out result); + return result; + } + + /// + /// Gets type of intersection between specified and this . + /// + /// A for intersection test. + /// A plane intersection type as an output parameter. + public void Intersects(ref Plane plane, out PlaneIntersectionType result) + { + result = plane.Intersects(ref _corners[0]); + for (int i = 1; i < _corners.Length; i++) + if (plane.Intersects(ref _corners[i]) != result) + result = PlaneIntersectionType.Intersecting; + } + + /// + /// Gets the distance of intersection of and this or null if no intersection happens. + /// + /// A for intersection test. + /// Distance at which ray intersects with this or null if no intersection happens. + public float? Intersects(Ray ray) + { + float? result; + Intersects(ref ray, out result); + return result; + } + + /// + /// Gets the distance of intersection of and this or null if no intersection happens. + /// + /// A for intersection test. + /// Distance at which ray intersects with this or null if no intersection happens as an output parameter. + public void Intersects(ref Ray ray, out float? result) + { + ContainmentType ctype; + this.Contains(ref ray.Position, out ctype); + + switch (ctype) + { + case ContainmentType.Disjoint: + result = null; + return; + case ContainmentType.Contains: + result = 0.0f; + return; + case ContainmentType.Intersects: + throw new NotImplementedException(); + default: + throw new ArgumentOutOfRangeException(); + } + } + + /// + /// Returns a representation of this in the format: + /// {Near:[nearPlane] Far:[farPlane] Left:[leftPlane] Right:[rightPlane] Top:[topPlane] Bottom:[bottomPlane]} + /// + /// representation of this . + public override string ToString() + { + return "{Near: " + this._planes[0] + + " Far:" + this._planes[1] + + " Left:" + this._planes[2] + + " Right:" + this._planes[3] + + " Top:" + this._planes[4] + + " Bottom:" + this._planes[5] + + "}"; + } + + #endregion + + #region Private Methods + + private void CreateCorners() + { + IntersectionPoint(ref this._planes[0], ref this._planes[2], ref this._planes[4], out this._corners[0]); + IntersectionPoint(ref this._planes[0], ref this._planes[3], ref this._planes[4], out this._corners[1]); + IntersectionPoint(ref this._planes[0], ref this._planes[3], ref this._planes[5], out this._corners[2]); + IntersectionPoint(ref this._planes[0], ref this._planes[2], ref this._planes[5], out this._corners[3]); + IntersectionPoint(ref this._planes[1], ref this._planes[2], ref this._planes[4], out this._corners[4]); + IntersectionPoint(ref this._planes[1], ref this._planes[3], ref this._planes[4], out this._corners[5]); + IntersectionPoint(ref this._planes[1], ref this._planes[3], ref this._planes[5], out this._corners[6]); + IntersectionPoint(ref this._planes[1], ref this._planes[2], ref this._planes[5], out this._corners[7]); + } + + private void CreatePlanes() + { + this._planes[0] = new Plane(-this._matrix.M13, -this._matrix.M23, -this._matrix.M33, -this._matrix.M43); + this._planes[1] = new Plane(this._matrix.M13 - this._matrix.M14, this._matrix.M23 - this._matrix.M24, this._matrix.M33 - this._matrix.M34, this._matrix.M43 - this._matrix.M44); + this._planes[2] = new Plane(-this._matrix.M14 - this._matrix.M11, -this._matrix.M24 - this._matrix.M21, -this._matrix.M34 - this._matrix.M31, -this._matrix.M44 - this._matrix.M41); + this._planes[3] = new Plane(this._matrix.M11 - this._matrix.M14, this._matrix.M21 - this._matrix.M24, this._matrix.M31 - this._matrix.M34, this._matrix.M41 - this._matrix.M44); + this._planes[4] = new Plane(this._matrix.M12 - this._matrix.M14, this._matrix.M22 - this._matrix.M24, this._matrix.M32 - this._matrix.M34, this._matrix.M42 - this._matrix.M44); + this._planes[5] = new Plane(-this._matrix.M14 - this._matrix.M12, -this._matrix.M24 - this._matrix.M22, -this._matrix.M34 - this._matrix.M32, -this._matrix.M44 - this._matrix.M42); + + this.NormalizePlane(ref this._planes[0]); + this.NormalizePlane(ref this._planes[1]); + this.NormalizePlane(ref this._planes[2]); + this.NormalizePlane(ref this._planes[3]); + this.NormalizePlane(ref this._planes[4]); + this.NormalizePlane(ref this._planes[5]); + } + + private static void IntersectionPoint(ref Plane a, ref Plane b, ref Plane c, out Vector3 result) + { + // Formula used + // d1 ( N2 * N3 ) + d2 ( N3 * N1 ) + d3 ( N1 * N2 ) + //P = ------------------------------------------------------------------------- + // N1 . ( N2 * N3 ) + // + // Note: N refers to the normal, d refers to the displacement. '.' means dot product. '*' means cross product + + Vector3 v1, v2, v3; + Vector3 cross; + + Vector3.Cross(ref b.Normal, ref c.Normal, out cross); + + float f; + Vector3.Dot(ref a.Normal, ref cross, out f); + f *= -1.0f; + + Vector3.Cross(ref b.Normal, ref c.Normal, out cross); + Vector3.Multiply(ref cross, a.D, out v1); + //v1 = (a.D * (Vector3.Cross(b.Normal, c.Normal))); + + + Vector3.Cross(ref c.Normal, ref a.Normal, out cross); + Vector3.Multiply(ref cross, b.D, out v2); + //v2 = (b.D * (Vector3.Cross(c.Normal, a.Normal))); + + + Vector3.Cross(ref a.Normal, ref b.Normal, out cross); + Vector3.Multiply(ref cross, c.D, out v3); + //v3 = (c.D * (Vector3.Cross(a.Normal, b.Normal))); + + result.X = (v1.X + v2.X + v3.X) / f; + result.Y = (v1.Y + v2.Y + v3.Y) / f; + result.Z = (v1.Z + v2.Z + v3.Z) / f; + } + + private void NormalizePlane(ref Plane p) + { + float factor = 1f / p.Normal.Length(); + p.Normal.X *= factor; + p.Normal.Y *= factor; + p.Normal.Z *= factor; + p.D *= factor; + } + + #endregion + } +} + diff --git a/MonoGame.Framework/BoundingSphere.cs b/MonoGame.Framework/BoundingSphere.cs new file mode 100644 index 00000000000..7b3b9922232 --- /dev/null +++ b/MonoGame.Framework/BoundingSphere.cs @@ -0,0 +1,640 @@ +// MIT License - Copyright (C) The Mono.Xna Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Serialization; + +namespace Microsoft.Xna.Framework +{ + /// + /// Describes a sphere in 3D-space for bounding operations. + /// + [DataContract] + [DebuggerDisplay("{DebugDisplayString,nq}")] + public struct BoundingSphere : IEquatable + { + #region Public Fields + + /// + /// The sphere center. + /// + [DataMember] + public Vector3 Center; + + /// + /// The sphere radius. + /// + [DataMember] + public float Radius; + + #endregion + + #region Internal Properties + + internal string DebugDisplayString + { + get + { + return string.Concat( + "Center( ", this.Center.DebugDisplayString, " ) \r\n", + "Radius( ", this.Radius.ToString(), " )" + ); + } + } + + #endregion + + #region Constructors + + /// + /// Constructs a bounding sphere with the specified center and radius. + /// + /// The sphere center. + /// The sphere radius. + public BoundingSphere(Vector3 center, float radius) + { + this.Center = center; + this.Radius = radius; + } + + #endregion + + #region Public Methods + + #region Contains + + /// + /// Test if a bounding box is fully inside, outside, or just intersecting the sphere. + /// + /// The box for testing. + /// The containment type. + public ContainmentType Contains(BoundingBox box) + { + //check if all corner is in sphere + bool inside = true; + foreach (Vector3 corner in box.GetCorners()) + { + if (this.Contains(corner) == ContainmentType.Disjoint) + { + inside = false; + break; + } + } + + if (inside) + return ContainmentType.Contains; + + //check if the distance from sphere center to cube face < radius + double dmin = 0; + + if (Center.X < box.Min.X) + dmin += (Center.X - box.Min.X) * (Center.X - box.Min.X); + + else if (Center.X > box.Max.X) + dmin += (Center.X - box.Max.X) * (Center.X - box.Max.X); + + if (Center.Y < box.Min.Y) + dmin += (Center.Y - box.Min.Y) * (Center.Y - box.Min.Y); + + else if (Center.Y > box.Max.Y) + dmin += (Center.Y - box.Max.Y) * (Center.Y - box.Max.Y); + + if (Center.Z < box.Min.Z) + dmin += (Center.Z - box.Min.Z) * (Center.Z - box.Min.Z); + + else if (Center.Z > box.Max.Z) + dmin += (Center.Z - box.Max.Z) * (Center.Z - box.Max.Z); + + if (dmin <= Radius * Radius) + return ContainmentType.Intersects; + + //else disjoint + return ContainmentType.Disjoint; + } + + /// + /// Test if a bounding box is fully inside, outside, or just intersecting the sphere. + /// + /// The box for testing. + /// The containment type as an output parameter. + public void Contains(ref BoundingBox box, out ContainmentType result) + { + result = this.Contains(box); + } + + /// + /// Test if a frustum is fully inside, outside, or just intersecting the sphere. + /// + /// The frustum for testing. + /// The containment type. + public ContainmentType Contains(BoundingFrustum frustum) + { + //check if all corner is in sphere + bool inside = true; + + Vector3[] corners = frustum.GetCorners(); + foreach (Vector3 corner in corners) + { + if (this.Contains(corner) == ContainmentType.Disjoint) + { + inside = false; + break; + } + } + if (inside) + return ContainmentType.Contains; + + //check if the distance from sphere center to frustrum face < radius + double dmin = 0; + //TODO : calcul dmin + + if (dmin <= Radius * Radius) + return ContainmentType.Intersects; + + //else disjoint + return ContainmentType.Disjoint; + } + + /// + /// Test if a frustum is fully inside, outside, or just intersecting the sphere. + /// + /// The frustum for testing. + /// The containment type as an output parameter. + public void Contains(ref BoundingFrustum frustum,out ContainmentType result) + { + result = this.Contains(frustum); + } + + /// + /// Test if a sphere is fully inside, outside, or just intersecting the sphere. + /// + /// The other sphere for testing. + /// The containment type. + public ContainmentType Contains(BoundingSphere sphere) + { + ContainmentType result; + Contains(ref sphere, out result); + return result; + } + + /// + /// Test if a sphere is fully inside, outside, or just intersecting the sphere. + /// + /// The other sphere for testing. + /// The containment type as an output parameter. + public void Contains(ref BoundingSphere sphere, out ContainmentType result) + { + float sqDistance; + Vector3.DistanceSquared(ref sphere.Center, ref Center, out sqDistance); + + if (sqDistance > (sphere.Radius + Radius) * (sphere.Radius + Radius)) + result = ContainmentType.Disjoint; + + else if (sqDistance <= (Radius - sphere.Radius) * (Radius - sphere.Radius)) + result = ContainmentType.Contains; + + else + result = ContainmentType.Intersects; + } + + /// + /// Test if a point is fully inside, outside, or just intersecting the sphere. + /// + /// The vector in 3D-space for testing. + /// The containment type. + public ContainmentType Contains(Vector3 point) + { + ContainmentType result; + Contains(ref point, out result); + return result; + } + + /// + /// Test if a point is fully inside, outside, or just intersecting the sphere. + /// + /// The vector in 3D-space for testing. + /// The containment type as an output parameter. + public void Contains(ref Vector3 point, out ContainmentType result) + { + float sqRadius = Radius * Radius; + float sqDistance; + Vector3.DistanceSquared(ref point, ref Center, out sqDistance); + + if (sqDistance > sqRadius) + result = ContainmentType.Disjoint; + + else if (sqDistance < sqRadius) + result = ContainmentType.Contains; + + else + result = ContainmentType.Intersects; + } + + #endregion + + #region CreateFromBoundingBox + + /// + /// Creates the smallest that can contain a specified . + /// + /// The box to create the sphere from. + /// The new . + public static BoundingSphere CreateFromBoundingBox(BoundingBox box) + { + BoundingSphere result; + CreateFromBoundingBox(ref box, out result); + return result; + } + + /// + /// Creates the smallest that can contain a specified . + /// + /// The box to create the sphere from. + /// The new as an output parameter. + public static void CreateFromBoundingBox(ref BoundingBox box, out BoundingSphere result) + { + // Find the center of the box. + Vector3 center = new Vector3((box.Min.X + box.Max.X) / 2.0f, + (box.Min.Y + box.Max.Y) / 2.0f, + (box.Min.Z + box.Max.Z) / 2.0f); + + // Find the distance between the center and one of the corners of the box. + float radius = Vector3.Distance(center, box.Max); + + result = new BoundingSphere(center, radius); + } + + #endregion + + /// + /// Creates the smallest that can contain a specified . + /// + /// The frustum to create the sphere from. + /// The new . + public static BoundingSphere CreateFromFrustum(BoundingFrustum frustum) + { + return CreateFromPoints(frustum.GetCorners()); + } + + /// + /// Creates the smallest that can contain a specified list of points in 3D-space. + /// + /// List of point to create the sphere from. + /// The new . + public static BoundingSphere CreateFromPoints(IEnumerable points) + { + if (points == null ) + throw new ArgumentNullException("points"); + + // From "Real-Time Collision Detection" (Page 89) + + var minx = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue); + var maxx = -minx; + var miny = minx; + var maxy = -minx; + var minz = minx; + var maxz = -minx; + + // Find the most extreme points along the principle axis. + var numPoints = 0; + foreach (var pt in points) + { + ++numPoints; + + if (pt.X < minx.X) + minx = pt; + if (pt.X > maxx.X) + maxx = pt; + if (pt.Y < miny.Y) + miny = pt; + if (pt.Y > maxy.Y) + maxy = pt; + if (pt.Z < minz.Z) + minz = pt; + if (pt.Z > maxz.Z) + maxz = pt; + } + + if (numPoints == 0) + throw new ArgumentException("You should have at least one point in points."); + + var sqDistX = Vector3.DistanceSquared(maxx, minx); + var sqDistY = Vector3.DistanceSquared(maxy, miny); + var sqDistZ = Vector3.DistanceSquared(maxz, minz); + + // Pick the pair of most distant points. + var min = minx; + var max = maxx; + if (sqDistY > sqDistX && sqDistY > sqDistZ) + { + max = maxy; + min = miny; + } + if (sqDistZ > sqDistX && sqDistZ > sqDistY) + { + max = maxz; + min = minz; + } + + var center = (min + max) * 0.5f; + var radius = Vector3.Distance(max, center); + + // Test every point and expand the sphere. + // The current bounding sphere is just a good approximation and may not enclose all points. + // From: Mathematics for 3D Game Programming and Computer Graphics, Eric Lengyel, Third Edition. + // Page 218 + float sqRadius = radius * radius; + foreach (var pt in points) + { + Vector3 diff = (pt-center); + float sqDist = diff.LengthSquared(); + if (sqDist > sqRadius) + { + float distance = (float)Math.Sqrt(sqDist); // equal to diff.Length(); + Vector3 direction = diff / distance; + Vector3 G = center - radius * direction; + center = (G + pt) / 2; + radius = Vector3.Distance(pt, center); + sqRadius = radius * radius; + } + } + + return new BoundingSphere(center, radius); + } + + /// + /// Creates the smallest that can contain two spheres. + /// + /// First sphere. + /// Second sphere. + /// The new . + public static BoundingSphere CreateMerged(BoundingSphere original, BoundingSphere additional) + { + BoundingSphere result; + CreateMerged(ref original, ref additional, out result); + return result; + } + + /// + /// Creates the smallest that can contain two spheres. + /// + /// First sphere. + /// Second sphere. + /// The new as an output parameter. + public static void CreateMerged(ref BoundingSphere original, ref BoundingSphere additional, out BoundingSphere result) + { + Vector3 ocenterToaCenter = Vector3.Subtract(additional.Center, original.Center); + float distance = ocenterToaCenter.Length(); + if (distance <= original.Radius + additional.Radius)//intersect + { + if (distance <= original.Radius - additional.Radius)//original contain additional + { + result = original; + return; + } + if (distance <= additional.Radius - original.Radius)//additional contain original + { + result = additional; + return; + } + } + //else find center of new sphere and radius + float leftRadius = Math.Max(original.Radius - distance, additional.Radius); + float Rightradius = Math.Max(original.Radius + distance, additional.Radius); + ocenterToaCenter = ocenterToaCenter + (((leftRadius - Rightradius) / (2 * ocenterToaCenter.Length())) * ocenterToaCenter);//oCenterToResultCenter + + result = new BoundingSphere(); + result.Center = original.Center + ocenterToaCenter; + result.Radius = (leftRadius + Rightradius) / 2; + } + + /// + /// Compares whether current instance is equal to specified . + /// + /// The to compare. + /// true if the instances are equal; false otherwise. + public bool Equals(BoundingSphere other) + { + return this.Center == other.Center && this.Radius == other.Radius; + } + + /// + /// Compares whether current instance is equal to specified . + /// + /// The to compare. + /// true if the instances are equal; false otherwise. + public override bool Equals(object obj) + { + if (obj is BoundingSphere) + return this.Equals((BoundingSphere)obj); + + return false; + } + + /// + /// Gets the hash code of this . + /// + /// Hash code of this . + public override int GetHashCode() + { + return this.Center.GetHashCode() + this.Radius.GetHashCode(); + } + + #region Intersects + + /// + /// Gets whether or not a specified intersects with this sphere. + /// + /// The box for testing. + /// true if intersects with this sphere; false otherwise. + public bool Intersects(BoundingBox box) + { + return box.Intersects(this); + } + + /// + /// Gets whether or not a specified intersects with this sphere. + /// + /// The box for testing. + /// true if intersects with this sphere; false otherwise. As an output parameter. + public void Intersects(ref BoundingBox box, out bool result) + { + box.Intersects(ref this, out result); + } + + /* + TODO : Make the public bool Intersects(BoundingFrustum frustum) overload + + public bool Intersects(BoundingFrustum frustum) + { + if (frustum == null) + throw new NullReferenceException(); + + throw new NotImplementedException(); + } + + */ + + /// + /// Gets whether or not the other intersects with this sphere. + /// + /// The other sphere for testing. + /// true if other intersects with this sphere; false otherwise. + public bool Intersects(BoundingSphere sphere) + { + bool result; + Intersects(ref sphere, out result); + return result; + } + + /// + /// Gets whether or not the other intersects with this sphere. + /// + /// The other sphere for testing. + /// true if other intersects with this sphere; false otherwise. As an output parameter. + public void Intersects(ref BoundingSphere sphere, out bool result) + { + float sqDistance; + Vector3.DistanceSquared(ref sphere.Center, ref Center, out sqDistance); + + if (sqDistance > (sphere.Radius + Radius) * (sphere.Radius + Radius)) + result = false; + else + result = true; + } + + /// + /// Gets whether or not a specified intersects with this sphere. + /// + /// The plane for testing. + /// Type of intersection. + public PlaneIntersectionType Intersects(Plane plane) + { + var result = default(PlaneIntersectionType); + // TODO: we might want to inline this for performance reasons + this.Intersects(ref plane, out result); + return result; + } + + /// + /// Gets whether or not a specified intersects with this sphere. + /// + /// The plane for testing. + /// Type of intersection as an output parameter. + public void Intersects(ref Plane plane, out PlaneIntersectionType result) + { + var distance = default(float); + // TODO: we might want to inline this for performance reasons + Vector3.Dot(ref plane.Normal, ref this.Center, out distance); + distance += plane.D; + if (distance > this.Radius) + result = PlaneIntersectionType.Front; + else if (distance < -this.Radius) + result = PlaneIntersectionType.Back; + else + result = PlaneIntersectionType.Intersecting; + } + + /// + /// Gets whether or not a specified intersects with this sphere. + /// + /// The ray for testing. + /// Distance of ray intersection or null if there is no intersection. + public float? Intersects(Ray ray) + { + return ray.Intersects(this); + } + + /// + /// Gets whether or not a specified intersects with this sphere. + /// + /// The ray for testing. + /// Distance of ray intersection or null if there is no intersection as an output parameter. + public void Intersects(ref Ray ray, out float? result) + { + ray.Intersects(ref this, out result); + } + + #endregion + + /// + /// Returns a representation of this in the format: + /// {Center:[] Radius:[]} + /// + /// A representation of this . + public override string ToString() + { + return "{Center:" + this.Center + " Radius:" + this.Radius + "}"; + } + + #region Transform + + /// + /// Creates a new that contains a transformation of translation and scale from this sphere by the specified . + /// + /// The transformation . + /// Transformed . + public BoundingSphere Transform(Matrix matrix) + { + BoundingSphere sphere = new BoundingSphere(); + sphere.Center = Vector3.Transform(this.Center, matrix); + sphere.Radius = this.Radius * ((float)Math.Sqrt((double)Math.Max(((matrix.M11 * matrix.M11) + (matrix.M12 * matrix.M12)) + (matrix.M13 * matrix.M13), Math.Max(((matrix.M21 * matrix.M21) + (matrix.M22 * matrix.M22)) + (matrix.M23 * matrix.M23), ((matrix.M31 * matrix.M31) + (matrix.M32 * matrix.M32)) + (matrix.M33 * matrix.M33))))); + return sphere; + } + + /// + /// Creates a new that contains a transformation of translation and scale from this sphere by the specified . + /// + /// The transformation . + /// Transformed as an output parameter. + public void Transform(ref Matrix matrix, out BoundingSphere result) + { + result.Center = Vector3.Transform(this.Center, matrix); + result.Radius = this.Radius * ((float)Math.Sqrt((double)Math.Max(((matrix.M11 * matrix.M11) + (matrix.M12 * matrix.M12)) + (matrix.M13 * matrix.M13), Math.Max(((matrix.M21 * matrix.M21) + (matrix.M22 * matrix.M22)) + (matrix.M23 * matrix.M23), ((matrix.M31 * matrix.M31) + (matrix.M32 * matrix.M32)) + (matrix.M33 * matrix.M33))))); + } + + #endregion + + /// + /// Deconstruction method for . + /// + /// + /// + public void Deconstruct(out Vector3 center, out float radius) + { + center = Center; + radius = Radius; + } + + #endregion + + #region Operators + + /// + /// Compares whether two instances are equal. + /// + /// instance on the left of the equal sign. + /// instance on the right of the equal sign. + /// true if the instances are equal; false otherwise. + public static bool operator == (BoundingSphere a, BoundingSphere b) + { + return a.Equals(b); + } + + /// + /// Compares whether two instances are not equal. + /// + /// instance on the left of the not equal sign. + /// instance on the right of the not equal sign. + /// true if the instances are not equal; false otherwise. + public static bool operator != (BoundingSphere a, BoundingSphere b) + { + return !a.Equals(b); + } + + #endregion + } +} diff --git a/MonoGame.Framework/Color.cs b/MonoGame.Framework/Color.cs new file mode 100644 index 00000000000..78becbafc95 --- /dev/null +++ b/MonoGame.Framework/Color.cs @@ -0,0 +1,1901 @@ +// MIT License - Copyright (C) The Mono.Xna Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Text; +using System.Runtime.Serialization; +using System.Diagnostics; + +namespace Microsoft.Xna.Framework +{ + /// + /// Describes a 32-bit packed color. + /// + [DataContract] + [DebuggerDisplay("{DebugDisplayString,nq}")] + public struct Color : IEquatable + { + static Color() + { + TransparentBlack = new Color(0); + Transparent = new Color(0); + AliceBlue = new Color(0xfffff8f0); + AntiqueWhite = new Color(0xffd7ebfa); + Aqua = new Color(0xffffff00); + Aquamarine = new Color(0xffd4ff7f); + Azure = new Color(0xfffffff0); + Beige = new Color(0xffdcf5f5); + Bisque = new Color(0xffc4e4ff); + Black = new Color(0xff000000); + BlanchedAlmond = new Color(0xffcdebff); + Blue = new Color(0xffff0000); + BlueViolet = new Color(0xffe22b8a); + Brown = new Color(0xff2a2aa5); + BurlyWood = new Color(0xff87b8de); + CadetBlue = new Color(0xffa09e5f); + Chartreuse = new Color(0xff00ff7f); + Chocolate = new Color(0xff1e69d2); + Coral = new Color(0xff507fff); + CornflowerBlue = new Color(0xffed9564); + Cornsilk = new Color(0xffdcf8ff); + Crimson = new Color(0xff3c14dc); + Cyan = new Color(0xffffff00); + DarkBlue = new Color(0xff8b0000); + DarkCyan = new Color(0xff8b8b00); + DarkGoldenrod = new Color(0xff0b86b8); + DarkGray = new Color(0xffa9a9a9); + DarkGreen = new Color(0xff006400); + DarkKhaki = new Color(0xff6bb7bd); + DarkMagenta = new Color(0xff8b008b); + DarkOliveGreen = new Color(0xff2f6b55); + DarkOrange = new Color(0xff008cff); + DarkOrchid = new Color(0xffcc3299); + DarkRed = new Color(0xff00008b); + DarkSalmon = new Color(0xff7a96e9); + DarkSeaGreen = new Color(0xff8bbc8f); + DarkSlateBlue = new Color(0xff8b3d48); + DarkSlateGray = new Color(0xff4f4f2f); + DarkTurquoise = new Color(0xffd1ce00); + DarkViolet = new Color(0xffd30094); + DeepPink = new Color(0xff9314ff); + DeepSkyBlue = new Color(0xffffbf00); + DimGray = new Color(0xff696969); + DodgerBlue = new Color(0xffff901e); + Firebrick = new Color(0xff2222b2); + FloralWhite = new Color(0xfff0faff); + ForestGreen = new Color(0xff228b22); + Fuchsia = new Color(0xffff00ff); + Gainsboro = new Color(0xffdcdcdc); + GhostWhite = new Color(0xfffff8f8); + Gold = new Color(0xff00d7ff); + Goldenrod = new Color(0xff20a5da); + Gray = new Color(0xff808080); + Green = new Color(0xff008000); + GreenYellow = new Color(0xff2fffad); + Honeydew = new Color(0xfff0fff0); + HotPink = new Color(0xffb469ff); + IndianRed = new Color(0xff5c5ccd); + Indigo = new Color(0xff82004b); + Ivory = new Color(0xfff0ffff); + Khaki = new Color(0xff8ce6f0); + Lavender = new Color(0xfffae6e6); + LavenderBlush = new Color(0xfff5f0ff); + LawnGreen = new Color(0xff00fc7c); + LemonChiffon = new Color(0xffcdfaff); + LightBlue = new Color(0xffe6d8ad); + LightCoral = new Color(0xff8080f0); + LightCyan = new Color(0xffffffe0); + LightGoldenrodYellow = new Color(0xffd2fafa); + LightGray = new Color(0xffd3d3d3); + LightGreen = new Color(0xff90ee90); + LightPink = new Color(0xffc1b6ff); + LightSalmon = new Color(0xff7aa0ff); + LightSeaGreen = new Color(0xffaab220); + LightSkyBlue = new Color(0xffface87); + LightSlateGray = new Color(0xff998877); + LightSteelBlue = new Color(0xffdec4b0); + LightYellow = new Color(0xffe0ffff); + Lime = new Color(0xff00ff00); + LimeGreen = new Color(0xff32cd32); + Linen = new Color(0xffe6f0fa); + Magenta = new Color(0xffff00ff); + Maroon = new Color(0xff000080); + MediumAquamarine = new Color(0xffaacd66); + MediumBlue = new Color(0xffcd0000); + MediumOrchid = new Color(0xffd355ba); + MediumPurple = new Color(0xffdb7093); + MediumSeaGreen = new Color(0xff71b33c); + MediumSlateBlue = new Color(0xffee687b); + MediumSpringGreen = new Color(0xff9afa00); + MediumTurquoise = new Color(0xffccd148); + MediumVioletRed = new Color(0xff8515c7); + MidnightBlue = new Color(0xff701919); + MintCream = new Color(0xfffafff5); + MistyRose = new Color(0xffe1e4ff); + Moccasin = new Color(0xffb5e4ff); + MonoGameOrange = new Color(0xff003ce7); + NavajoWhite = new Color(0xffaddeff); + Navy = new Color(0xff800000); + OldLace = new Color(0xffe6f5fd); + Olive = new Color(0xff008080); + OliveDrab = new Color(0xff238e6b); + Orange = new Color(0xff00a5ff); + OrangeRed = new Color(0xff0045ff); + Orchid = new Color(0xffd670da); + PaleGoldenrod = new Color(0xffaae8ee); + PaleGreen = new Color(0xff98fb98); + PaleTurquoise = new Color(0xffeeeeaf); + PaleVioletRed = new Color(0xff9370db); + PapayaWhip = new Color(0xffd5efff); + PeachPuff = new Color(0xffb9daff); + Peru = new Color(0xff3f85cd); + Pink = new Color(0xffcbc0ff); + Plum = new Color(0xffdda0dd); + PowderBlue = new Color(0xffe6e0b0); + Purple = new Color(0xff800080); + Red = new Color(0xff0000ff); + RosyBrown = new Color(0xff8f8fbc); + RoyalBlue = new Color(0xffe16941); + SaddleBrown = new Color(0xff13458b); + Salmon= new Color(0xff7280fa); + SandyBrown = new Color(0xff60a4f4); + SeaGreen = new Color(0xff578b2e); + SeaShell = new Color(0xffeef5ff); + Sienna = new Color(0xff2d52a0); + Silver = new Color(0xffc0c0c0); + SkyBlue = new Color(0xffebce87); + SlateBlue= new Color(0xffcd5a6a); + SlateGray= new Color(0xff908070); + Snow= new Color(0xfffafaff); + SpringGreen= new Color(0xff7fff00); + SteelBlue= new Color(0xffb48246); + Tan= new Color(0xff8cb4d2); + Teal= new Color(0xff808000); + Thistle= new Color(0xffd8bfd8); + Tomato= new Color(0xff4763ff); + Turquoise= new Color(0xffd0e040); + Violet= new Color(0xffee82ee); + Wheat= new Color(0xffb3def5); + White= new Color(uint.MaxValue); + WhiteSmoke= new Color(0xfff5f5f5); + Yellow = new Color(0xff00ffff); + YellowGreen = new Color(0xff32cd9a); + } + + // Stored as RGBA with R in the least significant octet: + // |-------|-------|-------|------- + // A B G R + private uint _packedValue; + + /// + /// Constructs an RGBA color from a packed value. + /// The value is a 32-bit unsigned integer, with R in the least significant octet. + /// + /// The packed value. + [CLSCompliant(false)] + public Color(uint packedValue) + { + _packedValue = packedValue; + } + + /// + /// Constructs an RGBA color from the XYZW unit length components of a vector. + /// + /// A representing color. + public Color(Vector4 color) + : this((int)(color.X * 255), (int)(color.Y * 255), (int)(color.Z * 255), (int)(color.W * 255)) + { + } + + /// + /// Constructs an RGBA color from the XYZ unit length components of a vector. Alpha value will be opaque. + /// + /// A representing color. + public Color(Vector3 color) + : this((int)(color.X * 255), (int)(color.Y * 255), (int)(color.Z * 255)) + { + } + + /// + /// Constructs an RGBA color from a and an alpha value. + /// + /// A for RGB values of new instance. + /// The alpha component value from 0 to 255. + public Color(Color color, int alpha) + { + if ((alpha & 0xFFFFFF00) != 0) + { + var clampedA = (uint)MathHelper.Clamp(alpha, Byte.MinValue, Byte.MaxValue); + + _packedValue = (color._packedValue & 0x00FFFFFF) | (clampedA << 24); + } + else + { + _packedValue = (color._packedValue & 0x00FFFFFF) | ((uint)alpha << 24); + } + } + + /// + /// Constructs an RGBA color from color and alpha value. + /// + /// A for RGB values of new instance. + /// Alpha component value from 0.0f to 1.0f. + public Color(Color color, float alpha): + this(color, (int)(alpha * 255)) + { + } + + /// + /// Constructs an RGBA color from scalars representing red, green and blue values. Alpha value will be opaque. + /// + /// Red component value from 0.0f to 1.0f. + /// Green component value from 0.0f to 1.0f. + /// Blue component value from 0.0f to 1.0f. + public Color(float r, float g, float b) + : this((int)(r * 255), (int)(g * 255), (int)(b * 255)) + { + } + + /// + /// Constructs an RGBA color from scalars representing red, green, blue and alpha values. + /// + /// Red component value from 0.0f to 1.0f. + /// Green component value from 0.0f to 1.0f. + /// Blue component value from 0.0f to 1.0f. + /// Alpha component value from 0.0f to 1.0f. + public Color(float r, float g, float b, float alpha) + : this((int)(r * 255), (int)(g * 255), (int)(b * 255), (int)(alpha * 255)) + { + } + + /// + /// Constructs an RGBA color from scalars representing red, green and blue values. Alpha value will be opaque. + /// + /// Red component value from 0 to 255. + /// Green component value from 0 to 255. + /// Blue component value from 0 to 255. + public Color(int r, int g, int b) + { + _packedValue = 0xFF000000; // A = 255 + + if (((r | g | b) & 0xFFFFFF00) != 0) + { + var clampedR = (uint)MathHelper.Clamp(r, Byte.MinValue, Byte.MaxValue); + var clampedG = (uint)MathHelper.Clamp(g, Byte.MinValue, Byte.MaxValue); + var clampedB = (uint)MathHelper.Clamp(b, Byte.MinValue, Byte.MaxValue); + + _packedValue |= (clampedB << 16) | (clampedG << 8) | (clampedR); + } + else + { + _packedValue |= ((uint)b << 16) | ((uint)g << 8) | ((uint)r); + } + } + + /// + /// Constructs an RGBA color from scalars representing red, green, blue and alpha values. + /// + /// Red component value from 0 to 255. + /// Green component value from 0 to 255. + /// Blue component value from 0 to 255. + /// Alpha component value from 0 to 255. + public Color(int r, int g, int b, int alpha) + { + if (((r | g | b | alpha) & 0xFFFFFF00) != 0) + { + var clampedR = (uint)MathHelper.Clamp(r, Byte.MinValue, Byte.MaxValue); + var clampedG = (uint)MathHelper.Clamp(g, Byte.MinValue, Byte.MaxValue); + var clampedB = (uint)MathHelper.Clamp(b, Byte.MinValue, Byte.MaxValue); + var clampedA = (uint)MathHelper.Clamp(alpha, Byte.MinValue, Byte.MaxValue); + + _packedValue = (clampedA << 24) | (clampedB << 16) | (clampedG << 8) | (clampedR); + } + else + { + _packedValue = ((uint)alpha << 24) | ((uint)b << 16) | ((uint)g << 8) | ((uint)r); + } + } + + /// + /// Constructs an RGBA color from scalars representing red, green, blue and alpha values. + /// + /// + /// This overload sets the values directly without clamping, and may therefore be faster than the other overloads. + /// + /// + /// + /// + /// + public Color(byte r, byte g, byte b, byte alpha) + { + _packedValue = ((uint)alpha << 24) | ((uint)b << 16) | ((uint)g << 8) | (r); + } + + /// + /// Gets or sets the blue component. + /// + [DataMember] + public byte B + { + get + { + unchecked + { + return (byte) (this._packedValue >> 16); + } + } + set + { + this._packedValue = (this._packedValue & 0xff00ffff) | ((uint)value << 16); + } + } + + /// + /// Gets or sets the green component. + /// + [DataMember] + public byte G + { + get + { + unchecked + { + return (byte)(this._packedValue >> 8); + } + } + set + { + this._packedValue = (this._packedValue & 0xffff00ff) | ((uint)value << 8); + } + } + + /// + /// Gets or sets the red component. + /// + [DataMember] + public byte R + { + get + { + unchecked + { + return (byte) this._packedValue; + } + } + set + { + this._packedValue = (this._packedValue & 0xffffff00) | value; + } + } + + /// + /// Gets or sets the alpha component. + /// + [DataMember] + public byte A + { + get + { + unchecked + { + return (byte)(this._packedValue >> 24); + } + } + set + { + this._packedValue = (this._packedValue & 0x00ffffff) | ((uint)value << 24); + } + } + + /// + /// Compares whether two instances are equal. + /// + /// instance on the left of the equal sign. + /// instance on the right of the equal sign. + /// true if the instances are equal; false otherwise. + public static bool operator ==(Color a, Color b) + { + return (a._packedValue == b._packedValue); + } + + /// + /// Compares whether two instances are not equal. + /// + /// instance on the left of the not equal sign. + /// instance on the right of the not equal sign. + /// true if the instances are not equal; false otherwise. + public static bool operator !=(Color a, Color b) + { + return (a._packedValue != b._packedValue); + } + + /// + /// Gets the hash code of this . + /// + /// Hash code of this . + public override int GetHashCode() + { + return this._packedValue.GetHashCode(); + } + + /// + /// Compares whether current instance is equal to specified object. + /// + /// The to compare. + /// true if the instances are equal; false otherwise. + public override bool Equals(object obj) + { + return ((obj is Color) && this.Equals((Color)obj)); + } + + #region Color Bank + /// + /// TransparentBlack color (R:0,G:0,B:0,A:0). + /// + public static Color TransparentBlack + { + get; + private set; + } + + /// + /// Transparent color (R:0,G:0,B:0,A:0). + /// + public static Color Transparent + { + get; + private set; + } + + /// + /// AliceBlue color (R:240,G:248,B:255,A:255). + /// + public static Color AliceBlue + { + get; + private set; + } + + /// + /// AntiqueWhite color (R:250,G:235,B:215,A:255). + /// + public static Color AntiqueWhite + { + get; + private set; + } + + /// + /// Aqua color (R:0,G:255,B:255,A:255). + /// + public static Color Aqua + { + get; + private set; + } + + /// + /// Aquamarine color (R:127,G:255,B:212,A:255). + /// + public static Color Aquamarine + { + get; + private set; + } + + /// + /// Azure color (R:240,G:255,B:255,A:255). + /// + public static Color Azure + { + get; + private set; + } + + /// + /// Beige color (R:245,G:245,B:220,A:255). + /// + public static Color Beige + { + get; + private set; + } + + /// + /// Bisque color (R:255,G:228,B:196,A:255). + /// + public static Color Bisque + { + get; + private set; + } + + /// + /// Black color (R:0,G:0,B:0,A:255). + /// + public static Color Black + { + get; + private set; + } + + /// + /// BlanchedAlmond color (R:255,G:235,B:205,A:255). + /// + public static Color BlanchedAlmond + { + get; + private set; + } + + /// + /// Blue color (R:0,G:0,B:255,A:255). + /// + public static Color Blue + { + get; + private set; + } + + /// + /// BlueViolet color (R:138,G:43,B:226,A:255). + /// + public static Color BlueViolet + { + get; + private set; + } + + /// + /// Brown color (R:165,G:42,B:42,A:255). + /// + public static Color Brown + { + get; + private set; + } + + /// + /// BurlyWood color (R:222,G:184,B:135,A:255). + /// + public static Color BurlyWood + { + get; + private set; + } + + /// + /// CadetBlue color (R:95,G:158,B:160,A:255). + /// + public static Color CadetBlue + { + get; + private set; + } + + /// + /// Chartreuse color (R:127,G:255,B:0,A:255). + /// + public static Color Chartreuse + { + get; + private set; + } + + /// + /// Chocolate color (R:210,G:105,B:30,A:255). + /// + public static Color Chocolate + { + get; + private set; + } + + /// + /// Coral color (R:255,G:127,B:80,A:255). + /// + public static Color Coral + { + get; + private set; + } + + /// + /// CornflowerBlue color (R:100,G:149,B:237,A:255). + /// + public static Color CornflowerBlue + { + get; + private set; + } + + /// + /// Cornsilk color (R:255,G:248,B:220,A:255). + /// + public static Color Cornsilk + { + get; + private set; + } + + /// + /// Crimson color (R:220,G:20,B:60,A:255). + /// + public static Color Crimson + { + get; + private set; + } + + /// + /// Cyan color (R:0,G:255,B:255,A:255). + /// + public static Color Cyan + { + get; + private set; + } + + /// + /// DarkBlue color (R:0,G:0,B:139,A:255). + /// + public static Color DarkBlue + { + get; + private set; + } + + /// + /// DarkCyan color (R:0,G:139,B:139,A:255). + /// + public static Color DarkCyan + { + get; + private set; + } + + /// + /// DarkGoldenrod color (R:184,G:134,B:11,A:255). + /// + public static Color DarkGoldenrod + { + get; + private set; + } + + /// + /// DarkGray color (R:169,G:169,B:169,A:255). + /// + public static Color DarkGray + { + get; + private set; + } + + /// + /// DarkGreen color (R:0,G:100,B:0,A:255). + /// + public static Color DarkGreen + { + get; + private set; + } + + /// + /// DarkKhaki color (R:189,G:183,B:107,A:255). + /// + public static Color DarkKhaki + { + get; + private set; + } + + /// + /// DarkMagenta color (R:139,G:0,B:139,A:255). + /// + public static Color DarkMagenta + { + get; + private set; + } + + /// + /// DarkOliveGreen color (R:85,G:107,B:47,A:255). + /// + public static Color DarkOliveGreen + { + get; + private set; + } + + /// + /// DarkOrange color (R:255,G:140,B:0,A:255). + /// + public static Color DarkOrange + { + get; + private set; + } + + /// + /// DarkOrchid color (R:153,G:50,B:204,A:255). + /// + public static Color DarkOrchid + { + get; + private set; + } + + /// + /// DarkRed color (R:139,G:0,B:0,A:255). + /// + public static Color DarkRed + { + get; + private set; + } + + /// + /// DarkSalmon color (R:233,G:150,B:122,A:255). + /// + public static Color DarkSalmon + { + get; + private set; + } + + /// + /// DarkSeaGreen color (R:143,G:188,B:139,A:255). + /// + public static Color DarkSeaGreen + { + get; + private set; + } + + /// + /// DarkSlateBlue color (R:72,G:61,B:139,A:255). + /// + public static Color DarkSlateBlue + { + get; + private set; + } + + /// + /// DarkSlateGray color (R:47,G:79,B:79,A:255). + /// + public static Color DarkSlateGray + { + get; + private set; + } + + /// + /// DarkTurquoise color (R:0,G:206,B:209,A:255). + /// + public static Color DarkTurquoise + { + get; + private set; + } + + /// + /// DarkViolet color (R:148,G:0,B:211,A:255). + /// + public static Color DarkViolet + { + get; + private set; + } + + /// + /// DeepPink color (R:255,G:20,B:147,A:255). + /// + public static Color DeepPink + { + get; + private set; + } + + /// + /// DeepSkyBlue color (R:0,G:191,B:255,A:255). + /// + public static Color DeepSkyBlue + { + get; + private set; + } + + /// + /// DimGray color (R:105,G:105,B:105,A:255). + /// + public static Color DimGray + { + get; + private set; + } + + /// + /// DodgerBlue color (R:30,G:144,B:255,A:255). + /// + public static Color DodgerBlue + { + get; + private set; + } + + /// + /// Firebrick color (R:178,G:34,B:34,A:255). + /// + public static Color Firebrick + { + get; + private set; + } + + /// + /// FloralWhite color (R:255,G:250,B:240,A:255). + /// + public static Color FloralWhite + { + get; + private set; + } + + /// + /// ForestGreen color (R:34,G:139,B:34,A:255). + /// + public static Color ForestGreen + { + get; + private set; + } + + /// + /// Fuchsia color (R:255,G:0,B:255,A:255). + /// + public static Color Fuchsia + { + get; + private set; + } + + /// + /// Gainsboro color (R:220,G:220,B:220,A:255). + /// + public static Color Gainsboro + { + get; + private set; + } + + /// + /// GhostWhite color (R:248,G:248,B:255,A:255). + /// + public static Color GhostWhite + { + get; + private set; + } + /// + /// Gold color (R:255,G:215,B:0,A:255). + /// + public static Color Gold + { + get; + private set; + } + + /// + /// Goldenrod color (R:218,G:165,B:32,A:255). + /// + public static Color Goldenrod + { + get; + private set; + } + + /// + /// Gray color (R:128,G:128,B:128,A:255). + /// + public static Color Gray + { + get; + private set; + } + + /// + /// Green color (R:0,G:128,B:0,A:255). + /// + public static Color Green + { + get; + private set; + } + + /// + /// GreenYellow color (R:173,G:255,B:47,A:255). + /// + public static Color GreenYellow + { + get; + private set; + } + + /// + /// Honeydew color (R:240,G:255,B:240,A:255). + /// + public static Color Honeydew + { + get; + private set; + } + + /// + /// HotPink color (R:255,G:105,B:180,A:255). + /// + public static Color HotPink + { + get; + private set; + } + + /// + /// IndianRed color (R:205,G:92,B:92,A:255). + /// + public static Color IndianRed + { + get; + private set; + } + + /// + /// Indigo color (R:75,G:0,B:130,A:255). + /// + public static Color Indigo + { + get; + private set; + } + + /// + /// Ivory color (R:255,G:255,B:240,A:255). + /// + public static Color Ivory + { + get; + private set; + } + + /// + /// Khaki color (R:240,G:230,B:140,A:255). + /// + public static Color Khaki + { + get; + private set; + } + + /// + /// Lavender color (R:230,G:230,B:250,A:255). + /// + public static Color Lavender + { + get; + private set; + } + + /// + /// LavenderBlush color (R:255,G:240,B:245,A:255). + /// + public static Color LavenderBlush + { + get; + private set; + } + + /// + /// LawnGreen color (R:124,G:252,B:0,A:255). + /// + public static Color LawnGreen + { + get; + private set; + } + + /// + /// LemonChiffon color (R:255,G:250,B:205,A:255). + /// + public static Color LemonChiffon + { + get; + private set; + } + + /// + /// LightBlue color (R:173,G:216,B:230,A:255). + /// + public static Color LightBlue + { + get; + private set; + } + + /// + /// LightCoral color (R:240,G:128,B:128,A:255). + /// + public static Color LightCoral + { + get; + private set; + } + + /// + /// LightCyan color (R:224,G:255,B:255,A:255). + /// + public static Color LightCyan + { + get; + private set; + } + + /// + /// LightGoldenrodYellow color (R:250,G:250,B:210,A:255). + /// + public static Color LightGoldenrodYellow + { + get; + private set; + } + + /// + /// LightGray color (R:211,G:211,B:211,A:255). + /// + public static Color LightGray + { + get; + private set; + } + + /// + /// LightGreen color (R:144,G:238,B:144,A:255). + /// + public static Color LightGreen + { + get; + private set; + } + + /// + /// LightPink color (R:255,G:182,B:193,A:255). + /// + public static Color LightPink + { + get; + private set; + } + + /// + /// LightSalmon color (R:255,G:160,B:122,A:255). + /// + public static Color LightSalmon + { + get; + private set; + } + + /// + /// LightSeaGreen color (R:32,G:178,B:170,A:255). + /// + public static Color LightSeaGreen + { + get; + private set; + } + + /// + /// LightSkyBlue color (R:135,G:206,B:250,A:255). + /// + public static Color LightSkyBlue + { + get; + private set; + } + + /// + /// LightSlateGray color (R:119,G:136,B:153,A:255). + /// + public static Color LightSlateGray + { + get; + private set; + } + + /// + /// LightSteelBlue color (R:176,G:196,B:222,A:255). + /// + public static Color LightSteelBlue + { + get; + private set; + } + + /// + /// LightYellow color (R:255,G:255,B:224,A:255). + /// + public static Color LightYellow + { + get; + private set; + } + + /// + /// Lime color (R:0,G:255,B:0,A:255). + /// + public static Color Lime + { + get; + private set; + } + + /// + /// LimeGreen color (R:50,G:205,B:50,A:255). + /// + public static Color LimeGreen + { + get; + private set; + } + + /// + /// Linen color (R:250,G:240,B:230,A:255). + /// + public static Color Linen + { + get; + private set; + } + + /// + /// Magenta color (R:255,G:0,B:255,A:255). + /// + public static Color Magenta + { + get; + private set; + } + + /// + /// Maroon color (R:128,G:0,B:0,A:255). + /// + public static Color Maroon + { + get; + private set; + } + + /// + /// MediumAquamarine color (R:102,G:205,B:170,A:255). + /// + public static Color MediumAquamarine + { + get; + private set; + } + + /// + /// MediumBlue color (R:0,G:0,B:205,A:255). + /// + public static Color MediumBlue + { + get; + private set; + } + + /// + /// MediumOrchid color (R:186,G:85,B:211,A:255). + /// + public static Color MediumOrchid + { + get; + private set; + } + + /// + /// MediumPurple color (R:147,G:112,B:219,A:255). + /// + public static Color MediumPurple + { + get; + private set; + } + + /// + /// MediumSeaGreen color (R:60,G:179,B:113,A:255). + /// + public static Color MediumSeaGreen + { + get; + private set; + } + + /// + /// MediumSlateBlue color (R:123,G:104,B:238,A:255). + /// + public static Color MediumSlateBlue + { + get; + private set; + } + + /// + /// MediumSpringGreen color (R:0,G:250,B:154,A:255). + /// + public static Color MediumSpringGreen + { + get; + private set; + } + + /// + /// MediumTurquoise color (R:72,G:209,B:204,A:255). + /// + public static Color MediumTurquoise + { + get; + private set; + } + + /// + /// MediumVioletRed color (R:199,G:21,B:133,A:255). + /// + public static Color MediumVioletRed + { + get; + private set; + } + + /// + /// MidnightBlue color (R:25,G:25,B:112,A:255). + /// + public static Color MidnightBlue + { + get; + private set; + } + + /// + /// MintCream color (R:245,G:255,B:250,A:255). + /// + public static Color MintCream + { + get; + private set; + } + + /// + /// MistyRose color (R:255,G:228,B:225,A:255). + /// + public static Color MistyRose + { + get; + private set; + } + + /// + /// Moccasin color (R:255,G:228,B:181,A:255). + /// + public static Color Moccasin + { + get; + private set; + } + + /// + /// MonoGame orange theme color (R:231,G:60,B:0,A:255). + /// + public static Color MonoGameOrange + { + get; + private set; + } + + /// + /// NavajoWhite color (R:255,G:222,B:173,A:255). + /// + public static Color NavajoWhite + { + get; + private set; + } + + /// + /// Navy color (R:0,G:0,B:128,A:255). + /// + public static Color Navy + { + get; + private set; + } + + /// + /// OldLace color (R:253,G:245,B:230,A:255). + /// + public static Color OldLace + { + get; + private set; + } + + /// + /// Olive color (R:128,G:128,B:0,A:255). + /// + public static Color Olive + { + get; + private set; + } + + /// + /// OliveDrab color (R:107,G:142,B:35,A:255). + /// + public static Color OliveDrab + { + get; + private set; + } + + /// + /// Orange color (R:255,G:165,B:0,A:255). + /// + public static Color Orange + { + get; + private set; + } + + /// + /// OrangeRed color (R:255,G:69,B:0,A:255). + /// + public static Color OrangeRed + { + get; + private set; + } + + /// + /// Orchid color (R:218,G:112,B:214,A:255). + /// + public static Color Orchid + { + get; + private set; + } + + /// + /// PaleGoldenrod color (R:238,G:232,B:170,A:255). + /// + public static Color PaleGoldenrod + { + get; + private set; + } + + /// + /// PaleGreen color (R:152,G:251,B:152,A:255). + /// + public static Color PaleGreen + { + get; + private set; + } + + /// + /// PaleTurquoise color (R:175,G:238,B:238,A:255). + /// + public static Color PaleTurquoise + { + get; + private set; + } + /// + /// PaleVioletRed color (R:219,G:112,B:147,A:255). + /// + public static Color PaleVioletRed + { + get; + private set; + } + + /// + /// PapayaWhip color (R:255,G:239,B:213,A:255). + /// + public static Color PapayaWhip + { + get; + private set; + } + + /// + /// PeachPuff color (R:255,G:218,B:185,A:255). + /// + public static Color PeachPuff + { + get; + private set; + } + + /// + /// Peru color (R:205,G:133,B:63,A:255). + /// + public static Color Peru + { + get; + private set; + } + + /// + /// Pink color (R:255,G:192,B:203,A:255). + /// + public static Color Pink + { + get; + private set; + } + + /// + /// Plum color (R:221,G:160,B:221,A:255). + /// + public static Color Plum + { + get; + private set; + } + + /// + /// PowderBlue color (R:176,G:224,B:230,A:255). + /// + public static Color PowderBlue + { + get; + private set; + } + + /// + /// Purple color (R:128,G:0,B:128,A:255). + /// + public static Color Purple + { + get; + private set; + } + + /// + /// Red color (R:255,G:0,B:0,A:255). + /// + public static Color Red + { + get; + private set; + } + + /// + /// RosyBrown color (R:188,G:143,B:143,A:255). + /// + public static Color RosyBrown + { + get; + private set; + } + + /// + /// RoyalBlue color (R:65,G:105,B:225,A:255). + /// + public static Color RoyalBlue + { + get; + private set; + } + + /// + /// SaddleBrown color (R:139,G:69,B:19,A:255). + /// + public static Color SaddleBrown + { + get; + private set; + } + + /// + /// Salmon color (R:250,G:128,B:114,A:255). + /// + public static Color Salmon + { + get; + private set; + } + + /// + /// SandyBrown color (R:244,G:164,B:96,A:255). + /// + public static Color SandyBrown + { + get; + private set; + } + + /// + /// SeaGreen color (R:46,G:139,B:87,A:255). + /// + public static Color SeaGreen + { + get; + private set; + } + + /// + /// SeaShell color (R:255,G:245,B:238,A:255). + /// + public static Color SeaShell + { + get; + private set; + } + + /// + /// Sienna color (R:160,G:82,B:45,A:255). + /// + public static Color Sienna + { + get; + private set; + } + + /// + /// Silver color (R:192,G:192,B:192,A:255). + /// + public static Color Silver + { + get; + private set; + } + + /// + /// SkyBlue color (R:135,G:206,B:235,A:255). + /// + public static Color SkyBlue + { + get; + private set; + } + + /// + /// SlateBlue color (R:106,G:90,B:205,A:255). + /// + public static Color SlateBlue + { + get; + private set; + } + + /// + /// SlateGray color (R:112,G:128,B:144,A:255). + /// + public static Color SlateGray + { + get; + private set; + } + + /// + /// Snow color (R:255,G:250,B:250,A:255). + /// + public static Color Snow + { + get; + private set; + } + + /// + /// SpringGreen color (R:0,G:255,B:127,A:255). + /// + public static Color SpringGreen + { + get; + private set; + } + + /// + /// SteelBlue color (R:70,G:130,B:180,A:255). + /// + public static Color SteelBlue + { + get; + private set; + } + + /// + /// Tan color (R:210,G:180,B:140,A:255). + /// + public static Color Tan + { + get; + private set; + } + + /// + /// Teal color (R:0,G:128,B:128,A:255). + /// + public static Color Teal + { + get; + private set; + } + + /// + /// Thistle color (R:216,G:191,B:216,A:255). + /// + public static Color Thistle + { + get; + private set; + } + + /// + /// Tomato color (R:255,G:99,B:71,A:255). + /// + public static Color Tomato + { + get; + private set; + } + + /// + /// Turquoise color (R:64,G:224,B:208,A:255). + /// + public static Color Turquoise + { + get; + private set; + } + + /// + /// Violet color (R:238,G:130,B:238,A:255). + /// + public static Color Violet + { + get; + private set; + } + + /// + /// Wheat color (R:245,G:222,B:179,A:255). + /// + public static Color Wheat + { + get; + private set; + } + + /// + /// White color (R:255,G:255,B:255,A:255). + /// + public static Color White + { + get; + private set; + } + + /// + /// WhiteSmoke color (R:245,G:245,B:245,A:255). + /// + public static Color WhiteSmoke + { + get; + private set; + } + + /// + /// Yellow color (R:255,G:255,B:0,A:255). + /// + public static Color Yellow + { + get; + private set; + } + + /// + /// YellowGreen color (R:154,G:205,B:50,A:255). + /// + public static Color YellowGreen + { + get; + private set; + } + #endregion + + /// + /// Performs linear interpolation of . + /// + /// Source . + /// Destination . + /// Interpolation factor. + /// Interpolated . + public static Color Lerp(Color value1, Color value2, Single amount) + { + amount = MathHelper.Clamp(amount, 0, 1); + return new Color( + (int)MathHelper.Lerp(value1.R, value2.R, amount), + (int)MathHelper.Lerp(value1.G, value2.G, amount), + (int)MathHelper.Lerp(value1.B, value2.B, amount), + (int)MathHelper.Lerp(value1.A, value2.A, amount) ); + } + + /// + /// should be used instead of this function. + /// + /// Interpolated . + [Obsolete("Color.Lerp should be used instead of this function.")] + public static Color LerpPrecise(Color value1, Color value2, Single amount) + { + amount = MathHelper.Clamp(amount, 0, 1); + return new Color( + (int)MathHelper.LerpPrecise(value1.R, value2.R, amount), + (int)MathHelper.LerpPrecise(value1.G, value2.G, amount), + (int)MathHelper.LerpPrecise(value1.B, value2.B, amount), + (int)MathHelper.LerpPrecise(value1.A, value2.A, amount)); + } + + /// + /// Multiply by value. + /// + /// Source . + /// Multiplicator. + /// Multiplication result. + public static Color Multiply(Color value, float scale) + { + return new Color((int)(value.R * scale), (int)(value.G * scale), (int)(value.B * scale), (int)(value.A * scale)); + } + + /// + /// Multiply by value. + /// + /// Source . + /// Multiplicator. + /// Multiplication result. + public static Color operator *(Color value, float scale) + { + return new Color((int)(value.R * scale), (int)(value.G * scale), (int)(value.B * scale), (int)(value.A * scale)); + } + + /// + /// Gets a representation for this object. + /// + /// A representation for this object. + public Vector3 ToVector3() + { + return new Vector3(R / 255.0f, G / 255.0f, B / 255.0f); + } + + /// + /// Gets a representation for this object. + /// + /// A representation for this object. + public Vector4 ToVector4() + { + return new Vector4(R / 255.0f, G / 255.0f, B / 255.0f, A / 255.0f); + } + + /// + /// Gets or sets packed value of this . + /// + [CLSCompliant(false)] + public UInt32 PackedValue + { + get { return _packedValue; } + set { _packedValue = value; } + } + + + internal string DebugDisplayString + { + get + { + return string.Concat( + this.R.ToString(), " ", + this.G.ToString(), " ", + this.B.ToString(), " ", + this.A.ToString() + ); + } + } + + + /// + /// Returns a representation of this in the format: + /// {R:[red] G:[green] B:[blue] A:[alpha]} + /// + /// representation of this . + public override string ToString () + { + StringBuilder sb = new StringBuilder(25); + sb.Append("{R:"); + sb.Append(R); + sb.Append(" G:"); + sb.Append(G); + sb.Append(" B:"); + sb.Append(B); + sb.Append(" A:"); + sb.Append(A); + sb.Append("}"); + return sb.ToString(); + } + + /// + /// Translate a non-premultipled alpha to a that contains premultiplied alpha. + /// + /// A representing color. + /// A which contains premultiplied alpha data. + public static Color FromNonPremultiplied(Vector4 vector) + { + return new Color(vector.X * vector.W, vector.Y * vector.W, vector.Z * vector.W, vector.W); + } + + /// + /// Translate a non-premultipled alpha to a that contains premultiplied alpha. + /// + /// Red component value. + /// Green component value. + /// Blue component value. + /// Alpha component value. + /// A which contains premultiplied alpha data. + public static Color FromNonPremultiplied(int r, int g, int b, int a) + { + return new Color(r * a / 255, g * a / 255, b * a / 255, a); + } + + #region IEquatable Members + + /// + /// Compares whether current instance is equal to specified . + /// + /// The to compare. + /// true if the instances are equal; false otherwise. + public bool Equals(Color other) + { + return this.PackedValue == other.PackedValue; + } + + #endregion + + /// + /// Deconstruction method for . + /// + /// + /// + /// + public void Deconstruct(out float r, out float g, out float b) + { + r = R; + g = G; + b = B; + } + + /// + /// Deconstruction method for with Alpha. + /// + /// + /// + /// + /// + public void Deconstruct(out float r, out float g, out float b, out float a) + { + r = R; + g = G; + b = B; + a = A; + } + } +} diff --git a/MonoGame.Framework/ContainmentType.cs b/MonoGame.Framework/ContainmentType.cs new file mode 100644 index 00000000000..f24039d46b4 --- /dev/null +++ b/MonoGame.Framework/ContainmentType.cs @@ -0,0 +1,25 @@ +// MIT License - Copyright (C) The Mono.Xna Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +namespace Microsoft.Xna.Framework +{ + /// + /// Defines how the bounding volumes intersects or contain one another. + /// + public enum ContainmentType + { + /// + /// Indicates that there is no overlap between two bounding volumes. + /// + Disjoint, + /// + /// Indicates that one bounding volume completely contains another volume. + /// + Contains, + /// + /// Indicates that bounding volumes partially overlap one another. + /// + Intersects + } +} \ No newline at end of file diff --git a/MonoGame.Framework/Content/ContentExtensions.cs b/MonoGame.Framework/Content/ContentExtensions.cs new file mode 100644 index 00000000000..1b6dac6e37f --- /dev/null +++ b/MonoGame.Framework/Content/ContentExtensions.cs @@ -0,0 +1,68 @@ +using System; +using System.Reflection; +using System.Linq; + +namespace Microsoft.Xna.Framework.Content +{ + internal static class ContentExtensions + { + public static ConstructorInfo GetDefaultConstructor(this Type type) + { +#if NET45 + var typeInfo = type.GetTypeInfo(); + var ctor = typeInfo.DeclaredConstructors.FirstOrDefault(c => !c.IsStatic && c.GetParameters().Length == 0); + return ctor; +#else + var attrs = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance; + return type.GetConstructor(attrs, null, new Type[0], null); +#endif + } + + public static PropertyInfo[] GetAllProperties(this Type type) + { + + // Sometimes, overridden properties of abstract classes can show up even with + // BindingFlags.DeclaredOnly is passed to GetProperties. Make sure that + // all properties in this list are defined in this class by comparing + // its get method with that of it's base class. If they're the same + // Then it's an overridden property. +#if NET45 + PropertyInfo[] infos= type.GetTypeInfo().DeclaredProperties.ToArray(); + var nonStaticPropertyInfos = from p in infos + where (p.GetMethod != null) && (!p.GetMethod.IsStatic) && + (p.GetMethod == p.GetMethod.GetRuntimeBaseDefinition()) + select p; + return nonStaticPropertyInfos.ToArray(); +#else + const BindingFlags attrs = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; + var allProps = type.GetProperties(attrs).ToList(); + var props = allProps.FindAll(p => p.GetGetMethod(true) != null && p.GetGetMethod(true) == p.GetGetMethod(true).GetBaseDefinition()).ToArray(); + return props; +#endif + } + + + public static FieldInfo[] GetAllFields(this Type type) + { +#if NET45 + FieldInfo[] fields= type.GetTypeInfo().DeclaredFields.ToArray(); + var nonStaticFields = from field in fields + where !field.IsStatic + select field; + return nonStaticFields.ToArray(); +#else + var attrs = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly; + return type.GetFields(attrs); +#endif + } + + public static bool IsClass(this Type type) + { +#if NET45 + return type.GetTypeInfo().IsClass; +#else + return type.IsClass; +#endif + } + } +} diff --git a/MonoGame.Framework/Content/ContentLoadException.cs b/MonoGame.Framework/Content/ContentLoadException.cs new file mode 100644 index 00000000000..5d10b2e6692 --- /dev/null +++ b/MonoGame.Framework/Content/ContentLoadException.cs @@ -0,0 +1,60 @@ +#region License +/* +Microsoft Public License (Ms-PL) +MonoGame - Copyright © 2009 The MonoGame Team + +All rights reserved. + +This license governs use of the accompanying software. If you use the software, you accept this license. If you do not +accept the license, do not use the software. + +1. Definitions +The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under +U.S. copyright law. + +A "contribution" is the original software, or any additions or changes to the software. +A "contributor" is any person that distributes its contribution under this license. +"Licensed patents" are a contributor's patent claims that read directly on its contribution. + +2. Grant of Rights +(A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. +(B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, +each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. + +3. Conditions and Limitations +(A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. +(B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, +your patent license from such contributor to the software ends automatically. +(C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution +notices that are present in the software. +(D) If you distribute any portion of the software in source code form, you may do so only under this license by including +a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object +code form, you may only do so under a license that complies with this license. +(E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees +or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent +permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular +purpose and non-infringement. +*/ +#endregion License + +using System; + +namespace Microsoft.Xna.Framework.Content +{ + public class ContentLoadException : Exception + { + public ContentLoadException() : base() + { + } + + public ContentLoadException(string message) : base(message) + { + } + + public ContentLoadException(string message, Exception innerException) : base(message,innerException) + { + } + } +} + diff --git a/MonoGame.Framework/Content/ContentManager.cs b/MonoGame.Framework/Content/ContentManager.cs new file mode 100644 index 00000000000..76c6afef451 --- /dev/null +++ b/MonoGame.Framework/Content/ContentManager.cs @@ -0,0 +1,510 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using MonoGame.Utilities; +using Microsoft.Xna.Framework.Graphics; +using System.Globalization; + +#if !WINDOWS_UAP +using Microsoft.Xna.Framework.Audio; +using Microsoft.Xna.Framework.Media; +#endif + +namespace Microsoft.Xna.Framework.Content +{ + public partial class ContentManager : IDisposable + { + const byte ContentCompressedLzx = 0x80; + const byte ContentCompressedLz4 = 0x40; + + private string _rootDirectory = string.Empty; + private IServiceProvider serviceProvider; + private IGraphicsDeviceService graphicsDeviceService; + private Dictionary loadedAssets = new Dictionary(StringComparer.OrdinalIgnoreCase); + private List disposableAssets = new List(); + private bool disposed; + private byte[] scratchBuffer; + + private static object ContentManagerLock = new object(); + private static List ContentManagers = new List(); + + private static readonly List targetPlatformIdentifiers = new List() + { + 'w', // Windows (XNA & DirectX) + 'x', // Xbox360 (XNA) + 'm', // WindowsPhone7.0 (XNA) + 'i', // iOS + 'a', // Android + 'd', // DesktopGL + 'X', // MacOSX + 'W', // WindowsStoreApp + 'n', // NativeClient + 'M', // WindowsPhone8 + 'r', // RaspberryPi + 'P', // PlayStation4 + 'v', // PSVita + 'O', // XboxOne + 'S', // Nintendo Switch + + // NOTE: There are additional idenfiers for consoles that + // are not defined in this repository. Be sure to ask the + // console port maintainers to ensure no collisions occur. + + + // Legacy identifiers... these could be reused in the + // future if we feel enough time has passed. + + 'p', // PlayStationMobile + 'g', // Windows (OpenGL) + 'l', // Linux + }; + + + static partial void PlatformStaticInit(); + + static ContentManager() + { + // Allow any per-platform static initialization to occur. + PlatformStaticInit(); + } + + private static void AddContentManager(ContentManager contentManager) + { + lock (ContentManagerLock) + { + // Check if the list contains this content manager already. Also take + // the opportunity to prune the list of any finalized content managers. + bool contains = false; + for (int i = ContentManagers.Count - 1; i >= 0; --i) + { + var contentRef = ContentManagers[i]; + if (ReferenceEquals(contentRef.Target, contentManager)) + contains = true; + if (!contentRef.IsAlive) + ContentManagers.RemoveAt(i); + } + if (!contains) + ContentManagers.Add(new WeakReference(contentManager)); + } + } + + private static void RemoveContentManager(ContentManager contentManager) + { + lock (ContentManagerLock) + { + // Check if the list contains this content manager and remove it. Also + // take the opportunity to prune the list of any finalized content managers. + for (int i = ContentManagers.Count - 1; i >= 0; --i) + { + var contentRef = ContentManagers[i]; + if (!contentRef.IsAlive || ReferenceEquals(contentRef.Target, contentManager)) + ContentManagers.RemoveAt(i); + } + } + } + + internal static void ReloadGraphicsContent() + { + lock (ContentManagerLock) + { + // Reload the graphic assets of each content manager. Also take the + // opportunity to prune the list of any finalized content managers. + for (int i = ContentManagers.Count - 1; i >= 0; --i) + { + var contentRef = ContentManagers[i]; + if (contentRef.IsAlive) + { + var contentManager = (ContentManager)contentRef.Target; + if (contentManager != null) + contentManager.ReloadGraphicsAssets(); + } + else + { + ContentManagers.RemoveAt(i); + } + } + } + } + + // Use C# destructor syntax for finalization code. + // This destructor will run only if the Dispose method + // does not get called. + // It gives your base class the opportunity to finalize. + // Do not provide destructors in types derived from this class. + ~ContentManager() + { + // Do not re-create Dispose clean-up code here. + // Calling Dispose(false) is optimal in terms of + // readability and maintainability. + Dispose(false); + } + + public ContentManager(IServiceProvider serviceProvider) + { + if (serviceProvider == null) + { + throw new ArgumentNullException("serviceProvider"); + } + this.serviceProvider = serviceProvider; + AddContentManager(this); + } + + public ContentManager(IServiceProvider serviceProvider, string rootDirectory) + { + if (serviceProvider == null) + { + throw new ArgumentNullException("serviceProvider"); + } + if (rootDirectory == null) + { + throw new ArgumentNullException("rootDirectory"); + } + this.RootDirectory = rootDirectory; + this.serviceProvider = serviceProvider; + AddContentManager(this); + } + + public void Dispose() + { + Dispose(true); + // Tell the garbage collector not to call the finalizer + // since all the cleanup will already be done. + GC.SuppressFinalize(this); + // Once disposed, content manager wont be used again + RemoveContentManager(this); + } + + // If disposing is true, it was called explicitly and we should dispose managed objects. + // If disposing is false, it was called by the finalizer and managed objects should not be disposed. + protected virtual void Dispose(bool disposing) + { + if (!disposed) + { + if (disposing) + { + Unload(); + } + + scratchBuffer = null; + disposed = true; + } + } + + public virtual T LoadLocalized (string assetName) + { + string [] cultureNames = + { + CultureInfo.CurrentCulture.Name, // eg. "en-US" + CultureInfo.CurrentCulture.TwoLetterISOLanguageName // eg. "en" + }; + + // Look first for a specialized language-country version of the asset, + // then if that fails, loop back around to see if we can find one that + // specifies just the language without the country part. + foreach (string cultureName in cultureNames) { + string localizedAssetName = assetName + '.' + cultureName; + + try { + return Load (localizedAssetName); + } catch (ContentLoadException) { } + } + + // If we didn't find any localized asset, fall back to the default name. + return Load (assetName); + } + + public virtual T Load(string assetName) + { + if (string.IsNullOrEmpty(assetName)) + { + throw new ArgumentNullException("assetName"); + } + if (disposed) + { + throw new ObjectDisposedException("ContentManager"); + } + + T result = default(T); + + // On some platforms, name and slash direction matter. + // We store the asset by a /-seperating key rather than how the + // path to the file was passed to us to avoid + // loading "content/asset1.xnb" and "content\\ASSET1.xnb" as if they were two + // different files. This matches stock XNA behavior. + // The dictionary will ignore case differences + var key = assetName.Replace('\\', '/'); + + // Check for a previously loaded asset first + object asset = null; + if (loadedAssets.TryGetValue(key, out asset)) + { + if (asset is T) + { + return (T)asset; + } + } + + // Load the asset. + result = ReadAsset(assetName, null); + + loadedAssets[key] = result; + return result; + } + + protected virtual Stream OpenStream(string assetName) + { + Stream stream; + try + { + var assetPath = Path.Combine(RootDirectory, assetName) + ".xnb"; + + // This is primarily for editor support. + // Setting the RootDirectory to an absolute path is useful in editor + // situations, but TitleContainer can ONLY be passed relative paths. +#if DESKTOPGL || WINDOWS + if (Path.IsPathRooted(assetPath)) + stream = File.OpenRead(assetPath); + else +#endif + stream = TitleContainer.OpenStream(assetPath); +#if ANDROID + // Read the asset into memory in one go. This results in a ~50% reduction + // in load times on Android due to slow Android asset streams. + MemoryStream memStream = new MemoryStream(); + stream.CopyTo(memStream); + memStream.Seek(0, SeekOrigin.Begin); + stream.Close(); + stream = memStream; +#endif + } + catch (FileNotFoundException fileNotFound) + { + throw new ContentLoadException("The content file was not found.", fileNotFound); + } +#if !WINDOWS_UAP + catch (DirectoryNotFoundException directoryNotFound) + { + throw new ContentLoadException("The directory was not found.", directoryNotFound); + } +#endif + catch (Exception exception) + { + throw new ContentLoadException("Opening stream error.", exception); + } + return stream; + } + + protected T ReadAsset(string assetName, Action recordDisposableObject) + { + if (string.IsNullOrEmpty(assetName)) + { + throw new ArgumentNullException("assetName"); + } + if (disposed) + { + throw new ObjectDisposedException("ContentManager"); + } + + string originalAssetName = assetName; + object result = null; + + if (this.graphicsDeviceService == null) + { + this.graphicsDeviceService = serviceProvider.GetService(typeof(IGraphicsDeviceService)) as IGraphicsDeviceService; + if (this.graphicsDeviceService == null) + { + throw new InvalidOperationException("No Graphics Device Service"); + } + } + + // Try to load as XNB file + var stream = OpenStream(assetName); + using (var xnbReader = new BinaryReader(stream)) + { + using (var reader = GetContentReaderFromXnb(assetName, stream, xnbReader, recordDisposableObject)) + { + result = reader.ReadAsset(); + if (result is GraphicsResource) + ((GraphicsResource)result).Name = originalAssetName; + } + } + + if (result == null) + throw new ContentLoadException("Could not load " + originalAssetName + " asset!"); + + return (T)result; + } + + private ContentReader GetContentReaderFromXnb(string originalAssetName, Stream stream, BinaryReader xnbReader, Action recordDisposableObject) + { + // The first 4 bytes should be the "XNB" header. i use that to detect an invalid file + byte x = xnbReader.ReadByte(); + byte n = xnbReader.ReadByte(); + byte b = xnbReader.ReadByte(); + byte platform = xnbReader.ReadByte(); + + if (x != 'X' || n != 'N' || b != 'B' || + !(targetPlatformIdentifiers.Contains((char)platform))) + { + throw new ContentLoadException("Asset does not appear to be a valid XNB file. Did you process your content for Windows?"); + } + + byte version = xnbReader.ReadByte(); + byte flags = xnbReader.ReadByte(); + + bool compressedLzx = (flags & ContentCompressedLzx) != 0; + bool compressedLz4 = (flags & ContentCompressedLz4) != 0; + if (version != 5 && version != 4) + { + throw new ContentLoadException("Invalid XNB version"); + } + + // The next int32 is the length of the XNB file + int xnbLength = xnbReader.ReadInt32(); + + Stream decompressedStream = null; + if (compressedLzx || compressedLz4) + { + // Decompress the xnb + int decompressedSize = xnbReader.ReadInt32(); + + if (compressedLzx) + { + int compressedSize = xnbLength - 14; + decompressedStream = new LzxDecoderStream(stream, decompressedSize, compressedSize); + } + else if (compressedLz4) + { + decompressedStream = new Lz4DecoderStream(stream); + } + } + else + { + decompressedStream = stream; + } + + var reader = new ContentReader(this, decompressedStream, this.graphicsDeviceService.GraphicsDevice, + originalAssetName, version, recordDisposableObject); + + return reader; + } + + internal void RecordDisposable(IDisposable disposable) + { + Debug.Assert(disposable != null, "The disposable is null!"); + + // Avoid recording disposable objects twice. ReloadAsset will try to record the disposables again. + // We don't know which asset recorded which disposable so just guard against storing multiple of the same instance. + if (!disposableAssets.Contains(disposable)) + disposableAssets.Add(disposable); + } + + /// + /// Virtual property to allow a derived ContentManager to have it's assets reloaded + /// + protected virtual Dictionary LoadedAssets + { + get { return loadedAssets; } + } + + protected virtual void ReloadGraphicsAssets() + { + foreach (var asset in LoadedAssets) + { + // This never executes as asset.Key is never null. This just forces the + // linker to include the ReloadAsset function when AOT compiled. + if (asset.Key == null) + ReloadAsset(asset.Key, Convert.ChangeType(asset.Value, asset.Value.GetType())); + + var methodInfo = ReflectionHelpers.GetMethodInfo(typeof(ContentManager), "ReloadAsset"); + var genericMethod = methodInfo.MakeGenericMethod(asset.Value.GetType()); + genericMethod.Invoke(this, new object[] { asset.Key, Convert.ChangeType(asset.Value, asset.Value.GetType()) }); + } + } + + protected virtual void ReloadAsset(string originalAssetName, T currentAsset) + { + string assetName = originalAssetName; + if (string.IsNullOrEmpty(assetName)) + { + throw new ArgumentNullException("assetName"); + } + if (disposed) + { + throw new ObjectDisposedException("ContentManager"); + } + + if (this.graphicsDeviceService == null) + { + this.graphicsDeviceService = serviceProvider.GetService(typeof(IGraphicsDeviceService)) as IGraphicsDeviceService; + if (this.graphicsDeviceService == null) + { + throw new InvalidOperationException("No Graphics Device Service"); + } + } + + var stream = OpenStream(assetName); + using (var xnbReader = new BinaryReader(stream)) + { + using (var reader = GetContentReaderFromXnb(assetName, stream, xnbReader, null)) + { + reader.ReadAsset(currentAsset); + } + } + } + + public virtual void Unload() + { + // Look for disposable assets. + foreach (var disposable in disposableAssets) + { + if (disposable != null) + disposable.Dispose(); + } + disposableAssets.Clear(); + loadedAssets.Clear(); + } + + public string RootDirectory + { + get + { + return _rootDirectory; + } + set + { + _rootDirectory = value; + } + } + + internal string RootDirectoryFullPath + { + get + { + return Path.Combine(TitleContainer.Location, RootDirectory); + } + } + + public IServiceProvider ServiceProvider + { + get + { + return this.serviceProvider; + } + } + + internal byte[] GetScratchBuffer(int size) + { + size = Math.Max(size, 1024 * 1024); + if (scratchBuffer == null || scratchBuffer.Length < size) + scratchBuffer = new byte[size]; + return scratchBuffer; + } + } +} diff --git a/MonoGame.Framework/Content/ContentReader.cs b/MonoGame.Framework/Content/ContentReader.cs new file mode 100644 index 00000000000..ced7817eadb --- /dev/null +++ b/MonoGame.Framework/Content/ContentReader.cs @@ -0,0 +1,308 @@ +// MIT License - Copyright (C) The Mono.Xna Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Utilities; + +namespace Microsoft.Xna.Framework.Content +{ + public sealed class ContentReader : BinaryReader + { + private ContentManager contentManager; + private Action recordDisposableObject; + private ContentTypeReaderManager typeReaderManager; + private GraphicsDevice graphicsDevice; + private string assetName; + private List>> sharedResourceFixups; + private ContentTypeReader[] typeReaders; + internal int version; + internal int sharedResourceCount; + + internal ContentTypeReader[] TypeReaders + { + get + { + return typeReaders; + } + } + + internal GraphicsDevice GraphicsDevice + { + get + { + return this.graphicsDevice; + } + } + + internal ContentReader(ContentManager manager, Stream stream, GraphicsDevice graphicsDevice, string assetName, int version, Action recordDisposableObject) + : base(stream) + { + this.graphicsDevice = graphicsDevice; + this.recordDisposableObject = recordDisposableObject; + this.contentManager = manager; + this.assetName = assetName; + this.version = version; + } + + public ContentManager ContentManager + { + get + { + return contentManager; + } + } + + public string AssetName + { + get + { + return assetName; + } + } + + internal object ReadAsset() + { + InitializeTypeReaders(); + + // Read primary object + object result = ReadObject(); + + // Read shared resources + ReadSharedResources(); + + return result; + } + + internal object ReadAsset(T existingInstance) + { + InitializeTypeReaders(); + + // Read primary object + object result = ReadObject(existingInstance); + + // Read shared resources + ReadSharedResources(); + + return result; + } + + internal void InitializeTypeReaders() + { + typeReaderManager = new ContentTypeReaderManager(); + typeReaders = typeReaderManager.LoadAssetReaders(this); + sharedResourceCount = Read7BitEncodedInt(); + sharedResourceFixups = new List>>(); + } + + internal void ReadSharedResources() + { + if (sharedResourceCount <= 0) + return; + + var sharedResources = new object[sharedResourceCount]; + for (var i = 0; i < sharedResourceCount; ++i) + sharedResources[i] = InnerReadObject(null); + + // Fixup shared resources by calling each registered action + foreach (var fixup in sharedResourceFixups) + fixup.Value(sharedResources[fixup.Key]); + } + + public T ReadExternalReference() + { + var externalReference = ReadString(); + + if (!String.IsNullOrEmpty(externalReference)) + { + return contentManager.Load(FileHelpers.ResolveRelativePath(assetName, externalReference)); + } + + return default(T); + } + + public Matrix ReadMatrix() + { + Matrix result = new Matrix(); + result.M11 = ReadSingle(); + result.M12 = ReadSingle(); + result.M13 = ReadSingle(); + result.M14 = ReadSingle(); + result.M21 = ReadSingle(); + result.M22 = ReadSingle(); + result.M23 = ReadSingle(); + result.M24 = ReadSingle(); + result.M31 = ReadSingle(); + result.M32 = ReadSingle(); + result.M33 = ReadSingle(); + result.M34 = ReadSingle(); + result.M41 = ReadSingle(); + result.M42 = ReadSingle(); + result.M43 = ReadSingle(); + result.M44 = ReadSingle(); + return result; + } + + private void RecordDisposable(T result) + { + var disposable = result as IDisposable; + if (disposable == null) + return; + + if (recordDisposableObject != null) + recordDisposableObject(disposable); + else + contentManager.RecordDisposable(disposable); + } + + public T ReadObject() + { + return InnerReadObject(default(T)); + } + + public T ReadObject(ContentTypeReader typeReader) + { + var result = (T)typeReader.Read(this, default(T)); + RecordDisposable(result); + return result; + } + + public T ReadObject(T existingInstance) + { + return InnerReadObject(existingInstance); + } + + private T InnerReadObject(T existingInstance) + { + var typeReaderIndex = Read7BitEncodedInt(); + if (typeReaderIndex == 0) + return existingInstance; + + if (typeReaderIndex > typeReaders.Length) + throw new ContentLoadException("Incorrect type reader index found!"); + + var typeReader = typeReaders[typeReaderIndex - 1]; + var result = (T)typeReader.Read(this, existingInstance); + + RecordDisposable(result); + + return result; + } + + public T ReadObject(ContentTypeReader typeReader, T existingInstance) + { + if (!ReflectionHelpers.IsValueType(typeReader.TargetType)) + return ReadObject(existingInstance); + + var result = (T)typeReader.Read(this, existingInstance); + + RecordDisposable(result); + + return result; + } + + public Quaternion ReadQuaternion() + { + Quaternion result = new Quaternion(); + result.X = ReadSingle(); + result.Y = ReadSingle(); + result.Z = ReadSingle(); + result.W = ReadSingle(); + return result; + } + + public T ReadRawObject() + { + return (T)ReadRawObject (default(T)); + } + + public T ReadRawObject(ContentTypeReader typeReader) + { + return (T)ReadRawObject(typeReader, default(T)); + } + + public T ReadRawObject(T existingInstance) + { + Type objectType = typeof(T); + foreach(ContentTypeReader typeReader in typeReaders) + { + if(typeReader.TargetType == objectType) + return (T)ReadRawObject(typeReader,existingInstance); + } + throw new NotSupportedException(); + } + + public T ReadRawObject(ContentTypeReader typeReader, T existingInstance) + { + return (T)typeReader.Read(this, existingInstance); + } + + public void ReadSharedResource(Action fixup) + { + int index = Read7BitEncodedInt(); + if (index > 0) + { + sharedResourceFixups.Add(new KeyValuePair>(index - 1, delegate(object v) + { + if (!(v is T)) + { + throw new ContentLoadException(String.Format("Error loading shared resource. Expected type {0}, received type {1}", typeof(T).Name, v.GetType().Name)); + } + fixup((T)v); + })); + } + } + + public Vector2 ReadVector2() + { + Vector2 result = new Vector2(); + result.X = ReadSingle(); + result.Y = ReadSingle(); + return result; + } + + public Vector3 ReadVector3() + { + Vector3 result = new Vector3(); + result.X = ReadSingle(); + result.Y = ReadSingle(); + result.Z = ReadSingle(); + return result; + } + + public Vector4 ReadVector4() + { + Vector4 result = new Vector4(); + result.X = ReadSingle(); + result.Y = ReadSingle(); + result.Z = ReadSingle(); + result.W = ReadSingle(); + return result; + } + + public Color ReadColor() + { + Color result = new Color(); + result.R = ReadByte(); + result.G = ReadByte(); + result.B = ReadByte(); + result.A = ReadByte(); + return result; + } + + internal new int Read7BitEncodedInt() + { + return base.Read7BitEncodedInt(); + } + + internal BoundingSphere ReadBoundingSphere() + { + var position = ReadVector3(); + var radius = ReadSingle(); + return new BoundingSphere(position, radius); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/AlphaTestEffectReader.cs b/MonoGame.Framework/Content/ContentReaders/AlphaTestEffectReader.cs new file mode 100644 index 00000000000..16fa4b28047 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/AlphaTestEffectReader.cs @@ -0,0 +1,24 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using Microsoft.Xna.Framework.Graphics; + +namespace Microsoft.Xna.Framework.Content +{ + class AlphaTestEffectReader : ContentTypeReader + { + protected internal override AlphaTestEffect Read(ContentReader input, AlphaTestEffect existingInstance) + { + var effect = new AlphaTestEffect(input.GraphicsDevice); + + effect.Texture = input.ReadExternalReference() as Texture2D; + effect.AlphaFunction = (CompareFunction)input.ReadInt32(); + effect.ReferenceAlpha = (int)input.ReadUInt32(); + effect.DiffuseColor = input.ReadVector3(); + effect.Alpha = input.ReadSingle(); + effect.VertexColorEnabled = input.ReadBoolean(); + return effect; + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/ArrayReader.cs b/MonoGame.Framework/Content/ContentReaders/ArrayReader.cs new file mode 100644 index 00000000000..82f3ef42e06 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/ArrayReader.cs @@ -0,0 +1,49 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using MonoGame.Utilities; + +namespace Microsoft.Xna.Framework.Content +{ + internal class ArrayReader : ContentTypeReader + { + ContentTypeReader elementReader; + + public ArrayReader() + { + } + + protected internal override void Initialize(ContentTypeReaderManager manager) + { + Type readerType = typeof(T); + elementReader = manager.GetTypeReader(readerType); + } + + protected internal override T[] Read(ContentReader input, T[] existingInstance) + { + uint count = input.ReadUInt32(); + T[] array = existingInstance; + if (array == null) + array = new T[count]; + + if (ReflectionHelpers.IsValueType(typeof(T))) + { + for (uint i = 0; i < count; i++) + { + array[i] = input.ReadObject(elementReader); + } + } + else + { + for (uint i = 0; i < count; i++) + { + var readerType = input.Read7BitEncodedInt(); + array[i] = readerType > 0 ? input.ReadObject(input.TypeReaders[readerType - 1]) : default(T); + } + } + return array; + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/BasicEffectReader.cs b/MonoGame.Framework/Content/ContentReaders/BasicEffectReader.cs new file mode 100644 index 00000000000..ee1327454aa --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/BasicEffectReader.cs @@ -0,0 +1,30 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using Microsoft.Xna.Framework.Graphics; + +namespace Microsoft.Xna.Framework.Content +{ + internal class BasicEffectReader : ContentTypeReader + { + protected internal override BasicEffect Read(ContentReader input, BasicEffect existingInstance) + { + var effect = new BasicEffect(input.GraphicsDevice); + var texture = input.ReadExternalReference() as Texture2D; + if (texture != null) + { + effect.Texture = texture; + effect.TextureEnabled = true; + } + + effect.DiffuseColor = input.ReadVector3(); + effect.EmissiveColor = input.ReadVector3(); + effect.SpecularColor = input.ReadVector3(); + effect.SpecularPower = input.ReadSingle(); + effect.Alpha = input.ReadSingle(); + effect.VertexColorEnabled = input.ReadBoolean(); + return effect; + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/BooleanReader.cs b/MonoGame.Framework/Content/ContentReaders/BooleanReader.cs new file mode 100644 index 00000000000..1c409b18ef7 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/BooleanReader.cs @@ -0,0 +1,25 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using System.Text; + +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; + +namespace Microsoft.Xna.Framework.Content +{ + internal class BooleanReader : ContentTypeReader + { + public BooleanReader() + { + } + + protected internal override bool Read(ContentReader input, bool existingInstance) + { + return input.ReadBoolean(); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/BoundingBoxReader.cs b/MonoGame.Framework/Content/ContentReaders/BoundingBoxReader.cs new file mode 100644 index 00000000000..befb6449e97 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/BoundingBoxReader.cs @@ -0,0 +1,17 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +namespace Microsoft.Xna.Framework.Content +{ + class BoundingBoxReader : ContentTypeReader + { + protected internal override BoundingBox Read(ContentReader input, BoundingBox existingInstance) + { + var min = input.ReadVector3(); + var max = input.ReadVector3(); + var result = new BoundingBox(min, max); + return result; + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/BoundingFrustumReader.cs b/MonoGame.Framework/Content/ContentReaders/BoundingFrustumReader.cs new file mode 100644 index 00000000000..a48046d92b1 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/BoundingFrustumReader.cs @@ -0,0 +1,21 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using Microsoft.Xna.Framework; + +namespace Microsoft.Xna.Framework.Content +{ + internal class BoundingFrustumReader : ContentTypeReader + { + public BoundingFrustumReader() + { + } + + protected internal override BoundingFrustum Read(ContentReader input, BoundingFrustum existingInstance) + { + return new BoundingFrustum(input.ReadMatrix()); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/BoundingSphereReader.cs b/MonoGame.Framework/Content/ContentReaders/BoundingSphereReader.cs new file mode 100644 index 00000000000..ed0845cb1cb --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/BoundingSphereReader.cs @@ -0,0 +1,23 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using Microsoft.Xna.Framework; + +namespace Microsoft.Xna.Framework.Content +{ + internal class BoundingSphereReader : ContentTypeReader + { + public BoundingSphereReader() + { + } + + protected internal override BoundingSphere Read(ContentReader input, BoundingSphere existingInstance) + { + Vector3 center = input.ReadVector3(); + float radius = input.ReadSingle(); + return new BoundingSphere(center, radius); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/ByteReader.cs b/MonoGame.Framework/Content/ContentReaders/ByteReader.cs new file mode 100644 index 00000000000..0ffb5cf94ad --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/ByteReader.cs @@ -0,0 +1,20 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Content +{ + internal class ByteReader : ContentTypeReader + { + public ByteReader() + { + } + + protected internal override byte Read(ContentReader input, byte existingInstance) + { + return input.ReadByte(); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/CharReader.cs b/MonoGame.Framework/Content/ContentReaders/CharReader.cs new file mode 100644 index 00000000000..12333c5732d --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/CharReader.cs @@ -0,0 +1,25 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using System.Text; + +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; + +namespace Microsoft.Xna.Framework.Content +{ + internal class CharReader : ContentTypeReader + { + public CharReader() + { + } + + protected internal override char Read(ContentReader input, char existingInstance) + { + return input.ReadChar(); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/ColorReader.cs b/MonoGame.Framework/Content/ContentReaders/ColorReader.cs new file mode 100644 index 00000000000..b29866a04d2 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/ColorReader.cs @@ -0,0 +1,28 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework; + +namespace Microsoft.Xna.Framework.Content +{ + internal class ColorReader : ContentTypeReader + { + public ColorReader () + { + } + + protected internal override Color Read (ContentReader input, Color existingInstance) + { + // Read RGBA as four separate bytes to make sure we comply with XNB format document + byte r = input.ReadByte(); + byte g = input.ReadByte(); + byte b = input.ReadByte(); + byte a = input.ReadByte(); + return new Color(r, g, b, a); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/CurveReader.cs b/MonoGame.Framework/Content/ContentReaders/CurveReader.cs new file mode 100644 index 00000000000..a6cc8077dee --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/CurveReader.cs @@ -0,0 +1,36 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Content +{ + internal class CurveReader : ContentTypeReader + { + protected internal override Curve Read(ContentReader input, Curve existingInstance) + { + Curve curve = existingInstance; + if (curve == null) + { + curve = new Curve(); + } + + curve.PreLoop = (CurveLoopType)input.ReadInt32(); + curve.PostLoop = (CurveLoopType)input.ReadInt32(); + int num6 = input.ReadInt32(); + + for (int i = 0; i < num6; i++) + { + float position = input.ReadSingle(); + float num4 = input.ReadSingle(); + float tangentIn = input.ReadSingle(); + float tangentOut = input.ReadSingle(); + CurveContinuity continuity = (CurveContinuity)input.ReadInt32(); + curve.Keys.Add(new CurveKey(position, num4, tangentIn, tangentOut, continuity)); + } + return curve; + } + } +} + diff --git a/MonoGame.Framework/Content/ContentReaders/DateTimeReader.cs b/MonoGame.Framework/Content/ContentReaders/DateTimeReader.cs new file mode 100644 index 00000000000..65cf1de1213 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/DateTimeReader.cs @@ -0,0 +1,24 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Content +{ + internal class DateTimeReader : ContentTypeReader + { + public DateTimeReader() + { + } + + protected internal override DateTime Read(ContentReader input, DateTime existingInstance) + { + UInt64 value = input.ReadUInt64(); + UInt64 mask = (UInt64)3 << 62; + long ticks = (long)(value & ~mask); + DateTimeKind kind = (DateTimeKind)((value >> 62) & 3); + return new DateTime(ticks, kind); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/DecimalReader.cs b/MonoGame.Framework/Content/ContentReaders/DecimalReader.cs new file mode 100644 index 00000000000..e2160f9bae4 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/DecimalReader.cs @@ -0,0 +1,20 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Content +{ + internal class DecimalReader : ContentTypeReader + { + public DecimalReader() + { + } + + protected internal override decimal Read(ContentReader input, decimal existingInstance) + { + return input.ReadDecimal(); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/DictionaryReader.cs b/MonoGame.Framework/Content/ContentReaders/DictionaryReader.cs new file mode 100644 index 00000000000..1a7f60a793f --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/DictionaryReader.cs @@ -0,0 +1,78 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using MonoGame.Utilities; + +namespace Microsoft.Xna.Framework.Content +{ + + internal class DictionaryReader : ContentTypeReader> + { + ContentTypeReader keyReader; + ContentTypeReader valueReader; + + Type keyType; + Type valueType; + + public DictionaryReader() + { + } + + protected internal override void Initialize(ContentTypeReaderManager manager) + { + keyType = typeof(TKey); + valueType = typeof(TValue); + + keyReader = manager.GetTypeReader(keyType); + valueReader = manager.GetTypeReader(valueType); + } + + public override bool CanDeserializeIntoExistingObject + { + get { return true; } + } + + protected internal override Dictionary Read(ContentReader input, Dictionary existingInstance) + { + int count = input.ReadInt32(); + Dictionary dictionary = existingInstance; + if (dictionary == null) + dictionary = new Dictionary(count); + else + dictionary.Clear(); + + for (int i = 0; i < count; i++) + { + TKey key; + TValue value; + + if (ReflectionHelpers.IsValueType(keyType)) + { + key = input.ReadObject(keyReader); + } + else + { + var readerType = input.Read7BitEncodedInt(); + key = readerType > 0 ? input.ReadObject(input.TypeReaders[readerType - 1]) : default(TKey); + } + + if (ReflectionHelpers.IsValueType(valueType)) + { + value = input.ReadObject(valueReader); + } + else + { + var readerType = input.Read7BitEncodedInt(); + value = readerType > 0 ? input.ReadObject(input.TypeReaders[readerType - 1]) : default(TValue); + } + + dictionary.Add(key, value); + } + return dictionary; + } + } +} + diff --git a/MonoGame.Framework/Content/ContentReaders/DoubleReader.cs b/MonoGame.Framework/Content/ContentReaders/DoubleReader.cs new file mode 100644 index 00000000000..cd89b5d126c --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/DoubleReader.cs @@ -0,0 +1,21 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Content +{ + internal class DoubleReader : ContentTypeReader + { + public DoubleReader() + { + } + + protected internal override double Read(ContentReader input, double existingInstance) + { + return input.ReadDouble(); + } + } +} + diff --git a/MonoGame.Framework/Content/ContentReaders/DualTextureEffectReader.cs b/MonoGame.Framework/Content/ContentReaders/DualTextureEffectReader.cs new file mode 100644 index 00000000000..c17f89694cb --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/DualTextureEffectReader.cs @@ -0,0 +1,25 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +using Microsoft.Xna.Framework.Graphics; + +namespace Microsoft.Xna.Framework.Content +{ + class DualTextureEffectReader : ContentTypeReader + { + protected internal override DualTextureEffect Read(ContentReader input, DualTextureEffect existingInstance) + { + DualTextureEffect effect = new DualTextureEffect(input.GraphicsDevice); + effect.Texture = input.ReadExternalReference() as Texture2D; + effect.Texture2 = input.ReadExternalReference() as Texture2D; + effect.DiffuseColor = input.ReadVector3 (); + effect.Alpha = input.ReadSingle (); + effect.VertexColorEnabled = input.ReadBoolean (); + return effect; + } + } +} + diff --git a/MonoGame.Framework/Content/ContentReaders/EffectMaterialReader.cs b/MonoGame.Framework/Content/ContentReaders/EffectMaterialReader.cs new file mode 100644 index 00000000000..3c84a91ec4d --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/EffectMaterialReader.cs @@ -0,0 +1,81 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.Xna.Framework.Graphics; +using MonoGame.Utilities; + +namespace Microsoft.Xna.Framework.Content +{ + internal class EffectMaterialReader : ContentTypeReader + { + protected internal override EffectMaterial Read (ContentReader input, EffectMaterial existingInstance) + { + var effect = input.ReadExternalReference (); + var effectMaterial = new EffectMaterial (effect); + + var dict = input.ReadObject> (); + + foreach (KeyValuePair item in dict) { + var parameter = effectMaterial.Parameters [item.Key]; + if (parameter != null) { + + Type itemType = item.Value.GetType(); + + if (ReflectionHelpers.IsAssignableFromType(typeof(Texture), itemType)) { + parameter.SetValue ((Texture)item.Value); + } + else if (ReflectionHelpers.IsAssignableFromType(typeof(int), itemType)) { + parameter.SetValue((int) item.Value); + } + else if (ReflectionHelpers.IsAssignableFromType(typeof(bool), itemType)) { + parameter.SetValue((bool) item.Value); + } + else if (ReflectionHelpers.IsAssignableFromType(typeof(float), itemType)) { + parameter.SetValue((float) item.Value); + } + else if (ReflectionHelpers.IsAssignableFromType(typeof(float []), itemType)) { + parameter.SetValue((float[]) item.Value); + } + else if (ReflectionHelpers.IsAssignableFromType(typeof(Vector2), itemType)) { + parameter.SetValue((Vector2) item.Value); + } + else if (ReflectionHelpers.IsAssignableFromType(typeof(Vector2 []), itemType)) { + parameter.SetValue((Vector2 []) item.Value); + } + else if (ReflectionHelpers.IsAssignableFromType(typeof(Vector3), itemType)) { + parameter.SetValue((Vector3) item.Value); + } + else if (ReflectionHelpers.IsAssignableFromType(typeof(Vector3 []), itemType)) { + parameter.SetValue((Vector3 []) item.Value); + } + else if (ReflectionHelpers.IsAssignableFromType(typeof(Vector4), itemType)) { + parameter.SetValue((Vector4) item.Value); + } + else if (ReflectionHelpers.IsAssignableFromType(typeof(Vector4 []), itemType)) { + parameter.SetValue((Vector4 []) item.Value); + } + else if (ReflectionHelpers.IsAssignableFromType(typeof(Matrix), itemType)) { + parameter.SetValue((Matrix) item.Value); + } + else if (ReflectionHelpers.IsAssignableFromType(typeof(Matrix []), itemType)) { + parameter.SetValue((Matrix[]) item.Value); + } + else if (ReflectionHelpers.IsAssignableFromType(typeof(Quaternion), itemType)) { + parameter.SetValue((Quaternion) item.Value); + } + else { + throw new NotSupportedException ("Parameter type is not supported"); + } + } else { + Debug.WriteLine ("No parameter " + item.Key); + } + } + + return effectMaterial; + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/EffectReader.cs b/MonoGame.Framework/Content/ContentReaders/EffectReader.cs new file mode 100644 index 00000000000..ef295c65c4f --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/EffectReader.cs @@ -0,0 +1,25 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using Microsoft.Xna.Framework.Graphics; + +namespace Microsoft.Xna.Framework.Content +{ + internal class EffectReader : ContentTypeReader + { + public EffectReader() + { + } + + protected internal override Effect Read(ContentReader input, Effect existingInstance) + { + int dataSize = input.ReadInt32(); + byte[] data = input.ContentManager.GetScratchBuffer(dataSize); + input.Read(data, 0, dataSize); + var effect = new Effect(input.GraphicsDevice, data, 0, dataSize); + effect.Name = input.AssetName; + return effect; + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/EnumReader.cs b/MonoGame.Framework/Content/ContentReaders/EnumReader.cs new file mode 100644 index 00000000000..ca7f91454dd --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/EnumReader.cs @@ -0,0 +1,29 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Content +{ + internal class EnumReader : ContentTypeReader + { + ContentTypeReader elementReader; + + public EnumReader() + { + } + + protected internal override void Initialize(ContentTypeReaderManager manager) + { + Type readerType = Enum.GetUnderlyingType(typeof(T)); + elementReader = manager.GetTypeReader(readerType); + } + + protected internal override T Read(ContentReader input, T existingInstance) + { + return input.ReadRawObject(elementReader); + } + } +} + diff --git a/MonoGame.Framework/Content/ContentReaders/EnvironmentMapEffectReader.cs b/MonoGame.Framework/Content/ContentReaders/EnvironmentMapEffectReader.cs new file mode 100644 index 00000000000..d7175cc68c6 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/EnvironmentMapEffectReader.cs @@ -0,0 +1,26 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using Microsoft.Xna.Framework.Graphics; + +namespace Microsoft.Xna.Framework.Content +{ + class EnvironmentMapEffectReader : ContentTypeReader + { + protected internal override EnvironmentMapEffect Read(ContentReader input, EnvironmentMapEffect existingInstance) + { + var effect = new EnvironmentMapEffect(input.GraphicsDevice); + effect.Texture = input.ReadExternalReference() as Texture2D; + effect.EnvironmentMap = input.ReadExternalReference() as TextureCube; + effect.EnvironmentMapAmount = input.ReadSingle (); + effect.EnvironmentMapSpecular = input.ReadVector3 (); + effect.FresnelFactor = input.ReadSingle (); + effect.DiffuseColor = input.ReadVector3 (); + effect.EmissiveColor = input.ReadVector3 (); + effect.Alpha = input.ReadSingle (); + return effect; + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/ExternalReferenceReader.cs b/MonoGame.Framework/Content/ContentReaders/ExternalReferenceReader.cs new file mode 100644 index 00000000000..68b8224d9fd --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/ExternalReferenceReader.cs @@ -0,0 +1,23 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +namespace Microsoft.Xna.Framework.Content +{ + /// + /// External reference reader, provided for compatibility with XNA Framework built content + /// + internal class ExternalReferenceReader : ContentTypeReader + { + public ExternalReferenceReader() + : base(null) + { + + } + + protected internal override object Read(ContentReader input, object existingInstance) + { + return input.ReadExternalReference(); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/IndexBufferReader.cs b/MonoGame.Framework/Content/ContentReaders/IndexBufferReader.cs new file mode 100644 index 00000000000..f5c8b21d9a1 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/IndexBufferReader.cs @@ -0,0 +1,31 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using Microsoft.Xna.Framework.Graphics; + +namespace Microsoft.Xna.Framework.Content +{ + class IndexBufferReader : ContentTypeReader + { + protected internal override IndexBuffer Read(ContentReader input, IndexBuffer existingInstance) + { + IndexBuffer indexBuffer = existingInstance; + + bool sixteenBits = input.ReadBoolean(); + int dataSize = input.ReadInt32(); + byte[] data = input.ContentManager.GetScratchBuffer(dataSize); + input.Read(data, 0, dataSize); + + if (indexBuffer == null) + { + indexBuffer = new IndexBuffer(input.GraphicsDevice, + sixteenBits ? IndexElementSize.SixteenBits : IndexElementSize.ThirtyTwoBits, + dataSize / (sixteenBits ? 2 : 4), BufferUsage.None); + } + + indexBuffer.SetData(data, 0, dataSize); + return indexBuffer; + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/Int16Reader.cs b/MonoGame.Framework/Content/ContentReaders/Int16Reader.cs new file mode 100644 index 00000000000..d6fce01deb9 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/Int16Reader.cs @@ -0,0 +1,22 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +using Microsoft.Xna.Framework.Content; + +namespace Microsoft.Xna.Framework.Content +{ + internal class Int16Reader : ContentTypeReader + { + public Int16Reader () + { + } + + protected internal override short Read (ContentReader input, short existingInstance) + { + return input.ReadInt16 (); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/Int32Reader.cs b/MonoGame.Framework/Content/ContentReaders/Int32Reader.cs new file mode 100644 index 00000000000..edaa846731a --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/Int32Reader.cs @@ -0,0 +1,20 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Content +{ + internal class Int32Reader : ContentTypeReader + { + public Int32Reader() + { + } + + protected internal override int Read(ContentReader input, int existingInstance) + { + return input.ReadInt32(); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/Int64Reader.cs b/MonoGame.Framework/Content/ContentReaders/Int64Reader.cs new file mode 100644 index 00000000000..3b2bb1135e3 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/Int64Reader.cs @@ -0,0 +1,22 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +using Microsoft.Xna.Framework.Content; + +namespace Microsoft.Xna.Framework.Content +{ + internal class Int64Reader : ContentTypeReader + { + public Int64Reader () + { + } + + protected internal override long Read (ContentReader input, long existingInstance) + { + return input.ReadInt64 (); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/ListReader.cs b/MonoGame.Framework/Content/ContentReaders/ListReader.cs new file mode 100644 index 00000000000..7293691d2c1 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/ListReader.cs @@ -0,0 +1,51 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using Microsoft.Xna.Framework.Content; +using MonoGame.Utilities; + +namespace Microsoft.Xna.Framework.Content +{ + internal class ListReader : ContentTypeReader> + { + ContentTypeReader elementReader; + + public ListReader() + { + } + + protected internal override void Initialize(ContentTypeReaderManager manager) + { + Type readerType = typeof(T); + elementReader = manager.GetTypeReader(readerType); + } + + public override bool CanDeserializeIntoExistingObject + { + get { return true; } + } + + protected internal override List Read(ContentReader input, List existingInstance) + { + int count = input.ReadInt32(); + List list = existingInstance; + if (list == null) list = new List(count); + for (int i = 0; i < count; i++) + { + if (ReflectionHelpers.IsValueType(typeof(T))) + { + list.Add(input.ReadObject(elementReader)); + } + else + { + var readerType = input.Read7BitEncodedInt(); + list.Add(readerType > 0 ? input.ReadObject(input.TypeReaders[readerType - 1]) : default(T)); + } + } + return list; + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/MatrixReader.cs b/MonoGame.Framework/Content/ContentReaders/MatrixReader.cs new file mode 100644 index 00000000000..54659e79b73 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/MatrixReader.cs @@ -0,0 +1,30 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +namespace Microsoft.Xna.Framework.Content +{ + class MatrixReader : ContentTypeReader + { + protected internal override Matrix Read(ContentReader input, Matrix existingInstance) + { + var m11 = input.ReadSingle(); + var m12 = input.ReadSingle(); + var m13 = input.ReadSingle(); + var m14 = input.ReadSingle(); + var m21 = input.ReadSingle(); + var m22 = input.ReadSingle(); + var m23 = input.ReadSingle(); + var m24 = input.ReadSingle(); + var m31 = input.ReadSingle(); + var m32 = input.ReadSingle(); + var m33 = input.ReadSingle(); + var m34 = input.ReadSingle(); + var m41 = input.ReadSingle(); + var m42 = input.ReadSingle(); + var m43 = input.ReadSingle(); + var m44 = input.ReadSingle(); + return new Matrix(m11, m12, m13, m14, m21, m22, m23, m24, m31, m32, m33, m34, m41, m42, m43, m44); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/ModelReader.cs b/MonoGame.Framework/Content/ContentReaders/ModelReader.cs new file mode 100644 index 00000000000..3441826259c --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/ModelReader.cs @@ -0,0 +1,199 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Diagnostics; +using Microsoft.Xna.Framework.Graphics; +using Microsoft.Xna.Framework.Content; +using System.Collections.Generic; + +namespace Microsoft.Xna.Framework.Content +{ + internal class ModelReader : ContentTypeReader + { +// List vertexBuffers = new List(); +// List indexBuffers = new List(); +// List effects = new List(); +// List sharedResources = new List(); + + public ModelReader () + { + } + + static int ReadBoneReference(ContentReader reader, uint boneCount) + { + uint boneId; + + // Read the bone ID, which may be encoded as either an 8 or 32 bit value. + if (boneCount < 255) + { + boneId = reader.ReadByte(); + } + else + { + boneId = reader.ReadUInt32(); + } + + // Print out the bone ID. + if (boneId != 0) + { + //Debug.WriteLine("bone #{0}", boneId - 1); + return (int)(boneId - 1); + } + else + { + //Debug.WriteLine("null"); + } + + return -1; + } + + protected internal override Model Read(ContentReader reader, Model existingInstance) + { + // Read the bone names and transforms. + uint boneCount = reader.ReadUInt32(); + //Debug.WriteLine("Bone count: {0}", boneCount); + + List bones = new List((int)boneCount); + + for (uint i = 0; i < boneCount; i++) + { + string name = reader.ReadObject(); + var matrix = reader.ReadMatrix(); + var bone = new ModelBone { Transform = matrix, Index = (int)i, Name = name }; + bones.Add(bone); + } + + // Read the bone hierarchy. + for (int i = 0; i < boneCount; i++) + { + var bone = bones[i]; + + //Debug.WriteLine("Bone {0} hierarchy:", i); + + // Read the parent bone reference. + //Debug.WriteLine("Parent: "); + var parentIndex = ReadBoneReference(reader, boneCount); + + if (parentIndex != -1) + { + bone.Parent = bones[parentIndex]; + } + + // Read the child bone references. + uint childCount = reader.ReadUInt32(); + + if (childCount != 0) + { + //Debug.WriteLine("Children:"); + + for (uint j = 0; j < childCount; j++) + { + var childIndex = ReadBoneReference(reader, boneCount); + if (childIndex != -1) + { + bone.AddChild(bones[childIndex]); + } + } + } + } + + List meshes = new List(); + + //// Read the mesh data. + int meshCount = reader.ReadInt32(); + //Debug.WriteLine("Mesh count: {0}", meshCount); + + for (int i = 0; i < meshCount; i++) + { + + //Debug.WriteLine("Mesh {0}", i); + string name = reader.ReadObject(); + var parentBoneIndex = ReadBoneReference(reader, boneCount); + var boundingSphere = reader.ReadBoundingSphere(); + + // Tag + var meshTag = reader.ReadObject(); + + // Read the mesh part data. + int partCount = reader.ReadInt32(); + //Debug.WriteLine("Mesh part count: {0}", partCount); + + List parts = new List(partCount); + + for (uint j = 0; j < partCount; j++) + { + ModelMeshPart part; + if (existingInstance != null) + part = existingInstance.Meshes[i].MeshParts[(int)j]; + else + part = new ModelMeshPart(); + + part.VertexOffset = reader.ReadInt32(); + part.NumVertices = reader.ReadInt32(); + part.StartIndex = reader.ReadInt32(); + part.PrimitiveCount = reader.ReadInt32(); + + // tag + part.Tag = reader.ReadObject(); + + parts.Add(part); + + int jj = (int)j; + reader.ReadSharedResource(delegate (VertexBuffer v) + { + parts[jj].VertexBuffer = v; + }); + reader.ReadSharedResource(delegate (IndexBuffer v) + { + parts[jj].IndexBuffer = v; + }); + reader.ReadSharedResource(delegate (Effect v) + { + parts[jj].Effect = v; + }); + + + } + + if (existingInstance != null) + continue; + + ModelMesh mesh = new ModelMesh(reader.GraphicsDevice, parts); + + // Tag reassignment + mesh.Tag = meshTag; + + mesh.Name = name; + mesh.ParentBone = bones[parentBoneIndex]; + mesh.ParentBone.AddMesh(mesh); + mesh.BoundingSphere = boundingSphere; + meshes.Add(mesh); + } + + if (existingInstance != null) + { + // Read past remaining data and return existing instance + ReadBoneReference(reader, boneCount); + reader.ReadObject(); + return existingInstance; + } + + // Read the final pieces of model data. + var rootBoneIndex = ReadBoneReference(reader, boneCount); + + Model model = new Model(reader.GraphicsDevice, bones, meshes); + + model.Root = bones[rootBoneIndex]; + + model.BuildHierarchy(); + + // Tag? + model.Tag = reader.ReadObject(); + + return model; + } + } +} + diff --git a/MonoGame.Framework/Content/ContentReaders/MultiArrayReader.cs b/MonoGame.Framework/Content/ContentReaders/MultiArrayReader.cs new file mode 100644 index 00000000000..9661a0d266f --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/MultiArrayReader.cs @@ -0,0 +1,83 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using MonoGame.Utilities; + +namespace Microsoft.Xna.Framework.Content +{ + internal class MultiArrayReader : ContentTypeReader + { + ContentTypeReader elementReader; + + public MultiArrayReader() { } + + protected internal override void Initialize(ContentTypeReaderManager manager) + { + Type readerType = typeof(T); + elementReader = manager.GetTypeReader(readerType); + } + + protected internal override Array Read(ContentReader input, Array existingInstance) + { + var rank = input.ReadInt32(); + if (rank < 1) + throw new RankException(); + + var dimensions = new int[rank]; + var count = 1; + for (int d = 0; d < dimensions.Length; d++) + count *= dimensions[d] = input.ReadInt32(); + + + var array = existingInstance; + if (array == null) + array = Array.CreateInstance(typeof(T), dimensions);//new T[count]; + else if (dimensions.Length != array.Rank) + throw new RankException("existingInstance"); + + var indices = new int[rank]; + + for (int i = 0; i < count; i++) + { + T value; + if (ReflectionHelpers.IsValueType(typeof(T))) + value = input.ReadObject(elementReader); + else + { + var readerType = input.Read7BitEncodedInt(); + if (readerType > 0) + value = input.ReadObject(input.TypeReaders[readerType - 1]); + else + value = default(T); + } + + CalcIndices(array, i, indices); + array.SetValue(value, indices); + } + + return array; + } + + static void CalcIndices(Array array, int index, int[] indices) + { + if (array.Rank != indices.Length) + throw new Exception("indices"); + + for (int d = 0; d < indices.Length; d++) + { + if (index == 0) + indices[d] = 0; + else + { + indices[d] = index % array.GetLength(d); + index /= array.GetLength(d); + } + } + + if (index != 0) + throw new ArgumentOutOfRangeException("index"); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/NullableReader.cs b/MonoGame.Framework/Content/ContentReaders/NullableReader.cs new file mode 100644 index 00000000000..dd2ffff90c6 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/NullableReader.cs @@ -0,0 +1,32 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Content +{ + internal class NullableReader : ContentTypeReader where T : struct + { + ContentTypeReader elementReader; + + public NullableReader() + { + } + + protected internal override void Initialize(ContentTypeReaderManager manager) + { + Type readerType = typeof(T); + elementReader = manager.GetTypeReader(readerType); + } + + protected internal override T? Read(ContentReader input, T? existingInstance) + { + if(input.ReadBoolean()) + return input.ReadObject(elementReader); + + return null; + } + } +} + diff --git a/MonoGame.Framework/Content/ContentReaders/PlaneReader.cs b/MonoGame.Framework/Content/ContentReaders/PlaneReader.cs new file mode 100644 index 00000000000..d901edd0e6c --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/PlaneReader.cs @@ -0,0 +1,22 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Content +{ + internal class PlaneReader : ContentTypeReader + { + public PlaneReader() + { + } + + protected internal override Plane Read(ContentReader input, Plane existingInstance) + { + existingInstance.Normal = input.ReadVector3(); + existingInstance.D = input.ReadSingle(); + return existingInstance; + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/PointReader.cs b/MonoGame.Framework/Content/ContentReaders/PointReader.cs new file mode 100644 index 00000000000..fde2a0cbd58 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/PointReader.cs @@ -0,0 +1,27 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using System.Text; + +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; + +namespace Microsoft.Xna.Framework.Content +{ + internal class PointReader : ContentTypeReader + { + public PointReader () + { + } + + protected internal override Point Read (ContentReader input, Point existingInstance) + { + int X = input.ReadInt32 (); + int Y = input.ReadInt32 (); + return new Point ( X, Y); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/QuaternionReader.cs b/MonoGame.Framework/Content/ContentReaders/QuaternionReader.cs new file mode 100644 index 00000000000..4479d14e186 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/QuaternionReader.cs @@ -0,0 +1,20 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Content +{ + internal class QuaternionReader : ContentTypeReader + { + public QuaternionReader() + { + } + + protected internal override Quaternion Read(ContentReader input, Quaternion existingInstance) + { + return input.ReadQuaternion(); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/RayReader.cs b/MonoGame.Framework/Content/ContentReaders/RayReader.cs new file mode 100644 index 00000000000..c7e0020c20f --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/RayReader.cs @@ -0,0 +1,23 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using Microsoft.Xna.Framework; + +namespace Microsoft.Xna.Framework.Content +{ + internal class RayReader : ContentTypeReader + { + public RayReader() + { + } + + protected internal override Ray Read(ContentReader input, Ray existingInstance) + { + Vector3 position = input.ReadVector3(); + Vector3 direction = input.ReadVector3(); + return new Ray(position, direction); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/RectangleReader.cs b/MonoGame.Framework/Content/ContentReaders/RectangleReader.cs new file mode 100644 index 00000000000..a09c4c36044 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/RectangleReader.cs @@ -0,0 +1,30 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using System.Text; + +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; + +namespace Microsoft.Xna.Framework.Content +{ + + internal class RectangleReader : ContentTypeReader + { + public RectangleReader() + { + } + + protected internal override Rectangle Read(ContentReader input, Rectangle existingInstance) + { + int left = input.ReadInt32(); + int top = input.ReadInt32(); + int width = input.ReadInt32(); + int height = input.ReadInt32(); + return new Rectangle(left, top, width, height); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/ReflectiveReader.cs b/MonoGame.Framework/Content/ContentReaders/ReflectiveReader.cs new file mode 100755 index 00000000000..6170463756c --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/ReflectiveReader.cs @@ -0,0 +1,191 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using MonoGame.Utilities; + +namespace Microsoft.Xna.Framework.Content +{ + internal class ReflectiveReader : ContentTypeReader + { + delegate void ReadElement(ContentReader input, object parent); + + private List _readers; + + private ConstructorInfo _constructor; + + private ContentTypeReader _baseTypeReader; + + + public ReflectiveReader() + : base(typeof(T)) + { + } + + public override bool CanDeserializeIntoExistingObject + { + get { return TargetType.IsClass(); } + } + + protected internal override void Initialize(ContentTypeReaderManager manager) + { + base.Initialize(manager); + + var baseType = ReflectionHelpers.GetBaseType(TargetType); + if (baseType != null && baseType != typeof(object)) + _baseTypeReader = manager.GetTypeReader(baseType); + + _constructor = TargetType.GetDefaultConstructor(); + + var properties = TargetType.GetAllProperties(); + var fields = TargetType.GetAllFields(); + _readers = new List(fields.Length + properties.Length); + + // Gather the properties. + foreach (var property in properties) + { + var read = GetElementReader(manager, property); + if (read != null) + _readers.Add(read); + } + + // Gather the fields. + foreach (var field in fields) + { + var read = GetElementReader(manager, field); + if (read != null) + _readers.Add(read); + } + } + + private static ReadElement GetElementReader(ContentTypeReaderManager manager, MemberInfo member) + { + var property = member as PropertyInfo; + var field = member as FieldInfo; + Debug.Assert(field != null || property != null); + + if (property != null) + { + // Properties must have at least a getter. + if (property.CanRead == false) + return null; + + // Skip over indexer properties. + if (property.GetIndexParameters().Any()) + return null; + } + + // Are we explicitly asked to ignore this item? + if (ReflectionHelpers.GetCustomAttribute(member) != null) + return null; + + var contentSerializerAttribute = ReflectionHelpers.GetCustomAttribute(member); + if (contentSerializerAttribute == null) + { + if (property != null) + { + // There is no ContentSerializerAttribute, so non-public + // properties cannot be deserialized. + if (!ReflectionHelpers.PropertyIsPublic(property)) + return null; + + // If the read-only property has a type reader, + // and CanDeserializeIntoExistingObject is true, + // then it is safe to deserialize into the existing object. + if (!property.CanWrite) + { + var typeReader = manager.GetTypeReader(property.PropertyType); + if (typeReader == null || !typeReader.CanDeserializeIntoExistingObject) + return null; + } + } + else + { + // There is no ContentSerializerAttribute, so non-public + // fields cannot be deserialized. + if (!field.IsPublic) + return null; + + // evolutional: Added check to skip initialise only fields + if (field.IsInitOnly) + return null; + } + } + + Action setter; + Type elementType; + if (property != null) + { + elementType = property.PropertyType; + if (property.CanWrite) + setter = (o, v) => property.SetValue(o, v, null); + else + setter = (o, v) => { }; + } + else + { + elementType = field.FieldType; + setter = field.SetValue; + } + + // Shared resources get special treatment. + if (contentSerializerAttribute != null && contentSerializerAttribute.SharedResource) + { + return (input, parent) => + { + Action action = value => setter(parent, value); + input.ReadSharedResource(action); + }; + } + + // We need to have a reader at this point. + var reader = manager.GetTypeReader(elementType); + if (reader == null) + if (elementType == typeof(System.Array)) + reader = new ArrayReader(); + else + throw new ContentLoadException(string.Format("Content reader could not be found for {0} type.", elementType.FullName)); + + // We use the construct delegate to pick the correct existing + // object to be the target of deserialization. + Func construct = parent => null; + if (property != null && !property.CanWrite) + construct = parent => property.GetValue(parent, null); + + return (input, parent) => + { + var existing = construct(parent); + var obj2 = input.ReadObject(reader, existing); + setter(parent, obj2); + }; + } + + protected internal override object Read(ContentReader input, object existingInstance) + { + T obj; + if (existingInstance != null) + obj = (T)existingInstance; + else + obj = (_constructor == null ? (T)Activator.CreateInstance(typeof(T)) : (T)_constructor.Invoke(null)); + + if(_baseTypeReader != null) + _baseTypeReader.Read(input, obj); + + // Box the type. + var boxed = (object)obj; + + foreach (var reader in _readers) + reader(input, boxed); + + // Unbox it... required for value types. + obj = (T)boxed; + + return obj; + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/SByteReader.cs b/MonoGame.Framework/Content/ContentReaders/SByteReader.cs new file mode 100644 index 00000000000..34416f870b6 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/SByteReader.cs @@ -0,0 +1,20 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Content +{ + internal class SByteReader : ContentTypeReader + { + public SByteReader() + { + } + + protected internal override sbyte Read(ContentReader input, sbyte existingInstance) + { + return input.ReadSByte(); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/SingleReader.cs b/MonoGame.Framework/Content/ContentReaders/SingleReader.cs new file mode 100644 index 00000000000..fd0845cb2be --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/SingleReader.cs @@ -0,0 +1,20 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Content +{ + internal class SingleReader : ContentTypeReader + { + public SingleReader() + { + } + + protected internal override float Read(ContentReader input, float existingInstance) + { + return input.ReadSingle(); + } + } +} \ No newline at end of file diff --git a/MonoGame.Framework/Content/ContentReaders/SkinnedEffectReader.cs b/MonoGame.Framework/Content/ContentReaders/SkinnedEffectReader.cs new file mode 100644 index 00000000000..70aa177b6f0 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/SkinnedEffectReader.cs @@ -0,0 +1,26 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +using Microsoft.Xna.Framework.Graphics; + +namespace Microsoft.Xna.Framework.Content +{ + class SkinnedEffectReader : ContentTypeReader + { + protected internal override SkinnedEffect Read(ContentReader input, SkinnedEffect existingInstance) + { + var effect = new SkinnedEffect(input.GraphicsDevice); + effect.Texture = input.ReadExternalReference () as Texture2D; + effect.WeightsPerVertex = input.ReadInt32 (); + effect.DiffuseColor = input.ReadVector3 (); + effect.EmissiveColor = input.ReadVector3 (); + effect.SpecularColor = input.ReadVector3 (); + effect.SpecularPower = input.ReadSingle (); + effect.Alpha = input.ReadSingle (); + return effect; + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/SongReader.cs b/MonoGame.Framework/Content/ContentReaders/SongReader.cs new file mode 100644 index 00000000000..535476d19a9 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/SongReader.cs @@ -0,0 +1,32 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.IO; +using Microsoft.Xna.Framework.Media; +using MonoGame.Utilities; + +namespace Microsoft.Xna.Framework.Content +{ + internal class SongReader : ContentTypeReader + { + protected internal override Song Read(ContentReader input, Song existingInstance) + { + var path = input.ReadString(); + + if (!String.IsNullOrEmpty(path)) + { + // Add the ContentManager's RootDirectory + var dirPath = Path.Combine(input.ContentManager.RootDirectoryFullPath, input.AssetName); + + // Resolve the relative path + path = FileHelpers.ResolveRelativePath(dirPath, path); + } + + var durationMs = input.ReadObject(); + + return new Song(path, durationMs); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/SoundEffectReader.cs b/MonoGame.Framework/Content/ContentReaders/SoundEffectReader.cs new file mode 100644 index 00000000000..19450e2cb3f --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/SoundEffectReader.cs @@ -0,0 +1,59 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using Microsoft.Xna.Framework.Audio; + + +namespace Microsoft.Xna.Framework.Content +{ + internal class SoundEffectReader : ContentTypeReader + { + protected internal override SoundEffect Read(ContentReader input, SoundEffect existingInstance) + { + // XNB format for SoundEffect... + // + // Byte [format size] Format WAVEFORMATEX structure + // UInt32 Data size + // Byte [data size] Data Audio waveform data + // Int32 Loop start In bytes (start must be format block aligned) + // Int32 Loop length In bytes (length must be format block aligned) + // Int32 Duration In milliseconds + + // The header containss the WAVEFORMATEX header structure + // defined as the following... + // + // WORD wFormatTag; // byte[0] +2 + // WORD nChannels; // byte[2] +2 + // DWORD nSamplesPerSec; // byte[4] +4 + // DWORD nAvgBytesPerSec; // byte[8] +4 + // WORD nBlockAlign; // byte[12] +2 + // WORD wBitsPerSample; // byte[14] +2 + // WORD cbSize; // byte[16] +2 + // + // We let the sound effect deal with parsing this based + // on what format the audio data actually is. + + var headerSize = input.ReadInt32(); + var header = input.ReadBytes(headerSize); + + // Read the audio data buffer. + var dataSize = input.ReadInt32(); + var data = input.ContentManager.GetScratchBuffer(dataSize); + input.Read(data, 0, dataSize); + + var loopStart = input.ReadInt32(); + var loopLength = input.ReadInt32(); + var durationMs = input.ReadInt32(); + + // Create the effect. + var effect = new SoundEffect(header, data, dataSize, durationMs, loopStart, loopLength); + + // Store the original asset name for debugging later. + effect.Name = input.AssetName; + + return effect; + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/SpriteFontReader.cs b/MonoGame.Framework/Content/ContentReaders/SpriteFontReader.cs new file mode 100644 index 00000000000..6ffb6f5a138 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/SpriteFontReader.cs @@ -0,0 +1,59 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; + +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; + +namespace Microsoft.Xna.Framework.Content +{ + internal class SpriteFontReader : ContentTypeReader + { + public SpriteFontReader() + { + } + + protected internal override SpriteFont Read(ContentReader input, SpriteFont existingInstance) + { + if (existingInstance != null) + { + // Read the texture into the existing texture instance + input.ReadObject(existingInstance.Texture); + + // discard the rest of the SpriteFont data as we are only reloading GPU resources for now + input.ReadObject>(); + input.ReadObject>(); + input.ReadObject>(); + input.ReadInt32(); + input.ReadSingle(); + input.ReadObject>(); + if (input.ReadBoolean()) + { + input.ReadChar(); + } + + return existingInstance; + } + else + { + // Create a fresh SpriteFont instance + Texture2D texture = input.ReadObject(); + List glyphs = input.ReadObject>(); + List cropping = input.ReadObject>(); + List charMap = input.ReadObject>(); + int lineSpacing = input.ReadInt32(); + float spacing = input.ReadSingle(); + List kerning = input.ReadObject>(); + char? defaultCharacter = null; + if (input.ReadBoolean()) + { + defaultCharacter = new char?(input.ReadChar()); + } + return new SpriteFont(texture, glyphs, cropping, charMap, lineSpacing, spacing, kerning, defaultCharacter); + } + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/StringReader.cs b/MonoGame.Framework/Content/ContentReaders/StringReader.cs new file mode 100644 index 00000000000..da72bd4fe76 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/StringReader.cs @@ -0,0 +1,22 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Xna.Framework.Content +{ + internal class StringReader : ContentTypeReader + { + public StringReader() + { + } + + protected internal override string Read(ContentReader input, string existingInstance) + { + return input.ReadString(); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/Texture2DReader.cs b/MonoGame.Framework/Content/ContentReaders/Texture2DReader.cs new file mode 100644 index 00000000000..4c12e4d0690 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/Texture2DReader.cs @@ -0,0 +1,181 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using Microsoft.Xna; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; + +namespace Microsoft.Xna.Framework.Content +{ + internal class Texture2DReader : ContentTypeReader + { + public Texture2DReader() + { + // Do nothing + } + + protected internal override Texture2D Read(ContentReader reader, Texture2D existingInstance) + { + Texture2D texture = null; + + var surfaceFormat = (SurfaceFormat)reader.ReadInt32(); + int width = reader.ReadInt32(); + int height = reader.ReadInt32(); + int levelCount = reader.ReadInt32(); + int levelCountOutput = levelCount; + + // If the system does not fully support Power of Two textures, + // skip any mip maps supplied with any non PoT textures. + if (levelCount > 1 && !reader.GraphicsDevice.GraphicsCapabilities.SupportsNonPowerOfTwo && + (!MathHelper.IsPowerOfTwo(width) || !MathHelper.IsPowerOfTwo(height))) + { + levelCountOutput = 1; + System.Diagnostics.Debug.WriteLine( + "Device does not support non Power of Two textures. Skipping mipmaps."); + } + + SurfaceFormat convertedFormat = surfaceFormat; + switch (surfaceFormat) + { + case SurfaceFormat.Dxt1: + case SurfaceFormat.Dxt1a: + if (!reader.GraphicsDevice.GraphicsCapabilities.SupportsDxt1) + convertedFormat = SurfaceFormat.Color; + break; + case SurfaceFormat.Dxt1SRgb: + if (!reader.GraphicsDevice.GraphicsCapabilities.SupportsDxt1) + convertedFormat = SurfaceFormat.ColorSRgb; + break; + case SurfaceFormat.Dxt3: + case SurfaceFormat.Dxt5: + if (!reader.GraphicsDevice.GraphicsCapabilities.SupportsS3tc) + convertedFormat = SurfaceFormat.Color; + break; + case SurfaceFormat.Dxt3SRgb: + case SurfaceFormat.Dxt5SRgb: + if (!reader.GraphicsDevice.GraphicsCapabilities.SupportsS3tc) + convertedFormat = SurfaceFormat.ColorSRgb; + break; + case SurfaceFormat.NormalizedByte4: + convertedFormat = SurfaceFormat.Color; + break; + } + + texture = existingInstance ?? new Texture2D(reader.GraphicsDevice, width, height, levelCountOutput > 1, convertedFormat); +#if OPENGL + Threading.BlockOnUIThread(() => + { +#endif + for (int level = 0; level < levelCount; level++) + { + var levelDataSizeInBytes = reader.ReadInt32(); + var levelData = reader.ContentManager.GetScratchBuffer(levelDataSizeInBytes); + reader.Read(levelData, 0, levelDataSizeInBytes); + int levelWidth = Math.Max(width >> level, 1); + int levelHeight = Math.Max(height >> level, 1); + + if (level >= levelCountOutput) + continue; + + //Convert the image data if required + switch (surfaceFormat) + { + case SurfaceFormat.Dxt1: + case SurfaceFormat.Dxt1SRgb: + case SurfaceFormat.Dxt1a: + if (!reader.GraphicsDevice.GraphicsCapabilities.SupportsDxt1 && convertedFormat == SurfaceFormat.Color) + { + levelData = DxtUtil.DecompressDxt1(levelData, levelWidth, levelHeight); + levelDataSizeInBytes = levelData.Length; + } + break; + case SurfaceFormat.Dxt3: + case SurfaceFormat.Dxt3SRgb: + if (!reader.GraphicsDevice.GraphicsCapabilities.SupportsS3tc) + if (!reader.GraphicsDevice.GraphicsCapabilities.SupportsS3tc && + convertedFormat == SurfaceFormat.Color) + { + levelData = DxtUtil.DecompressDxt3(levelData, levelWidth, levelHeight); + levelDataSizeInBytes = levelData.Length; + } + break; + case SurfaceFormat.Dxt5: + case SurfaceFormat.Dxt5SRgb: + if (!reader.GraphicsDevice.GraphicsCapabilities.SupportsS3tc) + if (!reader.GraphicsDevice.GraphicsCapabilities.SupportsS3tc && + convertedFormat == SurfaceFormat.Color) + { + levelData = DxtUtil.DecompressDxt5(levelData, levelWidth, levelHeight); + levelDataSizeInBytes = levelData.Length; + } + break; + case SurfaceFormat.Bgra5551: + { +#if OPENGL + // Shift the channels to suit OpenGL + int offset = 0; + for (int y = 0; y < levelHeight; y++) + { + for (int x = 0; x < levelWidth; x++) + { + ushort pixel = BitConverter.ToUInt16(levelData, offset); + pixel = (ushort)(((pixel & 0x7FFF) << 1) | ((pixel & 0x8000) >> 15)); + levelData[offset] = (byte)(pixel); + levelData[offset + 1] = (byte)(pixel >> 8); + offset += 2; + } + } +#endif + } + break; + case SurfaceFormat.Bgra4444: + { +#if OPENGL + // Shift the channels to suit OpenGL + int offset = 0; + for (int y = 0; y < levelHeight; y++) + { + for (int x = 0; x < levelWidth; x++) + { + ushort pixel = BitConverter.ToUInt16(levelData, offset); + pixel = (ushort)(((pixel & 0x0FFF) << 4) | ((pixel & 0xF000) >> 12)); + levelData[offset] = (byte)(pixel); + levelData[offset + 1] = (byte)(pixel >> 8); + offset += 2; + } + } +#endif + } + break; + case SurfaceFormat.NormalizedByte4: + { + int bytesPerPixel = surfaceFormat.GetSize(); + int pitch = levelWidth * bytesPerPixel; + for (int y = 0; y < levelHeight; y++) + { + for (int x = 0; x < levelWidth; x++) + { + int color = BitConverter.ToInt32(levelData, y * pitch + x * bytesPerPixel); + levelData[y * pitch + x * 4] = (byte)(((color >> 16) & 0xff)); //R:=W + levelData[y * pitch + x * 4 + 1] = (byte)(((color >> 8) & 0xff)); //G:=V + levelData[y * pitch + x * 4 + 2] = (byte)(((color) & 0xff)); //B:=U + levelData[y * pitch + x * 4 + 3] = (byte)(((color >> 24) & 0xff)); //A:=Q + } + } + } + break; + } + + texture.SetData(level, null, levelData, 0, levelDataSizeInBytes); + } +#if OPENGL + }); +#endif + + return texture; + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/Texture3DReader.cs b/MonoGame.Framework/Content/ContentReaders/Texture3DReader.cs new file mode 100644 index 00000000000..c5f11f5d8ad --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/Texture3DReader.cs @@ -0,0 +1,50 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using Microsoft.Xna.Framework.Graphics; + +namespace Microsoft.Xna.Framework.Content +{ + internal class Texture3DReader : ContentTypeReader + { + protected internal override Texture3D Read(ContentReader reader, Texture3D existingInstance) + { + Texture3D texture = null; + + SurfaceFormat format = (SurfaceFormat)reader.ReadInt32(); + int width = reader.ReadInt32(); + int height = reader.ReadInt32(); + int depth = reader.ReadInt32(); + int levelCount = reader.ReadInt32(); + + if (existingInstance == null) + texture = new Texture3D(reader.GraphicsDevice, width, height, depth, levelCount > 1, format); + else + texture = existingInstance; + +#if OPENGL + Threading.BlockOnUIThread(() => + { +#endif + for (int i = 0; i < levelCount; i++) + { + int dataSize = reader.ReadInt32(); + byte[] data = reader.ContentManager.GetScratchBuffer(dataSize); + reader.Read(data, 0, dataSize); + texture.SetData(i, 0, 0, width, height, 0, depth, data, 0, dataSize); + + // Calculate dimensions of next mip level. + width = Math.Max(width >> 1, 1); + height = Math.Max(height >> 1, 1); + depth = Math.Max(depth >> 1, 1); + } +#if OPENGL + }); +#endif + + return texture; + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/TextureCubeReader.cs b/MonoGame.Framework/Content/ContentReaders/TextureCubeReader.cs new file mode 100644 index 00000000000..30b626602db --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/TextureCubeReader.cs @@ -0,0 +1,47 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using Microsoft.Xna.Framework.Graphics; + +namespace Microsoft.Xna.Framework.Content +{ + internal class TextureCubeReader : ContentTypeReader + { + + protected internal override TextureCube Read(ContentReader reader, TextureCube existingInstance) + { + TextureCube textureCube = null; + + SurfaceFormat surfaceFormat = (SurfaceFormat)reader.ReadInt32(); + int size = reader.ReadInt32(); + int levels = reader.ReadInt32(); + + if (existingInstance == null) + textureCube = new TextureCube(reader.GraphicsDevice, size, levels > 1, surfaceFormat); + else + textureCube = existingInstance; + +#if OPENGL + Threading.BlockOnUIThread(() => + { +#endif + for (int face = 0; face < 6; face++) + { + for (int i = 0; i < levels; i++) + { + int faceSize = reader.ReadInt32(); + byte[] faceData = reader.ContentManager.GetScratchBuffer(faceSize); + reader.Read(faceData, 0, faceSize); + textureCube.SetData((CubeMapFace)face, i, null, faceData, 0, faceSize); + } + } +#if OPENGL + }); +#endif + + return textureCube; + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/TextureReader.cs b/MonoGame.Framework/Content/ContentReaders/TextureReader.cs new file mode 100644 index 00000000000..01bef184e30 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/TextureReader.cs @@ -0,0 +1,17 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using Microsoft.Xna.Framework.Graphics; + +namespace Microsoft.Xna.Framework.Content +{ + internal class TextureReader : ContentTypeReader + { + protected internal override Texture Read(ContentReader reader, Texture existingInstance) + { + return existingInstance; + } + } +} \ No newline at end of file diff --git a/MonoGame.Framework/Content/ContentReaders/TimeSpanReader.cs b/MonoGame.Framework/Content/ContentReaders/TimeSpanReader.cs new file mode 100644 index 00000000000..c5e28d97339 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/TimeSpanReader.cs @@ -0,0 +1,28 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +using Microsoft.Xna.Framework.Content; + +namespace Microsoft.Xna.Framework.Content +{ + internal class TimeSpanReader : ContentTypeReader + { + public TimeSpanReader () + { + } + + protected internal override TimeSpan Read (ContentReader input, TimeSpan existingInstance) + { + // Could not find any information on this really but from all the searching it looks + // like the constructor of number of ticks is long so I have placed that here for now + // long is a Int64 so we read with 64 + // PT2S + // + + return new TimeSpan(input.ReadInt64 ()); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/UInt16Reader.cs b/MonoGame.Framework/Content/ContentReaders/UInt16Reader.cs new file mode 100644 index 00000000000..6ba6ce6b6fe --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/UInt16Reader.cs @@ -0,0 +1,20 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Content +{ + internal class UInt16Reader : ContentTypeReader + { + public UInt16Reader() + { + } + + protected internal override ushort Read(ContentReader input, ushort existingInstance) + { + return input.ReadUInt16(); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/UInt32Reader.cs b/MonoGame.Framework/Content/ContentReaders/UInt32Reader.cs new file mode 100644 index 00000000000..27d01487af6 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/UInt32Reader.cs @@ -0,0 +1,20 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Content +{ + internal class UInt32Reader : ContentTypeReader + { + public UInt32Reader() + { + } + + protected internal override uint Read(ContentReader input, uint existingInstance) + { + return input.ReadUInt32(); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/UInt64Reader.cs b/MonoGame.Framework/Content/ContentReaders/UInt64Reader.cs new file mode 100644 index 00000000000..f4587196a98 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/UInt64Reader.cs @@ -0,0 +1,20 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Content +{ + internal class UInt64Reader : ContentTypeReader + { + public UInt64Reader() + { + } + + protected internal override ulong Read(ContentReader input, ulong existingInstance) + { + return input.ReadUInt64(); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/Vector2Reader.cs b/MonoGame.Framework/Content/ContentReaders/Vector2Reader.cs new file mode 100644 index 00000000000..93c914c36a9 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/Vector2Reader.cs @@ -0,0 +1,22 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +using Microsoft.Xna.Framework.Content; + +namespace Microsoft.Xna.Framework.Content +{ + internal class Vector2Reader : ContentTypeReader + { + public Vector2Reader () + { + } + + protected internal override Vector2 Read (ContentReader input, Vector2 existingInstance) + { + return input.ReadVector2 (); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/Vector3Reader.cs b/MonoGame.Framework/Content/ContentReaders/Vector3Reader.cs new file mode 100644 index 00000000000..acaac447d56 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/Vector3Reader.cs @@ -0,0 +1,22 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Xna.Framework.Content +{ + internal class Vector3Reader : ContentTypeReader + { + public Vector3Reader() + { + } + + protected internal override Vector3 Read(ContentReader input, Vector3 existingInstance) + { + return input.ReadVector3(); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/Vector4Reader.cs b/MonoGame.Framework/Content/ContentReaders/Vector4Reader.cs new file mode 100644 index 00000000000..2ae64e41449 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/Vector4Reader.cs @@ -0,0 +1,20 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; + +namespace Microsoft.Xna.Framework.Content +{ + internal class Vector4Reader : ContentTypeReader + { + public Vector4Reader() + { + } + + protected internal override Vector4 Read(ContentReader input, Vector4 existingInstance) + { + return input.ReadVector4(); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/VertexBufferReader.cs b/MonoGame.Framework/Content/ContentReaders/VertexBufferReader.cs new file mode 100644 index 00000000000..1a890ec63e8 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/VertexBufferReader.cs @@ -0,0 +1,25 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using Microsoft.Xna.Framework.Graphics; + +namespace Microsoft.Xna.Framework.Content +{ + class VertexBufferReader : ContentTypeReader + { + protected internal override VertexBuffer Read(ContentReader input, VertexBuffer existingInstance) + { + var declaration = input.ReadRawObject(); + var vertexCount = (int)input.ReadUInt32(); + int dataSize = vertexCount * declaration.VertexStride; + byte[] data = input.ContentManager.GetScratchBuffer(dataSize); + input.Read(data, 0, dataSize); + + var buffer = new VertexBuffer(input.GraphicsDevice, declaration, vertexCount, BufferUsage.None); + buffer.SetData(data, 0, dataSize); + return buffer; + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/VertexDeclarationReader.cs b/MonoGame.Framework/Content/ContentReaders/VertexDeclarationReader.cs new file mode 100644 index 00000000000..169a6601895 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/VertexDeclarationReader.cs @@ -0,0 +1,27 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using Microsoft.Xna.Framework.Graphics; +namespace Microsoft.Xna.Framework.Content +{ + internal class VertexDeclarationReader : ContentTypeReader + { + protected internal override VertexDeclaration Read(ContentReader reader, VertexDeclaration existingInstance) + { + var vertexStride = reader.ReadInt32(); + var elementCount = reader.ReadInt32(); + VertexElement[] elements = new VertexElement[elementCount]; + for (int i = 0; i < elementCount; ++i) + { + var offset = reader.ReadInt32(); + var elementFormat = (VertexElementFormat)reader.ReadInt32(); + var elementUsage = (VertexElementUsage)reader.ReadInt32(); + var usageIndex = reader.ReadInt32(); + elements[i] = new VertexElement(offset, elementFormat, elementUsage, usageIndex); + } + + return VertexDeclaration.GetOrCreate(vertexStride, elements); + } + } +} diff --git a/MonoGame.Framework/Content/ContentReaders/VideoReader.cs b/MonoGame.Framework/Content/ContentReaders/VideoReader.cs new file mode 100644 index 00000000000..941ca5d5983 --- /dev/null +++ b/MonoGame.Framework/Content/ContentReaders/VideoReader.cs @@ -0,0 +1,42 @@ +// MonoGame - Copyright (C) The MonoGame Team +// This file is subject to the terms and conditions defined in +// file 'LICENSE.txt', which is part of this source code package. + +using System; +using System.IO; +using Microsoft.Xna.Framework.Media; +using MonoGame.Utilities; + +namespace Microsoft.Xna.Framework.Content +{ + internal class VideoReader : ContentTypeReader