WinmmOutputPlugin.cxx 7.18 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright 2003-2017 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
#include "util/RuntimeError.hxx"
27
#include "util/Macros.hxx"
28
#include "util/StringCompare.hxx"
29

30 31
#include <array>

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

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

	WAVEHDR hdr;
};

41
class WinmmOutput final : AudioOutput {
42
	const UINT device_id;
43 44 45 46 47 48 49 50
	HWAVEOUT handle;

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

51
	std::array<WinmmBuffer, 8> buffers;
52
	unsigned next_buffer;
53

54
public:
55
	WinmmOutput(const ConfigBlock &block);
56 57 58 59 60

	HWAVEOUT GetHandle() {
		return handle;
	}

61
	static AudioOutput *Create(EventLoop &, const ConfigBlock &block) {
62 63 64
		return new WinmmOutput(block);
	}

65 66 67
private:
	void Open(AudioFormat &audio_format) override;
	void Close() noexcept override;
68

69 70 71
	size_t Play(const void *chunk, size_t size) override;
	void Drain() override;
	void Cancel() noexcept override;
72 73 74 75 76 77 78 79 80

private:
	/**
	 * Wait until the buffer is finished.
	 */
	void DrainBuffer(WinmmBuffer &buffer);

	void DrainAllBuffers();

81
	void Stop() noexcept;
82

83 84
};

85 86
static std::runtime_error
MakeWaveOutError(MMRESULT result, const char *prefix)
87 88 89 90
{
	char buffer[256];
	if (waveOutGetErrorTextA(result, buffer,
				 ARRAY_SIZE(buffer)) == MMSYSERR_NOERROR)
91
		return FormatRuntimeError("%s: %s", prefix, buffer);
92
	else
93
		return std::runtime_error(prefix);
94 95
}

96
HWAVEOUT
97
winmm_output_get_handle(WinmmOutput &output)
98
{
99
	return output.GetHandle();
100 101
}

102
static bool
103
winmm_output_test_default_device(void)
104
{
105
	return waveOutGetNumDevs() > 0;
106 107
}

108 109
static UINT
get_device_id(const char *device_name)
110 111
{
	/* if device is not specified use wave mapper */
112 113
	if (device_name == nullptr)
		return WAVE_MAPPER;
114 115

	UINT numdevs = waveOutGetNumDevs();
116 117 118 119

	/* check for device id */
	char *endptr;
	UINT id = strtoul(device_name, &endptr, 0);
120
	if (endptr > device_name && *endptr == 0) {
121 122 123 124 125
		if (id >= numdevs)
			throw FormatRuntimeError("device \"%s\" is not found",
						 device_name);

		return id;
126
	}
127 128

	/* check for device name */
129
	const AllocatedPath device_name_fs =
130
		AllocatedPath::FromUTF8Throw(device_name);
131

132
	for (UINT i = 0; i < numdevs; i++) {
133 134 135 136 137 138
		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. */
139 140
		if (StringStartsWith(device_name_fs.c_str(), caps.szPname))
			return i;
141 142
	}

143
	throw FormatRuntimeError("device \"%s\" is not found", device_name);
144 145
}

146
WinmmOutput::WinmmOutput(const ConfigBlock &block)
147
	:AudioOutput(0),
148
	 device_id(get_device_id(block.GetBlockValue("device")))
149
{
150
}
151

152 153
void
WinmmOutput::Open(AudioFormat &audio_format)
154
{
155 156
	event = CreateEvent(nullptr, false, false, nullptr);
	if (event == nullptr)
157
		throw std::runtime_error("CreateEvent() failed");
158

159 160
	switch (audio_format.format) {
	case SampleFormat::S16:
161 162
		break;

163
	case SampleFormat::S8:
164 165 166 167 168
	case SampleFormat::S24_P32:
	case SampleFormat::S32:
	case SampleFormat::FLOAT:
	case SampleFormat::DSD:
	case SampleFormat::UNDEFINED:
169
		/* we havn't tested formats other than S16 */
170
		audio_format.format = SampleFormat::S16;
171 172 173
		break;
	}

174
	if (audio_format.channels > 2)
175
		/* same here: more than stereo was not tested */
176
		audio_format.channels = 2;
177 178 179

	WAVEFORMATEX format;
	format.wFormatTag = WAVE_FORMAT_PCM;
180 181 182
	format.nChannels = audio_format.channels;
	format.nSamplesPerSec = audio_format.sample_rate;
	format.nBlockAlign = audio_format.GetFrameSize();
183
	format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign;
184
	format.wBitsPerSample = audio_format.GetSampleSize() * 8;
185 186
	format.cbSize = 0;

187 188
	MMRESULT result = waveOutOpen(&handle, device_id, &format,
				      (DWORD_PTR)event, 0, CALLBACK_EVENT);
189
	if (result != MMSYSERR_NOERROR) {
190
		CloseHandle(event);
191
		throw MakeWaveOutError(result, "waveOutOpen() failed");
192 193
	}

194 195
	for (auto &i : buffers)
		memset(&i.hdr, 0, sizeof(i.hdr));
196

197
	next_buffer = 0;
198 199
}

