WinmmOutputPlugin.cxx 8.52 KB
Newer Older
1
/*
2
 * Copyright 2003-2016 The Music Player Daemon Project
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
 * http://www.musicpd.org
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include "config.h"
21
#include "WinmmOutputPlugin.hxx"
22
#include "../OutputAPI.hxx"
23
#include "pcm/PcmBuffer.hxx"
Max Kellermann's avatar
Max Kellermann committed
24
#include "mixer/MixerList.hxx"
25
#include "fs/AllocatedPath.hxx"
26 27
#include "util/Error.hxx"
#include "util/Domain.hxx"
28
#include "util/Macros.hxx"
29
#include "util/StringCompare.hxx"
30

31 32
#include <stdlib.h>
#include <string.h>
33

34
struct WinmmBuffer {
35
	PcmBuffer buffer;
36 37 38 39

	WAVEHDR hdr;
};

40
struct WinmmOutput {
41
	AudioOutput base;
42

43
	UINT device_id;
44 45 46 47 48 49 50 51
	HWAVEOUT handle;

	/**
	 * This event is triggered by Windows when a buffer is
	 * finished.
	 */
	HANDLE event;

52
	WinmmBuffer buffers[8];
53
	unsigned next_buffer;
54

55
	WinmmOutput()
56
		:base(winmm_output_plugin) {}
57 58
};

59
static constexpr Domain winmm_output_domain("winmm_output");
60

61 62 63 64 65 66 67 68 69 70 71 72
static void
SetWaveOutError(Error &error, MMRESULT result, const char *prefix)
{
	char buffer[256];
	if (waveOutGetErrorTextA(result, buffer,
				 ARRAY_SIZE(buffer)) == MMSYSERR_NOERROR)
		error.Format(winmm_output_domain, int(result),
			     "%s: %s", prefix, buffer);
	else
		error.Set(winmm_output_domain, int(result), prefix);
}

73
HWAVEOUT
74
winmm_output_get_handle(WinmmOutput &output)
75
{
76
	return output.handle;
77 78
}

79
static bool
80
winmm_output_test_default_device(void)
81
{
82
	return waveOutGetNumDevs() > 0;
83 84
}

85
static bool
86
get_device_id(const char *device_name, UINT *device_id, Error &error)
87 88
{
	/* if device is not specified use wave mapper */
89
	if (device_name == nullptr) {
90 91 92 93 94
		*device_id = WAVE_MAPPER;
		return true;
	}

	UINT numdevs = waveOutGetNumDevs();
95 96 97 98

	/* check for device id */
	char *endptr;
	UINT id = strtoul(device_name, &endptr, 0);
99
	if (endptr > device_name && *endptr == 0) {
100 101 102 103 104 105 106
		if (id >= numdevs) {
			error.Format(winmm_output_domain,
				     "device \"%s\" is not found",
				     device_name);
			return false;
		}

107 108 109
		*device_id = id;
		return true;
	}
110 111

	/* check for device name */
112 113 114 115 116
	const AllocatedPath device_name_fs =
		AllocatedPath::FromUTF8(device_name, error);
	if (device_name_fs.IsNull())
		return false;

117
	for (UINT i = 0; i < numdevs; i++) {
118 119 120 121 122 123
		WAVEOUTCAPS caps;
		MMRESULT result = waveOutGetDevCaps(i, &caps, sizeof(caps));
		if (result != MMSYSERR_NOERROR)
			continue;
		/* szPname is only 32 chars long, so it is often truncated.
		   Use partial match to work around this. */
124
		if (StringStartsWith(device_name_fs.c_str(), caps.szPname)) {
125 126 127
			*device_id = i;
			return true;
		}
128 129
	}

130 131
	error.Format(winmm_output_domain,
		     "device \"%s\" is not found", device_name);
132
	return false;
133 134
}

