#region License // // The Open Toolkit Library License // // Copyright (c) 2006 - 2009 the Open Toolkit library. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights to // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of // the Software, and to permit persons to whom the Software is furnished to do // so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // #endregion using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; namespace OpenTK.Audio { /// /// Provides methods to instantiate, use and destroy an audio device for recording. /// Static methods are provided to list available devices known by the driver. /// public sealed class AudioCapture : IDisposable { #region Fields // This must stay private info so the end-user cannot call any Alc commands for the recording device. IntPtr Handle; // Alc.CaptureStop should be called prior to device shutdown, this keeps track of Alc.CaptureStart/Stop calls. bool _isrecording = false; ALFormat sample_format; int sample_frequency; #endregion #region Constructors static AudioCapture() { if (AudioDeviceEnumerator.IsOpenALSupported) // forces enumeration { } } /// /// Opens the default device for audio recording. /// Implicitly set parameters are: 22050Hz, 16Bit Mono, 4096 samples ringbuffer. /// public AudioCapture() : this(AudioCapture.DefaultDevice, 22050, ALFormat.Mono16, 4096) { } /// Opens a device for audio recording. /// The device name. /// The frequency that the data should be captured at. /// The requested capture buffer format. /// The size of OpenAL's capture internal ring-buffer. This value expects number of samples, not bytes. public AudioCapture(string deviceName, int frequency, ALFormat sampleFormat, int bufferSize) { if (!AudioDeviceEnumerator.IsOpenALSupported) throw new DllNotFoundException("openal32.dll"); if (frequency <= 0) throw new ArgumentOutOfRangeException("frequency"); if (bufferSize <= 0) throw new ArgumentOutOfRangeException("bufferSize"); // Try to open specified device. If it fails, try to open default device. device_name = deviceName; Handle = Alc.CaptureOpenDevice(deviceName, frequency, sampleFormat, bufferSize); if (Handle == IntPtr.Zero) { Debug.WriteLine(ErrorMessage(deviceName, frequency, sampleFormat, bufferSize)); device_name = "IntPtr.Zero"; Handle = Alc.CaptureOpenDevice(null, frequency, sampleFormat, bufferSize); } if (Handle == IntPtr.Zero) { Debug.WriteLine(ErrorMessage("IntPtr.Zero", frequency, sampleFormat, bufferSize)); device_name = AudioDeviceEnumerator.DefaultRecordingDevice; Handle = Alc.CaptureOpenDevice(AudioDeviceEnumerator.DefaultRecordingDevice, frequency, sampleFormat, bufferSize); } if (Handle == IntPtr.Zero) { // Everything we tried failed. Capture may not be supported, bail out. Debug.WriteLine(ErrorMessage(AudioDeviceEnumerator.DefaultRecordingDevice, frequency, sampleFormat, bufferSize)); device_name = "None"; throw new AudioDeviceException("All attempts to open capture devices returned IntPtr.Zero. See debug log for verbose list."); } // handle is not null, check for some Alc Error CheckErrors(); SampleFormat = sampleFormat; SampleFrequency = frequency; } #endregion Constructor #region Public Members #region CurrentDevice private string device_name; /// /// The name of the device associated with this instance. /// public string CurrentDevice { get { return device_name; } } #endregion #region AvailableDevices /// /// Returns a list of strings containing all known recording devices. /// public static IList AvailableDevices { get { return AudioDeviceEnumerator.AvailableRecordingDevices; } } #endregion #region DefaultDevice /// /// Returns the name of the device that will be used as recording default. /// public static string DefaultDevice { get { return AudioDeviceEnumerator.DefaultRecordingDevice; } } #endregion #region CheckErrors /// /// Checks for ALC error conditions. /// /// Raised when an out of memory error is detected. /// Raised when an invalid value is detected. /// Raised when an invalid device is detected. /// Raised when an invalid context is detected. public void CheckErrors() { new AudioDeviceErrorChecker(Handle).Dispose(); } #endregion #region CurrentError /// Returns the ALC error code for this device. public AlcError CurrentError { get { return Alc.GetError(Handle); } } #endregion #region Start & Stop /// /// Start recording samples. /// The number of available samples can be obtained through the property. /// The data can be queried with any method. /// public void Start() { Alc.CaptureStart(Handle); _isrecording = true; } /// Stop recording samples. This will not clear previously recorded samples. public void Stop() { Alc.CaptureStop(Handle); _isrecording = false; } #endregion Start & Stop Capture #region AvailableSamples /// Returns the number of available samples for capture. public int AvailableSamples { get { // TODO: Investigate inconsistency between documentation and actual usage. // Doc claims the 3rd param is Number-of-Bytes, but it appears to be Number-of-Int32s int result; Alc.GetInteger(Handle, AlcGetInteger.CaptureSamples, 1, out result); return result; } } #endregion Available samples property #region ReadSamples /// Fills the specified buffer with samples from the internal capture ring-buffer. This method does not block: it is an error to specify a sampleCount larger than AvailableSamples. /// A pointer to a previously initialized and pinned array. /// The number of samples to be written to the buffer. public void ReadSamples(IntPtr buffer, int sampleCount) { Alc.CaptureSamples(Handle, buffer, sampleCount); } /// Fills the specified buffer with samples from the internal capture ring-buffer. This method does not block: it is an error to specify a sampleCount larger than AvailableSamples. /// The buffer to fill. /// The number of samples to be written to the buffer. /// Raised when buffer is null. /// Raised when sampleCount is larger than the buffer. public void ReadSamples(TBuffer[] buffer, int sampleCount) where TBuffer : struct { if (buffer == null) throw new ArgumentNullException("buffer"); int buffer_size = BlittableValueType.Stride * buffer.Length; // This is more of a heuristic than a 100% valid check. However, it will work // correctly for 99.9% of all use cases. // This should never produce a false positive, but a false negative might // be produced with compressed sample formats (which are very rare). // Still, this is better than no check at all. if (sampleCount * GetSampleSize(SampleFormat) > buffer_size) throw new ArgumentOutOfRangeException("sampleCount"); GCHandle buffer_ptr = GCHandle.Alloc(buffer, GCHandleType.Pinned); try { ReadSamples(buffer_ptr.AddrOfPinnedObject(), sampleCount); } finally { buffer_ptr.Free(); } } #endregion #region SampleFormat & SampleFrequency /// /// Gets the OpenTK.Audio.ALFormat for this instance. /// public ALFormat SampleFormat { get { return sample_format; } private set { sample_format = value; } } /// /// Gets the sampling rate for this instance. /// public int SampleFrequency { get { return sample_frequency; } private set { sample_frequency = value; } } #endregion #region IsRunning /// /// Gets a value indicating whether this instance is currently capturing samples. /// public bool IsRunning { get { return _isrecording; } } #endregion #endregion #region Private Members // Retrieves the sample size in bytes for various ALFormats. // Compressed formats always return 1. static int GetSampleSize(ALFormat format) { switch (format) { case ALFormat.Mono8: return 1; case ALFormat.Mono16: return 2; case ALFormat.Stereo8: return 2; case ALFormat.Stereo16: return 4; case ALFormat.MonoFloat32Ext: return 4; case ALFormat.MonoDoubleExt: return 8; case ALFormat.StereoFloat32Ext: return 8; case ALFormat.StereoDoubleExt: return 16; case ALFormat.MultiQuad8Ext: return 4; case ALFormat.MultiQuad16Ext: return 8; case ALFormat.MultiQuad32Ext: return 16; case ALFormat.Multi51Chn8Ext: return 6; case ALFormat.Multi51Chn16Ext: return 12; case ALFormat.Multi51Chn32Ext: return 24; case ALFormat.Multi61Chn8Ext: return 7; case ALFormat.Multi71Chn16Ext: return 14; case ALFormat.Multi71Chn32Ext: return 28; case ALFormat.MultiRear8Ext: return 1; case ALFormat.MultiRear16Ext: return 2; case ALFormat.MultiRear32Ext: return 4; default: return 1; // Unknown sample size. } } // Converts an error code to an error string with additional information. string ErrorMessage(string devicename, int frequency, ALFormat bufferformat, int buffersize) { string alcerrmsg; AlcError alcerrcode = CurrentError; switch (alcerrcode) { case AlcError.OutOfMemory: alcerrmsg = alcerrcode.ToString() + ": The specified device is invalid, or can not capture audio."; break; case AlcError.InvalidValue: alcerrmsg = alcerrcode.ToString() + ": One of the parameters has an invalid value."; break; default: alcerrmsg = alcerrcode.ToString(); break; } return "The handle returned by Alc.CaptureOpenDevice is null." + "\nAlc Error: " + alcerrmsg + "\nDevice Name: " + devicename + "\nCapture frequency: " + frequency + "\nBuffer format: " + bufferformat + "\nBuffer Size: " + buffersize; } #endregion #region IDisposable Members ~AudioCapture() { Dispose(); } private bool IsDisposed; /// Closes the device and disposes the instance. public void Dispose() { this.Dispose(true); GC.SuppressFinalize(this); } private void Dispose(bool manual) { if (!this.IsDisposed) { if (this.Handle != IntPtr.Zero) { if (this._isrecording) this.Stop(); Alc.CaptureCloseDevice(this.Handle); } this.IsDisposed = true; } } #endregion Destructor } }