#region --- License --- /* Licensed under the MIT/X11 license. * Copyright (c) 2006-2008 the OpenTK Team. * This notice may not be removed from any source distribution. * See license.txt for licensing details. */ #endregion using System; using System.Collections.Generic; using System.Text; using System.Diagnostics; using OpenTK.Audio; namespace OpenTK.Audio { /// /// Provides methods to create and use an audio context. /// public sealed class AudioContext : IDisposable { #region --- Fields --- bool disposed; bool is_processing, is_synchronized; IntPtr device_handle; ContextHandle context_handle; bool context_exists; string device_name; static object audio_context_lock = new object(); static List available_devices = new List(); static Dictionary available_contexts = new Dictionary(); static bool openal_supported = true; static Version version; private enum Version { OpenAL10, OpenAL11 } #endregion #region --- Constructors --- #region static AudioContext() /// /// /// /// Runs before the actual class constructor, to load available devices. /// static AudioContext() { LoadAvailableDevices(); } #endregion #region public AudioContext() /// Constructs a new AudioContext, using the default audio device. public AudioContext() : this(null, 0, 0, false, true) { } #endregion #region public AudioContext(string device) /// Constructs a new AudioContext, using the specified audio device. /// The name of the audio device to use. /// /// Use AudioContext.AvailableDevices to obtain a list of all available audio devices. /// devices. /// public AudioContext(string device) : this(device, 0, 0, false, true) { } #endregion #region public AudioContext(string device, int freq) /// Constructs a new AudioContext, using the specified audio device and device parameters. /// The name of the audio device to use. /// Frequency for mixing output buffer, in units of Hz. Pass 0 for driver default. /// /// Use AudioContext.AvailableDevices to obtain a list of all available audio devices. /// devices. /// public AudioContext(string device, int freq) : this(device, freq, 0, false, true) { } #endregion #region public AudioContext(string device, int freq, int refresh) /// Constructs a new AudioContext, using the specified audio device and device parameters. /// The name of the audio device to use. /// Frequency for mixing output buffer, in units of Hz. Pass 0 for driver default. /// Refresh intervals, in units of Hz. Pass 0 for driver default. /// /// Use AudioContext.AvailableDevices to obtain a list of all available audio devices. /// devices. /// public AudioContext(string device, int freq, int refresh) : this(device, freq, refresh, false, true) { } #endregion #region public AudioContext(string device) /// Constructs a new AudioContext, using the specified audio device and device parameters. /// The name of the audio device to use. /// Frequency for mixing output buffer, in units of Hz. Pass 0 for driver default. /// Refresh intervals, in units of Hz. Pass 0 for driver default. /// Flag, indicating a synchronous context. /// /// Use AudioContext.AvailableDevices to obtain a list of all available audio devices. /// devices. /// public AudioContext(string device, int freq, int refresh, bool sync) : this(available_devices[0], freq, refresh, sync, true) { } #endregion #region public AudioContext(string device, int freq, int refresh, bool sync, bool enableEfx) /// Creates the audio context using the specified device and device parameters. /// The device descriptor obtained through AudioContext.AvailableDevices. /// Frequency for mixing output buffer, in units of Hz. Pass 0 for driver default. /// Refresh intervals, in units of Hz. Pass 0 for driver default. /// Flag, indicating a synchronous context. /// Indicates whether the EFX extension should be initialized, if present. /// Occurs when the device string is invalid. /// Occurs when a specified parameter is invalid. /// /// Occurs when the specified device is not available, or is in use by another program. /// /// /// Occurs when an audio context could not be created with the specified parameters. /// /// /// Occurs when an AudioContext already exists. /// /// For maximum compatibility, you are strongly recommended to use the default constructor. /// Multiple AudioContexts are not supported at this point. /// /// The number of auxilliary EFX sends depends on the audio hardware and drivers. Most Realtek devices, as well /// as the Creative SB Live!, support 1 auxilliary send. Creative's Audigy and X-Fi series support 4 sends. /// Values higher than supported will be clamped by the driver. /// /// public AudioContext(string device, int freq, int refresh, bool sync, bool enableEfx) { CreateContext(device, freq, refresh, sync, enableEfx); } #endregion #endregion #region --- Private Methods --- #region static void LoadAvailableDevices() /// /// /// /// Loads all available audio devices into the available_devices array. /// /// /// Only called by the static AudioContext constructor. /// static void LoadAvailableDevices() { lock (audio_context_lock) { if (available_devices.Count == 0) { try { Debug.WriteLine("Enumerating audio devices."); Debug.Indent(); if (Alc.IsExtensionPresent(IntPtr.Zero, "ALC_ENUMERATION_EXT")) { version = Version.OpenAL11; available_devices.AddRange(Alc.GetString(IntPtr.Zero, AlcGetStringList.DeviceSpecifier)); } else { version = Version.OpenAL10; Debug.Print("Device enumeration extension not available. Failed to enumerate devices."); } foreach (string s in available_devices) Debug.WriteLine(s); } catch (DllNotFoundException e) { Debug.WriteLine(e.ToString()); openal_supported = false; } finally { Debug.Unindent(); } } } } #endregion #region CreateContext /// /// Creates the audio context using the specified device. /// The device descriptor obtained through AudioContext.AvailableDevices, or null for the default device. /// Frequency for mixing output buffer, in units of Hz. Pass 0 for driver default. /// Refresh intervals, in units of Hz. Pass 0 for driver default. /// Flag, indicating a synchronous context. /// Indicates whether the EFX extension should be initialized, if present. /// /// Occurs when a specified parameter is invalid. /// /// Occurs when the specified device is not available, or is in use by another program. /// /// /// Occurs when an audio context could not be created with the specified parameters. /// /// /// Occurs when an AudioContext already exists. /// /// For maximum compatibility, you are strongly recommended to use the default constructor. /// Multiple AudioContexts are not supported at this point. /// /// The number of auxilliary EFX sends depends on the audio hardware and drivers. Most Realtek devices, as well /// as the Creative SB Live!, support 1 auxilliary send. Creative's Audigy and X-Fi series support 4 sends. /// Values higher than supported will be clamped by the driver. /// /// void CreateContext(string device, int freq, int refresh, bool sync, bool enableEfx) { if (!openal_supported) throw new DllNotFoundException("openal32.dll"); if (version == Version.OpenAL11 && available_devices.Count == 0) // Version.OpenAL10 does not support device enumeration. throw new NotSupportedException("No audio hardware is available."); if (context_exists) throw new NotSupportedException("Multiple AudioContexts are not supported."); if (freq < 0) throw new ArgumentOutOfRangeException("freq", freq, "Should be greater than zero."); if (refresh < 0) throw new ArgumentOutOfRangeException("refresh", refresh, "Should be greater than zero."); if (!String.IsNullOrEmpty(device)) device_handle = Alc.OpenDevice(device); if (device_handle == IntPtr.Zero) device_handle = Alc.OpenDevice(null); if (device_handle == IntPtr.Zero) Alc.OpenDevice(Alc.GetString(IntPtr.Zero, AlcGetString.DefaultDeviceSpecifier)); if (device_handle == IntPtr.Zero && available_devices.Count > 0) device_handle = Alc.OpenDevice(available_devices[0]); if (device_handle == IntPtr.Zero) throw new AudioDeviceException(String.Format("Audio device '{0}' does not exist or is tied up by another application.", String.IsNullOrEmpty(device) ? "default" : device)); CheckForAlcErrors(); // Build the attribute list List attributes = new List(); if (freq != 0) { attributes.Add((int)AlcContextAttributes.Frequency); attributes.Add(freq); } if (refresh != 0) { attributes.Add((int)AlcContextAttributes.Refresh); attributes.Add(refresh); } attributes.Add((int)AlcContextAttributes.Sync); attributes.Add(sync ? 1 : 0); if (enableEfx && Alc.IsExtensionPresent(device_handle, "ALC_EXT_EFX")) { int num_slots; Alc.GetInteger(device_handle, AlcGetInteger.EfxMaxAuxiliarySends, 1, out num_slots); attributes.Add((int)AlcContextAttributes.EfxMaxAuxiliarySends); attributes.Add(num_slots); } attributes.Add(0); context_handle = Alc.CreateContext(device_handle, attributes.ToArray()); if (context_handle == ContextHandle.Zero) { Alc.CloseDevice(device_handle); throw new AudioContextException("The audio context could not be created with the specified parameters."); } CheckForAlcErrors(); // HACK: OpenAL SI on Linux/ALSA crashes on MakeCurrent. This hack avoids calling MakeCurrent when // an old OpenAL version is detect - it may affect outdated OpenAL versions different than OpenAL SI, // but it looks like a good compromise for now. if (available_devices.Count > 0) MakeCurrent(); CheckForAlcErrors(); device_name = Alc.GetString(device_handle, AlcGetString.DeviceSpecifier); int attribute_count; Alc.GetInteger(device_handle, AlcGetInteger.AttributesSize, sizeof(int), out attribute_count); if (attribute_count > 0) { int[] device_attributes = new int[attribute_count]; Alc.GetInteger(device_handle, AlcGetInteger.AllAttributes, device_attributes.Length * sizeof(int), out device_attributes[0]); foreach (int attr in device_attributes) { switch ((AlcContextAttributes)attr) { case AlcContextAttributes.Sync: IsSynchronized = true; break; } } } lock (audio_context_lock) { available_contexts.Add(this.context_handle, this); context_exists = true; } } #endregion #region void CheckForAlcErrors() void CheckForAlcErrors() { AlcError err = Alc.GetError(device_handle); if (err != AlcError.NoError) throw new AudioContextException(err.ToString()); } #endregion #region static void MakeCurrent(AudioContext context) /// /// Makes the specified AudioContext current in the calling thread. /// The OpenTK.Audio.AudioContext to make current, or null. /// /// Occurs if this function is called after the AudioContext has been disposed. /// /// /// Occurs when the AudioContext could not be made current. /// static void MakeCurrent(AudioContext context) { lock (audio_context_lock) { if (!Alc.MakeContextCurrent(context != null ? context.context_handle : ContextHandle.Zero)) throw new AudioContextException(String.Format("ALC {0} error detected at {1}.", Alc.GetError(context != null ? (IntPtr)context.context_handle : IntPtr.Zero).ToString(), context != null ? context.ToString() : "null")); } } #endregion #region internal bool IsCurrent /// /// Gets or sets a System.Boolean indicating whether the AudioContext /// is current. /// /// /// Only one AudioContext can be current in the application at any time, /// regardless of the number of threads. /// internal bool IsCurrent { get { lock (audio_context_lock) { if (available_contexts.Count == 0) return false; else { return AudioContext.CurrentContext == this; } } } set { if (value) AudioContext.MakeCurrent(this); else AudioContext.MakeCurrent(null); } } #endregion #region IntPtr Device IntPtr Device { get { return device_handle; } } #endregion #endregion #region --- Public Members --- #region internal void MakeCurrent() /// Makes the AudioContext current in the calling thread. /// /// Occurs if this function is called after the AudioContext has been disposed. /// /// /// Occurs when the AudioContext could not be made current. /// /// /// Only one AudioContext can be current in the application at any time, /// regardless of the number of threads. /// public void MakeCurrent() { AudioContext.MakeCurrent(this); } #endregion #region public bool IsProcessing /// /// Gets a System.Boolean indicating whether the AudioContext is /// currently processing audio events. /// /// /// public bool IsProcessing { get { return is_processing; } private set { is_processing = value; } } #endregion #region public bool IsProcessing /// /// Gets a System.Boolean indicating whether the AudioContext is /// synchronized. /// /// public bool IsSynchronized { get { return is_synchronized; } private set { is_synchronized = value; } } #endregion #region public void Process /// /// Processes queued audio events. /// /// /// /// If AudioContext.IsSynchronized is true, this function will resume /// the internal audio processing thread. If AudioContext.IsSynchronized is false, /// you will need to call this function multiple times per second to process /// audio events. /// /// /// In some implementations this function may have no effect. /// /// /// Occurs when this function is called after the AudioContext had been disposed. /// /// /// public void Process() { if (disposed) throw new ObjectDisposedException(this.ToString()); Alc.ProcessContext(this.context_handle); IsProcessing = true; } #endregion #region public void Suspend /// /// Suspends processing of audio events. /// /// /// /// To avoid audio artifacts when calling this function, set audio gain to zero before /// suspending an AudioContext. /// /// /// In some implementations, it can be faster to suspend processing before changing /// AudioContext state. /// /// /// In some implementations this function may have no effect. /// /// /// Occurs when this function is called after the AudioContext had been disposed. /// /// /// public void Suspend() { if (disposed) throw new ObjectDisposedException(this.ToString()); Alc.SuspendContext(this.context_handle); IsProcessing = false; } #endregion #region public bool SupportsExtension(string extension) /// /// Checks whether the specified OpenAL extension is supported. /// /// The name of the extension to check (e.g. "ALC_EXT_EFX"). /// true if the extension is supported; false otherwise. public bool SupportsExtension(string extension) { if (disposed) throw new ObjectDisposedException(this.GetType().FullName); return Alc.IsExtensionPresent(this.Device, extension); } #endregion #region public static AudioContext CurrentContext /// /// Gets the OpenTK.Audio.AudioContext which is current in the application. /// /// /// Only one AudioContext can be current in the application at any time, /// regardless of the number of threads. /// public static AudioContext CurrentContext { get { lock (audio_context_lock) { if (available_contexts.Count == 0) return null; else { AudioContext context; AudioContext.available_contexts.TryGetValue( (ContextHandle)Alc.GetCurrentContext(), out context); return context; } } } } #endregion #region public static string[] AvailableDevices /// /// Gets a System.String array containing all available audio devices. /// /// This property allocates memory. public static string[] AvailableDevices { get { if (available_devices.Count == 0) LoadAvailableDevices(); return available_devices.ToArray(); } } #endregion #endregion #region --- IDisposable Members --- /// /// Disposes of the AudioContext, cleaning up all resources consumed by it. /// public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } void Dispose(bool manual) { if (!disposed) { if (this.IsCurrent) this.IsCurrent = false; if (context_handle != ContextHandle.Zero) { available_contexts.Remove(context_handle); Alc.DestroyContext(context_handle); } if (device_handle != IntPtr.Zero) Alc.CloseDevice(device_handle); if (manual) { } disposed = true; } } ~AudioContext() { this.Dispose(false); } #endregion #region --- Overrides --- public override int GetHashCode() { return base.GetHashCode(); } public override bool Equals(object obj) { return base.Equals(obj); } public override string ToString() { return String.Format("{0} (handle: {1}, device: {2})", this.device_name, this.context_handle, this.device_handle); } #endregion } }