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 }