RecorderOutputPlugin.cxx 7.52 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
 * 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 "config.h"
21
#include "RecorderOutputPlugin.hxx"
22
#include "../OutputAPI.hxx"
23
#include "../Wrapper.hxx"
24
#include "tag/Format.hxx"
25
#include "encoder/ToOutputStream.hxx"
26
#include "encoder/EncoderInterface.hxx"
27 28
#include "encoder/EncoderPlugin.hxx"
#include "encoder/EncoderList.hxx"
29
#include "config/ConfigError.hxx"
30
#include "config/ConfigPath.hxx"
31
#include "Log.hxx"
32
#include "fs/AllocatedPath.hxx"
33
#include "fs/io/FileOutputStream.hxx"
34
#include "util/RuntimeError.hxx"
35
#include "util/Domain.hxx"
36 37 38
#include "util/ScopeExit.hxx"

#include <stdexcept>
39 40

#include <assert.h>
41 42 43
#include <stdlib.h>

static constexpr Domain recorder_domain("recorder");
44

45 46 47
class RecorderOutput {
	friend struct AudioOutputWrapper<RecorderOutput>;

48
	AudioOutput base;
49

50 51 52
	/**
	 * The configured encoder plugin.
	 */
53 54
	PreparedEncoder *prepared_encoder = nullptr;
	Encoder *encoder;
55 56 57 58

	/**
	 * The destination file name.
	 */
59
	AllocatedPath path = AllocatedPath::Null();
60

61 62 63 64 65 66 67 68 69 70 71 72
	/**
	 * A string that will be used with FormatTag() to build the
	 * destination path.
	 */
	std::string format_path;

	/**
	 * The #AudioFormat that is currently active.  This is used
	 * for switching to another file.
	 */
	AudioFormat effective_audio_format;

73
	/**
74
	 * The destination file.
75
	 */
76
	FileOutputStream *file;
77

78
	RecorderOutput(const ConfigBlock &block);
79

80
	~RecorderOutput() {
81
		delete prepared_encoder;
82 83
	}

84 85
	static RecorderOutput *Create(EventLoop &event_loop,
				      const ConfigBlock &block);
86

87
	void Open(AudioFormat &audio_format);
88 89
	void Close();

90 91 92
	/**
	 * Writes pending data from the encoder to the output file.
	 */
93
	void EncoderToFile();
94 95

	void SendTag(const Tag &tag);
96

97
	size_t Play(const void *chunk, size_t size);
98 99

private:
100 101 102 103 104
	gcc_pure
	bool HasDynamicPath() const {
		return !format_path.empty();
	}

105 106
	/**
	 * Finish the encoder and commit the file.
107 108
	 *
	 * Throws #std::runtime_error on error.
109
	 */
110
	void Commit();
111 112

	void FinishFormat();
113
	void ReopenFormat(AllocatedPath &&new_path);
114 115
};

116 117
RecorderOutput::RecorderOutput(const ConfigBlock &block)
	:base(recorder_output_plugin, block)
118 119 120
{
	/* read configuration */

121
	const char *encoder_name =
122
		block.GetBlockValue("encoder", "vorbis");
123
	const auto encoder_plugin = encoder_plugin_get(encoder_name);
124 125
	if (encoder_plugin == nullptr)
		throw FormatRuntimeError("No such encoder: %s", encoder_name);
126

127
	path = block.GetPath("path");
128

129
	const char *fmt = block.GetBlockValue("format_path", nullptr);
130 131 132
	if (fmt != nullptr)
		format_path = fmt;

133 134
	if (path.IsNull() && fmt == nullptr)
		throw std::runtime_error("'path' not configured");
135

136 137
	if (!path.IsNull() && fmt != nullptr)
		throw std::runtime_error("Cannot have both 'path' and 'format_path'");
138 139 140

	/* initialize encoder */

141
	prepared_encoder = encoder_init(*encoder_plugin, block);
142 143
}

144
RecorderOutput *
145
RecorderOutput::Create(EventLoop &, const ConfigBlock &block)
146
{
147
	return new RecorderOutput(block);
148 149
}

150 151
inline void
RecorderOutput::EncoderToFile()
152
{
153
	assert(file != nullptr);
154

155
	EncoderToOutputStream(*file, *encoder);
156 157
}

158 159
inline void
RecorderOutput::Open(AudioFormat &audio_format)
160 161 162
{
	/* create the output file */

163 164 165
	if (!HasDynamicPath()) {
		assert(!path.IsNull());

166
		file = new FileOutputStream(path);
167 168 169 170 171 172 173
	} else {
		/* don't open the file just yet; wait until we have
		   a tag that we can use to build the path */
		assert(path.IsNull());

		file = nullptr;
	}
174 175 176

	/* open the encoder */

177 178 179
	try {
		encoder = prepared_encoder->Open(audio_format);
	} catch (const std::runtime_error &) {
180
		delete file;
181
		throw;
182 183
	}

184
	if (!HasDynamicPath()) {
185 186
		try {
			EncoderToFile();
187
		} catch (const std::runtime_error &) {
188
			delete encoder;
189
			throw;
190 191 192 193 194 195 196
		}
	} else {
		/* remember the AudioFormat for ReopenFormat() */
		effective_audio_format = audio_format;

		/* close the encoder for now; it will be opened as
		   soon as we have received a tag */
197
		delete encoder;
198 199 200
	}
}

