CdioParanoiaInputPlugin.cxx 9.25 KB
Newer Older
1
/*
Max Kellermann's avatar
Max Kellermann committed
2
 * Copyright (C) 2003-2014 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.
 */

/**
21 22
 * CD-Audio handling (requires libcdio_paranoia)
 */
23 24

#include "config.h"
25
#include "CdioParanoiaInputPlugin.hxx"
Max Kellermann's avatar
Max Kellermann committed
26 27
#include "../InputStream.hxx"
#include "../InputPlugin.hxx"
28
#include "util/StringUtil.hxx"
29 30
#include "util/Error.hxx"
#include "util/Domain.hxx"
31
#include "system/ByteOrder.hxx"
32
#include "fs/AllocatedPath.hxx"
33
#include "Log.hxx"
34 35
#include "config/ConfigData.hxx"
#include "config/ConfigError.hxx"
36 37 38 39 40 41 42 43 44

#include <stdio.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <stdlib.h>
#include <glib.h>
#include <assert.h>

45
#ifdef HAVE_CDIO_PARANOIA_PARANOIA_H
46
#include <cdio/paranoia/paranoia.h>
47
#else
48
#include <cdio/paranoia.h>
49 50
#endif

51 52
#include <cdio/cd_types.h>

53
struct CdioParanoiaInputStream {
54
	InputStream base;
55 56 57 58 59 60 61 62 63 64 65 66

	cdrom_drive_t *drv;
	CdIo_t *cdio;
	cdrom_paranoia_t *para;

	lsn_t lsn_from, lsn_to;
	int lsn_relofs;

	int trackno;

	char buffer[CDIO_CD_FRAMESIZE_RAW];
	int buffer_lsn;
67

68
	CdioParanoiaInputStream(const char *uri, Mutex &mutex, Cond &cond,
69
				int _trackno)
70 71
		:base(input_plugin_cdio_paranoia, uri, mutex, cond),
		 drv(nullptr), cdio(nullptr), para(nullptr),
72 73 74 75 76 77 78 79 80 81 82 83
		 trackno(_trackno)
	{
	}

	~CdioParanoiaInputStream() {
		if (para != nullptr)
			cdio_paranoia_free(para);
		if (drv != nullptr)
			cdio_cddap_close_no_free_cdio(drv);
		if (cdio != nullptr)
			cdio_destroy(cdio);
	}
84 85
};

86
static constexpr Domain cdio_domain("cdio");
87

88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
static bool default_reverse_endian;

static bool
input_cdio_init(const config_param &param, Error &error)
{
	const char *value = param.GetBlockValue("default_byte_order");
	if (value != nullptr) {
		if (strcmp(value, "little_endian") == 0)
			default_reverse_endian = IsBigEndian();
		else if (strcmp(value, "big_endian") == 0)
			default_reverse_endian = IsLittleEndian();
		else {
			error.Format(config_domain, 0,
				     "Unrecognized 'default_byte_order' setting: %s",
				     value);
			return false;
		}
	}

	return true;
}

110
static void
111
input_cdio_close(InputStream *is)
112
{
113
	CdioParanoiaInputStream *i = (CdioParanoiaInputStream *)is;
114

115
	delete i;
116 117
}

118
struct cdio_uri {
119 120 121 122 123
	char device[64];
	int track;
};

static bool
124
parse_cdio_uri(struct cdio_uri *dest, const char *src, Error &error)
125
{
126
	if (!StringStartsWith(src, "cdda://"))
127 128 129 130 131 132 133 134 135 136 137 138
		return false;

	src += 7;

	if (*src == 0) {
		/* play the whole CD in the default drive */
		dest->device[0] = 0;
		dest->track = -1;
		return true;
	}

	const char *slash = strrchr(src, '/');
139
	if (slash == nullptr) {
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
		/* play the whole CD in the specified drive */
		g_strlcpy(dest->device, src, sizeof(dest->device));
		dest->track = -1;
		return true;
	}

	size_t device_length = slash - src;
	if (device_length >= sizeof(dest->device))
		device_length = sizeof(dest->device) - 1;

	memcpy(dest->device, src, device_length);
	dest->device[device_length] = 0;

	const char *track = slash + 1;

	char *endptr;
	dest->track = strtoul(track, &endptr, 10);
	if (*endptr != 0) {
158
		error.Set(cdio_domain, "Malformed track number");
159 160 161 162 163 164 165 166 167 168
		return false;
	}

	if (endptr == track)
		/* play the whole CD */
		dest->track = -1;

	return true;
}

