|
|
1.1 root 1: /*
2: Hatari - avi_record.c
3:
4: This file is distributed under the GNU Public License, version 2 or at
5: your option any later version. Read the file gpl.txt for details.
6:
7: AVI File recording
8:
9: This allows Hatari to record a video file, with both video and audio
10: streams, at full frame rate.
11:
12: Video frames are saved using the current video frequency of the emulated
13: machine (50 Hz, 60 Hz, 70 Hz, ...). Frames can be stored using different
14: codecs. So far, supported codecs are :
15: - BMP : uncompressed RGB images. Very fast to save, very few cpu needed
16: but requires a lot of disk bandwidth and a lot of space.
17: - PNG : compressed RBG images. Depending on the compression level, this
18: can require more cpu and could slow down Hatari. As compressed images
19: are much smaller than BMP images, this will require less space on disk
1.1.1.2 ! root 20: and much less disk bandwidth. Compression levels 3 or 4 give good
1.1 root 21: tradeoff between cpu usage and file size and should not slow down Hatari
22: with recent computers.
23:
24: PNG compression will often give a x20 ratio when compared to BMP and should
25: be used if you have a powerful enough cpu.
26:
27: Sound is saved as 16 bits pcm stereo, using the current Hatari sound output
28: frequency. For best accuracy, sound frequency should be a multiple of the
29: video frequency ; this means 44.1 kHz is the best choice for 50/60 Hz video.
30:
31: The AVI file is divided into multiple chunks. Hatari will save one video stream
1.1.1.2 ! root 32: and one audio stream, so the overall structure of the file is the following :
1.1 root 33:
34: RIFF avi
35: LIST
36: hdrl
37: avih
38: LIST
39: strl
40: strh (vids)
41: strf
42: LIST
43: strl
44: strh (auds)
45: strf
46: LIST
47: INFO
48: LIST
49: movi
50: 00db
51: 01wb
52: ...
53: idx1
54: */
55:
56: const char AVIRecord_fileid[] = "Hatari avi_record.c : " __DATE__ " " __TIME__;
57:
58: #include <SDL.h>
59: #include <SDL_endian.h>
60:
61: #include "main.h"
62: #include "audio.h"
63: #include "configuration.h"
64: #include "log.h"
65: #include "screen.h"
66: #include "screenSnapShot.h"
67: #include "sound.h"
68: #include "statusbar.h"
69: #include "avi_record.h"
70:
71: /* after above that brings in config.h */
72: #if HAVE_LIBPNG
73: #include <png.h>
74: #endif
75:
76: #include "pixel_convert.h" /* inline functions */
77:
78:
79:
80: typedef struct
81: {
82: Uint8 ChunkName[4]; /* '00db', '01wb', 'idx1' */
83: Uint8 ChunkSize[4];
84: } AVI_CHUNK;
85:
86:
87: typedef struct
88: {
89: Uint8 identifier[4]; /* '00db', '01wb', 'idx1' */
90: Uint8 flags[4];
91: Uint8 offset[4];
92: Uint8 length[4];
93: } AVI_CHUNK_INDEX;
94:
95:
96: typedef struct
97: {
98: Uint8 ChunkName[4]; /* 'strh' */
99: Uint8 ChunkSize[4];
100:
101: Uint8 stream_type[4]; /* 'vids' or 'auds' */
102: Uint8 stream_handler[4];
103: Uint8 flags[4];
104: Uint8 priority[2];
105: Uint8 language[2];
106: Uint8 initial_frames[4];
107: Uint8 time_scale[4];
108: Uint8 data_rate[4];
109: Uint8 start_time[4];
110: Uint8 data_length[4];
111: Uint8 buffer_size[4];
112: Uint8 quality[4];
113: Uint8 sample_size[4];
114: Uint8 dest_left[2];
115: Uint8 dest_top[2];
116: Uint8 dest_right[2];
117: Uint8 dest_bottom[2];
118: } AVI_STREAM_HEADER;
119:
120:
121: typedef struct
122: {
123: Uint8 ChunkName[4]; /* 'strf' */
124: Uint8 ChunkSize[4];
125:
126: Uint8 size[4];
127: Uint8 width[4];
128: Uint8 height[4];
129: Uint8 planes[2];
130: Uint8 bit_count[2];
131: Uint8 compression[4];
132: Uint8 size_image[4];
133: Uint8 xpels_meter[4];
134: Uint8 ypels_meter[4];
135: Uint8 clr_used[4];
136: Uint8 clr_important[4];
137: } AVI_STREAM_FORMAT_VIDS;
138:
139: typedef struct
140: {
141: Uint8 ChunkName[4]; /* 'LIST' */
142: Uint8 ChunkSize[4];
143:
144: Uint8 Name[4]; /* 'strl' */
145: AVI_STREAM_HEADER Header; /* 'strh' */
146: AVI_STREAM_FORMAT_VIDS Format; /* 'strf' */
147: } AVI_STREAM_LIST_VIDS;
148:
149:
150: typedef struct
151: {
152: Uint8 ChunkName[4]; /* 'strf' */
153: Uint8 ChunkSize[4];
154:
155: Uint8 codec[2];
156: Uint8 channels[2];
157: Uint8 sample_rate[4];
158: Uint8 bit_rate[4];
159: Uint8 block_align[2];
160: Uint8 bits_per_sample[2];
161: Uint8 ext_size[2];
162: } AVI_STREAM_FORMAT_AUDS;
163:
164: typedef struct
165: {
166: Uint8 ChunkName[4]; /* 'LIST' */
167: Uint8 ChunkSize[4];
168:
169: Uint8 Name[4]; /* 'strl' */
170: AVI_STREAM_HEADER Header; /* 'strh' */
171: AVI_STREAM_FORMAT_AUDS Format; /* 'strf' */
172: } AVI_STREAM_LIST_AUDS;
173:
174:
175: typedef struct {
176: Uint8 ChunkName[4]; /* 'avih' */
177: Uint8 ChunkSize[4];
178:
179: Uint8 microsec_per_frame[4];
180: Uint8 max_bytes_per_second[4];
181: Uint8 padding_granularity[4];
182: Uint8 flags[4];
183: Uint8 total_frames[4];
184: Uint8 init_frame[4];
185: Uint8 nb_streams[4];
186: Uint8 buffer_size[4];
187: Uint8 width[4];
188: Uint8 height[4];
189: Uint8 scale[4];
190: Uint8 rate[4];
191: Uint8 start[4];
192: Uint8 length[4];
193: } AVI_STREAM_AVIH;
194:
195: typedef struct
196: {
197: Uint8 ChunkName[4]; /* 'LIST' */
198: Uint8 ChunkSize[4];
199:
200: Uint8 Name[4]; /* 'hdrl' */
201: AVI_STREAM_AVIH Header;
202: } AVI_STREAM_LIST_AVIH;
203:
204:
205: typedef struct {
206: Uint8 ChunkName[4]; /* 'ISFT' (software used) */
207: Uint8 ChunkSize[4];
208:
209: // Uint8 Text[2]; /* Text's size should be multiple of 2 (including '\0') */
210: } AVI_STREAM_INFO;
211:
212: typedef struct
213: {
214: Uint8 ChunkName[4]; /* 'LIST' */
215: Uint8 ChunkSize[4];
216:
217: Uint8 Name[4]; /* 'INFO' */
218: AVI_STREAM_INFO Info;
219: } AVI_STREAM_LIST_INFO;
220:
221:
222: typedef struct
223: {
224: Uint8 ChunkName[4]; /* 'LIST' */
225: Uint8 ChunkSize[4];
226:
227: Uint8 Name[4]; /* 'movi' */
228: } AVI_STREAM_LIST_MOVI;
229:
230:
231: typedef struct
232: {
233: Uint8 signature[4]; /* 'RIFF' */
234: Uint8 filesize[4];
235: Uint8 type[4]; /* 'AVI ' */
236:
237: } RIFF_HEADER;
238:
239:
240: typedef struct {
241: RIFF_HEADER RiffHeader;
242:
243: AVI_STREAM_LIST_AVIH AviHeader;
244:
245: AVI_STREAM_LIST_VIDS VideoStream;
246: AVI_STREAM_LIST_AUDS AudioStream;
247:
248: } AVI_FILE_HEADER;
249:
250:
251:
252: #define AUDIO_STREAM_WAVE_FORMAT_PCM 0x0001
253:
254: #define VIDEO_STREAM_RGB 0x00000000 /* fourcc for BMP video frames */
255: #define VIDEO_STREAM_PNG "MPNG" /* fourcc for PNG video frames */
256:
257: #define AVIF_HASINDEX 0x00000010 /* index at the end of the file */
258: #define AVIF_ISINTERLEAVED 0x00000100 /* data are interleaved */
259: #define AVIF_TRUSTCKTYPE 0x00000800 /* trust chunk type */
260:
261: #define AVIIF_KEYFRAME 0x00000010 /* frame is a keyframe */
262:
263:
264: typedef struct {
265: /* Input params to start recording */
266: int VideoCodec;
267: int VideoCodecCompressionLevel; /* 0-9 for png compression */
268:
269: SDL_Surface *Surface;
270:
271: int CropLeft;
272: int CropRight;
273: int CropTop;
274: int CropBottom;
275:
1.1.1.2 ! root 276: int Fps; /* Fps << 24 */
! 277: int Fps_scale; /* 1 << 24 */
1.1 root 278:
279: int AudioCodec;
280: int AudioFreq;
281:
282: /* Internal data used by the avi recorder */
283: int Width;
284: int Height;
285: int BitCount;
286: FILE *FileOut; /* file to write to */
287: int TotalVideoFrames; /* number of recorded video frames */
288: int TotalAudioSamples; /* number of recorded audio samples */
289: long MoviChunkPosStart; /* as returned by ftell() */
290: long MoviChunkPosEnd; /* as returned by ftell() */
291: } RECORD_AVI_PARAMS;
292:
293:
294:
295: bool bRecordingAvi = false;
296:
297: static RECORD_AVI_PARAMS AviParams;
298: static AVI_FILE_HEADER AviFileHeader;
299:
300:
301: static void Avi_StoreU16 ( Uint8 *p , Uint16 val );
302: static void Avi_StoreU32 ( Uint8 *p , Uint32 val );
303: static void Avi_Store4cc ( Uint8 *p , const char *text );
304: static Uint32 Avi_ReadU32 ( Uint8 *p );
305:
306: static int Avi_GetBmpSize ( int Width , int Height , int BitCount );
307:
308: static bool Avi_RecordVideoStream_BMP ( RECORD_AVI_PARAMS *pAviParams );
309: #if HAVE_LIBPNG
310: static bool Avi_RecordVideoStream_PNG ( RECORD_AVI_PARAMS *pAviParams );
311: #endif
312: static bool Avi_RecordAudioStream_PCM ( RECORD_AVI_PARAMS *pAviParams , Sint16 pSamples[][2], int SampleIndex, int SampleLength );
313:
314: static void Avi_BuildFileHeader ( RECORD_AVI_PARAMS *pAviParams , AVI_FILE_HEADER *pAviFileHeader );
315: static bool Avi_BuildIndex ( RECORD_AVI_PARAMS *pAviParams );
316:
317: static bool Avi_StartRecording_WithParams ( RECORD_AVI_PARAMS *pAviParams , char *AviFileName );
318: static bool Avi_StopRecording_WithParams ( RECORD_AVI_PARAMS *pAviParams );
319:
320:
321:
322:
323: static void Avi_StoreU16 ( Uint8 *p , Uint16 val )
324: {
325: *p++ = val & 0xff;
326: val >>= 8;
327: *p = val & 0xff;
328: }
329:
330:
331: static void Avi_StoreU32 ( Uint8 *p , Uint32 val )
332: {
333: *p++ = val & 0xff;
334: val >>= 8;
335: *p++ = val & 0xff;
336: val >>= 8;
337: *p++ = val & 0xff;
338: val >>= 8;
339: *p = val & 0xff;
340: }
341:
342:
343: static void Avi_Store4cc ( Uint8 *p , const char *text )
344: {
345: memcpy ( p , text , 4 );
346: }
347:
348:
349: static Uint32 Avi_ReadU32 ( Uint8 *p )
350: {
351: return (p[3]<<24) + (p[2]<<16) + (p[1]<<8) +p[0];
352: }
353:
354:
355: static int Avi_GetBmpSize ( int Width , int Height , int BitCount )
356: {
357: return ( Width * Height * BitCount / 8 ); /* bytes in one video frame */
358: }
359:
360:
361:
362: static bool Avi_RecordVideoStream_BMP ( RECORD_AVI_PARAMS *pAviParams )
363: {
364: AVI_CHUNK Chunk;
365: int SizeImage;
366: Uint8 LineBuf[ 3 * pAviParams->Width ]; /* temp buffer to convert to 24-bit BGR format */
367: Uint8 *pBitmapIn , *pBitmapOut;
368: int y;
369: int NeedLock;
370:
371: SizeImage = Avi_GetBmpSize ( pAviParams->Width , pAviParams->Height , pAviParams->BitCount );
372:
373: /* Write the video frame header */
374: Avi_Store4cc ( Chunk.ChunkName , "00db" ); /* stream 0, uncompressed DIB bytes */
375: Avi_StoreU32 ( Chunk.ChunkSize , SizeImage ); /* max size of RGB image */
376: if ( fwrite ( &Chunk , sizeof ( Chunk ) , 1 , pAviParams->FileOut ) != 1 )
377: {
378: perror ( "Avi_RecordVideoStream_BMP" );
379: Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to write bmp frame header" );
380: return false;
381: }
382:
383:
384: /* Write the video frame data */
385: NeedLock = SDL_MUSTLOCK( pAviParams->Surface );
386:
387: /* Points to the top left pixel after cropping borders */
388: /* For BMP format, frame is stored from bottom to top (origin is in bottom left corner) */
389: /* and bytes are in BGR order (not RGB) */
390: pBitmapIn = (Uint8 *)pAviParams->Surface->pixels
391: + pAviParams->Surface->pitch * ( pAviParams->CropTop + pAviParams->Height )
392: + pAviParams->CropLeft * pAviParams->Surface->format->BytesPerPixel;
393:
394: for ( y=0 ; y<pAviParams->Height ; y++ )
395: {
396: if ( NeedLock )
397: SDL_LockSurface ( pAviParams->Surface );
398:
399: pBitmapOut = LineBuf;
400: switch ( pAviParams->Surface->format->BytesPerPixel ) {
401: case 1 : PixelConvert_8to24Bits_BGR(LineBuf, pBitmapIn, pAviParams->Width, pAviParams->Surface->format->palette->colors);
402: break;
403: case 2 : PixelConvert_16to24Bits_BGR(LineBuf, (Uint16 *)pBitmapIn, pAviParams->Width, pAviParams->Surface->format);
404: break;
405: case 3 : PixelConvert_24to24Bits_BGR(LineBuf, pBitmapIn, pAviParams->Width);
406: break;
407: case 4 : PixelConvert_32to24Bits_BGR(LineBuf, (Uint32 *)pBitmapIn, pAviParams->Width, pAviParams->Surface->format);
408: break;
409: }
410:
411: if ( NeedLock )
412: SDL_UnlockSurface ( pAviParams->Surface );
413:
414: if ( (int)fwrite ( pBitmapOut , 1 , pAviParams->Width*3 , pAviParams->FileOut ) != pAviParams->Width*3 )
415: {
416: perror ( "Avi_RecordVideoStream_BMP" );
417: Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to write bmp video frame" );
418: return false;
419: }
420:
421: pBitmapIn -= pAviParams->Surface->pitch; /* go from bottom to top */
422: }
423:
424: return true;
425: }
426:
427:
428:
429: #if HAVE_LIBPNG
430: static bool Avi_RecordVideoStream_PNG ( RECORD_AVI_PARAMS *pAviParams )
431: {
432: AVI_CHUNK Chunk;
433: int SizeImage;
434: long ChunkPos;
435: Uint8 TempSize[4];
436:
437:
438: /* Write the video frame header */
439: ChunkPos = ftell ( pAviParams->FileOut );
440: Avi_Store4cc ( Chunk.ChunkName , "00dc" ); /* stream 0, compressed DIB bytes */
441: Avi_StoreU32 ( Chunk.ChunkSize , 0 ); /* size of PNG image (-> completed later) */
442: if ( fwrite ( &Chunk , sizeof ( Chunk ) , 1 , pAviParams->FileOut ) != 1 )
1.1.1.2 ! root 443: goto png_error;
! 444:
! 445: /* Write the video frame data */
! 446: SizeImage = ScreenSnapShot_SavePNG_ToFile ( pAviParams->Surface , pAviParams->FileOut ,
! 447: pAviParams->VideoCodecCompressionLevel , PNG_FILTER_NONE ,
! 448: pAviParams->CropLeft , pAviParams->CropRight , pAviParams->CropTop , pAviParams->CropBottom );
! 449: if ( SizeImage <= 0 )
! 450: goto png_error;
1.1 root 451: if ( SizeImage & 1 )
452: {
453: SizeImage++; /* add an extra '\0' byte to get an even size */
454: fputc ( '\0' , pAviParams->FileOut ); /* next chunk must be aligned on 16 bits boundary */
455: }
456:
457: /* Update the size of the video chunk */
458: Avi_StoreU32 ( TempSize , SizeImage );
459: if ( fseek ( pAviParams->FileOut , ChunkPos+4 , SEEK_SET ) != 0 )
1.1.1.2 ! root 460: goto png_error;
! 461: if ( fwrite ( TempSize , sizeof ( TempSize ) , 1 , pAviParams->FileOut ) != 1 )
! 462: goto png_error;
! 463:
! 464: /* Go to the end of the video frame data */
! 465: if ( fseek ( pAviParams->FileOut , 0 , SEEK_END ) != 0 )
! 466: goto png_error;
1.1 root 467: return true;
1.1.1.2 ! root 468:
! 469: png_error:
! 470: perror ( "Avi_RecordVideoStream_PNG" );
! 471: Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to write png frame" );
! 472: return false;
1.1 root 473: }
474: #endif /* HAVE_LIBPNG */
475:
476:
477:
478: bool Avi_RecordVideoStream ( void )
479: {
480: if ( AviParams.VideoCodec == AVI_RECORD_VIDEO_CODEC_BMP )
481: {
482: if ( Avi_RecordVideoStream_BMP ( &AviParams ) == false )
483: {
484: return false;
485: }
486: }
487: #if HAVE_LIBPNG
488: else if ( AviParams.VideoCodec == AVI_RECORD_VIDEO_CODEC_PNG )
489: {
490: if ( Avi_RecordVideoStream_PNG ( &AviParams ) == false )
491: {
492: return false;
493: }
494: }
495: #endif
496: else
497: {
498: return false;
499: }
500:
1.1.1.2 ! root 501: if (++AviParams.TotalVideoFrames % ( AviParams.Fps / AviParams.Fps_scale ) == 0)
! 502: {
! 503: int secs = AviParams.TotalVideoFrames / ( AviParams.Fps / AviParams.Fps_scale );
! 504: char str[6] = "00:00";
! 505: str[0] = '0' + (secs / 60) / 10;
! 506: str[1] = '0' + (secs / 60) % 10;
! 507: str[3] = '0' + (secs % 60) / 10;
! 508: str[4] = '0' + (secs % 60) % 10;
! 509: Main_SetTitle(str);
! 510: }
1.1 root 511: return true;
512: }
513:
514:
515:
516: static bool Avi_RecordAudioStream_PCM ( RECORD_AVI_PARAMS *pAviParams , Sint16 pSamples[][2] , int SampleIndex , int SampleLength )
517: {
518: AVI_CHUNK Chunk;
519: Sint16 sample[2];
520: int i;
521:
522: /* Write the audio frame header */
523: Avi_Store4cc ( Chunk.ChunkName , "01wb" ); /* stream 1, wave bytes */
524: Avi_StoreU32 ( Chunk.ChunkSize , SampleLength * 4 ); /* 16 bits, stereo -> 4 bytes */
525: if ( fwrite ( &Chunk , sizeof ( Chunk ) , 1 , pAviParams->FileOut ) != 1 )
526: {
527: perror ( "Avi_RecordAudioStream_PCM" );
528: Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to write pcm frame header" );
529: return false;
530: }
531:
532: /* Write the audio frame data */
533: for ( i = 0 ; i < SampleLength; i++ )
534: {
535: /* Convert sample to little endian */
536: sample[0] = SDL_SwapLE16 ( pSamples[ (SampleIndex+i) % MIXBUFFER_SIZE ][0]);
537: sample[1] = SDL_SwapLE16 ( pSamples[ (SampleIndex+i) % MIXBUFFER_SIZE ][1]);
538: /* And store */
539: if ( fwrite ( &sample , sizeof ( sample ) , 1 , pAviParams->FileOut ) != 1 )
540: {
541: perror ( "Avi_RecordAudioStream_PCM" );
542: Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to write pcm frame" );
543: return false;
544: }
545: }
546:
547: return true;
548: }
549:
550:
551:
552: bool Avi_RecordAudioStream ( Sint16 pSamples[][2] , int SampleIndex , int SampleLength )
553: {
554: if ( AviParams.AudioCodec == AVI_RECORD_AUDIO_CODEC_PCM )
555: {
556: if ( Avi_RecordAudioStream_PCM ( &AviParams , pSamples , SampleIndex , SampleLength ) == false )
557: {
558: return false;
559: }
560: }
561: else
562: {
563: return false;
564: }
565:
566: AviParams.TotalAudioSamples += SampleLength;
567: return true;
568: }
569:
570:
571:
572:
573: static void Avi_BuildFileHeader ( RECORD_AVI_PARAMS *pAviParams , AVI_FILE_HEADER *pAviFileHeader )
574: {
1.1.1.2 ! root 575: int Width , Height , BitCount , Fps , Fps_scale , SizeImage;
1.1 root 576: int AudioFreq;
577:
578: memset ( pAviFileHeader , 0 , sizeof ( *pAviFileHeader ) );
579:
580: Width = pAviParams->Width;
581: Height =pAviParams->Height;
582: BitCount = pAviParams->BitCount;
583: Fps = pAviParams->Fps;
1.1.1.2 ! root 584: Fps_scale = pAviParams->Fps_scale;
1.1 root 585: AudioFreq = pAviParams->AudioFreq;
586:
587: SizeImage = 0;
588: if ( pAviParams->VideoCodec == AVI_RECORD_VIDEO_CODEC_BMP )
589: SizeImage = Avi_GetBmpSize ( Width , Height , BitCount ); /* size of a BMP image */
590: else if ( pAviParams->VideoCodec == AVI_RECORD_VIDEO_CODEC_PNG )
591: SizeImage = Avi_GetBmpSize ( Width , Height , BitCount ); /* max size of a PNG image */
592:
593:
594: /* RIFF / AVI headers */
595: Avi_Store4cc ( pAviFileHeader->RiffHeader.signature , "RIFF" );
596: Avi_StoreU32 ( pAviFileHeader->RiffHeader.filesize , 0 ); /* total file size (-> completed later) */
597: Avi_Store4cc ( pAviFileHeader->RiffHeader.type , "AVI " );
598:
599: Avi_Store4cc ( pAviFileHeader->AviHeader.ChunkName , "LIST" );
600: Avi_StoreU32 ( pAviFileHeader->AviHeader.ChunkSize , sizeof ( AVI_STREAM_LIST_AVIH )
601: + sizeof ( AVI_STREAM_LIST_VIDS ) + sizeof ( AVI_STREAM_LIST_AUDS ) - 8 );
602: Avi_Store4cc ( pAviFileHeader->AviHeader.Name , "hdrl" );
603:
604: Avi_Store4cc ( pAviFileHeader->AviHeader.Header.ChunkName , "avih" );
605: Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.ChunkSize , sizeof ( AVI_STREAM_AVIH ) - 8 );
1.1.1.2 ! root 606: Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.microsec_per_frame , (Uint32)( ( 1000000 * (Sint64)Fps_scale ) / Fps ) );
! 607: Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.max_bytes_per_second , (Uint32)( ( (Sint64)SizeImage * Fps ) / Fps_scale + AudioFreq * 4 ) );
1.1 root 608: Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.padding_granularity , 0 );
609: Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.flags , AVIF_HASINDEX | AVIF_ISINTERLEAVED | AVIF_TRUSTCKTYPE );
610: Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.total_frames , 0 ); /* number of video frames (-> completed later) */
611: Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.init_frame , 0 );
612: Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.nb_streams , 2 ); /* 1 video and 1 audio */
613: Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.buffer_size , SizeImage );
614: Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.width , Width );
615: Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.height , Height );
616: Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.scale , 0 ); /* reserved */
617: Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.rate , 0 ); /* reserved */
618: Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.start , 0 ); /* reserved */
619: Avi_StoreU32 ( pAviFileHeader->AviHeader.Header.length , 0 ); /* reserved */
620:
621:
622: /* Video Stream */
623: Avi_Store4cc ( pAviFileHeader->VideoStream.ChunkName , "LIST" );
624: Avi_StoreU32 ( pAviFileHeader->VideoStream.ChunkSize , sizeof ( AVI_STREAM_LIST_VIDS ) - 8 );
625: Avi_Store4cc ( pAviFileHeader->VideoStream.Name , "strl" );
626:
627: Avi_Store4cc ( pAviFileHeader->VideoStream.Header.ChunkName , "strh" );
628: Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.ChunkSize , sizeof ( AVI_STREAM_HEADER ) - 8 );
629: Avi_Store4cc ( pAviFileHeader->VideoStream.Header.stream_type , "vids" );
630: if ( pAviParams->VideoCodec == AVI_RECORD_VIDEO_CODEC_BMP )
631: Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.stream_handler , VIDEO_STREAM_RGB );
632: else if ( pAviParams->VideoCodec == AVI_RECORD_VIDEO_CODEC_PNG )
633: Avi_Store4cc ( pAviFileHeader->VideoStream.Header.stream_handler , VIDEO_STREAM_PNG );
634: Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.flags , 0 );
635: Avi_StoreU16 ( pAviFileHeader->VideoStream.Header.priority , 0 );
636: Avi_StoreU16 ( pAviFileHeader->VideoStream.Header.language , 0 );
637: Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.initial_frames , 0 );
1.1.1.2 ! root 638: Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.time_scale , Fps_scale );
1.1 root 639: Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.data_rate , Fps );
640: Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.start_time , 0 );
641: Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.data_length , 0 ); /* number of video frames (-> completed later) */
642: Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.buffer_size , SizeImage ); /* size of an uncompressed frame */
643: Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.quality , -1 ); /* use default quality */
644: Avi_StoreU32 ( pAviFileHeader->VideoStream.Header.sample_size , 0 ); /* 0 for video */
645: Avi_StoreU16 ( pAviFileHeader->VideoStream.Header.dest_left , 0 );
646: Avi_StoreU16 ( pAviFileHeader->VideoStream.Header.dest_top , 0 );
647: Avi_StoreU16 ( pAviFileHeader->VideoStream.Header.dest_right , Width );
648: Avi_StoreU16 ( pAviFileHeader->VideoStream.Header.dest_bottom , Height );
649:
650: Avi_Store4cc ( pAviFileHeader->VideoStream.Format.ChunkName , "strf" );
651: Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.ChunkSize , sizeof ( AVI_STREAM_FORMAT_VIDS ) - 8 );
652: if ( pAviParams->VideoCodec == AVI_RECORD_VIDEO_CODEC_BMP )
653: {
654: Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.size , sizeof ( AVI_STREAM_FORMAT_VIDS ) - 8 );
655: Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.width , Width );
656: Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.height , Height );
657: Avi_StoreU16 ( pAviFileHeader->VideoStream.Format.planes , 1 ); /* always 1 */
658: Avi_StoreU16 ( pAviFileHeader->VideoStream.Format.bit_count , BitCount );
659: Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.compression , VIDEO_STREAM_RGB );
660: Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.size_image , SizeImage );
661: Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.xpels_meter , 0 );
662: Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.ypels_meter , 0 );
663: Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.clr_used , 0 ); /* no color map */
664: Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.clr_important , 0 ); /* no color map */
665: }
666: else if ( pAviParams->VideoCodec == AVI_RECORD_VIDEO_CODEC_PNG )
667: {
668: Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.size , sizeof ( AVI_STREAM_FORMAT_VIDS ) - 8 );
669: Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.width , Width );
670: Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.height , Height );
671: Avi_StoreU16 ( pAviFileHeader->VideoStream.Format.planes , 1 ); /* always 1 */
672: Avi_StoreU16 ( pAviFileHeader->VideoStream.Format.bit_count , BitCount );
673: Avi_Store4cc ( pAviFileHeader->VideoStream.Format.compression , VIDEO_STREAM_PNG );
674: Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.size_image , SizeImage ); /* max size if uncompressed */
675: Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.xpels_meter , 0 );
676: Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.ypels_meter , 0 );
677: Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.clr_used , 0 ); /* no color map */
678: Avi_StoreU32 ( pAviFileHeader->VideoStream.Format.clr_important , 0 ); /* no color map */
679: }
680:
681:
682: /* Audio Stream */
683: Avi_Store4cc ( pAviFileHeader->AudioStream.ChunkName , "LIST" );
684: Avi_StoreU32 ( pAviFileHeader->AudioStream.ChunkSize , sizeof ( AVI_STREAM_LIST_AUDS ) - 8 );
685: Avi_Store4cc ( pAviFileHeader->AudioStream.Name , "strl" );
686:
687: Avi_Store4cc ( pAviFileHeader->AudioStream.Header.ChunkName , "strh" );
688: Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.ChunkSize , sizeof ( AVI_STREAM_HEADER ) - 8 );
689: Avi_Store4cc ( pAviFileHeader->AudioStream.Header.stream_type , "auds" );
690: Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.stream_handler , 0 ); /* not used (or could be 1 for pcm ?) */
691: Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.flags , 0 );
692: Avi_StoreU16 ( pAviFileHeader->AudioStream.Header.priority , 0 );
693: Avi_StoreU16 ( pAviFileHeader->AudioStream.Header.language , 0 );
694: Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.initial_frames , 0 ); /* should be 1 in interleaved ? */
695: Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.time_scale , 1 );
696: Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.data_rate , AudioFreq );
697: Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.start_time , 0 );
698: Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.data_length , 0 ); /* number of audio samples (-> completed later) */
699: Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.buffer_size , AudioFreq * 4 / 50 ); /* min VBL freq is 50 Hz */
700: Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.quality , -1 ); /* use default quality */
701: Avi_StoreU32 ( pAviFileHeader->AudioStream.Header.sample_size , 4 ); /* 2 bytes, stereo */
702: Avi_StoreU16 ( pAviFileHeader->AudioStream.Header.dest_left , 0 );
703: Avi_StoreU16 ( pAviFileHeader->AudioStream.Header.dest_top , 0 );
704: Avi_StoreU16 ( pAviFileHeader->AudioStream.Header.dest_right , 0 );
705: Avi_StoreU16 ( pAviFileHeader->AudioStream.Header.dest_bottom , 0 );
706:
707: Avi_Store4cc ( pAviFileHeader->AudioStream.Format.ChunkName , "strf" );
708: Avi_StoreU32 ( pAviFileHeader->AudioStream.Format.ChunkSize , sizeof ( AVI_STREAM_FORMAT_AUDS ) - 8 );
709: if ( pAviParams->AudioCodec == AVI_RECORD_AUDIO_CODEC_PCM ) /* 16 bits stereo pcm */
710: {
711: Avi_StoreU16 ( pAviFileHeader->AudioStream.Format.codec , AUDIO_STREAM_WAVE_FORMAT_PCM ); /* 0x0001 */
712: Avi_StoreU16 ( pAviFileHeader->AudioStream.Format.channels , 2 );
713: Avi_StoreU32 ( pAviFileHeader->AudioStream.Format.sample_rate , AudioFreq );
714: Avi_StoreU32 ( pAviFileHeader->AudioStream.Format.bit_rate , AudioFreq * 2 * 2 ); /* 2 channels * 2 bytes */
715: Avi_StoreU16 ( pAviFileHeader->AudioStream.Format.block_align , 4 );
716: Avi_StoreU16 ( pAviFileHeader->AudioStream.Format.bits_per_sample , 16 );
717: Avi_StoreU16 ( pAviFileHeader->AudioStream.Format.ext_size , 0 );
718: }
719: }
720:
721:
722: static bool Avi_BuildIndex ( RECORD_AVI_PARAMS *pAviParams )
723: {
724: AVI_CHUNK Chunk;
725: long IndexChunkPosStart;
726: long Pos , PosWrite;
727: Uint8 TempSize[4];
728: AVI_CHUNK_INDEX ChunkIndex;
729: Uint32 Size;
730:
731: fseek ( pAviParams->FileOut , 0 , SEEK_END ); /* go to the end of the file */
732:
733: /* Write the 'idx1' chunk header */
734: IndexChunkPosStart = ftell ( pAviParams->FileOut );
735: Avi_Store4cc ( Chunk.ChunkName , "idx1" ); /* stream 0, uncompressed DIB bytes */
736: Avi_StoreU32 ( Chunk.ChunkSize , 0 ); /* index size (-> completed later) */
737: if ( fwrite ( &Chunk , sizeof ( Chunk ) , 1 , pAviParams->FileOut ) != 1 )
1.1.1.2 ! root 738: goto index_error;
1.1 root 739: PosWrite = ftell ( pAviParams->FileOut ); /* position to start writing indexes */
740:
741: /* Go to the first data chunk in the 'movi' chunk */
742: fseek ( pAviParams->FileOut , pAviParams->MoviChunkPosStart + sizeof ( AVI_STREAM_LIST_MOVI ) , SEEK_SET );
743: Pos = ftell ( pAviParams->FileOut );
744:
745: /* Build the index : we seek/read one data chunk and seek/write the */
746: /* corresponding infos at the end of the index, until we reach the */
747: /* end of the 'movi' chunk. */
748: while ( Pos < pAviParams->MoviChunkPosEnd )
749: {
750: /* Read the header for this data chunk: ChunkName and ChunkSize */
751: if (fread(&Chunk, sizeof(Chunk), 1, pAviParams->FileOut) != 1)
1.1.1.2 ! root 752: goto index_error;
1.1 root 753: Size = Avi_ReadU32 ( Chunk.ChunkSize );
754:
755: /* Write the index infos for this chunk */
756: fseek ( pAviParams->FileOut , PosWrite , SEEK_SET );
757: Avi_Store4cc ( ChunkIndex.identifier , (char *)Chunk.ChunkName ); /* 00dc, 00db, 01wb, ... */
758: Avi_StoreU32 ( ChunkIndex.flags , AVIIF_KEYFRAME ); /* AVIIF_KEYFRAME */
759: Avi_StoreU32 ( ChunkIndex.offset , Pos - pAviParams->MoviChunkPosStart - 8 ); /* pos relative to 'movi' */
760: Avi_StoreU32 ( ChunkIndex.length , Size );
1.1.1.2 ! root 761: if (fwrite ( &ChunkIndex , sizeof ( ChunkIndex ) , 1 , pAviParams->FileOut ) != 1)
! 762: goto index_error;
1.1 root 763: PosWrite = ftell ( pAviParams->FileOut ); /* position for the next index */
764:
765: /* Go to the next data chunk in the 'movi' chunk */
766: Pos = Pos + sizeof ( Chunk ) + Size; /* position of the next data chunk */
767: fseek ( pAviParams->FileOut , Pos , SEEK_SET );
768: }
769:
770: /* Update the size of the 'idx1' chunk */
771: Avi_StoreU32 ( TempSize , PosWrite - IndexChunkPosStart - 8 );
772: if ( fseek ( pAviParams->FileOut , IndexChunkPosStart+4 , SEEK_SET ) != 0 )
1.1.1.2 ! root 773: goto index_error;
! 774: if ( fwrite ( TempSize , sizeof ( TempSize ) , 1 , pAviParams->FileOut ) != 1 )
! 775: goto index_error;
1.1 root 776: return true;
1.1.1.2 ! root 777:
! 778: index_error:
! 779: perror ( "Avi_BuildIndex" );
! 780: Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to create index header" );
! 781: return false;
! 782:
1.1 root 783: }
784:
785:
786: static bool Avi_StartRecording_WithParams ( RECORD_AVI_PARAMS *pAviParams , char *AviFileName )
787: {
788: AVI_STREAM_LIST_INFO ListInfo;
789: char InfoString[ 100 ];
790: int Len , Len_rounded;
791: AVI_STREAM_LIST_MOVI ListMovi;
792:
793:
794: if ( bRecordingAvi == true ) /* already recording ? */
795: return false;
796:
797: /* Compute some video parameters */
798: pAviParams->Width = pAviParams->Surface->w - pAviParams->CropLeft - pAviParams->CropRight;
799: pAviParams->Height = pAviParams->Surface->h - pAviParams->CropTop - pAviParams->CropBottom;
800: pAviParams->BitCount = 24;
801:
802: #if !HAVE_LIBPNG
803: if ( pAviParams->VideoCodec == AVI_RECORD_VIDEO_CODEC_PNG )
804: {
805: perror ( "AviStartRecording" );
806: Log_AlertDlg ( LOG_ERROR, "AVI recording : Hatari was not built with libpng support" );
807: return false;
808: }
809: #endif
810:
811: /* Open the file */
812: pAviParams->FileOut = fopen ( AviFileName , "wb+" );
813: if ( !pAviParams->FileOut )
814: {
815: perror ( "AviStartRecording" );
816: Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to open file" );
817: return false;
818: }
819:
820: /* Build the AVI header */
821: Avi_BuildFileHeader ( pAviParams , &AviFileHeader );
822:
823: /* Write the AVI header */
824: if ( fwrite ( &AviFileHeader , sizeof ( AviFileHeader ) , 1 , pAviParams->FileOut ) != 1 )
825: {
826: perror ( "AviStartRecording" );
827: Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to write avi header" );
828: return false;
829: }
830:
831: /* Write the INFO header */
832: memset ( InfoString , 0 , sizeof ( InfoString ) );
833: Len = snprintf ( InfoString , sizeof ( InfoString ) , "%s - the Atari ST, STE, TT and Falcon emulator" , PROG_NAME ) + 1;
834: Len_rounded = Len + ( Len % 2 == 0 ? 0 : 1 ); /* round Len to the next multiple of 2 */
835: Avi_Store4cc ( ListInfo.ChunkName , "LIST" );
836: Avi_StoreU32 ( ListInfo.ChunkSize , sizeof ( AVI_STREAM_LIST_INFO ) - 8 + Len_rounded );
837: Avi_Store4cc ( ListInfo.Name , "INFO" );
838: Avi_Store4cc ( ListInfo.Info.ChunkName , "ISFT" );
839: Avi_StoreU32 ( ListInfo.Info.ChunkSize , Len );
840: if ( fwrite ( &ListInfo , sizeof ( ListInfo ) , 1 , pAviParams->FileOut ) != 1 )
841: {
842: perror ( "AviStartRecording" );
843: Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to write info header" );
844: return false;
845: }
846: /* Write the info string + '\0' and write an optionnal extra '\0' byte to get a total multiple of 2 */
847: if ( fwrite ( InfoString , Len_rounded , 1 , pAviParams->FileOut ) != 1 )
848: {
849: perror ( "AviStartRecording" );
850: Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to write info header" );
851: return false;
852: }
853:
854: /* Write the MOVI header */
855: Avi_Store4cc ( ListMovi.ChunkName , "LIST" );
856: Avi_StoreU32 ( ListMovi.ChunkSize , 0 ); /* completed when recording stops */
857: Avi_Store4cc ( ListMovi.Name , "movi" );
858: pAviParams->MoviChunkPosStart = ftell ( pAviParams->FileOut );
859: if ( fwrite ( &ListMovi , sizeof ( ListMovi ) , 1 , pAviParams->FileOut ) != 1 )
860: {
861: perror ( "AviStartRecording" );
862: Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to write movi header" );
863: return false;
864: }
865:
866:
867: /* We're ok to record */
868: Log_AlertDlg ( LOG_INFO, "AVI recording has been started");
869: bRecordingAvi = true;
870:
871: return true;
872: }
873:
874:
875:
876: static bool Avi_StopRecording_WithParams ( RECORD_AVI_PARAMS *pAviParams )
877: {
878: long FileSize;
879: Uint8 TempSize[4];
880:
881:
882: if ( bRecordingAvi == false ) /* no recording ? */
883: return true;
884:
885: /* Update the size of the 'movi' chunk */
886: fseek ( pAviParams->FileOut , 0 , SEEK_END ); /* go to the end of the 'movi' chunk */
887: pAviParams->MoviChunkPosEnd = ftell ( pAviParams->FileOut );
888: Avi_StoreU32 ( TempSize , pAviParams->MoviChunkPosEnd - pAviParams->MoviChunkPosStart - 8 );
889:
890: if ( fseek ( pAviParams->FileOut , pAviParams->MoviChunkPosStart+4 , SEEK_SET ) != 0 )
891: {
892: perror ( "AviStopRecording" );
893: Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to update movi header" );
894: return false;
895: }
896: if ( fwrite ( TempSize , sizeof ( TempSize ) , 1 , pAviParams->FileOut ) != 1 )
897: {
898: perror ( "AviStopRecording" );
899: Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to update movi header" );
900: return false;
901: }
902:
903: /* Build the index chunk */
904: if ( ! Avi_BuildIndex ( pAviParams ) )
905: {
906: perror ( "AviStopRecording" );
907: Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to build index" );
908: return false;
909: }
910:
911: /* Update the avi header (file size, number of output frames, ...) */
912: fseek ( pAviParams->FileOut , 0 , SEEK_END ); /* go to the end of the file */
913: FileSize = ftell ( pAviParams->FileOut );
914:
915: Avi_StoreU32 ( AviFileHeader.RiffHeader.filesize , FileSize - 8 ); /* 32 bits, limited to 4GB */
916: Avi_StoreU32 ( AviFileHeader.AviHeader.Header.total_frames , pAviParams->TotalVideoFrames ); /* number of video frames */
917: Avi_StoreU32 ( AviFileHeader.VideoStream.Header.data_length , pAviParams->TotalVideoFrames ); /* number of video frames */
918: Avi_StoreU32 ( AviFileHeader.AudioStream.Header.data_length , pAviParams->TotalAudioSamples ); /* number of audio samples */
919:
920: if ( fseek ( pAviParams->FileOut , 0 , SEEK_SET ) != 0 )
921: {
922: perror ( "AviStopRecording" );
923: Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to update avi header" );
924: return false;
925: }
926: if ( fwrite ( &AviFileHeader , sizeof ( AviFileHeader ) , 1 , pAviParams->FileOut ) != 1 )
927: {
928: perror ( "AviStopRecording" );
929: Log_AlertDlg ( LOG_ERROR, "AVI recording : failed to update avi header" );
930: return false;
931: }
932:
933:
934: /* Close the file */
935: fclose ( pAviParams->FileOut );
936:
937: Log_AlertDlg ( LOG_INFO, "AVI recording has been stopped");
938: bRecordingAvi = false;
939:
940: return true;
941: }
942:
943:
944:
945:
946: /*-----------------------------------------------------------------------*/
947: /**
948: * Are we recording an AVI ?
949: */
950: bool Avi_AreWeRecording ( void )
951: {
952: return bRecordingAvi;
953: }
954:
955:
956:
1.1.1.2 ! root 957: bool Avi_StartRecording ( char *FileName , bool CropGui , Uint32 Fps , Uint32 Fps_scale , int VideoCodec )
1.1 root 958: {
959: memset ( &AviParams , 0 , sizeof ( AviParams ) );
960:
961: AviParams.VideoCodec = VideoCodec;
962: AviParams.VideoCodecCompressionLevel = 9; /* png compression level */
963: AviParams.AudioCodec = AVI_RECORD_AUDIO_CODEC_PCM;
964: AviParams.AudioFreq = ConfigureParams.Sound.nPlaybackFreq;
965: AviParams.Surface = sdlscrn;
1.1.1.2 ! root 966:
! 967: /* Some video players (quicktime, ...) don't support a value of Fps_scale */
! 968: /* above 100000. So we decrease the precision from << 24 to << 16 for Fps and Fps_scale */
! 969: AviParams.Fps = Fps >> 8; /* refresh rate << 16 */
! 970: AviParams.Fps_scale = Fps_scale >> 8; /* 1 << 16 */
1.1 root 971:
972: if ( !CropGui ) /* Keep gui's status bar */
973: {
974: AviParams.CropLeft = 0;
975: AviParams.CropRight = 0;
976: AviParams.CropTop = 0;
977: AviParams.CropBottom = 0;
978: }
979: else /* Record only the content of the Atari's screen */
980: {
981: AviParams.CropLeft = 0;
982: AviParams.CropRight = 0;
983: AviParams.CropTop = 0;
984: AviParams.CropBottom = Statusbar_GetHeight();
985: }
986:
1.1.1.2 ! root 987: if (Avi_StartRecording_WithParams ( &AviParams , FileName ))
! 988: {
! 989: Main_SetTitle("00:00");
! 990: return true;
! 991: }
! 992: return false;
1.1 root 993: }
994:
995:
996: bool Avi_StopRecording ( void )
997: {
1.1.1.2 ! root 998: if (Avi_StopRecording_WithParams ( &AviParams ))
! 999: {
! 1000: Main_SetTitle(NULL);
! 1001: return true;
! 1002: }
! 1003: return false;
1.1 root 1004: }
1005:
1006:
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.