1 module iota.audio.wasapi; 2 3 version (Windows): 4 5 package import iota.audio.backend.windows; 6 7 import iota.audio.device; 8 import iota.audio.output; 9 10 //import core.thread; 11 12 import core.sys.windows.windows; 13 import core.sys.windows.objidl; 14 import core.sys.windows.wtypes; 15 16 import core.time; 17 18 import core.thread; 19 20 package IMMDeviceEnumerator immEnum; 21 package IMMDeviceCollection deviceList; 22 23 shared static ~this() { 24 if (immEnum !is null) immEnum.Release(); 25 if (deviceList !is null) deviceList.Release(); 26 CoUninitialize(); 27 } 28 /** 29 * Contains the last error code returned during driver or device initialization. 30 */ 31 public static HRESULT lastErrorCode; 32 33 package int initDriverWASAPI() { 34 if (immEnum) { 35 lastErrorCode = immEnum.EnumAudioEndpoints(EDataFlow.eRender, DEVICE_STATE_ACTIVE, deviceList); 36 switch (lastErrorCode) { 37 case S_OK: 38 if (deviceList !is null){ 39 initializedDriver = DriverType.WASAPI; 40 return AudioInitializationStatus.AllOk; 41 } else { 42 return AudioInitializationStatus.UninitializedDriver; 43 } 44 case E_OUTOFMEMORY: return AudioInitializationStatus.OutOfMemory; 45 case E_POINTER, E_INVALIDARG: return AudioInitializationStatus.InternalError; 46 default: return AudioInitializationStatus.Unknown; 47 } 48 } else return AudioInitializationStatus.DriverNotAvailable; 49 } 50 /** 51 * Implements a WASAPI device, and can create both input and output streams for such devices. 52 */ 53 public class WASAPIDevice : AudioDevice { 54 /// References the backend 55 protected IMMDevice backend; 56 /// Contains the properties of this device 57 protected IPropertyStore devProperties; 58 /// The audio client. Initialized upon a successful audiospecs request. 59 protected IAudioClient audioClient; 60 /// Windows specific audio format. 61 protected WAVEFORMATEX waudioSpecs; 62 /// Windows specific error codes 63 public HRESULT werrCode; 64 /** 65 * Creates an instance that refers to the backend. 66 */ 67 package this(IMMDevice backend) { 68 this.backend = backend; 69 werrCode = backend.OpenPropertyStore(STGM_READ, devProperties); 70 if (werrCode == S_OK) { 71 72 } 73 this._nOfOutputs = -1; 74 this._nOfInputs = -1; 75 } 76 ~this() { 77 if (backend !is null) 78 backend.Release(); 79 if (devProperties !is null) 80 devProperties.Release(); 81 if (audioClient !is null) 82 audioClient.Release(); 83 } 84 /** 85 * 86 */ 87 public @property AudioShareMode shareMode(AudioShareMode val) @nogc @safe pure nothrow { 88 return _shareMode = val; 89 } 90 /** 91 * Sets the device's audio specifications to the closest possible specifications. 92 * Params: 93 * reqSpecs = The requested audio secifications. Can be set AudioSpecs.init if don't care. 94 * flags = Tells the function what specifications are must or can be compromised. 95 * Returns: The actual audio specs, or AudioSpecs.init in case of failure. 96 * In case of a failure, `errCode` is also set with the corresponding flags. 97 */ 98 public override AudioSpecs requestSpecs(AudioSpecs reqSpecs, int flags = 0) { 99 reqSpecs.mirrorBufferSizes(); 100 if (reqSpecs.outputChannels) { 101 werrCode = backend.Activate(IID_IAudioClient, CLSCTX_ALL, null, cast(void**)&audioClient); 102 if (werrCode != S_OK) { 103 switch (werrCode) { 104 case E_NOINTERFACE: errCode = AudioInitializationStatus.DriverNotAvailable; break; 105 case AUDCLNT_E_DEVICE_INVALIDATED: errCode = AudioInitializationStatus.DeviceNotFound; break; 106 case E_OUTOFMEMORY: errCode = AudioInitializationStatus.OutOfMemory; break; 107 case E_INVALIDARG, E_POINTER: errCode = AudioInitializationStatus.InternalError; break; 108 default: errCode = AudioInitializationStatus.Unknown; break; 109 } 110 return AudioSpecs.init; 111 } 112 waudioSpecs.wFormatTag = (reqSpecs.format.flags & SampleFormat.Flag.Type_Test) != SampleFormat.Flag.Type_Float ? 113 WAVE_FORMAT_PCM : WAVE_FORMAT_IEEE_FLOAT; 114 waudioSpecs.nSamplesPerSec = reqSpecs.sampleRate; 115 waudioSpecs.nChannels = reqSpecs.outputChannels; 116 waudioSpecs.wBitsPerSample = reqSpecs.format.bits; 117 waudioSpecs.nBlockAlign = cast(ushort)((waudioSpecs.wBitsPerSample / 8) * reqSpecs.outputChannels); 118 waudioSpecs.nAvgBytesPerSec = waudioSpecs.nBlockAlign * waudioSpecs.nSamplesPerSec; 119 WAVEFORMATEX* closestMatch; 120 werrCode = audioClient.IsFormatSupported( 121 _shareMode == AudioShareMode.Shared ? AUDCLNT_SHAREMODE.AUDCLNT_SHAREMODE_SHARED : 122 AUDCLNT_SHAREMODE.AUDCLNT_SHAREMODE_EXCLUSIVE, &waudioSpecs, &closestMatch); 123 switch (werrCode) { 124 case S_OK: break; 125 case S_FALSE, AUDCLNT_E_UNSUPPORTED_FORMAT: 126 waudioSpecs = *closestMatch; 127 reqSpecs.sampleRate = closestMatch.nSamplesPerSec; 128 reqSpecs.format.bits = cast(ubyte)closestMatch.wBitsPerSample; 129 reqSpecs.mirrorBufferSizes(); 130 break; 131 case E_POINTER, E_INVALIDARG: 132 errCode = AudioInitializationStatus.InternalError; 133 return AudioSpecs.init; 134 case AUDCLNT_E_DEVICE_INVALIDATED: 135 errCode = AudioInitializationStatus.DeviceNotFound; 136 return AudioSpecs.init; 137 case AUDCLNT_E_SERVICE_NOT_RUNNING: 138 errCode = AudioInitializationStatus.DriverNotAvailable; 139 return AudioSpecs.init; 140 default: 141 errCode = AudioInitializationStatus.Unknown; 142 break; 143 } 144 CoTaskMemFree(closestMatch); 145 } 146 return _specs = reqSpecs; 147 } 148 /** 149 * Creates an OutputStream specific to the given driver/device, with the requested parameters, then returns it. Or 150 * null in case of an error. 151 */ 152 public override OutputStream createOutputStream() nothrow { 153 if (audioClient !is null) { 154 werrCode = audioClient.Initialize( 155 _shareMode == AudioShareMode.Shared ? AUDCLNT_SHAREMODE.AUDCLNT_SHAREMODE_SHARED : 156 AUDCLNT_SHAREMODE.AUDCLNT_SHAREMODE_EXCLUSIVE, 157 AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_EVENTCALLBACK, 158 _specs.bufferSize_time.total!"hnsecs", _specs.bufferSize_time.total!"hnsecs", &waudioSpecs, null); 159 if (werrCode != S_OK) { 160 switch (werrCode) { 161 case E_OUTOFMEMORY: errCode = StreamInitializationStatus.OutOfMemory; break; 162 163 default: errCode = StreamInitializationStatus.Unknown; break; 164 } 165 return null; 166 } 167 IAudioClient temp = audioClient; 168 audioClient = null; 169 return new WASAPIOutputStream(temp, _specs.outputChannels * _specs.bits, _specs.bufferSize_slmp); 170 } else 171 return null; 172 } 173 } 174 175 public class WASAPIOutputStream : OutputStream { 176 protected uint bufferSize; 177 protected uint frameSize; 178 protected IAudioClient backend; 179 protected IAudioRenderClient buffer; 180 protected HANDLE eventHandle; 181 public HRESULT werrCode; 182 package this(IAudioClient backend, uint frameSize, uint bufferSize) nothrow @nogc { 183 this.backend = backend; 184 this.frameSize = frameSize; 185 eventHandle = CreateEvent(null, FALSE, FALSE, null); 186 werrCode = backend.SetEventHandle(eventHandle); 187 switch (werrCode) { 188 case S_OK: break; 189 case AUDCLNT_E_EVENTHANDLE_NOT_EXPECTED, AUDCLNT_E_NOT_INITIALIZED: 190 errCode = StreamRuntimeStatus.InternalError; return; 191 case AUDCLNT_E_DEVICE_INVALIDATED: errCode = StreamRuntimeStatus.DeviceNotFound; return; 192 case AUDCLNT_E_SERVICE_NOT_RUNNING: errCode = StreamRuntimeStatus.NoAudioService; return; 193 default: errCode = StreamRuntimeStatus.Unknown; break; 194 } 195 this.bufferSize = bufferSize; 196 /+werrCode = backend.GetBufferSize(bufferSize); 197 switch (werrCode) { 198 case AUDCLNT_E_NOT_INITIALIZED, E_POINTER: errCode = StreamRuntimeStatus.InternalError; return; 199 case AUDCLNT_E_DEVICE_INVALIDATED: errCode = StreamRuntimeStatus.DeviceNotFound; return; 200 case AUDCLNT_E_SERVICE_NOT_RUNNING: errCode = StreamRuntimeStatus.NoAudioService; return; 201 case S_OK: break; 202 default: errCode = StreamRuntimeStatus.Unknown; break; 203 } 204 UINT32 padding; 205 backend.GetCurrentPadding(padding); 206 bufferSize -= padding;+/ 207 werrCode = backend.GetService(IID_IAudioRenderClient, cast(void**)&buffer); 208 switch (werrCode) { 209 case E_POINTER, AUDCLNT_E_NOT_INITIALIZED, AUDCLNT_E_WRONG_ENDPOINT_TYPE: 210 errCode = StreamRuntimeStatus.InternalError; 211 return; 212 case AUDCLNT_E_DEVICE_INVALIDATED: errCode = StreamRuntimeStatus.DeviceNotFound; return; 213 case AUDCLNT_E_SERVICE_NOT_RUNNING: errCode = StreamRuntimeStatus.NoAudioService; return; 214 case S_OK: break; 215 default: errCode = StreamRuntimeStatus.Unknown; break; 216 } 217 } 218 ~this() { 219 if (buffer !is null) buffer.Release(); 220 if (backend !is null) backend.Release(); 221 CloseHandle(eventHandle); 222 } 223 protected void audioThread() @nogc nothrow { 224 const size_t cbufferSize = bufferSize * (frameSize / 8); 225 while (statusCode & StatusFlags.IsRunning) { 226 werrCode = WaitForSingleObject(eventHandle, 500); 227 if (werrCode != WAIT_OBJECT_0) { 228 backend.Stop(); 229 errCode = StreamRuntimeStatus.Unknown; 230 } 231 ubyte* data; 232 do { // To do: This is a bit janky, and there's like some glitches in the test audio. 233 werrCode = buffer.GetBuffer(bufferSize, data); 234 if (werrCode == AUDCLNT_E_BUFFER_TOO_LARGE) statusCode |= StatusFlags.BufferOverrun; //Set buffer overrun flag if protection was engaged. 235 } while (werrCode == AUDCLNT_E_BUFFER_TOO_LARGE && (statusCode & StatusFlags.IsRunning)); 236 switch (werrCode) { 237 case AUDCLNT_E_BUFFER_TOO_LARGE: return; 238 case AUDCLNT_E_BUFFER_ERROR, AUDCLNT_E_BUFFER_SIZE_ERROR, AUDCLNT_E_OUT_OF_ORDER, 239 AUDCLNT_E_BUFFER_OPERATION_PENDING: 240 errCode = StreamRuntimeStatus.InternalError; 241 backend.Stop(); 242 return; 243 case AUDCLNT_E_DEVICE_INVALIDATED: 244 errCode = StreamRuntimeStatus.DeviceNotFound; 245 return; 246 case S_OK: break; 247 default: errCode = StreamRuntimeStatus.Unknown; break; 248 } 249 callback_buffer(data[0..cbufferSize]); 250 werrCode = buffer.ReleaseBuffer(bufferSize, 0); 251 switch (werrCode) { 252 case AUDCLNT_E_INVALID_SIZE, AUDCLNT_E_BUFFER_SIZE_ERROR, AUDCLNT_E_OUT_OF_ORDER: 253 errCode = StreamRuntimeStatus.InternalError; 254 backend.Stop(); 255 return; 256 case AUDCLNT_E_DEVICE_INVALIDATED: 257 errCode = StreamRuntimeStatus.DeviceNotFound; 258 backend.Stop(); 259 return; 260 case S_OK: break; 261 default: errCode = StreamRuntimeStatus.Unknown; break; 262 } 263 ResetEvent(eventHandle); 264 } 265 } 266 /** 267 * Runs the audio thread. This either means that it'll create a new low-level thread to feed the stream a steady 268 * amount of data, or use whatever backend the OS has. 269 * Returns: 0, or an error code if there was a failure. 270 */ 271 public override int runAudioThread() @nogc nothrow { 272 if (callback_buffer is null) return errCode = StreamRuntimeStatus.BufferCallbackNotSet; 273 werrCode = backend.Start(); 274 if (werrCode != S_OK) { 275 276 } 277 statusCode |= StatusFlags.IsRunning; 278 _threadID = createLowLevelThread(&audioThread); 279 return errCode = StreamRuntimeStatus.AllOk; 280 } 281 /** 282 * Suspends the audio thread by allowing it to escape normally and close things safely, or suspending it on the 283 * backend. 284 * Returns: 0, or an error code if there was a failure. 285 * Note: It's wise to put this function into a mutex, or a `synchronized` block. 286 */ 287 public override int suspendAudioThread() @nogc nothrow { 288 if (errCode) { 289 backend.Stop(); 290 return errCode; 291 } else { 292 werrCode = backend.Stop(); 293 if (werrCode != S_OK) { 294 295 } 296 statusCode &= ~StatusFlags.IsRunning; 297 return errCode = StreamRuntimeStatus.AllOk; 298 } 299 } 300 }