169
static AllocatedPath
170
cdio_detect_device(void)
171
{
172 173 174
	char **devices = cdio_get_devices_with_cap(nullptr, CDIO_FS_AUDIO,
						   false);
	if (devices == nullptr)
175
		return AllocatedPath::Null();
176

177
	AllocatedPath path = AllocatedPath::FromFS(devices[0]);
178
	cdio_free_device_list(devices);
179
	return path;
180 181
}

182
static InputStream *
183
input_cdio_open(const char *uri,
184
		Mutex &mutex, Cond &cond,
185
		Error &error)
186
{
187
	struct cdio_uri parsed_uri;
188
	if (!parse_cdio_uri(&parsed_uri, uri, error))
189
		return nullptr;
190

191 192 193
	CdioParanoiaInputStream *i =
		new CdioParanoiaInputStream(uri, mutex, cond,
					    parsed_uri.track);
194 195

	/* get list of CD's supporting CD-DA */
196 197
	const AllocatedPath device = parsed_uri.device[0] != 0
		? AllocatedPath::FromFS(parsed_uri.device)
198
		: cdio_detect_device();
199
	if (device.IsNull()) {
200 201
		error.Set(cdio_domain,
			  "Unable find or access a CD-ROM drive with an audio CD in it.");
202 203
		delete i;
		return nullptr;
204 205 206
	}

	/* Found such a CD-ROM with a CD-DA loaded. Use the first drive in the list. */
207
	i->cdio = cdio_open(device.c_str(), DRIVER_UNKNOWN);
208

209
	i->drv = cdio_cddap_identify_cdio(i->cdio, 1, nullptr);
210 211

	if ( !i->drv ) {
212
		error.Set(cdio_domain, "Unable to identify audio CD disc.");
213 214
		delete i;
		return nullptr;
215 216
	}

217
	cdda_verbose_set(i->drv, CDDA_MESSAGE_FORGETIT, CDDA_MESSAGE_FORGETIT);
218 219

	if ( 0 != cdio_cddap_open(i->drv) ) {
220
		error.Set(cdio_domain, "Unable to open disc.");
221 222
		delete i;
		return nullptr;
223 224
	}

225 226
	bool reverse_endian;
	switch (data_bigendianp(i->drv)) {
227
	case -1:
228
		LogDebug(cdio_domain, "drive returns unknown audio data");
229
		reverse_endian = default_reverse_endian;
230
		break;
231

232
	case 0:
233
		LogDebug(cdio_domain, "drive returns audio data Little Endian");
234
		reverse_endian = IsBigEndian();
235
		break;
236

237
	case 1:
238
		LogDebug(cdio_domain, "drive returns audio data Big Endian");
239
		reverse_endian = IsLittleEndian();
240
		break;
241

242
	default:
243 244
		error.Format(cdio_domain, "Drive returns unknown data type %d",
			     data_bigendianp(i->drv));
245 246
		delete i;
		return nullptr;
247 248 249
	}

	i->lsn_relofs = 0;
250 251 252 253 254 255 256 257

	if (i->trackno >= 0) {
		i->lsn_from = cdio_get_track_lsn(i->cdio, i->trackno);
		i->lsn_to = cdio_get_track_last_lsn(i->cdio, i->trackno);
	} else {
		i->lsn_from = 0;
		i->lsn_to = cdio_get_disc_last_lsn(i->cdio);
	}
258 259 260 261 262 263 264 265 266 267 268 269 270 271

	i->para = cdio_paranoia_init(i->drv);

	/* Set reading mode for full paranoia, but allow skipping sectors. */
	paranoia_modeset(i->para, PARANOIA_MODE_FULL^PARANOIA_MODE_NEVERSKIP);

	/* seek to beginning of the track */
	cdio_paranoia_seek(i->para, i->lsn_from, SEEK_SET);

	i->base.ready = true;
	i->base.seekable = true;
	i->base.size = (i->lsn_to - i->lsn_from + 1) * CDIO_CD_FRAMESIZE_RAW;

	/* hack to make MPD select the "pcm" decoder plugin */
272 273 274
	i->base.mime = reverse_endian
		? "audio/x-mpd-cdda-pcm-reverse"
		: "audio/x-mpd-cdda-pcm";
275 276 277 278 279

	return &i->base;
}

