1 /*****************************************************************************\
2      Snes9x - Portable Super Nintendo Entertainment System (TM) emulator.
3                 This file is licensed under the Snes9x License.
4    For further information, consult the LICENSE file in the root directory.
5 \*****************************************************************************/
6 
7 /***********************************************************************************
8   SNES9X for Mac OS (c) Copyright John Stiles
9 
10   Snes9x for Mac OS X
11 
12   (c) Copyright 2001 - 2011  zones
13   (c) Copyright 2002 - 2005  107
14   (c) Copyright 2002         PB1400c
15   (c) Copyright 2004         Alexander and Sander
16   (c) Copyright 2004 - 2005  Steven Seeger
17   (c) Copyright 2005         Ryan Vogt
18  ***********************************************************************************/
19 
20 
21 #include "snes9x.h"
22 #include "memmap.h"
23 #include "apu.h"
24 
25 #include <QuickTime/QuickTime.h>
26 
27 #include "mac-prefix.h"
28 #include "mac-gworld.h"
29 #include "mac-os.h"
30 #include "mac-screenshot.h"
31 #include "mac-quicktime.h"
32 
33 #define	kMovDoubleSize		(1 << 0)
34 #define	kMovExtendedHeight	(1 << 1)
35 
36 static void CheckError (OSStatus, int);
37 static void MacQTOpenVideoComponent (ComponentInstance *);
38 static void MacQTCloseVideoComponent (ComponentInstance);
39 static OSStatus WriteFrameCallBack (void *, ICMCompressionSessionRef, OSStatus, ICMEncodedFrameRef, void *);
40 
41 typedef struct
42 {
43 	Movie							movie;
44 	Track							vTrack, sTrack;
45 	Media							vMedia, sMedia;
46 	ComponentInstance				vci;
47 	SoundDescriptionHandle			soundDesc;
48 	DataHandler						dataHandler;
49 	Handle							soundBuffer;
50 	Handle							dataRef;
51 	OSType							dataRefType;
52 	CVPixelBufferPoolRef			pool;
53 	ICMCompressionSessionRef		session;
54 	ICMCompressionSessionOptionsRef	option;
55 	CGImageRef						srcImage;
56 	TimeValue64						timeStamp;
57 	long							keyFrame, keyFrameCount;
58 	long							frameSkip, frameSkipCount;
59 	int								width, height;
60 	int								soundBufferSize;
61 	int								samplesPerSec;
62 }	MacQTState;
63 
64 static MacQTState	sqt;
65 
66 
CheckError(OSStatus err,int n)67 static void CheckError (OSStatus err, int n)
68 {
69 	if (err != noErr)
70 	{
71 		char	mes[32];
72 
73 		sprintf(mes, "quicktime %02d", n);
74 		QuitWithFatalError(err, mes);
75 	}
76 }
77 
MacQTOpenVideoComponent(ComponentInstance * rci)78 static void MacQTOpenVideoComponent (ComponentInstance *rci)
79 {
80 	OSStatus			err;
81 	ComponentInstance	ci;
82 	CFDataRef			data;
83 
84 	ci = OpenDefaultComponent(StandardCompressionType, StandardCompressionSubType);
85 
86 	data = (CFDataRef) CFPreferencesCopyAppValue(CFSTR("QTVideoSetting"), kCFPreferencesCurrentApplication);
87 	if (data)
88 	{
89 		CFIndex	len;
90 		Handle	hdl;
91 
92 		len = CFDataGetLength(data);
93 		hdl = NewHandleClear((Size) len);
94 		if (MemError() == noErr)
95 		{
96 			HLock(hdl);
97 			CFDataGetBytes(data, CFRangeMake(0, len), (unsigned char *) *hdl);
98 			err = SCSetInfo(ci, scSettingsStateType, &hdl);
99 
100 			DisposeHandle(hdl);
101 		}
102 
103 		CFRelease(data);
104 	}
105 	else
106 	{
107 		SCSpatialSettings	ss;
108 		SCTemporalSettings	ts;
109 
110 		ss.codecType       = kAnimationCodecType;
111 		ss.codec           = 0;
112 		ss.depth           = 16;
113 		ss.spatialQuality  = codecMaxQuality;
114 		err = SCSetInfo(ci, scSpatialSettingsType, &ss);
115 
116 		ts.frameRate       = FixRatio(Memory.ROMFramesPerSecond, 1);
117 		ts.keyFrameRate    = Memory.ROMFramesPerSecond;
118 		ts.temporalQuality = codecMaxQuality;
119 		err = SCSetInfo(ci, scTemporalSettingsType, &ts);
120 	}
121 
122 	*rci = ci;
123 }
124 
MacQTCloseVideoComponent(ComponentInstance ci)125 static void MacQTCloseVideoComponent (ComponentInstance ci)
126 {
127 	OSStatus	err;
128 
129 	err = CloseComponent(ci);
130 }
131 
MacQTVideoConfig(void)132 void MacQTVideoConfig (void)
133 {
134 	OSStatus			err;
135 	ComponentInstance	ci;
136 
137 	MacQTOpenVideoComponent(&ci);
138 
139 	long	flag;
140 	flag = scListEveryCodec | scAllowZeroKeyFrameRate | scDisableFrameRateItem | scAllowEncodingWithCompressionSession;
141 	err = SCSetInfo(ci, scPreferenceFlagsType, &flag);
142 
143 	SCWindowSettings	ws;
144 	ws.size          = sizeof(SCWindowSettings);
145 	ws.windowRefKind = scWindowRefKindCarbon;
146 	ws.parentWindow  = NULL;
147 	err = SCSetInfo(ci, scWindowOptionsType, &ws);
148 
149 	err = SCRequestSequenceSettings(ci);
150 	if (err == noErr)
151 	{
152 		CFDataRef	data;
153 		Handle		hdl;
154 
155 		err = SCGetInfo(ci, scSettingsStateType, &hdl);
156 		if (err == noErr)
157 		{
158 			HLock(hdl);
159 			data = CFDataCreate(kCFAllocatorDefault, (unsigned char *) *hdl, GetHandleSize(hdl));
160 			if (data)
161 			{
162 				CFPreferencesSetAppValue(CFSTR("QTVideoSetting"), data, kCFPreferencesCurrentApplication);
163 				CFRelease(data);
164 			}
165 
166 			DisposeHandle(hdl);
167 		}
168 	}
169 
170 	MacQTCloseVideoComponent(ci);
171 }
172 
MacQTStartRecording(char * path)173 void MacQTStartRecording (char *path)
174 {
175 	OSStatus	err;
176 	CFStringRef str;
177 	CFURLRef	url;
178 
179 	memset(&sqt, 0, sizeof(sqt));
180 
181 	// storage
182 
183 	str = CFStringCreateWithCString(kCFAllocatorDefault, path, kCFStringEncodingUTF8);
184 	url = CFURLCreateWithFileSystemPath(kCFAllocatorDefault, str, kCFURLPOSIXPathStyle, false);
185 	err = QTNewDataReferenceFromCFURL(url, 0, &(sqt.dataRef), &(sqt.dataRefType));
186 	CheckError(err, 21);
187 	CFRelease(url);
188 	CFRelease(str);
189 
190 	err = CreateMovieStorage(sqt.dataRef, sqt.dataRefType, 'TVOD', smSystemScript, createMovieFileDeleteCurFile | newMovieActive, &(sqt.dataHandler), &(sqt.movie));
191 	CheckError(err, 22);
192 
193 	// video
194 
195 	MacQTOpenVideoComponent(&(sqt.vci));
196 
197 	long				flag;
198 	SCTemporalSettings	ts;
199 
200 	flag = scAllowEncodingWithCompressionSession;
201 	err = SCSetInfo(sqt.vci, scPreferenceFlagsType, &flag);
202 
203 	err = SCGetInfo(sqt.vci, scTemporalSettingsType, &ts);
204 	ts.frameRate = FixRatio(Memory.ROMFramesPerSecond, 1);
205 	if (ts.keyFrameRate < 1)
206 		ts.keyFrameRate = Memory.ROMFramesPerSecond;
207 	sqt.keyFrame  = sqt.keyFrameCount  = ts.keyFrameRate;
208 	sqt.frameSkip = sqt.frameSkipCount = (macQTMovFlag & 0xFF00) >> 8;
209 	err = SCSetInfo(sqt.vci, scTemporalSettingsType, &ts);
210 
211 	sqt.width  = ((macQTMovFlag & kMovDoubleSize) ? 2 : 1) * SNES_WIDTH;
212 	sqt.height = ((macQTMovFlag & kMovDoubleSize) ? 2 : 1) * ((macQTMovFlag & kMovExtendedHeight) ? SNES_HEIGHT_EXTENDED : SNES_HEIGHT);
213 
214 	sqt.srcImage = NULL;
215 	sqt.timeStamp = 0;
216 
217 	SCSpatialSettings			ss;
218 	ICMEncodedFrameOutputRecord	record;
219 	ICMMultiPassStorageRef		nullStorage = NULL;
220 
221 	err = SCCopyCompressionSessionOptions(sqt.vci, &(sqt.option));
222 	CheckError(err, 61);
223 	err = ICMCompressionSessionOptionsSetProperty(sqt.option, kQTPropertyClass_ICMCompressionSessionOptions, kICMCompressionSessionOptionsPropertyID_MultiPassStorage, sizeof(ICMMultiPassStorageRef), &nullStorage);
224 
225 	record.encodedFrameOutputCallback = WriteFrameCallBack;
226 	record.encodedFrameOutputRefCon   = NULL;
227 	record.frameDataAllocator         = NULL;
228 	err = SCGetInfo(sqt.vci, scSpatialSettingsType, &ss);
229 	err = ICMCompressionSessionCreate(kCFAllocatorDefault, sqt.width, sqt.height, ss.codecType, Memory.ROMFramesPerSecond, sqt.option, NULL, &record, &(sqt.session));
230 	CheckError(err, 62);
231 
232 	CFMutableDictionaryRef	dic;
233 	CFNumberRef				val;
234 	OSType					pix = k16BE555PixelFormat;
235 	int						row = sqt.width * 2;
236 
237 	dic = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
238 
239 	val = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &pix);
240 	CFDictionaryAddValue(dic, kCVPixelBufferPixelFormatTypeKey, val);
241 	CFRelease(val);
242 
243 	val = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &(sqt.width));
244 	CFDictionaryAddValue(dic, kCVPixelBufferWidthKey, val);
245 	CFRelease(val);
246 
247 	val = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &(sqt.height));
248 	CFDictionaryAddValue(dic, kCVPixelBufferHeightKey, val);
249 	CFRelease(val);
250 
251 	val = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &row);
252 	CFDictionaryAddValue(dic, kCVPixelBufferBytesPerRowAlignmentKey, val);
253 	CFRelease(val);
254 
255 	CFDictionaryAddValue(dic, kCVPixelBufferCGImageCompatibilityKey, kCFBooleanTrue);
256 	CFDictionaryAddValue(dic, kCVPixelBufferCGBitmapContextCompatibilityKey, kCFBooleanTrue);
257 
258 	err = CVPixelBufferPoolCreate(kCFAllocatorDefault, NULL, dic, &(sqt.pool));
259 	CheckError(err, 63);
260 
261 	CFRelease(dic);
262 
263 	sqt.vTrack = NewMovieTrack(sqt.movie, FixRatio(sqt.width, 1), FixRatio(sqt.height, 1), kNoVolume);
264 	CheckError(GetMoviesError(), 23);
265 
266 	sqt.vMedia = NewTrackMedia(sqt.vTrack, VideoMediaType, Memory.ROMFramesPerSecond, NULL, 0);
267 	CheckError(GetMoviesError(), 24);
268 
269 	err = BeginMediaEdits(sqt.vMedia);
270 	CheckError(err, 25);
271 
272 	// sound
273 
274 	sqt.soundDesc = (SoundDescriptionHandle) NewHandleClear(sizeof(SoundDescription));
275 	CheckError(MemError(), 26);
276 
277 	(**sqt.soundDesc).descSize    = sizeof(SoundDescription);
278 #ifdef __BIG_ENDIAN__
279 	(**sqt.soundDesc).dataFormat  = Settings.SixteenBitSound ? k16BitBigEndianFormat    : k8BitOffsetBinaryFormat;
280 #else
281 	(**sqt.soundDesc).dataFormat  = Settings.SixteenBitSound ? k16BitLittleEndianFormat : k8BitOffsetBinaryFormat;
282 #endif
283 	(**sqt.soundDesc).numChannels = Settings.Stereo ? 2 : 1;
284 	(**sqt.soundDesc).sampleSize  = Settings.SixteenBitSound ? 16 : 8;
285 	(**sqt.soundDesc).sampleRate  = (UnsignedFixed) FixRatio(Settings.SoundPlaybackRate, 1);
286 
287 	sqt.samplesPerSec = Settings.SoundPlaybackRate / Memory.ROMFramesPerSecond;
288 
289 	sqt.soundBufferSize = sqt.samplesPerSec;
290 	if (Settings.SixteenBitSound)
291 		sqt.soundBufferSize <<= 1;
292 	if (Settings.Stereo)
293 		sqt.soundBufferSize <<= 1;
294 
295 	sqt.soundBuffer = NewHandleClear(sqt.soundBufferSize);
296 	CheckError(MemError(), 27);
297 	HLock(sqt.soundBuffer);
298 
299 	sqt.sTrack = NewMovieTrack(sqt.movie, 0, 0, kFullVolume);
300 	CheckError(GetMoviesError(), 28);
301 
302 	sqt.sMedia = NewTrackMedia(sqt.sTrack, SoundMediaType, Settings.SoundPlaybackRate, NULL, 0);
303 	CheckError(GetMoviesError(), 29);
304 
305 	err = BeginMediaEdits(sqt.sMedia);
306 	CheckError(err, 30);
307 }
308 
MacQTRecordFrame(int width,int height)309 void MacQTRecordFrame (int width, int height)
310 {
311 	OSStatus	err;
312 
313 	// video
314 
315 	if (sqt.frameSkipCount == sqt.frameSkip)
316 	{
317 		CVPixelBufferRef	buf;
318 
319 		err = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, sqt.pool, &buf);
320 		if (err == noErr)
321 		{
322 			CGColorSpaceRef		color;
323 			CGContextRef		ctx;
324 			uint16				*p;
325 
326 			err = CVPixelBufferLockBaseAddress(buf, 0);
327 			p = (uint16 *) CVPixelBufferGetBaseAddress(buf);
328 
329 			color = CGColorSpaceCreateDeviceRGB();
330 			ctx = CGBitmapContextCreate((void *) p, sqt.width, sqt.height, 5, sqt.width * 2, color, kCGImageAlphaNoneSkipFirst | ((systemVersion >= 0x1040) ? kCGBitmapByteOrder16Host : 0));
331 			CGContextSetShouldAntialias(ctx, false);
332 
333 			if (sqt.srcImage)
334 				CGImageRelease(sqt.srcImage);
335 			sqt.srcImage = CreateGameScreenCGImage();
336 
337 			CGRect	dst = CGRectMake(0.0f, 0.0f, (float) sqt.width, (float) sqt.height);
338 
339 			if ((!(height % SNES_HEIGHT_EXTENDED)) && (!(macQTMovFlag & kMovExtendedHeight)))
340 			{
341 				CGRect	src;
342 
343 				src.size.width  = (float) width;
344 				src.size.height = (float) ((height > 256) ? (SNES_HEIGHT << 1) : SNES_HEIGHT);
345 				src.origin.x    = (float) 0;
346 				src.origin.y    = (float) height - src.size.height;
347 				DrawSubCGImage(ctx, sqt.srcImage, src, dst);
348 			}
349 			else
350 			if ((sqt.height << 1) % height)
351 			{
352 				CGContextSetRGBFillColor(ctx, 0.0f, 0.0f, 0.0f, 1.0f);
353 				CGContextFillRect(ctx, dst);
354 
355 				float	dh  = (float) ((sqt.height > 256) ? (SNES_HEIGHT << 1) : SNES_HEIGHT);
356 				float	ofs = (float) ((int) ((drawoverscan ? 1.0 : 0.5) * ((float) sqt.height - dh) + 0.5));
357 				dst = CGRectMake(0.0f, ofs, (float) sqt.width, dh);
358 				CGContextDrawImage(ctx, dst, sqt.srcImage);
359 			}
360 			else
361 				CGContextDrawImage(ctx, dst, sqt.srcImage);
362 
363 			CGContextRelease(ctx);
364 			CGColorSpaceRelease(color);
365 
366 		#ifndef __BIG_ENDIAN__
367 			for (int i = 0; i < sqt.width * sqt.height; i++)
368 				SWAP_WORD(p[i]);
369 		#endif
370 
371 			err = CVPixelBufferUnlockBaseAddress(buf, 0);
372 
373 			err = ICMCompressionSessionEncodeFrame(sqt.session, buf, sqt.timeStamp, 0, kICMValidTime_DisplayTimeStampIsValid, NULL, NULL, NULL);
374 
375 			CVPixelBufferRelease(buf);
376 		}
377 
378 		sqt.keyFrameCount--;
379 		if (sqt.keyFrameCount <= 0)
380 			sqt.keyFrameCount = sqt.keyFrame;
381 	}
382 
383 	sqt.frameSkipCount--;
384 	if (sqt.frameSkipCount < 0)
385 		sqt.frameSkipCount = sqt.frameSkip;
386 
387 	sqt.timeStamp++;
388 
389 	// sound
390 
391 	int	sample_count = sqt.soundBufferSize;
392 	if (Settings.SixteenBitSound)
393 		sample_count >>= 1;
394 
395 	S9xMixSamples((uint8 *) *(sqt.soundBuffer), sample_count);
396 
397 	err = AddMediaSample(sqt.sMedia, sqt.soundBuffer, 0, sqt.soundBufferSize, 1, (SampleDescriptionHandle) sqt.soundDesc, sqt.samplesPerSec, mediaSampleNotSync, NULL);
398 }
399 
WriteFrameCallBack(void * refCon,ICMCompressionSessionRef session,OSStatus r,ICMEncodedFrameRef frame,void * reserved)400 static OSStatus WriteFrameCallBack (void *refCon, ICMCompressionSessionRef session, OSStatus r, ICMEncodedFrameRef frame, void *reserved)
401 {
402 	OSStatus	err;
403 
404 	err = AddMediaSampleFromEncodedFrame(sqt.vMedia, frame, NULL);
405 	return (err);
406 }
407 
MacQTStopRecording(void)408 void MacQTStopRecording (void)
409 {
410 	OSStatus   err;
411 
412 	// video
413 
414 	err = ICMCompressionSessionCompleteFrames(sqt.session, true, 0, sqt.timeStamp);
415 	err = ExtendMediaDecodeDurationToDisplayEndTime(sqt.vMedia, NULL);
416 
417 	err = EndMediaEdits(sqt.vMedia);
418 	CheckError(err, 52);
419 
420 	err = InsertMediaIntoTrack(sqt.vTrack, 0, 0, (TimeValue) GetMediaDisplayDuration(sqt.vMedia), fixed1);
421 	CheckError(err, 58);
422 
423 	CGImageRelease(sqt.srcImage);
424 	CVPixelBufferPoolRelease(sqt.pool);
425 	ICMCompressionSessionRelease(sqt.session);
426 	ICMCompressionSessionOptionsRelease(sqt.option);
427 
428 	// sound
429 
430 	err = EndMediaEdits(sqt.sMedia);
431 	CheckError(err, 54);
432 
433 	err = InsertMediaIntoTrack(sqt.sTrack, 0, 0, GetMediaDuration(sqt.sMedia), fixed1);
434 	CheckError(err, 55);
435 
436 	DisposeHandle(sqt.soundBuffer);
437 	DisposeHandle((Handle) sqt.soundDesc);
438 
439 	// storage
440 
441 	err = AddMovieToStorage(sqt.movie, sqt.dataHandler);
442 	CheckError(err, 56);
443 
444 	MacQTCloseVideoComponent(sqt.vci);
445 
446 	err = CloseMovieStorage(sqt.dataHandler);
447 	CheckError(err, 57);
448 
449 	DisposeHandle(sqt.dataRef);
450 	DisposeMovie(sqt.movie);
451 }
452