#region License /* FNA - XNA4 Reimplementation for Desktop Platforms * Copyright 2009-2020 Ethan Lee and the MonoGame Team * * Released under the Microsoft Public License. * See LICENSE for details. */ #endregion #region Using Statements using System; using System.IO; using System.Collections.Generic; using System.Runtime.InteropServices; #endregion namespace Microsoft.Xna.Framework.Audio { // http://msdn.microsoft.com/en-us/library/microsoft.xna.framework.audio.soundeffect.aspx public sealed class SoundEffect : IDisposable { #region Public Properties public TimeSpan Duration { get { return TimeSpan.FromSeconds( (double) handle.PlayLength / (double) format.nSamplesPerSec ); } } public bool IsDisposed { get; private set; } public string Name { get; set; } #endregion #region Public Static Properties public static float MasterVolume { get { float result; FAudio.FAudioVoice_GetVolume( Device().MasterVoice, out result ); return result; } set { FAudio.FAudioVoice_SetVolume( Device().MasterVoice, value, 0 ); } } public static float DistanceScale { get { return Device().CurveDistanceScaler; } set { if (value <= 0.0f) { throw new ArgumentOutOfRangeException("value <= 0.0f"); } Device().CurveDistanceScaler = value; } } public static float DopplerScale { get { return Device().DopplerScale; } set { if (value < 0.0f) { throw new ArgumentOutOfRangeException("value < 0.0f"); } Device().DopplerScale = value; } } public static float SpeedOfSound { get { return Device().SpeedOfSound; } set { FAudioContext dev = Device(); dev.SpeedOfSound = value; FAudio.F3DAudioInitialize( dev.DeviceDetails.OutputFormat.dwChannelMask, dev.SpeedOfSound, dev.Handle3D ); } } #endregion #region Internal Variables internal List Instances = new List(); internal FAudio.FAudioBuffer handle; internal FAudio.FAudioWaveFormatEx format; internal uint loopStart; internal uint loopLength; #endregion #region Public Constructors public SoundEffect( byte[] buffer, int sampleRate, AudioChannels channels ) : this( null, buffer, 0, buffer.Length, 1, (ushort) channels, (uint) sampleRate, (uint) (sampleRate * ((ushort) channels * 2)), (ushort) ((ushort) channels * 2), 16, 0, 0 ) { } public SoundEffect( byte[] buffer, int offset, int count, int sampleRate, AudioChannels channels, int loopStart, int loopLength ) : this( null, buffer, offset, count, 1, (ushort) channels, (uint) sampleRate, (uint) (sampleRate * ((ushort) channels * 2)), (ushort) ((ushort) channels * 2), 16, loopStart, loopLength ) { } #endregion #region Internal Constructor internal SoundEffect( string name, byte[] buffer, int offset, int count, ushort wFormatTag, ushort nChannels, uint nSamplesPerSec, uint nAvgBytesPerSec, ushort nBlockAlign, ushort wBitsPerSample, int loopStart, int loopLength ) { Device(); Name = name; this.loopStart = (uint) loopStart; this.loopLength = (uint) loopLength; /* Buffer format */ format = new FAudio.FAudioWaveFormatEx(); format.wFormatTag = wFormatTag; format.nChannels = nChannels; format.nSamplesPerSec = nSamplesPerSec; format.nAvgBytesPerSec = nAvgBytesPerSec; format.nBlockAlign = nBlockAlign; format.wBitsPerSample = wBitsPerSample; format.cbSize = 0; /* May be needed for ADPCM? */ /* Easy stuff */ handle = new FAudio.FAudioBuffer(); handle.Flags = FAudio.FAUDIO_END_OF_STREAM; handle.pContext = IntPtr.Zero; /* Buffer data */ handle.AudioBytes = (uint) count; handle.pAudioData = Marshal.AllocHGlobal(count); Marshal.Copy( buffer, offset, handle.pAudioData, count ); /* Play regions */ handle.PlayBegin = 0; if (wFormatTag == 1) { handle.PlayLength = (uint) ( count / nChannels / (wBitsPerSample / 8) ); } else if (wFormatTag == 2) { handle.PlayLength = (uint) ( count / nBlockAlign * (((nBlockAlign / nChannels) - 6) * 2) ); } /* Set by Instances! */ handle.LoopBegin = 0; handle.LoopLength = 0; handle.LoopCount = 0; } #endregion #region Destructor ~SoundEffect() { if (Instances.Count > 0) { // STOP LEAKING YOUR INSTANCES, ARGH GC.ReRegisterForFinalize(this); return; } Dispose(); } #endregion #region Public Methods public void Dispose() { if (!IsDisposed) { /* FIXME: Is it ironic that we're generating * garbage with ToArray while cleaning up after * the program's leaks? * -flibit */ foreach (WeakReference instance in Instances.ToArray()) { object target = instance.Target; if (target != null) { (target as IDisposable).Dispose(); } } Instances.Clear(); Marshal.FreeHGlobal(handle.pAudioData); IsDisposed = true; } } public bool Play() { return Play(1.0f, 0.0f, 0.0f); } public bool Play(float volume, float pitch, float pan) { SoundEffectInstance instance = new SoundEffectInstance(this); instance.Volume = volume; instance.Pitch = pitch; instance.Pan = pan; instance.Play(); if (instance.State != SoundState.Playing) { // Ran out of AL sources, probably. instance.Dispose(); return false; } return true; } public SoundEffectInstance CreateInstance() { return new SoundEffectInstance(this); } #endregion #region Public Static Methods public static TimeSpan GetSampleDuration( int sizeInBytes, int sampleRate, AudioChannels channels ) { sizeInBytes /= 2; // 16-bit PCM! int ms = (int) ( (sizeInBytes / (int) channels) / (sampleRate / 1000.0f) ); return new TimeSpan(0, 0, 0, 0, ms); } public static int GetSampleSizeInBytes( TimeSpan duration, int sampleRate, AudioChannels channels ) { return (int) ( duration.TotalSeconds * sampleRate * (int) channels * 2 // 16-bit PCM! ); } public static SoundEffect FromStream(Stream stream) { // Sample data byte[] data; // WaveFormatEx data ushort wFormatTag; ushort nChannels; uint nSamplesPerSec; uint nAvgBytesPerSec; ushort nBlockAlign; ushort wBitsPerSample; // ushort cbSize; using (BinaryReader reader = new BinaryReader(stream)) { // RIFF Signature string signature = new string(reader.ReadChars(4)); if (signature != "RIFF") { throw new NotSupportedException("Specified stream is not a wave file."); } reader.ReadUInt32(); // Riff Chunk Size string wformat = new string(reader.ReadChars(4)); if (wformat != "WAVE") { throw new NotSupportedException("Specified stream is not a wave file."); } // WAVE Header string format_signature = new string(reader.ReadChars(4)); while (format_signature != "fmt ") { reader.ReadBytes(reader.ReadInt32()); format_signature = new string(reader.ReadChars(4)); } int format_chunk_size = reader.ReadInt32(); wFormatTag = reader.ReadUInt16(); nChannels = reader.ReadUInt16(); nSamplesPerSec = reader.ReadUInt32(); nAvgBytesPerSec = reader.ReadUInt32(); nBlockAlign = reader.ReadUInt16(); wBitsPerSample = reader.ReadUInt16(); // Reads residual bytes if (format_chunk_size > 16) { reader.ReadBytes(format_chunk_size - 16); } // data Signature string data_signature = new string(reader.ReadChars(4)); while (data_signature.ToLowerInvariant() != "data") { reader.ReadBytes(reader.ReadInt32()); data_signature = new string(reader.ReadChars(4)); } if (data_signature != "data") { throw new NotSupportedException("Specified wave file is not supported."); } int waveDataLength = reader.ReadInt32(); data = reader.ReadBytes(waveDataLength); } return new SoundEffect( null, data, 0, data.Length, wFormatTag, nChannels, nSamplesPerSec, nAvgBytesPerSec, nBlockAlign, wBitsPerSample, 0, 0 ); } #endregion #region FAudio Context internal class FAudioContext { public static FAudioContext Context = null; public readonly IntPtr Handle; public readonly byte[] Handle3D; public readonly IntPtr MasterVoice; public readonly FAudio.FAudioDeviceDetails DeviceDetails; public float CurveDistanceScaler; public float DopplerScale; public float SpeedOfSound; public IntPtr ReverbVoice; private FAudio.FAudioVoiceSends reverbSends; private FAudioContext(IntPtr ctx, uint devices) { Handle = ctx; uint i; for (i = 0; i < devices; i += 1) { FAudio.FAudio_GetDeviceDetails( Handle, i, out DeviceDetails ); if ((DeviceDetails.Role & FAudio.FAudioDeviceRole.FAudioDefaultGameDevice) == FAudio.FAudioDeviceRole.FAudioDefaultGameDevice) { break; } } if (i == devices) { i = 0; /* Oh well. */ FAudio.FAudio_GetDeviceDetails( Handle, i, out DeviceDetails ); } if (FAudio.FAudio_CreateMasteringVoice( Handle, out MasterVoice, FAudio.FAUDIO_DEFAULT_CHANNELS, FAudio.FAUDIO_DEFAULT_SAMPLERATE, 0, i, IntPtr.Zero ) != 0) { FAudio.FAudio_Release(ctx); FNALoggerEXT.LogError( "Failed to create mastering voice!" ); return; } CurveDistanceScaler = 1.0f; DopplerScale = 1.0f; SpeedOfSound = 343.5f; Handle3D = new byte[FAudio.F3DAUDIO_HANDLE_BYTESIZE]; FAudio.F3DAudioInitialize( DeviceDetails.OutputFormat.dwChannelMask, SpeedOfSound, Handle3D ); Context = this; } public void Dispose() { if (ReverbVoice != IntPtr.Zero) { FAudio.FAudioVoice_DestroyVoice(ReverbVoice); ReverbVoice = IntPtr.Zero; Marshal.FreeHGlobal(reverbSends.pSends); } FAudio.FAudioVoice_DestroyVoice(MasterVoice); FAudio.FAudio_Release(Handle); Context = null; } public unsafe void AttachReverb(IntPtr voice) { // Only create a reverb voice if they ask for it! if (ReverbVoice == IntPtr.Zero) { IntPtr reverb; FAudio.FAudioCreateReverb(out reverb, 0); IntPtr chainPtr; chainPtr = Marshal.AllocHGlobal( Marshal.SizeOf(typeof(FAudio.FAudioEffectChain)) ); FAudio.FAudioEffectChain* reverbChain = (FAudio.FAudioEffectChain*) chainPtr; reverbChain->EffectCount = 1; reverbChain->pEffectDescriptors = Marshal.AllocHGlobal( Marshal.SizeOf(typeof(FAudio.FAudioEffectDescriptor)) ); FAudio.FAudioEffectDescriptor* reverbDesc = (FAudio.FAudioEffectDescriptor*) reverbChain->pEffectDescriptors; reverbDesc->InitialState = 1; reverbDesc->OutputChannels = (uint) ( (DeviceDetails.OutputFormat.Format.nChannels == 6) ? 6 : 1 ); reverbDesc->pEffect = reverb; FAudio.FAudio_CreateSubmixVoice( Handle, out ReverbVoice, 1, /* Reverb will be omnidirectional */ DeviceDetails.OutputFormat.Format.nSamplesPerSec, 0, 0, IntPtr.Zero, chainPtr ); FAudio.FAPOBase_Release(reverb); Marshal.FreeHGlobal(reverbChain->pEffectDescriptors); Marshal.FreeHGlobal(chainPtr); // Defaults based on FAUDIOFX_I3DL2_PRESET_GENERIC IntPtr rvbParamsPtr = Marshal.AllocHGlobal( Marshal.SizeOf(typeof(FAudio.FAudioFXReverbParameters)) ); FAudio.FAudioFXReverbParameters* rvbParams = (FAudio.FAudioFXReverbParameters*) rvbParamsPtr; rvbParams->WetDryMix = 100.0f; rvbParams->ReflectionsDelay = 7; rvbParams->ReverbDelay = 11; rvbParams->RearDelay = FAudio.FAUDIOFX_REVERB_DEFAULT_REAR_DELAY; rvbParams->PositionLeft = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION; rvbParams->PositionRight = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION; rvbParams->PositionMatrixLeft = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION_MATRIX; rvbParams->PositionMatrixRight = FAudio.FAUDIOFX_REVERB_DEFAULT_POSITION_MATRIX; rvbParams->EarlyDiffusion = 15; rvbParams->LateDiffusion = 15; rvbParams->LowEQGain = 8; rvbParams->LowEQCutoff = 4; rvbParams->HighEQGain = 8; rvbParams->HighEQCutoff = 6; rvbParams->RoomFilterFreq = 5000f; rvbParams->RoomFilterMain = -10f; rvbParams->RoomFilterHF = -1f; rvbParams->ReflectionsGain = -26.0200005f; rvbParams->ReverbGain = 10.0f; rvbParams->DecayTime = 1.49000001f; rvbParams->Density = 100.0f; rvbParams->RoomSize = FAudio.FAUDIOFX_REVERB_DEFAULT_ROOM_SIZE; FAudio.FAudioVoice_SetEffectParameters( ReverbVoice, 0, rvbParamsPtr, (uint) Marshal.SizeOf(typeof(FAudio.FAudioFXReverbParameters)), 0 ); Marshal.FreeHGlobal(rvbParamsPtr); reverbSends = new FAudio.FAudioVoiceSends(); reverbSends.SendCount = 2; reverbSends.pSends = Marshal.AllocHGlobal( 2 * Marshal.SizeOf(typeof(FAudio.FAudioSendDescriptor)) ); FAudio.FAudioSendDescriptor* sendDesc = (FAudio.FAudioSendDescriptor*) reverbSends.pSends; sendDesc[0].Flags = 0; sendDesc[0].pOutputVoice = MasterVoice; sendDesc[1].Flags = 0; sendDesc[1].pOutputVoice = ReverbVoice; } // Oh hey here's where we actually attach it FAudio.FAudioVoice_SetOutputVoices( voice, ref reverbSends ); } public static void Create() { IntPtr ctx; try { FAudio.FAudioCreate( out ctx, 0, FAudio.FAUDIO_DEFAULT_PROCESSOR ); } catch { /* FAudio is missing, bail! */ return; } uint devices; FAudio.FAudio_GetDeviceCount( ctx, out devices ); if (devices == 0) { /* No sound cards, bail! */ FAudio.FAudio_Release(ctx); return; } Context = new FAudioContext(ctx, devices); } } internal static FAudioContext Device() { if (FAudioContext.Context != null) { return FAudioContext.Context; } FAudioContext.Create(); if (FAudioContext.Context == null) { throw new NoAudioHardwareException(); } return FAudioContext.Context; } #endregion } }