WinmmOutputPlugin.cxx 7.16 KB
Newer Older
1
/*
2
 * Copyright 2003-2018 The Music Player Daemon Project
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
 * 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.
 */

20
#include "WinmmOutputPlugin.hxx"
21
#include "../OutputAPI.hxx"
22
#include "pcm/PcmBuffer.hxx"
Max Kellermann's avatar
Max Kellermann committed
23
#include "mixer/MixerList.hxx"
24
#include "fs/AllocatedPath.hxx"
25
#include "util/RuntimeError.hxx"
26
#include "util/Macros.hxx"
27
#include "util/StringCompare.hxx"
28

29 30
#include <array>

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

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

	WAVEHDR hdr;
};

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

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

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

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

	HWAVEOUT GetHandle() {
		return handle;
	}

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

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

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

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

	void DrainAllBuffers();

80
	void Stop() noexcept;
81

82 83
};

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

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

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

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

	UINT numdevs = waveOutGetNumDevs();
115 116 117 118

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

		return id;
125
	}
126 127

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

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

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

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

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

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

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

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

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

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

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

196
	next_buffer = 0;
197 198
}

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

205
	waveOutClose(handle);
206

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

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

	memcpy(dest, data, size);

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

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

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

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

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

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

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

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

	return size;
}

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

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

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

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

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

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

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