201 202
inline void
RecorderOutput::Commit()
203
{
204 205
	assert(!path.IsNull());

206 207
	/* flush the encoder and write the rest to the file */

208 209 210 211 212 213
	try {
		encoder->End();
		EncoderToFile();
	} catch (...) {
		delete encoder;
		throw;
214
	}
215 216 217

	/* now really close everything */

218
	delete encoder;
219

220 221 222 223 224
	try {
		file->Commit();
	} catch (...) {
		delete file;
		throw;
225
	}
226 227

	delete file;
228 229 230 231 232
}

inline void
RecorderOutput::Close()
{
233 234 235 236 237 238 239 240
	if (file == nullptr) {
		/* not currently encoding to a file; nothing needs to
		   be done now */
		assert(HasDynamicPath());
		assert(path.IsNull());
		return;
	}

241
	try {
242
		Commit();
243 244 245
	} catch (const std::exception &e) {
		LogError(e);
	}
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260

	if (HasDynamicPath()) {
		assert(!path.IsNull());
		path.SetNull();
	}
}

void
RecorderOutput::FinishFormat()
{
	assert(HasDynamicPath());

	if (file == nullptr)
		return;

261
	try {
262
		Commit();
263 264 265
	} catch (const std::exception &e) {
		LogError(e);
	}
266 267 268 269 270

	file = nullptr;
	path.SetNull();
}

271 272
inline void
RecorderOutput::ReopenFormat(AllocatedPath &&new_path)
273 274 275 276 277
{
	assert(HasDynamicPath());
	assert(path.IsNull());
	assert(file == nullptr);

278
	FileOutputStream *new_file = new FileOutputStream(new_path);
279 280

	AudioFormat new_audio_format = effective_audio_format;
281 282 283 284

	try {
		encoder = prepared_encoder->Open(new_audio_format);
	} catch (...) {
285
		delete new_file;
286
		throw;
287 288 289 290 291 292
	}

	/* reopening the encoder must always result in the same
	   AudioFormat as before */
	assert(new_audio_format == effective_audio_format);

293 294 295
	try {
		EncoderToOutputStream(*new_file, *encoder);
	} catch (const std::exception &e) {
296
		delete encoder;
297
		delete new_file;
298
		throw;
299 300 301 302 303
	}

	path = std::move(new_path);
	file = new_file;

304 305
	FormatDebug(recorder_domain, "Recording to \"%s\"",
		    path.ToUTF8().c_str());
306 307
}

308 309 310
inline void
RecorderOutput::SendTag(const Tag &tag)
{
311 312 313 314 315 316 317 318 319 320
	if (HasDynamicPath()) {
		char *p = FormatTag(tag, format_path.c_str());
		if (p == nullptr || *p == 0) {
			/* no path could be composed with this tag:
			   don't write a file */
			free(p);
			FinishFormat();
			return;
		}

321 322 323 324 325 326 327 328
		AtScopeExit(p) { free(p); };

		AllocatedPath new_path = AllocatedPath::Null();

		try {
			new_path = ParsePath(p);
		} catch (const std::runtime_error &e) {
			LogError(e);
329 330 331 332 333 334 335
			FinishFormat();
			return;
		}

		if (new_path != path) {
			FinishFormat();

336 337 338 339
			try {
				ReopenFormat(std::move(new_path));
			} catch (const std::runtime_error &e) {
				LogError(e);
340 341 342 343 344
				return;
			}
		}
	}

345 346 347
	encoder->PreTag();
	EncoderToFile();
	encoder->SendTag(tag);
348 349
}

350
inline size_t
351
RecorderOutput::Play(const void *chunk, size_t size)
352
{
353 354 355 356 357 358 359 360
	if (file == nullptr) {
		/* not currently encoding to a file; discard incoming
		   data */
		assert(HasDynamicPath());
		assert(path.IsNull());
		return size;
	}

361
	encoder->Write(chunk, size);
362

363
	EncoderToFile();
364 365

	return size;
366 367
}

368 369
typedef AudioOutputWrapper<RecorderOutput> Wrapper;

370
const struct AudioOutputPlugin recorder_output_plugin = {
371 372
	"recorder",
	nullptr,
373
	&Wrapper::Init,
374
	&Wrapper::Finish,
375 376
	nullptr,
	nullptr,
377 378
	&Wrapper::Open,
	&Wrapper::Close,
379
	nullptr,
380
	&Wrapper::SendTag,
381
	&Wrapper::Play,
382 383 384 385
	nullptr,
	nullptr,
	nullptr,
	nullptr,
386
};