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