xref: /haiku/src/add-ons/kernel/file_systems/cdda/cdda.cpp (revision e8113cabe023042a1c96f338fab8f5f47f1688d4)
1 /*
2  * Copyright 2007, Axel Dörfler, axeld@pinc-software.de.
3  * Distributed under the terms of the MIT License.
4  */
5 
6 
7 #include "cdda.h"
8 
9 #include <KernelExport.h>
10 #include <device/scsi.h>
11 
12 #include <ctype.h>
13 #include <stdlib.h>
14 #include <string.h>
15 
16 
17 struct cdtext_pack_data {
18 	uint8	id;
19 	uint8	track;
20 	uint8	number;
21 	uint8	character_position : 4;
22 	uint8	block_number : 3;
23 	uint8	double_byte : 1;
24 	char	text[12];
25 	uint8	crc[2];
26 } _PACKED;
27 
28 enum {
29 	kTrackID	= 0x80,
30 	kArtistID	= 0x81,
31 	kMessageID	= 0x85,
32 };
33 
34 static const uint32 kBufferSize = 16384;
35 static const uint32 kSenseSize = 1024;
36 
37 
38 //	#pragma mark - string functions
39 
40 
41 static char *
42 copy_string(const char *string)
43 {
44 	if (string == NULL || !string[0])
45 		return NULL;
46 
47 	return strdup(string);
48 }
49 
50 
51 static bool
52 is_garbage(char c)
53 {
54 	return isspace(c) || c == '-' || c == '/' || c == '\\';
55 }
56 
57 
58 static void
59 sanitize_string(char *&string)
60 {
61 	if (string == NULL)
62 		return;
63 
64 	// strip garbage at the start
65 
66 	uint32 length = strlen(string);
67 	uint32 garbage = 0;
68 	while (is_garbage(string[garbage])) {
69 		garbage++;
70 	}
71 
72 	length -= garbage;
73 	if (garbage)
74 		memmove(string, string + garbage, length + 1);
75 
76 	// strip garbage from the end
77 
78 	while (length > 1 && isspace(string[length - 1])) {
79 		string[--length] = '\0';
80 	}
81 
82 	if (!string[0]) {
83 		// free string if it's empty
84 		free(string);
85 		string = NULL;
86 	}
87 }
88 
89 
90 //! Finds the first occurrence of \a find in \a string, ignores case.
91 static char*
92 find_string(const char *string, const char *find)
93 {
94 	if (string == NULL || find == NULL)
95 		return NULL;
96 
97 	char first = tolower(find[0]);
98 	if (first == '\0')
99 		return (char *)string;
100 
101 	int32 findLength = strlen(find) - 1;
102 	find++;
103 
104 	for (; string[0]; string++) {
105 		if (tolower(string[0]) != first)
106 			continue;
107 		if (strncasecmp(string + 1, find, findLength) == 0)
108 			return (char *)string;
109 	}
110 
111 	return NULL;
112 }
113 
114 
115 static void
116 cut_string(char *string, char *cut)
117 {
118 	if (string == NULL || cut == NULL)
119 		return;
120 
121 	char *found = find_string(string, cut);
122 	if (found != NULL) {
123 		uint32 foundLength = strlen(found);
124 		uint32 cutLength = strlen(cut);
125 		memmove(found, found + cutLength, foundLength + 1 - cutLength);
126 	}
127 }
128 
129 
130 static void
131 sanitize_album(cdtext &text)
132 {
133 	cut_string(text.album, text.artist);
134 	sanitize_string(text.album);
135 
136 	if (text.album != NULL && !strcasecmp(text.album, "My CD")) {
137 		// don't laugh, people really do that!
138 		free(text.album);
139 		text.album = NULL;
140 	}
141 
142 	if ((text.artist == NULL || text.artist[0] == NULL) && text.album != NULL) {
143 		// try to extract artist from album
144 		char *space = strstr(text.album, "  ");
145 		if (space != NULL) {
146 			space[0] = '\0';
147 			text.artist = text.album;
148 			text.album = copy_string(space + 2);
149 
150 			sanitize_string(text.artist);
151 			sanitize_string(text.album);
152 		}
153 	}
154 }
155 
156 
157 static void
158 sanitize_titles(cdtext &text)
159 {
160 	for (uint8 i = 0; i < text.track_count; i++) {
161 		cut_string(text.titles[i], "(Album Version)");
162 		sanitize_string(text.titles[i]);
163 		sanitize_string(text.artists[i]);
164 
165 		if (text.artists[i] != NULL && text.artist != NULL
166 			&& !strcasecmp(text.artists[i], text.artist)) {
167 			// if the title artist is the same as the main artist, remove it
168 			free(text.artists[i]);
169 			text.artists[i] = NULL;
170 		}
171 
172 		if (text.titles[i] != NULL && text.titles[i][0] == '\t' && i > 0)
173 			text.titles[i] = copy_string(text.titles[i - 1]);
174 	}
175 }
176 
177 
178 static bool
179 single_case(const char *string, bool &upper, bool &first)
180 {
181 	if (string == NULL)
182 		return true;
183 
184 	while (string[0]) {
185 		while (!isalpha(string[0])) {
186 			string++;
187 		}
188 
189 		if (first) {
190 			upper = isupper(string[0]) != 0;
191 			first = false;
192 		} else if ((isupper(string[0]) != 0) ^ upper)
193 			return false;
194 
195 		string++;
196 	}
197 
198 	return true;
199 }
200 
201 
202 static void
203 capitalize_string(char *string)
204 {
205 	if (string == NULL)
206 		return;
207 
208 	bool newWord = isalpha(string[0]) || isspace(string[0]);
209 	while (string[0]) {
210 		if (isalpha(string[0])) {
211 			if (newWord) {
212 				string[0] = toupper(string[0]);
213 				newWord = false;
214 			} else
215 				string[0] = tolower(string[0]);
216 		} else if (string[0] != '\'')
217 			newWord = true;
218 
219 		string++;
220 	}
221 }
222 
223 
224 static void
225 correct_case(cdtext &text)
226 {
227 	// check if all titles share a single case
228 	bool first = true;
229 	bool upper;
230 	if (!single_case(text.album, upper, first)
231 		|| !single_case(text.artist, upper, first))
232 		return;
233 
234 	for (int32 i = 0; i < text.track_count; i++) {
235 		if (!single_case(text.titles[i], upper, first)
236 			|| !single_case(text.artists[i], upper, first))
237 			return;
238 	}
239 
240 	// If we get here, everything has a single case; we fix that
241 	// and capitalize each word
242 
243 	capitalize_string(text.album);
244 	capitalize_string(text.artist);
245 	for (int32 i = 0; i < text.track_count; i++) {
246 		capitalize_string(text.titles[i]);
247 		capitalize_string(text.artists[i]);
248 	}
249 }
250 
251 
252 //	#pragma mark - CD-Text
253 
254 
255 cdtext::cdtext()
256 	:
257 	artist(NULL),
258 	album(NULL),
259 	genre(NULL),
260 	track_count(0)
261 {
262 	memset(titles, 0, sizeof(titles));
263 	memset(artists, 0, sizeof(artists));
264 }
265 
266 
267 cdtext::~cdtext()
268 {
269 	free(album);
270 	free(artist);
271 	free(genre);
272 
273 	for (uint8 i = 0; i < track_count; i++) {
274 		free(titles[i]);
275 		free(artists[i]);
276 	}
277 }
278 
279 
280 static bool
281 is_string_id(uint8 id)
282 {
283 	return id >= kTrackID && id <= kMessageID;
284 }
285 
286 
287 static bool
288 parse_pack_data(cdtext_pack_data *&pack, uint32 &packLeft,
289 	cdtext_pack_data *&lastPack, uint8 &id, uint8 &track, uint8 &state,
290 	char *buffer, size_t &length)
291 {
292 	if (packLeft < sizeof(cdtext_pack_data))
293 		return false;
294 
295 	uint8 number = pack->number;
296 	size_t size = length;
297 
298 	if (state != 0) {
299 		// we had a terminated string and a missing track
300 		track++;
301 		memcpy(buffer, lastPack->text + state, 12 - state);
302 		if (pack->track - track == 1)
303 			state = 0;
304 		else
305 			state += strnlen(buffer, 12 - state);
306 		return true;
307 	}
308 
309 	id = pack->id;
310 	track = pack->track;
311 	buffer[0] = '\0';
312 	length = 0;
313 
314 	size_t position = pack->character_position;
315 	if (position > 0 && lastPack != NULL) {
316 		memcpy(buffer, &lastPack->text[12 - position], position);
317 		length = position;
318 	}
319 
320 	while (id == pack->id && track == pack->track) {
321 #if 1
322 		dprintf("%u.%u.%u, %u.%u.%u, ", pack->id, pack->track, pack->number,
323 			pack->double_byte, pack->block_number, pack->character_position);
324 		for (int32 i = 0; i < 12; i++) {
325 			if (isprint(pack->text[i]))
326 				dprintf("%c", pack->text[i]);
327 		}
328 		dprintf("\n");
329 #endif
330 		if (is_string_id(id)) {
331 			// TODO: support double byte characters
332 			if (length + 12 < size) {
333 				memcpy(buffer + length, pack->text, 12);
334 				length += 12;
335 			}
336 		}
337 
338 		packLeft -= sizeof(cdtext_pack_data);
339 		if (packLeft < sizeof(cdtext_pack_data))
340 			return false;
341 
342 		lastPack = pack;
343 		number++;
344 		pack++;
345 
346 		if (pack->number != number)
347 			return false;
348 	}
349 
350 	if (id == pack->id) {
351 		length -= pack->character_position;
352 		if (length >= size)
353 			length = size - 1;
354 		buffer[length] = '\0';
355 
356 		if (pack->track > lastPack->track + 1) {
357 			// there is a missing track
358 			for (int32 i = 0; i < 12; i++) {
359 				if (lastPack->text[i] == '\0') {
360 					state = i + (lastPack->double_byte ? 2 : 1);
361 					break;
362 				}
363 			}
364 		}
365 	}
366 
367 	// TODO: convert text to UTF-8
368 	return true;
369 }
370 
371 
372 static void
373 dump_cdtext(cdtext &text)
374 {
375 	if (text.album)
376 		dprintf("Album:    \"%s\"\n", text.album);
377 	if (text.artist)
378 		dprintf("Artist:   \"%s\"\n", text.artist);
379 	for (uint8 i = 0; i < text.track_count; i++) {
380 		dprintf("Track %02u: \"%s\"%s%s%s\n", i + 1, text.titles[i],
381 			text.artists[i] ? " (" : "", text.artists[i] ? text.artists[i] : "",
382 			text.artists[i] ? ")" : "");
383 	}
384 }
385 
386 
387 static void
388 dump_toc(scsi_toc_toc *toc)
389 {
390 	int32 numTracks = toc->last_track + 1 - toc->first_track;
391 
392 	for (int32 i = 0; i < numTracks; i++) {
393 		scsi_toc_track& track = toc->tracks[i];
394 		scsi_cd_msf& next = toc->tracks[i + 1].start.time;
395 			// the last track is always lead-out
396 		scsi_cd_msf& start = toc->tracks[i].start.time;
397 		scsi_cd_msf length;
398 
399 		uint64 diff = next.minute * kFramesPerMinute
400 			+ next.second * kFramesPerSecond + next.frame
401 			- start.minute * kFramesPerMinute
402 			- start.second * kFramesPerSecond - start.frame;
403 		length.minute = diff / kFramesPerMinute;
404 		length.second = (diff % kFramesPerMinute) / kFramesPerSecond;
405 		length.frame = diff % kFramesPerSecond;
406 
407 		dprintf("%02u. %02u:%02u.%02u (length %02u:%02u.%02u)\n",
408 			track.track_number, start.minute, start.second, start.frame,
409 			length.minute, length.second, length.frame);
410 	}
411 }
412 
413 
414 static status_t
415 read_table_of_contents(int fd, uint32 track, uint8 format, uint8 *buffer,
416 	size_t bufferSize)
417 {
418 	raw_device_command raw;
419 	uint8 *senseData = (uint8 *)malloc(kSenseSize);
420 	if (senseData == NULL)
421 		return B_NO_MEMORY;
422 
423 	memset(&raw, 0, sizeof(raw_device_command));
424 	memset(senseData, 0, kSenseSize);
425 	memset(buffer, 0, bufferSize);
426 
427 	scsi_cmd_read_toc &toc = *(scsi_cmd_read_toc*)&raw.command;
428 	toc.opcode = SCSI_OP_READ_TOC;
429 	toc.time = 1;
430 	toc.format = format;
431 	toc.track = track;
432 	toc.allocation_length = B_HOST_TO_BENDIAN_INT16(bufferSize);
433 
434 	raw.command_length = 10;
435 	raw.flags = B_RAW_DEVICE_DATA_IN | B_RAW_DEVICE_REPORT_RESIDUAL
436 		| B_RAW_DEVICE_SHORT_READ_VALID;
437 	raw.scsi_status = 0;
438 	raw.cam_status = 0;
439 	raw.data = buffer;
440 	raw.data_length = bufferSize;
441 	raw.timeout = 10000000LL;	// 10 secs
442 	raw.sense_data = senseData;
443 	raw.sense_data_length = sizeof(kSenseSize);
444 
445 	if (ioctl(fd, B_RAW_DEVICE_COMMAND, &raw) == 0
446 		&& raw.scsi_status == 0 && raw.cam_status == 1) {
447 		free(senseData);
448 		return B_OK;
449 	}
450 
451 	free(senseData);
452 	return B_ERROR;
453 }
454 
455 
456 //	#pragma mark - exported functions
457 
458 
459 status_t
460 read_cdtext(int fd, struct cdtext &cdtext)
461 {
462 	uint8 *buffer = (uint8 *)malloc(kBufferSize);
463 	if (buffer == NULL)
464 		return B_NO_MEMORY;
465 
466 	// do it twice, just in case...
467 	// (at least my CD-ROM sometimes returned broken data on first try)
468 	read_table_of_contents(fd, 1, SCSI_TOC_FORMAT_CD_TEXT, buffer,
469 		kBufferSize);
470 	if (read_table_of_contents(fd, 1, SCSI_TOC_FORMAT_CD_TEXT, buffer,
471 			kBufferSize) < B_OK) {
472 		free(buffer);
473 		return B_ERROR;
474 	}
475 
476 	scsi_toc_general *header = (scsi_toc_general *)buffer;
477 
478 	size_t packLength = B_BENDIAN_TO_HOST_INT16(header->data_length) - 2;
479 	cdtext_pack_data *pack = (cdtext_pack_data *)(header + 1);
480 	cdtext_pack_data *lastPack = NULL;
481 	uint8 state = 0;
482 	char text[256];
483 
484 	while (true) {
485 		size_t length = sizeof(text);
486 		uint8 id, track;
487 
488 		if (!parse_pack_data(pack, packLength, lastPack, id, track,
489 				state, text, length))
490 			break;
491 
492 		switch (id) {
493 			case kTrackID:
494 				if (track == 0) {
495 					if (cdtext.album == NULL)
496 						cdtext.album = copy_string(text);
497 				} else if (track <= kMaxTracks) {
498 					if (cdtext.titles[track - 1] == NULL)
499 						cdtext.titles[track - 1] = copy_string(text);
500 					if (track > cdtext.track_count)
501 						cdtext.track_count = track;
502 				}
503 				break;
504 
505 			case kArtistID:
506 				if (track == 0) {
507 					if (cdtext.artist == NULL)
508 						cdtext.artist = copy_string(text);
509 				} else if (track <= kMaxTracks) {
510 					if (cdtext.artists[track - 1] == NULL)
511 						cdtext.artists[track - 1] = copy_string(text);
512 				}
513 				break;
514 
515 			default:
516 				if (is_string_id(id))
517 					dprintf("UNKNOWN %u: \"%s\"\n", id, text);
518 				break;
519 		}
520 	}
521 
522 	free(buffer);
523 
524 	sanitize_string(cdtext.artist);
525 	sanitize_album(cdtext);
526 	sanitize_titles(cdtext);
527 	correct_case(cdtext);
528 
529 	dump_cdtext(cdtext);
530 	return B_OK;
531 }
532 
533 
534 status_t
535 read_table_of_contents(int fd, scsi_toc_toc *toc, size_t length)
536 {
537 	status_t status = read_table_of_contents(fd, 1, SCSI_TOC_FORMAT_TOC,
538 		(uint8*)toc, length);
539 	if (status < B_OK)
540 		return status;
541 
542 	dump_toc(toc);
543 	return B_OK;
544 }
545 
546