namespace System.Media { using System; using System.IO; using System.ComponentModel; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Diagnostics; using System.Threading; using System.Net; using System.Globalization; using System.Security.Permissions; using System.Security; using System.Diagnostics.CodeAnalysis; /// [ Serializable, ToolboxItem(false), SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes"), // This is the first class added to System.Media namespace. SuppressMessage("Microsoft.Usage", "CA2240:ImplementISerializableCorrectly"), // vsw 427356 HostProtection(UI = true) ] public class SoundPlayer : Component, ISerializable { const int blockSize = 1024; const int defaultLoadTimeout = 10000;// 10 secs private Uri uri = null; private string soundLocation = String.Empty; private int loadTimeout = defaultLoadTimeout; private object tag = null; // used to lock all synchronous calls to the SoundPlayer object private ManualResetEvent semaphore = new ManualResetEvent(true); // the worker copyThread // we start the worker copyThread ONLY from entry points in the SoundPlayer API // we also set the tread to null only from the entry points in the SoundPlayer API private Thread copyThread = null; // local buffer information int currentPos = 0; private Stream stream = null; private bool isLoadCompleted = false; private Exception lastLoadException = null; private bool doesLoadAppearSynchronous = false; private byte[] streamData = null; private AsyncOperation asyncOperation = null; private readonly SendOrPostCallback loadAsyncOperationCompleted; // event private static readonly object EventLoadCompleted = new object(); private static readonly object EventSoundLocationChanged = new object(); private static readonly object EventStreamChanged = new object(); /// public SoundPlayer() { loadAsyncOperationCompleted = new SendOrPostCallback(LoadAsyncOperationCompleted); } /// public SoundPlayer(string soundLocation) : this() { if(soundLocation == null) { soundLocation = String.Empty; } SetupSoundLocation(soundLocation); } /// public SoundPlayer(Stream stream) : this() { this.stream = stream; } /** * Constructor used in deserialization */ /// [ SuppressMessage("Microsoft.Performance", "CA1808:AvoidCallsThatBoxValueTypes"), // SerializationInfo stores LoadTimeout as an object. SuppressMessage("Microsoft.Performance", "CA1801:AvoidUnusedParameters") // Serialization constructor needs a Context parameter. ] protected SoundPlayer(SerializationInfo serializationInfo, StreamingContext context) { foreach(SerializationEntry entry in serializationInfo) { switch (entry.Name) { case "SoundLocation" : SetupSoundLocation((string) entry.Value); break; case "Stream" : stream = (Stream) entry.Value; // when we deserialize a stream we have to reset its seek position // vsWhidbey 180361 if (stream.CanSeek) { stream.Seek(0, SeekOrigin.Begin); } break; case "LoadTimeout" : this.LoadTimeout = (int) entry.Value; break; } } } /// public int LoadTimeout { get { return loadTimeout; } set { if (value < 0) { throw new ArgumentOutOfRangeException("LoadTimeout", value, SR.GetString(SR.SoundAPILoadTimeout)); } loadTimeout = value; } } /// public string SoundLocation { get { if (uri != null && uri.IsFile) { FileIOPermission fiop = new FileIOPermission(PermissionState.None); fiop.AllFiles = FileIOPermissionAccess.PathDiscovery; fiop.Demand(); } return soundLocation; } set { if (value == null) value = String.Empty; if (soundLocation.Equals(value)) return; SetupSoundLocation(value); OnSoundLocationChanged(EventArgs.Empty); } } /// public Stream Stream { get { // if the path is set, we should return null // Path and Stream are mutually exclusive if (uri != null) return null; return this.stream; } set { if (stream == value) return; SetupStream(value); OnStreamChanged(EventArgs.Empty); } } /// public bool IsLoadCompleted { get { return isLoadCompleted; } } /// public object Tag { get { return tag; } set { tag = value; } } /// public void LoadAsync() { // if we have a file there is nothing to load - we just pass the file to the PlaySound function // if we have a stream, then we start loading the stream async // if (uri!= null && uri.IsFile){ Debug.Assert(stream == null, "we can't have a stream and a path at the same time"); isLoadCompleted = true; FileInfo fi = new FileInfo(uri.LocalPath); if (!fi.Exists) { throw new FileNotFoundException(SR.GetString(SR.SoundAPIFileDoesNotExist), this.soundLocation); } OnLoadCompleted(new AsyncCompletedEventArgs(null, false, null)); return; } // if we are actively loading, keep it running if (copyThread != null && copyThread.ThreadState == System.Threading.ThreadState.Running) { return; } isLoadCompleted = false; streamData = null; currentPos = 0; asyncOperation = AsyncOperationManager.CreateOperation(null); LoadStream(false); } private void LoadAsyncOperationCompleted(object arg) { OnLoadCompleted((AsyncCompletedEventArgs)arg); } // called for loading a stream synchronously // called either when the user is setting the path/stream and we are loading // or when loading took more time than the time out private void CleanupStreamData() { this.currentPos = 0; this.streamData = null; this.isLoadCompleted = false; this.lastLoadException = null; this.doesLoadAppearSynchronous = false; this.copyThread = null; this.semaphore.Set(); } /// public void Load() { // if we have a file there is nothing to load - we just pass the file to the PlaySound function // if we have a stream, then we start loading the stream [....] // if (uri != null && uri.IsFile){ Debug.Assert(stream == null, "we can't have a stream and a path at the same time"); FileInfo fi = new FileInfo(uri.LocalPath); if (!fi.Exists) { throw new FileNotFoundException(SR.GetString(SR.SoundAPIFileDoesNotExist), this.soundLocation); } isLoadCompleted = true; OnLoadCompleted(new AsyncCompletedEventArgs(null, false, null)); return; } LoadSync(); } [SuppressMessage("Microsoft.Security", "CA2103:ReviewImperativeSecurity")] // FileIOPermission based on URI path, but path isn't gonna change during scope of Demand private void LoadAndPlay(int flags) { // bug 16794: when the user does not specify a sound location nor a stream, play Beep if (String.IsNullOrEmpty(soundLocation) && stream == null) { SystemSounds.Beep.Play(); return; } if (uri != null && uri.IsFile) { // VSW 580992: With more than one thread, someone could call SoundPlayer::set_Location // between the time LoadAndPlay demands FileIO and the time it calls PlaySound under elevation. // // Another scenario is someone calling SoundPlayer::set_Location between the time // LoadAndPlay validates the sound file and the time it calls PlaySound. // The SoundPlayer will end up playing an un-validated sound file. // The solution is to store the uri.LocalPath on a local variable string localPath = uri.LocalPath; // request permission to read the file: // pass the full path to the FileIOPermission FileIOPermission perm = new FileIOPermission(FileIOPermissionAccess.Read, localPath); perm.Demand(); // play the path isLoadCompleted = true; System.Media.SoundPlayer.IntSecurity.SafeSubWindows.Demand(); System.ComponentModel.IntSecurity.UnmanagedCode.Assert(); // ValidateSoundFile calls into the MMIO API so we need UnmanagedCode permissions to do that. // And of course we need UnmanagedCode permissions to all Win32::PlaySound method. try { // don't use uri.AbsolutePath because that gives problems when there are whitespaces in file names ValidateSoundFile(localPath); UnsafeNativeMethods.PlaySound(localPath, IntPtr.Zero, NativeMethods.SND_NODEFAULT | flags); } finally { System.Security.CodeAccessPermission.RevertAssert(); } } else { LoadSync(); ValidateSoundData(streamData); System.Media.SoundPlayer.IntSecurity.SafeSubWindows.Demand(); System.ComponentModel.IntSecurity.UnmanagedCode.Assert(); try { UnsafeNativeMethods.PlaySound(streamData, IntPtr.Zero, NativeMethods.SND_MEMORY | NativeMethods.SND_NODEFAULT | flags); } finally { System.Security.CodeAccessPermission.RevertAssert(); } } } [SuppressMessage("Microsoft.Security", "CA2103:ReviewImperativeSecurity")] // WebPermission based on URI path, but path isn't gonna change during scope of Demand private void LoadSync() { Debug.Assert((uri == null || !uri.IsFile), "we only load streams"); // first make sure that any possible download ended if (!semaphore.WaitOne(LoadTimeout, false)) { if (copyThread != null) copyThread.Abort(); CleanupStreamData(); throw new TimeoutException(SR.GetString(SR.SoundAPILoadTimedOut)); } // if we have data, then we are done if (streamData != null) return; // setup the http stream if (uri != null && !uri.IsFile && stream == null) { WebPermission webPerm = new WebPermission(NetworkAccess.Connect, uri.AbsolutePath); webPerm.Demand(); WebRequest webRequest = WebRequest.Create(uri); webRequest.Timeout = LoadTimeout; WebResponse webResponse; webResponse = webRequest.GetResponse(); // now get the stream stream = webResponse.GetResponseStream(); } if (stream.CanSeek) { // if we can get data synchronously, then get it LoadStream(true); } else { // the data can't be loaded synchronously // load it async, then wait for it to finish doesLoadAppearSynchronous = true; // to avoid OnFailed call. LoadStream(false); if(!semaphore.WaitOne(LoadTimeout, false)) { if (copyThread != null) copyThread.Abort(); CleanupStreamData(); throw new TimeoutException(SR.GetString(SR.SoundAPILoadTimedOut)); } doesLoadAppearSynchronous = false; if (lastLoadException != null) { throw lastLoadException; } } // we don't need the worker copyThread anymore this.copyThread = null; } private void LoadStream(bool loadSync) { if (loadSync && stream.CanSeek) { int streamLen = (int) stream.Length; currentPos = 0; streamData = new byte[streamLen]; stream.Read(streamData, 0, streamLen); isLoadCompleted = true; OnLoadCompleted(new AsyncCompletedEventArgs(null, false, null)); } else { // lock any synchronous calls on the Sound object semaphore.Reset(); // start loading copyThread = new Thread(new ThreadStart(this.WorkerThread)); copyThread.Start(); } } /// public void Play() { LoadAndPlay(NativeMethods.SND_ASYNC); } /// public void PlaySync() { LoadAndPlay(NativeMethods.SND_SYNC); } /// public void PlayLooping() { LoadAndPlay(NativeMethods.SND_LOOP | NativeMethods.SND_ASYNC); } private static Uri ResolveUri(string partialUri) { Uri result = null; try { result = new Uri(partialUri); } catch (UriFormatException) { // eat URI parse exceptions } if (result == null) { // try relative to appbase try { result = new Uri(Path.GetFullPath(partialUri)); } catch (UriFormatException) { // eat URI parse exceptions } } return result; } private void SetupSoundLocation(string soundLocation) { // if we are loading a file, stop it right now // if (copyThread != null) { copyThread.Abort(); CleanupStreamData(); } uri = ResolveUri(soundLocation); this.soundLocation = soundLocation; stream = null; if (uri == null) { if (!String.IsNullOrEmpty(soundLocation)) throw new UriFormatException(SR.GetString(SR.SoundAPIBadSoundLocation)); } else { if (!uri.IsFile) { // we are referencing a web resource ... // // we treat it as a stream... // streamData = null; currentPos = 0; isLoadCompleted = false; } } } private void SetupStream(Stream stream) { if (this.copyThread != null) { copyThread.Abort(); CleanupStreamData(); } this.stream = stream; this.soundLocation = String.Empty; this.streamData = null; this.currentPos = 0; isLoadCompleted = false; if (stream != null) { uri = null; } } /// [ResourceExposure(ResourceScope.None)] [ResourceConsumption(ResourceScope.Machine, ResourceScope.Machine)] public void Stop() { IntSecurity.SafeSubWindows.Demand(); UnsafeNativeMethods.PlaySound((byte[]) null, IntPtr.Zero, NativeMethods.SND_PURGE); } /// public event AsyncCompletedEventHandler LoadCompleted { add { Events.AddHandler(EventLoadCompleted, value); } remove { Events.RemoveHandler(EventLoadCompleted, value); } } /// public event EventHandler SoundLocationChanged { add { Events.AddHandler(EventSoundLocationChanged, value); } remove { Events.RemoveHandler(EventSoundLocationChanged, value); } } /// public event EventHandler StreamChanged { add { Events.AddHandler(EventStreamChanged, value); } remove { Events.RemoveHandler(EventStreamChanged, value); } } /// protected virtual void OnLoadCompleted(AsyncCompletedEventArgs e) { AsyncCompletedEventHandler eh = (AsyncCompletedEventHandler) Events[EventLoadCompleted]; if (eh != null) { eh(this, e); } } /// protected virtual void OnSoundLocationChanged(EventArgs e) { EventHandler eh = (EventHandler) Events[EventSoundLocationChanged]; if (eh != null) { eh(this, e); } } /// protected virtual void OnStreamChanged(EventArgs e) { EventHandler eh = (EventHandler) Events[EventStreamChanged]; if (eh != null) { eh(this, e); } } [ SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes") // The set of reasons why WorkerThread should fail is not finite ] private void WorkerThread() { try { // setup the http stream if (uri != null && !uri.IsFile && stream == null) { WebRequest webRequest = WebRequest.Create(uri); WebResponse webResponse = webRequest.GetResponse(); stream = webResponse.GetResponseStream(); } this.streamData = new byte[blockSize]; int readBytes = stream.Read(streamData, currentPos, blockSize); int totalBytes = readBytes; while (readBytes > 0) { currentPos += readBytes; if (streamData.Length < currentPos + blockSize) { byte[] newData = new byte[streamData.Length * 2]; Array.Copy(streamData, newData, streamData.Length); streamData = newData; } readBytes = stream.Read(streamData, currentPos, blockSize); totalBytes += readBytes; } lastLoadException = null; } catch (Exception exception) { lastLoadException = exception; } if (!doesLoadAppearSynchronous) { // Post notification back to the UI thread. asyncOperation.PostOperationCompleted( loadAsyncOperationCompleted, new AsyncCompletedEventArgs(lastLoadException, false, null)); } isLoadCompleted = true; semaphore.Set(); } private unsafe void ValidateSoundFile(string fileName) { NativeMethods.MMCKINFO ckRIFF = new NativeMethods.MMCKINFO(); NativeMethods.MMCKINFO ck = new NativeMethods.MMCKINFO(); NativeMethods.WAVEFORMATEX waveFormat = null; int dw; IntPtr hMIO = UnsafeNativeMethods.mmioOpen(fileName, IntPtr.Zero, NativeMethods.MMIO_READ | NativeMethods.MMIO_ALLOCBUF); if (hMIO == IntPtr.Zero) throw new FileNotFoundException(SR.GetString(SR.SoundAPIFileDoesNotExist), this.soundLocation); try { ckRIFF.fccType = mmioFOURCC('W', 'A','V','E'); if (UnsafeNativeMethods.mmioDescend(hMIO, ckRIFF, null, NativeMethods.MMIO_FINDRIFF) != 0) throw new InvalidOperationException(SR.GetString(SR.SoundAPIInvalidWaveFile, this.soundLocation)); while (UnsafeNativeMethods.mmioDescend(hMIO, ck, ckRIFF, 0) == 0) { if (ck.dwDataOffset + ck.cksize > ckRIFF.dwDataOffset + ckRIFF.cksize) throw new InvalidOperationException(SR.GetString(SR.SoundAPIInvalidWaveHeader)); if (ck.ckID == mmioFOURCC('f','m','t',' ')) { if (waveFormat == null) { dw = ck.cksize; if (dw < Marshal.SizeOf(typeof(NativeMethods.WAVEFORMATEX))) dw = Marshal.SizeOf(typeof(NativeMethods.WAVEFORMATEX)); waveFormat = new NativeMethods.WAVEFORMATEX(); byte[] data = new byte[dw]; if (UnsafeNativeMethods.mmioRead(hMIO, data, dw) != dw) throw new InvalidOperationException(SR.GetString(SR.SoundAPIReadError, this.soundLocation)); fixed(byte* pdata = data) { Marshal.PtrToStructure((IntPtr) pdata, waveFormat); } } else { // // multiple formats? // } } UnsafeNativeMethods.mmioAscend(hMIO, ck, 0); } if (waveFormat == null) throw new InvalidOperationException(SR.GetString(SR.SoundAPIInvalidWaveHeader)); if (waveFormat.wFormatTag != NativeMethods.WAVE_FORMAT_PCM && waveFormat.wFormatTag != NativeMethods.WAVE_FORMAT_ADPCM && waveFormat.wFormatTag != NativeMethods.WAVE_FORMAT_IEEE_FLOAT) throw new InvalidOperationException(SR.GetString(SR.SoundAPIFormatNotSupported)); } finally { if (hMIO != IntPtr.Zero) UnsafeNativeMethods.mmioClose(hMIO, 0); } } private static void ValidateSoundData(byte[] data) { int position = 0; Int16 wFormatTag = -1; bool fmtChunkFound = false; // the RIFF header should be at least 12 bytes long. if (data.Length < 12) throw new System.InvalidOperationException(SR.GetString(SR.SoundAPIInvalidWaveHeader)); // validate the RIFF header if (data[0] != 'R' || data[1] != 'I' || data[2] != 'F' || data[3] != 'F') throw new System.InvalidOperationException(SR.GetString(SR.SoundAPIInvalidWaveHeader)); if (data[8] != 'W' || data[9] != 'A' || data[10] != 'V' || data[11] != 'E') throw new System.InvalidOperationException(SR.GetString(SR.SoundAPIInvalidWaveHeader)); // we only care about "fmt " chunk position = 12; int len = data.Length; while (!fmtChunkFound && position < len - 8) { if (data[position] == (byte)'f' && data[position + 1] == (byte)'m' && data[position + 2] == (byte)'t' && data[position+3] == (byte)' ') { // // fmt chunk // fmtChunkFound = true; int chunkSize = BytesToInt(data[position+7], data[position+6], data[position+5], data[position+4]); // // get the cbSize from the WAVEFORMATEX // int sizeOfWAVEFORMAT = 16; if (chunkSize != sizeOfWAVEFORMAT) { // we are dealing w/ WAVEFORMATEX // do extra validation int sizeOfWAVEFORMATEX = 18; // make sure the buffer is big enough to store a short if (len < position + 8 + sizeOfWAVEFORMATEX - 1) throw new System.InvalidOperationException(SR.GetString(SR.SoundAPIInvalidWaveHeader)); Int16 cbSize = BytesToInt16(data[position+8 + sizeOfWAVEFORMATEX - 1], data[position+8 + sizeOfWAVEFORMATEX-2]); if (cbSize + sizeOfWAVEFORMATEX != chunkSize) throw new System.InvalidOperationException(SR.GetString(SR.SoundAPIInvalidWaveHeader)); } // make sure the buffer passed in is big enough to store a short if(len < position + 9) throw new System.InvalidOperationException(SR.GetString(SR.SoundAPIInvalidWaveHeader)); wFormatTag = BytesToInt16(data[position+9], data[position+8]); position += chunkSize + 8; } else { position += 8 + BytesToInt(data[position+7], data[position+6], data[position+5], data[position+4]); } } if (!fmtChunkFound) throw new System.InvalidOperationException(SR.GetString(SR.SoundAPIInvalidWaveHeader)); if (wFormatTag != NativeMethods.WAVE_FORMAT_PCM && wFormatTag != NativeMethods.WAVE_FORMAT_ADPCM && wFormatTag != NativeMethods.WAVE_FORMAT_IEEE_FLOAT) throw new System.InvalidOperationException(SR.GetString(SR.SoundAPIFormatNotSupported)); } private static Int16 BytesToInt16(byte ch0, byte ch1) { int res; res = (int) ch1; res |= (int) (((int)ch0) << 8); return (Int16) res; } private static int BytesToInt(byte ch0, byte ch1, byte ch2, byte ch3) { return mmioFOURCC((char) ch3, (char)ch2, (char) ch1, (char)ch0); } private static int mmioFOURCC(char ch0, char ch1, char ch2, char ch3) { int result = 0; result |= ((int) ch0); result |= ((int) ch1) << 8; result |= ((int) ch2) << 16; result |= ((int) ch3) << 24; return result; } /// [SecurityPermission(SecurityAction.LinkDemand, Flags=SecurityPermissionFlag.SerializationFormatter)] [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")] // vsw 427356 [SuppressMessage("Microsoft.Security", "CA2123:OverrideLinkDemandsShouldBeIdenticalToBase", Justification = "System.dll is still using pre-v4 security model and needs this demand")] void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) { if (!String.IsNullOrEmpty(this.soundLocation)) { info.AddValue("SoundLocation", this.soundLocation); } if (this.stream != null) { info.AddValue("Stream", this.stream); } info.AddValue("LoadTimeout", this.loadTimeout); } private class IntSecurity { // Constructor added because of FxCop rules private IntSecurity() {} private static volatile CodeAccessPermission safeSubWindows; internal static CodeAccessPermission SafeSubWindows { get { if (safeSubWindows == null) { safeSubWindows = new UIPermission(UIPermissionWindow.SafeSubWindows); } return safeSubWindows; } } } private class NativeMethods { // Constructor added because of FxCop rules private NativeMethods() {} internal const int WAVE_FORMAT_PCM = 0x0001, WAVE_FORMAT_ADPCM = 0x0002, WAVE_FORMAT_IEEE_FLOAT = 0x0003; internal const int MMIO_READ = 0x00000000, MMIO_ALLOCBUF = 0x00010000, MMIO_FINDRIFF = 0x00000020; internal const int SND_SYNC = 0000, SND_ASYNC = 0x0001, SND_NODEFAULT = 0x0002, SND_MEMORY = 0x0004, SND_LOOP = 0x0008, SND_PURGE = 0x0040, SND_FILENAME = 0x00020000, SND_NOSTOP = 0x0010; [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)] internal class MMCKINFO { internal int ckID; internal int cksize; internal int fccType; internal int dwDataOffset; internal int dwFlags; } [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)] internal class WAVEFORMATEX { internal System.Int16 wFormatTag; internal System.Int16 nChannels; internal int nSamplesPerSec; internal int nAvgBytesPerSec; internal System.Int16 nBlockAlign; internal System.Int16 wBitsPerSample; internal System.Int16 cbSize; } } private class UnsafeNativeMethods { // Constructor added because of FxCop rules private UnsafeNativeMethods() {} [DllImport(ExternDll.WinMM, CharSet=CharSet.Auto)] [ResourceExposure(ResourceScope.Machine)] internal static extern bool PlaySound([MarshalAs(UnmanagedType.LPWStr)] string soundName, IntPtr hmod, int soundFlags); [DllImport(ExternDll.WinMM, ExactSpelling=true, CharSet=CharSet.Auto)] [ResourceExposure(ResourceScope.Machine)] internal static extern bool PlaySound(byte[] soundName, IntPtr hmod, int soundFlags); [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2101:SpecifyMarshalingForPInvokeStringArguments")] [DllImport(ExternDll.WinMM, CharSet=CharSet.Auto)] [ResourceExposure(ResourceScope.Machine)] internal static extern IntPtr mmioOpen(string fileName, IntPtr not_used, int flags); [DllImport(ExternDll.WinMM, CharSet=CharSet.Auto)] [ResourceExposure(ResourceScope.None)] internal static extern int mmioAscend(IntPtr hMIO, NativeMethods.MMCKINFO lpck, int flags); [DllImport(ExternDll.WinMM, CharSet=CharSet.Auto)] [ResourceExposure(ResourceScope.None)] internal static extern int mmioDescend(IntPtr hMIO, [MarshalAs(UnmanagedType.LPStruct)] NativeMethods.MMCKINFO lpck, [MarshalAs(UnmanagedType.LPStruct)] NativeMethods.MMCKINFO lcpkParent, int flags); [DllImport(ExternDll.WinMM, CharSet=CharSet.Auto)] [ResourceExposure(ResourceScope.None)] internal static extern int mmioRead(IntPtr hMIO, [MarshalAs(UnmanagedType.LPArray)] byte[] wf, int cch); [DllImport(ExternDll.WinMM, CharSet=CharSet.Auto)] [ResourceExposure(ResourceScope.None)] internal static extern int mmioClose(IntPtr hMIO, int flags); } } }