200
void
201
WinmmOutput::Close() noexcept
202
{
203 204
	for (auto &i : buffers)
		i.buffer.Clear();
205

206
	waveOutClose(handle);
207

208
	CloseHandle(event);
209 210 211 212 213
}

/**
 * Copy data into a buffer, and prepare the wave header.
 */
214
static void
215
winmm_set_buffer(HWAVEOUT handle, WinmmBuffer *buffer,
216
		 const void *data, size_t size)
217
{
218
	void *dest = buffer->buffer.Get(size);
219
	assert(dest != nullptr);
220 221 222 223

	memcpy(dest, data, size);

	memset(&buffer->hdr, 0, sizeof(buffer->hdr));
224
	buffer->hdr.lpData = (LPSTR)dest;
225 226
	buffer->hdr.dwBufferLength = size;

227
	MMRESULT result = waveOutPrepareHeader(handle, &buffer->hdr,
228
					       sizeof(buffer->hdr));
229 230 231
	if (result != MMSYSERR_NOERROR)
		throw MakeWaveOutError(result,
				       "waveOutPrepareHeader() failed");
232 233
}

234 235
void
WinmmOutput::DrainBuffer(WinmmBuffer &buffer)
236
{
237
	if ((buffer.hdr.dwFlags & WHDR_DONE) == WHDR_DONE)
238
		/* already finished */
239
		return;
240 241

	while (true) {
242 243 244
		MMRESULT result = waveOutUnprepareHeader(handle,
							 &buffer.hdr,
							 sizeof(buffer.hdr));
245
		if (result == MMSYSERR_NOERROR)
246 247 248 249
			return;
		else if (result != WAVERR_STILLPLAYING)
			throw MakeWaveOutError(result,
					       "waveOutUnprepareHeader() failed");
250 251

		/* wait some more */
252
		WaitForSingleObject(event, INFINITE);
253 254 255
	}
}

256
size_t
257
WinmmOutput::Play(const void *chunk, size_t size)
258 259
{
	/* get the next buffer from the ring and prepare it */
260 261 262
	WinmmBuffer *buffer = &buffers[next_buffer];
	DrainBuffer(*buffer);
	winmm_set_buffer(handle, buffer, chunk, size);
263 264

	/* enqueue the buffer */
265
	MMRESULT result = waveOutWrite(handle, &buffer->hdr,
266 267
				       sizeof(buffer->hdr));
	if (result != MMSYSERR_NOERROR) {
268
		waveOutUnprepareHeader(handle, &buffer->hdr,
269
				       sizeof(buffer->hdr));
270
		throw MakeWaveOutError(result, "waveOutWrite() failed");
271 272 273
	}

	/* mark our buffer as "used" */
274
	next_buffer = (next_buffer + 1) % buffers.size();
275 276 277 278

	return size;
}

279 280
void
WinmmOutput::DrainAllBuffers()
281
{
282
	for (unsigned i = next_buffer; i < buffers.size(); ++i)
283
		DrainBuffer(buffers[i]);
284

285 286
	for (unsigned i = 0; i < next_buffer; ++i)
		DrainBuffer(buffers[i]);
287 288
}

289
void
290
WinmmOutput::Stop() noexcept
291
{
292
	waveOutReset(handle);
293

294 295
	for (auto &i : buffers)
		waveOutUnprepareHeader(handle, &i.hdr, sizeof(i.hdr));
296 297
}

298 299
void
WinmmOutput::Drain()
300
{
301
	try {
302
		DrainAllBuffers();
303
	} catch (...) {
304
		Stop();
305 306
		throw;
	}
307 308
}

309
void
310
WinmmOutput::Cancel() noexcept
311
{
312
	Stop();
313 314
}

315
const struct AudioOutputPlugin winmm_output_plugin = {
316 317
	"winmm",
	winmm_output_test_default_device,
318
	WinmmOutput::Create,
319
	&winmm_mixer_plugin,
320
};