static bool
280
input_cdio_seek(InputStream *is,
281
		InputPlugin::offset_type offset, int whence, Error &error)
282
{
283
	CdioParanoiaInputStream *cis = (CdioParanoiaInputStream *)is;
284 285 286 287 288 289 290 291 292 293 294 295 296 297

	/* calculate absolute offset */
	switch (whence) {
	case SEEK_SET:
		break;
	case SEEK_CUR:
		offset += cis->base.offset;
		break;
	case SEEK_END:
		offset += cis->base.size;
		break;
	}

	if (offset < 0 || offset > cis->base.size) {
298 299
		error.Format(cdio_domain, "Invalid offset to seek %ld (%ld)",
			     (long int)offset, (long int)cis->base.size);
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
		return false;
	}

	/* simple case */
	if (offset == cis->base.offset)
		return true;

	/* calculate current LSN */
	cis->lsn_relofs = offset / CDIO_CD_FRAMESIZE_RAW;
	cis->base.offset = offset;

	cdio_paranoia_seek(cis->para, cis->lsn_from + cis->lsn_relofs, SEEK_SET);

	return true;
}

static size_t
317
input_cdio_read(InputStream *is, void *ptr, size_t length,
318
		Error &error)
319
{
320
	CdioParanoiaInputStream *cis = (CdioParanoiaInputStream *)is;
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
	size_t nbytes = 0;
	int diff;
	size_t len, maxwrite;
	int16_t *rbuf;
	char *s_err, *s_mess;
	char *wptr = (char *) ptr;

	while (length > 0) {


		/* end of track ? */
		if (cis->lsn_from + cis->lsn_relofs > cis->lsn_to)
			break;

		//current sector was changed ?
		if (cis->lsn_relofs != cis->buffer_lsn) {
337
			rbuf = cdio_paranoia_read(cis->para, nullptr);
338

339
			s_err = cdda_errors(cis->drv);
340
			if (s_err) {
341 342
				FormatError(cdio_domain,
					    "paranoia_read: %s", s_err);
343 344
				free(s_err);
			}
345
			s_mess = cdda_messages(cis->drv);
346 347 348 349
			if (s_mess) {
				free(s_mess);
			}
			if (!rbuf) {
350 351
				error.Set(cdio_domain,
					  "paranoia read error. Stopping.");
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386
				return 0;
			}
			//store current buffer
			memcpy(cis->buffer, rbuf, CDIO_CD_FRAMESIZE_RAW);
			cis->buffer_lsn = cis->lsn_relofs;
		} else {
			//use cached sector
			rbuf = (int16_t*) cis->buffer;
		}

		//correct offset
		diff = cis->base.offset - cis->lsn_relofs * CDIO_CD_FRAMESIZE_RAW;

		assert(diff >= 0 && diff < CDIO_CD_FRAMESIZE_RAW);

		maxwrite = CDIO_CD_FRAMESIZE_RAW - diff;  //# of bytes pending in current buffer
		len = (length < maxwrite? length : maxwrite);

		//skip diff bytes from this lsn
		memcpy(wptr, ((char*)rbuf) + diff, len);
		//update pointer
		wptr += len;
		nbytes += len;

		//update offset
		cis->base.offset += len;
		cis->lsn_relofs = cis->base.offset / CDIO_CD_FRAMESIZE_RAW;
		//update length
		length -= len;
	}

	return nbytes;
}

static bool
387
input_cdio_eof(InputStream *is)
388
{
389
	CdioParanoiaInputStream *cis = (CdioParanoiaInputStream *)is;
390 391 392 393

	return (cis->lsn_from + cis->lsn_relofs > cis->lsn_to);
}

394
const InputPlugin input_plugin_cdio_paranoia = {
395
	"cdio_paranoia",
396
	input_cdio_init,
397 398 399 400 401 402 403 404 405 406
	nullptr,
	input_cdio_open,
	input_cdio_close,
	nullptr,
	nullptr,
	nullptr,
	nullptr,
	input_cdio_read,
	input_cdio_eof,
	input_cdio_seek,
407
};