1/* vim: set ft=objc ts=4 nowrap: */
2/*
3 *  Player.m
4 *
5 *  Copyright (c) 1999 - 2003
6 *
7 *  Author: ACKyugo <ackyugo@geocities.co.jp>
8 *	    Andreas Schik <andreas@schik.de>
9 *
10 *  This program is free software; you can redistribute it and/or modify
11 *  it under the terms of the GNU General Public License as published by
12 *  the Free Software Foundation; either version 2 of the License, or
13 *  (at your option) any later version.
14 *
15 *  This program is distributed in the hope that it will be useful,
16 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
17 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 *  GNU General Public License for more details.
19 *
20 *  You should have received a copy of the GNU General Public License
21 *  along with this program; if not, write to the Free Software
22 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
23 */
24
25
26#include "Player.h"
27#include "LED.h"
28#include "TrackList.h"
29
30static BOOL mustReadTOC = NO;
31static Player *sharedPlayer = nil;
32
33@implementation Player
34
35- init
36{
37	if (sharedPlayer) {
38		[self dealloc];
39	} else {
40		self = [super init];
41
42		sharedPlayer = self;
43
44		track = 1;
45		drive = nil;
46		autoPlay = NO;
47
48		// we must already create the (hidden) track list
49		[TrackList sharedTrackList];
50
51		if (![self loadAudioCDBundle]) {
52			[self release];
53			return nil;
54		}
55
56		timer = [NSTimer scheduledTimerWithTimeInterval: 0.5
57									target: self
58								  selector: @selector(timer:)
59					 			  userInfo: self
60								   repeats: YES];
61
62		[[NSNotificationCenter defaultCenter] addObserver: self
63							   selector: @selector(playTrack:)
64							   name: @"PlayTrack"
65							   object: nil];
66	}
67	return sharedPlayer;
68}
69
70- (void) dealloc
71{
72	[[NSNotificationCenter defaultCenter] removeObserver: self
73					   name: @"PlayTrack"
74					   object: nil];
75	[drive release];
76	[timer invalidate];
77	[timer release];
78
79	[led release];
80	[window release];
81	[prev release];
82	[play release];
83	[pause release];
84	[stop release];
85	[next release];
86
87	[super dealloc];
88}
89
90- (BOOL) loadAudioCDBundle
91{
92	int i;
93	NSString	*path;
94	NSArray		*searchPaths;
95
96	// try to load the AudioCD bundle
97	searchPaths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,
98							NSUserDomainMask|NSLocalDomainMask|NSSystemDomainMask, YES);
99
100	for (i = 0; i < [searchPaths count]; i++) {
101		NSBundle *bundle;
102		NSString *bundlePath = [NSString stringWithFormat:
103						@"%@/Bundles/AudioCD.bundle",
104						[searchPaths objectAtIndex: i]];
105
106		bundle = [NSBundle bundleWithPath: bundlePath];
107		if (bundle) {
108			audiocdClass = [bundle principalClass];
109			if (audiocdClass) {
110				break;
111			}
112		}
113	}   // for (i = 0; i < [searchPaths count]; i++)
114
115	if (!audiocdClass) {
116		NSRunAlertPanel(@"CDPlayer",
117				_(@"Couldn't find AudioCD bundle."),
118				_(@"Exit"), nil, nil);
119		exit(-1);
120	}
121
122	path = [[NSUserDefaults standardUserDefaults] stringForKey: @"Device"];
123	drive = [[audiocdClass alloc] initWithHandler: self];
124	[drive startPollingWithPreferredDevice: path];
125
126	return YES;
127}
128
129
130//
131//
132//
133- (void) timer: (id)timer
134{
135	NSString	*path;
136	int		state;
137
138	path = [[NSUserDefaults standardUserDefaults] stringForKey: @"Device"];
139
140	if (mustReadTOC) {
141		[[TrackList sharedTrackList] setTOC: [drive readTOC]];
142		mustReadTOC = NO;
143	}
144
145	// is a disc in the drive?
146	if([drive cdPresent] == NO) {
147		[led setNoCD];
148		[led display];
149		return;
150	}
151
152	if (autoPlay) {
153		[drive playStart: 1 End: [drive totalTrack]];
154		autoPlay = NO;
155	}
156
157	state = [drive currentState];
158
159	if(state == AUDIOCD_PLAYING) {
160		[led setMin: [drive currentMin]];
161		[led setSec: [drive currentSec]];
162    	track = [drive currentTrack];
163	} else if(state == AUDIOCD_NOSTATUS) {
164		[led setMin: 0];
165		[led setSec: 0];
166	}
167	[led setTrack: track];
168	[led display];
169	[[TrackList sharedTrackList] setPlaysTrack: track];
170}
171
172
173
174//
175//
176//
177
178- (void) playTrack: (NSNotification *)not
179{
180	int nextTrack = [[[not userInfo] objectForKey: @"Track"] intValue];
181
182    if (nextTrack == track)
183		return;
184
185	track = nextTrack;
186	[drive playStart: track End: [drive totalTrack]];
187	[led setTrack: track];
188	[led display];
189	[[TrackList sharedTrackList] setPlaysTrack: track];
190}
191
192
193- (void) play: (id)sender
194{
195	if([drive cdPresent] == NO) {
196		return;
197	}
198
199	if ([drive currentState] == AUDIOCD_PAUSED) {
200		[drive resume];
201	} else {
202		[drive playStart: track End: [drive totalTrack]];
203	}
204}
205
206- (void) pause: (id)sender
207{
208	if ([drive cdPresent] == NO) {
209		return;
210	}
211
212	if ([drive currentState] == AUDIOCD_PLAYING) {
213		[drive pause];
214	} else if ([drive currentState] == AUDIOCD_PAUSED) {
215		[drive resume];
216	}
217}
218
219- (void) stop: (id)sender
220{
221	// the 'stop' button is also 'eject' if CD is already halted,
222	// but not the Controller, which may also stop the CD on exit
223	if(sender == stop) {
224		if ([drive cdPresent] == NO) {
225			[drive close];
226			return;
227		}
228
229		if((sender == stop) && ([drive currentState] == AUDIOCD_NOSTATUS)) {
230			[drive eject];
231			return;
232		}
233	}
234
235	[drive stop];
236	track = 1;
237	[led setTrack: track];
238	[led setMin: 0];
239	[led setSec: 0];
240	[led display];
241	[[TrackList sharedTrackList] setPlaysTrack: 0];
242}
243
244- (void) eject: (id)sender
245{
246	if([drive cdPresent] == NO) {
247		return;
248	}
249
250	[drive eject];
251}
252
253- (void) next: (id)sender
254{
255	if([drive cdPresent] == NO) {
256		return;
257	}
258
259	track++;
260	if(track > [drive totalTrack]) {
261		track = 1;
262	}
263
264	if(([drive currentState] == AUDIOCD_PLAYING) ||
265	   ([drive currentState] == AUDIOCD_PAUSED)) {
266		[drive playStart: track End: [drive totalTrack]];
267	}
268	[led setTrack: track];
269	[led display];
270	[[TrackList sharedTrackList] setPlaysTrack: track];
271}
272
273- (void) prev: (id)sender
274{
275	if([drive cdPresent] == NO) {
276		return;
277	}
278
279	// We jump back only if we are not playing at the moment
280	// or if we are at the very beginning of a playing track.
281	// The latter condition allows to jump back to the beginning
282	// of the current track before jumping back one more track.
283	if (([drive currentState] == AUDIOCD_NOSTATUS) ||
284		([drive currentSec] == 0 && [drive currentMin] == 0)) {
285		track--;
286		if(track < 1) {
287			track = [drive totalTrack];
288		}
289	}
290
291	if(([drive currentState] == AUDIOCD_PLAYING) ||
292	   ([drive currentState] == AUDIOCD_PAUSED)) {
293		[drive playStart: track End: [drive totalTrack]];
294	}
295
296	[led setTrack: track];
297	[led display];
298	[[TrackList sharedTrackList] setPlaysTrack: track];
299}
300
301
302//
303//
304//
305//
306- (BOOL) audioCD: (id)sender error: (int)no message: (NSString *)msg
307{
308	// FIXME: Take appropriate action when an error occurs!!
309//	NSRunAlertPanel(@"CDPlayer",
310//			msg,
311//			_(@"OK"), nil, nil);
312	NSLog(@"CDPlayer error: %@", msg);
313	return YES;
314}
315
316- (void) audioCDChanged: (id)sender
317{
318	mustReadTOC = YES;
319}
320
321
322- (BOOL) windowShouldClose: (id)sender
323{
324	[[NSApplication sharedApplication ] terminate: self];
325	return YES;
326}
327
328
329//
330//
331//
332- (void) buildInterface
333{
334	NSRect		frame;
335	unsigned int    style = NSTitledWindowMask | NSClosableWindowMask |
336				NSMiniaturizableWindowMask;
337	NSBundle	*bundle = [NSBundle mainBundle];
338	NSImage		*image;
339	NSString	*path;
340
341
342	frame = NSMakeRect(100, 100, 160, 73);
343	window = [[NSWindow alloc] initWithContentRect: frame
344					     styleMask: style
345					       backing: NSBackingStoreRetained
346					         defer: NO];
347	[window setTitle: @"CDPlayer"];
348	[window setDelegate: self];
349
350	frame = NSMakeRect(5, 38, 150, 30);
351	led = [[LED alloc]  initWithFrame: frame];
352
353	[led setNoCD];
354	[led display];
355	[[window contentView] addSubview: led];
356
357	path = [bundle pathForResource: @"prev" ofType: @"tiff"];
358	image = [[[NSImage alloc] initWithContentsOfFile: path] autorelease];
359	if(image == nil)    NSLog(@"cannot load prev.tiff");
360	frame = NSMakeRect( 5, 5, 30, 30);
361	prev = [[NSButton alloc] initWithFrame: frame];
362	[prev setButtonType: NSMomentaryPushButton];
363	[prev setImagePosition: NSImageOnly];
364	[prev setImage: image];
365	[prev setTarget: self];
366	[prev setAction: @selector(prev:)];
367	[[window contentView] addSubview: prev];
368
369	path = [bundle pathForResource: @"play" ofType: @"tiff"];
370	image = [[[NSImage alloc] initWithContentsOfFile: path] autorelease];
371	if(image == nil)    NSLog(@"cannot load play.tiff");
372	frame = NSMakeRect( 35, 5, 30, 30);
373	play = [[NSButton alloc] initWithFrame: frame];
374	[play setButtonType: NSMomentaryPushButton];
375	[play setImagePosition: NSImageOnly];
376	[play setImage: image];
377	[play setTarget: self];
378	[play setAction: @selector(play:)];
379	[[window contentView] addSubview: play];
380
381	path = [bundle pathForResource: @"pause" ofType: @"tiff"];
382	image = [[[NSImage alloc] initWithContentsOfFile: path] autorelease];
383	if(image == nil)    NSLog(@"cannot load pause.tiff");
384	frame = NSMakeRect( 65, 5, 30, 30);
385	pause = [[NSButton alloc] initWithFrame: frame];
386	[pause setButtonType: NSMomentaryPushButton];
387	[pause setImagePosition: NSImageOnly];
388	[pause setImage: image];
389	[pause setTarget: self];
390	[pause setAction: @selector(pause:)];
391	[[window contentView] addSubview: pause];
392
393	path = [bundle pathForResource: @"next" ofType: @"tiff"];
394	image = [[[NSImage alloc] initWithContentsOfFile: path] autorelease];
395	if(image == nil)    NSLog(@"cannot load next.tiff");
396	frame = NSMakeRect(95, 5, 30, 30);
397	next = [[NSButton alloc] initWithFrame: frame];
398	[next setButtonType: NSMomentaryPushButton];
399	[next setImagePosition: NSImageOnly];
400	[next setImage: image];
401	[next setTarget: self];
402	[next setAction: @selector(next:)];
403	[[window contentView] addSubview: next];
404
405	path = [bundle pathForResource: @"stop" ofType: @"tiff"];
406	image = [[[NSImage alloc] initWithContentsOfFile: path] autorelease];
407	if(image == nil)    NSLog(@"cannot load stop.tiff");
408	frame = NSMakeRect( 125, 5, 30, 30);
409	stop = [[NSButton alloc] initWithFrame: frame];
410	[stop setButtonType: NSMomentaryPushButton];
411	[stop setImagePosition: NSImageOnly];
412	[stop setImage: image];
413	[stop setTarget: self];
414	[stop setAction: @selector(stop:)];
415	[[window contentView] addSubview: stop];
416
417	[window orderFront: self];
418    [window setFrameAutosaveName: @"CDPlayerWindow"];
419    [window setFrameUsingName: @"CDPlayerWindow"];
420}
421
422//
423// services methods
424//
425
426- (void) getTOC: (NSPasteboard *) pboard
427	   userData: (NSString *) userData
428		  error: (NSString **) error
429{
430	TrackList *tl = [TrackList sharedTrackList];
431	int i, rows = [tl numberOfTracksInTOC];
432	NSMutableArray *array;
433
434	/*
435	 * If we don't have any rows, there is probably no CD.
436	 */
437	if (rows == 0) {
438		*error = _(@"No Audio CD found.");
439		return;
440	}
441
442	array = [[NSMutableArray alloc] init];
443
444	for (i = 0; i < rows; i++) {
445		[array addObject: [NSNumber numberWithInt: i]];
446	}
447
448	/*
449	 * This is a small hack, but we can clean this up later.
450	 */
451	if (![tl tableView: nil writeRows: array toPasteboard: pboard]) {
452		*error = _(@"Could not write TOC to pasteboard.");
453	} else {
454	}
455
456	RELEASE(array);
457}
458
459- (void) playCD: (NSPasteboard *) pboard
460	   userData: (NSString *) userData
461		  error: (NSString **) error
462{
463	NSArray *types = [pboard types];
464
465	// If CD is currently playing, we reject the request
466	if([drive currentState] == AUDIOCD_PLAYING) {
467        *error = _(@"Player.alreadyPlaying");
468        return;
469	}
470
471	/*
472	 * Do we have at least one valid pasteboard type?
473	 */
474	if (![types containsObject: NSFilenamesPboardType] &&
475			![types containsObject: NSStringPboardType]) {
476        *error = _(@"Player.noValidPboardType");
477        return;
478    }
479
480	/*
481	 * Try to add as much as possible, i.e. even if one pasteboard
482	 * type fails try the other one (if it exists in the pasteboard).
483	 */
484	if ([types containsObject: NSFilenamesPboardType] ||
485			[types containsObject: NSStringPboardType]) {
486		// Get the device name from the pboard
487		NSString *device = nil;
488		NSArray *devices = [pboard propertyListForType: NSFilenamesPboardType];
489		if (devices != nil) {
490			if ([devices count] != 1) {
491				*error = _(@"Player.tooManyFileNames");
492				return;
493			}
494			device = [devices objectAtIndex: 0];
495		} else {
496			device = [pboard propertyListForType: NSStringPboardType];
497		}
498		if (device == nil) {
499			*error = _(@"Player.noDeviceNameFound");
500			return;
501		}
502		autoPlay = YES;
503		[drive stopPolling];
504		[drive startPollingWithPreferredDevice: device];
505	}
506}
507
508//
509// class methods
510//
511+ (Player *) sharedPlayer
512{
513	if (!sharedPlayer) {
514		sharedPlayer = [[Player alloc] init];
515	}
516	return sharedPlayer;
517}
518
519
520@end
521