1 /* ScummVM - Graphic Adventure Engine
2  *
3  * ScummVM is the legal property of its developers, whose names
4  * are too numerous to list here. Please refer to the COPYRIGHT
5  * file distributed with this source distribution.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  * Original license header:
22  *
23  * Cabal - Legacy Game Implementations
24  *
25  * Cabal is the legal property of its developers, whose names
26  * are too numerous to list here. Please refer to the COPYRIGHT
27  * file distributed with this source distribution.
28  *
29  * This program is free software; you can redistribute it and/or
30  * modify it under the terms of the GNU General Public License
31  * as published by the Free Software Foundation; either version 2
32  * of the License, or (at your option) any later version.
33  *
34  * This program is distributed in the hope that it will be useful,
35  * but WITHOUT ANY WARRANTY; without even the implied warranty of
36  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
37  * GNU General Public License for more details.
38  *
39  * You should have received a copy of the GNU General Public License
40  * along with this program; if not, write to the Free Software
41  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
42  *
43  */
44 
45 #ifdef MACOSX
46 
47 #include <sys/stat.h>
48 #include <sys/mount.h>
49 #include <limits.h>
50 
51 #include "common/scummsys.h"
52 
53 #include "audio/audiostream.h"
54 #include "audio/decoders/aiff.h"
55 #include "audio/timestamp.h"
56 #include "common/config-manager.h"
57 #include "common/debug.h"
58 #include "common/fs.h"
59 #include "common/hashmap.h"
60 #include "common/textconsole.h"
61 #include "backends/audiocd/default/default-audiocd.h"
62 #include "backends/audiocd/macosx/macosx-audiocd.h"
63 #include "backends/fs/stdiostream.h"
64 
65 // Partially based on SDL's code
66 
67 /**
68  * The Mac OS X audio cd manager. Implements real audio cd playback.
69  */
70 class MacOSXAudioCDManager : public DefaultAudioCDManager {
71 public:
MacOSXAudioCDManager()72 	MacOSXAudioCDManager() {}
73 	~MacOSXAudioCDManager();
74 
75 	bool open();
76 	void close();
77 	bool play(int track, int numLoops, int startFrame, int duration, bool onlyEmulate = false);
78 
79 protected:
80 	bool openCD(int drive);
81 	bool openCD(const Common::String &drive);
82 
83 private:
84 	struct Drive {
DriveMacOSXAudioCDManager::Drive85 		Drive(const Common::String &m, const Common::String &d, const Common::String &f) :
86 			mountPoint(m), deviceName(d), fsType(f) {}
87 
88 		Common::String mountPoint;
89 		Common::String deviceName;
90 		Common::String fsType;
91 	};
92 
93 	typedef Common::Array<Drive> DriveList;
94 	DriveList detectAllDrives();
95 	DriveList detectCDDADrives();
96 
97 	bool findTrackNames(const Common::String &drivePath);
98 
99 	Common::HashMap<uint, Common::String> _trackMap;
100 };
101 
~MacOSXAudioCDManager()102 MacOSXAudioCDManager::~MacOSXAudioCDManager() {
103 	close();
104 }
105 
open()106 bool MacOSXAudioCDManager::open() {
107 	close();
108 
109 	if (openRealCD())
110 		return true;
111 
112 	return DefaultAudioCDManager::open();
113 }
114 
115 /**
116  * Find the base disk number of device name.
117  * Returns -1 if mount point is not /dev/disk*
118  */
findBaseDiskNumber(const Common::String & diskName)119 static int findBaseDiskNumber(const Common::String &diskName) {
120 	if (!diskName.hasPrefix("/dev/disk"))
121 		return -1;
122 
123 	const char *startPtr = diskName.c_str() + 9;
124 	char *endPtr;
125 	int baseDiskNumber = strtol(startPtr, &endPtr, 10);
126 	if (startPtr == endPtr)
127 		return -1;
128 
129 	return baseDiskNumber;
130 }
131 
openCD(int drive)132 bool MacOSXAudioCDManager::openCD(int drive) {
133 	DriveList allDrives = detectAllDrives();
134 	if (allDrives.empty())
135 		return false;
136 
137 	DriveList cddaDrives;
138 
139 	// Try to get the volume related to the game's path
140 	if (ConfMan.hasKey("path")) {
141 		Common::String gamePath = ConfMan.get("path");
142 		struct statfs gamePathStat;
143 		if (statfs(gamePath.c_str(), &gamePathStat) == 0) {
144 			int baseDiskNumber = findBaseDiskNumber(gamePathStat.f_mntfromname);
145 			if (baseDiskNumber >= 0) {
146 				// Look for a CDDA drive with the same base disk number
147 				for (uint32 i = 0; i < allDrives.size(); i++) {
148 					if (allDrives[i].fsType == "cddafs" && findBaseDiskNumber(allDrives[i].deviceName) == baseDiskNumber) {
149 						debug(1, "Preferring drive '%s'", allDrives[i].mountPoint.c_str());
150 						cddaDrives.push_back(allDrives[i]);
151 						allDrives.remove_at(i);
152 						break;
153 					}
154 				}
155 			}
156 		}
157 	}
158 
159 	// Add the remaining CDDA drives to the CDDA list
160 	for (uint32 i = 0; i < allDrives.size(); i++)
161 		if (allDrives[i].fsType == "cddafs")
162 			cddaDrives.push_back(allDrives[i]);
163 
164 	if (drive >= (int)cddaDrives.size())
165 		return false;
166 
167 	debug(1, "Using '%s' as the CD drive", cddaDrives[drive].mountPoint.c_str());
168 
169 	return findTrackNames(cddaDrives[drive].mountPoint);
170 }
171 
openCD(const Common::String & drive)172 bool MacOSXAudioCDManager::openCD(const Common::String &drive) {
173 	DriveList drives = detectAllDrives();
174 
175 	for (uint32 i = 0; i < drives.size(); i++) {
176 		if (drives[i].fsType != "cddafs")
177 			continue;
178 
179 		if (drives[i].mountPoint == drive || drives[i].deviceName == drive) {
180 			debug(1, "Using '%s' as the CD drive", drives[i].mountPoint.c_str());
181 			return findTrackNames(drives[i].mountPoint);
182 		}
183 	}
184 
185 	return false;
186 }
187 
close()188 void MacOSXAudioCDManager::close() {
189 	DefaultAudioCDManager::close();
190 	_trackMap.clear();
191 }
192 
193 enum {
194 	// Some crazy high number that we'll never actually hit
195 	kMaxDriveCount = 256
196 };
197 
detectAllDrives()198 MacOSXAudioCDManager::DriveList MacOSXAudioCDManager::detectAllDrives() {
199 	// Fetch the lists of drives
200 	struct statfs driveStats[kMaxDriveCount];
201 	int foundDrives = getfsstat(driveStats, sizeof(driveStats), MNT_WAIT);
202 	if (foundDrives <= 0)
203 		return DriveList();
204 
205 	DriveList drives;
206 	for (int i = 0; i < foundDrives; i++)
207 		drives.push_back(Drive(driveStats[i].f_mntonname, driveStats[i].f_mntfromname, driveStats[i].f_fstypename));
208 
209 	return drives;
210 }
211 
play(int track,int numLoops,int startFrame,int duration,bool onlyEmulate)212 bool MacOSXAudioCDManager::play(int track, int numLoops, int startFrame, int duration, bool onlyEmulate) {
213 	// Prefer emulation
214 	if (DefaultAudioCDManager::play(track, numLoops, startFrame, duration, onlyEmulate))
215 		return true;
216 
217 	// If we're set to only emulate, or have no CD drive, return here
218 	if (onlyEmulate || !_trackMap.contains(track))
219 		return false;
220 
221 	if (!numLoops && !startFrame)
222 		return false;
223 
224 	// Now load the AIFF track from the name
225 	Common::String fileName = _trackMap[track];
226 	Common::SeekableReadStream *stream = StdioStream::makeFromPath(fileName.c_str(), false);
227 
228 	if (!stream) {
229 		warning("Failed to open track '%s'", fileName.c_str());
230 		return false;
231 	}
232 
233 	Audio::AudioStream *audioStream = Audio::makeAIFFStream(stream, DisposeAfterUse::YES);
234 	if (!audioStream) {
235 		warning("Track '%s' is not an AIFF track", fileName.c_str());
236 		return false;
237 	}
238 
239 	Audio::SeekableAudioStream *seekStream = dynamic_cast<Audio::SeekableAudioStream *>(audioStream);
240 	if (!seekStream) {
241 		warning("Track '%s' is not seekable", fileName.c_str());
242 		return false;
243 	}
244 
245 	Audio::Timestamp start = Audio::Timestamp(0, startFrame, 75);
246 	Audio::Timestamp end = duration ? Audio::Timestamp(0, startFrame + duration, 75) : seekStream->getLength();
247 
248 	// Fake emulation since we're really playing an AIFF file
249 	_emulating = true;
250 
251 	_mixer->playStream(Audio::Mixer::kMusicSoundType, &_handle,
252 	                   Audio::makeLoopingAudioStream(seekStream, start, end, (numLoops < 1) ? numLoops + 1 : numLoops), -1, _cd.volume, _cd.balance);
253 	return true;
254 }
255 
findTrackNames(const Common::String & drivePath)256 bool MacOSXAudioCDManager::findTrackNames(const Common::String &drivePath) {
257 	Common::FSNode directory(drivePath);
258 
259 	if (!directory.exists()) {
260 		warning("Directory '%s' does not exist", drivePath.c_str());
261 		return false;
262 	}
263 
264 	if (!directory.isDirectory()) {
265 		warning("'%s' is not a directory", drivePath.c_str());
266 		return false;
267 	}
268 
269 	Common::FSList children;
270 	if (!directory.getChildren(children, Common::FSNode::kListFilesOnly)) {
271 		warning("Failed to find children for '%s'", drivePath.c_str());
272 		return false;
273 	}
274 
275 	for (uint32 i = 0; i < children.size(); i++) {
276 		if (!children[i].isDirectory()) {
277 			Common::String fileName = children[i].getName();
278 
279 			if (fileName.hasSuffix(".aiff") || fileName.hasSuffix(".cdda")) {
280 				uint j = 0;
281 
282 				// Search for the track ID in the file name.
283 				for (; j < fileName.size() && !Common::isDigit(fileName[j]); j++)
284 					;
285 
286 				const char *trackIDString = fileName.c_str() + j;
287 				char *endPtr = nullptr;
288 				long trackID = strtol(trackIDString, &endPtr, 10);
289 
290 				if (trackIDString != endPtr && trackID > 0 && trackID < UINT_MAX) {
291 					_trackMap[trackID - 1] = drivePath + '/' + fileName;
292 				} else {
293 					warning("Invalid track file name: '%s'", fileName.c_str());
294 				}
295 			}
296 		}
297 	}
298 
299 	return true;
300 }
301 
createMacOSXAudioCDManager()302 AudioCDManager *createMacOSXAudioCDManager() {
303 	return new MacOSXAudioCDManager();
304 }
305 
306 #endif // MACOSX
307