diff --git a/CSCore.Test/CSCore.Test.csproj b/CSCore.Test/CSCore.Test.csproj index 43881930..92c58664 100644 --- a/CSCore.Test/CSCore.Test.csproj +++ b/CSCore.Test/CSCore.Test.csproj @@ -126,6 +126,7 @@ + diff --git a/CSCore.Test/SoundOut/ALSoundOutBehaviourTests.cs b/CSCore.Test/SoundOut/ALSoundOutBehaviourTests.cs new file mode 100644 index 00000000..b4708fbc --- /dev/null +++ b/CSCore.Test/SoundOut/ALSoundOutBehaviourTests.cs @@ -0,0 +1,14 @@ +using CSCore.SoundOut; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CSCore.Test.SoundOut +{ + [TestClass] + public class ALSoundOutBehaviourTests : SoundOutBehaviourTests + { + protected override ISoundOut CreateSoundOut() + { + return new ALSoundOut(); + } + } +} diff --git a/CSCore/CSCore.csproj b/CSCore/CSCore.csproj index d81e2ee2..2fabd16b 100644 --- a/CSCore/CSCore.csproj +++ b/CSCore/CSCore.csproj @@ -327,12 +327,22 @@ + + + + + + + + + + diff --git a/CSCore/SoundOut/AL/ALContext.cs b/CSCore/SoundOut/AL/ALContext.cs new file mode 100644 index 00000000..871d0534 --- /dev/null +++ b/CSCore/SoundOut/AL/ALContext.cs @@ -0,0 +1,69 @@ +using System; + +namespace CSCore.SoundOut.AL +{ + internal class ALContext : IDisposable + { + /// + /// Gets the handle + /// + public IntPtr Handle { private set; get; } + + /// + /// Initializes a new ALContext class + /// + /// The handle + private ALContext(IntPtr contextHandle) + { + Handle = contextHandle; + } + + /// + /// Makes the context the current context + /// + public void MakeCurrent() + { + ALInterops.alcMakeContextCurrent(Handle); + } + + /// + /// Deconstructs the ALContext class + /// + ~ALContext() + { + Dispose(false); + } + + /// + /// Disposes the openal context + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes the openal context + /// + /// The disposing state + protected void Dispose(bool disposing) + { + if (Handle != IntPtr.Zero) + { + ALInterops.alcDestroyContext(Handle); + Handle = IntPtr.Zero; + } + } + + /// + /// Creates a new openal context + /// + /// The device handle + /// OpenALContext + public static ALContext CreateContext(IntPtr deviceHandle) + { + return new ALContext(ALInterops.alcCreateContext(deviceHandle, IntPtr.Zero)); + } + } +} diff --git a/CSCore/SoundOut/AL/ALDevice.cs b/CSCore/SoundOut/AL/ALDevice.cs new file mode 100644 index 00000000..5f98e7a6 --- /dev/null +++ b/CSCore/SoundOut/AL/ALDevice.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CSCore.SoundOut.AL +{ + public class ALDevice : IDisposable + { + private static ALDevice[] _devices; + + /// + /// Gets the name + /// + public string Name { private set; get; } + + /// + /// Gets the openal context + /// + internal ALContext Context { get; private set; } + + private IntPtr _deviceHandle; + private readonly List _sources; + private bool _isInitialized; + + /// + /// Initializes a new ALDevice class + /// + internal ALDevice(string deviceName) + { + Name = deviceName; + _sources = new List(); + } + + /// + /// Initializes the openal device + /// + public void Initialize() + { + if (!_isInitialized) + { + _deviceHandle = ALInterops.alcOpenDevice(Name); + Context = ALContext.CreateContext(_deviceHandle); + _isInitialized = true; + } + } + + internal ALErrorCode GetLastError() + { + return ALInterops.alGetError(); + } + + /// + /// Generates a new openal source + /// + /// + internal ALSource GenerateALSource() + { + Context.MakeCurrent(); + + var sources = new uint[1]; + ALInterops.alGenSources(1, sources); + + return new ALSource(this, sources[0]); + } + + /// + /// Deletes the specified openal source + /// + /// The source + internal void DeleteALSource(ALSource source) + { + Context.MakeCurrent(); + + var sources = new uint[1]; + sources[0] = source.Id; + + ALInterops.alDeleteSources(1, sources); + } + + /// + /// Enumerates the openal devices + /// + /// + public static ALDevice[] EnumerateALDevices() + { + if (_devices == null) + { + var deviceNames = ALInterops.GetALDeviceNames(); + var devices = new ALDevice[deviceNames.Length]; + + for (int i = 0; i < devices.Length; i++) + { + devices[i] = new ALDevice(deviceNames[i]); + } + + _devices = devices; + } + + return _devices; + } + + /// + /// Gets the default playback device + /// + public static ALDevice DefaultDevice + { + get { return EnumerateALDevices().FirstOrDefault(); } + } + + /// + /// Disposes the openal device + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes the openal device + /// + /// The disposing state + protected void Dispose(bool disposing) + { + if (disposing) + { + Context.Dispose(); + } + + if (_deviceHandle != IntPtr.Zero) + { + ALInterops.alcCloseDevice(_deviceHandle); + _deviceHandle = IntPtr.Zero; + } + } + } +} diff --git a/CSCore/SoundOut/AL/ALErrorCodes.cs b/CSCore/SoundOut/AL/ALErrorCodes.cs new file mode 100644 index 00000000..4a5eefad --- /dev/null +++ b/CSCore/SoundOut/AL/ALErrorCodes.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace CSCore.SoundOut.AL +{ + public enum ALErrorCode + { + /// + /// No Error + /// + NoError = 0x0, + + /// + /// Invalid Name + /// + InvalidName = 0xA001, + + /// + /// Invalid Enum + /// + InvalidEnum = 0xA002, + + /// + /// Invalid Value + /// + InvalidValue = 0xA003, + + /// + /// Invalid Operation + /// + InvalidOperation = 0xA004, + + /// + /// Out of Memory + /// + OutOfMemory = 0xA005 + } + + public enum ALCErrorCode + { + /// + /// No Error + /// + NoError = 0x0, + + /// + /// Invalid Device + /// + InvalidDevice = 0xA001, + + + /// + /// Invalid Context + /// + InvalidContext = 0xA002, + + /// + /// Invalid Enum + /// + InvalidEnum = 0xA003, + + /// + /// Invalid Value + /// + InvalidValue = 0xA004, + + /// + /// Out of Memory + /// + OutOfMemory = 0xA005 + } +} diff --git a/CSCore/SoundOut/AL/ALFormat.cs b/CSCore/SoundOut/AL/ALFormat.cs new file mode 100644 index 00000000..b4d20e6e --- /dev/null +++ b/CSCore/SoundOut/AL/ALFormat.cs @@ -0,0 +1,42 @@ +namespace CSCore.SoundOut.AL +{ + internal enum ALFormat + { + /// + /// Unknown. + /// + Unknown = 0, + + /// + /// Mono, 8Bit. + /// + Mono8Bit = 0x1100, + + /// + /// Mono, 16Bit. + /// + Mono16Bit = 0x1101, + + /// + /// Stereo, 8Bit. + /// + Stereo8Bit = 0x1102, + + /// + /// Stereo, 16Bit. + /// + Stereo16Bit = 0x1103, + + /// + /// Mono, float 32Bit. + /// This is not required to be supported on all implementations + /// + MonoFloat32Bit = 0x10010, + + /// + /// Stereo, float 32bit + /// This is not required to be supported on all implementations + /// + StereoFloat32Bit = 0x10011 + } +} diff --git a/CSCore/SoundOut/AL/ALInterops.cs b/CSCore/SoundOut/AL/ALInterops.cs new file mode 100644 index 00000000..8627df1a --- /dev/null +++ b/CSCore/SoundOut/AL/ALInterops.cs @@ -0,0 +1,206 @@ + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace CSCore.SoundOut.AL +{ + internal class ALInterops + { + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr alGetString(int name); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr alcGetString([In] IntPtr device, int name); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern sbyte alcIsExtensionPresent([In] IntPtr device, string extensionName); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern sbyte alIsExtensionPresent(string extensionName); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alcCaptureStart(IntPtr device); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alcCaptureStop(IntPtr device); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alcCaptureSamples(IntPtr device, IntPtr buffer, int numSamples); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr alcCaptureOpenDevice(string deviceName, uint frequency, ALFormat format, + int bufferSize); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alcCaptureCloseDevice(IntPtr device); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr alcOpenDevice(string deviceName); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr alcCloseDevice(IntPtr handle); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr alcCreateContext(IntPtr device, IntPtr attrlist); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alcMakeContextCurrent(IntPtr context); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr alcGetContextsDevice(IntPtr context); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern IntPtr alcGetCurrentContext(); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alcDestroyContext(IntPtr context); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alGetSourcei(uint sourceId, ALSourceParameters param, out int value); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alSourcePlay(uint sourceId); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alSourcePause(uint sourceId); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alSourceStop(uint sourceId); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alSourceQueueBuffers(uint sourceId, int number, uint[] bufferIDs); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alSourceUnqueueBuffers(uint sourceId, int buffers, uint[] buffersDequeued); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alGenSources(int count, uint[] sources); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alDeleteSources(int count, uint[] sources); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alGetSourcef(uint sourceId, ALSourceParameters param, out float value); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alGetSource3f(uint sourceId, ALSourceParameters param, out float val1, + out float val2, out float val3); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alSourcef(uint sourceId, ALSourceParameters param, float value); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alSourcefv(uint sourceId, ALSourceParameters param, float[] value); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alSource3f(uint sourceId, ALSourceParameters param, float val1, float val2, + float val3); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alSourcei(uint sourceId, ALSourceParameters param, float val1); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alGenBuffers(int count, uint[] bufferIDs); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alBufferData(uint bufferId, ALFormat format, byte[] data, int byteSize, + uint frequency); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alDeleteBuffers(int numBuffers, uint[] bufferIDs); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alListenerf(ALSourceParameters param, float val); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alListenerfv(ALSourceParameters param, float[] val); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alListener3f(ALSourceParameters param, float val1, float val2, float val3); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alGetListener3f(ALSourceParameters param, out float val1, out float val2, + out float val3); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alGetListenerf(ALSourceParameters param, out float val); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern void alGetListenerfv(ALSourceParameters param, float[] val); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern ALErrorCode alGetError(); + + [DllImport("OpenAL32.dll", CallingConvention = CallingConvention.Cdecl)] + internal static extern ALErrorCode alcGetError(IntPtr handle); + + public const int DeviceSpecifier = 0x1005; + + public const int AllDevicesSpecifier = 0x1013; + + internal static string[] GetALDeviceNames() + { + var strings = new string[0]; + if (IsExtensionPresent("ALC_ENUMERATE_ALL_EXT")) + { + strings = + ReadStringsFromMemory(alcGetString(IntPtr.Zero, + AllDevicesSpecifier)); + } + else if (IsExtensionPresent("ALC_ENUMERATION_EXT")) + { + strings = + ReadStringsFromMemory(alcGetString(IntPtr.Zero, DeviceSpecifier)); + } + + return strings; + } + + internal static string[] ReadStringsFromMemory(IntPtr location) + { + var strings = new List(); + + bool lastNull = false; + int i = -1; + byte c; + while (!((c = Marshal.ReadByte(location, ++i)) == '\0' && lastNull)) + { + if (c == '\0') + { + lastNull = true; + + strings.Add(Marshal.PtrToStringAnsi(location, i)); + location = new IntPtr((long) location + i + 1); + i = -1; + } + else + lastNull = false; + } + + return strings.ToArray(); + } + + internal static bool IsExtensionPresent(string extension) + { + sbyte result = extension.StartsWith("ALC") + ? alcIsExtensionPresent(IntPtr.Zero, extension) + : alIsExtensionPresent(extension); + + return (result == 1); + } + + internal static bool IsSupported() + { + try + { + alIsExtensionPresent("TEST_ONLY"); + return true; + } + catch (DllNotFoundException) + { + return false; + } + } + } +} diff --git a/CSCore/SoundOut/AL/ALPlayback.cs b/CSCore/SoundOut/AL/ALPlayback.cs new file mode 100644 index 00000000..2526a4cf --- /dev/null +++ b/CSCore/SoundOut/AL/ALPlayback.cs @@ -0,0 +1,350 @@ + +using System; +using System.Threading; + +namespace CSCore.SoundOut.AL +{ + internal class ALPlayback : IDisposable + { + /// + /// Gets the openal device + /// + public ALDevice Device { get; } + + /// + /// Gets the playback state + /// + public PlaybackState PlaybackState { private set; get; } + + /// + /// Gets the length in ms + /// + public long Length { private set; get; } + + /// + /// Gets the position in ms + /// + public long Position { set; get; } + + /// + /// Gets the latency + /// + public int Latency { private set; get; } + + /// + /// Raises when the playback state changed + /// + public event EventHandler PlaybackChanged; + + private readonly ALSource _source; + private Thread _playbackThread; + private readonly object _locker; + private IWaveSource _playbackStream; + private WaveFormat _waveFormat; + private int _bufferSize; + private ALFormat _alFormat; + + /// + /// Initializes a new ALPlayback class + /// + /// The device + public ALPlayback(ALDevice device) + { + Device = device; + _source = device.GenerateALSource(); + _locker = new object(); + PlaybackState = PlaybackState.Stopped; + } + + /// + /// Deconstructs the ALPlayback class + /// + ~ALPlayback() + { + Dispose(false); + } + + /// + /// Initializes the openal playback + /// + /// The stream + /// The format + public void Initialize(IWaveSource stream, WaveFormat format) + { + Initialize(stream, format, 150); + } + + /// + /// Initializes the openal playback + /// + /// The stream + /// The format + /// The latency + public void Initialize(IWaveSource stream, WaveFormat format, int latency) + { + _playbackStream = stream; + _waveFormat = stream.WaveFormat; + Latency = latency; + Length = stream.Length / format.BytesPerSecond * 1000; + _bufferSize = format.BytesPerSecond / 1000 * latency; + _alFormat = DetectAudioFormat(_waveFormat); + } + + /// + /// Starts the playback. + /// + public void Play() + { + if (PlaybackState == PlaybackState.Stopped) + { + _playbackThread = new Thread(PlaybackThread) {IsBackground = true}; + _playbackThread.Start(); + } + if (PlaybackState == PlaybackState.Paused) + { + lock (_locker) + { + Device.Context.MakeCurrent(); + ALInterops.alSourcePlay(_source.Id); + PlaybackState = PlaybackState.Playing; + RaisePlaybackChanged(); + } + } + } + + /// + /// Stops the playback. + /// + public void Stop() + { + lock (_locker) + { + Device.Context.MakeCurrent(); + ALInterops.alSourceStop(_source.Id); + PlaybackState = PlaybackState.Stopped; + RaisePlaybackChanged(); + } + } + + /// + /// Pause the playback. + /// + public void Pause() + { + lock (_locker) + { + Device.Context.MakeCurrent(); + ALInterops.alSourcePause(_source.Id); + PlaybackState = PlaybackState.Paused; + RaisePlaybackChanged(); + } + } + + /// + /// Resumes the playback. + /// + public void Resume() + { + lock (_locker) + { + Device.Context.MakeCurrent(); + ALInterops.alSourcePlay(_source.Id); + PlaybackState = PlaybackState.Playing; + RaisePlaybackChanged(); + } + } + + /// + /// Plays the stream + /// + private void PlaybackThread() + { + PlaybackState = PlaybackState.Playing; + RaisePlaybackChanged(); + + Device.Context.MakeCurrent(); + + var buffers = CreateBuffers(4); + + FillBuffers(buffers); + + ALInterops.alSourcePlay(_source.Id); + + try + { + while (_playbackStream.Position < _playbackStream.Length) + { + switch (PlaybackState) + { + case PlaybackState.Paused: + Thread.Sleep(Latency); + continue; + case PlaybackState.Stopped: + return; + } + + int finishedBuffersAmount; + ALInterops.alGetSourcei(_source.Id, ALSourceParameters.BuffersProcessed, out finishedBuffersAmount); + + if (finishedBuffersAmount == 0) + { + Thread.Sleep(Latency); + continue; + } + + var unqueuedBuffers = UnqueueBuffers(finishedBuffersAmount); + + FillBuffers(unqueuedBuffers); + + Position = _playbackStream.Position / _waveFormat.BytesPerSecond * 1000; + + int sourceState; + ALInterops.alGetSourcei(_source.Id, ALSourceParameters.SourceState, out sourceState); + if ((ALSourceState)sourceState == ALSourceState.Stopped) + { + ALInterops.alSourcePlay(_source.Id); + } + } + } + catch (Exception ex) + { + } + + PlaybackState = PlaybackState.Stopped; + RaisePlaybackChanged(); + } + + /// + /// Creates multiple openal buffers + /// + /// The amount + /// UInt Array + private uint[] CreateBuffers(int amount) + { + var bufferIds = new uint[amount]; + ALInterops.alGenBuffers(amount, bufferIds); + + return bufferIds; + } + + /// + /// Unqueues count buffers + /// + /// The buffers. + /// Count. + private uint[] UnqueueBuffers(int count) + { + var unqueueBuffers = new uint[count]; + ALInterops.alSourceUnqueueBuffers(_source.Id, count, unqueueBuffers); + return unqueueBuffers; + } + + /// + /// Fills the buffers from the playback stream + /// + /// Buffers. + private void FillBuffers(uint[] buffers) + { + for (int i = 0; i < buffers.Length; i++) + { + FillBuffer(buffers[i]); + } + } + + /// + /// Fills the buffer from the playback stream + /// + /// The buffer + private void FillBuffer(uint buffer) + { + var data = new byte[_bufferSize]; + + var dataLength = _playbackStream.Length - _playbackStream.Position < _bufferSize + ? _playbackStream.Read(data, 0, (int) (_playbackStream.Length - _playbackStream.Position)) + : _playbackStream.Read(data, 0, data.Length); + + if (dataLength == 0) return; + + ALInterops.alBufferData(buffer, _alFormat, data, dataLength, (uint)_waveFormat.SampleRate); + ALInterops.alSourceQueueBuffers(_source.Id, 1, new [] {buffer}); + } + + /// + /// Detects the openal format + /// + /// The wave format + /// ALFormat + private ALFormat DetectAudioFormat(WaveFormat format) + { + if (format.Channels > 1) + { + switch (format.BitsPerSample) + { + case 8: + return ALFormat.Stereo8Bit; + case 16: + return ALFormat.Stereo16Bit; + case 32: + return ALFormat.StereoFloat32Bit; + default: + throw new Exception("Unrecognized bitdepth requested: " + format.BitsPerSample); + } + } + else + { + switch (format.BitsPerSample) + { + case 8: + return ALFormat.Stereo8Bit; + case 16: + return ALFormat.Stereo16Bit; + case 32: + return ALFormat.StereoFloat32Bit; + default: + throw new Exception("Unrecognized bitdepth requested: " + format.BitsPerSample); + } + } + } + + /// + /// Raises the playback changed event + /// + private void RaisePlaybackChanged() + { + if (PlaybackChanged != null) + { + PlaybackChanged.Invoke(this, EventArgs.Empty); + } + } + + /// + /// Disposes the openal playback + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes the openal playback + /// + /// The disposing state + protected void Dispose(bool disposing) + { + Device.Context.MakeCurrent(); + + if (disposing) + { + _source.Dispose(); + } + + int finishedBuffersAmount; + ALInterops.alGetSourcei(_source.Id, ALSourceParameters.BuffersProcessed, out finishedBuffersAmount); + + var finishedBuffers = UnqueueBuffers (finishedBuffersAmount); + + ALInterops.alDeleteBuffers(finishedBuffersAmount, finishedBuffers); + } + } +} diff --git a/CSCore/SoundOut/AL/ALSource.cs b/CSCore/SoundOut/AL/ALSource.cs new file mode 100644 index 00000000..d33d8923 --- /dev/null +++ b/CSCore/SoundOut/AL/ALSource.cs @@ -0,0 +1,48 @@ +using System; + +namespace CSCore.SoundOut.AL +{ + internal class ALSource : IDisposable + { + /// + /// Gets the openal source id + /// + public uint Id { private set; get; } + + private readonly ALDevice _device; + + /// + /// Initializes a new ALSource class + /// + /// The device + /// The source id + public ALSource(ALDevice device, uint sourceId) + { + Id = sourceId; + _device = device; + } + + ~ALSource() + { + Dispose(false); + } + + /// + /// Disposes the openal source + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes the openal source + /// + /// The disposing state + protected void Dispose(bool disposing) + { + _device.DeleteALSource(this); + } + } +} diff --git a/CSCore/SoundOut/AL/ALSourceParameters.cs b/CSCore/SoundOut/AL/ALSourceParameters.cs new file mode 100644 index 00000000..336ca9ae --- /dev/null +++ b/CSCore/SoundOut/AL/ALSourceParameters.cs @@ -0,0 +1,96 @@ + +namespace CSCore.SoundOut.AL +{ + internal enum ALSourceParameters + { + /// + /// SourceState + /// + SourceState = 0x1010, + + /// + /// BuffersQueued + /// + BuffersQueued = 0x1015, + + /// + /// BuffersProcessed + /// + BuffersProcessed = 0x1016, + + /// + /// Pitch + /// + Pitch = 0x1003, + + /// + /// Position + /// + Position = 0x1004, + + /// + /// Direction + /// + Direction = 0x1005, + + /// + /// Velocity + /// + Velocity = 0x1006, + + /// + /// Gain + /// + Gain = 0x100A, + + /// + /// Min gain + /// + MinGain = 0x100D, + + /// + /// Max gain + /// + MaxGain = 0x100E, + + /// + /// Orientation + /// + Orientation = 0x100F, + + /// + /// Max distance + /// + MaxDistance = 0x1023, + + /// + /// Roll off factor + /// + RollOffFactor = 0x1021, + + /// + /// Cone outer gain + /// + ConeOuterGain = 0x1022, + + /// + /// Cone inner angle + /// + ConeInnerAngle = 0x1001, + + /// + /// Cone outer angle + /// + ConeOuterAngle = 0x1002, + + /// + /// Reference distance + /// + ReferenceDistance = 0x1020, + + /// + /// Source relative + /// + SourceRelative = 514 + } +} diff --git a/CSCore/SoundOut/AL/ALSourceState.cs b/CSCore/SoundOut/AL/ALSourceState.cs new file mode 100644 index 00000000..256777c7 --- /dev/null +++ b/CSCore/SoundOut/AL/ALSourceState.cs @@ -0,0 +1,26 @@ + +namespace CSCore.SoundOut.AL +{ + internal enum ALSourceState + { + /// + /// Initializing + /// + Initializing = 0x1011, + + /// + /// Playing + /// + Playing = 0x1012, + + /// + /// Paused + /// + Paused = 0x1013, + + /// + /// Stopped + /// + Stopped = 0x1014 + } +} diff --git a/CSCore/SoundOut/ALSoundOut.cs b/CSCore/SoundOut/ALSoundOut.cs new file mode 100644 index 00000000..0e81d110 --- /dev/null +++ b/CSCore/SoundOut/ALSoundOut.cs @@ -0,0 +1,204 @@ +using System; +using CSCore.SoundOut.AL; +using CSCore.Streams; + +namespace CSCore.SoundOut +{ + public class ALSoundOut : ISoundOut + { + public float Volume + { + get + { + if (_volumeSource != null) + { + return _volumeSource.Volume; + } + + return 0; + } + set + { + if (value < 0 || value > 1) + { + throw new ArgumentOutOfRangeException(); + } + + if (_volumeSource != null) + { + _volumeSource.Volume = value; + } + } + } + + public IWaveSource WaveSource { get; private set; } + + public PlaybackState PlaybackState + { + get + { + if (_alPlayback != null) + { + return _alPlayback.PlaybackState; + } + + return PlaybackState.Stopped; + } + } + + public event EventHandler Stopped; + + public int Latency { get; set; } + + private ALPlayback _alPlayback; + private VolumeSource _volumeSource; + private readonly ALDevice _alDevice; + + /// + /// Initializes a new ALSoundOut class with the default device and a latency of 150 ms + /// + public ALSoundOut() : this(ALDevice.DefaultDevice) + { + } + + /// + /// Initializes a new ALSoundOut class with a latency of 150 ms + /// + /// The openal device + public ALSoundOut(ALDevice device) + { + _alDevice = device; + _alDevice.Initialize(); + Latency = 150; + } + + ~ALSoundOut() + { + Dispose(false); + } + + /// + /// Plays the stream + /// + public void Play() + { + if (_alPlayback != null) + { + _alPlayback.Play(); + } + } + + /// + /// Resumes the stream + /// + public void Resume() + { + if (_alPlayback != null) + { + _alPlayback.Resume(); + } + } + + /// + /// Pause the stream + /// + public void Pause() + { + if (_alPlayback != null) + { + _alPlayback.Pause(); + } + } + + /// + /// Stops the stream + /// + public void Stop() + { + if (_alPlayback != null) + { + _alPlayback.Stop(); + } + } + + public void Initialize(IWaveSource source) + { + WaveSource = source; + _volumeSource = new VolumeSource(source.ToSampleSource()); + + if (_alPlayback != null) + { + _alPlayback.Stop(); + _alPlayback.Dispose(); + } + + _alPlayback = new ALPlayback(_alDevice); + _alPlayback.PlaybackChanged += PlaybackChanged; + + //choose right bit depth - openal possibly requires PCM format + int maxBitDepth = IsFloat32BitSupported() ? 32 : 16; + int bitDepth = 16; + switch (source.WaveFormat.BitsPerSample) + { + case 8: + bitDepth = 8; + break; + case 16: + bitDepth = 16; + break; + case 24: + case 32: + default: + bitDepth = maxBitDepth; + break; + } + _alPlayback.Initialize(_volumeSource.ToWaveSource(bitDepth), source.WaveFormat, Latency); + } + + private void PlaybackChanged(object sender, EventArgs e) + { + if (_alPlayback != null && _alPlayback.PlaybackState == PlaybackState.Stopped) + { + if (Stopped != null) + { + Stopped(this, new PlaybackStoppedEventArgs()); + } + } + } + + /// + /// Returns the last error code + /// + /// + public ALErrorCode GetLastError() + { + return _alDevice.GetLastError(); + } + + /// + /// Determines whether this OpenAL implementation supports float32bit audio format. + /// + /// true if this implementation supports float32bit; otherwise, false. + public bool IsFloat32BitSupported() + { + return ALInterops.IsExtensionPresent("AL_EXT_float32"); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (disposing) + { + if (_alPlayback != null) + { + _alPlayback.Dispose(); + } + } + } + } +}