135
static AudioOutput *
136
winmm_output_init(const ConfigBlock &block, Error &error)
137
{
138
	WinmmOutput *wo = new WinmmOutput();
139
	if (!wo->base.Configure(block, error)) {
140
		delete wo;
141
		return nullptr;
142 143
	}

144
	const char *device = block.GetBlockValue("device");
145
	if (!get_device_id(device, &wo->device_id, error)) {
146
		delete wo;
147
		return nullptr;
148 149
	}

150
	return &wo->base;
151 152 153
}

static void
154
winmm_output_finish(AudioOutput *ao)
155
{
156
	WinmmOutput *wo = (WinmmOutput *)ao;
157

158
	delete wo;
159 160 161
}

static bool
162
winmm_output_open(AudioOutput *ao, AudioFormat &audio_format,
163
		  Error &error)
164
{
165
	WinmmOutput *wo = (WinmmOutput *)ao;
166

167 168
	wo->event = CreateEvent(nullptr, false, false, nullptr);
	if (wo->event == nullptr) {
169
		error.Set(winmm_output_domain, "CreateEvent() failed");
170 171 172
		return false;
	}

173 174 175
	switch (audio_format.format) {
	case SampleFormat::S8:
	case SampleFormat::S16:
176 177
		break;

178 179 180 181 182
	case SampleFormat::S24_P32:
	case SampleFormat::S32:
	case SampleFormat::FLOAT:
	case SampleFormat::DSD:
	case SampleFormat::UNDEFINED:
183
		/* we havn't tested formats other than S16 */
184
		audio_format.format = SampleFormat::S16;
185 186 187
		break;
	}

188
	if (audio_format.channels > 2)
189
		/* same here: more than stereo was not tested */
190
		audio_format.channels = 2;
191 192 193

	WAVEFORMATEX format;
	format.wFormatTag = WAVE_FORMAT_PCM;
194 195 196
	format.nChannels = audio_format.channels;
	format.nSamplesPerSec = audio_format.sample_rate;
	format.nBlockAlign = audio_format.GetFrameSize();
197
	format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign;
198
	format.wBitsPerSample = audio_format.GetSampleSize() * 8;
199 200
	format.cbSize = 0;

201
	MMRESULT result = waveOutOpen(&wo->handle, wo->device_id, &format,
202 203 204
				      (DWORD_PTR)wo->event, 0, CALLBACK_EVENT);
	if (result != MMSYSERR_NOERROR) {
		CloseHandle(wo->event);
205
		SetWaveOutError(error, result, "waveOutOpen() failed");
206 207 208
		return false;
	}

209
	for (unsigned i = 0; i < ARRAY_SIZE(wo->buffers); ++i) {
210 211 212 213 214 215 216 217 218
		memset(&wo->buffers[i].hdr, 0, sizeof(wo->buffers[i].hdr));
	}

	wo->next_buffer = 0;

	return true;
}

static void
219
winmm_output_close(AudioOutput *ao)
220
{
221
	WinmmOutput *wo = (WinmmOutput *)ao;
222

223
	for (unsigned i = 0; i < ARRAY_SIZE(wo->buffers); ++i)
224
		wo->buffers[i].buffer.Clear();
225 226 227 228 229 230 231 232 233 234

	waveOutClose(wo->handle);

	CloseHandle(wo->event);
}

/**
 * Copy data into a buffer, and prepare the wave header.
 */
static bool
235
winmm_set_buffer(WinmmOutput *wo, WinmmBuffer *buffer,
236
		 const void *data, size_t size,
237
		 Error &error)
238
{
239
	void *dest = buffer->buffer.Get(size);
240
	assert(dest != nullptr);
241 242 243 244

	memcpy(dest, data, size);

	memset(&buffer->hdr, 0, sizeof(buffer->hdr));
245
	buffer->hdr.lpData = (LPSTR)dest;
246 247 248 249 250
	buffer->hdr.dwBufferLength = size;

	MMRESULT result = waveOutPrepareHeader(wo->handle, &buffer->hdr,
					       sizeof(buffer->hdr));
	if (result != MMSYSERR_NOERROR) {
251 252
		SetWaveOutError(error, result,
				"waveOutPrepareHeader() failed");
253 254 255 256 257 258 259 260 261 262
		return false;
	}

	return true;
}

/**
 * Wait until the buffer is finished.
 */
static bool
263
winmm_drain_buffer(WinmmOutput *wo, WinmmBuffer *buffer,
264
		   Error &error)
265 266 267 268 269 270 271 272 273 274 275 276
{
	if ((buffer->hdr.dwFlags & WHDR_DONE) == WHDR_DONE)
		/* already finished */
		return true;

	while (true) {
		MMRESULT result = waveOutUnprepareHeader(wo->handle,
							 &buffer->hdr,
							 sizeof(buffer->hdr));
		if (result == MMSYSERR_NOERROR)
			return true;
		else if (result != WAVERR_STILLPLAYING) {
277 278
			SetWaveOutError(error, result,
					"waveOutUnprepareHeader() failed");
279 280 281 282 283 284 285 286 287
			return false;
		}

		/* wait some more */
		WaitForSingleObject(wo->event, INFINITE);
	}
}

static size_t
288
winmm_output_play(AudioOutput *ao, const void *chunk, size_t size, Error &error)
289
{
290
	WinmmOutput *wo = (WinmmOutput *)ao;
291 292

	/* get the next buffer from the ring and prepare it */
293
	WinmmBuffer *buffer = &wo->buffers[wo->next_buffer];
294 295
	if (!winmm_drain_buffer(wo, buffer, error) ||
	    !winmm_set_buffer(wo, buffer, chunk, size, error))
296 297 298 299 300 301 302 303
		return 0;

	/* enqueue the buffer */
	MMRESULT result = waveOutWrite(wo->handle, &buffer->hdr,
				       sizeof(buffer->hdr));
	if (result != MMSYSERR_NOERROR) {
		waveOutUnprepareHeader(wo->handle, &buffer->hdr,
				       sizeof(buffer->hdr));
304
		SetWaveOutError(error, result, "waveOutWrite() failed");
305 306 307 308 309
		return 0;
	}

	/* mark our buffer as "used" */
	wo->next_buffer = (wo->next_buffer + 1) %
310
		ARRAY_SIZE(wo->buffers);
311 312 313 314 315

	return size;
}

static bool
316
winmm_drain_all_buffers(WinmmOutput *wo, Error &error)
317
{
318
	for (unsigned i = wo->next_buffer; i < ARRAY_SIZE(wo->buffers); ++i)
319
		if (!winmm_drain_buffer(wo, &wo->buffers[i], error))
320 321 322
			return false;

	for (unsigned i = 0; i < wo->next_buffer; ++i)
323
		if (!winmm_drain_buffer(wo, &wo->buffers[i], error))
324 325 326 327 328 329
			return false;

	return true;
}

static void
330
winmm_stop(WinmmOutput *wo)
331 332 333
{
	waveOutReset(wo->handle);

334
	for (unsigned i = 0; i < ARRAY_SIZE(wo->buffers); ++i) {
335
		WinmmBuffer *buffer = &wo->buffers[i];
336 337 338 339 340 341
		waveOutUnprepareHeader(wo->handle, &buffer->hdr,
				       sizeof(buffer->hdr));
	}
}

static void
342
winmm_output_drain(AudioOutput *ao)
343
{
344
	WinmmOutput *wo = (WinmmOutput *)ao;
345

346
	if (!winmm_drain_all_buffers(wo, IgnoreError()))
347
		winmm_stop(wo);
348 349 350
}

static void
351
winmm_output_cancel(AudioOutput *ao)
352
{
353
	WinmmOutput *wo = (WinmmOutput *)ao;
354

355
	winmm_stop(wo);
356 357
}

358
const struct AudioOutputPlugin winmm_output_plugin = {
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373
	"winmm",
	winmm_output_test_default_device,
	winmm_output_init,
	winmm_output_finish,
	nullptr,
	nullptr,
	winmm_output_open,
	winmm_output_close,
	nullptr,
	nullptr,
	winmm_output_play,
	winmm_output_drain,
	winmm_output_cancel,
	nullptr,
	&winmm_mixer_plugin,
374
};